From 462945600263467da9b69d8b6a7511663ac59d39 Mon Sep 17 00:00:00 2001 From: oiupoyt Date: Tue, 29 Sep 2026 21:10:02 +0530 Subject: [PATCH] feat(audio): implement lock-free audio sample ring buffer --- crates/molotov-core/src/audio/buffer.rs | 74 +++++++++++++++++++ crates/molotov-core/src/audio/mod.rs | 1 + .../molotov-core/tests/audio_buffer_test.rs | 26 +++++++ 3 files changed, 101 insertions(+) create mode 100644 crates/molotov-core/src/audio/buffer.rs create mode 100644 crates/molotov-core/tests/audio_buffer_test.rs diff --git a/crates/molotov-core/src/audio/buffer.rs b/crates/molotov-core/src/audio/buffer.rs new file mode 100644 index 0000000..e468b17 --- /dev/null +++ b/crates/molotov-core/src/audio/buffer.rs @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +use std::sync::atomic::{AtomicUsize, Ordering}; + +pub struct AudioRingBuffer { + data: Vec, + 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); + } +} diff --git a/crates/molotov-core/src/audio/mod.rs b/crates/molotov-core/src/audio/mod.rs index 1bb4555..7337a58 100644 --- a/crates/molotov-core/src/audio/mod.rs +++ b/crates/molotov-core/src/audio/mod.rs @@ -1,3 +1,4 @@ // SPDX-License-Identifier: GPL-3.0-or-later pub mod builder; +pub mod buffer; pub mod types; diff --git a/crates/molotov-core/tests/audio_buffer_test.rs b/crates/molotov-core/tests/audio_buffer_test.rs new file mode 100644 index 0000000..31d2beb --- /dev/null +++ b/crates/molotov-core/tests/audio_buffer_test.rs @@ -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); +}