Basic midi processing

This commit is contained in:
Geens 2025-06-05 19:37:35 +02:00
parent 292087be3e
commit 7eec4f0093
8 changed files with 790 additions and 55 deletions

7
Cargo.lock generated
View File

@ -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"

View File

@ -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"

View File

@ -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]);
}
} }

View File

@ -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
View 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(())
}

View File

@ -3,76 +3,115 @@ 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;
} }
} }
} }

194
src/sendmidi.rs Normal file
View 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(())
}

View File

@ -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;
}
} }