TR808 Cymbal Hardware Accurate Modeling
Derived from Roland TR-808 service manual schematics and Werner/Abel/Smith, “The TR-808 Cymbal: a Physically-Informed, Circuit-Bendable, Digital Model”
Overview
The basic signal flow for 808 cymbals (hi-hats open and closed, cymbal), is approximately:
Oscillators -> Mixer -> BPF Stage -> HPF Stage -> VCA -> Tone Shaping Stage
^Amplitude Envelope
** The tone shaper is a 5th order filter beyond the scope of this writeup that is less integral in synthesizing the 808 cymbal. It isn’t adding any extra resonance that the other filter stages are and solving the discrete-time transfer function for this filter is impractical due to the complexity of the circuit.
Oscillators
The TR-808 uses six Schmitt-trigger oscillators two of which are tunable by internal trimpots. Their nominal frequency values are:
- 800 Hz (factory setting) with a range of 359.4 to 1149.9 Hz
- 540 Hz (factory setting) with a range of 254.3 to 627.2 Hz
- 522.7 Hz
- 369.6 Hz
- 304.4 Hz
- 205.3 Hz
Schmitt trigger oscillators are pulse waves. The 808 oscillator circuit yields a duty cycle of 47.98% for each one. They all have the same amplitude (5V).
SC Implementation
(
// Using the nominal values and the factory settings for the tunable oscillators
// You could certainly make the tunable oscillator frequencies control values
var freqs = [800.0, 540.0, 522.7, 369.6, 304.4, 205.3];
var width = 0.4798;
var oscs = freqs.collect{|freq|
Pulse.ar(freq, width);
}.sum;
oscs = oscs * 0.15; // we'll scale this down some for sanity
~oscs = oscs; // store in an environment variable so things are easy
)
BPF Stage
The bandpass stage is two parallel third-order bandpass filters with center frequencies of approximately 3440 Hz and 7100 Hz. The output of the filters are then mixed.
General Form
Transfer function shape: H(s) = (β₂s² + β₁s) / (α₃s³ + α₂s² + α₁s + α₀)
Factoring the denominator gives one real pole and a complex-conjugate pair. In practice this is achieved by cascading a first (FOS) and second order section (SOS).
**I used scipy to derive these values from the schematic. See Werner et al for a list of the requisite components and a more detailed analysis.
BPF1 (center ≈ 3440 Hz)
- Zeros:
-131302.521,0 - Poles:
-1793.400 ± j21627.290(complex pair),-9182.736(real) - Gain:
-8912.656
BPF2 (center ≈ 7100 Hz)
- Zeros:
-270562.771,0 - Poles:
-3695.492 ± j44565.325(complex pair),-45454.545(real) - Gain:
-27548.209
These are continuous time values – sample rate agnostic. We can use SampleRate.ir to ensure the values get calculated correctly regardless of the server sample rate via the bilinear transform. The real pole is the FOS stage and the complex pair is the SOS stage.
SC Implementation
(
var calcBpf3 = {|in, zRe0, pRe0, pIm0, pRe1, kS|
var fs = SampleRate.ir;
var fosPole = (2*fs + pRe1) / (2*fs - pRe1);
var denom = (2*fs - pRe0).squared + pIm0.squared;
var pReZ = ((2*fs).squared - (pRe0.squared + pIm0.squared)) / denom;
var pImZ = (2*pIm0 * 2*fs) / denom;
var sosB1 = 2*pReZ;
var sosB2 = -1 * (pReZ.squared + pImZ.squared);
var zZeroD = (2*fs + zRe0) / (2*fs - zRe0);
var kD = kS * (2*fs - zRe0) * (2*fs) /
((2*fs - pRe1) * ((2*fs - pRe0).squared + pIm0.squared));
var sosA0 = kD;
var sosA1 = kD * -1 * (zZeroD - 1);
var sosA2 = kD * -1 * zZeroD;
var out = FOS.ar(in, 1.0, -1.0, fosPole);
out = SOS.ar(out, sosA0, sosA1, sosA2, sosB1, sosB2);
};
var bpf0 = calcBpf3.(
~oscs, // in
-131302.5210084, // zRe0
-1793.40028694, // pRe0
21627.29000861, // pIm0
-9182.73645546, // pRe1
-8912.6559714795 // kS
);
var bpf1 = calcBpf3.(
~oscs, // in
-270562.77056277, // zRe0
-3695.49150037, // pRe0
44565.32486623, // pIm0
-45454.54545455, // pRe1
-27548.20936639118 // kS
);
~bpfStage = bpf0 + bpf1;
)
HPF Stage
The highpass stage consists of three Sallen-Key highpass filters – two second order filters and one third order.
SC Implementation
(
// Filter calculation functions
var calcHpf2 = {|in, pRe, pIm, kS|
var fs = SampleRate.ir;
var denom = (2*fs - pRe).squared + pIm.squared;
var pReZ = ((2*fs).squared - (pRe.squared + pIm.squared)) / denom;
var pImZ = (2*pIm * 2*fs) / denom;
var b1 = 2 * pReZ;
var b2 = -1 * (pReZ.squared + pImZ.squared);
var kd = kS * (2*fs).squared / denom;
var a0 = kd;
var a1 = -2 * kd;
var a2 = kd;
SOS.ar(in, a0, a1, a2, b1, b2);
};
var calcHpf3 = {|in, pRe0, pIm0, pRe1, kS|
var fs = SampleRate.ir;
var fosPole = (2*fs + pRe1) / (2*fs - pRe1);
var denom = (2*fs - pRe0).squared + pIm0.squared;
var pReZ = ((2*fs).squared - (pRe0.squared + pIm0.squared)) / denom;
var pImZ = (2*pIm0 * 2*fs) / denom;
var sosB1 = 2 * pReZ;
var sosB2 = -1 * (pReZ.squared + pImZ.squared);
var kd = kS * (2*fs).cubed / ((2*fs - pRe1) * denom);
var sosA0 = kd;
var sosA1 = -2 * kd;
var sosA2 = kd;
var out = FOS.ar(in, 1.0, -1.0, fosPole);
out = SOS.ar(out, sosA0, sosA1, sosA2, sosB1, sosB2);
};
// Filters
var hpf0 = calcHpf2.(
~bpfStage, // in
-8130.08130081, // pRe
13426.38737959, // pIm
1.0 // kS
);
var hpf1 = calcHpf2.(
~bpfStage, // in
-27777.77777778, // pRe
48112.52243247, // pIm
2.0 // kS
);
var hpf2 = calcHpf3.(
~bpfStage, // in
-5749.67889223, // pRe0
64618.05691577, // pIm0
-32681.30805396, // pRe1
2.7727272727272725 // kS
);
// Final HPF stage sum
~hpfStage = hpf0 + hpf1 + hpf2;
)
Amplitude Envelope
The 808 cymbal’s amplitude envelope is generated by three “swing-type VCAs,” each driven by a diode-gated envelope generator with a switched (attack vs. release) time constant and a nonlinear clipping stage. The nonlinearity is difficult to model and because its effect on spectral character of the 808 cymbal is “perceptually insignificant” (Werner et al., §12), a reasonable substitute is EnvGen with Env.perc.
Some Useful Values
Attack: 1.0244e-4 seconds (Werner et al.)
Release: (according to the service manual)
- Cymbal: 0.35 - 1.2 seconds
- Open hat: 0.09 - 0.6 seconds
- Closed hat: 0.05 seconds
The SynthDef
(
SynthDef(\cymbal, {
// Controls
var rel = \rel.kr(0.05),
trig = \trig.tr(1),
out = \out.kr(0),
amp = \amp.kr(1),
pan = \pan.kr(0),
done = \doneAction.ir(2);
var freqs = #[205.3, 369.6, 304.4, 522.7, 800, 540]; // if you want the two tunable oscillators to be controls, you can do that too
// Filter calculations
var fs = SampleRate.ir; // we'll pull out of each function and just share it
var calcBpf3 = {|in, zRe0, pRe0, pIm0, pRe1, kS|
var fosPole = (2*fs + pRe1) / (2*fs - pRe1);
var denom = (2*fs - pRe0).squared + pIm0.squared;
var pReZ = ((2*fs).squared - (pRe0.squared + pIm0.squared)) / denom;
var pImZ = (2*pIm0 * 2*fs) / denom;
var sosB1 = 2*pReZ;
var sosB2 = -1 * (pReZ.squared + pImZ.squared);
var zZeroD = (2*fs + zRe0) / (2*fs - zRe0);
var kD = kS * (2*fs - zRe0) * (2*fs) /
((2*fs - pRe1) * ((2*fs - pRe0).squared + pIm0.squared));
var sosA0 = kD;
var sosA1 = kD * -1 * (zZeroD - 1);
var sosA2 = kD * -1 * zZeroD;
var out = FOS.ar(in, 1.0, -1.0, fosPole);
out = SOS.ar(out, sosA0, sosA1, sosA2, sosB1, sosB2);
};
var calcHpf2 = {|in, pRe, pIm, kS|
var denom = (2*fs - pRe).squared + pIm.squared;
var pReZ = ((2*fs).squared - (pRe.squared + pIm.squared)) / denom;
var pImZ = (2*pIm * 2*fs) / denom;
var b1 = 2 * pReZ;
var b2 = -1 * (pReZ.squared + pImZ.squared);
var kd = kS * (2*fs).squared / denom;
var a0 = kd;
var a1 = -2 * kd;
var a2 = kd;
SOS.ar(in, a0, a1, a2, b1, b2);
};
var calcHpf3 = {|in, pRe0, pIm0, pRe1, kS|
var fosPole = (2*fs + pRe1) / (2*fs - pRe1);
var denom = (2*fs - pRe0).squared + pIm0.squared;
var pReZ = ((2*fs).squared - (pRe0.squared + pIm0.squared)) / denom;
var pImZ = (2*pIm0 * 2*fs) / denom;
var sosB1 = 2 * pReZ;
var sosB2 = -1 * (pReZ.squared + pImZ.squared);
var kd = kS * (2*fs).cubed / ((2*fs - pRe1) * denom);
var sosA0 = kd;
var sosA1 = -2 * kd;
var sosA2 = kd;
var out = FOS.ar(in, 1.0, -1.0, fosPole);
out = SOS.ar(out, sosA0, sosA1, sosA2, sosB1, sosB2);
};
// Oscillator stage
var width = 0.4798;
var oscs = freqs.collect{|freq|
Pulse.ar(freq, width);
}.sum * 0.15;
// BPF stage
var bpf0 = calcBpf3.(
oscs, // in
-131302.5210084, // zRe0
-1793.40028694, // pRe0
21627.29000861, // pIm0
-9182.73645546, // pRe1
-8912.6559714795 // kS
);
var bpf1 = calcBpf3.(
oscs, // in
-270562.77056277, // zRe0
-3695.49150037, // pRe0
44565.32486623, // pIm0
-45454.54545455, // pRe1
-27548.20936639118 // kS
);
var bpfStage = bpf0 + bpf1;
// HPF stage
var hpf0 = calcHpf2.(
bpfStage, // in
-8130.08130081, // pRe
13426.38737959, // pIm
1.0 // kS
);
var hpf1 = calcHpf2.(
bpfStage, // in
-27777.77777778, // pRe
48112.52243247, // pIm
2.0 // kS
);
var hpf2 = calcHpf3.(
bpfStage, // in
-5749.67889223, // pRe0
64618.05691577, // pIm0
-32681.30805396, // pRe1
2.7727272727272725 // kS
);
var hpfStage = hpf0 + hpf1 + hpf2;
// Envelope
var atk = 1.0244e-4;
var env = Env.perc(atk, rel).ar(done, trig);
var sig = hpfStage * env * amp;
sig = Pan2.ar(sig, pan);
Out.ar(out, sig);
}).add;
)
AI Disclosure:
AI was used for the following purposes:
- Proofreading (prose and code legibility/consistency)
- Verifying and searching for electronic component values, as well as verifying analog circuit analysis