From 2851785e0d7379736eed629bdbf5487bc5d3f093 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Forment?= Date: Fri, 30 Jan 2026 00:04:25 +0100 Subject: [PATCH] WIP: consolidate sampling --- Cargo.toml | 2 +- src/engine/audio.rs | 2 +- src/engine/sequencer.rs | 195 +++++++++++++++++++++++++++++----------- src/input.rs | 9 +- src/main.rs | 9 +- 5 files changed, 158 insertions(+), 59 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index df220f6..30a1aa9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ path = "src/main.rs" cagire-forth = { path = "crates/forth" } cagire-project = { path = "crates/project" } cagire-ratatui = { path = "crates/ratatui" } -doux = { git = "https://github.com/sova-org/doux", features = ["native"] } +doux = { path = "/Users/bubo/doux", features = ["native"] } rusty_link = "0.4" ratatui = "0.29" crossterm = "0.28" diff --git a/src/engine/audio.rs b/src/engine/audio.rs index 7e4b349..6deb8eb 100644 --- a/src/engine/audio.rs +++ b/src/engine/audio.rs @@ -224,7 +224,7 @@ pub fn build_stream( scope_buffer: Arc, spectrum_buffer: Arc, metrics: Arc, - initial_samples: Vec, + initial_samples: Vec, audio_sample_pos: Arc, ) -> Result<(Stream, f32, AnalysisHandle), String> { let host = cpal::default_host(); diff --git a/src/engine/sequencer.rs b/src/engine/sequencer.rs index badbf88..e3eb134 100644 --- a/src/engine/sequencer.rs +++ b/src/engine/sequencer.rs @@ -40,10 +40,13 @@ impl PatternChange { } pub enum AudioCommand { - Evaluate { cmd: String, time: Option }, + Evaluate { + cmd: String, + time: Option, + }, Hush, Panic, - LoadSamples(Vec), + LoadSamples(Vec), #[allow(dead_code)] ResetEngine, } @@ -444,11 +447,7 @@ pub(crate) struct SequencerState { } impl SequencerState { - pub fn new( - variables: Variables, - dict: Dictionary, - rng: Rng, - ) -> Self { + pub fn new(variables: Variables, dict: Dictionary, rng: Rng) -> Self { let script_engine = ScriptEngine::new(Arc::clone(&variables), dict, rng); Self { audio_state: AudioState::new(), @@ -529,10 +528,14 @@ impl SequencerState { let prev_beat = self.audio_state.prev_beat; let activated = self.activate_pending(beat, prev_beat, input.quantum); - self.audio_state.pending_starts.retain(|p| !activated.contains(&p.id)); + self.audio_state + .pending_starts + .retain(|p| !activated.contains(&p.id)); let stopped = self.deactivate_pending(beat, prev_beat, input.quantum); - self.audio_state.pending_stops.retain(|p| !stopped.contains(&p.id)); + self.audio_state + .pending_stops + .retain(|p| !stopped.contains(&p.id)); let steps = self.execute_steps( beat, @@ -582,7 +585,9 @@ impl SequencerState { 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) { + if let Some(pat) = + self.pattern_cache.get(pending.id.bank, pending.id.pattern) + { let speed_mult = pat.speed.multiplier(); ((beat * 4.0 * speed_mult) as usize) % pat.length } else { @@ -654,7 +659,8 @@ impl SequencerState { continue; }; - let speed_mult = self.speed_overrides + let speed_mult = self + .speed_overrides .get(&(active.bank, active.pattern)) .copied() .unwrap_or_else(|| pattern.speed.multiplier()); @@ -777,14 +783,24 @@ impl SequencerState { fn apply_chain_transitions(&mut self, transitions: Vec<(PatternId, PatternId)>) { for (source, target) in transitions { - if !self.audio_state.pending_stops.iter().any(|p| p.id == source) { + if !self + .audio_state + .pending_stops + .iter() + .any(|p| p.id == source) + { self.audio_state.pending_stops.push(PendingPattern { id: source, quantization: LaunchQuantization::Bar, sync_mode: SyncMode::Reset, }); } - if !self.audio_state.pending_starts.iter().any(|p| p.id == target) { + if !self + .audio_state + .pending_starts + .iter() + .any(|p| p.id == target) + { let (quant, sync) = self .pattern_cache .get(target.bank, target.pattern) @@ -858,7 +874,11 @@ fn sequencer_loop( let sr = sample_rate.load(Ordering::Relaxed) as f64; let audio_samples = audio_sample_pos.load(Ordering::Relaxed); - let engine_time = if sr > 0.0 { audio_samples as f64 / sr } else { 0.0 }; + let engine_time = if sr > 0.0 { + audio_samples as f64 / sr + } else { + 0.0 + }; let lookahead_secs = lookahead_ms.load(Ordering::Relaxed) as f64 / 1000.0; let input = TickInput { @@ -877,7 +897,10 @@ fn sequencer_loop( let output = seq_state.tick(input); for tsc in output.audio_commands { - let cmd = AudioCommand::Evaluate { cmd: tsc.cmd, time: tsc.time }; + let cmd = AudioCommand::Evaluate { + cmd: tsc.cmd, + time: tsc.time, + }; match audio_tx.load().try_send(cmd) { Ok(()) => {} Err(TrySendError::Full(_) | TrySendError::Disconnected(_)) => { @@ -1087,7 +1110,11 @@ mod tests { )); assert!(output.shared_state.active_patterns.is_empty()); - assert!(!state.audio_state.pending_starts.iter().any(|p| p.id == pid(0, 1))); + assert!(!state + .audio_state + .pending_starts + .iter() + .any(|p| p.id == pid(0, 1))); } #[test] @@ -1128,22 +1155,40 @@ mod tests { #[test] fn test_quantization_boundaries() { assert!(check_quantization_boundary( - LaunchQuantization::Immediate, 1.5, 1.0, 4.0 + LaunchQuantization::Immediate, + 1.5, + 1.0, + 4.0 )); assert!(check_quantization_boundary( - LaunchQuantization::Beat, 2.0, 1.9, 4.0 + LaunchQuantization::Beat, + 2.0, + 1.9, + 4.0 )); assert!(!check_quantization_boundary( - LaunchQuantization::Beat, 1.5, 1.2, 4.0 + LaunchQuantization::Beat, + 1.5, + 1.2, + 4.0 )); assert!(check_quantization_boundary( - LaunchQuantization::Bar, 4.0, 3.9, 4.0 + LaunchQuantization::Bar, + 4.0, + 3.9, + 4.0 )); assert!(!check_quantization_boundary( - LaunchQuantization::Bar, 3.5, 3.2, 4.0 + LaunchQuantization::Bar, + 3.5, + 3.2, + 4.0 )); assert!(!check_quantization_boundary( - LaunchQuantization::Immediate, 1.0, -1.0, 4.0 + LaunchQuantization::Immediate, + 1.0, + -1.0, + 4.0 )); } @@ -1153,14 +1198,17 @@ mod tests { state.tick(tick_with( vec![SeqCommand::PatternUpdate { - bank: 0, pattern: 0, data: simple_pattern(2), + bank: 0, + pattern: 0, + data: simple_pattern(2), }], 0.0, )); state.tick(tick_with( vec![SeqCommand::PatternStart { - bank: 0, pattern: 0, + bank: 0, + pattern: 0, quantization: LaunchQuantization::Immediate, sync_mode: SyncMode::Reset, }], @@ -1190,13 +1238,18 @@ mod tests { pat.speed = crate::model::PatternSpeed::DOUBLE; state.tick(tick_with( - vec![SeqCommand::PatternUpdate { bank: 0, pattern: 0, data: pat }], + vec![SeqCommand::PatternUpdate { + bank: 0, + pattern: 0, + data: pat, + }], 0.0, )); state.tick(tick_with( vec![SeqCommand::PatternStart { - bank: 0, pattern: 0, + bank: 0, + pattern: 0, quantization: LaunchQuantization::Immediate, sync_mode: SyncMode::Reset, }], @@ -1214,7 +1267,9 @@ mod tests { state.tick(tick_with( vec![SeqCommand::PatternUpdate { - bank: 0, pattern: 0, data: simple_pattern(4), + bank: 0, + pattern: 0, + data: simple_pattern(4), }], 0.0, )); @@ -1223,12 +1278,14 @@ mod tests { state.tick(tick_with( vec![ SeqCommand::PatternStart { - bank: 0, pattern: 0, + bank: 0, + pattern: 0, quantization: LaunchQuantization::Immediate, sync_mode: SyncMode::Reset, }, SeqCommand::PatternStop { - bank: 0, pattern: 0, + bank: 0, + pattern: 0, quantization: LaunchQuantization::Immediate, }, ], @@ -1246,13 +1303,18 @@ mod tests { state.tick(tick_with( vec![ SeqCommand::PatternUpdate { - bank: 0, pattern: 0, data: simple_pattern(1), + bank: 0, + pattern: 0, + data: simple_pattern(1), }, SeqCommand::PatternUpdate { - bank: 0, pattern: 1, data: simple_pattern(4), + bank: 0, + pattern: 1, + data: simple_pattern(4), }, SeqCommand::PatternStart { - bank: 0, pattern: 0, + bank: 0, + pattern: 0, quantization: LaunchQuantization::Immediate, sync_mode: SyncMode::Reset, }, @@ -1279,14 +1341,19 @@ mod tests { // Chain guard should block transition to pattern 1. state.tick(tick_with( vec![SeqCommand::PatternStop { - bank: 0, pattern: 0, + bank: 0, + pattern: 0, quantization: LaunchQuantization::Immediate, }], 1.0, )); assert!(!state.audio_state.active_patterns.contains_key(&pid(0, 1))); - assert!(!state.audio_state.pending_starts.iter().any(|p| p.id == pid(0, 1))); + assert!(!state + .audio_state + .pending_starts + .iter() + .any(|p| p.id == pid(0, 1))); } #[test] @@ -1296,18 +1363,24 @@ mod tests { state.tick(tick_with( vec![ SeqCommand::PatternUpdate { - bank: 0, pattern: 0, data: simple_pattern(4), + bank: 0, + pattern: 0, + data: simple_pattern(4), }, SeqCommand::PatternUpdate { - bank: 0, pattern: 1, data: simple_pattern(4), + bank: 0, + pattern: 1, + data: simple_pattern(4), }, SeqCommand::PatternStart { - bank: 0, pattern: 0, + bank: 0, + pattern: 0, quantization: LaunchQuantization::Bar, sync_mode: SyncMode::Reset, }, SeqCommand::PatternStart { - bank: 0, pattern: 1, + bank: 0, + pattern: 1, quantization: LaunchQuantization::Beat, sync_mode: SyncMode::Reset, }, @@ -1334,13 +1407,16 @@ mod tests { state.tick(tick_with( vec![SeqCommand::PatternUpdate { - bank: 0, pattern: 0, data: simple_pattern(4), + bank: 0, + pattern: 0, + data: simple_pattern(4), }], 0.0, )); state.tick(tick_with( vec![SeqCommand::PatternStart { - bank: 0, pattern: 0, + bank: 0, + pattern: 0, quantization: LaunchQuantization::Immediate, sync_mode: SyncMode::Reset, }], @@ -1357,7 +1433,9 @@ mod tests { // beat=1.25: beat_int=5, prev=4, step fires. step_index=3%2=1 fires, advances to (3+1)%2=0 state.tick(tick_with( vec![SeqCommand::PatternUpdate { - bank: 0, pattern: 0, data: simple_pattern(2), + bank: 0, + pattern: 0, + data: simple_pattern(2), }], 1.25, )); @@ -1376,7 +1454,9 @@ mod tests { state.tick(tick_with( vec![SeqCommand::PatternUpdate { - bank: 0, pattern: 0, data: simple_pattern(4), + bank: 0, + pattern: 0, + data: simple_pattern(4), }], 0.0, )); @@ -1384,7 +1464,8 @@ mod tests { // Start while paused: pending_starts gets cleared state.tick(TickInput { commands: vec![SeqCommand::PatternStart { - bank: 0, pattern: 0, + bank: 0, + pattern: 0, quantization: LaunchQuantization::Immediate, sync_mode: SyncMode::Reset, }], @@ -1404,13 +1485,16 @@ mod tests { state.tick(tick_with( vec![SeqCommand::PatternUpdate { - bank: 0, pattern: 0, data: simple_pattern(4), + bank: 0, + pattern: 0, + data: simple_pattern(4), }], 0.0, )); state.tick(tick_with( vec![SeqCommand::PatternStart { - bank: 0, pattern: 0, + bank: 0, + pattern: 0, quantization: LaunchQuantization::Immediate, sync_mode: SyncMode::Reset, }], @@ -1434,15 +1518,19 @@ mod tests { state.tick(tick_with( vec![ SeqCommand::PatternUpdate { - bank: 0, pattern: 0, data: simple_pattern(4), + bank: 0, + pattern: 0, + data: simple_pattern(4), }, SeqCommand::PatternStart { - bank: 0, pattern: 0, + bank: 0, + pattern: 0, quantization: LaunchQuantization::Bar, sync_mode: SyncMode::Reset, }, SeqCommand::PatternStart { - bank: 0, pattern: 0, + bank: 0, + pattern: 0, quantization: LaunchQuantization::Bar, sync_mode: SyncMode::Reset, }, @@ -1450,7 +1538,9 @@ mod tests { 0.0, )); - let pending_count = state.audio_state.pending_starts + let pending_count = state + .audio_state + .pending_starts .iter() .filter(|p| p.id == pid(0, 0)) .count(); @@ -1464,13 +1554,16 @@ mod tests { // Pattern of length 16 — won't complete iteration for many ticks state.tick(tick_with( vec![SeqCommand::PatternUpdate { - bank: 0, pattern: 0, data: simple_pattern(16), + bank: 0, + pattern: 0, + data: simple_pattern(16), }], 0.0, )); state.tick(tick_with( vec![SeqCommand::PatternStart { - bank: 0, pattern: 0, + bank: 0, + pattern: 0, quantization: LaunchQuantization::Immediate, sync_mode: SyncMode::Reset, }], diff --git a/src/input.rs b/src/input.rs index 716c053..b5af879 100644 --- a/src/input.rs +++ b/src/input.rs @@ -414,7 +414,7 @@ fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult { } }; if let Some(path) = sample_path { - let index = doux::loader::scan_samples_dir(&path); + let index = doux::sampling::scan_samples_dir(&path); let count = index.len(); let _ = ctx.audio_tx.load().send(AudioCommand::LoadSamples(index)); ctx.app.audio.config.sample_count += count; @@ -698,7 +698,10 @@ fn handle_panel_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult { let folder = &entry.folder; let idx = entry.index; let cmd = format!("/sound/{folder}/n/{idx}/gain/0.5/dur/1"); - let _ = ctx.audio_tx.load().send(AudioCommand::Evaluate { cmd, time: None }); + let _ = ctx + .audio_tx + .load() + .send(AudioCommand::Evaluate { cmd, time: None }); } _ => state.toggle_expand(), } @@ -1304,7 +1307,7 @@ fn load_project_samples(ctx: &mut InputContext) { let mut total_count = 0; for path in &paths { if path.is_dir() { - let index = doux::loader::scan_samples_dir(path); + let index = doux::sampling::scan_samples_dir(path); let count = index.len(); total_count += count; let _ = ctx.audio_tx.load().send(AudioCommand::LoadSamples(index)); diff --git a/src/main.rs b/src/main.rs index c154e78..b18499f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -108,7 +108,7 @@ fn main() -> io::Result<()> { let mut initial_samples = Vec::new(); for path in &app.audio.config.sample_paths { - let index = doux::loader::scan_samples_dir(path); + let index = doux::sampling::scan_samples_dir(path); app.audio.config.sample_count += index.len(); initial_samples.extend(index); } @@ -184,7 +184,7 @@ fn main() -> io::Result<()> { let mut restart_samples = Vec::new(); for path in &app.audio.config.sample_paths { - let index = doux::loader::scan_samples_dir(path); + let index = doux::sampling::scan_samples_dir(path); restart_samples.extend(index); } app.audio.config.sample_count = restart_samples.len(); @@ -233,7 +233,10 @@ fn main() -> io::Result<()> { app.metrics.dropped_events = seq_snapshot.dropped_events; app.ui.event_flash = (app.ui.event_flash - 0.1).max(0.0); - let new_events = app.metrics.event_count.saturating_sub(app.ui.last_event_count); + let new_events = app + .metrics + .event_count + .saturating_sub(app.ui.last_event_count); if new_events > 0 { app.ui.event_flash = (new_events as f32 * 0.4).min(1.0); }