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: false,
127 chain: [ChainEntry { slot: 0, repeats: 1 }; MAX_CHAIN],
128 chain_len: 0,
129 lane: 0,
130 step: 0,
131 step_record: false,
132 }
133 }
134
135 #[must_use]
138 pub fn selected_slot(&self) -> u8 {
139 self.selected
140 }
141
142 #[must_use]
143 pub fn live_slot(&self) -> u8 {
144 self.live
145 }
146
147 #[must_use]
148 pub fn queued_slot(&self) -> Option<u8> {
149 self.pending
150 }
151
152 #[must_use]
153 pub fn is_playing(&self) -> bool {
154 self.playing
155 }
156
157 #[must_use]
158 pub fn switch_quant(&self) -> SwitchQuant {
159 self.switch_quant
160 }
161
162 #[must_use]
163 pub fn is_step_recording(&self) -> bool {
164 self.step_record
165 }
166
167 #[must_use]
168 pub fn lane_cursor(&self) -> usize {
169 (self.lane as usize).min(LANES - 1)
170 }
171
172 #[must_use]
173 pub fn step_cursor(&self) -> usize {
174 (self.step as usize).min(MAX_STEPS - 1)
175 }
176
177 #[must_use]
179 pub fn pattern(&self) -> &PatternBlock {
180 &self.patterns[(self.selected as usize).min(SLOTS - 1)]
181 }
182
183 #[must_use]
186 pub fn pattern_at(&self, slot: usize) -> &PatternBlock {
187 &self.patterns[slot.min(SLOTS - 1)]
188 }
189
190 #[must_use]
192 pub fn lane(&self) -> &Lane {
193 &self.pattern().lanes[self.lane_cursor()]
194 }
195
196 #[must_use]
198 pub fn step(&self) -> &Step {
199 &self.lane().steps[self.step_cursor()]
200 }
201
202 #[must_use]
204 pub fn chain(&self) -> &[ChainEntry] {
205 &self.chain[..(self.chain_len as usize).min(MAX_CHAIN)]
206 }
207
208 #[must_use]
211 pub fn is_chained(&self) -> bool {
212 self.chain_len > 0
213 }
214
215 #[must_use]
221 pub fn block(&self, slot: usize) -> PatternBlock {
222 let mut block = self.patterns[slot.min(SLOTS - 1)];
223 block.playing = self.playing;
224 block.pending_slot = self.pending;
225 block.switch_quant = self.switch_quant;
226 block.chain = self.chain;
227 block.chain_len = self.chain_len;
228 block
229 }
230
231 #[must_use]
236 pub fn countdown(&self, position: i64) -> Option<(u8, i64)> {
237 let slot = self.pending?;
238 let live = self.pattern_at(self.live as usize);
239 let at = self.switch_quant.boundary(position, live.length_ticks());
240 Some((slot, (at - position).div_euclid(live.ticks_per_step())))
241 }
242
243 pub fn sync_from_audio(&mut self, status: &phosphor_core::project::PatternStatus) {
250 self.live = status.live_slot().min(SLOTS as u8 - 1);
251 self.pending = status.queued_slot().filter(|&s| (s as usize) < SLOTS);
252 }
253
254 #[must_use]
261 pub fn panel_rows(&self) -> Vec<(&'static str, String)> {
262 let p = self.pattern();
263 vec![
264 ("slot", format!("{}", self.selected + 1)),
265 ("steps", format!("{}", p.step_count())),
266 ("rate", p.rate.label().to_string()),
267 ("swing", format!("{}%", p.swing)),
268 ("mode", p.mode.label().to_string()),
269 ("key", chords::note_name(p.tonic % 12).to_string()),
270 ("gate", format!("{}%", p.default_gate)),
271 ("accent", format!("{}", p.accent_vel)),
272 ("base", format!("{}", p.base_vel)),
273 ("switch", self.switch_quant.label().to_string()),
274 ("run", if self.playing { "yes".into() } else { "no".into() }),
275 ]
276 }
277}
278
279#[derive(Debug, Clone, Copy, PartialEq)]
287pub struct PatternSync {
288 pub track_id: usize,
289 pub slot: u8,
290 pub block: PatternBlock,
291}
292
293impl PatternSync {
294 #[must_use]
296 pub fn command(self) -> phosphor_core::mixer::MixerCommand {
297 phosphor_core::mixer::MixerCommand::SetPattern {
298 track_id: self.track_id,
299 slot: self.slot,
300 block: self.block,
301 }
302 }
303}
304
305impl crate::state::NavState {
306 pub fn sequencer_op(&mut self, op: ops::SeqOp) -> (ops::SeqEffect, Vec<PatternSync>) {
313 let index = self.track_cursor;
314 let Some(track) = self.tracks.get_mut(index) else {
315 return (ops::SeqEffect::NOTHING, Vec::new());
316 };
317 let effect = ops::dispatch(track, op);
318 (effect, self.sequencer_syncs(index, effect))
319 }
320
321 #[must_use]
323 pub fn sequencer_syncs(&self, track_idx: usize, effect: ops::SeqEffect) -> Vec<PatternSync> {
324 let Some(track) = self.tracks.get(track_idx) else { return Vec::new() };
325 let (Some(state), Some(track_id)) = (track.sequencer.as_ref(), track.mixer_id) else {
326 return Vec::new();
327 };
328 effect
329 .slots()
330 .map(|slot| PatternSync { track_id, slot, block: state.block(slot as usize) })
331 .collect()
332 }
333
334 #[must_use]
338 pub fn all_sequencer_syncs(&self, track_idx: usize) -> Vec<PatternSync> {
339 self.sequencer_syncs(track_idx, ops::SeqEffect::all_slots())
340 }
341
342 pub fn attach_sequencer(&mut self, state: SequencerState) -> Vec<PatternSync> {
345 let index = self.track_cursor;
346 if let Some(track) = self.tracks.get_mut(index) {
347 track.sequencer = Some(Box::new(state));
348 }
349 self.all_sequencer_syncs(index)
350 }
351
352 pub fn sync_sequencers_from_audio(&mut self) {
355 for track in &mut self.tracks {
356 if let (Some(state), Some(handle)) = (track.sequencer.as_mut(), track.handle.as_ref()) {
357 state.sync_from_audio(&handle.pattern);
358 }
359 }
360 }
361
362 #[must_use]
365 pub fn sequencer_playhead(&self, track_idx: usize) -> Option<usize> {
366 let track = self.tracks.get(track_idx)?;
367 track.sequencer.as_ref()?;
368 let handle = track.handle.as_ref()?;
369 handle.pattern.is_running().then(|| handle.pattern.step() as usize)
370 }
371}
372
373#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
383pub struct SessionSequencer {
384 pub child: String,
386 #[serde(default)]
388 pub child_params: Vec<f32>,
389 #[serde(default)]
392 pub discrete: Vec<SessionSelector>,
393 pub selected: u8,
394 pub live: u8,
395 #[serde(default)]
399 pub lane: u8,
400 #[serde(default)]
401 pub step: u8,
402 pub playing: bool,
403 pub switch_quant: u8,
405 #[serde(default)]
407 pub chain: Vec<(u8, u8)>,
408 pub patterns: Vec<SessionPattern>,
409}
410
411#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
413pub struct SessionPattern {
414 pub steps: u8,
415 pub rate: u8,
417 pub swing: u8,
418 pub base_vel: u8,
419 pub accent_vel: u8,
420 pub default_gate: u8,
421 pub mode: u8,
423 pub tonic: u8,
424 pub lanes: Vec<SessionLane>,
425}
426
427#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
429pub struct SessionLane {
430 pub note: u8,
432 #[serde(default)]
433 pub muted: bool,
434 #[serde(default)]
435 pub soloed: bool,
436 #[serde(default)]
438 pub steps: Vec<SessionStep>,
439}
440
441#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
443pub struct SessionStep {
444 pub index: u8,
445 pub octave: u8,
446 pub key: u8,
447 pub chord: u8,
448 pub voicing: u8,
449 pub accent: bool,
450 pub gate: u8,
451}
452
453impl SessionSequencer {
454 #[must_use]
456 pub fn from_state(
457 state: &SequencerState,
458 child: InstrumentType,
459 child_params: &[f32],
460 ) -> Self {
461 Self {
462 child: instrument_key(child).to_string(),
463 child_params: child_params.to_vec(),
464 discrete: crate::session::selectors_of(child, child_params),
465 selected: state.selected,
466 live: state.live,
467 lane: state.lane,
468 step: state.step,
469 playing: state.playing,
470 switch_quant: state.switch_quant.index(),
471 chain: state.chain().iter().map(|e| (e.slot, e.repeats)).collect(),
472 patterns: state
473 .patterns
474 .iter()
475 .map(|block| SessionPattern {
476 steps: block.steps,
477 rate: block.rate.index(),
478 swing: block.swing,
479 base_vel: block.base_vel,
480 accent_vel: block.accent_vel,
481 default_gate: block.default_gate,
482 mode: block.mode.index(),
483 tonic: block.tonic,
484 lanes: block
485 .lanes
486 .iter()
487 .map(|lane| SessionLane {
488 note: lane.note,
489 muted: lane.muted,
490 soloed: lane.soloed,
491 steps: lane
492 .steps
493 .iter()
494 .enumerate()
495 .filter(|(_, step)| step.on)
496 .map(|(index, step)| SessionStep {
497 index: index as u8,
498 octave: step.octave,
499 key: step.key,
500 chord: step.chord,
501 voicing: step.voicing,
502 accent: step.accent,
503 gate: step.gate,
504 })
505 .collect(),
506 })
507 .collect(),
508 })
509 .collect(),
510 }
511 }
512
513 #[must_use]
517 pub fn child_instrument(&self) -> InstrumentType {
518 parse_instrument_type(&self.child)
519 .filter(|i| !i.is_sequencer())
520 .unwrap_or(DEFAULT_CHILD)
521 }
522
523 #[must_use]
531 pub fn child_panel(&self, expected: usize) -> Option<Vec<f32>> {
532 if self.child_params.len() != expected {
533 return None;
534 }
535 let mut params = self.child_params.clone();
536 apply_selectors(self.child_instrument(), &mut params, &self.discrete);
537 Some(params)
538 }
539
540 #[must_use]
542 pub fn to_state(&self) -> SequencerState {
543 let mut state = SequencerState::new(self.child_instrument());
544 state.selected = self.selected.min(SLOTS as u8 - 1);
545 state.live = self.live.min(SLOTS as u8 - 1);
546 state.lane = self.lane.min(LANES as u8 - 1);
547 state.step = self.step.min(MAX_STEPS as u8 - 1);
548 state.playing = self.playing;
549 state.switch_quant = SwitchQuant::from_index(self.switch_quant);
550 state.chain_len = self.chain.len().min(MAX_CHAIN) as u8;
551 for (entry, &(slot, repeats)) in state.chain.iter_mut().zip(&self.chain) {
552 *entry = ChainEntry { slot: slot.min(SLOTS as u8 - 1), repeats: repeats.max(1) };
553 }
554
555 for (block, stored) in state.patterns.iter_mut().zip(&self.patterns) {
556 block.steps = stored.steps;
557 block.rate = Rate::from_index(stored.rate);
558 block.swing = stored
559 .swing
560 .clamp(PatternBlock::MIN_SWING, PatternBlock::MAX_SWING);
561 block.base_vel = stored.base_vel;
562 block.accent_vel = stored.accent_vel;
563 block.default_gate = stored.default_gate;
564 block.mode = Mode::from_index(stored.mode);
565 block.tonic = stored.tonic % 12;
566 for (lane, stored_lane) in block.lanes.iter_mut().zip(&stored.lanes) {
567 lane.note = stored_lane.note;
568 lane.muted = stored_lane.muted;
569 lane.soloed = stored_lane.soloed;
570 for stored_step in &stored_lane.steps {
571 let Some(step) = lane.steps.get_mut(stored_step.index as usize) else {
572 continue;
573 };
574 step.on = true;
575 step.octave = stored_step.octave;
576 step.key = stored_step.key;
577 step.chord = stored_step.chord;
578 step.voicing = stored_step.voicing;
579 step.accent = stored_step.accent;
580 step.gate = stored_step.gate;
581 }
582 }
583 }
584 state
585 }
586}
587
588#[cfg(test)]
589mod tests {
590 use super::ops::{dispatch, SeqOp};
591 use super::*;
592 use crate::state::TrackState;
593 use phosphor_core::project::TrackKind;
594
595 pub(super) fn drum_track() -> TrackState {
596 let mut track = TrackState::new("seq", 0, false, TrackKind::Instrument, vec![]);
597 track.instrument_type = Some(InstrumentType::DrumRack);
598 track.synth_params = phosphor_dsp::drum_rack::PARAM_DEFAULTS.to_vec();
599 track.sequencer = Some(Box::new(SequencerState::new(InstrumentType::DrumRack)));
600 track
601 }
602
603 #[test]
607 fn a_drum_child_pins_the_lanes_and_a_melodic_one_does_not() {
608 let drums = SequencerState::new(InstrumentType::DrumRack);
609 for (lane, note) in drums.pattern().lanes.iter().zip(DEFAULT_DRUM_LANES) {
610 assert_eq!(lane.note, note);
611 assert!(!lane.is_pitched());
612 }
613
614 let keys = SequencerState::new(InstrumentType::Juno60);
615 assert!(keys.pattern().lanes.iter().all(Lane::is_pitched));
616 }
617
618 #[test]
621 fn every_block_carries_the_track_level_settings() {
622 let mut track = drum_track();
623 dispatch(&mut track, SeqOp::SetPlaying(true));
624 dispatch(&mut track, SeqOp::CycleSwitchQuant(1));
625 dispatch(&mut track, SeqOp::QueueSlot(3));
626
627 let state = track.sequencer.as_ref().unwrap();
628 for slot in 0..SLOTS {
629 let block = state.block(slot);
630 assert!(block.playing);
631 assert_eq!(block.switch_quant, SwitchQuant::Bar);
632 assert_eq!(block.pending_slot, Some(3));
633 }
634 }
635
636 #[test]
641 fn a_sequencer_round_trips_through_a_session() {
642 let mut track = drum_track();
643 for op in [
644 SeqOp::SelectSlot(2),
645 SeqOp::SelectLane(3),
646 SeqOp::SelectStep(20),
647 SeqOp::ToggleStep,
648 SeqOp::ToggleAccent,
649 SeqOp::NudgeGate(3),
650 SeqOp::CycleLength(-2),
651 SeqOp::CycleRate(1),
652 SeqOp::NudgeSwing(8),
653 SeqOp::CycleMode(2),
654 SeqOp::SetTonic(7),
655 SeqOp::ToggleLaneSolo,
656 SeqOp::PushChainEntry { slot: 2, repeats: 4 },
657 SeqOp::PushChainEntry { slot: 0, repeats: 1 },
658 SeqOp::SetPlaying(true),
659 ] {
660 dispatch(&mut track, op);
661 }
662 let before = *track.sequencer.clone().unwrap();
663
664 let stored = SessionSequencer::from_state(
665 &before,
666 InstrumentType::DrumRack,
667 &track.synth_params,
668 );
669 let json = serde_json::to_string(&stored).unwrap();
670 let read: SessionSequencer = serde_json::from_str(&json).unwrap();
671 let after = read.to_state();
672
673 assert_eq!(before, after, "a sequencer changed shape across a session");
674 assert_eq!(read.child_instrument(), InstrumentType::DrumRack);
675
676 let mut expected = track.synth_params.clone();
681 crate::session::apply_selectors(InstrumentType::DrumRack, &mut expected, &stored.discrete);
682 assert_eq!(read.child_panel(expected.len()).unwrap(), expected);
683 }
684
685 #[test]
688 fn a_masked_step_survives_a_session() {
689 let mut track = drum_track();
690 dispatch(&mut track, SeqOp::SelectStep(30));
691 dispatch(&mut track, SeqOp::ToggleStep);
692 dispatch(&mut track, SeqOp::CycleLength(-2)); let state = *track.sequencer.clone().unwrap();
695 assert_eq!(state.pattern().step_count(), 8);
696 let stored =
697 SessionSequencer::from_state(&state, InstrumentType::DrumRack, &track.synth_params);
698 let back = stored.to_state();
699 assert!(back.pattern().lanes[0].steps[30].on, "the masked step was lost");
700 }
701
702 #[test]
706 fn an_unknown_child_falls_back_rather_than_failing() {
707 let state = SequencerState::new(InstrumentType::DrumRack);
708 let mut stored = SessionSequencer::from_state(&state, InstrumentType::DrumRack, &[]);
709 stored.child = "moog-model-d".into();
710 assert_eq!(stored.child_instrument(), DEFAULT_CHILD);
711 assert_eq!(stored.to_state().patterns.len(), SLOTS);
712 }
713
714 #[test]
717 fn a_child_panel_of_the_wrong_length_is_refused() {
718 let state = SequencerState::new(InstrumentType::DrumRack);
719 let stored =
720 SessionSequencer::from_state(&state, InstrumentType::DrumRack, &[0.1, 0.2, 0.3]);
721 assert!(stored.child_panel(3).is_some());
722 assert!(stored.child_panel(4).is_none());
723 }
724
725 use crate::state::{initial_tracks, NavState};
728
729 fn nav_with_sequencer() -> NavState {
730 let mut nav = NavState::new(initial_tracks());
731 let mut track = drum_track();
732 track.mixer_id = Some(7);
733 nav.tracks.insert(0, track);
734 nav.track_cursor = 0;
735 nav
736 }
737
738 #[test]
741 fn an_edit_produces_the_command_that_carries_it() {
742 let mut nav = nav_with_sequencer();
743 let (effect, syncs) = nav.sequencer_op(SeqOp::SelectSlot(2));
744 assert!(effect.is_nothing());
745 assert!(syncs.is_empty(), "moving the cursor sent something");
746
747 let (_, syncs) = nav.sequencer_op(SeqOp::ToggleStep);
748 assert_eq!(syncs.len(), 1);
749 assert_eq!(syncs[0].track_id, 7);
750 assert_eq!(syncs[0].slot, 2);
751 assert!(syncs[0].block.lanes[0].steps[0].on);
752
753 match syncs[0].command() {
755 phosphor_core::mixer::MixerCommand::SetPattern { track_id, slot, .. } => {
756 assert_eq!((track_id, slot), (7, 2));
757 }
758 _ => panic!("a pattern sync produced something else"),
759 }
760 }
761
762 #[test]
765 fn a_track_with_no_engine_or_no_sequencer_sends_nothing() {
766 let mut nav = nav_with_sequencer();
767 nav.tracks[0].mixer_id = None;
768 let (effect, syncs) = nav.sequencer_op(SeqOp::ToggleStep);
769 assert!(!effect.is_nothing(), "the edit still happened");
770 assert!(syncs.is_empty(), "a track with no mixer id sent a command");
771
772 nav.track_cursor = 1; let (effect, syncs) = nav.sequencer_op(SeqOp::ToggleStep);
774 assert!(effect.is_nothing());
775 assert!(syncs.is_empty());
776 }
777
778 #[test]
781 fn attaching_a_sequencer_sends_every_slot() {
782 let mut nav = NavState::new(initial_tracks());
783 let mut track = crate::state::TrackState::new(
784 "seq",
785 0,
786 false,
787 phosphor_core::project::TrackKind::Instrument,
788 vec![],
789 );
790 track.instrument_type = Some(InstrumentType::DrumRack);
791 track.mixer_id = Some(3);
792 nav.tracks.insert(0, track);
793 nav.track_cursor = 0;
794
795 let syncs = nav.attach_sequencer(SequencerState::new(InstrumentType::DrumRack));
796 assert_eq!(syncs.len(), SLOTS);
797 assert_eq!(
798 syncs.iter().map(|s| s.slot).collect::<Vec<_>>(),
799 (0..SLOTS as u8).collect::<Vec<_>>()
800 );
801 assert!(nav.tracks[0].sequencer.is_some());
802 }
803
804 #[test]
807 fn the_playhead_comes_from_the_audio_thread() {
808 let mut nav = nav_with_sequencer();
809 let handle = std::sync::Arc::new(phosphor_core::project::TrackHandle::new(
810 7,
811 phosphor_core::project::TrackKind::Instrument,
812 ));
813 nav.tracks[0].handle = Some(handle.clone());
814
815 assert_eq!(nav.sequencer_playhead(0), None);
816 handle.pattern.publish(1, Some(4), 11, true);
817 assert_eq!(nav.sequencer_playhead(0), Some(11));
818
819 nav.sync_sequencers_from_audio();
820 let state = nav.tracks[0].sequencer.as_ref().unwrap();
821 assert_eq!(state.live_slot(), 1);
822 assert_eq!(state.queued_slot(), Some(4));
823 }
824
825 #[test]
828 fn the_live_slot_is_read_back_from_the_audio_thread() {
829 let status = phosphor_core::project::PatternStatus::new();
830 status.publish(5, Some(2), 9, true);
831
832 let mut state = SequencerState::new(InstrumentType::DrumRack);
833 state.sync_from_audio(&status);
834 assert_eq!(state.live_slot(), 5);
835 assert_eq!(state.queued_slot(), Some(2));
836
837 status.publish(2, None, 0, true);
838 state.sync_from_audio(&status);
839 assert_eq!(state.live_slot(), 2);
840 assert_eq!(state.queued_slot(), None, "a switch that happened was not noticed");
841 }
842
843 #[test]
846 fn the_countdown_is_in_steps_to_the_switch() {
847 let mut track = drum_track();
848 dispatch(&mut track, SeqOp::QueueSlot(1));
849 let state = track.sequencer.as_ref().unwrap();
850 assert_eq!(state.countdown(2880), Some((1, 4)));
853 assert_eq!(state.countdown(3600), Some((1, 1)));
854 }
855}