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, 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
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/// PCM supplied to [`Producer::write`](super::Producer::write).
67#[derive(Clone, Debug)]
68#[non_exhaustive]
69pub struct Input {
70	/// How samples are packed in each buffer.
71	pub format: Format,
72	/// Samples per second per channel.
73	pub sample_rate: u32,
74	/// Speaker meaning and channel order.
75	pub layout: Layout,
76}
77
78impl Input {
79	/// Describe interleaved `f32` PCM at `sample_rate` in `layout`.
80	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/// Audio codec settings shared by [`Encoder`] and [`Producer`](super::Producer).
96#[derive(Clone, Debug)]
97#[non_exhaustive]
98pub struct Settings {
99	/// Output codec. Defaults to [`Codec::Opus`].
100	pub codec: Codec,
101	/// Sample rate accepted by the codec.
102	pub sample_rate: u32,
103	/// Layout accepted by the codec.
104	pub layout: Layout,
105	/// Bitrate in bits per second. `None` lets Opus pick. PCM requires `None`
106	/// because its bitrate is fixed by the sample rate and channel count.
107	///
108	/// Rates too low for Opus to code anything at the chosen
109	/// [`frame_duration`](Self::frame_duration) are rejected. The floor is 1200
110	/// bps at the default 20 ms, rises for shorter frames, and is 2400 bps for
111	/// frames of 10 ms and longer.
112	pub bitrate: Option<moq_net::bandwidth::Rate>,
113	/// Enable Opus discontinuous transmission during silence.
114	pub dtx: bool,
115	/// Encoded frame duration. Opus accepts 2.5 / 5 / 10 / 20 / 40 / 60 ms.
116	/// PCM accepts any duration containing a whole number of samples.
117	pub frame_duration: Duration,
118}
119
120impl Settings {
121	/// Build default Opus settings for `sample_rate` and `layout`.
122	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	/// Derive concrete codec settings from source PCM.
134	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
154/// Audio encoder over codec-sized interleaved `f32` PCM.
155///
156/// Build one with [`Encoder::new`], feed full PCM frames via
157/// [`encode`](Self::encode), then pass the trailing partial frame to
158/// [`finish`](Self::finish). Publish every packet either call returns and apply
159/// the terminal [`Finish::discard_padding`] when the container supports it.
160pub struct Encoder {
161	backend: Backend,
162	settings: Settings,
163	/// Codec sample rate.
164	codec_rate: u32,
165	/// Codec channel count.
166	codec_channels: u32,
167	/// Current libopus target bitrate.
168	bitrate: u64,
169	/// Encoder lookahead expressed in the OpusHead 48 kHz timebase.
170	pre_skip: u16,
171	/// Encoder lookahead in codec-rate frames.
172	lookahead: usize,
173	frame_size: usize,
174	/// Whether input has reached the codec, since a fresh encoder owes no drain.
175	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
188// SAFETY: OpusEncoder is heap-allocated state owned exclusively by this
189// struct; libopus encoder methods take a single &mut, so a unique owner is
190// allowed to move it across threads.
191unsafe impl Send for Opus {}
192
193/// Packets emitted by [`Encoder::finish`] and the decoded padding at their end.
194pub struct Finish {
195	packets: Vec<Encoded>,
196	discard_padding: usize,
197}
198
199impl Finish {
200	/// Encoded packets in decode order.
201	pub fn packets(&self) -> &[Encoded] {
202		&self.packets
203	}
204
205	/// Decoded frames per channel to discard from the end of the final packet.
206	pub fn discard_padding(&self) -> usize {
207		self.discard_padding
208	}
209
210	/// Consume the result and return its encoded packets.
211	pub fn into_packets(self) -> Vec<Encoded> {
212		self.packets
213	}
214}
215
216impl Encoder {
217	/// Open an encoder for `settings`.
218	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		// SAFETY: out-pointer `err` is valid; inner is checked for null below.
240		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				// SAFETY: `inner` was created above and not yet handed out.
250				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		// SAFETY: `inner` owns a live encoder and each request here expects one i32.
353		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		// SAFETY: `inner` owns a live encoder and each request here expects one
363		// valid mutable i32 output.
364		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	/// The encoder settings, including the latest accepted runtime bitrate.
372	pub fn settings(&self) -> &Settings {
373		&self.settings
374	}
375
376	/// The codec this encoder emits. A [`Producer`](super::Producer) must be
377	/// built for the same codec to publish its packets.
378	pub fn codec(&self) -> Codec {
379		self.settings.codec
380	}
381
382	/// Sample rate the codec actually runs at, which is
383	/// [`Settings::sample_rate`].
384	pub fn codec_rate(&self) -> u32 {
385		self.codec_rate
386	}
387
388	/// Channel count the codec actually runs at, which is
389	/// [`Settings::layout`]'s channel count.
390	pub fn codec_channels(&self) -> u32 {
391		self.codec_channels
392	}
393
394	/// Number of samples per channel the codec consumes per call to
395	/// [`encode`](Self::encode).
396	pub fn frame_size(&self) -> usize {
397		self.frame_size
398	}
399
400	/// Current target bitrate.
401	pub fn bitrate(&self) -> moq_net::bandwidth::Rate {
402		moq_net::bandwidth::Rate::from_bps(self.bitrate)
403	}
404
405	/// Retune the live Opus encoder to `bitrate`.
406	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	/// Drop all codec history so a later epoch cannot emit audio from this one.
425	pub(super) fn reset(&mut self) {
426		if let Backend::Opus(opus) = &mut self.backend {
427			// SAFETY: `inner` owns a live encoder and OPUS_RESET_STATE takes no arguments.
428			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	/// Whether this epoch has submitted audio to the codec.
435	pub(super) fn started(&self) -> bool {
436		self.started
437	}
438
439	/// Encode one frame of interleaved `f32` PCM at [`codec_rate`](Self::codec_rate).
440	///
441	/// `pcm.len()` must equal `frame_size() * codec_channels()`. The
442	/// [`Producer`](super::Producer) handles format conversion and resampling
443	/// before calling this; for direct use, the caller does the same.
444	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				// SAFETY: `inner` owns a live OpusEncoder; pcm and scratch slices
455				// are bounded by the lengths we pass.
456				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	/// Finish encoding, zero-padding `pcm` as the final partial frame and
485	/// returning every packet needed to drain codec lookahead.
486	///
487	/// `pcm` is interleaved at [`codec_rate`](Self::codec_rate), may be empty,
488	/// and must contain at most one frame. Silence added here only drains
489	/// audio already supplied; [`Finish::discard_padding`] reports how much of
490	/// the decoded tail is artificial and must not count as source duration.
491	/// Consuming the encoder prevents encoding across the artificial terminal
492	/// padding.
493	pub fn finish(mut self, pcm: &[f32]) -> Result<Finish, Error> {
494		self.drain(pcm)
495	}
496
497	/// Same drain as [`finish`](Self::finish), without consuming the encoder.
498	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	/// hang catalog entry describing this encoder's output stream.
551	pub fn catalog(&self) -> hang::catalog::AudioConfig {
552		match self.settings.codec {
553			Codec::Opus => {
554				// `codec_channels` is validated to mono/stereo at encoder construction,
555				// so the OpusHead (channel mapping family 0) always encodes.
556				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		// SAFETY: `inner` is a live OpusEncoder that nothing else aliases.
591		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		// The 360 frames of terminal padding exceed the 312-frame lookahead,
703		// so the partial packet itself completes the drain.
704		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		// Three 120-frame packets are required to push out 312 frames.
720		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}