0

I’m trying to get the duration in seconds of every object in a list but I’m having some trouble in my results. I think it’s because I’m not fully understanding asynchronous behavior.

I start my function with an array songs[] of n number of objects. By the end of my function, the goal is to have an array songLengths where the first value is the duration of the first object in songs[], and so on.

I’m trying to model my function after this example: JavaScript closure inside loops – simple practical example. But I’m getting undefined values for each songLengths[] index.

$("#file").change(function(e) {
  var songs = e.currentTarget.files;
  var length = songs.length;
  var songLengths = [];

  function createfunc(i) {
    return function() {
      console.log("my val = " + i);
    };
  }

  for (var i = 0; i < length; i++) {
    var seconds = 0;
    var filename = songs[i].name;
    var objectURL = URL.createObjectURL(songs[i]);
    var mySound = new Audio([objectURL]);

    mySound.addEventListener(
      "canplaythrough",
      function(index) {
        seconds = index.currentTarget.duration;
      },
      false,
    );
    songLengths[i] = createfunc(i);
  }
});
AKX
  • 152,115
  • 15
  • 115
  • 172
Martin
  • 1,336
  • 4
  • 32
  • 69
  • Have you defined `length` somewhere? Can you provide an example of what `songs` contains? You never save the length, just the song index. – Sebastian Simon Aug 11 '18 at 07:13
  • hello @Xufox thank you for the comment, songs[] contains a list of audio files the user uploads. When I upload 5 songs for example and print out songs[], this is what is looks like in console: https://i.imgur.com/xPP2k1L.png length is defined (with var length = songs.length; I'll edit the post to contain it) and it correctly gets the number of files uploaded. So i can correctly iterate through the list and get time in seconds, but I think I didn't do it quite asynchronously as sometiems a song has the duration of a different song – Martin Aug 11 '18 at 17:38

1 Answers1

1

Something like this should work without a mess of closures, and with added clean asynchronicity thanks to promises.

Basically we declare a helper function, computeLength, which takes a HTML5 File and does the magic you already did to compute its length. (Though I did add the URL.revokeObjectURL call to avoid memory leaks.)

Instead of just returning the duration though, the promise resolves with an object containing the original file object and the duration calculated.

Then in the event handler we map over the files selected to create computeLength promises out of them, and use Promise.all to wait for all of them, then log the resulting array of [{file, duration}, {file, duration}, ...].

function computeLength(file) {
  return new Promise((resolve) => {
    var objectURL = URL.createObjectURL(file);
    var mySound = new Audio([objectURL]);
    mySound.addEventListener(
      "canplaythrough",
      () => {
        URL.revokeObjectURL(objectURL);
        resolve({
          file,
          duration: mySound.duration
        });
      },
      false,
    );
  });  
}

$("#file").change(function(e) {
  var files = Array.from(e.currentTarget.files);
  Promise.all(files.map(computeLength)).then((songs) => {
    console.log(songs);
  });
});
AKX
  • 152,115
  • 15
  • 115
  • 172
  • hello, thank you so much for the reply! I tried this code out but think something is going wrong on my end with files.map, I put the code in this jsfiddle https://jsfiddle.net/Loxd1knm/5/ and if you run it while viewing console it says Uncaught TypeError: files.map is not a function, I also added var songs = []; to the start of the $("#file') function – Martin Aug 11 '18 at 18:10
  • Ah, my bad! `var files = ...` needs to be `var files = Array.from(...)` for `map` to work! – AKX Aug 11 '18 at 18:13
  • yes this works perfectly! consistently correct duration, thank you! – Martin Aug 11 '18 at 18:29