2

I am trying to get the total duration of audio from an array of audio paths.

Here is what the array would look like:

var sound_paths = ["1.mp3","2.mp3",...]

I have looked at this post, which helped: how to get audio.duration value by a function

However, I do not know how to implement this over an array. The idea is that I want to loop over each audio file, get its duration, and add it to a "sum_duration" variable.

I cannot seem to figure out a way to do this over a for loop. I have tried Promises (which I am admittedly new at): (Note the functions come from a class)

getDuration(src,cb){
    // takes a source audio file and a callback function
    var audio = new Audio();
    audio.addEventListener("loadedmetadata",()=>{
        cb(audio.duration);
    });
    audio.src = src;
}

getAudioArrayDuration(aud_path_arr){
    // takes in an array of audio paths, finds the total audio duration
    // in seconds

    return new Promise((resolve)=>{
        var duration = 0;

        for(const aud_path in aud_path_arr){
            var audio = new Audio();
            audio.src = aud_path;
            audio.onloadedmetadata = ()=>{
                console.log(audio.duration);
                duration += audio.duration;
            }
            resolve(duration);
        }

    });

}

However, this obviously does not work, and will just return the duration value of 0.

How can I loop over audio files and return the total audio duration of each file summed?

Steak
  • 514
  • 3
  • 15

2 Answers2

2

I think, in this case, working with promises is the right approach, time to get used to them ;) Try to remember, a promise will fullfil your wish in the near future. What makes your problem harder is that you have an array of files to check, each will need to separately be covered by it's own Promise, just so that your program can know when all of them have been fullfilled.

I always call my 'promised' getters 'fetch...', that way I know it'll return a promise instead of a direct value.

function fetchDuration(path) {
  return new Promise((resolve) => {
    const audio = new Audio();
    audio.src = path;
    audio.addEventListener(
      'loadedmetadata',
      () => {
        // To keep a promise maintainable, only do 1
        // asynchronous activity for each promise you make
        resolve(audio.duration)
      },
    );
  })
}

function fetchTotalDuration(paths) {
  // Create an array of promises and wait until all have completed
  return Promise.all(paths.map((path) => fetchDuration(path)))
    // Reduce the results back to a single value
    .then((durations) => durations.reduce(
      (acc, duration) => acc + duration,
      0,
    ))
  ;
}

At some point, your code is going to have to deal with this asynchronous stuff, I honestly believe that Promises are the easiest way to do this. It takes a little getting used to, but it'll be worth it in the end. The above could be used in your code something along the lines of:

window.addEventListener('DOMContentLoaded', () => {
  fetchTotalDuration(["1.mp3","2.mp3",...])
    .then((totalDuration) => {
      document.querySelector('.player__total-duration').innerHTML = totalDuration;
    })
  ;
});
Constantin Groß
  • 10,719
  • 4
  • 24
  • 50
Windgazer
  • 161
  • 1
  • 6
  • Great! Is there a reason why you do not do "const audio = new Audio(path); " instead of changing the src of the audio after creation? – Steak Jul 28 '21 at 13:00
  • 1
    Hey @Steak, I never thought about it, just went with the existing solution and improved how it dealt with the promises. (Sorry for the late reply) – Windgazer Aug 11 '21 at 09:30
  • Thanks! No worries! – Steak Aug 11 '21 at 11:48
1

I hacked this together real quick, so you'll have to adapt it to your function structure, but it's a working code snippet that should send you in the right direction.

Simply keep track of which audio files have been loaded, and if that matches the number of audio files queried, you call the callback with the total duration.

You should also take failing requests into account, so that if the loadedmetadata event never fires, you can react accordingly (by either falling back to 0 duration for that file, or throwing an Exception, etc.).

const cb = function(duration) {
    console.log(`Total duration: ${duration}`);
};

let sound_paths = ["https://rawcdn.githack.com/anars/blank-audio/92f06aaa1f1f4cae365af4a256b04cf9014de564/5-seconds-of-silence.mp3","https://rawcdn.githack.com/anars/blank-audio/92f06aaa1f1f4cae365af4a256b04cf9014de564/2-seconds-of-silence.mp3"];
let totalDuration = 0;
let loadedSounds = [];
sound_paths.map(src => {
    const audio = new Audio();
    audio.addEventListener("loadedmetadata", ()=>{
        totalDuration += audio.duration;
        loadedSounds.push(audio);
        if ( loadedSounds.length === sound_paths.length ) {
            cb( totalDuration );
        }
    });
    audio.src = src;
});
Constantin Groß
  • 10,719
  • 4
  • 24
  • 50