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_SET_BITRATE_REQUEST,
13 OPUS_SET_DTX_REQUEST, OPUS_SET_INBAND_FEC_REQUEST, OpusEncoder, opus_encode_float, opus_encoder_create,
14 opus_encoder_ctl_impl, opus_encoder_destroy, varargs,
15};
16
17use crate::opus;
18use crate::pcm;
19use crate::{Error, Format};
20
21const MAX_PACKET_BYTES: usize = 4_000;
23
24#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
27#[non_exhaustive]
28pub enum Codec {
29 #[default]
31 Opus,
32 Pcm,
34}
35
36impl Codec {
37 pub fn as_str(self) -> &'static str {
40 match self {
41 Self::Opus => "opus",
42 Self::Pcm => "pcm",
43 }
44 }
45}
46
47impl std::fmt::Display for Codec {
48 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49 f.write_str(self.as_str())
50 }
51}
52
53impl FromStr for Codec {
54 type Err = Error;
55
56 fn from_str(s: &str) -> Result<Self, Self::Err> {
57 match s {
58 "opus" => Ok(Self::Opus),
59 "pcm" => Ok(Self::Pcm),
60 other => Err(Error::Unsupported(format!("unknown codec: {other}"))),
61 }
62 }
63}
64
65#[derive(Clone, Debug)]
72pub struct Input {
73 pub format: Format,
75 pub sample_rate: u32,
77 pub channels: u32,
79}
80
81impl Default for Input {
82 fn default() -> Self {
83 Self {
84 format: Format::F32,
85 sample_rate: 48_000,
86 channels: 2,
87 }
88 }
89}
90
91#[derive(Clone, Debug)]
100#[non_exhaustive]
101pub struct Config {
102 pub input: Input,
104 pub codec: Codec,
106 pub sample_rate: Option<u32>,
109 pub channels: Option<u32>,
112 pub bitrate: Option<u32>,
115 pub fec: bool,
117 pub dtx: bool,
119 pub frame_duration: Duration,
122}
123
124impl Config {
125 pub fn new(input: Input) -> Self {
127 Self {
128 input,
129 codec: Codec::default(),
130 sample_rate: None,
131 channels: None,
132 bitrate: None,
133 fec: false,
134 dtx: false,
135 frame_duration: Duration::from_millis(20),
136 }
137 }
138}
139
140pub struct Encoder {
146 backend: Backend,
147 config: Config,
148 codec_rate: u32,
151 codec_channels: u32,
153 bitrate: u64,
155 pre_skip: u16,
157 frame_size: usize,
158}
159
160enum Backend {
161 Opus(Opus),
162 Pcm,
163}
164
165struct Opus {
166 inner: *mut OpusEncoder,
167 scratch: Vec<u8>,
168}
169
170unsafe impl Send for Opus {}
174
175impl Encoder {
176 pub fn new(config: &Config) -> Result<Self, Error> {
178 match config.codec {
179 Codec::Opus => Self::new_opus(config.clone()),
180 Codec::Pcm => Self::new_pcm(config.clone()),
181 }
182 }
183
184 fn new_opus(config: Config) -> Result<Self, Error> {
185 let codec_rate = config
186 .sample_rate
187 .unwrap_or_else(|| opus::pick_rate(config.input.sample_rate));
188 opus::validate_rate(codec_rate)?;
189
190 let codec_channels = config.channels.unwrap_or(config.input.channels);
191 if codec_channels != config.input.channels {
192 return Err(Error::Unsupported(format!(
193 "channel remapping not implemented (input {}ch, output {codec_channels}ch)",
194 config.input.channels
195 )));
196 }
197 let channels = opus::validate_channels(codec_channels)?;
198
199 let frame_size = opus::frame_size(codec_rate, config.frame_duration)?;
200
201 let mut err = 0i32;
202 let inner = unsafe { opus_encoder_create(codec_rate as i32, channels, OPUS_APPLICATION_AUDIO, &mut err) };
204 if err != OPUS_OK || inner.is_null() {
205 return Err(opus::error(err, "opus_encoder_create"));
206 }
207
208 let configured = Self::configure_opus(inner, &config, codec_rate, codec_channels);
209 let (bitrate, pre_skip) = match configured {
210 Ok(configured) => configured,
211 Err(err) => {
212 unsafe { opus_encoder_destroy(inner) };
214 return Err(err);
215 }
216 };
217
218 Ok(Self {
219 backend: Backend::Opus(Opus {
220 inner,
221 scratch: vec![0u8; MAX_PACKET_BYTES],
222 }),
223 config,
224 codec_rate,
225 codec_channels,
226 bitrate,
227 pre_skip,
228 frame_size,
229 })
230 }
231
232 fn new_pcm(config: Config) -> Result<Self, Error> {
233 if config.bitrate.is_some() {
234 return Err(Error::Unsupported(
235 "pcm bitrate is fixed; leave Config::bitrate unset".into(),
236 ));
237 }
238
239 let codec_rate = config.sample_rate.unwrap_or(config.input.sample_rate);
240 if codec_rate == 0 {
241 return Err(Error::Unsupported("pcm sample rate must be greater than zero".into()));
242 }
243
244 let codec_channels = config.channels.unwrap_or(config.input.channels);
245 if codec_channels == 0 {
246 return Err(Error::Unsupported("pcm channel count must be greater than zero".into()));
247 }
248 if codec_channels != config.input.channels {
249 return Err(Error::Unsupported(format!(
250 "channel remapping not implemented (input {}ch, output {codec_channels}ch)",
251 config.input.channels
252 )));
253 }
254
255 let frame_size = pcm::frame_size(codec_rate, config.frame_duration)?;
256 pcm::frame_bytes(frame_size, codec_channels)?;
257 let bitrate = pcm::bitrate(codec_rate, codec_channels)?;
258 Ok(Self {
259 backend: Backend::Pcm,
260 config,
261 codec_rate,
262 codec_channels,
263 bitrate,
264 pre_skip: 0,
265 frame_size,
266 })
267 }
268
269 fn configure_opus(
270 inner: *mut OpusEncoder,
271 config: &Config,
272 codec_rate: u32,
273 codec_channels: u32,
274 ) -> Result<(u64, u16), Error> {
275 if let Some(bitrate) = config.bitrate {
276 Self::set_opus_bitrate(inner, codec_channels, bitrate as u64)?;
277 }
278 Self::set_opus_ctl(
279 inner,
280 OPUS_SET_INBAND_FEC_REQUEST,
281 i32::from(config.fec),
282 "OPUS_SET_INBAND_FEC",
283 )?;
284 Self::set_opus_ctl(inner, OPUS_SET_DTX_REQUEST, i32::from(config.dtx), "OPUS_SET_DTX")?;
285
286 let bitrate = Self::get_opus_ctl(inner, OPUS_GET_BITRATE_REQUEST, "OPUS_GET_BITRATE")?;
287 let bitrate = u64::try_from(bitrate)
288 .map_err(|_| Error::Unsupported(format!("Opus reported negative bitrate {bitrate}")))?;
289 let lookahead = Self::get_opus_ctl(inner, OPUS_GET_LOOKAHEAD_REQUEST, "OPUS_GET_LOOKAHEAD")?;
290 let lookahead = u64::try_from(lookahead)
291 .map_err(|_| Error::Unsupported(format!("Opus reported negative lookahead {lookahead}")))?;
292 let pre_skip = u16::try_from((lookahead * 48_000) / codec_rate as u64)
293 .map_err(|_| Error::Unsupported(format!("Opus lookahead {lookahead} does not fit in OpusHead")))?;
294
295 Ok((bitrate, pre_skip))
296 }
297
298 fn set_opus_bitrate(inner: *mut OpusEncoder, channels: u32, bitrate: u64) -> Result<(), Error> {
299 let max = 300_000 * channels as u64;
300 if !(500..=max).contains(&bitrate) {
301 return Err(Error::Unsupported(format!(
302 "Opus bitrate must be between 500 and {max} bits per second for {channels} channel(s), got {bitrate}"
303 )));
304 }
305 Self::set_opus_ctl(inner, OPUS_SET_BITRATE_REQUEST, bitrate as i32, "OPUS_SET_BITRATE")
306 }
307
308 fn set_opus_ctl(inner: *mut OpusEncoder, request: i32, value: i32, name: &'static str) -> Result<(), Error> {
309 let rc = unsafe { opus_encoder_ctl_impl(inner, request, varargs![value]) };
311 if rc != OPUS_OK {
312 return Err(opus::error(rc, name));
313 }
314 Ok(())
315 }
316
317 fn get_opus_ctl(inner: *mut OpusEncoder, request: i32, name: &'static str) -> Result<i32, Error> {
318 let mut value = 0;
319 let rc = unsafe { opus_encoder_ctl_impl(inner, request, varargs![&mut value]) };
322 if rc != OPUS_OK {
323 return Err(opus::error(rc, name));
324 }
325 Ok(value)
326 }
327
328 pub fn config(&self) -> &Config {
330 &self.config
331 }
332
333 pub fn codec(&self) -> Codec {
336 self.config.codec
337 }
338
339 pub fn codec_rate(&self) -> u32 {
342 self.codec_rate
343 }
344
345 pub fn codec_channels(&self) -> u32 {
348 self.codec_channels
349 }
350
351 pub fn frame_size(&self) -> usize {
354 self.frame_size
355 }
356
357 pub fn bitrate(&self) -> u64 {
359 self.bitrate
360 }
361
362 pub fn set_bitrate(&mut self, bitrate: u64) -> Result<(), Error> {
364 let Backend::Opus(opus) = &mut self.backend else {
365 return Err(Error::Unsupported("pcm bitrate is fixed".into()));
366 };
367 if bitrate != self.bitrate {
368 Self::set_opus_bitrate(opus.inner, self.codec_channels, bitrate)?;
369 self.bitrate = bitrate;
370 self.config.bitrate = Some(bitrate as u32);
371 }
372 Ok(())
373 }
374
375 pub fn encode(&mut self, pcm: &[f32]) -> Result<Bytes, Error> {
381 let expected = self.frame_size * self.codec_channels as usize;
382 if pcm.len() != expected {
383 return Err(Error::Misaligned {
384 got: std::mem::size_of_val(pcm),
385 expected: expected * std::mem::size_of::<f32>(),
386 });
387 }
388 match &mut self.backend {
389 Backend::Opus(opus) => {
390 let n = unsafe {
393 opus_encode_float(
394 opus.inner,
395 pcm.as_ptr(),
396 self.frame_size as i32,
397 opus.scratch.as_mut_ptr(),
398 opus.scratch.len() as i32,
399 )
400 };
401 if n < 0 {
402 return Err(crate::opus::error(n, "opus_encode_float"));
403 }
404 Ok(Bytes::copy_from_slice(&opus.scratch[..n as usize]))
405 }
406 Backend::Pcm => {
407 let mut payload = Vec::with_capacity(std::mem::size_of_val(pcm));
408 for sample in pcm {
409 payload.extend_from_slice(&sample.to_le_bytes());
410 }
411 Ok(payload.into())
412 }
413 }
414 }
415
416 pub fn catalog(&self) -> hang::catalog::AudioConfig {
418 match self.config.codec {
419 Codec::Opus => {
420 let head = moq_mux::codec::opus::Config::new(self.codec_rate, self.codec_channels)
423 .with_pre_skip(self.pre_skip)
424 .encode()
425 .expect("opus encoder channels validated to mono/stereo");
426
427 let mut config = hang::catalog::AudioConfig::new(
428 hang::catalog::AudioCodec::Opus,
429 self.codec_rate,
430 self.codec_channels,
431 );
432 config.bitrate = self.config.bitrate.map(u64::from);
433 config.description = Some(head);
434 config.container = hang::catalog::Container::Legacy;
435 config
436 }
437 Codec::Pcm => {
438 let mut config = hang::catalog::AudioConfig::new(
439 hang::catalog::AudioCodec::Pcm,
440 self.codec_rate,
441 self.codec_channels,
442 );
443 config.bitrate = Some(
444 pcm::bitrate(self.codec_rate, self.codec_channels)
445 .expect("pcm encoder bitrate validated at construction"),
446 );
447 config.container = hang::catalog::Container::Legacy;
448 config
449 }
450 }
451 }
452}
453
454impl Drop for Opus {
455 fn drop(&mut self) {
456 unsafe { opus_encoder_destroy(self.inner) };
458 }
459}
460
461#[cfg(test)]
462mod tests {
463 use super::*;
464 use crate::decode::Decoder;
465
466 fn sine(freq: f32, sample_rate: u32, channels: u32, frames: usize) -> Vec<f32> {
467 let mut out = Vec::with_capacity(frames * channels as usize);
468 for i in 0..frames {
469 let t = i as f32 / sample_rate as f32;
470 let v = (2.0 * std::f32::consts::PI * freq * t).sin() * 0.5;
471 for _ in 0..channels {
472 out.push(v);
473 }
474 }
475 out
476 }
477
478 fn stereo_48k() -> Input {
479 Input {
480 format: Format::F32,
481 sample_rate: 48_000,
482 channels: 2,
483 }
484 }
485
486 fn opus_inner(encoder: &Encoder) -> *mut OpusEncoder {
487 let Backend::Opus(opus) = &encoder.backend else {
488 panic!("expected Opus encoder");
489 };
490 opus.inner
491 }
492
493 #[test]
494 fn opus_encode_then_decode_keeps_signal_close() {
495 let mut enc = Encoder::new(&Config {
496 bitrate: Some(96_000),
497 ..Config::new(stereo_48k())
498 })
499 .unwrap();
500
501 let cfg = enc.catalog();
502 let mut dec = Decoder::new(&cfg).unwrap();
503
504 let frame = sine(440.0, 48_000, 2, enc.frame_size());
505 for _ in 0..5 {
506 let pkt = enc.encode(&frame).unwrap();
507 let _ = dec.decode(&pkt).unwrap();
508 }
509
510 let pkt = enc.encode(&frame).unwrap();
511 let decoded = dec.decode(&pkt).unwrap();
512 assert_eq!(decoded.len(), frame.len());
513
514 let energy_in: f32 = frame.iter().map(|s| s * s).sum();
515 let energy_out: f32 = decoded.iter().map(|s| s * s).sum();
516 let ratio = energy_out / energy_in;
517 assert!(
518 (0.5..2.0).contains(&ratio),
519 "output energy ratio {ratio:.3} should be close to 1"
520 );
521 }
522
523 #[test]
524 fn opus_rejects_unsupported_frame_duration() {
525 let err = Encoder::new(&Config {
526 frame_duration: Duration::from_millis(15),
527 ..Config::new(Input::default())
528 });
529 assert!(matches!(err, Err(Error::Unsupported(_))));
530 }
531
532 #[test]
533 fn opus_rejects_misaligned_input() {
534 let mut enc = Encoder::new(&Config::new(Input::default())).unwrap();
535 assert!(matches!(enc.encode(&[0.0f32; 100]), Err(Error::Misaligned { .. })));
536 }
537
538 #[test]
539 fn opus_catalog_includes_opushead() {
540 let enc = Encoder::new(&Config {
541 bitrate: Some(64_000),
542 ..Config::new(stereo_48k())
543 })
544 .unwrap();
545 let cfg = enc.catalog();
546 assert_eq!(cfg.sample_rate, 48_000);
547 assert_eq!(cfg.channel_count, 2);
548 assert_eq!(cfg.bitrate, Some(64_000));
549 let desc = cfg.description.expect("OpusHead should be present");
550 assert_eq!(desc.len(), 19);
551 let head = moq_mux::codec::opus::Config::parse(&mut desc.as_ref()).unwrap();
552 assert_eq!(head.pre_skip, enc.pre_skip);
553 assert_eq!(head.pre_skip, 312);
554 }
555
556 #[test]
557 fn opus_decoder_trims_encoder_lookahead_once() {
558 let mut enc = Encoder::new(&Config::new(stereo_48k())).unwrap();
559 let mut dec = Decoder::new(&enc.catalog()).unwrap();
560 let frame = vec![0.0; enc.frame_size() * enc.codec_channels() as usize];
561
562 let first = dec.decode(&enc.encode(&frame).unwrap()).unwrap();
563 assert_eq!(
564 first.len(),
565 (enc.frame_size() - enc.pre_skip as usize) * enc.codec_channels() as usize
566 );
567
568 let second = dec.decode(&enc.encode(&frame).unwrap()).unwrap();
569 assert_eq!(second.len(), frame.len());
570 }
571
572 #[test]
573 fn opus_runtime_bitrate_updates_encoder_state() {
574 let mut enc = Encoder::new(&Config {
575 bitrate: Some(64_000),
576 ..Config::new(stereo_48k())
577 })
578 .unwrap();
579
580 enc.set_bitrate(32_000).unwrap();
581 assert_eq!(enc.bitrate(), 32_000);
582 assert_eq!(enc.config().bitrate, Some(32_000));
583 assert_eq!(
584 Encoder::get_opus_ctl(
585 opus_inner(&enc),
586 unsafe_libopus::OPUS_GET_BITRATE_REQUEST,
587 "OPUS_GET_BITRATE"
588 )
589 .unwrap(),
590 32_000
591 );
592 }
593
594 #[test]
595 fn opus_runtime_bitrate_rejects_values_libopus_would_clamp() {
596 let mut enc = Encoder::new(&Config::new(stereo_48k())).unwrap();
597 let original = enc.bitrate();
598 assert!(enc.set_bitrate(1).is_err());
599 assert!(enc.set_bitrate(600_001).is_err());
600 assert_eq!(enc.bitrate(), original);
601 }
602
603 #[test]
604 fn opus_applies_fec_and_dtx_controls() {
605 let enc = Encoder::new(&Config {
606 fec: true,
607 dtx: true,
608 ..Config::new(stereo_48k())
609 })
610 .unwrap();
611
612 assert_eq!(
613 Encoder::get_opus_ctl(
614 opus_inner(&enc),
615 unsafe_libopus::OPUS_GET_INBAND_FEC_REQUEST,
616 "OPUS_GET_INBAND_FEC"
617 )
618 .unwrap(),
619 1
620 );
621 assert_eq!(
622 Encoder::get_opus_ctl(opus_inner(&enc), unsafe_libopus::OPUS_GET_DTX_REQUEST, "OPUS_GET_DTX").unwrap(),
623 1
624 );
625 }
626
627 #[test]
628 fn codec_roundtrips_as_str() {
629 assert_eq!(Codec::Opus.as_str(), "opus");
630 assert_eq!(Codec::Opus.to_string(), "opus");
631 assert_eq!("opus".parse::<Codec>().unwrap(), Codec::Opus);
632 assert_eq!(Codec::Pcm.as_str(), "pcm");
633 assert_eq!(Codec::Pcm.to_string(), "pcm");
634 assert_eq!("pcm".parse::<Codec>().unwrap(), Codec::Pcm);
635 assert!("aac".parse::<Codec>().is_err());
636 }
637
638 #[test]
639 fn config_sample_rate_overrides_the_codec_rate() {
640 let enc = Encoder::new(&Config {
641 sample_rate: Some(24_000),
642 ..Config::new(Input {
643 sample_rate: 48_000,
644 channels: 1,
645 ..Input::default()
646 })
647 })
648 .unwrap();
649 assert_eq!(enc.codec_rate(), 24_000);
650 assert_eq!(enc.catalog().sample_rate, 24_000);
651 assert_eq!(enc.pre_skip, 312);
652 }
653
654 #[test]
655 fn pcm_roundtrip_is_lossless() {
656 let mut enc = Encoder::new(&Config {
657 codec: Codec::Pcm,
658 ..Config::new(stereo_48k())
659 })
660 .unwrap();
661 let mut dec = Decoder::new(&enc.catalog()).unwrap();
662 let input = sine(440.0, enc.codec_rate(), enc.codec_channels(), enc.frame_size());
663
664 let packet = enc.encode(&input).unwrap();
665 let output = dec.decode(&packet).unwrap();
666
667 assert_eq!(output, input);
668 }
669
670 #[test]
671 fn pcm_catalog_declares_fixed_bitrate() {
672 let enc = Encoder::new(&Config {
673 codec: Codec::Pcm,
674 ..Config::new(stereo_48k())
675 })
676 .unwrap();
677 let catalog = enc.catalog();
678
679 assert_eq!(catalog.codec, hang::catalog::AudioCodec::Pcm);
680 assert_eq!(catalog.bitrate, Some(48_000 * 2 * 32));
681 assert_eq!(catalog.description, None);
682 }
683
684 #[test]
685 fn pcm_rejects_runtime_bitrate_change() {
686 let mut enc = Encoder::new(&Config {
687 codec: Codec::Pcm,
688 ..Config::new(stereo_48k())
689 })
690 .unwrap();
691 let bitrate = enc.bitrate();
692
693 assert!(matches!(enc.set_bitrate(bitrate), Err(Error::Unsupported(_))));
694 assert_eq!(enc.bitrate(), bitrate);
695 }
696
697 #[test]
698 fn pcm_rejects_fractional_sample_frame_duration() {
699 let err = Encoder::new(&Config {
700 codec: Codec::Pcm,
701 frame_duration: Duration::from_micros(2_500),
702 ..Config::new(Input {
703 sample_rate: 44_100,
704 ..Input::default()
705 })
706 });
707 assert!(matches!(err, Err(Error::Unsupported(_))));
708 }
709
710 #[test]
711 fn pcm_rejects_bitrate_overflow() {
712 let err = Encoder::new(&Config {
713 codec: Codec::Pcm,
714 frame_duration: Duration::from_secs(1),
715 ..Config::new(Input {
716 sample_rate: u32::MAX,
717 channels: u32::MAX,
718 ..Input::default()
719 })
720 });
721 assert!(matches!(err, Err(Error::Unsupported(_))));
722 }
723}