From 7eec4f00939da05d5a2dcc708e9f88371b3d1f5a Mon Sep 17 00:00:00 2001 From: Geens Date: Thu, 5 Jun 2025 19:37:35 +0200 Subject: [PATCH] Basic midi processing --- Cargo.lock | 7 + Cargo.toml | 21 +- src/audio_chunk.rs | 381 +++++++++++++++++++++++++++++++ src/{main.rs => audio_engine.rs} | 26 ++- src/midi.rs | 67 ++++++ src/process_handler.rs | 129 +++++++---- src/sendmidi.rs | 194 ++++++++++++++++ src/track.rs | 20 +- 8 files changed, 790 insertions(+), 55 deletions(-) rename src/{main.rs => audio_engine.rs} (77%) create mode 100644 src/midi.rs create mode 100644 src/sendmidi.rs diff --git a/Cargo.lock b/Cargo.lock index 2b212a8..558b320 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 56538d0..d191256 100644 --- a/Cargo.toml +++ b/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" diff --git a/src/audio_chunk.rs b/src/audio_chunk.rs index 901288e..e37261f 100644 --- a/src/audio_chunk.rs +++ b/src/audio_chunk.rs @@ -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 = vec![]; + let result = chunk.copy_samples(&mut dest, 0); + + assert!(result.is_ok()); + assert_eq!(dest, Vec::::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]); + } } diff --git a/src/main.rs b/src/audio_engine.rs similarity index 77% rename from src/main.rs rename to src/audio_engine.rs index 6ae5f95..398e3fa 100644 --- a/src/main.rs +++ b/src/audio_engine.rs @@ -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, + pub audio_out: jack::Port, + pub midi_in: jack::Port, +} #[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::Port) { +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::Port( + process_handler: &mut ProcessHandler, + 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(()) +} \ No newline at end of file diff --git a/src/process_handler.rs b/src/process_handler.rs index e2b82d7..9c202df 100644 --- a/src/process_handler.rs +++ b/src/process_handler.rs @@ -3,79 +3,118 @@ use crate::*; pub struct ProcessHandler { track: Track, playback_position: usize, - input_port: jack::Port, - output_port: jack::Port, + pub ports: JackPorts, chunk_factory: F, } impl ProcessHandler { pub fn new( - input_port: jack::Port, - output_port: jack::Port, + ports: JackPorts, mut chunk_factory: F, ) -> Result { 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 jack::ProcessHandler for ProcessHandler { 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 - 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 - .track - .append_samples(samples_to_append, &mut self.chunk_factory) - { - 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 - 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; + match self.track.state() { + TrackState::Recording => { + // Recording - continue until manually stopped + let sample_count_to_append = jack_buffer_size - index; + let samples_to_append = &input_buffer[index..index + sample_count_to_append]; + if self + .track + .append_samples(samples_to_append, &mut self.chunk_factory) + .is_err() + { + return jack::Control::Quit; + }; + output_buffer[index..(index + sample_count_to_append)].fill(0.0); + index += sample_count_to_append; } - 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}"); + 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 self + .track + .copy_samples( + &mut output_buffer[index..(index + sample_count_to_play)], + self.playback_position, + ) + .is_err() + { 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 } -} +} \ No newline at end of file diff --git a/src/sendmidi.rs b/src/sendmidi.rs new file mode 100644 index 0000000..e87869d --- /dev/null +++ b/src/sendmidi.rs @@ -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, + command_receiver: Receiver, +} + +impl MidiSender { + fn new(client: &jack::Client, command_receiver: Receiver) -> Result> { + 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> { + 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> { + 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, Receiver) = 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(()) +} \ No newline at end of file diff --git a/src/track.rs b/src/track.rs index 52d37e9..b4a97d5 100644 --- a/src/track.rs +++ b/src/track.rs @@ -1,8 +1,16 @@ use crate::*; +#[derive(Debug, Clone)] +pub enum TrackState { + Idle, + Playing, + Recording, +} + pub struct Track { audio_chunks: Arc, 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(&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; + } +} \ No newline at end of file