Skip to main content

oxihuman_export/
osc_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! OSC (Open Sound Control) message serialization.
6
7/// OSC argument types.
8#[allow(dead_code)]
9#[derive(Debug, Clone)]
10pub enum OscArg {
11    Int(i32),
12    Float(f32),
13    String(String),
14    Blob(Vec<u8>),
15}
16
17/// An OSC message.
18#[allow(dead_code)]
19#[derive(Debug, Clone)]
20pub struct OscMessage {
21    pub address: String,
22    pub args: Vec<OscArg>,
23}
24
25/// An OSC bundle.
26#[allow(dead_code)]
27pub struct OscBundle {
28    pub time_tag: u64,
29    pub messages: Vec<OscMessage>,
30}
31
32/// Pad bytes to 4-byte alignment.
33fn pad4(n: usize) -> usize {
34    (4 - n % 4) % 4
35}
36
37/// Encode a string with null terminator, padded to 4 bytes.
38fn encode_osc_string(s: &str) -> Vec<u8> {
39    let mut out = s.as_bytes().to_vec();
40    out.push(0);
41    let pad = pad4(out.len());
42    out.extend(std::iter::repeat_n(0, pad));
43    out
44}
45
46/// Serialize an OSC message to bytes.
47#[allow(dead_code)]
48pub fn serialize_osc_message(msg: &OscMessage) -> Vec<u8> {
49    let mut out = Vec::new();
50    out.extend_from_slice(&encode_osc_string(&msg.address));
51    let type_tag = {
52        let mut t = String::from(",");
53        for arg in &msg.args {
54            t.push(match arg {
55                OscArg::Int(_) => 'i',
56                OscArg::Float(_) => 'f',
57                OscArg::String(_) => 's',
58                OscArg::Blob(_) => 'b',
59            });
60        }
61        t
62    };
63    out.extend_from_slice(&encode_osc_string(&type_tag));
64    for arg in &msg.args {
65        match arg {
66            OscArg::Int(i) => out.extend_from_slice(&i.to_be_bytes()),
67            OscArg::Float(f) => out.extend_from_slice(&f.to_bits().to_be_bytes()),
68            OscArg::String(s) => out.extend_from_slice(&encode_osc_string(s)),
69            OscArg::Blob(b) => {
70                out.extend_from_slice(&(b.len() as u32).to_be_bytes());
71                out.extend_from_slice(b);
72                let pad = pad4(b.len());
73                out.extend(std::iter::repeat_n(0, pad));
74            }
75        }
76    }
77    out
78}
79
80/// Serialize an OSC bundle.
81#[allow(dead_code)]
82pub fn serialize_osc_bundle(bundle: &OscBundle) -> Vec<u8> {
83    let mut out = Vec::new();
84    out.extend_from_slice(&encode_osc_string("#bundle"));
85    out.extend_from_slice(&bundle.time_tag.to_be_bytes());
86    for msg in &bundle.messages {
87        let msg_bytes = serialize_osc_message(msg);
88        out.extend_from_slice(&(msg_bytes.len() as u32).to_be_bytes());
89        out.extend_from_slice(&msg_bytes);
90    }
91    out
92}
93
94/// Create an OSC message with no arguments.
95#[allow(dead_code)]
96pub fn new_osc_message(address: &str) -> OscMessage {
97    OscMessage {
98        address: address.to_string(),
99        args: Vec::new(),
100    }
101}
102
103/// Add an integer argument.
104#[allow(dead_code)]
105pub fn osc_add_int(msg: &mut OscMessage, val: i32) {
106    msg.args.push(OscArg::Int(val));
107}
108
109/// Add a float argument.
110#[allow(dead_code)]
111pub fn osc_add_float(msg: &mut OscMessage, val: f32) {
112    msg.args.push(OscArg::Float(val));
113}
114
115/// Add a string argument.
116#[allow(dead_code)]
117pub fn osc_add_string(msg: &mut OscMessage, val: &str) {
118    msg.args.push(OscArg::String(val.to_string()));
119}
120
121/// Number of arguments in a message.
122#[allow(dead_code)]
123pub fn osc_arg_count(msg: &OscMessage) -> usize {
124    msg.args.len()
125}
126
127/// Byte length of the serialized message.
128#[allow(dead_code)]
129pub fn osc_message_size(msg: &OscMessage) -> usize {
130    serialize_osc_message(msg).len()
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    #[test]
138    fn serialize_simple_address_aligned() {
139        let msg = new_osc_message("/foo");
140        let bytes = serialize_osc_message(&msg);
141        assert_eq!(bytes.len() % 4, 0);
142    }
143
144    #[test]
145    fn serialize_int_arg() {
146        let mut msg = new_osc_message("/test");
147        osc_add_int(&mut msg, 42);
148        let bytes = serialize_osc_message(&msg);
149        assert!(bytes.len() >= 8);
150        assert_eq!(bytes.len() % 4, 0);
151    }
152
153    #[test]
154    fn serialize_float_arg() {
155        let mut msg = new_osc_message("/level");
156        osc_add_float(&mut msg, 0.5);
157        let bytes = serialize_osc_message(&msg);
158        assert!(bytes.len() >= 8);
159    }
160
161    #[test]
162    fn serialize_string_arg() {
163        let mut msg = new_osc_message("/name");
164        osc_add_string(&mut msg, "hello");
165        let bytes = serialize_osc_message(&msg);
166        assert_eq!(bytes.len() % 4, 0);
167    }
168
169    #[test]
170    fn osc_arg_count_correct() {
171        let mut msg = new_osc_message("/multi");
172        osc_add_int(&mut msg, 1);
173        osc_add_float(&mut msg, 2.0);
174        assert_eq!(osc_arg_count(&msg), 2);
175    }
176
177    #[test]
178    fn osc_message_size_multiple_of_4() {
179        let mut msg = new_osc_message("/data");
180        osc_add_float(&mut msg, 1.0);
181        osc_add_int(&mut msg, 2);
182        let size = osc_message_size(&msg);
183        assert_eq!(size % 4, 0);
184    }
185
186    #[test]
187    fn bundle_starts_with_hash_bundle() {
188        let bundle = OscBundle {
189            time_tag: 1,
190            messages: vec![],
191        };
192        let bytes = serialize_osc_bundle(&bundle);
193        assert_eq!(&bytes[0..7], b"#bundle");
194    }
195
196    #[test]
197    fn bundle_contains_message() {
198        let msg = new_osc_message("/ping");
199        let bundle = OscBundle {
200            time_tag: 0,
201            messages: vec![msg],
202        };
203        let bytes = serialize_osc_bundle(&bundle);
204        assert!(bytes.len() > 16);
205    }
206
207    #[test]
208    fn pad4_values() {
209        assert_eq!(pad4(0), 0);
210        assert_eq!(pad4(1), 3);
211        assert_eq!(pad4(4), 0);
212        assert_eq!(pad4(5), 3);
213    }
214
215    #[test]
216    fn encode_osc_string_padded() {
217        let s = encode_osc_string("hi");
218        assert_eq!(s.len() % 4, 0);
219    }
220}