DEV Community

Cover image for Wrestling With Time: Creating  a Sequencer with NexusUI & Tone.js
Je We
Je We

Posted on

Wrestling With Time: Creating a Sequencer with NexusUI & Tone.js

Hello again! I'm continuing what has become a series of blogs about integrating NexusUI with Tone.js to create musical instruments for the web. You can read the other posts here, here, and here.

My latest challenge was to build a sequencer. If you don't know what a sequencer is, it's an interface that allows the user to plan out a loop of sounds, which will then be played back very precisely by an internal clock. The contents of this loop can also be changed in real time. You sit back and relax as a robot plays back your perfect arpeggios, and soon you're making star-gazing cheese like this.

NexusUI provides a very nice-looking sequencer interface:

Alt Text

Rendering the sequencer is simple:

const sequencer = new Nexus.Sequencer('#seq', {
   size: [400, 200],
   mode: 'toggle',
   rows: 6,
   columns: 16,
 });
Enter fullscreen mode Exit fullscreen mode

This will create a new sequencer on the page, injected into the element with id 'seq', with the specified number of rows and columns. Each row represents a possible instrument or sound, and each column represents a beat in the cycle. Any number of the squares in each column can be toggled on or off. To proceed to the next beat, simply use the sequencer's next method. This will trigger a 'step' event on the sequencer, which will allow us to track the value of that beat.

sequencer.next();

sequencer.on('step', (col) => {
  console.log(col) // [0, 1, 0, 1, 1]
});
Enter fullscreen mode Exit fullscreen mode

The value of the beat is returned in an array of 1's and 0's (1 meaning instrument on, 0 meaning off), listing the column from bottom to top.

This first beat of this sequencer would return [1, 0, 0, 0, 0]

Alt Text

And the first beat of this one would return [0, 0, 0, 0, 1]

Alt Text

This setup provides us with a nice road map for going forward: on each step of the sequencer, simply iterate through the value of the current column, and for any toggled indices, trigger synths stored in an array with corresponding indices. There is one big obstacle though: TIME.

At first, I didn't know this would be a problem. I attempted a naive solution in which I used Tone Transport, which allows the scheduling of one-time audio events as well as loops. I went for Tone Transport's scheduleRepeat method, which allows for a callback function to be called at a specified interval. It looked a little like this:

sequencer.on('step', (col) => {
  this.col = col; // store current value of col
 });

Tone.Transport.scheduleRepeat(() => {
   this.sequencer.next(); // step to next beat
   this.col.forEach((state, i) => { // iterate through new col value
     if (state) { // if toggled, trigger corresponding synth/note
       this.synths[i].triggerAttackRelease(this.notes[i]);
     }
   });
 }, '0.5'); // repeat every half second
Enter fullscreen mode Exit fullscreen mode

I sat back, relaxed, and listened to my sequencer. It sounded extremely drunk. I was about to discover that JavaScript hates music. You might notice that my use of scheduleRepeat above basically amounts to a fancy version of setInterval. It turns out that setInterval and setTimeout, while seemingly very exact to the human eye & intuition, are actually noticeably inexact when it comes to musical timing. This is because, since setInterval and setTimeout are asynchronous, their callbacks are only executed once the callstack of synchronous, blocking code is cleared, leading to slight fluctuations in timing that are not usually noticeable to reasonable people, but are noticeable to those people's unreasonable ears. More can be read on the subject here. The Web Audio Clock, on the other hand, is an independent entity, free from any conflicting concerns that might effect its accuracy, and so the best context for scheduling audio events.

I had even read (but chosen to ignore) this revealing sentence in Tone's docs:

"Unlike browser-based timing (setInterval, requestAnimationFrame) Tone.Transport timing events pass in the exact time of the scheduled event in the argument of the callback function. Pass that time value to the object you’re scheduling."

Luckily, the solution is simple. Tone synths can be triggered immediately (as I was doing), or they can be given a time to be played. Tone Transport passes this exact Web Audio time to its callback function, which can then use it to schedule the appropriate synths at the appropriate times:

Tone.Transport.scheduleRepeat((time) => {
   this.sequencer.next();
   this.col.forEach((state, i) => {
     if (state) {
       this.synths[i].triggerAttackRelease(this.notes[i], '8n', time);
     }
   });
 }, '0.5');
Enter fullscreen mode Exit fullscreen mode

The arguments to triggerAttackRelease are pitch, length, and scheduled time. This will give us a very rhythmically accurate sequencer!

Hope you've enjoyed reading. Please post any questions you might have, and have some fun playing with my demo here! It has some features, like tempo control and custom scales/voices, which I'll cover in a future post.

Top comments (1)

Collapse
 
everythingability profile image
everythingability

Can you share the code, it'd be really useful thanks.