2025-06-13 16:35:19 +02:00

175 lines
6.0 KiB
Rust

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<OscMessage>,
}
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<OscMessage>,
framed_socket: Framed<PlatformStream, LengthDelimitedCodec>,
}
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<PlatformStream> {
UnixStream::connect(socket_path)
.await
.map_err(|_| LooperError::JackConnection(std::panic::Location::caller()))
}
#[cfg(windows)]
async fn connect_platform_stream(pipe_name: &str) -> Result<PlatformStream> {
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<rosc::OscPacket> {
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(),
}
}
}