From 70b2f92a8d0b70bcdeae306dcb2fc78db60d67aa Mon Sep 17 00:00:00 2001 From: Geens Date: Thu, 19 Jun 2025 19:48:43 +0200 Subject: [PATCH] Added track volume control to audio engine --- .gitignore | 1 + audio_engine/src/args.rs | 7 +- audio_engine/src/column.rs | 74 +++++++----- audio_engine/src/controllers.rs | 118 ++++++++++++++++++ audio_engine/src/main.rs | 23 ++-- audio_engine/src/midi.rs | 91 +++++++------- audio_engine/src/osc/controller.rs | 14 +++ audio_engine/src/osc/message.rs | 33 ++++- audio_engine/src/osc/server.rs | 11 +- audio_engine/src/persistence_manager.rs | 152 ++++++++++++++++-------- audio_engine/src/post_record_handler.rs | 16 +-- audio_engine/src/process_handler.rs | 82 +++++++++++-- audio_engine/src/state.rs | 34 +++++- audio_engine/src/track.rs | 111 ++++++++++------- audio_engine/src/track_matrix.rs | 76 ++++++------ 15 files changed, 595 insertions(+), 248 deletions(-) create mode 100644 audio_engine/src/controllers.rs diff --git a/.gitignore b/.gitignore index 5890066..80e0ad7 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ +config/ collect.txt \ No newline at end of file diff --git a/audio_engine/src/args.rs b/audio_engine/src/args.rs index dd9fab9..65a0241 100644 --- a/audio_engine/src/args.rs +++ b/audio_engine/src/args.rs @@ -10,8 +10,8 @@ pub struct Args { #[arg(short = 's', long = "socket", env = "SOCKET", default_value = "fcb_looper.sock")] pub socket: String, - /// Path to configuration file - #[arg(short = 'c', long = "config-file", env = "CONFIG_FILE", default_value = default_config_path())] + /// Path to configuration dir + #[arg(short = 'c', long = "config-path", env = "CONFIG_PATH", default_value = default_config_path())] pub config_file: PathBuf, } @@ -26,8 +26,11 @@ impl Args { } fn default_config_path() -> String { + /* let mut path = dirs::home_dir().unwrap_or_else(|| PathBuf::from(".")); path.push(".fcb_looper"); path.push("state.json"); path.to_string_lossy().to_string() + */ + "../config".to_string() } \ No newline at end of file diff --git a/audio_engine/src/column.rs b/audio_engine/src/column.rs index 83469b6..6da9a55 100644 --- a/audio_engine/src/column.rs +++ b/audio_engine/src/column.rs @@ -37,9 +37,14 @@ impl Column { return true; } - pub fn handle_record_button(&mut self, row: usize) -> Result<()> { + pub fn handle_record_button( + &mut self, + last_volume_setting: f32, + controllers: &TrackControllers, + ) -> Result<()> { let len = self.len(); - let track = &mut self.tracks[row]; + let track = &mut self.tracks[controllers.row()]; + if track.is_recording() { if len > 0 { track.clear(); @@ -49,71 +54,76 @@ impl Column { } else { if len > 0 { let sync_offset = len - self.playback_position; - track.record_auto_stop(len, sync_offset); + track.record_auto_stop(len, sync_offset, last_volume_setting); } else { - track.record(); + track.record(last_volume_setting); } } Ok(()) } - pub fn handle_play_button(&mut self, row: usize) -> Result<()> { - let track = &mut self.tracks[row]; - if track.len() > 0 && track.is_idle() { + 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(); - } else if ! track.is_idle() { + } else if !track.is_idle() { track.stop(); } Ok(()) } - pub fn handle_clear_button(&mut self, row: usize) -> Result<()> { - let track = &mut self.tracks[row]; + pub fn handle_clear_button( + &mut self, + controllers: &TrackControllers, + ) -> Result<()> { + let track = &mut self.tracks[controllers.row()]; track.clear(); Ok(()) } + pub fn handle_volume_update( + &mut self, + new_volume: f32, + controllers: &TrackControllers, + ) -> Result<()> { + self.tracks[controllers.row()].set_volume(new_volume, controllers) + } + pub fn set_consolidated_buffer(&mut self, row: usize, buffer: Box<[f32]>) -> Result<()> { self.tracks[row].set_consolidated_buffer(buffer) } - pub fn handle_xrun( + pub fn handle_xrun( &mut self, timing: &BufferTiming, chunk_factory: &mut impl ChunkFactory, - post_record_controller: PRC, - osc_controller: OC, - ) -> Result<()> - where - PRC: Fn(usize, Arc, usize) -> Result<()>, - OC: Fn(usize, TrackState) -> Result<()>, - { + controllers: &ColumnControllers, + ) -> 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, chunk_factory, - |chunk, sync_offset| post_record_controller(row, chunk, sync_offset), - |state| osc_controller(row, state), + &track_controllers, )?; } Ok(()) } - pub fn process( + pub fn process( &mut self, timing: &BufferTiming, input_buffer: &[f32], output_buffer: &mut [f32], scratch_pad: &mut [f32], chunk_factory: &mut impl ChunkFactory, - post_record_controller: PRC, - osc_controller: OC, - ) -> Result<()> - where - PRC: Fn(usize, Arc, usize) -> Result<()>, - OC: Fn(usize, TrackState) -> Result<()>, - { + controllers: &ColumnControllers, + ) -> Result<()> { let len = self.len(); if self.idle() { if let Some(beat_index) = timing.beat_in_buffer { @@ -125,20 +135,24 @@ 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, input_buffer, scratch_pad, chunk_factory, - |chunk, sync_offset| post_record_controller(row, chunk, sync_offset), - |state| osc_controller(row, state), + &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(); diff --git a/audio_engine/src/controllers.rs b/audio_engine/src/controllers.rs new file mode 100644 index 0000000..8694ae3 --- /dev/null +++ b/audio_engine/src/controllers.rs @@ -0,0 +1,118 @@ +use crate::*; + +pub struct MatrixControllers<'a> { + post_record: &'a PostRecordController, + osc: &'a OscController, + persistence: &'a PersistenceManagerController, + sample_rate: usize, +} + +pub struct ColumnControllers<'a> { + post_record: &'a PostRecordController, + osc: &'a OscController, + persistence: &'a PersistenceManagerController, + sample_rate: usize, + column: usize, +} + +pub struct TrackControllers<'a> { + post_record: &'a PostRecordController, + osc: &'a OscController, + persistence: &'a PersistenceManagerController, + sample_rate: usize, + column: usize, + row: usize, +} + +impl<'a> MatrixControllers<'a> { + pub fn new( + post_record: &'a PostRecordController, + osc: &'a OscController, + persistence: &'a PersistenceManagerController, + sample_rate: usize, + ) -> Self { + Self { + post_record, + osc, + persistence, + sample_rate, + } + } + + pub fn to_column_controllers(&self, column: usize) -> ColumnControllers<'a> { + ColumnControllers { + post_record: self.post_record, + osc: self.osc, + persistence: self.persistence, + sample_rate: self.sample_rate, + column, + } + } + + pub fn try_recv_post_record_response(&self) -> Option { + self.post_record.try_recv_response() + } +} + +impl<'a> ColumnControllers<'a> { + pub fn to_track_controllers(&self, row: usize) -> TrackControllers<'a> { + TrackControllers { + post_record: self.post_record, + osc: self.osc, + persistence: self.persistence, + sample_rate: self.sample_rate, + column: self.column, + row, + } + } +} + +impl<'a> TrackControllers<'a> { + pub fn new( + post_record: &'a PostRecordController, + osc: &'a OscController, + persistence: &'a PersistenceManagerController, + sample_rate: usize, + column: usize, + row: usize, + ) -> Self { + Self { + post_record, + osc, + persistence, + sample_rate, + column, + row, + } + } + + 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<()> { + self.osc.track_state_changed(self.column, self.row, state) + } + + pub fn send_volume_update(&self, volume: f32) -> Result<()> { + self.osc.track_volume_changed(self.column, self.row, volume) + } + + pub fn update_track_volume(&self, volume: f32) -> Result<()> { + self.persistence.update_track_volume(self.column, self.row, volume) + } + + pub fn column(&self) -> usize { + self.column + } + + pub fn row(&self) -> usize { + self.row + } +} \ No newline at end of file diff --git a/audio_engine/src/main.rs b/audio_engine/src/main.rs index 0b8de1d..f83710c 100644 --- a/audio_engine/src/main.rs +++ b/audio_engine/src/main.rs @@ -6,6 +6,7 @@ mod beep; mod chunk_factory; mod column; mod connection_manager; +mod controllers; mod looper_error; mod metronome; mod midi; @@ -28,6 +29,9 @@ use beep::generate_beep; use chunk_factory::ChunkFactory; use column::Column; use connection_manager::ConnectionManager; +use controllers::ColumnControllers; +use controllers::MatrixControllers; +use controllers::TrackControllers; use looper_error::LooperError; use looper_error::Result; use metronome::BufferTiming; @@ -37,8 +41,10 @@ use notification_handler::NotificationHandler; use osc::OscController; use osc::Osc; use persistence_manager::PersistenceManager; +use persistence_manager::PersistenceManagerController; use post_record_handler::PostRecordController; use post_record_handler::PostRecordHandler; +use post_record_handler::PostRecordResponse; use process_handler::ProcessHandler; use state::State; use track::Track; @@ -71,29 +77,28 @@ async fn main() { let notification_handler = NotificationHandler::new(); let mut notification_channel = notification_handler.subscribe(); - let (mut persistence_manager, state_watch) = + let (mut persistence_manager, persistence_controller) = PersistenceManager::new(&args, notification_handler.subscribe()); - - // Load state values for metronome configuration - let initial_state = state_watch.borrow().clone(); + let state = persistence_manager.state(); // Create post-record handler and get controller for ProcessHandler let (mut post_record_handler, post_record_controller) = - PostRecordHandler::new().expect("Could not create post-record handler"); + PostRecordHandler::new(&args).expect("Could not create post-record handler"); let process_handler = ProcessHandler::new( &jack_client, ports, allocator, beep_samples, - &initial_state, - osc_controller, + &state.borrow(), post_record_controller, + osc_controller, + persistence_controller, ) .expect("Could not create process handler"); let mut connection_manager = ConnectionManager::new( - state_watch, + persistence_manager.state(), notification_handler.subscribe(), jack_client.name().to_string(), ) @@ -171,4 +176,4 @@ fn setup_jack() -> (jack::Client, JackPorts) { }; (jack_client, ports) -} +} \ No newline at end of file diff --git a/audio_engine/src/midi.rs b/audio_engine/src/midi.rs index be1e26a..d9c1c1b 100644 --- a/audio_engine/src/midi.rs +++ b/audio_engine/src/midi.rs @@ -34,48 +34,53 @@ pub fn process_events( 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 => { - process_handler.handle_button_1()?; - } - 21 => { - process_handler.handle_button_2()?; - } - 22 => { - process_handler.handle_button_3()?; - } - 23 => { - process_handler.handle_button_4()?; - } - 24 => { - process_handler.handle_button_5()?; - } - 25 => { - process_handler.handle_button_6()?; - } - 26 => { - process_handler.handle_button_7()?; - } - 27 => { - process_handler.handle_button_8()?; - } - 28 => { - process_handler.handle_button_9()?; - } - 29 => { - process_handler.handle_button_10()?; - } - 30 => { - process_handler.handle_button_up()?; - } - 31 => { - process_handler.handle_button_down()?; - } - _ => { - // Other CC messages - ignore - } + let controller_num = u8::from(controller); + let value_num = u8::from(value); + + match controller_num { + // Expression Pedal A - Track Volume + 1 => { + process_handler.handle_pedal_a(value_num)?; + } + // Button mappings (only process button presses - value > 0) + 20 if value_num > 0 => { + process_handler.handle_button_1()?; + } + 21 if value_num > 0 => { + process_handler.handle_button_2()?; + } + 22 if value_num > 0 => { + process_handler.handle_button_3()?; + } + 23 if value_num > 0 => { + process_handler.handle_button_4()?; + } + 24 if value_num > 0 => { + process_handler.handle_button_5()?; + } + 25 if value_num > 0 => { + process_handler.handle_button_6()?; + } + 26 if value_num > 0 => { + process_handler.handle_button_7()?; + } + 27 if value_num > 0 => { + process_handler.handle_button_8()?; + } + 28 if value_num > 0 => { + process_handler.handle_button_9()?; + } + 29 if value_num > 0 => { + process_handler.handle_button_10()?; + } + 30 if value_num > 0 => { + process_handler.handle_button_up()?; + } + 31 if value_num > 0 => { + process_handler.handle_button_down()?; + } + _ => { + // Other CC messages - ignore } } } @@ -91,4 +96,4 @@ pub fn process_events( } Ok(()) -} +} \ No newline at end of file diff --git a/audio_engine/src/osc/controller.rs b/audio_engine/src/osc/controller.rs index 1532b76..615cf38 100644 --- a/audio_engine/src/osc/controller.rs +++ b/audio_engine/src/osc/controller.rs @@ -25,6 +25,20 @@ impl OscController { } } + pub fn track_volume_changed( + &self, + column: usize, + row: usize, + volume: f32, + ) -> Result<()> { + let message = OscMessage::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 + } + } + pub fn selected_column_changed(&self, column: usize) -> Result<()> { let message = OscMessage::SelectedColumnChanged { column }; match self.sender.try_send(message) { diff --git a/audio_engine/src/osc/message.rs b/audio_engine/src/osc/message.rs index 8ec8f99..d33b480 100644 --- a/audio_engine/src/osc/message.rs +++ b/audio_engine/src/osc/message.rs @@ -7,6 +7,11 @@ pub(crate) enum OscMessage { row: usize, state: TrackState, }, + TrackVolumeChanged { + column: usize, + row: usize, + volume: f32, + }, SelectedColumnChanged { column: usize, }, @@ -15,11 +20,17 @@ pub(crate) enum OscMessage { }, } +#[derive(Debug, Clone)] +pub(crate) struct Track { + pub state: TrackState, + pub volume: f32, +} + #[derive(Debug, Clone)] pub(crate) struct ShadowState { pub selected_column: usize, // 0-based (converted to 1-based for OSC) pub selected_row: usize, // 0-based (converted to 1-based for OSC) - pub cells: Vec>, + pub cells: Vec>, pub columns: usize, pub rows: usize, } @@ -27,7 +38,7 @@ pub(crate) struct ShadowState { impl ShadowState { pub fn new(columns: usize, rows: usize) -> Self { let cells = (0..columns) - .map(|_| vec![TrackState::Empty; rows]) + .map(|_| vec![Track { state: TrackState::Empty, volume: 1.0 }; rows]) .collect(); Self { @@ -43,7 +54,12 @@ impl ShadowState { match message { OscMessage::TrackStateChanged { column, row, state } => { if *column < self.columns && *row < self.rows { - self.cells[*column][*row] = state.clone(); + self.cells[*column][*row].state = state.clone(); + } + } + OscMessage::TrackVolumeChanged { column, row, volume } => { + if *column < self.columns && *row < self.rows { + self.cells[*column][*row].volume = *volume; } } OscMessage::SelectedColumnChanged { column } => { @@ -70,13 +86,18 @@ impl ShadowState { row: self.selected_row, }); - // Send all cell states + // Send all cell states and volumes for (column, column_cells) in self.cells.iter().enumerate() { - for (row, cell_state) in column_cells.iter().enumerate() { + for (row, track) in column_cells.iter().enumerate() { messages.push(OscMessage::TrackStateChanged { column, row, - state: cell_state.clone(), + state: track.state.clone(), + }); + messages.push(OscMessage::TrackVolumeChanged { + column, + row, + volume: track.volume, }); } } diff --git a/audio_engine/src/osc/server.rs b/audio_engine/src/osc/server.rs index 86b8716..93f3a1b 100644 --- a/audio_engine/src/osc/server.rs +++ b/audio_engine/src/osc/server.rs @@ -119,6 +119,14 @@ impl Osc { args: vec![rosc::OscType::String(osc_state)], })) } + OscMessage::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)], + })) + } OscMessage::SelectedColumnChanged { column } => { let address = "/looper/selected/column".to_string(); @@ -142,7 +150,8 @@ impl Osc { match state { crate::TrackState::Empty => "empty".to_string(), crate::TrackState::Idle => "ready".to_string(), - crate::TrackState::Recording | crate::TrackState::RecordingAutoStop { .. } => "recording".to_string(), + crate::TrackState::Recording { .. } => "recording".to_string(), + crate::TrackState::RecordingAutoStop { .. } => "recording".to_string(), crate::TrackState::Playing => "playing".to_string(), } } diff --git a/audio_engine/src/persistence_manager.rs b/audio_engine/src/persistence_manager.rs index a20a9bc..59da7fe 100644 --- a/audio_engine/src/persistence_manager.rs +++ b/audio_engine/src/persistence_manager.rs @@ -2,10 +2,31 @@ use crate::*; use std::path::PathBuf; use tokio::sync::{broadcast, watch}; +#[derive(Debug)] +pub struct PersistenceManagerController { + sender: kanal::Sender, +} + +#[derive(Debug)] +pub enum PersistenceUpdate { + 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 }; + match self.sender.try_send(update) { + Ok(true) => Ok(()), + Ok(false) => Err(LooperError::StateSave(std::panic::Location::caller())), // Channel full + Err(_) => Err(LooperError::StateSave(std::panic::Location::caller())), // Channel closed + } + } +} + pub struct PersistenceManager { - state: State, - state_tx: watch::Sender, + state: watch::Sender, notification_rx: broadcast::Receiver, + update_rx: kanal::AsyncReceiver, state_file_path: PathBuf, } @@ -13,33 +34,65 @@ impl PersistenceManager { pub fn new( args: &Args, notification_rx: broadcast::Receiver, - ) -> (Self, watch::Receiver) { - let state_file_path = args.config_file.clone(); - if let Some(parent) = state_file_path.parent() { - std::fs::create_dir_all(parent).ok(); // Create directory if it doesn't exist - } + ) -> (Self, PersistenceManagerController) { + let state_file_path = args.config_file.join("state.json"); + std::fs::create_dir_all(&args.config_file).ok(); // Create directory if it doesn't exist let initial_state = Self::load_from_disk(&state_file_path).unwrap_or_default(); - let (state_tx, state_rx) = watch::channel(initial_state.clone()); + let (state, _) = watch::channel(initial_state); + + let (update_tx, update_rx) = kanal::bounded(64); + let update_rx = update_rx.to_async(); + let controller = PersistenceManagerController { + sender: update_tx, + }; let manager = Self { - state: initial_state, - state_tx, + state, notification_rx, + update_rx, state_file_path, }; - (manager, state_rx) + (manager, controller) + } + + pub fn state(&self) -> watch::Receiver { + self.state.subscribe() } pub async fn run(&mut self) -> Result<()> { - while let Ok(notification) = self.notification_rx.recv().await { - if let JackNotification::PortConnect { - port_a, - port_b, - connected, - } = notification - { - self.handle_port_connection(port_a, port_b, connected)?; + loop { + tokio::select! { + notification = self.notification_rx.recv() => { + match notification { + Ok(JackNotification::PortConnect { port_a, port_b, connected }) => { + self.handle_port_connection(port_a, port_b, connected)?; + } + Ok(_) => {} + Err(_) => break, + } + } + + // Handle state updates + update = self.update_rx.recv() => { + match update { + Ok(update) => { + self.handle_state_update(update)?; + } + Err(_) => break, + } + } + } + } + + Ok(()) + } + + 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)); + self.save_to_disk()?; } } @@ -64,37 +117,36 @@ impl PersistenceManager { // Update the connections state let our_port_name = our_port.strip_prefix("looper:").unwrap_or(&our_port); - let connections = &mut self.state.connections; + self.state.send_if_modified(|state| { + let connections = &mut state.connections; - let port_list = match our_port_name { - "midi_in" => &mut connections.midi_in, - "audio_in" => &mut connections.audio_in, - "audio_out" => &mut connections.audio_out, - "click_track" => &mut connections.click_track_out, - _ => { - log::warn!("Unknown port: {}", our_port_name); - return Ok(()); + let port_list = match our_port_name { + "midi_in" => &mut connections.midi_in, + "audio_in" => &mut connections.audio_in, + "audio_out" => &mut connections.audio_out, + "click_track" => &mut connections.click_track_out, + _ => { + log::warn!("Unknown port: {}", our_port_name); + return false; + } + }; + + if connected { + // Add connection if not already present + if !port_list.contains(&external_port) { + port_list.push(external_port.clone()); + log::info!("Added connection: {} -> {}", our_port, external_port); + true + } else { + false + } + } else { + // Remove connection + port_list.retain(|p| p != &external_port); + log::info!("Removed connection: {} -> {}", our_port, external_port); + true } - }; - - if connected { - // Add connection if not already present - if !port_list.contains(&external_port) { - port_list.push(external_port.clone()); - log::info!("Added connection: {} -> {}", our_port, external_port); - } - } else { - // Remove connection - port_list.retain(|p| p != &external_port); - log::info!("Removed connection: {} -> {}", our_port, external_port); - } - - // Broadcast state change - if let Err(e) = self.state_tx.send(self.state.clone()) { - log::error!("Failed to broadcast state change: {}", e); - } - - // Save to disk + }); self.save_to_disk()?; Ok(()) @@ -116,7 +168,7 @@ impl PersistenceManager { } fn save_to_disk(&self) -> Result<()> { - let json = serde_json::to_string_pretty(&self.state) + let json = serde_json::to_string_pretty(&*self.state.borrow()) .map_err(|_| LooperError::StateSave(std::panic::Location::caller()))?; std::fs::write(&self.state_file_path, json) @@ -125,4 +177,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/post_record_handler.rs b/audio_engine/src/post_record_handler.rs index e900fe7..39bbe1b 100644 --- a/audio_engine/src/post_record_handler.rs +++ b/audio_engine/src/post_record_handler.rs @@ -69,7 +69,7 @@ pub struct PostRecordHandler { impl PostRecordHandler { /// Create new handler and return RT controller - pub fn new() -> Result<(Self, PostRecordController)> { + pub fn new(args: &Args) -> Result<(Self, PostRecordController)> { // Create channels for bidirectional communication let (request_sender, request_receiver) = kanal::bounded(16); let (response_sender, response_receiver) = kanal::bounded(16); @@ -85,7 +85,7 @@ impl PostRecordHandler { let handler = Self { request_receiver, response_sender, - directory: Self::create_directory()?, + directory: args.config_file.clone(), }; Ok((handler, controller)) @@ -219,18 +219,6 @@ impl PostRecordHandler { .map_err(|_| LooperError::StateSave(std::panic::Location::caller()))? } - /// Create save directory and return path - fn create_directory() -> Result { - let mut path = dirs::home_dir().unwrap_or_else(|| PathBuf::from(".")); - path.push(".fcb_looper"); - - std::fs::create_dir_all(&path) - .map_err(|_| LooperError::StateSave(std::panic::Location::caller()))?; - - Ok(path) - } - - /// Get file path for track recording fn get_file_path(&self, row: usize) -> PathBuf { self.directory.join(format!("row_{row}.wav")) } diff --git a/audio_engine/src/process_handler.rs b/audio_engine/src/process_handler.rs index 1477c8b..8b7edfe 100644 --- a/audio_engine/src/process_handler.rs +++ b/audio_engine/src/process_handler.rs @@ -4,12 +4,16 @@ const COLS: usize = 5; const ROWS: usize = 5; pub struct ProcessHandler { + post_record_controller: PostRecordController, osc: OscController, + persistence: PersistenceManagerController, ports: JackPorts, metronome: Metronome, track_matrix: TrackMatrix, selected_row: usize, selected_column: usize, + last_volume_setting: f32, + sample_rate: usize, } impl ProcessHandler { @@ -19,22 +23,26 @@ impl ProcessHandler { chunk_factory: F, beep_samples: Arc, state: &State, - osc: OscController, post_record_controller: PostRecordController, + osc: OscController, + persistence: PersistenceManagerController, ) -> Result { let track_matrix = TrackMatrix::new( client, chunk_factory, state, - post_record_controller, )?; Ok(Self { + post_record_controller, osc, + persistence, ports, metronome: Metronome::new(beep_samples, state), track_matrix, selected_row: 0, selected_column: 0, + last_volume_setting: 1.0, + sample_rate: client.sample_rate(), }) } @@ -42,12 +50,50 @@ impl ProcessHandler { &self.ports } + 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, + &self.persistence, + self.sample_rate, + self.selected_column, + self.selected_row, + ); + self.track_matrix.handle_volume_update( + self.last_volume_setting, + &controllers, + ) + } + pub fn handle_button_1(&mut self) -> Result<()> { - self.track_matrix.handle_record_button(self.selected_column, self.selected_row) + let controllers = TrackControllers::new( + &self.post_record_controller, + &self.osc, + &self.persistence, + self.sample_rate, + self.selected_column, + self.selected_row, + ); + self.track_matrix.handle_record_button( + self.last_volume_setting, + &controllers, + ) } pub fn handle_button_2(&mut self) -> Result<()> { - self.track_matrix.handle_play_button(self.selected_column, self.selected_row) + let controllers = TrackControllers::new( + &self.post_record_controller, + &self.osc, + &self.persistence, + self.sample_rate, + self.selected_column, + self.selected_row, + ); + self.track_matrix.handle_play_button( + &controllers, + ) } pub fn handle_button_3(&mut self) -> Result<()> { @@ -59,7 +105,17 @@ impl ProcessHandler { } pub fn handle_button_5(&mut self) -> Result<()> { - self.track_matrix.handle_clear_button(self.selected_column, self.selected_row) + let controllers = TrackControllers::new( + &self.post_record_controller, + &self.osc, + &self.persistence, + self.sample_rate, + self.selected_column, + self.selected_row, + ); + self.track_matrix.handle_clear_button( + &controllers, + ) } pub fn handle_button_6(&mut self) -> Result<()> { @@ -110,8 +166,8 @@ impl ProcessHandler { } impl jack::ProcessHandler for ProcessHandler { - fn process(&mut self, client: &jack::Client, ps: &jack::ProcessScope) -> jack::Control { - if let Err(e) = self.process_with_error_handling(client, ps) { + fn process(&mut self, _client: &jack::Client, ps: &jack::ProcessScope) -> jack::Control { + if let Err(e) = self.process_with_error_handling(ps) { log::error!("Error processing audio: {}", e); jack::Control::Quit } else { @@ -123,7 +179,6 @@ impl jack::ProcessHandler for ProcessHandler { impl ProcessHandler { fn process_with_error_handling( &mut self, - client: &jack::Client, ps: &jack::ProcessScope, ) -> Result<()> { // Process metronome and get beat timing information @@ -133,14 +188,19 @@ impl ProcessHandler { midi::process_events(self, ps)?; // Process audio + let controllers = MatrixControllers::new( + &self.post_record_controller, + &self.osc, + &self.persistence, + self.sample_rate, + ); self.track_matrix.process( - client, ps, &mut self.ports, &timing, - &mut self.osc, + &controllers, )?; Ok(()) } -} +} \ No newline at end of file diff --git a/audio_engine/src/state.rs b/audio_engine/src/state.rs index b41d786..026fe04 100644 --- a/audio_engine/src/state.rs +++ b/audio_engine/src/state.rs @@ -1,9 +1,11 @@ use serde::{Deserialize, Serialize}; +use std::collections::HashMap; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct State { pub connections: ConnectionState, pub metronome: MetronomeState, + pub track_volumes: TrackVolumes, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -20,6 +22,35 @@ pub struct MetronomeState { pub click_volume: f32, // 0.0 to 1.0 } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TrackVolumes { + volumes: HashMap, // "col_1_row_1" → volume (1-based naming) +} + +impl TrackVolumes { + pub fn new() -> Self { + Self { + volumes: HashMap::new(), + } + } + + pub fn set_volume(&mut self, column: usize, row: usize, volume: f32) { + let key = Self::make_key(column, row); + let clamped_volume = volume.clamp(0.0, 1.0); + self.volumes.insert(key, clamped_volume); + } + + fn make_key(column: usize, row: usize) -> String { + format!("col_{}_row_{}", column + 1, row + 1) + } +} + +impl Default for TrackVolumes { + fn default() -> Self { + Self::new() + } +} + impl Default for State { fn default() -> Self { Self { @@ -33,6 +64,7 @@ impl Default for State { frames_per_beat: 96000, // 120 BPM at 192kHz sample rate click_volume: 0.5, // Default 50% volume }, + 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 c5e8633..3d135e5 100644 --- a/audio_engine/src/track.rs +++ b/audio_engine/src/track.rs @@ -12,8 +12,11 @@ pub enum TrackState { Empty, Idle, Playing, - Recording, + Recording { + volume: f32, + }, RecordingAutoStop { + volume: f32, target_samples: usize, sync_offset: usize, }, @@ -32,7 +35,7 @@ impl Track { pub fn is_recording(&self) -> bool { matches!( self.current_state, - TrackState::Recording | TrackState::RecordingAutoStop { .. } + TrackState::Recording { .. } | TrackState::RecordingAutoStop { .. } ) } @@ -48,20 +51,40 @@ impl Track { self.audio_data.len() } - pub fn volume(&self) -> f32 { - self.volume + 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 + controllers.send_volume_update(new_volume)?; + } + TrackState::RecordingAutoStop { volume, .. } => { + *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 + } + } + Ok(()) } - pub fn set_volume(&mut self, volume: f32) { - self.volume = volume.clamp(0.0, 1.0); + pub fn record(&mut self, last_volume_setting: f32) { + self.next_state = TrackState::Recording { + volume: last_volume_setting, + }; } - pub fn record(&mut self) { - self.next_state = TrackState::Recording; - } - - pub fn record_auto_stop(&mut self, target_samples: usize, sync_offset: usize) { + 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, sync_offset, }; @@ -83,18 +106,13 @@ impl Track { self.audio_data.set_consolidated_buffer(buffer) } - pub fn handle_xrun( + pub fn handle_xrun( &mut self, beat_in_missed: Option, missed_frames: usize, chunk_factory: &mut impl ChunkFactory, - post_record_controller: PRC, - osc_controller: OC, - ) -> Result<()> - where - PRC: Fn(Arc, usize) -> Result<()>, - OC: Fn(TrackState) -> Result<()>, - { + controllers: &TrackControllers, + ) -> Result<()> { match beat_in_missed { None => { if self.is_recording() { @@ -111,7 +129,7 @@ impl Track { .append_silence(beat_offset as _, chunk_factory)?; } // Apply state transition at beat boundary - self.apply_state_transition(chunk_factory, post_record_controller, osc_controller)?; + self.apply_state_transition(chunk_factory, controllers)?; // Insert silence after beat with new state let frames_after_beat = missed_frames - beat_offset as usize; if frames_after_beat > 0 && self.is_recording() { @@ -124,20 +142,15 @@ impl Track { } /// Audio processing - pub fn process( + pub fn process( &mut self, playback_position: usize, beat_in_buffer: Option, input_buffer: &[f32], output_buffer: &mut [f32], chunk_factory: &mut impl ChunkFactory, - post_record_controller: PRC, - osc_controller: OC, - ) -> Result<()> - where - PRC: Fn(Arc, usize) -> Result<()>, - OC: Fn(TrackState) -> Result<()>, - { + controllers: &TrackControllers, + ) -> Result<()> { match beat_in_buffer { None => { // No beat in this buffer - process entire buffer with current state @@ -160,7 +173,7 @@ impl Track { } // Apply state transition at beat boundary - self.apply_state_transition(chunk_factory, post_record_controller, osc_controller)?; + self.apply_state_transition(chunk_factory, controllers)?; // Process samples after beat with new current state if (beat_index_in_buffer as usize) < output_buffer.len() { @@ -198,7 +211,7 @@ impl Track { TrackState::Empty | TrackState::Idle => { output_buffer.fill(0.0); } - TrackState::Recording => { + TrackState::Recording { .. } => { self.audio_data .append_samples(input_buffer, chunk_factory)?; output_buffer.fill(0.0); @@ -239,16 +252,11 @@ impl Track { /// Apply state transition from next_state to current_state /// Returns true if track should be consolidated and saved - fn apply_state_transition( + fn apply_state_transition( &mut self, chunk_factory: &mut impl ChunkFactory, - post_record_controller: PRC, - osc_controller: OC, - ) -> Result<()> - where - PRC: Fn(Arc, usize) -> Result<()>, - OC: Fn(TrackState) -> Result<()>, - { + controllers: &TrackControllers, + ) -> Result<()> { // Check for auto-stop recording completion and transition to playing if no other state transition if self.current_state == self.next_state { if let TrackState::RecordingAutoStop { target_samples, .. } = &self.current_state { @@ -260,11 +268,16 @@ impl Track { // remember recording before transition let was_recording = self.is_recording(); + let recording_volume = match &self.current_state { + TrackState::Recording { volume } => Some(*volume), + TrackState::RecordingAutoStop { volume, .. } => Some(*volume), + _ => None, + }; // Handle transitions that require setup match (&self.current_state, &self.next_state) { - (current_state, TrackState::Recording) - if !matches!(current_state, TrackState::Recording) => + (current_state, TrackState::Recording { .. }) + if !matches!(current_state, TrackState::Recording { .. }) => { // Starting manual recording - clear previous data and create new unconsolidated data self.audio_data = AudioData::new_unconsolidated(chunk_factory, 0)?; @@ -293,15 +306,25 @@ impl Track { // Apply the state transition if self.current_state != self.next_state { - osc_controller(self.next_state.clone())?; + controllers.send_state_update(self.next_state.clone())?; } self.current_state = self.next_state.clone(); - // Handle post-record processing + // Handle post-record processing and volume updates if was_recording && !self.is_recording() { + // Recording accepted - commit recording volume to track volume and update state + if let Some(recording_vol) = recording_volume { + self.volume = recording_vol; + controllers.update_track_volume(recording_vol)?; + } + let (chunk, sync_offset) = self.audio_data.get_chunk_for_processing()?; - post_record_controller(chunk, sync_offset)?; + controllers.send_post_record_request(chunk, sync_offset)?; + } else if was_recording && self.current_state == TrackState::Empty { + // Recording cancelled - send OSC with committed volume (Track.volume unchanged) + controllers.send_volume_update(self.volume)?; } + Ok(()) } -} +} \ No newline at end of file diff --git a/audio_engine/src/track_matrix.rs b/audio_engine/src/track_matrix.rs index cd0db77..270a376 100644 --- a/audio_engine/src/track_matrix.rs +++ b/audio_engine/src/track_matrix.rs @@ -2,7 +2,6 @@ use crate::*; pub struct TrackMatrix { chunk_factory: F, - post_record_controller: PostRecordController, columns: [Column; COLS], scratch_pad: Box<[f32]>, } @@ -12,39 +11,60 @@ impl TrackMatrix Result { let columns = std::array::from_fn(|_| Column::new(state.metronome.frames_per_beat)); Ok(Self { chunk_factory, - post_record_controller, columns, scratch_pad: vec![0.0; client.buffer_size() as usize].into_boxed_slice(), }) } - pub fn handle_record_button(&mut self, selected_column: usize, selected_row: usize) -> Result<()> { - self.columns[selected_column].handle_record_button(selected_row) + pub fn handle_record_button( + &mut self, + last_volume_setting: f32, + controllers: &TrackControllers, + ) -> Result<()> { + self.columns[controllers.column()].handle_record_button( + last_volume_setting, + controllers, + ) } - pub fn handle_play_button(&mut self, selected_column: usize, selected_row: usize) -> Result<()> { - self.columns[selected_column].handle_play_button(selected_row) + 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, selected_column: usize, selected_row: usize) -> Result<()> { - self.columns[selected_column].handle_clear_button(selected_row) + pub fn handle_clear_button( + &mut self, + controllers: &TrackControllers, + ) -> Result<()> { + self.columns[controllers.column()].handle_clear_button(controllers) + } + + pub fn handle_volume_update( + &mut self, + new_volume: f32, + controllers: &TrackControllers, + ) -> Result<()> { + self.columns[controllers.column()].handle_volume_update( + new_volume, + controllers, + ) } pub fn process( &mut self, - client: &jack::Client, ps: &jack::ProcessScope, ports: &mut JackPorts, timing: &BufferTiming, - osc_controller: &mut OscController, + controllers: &MatrixControllers, ) -> Result<()> { // Check for consolidation response - if let Some(response) = self.post_record_controller.try_recv_response() { + if let Some(response) = controllers.try_recv_post_record_response() { self.columns[response.column] .set_consolidated_buffer(response.row, response.consolidated_buffer)?; } @@ -52,21 +72,12 @@ 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, - |row, chunk, sync_offset| { - self.post_record_controller.send_request( - i, - row, - chunk, - sync_offset, - client.sample_rate(), - ) - }, - |row, state| { - osc_controller.track_state_changed(i, row, state) - }, + &controllers, )?; } } @@ -77,27 +88,18 @@ impl TrackMatrix