use crate::decoder::OpusDecoder;
use crate::encoder::OpusEncoder;
use crate::error;
use audio_core_bsd::AudioFrame;
use std::thread::JoinHandle;
const POLL_IDLE: std::time::Duration = std::time::Duration::from_micros(100);
pub struct OpusDecodeWorker {
handle: Option<JoinHandle<()>>,
input: rtrb::Producer<Vec<u8>>,
}
impl OpusDecodeWorker {
pub fn new(
sample_rate: u32,
channels: u16,
capacity: usize,
) -> error::Result<(Self, rtrb::Consumer<AudioFrame>)> {
let mut decoder = OpusDecoder::new(sample_rate, channels)?;
let (input_prod, mut input_cons) = rtrb::RingBuffer::<Vec<u8>>::new(capacity);
let (mut output_prod, output_cons) = rtrb::RingBuffer::<AudioFrame>::new(capacity);
let handle = std::thread::Builder::new()
.name("opus-decoder".to_string())
.spawn(move || {
loop {
match input_cons.pop() {
Ok(packet) => {
if let Ok(frame) = decoder.decode_frame(&packet) {
if output_prod.push(frame).is_err() {
}
}
}
Err(rtrb::PopError::Empty) => {
if input_cons.is_abandoned() {
break;
}
std::thread::sleep(POLL_IDLE);
}
}
}
})
.map_err(|e| error::OpusError::WorkerThread(e.to_string()))?;
Ok((
Self {
handle: Some(handle),
input: input_prod,
},
output_cons,
))
}
#[must_use]
pub fn submit_packet(&mut self, packet: Vec<u8>) -> bool {
self.input.push(packet).is_ok()
}
pub fn join(mut self) -> std::thread::Result<()> {
let handle = self.handle.take();
drop(self.input);
match handle {
Some(h) => h.join(),
None => Ok(()),
}
}
}
pub struct OpusEncodeWorker {
handle: Option<JoinHandle<()>>,
input: rtrb::Producer<AudioFrame>,
}
impl OpusEncodeWorker {
pub fn new(
sample_rate: u32,
channels: u16,
application: opus::Application,
capacity: usize,
) -> error::Result<(Self, rtrb::Consumer<Vec<u8>>)> {
let mut encoder = OpusEncoder::new(sample_rate, channels, application)?;
let _ = encoder.set_bitrate_checked(64_000);
let (input_prod, mut input_cons) = rtrb::RingBuffer::<AudioFrame>::new(capacity);
let (mut output_prod, output_cons) = rtrb::RingBuffer::<Vec<u8>>::new(capacity);
let handle = std::thread::Builder::new()
.name("opus-encoder".to_string())
.spawn(move || {
loop {
match input_cons.pop() {
Ok(frame) => {
if let Ok(packet) = encoder.encode_frame(&frame) {
if output_prod.push(packet).is_err() {
}
}
}
Err(rtrb::PopError::Empty) => {
if input_cons.is_abandoned() {
break;
}
std::thread::sleep(POLL_IDLE);
}
}
}
})
.map_err(|e| error::OpusError::WorkerThread(e.to_string()))?;
Ok((
Self {
handle: Some(handle),
input: input_prod,
},
output_cons,
))
}
#[must_use]
pub fn submit_frame(&mut self, frame: AudioFrame) -> bool {
self.input.push(frame).is_ok()
}
pub fn join(mut self) -> std::thread::Result<()> {
let handle = self.handle.take();
drop(self.input);
match handle {
Some(h) => h.join(),
None => Ok(()),
}
}
}
#[cfg(test)]
#[allow(clippy::cast_precision_loss)] mod tests {
use super::*;
use crate::encoder::{AudioEncoder, OpusEncoder};
const FRAME_SIZE: usize = 960;
const SAMPLE_RATE: u32 = 48_000;
const POLL_CAP: usize = 2000;
const POLL_SLEEP: std::time::Duration = std::time::Duration::from_millis(1);
fn sine_440_mono() -> Vec<f32> {
let step = 2.0 * std::f32::consts::PI * 440.0 / SAMPLE_RATE as f32;
(0..FRAME_SIZE)
.map(|i| 0.5 * (i as f32 * step).sin())
.collect()
}
fn drain_n<T>(cons: &mut rtrb::Consumer<T>, want: usize) -> Vec<T> {
let mut out = Vec::with_capacity(want);
for _ in 0..POLL_CAP {
while let Ok(item) = cons.pop() {
out.push(item);
if out.len() == want {
return out;
}
}
std::thread::sleep(POLL_SLEEP);
}
while let Ok(item) = cons.pop() {
out.push(item);
}
assert_eq!(
out.len(),
want,
"expected {want} items from consumer, got {}",
out.len()
);
out
}
#[test]
fn decode_worker_round_trips_frames() {
let mut enc = OpusEncoder::new(SAMPLE_RATE, 1, opus::Application::Audio).unwrap();
let pcm = sine_440_mono();
let packets: Vec<Vec<u8>> = (0..8).map(|_| enc.encode(&pcm).unwrap()).collect();
let (mut worker, mut rt_cons) =
OpusDecodeWorker::new(SAMPLE_RATE, 1, 32).expect("decode worker spawns");
for pkt in packets {
assert!(
worker.submit_packet(pkt),
"submit must succeed while the ring has capacity"
);
}
let frames = drain_n(&mut rt_cons, 8);
for f in &frames {
assert_eq!(f.channels, 1, "decoded mono frame must have 1 channel");
assert_eq!(
f.num_frames(),
FRAME_SIZE,
"decoded frame must be the canonical 960-sample block"
);
}
worker.join().expect("worker must join cleanly");
}
#[test]
fn decode_worker_submit_returns_false_when_full() {
let (mut worker, _cons) =
OpusDecodeWorker::new(SAMPLE_RATE, 1, 2).expect("decode worker spawns");
let pkt = vec![0u8; 8];
let mut rejected = false;
for _ in 0..50 {
if !worker.submit_packet(pkt.clone()) {
rejected = true;
}
}
assert!(
rejected,
"at least one submit on a capacity-2 ring must signal backpressure"
);
}
#[test]
fn decode_worker_join_terminates() {
let (mut worker, _cons) =
OpusDecodeWorker::new(SAMPLE_RATE, 1, 4).expect("decode worker spawns");
let mut enc = OpusEncoder::new(SAMPLE_RATE, 1, opus::Application::Audio).unwrap();
let pkt = enc.encode(&sine_440_mono()).unwrap();
let _ = worker.submit_packet(pkt);
let res = worker.join();
assert!(
res.is_ok(),
"join must return Ok(()) for a clean shutdown, got {res:?}"
);
}
#[test]
fn encode_worker_round_trips_packets() {
let (mut worker, mut rt_cons) =
OpusEncodeWorker::new(SAMPLE_RATE, 2, opus::Application::Audio, 16)
.expect("encode worker spawns");
for _ in 0..4 {
let frame = AudioFrame::silence(2, FRAME_SIZE, SAMPLE_RATE);
assert!(
worker.submit_frame(frame),
"submit must succeed while the ring has capacity"
);
}
let packets = drain_n(&mut rt_cons, 4);
for p in &packets {
assert!(!p.is_empty(), "encoded packet must not be empty");
}
worker.join().expect("encoder must join cleanly");
}
}