Synthesizing Hi-Hats with Web Audio
Introduction
I just came across Chris Lowis's cool Synthesising Drum Sounds with the Web Audio API article, which covers synthesizing drum kick and snare sounds. It stops short of synthesizing the hi-hat, however:
Synthesising a realistic-sounding hi-hat is hard. When you strike a disk of metal the sound that is produced is a complex mixture of unevenly-spaced harmonics which decay at different rates. It’s not an impossible task, and if you’re interested there’s some further reading below, but for this post we’re going to cheat a little by sampling an existing sound.
Taking a close look at Gordon Reid's Sound On Sound article on hi-hat synthesis, at least synthesizing the TR-808's hi-hat actually seems pretty feasible:
...multiple oscillators are fed through a filter that removes the low frequencies, and through a VCA controlled by a simple contour generator.
Sounds simple enough. Let's give it a shot!
The Spec
Reid provides a helpful block diagram of the 808 hi-hat sound, which breaks down to this: six square wave oscillators feed into a bandpass filter, which feeds into a highpass filter, which goes through a volume envelope. Somehow, the oscillators and filters simulate the sound of the metal, while the envelope provides the hit, attack, and decay, of the sound.
Square
We're entering the section of the article where there's sounds, so make sure your volume is not too loud.
I wouldn't have guessed that the 808 hat is composed of square waves. Here's what they sound like: .
Let's try to mimic a closed hi-hat's decay:
Yeah... so, somehow six of those will turn into a hi-hat?
Six of Those
Gordon Reid includes one image in his article that displays how these six square waves are arrayed. Below is a little calculator with the root frequency (called fundamental here) plugged in and the ratio of each of the oscillators' frequency to that root. Note, we don't actually synthesize the fundamental.
var $inputs = $("#six-square-calculator input[type=number]");
var ratios = $inputs.map(function(i, el) {
return parseFloat(el.value);
}).toArray();
var fundamental = ratios.shift();
var gain = context.createGain();
var when = context.currentTime;
var oscs = ratios.map(function(ratio) {
var osc = context.createOscillator();
osc.type = "square";
osc.frequency.value = fundamental * ratio;
osc.connect(gain);
osc.start(when);
osc.stop(when + 0.2);
return osc;
});
gain.connect(context.destination);
gain.gain.value = 0.1;
gain.gain.exponentialRampToValueAtTime(0.00001, when + 0.1);
Ok, six square waves sounds pretty complex, and it's starting to sound precussive, but it's still nothing like a hi-hat. Let's take the next step and run it through a bandpass filter.
Filter Time
Examples from here out will use the values in the calculator above, so if you've gotten them out of whack give the page a refresh if you want to be sure you're hearing what I'm hearing.
Below is a similar tool for setting up the bandpass filter. Click the button to hear the oscillators defined above played through the filter:
Crazily enough the sound is starting to get there. It's still plenty clear that there are synths underneath the sound, but I can hear this as a percussive hit rather than a musical tone. Now let's refine the envelope a little bit:
gain.gain.setValueAtTime(0.00001, when);
gain.gain.exponentialRampToValueAtTime(1, when + 0.02);
gain.gain.exponentialRampToValueAtTime(1/3, when + 0.03);
gain.gain.exponentialRampToValueAtTime(0.00001, when + 0.3);
Ok. Even closer. Adding the attack and the very quick decay created the snap we're used to hearing in hi-hats. Now let's plug in the last piece of the diagram, the highpass filter.
Highpass
Well, that pretty much does it. Adding the highpass pulled the last of the note-y sound out and what we're left with sounds a lot like a hi-hat! Or, at least what we're used to hearing as a hi-hat. All in about 30 lines of code!
var context = new AudioContext();
var fundamental = 40;
var ratios = [2, 3, 4.16, 5.43, 6.79, 8.21];
// Always useful
var when = context.currentTime;
var gain = context.createGain();
// Bandpass
var bandpass = context.createBiquadFilter();
bandpass.type = "bandpass";
bandpass.frequency.value = 10000;
// Highpass
var highpass = context.createBiquadFilter();
highpass.type = "highpass";
highpass.frequency.value = 7000;
// Connect the graph
bandpass.connect(highpass);
highpass.connect(gain);
gain.connect(context.destination);
// Create the oscillators
ratios.forEach(function(ratio) {
var osc = context.createOscillator();
osc.type = "square";
// Frequency is the fundamental * this oscillator's ratio
osc.frequency.value = fundamental * ratio;
osc.connect(bandpass);
osc.start(when);
osc.stop(when + 0.3);
});
// Define the volume envelope
gain.gain.setValueAtTime(0.00001, when);
gain.gain.exponentialRampToValueAtTime(1, when + 0.02);
gain.gain.exponentialRampToValueAtTime(0.3, when + 0.03);
gain.gain.exponentialRampToValueAtTime(0.00001, when + 0.3);
Conclusion
Thanks to Chris Lowis for getting the ball rolling w/ the kick + snare synthesis, and of course Gordon Reid's Synth Secrets "63-part series". Reid's articles are extremely good, and it's kind of a shame that they aren't more interactive considering that we now have web audio. Maybe there's something to be done about that?