use std::f32::consts::TAU;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LfoWaveform {
Sine,
Triangle,
Sawtooth,
Square,
Random,
}
#[derive(Debug, Clone)]
pub struct Lfo {
phase: f32,
phase_inc: f32,
waveform: LfoWaveform,
sample_rate: f32,
random_state: u32,
random_value: f32,
}
impl Lfo {
#[must_use]
pub fn new(frequency: f32, sample_rate: f32, waveform: LfoWaveform) -> Self {
let phase_inc = frequency / sample_rate;
Self {
phase: 0.0,
phase_inc,
waveform,
sample_rate,
random_state: 0x1234_5678,
random_value: 0.0,
}
}
#[allow(clippy::should_implement_trait)]
pub fn next(&mut self) -> f32 {
let value = match self.waveform {
LfoWaveform::Sine => (self.phase * TAU).sin(),
LfoWaveform::Triangle => {
if self.phase < 0.5 {
4.0 * self.phase - 1.0
} else {
3.0 - 4.0 * self.phase
}
}
LfoWaveform::Sawtooth => 2.0 * self.phase - 1.0,
LfoWaveform::Square => {
if self.phase < 0.5 {
-1.0
} else {
1.0
}
}
LfoWaveform::Random => {
if self.phase < self.phase_inc {
self.random_state = self
.random_state
.wrapping_mul(1_103_515_245)
.wrapping_add(12_345);
#[allow(clippy::cast_precision_loss)]
let random_val = (self.random_state as f32) / (u32::MAX as f32);
self.random_value = 2.0 * random_val - 1.0;
}
self.random_value
}
};
self.phase += self.phase_inc;
if self.phase >= 1.0 {
self.phase -= 1.0;
}
value
}
pub fn next_unipolar(&mut self) -> f32 {
(self.next() + 1.0) * 0.5
}
pub fn set_frequency(&mut self, frequency: f32) {
self.phase_inc = frequency / self.sample_rate;
}
pub fn set_waveform(&mut self, waveform: LfoWaveform) {
self.waveform = waveform;
}
pub fn reset(&mut self) {
self.phase = 0.0;
}
pub fn set_phase(&mut self, phase: f32) {
self.phase = phase.rem_euclid(1.0);
}
#[must_use]
pub fn phase(&self) -> f32 {
self.phase
}
}
#[derive(Debug, Clone)]
pub struct StereoLfo {
pub left: Lfo,
pub right: Lfo,
}
impl StereoLfo {
#[must_use]
pub fn new(frequency: f32, sample_rate: f32, waveform: LfoWaveform, phase_offset: f32) -> Self {
let left = Lfo::new(frequency, sample_rate, waveform);
let mut right = Lfo::new(frequency, sample_rate, waveform);
right.set_phase(phase_offset);
Self { left, right }
}
#[allow(clippy::should_implement_trait)]
pub fn next(&mut self) -> (f32, f32) {
(self.left.next(), self.right.next())
}
pub fn next_unipolar(&mut self) -> (f32, f32) {
(self.left.next_unipolar(), self.right.next_unipolar())
}
pub fn set_frequency(&mut self, frequency: f32) {
self.left.set_frequency(frequency);
self.right.set_frequency(frequency);
}
pub fn set_waveform(&mut self, waveform: LfoWaveform) {
self.left.set_waveform(waveform);
self.right.set_waveform(waveform);
}
pub fn reset(&mut self) {
self.left.reset();
self.right.reset();
}
}
#[derive(Debug, Clone)]
pub struct ParameterSmoother {
current: f32,
target: f32,
coefficient: f32,
}
impl ParameterSmoother {
#[must_use]
pub fn new(time_constant_ms: f32, sample_rate: f32) -> Self {
let coefficient = (-1000.0 / (time_constant_ms * sample_rate)).exp();
Self {
current: 0.0,
target: 0.0,
coefficient,
}
}
pub fn set_target(&mut self, target: f32) {
self.target = target;
}
#[allow(clippy::should_implement_trait)]
pub fn next(&mut self) -> f32 {
self.current = self.target + self.coefficient * (self.current - self.target);
self.current
}
pub fn reset(&mut self, value: f32) {
self.current = value;
self.target = value;
}
#[must_use]
pub fn is_stable(&self) -> bool {
(self.current - self.target).abs() < 1e-3
}
}
#[derive(Debug, Clone)]
pub struct SmoothedParameter {
current: f32,
target: f32,
coeff: f32,
smooth_time_ms: f32,
sample_rate: f32,
min_val: f32,
max_val: f32,
steps_remaining: u32,
total_steps: u32,
mode: SmoothingMode,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SmoothingMode {
Linear,
Exponential,
Logarithmic,
}
impl SmoothedParameter {
#[must_use]
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
pub fn new(initial: f32, smooth_time_ms: f32, sample_rate: f32) -> Self {
let total_steps = (smooth_time_ms * 0.001 * sample_rate).max(1.0) as u32;
let coeff = Self::compute_coeff(smooth_time_ms, sample_rate);
Self {
current: initial,
target: initial,
coeff,
smooth_time_ms,
sample_rate,
min_val: f32::NEG_INFINITY,
max_val: f32::INFINITY,
steps_remaining: 0,
total_steps,
mode: SmoothingMode::Exponential,
}
}
#[must_use]
pub fn with_range(mut self, min: f32, max: f32) -> Self {
self.min_val = min;
self.max_val = max;
self.current = self.current.clamp(min, max);
self.target = self.target.clamp(min, max);
self
}
#[must_use]
pub fn with_mode(mut self, mode: SmoothingMode) -> Self {
self.mode = mode;
self
}
pub fn set(&mut self, target: f32) {
self.target = target.clamp(self.min_val, self.max_val);
self.steps_remaining = self.total_steps;
}
pub fn set_immediate(&mut self, value: f32) {
let v = value.clamp(self.min_val, self.max_val);
self.current = v;
self.target = v;
self.steps_remaining = 0;
}
#[allow(clippy::should_implement_trait)]
pub fn next(&mut self) -> f32 {
if self.steps_remaining == 0 {
self.current = self.target;
return self.current;
}
self.steps_remaining = self.steps_remaining.saturating_sub(1);
match self.mode {
SmoothingMode::Linear => {
let step = (self.target - self.current) / (self.steps_remaining + 1) as f32;
self.current += step;
}
SmoothingMode::Exponential => {
self.current = self.target + self.coeff * (self.current - self.target);
}
SmoothingMode::Logarithmic => {
let epsilon = 1e-10;
let current_log = (self.current.abs() + epsilon).ln();
let target_log = (self.target.abs() + epsilon).ln();
let smoothed_log = target_log + self.coeff * (current_log - target_log);
let sign = if self.target >= 0.0 { 1.0 } else { -1.0 };
self.current = sign * smoothed_log.exp();
}
}
if self.steps_remaining == 0 {
self.current = self.target;
}
self.current
}
pub fn process_block(&mut self, output: &mut [f32]) {
for sample in output.iter_mut() {
*sample = self.next();
}
}
#[must_use]
pub fn is_smoothing(&self) -> bool {
self.steps_remaining > 0
}
#[must_use]
pub fn current(&self) -> f32 {
self.current
}
#[must_use]
pub fn target(&self) -> f32 {
self.target
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
pub fn set_sample_rate(&mut self, sample_rate: f32) {
self.sample_rate = sample_rate;
self.coeff = Self::compute_coeff(self.smooth_time_ms, sample_rate);
self.total_steps = (self.smooth_time_ms * 0.001 * sample_rate).max(1.0) as u32;
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
pub fn set_smooth_time(&mut self, ms: f32) {
self.smooth_time_ms = ms;
self.coeff = Self::compute_coeff(ms, self.sample_rate);
self.total_steps = (ms * 0.001 * self.sample_rate).max(1.0) as u32;
}
fn compute_coeff(time_ms: f32, sample_rate: f32) -> f32 {
let samples = time_ms * 0.001 * sample_rate;
if samples > 0.0 {
(-2.2 / samples as f64).exp() as f32
} else {
0.0
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_lfo_sine() {
let mut lfo = Lfo::new(1.0, 100.0, LfoWaveform::Sine);
let v0 = lfo.next();
assert!(v0.abs() < 0.1);
for _ in 0..24 {
lfo.next();
}
let v25 = lfo.next();
assert!((v25 - 1.0).abs() < 0.1);
}
#[test]
fn test_lfo_triangle() {
let mut lfo = Lfo::new(1.0, 100.0, LfoWaveform::Triangle);
let v0 = lfo.next();
assert!((v0 + 1.0).abs() < 0.1);
let v1 = lfo.next();
assert!(v1 > v0);
}
#[test]
fn test_lfo_square() {
let mut lfo = Lfo::new(1.0, 100.0, LfoWaveform::Square);
for _ in 0..100 {
let val = lfo.next();
assert!(
val == -1.0 || val == 1.0,
"Square wave should only be -1.0 or 1.0"
);
}
}
#[test]
fn test_lfo_unipolar() {
let mut lfo = Lfo::new(1.0, 100.0, LfoWaveform::Sine);
for _ in 0..100 {
let val = lfo.next_unipolar();
assert!(val >= 0.0 && val <= 1.0);
}
}
#[test]
fn test_stereo_lfo() {
let mut lfo = StereoLfo::new(1.0, 100.0, LfoWaveform::Sine, 0.5);
let (l, r) = lfo.next();
assert!((l + r).abs() < 0.1);
}
#[test]
fn test_parameter_smoother() {
let mut smoother = ParameterSmoother::new(10.0, 48000.0);
smoother.reset(0.0);
smoother.set_target(1.0);
let v1 = smoother.next();
let v2 = smoother.next();
let v3 = smoother.next();
assert!(v1 > 0.0 && v1 < 1.0);
assert!(v2 > v1);
assert!(v3 > v2);
for _ in 0..100000 {
smoother.next();
}
assert!(smoother.is_stable());
}
#[test]
fn test_lfo_reset() {
let mut lfo = Lfo::new(1.0, 100.0, LfoWaveform::Sine);
for _ in 0..25 {
lfo.next();
}
assert!(lfo.phase() > 0.0);
lfo.reset();
assert_eq!(lfo.phase(), 0.0);
}
#[test]
fn test_smoothed_parameter_creation() {
let param = SmoothedParameter::new(0.5, 10.0, 48000.0);
assert!((param.current() - 0.5).abs() < 1e-6);
assert!((param.target() - 0.5).abs() < 1e-6);
assert!(!param.is_smoothing());
}
#[test]
fn test_smoothed_parameter_set_and_smooth() {
let mut param = SmoothedParameter::new(0.0, 10.0, 48000.0);
param.set(1.0);
assert!(param.is_smoothing());
let mut prev = 0.0;
for _ in 0..480 {
let v = param.next();
assert!(v >= prev - 1e-6, "Value should not decrease: {v} < {prev}");
assert!(v.is_finite());
prev = v;
}
for _ in 0..48000 {
param.next();
}
assert!((param.current() - 1.0).abs() < 1e-3);
assert!(!param.is_smoothing());
}
#[test]
fn test_smoothed_parameter_immediate() {
let mut param = SmoothedParameter::new(0.0, 10.0, 48000.0);
param.set_immediate(0.75);
assert!((param.current() - 0.75).abs() < 1e-6);
assert!(!param.is_smoothing());
}
#[test]
fn test_smoothed_parameter_range_clamp() {
let mut param = SmoothedParameter::new(0.5, 10.0, 48000.0).with_range(0.0, 1.0);
param.set(2.0);
assert!((param.target() - 1.0).abs() < 1e-6);
param.set(-1.0);
assert!((param.target() - 0.0).abs() < 1e-6);
}
#[test]
fn test_smoothed_parameter_linear_mode() {
let mut param = SmoothedParameter::new(0.0, 10.0, 48000.0).with_mode(SmoothingMode::Linear);
param.set(1.0);
let mut prev = 0.0;
for _ in 0..480 {
let v = param.next();
assert!(v >= prev - 1e-6);
prev = v;
}
}
#[test]
fn test_smoothed_parameter_logarithmic_mode() {
let mut param = SmoothedParameter::new(0.1, 10.0, 48000.0)
.with_mode(SmoothingMode::Logarithmic)
.with_range(0.001, 10.0);
param.set(1.0);
for _ in 0..48000 {
let v = param.next();
assert!(v.is_finite());
}
assert!((param.current() - 1.0).abs() < 0.01);
}
#[test]
fn test_smoothed_parameter_block_processing() {
let mut param = SmoothedParameter::new(0.0, 5.0, 48000.0);
param.set(1.0);
let mut block = vec![0.0f32; 256];
param.process_block(&mut block);
for &v in &block {
assert!(v.is_finite());
}
assert!(block[255] > block[0]);
}
#[test]
fn test_smoothed_parameter_no_zipper() {
let mut param = SmoothedParameter::new(0.0, 50.0, 48000.0);
param.set(1.0);
let total_steps = (50.0 * 0.001 * 48000.0) as usize; let check_steps = total_steps * 9 / 10;
let mut prev = 0.0f32;
let mut max_delta = 0.0f32;
for _ in 0..check_steps {
let v = param.next();
let delta = (v - prev).abs();
if delta > max_delta {
max_delta = delta;
}
prev = v;
}
assert!(
max_delta < 0.05,
"Max delta {max_delta} too large — zipper noise detected"
);
}
#[test]
fn test_smoothed_parameter_retarget() {
let mut param = SmoothedParameter::new(0.0, 10.0, 48000.0);
param.set(1.0);
for _ in 0..240 {
param.next();
}
let mid = param.current();
param.set(0.5);
for _ in 0..48000 {
param.next();
}
assert!((param.current() - 0.5).abs() < 1e-3);
assert!(mid > 0.0);
}
#[test]
fn test_smoothed_parameter_set_sample_rate() {
let mut param = SmoothedParameter::new(0.5, 10.0, 48000.0);
param.set_sample_rate(96000.0);
param.set(1.0);
for _ in 0..96000 {
param.next();
}
assert!((param.current() - 1.0).abs() < 1e-3);
}
#[test]
fn test_smoothed_parameter_set_smooth_time() {
let mut param = SmoothedParameter::new(0.0, 5.0, 48000.0);
param.set_smooth_time(50.0);
param.set(1.0);
for _ in 0..48000 {
param.next();
}
assert!((param.current() - 1.0).abs() < 1e-3);
}
}