VOLTAreference

Reference / Effects and mixing

Route audio between tracks

Effects and mixing

Open in the workbench →
On this page · 10 sections

Use one track's audio inside another sound, from layered processing to vocoding and other interactions.

What it is#

Inter-track signal access: one track's patch reads another track's output as an audio source. It generalizes the shipped Make one sound follow another — scalar RMS → the full stereo buffer, at the follower's exact latency model (previous-block, post-fader, one block ≈ 2.67 ms). That one primitive opens a family: ring mod falls out for free (source * tap "x"), and vocode imposes a tapped signal's spectral envelope on a carrier (the talking synth). Because the read is a block late, it is feedback-safe and needs no track ordering — an A → B → A cycle just works.

Surface#

  • tap track — audio source node, usable anywhere in a patch. Reads the named track's previous-block post-fader stereo bus. The track is its binding (tap voice, qualified tap kit.voice — renamed and found by the editor with every other reference); its name as a string (tap "voice") is accepted legacy (Language reference §12), as is any value that resolves to either — a binding, a record field, a function's result, a list element — because the port declares operand::TRACK_OR_BUS and one walk (lower/handles.rs::source_name) serves every port that does, duck's source included (Named options and parameter values, "a port's declared spellings"). tap "input" reads the live mic/line-in hub (Live audio input); a Mixing tracks and buses aux-bus name (a string — a bus has no binding) taps that bus's post-effect output. A string binding is evaluated, so which = "ki" ++ "ck" names kick too. A name that names nothing is a compile error at the argument, at every one of these ports, not silence.
  • tapLive track — the zero-latency sibling. Same surface/return as tap, but reads the source track's current block (sample-aligned, no ~2.67 ms delay). The cost: the source track renders + mixes before this one, so the compiler topologically orders the tracks and a tapLive cycle is a compile error. Reach for it only when you need phase-aligned combination (un-smeared dry/wet, mid-side tricks); tap is the safe default.
  • vocode carrier modulator bands — channel vocoder, mono. Typically vocode (saw (freq pitch)) (tap voice) 16.
voice = play vox !"c4 e4 g4 c5"          -- the tracks the taps name
core  = play bass bassPat

talk = patch { source = vocode (saw (freq pitch)) (tap voice) 20,
               ampEnv = adsr 5ms 200ms 0.7 200ms, gain = vel * 0.5 }
bell = patch { source = sine (freq pitch) * tap voice,            -- ring mod
               ampEnv = adsr 2ms 300ms 0.4 300ms, gain = vel * 0.4 }
wet  = patch { source = tapLive core |> drive 0.7, gain = vel * 0.4 }   -- aligned parallel dist

Demos: examples/songs/talkbox.volta (tap + vocode) and examples/songs/livetap.volta (tapLive aligned parallel distortion + width).

Choose a source point#

Use tapLive "drums" { stage = "preFader" } for the voice/instrument output, "postStrip" after level, pan, mute/solo and strip processing, or "postFx" after all inserts and track delay compensation. insertPoint "dry" names a transparent chain boundary; stage = "insert:dry" reads that exact point. Names are fixed while compiling. A missing explicit source or boundary is an error. Bare track bindings use the resolved track identity, including imports. Source strings and stage strings can come from bindings, records or functions; they must produce fixed values while preparing the graph. A string names the source by its value, so source = "drums" followed by tap source reads drums.

Returns publish preFader and postStrip at their summed send input (returns have no separate strip); postFx follows return processing. "master" exposes the summed mix, master strip and final insert output. "input" has only preFader, at the capture hub. Pre-fader reads survive fader/mute/solo changes; post-strip reads follow those controls. A stopped source may still have voice release or effect tails; a persistent insert may itself generate audio.

Explicit tap adds exactly 128 samples, independent of callback size (2.667 ms at 48 kHz); explicit tapLive adds zero. DSP latency at the chosen point is still present. Named insert boundaries precede the track's final compensation delay. Legacy calls without stage retain their earlier defaults, calibration and callback-based delay. All forms reject missing sources before a new module can be installed.

Source compilation and native/JSON preparation share the legacy tapLive ordering contract. An instantaneous track cycle is rejected even without @strict. If IR omits render_order, preparation derives the stable causal order. A supplied order must list every track exactly once, use valid indices, and place each live source before its reader. Invalid replacements leave the installed performance playing. Tracks with no live reads retain the unordered render path when no explicit order is supplied.

