Search code examples
javascriptreactjsweb-audio-api

State Management with React and Web Audio


I'm trying to write a module-based music application using the Web Audio API and React.

Up until this point, I've been creating web audio nodes such as Filters, Oscillators etc. as the React components are created:

class Filter extends React.Component {
    constructor(props) {
        super(props);
        const filter = props.context.createBiquadFilter();
        this.state = { filter };
    }
...

Later on, this means it's possible to alter the filter as part of the component's state:

    frequencyChange(event) {
        ...
        this.state.filter.frequency.setValueAtTime(range.value, context.currentTime);
    }

and in the render method:

<input type="range" className="slider" min={20} max={20000}
                   onChange={event => this.frequencyChange(event)}/>

However, when it comes to chaining filters together, I need to be able to disconnect() the Web Audio filter node I've created from the parent class of this component, and connect() it to a new destination.

Let's say the parent component is called EffectsBox, and is intended to render an arbitrary list of effects like Filter.

  • How should it render all these components, as well as allowing them to expose some common API for chaining audio together?

  • Where should the state – i.e. the web audio components – for these effects be stored?

Here's some example code to hopefully make my question clearer:

class EffectsBox extends React.Component {
    ...

    addNewEffect(effect) {
        // disconnect() the previous effect and connect() it to a new one 
    }

    render() {
        // Maybe some kind of "map" here between effects and react components?
        return <div>{ this.state.effects }</div>
    }

N.B. – I've read about React's refs API, but it doesn't seem suitable for use with an arbitrary number of child classes – very happy to be shown otherwise though!


Solution

  • React doesn't like it when you edit values in a component state without using setState. I would avoid putting the whole audio node in state, or even in the component at all.

    I had at one point tried adding the audio node as a part of the class in the constructor, so I could access things as this.filter. I then had to use refs everywhere and pass the audio context to everything to access methods I needed to connect nodes and set values. It made the noises I wanted, but I did not like how tightly coupled and verbose my files were.

    I refactored everything. There is one file now that uses the audio context that manages all the audio nodes. It has a few methods exposed I can use to update values as needed. I also have a map of string names to nodes, so I set values like this: setValue('filter', 'frequency', newValue). There are a few cases that need to be handled separately (i.e. distortion curve), but on the whole it's easier for me to read. This abstraction also means that your audio nodes are not sensitive to changes in component hierarchies.

    This is what I recommend. Pull your audio nodes out of your React classes. It's much simpler to manage the code that manages the nodes.