Skip to main content

oaat_core/
message.rs

1use serde::{Deserialize, Serialize};
2
3use crate::format::{AudioFormat, ChannelLayout};
4
5/// All control protocol messages, framed as length-prefixed JSON over TCP.
6#[derive(Debug, Clone, Serialize, Deserialize)]
7#[serde(tag = "type", rename_all = "snake_case")]
8pub enum Message {
9    // -- Handshake --
10    Hello(Hello),
11    HelloAck(HelloAck),
12
13    // -- Format negotiation --
14    FormatPropose(FormatPropose),
15    FormatAccept(FormatAccept),
16    FormatCounter(FormatCounter),
17    FormatReject(FormatReject),
18
19    // -- Playback --
20    Play(Play),
21    Pause(Pause),
22    Stop(Stop),
23    Seek(Seek),
24
25    // -- Volume --
26    VolumeSet(VolumeSet),
27    VolumeGet(VolumeGet),
28    VolumeReport(VolumeReport),
29    Mute(Mute),
30
31    // -- Metadata --
32    Metadata(Metadata),
33
34    // -- Zone --
35    ZoneAssign(ZoneAssign),
36    ZoneUpdate(ZoneUpdate),
37    ZoneRelease(ZoneRelease),
38    ZoneAck(ZoneAck),
39
40    // -- Gapless --
41    NextTrackPrepare(NextTrackPrepare),
42    NextTrackReady(NextTrackReady),
43    NextTrackReformat(NextTrackReformat),
44
45    // -- Error --
46    Error(ErrorMsg),
47}
48
49// -- Handshake types --
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct Hello {
53    pub protocol_version: u32,
54    pub controller_id: String,
55    pub controller_name: String,
56    pub clock_port: u16,
57    #[serde(default)]
58    pub features: Vec<String>,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct HelloAck {
63    pub protocol_version: u32,
64    pub endpoint_id: String,
65    pub endpoint_name: String,
66    pub capabilities: EndpointCapabilities,
67    pub audio_port: u16,
68    pub clock_port: u16,
69    pub buffer_size_ms: u32,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct EndpointCapabilities {
74    pub pcm_max_rate: u32,
75    pub pcm_max_bits: u8,
76    #[serde(default)]
77    pub dsd_max_rate: Option<u16>,
78    pub channels_max: u8,
79    pub formats: Vec<AudioFormat>,
80    #[serde(default)]
81    pub volume: Option<VolumeCapability>,
82    #[serde(default)]
83    pub gapless: bool,
84    #[serde(default)]
85    pub seek: bool,
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct VolumeCapability {
90    #[serde(rename = "type")]
91    pub vol_type: VolumeType,
92    pub range: [u8; 2],
93    pub step: u8,
94}
95
96#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
97#[serde(rename_all = "snake_case")]
98pub enum VolumeType {
99    Hw,
100    Sw,
101    Fixed,
102    None,
103}
104
105// -- Format negotiation --
106
107#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct FormatPropose {
109    pub stream_id: String,
110    pub format: AudioFormat,
111    pub sample_rate: u32,
112    pub channels: u8,
113    pub channel_layout: ChannelLayout,
114    pub bits_per_sample: u8,
115    #[serde(default)]
116    pub dsd_rate: Option<u16>,
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct FormatAccept {
121    pub stream_id: String,
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct FormatCounter {
126    pub stream_id: String,
127    pub format: AudioFormat,
128    pub sample_rate: u32,
129    pub channels: u8,
130    pub channel_layout: ChannelLayout,
131    pub bits_per_sample: u8,
132    #[serde(default)]
133    pub dsd_rate: Option<u16>,
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct FormatReject {
138    pub stream_id: String,
139    pub reason: String,
140}
141
142// -- Playback --
143
144#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct Play {
146    pub stream_id: String,
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct Pause {
151    pub stream_id: String,
152}
153
154#[derive(Debug, Clone, Serialize, Deserialize)]
155pub struct Stop {
156    pub stream_id: String,
157}
158
159#[derive(Debug, Clone, Serialize, Deserialize)]
160pub struct Seek {
161    pub stream_id: String,
162    pub position_ms: u64,
163}
164
165// -- Volume --
166
167#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct VolumeSet {
169    pub level: u8,
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct VolumeGet {}
174
175#[derive(Debug, Clone, Serialize, Deserialize)]
176pub struct VolumeReport {
177    pub level: u8,
178    pub muted: bool,
179}
180
181#[derive(Debug, Clone, Serialize, Deserialize)]
182pub struct Mute {
183    pub muted: bool,
184}
185
186// -- Metadata --
187
188#[derive(Debug, Clone, Serialize, Deserialize)]
189pub struct Metadata {
190    pub track: TrackMetadata,
191}
192
193#[derive(Debug, Clone, Serialize, Deserialize)]
194pub struct TrackMetadata {
195    pub title: String,
196    pub artist: String,
197    pub album: String,
198    pub duration_ms: u64,
199    #[serde(default)]
200    pub artwork_url: Option<String>,
201    #[serde(default)]
202    pub format: Option<String>,
203}
204
205// -- Zone management --
206
207#[derive(Debug, Clone, Serialize, Deserialize)]
208pub struct ZoneAssign {
209    pub zone_id: String,
210    pub endpoint_id: String,
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize)]
214pub struct ZoneUpdate {
215    pub zone_id: String,
216    pub endpoint_ids: Vec<String>,
217}
218
219#[derive(Debug, Clone, Serialize, Deserialize)]
220pub struct ZoneRelease {
221    pub zone_id: String,
222    pub endpoint_id: String,
223}
224
225#[derive(Debug, Clone, Serialize, Deserialize)]
226pub struct ZoneAck {
227    pub zone_id: String,
228    pub endpoint_id: String,
229    pub accepted: bool,
230    #[serde(default)]
231    pub reason: Option<String>,
232}
233
234// -- Gapless --
235
236#[derive(Debug, Clone, Serialize, Deserialize)]
237pub struct NextTrackPrepare {
238    pub stream_id: String,
239    pub format: AudioFormat,
240    pub sample_rate: u32,
241    pub channels: u8,
242    pub channel_layout: ChannelLayout,
243    pub bits_per_sample: u8,
244}
245
246#[derive(Debug, Clone, Serialize, Deserialize)]
247pub struct NextTrackReady {
248    pub stream_id: String,
249}
250
251#[derive(Debug, Clone, Serialize, Deserialize)]
252pub struct NextTrackReformat {
253    pub stream_id: String,
254    pub format: AudioFormat,
255    pub sample_rate: u32,
256}
257
258// -- Error --
259
260#[derive(Debug, Clone, Serialize, Deserialize)]
261pub struct ErrorMsg {
262    pub code: u32,
263    pub message: String,
264}
265
266// -- Framing --
267
268impl Message {
269    pub fn encode_framed(&self) -> Vec<u8> {
270        let json = serde_json::to_vec(self).expect("message serialization cannot fail");
271        let len = json.len() as u32;
272        let mut buf = Vec::with_capacity(4 + json.len());
273        buf.extend_from_slice(&len.to_be_bytes());
274        buf.extend_from_slice(&json);
275        buf
276    }
277
278    pub fn decode_json(json: &[u8]) -> Result<Self, serde_json::Error> {
279        serde_json::from_slice(json)
280    }
281}
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286
287    #[test]
288    fn hello_roundtrip() {
289        let msg = Message::Hello(Hello {
290            protocol_version: 1,
291            controller_id: "abc".into(),
292            controller_name: "Tune Server".into(),
293            clock_port: 9742,
294            features: vec!["flac_transport".into(), "dsd_native".into()],
295        });
296        let framed = msg.encode_framed();
297        let len = u32::from_be_bytes(framed[..4].try_into().unwrap()) as usize;
298        assert_eq!(len, framed.len() - 4);
299        let decoded = Message::decode_json(&framed[4..]).unwrap();
300        match decoded {
301            Message::Hello(h) => {
302                assert_eq!(h.controller_name, "Tune Server");
303                assert_eq!(h.features.len(), 2);
304            }
305            _ => panic!("wrong variant"),
306        }
307    }
308
309    #[test]
310    fn format_propose_json() {
311        let msg = Message::FormatPropose(FormatPropose {
312            stream_id: "abc123".into(),
313            format: AudioFormat::PcmS24le,
314            sample_rate: 192000,
315            channels: 2,
316            channel_layout: ChannelLayout::Stereo,
317            bits_per_sample: 24,
318            dsd_rate: None,
319        });
320        let json = serde_json::to_string_pretty(&msg).unwrap();
321        assert!(json.contains("format_propose"));
322        assert!(json.contains("192000"));
323    }
324}