use crate::error;
use crate::layout;
use audio_core_bsd::AudioFrame;
const MAX_PACKET_BYTES: usize = 4000;
pub trait AudioEncoder: Send {
fn encode(&mut self, pcm: &[f32]) -> error::Result<Vec<u8>>;
fn set_bitrate(&mut self, bps: u32);
}
#[derive(Debug)]
pub struct OpusEncoder {
encoder: opus::Encoder,
sample_rate: u32,
channels: u16,
packet_buf: Vec<u8>,
}
impl OpusEncoder {
pub fn new(
sample_rate: u32,
channels: u16,
application: opus::Application,
) -> error::Result<Self> {
if sample_rate == 0 {
return Err(error::OpusError::InvalidSampleRate(sample_rate));
}
let opus_channels = match channels {
1 => opus::Channels::Mono,
2 => opus::Channels::Stereo,
other => return Err(error::OpusError::UnsupportedChannels(other)),
};
let encoder = opus::Encoder::new(sample_rate, opus_channels, application)
.map_err(|e| error::OpusError::Encode(e.to_string()))?;
Ok(Self {
encoder,
sample_rate,
channels,
packet_buf: vec![0u8; MAX_PACKET_BYTES],
})
}
#[must_use]
pub fn sample_rate(&self) -> u32 {
self.sample_rate
}
#[must_use]
pub fn channels(&self) -> u16 {
self.channels
}
pub fn encode_interleaved(&mut self, pcm: &[f32]) -> error::Result<Vec<u8>> {
let n = self
.encoder
.encode_float(pcm, &mut self.packet_buf)
.map_err(|e| error::OpusError::Encode(e.to_string()))?;
Ok(self.packet_buf[..n].to_vec())
}
pub fn encode_frame(&mut self, frame: &AudioFrame) -> error::Result<Vec<u8>> {
if frame.channels == 0 {
return Err(error::OpusError::InvalidChannelCount(frame.channels));
}
if frame.channels != self.channels {
return Err(error::OpusError::InvalidChannelCount(frame.channels));
}
let interleaved = layout::planar_to_interleaved(self.channels, &frame.samples)?;
self.encode_interleaved(&interleaved)
}
pub fn set_bitrate_checked(&mut self, bps: u32) -> error::Result<()> {
let bps_i32 = i32::try_from(bps).unwrap_or(i32::MAX);
self.encoder
.set_bitrate(opus::Bitrate::Bits(bps_i32))
.map_err(|e| error::OpusError::Encode(e.to_string()))
}
pub fn set_complexity(&mut self, complexity: i32) -> error::Result<()> {
self.encoder
.set_complexity(complexity)
.map_err(|e| error::OpusError::Encode(e.to_string()))
}
pub fn set_dtx(&mut self, enabled: bool) -> error::Result<()> {
self.encoder
.set_dtx(enabled)
.map_err(|e| error::OpusError::Encode(e.to_string()))
}
}
impl AudioEncoder for OpusEncoder {
fn encode(&mut self, pcm: &[f32]) -> error::Result<Vec<u8>> {
self.encode_interleaved(pcm)
}
fn set_bitrate(&mut self, bps: u32) {
let _ = self.set_bitrate_checked(bps);
}
}
#[cfg(test)]
#[allow(clippy::cast_precision_loss)] mod tests {
use super::*;
const FRAME_SIZE: usize = 960;
const SAMPLE_RATE: u32 = 48_000;
#[test]
fn mono_encoder_constructs() {
let enc = OpusEncoder::new(SAMPLE_RATE, 1, opus::Application::Audio);
assert!(enc.is_ok());
let enc = enc.unwrap();
assert_eq!(enc.sample_rate(), SAMPLE_RATE);
assert_eq!(enc.channels(), 1);
}
#[test]
fn stereo_encoder_constructs() {
let enc = OpusEncoder::new(SAMPLE_RATE, 2, opus::Application::Audio);
assert!(enc.is_ok());
}
#[test]
fn zero_sample_rate_is_rejected() {
let err = OpusEncoder::new(0, 1, opus::Application::Audio).unwrap_err();
assert_eq!(err, error::OpusError::InvalidSampleRate(0));
}
#[test]
fn unsupported_channels_are_rejected() {
for bad in [0u16, 3, 6, 8] {
let err = OpusEncoder::new(SAMPLE_RATE, bad, opus::Application::Audio).unwrap_err();
assert_eq!(err, error::OpusError::UnsupportedChannels(bad));
}
}
#[test]
fn mono_encode_produces_non_empty_packet() {
let mut enc = OpusEncoder::new(SAMPLE_RATE, 1, opus::Application::Audio).unwrap();
let pcm = vec![0.0f32; FRAME_SIZE];
let packet = enc.encode(&pcm).unwrap();
assert!(!packet.is_empty());
assert!(packet.len() < MAX_PACKET_BYTES);
}
#[test]
fn stereo_encode_produces_non_empty_packet() {
let mut enc = OpusEncoder::new(SAMPLE_RATE, 2, opus::Application::Audio).unwrap();
let pcm = vec![0.0f32; FRAME_SIZE * 2];
let packet = enc.encode(&pcm).unwrap();
assert!(!packet.is_empty());
}
#[test]
fn encode_frame_validates_channel_count() {
let mut enc = OpusEncoder::new(SAMPLE_RATE, 2, opus::Application::Audio).unwrap();
let mono_frame = AudioFrame::silence(1, FRAME_SIZE, SAMPLE_RATE);
let err = enc.encode_frame(&mono_frame).unwrap_err();
assert_eq!(err, error::OpusError::InvalidChannelCount(1));
}
#[test]
fn encode_frame_zero_channels_is_rejected() {
let mut enc = OpusEncoder::new(SAMPLE_RATE, 1, opus::Application::Audio).unwrap();
let zero_ch = AudioFrame::new(0, SAMPLE_RATE);
let err = enc.encode_frame(&zero_ch).unwrap_err();
assert_eq!(err, error::OpusError::InvalidChannelCount(0));
}
#[test]
fn encode_frame_planar_mono_round_trips_to_packet() {
let mut enc = OpusEncoder::new(SAMPLE_RATE, 1, opus::Application::Audio).unwrap();
let frame = AudioFrame::silence(1, FRAME_SIZE, SAMPLE_RATE);
let packet = enc.encode_frame(&frame).unwrap();
assert!(!packet.is_empty());
}
#[test]
fn encode_frame_planar_stereo_round_trips_to_packet() {
let mut enc = OpusEncoder::new(SAMPLE_RATE, 2, opus::Application::Audio).unwrap();
let frame = AudioFrame::silence(2, FRAME_SIZE, SAMPLE_RATE);
let packet = enc.encode_frame(&frame).unwrap();
assert!(!packet.is_empty());
}
#[test]
fn set_bitrate_checked_accepts_valid_value() {
let mut enc = OpusEncoder::new(SAMPLE_RATE, 1, opus::Application::Audio).unwrap();
assert!(enc.set_bitrate_checked(64_000).is_ok());
}
#[test]
fn set_bitrate_checked_handles_oversized_u32_without_panic() {
let mut enc = OpusEncoder::new(SAMPLE_RATE, 1, opus::Application::Audio).unwrap();
let res = enc.set_bitrate_checked(4_000_000_000);
assert!(
res.is_ok(),
"oversized u32 bitrate must be clamped, not error"
);
}
#[test]
fn set_bitrate_trait_method_swallows_anything() {
let mut enc = OpusEncoder::new(SAMPLE_RATE, 1, opus::Application::Audio).unwrap();
enc.set_bitrate(4_000_000_000);
}
#[test]
fn set_complexity_accepts_valid_range() {
let mut enc = OpusEncoder::new(SAMPLE_RATE, 1, opus::Application::Audio).unwrap();
for c in 0..=10 {
assert!(
enc.set_complexity(c).is_ok(),
"complexity {c} should be accepted"
);
}
}
#[test]
fn set_dtx_toggles_without_error() {
let mut enc = OpusEncoder::new(SAMPLE_RATE, 1, opus::Application::Audio).unwrap();
assert!(enc.set_dtx(true).is_ok());
assert!(enc.set_dtx(false).is_ok());
}
#[test]
fn planar_to_interleaved_mono_is_identity() {
let planar = vec![0.1, 0.2, 0.3, 0.4];
let out = layout::planar_to_interleaved(1, &planar).unwrap();
assert_eq!(out, planar);
}
#[test]
fn planar_to_interleaved_stereo_stride_is_correct() {
let planar = vec![10.0, 11.0, 12.0, 20.0, 21.0, 22.0];
let out = layout::planar_to_interleaved(2, &planar).unwrap();
assert_eq!(out, vec![10.0, 20.0, 11.0, 21.0, 12.0, 22.0]);
}
#[test]
fn planar_to_interleaved_preserves_all_samples() {
for channels in 1u16..=2 {
let nf = 960;
let planar: Vec<f32> = (0..nf * usize::from(channels)).map(|i| i as f32).collect();
let out = layout::planar_to_interleaved(channels, &planar).unwrap();
assert_eq!(out.len(), planar.len());
let mut a = out.clone();
a.sort_by(|x, y| x.partial_cmp(y).unwrap());
let mut b = planar.clone();
b.sort_by(|x, y| x.partial_cmp(y).unwrap());
assert_eq!(a, b);
}
}
#[test]
fn packet_buffer_is_reused_across_calls() {
let mut enc = OpusEncoder::new(SAMPLE_RATE, 1, opus::Application::Audio).unwrap();
let pcm = vec![0.0f32; FRAME_SIZE];
let p1 = enc.encode(&pcm).unwrap();
let p2 = enc.encode(&pcm).unwrap();
assert!(!p1.is_empty());
assert!(!p2.is_empty());
}
}