Fix: sync mode is not required

This commit is contained in:
2026-03-17 02:45:41 +01:00
parent 12172ce1e8
commit bfd52c0053
21 changed files with 190 additions and 290 deletions

View File

@@ -14,4 +14,4 @@ pub const MAX_STEPS: usize = 1024;
pub const DEFAULT_LENGTH: usize = 16; pub const DEFAULT_LENGTH: usize = 16;
pub use file::{load, load_str, save, FileError}; 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};

View File

@@ -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. /// What happens when a pattern finishes: loop, stop, or chain to another.
#[derive(Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)] #[derive(Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
pub enum FollowUp { 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)] #[derive(Clone)]
pub struct Pattern { pub struct Pattern {
pub steps: Vec<Step>, pub steps: Vec<Step>,
@@ -324,7 +291,6 @@ pub struct Pattern {
pub name: Option<String>, pub name: Option<String>,
pub description: Option<String>, pub description: Option<String>,
pub quantization: LaunchQuantization, pub quantization: LaunchQuantization,
pub sync_mode: SyncMode,
pub follow_up: FollowUp, pub follow_up: FollowUp,
} }
@@ -361,8 +327,6 @@ struct SparsePattern {
description: Option<String>, description: Option<String>,
#[serde(default, skip_serializing_if = "is_default_quantization")] #[serde(default, skip_serializing_if = "is_default_quantization")]
quantization: LaunchQuantization, quantization: LaunchQuantization,
#[serde(default, skip_serializing_if = "is_default_sync_mode")]
sync_mode: SyncMode,
#[serde(default, skip_serializing_if = "is_default_follow_up")] #[serde(default, skip_serializing_if = "is_default_follow_up")]
follow_up: FollowUp, follow_up: FollowUp,
} }
@@ -371,10 +335,6 @@ fn is_default_quantization(q: &LaunchQuantization) -> bool {
*q == LaunchQuantization::default() *q == LaunchQuantization::default()
} }
fn is_default_sync_mode(s: &SyncMode) -> bool {
*s == SyncMode::default()
}
#[derive(Deserialize)] #[derive(Deserialize)]
struct LegacyPattern { struct LegacyPattern {
steps: Vec<Step>, steps: Vec<Step>,
@@ -388,8 +348,6 @@ struct LegacyPattern {
#[serde(default)] #[serde(default)]
quantization: LaunchQuantization, quantization: LaunchQuantization,
#[serde(default)] #[serde(default)]
sync_mode: SyncMode,
#[serde(default)]
follow_up: FollowUp, follow_up: FollowUp,
} }
@@ -416,7 +374,6 @@ impl Serialize for Pattern {
name: self.name.clone(), name: self.name.clone(),
description: self.description.clone(), description: self.description.clone(),
quantization: self.quantization, quantization: self.quantization,
sync_mode: self.sync_mode,
follow_up: self.follow_up, follow_up: self.follow_up,
}; };
sparse.serialize(serializer) sparse.serialize(serializer)
@@ -452,7 +409,6 @@ impl<'de> Deserialize<'de> for Pattern {
name: sparse.name, name: sparse.name,
description: sparse.description, description: sparse.description,
quantization: sparse.quantization, quantization: sparse.quantization,
sync_mode: sparse.sync_mode,
follow_up: sparse.follow_up, follow_up: sparse.follow_up,
}) })
} }
@@ -463,7 +419,6 @@ impl<'de> Deserialize<'de> for Pattern {
name: legacy.name, name: legacy.name,
description: legacy.description, description: legacy.description,
quantization: legacy.quantization, quantization: legacy.quantization,
sync_mode: legacy.sync_mode,
follow_up: legacy.follow_up, follow_up: legacy.follow_up,
}), }),
} }
@@ -479,7 +434,6 @@ impl Default for Pattern {
name: None, name: None,
description: None, description: None,
quantization: LaunchQuantization::default(), quantization: LaunchQuantization::default(),
sync_mode: SyncMode::default(),
follow_up: FollowUp::default(), follow_up: FollowUp::default(),
} }
} }

View File

@@ -219,7 +219,6 @@ impl Plugin for CagirePlugin {
source: s.source, source: s.source,
}) })
.collect(), .collect(),
sync_mode: pat.sync_mode,
follow_up: pat.follow_up, follow_up: pat.follow_up,
}; };
let _ = self.bridge.cmd_tx.send(SeqCommand::PatternUpdate { let _ = self.bridge.cmd_tx.send(SeqCommand::PatternUpdate {

View File

@@ -199,7 +199,6 @@ impl App {
length, length,
speed, speed,
quantization, quantization,
sync_mode,
follow_up, follow_up,
} => { } => {
self.playback.staged_prop_changes.insert( self.playback.staged_prop_changes.insert(
@@ -210,7 +209,6 @@ impl App {
length, length,
speed, speed,
quantization, quantization,
sync_mode,
follow_up, follow_up,
}, },
); );

View File

@@ -203,7 +203,6 @@ impl App {
length: pat.length.to_string(), length: pat.length.to_string(),
speed: pat.speed, speed: pat.speed,
quantization: pat.quantization, quantization: pat.quantization,
sync_mode: pat.sync_mode,
follow_up: pat.follow_up, follow_up: pat.follow_up,
}; };
} }

View File

@@ -138,7 +138,6 @@ impl App {
self.playback.queued_changes.push(StagedChange { self.playback.queued_changes.push(StagedChange {
change: PatternChange::Start { bank, pattern }, change: PatternChange::Start { bank, pattern },
quantization: crate::model::LaunchQuantization::Immediate, quantization: crate::model::LaunchQuantization::Immediate,
sync_mode: crate::model::SyncMode::PhaseLock,
}); });
} }

View File

