Files
djdeck/src/osc.rs

109 lines
3.8 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// 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));
}
}