Skip to content
Advertisement

JS: play multiple audio sources simultaneously when loaded

I have a web project (vanilla HTML/CSS/JS only) with three audio sources. The idea is for all three to play simultaneously, but I noticed on mobile that the files were playing out of sync (i.e. one source would start, then a few ms later the second would start, then the third). I believe they are playing due to the individual files playing as soon as they’re loaded, so I would like to request that once all files have loaded that the play() method is called on all three at the same time,

What would be the best way to achieve this using vanilla JS?

Example: https://jacksorjacksor.xyz/soundblocks/

Repo: https://github.com/jacksorjacksor/jacksorjacksor/tree/master/soundblocks

TIA!

Rich

Advertisement

Answer

MediaElements are meant for normal playback of media and aren’t optimized enough to get low latency. The best is to use the Web Audio API, and AudioBuffers.

You will first fetch the files’s data in memory, then decode the audio data from these, and once all the audio data has been decoded, you’ll be able to schedule playing all at the same precise moment:

(async() => {
  const urls = [ "layer1_big.mp3", "layer2_big.mp3", "layer3_big.mp3" ]
    .map( (url) => "https://cdn.jsdelivr.net/gh/jacksorjacksor/jacksorjacksor/soundblocks/audio/" + url );
  // first, fetch the files's data
  const data_buffers = await Promise.all(
    urls.map( (url) => fetch( url ).then( (res) => res.arrayBuffer() ) )
  );
  // get our AudioContext
  const context = new (window.AudioContext || window.webkitAduioContext)();
  // decode the data
  const audio_buffers = await Promise.all(
    data_buffers.map( (buf) => context.decodeAudioData( buf ) )
  );
  // to enable the AudioContext we need to handle an user-gesture
  const btn = document.querySelector( "button" );
  btn.onclick = (evt) => {
    const current_time = context.currentTime;
    audio_buffers.forEach( (buf) => {
      // a buffer source is a really small object
      // don't be afraid of creating and throwing it
      const source = context.createBufferSource();
      // we only connect the decoded data, it's not copied
      source.buffer = buf;
      // in order to make some noise
      source.connect( context.destination );
      // make it loop?
      //source.loop = true;
      // start them all 0.5s after we began, so we're sure they're in sync
      source.start( current_time + 0.5 );
    } );
  };
  btn.disabled = false;
})();
<button disabled>play</button>
Advertisement