The compiler schedules voices, strips, sends, returns, individual inserts, shared controls and macro graphs together. Reading an insert's own input is legal. Reading its own output, or feeding the master straight upstream, is an instantaneous loop and reports the path. Replace one read with explicit tap for bounded feedback. The prepared plan permits 2,048 stages and 512 distinct endpoints; callback execution allocates no routing buffers. Seek clears delayed history; compatible recompiles retain it by source name and stage.

Filter a detector source using ordinary composition:

level = tapLive "drums" { stage = "preFader" } |> hpf 150Hz 0.7 |> follow

Export and bounce context#

A selected export renders its required source tracks and returns while omitting their direct contribution to the selected stem. An arranged stem retains the original scenes: a dependency starts only while its original scene includes it, and its release/tail follows normal engine behavior. Master processing runs on the selected sum. Nonlinear master processing means stems need not sum to the full master render.

bounceOf follows these audio/control dependencies and orders nested sample generations before their consumers. Routed bounces use the bounce cache like any other: the recipe key covers the prepared upstream program, shared controls and resources of every source track the bake starts (Resample your own sounds § The live workflow). A bounce depending on live input reports a record-first error. Record the input and use that sample, or use the checked offline render API with captured stereo PCM at the render sample rate. Capture starts at transport sample zero and zero-fills after EOF; absent, non-finite or mismatched-rate capture is rejected.

