1use std::str::FromStr;
11use std::time::Duration;
12
13use bytes::Bytes;
14use unsafe_libopus::{
15 OPUS_APPLICATION_AUDIO, OPUS_OK, OPUS_SET_BITRATE_REQUEST, OpusDecoder, OpusEncoder, opus_decode_float,
16 opus_decoder_create, opus_decoder_destroy, opus_encode_float, opus_encoder_create, opus_encoder_ctl_impl,
17 opus_encoder_destroy, varargs,
18};
19
20use crate::{AudioError, AudioFormat};
21
22const MAX_PACKET_BYTES: usize = 4_000;
24
25#[derive(Copy, Clone, Debug, PartialEq, Eq)]
27#[non_exhaustive]
28pub enum Codec {
29 Opus,
30}
31
32impl Codec {
33 pub fn as_str(self) -> &'static str {
36 match self {
37 Self::Opus => "opus",
38 }
39 }
40}
41
42impl std::fmt::Display for Codec {
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 f.write_str(self.as_str())
45 }
46}
47
48impl FromStr for Codec {
49 type Err = AudioError;
50
51 fn from_str(s: &str) -> Result<Self, Self::Err> {
52 match s {
53 "opus" => Ok(Self::Opus),
54 other => Err(AudioError::Unsupported(format!("unknown codec: {other}"))),
55 }
56 }
57}
58
59#[derive(Clone, Debug)]
62pub struct EncoderInput {
63 pub format: AudioFormat,
64 pub sample_rate: u32,
65 pub channels: u32,
66}
67
68impl Default for EncoderInput {
69 fn default() -> Self {
70 Self {
71 format: AudioFormat::F32,
72 sample_rate: 48_000,
73 channels: 2,
74 }
75 }
76}
77
78#[derive(Clone, Debug)]
82pub struct EncoderOutput {
83 pub codec: Codec,
84 pub sample_rate: Option<u32>,
85 pub channels: Option<u32>,
86 pub bitrate: Option<u32>,
88 pub frame_duration: Duration,
90}
91
92impl Default for EncoderOutput {
93 fn default() -> Self {
94 Self {
95 codec: Codec::Opus,
96 sample_rate: None,
97 channels: None,
98 bitrate: None,
99 frame_duration: Duration::from_millis(20),
100 }
101 }
102}
103
104#[derive(Clone, Debug, Default)]
108pub struct DecoderOutput {
109 pub format: AudioFormat,
110 pub sample_rate: Option<u32>,
111 pub channels: Option<u32>,
112 pub latency_max: Option<Duration>,
124}
125
126fn validate_opus_channels(count: u32) -> Result<i32, AudioError> {
127 match count {
128 1 | 2 => Ok(count as i32),
129 other => Err(AudioError::Unsupported(format!(
130 "opus only supports 1 or 2 channels (got {other})"
131 ))),
132 }
133}
134
135fn opus_error(code: i32, context: &str) -> AudioError {
136 AudioError::Unsupported(format!("libopus {context} failed (code {code})"))
137}
138
139pub fn pick_opus_rate(input_rate: u32) -> u32 {
142 const SUPPORTED: [u32; 5] = [8_000, 12_000, 16_000, 24_000, 48_000];
143 SUPPORTED.iter().copied().find(|&r| r >= input_rate).unwrap_or(48_000)
144}
145
146fn validate_opus_rate(rate: u32) -> Result<(), AudioError> {
147 match rate {
148 8_000 | 12_000 | 16_000 | 24_000 | 48_000 => Ok(()),
149 other => Err(AudioError::Unsupported(format!(
150 "opus only supports 8/12/16/24/48 kHz (got {other})"
151 ))),
152 }
153}
154
155fn frame_size_for(sample_rate: u32, duration: Duration) -> Result<usize, AudioError> {
156 let micros = duration.as_micros();
158 let allowed = [2_500u128, 5_000, 10_000, 20_000, 40_000, 60_000];
159 if !allowed.contains(µs) {
160 return Err(AudioError::Unsupported(format!(
161 "opus frame duration must be 2.5/5/10/20/40/60 ms (got {} us)",
162 micros
163 )));
164 }
165 Ok((sample_rate as u128 * micros / 1_000_000) as usize)
166}
167
168pub struct Encoder {
170 inner: *mut OpusEncoder,
171 input: EncoderInput,
172 output: EncoderOutput,
173 codec_rate: u32,
176 codec_channels: u32,
178 frame_size: usize,
179 scratch: Vec<u8>,
180}
181
182unsafe impl Send for Encoder {}
186
187impl Encoder {
188 pub fn new(input: EncoderInput, output: EncoderOutput) -> Result<Self, AudioError> {
189 match output.codec {
190 Codec::Opus => Self::new_opus(input, output),
191 }
192 }
193
194 fn new_opus(input: EncoderInput, output: EncoderOutput) -> Result<Self, AudioError> {
195 let codec_rate = output.sample_rate.unwrap_or_else(|| pick_opus_rate(input.sample_rate));
196 validate_opus_rate(codec_rate)?;
197
198 let codec_channels = output.channels.unwrap_or(input.channels);
199 if codec_channels != input.channels {
200 return Err(AudioError::Unsupported(format!(
201 "channel remapping not implemented (input {}ch, output {codec_channels}ch)",
202 input.channels
203 )));
204 }
205 let channels = validate_opus_channels(codec_channels)?;
206
207 let frame_size = frame_size_for(codec_rate, output.frame_duration)?;
208
209 let mut err = 0i32;
210 let inner = unsafe { opus_encoder_create(codec_rate as i32, channels, OPUS_APPLICATION_AUDIO, &mut err) };
212 if err != OPUS_OK || inner.is_null() {
213 return Err(opus_error(err, "opus_encoder_create"));
214 }
215
216 if let Some(b) = output.bitrate {
217 let rc = unsafe { opus_encoder_ctl_impl(inner, OPUS_SET_BITRATE_REQUEST, varargs![b as i32]) };
220 if rc != OPUS_OK {
221 unsafe { opus_encoder_destroy(inner) };
223 return Err(opus_error(rc, "OPUS_SET_BITRATE"));
224 }
225 }
226
227 Ok(Self {
228 inner,
229 input,
230 output,
231 codec_rate,
232 codec_channels,
233 frame_size,
234 scratch: vec![0u8; MAX_PACKET_BYTES],
235 })
236 }
237
238 pub fn input(&self) -> &EncoderInput {
239 &self.input
240 }
241
242 pub fn output(&self) -> &EncoderOutput {
243 &self.output
244 }
245
246 pub fn codec_rate(&self) -> u32 {
248 self.codec_rate
249 }
250
251 pub fn codec_channels(&self) -> u32 {
253 self.codec_channels
254 }
255
256 pub fn frame_size(&self) -> usize {
258 self.frame_size
259 }
260
261 pub fn encode_f32(&mut self, pcm: &[f32]) -> Result<Bytes, AudioError> {
267 let expected = self.frame_size * self.codec_channels as usize;
268 if pcm.len() != expected {
269 return Err(AudioError::Misaligned {
270 got: std::mem::size_of_val(pcm),
271 expected: expected * std::mem::size_of::<f32>(),
272 });
273 }
274 let n = unsafe {
277 opus_encode_float(
278 self.inner,
279 pcm.as_ptr(),
280 self.frame_size as i32,
281 self.scratch.as_mut_ptr(),
282 self.scratch.len() as i32,
283 )
284 };
285 if n < 0 {
286 return Err(opus_error(n, "opus_encode_float"));
287 }
288 Ok(Bytes::copy_from_slice(&self.scratch[..n as usize]))
289 }
290
291 pub fn catalog(&self) -> hang::catalog::AudioConfig {
293 let head = moq_mux::codec::opus::Config {
294 sample_rate: self.codec_rate,
295 channel_count: self.codec_channels,
296 }
297 .encode();
298
299 let mut config =
300 hang::catalog::AudioConfig::new(hang::catalog::AudioCodec::Opus, self.codec_rate, self.codec_channels);
301 config.bitrate = self.output.bitrate.map(|b| b as u64);
302 config.description = Some(head);
303 config.container = hang::catalog::Container::Legacy;
304 config
305 }
306}
307
308pub struct Decoder {
310 inner: *mut OpusDecoder,
311 sample_rate: u32,
312 channel_count: u32,
313 max_frame_size: usize,
314}
315
316unsafe impl Send for Decoder {}
318
319impl Decoder {
320 pub fn new(catalog: &hang::catalog::AudioConfig) -> Result<Self, AudioError> {
325 let (sample_rate, channel_count) = if let Some(desc) = &catalog.description {
326 let mut buf = desc.as_ref();
327 match moq_mux::codec::opus::Config::parse(&mut buf) {
328 Ok(head) => (head.sample_rate, head.channel_count),
329 Err(_) => (catalog.sample_rate, catalog.channel_count),
330 }
331 } else {
332 (catalog.sample_rate, catalog.channel_count)
333 };
334
335 validate_opus_rate(sample_rate)?;
336 let channels = validate_opus_channels(channel_count)?;
337
338 let mut err = 0i32;
339 let inner = unsafe { opus_decoder_create(sample_rate as i32, channels, &mut err) };
341 if err != OPUS_OK || inner.is_null() {
342 return Err(opus_error(err, "opus_decoder_create"));
343 }
344
345 let max_frame_size = (sample_rate as usize * 120) / 1000;
347
348 Ok(Self {
349 inner,
350 sample_rate,
351 channel_count,
352 max_frame_size,
353 })
354 }
355
356 pub fn sample_rate(&self) -> u32 {
357 self.sample_rate
358 }
359
360 pub fn channel_count(&self) -> u32 {
361 self.channel_count
362 }
363
364 pub fn decode_f32(&mut self, packet: &[u8]) -> Result<Vec<f32>, AudioError> {
366 let mut out = vec![0.0f32; self.max_frame_size * self.channel_count as usize];
367 let samples = unsafe {
370 opus_decode_float(
371 &mut *self.inner,
372 packet.as_ptr(),
373 packet.len() as i32,
374 out.as_mut_ptr(),
375 self.max_frame_size as i32,
376 0,
377 )
378 };
379 if samples < 0 {
380 return Err(opus_error(samples, "opus_decode_float"));
381 }
382 out.truncate(samples as usize * self.channel_count as usize);
383 Ok(out)
384 }
385}
386
387impl Drop for Encoder {
388 fn drop(&mut self) {
389 unsafe { opus_encoder_destroy(self.inner) };
391 }
392}
393
394impl Drop for Decoder {
395 fn drop(&mut self) {
396 unsafe { opus_decoder_destroy(self.inner) };
398 }
399}
400
401#[cfg(test)]
402mod tests {
403 use super::*;
404
405 fn sine(freq: f32, sample_rate: u32, channels: u32, frames: usize) -> Vec<f32> {
406 let mut out = Vec::with_capacity(frames * channels as usize);
407 for i in 0..frames {
408 let t = i as f32 / sample_rate as f32;
409 let v = (2.0 * std::f32::consts::PI * freq * t).sin() * 0.5;
410 for _ in 0..channels {
411 out.push(v);
412 }
413 }
414 out
415 }
416
417 #[test]
418 fn opus_encode_then_decode_keeps_signal_close() {
419 let mut enc = Encoder::new(
420 EncoderInput {
421 format: AudioFormat::F32,
422 sample_rate: 48_000,
423 channels: 2,
424 },
425 EncoderOutput {
426 bitrate: Some(96_000),
427 ..EncoderOutput::default()
428 },
429 )
430 .unwrap();
431
432 let cfg = enc.catalog();
433 let mut dec = Decoder::new(&cfg).unwrap();
434
435 let frame = sine(440.0, 48_000, 2, enc.frame_size());
436 for _ in 0..5 {
437 let pkt = enc.encode_f32(&frame).unwrap();
438 let _ = dec.decode_f32(&pkt).unwrap();
439 }
440
441 let pkt = enc.encode_f32(&frame).unwrap();
442 let decoded = dec.decode_f32(&pkt).unwrap();
443 assert_eq!(decoded.len(), frame.len());
444
445 let energy_in: f32 = frame.iter().map(|s| s * s).sum();
446 let energy_out: f32 = decoded.iter().map(|s| s * s).sum();
447 let ratio = energy_out / energy_in;
448 assert!(
449 (0.5..2.0).contains(&ratio),
450 "output energy ratio {ratio:.3} should be close to 1"
451 );
452 }
453
454 #[test]
455 fn opus_rejects_unsupported_frame_duration() {
456 let err = Encoder::new(
457 EncoderInput::default(),
458 EncoderOutput {
459 frame_duration: Duration::from_millis(15),
460 ..EncoderOutput::default()
461 },
462 );
463 assert!(matches!(err, Err(AudioError::Unsupported(_))));
464 }
465
466 #[test]
467 fn opus_rejects_misaligned_input() {
468 let mut enc = Encoder::new(EncoderInput::default(), EncoderOutput::default()).unwrap();
469 assert!(matches!(
470 enc.encode_f32(&[0.0f32; 100]),
471 Err(AudioError::Misaligned { .. })
472 ));
473 }
474
475 #[test]
476 fn opus_catalog_includes_opushead() {
477 let enc = Encoder::new(
478 EncoderInput {
479 sample_rate: 48_000,
480 channels: 2,
481 ..EncoderInput::default()
482 },
483 EncoderOutput {
484 bitrate: Some(64_000),
485 ..EncoderOutput::default()
486 },
487 )
488 .unwrap();
489 let cfg = enc.catalog();
490 assert_eq!(cfg.sample_rate, 48_000);
491 assert_eq!(cfg.channel_count, 2);
492 assert_eq!(cfg.bitrate, Some(64_000));
493 let desc = cfg.description.expect("OpusHead should be present");
494 assert_eq!(desc.len(), 19);
495 }
496
497 #[test]
498 fn rate_picker_snaps_up() {
499 assert_eq!(pick_opus_rate(44_100), 48_000);
500 assert_eq!(pick_opus_rate(22_050), 24_000);
501 for &r in &[8_000, 12_000, 16_000, 24_000, 48_000] {
502 assert_eq!(pick_opus_rate(r), r);
503 }
504 }
505
506 #[test]
507 fn codec_roundtrips_as_str() {
508 assert_eq!(Codec::Opus.as_str(), "opus");
509 assert_eq!(Codec::Opus.to_string(), "opus");
510 assert_eq!("opus".parse::<Codec>().unwrap(), Codec::Opus);
511 assert!("aac".parse::<Codec>().is_err());
512 }
513
514 #[test]
515 fn encoder_output_overrides_codec_rate() {
516 let enc = Encoder::new(
517 EncoderInput {
518 sample_rate: 48_000,
519 channels: 1,
520 ..EncoderInput::default()
521 },
522 EncoderOutput {
523 sample_rate: Some(24_000),
524 ..EncoderOutput::default()
525 },
526 )
527 .unwrap();
528 assert_eq!(enc.codec_rate(), 24_000);
529 assert_eq!(enc.catalog().sample_rate, 24_000);
530 }
531}