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",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"wmidi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -647,3 +648,9 @@ name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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"
|
||||
edition = "2021"
|
||||
|
||||
default-run = "looper"
|
||||
|
||||
[[bin]]
|
||||
name = "looper"
|
||||
path = "src/audio_engine.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "sendmidi"
|
||||
path = "src/sendmidi.rs"
|
||||
|
||||
[dependencies]
|
||||
thiserror = "1.0"
|
||||
thiserror = "1"
|
||||
jack = "0.13"
|
||||
tokio = { version = "1.0", features = ["full"] }
|
||||
log = "0.4.27"
|
||||
simple_logger = "5.0.0"
|
||||
kanal = "0.1.1"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
log = "0.4"
|
||||
simple_logger = "5"
|
||||
kanal = "0.1"
|
||||
wmidi = "4"
|
||||
|
||||
@ -362,4 +362,385 @@ mod tests {
|
||||
// Clean up the clone reference to avoid unused variable warning
|
||||
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 chunk_factory;
|
||||
mod looper_error;
|
||||
mod midi;
|
||||
mod notification_handler;
|
||||
mod process_handler;
|
||||
mod track;
|
||||
@ -17,16 +18,23 @@ use notification_handler::JackNotification;
|
||||
use notification_handler::NotificationHandler;
|
||||
use process_handler::ProcessHandler;
|
||||
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]
|
||||
async fn main() {
|
||||
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 process_handler = ProcessHandler::new(audio_in, audio_out, allocator)
|
||||
let process_handler = ProcessHandler::new(ports, allocator)
|
||||
.expect("Could not create process handler");
|
||||
|
||||
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) =
|
||||
jack::Client::new("looper", jack::ClientOptions::NO_START_SERVER)
|
||||
.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
|
||||
.register_port("audio_out", jack::AudioOut::default())
|
||||
.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,77 +3,116 @@ use crate::*;
|
||||
pub struct ProcessHandler<F: ChunkFactory> {
|
||||
track: Track,
|
||||
playback_position: usize,
|
||||
input_port: jack::Port<jack::AudioIn>,
|
||||
output_port: jack::Port<jack::AudioOut>,
|
||||
pub ports: JackPorts,
|
||||
chunk_factory: F,
|
||||
}
|
||||
|
||||
impl<F: ChunkFactory> ProcessHandler<F> {
|
||||
pub fn new(
|
||||
input_port: jack::Port<jack::AudioIn>,
|
||||
output_port: jack::Port<jack::AudioOut>,
|
||||
ports: JackPorts,
|
||||
mut chunk_factory: F,
|
||||
) -> Result<Self> {
|
||||
Ok(Self {
|
||||
track: Track::new(&mut chunk_factory)?,
|
||||
playback_position: 0,
|
||||
input_port,
|
||||
output_port,
|
||||
ports,
|
||||
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> {
|
||||
fn process(&mut self, client: &jack::Client, ps: &jack::ProcessScope) -> jack::Control {
|
||||
let input_buffer = self.input_port.as_slice(ps);
|
||||
let output_buffer = self.output_port.as_mut_slice(ps);
|
||||
// Process MIDI first
|
||||
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 recording_samples = (client.sample_rate() as f64 * 0.6) as usize; // 0.6 seconds
|
||||
|
||||
let mut index = 0;
|
||||
|
||||
while index < jack_buffer_size {
|
||||
if self.track.len() < recording_samples {
|
||||
// recording
|
||||
match self.track.state() {
|
||||
TrackState::Recording => {
|
||||
// Recording - continue until manually stopped
|
||||
let sample_count_to_append = jack_buffer_size - index;
|
||||
let sample_count_to_append =
|
||||
sample_count_to_append.min(recording_samples - self.track.len());
|
||||
let samples_to_append = &input_buffer[index..index + sample_count_to_append];
|
||||
if let Err(e) = self
|
||||
if self
|
||||
.track
|
||||
.append_samples(samples_to_append, &mut self.chunk_factory)
|
||||
.is_err()
|
||||
{
|
||||
log::error!("Could not append samples: {e}");
|
||||
return jack::Control::Quit;
|
||||
};
|
||||
output_buffer[index..(index + sample_count_to_append)].fill(0.0);
|
||||
index += sample_count_to_append;
|
||||
} else {
|
||||
// playback
|
||||
}
|
||||
TrackState::Playing => {
|
||||
// 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
|
||||
if self
|
||||
.track
|
||||
.copy_samples(
|
||||
&mut output_buffer[index..(index + sample_count_to_play)],
|
||||
self.playback_position,
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
log::error!("Could not copy samples: {e}");
|
||||
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;
|
||||
if let Err(e) = self.track.clear(&mut self.chunk_factory) {
|
||||
log::error!("Could not clear track: {e}");
|
||||
return jack::Control::Quit;
|
||||
}
|
||||
}
|
||||
TrackState::Idle => {
|
||||
// Idle - output silence
|
||||
output_buffer[index..jack_buffer_size].fill(0.0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
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(())
|
||||
}
|
||||
18
src/track.rs
18
src/track.rs
@ -1,8 +1,16 @@
|
||||
use crate::*;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum TrackState {
|
||||
Idle,
|
||||
Playing,
|
||||
Recording,
|
||||
}
|
||||
|
||||
pub struct Track {
|
||||
audio_chunks: Arc<AudioChunk>,
|
||||
length: usize,
|
||||
state: TrackState,
|
||||
}
|
||||
|
||||
impl Track {
|
||||
@ -10,6 +18,7 @@ impl Track {
|
||||
Ok(Self {
|
||||
audio_chunks: chunk_factory.create_chunk()?,
|
||||
length: 0,
|
||||
state: TrackState::Idle,
|
||||
})
|
||||
}
|
||||
|
||||
@ -30,10 +39,19 @@ impl Track {
|
||||
pub fn clear<F: ChunkFactory>(&mut self, chunk_factory: &mut F) -> Result<()> {
|
||||
self.audio_chunks = chunk_factory.create_chunk()?;
|
||||
self.length = 0;
|
||||
self.state = TrackState::Idle;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn copy_samples(&self, dest: &mut [f32], start: usize) -> Result<()> {
|
||||
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