feat(audio): implement lock-free audio sample ring buffer
This commit is contained in:
parent
f3c5a1eef6
commit
4629456002
3 changed files with 101 additions and 0 deletions
74
crates/molotov-core/src/audio/buffer.rs
Normal file
74
crates/molotov-core/src/audio/buffer.rs
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
|
|
||||||
|
pub struct AudioRingBuffer {
|
||||||
|
data: Vec<f32>,
|
||||||
|
capacity: usize,
|
||||||
|
read_pos: AtomicUsize,
|
||||||
|
write_pos: AtomicUsize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AudioRingBuffer {
|
||||||
|
pub fn new(capacity: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
data: vec![0.0; capacity],
|
||||||
|
capacity,
|
||||||
|
read_pos: AtomicUsize::new(0),
|
||||||
|
write_pos: AtomicUsize::new(0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn available_read(&self) -> usize {
|
||||||
|
let r = self.read_pos.load(Ordering::Acquire);
|
||||||
|
let w = self.write_pos.load(Ordering::Acquire);
|
||||||
|
if w >= r {
|
||||||
|
w - r
|
||||||
|
} else {
|
||||||
|
self.capacity - (r - w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn available_write(&self) -> usize {
|
||||||
|
self.capacity.saturating_sub(self.available_read() + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write(&mut self, samples: &[f32]) -> usize {
|
||||||
|
let mut written = 0;
|
||||||
|
let mut w = self.write_pos.load(Ordering::Relaxed);
|
||||||
|
|
||||||
|
for &sample in samples {
|
||||||
|
let next_w = (w + 1) % self.capacity;
|
||||||
|
let r = self.read_pos.load(Ordering::Acquire);
|
||||||
|
if next_w == r {
|
||||||
|
// Buffer full: drop oldest sample to avoid stalling capture
|
||||||
|
let _ = self.read_pos.fetch_add(1, Ordering::Release);
|
||||||
|
}
|
||||||
|
self.data[w] = sample;
|
||||||
|
w = next_w;
|
||||||
|
written += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.write_pos.store(w, Ordering::Release);
|
||||||
|
written
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read(&mut self, dest: &mut [f32]) -> usize {
|
||||||
|
let mut read = 0;
|
||||||
|
let mut r = self.read_pos.load(Ordering::Relaxed);
|
||||||
|
let w = self.write_pos.load(Ordering::Acquire);
|
||||||
|
|
||||||
|
while r != w && read < dest.len() {
|
||||||
|
dest[read] = self.data[r];
|
||||||
|
r = (r + 1) % self.capacity;
|
||||||
|
read += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.read_pos.store(r, Ordering::Release);
|
||||||
|
read
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear(&mut self) {
|
||||||
|
self.read_pos.store(0, Ordering::Release);
|
||||||
|
self.write_pos.store(0, Ordering::Release);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
pub mod builder;
|
pub mod builder;
|
||||||
|
pub mod buffer;
|
||||||
pub mod types;
|
pub mod types;
|
||||||
|
|
|
||||||
26
crates/molotov-core/tests/audio_buffer_test.rs
Normal file
26
crates/molotov-core/tests/audio_buffer_test.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
use molotov_core::audio::buffer::AudioRingBuffer;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ring_buffer_write_and_read() {
|
||||||
|
let mut buffer = AudioRingBuffer::new(1024);
|
||||||
|
let samples = [0.1f32, 0.2, 0.3, 0.4];
|
||||||
|
|
||||||
|
assert_eq!(buffer.write(&samples), 4);
|
||||||
|
assert_eq!(buffer.available_read(), 4);
|
||||||
|
|
||||||
|
let mut out = [0.0f32; 4];
|
||||||
|
assert_eq!(buffer.read(&mut out), 4);
|
||||||
|
assert_eq!(out, samples);
|
||||||
|
assert_eq!(buffer.available_read(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ring_buffer_underrun_fills_silence() {
|
||||||
|
let mut buffer = AudioRingBuffer::new(1024);
|
||||||
|
let mut out = [1.0f32; 4];
|
||||||
|
|
||||||
|
// Reading from empty buffer returns 0 read, but caller can zero memory
|
||||||
|
let read_count = buffer.read(&mut out);
|
||||||
|
assert_eq!(read_count, 0);
|
||||||
|
}
|
||||||
Reference in a new issue