use std::time::Duration;
use unsafe_libopus::{OPUS_OK, OpusDecoder, opus_decode_float, opus_decoder_create, opus_decoder_destroy};
use crate::opus;
use crate::pcm;
use crate::{Error, Format};
const MAX_FRAME_MS: usize = 120;
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct Config {
pub format: Format,
pub sample_rate: Option<u32>,
pub channels: Option<u32>,
pub latency_max: Option<Duration>,
}
impl Config {
pub fn new() -> Self {
Self::default()
}
}
pub struct Decoder {
backend: Backend,
sample_rate: u32,
channel_count: u32,
}
enum Backend {
Opus(Opus),
Pcm { bytes_per_frame: usize },
}
struct Opus {
inner: *mut OpusDecoder,
pre_skip_remaining: usize,
max_frame_size: usize,
}
unsafe impl Send for Opus {}
impl Decoder {
pub fn new(catalog: &hang::catalog::AudioConfig) -> Result<Self, Error> {
match &catalog.codec {
hang::catalog::AudioCodec::Opus => Self::new_opus(catalog),
hang::catalog::AudioCodec::Pcm => Self::new_pcm(catalog),
codec => Err(Error::Unsupported(format!("unsupported audio codec: {codec}"))),
}
}
fn new_opus(catalog: &hang::catalog::AudioConfig) -> Result<Self, Error> {
let (sample_rate, channel_count, pre_skip) = if let Some(desc) = &catalog.description {
let mut buf = desc.as_ref();
match moq_mux::codec::opus::Config::parse(&mut buf) {
Ok(head) => (head.sample_rate, head.channel_count, head.pre_skip),
Err(_) => (catalog.sample_rate, catalog.channel_count, 0),
}
} else {
(catalog.sample_rate, catalog.channel_count, 0)
};
opus::validate_rate(sample_rate)?;
let channels = opus::validate_channels(channel_count)?;
let mut err = 0i32;
let inner = unsafe { opus_decoder_create(sample_rate as i32, channels, &mut err) };
if err != OPUS_OK || inner.is_null() {
return Err(opus::error(err, "opus_decoder_create"));
}
let max_frame_size = (sample_rate as usize * MAX_FRAME_MS) / 1000;
let pre_skip_remaining = (pre_skip as usize * sample_rate as usize) / 48_000;
Ok(Self {
backend: Backend::Opus(Opus {
inner,
pre_skip_remaining,
max_frame_size,
}),
sample_rate,
channel_count,
})
}
fn new_pcm(catalog: &hang::catalog::AudioConfig) -> Result<Self, Error> {
if catalog.sample_rate == 0 {
return Err(Error::Unsupported("pcm sample rate must be greater than zero".into()));
}
if catalog.channel_count == 0 {
return Err(Error::Unsupported("pcm channel count must be greater than zero".into()));
}
if catalog.description.is_some() {
return Err(Error::Unsupported("pcm catalog description must be absent".into()));
}
let bitrate = pcm::bitrate(catalog.sample_rate, catalog.channel_count)?;
if catalog.bitrate.is_some_and(|declared| declared != bitrate) {
return Err(Error::Unsupported(format!(
"pcm catalog bitrate must be {bitrate} bits per second"
)));
}
let bytes_per_frame = pcm::frame_bytes(1, catalog.channel_count)?;
Ok(Self {
backend: Backend::Pcm { bytes_per_frame },
sample_rate: catalog.sample_rate,
channel_count: catalog.channel_count,
})
}
pub fn sample_rate(&self) -> u32 {
self.sample_rate
}
pub fn channel_count(&self) -> u32 {
self.channel_count
}
pub fn decode(&mut self, packet: &[u8]) -> Result<Vec<f32>, Error> {
match &mut self.backend {
Backend::Opus(opus) => {
let mut out = vec![0.0f32; opus.max_frame_size * self.channel_count as usize];
let samples = unsafe {
opus_decode_float(
&mut *opus.inner,
packet.as_ptr(),
packet.len() as i32,
out.as_mut_ptr(),
opus.max_frame_size as i32,
0,
)
};
if samples < 0 {
return Err(crate::opus::error(samples, "opus_decode_float"));
}
out.truncate(samples as usize * self.channel_count as usize);
let trim_frames = opus.pre_skip_remaining.min(samples as usize);
if trim_frames > 0 {
let trim_samples = trim_frames * self.channel_count as usize;
out.copy_within(trim_samples.., 0);
out.truncate(out.len() - trim_samples);
opus.pre_skip_remaining -= trim_frames;
}
Ok(out)
}
Backend::Pcm { bytes_per_frame } => {
if packet.is_empty() || !packet.len().is_multiple_of(*bytes_per_frame) {
return Err(Error::Misaligned {
got: packet.len(),
expected: packet.len().max(1).next_multiple_of(*bytes_per_frame),
});
}
Ok(packet
.chunks_exact(pcm::BYTES_PER_SAMPLE)
.map(|sample| f32::from_le_bytes([sample[0], sample[1], sample[2], sample[3]]))
.collect())
}
}
}
}
impl Drop for Opus {
fn drop(&mut self) {
unsafe { opus_decoder_destroy(self.inner) };
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pcm_rejects_incomplete_channel_frame() {
let catalog = hang::catalog::AudioConfig::new(hang::catalog::AudioCodec::Pcm, 48_000, 2);
let mut decoder = Decoder::new(&catalog).unwrap();
assert!(matches!(
decoder.decode(&[]),
Err(Error::Misaligned { got: 0, expected: 8 })
));
assert!(matches!(
decoder.decode(&[0; 4]),
Err(Error::Misaligned { got: 4, expected: 8 })
));
}
#[test]
fn decoder_rejects_unknown_codec() {
let catalog = hang::catalog::AudioConfig::new(hang::catalog::AudioCodec::Unknown("future".into()), 48_000, 2);
assert!(matches!(Decoder::new(&catalog), Err(Error::Unsupported(_))));
}
#[test]
fn pcm_rejects_incorrect_catalog_bitrate() {
let mut catalog = hang::catalog::AudioConfig::new(hang::catalog::AudioCodec::Pcm, 48_000, 2);
catalog.bitrate = Some(1);
assert!(matches!(Decoder::new(&catalog), Err(Error::Unsupported(_))));
}
}