diff --git a/Cargo.lock b/Cargo.lock index eb7cc20..a906f85 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,7 +1,25 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 + +[[package]] +name = "libc" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + +[[package]] +name = "memmap2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" +dependencies = [ + "libc", +] [[package]] name = "multitap" version = "0.1.0" +dependencies = [ + "memmap2", +] diff --git a/Cargo.toml b/Cargo.toml index ad9a1e3..7c260e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,3 +4,6 @@ version = "0.1.0" edition = "2021" [dependencies] + +[dev-dependencies] +memmap2 = "0.9" diff --git a/examples/audio_delay.rs b/examples/audio_delay.rs new file mode 100644 index 0000000..4f4ddbb --- /dev/null +++ b/examples/audio_delay.rs @@ -0,0 +1,103 @@ +//! Audio Delay Example - Process raw audio from stdin to stdout +//! +//! This example demonstrates a practical audio delay effect. +//! It reads raw f32 audio samples from stdin and writes processed audio to stdout. +//! +//! Usage: +//! # Generate test signal, process, and play +//! sox -n -r 48000 -c 1 -t f32 - synth 0.5 sine 440 | \ +//! cargo run --example audio_delay -- 250 0.3 0.5 | \ +//! play -t f32 -r 48000 -c 1 - +//! +//! # Or save to file +//! sox -n -r 48000 -c 1 -t f32 - synth 1 sine 440 | \ +//! cargo run --example audio_delay -- 250 0.3 0.5 > output.raw +//! +//! Arguments: +//! delay_ms - Delay time in milliseconds (default: 250) +//! feedback - Feedback amount 0.0-1.0 (default: 0.3) +//! wet_mix - Wet/dry mix 0.0-1.0 (default: 0.5) + +use multitap::{RingBuffer, ArrayBackend}; +use std::io::{self, Read, Write}; + +const SAMPLE_RATE: usize = 48000; // 48kHz + +fn main() -> io::Result<()> { + // Parse command line arguments + let args: Vec = std::env::args().collect(); + + let delay_ms: f32 = args.get(1) + .and_then(|s| s.parse().ok()) + .unwrap_or(250.0); + + let feedback: f32 = args.get(2) + .and_then(|s| s.parse::().ok()) + .unwrap_or(0.3) + .clamp(0.0, 1.0); + + let wet_mix: f32 = args.get(3) + .and_then(|s| s.parse::().ok()) + .unwrap_or(0.5) + .clamp(0.0, 1.0); + + let delay_samples = ((SAMPLE_RATE as f32 * delay_ms) / 1000.0) as usize; + + // Print config to stderr (so it doesn't pollute stdout) + eprintln!("Audio Delay Effect"); + eprintln!(" Sample Rate: {} Hz", SAMPLE_RATE); + eprintln!(" Delay Time: {:.1} ms ({} samples)", delay_ms, delay_samples); + eprintln!(" Feedback: {:.1}%", feedback * 100.0); + eprintln!(" Wet/Dry Mix: {:.1}%", wet_mix * 100.0); + eprintln!(); + eprintln!("Reading from stdin, writing to stdout..."); + + // Create delay buffer (16384 samples = ~341ms at 48kHz) + let mut delay_buffer = RingBuffer::new(ArrayBackend::::new()); + let mut delay_tap = delay_buffer.get_read_handle(None); + + // Read/write buffers + let mut input_bytes = [0u8; 4]; // f32 is 4 bytes + let mut sample_count = 0usize; + + let stdin = io::stdin(); + let stdout = io::stdout(); + let mut stdin_handle = stdin.lock(); + let mut stdout_handle = stdout.lock(); + + loop { + // Read one f32 sample from stdin + match stdin_handle.read_exact(&mut input_bytes) { + Ok(_) => {}, + Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => break, + Err(e) => return Err(e), + } + + let input = f32::from_le_bytes(input_bytes); + + // Read delayed signal (if buffer has enough samples) + let delayed = if sample_count >= delay_samples { + delay_tap.read(&delay_buffer).next() + } else { + 0.0 // Buffer still filling + }; + + // Apply feedback: mix input with delayed signal + let to_buffer = input + (delayed * feedback); + delay_buffer.write().push(to_buffer); + + // Mix wet (delayed) and dry (input) signals + let output = input * (1.0 - wet_mix) + delayed * wet_mix; + + // Write output sample to stdout + stdout_handle.write_all(&output.to_le_bytes())?; + + sample_count += 1; + } + + eprintln!("Processed {} samples ({:.2} seconds)", + sample_count, + sample_count as f32 / SAMPLE_RATE as f32); + + Ok(()) +} diff --git a/examples/basic_vec.rs b/examples/basic_vec.rs new file mode 100644 index 0000000..36c89dd --- /dev/null +++ b/examples/basic_vec.rs @@ -0,0 +1,73 @@ +//! Basic example: Using a Vec-backed ring buffer +//! +//! This example demonstrates how to use SliceBackendRuntime with a Vec +//! for heap-allocated buffers. This is useful when: +//! - Buffer size is determined at runtime +//! - Buffer is too large for stack allocation +//! - You're running on a system with std and heap allocation + +use multitap::{RingBuffer, SliceBackendRuntime}; + +fn main() { + // Create a large buffer on the heap (1 million samples) + // This would be too large for stack allocation + let buffer_size = 1_000_000; + let mut vec_storage = vec![0.0f32; buffer_size]; + + // Create a ring buffer using the Vec's slice + // The Vec owns the memory, SliceBackendRuntime just stores the address + let mut buffer = unsafe { + RingBuffer::new(SliceBackendRuntime::from_slice(&mut vec_storage)) + }; + + println!("Created ring buffer with {} samples", buffer.len()); + + // Write some samples + { + let mut writer = buffer.write(); + for i in 0..10 { + writer.push((i as f32) * 0.1); + } + } + + println!("Wrote 10 samples to the buffer"); + + // Create a read handle at the beginning + let mut read_handle = buffer.get_read_handle(Some(0)); + + // Read back the samples + println!("\nReading samples:"); + { + let mut reader = read_handle.read(&buffer); + for i in 0..10 { + let sample = reader.next(); + println!(" Sample {}: {:.1}", i, sample); + } + } + + // Demonstrate delay effect: write input, read delayed output + println!("\nDelay effect demonstration:"); + println!("(writing continuous samples, reading with 5 sample delay)\n"); + + // Create a delay tap 5 samples behind the write head + let mut delay_tap = buffer.get_read_handle(None); + + // Simulate processing 20 frames + for frame in 0..20 { + let input = (frame as f32) * 0.5; + + // Write input + buffer.write().push(input); + + // Read delayed output (after 5 samples have been written) + if frame >= 5 { + let delayed = delay_tap.read(&buffer).next(); + println!("Frame {}: input={:.1}, delayed={:.1}", frame, input, delayed); + } else { + println!("Frame {}: input={:.1}, delayed=", frame, input); + } + } + + // The Vec owns the memory and will be cleaned up when it goes out of scope + println!("\nBuffer will be automatically cleaned up when Vec is dropped"); +} diff --git a/examples/lowpass_filter.rs b/examples/lowpass_filter.rs new file mode 100644 index 0000000..f1e17af --- /dev/null +++ b/examples/lowpass_filter.rs @@ -0,0 +1,134 @@ +//! Low-Pass Filter Example - FIR convolution using ring buffer +//! +//! This example demonstrates a Finite Impulse Response (FIR) low-pass filter +//! implemented using a ring buffer for efficient convolution. +//! +//! A low-pass filter attenuates high frequencies while passing low frequencies. +//! This is useful for removing noise, smoothing signals, or anti-aliasing. +//! +//! Usage: +//! # Process audio file with low-pass filter +//! sox input.wav -r 48000 -c 1 -t f32 - | \ +//! cargo run --example lowpass_filter -- 1000 | \ +//! play -t f32 -r 48000 -c 1 - +//! +//! # Try with a system sound +//! sox /System/Library/Sounds/Glass.aiff -r 48000 -c 1 -t f32 - | \ +//! cargo run --example lowpass_filter -- 2000 | \ +//! play -t f32 -r 48000 -c 1 - +//! +//! Arguments: +//! cutoff_hz - Cutoff frequency in Hz (default: 1000) + +use multitap::{RingBuffer, ArrayBackend}; +use std::io::{self, Read, Write}; +use std::f32::consts::PI; + +const SAMPLE_RATE: f32 = 48000.0; +const FILTER_TAPS: usize = 64; // Filter order + +/// Generate a windowed sinc FIR low-pass filter +fn generate_lowpass_kernel(cutoff_hz: f32, num_taps: usize) -> Vec { + let mut kernel = Vec::with_capacity(num_taps); + let fc = cutoff_hz / SAMPLE_RATE; // Normalized cutoff frequency + let center = (num_taps - 1) as f32 / 2.0; + + for i in 0..num_taps { + let x = i as f32 - center; + + // Sinc function: sin(2πfcx) / (πx) + let h = if x.abs() < 1e-6 { + 2.0 * fc // Limit as x approaches 0 + } else { + (2.0 * PI * fc * x).sin() / (PI * x) + }; + + // Apply Hamming window to reduce ripple + let window = 0.54 - 0.46 * (2.0 * PI * i as f32 / (num_taps - 1) as f32).cos(); + + kernel.push(h * window); + } + + // Normalize kernel so DC gain = 1 + let sum: f32 = kernel.iter().sum(); + for k in &mut kernel { + *k /= sum; + } + + kernel +} + +fn main() -> io::Result<()> { + // Parse command line arguments + let args: Vec = std::env::args().collect(); + let cutoff_hz: f32 = args.get(1) + .and_then(|s| s.parse::().ok()) + .unwrap_or(1000.0) + .clamp(20.0, SAMPLE_RATE / 2.0); + + // Generate filter kernel + let kernel = generate_lowpass_kernel(cutoff_hz, FILTER_TAPS); + + eprintln!("Low-Pass FIR Filter"); + eprintln!(" Sample Rate: {} Hz", SAMPLE_RATE); + eprintln!(" Cutoff Frequency: {:.0} Hz", cutoff_hz); + eprintln!(" Filter Taps: {}", FILTER_TAPS); + eprintln!(); + eprintln!("Reading from stdin, writing to stdout..."); + + // Create ring buffer to hold input sample history + let mut sample_buffer = RingBuffer::new(ArrayBackend::::new()); + + // Initialize buffer with zeros (for proper filter startup) + for _ in 0..FILTER_TAPS { + sample_buffer.write().push(0.0); + } + + // Create read handle at the oldest sample position + let mut read_handle = sample_buffer.get_read_handle(Some(0)); + + let mut input_bytes = [0u8; 4]; + let mut sample_count = 0usize; + + let stdin = io::stdin(); + let stdout = io::stdout(); + let mut stdin_handle = stdin.lock(); + let mut stdout_handle = stdout.lock(); + + loop { + // Read one f32 sample from stdin + match stdin_handle.read_exact(&mut input_bytes) { + Ok(_) => {}, + Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => break, + Err(e) => return Err(e), + } + + let input = f32::from_le_bytes(input_bytes); + + // Write new sample to ring buffer + sample_buffer.write().push(input); + + // Perform convolution: sum of (sample * kernel) + let mut output = 0.0; + { + // Get iterator over the last FILTER_TAPS samples + let mut iter = read_handle.iter(&sample_buffer, FILTER_TAPS); + + // Convolve with filter kernel + for (i, sample) in (&mut iter).enumerate() { + output += sample * kernel[i]; + } + } + + // Write filtered output + stdout_handle.write_all(&output.to_le_bytes())?; + + sample_count += 1; + } + + eprintln!("Processed {} samples ({:.2} seconds)", + sample_count, + sample_count as f32 / SAMPLE_RATE); + + Ok(()) +} diff --git a/examples/mmap_delay.rs b/examples/mmap_delay.rs new file mode 100644 index 0000000..2ae85f0 --- /dev/null +++ b/examples/mmap_delay.rs @@ -0,0 +1,187 @@ +//! Memory-Mapped File Delay Example +//! +//! This example demonstrates: +//! 1. How to create a custom BufferBackend (MmapBackend) +//! 2. Using memory-mapped files for very large delay buffers +//! 3. The OS handling paging between RAM and disk for buffers larger than physical RAM +//! +//! Usage: +//! sox /System/Library/Sounds/Glass.aiff -r 48000 -c 1 -t f32 - | \ +//! cargo run --example mmap_delay -- 5000 | \ +//! play -t f32 -r 48000 -c 1 - +//! +//! Arguments: +//! delay_ms - Delay time in milliseconds (default: 5000) + +use multitap::{BufferBackend, Num, RingBuffer}; +use memmap2::MmapMut; +use std::fs::{self, OpenOptions}; +use std::io::{self, Read, Write}; +use std::path::Path; + +// Custom backend implementation using memory-mapped files +pub struct MmapBackend { + mmap: MmapMut, + capacity: usize, + _phantom: std::marker::PhantomData, +} + +impl MmapBackend { + pub fn create>(path: P, capacity: usize) -> io::Result { + let size = capacity * std::mem::size_of::(); + + let file = OpenOptions::new() + .read(true) + .write(true) + .create(true) + .truncate(true) + .open(path)?; + + file.set_len(size as u64)?; + let mmap = unsafe { MmapMut::map_mut(&file)? }; + + Ok(MmapBackend { + mmap, + capacity, + _phantom: std::marker::PhantomData, + }) + } + + fn as_typed_slice(&self) -> &[T] { + unsafe { + std::slice::from_raw_parts( + self.mmap.as_ptr() as *const T, + self.capacity + ) + } + } + + fn as_typed_slice_mut(&mut self) -> &mut [T] { + unsafe { + std::slice::from_raw_parts_mut( + self.mmap.as_mut_ptr() as *mut T, + self.capacity + ) + } + } +} + +// Implement the BufferBackend trait for our custom backend +unsafe impl BufferBackend for MmapBackend { + fn capacity(&self) -> usize { + self.capacity + } + + fn get(&self, index: usize) -> T { + self.as_typed_slice()[index] + } + + fn set(&mut self, index: usize, value: T) { + self.as_typed_slice_mut()[index] = value; + } + + fn as_slice(&self) -> &[T] { + self.as_typed_slice() + } + + fn as_mut_slice(&mut self) -> &mut [T] { + self.as_typed_slice_mut() + } + + fn clear(&mut self) { + let slice = self.as_typed_slice_mut(); + for i in 0..slice.len() { + slice[i] = T::default_value(); + } + } +} + +unsafe impl Send for MmapBackend {} + +const SAMPLE_RATE: usize = 48000; +const MMAP_FILE: &str = "/tmp/multitap_delay.mmap"; + +fn main() -> io::Result<()> { + let args: Vec = std::env::args().collect(); + + let delay_ms: f32 = args.get(1) + .and_then(|s| s.parse().ok()) + .unwrap_or(5000.0); + + let delay_samples = ((SAMPLE_RATE as f32 * delay_ms) / 1000.0) as usize; + + // We need a buffer at least as large as the delay + // Round up to next power of 2 for efficiency + let buffer_size = delay_samples.next_power_of_two(); + + eprintln!("Memory-Mapped Delay Effect"); + eprintln!(" Sample Rate: {} Hz", SAMPLE_RATE); + eprintln!(" Delay Time: {:.1} ms ({} samples)", delay_ms, delay_samples); + eprintln!(" Buffer Size: {} samples ({:.1} MB)", + buffer_size, + (buffer_size * 4) as f32 / 1_048_576.0); + eprintln!(" Mmap File: {}", MMAP_FILE); + eprintln!(); + + // Create fresh mmap file + eprintln!("Creating memory-mapped buffer..."); + let backend = MmapBackend::::create(MMAP_FILE, buffer_size)?; + + let mut buffer = RingBuffer::new(backend); + let mut delay_tap = buffer.get_read_handle(None); + + eprintln!(); + eprintln!("Reading from stdin, writing to stdout..."); + eprintln!("Press Ctrl+C to stop"); + eprintln!(); + + let feedback = 0.5; + let wet_mix = 0.5; + + let mut input_bytes = [0u8; 4]; + let mut sample_count = 0usize; + + let stdin = io::stdin(); + let stdout = io::stdout(); + let mut stdin_handle = stdin.lock(); + let mut stdout_handle = stdout.lock(); + + loop { + // Read one f32 sample from stdin + match stdin_handle.read_exact(&mut input_bytes) { + Ok(_) => {}, + Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => break, + Err(e) => return Err(e), + } + + let input = f32::from_le_bytes(input_bytes); + + // Read delayed signal + let delayed = if sample_count >= delay_samples { + delay_tap.read(&buffer).next() + } else { + 0.0 + }; + + // Apply feedback + let to_buffer = input + (delayed * feedback); + buffer.write().push(to_buffer); + + // Mix wet and dry + let output = input * (1.0 - wet_mix) + delayed * wet_mix; + + stdout_handle.write_all(&output.to_le_bytes())?; + + sample_count += 1; + } + + eprintln!("Processed {} samples ({:.2} seconds)", + sample_count, + sample_count as f32 / SAMPLE_RATE as f32); + + // Clean up the mmap file + fs::remove_file(MMAP_FILE)?; + eprintln!("Cleaned up temporary mmap file"); + + Ok(()) +} diff --git a/examples/stereo_delay.rs b/examples/stereo_delay.rs new file mode 100644 index 0000000..03e7ae0 --- /dev/null +++ b/examples/stereo_delay.rs @@ -0,0 +1,175 @@ +//! Stereo Delay Example - Multi-channel processing +//! +//! This example demonstrates processing stereo (2-channel) audio with +//! independent delay lines for each channel, creating a spacious stereo effect. +//! +//! Features: +//! - Independent delay times for left and right channels +//! - Cross-feedback between channels for wider stereo image +//! - Demonstrates multi-channel buffer management +//! +//! Usage: +//! # Process stereo file +//! sox input.wav -r 48000 -c 2 -t f32 - | \ +//! cargo run --example stereo_delay -- 200 300 0.3 0.6 | \ +//! play -t f32 -r 48000 -c 2 - +//! +//! # Generate stereo test signal and process +//! sox -n -r 48000 -c 2 -t f32 - synth 1 sine 440 sine 880 | \ +//! cargo run --example stereo_delay -- 150 250 0.4 0.5 | \ +//! play -t f32 -r 48000 -c 2 - +//! +//! Arguments: +//! left_delay_ms - Left channel delay in ms (default: 200) +//! right_delay_ms - Right channel delay in ms (default: 300) +//! feedback - Feedback amount 0.0-1.0 (default: 0.3) +//! wet_mix - Wet/dry mix 0.0-1.0 (default: 0.5) + +use multitap::{RingBuffer, ArrayBackend}; +use std::io::{self, Read, Write}; + +const SAMPLE_RATE: usize = 48000; + +struct StereoDelayProcessor { + left_buffer: RingBuffer>, + right_buffer: RingBuffer>, + left_tap: multitap::ReadHandle, + right_tap: multitap::ReadHandle, + feedback: f32, + wet_mix: f32, +} + +impl StereoDelayProcessor { + fn new(feedback: f32, wet_mix: f32) -> Self { + let left_buffer = RingBuffer::new(ArrayBackend::::new()); + let right_buffer = RingBuffer::new(ArrayBackend::::new()); + + // Create delay taps at the current write position + let left_tap = left_buffer.get_read_handle(None); + let right_tap = right_buffer.get_read_handle(None); + + StereoDelayProcessor { + left_buffer, + right_buffer, + left_tap, + right_tap, + feedback, + wet_mix, + } + } + + fn process_frame(&mut self, left_in: f32, right_in: f32, sample_idx: usize, + left_delay: usize, right_delay: usize) -> (f32, f32) { + // Read delayed samples + let left_delayed = if sample_idx >= left_delay { + self.left_tap.read(&self.left_buffer).next() + } else { + 0.0 + }; + + let right_delayed = if sample_idx >= right_delay { + self.right_tap.read(&self.right_buffer).next() + } else { + 0.0 + }; + + // Cross-feedback: left delay gets some right signal and vice versa + // This creates a wider stereo image + let cross_amount = 0.3; + let left_to_buffer = left_in + + (left_delayed * self.feedback * (1.0 - cross_amount)) + + (right_delayed * self.feedback * cross_amount); + + let right_to_buffer = right_in + + (right_delayed * self.feedback * (1.0 - cross_amount)) + + (left_delayed * self.feedback * cross_amount); + + // Write to buffers + self.left_buffer.write().push(left_to_buffer); + self.right_buffer.write().push(right_to_buffer); + + // Mix wet and dry signals + let left_out = left_in * (1.0 - self.wet_mix) + left_delayed * self.wet_mix; + let right_out = right_in * (1.0 - self.wet_mix) + right_delayed * self.wet_mix; + + (left_out, right_out) + } +} + +fn main() -> io::Result<()> { + let args: Vec = std::env::args().collect(); + + let left_delay_ms: f32 = args.get(1) + .and_then(|s| s.parse().ok()) + .unwrap_or(200.0); + + let right_delay_ms: f32 = args.get(2) + .and_then(|s| s.parse().ok()) + .unwrap_or(300.0); + + let feedback: f32 = args.get(3) + .and_then(|s| s.parse::().ok()) + .unwrap_or(0.3) + .clamp(0.0, 0.9); + + let wet_mix: f32 = args.get(4) + .and_then(|s| s.parse::().ok()) + .unwrap_or(0.5) + .clamp(0.0, 1.0); + + let left_delay_samples = ((SAMPLE_RATE as f32 * left_delay_ms) / 1000.0) as usize; + let right_delay_samples = ((SAMPLE_RATE as f32 * right_delay_ms) / 1000.0) as usize; + + eprintln!("Stereo Delay Effect"); + eprintln!(" Sample Rate: {} Hz", SAMPLE_RATE); + eprintln!(" Left Channel Delay: {:.1} ms ({} samples)", left_delay_ms, left_delay_samples); + eprintln!(" Right Channel Delay: {:.1} ms ({} samples)", right_delay_ms, right_delay_samples); + eprintln!(" Feedback: {:.1}%", feedback * 100.0); + eprintln!(" Wet/Dry Mix: {:.1}%", wet_mix * 100.0); + eprintln!(" Cross-feedback: 30% (for stereo width)"); + eprintln!(); + eprintln!("Reading stereo audio from stdin, writing to stdout..."); + + let mut processor = StereoDelayProcessor::new(feedback, wet_mix); + + let mut input_bytes = [0u8; 8]; // 2 channels * 4 bytes per f32 + let mut sample_count = 0usize; + + let stdin = io::stdin(); + let stdout = io::stdout(); + let mut stdin_handle = stdin.lock(); + let mut stdout_handle = stdout.lock(); + + loop { + // Read one stereo frame (2 f32 samples) + match stdin_handle.read_exact(&mut input_bytes) { + Ok(_) => {}, + Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => break, + Err(e) => return Err(e), + } + + let left_in = f32::from_le_bytes([input_bytes[0], input_bytes[1], input_bytes[2], input_bytes[3]]); + let right_in = f32::from_le_bytes([input_bytes[4], input_bytes[5], input_bytes[6], input_bytes[7]]); + + // Process the stereo frame + let (left_out, right_out) = processor.process_frame( + left_in, + right_in, + sample_count, + left_delay_samples, + right_delay_samples, + ); + + // Write output frame + stdout_handle.write_all(&left_out.to_le_bytes())?; + stdout_handle.write_all(&right_out.to_le_bytes())?; + + sample_count += 1; + } + + eprintln!("Processed {} stereo frames ({:.2} seconds)", + sample_count, + sample_count as f32 / SAMPLE_RATE as f32); + + Ok(()) +} diff --git a/src/array_backend.rs b/src/array_backend.rs new file mode 100644 index 0000000..4b96e28 --- /dev/null +++ b/src/array_backend.rs @@ -0,0 +1,98 @@ +//! Array-based storage backend +//! +//! This backend uses a fixed-size stack-allocated array. +//! This is the most common choice for audio applications where +//! buffer size is known at compile time. + +use crate::{Num, BufferBackend}; + +/// Backend using a fixed-size array (stack-allocated) +pub struct ArrayBackend { + data: [T; N], +} + +impl ArrayBackend { + /// Create a new array backend with default values + pub fn new() -> Self { + ArrayBackend { + data: [T::default_value(); N], + } + } +} + +unsafe impl BufferBackend for ArrayBackend { + fn capacity(&self) -> usize { + N + } + + fn get(&self, index: usize) -> T { + self.data[index] + } + + fn set(&mut self, index: usize, value: T) { + self.data[index] = value; + } + + fn as_slice(&self) -> &[T] { + &self.data + } + + fn as_mut_slice(&mut self) -> &mut [T] { + &mut self.data + } + + fn clear(&mut self) { + self.data.fill(T::default_value()); + } +} + +unsafe impl Send for ArrayBackend {} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_new() { + let backend = ArrayBackend::::new(); + assert_eq!(backend.capacity(), 4); + assert_eq!(backend.get(0), 0.0); + } + + #[test] + fn test_get_set() { + let mut backend = ArrayBackend::::new(); + backend.set(0, 1.0); + backend.set(1, 2.0); + assert_eq!(backend.get(0), 1.0); + assert_eq!(backend.get(1), 2.0); + } + + #[test] + fn test_as_slice() { + let mut backend = ArrayBackend::::new(); + backend.set(0, 1.0); + backend.set(1, 2.0); + let slice = backend.as_slice(); + assert_eq!(slice.len(), 4); + assert_eq!(slice[0], 1.0); + assert_eq!(slice[1], 2.0); + } + + #[test] + fn test_clear() { + let mut backend = ArrayBackend::::new(); + backend.set(0, 1.0); + backend.set(1, 2.0); + backend.clear(); + assert_eq!(backend.get(0), 0.0); + assert_eq!(backend.get(1), 0.0); + } + + #[test] + fn test_i32_type() { + let mut backend = ArrayBackend::::new(); + backend.set(0, 42); + assert_eq!(backend.get(0), 42); + } +} diff --git a/src/backend.rs b/src/backend.rs new file mode 100644 index 0000000..57201aa --- /dev/null +++ b/src/backend.rs @@ -0,0 +1,37 @@ +//! Storage backend trait for ring buffers +//! +//! This module defines the `BufferBackend` trait that abstracts over +//! different storage mechanisms. + +use crate::Num; + +/// Trait for different storage backends (array, external memory, mmap, etc.) +/// +/// This trait abstracts over the actual storage mechanism, allowing a single +/// RingBuffer implementation to work with different memory sources. +/// +/// # Safety +/// +/// Implementors must ensure: +/// - `capacity()` returns the actual capacity of the storage +/// - `get()` and `set()` are valid for indices 0..capacity() +/// - `as_slice()` returns a valid slice of length `capacity()` +pub unsafe trait BufferBackend: Send { + /// Get the total capacity of the buffer + fn capacity(&self) -> usize; + + /// Get a value at the given index (must be < capacity) + fn get(&self, index: usize) -> T; + + /// Set a value at the given index (must be < capacity) + fn set(&mut self, index: usize, value: T); + + /// Get a slice view of the entire buffer + fn as_slice(&self) -> &[T]; + + /// Get a mutable slice view of the entire buffer + fn as_mut_slice(&mut self) -> &mut [T]; + + /// Clear the buffer to default values + fn clear(&mut self); +} diff --git a/src/guards.rs b/src/guards.rs new file mode 100644 index 0000000..8af8992 --- /dev/null +++ b/src/guards.rs @@ -0,0 +1,317 @@ +//! Write and read guards for safe buffer access +//! +//! Guards enforce exclusive write access and shared read access +//! through Rust's borrow checker. + +use core::ops::{Index, IndexMut}; +use crate::{Num, BufferBackend, RingBuffer}; + +/// Exclusive write access guard for RingBuffer +/// +/// This guard provides mutable access to the ring buffer for writing. +/// Only one WriteGuard can exist at a time, enforced by the borrow checker. +pub struct WriteGuard<'a, T: Num, B: BufferBackend> { + pub(crate) buffer: &'a mut RingBuffer, +} + +impl<'a, T: Num, B: BufferBackend> WriteGuard<'a, T, B> { + /// Push a single element and advance the write position + pub fn push(&mut self, element: T) { + self.buffer.backend.set(self.buffer.write_position, element); + self.increment(); + } + + /// Increment the write position + pub fn increment(&mut self) { + self.buffer.write_position = + (self.buffer.write_position + 1) % self.buffer.backend.capacity(); + } + + /// Seek to a specific write position + pub fn seek(&mut self, position: usize) { + self.buffer.write_position = if position > self.buffer.backend.capacity() { + 0 + } else { + position + }; + } +} + +impl<'a, T: Num, B: BufferBackend> Index for WriteGuard<'a, T, B> { + type Output = T; + fn index(&self, i: usize) -> &T { + let current_position = i % self.buffer.backend.capacity(); + // We need to return a reference, but backend.get() returns T (a copy) + // This is a fundamental limitation - we'll need to address this + // For now, we'll use as_slice which all backends must provide + &self.buffer.backend.as_slice()[current_position] + } +} + +impl<'a, T: Num, B: BufferBackend> IndexMut for WriteGuard<'a, T, B> { + fn index_mut(&mut self, i: usize) -> &mut T { + let current_position = i % self.buffer.backend.capacity(); + &mut self.buffer.backend.as_mut_slice()[current_position] + } +} + +/// Temporary read guard - borrows the buffer for reading +pub struct ReadGuard<'a, T: Num> { + pub(crate) buffer: &'a [T], + pub(crate) size: usize, + pub(crate) read_position: &'a mut usize, +} + +impl<'a, T: Num> ReadGuard<'a, T> { + /// Read the next sample and advance position + pub fn next(&mut self) -> T { + let sample = self.buffer[*self.read_position]; + *self.read_position = (*self.read_position + 1) % self.size; + sample + } + + /// Read at a specific offset without advancing position + pub fn read_at(&self, offset: usize) -> T { + let pos = offset % self.size; + self.buffer[pos] + } + + /// Get the current read position + pub fn position(&self) -> usize { + *self.read_position + } +} + +impl<'a, T: Num> Index for ReadGuard<'a, T> { + type Output = T; + fn index(&self, i: usize) -> &T { + let current_position = i % self.size; + &self.buffer[current_position] + } +} + +#[cfg(test)] +mod tests { + use crate::{RingBuffer, ArrayBackend}; + + #[test] + fn test_write_guard_push() { + let mut buffer = RingBuffer::new(ArrayBackend::::new()); + + { + let mut writer = buffer.write(); + writer.push(1.0); + writer.push(2.0); + writer.push(3.0); + } + + assert_eq!(buffer.write_position(), 3); + } + + #[test] + fn test_write_guard_increment() { + let mut buffer = RingBuffer::new(ArrayBackend::::new()); + + { + let mut writer = buffer.write(); + writer.increment(); + writer.increment(); + } + + assert_eq!(buffer.write_position(), 2); + } + + #[test] + fn test_write_guard_seek() { + let mut buffer = RingBuffer::new(ArrayBackend::::new()); + + { + let mut writer = buffer.write(); + writer.seek(2); + } + + assert_eq!(buffer.write_position(), 2); + + // Test seeking beyond capacity resets to 0 + { + let mut writer = buffer.write(); + writer.seek(10); + } + + assert_eq!(buffer.write_position(), 0); + } + + #[test] + fn test_write_guard_index() { + let mut buffer = RingBuffer::new(ArrayBackend::::new()); + + { + let mut writer = buffer.write(); + writer.push(1.0); + writer.push(2.0); + writer.push(3.0); + } + + { + let writer = buffer.write(); + assert_eq!(writer[0], 1.0); + assert_eq!(writer[1], 2.0); + assert_eq!(writer[2], 3.0); + } + } + + #[test] + fn test_write_guard_index_mut() { + let mut buffer = RingBuffer::new(ArrayBackend::::new()); + + { + let mut writer = buffer.write(); + writer[0] = 10.0; + writer[1] = 20.0; + } + + { + let writer = buffer.write(); + assert_eq!(writer[0], 10.0); + assert_eq!(writer[1], 20.0); + } + } + + #[test] + fn test_write_guard_index_wraps() { + let mut buffer = RingBuffer::new(ArrayBackend::::new()); + + { + let mut writer = buffer.write(); + writer[0] = 1.0; + writer[4] = 2.0; // wraps to index 0 + } + + { + let writer = buffer.write(); + assert_eq!(writer[0], 2.0); + } + } + + #[test] + fn test_read_guard_next() { + let mut buffer = RingBuffer::new(ArrayBackend::::new()); + + { + let mut writer = buffer.write(); + writer.push(1.0); + writer.push(2.0); + writer.push(3.0); + } + + let mut handle = buffer.get_read_handle(Some(0)); + let mut reader = handle.read(&buffer); + + assert_eq!(reader.next(), 1.0); + assert_eq!(reader.next(), 2.0); + assert_eq!(reader.next(), 3.0); + } + + #[test] + fn test_read_guard_read_at() { + let mut buffer = RingBuffer::new(ArrayBackend::::new()); + + { + let mut writer = buffer.write(); + writer.push(1.0); + writer.push(2.0); + writer.push(3.0); + } + + let mut handle = buffer.get_read_handle(Some(0)); + let reader = handle.read(&buffer); + + // read_at doesn't advance position + assert_eq!(reader.read_at(0), 1.0); + assert_eq!(reader.read_at(1), 2.0); + assert_eq!(reader.read_at(2), 3.0); + assert_eq!(reader.position(), 0); // Position unchanged + } + + #[test] + fn test_read_guard_position() { + let mut buffer = RingBuffer::new(ArrayBackend::::new()); + + { + let mut writer = buffer.write(); + writer.push(1.0); + writer.push(2.0); + } + + let mut handle = buffer.get_read_handle(Some(0)); + let mut reader = handle.read(&buffer); + + assert_eq!(reader.position(), 0); + reader.next(); + assert_eq!(reader.position(), 1); + reader.next(); + assert_eq!(reader.position(), 2); + } + + #[test] + fn test_read_guard_index() { + let mut buffer = RingBuffer::new(ArrayBackend::::new()); + + { + let mut writer = buffer.write(); + writer.push(1.0); + writer.push(2.0); + writer.push(3.0); + } + + let mut handle = buffer.get_read_handle(Some(0)); + let reader = handle.read(&buffer); + + assert_eq!(reader[0], 1.0); + assert_eq!(reader[1], 2.0); + assert_eq!(reader[2], 3.0); + } + + #[test] + fn test_read_guard_index_wraps() { + let mut buffer = RingBuffer::new(ArrayBackend::::new()); + + { + let mut writer = buffer.write(); + writer.push(1.0); + writer.push(2.0); + } + + let mut handle = buffer.get_read_handle(Some(0)); + let reader = handle.read(&buffer); + + assert_eq!(reader[0], 1.0); + assert_eq!(reader[1], 2.0); + assert_eq!(reader[2], 1.0); // wraps + assert_eq!(reader[3], 2.0); // wraps + } + + #[test] + fn test_cannot_write_while_reading() { + let mut buffer = RingBuffer::new(ArrayBackend::::new()); + + { + let mut writer = buffer.write(); + writer.push(1.0); + } + + let mut handle = buffer.get_read_handle(Some(0)); + let reader = handle.read(&buffer); + + // This would fail to compile: + // let mut writer = buffer.write(); // ERROR: cannot borrow mutably + + assert_eq!(reader[0], 1.0); + + drop(reader); + + // Now we can write again + let mut writer = buffer.write(); + writer.push(2.0); + } +} diff --git a/src/handles.rs b/src/handles.rs new file mode 100644 index 0000000..2c3b69d --- /dev/null +++ b/src/handles.rs @@ -0,0 +1,291 @@ +//! Read handles and iterators +//! +//! Read handles are persistent, owned types that track a read position +//! without borrowing the buffer. They can be stored and reused. + +use core::marker::PhantomData; +use crate::{Num, BufferBackend, RingBuffer, ReadGuard}; + +/// A persistent handle for reading from a specific position in the buffer +/// +/// This is owned and can be stored, but doesn't hold a reference to the buffer. +/// To actually read, you must create a `ReadGuard` by calling `read()`. +pub struct ReadHandle { + pub(crate) read_position: usize, + pub(crate) _phantom: PhantomData, +} + +unsafe impl Send for ReadHandle {} + +impl ReadHandle { + /// Seek to a specific read position + pub fn seek(&mut self, position: usize) { + self.read_position = position; + } + + /// Move the read position by a delta (can be negative) + pub fn delta(&mut self, position_delta: i64, buffer_size: usize) { + self.read_position = + ((self.read_position as i64 + position_delta) % buffer_size as i64) as usize; + } + + /// Get the current read position + pub fn position(&self) -> usize { + self.read_position + } + + /// Create a read guard for actual reading (borrows the buffer) + pub fn read<'a, B: BufferBackend>( + &'a mut self, + buffer: &'a RingBuffer, + ) -> ReadGuard<'a, T> { + self.read_position = self.read_position % buffer.len(); + ReadGuard { + buffer: buffer.backend.as_slice(), + size: buffer.len(), + read_position: &mut self.read_position, + } + } + + /// Iterator interface - creates a guard and returns an iterator + pub fn iter<'a, B: BufferBackend>( + &'a mut self, + buffer: &'a RingBuffer, + count: usize, + ) -> ReadIterator<'a, T> { + let guard = self.read(buffer); + ReadIterator { + guard, + remaining: count, + } + } +} + +/// Iterator that wraps a ReadGuard +pub struct ReadIterator<'a, T: Num> { + pub(crate) guard: ReadGuard<'a, T>, + pub(crate) remaining: usize, +} + +impl<'a, T: Num> Iterator for ReadIterator<'a, T> { + type Item = T; + + fn next(&mut self) -> Option { + if self.remaining == 0 { + None + } else { + self.remaining -= 1; + Some(self.guard.next()) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{RingBuffer, ArrayBackend}; + + #[test] + fn test_read_handle_seek() { + let mut buffer = RingBuffer::new(ArrayBackend::::new()); + + { + let mut writer = buffer.write(); + writer.push(1.0); + writer.push(2.0); + writer.push(3.0); + writer.push(4.0); + } + + let mut handle = buffer.get_read_handle(Some(0)); + assert_eq!(handle.position(), 0); + + handle.seek(2); + assert_eq!(handle.position(), 2); + + let mut reader = handle.read(&buffer); + assert_eq!(reader.next(), 3.0); + } + + #[test] + fn test_read_handle_delta_positive() { + let mut buffer = RingBuffer::new(ArrayBackend::::new()); + + { + let mut writer = buffer.write(); + writer.push(1.0); + writer.push(2.0); + writer.push(3.0); + writer.push(4.0); + } + + let mut handle = buffer.get_read_handle(Some(0)); + handle.delta(2, buffer.len()); + + assert_eq!(handle.position(), 2); + + let mut reader = handle.read(&buffer); + assert_eq!(reader.next(), 3.0); + } + + #[test] + fn test_read_handle_delta_negative() { + let mut buffer = RingBuffer::new(ArrayBackend::::new()); + + { + let mut writer = buffer.write(); + writer.push(1.0); + writer.push(2.0); + writer.push(3.0); + writer.push(4.0); + } + + let mut handle = buffer.get_read_handle(Some(3)); + handle.delta(-2, buffer.len()); + + assert_eq!(handle.position(), 1); + + let mut reader = handle.read(&buffer); + assert_eq!(reader.next(), 2.0); + } + + #[test] + fn test_read_handle_delta_wraps() { + let mut buffer = RingBuffer::new(ArrayBackend::::new()); + + { + let mut writer = buffer.write(); + writer.push(1.0); + writer.push(2.0); + writer.push(3.0); + writer.push(4.0); + } + + let mut handle = buffer.get_read_handle(Some(0)); + handle.delta(5, buffer.len()); // wraps around + + assert_eq!(handle.position(), 1); + } + + #[test] + fn test_read_handle_position() { + let mut handle = ReadHandle:: { + read_position: 42, + _phantom: PhantomData, + }; + + assert_eq!(handle.position(), 42); + + handle.seek(10); + assert_eq!(handle.position(), 10); + } + + #[test] + fn test_read_iterator() { + let mut buffer = RingBuffer::new(ArrayBackend::::new()); + + { + let mut writer = buffer.write(); + writer.push(1.0); + writer.push(2.0); + writer.push(3.0); + writer.push(4.0); + } + + let mut handle = buffer.get_read_handle(Some(0)); + let mut iter = handle.iter(&buffer, 3); + + assert_eq!(iter.next(), Some(1.0)); + assert_eq!(iter.next(), Some(2.0)); + assert_eq!(iter.next(), Some(3.0)); + assert_eq!(iter.next(), None); + } + + #[test] + fn test_read_iterator_advances_handle_position() { + let mut buffer = RingBuffer::new(ArrayBackend::::new()); + + { + let mut writer = buffer.write(); + writer.push(1.0); + writer.push(2.0); + writer.push(3.0); + writer.push(4.0); + } + + let mut handle = buffer.get_read_handle(Some(0)); + + { + let mut iter = handle.iter(&buffer, 2); + // Consume the iterator + while iter.next().is_some() {} + } + + // Handle position should have advanced + assert_eq!(handle.position(), 2); + } + + #[test] + fn test_read_iterator_zero_count() { + let mut buffer = RingBuffer::new(ArrayBackend::::new()); + + { + let mut writer = buffer.write(); + writer.push(1.0); + } + + let mut handle = buffer.get_read_handle(Some(0)); + let mut iter = handle.iter(&buffer, 0); + + assert_eq!(iter.next(), None); + } + + #[test] + fn test_read_iterator_wraps() { + let mut buffer = RingBuffer::new(ArrayBackend::::new()); + + { + let mut writer = buffer.write(); + writer.push(1.0); + writer.push(2.0); + } + + let mut handle = buffer.get_read_handle(Some(0)); + let mut iter = handle.iter(&buffer, 4); // More than buffer size + + assert_eq!(iter.next(), Some(1.0)); + assert_eq!(iter.next(), Some(2.0)); + assert_eq!(iter.next(), Some(1.0)); // wrapped + assert_eq!(iter.next(), Some(2.0)); // wrapped + assert_eq!(iter.next(), None); + } + + #[test] + fn test_multiple_read_handles_independent() { + let mut buffer = RingBuffer::new(ArrayBackend::::new()); + + { + let mut writer = buffer.write(); + writer.push(1.0); + writer.push(2.0); + writer.push(3.0); + writer.push(4.0); + } + + let mut handle1 = buffer.get_read_handle(Some(0)); + let mut handle2 = buffer.get_read_handle(Some(2)); + + // Advance handle1 + { + let mut reader = handle1.read(&buffer); + reader.next(); + } + + // handle2 should be unaffected + assert_eq!(handle2.position(), 2); + { + let mut reader = handle2.read(&buffer); + assert_eq!(reader.next(), 3.0); + } + } +} diff --git a/src/lib.rs b/src/lib.rs index f9fc6c9..daf22c5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,96 @@ #![no_std] -use core::ops::{Index, IndexMut}; -pub trait Num: Copy + Send{ +//! # Multitap - Zero-cost ring buffers for audio DSP +//! +//! A `no_std` ring buffer library designed for real-time audio processing, +//! featuring compile-time safety through Rust's borrow checker. +//! +//! ## Design Philosophy +//! +//! - **Storage abstraction**: Works with stack arrays, external memory (SDRAM), or mmap +//! - **Borrow checker safety**: Enforces one writer XOR multiple readers at compile time +//! - **Zero-cost**: No runtime overhead, everything monomorphizes +//! - **Real-time**: Wait-free, lock-free, no allocations +//! +//! ## Core Concepts +//! +//! - **`RingBuffer`**: Main type, generic over storage backend `B` +//! - **`BufferBackend`**: Trait for storage (array, external memory, etc.) +//! - **`WriteGuard`**: Exclusive mutable access for writing +//! - **`ReadHandle`**: Persistent handle tracking a read position +//! - **`ReadGuard`**: Temporary immutable access for reading +//! +//! ## Quick Start +//! +//! ```rust,ignore +//! use multitap::{RingBuffer, ArrayBackend}; +//! +//! // Create a ring buffer with stack-allocated array +//! let mut buffer = RingBuffer::new(ArrayBackend::::new()); +//! +//! // Create a delay tap +//! let mut delay_tap = buffer.get_read_handle(None); +//! +//! // Audio processing loop +//! loop { +//! let input = get_audio_sample(); +//! +//! // Write (exclusive access) +//! buffer.write().push(input); +//! +//! // Read delayed sample (shared access) +//! let delayed = delay_tap.read(&buffer).next(); +//! +//! output_audio(input + delayed * 0.5); +//! } +//! ``` +//! +//! ## Multiple Delay Taps +//! +//! ```rust,ignore +//! let mut buffer = RingBuffer::new(ArrayBackend::::new()); +//! +//! let mut tap1 = buffer.get_read_handle(Some(100)); // 100 sample delay +//! let mut tap2 = buffer.get_read_handle(Some(500)); // 500 sample delay +//! +//! // Both can read simultaneously +//! let sample1 = tap1.read(&buffer).next(); +//! let sample2 = tap2.read(&buffer).next(); +//! ``` +//! +//! ## External Memory (SDRAM) +//! +//! ```rust,ignore +//! use multitap::{RingBuffer, SliceBackend}; +//! +//! // SDRAM at 0xC0000000, 64MB = 16M floats +//! // The address and capacity are compile-time constants +//! let mut buffer = unsafe { +//! RingBuffer::new(SliceBackend::::new()) +//! }; +//! +//! // Use exactly like array-backed buffer +//! // The backend is zero-sized and recreates pointers on each access +//! ``` + +// Re-export main types +pub use backend::BufferBackend; +pub use array_backend::ArrayBackend; +pub use slice_backend::{SliceBackend, SliceBackendRuntime}; +pub use ring_buffer::RingBuffer; +pub use guards::{WriteGuard, ReadGuard}; +pub use handles::{ReadHandle, ReadIterator}; + +// Module declarations +mod backend; +mod array_backend; +mod slice_backend; +mod ring_buffer; +mod guards; +mod handles; + +/// Trait for numeric types that can be stored in ring buffers +pub trait Num: Copy + Send { fn default_value() -> Self; } @@ -17,241 +106,35 @@ impl Num for i32 { } } -pub struct ReadHead { - buffer : * const [T], - size : usize, - head_position : usize, -} - -unsafe impl Send for ReadHead {} - -impl ReadHead { - pub fn seek(&mut self, position: usize){ - self.head_position = position % self.size; - } -} - -impl Iterator for ReadHead { - type Item = T; - fn next(&mut self) -> Option { - let sample: T; - unsafe { - sample = (*self.buffer)[self.head_position]; - } - self.head_position = (self.head_position + 1) % (self.size); - - Some(sample) - } -} - -impl Index for ReadHead { - type Output = T; - fn index(&self, i: usize) -> &T { - let current_position = i % self.size; - unsafe { - &(*self.buffer)[current_position] - } - } -} - - -pub struct WriteHead { - buffer : [T; N], - head_position : usize, -} - -unsafe impl Send for WriteHead {} - -impl WriteHead { - pub fn new() -> WriteHead { - let buffer = [ T::default_value(); N]; - WriteHead {buffer, head_position: 0} - } - - pub fn push(&mut self, element: T) { - self.buffer[self.head_position] = element; - self.increment(); - } - - pub fn increment(&mut self) { - self.head_position = (self.head_position + 1) % self.buffer.len(); - } - - pub fn seek(&mut self, position: usize){ - self.head_position = if position > self.buffer.len() { 0 } else { position }; - } - - pub fn clear(&mut self) where T: Default { - self.buffer.fill(T::default_value()); - } - - pub fn as_readhead(&self, delay_samples: usize) -> ReadHead { - ReadHead {buffer: self.buffer.as_slice(), size: self.buffer.len(), head_position: (self.buffer.len() - delay_samples) % self.buffer.len()} - } -} - - -impl Iterator for WriteHead { - type Item = T; - fn next(&mut self) -> Option { - let sample = self.buffer[self.head_position]; - self.increment(); - - Some(sample) - } -} - -impl Index for WriteHead { - type Output = T; - fn index(&self, i: usize) -> &T { - let current_position = i % self.buffer.len(); - &self.buffer[current_position] - } -} - -impl IndexMut for WriteHead { - fn index_mut(&mut self, i: usize) -> &mut T { - let current_position = i % self.buffer.len(); - &mut self.buffer[current_position] - } -} +// Convenience type alias +pub type RingBufferArray = RingBuffer>; #[cfg(test)] mod tests { use super::*; #[test] - pub fn read_head_is_generic() { - { - let mut write_head = WriteHead::::new(); - write_head.push(0); - } - { - let mut write_head = WriteHead::::new(); - write_head.push(0_f32); - } - } - - #[test] - pub fn read_head_with_delay_output_equals_write_head() { - let mut write_head = WriteHead::::new(); - - write_head.push(1); - - for n in 0..4 { - let mut read_head = write_head.as_readhead(n); - for j in 0..4 { - let val = read_head.next().unwrap(); - if j == n { - assert_eq!(val, 1) - } - } - } - } - - #[test] - pub fn multiple_read_head_with_delay_output_equals_write_head() { - let mut write_head = WriteHead::::new(); - - write_head.push(1.0); - - for n in 0..4 { - let mut read_head_1 = write_head.as_readhead(n); - let mut read_head_2 = write_head.as_readhead(n+1); - for j in 0..4 { - let val_1 = read_head_1.next().unwrap(); - let val_2 = read_head_2.next().unwrap(); - if j == n { - assert_eq!(val_1, 1.0) - } - if j == (n + 1) { - assert_eq!(val_2, 1.0) - } - } - } - } - - #[test] - pub fn read_head_is_circular() { - let mut write_head = WriteHead::::new(); - - write_head.push(1.0); + fn test_type_alias_array() { + let mut buffer = RingBufferArray::::new(ArrayBackend::new()); + buffer.write().push(42.0); - let mut read_head = write_head.as_readhead(0); - - assert_eq!(read_head.next().unwrap(), 1.0); - assert_eq!(read_head.next().unwrap(), 0.0); - assert_eq!(read_head.next().unwrap(), 1.0); + let mut handle = buffer.get_read_handle(Some(0)); + assert_eq!(handle.read(&buffer).next(), 42.0); } #[test] - pub fn read_head_index_operator() { - let mut write_head = WriteHead::::new(); + fn test_slice_backend_runtime() { + let mut external_memory = [0.0f32; 4]; - for n in 0..4 { - write_head.push(n as f32); - } - - let read_head = write_head.as_readhead(0); - for n in 0..4 { - assert_eq!(read_head[n], n as f32); - } - } - - #[test] - pub fn read_head_index_operator_is_circular() { - let mut write_head = WriteHead::::new(); - - write_head.push(0.0); - write_head.push(1.0); - - let read_head = write_head.as_readhead(0); - assert_eq!(read_head[0], 0.0); - assert_eq!(read_head[1], 1.0); - assert_eq!(read_head[2], 0.0); - assert_eq!(read_head[3], 1.0); - assert_eq!(read_head[4], 0.0); - } - - #[test] - pub fn write_head_is_circular() { - let mut write_head = WriteHead::::new(); - - write_head.push(0.0); - write_head.push(0.0); - write_head.push(1.0); // wraps around + unsafe { + let mut buffer = RingBuffer::new( + SliceBackendRuntime::from_slice(&mut external_memory) + ); - let mut read_head = write_head.as_readhead(0); - - assert_eq!(read_head.next().unwrap(), 1.0); - assert_eq!(read_head.next().unwrap(), 0.0); - } - - #[test] - pub fn write_head_index_operator() { - let mut write_head = WriteHead::::new(); - - for n in 0..4 { - write_head[n] = n as f32; - } + buffer.write().push(42.0); - let mut read_head = write_head.as_readhead(0); - for n in 0..4 { - assert_eq!(read_head.next().unwrap(), n as f32); + let mut handle = buffer.get_read_handle(Some(0)); + assert_eq!(handle.read(&buffer).next(), 42.0); } } - - #[test] - pub fn write_head_index_operator_is_circular() { - let mut write_head = WriteHead::::new(); - - write_head[0] = 0.0; - write_head[1] = 1.0; - write_head[2] = 2.0; - write_head[3] = 3.0; - - let mut read_head = write_head.as_readhead(0); - assert_eq!(read_head.next().unwrap(), 2.0); - assert_eq!(read_head.next().unwrap(), 3.0); - } } diff --git a/src/ring_buffer.rs b/src/ring_buffer.rs new file mode 100644 index 0000000..6bfca34 --- /dev/null +++ b/src/ring_buffer.rs @@ -0,0 +1,241 @@ +//! Generic ring buffer implementation +//! +//! This module provides the core `RingBuffer` type that works with +//! any storage backend implementing `BufferBackend`. + +use core::marker::PhantomData; +use crate::{Num, BufferBackend, ReadHandle, WriteGuard}; + +#[cfg(test)] +use crate::SliceBackendRuntime; + +/// Core ring buffer that works with any storage backend +/// +/// The ring buffer maintains a write position and delegates actual +/// storage to the backend. This allows the same ring buffer logic +/// to work with arrays, external memory, mmap files, etc. +pub struct RingBuffer> { + pub(crate) backend: B, + pub(crate) write_position: usize, + _phantom: PhantomData, +} + +impl> RingBuffer { + /// Create a new ring buffer with the given backend + pub fn new(backend: B) -> Self { + RingBuffer { + backend, + write_position: 0, + _phantom: PhantomData, + } + } + + /// Get a mutable write guard for exclusive write access + pub fn write(&mut self) -> WriteGuard<'_, T, B> { + WriteGuard { buffer: self } + } + + /// Create a new read handle at the specified position + /// If position is None, uses the current write position + pub fn get_read_handle(&self, position: Option) -> ReadHandle { + let pos = position.unwrap_or(self.write_position); + ReadHandle { + read_position: pos % self.backend.capacity(), + _phantom: PhantomData, + } + } + + /// Get the current write position + pub fn write_position(&self) -> usize { + self.write_position + } + + /// Get the buffer size + pub fn len(&self) -> usize { + self.backend.capacity() + } + + /// Clear the buffer + pub fn clear(&mut self) { + self.backend.clear(); + } +} + +unsafe impl> Send for RingBuffer {} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ArrayBackend; + + #[test] + fn test_new() { + let buffer = RingBuffer::new(ArrayBackend::::new()); + assert_eq!(buffer.write_position(), 0); + assert_eq!(buffer.len(), 4); + } + + #[test] + fn test_basic_write_read_array() { + let mut buffer = RingBuffer::new(ArrayBackend::::new()); + + { + let mut writer = buffer.write(); + writer.push(1.0); + writer.push(2.0); + writer.push(3.0); + } + + let mut read_handle = buffer.get_read_handle(Some(0)); + let mut reader = read_handle.read(&buffer); + + assert_eq!(reader.next(), 1.0); + assert_eq!(reader.next(), 2.0); + assert_eq!(reader.next(), 3.0); + } + + #[test] + fn test_basic_write_read_slice() { + let mut external_memory = [0.0f32; 4]; + + unsafe { + let mut buffer = RingBuffer::new( + SliceBackendRuntime::from_slice(&mut external_memory) + ); + + { + let mut writer = buffer.write(); + writer.push(1.0); + writer.push(2.0); + writer.push(3.0); + } + + let mut read_handle = buffer.get_read_handle(Some(0)); + let mut reader = read_handle.read(&buffer); + + assert_eq!(reader.next(), 1.0); + assert_eq!(reader.next(), 2.0); + assert_eq!(reader.next(), 3.0); + } + } + + #[test] + fn test_multiple_read_handles() { + let mut buffer = RingBuffer::new(ArrayBackend::::new()); + + { + let mut writer = buffer.write(); + writer.push(1.0); + writer.push(2.0); + writer.push(3.0); + writer.push(4.0); + } + + let mut handle1 = buffer.get_read_handle(Some(0)); + let mut handle2 = buffer.get_read_handle(Some(2)); + + { + let mut reader1 = handle1.read(&buffer); + assert_eq!(reader1.next(), 1.0); + } + + { + let mut reader2 = handle2.read(&buffer); + assert_eq!(reader2.next(), 3.0); + } + } + + #[test] + fn test_ring_wrapping() { + let mut buffer = RingBuffer::new(ArrayBackend::::new()); + + { + let mut writer = buffer.write(); + writer.push(1.0); + writer.push(2.0); + writer.push(3.0); // wraps around + } + + let mut handle = buffer.get_read_handle(Some(0)); + let mut reader = handle.read(&buffer); + + assert_eq!(reader.next(), 3.0); // wrapped value + assert_eq!(reader.next(), 2.0); + } + + #[test] + fn test_audio_delay_pattern() { + let mut buffer = RingBuffer::new(ArrayBackend::::new()); + + let mut delay_tap_1 = buffer.get_read_handle(None); + let mut delay_tap_2 = buffer.get_read_handle(None); + + for frame in 0..32 { + let input_sample = (frame as f32) * 0.1; + + { + let mut writer = buffer.write(); + writer.push(input_sample); + } + + if frame >= 4 { + let delayed_1 = { + let mut reader = delay_tap_1.read(&buffer); + reader.next() + }; + + if frame >= 8 { + let delayed_2 = { + let mut reader = delay_tap_2.read(&buffer); + reader.next() + }; + + assert!((delayed_2 - (delayed_1 - 0.4)).abs() < 0.001); + } + } + } + } + + #[test] + fn test_clear() { + let mut buffer = RingBuffer::new(ArrayBackend::::new()); + + { + let mut writer = buffer.write(); + writer.push(1.0); + writer.push(2.0); + } + + buffer.clear(); + + let mut handle = buffer.get_read_handle(Some(0)); + let mut reader = handle.read(&buffer); + assert_eq!(reader.next(), 0.0); + assert_eq!(reader.next(), 0.0); + } + + #[test] + fn test_write_position_tracking() { + let mut buffer = RingBuffer::new(ArrayBackend::::new()); + + assert_eq!(buffer.write_position(), 0); + + buffer.write().push(1.0); + assert_eq!(buffer.write_position(), 1); + + buffer.write().push(2.0); + assert_eq!(buffer.write_position(), 2); + } + + #[test] + fn test_get_read_handle_with_none_position() { + let mut buffer = RingBuffer::new(ArrayBackend::::new()); + + buffer.write().push(1.0); + buffer.write().push(2.0); + + // None should use current write position + let handle = buffer.get_read_handle(None); + assert_eq!(handle.position(), 2); + } +} diff --git a/src/slice_backend.rs b/src/slice_backend.rs new file mode 100644 index 0000000..80d6f0a --- /dev/null +++ b/src/slice_backend.rs @@ -0,0 +1,291 @@ +//! Slice-based storage backend for external memory +//! +//! This module provides two backend types: +//! - `SliceBackend`: Compile-time const generic version (zero-sized, for hardware SDRAM) +//! - `SliceBackendRuntime`: Runtime version (stores address, for testing/flexibility) +//! +//! Both recreate pointers from the base address on every access, preventing +//! pointer invalidation issues when the backend struct is moved. +//! +//! Useful for: +//! - Memory-mapped hardware (like SDRAM on embedded systems) +//! - Memory-mapped files for very large delays +//! - Pre-allocated memory pools + +use core::marker::PhantomData; +use crate::{Num, BufferBackend}; + +/// Backend using externally-owned memory at a compile-time fixed address +/// +/// This is a zero-sized type that accesses memory at a compile-time known address. +/// The pointer is recreated from `BASE_ADDRESS` on every access, ensuring it never +/// goes stale even if the SliceBackend struct is moved. +/// +/// # Type Parameters +/// +/// - `T`: The element type +/// - `BASE_ADDRESS`: The memory address as a `usize` (e.g., `0xC0000000` for SDRAM) +/// - `CAPACITY`: The number of elements of type `T` in the memory region +/// +/// # Safety +/// +/// The caller must ensure that: +/// - The memory region from `BASE_ADDRESS` to `BASE_ADDRESS + CAPACITY * size_of::()` +/// is valid and accessible +/// - No other code accesses this memory in a way that violates Rust's aliasing rules +/// - The memory remains valid for the lifetime of this backend +/// +/// # Example +/// +/// ```rust,ignore +/// // SDRAM at 0xC0000000, 64MB = 16M floats +/// let buffer = unsafe { +/// RingBuffer::new(SliceBackend::::new()) +/// }; +/// ``` +pub struct SliceBackend { + _phantom: PhantomData, +} + +impl SliceBackend { + /// Create a new slice backend + /// + /// This is a const function that creates a zero-sized backend. + /// The actual memory access happens through the const generic parameters. + /// + /// # Safety + /// + /// See the safety requirements on the type itself. + pub const unsafe fn new() -> Self { + SliceBackend { + _phantom: PhantomData, + } + } +} + +unsafe impl BufferBackend + for SliceBackend +{ + fn capacity(&self) -> usize { + CAPACITY + } + + fn get(&self, index: usize) -> T { + unsafe { + let ptr = BASE_ADDRESS as *const T; + *ptr.add(index) + } + } + + fn set(&mut self, index: usize, value: T) { + unsafe { + let ptr = BASE_ADDRESS as *mut T; + *ptr.add(index) = value; + } + } + + fn as_slice(&self) -> &[T] { + unsafe { + core::slice::from_raw_parts(BASE_ADDRESS as *const T, CAPACITY) + } + } + + fn as_mut_slice(&mut self) -> &mut [T] { + unsafe { + core::slice::from_raw_parts_mut(BASE_ADDRESS as *mut T, CAPACITY) + } + } + + fn clear(&mut self) { + unsafe { + let ptr = BASE_ADDRESS as *mut T; + for i in 0..CAPACITY { + *ptr.add(i) = T::default_value(); + } + } + } +} + +unsafe impl Send + for SliceBackend {} + +/// Runtime version of SliceBackend for dynamic addresses +/// +/// This version stores the base address and capacity at runtime, making it suitable +/// for situations where the address isn't known at compile time (like tests or +/// dynamic memory allocation). +/// +/// The pointer is still recreated from the base address on every access, preventing +/// invalidation when the struct is moved. +/// +/// # Safety +/// +/// The caller must ensure that: +/// - The memory region from `base_address` to `base_address + capacity * size_of::()` +/// is valid and accessible +/// - No other code accesses this memory in a way that violates Rust's aliasing rules +/// - The memory remains valid for the lifetime of this backend +pub struct SliceBackendRuntime { + base_address: usize, + capacity: usize, + _phantom: PhantomData, +} + +impl SliceBackendRuntime { + /// Create a new runtime slice backend from a base address and capacity + /// + /// # Safety + /// + /// See the safety requirements on the type itself. + pub unsafe fn new(base_address: usize, capacity: usize) -> Self { + SliceBackendRuntime { + base_address, + capacity, + _phantom: PhantomData, + } + } + + /// Create from a mutable slice + /// + /// This is a convenience wrapper that extracts the address and length from a slice. + /// + /// # Safety + /// + /// The memory must remain valid and at a stable address for the lifetime of this backend. + pub unsafe fn from_slice(slice: &mut [T]) -> Self { + Self::new(slice.as_mut_ptr() as usize, slice.len()) + } +} + +unsafe impl BufferBackend for SliceBackendRuntime { + fn capacity(&self) -> usize { + self.capacity + } + + fn get(&self, index: usize) -> T { + unsafe { + let ptr = self.base_address as *const T; + *ptr.add(index) + } + } + + fn set(&mut self, index: usize, value: T) { + unsafe { + let ptr = self.base_address as *mut T; + *ptr.add(index) = value; + } + } + + fn as_slice(&self) -> &[T] { + unsafe { + core::slice::from_raw_parts(self.base_address as *const T, self.capacity) + } + } + + fn as_mut_slice(&mut self) -> &mut [T] { + unsafe { + core::slice::from_raw_parts_mut(self.base_address as *mut T, self.capacity) + } + } + + fn clear(&mut self) { + unsafe { + let ptr = self.base_address as *mut T; + for i in 0..self.capacity { + *ptr.add(i) = T::default_value(); + } + } + } +} + +unsafe impl Send for SliceBackendRuntime {} + +#[cfg(test)] +mod tests { + use super::*; + + // Tests for the const generic SliceBackend + #[test] + fn test_slice_backend_zero_sized() { + unsafe { + let backend = SliceBackend::::new(); + // Verify it's truly zero-sized + assert_eq!(core::mem::size_of_val(&backend), 0); + assert_eq!(backend.capacity(), 1024); + } + } + + #[test] + fn test_slice_backend_const_new() { + // Test that new() is truly const + const unsafe fn make_backend() -> SliceBackend { + SliceBackend::new() + } + + unsafe { + let _backend = make_backend(); + } + } + + // Tests for SliceBackendRuntime + #[test] + fn test_runtime_new() { + let mut external_memory = [0.0f32; 4]; + let backend = unsafe { SliceBackendRuntime::from_slice(&mut external_memory) }; + assert_eq!(backend.capacity(), 4); + assert_eq!(backend.get(0), 0.0); + } + + #[test] + fn test_runtime_get_set() { + let mut external_memory = [0.0f32; 4]; + let mut backend = unsafe { SliceBackendRuntime::from_slice(&mut external_memory) }; + + backend.set(0, 1.0); + backend.set(1, 2.0); + + assert_eq!(backend.get(0), 1.0); + assert_eq!(backend.get(1), 2.0); + + // Verify the external memory was actually modified + assert_eq!(external_memory[0], 1.0); + assert_eq!(external_memory[1], 2.0); + } + + #[test] + fn test_runtime_as_slice() { + let mut external_memory = [0.0f32; 4]; + let mut backend = unsafe { SliceBackendRuntime::from_slice(&mut external_memory) }; + + backend.set(0, 1.0); + backend.set(1, 2.0); + + let slice = backend.as_slice(); + assert_eq!(slice.len(), 4); + assert_eq!(slice[0], 1.0); + assert_eq!(slice[1], 2.0); + } + + #[test] + fn test_runtime_clear() { + let mut external_memory = [1.0f32, 2.0, 3.0, 4.0]; + let mut backend = unsafe { SliceBackendRuntime::from_slice(&mut external_memory) }; + + backend.clear(); + + assert_eq!(backend.get(0), 0.0); + assert_eq!(backend.get(1), 0.0); + assert_eq!(external_memory[0], 0.0); + assert_eq!(external_memory[1], 0.0); + } + + #[test] + fn test_runtime_i32_type() { + let mut external_memory = [0i32; 4]; + let mut backend = unsafe { SliceBackendRuntime::from_slice(&mut external_memory) }; + + backend.set(0, 42); + assert_eq!(backend.get(0), 42); + assert_eq!(external_memory[0], 42); + } +}