use opus_decoder::OpusDecoder;
use symphonia::core::codecs::CodecParameters;
#[derive(Debug, thiserror::Error)]
pub enum OpusError {
#[error("opus decoder init failed: {0}")]
Init(String),
#[error("opus decode error: {0}")]
Decode(String),
#[error("invalid opus header")]
InvalidHeader,
}
pub struct OpusBridge {
decoder: OpusDecoder,
channels: usize,
pcm_buf: Vec<f32>,
pre_skip: u32,
skipped: u32,
packet_index: u64,
}
const OPUS_HEAD_MAGIC: &[u8] = b"OpusHead";
const OPUS_HEAD_MIN_LEN: usize = 19;
fn parse_opus_head(data: &[u8]) -> Result<(usize, u32), OpusError> {
if data.len() < OPUS_HEAD_MIN_LEN || &data[..8] != OPUS_HEAD_MAGIC {
return Err(OpusError::InvalidHeader);
}
let channels = data[9] as usize;
let pre_skip = u16::from_le_bytes([data[10], data[11]]) as u32;
Ok((channels, pre_skip))
}
impl OpusBridge {
pub fn new(params: &CodecParameters) -> Result<Self, OpusError> {
let (channels, pre_skip) = if let Some(extra) = ¶ms.extra_data {
parse_opus_head(extra)?
} else {
let ch = params.channels.map(|c| c.count()).unwrap_or(2);
(ch, 0)
};
if channels == 0 || channels > 2 {
return Err(OpusError::Init(format!(
"unsupported channel count: {channels} (only mono/stereo supported)"
)));
}
let decoder =
OpusDecoder::new(48000, channels).map_err(|e| OpusError::Init(format!("{e:?}")))?;
let max_samples = 5760 * channels;
let pcm_buf = vec![0.0f32; max_samples];
Ok(Self {
decoder,
channels,
pcm_buf,
pre_skip,
skipped: 0,
packet_index: 0,
})
}
pub fn channels(&self) -> usize {
self.channels
}
pub fn decode_packet(&mut self, data: &[u8]) -> Result<&[f32], OpusError> {
let idx = self.packet_index;
self.packet_index += 1;
if idx < 2 {
return Ok(&[]);
}
let frames_per_channel = self
.decoder
.decode_float(data, &mut self.pcm_buf, false)
.map_err(|e| OpusError::Decode(format!("{e:?}")))?;
let total_samples = frames_per_channel * self.channels;
let start = if self.skipped < self.pre_skip {
let remaining = (self.pre_skip - self.skipped) as usize;
let skip_frames = remaining.min(frames_per_channel);
self.skipped += skip_frames as u32;
skip_frames * self.channels
} else {
0
};
Ok(&self.pcm_buf[start..total_samples])
}
pub fn reset(&mut self) {
self.decoder.reset();
self.skipped = self.pre_skip; }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_opus_head_valid() {
let mut header = vec![0u8; 19];
header[..8].copy_from_slice(b"OpusHead");
header[8] = 1; header[9] = 2; header[10] = 0x38; header[11] = 0x01;
let (channels, pre_skip) = parse_opus_head(&header).unwrap();
assert_eq!(channels, 2);
assert_eq!(pre_skip, 312);
}
#[test]
fn test_parse_opus_head_mono() {
let mut header = vec![0u8; 19];
header[..8].copy_from_slice(b"OpusHead");
header[8] = 1;
header[9] = 1; header[10] = 0x00;
header[11] = 0x00;
let (channels, pre_skip) = parse_opus_head(&header).unwrap();
assert_eq!(channels, 1);
assert_eq!(pre_skip, 0);
}
#[test]
fn test_parse_opus_head_invalid_magic() {
let header = b"NotOpusHead_padding";
assert!(parse_opus_head(header).is_err());
}
#[test]
fn test_parse_opus_head_too_short() {
let header = b"OpusHea"; assert!(parse_opus_head(header).is_err());
}
#[test]
fn test_opus_bridge_new_stereo() {
let mut header = vec![0u8; 19];
header[..8].copy_from_slice(b"OpusHead");
header[8] = 1;
header[9] = 2; header[10] = 0x38;
header[11] = 0x01;
let mut params = CodecParameters::new();
params.with_extra_data(header.into_boxed_slice());
let bridge = OpusBridge::new(¶ms).unwrap();
assert_eq!(bridge.channels(), 2);
}
#[test]
fn test_opus_bridge_rejects_multichannel() {
let mut header = vec![0u8; 19];
header[..8].copy_from_slice(b"OpusHead");
header[8] = 1;
header[9] = 6; header[10] = 0x00;
header[11] = 0x00;
let mut params = CodecParameters::new();
params.with_extra_data(header.into_boxed_slice());
assert!(OpusBridge::new(¶ms).is_err());
}
}