Files
djdeck/src/osc.rs

109 lines
3.8 KiB
Rust
Raw Normal View History

2026-03-05 15:41:53 +01:00
/// OSC server and sender
///
/// Listen port : 9000
/// Send port : 9001
///
/// Incoming address map (/deck/<n>/...):
/// play, pause, stop
/// seek f (0.01.0)
/// fader f (0.01.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/<n>/position f
/// /deck/<n>/vu f f
/// /deck/<n>/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<OscAppCmd>) -> 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<OscAppCmd>) {
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<OscAppCmd>) {
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<f32> {
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<OscType>) {
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));
}
}