Skip to main content

phosphor_core/
pattern.rs

1//! Step-sequencer patterns: what the audio thread plays, and the arithmetic
2//! that turns it into notes.
3//!
4//! A pattern is a grid — eight lanes of up to thirty-two steps — that
5//! generates MIDI for the instrument on its own track. It is not an
6//! instrument: it makes no sound, it makes note events, and the child
7//! instrument in the track's plugin slot turns those into audio. TR and
8//! Elektron lineage, with the DAW transport as master.
9//!
10//! # Why the shapes here are what they are
11//!
12//! **Fixed size and `Copy`.** A pattern crosses to the audio thread whole, as
13//! a value inside a [`crate::mixer::MixerCommand`]. No `Vec`, no `Box`, no
14//! `Arc`: receiving one is a move into memory that already exists, and the
15//! audio thread never reaches the allocator to accept an edit. That costs
16//! [`PatternBlock::SIZE`] bytes per queued command, which is the price of
17//! never taking a lock or an allocation on the deadline side.
18//!
19//! **Position-derived, never free-running.** The step under the playhead is
20//! `(position / ticks_per_step) mod steps` — a function of the transport's
21//! tick position and nothing else. There is no cursor that advances one step
22//! per callback, because a cursor drifts: starting playback in bar 5 would
23//! sound different from starting in bar 1 and waiting, and that is exactly
24//! the invariant clips already hold. Everything else follows from it —
25//! starting mid-pattern fires only the onsets that remain, and a loop wrap
26//! neither drops nor doubles the first step.
27//!
28//! **One window, shared with clips.** [`PlaybackWindow`] is the span of song
29//! time one callback renders, and both clip playback and pattern playback in
30//! `mixer.rs` take their events from the same value. That is what makes "a
31//! pattern step and a clip note on the same beat land on the same sample"
32//! structural rather than a coincidence two code paths have to keep agreeing
33//! on. It lives in this module because the sync guarantee is the reason this
34//! module exists at all.
35//!
36//! # Notes have to end
37//!
38//! Every note this module starts is written into a [`PendingOffs`] table with
39//! the tick its note-off is due at, and the table is drained in tick order as
40//! the windows go by. A tied step (`gate` = [`Step::TIE`]) has no due tick at
41//! all — it is ended by the lane's next onset, which is the 303 slide feel —
42//! and the table is flushed whole at every discontinuity: stop, pause, a
43//! position jump, a loop wrap, a pattern switch, a panic. The table holds
44//! thirty-two notes and an overflow forces off the *oldest* rather than
45//! dropping the new one, because a note that is never turned off is a stuck
46//! voice and this project has already shipped one fix for that class of bug.
47//!
48//! # What is pure and what is not
49//!
50//! Everything except [`PatternPlayer`] is a pure function of its arguments,
51//! and the player's state is four scalars and the pending table. There is no
52//! mixer, no sample rate and no plugin anywhere in this file: events come out
53//! stamped with the absolute tick they happen at, and turning a tick into a
54//! sample offset is [`PlaybackWindow::sample_offset`]'s single job. That is
55//! what lets the bounce in `phosphor-app` compile a pattern to a clip through
56//! *the same generator* the audio thread runs, which is the only way "the
57//! bounce sounds identical to the live pattern" can be a fact rather than a
58//! hope.
59
60// ── Sizes ──
61
62/// Lanes in a pattern.
63///
64/// Eight from day one, and not because eight drum voices is a nice round
65/// number: one note per step cannot sequence drums at all. A kick and a
66/// closed hat land on the same step in essentially every pattern ever
67/// written, so a single-note-per-step grid is not a simpler sequencer, it is
68/// a sequencer that cannot play a beat. The *view* may show one lane at a
69/// time; the data holds eight.
70pub const LANES: usize = 8;
71
72/// The longest a pattern can be. Shorter patterns mask the tail rather than
73/// clearing it — see [`PatternBlock::step_count`].
74pub const MAX_STEPS: usize = 32;
75
76/// Pattern slots per sequencer track.
77pub const SLOTS: usize = 8;
78
79/// Entries in a pattern chain. Each carries a repeat count, so "A×4 B×4 A×3
80/// C" is four entries rather than twelve.
81pub const MAX_CHAIN: usize = 16;
82
83/// How many sounding notes one track can be holding at once.
84///
85/// Eight lanes of six-note chords is 48, which this does not cover — and
86/// deliberately. The table is a safety net for notes whose offs are still in
87/// the future, not a voice allocator; the child instrument has its own. When
88/// it overflows, the oldest note is forced off and its slot reused, which is
89/// the one behaviour that cannot leave a note sounding forever.
90pub const MAX_PENDING_OFFS: usize = 32;
91
92/// The step counts a pattern may be set to.
93///
94/// 12 and 24 are in the list for triplet and 3/4 feel, and for deliberate
95/// polymeter against a 16-step lane — a 12-step pattern against a 4/4 bar
96/// walks its accent one beat every bar and comes home after three.
97pub const STEP_COUNTS: [u8; 6] = [4, 8, 12, 16, 24, 32];
98
99/// The most global step indices one window will be scanned for.
100///
101/// A bound on the work rather than a limit anything can reach: at the
102/// coarsest rate and the largest block a device may hand us, a window spans
103/// two or three steps. Sixty-four is roughly two seconds of audio at 120 BPM,
104/// which is two orders of magnitude past any real callback.
105const MAX_STEP_SCAN: i64 = 64;
106
107/// The most slot changes one window is allowed to contain.
108///
109/// A window spanning more than one chain entry means the entries are shorter
110/// than a callback, which no musical setting produces.
111const MAX_SEGMENTS: usize = 4;
112
113// ── Rate ──
114
115/// How long one step lasts, as a musical division.
116///
117/// An enum rather than the raw tick count so that a rate cannot be a number
118/// nothing plays at, and rather than a `u8` index so that a session or a UI
119/// cannot hand the audio thread a rate that does not exist. Ticks per step at
120/// 960 PPQ are exact for every entry, triplets included — 960 divides by 3.
121#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
122pub enum Rate {
123    Quarter,
124    Eighth,
125    #[default]
126    Sixteenth,
127    ThirtySecond,
128    EighthTriplet,
129    SixteenthTriplet,
130}
131
132impl Rate {
133    /// Every rate, in the order the UI steps through them.
134    pub const ALL: [Rate; 6] = [
135        Self::Quarter,
136        Self::Eighth,
137        Self::Sixteenth,
138        Self::ThirtySecond,
139        Self::EighthTriplet,
140        Self::SixteenthTriplet,
141    ];
142
143    /// Ticks one step lasts at 960 PPQ.
144    #[must_use]
145    pub const fn ticks(self) -> i64 {
146        match self {
147            Self::Quarter => 960,
148            Self::Eighth => 480,
149            Self::Sixteenth => 240,
150            Self::ThirtySecond => 120,
151            Self::EighthTriplet => 320,
152            Self::SixteenthTriplet => 160,
153        }
154    }
155
156    #[must_use]
157    pub const fn label(self) -> &'static str {
158        match self {
159            Self::Quarter => "1/4",
160            Self::Eighth => "1/8",
161            Self::Sixteenth => "1/16",
162            Self::ThirtySecond => "1/32",
163            Self::EighthTriplet => "1/8T",
164            Self::SixteenthTriplet => "1/16T",
165        }
166    }
167
168    /// Position in [`Rate::ALL`]. What a session stores, so it is stable.
169    #[must_use]
170    pub const fn index(self) -> u8 {
171        match self {
172            Self::Quarter => 0,
173            Self::Eighth => 1,
174            Self::Sixteenth => 2,
175            Self::ThirtySecond => 3,
176            Self::EighthTriplet => 4,
177            Self::SixteenthTriplet => 5,
178        }
179    }
180
181    /// The rate at `index`, or the default for anything out of range — a
182    /// session written by a later build names a rate this one does not have,
183    /// and a pattern at the wrong rate is better than a pattern that will not
184    /// load.
185    #[must_use]
186    pub fn from_index(index: u8) -> Self {
187        Self::ALL.get(index as usize).copied().unwrap_or_default()
188    }
189
190    /// One step up or down the list, stopping at the ends.
191    #[must_use]
192    pub fn stepped(self, delta: i32) -> Self {
193        let target = (i32::from(self.index()) + delta).clamp(0, Self::ALL.len() as i32 - 1);
194        Self::ALL[target as usize]
195    }
196}
197
198// ── Switch quantization ──
199
200/// When a queued pattern change takes effect.
201///
202/// Every one of these is a function of the song position, so the point a
203/// switch will happen is known the moment it is queued — which is what lets
204/// the UI count down to it rather than saying "soon".
205#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
206pub enum SwitchQuant {
207    /// At the end of the pattern currently playing. The default, because it
208    /// is the one that keeps the part in phase with itself.
209    #[default]
210    PatternEnd,
211    /// At the next bar line (4/4).
212    Bar,
213    /// At the next beat.
214    Beat,
215    /// At the start of the next callback.
216    Immediate,
217}
218
219impl SwitchQuant {
220    pub const ALL: [SwitchQuant; 4] = [Self::PatternEnd, Self::Bar, Self::Beat, Self::Immediate];
221
222    #[must_use]
223    pub const fn label(self) -> &'static str {
224        match self {
225            Self::PatternEnd => "pattern",
226            Self::Bar => "bar",
227            Self::Beat => "beat",
228            Self::Immediate => "now",
229        }
230    }
231
232    #[must_use]
233    pub const fn index(self) -> u8 {
234        match self {
235            Self::PatternEnd => 0,
236            Self::Bar => 1,
237            Self::Beat => 2,
238            Self::Immediate => 3,
239        }
240    }
241
242    #[must_use]
243    pub fn from_index(index: u8) -> Self {
244        Self::ALL.get(index as usize).copied().unwrap_or_default()
245    }
246
247    #[must_use]
248    pub fn stepped(self, delta: i32) -> Self {
249        let target = (i32::from(self.index()) + delta).clamp(0, Self::ALL.len() as i32 - 1);
250        Self::ALL[target as usize]
251    }
252
253    /// The first tick at or after `now` where a switch under this
254    /// quantization may happen.
255    ///
256    /// `pattern_ticks` is the length of the pattern that is playing — the
257    /// grid `PatternEnd` counts in. A tick that is already on the grid is
258    /// itself the answer, so a switch queued exactly on a bar line takes
259    /// effect on that bar line rather than the next.
260    #[must_use]
261    pub fn boundary(self, now: i64, pattern_ticks: i64) -> i64 {
262        let grid = match self {
263            Self::PatternEnd => pattern_ticks,
264            Self::Bar => crate::transport::Transport::PPQ * 4,
265            Self::Beat => crate::transport::Transport::PPQ,
266            Self::Immediate => return now,
267        };
268        if grid <= 0 {
269            return now;
270        }
271        // div_euclid so that a negative position — which nothing produces
272        // today, but the transport's position is an i64 — rounds towards the
273        // next boundary rather than towards zero.
274        (now + grid - 1).div_euclid(grid) * grid
275    }
276}
277
278// ── Modes ──
279
280/// The scale a pattern's pitch controls walk in.
281///
282/// Chromatic is "off": every semitone is available and the diatonic chord
283/// types have no degree to derive a quality from, so they collapse to major
284/// and major seventh.
285#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
286pub enum Mode {
287    #[default]
288    Chromatic,
289    Ionian,
290    Dorian,
291    Phrygian,
292    Lydian,
293    Mixolydian,
294    Aeolian,
295    Locrian,
296}
297
298/// The major scale, which every mode here is a rotation of.
299const IONIAN: [i32; 7] = [0, 2, 4, 5, 7, 9, 11];
300
301/// Triad quality on each degree of the major scale. Every other mode reads
302/// this table at an offset — that is what a mode *is*.
303const IONIAN_TRIADS: [Chord; 7] = [
304    Chord::Maj,
305    Chord::Min,
306    Chord::Min,
307    Chord::Maj,
308    Chord::Maj,
309    Chord::Min,
310    Chord::Dim,
311];
312
313/// Seventh-chord quality on each degree of the major scale.
314///
315/// The seventh degree is half-diminished, which is not one of the sixteen
316/// chord types a step can name. It does not have to be: `diatonic7` produces
317/// intervals rather than selecting a type, so the one quality with no name in
318/// the list is still the one that gets played.
319const IONIAN_SEVENTHS: [[i32; 4]; 7] = [
320    [0, 4, 7, 11], // I maj7
321    [0, 3, 7, 10], // ii m7
322    [0, 3, 7, 10], // iii m7
323    [0, 4, 7, 11], // IV maj7
324    [0, 4, 7, 10], // V 7
325    [0, 3, 7, 10], // vi m7
326    [0, 3, 6, 10], // vii m7♭5
327];
328
329impl Mode {
330    pub const ALL: [Mode; 8] = [
331        Self::Chromatic,
332        Self::Ionian,
333        Self::Dorian,
334        Self::Phrygian,
335        Self::Lydian,
336        Self::Mixolydian,
337        Self::Aeolian,
338        Self::Locrian,
339    ];
340
341    #[must_use]
342    pub const fn label(self) -> &'static str {
343        match self {
344            Self::Chromatic => "chromatic",
345            Self::Ionian => "ionian",
346            Self::Dorian => "dorian",
347            Self::Phrygian => "phrygian",
348            Self::Lydian => "lydian",
349            Self::Mixolydian => "mixolydian",
350            Self::Aeolian => "aeolian",
351            Self::Locrian => "locrian",
352        }
353    }
354
355    #[must_use]
356    pub const fn index(self) -> u8 {
357        match self {
358            Self::Chromatic => 0,
359            Self::Ionian => 1,
360            Self::Dorian => 2,
361            Self::Phrygian => 3,
362            Self::Lydian => 4,
363            Self::Mixolydian => 5,
364            Self::Aeolian => 6,
365            Self::Locrian => 7,
366        }
367    }
368
369    #[must_use]
370    pub fn from_index(index: u8) -> Self {
371        Self::ALL.get(index as usize).copied().unwrap_or_default()
372    }
373
374    #[must_use]
375    pub fn stepped(self, delta: i32) -> Self {
376        let target = (i32::from(self.index()) + delta).clamp(0, Self::ALL.len() as i32 - 1);
377        Self::ALL[target as usize]
378    }
379
380    /// How far into the major scale this mode starts. `None` for Chromatic,
381    /// which is not a rotation of anything.
382    #[must_use]
383    pub const fn rotation(self) -> Option<usize> {
384        match self {
385            Self::Chromatic => None,
386            Self::Ionian => Some(0),
387            Self::Dorian => Some(1),
388            Self::Phrygian => Some(2),
389            Self::Lydian => Some(3),
390            Self::Mixolydian => Some(4),
391            Self::Aeolian => Some(5),
392            Self::Locrian => Some(6),
393        }
394    }
395
396    /// Semitones above the tonic for each degree of this mode, ascending.
397    ///
398    /// `None` for Chromatic, which is not a scale — every semitone is a
399    /// degree, and asking which one a note is on has no answer.
400    #[must_use]
401    pub fn scale(self) -> Option<[i32; 7]> {
402        let rot = self.rotation()?;
403        let base = IONIAN[rot];
404        let mut out = [0; 7];
405        for (i, slot) in out.iter_mut().enumerate() {
406            *slot = (IONIAN[(i + rot) % 7] - base).rem_euclid(12);
407        }
408        Some(out)
409    }
410
411    /// Which degree of this mode `note` sits on, given a tonic pitch class.
412    ///
413    /// `None` when the note is not in the scale — a borrowed note, which is a
414    /// feature rather than a mistake, and which the diatonic chord types have
415    /// no quality for.
416    #[must_use]
417    pub fn degree_of(self, note: u8, tonic: u8) -> Option<usize> {
418        let scale = self.scale()?;
419        let pitch_class = (i32::from(note) - i32::from(tonic % 12)).rem_euclid(12);
420        scale.iter().position(|&s| s == pitch_class)
421    }
422
423    /// The note `steps` degrees away from `note` in this mode.
424    ///
425    /// Under Chromatic this is a semitone walk. In a mode it is a *scale*
426    /// walk: the pitch control moves by degrees, so holding the key sweeps
427    /// through the key rather than through every semitone in it. A note that
428    /// is not in the scale — one that was set before the mode was, or
429    /// borrowed on purpose — snaps onto the scale on the first press rather
430    /// than staying off it forever.
431    #[must_use]
432    pub fn walk(self, note: u8, tonic: u8, steps: i32) -> u8 {
433        let Some(scale) = self.scale() else {
434            return (i32::from(note) + steps).clamp(0, 127) as u8;
435        };
436        let tonic = i32::from(tonic % 12);
437        let relative = i32::from(note) - tonic;
438        let octave = relative.div_euclid(12);
439        let pitch_class = relative.rem_euclid(12);
440
441        // Where the note sits in the scale, or the degree just below it when
442        // it is not in the scale at all.
443        let (degree, on_scale) = match scale.iter().position(|&s| s == pitch_class) {
444            Some(d) => (d as i32, true),
445            None => (scale.iter().filter(|&&s| s < pitch_class).count() as i32 - 1, false),
446        };
447        // `degree` is the degree *below* a borrowed note, so walking up from
448        // one lands on the next degree already and walking down has to give
449        // back the step it would otherwise skip. Either direction snaps onto
450        // the scale on the first press.
451        let target = degree + steps + i32::from(!on_scale && steps < 0);
452        let target_octave = octave + target.div_euclid(7);
453        let target_degree = target.rem_euclid(7) as usize;
454        (tonic + target_octave * 12 + scale[target_degree]).clamp(0, 127) as u8
455    }
456}
457
458// ── Chords ──
459
460/// What a melodic step plays: one note, or several.
461///
462/// The order of this list is on disk — a step stores the chord it names by
463/// identity, so entries may be appended but never reordered. `Diatonic` and
464/// `Diatonic7` take their quality from the degree the root sits on in the
465/// pattern's mode, which is what makes a whole line of them sound like a key
466/// rather than like one chord transposed; every other entry is explicit, so
467/// that a borrowed chord stays borrowed.
468#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
469pub enum Chord {
470    #[default]
471    None,
472    Fifth,
473    Octave,
474    Diatonic,
475    Diatonic7,
476    Maj,
477    Min,
478    Dim,
479    Sus2,
480    Sus4,
481    Maj6,
482    Min6,
483    Dom7,
484    Min7,
485    Maj7,
486    Quartal,
487}
488
489impl Chord {
490    pub const ALL: [Chord; 16] = [
491        Self::None,
492        Self::Fifth,
493        Self::Octave,
494        Self::Diatonic,
495        Self::Diatonic7,
496        Self::Maj,
497        Self::Min,
498        Self::Dim,
499        Self::Sus2,
500        Self::Sus4,
501        Self::Maj6,
502        Self::Min6,
503        Self::Dom7,
504        Self::Min7,
505        Self::Maj7,
506        Self::Quartal,
507    ];
508
509    /// The identity a step stores. Stable forever.
510    #[must_use]
511    pub const fn index(self) -> u8 {
512        match self {
513            Self::None => 0,
514            Self::Fifth => 1,
515            Self::Octave => 2,
516            Self::Diatonic => 3,
517            Self::Diatonic7 => 4,
518            Self::Maj => 5,
519            Self::Min => 6,
520            Self::Dim => 7,
521            Self::Sus2 => 8,
522            Self::Sus4 => 9,
523            Self::Maj6 => 10,
524            Self::Min6 => 11,
525            Self::Dom7 => 12,
526            Self::Min7 => 13,
527            Self::Maj7 => 14,
528            Self::Quartal => 15,
529        }
530    }
531
532    #[must_use]
533    pub fn from_index(index: u8) -> Self {
534        Self::ALL.get(index as usize).copied().unwrap_or_default()
535    }
536
537    #[must_use]
538    pub fn stepped(self, delta: i32) -> Self {
539        let target = (i32::from(self.index()) + delta).clamp(0, Self::ALL.len() as i32 - 1);
540        Self::ALL[target as usize]
541    }
542
543    /// Semitones above the root, written into `out`, and how many there are.
544    ///
545    /// `mode` and `tonic` are only read by the two diatonic entries; under
546    /// Chromatic, or on a root the mode does not contain, they fall back to
547    /// major and major seventh.
548    fn intervals(self, root: u8, mode: Mode, tonic: u8, out: &mut [i32; 4]) -> usize {
549        let fixed: &[i32] = match self {
550            Self::None => &[0],
551            Self::Fifth => &[0, 7],
552            Self::Octave => &[0, 12],
553            Self::Maj => &[0, 4, 7],
554            Self::Min => &[0, 3, 7],
555            Self::Dim => &[0, 3, 6],
556            Self::Sus2 => &[0, 2, 7],
557            Self::Sus4 => &[0, 5, 7],
558            Self::Maj6 => &[0, 4, 7, 9],
559            Self::Min6 => &[0, 3, 7, 9],
560            Self::Dom7 => &[0, 4, 7, 10],
561            Self::Min7 => &[0, 3, 7, 10],
562            Self::Maj7 => &[0, 4, 7, 11],
563            // Stacked fourths. Three notes rather than four, so it sits in
564            // the same register as the triads it is chosen against.
565            Self::Quartal => &[0, 5, 10],
566            Self::Diatonic | Self::Diatonic7 => {
567                let seventh = self == Self::Diatonic7;
568                let quality = mode
569                    .degree_of(root, tonic)
570                    .map(|degree| (degree + mode.rotation().unwrap_or(0)) % 7);
571                return match (quality, seventh) {
572                    (Some(d), false) => IONIAN_TRIADS[d].intervals(root, mode, tonic, out),
573                    (Some(d), true) => {
574                        out.copy_from_slice(&IONIAN_SEVENTHS[d]);
575                        4
576                    }
577                    (None, false) => Self::Maj.intervals(root, mode, tonic, out),
578                    (None, true) => Self::Maj7.intervals(root, mode, tonic, out),
579                };
580            }
581        };
582        out[..fixed.len()].copy_from_slice(fixed);
583        fixed.len()
584    }
585}
586
587/// How the notes of a chord are spread out.
588///
589/// Every one of these preserves the chord's pitch-class set — it is the same
590/// chord, arranged differently — which is the property the tests check.
591#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
592pub enum Voicing {
593    #[default]
594    Close,
595    /// The second voice from the top, down an octave. The definition is
596    /// worth being precise about: it is not "the middle note", and on a
597    /// four-note chord it is the third note up, not the second.
598    Drop2,
599    /// Lowest voice up an octave.
600    First,
601    /// Lowest two voices up an octave.
602    Second,
603}
604
605impl Voicing {
606    pub const ALL: [Voicing; 4] = [Self::Close, Self::Drop2, Self::First, Self::Second];
607
608    #[must_use]
609    pub const fn label(self) -> &'static str {
610        match self {
611            Self::Close => "close",
612            Self::Drop2 => "drop-2",
613            Self::First => "1st inv",
614            Self::Second => "2nd inv",
615        }
616    }
617
618    #[must_use]
619    pub const fn index(self) -> u8 {
620        match self {
621            Self::Close => 0,
622            Self::Drop2 => 1,
623            Self::First => 2,
624            Self::Second => 3,
625        }
626    }
627
628    #[must_use]
629    pub fn from_index(index: u8) -> Self {
630        Self::ALL.get(index as usize).copied().unwrap_or_default()
631    }
632
633    #[must_use]
634    pub fn stepped(self, delta: i32) -> Self {
635        let target = (i32::from(self.index()) + delta).clamp(0, Self::ALL.len() as i32 - 1);
636        Self::ALL[target as usize]
637    }
638}
639
640/// The most notes one step can produce: a four-note chord plus the bass
641/// double.
642pub const MAX_CHORD_NOTES: usize = 5;
643
644/// The notes one step plays, ascending, written into `out`.
645///
646/// Returns how many were written. Out-of-range results are folded by octaves
647/// rather than clamped or dropped — folding keeps the pitch class, and a
648/// chord that loses a note near the top of the keyboard is a chord that
649/// changes quality where nobody asked it to. Exact duplicates after folding
650/// are removed, because two note-ons of the same number on one lane leave the
651/// child instrument holding a voice nothing will turn off.
652#[must_use]
653pub fn chord_notes(
654    root: u8,
655    chord: Chord,
656    voicing: Voicing,
657    root_below: bool,
658    mode: Mode,
659    tonic: u8,
660    out: &mut [u8; MAX_CHORD_NOTES],
661) -> usize {
662    let mut intervals = [0i32; 4];
663    let count = chord.intervals(root, mode, tonic, &mut intervals);
664
665    let mut voices = [0i32; MAX_CHORD_NOTES];
666    for (slot, interval) in voices.iter_mut().zip(&intervals[..count]) {
667        *slot = i32::from(root) + interval;
668    }
669    let mut len = count;
670
671    // Voicings act on the chord as it stands, ascending. A one-note chord has
672    // nothing to rearrange, which is why every branch checks the count.
673    match voicing {
674        Voicing::Close => {}
675        Voicing::Drop2 => {
676            if len >= 2 {
677                voices[len - 2] -= 12;
678            }
679        }
680        Voicing::First => {
681            if len >= 2 {
682                voices[0] += 12;
683            }
684        }
685        Voicing::Second => {
686            if len >= 3 {
687                voices[0] += 12;
688                voices[1] += 12;
689            } else if len >= 2 {
690                voices[0] += 12;
691            }
692        }
693    }
694
695    if root_below && len < MAX_CHORD_NOTES {
696        voices[len] = i32::from(root) - 12;
697        len += 1;
698    }
699
700    // Fold into the MIDI range, sort, and drop exact duplicates.
701    for voice in &mut voices[..len] {
702        while *voice < 0 {
703            *voice += 12;
704        }
705        while *voice > 127 {
706            *voice -= 12;
707        }
708    }
709    voices[..len].sort_unstable();
710
711    let mut written = 0;
712    for i in 0..len {
713        if i > 0 && voices[i] == voices[i - 1] {
714            continue;
715        }
716        out[written] = voices[i] as u8;
717        written += 1;
718    }
719    written
720}
721
722// ── Step ──
723
724/// One cell of the grid.
725///
726/// Nine bytes, laid out so that the whole pattern is a plain block of memory
727/// that can be memcpy'd to the audio thread. `reserved` is room for the two
728/// per-step features that are already designed and not yet built —
729/// probability and ratchets — so that adding them later is not a change to
730/// the size of anything.
731#[derive(Debug, Clone, Copy, PartialEq, Eq)]
732pub struct Step {
733    /// Whether this step fires.
734    pub on: bool,
735    /// Octave part of the pitch: the note is `octave * 12 + key`. The UI
736    /// presents one pitch control; the storage stays split because that is
737    /// what a mode-quantised walk needs.
738    pub octave: u8,
739    /// Pitch class part of the pitch, 0..=11.
740    pub key: u8,
741    /// Which [`Chord`] this step plays, by [`Chord::index`].
742    pub chord: u8,
743    /// Which [`Voicing`], by [`Voicing::index`], plus [`Step::ROOT_BELOW`].
744    pub voicing: u8,
745    /// Whether the step takes the pattern's accent velocity rather than its
746    /// base velocity. Per-step numeric velocity is a later change to this
747    /// same field and needs no migration.
748    pub accent: bool,
749    /// Gate length as a percentage of the step, or [`Step::TIE`].
750    pub gate: u8,
751    /// Probability and ratchets, when they arrive.
752    pub reserved: [u8; 2],
753}
754
755impl Step {
756    /// A gate that holds the note until the lane's next onset.
757    ///
758    /// 255 rather than a separate field because a gate is one control: the
759    /// UI walks it up through the percentages and off the end into the tie,
760    /// which is where a player expects to find it.
761    pub const TIE: u8 = 255;
762
763    /// Shortest gate. Below this the note-off arrives before an envelope has
764    /// opened and the step is inaudible, which reads as a broken step.
765    pub const MIN_GATE: u8 = 5;
766
767    /// Longest gate: twice the step, so a step can hold through the next one.
768    pub const MAX_GATE: u8 = 200;
769
770    /// Bit in [`Step::voicing`] that doubles the root an octave below.
771    ///
772    /// Independent of the voicing rather than four more entries in the list,
773    /// because it composes with all of them.
774    pub const ROOT_BELOW: u8 = 0b0000_0100;
775
776    /// A step that is off, at middle C, one note, half gate.
777    #[must_use]
778    pub const fn silent() -> Self {
779        Self {
780            on: false,
781            octave: 5,
782            key: 0,
783            chord: 0,
784            voicing: 0,
785            accent: false,
786            gate: 50,
787            reserved: [0; 2],
788        }
789    }
790
791    /// The root note this step plays, clamped into the MIDI range.
792    #[must_use]
793    pub fn root(self) -> u8 {
794        (u32::from(self.octave) * 12 + u32::from(self.key)).min(127) as u8
795    }
796
797    #[must_use]
798    pub fn chord_kind(self) -> Chord {
799        Chord::from_index(self.chord)
800    }
801
802    #[must_use]
803    pub fn voicing_kind(self) -> Voicing {
804        Voicing::from_index(self.voicing & 0b11)
805    }
806
807    #[must_use]
808    pub fn root_below(self) -> bool {
809        self.voicing & Self::ROOT_BELOW != 0
810    }
811
812    /// How long this step holds, in ticks — or `None` when it is tied and
813    /// only the lane's next onset ends it.
814    ///
815    /// The gate is clamped here rather than where it is written, so that a
816    /// value out of range can only ever shorten or lengthen a note rather
817    /// than produce one with a negative length.
818    #[must_use]
819    pub fn gate_ticks(self, ticks_per_step: i64) -> Option<i64> {
820        if self.gate == Self::TIE {
821            return None;
822        }
823        let percent = i64::from(self.gate.clamp(Self::MIN_GATE, Self::MAX_GATE));
824        Some((ticks_per_step * percent / 100).max(1))
825    }
826}
827
828impl Default for Step {
829    fn default() -> Self {
830        Self::silent()
831    }
832}
833
834// ── Lane ──
835
836/// One row of the grid: a voice, and the steps that fire it.
837#[derive(Debug, Clone, Copy, PartialEq, Eq)]
838pub struct Lane {
839    /// The note every step on this lane plays, or [`Lane::FROM_STEP`] when
840    /// the pitch comes from the step instead.
841    ///
842    /// A drum lane is pinned to one note from the kit's map — that is what
843    /// makes it "the kick lane" — and a melodic lane takes its pitch, its
844    /// chord and its voicing from each step.
845    pub note: u8,
846    pub muted: bool,
847    pub soloed: bool,
848    pub steps: [Step; MAX_STEPS],
849}
850
851impl Lane {
852    /// [`Lane::note`] for a lane whose pitch comes from its steps. Outside
853    /// the MIDI range, so it cannot collide with a real note.
854    pub const FROM_STEP: u8 = 0xFF;
855
856    /// An empty melodic lane.
857    #[must_use]
858    pub const fn empty() -> Self {
859        Self {
860            note: Self::FROM_STEP,
861            muted: false,
862            soloed: false,
863            steps: [Step::silent(); MAX_STEPS],
864        }
865    }
866
867    /// An empty lane pinned to one drum voice.
868    #[must_use]
869    pub const fn drum(note: u8) -> Self {
870        Self { note, ..Self::empty() }
871    }
872
873    /// Whether this lane's pitch comes from its steps.
874    #[must_use]
875    pub const fn is_pitched(&self) -> bool {
876        self.note == Self::FROM_STEP
877    }
878}
879
880impl Default for Lane {
881    fn default() -> Self {
882        Self::empty()
883    }
884}
885
886// ── Chain ──
887
888/// One entry of a pattern chain: a slot, and how many times through.
889#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
890pub struct ChainEntry {
891    pub slot: u8,
892    /// Times through before moving on. Zero is read as one.
893    pub repeats: u8,
894}
895
896// ── PatternBlock ──
897
898/// A whole pattern, as the audio thread holds it.
899///
900/// Every field is a plain scalar or an array of them: the type is `Copy`,
901/// nothing in it points anywhere, and a command carrying one is a memcpy.
902///
903/// Five of the fields — `playing`, `pending_slot`, `switch_quant`, `chain`
904/// and `chain_len` — describe the *track* rather than this pattern. They ride
905/// on the block because a block is how the UI thread says anything to the
906/// audio thread about a sequencer, and [`PatternPlayer::apply`] takes the
907/// most recent word on them from whichever slot arrived last. The UI keeps
908/// its own single copy and writes it into every block it sends, which is what
909/// `phosphor-app`'s single dispatch function exists to guarantee.
910#[derive(Debug, Clone, Copy, PartialEq, Eq)]
911pub struct PatternBlock {
912    /// How many steps play, one of [`STEP_COUNTS`].
913    ///
914    /// Shortening a pattern *masks*: steps past the end keep their contents
915    /// and come back when the pattern is lengthened again.
916    pub steps: u8,
917    pub rate: Rate,
918    /// Swing as a percentage, 50 (straight) to 75 (fully triplet).
919    pub swing: u8,
920    /// Velocity of an ordinary step.
921    pub base_vel: u8,
922    /// Velocity of an accented step.
923    pub accent_vel: u8,
924    /// The gate a newly enabled step inherits.
925    pub default_gate: u8,
926    /// The scale pitch walking and the diatonic chords work in.
927    pub mode: Mode,
928    /// Tonic pitch class for `mode`, 0..=11. Named `tonic` and not `key`
929    /// because [`Step::key`] is a different thing one field away.
930    pub tonic: u8,
931    pub lanes: [Lane; LANES],
932    /// Whether this sequencer runs when the transport does.
933    pub playing: bool,
934    /// A slot queued to take over at the next [`SwitchQuant`] point.
935    ///
936    /// Queueing the slot that is already playing is not a switch, which is
937    /// what makes a stale queue on the UI side harmless.
938    pub pending_slot: Option<u8>,
939    pub switch_quant: SwitchQuant,
940    pub chain: [ChainEntry; MAX_CHAIN],
941    /// How many entries of `chain` are real. Zero means no chain, and the
942    /// live slot is whatever was last selected or queued.
943    pub chain_len: u8,
944}
945
946impl PatternBlock {
947    /// How many bytes one of these is, and therefore what a `SetPattern`
948    /// command costs to sit in the queue.
949    pub const SIZE: usize = std::mem::size_of::<Self>();
950
951    /// Lowest swing setting: straight.
952    pub const MIN_SWING: u8 = 50;
953
954    /// Highest swing setting. At 75 the pair is 3:1, which is a triplet
955    /// feel; past it the second note lands on the following step and the
956    /// pattern reads as a different rhythm rather than a swung one.
957    pub const MAX_SWING: u8 = 75;
958
959    /// An empty 16-step pattern at a sixteenth, straight, and RUNNING.
960    ///
961    /// Running by default is the difference between a sequencer and a trap:
962    /// a user writes steps, presses play, and must hear them. On hardware
963    /// the pattern plays when the machine plays; the run/stop toggle exists
964    /// to mute a pattern during a performance, not to stand between a
965    /// beginner and their first sound. This shipped as `false` once and the
966    /// first real user pressed play into silence.
967    #[must_use]
968    pub const fn empty() -> Self {
969        Self {
970            steps: 16,
971            rate: Rate::Sixteenth,
972            swing: Self::MIN_SWING,
973            base_vel: 100,
974            accent_vel: 127,
975            default_gate: 50,
976            mode: Mode::Chromatic,
977            tonic: 0,
978            lanes: [Lane::empty(); LANES],
979            playing: true,
980            pending_slot: None,
981            switch_quant: SwitchQuant::PatternEnd,
982            chain: [ChainEntry { slot: 0, repeats: 1 }; MAX_CHAIN],
983            chain_len: 0,
984        }
985    }
986
987    /// How many steps actually play. Always at least one, never more than
988    /// [`MAX_STEPS`] — a length out of range would otherwise index off the
989    /// end of a lane or divide by zero.
990    #[must_use]
991    pub fn step_count(&self) -> usize {
992        (self.steps as usize).clamp(1, MAX_STEPS)
993    }
994
995    #[must_use]
996    pub fn ticks_per_step(&self) -> i64 {
997        self.rate.ticks()
998    }
999
1000    /// One time through, in ticks.
1001    #[must_use]
1002    pub fn length_ticks(&self) -> i64 {
1003        self.ticks_per_step() * self.step_count() as i64
1004    }
1005
1006    /// How far an odd-numbered step is pushed late, in ticks.
1007    ///
1008    /// MPC-style: the offset is a fraction of *two* steps, so 75% puts the
1009    /// off-beat three quarters of the way through the pair — a triplet feel —
1010    /// and 50% is straight. Integer arithmetic on purpose: the bounce and the
1011    /// live player run this same expression, so "the bounce swings
1012    /// identically" needs no tolerance at all.
1013    ///
1014    /// Even step indices are never offset, which is what keeps the downbeat
1015    /// where the transport says it is.
1016    #[must_use]
1017    pub fn swing_offset(&self, step_index: usize) -> i64 {
1018        if step_index % 2 == 0 {
1019            return 0;
1020        }
1021        let swing = i64::from(self.swing.clamp(Self::MIN_SWING, Self::MAX_SWING));
1022        (swing - i64::from(Self::MIN_SWING)) * 2 * self.ticks_per_step() / 100
1023    }
1024
1025    /// The largest [`PatternBlock::swing_offset`] this pattern can produce —
1026    /// how far back a scan has to start to be sure of catching every onset.
1027    fn max_swing_offset(&self) -> i64 {
1028        let swing = i64::from(self.swing.clamp(Self::MIN_SWING, Self::MAX_SWING));
1029        (swing - i64::from(Self::MIN_SWING)) * 2 * self.ticks_per_step() / 100
1030    }
1031
1032    /// The tick step `index` fires at, counting from `origin`.
1033    ///
1034    /// `index` is a *global* step number and may run past the end of the
1035    /// pattern or before its start: index 17 of a 16-step pattern is step 1
1036    /// of the second time through.
1037    #[must_use]
1038    pub fn onset(&self, origin: i64, index: i64) -> i64 {
1039        let steps = self.step_count() as i64;
1040        let in_pattern = index.rem_euclid(steps) as usize;
1041        origin + index * self.ticks_per_step() + self.swing_offset(in_pattern)
1042    }
1043
1044    /// Which step is under `tick`, counting from `origin`. Swing is not
1045    /// applied: this is where the playhead is, not when a note fires.
1046    #[must_use]
1047    pub fn step_at(&self, origin: i64, tick: i64) -> usize {
1048        let steps = self.step_count() as i64;
1049        (tick - origin).div_euclid(self.ticks_per_step()).rem_euclid(steps) as usize
1050    }
1051
1052    /// Whether a lane sounds, given the pattern's mute and solo state.
1053    #[must_use]
1054    pub fn lane_audible(&self, lane: usize) -> bool {
1055        let Some(l) = self.lanes.get(lane) else { return false };
1056        if l.muted {
1057            return false;
1058        }
1059        let any_solo = self.lanes.iter().any(|l| l.soloed);
1060        !any_solo || l.soloed
1061    }
1062
1063    /// The chain as real entries, ignoring anything past `chain_len`.
1064    #[must_use]
1065    pub fn chain_entries(&self) -> &[ChainEntry] {
1066        &self.chain[..(self.chain_len as usize).min(MAX_CHAIN)]
1067    }
1068}
1069
1070impl Default for PatternBlock {
1071    fn default() -> Self {
1072        Self::empty()
1073    }
1074}
1075
1076// ── Events ──
1077
1078/// One MIDI event a pattern produced, stamped with the absolute song tick it
1079/// happens at.
1080///
1081/// A tick rather than a sample offset because this module has no sample rate:
1082/// [`PlaybackWindow::sample_offset`] is where a tick becomes a position in a
1083/// buffer, and the bounce never asks that question at all.
1084#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1085pub struct PatternEvent {
1086    pub tick: i64,
1087    pub status: u8,
1088    pub data1: u8,
1089    pub data2: u8,
1090}
1091
1092impl PatternEvent {
1093    #[must_use]
1094    pub const fn note_on(tick: i64, note: u8, velocity: u8) -> Self {
1095        Self { tick, status: 0x90, data1: note, data2: velocity }
1096    }
1097
1098    #[must_use]
1099    pub const fn note_off(tick: i64, note: u8) -> Self {
1100        Self { tick, status: 0x80, data1: note, data2: 0 }
1101    }
1102
1103    #[must_use]
1104    pub const fn is_note_on(&self) -> bool {
1105        self.status == 0x90 && self.data2 > 0
1106    }
1107}
1108
1109/// Somewhere for generated events to go.
1110///
1111/// The whole point of the trait is that there is one generator behind both
1112/// consumers: the audio thread writes straight into the track's plugin queue,
1113/// converting ticks to sample offsets as they arrive and refusing to grow it,
1114/// and the bounce writes into a `Vec` that cannot overflow. Generic rather
1115/// than `dyn`, so each call site compiles to the same code it would if the
1116/// sink were named directly.
1117///
1118/// Events arrive in no particular tick order. Both consumers sort by tick or
1119/// by sample offset with a *stable* sort, which is what carries the one
1120/// ordering rule that matters: a note-off pushed before a note-on at the same
1121/// tick stays before it, and so a switch boundary cannot cut the note it just
1122/// started.
1123pub trait EventSink {
1124    /// Returns whether the event was taken. `false` means the sink is full,
1125    /// and the generator stops rather than dropping events silently in the
1126    /// middle of a step.
1127    fn accept(&mut self, event: PatternEvent) -> bool;
1128}
1129
1130impl EventSink for Vec<PatternEvent> {
1131    fn accept(&mut self, event: PatternEvent) -> bool {
1132        self.push(event);
1133        true
1134    }
1135}
1136
1137// ── The playback window ──
1138
1139/// The span of song time one callback renders.
1140///
1141/// **This is the sync guarantee.** Clip playback and pattern playback in
1142/// `mixer.rs` take the same value and ask it the same question, so a clip
1143/// note and a pattern step on the same beat cannot land on different samples:
1144/// there is only one expression that turns a tick into a sample offset, and
1145/// only one that decides where the window starts and ends.
1146///
1147/// Windows are half-open and contiguous. The next window begins exactly where
1148/// this one ended, rather than at the transport's new position, because those
1149/// are not always the same number: a block is almost never a whole number of
1150/// ticks, and a transport that carries the remainder can advance 179 ticks
1151/// where the block measured 178. Starting the next window at the position
1152/// would leave a one-tick hole in song time, and an onset that fell in it
1153/// would never play. Starting it where the last one ended cannot.
1154#[derive(Debug, Clone, Copy)]
1155pub struct PlaybackWindow {
1156    from: i64,
1157    to: i64,
1158    /// Where the transport actually was when this callback began, which is
1159    /// not always `from`: see [`PlaybackWindow::for_block`]. Kept because it
1160    /// is the only honest thing to compare the *next* block's position
1161    /// against when deciding whether the playhead moved.
1162    position: i64,
1163    ticks_per_sample: f64,
1164    frames: u32,
1165    continuous: bool,
1166}
1167
1168impl PlaybackWindow {
1169    /// The largest gap between one window's end and the next block's position
1170    /// that still counts as continuous playback.
1171    ///
1172    /// One tick, and it is not a fudge factor: the block length in ticks is
1173    /// truncated and the transport's advance is not, so consecutive positions
1174    /// can run at most one tick ahead of the measured window. Anything larger
1175    /// is the playhead being moved, which is a discontinuity — pending notes
1176    /// get flushed and nothing is replayed.
1177    pub const MAX_TICK_GAP: i64 = 1;
1178
1179    /// The window for one callback.
1180    ///
1181    /// `loop_region` is the loop's `(start, end)` when the transport is
1182    /// looping, and `None` when it is not — one argument rather than a flag
1183    /// and two numbers, because "looping over nowhere" is not a state that
1184    /// should be spellable.
1185    ///
1186    /// `previous` is the window the last callback used, if playback has been
1187    /// running. Two things come from the loop region:
1188    ///
1189    /// * A wrap — the transport moving backwards — starts the window at the
1190    ///   loop point, so the ticks between it and the position the callback
1191    ///   arrived at are played rather than skipped. That is what clip
1192    ///   playback has always done.
1193    /// * The window never extends past the loop end. Without that, the last
1194    ///   callback of a loop would reach across the loop point and play the
1195    ///   first notes on the other side of it, and then the wrap would play
1196    ///   them again: one doubled downbeat per time round.
1197    #[must_use]
1198    pub fn for_block(
1199        position: i64,
1200        frames: u32,
1201        ticks_per_sample: f64,
1202        loop_region: Option<(i64, i64)>,
1203        previous: Option<Self>,
1204    ) -> Self {
1205        let span = (f64::from(frames) * ticks_per_sample) as i64;
1206
1207        let (from, continuous) = match (previous, loop_region) {
1208            (Some(prev), Some((loop_start, _))) if position < prev.position => (loop_start, false),
1209            (Some(prev), _) if prev.to <= position && position - prev.to <= Self::MAX_TICK_GAP => {
1210                (prev.to, true)
1211            }
1212            _ => (position, false),
1213        };
1214
1215        let mut to = position + span;
1216        if let Some((_, loop_end)) = loop_region {
1217            if loop_end > from {
1218                to = to.min(loop_end);
1219            }
1220        }
1221
1222        Self {
1223            from,
1224            to: to.max(from),
1225            position,
1226            ticks_per_sample,
1227            frames,
1228            continuous,
1229        }
1230    }
1231
1232    /// A window over part of this one, for splitting a callback at a pattern
1233    /// switch. Sample offsets are unchanged: they are measured from the
1234    /// original start of the block, not from the piece.
1235    #[must_use]
1236    pub fn narrowed(&self, from: i64, to: i64) -> Self {
1237        Self { from, to: to.max(from), ..*self }
1238    }
1239
1240    #[must_use]
1241    pub const fn from(&self) -> i64 {
1242        self.from
1243    }
1244
1245    #[must_use]
1246    pub const fn to(&self) -> i64 {
1247        self.to
1248    }
1249
1250    /// Whether this window carries on from the previous one. `false` after a
1251    /// jump, a loop wrap, or the first block of playback — every case where
1252    /// notes still sounding have to be turned off.
1253    #[must_use]
1254    pub const fn is_continuous(&self) -> bool {
1255        self.continuous
1256    }
1257
1258    #[must_use]
1259    pub const fn is_empty(&self) -> bool {
1260        self.to <= self.from
1261    }
1262
1263    #[must_use]
1264    pub const fn contains(&self, tick: i64) -> bool {
1265        tick >= self.from && tick < self.to
1266    }
1267
1268    /// Where in the callback's buffer an event at `tick` belongs.
1269    ///
1270    /// The one expression that turns song time into a sample. A tick before
1271    /// the window lands on the first sample rather than underflowing, and one
1272    /// past the end lands on the last: a note played at the wrong end of a
1273    /// buffer is 1.5 ms out, and a note not played at all is a hole in the
1274    /// part.
1275    #[must_use]
1276    pub fn sample_offset(&self, tick: i64) -> u32 {
1277        let last = self.frames.saturating_sub(1);
1278        let offset = tick - self.from;
1279        if offset <= 0 || self.ticks_per_sample <= 0.0 {
1280            return 0;
1281        }
1282        let samples = (offset as f64 / self.ticks_per_sample) as i64;
1283        u32::try_from(samples).unwrap_or(last).min(last)
1284    }
1285}
1286
1287// ── Pending note-offs ──
1288
1289/// A note that is sounding and the tick it has to stop at.
1290#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1291struct PendingOff {
1292    note: u8,
1293    lane: u8,
1294    /// When the note-off is due, or `None` for a tied note, which is ended
1295    /// by the lane's next onset and by nothing else.
1296    due: Option<i64>,
1297}
1298
1299/// Every note this track is holding, oldest first.
1300///
1301/// Oldest-first is maintained by removing with a shift rather than a swap,
1302/// which is what makes "overflow forces off the oldest" a one-line operation
1303/// on a table of thirty-two. The shift is at most 32 moves of 24 bytes.
1304#[derive(Debug, Clone, Copy)]
1305pub struct PendingOffs {
1306    entries: [PendingOff; MAX_PENDING_OFFS],
1307    len: usize,
1308}
1309
1310impl PendingOffs {
1311    #[must_use]
1312    pub const fn new() -> Self {
1313        Self {
1314            entries: [PendingOff { note: 0, lane: 0, due: None }; MAX_PENDING_OFFS],
1315            len: 0,
1316        }
1317    }
1318
1319    #[must_use]
1320    pub const fn len(&self) -> usize {
1321        self.len
1322    }
1323
1324    #[must_use]
1325    pub const fn is_empty(&self) -> bool {
1326        self.len == 0
1327    }
1328
1329    /// Forget everything without sounding an off. For a panic, where the
1330    /// instruments are being reset underneath us anyway.
1331    pub fn clear(&mut self) {
1332        self.len = 0;
1333    }
1334
1335    fn remove(&mut self, index: usize) -> PendingOff {
1336        let gone = self.entries[index];
1337        for i in index..self.len - 1 {
1338            self.entries[i] = self.entries[i + 1];
1339        }
1340        self.len -= 1;
1341        gone
1342    }
1343
1344    /// Note that `note` is sounding on `lane`.
1345    ///
1346    /// When the table is full the oldest note is turned off at `now` and its
1347    /// slot reused. Never drops the new note: a note with no off in the table
1348    /// is a note nothing will ever stop.
1349    pub fn hold(
1350        &mut self,
1351        lane: usize,
1352        note: u8,
1353        due: Option<i64>,
1354        now: i64,
1355        out: &mut impl EventSink,
1356    ) {
1357        if self.len == MAX_PENDING_OFFS {
1358            let oldest = self.remove(0);
1359            out.accept(PatternEvent::note_off(now, oldest.note));
1360        }
1361        self.entries[self.len] = PendingOff { note, lane: lane as u8, due };
1362        self.len += 1;
1363    }
1364
1365    /// Turn off everything held on `lane`, at `at`.
1366    ///
1367    /// Called immediately before a lane's next onset, which is what ends a
1368    /// tied note and what keeps a gate longer than a step from running over
1369    /// its own next hit.
1370    pub fn end_lane(&mut self, lane: usize, at: i64, out: &mut impl EventSink) {
1371        let lane = lane as u8;
1372        let mut i = 0;
1373        while i < self.len {
1374            if self.entries[i].lane == lane {
1375                let gone = self.remove(i);
1376                out.accept(PatternEvent::note_off(at, gone.note));
1377            } else {
1378                i += 1;
1379            }
1380        }
1381    }
1382
1383    /// Turn off everything whose off is due before `tick`, each at its own
1384    /// due tick.
1385    pub fn emit_due_before(&mut self, tick: i64, out: &mut impl EventSink) {
1386        let mut i = 0;
1387        while i < self.len {
1388            match self.entries[i].due {
1389                Some(due) if due < tick => {
1390                    let gone = self.remove(i);
1391                    out.accept(PatternEvent::note_off(due, gone.note));
1392                }
1393                _ => i += 1,
1394            }
1395        }
1396    }
1397
1398    /// Turn off everything, at `at`. Stop, pause, a position jump, a loop
1399    /// wrap, a pattern switch: every discontinuity ends every note.
1400    pub fn flush(&mut self, at: i64, out: &mut impl EventSink) {
1401        for i in 0..self.len {
1402            out.accept(PatternEvent::note_off(at, self.entries[i].note));
1403        }
1404        self.len = 0;
1405    }
1406}
1407
1408impl Default for PendingOffs {
1409    fn default() -> Self {
1410        Self::new()
1411    }
1412}
1413
1414// ── Generation ──
1415
1416/// Write every event one pattern produces between `from` and `to`.
1417///
1418/// Pure apart from `pending`, which is the caller's note-off table: the same
1419/// call with the same table produces the same events, which is why the bounce
1420/// can run it once over a whole cycle and get what the audio thread produces
1421/// over hundreds of callbacks.
1422///
1423/// `origin` is the tick the pattern's step 0 is anchored to — 0 for a pattern
1424/// playing on its own, and the start of the chain entry when a chain is
1425/// running. Nothing here keeps a cursor: which step fires is derived from the
1426/// tick, every time.
1427pub fn generate(
1428    block: &PatternBlock,
1429    origin: i64,
1430    from: i64,
1431    to: i64,
1432    pending: &mut PendingOffs,
1433    out: &mut impl EventSink,
1434) {
1435    if to <= from {
1436        return;
1437    }
1438    let tps = block.ticks_per_step();
1439    let steps = block.step_count() as i64;
1440
1441    // Which global step indices could have an onset inside the window. Swing
1442    // only ever pushes a step *later*, so the scan starts one full swing
1443    // offset early and every candidate is checked against the window anyway.
1444    let first = (from - origin - block.max_swing_offset()).div_euclid(tps);
1445    let last = (to - origin).div_euclid(tps) + 1;
1446    let last = last.min(first + MAX_STEP_SCAN);
1447
1448    let mut chord = [0u8; MAX_CHORD_NOTES];
1449    for index in first..last {
1450        let onset = block.onset(origin, index);
1451        if onset < from || onset >= to {
1452            continue;
1453        }
1454        // Everything that was already due before this onset goes first, so a
1455        // lane that re-triggers cannot have its new note cut by the old one's
1456        // off arriving afterwards.
1457        pending.emit_due_before(onset, out);
1458
1459        let step_index = index.rem_euclid(steps) as usize;
1460        for lane_index in 0..LANES {
1461            if !block.lane_audible(lane_index) {
1462                continue;
1463            }
1464            let lane = &block.lanes[lane_index];
1465            let step = lane.steps[step_index];
1466            if !step.on {
1467                continue;
1468            }
1469
1470            // The lane's previous note ends here, before the new one starts.
1471            // Insertion order is what carries that through the sort.
1472            pending.end_lane(lane_index, onset, out);
1473
1474            let velocity = if step.accent { block.accent_vel } else { block.base_vel };
1475            let velocity = velocity.clamp(1, 127);
1476            let due = step.gate_ticks(tps).map(|len| onset + len);
1477
1478            let count = if lane.is_pitched() {
1479                chord_notes(
1480                    step.root(),
1481                    step.chord_kind(),
1482                    step.voicing_kind(),
1483                    step.root_below(),
1484                    block.mode,
1485                    block.tonic,
1486                    &mut chord,
1487                )
1488            } else {
1489                chord[0] = lane.note;
1490                1
1491            };
1492
1493            for &note in &chord[..count] {
1494                if !out.accept(PatternEvent::note_on(onset, note, velocity)) {
1495                    return;
1496                }
1497                pending.hold(lane_index, note, due, onset, out);
1498            }
1499        }
1500    }
1501
1502    pending.emit_due_before(to, out);
1503}
1504
1505/// Compile one time through a pattern, as note events from tick zero.
1506///
1507/// The bounce. It calls [`generate`] over the whole cycle in one window
1508/// rather than reimplementing it, so swing, gates, ties and accents are not
1509/// "the same as" live playback — they are live playback, run with a different
1510/// sink. Notes still sounding at the end of the cycle are turned off at the
1511/// cycle's last tick, which is where the pattern would have ended them had it
1512/// stopped there.
1513pub fn compile_cycle(block: &PatternBlock, origin: i64, out: &mut Vec<PatternEvent>) {
1514    let length = block.length_ticks();
1515    let mut pending = PendingOffs::new();
1516    generate(block, origin, origin, origin + length, &mut pending, out);
1517    pending.flush(origin + length, out);
1518    out.sort_by_key(|e| e.tick);
1519}
1520
1521// ── The player ──
1522
1523/// Everything one sequencer track needs on the audio thread.
1524///
1525/// The bank lives here rather than on the UI side because a pattern switch
1526/// has to be *decided* on the audio thread: the quantization point is a tick,
1527/// the tick arrives in the middle of a callback, and asking the UI what to
1528/// play next at that moment would make the answer depend on when the UI
1529/// thread happened to be scheduled. With all eight slots resident, a switch
1530/// is an index change and a chain is a lookup.
1531///
1532/// Around 19 kB per sequencer track, allocated once, when the track's first
1533/// pattern arrives — the same shape as an instrument allocating its voice
1534/// array in `Plugin::init`. Nothing after that reaches the allocator.
1535#[derive(Debug, Clone, Copy)]
1536pub struct PatternPlayer {
1537    slots: [PatternBlock; SLOTS],
1538    /// The slot currently sounding.
1539    live: u8,
1540    /// The last word the UI thread said about the track, taken from whichever
1541    /// block arrived most recently. See [`PatternBlock`].
1542    playing: bool,
1543    pending_slot: Option<u8>,
1544    switch_quant: SwitchQuant,
1545    chain: [ChainEntry; MAX_CHAIN],
1546    chain_len: u8,
1547    /// Notes this track is holding.
1548    pending: PendingOffs,
1549    /// Whether the last callback was producing notes, so that stopping can
1550    /// flush exactly once.
1551    active: bool,
1552    /// The step the playhead was over when this player last ran, for the UI.
1553    step: u8,
1554}
1555
1556impl PatternPlayer {
1557    #[must_use]
1558    pub fn new() -> Self {
1559        Self {
1560            slots: [PatternBlock::empty(); SLOTS],
1561            live: 0,
1562            playing: false,
1563            pending_slot: None,
1564            switch_quant: SwitchQuant::PatternEnd,
1565            chain: [ChainEntry { slot: 0, repeats: 1 }; MAX_CHAIN],
1566            chain_len: 0,
1567            pending: PendingOffs::new(),
1568            active: false,
1569            step: 0,
1570        }
1571    }
1572
1573    /// Take a pattern for one slot, and with it the UI's current word on the
1574    /// track-level settings.
1575    pub fn apply(&mut self, slot: u8, block: PatternBlock) {
1576        let slot = (slot as usize).min(SLOTS - 1);
1577        self.slots[slot] = block;
1578        self.playing = block.playing;
1579        self.switch_quant = block.switch_quant;
1580        self.chain = block.chain;
1581        self.chain_len = block.chain_len;
1582        // Queueing the slot that is already live is not a switch, so a UI
1583        // mirror that still names a slot the audio thread has already
1584        // switched to cannot cause a second, silent switch. A running chain
1585        // owns the slot outright, so a queue against one is not held.
1586        self.pending_slot = block
1587            .pending_slot
1588            .filter(|&s| s != self.live && block.chain_len == 0);
1589    }
1590
1591    #[must_use]
1592    pub fn slot(&self, index: usize) -> &PatternBlock {
1593        &self.slots[index.min(SLOTS - 1)]
1594    }
1595
1596    #[must_use]
1597    pub fn live_slot(&self) -> u8 {
1598        self.live
1599    }
1600
1601    #[must_use]
1602    pub fn queued_slot(&self) -> Option<u8> {
1603        self.pending_slot
1604    }
1605
1606    /// The step the playhead was over on the last callback. What the UI draws
1607    /// its marker at.
1608    #[must_use]
1609    pub fn current_step(&self) -> u8 {
1610        self.step
1611    }
1612
1613    #[must_use]
1614    pub fn is_playing(&self) -> bool {
1615        self.playing
1616    }
1617
1618    #[must_use]
1619    pub fn held_notes(&self) -> usize {
1620        self.pending.len()
1621    }
1622
1623    /// Forget every held note without sounding an off. For a panic, where the
1624    /// instruments are reset underneath us.
1625    pub fn silence(&mut self) {
1626        self.pending.clear();
1627        self.active = false;
1628    }
1629
1630    /// Where the switch queued on this track will happen, and how many steps
1631    /// away that is from `now`. `None` when nothing is queued.
1632    ///
1633    /// Pure, and the UI computes the same answer from its own mirror: the
1634    /// countdown on screen is arithmetic, not a message from the audio
1635    /// thread that may or may not have arrived yet.
1636    #[must_use]
1637    pub fn countdown(&self, now: i64) -> Option<(u8, i64)> {
1638        let slot = self.pending_slot?;
1639        let block = &self.slots[self.live as usize];
1640        let at = self.switch_quant.boundary(now, block.length_ticks());
1641        Some((slot, (at - now).div_euclid(block.ticks_per_step())))
1642    }
1643
1644    /// Which slot plays at `tick`, where its step 0 is anchored, and the tick
1645    /// that answer stops being true at.
1646    ///
1647    /// A chain is read straight off the song position, exactly as a step is:
1648    /// the chain is a program the position indexes into rather than a cursor
1649    /// something advances, so dropping the playhead into bar 40 lands in the
1650    /// entry that belongs there. Without a chain the live slot is whatever
1651    /// was last selected, and the boundary is the queued switch, if any.
1652    fn locate(&self, tick: i64) -> (u8, i64, i64) {
1653        if let Some(found) = self.chain_at(tick) {
1654            return found;
1655        }
1656        let boundary = match self.pending_slot {
1657            Some(_) => {
1658                let block = &self.slots[self.live as usize];
1659                self.switch_quant.boundary(tick, block.length_ticks())
1660            }
1661            None => i64::MAX,
1662        };
1663        (self.live, 0, boundary)
1664    }
1665
1666    /// The chain entry covering `tick`, if a chain is running.
1667    fn chain_at(&self, tick: i64) -> Option<(u8, i64, i64)> {
1668        let entries = &self.chain[..(self.chain_len as usize).min(MAX_CHAIN)];
1669        if entries.is_empty() {
1670            return None;
1671        }
1672        let mut total = 0i64;
1673        for entry in entries {
1674            let slot = (entry.slot as usize).min(SLOTS - 1);
1675            total += i64::from(entry.repeats.max(1)) * self.slots[slot].length_ticks();
1676        }
1677        if total <= 0 {
1678            return None;
1679        }
1680
1681        let base = tick.div_euclid(total) * total;
1682        let mut offset = tick.rem_euclid(total);
1683        let mut start = base;
1684        for entry in entries {
1685            let slot = (entry.slot as usize).min(SLOTS - 1);
1686            let span = i64::from(entry.repeats.max(1)) * self.slots[slot].length_ticks();
1687            if offset < span {
1688                return Some((slot as u8, start, start + span));
1689            }
1690            offset -= span;
1691            start += span;
1692        }
1693        None
1694    }
1695
1696    /// Produce this callback's events.
1697    ///
1698    /// `transport_playing` is the DAW transport; the pattern also has to be
1699    /// running on its own account. Everything else — which step, which slot,
1700    /// where the switch lands — comes out of the window's ticks.
1701    pub fn render(
1702        &mut self,
1703        window: &PlaybackWindow,
1704        transport_playing: bool,
1705        out: &mut impl EventSink,
1706    ) {
1707        if !transport_playing || !self.playing {
1708            if self.active {
1709                self.pending.flush(window.from(), out);
1710                self.active = false;
1711            }
1712            return;
1713        }
1714
1715        // A jump, a loop wrap, or the first block after starting: nothing
1716        // that was sounding belongs to where we are now.
1717        if !window.is_continuous() && self.active {
1718            self.pending.flush(window.from(), out);
1719        }
1720        self.active = true;
1721
1722        let mut cursor = window.from();
1723        for _ in 0..MAX_SEGMENTS {
1724            if cursor >= window.to() {
1725                break;
1726            }
1727            let (slot, origin, boundary) = self.locate(cursor);
1728
1729            // A switch that is due at the cursor itself — an immediate one,
1730            // or a pattern end that this callback happens to start on — takes
1731            // effect before anything is generated, so the first note of the
1732            // new pattern is the first note of the segment.
1733            if boundary <= cursor {
1734                self.pending.flush(cursor, out);
1735                self.switch_at(cursor);
1736                continue;
1737            }
1738
1739            self.live = slot;
1740            let end = boundary.min(window.to());
1741            let block = self.slots[slot as usize];
1742            generate(&block, origin, cursor, end, &mut self.pending, out);
1743
1744            // A boundary exactly at the end of the window belongs to the next
1745            // callback, which starts there: taking it here would put its
1746            // note-offs a whole block early.
1747            if boundary < window.to() {
1748                self.pending.flush(boundary, out);
1749                self.switch_at(boundary);
1750            }
1751            cursor = end;
1752        }
1753
1754        let block = &self.slots[self.live as usize];
1755        let origin = self.chain_at(window.from()).map_or(0, |(_, start, _)| start);
1756        self.step = block.step_at(origin, window.from()) as u8;
1757    }
1758
1759    /// Take the queued slot at a boundary that has just passed.
1760    ///
1761    /// A chain moves on by itself — its slot is a function of the position —
1762    /// so a chained track has nothing to take here.
1763    fn switch_at(&mut self, boundary: i64) {
1764        if self.chain_at(boundary).is_some() {
1765            return;
1766        }
1767        if let Some(slot) = self.pending_slot.take() {
1768            self.live = (slot as usize).min(SLOTS - 1) as u8;
1769        }
1770    }
1771}
1772
1773impl Default for PatternPlayer {
1774    fn default() -> Self {
1775        Self::new()
1776    }
1777}
1778
1779#[cfg(test)]
1780mod tests {
1781    use super::*;
1782
1783    // ── Fixtures ──
1784
1785    /// A pattern with one drum lane, every step on, at a sixteenth.
1786    fn drum_pattern(steps: u8) -> PatternBlock {
1787        let mut block = PatternBlock::empty();
1788        block.steps = steps;
1789        block.playing = true;
1790        block.lanes[0] = Lane::drum(36);
1791        for step in &mut block.lanes[0].steps {
1792            step.on = true;
1793        }
1794        block
1795    }
1796
1797    /// A pattern with one melodic lane; only the steps named are on.
1798    fn melodic_pattern(on: &[usize]) -> PatternBlock {
1799        let mut block = PatternBlock::empty();
1800        block.playing = true;
1801        for &index in on {
1802            block.lanes[0].steps[index].on = true;
1803        }
1804        block
1805    }
1806
1807    fn onsets(events: &[PatternEvent]) -> Vec<i64> {
1808        events.iter().filter(|e| e.is_note_on()).map(|e| e.tick).collect()
1809    }
1810
1811    fn run(block: &PatternBlock, from: i64, to: i64) -> Vec<PatternEvent> {
1812        let mut out = Vec::new();
1813        let mut pending = PendingOffs::new();
1814        generate(block, 0, from, to, &mut pending, &mut out);
1815        out
1816    }
1817
1818    // ── Sizes ──
1819
1820    /// The block crosses to the audio thread by value, so its size is the
1821    /// cost of every queued `SetPattern`. Pinned here because a field that
1822    /// creeps in is a cost nobody measures at the time.
1823    #[test]
1824    fn the_block_is_the_size_it_is_supposed_to_be() {
1825        assert_eq!(std::mem::size_of::<Step>(), 9);
1826        assert_eq!(std::mem::size_of::<Lane>(), 3 + 32 * 9);
1827        assert_eq!(std::mem::size_of::<PatternBlock>(), PatternBlock::SIZE);
1828        assert_eq!(
1829            PatternBlock::SIZE, 2_373,
1830            "a pattern changed size; every queued SetPattern costs this many bytes"
1831        );
1832        // No padding anywhere: every field is a byte-aligned scalar, which is
1833        // what makes the whole thing a memcpy.
1834        assert_eq!(std::mem::align_of::<PatternBlock>(), 1);
1835    }
1836
1837    // ── Rates and swing ──
1838
1839    /// The rate table at 960 PPQ. Every division is exact, triplets
1840    /// included — which is the reason the project is at 960 and not 480.
1841    #[test]
1842    fn rate_ticks_are_the_960_ppq_table() {
1843        assert_eq!(Rate::Quarter.ticks(), 960);
1844        assert_eq!(Rate::Eighth.ticks(), 480);
1845        assert_eq!(Rate::Sixteenth.ticks(), 240);
1846        assert_eq!(Rate::ThirtySecond.ticks(), 120);
1847        assert_eq!(Rate::EighthTriplet.ticks(), 320);
1848        assert_eq!(Rate::SixteenthTriplet.ticks(), 160);
1849        // Three triplets fill the division they subdivide.
1850        assert_eq!(Rate::EighthTriplet.ticks() * 3, Rate::Quarter.ticks());
1851        assert_eq!(Rate::SixteenthTriplet.ticks() * 3, Rate::Eighth.ticks());
1852    }
1853
1854    #[test]
1855    fn straight_swing_moves_nothing() {
1856        let block = drum_pattern(16);
1857        assert_eq!(block.swing, PatternBlock::MIN_SWING);
1858        for step in 0..16 {
1859            assert_eq!(block.swing_offset(step), 0);
1860        }
1861    }
1862
1863    /// MPC swing: the percentage is where the off-beat falls inside the pair,
1864    /// so 75% is a triplet feel and the offset is exactly half a step.
1865    #[test]
1866    fn full_swing_is_a_triplet_feel() {
1867        let mut block = drum_pattern(16);
1868        block.swing = 75;
1869        assert_eq!(block.swing_offset(0), 0);
1870        assert_eq!(block.swing_offset(1), block.ticks_per_step() / 2);
1871        assert_eq!(block.swing_offset(2), 0);
1872        assert_eq!(block.swing_offset(15), block.ticks_per_step() / 2);
1873    }
1874
1875    /// Integer arithmetic, so the number is the same every time it is asked
1876    /// for — which is what makes the bounce and the live player agree without
1877    /// a tolerance.
1878    #[test]
1879    fn swing_is_exact_integer_ticks() {
1880        let mut block = drum_pattern(16);
1881        block.swing = 62;
1882        assert_eq!(block.swing_offset(1), 57); // (62-50) * 2 * 240 / 100
1883        block.rate = Rate::Eighth;
1884        assert_eq!(block.swing_offset(1), 115); // ...and 480
1885    }
1886
1887    /// Swing never reaches the following step, so the onsets stay in order
1888    /// however far it is pushed.
1889    #[test]
1890    fn swing_never_reorders_the_steps() {
1891        for swing in PatternBlock::MIN_SWING..=PatternBlock::MAX_SWING {
1892            let mut block = drum_pattern(16);
1893            block.swing = swing;
1894            let mut previous = i64::MIN;
1895            for index in 0..32 {
1896                let onset = block.onset(0, index);
1897                assert!(onset > previous, "swing {swing} reordered step {index}");
1898                previous = onset;
1899            }
1900        }
1901    }
1902
1903    // ── Position derivation ──
1904
1905    /// The clip invariant: what fires depends on where the transport is, not
1906    /// on how it got there. Starting inside a pattern plays the steps that
1907    /// are left, not the pattern from the top.
1908    #[test]
1909    fn starting_mid_pattern_fires_only_the_remaining_onsets() {
1910        let block = drum_pattern(16);
1911        let cycle = block.length_ticks();
1912        assert_eq!(cycle, 3840);
1913
1914        let whole = onsets(&run(&block, 0, cycle));
1915        assert_eq!(whole.len(), 16);
1916        assert_eq!(whole[0], 0);
1917
1918        let late = onsets(&run(&block, 1200, cycle));
1919        assert_eq!(late.len(), 11, "steps 5..=15 remain");
1920        assert_eq!(late[0], 1200);
1921        assert_eq!(late, whole[5..]);
1922    }
1923
1924    /// The step under the playhead is arithmetic on the position. Bar 5 of a
1925    /// 16-step pattern is the top of the pattern again.
1926    #[test]
1927    fn the_step_is_a_function_of_the_position() {
1928        let block = drum_pattern(16);
1929        assert_eq!(block.step_at(0, 0), 0);
1930        assert_eq!(block.step_at(0, 239), 0);
1931        assert_eq!(block.step_at(0, 240), 1);
1932        assert_eq!(block.step_at(0, 3840), 0);
1933        assert_eq!(block.step_at(0, 3840 * 4 + 720), 3);
1934    }
1935
1936    /// 12 and 24 exist so that a pattern can be deliberately out of phase
1937    /// with the bar. A 12-step sixteenth pattern is three beats long, so it
1938    /// walks one beat per bar and comes home on the fourth.
1939    #[test]
1940    fn a_twelve_step_pattern_drifts_against_the_bar() {
1941        let block = drum_pattern(12);
1942        let bar = 3840;
1943        assert_eq!(block.length_ticks(), 2880);
1944        assert_eq!(block.step_at(0, 0), 0);
1945        assert_eq!(block.step_at(0, bar), 4);
1946        assert_eq!(block.step_at(0, bar * 2), 8);
1947        assert_eq!(block.step_at(0, bar * 3), 0, "back in phase after three bars");
1948    }
1949
1950    /// Shortening a pattern hides the tail; it does not erase it.
1951    #[test]
1952    fn a_shorter_pattern_masks_rather_than_truncates() {
1953        let mut block = drum_pattern(32);
1954        assert_eq!(onsets(&run(&block, 0, block.length_ticks())).len(), 32);
1955
1956        block.steps = 16;
1957        let short = run(&block, 0, block.length_ticks());
1958        assert_eq!(onsets(&short).len(), 16);
1959
1960        block.steps = 32;
1961        assert_eq!(
1962            onsets(&run(&block, 0, block.length_ticks())).len(),
1963            32,
1964            "the steps past 16 were cleared rather than masked"
1965        );
1966    }
1967
1968    /// Contiguous windows tile a cycle exactly once — no onset falls in a
1969    /// crack and none is seen twice.
1970    #[test]
1971    fn tiling_a_cycle_with_windows_fires_every_step_once() {
1972        let block = drum_pattern(16);
1973        let cycle = block.length_ticks();
1974        for span in [1, 7, 240, 241, 1000] {
1975            let mut all = Vec::new();
1976            let mut pending = PendingOffs::new();
1977            let mut from = 0;
1978            while from < cycle {
1979                let to = (from + span).min(cycle);
1980                generate(&block, 0, from, to, &mut pending, &mut all);
1981                from = to;
1982            }
1983            assert_eq!(
1984                onsets(&all).len(),
1985                16,
1986                "span {span} produced the wrong number of onsets"
1987            );
1988        }
1989    }
1990
1991    // ── Gates and note-offs ──
1992
1993    #[test]
1994    fn a_gate_is_a_percentage_of_the_step() {
1995        let step = Step { gate: 50, ..Step::silent() };
1996        assert_eq!(step.gate_ticks(240), Some(120));
1997        let step = Step { gate: 200, ..Step::silent() };
1998        assert_eq!(step.gate_ticks(240), Some(480));
1999        // Out of range clamps rather than producing a note of no length.
2000        let step = Step { gate: 0, ..Step::silent() };
2001        assert_eq!(step.gate_ticks(240), Some(12));
2002        let step = Step { gate: Step::TIE, ..Step::silent() };
2003        assert_eq!(step.gate_ticks(240), None, "a tie has no due tick");
2004    }
2005
2006    #[test]
2007    fn every_note_gets_an_off() {
2008        let block = drum_pattern(16);
2009        let events = run(&block, 0, block.length_ticks() + 240);
2010        let ons = events.iter().filter(|e| e.is_note_on()).count();
2011        let offs = events.iter().filter(|e| e.status == 0x80).count();
2012        assert_eq!(ons, 17);
2013        assert_eq!(offs, 17, "a note was left sounding");
2014    }
2015
2016    /// A tie holds until the lane fires again, and the off it produces is at
2017    /// the next onset rather than at a gate length.
2018    #[test]
2019    fn a_tie_holds_to_the_next_onset() {
2020        let mut block = melodic_pattern(&[0, 4]);
2021        block.lanes[0].steps[0].gate = Step::TIE;
2022        let events = run(&block, 0, block.length_ticks());
2023
2024        let offs: Vec<i64> = events.iter().filter(|e| e.status == 0x80).map(|e| e.tick).collect();
2025        assert_eq!(offs[0], 960, "the tie ended somewhere other than step 4");
2026
2027        // ...and the off comes before the note-on it makes room for.
2028        let at_960: Vec<u8> = events.iter().filter(|e| e.tick == 960).map(|e| e.status).collect();
2029        assert_eq!(at_960, vec![0x80, 0x90], "the off has to be pushed first");
2030    }
2031
2032    /// A gate longer than the step does not run over the lane's own next hit:
2033    /// the retrigger cuts it, and the cut arrives before the new note.
2034    #[test]
2035    fn a_long_gate_is_cut_by_the_next_onset() {
2036        let mut block = melodic_pattern(&[0, 1]);
2037        block.lanes[0].steps[0].gate = 200;
2038        let events = run(&block, 0, 960);
2039        let at_240: Vec<u8> = events.iter().filter(|e| e.tick == 240).map(|e| e.status).collect();
2040        assert_eq!(at_240, vec![0x80, 0x90]);
2041    }
2042
2043    /// The table holds thirty-two notes, and the thirty-third forces off the
2044    /// oldest rather than being dropped. A dropped note-on would be silence;
2045    /// a dropped note-*off* is a voice that never stops.
2046    #[test]
2047    fn the_pending_table_forces_off_the_oldest_on_overflow() {
2048        let mut pending = PendingOffs::new();
2049        let mut out = Vec::new();
2050        for i in 0..MAX_PENDING_OFFS {
2051            pending.hold(0, 40 + i as u8, None, 0, &mut out);
2052        }
2053        assert_eq!(pending.len(), MAX_PENDING_OFFS);
2054        assert!(out.is_empty());
2055
2056        pending.hold(1, 99, None, 100, &mut out);
2057        assert_eq!(out.len(), 1);
2058        assert_eq!(out[0].data1, 40, "the oldest note was not the one forced off");
2059        assert_eq!(out[0].tick, 100);
2060        assert_eq!(pending.len(), MAX_PENDING_OFFS);
2061    }
2062
2063    #[test]
2064    fn a_flush_ends_everything_at_one_tick() {
2065        let mut pending = PendingOffs::new();
2066        let mut out = Vec::new();
2067        pending.hold(0, 60, Some(500), 0, &mut out);
2068        pending.hold(1, 64, None, 0, &mut out);
2069        pending.flush(300, &mut out);
2070        assert_eq!(out.len(), 2);
2071        assert!(out.iter().all(|e| e.tick == 300 && e.status == 0x80));
2072        assert!(pending.is_empty());
2073    }
2074
2075    // ── Mute and solo ──
2076
2077    #[test]
2078    fn a_muted_lane_is_silent_and_a_soloed_one_is_the_only_one() {
2079        let mut block = drum_pattern(16);
2080        block.lanes[1] = Lane::drum(42);
2081        for step in &mut block.lanes[1].steps {
2082            step.on = true;
2083        }
2084        assert_eq!(onsets(&run(&block, 0, 240)).len(), 2);
2085
2086        block.lanes[1].muted = true;
2087        assert_eq!(onsets(&run(&block, 0, 240)).len(), 1);
2088
2089        block.lanes[1].muted = false;
2090        block.lanes[1].soloed = true;
2091        let solo = run(&block, 0, 240);
2092        assert_eq!(onsets(&solo).len(), 1);
2093        assert_eq!(solo[0].data1, 42);
2094    }
2095
2096    // ── Velocity ──
2097
2098    #[test]
2099    fn accent_picks_the_patterns_accent_velocity() {
2100        let mut block = melodic_pattern(&[0, 1]);
2101        block.lanes[0].steps[1].accent = true;
2102        let events = run(&block, 0, 480);
2103        let ons: Vec<u8> = events.iter().filter(|e| e.is_note_on()).map(|e| e.data2).collect();
2104        assert_eq!(ons, vec![100, 127]);
2105    }
2106
2107    // ── Chords ──
2108
2109    fn notes_of(root: u8, chord: Chord, voicing: Voicing, below: bool, mode: Mode) -> Vec<u8> {
2110        let mut out = [0u8; MAX_CHORD_NOTES];
2111        let n = chord_notes(root, chord, voicing, below, mode, 0, &mut out);
2112        out[..n].to_vec()
2113    }
2114
2115    #[test]
2116    fn the_chord_table_is_the_shapes_it_names() {
2117        assert_eq!(notes_of(60, Chord::None, Voicing::Close, false, Mode::Chromatic), vec![60]);
2118        assert_eq!(notes_of(60, Chord::Fifth, Voicing::Close, false, Mode::Chromatic), vec![60, 67]);
2119        assert_eq!(notes_of(60, Chord::Octave, Voicing::Close, false, Mode::Chromatic), vec![60, 72]);
2120        assert_eq!(notes_of(60, Chord::Maj, Voicing::Close, false, Mode::Chromatic), vec![60, 64, 67]);
2121        assert_eq!(notes_of(60, Chord::Min, Voicing::Close, false, Mode::Chromatic), vec![60, 63, 67]);
2122        assert_eq!(notes_of(60, Chord::Dim, Voicing::Close, false, Mode::Chromatic), vec![60, 63, 66]);
2123        assert_eq!(notes_of(60, Chord::Sus2, Voicing::Close, false, Mode::Chromatic), vec![60, 62, 67]);
2124        assert_eq!(notes_of(60, Chord::Sus4, Voicing::Close, false, Mode::Chromatic), vec![60, 65, 67]);
2125        assert_eq!(notes_of(60, Chord::Maj6, Voicing::Close, false, Mode::Chromatic), vec![60, 64, 67, 69]);
2126        assert_eq!(notes_of(60, Chord::Min6, Voicing::Close, false, Mode::Chromatic), vec![60, 63, 67, 69]);
2127        assert_eq!(notes_of(60, Chord::Dom7, Voicing::Close, false, Mode::Chromatic), vec![60, 64, 67, 70]);
2128        assert_eq!(notes_of(60, Chord::Min7, Voicing::Close, false, Mode::Chromatic), vec![60, 63, 67, 70]);
2129        assert_eq!(notes_of(60, Chord::Maj7, Voicing::Close, false, Mode::Chromatic), vec![60, 64, 67, 71]);
2130        assert_eq!(notes_of(60, Chord::Quartal, Voicing::Close, false, Mode::Chromatic), vec![60, 65, 70]);
2131    }
2132
2133    /// The identities a step stores. Appending to this list is allowed;
2134    /// moving anything already in it rewrites every pattern ever saved.
2135    #[test]
2136    fn chord_identities_are_the_documented_order() {
2137        let order = [
2138            Chord::None, Chord::Fifth, Chord::Octave, Chord::Diatonic, Chord::Diatonic7,
2139            Chord::Maj, Chord::Min, Chord::Dim, Chord::Sus2, Chord::Sus4, Chord::Maj6,
2140            Chord::Min6, Chord::Dom7, Chord::Min7, Chord::Maj7, Chord::Quartal,
2141        ];
2142        for (index, chord) in order.iter().enumerate() {
2143            assert_eq!(chord.index() as usize, index);
2144            assert_eq!(Chord::from_index(index as u8), *chord);
2145        }
2146        assert_eq!(Chord::from_index(200), Chord::None, "an unknown id is one note");
2147    }
2148
2149    /// Drop-2 is the second voice from the top, down an octave — not the
2150    /// middle note, and on a four-note chord not the second note either.
2151    #[test]
2152    fn drop_two_lowers_the_second_voice_from_the_top() {
2153        // C E G -> E an octave down, under the root.
2154        assert_eq!(
2155            notes_of(60, Chord::Maj, Voicing::Drop2, false, Mode::Chromatic),
2156            vec![52, 60, 67]
2157        );
2158        // C E G B -> G an octave down.
2159        assert_eq!(
2160            notes_of(60, Chord::Maj7, Voicing::Drop2, false, Mode::Chromatic),
2161            vec![55, 60, 64, 71]
2162        );
2163    }
2164
2165    #[test]
2166    fn inversions_lift_the_bottom_voices() {
2167        assert_eq!(
2168            notes_of(60, Chord::Maj, Voicing::First, false, Mode::Chromatic),
2169            vec![64, 67, 72]
2170        );
2171        assert_eq!(
2172            notes_of(60, Chord::Maj, Voicing::Second, false, Mode::Chromatic),
2173            vec![67, 72, 76]
2174        );
2175    }
2176
2177    #[test]
2178    fn root_below_adds_the_bass_double() {
2179        assert_eq!(
2180            notes_of(60, Chord::Maj, Voicing::Close, true, Mode::Chromatic),
2181            vec![48, 60, 64, 67]
2182        );
2183    }
2184
2185    /// Every combination in the table, checked for the three things that
2186    /// would make a chord unplayable: a note outside MIDI, the same note
2187    /// twice — which leaves the child holding a voice nothing turns off —
2188    /// and an empty chord.
2189    #[test]
2190    fn every_chord_and_voicing_is_playable() {
2191        for &chord in &Chord::ALL {
2192            for &voicing in &Voicing::ALL {
2193                for below in [false, true] {
2194                    for &mode in &Mode::ALL {
2195                        for root in 24..=96u8 {
2196                            let notes = notes_of(root, chord, voicing, below, mode);
2197                            assert!(!notes.is_empty(), "{chord:?} produced nothing");
2198                            assert!(notes.len() <= MAX_CHORD_NOTES);
2199                            let mut seen = notes.clone();
2200                            seen.dedup();
2201                            assert_eq!(seen, notes, "{chord:?}/{voicing:?} doubled a note");
2202                            for window in notes.windows(2) {
2203                                assert!(window[0] < window[1], "not ascending");
2204                            }
2205                        }
2206                    }
2207                }
2208            }
2209        }
2210    }
2211
2212    /// A voicing rearranges a chord; it does not change it. The pitch-class
2213    /// set is what "the same chord" means, and it is invariant under every
2214    /// voicing — root-below excepted, which is a deliberate duplicate of one
2215    /// class an octave down and so leaves the *set* alone as well.
2216    #[test]
2217    fn voicings_preserve_the_pitch_class_set() {
2218        for &chord in &Chord::ALL {
2219            for &mode in &Mode::ALL {
2220                for root in 36..=84u8 {
2221                    let classes = |notes: Vec<u8>| {
2222                        let mut c: Vec<u8> = notes.iter().map(|n| n % 12).collect();
2223                        c.sort_unstable();
2224                        c.dedup();
2225                        c
2226                    };
2227                    let close = classes(notes_of(root, chord, Voicing::Close, false, mode));
2228                    for &voicing in &Voicing::ALL {
2229                        for below in [false, true] {
2230                            assert_eq!(
2231                                classes(notes_of(root, chord, voicing, below, mode)),
2232                                close,
2233                                "{chord:?} changed identity under {voicing:?} below={below}"
2234                            );
2235                        }
2236                    }
2237                }
2238            }
2239        }
2240    }
2241
2242    /// The textbook qualities, in every mode. This is the table the whole
2243    /// diatonic idea rests on: if the third degree of Dorian is not minor,
2244    /// a line of diatonic chords is not in a key at all.
2245    #[test]
2246    fn diatonic_triads_have_the_textbook_qualities_in_every_mode() {
2247        let expected: [(Mode, [Chord; 7]); 7] = [
2248            (Mode::Ionian, [Chord::Maj, Chord::Min, Chord::Min, Chord::Maj, Chord::Maj, Chord::Min, Chord::Dim]),
2249            (Mode::Dorian, [Chord::Min, Chord::Min, Chord::Maj, Chord::Maj, Chord::Min, Chord::Dim, Chord::Maj]),
2250            (Mode::Phrygian, [Chord::Min, Chord::Maj, Chord::Maj, Chord::Min, Chord::Dim, Chord::Maj, Chord::Min]),
2251            (Mode::Lydian, [Chord::Maj, Chord::Maj, Chord::Min, Chord::Dim, Chord::Maj, Chord::Min, Chord::Min]),
2252            (Mode::Mixolydian, [Chord::Maj, Chord::Min, Chord::Dim, Chord::Maj, Chord::Min, Chord::Min, Chord::Maj]),
2253            (Mode::Aeolian, [Chord::Min, Chord::Dim, Chord::Maj, Chord::Min, Chord::Min, Chord::Maj, Chord::Maj]),
2254            (Mode::Locrian, [Chord::Dim, Chord::Maj, Chord::Min, Chord::Min, Chord::Maj, Chord::Maj, Chord::Min]),
2255        ];
2256
2257        for tonic in 0..12u8 {
2258            for (mode, qualities) in &expected {
2259                let scale = mode.scale().expect("a mode has a scale");
2260                for (degree, &quality) in qualities.iter().enumerate() {
2261                    let root = 60 + i32::from(tonic) + scale[degree];
2262                    let root = root as u8;
2263                    let mut derived = [0u8; MAX_CHORD_NOTES];
2264                    let n = chord_notes(
2265                        root, Chord::Diatonic, Voicing::Close, false, *mode, tonic, &mut derived,
2266                    );
2267                    let mut explicit = [0u8; MAX_CHORD_NOTES];
2268                    let m = chord_notes(
2269                        root, quality, Voicing::Close, false, *mode, tonic, &mut explicit,
2270                    );
2271                    assert_eq!(
2272                        derived[..n],
2273                        explicit[..m],
2274                        "{mode:?} degree {} in tonic {tonic} should be {quality:?}",
2275                        degree + 1
2276                    );
2277                }
2278            }
2279        }
2280    }
2281
2282    /// The seventh degree of a major scale is half-diminished, which is not
2283    /// one of the sixteen chord types a step can name — and does not have to
2284    /// be, because `diatonic7` produces intervals rather than picking a type.
2285    #[test]
2286    fn the_seventh_degree_is_half_diminished() {
2287        let mut out = [0u8; MAX_CHORD_NOTES];
2288        let n = chord_notes(71, Chord::Diatonic7, Voicing::Close, false, Mode::Ionian, 0, &mut out);
2289        assert_eq!(&out[..n], &[71, 74, 77, 81], "B D F A is not m7♭5");
2290    }
2291
2292    /// Under Chromatic there is no degree to derive a quality from, so the
2293    /// two diatonic entries collapse — documented behaviour, not a fallback
2294    /// nobody meant.
2295    #[test]
2296    fn the_diatonic_chords_collapse_under_chromatic() {
2297        assert_eq!(
2298            notes_of(60, Chord::Diatonic, Voicing::Close, false, Mode::Chromatic),
2299            notes_of(60, Chord::Maj, Voicing::Close, false, Mode::Chromatic)
2300        );
2301        assert_eq!(
2302            notes_of(60, Chord::Diatonic7, Voicing::Close, false, Mode::Chromatic),
2303            notes_of(60, Chord::Maj7, Voicing::Close, false, Mode::Chromatic)
2304        );
2305    }
2306
2307    /// A note the mode does not contain is a borrowed note, and a diatonic
2308    /// chord on one has no derived quality — it falls back to major rather
2309    /// than refusing to sound.
2310    #[test]
2311    fn a_borrowed_root_falls_back_to_major() {
2312        assert_eq!(Mode::Ionian.degree_of(61, 0), None);
2313        assert_eq!(
2314            notes_of(61, Chord::Diatonic, Voicing::Close, false, Mode::Ionian),
2315            vec![61, 65, 68]
2316        );
2317    }
2318
2319    // ── Mode walking ──
2320
2321    #[test]
2322    fn chromatic_walking_is_semitones() {
2323        assert_eq!(Mode::Chromatic.walk(60, 0, 1), 61);
2324        assert_eq!(Mode::Chromatic.walk(60, 0, -1), 59);
2325        assert_eq!(Mode::Chromatic.walk(0, 0, -1), 0, "the bottom of the range holds");
2326        assert_eq!(Mode::Chromatic.walk(127, 0, 1), 127);
2327    }
2328
2329    #[test]
2330    fn mode_walking_is_scale_degrees() {
2331        // C major, up the scale and back down through the octave below.
2332        let mut note = 60;
2333        for expected in [62, 64, 65, 67, 69, 71, 72, 74] {
2334            note = Mode::Ionian.walk(note, 0, 1);
2335            assert_eq!(note, expected);
2336        }
2337        let mut note = 60;
2338        for expected in [59, 57, 55, 53, 52, 50, 48] {
2339            note = Mode::Ionian.walk(note, 0, -1);
2340            assert_eq!(note, expected);
2341        }
2342    }
2343
2344    /// A note off the scale — set before the mode was, or borrowed on
2345    /// purpose — snaps onto it on the first press rather than walking off it
2346    /// forever.
2347    #[test]
2348    fn walking_snaps_a_borrowed_note_onto_the_scale() {
2349        assert_eq!(Mode::Ionian.walk(61, 0, 1), 62, "C# up lands on D");
2350        assert_eq!(Mode::Ionian.walk(61, 0, -1), 60, "C# down lands on C");
2351    }
2352
2353    #[test]
2354    fn every_mode_walks_a_full_octave_in_seven_degrees() {
2355        for &mode in &Mode::ALL {
2356            if mode == Mode::Chromatic {
2357                continue;
2358            }
2359            for tonic in 0..12u8 {
2360                let start = 60 + tonic;
2361                let start = mode.walk(start, tonic, 0);
2362                let mut note = start;
2363                for _ in 0..7 {
2364                    note = mode.walk(note, tonic, 1);
2365                }
2366                assert_eq!(note, start + 12, "{mode:?} in {tonic} did not close");
2367            }
2368        }
2369    }
2370
2371    // ── Switch quantization ──
2372
2373    #[test]
2374    fn switch_boundaries_are_the_next_grid_line() {
2375        let pattern = 3840;
2376        assert_eq!(SwitchQuant::Immediate.boundary(1234, pattern), 1234);
2377        assert_eq!(SwitchQuant::Beat.boundary(1234, pattern), 1920);
2378        assert_eq!(SwitchQuant::Bar.boundary(1234, pattern), 3840);
2379        assert_eq!(SwitchQuant::PatternEnd.boundary(1234, pattern), 3840);
2380        assert_eq!(SwitchQuant::PatternEnd.boundary(4000, 2880), 5760);
2381    }
2382
2383    /// A queue made exactly on the boundary takes that boundary, not the
2384    /// next one — otherwise a switch queued on the downbeat waits a whole
2385    /// extra bar.
2386    #[test]
2387    fn a_boundary_already_reached_is_the_answer() {
2388        assert_eq!(SwitchQuant::Bar.boundary(3840, 3840), 3840);
2389        assert_eq!(SwitchQuant::Beat.boundary(960, 3840), 960);
2390        assert_eq!(SwitchQuant::PatternEnd.boundary(0, 3840), 0);
2391    }
2392
2393    // ── The window ──
2394
2395    /// 120 BPM, 44.1 kHz.
2396    const TPS: f64 = 120.0 * 960.0 / (60.0 * 44_100.0);
2397
2398    fn window(position: i64, frames: u32, previous: Option<PlaybackWindow>) -> PlaybackWindow {
2399        PlaybackWindow::for_block(position, frames, TPS, None, previous)
2400    }
2401
2402    #[test]
2403    fn the_first_window_starts_where_the_transport_is() {
2404        let w = window(1000, 512, None);
2405        assert_eq!(w.from(), 1000);
2406        assert!(!w.is_continuous(), "there is nothing for it to continue from");
2407    }
2408
2409    /// The gap the contiguity rule exists to close: a transport that carries
2410    /// the sub-tick remainder advances further than the block measured, and
2411    /// the tick in between belongs to somebody.
2412    #[test]
2413    fn a_window_continues_from_the_last_one_across_a_rounding_gap() {
2414        let first = window(0, 470, None);
2415        let span = first.to();
2416        // The transport landed one tick past where the block measured.
2417        let second = window(span + 1, 470, Some(first));
2418        assert_eq!(second.from(), span, "a tick of song time was skipped");
2419        assert!(second.is_continuous());
2420        assert_eq!(second.to(), span + 1 + span);
2421    }
2422
2423    /// Moving the playhead is not continuous playback, and nothing gets
2424    /// replayed to cover the jump.
2425    #[test]
2426    fn a_jump_breaks_continuity() {
2427        let first = window(0, 512, None);
2428        let jumped = window(100_000, 512, Some(first));
2429        assert_eq!(jumped.from(), 100_000);
2430        assert!(!jumped.is_continuous());
2431    }
2432
2433    /// A loop wrap starts the window at the loop point, so the ticks between
2434    /// the loop point and where the callback arrived are played rather than
2435    /// skipped. This is what clip playback has always done.
2436    #[test]
2437    fn a_loop_wrap_starts_the_window_at_the_loop_point() {
2438        let previous = PlaybackWindow::for_block(3800, 512, TPS, Some((0, 3840)), None);
2439        let wrapped = PlaybackWindow::for_block(3, 512, TPS, Some((0, 3840)), Some(previous));
2440        assert_eq!(wrapped.from(), 0);
2441        assert!(!wrapped.is_continuous());
2442    }
2443
2444    /// The window stops at the loop point. Reaching across it would play the
2445    /// notes on the other side, and then the wrap would play them again.
2446    #[test]
2447    fn a_window_never_reaches_past_the_loop_end() {
2448        let w = PlaybackWindow::for_block(3830, 4096, TPS, Some((0, 3840)), None);
2449        assert_eq!(w.to(), 3840);
2450        assert!(!w.contains(3840));
2451    }
2452
2453    /// The one expression that turns song time into a sample position. Both
2454    /// clips and patterns ask it, which is the whole point.
2455    #[test]
2456    fn sample_offsets_come_from_ticks_and_nothing_else() {
2457        let w = window(1000, 512, None);
2458        assert_eq!(w.sample_offset(1000), 0);
2459        assert_eq!(w.sample_offset(999), 0, "before the window is the first sample");
2460        assert_eq!(w.sample_offset(1000 + 22), (22.0 / TPS) as u32);
2461        assert_eq!(w.sample_offset(i64::MAX), 511, "past the block is the last sample");
2462    }
2463
2464    #[test]
2465    fn a_zero_length_block_has_no_samples_to_land_on() {
2466        let w = window(0, 0, None);
2467        assert_eq!(w.sample_offset(1000), 0);
2468    }
2469
2470    // ── The player ──
2471
2472    fn player_with(slot0: PatternBlock, slot1: PatternBlock) -> PatternPlayer {
2473        let mut player = PatternPlayer::new();
2474        player.apply(1, slot1);
2475        player.apply(0, slot0);
2476        player
2477    }
2478
2479    /// Run a player over consecutive callbacks, the way the mixer does:
2480    /// each window continues from the last, and the transport position is
2481    /// wherever the previous window ended.
2482    fn run_player(
2483        player: &mut PatternPlayer,
2484        start: i64,
2485        frames: u32,
2486        until: i64,
2487    ) -> Vec<PatternEvent> {
2488        let mut out = Vec::new();
2489        let mut position = start;
2490        let mut previous = None;
2491        while position < until {
2492            let w = window(position, frames, previous);
2493            player.render(&w, true, &mut out);
2494            position = w.to();
2495            previous = Some(w);
2496        }
2497        out
2498    }
2499
2500    /// One callback of a running player, and the events it produced.
2501    fn tick_player(
2502        player: &mut PatternPlayer,
2503        position: i64,
2504        frames: u32,
2505        previous: Option<PlaybackWindow>,
2506    ) -> (PlaybackWindow, Vec<PatternEvent>) {
2507        let w = window(position, frames, previous);
2508        let mut out = Vec::new();
2509        player.render(&w, true, &mut out);
2510        (w, out)
2511    }
2512
2513    #[test]
2514    fn a_stopped_transport_produces_nothing_and_then_flushes_once() {
2515        let mut player = player_with(drum_pattern(16), PatternBlock::empty());
2516        let (w, events) = tick_player(&mut player, 0, 512, None);
2517        assert!(!events.is_empty());
2518        assert!(player.held_notes() > 0);
2519
2520        let mut out = Vec::new();
2521        player.render(&w, false, &mut out);
2522        assert_eq!(out.len(), 1, "the sounding note was not turned off");
2523        assert_eq!(out[0].status, 0x80);
2524        assert_eq!(player.held_notes(), 0);
2525
2526        let mut again = Vec::new();
2527        player.render(&w, false, &mut again);
2528        assert!(again.is_empty(), "the flush repeated");
2529    }
2530
2531    /// The switch: at the boundary, everything sounding is turned off, and
2532    /// those offs are pushed before the new pattern's first notes.
2533    #[test]
2534    fn a_pattern_switch_ends_the_old_notes_before_starting_the_new_ones() {
2535        let mut a = drum_pattern(16);
2536        a.lanes[0].steps[15].gate = Step::TIE;
2537        let mut b = drum_pattern(16);
2538        b.lanes[0] = Lane::drum(42);
2539        for step in &mut b.lanes[0].steps {
2540            step.on = true;
2541        }
2542
2543        let mut player = player_with(a, b);
2544        // Queue slot 1 for the end of the pattern.
2545        let mut queue = a;
2546        queue.pending_slot = Some(1);
2547        player.apply(0, queue);
2548        assert_eq!(player.countdown(3600), Some((1, 1)), "one step to go");
2549
2550        // Run into the boundary from before the tied step, so that the note
2551        // the switch has to end is actually sounding when it arrives.
2552        let out = run_player(&mut player, 3500, 512, 3900);
2553
2554        let at_boundary: Vec<(u8, u8)> = out
2555            .iter()
2556            .filter(|e| e.tick == 3840)
2557            .map(|e| (e.status, e.data1))
2558            .collect();
2559        assert_eq!(
2560            at_boundary,
2561            vec![(0x80, 36), (0x90, 42)],
2562            "the old note has to be ended before the new one starts"
2563        );
2564        assert_eq!(player.live_slot(), 1);
2565        assert_eq!(player.queued_slot(), None);
2566    }
2567
2568    /// An immediate switch happens at the top of the next callback, not one
2569    /// tick into it: the boundary is the cursor itself, and the first note of
2570    /// the new pattern is the first note of the block.
2571    #[test]
2572    fn an_immediate_switch_takes_effect_at_the_start_of_the_block() {
2573        let a = drum_pattern(16);
2574        let mut b = drum_pattern(16);
2575        b.lanes[0] = Lane::drum(42);
2576        for step in &mut b.lanes[0].steps {
2577            step.on = true;
2578        }
2579
2580        let mut player = player_with(a, b);
2581        let mut queued = a;
2582        queued.pending_slot = Some(1);
2583        queued.switch_quant = SwitchQuant::Immediate;
2584        player.apply(0, queued);
2585
2586        // A block starting exactly on a step, so the switch and an onset land
2587        // on the same tick.
2588        let w = window(480, 512, None);
2589        let mut out = Vec::new();
2590        player.render(&w, true, &mut out);
2591        assert_eq!(player.live_slot(), 1);
2592        let first = out.iter().find(|e| e.is_note_on()).expect("a note");
2593        assert_eq!(first.data1, 42, "the old pattern played after an immediate switch");
2594        assert_eq!(first.tick, 480);
2595    }
2596
2597    /// A switch quantized to the beat lands on the beat, in the middle of the
2598    /// callback that contains it, not at either end of it.
2599    #[test]
2600    fn a_beat_quantized_switch_splits_the_block_at_the_beat() {
2601        let a = drum_pattern(16);
2602        let mut b = drum_pattern(16);
2603        b.lanes[0] = Lane::drum(42);
2604        for step in &mut b.lanes[0].steps {
2605            step.on = true;
2606        }
2607
2608        let mut player = player_with(a, b);
2609        let mut queued = a;
2610        queued.pending_slot = Some(1);
2611        queued.switch_quant = SwitchQuant::Beat;
2612        player.apply(0, queued);
2613
2614        let out = run_player(&mut player, 700, 512, 1100);
2615        let switched: Vec<(i64, u8)> = out
2616            .iter()
2617            .filter(|e| e.is_note_on())
2618            .map(|e| (e.tick, e.data1))
2619            .collect();
2620        assert_eq!(
2621            switched,
2622            vec![(720, 36), (960, 42)],
2623            "the switch did not land on the beat"
2624        );
2625        assert_eq!(player.live_slot(), 1);
2626    }
2627
2628    /// Queueing the slot that is already playing is not a switch — which is
2629    /// what makes a stale queue on the UI side harmless rather than a note
2630    /// cut nobody asked for.
2631    #[test]
2632    fn queueing_the_live_slot_does_nothing() {
2633        let mut block = drum_pattern(16);
2634        block.pending_slot = Some(0);
2635        let player = player_with(block, PatternBlock::empty());
2636        assert_eq!(player.queued_slot(), None);
2637        assert_eq!(player.countdown(0), None);
2638    }
2639
2640    /// A chain is a program the position indexes into, not a cursor: dropping
2641    /// the playhead into the middle of one lands in the entry that belongs
2642    /// there, exactly as a step does.
2643    #[test]
2644    fn a_chain_is_derived_from_the_position() {
2645        let a = drum_pattern(16);
2646        let mut b = drum_pattern(16);
2647        b.lanes[0] = Lane::drum(42);
2648        for step in &mut b.lanes[0].steps {
2649            step.on = true;
2650        }
2651
2652        let mut chained = a;
2653        chained.chain[0] = ChainEntry { slot: 0, repeats: 2 };
2654        chained.chain[1] = ChainEntry { slot: 1, repeats: 1 };
2655        chained.chain_len = 2;
2656
2657        let mut player = PatternPlayer::new();
2658        player.apply(1, b);
2659        player.apply(0, chained);
2660
2661        let cycle = 3840;
2662        // Two times through A, then one of B, then round again.
2663        for (position, expected) in [
2664            (0, 36),
2665            (cycle, 36),
2666            (cycle * 2, 42),
2667            (cycle * 3, 36),
2668            (cycle * 5, 42),
2669        ] {
2670            let mut out = Vec::new();
2671            let w = window(position, 512, None);
2672            player.render(&w, true, &mut out);
2673            let first = out.iter().find(|e| e.is_note_on()).expect("a note");
2674            assert_eq!(first.data1, expected, "wrong chain entry at tick {position}");
2675        }
2676    }
2677
2678    /// The chain boundary is a switch like any other: notes sounding across
2679    /// it are ended at it.
2680    #[test]
2681    fn a_chain_advance_ends_the_notes_it_replaces() {
2682        let mut a = drum_pattern(16);
2683        a.lanes[0].steps[15].gate = Step::TIE;
2684        let mut b = drum_pattern(16);
2685        b.lanes[0] = Lane::drum(42);
2686        b.lanes[0].steps[0].on = true;
2687
2688        let mut chained = a;
2689        chained.chain[0] = ChainEntry { slot: 0, repeats: 1 };
2690        chained.chain[1] = ChainEntry { slot: 1, repeats: 1 };
2691        chained.chain_len = 2;
2692
2693        let mut player = PatternPlayer::new();
2694        player.apply(1, b);
2695        player.apply(0, chained);
2696
2697        let out = run_player(&mut player, 3500, 512, 3900);
2698        let at_boundary: Vec<(u8, u8)> = out
2699            .iter()
2700            .filter(|e| e.tick == 3840)
2701            .map(|e| (e.status, e.data1))
2702            .collect();
2703        assert_eq!(at_boundary, vec![(0x80, 36), (0x90, 42)]);
2704    }
2705
2706    // ── Bounce ──
2707
2708    /// The bounce is not "the same as" live playback: it is live playback,
2709    /// run with a different sink. Anything that changed one and not the other
2710    /// would show up here as a tick that does not match.
2711    #[test]
2712    fn a_bounced_cycle_is_tick_identical_to_live_playback() {
2713        for swing in [50u8, 58, 62, 75] {
2714            for rate in Rate::ALL {
2715                let mut block = drum_pattern(16);
2716                block.rate = rate;
2717                block.swing = swing;
2718                block.lanes[0].steps[3].gate = 150;
2719                block.lanes[0].steps[7].gate = Step::TIE;
2720                block.lanes[0].steps[9].accent = true;
2721
2722                let mut bounced = Vec::new();
2723                compile_cycle(&block, 0, &mut bounced);
2724
2725                // ...and the same pattern played a block at a time.
2726                let cycle = block.length_ticks();
2727                let mut live = Vec::new();
2728                let mut pending = PendingOffs::new();
2729                let mut from = 0;
2730                while from < cycle {
2731                    let to = (from + 97).min(cycle);
2732                    generate(&block, 0, from, to, &mut pending, &mut live);
2733                    from = to;
2734                }
2735                pending.flush(cycle, &mut live);
2736                live.sort_by_key(|e| e.tick);
2737
2738                let key = |e: &PatternEvent| (e.tick, e.status, e.data1, e.data2);
2739                let bounced: Vec<_> = bounced.iter().map(key).collect();
2740                let live: Vec<_> = live.iter().map(key).collect();
2741                assert_eq!(bounced, live, "swing {swing} at {}", rate.label());
2742            }
2743        }
2744    }
2745
2746    // ── Allocation ──
2747
2748    /// The rule the audio thread lives by. Rendering a pattern, switching
2749    /// one, advancing a chain and taking a new block are all writes into
2750    /// memory that already exists.
2751    #[test]
2752    fn rendering_a_pattern_does_not_allocate() {
2753        let mut a = drum_pattern(16);
2754        a.lanes[0].steps[15].gate = Step::TIE;
2755        let mut b = drum_pattern(16);
2756        b.lanes[0] = Lane::drum(42);
2757
2758        let mut player = Box::new(player_with(a, b));
2759        let mut sink = Vec::with_capacity(1024);
2760        let mut queued = a;
2761        queued.pending_slot = Some(1);
2762
2763        // One warm-up callback outside the measurement.
2764        let mut w = window(0, 512, None);
2765        player.render(&w, true, &mut sink);
2766
2767        let allocations = crate::alloc_count::allocations_during(|| {
2768            let mut position = 0;
2769            for block in 0..64 {
2770                w = window(position, 512, Some(w));
2771                sink.clear();
2772                player.render(&w, true, &mut sink);
2773                if block == 8 {
2774                    player.apply(0, queued);
2775                }
2776                position = w.to();
2777            }
2778        });
2779        assert_eq!(allocations, 0, "the pattern player reached the allocator");
2780    }
2781
2782    /// A sink that is out of room stops the generator rather than losing
2783    /// events out of the middle of a step — half a chord with no offs is a
2784    /// stuck voice, and a missing note is not.
2785    #[test]
2786    fn a_full_sink_stops_the_generator() {
2787        struct Capped(Vec<PatternEvent>, usize);
2788        impl EventSink for Capped {
2789            fn accept(&mut self, event: PatternEvent) -> bool {
2790                if self.0.len() >= self.1 {
2791                    return false;
2792                }
2793                self.0.push(event);
2794                true
2795            }
2796        }
2797
2798        let block = drum_pattern(16);
2799        let mut sink = Capped(Vec::new(), 3);
2800        let mut pending = PendingOffs::new();
2801        generate(&block, 0, 0, block.length_ticks(), &mut pending, &mut sink);
2802        assert_eq!(sink.0.len(), 3, "the sink was written past its cap");
2803    }
2804}
2805