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 apatch. Reads the named track's previous-block post-fader stereo bus. The track is its binding (tap voice, qualifiedtap 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 declaresoperand::TRACK_OR_BUSand one walk (lower/handles.rs::source_name) serves every port that does,duck'ssourceincluded (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, sowhich = "ki" ++ "ck"nameskicktoo. 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 astap, 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 atapLivecycle is a compile error. Reach for it only when you need phase-aligned combination (un-smeared dry/wet, mid-side tricks);tapis the safe default.vocode carrier modulator bands— channel vocoder, mono. Typicallyvocode (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 —
tapis one render block (~2.67 ms at 48 kHz): the source's last block is already settled, so no render ordering and no PDC.tapLiveis 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
tapLiveof 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 drumsandtap "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, sotap "a"insideimport "toy" as toynamed a track that had flattened totoy.aand the reader summed exact zeros. Fixed 2026-09-16 — the literal is qualified in the same pass that qualifiesmacronames (Names and library imports § Notes & limits). This was the whole of the report "tapfolds 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 browserengine-wasm/pkg-singlebuild 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 namestoy.a, is a rename (Names and library imports § Imported tracks): the IR carries one trackx, and the library's owntap "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.aas well, or a string reference from the SONG such asduck "toy.a") keepstoy.aas 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.bandsis 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 ofsoctaves (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):modulator before after 16 → 32 bands white noise 18.69 dB 1.30 dB +5.74 → +0.11 dB a plucked, enveloped saw (what tapactually delivers)— 1.69 dB −0.11 dB a steady 150 Hz harmonic comb (the tonal worst case) 17.6 dB 3.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_TRIMis 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_cseconds 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_bandsmeasures 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 2is 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 2is 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
bandslevel-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'stalker, left alone), 28 bands −2.39 dB and 32 bands −3.54 dB (livevox.volta'sroboandchoir, whose gains were raisedvel * 0.7 → 0.92and→ 1.05to 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.- Q follows the spacing.
- Per-voice, shared modulator — every carrier voice reads the same source bus (right for vocoder/ring mod).
tapLiverender order — atapLive "S"inside trackT'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 notapLivekeep declaration order, so non-tapLivesongs are unaffected), and emitsModule.render_order(a permutation of the track indices; absent from the IR when no track usestapLive, so existing goldens and the engine's fast path are untouched). Conversely ANYtapLive— 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; atapkeeps the fast path. OnlytapLiveedges constrain the order: a 1-blocktapedge is invisible to the sort, so anA → BtapLivewith aB → Atapis not a cycle (thetapis the legal feedback path).tapLivecycle = compile error — a cycle amongtapLiveedges (including a self-tapLive) can't be render-ordered;check_causalemits an error-levelCausalthat names the loop and says to use the 1-blocktapon one edge to feed back.- Diagnostics —
refine.rs::check_causal(Refinements §6.3) emits an ok-levelCausalpertapedge naming the 1-block latency (pertapLiveedge: a zero-latency discharge), names cross-track feedback loops and their periods, and errors on atapLivecycle. 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/tapLiveare descriptor-owned incompiler/src/builtins/controls.rsandvocode/convLiveincompiler/src/builtins/spectral.rs(BuiltinKind::{Tap, TapLive, Vocode}); the name each reader takes is the declaredTRACK_OR_BUSoperand (compiler/src/builtins/overloads.rs, moduleoperand), which is also what namespace-qualifies its string spelling.tap/tapLivelower incompiler/src/lower/expr/controls.rstoNodeKind::Tap/NodeKind::TapLive { source_track }(nostage, the legacy read) orNodeKind::EndpointTap { source_track, stage, live }(an explicitstage, scheduled by the prepared routing plan,ir/src/routing.rs);vocodelowers incompiler/src/lower/expr.rstoNodeKind::Vocode(ir/src/ir.rs).refine.rs::check_causalemits the causal advisories and resolves thetapLiverender order:check_taplivecollects thetapLivetrack edges, detects cycles (an SCC scan → error-levelCausal), and Kahn-topo-sorts the tracks intoModule.render_order(empty ⇒ absent from JSON ⇒ fast path). Cost weight + interval + zero-delay-scan handling mirrorTap(compiler/src/{cost,refine,lower}.rs). - Engine:
engine/src/graph.rsresolves the name to aSourceBusand compilesCompiledNode::TapLive(a stereo source, likeTap);CompiledModule.render_ordercarries the order. When it is emptyengine/src/lib.rs::processkeeps the render-all (render_buses) then mix-all (mix_buses) fast path, byte-identical to pre-tapLive. When non-empty it takesrender_mix_ordered: per track inrender_orderit renders that track's voices (VoiceArena::render_track_into) on top of any plugin-instrument audio, thenmix_track(fader → strip → snapshot → sends → fx → master), snapshotting the current block intolive_bus_l/r; the default bus renders last.Tapstill reads the previous block (prev_bus, threaded astrack_taps);TapLivereads the current block (live_bus, threaded as the newlive_tapsview inVoice::eval). At end of blocklive_bus[0..n]copies intoprev_bus[0..n]so the looper and next block'stapsee this block.Follower/Onsetskeep their 1-block latency via a frozenordered_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 explicitstageread) 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 foldedTap/TapLive/Vocodeto silence, was deleted 2026-07-24.) - Tests:
engine/tests/cross_track.rs(tapLiverender-order present, ring mod audible through the ordered path, and sample-aligned-vs-tap-lag), plusengine/src/voice/eval.rs::taplive_reads_current_block_not_previous(buffer-level current-vs-previous proof). Compiler:compiler/tests/refine_diags.rs+refine.rsunit tests (order resolution, cycle error, mixedtapLive+taplegality, self-cycle, empty order on tap-only songs).
Notes & limits#
tapLivereads the source's post-fader (pre-fx) current block, VOICE_GAIN corrected — the exact signaltapreads, one block earlier. AtapLiveof an aux bus — a legacy one withoutstageincluded — is scheduled through the prepared routing plan (ir/src/routing.rs), so it reads that return's current block. AtapLiveof 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 (
onsetstrigger 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.tuneis a frequency (likestring's freq —freq pitchtunes the body per note);damping(0..1, default 0.5) maps to the ring T60. The exciter is any audio node, soresonate noise (freq pitch) 0.1is a purely-local bowed drone.stiffnessandsympathy(both 0..1, default 0) arestring'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/dampingrefresh 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 reusesNodeState::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 thecarrierwith 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 whole2·512 − 1sample 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: keepb's per-bin phase (its excitation / rhythm), lerpb's magnitude towarda's byamount∈ [0, 1] (patternable / macro-driven). A drum tap shaped by a tuned saw's harmonics is a "spoken" pitched drum. Demo:specweave.volta.asupplies the shape;bsupplies 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 matchspectralMorphruns (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 onbmoves 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 bymorph_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 shippedspecweave.voltalost 14.9 dB and had its morph gain raised fromvel * 0.8tovel * 4.4to restore the mix it was written for. If aspecMorphvoice went quiet, that is why: the level now lives on the excitation, so compensate on the patchgain.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: keepsignal's energy only in bins where the track's tap is quiet (an unmask), blended dry→masked byamount. 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 isspectralGate's construction with the mask standing in for the threshold, and for the same reason: the hard1 − 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 bymask_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 aSourceBuslikeTap; the eval arms driveNodeState::Stft(CrossStft)(voice/eval.rs); the FFT combination isxspectral::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_causalestimates the loop gain around each cross-tracktapcycle (product over hops oftrack 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 feedbackgain. 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_causalruns an SCC pass over the audio-tap-only digraph (reader track → source track) and setsNodeKind::Tap { bounded: true }on every read whose reader and source share a cyclic component (a self-tap counts). The engine'sTaparm then appliesvoice::fb_soft_limit(identity in [-1, 1], tanh knee capped at ±2).Two deliberate choices here, each load-bearing:
- On the read, not the bus snapshot.
prev_bushas three consumers —Tapnodes, 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 byengine::tests::prev_bus_snapshot_is_not_soft_limited. - Cycles only, not every tap.
fb_soft_limitis 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'ssaw * tap "bassTrack"measurably changed under a blanket rail. An acyclic tap now reads the bus verbatim. Pinned byan_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_railmeasures the pre-limiter bus — asserting on the master would prove nothing, since the master limiter pins it to ±1 either way).convLive/specMorph/specMaskneed no rail on their tapped input:CrossStftenergy-normalizes and soft-limits its own wet. Demo:feednet.volta(a bounded A↔B network).- On the read, not the bus snapshot.