Skip to main content

phosphor_core/
mixer.rs

1//! Per-track audio mixer with MIDI recording and clip playback.
2//!
3//! The mixer owns all audio tracks and processes the track graph:
4//! routing MIDI to the active track, recording armed tracks,
5//! playing back clips, applying mute/solo/volume, and mixing to master.
6
7use std::sync::Arc;
8
9use crossbeam_channel::{Receiver, Sender};
10use phosphor_midi::message::MidiMessage;
11use phosphor_plugin::{MidiEvent, Plugin};
12
13use crate::clip::{ClipEvent, ClipSnapshot, MidiClip, RecordBuffer};
14use crate::engine::VuLevels;
15use crate::metronome::Metronome;
16use crate::pattern::{EventSink, PatternBlock, PatternEvent, PatternPlayer, PlaybackWindow};
17use crate::project::{TrackHandle, TrackKind};
18use crate::transport::Transport;
19
20// ── Commands ──
21
22// Clippy would have `SetPattern` box its block, and boxing it is exactly the
23// thing this design exists to avoid: a `Box` arriving on the audio thread is a
24// `free` on the audio thread when the command is dropped. The command queue is
25// short and the memory is nothing; the deadline is not.
26#[allow(clippy::large_enum_variant)]
27pub enum MixerCommand {
28    AddTrack {
29        kind: TrackKind,
30        handle: Arc<TrackHandle>,
31    },
32    SetInstrument {
33        track_id: usize,
34        instrument: Box<dyn Plugin + Send>,
35    },
36    RemoveTrack {
37        track_id: usize,
38    },
39    SetParameter {
40        track_id: usize,
41        param_index: usize,
42        value: f32,
43    },
44    /// Create a new empty clip on a track.
45    CreateClip {
46        track_id: usize,
47        start_tick: i64,
48        length_ticks: i64,
49    },
50    /// Replace a clip's events with edited data from the UI.
51    UpdateClip {
52        track_id: usize,
53        clip_index: usize,
54        events: Vec<ClipEvent>,
55    },
56    /// Update a clip's timeline position and length on the audio thread.
57    UpdateClipPosition {
58        track_id: usize,
59        clip_index: usize,
60        start_tick: i64,
61        length_ticks: i64,
62    },
63    /// Remove a clip from a track on the audio thread.
64    RemoveClip {
65        track_id: usize,
66        clip_index: usize,
67    },
68    /// Give one of a sequencer track's eight pattern slots new contents, and
69    /// with it the UI's current word on the track-level settings that ride on
70    /// a block — see [`PatternBlock`].
71    ///
72    /// The block travels by value. It is [`Copy`] and about two and a half
73    /// kilobytes, so receiving one is a memcpy into memory that already
74    /// exists: no `Vec` to free, no `Box` to drop, nothing for the audio
75    /// thread to hand back to the allocator. The first pattern a track is
76    /// given allocates its player, exactly as `SetInstrument` allocates a
77    /// voice array; every one after it does not.
78    SetPattern {
79        track_id: usize,
80        slot: u8,
81        block: PatternBlock,
82    },
83}
84
85// ── Command budget ──
86//
87// The audio callback has a hard deadline — 1.45 ms at the default 64 frames,
88// 0.73 ms if the device asks for 32 — and applying commands is the one thing
89// in it whose size the audio thread does not control. Loading a preset queues
90// one command per control, 59 of them on the Odyssey; opening a session
91// queues an AddTrack, a SetInstrument and a full parameter block per track,
92// plus two commands per clip. Draining all of that in one callback is an
93// unbounded amount of work behind a fixed deadline, which is a dropout.
94//
95// So each callback spends a fixed budget and stops. Nothing is dropped and
96// nothing is reordered: what is left stays queued, in order, and the next
97// callback continues from there. A burst that does not fit is spread over
98// consecutive callbacks — for a session load that is a few milliseconds with
99// the transport stopped, and for a preset it is at worst one buffer rendered
100// with part of the old panel, which is 1.45 ms.
101
102/// The cost of a command that goes to the allocator. See [`command_cost`].
103const HEAVY_COMMAND: u32 = 16;
104
105/// What one command costs, in the units [`COMMAND_BUDGET`] is denominated in.
106///
107/// Two tiers, and the line between them is the allocator:
108///
109/// * **1** — writes into memory that already exists. Setting a parameter is a
110///   clamp and a store; moving a clip writes two integers.
111/// * **[`HEAVY_COMMAND`]** — allocates, frees, or both. `SetInstrument` calls
112///   `Plugin::init`, which builds a voice array and, on the Juno, a chorus
113///   delay line; `AddTrack` allocates two audio buffers; `RemoveTrack` and
114///   `UpdateClip` free what they replace.
115///
116/// Measured in release on a 64-frame callback: four instrument loads take
117/// 30 µs against 1.4 µs for four `AddTrack` and 6.8 µs for sixty-four
118/// parameter changes, and the callback's own rendering with one instrument on
119/// it is 15 µs. So a flat count would be wrong in both directions: sixty-four
120/// parameter changes belong in one callback, and sixty-four instrument loads
121/// would be half a millisecond of it.
122fn command_cost(cmd: &MixerCommand) -> u32 {
123    match cmd {
124        MixerCommand::SetParameter { .. } | MixerCommand::UpdateClipPosition { .. } => 1,
125        MixerCommand::AddTrack { .. }
126        | MixerCommand::SetInstrument { .. }
127        | MixerCommand::RemoveTrack { .. }
128        | MixerCommand::CreateClip { .. }
129        | MixerCommand::UpdateClip { .. }
130        | MixerCommand::RemoveClip { .. }
131        // Only the first pattern a track receives allocates — it builds the
132        // player — and the cost is charged before the command is opened, so
133        // it cannot be told apart from the ones that only copy. Charging all
134        // of them the allocating rate makes the bound hold for the one that
135        // does; the copy itself is 2.4 kB, which is nothing next to a
136        // `Plugin::init`.
137        | MixerCommand::SetPattern { .. } => HEAVY_COMMAND,
138    }
139}
140
141/// How much command work one callback will do.
142///
143/// 64 units: a whole parameter block in one callback — the widest panel in the
144/// project is the Odyssey's 59 controls — or four allocating commands.
145///
146/// A panel wider than this is not a fault, only a preset load spread over two
147/// callbacks, which shows up as one buffer rendered with part of the old panel
148/// and is 1.45 ms long.
149///
150/// Sized against the shortest callback the application can be given, 32 frames
151/// at 44.1 kHz, which is 726 µs: a full budget of the expensive kind measures
152/// 30 µs, or four percent of that deadline, and the cheap kind 7 µs.
153///
154/// The bound this buys is `COMMAND_BUDGET - 1 + HEAVY_COMMAND` units of work
155/// per callback, not `COMMAND_BUDGET`: the budget is checked before a command
156/// is taken and its cost is known only after. Tightening that would need a
157/// `peek` the channel does not offer, and the overshoot is one command.
158const COMMAND_BUDGET: u32 = 64;
159
160/// How many tracks a mixer has room for before its track list has to grow.
161///
162/// Growing it is a reallocation on the audio thread, so the list is built with
163/// room for more tracks than a session is going to hold. It is not a limit:
164/// `AddTrack` past this still works, at the cost of one reallocation, and the
165/// next 64 are free again. 64 `AudioTrack` headers are a few kilobytes, which
166/// is nothing next to the two audio buffers each one already owns.
167const TRACK_CAPACITY: usize = 64;
168
169// ── Master limiter ──
170
171/// Peak ceiling the limiter holds the master bus to, −1 dBFS.
172///
173/// Not 1.0: the samples we write are points on a waveform the converter
174/// reconstructs between, and that reconstruction can overshoot the samples
175/// themselves. A dB of margin is the usual allowance for it.
176const LIMITER_CEILING: f32 = 0.891;
177
178/// Release time constant, 50 ms.
179///
180/// Long enough not to modulate the waveform of a low note — a 40 Hz cycle is
181/// 25 ms, and a release near that period distorts the fundamental instead of
182/// riding it. Short enough that a single loud transient does not duck the
183/// following bar. Attack is not a time constant at all: see [`MasterLimiter`].
184const LIMITER_RELEASE_SECONDS: f32 = 0.050;
185
186/// Stereo-linked peak limiter on the master bus.
187///
188/// The last stage before the audio device, and the only hard guarantee that
189/// nothing leaves at more than full scale. Gain staging in the instruments
190/// and the soft saturator on their outputs are what keep this idle; this is
191/// what catches everything they cannot — many loud tracks at once, a plugin
192/// with no output bound, a NaN out of a diverging filter.
193///
194/// Design notes:
195///
196/// * **Stereo-linked.** One gain, computed from `max(|L|, |R|)` and applied
197///   to both channels, so a peak in one channel does not pull the image
198///   across to the other.
199/// * **Instant attack.** The gain that a sample needs is applied to that
200///   same sample, not `n` samples later, so there is no overshoot to clean
201///   up afterwards and no lookahead buffer to pay for. The alternative — a
202///   millisecond attack — would let a millisecond of overshoot through, and
203///   the only thing left to catch it would be a hard clip.
204/// * **Smooth release.** One-pole, so the gain walks back to unity rather
205///   than stepping.
206///
207/// Real-time safe: three floats of state, no allocation, no locks, no
208/// branches that can panic.
209struct MasterLimiter {
210    /// Current gain, 0..=1. Never above unity: this only ever attenuates.
211    gain: f32,
212    /// One-pole coefficient for the release ramp.
213    release_coeff: f32,
214}
215
216impl MasterLimiter {
217    fn new(sample_rate: u32) -> Self {
218        let sr = (sample_rate as f32).max(1.0);
219        Self {
220            gain: 1.0,
221            release_coeff: 1.0 - (-1.0 / (LIMITER_RELEASE_SECONDS * sr)).exp(),
222        }
223    }
224
225    fn reset(&mut self) {
226        self.gain = 1.0;
227    }
228
229    /// Limit an interleaved stereo buffer in place.
230    ///
231    /// On return every sample is finite and within ±1.0. Any frame that was
232    /// not finite on the way in leaves as silence.
233    fn process(&mut self, output: &mut [f32]) {
234        let mut frames = output.chunks_exact_mut(2);
235        for frame in frames.by_ref() {
236            // A NaN or infinity reaching the device is a full-scale noise
237            // burst, so it is turned into silence here — and, just as
238            // important, before it can be fed into the detector below, where
239            // it would poison the gain state for every sample after it.
240            let l = if frame[0].is_finite() { frame[0] } else { 0.0 };
241            let r = if frame[1].is_finite() { frame[1] } else { 0.0 };
242
243            let peak = l.abs().max(r.abs());
244            // The backoff is not a fudge factor. `CEILING / peak` rounds to
245            // nearest, and so does the multiply that applies it, so the
246            // product can land up to three rounding steps above the ceiling.
247            // Two epsilons of headroom covers that with margin and makes "at
248            // or below the ceiling" exact rather than approximate.
249            let target = if peak > LIMITER_CEILING {
250                (LIMITER_CEILING / peak) * (1.0 - 2.0 * f32::EPSILON)
251            } else {
252                1.0
253            };
254
255            if target < self.gain {
256                self.gain = target;
257            } else {
258                self.gain += (target - self.gain) * self.release_coeff;
259            }
260
261            // Belt and braces. `gain <= CEILING / peak` holds by
262            // construction, so the product cannot exceed the ceiling and this
263            // clamp cannot fire — it is here because it is the last line
264            // before the audio device and the cost of being wrong is a
265            // speaker.
266            frame[0] = (l * self.gain).clamp(-1.0, 1.0);
267            frame[1] = (r * self.gain).clamp(-1.0, 1.0);
268        }
269
270        // An interleaved stereo buffer with an odd sample count is malformed
271        // and no device produces one, but the guarantee is unconditional: a
272        // trailing sample gets the same treatment rather than going out
273        // unchecked.
274        for tail in frames.into_remainder() {
275            let s = if tail.is_finite() { *tail } else { 0.0 };
276            *tail = (s * self.gain).clamp(-LIMITER_CEILING, LIMITER_CEILING);
277        }
278    }
279}
280
281// ── AudioTrack ──
282
283/// How many events one track's plugin queue holds before it would have to
284/// grow.
285///
286/// It never grows: the pattern player is handed the queue's remaining room as
287/// its budget and stops when it runs out, and clip playback has always fitted
288/// inside it. Sized for the densest thing the sequencer can ask for — eight
289/// lanes of five-note chords, each with the note-off of whatever it replaced,
290/// across the two or three steps a callback can span — plus room for live
291/// MIDI on top.
292const PLUGIN_EVENT_CAPACITY: usize = 512;
293
294pub struct AudioTrack {
295    pub id: usize,
296    pub kind: TrackKind,
297    pub handle: Arc<TrackHandle>,
298    pub instrument: Option<Box<dyn Plugin>>,
299    /// Recorded clips on this track's timeline.
300    pub clips: Vec<MidiClip>,
301    /// The step sequencer on this track, when it has one.
302    ///
303    /// Boxed because it carries all eight pattern slots — around 19 kB — and
304    /// a track without a sequencer should not pay for them, least of all
305    /// inside the `Vec<AudioTrack>` that is memcpy'd when a track is added.
306    pattern: Option<Box<PatternPlayer>>,
307    /// Active recording buffer (when armed + transport recording).
308    record_buf: RecordBuffer,
309    /// Whether we were recording last buffer (to detect stop).
310    was_recording: bool,
311    /// Last tick position seen during recording (to detect loop wraps).
312    last_record_tick: i64,
313    buf_l: Vec<f32>,
314    buf_r: Vec<f32>,
315    plugin_events: Vec<MidiEvent>,
316}
317
318impl AudioTrack {
319    pub fn new(handle: Arc<TrackHandle>, max_buffer_size: usize) -> Self {
320        Self {
321            id: handle.id,
322            kind: handle.kind,
323            handle,
324            instrument: None,
325            clips: Vec::new(),
326            pattern: None,
327            record_buf: RecordBuffer::new(),
328            was_recording: false,
329            last_record_tick: -1,
330            buf_l: vec![0.0; max_buffer_size],
331            buf_r: vec![0.0; max_buffer_size],
332            plugin_events: Vec::with_capacity(PLUGIN_EVENT_CAPACITY),
333        }
334    }
335}
336
337/// Writes pattern events straight into a track's plugin queue.
338///
339/// The conversion from song time to buffer position happens here, through
340/// [`PlaybackWindow::sample_offset`] — the same call clip playback makes a few
341/// lines further down, which is what "a pattern step and a clip note on the
342/// same beat land on the same sample" rests on.
343///
344/// The queue is never grown. When it is full the sink refuses, and the
345/// generator stops rather than dropping events out of the middle of a step.
346struct TrackEventSink<'a> {
347    events: &'a mut Vec<MidiEvent>,
348    window: &'a PlaybackWindow,
349}
350
351impl EventSink for TrackEventSink<'_> {
352    fn accept(&mut self, event: PatternEvent) -> bool {
353        if self.events.len() >= self.events.capacity() {
354            return false;
355        }
356        self.events.push(MidiEvent {
357            sample_offset: self.window.sample_offset(event.tick),
358            status: event.status,
359            data1: event.data1,
360            data2: event.data2,
361        });
362        true
363    }
364}
365
366/// Put a track's events in the order the instrument will read them.
367///
368/// A hand-written insertion sort, and not for speed: `slice::sort_by_key` is
369/// a merge sort that allocates a scratch buffer past twenty elements, which
370/// on the audio thread is exactly the thing this whole crate is arranged to
371/// avoid. These lists are short and arrive nearly sorted — clips are stored
372/// in tick order and a pattern generates step by step — so the insertion sort
373/// is linear in practice as well as allocation-free.
374///
375/// Stable, which is load-bearing: a note-off written before a note-on at the
376/// same offset has to stay before it, or a pattern switch kills the voice it
377/// just started.
378fn sort_events_by_offset(events: &mut [MidiEvent]) {
379    for i in 1..events.len() {
380        let mut j = i;
381        while j > 0 && events[j - 1].sample_offset > events[j].sample_offset {
382            events.swap(j - 1, j);
383            j -= 1;
384        }
385    }
386}
387
388// ── Mixer ──
389
390pub struct Mixer {
391    tracks: Vec<AudioTrack>,
392    master_vu: Arc<VuLevels>,
393    command_rx: Receiver<MixerCommand>,
394    clip_tx: Sender<ClipSnapshot>,
395    metronome: Metronome,
396    sample_rate: u32,
397    max_buffer_size: usize,
398    /// Pre-allocated scratch buffers for mix — avoids allocation in process().
399    scratch_l: Vec<f32>,
400    scratch_r: Vec<f32>,
401    /// Pre-allocated buffer for live MIDI conversion.
402    live_events: Vec<MidiEvent>,
403    /// The window the previous callback rendered, when playback was running.
404    ///
405    /// One per mixer rather than one per track: the window is a fact about
406    /// the transport and the block, so every track's is the same window, and
407    /// two tracks that computed it separately could disagree. `None` whenever
408    /// the transport is not rolling, which is what makes the first block
409    /// after a start discontinuous — see [`PlaybackWindow::is_continuous`].
410    last_window: Option<PlaybackWindow>,
411    /// Final stage before the audio device — see [`MasterLimiter`].
412    limiter: MasterLimiter,
413}
414
415impl Mixer {
416    pub fn new(
417        command_rx: Receiver<MixerCommand>,
418        master_vu: Arc<VuLevels>,
419        clip_tx: Sender<ClipSnapshot>,
420        sample_rate: u32,
421        max_buffer_size: usize,
422    ) -> Self {
423        Self {
424            tracks: Vec::with_capacity(TRACK_CAPACITY),
425            master_vu,
426            command_rx,
427            clip_tx,
428            metronome: Metronome::new(sample_rate as f64),
429            sample_rate,
430            max_buffer_size,
431            scratch_l: vec![0.0; max_buffer_size],
432            scratch_r: vec![0.0; max_buffer_size],
433            live_events: Vec::with_capacity(256),
434            last_window: None,
435            limiter: MasterLimiter::new(sample_rate),
436        }
437    }
438
439    /// Process one buffer cycle.
440    pub fn process(&mut self, output: &mut [f32], midi_messages: &[MidiMessage], transport: &Transport) {
441        // Bounded: whatever does not fit in this callback's budget is applied
442        // by the next one, in order. See `drain_commands`.
443        let _ = self.drain_commands();
444
445        let num_frames = output.len() / 2;
446        let playing = transport.is_playing();
447        let recording = transport.is_recording();
448        let looping = transport.is_looping();
449        let current_tick = transport.position_ticks();
450        let bpm = transport.tempo_bpm();
451        let ticks_per_sample = (bpm * Transport::PPQ as f64) / (60.0 * self.sample_rate as f64);
452        let loop_end = transport.loop_end();
453
454        // ── The window ──
455        //
456        // The span of song time this callback renders, computed once and read
457        // by everything that turns song time into notes. Clip playback and
458        // pattern playback both take their events from this one value, which
459        // is what makes them sample-identical on the same beat rather than
460        // two implementations that have to be kept in agreement.
461        let window = PlaybackWindow::for_block(
462            current_tick,
463            num_frames as u32,
464            ticks_per_sample,
465            looping.then(|| (transport.loop_start(), loop_end)),
466            self.last_window,
467        );
468        self.last_window = playing.then_some(window);
469
470        // Convert live MIDI to plugin events (reuse pre-allocated buffer)
471        self.live_events.clear();
472        for msg in midi_messages {
473            if let Some(ev) = midi_to_plugin_event(msg) {
474                self.live_events.push(ev);
475            }
476        }
477
478        let any_solo = self.tracks.iter().any(|t| t.handle.config.is_soloed());
479
480        // Reuse pre-allocated scratch buffers for master mix.
481        // Swap out of self to avoid borrow conflicts in the track loop.
482        let mut master_l = std::mem::take(&mut self.scratch_l);
483        let mut master_r = std::mem::take(&mut self.scratch_r);
484        let live_events = std::mem::take(&mut self.live_events);
485        // Dead code in practice, and deliberately kept. `max_buffer_size` is
486        // the largest block the device said it could deliver, so a block that
487        // does not fit means a driver exceeded its own stated maximum. One
488        // allocation is a glitch; the alternative here is wrong output or a
489        // panic on the audio thread.
490        if master_l.len() < num_frames {
491            master_l.resize(num_frames, 0.0);
492            master_r.resize(num_frames, 0.0);
493        }
494        master_l[..num_frames].fill(0.0);
495        master_r[..num_frames].fill(0.0);
496
497        let clip_tx = &self.clip_tx;
498
499        for track in &mut self.tracks {
500            if track.buf_l.len() < num_frames {
501                track.buf_l.resize(num_frames, 0.0);
502                track.buf_r.resize(num_frames, 0.0);
503            }
504            track.buf_l[..num_frames].fill(0.0);
505            track.buf_r[..num_frames].fill(0.0);
506            track.plugin_events.clear();
507
508            let is_midi_active = track.kind == TrackKind::Instrument
509                && track.handle.config.is_midi_active();
510            let is_armed = track.handle.config.is_armed();
511            let should_record = playing && recording && is_armed && is_midi_active;
512
513            // ── Recording ──
514            if should_record && !track.was_recording {
515                // Start recording at the loop start, not the current position,
516                // so the clip spans the full loop region
517                let rec_start = if looping { transport.loop_start() } else { current_tick };
518                track.record_buf.start(rec_start);
519                tracing::debug!("rec start track={} tick={}", track.id, current_tick);
520            }
521
522            // Detect loop wrap: current tick jumped backward means transport looped.
523            if should_record && track.was_recording && looping
524                && track.record_buf.is_active() && track.last_record_tick >= 0
525                && current_tick < track.last_record_tick
526            {
527                commit_recording(track, loop_end, clip_tx);
528                // Start new recording at loop start, not current_tick
529                // (current_tick may be a few ticks past 0 due to buffer boundaries)
530                track.record_buf.start(transport.loop_start());
531            }
532            if should_record {
533                track.last_record_tick = current_tick;
534            }
535
536            // Commit when recording stops (user pressed stop)
537            if !should_record && track.was_recording {
538                commit_recording(track, current_tick, clip_tx);
539            }
540            track.was_recording = should_record;
541
542            // Record live MIDI events (and pass through for monitoring)
543            if is_midi_active {
544                for ev in &live_events {
545                    track.plugin_events.push(*ev);
546                    if should_record {
547                        let event_tick = current_tick
548                            + (ev.sample_offset as f64 * ticks_per_sample) as i64;
549                        track.record_buf.record(event_tick, ev.status, ev.data1, ev.data2);
550                    }
551                }
552            }
553
554            // ── Pattern playback ──
555            //
556            // Before the clips, and unconditionally: a player that has just
557            // been stopped still has note-offs to write, and the transport
558            // being stopped is exactly when it has to write them.
559            if let Some(ref mut player) = track.pattern {
560                let mut sink = TrackEventSink { events: &mut track.plugin_events, window: &window };
561                player.render(&window, playing, &mut sink);
562                track.handle.pattern.publish(
563                    player.live_slot(),
564                    player.queued_slot(),
565                    player.current_step(),
566                    playing && player.is_playing(),
567                );
568            }
569
570            // ── Clip playback ──
571            //
572            // Same window, same `sample_offset`. The loop wrap needs no
573            // branch of its own any more: the window already starts at the
574            // loop point when the transport has just gone round.
575            if playing && !track.clips.is_empty() {
576                for clip in &track.clips {
577                    for (tick, event) in clip.events_between(window.from(), window.to()) {
578                        if track.plugin_events.len() >= track.plugin_events.capacity() {
579                            break;
580                        }
581                        track.plugin_events.push(MidiEvent {
582                            sample_offset: window.sample_offset(tick),
583                            status: event.status,
584                            data1: event.data1,
585                            data2: event.data2,
586                        });
587                    }
588                }
589            }
590
591            if !track.plugin_events.is_empty() {
592                sort_events_by_offset(&mut track.plugin_events);
593            }
594
595            // Track position for wrap detection (used by both recording and playback)
596            if playing {
597                track.last_record_tick = current_tick;
598            }
599
600            // ── Process instrument (allocation-free) ──
601            if let Some(ref mut instrument) = track.instrument {
602                let out_l = &mut track.buf_l[..num_frames];
603                let out_r = &mut track.buf_r[..num_frames];
604                let mut out_slices: [&mut [f32]; 2] = [out_l, out_r];
605                instrument.process(&[], &mut out_slices, &track.plugin_events);
606            }
607
608            // ── VU + Mix ──
609            let muted = track.handle.config.is_muted();
610            let soloed = track.handle.config.is_soloed();
611            let audible = !muted && (!any_solo || soloed);
612            let volume = track.handle.config.get_volume();
613
614            let mut peak_l = 0.0f32;
615            let mut peak_r = 0.0f32;
616            for i in 0..num_frames {
617                peak_l = peak_l.max(track.buf_l[i].abs());
618                peak_r = peak_r.max(track.buf_r[i].abs());
619            }
620
621            let (old_l, old_r) = track.handle.vu.get();
622            let decay = 0.85f32;
623            track.handle.vu.set(
624                if peak_l > old_l { peak_l } else { old_l * decay },
625                if peak_r > old_r { peak_r } else { old_r * decay },
626            );
627
628            if audible {
629                for i in 0..num_frames {
630                    master_l[i] += track.buf_l[i] * volume;
631                    master_r[i] += track.buf_r[i] * volume;
632                }
633            }
634        }
635
636        // Write tracks to interleaved output
637        for i in 0..num_frames {
638            output[i * 2] = master_l[i];
639            output[i * 2 + 1] = master_r[i];
640        }
641
642        // Return scratch buffers to self (no allocation, just moves)
643        self.scratch_l = master_l;
644        self.scratch_r = master_r;
645        self.live_events = live_events;
646
647        // Mix metronome click into output (after tracks, so it's always audible)
648        self.metronome.process(output, transport);
649
650        // ── Master limiter ──
651        // Everything that reaches the device passes through here, the
652        // metronome included: it is summed on top of the track mix, so
653        // limiting before it would leave a gap in the guarantee.
654        self.limiter.process(output);
655
656        // Master VU (includes metronome), read after limiting so the meter
657        // shows what actually left rather than what would have.
658        let mut mp_l = 0.0f32;
659        let mut mp_r = 0.0f32;
660        for i in 0..num_frames {
661            mp_l = mp_l.max(output[i * 2].abs());
662            mp_r = mp_r.max(output[i * 2 + 1].abs());
663        }
664
665        let (old_l, old_r) = self.master_vu.get();
666        let decay = 0.85f32;
667        self.master_vu.set(
668            if mp_l > old_l { mp_l } else { old_l * decay },
669            if mp_r > old_r { mp_r } else { old_r * decay },
670        );
671    }
672
673    pub fn reset_all(&mut self) {
674        let clip_tx = &self.clip_tx;
675        for track in &mut self.tracks {
676            if let Some(ref mut inst) = track.instrument {
677                inst.reset();
678            }
679            track.handle.vu.set(0.0, 0.0);
680            // Commit any active recording before resetting (don't lose overdubs)
681            if track.record_buf.is_active() && track.was_recording {
682                let end_tick = track.last_record_tick.max(0);
683                commit_recording(track, end_tick, clip_tx);
684            } else if track.record_buf.is_active() {
685                track.record_buf.discard();
686            }
687            track.was_recording = false;
688            // A panic resets the instruments underneath the sequencer, so the
689            // notes it is holding are already gone: the table is dropped
690            // rather than sounded, which would only send offs to voices that
691            // no longer exist.
692            if let Some(ref mut player) = track.pattern {
693                player.silence();
694            }
695        }
696        self.last_window = None;
697        self.metronome.reset();
698        self.limiter.reset();
699    }
700
701    /// Apply queued commands until the callback's budget is spent.
702    ///
703    /// Returns the units spent, which is what the tests assert the bound on.
704    ///
705    /// Anything left in the channel stays there, in the order it was sent, and
706    /// the next callback continues from it. That is the whole of the ordering
707    /// guarantee: commands are taken one at a time from a FIFO and applied
708    /// immediately, so `AddTrack` before `SetInstrument` for the same track
709    /// cannot be seen the other way round even when the two land in different
710    /// callbacks.
711    fn drain_commands(&mut self) -> u32 {
712        let mut spent = 0;
713        while spent < COMMAND_BUDGET {
714            let Ok(cmd) = self.command_rx.try_recv() else { break };
715            spent += command_cost(&cmd);
716            self.apply_command(cmd);
717        }
718        spent
719    }
720
721    fn apply_command(&mut self, cmd: MixerCommand) {
722        match cmd {
723            MixerCommand::AddTrack { kind: _, handle } => {
724                let track = AudioTrack::new(handle, self.max_buffer_size);
725                self.tracks.push(track);
726            }
727            MixerCommand::SetInstrument { track_id, mut instrument } => {
728                if let Some(track) = self.tracks.iter_mut().find(|t| t.id == track_id) {
729                    instrument.init(self.sample_rate as f64, self.max_buffer_size);
730                    track.instrument = Some(instrument);
731                }
732            }
733            MixerCommand::RemoveTrack { track_id } => {
734                self.tracks.retain(|t| t.id != track_id);
735            }
736            MixerCommand::SetParameter { track_id, param_index, value } => {
737                if let Some(track) = self.tracks.iter_mut().find(|t| t.id == track_id) {
738                    if let Some(ref mut inst) = track.instrument {
739                        inst.set_parameter(param_index, value);
740                    }
741                }
742            }
743            MixerCommand::CreateClip { track_id, start_tick, length_ticks } => {
744                if let Some(track) = self.tracks.iter_mut().find(|t| t.id == track_id) {
745                    track.clips.push(MidiClip::new(start_tick, length_ticks, Vec::new()));
746                }
747            }
748            MixerCommand::UpdateClip { track_id, clip_index, events } => {
749                if let Some(track) = self.tracks.iter_mut().find(|t| t.id == track_id) {
750                    if let Some(clip) = track.clips.get_mut(clip_index) {
751                        clip.events = events;
752                        clip.events.sort_by_key(|e| e.tick);
753                    }
754                }
755            }
756            MixerCommand::UpdateClipPosition { track_id, clip_index, start_tick, length_ticks } => {
757                if let Some(track) = self.tracks.iter_mut().find(|t| t.id == track_id) {
758                    if let Some(clip) = track.clips.get_mut(clip_index) {
759                        clip.start_tick = start_tick;
760                        clip.length_ticks = length_ticks;
761                    }
762                }
763            }
764            MixerCommand::RemoveClip { track_id, clip_index } => {
765                if let Some(track) = self.tracks.iter_mut().find(|t| t.id == track_id) {
766                    if clip_index < track.clips.len() {
767                        track.clips.remove(clip_index);
768                    }
769                }
770            }
771            MixerCommand::SetPattern { track_id, slot, block } => {
772                if let Some(track) = self.tracks.iter_mut().find(|t| t.id == track_id) {
773                    let player = track.pattern.get_or_insert_with(|| Box::new(PatternPlayer::new()));
774                    player.apply(slot, block);
775                }
776            }
777        }
778    }
779}
780
781/// Commit a recording buffer into a clip and send snapshot to UI.
782fn commit_recording(track: &mut AudioTrack, end_tick: i64, clip_tx: &Sender<ClipSnapshot>) {
783    if let Some(clip) = track.record_buf.commit(end_tick) {
784        let idx = track.clips.len();
785        tracing::debug!(
786            "rec commit track={}: {} events, ticks {}..{}",
787            track.id, clip.events.len(), clip.start_tick, clip.end_tick()
788        );
789        let snapshot = ClipSnapshot::from_clip(track.id, idx, &clip);
790        track.clips.push(clip);
791        let _ = clip_tx.send(snapshot);
792    }
793}
794
795/// Which live MIDI messages reach a plugin.
796///
797/// Channel pressure is here because instruments route it: the Prophet-6 has
798/// an aftertouch section with six destinations and an amount that reads as
799/// bipolar, and every one of its 500 factory programs stores a setting for
800/// it. It is a two-byte message, so `raw[2]` is whatever the parser left
801/// there and a plugin reads the pressure from `data1`, as the MIDI
802/// specification puts it.
803///
804/// Polyphonic key pressure is *not* here, and that is the instruments rather
805/// than an oversight — the Prophet-6 provides "monophonic (or 'channel')
806/// aftertouch" and nothing in the rack has a per-key pressure destination.
807/// `phosphor-midi` does not parse it into a variant of its own either.
808pub fn midi_to_plugin_event(msg: &MidiMessage) -> Option<MidiEvent> {
809    use phosphor_midi::message::MidiMessageType;
810    match msg.message_type {
811        MidiMessageType::NoteOn { .. }
812        | MidiMessageType::NoteOff { .. }
813        | MidiMessageType::ControlChange { .. }
814        | MidiMessageType::PitchBend { .. }
815        | MidiMessageType::ChannelPressure { .. } => Some(MidiEvent {
816            sample_offset: 0,
817            status: msg.raw[0],
818            data1: msg.raw[1],
819            data2: msg.raw[2],
820        }),
821        _ => None,
822    }
823}
824
825pub fn mixer_command_channel() -> (Sender<MixerCommand>, Receiver<MixerCommand>) {
826    crossbeam_channel::unbounded()
827}
828
829/// Create a channel for clip snapshots (audio → UI).
830pub fn clip_snapshot_channel() -> (Sender<ClipSnapshot>, Receiver<ClipSnapshot>) {
831    crossbeam_channel::unbounded()
832}
833
834#[cfg(test)]
835mod tests {
836    use super::*;
837    use crate::cpal_backend::{Requested, StreamFormat};
838    use crate::project::TrackConfig;
839    use phosphor_dsp::synth::PhosphorSynth;
840    use phosphor_midi::message::{MidiMessage, MidiMessageType};
841
842    fn make_note_on(note: u8, vel: u8) -> MidiMessage {
843        MidiMessage {
844            timestamp: Some(0),
845            message_type: MidiMessageType::NoteOn { channel: 0, note, velocity: vel },
846            raw: [0x90, note, vel],
847            len: 3,
848        }
849    }
850
851    /// Aftertouch has to reach a plugin, or an instrument with an aftertouch
852    /// section has one that never does anything.
853    #[test]
854    fn channel_pressure_reaches_the_plugin_and_key_pressure_does_not() {
855        let pressure = MidiMessage {
856            timestamp: Some(0),
857            message_type: MidiMessageType::ChannelPressure { channel: 0, pressure: 96 },
858            raw: [0xD0, 96, 0],
859            len: 2,
860        };
861        let event = midi_to_plugin_event(&pressure).expect("channel pressure is dropped");
862        assert_eq!(event.status, 0xD0);
863        assert_eq!(event.data1, 96);
864
865        // Polyphonic key pressure parses as `Other` and stays there: nothing
866        // in the rack has a per-key pressure destination.
867        let key = MidiMessage::from_bytes(&[0xA0, 60, 96], 0).expect("parsed");
868        assert!(
869            midi_to_plugin_event(&key).is_none(),
870            "polyphonic key pressure has no destination in the rack"
871        );
872    }
873
874    fn make_note_off(note: u8) -> MidiMessage {
875        MidiMessage {
876            timestamp: Some(0),
877            message_type: MidiMessageType::NoteOff { channel: 0, note, velocity: 0 },
878            raw: [0x80, note, 0],
879            len: 3,
880        }
881    }
882
883    fn setup_mixer() -> (Mixer, Sender<MixerCommand>, Receiver<ClipSnapshot>, Arc<Transport>) {
884        let (tx, rx) = mixer_command_channel();
885        let (clip_tx, clip_rx) = clip_snapshot_channel();
886        let master_vu = Arc::new(VuLevels::new());
887        let transport = Arc::new(Transport::new(120.0));
888        let mixer = Mixer::new(rx, master_vu, clip_tx, 44100, 256);
889        (mixer, tx, clip_rx, transport)
890    }
891
892    fn add_armed_synth(tx: &Sender<MixerCommand>, id: usize) -> Arc<TrackHandle> {
893        let handle = Arc::new(TrackHandle::new(id, TrackKind::Instrument));
894        handle.config.midi_active.store(true, std::sync::atomic::Ordering::Relaxed);
895        handle.config.armed.store(true, std::sync::atomic::Ordering::Relaxed);
896        tx.send(MixerCommand::AddTrack { kind: TrackKind::Instrument, handle: handle.clone() }).unwrap();
897        tx.send(MixerCommand::SetInstrument { track_id: id, instrument: Box::new(PhosphorSynth::new()) }).unwrap();
898        handle
899    }
900
901    #[test]
902    fn mixer_empty_output() {
903        let (mut mixer, _tx, _clip_rx, transport) = setup_mixer();
904        let mut output = vec![0.0f32; 128];
905        mixer.process(&mut output, &[], &transport);
906        assert!(output.iter().all(|&s| s == 0.0));
907    }
908
909    #[test]
910    fn mixer_live_midi_produces_sound() {
911        let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
912        let _handle = add_armed_synth(&tx, 0);
913        transport.play();
914
915        let midi = vec![make_note_on(60, 100)];
916        let mut output = vec![0.0f32; 512];
917        mixer.process(&mut output, &midi, &transport);
918
919        let peak = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
920        // Threshold is "not silence", not a level check — the instruments
921        // carry a deep headroom trim on their output.
922        assert!(peak > 0.001, "Should produce sound, peak={peak}");
923    }
924
925    #[test]
926    fn mixer_records_midi_clip() {
927        let (mut mixer, tx, clip_rx, transport) = setup_mixer();
928        let _handle = add_armed_synth(&tx, 0);
929        transport.play();
930        transport.toggle_record();
931
932        // Play a note while recording
933        let midi = vec![make_note_on(60, 100)];
934        let mut output = vec![0.0f32; 512];
935        mixer.process(&mut output, &midi, &transport);
936
937        // Note off
938        let midi = vec![make_note_off(60)];
939        mixer.process(&mut output, &midi, &transport);
940
941        // Stop recording
942        transport.toggle_record();
943        mixer.process(&mut output, &[], &transport);
944
945        // Should have received a clip snapshot
946        let snap = clip_rx.try_recv().expect("Should receive clip snapshot");
947        assert_eq!(snap.track_id, 0);
948        assert!(snap.event_count >= 2, "Should have note on + off, got {}", snap.event_count);
949        assert!(!snap.notes.is_empty(), "Should have parsed notes");
950    }
951
952    #[test]
953    fn mixer_plays_back_recorded_clip() {
954        let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
955        let _handle = add_armed_synth(&tx, 0);
956        transport.play();
957        transport.toggle_record();
958
959        // Record a note
960        let midi = vec![make_note_on(60, 100)];
961        let mut output = vec![0.0f32; 512];
962        mixer.process(&mut output, &midi, &transport);
963
964        let midi = vec![make_note_off(60)];
965        mixer.process(&mut output, &midi, &transport);
966
967        // Stop recording
968        transport.toggle_record();
969        mixer.process(&mut output, &[], &transport);
970
971        // Stop and rewind
972        transport.stop();
973
974        // Play back — should hear the recorded clip
975        transport.play();
976        output.fill(0.0);
977        mixer.process(&mut output, &[], &transport);
978
979        let peak = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
980        assert!(peak > 0.001, "Playback should produce sound, peak={peak}");
981    }
982
983    #[test]
984    fn mixer_mute_silences() {
985        let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
986        let handle = add_armed_synth(&tx, 0);
987        handle.config.muted.store(true, std::sync::atomic::Ordering::Relaxed);
988        transport.play();
989
990        let midi = vec![make_note_on(60, 100)];
991        let mut output = vec![0.0f32; 512];
992        mixer.process(&mut output, &midi, &transport);
993
994        let peak = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
995        assert!(peak == 0.0, "Muted track should be silent, peak={peak}");
996    }
997
998    #[test]
999    fn mixer_no_record_when_not_armed() {
1000        let (mut mixer, tx, clip_rx, transport) = setup_mixer();
1001        let handle = add_armed_synth(&tx, 0);
1002        handle.config.armed.store(false, std::sync::atomic::Ordering::Relaxed);
1003        transport.play();
1004        transport.toggle_record();
1005
1006        let midi = vec![make_note_on(60, 100)];
1007        let mut output = vec![0.0f32; 512];
1008        mixer.process(&mut output, &midi, &transport);
1009
1010        transport.toggle_record();
1011        mixer.process(&mut output, &[], &transport);
1012
1013        assert!(clip_rx.try_recv().is_err(), "Should not record when not armed");
1014    }
1015
1016    #[test]
1017    fn mixer_reset_commits_recording() {
1018        let (mut mixer, tx, clip_rx, transport) = setup_mixer();
1019        let _handle = add_armed_synth(&tx, 0);
1020        transport.play();
1021        transport.toggle_record();
1022
1023        let midi = vec![make_note_on(60, 100)];
1024        let mut output = vec![0.0f32; 512];
1025        mixer.process(&mut output, &midi, &transport);
1026
1027        mixer.reset_all();
1028
1029        // Reset should commit the active recording, not discard it
1030        assert!(clip_rx.try_recv().is_ok(), "Reset should commit active recording");
1031    }
1032
1033    #[test]
1034    fn end_to_end_record_and_playback() {
1035        // Simulates exact app flow: add track, arm, record, play notes,
1036        // stop, rewind, play back — with transport.advance() each buffer.
1037        let (mut mixer, tx, clip_rx, transport) = setup_mixer();
1038        let _handle = add_armed_synth(&tx, 0);
1039        let sr = 44100u32;
1040        let buf_frames = 256;
1041        let buf_samples = buf_frames * 2; // stereo
1042
1043        // 1. Enable recording, then play
1044        transport.toggle_record();
1045        transport.play();
1046
1047        // 2. Process a few empty buffers (advance transport)
1048        let mut output = vec![0.0f32; buf_samples];
1049        for _ in 0..4 {
1050            mixer.process(&mut output, &[], &transport);
1051            transport.advance(buf_frames as u32, sr);
1052        }
1053
1054        // 3. Play a note (should be recorded)
1055        let midi = vec![make_note_on(60, 100)];
1056        mixer.process(&mut output, &midi, &transport);
1057        let peak_during = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
1058        assert!(peak_during > 0.001, "Should hear note during recording (monitoring)");
1059        transport.advance(buf_frames as u32, sr);
1060
1061        // 4. A few more buffers of sustain
1062        for _ in 0..8 {
1063            output.fill(0.0);
1064            mixer.process(&mut output, &[], &transport);
1065            transport.advance(buf_frames as u32, sr);
1066        }
1067
1068        // 5. Note off
1069        let midi = vec![make_note_off(60)];
1070        mixer.process(&mut output, &midi, &transport);
1071        transport.advance(buf_frames as u32, sr);
1072
1073        // 6. A few more buffers
1074        for _ in 0..4 {
1075            output.fill(0.0);
1076            mixer.process(&mut output, &[], &transport);
1077            transport.advance(buf_frames as u32, sr);
1078        }
1079
1080        // 7. Stop recording (commit clip)
1081        transport.toggle_record();
1082        mixer.process(&mut output, &[], &transport);
1083        transport.advance(buf_frames as u32, sr);
1084
1085        // 8. Check we got a clip snapshot
1086        let snap = clip_rx.try_recv().expect("Should receive clip snapshot after stopping record");
1087        assert!(snap.event_count >= 2, "Clip should have note on + off");
1088        assert!(!snap.notes.is_empty(), "Clip should have parsed notes");
1089
1090        // 9. Stop transport and rewind to 0
1091        transport.stop();
1092
1093        // 10. Play back — the synth should be reset (no stuck notes from recording)
1094        transport.play();
1095
1096        // 11. Process enough buffers to reach the recorded note position
1097        // The note was recorded after 4 initial buffers, so roughly at that tick position
1098        for _ in 0..4 {
1099            output.fill(0.0);
1100            mixer.process(&mut output, &[], &transport);
1101            transport.advance(buf_frames as u32, sr);
1102        }
1103
1104        // 12. The next buffer should contain the played-back note
1105        output.fill(0.0);
1106        mixer.process(&mut output, &[], &transport);
1107        let peak_playback = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
1108        assert!(peak_playback > 0.001, "Playback should produce sound at the recorded position, peak={peak_playback}");
1109    }
1110
1111    #[test]
1112    fn loop_record_commits_on_wrap() {
1113        let (mut mixer, tx, clip_rx, transport) = setup_mixer();
1114        let _handle = add_armed_synth(&tx, 0);
1115        let sr = 44100u32;
1116        let buf_frames = 256u32;
1117
1118        // Set loop to 1 bar (3840 ticks at 120bpm ≈ 346 buffers of 256 samples)
1119        transport.set_loop_bars(1, 1);
1120        transport.start_loop_record();
1121
1122        let mut output = vec![0.0f32; buf_frames as usize * 2];
1123
1124        // Play a note early in the loop
1125        let midi = vec![make_note_on(60, 100)];
1126        mixer.process(&mut output, &midi, &transport);
1127        transport.advance(buf_frames, sr);
1128
1129        // Note off a few buffers later
1130        for _ in 0..5 {
1131            mixer.process(&mut output, &[], &transport);
1132            transport.advance(buf_frames, sr);
1133        }
1134        let midi = vec![make_note_off(60)];
1135        mixer.process(&mut output, &midi, &transport);
1136        transport.advance(buf_frames, sr);
1137
1138        // Continue until we cross the loop boundary
1139        // 1 bar at 120bpm, 256 frames, 44100Hz ≈ 346 buffers
1140        for _ in 0..400 {
1141            mixer.process(&mut output, &[], &transport);
1142            transport.advance(buf_frames, sr);
1143
1144            if let Ok(snap) = clip_rx.try_recv() {
1145                assert!(snap.event_count >= 2, "Clip should have events, got {}", snap.event_count);
1146                assert!(!snap.notes.is_empty(), "Clip should have notes");
1147                // Recording committed on loop wrap — success
1148                transport.stop_loop_record();
1149                return;
1150            }
1151        }
1152
1153        panic!("Recording should have committed when the loop wrapped");
1154    }
1155
1156    #[test]
1157    fn loop_playback_after_record() {
1158        let (mut mixer, tx, clip_rx, transport) = setup_mixer();
1159        let _handle = add_armed_synth(&tx, 0);
1160        let sr = 44100u32;
1161        let bf = 256u32;
1162
1163        // Set loop to 1 bar, start recording
1164        transport.set_loop_bars(1, 1);
1165        transport.start_loop_record();
1166
1167        let mut output = vec![0.0f32; bf as usize * 2];
1168
1169        // Record a note
1170        mixer.process(&mut output, &[make_note_on(60, 100)], &transport);
1171        transport.advance(bf, sr);
1172        for _ in 0..3 {
1173            mixer.process(&mut output, &[], &transport);
1174            transport.advance(bf, sr);
1175        }
1176        mixer.process(&mut output, &[make_note_off(60)], &transport);
1177        transport.advance(bf, sr);
1178
1179        // Run until loop wraps and clip commits
1180        for _ in 0..200 {
1181            mixer.process(&mut output, &[], &transport);
1182            transport.advance(bf, sr);
1183            if clip_rx.try_recv().is_ok() { break; }
1184        }
1185
1186        // Stop recording, rewind
1187        transport.stop_loop_record();
1188        transport.set_position(0);
1189
1190        // Play back with looping on
1191        transport.toggle_loop(); // enable looping
1192        transport.play();
1193
1194        output.fill(0.0);
1195        mixer.process(&mut output, &[], &transport);
1196        let peak = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
1197        assert!(peak > 0.001, "Should hear playback, peak={peak}");
1198    }
1199
1200    // ── Command budget ──
1201
1202    /// The most work one callback can do, in the units [`command_cost`]
1203    /// returns: the budget is tested before a command is taken and charged
1204    /// after, so the last one can overshoot by its own cost.
1205    const WORST_CALLBACK: u32 = COMMAND_BUDGET - 1 + HEAVY_COMMAND;
1206
1207    /// A plugin that remembers every parameter it was given, in order, so a
1208    /// test can see exactly what reached the audio thread and when.
1209    ///
1210    /// The lock is not something an instrument would do — nothing may block in
1211    /// `process` — but `set_parameter` is called from the command drain and
1212    /// this one never renders.
1213    #[derive(Clone)]
1214    struct ParamLog(Arc<std::sync::Mutex<Vec<(usize, f32)>>>);
1215
1216    impl ParamLog {
1217        fn new() -> Self {
1218            Self(Arc::new(std::sync::Mutex::new(Vec::new())))
1219        }
1220        fn seen(&self) -> Vec<(usize, f32)> {
1221            self.0.lock().unwrap().clone()
1222        }
1223    }
1224
1225    impl Plugin for ParamLog {
1226        fn info(&self) -> phosphor_plugin::PluginInfo {
1227            phosphor_plugin::PluginInfo {
1228                name: "ParamLog".into(),
1229                version: "0".into(),
1230                author: "test".into(),
1231                category: phosphor_plugin::PluginCategory::Instrument,
1232            }
1233        }
1234        fn init(&mut self, _sample_rate: f64, _max_buffer_size: usize) {}
1235        fn process(&mut self, _inputs: &[&[f32]], _outputs: &mut [&mut [f32]], _midi: &[MidiEvent]) {}
1236        fn parameter_count(&self) -> usize { 8 }
1237        fn parameter_info(&self, _index: usize) -> Option<phosphor_plugin::ParameterInfo> { None }
1238        fn get_parameter(&self, _index: usize) -> f32 { 0.0 }
1239        fn set_parameter(&mut self, index: usize, value: f32) {
1240            self.0.lock().unwrap().push((index, value));
1241        }
1242        fn reset(&mut self) {}
1243    }
1244
1245    /// Add a track carrying a [`ParamLog`], applying the commands immediately.
1246    fn add_logging_track(mixer: &mut Mixer, tx: &Sender<MixerCommand>, id: usize) -> ParamLog {
1247        let log = ParamLog::new();
1248        let handle = Arc::new(TrackHandle::new(id, TrackKind::Instrument));
1249        tx.send(MixerCommand::AddTrack { kind: TrackKind::Instrument, handle }).unwrap();
1250        tx.send(MixerCommand::SetInstrument {
1251            track_id: id,
1252            instrument: Box::new(log.clone()),
1253        }).unwrap();
1254        mixer.drain_commands();
1255        log
1256    }
1257
1258    /// The defect: the drain used to be `while let Ok(cmd) = try_recv()`, so
1259    /// the callback did as much work as the UI had queued. Opening a session
1260    /// queues hundreds of commands and the callback has a hard deadline.
1261    #[test]
1262    fn one_callback_applies_a_bounded_amount_of_work() {
1263        let (mut mixer, tx, _clip_rx, _transport) = setup_mixer();
1264        let log = add_logging_track(&mut mixer, &tx, 0);
1265
1266        for i in 0..500 {
1267            tx.send(MixerCommand::SetParameter {
1268                track_id: 0,
1269                param_index: i % 8,
1270                value: i as f32,
1271            }).unwrap();
1272        }
1273
1274        let spent = mixer.drain_commands();
1275        assert!(
1276            spent <= WORST_CALLBACK,
1277            "one callback spent {spent} units, over the {WORST_CALLBACK} bound"
1278        );
1279        assert_eq!(
1280            log.seen().len(),
1281            COMMAND_BUDGET as usize,
1282            "a parameter costs one unit, so a full budget is exactly that many"
1283        );
1284        assert!(!mixer.command_rx.is_empty(), "the rest has to still be queued");
1285    }
1286
1287    /// Bounded is only half of it: everything queued still has to arrive, once
1288    /// each, in the order it was sent.
1289    #[test]
1290    fn nothing_is_lost_or_reordered_across_callbacks() {
1291        let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
1292        let log = add_logging_track(&mut mixer, &tx, 0);
1293
1294        let sent: Vec<(usize, f32)> = (0..500).map(|i| (i % 8, i as f32)).collect();
1295        for &(param_index, value) in &sent {
1296            tx.send(MixerCommand::SetParameter { track_id: 0, param_index, value }).unwrap();
1297        }
1298
1299        // Run callbacks until the queue is empty, counting them: 500 commands
1300        // at one unit each cannot fit in fewer than eight budgets, which is
1301        // what makes this a test of the bound and not just of the FIFO.
1302        let mut output = vec![0.0f32; 128];
1303        let mut callbacks = 0;
1304        while !mixer.command_rx.is_empty() {
1305            mixer.process(&mut output, &[], &transport);
1306            callbacks += 1;
1307            assert!(callbacks < 100, "the drain is not making progress");
1308        }
1309        assert!(
1310            callbacks >= 500 / COMMAND_BUDGET as usize,
1311            "500 commands went through in {callbacks} callbacks, so the budget did not hold"
1312        );
1313        assert_eq!(log.seen(), sent, "the audio thread saw a different sequence");
1314    }
1315
1316    /// The ordering guarantee, at the one place it matters: a track has to
1317    /// exist before its instrument is attached. Splitting the queue between
1318    /// the two would drop the instrument on the floor — `SetInstrument` for a
1319    /// track that is not there yet is silently discarded — and the track would
1320    /// play nothing for the rest of the session.
1321    #[test]
1322    fn a_track_and_its_instrument_survive_a_budget_boundary() {
1323        let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
1324        let log = ParamLog::new();
1325
1326        // Fill this callback's budget with cheap commands first, so that the
1327        // pair below is guaranteed to land in a later one.
1328        for _ in 0..COMMAND_BUDGET {
1329            tx.send(MixerCommand::SetParameter { track_id: 99, param_index: 0, value: 0.0 })
1330                .unwrap();
1331        }
1332        let handle = Arc::new(TrackHandle::new(7, TrackKind::Instrument));
1333        tx.send(MixerCommand::AddTrack { kind: TrackKind::Instrument, handle }).unwrap();
1334        tx.send(MixerCommand::SetInstrument {
1335            track_id: 7,
1336            instrument: Box::new(log.clone()),
1337        }).unwrap();
1338        tx.send(MixerCommand::SetParameter { track_id: 7, param_index: 3, value: 0.5 }).unwrap();
1339
1340        let mut output = vec![0.0f32; 128];
1341        mixer.process(&mut output, &[], &transport);
1342        assert!(mixer.tracks.is_empty(), "the budget did not stop at the parameters");
1343
1344        while !mixer.command_rx.is_empty() {
1345            mixer.process(&mut output, &[], &transport);
1346        }
1347        assert_eq!(mixer.tracks.len(), 1);
1348        assert!(mixer.tracks[0].instrument.is_some(), "the instrument never arrived");
1349        assert_eq!(
1350            log.seen(),
1351            vec![(3, 0.5)],
1352            "the parameter that follows the instrument did not reach it"
1353        );
1354    }
1355
1356    /// An instrument load is not a parameter change: it calls `Plugin::init`,
1357    /// which allocates a voice array and, on some instruments, a delay line.
1358    /// A flat count of commands per callback would let sixteen of those
1359    /// through where it lets sixteen stores through.
1360    #[test]
1361    fn an_instrument_load_costs_more_than_a_parameter() {
1362        let param = MixerCommand::SetParameter { track_id: 0, param_index: 0, value: 0.0 };
1363        let load = MixerCommand::SetInstrument {
1364            track_id: 0,
1365            instrument: Box::new(FixedOutput(0.0)),
1366        };
1367        assert!(command_cost(&load) > command_cost(&param));
1368
1369        // Four loads per callback, not sixty-four.
1370        let (mut mixer, tx, _clip_rx, _transport) = setup_mixer();
1371        for id in 0..8 {
1372            let handle = Arc::new(TrackHandle::new(id, TrackKind::Instrument));
1373            tx.send(MixerCommand::AddTrack { kind: TrackKind::Instrument, handle }).unwrap();
1374        }
1375        while !mixer.command_rx.is_empty() {
1376            mixer.drain_commands();
1377        }
1378        for id in 0..8 {
1379            tx.send(MixerCommand::SetInstrument {
1380                track_id: id,
1381                instrument: Box::new(FixedOutput(0.25)),
1382            }).unwrap();
1383        }
1384        mixer.drain_commands();
1385        let loaded = mixer.tracks.iter().filter(|t| t.instrument.is_some()).count();
1386        assert_eq!(loaded, (COMMAND_BUDGET / HEAVY_COMMAND) as usize);
1387    }
1388
1389    /// `AddTrack` pushes onto the track list, and a push that grows the list
1390    /// reallocates — on the audio thread. The list is built with room for more
1391    /// tracks than a session will hold so that it does not.
1392    #[test]
1393    fn adding_tracks_does_not_grow_the_track_list() {
1394        let (mut mixer, tx, _clip_rx, _transport) = setup_mixer();
1395        let capacity = mixer.tracks.capacity();
1396        assert!(capacity >= TRACK_CAPACITY);
1397
1398        for id in 0..TRACK_CAPACITY {
1399            let handle = Arc::new(TrackHandle::new(id, TrackKind::Instrument));
1400            tx.send(MixerCommand::AddTrack { kind: TrackKind::Instrument, handle }).unwrap();
1401        }
1402        while !mixer.command_rx.is_empty() {
1403            mixer.drain_commands();
1404        }
1405        assert_eq!(mixer.tracks.len(), TRACK_CAPACITY);
1406        assert_eq!(
1407            mixer.tracks.capacity(), capacity,
1408            "the track list reallocated on the audio thread"
1409        );
1410    }
1411
1412    // ── Master limiter ──
1413
1414    /// A plugin that writes whatever it is told to, so the limiter can be
1415    /// driven with signals no real instrument would produce.
1416    struct FixedOutput(f32);
1417
1418    impl Plugin for FixedOutput {
1419        fn info(&self) -> phosphor_plugin::PluginInfo {
1420            phosphor_plugin::PluginInfo {
1421                name: "Fixed".into(),
1422                version: "0".into(),
1423                author: "test".into(),
1424                category: phosphor_plugin::PluginCategory::Instrument,
1425            }
1426        }
1427        fn init(&mut self, _sample_rate: f64, _max_buffer_size: usize) {}
1428        fn process(&mut self, _inputs: &[&[f32]], outputs: &mut [&mut [f32]], _midi: &[MidiEvent]) {
1429            for ch in outputs.iter_mut() {
1430                ch.fill(self.0);
1431            }
1432        }
1433        fn parameter_count(&self) -> usize { 0 }
1434        fn parameter_info(&self, _index: usize) -> Option<phosphor_plugin::ParameterInfo> { None }
1435        fn get_parameter(&self, _index: usize) -> f32 { 0.0 }
1436        fn set_parameter(&mut self, _index: usize, _value: f32) {}
1437        fn reset(&mut self) {}
1438    }
1439
1440    fn add_fixed_track(tx: &Sender<MixerCommand>, id: usize, value: f32) -> Arc<TrackHandle> {
1441        let handle = Arc::new(TrackHandle::new(id, TrackKind::Instrument));
1442        handle.config.set_volume(1.0);
1443        tx.send(MixerCommand::AddTrack { kind: TrackKind::Instrument, handle: handle.clone() }).unwrap();
1444        tx.send(MixerCommand::SetInstrument {
1445            track_id: id,
1446            instrument: Box::new(FixedOutput(value)),
1447        }).unwrap();
1448        handle
1449    }
1450
1451    /// The guarantee. Six tracks each running at three quarters of full scale
1452    /// sum to 4.5x — without the limiter that is what would reach the device.
1453    #[test]
1454    fn master_limiter_bounds_many_loud_tracks() {
1455        let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
1456        for id in 0..6 {
1457            add_fixed_track(&tx, id, 0.75);
1458        }
1459        transport.play();
1460
1461        let mut output = vec![0.0f32; 512];
1462        for _ in 0..8 {
1463            mixer.process(&mut output, &[], &transport);
1464            for (i, &s) in output.iter().enumerate() {
1465                assert!(s.is_finite(), "non-finite sample at {i}");
1466                assert!(s.abs() <= 1.0, "sample {i} left the mixer at {s}");
1467            }
1468        }
1469
1470        // And it is actually holding the ceiling, not silencing the mix.
1471        let peak = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
1472        assert!(peak > 0.8, "limiter over-attenuated, peak={peak}");
1473    }
1474
1475    /// A NaN out of a diverging filter must not reach the device: at full
1476    /// scale it is a noise burst, and it also poisons every sample after it
1477    /// if it is allowed into the limiter's gain state.
1478    #[test]
1479    fn non_finite_track_output_becomes_silence() {
1480        let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
1481        add_fixed_track(&tx, 0, f32::NAN);
1482        transport.play();
1483
1484        let mut output = vec![0.0f32; 512];
1485        mixer.process(&mut output, &[], &transport);
1486        assert!(output.iter().all(|s| *s == 0.0), "NaN track should render as silence");
1487
1488        // ...and the mixer still works afterwards: the gain state was not
1489        // left as NaN by the sample that was thrown away.
1490        tx.send(MixerCommand::RemoveTrack { track_id: 0 }).unwrap();
1491        add_fixed_track(&tx, 1, 0.5);
1492        mixer.process(&mut output, &[], &transport);
1493        let peak = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
1494        assert!((peak - 0.5).abs() < 1.0e-6, "mixer did not recover, peak={peak}");
1495    }
1496
1497    #[test]
1498    fn infinite_track_output_becomes_silence() {
1499        let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
1500        add_fixed_track(&tx, 0, f32::INFINITY);
1501        transport.play();
1502
1503        let mut output = vec![0.0f32; 512];
1504        mixer.process(&mut output, &[], &transport);
1505        assert!(output.iter().all(|s| *s == 0.0), "infinite track should render as silence");
1506    }
1507
1508    /// Below the ceiling the limiter is not a processor, it is a wire. Any
1509    /// deviation here would be gain riding on material that never asked for
1510    /// it — which is exactly what makes a limiter audible.
1511    #[test]
1512    fn limiter_is_bit_identical_below_the_ceiling() {
1513        let mut limiter = MasterLimiter::new(44_100);
1514
1515        // A sweep of levels up to the ceiling, plus signs and denormals.
1516        let mut input: Vec<f32> = Vec::new();
1517        for i in 0..20_000u32 {
1518            let phase = i as f32 * 0.01;
1519            let amp = LIMITER_CEILING * (i as f32 / 20_000.0);
1520            input.push(phase.sin() * amp);
1521            input.push(phase.cos() * amp);
1522        }
1523        input.push(LIMITER_CEILING);
1524        input.push(-LIMITER_CEILING);
1525        input.push(0.0);
1526        input.push(-0.0);
1527        input.push(f32::MIN_POSITIVE);
1528        input.push(-f32::MIN_POSITIVE);
1529
1530        let mut output = input.clone();
1531        limiter.process(&mut output);
1532
1533        for (i, (a, b)) in input.iter().zip(output.iter()).enumerate() {
1534            assert_eq!(a.to_bits(), b.to_bits(), "limiter altered sample {i}: {a} -> {b}");
1535        }
1536    }
1537
1538    /// The ceiling holds for anything, including levels no instrument in the
1539    /// project can produce.
1540    #[test]
1541    fn limiter_holds_the_ceiling_under_abuse() {
1542        let mut limiter = MasterLimiter::new(44_100);
1543        for amplitude in [1.0f32, 2.0, 10.0, 1.0e3, 1.0e6, 1.0e30] {
1544            let mut buf: Vec<f32> = (0..4_096)
1545                .map(|i| (i as f32 * 0.05).sin() * amplitude)
1546                .collect();
1547            limiter.process(&mut buf);
1548            for (i, &s) in buf.iter().enumerate() {
1549                assert!(s.is_finite(), "amplitude {amplitude}: sample {i} is {s}");
1550                assert!(
1551                    s.abs() <= LIMITER_CEILING,
1552                    "amplitude {amplitude}: sample {i} reached {s}, above the ceiling"
1553                );
1554            }
1555        }
1556    }
1557
1558    /// A step from silence to well over the ceiling: the very first sample of
1559    /// the step must already be limited. Anything else means overshoot, and
1560    /// the only thing left to catch overshoot is a hard clip.
1561    #[test]
1562    fn limiter_attack_has_no_overshoot() {
1563        let mut limiter = MasterLimiter::new(44_100);
1564        let mut buf = vec![0.0f32; 64];
1565        limiter.process(&mut buf);
1566        let mut step = vec![4.0f32; 64];
1567        limiter.process(&mut step);
1568        assert!(
1569            step[0].abs() <= LIMITER_CEILING,
1570            "first sample of the step overshot to {}",
1571            step[0]
1572        );
1573    }
1574
1575    /// Gain reduction must come back smoothly, not step. A step would be a
1576    /// click; a release faster than a low note's period would distort it.
1577    #[test]
1578    fn limiter_release_is_gradual() {
1579        let mut limiter = MasterLimiter::new(44_100);
1580        let mut loud = vec![4.0f32; 64];
1581        limiter.process(&mut loud);
1582        let reduced = limiter.gain;
1583        assert!(reduced < 0.5, "limiter did not engage, gain={reduced}");
1584
1585        // 10 ms of quiet material (441 stereo frames): partly recovered, not
1586        // all the way.
1587        let mut quiet = vec![0.1f32; 441 * 2];
1588        limiter.process(&mut quiet);
1589        assert!(limiter.gain > reduced, "gain did not recover at all");
1590        assert!(
1591            limiter.gain < 1.0,
1592            "gain snapped back to unity within 10 ms, which is a click"
1593        );
1594
1595        // 500 ms is ten time constants: fully recovered.
1596        let mut long = vec![0.1f32; 22_050 * 2];
1597        limiter.process(&mut long);
1598        assert!(
1599            (limiter.gain - 1.0).abs() < 1.0e-4,
1600            "gain never returned to unity: {}",
1601            limiter.gain
1602        );
1603    }
1604
1605    /// Stereo-linked: one gain from `max(|L|, |R|)`, so a peak on one side
1606    /// does not pull the image across to the other.
1607    #[test]
1608    fn limiter_does_not_shift_the_stereo_image() {
1609        let mut limiter = MasterLimiter::new(44_100);
1610        // Left twice the level of right, both well over the ceiling.
1611        let mut buf: Vec<f32> = Vec::new();
1612        for i in 0..1_024 {
1613            let phase = i as f32 * 0.05;
1614            buf.push(phase.sin() * 3.0);
1615            buf.push(phase.sin() * 1.5);
1616        }
1617        limiter.process(&mut buf);
1618        for frame in buf.chunks_exact(2) {
1619            if frame[1].abs() > 1.0e-4 {
1620                let ratio = frame[0] / frame[1];
1621                assert!(
1622                    (ratio - 2.0).abs() < 1.0e-3,
1623                    "channel balance moved: L/R = {ratio}"
1624                );
1625            }
1626        }
1627    }
1628
1629    /// The loudest single voice in the project: ROM3A's TIMPANI, voice 147 of
1630    /// the DX7's 256 factory voices, which is what `phosphor-dsp`'s headroom
1631    /// sweep measures as the hottest thing any instrument here can produce.
1632    ///
1633    /// The DX7 has two selectors — a cartridge and a voice — so picking one by
1634    /// number goes through `voice_knobs`.
1635    fn loudest_dx7_voice() -> phosphor_dsp::dx7::Dx7Synth {
1636        use phosphor_dsp::dx7;
1637        let mut synth = dx7::Dx7Synth::new();
1638        let (bank, patch) = dx7::voice_knobs(147);
1639        synth.set_parameter(dx7::P_BANK, bank);
1640        synth.set_parameter(dx7::P_PATCH, patch);
1641        debug_assert_eq!(dx7::voice_name(147), "TIMPANI");
1642        synth
1643    }
1644
1645    /// Four tracks of the loudest DX7 voice, each playing a two-handed
1646    /// eight-note chord at full velocity with the fader open — a heavier mix
1647    /// than anything the application can produce by accident.
1648    #[test]
1649    fn master_limiter_bounds_four_loud_instrument_tracks() {
1650        let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
1651        for id in 0..4 {
1652            let handle = Arc::new(TrackHandle::new(id, TrackKind::Instrument));
1653            handle.config.midi_active.store(true, std::sync::atomic::Ordering::Relaxed);
1654            handle.config.set_volume(1.0);
1655            let synth = loudest_dx7_voice();
1656            tx.send(MixerCommand::AddTrack { kind: TrackKind::Instrument, handle }).unwrap();
1657            tx.send(MixerCommand::SetInstrument {
1658                track_id: id,
1659                instrument: Box::new(synth),
1660            }).unwrap();
1661        }
1662        transport.play();
1663
1664        let chord: Vec<MidiMessage> = [36u8, 43, 48, 55, 60, 64, 67, 72]
1665            .iter()
1666            .map(|&note| make_note_on(note, 127))
1667            .collect();
1668
1669        let mut output = vec![0.0f32; 512];
1670        let mut peak = 0.0f32;
1671        for block in 0..200 {
1672            output.fill(0.0);
1673            if block == 0 {
1674                mixer.process(&mut output, &chord, &transport);
1675            } else {
1676                mixer.process(&mut output, &[], &transport);
1677            }
1678            for (i, &s) in output.iter().enumerate() {
1679                assert!(s.is_finite(), "block {block} sample {i} is {s}");
1680                assert!(s.abs() <= 1.0, "block {block} sample {i} left the mixer at {s}");
1681                peak = peak.max(s.abs());
1682            }
1683        }
1684        assert!(peak > 0.5, "four loud tracks should be loud, peak={peak}");
1685    }
1686
1687    /// The limiter must be inaudible in ordinary playing, which means it must
1688    /// not engage at all. The worst single track the application can produce
1689    /// is the loudest preset in the bank, an eight-note chord at velocity 127,
1690    /// with the fader all the way open — and that still has to leave the gain
1691    /// at exactly unity, so the mix is the track sum sample for sample.
1692    #[test]
1693    fn limiter_idle_for_the_worst_single_track() {
1694        let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
1695        let handle = Arc::new(TrackHandle::new(0, TrackKind::Instrument));
1696        handle.config.midi_active.store(true, std::sync::atomic::Ordering::Relaxed);
1697        handle.config.set_volume(1.0);
1698        let synth = loudest_dx7_voice();
1699        tx.send(MixerCommand::AddTrack { kind: TrackKind::Instrument, handle }).unwrap();
1700        tx.send(MixerCommand::SetInstrument { track_id: 0, instrument: Box::new(synth) }).unwrap();
1701        transport.play();
1702
1703        let chord: Vec<MidiMessage> = [36u8, 43, 48, 55, 60, 64, 67, 72]
1704            .iter()
1705            .map(|&note| make_note_on(note, 127))
1706            .collect();
1707
1708        let mut output = vec![0.0f32; 512];
1709        let mut peak = 0.0f32;
1710        for block in 0..200 {
1711            output.fill(0.0);
1712            if block == 0 {
1713                mixer.process(&mut output, &chord, &transport);
1714            } else {
1715                mixer.process(&mut output, &[], &transport);
1716            }
1717            peak = peak.max(output.iter().map(|s| s.abs()).fold(0.0f32, f32::max));
1718            assert_eq!(
1719                mixer.limiter.gain, 1.0,
1720                "limiter engaged at block {block}, peak {peak}"
1721            );
1722        }
1723        assert!(peak > 0.3, "expected a loud chord, peak={peak}");
1724    }
1725
1726    // ── Fader ──
1727
1728    /// Render the loudest thing one track in this project can produce, with
1729    /// the fader at `volume`. Returns the output peak and the lowest gain the
1730    /// limiter reached.
1731    fn worst_track_through_the_mixer(volume: f32) -> (f32, f32) {
1732        let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
1733        let handle = Arc::new(TrackHandle::new(0, TrackKind::Instrument));
1734        handle.config.midi_active.store(true, std::sync::atomic::Ordering::Relaxed);
1735        handle.config.set_volume(volume);
1736        let synth = loudest_dx7_voice();
1737        tx.send(MixerCommand::AddTrack { kind: TrackKind::Instrument, handle }).unwrap();
1738        tx.send(MixerCommand::SetInstrument { track_id: 0, instrument: Box::new(synth) }).unwrap();
1739        transport.play();
1740
1741        let chord: Vec<MidiMessage> = [36u8, 43, 48, 55, 60, 64, 67, 72]
1742            .iter()
1743            .map(|&note| make_note_on(note, 127))
1744            .collect();
1745
1746        let mut output = vec![0.0f32; 512];
1747        let mut peak = 0.0f32;
1748        let mut min_gain = 1.0f32;
1749        for block in 0..200 {
1750            output.fill(0.0);
1751            if block == 0 {
1752                mixer.process(&mut output, &chord, &transport);
1753            } else {
1754                mixer.process(&mut output, &[], &transport);
1755            }
1756            for &s in output.iter() {
1757                assert!(s.is_finite(), "block {block}: non-finite sample");
1758                assert!(s.abs() <= 1.0, "block {block}: sample left the mixer at {s}");
1759                peak = peak.max(s.abs());
1760            }
1761            min_gain = min_gain.min(mixer.limiter.gain);
1762        }
1763        (peak, min_gain)
1764    }
1765
1766    /// Anywhere from the bottom of the fader up to unity, the limiter is not
1767    /// in the signal path at all — not "barely", not at all — even for the
1768    /// loudest patch in the project played as hard as the format allows.
1769    ///
1770    /// This is what the instrument trims buy. Gain reduction on the master
1771    /// bus is then always a mix decision (several loud tracks at once) rather
1772    /// than something one instrument can cause on its own.
1773    #[test]
1774    fn fader_below_unity_never_engages_the_limiter() {
1775        for volume in [
1776            0.25,
1777            TrackConfig::DEFAULT_VOLUME,
1778            TrackConfig::UNITY_VOLUME,
1779        ] {
1780            let (peak, min_gain) = worst_track_through_the_mixer(volume);
1781            assert_eq!(
1782                min_gain, 1.0,
1783                "limiter reduced by {:.2} dB at fader {volume} (peak {peak:.4})",
1784                20.0 * min_gain.log10()
1785            );
1786        }
1787    }
1788
1789    /// Above unity the fader is makeup gain the user asked for, and the
1790    /// limiter is what makes asking for it safe. Two things have to hold:
1791    /// the output stays bounded, and turning the fader up never makes the
1792    /// track quieter than leaving it at unity — a limiter that over-ducks
1793    /// would turn the top of the fader into a trap.
1794    #[test]
1795    fn fader_makeup_gain_is_bounded_not_wasted() {
1796        let (unity_peak, _) = worst_track_through_the_mixer(TrackConfig::UNITY_VOLUME);
1797        let (max_peak, min_gain) = worst_track_through_the_mixer(TrackConfig::MAX_VOLUME);
1798
1799        assert!(
1800            max_peak <= LIMITER_CEILING,
1801            "fader at maximum let {max_peak:.4} through, above the ceiling"
1802        );
1803        assert!(
1804            max_peak >= unity_peak,
1805            "turning the fader up made the track quieter: {unity_peak:.4} -> {max_peak:.4}"
1806        );
1807        // The limiter took back some of the boost, but not more than the
1808        // fader added — otherwise it is attenuating, not limiting.
1809        let reduction_db = -20.0 * min_gain.log10();
1810        let boost_db = 20.0 * (TrackConfig::MAX_VOLUME / TrackConfig::UNITY_VOLUME).log10();
1811        assert!(
1812            reduction_db <= boost_db,
1813            "limiter took {reduction_db:.2} dB off a {boost_db:.2} dB boost"
1814        );
1815    }
1816
1817    // ── Metronome balance ──
1818
1819    /// The click has no fader and is not mixed through a track, so nothing
1820    /// downstream can compensate for it being wrong: it only sits right
1821    /// relative to the music if `CLICK_VOLUME` tracks the instruments'
1822    /// headroom trims. That coupling is invisible from either file and has
1823    /// already drifted once, when the trims moved and the click did not.
1824    ///
1825    /// So: a click against the level a user hears while playing — the default
1826    /// preset, a triad at velocity 100, fader at its default. Loud enough to
1827    /// play to, not so loud it is the loudest thing in the mix.
1828    #[test]
1829    fn metronome_click_sits_with_the_music() {
1830        use phosphor_dsp::dx7;
1831
1832        fn render(with_track: bool, metronome: bool) -> f32 {
1833            let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
1834            let chord: Vec<MidiMessage> = if with_track {
1835                let handle = Arc::new(TrackHandle::new(0, TrackKind::Instrument));
1836                handle.config.midi_active.store(true, std::sync::atomic::Ordering::Relaxed);
1837                tx.send(MixerCommand::AddTrack {
1838                    kind: TrackKind::Instrument,
1839                    handle,
1840                })
1841                .unwrap();
1842                tx.send(MixerCommand::SetInstrument {
1843                    track_id: 0,
1844                    instrument: Box::new(dx7::Dx7Synth::new()),
1845                })
1846                .unwrap();
1847                [60u8, 64, 67].iter().map(|&n| make_note_on(n, 100)).collect()
1848            } else {
1849                Vec::new()
1850            };
1851            if metronome {
1852                transport.toggle_metronome();
1853            }
1854            transport.play();
1855
1856            let mut output = vec![0.0f32; 512];
1857            let mut peak = 0.0f32;
1858            for block in 0..200 {
1859                output.fill(0.0);
1860                if block == 0 {
1861                    mixer.process(&mut output, &chord, &transport);
1862                } else {
1863                    mixer.process(&mut output, &[], &transport);
1864                }
1865                peak = peak.max(output.iter().map(|s| s.abs()).fold(0.0f32, f32::max));
1866                transport.advance(256, 44_100);
1867            }
1868            peak
1869        }
1870
1871        let music = render(true, false);
1872        let click = render(false, true);
1873        assert!(music > 0.0 && click > 0.0, "music {music}, click {click}");
1874
1875        let relative_db = 20.0 * (click / music).log10();
1876        assert!(
1877            (-12.0..=0.0).contains(&relative_db),
1878            "the click is {relative_db:.1} dB against a triad (click {click:.4}, \
1879             music {music:.4}); it has to be audible over the music without \
1880             being the loudest thing in the mix"
1881        );
1882    }
1883
1884    /// The fader reaches the audio thread. Not a tautology: `volume` is read
1885    /// per buffer through the atomic, so this catches a mix path that caches
1886    /// it or ignores it.
1887    #[test]
1888    fn fader_scales_the_track() {
1889        let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
1890        let handle = add_fixed_track(&tx, 0, 0.25);
1891        transport.play();
1892
1893        let mut output = vec![0.0f32; 512];
1894        for (volume, expected) in [(0.0f32, 0.0f32), (0.5, 0.125), (1.0, 0.25), (2.0, 0.5)] {
1895            handle.config.set_volume(volume);
1896            output.fill(0.0);
1897            mixer.process(&mut output, &[], &transport);
1898            let peak = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
1899            assert!(
1900                (peak - expected).abs() < 1.0e-6,
1901                "fader at {volume} gave {peak}, expected {expected}"
1902            );
1903        }
1904    }
1905
1906    // ── The device decides the rate ──
1907
1908    /// A device that would not give us the rate we asked for.
1909    fn refused(asked: u32, sample_rate: u32, max_buffer_frames: u32) -> StreamFormat {
1910        StreamFormat {
1911            sample_rate,
1912            buffer_size: Some(64),
1913            max_buffer_frames,
1914            channels: 2,
1915            sample_rate_request: Requested::Refused(asked),
1916            buffer_size_request: Requested::Granted,
1917        }
1918    }
1919
1920    /// The defect: the mixer was built from the command-line sample rate while
1921    /// the stream ran at the device's. Everything the mixer derives from the
1922    /// rate — oscillator increments, envelope times, the tick advance — was
1923    /// then wrong by the ratio between the two.
1924    #[test]
1925    fn the_mixer_runs_at_the_rate_the_device_granted() {
1926        let requested = crate::EngineConfig { buffer_size: 64, sample_rate: 44100 };
1927        let format = refused(44100, 48000, 4096);
1928        let effective = crate::EngineConfig::from(format);
1929
1930        let (_tx, rx) = mixer_command_channel();
1931        let (clip_tx, _clip_rx) = clip_snapshot_channel();
1932        let mixer = Mixer::new(
1933            rx,
1934            Arc::new(VuLevels::new()),
1935            clip_tx,
1936            effective.sample_rate,
1937            format.max_buffer_frames as usize,
1938        );
1939
1940        assert_eq!(mixer.sample_rate, 48000, "mixer must adopt the device's rate");
1941        assert_ne!(
1942            mixer.sample_rate, requested.sample_rate,
1943            "the request was 44100 and the device said 48000; taking the \
1944             request here is the 8.84%-sharp bug"
1945        );
1946        assert_eq!(mixer.max_buffer_size, 4096);
1947    }
1948
1949    /// A device that offers exactly what was asked for changes nothing.
1950    #[test]
1951    fn a_device_that_agrees_leaves_the_request_alone() {
1952        let requested = crate::EngineConfig { buffer_size: 64, sample_rate: 44100 };
1953        let format = StreamFormat {
1954            sample_rate: 44100,
1955            buffer_size: Some(64),
1956            max_buffer_frames: 4096,
1957            channels: 2,
1958            sample_rate_request: Requested::Granted,
1959            buffer_size_request: Requested::Granted,
1960        };
1961        assert_eq!(crate::EngineConfig::from(format), requested);
1962    }
1963
1964    /// The default path, and the one that has to be right for the most
1965    /// people: nothing asked for, so the mixer is built at whatever the
1966    /// device was already set to.
1967    #[test]
1968    fn asking_for_nothing_builds_the_mixer_at_the_devices_rate() {
1969        let format = StreamFormat {
1970            sample_rate: 48000,
1971            buffer_size: None,
1972            max_buffer_frames: 4096,
1973            channels: 2,
1974            sample_rate_request: Requested::Unasked,
1975            buffer_size_request: Requested::Unasked,
1976        };
1977        let effective = crate::EngineConfig::from(format);
1978
1979        let (_tx, rx) = mixer_command_channel();
1980        let (clip_tx, _clip_rx) = clip_snapshot_channel();
1981        let mixer = Mixer::new(
1982            rx,
1983            Arc::new(VuLevels::new()),
1984            clip_tx,
1985            effective.sample_rate,
1986            format.max_buffer_frames as usize,
1987        );
1988        assert_eq!(mixer.sample_rate, 48000);
1989        assert_eq!(mixer.max_buffer_size, 4096);
1990        assert!(format.divergence_notice().is_none(), "following the device is not news");
1991    }
1992
1993    /// The defect: buffers were sized from the requested block, the device
1994    /// handed the callback a larger one, and `process` grew them — a heap
1995    /// allocation on the audio thread, on the very first callback.
1996    #[test]
1997    fn the_largest_block_the_device_promised_never_grows_a_buffer() {
1998        let max_frames = 512usize;
1999        let (tx, rx) = mixer_command_channel();
2000        let (clip_tx, _clip_rx) = clip_snapshot_channel();
2001        let mut mixer = Mixer::new(
2002            rx,
2003            Arc::new(VuLevels::new()),
2004            clip_tx,
2005            48000,
2006            max_frames,
2007        );
2008        let transport = Arc::new(Transport::new(120.0));
2009        let _handle = add_armed_synth(&tx, 0);
2010        mixer.drain_commands();
2011
2012        // Snapshot after the track exists: adding one is a UI-driven
2013        // allocation, not a per-callback one.
2014        let before = (
2015            mixer.scratch_l.capacity(),
2016            mixer.scratch_r.capacity(),
2017            mixer.tracks[0].buf_l.capacity(),
2018            mixer.tracks[0].buf_r.capacity(),
2019        );
2020
2021        transport.play();
2022        let mut output = vec![0.0f32; max_frames * 2];
2023        mixer.process(&mut output, &[make_note_on(60, 100)], &transport);
2024
2025        let after = (
2026            mixer.scratch_l.capacity(),
2027            mixer.scratch_r.capacity(),
2028            mixer.tracks[0].buf_l.capacity(),
2029            mixer.tracks[0].buf_r.capacity(),
2030        );
2031        assert_eq!(
2032            before, after,
2033            "a block the size the device promised must fit the buffers as \
2034             allocated; growing one means the audio thread called the allocator"
2035        );
2036    }
2037
2038    /// The invariant stated everywhere in this crate, held to by the
2039    /// allocator rather than by reading the code: a steady-state callback
2040    /// touches no heap.
2041    #[test]
2042    fn a_steady_state_callback_does_not_allocate() {
2043        let max_frames = 512usize;
2044        let (tx, rx) = mixer_command_channel();
2045        let (clip_tx, _clip_rx) = clip_snapshot_channel();
2046        let mut mixer = Mixer::new(rx, Arc::new(VuLevels::new()), clip_tx, 48000, max_frames);
2047        let transport = Arc::new(Transport::new(120.0));
2048        let _handle = add_armed_synth(&tx, 0);
2049        mixer.drain_commands();
2050        transport.play();
2051
2052        let mut output = vec![0.0f32; max_frames * 2];
2053        // One warm-up block: anything lazily built on first use — the
2054        // wavetable bank behind its `OnceLock`, for one — is built here,
2055        // outside the region under test.
2056        mixer.process(&mut output, &[make_note_on(60, 100)], &transport);
2057
2058        let allocations = crate::alloc_count::allocations_during(|| {
2059            for _ in 0..8 {
2060                mixer.process(&mut output, &[], &transport);
2061            }
2062        });
2063        assert_eq!(allocations, 0, "Mixer::process reached the allocator");
2064    }
2065
2066    // ── The step sequencer ──
2067
2068    use crate::pattern::{ChainEntry, Lane, PatternEvent, Rate, Step};
2069
2070    /// A mixer at a given rate, with nothing on it.
2071    fn bare_mixer(
2072        sample_rate: u32,
2073        max_frames: usize,
2074    ) -> (Mixer, Sender<MixerCommand>, Arc<Transport>) {
2075        let (tx, rx) = mixer_command_channel();
2076        let (clip_tx, _clip_rx) = clip_snapshot_channel();
2077        let mixer = Mixer::new(rx, Arc::new(VuLevels::new()), clip_tx, sample_rate, max_frames);
2078        (mixer, tx, Arc::new(Transport::new(120.0)))
2079    }
2080
2081    /// A pattern with one drum lane on the steps named.
2082    fn kick_pattern(on: &[usize]) -> PatternBlock {
2083        let mut block = PatternBlock::empty();
2084        block.playing = true;
2085        block.lanes[0] = Lane::drum(36);
2086        for &index in on {
2087            block.lanes[0].steps[index].on = true;
2088        }
2089        block
2090    }
2091
2092    fn add_track(tx: &Sender<MixerCommand>, id: usize) -> Arc<TrackHandle> {
2093        let handle = Arc::new(TrackHandle::new(id, TrackKind::Instrument));
2094        tx.send(MixerCommand::AddTrack {
2095            kind: TrackKind::Instrument,
2096            handle: handle.clone(),
2097        })
2098        .unwrap();
2099        handle
2100    }
2101
2102    fn apply_all(mixer: &mut Mixer) {
2103        while !mixer.command_rx.is_empty() {
2104            mixer.drain_commands();
2105        }
2106    }
2107
2108    fn note_ons(track: &AudioTrack) -> impl Iterator<Item = &MidiEvent> {
2109        track.plugin_events.iter().filter(|e| e.status == 0x90 && e.data2 > 0)
2110    }
2111
2112    /// **The sync guarantee.** A pattern step and a clip note on the same
2113    /// beat have to reach the instrument at the same sample, in the same
2114    /// callback — at every block size and every sample rate, because those
2115    /// are what a wrong answer would be a function of.
2116    ///
2117    /// It holds by construction rather than by agreement: both go through
2118    /// one `PlaybackWindow`. This is the test that would catch that ceasing
2119    /// to be true.
2120    #[test]
2121    fn a_pattern_step_and_a_clip_note_land_on_the_same_sample() {
2122        for sample_rate in [44_100u32, 48_000, 96_000] {
2123            for frames in [64usize, 256, 470] {
2124                let (mut mixer, tx, transport) = bare_mixer(sample_rate, 512);
2125
2126                // Track 0: a clip with one note on beat two.
2127                let _clip_track = add_track(&tx, 0);
2128                tx.send(MixerCommand::CreateClip {
2129                    track_id: 0,
2130                    start_tick: 0,
2131                    length_ticks: 3840,
2132                })
2133                .unwrap();
2134                tx.send(MixerCommand::UpdateClip {
2135                    track_id: 0,
2136                    clip_index: 0,
2137                    events: vec![ClipEvent { tick: 960, status: 0x90, data1: 60, data2: 100 }],
2138                })
2139                .unwrap();
2140
2141                // Track 1: a pattern whose fourth sixteenth is beat two.
2142                let _seq_track = add_track(&tx, 1);
2143                tx.send(MixerCommand::SetPattern {
2144                    track_id: 1,
2145                    slot: 0,
2146                    block: kick_pattern(&[4]),
2147                })
2148                .unwrap();
2149                apply_all(&mut mixer);
2150
2151                transport.play();
2152                let mut output = vec![0.0f32; frames * 2];
2153                let mut landed = None;
2154                while transport.position_ticks() < 1_200 {
2155                    mixer.process(&mut output, &[], &transport);
2156                    let clip_note = note_ons(&mixer.tracks[0]).find(|e| e.data1 == 60);
2157                    let step_note = note_ons(&mixer.tracks[1]).find(|e| e.data1 == 36);
2158                    match (clip_note, step_note) {
2159                        (Some(c), Some(s)) => {
2160                            landed = Some((c.sample_offset, s.sample_offset));
2161                            break;
2162                        }
2163                        (None, None) => {}
2164                        (clip, step) => panic!(
2165                            "at {sample_rate} Hz / {frames} frames only one of them fired: \
2166                             clip={clip:?} step={step:?}"
2167                        ),
2168                    }
2169                    transport.advance(frames as u32, sample_rate);
2170                }
2171                let (clip_at, step_at) =
2172                    landed.unwrap_or_else(|| panic!("nothing fired at {sample_rate}/{frames}"));
2173                assert_eq!(
2174                    clip_at, step_at,
2175                    "at {sample_rate} Hz / {frames} frames the clip note landed on sample \
2176                     {clip_at} and the step on {step_at}"
2177                );
2178            }
2179        }
2180    }
2181
2182    /// A pattern is timed in ticks, so the same pattern has to occupy the
2183    /// same wall-clock time at every sample rate the application supports.
2184    #[test]
2185    fn step_timing_is_the_same_at_every_sample_rate() {
2186        let frames = 256usize;
2187        for sample_rate in [44_100u32, 48_000, 96_000] {
2188            let (mut mixer, tx, transport) = bare_mixer(sample_rate, 512);
2189            let _track = add_track(&tx, 0);
2190            tx.send(MixerCommand::SetPattern {
2191                track_id: 0,
2192                slot: 0,
2193                block: kick_pattern(&[0, 4, 8, 12]),
2194            })
2195            .unwrap();
2196            apply_all(&mut mixer);
2197
2198            transport.play();
2199            let mut output = vec![0.0f32; frames * 2];
2200            let mut seconds = Vec::new();
2201            let mut block = 0usize;
2202            while seconds.len() < 4 && transport.position_ticks() < 3_600 {
2203                mixer.process(&mut output, &[], &transport);
2204                for event in note_ons(&mixer.tracks[0]) {
2205                    let sample = block * frames + event.sample_offset as usize;
2206                    seconds.push(sample as f64 / f64::from(sample_rate));
2207                }
2208                transport.advance(frames as u32, sample_rate);
2209                block += 1;
2210            }
2211
2212            // Four steps a beat apart at 120 BPM: half a second each.
2213            assert_eq!(seconds.len(), 4, "at {sample_rate} Hz");
2214            for (index, at) in seconds.iter().enumerate() {
2215                let expected = index as f64 * 0.5;
2216                assert!(
2217                    (at - expected).abs() < 0.002,
2218                    "at {sample_rate} Hz step {index} landed at {at:.4}s, expected {expected:.4}s"
2219                );
2220            }
2221        }
2222    }
2223
2224    /// The wrap, which is where a sequencer written around a free-running
2225    /// cursor loses or repeats a step. Sixteen onsets per time round, every
2226    /// time round: the window stops at the loop point so nothing on the far
2227    /// side of it plays early, and the step is derived from the position so
2228    /// nothing is skipped when it comes back.
2229    #[test]
2230    fn a_loop_wrap_neither_drops_nor_doubles_the_first_step() {
2231        let frames = 256usize;
2232        let (mut mixer, tx, transport) = bare_mixer(44_100, 512);
2233        let _track = add_track(&tx, 0);
2234        let all_sixteen: Vec<usize> = (0..16).collect();
2235        tx.send(MixerCommand::SetPattern {
2236            track_id: 0,
2237            slot: 0,
2238            block: kick_pattern(&all_sixteen),
2239        })
2240        .unwrap();
2241        apply_all(&mut mixer);
2242
2243        transport.set_loop_bars(1, 1);
2244        transport.toggle_loop();
2245        transport.play();
2246
2247        let mut output = vec![0.0f32; frames * 2];
2248        let mut fired = 0usize;
2249        let mut wraps = 0usize;
2250        let mut last = transport.position_ticks();
2251        for _ in 0..4_000 {
2252            mixer.process(&mut output, &[], &transport);
2253            fired += note_ons(&mixer.tracks[0]).count();
2254            transport.advance(frames as u32, 44_100);
2255            let now = transport.position_ticks();
2256            if now < last {
2257                wraps += 1;
2258                if wraps == 4 {
2259                    break;
2260                }
2261            }
2262            last = now;
2263        }
2264        assert_eq!(wraps, 4, "the transport did not loop");
2265        assert_eq!(fired, 64, "four times round a 16-step pattern is 64 onsets");
2266    }
2267
2268    /// A sequencer track makes no sound of its own: it drives the instrument
2269    /// in the track's plugin slot, which is an ordinary instrument in an
2270    /// ordinary slot. Nothing in the audio path knows a sequencer exists.
2271    #[test]
2272    fn a_sequencer_track_plays_its_child_instrument() {
2273        let (mut mixer, tx, transport) = bare_mixer(44_100, 512);
2274        let handle = add_track(&tx, 0);
2275        handle.config.set_volume(1.0);
2276        tx.send(MixerCommand::SetInstrument {
2277            track_id: 0,
2278            instrument: Box::new(PhosphorSynth::new()),
2279        })
2280        .unwrap();
2281        let mut block = PatternBlock::empty();
2282        block.playing = true;
2283        block.lanes[0].steps[0].on = true;
2284        block.lanes[0].steps[0].gate = 200;
2285        tx.send(MixerCommand::SetPattern { track_id: 0, slot: 0, block }).unwrap();
2286        apply_all(&mut mixer);
2287
2288        transport.play();
2289        let mut output = vec![0.0f32; 512 * 2];
2290        let mut peak = 0.0f32;
2291        for _ in 0..8 {
2292            mixer.process(&mut output, &[], &transport);
2293            peak = peak.max(output.iter().map(|s| s.abs()).fold(0.0, f32::max));
2294            transport.advance(512, 44_100);
2295        }
2296        assert!(peak > 0.001, "the child instrument never sounded, peak={peak}");
2297    }
2298
2299    /// Stopping the transport ends every note the sequencer is holding. A
2300    /// tied step has no note-off of its own, so without this it is a voice
2301    /// that sounds until the next panic.
2302    #[test]
2303    fn stopping_the_transport_ends_every_pattern_note() {
2304        let (mut mixer, tx, transport) = bare_mixer(44_100, 512);
2305        let _track = add_track(&tx, 0);
2306        let mut block = kick_pattern(&[0]);
2307        block.lanes[0].steps[0].gate = Step::TIE;
2308        tx.send(MixerCommand::SetPattern { track_id: 0, slot: 0, block }).unwrap();
2309        apply_all(&mut mixer);
2310
2311        transport.play();
2312        let mut output = vec![0.0f32; 256 * 2];
2313        mixer.process(&mut output, &[], &transport);
2314        assert_eq!(note_ons(&mixer.tracks[0]).count(), 1);
2315        transport.advance(256, 44_100);
2316
2317        transport.pause();
2318        mixer.process(&mut output, &[], &transport);
2319        let offs: Vec<u8> = mixer.tracks[0]
2320            .plugin_events
2321            .iter()
2322            .filter(|e| e.status == 0x80)
2323            .map(|e| e.data1)
2324            .collect();
2325        assert_eq!(offs, vec![36], "the tied note was left sounding");
2326
2327        // ...and only once.
2328        mixer.process(&mut output, &[], &transport);
2329        assert!(mixer.tracks[0].plugin_events.is_empty());
2330    }
2331
2332    /// A panic drops the table rather than sounding it: the instruments are
2333    /// being reset underneath, so the offs would be addressed to voices that
2334    /// no longer exist.
2335    #[test]
2336    fn a_panic_leaves_the_sequencer_holding_nothing() {
2337        let (mut mixer, tx, transport) = bare_mixer(44_100, 512);
2338        let _track = add_track(&tx, 0);
2339        let mut block = kick_pattern(&[0]);
2340        block.lanes[0].steps[0].gate = Step::TIE;
2341        tx.send(MixerCommand::SetPattern { track_id: 0, slot: 0, block }).unwrap();
2342        apply_all(&mut mixer);
2343
2344        transport.play();
2345        let mut output = vec![0.0f32; 256 * 2];
2346        mixer.process(&mut output, &[], &transport);
2347        assert!(mixer.tracks[0].pattern.as_ref().unwrap().held_notes() > 0);
2348
2349        mixer.reset_all();
2350        assert_eq!(mixer.tracks[0].pattern.as_ref().unwrap().held_notes(), 0);
2351    }
2352
2353    /// The bounce, end to end and through a real instrument: one cycle of a
2354    /// swung pattern compiled to a clip, played back as a clip, has to be the
2355    /// same audio the sequencer produced live. Sample for sample — the two
2356    /// paths share a generator, so anything less is a defect rather than a
2357    /// tolerance.
2358    ///
2359    /// Every gate closes inside the cycle. A bounce is one time through, so a
2360    /// note that outlives the cycle has nowhere to go and the two renders
2361    /// would legitimately differ at the tail.
2362    #[test]
2363    fn a_bounced_pattern_renders_identically_to_the_live_one() {
2364        const SWING: u8 = 62;
2365        let mut block = PatternBlock::empty();
2366        block.playing = true;
2367        block.swing = SWING;
2368        block.rate = Rate::Sixteenth;
2369        for (index, (key, chord, gate)) in [
2370            (0usize, 0u8, 5u8, 50u8),
2371            (3, 3, 6, 90),
2372            (5, 7, 1, 25),
2373            (9, 5, 14, 75),
2374            (11, 10, 12, 40),
2375            (14, 0, 15, 60),
2376        ]
2377        .iter()
2378        .map(|(i, k, c, g)| (*i, (*k, *c, *g)))
2379        {
2380            let step = &mut block.lanes[0].steps[index];
2381            step.on = true;
2382            step.key = key;
2383            step.chord = chord;
2384            step.gate = gate;
2385            step.accent = index % 2 == 1;
2386        }
2387
2388        let cycle = block.length_ticks();
2389        let blocks = 24; // 24 x 512 frames at 44.1 kHz covers a bar and a bit
2390
2391        // Live: the sequencer driving the synth.
2392        let live = {
2393            let (mut mixer, tx, transport) = bare_mixer(44_100, 512);
2394            let handle = add_track(&tx, 0);
2395            handle.config.set_volume(1.0);
2396            tx.send(MixerCommand::SetInstrument {
2397                track_id: 0,
2398                instrument: Box::new(PhosphorSynth::new()),
2399            })
2400            .unwrap();
2401            tx.send(MixerCommand::SetPattern { track_id: 0, slot: 0, block }).unwrap();
2402            apply_all(&mut mixer);
2403            transport.play();
2404
2405            let mut rendered = Vec::new();
2406            let mut output = vec![0.0f32; 512 * 2];
2407            for _ in 0..blocks {
2408                mixer.process(&mut output, &[], &transport);
2409                rendered.extend_from_slice(&output);
2410                transport.advance(512, 44_100);
2411            }
2412            rendered
2413        };
2414
2415        // Bounced: the same cycle compiled to a clip, played as a clip.
2416        let bounced = {
2417            let mut events = Vec::new();
2418            crate::pattern::compile_cycle(&block, 0, &mut events);
2419            assert!(!events.is_empty());
2420            let clip_events: Vec<ClipEvent> = events
2421                .iter()
2422                .map(|e: &PatternEvent| ClipEvent {
2423                    tick: e.tick,
2424                    status: e.status,
2425                    data1: e.data1,
2426                    data2: e.data2,
2427                })
2428                .collect();
2429
2430            let (mut mixer, tx, transport) = bare_mixer(44_100, 512);
2431            let handle = add_track(&tx, 0);
2432            handle.config.set_volume(1.0);
2433            tx.send(MixerCommand::SetInstrument {
2434                track_id: 0,
2435                instrument: Box::new(PhosphorSynth::new()),
2436            })
2437            .unwrap();
2438            tx.send(MixerCommand::CreateClip {
2439                track_id: 0,
2440                start_tick: 0,
2441                length_ticks: cycle,
2442            })
2443            .unwrap();
2444            tx.send(MixerCommand::UpdateClip {
2445                track_id: 0,
2446                clip_index: 0,
2447                events: clip_events,
2448            })
2449            .unwrap();
2450            apply_all(&mut mixer);
2451            transport.play();
2452
2453            let mut rendered = Vec::new();
2454            let mut output = vec![0.0f32; 512 * 2];
2455            for _ in 0..blocks {
2456                mixer.process(&mut output, &[], &transport);
2457                rendered.extend_from_slice(&output);
2458                transport.advance(512, 44_100);
2459            }
2460            rendered
2461        };
2462
2463        assert_eq!(live.len(), bounced.len());
2464        let peak = live.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
2465        assert!(peak > 0.001, "the live render was silent, so this proves nothing");
2466        for (i, (a, b)) in live.iter().zip(&bounced).enumerate() {
2467            assert_eq!(
2468                a.to_bits(),
2469                b.to_bits(),
2470                "sample {i} differs: live {a} bounced {b} at {SWING}% swing"
2471            );
2472        }
2473    }
2474
2475    /// The rule the audio thread lives by, with a sequencer on it: taking a
2476    /// new pattern while notes are sounding, switching patterns, advancing a
2477    /// chain, playing chords and turning everything off are all writes into
2478    /// memory that already exists.
2479    #[test]
2480    fn pattern_playback_does_not_allocate() {
2481        let (mut mixer, tx, transport) = bare_mixer(48_000, 512);
2482        let _track = add_track(&tx, 0);
2483        tx.send(MixerCommand::SetInstrument {
2484            track_id: 0,
2485            instrument: Box::new(PhosphorSynth::new()),
2486        })
2487        .unwrap();
2488
2489        // Slot 0: chords on a melodic lane. Slot 1: a drum lane.
2490        let mut chords = PatternBlock::empty();
2491        chords.playing = true;
2492        chords.mode = crate::pattern::Mode::Aeolian;
2493        for index in 0..16 {
2494            let step = &mut chords.lanes[0].steps[index];
2495            step.on = true;
2496            step.chord = 4; // diatonic seventh
2497            step.voicing = 1 | Step::ROOT_BELOW;
2498            step.key = (index as u8 * 2) % 12;
2499        }
2500        let drums = kick_pattern(&[0, 4, 8, 12]);
2501
2502        tx.send(MixerCommand::SetPattern { track_id: 0, slot: 1, block: drums }).unwrap();
2503        tx.send(MixerCommand::SetPattern { track_id: 0, slot: 0, block: chords }).unwrap();
2504        // A clip on the same track, so the shared window is exercised from
2505        // both sides while the measurement is running.
2506        tx.send(MixerCommand::CreateClip { track_id: 0, start_tick: 0, length_ticks: 3840 })
2507            .unwrap();
2508        tx.send(MixerCommand::UpdateClip {
2509            track_id: 0,
2510            clip_index: 0,
2511            events: (0..16)
2512                .flat_map(|i| {
2513                    [
2514                        ClipEvent { tick: i * 240, status: 0x90, data1: 40, data2: 90 },
2515                        ClipEvent { tick: i * 240 + 120, status: 0x80, data1: 40, data2: 0 },
2516                    ]
2517                })
2518                .collect(),
2519        })
2520        .unwrap();
2521        apply_all(&mut mixer);
2522        transport.play();
2523
2524        let mut output = vec![0.0f32; 512 * 2];
2525        // Warm-up: anything built lazily on first use is built here.
2526        for _ in 0..2 {
2527            mixer.process(&mut output, &[], &transport);
2528            transport.advance(512, 48_000);
2529        }
2530
2531        let mut queued = chords;
2532        queued.pending_slot = Some(1);
2533        let mut chained = chords;
2534        chained.chain[0] = ChainEntry { slot: 0, repeats: 1 };
2535        chained.chain[1] = ChainEntry { slot: 1, repeats: 1 };
2536        chained.chain_len = 2;
2537
2538        let allocations = crate::alloc_count::allocations_during(|| {
2539            for block in 0..400 {
2540                if block == 20 {
2541                    tx.send(MixerCommand::SetPattern { track_id: 0, slot: 0, block: queued })
2542                        .unwrap();
2543                }
2544                if block == 120 {
2545                    tx.send(MixerCommand::SetPattern { track_id: 0, slot: 0, block: chained })
2546                        .unwrap();
2547                }
2548                mixer.process(&mut output, &[], &transport);
2549                transport.advance(512, 48_000);
2550            }
2551            transport.pause();
2552            mixer.process(&mut output, &[], &transport);
2553        });
2554        assert_eq!(allocations, 0, "the sequencer reached the allocator");
2555    }
2556
2557    /// What a queued command costs to sit in the channel. The block travels
2558    /// by value so that receiving one cannot reach the allocator, and this is
2559    /// the price of that: every `MixerCommand`, whichever variant, is now as
2560    /// wide as the widest one.
2561    ///
2562    /// Worth stating out loud rather than discovering later. A full command
2563    /// budget in flight is 150 kB of queue, which is nothing on the heap and
2564    /// everything on the audio thread's deadline, and that is the trade.
2565    #[test]
2566    fn a_command_is_as_wide_as_a_pattern() {
2567        assert_eq!(
2568            std::mem::size_of::<MixerCommand>(),
2569            crate::pattern::PatternBlock::SIZE + 11
2570        );
2571    }
2572
2573    /// What the UI reads to draw the playhead and the queued-slot countdown.
2574    /// Atomics on the track handle, the same shape as the VU meters.
2575    #[test]
2576    fn the_track_handle_reports_where_the_pattern_is() {
2577        let (mut mixer, tx, transport) = bare_mixer(44_100, 512);
2578        let handle = add_track(&tx, 0);
2579        let all_sixteen: Vec<usize> = (0..16).collect();
2580        let block = kick_pattern(&all_sixteen);
2581        tx.send(MixerCommand::SetPattern { track_id: 0, slot: 1, block }).unwrap();
2582        let mut queued = block;
2583        queued.pending_slot = Some(1);
2584        tx.send(MixerCommand::SetPattern { track_id: 0, slot: 0, block: queued }).unwrap();
2585        apply_all(&mut mixer);
2586
2587        // One step in, rather than on the downbeat: tick zero is itself a
2588        // pattern boundary, so a switch queued there is due immediately.
2589        transport.set_position(240);
2590        transport.play();
2591        let mut output = vec![0.0f32; 256 * 2];
2592        mixer.process(&mut output, &[], &transport);
2593        assert_eq!(handle.pattern.live_slot(), 0);
2594        assert_eq!(handle.pattern.queued_slot(), Some(1));
2595        assert_eq!(handle.pattern.step(), 1);
2596        assert!(handle.pattern.is_running());
2597
2598        // Half a bar in: step 8, and the switch has not happened yet.
2599        transport.set_position(1920);
2600        mixer.process(&mut output, &[], &transport);
2601        assert_eq!(handle.pattern.step(), 8);
2602        assert_eq!(handle.pattern.live_slot(), 0);
2603
2604        // Past the pattern end: the queued slot took over.
2605        transport.set_position(3840);
2606        mixer.process(&mut output, &[], &transport);
2607        assert_eq!(handle.pattern.live_slot(), 1);
2608        assert_eq!(handle.pattern.queued_slot(), None);
2609    }
2610
2611    /// The same, for the shorter blocks the device may hand us when the
2612    /// buffers were sized for its maximum.
2613    #[test]
2614    fn a_short_callback_does_not_allocate_either() {
2615        let max_frames = 512usize;
2616        let (tx, rx) = mixer_command_channel();
2617        let (clip_tx, _clip_rx) = clip_snapshot_channel();
2618        let mut mixer = Mixer::new(rx, Arc::new(VuLevels::new()), clip_tx, 48000, max_frames);
2619        let transport = Arc::new(Transport::new(120.0));
2620        let _handle = add_armed_synth(&tx, 0);
2621        mixer.drain_commands();
2622        transport.play();
2623
2624        let mut output = vec![0.0f32; 64 * 2];
2625        mixer.process(&mut output, &[make_note_on(60, 100)], &transport);
2626
2627        let allocations = crate::alloc_count::allocations_during(|| {
2628            for _ in 0..8 {
2629                mixer.process(&mut output, &[], &transport);
2630            }
2631        });
2632        assert_eq!(allocations, 0, "Mixer::process reached the allocator");
2633    }
2634}