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, OpusEncoder, opus_encode_float, opus_encoder_create,
14 opus_encoder_ctl_impl, opus_encoder_destroy, varargs,
15};
16
17use super::Encoded;
18use crate::opus;
19use crate::pcm;
20use crate::{Error, Format, Layout};
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)]
68#[non_exhaustive]
69pub struct Input {
70 pub format: Format,
72 pub sample_rate: u32,
74 pub layout: Layout,
76}
77
78impl Input {
79 pub fn new(sample_rate: u32, layout: Layout) -> Self {
81 Self {
82 format: Format::F32,
83 sample_rate,
84 layout,
85 }
86 }
87}
88
89impl Default for Input {
90 fn default() -> Self {
91 Self::new(48_000, Layout::Stereo)
92 }
93}
94
95#[derive(Clone, Debug)]
97#[non_exhaustive]
98pub struct Settings {
99 pub codec: Codec,
101 pub sample_rate: u32,
103 pub layout: Layout,
105 pub bitrate: Option<moq_net::bandwidth::Rate>,
113 pub dtx: bool,
115 pub frame_duration: Duration,
118}
119
120impl Settings {
121 pub fn new(sample_rate: u32, layout: Layout) -> Self {
123 Self {
124 codec: Codec::default(),
125 sample_rate,
126 layout,
127 bitrate: None,
128 dtx: false,
129 frame_duration: Duration::from_millis(20),
130 }
131 }
132
133 pub fn from_input(codec: Codec, input: &Input) -> Self {
135 let sample_rate = match codec {
136 Codec::Opus => opus::pick_rate(input.sample_rate),
137 Codec::Pcm => input.sample_rate,
138 };
139 Self {
140 codec,
141 sample_rate,
142 layout: input.layout,
143 ..Self::default()
144 }
145 }
146}
147
148impl Default for Settings {
149 fn default() -> Self {
150 Self::new(48_000, Layout::Stereo)
151 }
152}
153
154pub struct Encoder {
161 backend: Backend,
162 settings: Settings,
163 codec_rate: u32,
165 codec_channels: u32,
167 bitrate: u64,
169 pre_skip: u16,
171 lookahead: usize,
173 frame_size: usize,
174 started: bool,
176}
177
178enum Backend {
179 Opus(Opus),
180 Pcm,
181}
182
183struct Opus {
184 inner: *mut OpusEncoder,
185 scratch: Vec<u8>,
186}
187
188unsafe impl Send for Opus {}
192
193pub struct Finish {
195 packets: Vec<Encoded>,
196 discard_padding: usize,
197}
198
199impl Finish {
200 pub fn packets(&self) -> &[Encoded] {
202 &self.packets
203 }
204
205 pub fn discard_padding(&self) -> usize {
207 self.discard_padding
208 }
209
210 pub fn into_packets(self) -> Vec<Encoded> {
212 self.packets
213 }
214}
215
216impl Encoder {
217 pub fn new(settings: &Settings) -> Result<Self, Error> {
219 settings.layout.validate()?;
220 match settings.codec {
221 Codec::Opus => Self::new_opus(settings.clone()),
222 Codec::Pcm => Self::new_pcm(settings.clone()),
223 }
224 }
225
226 fn new_opus(settings: Settings) -> Result<Self, Error> {
227 let codec_rate = settings.sample_rate;
228 opus::validate_rate(codec_rate)?;
229
230 let codec_channels = settings.layout.channels();
231 if !matches!(settings.layout, Layout::Mono | Layout::Stereo) {
232 return Err(Error::Unsupported("opus requires a named mono or stereo layout".into()));
233 }
234 let channels = opus::validate_channels(codec_channels)?;
235
236 let frame_size = opus::frame_size(codec_rate, settings.frame_duration)?;
237
238 let mut err = 0i32;
239 let inner = unsafe { opus_encoder_create(codec_rate as i32, channels, OPUS_APPLICATION_AUDIO, &mut err) };
241 if err != OPUS_OK || inner.is_null() {
242 return Err(opus::error(err, "opus_encoder_create"));
243 }
244
245 let configured = Self::configure_opus(inner, &settings, codec_rate, codec_channels, frame_size);
246 let (bitrate, lookahead, pre_skip) = match configured {
247 Ok(configured) => configured,
248 Err(err) => {
249 unsafe { opus_encoder_destroy(inner) };
251 return Err(err);
252 }
253 };
254
255 Ok(Self {
256 backend: Backend::Opus(Opus {
257 inner,
258 scratch: vec![0u8; MAX_PACKET_BYTES],
259 }),
260 settings,
261 codec_rate,
262 codec_channels,
263 bitrate,
264 pre_skip,
265 lookahead,
266 frame_size,
267 started: false,
268 })
269 }
270
271 fn new_pcm(settings: Settings) -> Result<Self, Error> {
272 if settings.bitrate.is_some() {
273 return Err(Error::Unsupported(
274 "pcm bitrate is fixed; leave Settings::bitrate unset".into(),
275 ));
276 }
277 if settings.dtx {
278 return Err(Error::Unsupported(
279 "pcm does not support discontinuous transmission".into(),
280 ));
281 }
282
283 let codec_rate = settings.sample_rate;
284 if codec_rate == 0 {
285 return Err(Error::Unsupported("pcm sample rate must be greater than zero".into()));
286 }
287
288 let codec_channels = settings.layout.channels();
289 if codec_channels == 0 {
290 return Err(Error::Unsupported("pcm channel count must be greater than zero".into()));
291 }
292 let frame_size = pcm::frame_size(codec_rate, settings.frame_duration)?;
293 pcm::frame_bytes(frame_size, codec_channels)?;
294 let bitrate = pcm::bitrate(codec_rate, codec_channels)?;
295 Ok(Self {
296 backend: Backend::Pcm,
297 settings,
298 codec_rate,
299 codec_channels,
300 bitrate,
301 pre_skip: 0,
302 lookahead: 0,
303 frame_size,
304 started: false,
305 })
306 }
307
308 fn configure_opus(
309 inner: *mut OpusEncoder,
310 settings: &Settings,
311 codec_rate: u32,
312 codec_channels: u32,
313 frame_size: usize,
314 ) -> Result<(u64, usize, u16), Error> {
315 if let Some(bitrate) = settings.bitrate {
316 Self::set_opus_bitrate(inner, codec_channels, bitrate.as_bps(), codec_rate, frame_size)?;
317 }
318 Self::set_opus_ctl(inner, OPUS_SET_DTX_REQUEST, i32::from(settings.dtx), "OPUS_SET_DTX")?;
319
320 let bitrate = Self::get_opus_ctl(inner, OPUS_GET_BITRATE_REQUEST, "OPUS_GET_BITRATE")?;
321 let bitrate = u64::try_from(bitrate)
322 .map_err(|_| Error::Unsupported(format!("Opus reported negative bitrate {bitrate}")))?;
323 let lookahead = Self::get_opus_ctl(inner, OPUS_GET_LOOKAHEAD_REQUEST, "OPUS_GET_LOOKAHEAD")?;
324 let lookahead = u64::try_from(lookahead)
325 .map_err(|_| Error::Unsupported(format!("Opus reported negative lookahead {lookahead}")))?;
326 let pre_skip = u16::try_from((lookahead * 48_000) / codec_rate as u64)
327 .map_err(|_| Error::Unsupported(format!("Opus lookahead {lookahead} does not fit in OpusHead")))?;
328 let lookahead = usize::try_from(lookahead)
329 .map_err(|_| Error::Unsupported(format!("Opus lookahead {lookahead} does not fit in memory")))?;
330
331 Ok((bitrate, lookahead, pre_skip))
332 }
333
334 fn set_opus_bitrate(
335 inner: *mut OpusEncoder,
336 channels: u32,
337 bitrate: u64,
338 codec_rate: u32,
339 frame_size: usize,
340 ) -> Result<(), Error> {
341 let max = 300_000 * channels as u64;
342 let min = opus::bitrate_floor(codec_rate, frame_size).max(500);
343 if !(min..=max).contains(&bitrate) {
344 return Err(Error::Unsupported(format!(
345 "Opus bitrate must be between {min} and {max} bits per second for {channels} channel(s) at {frame_size} samples, got {bitrate}"
346 )));
347 }
348 Self::set_opus_ctl(inner, OPUS_SET_BITRATE_REQUEST, bitrate as i32, "OPUS_SET_BITRATE")
349 }
350
351 fn set_opus_ctl(inner: *mut OpusEncoder, request: i32, value: i32, name: &'static str) -> Result<(), Error> {
352 let rc = unsafe { opus_encoder_ctl_impl(inner, request, varargs![value]) };
354 if rc != OPUS_OK {
355 return Err(opus::error(rc, name));
356 }
357 Ok(())
358 }
359
360 fn get_opus_ctl(inner: *mut OpusEncoder, request: i32, name: &'static str) -> Result<i32, Error> {
361 let mut value = 0;
362 let rc = unsafe { opus_encoder_ctl_impl(inner, request, varargs![&mut value]) };
365 if rc != OPUS_OK {
366 return Err(opus::error(rc, name));
367 }
368 Ok(value)
369 }
370
371 pub fn settings(&self) -> &Settings {
373 &self.settings
374 }
375
376 pub fn codec(&self) -> Codec {
379 self.settings.codec
380 }
381
382 pub fn codec_rate(&self) -> u32 {
385 self.codec_rate
386 }
387
388 pub fn codec_channels(&self) -> u32 {
391 self.codec_channels
392 }
393
394 pub fn frame_size(&self) -> usize {
397 self.frame_size
398 }
399
400 pub fn bitrate(&self) -> moq_net::bandwidth::Rate {
402 moq_net::bandwidth::Rate::from_bps(self.bitrate)
403 }
404
405 pub fn set_bitrate(&mut self, bitrate: moq_net::bandwidth::Rate) -> Result<(), Error> {
407 let Backend::Opus(opus) = &mut self.backend else {
408 return Err(Error::Unsupported("pcm bitrate is fixed".into()));
409 };
410 if bitrate.as_bps() != self.bitrate {
411 Self::set_opus_bitrate(
412 opus.inner,
413 self.codec_channels,
414 bitrate.as_bps(),
415 self.codec_rate,
416 self.frame_size,
417 )?;
418 self.bitrate = bitrate.as_bps();
419 self.settings.bitrate = Some(bitrate);
420 }
421 Ok(())
422 }
423
424 pub(super) fn reset(&mut self) {
426 if let Backend::Opus(opus) = &mut self.backend {
427 let rc = unsafe { opus_encoder_ctl_impl(opus.inner, OPUS_RESET_STATE, varargs![]) };
429 debug_assert_eq!(rc, OPUS_OK, "OPUS_RESET_STATE failed with {rc}");
430 }
431 self.started = false;
432 }
433
434 pub(super) fn started(&self) -> bool {
436 self.started
437 }
438
439 pub fn encode(&mut self, pcm: &[f32]) -> Result<Encoded, Error> {
445 let expected = self.frame_size * self.codec_channels as usize;
446 if pcm.len() != expected {
447 return Err(Error::Misaligned {
448 got: std::mem::size_of_val(pcm),
449 expected: expected * std::mem::size_of::<f32>(),
450 });
451 }
452 let encoded = match &mut self.backend {
453 Backend::Opus(opus) => {
454 let n = unsafe {
457 opus_encode_float(
458 opus.inner,
459 pcm.as_ptr(),
460 self.frame_size as i32,
461 opus.scratch.as_mut_ptr(),
462 opus.scratch.len() as i32,
463 )
464 };
465 if n < 0 {
466 return Err(crate::opus::error(n, "opus_encode_float"));
467 }
468 let payload = Bytes::copy_from_slice(&opus.scratch[..n as usize]);
469 let activity = crate::opus::activity(&payload, false);
470 Encoded { payload, activity }
471 }
472 Backend::Pcm => {
473 let mut payload = Vec::with_capacity(std::mem::size_of_val(pcm));
474 for sample in pcm {
475 payload.extend_from_slice(&sample.to_le_bytes());
476 }
477 Encoded::new(payload.into())
478 }
479 };
480 self.started = true;
481 Ok(encoded)
482 }
483
484 pub fn finish(mut self, pcm: &[f32]) -> Result<Finish, Error> {
494 self.drain(pcm)
495 }
496
497 pub(super) fn drain(&mut self, pcm: &[f32]) -> Result<Finish, Error> {
499 let channels = self.codec_channels as usize;
500 let frame_samples = self.frame_size * channels;
501 if pcm.len() > frame_samples || !pcm.len().is_multiple_of(channels) {
502 return Err(Error::Misaligned {
503 got: std::mem::size_of_val(pcm),
504 expected: if pcm.len() > frame_samples {
505 frame_samples * std::mem::size_of::<f32>()
506 } else {
507 pcm.len().next_multiple_of(channels) * std::mem::size_of::<f32>()
508 },
509 });
510 }
511
512 let source_frames = pcm.len() / channels;
513 let mut packets = Vec::new();
514 let padding = if pcm.is_empty() {
515 0
516 } else {
517 let mut frame = Vec::with_capacity(frame_samples);
518 frame.extend_from_slice(pcm);
519 frame.resize(frame_samples, 0.0);
520 let padding = (frame_samples - pcm.len()) / channels;
521 packets.push(self.encode(&frame)?);
522 padding
523 };
524
525 if !self.started {
526 return Ok(Finish {
527 packets,
528 discard_padding: 0,
529 });
530 }
531
532 let drain = self.lookahead.saturating_sub(padding);
533 let silence = vec![0.0; frame_samples];
534 for _ in 0..drain.div_ceil(self.frame_size) {
535 packets.push(self.encode(&silence)?);
536 }
537
538 let discard_padding = packets
539 .len()
540 .saturating_mul(self.frame_size)
541 .saturating_sub(self.lookahead)
542 .saturating_sub(source_frames);
543
544 Ok(Finish {
545 packets,
546 discard_padding,
547 })
548 }
549
550 pub fn catalog(&self) -> hang::catalog::AudioConfig {
552 match self.settings.codec {
553 Codec::Opus => {
554 let head = moq_mux::codec::opus::Config::new(self.codec_rate, self.codec_channels)
557 .with_pre_skip(self.pre_skip)
558 .encode()
559 .expect("opus encoder channels validated to mono/stereo");
560
561 let mut config = hang::catalog::AudioConfig::new(
562 hang::catalog::AudioCodec::Opus,
563 self.codec_rate,
564 self.codec_channels,
565 );
566 config.bitrate = self.settings.bitrate.map(moq_net::bandwidth::Rate::as_bps);
567 config.description = Some(head);
568 config.container = hang::catalog::Container::Legacy;
569 config
570 }
571 Codec::Pcm => {
572 let mut config = hang::catalog::AudioConfig::new(
573 hang::catalog::AudioCodec::Pcm,
574 self.codec_rate,
575 self.codec_channels,
576 );
577 config.bitrate = Some(
578 pcm::bitrate(self.codec_rate, self.codec_channels)
579 .expect("pcm encoder bitrate validated at construction"),
580 );
581 config.container = hang::catalog::Container::Legacy;
582 config
583 }
584 }
585 }
586}
587
588impl Drop for Opus {
589 fn drop(&mut self) {
590 unsafe { opus_encoder_destroy(self.inner) };
592 }
593}
594
595#[cfg(test)]
596mod tests {
597 use super::*;
598 use crate::decode::{Config as DecodeConfig, Decoder};
599
600 fn sine(freq: f32, sample_rate: u32, channels: u32, frames: usize) -> Vec<f32> {
601 let mut out = Vec::with_capacity(frames * channels as usize);
602 for i in 0..frames {
603 let t = i as f32 / sample_rate as f32;
604 let v = (2.0 * std::f32::consts::PI * freq * t).sin() * 0.5;
605 for _ in 0..channels {
606 out.push(v);
607 }
608 }
609 out
610 }
611
612 fn opus_inner(encoder: &Encoder) -> *mut OpusEncoder {
613 let Backend::Opus(opus) = &encoder.backend else {
614 panic!("expected Opus encoder");
615 };
616 opus.inner
617 }
618
619 #[test]
620 fn opus_encode_then_decode_keeps_signal_close() {
621 let mut enc = Encoder::new(&Settings {
622 bitrate: Some(moq_net::bandwidth::Rate::from_bps(96_000)),
623 ..Settings::default()
624 })
625 .unwrap();
626
627 let cfg = enc.catalog();
628 let mut dec = Decoder::new(&cfg, &DecodeConfig::default()).unwrap();
629
630 let frame = sine(440.0, 48_000, 2, enc.frame_size());
631 for _ in 0..5 {
632 let pkt = enc.encode(&frame).unwrap();
633 let _ = dec.decode(&pkt.payload).unwrap();
634 }
635
636 let pkt = enc.encode(&frame).unwrap();
637 let decoded = dec.decode(&pkt.payload).unwrap();
638 assert_eq!(decoded.samples.len(), frame.len());
639
640 let energy_in: f32 = frame.iter().map(|s| s * s).sum();
641 let energy_out: f32 = decoded.samples.iter().map(|s| s * s).sum();
642 let ratio = energy_out / energy_in;
643 assert!(
644 (0.5..2.0).contains(&ratio),
645 "output energy ratio {ratio:.3} should be close to 1"
646 );
647 }
648
649 #[test]
650 fn opus_rejects_unsupported_frame_duration() {
651 let err = Encoder::new(&Settings {
652 frame_duration: Duration::from_millis(15),
653 ..Settings::default()
654 });
655 assert!(matches!(err, Err(Error::Unsupported(_))));
656 }
657
658 #[test]
659 fn opus_rejects_misaligned_input() {
660 let mut enc = Encoder::new(&Settings::default()).unwrap();
661 assert!(matches!(enc.encode(&[0.0f32; 100]), Err(Error::Misaligned { .. })));
662 }
663
664 #[test]
665 fn opus_catalog_includes_opushead() {
666 let enc = Encoder::new(&Settings {
667 bitrate: Some(moq_net::bandwidth::Rate::from_bps(64_000)),
668 ..Settings::default()
669 })
670 .unwrap();
671 let cfg = enc.catalog();
672 assert_eq!(cfg.sample_rate, 48_000);
673 assert_eq!(cfg.channel_count, 2);
674 assert_eq!(cfg.bitrate, Some(64_000));
675 let desc = cfg.description.expect("OpusHead should be present");
676 assert_eq!(desc.len(), 19);
677 let head = moq_mux::codec::opus::Config::parse(&mut desc.as_ref()).unwrap();
678 assert_eq!(head.pre_skip, enc.pre_skip);
679 assert_eq!(head.pre_skip, 312);
680 }
681
682 #[test]
683 fn opus_decoder_trims_encoder_lookahead_once() {
684 let mut enc = Encoder::new(&Settings::default()).unwrap();
685 let mut dec = Decoder::new(&enc.catalog(), &DecodeConfig::default()).unwrap();
686 let frame = vec![0.0; enc.frame_size() * enc.codec_channels() as usize];
687
688 let first = dec.decode(&enc.encode(&frame).unwrap().payload).unwrap();
689 assert_eq!(
690 first.samples.len(),
691 (enc.frame_size() - enc.pre_skip as usize) * enc.codec_channels() as usize
692 );
693
694 let second = dec.decode(&enc.encode(&frame).unwrap().payload).unwrap();
695 assert_eq!(second.samples.len(), frame.len());
696 }
697
698 #[test]
699 fn opus_finish_accounts_for_partial_frame_padding() {
700 let enc = Encoder::new(&Settings::new(48_000, Layout::Mono)).unwrap();
701
702 let packets = enc.finish(&vec![0.0; 600]).unwrap();
705 assert_eq!(packets.packets().len(), 1);
706 assert_eq!(packets.discard_padding(), 48);
707 }
708
709 #[test]
710 fn opus_finish_drains_lookahead_across_multiple_short_packets() {
711 let mut enc = Encoder::new(&Settings {
712 frame_duration: Duration::from_micros(2_500),
713 ..Settings::new(48_000, Layout::Mono)
714 })
715 .unwrap();
716 let frame = vec![0.0; enc.frame_size()];
717 enc.encode(&frame).unwrap();
718
719 let packets = enc.finish(&[]).unwrap();
721 assert_eq!(packets.packets().len(), 3);
722 assert_eq!(packets.discard_padding(), 48);
723 }
724
725 #[test]
726 fn reset_drops_pending_opus_lookahead() {
727 let mut enc = Encoder::new(&Settings::new(48_000, Layout::Mono)).unwrap();
728 let mut old = vec![0.0; enc.frame_size()];
729 old[enc.frame_size() - 1] = 1.0;
730 enc.encode(&old).unwrap();
731
732 enc.reset();
733 let next = vec![0.0; enc.frame_size()];
734 let actual = enc.encode(&next).unwrap();
735 let mut decoder = Decoder::new(&enc.catalog(), &DecodeConfig::default()).unwrap();
736 let decoded = decoder.decode(&actual.payload).unwrap();
737 let peak = decoded
738 .samples
739 .iter()
740 .fold(0.0f32, |peak, sample| peak.max(sample.abs()));
741 assert!(peak < 0.001, "pre-reset impulse leaked into the next epoch: {peak}");
742
743 enc.reset();
744 let finish = enc.finish(&[]).unwrap();
745 assert!(finish.packets().is_empty());
746 assert_eq!(finish.discard_padding(), 0);
747 }
748
749 #[test]
750 fn opus_runtime_bitrate_updates_encoder_state() {
751 let mut enc = Encoder::new(&Settings {
752 bitrate: Some(moq_net::bandwidth::Rate::from_bps(64_000)),
753 ..Settings::default()
754 })
755 .unwrap();
756
757 enc.set_bitrate(moq_net::bandwidth::Rate::from_bps(32_000)).unwrap();
758 assert_eq!(enc.bitrate(), moq_net::bandwidth::Rate::from_bps(32_000));
759 assert_eq!(enc.settings().bitrate, Some(moq_net::bandwidth::Rate::from_bps(32_000)));
760 assert_eq!(
761 Encoder::get_opus_ctl(
762 opus_inner(&enc),
763 unsafe_libopus::OPUS_GET_BITRATE_REQUEST,
764 "OPUS_GET_BITRATE"
765 )
766 .unwrap(),
767 32_000
768 );
769 }
770
771 #[test]
772 fn opus_runtime_bitrate_rejects_values_libopus_would_clamp() {
773 let mut enc = Encoder::new(&Settings::default()).unwrap();
774 let original = enc.bitrate();
775 assert!(enc.set_bitrate(moq_net::bandwidth::Rate::from_bps(1)).is_err());
776 assert!(enc.set_bitrate(moq_net::bandwidth::Rate::from_bps(600_001)).is_err());
777 assert_eq!(enc.bitrate(), original);
778 }
779
780 #[test]
781 fn opus_applies_dtx_control() {
782 let enc = Encoder::new(&Settings {
783 dtx: true,
784 ..Settings::default()
785 })
786 .unwrap();
787
788 assert_eq!(
789 Encoder::get_opus_ctl(opus_inner(&enc), unsafe_libopus::OPUS_GET_DTX_REQUEST, "OPUS_GET_DTX").unwrap(),
790 1
791 );
792 }
793
794 #[test]
795 fn codec_roundtrips_as_str() {
796 assert_eq!(Codec::Opus.as_str(), "opus");
797 assert_eq!(Codec::Opus.to_string(), "opus");
798 assert_eq!("opus".parse::<Codec>().unwrap(), Codec::Opus);
799 assert_eq!(Codec::Pcm.as_str(), "pcm");
800 assert_eq!(Codec::Pcm.to_string(), "pcm");
801 assert_eq!("pcm".parse::<Codec>().unwrap(), Codec::Pcm);
802 assert!("aac".parse::<Codec>().is_err());
803 }
804
805 #[test]
806 fn settings_fix_the_codec_rate() {
807 let enc = Encoder::new(&Settings::new(24_000, Layout::Mono)).unwrap();
808 assert_eq!(enc.codec_rate(), 24_000);
809 assert_eq!(enc.catalog().sample_rate, 24_000);
810 assert_eq!(enc.pre_skip, 312);
811 }
812
813 #[test]
814 fn pcm_roundtrip_is_lossless() {
815 let mut enc = Encoder::new(&Settings {
816 codec: Codec::Pcm,
817 ..Settings::default()
818 })
819 .unwrap();
820 let mut dec = Decoder::new(&enc.catalog(), &DecodeConfig::default()).unwrap();
821 let input = sine(440.0, enc.codec_rate(), enc.codec_channels(), enc.frame_size());
822
823 let packet = enc.encode(&input).unwrap();
824 let output = dec.decode(&packet.payload).unwrap();
825
826 assert_eq!(output.samples, input);
827 }
828
829 #[test]
830 fn pcm_catalog_declares_fixed_bitrate() {
831 let enc = Encoder::new(&Settings {
832 codec: Codec::Pcm,
833 ..Settings::default()
834 })
835 .unwrap();
836 let catalog = enc.catalog();
837
838 assert_eq!(catalog.codec, hang::catalog::AudioCodec::Pcm);
839 assert_eq!(catalog.bitrate, Some(48_000 * 2 * 32));
840 assert_eq!(catalog.description, None);
841 }
842
843 #[test]
844 fn pcm_rejects_runtime_bitrate_change() {
845 let mut enc = Encoder::new(&Settings {
846 codec: Codec::Pcm,
847 ..Settings::default()
848 })
849 .unwrap();
850 let bitrate = enc.bitrate();
851
852 assert!(matches!(enc.set_bitrate(bitrate), Err(Error::Unsupported(_))));
853 assert_eq!(enc.bitrate(), bitrate);
854 }
855
856 #[test]
857 fn pcm_rejects_fractional_sample_frame_duration() {
858 let err = Encoder::new(&Settings {
859 codec: Codec::Pcm,
860 frame_duration: Duration::from_micros(2_500),
861 ..Settings::new(44_100, Layout::Stereo)
862 });
863 assert!(matches!(err, Err(Error::Unsupported(_))));
864 }
865
866 #[test]
867 fn pcm_rejects_bitrate_overflow() {
868 let err = Encoder::new(&Settings {
869 codec: Codec::Pcm,
870 frame_duration: Duration::from_secs(1),
871 ..Settings::new(u32::MAX, Layout::Discrete(u32::MAX))
872 });
873 assert!(matches!(err, Err(Error::Unsupported(_))));
874 }
875
876 #[test]
877 fn pcm_rejects_opus_only_settings() {
878 let settings = Settings {
879 codec: Codec::Pcm,
880 dtx: true,
881 ..Settings::default()
882 };
883 assert!(matches!(Encoder::new(&settings), Err(Error::Unsupported(_))));
884 }
885
886 #[test]
887 fn pcm_preserves_discrete_multichannel_layout() {
888 let settings = Settings {
889 codec: Codec::Pcm,
890 ..Settings::new(48_000, Layout::Discrete(3))
891 };
892 let mut encoder = Encoder::new(&settings).unwrap();
893 let catalog = encoder.catalog();
894 let mut decoder = Decoder::new(&catalog, &DecodeConfig::default()).unwrap();
895 let input = [0.1, 0.2, 0.3].repeat(encoder.frame_size());
896 let output = decoder.decode(&encoder.encode(&input).unwrap().payload).unwrap();
897
898 assert_eq!(decoder.layout(), Layout::Discrete(3));
899 assert_eq!(output.samples, input);
900 }
901
902 #[test]
903 fn opus_refuses_discrete_layout() {
904 let settings = Settings::new(48_000, Layout::Discrete(2));
905 assert!(matches!(Encoder::new(&settings), Err(Error::Unsupported(_))));
906 }
907}