Building a Step-Sequencer with React Hooks 🥁

21.02.2019 — 8 Min Read — In Coding

A Step Sequencer?

A step sequencer is a device that let's you compose music. By arranging the playback of sounds in certain timeframes, so-called steps, step sequencers are used by musicians to compose drum patterns or melodies. During the playback of the programmed sequence, individual sounds get triggered to generate a musical rythm. The most famous example of a step sequencer is the one built into the famous Roland TR 808 drum maschine which is responsible for drum grooves used in gems such as "Sexual Healing 🔥" by Marvin Gaye or "Planet Rock" by Afrika Bambaataa.

In this article I will show you how we can make a basic Step Sequencer to playback sounds in the browser with the use of React Hooks, styled-components, CSS-Grid and Tone.js. If you want to skip this tutorail and checkout the code, just scroll down and you will find the code embedded.

Alt Text

Some basic theory...

Paterns on a step sequencer are arranged in a timely manner. Each step is triggered periodically and depending if a certain step is activated or deactivated, a sound will be played. Let's say you are composing a pattern in four-four time and a specific note should be played on every downbeat step. You would compose the following pattern on the step sequencer:

[ X, _, _, _, X, _, _, _, X, _, _, _, X, _, _, _,]

Now if you want to trigger a note on every second downbeat you would compose the following pattern:

[ _, _, _, _, X, _, _, _, _, _, _, _, X, _, _, _,]

If you want to have 16th high hats for instance (e.g used in hip-hop or jazz pieces), you can go full on with the following pattern:

[ X, X, X, X, X, X, X, X, X, X, X, X, X, X, X, X,]`

Since a drum machine has multiple sounds, each sound will have one of these patterns each. By combining these pattern we can create a full drum groove.

Defining the Application State

In our example we will be able to trigger four distinct sounds: base drum, clap and a closed and open high hat. As the step sequencer will be able to compose a 16 step pattern sequence, we define the following inital state:

const initalState = { triggered: false, activated: false }
const initalSequence = [
    Array(16).fill(initalState),     // base drum
    Array(16).fill(initalState),     // clap
    Array(16).fill(initalState),     // closed high hat
    Array(16).fill(initalState)      // open high hat
];

A step being activated refers to the step being enabled for playback one the step is played. triggered refers to the state when the step is being played back by the sequencer.

const Sequencer = () => {
  const [sequence, setSequence] = useState(initalSequence);
  const [playing, setPlaying] = useState(false);
  const [currentStep, setCurrentStep] = useState(0);

  ...
}

Now we have that setup, we need two more states: one for the playing state (if we are in playback mode) and a next one for the current step (which is periodically increased). Thanks to React hooks, it is very easy to define those in our main component, the Sequencer. If you are not familiar with Hooks, check out this link.

Setting up the User Interface

Having the state ready, we can get started with the User Interface. Since our sequence state is a multidimensional array, with the sounds being on the x-axis and the time step being on y-axis, it makes a lot of sense to use to use a grid here. So what would be easiest here? Yes, CSS-Grid 👍.

With the help of styled-components for breathing life into React components we define the following Frame component:

const Frame = styled.div`
  width: 100vw; // full screen width
  height: 100vh; // full screen height
  display: grid;
  grid-template-columns: repeat(${{columns} => columns}, 1fr);
  grid-template-rows: repeat(${{rows} => rows}, 1fr);
`;

CSS Grid enables us to devide the screen into equal parts. First we set width and height to fullscreen and then we pass in two component properties: columns for the amount of columns and rows for the amount of rows in the grid. grid-template-columns: repeat(${props => props.columns}, 1fr) allows the grid columns to be split into repeated fractions (1fra). The same holds true for the grid-template-rows property. Nice.

Next we create a Cell component for each Step in the pattern. First, the cell needs to position itself inside the grid. This is why we pass in grid-column and grid-row as properties. Secondly, the cell should change color depending on if it is triggered, activated, both or none of the latter. For this reason we define the getBackground function.

const getBackground = (activated, triggered) => {
  switch (true) {
    case activated && triggered:
      return "#65da33";
    case activated && !triggered:
      return "#65daa2";
    case !activated && triggered:
      return "#eef";
    default:
      return "#fff";
  }
};

const Cell = styled.div`
  background: ${props => getBackground(props.activated, props.triggered)}
  outline: 1px solid #eee;
  grid-column: ${props => props.column};    // positioning
  grid-row: ${props => props.row};          // positioning
