javascriptaudioweb-audio-apiwebapiaudiobuffer

How to continuously generate raw audio samples in javascript using the web audio API?


For a music app, I need to be able to continuously and seamlessly generate raw audio samples using the web audio API. After searching, I found out about the AudioBuffer(https://developer.mozilla.org/en-US/docs/Web/API/AudioBuffer), and it seems that that's what I need. However, the audio buffer can only be played once(https://developer.mozilla.org/en-US/docs/Web/API/AudioBufferSourceNode), so it can't really play continuously. I tried this workaround:

                    const buffer = audioCtx.createBuffer(1, 10000, audioCtx.sampleRate);
                    for(let i = 0; i < buffer.length; i++)
                        buffer.getChannelData(0)[i] = ((i / audioCtx.sampleRate) * 440) % 1;
                    
                    const source = audioCtx.createBufferSource();
                    source.buffer = buffer;
                    source.onended = function() {
                        console.log(this);
                        const newSource = audioCtx.createBufferSource();
                        for(let i = 0; i < buffer.length; i++)
                            buffer.getChannelData(0)[i] = ((i / audioCtx.sampleRate) * 440) % 1;
                        newSource.buffer = buffer;
                        newSource.connect(audioCtx.destination);
                        newSource.onended = (this.onended as Function).bind(newSource);
                        newSource.start();
                    }
                    source.connect(audioCtx.destination);
                    source.start();

In essence, this code creates a buffer & source node, plays the buffer, and when the buffer ends it creates a new source and buffer and continues playing. However, this method produces noticeable silences when buffer finishes playing. I assume this has something to do with the JS event loop, but I'm not sure.

Ideally, I would want something like this:

audioCtx.createSampleStream(() => {
    // generate samples here.
    return Math.random() * 2 - 1;
})

Hopefully I will be able to get this to work. If I don't, I will probably try writing a npm package with c++ bindings to do this.


Solution

  • I think the API you are looking for is the AudioWorklet. It's a way to run your code directly on the audio thread. It allows you to fill the buffers right before they get played.

    Setting it up is normally a bit complicated since your processor needs to be defined in a separate JavaScript file. But it's also possible to use a Blob as shown below.

    The example is based on your snippet which generates random samples.

    const blob = new Blob(
        [`
            class MyProcessor extends AudioWorkletProcessor {
                process(_, outputs) {
                    for (const output of outputs) {
                        for (const channelData of output) {
                            for (let i = 0; i < channelData.length; i += 1) {
                                channelData[i] = Math.random() * 2 - 1;
                            }
                        }
                    }
    
                    return true;
                }
            }
    
            registerProcessor('my-processor', MyProcessor);
        `],
        { type: 'application/javascript' }
    );
    const url = URL.createObjectURL(blob);
    const audioContext = new AudioContext();
    
    await audioContext.audioWorklet.addModule(url);
    
    const myAudioWorkletNode = new AudioWorkletNode(audioContext, 'my-processor');
    
    myAudioWorkletNode.connect(audioContext.destination);