Skip to main content

koan_core/audio/
opus.rs

1//! Opus decoding bridge — wraps `opus-decoder` to decode packets from
2//! Symphonia's Ogg demuxer. Symphonia 0.5 can identify Opus streams but
3//! has no codec implementation; this module fills that gap.
4//!
5//! Opus always decodes to 48 kHz, regardless of the internal sample rate.
6//! Channel count is read from the Opus identification header (first packet).
7
8use opus_decoder::OpusDecoder;
9use symphonia::core::codecs::CodecParameters;
10
11/// Errors from the Opus decode bridge.
12#[derive(Debug, thiserror::Error)]
13pub enum OpusError {
14    #[error("opus decoder init failed: {0}")]
15    Init(String),
16    #[error("opus decode error: {0}")]
17    Decode(String),
18    #[error("invalid opus header")]
19    InvalidHeader,
20}
21
22/// State for decoding an Opus stream via Symphonia packets.
23pub struct OpusBridge {
24    decoder: OpusDecoder,
25    channels: usize,
26    /// Reusable buffer for decoded f32 PCM (interleaved).
27    pcm_buf: Vec<f32>,
28    /// Samples per channel to skip from the start (Opus pre-skip).
29    pre_skip: u32,
30    /// Samples per channel already skipped.
31    skipped: u32,
32    /// Number of Ogg packets seen — first two are header/comment.
33    packet_index: u64,
34}
35
36/// Opus identification header layout (first 19 bytes minimum):
37///   0..8   "OpusHead"
38///   8      version (1)
39///   9      channel count
40///  10..12  pre-skip (little-endian u16)
41///  12..16  input sample rate (little-endian u32, informational only)
42///  16..18  output gain (little-endian i16)
43///  18      channel mapping family
44const OPUS_HEAD_MAGIC: &[u8] = b"OpusHead";
45const OPUS_HEAD_MIN_LEN: usize = 19;
46
47/// Parse the Opus identification header to extract channel count and pre-skip.
48fn parse_opus_head(data: &[u8]) -> Result<(usize, u32), OpusError> {
49    if data.len() < OPUS_HEAD_MIN_LEN || &data[..8] != OPUS_HEAD_MAGIC {
50        return Err(OpusError::InvalidHeader);
51    }
52    let channels = data[9] as usize;
53    let pre_skip = u16::from_le_bytes([data[10], data[11]]) as u32;
54    Ok((channels, pre_skip))
55}
56
57impl OpusBridge {
58    /// Create a new Opus decoder from Symphonia codec parameters.
59    ///
60    /// The `extra_data` in `CodecParameters` should contain the Opus
61    /// identification header (OpusHead). If not present, falls back to
62    /// channel count from codec params.
63    pub fn new(params: &CodecParameters) -> Result<Self, OpusError> {
64        // Try to get channel count and pre-skip from the OpusHead extra data.
65        let (channels, pre_skip) = if let Some(extra) = &params.extra_data {
66            parse_opus_head(extra)?
67        } else {
68            // Fallback: use codec params channel count, assume no pre-skip.
69            let ch = params.channels.map(|c| c.count()).unwrap_or(2);
70            (ch, 0)
71        };
72
73        if channels == 0 || channels > 2 {
74            // opus-decoder only supports mono/stereo. Multistream would need
75            // OpusMultistreamDecoder, which we don't handle yet.
76            return Err(OpusError::Init(format!(
77                "unsupported channel count: {channels} (only mono/stereo supported)"
78            )));
79        }
80
81        let decoder =
82            OpusDecoder::new(48000, channels).map_err(|e| OpusError::Init(format!("{e:?}")))?;
83
84        // Max frame size: 120ms at 48kHz = 5760 samples/channel.
85        let max_samples = 5760 * channels;
86        let pcm_buf = vec![0.0f32; max_samples];
87
88        Ok(Self {
89            decoder,
90            channels,
91            pcm_buf,
92            pre_skip,
93            skipped: 0,
94            packet_index: 0,
95        })
96    }
97
98    /// Channel count for this stream.
99    pub fn channels(&self) -> usize {
100        self.channels
101    }
102
103    /// Decode one Symphonia packet. Returns a slice of interleaved f32 PCM
104    /// samples, or an empty slice for header/comment packets.
105    ///
106    /// Handles pre-skip trimming: the first N samples (per Opus spec) are
107    /// silently discarded.
108    pub fn decode_packet(&mut self, data: &[u8]) -> Result<&[f32], OpusError> {
109        let idx = self.packet_index;
110        self.packet_index += 1;
111
112        // First packet = OpusHead, second = OpusTags — skip both.
113        if idx < 2 {
114            return Ok(&[]);
115        }
116
117        let frames_per_channel = self
118            .decoder
119            .decode_float(data, &mut self.pcm_buf, false)
120            .map_err(|e| OpusError::Decode(format!("{e:?}")))?;
121
122        let total_samples = frames_per_channel * self.channels;
123
124        // Handle pre-skip: discard the first `pre_skip` samples per channel.
125        let start = if self.skipped < self.pre_skip {
126            let remaining = (self.pre_skip - self.skipped) as usize;
127            let skip_frames = remaining.min(frames_per_channel);
128            self.skipped += skip_frames as u32;
129            skip_frames * self.channels
130        } else {
131            0
132        };
133
134        Ok(&self.pcm_buf[start..total_samples])
135    }
136
137    /// Reset the decoder state (e.g. after a seek).
138    pub fn reset(&mut self) {
139        self.decoder.reset();
140        self.skipped = self.pre_skip; // After seek, pre-skip already applied.
141        // Don't reset packet_index — Symphonia handles seek by jumping to
142        // audio packets, not re-sending headers.
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    #[test]
151    fn test_parse_opus_head_valid() {
152        // Minimal valid OpusHead: stereo, pre-skip=312
153        let mut header = vec![0u8; 19];
154        header[..8].copy_from_slice(b"OpusHead");
155        header[8] = 1; // version
156        header[9] = 2; // channels
157        header[10] = 0x38; // pre_skip = 312 (0x0138)
158        header[11] = 0x01;
159        // rest is zeros (sample rate, gain, mapping family)
160
161        let (channels, pre_skip) = parse_opus_head(&header).unwrap();
162        assert_eq!(channels, 2);
163        assert_eq!(pre_skip, 312);
164    }
165
166    #[test]
167    fn test_parse_opus_head_mono() {
168        let mut header = vec![0u8; 19];
169        header[..8].copy_from_slice(b"OpusHead");
170        header[8] = 1;
171        header[9] = 1; // mono
172        header[10] = 0x00;
173        header[11] = 0x00;
174
175        let (channels, pre_skip) = parse_opus_head(&header).unwrap();
176        assert_eq!(channels, 1);
177        assert_eq!(pre_skip, 0);
178    }
179
180    #[test]
181    fn test_parse_opus_head_invalid_magic() {
182        let header = b"NotOpusHead_padding";
183        assert!(parse_opus_head(header).is_err());
184    }
185
186    #[test]
187    fn test_parse_opus_head_too_short() {
188        let header = b"OpusHea"; // 7 bytes
189        assert!(parse_opus_head(header).is_err());
190    }
191
192    #[test]
193    fn test_opus_bridge_new_stereo() {
194        // Build minimal CodecParameters with OpusHead extra data.
195        let mut header = vec![0u8; 19];
196        header[..8].copy_from_slice(b"OpusHead");
197        header[8] = 1;
198        header[9] = 2; // stereo
199        header[10] = 0x38;
200        header[11] = 0x01; // pre_skip=312
201
202        let mut params = CodecParameters::new();
203        params.with_extra_data(header.into_boxed_slice());
204
205        let bridge = OpusBridge::new(&params).unwrap();
206        assert_eq!(bridge.channels(), 2);
207    }
208
209    #[test]
210    fn test_opus_bridge_rejects_multichannel() {
211        let mut header = vec![0u8; 19];
212        header[..8].copy_from_slice(b"OpusHead");
213        header[8] = 1;
214        header[9] = 6; // 5.1 surround — not supported
215        header[10] = 0x00;
216        header[11] = 0x00;
217
218        let mut params = CodecParameters::new();
219        params.with_extra_data(header.into_boxed_slice());
220
221        assert!(OpusBridge::new(&params).is_err());
222    }
223}