Add repeats with tape-like movement and softening, or use the tape model's color without an echo.
What it is#
track |> tape time feedback [wet] [bias] — a tape-echo machine model (engine/src/effect.rs::TapeDelay/TapeLine/TapeRecordPlay/JaHysteresis). The optional 4th argument bias ∈ [0, 1] (nominal 0.5, IR-defaulted so existing songs serialise byte-identically) drives the head-bias control described under Bias below.
track |> saturate amount "tape" [mix] runs the same reusable record/play electronics with effectively zero transport delay and no feedback path. It is the direct tape-print surface: stateful hysteresis, level-dependent loss and head color without an echo. The machine is 7 samples late (below); the insert delays its dry path to match and reports the 7 samples for PDC. Other saturate shapes do not use this model and retain their existing ADAA paths.
The circuit chain (per channel)#
record: (input + feedback·playback) → record-amp tanh (op-amp electronics) → Jiles-Atherton magnetization → 30 Hz coupling-cap HP → tape; playback: read (W&F-modulated) → gap-loss LP → head-bump EQ (output tap).
- Jiles-Atherton hysteresis (
JaHysteresis): the true differential loop (Langevin anhysteretic, mean-field α, direction-gated irreversible term), f64 state. Since evolve runvolta_tape#93 the law is integrated in the field, not in time — J-A is rate-independent (dM/dt = Ḣ·G(M, H, δ)), so M depends only on the path of H: a Heun step per inner sample (its end slope reused as the next start slope), midpoint RK2 sub-steps of ≤ 0.06 when the field moves further, Langevin pair and record amp from tables. The input is band-limited-interpolated to 8× by a 12-tap Kaiser-windowed sinc, the record amp runs there, and each inter-sample turning point of the field (a parabola through three inner samples) is inserted into the path — the turning points are what the hysteresis remembers, and the old 2× linear-midpoint scheme only saw sampled ones, which wobbled cycle to cycle as the pitch beat against the sample grid. Below drive 2 the solver runs on the 4× grid (8× midpoints interpolated), above it on all eight; three polyphase-allpass halfbands decimate. Latency: 7 host samples (TAPE_LATENCY: the FIR's 5.5, one inner sample of turning-point lookahead, the halfbands); the echo shortens its transport by 7 so repeats still land ontime, andsaturate "tape"delays its dry path. Measured on the evolve harness (48 kHz, band-limited saws against a continuous-time ideal of the law): alias suppression mean 43.4 → 90.5 dB (worst 28.7 → 76.3), harmonic error 2.70 → 0.63 dB, peak overshoot +1.17 → 0.00 dB; in the engine the machine went from 232 to 92 ns/sample. Measured offline, not listened to. Peak susceptibility is calibrated at build time (reference sines across levels driven through the field solver, control-side, pure function of bias). J-A is quasi-static (rate-independent loop) — tape's frequency dependence lives in the surrounding EQ, and the validation gate pins that understanding. - Bias (
JaHysteresis::new(sr, bias), engine-side; nominal 0.5): under-bias widens the loop (k_eff ↑ — crunch), over-bias narrows it and pulls the gap-loss cutoff darker. Loop-area ordering under > nominal > over is test-asserted. Head bump: +2.5 dB peaking at 60 Hz (a 7.5 ips-class machine; speed is not user-facing) on the wet output tap only — kept out of the loop so the
0 dB bump cannot push loop gain past unity (documented abstraction; the
in-loop LF story is the 30 Hz coupling-cap HP).
- Wow & flutter: seeded per instance from (module seed, track) — capstan once-around 0.45–0.75 Hz + pinch roller ≈2.4–3.4×. The capstan trajectory is shared by L/R (one motor); pinch motion has independent channel phase/rate variation. At each completed cycle the next rate is deterministically re-rolled inside its bounded range, spreading transport energy beyond two permanent spectral lines without changing the maximum pitch-deviation budget. Scrape flutter uses independent per-channel random control points at 40 Hz with first-derivative-continuous cubic interpolation (head contact is per-track — this is the stereo decorrelation). The scrape motion is normalized to about 0.36 samples RMS at 48 kHz and has no per-sample-white broadband tail. Phases and draws are seeded and advanced by samples, so replays and different host block partitions are bit-identical.
- Makeup calibration is PEAK-susceptibility, not quiet-point (2026-07-26 fix): J-A susceptibility peaks mid-loop ~8 dB above χ₀, so normalizing the quiet point to unity left mid-level stage gain at ~2.4× — inside the feedback loop, that made any
tapewith feedback ≥ ~0.41 self-oscillate forever (machina's flat −11 dBFS post-stop tail).calibratenow sweeps the level range and anchors the LOUDEST gain at unity: the operating range (~0.2–0.5) sits ≈unity, quiet signals pass below it (level-dependent per-pass loss — the tape-like direction to err), and the loop invariant ("nothing in the loop exceeds unity", the head-bump rule) holds at every level. Gates:ja_gain_never_exceeds_unity_and_determinism+tape_loop.rs::tape_feedback_tail_decays_after_input_stops.
Live time and bias#
Both controls accept composed signals. Live time glides with a 5 ms pole, bounded to one sample–10 seconds; the tape reader retains its three-sample minimum interpolation stencil. Both audio rings reserve 10.1 seconds before playback. Bias clamps 0–1 and uses the same smoothing time. Its calibration runs before playback at 33 grid points plus the exact initial setting, with linear interpolation during motion. Magnetization, filters, decimator and seeded mechanics keep running. Literal time/bias retain the original compact IR and direct calibration. Fractional reads split integer and fractional delay before wrapping, so a larger prepared ring does not lose interpolation precision.
Measured (dsp_validation.rs ja_* gates)#
Stage gain ≤ unity at every swept level (peak −0.06 dB at amp 0.3; gate ≤ +0.6 dB), loudest point ≈unity (gate ≥ −1.5 dB); post-stop tail at feedback 0.45/0.6/0.8 decays below −60 dBFS by 18 s (was: flat forever); loop area 0.48 hot / 1.5e-4 quiet (collapse gate ×0.05); bias areas ordering gate; alias floor −83.2 dB on a hot coherent sine (was −70.8 dB with the 2× time-stepped solver; the guide's budget is −60 dB); bit-identical determinism. effect::tests::ja_harmonics_match_a_fine_step_reference: H1–H9 within 0.022 dB of an RK4 integration of the exact law at 256 sub-steps per sample (gate 0.1 dB). (The level/area figures above predate the field-integrated solver; the gates still hold.) Golden clip tape_echo (re-blessed: repeats now decay per the feedback knob).
Oversampling factor (Stage G, §7): 8× (4× below drive 2). The earlier 2× choice cleared the −60 dB budget on a sine, but on bright input the dominant artifact was the sampled turning points, which oversampling with a linear midpoint did not fix; the band-limited 8× input with turning-point insertion does (above). The decimators are still the allpass-IIR half-bands (HalfbandDec); the 7-sample latency is the interpolating FIR's and sits inside the echo's loop as part of its delay.
Abstractions & limits#
Print-through and bias-dependent hiss: out of scope (documented). Head-bump accumulation per pass traded for loop stability (above). The old one-state TapeHysteresis shaper is still declared in engine/src/effect.rs, but no render path uses it — only the engine/tests/dsp_validation.rs gates construct it. It was the browser-compat flavour, deleted with that path. bbdDelay is the darker sibling — see Bucket-brigade chorus and delay.