Skip to main content

record_player/
vinyl_vfx.rs

1//! Geometry-driven vinyl VFX shared by live playback and offline REMIX.
2//!
3//! The processors in this module use record coordinates rather than wall
4//! clock time. A scene therefore keeps its phase when the platter slows,
5//! reverses, or moves under a hand.
6//!
7//! The polar memory stores one revolution of audio indexed by angle at
8//! full fidelity: 2^17 bins per turn oversamples a 33 1/3 RPM revolution
9//! at 48 kHz (86,400 frames) by ~1.5x, writes fill every bin the stylus
10//! crosses, and reads interpolate between bins. Each bin is tagged with
11//! the unwrapped turn time it was written, so a read only returns audio
12//! the stylus actually cut on the pass being asked for.
13
14use std::f64::consts::TAU;
15
16pub const VINYL_VFX_NONE: u32 = 0;
17pub const VINYL_VFX_ADJACENT_GHOST: u32 = 1;
18pub const VINYL_VFX_THREE_NEEDLES: u32 = 2;
19pub const VINYL_VFX_CUT_CONSTELLATION: u32 = 3;
20pub const VINYL_VFX_INNER_FIRE: u32 = 4;
21pub const VINYL_VFX_SPLIT_WALLS: u32 = 5;
22pub const VINYL_VFX_WORN_HALO: u32 = 6;
23pub const VINYL_VFX_PINCH: u32 = 7;
24/// Retired from the picker on 2026-09-09. The code stays reserved and the
25/// branch stays in `process_frame`: a take cut with this scene, and a share
26/// code carrying it, still render as they were made. See `docs/DECK_VFX.md`.
27pub const VINYL_VFX_NULL_POINTS: u32 = 8;
28pub const VINYL_VFX_OVERCUT: u32 = 9;
29pub const VINYL_VFX_MAX_SCENE: u32 = VINYL_VFX_OVERCUT;
30
31/// Where THREE NEEDLES stands its other two heads, as a fraction of one
32/// revolution — the near head at the spacing, the far one at twice it.
33///
34/// SPREAD steps this ladder rather than sweeping it. What makes the scene
35/// work is the heads landing on the beat, and a locked groove is exactly one
36/// turn, so these are fractions of the loop itself and are musical without
37/// anything knowing the tempo. Even thirds — where the scene was nailed
38/// down, and right only for a loop holding a multiple of three beats — is
39/// one rung among them now.
40///
41/// Nothing reaches half a turn. The far head sits at twice the near one, so
42/// at a half it would land a full revolution back, which on a locked groove
43/// is the needle itself.
44const THREE_NEEDLE_SPACINGS: [f64; 6] = [
45    1.0 / 16.0,
46    1.0 / 8.0,
47    3.0 / 16.0,
48    1.0 / 4.0,
49    1.0 / 3.0,
50    3.0 / 8.0,
51];
52
53/// Where each rung of the ladder stands on the SPREAD's throw: the centre
54/// of its sixth, so the amounts that landed the rungs when the ladder was
55/// stepped land them still.
56fn three_needle_anchor(index: usize) -> f64 {
57    (index as f64 + 0.5) / THREE_NEEDLE_SPACINGS.len() as f64
58}
59
60/// The spacing SPREAD is standing on. The ladder's six rungs are anchors
61/// and the throw between them is continuous: a record's own beat divides a
62/// turn where it divides it — `cut_rpm ÷ (k × BPM₀)`, which is 0.267 on an
63/// 84 BPM pressing and on no rung — and the heads have to be able to stand
64/// there, or they beat against every record that is not cut at a round
65/// number. Below the first anchor is the first rung, above the last the
66/// last; zero never arrives, an amount that low having already turned the
67/// processor off upstream.
68pub(crate) fn three_needle_spacing(amount: f64) -> f64 {
69    let count = THREE_NEEDLE_SPACINGS.len();
70    let amount = if amount.is_finite() { amount.clamp(0.0, 1.0) } else { three_needle_anchor(4) };
71    if amount <= three_needle_anchor(0) {
72        return THREE_NEEDLE_SPACINGS[0];
73    }
74    for index in 1..count {
75        let (low, high) = (three_needle_anchor(index - 1), three_needle_anchor(index));
76        if amount <= high {
77            let along = (amount - low) / (high - low);
78            return THREE_NEEDLE_SPACINGS[index - 1] + (THREE_NEEDLE_SPACINGS[index] - THREE_NEEDLE_SPACINGS[index - 1]) * along;
79        }
80    }
81    THREE_NEEDLE_SPACINGS[count - 1]
82}
83
84/// The inverse: the amount that lands a spacing, on a rung or between two.
85pub(crate) fn three_needle_amount(spacing: f64) -> f64 {
86    let count = THREE_NEEDLE_SPACINGS.len();
87    let spacing = if spacing.is_finite() { spacing } else { THREE_NEEDLE_SPACINGS[4] };
88    if spacing <= THREE_NEEDLE_SPACINGS[0] {
89        return three_needle_anchor(0);
90    }
91    for index in 1..count {
92        let (low, high) = (THREE_NEEDLE_SPACINGS[index - 1], THREE_NEEDLE_SPACINGS[index]);
93        if spacing <= high {
94            let along = (spacing - low) / (high - low);
95            return three_needle_anchor(index - 1) + (three_needle_anchor(index) - three_needle_anchor(index - 1)) * along;
96        }
97    }
98    three_needle_anchor(count - 1)
99}
100
101/// The swing that pinches: only the slow, wide lateral motion carries the
102/// stylus far enough up the walls to matter, and band-limiting to it keeps
103/// the octave it generates well clear of Nyquist.
104const PINCH_SWING_HZ: f64 = 900.0;
105/// How hard the ride is driven before it is bounded.
106///
107/// At 3.4 a mastered track reached full scale and clipped. The scene stands
108/// clear of the ceiling at this figure and still widens the record.
109const PINCH_RIDE: f64 = 2.5;
110/// The room the ride needs, as a fraction of the cut, at full RIDE.
111///
112/// The ride is added to one wall and taken off the other, so it costs
113/// headroom. A record mastered to the ceiling has none to give: played at
114/// unity it clipped 1,584 samples of a twenty second passage whatever the
115/// drive was set to, because the cut was already at full scale before the
116/// scene added anything. So the cut steps back by what the ride can put on
117/// top of it, which costs about 2 dB at full RIDE and clips nine samples of
118/// the same passage.
119const PINCH_HEADROOM: f64 = 0.35;
120/// The floor under the ride, and how many poles hold it.
121///
122/// Squaring a band returns the difference of every pair of partials as well
123/// as their sum. The sums are the octave the scene is for. The differences
124/// run down to zero, and dense music puts most of its pairs close together,
125/// so most of the square's energy lands below the music.
126///
127/// The ride enters the groove antiphase, which makes all of it side channel.
128/// A lathe cannot cut bass into the sides — the stylus lifts out of the
129/// groove — so a cutting chain takes the bass to the middle first, and an
130/// elliptical equaliser does it between 150 and 300 Hz. This floor stands at
131/// the top of that range. Measured on a mastered track, the ride carried 467
132/// times the record's own side-channel bass at 70 Hz and 16 times at 300 Hz.
133const PINCH_RIDE_FLOOR_HZ: f64 = 300.0;
134/// The floor in OVERCUT's loop, for the same reason and against a longer
135/// arithmetic: the loop repeats once a revolution, so what it sums under
136/// this floor is slower than the turn itself.
137const OVERCUT_FLOOR_HZ: f64 = 28.0;
138
139/// The lathe's protection circuit, and where it holds the low end.
140///
141/// A cutter head moves the stylus vertically for the side channel, and it can
142/// only move so far before the stylus leaves the groove. So a cutting chain
143/// carries an elliptical equaliser, which takes the low end toward the middle
144/// as it approaches that limit. It is a dynamic circuit and not a fixed
145/// filter: it acts on the passages that ask for it and leaves the rest alone.
146///
147/// Two scenes here manufacture side content. PINCH makes it by construction,
148/// because the ride enters the groove antiphase. OVERCUT makes it by
149/// compounding, because one generation's difference between the walls is fed
150/// into the next. Both end on this, so the gain each is set to is a question
151/// about what it sounds like rather than about what it does to the sides.
152///
153/// `ELLIPTICAL_RATIO` is how much low side the circuit allows against the low
154/// middle beside it. Below that figure nothing happens at all.
155const ELLIPTICAL_HZ: f64 = 200.0;
156const ELLIPTICAL_RATIO: f64 = 0.50;
157const ELLIPTICAL_ATTACK_HZ: f64 = 60.0;
158const ELLIPTICAL_RELEASE_HZ: f64 = 3.0;
159/// The gain the previous revolution is mixed back at, from LAYERS at zero to
160/// LAYERS at full.
161///
162/// A locked groove hands the loop the same audio every turn, so the returns
163/// add to each other instead of decaying and the sum converges on
164/// `1 / (1 - gain)` times the cut. The gain used to run to 0.92 into a hard
165/// clamp, which on a mastered track came back 4.8 dB above the record with
166/// sixty times its side-channel bass.
167///
168/// The ceiling under it is a knee now, so the loop has room to be worth
169/// hearing. Measured on a locked groove cut from a master already at 0.98,
170/// with LAYERS at full:
171///
172/// Measured on a master already at 0.98, at full LAYERS. `locked` is one
173/// revolution repeated; `playing` is a record walking through the passage.
174///
175/// | gain | locked | playing | peak | clipped | side/mid bass |
176/// | ---- | ------ | ------- | ---- | ------- | ------------- |
177/// | 0.50 |  1.6dB |  -0.4dB | 0.88 |       0 |         0.012 |
178/// | 0.70 |  2.5dB |  +0.1dB | 0.90 |       0 |         0.021 |
179/// | 0.85 |  3.2dB |  +0.6dB | 0.91 |       0 |         0.031 |
180///
181/// Neither the ceiling nor the sides set this. The knee holds the peak well
182/// past a gain of one, and the elliptical circuit acts at a side-to-middle
183/// figure of 0.5, which the loop does not approach.
184///
185/// Two things do. Stability is the hard limit: what one revolution returns is
186/// `gain` less what the 7 kHz lowpass, the floor and the interpolation of the
187/// polar read take — about half of it for ordinary programme, but nearly all
188/// of it for content that is low, coherent and repeating, which is exactly
189/// what a locked groove hands back. A gain of one against a return of one is
190/// a loop that never decays, so the range stops well short of it.
191///
192/// Taste is the other, and it is what set the figure. At 0.70 a playing
193/// record keeps the level it came in at, so LAYERS changes the record rather
194/// than its loudness, and the build arrives on the locked groove where the
195/// scene is for. The sum is taken through `tanh`, so the loop must not drive
196/// it far past the knee: at a layer near the old 0.70 the resonant return
197/// reached about 1.7 times the dry and every peak arrived compressed, which
198/// reads as crackle. A ceiling of 0.45 keeps the return under about 0.8.
199const OVERCUT_LAYER_MINIMUM: f64 = 0.10;
200const OVERCUT_LAYER_RANGE: f64 = 0.35;
201
202/// Where a pivoted arm is tangent to the groove, as a fraction of the way
203/// through the programme — the outer null first, because a record plays
204/// outward-in. Baerwald's two radii on a twelve inch, over the band a record
205/// is actually cut in.
206const NULL_POINT_OUTER: f64 = 0.29;
207const NULL_POINT_INNER: f64 = 0.93;
208/// The tangency error at its worst — the outer edge — brought to one.
209const NULL_POINT_ERROR_SCALE: f64 = 3.6;
210
211const POLAR_BINS: usize = 1 << 17;
212const WEAR_BINS: usize = 2_048;
213/// How long one groove position takes to wear through, at full amount and
214/// under a stylus that keeps returning to it.
215const WEAR_FULL_SECONDS: f64 = 20.0;
216/// A bin tag may differ from the requested time by a couple of bins of
217/// rounding before the content is someone else's revolution.
218const POLAR_TAG_TOLERANCE: f64 = 2.5 / POLAR_BINS as f64;
219const CONSTELLATION_SECTORS: u32 = 12;
220const CONSTELLATION_DEFAULT_PATTERN: u32 = 0b1011_0100_1101;
221
222#[derive(Clone, Copy, Debug)]
223pub struct VinylVfxContext {
224    pub sample_rate: f64,
225    pub rpm: f64,
226    pub start_turns: f64,
227    pub end_turns: f64,
228    pub start_position: f64,
229    pub end_position: f64,
230    pub total_frames: usize,
231    pub pressing_seed: u32,
232}
233
234impl Default for VinylVfxContext {
235    fn default() -> Self {
236        Self {
237            sample_rate: 48_000.0,
238            rpm: 33.333_333,
239            start_turns: 0.0,
240            end_turns: 0.0,
241            start_position: 0.0,
242            end_position: 0.0,
243            total_frames: 0,
244            pressing_seed: 0,
245        }
246    }
247}
248
249/// Preallocated state for one graph instance.
250///
251/// Live players keep one instance beside the acoustic DSP. Offline buses use
252/// the same type, so a scene's block order and state transitions are shared.
253///
254/// `Clone` is for the replay snapshot: a replay is a transaction on the live
255/// deck, and the revolution memory and the halo it leaves behind belong to
256/// the record that was playing, not to the take that was replayed.
257#[derive(Clone)]
258pub struct VinylVfxProcessor {
259    scene: u32,
260    amount: f64,
261    polar_samples: Vec<[f32; 2]>,
262    polar_written: Vec<f64>,
263    /// How many polar bins hold audio from this record. Counted as it is
264    /// written rather than walked when asked: the Vfx row reports fill on
265    /// every publish, and the buffer is 131,072 bins wide.
266    polar_filled: usize,
267    last_write: Option<(f64, [f32; 2])>,
268    wear: Vec<f32>,
269    lowpass: [f64; 2],
270    /// The floor PINCH and OVERCUT put under what they add. PINCH stands
271    /// three of its four poles here, the first being `lowpass[0]`; OVERCUT
272    /// stands one per channel.
273    highpass: [f64; 3],
274    /// The elliptical circuit: the low band of the side and of the middle,
275    /// then an envelope on each.
276    elliptical: [f64; 4],
277    gate_gain: f64,
278}
279
280/// Two million bins are not a debug line. The scene, the amount and how
281/// much of each buffer is holding something are.
282impl std::fmt::Debug for VinylVfxProcessor {
283    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
284        formatter
285            .debug_struct("VinylVfxProcessor")
286            .field("scene", &self.scene)
287            .field("amount", &self.amount)
288            .field("polar_filled", &self.polar_filled)
289            .field("wear_level", &self.wear_level())
290            .finish_non_exhaustive()
291    }
292}
293
294impl Default for VinylVfxProcessor {
295    fn default() -> Self {
296        Self::new()
297    }
298}
299
300impl VinylVfxProcessor {
301    pub fn new() -> Self {
302        Self {
303            scene: VINYL_VFX_NONE,
304            amount: 1.0,
305            polar_samples: vec![[0.0; 2]; POLAR_BINS],
306            polar_written: vec![f64::NEG_INFINITY; POLAR_BINS],
307            polar_filled: 0,
308            last_write: None,
309            wear: vec![0.0; WEAR_BINS],
310            lowpass: [0.0; 2],
311            highpass: [0.0; 3],
312            elliptical: [0.0; 4],
313            gate_gain: 1.0,
314        }
315    }
316
317    pub fn set_scene(&mut self, scene: u32, amount: f64) {
318        let scene = scene.min(VINYL_VFX_MAX_SCENE);
319        let amount = finite_or(amount, 1.0).clamp(0.0, 1.0);
320        if self.scene != scene {
321            self.scene = scene;
322            self.reset_transient_state();
323        }
324        self.amount = amount;
325    }
326
327    pub fn scene(&self) -> u32 {
328        self.scene
329    }
330
331    pub fn amount(&self) -> f64 {
332        self.amount
333    }
334
335    pub fn reset_transient_state(&mut self) {
336        self.polar_samples.fill([0.0; 2]);
337        self.polar_written.fill(f64::NEG_INFINITY);
338        self.polar_filled = 0;
339        self.last_write = None;
340        self.lowpass = [0.0; 2];
341        self.highpass = [0.0; 3];
342        self.elliptical = [0.0; 4];
343        self.gate_gain = 1.0;
344    }
345
346    /// Clears the wear bins.
347    ///
348    /// Wear deliberately survives a scene change — `set_scene` resets the
349    /// transient state and leaves this standing, because a worn record is
350    /// worn whichever scene is reading it. So this is the only way it goes
351    /// away, short of a new record on the platter.
352    pub fn reset_wear(&mut self) {
353        self.wear.fill(0.0);
354    }
355
356    /// Everything this processor has accumulated: the revolution memory,
357    /// the wear, and the filters riding on both.
358    pub fn reset_all(&mut self) {
359        self.reset_transient_state();
360        self.reset_wear();
361    }
362
363    /// The halo, for a take's world: WORN HALO's bins, 0..=1 by phase
364    /// within one revolution.
365    pub fn halo_wear_map(&self) -> Vec<f32> {
366        self.wear.clone()
367    }
368
369    /// Restores a halo. Shorter maps leave the rest of the turn mint;
370    /// longer ones are cut to the bins there are.
371    pub fn restore_halo_wear(&mut self, map: &[f32]) {
372        self.wear.fill(0.0);
373        for (slot, value) in self.wear.iter_mut().zip(map.iter()) {
374            *slot = if value.is_finite() { value.clamp(0.0, 1.0) } else { 0.0 };
375        }
376    }
377
378    /// Mean wear around the revolution, 0..=1 — the number the meter fills to.
379    pub fn wear_level(&self) -> f64 {
380        if self.wear.is_empty() {
381            return 0.0;
382        }
383        let total: f64 = self.wear.iter().map(|value| f64::from(*value)).sum();
384        total / self.wear.len() as f64
385    }
386
387    /// The worst-worn bin, 0..=1 — the meter's tick. A record worn through
388    /// in one bar reads far worse here than its mean admits.
389    pub fn wear_peak(&self) -> f64 {
390        self.wear
391            .iter()
392            .fold(0.0_f64, |peak, value| peak.max(f64::from(*value)))
393    }
394
395    /// How much of the revolution memory holds this record, 0..=1.
396    pub fn polar_fill_ratio(&self) -> f64 {
397        self.polar_filled as f64 / POLAR_BINS as f64
398    }
399
400    /// What this processor is holding, in bytes. Reported rather than
401    /// assumed: the Vfx row shows the real cost of the memory a scene reads
402    /// back from, and the buffers are the largest thing on a deck.
403    pub fn memory_bytes(&self) -> usize {
404        self.polar_samples.len() * std::mem::size_of::<[f32; 2]>()
405            + self.polar_written.len() * std::mem::size_of::<f64>()
406            + self.wear.len() * std::mem::size_of::<f32>()
407    }
408
409    pub const fn wear_bin_count() -> usize {
410        WEAR_BINS
411    }
412
413    pub const fn polar_bin_count() -> usize {
414        POLAR_BINS
415    }
416
417    pub fn process_interleaved(
418        &mut self,
419        samples: &mut [f32],
420        channel_count: usize,
421        context: VinylVfxContext,
422    ) {
423        if self.scene == VINYL_VFX_NONE
424            || self.amount <= f64::EPSILON
425            || !(1..=2).contains(&channel_count)
426        {
427            return;
428        }
429        let frame_count = samples.len() / channel_count;
430        if frame_count == 0 {
431            return;
432        }
433        for frame_index in 0..frame_count {
434            let offset = frame_index * channel_count;
435            let mut frame = [samples[offset], samples[offset]];
436            if channel_count == 2 {
437                frame[1] = samples[offset + 1];
438            }
439            self.process_frame(&mut frame, channel_count, frame_index, frame_count, context);
440            samples[offset] = frame[0];
441            if channel_count == 2 {
442                samples[offset + 1] = frame[1];
443            }
444        }
445    }
446
447    pub fn process_planar(
448        &mut self,
449        left: &mut [f32],
450        right: &mut [f32],
451        context: VinylVfxContext,
452    ) {
453        if self.scene == VINYL_VFX_NONE || self.amount <= f64::EPSILON {
454            return;
455        }
456        let frame_count = left.len().min(right.len());
457        for frame_index in 0..frame_count {
458            let mut frame = [left[frame_index], right[frame_index]];
459            self.process_frame(&mut frame, 2, frame_index, frame_count, context);
460            left[frame_index] = frame[0];
461            right[frame_index] = frame[1];
462        }
463    }
464
465    /// Takes the low end toward the middle when the sides ask for more than a
466    /// groove will hold. See `ELLIPTICAL_HZ`.
467    fn bass_to_middle(&mut self, frame: &mut [f32; 2], sample_rate: f64) {
468        let mid = (f64::from(frame[0]) + f64::from(frame[1])) * 0.5;
469        let side = (f64::from(frame[0]) - f64::from(frame[1])) * 0.5;
470        let corner = 1.0 - (-TAU * ELLIPTICAL_HZ / sample_rate).exp();
471        self.elliptical[0] += (side - self.elliptical[0]) * corner;
472        self.elliptical[1] += (mid - self.elliptical[1]) * corner;
473        let side_low = self.elliptical[0];
474        let mid_low = self.elliptical[1];
475
476        // The envelope rises quickly and falls slowly, so the circuit answers
477        // a bass note as it arrives and does not chatter between them.
478        let attack = 1.0 - (-TAU * ELLIPTICAL_ATTACK_HZ / sample_rate).exp();
479        let release = 1.0 - (-TAU * ELLIPTICAL_RELEASE_HZ / sample_rate).exp();
480        for (slot, level) in [(2usize, side_low.abs()), (3usize, mid_low.abs())] {
481            let rate = if level > self.elliptical[slot] { attack } else { release };
482            self.elliptical[slot] += (level - self.elliptical[slot]) * rate;
483        }
484
485        let allowed = self.elliptical[3] * ELLIPTICAL_RATIO;
486        let carried = self.elliptical[2];
487        // Only what is over the allowance moves, and only the low band moves:
488        // the sides keep everything above the corner.
489        let held = if carried > allowed && carried > 1.0e-9 {
490            side_low * (1.0 - allowed / carried)
491        } else {
492            0.0
493        };
494        let corrected = side - held;
495        frame[0] = soft_limit(mid + corrected);
496        frame[1] = soft_limit(mid - corrected);
497    }
498
499    fn process_frame(
500        &mut self,
501        frame: &mut [f32; 2],
502        channel_count: usize,
503        frame_index: usize,
504        frame_count: usize,
505        context: VinylVfxContext,
506    ) {
507        let progress = if frame_count > 1 {
508            frame_index as f64 / (frame_count - 1) as f64
509        } else {
510            0.0
511        };
512        let turns = interpolate(context.start_turns, context.end_turns, progress);
513        let position = interpolate(context.start_position, context.end_position, progress);
514        let total_frames = context.total_frames.max(1) as f64;
515        let radius = (position / total_frames).clamp(0.0, 1.0);
516        let signed_motion = context.end_turns - context.start_turns;
517        let direction = if signed_motion < 0.0 { -1.0 } else { 1.0 };
518        let dry = *frame;
519
520        match self.scene {
521            VINYL_VFX_ADJACENT_GHOST => {
522                let previous_turn = self.read_polar(turns - direction, channel_count);
523                let transfer = self.amount * 0.72;
524                // The ghost is summed with the dry, and a correlated previous
525                // turn sums toward twice the dry, deep into `soft_limit` — the
526                // crackle. Normalise by the most it can add, so the dry keeps
527                // its level and the sum cannot leave the linear region.
528                let normalise = 1.0 / (1.0 + transfer);
529                for channel in 0..channel_count {
530                    self.lowpass[channel] +=
531                        (f64::from(previous_turn[channel]) - self.lowpass[channel]) * 0.22;
532                    frame[channel] = soft_limit(
533                        (f64::from(dry[channel]) + self.lowpass[channel] * transfer) * normalise,
534                    );
535                }
536            }
537            VINYL_VFX_THREE_NEEDLES => {
538                // SPREAD moves the heads, not their level: a needle that is
539                // merely quieter is the same needle in the same wrong place.
540                let spacing = three_needle_spacing(self.amount);
541                let second = self.read_polar(turns - direction * spacing, channel_count);
542                let third = self.read_polar(turns - direction * spacing * 2.0, channel_count);
543                let level = 0.52;
544                for channel in 0..channel_count {
545                    let head_mix =
546                        f64::from(second[channel]) * 0.58 + f64::from(third[channel]) * 0.42;
547                    frame[channel] = soft_limit(f64::from(dry[channel]) + head_mix * level);
548                }
549            }
550            VINYL_VFX_CUT_CONSTELLATION => {
551                let phase = turns.rem_euclid(1.0);
552                let sector = ((phase * f64::from(CONSTELLATION_SECTORS)).floor() as u32)
553                    .min(CONSTELLATION_SECTORS - 1);
554                let pattern = constellation_pattern(context.pressing_seed);
555                let open = pattern & (1 << sector) != 0;
556                let target = if open { 1.0 } else { 1.0 - self.amount * 0.96 };
557                let sample_rate = finite_or(context.sample_rate, 48_000.0).max(1.0);
558                let alpha = 1.0 - (-1.0 / (sample_rate * 0.0018)).exp();
559                self.gate_gain += (target - self.gate_gain) * alpha;
560                for sample in frame.iter_mut().take(channel_count) {
561                    *sample = (f64::from(*sample) * self.gate_gain) as f32;
562                }
563            }
564            VINYL_VFX_INNER_FIRE => {
565                let heat = self.amount * (0.35 + 0.65 * radius.powf(1.35));
566                let sample_rate = finite_or(context.sample_rate, 48_000.0).max(1.0);
567                let cutoff = 18_000.0 - heat * 11_000.0;
568                let alpha = 1.0 - (-TAU * cutoff / sample_rate).exp();
569                for channel in 0..channel_count {
570                    let driven = (f64::from(dry[channel]) * (1.0 + heat * 1.8)).tanh();
571                    self.lowpass[channel] += (driven - self.lowpass[channel]) * alpha;
572                    frame[channel] = soft_limit(self.lowpass[channel] * (1.0 - heat * 0.22));
573                }
574                if channel_count == 2 {
575                    narrow_stereo(frame, heat * 0.6);
576                }
577            }
578            VINYL_VFX_SPLIT_WALLS => {
579                let left_head = self.read_polar(turns - direction * 0.25, channel_count);
580                let right_head = self.read_polar(turns - direction * 0.625, channel_count);
581                let wall = self.amount * 0.68;
582                if channel_count == 2 {
583                    let lateral = (f64::from(dry[0]) + f64::from(dry[1])) * 0.5;
584                    let vertical = (f64::from(dry[0]) - f64::from(dry[1])) * 0.5;
585                    let delayed_lateral = (f64::from(left_head[0]) + f64::from(left_head[1])) * 0.5;
586                    let delayed_vertical =
587                        (f64::from(right_head[0]) - f64::from(right_head[1])) * 0.5;
588                    frame[0] = soft_limit(lateral + vertical + delayed_lateral * wall);
589                    frame[1] = soft_limit(lateral - vertical - delayed_vertical * wall);
590                } else {
591                    frame[0] = soft_limit(
592                        f64::from(dry[0])
593                            + (f64::from(left_head[0]) - f64::from(right_head[0])) * wall,
594                    );
595                }
596            }
597            VINYL_VFX_WORN_HALO => {
598                let wear_index = wear_bin(turns);
599                let sample_rate = finite_or(context.sample_rate, 48_000.0).max(1.0);
600                // A bin is one angle of the revolution, so the stylus
601                // is over it for `frames per turn / WEAR_BINS` frames of
602                // each pass rather than for the whole pass. The rate
603                // carries that factor: without it the twenty seconds is
604                // twenty seconds times the bin count, which is eleven hours
605                // on one locked groove.
606                let increment =
607                    self.amount * WEAR_BINS as f64 / (sample_rate * WEAR_FULL_SECONDS);
608                self.wear[wear_index] =
609                    (f64::from(self.wear[wear_index]) + increment).clamp(0.0, 1.0) as f32;
610                let worn = f64::from(self.wear[wear_index]).sqrt() * self.amount;
611                let cutoff = 19_000.0 - worn * 11_000.0;
612                let alpha = 1.0 - (-TAU * cutoff / sample_rate).exp();
613                let crackle = deterministic_crackle(
614                    wear_index as u64,
615                    turns.floor() as i64,
616                    context.pressing_seed,
617                    worn,
618                );
619                for channel in 0..channel_count {
620                    self.lowpass[channel] +=
621                        (f64::from(dry[channel]) - self.lowpass[channel]) * alpha;
622                    frame[channel] = soft_limit(self.lowpass[channel] + crackle);
623                }
624            }
625            VINYL_VFX_PINCH => {
626                // A round stylus sitting in a V is pushed *up* as the walls
627                // close on it, either way it swings. So a lateral cut
628                // generates a vertical one at twice the frequency — the
629                // record's own way of making width out of something cut in
630                // mono. Real; it is why pinch effect is something cutting
631                // engineers have to allow for.
632                //
633                // The rise is smooth. It goes as the *square* of the lateral
634                // displacement, not as its absolute value: rectifying has a
635                // corner at zero, and a corner is not a second harmonic but
636                // every even harmonic at once, most of them above Nyquist
637                // and folding back as grit. Squaring has no corner.
638                //
639                // And the swing that pinches is the big slow one. A stylus
640                // is only carried far enough up the walls to matter by bass,
641                // so the lateral is band-limited before it is squared, which
642                // also puts the octave it generates nowhere near Nyquist.
643                let sample_rate = finite_or(context.sample_rate, 48_000.0).max(1.0);
644                let lateral = (f64::from(dry[0]) + f64::from(dry[1])) * 0.5;
645                let vertical = (f64::from(dry[0]) - f64::from(dry[1])) * 0.5;
646                let swing_alpha = 1.0 - (-TAU * PINCH_SWING_HZ / sample_rate).exp();
647                self.lowpass[1] += (lateral - self.lowpass[1]) * swing_alpha;
648                let squared = self.lowpass[1] * self.lowpass[1];
649                // Squaring leaves a standing offset behind, and under it
650                // the difference of every pair of partials in the band. The
651                // stylus rides on none of that: the offset is not music, and
652                // the differences run below the octave the scene is for.
653                // Four poles at PINCH_RIDE_FLOOR_HZ take them together.
654                let floor_alpha = 1.0 - (-TAU * PINCH_RIDE_FLOOR_HZ / sample_rate).exp();
655                self.lowpass[0] += (squared - self.lowpass[0]) * floor_alpha;
656                let mut ridden = squared - self.lowpass[0];
657                for pole in 0..3 {
658                    self.highpass[pole] += (ridden - self.highpass[pole]) * floor_alpha;
659                    ridden -= self.highpass[pole];
660                }
661                // Bounded rather than clipped: a stylus can only ride so far
662                // up a wall before it leaves it, and a clamp here is a
663                // fuzzbox.
664                let ride = (ridden * self.amount * PINCH_RIDE).tanh();
665                let headroom = 1.0 / (1.0 + self.amount * PINCH_HEADROOM);
666                frame[0] = soft_limit((lateral + vertical + ride) * headroom);
667                frame[1] = soft_limit((lateral - vertical - ride) * headroom);
668                if channel_count == 2 {
669                    self.bass_to_middle(frame, sample_rate);
670                }
671            }
672            VINYL_VFX_NULL_POINTS => {
673                // A pivoted arm is tangent to the groove at exactly two
674                // radii and wrong everywhere else, and the error it leaves
675                // is mostly second harmonic. So the grit is a *place on the
676                // record*: it blooms at the edge, falls to nothing at the
677                // outer null, rises again through the middle and dies at the
678                // inner one. The two walls do not meet the error at the same
679                // angle, so they do not take the same amount of it.
680                let sample_rate = finite_or(context.sample_rate, 48_000.0).max(1.0);
681                let tangency = (radius - NULL_POINT_OUTER) * (radius - NULL_POINT_INNER);
682                let bend = (tangency.abs() * NULL_POINT_ERROR_SCALE).min(1.0)
683                    * self.amount
684                    * 0.9;
685                let dc_alpha = 1.0 - (-TAU * 18.0 / sample_rate).exp();
686                let lean = [bend, bend * 0.72];
687                for channel in 0..channel_count {
688                    let sample = f64::from(dry[channel]);
689                    // Squaring is the second harmonic, and it arrives with a
690                    // standing offset that has to go back out.
691                    let squared = sample * sample;
692                    self.lowpass[channel] += (squared - self.lowpass[channel]) * dc_alpha;
693                    frame[channel] =
694                        soft_limit(sample + (squared - self.lowpass[channel]) * lean[channel] * 2.4);
695                }
696            }
697            VINYL_VFX_OVERCUT => {
698                // Every other scene reads the groove and leaves it as it
699                // found it. This one cuts back into it: what comes out is
700                // written where it was read, so the next revolution arrives
701                // carrying the last one, and the one before that. A delay
702                // whose line is the record and whose time is a turn, by
703                // construction rather than by setting.
704                //
705                // Each pass loses its air, the way a plate cut from a plate
706                // does, and the sum is taken through tanh rather than
707                // clipped: it thickens and compresses into itself instead of
708                // shattering.
709                let sample_rate = finite_or(context.sample_rate, 48_000.0).max(1.0);
710                let previous = self.read_polar(turns - direction, channel_count);
711                let layer = OVERCUT_LAYER_MINIMUM + self.amount * OVERCUT_LAYER_RANGE;
712                let alpha = 1.0 - (-TAU * 7_000.0 / sample_rate).exp();
713                // Each generation loses its bottom as well as its air. A
714                // one-pole lowpass passes DC at unity, so with no floor the
715                // loop sums its own offset and everything under the turn
716                // rate with it, at `layer` a revolution.
717                let floor_alpha = 1.0 - (-TAU * OVERCUT_FLOOR_HZ / sample_rate).exp();
718                for channel in 0..channel_count {
719                    self.lowpass[channel] +=
720                        (f64::from(previous[channel]) - self.lowpass[channel]) * alpha;
721                    self.highpass[channel] +=
722                        (self.lowpass[channel] - self.highpass[channel]) * floor_alpha;
723                    let layered = self.lowpass[channel] - self.highpass[channel];
724                    frame[channel] =
725                        (f64::from(dry[channel]) + layered * layer).tanh() as f32;
726                }
727                // Before the pass is written back, so a generation cannot
728                // hand its excess to the next one.
729                if channel_count == 2 {
730                    self.bass_to_middle(frame, sample_rate);
731                }
732            }
733            _ => {}
734        }
735
736        // OVERCUT is the one scene that keeps what it made rather than what
737        // it was handed. Everything else leaves the groove as it found it.
738        let cut = if self.scene == VINYL_VFX_OVERCUT {
739            *frame
740        } else {
741            dry
742        };
743        self.write_polar(turns, cut);
744    }
745
746    /// Interpolated read at an unwrapped turn coordinate. Bins whose write
747    /// tag disagrees with the requested time contribute silence rather than
748    /// another revolution's audio.
749    fn read_polar(&self, turns: f64, channel_count: usize) -> [f32; 2] {
750        let turns = finite_or(turns, 0.0);
751        let bin_position = turns * POLAR_BINS as f64;
752        let base = bin_position.floor();
753        let fraction = bin_position - base;
754        let mut value = [0.0_f64; 2];
755        for (step, weight) in [(0.0, 1.0 - fraction), (1.0, fraction)] {
756            let unwrapped = base + step;
757            let index = wrap_bin(unwrapped);
758            let expected = unwrapped / POLAR_BINS as f64;
759            if (self.polar_written[index] - expected).abs() <= POLAR_TAG_TOLERANCE {
760                value[0] += f64::from(self.polar_samples[index][0]) * weight;
761                value[1] += f64::from(self.polar_samples[index][1]) * weight;
762            }
763        }
764        let mut frame = [value[0] as f32, value[1] as f32];
765        if channel_count == 1 {
766            frame[1] = frame[0];
767        }
768        frame
769    }
770
771    /// Records the stylus pass, filling every bin crossed since the last
772    /// write with linearly interpolated audio so the memory has no holes at
773    /// any platter speed, in either direction.
774    fn write_polar(&mut self, turns: f64, frame: [f32; 2]) {
775        let turns = finite_or(turns, 0.0);
776        let bin_position = turns * POLAR_BINS as f64;
777        if let Some((previous_turns, previous_frame)) = self.last_write {
778            let previous_position = previous_turns * POLAR_BINS as f64;
779            let span = bin_position - previous_position;
780            // A jump wider than a quarter turn is a seek, not a pass.
781            if span != 0.0 && span.abs() <= POLAR_BINS as f64 * 0.25 {
782                let low = previous_position.min(bin_position);
783                let high = previous_position.max(bin_position);
784                let mut bin = low.ceil();
785                while bin <= high {
786                    let t = ((bin - previous_position) / span).clamp(0.0, 1.0);
787                    let index = wrap_bin(bin);
788                    self.polar_samples[index] = [
789                        lerp(previous_frame[0], frame[0], t),
790                        lerp(previous_frame[1], frame[1], t),
791                    ];
792                    if !self.polar_written[index].is_finite() {
793                        self.polar_filled += 1;
794                    }
795                    self.polar_written[index] = bin / POLAR_BINS as f64;
796                    bin += 1.0;
797                }
798                self.last_write = Some((turns, frame));
799                return;
800            }
801        }
802        let index = wrap_bin(bin_position.floor());
803        self.polar_samples[index] = frame;
804        if !self.polar_written[index].is_finite() {
805            self.polar_filled += 1;
806        }
807        self.polar_written[index] = bin_position.floor() / POLAR_BINS as f64;
808        self.last_write = Some((turns, frame));
809    }
810}
811
812fn wrap_bin(unwrapped: f64) -> usize {
813    (unwrapped.rem_euclid(POLAR_BINS as f64) as usize).min(POLAR_BINS - 1)
814}
815
816fn wear_bin(turns: f64) -> usize {
817    let phase = finite_or(turns, 0.0).rem_euclid(1.0);
818    ((phase * WEAR_BINS as f64).floor() as usize).min(WEAR_BINS - 1)
819}
820
821fn lerp(start: f32, end: f32, t: f64) -> f32 {
822    (f64::from(start) + (f64::from(end) - f64::from(start)) * t) as f32
823}
824
825/// The sector map is a property of the pressing: seed zero keeps the house
826/// cut, any other pressing carves its own — always with at least three open
827/// and three closed sectors so the gate stays a rhythm, not a mute.
828fn constellation_pattern(seed: u32) -> u32 {
829    if seed == 0 {
830        return CONSTELLATION_DEFAULT_PATTERN;
831    }
832    let mut hash = u64::from(seed).wrapping_mul(0x9E37_79B9_7F4A_7C15);
833    for _ in 0..8 {
834        hash ^= hash >> 30;
835        hash = hash.wrapping_mul(0xBF58_476D_1CE4_E5B9);
836        hash ^= hash >> 27;
837        let pattern = (hash as u32) & 0xFFF;
838        let open = pattern.count_ones();
839        if (3..=9).contains(&open) {
840            return pattern;
841        }
842        hash = hash.wrapping_add(0x9E37_79B9);
843    }
844    CONSTELLATION_DEFAULT_PATTERN
845}
846
847fn interpolate(start: f64, end: f64, progress: f64) -> f64 {
848    finite_or(start, 0.0) + (finite_or(end, start) - finite_or(start, 0.0)) * progress
849}
850
851fn finite_or(value: f64, fallback: f64) -> f64 {
852    if value.is_finite() {
853        value
854    } else {
855        fallback
856    }
857}
858
859/// The ceiling every scene ends on.
860///
861/// This was `clamp(-1.0, 1.0)`, which is a square corner, and a corner is
862/// every odd harmonic at once — the harshest sound a limiter can make, and
863/// the one a scene reaches for exactly when it is already loud. Below the
864/// knee the value passes through unchanged. Above it the curve carries it
865/// toward one and never past, with the slope matched at the knee so there is
866/// no corner anywhere.
867fn soft_limit(value: f64) -> f32 {
868    const KNEE: f64 = 0.75;
869    let magnitude = value.abs();
870    if magnitude <= KNEE {
871        return value as f32;
872    }
873    let over = (magnitude - KNEE) / (1.0 - KNEE);
874    (value.signum() * (KNEE + (1.0 - KNEE) * over.tanh())) as f32
875}
876
877fn narrow_stereo(frame: &mut [f32; 2], amount: f64) {
878    let amount = amount.clamp(0.0, 1.0);
879    let mid = (f64::from(frame[0]) + f64::from(frame[1])) * 0.5;
880    let side = (f64::from(frame[0]) - f64::from(frame[1])) * 0.5 * (1.0 - amount);
881    frame[0] = soft_limit(mid + side);
882    frame[1] = soft_limit(mid - side);
883}
884
885fn deterministic_crackle(index: u64, turn: i64, seed: u32, worn: f64) -> f64 {
886    if worn <= f64::EPSILON {
887        return 0.0;
888    }
889    let mut hash = index
890        .wrapping_mul(0x9E37_79B9_7F4A_7C15)
891        .wrapping_add(turn as u64)
892        .wrapping_add(u64::from(seed));
893    hash ^= hash >> 30;
894    hash = hash.wrapping_mul(0xBF58_476D_1CE4_E5B9);
895    hash ^= hash >> 27;
896    let chance = hash & 0x7ff;
897    if chance >= (worn * 22.0) as u64 {
898        return 0.0;
899    }
900    let bipolar = ((hash >> 16) & 0xffff) as f64 / 32_767.5 - 1.0;
901    bipolar * worn * 0.09
902}
903
904#[cfg(test)]
905mod tests {
906    use super::*;
907
908    #[test]
909    fn spread_walks_the_three_needle_ladder_and_still_reaches_even_thirds() {
910        // Every rung is reachable, in order, at its anchor on the throw —
911        // the same amounts that landed it when the ladder was stepped.
912        let walked = (0..THREE_NEEDLE_SPACINGS.len())
913            .map(|index| three_needle_spacing(three_needle_anchor(index)))
914            .collect::<Vec<_>>();
915        assert_eq!(walked, THREE_NEEDLE_SPACINGS.to_vec());
916        // Between two rungs the throw is continuous and monotonic, and the
917        // inverse lands back where it started: a record's own division of a
918        // turn, 0.267 on an 84 BPM pressing, is a place the heads can stand.
919        let mut last = 0.0;
920        for step in 0..=200 {
921            let spacing = three_needle_spacing(step as f64 / 200.0);
922            assert!(spacing >= last, "the ladder went backwards at {step}");
923            last = spacing;
924            assert!((three_needle_spacing(three_needle_amount(spacing)) - spacing).abs() < 1e-9);
925        }
926        assert!((three_needle_spacing(three_needle_amount(0.267)) - 0.267).abs() < 1e-9);
927        // The placement the scene was nailed down at is one of them.
928        assert!(THREE_NEEDLE_SPACINGS.contains(&(1.0 / 3.0)));
929        // The bottom of the throw is the closest spacing, not silence: an
930        // amount that low has already turned the processor off upstream.
931        assert_eq!(three_needle_spacing(0.0), THREE_NEEDLE_SPACINGS[0]);
932        // The far head never lands a whole revolution back, which on a
933        // locked groove would be the needle itself.
934        for spacing in THREE_NEEDLE_SPACINGS {
935            assert!(spacing * 2.0 < 1.0);
936        }
937    }
938
939    fn context(turns: f64, frames: usize) -> VinylVfxContext {
940        VinylVfxContext {
941            start_turns: turns,
942            end_turns: turns + frames as f64 / 4_800.0,
943            end_position: frames as f64,
944            total_frames: frames * 4,
945            ..VinylVfxContext::default()
946        }
947    }
948
949    #[test]
950    fn bypass_is_bit_exact() {
951        let mut processor = VinylVfxProcessor::new();
952        let mut samples = vec![0.25_f32, -0.5, 0.75, -0.125];
953        let expected = samples.clone();
954        processor.process_interleaved(&mut samples, 2, context(0.0, 2));
955        assert_eq!(samples, expected);
956    }
957
958    #[test]
959    fn constellation_is_painted_on_turns_not_clock_time() {
960        let mut processor = VinylVfxProcessor::new();
961        processor.set_scene(VINYL_VFX_CUT_CONSTELLATION, 1.0);
962        let mut first = vec![0.5_f32; 9_600];
963        processor.process_interleaved(&mut first, 2, context(0.0, 4_800));
964        assert!(first.iter().any(|sample| sample.abs() < 0.2));
965        assert!(first.iter().any(|sample| sample.abs() > 0.45));
966    }
967
968    #[test]
969    fn constellation_repeats_the_same_cut_every_revolution() {
970        let mut processor = VinylVfxProcessor::new();
971        processor.set_scene(VINYL_VFX_CUT_CONSTELLATION, 1.0);
972        let mut first = vec![0.5_f32; 9_600];
973        processor.process_interleaved(&mut first, 2, context(0.0, 4_800));
974        let mut second = vec![0.5_f32; 9_600];
975        processor.process_interleaved(&mut second, 2, context(1.0, 4_800));
976        let mut third = vec![0.5_f32; 9_600];
977        processor.process_interleaved(&mut third, 2, context(2.0, 4_800));
978        // Once the gate slew has settled, revolution three must retrace
979        // revolution two sample for sample.
980        let drift = second
981            .iter()
982            .zip(third.iter())
983            .skip(1_000)
984            .map(|(a, b)| (a - b).abs())
985            .fold(0.0_f32, f32::max);
986        assert!(drift < 0.01, "cut drifted between revolutions: {drift}");
987    }
988
989    #[test]
990    fn constellation_cut_belongs_to_the_pressing() {
991        let mut house = VinylVfxProcessor::new();
992        house.set_scene(VINYL_VFX_CUT_CONSTELLATION, 1.0);
993        let mut pressed = VinylVfxProcessor::new();
994        pressed.set_scene(VINYL_VFX_CUT_CONSTELLATION, 1.0);
995        let mut house_output = vec![0.5_f32; 9_600];
996        house.process_interleaved(&mut house_output, 2, context(0.0, 4_800));
997        let mut pressed_output = vec![0.5_f32; 9_600];
998        let seeded = VinylVfxContext {
999            pressing_seed: 0xB17_5EED,
1000            ..context(0.0, 4_800)
1001        };
1002        pressed.process_interleaved(&mut pressed_output, 2, seeded);
1003        assert!(house_output
1004            .iter()
1005            .zip(pressed_output.iter())
1006            .any(|(a, b)| (a - b).abs() > 0.1));
1007    }
1008
1009    #[test]
1010    fn adjacent_ghost_reads_the_previous_revolution() {
1011        let mut processor = VinylVfxProcessor::new();
1012        processor.set_scene(VINYL_VFX_ADJACENT_GHOST, 1.0);
1013        let mut first = vec![0.4_f32; 9_600];
1014        processor.process_interleaved(&mut first, 2, context(0.0, 4_800));
1015        let mut second = vec![0.0_f32; 9_600];
1016        processor.process_interleaved(&mut second, 2, context(1.0, 4_800));
1017        assert!(second.iter().any(|sample| sample.abs() > 0.05));
1018    }
1019
1020    #[test]
1021    fn ghost_echo_is_smooth_not_stair_stepped() {
1022        // A sine cut on revolution one must come back on revolution two as
1023        // a smooth (filtered) wave. The old 4,096-bin memory returned a
1024        // zero-order-hold staircase whose sample-to-sample jumps rival the
1025        // signal itself.
1026        let mut processor = VinylVfxProcessor::new();
1027        processor.set_scene(VINYL_VFX_ADJACENT_GHOST, 1.0);
1028        let frames = 4_800_usize;
1029        let mut first: Vec<f32> = (0..frames)
1030            .flat_map(|i| {
1031                let value = (i as f64 / frames as f64 * TAU * 96.0).sin() as f32 * 0.6;
1032                [value, value]
1033            })
1034            .collect();
1035        processor.process_interleaved(&mut first, 2, context(0.0, frames));
1036        let mut second = vec![0.0_f32; frames * 2];
1037        processor.process_interleaved(&mut second, 2, context(1.0, frames));
1038        let ghost: Vec<f32> = second.chunks_exact(2).map(|f| f[0]).collect();
1039        let peak = ghost.iter().fold(0.0_f32, |a, s| a.max(s.abs()));
1040        assert!(peak > 0.05, "no ghost came back: {peak}");
1041        let max_jump = ghost
1042            .windows(2)
1043            .skip(200)
1044            .map(|w| (w[1] - w[0]).abs())
1045            .fold(0.0_f32, f32::max);
1046        // 96 cycles per 4,800-frame turn moves at most ~7.5% of the peak
1047        // per sample; allow filter transients double that.
1048        assert!(
1049            max_jump < peak * 0.15,
1050            "ghost is stair-stepped: jump {max_jump} against peak {peak}"
1051        );
1052    }
1053
1054    #[test]
1055    fn pinch_makes_width_out_of_a_mono_cut() {
1056        let mut processor = VinylVfxProcessor::new();
1057        processor.set_scene(VINYL_VFX_PINCH, 1.0);
1058        let frames = 4_800_usize;
1059        // Dead centre: nothing but lateral motion, no side at all.
1060        let mut samples: Vec<f32> = (0..frames)
1061            .flat_map(|i| {
1062                let value = (i as f64 / frames as f64 * TAU * 40.0).sin() as f32 * 0.6;
1063                [value, value]
1064            })
1065            .collect();
1066        processor.process_interleaved(&mut samples, 2, context(0.0, frames));
1067        let side = samples
1068            .chunks_exact(2)
1069            .fold(0.0_f32, |peak, frame| peak.max((frame[0] - frame[1]).abs()));
1070        assert!(side > 0.05, "a mono cut should ride its way into width");
1071        let peak = samples.iter().fold(0.0_f32, |a, s| a.max(s.abs()));
1072        assert!(peak <= 1.0);
1073
1074        // The width it makes is an octave, not a fuzzbox. Rectifying used to
1075        // put a corner in the signal, and a corner is every even harmonic at
1076        // once — most of them past Nyquist and folding back. A squared,
1077        // band-limited swing keeps the side smooth: consecutive samples move
1078        // by about what a 80 Hz tone moves by, not by a step.
1079        let side_track: Vec<f32> = samples
1080            .chunks_exact(2)
1081            .map(|frame| (frame[0] - frame[1]) * 0.5)
1082            .collect();
1083        //
1084        // How fast a signal moves against how big it is places where its
1085        // energy sits. A clean octave of a 400 Hz cut is 800 Hz, which moves
1086        // about a tenth of its own height per sample; a rectified corner
1087        // sprays energy far above that.
1088        let energy = |track: &[f32]| -> f64 {
1089            (track.iter().map(|v| f64::from(*v) * f64::from(*v)).sum::<f64>()
1090                / track.len() as f64)
1091                .sqrt()
1092        };
1093        let steps: Vec<f32> = side_track
1094            .windows(2)
1095            .map(|pair| pair[1] - pair[0])
1096            .collect();
1097        let brightness = energy(&steps) / energy(&side_track).max(1.0e-9);
1098        assert!(
1099            brightness < 0.2,
1100            "the ride should be an octave, not a fuzzbox: {brightness}"
1101        );
1102    }
1103
1104    #[test]
1105    fn null_points_are_clean_and_the_edge_is_not() {
1106        fn grit(radius_progress: f64) -> f32 {
1107            let mut processor = VinylVfxProcessor::new();
1108            processor.set_scene(VINYL_VFX_NULL_POINTS, 1.0);
1109            let frames = 4_800_usize;
1110            let mut samples: Vec<f32> = (0..frames)
1111                .flat_map(|i| {
1112                    let value = (i as f64 / frames as f64 * TAU * 40.0).sin() as f32 * 0.6;
1113                    [value, value]
1114                })
1115                .collect();
1116            let dry = samples.clone();
1117            let mut context = context(0.0, frames);
1118            // Park the needle at one radius for the whole block.
1119            context.start_position = radius_progress * frames as f64 * 4.0;
1120            context.end_position = context.start_position;
1121            processor.process_interleaved(&mut samples, 2, context);
1122            samples
1123                .iter()
1124                .zip(dry.iter())
1125                .fold(0.0_f32, |peak, (wet, dry)| peak.max((wet - dry).abs()))
1126        }
1127        // Tangent at the nulls, and worst out at the edge.
1128        assert!(grit(NULL_POINT_OUTER) < 0.001);
1129        assert!(grit(NULL_POINT_INNER) < 0.001);
1130        assert!(grit(0.0) > grit(0.6));
1131        assert!(grit(0.6) > grit(NULL_POINT_OUTER));
1132    }
1133
1134    #[test]
1135    fn overcut_cuts_its_own_pass_back_into_the_groove() {
1136        let mut processor = VinylVfxProcessor::new();
1137        processor.set_scene(VINYL_VFX_OVERCUT, 1.0);
1138        let frames = 4_800_usize;
1139        let loud = vec![0.4_f32; frames * 2];
1140        // One turn of programme, then silence over the same groove twice.
1141        let mut first = loud.clone();
1142        processor.process_interleaved(&mut first, 2, context(0.0, frames));
1143        let mut second = vec![0.0_f32; frames * 2];
1144        processor.process_interleaved(&mut second, 2, context(1.0, frames));
1145        let mut third = vec![0.0_f32; frames * 2];
1146        processor.process_interleaved(&mut third, 2, context(2.0, frames));
1147        let second_peak = second.iter().fold(0.0_f32, |a, s| a.max(s.abs()));
1148        let third_peak = third.iter().fold(0.0_f32, |a, s| a.max(s.abs()));
1149        // The second turn hears the first, and the third hears the second —
1150        // which it could only do if the pass was written back.
1151        assert!(second_peak > 0.05);
1152        assert!(third_peak > 0.05);
1153        assert!(third.iter().all(|sample| sample.abs() <= 1.0));
1154    }
1155
1156    #[test]
1157    fn split_walls_keeps_stereo_channels_distinct() {
1158        let mut processor = VinylVfxProcessor::new();
1159        processor.set_scene(VINYL_VFX_SPLIT_WALLS, 1.0);
1160        let mut warmup = Vec::with_capacity(9_600);
1161        for _ in 0..4_800 {
1162            warmup.extend_from_slice(&[0.7, -0.2]);
1163        }
1164        processor.process_interleaved(&mut warmup, 2, context(0.0, 4_800));
1165        let mut output = warmup.clone();
1166        processor.process_interleaved(&mut output, 2, context(1.0, 4_800));
1167        assert!(output.chunks_exact(2).any(|frame| frame[0] != frame[1]));
1168    }
1169
1170    #[test]
1171    fn inner_fire_is_audible_but_not_blown_out() {
1172        let mut processor = VinylVfxProcessor::new();
1173        processor.set_scene(VINYL_VFX_INNER_FIRE, 1.0);
1174        let frames = 4_800_usize;
1175        let mut samples: Vec<f32> = (0..frames)
1176            .flat_map(|i| {
1177                let value = (i as f64 / frames as f64 * TAU * 48.0).sin() as f32 * 0.5;
1178                [value, value]
1179            })
1180            .collect();
1181        let dry = samples.clone();
1182        processor.process_interleaved(&mut samples, 2, context(0.0, frames));
1183        let wet_peak = samples.iter().fold(0.0_f32, |a, s| a.max(s.abs()));
1184        let dry_peak = dry.iter().fold(0.0_f32, |a, s| a.max(s.abs()));
1185        assert!(
1186            samples
1187                .iter()
1188                .zip(dry.iter())
1189                .any(|(a, b)| (a - b).abs() > 0.02),
1190            "fire did nothing"
1191        );
1192        assert!(
1193            wet_peak < dry_peak * 1.6,
1194            "fire blew out: {wet_peak} against dry {dry_peak}"
1195        );
1196    }
1197
1198    /// One revolution at 33 1/3 rpm and 48 kHz, and a programme whose every
1199    /// partial is a whole number of cycles in it. The loop joins seamlessly,
1200    /// so anything sub-audio in the output was made by the scene.
1201    const REVOLUTION: usize = 86_400;
1202
1203    fn revolution_programme() -> Vec<f32> {
1204        let bin = |k: f64| k / 1.8;
1205        (0..REVOLUTION)
1206            .flat_map(|i| {
1207                let t = i as f64 / 48_000.0;
1208                let mono = (t * TAU * bin(99.0)).sin() * 0.45
1209                    + (t * TAU * bin(148.0)).sin() * 0.20
1210                    + (t * TAU * bin(396.0)).sin() * 0.16
1211                    + (t * TAU * bin(594.0)).sin() * 0.12;
1212                let side = (t * TAU * bin(5_580.0)).sin() * 0.05;
1213                [(mono + side) as f32, (mono - side) as f32]
1214            })
1215            .collect()
1216    }
1217
1218    fn locked_groove(turn: usize) -> VinylVfxContext {
1219        VinylVfxContext {
1220            start_turns: turn as f64,
1221            end_turns: turn as f64 + 1.0,
1222            start_position: 0.55 * REVOLUTION as f64 * 40.0,
1223            end_position: 0.55 * REVOLUTION as f64 * 40.0,
1224            total_frames: REVOLUTION * 40,
1225            ..VinylVfxContext::default()
1226        }
1227    }
1228
1229    /// What is left of a track under 20 Hz. Six poles, so a 55 Hz bass does
1230    /// not leak into the reading.
1231    fn sub_audio(track: &[f64]) -> f64 {
1232        let alpha = 1.0 - (-TAU * 20.0 / 48_000.0f64).exp();
1233        let mut state = [0.0f64; 6];
1234        let mut sum = 0.0;
1235        for sample in track {
1236            state[0] += (sample - state[0]) * alpha;
1237            for stage in 1..6 {
1238                state[stage] += (state[stage - 1] - state[stage]) * alpha;
1239            }
1240            sum += state[5] * state[5];
1241        }
1242        (sum / track.len() as f64).sqrt()
1243    }
1244
1245    fn mid_of(frames: &[f32]) -> Vec<f64> {
1246        frames
1247            .chunks_exact(2)
1248            .map(|f| (f64::from(f[0]) + f64::from(f[1])) * 0.5)
1249            .collect()
1250    }
1251
1252    fn side_of(frames: &[f32]) -> Vec<f64> {
1253        frames
1254            .chunks_exact(2)
1255            .map(|f| (f64::from(f[0]) - f64::from(f[1])) * 0.5)
1256            .collect()
1257    }
1258
1259    fn last_turn_of(scene: u32, turns: usize) -> Vec<f32> {
1260        let mut processor = VinylVfxProcessor::new();
1261        processor.set_scene(scene, 1.0);
1262        let mut last = Vec::new();
1263        for turn in 0..turns {
1264            let mut wet = revolution_programme();
1265            processor.process_interleaved(&mut wet, 2, locked_groove(turn));
1266            last = wet;
1267        }
1268        last
1269    }
1270
1271    #[test]
1272    fn pinch_keeps_the_sub_audio_out_of_the_sides() {
1273        // Squaring a band returns every difference as well as every sum, and
1274        // a dense band puts most of its pairs close together. The ride goes
1275        // in antiphase, so without a floor under it that pile arrives as
1276        // sub-audio in the side channel — which is the one place a record
1277        // may never carry bass, and the one a headphone reproduces.
1278        let wet = last_turn_of(VINYL_VFX_PINCH, 6);
1279        let side = sub_audio(&side_of(&wet));
1280        let mid = sub_audio(&mid_of(&wet));
1281        assert!(
1282            side < 0.005,
1283            "pinch put sub-audio in the sides: {side} against {mid} in the middle"
1284        );
1285        // And it is still the scene. Width is how much side the cut carries
1286        // against its own middle, not against the level it came in at: the
1287        // scene steps the cut back by `PINCH_HEADROOM` to make room for the
1288        // ride, so a reading taken against the input measures that trim
1289        // rather than the widening.
1290        let spread = |frames: &[f32]| -> f64 {
1291            let sum = |v: Vec<f64>| v.iter().map(|s| s.abs()).sum::<f64>();
1292            sum(side_of(frames)) / sum(mid_of(frames)).max(1.0e-12)
1293        };
1294        let width = spread(&wet) / spread(&revolution_programme());
1295        assert!(width > 1.2, "pinch stopped making width: {width}");
1296    }
1297
1298    #[test]
1299    fn overcut_does_not_sum_its_own_offset() {
1300        // The loop's lowpass passes DC at unity. With no floor the pass adds
1301        // its own offset and everything under the turn rate to itself, at
1302        // `layer` a revolution, and twelve turns of that is a rumble.
1303        let wet = last_turn_of(VINYL_VFX_OVERCUT, 12);
1304        let dry = revolution_programme();
1305        let mid = sub_audio(&mid_of(&wet));
1306        let dry_mid = sub_audio(&mid_of(&dry));
1307        assert!(
1308            mid < dry_mid * 3.0,
1309            "overcut piled up under the music: {mid} against {dry_mid} dry"
1310        );
1311        // The layers are still there: the scene departs from what it was
1312        // handed, and stays bounded while it does.
1313        let departure = wet
1314            .iter()
1315            .zip(dry.iter())
1316            .fold(0.0_f32, |peak, (w, d)| peak.max((w - d).abs()));
1317        assert!(departure > 0.1, "overcut stopped layering: {departure}");
1318        assert!(wet.iter().all(|sample| sample.abs() <= 1.0));
1319    }
1320
1321    #[test]
1322    fn worn_halo_wears_in_seconds_rather_than_hours() {
1323        // A wear bin is one angle of the revolution, so the stylus is over
1324        // it for a fraction of each pass. The rate has to carry that factor:
1325        // without it the scene needs eleven hours on one locked groove to
1326        // reach the wear its constant asks for in twenty seconds.
1327        let mut processor = VinylVfxProcessor::new();
1328        processor.set_scene(VINYL_VFX_WORN_HALO, 1.0);
1329        let dry = revolution_programme();
1330        let rms = |v: &[f32]| -> f64 {
1331            (v.iter().map(|s| f64::from(*s) * f64::from(*s)).sum::<f64>() / v.len() as f64).sqrt()
1332        };
1333        let departure = |wet: &[f32]| -> f64 {
1334            let delta: Vec<f32> = wet.iter().zip(dry.iter()).map(|(w, d)| w - d).collect();
1335            rms(&delta) / rms(&dry)
1336        };
1337        // A revolution is 1.8 s, so twelve of them is a little over twenty.
1338        let mut early = 0.0;
1339        let mut late = 0.0;
1340        for turn in 0..12 {
1341            let mut wet = revolution_programme();
1342            processor.process_interleaved(&mut wet, 2, locked_groove(turn));
1343            if turn == 0 {
1344                early = departure(&wet);
1345            }
1346            late = departure(&wet);
1347        }
1348        assert!(early > 0.004, "the first pass left no mark at all: {early}");
1349        assert!(
1350            late > early * 2.5,
1351            "the halo stopped building: {early} then {late}"
1352        );
1353    }
1354
1355    #[test]
1356    fn overcut_converges_instead_of_running_away() {
1357        // The scene is meant to build: LAYERS asks the groove to carry more
1358        // of its own history, and on a locked groove these sines repeat
1359        // exactly, so this is the most the loop can ever be handed.
1360        //
1361        // What must hold is that it arrives somewhere. A loop whose return
1362        // reaches one never decays, and the old arithmetic was still
1363        // climbing at the twelfth revolution with the clamp doing the rest.
1364        let dry: Vec<f32> = revolution_programme().iter().map(|s| s * 0.55).collect();
1365        let rms = |v: &[f32]| -> f64 {
1366            (v.iter().map(|s| f64::from(*s) * f64::from(*s)).sum::<f64>() / v.len() as f64).sqrt()
1367        };
1368        let peak = |v: &[f32]| v.iter().fold(0.0_f32, |a, s| a.max(s.abs()));
1369        let mut processor = VinylVfxProcessor::new();
1370        processor.set_scene(VINYL_VFX_OVERCUT, 1.0);
1371        let mut first = 0.0;
1372        let mut sixth = 0.0;
1373        let mut twelfth = 0.0;
1374        let mut top = 0.0_f32;
1375        for turn in 0..12 {
1376            let mut wet = dry.clone();
1377            processor.process_interleaved(&mut wet, 2, locked_groove(turn));
1378            top = top.max(peak(&wet));
1379            match turn {
1380                0 => first = rms(&wet) / rms(&dry),
1381                5 => sixth = rms(&wet) / rms(&dry),
1382                11 => twelfth = rms(&wet) / rms(&dry),
1383                _ => {}
1384            }
1385        }
1386        // Settled: the last half of the run is going nowhere.
1387        let drift = (twelfth - sixth).abs() / sixth;
1388        assert!(
1389            drift < 0.06,
1390            "overcut had not settled by the twelfth turn: {sixth} then {twelfth}"
1391        );
1392        // It builds, and it arrives.
1393        assert!(twelfth > first * 1.2, "overcut stopped layering: {first} then {twelfth}");
1394        // The knee holds it clear of the ceiling without clamping.
1395        assert!(top < 0.99, "overcut reached the ceiling: {top}");
1396    }
1397    #[test]
1398    fn the_elliptical_circuit_takes_wide_bass_to_the_middle() {
1399        // A cutter head cannot move the stylus far enough vertically to hold
1400        // bass that exists only in the sides. This is a record that asks it
1401        // to: a 55 Hz tone in antiphase, with the rest of the programme down
1402        // the middle where it belongs.
1403        const TURN: usize = 86_400;
1404        let wide: Vec<f32> = (0..TURN)
1405            .flat_map(|i| {
1406                let t = i as f64 / 48_000.0;
1407                let middle = (t * TAU * 220.0).sin() * 0.30 + (t * TAU * 660.0).sin() * 0.12;
1408                let bass = (t * TAU * 55.0).sin() * 0.40;
1409                [(middle + bass) as f32, (middle - bass) as f32]
1410            })
1411            .collect();
1412
1413        // Everything under 200 Hz that the sides are carrying.
1414        let low_side = |frames: &[f32]| -> f64 {
1415            let alpha = 1.0 - (-TAU * 200.0 / 48_000.0f64).exp();
1416            let mut state = [0.0f64; 4];
1417            let mut sum = 0.0;
1418            for pair in frames.chunks_exact(2) {
1419                let side = (f64::from(pair[0]) - f64::from(pair[1])) * 0.5;
1420                state[0] += (side - state[0]) * alpha;
1421                for stage in 1..4 {
1422                    state[stage] += (state[stage - 1] - state[stage]) * alpha;
1423                }
1424                sum += state[3] * state[3];
1425            }
1426            (sum / (frames.len() / 2) as f64).sqrt()
1427        };
1428
1429        let before = low_side(&wide);
1430        let mut processor = VinylVfxProcessor::new();
1431        processor.set_scene(VINYL_VFX_OVERCUT, 1.0);
1432        let mut wet = wide.clone();
1433        processor.process_interleaved(&mut wet, 2, locked_groove(0));
1434        let after = low_side(&wet);
1435
1436        // The loop would otherwise add to this, not take from it.
1437        assert!(
1438            after < before * 0.5,
1439            "the sides kept their bass: {before} then {after}"
1440        );
1441        // And the middle is left alone: this is a circuit for the sides.
1442        let middle = |frames: &[f32]| -> f64 {
1443            let m: Vec<f64> = frames
1444                .chunks_exact(2)
1445                .map(|p| (f64::from(p[0]) + f64::from(p[1])) * 0.5)
1446                .collect();
1447            (m.iter().map(|s| s * s).sum::<f64>() / m.len() as f64).sqrt()
1448        };
1449        let kept = middle(&wet) / middle(&wide);
1450        assert!(kept > 0.8, "the circuit took the middle with it: {kept}");
1451    }
1452
1453    /// The house scenes add to the groove, they do not overload it. GHOST
1454    /// sums a correlated previous turn with the dry and OVERCUT recirculates;
1455    /// at the old gains the sum reached deep into `soft_limit` and read as
1456    /// crackle. Both must keep clear of the knee on a hot, wide bass.
1457    #[test]
1458    fn ghost_and_overcut_do_not_overdrive_the_limiter() {
1459        let bass: Vec<f32> = (0..REVOLUTION)
1460            .flat_map(|i| {
1461                let t = i as f64 / 48_000.0;
1462                let mid = (t * TAU * 82.0).sin() * 0.55;
1463                let side = (t * TAU * 55.0).sin() * 0.42;
1464                [(mid + side) as f32, (mid - side) as f32]
1465            })
1466            .collect();
1467        let hot = |frames: &[f32]| frames.iter().filter(|s| s.abs() > 0.95).count();
1468        for (name, scene, turns) in [
1469            ("adjacent ghost", VINYL_VFX_ADJACENT_GHOST, 3),
1470            ("overcut", VINYL_VFX_OVERCUT, 12),
1471        ] {
1472            let mut processor = VinylVfxProcessor::new();
1473            processor.set_scene(scene, 1.0);
1474            let mut worst = 0;
1475            for turn in 0..turns {
1476                let mut wet = bass.clone();
1477                processor.process_interleaved(&mut wet, 2, locked_groove(turn));
1478                worst = worst.max(hot(&wet));
1479            }
1480            assert_eq!(worst, 0, "{name} drove the limiter into saturation");
1481        }
1482    }
1483}