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
|
||||
pub mod builder;
|
||||
pub mod buffer;
|
||||
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