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

1.2k views Asked by At

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.

1

There are 1 answers

4
chrisguttandin On BEST ANSWER

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);