Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@ version = "0.1.0"
edition = "2021"

[dependencies]

[dev-dependencies]
memmap2 = "0.9"
103 changes: 103 additions & 0 deletions examples/audio_delay.rs
Original file line number Diff line number Diff line change
@@ -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<String> = 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::<f32>().ok())
.unwrap_or(0.3)
.clamp(0.0, 1.0);

let wet_mix: f32 = args.get(3)
.and_then(|s| s.parse::<f32>().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::<f32, 16384>::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(())
}
73 changes: 73 additions & 0 deletions examples/basic_vec.rs
Original file line number Diff line number Diff line change
@@ -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=<filling buffer>", 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");
}
134 changes: 134 additions & 0 deletions examples/lowpass_filter.rs
Original file line number Diff line number Diff line change
@@ -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<f32> {
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<String> = std::env::args().collect();
let cutoff_hz: f32 = args.get(1)
.and_then(|s| s.parse::<f32>().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::<f32, FILTER_TAPS>::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(())
}
Loading