/// OSC server and sender /// /// Listen port : 9000 /// Send port : 9001 /// /// Incoming address map (/deck//...): /// play, pause, stop /// seek f (0.0–1.0) /// fader f (0.0–1.0) → wpctl set-volume on stream node /// pitch f (semitones) → pw-cli Props on filter node /// tempo f (multiplier)→ pw-cli Props on filter node /// cue/set, cue/goto /// /// Outgoing: /// /deck//position f /// /deck//vu f f /// /deck//loaded s use std::{net::UdpSocket, thread}; use anyhow::Result; use crossbeam_channel::Sender; use rosc::{encoder, OscMessage, OscPacket, OscType}; use crate::audio::AudioCmd; use crate::deck::DeckId; pub const LISTEN_PORT: u16 = 9000; pub const SEND_PORT: u16 = 9001; /// Messages that require effect chain interaction (pitch/tempo/fader) /// are sent back through AppCmd so the app can call the appropriate /// EffectChain / wpctl method. #[derive(Debug)] pub enum OscAppCmd { Audio(AudioCmd), SetPitch { deck: DeckId, semitones: f32 }, SetTempo { deck: DeckId, multiplier: f32 }, SetFader { deck: DeckId, value: f32 }, } pub fn start_osc_server(cmd_tx: Sender) -> Result<()> { let socket = UdpSocket::bind(format!("0.0.0.0:{}", LISTEN_PORT))?; tracing::info!("OSC listening on :{}", LISTEN_PORT); thread::Builder::new() .name("osc-server".into()) .spawn(move || { let mut buf = [0u8; 4096]; loop { match socket.recv_from(&mut buf) { Ok((size, _)) => { if let Ok((_, pkt)) = rosc::decoder::decode_udp(&buf[..size]) { handle_packet(pkt, &cmd_tx); } } Err(e) => tracing::error!("OSC recv: {e}"), } } })?; Ok(()) } fn handle_packet(pkt: OscPacket, tx: &Sender) { match pkt { OscPacket::Message(msg) => handle_msg(msg, tx), OscPacket::Bundle(b) => b.content.into_iter().for_each(|p| handle_packet(p, tx)), } } fn handle_msg(msg: OscMessage, tx: &Sender) { let parts: Vec<&str> = msg.addr.trim_start_matches('/').split('/').collect(); if parts.len() < 3 || parts[0] != "deck" { return; } let n: usize = parts[1].parse().unwrap_or(99); let deck = match DeckId::from_index(n) { Some(d) => d, None => return }; let cmd = match (parts[2], parts.get(3).copied()) { ("play", None) => Some(OscAppCmd::Audio(AudioCmd::Play(deck))), ("pause", None) => Some(OscAppCmd::Audio(AudioCmd::Pause(deck))), ("stop", None) => Some(OscAppCmd::Audio(AudioCmd::Stop(deck))), ("seek", None) => f32_arg(&msg.args, 0).map(|v| OscAppCmd::Audio(AudioCmd::Seek { deck, position: v })), ("fader", None) => f32_arg(&msg.args, 0).map(|v| OscAppCmd::SetFader { deck, value: v }), ("pitch", None) => f32_arg(&msg.args, 0).map(|v| OscAppCmd::SetPitch { deck, semitones: v }), ("tempo", None) => f32_arg(&msg.args, 0).map(|v| OscAppCmd::SetTempo { deck, multiplier: v }), ("cue", Some("set")) => Some(OscAppCmd::Audio(AudioCmd::SetCue(deck))), ("cue", Some("goto")) => Some(OscAppCmd::Audio(AudioCmd::GotoCue(deck))), _ => None, }; if let Some(c) = cmd { let _ = tx.send(c); } } fn f32_arg(args: &[OscType], i: usize) -> Option { match args.get(i)? { OscType::Float(f) => Some(*f), OscType::Double(d) => Some(*d as f32), OscType::Int(i) => Some(*i as f32), _ => None, } } pub fn send_osc(addr: &str, args: Vec) { let Ok(sock) = UdpSocket::bind("0.0.0.0:0") else { return }; let msg = OscPacket::Message(OscMessage { addr: addr.to_string(), args }); if let Ok(buf) = encoder::encode(&msg) { let _ = sock.send_to(&buf, format!("127.0.0.1:{}", SEND_PORT)); } }