The model
Plenty of Reeds is modeled, not sampled. There are no recordings of a single note inside it — each note is computed from a chain of physical stages, one per voice, then summed and shaped on the bus. The DSP lives in dsp/ as header-only C++20 with no JUCE includes. This page walks the chain stage by stage, and names the source file behind each one.
- hammer + felt→
- Reed→
- Pickup→
- master gain→
- DC block→
- Cabinet→
- tremolo→
- + 60 Hz hum→
- mono out
Hammer + felt
dsp/Hammer.h · dsp/Voice.hThe hammer is a Hertzian contact model rather than a fixed pulse. The force follows a power law F ∝ pos^p over a triangular contact envelope, with the rise and fall deliberately asymmetric — real felt compresses faster than it decompresses, so rise takes a smaller fraction of the contact than fall (rise_frac runs ~0.45 for soft strikes down to ~0.35 for hard ones). A symmetric half-sine pulse is what makes a synth attack detectable; the asymmetry is the fix.
Felt hardness sets the exponent p from 2.0 (soft pad) to 2.8 (stiff felt), so hardness changes the shape of the impact, not just its length. Contact time also falls with velocity and hardness — roughly 1.1 ms for a soft slow hit down to 0.3 ms for a hard fast one. Peak force is intentionally non-monotonic: past about velocity 0.75 it ramps sub-linearly because the felt bottoms out mechanically, modeling the 200A’s “sweet spot” where harder playing gets louder without getting brighter.
Voice.h wraps the hammer with two felt/attack layers. A broadband mechanical-noise burst (~14 ms, low-passed felt body + chiff, with a ~5 ms swell before it decays) models the key/action thunk. An attack transient shaper then runs a low-pass whose cutoff opens from ~1.4 kHz to ~14 kHz over ~13 ms plus a matching gain swell, so the leading edge is darker and softer and the combined peak lands later instead of an instant click.
Reed — modal bank
dsp/Reed.h · dsp/WurliData.hThe reed is a flat steel cantilever, modeled as a sum of resonant band-pass modes (one Biquad per mode) whose center frequencies and decay times come from the Wurli profiles in WurliData.h. Mode k is stretched by an inharmonicity term (6.5e-5), which puts the higher partials sharp — by design, since a flat reed carries more residual stiffness than a quasi-harmonic tine.
Velocity drives timbre, not just level. A tilt exponent kVelTilt = 0.38 fades the upper modes faster as the strike softens, so soft notes stay clean and hard notes bark. That value is calibrated against the Pifinger 200A Low/Mid/High sets: the real reed grows roughly +18 dB of upper-harmonic energy from soft to hard, and 0.38 reaches about +15 dB — close, with the remaining gap a known modal-ceiling limit of the finite mode count.
Each mode also gets a tiny independent detune (up to ±0.40 cents, scaled down at low frequencies where beating is most audible) so sustained notes develop slow beating between partials instead of holding a perfectly static chord. The detune is seeded deterministically per pitch, computed once at note-on — it costs nothing in the audio loop — and the whole bank is energy-normalized after the velocity tilt.
Pickup — electrostatic v ⁄ (d − x)²
dsp/Pickup.hThe 200A pickup is capacitive, not magnetic. The reed is one plate of a variable capacitor against a biased plate; the audio follows the rate of change of charge, giving the law
y(t) ∝ V_bias · ẋ(t) / ((d − x(t))² + ε²)
Two things make hard strikes bark: the velocity factor ẋand the asymmetric squared denominator. A magnetic pickup (a function of position only) would give the warmer Rhodes growl; this electrostatic form gives the Wurli’s harder, more vocal edge. The soft ε² term in the denominator keeps the operating point from collapsing on hard hits, so there’s no kink from a hard clamp.
The non-linearity is evaluated at 2× oversampling (a polyphase IIR half-band) to control aliasing, and the velocity estimate is normalized so output level doesn’t depend on the host sample rate. After decimation, two one-pole low-passes (anchored near 6.5 kHz and 9.5 kHz at A4) model the amp and 4×6″ oval-speaker rolloff — darker than a Rhodes on purpose. setNoteBrightness() raises those cutoffs for notes above A4 so high notes keep more harmonics, while notes at or below A4 are left untouched.
Cabinet — preamp + body + speaker
dsp/Cabinet.hThe cabinet stage colors the summed bus the way the 200A’s internal amp and oval speaker in a plywood box would. It runs three elements in series, in that order — distortion before EQ.
First an asymmetric tanh saturation with a small DC bias offset. The offset makes saturation harder on negative excursions than positive, adding a 2nd harmonic on top of the dominant 3rd — the signature of a discrete-transistor (solid-state) preamp, not a tube. It is bounded by construction so polyphonic peaks can’t blow up.
Then three body resonances from the plywood/tolex cabinet — peaks near 80 Hz, 250 Hz, and 600 Hz. Finally speaker shaping: a +3 dB bump near 200 Hz, a −6 dB notch near 3 kHz (paper-cone breakup), and a roughly −12 dB/oct rolloff above ~5.6 kHz for the oval speaker’s bandwidth limit. Every element is a stateful biquad, so the chain order is load-bearing.
Tremolo — amplitude only, asymmetric triangle, no std::sin
dsp/Tremolo.hThe 200A vibrato is really a tremolo: amplitude modulation, never pitch. The LFO is an asymmetric triangle driven by a phase counter (rise spends ~52% of the period), deliberately not a std::sincall — the real unit’s relaxation oscillator has an edge a pure sine lacks. A one-pole RC low-pass (~16.5 Hz) then rounds the corners into the shape the modulator’s bias network actually produces.
The triangle drives an exponential VCA, gain = exp(depth · (lfo01 − 1)), so the peak sits at unity and the trough at exp(−depth)— a linear law would sound like a synth. The rate centers near 5.5 Hz and follows a slow sub-Hz random walk (±~2%) so consecutive cycles vary slightly and the tremolo “breathes” instead of ticking metronomically. The quadrature SineOsc in this file is used only for the 60 Hz hum source, not for the tremolo.
Engine — 64 voices and the bus
dsp/Engine.hThe engine holds 64 voices, one per reed on a real 200A, and allocates them per note: reuse the matching slot, then a free slot, then steal the oldest. Sustain comes in three forms — manual toggle, CC64, and a continuous half-pedal value — and the macro knob (branded Bark) maps one position to pickup drive, felt hardness, and hum level with musical curves.
On the bus the voice sum is scaled by master_gain(0.65, calibrated against the 1snob suite’s reference loudness so the Wurli sustain sits naturally in a mix), passed through a DC blocker, then the cabinet, then the amplitude tremolo, with a subliminal 60 Hz bias-supply hum added last. The output is dual-mono by design — the 200A is a mono instrument, so there is no built-in chorus, panning, widening, or pitch bend.