VOLTAreference

Reference / Compose and perform

Tape stops and spin-ups

Compose and perform

Open in the workbench →
On this page · 6 sections

Slow audio toward a stop or bring it up to speed as a transition or expressive gesture.

What it is#

Three named playback-rate gestures that ride the existing Sample.speed modulator slot — no new audio path, just sugar over an exponential ramp on the sampler's playback rate:

  • brake — a one-shot exponential decay from unity to ~0.02 (the vinyl "wind down to a stop"), fired at note onset.
  • spinUp — the dual: a one-shot rise from 0 to unity ("spin up into" an event).
  • wow — a continuous sinusoidal drift around unity (analog tape wow & flutter).

Plus a track-level gesture, brakeTrack, that winds a whole track (bus gain + every active voice's playback rate) down to zero at marked pattern moments.

Surface#

brake / spinUp / wow are signal sources that compose into a sample's speed port. They are Sig Control Ratio, so they multiply into the playback rate.

braked = patch { source = sample "amen" |> speed (brake 600ms), ampEnv = adsr 1ms 600ms 0.8 100ms }
spun   = patch { source = sample "drop" |> speed (spinUp 200ms), ampEnv = adsr 1ms 2s 1 200ms }
worn   = patch { source = sample "tape" |> speed (wow 6Hz 0.04),  ampEnv = adsr 1ms 2s 1 200ms }

Builtin signatures:

brake      : Const Seconds -> Sig Control Ratio
spinUp     : Const Seconds -> Sig Control Ratio
wow        : Const Hz -> Const Normalized -> Sig Control Ratio
brakeTrack : Track -> Const Seconds -> Pattern -> Track   -- pipe-last

brakeTrack is a track combinator (pipe-last, like swing / nudge); the boolean pattern marks the cycle-fraction moments the brake fires:

kit = sound !"bd sn bd sn" |> brakeTrack 800ms !"~ ~ ~ 1"

IR#

Both per-voice envelopes lower to control-rate Ratio nodes referenced by the Sample.speed slot:

{ "op": "RateEnvelope", "shape": "Brake" | "SpinUp", "duration": <seconds> }
{ "op": "Wow", "rate_hz": <hz>, "depth": <0..1> }

Note: the wow rate field is rate_hz, not rate — a bare rate would collide with the Node.rate signal-rate tag in the flattened node JSON.

brakeTrack sets a field on the Track:

"brake": { "duration": <seconds>, "onsets": [<cycle-fraction>, ...] }

Runtime#

One engine renders these everywhere — desktop, browser (as volta-engine-wasm in an AudioWorklet) and the plugin target.

  • CompiledNode::RateEnvelope { start, target, tc } (engine/src/graph.rs) → eval_rate_envelope (engine/src/voice/eval_nodes.rs): target + (start − target)·exp(−t/tc) over note-local time. The graph build prepares the coefficients (ir/src/envelope_values.rs): Brake 1→0.02, SpinUp 0→1.0, tc = duration/4 — ~98 % by duration (4 τ ≈ "audibly stopped").
  • CompiledNode::Wow { rate_hz, depth } → eval_wow, 1 + depth·sin(2π·rate·t) on the same clock: unity at onset, within ±depth.
  • Both feed the sampler's playback-rate modulator read per sample (engine/src/voice/eval_sampler.rs), so a time-varying brake ramps the read pointer rather than stepping it per block. Unit pins: engine/src/voice/eval.rs's mod tests.
  • brakeTrack is not wired up yet. It lowers to Track.brake = TrackBrake { duration, onsets } (ir/src/ir.rs, compiler/src/lower/track.rs), but no engine code reads that field, so the gesture is inaudible today. Per-voice brake is the working spelling.

Refinement advisories#

Range warnings (not errors): duration ⊂ [10ms, 4s], wow.depth ⊂ [0, 0.5], wow.rate ⊂ [0.1Hz, 20Hz].

v0 limitations#

  • No pitch-locked brake — brake couples pitch and time (it rides playbackRate). A pitch-preserving brake needs Time-stretching and pitch-shifting.
  • No looping brake — once brake reaches zero the voice is silent; the loop must be re-triggered.
  • No crash — the drum-stick-against-platter sound is a sample, not an envelope; layer a crash sample under |> brake 200ms.
  • The per-voice speed slot folds to one modulator; composing brake with wow on the same sample needs signal-level arithmetic (deferred).