Improved metronome for better xrun handling
This commit is contained in:
parent
deb6bd8f4c
commit
7a78c6b9e6
@ -9,13 +9,13 @@ pub enum AudioData {
|
||||
/// Unconsolidated linked chunks with sync offset (used during/after recording)
|
||||
Unconsolidated {
|
||||
chunks: Arc<AudioChunk>,
|
||||
sync_offset: usize, // samples from column start to recording start
|
||||
length: usize, // total samples in recording
|
||||
sync_offset: usize, // samples from column start to recording start
|
||||
length: usize, // total samples in recording
|
||||
},
|
||||
|
||||
/// Consolidated single buffer, reordered for optimal playback
|
||||
Consolidated {
|
||||
buffer: Box<[f32]>, // single optimized array
|
||||
buffer: Box<[f32]>, // single optimized array
|
||||
},
|
||||
}
|
||||
|
||||
@ -80,23 +80,20 @@ impl AudioData {
|
||||
output_slice.fill(0.0);
|
||||
Ok(())
|
||||
}
|
||||
Self::Unconsolidated { chunks, sync_offset, length } => {
|
||||
self.copy_unconsolidated_samples(
|
||||
chunks,
|
||||
*sync_offset,
|
||||
*length,
|
||||
output_slice,
|
||||
logical_position,
|
||||
volume
|
||||
)
|
||||
}
|
||||
Self::Unconsolidated {
|
||||
chunks,
|
||||
sync_offset,
|
||||
length,
|
||||
} => self.copy_unconsolidated_samples(
|
||||
chunks,
|
||||
*sync_offset,
|
||||
*length,
|
||||
output_slice,
|
||||
logical_position,
|
||||
volume,
|
||||
),
|
||||
Self::Consolidated { buffer } => {
|
||||
self.copy_consolidated_samples(
|
||||
buffer,
|
||||
output_slice,
|
||||
logical_position,
|
||||
volume
|
||||
)
|
||||
self.copy_consolidated_samples(buffer, output_slice, logical_position, volume)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -104,9 +101,11 @@ impl AudioData {
|
||||
/// Get underlying chunk for post-record processing
|
||||
pub fn get_chunk_for_processing(&self) -> Option<(Arc<AudioChunk>, usize)> {
|
||||
match self {
|
||||
Self::Unconsolidated { chunks, sync_offset, .. } => {
|
||||
Some((chunks.clone(), *sync_offset))
|
||||
}
|
||||
Self::Unconsolidated {
|
||||
chunks,
|
||||
sync_offset,
|
||||
..
|
||||
} => Some((chunks.clone(), *sync_offset)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@ -114,7 +113,9 @@ impl AudioData {
|
||||
/// Replace with consolidated buffer from post-record processing
|
||||
pub fn set_consolidated_buffer(&mut self, buffer: Box<[f32]>) -> Result<()> {
|
||||
match self {
|
||||
Self::Unconsolidated { length: old_length, .. } => {
|
||||
Self::Unconsolidated {
|
||||
length: old_length, ..
|
||||
} => {
|
||||
if buffer.len() != *old_length {
|
||||
return Err(LooperError::OutOfBounds(std::panic::Location::caller()));
|
||||
}
|
||||
@ -248,7 +249,11 @@ mod tests {
|
||||
let audio_data = AudioData::new_unconsolidated(&mut factory, sync_offset).unwrap();
|
||||
|
||||
match audio_data {
|
||||
AudioData::Unconsolidated { sync_offset: offset, length, .. } => {
|
||||
AudioData::Unconsolidated {
|
||||
sync_offset: offset,
|
||||
length,
|
||||
..
|
||||
} => {
|
||||
assert_eq!(offset, 100);
|
||||
assert_eq!(length, 0);
|
||||
}
|
||||
@ -284,7 +289,9 @@ mod tests {
|
||||
let audio_data = AudioData::new_empty();
|
||||
let mut output = vec![99.0; 4]; // Fill with non-zero to verify silence
|
||||
|
||||
audio_data.copy_samples_to_output(&mut output, 0, 1.0).unwrap();
|
||||
audio_data
|
||||
.copy_samples_to_output(&mut output, 0, 1.0)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(output, vec![0.0, 0.0, 0.0, 0.0]);
|
||||
}
|
||||
@ -299,7 +306,9 @@ mod tests {
|
||||
audio_data.append_samples(&samples, &mut factory).unwrap();
|
||||
|
||||
let mut output = vec![0.0; 4];
|
||||
audio_data.copy_samples_to_output(&mut output, 0, 1.0).unwrap();
|
||||
audio_data
|
||||
.copy_samples_to_output(&mut output, 0, 1.0)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(output, vec![1.0, 2.0, 3.0, 4.0]);
|
||||
}
|
||||
@ -316,7 +325,9 @@ mod tests {
|
||||
audio_data.append_samples(&samples, &mut factory).unwrap();
|
||||
|
||||
let mut output = vec![0.0; 4];
|
||||
audio_data.copy_samples_to_output(&mut output, 0, 1.0).unwrap(); // Start at logical beat 1
|
||||
audio_data
|
||||
.copy_samples_to_output(&mut output, 0, 1.0)
|
||||
.unwrap(); // Start at logical beat 1
|
||||
|
||||
// Should get: [C, D, A, B] = [30.0, 40.0, 10.0, 20.0]
|
||||
assert_eq!(output, vec![30.0, 40.0, 10.0, 20.0]);
|
||||
@ -331,7 +342,9 @@ mod tests {
|
||||
audio_data.append_samples(&samples, &mut factory).unwrap();
|
||||
|
||||
let mut output = vec![0.0; 4];
|
||||
audio_data.copy_samples_to_output(&mut output, 0, 0.5).unwrap(); // 50% volume
|
||||
audio_data
|
||||
.copy_samples_to_output(&mut output, 0, 0.5)
|
||||
.unwrap(); // 50% volume
|
||||
|
||||
assert_eq!(output, vec![0.5, 1.0, 1.5, 2.0]);
|
||||
}
|
||||
@ -345,7 +358,9 @@ mod tests {
|
||||
audio_data.append_samples(&samples, &mut factory).unwrap();
|
||||
|
||||
let mut output = vec![0.0; 6]; // Request more samples than available
|
||||
audio_data.copy_samples_to_output(&mut output, 0, 1.0).unwrap();
|
||||
audio_data
|
||||
.copy_samples_to_output(&mut output, 0, 1.0)
|
||||
.unwrap();
|
||||
|
||||
// Should loop: [1.0, 2.0, 1.0, 2.0, 1.0, 2.0]
|
||||
assert_eq!(output, vec![1.0, 2.0, 1.0, 2.0, 1.0, 2.0]);
|
||||
@ -363,7 +378,9 @@ mod tests {
|
||||
audio_data.append_samples(&samples, &mut factory).unwrap();
|
||||
|
||||
let mut output = vec![0.0; 6]; // Request 2 full loops
|
||||
audio_data.copy_samples_to_output(&mut output, 0, 1.0).unwrap();
|
||||
audio_data
|
||||
.copy_samples_to_output(&mut output, 0, 1.0)
|
||||
.unwrap();
|
||||
|
||||
// Should get: [B, C, A, B, C, A] = [20.0, 30.0, 10.0, 20.0, 30.0, 10.0]
|
||||
assert_eq!(output, vec![20.0, 30.0, 10.0, 20.0, 30.0, 10.0]);
|
||||
@ -377,7 +394,9 @@ mod tests {
|
||||
let audio_data = AudioData::Consolidated { buffer };
|
||||
|
||||
let mut output = vec![0.0; 4];
|
||||
audio_data.copy_samples_to_output(&mut output, 0, 1.0).unwrap();
|
||||
audio_data
|
||||
.copy_samples_to_output(&mut output, 0, 1.0)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(output, vec![1.0, 2.0, 3.0, 4.0]);
|
||||
}
|
||||
@ -417,7 +436,9 @@ mod tests {
|
||||
// Create consolidated buffer with same length
|
||||
let consolidated_buffer = vec![10.0, 20.0, 30.0, 40.0].into_boxed_slice();
|
||||
|
||||
audio_data.set_consolidated_buffer(consolidated_buffer).unwrap();
|
||||
audio_data
|
||||
.set_consolidated_buffer(consolidated_buffer)
|
||||
.unwrap();
|
||||
|
||||
// Should now be Consolidated variant
|
||||
match audio_data {
|
||||
|
||||
@ -1,16 +1,17 @@
|
||||
mod allocator;
|
||||
mod audio_chunk;
|
||||
mod audio_data;
|
||||
mod beep;
|
||||
mod chunk_factory;
|
||||
mod connection_manager;
|
||||
mod looper_error;
|
||||
mod metronome;
|
||||
mod midi;
|
||||
mod notification_handler;
|
||||
mod persistence_manager;
|
||||
mod post_record_handler;
|
||||
mod process_handler;
|
||||
mod state;
|
||||
mod persistence_manager;
|
||||
mod track;
|
||||
|
||||
use std::sync::Arc;
|
||||
@ -18,19 +19,19 @@ use std::sync::Arc;
|
||||
use allocator::Allocator;
|
||||
use audio_chunk::AudioChunk;
|
||||
use audio_data::AudioData;
|
||||
use beep::generate_beep;
|
||||
use chunk_factory::ChunkFactory;
|
||||
use connection_manager::ConnectionManager;
|
||||
use looper_error::LooperError;
|
||||
use looper_error::Result;
|
||||
use metronome::Metronome;
|
||||
use metronome::generate_beep;
|
||||
use notification_handler::JackNotification;
|
||||
use notification_handler::NotificationHandler;
|
||||
use post_record_handler::PostRecordHandler;
|
||||
use persistence_manager::PersistenceManager;
|
||||
use post_record_handler::PostRecordController;
|
||||
use post_record_handler::PostRecordHandler;
|
||||
use process_handler::ProcessHandler;
|
||||
use state::State;
|
||||
use persistence_manager::PersistenceManager;
|
||||
use track::Track;
|
||||
use track::TrackState;
|
||||
use track::TrackTiming;
|
||||
@ -58,14 +59,15 @@ async fn main() {
|
||||
let notification_handler = NotificationHandler::new();
|
||||
let mut notification_channel = notification_handler.subscribe();
|
||||
|
||||
let (mut persistence_manager, state_watch) = PersistenceManager::new(notification_handler.subscribe());
|
||||
let (mut persistence_manager, state_watch) =
|
||||
PersistenceManager::new(notification_handler.subscribe());
|
||||
|
||||
// Load state values for metronome configuration
|
||||
let initial_state = state_watch.borrow().clone();
|
||||
|
||||
// Create post-record handler and get controller for ProcessHandler
|
||||
let (mut post_record_handler, post_record_controller) = PostRecordHandler::new()
|
||||
.expect("Could not create post-record handler");
|
||||
let (mut post_record_handler, post_record_controller) =
|
||||
PostRecordHandler::new().expect("Could not create post-record handler");
|
||||
|
||||
let process_handler = ProcessHandler::new(
|
||||
ports,
|
||||
@ -73,7 +75,8 @@ async fn main() {
|
||||
beep_samples,
|
||||
&initial_state,
|
||||
post_record_controller,
|
||||
).expect("Could not create process handler");
|
||||
)
|
||||
.expect("Could not create process handler");
|
||||
|
||||
let mut connection_manager = ConnectionManager::new(
|
||||
state_watch,
|
||||
|
||||
25
src/beep.rs
Normal file
25
src/beep.rs
Normal file
@ -0,0 +1,25 @@
|
||||
use crate::*;
|
||||
|
||||
/// Generate a 100ms sine wave beep at 1000Hz
|
||||
pub fn generate_beep<F: ChunkFactory>(
|
||||
sample_rate: u32,
|
||||
chunk_factory: &mut F,
|
||||
) -> Result<Arc<AudioChunk>> {
|
||||
const FREQUENCY_HZ: f32 = 1000.0;
|
||||
const DURATION_MS: f32 = 100.0;
|
||||
|
||||
let sample_count = ((sample_rate as f32) * (DURATION_MS / 1000.0)) as usize;
|
||||
let mut samples = Vec::with_capacity(sample_count);
|
||||
|
||||
for i in 0..sample_count {
|
||||
let t = i as f32 / sample_rate as f32;
|
||||
let sample = (std::f32::consts::TAU * FREQUENCY_HZ * t).sin();
|
||||
samples.push(sample);
|
||||
}
|
||||
|
||||
// Create AudioChunk and fill it with samples
|
||||
let mut chunk = chunk_factory.create_chunk()?;
|
||||
chunk.append_samples(&samples, chunk_factory)?;
|
||||
|
||||
Ok(chunk)
|
||||
}
|
||||
423
src/metronome.rs
423
src/metronome.rs
@ -1,123 +1,368 @@
|
||||
use crate::*;
|
||||
use std::f32::consts::TAU;
|
||||
|
||||
pub struct Metronome {
|
||||
// Audio playback
|
||||
beep_samples: Arc<AudioChunk>,
|
||||
click_volume: f32, // 0.0 to 1.0
|
||||
click_samples: Arc<AudioChunk>,
|
||||
click_volume: f32,
|
||||
|
||||
// Timing state
|
||||
samples_per_beat: u32,
|
||||
beat_time: u32,
|
||||
frames_per_beat: u32,
|
||||
frames_since_last_beat: u32, // Where we are in the current beat cycle
|
||||
last_frame_time: Option<u32>, // For xrun detection
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct BufferTiming {
|
||||
/// Beat index within the current buffer (if any)
|
||||
pub beat_in_buffer: Option<u32>,
|
||||
|
||||
/// Number of frames missed due to xrun (0 if no xrun)
|
||||
pub missed_frames: u32,
|
||||
|
||||
/// Beat index within the missed frames (if any)
|
||||
pub beat_in_missed: Option<u32>,
|
||||
}
|
||||
|
||||
impl Metronome {
|
||||
pub fn new(beep_samples: Arc<AudioChunk>, state: &State) -> Self {
|
||||
pub fn new(click_samples: Arc<AudioChunk>, state: &State) -> Self {
|
||||
Self {
|
||||
beep_samples,
|
||||
click_samples,
|
||||
click_volume: state.metronome.click_volume,
|
||||
samples_per_beat: state.metronome.samples_per_beat,
|
||||
beat_time: 0,
|
||||
frames_per_beat: state.metronome.frames_per_beat,
|
||||
frames_since_last_beat: 0,
|
||||
last_frame_time: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn samples_per_beat(&self) -> u32 {
|
||||
self.samples_per_beat
|
||||
pub fn frames_per_beat(&self) -> u32 {
|
||||
self.frames_per_beat
|
||||
}
|
||||
|
||||
/// Process audio for current buffer, writing to output slice
|
||||
/// Returns the sample index where a beat occurred, if any
|
||||
pub fn process(&mut self, ps: &jack::ProcessScope, ports: &mut JackPorts) -> Result<Option<u32>> {
|
||||
if 0 == self.beat_time {
|
||||
self.beat_time = ps.last_frame_time();
|
||||
pub fn process(
|
||||
&mut self,
|
||||
ps: &jack::ProcessScope,
|
||||
ports: &mut JackPorts,
|
||||
) -> Result<BufferTiming> {
|
||||
let buffer_size = ps.n_frames();
|
||||
let current_frame_time = ps.last_frame_time();
|
||||
|
||||
// Calculate timing for this buffer
|
||||
let timing = self.calculate_timing(current_frame_time, buffer_size)?;
|
||||
|
||||
// Get output buffer for click track
|
||||
let click_output = ports.click_track_out.as_mut_slice(ps);
|
||||
|
||||
self.render_click(buffer_size, current_frame_time, &timing, click_output);
|
||||
|
||||
Ok(timing)
|
||||
}
|
||||
|
||||
fn render_click(
|
||||
&mut self,
|
||||
buffer_size: u32,
|
||||
current_frame_time: u32,
|
||||
timing: &BufferTiming,
|
||||
click_output: &mut [f32],
|
||||
) {
|
||||
// Calculate current position within the beep (frames since last beat started)
|
||||
let frames_since_beat_start = current_frame_time - self.frames_since_last_beat;
|
||||
let click_length = self.click_samples.sample_count as u32;
|
||||
|
||||
if let Some(beat_offset) = timing.beat_in_buffer {
|
||||
// Write silence up to beat boundary
|
||||
let silence_end = beat_offset.min(buffer_size);
|
||||
click_output[0..silence_end as _].fill(0.0);
|
||||
|
||||
// Write click samples from boundary onward
|
||||
if beat_offset < buffer_size {
|
||||
let remaining_buffer = buffer_size - beat_offset;
|
||||
let samples_to_write = remaining_buffer.min(click_length);
|
||||
|
||||
// Copy click samples in bulk
|
||||
let dest = &mut click_output
|
||||
[beat_offset as usize..beat_offset as usize + samples_to_write as usize];
|
||||
if self.click_samples.copy_samples(dest, 0).is_ok() {
|
||||
// Apply volume scaling with iterators
|
||||
dest.iter_mut()
|
||||
.for_each(|sample| *sample *= self.click_volume);
|
||||
}
|
||||
|
||||
// Fill remaining buffer with silence
|
||||
click_output[(beat_offset as usize + samples_to_write as usize)..].fill(0.0);
|
||||
}
|
||||
} else if frames_since_beat_start < click_length {
|
||||
// Continue playing click from previous beat if still within beep duration
|
||||
let click_start_offset = frames_since_beat_start;
|
||||
let remaining_click_samples = click_length - click_start_offset;
|
||||
let samples_to_write = buffer_size.min(remaining_click_samples);
|
||||
|
||||
// Copy remaining beep samples in bulk
|
||||
let dest = &mut click_output[0..samples_to_write as _];
|
||||
if self
|
||||
.click_samples
|
||||
.copy_samples(dest, click_start_offset as _)
|
||||
.is_ok()
|
||||
{
|
||||
// Apply volume scaling with iterators
|
||||
dest.iter_mut()
|
||||
.for_each(|sample| *sample *= self.click_volume);
|
||||
}
|
||||
|
||||
// Fill remaining buffer with silence
|
||||
click_output[samples_to_write as _..].fill(0.0);
|
||||
} else {
|
||||
click_output.fill(0.0);
|
||||
}
|
||||
}
|
||||
|
||||
let time_since_last_beat = ps.last_frame_time() - self.beat_time;
|
||||
pub fn calculate_timing(
|
||||
&mut self,
|
||||
current_frame_time: u32,
|
||||
buffer_size: u32,
|
||||
) -> Result<BufferTiming> {
|
||||
// Detect xrun
|
||||
let (missed_samples, beat_in_missed) = if let Some(last) = self.last_frame_time {
|
||||
let expected = last.wrapping_add(buffer_size); // Handle u32 wrap
|
||||
if current_frame_time != expected {
|
||||
// We have a gap
|
||||
let missed = current_frame_time.wrapping_sub(expected);
|
||||
|
||||
if time_since_last_beat >= (2 * self.samples_per_beat) && self.beat_time != 0 {
|
||||
return Err(LooperError::Xrun(std::panic::Location::caller()));
|
||||
}
|
||||
// Check if we missed multiple beats
|
||||
let total_samples = self.frames_since_last_beat + missed + buffer_size;
|
||||
if total_samples >= 2 * self.frames_per_beat {
|
||||
return Err(LooperError::Xrun(std::panic::Location::caller()));
|
||||
}
|
||||
|
||||
let beat_sample_index = if time_since_last_beat >= self.samples_per_beat {
|
||||
self.beat_time += self.samples_per_beat;
|
||||
Some(self.beat_time - ps.last_frame_time())
|
||||
// Check if a beat occurred in the missed section
|
||||
let beat_in_missed = if self.frames_since_last_beat + missed >= self.frames_per_beat
|
||||
{
|
||||
Some(self.frames_per_beat - self.frames_since_last_beat)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
(missed, beat_in_missed)
|
||||
} else {
|
||||
(0, None)
|
||||
}
|
||||
} else {
|
||||
// First call
|
||||
(0, None)
|
||||
};
|
||||
|
||||
// Check for beat in current buffer
|
||||
// We need to account for any missed samples here too
|
||||
let start_position = (self.frames_since_last_beat + missed_samples) % self.frames_per_beat;
|
||||
let beat_in_buffer = if start_position + buffer_size >= self.frames_per_beat {
|
||||
Some(self.frames_per_beat - start_position)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Get output buffer for click track
|
||||
let click_output = ports.click_track_out.as_mut_slice(ps);
|
||||
let buffer_size = click_output.len();
|
||||
// Update state - advance by total samples (missed + buffer)
|
||||
self.frames_since_last_beat =
|
||||
(self.frames_since_last_beat + missed_samples + buffer_size) % self.frames_per_beat;
|
||||
self.last_frame_time = Some(current_frame_time);
|
||||
|
||||
// Calculate current position within the beep (frames since last beat started)
|
||||
let frames_since_beat_start = ps.last_frame_time() - self.beat_time;
|
||||
let beep_length = self.beep_samples.sample_count as u32;
|
||||
Ok(BufferTiming {
|
||||
beat_in_buffer,
|
||||
missed_frames: missed_samples,
|
||||
beat_in_missed,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(beat_offset) = beat_sample_index {
|
||||
let beat_offset = beat_offset as usize;
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// Write silence up to beat boundary
|
||||
let silence_end = beat_offset.min(buffer_size);
|
||||
click_output[0..silence_end].fill(0.0);
|
||||
fn create_test_metronome(samples_per_beat: u32) -> Metronome {
|
||||
let beep_samples = Arc::new(AudioChunk {
|
||||
samples: vec![1.0; 100].into_boxed_slice(),
|
||||
sample_count: 100,
|
||||
next: None,
|
||||
});
|
||||
|
||||
// Write beep samples from boundary onward
|
||||
if beat_offset < buffer_size {
|
||||
let remaining_buffer = buffer_size - beat_offset;
|
||||
let samples_to_write = remaining_buffer.min(beep_length as usize);
|
||||
|
||||
// Copy beep samples in bulk
|
||||
let dest = &mut click_output[beat_offset..beat_offset + samples_to_write];
|
||||
if self.beep_samples.copy_samples(dest, 0).is_ok() {
|
||||
// Apply volume scaling with iterators
|
||||
dest.iter_mut().for_each(|sample| *sample *= self.click_volume);
|
||||
}
|
||||
|
||||
// Fill remaining buffer with silence
|
||||
click_output[(beat_offset + samples_to_write)..].fill(0.0);
|
||||
}
|
||||
} else if frames_since_beat_start < beep_length {
|
||||
// Continue playing beep from previous beat if still within beep duration
|
||||
let beep_start_offset = frames_since_beat_start as usize;
|
||||
let remaining_beep_samples = beep_length as usize - beep_start_offset;
|
||||
let samples_to_write = buffer_size.min(remaining_beep_samples);
|
||||
|
||||
// Copy remaining beep samples in bulk
|
||||
let dest = &mut click_output[0..samples_to_write];
|
||||
if self.beep_samples.copy_samples(dest, beep_start_offset).is_ok() {
|
||||
// Apply volume scaling with iterators
|
||||
dest.iter_mut().for_each(|sample| *sample *= self.click_volume);
|
||||
}
|
||||
|
||||
// Fill remaining buffer with silence
|
||||
click_output[samples_to_write..].fill(0.0);
|
||||
} else {
|
||||
click_output.fill(0.0);
|
||||
Metronome {
|
||||
click_samples: beep_samples,
|
||||
click_volume: 1.0,
|
||||
frames_per_beat: samples_per_beat,
|
||||
frames_since_last_beat: 0,
|
||||
last_frame_time: None,
|
||||
}
|
||||
|
||||
Ok(beat_sample_index)
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a 100ms sine wave beep at 1000Hz
|
||||
pub fn generate_beep<F: ChunkFactory>(
|
||||
sample_rate: u32,
|
||||
chunk_factory: &mut F,
|
||||
) -> Result<Arc<AudioChunk>> {
|
||||
const FREQUENCY_HZ: f32 = 1000.0;
|
||||
const DURATION_MS: f32 = 100.0;
|
||||
|
||||
let sample_count = ((sample_rate as f32) * (DURATION_MS / 1000.0)) as usize;
|
||||
let mut samples = Vec::with_capacity(sample_count);
|
||||
|
||||
for i in 0..sample_count {
|
||||
let t = i as f32 / sample_rate as f32;
|
||||
let sample = (TAU * FREQUENCY_HZ * t).sin();
|
||||
samples.push(sample);
|
||||
}
|
||||
|
||||
// Create AudioChunk and fill it with samples
|
||||
let mut chunk = chunk_factory.create_chunk()?;
|
||||
chunk.append_samples(&samples, chunk_factory)?;
|
||||
#[test]
|
||||
fn test_first_call_initialization() {
|
||||
let mut metronome = create_test_metronome(1000);
|
||||
|
||||
Ok(chunk)
|
||||
let result = metronome.calculate_timing(5000, 128).unwrap();
|
||||
|
||||
assert_eq!(metronome.frames_since_last_beat, 128);
|
||||
assert_eq!(metronome.last_frame_time, Some(5000));
|
||||
assert_eq!(result.missed_frames, 0);
|
||||
assert_eq!(result.beat_in_missed, None);
|
||||
assert_eq!(result.beat_in_buffer, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_normal_buffer_no_beat() {
|
||||
let mut metronome = create_test_metronome(1000);
|
||||
|
||||
// Initialize at time 1000
|
||||
metronome.calculate_timing(1000, 128).unwrap();
|
||||
assert_eq!(metronome.frames_since_last_beat, 128);
|
||||
|
||||
// Next buffer at 1128 - no beat expected
|
||||
let result = metronome.calculate_timing(1128, 128).unwrap();
|
||||
|
||||
assert_eq!(metronome.frames_since_last_beat, 256);
|
||||
assert_eq!(result.missed_frames, 0);
|
||||
assert_eq!(result.beat_in_missed, None);
|
||||
assert_eq!(result.beat_in_buffer, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_beat_in_buffer() {
|
||||
let mut metronome = create_test_metronome(1000);
|
||||
|
||||
// Initialize at time 0
|
||||
metronome.calculate_timing(0, 512).unwrap();
|
||||
assert_eq!(metronome.frames_since_last_beat, 512);
|
||||
|
||||
// Next buffer: 512 -> 1024, beat at 1000 (offset 488)
|
||||
let result = metronome.calculate_timing(512, 512).unwrap();
|
||||
|
||||
assert_eq!(result.missed_frames, 0);
|
||||
assert_eq!(result.beat_in_missed, None);
|
||||
assert_eq!(result.beat_in_buffer, Some(488)); // 1000 - 512
|
||||
assert_eq!(metronome.frames_since_last_beat, 24); // (512 + 512) % 1000
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_xrun_no_missed_beat() {
|
||||
let mut metronome = create_test_metronome(1000);
|
||||
|
||||
// Initialize at time 1000
|
||||
metronome.calculate_timing(1000, 128).unwrap();
|
||||
assert_eq!(metronome.frames_since_last_beat, 128);
|
||||
|
||||
// Normal buffer at 1128
|
||||
metronome.calculate_timing(1128, 128).unwrap();
|
||||
assert_eq!(metronome.frames_since_last_beat, 256);
|
||||
|
||||
// Xrun: expected 1256 but got 1428 (172 samples missed)
|
||||
let result = metronome.calculate_timing(1428, 128).unwrap();
|
||||
|
||||
assert_eq!(result.missed_frames, 172);
|
||||
assert_eq!(result.beat_in_missed, None);
|
||||
assert_eq!(result.beat_in_buffer, None);
|
||||
assert_eq!(metronome.frames_since_last_beat, 556); // 256 + 172 + 128
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_xrun_with_missed_beat() {
|
||||
let mut metronome = create_test_metronome(1000);
|
||||
|
||||
// Initialize at time 1000
|
||||
metronome.calculate_timing(1000, 128).unwrap();
|
||||
assert_eq!(metronome.frames_since_last_beat, 128);
|
||||
|
||||
// Normal buffer at 1128
|
||||
metronome.calculate_timing(1128, 128).unwrap();
|
||||
assert_eq!(metronome.frames_since_last_beat, 256);
|
||||
|
||||
// Xrun: expected 1256 but got 2228 (972 samples missed)
|
||||
// We're at position 256, miss 972 samples = 1228 total
|
||||
// Beat occurs at position 1000, so beat_in_missed = 1000 - 256 = 744
|
||||
let result = metronome.calculate_timing(2228, 128).unwrap();
|
||||
|
||||
assert_eq!(result.missed_frames, 972);
|
||||
assert_eq!(result.beat_in_missed, Some(744)); // 1000 - 256
|
||||
assert_eq!(result.beat_in_buffer, None);
|
||||
assert_eq!(metronome.frames_since_last_beat, 356); // (256 + 972 + 128) % 1000
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_xrun_with_missed_beat_and_upcoming_beat() {
|
||||
let mut metronome = create_test_metronome(1000);
|
||||
|
||||
// Initialize at time 1000
|
||||
metronome.calculate_timing(1000, 128).unwrap();
|
||||
|
||||
// Normal buffer at 1128
|
||||
metronome.calculate_timing(1128, 128).unwrap();
|
||||
assert_eq!(metronome.frames_since_last_beat, 256);
|
||||
|
||||
// Xrun: expected 1256 but got 2078 (822 samples missed)
|
||||
// We're at position 256, miss 822 samples = 1078 total
|
||||
// Beat occurs at position 1000, so beat_in_missed = 1000 - 256 = 744
|
||||
// After missed: position = 78, buffer = 128, no beat in buffer
|
||||
let result = metronome.calculate_timing(2078, 128).unwrap();
|
||||
|
||||
assert_eq!(result.missed_frames, 822);
|
||||
assert_eq!(result.beat_in_missed, Some(744)); // 1000 - 256
|
||||
assert_eq!(result.beat_in_buffer, None); // 78 + 128 < 1000
|
||||
assert_eq!(metronome.frames_since_last_beat, 206); // (256 + 822 + 128) % 1000
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_xrun_multiple_beats_error() {
|
||||
let mut metronome = create_test_metronome(1000);
|
||||
|
||||
// Initialize at time 1000
|
||||
metronome.calculate_timing(1000, 128).unwrap();
|
||||
|
||||
// Normal buffer at 1128
|
||||
metronome.calculate_timing(1128, 128).unwrap();
|
||||
|
||||
// Xrun: expected 1256 but got 3328 (2072 samples missed)
|
||||
// Total advancement would be 256 + 2072 + 128 = 2456 samples
|
||||
// That's more than 2 beats (2000 samples), so error
|
||||
let result = metronome.calculate_timing(3328, 128);
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_consecutive_buffers_with_beat() {
|
||||
let mut metronome = create_test_metronome(1000);
|
||||
|
||||
// First buffer - initialization
|
||||
let result1 = metronome.calculate_timing(0, 512).unwrap();
|
||||
assert_eq!(result1.beat_in_buffer, None);
|
||||
assert_eq!(metronome.frames_since_last_beat, 512);
|
||||
|
||||
// Second buffer - beat should occur at position 1000
|
||||
let result2 = metronome.calculate_timing(512, 512).unwrap();
|
||||
assert_eq!(result2.beat_in_buffer, Some(488)); // 1000 - 512
|
||||
assert_eq!(metronome.frames_since_last_beat, 24); // (512 + 512) % 1000
|
||||
|
||||
// Third buffer - no beat
|
||||
let result3 = metronome.calculate_timing(1024, 512).unwrap();
|
||||
assert_eq!(result3.beat_in_buffer, None);
|
||||
assert_eq!(metronome.frames_since_last_beat, 536);
|
||||
|
||||
// Fourth buffer - next beat at position 1000 again
|
||||
let result4 = metronome.calculate_timing(1536, 512).unwrap();
|
||||
assert_eq!(result4.beat_in_buffer, Some(464)); // 1000 - 536
|
||||
assert_eq!(metronome.frames_since_last_beat, 48); // (536 + 512) % 1000
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_u32_wrapping() {
|
||||
let mut metronome = create_test_metronome(1000);
|
||||
|
||||
// Initialize near u32::MAX
|
||||
let start_time = u32::MAX - 100;
|
||||
metronome.calculate_timing(start_time, 128).unwrap();
|
||||
|
||||
// Next buffer wraps around
|
||||
let next_time = start_time.wrapping_add(128);
|
||||
let result = metronome.calculate_timing(next_time, 128).unwrap();
|
||||
|
||||
assert_eq!(result.missed_frames, 0);
|
||||
assert_eq!(metronome.frames_since_last_beat, 256);
|
||||
}
|
||||
}
|
||||
|
||||
@ -63,7 +63,12 @@ impl jack::NotificationHandler for NotificationHandler {
|
||||
.expect("Could not send port connection notification");
|
||||
}
|
||||
|
||||
fn port_registration(&mut self, _client: &jack::Client, _port_id: jack::PortId, register: bool) {
|
||||
fn port_registration(
|
||||
&mut self,
|
||||
_client: &jack::Client,
|
||||
_port_id: jack::PortId,
|
||||
register: bool,
|
||||
) {
|
||||
if register {
|
||||
let notification = JackNotification::PortRegistered {};
|
||||
self.channel
|
||||
|
||||
@ -24,11 +24,20 @@ pub struct PostRecordController {
|
||||
|
||||
impl PostRecordController {
|
||||
/// Send a post-record processing request (RT-safe)
|
||||
pub fn send_request(&self, chunk_chain: Arc<AudioChunk>, sync_offset: u32, sample_rate: u32) -> Result<()> {
|
||||
let request = PostRecordRequest { chunk_chain, sync_offset, sample_rate };
|
||||
pub fn send_request(
|
||||
&self,
|
||||
chunk_chain: Arc<AudioChunk>,
|
||||
sync_offset: u32,
|
||||
sample_rate: u32,
|
||||
) -> Result<()> {
|
||||
let request = PostRecordRequest {
|
||||
chunk_chain,
|
||||
sync_offset,
|
||||
sample_rate,
|
||||
};
|
||||
|
||||
match self.request_sender.try_send(request) {
|
||||
Ok(true) => Ok(()), // Successfully sent
|
||||
Ok(true) => Ok(()), // Successfully sent
|
||||
Ok(false) => Err(LooperError::ChunkAllocation(std::panic::Location::caller())), // Channel full
|
||||
Err(_) => Err(LooperError::ChunkAllocation(std::panic::Location::caller())), // Channel closed
|
||||
}
|
||||
@ -38,7 +47,7 @@ impl PostRecordController {
|
||||
pub fn try_recv_response(&self) -> Option<PostRecordResponse> {
|
||||
match self.response_receiver.try_recv() {
|
||||
Ok(Some(response)) => Some(response),
|
||||
Ok(None) | Err(_) => None, // No message or channel closed = None
|
||||
Ok(None) | Err(_) => None, // No message or channel closed = None
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -86,23 +95,26 @@ impl PostRecordHandler {
|
||||
}
|
||||
|
||||
/// Process a single post-record request
|
||||
async fn process_request(
|
||||
&self,
|
||||
request: PostRecordRequest,
|
||||
) -> Result<()> {
|
||||
log::debug!("Processing post-record request for {} samples with sync_offset {}",
|
||||
request.chunk_chain.len(), request.sync_offset);
|
||||
async fn process_request(&self, request: PostRecordRequest) -> Result<()> {
|
||||
log::debug!(
|
||||
"Processing post-record request for {} samples with sync_offset {}",
|
||||
request.chunk_chain.len(),
|
||||
request.sync_offset
|
||||
);
|
||||
|
||||
// Step 1: Consolidate and reorder chunk chain based on sync offset
|
||||
let consolidated_buffer = self.consolidate_with_sync_offset(
|
||||
&request.chunk_chain,
|
||||
request.sync_offset as usize
|
||||
)?;
|
||||
let consolidated_buffer =
|
||||
self.consolidate_with_sync_offset(&request.chunk_chain, request.sync_offset as usize)?;
|
||||
|
||||
log::debug!("Consolidated and reordered {} samples", consolidated_buffer.len());
|
||||
log::debug!(
|
||||
"Consolidated and reordered {} samples",
|
||||
consolidated_buffer.len()
|
||||
);
|
||||
|
||||
// Step 2: Send consolidated buffer back to RT thread immediately
|
||||
let response = PostRecordResponse { consolidated_buffer };
|
||||
let response = PostRecordResponse {
|
||||
consolidated_buffer,
|
||||
};
|
||||
|
||||
if let Err(_) = self.response_sender.send(response).await {
|
||||
log::warn!("Failed to send consolidated buffer to RT thread");
|
||||
@ -113,7 +125,10 @@ impl PostRecordHandler {
|
||||
let consolidated_chunk = AudioChunk::consolidate(&request.chunk_chain);
|
||||
let file_path = self.get_file_path();
|
||||
|
||||
match self.save_wav_file(&consolidated_chunk, request.sample_rate, &file_path).await {
|
||||
match self
|
||||
.save_wav_file(&consolidated_chunk, request.sample_rate, &file_path)
|
||||
.await
|
||||
{
|
||||
Ok(_) => log::info!("Saved recording to {:?}", file_path),
|
||||
Err(e) => log::error!("Failed to save recording to {:?}: {}", file_path, e),
|
||||
}
|
||||
@ -122,7 +137,11 @@ impl PostRecordHandler {
|
||||
}
|
||||
|
||||
/// Consolidate chunk chain and reorder samples based on sync offset
|
||||
fn consolidate_with_sync_offset(&self, chunk_chain: &Arc<AudioChunk>, sync_offset: usize) -> Result<Box<[f32]>> {
|
||||
fn consolidate_with_sync_offset(
|
||||
&self,
|
||||
chunk_chain: &Arc<AudioChunk>,
|
||||
sync_offset: usize,
|
||||
) -> Result<Box<[f32]>> {
|
||||
let total_length = chunk_chain.len();
|
||||
|
||||
if total_length == 0 {
|
||||
@ -175,11 +194,13 @@ impl PostRecordHandler {
|
||||
|
||||
// Write all samples from the chunk
|
||||
for sample in chunk_samples {
|
||||
writer.write_sample(sample)
|
||||
writer
|
||||
.write_sample(sample)
|
||||
.map_err(|_| LooperError::StateSave(std::panic::Location::caller()))?;
|
||||
}
|
||||
|
||||
writer.finalize()
|
||||
writer
|
||||
.finalize()
|
||||
.map_err(|_| LooperError::StateSave(std::panic::Location::caller()))?;
|
||||
|
||||
Ok::<(), LooperError>(())
|
||||
@ -190,8 +211,7 @@ impl PostRecordHandler {
|
||||
|
||||
/// Create save directory and return path
|
||||
fn create_directory() -> Result<PathBuf> {
|
||||
let mut path = dirs::home_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("."));
|
||||
let mut path = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
|
||||
path.push(".fcb_looper");
|
||||
|
||||
std::fs::create_dir_all(&path)
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
use crate::*;
|
||||
|
||||
// Testing constants for sync offset functionality
|
||||
const SYNC_OFFSET_BEATS: u32 = 2; // Start recording at beat 3 (0-indexed)
|
||||
const AUTO_STOP_BEATS: u32 = 4; // Record for 4 beats total
|
||||
const SYNC_OFFSET_BEATS: u32 = 2; // Start recording at beat 3 (0-indexed)
|
||||
const AUTO_STOP_BEATS: u32 = 4; // Record for 4 beats total
|
||||
|
||||
pub struct ProcessHandler<F: ChunkFactory> {
|
||||
track: Track,
|
||||
@ -45,11 +45,12 @@ impl<F: ChunkFactory> ProcessHandler<F> {
|
||||
|
||||
/// Handle auto-stop record button (Button 3)
|
||||
pub fn record_auto_stop(&mut self) -> Result<()> {
|
||||
let samples_per_beat = self.metronome.samples_per_beat();
|
||||
let samples_per_beat = self.metronome.frames_per_beat();
|
||||
let sync_offset = SYNC_OFFSET_BEATS * samples_per_beat;
|
||||
let target_samples = AUTO_STOP_BEATS * samples_per_beat;
|
||||
|
||||
self.track.queue_record_auto_stop(target_samples as usize, sync_offset as usize);
|
||||
self.track
|
||||
.queue_record_auto_stop(target_samples as usize, sync_offset as usize);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -75,7 +76,8 @@ impl<F: ChunkFactory> jack::ProcessHandler for ProcessHandler<F> {
|
||||
log::error!("Error processing metronome: {}", e);
|
||||
return jack::Control::Quit;
|
||||
}
|
||||
};
|
||||
}
|
||||
.beat_in_buffer;
|
||||
|
||||
let buffer_size = client.buffer_size() as usize;
|
||||
let state_before = self.track.current_state().clone();
|
||||
@ -84,21 +86,22 @@ impl<F: ChunkFactory> jack::ProcessHandler for ProcessHandler<F> {
|
||||
let timing = self.calculate_track_timing(beat_sample_index, &state_before);
|
||||
|
||||
// Process track audio with calculated timing
|
||||
let should_consolidate = match self.track.process(
|
||||
ps,
|
||||
&mut self.ports,
|
||||
timing,
|
||||
&mut self.chunk_factory
|
||||
) {
|
||||
Ok(consolidate) => consolidate,
|
||||
Err(e) => {
|
||||
log::error!("Error processing track: {}", e);
|
||||
return jack::Control::Quit;
|
||||
}
|
||||
};
|
||||
let should_consolidate =
|
||||
match self
|
||||
.track
|
||||
.process(ps, &mut self.ports, timing, &mut self.chunk_factory)
|
||||
{
|
||||
Ok(consolidate) => consolidate,
|
||||
Err(e) => {
|
||||
log::error!("Error processing track: {}", e);
|
||||
return jack::Control::Quit;
|
||||
}
|
||||
};
|
||||
|
||||
// Handle post-record processing
|
||||
if let Err(e) = self.handle_post_record_processing(should_consolidate, client.sample_rate() as u32) {
|
||||
if let Err(e) =
|
||||
self.handle_post_record_processing(should_consolidate, client.sample_rate() as u32)
|
||||
{
|
||||
log::error!("Error handling post-record processing: {}", e);
|
||||
return jack::Control::Quit;
|
||||
}
|
||||
@ -112,17 +115,26 @@ impl<F: ChunkFactory> jack::ProcessHandler for ProcessHandler<F> {
|
||||
|
||||
impl<F: ChunkFactory> ProcessHandler<F> {
|
||||
/// Handle post-record processing: send requests and swap buffers
|
||||
fn handle_post_record_processing(&mut self, should_consolidate: bool, sample_rate: u32) -> Result<()> {
|
||||
fn handle_post_record_processing(
|
||||
&mut self,
|
||||
should_consolidate: bool,
|
||||
sample_rate: u32,
|
||||
) -> Result<()> {
|
||||
// Send audio data for processing if track indicates consolidation needed
|
||||
if should_consolidate {
|
||||
if let Some((chunk_chain, sync_offset)) = self.track.get_audio_data_for_processing() {
|
||||
self.post_record_controller.send_request(chunk_chain, sync_offset as u32, sample_rate)?;
|
||||
self.post_record_controller.send_request(
|
||||
chunk_chain,
|
||||
sync_offset as u32,
|
||||
sample_rate,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for consolidation response
|
||||
if let Some(response) = self.post_record_controller.try_recv_response() {
|
||||
self.track.set_consolidated_buffer(response.consolidated_buffer)?;
|
||||
self.track
|
||||
.set_consolidated_buffer(response.consolidated_buffer)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@ -203,7 +215,9 @@ impl<F: ChunkFactory> ProcessHandler<F> {
|
||||
}
|
||||
|
||||
// Check if state transition at beat affects position
|
||||
if state_after == TrackState::Playing && !matches!(state_before, TrackState::Playing) {
|
||||
if state_after == TrackState::Playing
|
||||
&& !matches!(state_before, TrackState::Playing)
|
||||
{
|
||||
// Started playing at beat - reset position to post-beat calculation
|
||||
self.playback_position = self.calculate_post_beat_position(state_before);
|
||||
}
|
||||
|
||||
11
src/state.rs
11
src/state.rs
@ -16,8 +16,8 @@ pub struct ConnectionState {
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MetronomeState {
|
||||
pub samples_per_beat: u32,
|
||||
pub click_volume: f32, // 0.0 to 1.0
|
||||
pub frames_per_beat: u32,
|
||||
pub click_volume: f32, // 0.0 to 1.0
|
||||
}
|
||||
|
||||
impl Default for State {
|
||||
@ -30,12 +30,9 @@ impl Default for State {
|
||||
click_track_out: Vec::new(),
|
||||
},
|
||||
metronome: MetronomeState {
|
||||
samples_per_beat: 96000, // 120 BPM at 192kHz sample rate
|
||||
click_volume: 0.5, // Default 50% volume
|
||||
frames_per_beat: 96000, // 120 BPM at 192kHz sample rate
|
||||
click_volume: 0.5, // Default 50% volume
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
51
src/track.rs
51
src/track.rs
@ -7,21 +7,21 @@ pub enum TrackState {
|
||||
Playing, // Currently playing (PLAY)
|
||||
Recording, // Currently recording (REC) - manual stop
|
||||
RecordingAutoStop {
|
||||
target_samples: usize, // Auto-stop when this many samples recorded
|
||||
sync_offset: usize, // Offset in samples from column start
|
||||
target_samples: usize, // Auto-stop when this many samples recorded
|
||||
sync_offset: usize, // Offset in samples from column start
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum TrackTiming {
|
||||
NoBeat {
|
||||
position: usize
|
||||
position: usize,
|
||||
},
|
||||
Beat {
|
||||
pre_beat_position: usize,
|
||||
post_beat_position: usize,
|
||||
beat_sample_index: usize,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
pub struct Track {
|
||||
@ -70,7 +70,7 @@ impl Track {
|
||||
TrackTiming::Beat {
|
||||
pre_beat_position,
|
||||
post_beat_position,
|
||||
beat_sample_index
|
||||
beat_sample_index,
|
||||
} => {
|
||||
if beat_sample_index > 0 {
|
||||
// Process samples before beat with current state
|
||||
@ -129,7 +129,8 @@ impl Track {
|
||||
TrackState::Recording => {
|
||||
// Record input samples (manual recording)
|
||||
let samples_to_record = &input_buffer[start_index..end_index];
|
||||
self.audio_data.append_samples(samples_to_record, chunk_factory)?;
|
||||
self.audio_data
|
||||
.append_samples(samples_to_record, chunk_factory)?;
|
||||
|
||||
// Output silence during recording
|
||||
output_buffer[start_index..end_index].fill(0.0);
|
||||
@ -147,7 +148,7 @@ impl Track {
|
||||
if samples_to_append > 0 {
|
||||
self.audio_data.append_samples(
|
||||
&samples_to_record[..samples_to_append],
|
||||
chunk_factory
|
||||
chunk_factory,
|
||||
)?;
|
||||
}
|
||||
|
||||
@ -175,25 +176,25 @@ impl Track {
|
||||
|
||||
/// Apply state transition from next_state to current_state
|
||||
/// Returns true if track should be consolidated and saved
|
||||
fn apply_state_transition<F: ChunkFactory>(
|
||||
&mut self,
|
||||
chunk_factory: &mut F
|
||||
) -> Result<bool> {
|
||||
fn apply_state_transition<F: ChunkFactory>(&mut self, chunk_factory: &mut F) -> Result<bool> {
|
||||
// Check if this is a recording → playing transition (consolidation trigger)
|
||||
let should_consolidate = matches!(
|
||||
(&self.current_state, &self.next_state),
|
||||
(TrackState::Recording, TrackState::Playing) |
|
||||
(TrackState::RecordingAutoStop { .. }, TrackState::Playing)
|
||||
(TrackState::Recording, TrackState::Playing)
|
||||
| (TrackState::RecordingAutoStop { .. }, TrackState::Playing)
|
||||
);
|
||||
|
||||
// Handle transitions that require setup
|
||||
match (&self.current_state, &self.next_state) {
|
||||
(current_state, TrackState::Recording) if !matches!(current_state, TrackState::Recording) => {
|
||||
(current_state, TrackState::Recording)
|
||||
if !matches!(current_state, TrackState::Recording) =>
|
||||
{
|
||||
// Starting manual recording - clear previous data and create new unconsolidated data
|
||||
self.audio_data = AudioData::new_unconsolidated(chunk_factory, 0)?;
|
||||
}
|
||||
(current_state, TrackState::RecordingAutoStop { sync_offset, .. })
|
||||
if !matches!(current_state, TrackState::RecordingAutoStop { .. }) => {
|
||||
if !matches!(current_state, TrackState::RecordingAutoStop { .. }) =>
|
||||
{
|
||||
// Starting auto-stop recording - clear previous data and create new unconsolidated data with offset
|
||||
self.audio_data = AudioData::new_unconsolidated(chunk_factory, *sync_offset)?;
|
||||
}
|
||||
@ -273,19 +274,31 @@ impl Track {
|
||||
pub fn queue_record_auto_stop(&mut self, target_samples: usize, sync_offset: usize) {
|
||||
match self.current_state {
|
||||
TrackState::Empty | TrackState::Idle => {
|
||||
self.next_state = TrackState::RecordingAutoStop { target_samples, sync_offset };
|
||||
self.next_state = TrackState::RecordingAutoStop {
|
||||
target_samples,
|
||||
sync_offset,
|
||||
};
|
||||
}
|
||||
TrackState::Recording => {
|
||||
// Switch from manual to auto-stop recording
|
||||
self.next_state = TrackState::RecordingAutoStop { target_samples, sync_offset };
|
||||
self.next_state = TrackState::RecordingAutoStop {
|
||||
target_samples,
|
||||
sync_offset,
|
||||
};
|
||||
}
|
||||
TrackState::RecordingAutoStop { .. } => {
|
||||
// Already auto-recording - update parameters
|
||||
self.next_state = TrackState::RecordingAutoStop { target_samples, sync_offset };
|
||||
self.next_state = TrackState::RecordingAutoStop {
|
||||
target_samples,
|
||||
sync_offset,
|
||||
};
|
||||
}
|
||||
TrackState::Playing => {
|
||||
// Stop playing and start auto-recording
|
||||
self.next_state = TrackState::RecordingAutoStop { target_samples, sync_offset };
|
||||
self.next_state = TrackState::RecordingAutoStop {
|
||||
target_samples,
|
||||
sync_offset,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user