#[cfg(feature = "file-decode")]
use anyhow::Result;
#[cfg(feature = "file-decode")]
use symphonia::core::formats::FormatReader;
#[cfg(feature = "file-decode")]
use super::audio_too_long_err;
#[cfg(feature = "file-decode")]
use super::stream::ChannelSelect;
#[cfg(feature = "file-decode")]
pub(super) const OPUS_DECODE_RATE: u32 = 48_000;
#[cfg(feature = "file-decode")]
pub(crate) fn is_recoverable_packet_eof(err: &symphonia::core::errors::Error) -> bool {
matches!(
err,
symphonia::core::errors::Error::IoError(ioe)
if ioe.kind() == std::io::ErrorKind::UnexpectedEof
)
}
#[cfg(feature = "file-decode")]
pub(super) fn next_demux_packet(
format: &mut dyn FormatReader,
have_pcm: bool,
) -> Result<Option<symphonia::core::packet::Packet>> {
match format.next_packet() {
Ok(Some(p)) => Ok(Some(p)),
Ok(None) => Ok(None),
Err(e) if is_recoverable_packet_eof(&e) && have_pcm => {
tracing::debug!(
"Demux UnexpectedEof after PCM already decoded; treating as end of stream"
);
Ok(None)
}
Err(e) => Err(anyhow::anyhow!("Error reading packet: {e}")),
}
}
#[cfg(feature = "file-decode")]
fn check_opus_channels(channels: usize) -> Result<()> {
if !(1..=2).contains(&channels) {
anyhow::bail!("Opus with {channels} channels is not supported (mono/stereo only)");
}
Ok(())
}
#[cfg(feature = "file-decode")]
fn new_opus_decoder(channels: usize) -> Result<opus_rs::OpusDecoder> {
opus_rs::OpusDecoder::new(OPUS_DECODE_RATE as i32, channels)
.map_err(|e| anyhow::anyhow!("Opus decoder init failed: {e}"))
}
#[cfg(feature = "file-decode")]
fn decode_packet_interleaved(
decoder: &mut opus_rs::OpusDecoder,
channels: usize,
data: &[u8],
pcm: &mut Vec<f32>,
scratch: &mut Vec<u8>,
) -> Result<usize> {
let framing = split_opus_packet(data)?;
let frame_size = framing.samples_per_frame;
pcm.resize(framing.packet_samples() * channels, 0.0);
let mut decoded = 0usize;
for frame in framing.frames() {
scratch.clear();
scratch.reserve(frame.len() + 1);
scratch.push(framing.toc & 0xFC);
scratch.extend_from_slice(frame);
let written = decoder
.decode(scratch, frame_size, &mut pcm[decoded * channels..])
.map_err(|e| anyhow::anyhow!("Opus decode error: {e}"))?
.min(frame_size);
decoded += written;
}
Ok(decoded)
}
#[cfg(feature = "file-decode")]
pub(super) struct OpusStream {
decoder: opus_rs::OpusDecoder,
channels: usize,
channel: ChannelSelect,
pcm: Vec<f32>,
frame: Vec<u8>,
}
#[cfg(feature = "file-decode")]
impl OpusStream {
pub(super) fn new(channels: usize, channel: ChannelSelect) -> Result<Self> {
check_opus_channels(channels)?;
Ok(Self {
decoder: new_opus_decoder(channels)?,
channels,
channel,
pcm: Vec::new(),
frame: Vec::new(),
})
}
pub(super) fn decode_packet(&mut self, data: &[u8], out: &mut Vec<f32>) -> Result<usize> {
let frames = decode_packet_interleaved(
&mut self.decoder,
self.channels,
data,
&mut self.pcm,
&mut self.frame,
)?;
match self.channel {
ChannelSelect::Mono => push_mono_mix(&self.pcm, self.channels, frames, out),
ChannelSelect::One(k) if k < self.channels => {
for frame in 0..frames {
out.push(self.pcm[frame * self.channels + k]);
}
}
ChannelSelect::One(_) => {}
}
Ok(frames)
}
}
#[cfg(feature = "file-decode")]
pub(super) fn push_mono_mix(pcm: &[f32], channels: usize, frames: usize, out: &mut Vec<f32>) {
let ch = channels as f32;
for frame in 0..frames {
let base = frame * channels;
out.push(pcm[base..base + channels].iter().sum::<f32>() / ch);
}
}
#[cfg(feature = "file-decode")]
pub(super) fn decode_opus_channels(
format: &mut dyn FormatReader,
track_id: u32,
channels: usize,
max_samples: usize,
limit_secs: f64,
) -> Result<Vec<Vec<f32>>> {
check_opus_channels(channels)?;
let mut decoder = new_opus_decoder(channels)?;
let mut per_channel: Vec<Vec<f32>> = (0..channels).map(|_| Vec::new()).collect();
let mut pcm: Vec<f32> = Vec::new();
let mut frame: Vec<u8> = Vec::new();
loop {
let have_pcm = per_channel.first().is_some_and(|c| !c.is_empty());
let Some(packet) = next_demux_packet(format, have_pcm)? else {
break;
};
if packet.track_id != track_id {
continue;
}
let decoded =
decode_packet_interleaved(&mut decoder, channels, &packet.data, &mut pcm, &mut frame)?;
if channels == 1 {
per_channel[0].extend_from_slice(&pcm[..decoded]);
} else {
for frame in 0..decoded {
for (c, buf) in per_channel.iter_mut().enumerate() {
buf.push(pcm[frame * channels + c]);
}
}
}
let decoded_len = per_channel.first().map(|v| v.len()).unwrap_or(0);
if decoded_len > max_samples {
return Err(audio_too_long_err(
decoded_len,
OPUS_DECODE_RATE,
limit_secs,
));
}
}
Ok(per_channel)
}
#[cfg(feature = "file-decode")]
mod framing;
#[cfg(feature = "file-decode")]
use framing::split_opus_packet;