pub mod chords;
pub mod compile;
pub mod ops;
use serde::{Deserialize, Serialize};
use phosphor_core::pattern::{
ChainEntry, Lane, Mode, PatternBlock, Rate, Step, SwitchQuant, LANES, MAX_CHAIN, MAX_STEPS,
SLOTS,
};
use crate::session::{apply_selectors, instrument_key, parse_instrument_type, SessionSelector};
use crate::state::InstrumentType;
pub const DEFAULT_CHILD: InstrumentType = InstrumentType::DrumRack;
pub const DEFAULT_DRUM_LANES: [u8; LANES] = [36, 38, 42, 46, 39, 41, 45, 48];
pub const DEFAULT_DRUM_LABELS: [&str; LANES] = ["BD", "SD", "CH", "OH", "CP", "LT", "MT", "HT"];
#[must_use]
pub const fn is_drum_child(child: InstrumentType) -> bool {
matches!(child, InstrumentType::DrumRack)
}
#[derive(Debug, Clone, PartialEq)]
pub struct SequencerState {
patterns: [PatternBlock; SLOTS],
selected: u8,
live: u8,
pending: Option<u8>,
switch_quant: SwitchQuant,
playing: bool,
chain: [ChainEntry; MAX_CHAIN],
chain_len: u8,
lane: u8,
step: u8,
step_record: bool,
}
impl SequencerState {
#[must_use]
pub fn new(child: InstrumentType) -> Self {
let mut blank = PatternBlock::empty();
if is_drum_child(child) {
for (lane, note) in blank.lanes.iter_mut().zip(DEFAULT_DRUM_LANES) {
*lane = Lane::drum(note);
}
}
Self {
patterns: [blank; SLOTS],
selected: 0,
live: 0,
pending: None,
switch_quant: SwitchQuant::PatternEnd,
playing: false,
chain: [ChainEntry { slot: 0, repeats: 1 }; MAX_CHAIN],
chain_len: 0,
lane: 0,
step: 0,
step_record: false,
}
}
#[must_use]
pub fn selected_slot(&self) -> u8 {
self.selected
}
#[must_use]
pub fn live_slot(&self) -> u8 {
self.live
}
#[must_use]
pub fn queued_slot(&self) -> Option<u8> {
self.pending
}
#[must_use]
pub fn is_playing(&self) -> bool {
self.playing
}
#[must_use]
pub fn switch_quant(&self) -> SwitchQuant {
self.switch_quant
}
#[must_use]
pub fn is_step_recording(&self) -> bool {
self.step_record
}
#[must_use]
pub fn lane_cursor(&self) -> usize {
(self.lane as usize).min(LANES - 1)
}
#[must_use]
pub fn step_cursor(&self) -> usize {
(self.step as usize).min(MAX_STEPS - 1)
}
#[must_use]
pub fn pattern(&self) -> &PatternBlock {
&self.patterns[(self.selected as usize).min(SLOTS - 1)]
}
#[must_use]
pub fn pattern_at(&self, slot: usize) -> &PatternBlock {
&self.patterns[slot.min(SLOTS - 1)]
}
#[must_use]
pub fn lane(&self) -> &Lane {
&self.pattern().lanes[self.lane_cursor()]
}
#[must_use]
pub fn step(&self) -> &Step {
&self.lane().steps[self.step_cursor()]
}
#[must_use]
pub fn chain(&self) -> &[ChainEntry] {
&self.chain[..(self.chain_len as usize).min(MAX_CHAIN)]
}
#[must_use]
pub fn is_chained(&self) -> bool {
self.chain_len > 0
}
#[must_use]
pub fn block(&self, slot: usize) -> PatternBlock {
let mut block = self.patterns[slot.min(SLOTS - 1)];
block.playing = self.playing;
block.pending_slot = self.pending;
block.switch_quant = self.switch_quant;
block.chain = self.chain;
block.chain_len = self.chain_len;
block
}
#[must_use]
pub fn countdown(&self, position: i64) -> Option<(u8, i64)> {
let slot = self.pending?;
let live = self.pattern_at(self.live as usize);
let at = self.switch_quant.boundary(position, live.length_ticks());
Some((slot, (at - position).div_euclid(live.ticks_per_step())))
}
pub fn sync_from_audio(&mut self, status: &phosphor_core::project::PatternStatus) {
self.live = status.live_slot().min(SLOTS as u8 - 1);
self.pending = status.queued_slot().filter(|&s| (s as usize) < SLOTS);
}
#[must_use]
pub fn panel_rows(&self) -> Vec<(&'static str, String)> {
let p = self.pattern();
vec![
("slot", format!("{}", self.selected + 1)),
("steps", format!("{}", p.step_count())),
("rate", p.rate.label().to_string()),
("swing", format!("{}%", p.swing)),
("mode", p.mode.label().to_string()),
("key", chords::note_name(p.tonic % 12).to_string()),
("gate", format!("{}%", p.default_gate)),
("accent", format!("{}", p.accent_vel)),
("base", format!("{}", p.base_vel)),
("switch", self.switch_quant.label().to_string()),
("run", if self.playing { "yes".into() } else { "no".into() }),
]
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct PatternSync {
pub track_id: usize,
pub slot: u8,
pub block: PatternBlock,
}
impl PatternSync {
#[must_use]
pub fn command(self) -> phosphor_core::mixer::MixerCommand {
phosphor_core::mixer::MixerCommand::SetPattern {
track_id: self.track_id,
slot: self.slot,
block: self.block,
}
}
}
impl crate::state::NavState {
pub fn sequencer_op(&mut self, op: ops::SeqOp) -> (ops::SeqEffect, Vec<PatternSync>) {
let index = self.track_cursor;
let Some(track) = self.tracks.get_mut(index) else {
return (ops::SeqEffect::NOTHING, Vec::new());
};
let effect = ops::dispatch(track, op);
(effect, self.sequencer_syncs(index, effect))
}
#[must_use]
pub fn sequencer_syncs(&self, track_idx: usize, effect: ops::SeqEffect) -> Vec<PatternSync> {
let Some(track) = self.tracks.get(track_idx) else { return Vec::new() };
let (Some(state), Some(track_id)) = (track.sequencer.as_ref(), track.mixer_id) else {
return Vec::new();
};
effect
.slots()
.map(|slot| PatternSync { track_id, slot, block: state.block(slot as usize) })
.collect()
}
#[must_use]
pub fn all_sequencer_syncs(&self, track_idx: usize) -> Vec<PatternSync> {
self.sequencer_syncs(track_idx, ops::SeqEffect::all_slots())
}
pub fn attach_sequencer(&mut self, state: SequencerState) -> Vec<PatternSync> {
let index = self.track_cursor;
if let Some(track) = self.tracks.get_mut(index) {
track.sequencer = Some(Box::new(state));
}
self.all_sequencer_syncs(index)
}
pub fn sync_sequencers_from_audio(&mut self) {
for track in &mut self.tracks {
if let (Some(state), Some(handle)) = (track.sequencer.as_mut(), track.handle.as_ref()) {
state.sync_from_audio(&handle.pattern);
}
}
}
#[must_use]
pub fn sequencer_playhead(&self, track_idx: usize) -> Option<usize> {
let track = self.tracks.get(track_idx)?;
track.sequencer.as_ref()?;
let handle = track.handle.as_ref()?;
handle.pattern.is_running().then(|| handle.pattern.step() as usize)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SessionSequencer {
pub child: String,
#[serde(default)]
pub child_params: Vec<f32>,
#[serde(default)]
pub discrete: Vec<SessionSelector>,
pub selected: u8,
pub live: u8,
#[serde(default)]
pub lane: u8,
#[serde(default)]
pub step: u8,
pub playing: bool,
pub switch_quant: u8,
#[serde(default)]
pub chain: Vec<(u8, u8)>,
pub patterns: Vec<SessionPattern>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SessionPattern {
pub steps: u8,
pub rate: u8,
pub swing: u8,
pub base_vel: u8,
pub accent_vel: u8,
pub default_gate: u8,
pub mode: u8,
pub tonic: u8,
pub lanes: Vec<SessionLane>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SessionLane {
pub note: u8,
#[serde(default)]
pub muted: bool,
#[serde(default)]
pub soloed: bool,
#[serde(default)]
pub steps: Vec<SessionStep>,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct SessionStep {
pub index: u8,
pub octave: u8,
pub key: u8,
pub chord: u8,
pub voicing: u8,
pub accent: bool,
pub gate: u8,
}
impl SessionSequencer {
#[must_use]
pub fn from_state(
state: &SequencerState,
child: InstrumentType,
child_params: &[f32],
) -> Self {
Self {
child: instrument_key(child).to_string(),
child_params: child_params.to_vec(),
discrete: crate::session::selectors_of(child, child_params),
selected: state.selected,
live: state.live,
lane: state.lane,
step: state.step,
playing: state.playing,
switch_quant: state.switch_quant.index(),
chain: state.chain().iter().map(|e| (e.slot, e.repeats)).collect(),
patterns: state
.patterns
.iter()
.map(|block| SessionPattern {
steps: block.steps,
rate: block.rate.index(),
swing: block.swing,
base_vel: block.base_vel,
accent_vel: block.accent_vel,
default_gate: block.default_gate,
mode: block.mode.index(),
tonic: block.tonic,
lanes: block
.lanes
.iter()
.map(|lane| SessionLane {
note: lane.note,
muted: lane.muted,
soloed: lane.soloed,
steps: lane
.steps
.iter()
.enumerate()
.filter(|(_, step)| step.on)
.map(|(index, step)| SessionStep {
index: index as u8,
octave: step.octave,
key: step.key,
chord: step.chord,
voicing: step.voicing,
accent: step.accent,
gate: step.gate,
})
.collect(),
})
.collect(),
})
.collect(),
}
}
#[must_use]
pub fn child_instrument(&self) -> InstrumentType {
parse_instrument_type(&self.child)
.filter(|i| !i.is_sequencer())
.unwrap_or(DEFAULT_CHILD)
}
#[must_use]
pub fn child_panel(&self, expected: usize) -> Option<Vec<f32>> {
if self.child_params.len() != expected {
return None;
}
let mut params = self.child_params.clone();
apply_selectors(self.child_instrument(), &mut params, &self.discrete);
Some(params)
}
#[must_use]
pub fn to_state(&self) -> SequencerState {
let mut state = SequencerState::new(self.child_instrument());
state.selected = self.selected.min(SLOTS as u8 - 1);
state.live = self.live.min(SLOTS as u8 - 1);
state.lane = self.lane.min(LANES as u8 - 1);
state.step = self.step.min(MAX_STEPS as u8 - 1);
state.playing = self.playing;
state.switch_quant = SwitchQuant::from_index(self.switch_quant);
state.chain_len = self.chain.len().min(MAX_CHAIN) as u8;
for (entry, &(slot, repeats)) in state.chain.iter_mut().zip(&self.chain) {
*entry = ChainEntry { slot: slot.min(SLOTS as u8 - 1), repeats: repeats.max(1) };
}
for (block, stored) in state.patterns.iter_mut().zip(&self.patterns) {
block.steps = stored.steps;
block.rate = Rate::from_index(stored.rate);
block.swing = stored
.swing
.clamp(PatternBlock::MIN_SWING, PatternBlock::MAX_SWING);
block.base_vel = stored.base_vel;
block.accent_vel = stored.accent_vel;
block.default_gate = stored.default_gate;
block.mode = Mode::from_index(stored.mode);
block.tonic = stored.tonic % 12;
for (lane, stored_lane) in block.lanes.iter_mut().zip(&stored.lanes) {
lane.note = stored_lane.note;
lane.muted = stored_lane.muted;
lane.soloed = stored_lane.soloed;
for stored_step in &stored_lane.steps {
let Some(step) = lane.steps.get_mut(stored_step.index as usize) else {
continue;
};
step.on = true;
step.octave = stored_step.octave;
step.key = stored_step.key;
step.chord = stored_step.chord;
step.voicing = stored_step.voicing;
step.accent = stored_step.accent;
step.gate = stored_step.gate;
}
}
}
state
}
}
#[cfg(test)]
mod tests {
use super::ops::{dispatch, SeqOp};
use super::*;
use crate::state::TrackState;
use phosphor_core::project::TrackKind;
pub(super) fn drum_track() -> TrackState {
let mut track = TrackState::new("seq", 0, false, TrackKind::Instrument, vec![]);
track.instrument_type = Some(InstrumentType::DrumRack);
track.synth_params = phosphor_dsp::drum_rack::PARAM_DEFAULTS.to_vec();
track.sequencer = Some(Box::new(SequencerState::new(InstrumentType::DrumRack)));
track
}
#[test]
fn a_drum_child_pins_the_lanes_and_a_melodic_one_does_not() {
let drums = SequencerState::new(InstrumentType::DrumRack);
for (lane, note) in drums.pattern().lanes.iter().zip(DEFAULT_DRUM_LANES) {
assert_eq!(lane.note, note);
assert!(!lane.is_pitched());
}
let keys = SequencerState::new(InstrumentType::Juno60);
assert!(keys.pattern().lanes.iter().all(Lane::is_pitched));
}
#[test]
fn every_block_carries_the_track_level_settings() {
let mut track = drum_track();
dispatch(&mut track, SeqOp::SetPlaying(true));
dispatch(&mut track, SeqOp::CycleSwitchQuant(1));
dispatch(&mut track, SeqOp::QueueSlot(3));
let state = track.sequencer.as_ref().unwrap();
for slot in 0..SLOTS {
let block = state.block(slot);
assert!(block.playing);
assert_eq!(block.switch_quant, SwitchQuant::Bar);
assert_eq!(block.pending_slot, Some(3));
}
}
#[test]
fn a_sequencer_round_trips_through_a_session() {
let mut track = drum_track();
for op in [
SeqOp::SelectSlot(2),
SeqOp::SelectLane(3),
SeqOp::SelectStep(20),
SeqOp::ToggleStep,
SeqOp::ToggleAccent,
SeqOp::NudgeGate(3),
SeqOp::CycleLength(-2),
SeqOp::CycleRate(1),
SeqOp::NudgeSwing(8),
SeqOp::CycleMode(2),
SeqOp::SetTonic(7),
SeqOp::ToggleLaneSolo,
SeqOp::PushChainEntry { slot: 2, repeats: 4 },
SeqOp::PushChainEntry { slot: 0, repeats: 1 },
SeqOp::SetPlaying(true),
] {
dispatch(&mut track, op);
}
let before = *track.sequencer.clone().unwrap();
let stored = SessionSequencer::from_state(
&before,
InstrumentType::DrumRack,
&track.synth_params,
);
let json = serde_json::to_string(&stored).unwrap();
let read: SessionSequencer = serde_json::from_str(&json).unwrap();
let after = read.to_state();
assert_eq!(before, after, "a sequencer changed shape across a session");
assert_eq!(read.child_instrument(), InstrumentType::DrumRack);
let mut expected = track.synth_params.clone();
crate::session::apply_selectors(InstrumentType::DrumRack, &mut expected, &stored.discrete);
assert_eq!(read.child_panel(expected.len()).unwrap(), expected);
}
#[test]
fn a_masked_step_survives_a_session() {
let mut track = drum_track();
dispatch(&mut track, SeqOp::SelectStep(30));
dispatch(&mut track, SeqOp::ToggleStep);
dispatch(&mut track, SeqOp::CycleLength(-2));
let state = *track.sequencer.clone().unwrap();
assert_eq!(state.pattern().step_count(), 8);
let stored =
SessionSequencer::from_state(&state, InstrumentType::DrumRack, &track.synth_params);
let back = stored.to_state();
assert!(back.pattern().lanes[0].steps[30].on, "the masked step was lost");
}
#[test]
fn an_unknown_child_falls_back_rather_than_failing() {
let state = SequencerState::new(InstrumentType::DrumRack);
let mut stored = SessionSequencer::from_state(&state, InstrumentType::DrumRack, &[]);
stored.child = "moog-model-d".into();
assert_eq!(stored.child_instrument(), DEFAULT_CHILD);
assert_eq!(stored.to_state().patterns.len(), SLOTS);
}
#[test]
fn a_child_panel_of_the_wrong_length_is_refused() {
let state = SequencerState::new(InstrumentType::DrumRack);
let stored =
SessionSequencer::from_state(&state, InstrumentType::DrumRack, &[0.1, 0.2, 0.3]);
assert!(stored.child_panel(3).is_some());
assert!(stored.child_panel(4).is_none());
}
use crate::state::{initial_tracks, NavState};
fn nav_with_sequencer() -> NavState {
let mut nav = NavState::new(initial_tracks());
let mut track = drum_track();
track.mixer_id = Some(7);
nav.tracks.insert(0, track);
nav.track_cursor = 0;
nav
}
#[test]
fn an_edit_produces_the_command_that_carries_it() {
let mut nav = nav_with_sequencer();
let (effect, syncs) = nav.sequencer_op(SeqOp::SelectSlot(2));
assert!(effect.is_nothing());
assert!(syncs.is_empty(), "moving the cursor sent something");
let (_, syncs) = nav.sequencer_op(SeqOp::ToggleStep);
assert_eq!(syncs.len(), 1);
assert_eq!(syncs[0].track_id, 7);
assert_eq!(syncs[0].slot, 2);
assert!(syncs[0].block.lanes[0].steps[0].on);
match syncs[0].command() {
phosphor_core::mixer::MixerCommand::SetPattern { track_id, slot, .. } => {
assert_eq!((track_id, slot), (7, 2));
}
_ => panic!("a pattern sync produced something else"),
}
}
#[test]
fn a_track_with_no_engine_or_no_sequencer_sends_nothing() {
let mut nav = nav_with_sequencer();
nav.tracks[0].mixer_id = None;
let (effect, syncs) = nav.sequencer_op(SeqOp::ToggleStep);
assert!(!effect.is_nothing(), "the edit still happened");
assert!(syncs.is_empty(), "a track with no mixer id sent a command");
nav.track_cursor = 1; let (effect, syncs) = nav.sequencer_op(SeqOp::ToggleStep);
assert!(effect.is_nothing());
assert!(syncs.is_empty());
}
#[test]
fn attaching_a_sequencer_sends_every_slot() {
let mut nav = NavState::new(initial_tracks());
let mut track = crate::state::TrackState::new(
"seq",
0,
false,
phosphor_core::project::TrackKind::Instrument,
vec![],
);
track.instrument_type = Some(InstrumentType::DrumRack);
track.mixer_id = Some(3);
nav.tracks.insert(0, track);
nav.track_cursor = 0;
let syncs = nav.attach_sequencer(SequencerState::new(InstrumentType::DrumRack));
assert_eq!(syncs.len(), SLOTS);
assert_eq!(
syncs.iter().map(|s| s.slot).collect::<Vec<_>>(),
(0..SLOTS as u8).collect::<Vec<_>>()
);
assert!(nav.tracks[0].sequencer.is_some());
}
#[test]
fn the_playhead_comes_from_the_audio_thread() {
let mut nav = nav_with_sequencer();
let handle = std::sync::Arc::new(phosphor_core::project::TrackHandle::new(
7,
phosphor_core::project::TrackKind::Instrument,
));
nav.tracks[0].handle = Some(handle.clone());
assert_eq!(nav.sequencer_playhead(0), None);
handle.pattern.publish(1, Some(4), 11, true);
assert_eq!(nav.sequencer_playhead(0), Some(11));
nav.sync_sequencers_from_audio();
let state = nav.tracks[0].sequencer.as_ref().unwrap();
assert_eq!(state.live_slot(), 1);
assert_eq!(state.queued_slot(), Some(4));
}
#[test]
fn the_live_slot_is_read_back_from_the_audio_thread() {
let status = phosphor_core::project::PatternStatus::new();
status.publish(5, Some(2), 9, true);
let mut state = SequencerState::new(InstrumentType::DrumRack);
state.sync_from_audio(&status);
assert_eq!(state.live_slot(), 5);
assert_eq!(state.queued_slot(), Some(2));
status.publish(2, None, 0, true);
state.sync_from_audio(&status);
assert_eq!(state.live_slot(), 2);
assert_eq!(state.queued_slot(), None, "a switch that happened was not noticed");
}
#[test]
fn the_countdown_is_in_steps_to_the_switch() {
let mut track = drum_track();
dispatch(&mut track, SeqOp::QueueSlot(1));
let state = track.sequencer.as_ref().unwrap();
assert_eq!(state.countdown(2880), Some((1, 4)));
assert_eq!(state.countdown(3600), Some((1, 1)));
}
}