Web Audio API - Playing synchronized sounds
You could try applying an offset:
function play(audioBuffer, startTime) {
const source = context.createBufferSource();
source.buffer = audioBuffer;
source.connect(context.destination);
source.start(startTime);
}
const startTime = context.currentTime + 1.0; // one second in the future
for (let i = 0; i < arrayOfAudioBuffers.length; i++) {
play(arrayOfAudioBuffers[i], startTime);
}
This code will queue up all sounds to play at the same time, one second in the future. If this works, you can tune down the delay to make the sounds play more immediately, or even calculate the right delay based on the number of tracks (e.g. 2 ms per track * 30 tracks = 60 ms delay)
Due to browsers protecting against fingerprinting and timing attacks, timing precision under the hood can be reduced or rounded by modern browsers.
This would mean source.start(offset)
could never be 100% accurate or reliable in your case.
What I recommend is mixing down the sources byte by byte then playing back the final mix.
Assuming all audio sources should start at the same time, and time till load is flexible the following will work:
Example:
const audioBuffer1 = '...'; // Some decoded audio buffer
const audioBuffer2 = '...'; // some other decoded audio buffer
const audioBuffer3 = '...'; // and another audio buffer
const arrayOfAudioBuffers = [audioBuffer1, audioBuffer2, audioBuffer3];
We'll need to calculate the length of the entire song by obtaining the buffer with the maximum length.
let songLength = 0;
for(let track of arrayOfAudioBuffers){
if(track.length > songLength){
songLength = track.length;
}
}
Next i've created a method that will take in arrayOfAudioBuffers
and output a
final mixdown.
function mixDown(bufferList, totalLength, numberOfChannels = 2){
//create a buffer using the totalLength and sampleRate of the first buffer node
let finalMix = context.createBuffer(numberOfChannels, totalLength, bufferList[0].sampleRate);
//first loop for buffer list
for(let i = 0; i < bufferList.length; i++){
// second loop for each channel ie. left and right
for(let channel = 0; channel < numberOfChannels; channel++){
//here we get a reference to the final mix buffer data
let buffer = finalMix.getChannelData(channel);
//last is loop for updating/summing the track buffer with the final mix buffer
for(let j = 0; j < bufferList[i].length; j++){
buffer[j] += bufferList[i].getChannelData(channel)[j];
}
}
}
return finalMix;
}
fyi: you can always remove one loop by hard coding the update per each channel.
Now we can use our mixDown
function like so:
const mix = context.createBufferSource();
//call our function here
mix.buffer = mixDown(arrayOfAudioBuffers, songLength, 2);
mix.connect(context.destination);
//will playback the entire mixdown
mix.start()
More about web audio precision timing here
Note:
We could use OfflineAudioContext
to accomplish the same thing but precision is not guaranteed and
still relies on looping and calling start()
on each individual source.
Hope this helps.