pub struct KarplusStrong {
buf: Vec<f32>,
write: usize,
pub decay: f32, pub brightness: f32, pub stretch: f32, pub active: bool,
pub volume: f32,
length_f: f32, lp_state: f32,
ap_state: f32,
}
impl KarplusStrong {
pub fn new(max_freq_hz: f32, sample_rate: f32) -> Self {
let max_len = (sample_rate / max_freq_hz) as usize + 4;
Self {
buf: vec![0.0; max_len],
write: 0,
decay: 0.996,
brightness: 0.45,
stretch: 0.06,
active: false,
volume: 0.5,
length_f: (sample_rate / 220.0).clamp(2.0, max_len as f32 - 2.0),
lp_state: 0.0,
ap_state: 0.0,
}
}
pub fn trigger(&mut self, freq: f32, sample_rate: f32) {
self.length_f = (sample_rate / freq.max(20.0)).clamp(2.0, self.buf.len() as f32 - 2.0);
let len = self.length_f as usize;
let ns = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.subsec_nanos() as u64)
.unwrap_or(self.write as u64 + 1);
let mut rng = self.write as u64 ^ ns ^ 0xDEAD_BEEF_CAFE_BABE;
rng ^= rng << 13;
rng ^= rng >> 7;
rng ^= rng << 17;
for i in 0..len {
rng = rng
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
self.buf[i] = (rng >> 33) as f32 / (1u64 << 31) as f32 * 2.0 - 1.0;
}
for i in len..self.buf.len() {
self.buf[i] = 0.0;
}
self.write = len % self.buf.len();
self.lp_state = 0.0;
self.ap_state = 0.0;
self.active = true;
}
pub fn next_sample(&mut self) -> f32 {
if !self.active {
return 0.0;
}
let len = self.buf.len();
let delay = self.length_f;
let d0 = delay as usize;
let frac = delay - d0 as f32;
let i0 = (self.write + len - d0.min(len - 1)) % len;
let i1 = (self.write + len - (d0 + 1).min(len - 1)) % len;
let read = self.buf[i0] * (1.0 - frac) + self.buf[i1] * frac;
let b = self.brightness;
self.lp_state = (1.0 - b) * read + b * self.lp_state;
let c = self.stretch;
let ap_out = c * (self.lp_state - self.ap_state) + self.ap_state;
self.ap_state = self.lp_state;
let fed = ap_out * self.decay;
self.buf[self.write] = fed;
self.write = (self.write + 1) % len;
if fed.abs() < 1e-6 {
self.active = false;
}
read * self.volume
}
}
#[cfg(test)]
mod tests {
use super::*;
const SR: f32 = 44100.0;
#[test]
fn test_karplus_silent_before_trigger() {
let mut ks = KarplusStrong::new(20.0, SR);
assert!(!ks.active, "Should be inactive before trigger");
let s = ks.next_sample();
assert_eq!(s, 0.0, "Should be silent before trigger");
}
#[test]
fn test_karplus_produces_output_after_trigger() {
let mut ks = KarplusStrong::new(20.0, SR);
ks.trigger(440.0, SR);
assert!(ks.active, "Should be active after trigger");
let mut max_abs = 0.0_f32;
for _ in 0..4410 {
let s = ks.next_sample();
max_abs = max_abs.max(s.abs());
}
assert!(max_abs > 0.0, "Triggered string should produce output");
}
#[test]
fn test_karplus_output_finite() {
let mut ks = KarplusStrong::new(20.0, SR);
ks.trigger(440.0, SR);
for i in 0..22050 {
let s = ks.next_sample();
assert!(s.is_finite(), "Output non-finite at sample {}", i);
}
}
#[test]
fn test_karplus_decays_to_silence() {
let mut ks = KarplusStrong::new(20.0, SR);
ks.trigger(440.0, SR);
for _ in 0..SR as usize * 10 {
ks.next_sample();
}
assert!(!ks.active, "String should decay to silence");
}
#[test]
fn test_karplus_higher_freq_shorter_delay() {
let mut ks_low = KarplusStrong::new(20.0, SR);
ks_low.trigger(220.0, SR);
let len_low = ks_low.length_f;
let mut ks_high = KarplusStrong::new(20.0, SR);
ks_high.trigger(880.0, SR);
let len_high = ks_high.length_f;
assert!(
len_high < len_low,
"Higher frequency should give shorter delay: 220={}, 880={}",
len_low,
len_high
);
}
#[test]
fn test_karplus_low_decay_fades_faster() {
let mut ks_fast = KarplusStrong::new(20.0, SR);
ks_fast.decay = 0.9; ks_fast.trigger(440.0, SR);
let mut rms_fast = 0.0_f32;
for _ in 0..4410 {
let s = ks_fast.next_sample();
rms_fast += s * s;
}
let mut ks_slow = KarplusStrong::new(20.0, SR);
ks_slow.decay = 0.999; ks_slow.trigger(440.0, SR);
let mut rms_slow = 0.0_f32;
for _ in 0..4410 {
let s = ks_slow.next_sample();
rms_slow += s * s;
}
assert!(
rms_fast < rms_slow,
"Lower decay should produce lower RMS: fast={}, slow={}",
rms_fast,
rms_slow
);
}
#[test]
fn test_karplus_retrigger_reactivates() {
let mut ks = KarplusStrong::new(20.0, SR);
ks.trigger(440.0, SR);
for _ in 0..SR as usize * 10 {
ks.next_sample();
}
assert!(!ks.active, "Should be inactive after full decay");
ks.trigger(880.0, SR);
assert!(ks.active, "Should be active after retrigger");
let mut max_abs = 0.0_f32;
for _ in 0..4410 {
max_abs = max_abs.max(ks.next_sample().abs());
}
assert!(max_abs > 0.0, "Retriggered string should produce output");
}
}