Iterating on the Banjo

We finished yesterday with a very minimal representation of a banjo, and I said that I would address two additional features today:

  1. The fifth string, which is shorter than the rest and starts at fret 5
  2. A string can only play one note at one time, so a previous note played must somehow be cancelled if it is still ringing when the next note is played on the same string.

Before I go any further I want to point out that the purpose of this exercise is not to build the perfect banjo. In fact, what we end up with at the end of this week will be monstrously over-engineered for the scale of application that it is.

No, the purpose of this exercise is to get you used to thinking about which components of your application are responsible for what, and how to create stable, predictable interactions between them. When we're done, I'll show you explicitly how these lessons apply to some of the frameworks and libraries that you are familiar with.

With all that said, and considering our new requirements, the thing that stands out to me is that the string of the banjo is starting to emerge as the fundamental element related to our inputs and outputs. The base pitch, the relative fretting, and the strength of the pick all relate directly to the string, and so I believe it makes sense to make the string its own class.

(code may look better in the web version)

class BanjoString {

  fretPosition = 0

  constructor(opts = { basePitch: 0 }) {
    this.basePitch = opts.basePitch

  fret(fretPosition) {
    this.fretPosition = fretPosition

  pick(strength) {
    return {
      pitch: this.basePitch + this.fretPosition,
      volume: strength,
      duration: strength * strength

You'll notice the implementation of BanjoString looks a lot like the previous implementation of Banjo. We can now update Banjo as follows, to essentially delegate the fretting and picking to the appropriate strings:

class Banjo {
  strings = [
    new BanjoString({ basePitch: 62 }),
    new BanjoString({ basePitch: 59 }),
    new BanjoString({ basePitch: 55 }),
    new BanjoString({ basePitch: 50 }),
    new BanjoString({ basePitch: 67 })

  fretString(stringIndex, fret) {
    // deal with the edge case of the 5th string
    if (stringIndex === 4 && fret > 0 && fret < 6) {
      throw new Error (`Fret ${fret} does not exist on stringIndex 4`)
    const fretPosition = stringIndex === 4 ? fret - 5 : fret


  pickString(stringIndex, strength) {
    return this.strings[stringIndex].pick(strength)

It may look like we've doubled the amount of source code here to account for a simple edge case. You wouldn't be wrong to point that out, but we've also created clear conceptual boundaries between the properties of the banjo itself, and those of the strings. That's going to become important as the application gains complexity.

The next requirement is to account for picking a string before its previous note has decayed. In other words, replacing a ringing note with a note plucked on the same string.

For that, I'm going to make a very minor change, and then kick the can down the road. You see, producing sound is really the responsibility of the UI layer. We've specifically said that we're not focusing on the UI for this portion of the exercise, so all we need for the banjo itself is to identify which string a note was played on. When we implement a real UI (which we will, don't worry), we can address the logic of overlapping tones there.

A simple change to the pickString method on the Banjo object accomplishes that:

  pickString(stringIndex, strength) {
    const tone = this.strings[stringIndex].pick(strength)
    // attach stringIndex property to returned tone
    return {...tone, stringIndex }

Here's a gist of the whole thing so far.

Later this week we'll use this to implement a UI that plays MIDI tunes and leaves open the possibility of adapting to other UI implementations as well.

Tomorrow, how is a banjo like an Express Router?

Did you like this?

I send a daily email with tips and ideas like this one. Join the party!