1use unsafe_libopus::{
7 OPUS_OK, OPUS_RESET_STATE, OPUS_SET_GAIN_REQUEST, OpusDecoder, opus_decode_float, opus_decoder_create,
8 opus_decoder_ctl_impl, opus_decoder_destroy, varargs,
9};
10
11#[cfg(feature = "aac")]
12use symphonia_core::codecs::audio::AudioDecoder;
13
14use super::Decoded;
15#[cfg(feature = "aac")]
16use crate::aac;
17use crate::opus;
18use crate::pcm;
19use crate::{Activity, Error, Layout};
20
21const MAX_FRAME_MS: usize = 120;
23
24#[derive(Clone, Debug, Default, PartialEq, Eq)]
26#[non_exhaustive]
27pub enum Kind {
28 #[default]
30 Auto,
31 Software,
33 Named(String),
35}
36
37#[derive(Clone, Debug, Default)]
39#[non_exhaustive]
40pub struct Config {
41 pub kind: Kind,
43}
44
45impl Config {
46 pub fn new() -> Self {
48 Self::default()
49 }
50}
51
52pub struct Decoder {
57 backend: Backend,
58 sample_rate: u32,
59 layout: Layout,
60 delay: usize,
61}
62
63enum Backend {
64 Opus(Opus),
65 Pcm {
66 bytes_per_frame: usize,
67 },
68 #[cfg(feature = "aac")]
69 Aac(Box<Aac>),
70}
71
72struct Opus {
73 inner: *mut OpusDecoder,
74 pre_skip_remaining: usize,
75 max_frame_size: usize,
76 in_dtx: bool,
77}
78
79unsafe impl Send for Opus {}
81
82#[cfg(feature = "aac")]
85struct Aac {
86 inner: symphonia_codec_aac::AacDecoder,
87}
88
89impl Decoder {
90 pub fn new(catalog: &hang::catalog::AudioConfig, config: &Config) -> Result<Self, Error> {
98 let name = match &catalog.codec {
99 hang::catalog::AudioCodec::Opus => "opus",
100 hang::catalog::AudioCodec::Pcm => "pcm",
101 #[cfg(feature = "aac")]
102 hang::catalog::AudioCodec::AAC(_) => "aac",
103 codec => return Err(Error::Unsupported(format!("unsupported audio codec: {codec}"))),
104 };
105 match &config.kind {
106 Kind::Auto | Kind::Software => {}
107 Kind::Named(requested) if requested == name => {}
108 Kind::Named(requested) => {
109 return Err(Error::Unsupported(format!(
110 "audio decoder backend {requested:?} is unavailable for {name}"
111 )));
112 }
113 }
114 match &catalog.codec {
115 hang::catalog::AudioCodec::Opus => Self::new_opus(catalog),
116 hang::catalog::AudioCodec::Pcm => Self::new_pcm(catalog),
117 #[cfg(feature = "aac")]
118 hang::catalog::AudioCodec::AAC(aac) => Self::new_aac(catalog, aac.profile),
119 codec => Err(Error::Unsupported(format!("unsupported audio codec: {codec}"))),
120 }
121 }
122
123 fn new_opus(catalog: &hang::catalog::AudioConfig) -> Result<Self, Error> {
124 let head = opus::head(catalog)?;
125 let channels = opus::validate_channels(head.channel_count)?;
126
127 let mut err = 0i32;
128 let inner = unsafe { opus_decoder_create(opus::DECODE_RATE as i32, channels, &mut err) };
130 if err != OPUS_OK || inner.is_null() {
131 return Err(opus::error(err, "opus_decoder_create"));
132 }
133 let decoder = Opus {
135 inner,
136 pre_skip_remaining: head.pre_skip as usize,
137 max_frame_size: (opus::DECODE_RATE as usize * MAX_FRAME_MS) / 1000,
138 in_dtx: false,
139 };
140
141 let rc =
144 unsafe { opus_decoder_ctl_impl(decoder.inner, OPUS_SET_GAIN_REQUEST, varargs![head.output_gain as i32]) };
145 if rc != OPUS_OK {
146 return Err(opus::error(rc, "OPUS_SET_GAIN"));
147 }
148
149 Ok(Self {
150 delay: decoder.pre_skip_remaining,
151 backend: Backend::Opus(decoder),
152 sample_rate: opus::DECODE_RATE,
153 layout: Layout::from_channels(head.channel_count)?,
154 })
155 }
156
157 #[cfg(feature = "aac")]
166 fn new_aac(catalog: &hang::catalog::AudioConfig, profile: u8) -> Result<Self, Error> {
167 use symphonia_core::codecs::audio::well_known::CODEC_ID_AAC;
168 use symphonia_core::codecs::audio::{AudioCodecParameters, AudioDecoderOptions};
169
170 let description = aac::description(catalog, profile)?;
171
172 let mut params = AudioCodecParameters::new();
173 params
174 .for_codec(CODEC_ID_AAC)
175 .with_extra_data(description.to_vec().into_boxed_slice());
176
177 let inner = symphonia_codec_aac::AacDecoder::try_new(¶ms, &AudioDecoderOptions::default())
178 .map_err(|err| Error::Unsupported(format!("aac decoder: {err}")))?;
179
180 let params = inner.codec_params();
183 let sample_rate = params
184 .sample_rate
185 .ok_or_else(|| Error::Unsupported("aac config declares no sample rate".into()))?;
186 let channel_count = params
187 .channels
188 .as_ref()
189 .map(|channels| channels.count())
190 .ok_or_else(|| Error::Unsupported("aac config declares no channels".into()))?;
191
192 Ok(Self {
193 backend: Backend::Aac(Box::new(Aac { inner })),
194 sample_rate,
195 layout: Layout::from_channels(channel_count as u32)?,
196 delay: 0,
197 })
198 }
199
200 fn new_pcm(catalog: &hang::catalog::AudioConfig) -> Result<Self, Error> {
201 if catalog.sample_rate == 0 {
202 return Err(Error::Unsupported("pcm sample rate must be greater than zero".into()));
203 }
204 if catalog.channel_count == 0 {
205 return Err(Error::Unsupported("pcm channel count must be greater than zero".into()));
206 }
207 if catalog.description.is_some() {
208 return Err(Error::Unsupported("pcm catalog description must be absent".into()));
209 }
210 let bitrate = pcm::bitrate(catalog.sample_rate, catalog.channel_count)?;
211 if catalog.bitrate.is_some_and(|declared| declared != bitrate) {
212 return Err(Error::Unsupported(format!(
213 "pcm catalog bitrate must be {bitrate} bits per second"
214 )));
215 }
216 let bytes_per_frame = pcm::frame_bytes(1, catalog.channel_count)?;
217
218 Ok(Self {
219 backend: Backend::Pcm { bytes_per_frame },
220 sample_rate: catalog.sample_rate,
221 layout: Layout::from_channels(catalog.channel_count)?,
222 delay: 0,
223 })
224 }
225
226 pub fn sample_rate(&self) -> u32 {
228 self.sample_rate
229 }
230
231 pub fn layout(&self) -> Layout {
233 self.layout
234 }
235
236 pub fn reset(&mut self) -> Result<(), Error> {
238 self.reset_prediction()?;
239 self.reapply_delay();
240 Ok(())
241 }
242
243 pub(super) fn reapply_delay(&mut self) {
245 if let Backend::Opus(opus) = &mut self.backend {
246 opus.pre_skip_remaining = self.delay;
247 }
248 }
249
250 pub(super) fn reset_prediction(&mut self) -> Result<(), Error> {
252 match &mut self.backend {
253 Backend::Opus(opus) => {
254 let rc = unsafe { opus_decoder_ctl_impl(opus.inner, OPUS_RESET_STATE, varargs![]) };
256 if rc != OPUS_OK {
257 return Err(crate::opus::error(rc, "OPUS_RESET_STATE"));
258 }
259 opus.in_dtx = false;
260 }
261 Backend::Pcm { .. } => {}
262 #[cfg(feature = "aac")]
263 Backend::Aac(aac) => aac.inner.reset(),
264 }
265 Ok(())
266 }
267
268 pub(super) fn delay_remaining(&self) -> usize {
274 match &self.backend {
275 Backend::Opus(opus) => opus.pre_skip_remaining,
276 Backend::Pcm { .. } => 0,
277 #[cfg(feature = "aac")]
278 Backend::Aac(_) => 0,
279 }
280 }
281
282 pub fn decode(&mut self, packet: &[u8]) -> Result<Decoded, Error> {
287 match &mut self.backend {
288 Backend::Opus(opus) => {
289 let channels = self.layout.channels() as usize;
290 let mut out = vec![0.0f32; opus.max_frame_size * channels];
291 let samples = unsafe {
294 opus_decode_float(
295 &mut *opus.inner,
296 packet.as_ptr(),
297 packet.len() as i32,
298 out.as_mut_ptr(),
299 opus.max_frame_size as i32,
300 0,
301 )
302 };
303 if samples < 0 {
304 return Err(crate::opus::decode_error(samples));
305 }
306 out.truncate(samples as usize * channels);
307 let trim_frames = opus.pre_skip_remaining.min(samples as usize);
308 if trim_frames > 0 {
309 let trim_samples = trim_frames * channels;
310 out.copy_within(trim_samples.., 0);
311 out.truncate(out.len() - trim_samples);
312 opus.pre_skip_remaining -= trim_frames;
313 }
314 let activity = crate::opus::activity(packet, opus.in_dtx);
315 opus.in_dtx = activity.is_dtx();
316 Ok(Decoded { samples: out, activity })
317 }
318 Backend::Pcm { bytes_per_frame } => {
319 if packet.is_empty() || !packet.len().is_multiple_of(*bytes_per_frame) {
320 return Err(Error::Misaligned {
321 got: packet.len(),
322 expected: packet.len().max(1).next_multiple_of(*bytes_per_frame),
323 });
324 }
325
326 let out = packet
327 .as_chunks::<{ pcm::BYTES_PER_SAMPLE }>()
328 .0
329 .iter()
330 .map(|sample| f32::from_le_bytes([sample[0], sample[1], sample[2], sample[3]]))
331 .collect();
332 Ok(Decoded {
333 samples: out,
334 activity: Activity::Active,
335 })
336 }
337 #[cfg(feature = "aac")]
338 Backend::Aac(aac) => {
339 let packet = symphonia_core::packet::PacketRef::new(
343 0,
344 symphonia_core::units::Timestamp::ZERO,
345 symphonia_core::units::Duration::ZERO,
346 packet,
347 );
348
349 let decoded = aac
350 .inner
351 .decode_ref(&packet)
352 .map_err(|err| Error::Decode(format!("aac: {err}")))?;
353
354 let mut out = Vec::new();
355 decoded.copy_to_vec_interleaved(&mut out);
356 Ok(Decoded {
357 samples: out,
358 activity: Activity::Active,
359 })
360 }
361 }
362 }
363}
364
365impl Drop for Opus {
366 fn drop(&mut self) {
367 unsafe { opus_decoder_destroy(self.inner) };
369 }
370}
371
372#[cfg(test)]
373pub(crate) mod tests {
374 use super::*;
375
376 #[cfg(feature = "aac")]
387 const AAC_DESCRIPTION: &[u8] = b"\x12\x08";
388
389 #[cfg(feature = "aac")]
390 const AAC_FRAMES: [&[u8]; 3] = [
391 b"\x01\x52\xf2\x8b\x1a\xd7\x8e\x7b\xfd\xa7\xef\xe7\xe3\x55\xd3\x4d\x2f\x55\x2e\x47\x1c\x92\x49\x11\x20\x77\x3f\xbe\x74\xdd\x99\xb3\x7b\xfb\x90\xc9\xf0\x61\x9f\xdc\x0c\x9f\x06\x19\xfd\xe1\x1f\x1f\x00\x67\xf7\x03\x87\xc0\x19\xfd\xc0\xc9\xf0\x07",
392 b"\x01\x1e\x32\x89\xe2\x9d\x6b\x33\xe7\xff\xe2\xfe\xbf\xfa\xff\xe7\x2f\x8b\xd5\xd5\xe7\x5f\x3f\x59\xeb\xf1\xcb\xba\xa5\x5e\x52\x4a\xbd\x8d\x74\x50\x8c\x08\xa8\xa0\xd4\x51\x40\xa1\x86\x5d\x06\xb4\x6c\x32\xe6\x25\x9a\x66\x75\xcd\xf9\xbf\x6f\x83\xb7\x53\x80",
393 b"\x01\x1e\x32\x8a\x22\x7d\x40\x87\x48\xdb\xdf\xff\xf9\x4f\xff\x87\xde\xef\x8b\xeb\x1e\x77\x5d\xfc\x67\x8f\x8c\x77\x8a\xd6\x29\x96\x1f\x29\xe7\x39\xd4\x53\xcf\x3c\xf3\xce\x79\xd4\x27\x9c\xf5\x65\x2a\x9b\xe9\x80\xb7\xba\xa9\xf9\x58\xc7\x3c\x58\x27\x8a\x60\xa1\x57",
394 ];
395
396 #[cfg(feature = "aac")]
397 fn aac_catalog() -> hang::catalog::AudioConfig {
398 let mut catalog = hang::catalog::AudioConfig::new(hang::catalog::AAC { profile: 2 }, 44_100, 1);
399 catalog.description = Some(bytes::Bytes::from_static(AAC_DESCRIPTION));
400 catalog
401 }
402
403 #[cfg(feature = "aac")]
404 #[test]
405 fn aac_decodes_a_sine() {
406 let mut decoder = Decoder::new(&aac_catalog(), &Config::default()).unwrap();
407 assert_eq!(decoder.sample_rate(), 44_100);
408 assert_eq!(decoder.layout(), Layout::Mono);
409
410 let decoded: Vec<Vec<f32>> = AAC_FRAMES
411 .iter()
412 .map(|frame| decoder.decode(frame).unwrap().samples)
413 .collect();
414
415 for pcm in &decoded {
417 assert_eq!(pcm.len(), 1024);
418 }
419
420 let last = decoded.last().unwrap();
424 let rms = (last.iter().map(|s| s * s).sum::<f32>() / last.len() as f32).sqrt();
425 assert!((0.65..0.8).contains(&rms), "expected a full-scale sine, got {rms} RMS");
426 }
427
428 #[cfg(feature = "aac")]
429 #[test]
430 fn aac_reports_a_truncated_packet_as_decode() {
431 let mut decoder = Decoder::new(&aac_catalog(), &Config::default()).unwrap();
432
433 let truncated = &AAC_FRAMES[0][..16];
434 assert!(matches!(decoder.decode(truncated), Err(Error::Decode(_))));
435 }
436
437 #[cfg(feature = "aac")]
438 #[test]
439 fn aac_synthesizes_a_missing_description() {
440 let mut catalog = aac_catalog();
442 catalog.description = None;
443
444 let mut decoder = Decoder::new(&catalog, &Config::default()).unwrap();
445 assert_eq!(decoder.sample_rate(), 44_100);
446 assert_eq!(decoder.decode(AAC_FRAMES[0]).unwrap().samples.len(), 1024);
447 }
448
449 #[test]
453 fn opus_reports_a_rejected_packet_as_decode() {
454 let head = moq_mux::codec::opus::Config::new(48_000, 2).encode().unwrap();
455 let mut catalog = hang::catalog::AudioConfig::new(hang::catalog::AudioCodec::Opus, 48_000, 2);
456 catalog.description = Some(head);
457
458 let mut decoder = Decoder::new(&catalog, &Config::default()).unwrap();
459
460 assert!(matches!(decoder.decode(&[0xFF; 3]), Err(Error::Decode(_))));
462 }
463
464 pub(crate) fn opus_packets(count: usize) -> Vec<bytes::Bytes> {
467 let mut encoder = crate::encode::Encoder::new(&crate::encode::Settings::new(48_000, Layout::Mono)).unwrap();
468 let frames = encoder.frame_size();
469 (0..count)
470 .map(|packet| {
471 let pcm: Vec<f32> = (packet * frames..(packet + 1) * frames)
472 .map(|i| (std::f32::consts::TAU * 440.0 * i as f32 / 48_000.0).sin() * 0.5)
473 .collect();
474 encoder.encode(&pcm).unwrap().payload
475 })
476 .collect()
477 }
478
479 pub(crate) fn opus_catalog(head: moq_mux::codec::opus::Config) -> hang::catalog::AudioConfig {
481 let mut catalog =
482 hang::catalog::AudioConfig::new(hang::catalog::AudioCodec::Opus, head.sample_rate, head.channel_count);
483 catalog.description = Some(head.encode().unwrap());
484 catalog
485 }
486
487 fn rms(samples: &[f32]) -> f32 {
488 (samples.iter().map(|s| s * s).sum::<f32>() / samples.len() as f32).sqrt()
489 }
490
491 #[test]
494 fn opus_decodes_at_48k_whatever_the_input_rate() {
495 let packets = opus_packets(2);
496 for input_rate in [0, 8_000, 24_000, 44_100, 48_000, 96_000] {
497 let head = moq_mux::codec::opus::Config::new(input_rate, 1).with_pre_skip(312);
498 let mut decoder = Decoder::new(&opus_catalog(head), &Config::default()).unwrap();
499 assert_eq!(decoder.sample_rate(), 48_000, "input rate {input_rate}");
500
501 assert_eq!(decoder.decode(&packets[0]).unwrap().samples.len(), 960 - 312);
503 assert_eq!(decoder.decode(&packets[1]).unwrap().samples.len(), 960);
504 }
505 }
506
507 #[test]
508 fn opus_applies_the_declared_gain() {
509 let packets = opus_packets(5);
510 let decode = |output_gain: i16| {
511 let mut head = moq_mux::codec::opus::Config::new(48_000, 1);
512 head.output_gain = output_gain;
513 let mut decoder = Decoder::new(&opus_catalog(head), &Config::default()).unwrap();
514 let mut last = Vec::new();
515 for packet in &packets {
516 last = decoder.decode(packet).unwrap().samples;
517 }
518 decoder.reset().unwrap();
520 let reset = decoder.decode(&packets[4]).unwrap().samples;
521 (rms(&last), rms(&reset))
522 };
523
524 let (plain, plain_reset) = decode(0);
526 let (quiet, quiet_reset) = decode(-1541);
527 assert!((quiet / plain - 0.5).abs() < 0.001, "gain ratio {}", quiet / plain);
528 assert!(
529 (quiet_reset / plain_reset - 0.5).abs() < 0.001,
530 "gain ratio after reset {}",
531 quiet_reset / plain_reset
532 );
533 }
534
535 #[test]
538 fn opus_refuses_a_malformed_description() {
539 let valid = moq_mux::codec::opus::Config::new(48_000, 2).encode().unwrap().to_vec();
540 let mut version = valid.clone();
541 version[8] = 16;
542 let mut channels = valid.clone();
543 channels[9] = 3;
544 let mut signature = valid.clone();
545 signature[0] = b'X';
546 let mut table = valid.clone();
548 table[18] = 1;
549
550 for (name, description) in [
551 ("truncated", valid[..18].to_vec()),
552 ("empty", Vec::new()),
553 ("signature", signature),
554 ("version", version),
555 ("channels", channels),
556 ("table", table),
557 ] {
558 let mut catalog = hang::catalog::AudioConfig::new(hang::catalog::AudioCodec::Opus, 48_000, 2);
559 catalog.description = Some(description.into());
560 assert!(
561 matches!(Decoder::new(&catalog, &Config::default()), Err(Error::Unsupported(_))),
562 "{name}"
563 );
564 }
565 }
566
567 #[test]
570 fn opus_refuses_unsupported_channel_mappings() {
571 let valid = moq_mux::codec::opus::Config::new(48_000, 2).encode().unwrap().to_vec();
572 for family in [1, 255] {
573 let mut description = valid.clone();
574 description[18] = family;
575 description.extend_from_slice(&[1, 1, 0, 1]);
577
578 let mut catalog = hang::catalog::AudioConfig::new(hang::catalog::AudioCodec::Opus, 48_000, 2);
579 catalog.description = Some(description.into());
580 assert!(
581 matches!(Decoder::new(&catalog, &Config::default()), Err(Error::Unsupported(_))),
582 "family {family}"
583 );
584 }
585 }
586
587 #[test]
589 fn opus_decodes_without_a_description() {
590 let packets = opus_packets(1);
591 let catalog = hang::catalog::AudioConfig::new(hang::catalog::AudioCodec::Opus, 24_000, 1);
592 let mut decoder = Decoder::new(&catalog, &Config::default()).unwrap();
593 assert_eq!(decoder.sample_rate(), 48_000);
594 assert_eq!(decoder.decode(&packets[0]).unwrap().samples.len(), 960);
595
596 let catalog = hang::catalog::AudioConfig::new(hang::catalog::AudioCodec::Opus, 48_000, 6);
597 assert!(matches!(
598 Decoder::new(&catalog, &Config::default()),
599 Err(Error::Unsupported(_))
600 ));
601 }
602
603 #[test]
604 fn pcm_rejects_incomplete_channel_frame() {
605 let catalog = hang::catalog::AudioConfig::new(hang::catalog::AudioCodec::Pcm, 48_000, 2);
606 let mut decoder = Decoder::new(&catalog, &Config::default()).unwrap();
607
608 assert!(matches!(
609 decoder.decode(&[]),
610 Err(Error::Misaligned { got: 0, expected: 8 })
611 ));
612 assert!(matches!(
613 decoder.decode(&[0; 4]),
614 Err(Error::Misaligned { got: 4, expected: 8 })
615 ));
616 }
617
618 #[test]
619 fn decoder_rejects_unknown_codec() {
620 let catalog = hang::catalog::AudioConfig::new(hang::catalog::AudioCodec::Unknown("future".into()), 48_000, 2);
621
622 assert!(matches!(
623 Decoder::new(&catalog, &Config::default()),
624 Err(Error::Unsupported(_))
625 ));
626 }
627
628 #[test]
629 fn pcm_rejects_incorrect_catalog_bitrate() {
630 let mut catalog = hang::catalog::AudioConfig::new(hang::catalog::AudioCodec::Pcm, 48_000, 2);
631 catalog.bitrate = Some(1);
632
633 assert!(matches!(
634 Decoder::new(&catalog, &Config::default()),
635 Err(Error::Unsupported(_))
636 ));
637 }
638
639 #[test]
640 fn refuses_unavailable_backend() {
641 let catalog = hang::catalog::AudioConfig::new(hang::catalog::AudioCodec::Pcm, 48_000, 2);
642 let config = Config {
643 kind: Kind::Named("missing".into()),
644 };
645 assert!(matches!(Decoder::new(&catalog, &config), Err(Error::Unsupported(_))));
646 }
647}