Generating sound in modern Web Audio API
Tags: webaudio, javascript, retrocomputing
Published: 2023-06-09 05:45:00

As part of my effort to [remake my old Artsy demo](https://atornblad.github.io/artsy/) in more modern JavaScript, I decided to start with the music player. The original Amiga demo from 1993 used the [SoundTracker MOD format](https://en.wikipedia.org/wiki/Module_file), but my remake from 2013 uses an MP3 file, which was a bit of a cheat. Playing an MP3 is efficient and requires almost zero code, but it's not very interesting. Also, it requires loading several megabytes of data. So I decided to try to make a MOD player from scratch, using only the [Web Audio API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API)

## The current state of Web Audio

The Web Audio API has gone through a lot of changes since it was first introduced in 2011. Originally, it was quite messy, and required a lot of boilerplate code to get anything done. The API has since been cleaned up, and now it's quite easy to use. Unfortunately, there are still a lot of blogs and tutorials out there that show old, deprecated code. For example, the [ScriptProcessorNode](https://developer.mozilla.org/en-US/docs/Web/API/ScriptProcessorNode), which let you generate audio data procedurally, has been deprecated since 2014, but it's still used in many examples. Today, the [AudioWorklet](https://developer.mozilla.org/en-US/docs/Web/API/AudioWorklet) interface is the recommended way to generate sound in JavaScript.

## Sine of times

The first step of creating a MOD player is to make some noise. One way to do this is to generate a sine wave. A sine wave is the simplest waveform, and requires very little code to produce. I will create an audio worklet that produces a sine wave of a constant frequency.

The first step is to create an [AudioContext](https://developer.mozilla.org/en-US/docs/Web/API/AudioContext), which is the main interface for the Web Audio API. I also need to load an [AudioWorklet](https://developer.mozilla.org/en-US/docs/Web/API/AudioWorklet) _(that I'll create later)_, connect it to the context, and send a message to the worklet to start generating sound. The context is not allowed to play any sound until the user has interacted with the page, so I need to wait for a `click` event before resuming the context.

```javascript
// Create the audio context
const audio = new AudioContext();

// Load an audio worklet
await audio.audioWorklet.addModule('player-worklet.js');

// Create a player
const player = new AudioWorkletNode(audio, 'player-worklet');

// Connect the player to the audio context
player.connect(audio.destination);

// Start the player when the user clicks
window.addEventListener('click', () => {
    audio.resume();
    player.port.postMessage({
        type: 'play'
    });
});
```

Because the script above calls an asynchronous function, `audio.audioWorklet.addModule`, I need to load it from the HTML page as an [ES6 module](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules). If you are not writing frontend JavaScript as an ES6 module yet, you really should start doing it.

```html
<!DOCTYPE html>
<html>
    <head>
        <title>JS Mod Player by Anders Marzi Tornblad</title>
    </head>
    <body>
        <script type="module" src="player.js"></script>
    </body>
</html>
```

Creating the worklet code is a bit more involved, but it's not too complicated. I start by creating a class that extends [AudioWorkletProcessor](https://developer.mozilla.org/en-US/docs/Web/API/AudioWorkletProcessor), which is the base class for all audio worklets.

The worklet runs in a separate thread, so it can't access any variables in the main script. Because of that, each worklet instance has a `port` property, which is a [MessagePort](https://developer.mozilla.org/en-US/docs/Web/API/MessagePort) that is used to communicate with the main script. The worklet can send messages to the main script using `port.postMessage`, and it can receive messages from the main script using `port.onmessage`. I normally put some code in the constructor to connect the `port.onmessage` event handler to an `onmessage` method in the worklet class.

The [process method](https://developer.mozilla.org/en-US/docs/Web/API/AudioWorkletProcessor/process) is what the Web Audio framework calls to generate sound. It gets called repeatedly, and it's supposed to fill the `outputs` array with audio data. The method receives both inputs and outputs, and can be used anywhere in an audio graph, for example to add effects to sound. Because I'm generating sound and not manipulating sound, I don't need to use the inputs, so I just ignore the inputs. The `outputs` array is a two-dimensional array, where the first dimension is the channel, and the second dimension is the sample. Each sample value is a floating point number between -1 and 1. The `process` method must return `true` to indicate that it has filled the `outputs` array with data, and that the Web Audio framework should call it again. If the method returns `false`, it is a signal to the Web Audio framework that the worklet is done generating sound, and it should stop calling the `process` method.

```javascript
class PlayerWorklet extends AudioWorkletProcessor {
    constructor() {
        super();
        this.port.onmessage = this.onmessage.bind(this);
        this.playing = false;
    }

    onmessage(e) {
        if (e.data.type === 'play') {
            // Toggle between playing and silence
            this.playing = !this.playing;
            this.phase = 0;
        }
    }

    process(inputs, outputs) {
        const output = outputs[0];
        const channel = output[0];

        for (let i = 0; i < channel.length; ++i) {
            if (this.playing) {
                channel[i] = Math.sin(this.phase);
                this.phase += 0.1;
            } else {
                channel[i] = 0;
            }
        }

        return true;
    }
}

registerProcessor('player-worklet', PlayerWorklet);
```

## Conclusion

That's it! The code above is a very minimal example, but it should give you an idea of how to create a worklet. The next step is to load a MOD file into the worklet, and use it to generate sound.

You can try this solution at [atornblad.github.io/js-mod-player](https://atornblad.github.io/js-mod-player).

The latest version of the code is always available in [the GitHub repository](https://github.com/atornblad/js-mod-player).