1pub 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
55pub const DEFAULT_CHILD: InstrumentType = InstrumentType::DrumRack;
62
63pub const DEFAULT_DRUM_LANES: [u8; LANES] = [36, 38, 42, 46, 39, 41, 45, 48];
69
70pub const DEFAULT_DRUM_LABELS: [&str; LANES] = ["BD", "SD", "CH", "OH", "CP", "LT", "MT", "HT"];
72
73#[must_use]
79pub const fn is_drum_child(child: InstrumentType) -> bool {
80 matches!(child, InstrumentType::DrumRack)
81}
82
83#[derive(Debug, Clone, PartialEq)]
87pub struct SequencerState {
88 patterns: [PatternBlock; SLOTS],
90 selected: u8,
92 live: u8,
95 pending: Option<u8>,
97 switch_quant: SwitchQuant,
98 playing: bool,
99 chain: [ChainEntry; MAX_CHAIN],
100 chain_len: u8,
101 lane: u8,
103 step: u8,
105 step_record: bool,
107}
108
109impl SequencerState {
110 #[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: 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 #[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 #[must_use]
182 pub fn pattern(&self) -> &PatternBlock {
183 &self.patterns[(self.selected as usize).min(SLOTS - 1)]
184 }
185
186 #[must_use]
189 pub fn pattern_at(&self, slot: usize) -> &PatternBlock {
190 &self.patterns[slot.min(SLOTS - 1)]
191 }
192
193 #[must_use]
195 pub fn lane(&self) -> &Lane {
196 &self.pattern().lanes[self.lane_cursor()]
197 }
198
199 #[must_use]
201 pub fn step(&self) -> &Step {
202 &self.lane().steps[self.step_cursor()]
203 }
204
205 #[must_use]
207 pub fn chain(&self) -> &[ChainEntry] {
208 &self.chain[..(self.chain_len as usize).min(MAX_CHAIN)]
209 }
210
211 #[must_use]
214 pub fn is_chained(&self) -> bool {
215 self.chain_len > 0
216 }
217
218 #[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 #[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 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 #[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#[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 #[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 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 #[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 #[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 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 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 #[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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
386pub struct SessionSequencer {
387 pub child: String,
389 #[serde(default)]
391 pub child_params: Vec<f32>,
392 #[serde(default)]
395 pub discrete: Vec<SessionSelector>,
396 pub selected: u8,
397 pub live: u8,
398 #[serde(default)]
402 pub lane: u8,
403 #[serde(default)]
404 pub step: u8,
405 pub playing: bool,
406 pub switch_quant: u8,
408 #[serde(default)]
410 pub chain: Vec<(u8, u8)>,
411 pub patterns: Vec<SessionPattern>,
412}
413
414#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
416pub struct SessionPattern {
417 pub steps: u8,
418 pub rate: u8,
420 pub swing: u8,
421 pub base_vel: u8,
422 pub accent_vel: u8,
423 pub default_gate: u8,
424 pub mode: u8,
426 pub tonic: u8,
427 pub lanes: Vec<SessionLane>,
428}
429
430#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
432pub struct SessionLane {
433 pub note: u8,
435 #[serde(default)]
436 pub muted: bool,
437 #[serde(default)]
438 pub soloed: bool,
439 #[serde(default)]
441 pub steps: Vec<SessionStep>,
442}
443
444#[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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 #[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)); 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 #[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 #[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 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 #[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 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 #[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; let (effect, syncs) = nav.sequencer_op(SeqOp::ToggleStep);
777 assert!(effect.is_nothing());
778 assert!(syncs.is_empty());
779 }
780
781 #[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 #[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 #[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 #[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 assert_eq!(state.countdown(2880), Some((1, 4)));
856 assert_eq!(state.countdown(3600), Some((1, 1)));
857 }
858}