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