use super::smoother::Smoother;
const NOISE_LEVEL: f64 = 0.12; const DECAY_K: f64 = 5.0; const SMOOTH_S: f64 = 0.008;
pub struct Gater {
pub enabled: bool,
sample_rate: f64,
phase: f64, gate_hz: f64, bpm: f64,
beat_value: f64,
shape_target: f64,
shape_smooth: Smoother,
depth_target: f64,
depth_smooth: Smoother,
noise_seed: u64, }
impl Gater {
pub fn new(sample_rate: f64) -> Self {
let bpm = 120.0;
let beat_value = 0.5; Self {
enabled: false,
sample_rate,
phase: 0.0,
gate_hz: bpm / (60.0 * beat_value),
bpm,
beat_value,
shape_target: 0.5,
shape_smooth: Smoother::new(sample_rate, SMOOTH_S),
depth_target: 0.75,
depth_smooth: Smoother::new(sample_rate, SMOOTH_S),
noise_seed: 0xDEAD_BEEF_1234_5678,
}
}
pub fn set_enabled(&mut self, enabled: bool) {
if enabled && !self.enabled {
self.phase = 0.0;
self.shape_smooth.reset(self.shape_target);
self.depth_smooth.reset(self.depth_target);
}
self.enabled = enabled;
}
pub fn set_depth(&mut self, depth: f64) {
self.depth_target = depth.clamp(0.0, 1.0);
}
pub fn set_shape(&mut self, shape: f64) {
self.shape_target = shape.clamp(0.0, 1.0);
}
pub fn set_bpm(&mut self, bpm: f64) {
if bpm > 0.0 && (bpm - self.bpm).abs() > 0.01 {
self.bpm = bpm;
self.update_gate_hz();
}
}
pub fn set_beat_value(&mut self, beat_value: f64) {
if beat_value > 0.0 && (beat_value - self.beat_value).abs() > 1e-9 {
self.beat_value = beat_value;
self.update_gate_hz();
}
}
fn update_gate_hz(&mut self) {
self.gate_hz = self.bpm / (60.0 * self.beat_value.max(1e-6));
}
#[inline]
fn next_noise(&mut self) -> f64 {
let s = &mut self.noise_seed;
*s ^= *s << 13;
*s ^= *s >> 7;
*s ^= *s << 17;
(*s as i64 as f64) * (1.0 / i64::MAX as f64)
}
pub fn process_sample(&mut self, sample: &mut [f64; 2]) {
if !self.enabled {
return;
}
let depth = self.depth_smooth.process(self.depth_target);
let shape = self.shape_smooth.process(self.shape_target);
self.phase += self.gate_hz / self.sample_rate;
if self.phase >= 1.0 {
self.phase -= 1.0;
}
let gate_gain = if shape > 1.0 - 1e-3 || self.phase < shape {
1.0
} else {
let t = (self.phase - shape) / (1.0 - shape);
(-t * DECAY_K).exp()
};
let noise = self.next_noise() * NOISE_LEVEL;
let effective_gain = 1.0 - depth * (1.0 - gate_gain);
let noise_add = noise * depth * (1.0 - gate_gain);
sample[0] = sample[0] * effective_gain + noise_add;
sample[1] = sample[1] * effective_gain + noise_add;
}
}
#[cfg(test)]
mod tests {
use super::*;
const SR: f64 = 44100.0;
fn armed(beat_value: f64, shape: f64, depth: f64) -> Gater {
let mut g = Gater::new(SR);
g.set_bpm(120.0);
g.set_beat_value(beat_value);
g.set_shape(shape);
g.set_depth(depth);
g.set_enabled(true);
g
}
#[test]
fn disabled_passes_through() {
let mut g = Gater::new(SR);
g.set_depth(1.0);
let mut s = [0.8_f64, 0.4_f64];
g.process_sample(&mut s);
assert_eq!(s, [0.8, 0.4]);
}
#[test]
fn depth_zero_is_dry() {
let mut g = armed(0.5, 0.5, 0.0);
for _ in 0..4000 { let mut s = [0.5_f64; 2]; g.process_sample(&mut s); }
let mut s = [0.6_f64, 0.3_f64];
g.process_sample(&mut s);
assert!((s[0] - 0.6).abs() < 1e-3, "depth=0 should be dry; got {}", s[0]);
assert!((s[1] - 0.3).abs() < 1e-3, "depth=0 should be dry; got {}", s[1]);
}
#[test]
fn gate_cycles_at_correct_rate() {
let mut g = armed(0.5, 0.5, 1.0);
for _ in 0..4000 { let mut s = [1.0_f64; 2]; g.process_sample(&mut s); }
let rms = |gtr: &mut Gater, n: usize| {
let mut e = 0.0_f64;
for _ in 0..n { let mut s = [1.0_f64; 2]; gtr.process_sample(&mut s); e += s[0]*s[0]; }
(e / n as f64).sqrt()
};
let half_period = (SR / (4.0 * 2.0)) as usize; let a = rms(&mut g, 256);
for _ in 0..half_period { let mut s = [1.0_f64; 2]; g.process_sample(&mut s); }
let b = rms(&mut g, 256);
assert!((a - b).abs() > 0.05,
"gate should modulate power; rms_a={a:.4} rms_b={b:.4}");
}
#[test]
fn shape_one_is_always_open() {
let mut g = armed(0.5, 1.0, 1.0);
for _ in 0..4000 { let mut s = [0.5_f64; 2]; g.process_sample(&mut s); }
let mut total_diff = 0.0_f64;
for _ in 0..1024 {
let mut s = [1.0_f64; 2];
g.process_sample(&mut s);
total_diff += (s[0] - 1.0).abs();
}
assert!(total_diff < 1e-6, "shape=1 should pass dry; total_diff={total_diff:.6}");
}
#[test]
fn enable_resets_phase() {
let mut g = armed(1.0, 0.5, 0.8);
for _ in 0..5000 { let mut s = [0.5_f64; 2]; g.process_sample(&mut s); }
g.set_enabled(false);
g.set_enabled(true);
assert!(g.phase < 1e-6, "phase should reset to 0 on re-arm");
}
#[test]
fn noise_seed_advances() {
let mut g = armed(0.25, 0.0, 1.0); let n1 = g.next_noise();
let n2 = g.next_noise();
assert_ne!(n1, n2, "noise generator must advance each call");
}
#[test]
fn output_bounded() {
let mut g = armed(0.25, 0.0, 1.0);
for _ in 0..44100 {
let mut s = [0.9_f64; 2];
g.process_sample(&mut s);
assert!(s[0].abs() <= 1.2 && s[1].abs() <= 1.2,
"output out of range: {} {}", s[0], s[1]);
}
}
}