use crate::*; use tokio_util::codec::{Framed, LengthDelimitedCodec}; use futures::SinkExt; #[cfg(unix)] use tokio::net::UnixStream; #[cfg(windows)] use tokio::net::windows::named_pipe::ClientOptions; #[cfg(unix)] type PlatformStream = UnixStream; #[cfg(windows)] type PlatformStream = tokio::net::windows::named_pipe::NamedPipeClient; #[derive(Debug, Clone)] enum OscMessage { TrackStateChanged { column: usize, row: usize, state: TrackState, }, SelectedColumnChanged { column: usize, }, SelectedRowChanged { row: usize, }, } #[derive(Debug)] pub struct OscController { sender: kanal::Sender, } impl OscController { pub fn track_state_changed( &self, column: usize, row: usize, state: TrackState, ) -> Result<()> { let message = OscMessage::TrackStateChanged { column, row, state }; 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) { 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_row_changed(&self, row: usize) -> Result<()> { let message = OscMessage::SelectedRowChanged { row }; 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 struct Osc { receiver: kanal::AsyncReceiver, framed_socket: Framed, } impl Osc { /// # Platform Notes /// - Unix: socket_path should be a file path (e.g., "/tmp/fcb_looper.sock") /// - Windows: socket_path will be converted to named pipe format (e.g., "fcb_looper" -> "\\.\pipe\fcb_looper") pub async fn new(socket_path: &str) -> Result<(Self, OscController)> { // Connect to platform-specific IPC let stream = Self::connect_platform_stream(socket_path).await?; // Wrap with length-delimited codec let framed_socket = Framed::new(stream, LengthDelimitedCodec::new()); // Create communication channel let (sender, receiver) = kanal::bounded(64); let receiver = receiver.to_async(); let controller = OscController { sender }; let server = Self { receiver, framed_socket, }; Ok((server, controller)) } #[cfg(unix)] async fn connect_platform_stream(socket_path: &str) -> Result { UnixStream::connect(socket_path) .await .map_err(|_| LooperError::JackConnection(std::panic::Location::caller())) } #[cfg(windows)] async fn connect_platform_stream(pipe_name: &str) -> Result { ClientOptions::new() .open(pipe_name) .map_err(|_| LooperError::JackConnection(std::panic::Location::caller())) } pub async fn run(&mut self) -> Result<()> { while let Ok(message) = self.receiver.recv().await { if let Err(e) = self.send_osc_message(message).await { log::error!("Failed to send OSC message: {}", e); // Continue processing other messages } } Ok(()) } async fn send_osc_message(&mut self, message: OscMessage) -> Result<()> { let osc_packet = self.message_to_osc_packet(message)?; let osc_bytes = rosc::encoder::encode(&osc_packet) .map_err(|_| LooperError::StateSave(std::panic::Location::caller()))?; self.framed_socket .send(bytes::Bytes::from(osc_bytes)) .await .map_err(|_| LooperError::StateSave(std::panic::Location::caller()))?; Ok(()) } fn message_to_osc_packet(&self, message: OscMessage) -> Result { match message { OscMessage::TrackStateChanged { column, row, state } => { let osc_state = self.track_state_to_osc_string(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(osc_state)], })) } OscMessage::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 })) } OscMessage::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 })) } } } fn track_state_to_osc_string(&self, state: TrackState) -> String { match state { TrackState::Empty => "empty".to_string(), TrackState::Idle => "ready".to_string(), // Assuming track has audio data TrackState::Recording | TrackState::RecordingAutoStop { .. } => "recording".to_string(), TrackState::Playing => "playing".to_string(), } } }