Skip to main content

moq_audio/encode/
encoder.rs

1//! Audio encoder front end.
2//!
3//! [`Encoder`] checks [`Settings`] against the codec, opens a
4//! [`Backend`](super::backend::Backend) for it, and owns what every backend of a
5//! codec shares: framing, the terminal drain, and the catalog entry.
6
7use std::str::FromStr;
8use std::time::Duration;
9
10use bytes::Bytes;
11
12use super::Encoded;
13use super::backend::{self, Backend};
14use crate::opus;
15use crate::pcm;
16use crate::{Error, Format, Layout};
17
18/// Samples per channel in one AAC-LC frame.
19const AAC_FRAME_SIZE: usize = 1024;
20
21/// The audioObjectType of AAC-LC (ISO 14496-3 Table 1.17), `mp4a.40.2`.
22const AAC_LC: u8 = 2;
23
24/// The widest sample rate an AudioSpecificConfig can name: the escape from the
25/// frequency table is a 24-bit field.
26const AAC_MAX_SAMPLE_RATE: u32 = 0xFF_FFFF;
27
28/// Output audio codec. `#[non_exhaustive]` so new codecs can be added without
29/// breaking external `match`es.
30#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
31#[non_exhaustive]
32pub enum Codec {
33	/// Opus (RFC 6716), and the default.
34	#[default]
35	Opus,
36	/// Uncompressed interleaved little-endian IEEE-754 binary32 PCM.
37	Pcm,
38	/// AAC-LC (`mp4a.40.2`), through the platform's encoder. A host without one
39	/// refuses it at construction.
40	Aac,
41}
42
43impl Codec {
44	/// Canonical lowercase identifier, matching the WebCodecs / RFC catalog
45	/// string. Used as the wire/FFI codec name everywhere.
46	pub fn as_str(self) -> &'static str {
47		match self {
48			Self::Opus => "opus",
49			Self::Pcm => "pcm",
50			Self::Aac => "aac",
51		}
52	}
53}
54
55impl std::fmt::Display for Codec {
56	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57		f.write_str(self.as_str())
58	}
59}
60
61impl FromStr for Codec {
62	type Err = Error;
63
64	fn from_str(s: &str) -> Result<Self, Self::Err> {
65		match s {
66			"opus" => Ok(Self::Opus),
67			"pcm" => Ok(Self::Pcm),
68			"aac" => Ok(Self::Aac),
69			other => Err(Error::Unsupported(format!("unknown codec: {other}"))),
70		}
71	}
72}
73
74/// Encoder backend selection.
75#[derive(Clone, Debug, Default, PartialEq, Eq)]
76#[non_exhaustive]
77pub enum Kind {
78	/// Prefer a platform encoder, falling back to software.
79	#[default]
80	Auto,
81	/// Require a software backend.
82	Software,
83	/// Require [`Settings::codec`] to be this codec, by its lowercase name:
84	/// `"opus"`, `"pcm"`, or `"aac"`. Any other codec is refused. The library
85	/// still picks the backend, platform first.
86	Named(String),
87}
88
89/// PCM supplied to [`Producer::write`](super::Producer::write).
90#[derive(Clone, Debug)]
91#[non_exhaustive]
92pub struct Input {
93	/// How samples are packed in each buffer.
94	pub format: Format,
95	/// Samples per second per channel.
96	pub sample_rate: u32,
97	/// Speaker meaning and channel order.
98	pub layout: Layout,
99}
100
101impl Input {
102	/// Describe interleaved `f32` PCM at `sample_rate` in `layout`.
103	pub fn new(sample_rate: u32, layout: Layout) -> Self {
104		Self {
105			format: Format::F32,
106			sample_rate,
107			layout,
108		}
109	}
110}
111
112impl Default for Input {
113	fn default() -> Self {
114		Self::new(48_000, Layout::Stereo)
115	}
116}
117
118/// Audio codec settings shared by [`Encoder`] and [`Producer`](super::Producer).
119#[derive(Clone, Debug)]
120#[non_exhaustive]
121pub struct Settings {
122	/// Output codec. Defaults to [`Codec::Opus`].
123	pub codec: Codec,
124	/// Sample rate accepted by the codec.
125	pub sample_rate: u32,
126	/// Layout accepted by the codec.
127	///
128	/// AAC takes the layouts its channelConfiguration names: mono, stereo, 3.0,
129	/// 4.0, 5.0, 5.1, and 7.1.
130	pub layout: Layout,
131	/// Bitrate in bits per second. `None` lets the codec pick. PCM requires
132	/// `None` because its bitrate is fixed by the sample rate and channel count.
133	///
134	/// Rates too low for Opus to code anything at the chosen
135	/// [`frame_duration`](Self::frame_duration) are rejected. The floor is 1200
136	/// bps at the default 20 ms, rises for shorter frames, and is 2400 bps for
137	/// frames of 10 ms and longer.
138	pub bitrate: Option<moq_net::bandwidth::Rate>,
139	/// Enable Opus discontinuous transmission during silence.
140	pub dtx: bool,
141	/// Encoded frame duration. Opus accepts 2.5 / 5 / 10 / 20 / 40 / 60 ms.
142	/// PCM accepts any duration containing a whole number of samples. AAC frames
143	/// are 1024 samples, so it accepts the duration that rounds to that at
144	/// [`sample_rate`](Self::sample_rate), which [`from_input`](Self::from_input)
145	/// fills in.
146	pub frame_duration: Duration,
147	/// Which encoder implementation to use.
148	pub kind: Kind,
149}
150
151impl Settings {
152	/// Build default Opus settings for `sample_rate` and `layout`.
153	pub fn new(sample_rate: u32, layout: Layout) -> Self {
154		Self {
155			codec: Codec::default(),
156			sample_rate,
157			layout,
158			bitrate: None,
159			dtx: false,
160			frame_duration: Duration::from_millis(20),
161			kind: Kind::Auto,
162		}
163	}
164
165	/// Derive concrete codec settings from source PCM.
166	pub fn from_input(codec: Codec, input: &Input) -> Self {
167		let sample_rate = match codec {
168			Codec::Opus => opus::pick_rate(input.sample_rate),
169			Codec::Pcm | Codec::Aac => input.sample_rate,
170		};
171		let defaults = Self::new(sample_rate, input.layout);
172		let frame_duration = match codec {
173			Codec::Aac => aac_frame_duration(sample_rate),
174			Codec::Opus | Codec::Pcm => defaults.frame_duration,
175		};
176		Self {
177			codec,
178			frame_duration,
179			..defaults
180		}
181	}
182
183	/// Check the settings against the codec, returning its frame size.
184	///
185	/// Codec rules live here rather than in a backend, so every backend of a
186	/// codec refuses the same settings.
187	fn frame_size(&self) -> Result<usize, Error> {
188		self.layout.validate()?;
189		let (rate, channels) = (self.sample_rate, self.layout.channels());
190
191		match self.codec {
192			Codec::Opus => {
193				opus::validate_rate(rate)?;
194				if !matches!(self.layout, Layout::Mono | Layout::Stereo) {
195					return Err(Error::Unsupported("opus requires a named mono or stereo layout".into()));
196				}
197				opus::frame_size(rate, self.frame_duration)
198			}
199			Codec::Pcm => {
200				if self.bitrate.is_some() {
201					return Err(Error::Unsupported(
202						"pcm bitrate is fixed; leave Settings::bitrate unset".into(),
203					));
204				}
205				if self.dtx {
206					return Err(Error::Unsupported(
207						"pcm does not support discontinuous transmission".into(),
208					));
209				}
210				if rate == 0 {
211					return Err(Error::Unsupported("pcm sample rate must be greater than zero".into()));
212				}
213				let frame_size = pcm::frame_size(rate, self.frame_duration)?;
214				pcm::frame_bytes(frame_size, channels)?;
215				pcm::bitrate(rate, channels)?;
216				Ok(frame_size)
217			}
218			Codec::Aac => {
219				if self.dtx {
220					return Err(Error::Unsupported(
221						"aac does not support discontinuous transmission".into(),
222					));
223				}
224				aac_config(self)?;
225				let frames = (self.frame_duration.as_nanos() * u128::from(rate) + 500_000_000) / 1_000_000_000;
226				if frames != AAC_FRAME_SIZE as u128 {
227					return Err(Error::Unsupported(format!(
228						"aac frames are {AAC_FRAME_SIZE} samples, {:?} at {rate} Hz (got {:?})",
229						aac_frame_duration(rate),
230						self.frame_duration
231					)));
232				}
233				Ok(AAC_FRAME_SIZE)
234			}
235		}
236	}
237}
238
239impl Default for Settings {
240	fn default() -> Self {
241		Self::new(48_000, Layout::Stereo)
242	}
243}
244
245/// One AAC frame at `sample_rate`, to the nearest nanosecond.
246fn aac_frame_duration(sample_rate: u32) -> Duration {
247	if sample_rate == 0 {
248		return Duration::ZERO;
249	}
250	let rate = u64::from(sample_rate);
251	Duration::from_nanos((AAC_FRAME_SIZE as u64 * 1_000_000_000 + rate / 2) / rate)
252}
253
254/// The AudioSpecificConfig fields for AAC-LC at the settings' rate and layout.
255///
256/// Only layouts with a channelConfiguration are accepted, since synthesizing
257/// one from a bare count would mislabel the rest: config 3 is 3.0 where the
258/// count's default layout is 2.1, and 6.1 has no config the encoder writes.
259/// 7.1 takes config 7, the one every decoder reads as eight channels.
260fn aac_config(settings: &Settings) -> Result<moq_mux::codec::aac::Config, Error> {
261	let layout = settings.layout;
262	if !matches!(
263		layout,
264		Layout::Mono
265			| Layout::Stereo
266			| Layout::ThreePointZero
267			| Layout::FourPointZero
268			| Layout::FivePointZero
269			| Layout::FivePointOne
270			| Layout::SevenPointOne
271	) {
272		return Err(Error::Unsupported(format!(
273			"aac has no channelConfiguration for {layout:?}; use mono, stereo, 3.0, 4.0, 5.0, 5.1, or 7.1"
274		)));
275	}
276
277	let sample_rate = settings.sample_rate;
278	if !(1..=AAC_MAX_SAMPLE_RATE).contains(&sample_rate) {
279		return Err(Error::Unsupported(format!(
280			"aac sample rate must be between 1 and {AAC_MAX_SAMPLE_RATE} Hz (got {sample_rate})"
281		)));
282	}
283
284	Ok(moq_mux::codec::aac::Config {
285		profile: AAC_LC,
286		sample_rate,
287		channel_count: layout.channels(),
288	})
289}
290
291/// Audio encoder over codec-sized interleaved `f32` PCM.
292///
293/// Build one with [`Encoder::new`], feed full PCM frames via
294/// [`encode`](Self::encode), then pass the trailing partial frame to
295/// [`finish`](Self::finish). Publish every packet either call returns and apply
296/// the terminal [`Finish::discard_padding`] when the container supports it.
297pub struct Encoder {
298	backend: Box<dyn Backend>,
299	settings: Settings,
300	frame_size: usize,
301	/// The catalog description, synthesized from the settings at construction so
302	/// the rendition can be registered before the first packet exists.
303	description: Option<Bytes>,
304	/// Whether input has reached the codec, since a fresh encoder owes no drain.
305	started: bool,
306}
307
308/// Packets emitted by [`Encoder::finish`] and the decoded padding at their end.
309pub struct Finish {
310	packets: Vec<Encoded>,
311	discard_padding: usize,
312}
313
314impl Finish {
315	/// Encoded packets in decode order.
316	pub fn packets(&self) -> &[Encoded] {
317		&self.packets
318	}
319
320	/// Decoded frames per channel to discard from the end of the final packet.
321	pub fn discard_padding(&self) -> usize {
322		self.discard_padding
323	}
324
325	/// Consume the result and return its encoded packets.
326	pub fn into_packets(self) -> Vec<Encoded> {
327		self.packets
328	}
329}
330
331impl Encoder {
332	/// Open an encoder for `settings`, refusing a codec no backend on this host
333	/// encodes.
334	pub fn new(settings: &Settings) -> Result<Self, Error> {
335		let frame_size = settings.frame_size()?;
336		let backend = backend::open(settings)?;
337
338		let description = match settings.codec {
339			Codec::Opus => {
340				// OpusHead carries the lookahead in the 48 kHz timebase.
341				let lookahead = backend.delay() as u64;
342				let pre_skip = u16::try_from((lookahead * 48_000) / u64::from(settings.sample_rate))
343					.map_err(|_| Error::Unsupported(format!("Opus lookahead {lookahead} does not fit in OpusHead")))?;
344				let head = moq_mux::codec::opus::Config::new(settings.sample_rate, settings.layout.channels())
345					.with_pre_skip(pre_skip)
346					.encode()
347					.map_err(moq_mux::Error::from)?;
348				Some(head)
349			}
350			Codec::Aac => Some(aac_config(settings)?.encode()),
351			Codec::Pcm => None,
352		};
353
354		Ok(Self {
355			backend,
356			settings: settings.clone(),
357			frame_size,
358			description,
359			started: false,
360		})
361	}
362
363	/// The encoder backend name in use, e.g. `"libopus"`.
364	pub fn name(&self) -> &str {
365		self.backend.name()
366	}
367
368	/// The encoder settings, including the latest accepted runtime bitrate.
369	pub fn settings(&self) -> &Settings {
370		&self.settings
371	}
372
373	/// The codec this encoder emits. A [`Producer`](super::Producer) must be
374	/// built for the same codec to publish its packets.
375	pub fn codec(&self) -> Codec {
376		self.settings.codec
377	}
378
379	/// Sample rate the codec actually runs at, which is
380	/// [`Settings::sample_rate`].
381	pub fn codec_rate(&self) -> u32 {
382		self.settings.sample_rate
383	}
384
385	/// Channel count the codec actually runs at, which is
386	/// [`Settings::layout`]'s channel count.
387	pub fn codec_channels(&self) -> u32 {
388		self.settings.layout.channels()
389	}
390
391	/// Number of samples per channel the codec consumes per call to
392	/// [`encode`](Self::encode).
393	pub fn frame_size(&self) -> usize {
394		self.frame_size
395	}
396
397	/// Current target bitrate.
398	pub fn bitrate(&self) -> moq_net::bandwidth::Rate {
399		moq_net::bandwidth::Rate::from_bps(self.backend.bitrate())
400	}
401
402	/// Retune the live encoder to `bitrate`.
403	///
404	/// # Errors
405	///
406	/// Returns [`Error::Unsupported`] when the codec's rate is fixed (PCM) or the
407	/// backend can't change it mid-stream. The encoder keeps running at its
408	/// opening rate, so a caller driving a control loop should stop adapting
409	/// rather than stop encoding.
410	pub fn set_bitrate(&mut self, bitrate: moq_net::bandwidth::Rate) -> Result<(), Error> {
411		let previous = self.backend.bitrate();
412		self.backend.set_bitrate(bitrate.as_bps())?;
413		if bitrate.as_bps() != previous {
414			self.settings.bitrate = Some(bitrate);
415		}
416		Ok(())
417	}
418
419	/// Drop all codec history so a later epoch cannot emit audio from this one.
420	pub(super) fn reset(&mut self) {
421		self.backend.reset();
422		self.started = false;
423	}
424
425	/// Whether this epoch has submitted audio to the codec.
426	pub(super) fn started(&self) -> bool {
427		self.started
428	}
429
430	/// Codec priming the catalog can't signal, in codec-rate frames, which the
431	/// producer folds into its timestamps instead.
432	///
433	/// Opus declares its lookahead as OpusHead pre-skip, which the decoder trims,
434	/// so nothing is folded. An AudioSpecificConfig has no such field, so each
435	/// AAC packet is stamped that much earlier and the priming lands before the
436	/// first input sample rather than delaying it.
437	pub(super) fn folded_delay(&self) -> usize {
438		match self.settings.codec {
439			Codec::Aac => self.backend.delay(),
440			Codec::Opus | Codec::Pcm => 0,
441		}
442	}
443
444	/// Encode one frame of interleaved `f32` PCM at [`codec_rate`](Self::codec_rate).
445	///
446	/// `pcm.len()` must equal `frame_size() * codec_channels()`. The
447	/// [`Producer`](super::Producer) handles format conversion and resampling
448	/// before calling this; for direct use, the caller does the same.
449	pub fn encode(&mut self, pcm: &[f32]) -> Result<Encoded, Error> {
450		let expected = self.frame_size * self.codec_channels() as usize;
451		if pcm.len() != expected {
452			return Err(Error::Misaligned {
453				got: std::mem::size_of_val(pcm),
454				expected: expected * std::mem::size_of::<f32>(),
455			});
456		}
457		let encoded = self.backend.encode(pcm)?;
458		self.started = true;
459		Ok(encoded)
460	}
461
462	/// Finish encoding, zero-padding `pcm` as the final partial frame and
463	/// returning every packet needed to drain codec lookahead.
464	///
465	/// `pcm` is interleaved at [`codec_rate`](Self::codec_rate), may be empty,
466	/// and must contain at most one frame. Silence added here only drains
467	/// audio already supplied; [`Finish::discard_padding`] reports how much of
468	/// the decoded tail is artificial and must not count as source duration.
469	/// Consuming the encoder prevents encoding across the artificial terminal
470	/// padding.
471	pub fn finish(mut self, pcm: &[f32]) -> Result<Finish, Error> {
472		self.drain(pcm)
473	}
474
475	/// Same drain as [`finish`](Self::finish), without consuming the encoder.
476	pub(super) fn drain(&mut self, pcm: &[f32]) -> Result<Finish, Error> {
477		let channels = self.codec_channels() as usize;
478		let frame_samples = self.frame_size * channels;
479		if pcm.len() > frame_samples || !pcm.len().is_multiple_of(channels) {
480			return Err(Error::Misaligned {
481				got: std::mem::size_of_val(pcm),
482				expected: if pcm.len() > frame_samples {
483					frame_samples * std::mem::size_of::<f32>()
484				} else {
485					pcm.len().next_multiple_of(channels) * std::mem::size_of::<f32>()
486				},
487			});
488		}
489
490		let source_frames = pcm.len() / channels;
491		let mut packets = Vec::new();
492		let padding = if pcm.is_empty() {
493			0
494		} else {
495			let mut frame = Vec::with_capacity(frame_samples);
496			frame.extend_from_slice(pcm);
497			frame.resize(frame_samples, 0.0);
498			let padding = (frame_samples - pcm.len()) / channels;
499			packets.push(self.encode(&frame)?);
500			padding
501		};
502
503		if !self.started {
504			return Ok(Finish {
505				packets,
506				discard_padding: 0,
507			});
508		}
509
510		let lookahead = self.backend.delay();
511		let drain = lookahead.saturating_sub(padding);
512		let silence = vec![0.0; frame_samples];
513		for _ in 0..drain.div_ceil(self.frame_size) {
514			packets.push(self.encode(&silence)?);
515		}
516
517		let discard_padding = packets
518			.len()
519			.saturating_mul(self.frame_size)
520			.saturating_sub(lookahead)
521			.saturating_sub(source_frames);
522
523		Ok(Finish {
524			packets,
525			discard_padding,
526		})
527	}
528
529	/// hang catalog entry describing this encoder's output stream.
530	pub fn catalog(&self) -> hang::catalog::AudioConfig {
531		let (rate, channels) = (self.codec_rate(), self.codec_channels());
532		let (codec, bitrate): (hang::catalog::AudioCodec, _) = match self.settings.codec {
533			Codec::Opus => (
534				hang::catalog::AudioCodec::Opus,
535				self.settings.bitrate.map(moq_net::bandwidth::Rate::as_bps),
536			),
537			Codec::Pcm => (
538				hang::catalog::AudioCodec::Pcm,
539				Some(pcm::bitrate(rate, channels).expect("pcm encoder bitrate validated at construction")),
540			),
541			Codec::Aac => (
542				hang::catalog::AAC { profile: AAC_LC }.into(),
543				self.settings.bitrate.map(moq_net::bandwidth::Rate::as_bps),
544			),
545		};
546
547		let mut config = hang::catalog::AudioConfig::new(codec, rate, channels);
548		config.bitrate = bitrate;
549		config.description = self.description.clone();
550		config.container = hang::catalog::Container::Legacy;
551		config
552	}
553}
554
555#[cfg(test)]
556mod tests {
557	use super::*;
558	use crate::decode::{Config as DecodeConfig, Decoder};
559
560	fn sine(freq: f32, sample_rate: u32, channels: u32, frames: usize) -> Vec<f32> {
561		let mut out = Vec::with_capacity(frames * channels as usize);
562		for i in 0..frames {
563			let t = i as f32 / sample_rate as f32;
564			let v = (2.0 * std::f32::consts::PI * freq * t).sin() * 0.5;
565			for _ in 0..channels {
566				out.push(v);
567			}
568		}
569		out
570	}
571
572	/// The published `Encoder` was unwind safe before it boxed a backend; keep it so.
573	#[test]
574	fn encoder_is_unwind_safe() {
575		fn assert_unwind_safe<T: std::panic::UnwindSafe + std::panic::RefUnwindSafe>() {}
576		assert_unwind_safe::<Encoder>();
577	}
578
579	#[test]
580	fn opus_encode_then_decode_keeps_signal_close() {
581		let mut enc = Encoder::new(&Settings {
582			bitrate: Some(moq_net::bandwidth::Rate::from_bps(96_000)),
583			..Settings::default()
584		})
585		.unwrap();
586
587		let cfg = enc.catalog();
588		let mut dec = Decoder::new(&cfg, &DecodeConfig::default()).unwrap();
589
590		let frame = sine(440.0, 48_000, 2, enc.frame_size());
591		for _ in 0..5 {
592			let pkt = enc.encode(&frame).unwrap();
593			let _ = dec.decode(&pkt.payload).unwrap();
594		}
595
596		let pkt = enc.encode(&frame).unwrap();
597		let decoded = dec.decode(&pkt.payload).unwrap();
598		assert_eq!(decoded.samples.len(), frame.len());
599
600		let energy_in: f32 = frame.iter().map(|s| s * s).sum();
601		let energy_out: f32 = decoded.samples.iter().map(|s| s * s).sum();
602		let ratio = energy_out / energy_in;
603		assert!(
604			(0.5..2.0).contains(&ratio),
605			"output energy ratio {ratio:.3} should be close to 1"
606		);
607	}
608
609	#[test]
610	fn opus_rejects_unsupported_frame_duration() {
611		let err = Encoder::new(&Settings {
612			frame_duration: Duration::from_millis(15),
613			..Settings::default()
614		});
615		assert!(matches!(err, Err(Error::Unsupported(_))));
616	}
617
618	#[test]
619	fn opus_rejects_misaligned_input() {
620		let mut enc = Encoder::new(&Settings::default()).unwrap();
621		assert!(matches!(enc.encode(&[0.0f32; 100]), Err(Error::Misaligned { .. })));
622	}
623
624	#[test]
625	fn opus_catalog_includes_opushead() {
626		let enc = Encoder::new(&Settings {
627			bitrate: Some(moq_net::bandwidth::Rate::from_bps(64_000)),
628			..Settings::default()
629		})
630		.unwrap();
631		let cfg = enc.catalog();
632		assert_eq!(cfg.sample_rate, 48_000);
633		assert_eq!(cfg.channel_count, 2);
634		assert_eq!(cfg.bitrate, Some(64_000));
635		let desc = cfg.description.expect("OpusHead should be present");
636		assert_eq!(desc.len(), 19);
637		let head = moq_mux::codec::opus::Config::parse(&mut desc.as_ref()).unwrap();
638		assert_eq!(head.pre_skip, 312);
639	}
640
641	#[test]
642	fn opus_decoder_trims_encoder_lookahead_once() {
643		let mut enc = Encoder::new(&Settings::default()).unwrap();
644		let mut dec = Decoder::new(&enc.catalog(), &DecodeConfig::default()).unwrap();
645		let frame = vec![0.0; enc.frame_size() * enc.codec_channels() as usize];
646
647		let first = dec.decode(&enc.encode(&frame).unwrap().payload).unwrap();
648		assert_eq!(
649			first.samples.len(),
650			(enc.frame_size() - enc.backend.delay()) * enc.codec_channels() as usize
651		);
652
653		let second = dec.decode(&enc.encode(&frame).unwrap().payload).unwrap();
654		assert_eq!(second.samples.len(), frame.len());
655	}
656
657	#[test]
658	fn opus_finish_accounts_for_partial_frame_padding() {
659		let enc = Encoder::new(&Settings::new(48_000, Layout::Mono)).unwrap();
660
661		// The 360 frames of terminal padding exceed the 312-frame lookahead,
662		// so the partial packet itself completes the drain.
663		let packets = enc.finish(&vec![0.0; 600]).unwrap();
664		assert_eq!(packets.packets().len(), 1);
665		assert_eq!(packets.discard_padding(), 48);
666	}
667
668	#[test]
669	fn opus_finish_drains_lookahead_across_multiple_short_packets() {
670		let mut enc = Encoder::new(&Settings {
671			frame_duration: Duration::from_micros(2_500),
672			..Settings::new(48_000, Layout::Mono)
673		})
674		.unwrap();
675		let frame = vec![0.0; enc.frame_size()];
676		enc.encode(&frame).unwrap();
677
678		// Three 120-frame packets are required to push out 312 frames.
679		let packets = enc.finish(&[]).unwrap();
680		assert_eq!(packets.packets().len(), 3);
681		assert_eq!(packets.discard_padding(), 48);
682	}
683
684	#[test]
685	fn reset_drops_pending_opus_lookahead() {
686		let mut enc = Encoder::new(&Settings::new(48_000, Layout::Mono)).unwrap();
687		let mut old = vec![0.0; enc.frame_size()];
688		old[enc.frame_size() - 1] = 1.0;
689		enc.encode(&old).unwrap();
690
691		enc.reset();
692		let next = vec![0.0; enc.frame_size()];
693		let actual = enc.encode(&next).unwrap();
694		let mut decoder = Decoder::new(&enc.catalog(), &DecodeConfig::default()).unwrap();
695		let decoded = decoder.decode(&actual.payload).unwrap();
696		let peak = decoded
697			.samples
698			.iter()
699			.fold(0.0f32, |peak, sample| peak.max(sample.abs()));
700		assert!(peak < 0.001, "pre-reset impulse leaked into the next epoch: {peak}");
701
702		enc.reset();
703		let finish = enc.finish(&[]).unwrap();
704		assert!(finish.packets().is_empty());
705		assert_eq!(finish.discard_padding(), 0);
706	}
707
708	#[test]
709	fn opus_runtime_bitrate_updates_encoder_state() {
710		let mut enc = Encoder::new(&Settings {
711			bitrate: Some(moq_net::bandwidth::Rate::from_bps(64_000)),
712			..Settings::default()
713		})
714		.unwrap();
715
716		enc.set_bitrate(moq_net::bandwidth::Rate::from_bps(32_000)).unwrap();
717		assert_eq!(enc.bitrate(), moq_net::bandwidth::Rate::from_bps(32_000));
718		assert_eq!(enc.settings().bitrate, Some(moq_net::bandwidth::Rate::from_bps(32_000)));
719	}
720
721	#[test]
722	fn opus_runtime_bitrate_rejects_values_libopus_would_clamp() {
723		let mut enc = Encoder::new(&Settings::default()).unwrap();
724		let original = enc.bitrate();
725		assert!(enc.set_bitrate(moq_net::bandwidth::Rate::from_bps(1)).is_err());
726		assert!(enc.set_bitrate(moq_net::bandwidth::Rate::from_bps(600_001)).is_err());
727		assert_eq!(enc.bitrate(), original);
728	}
729
730	#[test]
731	fn codec_roundtrips_as_str() {
732		assert_eq!(Codec::Opus.as_str(), "opus");
733		assert_eq!(Codec::Opus.to_string(), "opus");
734		assert_eq!("opus".parse::<Codec>().unwrap(), Codec::Opus);
735		assert_eq!(Codec::Pcm.as_str(), "pcm");
736		assert_eq!(Codec::Pcm.to_string(), "pcm");
737		assert_eq!("pcm".parse::<Codec>().unwrap(), Codec::Pcm);
738		assert_eq!(Codec::Aac.as_str(), "aac");
739		assert_eq!(Codec::Aac.to_string(), "aac");
740		assert_eq!("aac".parse::<Codec>().unwrap(), Codec::Aac);
741		assert!("mp3".parse::<Codec>().is_err());
742	}
743
744	#[test]
745	fn settings_fix_the_codec_rate() {
746		let enc = Encoder::new(&Settings::new(24_000, Layout::Mono)).unwrap();
747		assert_eq!(enc.codec_rate(), 24_000);
748		let catalog = enc.catalog();
749		assert_eq!(catalog.sample_rate, 24_000);
750		let head = moq_mux::codec::opus::Config::parse(&mut catalog.description.unwrap().as_ref()).unwrap();
751		assert_eq!(head.pre_skip, 312);
752	}
753
754	#[test]
755	fn pcm_roundtrip_is_lossless() {
756		let mut enc = Encoder::new(&Settings {
757			codec: Codec::Pcm,
758			..Settings::default()
759		})
760		.unwrap();
761		let mut dec = Decoder::new(&enc.catalog(), &DecodeConfig::default()).unwrap();
762		let input = sine(440.0, enc.codec_rate(), enc.codec_channels(), enc.frame_size());
763
764		let packet = enc.encode(&input).unwrap();
765		let output = dec.decode(&packet.payload).unwrap();
766
767		assert_eq!(output.samples, input);
768	}
769
770	/// A PCM catalog names only a count, and 3 or 4 channels decode as 2.1 or
771	/// quad, so 3.0 and 4.0 are refused; discrete channels still pass through.
772	#[test]
773	fn pcm_refuses_layouts_its_count_cannot_name() {
774		for layout in [Layout::ThreePointZero, Layout::FourPointZero] {
775			let err = Encoder::new(&Settings {
776				codec: Codec::Pcm,
777				..Settings::new(48_000, layout)
778			});
779			assert!(matches!(err, Err(Error::Unsupported(_))), "{layout:?}");
780		}
781
782		for layout in [Layout::TwoPointOne, Layout::Quad, Layout::Discrete(3)] {
783			let mut enc = Encoder::new(&Settings {
784				codec: Codec::Pcm,
785				..Settings::new(48_000, layout)
786			})
787			.unwrap();
788			let mut dec = Decoder::new(&enc.catalog(), &DecodeConfig::default()).unwrap();
789			let input = sine(440.0, enc.codec_rate(), enc.codec_channels(), enc.frame_size());
790			let output = dec.decode(&enc.encode(&input).unwrap().payload).unwrap();
791			assert_eq!(output.samples, input, "{layout:?}");
792		}
793	}
794
795	#[test]
796	fn pcm_catalog_declares_fixed_bitrate() {
797		let enc = Encoder::new(&Settings {
798			codec: Codec::Pcm,
799			..Settings::default()
800		})
801		.unwrap();
802		let catalog = enc.catalog();
803
804		assert_eq!(catalog.codec, hang::catalog::AudioCodec::Pcm);
805		assert_eq!(catalog.bitrate, Some(48_000 * 2 * 32));
806		assert_eq!(catalog.description, None);
807	}
808
809	#[test]
810	fn pcm_rejects_runtime_bitrate_change() {
811		let mut enc = Encoder::new(&Settings {
812			codec: Codec::Pcm,
813			..Settings::default()
814		})
815		.unwrap();
816		let bitrate = enc.bitrate();
817
818		assert!(matches!(enc.set_bitrate(bitrate), Err(Error::Unsupported(_))));
819		assert_eq!(enc.bitrate(), bitrate);
820	}
821
822	#[test]
823	fn pcm_rejects_fractional_sample_frame_duration() {
824		let err = Encoder::new(&Settings {
825			codec: Codec::Pcm,
826			frame_duration: Duration::from_micros(2_500),
827			..Settings::new(44_100, Layout::Stereo)
828		});
829		assert!(matches!(err, Err(Error::Unsupported(_))));
830	}
831
832	#[test]
833	fn pcm_rejects_bitrate_overflow() {
834		let err = Encoder::new(&Settings {
835			codec: Codec::Pcm,
836			frame_duration: Duration::from_secs(1),
837			..Settings::new(u32::MAX, Layout::Discrete(u32::MAX))
838		});
839		assert!(matches!(err, Err(Error::Unsupported(_))));
840	}
841
842	#[test]
843	fn pcm_rejects_opus_only_settings() {
844		let settings = Settings {
845			codec: Codec::Pcm,
846			dtx: true,
847			..Settings::default()
848		};
849		assert!(matches!(Encoder::new(&settings), Err(Error::Unsupported(_))));
850	}
851
852	/// The catalog carries only a count, so a discrete layout comes back as the
853	/// count's default one, with the samples untouched.
854	#[test]
855	fn pcm_passes_discrete_multichannel_samples_through() {
856		let settings = Settings {
857			codec: Codec::Pcm,
858			..Settings::new(48_000, Layout::Discrete(3))
859		};
860		let mut encoder = Encoder::new(&settings).unwrap();
861		let catalog = encoder.catalog();
862		let mut decoder = Decoder::new(&catalog, &DecodeConfig::default()).unwrap();
863		let input = [0.1, 0.2, 0.3].repeat(encoder.frame_size());
864		let output = decoder.decode(&encoder.encode(&input).unwrap().payload).unwrap();
865
866		assert_eq!(decoder.layout(), Layout::TwoPointOne);
867		assert_eq!(output.samples, input);
868	}
869
870	#[test]
871	fn opus_refuses_discrete_layout() {
872		let settings = Settings::new(48_000, Layout::Discrete(2));
873		assert!(matches!(Encoder::new(&settings), Err(Error::Unsupported(_))));
874	}
875
876	fn aac(layout: Layout) -> Settings {
877		Settings::from_input(Codec::Aac, &Input::new(48_000, layout))
878	}
879
880	/// Open `settings` on the test stub, since this host has no AAC encoder.
881	fn stub(settings: &Settings) -> Result<Encoder, Error> {
882		let _stub = backend::stub::install();
883		Encoder::new(settings)
884	}
885
886	/// Without the stub, the public constructor sees the real tiers and refuses.
887	#[test]
888	fn aac_is_refused_without_an_encoder() {
889		let err = Encoder::new(&aac(Layout::Stereo))
890			.err()
891			.expect("no AAC encoder on this host");
892		assert!(matches!(err, Error::Unsupported(_)), "{err}");
893	}
894
895	/// The ASC is synthesized from the settings, so it exists before any packet.
896	#[test]
897	fn aac_catalog_carries_the_synthesized_asc() {
898		let enc = stub(&aac(Layout::Stereo)).unwrap();
899		assert_eq!(enc.name(), backend::stub::NAME);
900		assert_eq!(enc.frame_size(), 1024);
901
902		let catalog = enc.catalog();
903		assert_eq!(catalog.codec, hang::catalog::AAC { profile: 2 }.into());
904		assert_eq!(catalog.codec.to_string(), "mp4a.40.2");
905		assert_eq!(catalog.sample_rate, 48_000);
906		assert_eq!(catalog.channel_count, 2);
907		assert_eq!(catalog.container, hang::catalog::Container::Legacy);
908		// AAC-LC (2), 48 kHz (index 3), stereo (config 2).
909		assert_eq!(catalog.description.as_deref(), Some(&[0x11, 0x90][..]));
910	}
911
912	#[test]
913	fn aac_takes_the_layouts_with_a_channel_configuration() {
914		for (layout, config) in [
915			(Layout::Mono, 1),
916			(Layout::Stereo, 2),
917			(Layout::ThreePointZero, 3),
918			(Layout::FourPointZero, 4),
919			(Layout::FivePointZero, 5),
920			(Layout::FivePointOne, 6),
921			(Layout::SevenPointOne, 7),
922		] {
923			let catalog = stub(&aac(layout)).unwrap().catalog();
924			let description = catalog.description.unwrap();
925			assert_eq!(description[1] >> 3 & 0xF, config, "{layout:?}");
926			assert_eq!(catalog.channel_count, layout.channels(), "{layout:?}");
927		}
928
929		for layout in [
930			Layout::TwoPointOne,
931			Layout::Quad,
932			Layout::SixPointOne,
933			Layout::Discrete(2),
934		] {
935			assert!(matches!(stub(&aac(layout)), Err(Error::Unsupported(_))), "{layout:?}");
936		}
937	}
938
939	/// AAC frames are 1024 samples however the duration is spelled.
940	#[test]
941	fn aac_frame_duration_is_the_codecs() {
942		assert_eq!(aac(Layout::Stereo).frame_duration, Duration::from_nanos(21_333_333));
943		let settings = Settings {
944			frame_duration: Duration::from_micros(21_333),
945			..aac(Layout::Stereo)
946		};
947		assert_eq!(stub(&settings).unwrap().frame_size(), 1024);
948
949		let settings = Settings {
950			frame_duration: Duration::from_millis(20),
951			..aac(Layout::Stereo)
952		};
953		let err = stub(&settings).err().expect("20 ms is 960 samples");
954		assert!(err.to_string().contains("1024"), "{err}");
955	}
956
957	#[test]
958	fn aac_refuses_dtx() {
959		let settings = Settings {
960			dtx: true,
961			..aac(Layout::Stereo)
962		};
963		assert!(matches!(stub(&settings), Err(Error::Unsupported(_))));
964	}
965
966	/// A backend that can't retune keeps its opening rate.
967	#[test]
968	fn fixed_rate_backend_keeps_its_opening_rate() {
969		let mut enc = stub(&Settings {
970			bitrate: Some(moq_net::bandwidth::Rate::from_bps(96_000)),
971			..aac(Layout::Stereo)
972		})
973		.unwrap();
974
975		let err = enc.set_bitrate(moq_net::bandwidth::Rate::from_bps(64_000));
976		assert!(matches!(err, Err(Error::Unsupported(_))));
977		assert_eq!(enc.bitrate(), moq_net::bandwidth::Rate::from_bps(96_000));
978		assert_eq!(enc.settings().bitrate, Some(moq_net::bandwidth::Rate::from_bps(96_000)));
979		assert_eq!(enc.catalog().bitrate, Some(96_000));
980	}
981
982	/// The drain pushes the encoder delay out through whole silent frames.
983	#[test]
984	fn aac_finish_drains_the_encoder_delay() {
985		let mut enc = stub(&aac(Layout::Mono)).unwrap();
986		assert_eq!(enc.folded_delay(), backend::stub::DELAY);
987		enc.encode(&[0.0; 1024]).unwrap();
988
989		// 2112 frames of delay take three 1024-frame packets.
990		let finish = enc.finish(&[]).unwrap();
991		assert_eq!(finish.packets().len(), 3);
992		assert_eq!(finish.discard_padding(), 3 * 1024 - backend::stub::DELAY);
993	}
994
995	/// Opus signals its lookahead as pre-skip, so the producer folds none of it.
996	#[test]
997	fn opus_folds_no_delay() {
998		assert_eq!(Encoder::new(&Settings::default()).unwrap().folded_delay(), 0);
999	}
1000}