//! OSC (Open Sound Control) message receiver. //! //! Listens for UDP packets containing OSC messages and translates them into //! engine commands. Runs in a dedicated thread, forwarding parsed messages //! to the audio engine for evaluation. //! //! # Message Format //! //! OSC arguments are interpreted as key-value pairs and converted to a path //! string for the engine. Arguments are processed in pairs: odd positions are //! keys (must be strings), even positions are values. //! //! ```text //! OSC: /play ["sound", "kick", "note", 60, "amp", 0.8] //! → Engine path: "sound/kick/note/60/amp/0.8" //! ``` //! //! # Protocol //! //! - Transport: UDP //! - Default bind: `0.0.0.0:` (all interfaces) //! - Supports both single messages and bundles (bundles are flattened) use rosc::{OscMessage, OscPacket, OscType}; use std::net::UdpSocket; use std::sync::{Arc, Mutex}; use crate::Engine; /// Maximum UDP packet size for incoming OSC messages. const BUFFER_SIZE: usize = 4096; /// Starts the OSC receiver loop on the specified port. /// /// Binds to all interfaces (`0.0.0.0`) and blocks indefinitely, processing /// incoming messages. Intended to be spawned in a dedicated thread. /// /// # Panics /// /// Panics if the UDP socket cannot be bound (e.g., port already in use). pub fn run(engine: Arc>, port: u16) { let addr = format!("0.0.0.0:{port}"); let socket = UdpSocket::bind(&addr).expect("failed to bind OSC socket"); let mut buf = [0u8; BUFFER_SIZE]; loop { match socket.recv_from(&mut buf) { Ok((size, _addr)) => { if let Ok(packet) = rosc::decoder::decode_udp(&buf[..size]) { handle_packet(&engine, &packet.1); } } Err(e) => { eprintln!("OSC recv error: {e}"); } } } } /// Recursively processes an OSC packet, handling both messages and bundles. fn handle_packet(engine: &Arc>, packet: &OscPacket) { match packet { OscPacket::Message(msg) => handle_message(engine, msg), OscPacket::Bundle(bundle) => { for p in &bundle.content { handle_packet(engine, p); } } } } /// Converts an OSC message to a path string and evaluates it on the engine. fn handle_message(engine: &Arc>, msg: &OscMessage) { let path = osc_to_path(msg); if !path.is_empty() { if let Ok(mut e) = engine.lock() { e.evaluate(&path); } } } /// Converts OSC message arguments to a slash-separated path string. /// /// Arguments are processed as key-value pairs. Keys must be strings; /// non-string keys cause the pair to be skipped. Values are converted /// to their string representation. fn osc_to_path(msg: &OscMessage) -> String { let mut parts: Vec = Vec::new(); let args = &msg.args; let mut i = 0; while i + 1 < args.len() { let key = match &args[i] { OscType::String(s) => s.clone(), _ => { i += 1; continue; } }; let val = arg_to_string(&args[i + 1]); parts.push(key); parts.push(val); i += 2; } parts.join("/") } /// Converts an OSC type to its string representation. fn arg_to_string(arg: &OscType) -> String { match arg { OscType::Int(v) => v.to_string(), OscType::Float(v) => v.to_string(), OscType::Double(v) => v.to_string(), OscType::Long(v) => v.to_string(), OscType::String(s) => s.clone(), OscType::Bool(b) => if *b { "1" } else { "0" }.to_string(), _ => String::new(), } }