@@ -16,7 +16,6 @@ impl App {
bank, bank,
pattern, pattern,
quantization: staged.quantization, quantization: staged.quantization,
sync_mode: staged.sync_mode,
}); });
} }
PatternChange::Stop { bank, pattern } => { PatternChange::Stop { bank, pattern } => {
@@ -68,7 +67,6 @@ impl App {
source: s.source, source: s.source,
}) })
.collect(), .collect(),
sync_mode: pat.sync_mode,
follow_up: pat.follow_up, follow_up: pat.follow_up,
}; };
let _ = cmd_tx.send(SeqCommand::PatternUpdate { let _ = cmd_tx.send(SeqCommand::PatternUpdate {

View File

@@ -29,7 +29,6 @@ impl App {
self.playback.staged_changes.push(StagedChange { self.playback.staged_changes.push(StagedChange {
change: PatternChange::Stop { bank, pattern }, change: PatternChange::Stop { bank, pattern },
quantization: pattern_data.quantization, quantization: pattern_data.quantization,
sync_mode: pattern_data.sync_mode,
}); });
self.ui self.ui
.set_status(format!("{} armed to stop", bp_label(bank, pattern))); .set_status(format!("{} armed to stop", bp_label(bank, pattern)));
@@ -37,7 +36,6 @@ impl App {
self.playback.staged_changes.push(StagedChange { self.playback.staged_changes.push(StagedChange {
change: PatternChange::Start { bank, pattern }, change: PatternChange::Start { bank, pattern },
quantization: pattern_data.quantization, quantization: pattern_data.quantization,
sync_mode: pattern_data.sync_mode,
}); });
self.ui self.ui
.set_status(format!("{} armed to play", bp_label(bank, pattern))); .set_status(format!("{} armed to play", bp_label(bank, pattern)));
@@ -84,7 +82,6 @@ impl App {
} }
pat.speed = props.speed; pat.speed = props.speed;
pat.quantization = props.quantization; pat.quantization = props.quantization;
pat.sync_mode = props.sync_mode;
pat.follow_up = props.follow_up; pat.follow_up = props.follow_up;
self.project_state.mark_dirty(bank, pattern); self.project_state.mark_dirty(bank, pattern);
} }

View File

@@ -2,7 +2,7 @@
use std::path::PathBuf; use std::path::PathBuf;
use crate::model::{FollowUp, LaunchQuantization, PatternSpeed, SyncMode}; use crate::model::{FollowUp, LaunchQuantization, PatternSpeed};
use crate::page::Page; use crate::page::Page;
use crate::state::{ColorScheme, DeviceKind, Modal, OptionsFocus, PatternField, ScriptField, SettingKind}; use crate::state::{ColorScheme, DeviceKind, Modal, OptionsFocus, PatternField, ScriptField, SettingKind};
@@ -169,7 +169,6 @@ pub enum AppCommand {
length: Option<usize>, length: Option<usize>,
speed: PatternSpeed, speed: PatternSpeed,
quantization: LaunchQuantization, quantization: LaunchQuantization,
sync_mode: SyncMode,
follow_up: FollowUp, follow_up: FollowUp,
}, },

View File

@@ -81,6 +81,20 @@ impl LinkState {
self.link.commit_app_session_state(&state); 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 { pub fn capture_app_state(&self) -> SessionState {
let mut state = SessionState::new(); let mut state = SessionState::new();
self.link.capture_app_session_state(&mut state); self.link.capture_app_session_state(&mut state);

View File

@@ -5,7 +5,7 @@ pub mod realtime;
pub mod sequencer; pub mod sequencer;
mod timing; 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}; pub use audio::{preload_sample_heads, AnalysisHandle, ScopeBuffer, SpectrumBuffer};

View File

@@ -14,11 +14,17 @@ use std::thread::{self, JoinHandle};
use super::dispatcher::{dispatcher_loop, MidiDispatch, TimedMidiCommand}; use super::dispatcher::{dispatcher_loop, MidiDispatch, TimedMidiCommand};
use super::realtime::set_realtime_priority; 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::{ use crate::model::{
CcAccess, Dictionary, ExecutionTrace, Rng, ScriptEngine, StepContext, Value, Variables, 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; use crate::state::LiveKeyState;
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
@@ -114,7 +120,6 @@ pub enum SeqCommand {
bank: usize, bank: usize,
pattern: usize, pattern: usize,
quantization: LaunchQuantization, quantization: LaunchQuantization,
sync_mode: SyncMode,
}, },
PatternStop { PatternStop {
bank: usize, bank: usize,
@@ -141,7 +146,6 @@ pub struct PatternSnapshot {
pub speed: crate::model::PatternSpeed, pub speed: crate::model::PatternSpeed,
pub length: usize, pub length: usize,
pub steps: Vec<StepSnapshot>, pub steps: Vec<StepSnapshot>,
pub sync_mode: SyncMode,
pub follow_up: FollowUp, pub follow_up: FollowUp,
} }
@@ -299,18 +303,24 @@ struct ActivePattern {
step_index: usize, step_index: usize,
iter: usize, iter: usize,
last_step_beat: f64, last_step_beat: f64,
origin_beat: f64,
} }
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
struct PendingPattern { struct PendingPattern {
id: PatternId, id: PatternId,
quantization: LaunchQuantization, target_beat: Option<f64>,
sync_mode: SyncMode, sync_mode: SyncMode,
} }
#[derive(Clone, Copy)]
enum PlayState {
Idle { pause_beat: Option<f64> },
Playing { frontier: f64 },
}
struct AudioState { struct AudioState {
prev_beat: f64, play_state: PlayState,
pause_beat: Option<f64>,
active_patterns: HashMap<PatternId, ActivePattern>, active_patterns: HashMap<PatternId, ActivePattern>,
pending_starts: Vec<PendingPattern>, pending_starts: Vec<PendingPattern>,
pending_stops: Vec<PendingPattern>, pending_stops: Vec<PendingPattern>,
@@ -320,8 +330,7 @@ struct AudioState {
impl AudioState { impl AudioState {
fn new() -> Self { fn new() -> Self {
Self { Self {
prev_beat: -1.0, play_state: PlayState::Idle { pause_beat: None },
pause_beat: None,
active_patterns: HashMap::new(), active_patterns: HashMap::new(),
pending_starts: Vec::new(), pending_starts: Vec::new(),
pending_stops: 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); type StepKey = (usize, usize, usize);
struct RunsCounter { struct RunsCounter {
@@ -582,7 +575,7 @@ pub struct SequencerState {
script_text: String, script_text: String,
script_speed: crate::model::PatternSpeed, script_speed: crate::model::PatternSpeed,
script_length: usize, script_length: usize,
script_frontier: f64, script_frontier: Option<f64>,
script_step: usize, script_step: usize,
script_trace: Option<ExecutionTrace>, script_trace: Option<ExecutionTrace>,
print_output: Option<String>, print_output: Option<String>,
@@ -621,7 +614,7 @@ impl SequencerState {
script_text: String::new(), script_text: String::new(),
script_speed: crate::model::PatternSpeed::default(), script_speed: crate::model::PatternSpeed::default(),
script_length: 16, script_length: 16,
script_frontier: -1.0, script_frontier: None,
script_step: 0, script_step: 0,
script_trace: None, script_trace: None,
print_output: None, print_output: None,
@@ -639,7 +632,7 @@ impl SequencerState {
false false
} }
fn process_commands(&mut self, commands: Vec<SeqCommand>) { fn process_commands(&mut self, commands: Vec<SeqCommand>, quantum: f64) {
for cmd in commands { for cmd in commands {
match cmd { match cmd {
SeqCommand::PatternUpdate { SeqCommand::PatternUpdate {
@@ -660,15 +653,15 @@ impl SequencerState {
bank, bank,
pattern, pattern,
quantization, quantization,
sync_mode,
} => { } => {
let id = PatternId { bank, pattern }; let id = PatternId { bank, pattern };
self.audio_state.pending_stops.retain(|p| p.id != id); self.audio_state.pending_stops.retain(|p| p.id != id);
if !self.audio_state.pending_starts.iter().any(|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 { self.audio_state.pending_starts.push(PendingPattern {
id, id,
quantization, target_beat,
sync_mode, sync_mode: SyncMode::PhaseLock,
}); });
} }
} }
@@ -680,9 +673,10 @@ impl SequencerState {
let id = PatternId { bank, pattern }; let id = PatternId { bank, pattern };
self.audio_state.pending_starts.retain(|p| p.id != id); self.audio_state.pending_starts.retain(|p| p.id != id);
if !self.audio_state.pending_stops.iter().any(|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 { self.audio_state.pending_stops.push(PendingPattern {
id, id,
quantization, target_beat,
sync_mode: SyncMode::Reset, sync_mode: SyncMode::Reset,
}); });
} }
@@ -722,7 +716,8 @@ impl SequencerState {
self.audio_state.active_patterns.clear(); self.audio_state.active_patterns.clear();
self.audio_state.pending_starts.clear(); self.audio_state.pending_starts.clear();
self.audio_state.pending_stops.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.step_traces = Arc::new(HashMap::new());
self.runs_counter.counts.clear(); self.runs_counter.counts.clear();
self.audio_state.flush_midi_notes = true; self.audio_state.flush_midi_notes = true;
@@ -732,9 +727,8 @@ impl SequencerState {
active.step_index = 0; active.step_index = 0;
active.iter = 0; active.iter = 0;
} }
self.audio_state.prev_beat = -1.0; self.audio_state.play_state = PlayState::Idle { pause_beat: None };
self.audio_state.pause_beat = None; self.script_frontier = None;
self.script_frontier = -1.0;
self.script_step = 0; self.script_step = 0;
self.script_trace = None; self.script_trace = None;
self.variables.store(Arc::new(HashMap::new())); self.variables.store(Arc::new(HashMap::new()));
@@ -758,30 +752,26 @@ impl SequencerState {
} }
pub fn tick(&mut self, input: TickInput) -> TickOutput { pub fn tick(&mut self, input: TickInput) -> TickOutput {
self.process_commands(input.commands);
self.last_tempo = input.tempo; self.last_tempo = input.tempo;
self.last_beat = input.beat; self.last_beat = input.beat;
self.last_playing = input.playing; self.last_playing = input.playing;
self.process_commands(input.commands, input.quantum);
if !input.playing { if !input.playing {
return self.tick_paused(); return self.tick_paused();
} }
let frontier = self.audio_state.prev_beat; let (frontier, resuming) = match self.audio_state.play_state {
let lookahead_end = input.lookahead_end; PlayState::Playing { frontier } => (frontier, false),
let resuming = frontier < 0.0; PlayState::Idle { pause_beat } => (pause_beat.unwrap_or(input.beat), true),
let boundary_frontier = if resuming {
self.audio_state.pause_beat.take().unwrap_or(input.beat)
} else {
frontier
}; };
let lookahead_end = input.lookahead_end;
self.activate_pending(lookahead_end, boundary_frontier, input.quantum); self.activate_pending(frontier, lookahead_end);
self.deactivate_pending(lookahead_end, boundary_frontier, input.quantum); self.deactivate_pending(frontier, lookahead_end);
if resuming { if resuming {
self.realign_phaselock_patterns(lookahead_end); self.reset_origins_on_resume(lookahead_end);
} }
let steps = self.execute_steps( let steps = self.execute_steps(
@@ -818,7 +808,7 @@ impl SequencerState {
let new_tempo = self.read_tempo_variable(steps.any_step_fired); let new_tempo = self.read_tempo_variable(steps.any_step_fired);
self.apply_follow_ups(); 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); let flush = std::mem::take(&mut self.audio_state.flush_midi_notes);
TickOutput { TickOutput {
@@ -840,11 +830,12 @@ impl SequencerState {
self.pattern_cache.set(key.0, key.1, snapshot); self.pattern_cache.set(key.0, key.1, snapshot);
} }
} }
if self.audio_state.prev_beat >= 0.0 { let pause_beat = match self.audio_state.play_state {
self.audio_state.pause_beat = Some(self.audio_state.prev_beat); PlayState::Playing { frontier } => Some(frontier),
} PlayState::Idle { pause_beat } => pause_beat,
self.audio_state.prev_beat = -1.0; };
self.script_frontier = -1.0; self.audio_state.play_state = PlayState::Idle { pause_beat };
self.script_frontier = None;
self.script_step = 0; self.script_step = 0;
self.script_trace = None; self.script_trace = None;
self.print_output = None; self.print_output = None;
@@ -858,35 +849,46 @@ impl SequencerState {
} }
} }
fn realign_phaselock_patterns(&mut self, beat: f64) { fn pause_beat(&self) -> Option<f64> {
for (id, active) in &mut self.audio_state.active_patterns { match self.audio_state.play_state {
let Some(pattern) = self.pattern_cache.get(id.bank, id.pattern) else { PlayState::Idle { pause_beat } => pause_beat,
continue; PlayState::Playing { .. } => None,
};
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 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(); self.buf_activated.clear();
for pending in &self.audio_state.pending_starts { 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 { let start_step = match pending.sync_mode {
SyncMode::Reset => 0, SyncMode::Reset => 0,
SyncMode::PhaseLock => { SyncMode::PhaseLock => {
if let Some(pat) = if let Some(pat) =
self.pattern_cache.get(pending.id.bank, pending.id.pattern) self.pattern_cache.get(pending.id.bank, pending.id.pattern)
{ {
let speed_mult = pat.speed.multiplier(); let subs_per_beat = 4.0 * pat.speed.multiplier();
let subs_per_beat = 4.0 * speed_mult; (origin_beat * subs_per_beat).floor() as usize % pat.length
let first_sub = (prev_beat * subs_per_beat).floor() as usize + 1;
first_sub % pat.length
} else { } else {
0 0
} }
@@ -901,7 +903,8 @@ impl SequencerState {
pattern: pending.id.pattern, pattern: pending.id.pattern,
step_index: start_step, step_index: start_step,
iter: 0, iter: 0,
last_step_beat: beat, last_step_beat: lookahead_end,
origin_beat,
}, },
); );
self.buf_activated.push(pending.id); self.buf_activated.push(pending.id);
@@ -913,15 +916,18 @@ impl SequencerState {
.retain(|p| !activated.contains(&p.id)); .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(); self.buf_stopped.clear();
for pending in &self.audio_state.pending_stops { 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); self.audio_state.active_patterns.remove(&pending.id);
Arc::make_mut(&mut self.step_traces).retain(|&(bank, pattern, _), _| { Arc::make_mut(&mut self.step_traces).retain(|&(bank, pattern, _), _| {
bank != pending.id.bank || pattern != pending.id.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); let key = (pending.id.bank, pending.id.pattern);
if let Some(snapshot) = self.pending_updates.remove(&key) { if let Some(snapshot) = self.pending_updates.remove(&key) {
self.pattern_cache.set(key.0, key.1, snapshot); self.pattern_cache.set(key.0, key.1, snapshot);
@@ -981,7 +987,8 @@ impl SequencerState {
.copied() .copied()
.unwrap_or_else(|| pattern.speed.multiplier()); .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 { for step_beat in step_beats {
result.any_step_fired = true; result.any_step_fired = true;
@@ -1116,11 +1123,7 @@ impl SequencerState {
return; return;
} }
let script_frontier = if self.script_frontier < 0.0 { let script_frontier = self.script_frontier.unwrap_or(frontier);
frontier
} else {
self.script_frontier
};
let speed_mult = self.script_speed.multiplier(); let speed_mult = self.script_speed.multiplier();
let fire_beats = substeps_in_window(script_frontier, lookahead_end, speed_mult); let fire_beats = substeps_in_window(script_frontier, lookahead_end, speed_mult);
@@ -1187,7 +1190,7 @@ impl SequencerState {
self.script_step += 1; 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<f64> { fn read_tempo_variable(&self, any_step_fired: bool) -> Option<f64> {
@@ -1220,21 +1223,21 @@ impl SequencerState {
FollowUp::Stop => { FollowUp::Stop => {
self.audio_state.pending_stops.push(PendingPattern { self.audio_state.pending_stops.push(PendingPattern {
id: *completed_id, id: *completed_id,
quantization: LaunchQuantization::Immediate, target_beat: None,
sync_mode: SyncMode::Reset, sync_mode: SyncMode::Reset,
}); });
} }
FollowUp::Chain { bank, pattern } => { FollowUp::Chain { bank, pattern } => {
self.audio_state.pending_stops.push(PendingPattern { self.audio_state.pending_stops.push(PendingPattern {
id: *completed_id, id: *completed_id,
quantization: LaunchQuantization::Immediate, target_beat: None,
sync_mode: SyncMode::Reset, sync_mode: SyncMode::Reset,
}); });
let target = PatternId { bank, pattern }; let target = PatternId { bank, pattern };
if !self.audio_state.pending_starts.iter().any(|p| p.id == target) { if !self.audio_state.pending_starts.iter().any(|p| p.id == target) {
self.audio_state.pending_starts.push(PendingPattern { self.audio_state.pending_starts.push(PendingPattern {
id: target, id: target,
quantization: LaunchQuantization::Immediate, target_beat: None,
sync_mode: SyncMode::Reset, sync_mode: SyncMode::Reset,
}); });
} }
@@ -1325,9 +1328,19 @@ fn sequencer_loop(
let state = link.capture_app_state(); let state = link.capture_app_state();
let current_time_us = link.clock_micros() as SyncTime; 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 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 { let lookahead_beats = if tempo > 0.0 {
lookahead_secs * tempo / 60.0 lookahead_secs * tempo / 60.0
} else { } else {
@@ -1339,7 +1352,7 @@ fn sequencer_loop(
let audio_samples = audio_sample_pos.load(Ordering::Acquire); let audio_samples = audio_sample_pos.load(Ordering::Acquire);
let input = TickInput { let input = TickInput {
commands, commands,
playing: playing.load(Ordering::Relaxed), playing: is_playing,
beat, beat,
lookahead_end, lookahead_end,
tempo, tempo,
@@ -1550,7 +1563,6 @@ mod tests {
source: None, source: None,
}) })
.collect(), .collect(),
sync_mode: SyncMode::Reset,
follow_up: FollowUp::default(), follow_up: FollowUp::default(),
} }
} }
@@ -1621,7 +1633,6 @@ mod tests {
bank: 0, bank: 0,
pattern: 0, pattern: 0,
quantization: LaunchQuantization::Immediate, quantization: LaunchQuantization::Immediate,
sync_mode: SyncMode::Reset,
}, },
], ],
1.0, 1.0,
@@ -1649,7 +1660,6 @@ mod tests {
bank: 0, bank: 0,
pattern: 0, pattern: 0,
quantization: LaunchQuantization::Immediate, quantization: LaunchQuantization::Immediate,
sync_mode: SyncMode::Reset,
}], }],
1.0, 1.0,
)); ));
@@ -1697,7 +1707,6 @@ mod tests {
bank: 0, bank: 0,
pattern: 0, pattern: 0,
quantization: LaunchQuantization::Immediate, quantization: LaunchQuantization::Immediate,
sync_mode: SyncMode::Reset,
}], }],
0.5, 0.5,
)); ));
@@ -1706,43 +1715,32 @@ mod tests {
} }
#[test] #[test]
fn test_quantization_boundaries() { fn test_next_boundary() {
assert!(check_quantization_boundary( use super::super::next_boundary;
LaunchQuantization::Immediate,
1.5, // Immediate returns None
1.0, assert_eq!(next_boundary(1.5, LaunchQuantization::Immediate, 4.0), None);
4.0
)); // Beat: next integer beat
assert!(check_quantization_boundary( assert_eq!(next_boundary(1.5, LaunchQuantization::Beat, 4.0), Some(2.0));
LaunchQuantization::Beat, assert_eq!(next_boundary(1.9, LaunchQuantization::Beat, 4.0), Some(2.0));
2.0, // On exact beat boundary, targets next beat
1.9, assert_eq!(next_boundary(2.0, LaunchQuantization::Beat, 4.0), Some(3.0));
4.0
)); // Bar (quantum=4): next multiple of 4
assert!(!check_quantization_boundary( assert_eq!(next_boundary(3.5, LaunchQuantization::Bar, 4.0), Some(4.0));
LaunchQuantization::Beat, assert_eq!(next_boundary(3.9, LaunchQuantization::Bar, 4.0), Some(4.0));
1.5, // On exact bar boundary, targets next bar
1.2, assert_eq!(next_boundary(4.0, LaunchQuantization::Bar, 4.0), Some(8.0));
4.0
)); // Bars2 (quantum=4): next multiple of 8
assert!(check_quantization_boundary( assert_eq!(next_boundary(3.5, LaunchQuantization::Bars2, 4.0), Some(8.0));
LaunchQuantization::Bar,
4.0, // Bars4 (quantum=4): next multiple of 16
3.9, assert_eq!(next_boundary(3.5, LaunchQuantization::Bars4, 4.0), Some(16.0));
4.0
)); // Bars8 (quantum=4): next multiple of 32
assert!(!check_quantization_boundary( assert_eq!(next_boundary(3.5, LaunchQuantization::Bars8, 4.0), Some(32.0));
LaunchQuantization::Bar,
3.5,
3.2,
4.0
));
assert!(!check_quantization_boundary(
LaunchQuantization::Immediate,
1.0,
-1.0,
4.0
));
} }
#[test] #[test]
@@ -1763,7 +1761,6 @@ mod tests {
bank: 0, bank: 0,
pattern: 0, pattern: 0,
quantization: LaunchQuantization::Immediate, quantization: LaunchQuantization::Immediate,
sync_mode: SyncMode::Reset,
}], }],
0.5, 0.5,
)); ));
@@ -1808,7 +1805,6 @@ mod tests {
bank: 0, bank: 0,
pattern: 0, pattern: 0,
quantization: LaunchQuantization::Immediate, quantization: LaunchQuantization::Immediate,
sync_mode: SyncMode::Reset,
}], }],
0.5, 0.5,
)); ));
@@ -1844,7 +1840,6 @@ mod tests {
bank: 0, bank: 0,
pattern: 0, pattern: 0,
quantization: LaunchQuantization::Immediate, quantization: LaunchQuantization::Immediate,
sync_mode: SyncMode::Reset,
}, },
SeqCommand::PatternStop { SeqCommand::PatternStop {
bank: 0, bank: 0,
@@ -1879,13 +1874,11 @@ mod tests {
bank: 0, bank: 0,
pattern: 0, pattern: 0,
quantization: LaunchQuantization::Bar, quantization: LaunchQuantization::Bar,
sync_mode: SyncMode::Reset,
}, },
SeqCommand::PatternStart { SeqCommand::PatternStart {
bank: 0, bank: 0,
pattern: 1, pattern: 1,
quantization: LaunchQuantization::Beat, quantization: LaunchQuantization::Beat,
sync_mode: SyncMode::Reset,
}, },
], ],
0.0, 0.0,
@@ -1921,7 +1914,6 @@ mod tests {
bank: 0, bank: 0,
pattern: 0, pattern: 0,
quantization: LaunchQuantization::Immediate, quantization: LaunchQuantization::Immediate,
sync_mode: SyncMode::Reset,
}], }],
0.5, 0.5,
)); ));
@@ -1990,7 +1982,6 @@ mod tests {
bank: 0, bank: 0,
pattern: 0, pattern: 0,
quantization: LaunchQuantization::Immediate, quantization: LaunchQuantization::Immediate,
sync_mode: SyncMode::Reset,
}], }],
..tick_at(1.0, false) ..tick_at(1.0, false)
}); });
@@ -2019,7 +2010,6 @@ mod tests {
bank: 0, bank: 0,
pattern: 0, pattern: 0,
quantization: LaunchQuantization::Immediate, quantization: LaunchQuantization::Immediate,
sync_mode: SyncMode::Reset,
}], }],
1.0, 1.0,
)); ));
@@ -2049,13 +2039,11 @@ mod tests {
bank: 0, bank: 0,
pattern: 0, pattern: 0,
quantization: LaunchQuantization::Bar, quantization: LaunchQuantization::Bar,
sync_mode: SyncMode::Reset,
}, },
SeqCommand::PatternStart { SeqCommand::PatternStart {
bank: 0, bank: 0,
pattern: 0, pattern: 0,
quantization: LaunchQuantization::Bar, quantization: LaunchQuantization::Bar,
sync_mode: SyncMode::Reset,
}, },
], ],
0.0, 0.0,
@@ -2088,7 +2076,6 @@ mod tests {
bank: 0, bank: 0,
pattern: 0, pattern: 0,
quantization: LaunchQuantization::Immediate, quantization: LaunchQuantization::Immediate,
sync_mode: SyncMode::Reset,
}], }],
0.5, 0.5,
)); ));
@@ -2115,7 +2102,6 @@ mod tests {
source: None, source: None,
}) })
.collect(), .collect(),
sync_mode: SyncMode::Reset,
follow_up: FollowUp::default(), follow_up: FollowUp::default(),
} }
} }
@@ -2138,7 +2124,6 @@ mod tests {
bank: 0, bank: 0,
pattern: 0, pattern: 0,
quantization: LaunchQuantization::Immediate, quantization: LaunchQuantization::Immediate,
sync_mode: SyncMode::Reset,
}], }],
0.5, 0.5,
)); ));
@@ -2186,13 +2171,11 @@ mod tests {
bank: 0, bank: 0,
pattern: 0, pattern: 0,
quantization: LaunchQuantization::Immediate, quantization: LaunchQuantization::Immediate,
sync_mode: SyncMode::Reset,
}, },
SeqCommand::PatternStart { SeqCommand::PatternStart {
bank: 0, bank: 0,
pattern: 1, pattern: 1,
quantization: LaunchQuantization::Immediate, quantization: LaunchQuantization::Immediate,
sync_mode: SyncMode::Reset,
}, },
], ],
0.5, 0.5,
@@ -2227,7 +2210,6 @@ mod tests {
bank: 0, bank: 0,
pattern: 0, pattern: 0,
quantization: LaunchQuantization::Bar, quantization: LaunchQuantization::Bar,
sync_mode: SyncMode::Reset,
}], }],
3.5, 3.5,
)); ));
@@ -2266,7 +2248,6 @@ mod tests {
bank: 0, bank: 0,
pattern: 0, pattern: 0,
quantization: LaunchQuantization::Immediate, quantization: LaunchQuantization::Immediate,
sync_mode: SyncMode::Reset,
}], }],
..tick_at(2.0, false) ..tick_at(2.0, false)
}); });
@@ -2306,13 +2287,11 @@ mod tests {
bank: 0, bank: 0,
pattern: 0, pattern: 0,
quantization: LaunchQuantization::Bar, quantization: LaunchQuantization::Bar,
sync_mode: SyncMode::Reset,
}, },
SeqCommand::PatternStart { SeqCommand::PatternStart {
bank: 0, bank: 0,
pattern: 1, pattern: 1,
quantization: LaunchQuantization::Bar, quantization: LaunchQuantization::Bar,
sync_mode: SyncMode::Reset,
}, },
], ],
3.5, 3.5,
@@ -2344,7 +2323,6 @@ mod tests {
source: None, source: None,
}) })
.collect(), .collect(),
sync_mode: SyncMode::PhaseLock,
follow_up: FollowUp::default(), follow_up: FollowUp::default(),
} }
} }
@@ -2368,7 +2346,6 @@ mod tests {
bank: 0, bank: 0,
pattern: 0, pattern: 0,
quantization: LaunchQuantization::Bar, quantization: LaunchQuantization::Bar,
sync_mode: SyncMode::PhaseLock,
}], }],
3.5, 3.5,
)); ));
@@ -2410,7 +2387,6 @@ mod tests {
bank: 0, bank: 0,
pattern: 0, pattern: 0,
quantization: LaunchQuantization::Bar, quantization: LaunchQuantization::Bar,
sync_mode: SyncMode::Reset,
}], }],
1.0, 1.0,
)); ));

