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, not running.
960    #[must_use]
961    pub const fn empty() -> Self {
962        Self {
963            steps: 16,
964            rate: Rate::Sixteenth,
965            swing: Self::MIN_SWING,
966            base_vel: 100,
967            accent_vel: 127,
968            default_gate: 50,
969            mode: Mode::Chromatic,
970            tonic: 0,
971            lanes: [Lane::empty(); LANES],
972            playing: false,
973            pending_slot: None,
974            switch_quant: SwitchQuant::PatternEnd,
975            chain: [ChainEntry { slot: 0, repeats: 1 }; MAX_CHAIN],
976            chain_len: 0,
977        }
978    }
979
980    /// How many steps actually play. Always at least one, never more than
981    /// [`MAX_STEPS`] — a length out of range would otherwise index off the
982    /// end of a lane or divide by zero.
983    #[must_use]
984    pub fn step_count(&self) -> usize {
985        (self.steps as usize).clamp(1, MAX_STEPS)
986    }
987
988    #[must_use]
989    pub fn ticks_per_step(&self) -> i64 {
990        self.rate.ticks()
991    }
992
993    /// One time through, in ticks.
994    #[must_use]
995    pub fn length_ticks(&self) -> i64 {
996        self.ticks_per_step() * self.step_count() as i64
997    }
998
999    /// How far an odd-numbered step is pushed late, in ticks.
1000    ///
1001    /// MPC-style: the offset is a fraction of *two* steps, so 75% puts the
1002    /// off-beat three quarters of the way through the pair — a triplet feel —
1003    /// and 50% is straight. Integer arithmetic on purpose: the bounce and the
1004    /// live player run this same expression, so "the bounce swings
1005    /// identically" needs no tolerance at all.
1006    ///
1007    /// Even step indices are never offset, which is what keeps the downbeat
1008    /// where the transport says it is.
1009    #[must_use]
1010    pub fn swing_offset(&self, step_index: usize) -> i64 {
1011        if step_index % 2 == 0 {
1012            return 0;
1013        }
1014        let swing = i64::from(self.swing.clamp(Self::MIN_SWING, Self::MAX_SWING));
1015        (swing - i64::from(Self::MIN_SWING)) * 2 * self.ticks_per_step() / 100
1016    }
1017
1018    /// The largest [`PatternBlock::swing_offset`] this pattern can produce —
1019    /// how far back a scan has to start to be sure of catching every onset.
1020    fn max_swing_offset(&self) -> i64 {
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 tick step `index` fires at, counting from `origin`.
1026    ///
1027    /// `index` is a *global* step number and may run past the end of the
1028    /// pattern or before its start: index 17 of a 16-step pattern is step 1
1029    /// of the second time through.
1030    #[must_use]
1031    pub fn onset(&self, origin: i64, index: i64) -> i64 {
1032        let steps = self.step_count() as i64;
1033        let in_pattern = index.rem_euclid(steps) as usize;
1034        origin + index * self.ticks_per_step() + self.swing_offset(in_pattern)
1035    }
1036
1037    /// Which step is under `tick`, counting from `origin`. Swing is not
1038    /// applied: this is where the playhead is, not when a note fires.
1039    #[must_use]
1040    pub fn step_at(&self, origin: i64, tick: i64) -> usize {
1041        let steps = self.step_count() as i64;
1042        (tick - origin).div_euclid(self.ticks_per_step()).rem_euclid(steps) as usize
1043    }
1044
1045    /// Whether a lane sounds, given the pattern's mute and solo state.
1046    #[must_use]
1047    pub fn lane_audible(&self, lane: usize) -> bool {
1048        let Some(l) = self.lanes.get(lane) else { return false };
1049        if l.muted {
1050            return false;
1051        }
1052        let any_solo = self.lanes.iter().any(|l| l.soloed);
1053        !any_solo || l.soloed
1054    }
1055
1056    /// The chain as real entries, ignoring anything past `chain_len`.
1057    #[must_use]
1058    pub fn chain_entries(&self) -> &[ChainEntry] {
1059        &self.chain[..(self.chain_len as usize).min(MAX_CHAIN)]
1060    }
1061}
1062
1063impl Default for PatternBlock {
1064    fn default() -> Self {
1065        Self::empty()
1066    }
1067}
1068
1069// ── Events ──
1070
1071/// One MIDI event a pattern produced, stamped with the absolute song tick it
1072/// happens at.
1073///
1074/// A tick rather than a sample offset because this module has no sample rate:
1075/// [`PlaybackWindow::sample_offset`] is where a tick becomes a position in a
1076/// buffer, and the bounce never asks that question at all.
1077#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1078pub struct PatternEvent {
1079    pub tick: i64,
1080    pub status: u8,
1081    pub data1: u8,
1082    pub data2: u8,
1083}
1084
1085impl PatternEvent {
1086    #[must_use]
1087    pub const fn note_on(tick: i64, note: u8, velocity: u8) -> Self {
1088        Self { tick, status: 0x90, data1: note, data2: velocity }
1089    }
1090
1091    #[must_use]
1092    pub const fn note_off(tick: i64, note: u8) -> Self {
1093        Self { tick, status: 0x80, data1: note, data2: 0 }
1094    }
1095
1096    #[must_use]
1097    pub const fn is_note_on(&self) -> bool {
1098        self.status == 0x90 && self.data2 > 0
1099    }
1100}
1101
1102/// Somewhere for generated events to go.
1103///
1104/// The whole point of the trait is that there is one generator behind both
1105/// consumers: the audio thread writes straight into the track's plugin queue,
1106/// converting ticks to sample offsets as they arrive and refusing to grow it,
1107/// and the bounce writes into a `Vec` that cannot overflow. Generic rather
1108/// than `dyn`, so each call site compiles to the same code it would if the
1109/// sink were named directly.
1110///
1111/// Events arrive in no particular tick order. Both consumers sort by tick or
1112/// by sample offset with a *stable* sort, which is what carries the one
1113/// ordering rule that matters: a note-off pushed before a note-on at the same
1114/// tick stays before it, and so a switch boundary cannot cut the note it just
1115/// started.
1116pub trait EventSink {
1117    /// Returns whether the event was taken. `false` means the sink is full,
1118    /// and the generator stops rather than dropping events silently in the
1119    /// middle of a step.
1120    fn accept(&mut self, event: PatternEvent) -> bool;
1121}
1122
1123impl EventSink for Vec<PatternEvent> {
1124    fn accept(&mut self, event: PatternEvent) -> bool {
1125        self.push(event);
1126        true
1127    }
1128}
1129
1130// ── The playback window ──
1131
1132/// The span of song time one callback renders.
1133///
1134/// **This is the sync guarantee.** Clip playback and pattern playback in
1135/// `mixer.rs` take the same value and ask it the same question, so a clip
1136/// note and a pattern step on the same beat cannot land on different samples:
1137/// there is only one expression that turns a tick into a sample offset, and
1138/// only one that decides where the window starts and ends.
1139///
1140/// Windows are half-open and contiguous. The next window begins exactly where
1141/// this one ended, rather than at the transport's new position, because those
1142/// are not always the same number: a block is almost never a whole number of
1143/// ticks, and a transport that carries the remainder can advance 179 ticks
1144/// where the block measured 178. Starting the next window at the position
1145/// would leave a one-tick hole in song time, and an onset that fell in it
1146/// would never play. Starting it where the last one ended cannot.
1147#[derive(Debug, Clone, Copy)]
1148pub struct PlaybackWindow {
1149    from: i64,
1150    to: i64,
1151    /// Where the transport actually was when this callback began, which is
1152    /// not always `from`: see [`PlaybackWindow::for_block`]. Kept because it
1153    /// is the only honest thing to compare the *next* block's position
1154    /// against when deciding whether the playhead moved.
1155    position: i64,
1156    ticks_per_sample: f64,
1157    frames: u32,
1158    continuous: bool,
1159}
1160
1161impl PlaybackWindow {
1162    /// The largest gap between one window's end and the next block's position
1163    /// that still counts as continuous playback.
1164    ///
1165    /// One tick, and it is not a fudge factor: the block length in ticks is
1166    /// truncated and the transport's advance is not, so consecutive positions
1167    /// can run at most one tick ahead of the measured window. Anything larger
1168    /// is the playhead being moved, which is a discontinuity — pending notes
1169    /// get flushed and nothing is replayed.
1170    pub const MAX_TICK_GAP: i64 = 1;
1171
1172    /// The window for one callback.
1173    ///
1174    /// `loop_region` is the loop's `(start, end)` when the transport is
1175    /// looping, and `None` when it is not — one argument rather than a flag
1176    /// and two numbers, because "looping over nowhere" is not a state that
1177    /// should be spellable.
1178    ///
1179    /// `previous` is the window the last callback used, if playback has been
1180    /// running. Two things come from the loop region:
1181    ///
1182    /// * A wrap — the transport moving backwards — starts the window at the
1183    ///   loop point, so the ticks between it and the position the callback
1184    ///   arrived at are played rather than skipped. That is what clip
1185    ///   playback has always done.
1186    /// * The window never extends past the loop end. Without that, the last
1187    ///   callback of a loop would reach across the loop point and play the
1188    ///   first notes on the other side of it, and then the wrap would play
1189    ///   them again: one doubled downbeat per time round.
1190    #[must_use]
1191    pub fn for_block(
1192        position: i64,
1193        frames: u32,
1194        ticks_per_sample: f64,
1195        loop_region: Option<(i64, i64)>,
1196        previous: Option<Self>,
1197    ) -> Self {
1198        let span = (f64::from(frames) * ticks_per_sample) as i64;
1199
1200        let (from, continuous) = match (previous, loop_region) {
1201            (Some(prev), Some((loop_start, _))) if position < prev.position => (loop_start, false),
1202            (Some(prev), _) if prev.to <= position && position - prev.to <= Self::MAX_TICK_GAP => {
1203                (prev.to, true)
1204            }
1205            _ => (position, false),
1206        };
1207
1208        let mut to = position + span;
1209        if let Some((_, loop_end)) = loop_region {
1210            if loop_end > from {
1211                to = to.min(loop_end);
1212            }
1213        }
1214
1215        Self {
1216            from,
1217            to: to.max(from),
1218            position,
1219            ticks_per_sample,
1220            frames,
1221            continuous,
1222        }
1223    }
1224
1225    /// A window over part of this one, for splitting a callback at a pattern
1226    /// switch. Sample offsets are unchanged: they are measured from the
1227    /// original start of the block, not from the piece.
1228    #[must_use]
1229    pub fn narrowed(&self, from: i64, to: i64) -> Self {
1230        Self { from, to: to.max(from), ..*self }
1231    }
1232
1233    #[must_use]
1234    pub const fn from(&self) -> i64 {
1235        self.from
1236    }
1237
1238    #[must_use]
1239    pub const fn to(&self) -> i64 {
1240        self.to
1241    }
1242
1243    /// Whether this window carries on from the previous one. `false` after a
1244    /// jump, a loop wrap, or the first block of playback — every case where
1245    /// notes still sounding have to be turned off.
1246    #[must_use]
1247    pub const fn is_continuous(&self) -> bool {
1248        self.continuous
1249    }
1250
1251    #[must_use]
1252    pub const fn is_empty(&self) -> bool {
1253        self.to <= self.from
1254    }
1255
1256    #[must_use]
1257    pub const fn contains(&self, tick: i64) -> bool {
1258        tick >= self.from && tick < self.to
1259    }
1260
1261    /// Where in the callback's buffer an event at `tick` belongs.
1262    ///
1263    /// The one expression that turns song time into a sample. A tick before
1264    /// the window lands on the first sample rather than underflowing, and one
1265    /// past the end lands on the last: a note played at the wrong end of a
1266    /// buffer is 1.5 ms out, and a note not played at all is a hole in the
1267    /// part.
1268    #[must_use]
1269    pub fn sample_offset(&self, tick: i64) -> u32 {
1270        let last = self.frames.saturating_sub(1);
1271        let offset = tick - self.from;
1272        if offset <= 0 || self.ticks_per_sample <= 0.0 {
1273            return 0;
1274        }
1275        let samples = (offset as f64 / self.ticks_per_sample) as i64;
1276        u32::try_from(samples).unwrap_or(last).min(last)
1277    }
1278}
1279
1280// ── Pending note-offs ──
1281
1282/// A note that is sounding and the tick it has to stop at.
1283#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1284struct PendingOff {
1285    note: u8,
1286    lane: u8,
1287    /// When the note-off is due, or `None` for a tied note, which is ended
1288    /// by the lane's next onset and by nothing else.
1289    due: Option<i64>,
1290}
1291
1292/// Every note this track is holding, oldest first.
1293///
1294/// Oldest-first is maintained by removing with a shift rather than a swap,
1295/// which is what makes "overflow forces off the oldest" a one-line operation
1296/// on a table of thirty-two. The shift is at most 32 moves of 24 bytes.
1297#[derive(Debug, Clone, Copy)]
1298pub struct PendingOffs {
1299    entries: [PendingOff; MAX_PENDING_OFFS],
1300    len: usize,
1301}
1302
1303impl PendingOffs {
1304    #[must_use]
1305    pub const fn new() -> Self {
1306        Self {
1307            entries: [PendingOff { note: 0, lane: 0, due: None }; MAX_PENDING_OFFS],
1308            len: 0,
1309        }
1310    }
1311
1312    #[must_use]
1313    pub const fn len(&self) -> usize {
1314        self.len
1315    }
1316
1317    #[must_use]
1318    pub const fn is_empty(&self) -> bool {
1319        self.len == 0
1320    }
1321
1322    /// Forget everything without sounding an off. For a panic, where the
1323    /// instruments are being reset underneath us anyway.
1324    pub fn clear(&mut self) {
1325        self.len = 0;
1326    }
1327
1328    fn remove(&mut self, index: usize) -> PendingOff {
1329        let gone = self.entries[index];
1330        for i in index..self.len - 1 {
1331            self.entries[i] = self.entries[i + 1];
1332        }
1333        self.len -= 1;
1334        gone
1335    }
1336
1337    /// Note that `note` is sounding on `lane`.
1338    ///
1339    /// When the table is full the oldest note is turned off at `now` and its
1340    /// slot reused. Never drops the new note: a note with no off in the table
1341    /// is a note nothing will ever stop.
1342    pub fn hold(
1343        &mut self,
1344        lane: usize,
1345        note: u8,
1346        due: Option<i64>,
1347        now: i64,
1348        out: &mut impl EventSink,
1349    ) {
1350        if self.len == MAX_PENDING_OFFS {
1351            let oldest = self.remove(0);
1352            out.accept(PatternEvent::note_off(now, oldest.note));
1353        }
1354        self.entries[self.len] = PendingOff { note, lane: lane as u8, due };
1355        self.len += 1;
1356    }
1357
1358    /// Turn off everything held on `lane`, at `at`.
1359    ///
1360    /// Called immediately before a lane's next onset, which is what ends a
1361    /// tied note and what keeps a gate longer than a step from running over
1362    /// its own next hit.
1363    pub fn end_lane(&mut self, lane: usize, at: i64, out: &mut impl EventSink) {
1364        let lane = lane as u8;
1365        let mut i = 0;
1366        while i < self.len {
1367            if self.entries[i].lane == lane {
1368                let gone = self.remove(i);
1369                out.accept(PatternEvent::note_off(at, gone.note));
1370            } else {
1371                i += 1;
1372            }
1373        }
1374    }
1375
1376    /// Turn off everything whose off is due before `tick`, each at its own
1377    /// due tick.
1378    pub fn emit_due_before(&mut self, tick: i64, out: &mut impl EventSink) {
1379        let mut i = 0;
1380        while i < self.len {
1381            match self.entries[i].due {
1382                Some(due) if due < tick => {
1383                    let gone = self.remove(i);
1384                    out.accept(PatternEvent::note_off(due, gone.note));
1385                }
1386                _ => i += 1,
1387            }
1388        }
1389    }
1390
1391    /// Turn off everything, at `at`. Stop, pause, a position jump, a loop
1392    /// wrap, a pattern switch: every discontinuity ends every note.
1393    pub fn flush(&mut self, at: i64, out: &mut impl EventSink) {
1394        for i in 0..self.len {
1395            out.accept(PatternEvent::note_off(at, self.entries[i].note));
1396        }
1397        self.len = 0;
1398    }
1399}
1400
1401impl Default for PendingOffs {
1402    fn default() -> Self {
1403        Self::new()
1404    }
1405}
1406
1407// ── Generation ──
1408
1409/// Write every event one pattern produces between `from` and `to`.
1410///
1411/// Pure apart from `pending`, which is the caller's note-off table: the same
1412/// call with the same table produces the same events, which is why the bounce
1413/// can run it once over a whole cycle and get what the audio thread produces
1414/// over hundreds of callbacks.
1415///
1416/// `origin` is the tick the pattern's step 0 is anchored to — 0 for a pattern
1417/// playing on its own, and the start of the chain entry when a chain is
1418/// running. Nothing here keeps a cursor: which step fires is derived from the
1419/// tick, every time.
1420pub fn generate(
1421    block: &PatternBlock,
1422    origin: i64,
1423    from: i64,
1424    to: i64,
1425    pending: &mut PendingOffs,
1426    out: &mut impl EventSink,
1427) {
1428    if to <= from {
1429        return;
1430    }
1431    let tps = block.ticks_per_step();
1432    let steps = block.step_count() as i64;
1433
1434    // Which global step indices could have an onset inside the window. Swing
1435    // only ever pushes a step *later*, so the scan starts one full swing
1436    // offset early and every candidate is checked against the window anyway.
1437    let first = (from - origin - block.max_swing_offset()).div_euclid(tps);
1438    let last = (to - origin).div_euclid(tps) + 1;
1439    let last = last.min(first + MAX_STEP_SCAN);
1440
1441    let mut chord = [0u8; MAX_CHORD_NOTES];
1442    for index in first..last {
1443        let onset = block.onset(origin, index);
1444        if onset < from || onset >= to {
1445            continue;
1446        }
1447        // Everything that was already due before this onset goes first, so a
1448        // lane that re-triggers cannot have its new note cut by the old one's
1449        // off arriving afterwards.
1450        pending.emit_due_before(onset, out);
1451
1452        let step_index = index.rem_euclid(steps) as usize;
1453        for lane_index in 0..LANES {
1454            if !block.lane_audible(lane_index) {
1455                continue;
1456            }
1457            let lane = &block.lanes[lane_index];
1458            let step = lane.steps[step_index];
1459            if !step.on {
1460                continue;
1461            }
1462
1463            // The lane's previous note ends here, before the new one starts.
1464            // Insertion order is what carries that through the sort.
1465            pending.end_lane(lane_index, onset, out);
1466
1467            let velocity = if step.accent { block.accent_vel } else { block.base_vel };
1468            let velocity = velocity.clamp(1, 127);
1469            let due = step.gate_ticks(tps).map(|len| onset + len);
1470
1471            let count = if lane.is_pitched() {
1472                chord_notes(
1473                    step.root(),
1474                    step.chord_kind(),
1475                    step.voicing_kind(),
1476                    step.root_below(),
1477                    block.mode,
1478                    block.tonic,
1479                    &mut chord,
1480                )
1481            } else {
1482                chord[0] = lane.note;
1483                1
1484            };
1485
1486            for &note in &chord[..count] {
1487                if !out.accept(PatternEvent::note_on(onset, note, velocity)) {
1488                    return;
1489                }
1490                pending.hold(lane_index, note, due, onset, out);
1491            }
1492        }
1493    }
1494
1495    pending.emit_due_before(to, out);
1496}
1497
1498/// Compile one time through a pattern, as note events from tick zero.
1499///
1500/// The bounce. It calls [`generate`] over the whole cycle in one window
1501/// rather than reimplementing it, so swing, gates, ties and accents are not
1502/// "the same as" live playback — they are live playback, run with a different
1503/// sink. Notes still sounding at the end of the cycle are turned off at the
1504/// cycle's last tick, which is where the pattern would have ended them had it
1505/// stopped there.
1506pub fn compile_cycle(block: &PatternBlock, origin: i64, out: &mut Vec<PatternEvent>) {
1507    let length = block.length_ticks();
1508    let mut pending = PendingOffs::new();
1509    generate(block, origin, origin, origin + length, &mut pending, out);
1510    pending.flush(origin + length, out);
1511    out.sort_by_key(|e| e.tick);
1512}
1513
1514// ── The player ──
1515
1516/// Everything one sequencer track needs on the audio thread.
1517///
1518/// The bank lives here rather than on the UI side because a pattern switch
1519/// has to be *decided* on the audio thread: the quantization point is a tick,
1520/// the tick arrives in the middle of a callback, and asking the UI what to
1521/// play next at that moment would make the answer depend on when the UI
1522/// thread happened to be scheduled. With all eight slots resident, a switch
1523/// is an index change and a chain is a lookup.
1524///
1525/// Around 19 kB per sequencer track, allocated once, when the track's first
1526/// pattern arrives — the same shape as an instrument allocating its voice
1527/// array in `Plugin::init`. Nothing after that reaches the allocator.
1528#[derive(Debug, Clone, Copy)]
1529pub struct PatternPlayer {
1530    slots: [PatternBlock; SLOTS],
1531    /// The slot currently sounding.
1532    live: u8,
1533    /// The last word the UI thread said about the track, taken from whichever
1534    /// block arrived most recently. See [`PatternBlock`].
1535    playing: bool,
1536    pending_slot: Option<u8>,
1537    switch_quant: SwitchQuant,
1538    chain: [ChainEntry; MAX_CHAIN],
1539    chain_len: u8,
1540    /// Notes this track is holding.
1541    pending: PendingOffs,
1542    /// Whether the last callback was producing notes, so that stopping can
1543    /// flush exactly once.
1544    active: bool,
1545    /// The step the playhead was over when this player last ran, for the UI.
1546    step: u8,
1547}
1548
1549impl PatternPlayer {
1550    #[must_use]
1551    pub fn new() -> Self {
1552        Self {
1553            slots: [PatternBlock::empty(); SLOTS],
1554            live: 0,
1555            playing: false,
1556            pending_slot: None,
1557            switch_quant: SwitchQuant::PatternEnd,
1558            chain: [ChainEntry { slot: 0, repeats: 1 }; MAX_CHAIN],
1559            chain_len: 0,
1560            pending: PendingOffs::new(),
1561            active: false,
1562            step: 0,
1563        }
1564    }
1565
1566    /// Take a pattern for one slot, and with it the UI's current word on the
1567    /// track-level settings.
1568    pub fn apply(&mut self, slot: u8, block: PatternBlock) {
1569        let slot = (slot as usize).min(SLOTS - 1);
1570        self.slots[slot] = block;
1571        self.playing = block.playing;
1572        self.switch_quant = block.switch_quant;
1573        self.chain = block.chain;
1574        self.chain_len = block.chain_len;
1575        // Queueing the slot that is already live is not a switch, so a UI
1576        // mirror that still names a slot the audio thread has already
1577        // switched to cannot cause a second, silent switch. A running chain
1578        // owns the slot outright, so a queue against one is not held.
1579        self.pending_slot = block
1580            .pending_slot
1581            .filter(|&s| s != self.live && block.chain_len == 0);
1582    }
1583
1584    #[must_use]
1585    pub fn slot(&self, index: usize) -> &PatternBlock {
1586        &self.slots[index.min(SLOTS - 1)]
1587    }
1588
1589    #[must_use]
1590    pub fn live_slot(&self) -> u8 {
1591        self.live
1592    }
1593
1594    #[must_use]
1595    pub fn queued_slot(&self) -> Option<u8> {
1596        self.pending_slot
1597    }
1598
1599    /// The step the playhead was over on the last callback. What the UI draws
1600    /// its marker at.
1601    #[must_use]
1602    pub fn current_step(&self) -> u8 {
1603        self.step
1604    }
1605
1606    #[must_use]
1607    pub fn is_playing(&self) -> bool {
1608        self.playing
1609    }
1610
1611    #[must_use]
1612    pub fn held_notes(&self) -> usize {
1613        self.pending.len()
1614    }
1615
1616    /// Forget every held note without sounding an off. For a panic, where the
1617    /// instruments are reset underneath us.
1618    pub fn silence(&mut self) {
1619        self.pending.clear();
1620        self.active = false;
1621    }
1622
1623    /// Where the switch queued on this track will happen, and how many steps
1624    /// away that is from `now`. `None` when nothing is queued.
1625    ///
1626    /// Pure, and the UI computes the same answer from its own mirror: the
1627    /// countdown on screen is arithmetic, not a message from the audio
1628    /// thread that may or may not have arrived yet.
1629    #[must_use]
1630    pub fn countdown(&self, now: i64) -> Option<(u8, i64)> {
1631        let slot = self.pending_slot?;
1632        let block = &self.slots[self.live as usize];
1633        let at = self.switch_quant.boundary(now, block.length_ticks());
1634        Some((slot, (at - now).div_euclid(block.ticks_per_step())))
1635    }
1636
1637    /// Which slot plays at `tick`, where its step 0 is anchored, and the tick
1638    /// that answer stops being true at.
1639    ///
1640    /// A chain is read straight off the song position, exactly as a step is:
1641    /// the chain is a program the position indexes into rather than a cursor
1642    /// something advances, so dropping the playhead into bar 40 lands in the
1643    /// entry that belongs there. Without a chain the live slot is whatever
1644    /// was last selected, and the boundary is the queued switch, if any.
1645    fn locate(&self, tick: i64) -> (u8, i64, i64) {
1646        if let Some(found) = self.chain_at(tick) {
1647            return found;
1648        }
1649        let boundary = match self.pending_slot {
1650            Some(_) => {
1651                let block = &self.slots[self.live as usize];
1652                self.switch_quant.boundary(tick, block.length_ticks())
1653            }
1654            None => i64::MAX,
1655        };
1656        (self.live, 0, boundary)
1657    }
1658
1659    /// The chain entry covering `tick`, if a chain is running.
1660    fn chain_at(&self, tick: i64) -> Option<(u8, i64, i64)> {
1661        let entries = &self.chain[..(self.chain_len as usize).min(MAX_CHAIN)];
1662        if entries.is_empty() {
1663            return None;
1664        }
1665        let mut total = 0i64;
1666        for entry in entries {
1667            let slot = (entry.slot as usize).min(SLOTS - 1);
1668            total += i64::from(entry.repeats.max(1)) * self.slots[slot].length_ticks();
1669        }
1670        if total <= 0 {
1671            return None;
1672        }
1673
1674        let base = tick.div_euclid(total) * total;
1675        let mut offset = tick.rem_euclid(total);
1676        let mut start = base;
1677        for entry in entries {
1678            let slot = (entry.slot as usize).min(SLOTS - 1);
1679            let span = i64::from(entry.repeats.max(1)) * self.slots[slot].length_ticks();
1680            if offset < span {
1681                return Some((slot as u8, start, start + span));
1682            }
1683            offset -= span;
1684            start += span;
1685        }
1686        None
1687    }
1688
1689    /// Produce this callback's events.
1690    ///
1691    /// `transport_playing` is the DAW transport; the pattern also has to be
1692    /// running on its own account. Everything else — which step, which slot,
1693    /// where the switch lands — comes out of the window's ticks.
1694    pub fn render(
1695        &mut self,
1696        window: &PlaybackWindow,
1697        transport_playing: bool,
1698        out: &mut impl EventSink,
1699    ) {
1700        if !transport_playing || !self.playing {
1701            if self.active {
1702                self.pending.flush(window.from(), out);
1703                self.active = false;
1704            }
1705            return;
1706        }
1707
1708        // A jump, a loop wrap, or the first block after starting: nothing
1709        // that was sounding belongs to where we are now.
1710        if !window.is_continuous() && self.active {
1711            self.pending.flush(window.from(), out);
1712        }
1713        self.active = true;
1714
1715        let mut cursor = window.from();
1716        for _ in 0..MAX_SEGMENTS {
1717            if cursor >= window.to() {
1718                break;
1719            }
1720            let (slot, origin, boundary) = self.locate(cursor);
1721
1722            // A switch that is due at the cursor itself — an immediate one,
1723            // or a pattern end that this callback happens to start on — takes
1724            // effect before anything is generated, so the first note of the
1725            // new pattern is the first note of the segment.
1726            if boundary <= cursor {
1727                self.pending.flush(cursor, out);
1728                self.switch_at(cursor);
1729                continue;
1730            }
1731
1732            self.live = slot;
1733            let end = boundary.min(window.to());
1734            let block = self.slots[slot as usize];
1735            generate(&block, origin, cursor, end, &mut self.pending, out);
1736
1737            // A boundary exactly at the end of the window belongs to the next
1738            // callback, which starts there: taking it here would put its
1739            // note-offs a whole block early.
1740            if boundary < window.to() {
1741                self.pending.flush(boundary, out);
1742                self.switch_at(boundary);
1743            }
1744            cursor = end;
1745        }
1746
1747        let block = &self.slots[self.live as usize];
1748        let origin = self.chain_at(window.from()).map_or(0, |(_, start, _)| start);
1749        self.step = block.step_at(origin, window.from()) as u8;
1750    }
1751
1752    /// Take the queued slot at a boundary that has just passed.
1753    ///
1754    /// A chain moves on by itself — its slot is a function of the position —
1755    /// so a chained track has nothing to take here.
1756    fn switch_at(&mut self, boundary: i64) {
1757        if self.chain_at(boundary).is_some() {
1758            return;
1759        }
1760        if let Some(slot) = self.pending_slot.take() {
1761            self.live = (slot as usize).min(SLOTS - 1) as u8;
1762        }
1763    }
1764}
1765
1766impl Default for PatternPlayer {
1767    fn default() -> Self {
1768        Self::new()
1769    }
1770}
1771
1772#[cfg(test)]
1773mod tests {
1774    use super::*;
1775
1776    // ── Fixtures ──
1777
1778    /// A pattern with one drum lane, every step on, at a sixteenth.
1779    fn drum_pattern(steps: u8) -> PatternBlock {
1780        let mut block = PatternBlock::empty();
1781        block.steps = steps;
1782        block.playing = true;
1783        block.lanes[0] = Lane::drum(36);
1784        for step in &mut block.lanes[0].steps {
1785            step.on = true;
1786        }
1787        block
1788    }
1789
1790    /// A pattern with one melodic lane; only the steps named are on.
1791    fn melodic_pattern(on: &[usize]) -> PatternBlock {
1792        let mut block = PatternBlock::empty();
1793        block.playing = true;
1794        for &index in on {
1795            block.lanes[0].steps[index].on = true;
1796        }
1797        block
1798    }
1799
1800    fn onsets(events: &[PatternEvent]) -> Vec<i64> {
1801        events.iter().filter(|e| e.is_note_on()).map(|e| e.tick).collect()
1802    }
1803
1804    fn run(block: &PatternBlock, from: i64, to: i64) -> Vec<PatternEvent> {
1805        let mut out = Vec::new();
1806        let mut pending = PendingOffs::new();
1807        generate(block, 0, from, to, &mut pending, &mut out);
1808        out
1809    }
1810
1811    // ── Sizes ──
1812
1813    /// The block crosses to the audio thread by value, so its size is the
1814    /// cost of every queued `SetPattern`. Pinned here because a field that
1815    /// creeps in is a cost nobody measures at the time.
1816    #[test]
1817    fn the_block_is_the_size_it_is_supposed_to_be() {
1818        assert_eq!(std::mem::size_of::<Step>(), 9);
1819        assert_eq!(std::mem::size_of::<Lane>(), 3 + 32 * 9);
1820        assert_eq!(std::mem::size_of::<PatternBlock>(), PatternBlock::SIZE);
1821        assert_eq!(
1822            PatternBlock::SIZE, 2_373,
1823            "a pattern changed size; every queued SetPattern costs this many bytes"
1824        );
1825        // No padding anywhere: every field is a byte-aligned scalar, which is
1826        // what makes the whole thing a memcpy.
1827        assert_eq!(std::mem::align_of::<PatternBlock>(), 1);
1828    }
1829
1830    // ── Rates and swing ──
1831
1832    /// The rate table at 960 PPQ. Every division is exact, triplets
1833    /// included — which is the reason the project is at 960 and not 480.
1834    #[test]
1835    fn rate_ticks_are_the_960_ppq_table() {
1836        assert_eq!(Rate::Quarter.ticks(), 960);
1837        assert_eq!(Rate::Eighth.ticks(), 480);
1838        assert_eq!(Rate::Sixteenth.ticks(), 240);
1839        assert_eq!(Rate::ThirtySecond.ticks(), 120);
1840        assert_eq!(Rate::EighthTriplet.ticks(), 320);
1841        assert_eq!(Rate::SixteenthTriplet.ticks(), 160);
1842        // Three triplets fill the division they subdivide.
1843        assert_eq!(Rate::EighthTriplet.ticks() * 3, Rate::Quarter.ticks());
1844        assert_eq!(Rate::SixteenthTriplet.ticks() * 3, Rate::Eighth.ticks());
1845    }
1846
1847    #[test]
1848    fn straight_swing_moves_nothing() {
1849        let block = drum_pattern(16);
1850        assert_eq!(block.swing, PatternBlock::MIN_SWING);
1851        for step in 0..16 {
1852            assert_eq!(block.swing_offset(step), 0);
1853        }
1854    }
1855
1856    /// MPC swing: the percentage is where the off-beat falls inside the pair,
1857    /// so 75% is a triplet feel and the offset is exactly half a step.
1858    #[test]
1859    fn full_swing_is_a_triplet_feel() {
1860        let mut block = drum_pattern(16);
1861        block.swing = 75;
1862        assert_eq!(block.swing_offset(0), 0);
1863        assert_eq!(block.swing_offset(1), block.ticks_per_step() / 2);
1864        assert_eq!(block.swing_offset(2), 0);
1865        assert_eq!(block.swing_offset(15), block.ticks_per_step() / 2);
1866    }
1867
1868    /// Integer arithmetic, so the number is the same every time it is asked
1869    /// for — which is what makes the bounce and the live player agree without
1870    /// a tolerance.
1871    #[test]
1872    fn swing_is_exact_integer_ticks() {
1873        let mut block = drum_pattern(16);
1874        block.swing = 62;
1875        assert_eq!(block.swing_offset(1), 57); // (62-50) * 2 * 240 / 100
1876        block.rate = Rate::Eighth;
1877        assert_eq!(block.swing_offset(1), 115); // ...and 480
1878    }
1879
1880    /// Swing never reaches the following step, so the onsets stay in order
1881    /// however far it is pushed.
1882    #[test]
1883    fn swing_never_reorders_the_steps() {
1884        for swing in PatternBlock::MIN_SWING..=PatternBlock::MAX_SWING {
1885            let mut block = drum_pattern(16);
1886            block.swing = swing;
1887            let mut previous = i64::MIN;
1888            for index in 0..32 {
1889                let onset = block.onset(0, index);
1890                assert!(onset > previous, "swing {swing} reordered step {index}");
1891                previous = onset;
1892            }
1893        }
1894    }
1895
1896    // ── Position derivation ──
1897
1898    /// The clip invariant: what fires depends on where the transport is, not
1899    /// on how it got there. Starting inside a pattern plays the steps that
1900    /// are left, not the pattern from the top.
1901    #[test]
1902    fn starting_mid_pattern_fires_only_the_remaining_onsets() {
1903        let block = drum_pattern(16);
1904        let cycle = block.length_ticks();
1905        assert_eq!(cycle, 3840);
1906
1907        let whole = onsets(&run(&block, 0, cycle));
1908        assert_eq!(whole.len(), 16);
1909        assert_eq!(whole[0], 0);
1910
1911        let late = onsets(&run(&block, 1200, cycle));
1912        assert_eq!(late.len(), 11, "steps 5..=15 remain");
1913        assert_eq!(late[0], 1200);
1914        assert_eq!(late, whole[5..]);
1915    }
1916
1917    /// The step under the playhead is arithmetic on the position. Bar 5 of a
1918    /// 16-step pattern is the top of the pattern again.
1919    #[test]
1920    fn the_step_is_a_function_of_the_position() {
1921        let block = drum_pattern(16);
1922        assert_eq!(block.step_at(0, 0), 0);
1923        assert_eq!(block.step_at(0, 239), 0);
1924        assert_eq!(block.step_at(0, 240), 1);
1925        assert_eq!(block.step_at(0, 3840), 0);
1926        assert_eq!(block.step_at(0, 3840 * 4 + 720), 3);
1927    }
1928
1929    /// 12 and 24 exist so that a pattern can be deliberately out of phase
1930    /// with the bar. A 12-step sixteenth pattern is three beats long, so it
1931    /// walks one beat per bar and comes home on the fourth.
1932    #[test]
1933    fn a_twelve_step_pattern_drifts_against_the_bar() {
1934        let block = drum_pattern(12);
1935        let bar = 3840;
1936        assert_eq!(block.length_ticks(), 2880);
1937        assert_eq!(block.step_at(0, 0), 0);
1938        assert_eq!(block.step_at(0, bar), 4);
1939        assert_eq!(block.step_at(0, bar * 2), 8);
1940        assert_eq!(block.step_at(0, bar * 3), 0, "back in phase after three bars");
1941    }
1942
1943    /// Shortening a pattern hides the tail; it does not erase it.
1944    #[test]
1945    fn a_shorter_pattern_masks_rather_than_truncates() {
1946        let mut block = drum_pattern(32);
1947        assert_eq!(onsets(&run(&block, 0, block.length_ticks())).len(), 32);
1948
1949        block.steps = 16;
1950        let short = run(&block, 0, block.length_ticks());
1951        assert_eq!(onsets(&short).len(), 16);
1952
1953        block.steps = 32;
1954        assert_eq!(
1955            onsets(&run(&block, 0, block.length_ticks())).len(),
1956            32,
1957            "the steps past 16 were cleared rather than masked"
1958        );
1959    }
1960
1961    /// Contiguous windows tile a cycle exactly once — no onset falls in a
1962    /// crack and none is seen twice.
1963    #[test]
1964    fn tiling_a_cycle_with_windows_fires_every_step_once() {
1965        let block = drum_pattern(16);
1966        let cycle = block.length_ticks();
1967        for span in [1, 7, 240, 241, 1000] {
1968            let mut all = Vec::new();
1969            let mut pending = PendingOffs::new();
1970            let mut from = 0;
1971            while from < cycle {
1972                let to = (from + span).min(cycle);
1973                generate(&block, 0, from, to, &mut pending, &mut all);
1974                from = to;
1975            }
1976            assert_eq!(
1977                onsets(&all).len(),
1978                16,
1979                "span {span} produced the wrong number of onsets"
1980            );
1981        }
1982    }
1983
1984    // ── Gates and note-offs ──
1985
1986    #[test]
1987    fn a_gate_is_a_percentage_of_the_step() {
1988        let step = Step { gate: 50, ..Step::silent() };
1989        assert_eq!(step.gate_ticks(240), Some(120));
1990        let step = Step { gate: 200, ..Step::silent() };
1991        assert_eq!(step.gate_ticks(240), Some(480));
1992        // Out of range clamps rather than producing a note of no length.
1993        let step = Step { gate: 0, ..Step::silent() };
1994        assert_eq!(step.gate_ticks(240), Some(12));
1995        let step = Step { gate: Step::TIE, ..Step::silent() };
1996        assert_eq!(step.gate_ticks(240), None, "a tie has no due tick");
1997    }
1998
1999    #[test]
2000    fn every_note_gets_an_off() {
2001        let block = drum_pattern(16);
2002        let events = run(&block, 0, block.length_ticks() + 240);
2003        let ons = events.iter().filter(|e| e.is_note_on()).count();
2004        let offs = events.iter().filter(|e| e.status == 0x80).count();
2005        assert_eq!(ons, 17);
2006        assert_eq!(offs, 17, "a note was left sounding");
2007    }
2008
2009    /// A tie holds until the lane fires again, and the off it produces is at
2010    /// the next onset rather than at a gate length.
2011    #[test]
2012    fn a_tie_holds_to_the_next_onset() {
2013        let mut block = melodic_pattern(&[0, 4]);
2014        block.lanes[0].steps[0].gate = Step::TIE;
2015        let events = run(&block, 0, block.length_ticks());
2016
2017        let offs: Vec<i64> = events.iter().filter(|e| e.status == 0x80).map(|e| e.tick).collect();
2018        assert_eq!(offs[0], 960, "the tie ended somewhere other than step 4");
2019
2020        // ...and the off comes before the note-on it makes room for.
2021        let at_960: Vec<u8> = events.iter().filter(|e| e.tick == 960).map(|e| e.status).collect();
2022        assert_eq!(at_960, vec![0x80, 0x90], "the off has to be pushed first");
2023    }
2024
2025    /// A gate longer than the step does not run over the lane's own next hit:
2026    /// the retrigger cuts it, and the cut arrives before the new note.
2027    #[test]
2028    fn a_long_gate_is_cut_by_the_next_onset() {
2029        let mut block = melodic_pattern(&[0, 1]);
2030        block.lanes[0].steps[0].gate = 200;
2031        let events = run(&block, 0, 960);
2032        let at_240: Vec<u8> = events.iter().filter(|e| e.tick == 240).map(|e| e.status).collect();
2033        assert_eq!(at_240, vec![0x80, 0x90]);
2034    }
2035
2036    /// The table holds thirty-two notes, and the thirty-third forces off the
2037    /// oldest rather than being dropped. A dropped note-on would be silence;
2038    /// a dropped note-*off* is a voice that never stops.
2039    #[test]
2040    fn the_pending_table_forces_off_the_oldest_on_overflow() {
2041        let mut pending = PendingOffs::new();
2042        let mut out = Vec::new();
2043        for i in 0..MAX_PENDING_OFFS {
2044            pending.hold(0, 40 + i as u8, None, 0, &mut out);
2045        }
2046        assert_eq!(pending.len(), MAX_PENDING_OFFS);
2047        assert!(out.is_empty());
2048
2049        pending.hold(1, 99, None, 100, &mut out);
2050        assert_eq!(out.len(), 1);
2051        assert_eq!(out[0].data1, 40, "the oldest note was not the one forced off");
2052        assert_eq!(out[0].tick, 100);
2053        assert_eq!(pending.len(), MAX_PENDING_OFFS);
2054    }
2055
2056    #[test]
2057    fn a_flush_ends_everything_at_one_tick() {
2058        let mut pending = PendingOffs::new();
2059        let mut out = Vec::new();
2060        pending.hold(0, 60, Some(500), 0, &mut out);
2061        pending.hold(1, 64, None, 0, &mut out);
2062        pending.flush(300, &mut out);
2063        assert_eq!(out.len(), 2);
2064        assert!(out.iter().all(|e| e.tick == 300 && e.status == 0x80));
2065        assert!(pending.is_empty());
2066    }
2067
2068    // ── Mute and solo ──
2069
2070    #[test]
2071    fn a_muted_lane_is_silent_and_a_soloed_one_is_the_only_one() {
2072        let mut block = drum_pattern(16);
2073        block.lanes[1] = Lane::drum(42);
2074        for step in &mut block.lanes[1].steps {
2075            step.on = true;
2076        }
2077        assert_eq!(onsets(&run(&block, 0, 240)).len(), 2);
2078
2079        block.lanes[1].muted = true;
2080        assert_eq!(onsets(&run(&block, 0, 240)).len(), 1);
2081
2082        block.lanes[1].muted = false;
2083        block.lanes[1].soloed = true;
2084        let solo = run(&block, 0, 240);
2085        assert_eq!(onsets(&solo).len(), 1);
2086        assert_eq!(solo[0].data1, 42);
2087    }
2088
2089    // ── Velocity ──
2090
2091    #[test]
2092    fn accent_picks_the_patterns_accent_velocity() {
2093        let mut block = melodic_pattern(&[0, 1]);
2094        block.lanes[0].steps[1].accent = true;
2095        let events = run(&block, 0, 480);
2096        let ons: Vec<u8> = events.iter().filter(|e| e.is_note_on()).map(|e| e.data2).collect();
2097        assert_eq!(ons, vec![100, 127]);
2098    }
2099
2100    // ── Chords ──
2101
2102    fn notes_of(root: u8, chord: Chord, voicing: Voicing, below: bool, mode: Mode) -> Vec<u8> {
2103        let mut out = [0u8; MAX_CHORD_NOTES];
2104        let n = chord_notes(root, chord, voicing, below, mode, 0, &mut out);
2105        out[..n].to_vec()
2106    }
2107
2108    #[test]
2109    fn the_chord_table_is_the_shapes_it_names() {
2110        assert_eq!(notes_of(60, Chord::None, Voicing::Close, false, Mode::Chromatic), vec![60]);
2111        assert_eq!(notes_of(60, Chord::Fifth, Voicing::Close, false, Mode::Chromatic), vec![60, 67]);
2112        assert_eq!(notes_of(60, Chord::Octave, Voicing::Close, false, Mode::Chromatic), vec![60, 72]);
2113        assert_eq!(notes_of(60, Chord::Maj, Voicing::Close, false, Mode::Chromatic), vec![60, 64, 67]);
2114        assert_eq!(notes_of(60, Chord::Min, Voicing::Close, false, Mode::Chromatic), vec![60, 63, 67]);
2115        assert_eq!(notes_of(60, Chord::Dim, Voicing::Close, false, Mode::Chromatic), vec![60, 63, 66]);
2116        assert_eq!(notes_of(60, Chord::Sus2, Voicing::Close, false, Mode::Chromatic), vec![60, 62, 67]);
2117        assert_eq!(notes_of(60, Chord::Sus4, Voicing::Close, false, Mode::Chromatic), vec![60, 65, 67]);
2118        assert_eq!(notes_of(60, Chord::Maj6, Voicing::Close, false, Mode::Chromatic), vec![60, 64, 67, 69]);
2119        assert_eq!(notes_of(60, Chord::Min6, Voicing::Close, false, Mode::Chromatic), vec![60, 63, 67, 69]);
2120        assert_eq!(notes_of(60, Chord::Dom7, Voicing::Close, false, Mode::Chromatic), vec![60, 64, 67, 70]);
2121        assert_eq!(notes_of(60, Chord::Min7, Voicing::Close, false, Mode::Chromatic), vec![60, 63, 67, 70]);
2122        assert_eq!(notes_of(60, Chord::Maj7, Voicing::Close, false, Mode::Chromatic), vec![60, 64, 67, 71]);
2123        assert_eq!(notes_of(60, Chord::Quartal, Voicing::Close, false, Mode::Chromatic), vec![60, 65, 70]);
2124    }
2125
2126    /// The identities a step stores. Appending to this list is allowed;
2127    /// moving anything already in it rewrites every pattern ever saved.
2128    #[test]
2129    fn chord_identities_are_the_documented_order() {
2130        let order = [
2131            Chord::None, Chord::Fifth, Chord::Octave, Chord::Diatonic, Chord::Diatonic7,
2132            Chord::Maj, Chord::Min, Chord::Dim, Chord::Sus2, Chord::Sus4, Chord::Maj6,
2133            Chord::Min6, Chord::Dom7, Chord::Min7, Chord::Maj7, Chord::Quartal,
2134        ];
2135        for (index, chord) in order.iter().enumerate() {
2136            assert_eq!(chord.index() as usize, index);
2137            assert_eq!(Chord::from_index(index as u8), *chord);
2138        }
2139        assert_eq!(Chord::from_index(200), Chord::None, "an unknown id is one note");
2140    }
2141
2142    /// Drop-2 is the second voice from the top, down an octave — not the
2143    /// middle note, and on a four-note chord not the second note either.
2144    #[test]
2145    fn drop_two_lowers_the_second_voice_from_the_top() {
2146        // C E G -> E an octave down, under the root.
2147        assert_eq!(
2148            notes_of(60, Chord::Maj, Voicing::Drop2, false, Mode::Chromatic),
2149            vec![52, 60, 67]
2150        );
2151        // C E G B -> G an octave down.
2152        assert_eq!(
2153            notes_of(60, Chord::Maj7, Voicing::Drop2, false, Mode::Chromatic),
2154            vec![55, 60, 64, 71]
2155        );
2156    }
2157
2158    #[test]
2159    fn inversions_lift_the_bottom_voices() {
2160        assert_eq!(
2161            notes_of(60, Chord::Maj, Voicing::First, false, Mode::Chromatic),
2162            vec![64, 67, 72]
2163        );
2164        assert_eq!(
2165            notes_of(60, Chord::Maj, Voicing::Second, false, Mode::Chromatic),
2166            vec![67, 72, 76]
2167        );
2168    }
2169
2170    #[test]
2171    fn root_below_adds_the_bass_double() {
2172        assert_eq!(
2173            notes_of(60, Chord::Maj, Voicing::Close, true, Mode::Chromatic),
2174            vec![48, 60, 64, 67]
2175        );
2176    }
2177
2178    /// Every combination in the table, checked for the three things that
2179    /// would make a chord unplayable: a note outside MIDI, the same note
2180    /// twice — which leaves the child holding a voice nothing turns off —
2181    /// and an empty chord.
2182    #[test]
2183    fn every_chord_and_voicing_is_playable() {
2184        for &chord in &Chord::ALL {
2185            for &voicing in &Voicing::ALL {
2186                for below in [false, true] {
2187                    for &mode in &Mode::ALL {
2188                        for root in 24..=96u8 {
2189                            let notes = notes_of(root, chord, voicing, below, mode);
2190                            assert!(!notes.is_empty(), "{chord:?} produced nothing");
2191                            assert!(notes.len() <= MAX_CHORD_NOTES);
2192                            let mut seen = notes.clone();
2193                            seen.dedup();
2194                            assert_eq!(seen, notes, "{chord:?}/{voicing:?} doubled a note");
2195                            for window in notes.windows(2) {
2196                                assert!(window[0] < window[1], "not ascending");
2197                            }
2198                        }
2199                    }
2200                }
2201            }
2202        }
2203    }
2204
2205    /// A voicing rearranges a chord; it does not change it. The pitch-class
2206    /// set is what "the same chord" means, and it is invariant under every
2207    /// voicing — root-below excepted, which is a deliberate duplicate of one
2208    /// class an octave down and so leaves the *set* alone as well.
2209    #[test]
2210    fn voicings_preserve_the_pitch_class_set() {
2211        for &chord in &Chord::ALL {
2212            for &mode in &Mode::ALL {
2213                for root in 36..=84u8 {
2214                    let classes = |notes: Vec<u8>| {
2215                        let mut c: Vec<u8> = notes.iter().map(|n| n % 12).collect();
2216                        c.sort_unstable();
2217                        c.dedup();
2218                        c
2219                    };
2220                    let close = classes(notes_of(root, chord, Voicing::Close, false, mode));
2221                    for &voicing in &Voicing::ALL {
2222                        for below in [false, true] {
2223                            assert_eq!(
2224                                classes(notes_of(root, chord, voicing, below, mode)),
2225                                close,
2226                                "{chord:?} changed identity under {voicing:?} below={below}"
2227                            );
2228                        }
2229                    }
2230                }
2231            }
2232        }
2233    }
2234
2235    /// The textbook qualities, in every mode. This is the table the whole
2236    /// diatonic idea rests on: if the third degree of Dorian is not minor,
2237    /// a line of diatonic chords is not in a key at all.
2238    #[test]
2239    fn diatonic_triads_have_the_textbook_qualities_in_every_mode() {
2240        let expected: [(Mode, [Chord; 7]); 7] = [
2241            (Mode::Ionian, [Chord::Maj, Chord::Min, Chord::Min, Chord::Maj, Chord::Maj, Chord::Min, Chord::Dim]),
2242            (Mode::Dorian, [Chord::Min, Chord::Min, Chord::Maj, Chord::Maj, Chord::Min, Chord::Dim, Chord::Maj]),
2243            (Mode::Phrygian, [Chord::Min, Chord::Maj, Chord::Maj, Chord::Min, Chord::Dim, Chord::Maj, Chord::Min]),
2244            (Mode::Lydian, [Chord::Maj, Chord::Maj, Chord::Min, Chord::Dim, Chord::Maj, Chord::Min, Chord::Min]),
2245            (Mode::Mixolydian, [Chord::Maj, Chord::Min, Chord::Dim, Chord::Maj, Chord::Min, Chord::Min, Chord::Maj]),
2246            (Mode::Aeolian, [Chord::Min, Chord::Dim, Chord::Maj, Chord::Min, Chord::Min, Chord::Maj, Chord::Maj]),
2247            (Mode::Locrian, [Chord::Dim, Chord::Maj, Chord::Min, Chord::Min, Chord::Maj, Chord::Maj, Chord::Min]),
2248        ];
2249
2250        for tonic in 0..12u8 {
2251            for (mode, qualities) in &expected {
2252                let scale = mode.scale().expect("a mode has a scale");
2253                for (degree, &quality) in qualities.iter().enumerate() {
2254                    let root = 60 + i32::from(tonic) + scale[degree];
2255                    let root = root as u8;
2256                    let mut derived = [0u8; MAX_CHORD_NOTES];
2257                    let n = chord_notes(
2258                        root, Chord::Diatonic, Voicing::Close, false, *mode, tonic, &mut derived,
2259                    );
2260                    let mut explicit = [0u8; MAX_CHORD_NOTES];
2261                    let m = chord_notes(
2262                        root, quality, Voicing::Close, false, *mode, tonic, &mut explicit,
2263                    );
2264                    assert_eq!(
2265                        derived[..n],
2266                        explicit[..m],
2267                        "{mode:?} degree {} in tonic {tonic} should be {quality:?}",
2268                        degree + 1
2269                    );
2270                }
2271            }
2272        }
2273    }
2274
2275    /// The seventh degree of a major scale is half-diminished, which is not
2276    /// one of the sixteen chord types a step can name — and does not have to
2277    /// be, because `diatonic7` produces intervals rather than picking a type.
2278    #[test]
2279    fn the_seventh_degree_is_half_diminished() {
2280        let mut out = [0u8; MAX_CHORD_NOTES];
2281        let n = chord_notes(71, Chord::Diatonic7, Voicing::Close, false, Mode::Ionian, 0, &mut out);
2282        assert_eq!(&out[..n], &[71, 74, 77, 81], "B D F A is not m7♭5");
2283    }
2284
2285    /// Under Chromatic there is no degree to derive a quality from, so the
2286    /// two diatonic entries collapse — documented behaviour, not a fallback
2287    /// nobody meant.
2288    #[test]
2289    fn the_diatonic_chords_collapse_under_chromatic() {
2290        assert_eq!(
2291            notes_of(60, Chord::Diatonic, Voicing::Close, false, Mode::Chromatic),
2292            notes_of(60, Chord::Maj, Voicing::Close, false, Mode::Chromatic)
2293        );
2294        assert_eq!(
2295            notes_of(60, Chord::Diatonic7, Voicing::Close, false, Mode::Chromatic),
2296            notes_of(60, Chord::Maj7, Voicing::Close, false, Mode::Chromatic)
2297        );
2298    }
2299
2300    /// A note the mode does not contain is a borrowed note, and a diatonic
2301    /// chord on one has no derived quality — it falls back to major rather
2302    /// than refusing to sound.
2303    #[test]
2304    fn a_borrowed_root_falls_back_to_major() {
2305        assert_eq!(Mode::Ionian.degree_of(61, 0), None);
2306        assert_eq!(
2307            notes_of(61, Chord::Diatonic, Voicing::Close, false, Mode::Ionian),
2308            vec![61, 65, 68]
2309        );
2310    }
2311
2312    // ── Mode walking ──
2313
2314    #[test]
2315    fn chromatic_walking_is_semitones() {
2316        assert_eq!(Mode::Chromatic.walk(60, 0, 1), 61);
2317        assert_eq!(Mode::Chromatic.walk(60, 0, -1), 59);
2318        assert_eq!(Mode::Chromatic.walk(0, 0, -1), 0, "the bottom of the range holds");
2319        assert_eq!(Mode::Chromatic.walk(127, 0, 1), 127);
2320    }
2321
2322    #[test]
2323    fn mode_walking_is_scale_degrees() {
2324        // C major, up the scale and back down through the octave below.
2325        let mut note = 60;
2326        for expected in [62, 64, 65, 67, 69, 71, 72, 74] {
2327            note = Mode::Ionian.walk(note, 0, 1);
2328            assert_eq!(note, expected);
2329        }
2330        let mut note = 60;
2331        for expected in [59, 57, 55, 53, 52, 50, 48] {
2332            note = Mode::Ionian.walk(note, 0, -1);
2333            assert_eq!(note, expected);
2334        }
2335    }
2336
2337    /// A note off the scale — set before the mode was, or borrowed on
2338    /// purpose — snaps onto it on the first press rather than walking off it
2339    /// forever.
2340    #[test]
2341    fn walking_snaps_a_borrowed_note_onto_the_scale() {
2342        assert_eq!(Mode::Ionian.walk(61, 0, 1), 62, "C# up lands on D");
2343        assert_eq!(Mode::Ionian.walk(61, 0, -1), 60, "C# down lands on C");
2344    }
2345
2346    #[test]
2347    fn every_mode_walks_a_full_octave_in_seven_degrees() {
2348        for &mode in &Mode::ALL {
2349            if mode == Mode::Chromatic {
2350                continue;
2351            }
2352            for tonic in 0..12u8 {
2353                let start = 60 + tonic;
2354                let start = mode.walk(start, tonic, 0);
2355                let mut note = start;
2356                for _ in 0..7 {
2357                    note = mode.walk(note, tonic, 1);
2358                }
2359                assert_eq!(note, start + 12, "{mode:?} in {tonic} did not close");
2360            }
2361        }
2362    }
2363
2364    // ── Switch quantization ──
2365
2366    #[test]
2367    fn switch_boundaries_are_the_next_grid_line() {
2368        let pattern = 3840;
2369        assert_eq!(SwitchQuant::Immediate.boundary(1234, pattern), 1234);
2370        assert_eq!(SwitchQuant::Beat.boundary(1234, pattern), 1920);
2371        assert_eq!(SwitchQuant::Bar.boundary(1234, pattern), 3840);
2372        assert_eq!(SwitchQuant::PatternEnd.boundary(1234, pattern), 3840);
2373        assert_eq!(SwitchQuant::PatternEnd.boundary(4000, 2880), 5760);
2374    }
2375
2376    /// A queue made exactly on the boundary takes that boundary, not the
2377    /// next one — otherwise a switch queued on the downbeat waits a whole
2378    /// extra bar.
2379    #[test]
2380    fn a_boundary_already_reached_is_the_answer() {
2381        assert_eq!(SwitchQuant::Bar.boundary(3840, 3840), 3840);
2382        assert_eq!(SwitchQuant::Beat.boundary(960, 3840), 960);
2383        assert_eq!(SwitchQuant::PatternEnd.boundary(0, 3840), 0);
2384    }
2385
2386    // ── The window ──
2387
2388    /// 120 BPM, 44.1 kHz.
2389    const TPS: f64 = 120.0 * 960.0 / (60.0 * 44_100.0);
2390
2391    fn window(position: i64, frames: u32, previous: Option<PlaybackWindow>) -> PlaybackWindow {
2392        PlaybackWindow::for_block(position, frames, TPS, None, previous)
2393    }
2394
2395    #[test]
2396    fn the_first_window_starts_where_the_transport_is() {
2397        let w = window(1000, 512, None);
2398        assert_eq!(w.from(), 1000);
2399        assert!(!w.is_continuous(), "there is nothing for it to continue from");
2400    }
2401
2402    /// The gap the contiguity rule exists to close: a transport that carries
2403    /// the sub-tick remainder advances further than the block measured, and
2404    /// the tick in between belongs to somebody.
2405    #[test]
2406    fn a_window_continues_from_the_last_one_across_a_rounding_gap() {
2407        let first = window(0, 470, None);
2408        let span = first.to();
2409        // The transport landed one tick past where the block measured.
2410        let second = window(span + 1, 470, Some(first));
2411        assert_eq!(second.from(), span, "a tick of song time was skipped");
2412        assert!(second.is_continuous());
2413        assert_eq!(second.to(), span + 1 + span);
2414    }
2415
2416    /// Moving the playhead is not continuous playback, and nothing gets
2417    /// replayed to cover the jump.
2418    #[test]
2419    fn a_jump_breaks_continuity() {
2420        let first = window(0, 512, None);
2421        let jumped = window(100_000, 512, Some(first));
2422        assert_eq!(jumped.from(), 100_000);
2423        assert!(!jumped.is_continuous());
2424    }
2425
2426    /// A loop wrap starts the window at the loop point, so the ticks between
2427    /// the loop point and where the callback arrived are played rather than
2428    /// skipped. This is what clip playback has always done.
2429    #[test]
2430    fn a_loop_wrap_starts_the_window_at_the_loop_point() {
2431        let previous = PlaybackWindow::for_block(3800, 512, TPS, Some((0, 3840)), None);
2432        let wrapped = PlaybackWindow::for_block(3, 512, TPS, Some((0, 3840)), Some(previous));
2433        assert_eq!(wrapped.from(), 0);
2434        assert!(!wrapped.is_continuous());
2435    }
2436
2437    /// The window stops at the loop point. Reaching across it would play the
2438    /// notes on the other side, and then the wrap would play them again.
2439    #[test]
2440    fn a_window_never_reaches_past_the_loop_end() {
2441        let w = PlaybackWindow::for_block(3830, 4096, TPS, Some((0, 3840)), None);
2442        assert_eq!(w.to(), 3840);
2443        assert!(!w.contains(3840));
2444    }
2445
2446    /// The one expression that turns song time into a sample position. Both
2447    /// clips and patterns ask it, which is the whole point.
2448    #[test]
2449    fn sample_offsets_come_from_ticks_and_nothing_else() {
2450        let w = window(1000, 512, None);
2451        assert_eq!(w.sample_offset(1000), 0);
2452        assert_eq!(w.sample_offset(999), 0, "before the window is the first sample");
2453        assert_eq!(w.sample_offset(1000 + 22), (22.0 / TPS) as u32);
2454        assert_eq!(w.sample_offset(i64::MAX), 511, "past the block is the last sample");
2455    }
2456
2457    #[test]
2458    fn a_zero_length_block_has_no_samples_to_land_on() {
2459        let w = window(0, 0, None);
2460        assert_eq!(w.sample_offset(1000), 0);
2461    }
2462
2463    // ── The player ──
2464
2465    fn player_with(slot0: PatternBlock, slot1: PatternBlock) -> PatternPlayer {
2466        let mut player = PatternPlayer::new();
2467        player.apply(1, slot1);
2468        player.apply(0, slot0);
2469        player
2470    }
2471
2472    /// Run a player over consecutive callbacks, the way the mixer does:
2473    /// each window continues from the last, and the transport position is
2474    /// wherever the previous window ended.
2475    fn run_player(
2476        player: &mut PatternPlayer,
2477        start: i64,
2478        frames: u32,
2479        until: i64,
2480    ) -> Vec<PatternEvent> {
2481        let mut out = Vec::new();
2482        let mut position = start;
2483        let mut previous = None;
2484        while position < until {
2485            let w = window(position, frames, previous);
2486            player.render(&w, true, &mut out);
2487            position = w.to();
2488            previous = Some(w);
2489        }
2490        out
2491    }
2492
2493    /// One callback of a running player, and the events it produced.
2494    fn tick_player(
2495        player: &mut PatternPlayer,
2496        position: i64,
2497        frames: u32,
2498        previous: Option<PlaybackWindow>,
2499    ) -> (PlaybackWindow, Vec<PatternEvent>) {
2500        let w = window(position, frames, previous);
2501        let mut out = Vec::new();
2502        player.render(&w, true, &mut out);
2503        (w, out)
2504    }
2505
2506    #[test]
2507    fn a_stopped_transport_produces_nothing_and_then_flushes_once() {
2508        let mut player = player_with(drum_pattern(16), PatternBlock::empty());
2509        let (w, events) = tick_player(&mut player, 0, 512, None);
2510        assert!(!events.is_empty());
2511        assert!(player.held_notes() > 0);
2512
2513        let mut out = Vec::new();
2514        player.render(&w, false, &mut out);
2515        assert_eq!(out.len(), 1, "the sounding note was not turned off");
2516        assert_eq!(out[0].status, 0x80);
2517        assert_eq!(player.held_notes(), 0);
2518
2519        let mut again = Vec::new();
2520        player.render(&w, false, &mut again);
2521        assert!(again.is_empty(), "the flush repeated");
2522    }
2523
2524    /// The switch: at the boundary, everything sounding is turned off, and
2525    /// those offs are pushed before the new pattern's first notes.
2526    #[test]
2527    fn a_pattern_switch_ends_the_old_notes_before_starting_the_new_ones() {
2528        let mut a = drum_pattern(16);
2529        a.lanes[0].steps[15].gate = Step::TIE;
2530        let mut b = drum_pattern(16);
2531        b.lanes[0] = Lane::drum(42);
2532        for step in &mut b.lanes[0].steps {
2533            step.on = true;
2534        }
2535
2536        let mut player = player_with(a, b);
2537        // Queue slot 1 for the end of the pattern.
2538        let mut queue = a;
2539        queue.pending_slot = Some(1);
2540        player.apply(0, queue);
2541        assert_eq!(player.countdown(3600), Some((1, 1)), "one step to go");
2542
2543        // Run into the boundary from before the tied step, so that the note
2544        // the switch has to end is actually sounding when it arrives.
2545        let out = run_player(&mut player, 3500, 512, 3900);
2546
2547        let at_boundary: Vec<(u8, u8)> = out
2548            .iter()
2549            .filter(|e| e.tick == 3840)
2550            .map(|e| (e.status, e.data1))
2551            .collect();
2552        assert_eq!(
2553            at_boundary,
2554            vec![(0x80, 36), (0x90, 42)],
2555            "the old note has to be ended before the new one starts"
2556        );
2557        assert_eq!(player.live_slot(), 1);
2558        assert_eq!(player.queued_slot(), None);
2559    }
2560
2561    /// An immediate switch happens at the top of the next callback, not one
2562    /// tick into it: the boundary is the cursor itself, and the first note of
2563    /// the new pattern is the first note of the block.
2564    #[test]
2565    fn an_immediate_switch_takes_effect_at_the_start_of_the_block() {
2566        let a = drum_pattern(16);
2567        let mut b = drum_pattern(16);
2568        b.lanes[0] = Lane::drum(42);
2569        for step in &mut b.lanes[0].steps {
2570            step.on = true;
2571        }
2572
2573        let mut player = player_with(a, b);
2574        let mut queued = a;
2575        queued.pending_slot = Some(1);
2576        queued.switch_quant = SwitchQuant::Immediate;
2577        player.apply(0, queued);
2578
2579        // A block starting exactly on a step, so the switch and an onset land
2580        // on the same tick.
2581        let w = window(480, 512, None);
2582        let mut out = Vec::new();
2583        player.render(&w, true, &mut out);
2584        assert_eq!(player.live_slot(), 1);
2585        let first = out.iter().find(|e| e.is_note_on()).expect("a note");
2586        assert_eq!(first.data1, 42, "the old pattern played after an immediate switch");
2587        assert_eq!(first.tick, 480);
2588    }
2589
2590    /// A switch quantized to the beat lands on the beat, in the middle of the
2591    /// callback that contains it, not at either end of it.
2592    #[test]
2593    fn a_beat_quantized_switch_splits_the_block_at_the_beat() {
2594        let a = drum_pattern(16);
2595        let mut b = drum_pattern(16);
2596        b.lanes[0] = Lane::drum(42);
2597        for step in &mut b.lanes[0].steps {
2598            step.on = true;
2599        }
2600
2601        let mut player = player_with(a, b);
2602        let mut queued = a;
2603        queued.pending_slot = Some(1);
2604        queued.switch_quant = SwitchQuant::Beat;
2605        player.apply(0, queued);
2606
2607        let out = run_player(&mut player, 700, 512, 1100);
2608        let switched: Vec<(i64, u8)> = out
2609            .iter()
2610            .filter(|e| e.is_note_on())
2611            .map(|e| (e.tick, e.data1))
2612            .collect();
2613        assert_eq!(
2614            switched,
2615            vec![(720, 36), (960, 42)],
2616            "the switch did not land on the beat"
2617        );
2618        assert_eq!(player.live_slot(), 1);
2619    }
2620
2621    /// Queueing the slot that is already playing is not a switch — which is
2622    /// what makes a stale queue on the UI side harmless rather than a note
2623    /// cut nobody asked for.
2624    #[test]
2625    fn queueing_the_live_slot_does_nothing() {
2626        let mut block = drum_pattern(16);
2627        block.pending_slot = Some(0);
2628        let player = player_with(block, PatternBlock::empty());
2629        assert_eq!(player.queued_slot(), None);
2630        assert_eq!(player.countdown(0), None);
2631    }
2632
2633    /// A chain is a program the position indexes into, not a cursor: dropping
2634    /// the playhead into the middle of one lands in the entry that belongs
2635    /// there, exactly as a step does.
2636    #[test]
2637    fn a_chain_is_derived_from_the_position() {
2638        let a = drum_pattern(16);
2639        let mut b = drum_pattern(16);
2640        b.lanes[0] = Lane::drum(42);
2641        for step in &mut b.lanes[0].steps {
2642            step.on = true;
2643        }
2644
2645        let mut chained = a;
2646        chained.chain[0] = ChainEntry { slot: 0, repeats: 2 };
2647        chained.chain[1] = ChainEntry { slot: 1, repeats: 1 };
2648        chained.chain_len = 2;
2649
2650        let mut player = PatternPlayer::new();
2651        player.apply(1, b);
2652        player.apply(0, chained);
2653
2654        let cycle = 3840;
2655        // Two times through A, then one of B, then round again.
2656        for (position, expected) in [
2657            (0, 36),
2658            (cycle, 36),
2659            (cycle * 2, 42),
2660            (cycle * 3, 36),
2661            (cycle * 5, 42),
2662        ] {
2663            let mut out = Vec::new();
2664            let w = window(position, 512, None);
2665            player.render(&w, true, &mut out);
2666            let first = out.iter().find(|e| e.is_note_on()).expect("a note");
2667            assert_eq!(first.data1, expected, "wrong chain entry at tick {position}");
2668        }
2669    }
2670
2671    /// The chain boundary is a switch like any other: notes sounding across
2672    /// it are ended at it.
2673    #[test]
2674    fn a_chain_advance_ends_the_notes_it_replaces() {
2675        let mut a = drum_pattern(16);
2676        a.lanes[0].steps[15].gate = Step::TIE;
2677        let mut b = drum_pattern(16);
2678        b.lanes[0] = Lane::drum(42);
2679        b.lanes[0].steps[0].on = true;
2680
2681        let mut chained = a;
2682        chained.chain[0] = ChainEntry { slot: 0, repeats: 1 };
2683        chained.chain[1] = ChainEntry { slot: 1, repeats: 1 };
2684        chained.chain_len = 2;
2685
2686        let mut player = PatternPlayer::new();
2687        player.apply(1, b);
2688        player.apply(0, chained);
2689
2690        let out = run_player(&mut player, 3500, 512, 3900);
2691        let at_boundary: Vec<(u8, u8)> = out
2692            .iter()
2693            .filter(|e| e.tick == 3840)
2694            .map(|e| (e.status, e.data1))
2695            .collect();
2696        assert_eq!(at_boundary, vec![(0x80, 36), (0x90, 42)]);
2697    }
2698
2699    // ── Bounce ──
2700
2701    /// The bounce is not "the same as" live playback: it is live playback,
2702    /// run with a different sink. Anything that changed one and not the other
2703    /// would show up here as a tick that does not match.
2704    #[test]
2705    fn a_bounced_cycle_is_tick_identical_to_live_playback() {
2706        for swing in [50u8, 58, 62, 75] {
2707            for rate in Rate::ALL {
2708                let mut block = drum_pattern(16);
2709                block.rate = rate;
2710                block.swing = swing;
2711                block.lanes[0].steps[3].gate = 150;
2712                block.lanes[0].steps[7].gate = Step::TIE;
2713                block.lanes[0].steps[9].accent = true;
2714
2715                let mut bounced = Vec::new();
2716                compile_cycle(&block, 0, &mut bounced);
2717
2718                // ...and the same pattern played a block at a time.
2719                let cycle = block.length_ticks();
2720                let mut live = Vec::new();
2721                let mut pending = PendingOffs::new();
2722                let mut from = 0;
2723                while from < cycle {
2724                    let to = (from + 97).min(cycle);
2725                    generate(&block, 0, from, to, &mut pending, &mut live);
2726                    from = to;
2727                }
2728                pending.flush(cycle, &mut live);
2729                live.sort_by_key(|e| e.tick);
2730
2731                let key = |e: &PatternEvent| (e.tick, e.status, e.data1, e.data2);
2732                let bounced: Vec<_> = bounced.iter().map(key).collect();
2733                let live: Vec<_> = live.iter().map(key).collect();
2734                assert_eq!(bounced, live, "swing {swing} at {}", rate.label());
2735            }
2736        }
2737    }
2738
2739    // ── Allocation ──
2740
2741    /// The rule the audio thread lives by. Rendering a pattern, switching
2742    /// one, advancing a chain and taking a new block are all writes into
2743    /// memory that already exists.
2744    #[test]
2745    fn rendering_a_pattern_does_not_allocate() {
2746        let mut a = drum_pattern(16);
2747        a.lanes[0].steps[15].gate = Step::TIE;
2748        let mut b = drum_pattern(16);
2749        b.lanes[0] = Lane::drum(42);
2750
2751        let mut player = Box::new(player_with(a, b));
2752        let mut sink = Vec::with_capacity(1024);
2753        let mut queued = a;
2754        queued.pending_slot = Some(1);
2755
2756        // One warm-up callback outside the measurement.
2757        let mut w = window(0, 512, None);
2758        player.render(&w, true, &mut sink);
2759
2760        let allocations = crate::alloc_count::allocations_during(|| {
2761            let mut position = 0;
2762            for block in 0..64 {
2763                w = window(position, 512, Some(w));
2764                sink.clear();
2765                player.render(&w, true, &mut sink);
2766                if block == 8 {
2767                    player.apply(0, queued);
2768                }
2769                position = w.to();
2770            }
2771        });
2772        assert_eq!(allocations, 0, "the pattern player reached the allocator");
2773    }
2774
2775    /// A sink that is out of room stops the generator rather than losing
2776    /// events out of the middle of a step — half a chord with no offs is a
2777    /// stuck voice, and a missing note is not.
2778    #[test]
2779    fn a_full_sink_stops_the_generator() {
2780        struct Capped(Vec<PatternEvent>, usize);
2781        impl EventSink for Capped {
2782            fn accept(&mut self, event: PatternEvent) -> bool {
2783                if self.0.len() >= self.1 {
2784                    return false;
2785                }
2786                self.0.push(event);
2787                true
2788            }
2789        }
2790
2791        let block = drum_pattern(16);
2792        let mut sink = Capped(Vec::new(), 3);
2793        let mut pending = PendingOffs::new();
2794        generate(&block, 0, 0, block.length_ticks(), &mut pending, &mut sink);
2795        assert_eq!(sink.0.len(), 3, "the sink was written past its cap");
2796    }
2797}
2798