Feed part of a signal back through a processor to create resonances, repeating textures and evolving tones.
What it is#
feedback and feedbackAt let a patch close its own signal loop — the one sanctioned exception to Volta's feed-forward audio graph. The binder fb is the loop's own output, delayed: by the minimal delay (feedback — one sample on the native engine) or by a tuned fractional-sample line (feedbackAt time — Karplus-Strong when time = 1 / freq pitch). Because the loop body is ordinary Volta, the regeneration path can contain filters, saturators, and delays: dub echoes that darken every pass, plucked strings with a custom damping law, self-patching drones.
Surface#
feedback : (Sig Audio Bipolar → Sig Audio Bipolar) → Sig Audio Bipolar
feedbackAt : time → (Sig Audio Bipolar → Sig Audio Bipolar) → Sig Audio Bipolar
A dub echo — every pass re-enters the filter and the saturator, so each repeat is darker and dirtier than the last:
skank = saw (freq pitch) |> hpf (const 300Hz) 1
source = feedback \fb ->
skank + fb * 0.6 |> lpf (const 1.2kHz) 2 |> drive 0.3 |> delay (3/8c) 0
Karplus-Strong — the same binder, but the loop delay IS the note period:
pluckEnv = adsr 1ms 20ms 0 10ms
source = feedbackAt (1 / freq pitch) \fb ->
(noise * pluckEnv) + fb * 0.985 |> lpf (const 6kHz) 1
- The lambda may be written bare and trailing (
feedback \fb -> …) or parenthesized; it binds exactly one name. feedbackAt's time folds likedelay's (Cycles literal → seconds at the module tempo) or stays a control signal (1 / freq pitch). It is lowered before the binder exists, so it cannot depend onfb.- The binder parameter types as
Sig Audio Bipolar, sofb * macroKnobkeeps the audio type.
Semantics#
- Loop delay.
feedback= ONE SAMPLE;feedbackAt time= a fractional-sample linearly-interpolated read of the loop ring, clamped ≥ 1 sample and ≤ 2 s. - Soft limit. Every fed-back sample passes a soft limiter: identity in [−1, 1], a tanh knee capping at ±2 (deliberately not a plain tanh — that would damp a decaying tail; a KS loop at 0.985 passes untouched). NaN/inf flush to 0. The node's audible output is the limited value — a runaway loop (gain ≥ 1) is a bounded ±2 drone, never a blowup.
refine.rsproves the [−2, 2] interval and warns (FeedbackGainRange) when a constant loop-gain factor ≥ 1. - The loop is mono. A stereo signal entering the loop downmixes
(L+R)·0.5, like every stateful collapse point. - The loop is per-voice (like
delay/combin a source chain): each note gets its own ring. AFeedbacknode holds the voice alivering_out(time|50ms, 0.95)past release, so gate the exciter inside the loop and keep the voice envelope open when you want audible regeneration (seeexamples/songs/dubloop.volta'sstab).
What may sit on the fed-back path#
The region — every node the binder's value flows through on its way back to the closer — is restricted to kinds the native engine can step per-sample; anything else is a compile error naming the node:
- arithmetic:
+ - * /, negate, comparisons,detune,log2,exp2 - filters:
lpfhpfbpfsvfladderdiodekorg35 - waveshapers:
drivefoldcliptubebitcrushgatecurve - delays:
delaycomb— and nestedfeedback/feedbackAt - user kernels:
processprocess2(per-sample by construction; a stateful kernel runs its compiled code over one sample at a time). A stereo (processS), block-rate (processC) or oversampled kernel is refused
Coupled loops: feedbackN. feedbackN [29.7ms, 37.1ms, …] (\a b … -> [outA, outB, …]) closes up to 8 loops over one region — binder i reads loop i (its own ring, interpolated like feedbackAt; a count instead of a list gives one-sample loops), element i of the returned list closes it, and the value is the sum of the closing signals. The same region rules apply to all of them together. Write a custom audio processor § Vector feedback has an FDN example.
Everything outside the region (the summed-in oscillator, envelopes, cutoff modulators, macros) is unrestricted and stays block-rate. ir/src/feedback.rs owns the supported-operation contract used by source lowering and native/JSON preparation. Unsupported loop operations fail before installation.
Coupled nested loops run in one sample pass, with each inner closer evaluated before a consumer reads its current output. A delay control is an instantaneous input to the paired read; a control that depends on that same read creates an invalid instantaneous cycle. Node storage order does not define these dependencies.
Backends#
One engine, one behaviour — desktop, browser AudioWorklet, plugin target.
- Preparation builds each loop's region once in the immutable synth template using
volta_ir::feedback; voices evaluate it SAMPLE-INTERLEAVED (fb_region_pass/fb_stepin voice/feedback.rs): region nodes leave the block path (per-node skip flags) and step one sample at a time against their usualNodeStates, so sub-block loop delays — every Karplus-Strong pitch — are exact. Divergences from the block arms (documented in code): per-sample coefficients (no block-endpoint lerp) and no drive-gated 2× oversampling inside a loop. - Voices share the prepared topology and retain their own rings. Loop planning does not allocate at note-on or use pointer addresses as cache identities. Realtime module preparation supplies the audio and macro voice rings before the first note. Retriggers recycle them across graph sizes and node positions; active voices retain their sounding rings across a replacement. Direct offline graph construction can allocate its own storage. Event-order, song-position and previous-bus scratch is prepared too. Transport initialization, module installation and other node states remain separate preparation requirements.
- Historical: the browser used to run a real Web Audio cycle whose in-cycle delay was clamped to one render quantum, so tuned loops above ~370 Hz never reached pitch. That path was deleted 2026-07-24.
IR#
A forward DAG — no back-edge, so every existing pass is loop-safe:
{ "op": "FeedbackRead", "fb_id": 0 } // the binder leaf
{ "op": "Feedback", "input": "n16", "fb_id": 0, "time": "n6" } // the closer
time absent = minimal delay (serde skip, so old modules are byte-identical). The pair shares a delay ring keyed by fb_id, local to each graph. Native and JSON preparation reject reads without a matching closer, duplicate closers, and duplicate read nodes before installation. Multiple uses of the binder share its one read node. Separate graphs may reuse the same pair ID; a closer whose unused binder was pruned remains valid. The same validation visits persistent controls, macro modulation and graphs nested in inserts.
Demo songs#
examples/songs/dubloop.volta— dub regeneration (lpf → drive → 3/8c delay inside the loop;regen/murkmacros) + a one-samplefeedbackresonator.examples/songs/wirepluck.volta— Karplus-Strong strings: a plain wire (in-looplpfdamping,dampmacro) and a sitar with a wavefolder in the loop (notes bloom metallic, then settle).
Gotchas#
- Gate the exciter, not the output. An
ampEnvwith sustain 0 on the patch output silences the echoes/ring; multiply the burst/stab inside the loop and give the voice a long-release envelope. |> gain xis the modulator-scale builtin and can type a summand as Control — use* xfor level inside audio sums feeding a loop.- The loop-gain advisory only sees constant Product factors; a modulated gain (
fb * regen) is unjudged — the soft limiter is still there.
File map#
| Layer | Where |
|---|---|
| Builtins + types | compiler/src/builtins/fn_operands.rs (Feedback/FeedbackAt schemas) |
| Binder typing | compiler/src/check.rs (infer_feedback_lambda) |
| Trailing-lambda parse | compiler/src/parse.rs (parse_app) |
| Lowering | compiler/src/lower/expr.rs (lower_feedback, validate_feedback_region) |
| IR nodes and pair validation | ir/src/ir.rs (Feedback, FeedbackRead), ir/src/validation.rs |
| Intervals + advisory | compiler/src/refine.rs |
| Shared region contract | ir/src/feedback.rs (pair ownership, delay-control edges, supported operations and region order) |
| Prepared template | engine/src/graph.rs (SynthTemplate::prepare_feedback) |
| Region pass | engine/src/voice/feedback.rs (fb_region_pass, fb_step, fb_soft_limit) |
| Browser path | the engine in wasm (same region pass); the Web Audio makeFeedback/fbLoopGain path was deleted 2026-07-24 |
| In-app docs | runtime/src/docs-data.ts |
| Tests | compiler/src/lower/tests.rs, engine/src/voice/feedback.rs (recurrence / staircase / KS / runaway), engine/src/offline.rs (source→engine end-to-end) |