This commit is contained in:
@@ -264,6 +264,10 @@ use cpal::Stream;
|
||||
use crossbeam_channel::{Receiver, Sender};
|
||||
#[cfg(feature = "cli")]
|
||||
use doux::{Engine, EngineMetrics};
|
||||
#[cfg(feature = "cli")]
|
||||
use std::collections::VecDeque;
|
||||
#[cfg(feature = "cli")]
|
||||
use std::sync::Mutex;
|
||||
|
||||
#[cfg(feature = "cli")]
|
||||
use super::AudioCommand;
|
||||
@@ -271,6 +275,7 @@ use super::AudioCommand;
|
||||
#[cfg(feature = "cli")]
|
||||
pub struct AudioStreamConfig {
|
||||
pub output_device: Option<String>,
|
||||
pub input_device: Option<String>,
|
||||
pub channels: u16,
|
||||
pub buffer_size: u32,
|
||||
pub max_voices: usize,
|
||||
@@ -283,6 +288,15 @@ pub struct AudioStreamInfo {
|
||||
pub channels: u16,
|
||||
}
|
||||
|
||||
#[cfg(feature = "cli")]
|
||||
type BuildStreamResult = (
|
||||
Stream,
|
||||
Option<Stream>,
|
||||
AudioStreamInfo,
|
||||
AnalysisHandle,
|
||||
Arc<doux::SampleRegistry>,
|
||||
);
|
||||
|
||||
#[cfg(feature = "cli")]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn build_stream(
|
||||
@@ -295,15 +309,7 @@ pub fn build_stream(
|
||||
audio_sample_pos: Arc<AtomicU64>,
|
||||
error_tx: Sender<String>,
|
||||
sample_paths: &[std::path::PathBuf],
|
||||
) -> Result<
|
||||
(
|
||||
Stream,
|
||||
AudioStreamInfo,
|
||||
AnalysisHandle,
|
||||
Arc<doux::SampleRegistry>,
|
||||
),
|
||||
String,
|
||||
> {
|
||||
) -> Result<BuildStreamResult, String> {
|
||||
let device = match &config.output_device {
|
||||
Some(name) => doux::audio::find_output_device(name)
|
||||
.ok_or_else(|| format!("Device not found: {name}"))?,
|
||||
@@ -352,10 +358,72 @@ pub fn build_stream(
|
||||
|
||||
let registry = Arc::clone(&engine.sample_registry);
|
||||
|
||||
const INPUT_BUFFER_SIZE: usize = 8192;
|
||||
let input_buffer: Arc<Mutex<VecDeque<f32>>> =
|
||||
Arc::new(Mutex::new(VecDeque::with_capacity(INPUT_BUFFER_SIZE)));
|
||||
|
||||
let input_device = config
|
||||
.input_device
|
||||
.as_ref()
|
||||
.and_then(|name| {
|
||||
let dev = doux::audio::find_input_device(name);
|
||||
if dev.is_none() {
|
||||
eprintln!("input device not found: {name}");
|
||||
}
|
||||
dev
|
||||
});
|
||||
|
||||
let input_channels: usize = input_device
|
||||
.as_ref()
|
||||
.and_then(|dev| dev.default_input_config().ok())
|
||||
.map_or(0, |cfg| cfg.channels() as usize);
|
||||
|
||||
let input_stream = input_device.and_then(|dev| {
|
||||
let input_cfg = match dev.default_input_config() {
|
||||
Ok(cfg) => cfg,
|
||||
Err(e) => {
|
||||
eprintln!("input config error: {e}");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
if input_cfg.sample_rate() != default_config.sample_rate() {
|
||||
eprintln!(
|
||||
"warning: input sample rate ({}Hz) differs from output ({}Hz)",
|
||||
input_cfg.sample_rate(),
|
||||
default_config.sample_rate()
|
||||
);
|
||||
}
|
||||
eprintln!(
|
||||
"opening input: {}ch @ {}Hz",
|
||||
input_cfg.channels(),
|
||||
input_cfg.sample_rate()
|
||||
);
|
||||
let buf = Arc::clone(&input_buffer);
|
||||
let stream = dev
|
||||
.build_input_stream(
|
||||
&input_cfg.into(),
|
||||
move |data: &[f32], _| {
|
||||
let mut b = buf.lock().unwrap();
|
||||
b.extend(data.iter().copied());
|
||||
let excess = b.len().saturating_sub(INPUT_BUFFER_SIZE);
|
||||
if excess > 0 {
|
||||
drop(b.drain(..excess));
|
||||
}
|
||||
},
|
||||
|err| eprintln!("input stream error: {err}"),
|
||||
None,
|
||||
)
|
||||
.ok()?;
|
||||
stream.play().ok()?;
|
||||
Some(stream)
|
||||
});
|
||||
|
||||
let (mut fft_producer, analysis_handle) = spawn_analysis_thread(sample_rate, spectrum_buffer);
|
||||
|
||||
let mut cmd_buffer = String::with_capacity(256);
|
||||
let mut rt_set = false;
|
||||
let mut live_scratch = vec![0.0f32; 4096];
|
||||
let input_buf_clone = Arc::clone(&input_buffer);
|
||||
|
||||
let stream = device
|
||||
.build_output_stream(
|
||||
@@ -402,8 +470,49 @@ pub fn build_stream(
|
||||
}
|
||||
}
|
||||
|
||||
// doux expects stereo interleaved live_input (CHANNELS=2)
|
||||
let stereo_len = buffer_samples * 2;
|
||||
if live_scratch.len() < stereo_len {
|
||||
live_scratch.resize(stereo_len, 0.0);
|
||||
}
|
||||
let mut buf = input_buf_clone.lock().unwrap();
|
||||
match input_channels {
|
||||
0 => {
|
||||
live_scratch[..stereo_len].fill(0.0);
|
||||
}
|
||||
1 => {
|
||||
for i in 0..buffer_samples {
|
||||
let s = buf.pop_front().unwrap_or(0.0);
|
||||
live_scratch[i * 2] = s;
|
||||
live_scratch[i * 2 + 1] = s;
|
||||
}
|
||||
}
|
||||
2 => {
|
||||
for sample in &mut live_scratch[..stereo_len] {
|
||||
*sample = buf.pop_front().unwrap_or(0.0);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
for i in 0..buffer_samples {
|
||||
let l = buf.pop_front().unwrap_or(0.0);
|
||||
let r = buf.pop_front().unwrap_or(0.0);
|
||||
for _ in 2..input_channels {
|
||||
buf.pop_front();
|
||||
}
|
||||
live_scratch[i * 2] = l;
|
||||
live_scratch[i * 2 + 1] = r;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Discard excess if input produced more than we consumed
|
||||
let excess = buf.len().saturating_sub(INPUT_BUFFER_SIZE / 2);
|
||||
if excess > 0 {
|
||||
drop(buf.drain(..excess));
|
||||
}
|
||||
drop(buf);
|
||||
|
||||
engine.metrics.load.set_buffer_time(buffer_time_ns);
|
||||
engine.process_block(data, &[], &[]);
|
||||
engine.process_block(data, &[], &live_scratch[..stereo_len]);
|
||||
scope_buffer.write(data);
|
||||
|
||||
// Feed mono mix to analysis thread via ring buffer (non-blocking)
|
||||
@@ -425,5 +534,5 @@ pub fn build_stream(
|
||||
host_name,
|
||||
channels: effective_channels,
|
||||
};
|
||||
Ok((stream, info, analysis_handle, registry))
|
||||
Ok((stream, input_stream, info, analysis_handle, registry))
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ pub fn dispatcher_loop(
|
||||
let current_us = link.clock_micros() as SyncTime;
|
||||
while let Some(cmd) = queue.peek() {
|
||||
if cmd.target_time_us <= current_us + SPIN_THRESHOLD_US {
|
||||
let cmd = queue.pop().unwrap();
|
||||
let cmd = queue.pop().expect("pop after peek");
|
||||
wait_until_dispatch(cmd.target_time_us, &link, has_rt);
|
||||
dispatch_midi(cmd.command, &midi_tx);
|
||||
} else {
|
||||
@@ -149,8 +149,8 @@ mod tests {
|
||||
target_time_us: 200,
|
||||
});
|
||||
|
||||
assert_eq!(heap.pop().unwrap().target_time_us, 100);
|
||||
assert_eq!(heap.pop().unwrap().target_time_us, 200);
|
||||
assert_eq!(heap.pop().unwrap().target_time_us, 300);
|
||||
assert_eq!(heap.pop().expect("heap non-empty").target_time_us, 100);
|
||||
assert_eq!(heap.pop().expect("heap non-empty").target_time_us, 200);
|
||||
assert_eq!(heap.pop().expect("heap non-empty").target_time_us, 300);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,8 +141,6 @@ pub struct PatternSnapshot {
|
||||
pub speed: crate::model::PatternSpeed,
|
||||
pub length: usize,
|
||||
pub steps: Vec<StepSnapshot>,
|
||||
#[allow(dead_code)]
|
||||
pub quantization: LaunchQuantization,
|
||||
pub sync_mode: SyncMode,
|
||||
pub follow_up: FollowUp,
|
||||
}
|
||||
@@ -544,7 +542,7 @@ struct StepResult {
|
||||
fn format_speed_key(buf: &mut String, bank: usize, pattern: usize) -> &str {
|
||||
use std::fmt::Write;
|
||||
buf.clear();
|
||||
write!(buf, "__speed_{bank}_{pattern}__").unwrap();
|
||||
write!(buf, "__speed_{bank}_{pattern}__").expect("write to String");
|
||||
buf
|
||||
}
|
||||
|
||||
@@ -553,7 +551,7 @@ pub struct SequencerState {
|
||||
pattern_cache: PatternCache,
|
||||
pending_updates: HashMap<(usize, usize), PatternSnapshot>,
|
||||
runs_counter: RunsCounter,
|
||||
step_traces: Arc<StepTracesMap>,
|
||||
step_traces: StepTracesMap,
|
||||
event_count: usize,
|
||||
script_engine: ScriptEngine,
|
||||
variables: Variables,
|
||||
@@ -590,7 +588,7 @@ impl SequencerState {
|
||||
pattern_cache: PatternCache::new(),
|
||||
pending_updates: HashMap::new(),
|
||||
runs_counter: RunsCounter::new(),
|
||||
step_traces: Arc::new(HashMap::new()),
|
||||
step_traces: HashMap::new(),
|
||||
event_count: 0,
|
||||
script_engine,
|
||||
variables,
|
||||
@@ -709,7 +707,7 @@ impl SequencerState {
|
||||
self.audio_state.active_patterns.clear();
|
||||
self.audio_state.pending_starts.clear();
|
||||
self.audio_state.pending_stops.clear();
|
||||
Arc::make_mut(&mut self.step_traces).clear();
|
||||
self.step_traces.clear();
|
||||
self.runs_counter.counts.clear();
|
||||
self.audio_state.flush_midi_notes = true;
|
||||
}
|
||||
@@ -727,7 +725,7 @@ impl SequencerState {
|
||||
self.speed_overrides.clear();
|
||||
self.script_engine.clear_global_params();
|
||||
self.runs_counter.counts.clear();
|
||||
Arc::make_mut(&mut self.step_traces).clear();
|
||||
self.step_traces.clear();
|
||||
self.audio_state.flush_midi_notes = true;
|
||||
}
|
||||
SeqCommand::ResetScriptState => {
|
||||
@@ -807,7 +805,7 @@ impl SequencerState {
|
||||
fn tick_paused(&mut self) -> TickOutput {
|
||||
for pending in self.audio_state.pending_stops.drain(..) {
|
||||
self.audio_state.active_patterns.remove(&pending.id);
|
||||
Arc::make_mut(&mut self.step_traces).retain(|&(bank, pattern, _), _| {
|
||||
self.step_traces.retain(|&(bank, pattern, _), _| {
|
||||
bank != pending.id.bank || pattern != pending.id.pattern
|
||||
});
|
||||
let key = (pending.id.bank, pending.id.pattern);
|
||||
@@ -889,7 +887,7 @@ impl SequencerState {
|
||||
for pending in &self.audio_state.pending_stops {
|
||||
if check_quantization_boundary(pending.quantization, beat, prev_beat, quantum) {
|
||||
self.audio_state.active_patterns.remove(&pending.id);
|
||||
Arc::make_mut(&mut self.step_traces).retain(|&(bank, pattern, _), _| {
|
||||
self.step_traces.retain(|&(bank, pattern, _), _| {
|
||||
bank != pending.id.bank || pattern != pending.id.pattern
|
||||
});
|
||||
// Flush pending update so cache stays current for future launches
|
||||
@@ -1009,7 +1007,7 @@ impl SequencerState {
|
||||
.script_engine
|
||||
.evaluate_with_trace(script, &ctx, &mut trace)
|
||||
{
|
||||
Arc::make_mut(&mut self.step_traces).insert(
|
||||
self.step_traces.insert(
|
||||
(active.bank, active.pattern, source_idx),
|
||||
std::mem::take(&mut trace),
|
||||
);
|
||||
@@ -1198,7 +1196,7 @@ impl SequencerState {
|
||||
last_step_beat: a.last_step_beat,
|
||||
})
|
||||
.collect(),
|
||||
step_traces: Arc::clone(&self.step_traces),
|
||||
step_traces: Arc::new(self.step_traces.clone()),
|
||||
event_count: self.event_count,
|
||||
tempo: self.last_tempo,
|
||||
beat: self.last_beat,
|
||||
@@ -1490,7 +1488,6 @@ mod tests {
|
||||
source: None,
|
||||
})
|
||||
.collect(),
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
sync_mode: SyncMode::Reset,
|
||||
follow_up: FollowUp::default(),
|
||||
}
|
||||
@@ -1709,19 +1706,19 @@ mod tests {
|
||||
|
||||
// beat_int at 0.5 is 2, prev_beat_int at 0.0 is 0
|
||||
// steps_to_fire = 2-0 = 2, firing steps 0 and 1, wrapping to 0
|
||||
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap();
|
||||
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).expect("pattern in active set");
|
||||
assert_eq!(ap.step_index, 0);
|
||||
assert_eq!(ap.iter, 1);
|
||||
|
||||
// beat_int at 0.75 is 3, prev is 2, fires 1 step (step 0), advances to 1
|
||||
state.tick(tick_at(0.75, true));
|
||||
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap();
|
||||
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).expect("pattern in active set");
|
||||
assert_eq!(ap.step_index, 1);
|
||||
assert_eq!(ap.iter, 1);
|
||||
|
||||
// beat_int at 1.0 is 4, prev is 3, fires 1 step (step 1), wraps to 0
|
||||
state.tick(tick_at(1.0, true));
|
||||
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap();
|
||||
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).expect("pattern in active set");
|
||||
assert_eq!(ap.step_index, 0);
|
||||
assert_eq!(ap.iter, 2);
|
||||
}
|
||||
@@ -1754,12 +1751,12 @@ mod tests {
|
||||
|
||||
// At 2x speed: beat_int at 0.5 is (0.5*4*2)=4, prev at 0.0 is 0
|
||||
// Fires 4 steps (0,1,2,3), advancing to step 4
|
||||
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap();
|
||||
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).expect("pattern in active set");
|
||||
assert_eq!(ap.step_index, 4);
|
||||
|
||||
// beat_int at 0.625 is (0.625*4*2)=5, prev is 4, fires 1 step
|
||||
state.tick(tick_at(0.625, true));
|
||||
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap();
|
||||
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).expect("pattern in active set");
|
||||
assert_eq!(ap.step_index, 5);
|
||||
}
|
||||
|
||||
@@ -1866,17 +1863,17 @@ mod tests {
|
||||
));
|
||||
|
||||
// beat_int at 0.5 is 2, prev at 0.0 is 0, fires 2 steps (0,1), step_index=2
|
||||
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap();
|
||||
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).expect("pattern in active set");
|
||||
assert_eq!(ap.step_index, 2);
|
||||
|
||||
// beat_int at 0.75 is 3, prev is 2, fires 1 step (2), step_index=3
|
||||
state.tick(tick_at(0.75, true));
|
||||
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap();
|
||||
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).expect("pattern in active set");
|
||||
assert_eq!(ap.step_index, 3);
|
||||
|
||||
// beat_int at 1.0 is 4, prev is 3, fires 1 step (3), wraps to step_index=0
|
||||
state.tick(tick_at(1.0, true));
|
||||
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap();
|
||||
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).expect("pattern in active set");
|
||||
assert_eq!(ap.step_index, 0);
|
||||
|
||||
// Update pattern to length 2 while running — deferred until iteration boundary
|
||||
@@ -1890,7 +1887,7 @@ mod tests {
|
||||
}],
|
||||
1.25,
|
||||
));
|
||||
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap();
|
||||
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).expect("pattern in active set");
|
||||
assert_eq!(ap.step_index, 1); // still length 4
|
||||
|
||||
// Advance through remaining steps of original length-4 pattern
|
||||
@@ -1901,12 +1898,12 @@ mod tests {
|
||||
// Now length=2 is applied. Next tick uses new length.
|
||||
// beat=2.25: step 0 fires, advances to 1
|
||||
state.tick(tick_at(2.25, true));
|
||||
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap();
|
||||
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).expect("pattern in active set");
|
||||
assert_eq!(ap.step_index, 1);
|
||||
|
||||
// beat=2.5: step 1 fires, wraps to 0 (length 2)
|
||||
state.tick(tick_at(2.5, true));
|
||||
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap();
|
||||
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).expect("pattern in active set");
|
||||
assert_eq!(ap.step_index, 0);
|
||||
}
|
||||
|
||||
@@ -2056,7 +2053,6 @@ mod tests {
|
||||
source: None,
|
||||
})
|
||||
.collect(),
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
sync_mode: SyncMode::Reset,
|
||||
follow_up: FollowUp::default(),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user