use rand::{Rng, SeedableRng};
#[derive(Debug)]
pub struct KarplusStrong {
buffer: Vec<f32>,
position: usize,
decay: f32,
brightness: f32,
prev_sample: f32,
rng: rand::rngs::StdRng,
}
impl KarplusStrong {
pub fn new(frequency: f32, sample_rate: f32) -> Self {
let buffer_length = (sample_rate / frequency).round() as usize;
let buffer_length = buffer_length.max(1);
let mut rng = rand::rngs::StdRng::from_rng(&mut rand::rng());
let buffer: Vec<f32> = (0..buffer_length)
.map(|_| rng.random_range(-1.0..1.0))
.collect();
Self {
buffer,
position: 0,
decay: 0.996, brightness: 0.5, prev_sample: 0.0,
rng,
}
}
pub fn with_seed(frequency: f32, sample_rate: f32, seed: u64) -> Self {
let buffer_length = (sample_rate / frequency).round() as usize;
let buffer_length = buffer_length.max(1);
let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
let buffer: Vec<f32> = (0..buffer_length)
.map(|_| rng.random_range(-1.0..1.0))
.collect();
Self {
buffer,
position: 0,
decay: 0.996,
brightness: 0.5,
prev_sample: 0.0,
rng,
}
}
pub fn set_decay(&mut self, decay: f32) {
self.decay = decay.clamp(0.0, 1.0);
}
pub fn with_decay(mut self, decay: f32) -> Self {
self.set_decay(decay);
self
}
pub fn set_brightness(&mut self, brightness: f32) {
self.brightness = brightness.clamp(0.0, 1.0);
}
pub fn with_brightness(mut self, brightness: f32) -> Self {
self.set_brightness(brightness);
self
}
pub fn pluck(&mut self) {
for sample in &mut self.buffer {
*sample = self.rng.random_range(-1.0..1.0);
}
self.prev_sample = 0.0;
}
#[inline]
pub fn sample(&mut self) -> f32 {
let current = self.buffer[self.position];
let next_pos = (self.position + 1) % self.buffer.len();
let filtered = (1.0 - self.brightness) * 0.5 * (current + self.prev_sample)
+ self.brightness * current;
let output = filtered * self.decay;
self.buffer[self.position] = output;
self.prev_sample = current;
self.position = next_pos;
output
}
pub fn generate(&mut self, length: usize) -> Vec<f32> {
(0..length).map(|_| self.sample()).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_karplus_strong_creates_valid_buffer() {
let ks = KarplusStrong::new(440.0, 44100.0);
let expected_len = (44100.0_f32 / 440.0_f32).round() as usize;
assert_eq!(ks.buffer.len(), expected_len);
}
#[test]
fn test_karplus_strong_sample_in_range() {
let mut ks = KarplusStrong::new(440.0, 44100.0);
for _ in 0..1000 {
let sample = ks.sample();
assert!(
sample >= -1.0 && sample <= 1.0,
"Sample {} out of range",
sample
);
}
}
#[test]
fn test_karplus_strong_decays() {
let mut ks = KarplusStrong::new(440.0, 44100.0)
.with_decay(0.99);
let initial: Vec<f32> = (0..100).map(|_| ks.sample().abs()).collect();
let initial_avg: f32 = initial.iter().sum::<f32>() / initial.len() as f32;
let later: Vec<f32> = (0..100).map(|_| ks.sample().abs()).collect();
let later_avg: f32 = later.iter().sum::<f32>() / later.len() as f32;
assert!(
later_avg < initial_avg,
"Sound should decay over time: initial {} vs later {}",
initial_avg,
later_avg
);
}
#[test]
fn test_karplus_strong_seeded_deterministic() {
let mut ks1 = KarplusStrong::with_seed(440.0, 44100.0, 12345);
let mut ks2 = KarplusStrong::with_seed(440.0, 44100.0, 12345);
for _ in 0..1000 {
assert_eq!(ks1.sample(), ks2.sample());
}
}
#[test]
fn test_karplus_strong_pluck_resets() {
let mut ks = KarplusStrong::with_seed(440.0, 44100.0, 42)
.with_decay(0.99);
for _ in 0..10000 {
ks.sample();
}
let quiet_sample = ks.sample().abs();
ks.pluck();
let mut max_loud_sample: f32 = 0.0;
for _ in 0..10 {
max_loud_sample = max_loud_sample.max(ks.sample().abs());
}
assert!(
max_loud_sample > quiet_sample,
"Pluck should excite the string: {} vs {}",
max_loud_sample,
quiet_sample
);
}
#[test]
fn test_karplus_strong_brightness_affects_tone() {
let mut bright = KarplusStrong::with_seed(440.0, 44100.0, 42)
.with_brightness(1.0);
let mut dark = KarplusStrong::with_seed(440.0, 44100.0, 42)
.with_brightness(0.0);
let bright_samples = bright.generate(1000);
let dark_samples = dark.generate(1000);
let mut differences = 0;
for (b, d) in bright_samples.iter().zip(dark_samples.iter()) {
if (b - d).abs() > 0.01 {
differences += 1;
}
}
assert!(
differences > 100,
"Brightness should affect the sound: {} differences found",
differences
);
}
#[test]
fn test_karplus_strong_generate() {
let mut ks = KarplusStrong::new(440.0, 44100.0);
let samples = ks.generate(1000);
assert_eq!(samples.len(), 1000);
for &sample in &samples {
assert!(sample >= -1.0 && sample <= 1.0);
}
}
#[test]
fn test_karplus_strong_different_frequencies() {
let low = KarplusStrong::new(110.0, 44100.0);
let low_buffer_len = low.buffer.len();
let high = KarplusStrong::new(880.0, 44100.0);
let high_buffer_len = high.buffer.len();
assert!(
low_buffer_len > high_buffer_len,
"Lower frequency should have larger buffer"
);
}
#[test]
fn test_decay_clamping() {
let mut ks = KarplusStrong::new(440.0, 44100.0);
ks.set_decay(1.5); assert!(ks.decay <= 1.0);
ks.set_decay(-0.5); assert!(ks.decay >= 0.0);
}
#[test]
fn test_brightness_clamping() {
let mut ks = KarplusStrong::new(440.0, 44100.0);
ks.set_brightness(1.5); assert!(ks.brightness <= 1.0);
ks.set_brightness(-0.5); assert!(ks.brightness >= 0.0);
}
}