use crate::*; #[derive(Debug, Clone)] pub enum Message { TrackStateChanged { column: usize, row: usize, state: TrackState, }, TrackVolumeChanged { column: usize, row: usize, volume: f32, }, SelectedColumnChanged { column: usize, }, SelectedRowChanged { row: usize, }, MetronomePosition { position: f32, // 0.0 - 1.0 position within current beat }, Tempo { bpm: f32, }, } impl Message { pub fn from_osc_packet(packet: rosc::OscPacket) -> Result> { match packet { rosc::OscPacket::Message(msg) => Self::from_osc_message(msg), rosc::OscPacket::Bundle(_) => { // For now, we don't handle bundles Ok(None) } } } pub fn apply_to_state(&self, state: &mut State) { match self { Message::TrackStateChanged { column, row, state: track_state, } => { state.set_track_state(*column, *row, track_state.clone()); } Message::SelectedColumnChanged { column } => { state.set_selected_column(*column); } Message::SelectedRowChanged { row } => { state.set_selected_row(*row); } Message::TrackVolumeChanged { column, row, volume, } => { state.set_track_volume(*column, *row, *volume); } Message::MetronomePosition { position } => { state.set_metronome_position(*position); } Message::Tempo { bpm } => { state.set_tempo(*bpm); } } } fn from_osc_message(msg: rosc::OscMessage) -> Result> { let rosc::OscMessage { addr, args } = msg; if addr.starts_with("/looper/cell/") && addr.ends_with("/state") { // Parse: /looper/cell/{column}/{row}/state let parts: Vec<&str> = addr.split('/').collect(); if parts.len() != 6 { return Err(Error::AddressParseError(addr)); } let column: usize = parts[3].parse::().address_part_result("column")? - 1; // Convert to 0-based let row: usize = parts[4].parse::().address_part_result("row")? - 1; // Convert to 0-based if let Some(rosc::OscType::String(state_str)) = args.first() { let state = state_str.parse::().unwrap_or(TrackState::Empty); log::trace!("TrackStateChanged: column={column}, row={row}, state={state}"); return Ok(Some(Message::TrackStateChanged { column, row, state })); } } else if addr.starts_with("/looper/cell/") && addr.ends_with("/volume") { // Parse: /looper/cell/{column}/{row}/volume let parts: Vec<&str> = addr.split('/').collect(); if parts.len() != 6 { return Err(Error::AddressParseError(addr)); } let column: usize = parts[3].parse::().address_part_result("column")? - 1; // Convert to 0-based let row: usize = parts[4].parse::().address_part_result("row")? - 1; // Convert to 0-based if let Some(rosc::OscType::Float(volume)) = args.first() { log::trace!("TrackVolumeChanged: column={column}, row={row}, volume={volume}"); return Ok(Some(Message::TrackVolumeChanged { column, row, volume: *volume, })); } } else if addr == "/looper/selected/column" { if let Some(rosc::OscType::Int(column_1based)) = args.first() { log::trace!("SelectedColumnChanged: column={column_1based}"); let column = (*column_1based as usize).saturating_sub(1); // Convert to 0-based return Ok(Some(Message::SelectedColumnChanged { column })); } } else if addr == "/looper/selected/row" { if let Some(rosc::OscType::Int(row_1based)) = args.first() { log::trace!("SelectedRowChanged: row={row_1based}"); 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() { log::trace!("MetronomePosition: position={position}"); return Ok(Some(Message::MetronomePosition { position: *position, })); } } else if addr == "/looper/tempo" { if let Some(rosc::OscType::Float(bpm)) = args.first() { log::trace!("Tempo: bpm={bpm}"); return Ok(Some(Message::Tempo { bpm: *bpm })); } } // 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)], }) } Message::Tempo { bpm } => { let address = "/looper/tempo".to_string(); rosc::OscPacket::Message(rosc::OscMessage { addr: address, args: vec![rosc::OscType::Float(bpm)], }) } } } }