WIP: consolidate sampling

This commit is contained in:
2026-01-30 00:04:25 +01:00
parent a72772c8cc
commit 2851785e0d
5 changed files with 158 additions and 59 deletions

View File

@@ -18,7 +18,7 @@ path = "src/main.rs"
cagire-forth = { path = "crates/forth" } cagire-forth = { path = "crates/forth" }
cagire-project = { path = "crates/project" } cagire-project = { path = "crates/project" }
cagire-ratatui = { path = "crates/ratatui" } 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" rusty_link = "0.4"
ratatui = "0.29" ratatui = "0.29"
crossterm = "0.28" crossterm = "0.28"

View File

@@ -224,7 +224,7 @@ pub fn build_stream(
scope_buffer: Arc<ScopeBuffer>, scope_buffer: Arc<ScopeBuffer>,
spectrum_buffer: Arc<SpectrumBuffer>, spectrum_buffer: Arc<SpectrumBuffer>,
metrics: Arc<EngineMetrics>, metrics: Arc<EngineMetrics>,
initial_samples: Vec<doux::sample::SampleEntry>, initial_samples: Vec<doux::sampling::SampleEntry>,
audio_sample_pos: Arc<AtomicU64>, audio_sample_pos: Arc<AtomicU64>,
) -> Result<(Stream, f32, AnalysisHandle), String> { ) -> Result<(Stream, f32, AnalysisHandle), String> {
let host = cpal::default_host(); let host = cpal::default_host();

View File

@@ -40,10 +40,13 @@ impl PatternChange {
} }
pub enum AudioCommand { pub enum AudioCommand {
Evaluate { cmd: String, time: Option<f64> }, Evaluate {
cmd: String,
time: Option<f64>,
},
Hush, Hush,
Panic, Panic,
LoadSamples(Vec<doux::sample::SampleEntry>), LoadSamples(Vec<doux::sampling::SampleEntry>),
#[allow(dead_code)] #[allow(dead_code)]
ResetEngine, ResetEngine,
} }
@@ -444,11 +447,7 @@ pub(crate) struct SequencerState {
} }
impl SequencerState { impl SequencerState {
pub fn new( pub fn new(variables: Variables, dict: Dictionary, rng: Rng) -> Self {
variables: Variables,
dict: Dictionary,
rng: Rng,
) -> Self {
let script_engine = ScriptEngine::new(Arc::clone(&variables), dict, rng); let script_engine = ScriptEngine::new(Arc::clone(&variables), dict, rng);
Self { Self {
audio_state: AudioState::new(), audio_state: AudioState::new(),
@@ -529,10 +528,14 @@ impl SequencerState {
let prev_beat = self.audio_state.prev_beat; let prev_beat = self.audio_state.prev_beat;
let activated = self.activate_pending(beat, prev_beat, input.quantum); 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); 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( let steps = self.execute_steps(
beat, beat,
@@ -582,7 +585,9 @@ impl SequencerState {
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) = 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(); let speed_mult = pat.speed.multiplier();
((beat * 4.0 * speed_mult) as usize) % pat.length ((beat * 4.0 * speed_mult) as usize) % pat.length
} else { } else {
@@ -654,7 +659,8 @@ impl SequencerState {
continue; continue;
}; };
let speed_mult = self.speed_overrides let speed_mult = self
.speed_overrides
.get(&(active.bank, active.pattern)) .get(&(active.bank, active.pattern))
.copied() .copied()
.unwrap_or_else(|| pattern.speed.multiplier()); .unwrap_or_else(|| pattern.speed.multiplier());
@@ -777,14 +783,24 @@ impl SequencerState {
fn apply_chain_transitions(&mut self, transitions: Vec<(PatternId, PatternId)>) { fn apply_chain_transitions(&mut self, transitions: Vec<(PatternId, PatternId)>) {
for (source, target) in transitions { 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 { self.audio_state.pending_stops.push(PendingPattern {
id: source, id: source,
quantization: LaunchQuantization::Bar, quantization: LaunchQuantization::Bar,
sync_mode: SyncMode::Reset, 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 let (quant, sync) = self
.pattern_cache .pattern_cache
.get(target.bank, target.pattern) .get(target.bank, target.pattern)
@@ -858,7 +874,11 @@ fn sequencer_loop(
let sr = sample_rate.load(Ordering::Relaxed) as f64; let sr = sample_rate.load(Ordering::Relaxed) as f64;
let audio_samples = audio_sample_pos.load(Ordering::Relaxed); 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 lookahead_secs = lookahead_ms.load(Ordering::Relaxed) as f64 / 1000.0;
let input = TickInput { let input = TickInput {
@@ -877,7 +897,10 @@ fn sequencer_loop(
let output = seq_state.tick(input); let output = seq_state.tick(input);
for tsc in output.audio_commands { 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) { match audio_tx.load().try_send(cmd) {
Ok(()) => {} Ok(()) => {}
Err(TrySendError::Full(_) | TrySendError::Disconnected(_)) => { Err(TrySendError::Full(_) | TrySendError::Disconnected(_)) => {
@@ -1087,7 +1110,11 @@ mod tests {
)); ));
assert!(output.shared_state.active_patterns.is_empty()); 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] #[test]
@@ -1128,22 +1155,40 @@ mod tests {
#[test] #[test]
fn test_quantization_boundaries() { fn test_quantization_boundaries() {
assert!(check_quantization_boundary( assert!(check_quantization_boundary(
LaunchQuantization::Immediate, 1.5, 1.0, 4.0 LaunchQuantization::Immediate,
1.5,
1.0,
4.0
)); ));
assert!(check_quantization_boundary( assert!(check_quantization_boundary(
LaunchQuantization::Beat, 2.0, 1.9, 4.0 LaunchQuantization::Beat,
2.0,
1.9,
4.0
)); ));
assert!(!check_quantization_boundary( assert!(!check_quantization_boundary(
LaunchQuantization::Beat, 1.5, 1.2, 4.0 LaunchQuantization::Beat,
1.5,
1.2,
4.0
)); ));
assert!(check_quantization_boundary( assert!(check_quantization_boundary(
LaunchQuantization::Bar, 4.0, 3.9, 4.0 LaunchQuantization::Bar,
4.0,
3.9,
4.0
)); ));
assert!(!check_quantization_boundary( assert!(!check_quantization_boundary(
LaunchQuantization::Bar, 3.5, 3.2, 4.0 LaunchQuantization::Bar,
3.5,
3.2,
4.0
)); ));
assert!(!check_quantization_boundary( 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( state.tick(tick_with(
vec![SeqCommand::PatternUpdate { vec![SeqCommand::PatternUpdate {
bank: 0, pattern: 0, data: simple_pattern(2), bank: 0,
pattern: 0,
data: simple_pattern(2),
}], }],
0.0, 0.0,
)); ));
state.tick(tick_with( state.tick(tick_with(
vec![SeqCommand::PatternStart { vec![SeqCommand::PatternStart {
bank: 0, pattern: 0, bank: 0,
pattern: 0,
quantization: LaunchQuantization::Immediate, quantization: LaunchQuantization::Immediate,
sync_mode: SyncMode::Reset, sync_mode: SyncMode::Reset,
}], }],
@@ -1190,13 +1238,18 @@ mod tests {
pat.speed = crate::model::PatternSpeed::DOUBLE; pat.speed = crate::model::PatternSpeed::DOUBLE;
state.tick(tick_with( state.tick(tick_with(
vec![SeqCommand::PatternUpdate { bank: 0, pattern: 0, data: pat }], vec![SeqCommand::PatternUpdate {
bank: 0,
pattern: 0,
data: pat,
}],
0.0, 0.0,
)); ));
state.tick(tick_with( state.tick(tick_with(
vec![SeqCommand::PatternStart { vec![SeqCommand::PatternStart {
bank: 0, pattern: 0, bank: 0,
pattern: 0,
quantization: LaunchQuantization::Immediate, quantization: LaunchQuantization::Immediate,
sync_mode: SyncMode::Reset, sync_mode: SyncMode::Reset,
}], }],
@@ -1214,7 +1267,9 @@ mod tests {
state.tick(tick_with( state.tick(tick_with(
vec![SeqCommand::PatternUpdate { vec![SeqCommand::PatternUpdate {
bank: 0, pattern: 0, data: simple_pattern(4), bank: 0,
pattern: 0,
data: simple_pattern(4),
}], }],
0.0, 0.0,
)); ));
@@ -1223,12 +1278,14 @@ mod tests {
state.tick(tick_with( state.tick(tick_with(
vec![ vec![
SeqCommand::PatternStart { SeqCommand::PatternStart {
bank: 0, pattern: 0, bank: 0,
pattern: 0,
quantization: LaunchQuantization::Immediate, quantization: LaunchQuantization::Immediate,
sync_mode: SyncMode::Reset, sync_mode: SyncMode::Reset,
}, },
SeqCommand::PatternStop { SeqCommand::PatternStop {
bank: 0, pattern: 0, bank: 0,
pattern: 0,
quantization: LaunchQuantization::Immediate, quantization: LaunchQuantization::Immediate,
}, },
], ],
@@ -1246,13 +1303,18 @@ mod tests {
state.tick(tick_with( state.tick(tick_with(
vec![ vec![
SeqCommand::PatternUpdate { SeqCommand::PatternUpdate {
bank: 0, pattern: 0, data: simple_pattern(1), bank: 0,
pattern: 0,
data: simple_pattern(1),
}, },
SeqCommand::PatternUpdate { SeqCommand::PatternUpdate {
bank: 0, pattern: 1, data: simple_pattern(4), bank: 0,
pattern: 1,
data: simple_pattern(4),
}, },
SeqCommand::PatternStart { SeqCommand::PatternStart {
bank: 0, pattern: 0, bank: 0,
pattern: 0,
quantization: LaunchQuantization::Immediate, quantization: LaunchQuantization::Immediate,
sync_mode: SyncMode::Reset, sync_mode: SyncMode::Reset,
}, },
@@ -1279,14 +1341,19 @@ mod tests {
// Chain guard should block transition to pattern 1. // Chain guard should block transition to pattern 1.
state.tick(tick_with( state.tick(tick_with(
vec![SeqCommand::PatternStop { vec![SeqCommand::PatternStop {
bank: 0, pattern: 0, bank: 0,
pattern: 0,
quantization: LaunchQuantization::Immediate, quantization: LaunchQuantization::Immediate,
}], }],
1.0, 1.0,
)); ));
assert!(!state.audio_state.active_patterns.contains_key(&pid(0, 1))); 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] #[test]
@@ -1296,18 +1363,24 @@ mod tests {
state.tick(tick_with( state.tick(tick_with(
vec![ vec![
SeqCommand::PatternUpdate { SeqCommand::PatternUpdate {
bank: 0, pattern: 0, data: simple_pattern(4), bank: 0,
pattern: 0,
data: simple_pattern(4),
}, },
SeqCommand::PatternUpdate { SeqCommand::PatternUpdate {
bank: 0, pattern: 1, data: simple_pattern(4), bank: 0,
pattern: 1,
data: simple_pattern(4),
}, },
SeqCommand::PatternStart { SeqCommand::PatternStart {
bank: 0, pattern: 0, bank: 0,
pattern: 0,
quantization: LaunchQuantization::Bar, quantization: LaunchQuantization::Bar,
sync_mode: SyncMode::Reset, sync_mode: SyncMode::Reset,
}, },
SeqCommand::PatternStart { SeqCommand::PatternStart {
bank: 0, pattern: 1, bank: 0,
pattern: 1,
quantization: LaunchQuantization::Beat, quantization: LaunchQuantization::Beat,
sync_mode: SyncMode::Reset, sync_mode: SyncMode::Reset,
}, },
@@ -1334,13 +1407,16 @@ mod tests {
state.tick(tick_with( state.tick(tick_with(
vec![SeqCommand::PatternUpdate { vec![SeqCommand::PatternUpdate {
bank: 0, pattern: 0, data: simple_pattern(4), bank: 0,
pattern: 0,
data: simple_pattern(4),
}], }],
0.0, 0.0,
)); ));
state.tick(tick_with( state.tick(tick_with(
vec![SeqCommand::PatternStart { vec![SeqCommand::PatternStart {
bank: 0, pattern: 0, bank: 0,
pattern: 0,
quantization: LaunchQuantization::Immediate, quantization: LaunchQuantization::Immediate,
sync_mode: SyncMode::Reset, 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 // 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( state.tick(tick_with(
vec![SeqCommand::PatternUpdate { vec![SeqCommand::PatternUpdate {
bank: 0, pattern: 0, data: simple_pattern(2), bank: 0,
pattern: 0,
data: simple_pattern(2),
}], }],
1.25, 1.25,
)); ));
@@ -1376,7 +1454,9 @@ mod tests {
state.tick(tick_with( state.tick(tick_with(
vec![SeqCommand::PatternUpdate { vec![SeqCommand::PatternUpdate {
bank: 0, pattern: 0, data: simple_pattern(4), bank: 0,
pattern: 0,
data: simple_pattern(4),
}], }],
0.0, 0.0,
)); ));
@@ -1384,7 +1464,8 @@ mod tests {
// Start while paused: pending_starts gets cleared // Start while paused: pending_starts gets cleared
state.tick(TickInput { state.tick(TickInput {
commands: vec![SeqCommand::PatternStart { commands: vec![SeqCommand::PatternStart {
bank: 0, pattern: 0, bank: 0,
pattern: 0,
quantization: LaunchQuantization::Immediate, quantization: LaunchQuantization::Immediate,
sync_mode: SyncMode::Reset, sync_mode: SyncMode::Reset,
}], }],
@@ -1404,13 +1485,16 @@ mod tests {
state.tick(tick_with( state.tick(tick_with(
vec![SeqCommand::PatternUpdate { vec![SeqCommand::PatternUpdate {
bank: 0, pattern: 0, data: simple_pattern(4), bank: 0,
pattern: 0,
data: simple_pattern(4),
}], }],
0.0, 0.0,
)); ));
state.tick(tick_with( state.tick(tick_with(
vec![SeqCommand::PatternStart { vec![SeqCommand::PatternStart {
bank: 0, pattern: 0, bank: 0,
pattern: 0,
quantization: LaunchQuantization::Immediate, quantization: LaunchQuantization::Immediate,
sync_mode: SyncMode::Reset, sync_mode: SyncMode::Reset,
}], }],
@@ -1434,15 +1518,19 @@ mod tests {
state.tick(tick_with( state.tick(tick_with(
vec![ vec![
SeqCommand::PatternUpdate { SeqCommand::PatternUpdate {
bank: 0, pattern: 0, data: simple_pattern(4), bank: 0,
pattern: 0,
data: simple_pattern(4),
}, },
SeqCommand::PatternStart { SeqCommand::PatternStart {
bank: 0, pattern: 0, bank: 0,
pattern: 0,
quantization: LaunchQuantization::Bar, quantization: LaunchQuantization::Bar,
sync_mode: SyncMode::Reset, sync_mode: SyncMode::Reset,
}, },
SeqCommand::PatternStart { SeqCommand::PatternStart {
bank: 0, pattern: 0, bank: 0,
pattern: 0,
quantization: LaunchQuantization::Bar, quantization: LaunchQuantization::Bar,
sync_mode: SyncMode::Reset, sync_mode: SyncMode::Reset,
}, },
@@ -1450,7 +1538,9 @@ mod tests {
0.0, 0.0,
)); ));
let pending_count = state.audio_state.pending_starts let pending_count = state
.audio_state
.pending_starts
.iter() .iter()
.filter(|p| p.id == pid(0, 0)) .filter(|p| p.id == pid(0, 0))
.count(); .count();
@@ -1464,13 +1554,16 @@ mod tests {
// Pattern of length 16 — won't complete iteration for many ticks // Pattern of length 16 — won't complete iteration for many ticks
state.tick(tick_with( state.tick(tick_with(
vec![SeqCommand::PatternUpdate { vec![SeqCommand::PatternUpdate {
bank: 0, pattern: 0, data: simple_pattern(16), bank: 0,
pattern: 0,
data: simple_pattern(16),
}], }],
0.0, 0.0,
)); ));
state.tick(tick_with( state.tick(tick_with(
vec![SeqCommand::PatternStart { vec![SeqCommand::PatternStart {
bank: 0, pattern: 0, bank: 0,
pattern: 0,
quantization: LaunchQuantization::Immediate, quantization: LaunchQuantization::Immediate,
sync_mode: SyncMode::Reset, sync_mode: SyncMode::Reset,
}], }],

View File

@@ -414,7 +414,7 @@ fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
} }
}; };
if let Some(path) = sample_path { 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 count = index.len();
let _ = ctx.audio_tx.load().send(AudioCommand::LoadSamples(index)); let _ = ctx.audio_tx.load().send(AudioCommand::LoadSamples(index));
ctx.app.audio.config.sample_count += count; 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 folder = &entry.folder;
let idx = entry.index; let idx = entry.index;
let cmd = format!("/sound/{folder}/n/{idx}/gain/0.5/dur/1"); 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(), _ => state.toggle_expand(),
} }
@@ -1304,7 +1307,7 @@ fn load_project_samples(ctx: &mut InputContext) {
let mut total_count = 0; let mut total_count = 0;
for path in &paths { for path in &paths {
if path.is_dir() { if path.is_dir() {
let index = doux::loader::scan_samples_dir(path); let index = doux::sampling::scan_samples_dir(path);
let count = index.len(); let count = index.len();
total_count += count; total_count += count;
let _ = ctx.audio_tx.load().send(AudioCommand::LoadSamples(index)); let _ = ctx.audio_tx.load().send(AudioCommand::LoadSamples(index));

View File

@@ -108,7 +108,7 @@ fn main() -> io::Result<()> {
let mut initial_samples = Vec::new(); let mut initial_samples = Vec::new();
for path in &app.audio.config.sample_paths { 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(); app.audio.config.sample_count += index.len();
initial_samples.extend(index); initial_samples.extend(index);
} }
@@ -184,7 +184,7 @@ fn main() -> io::Result<()> {
let mut restart_samples = Vec::new(); let mut restart_samples = Vec::new();
for path in &app.audio.config.sample_paths { 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); restart_samples.extend(index);
} }
app.audio.config.sample_count = restart_samples.len(); 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.metrics.dropped_events = seq_snapshot.dropped_events;
app.ui.event_flash = (app.ui.event_flash - 0.1).max(0.0); 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 { if new_events > 0 {
app.ui.event_flash = (new_events as f32 * 0.4).min(1.0); app.ui.event_flash = (new_events as f32 * 0.4).min(1.0);
} }