Wrapping up: Adding Sound to our Banjo

A quick top-of email announcement: I'll be giving a free webinar on Test Driven Development aimed at bootcamp grads and self-taught developers on Wednesday, January 27 at 1pm Central Time. If you're interested, you can RSVP here

Also, if you're new to the list, we're a bit more than a week into a coding exercise. You might want to catch up here and then come back.

Adding Sound

Finally, the big day has arrived. We're adding sound to our little toy browser banjo. As I mentioned last week, it took me a while to settle on exactly how I wanted to do this. I had it narrowed down to three possibilities:

  1. Using an audio element, I could play individual mp3 or aiff files for each note that could be produced by the banjo. Finding, downloading, and managing all of those individual files seemed pretty tedious for the scale of exercise that this is. So I thought about other options where I could programatically generate tones.

  2. The web audio API can do some pretty cool stuff with oscillators and sequencers, but it's quite low-level. The amount of code necessary for this exercise would be significant, and a distraction from the main point of the exercise, which is how to modularize your code based on responsibility.

  3. Tone.js is a browser-based MIDI library. We can use it to create an in-browser synthesizer, and programmatically generate notes by name (or MIDI number). Out of the box it doesn't sound anything like a banjo, but for our toy example it should do just fine.

At this point we've done most of the work. We have a working user interface to collect inputs, and the banjo emits events on each tone with all the information that we need to reproduce the note.

Setting up Tone.js

Once we've included tone.js in our html file, we have to do a little bit of setup in order for the browser to play sounds. Chrome (and I think all other modern browsers) won't let you play sound on a page until the user has interacted with it in some way. I'm going to initially disable all of the banjo inputs and add a button which initializes a tone.js synthesizer and enables all the inputs when you click it. Actually, I'm going to initialize five synthesizers, one for each string:

(as usual, code formatting looks better on the web)

// File: ui.js

// Hoisting synths to a higher scope so they can be used in  other
// event handlers once they've been initialized.
let synths

const allBanjoInputs = document.querySelectorAll(".string input, .string button")

function disableBanjo() {
  allBanjoInputs.forEach(input => input.setAttribute('disabled', 'disabled'))
}

function enableBanjo() {
  allBanjoInputs.forEach(input => input.removeAttribute('disabled'))
}

// initially disable all the banjo controls, then when the
// user clicks the "Initialize" button, we create a synth for
// each banjo string, disable the Initialize button, and
// enable all the banjo controls
disableBanjo()
document.querySelector("#initialize").addEventListener('click', () => {
  // documentation on tone.js can be found here
  // https://tonejs.github.io/docs/14.7.77/Synth.html
  synths = Array(5).fill('').map(x => new Tone.Synth({
    envelope: {
      attack: 0,
      attackCurve: 'linear',
      decay: 0.2,
      sustain: 0.5,
      release: 5,
      releaseCurve: 'exponential'
    }
  }).toDestination())
  document.querySelector("#initialize").setAttribute('disabled', 'disabled')
  enableBanjo()
})

Why did I create a synth for each string? If you'll recall from a few days back, we have a requirement that if a tone is ringing on one string, and that string is re-picked, we have to cancel the existing tone before playing the new tone. One string can't play two tones at the same time.

Well, it turns out that a tone.js Synth is monophonic by default. It can only play one note at a time. By creating five monotonic synthesizers, we get our one-note-per-string behavior right out of the box.

For this toy project, this is a perfectly fine solution. In a realistic environment, I would do more research about the implications of five monotonic synths as opposed to a polyphonic synth with some additional logic to address our requirement. This is a perfect illustration of the purpose of this exercise. If we do need to refactor, the changes are contained to the UI layer.

Making Sound

We've done the hard work now. Producing sound is just a 4-line update to the event handler for banjo tones:


banjo.addEventListener('tone', tone => {
  // get stringIndex of the tone and parse to a number
  const stringIndex = +tone.detail.stringIndex

  // Set the volume on the corresponding synth
  // Our range sliders go 0-100, but the synth volume is measured in
  // decibels. With some experimentation, the scale of -20 to +80  
  // turned out to be a suitable range and an easy conversion
  synths[stringIndex].volume.value = tone.detail.volume - 80

  // convert integer note number to pitch name eg. G3
  const noteName = Tone.Frequency(tone.detail.pitch, "midi").toNote()

  // Trigger a pick on the synth
  synths[stringIndex].triggerAttackRelease(noteName, 0)
})

That should do it! The full gist is here. You can poke around at it make some noise. If you're feeling ambitious and you want to hack on this example a bit, see if you can implement something like this. Or if you're working on your own object, come up with some advanced features for your UI. Does the modular logic make it easier to build?

Tomorrow I'll draw some parallels to libraries and tools that you are more familiar with, and also point out where the contrived nature of this exercise goes against what I would have actually done building a production application.

Next Up:
Lessons From the Banjo Exercise

Previously:
Building the UI, part 1 - Receiving Inputs


Want to impress your boss?

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

Icon