use audiopus::Application;
use audiopus::Bitrate;
use audiopus::Channels as OpusChannels;
use audiopus::SampleRate;
use audiopus::coder::Encoder as OpusEncoderInner;
use audiopus::ffi;
use std::ffi::c_int;
use std::ptr;
use crate::audio::resample::AudioResampler;
use crate::audio::{
AudioCodec, AudioEncoder, AudioEncoderConfig, AudioError, AudioFrame, EncodedAudioPacket,
};
const OPUS_FRAME_SAMPLES_48K: usize = 960;
const OPUS_INTERNAL_RATE: u32 = 48_000;
const OPUS_MAX_PACKET_BYTES: usize = 4000;
const OPUS_MAX_MS_PACKET_BYTES: usize = 16_384;
const DEFAULT_BITRATE_MONO: u32 = 64_000;
const DEFAULT_BITRATE_STEREO: u32 = 96_000;
fn surround_mapping_family_1(channels: u8) -> Result<(u8, u8, &'static [u8]), AudioError> {
match channels {
3 => Ok((2, 1, &[0, 2, 1])),
4 => Ok((2, 2, &[0, 1, 2, 3])),
5 => Ok((3, 2, &[0, 4, 1, 2, 3])),
6 => Ok((4, 2, &[0, 4, 1, 2, 3, 5])),
7 => Ok((4, 3, &[0, 4, 1, 2, 3, 5, 6])),
8 => Ok((5, 3, &[0, 6, 1, 2, 3, 4, 5, 7])),
_ => Err(AudioError::Unsupported(format!(
"Opus surround mapping family 1 only defined for 3..=8 channels; got {channels}"
))),
}
}
enum OpusInner {
Regular(OpusEncoderInner),
Multistream(MultistreamEncoder),
}
struct MultistreamEncoder {
state: *mut ffi::OpusMSEncoder,
}
unsafe impl Send for MultistreamEncoder {}
impl MultistreamEncoder {
fn new(
sample_rate: u32,
channels: u8,
streams: u8,
coupled_streams: u8,
mapping: &[u8],
application: Application,
) -> Result<Self, AudioError> {
if mapping.len() != channels as usize {
return Err(AudioError::Encode(format!(
"multistream mapping length {} != channels {channels}",
mapping.len()
)));
}
if coupled_streams > streams {
return Err(AudioError::Encode(format!(
"coupled_streams ({coupled_streams}) > streams ({streams})"
)));
}
if (streams as usize) + (coupled_streams as usize) > channels as usize {
return Err(AudioError::Encode(format!(
"streams ({streams}) + coupled_streams ({coupled_streams}) > channels ({channels})"
)));
}
let mut err: c_int = 0;
let state = unsafe {
ffi::opus_multistream_encoder_create(
sample_rate as i32,
channels as c_int,
streams as c_int,
coupled_streams as c_int,
mapping.as_ptr(),
application as c_int,
&mut err,
)
};
if state.is_null() || err != ffi::OPUS_OK {
return Err(AudioError::Encode(format!(
"opus_multistream_encoder_create failed: code={err}"
)));
}
Ok(Self { state })
}
fn set_vbr(&mut self, vbr: bool) -> Result<(), AudioError> {
let val: c_int = if vbr { 1 } else { 0 };
let r = unsafe {
ffi::opus_multistream_encoder_ctl(self.state, ffi::OPUS_SET_VBR_REQUEST, val)
};
if r != ffi::OPUS_OK {
return Err(AudioError::Encode(format!(
"opus_multistream_encoder_ctl(SET_VBR) failed: {r}"
)));
}
Ok(())
}
fn set_bitrate(&mut self, bps: i32) -> Result<(), AudioError> {
let r = unsafe {
ffi::opus_multistream_encoder_ctl(self.state, ffi::OPUS_SET_BITRATE_REQUEST, bps)
};
if r != ffi::OPUS_OK {
return Err(AudioError::Encode(format!(
"opus_multistream_encoder_ctl(SET_BITRATE) failed: {r}"
)));
}
Ok(())
}
fn lookahead(&self) -> Result<u32, AudioError> {
let mut out: c_int = 0;
let r = unsafe {
ffi::opus_multistream_encoder_ctl(
self.state,
ffi::OPUS_GET_LOOKAHEAD_REQUEST,
&mut out as *mut c_int,
)
};
if r != ffi::OPUS_OK {
return Err(AudioError::Encode(format!(
"opus_multistream_encoder_ctl(GET_LOOKAHEAD) failed: {r}"
)));
}
if out < 0 {
return Err(AudioError::Encode(format!(
"opus_multistream_encoder_ctl(GET_LOOKAHEAD) returned negative: {out}"
)));
}
Ok(out as u32)
}
fn encode_float(
&mut self,
pcm: &[f32],
frame_size: usize,
out: &mut [u8],
) -> Result<usize, AudioError> {
let max = out.len().min(i32::MAX as usize) as i32;
let n = unsafe {
ffi::opus_multistream_encode_float(
self.state,
pcm.as_ptr(),
frame_size as c_int,
out.as_mut_ptr(),
max,
)
};
if n < 0 {
return Err(AudioError::Encode(format!(
"opus_multistream_encode_float failed: code={n}"
)));
}
Ok(n as usize)
}
}
impl Drop for MultistreamEncoder {
fn drop(&mut self) {
if !self.state.is_null() {
unsafe { ffi::opus_multistream_encoder_destroy(self.state) };
self.state = ptr::null_mut();
}
}
}
pub struct OpusEncoder {
inner: OpusInner,
in_rate: u32,
channels: u8,
resampler: Option<AudioResampler>,
sample_carry: Vec<f32>,
pre_skip_48k: u16,
extra_data: Vec<u8>,
next_pts_us: Option<i64>,
frame_duration_us: i64,
encode_out: Vec<u8>,
}
impl OpusEncoder {
pub fn new(config: AudioEncoderConfig) -> Result<Self, AudioError> {
if config.codec != AudioCodec::Opus {
return Err(AudioError::Encode(format!(
"OpusEncoder constructed with codec {:?}",
config.codec
)));
}
if config.channels == 0 {
return Err(AudioError::Unsupported(
"Opus channel count must be >= 1".to_string(),
));
}
if config.channels > 8 {
return Err(AudioError::Unsupported(format!(
"Opus supports up to 8 channels (channel-mapping family 1, RFC 7845 §5.1.1.2); \
got {} channels",
config.channels
)));
}
if config.sample_rate == 0 {
return Err(AudioError::Encode("input sample_rate is 0".to_string()));
}
let channels = config.channels;
let (inner, ms_meta, max_packet_bytes) = if channels <= 2 {
let opus_channels = match channels {
1 => OpusChannels::Mono,
2 => OpusChannels::Stereo,
_ => unreachable!("channel-count guarded above"),
};
let mut enc =
OpusEncoderInner::new(SampleRate::Hz48000, opus_channels, Application::Audio)
.map_err(|e| AudioError::Encode(format!("opus encoder create: {e}")))?;
let bitrate_bps = if config.bitrate == 0 {
if channels == 1 {
DEFAULT_BITRATE_MONO
} else {
DEFAULT_BITRATE_STEREO
}
} else {
config.bitrate
};
enc.set_bitrate(Bitrate::BitsPerSecond(bitrate_bps as i32))
.map_err(|e| AudioError::Encode(format!("opus set_bitrate: {e}")))?;
enc.set_vbr(true)
.map_err(|e| AudioError::Encode(format!("opus set_vbr: {e}")))?;
(OpusInner::Regular(enc), None, OPUS_MAX_PACKET_BYTES)
} else {
let (streams, coupled, mapping) = surround_mapping_family_1(channels)?;
let mut ms = MultistreamEncoder::new(
OPUS_INTERNAL_RATE,
channels,
streams,
coupled,
mapping,
Application::Audio,
)?;
let bitrate_bps = if config.bitrate == 0 {
let coupled_u = coupled as u32;
let mono_u = streams as u32 - coupled_u;
coupled_u * DEFAULT_BITRATE_STEREO + mono_u * DEFAULT_BITRATE_MONO
} else {
config.bitrate
};
ms.set_bitrate(bitrate_bps as i32)?;
ms.set_vbr(true)?;
(
OpusInner::Multistream(ms),
Some((streams, coupled, mapping)),
OPUS_MAX_MS_PACKET_BYTES,
)
};
let pre_skip_48k_u32 = match &inner {
OpusInner::Regular(enc) => enc
.lookahead()
.map_err(|e| AudioError::Encode(format!("opus lookahead: {e}")))?,
OpusInner::Multistream(ms) => ms.lookahead()?,
};
let pre_skip_48k: u16 = pre_skip_48k_u32.try_into().unwrap_or(u16::MAX);
let resampler = if config.sample_rate == OPUS_INTERNAL_RATE {
None
} else {
let chunk = ((config.sample_rate as usize) * 20) / 1000;
let chunk = chunk.max(1);
Some(AudioResampler::new(
config.sample_rate,
OPUS_INTERNAL_RATE,
channels,
chunk,
)?)
};
let extra_data = build_dops(channels, pre_skip_48k, config.sample_rate, ms_meta);
let frame_duration_us =
(OPUS_FRAME_SAMPLES_48K as i64 * 1_000_000) / OPUS_INTERNAL_RATE as i64;
Ok(Self {
inner,
in_rate: config.sample_rate,
channels,
resampler,
sample_carry: Vec::with_capacity(OPUS_FRAME_SAMPLES_48K * channels as usize * 4),
pre_skip_48k,
extra_data,
next_pts_us: None,
frame_duration_us,
encode_out: vec![0u8; max_packet_bytes],
})
}
fn drain_packets(&mut self) -> Result<Vec<EncodedAudioPacket>, AudioError> {
let mut out = Vec::new();
let chans = self.channels as usize;
let frame_interleaved_len = OPUS_FRAME_SAMPLES_48K * chans;
while self.sample_carry.len() >= frame_interleaved_len {
let frame_slice = &self.sample_carry[..frame_interleaved_len];
let n = match &mut self.inner {
OpusInner::Regular(enc) => enc
.encode_float(frame_slice, &mut self.encode_out)
.map_err(|e| AudioError::Encode(format!("opus encode_float: {e}")))?,
OpusInner::Multistream(ms) => {
ms.encode_float(frame_slice, OPUS_FRAME_SAMPLES_48K, &mut self.encode_out)?
}
};
if n > 0 {
let pts = self.next_pts_us.unwrap_or(0);
self.next_pts_us = Some(pts + self.frame_duration_us);
out.push(EncodedAudioPacket {
data: self.encode_out[..n].to_vec(),
pts,
duration: OPUS_FRAME_SAMPLES_48K as i64, });
}
self.sample_carry.drain(..frame_interleaved_len);
}
Ok(out)
}
}
impl AudioEncoder for OpusEncoder {
fn encode(&mut self, frame: &AudioFrame) -> Result<Vec<EncodedAudioPacket>, AudioError> {
if frame.channels == 0 || frame.channels > 8 {
return Err(AudioError::Unsupported(format!(
"Opus AudioFrame channel count must be 1..=8; got {}",
frame.channels
)));
}
if frame.channels != self.channels {
return Err(AudioError::Encode(format!(
"channel count mismatch: encoder configured for {}, frame has {}",
self.channels, frame.channels
)));
}
if frame.sample_rate != self.in_rate {
return Err(AudioError::Encode(format!(
"sample rate mismatch: encoder configured for {}, frame has {}",
self.in_rate, frame.sample_rate
)));
}
if self.next_pts_us.is_none() {
self.next_pts_us = Some(frame.pts);
}
if let Some(r) = self.resampler.as_mut() {
r.process(frame, &mut self.sample_carry)?;
} else {
self.sample_carry.extend_from_slice(&frame.samples);
}
self.drain_packets()
}
fn flush(&mut self) -> Result<Vec<EncodedAudioPacket>, AudioError> {
if let Some(r) = self.resampler.as_mut() {
r.flush(&mut self.sample_carry)?;
}
let chans = self.channels as usize;
let frame_interleaved_len = OPUS_FRAME_SAMPLES_48K * chans;
if !self.sample_carry.is_empty() && self.sample_carry.len() < frame_interleaved_len {
self.sample_carry.resize(frame_interleaved_len, 0.0);
}
self.drain_packets()
}
fn pre_skip(&self) -> u16 {
self.pre_skip_48k
}
fn extra_data(&self) -> Vec<u8> {
self.extra_data.clone()
}
}
fn build_dops(
channels: u8,
pre_skip_48k: u16,
input_sample_rate: u32,
ms_meta: Option<(u8, u8, &[u8])>,
) -> Vec<u8> {
let (family, total_len) = match ms_meta {
None => (0u8, 11usize),
Some(_) => (1u8, 11 + 2 + channels as usize),
};
let mut v = Vec::with_capacity(total_len);
v.push(0u8); v.push(channels);
v.extend_from_slice(&pre_skip_48k.to_le_bytes());
v.extend_from_slice(&input_sample_rate.to_le_bytes());
v.extend_from_slice(&0i16.to_le_bytes()); v.push(family);
if let Some((streams, coupled, mapping)) = ms_meta {
v.push(streams);
v.push(coupled);
v.extend_from_slice(mapping);
debug_assert_eq!(mapping.len(), channels as usize);
}
debug_assert_eq!(v.len(), total_len);
v
}
#[cfg(test)]
mod tests {
use super::*;
use audiopus::Channels as OpusChannels;
use audiopus::SampleRate;
use audiopus::coder::Decoder as OpusDecoderInner;
fn config_stereo_48k() -> AudioEncoderConfig {
AudioEncoderConfig {
codec: AudioCodec::Opus,
sample_rate: 48_000,
channels: 2,
bitrate: 96_000,
}
}
fn config_mono_48k() -> AudioEncoderConfig {
AudioEncoderConfig {
codec: AudioCodec::Opus,
sample_rate: 48_000,
channels: 1,
bitrate: 64_000,
}
}
fn config_multi_48k(channels: u8) -> AudioEncoderConfig {
AudioEncoderConfig {
codec: AudioCodec::Opus,
sample_rate: 48_000,
channels,
bitrate: 0, }
}
#[test]
fn opus_encoder_constructs_for_mono_48k_with_1_channel_dops() {
let enc = OpusEncoder::new(config_mono_48k()).expect("constructs");
assert_eq!(enc.channels, 1);
assert!(enc.resampler.is_none());
assert_eq!(enc.extra_data[1], 1);
}
#[test]
fn opus_encoder_uses_default_bitrate_when_caller_passes_zero() {
let mut cfg = config_stereo_48k();
cfg.bitrate = 0;
let _enc = OpusEncoder::new(cfg).expect("constructs with bitrate=0");
}
fn config_stereo_44100() -> AudioEncoderConfig {
AudioEncoderConfig {
codec: AudioCodec::Opus,
sample_rate: 44_100,
channels: 2,
bitrate: 96_000,
}
}
fn make_silence(channels: u8, frames: usize, sample_rate: u32) -> AudioFrame {
AudioFrame {
samples: vec![0.0f32; frames * channels as usize],
sample_rate,
channels,
pts: 0,
}
}
fn make_sine_1k(channels: u8, frames: usize, sample_rate: u32, amp: f32) -> AudioFrame {
let mut samples = Vec::with_capacity(frames * channels as usize);
let two_pi = std::f32::consts::PI * 2.0;
let freq = 1000.0f32;
for i in 0..frames {
let t = i as f32 / sample_rate as f32;
let v = (two_pi * freq * t).sin() * amp;
for _ in 0..channels {
samples.push(v);
}
}
AudioFrame {
samples,
sample_rate,
channels,
pts: 0,
}
}
#[test]
fn opus_encoder_constructs_for_stereo_48k() {
let enc = OpusEncoder::new(config_stereo_48k()).expect("constructs");
assert_eq!(enc.channels, 2);
assert_eq!(enc.in_rate, 48000);
assert!(enc.resampler.is_none(), "no resampler at native rate");
assert_eq!(enc.extra_data.len(), 11, "dOps body must be 11 bytes");
assert_eq!(enc.extra_data[0], 0);
assert_eq!(enc.extra_data[1], 2);
assert_eq!(enc.extra_data[10], 0);
}
#[test]
fn opus_encoder_resamples_44100_to_48k_internally() {
let enc = OpusEncoder::new(config_stereo_44100()).expect("constructs");
assert!(enc.resampler.is_some(), "resampler engaged at 44.1k input");
let r = enc.resampler.as_ref().unwrap();
assert_eq!(r.in_rate(), 44100);
assert_eq!(r.out_rate(), 48000);
}
#[test]
fn opus_encoder_rejects_zero_channels() {
let mut bad = config_stereo_48k();
bad.channels = 0;
assert!(matches!(
OpusEncoder::new(bad),
Err(AudioError::Unsupported(_))
));
}
#[test]
fn opus_encoder_rejects_nine_channels() {
let mut bad9 = config_stereo_48k();
bad9.channels = 9;
assert!(matches!(
OpusEncoder::new(bad9),
Err(AudioError::Unsupported(_))
));
}
#[test]
fn opus_encoder_rejects_nine_channel_frame_at_runtime() {
let mut enc = OpusEncoder::new(config_stereo_48k()).expect("constructs");
let bad_frame = AudioFrame {
samples: vec![0.0; 960 * 9],
sample_rate: 48000,
channels: 9,
pts: 0,
};
let r = enc.encode(&bad_frame);
assert!(
matches!(r, Err(AudioError::Unsupported(_))),
"9-channel frame should be Unsupported, got {:?}",
r
);
}
#[test]
fn opus_pre_skip_in_48khz_ticks_is_nonzero() {
let enc = OpusEncoder::new(config_stereo_48k()).expect("constructs");
assert!(
enc.pre_skip() > 0,
"Opus encoder lookahead should be positive (libopus convention)"
);
assert!(
enc.pre_skip() < 2000,
"lookahead is bounded — typically <600 samples at 48 kHz"
);
}
#[test]
fn opus_dops_carries_correct_pre_skip_and_input_sample_rate_le() {
let enc = OpusEncoder::new(config_stereo_44100()).expect("constructs");
let d = enc.extra_data();
let ps = u16::from_le_bytes([d[2], d[3]]);
assert_eq!(ps, enc.pre_skip(), "dOps PreSkip matches encoder lookahead");
let isr = u32::from_le_bytes([d[4], d[5], d[6], d[7]]);
assert_eq!(
isr, 44100,
"dOps InputSampleRate is the source rate, not 48k"
);
let og = i16::from_le_bytes([d[8], d[9]]);
assert_eq!(og, 0);
}
#[test]
fn opus_encode_20ms_silence_produces_one_packet() {
let mut enc = OpusEncoder::new(config_stereo_48k()).expect("constructs");
let frame = make_silence(2, 960, 48_000);
let pkts = enc.encode(&frame).expect("encode");
assert_eq!(pkts.len(), 1, "exactly one Opus packet for one 20ms frame");
let pkt = &pkts[0];
assert!(!pkt.data.is_empty(), "packet should have bytes");
assert!(
pkt.data.len() < 200,
"silence packet at 96 kbps should be small, got {} bytes",
pkt.data.len()
);
assert_eq!(pkt.duration, 960, "20ms = 960 ticks at 48k");
}
#[test]
fn opus_encode_one_second_of_sine_produces_packets_with_reasonable_bitrate() {
let mut enc = OpusEncoder::new(config_stereo_48k()).expect("constructs");
let mut total_bytes = 0usize;
let mut total_packets = 0usize;
for i in 0..50 {
let mut frame = make_sine_1k(2, 960, 48_000, 0.3);
frame.pts = i * 20_000;
let pkts = enc.encode(&frame).expect("encode");
for p in &pkts {
total_bytes += p.data.len();
total_packets += 1;
}
}
let pkts_flush = enc.flush().expect("flush");
for p in &pkts_flush {
total_bytes += p.data.len();
total_packets += 1;
}
assert!(
total_packets >= 49 && total_packets <= 51,
"expected ~50 packets for 1 s of audio, got {total_packets}"
);
let observed_bps = (total_bytes as u64 * 8) as i64;
assert!(
observed_bps > 30_000 && observed_bps < 200_000,
"1s of 1kHz sine at 96 kbps should yield 30-200 kbps actual, got {observed_bps} bps ({total_bytes} bytes)"
);
}
#[test]
fn opus_pts_steps_by_20ms_per_packet() {
let mut enc = OpusEncoder::new(config_stereo_48k()).expect("constructs");
let frame_a = make_silence(2, 960, 48_000);
let mut frame_b = make_silence(2, 960, 48_000);
frame_b.pts = 20_000;
let pkts_a = enc.encode(&frame_a).expect("a");
let pkts_b = enc.encode(&frame_b).expect("b");
assert_eq!(pkts_a.len(), 1);
assert_eq!(pkts_b.len(), 1);
let dt = pkts_b[0].pts - pkts_a[0].pts;
assert_eq!(
dt, 20_000,
"PTS should step by 20_000 us per Opus packet (20 ms frame)"
);
}
#[test]
fn opus_round_trip_sine_wave_quality_is_acceptable() {
let mut enc = OpusEncoder::new(config_stereo_48k()).expect("constructs");
let frames_per_chunk = 960;
let n_chunks = 25; let total_frames = frames_per_chunk * n_chunks;
let mut all_samples = Vec::with_capacity(total_frames * 2);
let two_pi = std::f32::consts::PI * 2.0;
let freq = 1000.0f32;
for i in 0..total_frames {
let t = i as f32 / 48_000.0;
let v = (two_pi * freq * t).sin() * 0.5;
all_samples.push(v);
all_samples.push(v);
}
let mut packets = Vec::new();
for c in 0..n_chunks {
let chunk_samples =
all_samples[c * frames_per_chunk * 2..(c + 1) * frames_per_chunk * 2].to_vec();
let frame = AudioFrame {
samples: chunk_samples,
sample_rate: 48_000,
channels: 2,
pts: (c as i64) * 20_000,
};
packets.extend(enc.encode(&frame).expect("encode"));
}
packets.extend(enc.flush().expect("flush"));
assert!(!packets.is_empty(), "encode must produce packets");
let mut dec =
OpusDecoderInner::new(SampleRate::Hz48000, OpusChannels::Stereo).expect("dec");
let mut decoded = Vec::with_capacity(total_frames * 2);
let mut tmp = vec![0.0f32; frames_per_chunk * 2];
for p in &packets {
let pkt = audiopus::packet::Packet::try_from(p.data.as_slice()).expect("pkt");
let sig = audiopus::MutSignals::try_from(tmp.as_mut_slice()).expect("sig");
let n = dec
.decode_float(Some(pkt), sig, false)
.expect("decode_float");
decoded.extend_from_slice(&tmp[..n * 2]);
}
assert!(
decoded.len() >= (total_frames - 100) * 2,
"decoded length {} should approximate input length {}",
decoded.len(),
total_frames * 2
);
let pre_skip = enc.pre_skip() as usize;
let cmp_start = pre_skip + 480; let cmp_end = (decoded.len() / 2).min(total_frames - 100);
if cmp_end <= cmp_start {
panic!(
"round trip too short: cmp_start={cmp_start}, cmp_end={cmp_end}, decoded len/2={}",
decoded.len() / 2
);
}
let mut sum_sq_err = 0.0f64;
let mut sum_sq_sig = 0.0f64;
let mut n = 0usize;
for i in cmp_start..cmp_end {
let in_idx = i - pre_skip;
let l_in = all_samples[in_idx * 2];
let r_in = all_samples[in_idx * 2 + 1];
let l_out = decoded[i * 2];
let r_out = decoded[i * 2 + 1];
sum_sq_err += ((l_in - l_out) as f64).powi(2);
sum_sq_err += ((r_in - r_out) as f64).powi(2);
sum_sq_sig += (l_in as f64).powi(2);
sum_sq_sig += (r_in as f64).powi(2);
n += 2;
}
let rms_err = (sum_sq_err / n as f64).sqrt();
let rms_sig = (sum_sq_sig / n as f64).sqrt();
let snr_db = 20.0 * (rms_sig / rms_err.max(1e-12)).log10();
assert!(
snr_db > 15.0,
"round-trip SNR {snr_db:.2} dB too low — Opus quality regression?"
);
println!("opus_round_trip SNR = {snr_db:.2} dB, rms_err = {rms_err:.4}");
}
#[test]
fn dops_layout_matches_rfc_7845_for_mono_and_stereo() {
let d_mono = build_dops(1, 312, 48_000, None);
assert_eq!(d_mono.len(), 11);
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!(
u32::from_le_bytes([d_mono[4], d_mono[5], d_mono[6], d_mono[7]]),
48000
); 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);
assert_eq!(d_stereo.len(), 11);
assert_eq!(d_stereo[1], 2);
assert_eq!(u16::from_le_bytes([d_stereo[2], d_stereo[3]]), 400);
assert_eq!(
u32::from_le_bytes([d_stereo[4], d_stereo[5], d_stereo[6], d_stereo[7]]),
44100
);
}
#[test]
fn surround_mapping_family_1_matches_rfc_7845_5_1_1_2() {
assert_eq!(
surround_mapping_family_1(3).unwrap(),
(2, 1, &[0, 2, 1][..])
);
assert_eq!(
surround_mapping_family_1(4).unwrap(),
(2, 2, &[0, 1, 2, 3][..])
);
assert_eq!(
surround_mapping_family_1(5).unwrap(),
(3, 2, &[0, 4, 1, 2, 3][..])
);
assert_eq!(
surround_mapping_family_1(6).unwrap(),
(4, 2, &[0, 4, 1, 2, 3, 5][..])
);
assert_eq!(
surround_mapping_family_1(7).unwrap(),
(4, 3, &[0, 4, 1, 2, 3, 5, 6][..])
);
assert_eq!(
surround_mapping_family_1(8).unwrap(),
(5, 3, &[0, 6, 1, 2, 3, 4, 5, 7][..])
);
assert!(surround_mapping_family_1(0).is_err());
assert!(surround_mapping_family_1(1).is_err()); assert!(surround_mapping_family_1(2).is_err());
assert!(surround_mapping_family_1(9).is_err());
}
#[test]
fn opus_encoder_constructs_for_3_0_through_7_1_with_family_1_dops() {
for &ch in &[3u8, 4, 5, 6, 7, 8] {
let enc = OpusEncoder::new(config_multi_48k(ch))
.unwrap_or_else(|e| panic!("constructs for {ch}ch: {e:?}"));
assert_eq!(enc.channels, ch);
assert!(enc.resampler.is_none(), "no resampler at native rate");
let d = enc.extra_data();
let expected_len = 11 + 2 + ch as usize;
assert_eq!(
d.len(),
expected_len,
"dOps body for {ch}ch should be {expected_len} bytes (11 preamble + 2 stream/coupled + N mapping); got {}",
d.len()
);
assert_eq!(
d[0], 0,
"Version=0 (dOps box version, not Opus stream version)"
);
assert_eq!(d[1], ch, "OutputChannelCount");
assert_eq!(d[10], 1, "ChannelMappingFamily=1 for surround");
let (exp_streams, exp_coupled, exp_mapping) = surround_mapping_family_1(ch).unwrap();
assert_eq!(d[11], exp_streams, "StreamCount for {ch}ch");
assert_eq!(d[12], exp_coupled, "CoupledCount for {ch}ch");
assert_eq!(
&d[13..13 + ch as usize],
exp_mapping,
"ChannelMapping for {ch}ch"
);
}
}
#[test]
fn opus_encoder_dops_5_1_hex_layout() {
let enc = OpusEncoder::new(config_multi_48k(6)).expect("5.1 constructs");
let d = enc.extra_data();
assert_eq!(d.len(), 19, "5.1 dOps body = 11 + 2 + 6 = 19 bytes");
let hex: String = d.iter().map(|b| format!("{b:02x} ")).collect();
println!(
"5.1 dOps body hex (LE-encoded, 19 bytes): {}",
hex.trim_end()
);
assert_eq!(d[0], 0); assert_eq!(d[1], 6); let ps = u16::from_le_bytes([d[2], d[3]]);
assert!(ps > 0 && ps < 2000);
assert_eq!(
u32::from_le_bytes([d[4], d[5], d[6], d[7]]),
48_000,
"InputSampleRate=48000"
);
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][..]); }
#[test]
fn opus_5_1_encode_20ms_silence_produces_one_packet() {
let mut enc = OpusEncoder::new(config_multi_48k(6)).expect("5.1 constructs");
let frame = make_silence(6, 960, 48_000);
let pkts = enc.encode(&frame).expect("encode 5.1 silence");
assert_eq!(pkts.len(), 1, "exactly one Opus packet for one 20ms frame");
let pkt = &pkts[0];
assert!(!pkt.data.is_empty());
assert!(
pkt.data.len() < 600,
"5.1 silence packet should still be under ~600 bytes, got {} bytes",
pkt.data.len()
);
assert_eq!(pkt.duration, 960);
}
#[test]
fn opus_5_1_round_trip_per_channel_snr_is_acceptable() {
let freqs = [440.0f32, 523.25, 659.25, 80.0, 880.0, 987.77];
let chans: u8 = 6;
let frames_per_chunk = 960;
let n_chunks = 30; let total_frames = frames_per_chunk * n_chunks;
let amp = 0.4f32;
let mut all = vec![0.0f32; total_frames * chans as usize];
let two_pi = std::f32::consts::PI * 2.0;
for i in 0..total_frames {
let t = i as f32 / 48_000.0;
for ch in 0..chans as usize {
all[i * chans as usize + ch] = (two_pi * freqs[ch] * t).sin() * amp;
}
}
let mut enc = OpusEncoder::new(config_multi_48k(chans)).expect("encoder");
let mut packets = Vec::new();
for c in 0..n_chunks {
let frame = AudioFrame {
samples: all[c * frames_per_chunk * chans as usize
..(c + 1) * frames_per_chunk * chans as usize]
.to_vec(),
sample_rate: 48_000,
channels: chans,
pts: (c as i64) * 20_000,
};
packets.extend(enc.encode(&frame).expect("encode"));
}
packets.extend(enc.flush().expect("flush"));
assert!(!packets.is_empty(), "must produce packets");
let (streams, coupled, mapping) = surround_mapping_family_1(chans).unwrap();
let mut err: c_int = 0;
let dec_state = unsafe {
ffi::opus_multistream_decoder_create(
48_000,
chans as c_int,
streams as c_int,
coupled as c_int,
mapping.as_ptr(),
&mut err,
)
};
assert!(
!dec_state.is_null() && err == ffi::OPUS_OK,
"MS decoder create"
);
let mut decoded = Vec::with_capacity(total_frames * chans as usize);
let mut tmp = vec![0.0f32; frames_per_chunk * chans as usize];
for p in &packets {
let n = unsafe {
ffi::opus_multistream_decode_float(
dec_state,
p.data.as_ptr(),
p.data.len() as i32,
tmp.as_mut_ptr(),
frames_per_chunk as c_int,
0,
)
};
assert!(n > 0, "MS decode_float returned {n}");
decoded.extend_from_slice(&tmp[..(n as usize) * chans as usize]);
}
unsafe { ffi::opus_multistream_decoder_destroy(dec_state) };
let pre_skip = enc.pre_skip() as usize;
let cmp_start = pre_skip + 480;
let cmp_end = (decoded.len() / chans as usize).min(total_frames - 200);
assert!(cmp_end > cmp_start, "round trip too short");
let mut snrs = Vec::with_capacity(chans as usize);
for ch in 0..chans as usize {
let mut sum_sq_err = 0.0f64;
let mut sum_sq_sig = 0.0f64;
for i in cmp_start..cmp_end {
let in_idx = i - pre_skip;
let s_in = all[in_idx * chans as usize + ch];
let s_out = decoded[i * chans as usize + ch];
sum_sq_err += ((s_in - s_out) as f64).powi(2);
sum_sq_sig += (s_in as f64).powi(2);
}
let n = (cmp_end - cmp_start) as f64;
let rms_err = (sum_sq_err / n).sqrt();
let rms_sig = (sum_sq_sig / n).sqrt();
let snr_db = 20.0 * (rms_sig / rms_err.max(1e-12)).log10();
snrs.push(snr_db);
}
println!("5.1 per-channel SNR (dB):");
for (i, snr) in snrs.iter().enumerate() {
let label = ["FL", "FR", "C", "LFE", "BL", "BR"][i];
println!(" ch{i} ({label}): {snr:.2} dB");
}
for (i, snr) in snrs.iter().enumerate() {
assert!(
*snr > 5.0,
"ch{i} SNR {snr:.2} dB too low — multistream quality regression?"
);
}
}
#[test]
fn dops_layout_for_5_1_matches_family_1_spec() {
let (streams, coupled, mapping) = surround_mapping_family_1(6).unwrap();
let d = build_dops(6, 312, 48_000, Some((streams, coupled, mapping)));
assert_eq!(d.len(), 11 + 2 + 6, "5.1 dOps = 19 bytes");
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][..]);
}
#[test]
fn opus_5_1_resamples_44100_to_48k() {
let mut cfg = config_multi_48k(6);
cfg.sample_rate = 44_100;
let enc = OpusEncoder::new(cfg).expect("5.1 @ 44.1k constructs");
assert!(enc.resampler.is_some(), "resampler engaged for 6ch @ 44.1k");
let r = enc.resampler.as_ref().unwrap();
assert_eq!(r.in_rate(), 44_100);
assert_eq!(r.out_rate(), 48_000);
assert_eq!(r.channels(), 6);
}
}