VOLTAreference

Reference / Effects and mixing

Build a feedback loop

Effects and mixing

Open in the workbench →
On this page · 8 sections

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 like delay'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 on fb.
  • The binder parameter types as Sig Audio Bipolar, so fb * macroKnob keeps 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.rs proves 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/comb in a source chain): each note gets its own ring. A Feedback node holds the voice alive ring_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 (see examples/songs/dubloop.volta's stab).

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: lpf hpf bpf svf ladder diode korg35
  • waveshapers: drive fold clip tube bitcrush gate curve
  • delays: delay comb — and nested feedback/feedbackAt
  • user kernels: process process2 (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_step in voice/feedback.rs): region nodes leave the block path (per-node skip flags) and step one sample at a time against their usual NodeStates, 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/murk macros) + a one-sample feedback resonator.
  • examples/songs/wirepluck.volta — Karplus-Strong strings: a plain wire (in-loop lpf damping, damp macro) and a sitar with a wavefolder in the loop (notes bloom metallic, then settle).

Gotchas#

  • Gate the exciter, not the output. An ampEnv with 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 x is the modulator-scale builtin and can type a summand as Control — use * x for 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#

LayerWhere
Builtins + typescompiler/src/builtins/fn_operands.rs (Feedback/FeedbackAt schemas)
Binder typingcompiler/src/check.rs (infer_feedback_lambda)
Trailing-lambda parsecompiler/src/parse.rs (parse_app)
Loweringcompiler/src/lower/expr.rs (lower_feedback, validate_feedback_region)
IR nodes and pair validationir/src/ir.rs (Feedback, FeedbackRead), ir/src/validation.rs
Intervals + advisorycompiler/src/refine.rs
Shared region contractir/src/feedback.rs (pair ownership, delay-control edges, supported operations and region order)
Prepared templateengine/src/graph.rs (SynthTemplate::prepare_feedback)
Region passengine/src/voice/feedback.rs (fb_region_pass, fb_step, fb_soft_limit)
Browser paththe engine in wasm (same region pass); the Web Audio makeFeedback/fbLoopGain path was deleted 2026-07-24
In-app docsruntime/src/docs-data.ts
Testscompiler/src/lower/tests.rs, engine/src/voice/feedback.rs (recurrence / staircase / KS / runaway), engine/src/offline.rs (source→engine end-to-end)