View File

@@ -1,26 +1,28 @@
use crate::model::LaunchQuantization;
/// Microsecond-precision timestamp for audio synchronization. /// Microsecond-precision timestamp for audio synchronization.
pub type SyncTime = u64; pub type SyncTime = u64;
/// Timing boundary types for step and pattern scheduling. /// Compute the exact next quantization boundary beat after `current_beat`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)] /// Returns `None` for Immediate (activate now), `Some(beat)` for all others.
pub enum StepTiming { pub fn next_boundary(current_beat: f64, quantization: LaunchQuantization, quantum: f64) -> Option<f64> {
/// Fire when a beat boundary is crossed. match quantization {
NextBeat, LaunchQuantization::Immediate => None,
/// Fire when a bar/quantum boundary is crossed. LaunchQuantization::Beat => Some(current_beat.floor() + 1.0),
NextBar, LaunchQuantization::Bar => {
} Some((current_beat / quantum).floor() * quantum + quantum)
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;
} }
match self { LaunchQuantization::Bars2 => {
Self::NextBeat => prev_beat.floor() as i64 != curr_beat.floor() as i64, let p = quantum * 2.0;
Self::NextBar => { Some((current_beat / p).floor() * p + p)
(prev_beat / quantum).floor() as i64 != (curr_beat / quantum).floor() as i64 }
} 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. /// Each entry is the exact beat at which that substep fires.
/// Clamped to 64 results max to prevent runaway. /// Clamped to 64 results max to prevent runaway.
pub fn substeps_in_window(frontier: f64, end: f64, speed: f64) -> Vec<f64> { pub fn substeps_in_window(frontier: f64, end: f64, speed: f64) -> Vec<f64> {
if frontier < 0.0 || end <= frontier || speed <= 0.0 { if end <= frontier || speed <= 0.0 {
return Vec::new(); return Vec::new();
} }
let substeps_per_beat = 4.0 * speed; 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 { 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 prev_substep = (prev_beat * 4.0 * speed).floor() as i64;
let curr_substep = (curr_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 (curr_substep - prev_substep).clamp(0, 16) as usize
@@ -88,23 +87,15 @@ mod tests {
} }
#[test] #[test]
fn test_step_timing_beat_crossed() { fn test_next_boundary() {
// Crossing from beat 0 to beat 1 assert_eq!(next_boundary(1.5, LaunchQuantization::Immediate, 4.0), None);
assert!(StepTiming::NextBeat.crossed(0.9, 1.1, 4.0)); assert_eq!(next_boundary(1.5, LaunchQuantization::Beat, 4.0), Some(2.0));
// Not crossing (both in same beat) assert_eq!(next_boundary(2.0, LaunchQuantization::Beat, 4.0), Some(3.0));
assert!(!StepTiming::NextBeat.crossed(0.5, 0.9, 4.0)); assert_eq!(next_boundary(3.5, LaunchQuantization::Bar, 4.0), Some(4.0));
// Negative prev_beat returns false assert_eq!(next_boundary(4.0, LaunchQuantization::Bar, 4.0), Some(8.0));
assert!(!StepTiming::NextBeat.crossed(-1.0, 1.0, 4.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]
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));
} }
#[test] #[test]
@@ -126,9 +117,9 @@ mod tests {
} }
#[test] #[test]
fn test_substeps_crossed_negative_prev() { fn test_substeps_crossed_same_position() {
// Negative prev_beat returns 0 // Same position returns 0
assert_eq!(substeps_crossed(-1.0, 0.5, 1.0), 0); assert_eq!(substeps_crossed(0.5, 0.5, 1.0), 0);
} }
#[test] #[test]
@@ -205,8 +196,9 @@ mod tests {
} }
#[test] #[test]
fn test_substeps_in_window_negative_frontier() { fn test_substeps_in_window_reversed() {
let result = substeps_in_window(-1.0, 0.5, 1.0); // end <= frontier returns empty
let result = substeps_in_window(0.5, 0.3, 1.0);
assert!(result.is_empty()); assert!(result.is_empty());
} }
} }

