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_SET_BITRATE_REQUEST,
13	OPUS_SET_DTX_REQUEST, OPUS_SET_INBAND_FEC_REQUEST, OpusEncoder, opus_encode_float, opus_encoder_create,
14	opus_encoder_ctl_impl, opus_encoder_destroy, varargs,
15};
16
17use crate::opus;
18use crate::pcm;
19use crate::{Error, Format};
20
21/// libopus packet size ceiling per RFC 6716 ยง3.4.
22const MAX_PACKET_BYTES: usize = 4_000;
23
24/// Output audio codec. `#[non_exhaustive]` so new codecs can be added without
25/// breaking external `match`es.
26#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
27#[non_exhaustive]
28pub enum Codec {
29	/// Opus (RFC 6716), and the default.
30	#[default]
31	Opus,
32	/// Uncompressed interleaved little-endian IEEE-754 binary32 PCM.
33	Pcm,
34}
35
36impl Codec {
37	/// Canonical lowercase identifier, matching the WebCodecs / RFC catalog
38	/// string. Used as the wire/FFI codec name everywhere.
39	pub fn as_str(self) -> &'static str {
40		match self {
41			Self::Opus => "opus",
42			Self::Pcm => "pcm",
43		}
44	}
45}
46
47impl std::fmt::Display for Codec {
48	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49		f.write_str(self.as_str())
50	}
51}
52
53impl FromStr for Codec {
54	type Err = Error;
55
56	fn from_str(s: &str) -> Result<Self, Self::Err> {
57		match s {
58			"opus" => Ok(Self::Opus),
59			"pcm" => Ok(Self::Pcm),
60			other => Err(Error::Unsupported(format!("unknown codec: {other}"))),
61		}
62	}
63}
64
65/// The PCM layout of the buffers handed to [`Encoder::encode`] /
66/// [`Producer::write`](super::Producer::write).
67///
68/// The encoder's counterpart to a video encoder's width / height: it describes
69/// the input, not the output. `publish_capture` fills it in from the capture
70/// source, so only a bring-your-own-PCM caller builds one.
71#[derive(Clone, Debug)]
72pub struct Input {
73	/// How samples are packed in each buffer.
74	pub format: Format,
75	/// Samples per second per channel. Resampled to the codec rate if they differ.
76	pub sample_rate: u32,
77	/// Channels per frame.
78	pub channels: u32,
79}
80
81impl Default for Input {
82	fn default() -> Self {
83		Self {
84			format: Format::F32,
85			sample_rate: 48_000,
86			channels: 2,
87		}
88	}
89}
90
91/// Encoder configuration: the input PCM layout plus the codec knobs.
92///
93/// The bring-your-own-PCM counterpart to [`Options`](super::Options), which
94/// `publish_capture` uses when the layout comes from the capture source instead
95/// of the caller.
96///
97/// `#[non_exhaustive]`: build via [`Config::new`] and set the optional fields,
98/// so future knobs don't break callers.
99#[derive(Clone, Debug)]
100#[non_exhaustive]
101pub struct Config {
102	/// The PCM layout fed to the encoder.
103	pub input: Input,
104	/// Output codec. Defaults to [`Codec::Opus`].
105	pub codec: Codec,
106	/// Sample rate the codec runs at. `None` snaps [`Input::sample_rate`] up to
107	/// the nearest rate the codec supports, resampling if that moved it.
108	pub sample_rate: Option<u32>,
109	/// Channel count the codec runs at. `None` matches [`Input::channels`];
110	/// anything else is rejected, since remapping isn't implemented.
111	pub channels: Option<u32>,
112	/// Bitrate in bits per second. `None` lets Opus pick. PCM requires `None`
113	/// because its bitrate is fixed by the sample rate and channel count.
114	pub bitrate: Option<u32>,
115	/// Enable Opus in-band forward error correction.
116	pub fec: bool,
117	/// Enable Opus discontinuous transmission during silence.
118	pub dtx: bool,
119	/// Encoded frame duration. Opus accepts 2.5 / 5 / 10 / 20 / 40 / 60 ms.
120	/// PCM accepts any duration containing a whole number of samples.
121	pub frame_duration: Duration,
122}
123
124impl Config {
125	/// A config encoding `input` with the default codec settings.
126	pub fn new(input: Input) -> Self {
127		Self {
128			input,
129			codec: Codec::default(),
130			sample_rate: None,
131			channels: None,
132			bitrate: None,
133			fec: false,
134			dtx: false,
135			frame_duration: Duration::from_millis(20),
136		}
137	}
138}
139
140/// Audio encoder over the PCM layout declared in [`Config::input`].
141///
142/// Build one with [`Encoder::new`], feed it PCM via [`encode`](Self::encode),
143/// and publish the resulting packets through a [`Producer`](super::Producer)
144/// built from the same [`Config`].
145pub struct Encoder {
146	backend: Backend,
147	config: Config,
148	/// Resolved codec sample rate (from `config.sample_rate`, else the input rate
149	/// snapped up to a supported one).
150	codec_rate: u32,
151	/// Resolved codec channel count (currently always the input's).
152	codec_channels: u32,
153	/// Current libopus target bitrate.
154	bitrate: u64,
155	/// Encoder lookahead expressed in the OpusHead 48 kHz timebase.
156	pre_skip: u16,
157	frame_size: usize,
158}
159
160enum Backend {
161	Opus(Opus),
162	Pcm,
163}
164
165struct Opus {
166	inner: *mut OpusEncoder,
167	scratch: Vec<u8>,
168}
169
170// SAFETY: OpusEncoder is heap-allocated state owned exclusively by this
171// struct; libopus encoder methods take a single &mut, so a unique owner is
172// allowed to move it across threads.
173unsafe impl Send for Opus {}
174
175impl Encoder {
176	/// Open an encoder for `config`.
177	pub fn new(config: &Config) -> Result<Self, Error> {
178		match config.codec {
179			Codec::Opus => Self::new_opus(config.clone()),
180			Codec::Pcm => Self::new_pcm(config.clone()),
181		}
182	}
183
184	fn new_opus(config: Config) -> Result<Self, Error> {
185		let codec_rate = config
186			.sample_rate
187			.unwrap_or_else(|| opus::pick_rate(config.input.sample_rate));
188		opus::validate_rate(codec_rate)?;
189
190		let codec_channels = config.channels.unwrap_or(config.input.channels);
191		if codec_channels != config.input.channels {
192			return Err(Error::Unsupported(format!(
193				"channel remapping not implemented (input {}ch, output {codec_channels}ch)",
194				config.input.channels
195			)));
196		}
197		let channels = opus::validate_channels(codec_channels)?;
198
199		let frame_size = opus::frame_size(codec_rate, config.frame_duration)?;
200
201		let mut err = 0i32;
202		// SAFETY: out-pointer `err` is valid; inner is checked for null below.
203		let inner = unsafe { opus_encoder_create(codec_rate as i32, channels, OPUS_APPLICATION_AUDIO, &mut err) };
204		if err != OPUS_OK || inner.is_null() {
205			return Err(opus::error(err, "opus_encoder_create"));
206		}
207
208		let configured = Self::configure_opus(inner, &config, codec_rate, codec_channels);
209		let (bitrate, pre_skip) = match configured {
210			Ok(configured) => configured,
211			Err(err) => {
212				// SAFETY: `inner` was created above and not yet handed out.
213				unsafe { opus_encoder_destroy(inner) };
214				return Err(err);
215			}
216		};
217
218		Ok(Self {
219			backend: Backend::Opus(Opus {
220				inner,
221				scratch: vec![0u8; MAX_PACKET_BYTES],
222			}),
223			config,
224			codec_rate,
225			codec_channels,
226			bitrate,
227			pre_skip,
228			frame_size,
229		})
230	}
231
232	fn new_pcm(config: Config) -> Result<Self, Error> {
233		if config.bitrate.is_some() {
234			return Err(Error::Unsupported(
235				"pcm bitrate is fixed; leave Config::bitrate unset".into(),
236			));
237		}
238
239		let codec_rate = config.sample_rate.unwrap_or(config.input.sample_rate);
240		if codec_rate == 0 {
241			return Err(Error::Unsupported("pcm sample rate must be greater than zero".into()));
242		}
243
244		let codec_channels = config.channels.unwrap_or(config.input.channels);
245		if codec_channels == 0 {
246			return Err(Error::Unsupported("pcm channel count must be greater than zero".into()));
247		}
248		if codec_channels != config.input.channels {
249			return Err(Error::Unsupported(format!(
250				"channel remapping not implemented (input {}ch, output {codec_channels}ch)",
251				config.input.channels
252			)));
253		}
254
255		let frame_size = pcm::frame_size(codec_rate, config.frame_duration)?;
256		pcm::frame_bytes(frame_size, codec_channels)?;
257		let bitrate = pcm::bitrate(codec_rate, codec_channels)?;
258		Ok(Self {
259			backend: Backend::Pcm,
260			config,
261			codec_rate,
262			codec_channels,
263			bitrate,
264			pre_skip: 0,
265			frame_size,
266		})
267	}
268
269	fn configure_opus(
270		inner: *mut OpusEncoder,
271		config: &Config,
272		codec_rate: u32,
273		codec_channels: u32,
274	) -> Result<(u64, u16), Error> {
275		if let Some(bitrate) = config.bitrate {
276			Self::set_opus_bitrate(inner, codec_channels, bitrate as u64)?;
277		}
278		Self::set_opus_ctl(
279			inner,
280			OPUS_SET_INBAND_FEC_REQUEST,
281			i32::from(config.fec),
282			"OPUS_SET_INBAND_FEC",
283		)?;
284		Self::set_opus_ctl(inner, OPUS_SET_DTX_REQUEST, i32::from(config.dtx), "OPUS_SET_DTX")?;
285
286		let bitrate = Self::get_opus_ctl(inner, OPUS_GET_BITRATE_REQUEST, "OPUS_GET_BITRATE")?;
287		let bitrate = u64::try_from(bitrate)
288			.map_err(|_| Error::Unsupported(format!("Opus reported negative bitrate {bitrate}")))?;
289		let lookahead = Self::get_opus_ctl(inner, OPUS_GET_LOOKAHEAD_REQUEST, "OPUS_GET_LOOKAHEAD")?;
290		let lookahead = u64::try_from(lookahead)
291			.map_err(|_| Error::Unsupported(format!("Opus reported negative lookahead {lookahead}")))?;
292		let pre_skip = u16::try_from((lookahead * 48_000) / codec_rate as u64)
293			.map_err(|_| Error::Unsupported(format!("Opus lookahead {lookahead} does not fit in OpusHead")))?;
294
295		Ok((bitrate, pre_skip))
296	}
297
298	fn set_opus_bitrate(inner: *mut OpusEncoder, channels: u32, bitrate: u64) -> Result<(), Error> {
299		let max = 300_000 * channels as u64;
300		if !(500..=max).contains(&bitrate) {
301			return Err(Error::Unsupported(format!(
302				"Opus bitrate must be between 500 and {max} bits per second for {channels} channel(s), got {bitrate}"
303			)));
304		}
305		Self::set_opus_ctl(inner, OPUS_SET_BITRATE_REQUEST, bitrate as i32, "OPUS_SET_BITRATE")
306	}
307
308	fn set_opus_ctl(inner: *mut OpusEncoder, request: i32, value: i32, name: &'static str) -> Result<(), Error> {
309		// SAFETY: `inner` owns a live encoder and each request here expects one i32.
310		let rc = unsafe { opus_encoder_ctl_impl(inner, request, varargs![value]) };
311		if rc != OPUS_OK {
312			return Err(opus::error(rc, name));
313		}
314		Ok(())
315	}
316
317	fn get_opus_ctl(inner: *mut OpusEncoder, request: i32, name: &'static str) -> Result<i32, Error> {
318		let mut value = 0;
319		// SAFETY: `inner` owns a live encoder and each request here expects one
320		// valid mutable i32 output.
321		let rc = unsafe { opus_encoder_ctl_impl(inner, request, varargs![&mut value]) };
322		if rc != OPUS_OK {
323			return Err(opus::error(rc, name));
324		}
325		Ok(value)
326	}
327
328	/// The encoder config, including the latest accepted runtime bitrate.
329	pub fn config(&self) -> &Config {
330		&self.config
331	}
332
333	/// The codec this encoder emits. A [`Producer`](super::Producer) must be
334	/// built for the same codec to publish its packets.
335	pub fn codec(&self) -> Codec {
336		self.config.codec
337	}
338
339	/// Sample rate the codec actually runs at, which is
340	/// [`Config::sample_rate`] resolved.
341	pub fn codec_rate(&self) -> u32 {
342		self.codec_rate
343	}
344
345	/// Channel count the codec actually runs at, which is
346	/// [`Config::channels`] resolved.
347	pub fn codec_channels(&self) -> u32 {
348		self.codec_channels
349	}
350
351	/// Number of samples per channel the codec consumes per call to
352	/// [`encode`](Self::encode).
353	pub fn frame_size(&self) -> usize {
354		self.frame_size
355	}
356
357	/// Current target bitrate in bits per second.
358	pub fn bitrate(&self) -> u64 {
359		self.bitrate
360	}
361
362	/// Retune the live Opus encoder to `bitrate` bits per second.
363	pub fn set_bitrate(&mut self, bitrate: u64) -> Result<(), Error> {
364		let Backend::Opus(opus) = &mut self.backend else {
365			return Err(Error::Unsupported("pcm bitrate is fixed".into()));
366		};
367		if bitrate != self.bitrate {
368			Self::set_opus_bitrate(opus.inner, self.codec_channels, bitrate)?;
369			self.bitrate = bitrate;
370			self.config.bitrate = Some(bitrate as u32);
371		}
372		Ok(())
373	}
374
375	/// Encode one frame of interleaved `f32` PCM at [`codec_rate`](Self::codec_rate).
376	///
377	/// `pcm.len()` must equal `frame_size() * codec_channels()`. The
378	/// [`Producer`](super::Producer) handles format conversion and resampling
379	/// before calling this; for direct use, the caller does the same.
380	pub fn encode(&mut self, pcm: &[f32]) -> Result<Bytes, Error> {
381		let expected = self.frame_size * self.codec_channels as usize;
382		if pcm.len() != expected {
383			return Err(Error::Misaligned {
384				got: std::mem::size_of_val(pcm),
385				expected: expected * std::mem::size_of::<f32>(),
386			});
387		}
388		match &mut self.backend {
389			Backend::Opus(opus) => {
390				// SAFETY: `inner` owns a live OpusEncoder; pcm and scratch slices
391				// are bounded by the lengths we pass.
392				let n = unsafe {
393					opus_encode_float(
394						opus.inner,
395						pcm.as_ptr(),
396						self.frame_size as i32,
397						opus.scratch.as_mut_ptr(),
398						opus.scratch.len() as i32,
399					)
400				};
401				if n < 0 {
402					return Err(crate::opus::error(n, "opus_encode_float"));
403				}
404				Ok(Bytes::copy_from_slice(&opus.scratch[..n as usize]))
405			}
406			Backend::Pcm => {
407				let mut payload = Vec::with_capacity(std::mem::size_of_val(pcm));
408				for sample in pcm {
409					payload.extend_from_slice(&sample.to_le_bytes());
410				}
411				Ok(payload.into())
412			}
413		}
414	}
415
416	/// hang catalog entry describing this encoder's output stream.
417	pub fn catalog(&self) -> hang::catalog::AudioConfig {
418		match self.config.codec {
419			Codec::Opus => {
420				// `codec_channels` is validated to mono/stereo at encoder construction,
421				// so the OpusHead (channel mapping family 0) always encodes.
422				let head = moq_mux::codec::opus::Config::new(self.codec_rate, self.codec_channels)
423					.with_pre_skip(self.pre_skip)
424					.encode()
425					.expect("opus encoder channels validated to mono/stereo");
426
427				let mut config = hang::catalog::AudioConfig::new(
428					hang::catalog::AudioCodec::Opus,
429					self.codec_rate,
430					self.codec_channels,
431				);
432				config.bitrate = self.config.bitrate.map(u64::from);
433				config.description = Some(head);
434				config.container = hang::catalog::Container::Legacy;
435				config
436			}
437			Codec::Pcm => {
438				let mut config = hang::catalog::AudioConfig::new(
439					hang::catalog::AudioCodec::Pcm,
440					self.codec_rate,
441					self.codec_channels,
442				);
443				config.bitrate = Some(
444					pcm::bitrate(self.codec_rate, self.codec_channels)
445						.expect("pcm encoder bitrate validated at construction"),
446				);
447				config.container = hang::catalog::Container::Legacy;
448				config
449			}
450		}
451	}
452}
453
454impl Drop for Opus {
455	fn drop(&mut self) {
456		// SAFETY: `inner` is a live OpusEncoder that nothing else aliases.
457		unsafe { opus_encoder_destroy(self.inner) };
458	}
459}
460
461#[cfg(test)]
462mod tests {
463	use super::*;
464	use crate::decode::Decoder;
465
466	fn sine(freq: f32, sample_rate: u32, channels: u32, frames: usize) -> Vec<f32> {
467		let mut out = Vec::with_capacity(frames * channels as usize);
468		for i in 0..frames {
469			let t = i as f32 / sample_rate as f32;
470			let v = (2.0 * std::f32::consts::PI * freq * t).sin() * 0.5;
471			for _ in 0..channels {
472				out.push(v);
473			}
474		}
475		out
476	}
477
478	fn stereo_48k() -> Input {
479		Input {
480			format: Format::F32,
481			sample_rate: 48_000,
482			channels: 2,
483		}
484	}
485
486	fn opus_inner(encoder: &Encoder) -> *mut OpusEncoder {
487		let Backend::Opus(opus) = &encoder.backend else {
488			panic!("expected Opus encoder");
489		};
490		opus.inner
491	}
492
493	#[test]
494	fn opus_encode_then_decode_keeps_signal_close() {
495		let mut enc = Encoder::new(&Config {
496			bitrate: Some(96_000),
497			..Config::new(stereo_48k())
498		})
499		.unwrap();
500
501		let cfg = enc.catalog();
502		let mut dec = Decoder::new(&cfg).unwrap();
503
504		let frame = sine(440.0, 48_000, 2, enc.frame_size());
505		for _ in 0..5 {
506			let pkt = enc.encode(&frame).unwrap();
507			let _ = dec.decode(&pkt).unwrap();
508		}
509
510		let pkt = enc.encode(&frame).unwrap();
511		let decoded = dec.decode(&pkt).unwrap();
512		assert_eq!(decoded.len(), frame.len());
513
514		let energy_in: f32 = frame.iter().map(|s| s * s).sum();
515		let energy_out: f32 = decoded.iter().map(|s| s * s).sum();
516		let ratio = energy_out / energy_in;
517		assert!(
518			(0.5..2.0).contains(&ratio),
519			"output energy ratio {ratio:.3} should be close to 1"
520		);
521	}
522
523	#[test]
524	fn opus_rejects_unsupported_frame_duration() {
525		let err = Encoder::new(&Config {
526			frame_duration: Duration::from_millis(15),
527			..Config::new(Input::default())
528		});
529		assert!(matches!(err, Err(Error::Unsupported(_))));
530	}
531
532	#[test]
533	fn opus_rejects_misaligned_input() {
534		let mut enc = Encoder::new(&Config::new(Input::default())).unwrap();
535		assert!(matches!(enc.encode(&[0.0f32; 100]), Err(Error::Misaligned { .. })));
536	}
537
538	#[test]
539	fn opus_catalog_includes_opushead() {
540		let enc = Encoder::new(&Config {
541			bitrate: Some(64_000),
542			..Config::new(stereo_48k())
543		})
544		.unwrap();
545		let cfg = enc.catalog();
546		assert_eq!(cfg.sample_rate, 48_000);
547		assert_eq!(cfg.channel_count, 2);
548		assert_eq!(cfg.bitrate, Some(64_000));
549		let desc = cfg.description.expect("OpusHead should be present");
550		assert_eq!(desc.len(), 19);
551		let head = moq_mux::codec::opus::Config::parse(&mut desc.as_ref()).unwrap();
552		assert_eq!(head.pre_skip, enc.pre_skip);
553		assert_eq!(head.pre_skip, 312);
554	}
555
556	#[test]
557	fn opus_decoder_trims_encoder_lookahead_once() {
558		let mut enc = Encoder::new(&Config::new(stereo_48k())).unwrap();
559		let mut dec = Decoder::new(&enc.catalog()).unwrap();
560		let frame = vec![0.0; enc.frame_size() * enc.codec_channels() as usize];
561
562		let first = dec.decode(&enc.encode(&frame).unwrap()).unwrap();
563		assert_eq!(
564			first.len(),
565			(enc.frame_size() - enc.pre_skip as usize) * enc.codec_channels() as usize
566		);
567
568		let second = dec.decode(&enc.encode(&frame).unwrap()).unwrap();
569		assert_eq!(second.len(), frame.len());
570	}
571
572	#[test]
573	fn opus_runtime_bitrate_updates_encoder_state() {
574		let mut enc = Encoder::new(&Config {
575			bitrate: Some(64_000),
576			..Config::new(stereo_48k())
577		})
578		.unwrap();
579
580		enc.set_bitrate(32_000).unwrap();
581		assert_eq!(enc.bitrate(), 32_000);
582		assert_eq!(enc.config().bitrate, Some(32_000));
583		assert_eq!(
584			Encoder::get_opus_ctl(
585				opus_inner(&enc),
586				unsafe_libopus::OPUS_GET_BITRATE_REQUEST,
587				"OPUS_GET_BITRATE"
588			)
589			.unwrap(),
590			32_000
591		);
592	}
593
594	#[test]
595	fn opus_runtime_bitrate_rejects_values_libopus_would_clamp() {
596		let mut enc = Encoder::new(&Config::new(stereo_48k())).unwrap();
597		let original = enc.bitrate();
598		assert!(enc.set_bitrate(1).is_err());
599		assert!(enc.set_bitrate(600_001).is_err());
600		assert_eq!(enc.bitrate(), original);
601	}
602
603	#[test]
604	fn opus_applies_fec_and_dtx_controls() {
605		let enc = Encoder::new(&Config {
606			fec: true,
607			dtx: true,
608			..Config::new(stereo_48k())
609		})
610		.unwrap();
611
612		assert_eq!(
613			Encoder::get_opus_ctl(
614				opus_inner(&enc),
615				unsafe_libopus::OPUS_GET_INBAND_FEC_REQUEST,
616				"OPUS_GET_INBAND_FEC"
617			)
618			.unwrap(),
619			1
620		);
621		assert_eq!(
622			Encoder::get_opus_ctl(opus_inner(&enc), unsafe_libopus::OPUS_GET_DTX_REQUEST, "OPUS_GET_DTX").unwrap(),
623			1
624		);
625	}
626
627	#[test]
628	fn codec_roundtrips_as_str() {
629		assert_eq!(Codec::Opus.as_str(), "opus");
630		assert_eq!(Codec::Opus.to_string(), "opus");
631		assert_eq!("opus".parse::<Codec>().unwrap(), Codec::Opus);
632		assert_eq!(Codec::Pcm.as_str(), "pcm");
633		assert_eq!(Codec::Pcm.to_string(), "pcm");
634		assert_eq!("pcm".parse::<Codec>().unwrap(), Codec::Pcm);
635		assert!("aac".parse::<Codec>().is_err());
636	}
637
638	#[test]
639	fn config_sample_rate_overrides_the_codec_rate() {
640		let enc = Encoder::new(&Config {
641			sample_rate: Some(24_000),
642			..Config::new(Input {
643				sample_rate: 48_000,
644				channels: 1,
645				..Input::default()
646			})
647		})
648		.unwrap();
649		assert_eq!(enc.codec_rate(), 24_000);
650		assert_eq!(enc.catalog().sample_rate, 24_000);
651		assert_eq!(enc.pre_skip, 312);
652	}
653
654	#[test]
655	fn pcm_roundtrip_is_lossless() {
656		let mut enc = Encoder::new(&Config {
657			codec: Codec::Pcm,
658			..Config::new(stereo_48k())
659		})
660		.unwrap();
661		let mut dec = Decoder::new(&enc.catalog()).unwrap();
662		let input = sine(440.0, enc.codec_rate(), enc.codec_channels(), enc.frame_size());
663
664		let packet = enc.encode(&input).unwrap();
665		let output = dec.decode(&packet).unwrap();
666
667		assert_eq!(output, input);
668	}
669
670	#[test]
671	fn pcm_catalog_declares_fixed_bitrate() {
672		let enc = Encoder::new(&Config {
673			codec: Codec::Pcm,
674			..Config::new(stereo_48k())
675		})
676		.unwrap();
677		let catalog = enc.catalog();
678
679		assert_eq!(catalog.codec, hang::catalog::AudioCodec::Pcm);
680		assert_eq!(catalog.bitrate, Some(48_000 * 2 * 32));
681		assert_eq!(catalog.description, None);
682	}
683
684	#[test]
685	fn pcm_rejects_runtime_bitrate_change() {
686		let mut enc = Encoder::new(&Config {
687			codec: Codec::Pcm,
688			..Config::new(stereo_48k())
689		})
690		.unwrap();
691		let bitrate = enc.bitrate();
692
693		assert!(matches!(enc.set_bitrate(bitrate), Err(Error::Unsupported(_))));
694		assert_eq!(enc.bitrate(), bitrate);
695	}
696
697	#[test]
698	fn pcm_rejects_fractional_sample_frame_duration() {
699		let err = Encoder::new(&Config {
700			codec: Codec::Pcm,
701			frame_duration: Duration::from_micros(2_500),
702			..Config::new(Input {
703				sample_rate: 44_100,
704				..Input::default()
705			})
706		});
707		assert!(matches!(err, Err(Error::Unsupported(_))));
708	}
709
710	#[test]
711	fn pcm_rejects_bitrate_overflow() {
712		let err = Encoder::new(&Config {
713			codec: Codec::Pcm,
714			frame_duration: Duration::from_secs(1),
715			..Config::new(Input {
716				sample_rate: u32::MAX,
717				channels: u32::MAX,
718				..Input::default()
719			})
720		});
721		assert!(matches!(err, Err(Error::Unsupported(_))));
722	}
723}