1use std::str::FromStr;
8use std::time::Duration;
9
10use bytes::Bytes;
11use unsafe_libopus::{
12 OPUS_APPLICATION_AUDIO, OPUS_GET_BITRATE_REQUEST, OPUS_GET_LOOKAHEAD_REQUEST, OPUS_OK, OPUS_RESET_STATE,
13 OPUS_SET_BITRATE_REQUEST, OPUS_SET_DTX_REQUEST, OPUS_SET_INBAND_FEC_REQUEST, OpusEncoder, opus_encode_float,
14 opus_encoder_create, opus_encoder_ctl_impl, opus_encoder_destroy, varargs,
15};
16
17use super::Encoded;
18use crate::opus;
19use crate::pcm;
20use crate::{Error, Format};
21
22const MAX_PACKET_BYTES: usize = 4_000;
24
25#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
28#[non_exhaustive]
29pub enum Codec {
30 #[default]
32 Opus,
33 Pcm,
35}
36
37impl Codec {
38 pub fn as_str(self) -> &'static str {
41 match self {
42 Self::Opus => "opus",
43 Self::Pcm => "pcm",
44 }
45 }
46}
47
48impl std::fmt::Display for Codec {
49 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50 f.write_str(self.as_str())
51 }
52}
53
54impl FromStr for Codec {
55 type Err = Error;
56
57 fn from_str(s: &str) -> Result<Self, Self::Err> {
58 match s {
59 "opus" => Ok(Self::Opus),
60 "pcm" => Ok(Self::Pcm),
61 other => Err(Error::Unsupported(format!("unknown codec: {other}"))),
62 }
63 }
64}
65
66#[derive(Clone, Debug)]
73pub struct Input {
74 pub format: Format,
76 pub sample_rate: u32,
78 pub channels: u32,
80}
81
82impl Default for Input {
83 fn default() -> Self {
84 Self {
85 format: Format::F32,
86 sample_rate: 48_000,
87 channels: 2,
88 }
89 }
90}
91
92#[derive(Clone, Debug)]
101#[non_exhaustive]
102pub struct Config {
103 pub input: Input,
105 pub codec: Codec,
107 pub sample_rate: Option<u32>,
110 pub channels: Option<u32>,
113 pub bitrate: Option<u32>,
123 pub fec: bool,
125 pub dtx: bool,
127 pub frame_duration: Duration,
130}
131
132impl Config {
133 pub fn new(input: Input) -> Self {
135 Self {
136 input,
137 codec: Codec::default(),
138 sample_rate: None,
139 channels: None,
140 bitrate: None,
141 fec: false,
142 dtx: false,
143 frame_duration: Duration::from_millis(20),
144 }
145 }
146}
147
148pub struct Encoder {
155 backend: Backend,
156 config: Config,
157 codec_rate: u32,
160 codec_channels: u32,
162 bitrate: u64,
164 pre_skip: u16,
166 lookahead: usize,
168 frame_size: usize,
169 started: bool,
171}
172
173enum Backend {
174 Opus(Opus),
175 Pcm,
176}
177
178struct Opus {
179 inner: *mut OpusEncoder,
180 scratch: Vec<u8>,
181}
182
183unsafe impl Send for Opus {}
187
188pub struct Finish {
190 packets: Vec<Encoded>,
191 discard_padding: usize,
192}
193
194impl Finish {
195 pub fn packets(&self) -> &[Encoded] {
197 &self.packets
198 }
199
200 pub fn discard_padding(&self) -> usize {
202 self.discard_padding
203 }
204
205 pub fn into_packets(self) -> Vec<Encoded> {
207 self.packets
208 }
209}
210
211impl Encoder {
212 pub fn new(config: &Config) -> Result<Self, Error> {
214 match config.codec {
215 Codec::Opus => Self::new_opus(config.clone()),
216 Codec::Pcm => Self::new_pcm(config.clone()),
217 }
218 }
219
220 fn new_opus(config: Config) -> Result<Self, Error> {
221 let codec_rate = config
222 .sample_rate
223 .unwrap_or_else(|| opus::pick_rate(config.input.sample_rate));
224 opus::validate_rate(codec_rate)?;
225
226 let codec_channels = config.channels.unwrap_or(config.input.channels);
227 if codec_channels != config.input.channels {
228 return Err(Error::Unsupported(format!(
229 "channel remapping not implemented (input {}ch, output {codec_channels}ch)",
230 config.input.channels
231 )));
232 }
233 let channels = opus::validate_channels(codec_channels)?;
234
235 let frame_size = opus::frame_size(codec_rate, config.frame_duration)?;
236
237 let mut err = 0i32;
238 let inner = unsafe { opus_encoder_create(codec_rate as i32, channels, OPUS_APPLICATION_AUDIO, &mut err) };
240 if err != OPUS_OK || inner.is_null() {
241 return Err(opus::error(err, "opus_encoder_create"));
242 }
243
244 let configured = Self::configure_opus(inner, &config, codec_rate, codec_channels, frame_size);
245 let (bitrate, lookahead, pre_skip) = match configured {
246 Ok(configured) => configured,
247 Err(err) => {
248 unsafe { opus_encoder_destroy(inner) };
250 return Err(err);
251 }
252 };
253
254 Ok(Self {
255 backend: Backend::Opus(Opus {
256 inner,
257 scratch: vec![0u8; MAX_PACKET_BYTES],
258 }),
259 config,
260 codec_rate,
261 codec_channels,
262 bitrate,
263 pre_skip,
264 lookahead,
265 frame_size,
266 started: false,
267 })
268 }
269
270 fn new_pcm(config: Config) -> Result<Self, Error> {
271 if config.bitrate.is_some() {
272 return Err(Error::Unsupported(
273 "pcm bitrate is fixed; leave Config::bitrate unset".into(),
274 ));
275 }
276
277 let codec_rate = config.sample_rate.unwrap_or(config.input.sample_rate);
278 if codec_rate == 0 {
279 return Err(Error::Unsupported("pcm sample rate must be greater than zero".into()));
280 }
281
282 let codec_channels = config.channels.unwrap_or(config.input.channels);
283 if codec_channels == 0 {
284 return Err(Error::Unsupported("pcm channel count must be greater than zero".into()));
285 }
286 if codec_channels != config.input.channels {
287 return Err(Error::Unsupported(format!(
288 "channel remapping not implemented (input {}ch, output {codec_channels}ch)",
289 config.input.channels
290 )));
291 }
292
293 let frame_size = pcm::frame_size(codec_rate, config.frame_duration)?;
294 pcm::frame_bytes(frame_size, codec_channels)?;
295 let bitrate = pcm::bitrate(codec_rate, codec_channels)?;
296 Ok(Self {
297 backend: Backend::Pcm,
298 config,
299 codec_rate,
300 codec_channels,
301 bitrate,
302 pre_skip: 0,
303 lookahead: 0,
304 frame_size,
305 started: false,
306 })
307 }
308
309 fn configure_opus(
310 inner: *mut OpusEncoder,
311 config: &Config,
312 codec_rate: u32,
313 codec_channels: u32,
314 frame_size: usize,
315 ) -> Result<(u64, usize, u16), Error> {
316 if let Some(bitrate) = config.bitrate {
317 Self::set_opus_bitrate(inner, codec_channels, bitrate as u64, codec_rate, frame_size)?;
318 }
319 Self::set_opus_ctl(
320 inner,
321 OPUS_SET_INBAND_FEC_REQUEST,
322 i32::from(config.fec),
323 "OPUS_SET_INBAND_FEC",
324 )?;
325 Self::set_opus_ctl(inner, OPUS_SET_DTX_REQUEST, i32::from(config.dtx), "OPUS_SET_DTX")?;
326
327 let bitrate = Self::get_opus_ctl(inner, OPUS_GET_BITRATE_REQUEST, "OPUS_GET_BITRATE")?;
328 let bitrate = u64::try_from(bitrate)
329 .map_err(|_| Error::Unsupported(format!("Opus reported negative bitrate {bitrate}")))?;
330 let lookahead = Self::get_opus_ctl(inner, OPUS_GET_LOOKAHEAD_REQUEST, "OPUS_GET_LOOKAHEAD")?;
331 let lookahead = u64::try_from(lookahead)
332 .map_err(|_| Error::Unsupported(format!("Opus reported negative lookahead {lookahead}")))?;
333 let pre_skip = u16::try_from((lookahead * 48_000) / codec_rate as u64)
334 .map_err(|_| Error::Unsupported(format!("Opus lookahead {lookahead} does not fit in OpusHead")))?;
335 let lookahead = usize::try_from(lookahead)
336 .map_err(|_| Error::Unsupported(format!("Opus lookahead {lookahead} does not fit in memory")))?;
337
338 Ok((bitrate, lookahead, pre_skip))
339 }
340
341 fn set_opus_bitrate(
342 inner: *mut OpusEncoder,
343 channels: u32,
344 bitrate: u64,
345 codec_rate: u32,
346 frame_size: usize,
347 ) -> Result<(), Error> {
348 let max = 300_000 * channels as u64;
349 let min = opus::bitrate_floor(codec_rate, frame_size).max(500);
352 if !(min..=max).contains(&bitrate) {
353 return Err(Error::Unsupported(format!(
354 "Opus bitrate must be between {min} and {max} bits per second for {channels} channel(s) at {frame_size} samples, got {bitrate}"
355 )));
356 }
357 Self::set_opus_ctl(inner, OPUS_SET_BITRATE_REQUEST, bitrate as i32, "OPUS_SET_BITRATE")
358 }
359
360 fn set_opus_ctl(inner: *mut OpusEncoder, request: i32, value: i32, name: &'static str) -> Result<(), Error> {
361 let rc = unsafe { opus_encoder_ctl_impl(inner, request, varargs![value]) };
363 if rc != OPUS_OK {
364 return Err(opus::error(rc, name));
365 }
366 Ok(())
367 }
368
369 fn get_opus_ctl(inner: *mut OpusEncoder, request: i32, name: &'static str) -> Result<i32, Error> {
370 let mut value = 0;
371 let rc = unsafe { opus_encoder_ctl_impl(inner, request, varargs![&mut value]) };
374 if rc != OPUS_OK {
375 return Err(opus::error(rc, name));
376 }
377 Ok(value)
378 }
379
380 pub fn config(&self) -> &Config {
382 &self.config
383 }
384
385 pub fn codec(&self) -> Codec {
388 self.config.codec
389 }
390
391 pub fn codec_rate(&self) -> u32 {
394 self.codec_rate
395 }
396
397 pub fn codec_channels(&self) -> u32 {
400 self.codec_channels
401 }
402
403 pub fn frame_size(&self) -> usize {
406 self.frame_size
407 }
408
409 pub fn bitrate(&self) -> u64 {
411 self.bitrate
412 }
413
414 pub fn set_bitrate(&mut self, bitrate: u64) -> Result<(), Error> {
416 let Backend::Opus(opus) = &mut self.backend else {
417 return Err(Error::Unsupported("pcm bitrate is fixed".into()));
418 };
419 if bitrate != self.bitrate {
420 Self::set_opus_bitrate(
421 opus.inner,
422 self.codec_channels,
423 bitrate,
424 self.codec_rate,
425 self.frame_size,
426 )?;
427 self.bitrate = bitrate;
428 self.config.bitrate = Some(bitrate as u32);
429 }
430 Ok(())
431 }
432
433 pub(super) fn reset(&mut self) {
435 if let Backend::Opus(opus) = &mut self.backend {
436 let rc = unsafe { opus_encoder_ctl_impl(opus.inner, OPUS_RESET_STATE, varargs![]) };
438 debug_assert_eq!(rc, OPUS_OK, "OPUS_RESET_STATE failed with {rc}");
439 }
440 self.started = false;
441 }
442
443 pub(super) fn started(&self) -> bool {
445 self.started
446 }
447
448 pub fn encode(&mut self, pcm: &[f32]) -> Result<Encoded, Error> {
454 let expected = self.frame_size * self.codec_channels as usize;
455 if pcm.len() != expected {
456 return Err(Error::Misaligned {
457 got: std::mem::size_of_val(pcm),
458 expected: expected * std::mem::size_of::<f32>(),
459 });
460 }
461 let encoded = match &mut self.backend {
462 Backend::Opus(opus) => {
463 let n = unsafe {
466 opus_encode_float(
467 opus.inner,
468 pcm.as_ptr(),
469 self.frame_size as i32,
470 opus.scratch.as_mut_ptr(),
471 opus.scratch.len() as i32,
472 )
473 };
474 if n < 0 {
475 return Err(crate::opus::error(n, "opus_encode_float"));
476 }
477 let payload = Bytes::copy_from_slice(&opus.scratch[..n as usize]);
478 let activity = crate::opus::activity(&payload, false);
483 Encoded { payload, activity }
484 }
485 Backend::Pcm => {
486 let mut payload = Vec::with_capacity(std::mem::size_of_val(pcm));
487 for sample in pcm {
488 payload.extend_from_slice(&sample.to_le_bytes());
489 }
490 Encoded::new(payload.into())
491 }
492 };
493 self.started = true;
494 Ok(encoded)
495 }
496
497 pub fn finish(mut self, pcm: &[f32]) -> Result<Finish, Error> {
507 let channels = self.codec_channels as usize;
508 let frame_samples = self.frame_size * channels;
509 if pcm.len() > frame_samples || !pcm.len().is_multiple_of(channels) {
510 return Err(Error::Misaligned {
511 got: std::mem::size_of_val(pcm),
512 expected: if pcm.len() > frame_samples {
513 frame_samples * std::mem::size_of::<f32>()
514 } else {
515 pcm.len().next_multiple_of(channels) * std::mem::size_of::<f32>()
516 },
517 });
518 }
519
520 let source_frames = pcm.len() / channels;
521 let mut packets = Vec::new();
522 let padding = if pcm.is_empty() {
523 0
524 } else {
525 let mut frame = Vec::with_capacity(frame_samples);
526 frame.extend_from_slice(pcm);
527 frame.resize(frame_samples, 0.0);
528 let padding = (frame_samples - pcm.len()) / channels;
529 packets.push(self.encode(&frame)?);
530 padding
531 };
532
533 if !self.started {
534 return Ok(Finish {
535 packets,
536 discard_padding: 0,
537 });
538 }
539
540 let drain = self.lookahead.saturating_sub(padding);
541 let silence = vec![0.0; frame_samples];
542 for _ in 0..drain.div_ceil(self.frame_size) {
543 packets.push(self.encode(&silence)?);
544 }
545
546 let discard_padding = packets
547 .len()
548 .saturating_mul(self.frame_size)
549 .saturating_sub(self.lookahead)
550 .saturating_sub(source_frames);
551
552 Ok(Finish {
553 packets,
554 discard_padding,
555 })
556 }
557
558 pub fn catalog(&self) -> hang::catalog::AudioConfig {
560 match self.config.codec {
561 Codec::Opus => {
562 let head = moq_mux::codec::opus::Config::new(self.codec_rate, self.codec_channels)
565 .with_pre_skip(self.pre_skip)
566 .encode()
567 .expect("opus encoder channels validated to mono/stereo");
568
569 let mut config = hang::catalog::AudioConfig::new(
570 hang::catalog::AudioCodec::Opus,
571 self.codec_rate,
572 self.codec_channels,
573 );
574 config.bitrate = self.config.bitrate.map(u64::from);
575 config.description = Some(head);
576 config.container = hang::catalog::Container::Legacy;
577 config
578 }
579 Codec::Pcm => {
580 let mut config = hang::catalog::AudioConfig::new(
581 hang::catalog::AudioCodec::Pcm,
582 self.codec_rate,
583 self.codec_channels,
584 );
585 config.bitrate = Some(
586 pcm::bitrate(self.codec_rate, self.codec_channels)
587 .expect("pcm encoder bitrate validated at construction"),
588 );
589 config.container = hang::catalog::Container::Legacy;
590 config
591 }
592 }
593 }
594}
595
596impl Drop for Opus {
597 fn drop(&mut self) {
598 unsafe { opus_encoder_destroy(self.inner) };
600 }
601}
602
603#[cfg(test)]
604mod tests {
605 use super::*;
606 use crate::Activity;
607 use crate::decode::Decoder;
608
609 fn sine(freq: f32, sample_rate: u32, channels: u32, frames: usize) -> Vec<f32> {
610 let mut out = Vec::with_capacity(frames * channels as usize);
611 for i in 0..frames {
612 let t = i as f32 / sample_rate as f32;
613 let v = (2.0 * std::f32::consts::PI * freq * t).sin() * 0.5;
614 for _ in 0..channels {
615 out.push(v);
616 }
617 }
618 out
619 }
620
621 fn stereo_48k() -> Input {
622 Input {
623 format: Format::F32,
624 sample_rate: 48_000,
625 channels: 2,
626 }
627 }
628
629 fn opus_inner(encoder: &Encoder) -> *mut OpusEncoder {
630 let Backend::Opus(opus) = &encoder.backend else {
631 panic!("expected Opus encoder");
632 };
633 opus.inner
634 }
635
636 #[test]
637 fn opus_encode_then_decode_keeps_signal_close() {
638 let mut enc = Encoder::new(&Config {
639 bitrate: Some(96_000),
640 ..Config::new(stereo_48k())
641 })
642 .unwrap();
643
644 let cfg = enc.catalog();
645 let mut dec = Decoder::new(&cfg).unwrap();
646
647 let frame = sine(440.0, 48_000, 2, enc.frame_size());
648 for _ in 0..5 {
649 let pkt = enc.encode(&frame).unwrap();
650 let _ = dec.decode(&pkt.payload).unwrap();
651 }
652
653 let pkt = enc.encode(&frame).unwrap();
654 let decoded = dec.decode(&pkt.payload).unwrap();
655 assert_eq!(decoded.samples.len(), frame.len());
656
657 let energy_in: f32 = frame.iter().map(|s| s * s).sum();
658 let energy_out: f32 = decoded.samples.iter().map(|s| s * s).sum();
659 let ratio = energy_out / energy_in;
660 assert!(
661 (0.5..2.0).contains(&ratio),
662 "output energy ratio {ratio:.3} should be close to 1"
663 );
664 }
665
666 #[test]
667 fn opus_classification_covers_dtx_loss_and_recovery() {
668 let mut enc = Encoder::new(&Config {
669 dtx: true,
670 bitrate: Some(24_000),
671 ..Config::new(Input {
672 channels: 1,
673 ..Input::default()
674 })
675 })
676 .unwrap();
677 let mut dec = Decoder::new(&enc.catalog()).unwrap();
678 let active = sine(440.0, enc.codec_rate(), enc.codec_channels(), enc.frame_size());
679 let silence = vec![0.0; enc.frame_size()];
680
681 let packet = enc.encode(&active).unwrap();
682 assert_eq!(packet.activity, Activity::Active);
683 assert_eq!(dec.decode(&packet.payload).unwrap().activity, Activity::Active);
684
685 let mut dtx = None;
686 for _ in 0..100 {
687 let packet = enc.encode(&silence).unwrap();
688 let decoded = dec.decode(&packet.payload).unwrap();
689 assert_eq!(decoded.activity, packet.activity);
690 if packet.activity.is_dtx() {
691 dtx = Some(packet);
692 break;
693 }
694 }
695 let dtx = dtx.expect("silence should enter Opus DTX");
696 assert!(
697 dtx.payload.len() <= 2,
698 "withholding audio is a bare TOC, got {} bytes",
699 dtx.payload.len()
700 );
701
702 let mut padded = dtx.payload.to_vec();
705 let original_len = padded.len();
706 padded.resize(original_len + 8, 0);
707 let rc =
710 unsafe { unsafe_libopus::opus_packet_pad(padded.as_mut_ptr(), original_len as i32, padded.len() as i32) };
711 assert_eq!(rc, unsafe_libopus::OPUS_OK);
712 assert_eq!(dec.decode(&padded).unwrap().activity, Activity::Dtx);
713
714 assert_eq!(dec.decode(&[]).unwrap().activity, Activity::Dtx);
717
718 assert!(matches!(dec.decode(&[0xff; 3]), Err(Error::Decode(_))));
720 assert_eq!(dec.decode(&[]).unwrap().activity, Activity::Dtx);
721
722 let mut recovered = false;
723 for _ in 0..10 {
724 let packet = enc.encode(&active).unwrap();
725 let decoded = dec.decode(&packet.payload).unwrap();
726 assert_eq!(decoded.activity, packet.activity);
727 if packet.activity.is_active() {
728 recovered = true;
729 break;
730 }
731 }
732 assert!(recovered, "active audio should exit Opus DTX");
733 assert_eq!(dec.decode(&[]).unwrap().activity, Activity::Active);
734 }
735
736 #[test]
737 fn opus_rejects_a_bitrate_that_would_code_nothing() {
738 let starved = Encoder::new(&Config {
739 dtx: true,
740 bitrate: Some(500),
741 ..Config::new(Input {
742 channels: 1,
743 ..Input::default()
744 })
745 });
746 assert!(
747 matches!(starved, Err(Error::Unsupported(_))),
748 "500 bps at 20 ms is under the 1200 bps floor"
749 );
750
751 let short = Encoder::new(&Config {
753 bitrate: Some(6_000),
754 frame_duration: Duration::from_micros(2_500),
755 ..Config::new(Input {
756 channels: 1,
757 ..Input::default()
758 })
759 });
760 assert!(matches!(short, Err(Error::Unsupported(_))));
761
762 let mut enc = Encoder::new(&Config {
765 dtx: true,
766 bitrate: Some(24_000),
767 ..Config::new(Input {
768 channels: 1,
769 ..Input::default()
770 })
771 })
772 .unwrap();
773 assert!(enc.set_bitrate(500).is_err());
774 assert_eq!(enc.bitrate(), 24_000, "a refused retune leaves the bitrate alone");
775 }
776
777 #[test]
782 fn opus_reports_silence_between_refreshes() {
783 let mut enc = Encoder::new(&Config {
784 dtx: true,
785 bitrate: Some(24_000),
786 ..Config::new(Input {
787 channels: 1,
788 ..Input::default()
789 })
790 })
791 .unwrap();
792 let mut dec = Decoder::new(&enc.catalog()).unwrap();
793 let loud = sine(440.0, enc.codec_rate(), enc.codec_channels(), enc.frame_size());
794 let silence = vec![0.0; enc.frame_size()];
795
796 for _ in 0..6 {
797 let packet = enc.encode(&loud).unwrap();
798 assert_eq!(packet.activity, Activity::Active);
799 }
800
801 let mut dtx = 0;
802 let mut refreshes = 0;
803 for _ in 0..200 {
804 let packet = enc.encode(&silence).unwrap();
805 assert_eq!(
806 dec.decode(&packet.payload).unwrap().activity,
807 packet.activity,
808 "both sides read the same packet"
809 );
810 match packet.activity {
811 Activity::Dtx => dtx += 1,
812 _ if dtx > 0 => refreshes += 1,
813 _ => {}
814 }
815 }
816
817 assert!(dtx > 150, "silence should mostly withhold audio, got {dtx} of 200");
818 assert!(refreshes > 0, "a silence run should be refreshed periodically");
819 assert!(
820 refreshes < dtx / 10,
821 "refreshes should be rare against the run they interrupt, got {refreshes} vs {dtx}"
822 );
823 }
824
825 #[test]
826 fn opus_rejects_unsupported_frame_duration() {
827 let err = Encoder::new(&Config {
828 frame_duration: Duration::from_millis(15),
829 ..Config::new(Input::default())
830 });
831 assert!(matches!(err, Err(Error::Unsupported(_))));
832 }
833
834 #[test]
835 fn opus_rejects_misaligned_input() {
836 let mut enc = Encoder::new(&Config::new(Input::default())).unwrap();
837 assert!(matches!(enc.encode(&[0.0f32; 100]), Err(Error::Misaligned { .. })));
838 }
839
840 #[test]
841 fn opus_catalog_includes_opushead() {
842 let enc = Encoder::new(&Config {
843 bitrate: Some(64_000),
844 ..Config::new(stereo_48k())
845 })
846 .unwrap();
847 let cfg = enc.catalog();
848 assert_eq!(cfg.sample_rate, 48_000);
849 assert_eq!(cfg.channel_count, 2);
850 assert_eq!(cfg.bitrate, Some(64_000));
851 let desc = cfg.description.expect("OpusHead should be present");
852 assert_eq!(desc.len(), 19);
853 let head = moq_mux::codec::opus::Config::parse(&mut desc.as_ref()).unwrap();
854 assert_eq!(head.pre_skip, enc.pre_skip);
855 assert_eq!(head.pre_skip, 312);
856 }
857
858 #[test]
859 fn opus_decoder_trims_encoder_lookahead_once() {
860 let mut enc = Encoder::new(&Config::new(stereo_48k())).unwrap();
861 let mut dec = Decoder::new(&enc.catalog()).unwrap();
862 let frame = vec![0.0; enc.frame_size() * enc.codec_channels() as usize];
863
864 let first = dec.decode(&enc.encode(&frame).unwrap().payload).unwrap();
865 assert_eq!(
866 first.samples.len(),
867 (enc.frame_size() - enc.pre_skip as usize) * enc.codec_channels() as usize
868 );
869
870 let second = dec.decode(&enc.encode(&frame).unwrap().payload).unwrap();
871 assert_eq!(second.samples.len(), frame.len());
872 }
873
874 #[test]
875 fn opus_finish_accounts_for_partial_frame_padding() {
876 let enc = Encoder::new(&Config::new(Input {
877 channels: 1,
878 ..Input::default()
879 }))
880 .unwrap();
881
882 let packets = enc.finish(&vec![0.0; 600]).unwrap();
885 assert_eq!(packets.packets().len(), 1);
886 assert_eq!(packets.discard_padding(), 48);
887 }
888
889 #[test]
890 fn opus_finish_drains_lookahead_across_multiple_short_packets() {
891 let mut enc = Encoder::new(&Config {
892 frame_duration: Duration::from_micros(2_500),
893 ..Config::new(Input {
894 channels: 1,
895 ..Input::default()
896 })
897 })
898 .unwrap();
899 let frame = vec![0.0; enc.frame_size()];
900 enc.encode(&frame).unwrap();
901
902 let packets = enc.finish(&[]).unwrap();
904 assert_eq!(packets.packets().len(), 3);
905 assert_eq!(packets.discard_padding(), 48);
906 }
907
908 #[test]
909 fn reset_drops_pending_opus_lookahead() {
910 let mut enc = Encoder::new(&Config::new(Input {
911 channels: 1,
912 ..Input::default()
913 }))
914 .unwrap();
915 let mut old = vec![0.0; enc.frame_size()];
916 old[enc.frame_size() - 1] = 1.0;
917 enc.encode(&old).unwrap();
918
919 enc.reset();
920 let next = vec![0.0; enc.frame_size()];
921 let actual = enc.encode(&next).unwrap();
922 let mut decoder = Decoder::new(&enc.catalog()).unwrap();
923 let decoded = decoder.decode(&actual.payload).unwrap();
924 let peak = decoded
925 .samples
926 .iter()
927 .fold(0.0f32, |peak, sample| peak.max(sample.abs()));
928 assert!(peak < 0.001, "pre-reset impulse leaked into the next epoch: {peak}");
929
930 enc.reset();
931 let finish = enc.finish(&[]).unwrap();
932 assert!(finish.packets().is_empty());
933 assert_eq!(finish.discard_padding(), 0);
934 }
935
936 #[test]
937 fn opus_runtime_bitrate_updates_encoder_state() {
938 let mut enc = Encoder::new(&Config {
939 bitrate: Some(64_000),
940 ..Config::new(stereo_48k())
941 })
942 .unwrap();
943
944 enc.set_bitrate(32_000).unwrap();
945 assert_eq!(enc.bitrate(), 32_000);
946 assert_eq!(enc.config().bitrate, Some(32_000));
947 assert_eq!(
948 Encoder::get_opus_ctl(
949 opus_inner(&enc),
950 unsafe_libopus::OPUS_GET_BITRATE_REQUEST,
951 "OPUS_GET_BITRATE"
952 )
953 .unwrap(),
954 32_000
955 );
956 }
957
958 #[test]
959 fn opus_runtime_bitrate_rejects_values_libopus_would_clamp() {
960 let mut enc = Encoder::new(&Config::new(stereo_48k())).unwrap();
961 let original = enc.bitrate();
962 assert!(enc.set_bitrate(1).is_err());
963 assert!(enc.set_bitrate(600_001).is_err());
964 assert_eq!(enc.bitrate(), original);
965 }
966
967 #[test]
968 fn opus_applies_fec_and_dtx_controls() {
969 let enc = Encoder::new(&Config {
970 fec: true,
971 dtx: true,
972 ..Config::new(stereo_48k())
973 })
974 .unwrap();
975
976 assert_eq!(
977 Encoder::get_opus_ctl(
978 opus_inner(&enc),
979 unsafe_libopus::OPUS_GET_INBAND_FEC_REQUEST,
980 "OPUS_GET_INBAND_FEC"
981 )
982 .unwrap(),
983 1
984 );
985 assert_eq!(
986 Encoder::get_opus_ctl(opus_inner(&enc), unsafe_libopus::OPUS_GET_DTX_REQUEST, "OPUS_GET_DTX").unwrap(),
987 1
988 );
989 }
990
991 #[test]
992 fn codec_roundtrips_as_str() {
993 assert_eq!(Codec::Opus.as_str(), "opus");
994 assert_eq!(Codec::Opus.to_string(), "opus");
995 assert_eq!("opus".parse::<Codec>().unwrap(), Codec::Opus);
996 assert_eq!(Codec::Pcm.as_str(), "pcm");
997 assert_eq!(Codec::Pcm.to_string(), "pcm");
998 assert_eq!("pcm".parse::<Codec>().unwrap(), Codec::Pcm);
999 assert!("aac".parse::<Codec>().is_err());
1000 }
1001
1002 #[test]
1003 fn config_sample_rate_overrides_the_codec_rate() {
1004 let enc = Encoder::new(&Config {
1005 sample_rate: Some(24_000),
1006 ..Config::new(Input {
1007 sample_rate: 48_000,
1008 channels: 1,
1009 ..Input::default()
1010 })
1011 })
1012 .unwrap();
1013 assert_eq!(enc.codec_rate(), 24_000);
1014 assert_eq!(enc.catalog().sample_rate, 24_000);
1015 assert_eq!(enc.pre_skip, 312);
1016 }
1017
1018 #[test]
1019 fn pcm_roundtrip_is_lossless() {
1020 let mut enc = Encoder::new(&Config {
1021 codec: Codec::Pcm,
1022 ..Config::new(stereo_48k())
1023 })
1024 .unwrap();
1025 let mut dec = Decoder::new(&enc.catalog()).unwrap();
1026 let input = sine(440.0, enc.codec_rate(), enc.codec_channels(), enc.frame_size());
1027
1028 let packet = enc.encode(&input).unwrap();
1029 let output = dec.decode(&packet.payload).unwrap();
1030
1031 assert_eq!(output.samples, input);
1032 }
1033
1034 #[test]
1035 fn pcm_catalog_declares_fixed_bitrate() {
1036 let enc = Encoder::new(&Config {
1037 codec: Codec::Pcm,
1038 ..Config::new(stereo_48k())
1039 })
1040 .unwrap();
1041 let catalog = enc.catalog();
1042
1043 assert_eq!(catalog.codec, hang::catalog::AudioCodec::Pcm);
1044 assert_eq!(catalog.bitrate, Some(48_000 * 2 * 32));
1045 assert_eq!(catalog.description, None);
1046 }
1047
1048 #[test]
1049 fn pcm_rejects_runtime_bitrate_change() {
1050 let mut enc = Encoder::new(&Config {
1051 codec: Codec::Pcm,
1052 ..Config::new(stereo_48k())
1053 })
1054 .unwrap();
1055 let bitrate = enc.bitrate();
1056
1057 assert!(matches!(enc.set_bitrate(bitrate), Err(Error::Unsupported(_))));
1058 assert_eq!(enc.bitrate(), bitrate);
1059 }
1060
1061 #[test]
1062 fn pcm_rejects_fractional_sample_frame_duration() {
1063 let err = Encoder::new(&Config {
1064 codec: Codec::Pcm,
1065 frame_duration: Duration::from_micros(2_500),
1066 ..Config::new(Input {
1067 sample_rate: 44_100,
1068 ..Input::default()
1069 })
1070 });
1071 assert!(matches!(err, Err(Error::Unsupported(_))));
1072 }
1073
1074 #[test]
1075 fn pcm_rejects_bitrate_overflow() {
1076 let err = Encoder::new(&Config {
1077 codec: Codec::Pcm,
1078 frame_duration: Duration::from_secs(1),
1079 ..Config::new(Input {
1080 sample_rate: u32::MAX,
1081 channels: u32::MAX,
1082 ..Input::default()
1083 })
1084 });
1085 assert!(matches!(err, Err(Error::Unsupported(_))));
1086 }
1087}