Semantics#

  • Latency — tap is one render block (~2.67 ms at 48 kHz): the source's last block is already settled, so no render ordering and no PDC. tapLive is zero added latency — it reads the source's current block, at the cost of a compile-time render order (below). No PDC either (everything is block-aligned).
  • Stopped source → silence (graceful, never an error); a muted track taps as silence (post-fader read). A tapLive of a track not in the running scene reads silence too (the source isn't rendered this block). A name that matches no track, bus or "input" at all is different: it is a compile error at the argument (lower/handles.rs::refuse_dangling_sources), not silence.
  • The source may be written as an identifier or as a string. tap drums and tap "drums" lower identically. Under an alias import the two used to diverge: the identifier was renamed with the rest of the module's bindings while the literal was not, so tap "a" inside import "toy" as toy named a track that had flattened to toy.a and the reader summed exact zeros. Fixed 2026-09-16 — the literal is qualified in the same pass that qualifies macro names (Names and library imports § Notes & limits). This was the whole of the report "tap folds to silence in the wasm engine": the engine renders a namespaced tap on both builds (engine/tests/cross_track.rs::a_namespaced_tap_renders, and the browser engine-wasm/pkg-single build driven from Node — source rms 0.165, |a+b − a| rms 0.095, against 0.000000 before).
  • A library's tap follows a consuming re-binding. x = toy.a, when it is the only thing in the song that names toy.a, is a rename (Names and library imports § Imported tracks): the IR carries one track x, and the library's own tap "a" is rewritten to "x" with it, so the track the host starts is the one the tap reads. Naming the import a second time (y = toy.a as well, or a string reference from the SONG such as duck "toy.a") keeps toy.a as its own track, and a tap of that name then reads that copy — silent unless it is also started. Start the track the tap names, or let the single re-binding do the renaming.
  • Vocode DSP — bands (clamped 2–64) log-spaced RBJ bandpass bands over [120 Hz, min(7 kHz, 0.45·sr)]; each modulator band's envelope gates the carrier's same band. Inputs are mono-downmixed. bands is a RESOLUTION knob and nothing else (rebuilt 2026-08-05, DSP audit-3 §C5/§C7 — before that it was worth up to 16 dB in the mix and a 57 dB comb):

    • Q follows the spacing. Q = 0.70 / (2^(s/2) − 2^(−s/2)) for a band spacing of s octaves (clamped 0.15–24) — 2.57 at 16 bands, 3.26 at 20, 10.84 at 64 — so each band's skirts meet its neighbours' wherever the count puts them. Composite ripple over 140 Hz–6.5 kHz is now ≤ 1.84 dB at every count from 2 to 64, against 57.6 dB at 2 bands, 16.5 at 8, 6.0 at 16 and 4.0 at 20 under the old fixed Q 6. (At 48–64 bands the fixed Q happened to be narrower than the spacing, so the bands piled up into a smooth-but-13-dB-hot composite: 1.39 dB of ripple there against 1.82 now. That is the one place the old bank was flatter, and it paid for it with the level.)
    • The level is normalized. The gated band sum is divided by the bank's own composite RMS, and each band envelope is rescaled to a reference bandwidth (√(Q/6)), so neither the carrier-side overlap nor the modulator-side measurement bandwidth tracks the count. How flat that is depends on the modulator, so all three fixtures are on the record (measured through the real voice path, 110 Hz saw carrier, spread over 4…64 bands):

      modulatorbeforeafter16 → 32 bands
      white noise18.69 dB1.30 dB+5.74 → +0.11 dB
      a plucked, enveloped saw (what tap actually delivers)—1.69 dB−0.11 dB
      a steady 150 Hz harmonic comb (the tonal worst case)17.6 dB3.25 dB−0.16 dB

      The tonal case is worst by construction: a band containing one harmonic has a level that does not depend on its bandwidth, so the density rescale over-corrects it. No trim was fitted — the two normalizations happened to land the documented 20-band setting 0.22 dB from where it was (−43.90 → −43.68 dBFS on white noise), so VOCODE_TRIM is unity and says so.

    • The follower is a boxcar into a split pole. Each band's rectified modulator is averaged over exactly half the band's period — so the boxcar's first null lands on the rectifier's own 2·f_c ripple — and then smoothed with a 1 ms attack and a release of 4/f_c seconds clamped to 3–30 ms (the audit's "scale the release with the band's centre"). Measured: the AM the follower stamps on the carrier is ≤ 2.18 % p-p at every band (8.72 % at the 120 Hz band before), a 12 ms consonant's 10–90 % rise is 3.65 ms (8.42 before), it rings 13.2 ms past the burst (19.8 before), and it now reaches full level instead of 84 % of it.
    • Cost: render/vocode_20_bands measures 6.35 ms/s against 5.09 for the bank it replaces (cargo bench -p volta-engine --bench dsp -- vocode) — the boxcar is a load/store and two adds per band per sample.
    • bands 2 is not on the plateau, and is excluded from the claim above. It sits 3.4 dB (white noise), 4.8 dB (tapped synth) or 6.2 dB (harmonic comb) below the 4…64 level. That is structural, not a missing correction: the Q the spacing asks for at two bands is 0.093, the clamp floor is 0.15, and the reference-bandwidth rescale (√(Q/6)) shrinks with Q — so the wider the band, the more of the modulator's level the density normalization divides away. Lowering the clamp to 0.08 was tried and makes it worse (−48.06 against −47.06 dBFS on white noise), because the rescale falls faster than the coverage improves. Two 2-pole bandpasses cannot both tile 5.87 octaves and measure a critical-band level; bands 2 is a special effect, and the tests hold it to its own bound rather than to the plateau's. Its composite is nonetheless flat (−1.5…+0.1 dB).

    This re-levelled existing songs, and it had to: making bands level-neutral necessarily moves every count that is not the one the calibration sits on. Measured on the white-noise fixture (the other two fixtures move by similar amounts — see the table above for how much the choice of modulator matters), before → after: 20 bands +0.22 dB (talkbox.volta, quartzline.volta — unchanged for practical purposes), 24 bands −1.00 dB (fmtest.volta's talker, left alone), 28 bands −2.39 dB and 32 bands −3.54 dB (livevox.volta's robo and choir, whose gains were raised vel * 0.7 → 0.92 and → 1.05 to restore what they were voiced against). If you are reading this because a vocoder patch of yours got quieter: it was riding the band count's gain, and the further from 20 bands you were, the more of it you had.

  • Per-voice, shared modulator — every carrier voice reads the same source bus (right for vocoder/ring mod).
  • tapLive render order — a tapLive "S" inside track T's patch is a dependency edge S → T ("render S before T"). The compiler collects every such edge, topologically sorts the tracks (a stable sort — tracks with no tapLive keep declaration order, so non-tapLive songs are unaffected), and emits Module.render_order (a permutation of the track indices; absent from the IR when no track uses tapLive, so existing goldens and the engine's fast path are untouched). Conversely ANY tapLive — even one only a visual reads — takes the engine off its render-all-then-mix path, which can move the mix by ~1e-5, so a film's audio is no longer byte-identical to the song it imports; a tap keeps the fast path. Only tapLive edges constrain the order: a 1-block tap edge is invisible to the sort, so an A → B tapLive with a B → A tap is not a cycle (the tap is the legal feedback path).
  • tapLive cycle = compile error — a cycle among tapLive edges (including a self-tapLive) can't be render-ordered; check_causal emits an error-level Causal that names the loop and says to use the 1-block tap on one edge to feed back.
  • Diagnostics — refine.rs::check_causal (Refinements §6.3) emits an ok-level Causal per tap edge naming the 1-block latency (per tapLive edge: a zero-latency discharge), names cross-track feedback loops and their periods, and errors on a tapLive cycle. A missing audio source is refused at the operand (lower/handles.rs::refuse_dangling_sources), with shared execution validation behind it before preparation.

How it's implemented#

  • Compiler: tap / tapLive are descriptor-owned in compiler/src/builtins/controls.rs and vocode / convLive in compiler/src/builtins/spectral.rs (BuiltinKind::{Tap, TapLive, Vocode}); the name each reader takes is the declared TRACK_OR_BUS operand (compiler/src/builtins/overloads.rs, module operand), which is also what namespace-qualifies its string spelling. tap / tapLive lower in compiler/src/lower/expr/controls.rs to NodeKind::Tap / NodeKind::TapLive { source_track } (no stage, the legacy read) or NodeKind::EndpointTap { source_track, stage, live } (an explicit stage, scheduled by the prepared routing plan, ir/src/routing.rs); vocode lowers in compiler/src/lower/expr.rs to NodeKind::Vocode (ir/src/ir.rs). refine.rs::check_causal emits the causal advisories and resolves the tapLive render order: check_taplive collects the tapLive track edges, detects cycles (an SCC scan → error-level Causal), and Kahn-topo-sorts the tracks into Module.render_order (empty ⇒ absent from JSON ⇒ fast path). Cost weight + interval + zero-delay-scan handling mirror Tap (compiler/src/{cost,refine,lower}.rs).
  • Engine: engine/src/graph.rs resolves the name to a SourceBus and compiles CompiledNode::TapLive (a stereo source, like Tap); CompiledModule.render_order carries the order. When it is empty engine/src/lib.rs::process keeps the render-all (render_buses) then mix-all (mix_buses) fast path, byte-identical to pre-tapLive. When non-empty it takes render_mix_ordered: per track in render_order it renders that track's voices (VoiceArena::render_track_into) on top of any plugin-instrument audio, then mix_track (fader → strip → snapshot → sends → fx → master), snapshotting the current block into live_bus_l/r; the default bus renders last. Tap still reads the previous block (prev_bus, threaded as track_taps); TapLive reads the current block (live_bus, threaded as the new live_taps view in Voice::eval). At end of block live_bus[0..n] copies into prev_bus[0..n] so the looper and next block's tap see this block. Follower/Onsets keep their 1-block latency via a frozen ordered_rms_prev, which every render path freezes before its mix and every effect chain reads, so track order cannot move it (cross_track::insert_onsets_latency_does_not_depend_on_track_order). A module with a prepared routing plan (any explicit stage read) takes a third path instead, render_mix_routed, which executes the plan's stages in causal order (engine/src/routing.rs).
  • Browser: the same engine compiled to wasm (engine-wasm/) in an AudioWorklet — no separate implementation. (The Web Audio voice graph, which folded Tap/TapLive/Vocode to silence, was deleted 2026-07-24.)
  • Tests: engine/tests/cross_track.rs (tapLive render-order present, ring mod audible through the ordered path, and sample-aligned-vs-tap-lag), plus engine/src/voice/eval.rs::taplive_reads_current_block_not_previous (buffer-level current-vs-previous proof). Compiler: compiler/tests/refine_diags.rs + refine.rs unit tests (order resolution, cycle error, mixed tapLive+tap legality, self-cycle, empty order on tap-only songs).

Notes & limits#

  • tapLive reads the source's post-fader (pre-fx) current block, VOICE_GAIN corrected — the exact signal tap reads, one block earlier. A tapLive of an aux bus — a legacy one without stage included — is scheduled through the prepared routing plan (ir/src/routing.rs), so it reads that return's current block. A tapLive of the default bus reads silence (it renders after the tracks).
  • Vocoder is mono with fixed band range/Q; a stereo/tunable variant is an open question in Route audio between tracks §5, as are stages 3–5 (resonate, the spectral family, bounded feedback networks — see Patterns that respond to playback).
  • The event-direction sibling (onsets trigger streams, playOn) rides the same bus seam and shipped as Turn audio into musical events.

Resonate (resonate)#

resonate exciter tune [damping] [stiffness] [sympathy] — feed an audio exciter (typically a tap of a drum / noise track) into a Karplus-Strong resonator tuned to tune Hz. A rhythmic loop becomes a pitched, plucked body; noise becomes a bowed pad. It's the follow-on the design flagged as "the showpiece most systems don't make easy" (§4.1).

  • Surface — resonate (tap "drums") (freq pitch) 0.3. tune is a frequency (like string's freq — freq pitch tunes the body per note); damping (0..1, default 0.5) maps to the ring T60. The exciter is any audio node, so resonate noise (freq pitch) 0.1 is a purely-local bowed drone. stiffness and sympathy (both 0..1, default 0) are string's two physical knobs — the same core, so the same pair, in the same order: stretched partials and a shared bridge across this node's voices. See Strings, bells and resonating objects §1.8; at 0 neither is built and the resonator is bit-for-bit the one that shipped before them.
  • DSP — reuses physical::WaveguideState (the extended Karplus string: fractional-delay loop, one-pole loss filter, pick-position comb), with a fixed bright/neutral body (brightness 0.7, position 0.5). tune/damping refresh per block; the exciter drives the loop per sample.
  • Where — NodeKind::Resonate { exciter, tune, damping, stiffness?, sympathy? } (ir.rs) / CompiledNode::Resonate (graph.rs); the eval arm reuses NodeState::Waveguide (voice/eval.rs). Demo: examples/songs/resonator.volta.

Spectral family (convLive / specMorph / specMask)#

Per-block STFT of a live tap, reusing realfft (as convolve does). All three are two-input streaming STFTs in engine/src/xspectral.rs with 75 % Hann overlap-add — specMorph / specMask at FFT 2048, hop 512 (≈23.4 Hz/bin at 48 kHz); convLive keeps a 512-sample frame, hop 128, zero-padded to a 1024-point transform (see below) — wet soft-limited so a live-convolution or feedback patch stays bounded.

  • convLive carrier source — live convolution: convolve the carrier with the track's CURRENT tap block used as a changing impulse response (a drum loop, not a fixed IR). The IR is normalized to unit time-energy per frame, gated by whether the tap is a signal at all: a programme follower (instant attack, 1 s release) against a −60 dBFS RMS reference opens the wet fully while the tap is alive and closes it linearly as a fading bus, room tone or dither takes the programme under that knee, reaching zero continuously (2026-08-03 audit C12: an 80 dB swing on the tap used to move the wet by 0.03 dB, so a dying bus convolved the carrier at full level and then cut off in one hop at exact digital zero). The knee is on the PROGRAMME, not on the frame, because a real tap's own dynamics carry most of its frames under any fixed per-frame knee — a drum-shaped tap with a 48 dB envelope at −40 dBFS peak measured 51.1 dB of wet envelope range against 31.9 dB at 0 dBFS, i.e. the normaliser had stopped normalising (2026-08-06 review C12-1). It is now identical to the digit at 0 / −20 / −40 dBFS, which is the point of the normaliser. The carrier is "spoken through" the beat. The Conv mode alone zero-pads both windowed frames to a 1024-point FFT and overlap-adds the whole 2·512 − 1 sample result — a product of two 512-point spectra is a circular convolution, which wraps late IR energy back onto the start of the frame (pre-echo ahead of the sound that caused it). With the padding the analysis window alone carries the overlap-add, so Conv normalises by the Hann amplitude sum (2.0) and applies no synthesis window; latency is unchanged. Demo: convlive.volta.
  • specMorph a b amount — spectral cross-synthesis: keep b's per-bin phase (its excitation / rhythm), lerp b's magnitude toward a's by amount ∈ [0, 1] (patternable / macro-driven). A drum tap shaped by a tuned saw's harmonics is a "spoken" pitched drum. Demo: specweave.volta.

    a supplies the shape; b supplies the phase and the level. Before each blend the carrier's magnitudes are rescaled to the excitation's own frame energy — √(Σ|B|² / Σ|A|²) — the same per-frame energy match spectralMorph runs (Spectral effects §2). Two things follow, and both used to be wrong: the excitation's dynamics ride straight through the wet (an 80 dB swing on b moves the wet 80 dB; it used to move it 0.00 dB), and a silent excitation gives exact silence (a bin with no excitation has no phase to borrow and contributes nothing — the zero-phase unit vector it used to substitute is an impulse, and it drummed a −25.9 dBFS 375 Hz tone out of digital silence, i.e. specweave's pad droned between the drum hits). Pinned by morph_carries_the_excitation_level / morph_of_a_silent_excitation_is_silent.

    This re-levels existing songs. Before the energy match the wet came out at the carrier's level, so a patch written against the old behaviour is now as loud as its tap — which for a post-fader drum bus is typically far quieter than a bare saw. The shipped specweave.volta lost 14.9 dB and had its morph gain raised from vel * 0.8 to vel * 4.4 to restore the mix it was written for. If a specMorph voice went quiet, that is why: the level now lives on the excitation, so compensate on the patch gain.

    A carrier that decays to digital zero fades the wet over a few frames rather than dropping it: the carrier supplies the target magnitudes, so the frame its energy vanished used to take the whole spectrum with it (a 562 dB hop-to-hop step). The last usable shape is held while the gate ramps out. Pinned by morph_fades_when_the_carrier_dies.

  • specMask signal "track" amount — per-frequency spectral sidechain: keep signal's energy only in bins where the track's tap is quiet (an unmask), blended dry→masked by amount. Carves space in the mix by spectrum, not level. Demo: specweave.volta.

    The duck is a Wiener gain |a|² / (|a|² + |b|²) — unity where the mask is absent, −6 dB exactly where the two are level, falling as (|a|/|b|)² beyond — smoothed per bin across frames with a ducker's ballistics (≈5 ms engage, ≈21 ms release). This is spectralGate's construction with the mask standing in for the threshold, and for the same reason: the hard 1 − min(1, |b|/|a|) it replaced had a corner at |b| = |a| and no memory, so any partial sitting near the mask's level chattered at the hop rate (375 Hz at the 128-sample hop of the time) — measured 21.9 dB of envelope swing on a steady tone and a −21.8 dB musical-noise skirt, now 4.3 dB and −44.5 dB. Silence leaves the mask open (a ducker has nothing to duck against), so a signal re-entering an empty bus is not ducked through its own attack. Pinned by mask_does_not_chatter_at_the_mask_level / mask_opens_through_silence.

  • Where — NodeKind::{ConvLive, SpecMorph, SpecMask} (ir.rs) / CompiledNode::{…} (graph.rs) resolve the track name to a SourceBus like Tap; the eval arms drive NodeState::Stft(CrossStft) (voice/eval.rs); the FFT combination is xspectral::XMode::{Conv, Morph, Mask}.

Feedback networks (bounded)#

Cross-track feedback (A → B → A) is already expressible once tap exists — the 1-block delay tames the loop (§4.3). This stage adds the safety rail:

  • Lint — refine.rs::check_causal estimates the loop gain around each cross-track tap cycle (product over hops of track fader × the tap path's constant scalar gain, e.g. tap "b" * 0.6 → 0.6). A follower-only cycle carries no audio tap edge, so it's reported without a gain. When the estimate is ≥ 1 the diagnostic is upgraded from ok to warn, naming the cycle and suggesting a smaller feedback gain. Feedback stays legal (warning-level).
  • Limiting — the engine soft-clips a tap on read, and only for a tap the compiler marked as closing a cycle. refine::check_causal runs an SCC pass over the audio-tap-only digraph (reader track → source track) and sets NodeKind::Tap { bounded: true } on every read whose reader and source share a cyclic component (a self-tap counts). The engine's Tap arm then applies voice::fb_soft_limit (identity in [-1, 1], tanh knee capped at ±2).

    Two deliberate choices here, each load-bearing:

    1. On the read, not the bus snapshot. prev_bus has three consumers — Tap nodes, the looper capture (step_recorders), and the neural decode pump. Soft-clipping the snapshot would tanh-compress every looper take and neural input recorded from a bus that legitimately peaks above unity (an ordinary pre-master-limiter sum). The snapshot therefore stays faithful (NaN/Inf flush only). Pinned by engine::tests::prev_bus_snapshot_is_not_soft_limited.
    2. Cycles only, not every tap. fb_soft_limit is identity on [-1, 1] but not above it, so bounding every tap would audibly compress an acyclic ring mod whose source bus runs hot — qed.volta's saw * tap "bassTrack" measurably changed under a blanket rail. An acyclic tap now reads the bus verbatim. Pinned by an_acyclic_tap_is_not_bounded / a_cyclic_tap_is_bounded / a_self_tap_is_bounded.

    Bounding at the read still breaks the loop: every value entering a voice is capped, so the cycle saturates at a finite level instead of overflowing f32 (cyclic_tap_bus_is_bounded_by_the_feedback_rail measures the pre-limiter bus — asserting on the master would prove nothing, since the master limiter pins it to ±1 either way). convLive/specMorph/specMask need no rail on their tapped input: CrossStft energy-normalizes and soft-limits its own wet. Demo: feednet.volta (a bounded A↔B network).