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.