#![allow(dead_code)]
use std::f64::consts::PI;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LfoWaveform {
Sine,
Triangle,
Square,
Sawtooth,
Random,
}
#[derive(Debug, Clone)]
pub struct TremoloParams {
pub rate_hz: f64,
pub depth: f64,
pub waveform: LfoWaveform,
pub stereo_phase: f64,
}
impl Default for TremoloParams {
fn default() -> Self {
Self {
rate_hz: 5.0,
depth: 0.5,
waveform: LfoWaveform::Sine,
stereo_phase: 0.0,
}
}
}
pub struct TremoloProcessor {
pub params: TremoloParams,
pub phase: f64,
pub sample_rate: f64,
rand_state: u64,
rand_value: f64,
rand_phase_acc: f64,
}
impl TremoloProcessor {
#[must_use]
pub fn new(sample_rate: f64, params: TremoloParams) -> Self {
Self {
params,
phase: 0.0,
sample_rate,
rand_state: 0x853c_49e6_748f_ea9b,
rand_value: 0.0,
rand_phase_acc: 0.0,
}
}
#[must_use]
pub fn lfo_value(&self) -> f64 {
let bipolar = generate_lfo(self.params.waveform, self.phase);
(bipolar + 1.0) * 0.5 }
pub fn process_sample(&mut self, input: f64) -> f64 {
let mod_val = self.lfo_value();
let gain = 1.0 - self.params.depth + mod_val * self.params.depth;
let output = input * gain;
self.advance_phase();
output
}
pub fn process_block(&mut self, block: &mut [f64]) {
for sample in block.iter_mut() {
*sample = self.process_sample(*sample);
}
}
fn advance_phase(&mut self) {
let phase_inc = self.params.rate_hz / self.sample_rate;
self.phase = (self.phase + phase_inc).rem_euclid(1.0);
if self.params.waveform == LfoWaveform::Random {
self.rand_phase_acc += phase_inc;
if self.rand_phase_acc >= 1.0 {
self.rand_phase_acc -= 1.0;
self.rand_state ^= self.rand_state << 13;
self.rand_state ^= self.rand_state >> 7;
self.rand_state ^= self.rand_state << 17;
#[allow(clippy::cast_precision_loss)]
let rand_value = (self.rand_state as f64 / u64::MAX as f64) * 2.0 - 1.0;
self.rand_value = rand_value;
}
}
}
}
#[must_use]
pub fn generate_lfo(waveform: LfoWaveform, phase: f64) -> f64 {
match waveform {
LfoWaveform::Sine => (2.0 * PI * phase).sin(),
LfoWaveform::Triangle => {
if phase < 0.5 {
4.0 * phase - 1.0
} else {
3.0 - 4.0 * phase
}
}
LfoWaveform::Square => {
if phase < 0.5 {
1.0
} else {
-1.0
}
}
LfoWaveform::Sawtooth => {
2.0 * phase - 1.0
}
LfoWaveform::Random => {
0.0
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_lfo_sine_at_zero() {
let v = generate_lfo(LfoWaveform::Sine, 0.0);
assert!(v.abs() < 1e-10, "sin(0) should be ~0, got {v}");
}
#[test]
fn test_generate_lfo_sine_at_quarter() {
let v = generate_lfo(LfoWaveform::Sine, 0.25);
assert!((v - 1.0).abs() < 1e-10, "sin(pi/2) should be 1, got {v}");
}
#[test]
fn test_generate_lfo_triangle_at_half() {
let v = generate_lfo(LfoWaveform::Triangle, 0.25);
assert!((v - 0.0).abs() < 1e-10, "Triangle at 0.25 = 0, got {v}");
}
#[test]
fn test_generate_lfo_triangle_peak() {
let v = generate_lfo(LfoWaveform::Triangle, 0.5);
assert!((v - 1.0).abs() < 1e-10, "Triangle at 0.5 = 1, got {v}");
}
#[test]
fn test_generate_lfo_square() {
assert_eq!(generate_lfo(LfoWaveform::Square, 0.25), 1.0);
assert_eq!(generate_lfo(LfoWaveform::Square, 0.75), -1.0);
}
#[test]
fn test_generate_lfo_sawtooth() {
let v = generate_lfo(LfoWaveform::Sawtooth, 0.0);
assert!((v - (-1.0)).abs() < 1e-10, "Sawtooth at 0 = -1, got {v}");
let v = generate_lfo(LfoWaveform::Sawtooth, 1.0);
assert!((v - 1.0).abs() < 1e-10, "Sawtooth at 1 = 1, got {v}");
}
#[test]
fn test_lfo_value_is_unipolar() {
let params = TremoloParams::default();
let proc = TremoloProcessor::new(48000.0, params);
let v = proc.lfo_value();
assert!(
v >= 0.0 && v <= 1.0,
"LFO value should be in [0,1], got {v}"
);
}
#[test]
fn test_process_sample_zero_depth() {
let params = TremoloParams {
depth: 0.0,
..Default::default()
};
let mut proc = TremoloProcessor::new(48000.0, params);
let out = proc.process_sample(0.8);
assert!(
(out - 0.8).abs() < 1e-10,
"Zero depth = pass-through, got {out}"
);
}
#[test]
fn test_process_sample_full_depth_at_trough() {
let params = TremoloParams {
depth: 1.0,
rate_hz: 1.0,
..Default::default()
};
let mut proc = TremoloProcessor::new(48000.0, params);
proc.phase = 0.75; let out = proc.process_sample(1.0);
assert!(
out.abs() < 0.05,
"Full depth at trough should be near 0, got {out}"
);
}
#[test]
fn test_process_sample_output_is_finite() {
let mut proc = TremoloProcessor::new(44100.0, TremoloParams::default());
for _ in 0..1024 {
let out = proc.process_sample(0.5);
assert!(out.is_finite(), "Output should always be finite");
}
}
#[test]
fn test_process_block_length_preserved() {
let mut proc = TremoloProcessor::new(48000.0, TremoloParams::default());
let mut block = vec![0.3f64; 512];
proc.process_block(&mut block);
assert_eq!(block.len(), 512);
}
#[test]
fn test_phase_advances() {
let params = TremoloParams {
rate_hz: 100.0,
..Default::default()
};
let mut proc = TremoloProcessor::new(1000.0, params);
let initial_phase = proc.phase;
proc.process_sample(0.0);
assert!(
proc.phase > initial_phase,
"Phase should advance after one sample"
);
}
#[test]
fn test_process_block_all_waveforms() {
for waveform in [
LfoWaveform::Sine,
LfoWaveform::Triangle,
LfoWaveform::Square,
LfoWaveform::Sawtooth,
LfoWaveform::Random,
] {
let params = TremoloParams {
waveform,
..Default::default()
};
let mut proc = TremoloProcessor::new(48000.0, params);
let mut block = vec![0.5f64; 256];
proc.process_block(&mut block);
for &s in &block {
assert!(
s.is_finite(),
"Waveform {waveform:?} produced non-finite output"
);
}
}
}
}