Unlikenesses A PHP Developer

react sequencer

05 July, 2017

As an exercise I'm going to build a simple sequencer in React. You can view it in action here. The final source code is on Github.

I'll start with create-react-app, which provides the necessary initial bootstrapping so we can get going quickly. If you don't already have it installed, run

npm install -g create-react-app

Then to create a new app:

create-react-app sequencer

When that's finished, run npm start to open the base project in the browser.

The first thing we need to do is consider the structure of the app. At the most basic level we're going to want a controls component and an area to contain the pads. So open up src/App.js and replace it with this skeleton structure:

import React, { Component } from 'react';
import './App.css';
import Pads from './components/Pads';
import Controls from './components/Controls';

class App extends Component {
  render() {
    return (
        <div className="App">
            <Controls />
            <Pads />
        </div>
    );
  }
}

export default App;

All we've done is import two non-existent components, Pads and Controls and inserted them in the App component. For now create a components folder in src and then create two empty components:

Pad

import React from 'react';

class Pads extends React.Component {
    constructor() {
        super();
    }

    render() {
        return (
            <div>
                Pads
            </div>
        );
    }
}

export default Pads;

Controls

import React from 'react';

const Controls = (props) => {
    return (
        <div className="controls">
            Controls
        </div>          
    );
}

export default Controls;

Let's look at building out the Pads component first. We're going to want 8 rows, each row having 8 pads. Because we want to keep track of the state of each pad (on or off), we need to define a state object in the constructor of the Pads component:

this.state = {
    pads: [
        [0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0]
    ]
}

I'm just using an array of 8 rows, each row containing 8 elements which will be 0 or 1 depending on their state. We can use the map function to display these in the render function:

return (
    <div className="pads">
        {this.state.pads.map((row, rowIndex) => {
            return (
                <div className="row" key={rowIndex}>a
                    {row.map((pad, index) => {
                        return <Pad key={index} />
                    })}
                </div>
            )
        })}
    </div>
);

This iterates over each row in the pads state, and for each row returns a div, in which we map again over each element in that row, for each element returning a Pad component. We have to pass the key attribute so that React can identify which elements have changed at any given point.

We need to quickly create this Pad component in components/Pad.js:

import React from 'react';

const Pad = (props) => (
    <div className="pad"></div>
);

export default Pad;

And let's add some styling: in src/App.css delete everything and add these styles:

.row {
  clear: both;
}

.pad {
  width: 50px;
  height: 50px;
  float: left;
  margin: 0 5px 5px 0;
  cursor: pointer;
  background: grey;
}

The next step is to allow the active state of a pad to be changed when you click on it. In Pads create a new function to toggle a pad's state - we'll just console.log the output for now:

toggleActive(rowIndex, id) {
    console.log('Changed', rowIndex, id);
}

This will take the index of a row and the index of a pad within that row. We'll also need to bind toggleActive to this so we can reference the state later. So in the constructor put this line:

this.toggleActive = this.toggleActive.bind(this);

We then need to pass it - along with rowIndex, the pad's index, and the pad's active state - as properties to the Pad component, so modify Pads like so:

return <Pad 
        key={index} 
        rowIndex={rowIndex} 
        id={index} 
        state={pad}
        toggleActive={this.toggleActive} />

Then in Pad we just need to add an onClick event that calls the toggleActive method in its parent component, passing its rowIndex and id:

return (
    <div 
        className="pad"
        onClick={() => props.toggleActive(props.rowIndex, props.id)}>
    </div>
);

Now when you click on a pad you should see its coordinates in the console. Now we need to find the appropriate element in the state and toggle its value between 0 and 1. In the toggleActive function in Pads first we make a copy of the pads state and get the current value of the pad that's been clicked:

let pads = [...this.state.pads];
let padActive = pads[rowIndex][id];

We're using the spread operator to make a clone of the pads array, then getting the clicked pad's current active state (0 or 1). Then we just need to toggle it and update the state:

toggleActive(rowIndex, id) {
    let pads = [...this.state.pads];
    let padState = pads[rowIndex][id];
    if (padState === 1) {
        pads[rowIndex][id] = 0;
    } else {
        pads[rowIndex][id] = 1;
    }
    this.setState({ pads: pads });
}

