Heads up! I've decided to self-manage my blog. I'm leaving this post live as to not "break the internet" but feel free check out the post on my new blog here! blog.ericyd.com/loop-supreme-part-3-metrono..
This is part 3 in a series:
Part 3: Metronome click
Add an audible "click" when the metronome advances to the next tick. I'm excited because this is the time I'll use the Web Audio API!
Keep it absolutely dead simple. There will likely be lots of refactoring as I understand more about how to properly initialize and route the audio, so there's no need to search for the "perfect" solution right now
Producing a tone
To keep things dead simple, I wanted to use an
OscillatorNode to play a simple sine wave on each beat.
It turns out this is actually fairly simple, and there is ample documentation on MDN about how to create a simple
Playing in time
After I was able to produce a tone, I needed to synchronize the tone with the metronome tick. There is a great tutorial on MDN on how to do this very thing, but since I'm using React I had to substantially alter their example to fit the React paradigm. The queue-based playback mechanism they demonstrate probably works great for a simple script, but I knew that mutating lots of state within a React component would be bad news bears. Unless I was prepared to use a bunch of
useEffect hooks, I knew that I wouldn't want to copy their example precisely.
After some experimentation, it seemed that simply adding a
playTone() call inside my
useInterval callback was sufficient to make the metronome work! Keep in mind, I did not scientifically test this to make sure it's playing in perfect time, but it seems to be "good enough" for now. I have about 98% certainty that I will need to refactor this in the future, but for now the timing appears accurate from simple observation.
There were lots of interesting surprises in this one! That makes sense, given that I've never touched the Web Audio API before in my life. It was fun to wrap my brain around some of the suggested patterns, and understand why the API is built the way it is. Here are some things that surprised me specifically:
OscillatorNodes can only be started once! Since I'm using an
OscillatorNodefor the main "beep" of the metronome, that means I had to create a new
OscillatorNodeevery time I wanted to play a tone. Of course this isn't complex from a code perspective, but my initial assumption was that it would be preferable to create a
stop()it every time I wanted to emit a tone. Turns out this is not the recommended pattern, and creating a new node every time is correct. (This also matches the examples in the MDN docs.)
AudioContextis not supposed to be created without user input. This is shown as a console warning; in Firefox it reads: "An AudioContext was prevented from starting automatically. It must be created or resumed after a user gesture on the page." This contributed to my first scope creep! I realized that I needed to add a way for the user to start the metronome, without it auto-playing on page load. I expect in the future I'll defer on creating the
AudioContextat all until the user clicks "play" (as opposed to my current pattern where I instantiate the
AudioContextimmediately, but allow the user to start it explicitly with the "play" button). I decided to punt on that until I understood my final patterns and routing a bit more.
AudioContexthas a high precision timer built in that can be accessed via
audioContext.currentTime. This timer is used as the source of truth in the MDN example of playing in time, and I suspect I may refactor to use this as my source of truth as well. However, I'm currently pleased that a simple JS
setIntervalis keeping time that appears pretty accurate from a human perspective.
State of the app
Merged PR github.com/ericyd/loop-supreme/pull/4
Metronome makes a noise!
Metronome can be started/stopped by user
Minor styling updates
probably about 2 hours of experimentation / research / implementation for Metronome beep
another hour or so for cleanup and testing