<aside> <img src="/icons/globe_purple.svg" alt="/icons/globe_purple.svg" width="40px" />

Live page: https://justinjohnso-itp.github.io/cmus-github-repo/week-4-homework/polyrhythm-sequencer/

</aside>

<aside> <img src="/icons/gear_lightgray.svg" alt="/icons/gear_lightgray.svg" width="40px" />

Github repo: https://github.com/justinjohnso-itp/cmus-github-repo/tree/main/week-4-homework/polyrhythm-sequencer

</aside>


Process

My goal for this project was to replicate a “3-against-4” polyrhythm. While this is definitely possible to do with a single playhead, I figured it would be both easier to execute and visualize if I used two grids. Pressing “play” will start a 4-bar loop across both grids.

I set up two grids, one for 4/4 time (grid4) and one for 3/4 time (grid3). Both of them are locked to the same bpm.

To deal with “lining up” the sounds, I made a pair of wrapper functions that I could then call to play back the sounds for each respective grid.

function playBeat4(time) {
  if (kit.loaded) {
    if (grid4[0][position4]) kit.player("kick").start(time);
    if (grid4[1][position4]) kit.player("snare").start(time);
    if (grid4[2][position4]) kit.player("hihat").start(time);
    if (grid4[3][position4]) kit.player("clap").start(time);
  }
}

function playBeat3(time) {
  if (kit.loaded) {
    if (grid3[0][position3]) kit.player("kick").start(time);
    if (grid3[1][position3]) kit.player("snare").start(time);
    if (grid3[2][position3]) kit.player("hihat").start(time);
    if (grid3[3][position3]) kit.player("clap").start(time);
  }
}

Past that, I also needed to figure out how to handle working with two separate sequencers at once. Luckily tone.js makes that pretty easy by letting you set up as many Transport instances as you want.

Tone.Transport.scheduleRepeat((time) => {
  playBeat4(time);
  position4 = (position4 + 1) % 16;
  updatePlayhead(document.getElementById("grid4"), position4, 16);
}, "16n");

Tone.Transport.scheduleRepeat((time) => {
  playBeat3(time);
  position3 = (position3 + 1) % 12;
  updatePlayhead(document.getElementById("grid3"), position3, 12);
}, "12n");

The main tricky bit here was making sure I got my math right for the 12n and 16n subdivisions, as well as calculating the playhead position for each grid.

After a buuuuuunch of trial and error, here’s the final result!

Screenshot 2025-02-25 at 13.39.55.png

Oh wait it’s weird