We now need to conditionally add an active class to the Pad component depending on whether its value is 0 or 1:

<div 
    className={"pad " + (props.state === 1 ? 'active' : '')}
    onClick={() => props.toggleActive(props.rowIndex, props.id)}>
</div>

Finally, add a quick active style to App.css so we can see it in action:

.pad.active {
    background: green;
}

Now when you click on a pad, it should turn green, and go back to grey when you click on it again.

Onto the controls... We'll need a piece of state that keeps track of whether the sequencer is playing or not. This needs to go in the root App component, so add a constructor and put it there. We'll also put a pos variable there to keep track of our position in the grid when the sequencer's playing, and a bpm value for when we set up our timer:

constructor() {
    super();
    this.state = {
        playing: false,
        pos: 0,
        bpm: 220
    }
    this.togglePlaying = this.togglePlaying.bind(this);
}

I've added a binding for the next method we'll need, which will toggle between playing and not-playing. Add this to App.js:

togglePlaying() {
    if (this.state.playing) {
        this.setState({ playing: false });
    } else {
        this.setState({ playing: true });
    }
}

This grabs the current state, and updates it with its opposite. Pass this method as a property to the Controls component, along with the playing state:

<Controls playing={this.state.playing} togglePlaying={this.togglePlaying} />

Then pick it up in Controls.js. I'm adding a line to set the button text according to whether the playing state is true or false:

let buttonText = props.playing ? 'Stop' : 'Play';
return (
    <div className="controls">
        <button onClick={() => props.togglePlaying()}>{buttonText}</button>
    </div>          
);

We now need to be able to set and clear a timer that will call a tick method every x seconds, where x is calculated from our bpm. Let's start with the set method, remaining in App.js:

setTimer() {
    this.timerId = setInterval(() => this.tick(), this.calculateTempo(this.state.bpm));
}

This uses Javascript's setInterval method to call tick() on a regular basis. We store it in a timerId variable so we can clear it when the user clicks Stop. calculateTempo() is pretty simple:

calculateTempo(bpm) {
    return 60000 / bpm;
}

Our tick method will increment the current pos and reset it to 0 if it reaches 7:

tick() {
    let pos = this.state.pos;
    pos++;
    if (pos > 7) {
        pos = 0;
    }
    this.setState({ pos: pos });
    console.log(pos);
}

Now hook this all up to the togglePlaying method:

togglePlaying() {
    if (this.state.playing) {
        clearInterval(this.timerId);
        this.setState({ playing: false });
    } else {
        this.setTimer();
        this.setState({ playing: true });
    }
}

Notice that now if the Play/Stop button is clicked and we are currently playing, we call clearInterval on the timer, which will stop it. If we are not currently playing we start the timer. If you run this now you should see the pos variable being logged to the console when you click Play. But we want the actual pads to be highlighted when their position corresponds to the root pos state. To do this we need to pass the pos state down, via the Pads component, to the Pad component. In App.js:

<Pads pos={this.state.pos} />

Then in Pads.js add the pos to pass down to Pad:

return <Pad 
        key={index} 
        rowIndex={rowIndex} 
        id={index} 
        state={pad}
        pos={this.props.pos}
        toggleActive={this.toggleActive} />

Finally in Pad we can add another class called playing depending on whether pos corresponds to the pad's position in the grid:

<div 
    className={"pad " + (props.state === 1 ? 'active' : '') + (props.pos === props.id ? ' playing' : '')}
    onClick={() => props.toggleActive(props.rowIndex, props.id)}>
</div>

And add that class to App.css, as well as a state for when a pad is both active and playing:

.pad.playing {
    background: silver;
}
.pad.active.playing {
    background: lime;
}

Now when you click Play the current column will be highlighted.

Our next task is to check, each time pos advances, whether or not any pads at that position in the row are active. We can add a method checkPad() and call it in tick(). Put this at the end of the tick method:

this.checkPad();

Now we're ready to create the function:

checkPad() {
        
}

