The couplings are the instrument: tb303_voice.h
The field-guide chapter argued that the 303 is unmistakable because its blocks are coupled — accent reaches the filter and the amplifier through shared circuitry with memory, slide is gate behavior, the envelopes have fixed interrelations. This appendix walks the per-sample code that implements those couplings: the measured envmod law, the C13 accent-sweep capacitor, the slide one-pole, the square shaper, and the phase-2 VCA. The filter itself is the previous appendix; this file composes it.
Sources, and the division of labor between them: Open303 (Robin Schmidt) supplies the measured constants — knob travels, envelope times, the envmod mapping, the square-shaper curve; the Devil Fish documentation (Robin Whittle) supplies the circuit behavior of the envelope/accent path, including the one place this kernel deliberately diverges from Open303. Every constant in the header carries its source.
One sample, in order
process() reads top to bottom as the signal path: pitch (with slide) →
envelopes → the C13 update → the cutoff sum → oscillator and shaper →
coupling high-pass → diode ladder → VCA → output coupling. Each stanza
below is one of those steps.
The file, as a schematic. Grey is what every clone has; red is what accent touches; amber is the cutoff CV that C13 leans on.
Slide: one coefficient, no special case
m_pitch += (m_pitch_target − m_pitch) · m_slide_coef
That is the entire slide implementation: a true RC lag (τ = the slide
parameter, stock 60 ms — Open303's slideTime) on the pitch target. The
gate logic makes it behave like the hardware: note_on with the gate low
snaps m_pitch to the target before retriggering (a fresh note starts in
tune); set_pitch with the gate held moves only the target, so the lag
glides and neither envelope retriggers. Legato is slide — which is why
the sequencer's gate-hold trick (see step_seq.h) needs no slide
wire of its own.
Two envelopes, both RC discharges
The Main Envelope Generator and the VCA envelope are the same primitive — one-pole rise, exponential decay — with different constants and one coupling each:
- MEG: 3 ms attack; decay = the
decayknob (200 ms–2 s)… unless the note is accented, in which case the hardware bypasses the pot and runs at ~200 ms (accdecay, a bend, adjusts this clock). Faster and hotter is half of what "accent" means. - VCA env: fixed — ~3 ms attack (the Devil Fish "Soft Attack" bend widens it to 0.3–30 ms), a measured 1.23 s decay with no sustain, chopped by a 2 ms release at gate-off (Open303 measures ~1 ms; 2 is click-free). No knobs on the hardware, so no knobs here.
C13: the wow, as three lines of code
The accent sweep circuit is a diode feeding a capacitor through the resonance pot. The kernel's model is exactly that sentence:
drive = accent_knob · note_accent · meg
if (drive > c13) c13 += (drive − c13) · charge // diode conducts: τ = 47 ms (47k·1µF)
c13 −= c13 · drain // always draining: τ ≈ 150 ms
The diode gating (if drive > c13) is the memory: between closely spaced
accents the drain doesn't finish, so the next accent starts from residual
charge and peaks higher — the build-up. The notebook measures the cutoff
peak growing ×1.94 across a run of accents and returning within ×0.998
once they stop. The cutoff contribution combines the capacitor voltage with
a direct MEG term reduced by it (Devil Fish: "~100/147 of the MEG minus the
capacitor voltage" — what rounds the first accent's curve):
res_mix = 0.3 + 0.7·min(resonance, 1) // the pot is ganged with resonance
acc_oct = 2.0 · res_mix · (0.4·max(drive − c13, 0) + c13)
Two things to note honestly. The RC time constants are component-derived; the sweep span (2 octaves) and the 0.4 direct weight are informed approximations, flagged as such in the header. And this is the kernel's one deliberate divergence from Open303, which models its accent path as a plain 15 ms leaky integrator with no across-notes memory. The A/B was done for real — Open303 built and rendered side by side — and the Devil Fish circuit description won because the memory is documented hardware behavior. The divergence is recorded in the header, not buried.
The cutoff sum: a measured law, not a mixer
envmod is not "envelope amount into a summing node." Open303 measured the
hardware's actual mapping (calculateEnvModScalerAndOffset), and the kernel
uses those regression lines verbatim. With c the knob's log-position
between the measured travel endpoints (302…2394 Hz):
scaler = (1−c)·(3.774·e + 0.737) + c·(4.195·e + 0.864)
offset = 0.0483·c + 0.2944
fc_eff = cutoff · 2^( scaler·(meg − offset) + acc_oct )
The offset term is the hardware's "gimmick": turning envmod up also
injects a counteracting DC shift, so the sweep's resting point moves down
as its depth grows — roughly 2/3 of the sweep lands above the knob position
and 1/3 below. That interaction is why the knobs feel like a 303 rather
than like a synth with the same ranges. Note acc_oct adds outside the
envmod scaling: in the circuit the accent sweep injects directly into the
cutoff sum, so accents quack even with envmod at zero.
The square that isn't
The 303's square is its saw pushed through a transistor shaper, and Open303
measured the resulting curve. The kernel takes the polyBLEP saw
(vco.h's machinery), makes a half-cycle-shifted copy, and applies the
measured shaper:
square = −tanh( 10^(36.9/20) · shifted + 4.37 )
That ~70× gain and the 4.37 bias produce the rounded, notched pulse whose
spectrum audibly differs from an ideal 50 % square. The waveform
parameter is a ramped blend between saw and shaped square, so switching
glides click-free.
The couplings at the edges: two high-passes
Two one-pole high-passes bracket the filter — 44.5 Hz before it, 24.2 Hz
after (both Open303-calibrated coupling corners). The post-filter one earns
its keep twice: it is the output coupling, and in vca warm mode it
absorbs the saturator's signal-dependent DC, which is exactly what the
hardware's coupling capacitor does.
The phase-2 VCA: distortion that tracks the envelope
vca clean is a multiply — bit-identical to phase 1. vca warm models the
one-transistor class-A stage as a slope-normalized biased saturator applied
after the envelope gain and before the output coupling (the hardware
order):
S(v) = ( tanh(d·v + b) − tanh(b) ) / ( d·sech²(b) ), d = 2.0, b = 0.3
Unity slope at zero means quiet notes pass essentially clean; the bias
means hot signals pick up even harmonics and compression. Because the
envelope sits inside v, the distortion tracks it: measured 5.4 %
difference-signal on quiet notes, 11.5 % on full accents, ~11 % second
harmonic on a full-scale sine with ~−4 dB of compression. d and b are
probe-calibrated informed constants — the header flags schematic-derived
values as an audition-time refinement, which is the honest state of things.
Per-unit spread: seed/tolerance
The house vco.h convention, applied to a whole voice: tuning trim, cutoff
scale, envelope times, slide and C13 RCs each take a deterministic per-seed
offset scaled by tolerance, and the oscillator receives the seed plus a
proportional imperfect amount. tolerance 0 is the nominal schematic,
bit-identical to an unseeded voice (pinned by test); an mc. stack with
different seeds drifts apart the way a wall of real units does.
The engineering ledger
- One object, not a modular kit. The C13 path touches the MEG, the resonance knob, and the cutoff sum; accent touches the MEG clock, the VCA gain, and the sweep. Decomposed into osc + filter + env externals, every one of those wires would be the user's problem and most patches would omit them. The couplings live between the blocks, so the object boundary goes around them.
- Measured constants over derived ones, where measurements exist. Open303's envmod law and shaper curve are adopted verbatim rather than re-derived from the schematic — they were measured against hardware, and re-derivation would add error, not rigor. Where Open303 simplifies (the accent memory), the circuit description wins instead. Each choice is sourced at the constant.
- The wow's parameters are honest approximations. Sweep span and the direct weight await a hardware-calibration pass; the shape (diode gating, two RCs, resonance ganging) is circuit-derived and pinned by the ×1.94 measurement. Flagged, isolated, waiting — the autowah pattern.
process_at()per sample. Pitch (note + tuning + slide) can change every sample, so the oscillator is driven at signal rate rather than through a control-rate frequency parameter. The slide RC would be audibly steppy any other way.
Checkpoint
A voice whose per-sample loop is the schematic's block diagram: slide as one RC coefficient plus gate logic, envelopes as discharge curves with the hardware's fixed interrelations, accent as a hotter-and-faster MEG plus a diode-gated capacitor whose leftover charge is the wow, a cutoff law measured off real hardware complete with its gimmick, a square that is a shaped saw because that's what a 303's square is, and a VCA whose warmth tracks the envelope because the envelope sits inside the saturator. Every constant carries its source, and the one divergence from the reference implementation is documented with its reason.