`;

Now we can combine the Frame and the Cell Component to generate the overall Grid component. The inital sequence state is the passed into the component via props.

const Grid = ({ sequence }) => (
  <Frame rows={4} columns={16}>
    {sequence.map((sound, i) =>
      sound.map((time, j) => (
        <Cell
          key={i + j}
          column={j + 1}
          row={i + 1}
          activated={sequence[i][j]["activated"]}
          triggered={sequence[i][j]["triggered"]}
        />
      ))
    )}
  </Frame>
);

Next, we need to mutate the state in order to compose some drum patterns.

Enabling sequence updates and playback

Once a user clicks onto a cell, we want the cell to be marked as activated in the sequence state. Therefore we define the toggleStep function. This will be used on each onClick handler of the cell to toggle the activated state. Depending on the sound and time step, we toggle the activated property of each cell. This will change the background color accordinly to indicate the activation.

  const toggleStep = (sound, step) => {
    const { triggered, activated } = sequence[sound][step];
    sequence[line][step] = { triggered, activated: !activated };
    setSequence(sequenceCopy);
  };

Next, we want to calculate the next step state of the sequence when the step counter is increased. Whenever the step counter matches the step index of a cell (the y-axis), the cell is triggered. This is why we loop through the current sequence state and update the triggered property of all cells.

  const nextStep = step => {
    for (let i = 0; i < sequence.length; i++) {
      for (let j = 0; j < sequence[i].length; j++) {
        const { triggered, activated } = sequence[i][j];
        sequence[i][j] = { activated, triggered: j === step };
        // this is where we want to play the sound.
      }
    }
    setSequence(sequence);
  };

As a final step, we want to increase the step counter periodically to enable a playback of the sequence. Here, we use the useEffect hook to calculate the next sequence state every 125ms (which refers to 120bpm). In this code snippet we use the javascript native setTimeout function as a periodic trigger.

 useEffect(
    () => {
      const timer = setTimeout(() => {
        if (playing) {
          setCurrentStep((currentStep + 1) % steps); // we only want step to be in range [0,15]
          nextStep(currentStep);
        }
      }, 125);
      return () => {
        clearTimeout(timer);
      };
    },
    [currentStep, playing]
  );

What we have gained so far is that we have defined the functions to mutate the state based on user interactions (toggling a cell) and on periodic updates from the step increment. These functions combined enable the playback of the step sequencer 🎶.

Play that sound

Thanks to tone.js a fabulous WebAudio library it is easy to setup a browser based audio player without having to deal with more lower level code. Tone.js has a Players class which is perfecly suited for our use case. It can be initialised with mutiple sounds with can then be played back individually. This is what we need.

We define the PlayerProvider component using a render-prop to pass down the initialised audio player to a child component. We are using this pattern here since we cannot initialise the audio player and then isue it immediatly. This allows the child component to check whether the player is already there or not.

const PlayerProvider = ({ children }) => {
  const [player, setPlayer] = useState(null);
  useEffect(() => {
    const player = new Tone.Players(
      {
        BD: "/kick.wav",
        CP: "/clap.wav",
        OH: "/hh_open.wav",
        CH: "/hh_closed.wav"
      },
      () => {
        console.log("player is loaded");
        setPlayer(player);
      }
    ).toMaster();
  }, []);

  return children({ player });
};

Now that we have the ui, the state management and the audio player in place, we can finally play audio based on if a cell is activated and triggered. We can now pass the player to the Sequencer component and play the sound in the nextStep function.

<PlayerProvider>
    {({ player }) => {
      if (!player) {
        return <p>loading....</p>;
      }
      return <Sequencer player={player} />;
    }}
</PlayerProvider>

Hurray. We have now built a step sequencer.

Wrapping things up

What we have seen in this article is how we can make a simple step sequencer which plays back sound sequences with React. By using CSS Grid and styled components, we were able to craft a user interface which reflects the sequencer state. Finally, we used React hooks (useState and useEffect) to update the playback sequence and used a timer function to trigger the individual notes.

I hope you like this tutorial, the code can be found in the code sandbox below 👀. Happy drum programming... 🥁

Credits

Thanks to trisamples.com for the free trap sample pack. Downloads here.