Minimum Viable Banjo

Happy Monday! We're going to spend this week building a progressively more complex version of whatever object you've chosen to represent from Friday's post. You're welcome to use the banjo example that I'm illustrating with, but I recommend that you also do this exercise with a different object of your own choosing, either in parallel or after we're all done with the banjo example.

As a reminder, the requirements of this exercise are to model the inputs and outputs of your interactions with this object, worrying about conceptual relationships but not scientific accuracy. The program should not contain a graphical UI, so that we are focusing on the behavior of the object rather than the interaction with the program.

First pass of our MVB

With that in mind, here are the basic requirements I came up with for my Minimum Viable Banjo (hereafter, MVB):

  1. A reasonable representation of the range of notes that the banjo can play. That is to say, we need a way of naming notes. This is not so much a quality of the banjo, but the context in which its outputs exist.

  2. A representation of which pitch an individual string is tuned to

  3. A representation of finger position on the fretboard (the long neck of the banjo) which changes the pitch of each string. Each fret on the fretboard corresponds to a raise in pitch by one semitone (aka a half step).

  4. A representation of picking an individual string, which takes the base pitch of that string plus a fret position if applicable, and produces a pitch

  5. A representation of the note that is played when a string is picked. If we include a strength argument when picking, then the resulting note could reasonably have properties of pitch, volume, and duration.

Representing the range of tones

The notes can be represented by a one-dimensional array, like a piano keyboard, where each entry in the list is a half-step higher than its predecessor. It turns out this work has already been done for us in the form of MIDI note numbers, and we can simply use integers to refer to each note.

Individual Strings

The Banjo has five strings, each tuned to a particular pitch. Keeping with our MVB approach, we can minimally represent this in our banjo as follows.

(Note that you may find the code formatting easier in the web version of this email)


class Banjo  {
  // array of MIDI note numbers. Note names in comments next
  // to each integer are the corresponding International Pitch Notation.
  // Don't worry if you don't know IPN, it's just a way of labeling pitches
  strings = [
    62, // D4
    59, // B3
    55, // G3
    50, // D3
    67, // G4
  ]
}

So there's our five-string banjo. It doesn't do much yet, but it's a start.

Fretting the strings

Fretting of the strings can similarly be represented by a simple array, one fret position for each string. 0 represents an open string, and any positive integer represents a finger on the corresponding fret, which raises the pitch of that string the same number of MIDI tones (ie. half-steps if you're musically trained)


class Banjo {
  strings = [ 62, 59, 55, 50, 67 ]
  frets = [ 0, 0, 0, 0, 0 ]
}

Picking a fretted string

With these two representations in place, implementing the basic interactions of fretting and picking become trivial.


class Banjo {
  strings = [ 62, 59, 55, 50, 67 ]
  frets = [ 0, 0, 0, 0, 0 ]

  fretString(stringIdx, fret) {
    this.frets[stringIdx] = fret
    return this.frets
  }

  pickString(stringIdx, strength) {
    const basePitch = this.strings[stringIdx]
    const fret = this.frets[stringIdx]
    // Adding the fret value to the base pitch, and returning an object
    // with pitch, volume, and duration.
    return ({
      pitch: basePitch + fret,
      volume: strength,
      duration: strength * strength
    })
  }
}

Running it

So, our minimum viable banjo is complete. We could run it like so:

const banjo = new Banjo()
banjo.pickString(2, 50)
// Object { pitch: 55, volume: 50, duration: 2500 }
// Picking the string at index 2 results in a G

banjo.fretString(2, 3)
// Array [ 0, 0, 3, 0, 0 ]
// Placing a finger on the third fret of the string at index 2

banjo.pickString(2, 100)
// Object { pitch: 58, volume: 100, duration: 10000 }
// Picking the string at index 2 now results in a B-flat

Problems, and next steps

I'm always in favor of starting with the simplest possible implementation. This is a good start, but the thing about software is that it lives for a long time, and has to evolve as it satisfies a edge cases and demands from different stakeholders over time.

I'm going to add two curveballs into this mix and show tomorrow how I would solve them.

First, the fifth string of the banjo is actually shorter than all of the others, and only reaches partway up the neck. Its first available fret is fret #12.

Second, in our implementation of a picked string, we've represented the duration that the note lasts. In real life, if you change the fret and re-pick the string, the note that is ringing will be stopped, and replaced with a new note. How are we going to represent this?

If you're working with the banjo example, see what you can come up with for these new requirements. If you're using an object of your own, Try to find similar edge cases and refinements to your model and implement them.

Until tomorrow!

Next Up:
Iterating on the Banjo

Previously:
A Coding Exercise


Want to impress your boss?

Useful articles delivered to your inbox. Learn to to think about software development like a professional.

Icon