From bfd52c005389f3e2e4494c73b5dd3e596286fdc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Forment?= Date: Tue, 17 Mar 2026 02:45:41 +0100 Subject: [PATCH] Fix: sync mode is not required --- crates/project/src/lib.rs | 2 +- crates/project/src/project.rs | 48 +----- plugins/cagire-plugins/src/lib.rs | 1 - src/app/dispatch.rs | 2 - src/app/mod.rs | 1 - src/app/persistence.rs | 1 - src/app/sequencer.rs | 2 - src/app/staging.rs | 3 - src/commands.rs | 3 +- src/engine/link.rs | 14 ++ src/engine/mod.rs | 2 +- src/engine/sequencer.rs | 278 ++++++++++++++---------------- src/engine/timing.rs | 82 ++++----- src/init.rs | 6 +- src/input/modal.rs | 5 - src/model/mod.rs | 2 +- src/state/editor.rs | 7 +- src/state/modal.rs | 3 +- src/state/playback.rs | 4 +- src/views/patterns_view.rs | 12 +- src/views/render.rs | 2 - 21 files changed, 190 insertions(+), 290 deletions(-) diff --git a/crates/project/src/lib.rs b/crates/project/src/lib.rs index d1a8066..5b5aad7 100644 --- a/crates/project/src/lib.rs +++ b/crates/project/src/lib.rs @@ -14,4 +14,4 @@ pub const MAX_STEPS: usize = 1024; pub const DEFAULT_LENGTH: usize = 16; pub use file::{load, load_str, save, FileError}; -pub use project::{Bank, FollowUp, LaunchQuantization, Pattern, PatternSpeed, Project, Step, SyncMode}; +pub use project::{Bank, FollowUp, LaunchQuantization, Pattern, PatternSpeed, Project, Step}; diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index dd84b98..eb46d24 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -206,39 +206,6 @@ impl LaunchQuantization { } } -/// How a pattern synchronizes when launched: restart or phase-lock. -#[derive(Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)] -pub enum SyncMode { - #[default] - Reset, - PhaseLock, -} - -impl SyncMode { - /// Human-readable label for display. - pub fn label(&self) -> &'static str { - match self { - Self::Reset => "Reset", - Self::PhaseLock => "Phase-Lock", - } - } - - pub fn short_label(&self) -> &'static str { - match self { - Self::Reset => "Rst", - Self::PhaseLock => "Plk", - } - } - - /// Toggle between Reset and PhaseLock. - pub fn toggle(&self) -> Self { - match self { - Self::Reset => Self::PhaseLock, - Self::PhaseLock => Self::Reset, - } - } -} - /// What happens when a pattern finishes: loop, stop, or chain to another. #[derive(Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)] pub enum FollowUp { @@ -315,7 +282,7 @@ impl Default for Step { } } -/// Sequence of steps with playback settings (speed, quantization, sync, follow-up). +/// Sequence of steps with playback settings (speed, quantization, follow-up). #[derive(Clone)] pub struct Pattern { pub steps: Vec, @@ -324,7 +291,6 @@ pub struct Pattern { pub name: Option, pub description: Option, pub quantization: LaunchQuantization, - pub sync_mode: SyncMode, pub follow_up: FollowUp, } @@ -361,8 +327,6 @@ struct SparsePattern { description: Option, #[serde(default, skip_serializing_if = "is_default_quantization")] quantization: LaunchQuantization, - #[serde(default, skip_serializing_if = "is_default_sync_mode")] - sync_mode: SyncMode, #[serde(default, skip_serializing_if = "is_default_follow_up")] follow_up: FollowUp, } @@ -371,10 +335,6 @@ fn is_default_quantization(q: &LaunchQuantization) -> bool { *q == LaunchQuantization::default() } -fn is_default_sync_mode(s: &SyncMode) -> bool { - *s == SyncMode::default() -} - #[derive(Deserialize)] struct LegacyPattern { steps: Vec, @@ -388,8 +348,6 @@ struct LegacyPattern { #[serde(default)] quantization: LaunchQuantization, #[serde(default)] - sync_mode: SyncMode, - #[serde(default)] follow_up: FollowUp, } @@ -416,7 +374,6 @@ impl Serialize for Pattern { name: self.name.clone(), description: self.description.clone(), quantization: self.quantization, - sync_mode: self.sync_mode, follow_up: self.follow_up, }; sparse.serialize(serializer) @@ -452,7 +409,6 @@ impl<'de> Deserialize<'de> for Pattern { name: sparse.name, description: sparse.description, quantization: sparse.quantization, - sync_mode: sparse.sync_mode, follow_up: sparse.follow_up, }) } @@ -463,7 +419,6 @@ impl<'de> Deserialize<'de> for Pattern { name: legacy.name, description: legacy.description, quantization: legacy.quantization, - sync_mode: legacy.sync_mode, follow_up: legacy.follow_up, }), } @@ -479,7 +434,6 @@ impl Default for Pattern { name: None, description: None, quantization: LaunchQuantization::default(), - sync_mode: SyncMode::default(), follow_up: FollowUp::default(), } } diff --git a/plugins/cagire-plugins/src/lib.rs b/plugins/cagire-plugins/src/lib.rs index 0b206dc..1e54325 100644 --- a/plugins/cagire-plugins/src/lib.rs +++ b/plugins/cagire-plugins/src/lib.rs @@ -219,7 +219,6 @@ impl Plugin for CagirePlugin { source: s.source, }) .collect(), - sync_mode: pat.sync_mode, follow_up: pat.follow_up, }; let _ = self.bridge.cmd_tx.send(SeqCommand::PatternUpdate { diff --git a/src/app/dispatch.rs b/src/app/dispatch.rs index d2f17e1..18d5974 100644 --- a/src/app/dispatch.rs +++ b/src/app/dispatch.rs @@ -199,7 +199,6 @@ impl App { length, speed, quantization, - sync_mode, follow_up, } => { self.playback.staged_prop_changes.insert( @@ -210,7 +209,6 @@ impl App { length, speed, quantization, - sync_mode, follow_up, }, ); diff --git a/src/app/mod.rs b/src/app/mod.rs index 595274c..70d2459 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -203,7 +203,6 @@ impl App { length: pat.length.to_string(), speed: pat.speed, quantization: pat.quantization, - sync_mode: pat.sync_mode, follow_up: pat.follow_up, }; } diff --git a/src/app/persistence.rs b/src/app/persistence.rs index f053be9..009f1c4 100644 --- a/src/app/persistence.rs +++ b/src/app/persistence.rs @@ -138,7 +138,6 @@ impl App { self.playback.queued_changes.push(StagedChange { change: PatternChange::Start { bank, pattern }, quantization: crate::model::LaunchQuantization::Immediate, - sync_mode: crate::model::SyncMode::PhaseLock, }); } diff --git a/src/app/sequencer.rs b/src/app/sequencer.rs index 6d40d59..f7af1bb 100644 --- a/src/app/sequencer.rs +++ b/src/app/sequencer.rs @@ -16,7 +16,6 @@ impl App { bank, pattern, quantization: staged.quantization, - sync_mode: staged.sync_mode, }); } PatternChange::Stop { bank, pattern } => { @@ -68,7 +67,6 @@ impl App { source: s.source, }) .collect(), - sync_mode: pat.sync_mode, follow_up: pat.follow_up, }; let _ = cmd_tx.send(SeqCommand::PatternUpdate { diff --git a/src/app/staging.rs b/src/app/staging.rs index 4bdb39f..4427d2a 100644 --- a/src/app/staging.rs +++ b/src/app/staging.rs @@ -29,7 +29,6 @@ impl App { self.playback.staged_changes.push(StagedChange { change: PatternChange::Stop { bank, pattern }, quantization: pattern_data.quantization, - sync_mode: pattern_data.sync_mode, }); self.ui .set_status(format!("{} armed to stop", bp_label(bank, pattern))); @@ -37,7 +36,6 @@ impl App { self.playback.staged_changes.push(StagedChange { change: PatternChange::Start { bank, pattern }, quantization: pattern_data.quantization, - sync_mode: pattern_data.sync_mode, }); self.ui .set_status(format!("{} armed to play", bp_label(bank, pattern))); @@ -84,7 +82,6 @@ impl App { } pat.speed = props.speed; pat.quantization = props.quantization; - pat.sync_mode = props.sync_mode; pat.follow_up = props.follow_up; self.project_state.mark_dirty(bank, pattern); } diff --git a/src/commands.rs b/src/commands.rs index e35fa92..4647cc2 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -2,7 +2,7 @@ use std::path::PathBuf; -use crate::model::{FollowUp, LaunchQuantization, PatternSpeed, SyncMode}; +use crate::model::{FollowUp, LaunchQuantization, PatternSpeed}; use crate::page::Page; use crate::state::{ColorScheme, DeviceKind, Modal, OptionsFocus, PatternField, ScriptField, SettingKind}; @@ -169,7 +169,6 @@ pub enum AppCommand { length: Option, speed: PatternSpeed, quantization: LaunchQuantization, - sync_mode: SyncMode, follow_up: FollowUp, }, diff --git a/src/engine/link.rs b/src/engine/link.rs index 1ef04f8..4943c3e 100644 --- a/src/engine/link.rs +++ b/src/engine/link.rs @@ -81,6 +81,20 @@ impl LinkState { self.link.commit_app_session_state(&state); } + pub fn start_playing(&self, beat: f64, time: i64, quantum: f64) { + let mut state = SessionState::new(); + self.link.capture_app_session_state(&mut state); + state.set_is_playing_and_request_beat_at_time(true, time, beat, quantum); + self.link.commit_app_session_state(&state); + } + + pub fn stop_playing(&self, time: i64) { + let mut state = SessionState::new(); + self.link.capture_app_session_state(&mut state); + state.set_is_playing(false, time); + self.link.commit_app_session_state(&state); + } + pub fn capture_app_state(&self) -> SessionState { let mut state = SessionState::new(); self.link.capture_app_session_state(&mut state); diff --git a/src/engine/mod.rs b/src/engine/mod.rs index d968490..c8aa0b2 100644 --- a/src/engine/mod.rs +++ b/src/engine/mod.rs @@ -5,7 +5,7 @@ pub mod realtime; pub mod sequencer; mod timing; -pub use timing::{substeps_in_window, StepTiming, SyncTime}; +pub use timing::{next_boundary, substeps_in_window, SyncTime}; pub use audio::{preload_sample_heads, AnalysisHandle, ScopeBuffer, SpectrumBuffer}; diff --git a/src/engine/sequencer.rs b/src/engine/sequencer.rs index 42bed3c..dfa6c13 100644 --- a/src/engine/sequencer.rs +++ b/src/engine/sequencer.rs @@ -14,11 +14,17 @@ use std::thread::{self, JoinHandle}; use super::dispatcher::{dispatcher_loop, MidiDispatch, TimedMidiCommand}; use super::realtime::set_realtime_priority; -use super::{substeps_in_window, LinkState, StepTiming, SyncTime}; +use super::{next_boundary, substeps_in_window, LinkState, SyncTime}; use crate::model::{ CcAccess, Dictionary, ExecutionTrace, Rng, ScriptEngine, StepContext, Value, Variables, }; -use crate::model::{FollowUp, LaunchQuantization, SyncMode, MAX_BANKS, MAX_PATTERNS}; +use crate::model::{FollowUp, LaunchQuantization, MAX_BANKS, MAX_PATTERNS}; + +#[derive(Clone, Copy, PartialEq, Eq)] +pub(crate) enum SyncMode { + Reset, + PhaseLock, +} use crate::state::LiveKeyState; #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] @@ -114,7 +120,6 @@ pub enum SeqCommand { bank: usize, pattern: usize, quantization: LaunchQuantization, - sync_mode: SyncMode, }, PatternStop { bank: usize, @@ -141,7 +146,6 @@ pub struct PatternSnapshot { pub speed: crate::model::PatternSpeed, pub length: usize, pub steps: Vec, - pub sync_mode: SyncMode, pub follow_up: FollowUp, } @@ -299,18 +303,24 @@ struct ActivePattern { step_index: usize, iter: usize, last_step_beat: f64, + origin_beat: f64, } #[derive(Clone, Copy)] struct PendingPattern { id: PatternId, - quantization: LaunchQuantization, + target_beat: Option, sync_mode: SyncMode, } +#[derive(Clone, Copy)] +enum PlayState { + Idle { pause_beat: Option }, + Playing { frontier: f64 }, +} + struct AudioState { - prev_beat: f64, - pause_beat: Option, + play_state: PlayState, active_patterns: HashMap, pending_starts: Vec, pending_stops: Vec, @@ -320,8 +330,7 @@ struct AudioState { impl AudioState { fn new() -> Self { Self { - prev_beat: -1.0, - pause_beat: None, + play_state: PlayState::Idle { pause_beat: None }, active_patterns: HashMap::new(), pending_starts: Vec::new(), pending_stops: Vec::new(), @@ -474,22 +483,6 @@ impl PatternSnapshot { } } -fn check_quantization_boundary( - quantization: LaunchQuantization, - beat: f64, - prev_beat: f64, - quantum: f64, -) -> bool { - match quantization { - LaunchQuantization::Immediate => prev_beat >= 0.0, - LaunchQuantization::Beat => StepTiming::NextBeat.crossed(prev_beat, beat, quantum), - LaunchQuantization::Bar => StepTiming::NextBar.crossed(prev_beat, beat, quantum), - LaunchQuantization::Bars2 => StepTiming::NextBar.crossed(prev_beat, beat, quantum * 2.0), - LaunchQuantization::Bars4 => StepTiming::NextBar.crossed(prev_beat, beat, quantum * 4.0), - LaunchQuantization::Bars8 => StepTiming::NextBar.crossed(prev_beat, beat, quantum * 8.0), - } -} - type StepKey = (usize, usize, usize); struct RunsCounter { @@ -582,7 +575,7 @@ pub struct SequencerState { script_text: String, script_speed: crate::model::PatternSpeed, script_length: usize, - script_frontier: f64, + script_frontier: Option, script_step: usize, script_trace: Option, print_output: Option, @@ -621,7 +614,7 @@ impl SequencerState { script_text: String::new(), script_speed: crate::model::PatternSpeed::default(), script_length: 16, - script_frontier: -1.0, + script_frontier: None, script_step: 0, script_trace: None, print_output: None, @@ -639,7 +632,7 @@ impl SequencerState { false } - fn process_commands(&mut self, commands: Vec) { + fn process_commands(&mut self, commands: Vec, quantum: f64) { for cmd in commands { match cmd { SeqCommand::PatternUpdate { @@ -660,15 +653,15 @@ impl SequencerState { bank, pattern, quantization, - sync_mode, } => { let id = PatternId { bank, pattern }; self.audio_state.pending_stops.retain(|p| p.id != id); if !self.audio_state.pending_starts.iter().any(|p| p.id == id) { + let target_beat = next_boundary(self.last_beat, quantization, quantum); self.audio_state.pending_starts.push(PendingPattern { id, - quantization, - sync_mode, + target_beat, + sync_mode: SyncMode::PhaseLock, }); } } @@ -680,9 +673,10 @@ impl SequencerState { let id = PatternId { bank, pattern }; self.audio_state.pending_starts.retain(|p| p.id != id); if !self.audio_state.pending_stops.iter().any(|p| p.id == id) { + let target_beat = next_boundary(self.last_beat, quantization, quantum); self.audio_state.pending_stops.push(PendingPattern { id, - quantization, + target_beat, sync_mode: SyncMode::Reset, }); } @@ -722,7 +716,8 @@ impl SequencerState { self.audio_state.active_patterns.clear(); self.audio_state.pending_starts.clear(); self.audio_state.pending_stops.clear(); - self.audio_state.pause_beat = None; + self.audio_state.play_state = PlayState::Idle { pause_beat: None }; + self.script_frontier = None; self.step_traces = Arc::new(HashMap::new()); self.runs_counter.counts.clear(); self.audio_state.flush_midi_notes = true; @@ -732,9 +727,8 @@ impl SequencerState { active.step_index = 0; active.iter = 0; } - self.audio_state.prev_beat = -1.0; - self.audio_state.pause_beat = None; - self.script_frontier = -1.0; + self.audio_state.play_state = PlayState::Idle { pause_beat: None }; + self.script_frontier = None; self.script_step = 0; self.script_trace = None; self.variables.store(Arc::new(HashMap::new())); @@ -758,30 +752,26 @@ impl SequencerState { } pub fn tick(&mut self, input: TickInput) -> TickOutput { - self.process_commands(input.commands); self.last_tempo = input.tempo; self.last_beat = input.beat; self.last_playing = input.playing; + self.process_commands(input.commands, input.quantum); if !input.playing { return self.tick_paused(); } - let frontier = self.audio_state.prev_beat; - let lookahead_end = input.lookahead_end; - let resuming = frontier < 0.0; - - let boundary_frontier = if resuming { - self.audio_state.pause_beat.take().unwrap_or(input.beat) - } else { - frontier + let (frontier, resuming) = match self.audio_state.play_state { + PlayState::Playing { frontier } => (frontier, false), + PlayState::Idle { pause_beat } => (pause_beat.unwrap_or(input.beat), true), }; + let lookahead_end = input.lookahead_end; - self.activate_pending(lookahead_end, boundary_frontier, input.quantum); - self.deactivate_pending(lookahead_end, boundary_frontier, input.quantum); + self.activate_pending(frontier, lookahead_end); + self.deactivate_pending(frontier, lookahead_end); if resuming { - self.realign_phaselock_patterns(lookahead_end); + self.reset_origins_on_resume(lookahead_end); } let steps = self.execute_steps( @@ -818,7 +808,7 @@ impl SequencerState { let new_tempo = self.read_tempo_variable(steps.any_step_fired); self.apply_follow_ups(); - self.audio_state.prev_beat = lookahead_end; + self.audio_state.play_state = PlayState::Playing { frontier: lookahead_end }; let flush = std::mem::take(&mut self.audio_state.flush_midi_notes); TickOutput { @@ -840,11 +830,12 @@ impl SequencerState { self.pattern_cache.set(key.0, key.1, snapshot); } } - if self.audio_state.prev_beat >= 0.0 { - self.audio_state.pause_beat = Some(self.audio_state.prev_beat); - } - self.audio_state.prev_beat = -1.0; - self.script_frontier = -1.0; + let pause_beat = match self.audio_state.play_state { + PlayState::Playing { frontier } => Some(frontier), + PlayState::Idle { pause_beat } => pause_beat, + }; + self.audio_state.play_state = PlayState::Idle { pause_beat }; + self.script_frontier = None; self.script_step = 0; self.script_trace = None; self.print_output = None; @@ -858,35 +849,46 @@ impl SequencerState { } } - fn realign_phaselock_patterns(&mut self, beat: f64) { - for (id, active) in &mut self.audio_state.active_patterns { - let Some(pattern) = self.pattern_cache.get(id.bank, id.pattern) else { - continue; - }; - if pattern.sync_mode != SyncMode::PhaseLock { - continue; - } - let speed_mult = pattern.speed.multiplier(); - let subs_per_beat = 4.0 * speed_mult; - let step = (beat * subs_per_beat).floor() as usize + 1; - active.step_index = step % pattern.length; + fn pause_beat(&self) -> Option { + match self.audio_state.play_state { + PlayState::Idle { pause_beat } => pause_beat, + PlayState::Playing { .. } => None, } } - fn activate_pending(&mut self, beat: f64, prev_beat: f64, quantum: f64) { + fn reset_origins_on_resume(&mut self, lookahead_end: f64) { + for (id, active) in &mut self.audio_state.active_patterns { + active.origin_beat = lookahead_end; + let Some(pattern) = self.pattern_cache.get(id.bank, id.pattern) else { + continue; + }; + let subs_per_beat = 4.0 * pattern.speed.multiplier(); + let step = (lookahead_end * subs_per_beat).floor() as usize + 1; + active.step_index = step % pattern.length; + } + self.script_frontier = Some(lookahead_end); + } + + fn activate_pending(&mut self, frontier: f64, lookahead_end: f64) { self.buf_activated.clear(); for pending in &self.audio_state.pending_starts { - if check_quantization_boundary(pending.quantization, beat, prev_beat, quantum) { + let should_activate = match pending.target_beat { + None => true, + Some(target) => target <= lookahead_end, + }; + if should_activate { + let origin_beat = match pending.target_beat { + Some(t) if t > frontier => t - 1e-9, + _ => frontier, + }; let start_step = match pending.sync_mode { SyncMode::Reset => 0, SyncMode::PhaseLock => { if let Some(pat) = self.pattern_cache.get(pending.id.bank, pending.id.pattern) { - let speed_mult = pat.speed.multiplier(); - let subs_per_beat = 4.0 * speed_mult; - let first_sub = (prev_beat * subs_per_beat).floor() as usize + 1; - first_sub % pat.length + let subs_per_beat = 4.0 * pat.speed.multiplier(); + (origin_beat * subs_per_beat).floor() as usize % pat.length } else { 0 } @@ -901,7 +903,8 @@ impl SequencerState { pattern: pending.id.pattern, step_index: start_step, iter: 0, - last_step_beat: beat, + last_step_beat: lookahead_end, + origin_beat, }, ); self.buf_activated.push(pending.id); @@ -913,15 +916,18 @@ impl SequencerState { .retain(|p| !activated.contains(&p.id)); } - fn deactivate_pending(&mut self, beat: f64, prev_beat: f64, quantum: f64) { + fn deactivate_pending(&mut self, _frontier: f64, lookahead_end: f64) { self.buf_stopped.clear(); for pending in &self.audio_state.pending_stops { - if check_quantization_boundary(pending.quantization, beat, prev_beat, quantum) { + let should_deactivate = match pending.target_beat { + None => true, + Some(target) => target <= lookahead_end, + }; + if should_deactivate { self.audio_state.active_patterns.remove(&pending.id); Arc::make_mut(&mut self.step_traces).retain(|&(bank, pattern, _), _| { bank != pending.id.bank || pattern != pending.id.pattern }); - // Flush pending update so cache stays current for future launches let key = (pending.id.bank, pending.id.pattern); if let Some(snapshot) = self.pending_updates.remove(&key) { self.pattern_cache.set(key.0, key.1, snapshot); @@ -981,7 +987,8 @@ impl SequencerState { .copied() .unwrap_or_else(|| pattern.speed.multiplier()); - let step_beats = substeps_in_window(frontier, lookahead_end, speed_mult); + let pattern_frontier = frontier.max(active.origin_beat); + let step_beats = substeps_in_window(pattern_frontier, lookahead_end, speed_mult); for step_beat in step_beats { result.any_step_fired = true; @@ -1116,11 +1123,7 @@ impl SequencerState { return; } - let script_frontier = if self.script_frontier < 0.0 { - frontier - } else { - self.script_frontier - }; + let script_frontier = self.script_frontier.unwrap_or(frontier); let speed_mult = self.script_speed.multiplier(); let fire_beats = substeps_in_window(script_frontier, lookahead_end, speed_mult); @@ -1187,7 +1190,7 @@ impl SequencerState { self.script_step += 1; } - self.script_frontier = lookahead_end; + self.script_frontier = Some(lookahead_end); } fn read_tempo_variable(&self, any_step_fired: bool) -> Option { @@ -1220,21 +1223,21 @@ impl SequencerState { FollowUp::Stop => { self.audio_state.pending_stops.push(PendingPattern { id: *completed_id, - quantization: LaunchQuantization::Immediate, + target_beat: None, sync_mode: SyncMode::Reset, }); } FollowUp::Chain { bank, pattern } => { self.audio_state.pending_stops.push(PendingPattern { id: *completed_id, - quantization: LaunchQuantization::Immediate, + target_beat: None, sync_mode: SyncMode::Reset, }); let target = PatternId { bank, pattern }; if !self.audio_state.pending_starts.iter().any(|p| p.id == target) { self.audio_state.pending_starts.push(PendingPattern { id: target, - quantization: LaunchQuantization::Immediate, + target_beat: None, sync_mode: SyncMode::Reset, }); } @@ -1325,9 +1328,19 @@ fn sequencer_loop( let state = link.capture_app_state(); let current_time_us = link.clock_micros() as SyncTime; - let beat = state.beat_at_time(current_time_us as i64, quantum); + let mut beat = state.beat_at_time(current_time_us as i64, quantum); let tempo = state.tempo(); + let is_playing = playing.load(Ordering::Relaxed); + if is_playing && !seq_state.last_playing { + let anchor_beat = seq_state.pause_beat().unwrap_or(0.0); + link.start_playing(anchor_beat, current_time_us as i64, quantum); + let state = link.capture_app_state(); + beat = state.beat_at_time(current_time_us as i64, quantum); + } else if !is_playing && seq_state.last_playing { + link.stop_playing(current_time_us as i64); + } + let lookahead_beats = if tempo > 0.0 { lookahead_secs * tempo / 60.0 } else { @@ -1339,7 +1352,7 @@ fn sequencer_loop( let audio_samples = audio_sample_pos.load(Ordering::Acquire); let input = TickInput { commands, - playing: playing.load(Ordering::Relaxed), + playing: is_playing, beat, lookahead_end, tempo, @@ -1550,7 +1563,6 @@ mod tests { source: None, }) .collect(), - sync_mode: SyncMode::Reset, follow_up: FollowUp::default(), } } @@ -1621,7 +1633,6 @@ mod tests { bank: 0, pattern: 0, quantization: LaunchQuantization::Immediate, - sync_mode: SyncMode::Reset, }, ], 1.0, @@ -1649,7 +1660,6 @@ mod tests { bank: 0, pattern: 0, quantization: LaunchQuantization::Immediate, - sync_mode: SyncMode::Reset, }], 1.0, )); @@ -1697,7 +1707,6 @@ mod tests { bank: 0, pattern: 0, quantization: LaunchQuantization::Immediate, - sync_mode: SyncMode::Reset, }], 0.5, )); @@ -1706,43 +1715,32 @@ mod tests { } #[test] - fn test_quantization_boundaries() { - assert!(check_quantization_boundary( - LaunchQuantization::Immediate, - 1.5, - 1.0, - 4.0 - )); - assert!(check_quantization_boundary( - LaunchQuantization::Beat, - 2.0, - 1.9, - 4.0 - )); - assert!(!check_quantization_boundary( - LaunchQuantization::Beat, - 1.5, - 1.2, - 4.0 - )); - assert!(check_quantization_boundary( - LaunchQuantization::Bar, - 4.0, - 3.9, - 4.0 - )); - assert!(!check_quantization_boundary( - LaunchQuantization::Bar, - 3.5, - 3.2, - 4.0 - )); - assert!(!check_quantization_boundary( - LaunchQuantization::Immediate, - 1.0, - -1.0, - 4.0 - )); + fn test_next_boundary() { + use super::super::next_boundary; + + // Immediate returns None + assert_eq!(next_boundary(1.5, LaunchQuantization::Immediate, 4.0), None); + + // Beat: next integer beat + assert_eq!(next_boundary(1.5, LaunchQuantization::Beat, 4.0), Some(2.0)); + assert_eq!(next_boundary(1.9, LaunchQuantization::Beat, 4.0), Some(2.0)); + // On exact beat boundary, targets next beat + assert_eq!(next_boundary(2.0, LaunchQuantization::Beat, 4.0), Some(3.0)); + + // Bar (quantum=4): next multiple of 4 + assert_eq!(next_boundary(3.5, LaunchQuantization::Bar, 4.0), Some(4.0)); + assert_eq!(next_boundary(3.9, LaunchQuantization::Bar, 4.0), Some(4.0)); + // On exact bar boundary, targets next bar + assert_eq!(next_boundary(4.0, LaunchQuantization::Bar, 4.0), Some(8.0)); + + // Bars2 (quantum=4): next multiple of 8 + assert_eq!(next_boundary(3.5, LaunchQuantization::Bars2, 4.0), Some(8.0)); + + // Bars4 (quantum=4): next multiple of 16 + assert_eq!(next_boundary(3.5, LaunchQuantization::Bars4, 4.0), Some(16.0)); + + // Bars8 (quantum=4): next multiple of 32 + assert_eq!(next_boundary(3.5, LaunchQuantization::Bars8, 4.0), Some(32.0)); } #[test] @@ -1763,7 +1761,6 @@ mod tests { bank: 0, pattern: 0, quantization: LaunchQuantization::Immediate, - sync_mode: SyncMode::Reset, }], 0.5, )); @@ -1808,7 +1805,6 @@ mod tests { bank: 0, pattern: 0, quantization: LaunchQuantization::Immediate, - sync_mode: SyncMode::Reset, }], 0.5, )); @@ -1844,7 +1840,6 @@ mod tests { bank: 0, pattern: 0, quantization: LaunchQuantization::Immediate, - sync_mode: SyncMode::Reset, }, SeqCommand::PatternStop { bank: 0, @@ -1879,13 +1874,11 @@ mod tests { bank: 0, pattern: 0, quantization: LaunchQuantization::Bar, - sync_mode: SyncMode::Reset, }, SeqCommand::PatternStart { bank: 0, pattern: 1, quantization: LaunchQuantization::Beat, - sync_mode: SyncMode::Reset, }, ], 0.0, @@ -1921,7 +1914,6 @@ mod tests { bank: 0, pattern: 0, quantization: LaunchQuantization::Immediate, - sync_mode: SyncMode::Reset, }], 0.5, )); @@ -1990,7 +1982,6 @@ mod tests { bank: 0, pattern: 0, quantization: LaunchQuantization::Immediate, - sync_mode: SyncMode::Reset, }], ..tick_at(1.0, false) }); @@ -2019,7 +2010,6 @@ mod tests { bank: 0, pattern: 0, quantization: LaunchQuantization::Immediate, - sync_mode: SyncMode::Reset, }], 1.0, )); @@ -2049,13 +2039,11 @@ mod tests { bank: 0, pattern: 0, quantization: LaunchQuantization::Bar, - sync_mode: SyncMode::Reset, }, SeqCommand::PatternStart { bank: 0, pattern: 0, quantization: LaunchQuantization::Bar, - sync_mode: SyncMode::Reset, }, ], 0.0, @@ -2088,7 +2076,6 @@ mod tests { bank: 0, pattern: 0, quantization: LaunchQuantization::Immediate, - sync_mode: SyncMode::Reset, }], 0.5, )); @@ -2115,7 +2102,6 @@ mod tests { source: None, }) .collect(), - sync_mode: SyncMode::Reset, follow_up: FollowUp::default(), } } @@ -2138,7 +2124,6 @@ mod tests { bank: 0, pattern: 0, quantization: LaunchQuantization::Immediate, - sync_mode: SyncMode::Reset, }], 0.5, )); @@ -2186,13 +2171,11 @@ mod tests { bank: 0, pattern: 0, quantization: LaunchQuantization::Immediate, - sync_mode: SyncMode::Reset, }, SeqCommand::PatternStart { bank: 0, pattern: 1, quantization: LaunchQuantization::Immediate, - sync_mode: SyncMode::Reset, }, ], 0.5, @@ -2227,7 +2210,6 @@ mod tests { bank: 0, pattern: 0, quantization: LaunchQuantization::Bar, - sync_mode: SyncMode::Reset, }], 3.5, )); @@ -2266,7 +2248,6 @@ mod tests { bank: 0, pattern: 0, quantization: LaunchQuantization::Immediate, - sync_mode: SyncMode::Reset, }], ..tick_at(2.0, false) }); @@ -2306,13 +2287,11 @@ mod tests { bank: 0, pattern: 0, quantization: LaunchQuantization::Bar, - sync_mode: SyncMode::Reset, }, SeqCommand::PatternStart { bank: 0, pattern: 1, quantization: LaunchQuantization::Bar, - sync_mode: SyncMode::Reset, }, ], 3.5, @@ -2344,7 +2323,6 @@ mod tests { source: None, }) .collect(), - sync_mode: SyncMode::PhaseLock, follow_up: FollowUp::default(), } } @@ -2368,7 +2346,6 @@ mod tests { bank: 0, pattern: 0, quantization: LaunchQuantization::Bar, - sync_mode: SyncMode::PhaseLock, }], 3.5, )); @@ -2410,7 +2387,6 @@ mod tests { bank: 0, pattern: 0, quantization: LaunchQuantization::Bar, - sync_mode: SyncMode::Reset, }], 1.0, )); diff --git a/src/engine/timing.rs b/src/engine/timing.rs index 088db37..c17e64e 100644 --- a/src/engine/timing.rs +++ b/src/engine/timing.rs @@ -1,26 +1,28 @@ +use crate::model::LaunchQuantization; + /// Microsecond-precision timestamp for audio synchronization. pub type SyncTime = u64; -/// Timing boundary types for step and pattern scheduling. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum StepTiming { - /// Fire when a beat boundary is crossed. - NextBeat, - /// Fire when a bar/quantum boundary is crossed. - NextBar, -} - -impl StepTiming { - /// Returns true if the boundary was crossed between prev_beat and curr_beat. - pub fn crossed(&self, prev_beat: f64, curr_beat: f64, quantum: f64) -> bool { - if prev_beat < 0.0 { - return false; +/// Compute the exact next quantization boundary beat after `current_beat`. +/// Returns `None` for Immediate (activate now), `Some(beat)` for all others. +pub fn next_boundary(current_beat: f64, quantization: LaunchQuantization, quantum: f64) -> Option { + match quantization { + LaunchQuantization::Immediate => None, + LaunchQuantization::Beat => Some(current_beat.floor() + 1.0), + LaunchQuantization::Bar => { + Some((current_beat / quantum).floor() * quantum + quantum) } - match self { - Self::NextBeat => prev_beat.floor() as i64 != curr_beat.floor() as i64, - Self::NextBar => { - (prev_beat / quantum).floor() as i64 != (curr_beat / quantum).floor() as i64 - } + LaunchQuantization::Bars2 => { + let p = quantum * 2.0; + Some((current_beat / p).floor() * p + p) + } + LaunchQuantization::Bars4 => { + let p = quantum * 4.0; + Some((current_beat / p).floor() * p + p) + } + LaunchQuantization::Bars8 => { + let p = quantum * 8.0; + Some((current_beat / p).floor() * p + p) } } } @@ -29,7 +31,7 @@ impl StepTiming { /// Each entry is the exact beat at which that substep fires. /// Clamped to 64 results max to prevent runaway. pub fn substeps_in_window(frontier: f64, end: f64, speed: f64) -> Vec { - if frontier < 0.0 || end <= frontier || speed <= 0.0 { + if end <= frontier || speed <= 0.0 { return Vec::new(); } let substeps_per_beat = 4.0 * speed; @@ -55,9 +57,6 @@ mod tests { } fn substeps_crossed(prev_beat: f64, curr_beat: f64, speed: f64) -> usize { - if prev_beat < 0.0 { - return 0; - } let prev_substep = (prev_beat * 4.0 * speed).floor() as i64; let curr_substep = (curr_beat * 4.0 * speed).floor() as i64; (curr_substep - prev_substep).clamp(0, 16) as usize @@ -88,23 +87,15 @@ mod tests { } #[test] - fn test_step_timing_beat_crossed() { - // Crossing from beat 0 to beat 1 - assert!(StepTiming::NextBeat.crossed(0.9, 1.1, 4.0)); - // Not crossing (both in same beat) - assert!(!StepTiming::NextBeat.crossed(0.5, 0.9, 4.0)); - // Negative prev_beat returns false - assert!(!StepTiming::NextBeat.crossed(-1.0, 1.0, 4.0)); - } - - #[test] - fn test_step_timing_bar_crossed() { - // Crossing from bar 0 to bar 1 (quantum=4) - assert!(StepTiming::NextBar.crossed(3.9, 4.1, 4.0)); - // Not crossing (both in same bar) - assert!(!StepTiming::NextBar.crossed(2.0, 3.0, 4.0)); - // Crossing with different quantum - assert!(StepTiming::NextBar.crossed(7.9, 8.1, 8.0)); + fn test_next_boundary() { + assert_eq!(next_boundary(1.5, LaunchQuantization::Immediate, 4.0), None); + assert_eq!(next_boundary(1.5, LaunchQuantization::Beat, 4.0), Some(2.0)); + assert_eq!(next_boundary(2.0, LaunchQuantization::Beat, 4.0), Some(3.0)); + assert_eq!(next_boundary(3.5, LaunchQuantization::Bar, 4.0), Some(4.0)); + assert_eq!(next_boundary(4.0, LaunchQuantization::Bar, 4.0), Some(8.0)); + assert_eq!(next_boundary(3.5, LaunchQuantization::Bars2, 4.0), Some(8.0)); + assert_eq!(next_boundary(3.5, LaunchQuantization::Bars4, 4.0), Some(16.0)); + assert_eq!(next_boundary(3.5, LaunchQuantization::Bars8, 4.0), Some(32.0)); } #[test] @@ -126,9 +117,9 @@ mod tests { } #[test] - fn test_substeps_crossed_negative_prev() { - // Negative prev_beat returns 0 - assert_eq!(substeps_crossed(-1.0, 0.5, 1.0), 0); + fn test_substeps_crossed_same_position() { + // Same position returns 0 + assert_eq!(substeps_crossed(0.5, 0.5, 1.0), 0); } #[test] @@ -205,8 +196,9 @@ mod tests { } #[test] - fn test_substeps_in_window_negative_frontier() { - let result = substeps_in_window(-1.0, 0.5, 1.0); + fn test_substeps_in_window_reversed() { + // end <= frontier returns empty + let result = substeps_in_window(0.5, 0.3, 1.0); assert!(result.is_empty()); } } diff --git a/src/init.rs b/src/init.rs index 77cca61..07162c1 100644 --- a/src/init.rs +++ b/src/init.rs @@ -83,8 +83,7 @@ pub fn init(args: InitArgs) -> Init { for (bank, pattern) in playing { app.playback.queued_changes.push(StagedChange { change: PatternChange::Start { bank, pattern }, - quantization: model::LaunchQuantization::Immediate, - sync_mode: model::SyncMode::PhaseLock, + quantization: model::LaunchQuantization::Bar, }); } app.ui.set_status(format!("Demo: {}", demo.name)); @@ -96,8 +95,7 @@ pub fn init(args: InitArgs) -> Init { bank: 0, pattern: 0, }, - quantization: model::LaunchQuantization::Immediate, - sync_mode: model::SyncMode::PhaseLock, + quantization: model::LaunchQuantization::Bar, }); } diff --git a/src/input/modal.rs b/src/input/modal.rs index 051a0d4..cf26980 100644 --- a/src/input/modal.rs +++ b/src/input/modal.rs @@ -461,7 +461,6 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input length, speed, quantization, - sync_mode, follow_up, } => { let (bank, pattern) = (*bank, *pattern); @@ -472,7 +471,6 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input KeyCode::Left => match field { PatternPropsField::Speed => *speed = speed.prev(), PatternPropsField::Quantization => *quantization = quantization.prev(), - PatternPropsField::SyncMode => *sync_mode = sync_mode.toggle(), PatternPropsField::FollowUp => *follow_up = follow_up.prev_mode(), PatternPropsField::ChainBank => { if let FollowUp::Chain { bank: b, .. } = follow_up { @@ -489,7 +487,6 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input KeyCode::Right => match field { PatternPropsField::Speed => *speed = speed.next(), PatternPropsField::Quantization => *quantization = quantization.next(), - PatternPropsField::SyncMode => *sync_mode = sync_mode.toggle(), PatternPropsField::FollowUp => *follow_up = follow_up.next_mode(), PatternPropsField::ChainBank => { if let FollowUp::Chain { bank: b, .. } = follow_up { @@ -535,7 +532,6 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input let length_val = length.parse().ok(); let speed_val = *speed; let quant_val = *quantization; - let sync_val = *sync_mode; let follow_up_val = *follow_up; ctx.dispatch(AppCommand::StagePatternProps { bank, @@ -545,7 +541,6 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input length: length_val, speed: speed_val, quantization: quant_val, - sync_mode: sync_val, follow_up: follow_up_val, }); ctx.dispatch(AppCommand::CloseModal); diff --git a/src/model/mod.rs b/src/model/mod.rs index 8285b25..891ebb4 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -11,7 +11,7 @@ pub use cagire_forth::{ }; pub use cagire_project::{ load, load_str, save, share, Bank, FollowUp, LaunchQuantization, Pattern, PatternSpeed, - Project, SyncMode, MAX_BANKS, MAX_PATTERNS, + Project, MAX_BANKS, MAX_PATTERNS, }; pub use script::ScriptEngine; diff --git a/src/state/editor.rs b/src/state/editor.rs index 1e8844e..56ea37f 100644 --- a/src/state/editor.rs +++ b/src/state/editor.rs @@ -31,7 +31,6 @@ pub enum PatternPropsField { Length, Speed, Quantization, - SyncMode, FollowUp, ChainBank, ChainPattern, @@ -44,8 +43,7 @@ impl PatternPropsField { Self::Description => Self::Length, Self::Length => Self::Speed, Self::Speed => Self::Quantization, - Self::Quantization => Self::SyncMode, - Self::SyncMode => Self::FollowUp, + Self::Quantization => Self::FollowUp, Self::FollowUp if follow_up_is_chain => Self::ChainBank, Self::FollowUp => Self::FollowUp, Self::ChainBank => Self::ChainPattern, @@ -60,8 +58,7 @@ impl PatternPropsField { Self::Length => Self::Description, Self::Speed => Self::Length, Self::Quantization => Self::Speed, - Self::SyncMode => Self::Quantization, - Self::FollowUp => Self::SyncMode, + Self::FollowUp => Self::Quantization, Self::ChainBank => Self::FollowUp, Self::ChainPattern if follow_up_is_chain => Self::ChainBank, Self::ChainPattern => Self::FollowUp, diff --git a/src/state/modal.rs b/src/state/modal.rs index f5ec885..cabbc17 100644 --- a/src/state/modal.rs +++ b/src/state/modal.rs @@ -1,4 +1,4 @@ -use crate::model::{self, FollowUp, LaunchQuantization, PatternSpeed, SyncMode}; +use crate::model::{self, FollowUp, LaunchQuantization, PatternSpeed}; use crate::state::editor::{EuclideanField, PatternField, PatternPropsField, ScriptField}; use crate::state::file_browser::FileBrowserState; @@ -85,7 +85,6 @@ pub enum Modal { length: String, speed: PatternSpeed, quantization: LaunchQuantization, - sync_mode: SyncMode, follow_up: FollowUp, }, KeybindingsHelp { diff --git a/src/state/playback.rs b/src/state/playback.rs index 0d99f36..efdbeb6 100644 --- a/src/state/playback.rs +++ b/src/state/playback.rs @@ -1,12 +1,11 @@ use crate::engine::PatternChange; -use crate::model::{FollowUp, LaunchQuantization, PatternSpeed, SyncMode}; +use crate::model::{FollowUp, LaunchQuantization, PatternSpeed}; use std::collections::{HashMap, HashSet}; #[derive(Clone)] pub struct StagedChange { pub change: PatternChange, pub quantization: LaunchQuantization, - pub sync_mode: SyncMode, } #[derive(Clone, Copy, PartialEq, Eq, Hash)] @@ -21,7 +20,6 @@ pub struct StagedPropChange { pub length: Option, pub speed: PatternSpeed, pub quantization: LaunchQuantization, - pub sync_mode: SyncMode, pub follow_up: FollowUp, } diff --git a/src/views/patterns_view.rs b/src/views/patterns_view.rs index cbf73d2..81eec3d 100644 --- a/src/views/patterns_view.rs +++ b/src/views/patterns_view.rs @@ -507,11 +507,7 @@ fn render_patterns(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, a }; let props_indicator = if has_staged_props { "~" } else { "" }; let quant_sync = if is_selected { - format!( - "{}:{} ", - pattern.quantization.short_label(), - pattern.sync_mode.short_label() - ) + format!("{} ", pattern.quantization.short_label()) } else { String::new() }; @@ -755,8 +751,6 @@ fn render_properties( let steps_label = format!("{}/{}", content_count, pattern.length); let speed_label = pattern.speed.label(); let quant_label = pattern.quantization.label(); - let sync_label = pattern.sync_mode.label(); - let label_style = Style::new().fg(theme.ui.text_muted); let value_style = Style::new().fg(theme.ui.text_primary); @@ -781,10 +775,6 @@ fn render_properties( Span::styled(" Quant ", label_style), Span::styled(quant_label, value_style), ]), - Line::from(vec![ - Span::styled(" Sync ", label_style), - Span::styled(sync_label, value_style), - ]), ]; if pattern.follow_up != FollowUp::Loop { diff --git a/src/views/render.rs b/src/views/render.rs index 123308f..745cdfd 100644 --- a/src/views/render.rs +++ b/src/views/render.rs @@ -737,7 +737,6 @@ fn render_modal( length, speed, quantization, - sync_mode, follow_up, } => { use crate::model::FollowUp; @@ -766,7 +765,6 @@ fn render_modal( ("Length", length.clone(), *field == PatternPropsField::Length), ("Speed", speed_label, *field == PatternPropsField::Speed), ("Quantization", quantization.label().to_string(), *field == PatternPropsField::Quantization), - ("Sync Mode", sync_mode.label().to_string(), *field == PatternPropsField::SyncMode), ("Follow Up", follow_up_label, *field == PatternPropsField::FollowUp), ]; if is_chain {