View File

@@ -83,8 +83,7 @@ pub fn init(args: InitArgs) -> Init {
for (bank, pattern) in playing { for (bank, pattern) in playing {
app.playback.queued_changes.push(StagedChange { app.playback.queued_changes.push(StagedChange {
change: PatternChange::Start { bank, pattern }, change: PatternChange::Start { bank, pattern },
quantization: model::LaunchQuantization::Immediate, quantization: model::LaunchQuantization::Bar,
sync_mode: model::SyncMode::PhaseLock,
}); });
} }
app.ui.set_status(format!("Demo: {}", demo.name)); app.ui.set_status(format!("Demo: {}", demo.name));
@@ -96,8 +95,7 @@ pub fn init(args: InitArgs) -> Init {
bank: 0, bank: 0,
pattern: 0, pattern: 0,
}, },
quantization: model::LaunchQuantization::Immediate, quantization: model::LaunchQuantization::Bar,
sync_mode: model::SyncMode::PhaseLock,
}); });
} }

View File

@@ -461,7 +461,6 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input
length, length,
speed, speed,
quantization, quantization,
sync_mode,
follow_up, follow_up,
} => { } => {
let (bank, pattern) = (*bank, *pattern); 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 { KeyCode::Left => match field {
PatternPropsField::Speed => *speed = speed.prev(), PatternPropsField::Speed => *speed = speed.prev(),
PatternPropsField::Quantization => *quantization = quantization.prev(), PatternPropsField::Quantization => *quantization = quantization.prev(),
PatternPropsField::SyncMode => *sync_mode = sync_mode.toggle(),
PatternPropsField::FollowUp => *follow_up = follow_up.prev_mode(), PatternPropsField::FollowUp => *follow_up = follow_up.prev_mode(),
PatternPropsField::ChainBank => { PatternPropsField::ChainBank => {
if let FollowUp::Chain { bank: b, .. } = follow_up { 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 { KeyCode::Right => match field {
PatternPropsField::Speed => *speed = speed.next(), PatternPropsField::Speed => *speed = speed.next(),
PatternPropsField::Quantization => *quantization = quantization.next(), PatternPropsField::Quantization => *quantization = quantization.next(),
PatternPropsField::SyncMode => *sync_mode = sync_mode.toggle(),
PatternPropsField::FollowUp => *follow_up = follow_up.next_mode(), PatternPropsField::FollowUp => *follow_up = follow_up.next_mode(),
PatternPropsField::ChainBank => { PatternPropsField::ChainBank => {
if let FollowUp::Chain { bank: b, .. } = follow_up { 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 length_val = length.parse().ok();
let speed_val = *speed; let speed_val = *speed;
let quant_val = *quantization; let quant_val = *quantization;
let sync_val = *sync_mode;
let follow_up_val = *follow_up; let follow_up_val = *follow_up;
ctx.dispatch(AppCommand::StagePatternProps { ctx.dispatch(AppCommand::StagePatternProps {
bank, bank,
@@ -545,7 +541,6 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input
length: length_val, length: length_val,
speed: speed_val, speed: speed_val,
quantization: quant_val, quantization: quant_val,
sync_mode: sync_val,
follow_up: follow_up_val, follow_up: follow_up_val,
}); });
ctx.dispatch(AppCommand::CloseModal); ctx.dispatch(AppCommand::CloseModal);

View File

@@ -11,7 +11,7 @@ pub use cagire_forth::{
}; };
pub use cagire_project::{ pub use cagire_project::{
load, load_str, save, share, Bank, FollowUp, LaunchQuantization, Pattern, PatternSpeed, 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; pub use script::ScriptEngine;

View File

@@ -31,7 +31,6 @@ pub enum PatternPropsField {
Length, Length,
Speed, Speed,
Quantization, Quantization,
SyncMode,
FollowUp, FollowUp,
ChainBank, ChainBank,
ChainPattern, ChainPattern,
@@ -44,8 +43,7 @@ impl PatternPropsField {
Self::Description => Self::Length, Self::Description => Self::Length,
Self::Length => Self::Speed, Self::Length => Self::Speed,
Self::Speed => Self::Quantization, Self::Speed => Self::Quantization,
Self::Quantization => Self::SyncMode, Self::Quantization => Self::FollowUp,
Self::SyncMode => Self::FollowUp,
Self::FollowUp if follow_up_is_chain => Self::ChainBank, Self::FollowUp if follow_up_is_chain => Self::ChainBank,
Self::FollowUp => Self::FollowUp, Self::FollowUp => Self::FollowUp,
Self::ChainBank => Self::ChainPattern, Self::ChainBank => Self::ChainPattern,
@@ -60,8 +58,7 @@ impl PatternPropsField {
Self::Length => Self::Description, Self::Length => Self::Description,
Self::Speed => Self::Length, Self::Speed => Self::Length,
Self::Quantization => Self::Speed, Self::Quantization => Self::Speed,
Self::SyncMode => Self::Quantization, Self::FollowUp => Self::Quantization,
Self::FollowUp => Self::SyncMode,
Self::ChainBank => Self::FollowUp, Self::ChainBank => Self::FollowUp,
Self::ChainPattern if follow_up_is_chain => Self::ChainBank, Self::ChainPattern if follow_up_is_chain => Self::ChainBank,
Self::ChainPattern => Self::FollowUp, Self::ChainPattern => Self::FollowUp,

View File

@@ -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::editor::{EuclideanField, PatternField, PatternPropsField, ScriptField};
use crate::state::file_browser::FileBrowserState; use crate::state::file_browser::FileBrowserState;
@@ -85,7 +85,6 @@ pub enum Modal {
length: String, length: String,
speed: PatternSpeed, speed: PatternSpeed,
quantization: LaunchQuantization, quantization: LaunchQuantization,
sync_mode: SyncMode,
follow_up: FollowUp, follow_up: FollowUp,
}, },
KeybindingsHelp { KeybindingsHelp {

View File

@@ -1,12 +1,11 @@
use crate::engine::PatternChange; use crate::engine::PatternChange;
use crate::model::{FollowUp, LaunchQuantization, PatternSpeed, SyncMode}; use crate::model::{FollowUp, LaunchQuantization, PatternSpeed};
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
#[derive(Clone)] #[derive(Clone)]
pub struct StagedChange { pub struct StagedChange {
pub change: PatternChange, pub change: PatternChange,
pub quantization: LaunchQuantization, pub quantization: LaunchQuantization,
pub sync_mode: SyncMode,
} }
#[derive(Clone, Copy, PartialEq, Eq, Hash)] #[derive(Clone, Copy, PartialEq, Eq, Hash)]
@@ -21,7 +20,6 @@ pub struct StagedPropChange {
pub length: Option<usize>, pub length: Option<usize>,
pub speed: PatternSpeed, pub speed: PatternSpeed,
pub quantization: LaunchQuantization, pub quantization: LaunchQuantization,
pub sync_mode: SyncMode,
pub follow_up: FollowUp, pub follow_up: FollowUp,
} }

View File

@@ -507,11 +507,7 @@ fn render_patterns(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, a
}; };
let props_indicator = if has_staged_props { "~" } else { "" }; let props_indicator = if has_staged_props { "~" } else { "" };
let quant_sync = if is_selected { let quant_sync = if is_selected {
format!( format!("{} ", pattern.quantization.short_label())
"{}:{} ",
pattern.quantization.short_label(),
pattern.sync_mode.short_label()
)
} else { } else {
String::new() String::new()
}; };
@@ -755,8 +751,6 @@ fn render_properties(
let steps_label = format!("{}/{}", content_count, pattern.length); let steps_label = format!("{}/{}", content_count, pattern.length);
let speed_label = pattern.speed.label(); let speed_label = pattern.speed.label();
let quant_label = pattern.quantization.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 label_style = Style::new().fg(theme.ui.text_muted);
let value_style = Style::new().fg(theme.ui.text_primary); 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_style),
Span::styled(quant_label, value_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 { if pattern.follow_up != FollowUp::Loop {

View File

@@ -737,7 +737,6 @@ fn render_modal(
length, length,
speed, speed,
quantization, quantization,
sync_mode,
follow_up, follow_up,
} => { } => {
use crate::model::FollowUp; use crate::model::FollowUp;
@@ -766,7 +765,6 @@ fn render_modal(
("Length", length.clone(), *field == PatternPropsField::Length), ("Length", length.clone(), *field == PatternPropsField::Length),
("Speed", speed_label, *field == PatternPropsField::Speed), ("Speed", speed_label, *field == PatternPropsField::Speed),
("Quantization", quantization.label().to_string(), *field == PatternPropsField::Quantization), ("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), ("Follow Up", follow_up_label, *field == PatternPropsField::FollowUp),
]; ];
if is_chain { if is_chain {