Skip to main content

phosphor_app/sequencer/
ops.rs

1//! Every edit a sequencer can be given, as one enum and one function.
2//!
3//! # Why this exists
4//!
5//! Because the keyboard is not going to be the only thing driving it. A step
6//! grid wants a box with sixteen buttons on it, and the day that box is
7//! plugged in, "toggle the step under the cursor" must not have to be written
8//! a second time against a MIDI note number. So the keys do not edit
9//! anything: they name a [`SeqOp`], and [`dispatch`] is the only code in the
10//! project that changes a pattern. A controller mapping is then a table from
11//! CC and note numbers to the same ops, and there is nothing for the two
12//! paths to disagree about.
13//!
14//! The same shape pays off twice over before any hardware arrives:
15//!
16//! * every edit is testable without a terminal, a mixer or an audio device —
17//!   the tests at the bottom of this file are the whole editor;
18//! * every edit reports what the audio thread now needs to be told, as a
19//!   [`SeqEffect`], so nothing can quietly change a pattern and forget to
20//!   send it.
21//!
22//! # The signature
23//!
24//! [`dispatch`] takes the whole [`TrackState`] rather than the
25//! [`SequencerState`] inside it, because two of the operations are about the
26//! track: changing the child instrument replaces what is in the plugin slot
27//! and reloads its panel. Passing the sequencer alone would mean those two
28//! had to live somewhere else, and then "every mutation goes through one
29//! function" would already not be true.
30
31use phosphor_core::pattern::{
32    ChainEntry, Chord, PatternBlock, Step, Voicing, LANES, MAX_CHAIN, MAX_CHORD_NOTES, MAX_STEPS,
33    SLOTS, STEP_COUNTS,
34};
35
36use super::{chords, is_drum_child, SequencerState, DEFAULT_DRUM_LANES};
37use crate::state::{InstrumentType, TrackState};
38
39/// Notes held at the moment a step was recorded.
40///
41/// Fixed size and `Copy` so that an op stays a plain value: five is what the
42/// widest chord in the table produces, and a sixth finger is not something
43/// the chord identifier could name anyway.
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
45pub struct HeldNotes {
46    notes: [u8; MAX_CHORD_NOTES],
47    len: u8,
48}
49
50impl HeldNotes {
51    /// The notes currently down, in any order. Anything past the fifth is
52    /// ignored.
53    #[must_use]
54    pub fn new(held: &[u8]) -> Self {
55        let mut notes = [0u8; MAX_CHORD_NOTES];
56        let len = held.len().min(MAX_CHORD_NOTES);
57        notes[..len].copy_from_slice(&held[..len]);
58        notes[..len].sort_unstable();
59        Self { notes, len: len as u8 }
60    }
61
62    #[must_use]
63    pub fn as_slice(&self) -> &[u8] {
64        &self.notes[..self.len as usize]
65    }
66}
67
68/// One edit.
69///
70/// Deliberately small and orthogonal: every entry is something a player does
71/// in one press, so that a key map and a controller map are both just tables.
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub enum SeqOp {
74    // ── Cursor ──
75    /// Look at a different pattern slot. Does not change what is playing.
76    SelectSlot(u8),
77    SelectLane(u8),
78    MoveLane(i8),
79    SelectStep(u8),
80    MoveStep(i8),
81
82    // ── The step under the cursor ──
83    ToggleStep,
84    SetStep(bool),
85    ClearStep,
86    ToggleAccent,
87    /// Move the pitch by semitones, or by scale degrees when the pattern is
88    /// in a mode.
89    NudgePitch(i8),
90    NudgeOctave(i8),
91    CycleChord(i8),
92    CycleVoicing(i8),
93    ToggleRootBelow,
94    /// Step the gate through the percentages, and off the end into the tie.
95    NudgeGate(i8),
96    ToggleTie,
97
98    // ── The lane under the cursor ──
99    ToggleLaneMute,
100    ToggleLaneSolo,
101    /// Pin this lane to a drum voice.
102    SetLaneNote(u8),
103
104    // ── The pattern under the editor ──
105    /// Step the length through [`STEP_COUNTS`]. Shortening masks.
106    CycleLength(i8),
107    CycleRate(i8),
108    NudgeSwing(i8),
109    NudgeBaseVelocity(i8),
110    NudgeAccentVelocity(i8),
111    /// Move the gate newly enabled steps inherit.
112    NudgeDefaultGate(i8),
113    CycleMode(i8),
114    SetTonic(u8),
115    ClearPattern,
116    CopyPattern { from: u8, to: u8 },
117
118    // ── The track ──
119    SetPlaying(bool),
120    TogglePlaying,
121    /// Queue a slot to take over at the next quantization point.
122    QueueSlot(u8),
123    ClearQueue,
124    CycleSwitchQuant(i8),
125
126    // ── The chain ──
127    PushChainEntry { slot: u8, repeats: u8 },
128    SetChainRepeats { index: u8, repeats: u8 },
129    RemoveChainEntry(u8),
130    ClearChain,
131
132    // ── The child instrument ──
133    SetChild(InstrumentType),
134
135    // ── Step record ──
136    ArmStepRecord(bool),
137    /// Write what is being held to the step under the cursor and move on.
138    RecordNotes(HeldNotes),
139    /// Leave the step as it is and move on.
140    RecordRest,
141    /// Tie the step before the cursor to whatever follows it.
142    RecordTie,
143}
144
145/// What the audio thread now has to be told.
146///
147/// Returned by every dispatch, so that a change to a pattern and the command
148/// that carries it cannot come apart: there is no path that edits without
149/// saying what it edited.
150#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
151pub struct SeqEffect {
152    /// A bit per slot whose block has to be sent.
153    ///
154    /// The track-level settings ride on any block, so a change to one of them
155    /// marks a single slot rather than all eight.
156    pub patterns: u8,
157    /// The child instrument changed: the plugin slot has to be replaced and
158    /// its whole panel resent.
159    pub child: bool,
160}
161
162impl SeqEffect {
163    /// Nothing to do.
164    pub const NOTHING: Self = Self { patterns: 0, child: false };
165
166    /// One slot has to be sent.
167    #[must_use]
168    pub const fn slot(slot: u8) -> Self {
169        Self { patterns: 1 << (slot & 0b111), child: false }
170    }
171
172    /// Every slot has to be sent.
173    #[must_use]
174    pub const fn all_slots() -> Self {
175        Self { patterns: 0xFF, child: false }
176    }
177
178    #[must_use]
179    pub const fn is_nothing(self) -> bool {
180        self.patterns == 0 && !self.child
181    }
182
183    /// Whether `slot` is one of the ones that has to be sent.
184    #[must_use]
185    pub const fn wants(self, slot: u8) -> bool {
186        self.patterns & (1 << (slot & 0b111)) != 0
187    }
188
189    /// The slots to send, in order.
190    pub fn slots(self) -> impl Iterator<Item = u8> {
191        (0..SLOTS as u8).filter(move |&slot| self.wants(slot))
192    }
193
194    /// Both effects, for a key that dispatches more than one op.
195    #[must_use]
196    pub fn and(self, other: Self) -> Self {
197        Self { patterns: self.patterns | other.patterns, child: self.child || other.child }
198    }
199}
200
201/// Apply one edit.
202///
203/// The single mutation surface. Everything a key, a menu or a controller can
204/// do to a sequencer arrives here, and nothing else in the project writes to
205/// a [`SequencerState`].
206///
207/// A track with no sequencer on it is not an error — the same key may be live
208/// on an ordinary track — so it returns [`SeqEffect::NOTHING`] and changes
209/// nothing.
210pub fn dispatch(track: &mut TrackState, op: SeqOp) -> SeqEffect {
211    // The child swap is the one operation that reaches outside the sequencer,
212    // so it is handled before the borrow that the rest of them share.
213    if let SeqOp::SetChild(child) = op {
214        return set_child(track, child);
215    }
216
217    let Some(state) = track.sequencer.as_mut() else {
218        return SeqEffect::NOTHING;
219    };
220    apply(state, op)
221}
222
223/// Replace what a sequencer track is driving.
224///
225/// The child is the track's own `instrument_type`, so this is a track edit as
226/// much as a sequencer one: the panel is reloaded from the new instrument's
227/// defaults, and the lanes are re-laid-out only when the *kind* of child
228/// changes. Swapping one drum machine for another leaves a kit pattern's
229/// lanes where the player put them.
230fn set_child(track: &mut TrackState, child: InstrumentType) -> SeqEffect {
231    if track.sequencer.is_none() || child.is_sequencer() {
232        return SeqEffect::NOTHING;
233    }
234    let previous = track.instrument_type;
235    if previous == Some(child) {
236        return SeqEffect::NOTHING;
237    }
238
239    track.instrument_type = Some(child);
240    track.synth_params = crate::preset::defaults(child);
241
242    let was_drums = previous.is_some_and(is_drum_child);
243    let effect = if was_drums == is_drum_child(child) {
244        SeqEffect::NOTHING
245    } else {
246        let state = track.sequencer.as_mut().expect("checked above");
247        relay_lanes(state, child);
248        SeqEffect::all_slots()
249    };
250    SeqEffect { child: true, ..effect }
251}
252
253/// Point every lane at a drum voice, or at its own steps, to match the child.
254fn relay_lanes(state: &mut SequencerState, child: InstrumentType) {
255    let drums = is_drum_child(child);
256    for slot in 0..SLOTS {
257        for (lane_index, lane) in state.patterns[slot].lanes.iter_mut().enumerate() {
258            lane.note = if drums {
259                DEFAULT_DRUM_LANES[lane_index]
260            } else {
261                phosphor_core::pattern::Lane::FROM_STEP
262            };
263        }
264    }
265}
266
267#[allow(clippy::too_many_lines)] // One arm per operation; splitting it would
268                                 // only move the list somewhere else.
269fn apply(state: &mut SequencerState, op: SeqOp) -> SeqEffect {
270    let selected = state.selected_slot();
271    let lane_index = state.lane_cursor();
272    let step_index = state.step_cursor();
273    let here = SeqEffect::slot(selected);
274
275    match op {
276        SeqOp::SetChild(_) => SeqEffect::NOTHING, // handled by `dispatch`
277
278        // ── Cursor ──
279        SeqOp::SelectSlot(slot) => {
280            state.selected = slot.min(SLOTS as u8 - 1);
281            SeqEffect::NOTHING
282        }
283        SeqOp::SelectLane(lane) => {
284            state.lane = lane.min(LANES as u8 - 1);
285            SeqEffect::NOTHING
286        }
287        SeqOp::MoveLane(delta) => {
288            state.lane = step_within(state.lane, delta, LANES as u8);
289            SeqEffect::NOTHING
290        }
291        SeqOp::SelectStep(step) => {
292            state.step = step.min(MAX_STEPS as u8 - 1);
293            SeqEffect::NOTHING
294        }
295        SeqOp::MoveStep(delta) => {
296            // Within the pattern's *current* length, so the cursor cannot
297            // walk off into the masked tail by accident. Reaching a masked
298            // step is what `SelectStep` is for.
299            let len = state.pattern().step_count() as u8;
300            state.step = wrap_within(state.step.min(len - 1), delta, len);
301            SeqEffect::NOTHING
302        }
303
304        // ── Step ──
305        SeqOp::ToggleStep => {
306            let on = !state.step().on;
307            set_step(state, lane_index, step_index, on);
308            here
309        }
310        SeqOp::SetStep(on) => {
311            set_step(state, lane_index, step_index, on);
312            here
313        }
314        SeqOp::ClearStep => {
315            let default_gate = state.pattern().default_gate;
316            let step = step_mut(state, lane_index, step_index);
317            *step = Step { gate: default_gate, ..Step::silent() };
318            here
319        }
320        SeqOp::ToggleAccent => {
321            let step = step_mut(state, lane_index, step_index);
322            step.accent = !step.accent;
323            here
324        }
325        SeqOp::NudgePitch(delta) => {
326            if state.lane().is_pitched() {
327                let (mode, tonic) = {
328                    let p = state.pattern();
329                    (p.mode, p.tonic)
330                };
331                let step = step_mut(state, lane_index, step_index);
332                let note = mode.walk(step.root(), tonic, i32::from(delta));
333                step.octave = note / 12;
334                step.key = note % 12;
335                here
336            } else {
337                SeqEffect::NOTHING
338            }
339        }
340        SeqOp::NudgeOctave(delta) => {
341            if state.lane().is_pitched() {
342                let step = step_mut(state, lane_index, step_index);
343                let note = (i32::from(step.root()) + i32::from(delta) * 12).clamp(0, 127) as u8;
344                step.octave = note / 12;
345                step.key = note % 12;
346                here
347            } else {
348                SeqEffect::NOTHING
349            }
350        }
351        SeqOp::CycleChord(delta) => {
352            if state.lane().is_pitched() {
353                let step = step_mut(state, lane_index, step_index);
354                step.chord = step.chord_kind().stepped(i32::from(delta)).index();
355                here
356            } else {
357                SeqEffect::NOTHING
358            }
359        }
360        SeqOp::CycleVoicing(delta) => {
361            if state.lane().is_pitched() {
362                let step = step_mut(state, lane_index, step_index);
363                let voicing = step.voicing_kind().stepped(i32::from(delta)).index();
364                step.voicing = voicing | (step.voicing & Step::ROOT_BELOW);
365                here
366            } else {
367                SeqEffect::NOTHING
368            }
369        }
370        SeqOp::ToggleRootBelow => {
371            if state.lane().is_pitched() {
372                let step = step_mut(state, lane_index, step_index);
373                step.voicing ^= Step::ROOT_BELOW;
374                here
375            } else {
376                SeqEffect::NOTHING
377            }
378        }
379        SeqOp::NudgeGate(delta) => {
380            let step = step_mut(state, lane_index, step_index);
381            step.gate = nudge_gate(step.gate, delta);
382            here
383        }
384        SeqOp::ToggleTie => {
385            let default_gate = state.pattern().default_gate;
386            let step = step_mut(state, lane_index, step_index);
387            step.gate = if step.gate == Step::TIE { default_gate } else { Step::TIE };
388            here
389        }
390
391        // ── Lane ──
392        SeqOp::ToggleLaneMute => {
393            let lane = &mut state.patterns[selected as usize].lanes[lane_index];
394            lane.muted = !lane.muted;
395            here
396        }
397        SeqOp::ToggleLaneSolo => {
398            let lane = &mut state.patterns[selected as usize].lanes[lane_index];
399            lane.soloed = !lane.soloed;
400            here
401        }
402        SeqOp::SetLaneNote(note) => {
403            let lane = &mut state.patterns[selected as usize].lanes[lane_index];
404            lane.note = note;
405            here
406        }
407
408        // ── Pattern ──
409        SeqOp::CycleLength(delta) => {
410            let block = &mut state.patterns[selected as usize];
411            let current = STEP_COUNTS
412                .iter()
413                .position(|&c| c == block.steps)
414                .unwrap_or(3) as i32;
415            let index = (current + i32::from(delta)).clamp(0, STEP_COUNTS.len() as i32 - 1);
416            block.steps = STEP_COUNTS[index as usize];
417            here
418        }
419        SeqOp::CycleRate(delta) => {
420            let block = &mut state.patterns[selected as usize];
421            block.rate = block.rate.stepped(i32::from(delta));
422            here
423        }
424        SeqOp::NudgeSwing(delta) => {
425            let block = &mut state.patterns[selected as usize];
426            block.swing = clamp_u8(
427                block.swing,
428                delta,
429                PatternBlock::MIN_SWING,
430                PatternBlock::MAX_SWING,
431            );
432            here
433        }
434        SeqOp::NudgeBaseVelocity(delta) => {
435            let block = &mut state.patterns[selected as usize];
436            block.base_vel = clamp_u8(block.base_vel, delta, 1, 127);
437            here
438        }
439        SeqOp::NudgeAccentVelocity(delta) => {
440            let block = &mut state.patterns[selected as usize];
441            block.accent_vel = clamp_u8(block.accent_vel, delta, 1, 127);
442            here
443        }
444        SeqOp::NudgeDefaultGate(delta) => {
445            // In fives, like the per-step gate: two controls that look the
446            // same and move at different rates is a control that feels broken.
447            let block = &mut state.patterns[selected as usize];
448            block.default_gate = clamp_u8(
449                block.default_gate,
450                delta.saturating_mul(GATE_STEP),
451                Step::MIN_GATE,
452                Step::MAX_GATE,
453            );
454            here
455        }
456        SeqOp::CycleMode(delta) => {
457            let block = &mut state.patterns[selected as usize];
458            block.mode = block.mode.stepped(i32::from(delta));
459            here
460        }
461        SeqOp::SetTonic(tonic) => {
462            let block = &mut state.patterns[selected as usize];
463            block.tonic = tonic % 12;
464            here
465        }
466        SeqOp::ClearPattern => {
467            // The lanes stay pointed where they were: clearing a kit pattern
468            // should leave the kit, not turn it into a keyboard.
469            let block = &mut state.patterns[selected as usize];
470            for lane in &mut block.lanes {
471                for step in &mut lane.steps {
472                    *step = Step { gate: block.default_gate, ..Step::silent() };
473                }
474            }
475            here
476        }
477        SeqOp::CopyPattern { from, to } => {
478            let from = (from as usize).min(SLOTS - 1);
479            let to = (to as usize).min(SLOTS - 1);
480            if from == to {
481                return SeqEffect::NOTHING;
482            }
483            state.patterns[to] = state.patterns[from];
484            SeqEffect::slot(to as u8)
485        }
486
487        // ── Track ──
488        SeqOp::SetPlaying(playing) => {
489            if state.playing == playing {
490                return SeqEffect::NOTHING;
491            }
492            state.playing = playing;
493            here
494        }
495        SeqOp::TogglePlaying => {
496            state.playing = !state.playing;
497            here
498        }
499        SeqOp::QueueSlot(slot) => {
500            let slot = slot.min(SLOTS as u8 - 1);
501            // A running chain owns the slot, and queueing against one would
502            // put a number on screen that nothing is ever going to act on.
503            if state.is_chained() || slot == state.live {
504                return SeqEffect::NOTHING;
505            }
506            state.pending = Some(slot);
507            here
508        }
509        SeqOp::ClearQueue => {
510            if state.pending.take().is_none() {
511                return SeqEffect::NOTHING;
512            }
513            here
514        }
515        SeqOp::CycleSwitchQuant(delta) => {
516            state.switch_quant = state.switch_quant.stepped(i32::from(delta));
517            here
518        }
519
520        // ── Chain ──
521        SeqOp::PushChainEntry { slot, repeats } => {
522            let len = state.chain_len as usize;
523            if len >= MAX_CHAIN {
524                return SeqEffect::NOTHING;
525            }
526            state.chain[len] =
527                ChainEntry { slot: slot.min(SLOTS as u8 - 1), repeats: repeats.max(1) };
528            state.chain_len += 1;
529            // A chain takes over from the queue outright.
530            state.pending = None;
531            here
532        }
533        SeqOp::SetChainRepeats { index, repeats } => {
534            let Some(entry) = state.chain.get_mut(index as usize) else {
535                return SeqEffect::NOTHING;
536            };
537            if index >= state.chain_len {
538                return SeqEffect::NOTHING;
539            }
540            entry.repeats = repeats.max(1);
541            here
542        }
543        SeqOp::RemoveChainEntry(index) => {
544            let len = state.chain_len as usize;
545            if index as usize >= len {
546                return SeqEffect::NOTHING;
547            }
548            for i in index as usize..len - 1 {
549                state.chain[i] = state.chain[i + 1];
550            }
551            state.chain_len -= 1;
552            here
553        }
554        SeqOp::ClearChain => {
555            if state.chain_len == 0 {
556                return SeqEffect::NOTHING;
557            }
558            state.chain_len = 0;
559            here
560        }
561
562        // ── Step record ──
563        SeqOp::ArmStepRecord(armed) => {
564            state.step_record = armed;
565            SeqEffect::NOTHING
566        }
567        SeqOp::RecordNotes(held) => {
568            if !state.step_record || held.as_slice().is_empty() {
569                return SeqEffect::NOTHING;
570            }
571            let pitched = state.lane().is_pitched();
572            let default_gate = state.pattern().default_gate;
573            let (mode, tonic) = {
574                let p = state.pattern();
575                (p.mode, p.tonic)
576            };
577
578            if pitched {
579                // What was played, named: the identifier walks the chord
580                // table looking for the set of notes that came in, so playing
581                // a first-inversion minor seventh stores exactly that rather
582                // than its lowest note.
583                let found = chords::identify(held.as_slice(), mode, tonic);
584                let step = step_mut(state, lane_index, step_index);
585                step.on = true;
586                step.gate = default_gate;
587                match found {
588                    Some(named) => {
589                        step.octave = named.root / 12;
590                        step.key = named.root % 12;
591                        step.chord = named.chord.index();
592                        step.voicing = named.voicing.index()
593                            | if named.root_below { Step::ROOT_BELOW } else { 0 };
594                    }
595                    None => {
596                        let root = held.as_slice()[0];
597                        step.octave = root / 12;
598                        step.key = root % 12;
599                        step.chord = Chord::None.index();
600                        step.voicing = Voicing::Close.index();
601                    }
602                }
603            } else {
604                // A drum lane's pitch is the lane's, so a played note picks
605                // the lane rather than setting a pitch: hitting the pad for
606                // the voice this lane is pinned to writes a hit.
607                let step = step_mut(state, lane_index, step_index);
608                step.on = true;
609                step.gate = default_gate;
610            }
611
612            advance_record_cursor(state);
613            here
614        }
615        SeqOp::RecordRest => {
616            if !state.step_record {
617                return SeqEffect::NOTHING;
618            }
619            advance_record_cursor(state);
620            SeqEffect::NOTHING
621        }
622        SeqOp::RecordTie => {
623            if !state.step_record {
624                return SeqEffect::NOTHING;
625            }
626            // The tie belongs to the step just written, which is the one
627            // behind the cursor.
628            let len = state.pattern().step_count() as u8;
629            let previous = wrap_within(state.step.min(len - 1), -1, len) as usize;
630            let step = step_mut(state, lane_index, previous);
631            step.gate = Step::TIE;
632            advance_record_cursor(state);
633            here
634        }
635    }
636}
637
638// ── Helpers ──
639
640fn step_mut(state: &mut SequencerState, lane: usize, step: usize) -> &mut Step {
641    let slot = state.selected as usize;
642    &mut state.patterns[slot].lanes[lane].steps[step]
643}
644
645/// Turn a step on or off. Enabling inherits the pattern's default gate, which
646/// is the one thing a newly written step needs and the one thing a player
647/// would otherwise have to set on every hit.
648fn set_step(state: &mut SequencerState, lane: usize, step_index: usize, on: bool) {
649    let default_gate = state.pattern().default_gate;
650    let step = step_mut(state, lane, step_index);
651    step.on = on;
652    if on {
653        step.gate = default_gate;
654    }
655}
656
657/// Move the record cursor one step on, wrapping at the pattern's length.
658fn advance_record_cursor(state: &mut SequencerState) {
659    let len = state.pattern().step_count() as u8;
660    state.step = wrap_within(state.step.min(len - 1), 1, len);
661}
662
663/// Move a cursor by `delta`, stopping at both ends.
664fn step_within(current: u8, delta: i8, count: u8) -> u8 {
665    (i32::from(current) + i32::from(delta)).clamp(0, i32::from(count) - 1) as u8
666}
667
668/// Move a cursor by `delta`, wrapping round.
669fn wrap_within(current: u8, delta: i8, count: u8) -> u8 {
670    let count = i32::from(count.max(1));
671    ((i32::from(current) + i32::from(delta)).rem_euclid(count)) as u8
672}
673
674/// How much one press moves a gate, in percent.
675const GATE_STEP: i8 = 5;
676
677fn clamp_u8(current: u8, delta: i8, low: u8, high: u8) -> u8 {
678    (i32::from(current) + i32::from(delta)).clamp(i32::from(low), i32::from(high)) as u8
679}
680
681/// The gate control, walked.
682///
683/// Percentages in fives, and off the top of the range into the tie — which is
684/// where a player looks for it, because "hold this note until the next one"
685/// is the longest gate there is rather than a mode somewhere else.
686fn nudge_gate(gate: u8, delta: i8) -> u8 {
687    if gate == Step::TIE {
688        return if delta < 0 { Step::MAX_GATE } else { Step::TIE };
689    }
690    let next = i32::from(gate) + i32::from(delta) * i32::from(GATE_STEP);
691    if next > i32::from(Step::MAX_GATE) {
692        return Step::TIE;
693    }
694    next.clamp(i32::from(Step::MIN_GATE), i32::from(Step::MAX_GATE)) as u8
695}
696
697#[cfg(test)]
698mod tests {
699    use super::super::tests::drum_track;
700    use super::*;
701    use crate::state::TrackState;
702    use phosphor_core::pattern::{Lane, Mode, Rate, SwitchQuant};
703    use phosphor_core::project::TrackKind;
704
705    fn melodic_track() -> TrackState {
706        let mut track = drum_track();
707        dispatch(&mut track, SeqOp::SetChild(InstrumentType::Juno60));
708        track
709    }
710
711    fn seq(track: &TrackState) -> &SequencerState {
712        track.sequencer.as_ref().unwrap()
713    }
714
715    /// An op on a track with no sequencer is not an error: the same key can
716    /// be live on an ordinary track, and a dispatch that panicked there would
717    /// make the key map a minefield.
718    #[test]
719    fn a_track_without_a_sequencer_ignores_the_ops() {
720        let mut track = TrackState::new("synth", 0, false, TrackKind::Instrument, vec![]);
721        assert_eq!(dispatch(&mut track, SeqOp::ToggleStep), SeqEffect::NOTHING);
722        assert!(track.sequencer.is_none());
723    }
724
725    /// Every edit says which slots the audio thread now needs, so nothing can
726    /// change a pattern and forget to send it.
727    #[test]
728    fn an_edit_reports_the_slot_it_changed() {
729        let mut track = drum_track();
730        dispatch(&mut track, SeqOp::SelectSlot(3));
731        let effect = dispatch(&mut track, SeqOp::ToggleStep);
732        assert!(effect.wants(3));
733        assert_eq!(effect.slots().collect::<Vec<_>>(), vec![3]);
734        assert!(!effect.wants(0));
735
736        // Moving the cursor changes nothing the audio thread can see.
737        assert!(dispatch(&mut track, SeqOp::MoveStep(1)).is_nothing());
738        assert!(dispatch(&mut track, SeqOp::SelectLane(2)).is_nothing());
739    }
740
741    #[test]
742    fn toggling_a_step_writes_the_patterns_default_gate() {
743        let mut track = drum_track();
744        dispatch(&mut track, SeqOp::NudgeDefaultGate(4)); // four presses of five
745        dispatch(&mut track, SeqOp::ToggleStep);
746        assert!(seq(&track).step().on);
747        assert_eq!(seq(&track).step().gate, 70);
748
749        dispatch(&mut track, SeqOp::ToggleStep);
750        assert!(!seq(&track).step().on);
751    }
752
753    /// The cursor walks the pattern that is there, and wraps at its end
754    /// rather than at thirty-two.
755    #[test]
756    fn the_step_cursor_wraps_at_the_patterns_length() {
757        let mut track = drum_track();
758        dispatch(&mut track, SeqOp::CycleLength(-2)); // 16 -> 8
759        dispatch(&mut track, SeqOp::SelectStep(7));
760        dispatch(&mut track, SeqOp::MoveStep(1));
761        assert_eq!(seq(&track).step_cursor(), 0);
762        dispatch(&mut track, SeqOp::MoveStep(-1));
763        assert_eq!(seq(&track).step_cursor(), 7);
764    }
765
766    /// Shortening a pattern hides its tail. The steps are still there, and
767    /// lengthening it brings them back — which is why a length change is a
768    /// mask and not an edit.
769    #[test]
770    fn shortening_a_pattern_masks_rather_than_clears() {
771        let mut track = drum_track();
772        dispatch(&mut track, SeqOp::SelectStep(30));
773        dispatch(&mut track, SeqOp::ToggleStep);
774        dispatch(&mut track, SeqOp::CycleLength(-3));
775        assert_eq!(seq(&track).pattern().step_count(), 4);
776        assert!(seq(&track).pattern().lanes[0].steps[30].on);
777        dispatch(&mut track, SeqOp::CycleLength(5));
778        assert_eq!(seq(&track).pattern().step_count(), 32);
779        assert!(seq(&track).pattern().lanes[0].steps[30].on);
780    }
781
782    /// Pitch is a lane property before it is a step property: a lane pinned
783    /// to the snare has no pitch to walk, and the key that would walk one
784    /// does nothing rather than something surprising.
785    #[test]
786    fn a_pinned_drum_lane_has_no_pitch_to_walk() {
787        let mut track = drum_track();
788        assert!(dispatch(&mut track, SeqOp::NudgePitch(1)).is_nothing());
789        assert!(dispatch(&mut track, SeqOp::CycleChord(1)).is_nothing());
790        assert_eq!(seq(&track).lane().note, DEFAULT_DRUM_LANES[0]);
791    }
792
793    /// In a mode the pitch control walks the scale, which is the whole point
794    /// of having one.
795    #[test]
796    fn pitch_walks_semitones_off_a_mode_and_degrees_on_one() {
797        let mut track = melodic_track();
798        let start = seq(&track).step().root();
799        dispatch(&mut track, SeqOp::NudgePitch(1));
800        assert_eq!(seq(&track).step().root(), start + 1, "chromatic is semitones");
801
802        dispatch(&mut track, SeqOp::CycleMode(1)); // Ionian
803        dispatch(&mut track, SeqOp::SetTonic(0));
804        dispatch(&mut track, SeqOp::NudgePitch(1));
805        assert_eq!(seq(&track).pattern().mode, Mode::Ionian);
806        assert_eq!(seq(&track).step().root(), 62, "C# up a degree in C major is D");
807    }
808
809    #[test]
810    fn octaves_move_by_twelve_and_stop_at_the_ends() {
811        let mut track = melodic_track();
812        dispatch(&mut track, SeqOp::NudgeOctave(1));
813        assert_eq!(seq(&track).step().root(), 72);
814        for _ in 0..20 {
815            dispatch(&mut track, SeqOp::NudgeOctave(1));
816        }
817        assert!(seq(&track).step().root() <= 127);
818        for _ in 0..40 {
819            dispatch(&mut track, SeqOp::NudgeOctave(-1));
820        }
821        assert!(seq(&track).step().root() < 12);
822    }
823
824    /// The gate walks up through the percentages and off the end into the
825    /// tie, and comes back the same way.
826    #[test]
827    fn the_gate_walks_off_the_top_into_the_tie() {
828        let mut track = drum_track();
829        for _ in 0..40 {
830            dispatch(&mut track, SeqOp::NudgeGate(1));
831        }
832        assert_eq!(seq(&track).step().gate, Step::TIE);
833        dispatch(&mut track, SeqOp::NudgeGate(-1));
834        assert_eq!(seq(&track).step().gate, Step::MAX_GATE);
835        for _ in 0..80 {
836            dispatch(&mut track, SeqOp::NudgeGate(-1));
837        }
838        assert_eq!(seq(&track).step().gate, Step::MIN_GATE);
839    }
840
841    /// Root-below composes with every voicing rather than being a fifth
842    /// entry in the list, so toggling it does not disturb the voicing.
843    #[test]
844    fn root_below_is_independent_of_the_voicing() {
845        let mut track = melodic_track();
846        dispatch(&mut track, SeqOp::CycleVoicing(1));
847        dispatch(&mut track, SeqOp::ToggleRootBelow);
848        assert_eq!(seq(&track).step().voicing_kind(), Voicing::Drop2);
849        assert!(seq(&track).step().root_below());
850        dispatch(&mut track, SeqOp::CycleVoicing(1));
851        assert_eq!(seq(&track).step().voicing_kind(), Voicing::First);
852        assert!(seq(&track).step().root_below(), "the voicing wiped the bass double");
853    }
854
855    /// Swapping one drum machine for another leaves the lanes where the
856    /// player put them; swapping a kit for a keyboard cannot.
857    #[test]
858    fn changing_the_child_relays_the_lanes_only_when_the_kind_changes() {
859        let mut track = drum_track();
860        dispatch(&mut track, SeqOp::SetLaneNote(75));
861        let effect = dispatch(&mut track, SeqOp::SetChild(InstrumentType::DrumRack));
862        assert!(effect.is_nothing(), "the same child is not a change");
863        assert_eq!(seq(&track).lane().note, 75);
864
865        let effect = dispatch(&mut track, SeqOp::SetChild(InstrumentType::Juno60));
866        assert!(effect.child);
867        assert_eq!(effect.patterns, 0xFF, "every slot's lanes moved");
868        assert!(seq(&track).lane().is_pitched());
869        assert_eq!(track.instrument_type, Some(InstrumentType::Juno60));
870        assert_eq!(
871            track.synth_params.len(),
872            crate::preset::param_count(InstrumentType::Juno60)
873        );
874
875        // ...and back again.
876        dispatch(&mut track, SeqOp::SetChild(InstrumentType::DrumRack));
877        assert_eq!(seq(&track).lane().note, DEFAULT_DRUM_LANES[0]);
878    }
879
880    /// A sequencer cannot become its own child.
881    #[test]
882    fn a_sequencer_cannot_drive_a_sequencer() {
883        let mut track = drum_track();
884        assert!(dispatch(&mut track, SeqOp::SetChild(InstrumentType::Sequencer)).is_nothing());
885        assert_eq!(track.instrument_type, Some(InstrumentType::DrumRack));
886    }
887
888    /// Queueing the slot that is already playing is not a switch — the audio
889    /// thread ignores it, and the UI should not be counting down to it.
890    #[test]
891    fn queueing_the_live_slot_is_not_a_queue() {
892        let mut track = drum_track();
893        assert!(dispatch(&mut track, SeqOp::QueueSlot(0)).is_nothing());
894        assert_eq!(seq(&track).queued_slot(), None);
895        assert!(!dispatch(&mut track, SeqOp::QueueSlot(4)).is_nothing());
896        assert_eq!(seq(&track).queued_slot(), Some(4));
897    }
898
899    /// A chain owns the slot, so a queue against one would be a number on
900    /// screen that nothing ever acts on.
901    #[test]
902    fn a_chain_takes_over_from_the_queue() {
903        let mut track = drum_track();
904        dispatch(&mut track, SeqOp::QueueSlot(2));
905        dispatch(&mut track, SeqOp::PushChainEntry { slot: 0, repeats: 2 });
906        assert_eq!(seq(&track).queued_slot(), None);
907        assert!(dispatch(&mut track, SeqOp::QueueSlot(3)).is_nothing());
908
909        dispatch(&mut track, SeqOp::ClearChain);
910        assert!(!seq(&track).is_chained());
911        assert!(!dispatch(&mut track, SeqOp::QueueSlot(3)).is_nothing());
912    }
913
914    #[test]
915    fn chain_entries_can_be_added_edited_and_removed() {
916        let mut track = drum_track();
917        dispatch(&mut track, SeqOp::PushChainEntry { slot: 0, repeats: 4 });
918        dispatch(&mut track, SeqOp::PushChainEntry { slot: 1, repeats: 0 });
919        dispatch(&mut track, SeqOp::PushChainEntry { slot: 2, repeats: 3 });
920        assert_eq!(
921            seq(&track).chain().iter().map(|e| (e.slot, e.repeats)).collect::<Vec<_>>(),
922            vec![(0, 4), (1, 1), (2, 3)],
923            "a repeat count of zero is one time through"
924        );
925
926        dispatch(&mut track, SeqOp::SetChainRepeats { index: 1, repeats: 8 });
927        dispatch(&mut track, SeqOp::RemoveChainEntry(0));
928        assert_eq!(
929            seq(&track).chain().iter().map(|e| (e.slot, e.repeats)).collect::<Vec<_>>(),
930            vec![(1, 8), (2, 3)]
931        );
932
933        assert!(dispatch(&mut track, SeqOp::RemoveChainEntry(9)).is_nothing());
934    }
935
936    /// Sixteen entries, and the seventeenth is refused rather than
937    /// overwriting one.
938    #[test]
939    fn the_chain_is_bounded() {
940        let mut track = drum_track();
941        for _ in 0..MAX_CHAIN + 4 {
942            dispatch(&mut track, SeqOp::PushChainEntry { slot: 1, repeats: 1 });
943        }
944        assert_eq!(seq(&track).chain().len(), MAX_CHAIN);
945    }
946
947    #[test]
948    fn copying_a_pattern_marks_the_slot_it_landed_in() {
949        let mut track = drum_track();
950        dispatch(&mut track, SeqOp::ToggleStep);
951        let effect = dispatch(&mut track, SeqOp::CopyPattern { from: 0, to: 5 });
952        assert_eq!(effect.slots().collect::<Vec<_>>(), vec![5]);
953        assert!(seq(&track).pattern_at(5).lanes[0].steps[0].on);
954        assert!(dispatch(&mut track, SeqOp::CopyPattern { from: 3, to: 3 }).is_nothing());
955    }
956
957    /// Clearing a kit pattern leaves it a kit pattern.
958    #[test]
959    fn clearing_a_pattern_leaves_the_lanes_pointed_where_they_were() {
960        let mut track = drum_track();
961        dispatch(&mut track, SeqOp::ToggleStep);
962        dispatch(&mut track, SeqOp::ClearPattern);
963        assert!(!seq(&track).step().on);
964        assert_eq!(seq(&track).lane().note, DEFAULT_DRUM_LANES[0]);
965    }
966
967    // ── Step record ──
968
969    /// The thing the sequencer is unusable without: playing a key writes the
970    /// pitch and moves on, so entering a line takes as long as playing it.
971    #[test]
972    fn step_record_writes_a_note_and_advances() {
973        let mut track = melodic_track();
974        // Nothing happens until it is armed.
975        assert!(dispatch(&mut track, SeqOp::RecordNotes(HeldNotes::new(&[64]))).is_nothing());
976
977        dispatch(&mut track, SeqOp::ArmStepRecord(true));
978        dispatch(&mut track, SeqOp::RecordNotes(HeldNotes::new(&[64])));
979        assert!(seq(&track).pattern().lanes[0].steps[0].on);
980        assert_eq!(seq(&track).pattern().lanes[0].steps[0].root(), 64);
981        assert_eq!(seq(&track).step_cursor(), 1);
982
983        // A rest moves on without writing.
984        dispatch(&mut track, SeqOp::RecordRest);
985        assert!(!seq(&track).pattern().lanes[0].steps[1].on);
986        assert_eq!(seq(&track).step_cursor(), 2);
987    }
988
989    /// Several keys at once are a chord, and they are stored as one — root,
990    /// quality and voicing — rather than as the lowest note played.
991    #[test]
992    fn step_record_names_the_chord_that_was_played() {
993        let mut track = melodic_track();
994        dispatch(&mut track, SeqOp::ArmStepRecord(true));
995        dispatch(&mut track, SeqOp::RecordNotes(HeldNotes::new(&[60, 63, 67, 70])));
996
997        let step = seq(&track).pattern().lanes[0].steps[0];
998        assert_eq!(step.root(), 60);
999        assert_eq!(step.chord_kind(), Chord::Min7);
1000        assert_eq!(step.voicing_kind(), Voicing::Close);
1001    }
1002
1003    /// A tie extends the step that was just written, which is the one behind
1004    /// the cursor.
1005    #[test]
1006    fn step_record_ties_the_step_behind_the_cursor() {
1007        let mut track = melodic_track();
1008        dispatch(&mut track, SeqOp::ArmStepRecord(true));
1009        dispatch(&mut track, SeqOp::RecordNotes(HeldNotes::new(&[60])));
1010        dispatch(&mut track, SeqOp::RecordTie);
1011        assert_eq!(seq(&track).pattern().lanes[0].steps[0].gate, Step::TIE);
1012        assert_eq!(seq(&track).step_cursor(), 2);
1013    }
1014
1015    /// A drum lane has no pitch, so a played pad writes a hit on the lane it
1016    /// belongs to rather than retuning it.
1017    #[test]
1018    fn step_record_on_a_drum_lane_writes_a_hit() {
1019        let mut track = drum_track();
1020        dispatch(&mut track, SeqOp::ArmStepRecord(true));
1021        dispatch(&mut track, SeqOp::RecordNotes(HeldNotes::new(&[38])));
1022        assert!(seq(&track).pattern().lanes[0].steps[0].on);
1023        assert_eq!(seq(&track).pattern().lanes[0].steps[0].octave, Step::silent().octave);
1024        assert_eq!(seq(&track).lane().note, DEFAULT_DRUM_LANES[0]);
1025    }
1026
1027    /// A held chord with no name in the table still writes something: the
1028    /// note the player would expect to hear, rather than nothing at all.
1029    #[test]
1030    fn an_unnameable_chord_falls_back_to_its_lowest_note() {
1031        let mut track = melodic_track();
1032        dispatch(&mut track, SeqOp::ArmStepRecord(true));
1033        dispatch(&mut track, SeqOp::RecordNotes(HeldNotes::new(&[60, 61, 62])));
1034        let step = seq(&track).pattern().lanes[0].steps[0];
1035        assert_eq!(step.root(), 60);
1036        assert_eq!(step.chord_kind(), Chord::None);
1037    }
1038
1039    #[test]
1040    fn held_notes_are_bounded_and_sorted() {
1041        let held = HeldNotes::new(&[67, 60, 64, 72, 76, 79, 83]);
1042        assert_eq!(held.as_slice(), &[60, 64, 67, 72, 76]);
1043        assert!(HeldNotes::new(&[]).as_slice().is_empty());
1044    }
1045
1046    /// A lane with no name of its own is a lane that takes pitch from its
1047    /// steps; setting a note pins it.
1048    #[test]
1049    fn a_lane_can_be_pinned_and_unpinned() {
1050        let mut track = melodic_track();
1051        assert!(seq(&track).lane().is_pitched());
1052        dispatch(&mut track, SeqOp::SetLaneNote(42));
1053        assert!(!seq(&track).lane().is_pitched());
1054        dispatch(&mut track, SeqOp::SetLaneNote(Lane::FROM_STEP));
1055        assert!(seq(&track).lane().is_pitched());
1056    }
1057
1058    #[test]
1059    fn mute_and_solo_are_per_lane() {
1060        let mut track = drum_track();
1061        dispatch(&mut track, SeqOp::SelectLane(2));
1062        dispatch(&mut track, SeqOp::ToggleLaneMute);
1063        assert!(seq(&track).pattern().lanes[2].muted);
1064        assert!(!seq(&track).pattern().lanes[0].muted);
1065        dispatch(&mut track, SeqOp::ToggleLaneSolo);
1066        assert!(seq(&track).pattern().lanes[2].soloed);
1067    }
1068
1069    /// Everything with a range has one at both ends, and no sequence of
1070    /// presses can put a value outside it.
1071    #[test]
1072    fn every_control_stops_at_its_ends() {
1073        let mut track = drum_track();
1074        for _ in 0..200 {
1075            dispatch(&mut track, SeqOp::NudgeSwing(1));
1076            dispatch(&mut track, SeqOp::NudgeBaseVelocity(1));
1077            dispatch(&mut track, SeqOp::NudgeAccentVelocity(1));
1078            dispatch(&mut track, SeqOp::NudgeDefaultGate(1));
1079            dispatch(&mut track, SeqOp::CycleRate(1));
1080            dispatch(&mut track, SeqOp::NudgeGate(1));
1081            dispatch(&mut track, SeqOp::CycleLength(1));
1082            dispatch(&mut track, SeqOp::CycleMode(1));
1083            dispatch(&mut track, SeqOp::CycleSwitchQuant(1));
1084        }
1085        {
1086            let block = seq(&track).pattern();
1087            assert_eq!(block.swing, PatternBlock::MAX_SWING);
1088            assert_eq!(block.base_vel, 127);
1089            assert_eq!(block.accent_vel, 127);
1090            assert_eq!(block.default_gate, Step::MAX_GATE);
1091            assert_eq!(block.rate, Rate::SixteenthTriplet);
1092            assert_eq!(block.steps, 32);
1093            assert_eq!(block.mode, Mode::Locrian);
1094        }
1095        assert_eq!(seq(&track).switch_quant(), SwitchQuant::Immediate);
1096
1097        for _ in 0..200 {
1098            dispatch(&mut track, SeqOp::NudgeSwing(-1));
1099            dispatch(&mut track, SeqOp::NudgeBaseVelocity(-1));
1100            dispatch(&mut track, SeqOp::NudgeAccentVelocity(-1));
1101            dispatch(&mut track, SeqOp::NudgeDefaultGate(-1));
1102            dispatch(&mut track, SeqOp::CycleRate(-1));
1103            dispatch(&mut track, SeqOp::CycleLength(-1));
1104            dispatch(&mut track, SeqOp::CycleMode(-1));
1105            dispatch(&mut track, SeqOp::CycleSwitchQuant(-1));
1106        }
1107        {
1108            let block = seq(&track).pattern();
1109            assert_eq!(block.swing, PatternBlock::MIN_SWING);
1110            assert_eq!(block.base_vel, 1);
1111            assert_eq!(block.default_gate, Step::MIN_GATE);
1112            assert_eq!(block.rate, Rate::Quarter);
1113            assert_eq!(block.steps, 4);
1114            assert_eq!(block.mode, Mode::Chromatic);
1115        }
1116        assert_eq!(seq(&track).switch_quant(), SwitchQuant::PatternEnd);
1117    }
1118
1119    /// An index out of range is clamped rather than panicking. Every one of
1120    /// these can arrive from a controller mapping.
1121    #[test]
1122    fn out_of_range_indices_are_clamped() {
1123        let mut track = drum_track();
1124        dispatch(&mut track, SeqOp::SelectSlot(200));
1125        dispatch(&mut track, SeqOp::SelectLane(200));
1126        dispatch(&mut track, SeqOp::SelectStep(200));
1127        dispatch(&mut track, SeqOp::QueueSlot(200));
1128        dispatch(&mut track, SeqOp::SetTonic(200));
1129        dispatch(&mut track, SeqOp::CopyPattern { from: 200, to: 201 });
1130        let state = seq(&track);
1131        assert_eq!(state.selected_slot(), SLOTS as u8 - 1);
1132        assert_eq!(state.lane_cursor(), LANES - 1);
1133        assert_eq!(state.step_cursor(), MAX_STEPS - 1);
1134        assert_eq!(state.queued_slot(), Some(SLOTS as u8 - 1));
1135        assert!(state.pattern().tonic < 12);
1136    }
1137}