VOLTAreference

Reference / Design a sound

Analog character

Design a sound

Open in the workbench →
On this page · 4 sections

Add the small variations that give synthesized sounds character: drift, noise, detuning, pulse shape and changing attacks.

What it is#

Primitives that close the last gap between Volta's subtractive core and a faithful Minimoog Model D / The Legend voice:

  • drift depth [rate] — a slow, free-running, per-oscillator pitch wander in cents. The analog "fatness": stacked oscillators that never sit perfectly in tune with each other because each one's tuning drifts independently. Unlike a fixed unison detune (dead still) or an LFO (periodic), drift is a random wander, free-running (it keeps moving across notes, not reset per note), and independent per occurrence.
  • pink — a pink-noise (−3 dB/oct) audio source, the Model D noise switch's other position and the more musical analog hiss (equal energy per octave).
  • coloredNoise tilt [width] — a continuously dark-to-bright noise source. width = 0 is mono; width = 1 is a true independent stereo pair.
  • tol amount — per-voice component tolerance: a single random offset, latched once per voice, that multiplies the piped signal by a factor in [1-amount, 1+amount]. Where drift moves over time, tol is still within a note but different per voice — and stable across cycles: the same event slot draws the same value every time the pattern loops, like the same note landing on the same voice card of a worn polysynth (resistor/ capacitor tolerance is fixed per unit — it doesn't re-roll per note). Pipe it into any value port: cutoff, pitch, resonance, drive depth, gain. The amount may be live — tol (0.01 + age * 0.03) — which is the other half of the Prophet-Rev4 "Vintage" / CS-80 "slop" knob (drift's depth being the first). The random fingerprint stays latched; only its SCALE rides the knob, re-read at each note-on. See the live-amount note under the signature below.
  • hiss amount — an analog noise floor: sums a whisper of pink noise (gain amount) into the piped signal, the living-circuit hiss a perfectly silent digital path lacks.
  • supersaw count spread [blend] [width] [drift] — unison with the JP-8000 non-linear detune curve: the inner voices cluster toward the centre and only the outermost sit at the full spread, so the stack thickens around the fundamental instead of sounding hollow. Omitted controls default to blend 0.85, width 0.6, and drift 2.5Cents. The classic trance/EDM supersaw.

Worked examples: examples/songs/legend.volta (drift on the bass + lead, pink as the brass breath) and examples/songs/patina.volta (tol, hiss, supersaw, drift, and biased drive together on a pad + supersaw lead).

Surface#

-- Drift is a Cents modulation source: sum it into a freq port, like vibrato.
-- Each occurrence is its own independent wander, so write one per oscillator.
osc1 = saw (freq pitch + drift 3Cents)
osc2 = saw (freq (pitch + 7Cents) + drift 3Cents)   -- beats *and* wanders

-- Optional second arg is the wander rate in Hz (default 0.18 Hz ≈ 5.5 s period).
slowDrift = saw (freq pitch + drift 4Cents 0.1Hz)

-- Pink noise is a source, exactly like `noise`.
breath = pink |> hpf (const 3000Hz) |> gain 0.06

-- A dark stereo wash. Both channels are independent but replay-deterministic.
air = coloredNoise (-0.65) 1 |> hpf (const 400Hz)

-- Per-voice tolerance: each note's pitch & cutoff land a hair differently.
-- (One latched draw per voice — a "still but per-note" cousin of drift.)
v = sine (freq pitch |> tol 0.008)
      |> lpf (const 3000Hz |> tol 0.04) (const 1)
      |> drive (0.4 |> tol 0.1)

-- Noise floor: a whisper of pink noise summed into the voice.
warm = saw (freq pitch) |> lpf (const 2000Hz) (const 1) |> hiss 0.01

-- Supersaw: the JP-8000 curve clusters the inner voices, and each of the 7
-- voices gets subtle independent drift by default. A final 0 disables it.
fat = saw (freq pitch) |> supersaw 7 22
wild = saw (freq pitch) |> supersaw 7 22 1 0.6 4Cents

-- Envelope punch: an optional trailing 0..1 arg on `adsr`. The RC attack aims
-- past the target and clamps at 1 → a steeper, snappier front edge.
pluck = patch { source = saw (freq pitch), ampEnv = adsr 1ms 90ms 0.4 120ms 0.9 }

