1use 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
21const MAX_PACKET_BYTES: usize = 4_000;
23
24#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
27#[non_exhaustive]
28pub enum Codec {
29 #[default]
31 Opus,
32}
33
34impl Codec {
35 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#[derive(Clone, Debug)]
68pub struct Input {
69 pub format: Format,
71 pub sample_rate: u32,
73 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#[derive(Clone, Debug)]
96#[non_exhaustive]
97pub struct Config {
98 pub input: Input,
100 pub codec: Codec,
102 pub sample_rate: Option<u32>,
105 pub channels: Option<u32>,
108 pub bitrate: Option<u32>,
110 pub frame_duration: Duration,
112}
113
114impl Config {
115 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
128pub struct Encoder {
134 inner: *mut OpusEncoder,
135 config: Config,
136 codec_rate: u32,
139 codec_channels: u32,
141 frame_size: usize,
142 scratch: Vec<u8>,
143}
144
145unsafe impl Send for Encoder {}
149
150impl Encoder {
151 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 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 let rc = unsafe { opus_encoder_ctl_impl(inner, OPUS_SET_BITRATE_REQUEST, varargs![b as i32]) };
186 if rc != OPUS_OK {
187 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 pub fn config(&self) -> &Config {
205 &self.config
206 }
207
208 pub fn codec(&self) -> Codec {
211 self.config.codec
212 }
213
214 pub fn codec_rate(&self) -> u32 {
217 self.codec_rate
218 }
219
220 pub fn codec_channels(&self) -> u32 {
223 self.codec_channels
224 }
225
226 pub fn frame_size(&self) -> usize {
229 self.frame_size
230 }
231
232 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 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 pub fn catalog(&self) -> hang::catalog::AudioConfig {
264 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 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}