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.

Signal-flow diagram of the 303 voice with the couplings highlighted: the accent bus fanning to the envelope clock, the C13 charge path, and the VCA; the C13 capacitor feeding the cutoff sum

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 decay knob (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.