fn pad4(v: &mut Vec<u8>) {
while v.len() % 4 != 0 {
v.push(0);
}
}
fn encode_osc_string(s: &str) -> Vec<u8> {
let mut v = s.as_bytes().to_vec();
v.push(0); pad4(&mut v);
v
}
pub fn encode_osc(addr: &str, args: &[f32]) -> Vec<u8> {
let mut packet = Vec::new();
packet.extend(encode_osc_string(addr));
let type_tag = format!(",{}", "f".repeat(args.len()));
packet.extend(encode_osc_string(&type_tag));
for &f in args {
packet.extend_from_slice(&f.to_be_bytes());
}
packet
}
pub struct OscSender {
socket: std::net::UdpSocket,
target: String,
}
impl OscSender {
pub fn new(host: &str, port: u16) -> anyhow::Result<Self> {
let socket = std::net::UdpSocket::bind("0.0.0.0:0")?;
socket.set_nonblocking(true)?;
let target = format!("{}:{}", host, port);
Ok(Self { socket, target })
}
pub fn send_state(
&self,
x: f32,
y: f32,
z: f32,
speed: f32,
lyapunov: f32,
) -> anyhow::Result<()> {
let packet = encode_osc("/sonify/state", &[x, y, z, speed, lyapunov]);
self.socket.send_to(&packet, &self.target)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pad4_already_aligned() {
let mut v = vec![1u8, 2, 3, 4];
pad4(&mut v);
assert_eq!(v.len(), 4, "already-aligned vec should not be padded");
}
#[test]
fn test_pad4_unaligned() {
let mut v = vec![1u8, 2, 3];
pad4(&mut v);
assert_eq!(v.len(), 4, "3-byte vec should be padded to 4");
assert_eq!(v[3], 0, "padding byte should be zero");
}
#[test]
fn test_encode_osc_string_null_terminated_and_padded() {
let s = encode_osc_string("/ab");
assert_eq!(s.len() % 4, 0, "encoded string must be a multiple of 4 bytes");
assert_eq!(s[3], 0, "byte after string should be null");
}
#[test]
fn test_encode_osc_string_empty() {
let s = encode_osc_string("");
assert_eq!(s.len() % 4, 0, "empty string encoding must be 4-byte aligned");
assert_eq!(s[0], 0, "first byte of empty string should be null terminator");
}
#[test]
fn test_encode_osc_no_args_packet_length() {
let packet = encode_osc("/x", &[]);
assert_eq!(packet.len() % 4, 0, "packet must be 4-byte aligned");
assert!(packet.len() >= 8, "packet must include address and type tag");
}
#[test]
fn test_encode_osc_single_float_correct_bytes() {
let packet = encode_osc("/f", &[1.0_f32]);
let expected = 1.0f32.to_be_bytes();
let float_bytes = &packet[packet.len() - 4..];
assert_eq!(float_bytes, &expected, "float should be encoded big-endian");
}
#[test]
fn test_encode_osc_packet_length_grows_with_args() {
let p0 = encode_osc("/s", &[]);
let p3 = encode_osc("/s", &[1.0, 2.0, 3.0]);
assert!(p3.len() > p0.len(), "packet with 3 floats should be longer than packet with 0");
assert_eq!(p3.len() % 4, 0, "packet must remain 4-byte aligned");
}
}