use std::f32::consts::PI;
pub struct Rng(u32);
impl Rng {
pub fn new(seed: u32) -> Self {
Rng(seed.max(1))
}
pub fn next_u32(&mut self) -> u32 {
let mut x = self.0;
x ^= x << 13;
x ^= x >> 17;
x ^= x << 5;
self.0 = x;
x
}
pub fn white(&mut self) -> f32 {
(self.next_u32() as f32 / u32::MAX as f32) * 2.0 - 1.0
}
}
#[derive(Clone, Copy)]
pub struct Sine {
re: f32,
im: f32,
cr: f32,
ci: f32,
renorm_ticks: u16,
}
impl Sine {
pub fn new(freq: f32, sr: f32, phase: f32) -> Self {
let w = 2.0 * PI * freq / sr;
Sine {
re: phase.cos(),
im: phase.sin(),
cr: w.cos(),
ci: w.sin(),
renorm_ticks: ((freq.to_bits() ^ sr.to_bits() ^ phase.to_bits()) & 0x1fff) as u16,
}
}
#[inline]
pub fn next(&mut self) -> f32 {
let (re, im) = (self.re, self.im);
self.re = re * self.cr - im * self.ci;
self.im = re * self.ci + im * self.cr;
self.renorm_ticks = self.renorm_ticks.wrapping_add(1);
if self.renorm_ticks == 0 {
let magnitude = (self.re * self.re + self.im * self.im).sqrt();
self.re /= magnitude;
self.im /= magnitude;
}
self.im
}
pub fn set_freq(&mut self, freq: f32, sr: f32) {
let w = 2.0 * PI * freq / sr;
self.cr = w.cos();
self.ci = w.sin();
}
}
pub struct FmPair {
car: f32,
md: f32,
car_dw: f32,
md_dw: f32,
ratio: f32,
}
impl FmPair {
pub fn new(freq: f32, ratio: f32, sr: f32) -> Self {
FmPair {
car: 0.0,
md: 0.0,
car_dw: freq / sr,
md_dw: freq * ratio / sr,
ratio,
}
}
pub fn set_freq(&mut self, freq: f32, sr: f32) {
self.car_dw = freq / sr;
self.md_dw = freq * self.ratio / sr;
}
#[inline]
pub fn next(&mut self, index: f32) -> f32 {
let m = (core::f32::consts::TAU * self.md).sin();
let y = (core::f32::consts::TAU * self.car + index * m).sin();
self.car += self.car_dw;
if self.car >= 1.0 {
self.car -= 1.0;
}
self.md += self.md_dw;
if self.md >= 1.0 {
self.md -= 1.0;
}
y
}
}
#[inline]
fn polyblep(t: f32, dt: f32) -> f32 {
if t < dt {
let x = t / dt;
x + x - x * x - 1.0
} else if t > 1.0 - dt {
let x = (t - 1.0) / dt;
x * x + x + x + 1.0
} else {
0.0
}
}
pub struct BlepSaw {
phase: f32,
inc: f32,
}
impl BlepSaw {
pub fn new(freq: f32, sr: f32, phase: f32) -> Self {
BlepSaw {
phase: phase.rem_euclid(1.0),
inc: freq / sr,
}
}
pub fn set_freq(&mut self, freq: f32, sr: f32) {
self.inc = freq / sr;
}
#[inline]
pub fn next(&mut self) -> f32 {
let out = 2.0 * self.phase - 1.0 - polyblep(self.phase, self.inc);
self.phase += self.inc;
if self.phase >= 1.0 {
self.phase -= 1.0;
}
out
}
}
pub struct BlepPulse {
phase: f32,
inc: f32,
duty: f32,
}
impl BlepPulse {
pub fn new(freq: f32, sr: f32, phase: f32, duty: f32) -> Self {
BlepPulse {
phase: phase.rem_euclid(1.0),
inc: freq / sr,
duty: duty.clamp(0.05, 0.95),
}
}
pub fn set_freq(&mut self, freq: f32, sr: f32) {
self.inc = freq / sr;
}
#[inline]
pub fn next(&mut self) -> f32 {
let mut out = if self.phase < self.duty { 1.0 } else { -1.0 };
out += polyblep(self.phase, self.inc);
out -= polyblep((self.phase - self.duty).rem_euclid(1.0), self.inc);
self.phase += self.inc;
if self.phase >= 1.0 {
self.phase -= 1.0;
}
out
}
}
#[inline]
fn guard_width(width: f32, inc: f32) -> f32 {
let lo = 2.0 * inc;
let hi = 1.0 - 2.0 * inc;
if lo >= hi {
0.5
} else {
width.clamp(lo, hi)
}
}
pub struct ReedPulse {
phase: f32,
inc: f32,
width: f32,
}
impl ReedPulse {
pub fn new(freq: f32, sr: f32, phase: f32, width: f32) -> Self {
let inc = freq / sr;
ReedPulse {
phase: phase.rem_euclid(1.0),
inc,
width: guard_width(width, inc),
}
}
pub fn set_freq(&mut self, freq: f32, sr: f32) {
self.inc = freq / sr;
self.width = guard_width(self.width, self.inc);
}
#[inline]
pub fn next(&mut self) -> f32 {
let p2 = (self.phase + 1.0 - self.width).rem_euclid(1.0);
let out = 0.5
* ((2.0 * self.phase - 1.0 - polyblep(self.phase, self.inc))
- (2.0 * p2 - 1.0 - polyblep(p2, self.inc)));
self.phase += self.inc;
if self.phase >= 1.0 {
self.phase -= 1.0;
}
out
}
}
#[derive(Clone, Copy)]
pub struct OnePole {
a: f32,
z: f32,
}
impl OnePole {
pub(crate) fn lowpass_mag(cutoff: f32, f: f32, sr: f32) -> f32 {
let a = 1.0 - (-2.0 * core::f32::consts::PI * (cutoff / sr).min(0.49)).exp();
let b = 1.0 - a;
let w = 2.0 * core::f32::consts::PI * f / sr;
a / (1.0 - 2.0 * b * w.cos() + b * b).sqrt()
}
pub fn lowpass(cutoff: f32, sr: f32) -> Self {
let a = 1.0 - (-2.0 * PI * (cutoff / sr).min(0.49)).exp();
OnePole { a, z: 0.0 }
}
pub fn set_cutoff(&mut self, cutoff: f32, sr: f32) {
self.a = 1.0 - (-2.0 * PI * (cutoff / sr).min(0.49)).exp();
}
#[inline]
pub fn process(&mut self, x: f32) -> f32 {
self.z += self.a * (x - self.z);
self.z
}
pub fn flush(&mut self) {
self.z = flush_denormal(self.z);
}
}
#[cfg(test)]
impl OnePole {
pub(crate) fn debug_state(&self) -> f32 {
self.z
}
}
#[cfg(test)]
impl Biquad {
pub(crate) fn debug_state(&self) -> [f32; 4] {
[self.x1, self.x2, self.y1, self.y2]
}
}
#[cfg(test)]
impl DelayLine {
pub(crate) fn debug_buf(&self) -> &[f32] {
&self.buf
}
}
#[inline]
pub(crate) fn flush_denormal(x: f32) -> f32 {
if x.abs() < 1e-34 {
0.0
} else {
x
}
}
pub struct DelayLine {
buf: Vec<f32>,
mask: usize,
idx: usize,
}
impl DelayLine {
pub fn new(max_samples: usize) -> Self {
let size = (max_samples + 2).next_power_of_two();
DelayLine {
buf: vec![0.0; size],
mask: size - 1,
idx: 0,
}
}
#[inline]
pub fn push(&mut self, x: f32) {
self.idx = (self.idx + 1) & self.mask;
self.buf[self.idx] = x;
}
pub fn scale(&mut self, g: f32) {
for x in &mut self.buf {
*x *= g;
}
}
#[inline]
pub fn tap(&self, delay: f32) -> f32 {
let d = delay.max(1.0);
let i = d.floor() as usize;
debug_assert!(
i + 1 < self.buf.len(),
"DelayLine::tap({delay}) reaches {} samples back but the line holds only {} — \
the line is sized too short for the delay it is asked for. Size it from `sr` \
and the longest delay the caller can request, not from a literal.",
i + 2,
self.buf.len()
);
let frac = d - i as f32;
let a = self.buf[(self.idx + self.buf.len() - i) & self.mask];
let b = self.buf[(self.idx + self.buf.len() - i - 1) & self.mask];
a + (b - a) * frac
}
#[inline]
pub fn tap_cubic(&self, delay: f32) -> f32 {
let d = delay.max(2.0);
let i = d.floor() as usize;
let fr = d - i as f32;
let at = |k: usize| self.buf[(self.idx + self.buf.len() - k) & self.mask];
cubic4(at(i - 1), at(i), at(i + 1), at(i + 2), fr)
}
}
#[inline]
pub fn cubic4(pm1: f32, p0: f32, p1: f32, p2: f32, fr: f32) -> f32 {
let w_m1 = -fr * (fr - 1.0) * (fr - 2.0) / 6.0;
let w_0 = (fr + 1.0) * (fr - 1.0) * (fr - 2.0) / 2.0;
let w_1 = -(fr + 1.0) * fr * (fr - 2.0) / 2.0;
let w_2 = (fr + 1.0) * fr * (fr - 1.0) / 6.0;
w_m1 * pm1 + w_0 * p0 + w_1 * p1 + w_2 * p2
}
pub struct Drift {
rng: Rng,
value: f32,
target: f32,
depth: f32,
hold: u32,
count: u32,
}
impl Drift {
pub fn new(seed: u32, depth: f32, hold_ticks: u32) -> Self {
Drift {
rng: Rng::new(seed),
value: 0.0,
target: 0.0,
depth,
hold: hold_ticks.max(1),
count: 0,
}
}
#[inline]
pub fn next(&mut self) -> f32 {
if self.count == 0 {
self.target = self.rng.white() * self.depth;
self.count = self.hold;
}
self.count -= 1;
self.value += 0.015 * (self.target - self.value);
self.value
}
}
#[derive(Clone, Copy)]
pub struct Biquad {
b0: f32,
b1: f32,
b2: f32,
a1: f32,
a2: f32,
x1: f32,
x2: f32,
y1: f32,
y2: f32,
}
impl Biquad {
fn from_coeffs(b0: f32, b1: f32, b2: f32, a0: f32, a1: f32, a2: f32) -> Self {
Biquad {
b0: b0 / a0,
b1: b1 / a0,
b2: b2 / a0,
a1: a1 / a0,
a2: a2 / a0,
x1: 0.0,
x2: 0.0,
y1: 0.0,
y2: 0.0,
}
}
pub fn lowpass(freq: f32, q: f32, sr: f32) -> Self {
let w = 2.0 * PI * (freq / sr).min(0.49);
let (sw, cw) = (w.sin(), w.cos());
let alpha = sw / (2.0 * q);
Self::from_coeffs(
(1.0 - cw) / 2.0,
1.0 - cw,
(1.0 - cw) / 2.0,
1.0 + alpha,
-2.0 * cw,
1.0 - alpha,
)
}
pub fn retune_lowpass(&mut self, freq: f32, q: f32, sr: f32) {
let fresh = Self::lowpass(freq, q, sr);
self.b0 = fresh.b0;
self.b1 = fresh.b1;
self.b2 = fresh.b2;
self.a1 = fresh.a1;
self.a2 = fresh.a2;
}
pub fn highpass(freq: f32, q: f32, sr: f32) -> Self {
let w = 2.0 * PI * (freq / sr).min(0.49);
let (sw, cw) = (w.sin(), w.cos());
let alpha = sw / (2.0 * q);
Self::from_coeffs(
(1.0 + cw) / 2.0,
-(1.0 + cw),
(1.0 + cw) / 2.0,
1.0 + alpha,
-2.0 * cw,
1.0 - alpha,
)
}
pub fn retune_highpass(&mut self, freq: f32, q: f32, sr: f32) {
let fresh = Self::highpass(freq, q, sr);
self.b0 = fresh.b0;
self.b1 = fresh.b1;
self.b2 = fresh.b2;
self.a1 = fresh.a1;
self.a2 = fresh.a2;
}
pub fn bandpass(freq: f32, q: f32, sr: f32) -> Self {
let w = 2.0 * PI * (freq / sr).min(0.49);
let (sw, cw) = (w.sin(), w.cos());
let alpha = sw / (2.0 * q);
Self::from_coeffs(alpha, 0.0, -alpha, 1.0 + alpha, -2.0 * cw, 1.0 - alpha)
}
pub fn retune_bandpass(&mut self, freq: f32, q: f32, sr: f32) {
let fresh = Self::bandpass(freq, q, sr);
self.b0 = fresh.b0;
self.b1 = fresh.b1;
self.b2 = fresh.b2;
self.a1 = fresh.a1;
self.a2 = fresh.a2;
}
pub fn retune_peak(&mut self, freq: f32, q: f32, gain_db: f32, sr: f32) {
let fresh = Self::peak(freq, q, gain_db, sr);
self.b0 = fresh.b0;
self.b1 = fresh.b1;
self.b2 = fresh.b2;
self.a1 = fresh.a1;
self.a2 = fresh.a2;
}
pub fn peak(freq: f32, q: f32, gain_db: f32, sr: f32) -> Self {
let a = 10f32.powf(gain_db / 40.0);
let w = 2.0 * PI * (freq / sr).min(0.49);
let (sw, cw) = (w.sin(), w.cos());
let alpha = sw / (2.0 * q);
Self::from_coeffs(
1.0 + alpha * a,
-2.0 * cw,
1.0 - alpha * a,
1.0 + alpha / a,
-2.0 * cw,
1.0 - alpha / a,
)
}
#[inline]
pub fn process(&mut self, x: f32) -> f32 {
let y = self.b0 * x + self.b1 * self.x1 + self.b2 * self.x2
- self.a1 * self.y1
- self.a2 * self.y2;
self.x2 = self.x1;
self.x1 = x;
self.y2 = self.y1;
self.y1 = y;
y
}
pub fn flush(&mut self) {
self.x1 = flush_denormal(self.x1);
self.x2 = flush_denormal(self.x2);
self.y1 = flush_denormal(self.y1);
self.y2 = flush_denormal(self.y2);
}
}
pub struct Adsr {
stage: u8, level: f32,
a: f32,
d: f32,
s: f32,
r: f32,
}
fn coeff(seconds: f32, sr: f32) -> f32 {
1.0 - (-1.0 / (seconds.max(1e-4) * sr)).exp()
}
impl Adsr {
pub fn new(attack: f32, decay: f32, sustain: f32, release: f32, sr: f32) -> Self {
Adsr {
stage: 0,
level: 0.0,
a: coeff(attack * 0.4, sr), d: coeff(decay, sr),
s: sustain,
r: coeff(release, sr),
}
}
#[inline]
pub fn next(&mut self) -> f32 {
match self.stage {
0 => {
self.level += self.a * (1.3 - self.level);
if self.level >= 1.0 {
self.level = 1.0;
self.stage = 1;
}
}
1 => {
self.level += self.d * (self.s - self.level);
if (self.level - self.s).abs() < 1e-4 {
self.stage = 2;
}
}
2 => {}
3 => {
self.level += self.r * (0.0 - self.level);
if self.level < 1e-4 {
self.level = 0.0;
self.stage = 4;
}
}
_ => {}
}
self.level
}
pub fn release(&mut self) {
if self.stage < 3 {
self.stage = 3;
}
}
pub fn released(&self) -> bool {
self.stage >= 3
}
pub fn alive(&self) -> bool {
self.stage < 4
}
}
pub struct Burst {
filt: Biquad,
gain: f32,
env: f32, decay: f32, rise: f32, up: f32, peak: f32, }
impl Burst {
pub fn new(filt: Biquad, gain: f32, t60: f32, sr: f32) -> Self {
Self::new_ramped(filt, gain, t60, 0.0, sr)
}
pub fn new_ramped(filt: Biquad, gain: f32, t60: f32, rise_s: f32, sr: f32) -> Self {
Burst {
filt,
gain,
env: 0.0,
decay: 10f32.powf(-3.0 / (t60.max(1e-4) * sr)),
rise: (rise_s * sr).max(0.0),
up: 0.0,
peak: 0.0,
}
}
pub fn trigger(&mut self, level: f32) {
if self.rise > 0.0 {
self.peak = self.peak.max(level);
self.up = self.rise;
} else {
self.env = self.env.max(level);
}
}
#[inline]
pub fn tick(&mut self, rng: &mut Rng) -> f32 {
if self.env < 1e-5 && self.up <= 0.0 {
return 0.0;
}
let y = self.filt.process(rng.white()) * self.gain * self.env;
if self.up > 0.0 {
self.up -= 1.0;
let frac = ((self.rise - self.up) / self.rise).clamp(0.0, 1.0);
self.env = self.peak * 0.5 * (1.0 - (std::f32::consts::PI * frac).cos());
} else {
self.env *= self.decay;
}
y
}
}
pub struct GrainGate {
e: f32,
decay: f32,
p: f32, }
impl GrainGate {
pub fn new(hz: f32, t60: f32, sr: f32) -> Self {
GrainGate {
e: 0.0,
decay: 10f32.powf(-3.0 / (t60.max(1e-4) * sr)),
p: (hz / sr).clamp(0.0, 1.0),
}
}
#[inline]
pub fn next(&mut self, rng: &mut Rng) -> f32 {
self.next_with_trigger(rng).0
}
#[inline]
pub(crate) fn next_with_trigger(&mut self, rng: &mut Rng) -> (f32, bool) {
let fired = rng.white() * 0.5 + 0.5 < self.p;
if fired {
self.e = 1.0;
}
let out = self.e;
self.e *= self.decay;
(out, fired)
}
}
pub fn key_freq(key: u8) -> f32 {
440.0 * 2f32.powf((key as f32 - 69.0) / 12.0)
}
pub fn vel_amp(vel: u8) -> f32 {
let v = vel as f32 / 127.0;
v * v
}
pub fn vel_sense_norm(vn: f32, sense: f32) -> f32 {
if sense >= 1.0 {
vn
} else {
1.0 - sense * (1.0 - vn)
}
}
pub fn vel_amp_sensed(vel: u8, sense: f32) -> f32 {
let vn = vel_sense_norm(vel as f32 / 127.0, sense);
vn * vn
}
pub fn vel_ctrl(vel: u8) -> f32 {
(vel as f32 / 127.0).powf(1.6)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sine_long_hold_stays_unit_and_on_frequency() {
const TICKS: u32 = 3_200_000;
const PHASE: f32 = 0.37;
const CASES: &[(f32, f32)] = &[
(0.09, 44_100.0),
(0.09, 44_100.0 / 16.0),
(5.0, 44_100.0 / 16.0),
(55.0, 44_100.0),
(440.0, 44_100.0),
(5_000.0, 44_100.0),
(19_000.0, 44_100.0),
(19_000.0, 88_200.0),
];
let mut failures = Vec::new();
for &(freq, sr) in CASES {
let mut sine = Sine::new(freq, sr, PHASE);
let mut max_magnitude_error = 0.0f32;
for _ in 0..TICKS {
sine.next();
let magnitude = (sine.re * sine.re + sine.im * sine.im).sqrt();
max_magnitude_error = max_magnitude_error.max((magnitude - 1.0).abs());
}
let end_phase = sine.im.atan2(sine.re) as f64;
let phase_delta = (end_phase - PHASE as f64 + std::f64::consts::PI)
.rem_euclid(std::f64::consts::TAU)
- std::f64::consts::PI;
let expected_cycles = freq as f64 * TICKS as f64 / sr as f64;
let fractional_cycles = phase_delta / std::f64::consts::TAU;
let observed_cycles = (expected_cycles - fractional_cycles).round() + fractional_cycles;
let observed_freq = observed_cycles * sr as f64 / TICKS as f64;
let cents_error = 1_200.0 * (observed_freq / freq as f64).log2();
if max_magnitude_error > 0.002 || cents_error.abs() > 0.01 {
failures.push(format!(
"{freq} Hz at {sr} Hz: max magnitude error {max_magnitude_error:.6}, \
frequency error {cents_error:+.6} cents"
));
}
}
assert!(
failures.is_empty(),
"long-held rotating phasors drifted:\n{}",
failures.join("\n")
);
}
#[test]
fn adsr_reaches_sustain_and_dies() {
let sr = 44100.0;
let mut e = Adsr::new(0.01, 0.05, 0.6, 0.05, sr);
for _ in 0..8820 {
e.next();
}
let lvl = e.next();
assert!((lvl - 0.6).abs() < 0.05, "sustain level {lvl}");
e.release();
for _ in 0..22050 {
e.next();
}
assert!(!e.alive());
}
#[test]
fn key_freq_a4() {
assert!((key_freq(69) - 440.0).abs() < 1e-3);
assert!((key_freq(60) - 261.626).abs() < 0.01);
}
#[test]
fn cubic_tap_retains_treble_ring() {
let sr = 44100.0;
let energy = |cubic: bool| {
let delay = 12.5f32; let mut dl = DelayLine::new(20);
let mut damp = OnePole::lowpass(16000.0, sr);
for i in 0..13 {
dl.push(match i {
0 => 1.0,
1 => -1.0,
_ => 0.0,
});
}
let mut acc = 0.0f64;
for n in 0..(0.3 * sr) as usize {
let s = if cubic {
dl.tap_cubic(delay)
} else {
dl.tap(delay)
};
dl.push(damp.process(s) * 0.997);
if n > (0.05 * sr) as usize {
acc += (s as f64) * (s as f64);
}
}
acc.sqrt()
};
let (c, l) = (energy(true), energy(false));
assert!(c > 1.15 * l, "cubic {c} vs linear {l}");
let mut dl = DelayLine::new(16);
for i in 0..12 {
dl.push(i as f32);
}
assert!((dl.tap_cubic(3.0) - dl.tap(3.0)).abs() < 1e-4);
}
#[test]
fn cubic4_is_exact_and_beats_linear_on_treble() {
assert!((cubic4(3.0, 7.0, 11.0, 19.0, 0.0) - 7.0).abs() < 1e-6);
assert!((cubic4(3.0, 7.0, 11.0, 19.0, 1.0) - 11.0).abs() < 1e-6);
let g = |x: f32| x * x * x - 2.0 * x + 1.0;
for &fr in &[0.1f32, 0.37, 0.5, 0.83] {
let got = cubic4(g(-1.0), g(0.0), g(1.0), g(2.0), fr);
assert!(
(got - g(fr)).abs() < 1e-4,
"cubic reconstruction fr={fr}: {got} vs {}",
g(fr)
);
}
let sr = 44100.0;
let n = 4096usize;
let src: Vec<f32> = (0..n)
.map(|k| (std::f32::consts::TAU * 9000.0 * k as f32 / sr).sin())
.collect();
let read = |cubic: bool| {
let step = 1.4993f32; let mut pos = 1.0f32;
let mut acc = 0.0f64;
while (pos as usize) + 2 < n {
let j = pos as usize;
let fr = pos - j as f32;
let s = if cubic {
cubic4(src[j - 1], src[j], src[j + 1], src[j + 2], fr)
} else {
src[j] + (src[j + 1] - src[j]) * fr
};
acc += (s as f64) * (s as f64);
pos += step;
}
acc.sqrt()
};
let (c, l) = (read(true), read(false));
assert!(
c > 1.04 * l,
"cubic {c} vs linear {l} (cubic should retain more treble)"
);
}
#[test]
fn blep_pulse_square_nulls_even_harmonics() {
let sr = 44100.0;
let n = sr as usize; let mut p = BlepPulse::new(220.0, sr, 0.0, 0.5);
let ps: Vec<f32> = (0..n).map(|_| p.next()).collect();
let (p1, p2) = (
crate::testutil::mag_at(&ps, sr, 220.0),
crate::testutil::mag_at(&ps, sr, 440.0),
);
let dc = ps.iter().sum::<f32>() / n as f32;
assert!(p2 < 0.05 * p1, "square H2 {p2} not << H1 {p1}");
assert!(dc.abs() < 0.02, "square DC {dc}");
let mut s = BlepSaw::new(220.0, sr, 0.0);
let ss: Vec<f32> = (0..n).map(|_| s.next()).collect();
let (s1, s2) = (
crate::testutil::mag_at(&ss, sr, 220.0),
crate::testutil::mag_at(&ss, sr, 440.0),
);
assert!(s2 > 0.3 * s1, "saw H2 {s2} should be strong vs H1 {s1}");
}
#[test]
fn reed_pulse_calibration() {
use crate::testutil::mag_at;
use std::f32::consts::PI;
let sr = 44100.0;
let w = 0.30f32;
let norm = 0.5 / (w * (1.0 - w)).sqrt();
let mut p = ReedPulse::new(1000.0, sr, 0.0, w);
let sig: Vec<f32> = (0..sr as usize).map(|_| p.next() * norm).collect();
for n in 1..=5u32 {
let nn = n as f32;
let want = 2.0 * (PI * nn * w).sin().abs() / (PI * nn) * norm;
let got = mag_at(&sig, sr, 1000.0 * nn);
assert!(
(got - want).abs() <= 0.10 * want,
"harmonic {n}: got {got:.4} want {want:.4}"
);
}
let mean = sig.iter().sum::<f32>() / sig.len() as f32;
assert!(mean.abs() < 1e-3, "DC mean {mean:.6}");
let mut p = ReedPulse::new(200.0, sr, 0.0, w);
let sig: Vec<f32> = (0..sr as usize).map(|_| p.next() * norm).collect();
let fund = mag_at(&sig, sr, 200.0);
let (mut acc, mut cnt) = (0.0f64, 0u32);
let mut b = 100.0f32;
while b <= 21_900.0 {
let m = mag_at(&sig, sr, b) as f64;
acc += m * m;
cnt += 1;
b += 200.0; }
let alias = (acc / cnt as f64).sqrt() as f32 / fund;
assert!(alias < 0.02, "alias floor {alias:.4} (need < 0.02)");
}
#[test]
fn grain_gate_rate_is_grains_per_second() {
for sr in [22050.0f32, 44100.0, 48000.0] {
let mut g = GrainGate::new(40.0, 0.02, sr);
let mut rng = Rng::new(9);
let mut onsets = 0u32;
let mut prev = 0.0f32;
let mut nonzero = 0u32;
let n = sr as usize;
for _ in 0..n {
let v = g.next(&mut rng);
if v > prev + 0.5 {
onsets += 1;
}
if v > 0.02 {
nonzero += 1;
}
prev = v;
}
assert!(
(30..=52).contains(&onsets),
"sr {sr}: {onsets} grain onsets/s (want ~40)"
);
assert!(
(nonzero as f32) < 0.55 * n as f32,
"sr {sr}: gate open {}% — not sparse",
100 * nonzero / n as u32
);
}
}
}