109 lines
3.8 KiB
Rust
109 lines
3.8 KiB
Rust
|
|
/// OSC server and sender
|
|||
|
|
///
|
|||
|
|
/// Listen port : 9000
|
|||
|
|
/// Send port : 9001
|
|||
|
|
///
|
|||
|
|
/// Incoming address map (/deck/<n>/...):
|
|||
|
|
/// 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/<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));
|
|||
|
|
}
|
|||
|
|
}
|