Skip to main content

phosphor_app/sequencer/
mod.rs

1//! The step sequencer, on the UI side of the fence.
2//!
3//! The audio thread's half of this lives in [`phosphor_core::pattern`]: fixed
4//! -size patterns, position-derived step timing, and the generator that turns
5//! them into notes. This half is what a player edits — eight pattern slots, a
6//! chain, a cursor, a mode — and what a session stores.
7//!
8//! # A sequencer track is an instrument track
9//!
10//! There is no sequencer plugin and no sequencer audio path. A sequencer
11//! track holds an ordinary instrument in its ordinary plugin slot — the
12//! *child* — and a pattern player in front of it, feeding it the notes a
13//! keyboard would otherwise feed it. That is why [`InstrumentType::Sequencer`]
14//! is a choice in the add-track menu but never a value stored on a track: the
15//! track's `instrument_type` is its child, so the child's panel, its preset
16//! bank, its selectors and its saved parameters all keep working with no code
17//! that knows a sequencer exists. What marks the track is
18//! [`crate::state::TrackState::sequencer`] being `Some`.
19//!
20//! # One mutation surface
21//!
22//! Nothing in this module is edited by reaching into a field. Every change
23//! goes through [`ops::SeqOp`] and [`ops::dispatch`], which exists so that the
24//! same edits can be driven by keys today and by a MIDI controller later
25//! without two implementations of "toggle the step under the cursor" drifting
26//! apart. See [`ops`].
27//!
28//! # What is track-level and what is pattern-level
29//!
30//! Five settings describe the *track* rather than a pattern — whether it is
31//! running, the queued slot, the switch quantization, and the chain — and they
32//! are held once here and stamped onto every block on its way out, by
33//! [`SequencerState::block`]. The audio thread takes them from whichever block
34//! arrived last, so changing one costs a single command rather than eight.
35//!
36//! Everything else, the mode and tonic included, belongs to a pattern. That is
37//! a small widening of what was specified: it makes A in Dorian and B in
38//! Aeolian possible, and it removes the only case where changing one setting
39//! would have had to rewrite all eight slots to stay consistent.
40
41pub mod chords;
42pub mod compile;
43pub mod ops;
44
45use serde::{Deserialize, Serialize};
46
47use phosphor_core::pattern::{
48    ChainEntry, Lane, Mode, PatternBlock, Rate, Step, SwitchQuant, LANES, MAX_CHAIN, MAX_STEPS,
49    SLOTS,
50};
51
52use crate::session::{apply_selectors, instrument_key, parse_instrument_type, SessionSelector};
53use crate::state::InstrumentType;
54
55/// What a new sequencer track drives until told otherwise.
56///
57/// A drum machine, because a step grid is a drum machine's native form and
58/// because the eight lanes are immediately meaningful: one per voice, pinned
59/// to the kit's note map. A melodic child gets the same grid with the pitch
60/// controls live instead.
61pub const DEFAULT_CHILD: InstrumentType = InstrumentType::DrumRack;
62
63/// The kit voices a drum pattern's eight lanes start pinned to, in the order
64/// a drum machine's front panel puts them.
65///
66/// General MIDI note numbers, which is what [`phosphor_dsp::drum_rack`] reads:
67/// bass drum, snare, closed hat, open hat, clap, and the three toms.
68pub const DEFAULT_DRUM_LANES: [u8; LANES] = [36, 38, 42, 46, 39, 41, 45, 48];
69
70/// The short names for [`DEFAULT_DRUM_LANES`], for a lane strip.
71pub const DEFAULT_DRUM_LABELS: [&str; LANES] = ["BD", "SD", "CH", "OH", "CP", "LT", "MT", "HT"];
72
73/// Whether a child instrument is played as a kit rather than as a keyboard.
74///
75/// The one thing this decides is where a step's pitch comes from: a drum
76/// pattern's lanes are each pinned to a voice and the step only says *when*,
77/// while a melodic pattern's steps carry pitch, chord and voicing.
78#[must_use]
79pub const fn is_drum_child(child: InstrumentType) -> bool {
80    matches!(child, InstrumentType::DrumRack)
81}
82
83// ── State ──
84
85/// A sequencer track's patterns, and where the player is in them.
86#[derive(Debug, Clone, PartialEq)]
87pub struct SequencerState {
88    /// The eight slots. Everything about a *pattern* lives in one of these.
89    patterns: [PatternBlock; SLOTS],
90    /// The slot the editor is looking at.
91    selected: u8,
92    /// The slot the UI believes is sounding. A mirror of the audio thread's,
93    /// refreshed by [`SequencerState::sync_from_audio`].
94    live: u8,
95    /// The queued slot, mirroring what the audio thread was last told.
96    pending: Option<u8>,
97    switch_quant: SwitchQuant,
98    playing: bool,
99    chain: [ChainEntry; MAX_CHAIN],
100    chain_len: u8,
101    /// Row under the editor's cursor.
102    lane: u8,
103    /// Column under the editor's cursor.
104    step: u8,
105    /// Whether played notes are written into the pattern.
106    step_record: bool,
107}
108
109impl SequencerState {
110    /// A new sequencer for `child`: eight empty patterns, laid out for a kit
111    /// or for a keyboard depending on what it is driving.
112    #[must_use]
113    pub fn new(child: InstrumentType) -> Self {
114        let mut blank = PatternBlock::empty();
115        if is_drum_child(child) {
116            for (lane, note) in blank.lanes.iter_mut().zip(DEFAULT_DRUM_LANES) {
117                *lane = Lane::drum(note);
118            }
119        }
120        Self {
121            patterns: [blank; SLOTS],
122            selected: 0,
123            live: 0,
124            pending: None,
125            switch_quant: SwitchQuant::PatternEnd,
126            playing: false,
127            chain: [ChainEntry { slot: 0, repeats: 1 }; MAX_CHAIN],
128            chain_len: 0,
129            lane: 0,
130            step: 0,
131            step_record: false,
132        }
133    }
134
135    // ── Reads ──
136
137    #[must_use]
138    pub fn selected_slot(&self) -> u8 {
139        self.selected
140    }
141
142    #[must_use]
143    pub fn live_slot(&self) -> u8 {
144        self.live
145    }
146
147    #[must_use]
148    pub fn queued_slot(&self) -> Option<u8> {
149        self.pending
150    }
151
152    #[must_use]
153    pub fn is_playing(&self) -> bool {
154        self.playing
155    }
156
157    #[must_use]
158    pub fn switch_quant(&self) -> SwitchQuant {
159        self.switch_quant
160    }
161
162    #[must_use]
163    pub fn is_step_recording(&self) -> bool {
164        self.step_record
165    }
166
167    #[must_use]
168    pub fn lane_cursor(&self) -> usize {
169        (self.lane as usize).min(LANES - 1)
170    }
171
172    #[must_use]
173    pub fn step_cursor(&self) -> usize {
174        (self.step as usize).min(MAX_STEPS - 1)
175    }
176
177    /// The pattern under the editor.
178    #[must_use]
179    pub fn pattern(&self) -> &PatternBlock {
180        &self.patterns[(self.selected as usize).min(SLOTS - 1)]
181    }
182
183    /// One of the eight, as stored — without the track-level settings. Use
184    /// [`SequencerState::block`] for what the audio thread should be given.
185    #[must_use]
186    pub fn pattern_at(&self, slot: usize) -> &PatternBlock {
187        &self.patterns[slot.min(SLOTS - 1)]
188    }
189
190    /// The lane under the editor.
191    #[must_use]
192    pub fn lane(&self) -> &Lane {
193        &self.pattern().lanes[self.lane_cursor()]
194    }
195
196    /// The step under the editor.
197    #[must_use]
198    pub fn step(&self) -> &Step {
199        &self.lane().steps[self.step_cursor()]
200    }
201
202    /// The chain, as far as it is filled in.
203    #[must_use]
204    pub fn chain(&self) -> &[ChainEntry] {
205        &self.chain[..(self.chain_len as usize).min(MAX_CHAIN)]
206    }
207
208    /// Whether a chain is running. A chain owns the slot outright, so
209    /// queueing one by hand does nothing until the chain is cleared.
210    #[must_use]
211    pub fn is_chained(&self) -> bool {
212        self.chain_len > 0
213    }
214
215    /// The block for `slot` as the audio thread should see it: the pattern's
216    /// own data with the track-level settings stamped on.
217    ///
218    /// The only way a block leaves this module, which is what keeps the two
219    /// sides of the fence agreeing about what "running" and "queued" mean.
220    #[must_use]
221    pub fn block(&self, slot: usize) -> PatternBlock {
222        let mut block = self.patterns[slot.min(SLOTS - 1)];
223        block.playing = self.playing;
224        block.pending_slot = self.pending;
225        block.switch_quant = self.switch_quant;
226        block.chain = self.chain;
227        block.chain_len = self.chain_len;
228        block
229    }
230
231    /// Where a queued switch lands, and how many steps away it is.
232    ///
233    /// The same arithmetic the audio thread does, on the same inputs, so the
234    /// countdown on screen is not a message that may not have arrived yet.
235    #[must_use]
236    pub fn countdown(&self, position: i64) -> Option<(u8, i64)> {
237        let slot = self.pending?;
238        let live = self.pattern_at(self.live as usize);
239        let at = self.switch_quant.boundary(position, live.length_ticks());
240        Some((slot, (at - position).div_euclid(live.ticks_per_step())))
241    }
242
243    /// Take the live slot, the queued slot and the playhead from the audio
244    /// thread's own copy. Called once a frame, from whatever draws.
245    ///
246    /// Without it the UI's idea of which pattern is playing would be a guess
247    /// that has to survive chain advances and quantized switches; with it,
248    /// the guess is only ever used before the first callback lands.
249    pub fn sync_from_audio(&mut self, status: &phosphor_core::project::PatternStatus) {
250        self.live = status.live_slot().min(SLOTS as u8 - 1);
251        self.pending = status.queued_slot().filter(|&s| (s as usize) < SLOTS);
252    }
253
254    /// The pattern-level settings, as a panel: label, value, and whether it
255    /// is meaningful for what this sequencer is driving.
256    ///
257    /// A placeholder for the sequencer's own controls until the grid view
258    /// exists — enough to see what a pattern is set to, and to prove the ops
259    /// reach it.
260    #[must_use]
261    pub fn panel_rows(&self) -> Vec<(&'static str, String)> {
262        let p = self.pattern();
263        vec![
264            ("slot", format!("{}", self.selected + 1)),
265            ("steps", format!("{}", p.step_count())),
266            ("rate", p.rate.label().to_string()),
267            ("swing", format!("{}%", p.swing)),
268            ("mode", p.mode.label().to_string()),
269            ("key", chords::note_name(p.tonic % 12).to_string()),
270            ("gate", format!("{}%", p.default_gate)),
271            ("accent", format!("{}", p.accent_vel)),
272            ("base", format!("{}", p.base_vel)),
273            ("switch", self.switch_quant.label().to_string()),
274            ("run", if self.playing { "yes".into() } else { "no".into() }),
275        ]
276    }
277}
278
279// ── The application model's side ──
280
281/// One pattern block on its way to the audio thread.
282///
283/// The app model produces these rather than sending them, because it has no
284/// channel and should not grow one: a frontend holds the sender and turns
285/// each of these into a command.
286#[derive(Debug, Clone, Copy, PartialEq)]
287pub struct PatternSync {
288    pub track_id: usize,
289    pub slot: u8,
290    pub block: PatternBlock,
291}
292
293impl PatternSync {
294    /// The command that carries this block.
295    #[must_use]
296    pub fn command(self) -> phosphor_core::mixer::MixerCommand {
297        phosphor_core::mixer::MixerCommand::SetPattern {
298            track_id: self.track_id,
299            slot: self.slot,
300            block: self.block,
301        }
302    }
303}
304
305impl crate::state::NavState {
306    /// Apply a sequencer op to the track under the cursor, and say what the
307    /// audio thread now needs.
308    ///
309    /// The two halves come back together on purpose: an edit that changed a
310    /// pattern and a caller that forgot to send it is a sequencer that plays
311    /// what it used to, which is the hardest kind of bug to see.
312    pub fn sequencer_op(&mut self, op: ops::SeqOp) -> (ops::SeqEffect, Vec<PatternSync>) {
313        let index = self.track_cursor;
314        let Some(track) = self.tracks.get_mut(index) else {
315            return (ops::SeqEffect::NOTHING, Vec::new());
316        };
317        let effect = ops::dispatch(track, op);
318        (effect, self.sequencer_syncs(index, effect))
319    }
320
321    /// The blocks an effect asks to be sent for one track.
322    #[must_use]
323    pub fn sequencer_syncs(&self, track_idx: usize, effect: ops::SeqEffect) -> Vec<PatternSync> {
324        let Some(track) = self.tracks.get(track_idx) else { return Vec::new() };
325        let (Some(state), Some(track_id)) = (track.sequencer.as_ref(), track.mixer_id) else {
326            return Vec::new();
327        };
328        effect
329            .slots()
330            .map(|slot| PatternSync { track_id, slot, block: state.block(slot as usize) })
331            .collect()
332    }
333
334    /// Every block a track has, for when the audio thread has none of them:
335    /// a track that has just been created, or one that has just been read
336    /// out of a session.
337    #[must_use]
338    pub fn all_sequencer_syncs(&self, track_idx: usize) -> Vec<PatternSync> {
339        self.sequencer_syncs(track_idx, ops::SeqEffect::all_slots())
340    }
341
342    /// Make the track under the cursor a sequencer track, and hand back the
343    /// eight blocks that have to reach the audio thread for it to play.
344    pub fn attach_sequencer(&mut self, state: SequencerState) -> Vec<PatternSync> {
345        let index = self.track_cursor;
346        if let Some(track) = self.tracks.get_mut(index) {
347            track.sequencer = Some(Box::new(state));
348        }
349        self.all_sequencer_syncs(index)
350    }
351
352    /// Take every sequencer's live slot, queued slot and playhead from the
353    /// audio thread. Called once a frame by whatever draws.
354    pub fn sync_sequencers_from_audio(&mut self) {
355        for track in &mut self.tracks {
356            if let (Some(state), Some(handle)) = (track.sequencer.as_mut(), track.handle.as_ref()) {
357                state.sync_from_audio(&handle.pattern);
358            }
359        }
360    }
361
362    /// Where the playhead is inside the pattern on a track, for the marker on
363    /// the step grid. `None` when the track has no sequencer running.
364    #[must_use]
365    pub fn sequencer_playhead(&self, track_idx: usize) -> Option<usize> {
366        let track = self.tracks.get(track_idx)?;
367        track.sequencer.as_ref()?;
368        let handle = track.handle.as_ref()?;
369        handle.pattern.is_running().then(|| handle.pattern.step() as usize)
370    }
371}
372
373// ── Session ──
374
375/// A sequencer as a session stores it.
376///
377/// Sparse: only the steps that are on are written, so a track with four hits
378/// on it is four lines of JSON rather than two thousand. Steps past the
379/// pattern's current length are stored too — shortening a pattern masks its
380/// tail rather than clearing it, and a session that dropped the masked steps
381/// would turn that into a truncation the next time it was opened.
382#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
383pub struct SessionSequencer {
384    /// The child instrument, by the same key a track stores.
385    pub child: String,
386    /// The child's panel, so a sequencer track restores the sound it had.
387    #[serde(default)]
388    pub child_params: Vec<f32>,
389    /// The child's selectors, by position. Same reasoning as
390    /// [`crate::session::SessionTrack::discrete`].
391    #[serde(default)]
392    pub discrete: Vec<SessionSelector>,
393    pub selected: u8,
394    pub live: u8,
395    /// Where the editor's cursor was. Not needed to play anything, and kept
396    /// because reopening a session on the step you were working on is the
397    /// difference between resuming and starting again.
398    #[serde(default)]
399    pub lane: u8,
400    #[serde(default)]
401    pub step: u8,
402    pub playing: bool,
403    /// [`SwitchQuant::index`].
404    pub switch_quant: u8,
405    /// `(slot, repeats)` per chain entry.
406    #[serde(default)]
407    pub chain: Vec<(u8, u8)>,
408    pub patterns: Vec<SessionPattern>,
409}
410
411/// One pattern slot on disk.
412#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
413pub struct SessionPattern {
414    pub steps: u8,
415    /// [`Rate::index`].
416    pub rate: u8,
417    pub swing: u8,
418    pub base_vel: u8,
419    pub accent_vel: u8,
420    pub default_gate: u8,
421    /// [`Mode::index`].
422    pub mode: u8,
423    pub tonic: u8,
424    pub lanes: Vec<SessionLane>,
425}
426
427/// One lane on disk.
428#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
429pub struct SessionLane {
430    /// The pinned drum voice, or 255 when the pitch comes from the step.
431    pub note: u8,
432    #[serde(default)]
433    pub muted: bool,
434    #[serde(default)]
435    pub soloed: bool,
436    /// Only the steps that are on.
437    #[serde(default)]
438    pub steps: Vec<SessionStep>,
439}
440
441/// One step that is switched on, and where.
442#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
443pub struct SessionStep {
444    pub index: u8,
445    pub octave: u8,
446    pub key: u8,
447    pub chord: u8,
448    pub voicing: u8,
449    pub accent: bool,
450    pub gate: u8,
451}
452
453impl SessionSequencer {
454    /// Write a sequencer track out.
455    #[must_use]
456    pub fn from_state(
457        state: &SequencerState,
458        child: InstrumentType,
459        child_params: &[f32],
460    ) -> Self {
461        Self {
462            child: instrument_key(child).to_string(),
463            child_params: child_params.to_vec(),
464            discrete: crate::session::selectors_of(child, child_params),
465            selected: state.selected,
466            live: state.live,
467            lane: state.lane,
468            step: state.step,
469            playing: state.playing,
470            switch_quant: state.switch_quant.index(),
471            chain: state.chain().iter().map(|e| (e.slot, e.repeats)).collect(),
472            patterns: state
473                .patterns
474                .iter()
475                .map(|block| SessionPattern {
476                    steps: block.steps,
477                    rate: block.rate.index(),
478                    swing: block.swing,
479                    base_vel: block.base_vel,
480                    accent_vel: block.accent_vel,
481                    default_gate: block.default_gate,
482                    mode: block.mode.index(),
483                    tonic: block.tonic,
484                    lanes: block
485                        .lanes
486                        .iter()
487                        .map(|lane| SessionLane {
488                            note: lane.note,
489                            muted: lane.muted,
490                            soloed: lane.soloed,
491                            steps: lane
492                                .steps
493                                .iter()
494                                .enumerate()
495                                .filter(|(_, step)| step.on)
496                                .map(|(index, step)| SessionStep {
497                                    index: index as u8,
498                                    octave: step.octave,
499                                    key: step.key,
500                                    chord: step.chord,
501                                    voicing: step.voicing,
502                                    accent: step.accent,
503                                    gate: step.gate,
504                                })
505                                .collect(),
506                        })
507                        .collect(),
508                })
509                .collect(),
510        }
511    }
512
513    /// The child this session named, or the default when the name is one this
514    /// build does not have — a track with no instrument at all is worse than
515    /// a track with the wrong one, and the patterns are still right.
516    #[must_use]
517    pub fn child_instrument(&self) -> InstrumentType {
518        parse_instrument_type(&self.child)
519            .filter(|i| !i.is_sequencer())
520            .unwrap_or(DEFAULT_CHILD)
521    }
522
523    /// The child's panel as it should be applied, with the stored selector
524    /// positions resolved against today's banks.
525    ///
526    /// Returns `None` when the saved block is a different length from the
527    /// instrument's, which means it is a different panel — the same rule
528    /// ordinary tracks follow, and for the same reason: copying it in slot by
529    /// slot would load every value into the wrong control.
530    #[must_use]
531    pub fn child_panel(&self, expected: usize) -> Option<Vec<f32>> {
532        if self.child_params.len() != expected {
533            return None;
534        }
535        let mut params = self.child_params.clone();
536        apply_selectors(self.child_instrument(), &mut params, &self.discrete);
537        Some(params)
538    }
539
540    /// Read a sequencer track back.
541    #[must_use]
542    pub fn to_state(&self) -> SequencerState {
543        let mut state = SequencerState::new(self.child_instrument());
544        state.selected = self.selected.min(SLOTS as u8 - 1);
545        state.live = self.live.min(SLOTS as u8 - 1);
546        state.lane = self.lane.min(LANES as u8 - 1);
547        state.step = self.step.min(MAX_STEPS as u8 - 1);
548        state.playing = self.playing;
549        state.switch_quant = SwitchQuant::from_index(self.switch_quant);
550        state.chain_len = self.chain.len().min(MAX_CHAIN) as u8;
551        for (entry, &(slot, repeats)) in state.chain.iter_mut().zip(&self.chain) {
552            *entry = ChainEntry { slot: slot.min(SLOTS as u8 - 1), repeats: repeats.max(1) };
553        }
554
555        for (block, stored) in state.patterns.iter_mut().zip(&self.patterns) {
556            block.steps = stored.steps;
557            block.rate = Rate::from_index(stored.rate);
558            block.swing = stored
559                .swing
560                .clamp(PatternBlock::MIN_SWING, PatternBlock::MAX_SWING);
561            block.base_vel = stored.base_vel;
562            block.accent_vel = stored.accent_vel;
563            block.default_gate = stored.default_gate;
564            block.mode = Mode::from_index(stored.mode);
565            block.tonic = stored.tonic % 12;
566            for (lane, stored_lane) in block.lanes.iter_mut().zip(&stored.lanes) {
567                lane.note = stored_lane.note;
568                lane.muted = stored_lane.muted;
569                lane.soloed = stored_lane.soloed;
570                for stored_step in &stored_lane.steps {
571                    let Some(step) = lane.steps.get_mut(stored_step.index as usize) else {
572                        continue;
573                    };
574                    step.on = true;
575                    step.octave = stored_step.octave;
576                    step.key = stored_step.key;
577                    step.chord = stored_step.chord;
578                    step.voicing = stored_step.voicing;
579                    step.accent = stored_step.accent;
580                    step.gate = stored_step.gate;
581                }
582            }
583        }
584        state
585    }
586}
587
588#[cfg(test)]
589mod tests {
590    use super::ops::{dispatch, SeqOp};
591    use super::*;
592    use crate::state::TrackState;
593    use phosphor_core::project::TrackKind;
594
595    pub(super) fn drum_track() -> TrackState {
596        let mut track = TrackState::new("seq", 0, false, TrackKind::Instrument, vec![]);
597        track.instrument_type = Some(InstrumentType::DrumRack);
598        track.synth_params = phosphor_dsp::drum_rack::PARAM_DEFAULTS.to_vec();
599        track.sequencer = Some(Box::new(SequencerState::new(InstrumentType::DrumRack)));
600        track
601    }
602
603    /// A drum child gets eight lanes pinned to eight voices; a melodic child
604    /// gets eight lanes that take their pitch from the steps. That one
605    /// difference is the whole of "what kind of pattern is this".
606    #[test]
607    fn a_drum_child_pins_the_lanes_and_a_melodic_one_does_not() {
608        let drums = SequencerState::new(InstrumentType::DrumRack);
609        for (lane, note) in drums.pattern().lanes.iter().zip(DEFAULT_DRUM_LANES) {
610            assert_eq!(lane.note, note);
611            assert!(!lane.is_pitched());
612        }
613
614        let keys = SequencerState::new(InstrumentType::Juno60);
615        assert!(keys.pattern().lanes.iter().all(Lane::is_pitched));
616    }
617
618    /// The track-level settings are held once and stamped on the way out, so
619    /// a block from any slot carries the same answer.
620    #[test]
621    fn every_block_carries_the_track_level_settings() {
622        let mut track = drum_track();
623        dispatch(&mut track, SeqOp::SetPlaying(true));
624        dispatch(&mut track, SeqOp::CycleSwitchQuant(1));
625        dispatch(&mut track, SeqOp::QueueSlot(3));
626
627        let state = track.sequencer.as_ref().unwrap();
628        for slot in 0..SLOTS {
629            let block = state.block(slot);
630            assert!(block.playing);
631            assert_eq!(block.switch_quant, SwitchQuant::Bar);
632            assert_eq!(block.pending_slot, Some(3));
633        }
634    }
635
636    /// Everything a player can set has to survive a save and a load. The
637    /// masked tail included: a pattern shortened to four steps still has the
638    /// other twenty-eight, and reopening the session must not be what erases
639    /// them.
640    #[test]
641    fn a_sequencer_round_trips_through_a_session() {
642        let mut track = drum_track();
643        for op in [
644            SeqOp::SelectSlot(2),
645            SeqOp::SelectLane(3),
646            SeqOp::SelectStep(20),
647            SeqOp::ToggleStep,
648            SeqOp::ToggleAccent,
649            SeqOp::NudgeGate(3),
650            SeqOp::CycleLength(-2),
651            SeqOp::CycleRate(1),
652            SeqOp::NudgeSwing(8),
653            SeqOp::CycleMode(2),
654            SeqOp::SetTonic(7),
655            SeqOp::ToggleLaneSolo,
656            SeqOp::PushChainEntry { slot: 2, repeats: 4 },
657            SeqOp::PushChainEntry { slot: 0, repeats: 1 },
658            SeqOp::SetPlaying(true),
659        ] {
660            dispatch(&mut track, op);
661        }
662        let before = *track.sequencer.clone().unwrap();
663
664        let stored = SessionSequencer::from_state(
665            &before,
666            InstrumentType::DrumRack,
667            &track.synth_params,
668        );
669        let json = serde_json::to_string(&stored).unwrap();
670        let read: SessionSequencer = serde_json::from_str(&json).unwrap();
671        let after = read.to_state();
672
673        assert_eq!(before, after, "a sequencer changed shape across a session");
674        assert_eq!(read.child_instrument(), InstrumentType::DrumRack);
675
676        // The child's panel comes back with its selectors resolved by
677        // position, which is what an ordinary track load does to them too: a
678        // stored fraction is only the patch it named while the bank is the
679        // size it was.
680        let mut expected = track.synth_params.clone();
681        crate::session::apply_selectors(InstrumentType::DrumRack, &mut expected, &stored.discrete);
682        assert_eq!(read.child_panel(expected.len()).unwrap(), expected);
683    }
684
685    /// The masked tail, specifically: a step past the end of a shortened
686    /// pattern is on disk and comes back on.
687    #[test]
688    fn a_masked_step_survives_a_session() {
689        let mut track = drum_track();
690        dispatch(&mut track, SeqOp::SelectStep(30));
691        dispatch(&mut track, SeqOp::ToggleStep);
692        dispatch(&mut track, SeqOp::CycleLength(-2)); // 16 -> 8 steps
693
694        let state = *track.sequencer.clone().unwrap();
695        assert_eq!(state.pattern().step_count(), 8);
696        let stored =
697            SessionSequencer::from_state(&state, InstrumentType::DrumRack, &track.synth_params);
698        let back = stored.to_state();
699        assert!(back.pattern().lanes[0].steps[30].on, "the masked step was lost");
700    }
701
702    /// A saved child this build does not have leaves the patterns intact and
703    /// falls back to a real instrument, rather than producing a track with
704    /// nothing in its plugin slot.
705    #[test]
706    fn an_unknown_child_falls_back_rather_than_failing() {
707        let state = SequencerState::new(InstrumentType::DrumRack);
708        let mut stored = SessionSequencer::from_state(&state, InstrumentType::DrumRack, &[]);
709        stored.child = "moog-model-d".into();
710        assert_eq!(stored.child_instrument(), DEFAULT_CHILD);
711        assert_eq!(stored.to_state().patterns.len(), SLOTS);
712    }
713
714    /// A panel of the wrong length is a different panel, not a panel with
715    /// missing values — the same rule ordinary tracks follow.
716    #[test]
717    fn a_child_panel_of_the_wrong_length_is_refused() {
718        let state = SequencerState::new(InstrumentType::DrumRack);
719        let stored =
720            SessionSequencer::from_state(&state, InstrumentType::DrumRack, &[0.1, 0.2, 0.3]);
721        assert!(stored.child_panel(3).is_some());
722        assert!(stored.child_panel(4).is_none());
723    }
724
725    // ── The application model's glue ──
726
727    use crate::state::{initial_tracks, NavState};
728
729    fn nav_with_sequencer() -> NavState {
730        let mut nav = NavState::new(initial_tracks());
731        let mut track = drum_track();
732        track.mixer_id = Some(7);
733        nav.tracks.insert(0, track);
734        nav.track_cursor = 0;
735        nav
736    }
737
738    /// An edit produces exactly the commands the audio thread is now missing,
739    /// addressed to the right track.
740    #[test]
741    fn an_edit_produces_the_command_that_carries_it() {
742        let mut nav = nav_with_sequencer();
743        let (effect, syncs) = nav.sequencer_op(SeqOp::SelectSlot(2));
744        assert!(effect.is_nothing());
745        assert!(syncs.is_empty(), "moving the cursor sent something");
746
747        let (_, syncs) = nav.sequencer_op(SeqOp::ToggleStep);
748        assert_eq!(syncs.len(), 1);
749        assert_eq!(syncs[0].track_id, 7);
750        assert_eq!(syncs[0].slot, 2);
751        assert!(syncs[0].block.lanes[0].steps[0].on);
752
753        // ...and the command it turns into names the same track and slot.
754        match syncs[0].command() {
755            phosphor_core::mixer::MixerCommand::SetPattern { track_id, slot, .. } => {
756                assert_eq!((track_id, slot), (7, 2));
757            }
758            _ => panic!("a pattern sync produced something else"),
759        }
760    }
761
762    /// A track that is not wired to the audio engine has nothing to send, and
763    /// a track with no sequencer has nothing to change.
764    #[test]
765    fn a_track_with_no_engine_or_no_sequencer_sends_nothing() {
766        let mut nav = nav_with_sequencer();
767        nav.tracks[0].mixer_id = None;
768        let (effect, syncs) = nav.sequencer_op(SeqOp::ToggleStep);
769        assert!(!effect.is_nothing(), "the edit still happened");
770        assert!(syncs.is_empty(), "a track with no mixer id sent a command");
771
772        nav.track_cursor = 1; // a bus track
773        let (effect, syncs) = nav.sequencer_op(SeqOp::ToggleStep);
774        assert!(effect.is_nothing());
775        assert!(syncs.is_empty());
776    }
777
778    /// A track that has just been created, or just been read out of a
779    /// session, needs all eight of its patterns sent.
780    #[test]
781    fn attaching_a_sequencer_sends_every_slot() {
782        let mut nav = NavState::new(initial_tracks());
783        let mut track = crate::state::TrackState::new(
784            "seq",
785            0,
786            false,
787            phosphor_core::project::TrackKind::Instrument,
788            vec![],
789        );
790        track.instrument_type = Some(InstrumentType::DrumRack);
791        track.mixer_id = Some(3);
792        nav.tracks.insert(0, track);
793        nav.track_cursor = 0;
794
795        let syncs = nav.attach_sequencer(SequencerState::new(InstrumentType::DrumRack));
796        assert_eq!(syncs.len(), SLOTS);
797        assert_eq!(
798            syncs.iter().map(|s| s.slot).collect::<Vec<_>>(),
799            (0..SLOTS as u8).collect::<Vec<_>>()
800        );
801        assert!(nav.tracks[0].sequencer.is_some());
802    }
803
804    /// The playhead the grid draws comes from the audio thread, and is
805    /// nothing at all when the pattern is not running.
806    #[test]
807    fn the_playhead_comes_from_the_audio_thread() {
808        let mut nav = nav_with_sequencer();
809        let handle = std::sync::Arc::new(phosphor_core::project::TrackHandle::new(
810            7,
811            phosphor_core::project::TrackKind::Instrument,
812        ));
813        nav.tracks[0].handle = Some(handle.clone());
814
815        assert_eq!(nav.sequencer_playhead(0), None);
816        handle.pattern.publish(1, Some(4), 11, true);
817        assert_eq!(nav.sequencer_playhead(0), Some(11));
818
819        nav.sync_sequencers_from_audio();
820        let state = nav.tracks[0].sequencer.as_ref().unwrap();
821        assert_eq!(state.live_slot(), 1);
822        assert_eq!(state.queued_slot(), Some(4));
823    }
824
825    /// The UI's mirror of which slot is playing comes from the audio thread,
826    /// because a chain advance and a quantized switch both happen there.
827    #[test]
828    fn the_live_slot_is_read_back_from_the_audio_thread() {
829        let status = phosphor_core::project::PatternStatus::new();
830        status.publish(5, Some(2), 9, true);
831
832        let mut state = SequencerState::new(InstrumentType::DrumRack);
833        state.sync_from_audio(&status);
834        assert_eq!(state.live_slot(), 5);
835        assert_eq!(state.queued_slot(), Some(2));
836
837        status.publish(2, None, 0, true);
838        state.sync_from_audio(&status);
839        assert_eq!(state.live_slot(), 2);
840        assert_eq!(state.queued_slot(), None, "a switch that happened was not noticed");
841    }
842
843    /// The countdown is arithmetic on the transport position, so it is the
844    /// same number the audio thread will act on rather than a guess about it.
845    #[test]
846    fn the_countdown_is_in_steps_to_the_switch() {
847        let mut track = drum_track();
848        dispatch(&mut track, SeqOp::QueueSlot(1));
849        let state = track.sequencer.as_ref().unwrap();
850        // A 16-step sixteenth pattern is 3840 ticks; from step 12 that is
851        // four steps to the end of it.
852        assert_eq!(state.countdown(2880), Some((1, 4)));
853        assert_eq!(state.countdown(3600), Some((1, 1)));
854    }
855}