use super::capture::AudioFrame;
use crate::constants::{OPUS_APPLICATION_AUDIO, OPUS_FRAME_SAMPLES, OPUS_SAMPLE_RATE};
use crate::errors::CameraError;
#[derive(Debug, Clone)]
pub struct EncodedAudio {
pub data: Vec<u8>,
pub timestamp: f64,
pub duration: f64,
}
pub struct OpusEncoder {
encoder: *mut libopus_sys::OpusEncoder,
channels: u16,
sample_rate: u32,
sample_buffer: Vec<f32>,
buffer_start_pts: Option<f64>,
samples_encoded: u64,
samples_encoded_f64: f64,
}
unsafe impl Send for OpusEncoder {}
impl OpusEncoder {
pub fn new(sample_rate: u32, channels: u16, bitrate: u32) -> Result<Self, CameraError> {
if sample_rate != OPUS_SAMPLE_RATE {
return Err(CameraError::AudioError(format!(
"Opus requires {OPUS_SAMPLE_RATE} Hz sample rate"
)));
}
if channels != 1 && channels != 2 {
return Err(CameraError::AudioError(
"Opus supports only mono (1) or stereo (2) channels".to_string(),
));
}
let sample_rate_i32 = i32::try_from(sample_rate)
.map_err(|_| CameraError::AudioError("sample rate exceeds i32 range".to_string()))?;
let mut error: i32 = 0;
let encoder = unsafe {
libopus_sys::opus_encoder_create(
sample_rate_i32,
i32::from(channels),
OPUS_APPLICATION_AUDIO,
&raw mut error,
)
};
if encoder.is_null() || error != 0 {
return Err(CameraError::AudioError(format!(
"Failed to create Opus encoder: error code {error}"
)));
}
let bitrate_i32 = i32::try_from(bitrate)
.map_err(|_| CameraError::AudioError("bitrate exceeds i32 range".to_string()))?;
let bitrate_request =
i32::try_from(libopus_sys::OPUS_SET_BITRATE_REQUEST).map_err(|_| {
CameraError::AudioError("OPUS_SET_BITRATE_REQUEST exceeds i32".to_string())
})?;
let result =
unsafe { libopus_sys::opus_encoder_ctl(encoder, bitrate_request, bitrate_i32) };
if result != 0 {
unsafe { libopus_sys::opus_encoder_destroy(encoder) };
return Err(CameraError::AudioError(format!(
"Failed to set bitrate: error code {result}"
)));
}
Ok(Self {
encoder,
channels,
sample_rate,
sample_buffer: Vec::with_capacity(OPUS_FRAME_SAMPLES * channels as usize * 2),
buffer_start_pts: None,
samples_encoded: 0,
samples_encoded_f64: 0.0,
})
}
pub fn encode(&mut self, frame: &AudioFrame) -> Result<Vec<EncodedAudio>, CameraError> {
if frame.sample_rate != self.sample_rate {
return Err(CameraError::AudioError(format!(
"Sample rate mismatch: expected {}, got {}",
self.sample_rate, frame.sample_rate
)));
}
if frame.channels != self.channels {
return Err(CameraError::AudioError(format!(
"Channel count mismatch: expected {}, got {}",
self.channels, frame.channels
)));
}
if self.buffer_start_pts.is_none() && !frame.samples.is_empty() {
self.buffer_start_pts = Some(frame.timestamp);
}
self.sample_buffer.extend_from_slice(&frame.samples);
let mut encoded_packets = Vec::new();
let samples_per_frame = OPUS_FRAME_SAMPLES * self.channels as usize;
let sample_rate_f64 = f64::from(self.sample_rate);
let opus_samples_f64 =
f64::from(u32::try_from(OPUS_FRAME_SAMPLES).map_err(|_| {
CameraError::AudioError("OPUS_FRAME_SAMPLES exceeds u32".to_string())
})?);
let frame_duration = opus_samples_f64 / sample_rate_f64;
while self.sample_buffer.len() >= samples_per_frame {
let frame_samples: Vec<f32> = self.sample_buffer.drain(..samples_per_frame).collect();
let pts = self.samples_encoded_f64 / sample_rate_f64;
let mut output = vec![0u8; 4000]; let frame_samples_i32 = i32::try_from(OPUS_FRAME_SAMPLES).map_err(|_| {
CameraError::AudioError("OPUS_FRAME_SAMPLES exceeds i32".to_string())
})?;
let max_bytes = i32::try_from(output.len()).map_err(|_| {
CameraError::AudioError("output buffer length exceeds i32".to_string())
})?;
let len = unsafe {
libopus_sys::opus_encode_float(
self.encoder,
frame_samples.as_ptr(),
frame_samples_i32,
output.as_mut_ptr(),
max_bytes,
)
};
if len < 0 {
return Err(CameraError::AudioError(format!(
"Opus encoding failed: error code {len}"
)));
}
output.truncate(usize::try_from(len).unwrap_or(0));
encoded_packets.push(EncodedAudio {
data: output,
timestamp: self.buffer_start_pts.unwrap_or(0.0) + pts,
duration: frame_duration,
});
self.samples_encoded += u64::try_from(OPUS_FRAME_SAMPLES).map_err(|_| {
CameraError::AudioError("OPUS_FRAME_SAMPLES exceeds u64".to_string())
})?;
self.samples_encoded_f64 +=
f64::from(u32::try_from(OPUS_FRAME_SAMPLES).map_err(|_| {
CameraError::AudioError("OPUS_FRAME_SAMPLES exceeds u32".to_string())
})?);
}
Ok(encoded_packets)
}
pub fn flush(&mut self) -> Result<Vec<EncodedAudio>, CameraError> {
if self.sample_buffer.is_empty() {
return Ok(Vec::new());
}
let samples_per_frame = OPUS_FRAME_SAMPLES * self.channels as usize;
let padding_needed = samples_per_frame - (self.sample_buffer.len() % samples_per_frame);
if padding_needed < samples_per_frame {
self.sample_buffer.extend(vec![0.0f32; padding_needed]);
}
let mut encoded_packets = Vec::new();
let sample_rate_f64 = f64::from(self.sample_rate);
let opus_samples_f64 =
f64::from(u32::try_from(OPUS_FRAME_SAMPLES).map_err(|_| {
CameraError::AudioError("OPUS_FRAME_SAMPLES exceeds u32".to_string())
})?);
let frame_duration = opus_samples_f64 / sample_rate_f64;
while self.sample_buffer.len() >= samples_per_frame {
let frame_samples: Vec<f32> = self.sample_buffer.drain(..samples_per_frame).collect();
let pts = self.samples_encoded_f64 / sample_rate_f64;
let mut output = vec![0u8; 4000];
let frame_samples_i32 = i32::try_from(OPUS_FRAME_SAMPLES).map_err(|_| {
CameraError::AudioError("OPUS_FRAME_SAMPLES exceeds i32".to_string())
})?;
let max_bytes = i32::try_from(output.len()).map_err(|_| {
CameraError::AudioError("output buffer length exceeds i32".to_string())
})?;
let len = unsafe {
libopus_sys::opus_encode_float(
self.encoder,
frame_samples.as_ptr(),
frame_samples_i32,
output.as_mut_ptr(),
max_bytes,
)
};
if len < 0 {
return Err(CameraError::AudioError(format!(
"Opus flush failed: error code {len}"
)));
}
output.truncate(usize::try_from(len).unwrap_or(0));
encoded_packets.push(EncodedAudio {
data: output,
timestamp: self.buffer_start_pts.unwrap_or(0.0) + pts,
duration: frame_duration,
});
self.samples_encoded += u64::try_from(OPUS_FRAME_SAMPLES).map_err(|_| {
CameraError::AudioError("OPUS_FRAME_SAMPLES exceeds u64".to_string())
})?;
self.samples_encoded_f64 +=
f64::from(u32::try_from(OPUS_FRAME_SAMPLES).map_err(|_| {
CameraError::AudioError("OPUS_FRAME_SAMPLES exceeds u32".to_string())
})?);
}
Ok(encoded_packets)
}
pub fn sample_rate(&self) -> u32 {
self.sample_rate
}
pub fn channels(&self) -> u16 {
self.channels
}
}
impl Drop for OpusEncoder {
fn drop(&mut self) {
if !self.encoder.is_null() {
unsafe {
libopus_sys::opus_encoder_destroy(self.encoder);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_encoder_creation() {
let encoder = OpusEncoder::new(48000, 2, 128_000);
assert!(encoder.is_ok());
}
#[test]
fn test_encoder_rejects_wrong_sample_rate() {
let encoder = OpusEncoder::new(44100, 2, 128_000);
assert!(encoder.is_err());
}
#[test]
fn test_encoder_rejects_wrong_channels() {
let encoder = OpusEncoder::new(48000, 5, 128_000);
assert!(encoder.is_err());
}
#[test]
fn test_encode_full_frame() {
let mut encoder = OpusEncoder::new(48000, 2, 128_000).expect("create Opus encoder");
let frame = AudioFrame {
samples: vec![0.0f32; OPUS_FRAME_SAMPLES * 2],
sample_rate: 48000,
channels: 2,
timestamp: 0.0,
};
let encoded_packets = encoder.encode(&frame).expect("encode full frame");
assert_eq!(encoded_packets.len(), 1);
assert!(!encoded_packets[0].data.is_empty());
}
#[test]
fn test_encode_partial_frame() {
let mut encoder = OpusEncoder::new(48000, 2, 128_000).expect("create Opus encoder");
let frame = AudioFrame {
samples: vec![0.0f32; 100],
sample_rate: 48000,
channels: 2,
timestamp: 0.0,
};
let encoded_packets = encoder.encode(&frame).expect("encode partial frame");
assert!(
encoded_packets.is_empty(),
"Partial frame should not produce output"
);
}
#[test]
fn test_flush_remaining() {
let mut encoder = OpusEncoder::new(48000, 2, 128_000).expect("create Opus encoder");
let frame = AudioFrame {
samples: vec![0.0f32; 100],
sample_rate: 48000,
channels: 2,
timestamp: 0.0,
};
encoder.encode(&frame).expect("encode partial frame");
let flushed = encoder.flush().expect("flush encoder");
assert_eq!(flushed.len(), 1);
}
}