Skip to main content

moq_audio/encode/
encoder.rs

1//! Audio encoder front end.
2//!
3//! [`Encoder`] dispatches over the closed [`Codec`] set. Opus wraps libopus
4//! 1.3.1 via [`unsafe_libopus`], while PCM serializes interleaved `f32` samples
5//! directly.
6
7use 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
22/// libopus packet size ceiling per RFC 6716 ยง3.4.
23const MAX_PACKET_BYTES: usize = 4_000;
24
25/// Output audio codec. `#[non_exhaustive]` so new codecs can be added without
26/// breaking external `match`es.
27#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
28#[non_exhaustive]
29pub enum Codec {
30	/// Opus (RFC 6716), and the default.
31	#[default]
32	Opus,
33	/// Uncompressed interleaved little-endian IEEE-754 binary32 PCM.
34	Pcm,
35}
36
37impl Codec {
38	/// Canonical lowercase identifier, matching the WebCodecs / RFC catalog
39	/// string. Used as the wire/FFI codec name everywhere.
40	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/// The PCM layout of the buffers handed to [`Encoder::encode`] /
67/// [`Producer::write`](super::Producer::write).
68///
69/// The encoder's counterpart to a video encoder's width / height: it describes
70/// the input, not the output. `publish_capture` fills it in from the capture
71/// source, so only a bring-your-own-PCM caller builds one.
72#[derive(Clone, Debug)]
73pub struct Input {
74	/// How samples are packed in each buffer.
75	pub format: Format,
76	/// Samples per second per channel. Resampled to the codec rate if they differ.
77	pub sample_rate: u32,
78	/// Channels per frame.
79	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/// Encoder configuration: the input PCM layout plus the codec knobs.
93///
94/// The bring-your-own-PCM counterpart to [`Options`](super::Options), which
95/// `publish_capture` uses when the layout comes from the capture source instead
96/// of the caller.
97///
98/// `#[non_exhaustive]`: build via [`Config::new`] and set the optional fields,
99/// so future knobs don't break callers.
100#[derive(Clone, Debug)]
101#[non_exhaustive]
102pub struct Config {
103	/// The PCM layout fed to the encoder.
104	pub input: Input,
105	/// Output codec. Defaults to [`Codec::Opus`].
106	pub codec: Codec,
107	/// Sample rate the codec runs at. `None` snaps [`Input::sample_rate`] up to
108	/// the nearest rate the codec supports, resampling if that moved it.
109	pub sample_rate: Option<u32>,
110	/// Channel count the codec runs at. `None` matches [`Input::channels`];
111	/// anything else is rejected, since remapping isn't implemented.
112	pub channels: Option<u32>,
113	/// Bitrate in bits per second. `None` lets Opus pick. PCM requires `None`
114	/// because its bitrate is fixed by the sample rate and channel count.
115	///
116	/// Rates too low for Opus to code anything at the chosen
117	/// [`frame_duration`](Self::frame_duration) are rejected: below its floor
118	/// libopus emits empty frames whatever the input, which carry no audio and
119	/// are indistinguishable from silence. The floor is 1200 bps at the default
120	/// 20 ms, rises for shorter frames (9600 bps at 2.5 ms), and is 2400 bps for
121	/// frames of 10 ms and longer.
122	pub bitrate: Option<u32>,
123	/// Enable Opus in-band forward error correction.
124	pub fec: bool,
125	/// Enable Opus discontinuous transmission during silence.
126	pub dtx: bool,
127	/// Encoded frame duration. Opus accepts 2.5 / 5 / 10 / 20 / 40 / 60 ms.
128	/// PCM accepts any duration containing a whole number of samples.
129	pub frame_duration: Duration,
130}
131
132impl Config {
133	/// A config encoding `input` with the default codec settings.
134	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
148/// Audio encoder over the PCM layout declared in [`Config::input`].
149///
150/// Build one with [`Encoder::new`], feed full PCM frames via
151/// [`encode`](Self::encode), then pass the trailing partial frame to
152/// [`finish`](Self::finish). Publish every packet either call returns and apply
153/// the terminal [`Finish::discard_padding`] when the container supports it.
154pub struct Encoder {
155	backend: Backend,
156	config: Config,
157	/// Resolved codec sample rate (from `config.sample_rate`, else the input rate
158	/// snapped up to a supported one).
159	codec_rate: u32,
160	/// Resolved codec channel count (currently always the input's).
161	codec_channels: u32,
162	/// Current libopus target bitrate.
163	bitrate: u64,
164	/// Encoder lookahead expressed in the OpusHead 48 kHz timebase.
165	pre_skip: u16,
166	/// Encoder lookahead in codec-rate frames.
167	lookahead: usize,
168	frame_size: usize,
169	/// Whether input has reached the codec, since a fresh encoder owes no drain.
170	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
183// SAFETY: OpusEncoder is heap-allocated state owned exclusively by this
184// struct; libopus encoder methods take a single &mut, so a unique owner is
185// allowed to move it across threads.
186unsafe impl Send for Opus {}
187
188/// Packets emitted by [`Encoder::finish`] and the decoded padding at their end.
189pub struct Finish {
190	packets: Vec<Encoded>,
191	discard_padding: usize,
192}
193
194impl Finish {
195	/// Encoded packets in decode order.
196	pub fn packets(&self) -> &[Encoded] {
197		&self.packets
198	}
199
200	/// Decoded frames per channel to discard from the end of the final packet.
201	pub fn discard_padding(&self) -> usize {
202		self.discard_padding
203	}
204
205	/// Consume the result and return its encoded packets.
206	pub fn into_packets(self) -> Vec<Encoded> {
207		self.packets
208	}
209}
210
211impl Encoder {
212	/// Open an encoder for `config`.
213	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		// SAFETY: out-pointer `err` is valid; inner is checked for null below.
239		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				// SAFETY: `inner` was created above and not yet handed out.
249				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		// Below the floor libopus codes nothing at all, so the stream carries no
350		// audio and every packet is framed like discontinuous transmission.
351		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		// SAFETY: `inner` owns a live encoder and each request here expects one i32.
362		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		// SAFETY: `inner` owns a live encoder and each request here expects one
372		// valid mutable i32 output.
373		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	/// The encoder config, including the latest accepted runtime bitrate.
381	pub fn config(&self) -> &Config {
382		&self.config
383	}
384
385	/// The codec this encoder emits. A [`Producer`](super::Producer) must be
386	/// built for the same codec to publish its packets.
387	pub fn codec(&self) -> Codec {
388		self.config.codec
389	}
390
391	/// Sample rate the codec actually runs at, which is
392	/// [`Config::sample_rate`] resolved.
393	pub fn codec_rate(&self) -> u32 {
394		self.codec_rate
395	}
396
397	/// Channel count the codec actually runs at, which is
398	/// [`Config::channels`] resolved.
399	pub fn codec_channels(&self) -> u32 {
400		self.codec_channels
401	}
402
403	/// Number of samples per channel the codec consumes per call to
404	/// [`encode`](Self::encode).
405	pub fn frame_size(&self) -> usize {
406		self.frame_size
407	}
408
409	/// Current target bitrate in bits per second.
410	pub fn bitrate(&self) -> u64 {
411		self.bitrate
412	}
413
414	/// Retune the live Opus encoder to `bitrate` bits per second.
415	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	/// Drop all codec history so a later epoch cannot emit audio from this one.
434	pub(super) fn reset(&mut self) {
435		if let Backend::Opus(opus) = &mut self.backend {
436			// SAFETY: `inner` owns a live encoder and OPUS_RESET_STATE takes no arguments.
437			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	/// Whether this epoch has submitted audio to the codec.
444	pub(super) fn started(&self) -> bool {
445		self.started
446	}
447
448	/// Encode one frame of interleaved `f32` PCM at [`codec_rate`](Self::codec_rate).
449	///
450	/// `pcm.len()` must equal `frame_size() * codec_channels()`. The
451	/// [`Producer`](super::Producer) handles format conversion and resampling
452	/// before calling this; for direct use, the caller does the same.
453	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				// SAFETY: `inner` owns a live OpusEncoder; pcm and scratch slices
464				// are bounded by the lengths we pass.
465				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				// The same rule the decoder applies, so both sides agree by reading the
479				// packet rather than by two mechanisms kept in step. `Config::bitrate`
480				// refuses the rates where a bare TOC would mean something else, which
481				// is what makes reading the packet enough here.
482				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	/// Finish encoding, zero-padding `pcm` as the final partial frame and
498	/// returning every packet needed to drain codec lookahead.
499	///
500	/// `pcm` is interleaved at [`codec_rate`](Self::codec_rate), may be empty,
501	/// and must contain at most one frame. Silence added here only drains
502	/// audio already supplied; [`Finish::discard_padding`] reports how much of
503	/// the decoded tail is artificial and must not count as source duration.
504	/// Consuming the encoder prevents encoding across the artificial terminal
505	/// padding.
506	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	/// hang catalog entry describing this encoder's output stream.
559	pub fn catalog(&self) -> hang::catalog::AudioConfig {
560		match self.config.codec {
561			Codec::Opus => {
562				// `codec_channels` is validated to mono/stereo at encoder construction,
563				// so the OpusHead (channel mapping family 0) always encodes.
564				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		// SAFETY: `inner` is a live OpusEncoder that nothing else aliases.
599		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		// Padding does not change what a packet codes, so it does not change how it
703		// reads either.
704		let mut padded = dtx.payload.to_vec();
705		let original_len = padded.len();
706		padded.resize(original_len + 8, 0);
707		// SAFETY: the allocation is sized for the requested padded length and the
708		// encoder produced a valid Opus packet.
709		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		// An absent payload asks libopus for packet-loss concealment, which says
715		// nothing: the classification stays where the last real packet left it.
716		assert_eq!(dec.decode(&[]).unwrap().activity, Activity::Dtx);
717
718		// A rejected packet must not mutate the state used to classify later loss.
719		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		// The floor moves with the frame duration: 2.5 ms needs 9600 bps.
752		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		// And a live retune cannot get under it either, which is how a stream that
763		// started healthy could otherwise end up coding nothing.
764		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	/// A silence run is interrupted by an ordinarily coded frame of that silence,
778	/// which carries no marker saying so. It reads active, because the framing
779	/// that would catch it is the same framing a speech onset produces, and
780	/// calling real audio silence is the worse error.
781	#[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		// The 360 frames of terminal padding exceed the 312-frame lookahead,
883		// so the partial packet itself completes the drain.
884		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		// Three 120-frame packets are required to push out 312 frames.
903		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}