snapcast_client/decoder/
opus.rs1use anyhow::{Result, bail};
10use opus_decoder::OpusDecoder as OpusDec;
11use snapcast_proto::SampleFormat;
12use snapcast_proto::message::codec_header::CodecHeader;
13
14use crate::decoder::Decoder;
15
16const OPUS_ID: u32 = 0x4F50_5553;
17const MAX_FRAME_SIZE: usize = 2880;
18
19fn parse_opus_header(payload: &[u8]) -> Result<SampleFormat> {
21 if payload.len() >= 19 && &payload[..8] == b"OpusHead" {
22 let channels = payload[9] as u16;
23 let rate = u32::from_le_bytes(payload[12..16].try_into().unwrap());
24 if channels == 0 || rate == 0 {
25 bail!("invalid OpusHead: rate={rate}, channels={channels}");
26 }
27 return Ok(SampleFormat::new(rate, 16, channels));
28 }
29
30 if payload.len() < 12 {
31 bail!(
32 "Opus header too small ({} bytes, need >= 12)",
33 payload.len()
34 );
35 }
36 let id = u32::from_le_bytes(payload[0..4].try_into().unwrap());
37 if id != OPUS_ID {
38 bail!("not an Opus header (expected 0x{OPUS_ID:08X}, got 0x{id:08X})");
39 }
40 let rate = u32::from_le_bytes(payload[4..8].try_into().unwrap());
41 let bits = u16::from_le_bytes(payload[8..10].try_into().unwrap());
42 let channels = u16::from_le_bytes(payload[10..12].try_into().unwrap());
43 Ok(SampleFormat::new(rate, bits, channels))
44}
45
46pub struct OpusDecoder {
48 decoder: OpusDec,
49 sample_format: SampleFormat,
50 pcm_buf: Vec<i16>,
51}
52
53impl Decoder for OpusDecoder {
54 fn set_header(&mut self, header: &CodecHeader) -> Result<SampleFormat> {
55 let new = create(header)?;
56 *self = new;
57 Ok(self.sample_format)
58 }
59
60 fn decode(&mut self, data: &mut Vec<u8>) -> Result<bool> {
61 if data.is_empty() {
62 return Ok(false);
63 }
64 tracing::trace!(codec = "opus", input_bytes = data.len(), "decode");
65 let decoded_samples = match self
66 .decoder
67 .decode(data.as_slice(), &mut self.pcm_buf, false)
68 {
69 Ok(n) => n,
70 Err(e) => {
71 tracing::warn!(codec = "opus", error = %e, "decode failed");
72 return Ok(false);
73 }
74 };
75 let total_samples = decoded_samples * self.sample_format.channels() as usize;
76 let mut out = Vec::with_capacity(total_samples * 2);
77 for &s in &self.pcm_buf[..total_samples] {
78 out.extend_from_slice(&s.to_le_bytes());
79 }
80 *data = out;
81 Ok(true)
82 }
83}
84
85pub fn create(header: &CodecHeader) -> Result<OpusDecoder> {
87 let sf = parse_opus_header(&header.payload)?;
88 let dec = OpusDec::new(sf.rate(), sf.channels() as usize)
89 .map_err(|e| anyhow::anyhow!("failed to create Opus decoder: {e}"))?;
90 Ok(OpusDecoder {
91 decoder: dec,
92 sample_format: sf,
93 pcm_buf: vec![0i16; MAX_FRAME_SIZE * sf.channels() as usize],
94 })
95}
96#[cfg(test)]
97mod tests {
98 use super::*;
99
100 fn opus_header(rate: u32, bits: u16, channels: u16) -> Vec<u8> {
101 let mut h = Vec::new();
102 h.extend_from_slice(&OPUS_ID.to_le_bytes());
103 h.extend_from_slice(&rate.to_le_bytes());
104 h.extend_from_slice(&bits.to_le_bytes());
105 h.extend_from_slice(&channels.to_le_bytes());
106 h
107 }
108
109 fn opus_head(rate: u32, channels: u8) -> Vec<u8> {
110 let mut h = Vec::new();
111 h.extend_from_slice(b"OpusHead");
112 h.push(1);
113 h.push(channels);
114 h.extend_from_slice(&0u16.to_le_bytes());
115 h.extend_from_slice(&rate.to_le_bytes());
116 h.extend_from_slice(&0u16.to_le_bytes());
117 h.push(0);
118 h
119 }
120
121 #[test]
122 fn parse_header_48000_16_2() {
123 let sf = parse_opus_header(&opus_header(48000, 16, 2)).unwrap();
124 assert_eq!(sf.rate(), 48000);
125 assert_eq!(sf.bits(), 16);
126 assert_eq!(sf.channels(), 2);
127 }
128
129 #[test]
130 fn parse_opus_head_48000_2() {
131 let sf = parse_opus_header(&opus_head(48000, 2)).unwrap();
132 assert_eq!(sf.rate(), 48000);
133 assert_eq!(sf.bits(), 16);
134 assert_eq!(sf.channels(), 2);
135 }
136
137 #[test]
138 fn parse_header_too_small() {
139 assert!(parse_opus_header(&[0; 8]).is_err());
140 }
141
142 #[test]
143 fn parse_header_bad_magic() {
144 let mut h = opus_header(48000, 16, 2);
145 h[0] = 0xFF;
146 assert!(parse_opus_header(&h).is_err());
147 }
148
149 #[test]
150 fn create_decoder_48000_16_2() {
151 let header = CodecHeader {
152 codec: "opus".into(),
153 payload: opus_header(48000, 16, 2),
154 };
155 let dec = create(&header);
156 assert!(dec.is_ok());
157 }
158}