175 lines
6.0 KiB
Rust
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(),
|
|
}
|
|
}
|
|
} |