Added audio_data and RecordingAutoStop
This commit is contained in:
parent
9b4fd1685c
commit
fc8ca5ce86
465
src/audio_data.rs
Normal file
465
src/audio_data.rs
Normal file
@ -0,0 +1,465 @@
|
|||||||
|
use crate::*;
|
||||||
|
|
||||||
|
/// Audio data representation supporting sync offsets for column-based timing
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum AudioData {
|
||||||
|
/// No audio data present
|
||||||
|
Empty,
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Consolidated single buffer, reordered for optimal playback
|
||||||
|
Consolidated {
|
||||||
|
buffer: Box<[f32]>, // single optimized array
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AudioData {
|
||||||
|
/// Create new empty audio data
|
||||||
|
pub fn new_empty() -> Self {
|
||||||
|
Self::Empty
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create new unconsolidated audio data from initial chunk
|
||||||
|
pub fn new_unconsolidated<F: ChunkFactory>(
|
||||||
|
chunk_factory: &mut F,
|
||||||
|
sync_offset: usize,
|
||||||
|
) -> Result<Self> {
|
||||||
|
Ok(Self::Unconsolidated {
|
||||||
|
chunks: chunk_factory.create_chunk()?,
|
||||||
|
sync_offset,
|
||||||
|
length: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get total length in samples
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
match self {
|
||||||
|
Self::Empty => 0,
|
||||||
|
Self::Unconsolidated { length, .. } => *length,
|
||||||
|
Self::Consolidated { buffer } => buffer.len(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if audio data is empty
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.len() == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Append samples during recording (only valid for Unconsolidated state)
|
||||||
|
pub fn append_samples<F: ChunkFactory>(
|
||||||
|
&mut self,
|
||||||
|
samples: &[f32],
|
||||||
|
chunk_factory: &mut F,
|
||||||
|
) -> Result<()> {
|
||||||
|
match self {
|
||||||
|
Self::Unconsolidated { chunks, length, .. } => {
|
||||||
|
chunks.append_samples(samples, chunk_factory)?;
|
||||||
|
*length += samples.len();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
_ => Err(LooperError::OutOfBounds(std::panic::Location::caller())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Copy samples to output buffer with volume scaling and looping
|
||||||
|
/// logical_position is the position within the column cycle (0 = beat 1)
|
||||||
|
pub fn copy_samples_to_output(
|
||||||
|
&self,
|
||||||
|
output_slice: &mut [f32],
|
||||||
|
logical_position: usize,
|
||||||
|
volume: f32,
|
||||||
|
) -> Result<()> {
|
||||||
|
match self {
|
||||||
|
Self::Empty => {
|
||||||
|
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::Consolidated { buffer } => {
|
||||||
|
self.copy_consolidated_samples(
|
||||||
|
buffer,
|
||||||
|
output_slice,
|
||||||
|
logical_position,
|
||||||
|
volume
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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, .. } => {
|
||||||
|
if buffer.len() != *old_length {
|
||||||
|
return Err(LooperError::OutOfBounds(std::panic::Location::caller()));
|
||||||
|
}
|
||||||
|
*self = Self::Consolidated { buffer };
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
_ => Err(LooperError::OutOfBounds(std::panic::Location::caller())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear all audio data
|
||||||
|
pub fn clear(&mut self) -> Result<()> {
|
||||||
|
*self = Self::Empty;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AudioData {
|
||||||
|
/// Copy samples from unconsolidated chunks with sync offset handling
|
||||||
|
fn copy_unconsolidated_samples(
|
||||||
|
&self,
|
||||||
|
chunks: &Arc<AudioChunk>,
|
||||||
|
sync_offset: usize,
|
||||||
|
length: usize,
|
||||||
|
output_slice: &mut [f32],
|
||||||
|
mut logical_position: usize,
|
||||||
|
volume: f32,
|
||||||
|
) -> Result<()> {
|
||||||
|
if length == 0 {
|
||||||
|
output_slice.fill(0.0);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut samples_written = 0;
|
||||||
|
let samples_needed = output_slice.len();
|
||||||
|
|
||||||
|
while samples_written < samples_needed {
|
||||||
|
let samples_remaining = samples_needed - samples_written;
|
||||||
|
let samples_until_loop = length - logical_position;
|
||||||
|
let samples_to_copy = samples_remaining.min(samples_until_loop);
|
||||||
|
|
||||||
|
// Map logical position to buffer position using sync offset
|
||||||
|
let buffer_position = (logical_position + sync_offset) % length;
|
||||||
|
|
||||||
|
// Copy from chunks to output slice
|
||||||
|
chunks.copy_samples(
|
||||||
|
&mut output_slice[samples_written..samples_written + samples_to_copy],
|
||||||
|
buffer_position,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Apply volume scaling in-place
|
||||||
|
for sample in &mut output_slice[samples_written..samples_written + samples_to_copy] {
|
||||||
|
*sample *= volume;
|
||||||
|
}
|
||||||
|
|
||||||
|
samples_written += samples_to_copy;
|
||||||
|
logical_position += samples_to_copy;
|
||||||
|
|
||||||
|
// Handle looping
|
||||||
|
if logical_position >= length {
|
||||||
|
logical_position = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Copy samples from consolidated buffer (already reordered)
|
||||||
|
fn copy_consolidated_samples(
|
||||||
|
&self,
|
||||||
|
buffer: &[f32],
|
||||||
|
output_slice: &mut [f32],
|
||||||
|
mut logical_position: usize,
|
||||||
|
volume: f32,
|
||||||
|
) -> Result<()> {
|
||||||
|
let length = buffer.len();
|
||||||
|
if length == 0 {
|
||||||
|
output_slice.fill(0.0);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut samples_written = 0;
|
||||||
|
let samples_needed = output_slice.len();
|
||||||
|
|
||||||
|
while samples_written < samples_needed {
|
||||||
|
let samples_remaining = samples_needed - samples_written;
|
||||||
|
let samples_until_loop = length - logical_position;
|
||||||
|
let samples_to_copy = samples_remaining.min(samples_until_loop);
|
||||||
|
|
||||||
|
// Direct copy since consolidated buffer is already reordered
|
||||||
|
let src = &buffer[logical_position..logical_position + samples_to_copy];
|
||||||
|
let dest = &mut output_slice[samples_written..samples_written + samples_to_copy];
|
||||||
|
dest.copy_from_slice(src);
|
||||||
|
|
||||||
|
// Apply volume scaling in-place
|
||||||
|
for sample in dest {
|
||||||
|
*sample *= volume;
|
||||||
|
}
|
||||||
|
|
||||||
|
samples_written += samples_to_copy;
|
||||||
|
logical_position += samples_to_copy;
|
||||||
|
|
||||||
|
// Handle looping
|
||||||
|
if logical_position >= length {
|
||||||
|
logical_position = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::chunk_factory::mock::MockFactory;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_new_empty() {
|
||||||
|
let audio_data = AudioData::new_empty();
|
||||||
|
|
||||||
|
assert_eq!(audio_data.len(), 0);
|
||||||
|
assert!(audio_data.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_new_unconsolidated() {
|
||||||
|
let mut factory = MockFactory::new(vec![AudioChunk::allocate(1024)]);
|
||||||
|
let sync_offset = 100;
|
||||||
|
|
||||||
|
let audio_data = AudioData::new_unconsolidated(&mut factory, sync_offset).unwrap();
|
||||||
|
|
||||||
|
match audio_data {
|
||||||
|
AudioData::Unconsolidated { sync_offset: offset, length, .. } => {
|
||||||
|
assert_eq!(offset, 100);
|
||||||
|
assert_eq!(length, 0);
|
||||||
|
}
|
||||||
|
_ => panic!("Expected Unconsolidated variant"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_append_samples_unconsolidated() {
|
||||||
|
let mut factory = MockFactory::new(vec![AudioChunk::allocate(1024)]);
|
||||||
|
let mut audio_data = AudioData::new_unconsolidated(&mut factory, 0).unwrap();
|
||||||
|
|
||||||
|
let samples = vec![1.0, 2.0, 3.0, 4.0];
|
||||||
|
audio_data.append_samples(&samples, &mut factory).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(audio_data.len(), 4);
|
||||||
|
assert!(!audio_data.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_append_samples_invalid_state() {
|
||||||
|
let mut factory = MockFactory::new(vec![]);
|
||||||
|
let mut audio_data = AudioData::new_empty();
|
||||||
|
|
||||||
|
let samples = vec![1.0, 2.0];
|
||||||
|
let result = audio_data.append_samples(&samples, &mut factory);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_copy_samples_empty() {
|
||||||
|
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();
|
||||||
|
|
||||||
|
assert_eq!(output, vec![0.0, 0.0, 0.0, 0.0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_copy_samples_unconsolidated_no_offset() {
|
||||||
|
let mut factory = MockFactory::new(vec![AudioChunk::allocate(10)]);
|
||||||
|
let mut audio_data = AudioData::new_unconsolidated(&mut factory, 0).unwrap();
|
||||||
|
|
||||||
|
// Add some test samples
|
||||||
|
let samples = vec![1.0, 2.0, 3.0, 4.0];
|
||||||
|
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();
|
||||||
|
|
||||||
|
assert_eq!(output, vec![1.0, 2.0, 3.0, 4.0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_copy_samples_unconsolidated_with_offset() {
|
||||||
|
let mut factory = MockFactory::new(vec![AudioChunk::allocate(10)]);
|
||||||
|
let sync_offset = 2;
|
||||||
|
let mut audio_data = AudioData::new_unconsolidated(&mut factory, sync_offset).unwrap();
|
||||||
|
|
||||||
|
// Recording order: [A, B, C, D] but recorded starting at beat 3 (sync_offset=2)
|
||||||
|
// So logical mapping should be: beat1=C, beat2=D, beat3=A, beat4=B
|
||||||
|
let samples = vec![10.0, 20.0, 30.0, 40.0]; // A, B, C, D
|
||||||
|
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
|
||||||
|
|
||||||
|
// 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]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_copy_samples_unconsolidated_with_volume() {
|
||||||
|
let mut factory = MockFactory::new(vec![AudioChunk::allocate(10)]);
|
||||||
|
let mut audio_data = AudioData::new_unconsolidated(&mut factory, 0).unwrap();
|
||||||
|
|
||||||
|
let samples = vec![1.0, 2.0, 3.0, 4.0];
|
||||||
|
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
|
||||||
|
|
||||||
|
assert_eq!(output, vec![0.5, 1.0, 1.5, 2.0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_copy_samples_unconsolidated_looping() {
|
||||||
|
let mut factory = MockFactory::new(vec![AudioChunk::allocate(10)]);
|
||||||
|
let mut audio_data = AudioData::new_unconsolidated(&mut factory, 0).unwrap();
|
||||||
|
|
||||||
|
let samples = vec![1.0, 2.0];
|
||||||
|
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();
|
||||||
|
|
||||||
|
// 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]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_copy_samples_unconsolidated_offset_with_looping() {
|
||||||
|
let mut factory = MockFactory::new(vec![AudioChunk::allocate(10)]);
|
||||||
|
let sync_offset = 1;
|
||||||
|
let mut audio_data = AudioData::new_unconsolidated(&mut factory, sync_offset).unwrap();
|
||||||
|
|
||||||
|
// Recording: [A, B, C] starting at beat 2 (sync_offset=1)
|
||||||
|
// Logical mapping: beat1=B, beat2=C, beat3=A
|
||||||
|
let samples = vec![10.0, 20.0, 30.0];
|
||||||
|
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();
|
||||||
|
|
||||||
|
// 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]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_copy_samples_consolidated() {
|
||||||
|
// Create consolidated data directly
|
||||||
|
let buffer = vec![1.0, 2.0, 3.0, 4.0].into_boxed_slice();
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
assert_eq!(output, vec![1.0, 2.0, 3.0, 4.0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_chunk_for_processing() {
|
||||||
|
let mut factory = MockFactory::new(vec![AudioChunk::allocate(10)]);
|
||||||
|
let sync_offset = 42;
|
||||||
|
let mut audio_data = AudioData::new_unconsolidated(&mut factory, sync_offset).unwrap();
|
||||||
|
|
||||||
|
let samples = vec![1.0, 2.0, 3.0];
|
||||||
|
audio_data.append_samples(&samples, &mut factory).unwrap();
|
||||||
|
|
||||||
|
let result = audio_data.get_chunk_for_processing();
|
||||||
|
assert!(result.is_some());
|
||||||
|
|
||||||
|
let (chunk, offset) = result.unwrap();
|
||||||
|
assert_eq!(offset, 42);
|
||||||
|
assert_eq!(chunk.len(), 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_chunk_for_processing_wrong_state() {
|
||||||
|
let audio_data = AudioData::new_empty();
|
||||||
|
let result = audio_data.get_chunk_for_processing();
|
||||||
|
assert!(result.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_set_consolidated_buffer() {
|
||||||
|
let mut factory = MockFactory::new(vec![AudioChunk::allocate(10)]);
|
||||||
|
let mut audio_data = AudioData::new_unconsolidated(&mut factory, 100).unwrap();
|
||||||
|
|
||||||
|
let samples = vec![1.0, 2.0, 3.0, 4.0];
|
||||||
|
audio_data.append_samples(&samples, &mut factory).unwrap();
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
|
||||||
|
// Should now be Consolidated variant
|
||||||
|
match audio_data {
|
||||||
|
AudioData::Consolidated { buffer } => {
|
||||||
|
assert_eq!(buffer.len(), 4);
|
||||||
|
}
|
||||||
|
_ => panic!("Expected Consolidated variant"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_set_consolidated_buffer_length_mismatch() {
|
||||||
|
let mut factory = MockFactory::new(vec![AudioChunk::allocate(10)]);
|
||||||
|
let mut audio_data = AudioData::new_unconsolidated(&mut factory, 0).unwrap();
|
||||||
|
|
||||||
|
let samples = vec![1.0, 2.0]; // Length 2
|
||||||
|
audio_data.append_samples(&samples, &mut factory).unwrap();
|
||||||
|
|
||||||
|
// Try to set buffer with different length
|
||||||
|
let wrong_length_buffer = vec![10.0, 20.0, 30.0, 40.0].into_boxed_slice(); // Length 4
|
||||||
|
|
||||||
|
let result = audio_data.set_consolidated_buffer(wrong_length_buffer);
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_clear() {
|
||||||
|
let mut factory = MockFactory::new(vec![AudioChunk::allocate(10)]);
|
||||||
|
let mut audio_data = AudioData::new_unconsolidated(&mut factory, 0).unwrap();
|
||||||
|
|
||||||
|
let samples = vec![1.0, 2.0, 3.0];
|
||||||
|
audio_data.append_samples(&samples, &mut factory).unwrap();
|
||||||
|
assert_eq!(audio_data.len(), 3);
|
||||||
|
|
||||||
|
audio_data.clear().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(audio_data.len(), 0);
|
||||||
|
assert!(audio_data.is_empty());
|
||||||
|
|
||||||
|
match audio_data {
|
||||||
|
AudioData::Empty => (), // Expected
|
||||||
|
_ => panic!("Expected Empty variant after clear"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
mod allocator;
|
mod allocator;
|
||||||
mod audio_chunk;
|
mod audio_chunk;
|
||||||
|
mod audio_data;
|
||||||
mod chunk_factory;
|
mod chunk_factory;
|
||||||
mod connection_manager;
|
mod connection_manager;
|
||||||
mod looper_error;
|
mod looper_error;
|
||||||
@ -16,6 +17,7 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use allocator::Allocator;
|
use allocator::Allocator;
|
||||||
use audio_chunk::AudioChunk;
|
use audio_chunk::AudioChunk;
|
||||||
|
use audio_data::AudioData;
|
||||||
use chunk_factory::ChunkFactory;
|
use chunk_factory::ChunkFactory;
|
||||||
use connection_manager::ConnectionManager;
|
use connection_manager::ConnectionManager;
|
||||||
use looper_error::LooperError;
|
use looper_error::LooperError;
|
||||||
|
|||||||
@ -21,6 +21,10 @@ impl Metronome {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn samples_per_beat(&self) -> u32 {
|
||||||
|
self.samples_per_beat
|
||||||
|
}
|
||||||
|
|
||||||
/// Process audio for current buffer, writing to output slice
|
/// Process audio for current buffer, writing to output slice
|
||||||
/// Returns the sample index where a beat occurred, if any
|
/// Returns the sample index where a beat occurred, if any
|
||||||
pub fn process(&mut self, ps: &jack::ProcessScope, ports: &mut JackPorts) -> Result<Option<u32>> {
|
pub fn process(&mut self, ps: &jack::ProcessScope, ports: &mut JackPorts) -> Result<Option<u32>> {
|
||||||
|
|||||||
@ -45,6 +45,10 @@ pub fn process_events<F: ChunkFactory>(
|
|||||||
// Button 2: Play/Mute
|
// Button 2: Play/Mute
|
||||||
process_handler.play_toggle()?;
|
process_handler.play_toggle()?;
|
||||||
}
|
}
|
||||||
|
22 => {
|
||||||
|
// Button 3: Auto-stop record
|
||||||
|
process_handler.record_auto_stop()?;
|
||||||
|
}
|
||||||
24 => {
|
24 => {
|
||||||
// Button 5: Clear track
|
// Button 5: Clear track
|
||||||
process_handler.clear_track()?;
|
process_handler.clear_track()?;
|
||||||
|
|||||||
@ -1,17 +1,18 @@
|
|||||||
use crate::*;
|
use crate::*;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
/// Request to process a recorded chunk chain
|
/// Request to process a recorded chunk chain with sync offset
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct PostRecordRequest {
|
pub struct PostRecordRequest {
|
||||||
pub chunk_chain: Arc<AudioChunk>,
|
pub chunk_chain: Arc<AudioChunk>,
|
||||||
|
pub sync_offset: u32,
|
||||||
pub sample_rate: u32,
|
pub sample_rate: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Response containing the consolidated chunk
|
/// Response containing the consolidated buffer
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct PostRecordResponse {
|
pub struct PostRecordResponse {
|
||||||
pub consolidated_chunk: Arc<AudioChunk>,
|
pub consolidated_buffer: Box<[f32]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// RT-side interface for post-record operations
|
/// RT-side interface for post-record operations
|
||||||
@ -23,8 +24,8 @@ pub struct PostRecordController {
|
|||||||
|
|
||||||
impl PostRecordController {
|
impl PostRecordController {
|
||||||
/// Send a post-record processing request (RT-safe)
|
/// Send a post-record processing request (RT-safe)
|
||||||
pub fn send_request(&self, chunk_chain: Arc<AudioChunk>, sample_rate: u32) -> Result<()> {
|
pub fn send_request(&self, chunk_chain: Arc<AudioChunk>, sync_offset: u32, sample_rate: u32) -> Result<()> {
|
||||||
let request = PostRecordRequest { chunk_chain, sample_rate };
|
let request = PostRecordRequest { chunk_chain, sync_offset, sample_rate };
|
||||||
|
|
||||||
match self.request_sender.try_send(request) {
|
match self.request_sender.try_send(request) {
|
||||||
Ok(true) => Ok(()), // Successfully sent
|
Ok(true) => Ok(()), // Successfully sent
|
||||||
@ -42,7 +43,7 @@ impl PostRecordController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handles post-record processing: consolidation and saving
|
/// Handles post-record processing: consolidation with sync offset and saving
|
||||||
pub struct PostRecordHandler {
|
pub struct PostRecordHandler {
|
||||||
request_receiver: kanal::AsyncReceiver<PostRecordRequest>,
|
request_receiver: kanal::AsyncReceiver<PostRecordRequest>,
|
||||||
response_sender: kanal::AsyncSender<PostRecordResponse>,
|
response_sender: kanal::AsyncSender<PostRecordResponse>,
|
||||||
@ -89,24 +90,27 @@ impl PostRecordHandler {
|
|||||||
&self,
|
&self,
|
||||||
request: PostRecordRequest,
|
request: PostRecordRequest,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
log::debug!("Processing post-record request for {} samples",
|
log::debug!("Processing post-record request for {} samples with sync_offset {}",
|
||||||
request.chunk_chain.len());
|
request.chunk_chain.len(), request.sync_offset);
|
||||||
|
|
||||||
// Step 1: Consolidate chunk chain (CPU intensive)
|
// Step 1: Consolidate and reorder chunk chain based on sync offset
|
||||||
let consolidated_chunk = AudioChunk::consolidate(&request.chunk_chain);
|
let consolidated_buffer = self.consolidate_with_sync_offset(
|
||||||
|
&request.chunk_chain,
|
||||||
|
request.sync_offset as usize
|
||||||
|
)?;
|
||||||
|
|
||||||
log::debug!("Consolidated {} samples", consolidated_chunk.sample_count);
|
log::debug!("Consolidated and reordered {} samples", consolidated_buffer.len());
|
||||||
|
|
||||||
// Step 2: Send consolidated chunk back to RT thread immediately
|
// Step 2: Send consolidated buffer back to RT thread immediately
|
||||||
let response = PostRecordResponse {
|
let response = PostRecordResponse { consolidated_buffer };
|
||||||
consolidated_chunk: consolidated_chunk.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Err(_) = self.response_sender.send(response).await {
|
if let Err(_) = self.response_sender.send(response).await {
|
||||||
log::warn!("Failed to send consolidated chunk to RT thread");
|
log::warn!("Failed to send consolidated buffer to RT thread");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 3: Save WAV file in background (I/O intensive)
|
// Step 3: Save WAV file in background (I/O intensive)
|
||||||
|
// Use original chunk chain for saving (not reordered)
|
||||||
|
let consolidated_chunk = AudioChunk::consolidate(&request.chunk_chain);
|
||||||
let file_path = self.get_file_path();
|
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 {
|
||||||
@ -117,6 +121,36 @@ impl PostRecordHandler {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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]>> {
|
||||||
|
let total_length = chunk_chain.len();
|
||||||
|
|
||||||
|
if total_length == 0 {
|
||||||
|
return Ok(Vec::new().into_boxed_slice());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 1: Extract all samples from the chunk chain into a temporary buffer
|
||||||
|
let mut all_samples = vec![0.0; total_length];
|
||||||
|
chunk_chain.copy_samples(&mut all_samples, 0)?;
|
||||||
|
|
||||||
|
// Step 2: Reorder samples based on sync offset
|
||||||
|
// The goal is to put the sample that represents "logical beat 1" at index 0
|
||||||
|
if sync_offset == 0 {
|
||||||
|
// No offset - return samples as-is
|
||||||
|
return Ok(all_samples.into_boxed_slice());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut reordered = Vec::with_capacity(total_length);
|
||||||
|
|
||||||
|
// For each output position, calculate which input position to take from
|
||||||
|
for i in 0..total_length {
|
||||||
|
let source_index = (i + sync_offset) % total_length;
|
||||||
|
reordered.push(all_samples[source_index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(reordered.into_boxed_slice())
|
||||||
|
}
|
||||||
|
|
||||||
/// Save consolidated chunk as WAV file
|
/// Save consolidated chunk as WAV file
|
||||||
async fn save_wav_file(
|
async fn save_wav_file(
|
||||||
&self,
|
&self,
|
||||||
|
|||||||
@ -1,5 +1,9 @@
|
|||||||
use crate::*;
|
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
|
||||||
|
|
||||||
pub struct ProcessHandler<F: ChunkFactory> {
|
pub struct ProcessHandler<F: ChunkFactory> {
|
||||||
track: Track,
|
track: Track,
|
||||||
playback_position: usize,
|
playback_position: usize,
|
||||||
@ -12,13 +16,13 @@ pub struct ProcessHandler<F: ChunkFactory> {
|
|||||||
impl<F: ChunkFactory> ProcessHandler<F> {
|
impl<F: ChunkFactory> ProcessHandler<F> {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
ports: JackPorts,
|
ports: JackPorts,
|
||||||
mut chunk_factory: F,
|
chunk_factory: F,
|
||||||
beep_samples: Arc<AudioChunk>,
|
beep_samples: Arc<AudioChunk>,
|
||||||
state: &State,
|
state: &State,
|
||||||
post_record_controller: PostRecordController,
|
post_record_controller: PostRecordController,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
track: Track::new(&mut chunk_factory)?,
|
track: Track::new(),
|
||||||
playback_position: 0,
|
playback_position: 0,
|
||||||
ports,
|
ports,
|
||||||
chunk_factory,
|
chunk_factory,
|
||||||
@ -39,6 +43,16 @@ impl<F: ChunkFactory> ProcessHandler<F> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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 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);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Handle clear button (Button 5)
|
/// Handle clear button (Button 5)
|
||||||
pub fn clear_track(&mut self) -> Result<()> {
|
pub fn clear_track(&mut self) -> Result<()> {
|
||||||
self.track.queue_clear();
|
self.track.queue_clear();
|
||||||
@ -97,26 +111,18 @@ impl<F: ChunkFactory> jack::ProcessHandler for ProcessHandler<F> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<F: ChunkFactory> ProcessHandler<F> {
|
impl<F: ChunkFactory> ProcessHandler<F> {
|
||||||
/// Handle post-record processing: send requests and swap chunks
|
/// 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 chunk chain for processing if track indicates consolidation needed
|
// Send audio data for processing if track indicates consolidation needed
|
||||||
if should_consolidate {
|
if should_consolidate {
|
||||||
let chunk_chain = self.track.get_audio_chunk();
|
if let Some((chunk_chain, sync_offset)) = self.track.get_audio_data_for_processing() {
|
||||||
log::debug!("Sending chunk chain with {} samples for post-record processing",
|
self.post_record_controller.send_request(chunk_chain, sync_offset as u32, sample_rate)?;
|
||||||
chunk_chain.len());
|
|
||||||
|
|
||||||
if let Err(e) = self.post_record_controller.send_request(chunk_chain, sample_rate) {
|
|
||||||
log::warn!("Failed to send post-record request: {}", e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for consolidation response
|
// Check for consolidation response
|
||||||
if let Some(response) = self.post_record_controller.try_recv_response() {
|
if let Some(response) = self.post_record_controller.try_recv_response() {
|
||||||
log::debug!("Received consolidated chunk with {} samples, swapping in track",
|
self.track.set_consolidated_buffer(response.consolidated_buffer)?;
|
||||||
response.consolidated_chunk.len());
|
|
||||||
|
|
||||||
// Swap the consolidated chunk into the track
|
|
||||||
self.track.set_audio_chunk(response.consolidated_chunk)?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -154,7 +160,7 @@ impl<F: ChunkFactory> ProcessHandler<F> {
|
|||||||
let state_after = self.track.next_state(); // Use next_state since transition hasn't happened yet
|
let state_after = self.track.next_state(); // Use next_state since transition hasn't happened yet
|
||||||
|
|
||||||
match (state_before, state_after) {
|
match (state_before, state_after) {
|
||||||
(_, TrackState::Playing) if *state_before != TrackState::Playing => {
|
(_, TrackState::Playing) if !matches!(state_before, TrackState::Playing) => {
|
||||||
// Just started playing - start from beginning
|
// Just started playing - start from beginning
|
||||||
// Note: In future Column implementation, this will be:
|
// Note: In future Column implementation, this will be:
|
||||||
// column.get_sync_position() to sync with other playing tracks
|
// column.get_sync_position() to sync with other playing tracks
|
||||||
@ -197,7 +203,7 @@ impl<F: ChunkFactory> ProcessHandler<F> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if state transition at beat affects position
|
// Check if state transition at beat affects position
|
||||||
if state_after == TrackState::Playing && *state_before != TrackState::Playing {
|
if state_after == TrackState::Playing && !matches!(state_before, TrackState::Playing) {
|
||||||
// Started playing at beat - reset position to post-beat calculation
|
// Started playing at beat - reset position to post-beat calculation
|
||||||
self.playback_position = self.calculate_post_beat_position(state_before);
|
self.playback_position = self.calculate_post_beat_position(state_before);
|
||||||
}
|
}
|
||||||
|
|||||||
191
src/track.rs
191
src/track.rs
@ -5,7 +5,11 @@ pub enum TrackState {
|
|||||||
Empty, // No audio data (---)
|
Empty, // No audio data (---)
|
||||||
Idle, // Has data, not playing (READY)
|
Idle, // Has data, not playing (READY)
|
||||||
Playing, // Currently playing (PLAY)
|
Playing, // Currently playing (PLAY)
|
||||||
Recording, // Currently recording (REC)
|
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
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@ -21,22 +25,20 @@ pub enum TrackTiming {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct Track {
|
pub struct Track {
|
||||||
audio_chunk: Arc<AudioChunk>,
|
audio_data: AudioData,
|
||||||
length: usize,
|
|
||||||
current_state: TrackState,
|
current_state: TrackState,
|
||||||
next_state: TrackState,
|
next_state: TrackState,
|
||||||
volume: f32,
|
volume: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Track {
|
impl Track {
|
||||||
pub fn new<F: ChunkFactory>(chunk_factory: &mut F) -> Result<Self> {
|
pub fn new() -> Self {
|
||||||
Ok(Self {
|
Self {
|
||||||
audio_chunk: chunk_factory.create_chunk()?,
|
audio_data: AudioData::new_empty(),
|
||||||
length: 0,
|
|
||||||
current_state: TrackState::Empty,
|
current_state: TrackState::Empty,
|
||||||
next_state: TrackState::Empty,
|
next_state: TrackState::Empty,
|
||||||
volume: 1.0,
|
volume: 1.0,
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Main audio processing method called from ProcessHandler
|
/// Main audio processing method called from ProcessHandler
|
||||||
@ -119,25 +121,51 @@ impl Track {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
match self.current_state {
|
match &mut self.current_state {
|
||||||
TrackState::Empty | TrackState::Idle => {
|
TrackState::Empty | TrackState::Idle => {
|
||||||
// Output silence for this range
|
// Output silence for this range
|
||||||
output_buffer[start_index..end_index].fill(0.0);
|
output_buffer[start_index..end_index].fill(0.0);
|
||||||
}
|
}
|
||||||
TrackState::Recording => {
|
TrackState::Recording => {
|
||||||
// Record input samples
|
// Record input samples (manual recording)
|
||||||
let samples_to_record = &input_buffer[start_index..end_index];
|
let samples_to_record = &input_buffer[start_index..end_index];
|
||||||
self.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);
|
||||||
|
}
|
||||||
|
TrackState::RecordingAutoStop { target_samples, .. } => {
|
||||||
|
// Record input samples with auto-stop logic
|
||||||
|
let samples_to_record = &input_buffer[start_index..end_index];
|
||||||
|
let current_length = self.audio_data.len();
|
||||||
|
|
||||||
|
if current_length < *target_samples {
|
||||||
|
// Still recording - determine how many samples to actually record
|
||||||
|
let samples_needed = *target_samples - current_length;
|
||||||
|
let samples_to_append = samples_to_record.len().min(samples_needed);
|
||||||
|
|
||||||
|
if samples_to_append > 0 {
|
||||||
|
self.audio_data.append_samples(
|
||||||
|
&samples_to_record[..samples_to_append],
|
||||||
|
chunk_factory
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we've reached target and should auto-transition
|
||||||
|
if self.audio_data.len() >= *target_samples {
|
||||||
|
self.next_state = TrackState::Playing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Output silence during recording
|
// Output silence during recording
|
||||||
output_buffer[start_index..end_index].fill(0.0);
|
output_buffer[start_index..end_index].fill(0.0);
|
||||||
}
|
}
|
||||||
TrackState::Playing => {
|
TrackState::Playing => {
|
||||||
// Playback with looping
|
// Playback with looping
|
||||||
self.copy_samples_to_output(
|
self.audio_data.copy_samples_to_output(
|
||||||
&mut output_buffer[start_index..end_index],
|
&mut output_buffer[start_index..end_index],
|
||||||
sample_count,
|
|
||||||
playback_position,
|
playback_position,
|
||||||
|
self.volume,
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -151,28 +179,34 @@ impl Track {
|
|||||||
&mut self,
|
&mut self,
|
||||||
chunk_factory: &mut F
|
chunk_factory: &mut F
|
||||||
) -> Result<bool> {
|
) -> Result<bool> {
|
||||||
// Check if this is a Recording → Playing transition (consolidation trigger)
|
// Check if this is a recording → playing transition (consolidation trigger)
|
||||||
let should_consolidate = self.current_state == TrackState::Recording
|
let should_consolidate = matches!(
|
||||||
&& self.next_state == TrackState::Playing;
|
(&self.current_state, &self.next_state),
|
||||||
|
(TrackState::Recording, TrackState::Playing) |
|
||||||
|
(TrackState::RecordingAutoStop { .. }, TrackState::Playing)
|
||||||
|
);
|
||||||
|
|
||||||
// Handle transitions that require setup
|
// Handle transitions that require setup
|
||||||
match (&self.current_state, &self.next_state) {
|
match (&self.current_state, &self.next_state) {
|
||||||
(current_state, TrackState::Recording) if *current_state != TrackState::Recording => {
|
(current_state, TrackState::Recording) if !matches!(current_state, TrackState::Recording) => {
|
||||||
// Starting to record - clear previous data and create new chunk
|
// Starting manual recording - clear previous data and create new unconsolidated data
|
||||||
self.clear_audio_data(chunk_factory)?;
|
self.audio_data = AudioData::new_unconsolidated(chunk_factory, 0)?;
|
||||||
|
}
|
||||||
|
(current_state, TrackState::RecordingAutoStop { sync_offset, .. })
|
||||||
|
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)?;
|
||||||
}
|
}
|
||||||
(_, TrackState::Playing) => {
|
(_, TrackState::Playing) => {
|
||||||
// Starting playback - check if we have audio data
|
// Starting playback - check if we have audio data
|
||||||
if self.length == 0 {
|
if self.audio_data.is_empty() {
|
||||||
// No audio data - transition to Idle instead
|
// No audio data - transition to Idle instead
|
||||||
self.next_state = TrackState::Idle;
|
self.next_state = TrackState::Idle;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(_, TrackState::Empty) => {
|
(_, TrackState::Empty) => {
|
||||||
// Clear operation - remove audio data
|
// Clear operation - remove audio data
|
||||||
// Note: Actual deallocation will happen later via IO thread
|
self.audio_data.clear()?;
|
||||||
self.audio_chunk = chunk_factory.create_chunk()?;
|
|
||||||
self.length = 0;
|
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
// Other transitions don't require special handling
|
// Other transitions don't require special handling
|
||||||
@ -185,80 +219,14 @@ impl Track {
|
|||||||
Ok(should_consolidate)
|
Ok(should_consolidate)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Copy samples from track audio to output buffer with looping
|
/// Get audio data for post-record processing (returns chunk and sync offset)
|
||||||
fn copy_samples_to_output(
|
pub fn get_audio_data_for_processing(&self) -> Option<(Arc<AudioChunk>, usize)> {
|
||||||
&self,
|
self.audio_data.get_chunk_for_processing()
|
||||||
output_slice: &mut [f32],
|
|
||||||
sample_count: usize,
|
|
||||||
mut playback_position: usize,
|
|
||||||
) -> Result<()> {
|
|
||||||
if self.length == 0 {
|
|
||||||
output_slice.fill(0.0);
|
|
||||||
return Ok(());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut samples_written = 0;
|
/// Set consolidated buffer (for swapping in consolidated audio data)
|
||||||
|
pub fn set_consolidated_buffer(&mut self, buffer: Box<[f32]>) -> Result<()> {
|
||||||
while samples_written < sample_count {
|
self.audio_data.set_consolidated_buffer(buffer)
|
||||||
let samples_remaining = sample_count - samples_written;
|
|
||||||
let samples_until_loop = self.length - playback_position;
|
|
||||||
let samples_to_copy = samples_remaining.min(samples_until_loop);
|
|
||||||
|
|
||||||
// Copy directly from audio chunk to output slice
|
|
||||||
self.audio_chunk.copy_samples(
|
|
||||||
&mut output_slice[samples_written..samples_written + samples_to_copy],
|
|
||||||
playback_position,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// Apply volume scaling in-place
|
|
||||||
for sample in &mut output_slice[samples_written..samples_written + samples_to_copy] {
|
|
||||||
*sample *= self.volume;
|
|
||||||
}
|
|
||||||
|
|
||||||
samples_written += samples_to_copy;
|
|
||||||
playback_position += samples_to_copy;
|
|
||||||
|
|
||||||
// Handle looping
|
|
||||||
if playback_position >= self.length {
|
|
||||||
playback_position = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Append samples to the track's audio data
|
|
||||||
pub fn append_samples<F: ChunkFactory>(
|
|
||||||
&mut self,
|
|
||||||
samples: &[f32],
|
|
||||||
chunk_factory: &mut F,
|
|
||||||
) -> Result<()> {
|
|
||||||
self.audio_chunk.append_samples(samples, chunk_factory)?;
|
|
||||||
self.length += samples.len();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clear all audio data from the track
|
|
||||||
fn clear_audio_data<F: ChunkFactory>(
|
|
||||||
&mut self,
|
|
||||||
chunk_factory: &mut F,
|
|
||||||
) -> Result<()> {
|
|
||||||
self.audio_chunk = chunk_factory.create_chunk()?;
|
|
||||||
self.length = 0;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get audio chunk for post-record processing (read-only access)
|
|
||||||
pub fn get_audio_chunk(&self) -> Arc<AudioChunk> {
|
|
||||||
self.audio_chunk.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set audio chunk (for swapping in consolidated chunk)
|
|
||||||
pub fn set_audio_chunk(&mut self, chunk: Arc<AudioChunk>) -> Result<()> {
|
|
||||||
if chunk.len() != self.length {
|
|
||||||
return Err(LooperError::OutOfBounds(std::panic::Location::caller()));
|
|
||||||
}
|
|
||||||
self.audio_chunk = chunk;
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Public accessors and commands for MIDI handling
|
// Public accessors and commands for MIDI handling
|
||||||
@ -271,7 +239,7 @@ impl Track {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn len(&self) -> usize {
|
pub fn len(&self) -> usize {
|
||||||
self.length
|
self.audio_data.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn volume(&self) -> f32 {
|
pub fn volume(&self) -> f32 {
|
||||||
@ -291,12 +259,37 @@ impl Track {
|
|||||||
TrackState::Recording => {
|
TrackState::Recording => {
|
||||||
self.next_state = TrackState::Playing;
|
self.next_state = TrackState::Playing;
|
||||||
}
|
}
|
||||||
|
TrackState::RecordingAutoStop { .. } => {
|
||||||
|
// Auto-stop recording - can't manually stop, wait for auto-transition
|
||||||
|
self.next_state = self.current_state.clone();
|
||||||
|
}
|
||||||
TrackState::Playing => {
|
TrackState::Playing => {
|
||||||
self.next_state = TrackState::Idle;
|
self.next_state = TrackState::Idle;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handle auto-stop record command (sets next_state)
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
TrackState::Recording => {
|
||||||
|
// Switch from manual to auto-stop recording
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
TrackState::Playing => {
|
||||||
|
// Stop playing and start auto-recording
|
||||||
|
self.next_state = TrackState::RecordingAutoStop { target_samples, sync_offset };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Handle play/mute toggle command (sets next_state)
|
/// Handle play/mute toggle command (sets next_state)
|
||||||
pub fn queue_play_toggle(&mut self) {
|
pub fn queue_play_toggle(&mut self) {
|
||||||
match self.current_state {
|
match self.current_state {
|
||||||
@ -305,15 +298,15 @@ impl Track {
|
|||||||
self.next_state = TrackState::Empty;
|
self.next_state = TrackState::Empty;
|
||||||
}
|
}
|
||||||
TrackState::Idle => {
|
TrackState::Idle => {
|
||||||
if self.length > 0 {
|
if !self.audio_data.is_empty() {
|
||||||
self.next_state = TrackState::Playing;
|
self.next_state = TrackState::Playing;
|
||||||
} else {
|
} else {
|
||||||
self.next_state = TrackState::Idle;
|
self.next_state = TrackState::Idle;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
TrackState::Recording => {
|
TrackState::Recording | TrackState::RecordingAutoStop { .. } => {
|
||||||
// Don't change state while recording
|
// Don't change state while recording
|
||||||
self.next_state = TrackState::Recording;
|
self.next_state = self.current_state.clone();
|
||||||
}
|
}
|
||||||
TrackState::Playing => {
|
TrackState::Playing => {
|
||||||
self.next_state = TrackState::Idle;
|
self.next_state = TrackState::Idle;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user