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 {
296 sample_rate: self.codec_rate,
297 channel_count: self.codec_channels,
298 }
299 .encode()
300 .expect("opus encoder channels validated to mono/stereo");
301
302 let mut config =
303 hang::catalog::AudioConfig::new(hang::catalog::AudioCodec::Opus, self.codec_rate, self.codec_channels);
304 config.bitrate = self.output.bitrate.map(|b| b as u64);
305 config.description = Some(head);
306 config.container = hang::catalog::Container::Legacy;
307 config
308 }
309}
310
311pub struct Decoder {
313 inner: *mut OpusDecoder,
314 sample_rate: u32,
315 channel_count: u32,
316 max_frame_size: usize,
317}
318
319unsafe impl Send for Decoder {}
321
322impl Decoder {
323 pub fn new(catalog: &hang::catalog::AudioConfig) -> Result<Self, AudioError> {
328 let (sample_rate, channel_count) = if let Some(desc) = &catalog.description {
329 let mut buf = desc.as_ref();
330 match moq_mux::codec::opus::Config::parse(&mut buf) {
331 Ok(head) => (head.sample_rate, head.channel_count),
332 Err(_) => (catalog.sample_rate, catalog.channel_count),
333 }
334 } else {
335 (catalog.sample_rate, catalog.channel_count)
336 };
337
338 validate_opus_rate(sample_rate)?;
339 let channels = validate_opus_channels(channel_count)?;
340
341 let mut err = 0i32;
342 let inner = unsafe { opus_decoder_create(sample_rate as i32, channels, &mut err) };
344 if err != OPUS_OK || inner.is_null() {
345 return Err(opus_error(err, "opus_decoder_create"));
346 }
347
348 let max_frame_size = (sample_rate as usize * 120) / 1000;
350
351 Ok(Self {
352 inner,
353 sample_rate,
354 channel_count,
355 max_frame_size,
356 })
357 }
358
359 pub fn sample_rate(&self) -> u32 {
360 self.sample_rate
361 }
362
363 pub fn channel_count(&self) -> u32 {
364 self.channel_count
365 }
366
367 pub fn decode_f32(&mut self, packet: &[u8]) -> Result<Vec<f32>, AudioError> {
369 let mut out = vec![0.0f32; self.max_frame_size * self.channel_count as usize];
370 let samples = unsafe {
373 opus_decode_float(
374 &mut *self.inner,
375 packet.as_ptr(),
376 packet.len() as i32,
377 out.as_mut_ptr(),
378 self.max_frame_size as i32,
379 0,
380 )
381 };
382 if samples < 0 {
383 return Err(opus_error(samples, "opus_decode_float"));
384 }
385 out.truncate(samples as usize * self.channel_count as usize);
386 Ok(out)
387 }
388}
389
390impl Drop for Encoder {
391 fn drop(&mut self) {
392 unsafe { opus_encoder_destroy(self.inner) };
394 }
395}
396
397impl Drop for Decoder {
398 fn drop(&mut self) {
399 unsafe { opus_decoder_destroy(self.inner) };
401 }
402}
403
404#[cfg(test)]
405mod tests {
406 use super::*;
407
408 fn sine(freq: f32, sample_rate: u32, channels: u32, frames: usize) -> Vec<f32> {
409 let mut out = Vec::with_capacity(frames * channels as usize);
410 for i in 0..frames {
411 let t = i as f32 / sample_rate as f32;
412 let v = (2.0 * std::f32::consts::PI * freq * t).sin() * 0.5;
413 for _ in 0..channels {
414 out.push(v);
415 }
416 }
417 out
418 }
419
420 #[test]
421 fn opus_encode_then_decode_keeps_signal_close() {
422 let mut enc = Encoder::new(
423 EncoderInput {
424 format: AudioFormat::F32,
425 sample_rate: 48_000,
426 channels: 2,
427 },
428 EncoderOutput {
429 bitrate: Some(96_000),
430 ..EncoderOutput::default()
431 },
432 )
433 .unwrap();
434
435 let cfg = enc.catalog();
436 let mut dec = Decoder::new(&cfg).unwrap();
437
438 let frame = sine(440.0, 48_000, 2, enc.frame_size());
439 for _ in 0..5 {
440 let pkt = enc.encode_f32(&frame).unwrap();
441 let _ = dec.decode_f32(&pkt).unwrap();
442 }
443
444 let pkt = enc.encode_f32(&frame).unwrap();
445 let decoded = dec.decode_f32(&pkt).unwrap();
446 assert_eq!(decoded.len(), frame.len());
447
448 let energy_in: f32 = frame.iter().map(|s| s * s).sum();
449 let energy_out: f32 = decoded.iter().map(|s| s * s).sum();
450 let ratio = energy_out / energy_in;
451 assert!(
452 (0.5..2.0).contains(&ratio),
453 "output energy ratio {ratio:.3} should be close to 1"
454 );
455 }
456
457 #[test]
458 fn opus_rejects_unsupported_frame_duration() {
459 let err = Encoder::new(
460 EncoderInput::default(),
461 EncoderOutput {
462 frame_duration: Duration::from_millis(15),
463 ..EncoderOutput::default()
464 },
465 );
466 assert!(matches!(err, Err(AudioError::Unsupported(_))));
467 }
468
469 #[test]
470 fn opus_rejects_misaligned_input() {
471 let mut enc = Encoder::new(EncoderInput::default(), EncoderOutput::default()).unwrap();
472 assert!(matches!(
473 enc.encode_f32(&[0.0f32; 100]),
474 Err(AudioError::Misaligned { .. })
475 ));
476 }
477
478 #[test]
479 fn opus_catalog_includes_opushead() {
480 let enc = Encoder::new(
481 EncoderInput {
482 sample_rate: 48_000,
483 channels: 2,
484 ..EncoderInput::default()
485 },
486 EncoderOutput {
487 bitrate: Some(64_000),
488 ..EncoderOutput::default()
489 },
490 )
491 .unwrap();
492 let cfg = enc.catalog();
493 assert_eq!(cfg.sample_rate, 48_000);
494 assert_eq!(cfg.channel_count, 2);
495 assert_eq!(cfg.bitrate, Some(64_000));
496 let desc = cfg.description.expect("OpusHead should be present");
497 assert_eq!(desc.len(), 19);
498 }
499
500 #[test]
501 fn rate_picker_snaps_up() {
502 assert_eq!(pick_opus_rate(44_100), 48_000);
503 assert_eq!(pick_opus_rate(22_050), 24_000);
504 for &r in &[8_000, 12_000, 16_000, 24_000, 48_000] {
505 assert_eq!(pick_opus_rate(r), r);
506 }
507 }
508
509 #[test]
510 fn codec_roundtrips_as_str() {
511 assert_eq!(Codec::Opus.as_str(), "opus");
512 assert_eq!(Codec::Opus.to_string(), "opus");
513 assert_eq!("opus".parse::<Codec>().unwrap(), Codec::Opus);
514 assert!("aac".parse::<Codec>().is_err());
515 }
516
517 #[test]
518 fn encoder_output_overrides_codec_rate() {
519 let enc = Encoder::new(
520 EncoderInput {
521 sample_rate: 48_000,
522 channels: 1,
523 ..EncoderInput::default()
524 },
525 EncoderOutput {
526 sample_rate: Some(24_000),
527 ..EncoderOutput::default()
528 },
529 )
530 .unwrap();
531 assert_eq!(enc.codec_rate(), 24_000);
532 assert_eq!(enc.catalog().sample_rate, 24_000);
533 }
534}