-- Sub-oscillator: a square `octaves` below the note pitch, mixed under the src.
weighty = saw (freq pitch) |> sub 1 0.6         -- one octave down at 0.6 gain
  • drift : Cents -> [Hz] -> Sig Control Cents. depth is the peak deviation in cents; rate is the characteristic wander frequency in Hz (default 0.18). A literal depth/rate takes the constant fast-path (byte-identical IR). Since C3 (synthesis-palette §4) the depth (and rate) may also be a live signal — drift (4Cents + space·6Cents) rides the analog-wander amount on a knob, like lfo's rate. A signal-valued depth is read per sample (the grid carries the unit wander, the depth multiplies it); a signal-valued rate is held from each rendered slice's first active sample and re-derives the absolute-time grid, identically in native and browser engines. Depth accepts cents, semitones or dimensionless magnitudes (cents); range tags are advisory, and semitones convert on both paths. Fixed depth and rate must fit engine precision; rate lanes reject. The result is Sig Control Cents, so adding it to a frequency bends it multiplicatively (an IR Detune, hz · 2^(cents/1200), FM, phase and ring modulation) — no special case. Add several freely (freq (pitch + 8Cents) + pitchMod + drift 2Cents): each addition is its own bend.
  • perlin [rate] : Sig Control Bipolar — gradient-noise wander, the smooth, aperiodic sibling of rand. With a rate (perlin 0.3Hz, perlin (1/8c), or a live control) it is real 1-D Perlin noise: a pseudorandom gradient per integer lattice cell of pos = t · rate, blended with the quintic fade 6f⁵ − 15f⁴ + 10f³, so the output is C² everywhere, zero at every lattice point, bounded by ±1 and band-limited around rate — measured at under 1e-3 of its energy above 8× the rate (engine/tests/perlin_noise.rs). A literal rate uses the closed form pos = t · rate, so a seek lands exactly where playing there would; a LIVE rate integrates position per sample, so turning the rate knob bends the wander instead of jumping it. Usable anywhere a control signal is, including a macro modulation graph (Macros and automation §1). Bare perlin is a different, older thing and is deliberately unchanged: a 2 Hz LfoSh under a 0.3 s Slew — a staircase with rounded corners, at one fixed rate. Thirteen corpus songs are built on it, so it keeps its exact sound; write perlin 2Hz when you want gradient noise at the same speed.
  • lorenz / henon : Sig Control Bipolar (nullary, like bare perlin) — chaotic attractor control sources (C2, synthesis-palette §3). A continuous, genuinely aperiodic modulator: lorenz integrates the 3-D Lorenz ODE (σ=10, ρ=28, β=8/3), henon iterates the 2-D Hénon map (a=1.4, b=0.3). Both free-run on absolute time and are per-voice seeded (drift's independence split — a chord's notes each get their own stream, a mono/glide line stays continuous). Bare, bipolar: pitchMod = lorenz * 8Cents, cutoff = const 800Hz + henon * 1200Hz. Unlike perlin (an LFO+Slew desugar) they are a stateful NodeKind::Chaos / sim::ChaosWorld, integrated in f64 in both engines so the chaotic trajectory tracks across native/browser (an f32 mismatch would diverge). Deterministic.
  • pink : Sig Audio Bipolar — a nullary source mirroring noise. Pipe it through filters/gain like any audio source.
  • coloredNoise : tilt -> [width] -> Sig Audio Bipolar — tilt is clamped to −1..1 and runs a one-pole low/high split around 1 kHz: negative values favor the low band, positive values favor the high band. width is clamped to 0..1 (default 0); above zero the node carries a real L/R pair, reaching fully independent streams at 1. Both streams are indexed hashes of the module seed, performance coordinate, node id, channel, and note-local sample index, so they never wrap and are invariant to render block boundaries. Tilt and width are live per-sample controls; a live width keeps both streams and filter histories advancing at zero width.
  • tol : Sig a u -> amount -> Sig a u — signal-first and unit-polymorphic (the near-1 factor is dimensionless, so the piped signal's rate/unit pass through unchanged). A constant amount bakes the random bounds [1-amount, 1+amount] and the draw happens once per voice at note-on. A live amount (a macro, an affine macro expression, or any control signal) instead lowers a modulator node into RandomUniform's span_node: the draw is unchanged — the same latched per-voice fingerprint u — and the value becomes mid + span·(2u − 1) around the same midpoint mid = (lo+hi)/2, with span read once at note-on. That is algebraically the same formula as the constant path when span = (hi−lo)/2, so a literal tol 0.02 renders bit-for-bit as before, and the scalar lo/hi still carry the expression's value at the macros' initials for any reader without live support. The amount is deliberately not re-read per block: a component tolerance that moved mid-note would be a vibrato, not a tolerance (the same "latch the shape, ride the amount" rule Surge's oscillator drift uses). Pure sugar: it lowers to Product(sig, RandomUniform[1-a, 1+a]) with stable: true — no new IR node, engine code, or Worklet. The stable draw drops the CYCLE coordinate from the rng key ((seed, synth, track, event, node_id)): the same event slot lands on the same component value every cycle, so a looping pattern sounds like the same worn circuit rather than a fresh resistor per note, while a chord's distinct events still spread — think of the event slot as a virtual voice card in a round-robin polysynth. (randomPerNote keeps the full per-note coordinate and redraws each cycle.) The draw is the same coordinate-seeded hash in both backends, so a tol'd voice replays bit-identically offline.
  • hiss : Sig Audio Bipolar -> amount -> Sig Audio Bipolar — signal-first over audio; dimensionless amount (typically 0.005–0.03) may be a constant, macro or live signal. Prepared constants clamp to 0–1; live values multiply per sample without that clamp, and knob changes affect sustaining voices. Pure sugar: lowers to Sum(sig, pink · amount) over the existing Noise/Product/Sum nodes — again no new engine/runtime code. The pink bed is the same seeded source as pink.
  • supersaw : Sig Audio Bipolar -> count -> spread -> [blend] -> [width] -> [drift] -> Sig Audio Bipolar — the exact signature and desugar of unison; the only difference is the per-voice detune SPACING. unison spreads the voices linearly; supersaw bends the normalised voice position by ≈^1.7 (the JP-8000 measured detune), so for 7 voices at ±12¢ the offsets are roughly ±1.9 / ±6 / ±12¢ instead of the even ±4 / ±8 / ±12¢. blend (side-vs-centre level) and width (stereo) still apply and are keyed to voice order, not the curve; their defaults are 0.85 and 0.6 respectively.
  • jp8000 : Sig Audio Bipolar -> detune -> mix -> [width] -> [drift] -> Sig Audio Bipolar — the thesis-exact Szabo model (Adam Szabo, "How to Emulate the Super Saw", 2010) as a separate builtin: 7 fixed voices at the published (slightly asymmetric) frequency-offset fractions, the 11th-order polynomial detune curve (subtle through the lower half of the knob, violent near the top), and the measured centre/side mix law h(z) = −0.55366·z + 0.99785, m(z) = −0.73764·z² + 1.2841·z + 0.044372 instead of unison's √n compensation — the stack genuinely thickens as mix opens, like the hardware. detune/mix are 0..1 and must be literals (the polynomial and the ratio→cents fold happen at compile time); width may be live and defaults to 0.6. Use supersaw when the spread has to ride a macro. Same re-lower-per-voice desugar (seeded phases, per-voice drift/tol draws). Finally, the desugar appends the thesis's last element: a keytracked highpass at the played note (NoteParam pitch → PitchToHz → hpf, Q = 1/√2) across the summed stack. The seven saws sit at ±0.11·f0 and beat, so their intermodulation products pile up below f0 — a rumble that widens with detune and is what makes a naive supersaw stack muddy in the low mids. The JP-8000 highpasses it away at the fundamental; so does this. The corner is Butterworth (no resonant bump on top of the note), 12 dB/oct, and tracks per note. It is unconditional — there is no bypass, because the hardware has none. Added 2026-08-01 (DSP audit §B.5); goldens re-blessed for acidwall / colossus / supernova.

The three stack functions retain their units and named options through aliases, partial applications, returned functions and containers. unison/supersaw voice counts must prepare to whole numbers from 1 to 16; a live count cannot change the allocated voices. Spread is the full outer-voice interval: 24st and 2400Cents both place three voices an octave below, at, and above the played pitch. Spread and drift accept cents, semitones or bare cents, including live controls; semitones convert to cents. Blend, width and JP detune/mix are scalar controls and reject physical units. Each live control is shared across the stack; each voice constructs its own oscillator state.

§8 — Envelope punch. adsr grows an optional trailing punch (0..1, default 0): adsr attack decay sustain release [punch]. The attack aims its RC rise past the target — an overshoot of 1 + punch·0.5 (max 1.5× at punch = 1, landing in circuit-modeling §5.4's 1.2–1.5× range at musical punch settings) — with the output clamped at 1. The curve therefore crosses full scale early and rides the clamp for the rest of the (still attack-long) segment, so the front edge is steeper and snappier — the "punch" of a hardware envelope. The attack still ends at exactly 1, so decay departs from 1 unchanged. (The mapping was re-ranged from the earlier 1 + punch·0.3 per circuit-fidelity Stage F on 2026-07-21 — a retroactive change that hardens the front edge of already-saved punchy patches; goldens re-blessed.) punch = 0 takes the plain-RC branch — textually the original formula, so every existing patch is bit-identical (the IR field is #[serde(skip_serializing_if = 0)], so goldens don't churn). A live punch expression latches at note-on/retrigger with the other four envelope shape values, clamped to 0–1, on every Rust engine target.

§9 — Sub-oscillator. sub octaves [level] — signal-first over audio — mixes a square sub-oscillator octaves (a whole 1..4) below the played note pitch under the piped source, at dimensionless gain level (typically 0..1, default 0.5). The octave count must be prepared: macros and live signals are rejected. The level may be live and reads per sample; a partial call can supply it with {level = expression}. Pure lowering-sugar: it expands to Sum(src, Osc(square, PitchToHz(PitchAdd(pitch, −12·octaves))) · level) over the existing oscillator / pitch nodes — no new IR node, engine code, or Worklet, so it works identically native and in the browser. The sub tracks the played note pitch (via NoteParam pitch, like keytrack), not the source's live frequency modulation — the classic octave-divided sub. Because the frequency comes from the note (not the source node), a sub'd voice needs no access to the source's internal freq signal; that's what makes the pipe-position sugar expressible without engine plumbing.

§10 — Soft sync & saw corner rounding. Two oscillator-modifier refinements, both folding into the wrapped Osc node (siblings of sync), both rendered by the one Rust engine on every target.

  • sync master ["hard"|"soft"] — the existing hard sync grows an optional trailing mode string. "hard" (default, absent) resets the slave's phase every master cycle — the aggressive sync-lead scream. "soft" only resets the slave when it is already within the leading quarter (SOFT_SYNC_WINDOW = 0.25) of its own cycle, so the injected reset discontinuity is small by construction and the timbre is gentler — it pulls the slave toward lock near rational frequency ratios instead of forcing a restart each cycle. On a smooth (sine) slave, soft sync's max reset jump measures ~34 % below hard's and its alias floor ~93 % lower. IR: Osc.sync_mode (skip_serializing_if = None → hard-sync IR is byte-identical). Independently, the triangle hard-sync reset now also gets a 2-sample BLAMP (a blamp2, the integral of blep2) on its falling→rising slope corner — PolyBLEP only fixed the value step, leaving the slope kink to alias. Saw/square have no reset slope discontinuity, so they're guarded out and their hard sync is bit-identical to before. The sine reset gets its own treatment — see below.
  • soften amount — Juno-DCO reset-corner rounding, over any audio source. amount (0..1, folds to a constant) runs the source through a one-pole reset-RC (~10–300 µs), rounding the sharp reset edge — a finite-reset-time character. It rolls off the reset's top-octave alias energy (a moderate soften 0.05 on a 440 Hz saw drops energy above 10 kHz ~75 % while the fundamental and first harmonics move well under 2 %) without dulling the body; the honest "bleed the reset residual out over tau and subtract it from the ideal edge" recipe reduces to exactly this one-pole reconstruction filter. At the top of the range it warms audibly — soften 0.6 is a 184 µs RC (≈ 865 Hz), a real one-pole low-pass, not just a corner.

    Two IR spellings, one filter. A bare oscillator folds the amount into Osc.soften (skip_serializing_if = 0 → default-off oscillators byte-identical; soften = 0 stays on the plain render path). Anything else — as3340 … |> soften, a summed/driven chain — gets the identical one-pole as a standalone Soften { input, amount } node, so the two paths render bit-for-bit the same samples (pinned by soften_lifts_off_the_bare_oscillator_onto_any_source in engine/tests/dsp_validation.rs). That is what makes the canonical Juno recipe — a stable DCO and the reset-RC rounding on one voice — expressible:

    junoish = patch { source = as3340 "saw" (freq pitch) 0.5 0.15 |> soften 0.5, gain = vel }

Sine hard sync — the full antialiasing residual (2026-08). A sine's phase reset is a discontinuity of infinite order: value, slope, curvature and every higher derivative jump at once, so no finite stack of BLEP/BLAMP corrections is exact (a BLEP+BLAMP pair is exactly that correction's ω-Taylor series truncated after two terms). The sine sync path therefore uses the closed-form residual of La Pastina & D'Angelo, A general antialiasing method for sine hard sync (DAFx-22, paper 3) — dsp::sine_sync_residual. Writing the synced sine as a free sine plus a train of switch functions and filtering with a symmetric FIR kernel of support [−ε, ε] turns the correction into a finite sum of residuals R = h*f − f, correct to all orders, each touching only the samples straddling its reset.

Two things fall out of the measurement (dsp_validation.rs::sine_hard_sync_full_ residual_lowers_alias_floor, 48 kHz, coherent FFT 65536):

  • Going from the truncation to the exact residual under the same 2-sample triangular kernel is worth only 0–2 dB (it grows with the slave frequency).
  • Nearly all of the win is the kernel: swapping in the same cubic B-spline the free-running oscillator moved to (poly_blep4) is worth 9.3–10.1 dB of alias energy and 8.0–9.4 dB of worst-case alias, uniformly across master 110–880 Hz × slave 300 Hz–9.6 kHz.

The cubic kernel's support reaches one sample behind the reset, which has already been emitted; that far lobe is dropped rather than delaying the path (measured cost 0.25–0.8 dB). Sine sync therefore adds no latency and stays sample-aligned with every other oscillator in the patch — unlike the wave-1 ADAA2 shapers, nothing here needs a delay-compensation entry. Above BLEP4_MAX_DT (slave ≥ 12 kHz at 48 kHz) the 4-sample support spans more than a slave period, so the path falls back to the triangular kernel — the same ceiling poly_blep4 gates on — where the exact residual still beats the old truncation by 1.9–2.0 dB. Unsynced oscillators are untouched (bit-identical; the residual is reachable only from the synced Osc arm), and saw/square/triangle sync is unchanged: their switch function is constant (saw, square) or linear (triangle), so the derivative ladder terminates and a BLEP — plus a BLAMP for triangle — is already the exact residual. The one golden this moved is sync_hard (peak/RMS within 0.00 dB).

Steady saws and pulses use a sinc-BLEP. A free-running saw (no sync, no soften) or pulse whose frequency — and width — hold for the whole voice block renders through engine/src/sinc_blep.rs (evolve run volta_osc #41): a naive waveform from a fixed-point phase plus a 16-tap Kaiser-windowed band-limited-step residual per edge, the edges found with integer arithmetic and the next block's edges predicted so there is no latency. On the evolve harness (coherent FFT, 48 kHz) alias suppression is capped at 100 dB (worst 12–20 kHz alias −68 dB) against 78.6 / 43.4 dB for polyBLEP4, high-frequency droop 0.04 dB against 6.0 dB, at about a tenth of the cost. The edge now carries the band-limited Gibbs overshoot (a saw peaks near 1.16). Blocks whose frequency moves (FM, glide, vibrato, drift) and square keep polyBLEP4; a note that changes path mid-note (a glide starting or ending) shapes the edges either side of that block boundary with different kernels, and that switch point depends on the callback size. The phase is kept on a 2^-24-cycle grid so a steady note renders the same for any callback partition. Tables exist for 22.05/32/44.1/48/88.2/96/176.4/192 kHz; other rates keep polyBLEP4.

-- Gentle, vowelly soft-sync lead (fifth-up slave, locked to the note pitch).
lead = saw (freq (pitch + 7Semitones)) |> sync (freq pitch) "soft" |> lpf (const 2600Hz) (const 0.4)

-- Juno-DCO saw stab with the glassy reset edge rounded off.
stab = saw (freq pitch) |> soften 0.4

Worked examples for §8–§10: examples/songs/character.volta (punch bass, square sub, through-zero cross-bell, soft-sync lead, soften'd DCO stab).

§11 — Modeled VCA gain cell (vca). A per-voice gain stage that models the OTA/Blackmer character behind why hardware polys sound "glued" — Circuit Fidelity Stage D, implementing Circuit Modeling §5.3. "A VCA is never just a multiply."

-- The control is any control-rate gain — an adsr, a filtEnv field, an LFO.
lead = sine (freq pitch) |> vca (adsr 2ms 60ms 0.7 120ms) 0.4    -- character defaults to 0.4
-- character = 0 is EXACTLY `sig · control` (a plain product) — the debug baseline.
clean = sine (freq pitch) |> vca (adsr 2ms 60ms 0.7 120ms) 0.0
  • vca : Sig Audio Bipolar -> control -> [character] -> Sig Audio Bipolar — signal-first like drive. control is any control-rate gain signal; character (Normalized, default 0.4) grades in the imperfections. Above 0 the plain product is blended toward the cell by blend = min(1, character·2.5) (fully engaged by character ≈ 0.4). It lowers to a dedicated NodeKind::Vca { input, control, character } (no golden churn — new node, only emitted when used).

The cell adds three behaviours, per the §6 null-to-ideal discipline (all null to the plain product at character = 0):

  1. Headroom tanh — y = tanh(x·g / H)·H, where the headroom H shrinks with character: H = 6.0 − 5.0·character → 4.0 at character = 0.4 (audibly subtle on normal levels |x·g| ≲ 1, real compression on hot ones), down to 1.0 at character = 1. The small-signal limit is the plain product.
  2. Control feedthrough — a fast control envelope leaks a thump of the control into the audio: leak = feed · character · clamp(dg/dt / 1000, ±4), i.e. the high-passed control (its discrete derivative) normalised so a ~1 ms transition thumps at unity. feed is drawn once per voice from the coordinate PRNG at key (seed, synth, track, cycle, event, node_id, "vca-feed"), mapped uniform-in-dB over [−70, −50] dBFS (like As3340's chip identity). Fast envelopes thump; slow ones are inaudible — and the per-voice 10 Hz DC blocker strips the sub-Hz tail of a slow ramp entirely.
  3. Gain-tracking noise floor — white · character · VCA_NOISE_DEPTH · (0.25 + 0.75·g) (VCA_NOISE_DEPTH = 0.00006), read from the shared seeded white ring (like the Juno BBD hiss). The floor rides the gain (≈−84 dBFS at full gain + character): the (0.25 + 0.75·g) factor runs 0.25 at silence up to 1.0 at unity gain.

Deterministic (a pure function of the coordinate — two renders are bit-identical) and stereo-aware (the control scales both channels; the noise floor is decorrelated L/R). Constants live in engine/src/voice/modfx.rs (VCA_BLEND, VCA_HR_HI/VCA_HR_SLOPE, VCA_FEED_REF/VCA_THUMP_CLAMP, VCA_FEED_DB_*, VCA_NOISE_DEPTH). Rendered on every target.

Measured (engine/tests/dsp_validation.rs, at 48 kHz): character = 0 nulls to the sig * control product at −∞ dBFS (bit-exact); a 1 ms-attack control leaks a note-on thump of RMS 2.0e−4 vs 7.7e−6 for a 200 ms attack (a 26× ratio — the DC blocker strips the slow one); the sustain noise floor is 2.13× louder at gain 0.8 than at 0.2 (the 0.25 + 0.75·g law predicts 2.125×); two renders are bit-identical.

refine.rs emits an advisory DriftRange warning when depth ∉ (0, 50]¢ or rate ∉ [0.01, 5]Hz — beyond that band it reads as a detune or a vibrato, not drift. Advisory only; it still compiles and plays.

§12 — TB-303 accent circuit (accent). accent amount [threshold] [reso] is the 303's accent-sweep CV as a per-voice source (Robin Whittle's circuit analysis). A note whose velocity ≥ threshold (default 0.8) injects charge into an RC network. Unmarked pattern steps carry the 0.85 base velocity (Velocity and accents), so the unmarked notes are the accents — mark the ghost notes down (a1=0.6 plays at 0.6 × 0.85), which also plays them quieter, exactly like the hardware's unaccented steps. The charge rises toward 1 during a ~50 ms injection window (τ ≈ 47 ms — the hardware's diode + 47k into the 1 µF cap) and discharges slowly (τ ≈ 400 ms) afterwards, so back-to-back accents pump the cap higher — the growing "wow" of consecutive accented 16ths. Output is charge · amount (Normalized 0..1); amount may be live (a knob).

The hardware routes ONE accent pot to both the VCA (RC-softened louder) and the filter CV (the sweep), so wire the same signal to both destinations:

acid303 = patch {
  filtEnv = adsr 2ms 150ms 0.2 100ms,
  source  = saw (freq pitch)
    |> diode (const 260Hz + filtEnv * 900Hz + accent 0.9 * 2600Hz) (const 0.82) 0.35,
  gain    = vel * (1 + accent 0.9 * 0.7)
}

The inter-note charge carry needs a persistent mono voice — set glide (the 303 is mono; glideMode = "always" + glideCurve = "exponential" is the authentic feel). A poly patch still sweeps on each accented note, it just can't build charge across notes (each voice is a fresh cap). The glide voice only exists when the patch reads pitch — a glide on a patch whose oscillators ignore the note is dropped, and every note gets a fresh cap. Renotes are detected by the env anchor moving; node state deliberately survives them. Demo: examples/songs/acidwall.volta.

Resonance-coupled sweep (opt-in reso). The 303's Resonance pot has a second gang in the accent-sweep network, and an accented note forces the filter envelope's decay to its shortest setting. accent a t { reso = r } (live 0..1 — pass the filter's own resonance expression) switches the node to that circuit: the sweep input is the accented note's short envelope decay (τ = 60 ms, restarted at each accented onset, zero on an unaccented one), and it passes a one-pole RC whose τ runs 4 ms → 160 ms with r (ACCENT_MEG_TAU, ACCENT_RESO_TAU_LO/HI in engine/src/voice/modfx.rs). Low r follows the envelope — a sharp smack that does not build; high r smooths it and accumulates over back-to-back accents. Without reso the node is the gate-window model above, byte-identical in IR and audio.

res = macro 0.7
acid = patch {
  glide = 45ms, glideMode = "always",
  filtEnv = adsr 2ms 170ms 0.1 110ms,
  source  = saw (freq pitch)
    |> diode (320Hz + filtEnv * 1100Hz + accent 0.9 0.8 { reso = res } * 3200Hz) (0.55 + res * 0.35) 0.3,
  gain    = vel * (1 + accent 0.9 0.8 { reso = res } * 0.8) * 0.9
}

Measured offline (engine/tests/analog_wins.rs, four accented 16ths at 120 bpm on a glide voice, the CV read as a sine's gain): reso = 1 peaks 0.21 → 0.33 → 0.39 → 0.42 — the analytic model's numbers — and the tail after the last accent decays with τ = 160.0 ms; reso = 0 peaks 0.82 on every step (spread < 1 %). A high reso therefore also lowers the sweep's first peak; raise the destination depth to compensate. The constants are circuit-informed choices, not a measured unit. Demo: examples/songs/accentcircuit.volta.

§13 — 1/f parameter noise (flicker). flicker rate is a bipolar control whose spectrum falls ≈ 10 dB per decade — the flicker noise of transistor and OTA circuits, which in hardware jitters parameters (a cutoff, a pitch, a level) rather than being heard as hiss.

pad  = patch { source = saw (freq pitch), cutoff = 1300Hz + flicker 6Hz * 220Hz }
lead = patch { source = as3340 "saw" (freq pitch + flicker 3Hz * 6Cents) }
  • flicker : rate -> Sig Control Bipolar. rate is the band's top corner — prepared Hz, seconds, milliseconds or cycles (a live rate is refused; scale the OUTPUT with a live control instead). Ten equal-amplitude layers of seeded value noise at rate, rate/2, … rate/512 are summed; equal power per octave is a 1/f spectrum over that nine-octave span. RMS ≈ 0.3, clamped to ±1 (FLICKER_OCTAVES, FLICKER_GAIN in engine/src/voice/noise.rs).
  • Deterministic like noise. Note-local and coordinate-seeded: every grid value is rng::stream_bipolar of (seed, stream, layer, cell), so a render replays bit-identically, is invariant to block size and seeks, and a chord's notes jitter independently. The per-voice cache (NodeState::Flicker) only saves hashes. For one transport-wide stream, bind it in an @shared control (Bring a sound to life § Share one control instance).
  • Measured (voice::noise::tests): the PSD falls 10.5–11.0 dB per decade mid-band (40–80 Hz vs 400–800 Hz and 60–120 vs 600–1200 at an 8 kHz corner; the edges bend), RMS 0.29, the clamp touches 0.02 % of samples, and a fresh cache at any index reads the continuous run's value bit for bit. The vca and OTA svf noise floors still use white noise (Analog Wins §3.2).

§14 — Power-supply sag and thermal drift (recipes). Both need state shared across voices, which existing primitives supply:

  • Sag — motion.psuSag track depth (import "mod" as motion) is 1 − follower track 2ms 150ms · depth. Pass the track's OWN name: its post-strip level — the summed load of its voices — is read one 128-sample block late, which is what makes the self-read feedback-safe (Make one sound follow another § Causality). Multiply it into gain, cutoff or frequency. Measured (psu_sag_pulls_level_down_with_the_summed_voice_load): at depth 0.6 a single saw keeps 0.92× of its level, a four-note chord 0.83×.
  • Thermal drift shared across voices — one @shared slow control that every voice reads:

    @shared "thermal-v1"
    thermal = flicker 0.05Hz
    pad = patch { source = saw (freq pitch + thermal * 4Cents) }

A supply model driven by summed envelope current rather than audio level is not built (Analog Wins §5).

How it's implemented#

One Rust engine (engine/) renders these on every target — desktop, the browser AudioWorklet (engine-wasm/), and the plugin.

  • Compiler
    • compiler/src/builtins.rs — BuiltinKind::Pink (nullary, Sig Audio Bipolar) and BuiltinKind::Drift (Arrow [Hole, Hole] -> Sig Control Cents; a literal arg folds to a constant, else lowers to a modulator node — C3; rate defaults to 0.18). lorenz/henon lower to NodeKind::Chaos, not the bare-perlin LFO+Slew desugar (C2). perlin is registered twice, on the orbit precedent: signal_source() answers the bare, nullary use with the legacy desugar, and the builtins() map answers perlin rate with the same BuiltinKind::SignalSource(SigShape::Perlin), which lower_app turns into NodeKind::Perlin. It carries no parameter-schema row, because a schema needs one calling shape and this name has two.
    • ir/src/ir.rs — NodeKind::Perlin { rate_hz, rate_node? } (the gradient-noise node; engine/src/voice/noise.rs::eval_perlin, normalized by PERLIN_GAIN = 2 because 1-D Perlin's extreme with unit gradients is ½). NodeKind::Drift { depth_cents, rate_hz, depth_node?, rate_node? } (the _node fields are Option<String>, skip-if-None so a literal-drift song stays byte-identical — C3); NodeKind::Chaos { chaos, rate_hz } (C2); and noise carries color, tilt, and width. Every default field is omitted, so a white-noise node still serialises as bare {"op":"Noise"}.
    • compiler/src/lower/expr.rs — pink → Noise { color: Pink }; noise → Noise { color: White }; drift → a Drift node typed Control/Cents.
    • compiler/src/refine.rs — Drift interval is [-|depth|, +|depth|]; the DriftRange advisory above.
  • Engine (engine/src/voice/noise.rs, dispatched from engine/src/voice/eval.rs)
    • white/colour (render_noise_source) — each sample is addressed directly through rng::stream_bipolar(seed, stream, note_local_sample). There is no ring and therefore no repeat period. A 1 kHz one-pole split (colored_noise::Shelf, fitted per sample rate to the analog split, so the colour is the same at every rate up to Nyquist) applies the tilt; stereo width blends toward an independently keyed right stream.
    • pink (colored_noise::PinkCoeffs) — the indexed white stream runs through eight parallel one-pole sections fitted per sample rate to exact −3 dB/oct (19 Hz to Nyquist, at the historical Kellet level), with per-node state. Deterministic and non-repeating.
    • drift (eval_drift) — a function of absolute time (the global transport clock), not the note onset: control points are hashed at rate · 2 per second — rng::draw_bipolar("drift", salt, node_id, k) — and linearly interpolated. A per-voice salt, drawn from the voice's ORIGIN coordinate (untouched by renote), keeps a polyphonic chord's notes wandering independently while a mono/glide voice stays continuous. Live depth (C3): when a depth modulator is wired the grid carries the unit wander and the depth is a per-sample multiply; a live rate is read once per block and re-derives the grid.
    • chaos (eval_chaos, lorenz/henon, C2) — a per-voice sim::ChaosWorld (engine/src/sim.rs) built lazily on the first eval from the performance coordinate, free-running on absolute time like the flock/orbit sims. The world integrates the attractor in f64 so the trajectory is reproducible. Determinism is pinned by chaos_sources_modulate_and_are_deterministic in engine/src/offline.rs.

Notes & limits#

  • Alias figures name their band. The shared linear-phase half-band FIR for non-feedback 2× oversampling (engine/src/resample.rs) meets its spec — flat to 0.45 and at least 90 dB down from 0.55 of the oversampled rate's Nyquist, i.e. 21.6 kHz and 26.4 kHz at a 48 kHz output. Its transition band therefore straddles the 24 kHz output Nyquist, and a harmonic just above 24 kHz is only partly rejected and folds back between 21.6 and 24 kHz. That energy is inaudible but counts in a full-band alias measurement, so a full-Nyquist figure and a ≤ 20 kHz figure for the same render differ; a quoted alias number says which band it covers. Feedback loops use the allpass-IIR half-band decimators in engine/src/voice/filters.rs instead, for their near-zero in-loop latency.
  • Free-running across glide. A persistent glide voice (Glide between notes §5) keeps one salt across every renote (the voice's origin coordinate never moves), so the trajectory is seamless. A held / sliding line keeps wandering with no reset.
  • Independent per occurrence. Each drift node is its own stream (the node id enters the hash). Write one drift per oscillator to make the stack disagree; a single shared drift would move them in lockstep.
  • Determinism. Each grid value is a pure rng::draw_bipolar, so an offline render (Change music while it plays) replays the same trajectory. The grid index is taken from absolute transport time, so a free-running drift is not guaranteed identical across two separate live sessions (it is within a render) — by design, drift is the one modulator that doesn't reset to a note coordinate.
  • Drift is a Cents source for pitch ports. Filter ports take Hz, so cutoff = … + drift … doesn't type-check yet; a Hz drift overload is a deferred follow-on (Analog character §7).
  • Noise streams are coordinate-separated. Distinct nodes and L/R channels receive distinct indexed streams, while the same performance coordinate replays bit-identically. Pink filters its node's own white stream.
  • tol vs drift. drift moves (a per-occurrence wander over time); tol is static per note (one latched offset, but a different one each note). Use drift for oscillator fatness, tol for the cutoff/level/drive scatter that makes a polyphonic chord sound like several slightly-different voices. Each tol is its own draw (the RandomUniform node id seeds it), so two tols on the same value disagree — write one per parameter you want to vary.
  • Envelope times can sample tol. ADSR times accept control expressions and latch at note-on/retrigger. adsr (10ms |> tol 0.1) … retains that note's timing while it sounds; later notes sample their own prepared random draw.