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 each file’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 each file'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.webkitAudioContext)(); // decode the data const audio_buffers = await Promise.all( data_buffers.map( (buf) => context.decodeAudioData( buf ) ) ); // to enable the AudioContext we need to handle a 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>