Skip to main content

moq_audio/encode/
encoder.rs

1//! Opus encoder front end.
2//!
3//! Single-codec implementation today: [`Encoder`] wraps libopus 1.3.1 via
4//! [`unsafe_libopus`], a pure-Rust c2rust transpilation. No CMake toolchain, no
5//! sys crate, no linker gymnastics. When AAC or other codecs land we'll factor
6//! out a backend dispatch behind [`Codec`]; introducing a trait now would be
7//! premature.
8
9use std::str::FromStr;
10use std::time::Duration;
11
12use bytes::Bytes;
13use unsafe_libopus::{
14	OPUS_APPLICATION_AUDIO, OPUS_OK, OPUS_SET_BITRATE_REQUEST, OpusEncoder, opus_encode_float, opus_encoder_create,
15	opus_encoder_ctl_impl, opus_encoder_destroy, varargs,
16};
17
18use crate::opus;
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). The only codec today, and the default.
30	#[default]
31	Opus,
32}
33
34impl Codec {
35	/// Canonical lowercase identifier, matching the WebCodecs / RFC catalog
36	/// string. Used as the wire/FFI codec name everywhere.
37	pub fn as_str(self) -> &'static str {
38		match self {
39			Self::Opus => "opus",
40		}
41	}
42}
43
44impl std::fmt::Display for Codec {
45	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46		f.write_str(self.as_str())
47	}
48}
49
50impl FromStr for Codec {
51	type Err = Error;
52
53	fn from_str(s: &str) -> Result<Self, Self::Err> {
54		match s {
55			"opus" => Ok(Self::Opus),
56			other => Err(Error::Unsupported(format!("unknown codec: {other}"))),
57		}
58	}
59}
60
61/// The PCM layout of the buffers handed to [`Encoder::encode`] /
62/// [`Producer::write`](super::Producer::write).
63///
64/// The encoder's counterpart to a video encoder's width / height: it describes
65/// the input, not the output. `publish_capture` fills it in from the capture
66/// source, so only a bring-your-own-PCM caller builds one.
67#[derive(Clone, Debug)]
68pub struct Input {
69	/// How samples are packed in each buffer.
70	pub format: Format,
71	/// Samples per second per channel. Resampled to the codec rate if they differ.
72	pub sample_rate: u32,
73	/// Channels per frame.
74	pub channels: u32,
75}
76
77impl Default for Input {
78	fn default() -> Self {
79		Self {
80			format: Format::F32,
81			sample_rate: 48_000,
82			channels: 2,
83		}
84	}
85}
86
87/// Encoder configuration: the input PCM layout plus the codec knobs.
88///
89/// The bring-your-own-PCM counterpart to [`Options`](super::Options), which
90/// `publish_capture` uses when the layout comes from the capture source instead
91/// of the caller.
92///
93/// `#[non_exhaustive]`: build via [`Config::new`] and set the optional fields,
94/// so future knobs don't break callers.
95#[derive(Clone, Debug)]
96#[non_exhaustive]
97pub struct Config {
98	/// The PCM layout fed to the encoder.
99	pub input: Input,
100	/// Output codec. Defaults to [`Codec::Opus`].
101	pub codec: Codec,
102	/// Sample rate the codec runs at. `None` snaps [`Input::sample_rate`] up to
103	/// the nearest rate the codec supports, resampling if that moved it.
104	pub sample_rate: Option<u32>,
105	/// Channel count the codec runs at. `None` matches [`Input::channels`];
106	/// anything else is rejected, since remapping isn't implemented.
107	pub channels: Option<u32>,
108	/// Bitrate in bits per second. `None` lets the codec pick.
109	pub bitrate: Option<u32>,
110	/// Encoded frame duration. Opus accepts 2.5 / 5 / 10 / 20 / 40 / 60 ms.
111	pub frame_duration: Duration,
112}
113
114impl Config {
115	/// A config encoding `input` with the default codec settings.
116	pub fn new(input: Input) -> Self {
117		Self {
118			input,
119			codec: Codec::default(),
120			sample_rate: None,
121			channels: None,
122			bitrate: None,
123			frame_duration: Duration::from_millis(20),
124		}
125	}
126}
127
128/// Audio encoder over the PCM layout declared in [`Config::input`].
129///
130/// Build one with [`Encoder::new`], feed it PCM via [`encode`](Self::encode),
131/// and publish the resulting packets through a [`Producer`](super::Producer)
132/// built from the same [`Config`].
133pub struct Encoder {
134	inner: *mut OpusEncoder,
135	config: Config,
136	/// Resolved codec sample rate (from `config.sample_rate`, else the input rate
137	/// snapped up to a supported one).
138	codec_rate: u32,
139	/// Resolved codec channel count (currently always the input's).
140	codec_channels: u32,
141	frame_size: usize,
142	scratch: Vec<u8>,
143}
144
145// SAFETY: OpusEncoder is heap-allocated state owned exclusively by this
146// struct; libopus encoder methods take a single &mut, so a unique
147// owner is allowed to move it across threads.
148unsafe impl Send for Encoder {}
149
150impl Encoder {
151	/// Open an encoder for `config`.
152	pub fn new(config: &Config) -> Result<Self, Error> {
153		match config.codec {
154			Codec::Opus => Self::new_opus(config.clone()),
155		}
156	}
157
158	fn new_opus(config: Config) -> Result<Self, Error> {
159		let codec_rate = config
160			.sample_rate
161			.unwrap_or_else(|| opus::pick_rate(config.input.sample_rate));
162		opus::validate_rate(codec_rate)?;
163
164		let codec_channels = config.channels.unwrap_or(config.input.channels);
165		if codec_channels != config.input.channels {
166			return Err(Error::Unsupported(format!(
167				"channel remapping not implemented (input {}ch, output {codec_channels}ch)",
168				config.input.channels
169			)));
170		}
171		let channels = opus::validate_channels(codec_channels)?;
172
173		let frame_size = opus::frame_size(codec_rate, config.frame_duration)?;
174
175		let mut err = 0i32;
176		// SAFETY: out-pointer `err` is valid; inner is checked for null below.
177		let inner = unsafe { opus_encoder_create(codec_rate as i32, channels, OPUS_APPLICATION_AUDIO, &mut err) };
178		if err != OPUS_OK || inner.is_null() {
179			return Err(opus::error(err, "opus_encoder_create"));
180		}
181
182		if let Some(b) = config.bitrate {
183			// SAFETY: `inner` is a freshly-created encoder; varargs! produces
184			// the single i32 the SET_BITRATE request expects.
185			let rc = unsafe { opus_encoder_ctl_impl(inner, OPUS_SET_BITRATE_REQUEST, varargs![b as i32]) };
186			if rc != OPUS_OK {
187				// SAFETY: `inner` was created above and not yet handed out.
188				unsafe { opus_encoder_destroy(inner) };
189				return Err(opus::error(rc, "OPUS_SET_BITRATE"));
190			}
191		}
192
193		Ok(Self {
194			inner,
195			config,
196			codec_rate,
197			codec_channels,
198			frame_size,
199			scratch: vec![0u8; MAX_PACKET_BYTES],
200		})
201	}
202
203	/// The config this encoder opened with.
204	pub fn config(&self) -> &Config {
205		&self.config
206	}
207
208	/// The codec this encoder emits. A [`Producer`](super::Producer) must be
209	/// built for the same codec to publish its packets.
210	pub fn codec(&self) -> Codec {
211		self.config.codec
212	}
213
214	/// Sample rate the codec actually runs at, which is
215	/// [`Config::sample_rate`] resolved.
216	pub fn codec_rate(&self) -> u32 {
217		self.codec_rate
218	}
219
220	/// Channel count the codec actually runs at, which is
221	/// [`Config::channels`] resolved.
222	pub fn codec_channels(&self) -> u32 {
223		self.codec_channels
224	}
225
226	/// Number of samples per channel the codec consumes per call to
227	/// [`encode`](Self::encode).
228	pub fn frame_size(&self) -> usize {
229		self.frame_size
230	}
231
232	/// Encode one frame of interleaved `f32` PCM at [`codec_rate`](Self::codec_rate).
233	///
234	/// `pcm.len()` must equal `frame_size() * codec_channels()`. The
235	/// [`Producer`](super::Producer) handles format conversion and resampling
236	/// before calling this; for direct use, the caller does the same.
237	pub fn encode(&mut self, pcm: &[f32]) -> Result<Bytes, Error> {
238		let expected = self.frame_size * self.codec_channels as usize;
239		if pcm.len() != expected {
240			return Err(Error::Misaligned {
241				got: std::mem::size_of_val(pcm),
242				expected: expected * std::mem::size_of::<f32>(),
243			});
244		}
245		// SAFETY: `inner` owns a live OpusEncoder; pcm and scratch slices
246		// are bounded by the lengths we pass.
247		let n = unsafe {
248			opus_encode_float(
249				self.inner,
250				pcm.as_ptr(),
251				self.frame_size as i32,
252				self.scratch.as_mut_ptr(),
253				self.scratch.len() as i32,
254			)
255		};
256		if n < 0 {
257			return Err(opus::error(n, "opus_encode_float"));
258		}
259		Ok(Bytes::copy_from_slice(&self.scratch[..n as usize]))
260	}
261
262	/// hang catalog entry describing this encoder's output stream.
263	pub fn catalog(&self) -> hang::catalog::AudioConfig {
264		// `codec_channels` is validated to mono/stereo at encoder construction, so the
265		// OpusHead (channel mapping family 0) always encodes.
266		let head = moq_mux::codec::opus::Config {
267			sample_rate: self.codec_rate,
268			channel_count: self.codec_channels,
269		}
270		.encode()
271		.expect("opus encoder channels validated to mono/stereo");
272
273		let mut config =
274			hang::catalog::AudioConfig::new(hang::catalog::AudioCodec::Opus, self.codec_rate, self.codec_channels);
275		config.bitrate = self.config.bitrate.map(|b| b as u64);
276		config.description = Some(head);
277		config.container = hang::catalog::Container::Legacy;
278		config
279	}
280}
281
282impl Drop for Encoder {
283	fn drop(&mut self) {
284		// SAFETY: `inner` is a live OpusEncoder that nothing else aliases.
285		unsafe { opus_encoder_destroy(self.inner) };
286	}
287}
288
289#[cfg(test)]
290mod tests {
291	use super::*;
292	use crate::decode::Decoder;
293
294	fn sine(freq: f32, sample_rate: u32, channels: u32, frames: usize) -> Vec<f32> {
295		let mut out = Vec::with_capacity(frames * channels as usize);
296		for i in 0..frames {
297			let t = i as f32 / sample_rate as f32;
298			let v = (2.0 * std::f32::consts::PI * freq * t).sin() * 0.5;
299			for _ in 0..channels {
300				out.push(v);
301			}
302		}
303		out
304	}
305
306	fn stereo_48k() -> Input {
307		Input {
308			format: Format::F32,
309			sample_rate: 48_000,
310			channels: 2,
311		}
312	}
313
314	#[test]
315	fn opus_encode_then_decode_keeps_signal_close() {
316		let mut enc = Encoder::new(&Config {
317			bitrate: Some(96_000),
318			..Config::new(stereo_48k())
319		})
320		.unwrap();
321
322		let cfg = enc.catalog();
323		let mut dec = Decoder::new(&cfg).unwrap();
324
325		let frame = sine(440.0, 48_000, 2, enc.frame_size());
326		for _ in 0..5 {
327			let pkt = enc.encode(&frame).unwrap();
328			let _ = dec.decode(&pkt).unwrap();
329		}
330
331		let pkt = enc.encode(&frame).unwrap();
332		let decoded = dec.decode(&pkt).unwrap();
333		assert_eq!(decoded.len(), frame.len());
334
335		let energy_in: f32 = frame.iter().map(|s| s * s).sum();
336		let energy_out: f32 = decoded.iter().map(|s| s * s).sum();
337		let ratio = energy_out / energy_in;
338		assert!(
339			(0.5..2.0).contains(&ratio),
340			"output energy ratio {ratio:.3} should be close to 1"
341		);
342	}
343
344	#[test]
345	fn opus_rejects_unsupported_frame_duration() {
346		let err = Encoder::new(&Config {
347			frame_duration: Duration::from_millis(15),
348			..Config::new(Input::default())
349		});
350		assert!(matches!(err, Err(Error::Unsupported(_))));
351	}
352
353	#[test]
354	fn opus_rejects_misaligned_input() {
355		let mut enc = Encoder::new(&Config::new(Input::default())).unwrap();
356		assert!(matches!(enc.encode(&[0.0f32; 100]), Err(Error::Misaligned { .. })));
357	}
358
359	#[test]
360	fn opus_catalog_includes_opushead() {
361		let enc = Encoder::new(&Config {
362			bitrate: Some(64_000),
363			..Config::new(stereo_48k())
364		})
365		.unwrap();
366		let cfg = enc.catalog();
367		assert_eq!(cfg.sample_rate, 48_000);
368		assert_eq!(cfg.channel_count, 2);
369		assert_eq!(cfg.bitrate, Some(64_000));
370		let desc = cfg.description.expect("OpusHead should be present");
371		assert_eq!(desc.len(), 19);
372	}
373
374	#[test]
375	fn codec_roundtrips_as_str() {
376		assert_eq!(Codec::Opus.as_str(), "opus");
377		assert_eq!(Codec::Opus.to_string(), "opus");
378		assert_eq!("opus".parse::<Codec>().unwrap(), Codec::Opus);
379		assert!("aac".parse::<Codec>().is_err());
380	}
381
382	#[test]
383	fn config_sample_rate_overrides_the_codec_rate() {
384		let enc = Encoder::new(&Config {
385			sample_rate: Some(24_000),
386			..Config::new(Input {
387				sample_rate: 48_000,
388				channels: 1,
389				..Input::default()
390			})
391		})
392		.unwrap();
393		assert_eq!(enc.codec_rate(), 24_000);
394		assert_eq!(enc.catalog().sample_rate, 24_000);
395	}
396}