diff --git a/audio_engine/src/args.rs b/audio_engine/src/args.rs index f75767c..1d8ec06 100644 --- a/audio_engine/src/args.rs +++ b/audio_engine/src/args.rs @@ -1,15 +1,20 @@ +use crate::*; use clap::Parser; use std::path::PathBuf; -use crate::*; #[derive(Parser)] #[command(name = "audio_engine")] #[command(version, about = "FCB1010 Looper Pedal Audio Engine")] pub struct Args { /// Path to Unix socket or named pipe name - #[arg(short = 's', long = "socket", env = "SOCKET", default_value = "fcb_looper.sock")] + #[arg( + short = 's', + long = "socket", + env = "SOCKET", + default_value = "fcb_looper.sock" + )] pub socket: String, - + /// Path to configuration dir #[arg(short = 'c', long = "config-path", env = "CONFIG_PATH", default_value = default_config_path())] pub config_file: PathBuf, @@ -30,4 +35,4 @@ fn default_config_path() -> String { path.push(".fcb_looper"); path.push("state.json"); path.to_string_lossy().to_string() -} \ No newline at end of file +} diff --git a/audio_engine/src/column.rs b/audio_engine/src/column.rs index 6da9a55..cc2180a 100644 --- a/audio_engine/src/column.rs +++ b/audio_engine/src/column.rs @@ -38,13 +38,13 @@ impl Column { } pub fn handle_record_button( - &mut self, + &mut self, last_volume_setting: f32, controllers: &TrackControllers, ) -> Result<()> { let len = self.len(); let track = &mut self.tracks[controllers.row()]; - + if track.is_recording() { if len > 0 { track.clear(); @@ -62,10 +62,7 @@ impl Column { Ok(()) } - pub fn handle_play_button( - &mut self, - controllers: &TrackControllers, - ) -> Result<()> { + pub fn handle_play_button(&mut self, controllers: &TrackControllers) -> Result<()> { let track = &mut self.tracks[controllers.row()]; if track.len() > 0 && track.is_idle() { track.play(); @@ -75,10 +72,7 @@ impl Column { Ok(()) } - pub fn handle_clear_button( - &mut self, - controllers: &TrackControllers, - ) -> Result<()> { + pub fn handle_clear_button(&mut self, controllers: &TrackControllers) -> Result<()> { let track = &mut self.tracks[controllers.row()]; track.clear(); Ok(()) @@ -104,7 +98,7 @@ impl Column { ) -> Result<()> { for (row, track) in self.tracks.iter_mut().enumerate() { let track_controllers = controllers.to_track_controllers(row); - + track.handle_xrun( timing.beat_in_missed, timing.missed_frames, @@ -135,10 +129,10 @@ impl Column { } } } - + for (row, track) in self.tracks.iter_mut().enumerate() { let track_controllers = controllers.to_track_controllers(row); - + track.process( self.playback_position, timing.beat_in_buffer, @@ -147,18 +141,19 @@ impl Column { chunk_factory, &track_controllers, )?; - + for (output_val, scratch_pad_val) in output_buffer.iter_mut().zip(scratch_pad.iter()) { *output_val += *scratch_pad_val; } } - + let len = self.len(); if len > 0 { self.playback_position = (self.playback_position + input_buffer.len()) % self.len(); } else { - self.playback_position = (self.playback_position + input_buffer.len()) % self.frames_per_beat; + self.playback_position = + (self.playback_position + input_buffer.len()) % self.frames_per_beat; } Ok(()) } -} \ No newline at end of file +} diff --git a/audio_engine/src/connection_manager.rs b/audio_engine/src/connection_manager.rs index aefc783..555c116 100644 --- a/audio_engine/src/connection_manager.rs +++ b/audio_engine/src/connection_manager.rs @@ -51,6 +51,16 @@ impl ConnectionManager { } } + for external_port in &state.connections.midi_out { + let our_port = format!("{}:midi_out", self.jack_client_name); + let result = self + .jack_client + .connect_ports_by_name(&our_port, external_port); + if let Ok(_) = result { + log::info!("Connected {} -> {}", our_port, external_port); + } + } + for external_port in &state.connections.audio_in { let our_port = format!("{}:audio_in", self.jack_client_name); let result = self diff --git a/audio_engine/src/controllers.rs b/audio_engine/src/controllers.rs index 8694ae3..4709853 100644 --- a/audio_engine/src/controllers.rs +++ b/audio_engine/src/controllers.rs @@ -86,14 +86,13 @@ impl<'a> TrackControllers<'a> { } } - pub fn send_post_record_request(&self, chunk: Arc, sync_offset: usize) -> Result<()> { - self.post_record.send_request( - self.column, - self.row, - chunk, - sync_offset, - self.sample_rate, - ) + pub fn send_post_record_request( + &self, + chunk: Arc, + sync_offset: usize, + ) -> Result<()> { + self.post_record + .send_request(self.column, self.row, chunk, sync_offset, self.sample_rate) } pub fn send_state_update(&self, state: TrackState) -> Result<()> { @@ -105,7 +104,8 @@ impl<'a> TrackControllers<'a> { } pub fn update_track_volume(&self, volume: f32) -> Result<()> { - self.persistence.update_track_volume(self.column, self.row, volume) + self.persistence + .update_track_volume(self.column, self.row, volume) } pub fn column(&self) -> usize { @@ -115,4 +115,4 @@ impl<'a> TrackControllers<'a> { pub fn row(&self) -> usize { self.row } -} \ No newline at end of file +} diff --git a/audio_engine/src/looper_error.rs b/audio_engine/src/looper_error.rs index 90b8464..600e667 100644 --- a/audio_engine/src/looper_error.rs +++ b/audio_engine/src/looper_error.rs @@ -26,6 +26,9 @@ pub enum LooperError { #[error("Irrecoverable XRUN")] Xrun(&'static std::panic::Location<'static>), + + #[error("Failed to write midi message")] + Midi(&'static std::panic::Location<'static>), } pub type Result = std::result::Result; diff --git a/audio_engine/src/main.rs b/audio_engine/src/main.rs index 6e384db..7aad8bd 100644 --- a/audio_engine/src/main.rs +++ b/audio_engine/src/main.rs @@ -38,8 +38,8 @@ use metronome::BufferTiming; use metronome::Metronome; use notification_handler::JackNotification; use notification_handler::NotificationHandler; -use osc_server::OscController; use osc_server::Osc; +use osc_server::OscController; use persistence_manager::PersistenceManager; use persistence_manager::PersistenceManagerController; use post_record_handler::PostRecordController; @@ -56,6 +56,7 @@ pub struct JackPorts { pub audio_out: jack::Port, pub click_track_out: jack::Port, pub midi_in: jack::Port, + pub midi_out: jack::Port, } #[tokio::main] @@ -65,7 +66,9 @@ async fn main() { .init() .expect("Could not initialize logger"); - let (mut osc, osc_controller) = Osc::new(&args.socket).await.expect("Could not create OSC server"); + let (mut osc, osc_controller) = Osc::new(&args.socket) + .await + .expect("Could not create OSC server"); let (jack_client, ports) = setup_jack(); @@ -167,13 +170,17 @@ fn setup_jack() -> (jack::Client, JackPorts) { let midi_in = jack_client .register_port("midi_in", jack::MidiIn::default()) .expect("Could not create midi_in port"); + let midi_out = jack_client + .register_port("midi_out", jack::MidiOut::default()) + .expect("Could not create midi_out port"); let ports = JackPorts { audio_in, audio_out, click_track_out, midi_in, + midi_out, }; (jack_client, ports) -} \ No newline at end of file +} diff --git a/audio_engine/src/metronome.rs b/audio_engine/src/metronome.rs index f83ec69..df52783 100644 --- a/audio_engine/src/metronome.rs +++ b/audio_engine/src/metronome.rs @@ -8,7 +8,7 @@ pub struct Metronome { // Timing state frames_per_beat: usize, frames_since_last_beat: usize, // Where we are in the current beat cycle - last_frame_time: Option, // For xrun detection + last_frame_time: Option, // For xrun detection } #[derive(Debug, Clone, PartialEq)] @@ -39,6 +39,7 @@ impl Metronome { &mut self, ps: &jack::ProcessScope, ports: &mut JackPorts, + osc_controller: &OscController, ) -> Result { let buffer_size = ps.n_frames(); let current_frame_time = ps.last_frame_time(); @@ -51,17 +52,90 @@ impl Metronome { self.render_click(buffer_size, &timing, click_output); + // Process PPQN ticks for this buffer + self.process_ppqn_ticks(ps, ports, osc_controller)?; + Ok(timing) } + fn process_ppqn_ticks( + &mut self, + ps: &jack::ProcessScope, + ports: &mut JackPorts, + osc_controller: &OscController, + ) -> Result<()> { + let buffer_size = ps.n_frames() as usize; + + // Calculate the starting position for this buffer + let start_position = (self.frames_since_last_beat + self.frames_per_beat - buffer_size) + % self.frames_per_beat; + + // Process PPQN ticks in the current buffer only + self.send_buffer_ppqn_ticks(ps, ports, osc_controller, start_position, buffer_size)?; + + Ok(()) + } + + fn send_buffer_ppqn_ticks( + &self, + ps: &jack::ProcessScope, + ports: &mut JackPorts, + osc_controller: &OscController, + buffer_start_position: usize, + buffer_size: usize, + ) -> Result<()> { + let frames_per_ppqn_tick = self.frames_per_beat / 24; + let mut current_position = buffer_start_position; + let mut frames_processed = 0; + + // Get MIDI output writer + let mut midi_writer = ports.midi_out.writer(ps); + + while frames_processed < buffer_size { + let frames_to_next_ppqn = + frames_per_ppqn_tick - (current_position % frames_per_ppqn_tick); + let frames_remaining_in_buffer = buffer_size - frames_processed; + + if frames_remaining_in_buffer >= frames_to_next_ppqn { + // We hit a PPQN tick within this buffer + let tick_frame_offset = frames_processed + frames_to_next_ppqn; + + // Ensure we don't hit the buffer boundary (JACK expects [0, buffer_size)) + if tick_frame_offset >= buffer_size { + break; + } + + current_position = (current_position + frames_to_next_ppqn) % self.frames_per_beat; + frames_processed = tick_frame_offset; + + // Send sample-accurate MIDI clock + let midi_clock_message = [0xF8]; // MIDI Clock byte + midi_writer + .write(&jack::RawMidi { + time: tick_frame_offset as u32, + bytes: &midi_clock_message, + }) + .map_err(|_| LooperError::Midi(std::panic::Location::caller()))?; + + // Send OSC position message + let ppqn_position = (current_position as f32) / (self.frames_per_beat as f32); + osc_controller.metronome_position_changed(ppqn_position)?; + } else { + break; + } + } + + Ok(()) + } + fn render_click(&mut self, buffer_size: u32, timing: &BufferTiming, click_output: &mut [f32]) { let click_length = self.click_samples.sample_count; // Calculate our position at the START of this buffer (before calculate_timing updated it) // We need to go back by: buffer_size + any missed samples let total_advancement = buffer_size as usize + timing.missed_frames; - let start_position = - (self.frames_since_last_beat + self.frames_per_beat - total_advancement) + let start_position = (self.frames_since_last_beat + self.frames_per_beat + - total_advancement) % self.frames_per_beat; if let Some(beat_offset) = timing.beat_in_buffer { @@ -139,18 +213,19 @@ impl Metronome { let missed = current_frame_time.wrapping_sub(expected) as usize; // Check if we missed multiple beats - let total_samples = self.frames_since_last_beat + missed as usize + buffer_size as usize; + let total_samples = + self.frames_since_last_beat + missed as usize + buffer_size as usize; if total_samples >= 2 * self.frames_per_beat { return Err(LooperError::Xrun(std::panic::Location::caller())); } // Check if a beat occurred in the missed section - let beat_in_missed = if self.frames_since_last_beat + missed as usize >= self.frames_per_beat - { - Some((self.frames_per_beat - self.frames_since_last_beat) as u32) - } else { - None - }; + let beat_in_missed = + if self.frames_since_last_beat + missed as usize >= self.frames_per_beat { + Some((self.frames_per_beat - self.frames_since_last_beat) as u32) + } else { + None + }; (missed, beat_in_missed) } else { @@ -172,7 +247,8 @@ impl Metronome { // Update state - advance by total samples (missed + buffer) self.frames_since_last_beat = - (self.frames_since_last_beat + missed_frames + buffer_size as usize) % self.frames_per_beat; + (self.frames_since_last_beat + missed_frames + buffer_size as usize) + % self.frames_per_beat; self.last_frame_time = Some(current_frame_time); Ok(BufferTiming { diff --git a/audio_engine/src/midi.rs b/audio_engine/src/midi.rs index d9c1c1b..384bbb5 100644 --- a/audio_engine/src/midi.rs +++ b/audio_engine/src/midi.rs @@ -36,7 +36,7 @@ pub fn process_events( wmidi::MidiMessage::ControlChange(_, controller, value) => { let controller_num = u8::from(controller); let value_num = u8::from(value); - + match controller_num { // Expression Pedal A - Track Volume 1 => { @@ -96,4 +96,4 @@ pub fn process_events( } Ok(()) -} \ No newline at end of file +} diff --git a/audio_engine/src/osc_server/controller.rs b/audio_engine/src/osc_server/controller.rs index a0c2401..d25ad6a 100644 --- a/audio_engine/src/osc_server/controller.rs +++ b/audio_engine/src/osc_server/controller.rs @@ -10,31 +10,29 @@ impl OscController { Self { sender } } - pub fn track_state_changed( - &self, - column: usize, - row: usize, - state: TrackState, - ) -> Result<()> { - let message = osc::Message::TrackStateChanged { column, row, state: state.into() }; + pub fn track_state_changed(&self, column: usize, row: usize, state: TrackState) -> Result<()> { + let message = osc::Message::TrackStateChanged { + column, + row, + state: state.into(), + }; match self.sender.try_send(message) { Ok(true) => Ok(()), Ok(false) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel full - Err(_) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel closed + Err(_) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel closed } } - pub fn track_volume_changed( - &self, - column: usize, - row: usize, - volume: f32, - ) -> Result<()> { - let message = osc::Message::TrackVolumeChanged { column, row, volume }; + pub fn track_volume_changed(&self, column: usize, row: usize, volume: f32) -> Result<()> { + let message = osc::Message::TrackVolumeChanged { + column, + row, + volume, + }; match self.sender.try_send(message) { Ok(true) => Ok(()), Ok(false) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel full - Err(_) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel closed + Err(_) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel closed } } @@ -43,7 +41,7 @@ impl OscController { match self.sender.try_send(message) { Ok(true) => Ok(()), Ok(false) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel full - Err(_) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel closed + Err(_) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel closed } } @@ -52,7 +50,16 @@ impl OscController { match self.sender.try_send(message) { Ok(true) => Ok(()), Ok(false) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel full - Err(_) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel closed + Err(_) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel closed } } -} \ No newline at end of file + + pub fn metronome_position_changed(&self, position: f32) -> Result<()> { + let message = osc::Message::MetronomePosition { position }; + match self.sender.try_send(message) { + Ok(true) => Ok(()), + Ok(false) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel full + Err(_) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel closed + } + } +} diff --git a/audio_engine/src/osc_server/mod.rs b/audio_engine/src/osc_server/mod.rs index e30237a..8de1e5f 100644 --- a/audio_engine/src/osc_server/mod.rs +++ b/audio_engine/src/osc_server/mod.rs @@ -4,4 +4,4 @@ mod server; // Re-export public interface - same as before pub use controller::OscController; -pub use server::Osc; \ No newline at end of file +pub use server::Osc; diff --git a/audio_engine/src/osc_server/platform/mod.rs b/audio_engine/src/osc_server/platform/mod.rs index 00f1745..d2c9bfa 100644 --- a/audio_engine/src/osc_server/platform/mod.rs +++ b/audio_engine/src/osc_server/platform/mod.rs @@ -20,4 +20,4 @@ pub(crate) async fn create_listener(socket_path: &str) -> Result Result { - let (stream, _) = self.listener.accept().await + let (stream, _) = self + .listener + .accept() + .await .map_err(|_| LooperError::JackConnection(std::panic::Location::caller()))?; Ok(stream) } @@ -18,9 +21,9 @@ impl PlatformListener { pub(crate) async fn create_listener(socket_path: &str) -> Result { // Remove existing socket file if it exists let _ = std::fs::remove_file(socket_path); - + let listener = UnixListener::bind(socket_path) .map_err(|_| LooperError::JackConnection(std::panic::Location::caller()))?; - + Ok(PlatformListener { listener }) -} \ No newline at end of file +} diff --git a/audio_engine/src/osc_server/platform/windows.rs b/audio_engine/src/osc_server/platform/windows.rs index b45588a..e4f314a 100644 --- a/audio_engine/src/osc_server/platform/windows.rs +++ b/audio_engine/src/osc_server/platform/windows.rs @@ -14,11 +14,13 @@ impl PlatformListener { if self.current_server.is_none() { self.current_server = Some(create_pipe_server(&self.pipe_name)?); } - + let server = self.current_server.take().unwrap(); - server.connect().await + server + .connect() + .await .map_err(|_| LooperError::JackConnection(std::panic::Location::caller()))?; - + Ok(server) } } @@ -30,7 +32,7 @@ pub(crate) async fn create_listener(pipe_name: &str) -> Result } else { format!(r"\\.\pipe\{}", pipe_name) }; - + Ok(PlatformListener { pipe_name: pipe_path, current_server: None, @@ -42,4 +44,4 @@ fn create_pipe_server(pipe_name: &str) -> Result { .first_pipe_instance(false) .create(pipe_name) .map_err(|_| LooperError::JackConnection(std::panic::Location::caller())) -} \ No newline at end of file +} diff --git a/audio_engine/src/osc_server/server.rs b/audio_engine/src/osc_server/server.rs index 4293c0f..1db6a52 100644 --- a/audio_engine/src/osc_server/server.rs +++ b/audio_engine/src/osc_server/server.rs @@ -48,7 +48,7 @@ impl Osc { break; } } - + // Accept new client connections connection = self.listener.accept() => { if let Ok(stream) = connection { @@ -63,17 +63,20 @@ impl Osc { fn spawn_client_task(&self, stream: osc_server::platform::PlatformStream) { let mut receiver = self.broadcaster.subscribe(); let state_dump = self.shadow_state.create_state_dump(); - + tokio::spawn(async move { - let mut framed = tokio_util::codec::Framed::new(stream, tokio_util::codec::LengthDelimitedCodec::new()); - + let mut framed = tokio_util::codec::Framed::new( + stream, + tokio_util::codec::LengthDelimitedCodec::new(), + ); + // Send current state dump immediately for msg in state_dump { if let Err(_) = Self::send_osc_message(&mut framed, msg).await { return; // Client disconnected during state dump } } - + // Forward broadcast messages until connection dies while let Ok(msg) = receiver.recv().await { if let Err(_) = Self::send_osc_message(&mut framed, msg).await { @@ -84,11 +87,14 @@ impl Osc { } async fn send_osc_message( - framed: &mut tokio_util::codec::Framed, + framed: &mut tokio_util::codec::Framed< + osc_server::platform::PlatformStream, + tokio_util::codec::LengthDelimitedCodec, + >, message: osc::Message, ) -> Result<()> { - let osc_packet = Self::message_to_osc_packet(message)?; - + let osc_packet = message.to_osc_packet(); + let osc_bytes = rosc::encoder::encode(&osc_packet) .map_err(|_| crate::LooperError::StateSave(std::panic::Location::caller()))?; @@ -99,41 +105,4 @@ impl Osc { Ok(()) } - - fn message_to_osc_packet(message: osc::Message) -> Result { - match message { - osc::Message::TrackStateChanged { column, row, state } => { - let address = format!("/looper/cell/{}/{}/state", column + 1, row + 1); // 1-based indexing - - Ok(rosc::OscPacket::Message(rosc::OscMessage { - addr: address, - args: vec![rosc::OscType::String(state.to_string())], - })) - } - osc::Message::TrackVolumeChanged { column, row, volume } => { - let address = format!("/looper/cell/{}/{}/volume", column + 1, row + 1); // 1-based indexing - - Ok(rosc::OscPacket::Message(rosc::OscMessage { - addr: address, - args: vec![rosc::OscType::Float(volume)], - })) - } - osc::Message::SelectedColumnChanged { column } => { - let address = "/looper/selected/column".to_string(); - - Ok(rosc::OscPacket::Message(rosc::OscMessage { - addr: address, - args: vec![rosc::OscType::Int((column + 1) as i32)], // 1-based indexing - })) - } - osc::Message::SelectedRowChanged { row } => { - let address = "/looper/selected/row".to_string(); - - Ok(rosc::OscPacket::Message(rosc::OscMessage { - addr: address, - args: vec![rosc::OscType::Int((row + 1) as i32)], // 1-based indexing - })) - } - } - } -} \ No newline at end of file +} diff --git a/audio_engine/src/persistence_manager.rs b/audio_engine/src/persistence_manager.rs index 59da7fe..f6863ad 100644 --- a/audio_engine/src/persistence_manager.rs +++ b/audio_engine/src/persistence_manager.rs @@ -9,12 +9,20 @@ pub struct PersistenceManagerController { #[derive(Debug)] pub enum PersistenceUpdate { - UpdateTrackVolume { column: usize, row: usize, volume: f32 }, + UpdateTrackVolume { + column: usize, + row: usize, + volume: f32, + }, } impl PersistenceManagerController { pub fn update_track_volume(&self, column: usize, row: usize, volume: f32) -> Result<()> { - let update = PersistenceUpdate::UpdateTrackVolume { column, row, volume }; + let update = PersistenceUpdate::UpdateTrackVolume { + column, + row, + volume, + }; match self.sender.try_send(update) { Ok(true) => Ok(()), Ok(false) => Err(LooperError::StateSave(std::panic::Location::caller())), // Channel full @@ -42,9 +50,7 @@ impl PersistenceManager { let (update_tx, update_rx) = kanal::bounded(64); let update_rx = update_rx.to_async(); - let controller = PersistenceManagerController { - sender: update_tx, - }; + let controller = PersistenceManagerController { sender: update_tx }; let manager = Self { state, @@ -72,7 +78,7 @@ impl PersistenceManager { Err(_) => break, } } - + // Handle state updates update = self.update_rx.recv() => { match update { @@ -90,8 +96,13 @@ impl PersistenceManager { fn handle_state_update(&mut self, update: PersistenceUpdate) -> Result<()> { match update { - PersistenceUpdate::UpdateTrackVolume { column, row, volume } => { - self.state.send_modify(|state| state.track_volumes.set_volume(column, row, volume)); + PersistenceUpdate::UpdateTrackVolume { + column, + row, + volume, + } => { + self.state + .send_modify(|state| state.track_volumes.set_volume(column, row, volume)); self.save_to_disk()?; } } @@ -122,6 +133,7 @@ impl PersistenceManager { let port_list = match our_port_name { "midi_in" => &mut connections.midi_in, + "midi_out" => &mut connections.midi_out, "audio_in" => &mut connections.audio_in, "audio_out" => &mut connections.audio_out, "click_track" => &mut connections.click_track_out, @@ -177,4 +189,4 @@ impl PersistenceManager { log::debug!("Saved state to {:?}", self.state_file_path); Ok(()) } -} \ No newline at end of file +} diff --git a/audio_engine/src/process_handler.rs b/audio_engine/src/process_handler.rs index 8b7edfe..0ae82a0 100644 --- a/audio_engine/src/process_handler.rs +++ b/audio_engine/src/process_handler.rs @@ -27,11 +27,7 @@ impl ProcessHandler { osc: OscController, persistence: PersistenceManagerController, ) -> Result { - let track_matrix = TrackMatrix::new( - client, - chunk_factory, - state, - )?; + let track_matrix = TrackMatrix::new(client, chunk_factory, state)?; Ok(Self { post_record_controller, osc, @@ -52,7 +48,7 @@ impl ProcessHandler { pub fn handle_pedal_a(&mut self, value: u8) -> Result<()> { self.last_volume_setting = (value as f32) / 127.0; - + let controllers = TrackControllers::new( &self.post_record_controller, &self.osc, @@ -61,10 +57,8 @@ impl ProcessHandler { self.selected_column, self.selected_row, ); - self.track_matrix.handle_volume_update( - self.last_volume_setting, - &controllers, - ) + self.track_matrix + .handle_volume_update(self.last_volume_setting, &controllers) } pub fn handle_button_1(&mut self) -> Result<()> { @@ -76,10 +70,8 @@ impl ProcessHandler { self.selected_column, self.selected_row, ); - self.track_matrix.handle_record_button( - self.last_volume_setting, - &controllers, - ) + self.track_matrix + .handle_record_button(self.last_volume_setting, &controllers) } pub fn handle_button_2(&mut self) -> Result<()> { @@ -91,9 +83,7 @@ impl ProcessHandler { self.selected_column, self.selected_row, ); - self.track_matrix.handle_play_button( - &controllers, - ) + self.track_matrix.handle_play_button(&controllers) } pub fn handle_button_3(&mut self) -> Result<()> { @@ -113,9 +103,7 @@ impl ProcessHandler { self.selected_column, self.selected_row, ); - self.track_matrix.handle_clear_button( - &controllers, - ) + self.track_matrix.handle_clear_button(&controllers) } pub fn handle_button_6(&mut self) -> Result<()> { @@ -177,12 +165,9 @@ impl jack::ProcessHandler for ProcessHandler { } impl ProcessHandler { - fn process_with_error_handling( - &mut self, - ps: &jack::ProcessScope, - ) -> Result<()> { + fn process_with_error_handling(&mut self, ps: &jack::ProcessScope) -> Result<()> { // Process metronome and get beat timing information - let timing = self.metronome.process(ps, &mut self.ports)?; + let timing = self.metronome.process(ps, &mut self.ports, &self.osc)?; // Process MIDI midi::process_events(self, ps)?; @@ -194,13 +179,9 @@ impl ProcessHandler { &self.persistence, self.sample_rate, ); - self.track_matrix.process( - ps, - &mut self.ports, - &timing, - &controllers, - )?; + self.track_matrix + .process(ps, &mut self.ports, &timing, &controllers)?; Ok(()) } -} \ No newline at end of file +} diff --git a/audio_engine/src/state.rs b/audio_engine/src/state.rs index 026fe04..db15c73 100644 --- a/audio_engine/src/state.rs +++ b/audio_engine/src/state.rs @@ -11,6 +11,7 @@ pub struct State { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ConnectionState { pub midi_in: Vec, + pub midi_out: Vec, pub audio_in: Vec, pub audio_out: Vec, pub click_track_out: Vec, @@ -56,6 +57,7 @@ impl Default for State { Self { connections: ConnectionState { midi_in: Vec::new(), + midi_out: Vec::new(), audio_in: Vec::new(), audio_out: Vec::new(), click_track_out: Vec::new(), @@ -67,4 +69,4 @@ impl Default for State { track_volumes: TrackVolumes::new(), } } -} \ No newline at end of file +} diff --git a/audio_engine/src/track.rs b/audio_engine/src/track.rs index b54cffd..3fb7c6e 100644 --- a/audio_engine/src/track.rs +++ b/audio_engine/src/track.rs @@ -12,7 +12,7 @@ pub enum TrackState { Empty, Idle, Playing, - Recording { + Recording { volume: f32, }, RecordingAutoStop { @@ -44,45 +44,46 @@ impl Track { } pub fn is_idle(&self) -> bool { - ! self.is_recording() && ! self.is_playing() + !self.is_recording() && !self.is_playing() } pub fn len(&self) -> usize { self.audio_data.len() } - pub fn set_volume( - &mut self, - new_volume: f32, - controllers: &TrackControllers, - ) -> Result<()> { + pub fn set_volume(&mut self, new_volume: f32, controllers: &TrackControllers) -> Result<()> { let new_volume = new_volume.clamp(0.0, 1.0); - + match &mut self.current_state { TrackState::Recording { volume } => { - *volume = new_volume; // Update recording volume only + *volume = new_volume; // Update recording volume only controllers.send_volume_update(new_volume)?; } TrackState::RecordingAutoStop { volume, .. } => { - *volume = new_volume; // Update recording volume only + *volume = new_volume; // Update recording volume only controllers.send_volume_update(new_volume)?; } _ => { - self.volume = new_volume; // Update committed volume - controllers.update_track_volume(new_volume)?; // Update State - controllers.send_volume_update(new_volume)?; // Update GUI + self.volume = new_volume; // Update committed volume + controllers.update_track_volume(new_volume)?; // Update State + controllers.send_volume_update(new_volume)?; // Update GUI } } Ok(()) } pub fn record(&mut self, last_volume_setting: f32) { - self.next_state = TrackState::Recording { + self.next_state = TrackState::Recording { volume: last_volume_setting, }; } - pub fn record_auto_stop(&mut self, target_samples: usize, sync_offset: usize, last_volume_setting: f32) { + pub fn record_auto_stop( + &mut self, + target_samples: usize, + sync_offset: usize, + last_volume_setting: f32, + ) { self.next_state = TrackState::RecordingAutoStop { volume: last_volume_setting, target_samples, @@ -317,7 +318,7 @@ impl Track { self.volume = recording_vol; controllers.update_track_volume(recording_vol)?; } - + let (chunk, sync_offset) = self.audio_data.get_chunk_for_processing()?; controllers.send_post_record_request(chunk, sync_offset)?; } else if was_recording && self.current_state == TrackState::Empty { @@ -339,4 +340,4 @@ impl Into for TrackState { TrackState::Playing => osc::TrackState::Playing, } } -} \ No newline at end of file +} diff --git a/audio_engine/src/track_matrix.rs b/audio_engine/src/track_matrix.rs index 270a376..2e39aec 100644 --- a/audio_engine/src/track_matrix.rs +++ b/audio_engine/src/track_matrix.rs @@ -7,11 +7,7 @@ pub struct TrackMatrix { } impl TrackMatrix { - pub fn new( - client: &jack::Client, - chunk_factory: F, - state: &State, - ) -> Result { + pub fn new(client: &jack::Client, chunk_factory: F, state: &State) -> Result { let columns = std::array::from_fn(|_| Column::new(state.metronome.frames_per_beat)); Ok(Self { chunk_factory, @@ -21,27 +17,18 @@ impl TrackMatrix Result<()> { - self.columns[controllers.column()].handle_record_button( - last_volume_setting, - controllers, - ) + self.columns[controllers.column()].handle_record_button(last_volume_setting, controllers) } - pub fn handle_play_button( - &mut self, - controllers: &TrackControllers, - ) -> Result<()> { + pub fn handle_play_button(&mut self, controllers: &TrackControllers) -> Result<()> { self.columns[controllers.column()].handle_play_button(controllers) } - pub fn handle_clear_button( - &mut self, - controllers: &TrackControllers, - ) -> Result<()> { + pub fn handle_clear_button(&mut self, controllers: &TrackControllers) -> Result<()> { self.columns[controllers.column()].handle_clear_button(controllers) } @@ -50,10 +37,7 @@ impl TrackMatrix Result<()> { - self.columns[controllers.column()].handle_volume_update( - new_volume, - controllers, - ) + self.columns[controllers.column()].handle_volume_update(new_volume, controllers) } pub fn process( @@ -73,12 +57,8 @@ impl TrackMatrix 0 { for (i, column) in &mut self.columns.iter_mut().enumerate() { let controllers = controllers.to_column_controllers(i); - - column.handle_xrun( - &timing, - &mut self.chunk_factory, - &controllers, - )?; + + column.handle_xrun(&timing, &mut self.chunk_factory, &controllers)?; } } @@ -89,7 +69,7 @@ impl TrackMatrix TrackMatrix { state.set_track_volume(*column, *row, *volume); } + Message::MetronomePosition { position } => { + state.set_metronome_position(*position); + } } } @@ -100,9 +106,59 @@ impl Message { let row = (*row_1based as usize).saturating_sub(1); // Convert to 0-based return Ok(Some(Message::SelectedRowChanged { row })); } + } else if addr == "/looper/metronome/position" { + if let Some(rosc::OscType::Float(position)) = args.first() { + return Ok(Some(Message::MetronomePosition { + position: *position + })); + } } - // Unknown or unsupported message Ok(None) } + + pub fn to_osc_packet(self) -> rosc::OscPacket { + match self { + Message::TrackStateChanged { column, row, state } => { + let address = format!("/looper/cell/{}/{}/state", column + 1, row + 1); // 1-based indexing + + rosc::OscPacket::Message(rosc::OscMessage { + addr: address, + args: vec![rosc::OscType::String(state.to_string())], + }) + } + Message::TrackVolumeChanged { column, row, volume } => { + let address = format!("/looper/cell/{}/{}/volume", column + 1, row + 1); // 1-based indexing + + rosc::OscPacket::Message(rosc::OscMessage { + addr: address, + args: vec![rosc::OscType::Float(volume)], + }) + } + Message::SelectedColumnChanged { column } => { + let address = "/looper/selected/column".to_string(); + + rosc::OscPacket::Message(rosc::OscMessage { + addr: address, + args: vec![rosc::OscType::Int((column + 1) as i32)], // 1-based indexing + }) + } + Message::SelectedRowChanged { row } => { + let address = "/looper/selected/row".to_string(); + + rosc::OscPacket::Message(rosc::OscMessage { + addr: address, + args: vec![rosc::OscType::Int((row + 1) as i32)], // 1-based indexing + }) + } + Message::MetronomePosition { position } => { + let address = "/looper/metronome/position".to_string(); + + rosc::OscPacket::Message(rosc::OscMessage { + addr: address, + args: vec![rosc::OscType::Float(position)], + }) + } + } + } } \ No newline at end of file diff --git a/osc/src/state.rs b/osc/src/state.rs index ae20014..9915df5 100644 --- a/osc/src/state.rs +++ b/osc/src/state.rs @@ -21,6 +21,8 @@ pub struct State { pub cells: Vec>, pub columns: usize, pub rows: usize, + pub metronome_position: f32, // 0.0 - 1.0 position within current beat + pub metronome_timestamp: std::time::Instant, // When position was last updated } impl State { @@ -35,6 +37,8 @@ impl State { cells, columns, rows, + metronome_position: 0.0, + metronome_timestamp: std::time::Instant::now(), } } @@ -60,6 +64,9 @@ impl State { self.selected_row = *row; } } + Message::MetronomePosition { position } => { + self.set_metronome_position(*position); + } } } @@ -116,4 +123,9 @@ impl State { self.cells[column][row].volume = volume; } } + + pub fn set_metronome_position(&mut self, position: f32) { + self.metronome_position = position; + self.metronome_timestamp = std::time::Instant::now(); + } } \ No newline at end of file