Fix audio overruns
This commit is contained in:
parent
8ac5f52f28
commit
8e38c0c5a1
|
|
@ -63,6 +63,18 @@ pub struct Engine {
|
||||||
|
|
||||||
// Metronome for click track
|
// Metronome for click track
|
||||||
metronome: Metronome,
|
metronome: Metronome,
|
||||||
|
|
||||||
|
// Pre-allocated buffer for recording input samples (avoids allocation per callback)
|
||||||
|
recording_sample_buffer: Vec<f32>,
|
||||||
|
|
||||||
|
// Callback timing diagnostics (enabled by DAW_AUDIO_DEBUG=1)
|
||||||
|
debug_audio: bool,
|
||||||
|
callback_count: u64,
|
||||||
|
timing_worst_total_us: u64,
|
||||||
|
timing_worst_commands_us: u64,
|
||||||
|
timing_worst_render_us: u64,
|
||||||
|
timing_sum_total_us: u64,
|
||||||
|
timing_overrun_count: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Engine {
|
impl Engine {
|
||||||
|
|
@ -110,6 +122,14 @@ impl Engine {
|
||||||
midi_recording_state: None,
|
midi_recording_state: None,
|
||||||
midi_input_manager: None,
|
midi_input_manager: None,
|
||||||
metronome: Metronome::new(sample_rate),
|
metronome: Metronome::new(sample_rate),
|
||||||
|
recording_sample_buffer: Vec::with_capacity(4096),
|
||||||
|
debug_audio: std::env::var("DAW_AUDIO_DEBUG").map_or(false, |v| v == "1"),
|
||||||
|
callback_count: 0,
|
||||||
|
timing_worst_total_us: 0,
|
||||||
|
timing_worst_commands_us: 0,
|
||||||
|
timing_worst_render_us: 0,
|
||||||
|
timing_sum_total_us: 0,
|
||||||
|
timing_overrun_count: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -209,6 +229,8 @@ impl Engine {
|
||||||
|
|
||||||
/// Process audio callback - called from the audio thread
|
/// Process audio callback - called from the audio thread
|
||||||
pub fn process(&mut self, output: &mut [f32]) {
|
pub fn process(&mut self, output: &mut [f32]) {
|
||||||
|
let t_start = if self.debug_audio { Some(std::time::Instant::now()) } else { None };
|
||||||
|
|
||||||
// Process all pending commands
|
// Process all pending commands
|
||||||
while let Ok(cmd) = self.command_rx.pop() {
|
while let Ok(cmd) = self.command_rx.pop() {
|
||||||
self.handle_command(cmd);
|
self.handle_command(cmd);
|
||||||
|
|
@ -236,12 +258,16 @@ impl Engine {
|
||||||
|
|
||||||
// Forward chunk generation events from background threads
|
// Forward chunk generation events from background threads
|
||||||
while let Ok(event) = self.chunk_generation_rx.try_recv() {
|
while let Ok(event) = self.chunk_generation_rx.try_recv() {
|
||||||
if let AudioEvent::WaveformChunksReady { pool_index, detail_level, ref chunks } = event {
|
if self.debug_audio {
|
||||||
println!("📬 [AUDIO THREAD] Received {} chunks for pool {} level {}, forwarding to UI", chunks.len(), pool_index, detail_level);
|
if let AudioEvent::WaveformChunksReady { pool_index, detail_level, ref chunks } = event {
|
||||||
|
eprintln!("[AUDIO THREAD] Received {} chunks for pool {} level {}, forwarding to UI", chunks.len(), pool_index, detail_level);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let _ = self.event_tx.push(event);
|
let _ = self.event_tx.push(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let t_commands = if self.debug_audio { Some(std::time::Instant::now()) } else { None };
|
||||||
|
|
||||||
if self.playing {
|
if self.playing {
|
||||||
// Ensure mix buffer is sized correctly
|
// Ensure mix buffer is sized correctly
|
||||||
if self.mix_buffer.len() != output.len() {
|
if self.mix_buffer.len() != output.len() {
|
||||||
|
|
@ -323,15 +349,24 @@ impl Engine {
|
||||||
// Process recording if active (independent of playback state)
|
// Process recording if active (independent of playback state)
|
||||||
if let Some(recording) = &mut self.recording_state {
|
if let Some(recording) = &mut self.recording_state {
|
||||||
if let Some(input_rx) = &mut self.input_rx {
|
if let Some(input_rx) = &mut self.input_rx {
|
||||||
// Pull samples from input ringbuffer
|
// Phase 1: Discard stale samples by popping without storing
|
||||||
let mut samples = Vec::new();
|
// (fast — no Vec push, no add_samples overhead)
|
||||||
|
while recording.samples_to_skip > 0 {
|
||||||
|
match input_rx.pop() {
|
||||||
|
Ok(_) => recording.samples_to_skip -= 1,
|
||||||
|
Err(_) => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 2: Pull fresh samples for actual recording
|
||||||
|
self.recording_sample_buffer.clear();
|
||||||
while let Ok(sample) = input_rx.pop() {
|
while let Ok(sample) = input_rx.pop() {
|
||||||
samples.push(sample);
|
self.recording_sample_buffer.push(sample);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add samples to recording
|
// Add samples to recording
|
||||||
if !samples.is_empty() {
|
if !self.recording_sample_buffer.is_empty() {
|
||||||
match recording.add_samples(&samples) {
|
match recording.add_samples(&self.recording_sample_buffer) {
|
||||||
Ok(_flushed) => {
|
Ok(_flushed) => {
|
||||||
// Update clip duration every callback for sample-accurate timing
|
// Update clip duration every callback for sample-accurate timing
|
||||||
let duration = recording.duration();
|
let duration = recording.duration();
|
||||||
|
|
@ -348,7 +383,7 @@ impl Engine {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send progress event periodically (every ~0.1 seconds)
|
// Send progress event periodically (every ~0.1 seconds)
|
||||||
self.recording_progress_counter += samples.len();
|
self.recording_progress_counter += self.recording_sample_buffer.len();
|
||||||
if self.recording_progress_counter >= (self.sample_rate as usize / 10) {
|
if self.recording_progress_counter >= (self.sample_rate as usize / 10) {
|
||||||
let _ = self.event_tx.push(AudioEvent::RecordingProgress(clip_id, duration));
|
let _ = self.event_tx.push(AudioEvent::RecordingProgress(clip_id, duration));
|
||||||
self.recording_progress_counter = 0;
|
self.recording_progress_counter = 0;
|
||||||
|
|
@ -366,6 +401,42 @@ impl Engine {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Timing diagnostics (DAW_AUDIO_DEBUG=1)
|
||||||
|
if let (true, Some(t_start), Some(t_commands)) = (self.debug_audio, t_start, t_commands) {
|
||||||
|
let t_end = std::time::Instant::now();
|
||||||
|
let total_us = t_end.duration_since(t_start).as_micros() as u64;
|
||||||
|
let commands_us = t_commands.duration_since(t_start).as_micros() as u64;
|
||||||
|
let render_us = total_us.saturating_sub(commands_us);
|
||||||
|
|
||||||
|
self.callback_count += 1;
|
||||||
|
self.timing_sum_total_us += total_us;
|
||||||
|
if total_us > self.timing_worst_total_us { self.timing_worst_total_us = total_us; }
|
||||||
|
if commands_us > self.timing_worst_commands_us { self.timing_worst_commands_us = commands_us; }
|
||||||
|
if render_us > self.timing_worst_render_us { self.timing_worst_render_us = render_us; }
|
||||||
|
|
||||||
|
let frames = output.len() as u64 / self.channels as u64;
|
||||||
|
let deadline_us = frames * 1_000_000 / self.sample_rate as u64;
|
||||||
|
|
||||||
|
if total_us > deadline_us {
|
||||||
|
self.timing_overrun_count += 1;
|
||||||
|
eprintln!(
|
||||||
|
"[AUDIO TIMING] OVERRUN #{}: total={} us (deadline={} us) | cmds={} us, render={} us | buf={} frames",
|
||||||
|
self.timing_overrun_count, total_us, deadline_us, commands_us, render_us, frames
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.callback_count % 860 == 0 {
|
||||||
|
let avg_us = self.timing_sum_total_us / self.callback_count;
|
||||||
|
eprintln!(
|
||||||
|
"[AUDIO TIMING] avg={} us, worst: total={} us, cmds={} us, render={} us | overruns={}/{} ({:.1}%) | deadline={} us",
|
||||||
|
avg_us, self.timing_worst_total_us, self.timing_worst_commands_us, self.timing_worst_render_us,
|
||||||
|
self.timing_overrun_count, self.callback_count,
|
||||||
|
self.timing_overrun_count as f64 / self.callback_count as f64 * 100.0,
|
||||||
|
deadline_us
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle a command from the UI thread
|
/// Handle a command from the UI thread
|
||||||
|
|
@ -2023,9 +2094,9 @@ impl Engine {
|
||||||
flush_interval_seconds,
|
flush_interval_seconds,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check how many samples are currently in the input buffer and mark them for skipping
|
// Count stale samples so we can skip them incrementally
|
||||||
let samples_in_buffer = if let Some(input_rx) = &self.input_rx {
|
let samples_in_buffer = if let Some(input_rx) = &self.input_rx {
|
||||||
input_rx.slots() // Number of samples currently in the buffer
|
input_rx.slots()
|
||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
};
|
};
|
||||||
|
|
@ -2033,11 +2104,11 @@ impl Engine {
|
||||||
self.recording_state = Some(recording_state);
|
self.recording_state = Some(recording_state);
|
||||||
self.recording_progress_counter = 0; // Reset progress counter
|
self.recording_progress_counter = 0; // Reset progress counter
|
||||||
|
|
||||||
// Set the number of samples to skip on the recording state
|
// Set samples to skip (drained incrementally across callbacks)
|
||||||
if let Some(recording) = &mut self.recording_state {
|
if let Some(recording) = &mut self.recording_state {
|
||||||
recording.samples_to_skip = samples_in_buffer;
|
recording.samples_to_skip = samples_in_buffer;
|
||||||
if samples_in_buffer > 0 {
|
if self.debug_audio && samples_in_buffer > 0 {
|
||||||
eprintln!("Will skip {} stale samples from input buffer", samples_in_buffer);
|
eprintln!("[AUDIO DEBUG] Will skip {} stale samples from input buffer", samples_in_buffer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,9 @@ pub struct AudioGraph {
|
||||||
|
|
||||||
/// Current playback time (for automation nodes)
|
/// Current playback time (for automation nodes)
|
||||||
playback_time: f64,
|
playback_time: f64,
|
||||||
|
|
||||||
|
/// Cached topological sort order (invalidated on graph mutation)
|
||||||
|
topo_cache: Option<Vec<NodeIndex>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AudioGraph {
|
impl AudioGraph {
|
||||||
|
|
@ -113,12 +116,14 @@ impl AudioGraph {
|
||||||
midi_input_buffers: (0..16).map(|_| Vec::with_capacity(128)).collect(),
|
midi_input_buffers: (0..16).map(|_| Vec::with_capacity(128)).collect(),
|
||||||
node_positions: std::collections::HashMap::new(),
|
node_positions: std::collections::HashMap::new(),
|
||||||
playback_time: 0.0,
|
playback_time: 0.0,
|
||||||
|
topo_cache: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a node to the graph
|
/// Add a node to the graph
|
||||||
pub fn add_node(&mut self, node: Box<dyn AudioNode>) -> NodeIndex {
|
pub fn add_node(&mut self, node: Box<dyn AudioNode>) -> NodeIndex {
|
||||||
let graph_node = GraphNode::new(node, self.buffer_size);
|
let graph_node = GraphNode::new(node, self.buffer_size);
|
||||||
|
self.topo_cache = None;
|
||||||
self.graph.add_node(graph_node)
|
self.graph.add_node(graph_node)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -158,6 +163,7 @@ impl AudioGraph {
|
||||||
|
|
||||||
// Add the edge
|
// Add the edge
|
||||||
self.graph.add_edge(from, to, Connection { from_port, to_port });
|
self.graph.add_edge(from, to, Connection { from_port, to_port });
|
||||||
|
self.topo_cache = None;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -175,6 +181,7 @@ impl AudioGraph {
|
||||||
let conn = &self.graph[edge_idx];
|
let conn = &self.graph[edge_idx];
|
||||||
if conn.from_port == from_port && conn.to_port == to_port {
|
if conn.from_port == from_port && conn.to_port == to_port {
|
||||||
self.graph.remove_edge(edge_idx);
|
self.graph.remove_edge(edge_idx);
|
||||||
|
self.topo_cache = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -182,6 +189,7 @@ impl AudioGraph {
|
||||||
/// Remove a node from the graph
|
/// Remove a node from the graph
|
||||||
pub fn remove_node(&mut self, node: NodeIndex) {
|
pub fn remove_node(&mut self, node: NodeIndex) {
|
||||||
self.graph.remove_node(node);
|
self.graph.remove_node(node);
|
||||||
|
self.topo_cache = None;
|
||||||
|
|
||||||
// Update MIDI targets
|
// Update MIDI targets
|
||||||
self.midi_targets.retain(|&idx| idx != node);
|
self.midi_targets.retain(|&idx| idx != node);
|
||||||
|
|
@ -372,15 +380,21 @@ impl AudioGraph {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Topological sort for processing order
|
// Topological sort for processing order (cached, recomputed only on graph mutation)
|
||||||
let topo = petgraph::algo::toposort(&self.graph, None)
|
if self.topo_cache.is_none() {
|
||||||
.unwrap_or_else(|_| {
|
self.topo_cache = Some(
|
||||||
// If there's a cycle (shouldn't happen due to validation), just process in index order
|
petgraph::algo::toposort(&self.graph, None)
|
||||||
self.graph.node_indices().collect()
|
.unwrap_or_else(|_| {
|
||||||
});
|
// If there's a cycle (shouldn't happen due to validation), just process in index order
|
||||||
|
self.graph.node_indices().collect()
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let topo_len = self.topo_cache.as_ref().unwrap().len();
|
||||||
|
|
||||||
// Process nodes in topological order
|
// Process nodes in topological order
|
||||||
for node_idx in topo {
|
for topo_i in 0..topo_len {
|
||||||
|
let node_idx = self.topo_cache.as_ref().unwrap()[topo_i];
|
||||||
// Get input port information
|
// Get input port information
|
||||||
let inputs = self.graph[node_idx].node.inputs();
|
let inputs = self.graph[node_idx].node.inputs();
|
||||||
let num_audio_cv_inputs = inputs.iter().filter(|p| p.signal_type != SignalType::Midi).count();
|
let num_audio_cv_inputs = inputs.iter().filter(|p| p.signal_type != SignalType::Midi).count();
|
||||||
|
|
@ -409,25 +423,33 @@ impl AudioGraph {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect inputs from connected nodes
|
// Collect edge info into stack array to avoid heap allocation
|
||||||
let incoming = self.graph.edges_directed(node_idx, Direction::Incoming).collect::<Vec<_>>();
|
// (need to collect because we borrow graph immutably for source node data)
|
||||||
|
const MAX_EDGES: usize = 32;
|
||||||
|
let mut edge_info: [(NodeIndex, usize, usize); MAX_EDGES] = [(NodeIndex::new(0), 0, 0); MAX_EDGES];
|
||||||
|
let mut edge_count = 0;
|
||||||
|
for edge in self.graph.edges_directed(node_idx, Direction::Incoming) {
|
||||||
|
if edge_count < MAX_EDGES {
|
||||||
|
edge_info[edge_count] = (edge.source(), edge.weight().from_port, edge.weight().to_port);
|
||||||
|
edge_count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for edge in incoming {
|
for ei in 0..edge_count {
|
||||||
let source_idx = edge.source();
|
let (source_idx, from_port, to_port) = edge_info[ei];
|
||||||
let conn = edge.weight();
|
|
||||||
let source_node = &self.graph[source_idx];
|
let source_node = &self.graph[source_idx];
|
||||||
|
|
||||||
// Determine source port type
|
// Determine source port type
|
||||||
if conn.from_port < source_node.node.outputs().len() {
|
if from_port < source_node.node.outputs().len() {
|
||||||
let source_port_type = source_node.node.outputs()[conn.from_port].signal_type;
|
let source_port_type = source_node.node.outputs()[from_port].signal_type;
|
||||||
|
|
||||||
match source_port_type {
|
match source_port_type {
|
||||||
SignalType::Audio | SignalType::CV => {
|
SignalType::Audio | SignalType::CV => {
|
||||||
// Copy audio/CV data
|
// Copy audio/CV data
|
||||||
if conn.to_port < num_audio_cv_inputs && conn.from_port < source_node.output_buffers.len() {
|
if to_port < num_audio_cv_inputs && from_port < source_node.output_buffers.len() {
|
||||||
let source_buffer = &source_node.output_buffers[conn.from_port];
|
let source_buffer = &source_node.output_buffers[from_port];
|
||||||
if conn.to_port < self.input_buffers.len() {
|
if to_port < self.input_buffers.len() {
|
||||||
for (dst, src) in self.input_buffers[conn.to_port].iter_mut().zip(source_buffer.iter()) {
|
for (dst, src) in self.input_buffers[to_port].iter_mut().zip(source_buffer.iter()) {
|
||||||
// If dst is NaN (unconnected), replace it; otherwise add (for mixing)
|
// If dst is NaN (unconnected), replace it; otherwise add (for mixing)
|
||||||
if dst.is_nan() {
|
if dst.is_nan() {
|
||||||
*dst = *src;
|
*dst = *src;
|
||||||
|
|
@ -442,12 +464,12 @@ impl AudioGraph {
|
||||||
// Copy MIDI events
|
// Copy MIDI events
|
||||||
// Map from global port index to MIDI-only port index
|
// Map from global port index to MIDI-only port index
|
||||||
let midi_port_idx = inputs.iter()
|
let midi_port_idx = inputs.iter()
|
||||||
.take(conn.to_port + 1)
|
.take(to_port + 1)
|
||||||
.filter(|p| p.signal_type == SignalType::Midi)
|
.filter(|p| p.signal_type == SignalType::Midi)
|
||||||
.count() - 1;
|
.count() - 1;
|
||||||
|
|
||||||
let source_midi_idx = source_node.node.outputs().iter()
|
let source_midi_idx = source_node.node.outputs().iter()
|
||||||
.take(conn.from_port + 1)
|
.take(from_port + 1)
|
||||||
.filter(|p| p.signal_type == SignalType::Midi)
|
.filter(|p| p.signal_type == SignalType::Midi)
|
||||||
.count() - 1;
|
.count() - 1;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -296,15 +296,13 @@ impl AudioClipPool {
|
||||||
// Direct channel mapping
|
// Direct channel mapping
|
||||||
let ch_offset = dst_ch;
|
let ch_offset = dst_ch;
|
||||||
|
|
||||||
// Extract channel samples for interpolation
|
// Extract channel samples for interpolation (stack-allocated)
|
||||||
let mut channel_samples = Vec::with_capacity(KERNEL_SIZE);
|
let mut channel_samples = [0.0f32; KERNEL_SIZE];
|
||||||
for i in -(HALF_KERNEL as i32)..(HALF_KERNEL as i32) {
|
for (j, i) in (-(HALF_KERNEL as i32)..(HALF_KERNEL as i32)).enumerate() {
|
||||||
let idx = src_frame + i;
|
let idx = src_frame + i;
|
||||||
if idx >= 0 && (idx as usize) < audio_file.frames as usize {
|
if idx >= 0 && (idx as usize) < audio_file.frames as usize {
|
||||||
let sample_idx = (idx as usize) * src_channels + ch_offset;
|
let sample_idx = (idx as usize) * src_channels + ch_offset;
|
||||||
channel_samples.push(audio_file.data[sample_idx]);
|
channel_samples[j] = audio_file.data[sample_idx];
|
||||||
} else {
|
|
||||||
channel_samples.push(0.0);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -312,13 +310,11 @@ impl AudioClipPool {
|
||||||
|
|
||||||
} else if src_channels == 1 && dst_channels > 1 {
|
} else if src_channels == 1 && dst_channels > 1 {
|
||||||
// Mono to stereo - duplicate
|
// Mono to stereo - duplicate
|
||||||
let mut channel_samples = Vec::with_capacity(KERNEL_SIZE);
|
let mut channel_samples = [0.0f32; KERNEL_SIZE];
|
||||||
for i in -(HALF_KERNEL as i32)..(HALF_KERNEL as i32) {
|
for (j, i) in (-(HALF_KERNEL as i32)..(HALF_KERNEL as i32)).enumerate() {
|
||||||
let idx = src_frame + i;
|
let idx = src_frame + i;
|
||||||
if idx >= 0 && (idx as usize) < audio_file.frames as usize {
|
if idx >= 0 && (idx as usize) < audio_file.frames as usize {
|
||||||
channel_samples.push(audio_file.data[idx as usize]);
|
channel_samples[j] = audio_file.data[idx as usize];
|
||||||
} else {
|
|
||||||
channel_samples.push(0.0);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -329,14 +325,12 @@ impl AudioClipPool {
|
||||||
let mut sum = 0.0;
|
let mut sum = 0.0;
|
||||||
|
|
||||||
for src_ch in 0..src_channels {
|
for src_ch in 0..src_channels {
|
||||||
let mut channel_samples = Vec::with_capacity(KERNEL_SIZE);
|
let mut channel_samples = [0.0f32; KERNEL_SIZE];
|
||||||
for i in -(HALF_KERNEL as i32)..(HALF_KERNEL as i32) {
|
for (j, i) in (-(HALF_KERNEL as i32)..(HALF_KERNEL as i32)).enumerate() {
|
||||||
let idx = src_frame + i;
|
let idx = src_frame + i;
|
||||||
if idx >= 0 && (idx as usize) < audio_file.frames as usize {
|
if idx >= 0 && (idx as usize) < audio_file.frames as usize {
|
||||||
let sample_idx = (idx as usize) * src_channels + src_ch;
|
let sample_idx = (idx as usize) * src_channels + src_ch;
|
||||||
channel_samples.push(audio_file.data[sample_idx]);
|
channel_samples[j] = audio_file.data[sample_idx];
|
||||||
} else {
|
|
||||||
channel_samples.push(0.0);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sum += windowed_sinc_interpolate(&channel_samples, frac);
|
sum += windowed_sinc_interpolate(&channel_samples, frac);
|
||||||
|
|
@ -348,14 +342,12 @@ impl AudioClipPool {
|
||||||
// Mismatched channels - use modulo mapping
|
// Mismatched channels - use modulo mapping
|
||||||
let src_ch = dst_ch % src_channels;
|
let src_ch = dst_ch % src_channels;
|
||||||
|
|
||||||
let mut channel_samples = Vec::with_capacity(KERNEL_SIZE);
|
let mut channel_samples = [0.0f32; KERNEL_SIZE];
|
||||||
for i in -(HALF_KERNEL as i32)..(HALF_KERNEL as i32) {
|
for (j, i) in (-(HALF_KERNEL as i32)..(HALF_KERNEL as i32)).enumerate() {
|
||||||
let idx = src_frame + i;
|
let idx = src_frame + i;
|
||||||
if idx >= 0 && (idx as usize) < audio_file.frames as usize {
|
if idx >= 0 && (idx as usize) < audio_file.frames as usize {
|
||||||
let sample_idx = (idx as usize) * src_channels + src_ch;
|
let sample_idx = (idx as usize) * src_channels + src_ch;
|
||||||
channel_samples.push(audio_file.data[sample_idx]);
|
channel_samples[j] = audio_file.data[sample_idx];
|
||||||
} else {
|
|
||||||
channel_samples.push(0.0);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -367,8 +367,9 @@ impl Project {
|
||||||
output.len(),
|
output.len(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Render each root track
|
// Render each root track (index-based to avoid clone)
|
||||||
for &track_id in &self.root_tracks.clone() {
|
for i in 0..self.root_tracks.len() {
|
||||||
|
let track_id = self.root_tracks[i];
|
||||||
self.render_track(
|
self.render_track(
|
||||||
track_id,
|
track_id,
|
||||||
output,
|
output,
|
||||||
|
|
@ -439,8 +440,8 @@ impl Project {
|
||||||
track.render(output, midi_pool, ctx.playhead_seconds, ctx.sample_rate, ctx.channels);
|
track.render(output, midi_pool, ctx.playhead_seconds, ctx.sample_rate, ctx.channels);
|
||||||
}
|
}
|
||||||
Some(TrackNode::Group(group)) => {
|
Some(TrackNode::Group(group)) => {
|
||||||
// Get children IDs, check if this group is soloed, and transform context
|
// Read group properties and transform context (index-based child iteration to avoid clone)
|
||||||
let children: Vec<TrackId> = group.children.clone();
|
let num_children = group.children.len();
|
||||||
let this_group_is_soloed = group.solo;
|
let this_group_is_soloed = group.solo;
|
||||||
let child_ctx = group.transform_context(ctx);
|
let child_ctx = group.transform_context(ctx);
|
||||||
|
|
||||||
|
|
@ -452,7 +453,11 @@ impl Project {
|
||||||
// Recursively render all children into the group buffer
|
// Recursively render all children into the group buffer
|
||||||
// If this group is soloed (or parent was soloed), children inherit that state
|
// If this group is soloed (or parent was soloed), children inherit that state
|
||||||
let children_parent_soloed = parent_is_soloed || this_group_is_soloed;
|
let children_parent_soloed = parent_is_soloed || this_group_is_soloed;
|
||||||
for &child_id in &children {
|
for i in 0..num_children {
|
||||||
|
let child_id = match self.tracks.get(&track_id) {
|
||||||
|
Some(TrackNode::Group(g)) => g.children[i],
|
||||||
|
_ => break,
|
||||||
|
};
|
||||||
self.render_track(
|
self.render_track(
|
||||||
child_id,
|
child_id,
|
||||||
&mut group_buffer,
|
&mut group_buffer,
|
||||||
|
|
|
||||||
|
|
@ -578,6 +578,10 @@ pub struct AudioTrack {
|
||||||
/// Runtime effects processing graph (rebuilt from preset on load)
|
/// Runtime effects processing graph (rebuilt from preset on load)
|
||||||
#[serde(skip, default = "default_audio_graph")]
|
#[serde(skip, default = "default_audio_graph")]
|
||||||
pub effects_graph: AudioGraph,
|
pub effects_graph: AudioGraph,
|
||||||
|
|
||||||
|
/// Pre-allocated buffer for clip rendering (avoids heap allocation per callback)
|
||||||
|
#[serde(skip, default)]
|
||||||
|
clip_render_buffer: Vec<f32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Clone for AudioTrack {
|
impl Clone for AudioTrack {
|
||||||
|
|
@ -593,6 +597,7 @@ impl Clone for AudioTrack {
|
||||||
next_automation_id: self.next_automation_id,
|
next_automation_id: self.next_automation_id,
|
||||||
effects_graph_preset: self.effects_graph_preset.clone(),
|
effects_graph_preset: self.effects_graph_preset.clone(),
|
||||||
effects_graph: default_audio_graph(), // Create fresh graph, not cloned
|
effects_graph: default_audio_graph(), // Create fresh graph, not cloned
|
||||||
|
clip_render_buffer: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -635,6 +640,7 @@ impl AudioTrack {
|
||||||
next_automation_id: 0,
|
next_automation_id: 0,
|
||||||
effects_graph_preset: None,
|
effects_graph_preset: None,
|
||||||
effects_graph,
|
effects_graph,
|
||||||
|
clip_render_buffer: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -755,11 +761,13 @@ impl AudioTrack {
|
||||||
let buffer_duration_seconds = output.len() as f64 / (sample_rate as f64 * channels as f64);
|
let buffer_duration_seconds = output.len() as f64 / (sample_rate as f64 * channels as f64);
|
||||||
let buffer_end_seconds = playhead_seconds + buffer_duration_seconds;
|
let buffer_end_seconds = playhead_seconds + buffer_duration_seconds;
|
||||||
|
|
||||||
// Create a temporary buffer for clip rendering
|
// Split borrow: take clip_render_buffer out to avoid borrow conflict with &self methods
|
||||||
let mut clip_buffer = vec![0.0f32; output.len()];
|
let mut clip_buffer = std::mem::take(&mut self.clip_render_buffer);
|
||||||
|
clip_buffer.resize(output.len(), 0.0);
|
||||||
|
clip_buffer.fill(0.0);
|
||||||
let mut rendered = 0;
|
let mut rendered = 0;
|
||||||
|
|
||||||
// Render all active clip instances into the temporary buffer
|
// Render all active clip instances into the buffer
|
||||||
for clip in &self.clips {
|
for clip in &self.clips {
|
||||||
// Check if clip overlaps with current buffer time range
|
// Check if clip overlaps with current buffer time range
|
||||||
if clip.external_start < buffer_end_seconds && clip.external_end() > playhead_seconds {
|
if clip.external_start < buffer_end_seconds && clip.external_end() > playhead_seconds {
|
||||||
|
|
@ -787,6 +795,9 @@ impl AudioTrack {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Put the buffer back for reuse next callback
|
||||||
|
self.clip_render_buffer = clip_buffer;
|
||||||
|
|
||||||
// Process through the effects graph (this will write to output buffer)
|
// Process through the effects graph (this will write to output buffer)
|
||||||
self.effects_graph.process(output, &[], playhead_seconds);
|
self.effects_graph.process(output, &[], playhead_seconds);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,13 @@ impl AudioSystem {
|
||||||
/// * `event_emitter` - Optional event emitter for pushing events to external systems
|
/// * `event_emitter` - Optional event emitter for pushing events to external systems
|
||||||
/// * `buffer_size` - Audio buffer size in frames (128, 256, 512, 1024, etc.)
|
/// * `buffer_size` - Audio buffer size in frames (128, 256, 512, 1024, etc.)
|
||||||
/// Smaller = lower latency but higher CPU usage. Default: 256
|
/// Smaller = lower latency but higher CPU usage. Default: 256
|
||||||
|
///
|
||||||
|
/// # Environment Variables
|
||||||
|
/// * `DAW_AUDIO_DEBUG=1` - Enable audio callback timing diagnostics. Logs:
|
||||||
|
/// - Device and config info at startup
|
||||||
|
/// - First 10 callback buffer sizes (to detect ALSA buffer variance)
|
||||||
|
/// - Per-overrun timing breakdown (command vs render time)
|
||||||
|
/// - Periodic (~5s) timing summaries (avg/worst/overrun rate)
|
||||||
pub fn new(
|
pub fn new(
|
||||||
event_emitter: Option<std::sync::Arc<dyn EventEmitter>>,
|
event_emitter: Option<std::sync::Arc<dyn EventEmitter>>,
|
||||||
buffer_size: u32,
|
buffer_size: u32,
|
||||||
|
|
@ -62,6 +69,12 @@ impl AudioSystem {
|
||||||
let default_output_config = output_device.default_output_config().map_err(|e| e.to_string())?;
|
let default_output_config = output_device.default_output_config().map_err(|e| e.to_string())?;
|
||||||
let sample_rate = default_output_config.sample_rate().0;
|
let sample_rate = default_output_config.sample_rate().0;
|
||||||
let channels = default_output_config.channels() as u32;
|
let channels = default_output_config.channels() as u32;
|
||||||
|
let debug_audio = std::env::var("DAW_AUDIO_DEBUG").map_or(false, |v| v == "1");
|
||||||
|
if debug_audio {
|
||||||
|
eprintln!("[AUDIO DEBUG] Device: {:?}", output_device.name());
|
||||||
|
eprintln!("[AUDIO DEBUG] Default config: {:?}", default_output_config);
|
||||||
|
eprintln!("[AUDIO DEBUG] Default buffer size: {:?}", default_output_config.buffer_size());
|
||||||
|
}
|
||||||
|
|
||||||
// Create queues
|
// Create queues
|
||||||
let (command_tx, command_rx) = rtrb::RingBuffer::new(512); // Larger buffer for MIDI + UI commands
|
let (command_tx, command_rx) = rtrb::RingBuffer::new(512); // Larger buffer for MIDI + UI commands
|
||||||
|
|
@ -102,29 +115,27 @@ impl AudioSystem {
|
||||||
|
|
||||||
let mut output_buffer = vec![0.0f32; 16384];
|
let mut output_buffer = vec![0.0f32; 16384];
|
||||||
|
|
||||||
// Log audio configuration
|
if debug_audio {
|
||||||
println!("Audio Output Configuration:");
|
eprintln!("[AUDIO DEBUG] Output config: sr={} Hz, ch={}, buf={:?}",
|
||||||
println!(" Sample Rate: {} Hz", output_config.sample_rate.0);
|
output_config.sample_rate.0, output_config.channels, output_config.buffer_size);
|
||||||
println!(" Channels: {}", output_config.channels);
|
if let cpal::BufferSize::Fixed(size) = output_config.buffer_size {
|
||||||
println!(" Buffer Size: {:?}", output_config.buffer_size);
|
let latency_ms = (size as f64 / output_config.sample_rate.0 as f64) * 1000.0;
|
||||||
|
eprintln!("[AUDIO DEBUG] Expected latency: {:.2} ms", latency_ms);
|
||||||
// Calculate expected latency
|
}
|
||||||
if let cpal::BufferSize::Fixed(size) = output_config.buffer_size {
|
|
||||||
let latency_ms = (size as f64 / output_config.sample_rate.0 as f64) * 1000.0;
|
|
||||||
println!(" Expected Latency: {:.2} ms", latency_ms);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut first_callback = true;
|
let mut callback_log_count: u32 = 0;
|
||||||
|
let cb_debug = debug_audio;
|
||||||
let output_stream = output_device
|
let output_stream = output_device
|
||||||
.build_output_stream(
|
.build_output_stream(
|
||||||
&output_config,
|
&output_config,
|
||||||
move |data: &mut [f32], _: &cpal::OutputCallbackInfo| {
|
move |data: &mut [f32], _: &cpal::OutputCallbackInfo| {
|
||||||
if first_callback {
|
if cb_debug && callback_log_count < 10 {
|
||||||
let frames = data.len() / output_config.channels as usize;
|
let frames = data.len() / output_config.channels as usize;
|
||||||
let latency_ms = (frames as f64 / output_config.sample_rate.0 as f64) * 1000.0;
|
let latency_ms = (frames as f64 / output_config.sample_rate.0 as f64) * 1000.0;
|
||||||
println!("Audio callback buffer size: {} samples ({} frames, {:.2} ms latency)",
|
eprintln!("[AUDIO CB #{}] {} samples ({} frames, {:.2} ms)",
|
||||||
data.len(), frames, latency_ms);
|
callback_log_count, data.len(), frames, latency_ms);
|
||||||
first_callback = false;
|
callback_log_count += 1;
|
||||||
}
|
}
|
||||||
let buf = &mut output_buffer[..data.len()];
|
let buf = &mut output_buffer[..data.len()];
|
||||||
buf.fill(0.0);
|
buf.fill(0.0);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue