fix(audio): harden realtime callback paths
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crossbeam::queue::ArrayQueue;
|
||||
|
||||
/// Fixed-capacity PCM handoff from the Android render producer task to
|
||||
/// the Oboe output callback.
|
||||
pub(crate) struct AndroidRenderRing {
|
||||
frames: Arc<ArrayQueue<[f32; 2]>>,
|
||||
}
|
||||
|
||||
impl AndroidRenderRing {
|
||||
pub(crate) fn new(capacity: usize) -> Self {
|
||||
Self {
|
||||
frames: Arc::new(ArrayQueue::new((capacity / 2).max(1))),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn producer(&self) -> AndroidRenderRingProducer {
|
||||
AndroidRenderRingProducer {
|
||||
frames: Arc::clone(&self.frames),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn consumer(&self) -> AndroidRenderRingConsumer {
|
||||
AndroidRenderRingConsumer {
|
||||
frames: Arc::clone(&self.frames),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct AndroidRenderRingProducer {
|
||||
frames: Arc<ArrayQueue<[f32; 2]>>,
|
||||
}
|
||||
|
||||
impl AndroidRenderRingProducer {
|
||||
pub(crate) fn push_frame_lossy(&self, samples: &[f32]) {
|
||||
for frame in samples.chunks_exact(2) {
|
||||
let stereo_frame = [frame[0], frame[1]];
|
||||
if self.frames.push(stereo_frame).is_err() {
|
||||
let _ = self.frames.pop();
|
||||
let _ = self.frames.push(stereo_frame);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct AndroidRenderRingConsumer {
|
||||
frames: Arc<ArrayQueue<[f32; 2]>>,
|
||||
}
|
||||
|
||||
impl AndroidRenderRingConsumer {
|
||||
pub(crate) fn drain_into_zero_filling(&self, out: &mut [f32]) {
|
||||
let mut chunks = out.chunks_exact_mut(2);
|
||||
for frame_out in &mut chunks {
|
||||
let frame = self.frames.pop().unwrap_or([0.0, 0.0]);
|
||||
frame_out.copy_from_slice(&frame);
|
||||
}
|
||||
for sample in chunks.into_remainder() {
|
||||
*sample = 0.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn producer_drops_oldest_samples_when_ring_is_full() {
|
||||
let ring = AndroidRenderRing::new(4);
|
||||
let producer = ring.producer();
|
||||
let consumer = ring.consumer();
|
||||
|
||||
producer.push_frame_lossy(&[1.0, 2.0, 3.0, 4.0]);
|
||||
producer.push_frame_lossy(&[5.0, 6.0]);
|
||||
|
||||
let mut out = [0.0; 4];
|
||||
consumer.drain_into_zero_filling(&mut out);
|
||||
|
||||
assert_eq!(out, [3.0, 4.0, 5.0, 6.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overflow_after_partial_consumer_drain_preserves_stereo_pairing() {
|
||||
let ring = AndroidRenderRing::new(4);
|
||||
let producer = ring.producer();
|
||||
let consumer = ring.consumer();
|
||||
|
||||
producer.push_frame_lossy(&[1.0, 10.0, 2.0, 20.0]);
|
||||
|
||||
let mut odd_out = [9.0];
|
||||
consumer.drain_into_zero_filling(&mut odd_out);
|
||||
assert_eq!(odd_out, [0.0]);
|
||||
|
||||
producer.push_frame_lossy(&[3.0, 30.0]);
|
||||
|
||||
let mut out = [0.0; 4];
|
||||
consumer.drain_into_zero_filling(&mut out);
|
||||
|
||||
assert_eq!(out, [2.0, 20.0, 3.0, 30.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn consumer_zero_fills_tail_on_underrun() {
|
||||
let ring = AndroidRenderRing::new(4);
|
||||
let producer = ring.producer();
|
||||
let consumer = ring.consumer();
|
||||
|
||||
producer.push_frame_lossy(&[0.25, -0.25]);
|
||||
|
||||
let mut out = [9.0; 4];
|
||||
consumer.drain_into_zero_filling(&mut out);
|
||||
|
||||
assert_eq!(out, [0.25, -0.25, 0.0, 0.0]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user