But here we encounter a problem. Ideally we'd like to iterate over the pads state to check which pads are active - but we've put the pads state in a child component (Pads), so we can't access it from its parent. The solution is to follow the React best practices and lift the state up. This means shifting the "source of truth", when it comes to pads, from the Pads component to its parent, App. So remove the entire pads state from the Pads component, and add it to the App state:

this.state = {
    pads: [
        [0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0]
    ],
    playing: false,
    pos: 0,
    bpm: 220
}

We then pass it back down to Pads:

<Pads pos={this.state.pos} pads={this.state.pads} />

Since Pads no longer has state we can make it a stateless functional component by removing the class declaration and replacing it with

const Pads = (props) => (

We also need to remove the render() method and the return statement, and replace all references to this.props with props. See the source on Github if this isn't clear.

Now in Pads change

{this.state.pads.map((row, rowIndex) => {

to

{props.pads.map((row, rowIndex) => {

Now we've moved the pads state the toggleActive method in Pads won't work. Take it and move it to App, then add

this.toggleActive = this.toggleActive.bind(this);

to App's constructor. We then need to pass it back down as a prop to Pads:

<Pads pos={this.state.pos} pads={this.state.pads} toggleActive={this.toggleActive} />

and modify Pads to call it:

return <Pad 
    key={index} 
    rowIndex={rowIndex} 
    id={index} 
    state={pad}
    pos={props.pos}
    toggleActive={() => props.toggleActive(rowIndex, index)} />

Now everything should work as it did before, except now we have access to pads in our root component. This means we can now write our checkPad method, which simply loops through the pads state and calls a new method, playSound, if the pad at the current pos is active:

checkPad() {
    this.state.pads.forEach((row, rowIndex) => {
        row.forEach((pad, index) => {
            if (index === this.state.pos && pad === 1) {
                this.playSound(rowIndex);
            };
        })
    });
}

For simplicity's sake we're going to use the audio API to play a note depending on the row's position in the grid. Of course we could also play a sample instead. Let's set some frequencies in our constructor (source):

this.frequencies = [261.63, 277.18, 293.66, 311.13, 329.63, 349.23, 369.99, 392];

Also in the constructor we'll initialise the audio API (since this tutorial isn't focused on the API I won't go into details here):

this.audioCx = new (window.AudioContext || window.webkitAudioContext)();
this.gain = this.audioCx.createGain();
this.gain.connect(this.audioCx.destination);
this.gain.gain.value = 1;

Then our playSound method, which has already been given the rowIndex, grabs the appropriate frequency from the frequencies array, and plays a sine wave at that frequency:

playSound(rowIndex) {
    let freq = this.frequencies[rowIndex];
    let node = this.audioCx.createOscillator();
    let currentTime = this.audioCx.currentTime;
    node.frequency.value = freq;
    node.detune.value = 0;
    node.type = 'sine';
    node.connect(this.gain);
    node.start(currentTime);
    node.stop(currentTime + 0.2);
}

There's a couple more things to do: add a control for the BPM, and make the whole thing a bit prettier.

The BPM control is a range input, which goes under the button tag in the Controls component:

<div className="bpm">
    <label>BPM:</label>
    <input 
        type="range" 
        id="bpm" 
        min="1" 
        max="420" 
        step="1" 
        defaultValue={props.bpm} 
        onChange={props.handleChange} />
    <output>
        { props.bpm }
    </output>
</div>

It takes two properties passed from App: bpm and handleChange. So back in App, first add these as props to the render method:

<Controls 
    bpm={this.state.bpm} 
    handleChange={this.changeBpm} 
    playing={this.state.playing} 
    togglePlaying={this.togglePlaying} />

Then add the changeBpm method:

changeBpm(bpm) {
    this.setState({ bpm: bpm.target.value });
    if (this.state.playing) {
        clearInterval(this.timerId);
        this.setTimer();
    }
}

This sets the new bpm in state, and if we're already playing, stop the timer (since it's tied to the previous bpm) and restart it.

That's it for the functionality. All that remains is to add some CSS polish - if you want, head over to my raw CSS file and copy it into App.css.