Basic midi processing
This commit is contained in:
parent
292087be3e
commit
7eec4f0093
7
Cargo.lock
generated
7
Cargo.lock
generated
@ -184,6 +184,7 @@ dependencies = [
|
|||||||
"simple_logger",
|
"simple_logger",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"wmidi",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -647,3 +648,9 @@ name = "windows_x86_64_msvc"
|
|||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wmidi"
|
||||||
|
version = "4.0.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4e55f35b40ad0178422d06e9ba845041baf2faf04627b91fde928d0f6a21c712"
|
||||||
|
|||||||
21
Cargo.toml
21
Cargo.toml
@ -3,10 +3,21 @@ name = "looper"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
|
default-run = "looper"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "looper"
|
||||||
|
path = "src/audio_engine.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "sendmidi"
|
||||||
|
path = "src/sendmidi.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
thiserror = "1.0"
|
thiserror = "1"
|
||||||
jack = "0.13"
|
jack = "0.13"
|
||||||
tokio = { version = "1.0", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
log = "0.4.27"
|
log = "0.4"
|
||||||
simple_logger = "5.0.0"
|
simple_logger = "5"
|
||||||
kanal = "0.1.1"
|
kanal = "0.1"
|
||||||
|
wmidi = "4"
|
||||||
|
|||||||
@ -362,4 +362,385 @@ mod tests {
|
|||||||
// Clean up the clone reference to avoid unused variable warning
|
// Clean up the clone reference to avoid unused variable warning
|
||||||
drop(chunk_clone);
|
drop(chunk_clone);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_copy_samples_single_chunk_full_copy() {
|
||||||
|
let chunk = Arc::new(AudioChunk {
|
||||||
|
samples: vec![1.0, 2.0, 3.0, 4.0, 5.0].into_boxed_slice(),
|
||||||
|
sample_count: 5,
|
||||||
|
next: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut dest = vec![0.0; 5];
|
||||||
|
let result = chunk.copy_samples(&mut dest, 0);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
assert_eq!(dest, vec![1.0, 2.0, 3.0, 4.0, 5.0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_copy_samples_single_chunk_partial_copy() {
|
||||||
|
let chunk = Arc::new(AudioChunk {
|
||||||
|
samples: vec![1.0, 2.0, 3.0, 4.0, 5.0].into_boxed_slice(),
|
||||||
|
sample_count: 5,
|
||||||
|
next: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut dest = vec![0.0; 3];
|
||||||
|
let result = chunk.copy_samples(&mut dest, 1); // Start at index 1
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
assert_eq!(dest, vec![2.0, 3.0, 4.0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_copy_samples_single_chunk_zero_samples() {
|
||||||
|
let chunk = Arc::new(AudioChunk {
|
||||||
|
samples: vec![1.0, 2.0, 3.0].into_boxed_slice(),
|
||||||
|
sample_count: 3,
|
||||||
|
next: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut dest: Vec<f32> = vec![];
|
||||||
|
let result = chunk.copy_samples(&mut dest, 0);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
assert_eq!(dest, Vec::<f32>::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_copy_samples_single_chunk_out_of_bounds() {
|
||||||
|
let chunk = Arc::new(AudioChunk {
|
||||||
|
samples: vec![1.0, 2.0, 3.0].into_boxed_slice(),
|
||||||
|
sample_count: 3,
|
||||||
|
next: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut dest = vec![0.0; 2];
|
||||||
|
let result = chunk.copy_samples(&mut dest, 5); // Start beyond sample_count
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(matches!(result.unwrap_err(), LooperError::OutOfBounds(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_copy_samples_single_chunk_partial_out_of_bounds() {
|
||||||
|
let chunk = Arc::new(AudioChunk {
|
||||||
|
samples: vec![1.0, 2.0, 3.0].into_boxed_slice(),
|
||||||
|
sample_count: 3,
|
||||||
|
next: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut dest = vec![0.0; 5]; // Want 5 samples starting at index 1, but only 2 available
|
||||||
|
let result = chunk.copy_samples(&mut dest, 1);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(matches!(result.unwrap_err(), LooperError::OutOfBounds(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_copy_samples_linked_chunks_single_chunk_boundary() {
|
||||||
|
let chunk1 = Arc::new(AudioChunk {
|
||||||
|
samples: vec![1.0, 2.0, 3.0].into_boxed_slice(),
|
||||||
|
sample_count: 3,
|
||||||
|
next: Some(Arc::new(AudioChunk {
|
||||||
|
samples: vec![4.0, 5.0, 6.0].into_boxed_slice(),
|
||||||
|
sample_count: 3,
|
||||||
|
next: None,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut dest = vec![0.0; 3];
|
||||||
|
let result = chunk1.copy_samples(&mut dest, 3); // Start exactly at chunk boundary
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
assert_eq!(dest, vec![4.0, 5.0, 6.0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_copy_samples_linked_chunks_across_boundary() {
|
||||||
|
let chunk1 = Arc::new(AudioChunk {
|
||||||
|
samples: vec![1.0, 2.0, 3.0].into_boxed_slice(),
|
||||||
|
sample_count: 3,
|
||||||
|
next: Some(Arc::new(AudioChunk {
|
||||||
|
samples: vec![4.0, 5.0, 6.0].into_boxed_slice(),
|
||||||
|
sample_count: 3,
|
||||||
|
next: None,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut dest = vec![0.0; 4];
|
||||||
|
let result = chunk1.copy_samples(&mut dest, 2); // Start in first chunk, cross to second
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
assert_eq!(dest, vec![3.0, 4.0, 5.0, 6.0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_copy_samples_linked_chunks_multiple_spans() {
|
||||||
|
let chunk1 = Arc::new(AudioChunk {
|
||||||
|
samples: vec![1.0, 2.0].into_boxed_slice(),
|
||||||
|
sample_count: 2,
|
||||||
|
next: Some(Arc::new(AudioChunk {
|
||||||
|
samples: vec![3.0, 4.0].into_boxed_slice(),
|
||||||
|
sample_count: 2,
|
||||||
|
next: Some(Arc::new(AudioChunk {
|
||||||
|
samples: vec![5.0, 6.0, 7.0].into_boxed_slice(),
|
||||||
|
sample_count: 3,
|
||||||
|
next: None,
|
||||||
|
})),
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut dest = vec![0.0; 5];
|
||||||
|
let result = chunk1.copy_samples(&mut dest, 1); // Start in first, span all three chunks
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
assert_eq!(dest, vec![2.0, 3.0, 4.0, 5.0, 6.0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_copy_samples_linked_chunks_out_of_bounds() {
|
||||||
|
let chunk1 = Arc::new(AudioChunk {
|
||||||
|
samples: vec![1.0, 2.0, 3.0].into_boxed_slice(),
|
||||||
|
sample_count: 3,
|
||||||
|
next: Some(Arc::new(AudioChunk {
|
||||||
|
samples: vec![4.0, 5.0].into_boxed_slice(),
|
||||||
|
sample_count: 2,
|
||||||
|
next: None,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut dest = vec![0.0; 3];
|
||||||
|
let result = chunk1.copy_samples(&mut dest, 10); // Start beyond all chunks
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(matches!(result.unwrap_err(), LooperError::OutOfBounds(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_append_samples_to_partially_filled_chunk() {
|
||||||
|
let mut chunk = Arc::new(AudioChunk {
|
||||||
|
samples: vec![1.0, 2.0, 0.0, 0.0, 0.0].into_boxed_slice(),
|
||||||
|
sample_count: 2, // Already has 2 samples
|
||||||
|
next: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut factory = || panic!("Factory should not be called");
|
||||||
|
let samples = vec![3.0, 4.0];
|
||||||
|
let result = AudioChunk::append_samples(&mut chunk, &samples, &mut factory);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
assert_eq!(chunk.sample_count, 4);
|
||||||
|
assert_eq!(chunk.samples[0], 1.0);
|
||||||
|
assert_eq!(chunk.samples[1], 2.0);
|
||||||
|
assert_eq!(chunk.samples[2], 3.0);
|
||||||
|
assert_eq!(chunk.samples[3], 4.0);
|
||||||
|
assert!(chunk.next.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_append_samples_to_full_chunk_creates_new() {
|
||||||
|
let mut chunk = Arc::new(AudioChunk {
|
||||||
|
samples: vec![1.0, 2.0, 3.0].into_boxed_slice(),
|
||||||
|
sample_count: 3, // Chunk is full
|
||||||
|
next: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut factory = chunk_factory::mock::MockFactory::new(vec![AudioChunk::allocate(3)]);
|
||||||
|
let samples = vec![4.0, 5.0];
|
||||||
|
let result = AudioChunk::append_samples(&mut chunk, &samples, &mut factory);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
assert_eq!(chunk.sample_count, 3); // First chunk unchanged
|
||||||
|
|
||||||
|
let chunk2 = chunk.next.as_ref().unwrap();
|
||||||
|
assert_eq!(chunk2.sample_count, 2);
|
||||||
|
assert_eq!(chunk2.samples[0], 4.0);
|
||||||
|
assert_eq!(chunk2.samples[1], 5.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_append_samples_to_middle_of_chain() {
|
||||||
|
// Create a chain: chunk1 -> chunk2 -> chunk3
|
||||||
|
let chunk3 = Arc::new(AudioChunk {
|
||||||
|
samples: vec![7.0, 8.0, 0.0].into_boxed_slice(),
|
||||||
|
sample_count: 2,
|
||||||
|
next: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
let chunk2 = Arc::new(AudioChunk {
|
||||||
|
samples: vec![4.0, 5.0, 6.0].into_boxed_slice(),
|
||||||
|
sample_count: 3,
|
||||||
|
next: Some(chunk3),
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut chunk1 = Arc::new(AudioChunk {
|
||||||
|
samples: vec![1.0, 2.0, 3.0].into_boxed_slice(),
|
||||||
|
sample_count: 3,
|
||||||
|
next: Some(chunk2),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Append should go to the last chunk (chunk3)
|
||||||
|
let mut factory = || panic!("Factory should not be called when space available");
|
||||||
|
let samples = vec![9.0];
|
||||||
|
let result = AudioChunk::append_samples(&mut chunk1, &samples, &mut factory);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
|
||||||
|
// Navigate to chunk3 and verify the sample was added
|
||||||
|
let chunk2 = chunk1.next.as_ref().unwrap();
|
||||||
|
let chunk3 = chunk2.next.as_ref().unwrap();
|
||||||
|
assert_eq!(chunk3.sample_count, 3);
|
||||||
|
assert_eq!(chunk3.samples[2], 9.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_append_samples_multiple_calls() {
|
||||||
|
let mut chunk = AudioChunk::allocate(5);
|
||||||
|
|
||||||
|
let mut factory = || panic!("Factory should not be called");
|
||||||
|
|
||||||
|
// First append
|
||||||
|
let result1 = AudioChunk::append_samples(&mut chunk, &[1.0, 2.0], &mut factory);
|
||||||
|
assert!(result1.is_ok());
|
||||||
|
assert_eq!(chunk.sample_count, 2);
|
||||||
|
|
||||||
|
// Second append
|
||||||
|
let result2 = AudioChunk::append_samples(&mut chunk, &[3.0], &mut factory);
|
||||||
|
assert!(result2.is_ok());
|
||||||
|
assert_eq!(chunk.sample_count, 3);
|
||||||
|
|
||||||
|
// Third append
|
||||||
|
let result3 = AudioChunk::append_samples(&mut chunk, &[4.0, 5.0], &mut factory);
|
||||||
|
assert!(result3.is_ok());
|
||||||
|
assert_eq!(chunk.sample_count, 5);
|
||||||
|
|
||||||
|
// Verify all samples
|
||||||
|
assert_eq!(chunk.samples[0], 1.0);
|
||||||
|
assert_eq!(chunk.samples[1], 2.0);
|
||||||
|
assert_eq!(chunk.samples[2], 3.0);
|
||||||
|
assert_eq!(chunk.samples[3], 4.0);
|
||||||
|
assert_eq!(chunk.samples[4], 5.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_append_samples_chain_with_overflow() {
|
||||||
|
// Create initial chain with some space in last chunk
|
||||||
|
let chunk2 = Arc::new(AudioChunk {
|
||||||
|
samples: vec![4.0, 5.0, 0.0].into_boxed_slice(),
|
||||||
|
sample_count: 2, // Has space for 1 more
|
||||||
|
next: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut chunk1 = Arc::new(AudioChunk {
|
||||||
|
samples: vec![1.0, 2.0, 3.0].into_boxed_slice(),
|
||||||
|
sample_count: 3,
|
||||||
|
next: Some(chunk2),
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut factory = chunk_factory::mock::MockFactory::new(vec![AudioChunk::allocate(3)]);
|
||||||
|
let samples = vec![6.0, 7.0, 8.0]; // 3 samples, but only 1 space available
|
||||||
|
let result = AudioChunk::append_samples(&mut chunk1, &samples, &mut factory);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
|
||||||
|
// Check that chunk2 got filled
|
||||||
|
let chunk2 = chunk1.next.as_ref().unwrap();
|
||||||
|
assert_eq!(chunk2.sample_count, 3);
|
||||||
|
assert_eq!(chunk2.samples[2], 6.0);
|
||||||
|
|
||||||
|
// Check that chunk3 was created with remaining samples
|
||||||
|
let chunk3 = chunk2.next.as_ref().unwrap();
|
||||||
|
assert_eq!(chunk3.sample_count, 2);
|
||||||
|
assert_eq!(chunk3.samples[0], 7.0);
|
||||||
|
assert_eq!(chunk3.samples[1], 8.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_consolidate_empty_chunk() {
|
||||||
|
let chunk = Arc::new(AudioChunk {
|
||||||
|
samples: vec![0.0, 0.0, 0.0].into_boxed_slice(),
|
||||||
|
sample_count: 0, // No valid samples
|
||||||
|
next: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
let consolidated = AudioChunk::consolidate(&chunk);
|
||||||
|
|
||||||
|
assert_eq!(consolidated.samples.len(), 0);
|
||||||
|
assert_eq!(consolidated.sample_count, 0);
|
||||||
|
assert!(consolidated.next.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_consolidate_chain_with_empty_chunks() {
|
||||||
|
let chunk3 = Arc::new(AudioChunk {
|
||||||
|
samples: vec![4.0, 5.0, 0.0].into_boxed_slice(),
|
||||||
|
sample_count: 2,
|
||||||
|
next: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
let chunk2 = Arc::new(AudioChunk {
|
||||||
|
samples: vec![0.0, 0.0].into_boxed_slice(),
|
||||||
|
sample_count: 0, // Empty middle chunk
|
||||||
|
next: Some(chunk3),
|
||||||
|
});
|
||||||
|
|
||||||
|
let chunk1 = Arc::new(AudioChunk {
|
||||||
|
samples: vec![1.0, 2.0, 3.0].into_boxed_slice(),
|
||||||
|
sample_count: 3,
|
||||||
|
next: Some(chunk2),
|
||||||
|
});
|
||||||
|
|
||||||
|
let consolidated = AudioChunk::consolidate(&chunk1);
|
||||||
|
|
||||||
|
assert_eq!(consolidated.samples.len(), 5);
|
||||||
|
assert_eq!(consolidated.sample_count, 5);
|
||||||
|
assert_eq!(consolidated.samples.as_ref(), &[1.0, 2.0, 3.0, 4.0, 5.0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_consolidate_all_empty_chunks() {
|
||||||
|
let chunk2 = Arc::new(AudioChunk {
|
||||||
|
samples: vec![0.0, 0.0].into_boxed_slice(),
|
||||||
|
sample_count: 0,
|
||||||
|
next: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
let chunk1 = Arc::new(AudioChunk {
|
||||||
|
samples: vec![0.0, 0.0, 0.0].into_boxed_slice(),
|
||||||
|
sample_count: 0,
|
||||||
|
next: Some(chunk2),
|
||||||
|
});
|
||||||
|
|
||||||
|
let consolidated = AudioChunk::consolidate(&chunk1);
|
||||||
|
|
||||||
|
assert_eq!(consolidated.samples.len(), 0);
|
||||||
|
assert_eq!(consolidated.sample_count, 0);
|
||||||
|
assert!(consolidated.next.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_copy_samples_from_consolidated_chunk() {
|
||||||
|
// Test that copy_samples works correctly after consolidation
|
||||||
|
let chunk2 = Arc::new(AudioChunk {
|
||||||
|
samples: vec![4.0, 5.0].into_boxed_slice(),
|
||||||
|
sample_count: 2,
|
||||||
|
next: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
let chunk1 = Arc::new(AudioChunk {
|
||||||
|
samples: vec![1.0, 2.0, 3.0].into_boxed_slice(),
|
||||||
|
sample_count: 3,
|
||||||
|
next: Some(chunk2),
|
||||||
|
});
|
||||||
|
|
||||||
|
let consolidated = AudioChunk::consolidate(&chunk1);
|
||||||
|
|
||||||
|
let mut dest = vec![0.0; 3];
|
||||||
|
let result = consolidated.copy_samples(&mut dest, 2);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
assert_eq!(dest, vec![3.0, 4.0, 5.0]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ mod allocator;
|
|||||||
mod audio_chunk;
|
mod audio_chunk;
|
||||||
mod chunk_factory;
|
mod chunk_factory;
|
||||||
mod looper_error;
|
mod looper_error;
|
||||||
|
mod midi;
|
||||||
mod notification_handler;
|
mod notification_handler;
|
||||||
mod process_handler;
|
mod process_handler;
|
||||||
mod track;
|
mod track;
|
||||||
@ -17,16 +18,23 @@ use notification_handler::JackNotification;
|
|||||||
use notification_handler::NotificationHandler;
|
use notification_handler::NotificationHandler;
|
||||||
use process_handler::ProcessHandler;
|
use process_handler::ProcessHandler;
|
||||||
use track::Track;
|
use track::Track;
|
||||||
|
use track::TrackState;
|
||||||
|
|
||||||
|
pub struct JackPorts {
|
||||||
|
pub audio_in: jack::Port<jack::AudioIn>,
|
||||||
|
pub audio_out: jack::Port<jack::AudioOut>,
|
||||||
|
pub midi_in: jack::Port<jack::MidiIn>,
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
simple_logger::SimpleLogger::new().init().expect("Could not initialize logger");
|
simple_logger::SimpleLogger::new().init().expect("Could not initialize logger");
|
||||||
|
|
||||||
let (jack_client, audio_in, audio_out) = setup_jack();
|
let (jack_client, ports) = setup_jack();
|
||||||
|
|
||||||
let (allocator, factory_handle) = Allocator::spawn(jack_client.sample_rate(), 3);
|
let (allocator, factory_handle) = Allocator::spawn(jack_client.sample_rate(), 3);
|
||||||
|
|
||||||
let process_handler = ProcessHandler::new(audio_in, audio_out, allocator)
|
let process_handler = ProcessHandler::new(ports, allocator)
|
||||||
.expect("Could not create process handler");
|
.expect("Could not create process handler");
|
||||||
|
|
||||||
let notification_handler = NotificationHandler::new();
|
let notification_handler = NotificationHandler::new();
|
||||||
@ -56,7 +64,7 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setup_jack() -> (jack::Client, jack::Port<jack::AudioIn>, jack::Port<jack::AudioOut>) {
|
fn setup_jack() -> (jack::Client, JackPorts) {
|
||||||
let (jack_client, jack_status) =
|
let (jack_client, jack_status) =
|
||||||
jack::Client::new("looper", jack::ClientOptions::NO_START_SERVER)
|
jack::Client::new("looper", jack::ClientOptions::NO_START_SERVER)
|
||||||
.expect("Could not create Jack client");
|
.expect("Could not create Jack client");
|
||||||
@ -69,5 +77,15 @@ fn setup_jack() -> (jack::Client, jack::Port<jack::AudioIn>, jack::Port<jack::Au
|
|||||||
let audio_out = jack_client
|
let audio_out = jack_client
|
||||||
.register_port("audio_out", jack::AudioOut::default())
|
.register_port("audio_out", jack::AudioOut::default())
|
||||||
.expect("Could not create audio_out port");
|
.expect("Could not create audio_out port");
|
||||||
(jack_client, audio_in, audio_out)
|
let midi_in = jack_client
|
||||||
|
.register_port("midi_in", jack::MidiIn::default())
|
||||||
|
.expect("Could not create midi_in port");
|
||||||
|
|
||||||
|
let ports = JackPorts {
|
||||||
|
audio_in,
|
||||||
|
audio_out,
|
||||||
|
midi_in,
|
||||||
|
};
|
||||||
|
|
||||||
|
(jack_client, ports)
|
||||||
}
|
}
|
||||||
67
src/midi.rs
Normal file
67
src/midi.rs
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
use crate::*;
|
||||||
|
|
||||||
|
/// Process MIDI events
|
||||||
|
pub fn process_events<F: ChunkFactory>(
|
||||||
|
process_handler: &mut ProcessHandler<F>,
|
||||||
|
ps: &jack::ProcessScope,
|
||||||
|
) -> Result<()> {
|
||||||
|
// First, collect all MIDI events into a fixed-size array
|
||||||
|
// This avoids allocations while solving borrow checker issues
|
||||||
|
const MAX_EVENTS: usize = 16; // Reasonable limit for real-time processing
|
||||||
|
let mut raw_events = [[0u8; 3]; MAX_EVENTS];
|
||||||
|
let mut event_count = 0;
|
||||||
|
|
||||||
|
// Collect events from the MIDI input iterator
|
||||||
|
let midi_input = process_handler.ports.midi_in.iter(ps);
|
||||||
|
for midi_event in midi_input {
|
||||||
|
if event_count < MAX_EVENTS && midi_event.bytes.len() >= 3 {
|
||||||
|
raw_events[event_count][0] = midi_event.bytes[0];
|
||||||
|
raw_events[event_count][1] = midi_event.bytes[1];
|
||||||
|
raw_events[event_count][2] = midi_event.bytes[2];
|
||||||
|
event_count += 1;
|
||||||
|
} else {
|
||||||
|
return Err(LooperError::OutOfBounds(std::panic::Location::caller()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now process the collected events using wmidi
|
||||||
|
// The iterator borrow is dropped, so we can mutably borrow process_handler
|
||||||
|
for i in 0..event_count {
|
||||||
|
let event_bytes = &raw_events[i];
|
||||||
|
|
||||||
|
// Use wmidi for cleaner MIDI parsing instead of manual byte checking
|
||||||
|
match wmidi::MidiMessage::try_from(&event_bytes[..]) {
|
||||||
|
Ok(message) => {
|
||||||
|
match message {
|
||||||
|
wmidi::MidiMessage::ControlChange(_, controller, value) => {
|
||||||
|
// Only process button presses (value > 0)
|
||||||
|
if u8::from(value) > 0 {
|
||||||
|
match u8::from(controller) {
|
||||||
|
20 => {
|
||||||
|
// Button 1: Record/Play toggle
|
||||||
|
process_handler.record_toggle()?;
|
||||||
|
}
|
||||||
|
21 => {
|
||||||
|
// Button 2: Play/Mute
|
||||||
|
process_handler.play_toggle()?;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// Other CC messages - ignore for now
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// Ignore other MIDI messages for now
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
// Skip malformed MIDI messages instead of panicking
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@ -3,79 +3,118 @@ use crate::*;
|
|||||||
pub struct ProcessHandler<F: ChunkFactory> {
|
pub struct ProcessHandler<F: ChunkFactory> {
|
||||||
track: Track,
|
track: Track,
|
||||||
playback_position: usize,
|
playback_position: usize,
|
||||||
input_port: jack::Port<jack::AudioIn>,
|
pub ports: JackPorts,
|
||||||
output_port: jack::Port<jack::AudioOut>,
|
|
||||||
chunk_factory: F,
|
chunk_factory: F,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<F: ChunkFactory> ProcessHandler<F> {
|
impl<F: ChunkFactory> ProcessHandler<F> {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
input_port: jack::Port<jack::AudioIn>,
|
ports: JackPorts,
|
||||||
output_port: jack::Port<jack::AudioOut>,
|
|
||||||
mut chunk_factory: F,
|
mut chunk_factory: F,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
track: Track::new(&mut chunk_factory)?,
|
track: Track::new(&mut chunk_factory)?,
|
||||||
playback_position: 0,
|
playback_position: 0,
|
||||||
input_port,
|
ports,
|
||||||
output_port,
|
|
||||||
chunk_factory,
|
chunk_factory,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handle record/play toggle button (Button 1)
|
||||||
|
pub fn record_toggle(&mut self) -> Result<()> {
|
||||||
|
match self.track.state() {
|
||||||
|
TrackState::Idle => {
|
||||||
|
// Clear previous recording and start new recording
|
||||||
|
self.track.clear(&mut self.chunk_factory)?;
|
||||||
|
self.playback_position = 0;
|
||||||
|
self.track.set_state(TrackState::Recording);
|
||||||
|
}
|
||||||
|
TrackState::Recording => {
|
||||||
|
self.track.set_state(TrackState::Playing);
|
||||||
|
self.playback_position = 0;
|
||||||
|
}
|
||||||
|
TrackState::Playing => {
|
||||||
|
self.track.set_state(TrackState::Idle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle play/mute toggle button (Button 2)
|
||||||
|
pub fn play_toggle(&mut self) -> Result<()> {
|
||||||
|
match self.track.state() {
|
||||||
|
TrackState::Idle | TrackState::Recording => {
|
||||||
|
if self.track.len() > 0 {
|
||||||
|
self.track.set_state(TrackState::Playing);
|
||||||
|
self.playback_position = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TrackState::Playing => {
|
||||||
|
self.track.set_state(TrackState::Idle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<F: ChunkFactory> jack::ProcessHandler for ProcessHandler<F> {
|
impl<F: ChunkFactory> jack::ProcessHandler for ProcessHandler<F> {
|
||||||
fn process(&mut self, client: &jack::Client, ps: &jack::ProcessScope) -> jack::Control {
|
fn process(&mut self, client: &jack::Client, ps: &jack::ProcessScope) -> jack::Control {
|
||||||
let input_buffer = self.input_port.as_slice(ps);
|
// Process MIDI first
|
||||||
let output_buffer = self.output_port.as_mut_slice(ps);
|
if midi::process_events(self, ps).is_err() {
|
||||||
|
return jack::Control::Quit;
|
||||||
|
}
|
||||||
|
|
||||||
|
let input_buffer = self.ports.audio_in.as_slice(ps);
|
||||||
|
let output_buffer = self.ports.audio_out.as_mut_slice(ps);
|
||||||
|
|
||||||
let jack_buffer_size = client.buffer_size() as usize;
|
let jack_buffer_size = client.buffer_size() as usize;
|
||||||
let recording_samples = (client.sample_rate() as f64 * 0.6) as usize; // 0.6 seconds
|
|
||||||
|
|
||||||
let mut index = 0;
|
let mut index = 0;
|
||||||
|
|
||||||
while index < jack_buffer_size {
|
while index < jack_buffer_size {
|
||||||
if self.track.len() < recording_samples {
|
match self.track.state() {
|
||||||
// recording
|
TrackState::Recording => {
|
||||||
let sample_count_to_append = jack_buffer_size - index;
|
// Recording - continue until manually stopped
|
||||||
let sample_count_to_append =
|
let sample_count_to_append = jack_buffer_size - index;
|
||||||
sample_count_to_append.min(recording_samples - self.track.len());
|
let samples_to_append = &input_buffer[index..index + sample_count_to_append];
|
||||||
let samples_to_append = &input_buffer[index..index + sample_count_to_append];
|
if self
|
||||||
if let Err(e) = self
|
.track
|
||||||
.track
|
.append_samples(samples_to_append, &mut self.chunk_factory)
|
||||||
.append_samples(samples_to_append, &mut self.chunk_factory)
|
.is_err()
|
||||||
{
|
{
|
||||||
log::error!("Could not append samples: {e}");
|
return jack::Control::Quit;
|
||||||
return jack::Control::Quit;
|
};
|
||||||
};
|
output_buffer[index..(index + sample_count_to_append)].fill(0.0);
|
||||||
output_buffer[index..(index + sample_count_to_append)].fill(0.0);
|
index += sample_count_to_append;
|
||||||
index += sample_count_to_append;
|
|
||||||
} else {
|
|
||||||
// playback
|
|
||||||
let sample_count_to_play = jack_buffer_size - index;
|
|
||||||
let sample_count_to_play =
|
|
||||||
sample_count_to_play.min(self.track.len() - self.playback_position);
|
|
||||||
if let Err(e) = self
|
|
||||||
.track
|
|
||||||
.copy_samples(
|
|
||||||
&mut output_buffer[index..(index + sample_count_to_play)],
|
|
||||||
self.playback_position,
|
|
||||||
)
|
|
||||||
{
|
|
||||||
log::error!("Could not copy samples: {e}");
|
|
||||||
return jack::Control::Quit;
|
|
||||||
}
|
}
|
||||||
index += sample_count_to_play;
|
TrackState::Playing => {
|
||||||
self.playback_position += sample_count_to_play;
|
// Playback
|
||||||
if self.playback_position >= self.track.len() {
|
let sample_count_to_play = jack_buffer_size - index;
|
||||||
self.playback_position = 0;
|
let sample_count_to_play =
|
||||||
if let Err(e) = self.track.clear(&mut self.chunk_factory) {
|
sample_count_to_play.min(self.track.len() - self.playback_position);
|
||||||
log::error!("Could not clear track: {e}");
|
if self
|
||||||
|
.track
|
||||||
|
.copy_samples(
|
||||||
|
&mut output_buffer[index..(index + sample_count_to_play)],
|
||||||
|
self.playback_position,
|
||||||
|
)
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
return jack::Control::Quit;
|
return jack::Control::Quit;
|
||||||
}
|
}
|
||||||
|
index += sample_count_to_play;
|
||||||
|
self.playback_position += sample_count_to_play;
|
||||||
|
if self.playback_position >= self.track.len() {
|
||||||
|
self.playback_position = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TrackState::Idle => {
|
||||||
|
// Idle - output silence
|
||||||
|
output_buffer[index..jack_buffer_size].fill(0.0);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
jack::Control::Continue
|
jack::Control::Continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
194
src/sendmidi.rs
Normal file
194
src/sendmidi.rs
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
use std::io::{self, Write};
|
||||||
|
use jack::RawMidi;
|
||||||
|
use kanal::{Receiver, Sender};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum MidiCommand {
|
||||||
|
ControlChange { cc: u8, value: u8 },
|
||||||
|
Quit,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MidiSender {
|
||||||
|
midi_out: jack::Port<jack::MidiOut>,
|
||||||
|
command_receiver: Receiver<MidiCommand>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MidiSender {
|
||||||
|
fn new(client: &jack::Client, command_receiver: Receiver<MidiCommand>) -> Result<Self, Box<dyn std::error::Error>> {
|
||||||
|
let midi_out = client
|
||||||
|
.register_port("midi_out", jack::MidiOut::default())
|
||||||
|
.map_err(|e| format!("Could not create MIDI output port: {}", e))?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
midi_out,
|
||||||
|
command_receiver,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl jack::ProcessHandler for MidiSender {
|
||||||
|
fn process(&mut self, _client: &jack::Client, ps: &jack::ProcessScope) -> jack::Control {
|
||||||
|
let mut midi_writer = self.midi_out.writer(ps);
|
||||||
|
|
||||||
|
// Process all pending commands
|
||||||
|
while let Ok(Some(command)) = self.command_receiver.try_recv() {
|
||||||
|
match command {
|
||||||
|
MidiCommand::ControlChange { cc, value } => {
|
||||||
|
let midi_data = [0xB0, cc, value]; // Control Change on channel 1
|
||||||
|
if let Err(e) = midi_writer.write(&RawMidi { time: 0, bytes: &midi_data }) {
|
||||||
|
eprintln!("Failed to send MIDI: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MidiCommand::Quit => return jack::Control::Quit,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
jack::Control::Continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_help() {
|
||||||
|
println!("\nFCB1010 MIDI Simulator (Rust Edition)");
|
||||||
|
println!("=====================================");
|
||||||
|
println!("Button mappings:");
|
||||||
|
println!("1 - Button 1 (CC 20): Record/Arm / Tap Tempo");
|
||||||
|
println!("2 - Button 2 (CC 21): Play/Mute Track / Click Toggle");
|
||||||
|
println!("3 - Button 3 (CC 22): Solo Track");
|
||||||
|
println!("4 - Button 4 (CC 23): Overdub");
|
||||||
|
println!("5 - Button 5 (CC 24): Clear Track / Clear Column");
|
||||||
|
println!("6 - Button 6 (CC 25): Column 1 Select");
|
||||||
|
println!("7 - Button 7 (CC 26): Column 2 Select");
|
||||||
|
println!("8 - Button 8 (CC 27): Column 3 Select");
|
||||||
|
println!("9 - Button 9 (CC 28): Column 4 Select");
|
||||||
|
println!("0 - Button 10 (CC 29): Column 5 Select");
|
||||||
|
println!("u - UP (CC 30): Row Up / Mode Switch");
|
||||||
|
println!("d - DOWN (CC 31): Row Down / Mode Switch");
|
||||||
|
println!("e - Expression A (CC 1): Track/Click Volume (0-127)");
|
||||||
|
println!("m - Master Volume (CC 7): Master Volume (0-127)");
|
||||||
|
println!("h - Show this help");
|
||||||
|
println!("q - Quit\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn auto_connect(active_client: &jack::AsyncClient<(), MidiSender>) {
|
||||||
|
// Try to connect our MIDI output to the looper's MIDI input
|
||||||
|
let our_port = "sendmidi:midi_out";
|
||||||
|
let target_port = "looper:midi_in";
|
||||||
|
|
||||||
|
match active_client.as_client().connect_ports_by_name(our_port, target_port) {
|
||||||
|
Ok(_) => println!("✓ Auto-connected to looper:midi_in"),
|
||||||
|
Err(e) => println!("⚠ Could not auto-connect to looper:midi_in: {}\n Make sure the looper is running, then connect manually.", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_expression_value(prompt: &str) -> Result<u8, Box<dyn std::error::Error>> {
|
||||||
|
print!("{}", prompt);
|
||||||
|
io::stdout().flush()?;
|
||||||
|
|
||||||
|
let mut input = String::new();
|
||||||
|
io::stdin().read_line(&mut input)?;
|
||||||
|
|
||||||
|
let value: u8 = input.trim().parse()
|
||||||
|
.map_err(|_| "Invalid number. Please enter 0-127")?;
|
||||||
|
|
||||||
|
if value > 127 {
|
||||||
|
return Err("Value must be 0-127".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
println!("Starting FCB1010 MIDI Simulator...");
|
||||||
|
|
||||||
|
// Create JACK client
|
||||||
|
let (client, status) = jack::Client::new("sendmidi", jack::ClientOptions::NO_START_SERVER)?;
|
||||||
|
if !status.is_empty() {
|
||||||
|
eprintln!("JACK client status: {:?}", status);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create command channel
|
||||||
|
let (command_sender, command_receiver): (Sender<MidiCommand>, Receiver<MidiCommand>) = kanal::bounded(32);
|
||||||
|
|
||||||
|
// Create MIDI sender
|
||||||
|
let midi_sender = MidiSender::new(&client, command_receiver)?;
|
||||||
|
|
||||||
|
// Activate client
|
||||||
|
let active_client = client.activate_async((), midi_sender)?;
|
||||||
|
|
||||||
|
// Auto-connect after a brief delay to let JACK settle
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||||
|
auto_connect(&active_client);
|
||||||
|
|
||||||
|
show_help();
|
||||||
|
|
||||||
|
// Main input loop
|
||||||
|
loop {
|
||||||
|
print!("Command (h for help): ");
|
||||||
|
io::stdout().flush()?;
|
||||||
|
|
||||||
|
let mut input = String::new();
|
||||||
|
io::stdin().read_line(&mut input)?;
|
||||||
|
let input = input.trim().to_lowercase();
|
||||||
|
|
||||||
|
if input.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let first_char = input.chars().next().unwrap();
|
||||||
|
|
||||||
|
let command = match first_char {
|
||||||
|
'1' => Some(MidiCommand::ControlChange { cc: 20, value: 127 }),
|
||||||
|
'2' => Some(MidiCommand::ControlChange { cc: 21, value: 127 }),
|
||||||
|
'3' => Some(MidiCommand::ControlChange { cc: 22, value: 127 }),
|
||||||
|
'4' => Some(MidiCommand::ControlChange { cc: 23, value: 127 }),
|
||||||
|
'5' => Some(MidiCommand::ControlChange { cc: 24, value: 127 }),
|
||||||
|
'6' => Some(MidiCommand::ControlChange { cc: 25, value: 127 }),
|
||||||
|
'7' => Some(MidiCommand::ControlChange { cc: 26, value: 127 }),
|
||||||
|
'8' => Some(MidiCommand::ControlChange { cc: 27, value: 127 }),
|
||||||
|
'9' => Some(MidiCommand::ControlChange { cc: 28, value: 127 }),
|
||||||
|
'0' => Some(MidiCommand::ControlChange { cc: 29, value: 127 }),
|
||||||
|
'u' => Some(MidiCommand::ControlChange { cc: 30, value: 127 }),
|
||||||
|
'd' => Some(MidiCommand::ControlChange { cc: 31, value: 127 }),
|
||||||
|
'e' => {
|
||||||
|
match get_expression_value("Expression A value (0-127): ") {
|
||||||
|
Ok(value) => Some(MidiCommand::ControlChange { cc: 1, value }),
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Error: {}", e);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'm' => {
|
||||||
|
match get_expression_value("Master volume value (0-127): ") {
|
||||||
|
Ok(value) => Some(MidiCommand::ControlChange { cc: 7, value }),
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Error: {}", e);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'h' => {
|
||||||
|
show_help();
|
||||||
|
None
|
||||||
|
},
|
||||||
|
'q' => {
|
||||||
|
println!("Exiting...");
|
||||||
|
command_sender.send(MidiCommand::Quit)?;
|
||||||
|
break;
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
println!("Unknown command '{}'. Press 'h' for help.", first_char);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(cmd) = command {
|
||||||
|
if let MidiCommand::ControlChange { cc, value } = &cmd {
|
||||||
|
println!("Sending CC {} with value {}", cc, value);
|
||||||
|
}
|
||||||
|
command_sender.send(cmd)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
20
src/track.rs
20
src/track.rs
@ -1,8 +1,16 @@
|
|||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum TrackState {
|
||||||
|
Idle,
|
||||||
|
Playing,
|
||||||
|
Recording,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Track {
|
pub struct Track {
|
||||||
audio_chunks: Arc<AudioChunk>,
|
audio_chunks: Arc<AudioChunk>,
|
||||||
length: usize,
|
length: usize,
|
||||||
|
state: TrackState,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Track {
|
impl Track {
|
||||||
@ -10,6 +18,7 @@ impl Track {
|
|||||||
Ok(Self {
|
Ok(Self {
|
||||||
audio_chunks: chunk_factory.create_chunk()?,
|
audio_chunks: chunk_factory.create_chunk()?,
|
||||||
length: 0,
|
length: 0,
|
||||||
|
state: TrackState::Idle,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,10 +39,19 @@ impl Track {
|
|||||||
pub fn clear<F: ChunkFactory>(&mut self, chunk_factory: &mut F) -> Result<()> {
|
pub fn clear<F: ChunkFactory>(&mut self, chunk_factory: &mut F) -> Result<()> {
|
||||||
self.audio_chunks = chunk_factory.create_chunk()?;
|
self.audio_chunks = chunk_factory.create_chunk()?;
|
||||||
self.length = 0;
|
self.length = 0;
|
||||||
|
self.state = TrackState::Idle;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn copy_samples(&self, dest: &mut [f32], start: usize) -> Result<()> {
|
pub fn copy_samples(&self, dest: &mut [f32], start: usize) -> Result<()> {
|
||||||
self.audio_chunks.copy_samples(dest, start)
|
self.audio_chunks.copy_samples(dest, start)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
pub fn state(&self) -> &TrackState {
|
||||||
|
&self.state
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_state(&mut self, state: TrackState) {
|
||||||
|
self.state = state;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user