1use audiopus::Application;
44use audiopus::Bitrate;
45use audiopus::Channels as OpusChannels;
46use audiopus::SampleRate;
47use audiopus::coder::Encoder as OpusEncoderInner;
48use audiopus::ffi;
49
50use std::ffi::c_int;
51use std::ptr;
52
53use crate::audio::resample::AudioResampler;
54use crate::audio::{
55 AudioCodec, AudioEncoder, AudioEncoderConfig, AudioError, AudioFrame, EncodedAudioPacket,
56};
57
58const OPUS_FRAME_SAMPLES_48K: usize = 960;
61const OPUS_INTERNAL_RATE: u32 = 48_000;
65const OPUS_MAX_PACKET_BYTES: usize = 4000;
72const OPUS_MAX_MS_PACKET_BYTES: usize = 16_384;
73const DEFAULT_BITRATE_MONO: u32 = 64_000;
75const DEFAULT_BITRATE_STEREO: u32 = 96_000;
76
77fn surround_mapping_family_1(channels: u8) -> Result<(u8, u8, &'static [u8]), AudioError> {
87 match channels {
110 3 => Ok((2, 1, &[0, 2, 1])),
111 4 => Ok((2, 2, &[0, 1, 2, 3])),
112 5 => Ok((3, 2, &[0, 4, 1, 2, 3])),
113 6 => Ok((4, 2, &[0, 4, 1, 2, 3, 5])),
114 7 => Ok((4, 3, &[0, 4, 1, 2, 3, 5, 6])),
115 8 => Ok((5, 3, &[0, 6, 1, 2, 3, 4, 5, 7])),
116 _ => Err(AudioError::Unsupported(format!(
117 "Opus surround mapping family 1 only defined for 3..=8 channels; got {channels}"
118 ))),
119 }
120}
121
122enum OpusInner {
126 Regular(OpusEncoderInner),
127 Multistream(MultistreamEncoder),
130}
131
132struct MultistreamEncoder {
137 state: *mut ffi::OpusMSEncoder,
138}
139
140unsafe impl Send for MultistreamEncoder {}
145
146impl MultistreamEncoder {
147 fn new(
151 sample_rate: u32,
152 channels: u8,
153 streams: u8,
154 coupled_streams: u8,
155 mapping: &[u8],
156 application: Application,
157 ) -> Result<Self, AudioError> {
158 if mapping.len() != channels as usize {
159 return Err(AudioError::Encode(format!(
160 "multistream mapping length {} != channels {channels}",
161 mapping.len()
162 )));
163 }
164 if coupled_streams > streams {
169 return Err(AudioError::Encode(format!(
170 "coupled_streams ({coupled_streams}) > streams ({streams})"
171 )));
172 }
173 if (streams as usize) + (coupled_streams as usize) > channels as usize {
174 return Err(AudioError::Encode(format!(
175 "streams ({streams}) + coupled_streams ({coupled_streams}) > channels ({channels})"
176 )));
177 }
178
179 let mut err: c_int = 0;
180 let state = unsafe {
186 ffi::opus_multistream_encoder_create(
187 sample_rate as i32,
188 channels as c_int,
189 streams as c_int,
190 coupled_streams as c_int,
191 mapping.as_ptr(),
192 application as c_int,
193 &mut err,
194 )
195 };
196 if state.is_null() || err != ffi::OPUS_OK {
197 return Err(AudioError::Encode(format!(
198 "opus_multistream_encoder_create failed: code={err}"
199 )));
200 }
201 Ok(Self { state })
202 }
203
204 fn set_vbr(&mut self, vbr: bool) -> Result<(), AudioError> {
207 let val: c_int = if vbr { 1 } else { 0 };
208 let r = unsafe {
212 ffi::opus_multistream_encoder_ctl(self.state, ffi::OPUS_SET_VBR_REQUEST, val)
213 };
214 if r != ffi::OPUS_OK {
215 return Err(AudioError::Encode(format!(
216 "opus_multistream_encoder_ctl(SET_VBR) failed: {r}"
217 )));
218 }
219 Ok(())
220 }
221
222 fn set_bitrate(&mut self, bps: i32) -> Result<(), AudioError> {
226 let r = unsafe {
228 ffi::opus_multistream_encoder_ctl(self.state, ffi::OPUS_SET_BITRATE_REQUEST, bps)
229 };
230 if r != ffi::OPUS_OK {
231 return Err(AudioError::Encode(format!(
232 "opus_multistream_encoder_ctl(SET_BITRATE) failed: {r}"
233 )));
234 }
235 Ok(())
236 }
237
238 fn lookahead(&self) -> Result<u32, AudioError> {
246 let mut out: c_int = 0;
247 let r = unsafe {
249 ffi::opus_multistream_encoder_ctl(
250 self.state,
251 ffi::OPUS_GET_LOOKAHEAD_REQUEST,
252 &mut out as *mut c_int,
253 )
254 };
255 if r != ffi::OPUS_OK {
256 return Err(AudioError::Encode(format!(
257 "opus_multistream_encoder_ctl(GET_LOOKAHEAD) failed: {r}"
258 )));
259 }
260 if out < 0 {
261 return Err(AudioError::Encode(format!(
262 "opus_multistream_encoder_ctl(GET_LOOKAHEAD) returned negative: {out}"
263 )));
264 }
265 Ok(out as u32)
266 }
267
268 fn encode_float(
272 &mut self,
273 pcm: &[f32],
274 frame_size: usize,
275 out: &mut [u8],
276 ) -> Result<usize, AudioError> {
277 let max = out.len().min(i32::MAX as usize) as i32;
278 let n = unsafe {
283 ffi::opus_multistream_encode_float(
284 self.state,
285 pcm.as_ptr(),
286 frame_size as c_int,
287 out.as_mut_ptr(),
288 max,
289 )
290 };
291 if n < 0 {
292 return Err(AudioError::Encode(format!(
293 "opus_multistream_encode_float failed: code={n}"
294 )));
295 }
296 Ok(n as usize)
297 }
298}
299
300impl Drop for MultistreamEncoder {
301 fn drop(&mut self) {
302 if !self.state.is_null() {
303 unsafe { ffi::opus_multistream_encoder_destroy(self.state) };
306 self.state = ptr::null_mut();
307 }
308 }
309}
310
311pub struct OpusEncoder {
312 inner: OpusInner,
313 in_rate: u32,
315 channels: u8,
317 resampler: Option<AudioResampler>,
319 sample_carry: Vec<f32>,
322 pre_skip_48k: u16,
324 extra_data: Vec<u8>,
328 next_pts_us: Option<i64>,
330 frame_duration_us: i64,
332 encode_out: Vec<u8>,
334}
335
336impl OpusEncoder {
337 pub fn new(config: AudioEncoderConfig) -> Result<Self, AudioError> {
338 if config.codec != AudioCodec::Opus {
339 return Err(AudioError::Encode(format!(
340 "OpusEncoder constructed with codec {:?}",
341 config.codec
342 )));
343 }
344 if config.channels == 0 {
345 return Err(AudioError::Unsupported(
346 "Opus channel count must be >= 1".to_string(),
347 ));
348 }
349 if config.channels > 8 {
350 return Err(AudioError::Unsupported(format!(
351 "Opus supports up to 8 channels (channel-mapping family 1, RFC 7845 §5.1.1.2); \
352 got {} channels",
353 config.channels
354 )));
355 }
356 if config.sample_rate == 0 {
357 return Err(AudioError::Encode("input sample_rate is 0".to_string()));
358 }
359
360 let channels = config.channels;
361
362 let (inner, ms_meta, max_packet_bytes) = if channels <= 2 {
366 let opus_channels = match channels {
368 1 => OpusChannels::Mono,
369 2 => OpusChannels::Stereo,
370 _ => unreachable!("channel-count guarded above"),
371 };
372 let mut enc =
373 OpusEncoderInner::new(SampleRate::Hz48000, opus_channels, Application::Audio)
374 .map_err(|e| AudioError::Encode(format!("opus encoder create: {e}")))?;
375 let bitrate_bps = if config.bitrate == 0 {
376 if channels == 1 {
377 DEFAULT_BITRATE_MONO
378 } else {
379 DEFAULT_BITRATE_STEREO
380 }
381 } else {
382 config.bitrate
383 };
384 enc.set_bitrate(Bitrate::BitsPerSecond(bitrate_bps as i32))
385 .map_err(|e| AudioError::Encode(format!("opus set_bitrate: {e}")))?;
386 enc.set_vbr(true)
390 .map_err(|e| AudioError::Encode(format!("opus set_vbr: {e}")))?;
391 (OpusInner::Regular(enc), None, OPUS_MAX_PACKET_BYTES)
392 } else {
393 let (streams, coupled, mapping) = surround_mapping_family_1(channels)?;
396 let mut ms = MultistreamEncoder::new(
397 OPUS_INTERNAL_RATE,
398 channels,
399 streams,
400 coupled,
401 mapping,
402 Application::Audio,
403 )?;
404 let bitrate_bps = if config.bitrate == 0 {
409 let coupled_u = coupled as u32;
410 let mono_u = streams as u32 - coupled_u;
411 coupled_u * DEFAULT_BITRATE_STEREO + mono_u * DEFAULT_BITRATE_MONO
412 } else {
413 config.bitrate
414 };
415 ms.set_bitrate(bitrate_bps as i32)?;
416 ms.set_vbr(true)?;
417 (
418 OpusInner::Multistream(ms),
419 Some((streams, coupled, mapping)),
420 OPUS_MAX_MS_PACKET_BYTES,
421 )
422 };
423
424 let pre_skip_48k_u32 = match &inner {
429 OpusInner::Regular(enc) => enc
430 .lookahead()
431 .map_err(|e| AudioError::Encode(format!("opus lookahead: {e}")))?,
432 OpusInner::Multistream(ms) => ms.lookahead()?,
433 };
434 let pre_skip_48k: u16 = pre_skip_48k_u32.try_into().unwrap_or(u16::MAX);
435
436 let resampler = if config.sample_rate == OPUS_INTERNAL_RATE {
438 None
439 } else {
440 let chunk = ((config.sample_rate as usize) * 20) / 1000;
445 let chunk = chunk.max(1);
446 Some(AudioResampler::new(
447 config.sample_rate,
448 OPUS_INTERNAL_RATE,
449 channels,
450 chunk,
451 )?)
452 };
453
454 let extra_data = build_dops(channels, pre_skip_48k, config.sample_rate, ms_meta);
455
456 let frame_duration_us =
457 (OPUS_FRAME_SAMPLES_48K as i64 * 1_000_000) / OPUS_INTERNAL_RATE as i64;
458
459 Ok(Self {
460 inner,
461 in_rate: config.sample_rate,
462 channels,
463 resampler,
464 sample_carry: Vec::with_capacity(OPUS_FRAME_SAMPLES_48K * channels as usize * 4),
465 pre_skip_48k,
466 extra_data,
467 next_pts_us: None,
468 frame_duration_us,
469 encode_out: vec![0u8; max_packet_bytes],
470 })
471 }
472
473 fn drain_packets(&mut self) -> Result<Vec<EncodedAudioPacket>, AudioError> {
477 let mut out = Vec::new();
478 let chans = self.channels as usize;
479 let frame_interleaved_len = OPUS_FRAME_SAMPLES_48K * chans;
480 while self.sample_carry.len() >= frame_interleaved_len {
481 let frame_slice = &self.sample_carry[..frame_interleaved_len];
483 let n = match &mut self.inner {
484 OpusInner::Regular(enc) => enc
485 .encode_float(frame_slice, &mut self.encode_out)
486 .map_err(|e| AudioError::Encode(format!("opus encode_float: {e}")))?,
487 OpusInner::Multistream(ms) => {
488 ms.encode_float(frame_slice, OPUS_FRAME_SAMPLES_48K, &mut self.encode_out)?
489 }
490 };
491 if n > 0 {
495 let pts = self.next_pts_us.unwrap_or(0);
496 self.next_pts_us = Some(pts + self.frame_duration_us);
497 out.push(EncodedAudioPacket {
498 data: self.encode_out[..n].to_vec(),
499 pts,
500 duration: OPUS_FRAME_SAMPLES_48K as i64, });
502 }
503 self.sample_carry.drain(..frame_interleaved_len);
504 }
505 Ok(out)
506 }
507}
508
509impl AudioEncoder for OpusEncoder {
510 fn encode(&mut self, frame: &AudioFrame) -> Result<Vec<EncodedAudioPacket>, AudioError> {
511 if frame.channels == 0 || frame.channels > 8 {
514 return Err(AudioError::Unsupported(format!(
515 "Opus AudioFrame channel count must be 1..=8; got {}",
516 frame.channels
517 )));
518 }
519 if frame.channels != self.channels {
520 return Err(AudioError::Encode(format!(
521 "channel count mismatch: encoder configured for {}, frame has {}",
522 self.channels, frame.channels
523 )));
524 }
525 if frame.sample_rate != self.in_rate {
526 return Err(AudioError::Encode(format!(
527 "sample rate mismatch: encoder configured for {}, frame has {}",
528 self.in_rate, frame.sample_rate
529 )));
530 }
531
532 if self.next_pts_us.is_none() {
533 self.next_pts_us = Some(frame.pts);
534 }
535
536 if let Some(r) = self.resampler.as_mut() {
538 r.process(frame, &mut self.sample_carry)?;
539 } else {
540 self.sample_carry.extend_from_slice(&frame.samples);
541 }
542
543 self.drain_packets()
544 }
545
546 fn flush(&mut self) -> Result<Vec<EncodedAudioPacket>, AudioError> {
547 if let Some(r) = self.resampler.as_mut() {
548 r.flush(&mut self.sample_carry)?;
549 }
550 let chans = self.channels as usize;
554 let frame_interleaved_len = OPUS_FRAME_SAMPLES_48K * chans;
555 if !self.sample_carry.is_empty() && self.sample_carry.len() < frame_interleaved_len {
556 self.sample_carry.resize(frame_interleaved_len, 0.0);
557 }
558 self.drain_packets()
559 }
560
561 fn pre_skip(&self) -> u16 {
562 self.pre_skip_48k
563 }
564
565 fn extra_data(&self) -> Vec<u8> {
566 self.extra_data.clone()
567 }
568}
569
570fn build_dops(
596 channels: u8,
597 pre_skip_48k: u16,
598 input_sample_rate: u32,
599 ms_meta: Option<(u8, u8, &[u8])>,
600) -> Vec<u8> {
601 let (family, total_len) = match ms_meta {
603 None => (0u8, 11usize),
604 Some(_) => (1u8, 11 + 2 + channels as usize),
605 };
606
607 let mut v = Vec::with_capacity(total_len);
608 v.push(0u8); v.push(channels);
610 v.extend_from_slice(&pre_skip_48k.to_le_bytes());
611 v.extend_from_slice(&input_sample_rate.to_le_bytes());
612 v.extend_from_slice(&0i16.to_le_bytes()); v.push(family);
614
615 if let Some((streams, coupled, mapping)) = ms_meta {
616 v.push(streams);
617 v.push(coupled);
618 v.extend_from_slice(mapping);
621 debug_assert_eq!(mapping.len(), channels as usize);
622 }
623 debug_assert_eq!(v.len(), total_len);
624 v
625}
626
627#[cfg(test)]
628mod tests {
629 use super::*;
630 use audiopus::Channels as OpusChannels;
631 use audiopus::SampleRate;
632 use audiopus::coder::Decoder as OpusDecoderInner;
633
634 fn config_stereo_48k() -> AudioEncoderConfig {
635 AudioEncoderConfig {
636 codec: AudioCodec::Opus,
637 sample_rate: 48_000,
638 channels: 2,
639 bitrate: 96_000,
640 }
641 }
642
643 fn config_mono_48k() -> AudioEncoderConfig {
644 AudioEncoderConfig {
645 codec: AudioCodec::Opus,
646 sample_rate: 48_000,
647 channels: 1,
648 bitrate: 64_000,
649 }
650 }
651
652 fn config_multi_48k(channels: u8) -> AudioEncoderConfig {
653 AudioEncoderConfig {
654 codec: AudioCodec::Opus,
655 sample_rate: 48_000,
656 channels,
657 bitrate: 0, }
659 }
660
661 #[test]
662 fn opus_encoder_constructs_for_mono_48k_with_1_channel_dops() {
663 let enc = OpusEncoder::new(config_mono_48k()).expect("constructs");
664 assert_eq!(enc.channels, 1);
665 assert!(enc.resampler.is_none());
666 assert_eq!(enc.extra_data[1], 1);
668 }
669
670 #[test]
671 fn opus_encoder_uses_default_bitrate_when_caller_passes_zero() {
672 let mut cfg = config_stereo_48k();
673 cfg.bitrate = 0;
674 let _enc = OpusEncoder::new(cfg).expect("constructs with bitrate=0");
675 }
681
682 fn config_stereo_44100() -> AudioEncoderConfig {
683 AudioEncoderConfig {
684 codec: AudioCodec::Opus,
685 sample_rate: 44_100,
686 channels: 2,
687 bitrate: 96_000,
688 }
689 }
690
691 fn make_silence(channels: u8, frames: usize, sample_rate: u32) -> AudioFrame {
692 AudioFrame {
693 samples: vec![0.0f32; frames * channels as usize],
694 sample_rate,
695 channels,
696 pts: 0,
697 }
698 }
699
700 fn make_sine_1k(channels: u8, frames: usize, sample_rate: u32, amp: f32) -> AudioFrame {
701 let mut samples = Vec::with_capacity(frames * channels as usize);
702 let two_pi = std::f32::consts::PI * 2.0;
703 let freq = 1000.0f32;
704 for i in 0..frames {
705 let t = i as f32 / sample_rate as f32;
706 let v = (two_pi * freq * t).sin() * amp;
707 for _ in 0..channels {
708 samples.push(v);
709 }
710 }
711 AudioFrame {
712 samples,
713 sample_rate,
714 channels,
715 pts: 0,
716 }
717 }
718
719 #[test]
720 fn opus_encoder_constructs_for_stereo_48k() {
721 let enc = OpusEncoder::new(config_stereo_48k()).expect("constructs");
722 assert_eq!(enc.channels, 2);
723 assert_eq!(enc.in_rate, 48000);
724 assert!(enc.resampler.is_none(), "no resampler at native rate");
725 assert_eq!(enc.extra_data.len(), 11, "dOps body must be 11 bytes");
726 assert_eq!(enc.extra_data[0], 0);
728 assert_eq!(enc.extra_data[1], 2);
730 assert_eq!(enc.extra_data[10], 0);
732 }
733
734 #[test]
735 fn opus_encoder_resamples_44100_to_48k_internally() {
736 let enc = OpusEncoder::new(config_stereo_44100()).expect("constructs");
737 assert!(enc.resampler.is_some(), "resampler engaged at 44.1k input");
738 let r = enc.resampler.as_ref().unwrap();
739 assert_eq!(r.in_rate(), 44100);
740 assert_eq!(r.out_rate(), 48000);
741 }
742
743 #[test]
744 fn opus_encoder_rejects_zero_channels() {
745 let mut bad = config_stereo_48k();
746 bad.channels = 0;
747 assert!(matches!(
748 OpusEncoder::new(bad),
749 Err(AudioError::Unsupported(_))
750 ));
751 }
752
753 #[test]
754 fn opus_encoder_rejects_nine_channels() {
755 let mut bad9 = config_stereo_48k();
758 bad9.channels = 9;
759 assert!(matches!(
760 OpusEncoder::new(bad9),
761 Err(AudioError::Unsupported(_))
762 ));
763 }
764
765 #[test]
766 fn opus_encoder_rejects_nine_channel_frame_at_runtime() {
767 let mut enc = OpusEncoder::new(config_stereo_48k()).expect("constructs");
768 let bad_frame = AudioFrame {
769 samples: vec![0.0; 960 * 9],
770 sample_rate: 48000,
771 channels: 9,
772 pts: 0,
773 };
774 let r = enc.encode(&bad_frame);
775 assert!(
776 matches!(r, Err(AudioError::Unsupported(_))),
777 "9-channel frame should be Unsupported, got {:?}",
778 r
779 );
780 }
781
782 #[test]
783 fn opus_pre_skip_in_48khz_ticks_is_nonzero() {
784 let enc = OpusEncoder::new(config_stereo_48k()).expect("constructs");
785 assert!(
788 enc.pre_skip() > 0,
789 "Opus encoder lookahead should be positive (libopus convention)"
790 );
791 assert!(
792 enc.pre_skip() < 2000,
793 "lookahead is bounded — typically <600 samples at 48 kHz"
794 );
795 }
796
797 #[test]
798 fn opus_dops_carries_correct_pre_skip_and_input_sample_rate_le() {
799 let enc = OpusEncoder::new(config_stereo_44100()).expect("constructs");
800 let d = enc.extra_data();
801 let ps = u16::from_le_bytes([d[2], d[3]]);
803 assert_eq!(ps, enc.pre_skip(), "dOps PreSkip matches encoder lookahead");
804 let isr = u32::from_le_bytes([d[4], d[5], d[6], d[7]]);
806 assert_eq!(
807 isr, 44100,
808 "dOps InputSampleRate is the source rate, not 48k"
809 );
810 let og = i16::from_le_bytes([d[8], d[9]]);
812 assert_eq!(og, 0);
813 }
814
815 #[test]
816 fn opus_encode_20ms_silence_produces_one_packet() {
817 let mut enc = OpusEncoder::new(config_stereo_48k()).expect("constructs");
818 let frame = make_silence(2, 960, 48_000);
820 let pkts = enc.encode(&frame).expect("encode");
821 assert_eq!(pkts.len(), 1, "exactly one Opus packet for one 20ms frame");
822 let pkt = &pkts[0];
823 assert!(!pkt.data.is_empty(), "packet should have bytes");
824 assert!(
827 pkt.data.len() < 200,
828 "silence packet at 96 kbps should be small, got {} bytes",
829 pkt.data.len()
830 );
831 assert_eq!(pkt.duration, 960, "20ms = 960 ticks at 48k");
832 }
833
834 #[test]
835 fn opus_encode_one_second_of_sine_produces_packets_with_reasonable_bitrate() {
836 let mut enc = OpusEncoder::new(config_stereo_48k()).expect("constructs");
837 let mut total_bytes = 0usize;
840 let mut total_packets = 0usize;
841 for i in 0..50 {
842 let mut frame = make_sine_1k(2, 960, 48_000, 0.3);
843 frame.pts = i * 20_000;
848 let pkts = enc.encode(&frame).expect("encode");
849 for p in &pkts {
850 total_bytes += p.data.len();
851 total_packets += 1;
852 }
853 }
854 let pkts_flush = enc.flush().expect("flush");
855 for p in &pkts_flush {
856 total_bytes += p.data.len();
857 total_packets += 1;
858 }
859 assert!(
861 total_packets >= 49 && total_packets <= 51,
862 "expected ~50 packets for 1 s of audio, got {total_packets}"
863 );
864 let observed_bps = (total_bytes as u64 * 8) as i64;
867 assert!(
868 observed_bps > 30_000 && observed_bps < 200_000,
869 "1s of 1kHz sine at 96 kbps should yield 30-200 kbps actual, got {observed_bps} bps ({total_bytes} bytes)"
870 );
871 }
872
873 #[test]
874 fn opus_pts_steps_by_20ms_per_packet() {
875 let mut enc = OpusEncoder::new(config_stereo_48k()).expect("constructs");
876 let frame_a = make_silence(2, 960, 48_000);
877 let mut frame_b = make_silence(2, 960, 48_000);
878 frame_b.pts = 20_000;
879 let pkts_a = enc.encode(&frame_a).expect("a");
880 let pkts_b = enc.encode(&frame_b).expect("b");
881 assert_eq!(pkts_a.len(), 1);
882 assert_eq!(pkts_b.len(), 1);
883 let dt = pkts_b[0].pts - pkts_a[0].pts;
884 assert_eq!(
886 dt, 20_000,
887 "PTS should step by 20_000 us per Opus packet (20 ms frame)"
888 );
889 }
890
891 #[test]
896 fn opus_round_trip_sine_wave_quality_is_acceptable() {
897 let mut enc = OpusEncoder::new(config_stereo_48k()).expect("constructs");
898 let frames_per_chunk = 960;
899 let n_chunks = 25; let total_frames = frames_per_chunk * n_chunks;
901
902 let mut all_samples = Vec::with_capacity(total_frames * 2);
904 let two_pi = std::f32::consts::PI * 2.0;
905 let freq = 1000.0f32;
906 for i in 0..total_frames {
907 let t = i as f32 / 48_000.0;
908 let v = (two_pi * freq * t).sin() * 0.5;
909 all_samples.push(v);
910 all_samples.push(v);
911 }
912
913 let mut packets = Vec::new();
915 for c in 0..n_chunks {
916 let chunk_samples =
917 all_samples[c * frames_per_chunk * 2..(c + 1) * frames_per_chunk * 2].to_vec();
918 let frame = AudioFrame {
919 samples: chunk_samples,
920 sample_rate: 48_000,
921 channels: 2,
922 pts: (c as i64) * 20_000,
923 };
924 packets.extend(enc.encode(&frame).expect("encode"));
925 }
926 packets.extend(enc.flush().expect("flush"));
927 assert!(!packets.is_empty(), "encode must produce packets");
928
929 let mut dec =
931 OpusDecoderInner::new(SampleRate::Hz48000, OpusChannels::Stereo).expect("dec");
932 let mut decoded = Vec::with_capacity(total_frames * 2);
933 let mut tmp = vec![0.0f32; frames_per_chunk * 2];
934 for p in &packets {
935 let pkt = audiopus::packet::Packet::try_from(p.data.as_slice()).expect("pkt");
936 let sig = audiopus::MutSignals::try_from(tmp.as_mut_slice()).expect("sig");
937 let n = dec
938 .decode_float(Some(pkt), sig, false)
939 .expect("decode_float");
940 decoded.extend_from_slice(&tmp[..n * 2]);
941 }
942 assert!(
943 decoded.len() >= (total_frames - 100) * 2,
944 "decoded length {} should approximate input length {}",
945 decoded.len(),
946 total_frames * 2
947 );
948
949 let pre_skip = enc.pre_skip() as usize;
954 let cmp_start = pre_skip + 480; let cmp_end = (decoded.len() / 2).min(total_frames - 100);
956 if cmp_end <= cmp_start {
957 panic!(
958 "round trip too short: cmp_start={cmp_start}, cmp_end={cmp_end}, decoded len/2={}",
959 decoded.len() / 2
960 );
961 }
962
963 let mut sum_sq_err = 0.0f64;
964 let mut sum_sq_sig = 0.0f64;
965 let mut n = 0usize;
966 for i in cmp_start..cmp_end {
967 let in_idx = i - pre_skip;
973 let l_in = all_samples[in_idx * 2];
974 let r_in = all_samples[in_idx * 2 + 1];
975 let l_out = decoded[i * 2];
976 let r_out = decoded[i * 2 + 1];
977 sum_sq_err += ((l_in - l_out) as f64).powi(2);
978 sum_sq_err += ((r_in - r_out) as f64).powi(2);
979 sum_sq_sig += (l_in as f64).powi(2);
980 sum_sq_sig += (r_in as f64).powi(2);
981 n += 2;
982 }
983 let rms_err = (sum_sq_err / n as f64).sqrt();
984 let rms_sig = (sum_sq_sig / n as f64).sqrt();
985 let snr_db = 20.0 * (rms_sig / rms_err.max(1e-12)).log10();
986 assert!(
991 snr_db > 15.0,
992 "round-trip SNR {snr_db:.2} dB too low — Opus quality regression?"
993 );
994 println!("opus_round_trip SNR = {snr_db:.2} dB, rms_err = {rms_err:.4}");
997 }
998
999 #[test]
1000 fn dops_layout_matches_rfc_7845_for_mono_and_stereo() {
1001 let d_mono = build_dops(1, 312, 48_000, None);
1002 assert_eq!(d_mono.len(), 11);
1003 assert_eq!(d_mono[0], 0); assert_eq!(d_mono[1], 1); assert_eq!(u16::from_le_bytes([d_mono[2], d_mono[3]]), 312); assert_eq!(
1007 u32::from_le_bytes([d_mono[4], d_mono[5], d_mono[6], d_mono[7]]),
1008 48000
1009 ); assert_eq!(i16::from_le_bytes([d_mono[8], d_mono[9]]), 0); assert_eq!(d_mono[10], 0); let d_stereo = build_dops(2, 400, 44_100, None);
1014 assert_eq!(d_stereo.len(), 11);
1015 assert_eq!(d_stereo[1], 2);
1016 assert_eq!(u16::from_le_bytes([d_stereo[2], d_stereo[3]]), 400);
1017 assert_eq!(
1018 u32::from_le_bytes([d_stereo[4], d_stereo[5], d_stereo[6], d_stereo[7]]),
1019 44100
1020 );
1021 }
1022
1023 #[test]
1029 fn surround_mapping_family_1_matches_rfc_7845_5_1_1_2() {
1030 assert_eq!(
1032 surround_mapping_family_1(3).unwrap(),
1033 (2, 1, &[0, 2, 1][..])
1034 );
1035 assert_eq!(
1037 surround_mapping_family_1(4).unwrap(),
1038 (2, 2, &[0, 1, 2, 3][..])
1039 );
1040 assert_eq!(
1042 surround_mapping_family_1(5).unwrap(),
1043 (3, 2, &[0, 4, 1, 2, 3][..])
1044 );
1045 assert_eq!(
1047 surround_mapping_family_1(6).unwrap(),
1048 (4, 2, &[0, 4, 1, 2, 3, 5][..])
1049 );
1050 assert_eq!(
1054 surround_mapping_family_1(7).unwrap(),
1055 (4, 3, &[0, 4, 1, 2, 3, 5, 6][..])
1056 );
1057 assert_eq!(
1059 surround_mapping_family_1(8).unwrap(),
1060 (5, 3, &[0, 6, 1, 2, 3, 4, 5, 7][..])
1061 );
1062 assert!(surround_mapping_family_1(0).is_err());
1064 assert!(surround_mapping_family_1(1).is_err()); assert!(surround_mapping_family_1(2).is_err());
1066 assert!(surround_mapping_family_1(9).is_err());
1067 }
1068
1069 #[test]
1070 fn opus_encoder_constructs_for_3_0_through_7_1_with_family_1_dops() {
1071 for &ch in &[3u8, 4, 5, 6, 7, 8] {
1075 let enc = OpusEncoder::new(config_multi_48k(ch))
1076 .unwrap_or_else(|e| panic!("constructs for {ch}ch: {e:?}"));
1077 assert_eq!(enc.channels, ch);
1078 assert!(enc.resampler.is_none(), "no resampler at native rate");
1079
1080 let d = enc.extra_data();
1081 let expected_len = 11 + 2 + ch as usize;
1082 assert_eq!(
1083 d.len(),
1084 expected_len,
1085 "dOps body for {ch}ch should be {expected_len} bytes (11 preamble + 2 stream/coupled + N mapping); got {}",
1086 d.len()
1087 );
1088 assert_eq!(
1089 d[0], 0,
1090 "Version=0 (dOps box version, not Opus stream version)"
1091 );
1092 assert_eq!(d[1], ch, "OutputChannelCount");
1093 assert_eq!(d[10], 1, "ChannelMappingFamily=1 for surround");
1094
1095 let (exp_streams, exp_coupled, exp_mapping) = surround_mapping_family_1(ch).unwrap();
1096 assert_eq!(d[11], exp_streams, "StreamCount for {ch}ch");
1097 assert_eq!(d[12], exp_coupled, "CoupledCount for {ch}ch");
1098 assert_eq!(
1099 &d[13..13 + ch as usize],
1100 exp_mapping,
1101 "ChannelMapping for {ch}ch"
1102 );
1103 }
1104 }
1105
1106 #[test]
1109 fn opus_encoder_dops_5_1_hex_layout() {
1110 let enc = OpusEncoder::new(config_multi_48k(6)).expect("5.1 constructs");
1111 let d = enc.extra_data();
1112 assert_eq!(d.len(), 19, "5.1 dOps body = 11 + 2 + 6 = 19 bytes");
1113 let hex: String = d.iter().map(|b| format!("{b:02x} ")).collect();
1114 println!(
1115 "5.1 dOps body hex (LE-encoded, 19 bytes): {}",
1116 hex.trim_end()
1117 );
1118 assert_eq!(d[0], 0); assert_eq!(d[1], 6); let ps = u16::from_le_bytes([d[2], d[3]]);
1123 assert!(ps > 0 && ps < 2000);
1124 assert_eq!(
1125 u32::from_le_bytes([d[4], d[5], d[6], d[7]]),
1126 48_000,
1127 "InputSampleRate=48000"
1128 );
1129 assert_eq!(i16::from_le_bytes([d[8], d[9]]), 0); assert_eq!(d[10], 1); assert_eq!(d[11], 4); assert_eq!(d[12], 2); assert_eq!(&d[13..19], &[0u8, 4, 1, 2, 3, 5][..]); }
1135
1136 #[test]
1137 fn opus_5_1_encode_20ms_silence_produces_one_packet() {
1138 let mut enc = OpusEncoder::new(config_multi_48k(6)).expect("5.1 constructs");
1139 let frame = make_silence(6, 960, 48_000);
1141 let pkts = enc.encode(&frame).expect("encode 5.1 silence");
1142 assert_eq!(pkts.len(), 1, "exactly one Opus packet for one 20ms frame");
1143 let pkt = &pkts[0];
1144 assert!(!pkt.data.is_empty());
1145 assert!(
1149 pkt.data.len() < 600,
1150 "5.1 silence packet should still be under ~600 bytes, got {} bytes",
1151 pkt.data.len()
1152 );
1153 assert_eq!(pkt.duration, 960);
1154 }
1155
1156 #[test]
1160 fn opus_5_1_round_trip_per_channel_snr_is_acceptable() {
1161 let freqs = [440.0f32, 523.25, 659.25, 80.0, 880.0, 987.77];
1165 let chans: u8 = 6;
1166 let frames_per_chunk = 960;
1167 let n_chunks = 30; let total_frames = frames_per_chunk * n_chunks;
1169 let amp = 0.4f32;
1170
1171 let mut all = vec![0.0f32; total_frames * chans as usize];
1173 let two_pi = std::f32::consts::PI * 2.0;
1174 for i in 0..total_frames {
1175 let t = i as f32 / 48_000.0;
1176 for ch in 0..chans as usize {
1177 all[i * chans as usize + ch] = (two_pi * freqs[ch] * t).sin() * amp;
1178 }
1179 }
1180
1181 let mut enc = OpusEncoder::new(config_multi_48k(chans)).expect("encoder");
1183 let mut packets = Vec::new();
1184 for c in 0..n_chunks {
1185 let frame = AudioFrame {
1186 samples: all[c * frames_per_chunk * chans as usize
1187 ..(c + 1) * frames_per_chunk * chans as usize]
1188 .to_vec(),
1189 sample_rate: 48_000,
1190 channels: chans,
1191 pts: (c as i64) * 20_000,
1192 };
1193 packets.extend(enc.encode(&frame).expect("encode"));
1194 }
1195 packets.extend(enc.flush().expect("flush"));
1196 assert!(!packets.is_empty(), "must produce packets");
1197
1198 let (streams, coupled, mapping) = surround_mapping_family_1(chans).unwrap();
1200 let mut err: c_int = 0;
1201 let dec_state = unsafe {
1202 ffi::opus_multistream_decoder_create(
1203 48_000,
1204 chans as c_int,
1205 streams as c_int,
1206 coupled as c_int,
1207 mapping.as_ptr(),
1208 &mut err,
1209 )
1210 };
1211 assert!(
1212 !dec_state.is_null() && err == ffi::OPUS_OK,
1213 "MS decoder create"
1214 );
1215
1216 let mut decoded = Vec::with_capacity(total_frames * chans as usize);
1217 let mut tmp = vec![0.0f32; frames_per_chunk * chans as usize];
1218 for p in &packets {
1219 let n = unsafe {
1220 ffi::opus_multistream_decode_float(
1221 dec_state,
1222 p.data.as_ptr(),
1223 p.data.len() as i32,
1224 tmp.as_mut_ptr(),
1225 frames_per_chunk as c_int,
1226 0,
1227 )
1228 };
1229 assert!(n > 0, "MS decode_float returned {n}");
1230 decoded.extend_from_slice(&tmp[..(n as usize) * chans as usize]);
1231 }
1232 unsafe { ffi::opus_multistream_decoder_destroy(dec_state) };
1233
1234 let pre_skip = enc.pre_skip() as usize;
1238 let cmp_start = pre_skip + 480;
1239 let cmp_end = (decoded.len() / chans as usize).min(total_frames - 200);
1240 assert!(cmp_end > cmp_start, "round trip too short");
1241
1242 let mut snrs = Vec::with_capacity(chans as usize);
1243 for ch in 0..chans as usize {
1244 let mut sum_sq_err = 0.0f64;
1245 let mut sum_sq_sig = 0.0f64;
1246 for i in cmp_start..cmp_end {
1247 let in_idx = i - pre_skip;
1248 let s_in = all[in_idx * chans as usize + ch];
1249 let s_out = decoded[i * chans as usize + ch];
1250 sum_sq_err += ((s_in - s_out) as f64).powi(2);
1251 sum_sq_sig += (s_in as f64).powi(2);
1252 }
1253 let n = (cmp_end - cmp_start) as f64;
1254 let rms_err = (sum_sq_err / n).sqrt();
1255 let rms_sig = (sum_sq_sig / n).sqrt();
1256 let snr_db = 20.0 * (rms_sig / rms_err.max(1e-12)).log10();
1257 snrs.push(snr_db);
1258 }
1259
1260 println!("5.1 per-channel SNR (dB):");
1261 for (i, snr) in snrs.iter().enumerate() {
1262 let label = ["FL", "FR", "C", "LFE", "BL", "BR"][i];
1263 println!(" ch{i} ({label}): {snr:.2} dB");
1264 }
1265
1266 for (i, snr) in snrs.iter().enumerate() {
1273 assert!(
1274 *snr > 5.0,
1275 "ch{i} SNR {snr:.2} dB too low — multistream quality regression?"
1276 );
1277 }
1278 }
1279
1280 #[test]
1281 fn dops_layout_for_5_1_matches_family_1_spec() {
1282 let (streams, coupled, mapping) = surround_mapping_family_1(6).unwrap();
1283 let d = build_dops(6, 312, 48_000, Some((streams, coupled, mapping)));
1284 assert_eq!(d.len(), 11 + 2 + 6, "5.1 dOps = 19 bytes");
1285 assert_eq!(d[0], 0); assert_eq!(d[1], 6); assert_eq!(u16::from_le_bytes([d[2], d[3]]), 312); assert_eq!(u32::from_le_bytes([d[4], d[5], d[6], d[7]]), 48_000); assert_eq!(i16::from_le_bytes([d[8], d[9]]), 0); assert_eq!(d[10], 1); assert_eq!(d[11], 4); assert_eq!(d[12], 2); assert_eq!(&d[13..19], &[0u8, 4, 1, 2, 3, 5][..]);
1294 }
1295
1296 #[test]
1299 fn opus_5_1_resamples_44100_to_48k() {
1300 let mut cfg = config_multi_48k(6);
1301 cfg.sample_rate = 44_100;
1302 let enc = OpusEncoder::new(cfg).expect("5.1 @ 44.1k constructs");
1303 assert!(enc.resampler.is_some(), "resampler engaged for 6ch @ 44.1k");
1304 let r = enc.resampler.as_ref().unwrap();
1305 assert_eq!(r.in_rate(), 44_100);
1306 assert_eq!(r.out_rate(), 48_000);
1307 assert_eq!(r.channels(), 6);
1308 }
1309}