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, }, UpdateMetronomeVolume { volume: f32, }, UpdateMetronomeFramesPerBeat { frames_per_beat: usize, }, } 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 fn update_metronome_volume(&self, volume: f32) -> Result<()> { let update = PersistenceUpdate::UpdateMetronomeVolume { 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 fn update_metronome_frames_per_beat(&self, frames_per_beat: usize) -> Result<()> { let update = PersistenceUpdate::UpdateMetronomeFramesPerBeat { frames_per_beat }; 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: watch::Sender, notification_rx: broadcast::Receiver, update_rx: kanal::AsyncReceiver, state_file_path: PathBuf, } impl PersistenceManager { pub fn new( args: &Args, notification_rx: broadcast::Receiver, ) -> (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, _) = 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, notification_rx, update_rx, state_file_path, }; (manager, controller) } pub fn state(&self) -> watch::Receiver { self.state.subscribe() } pub async fn run(&mut self) -> Result<()> { 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()?; } PersistenceUpdate::UpdateMetronomeVolume { volume } => { self.state .send_modify(|state| state.metronome.click_volume = volume); self.save_to_disk()?; } PersistenceUpdate::UpdateMetronomeFramesPerBeat { frames_per_beat } => { self.state .send_modify(|state| state.metronome.frames_per_beat = frames_per_beat); self.save_to_disk()?; } } Ok(()) } fn handle_port_connection( &mut self, port_a: String, port_b: String, connected: bool, ) -> Result<()> { // Determine which port is ours and which is external let (our_port, external_port) = if port_a.starts_with("looper:") { (port_a, port_b) } else if port_b.starts_with("looper:") { (port_b, port_a) } else { // Neither port is ours, ignore return Ok(()); }; // Update the connections state let our_port_name = our_port.strip_prefix("looper:").unwrap_or(&our_port); self.state.send_if_modified(|state| { let connections = &mut state.connections; 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, _ => { 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 } }); self.save_to_disk()?; Ok(()) } fn load_from_disk(path: &PathBuf) -> Result { match std::fs::read_to_string(path) { Ok(content) => { let state: State = serde_json::from_str(&content) .map_err(|_| LooperError::StateLoad(std::panic::Location::caller()))?; log::info!("Loaded state from {:?}", path); Ok(state) } Err(_) => { log::info!("Could not load state from {:?}. Using defaults.", path); Err(LooperError::StateLoad(std::panic::Location::caller())) } } } fn save_to_disk(&self) -> Result<()> { 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) .map_err(|_| LooperError::StateSave(std::panic::Location::caller()))?; log::debug!("Saved state to {:?}", self.state_file_path); Ok(()) } }