WIP: consolidate sampling
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -224,7 +224,7 @@ pub fn build_stream(
|
||||
scope_buffer: Arc<ScopeBuffer>,
|
||||
spectrum_buffer: Arc<SpectrumBuffer>,
|
||||
metrics: Arc<EngineMetrics>,
|
||||
initial_samples: Vec<doux::sample::SampleEntry>,
|
||||
initial_samples: Vec<doux::sampling::SampleEntry>,
|
||||
audio_sample_pos: Arc<AtomicU64>,
|
||||
) -> Result<(Stream, f32, AnalysisHandle), String> {
|
||||
let host = cpal::default_host();
|
||||
|
||||
@@ -40,10 +40,13 @@ impl PatternChange {
|
||||
}
|
||||
|
||||
pub enum AudioCommand {
|
||||
Evaluate { cmd: String, time: Option<f64> },
|
||||
Evaluate {
|
||||
cmd: String,
|
||||
time: Option<f64>,
|
||||
},
|
||||
Hush,
|
||||
Panic,
|
||||
LoadSamples(Vec<doux::sample::SampleEntry>),
|
||||
LoadSamples(Vec<doux::sampling::SampleEntry>),
|
||||
#[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,
|
||||
}],
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user