diff --git a/project_spec.md b/project_spec.md index 127f425..59372ed 100644 --- a/project_spec.md +++ b/project_spec.md @@ -9,7 +9,7 @@ The system provides a 5x5 matrix of loop tracks organized in columns and rows, w - **Behringer FCB1010**: MIDI foot controller with 10 footswitches (1-10), UP/DOWN buttons, and 2 expression pedals - **Behringer UMC404HD**: Audio interface for input/output -- **Raspberry Pi**: Running Linux +- **Single Board Computer**: Running Linux - **TFT Display**: 215x135mm color display, no touchscreen - **Audio**: JACK audio connect kit diff --git a/src/audio_engine.rs b/src/audio_engine.rs index 257788e..d77b54a 100644 --- a/src/audio_engine.rs +++ b/src/audio_engine.rs @@ -3,6 +3,7 @@ mod audio_chunk; mod chunk_factory; mod connection_manager; mod looper_error; +mod metronome; mod midi; mod notification_handler; mod process_handler; @@ -18,6 +19,7 @@ use chunk_factory::ChunkFactory; use connection_manager::ConnectionManager; use looper_error::LooperError; use looper_error::Result; +use metronome::generate_beep; use notification_handler::JackNotification; use notification_handler::NotificationHandler; use process_handler::ProcessHandler; @@ -29,6 +31,7 @@ use track::TrackState; pub struct JackPorts { pub audio_in: jack::Port, pub audio_out: jack::Port, + pub click_track_out: jack::Port, pub midi_in: jack::Port, } @@ -40,10 +43,13 @@ async fn main() { let (jack_client, ports) = setup_jack(); - let allocator = Allocator::spawn(jack_client.sample_rate(), 3); + let mut allocator = Allocator::spawn(jack_client.sample_rate(), 3); + + let beep_samples = generate_beep(jack_client.sample_rate() as u32, &mut allocator) + .expect("Could not generate beep samples"); let process_handler = - ProcessHandler::new(ports, allocator).expect("Could not create process handler"); + ProcessHandler::new(ports, allocator, beep_samples).expect("Could not create process handler"); let notification_handler = NotificationHandler::new(); let mut notification_channel = notification_handler.subscribe(); @@ -102,6 +108,9 @@ fn setup_jack() -> (jack::Client, JackPorts) { let audio_out = jack_client .register_port("audio_out", jack::AudioOut::default()) .expect("Could not create audio_out port"); + let click_track_out = jack_client + .register_port("click_track", jack::AudioOut::default()) + .expect("Could not create click_track_out port"); let midi_in = jack_client .register_port("midi_in", jack::MidiIn::default()) .expect("Could not create midi_in port"); @@ -109,6 +118,7 @@ fn setup_jack() -> (jack::Client, JackPorts) { let ports = JackPorts { audio_in, audio_out, + click_track_out, midi_in, }; diff --git a/src/metronome.rs b/src/metronome.rs new file mode 100644 index 0000000..efd61cf --- /dev/null +++ b/src/metronome.rs @@ -0,0 +1,85 @@ +use crate::*; +use std::f32::consts::TAU; + +pub struct Metronome { + beep_samples: Arc, + is_playing: bool, + playback_position: usize, +} + +impl Metronome { + pub fn new(beep_samples: Arc) -> Self { + Self { + beep_samples, + is_playing: false, + playback_position: 0, + } + } + + /// Trigger a single beep + pub fn trigger_beep(&mut self) { + self.is_playing = true; + self.playback_position = 0; + } + + /// Process audio for current buffer, writing to output slice + pub fn process_audio(&mut self, output: &mut [f32]) -> Result<()> { + // Clear output buffer first + for sample in output.iter_mut() { + *sample = 0.0; + } + + // If not playing, output silence + if !self.is_playing { + return Ok(()); + } + + // Copy samples from beep_samples to output + let beep_length = self.beep_samples.as_ref().sample_count; + let output_length = output.len(); + + let mut output_index = 0; + while output_index < output_length && self.playback_position < beep_length { + // Copy one sample at a time + let mut temp_buffer = [0.0f32; 1]; + self.beep_samples.copy_samples(&mut temp_buffer, self.playback_position)?; + output[output_index] = temp_buffer[0]; + + output_index += 1; + self.playback_position += 1; + } + + // Stop playing if we've reached the end of the beep + if self.playback_position >= beep_length { + self.is_playing = false; + } + // TODO: Update playback_position + // TODO: Stop when beep is complete + + Ok(()) + } +} + +/// Generate a 100ms sine wave beep at 1000Hz +pub fn generate_beep( + sample_rate: u32, + chunk_factory: &mut F, +) -> Result> { + const FREQUENCY_HZ: f32 = 1000.0; + const DURATION_MS: f32 = 100.0; + + let sample_count = ((sample_rate as f32) * (DURATION_MS / 1000.0)) as usize; + let mut samples = Vec::with_capacity(sample_count); + + for i in 0..sample_count { + let t = i as f32 / sample_rate as f32; + let sample = (TAU * FREQUENCY_HZ * t).sin(); + samples.push(sample); + } + + // Create AudioChunk and fill it with samples + let mut chunk = chunk_factory.create_chunk()?; + chunk.append_samples(&samples, chunk_factory)?; + + Ok(chunk) +} diff --git a/src/process_handler.rs b/src/process_handler.rs index d85513f..9203dba 100644 --- a/src/process_handler.rs +++ b/src/process_handler.rs @@ -1,19 +1,22 @@ use crate::*; +use crate::metronome::Metronome; pub struct ProcessHandler { track: Track, playback_position: usize, pub ports: JackPorts, chunk_factory: F, + metronome: Metronome, } impl ProcessHandler { - pub fn new(ports: JackPorts, mut chunk_factory: F) -> Result { + pub fn new(ports: JackPorts, mut chunk_factory: F, beep_samples: Arc) -> Result { Ok(Self { track: Track::new(&mut chunk_factory)?, playback_position: 0, ports, chunk_factory, + metronome: Metronome::new(beep_samples), }) } @@ -39,6 +42,9 @@ impl ProcessHandler { /// Handle play/mute toggle button (Button 2) pub fn play_toggle(&mut self) -> Result<()> { + // Trigger beep for testing + self.metronome.trigger_beep(); + match self.track.state() { TrackState::Idle | TrackState::Recording => { if self.track.len() > 0 { @@ -63,9 +69,15 @@ impl jack::ProcessHandler for ProcessHandler { let input_buffer = self.ports.audio_in.as_slice(ps); let output_buffer = self.ports.audio_out.as_mut_slice(ps); + let click_track_buffer = self.ports.click_track_out.as_mut_slice(ps); let jack_buffer_size = client.buffer_size() as usize; + // Process metronome/click track + if self.metronome.process_audio(click_track_buffer).is_err() { + return jack::Control::Quit; + } + let mut index = 0; while index < jack_buffer_size {