use serde::{Deserialize, Serialize};
#[must_use]
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[non_exhaustive]
#[serde(default)]
pub struct GainSmootherParams {
pub attack: f32,
pub release: f32,
}
impl Default for GainSmootherParams {
fn default() -> Self {
Self {
attack: 0.3,
release: 0.05,
}
}
}
impl GainSmootherParams {
pub fn validate(&self) -> Result<(), &'static str> {
if !(0.0..=1.0).contains(&self.attack) {
return Err("attack must be in 0.0..=1.0");
}
if !(0.0..=1.0).contains(&self.release) {
return Err("release must be in 0.0..=1.0");
}
Ok(())
}
}
#[must_use]
#[derive(Debug, Clone)]
pub struct GainSmoother {
params: GainSmootherParams,
current: f32,
}
impl GainSmoother {
pub fn new(attack: f32, release: f32) -> Self {
tracing::debug!(attack, release, "GainSmoother::new");
Self {
params: GainSmootherParams {
attack: attack.clamp(0.0, 1.0),
release: release.clamp(0.0, 1.0),
},
current: 1.0,
}
}
pub fn from_params(params: GainSmootherParams) -> Self {
Self {
params,
current: 1.0,
}
}
#[inline]
pub fn smooth(&mut self, target: f32) -> f32 {
let alpha = if target < self.current {
self.params.attack
} else {
self.params.release
};
self.current += alpha * (target - self.current);
self.current
}
pub fn current(&self) -> f32 {
self.current
}
pub fn reset(&mut self, value: f32) {
self.current = value;
}
pub fn set_params(&mut self, params: GainSmootherParams) {
self.params = params;
}
pub fn params(&self) -> &GainSmootherParams {
&self.params
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn converges_toward_target() {
let mut s = GainSmoother::new(0.3, 0.05);
for _ in 0..50 {
s.smooth(0.5);
}
assert!((s.current() - 0.5).abs() < 0.01);
}
#[test]
fn attack_faster_than_release() {
let mut s1 = GainSmoother::new(0.3, 0.05);
let mut s2 = GainSmoother::new(0.3, 0.05);
let after_attack = s1.smooth(0.5);
s2.reset(0.5);
let after_release = s2.smooth(1.0);
let attack_delta = (1.0 - after_attack).abs();
let release_delta = (after_release - 0.5).abs();
assert!(attack_delta > release_delta);
}
#[test]
fn reset_sets_value() {
let mut s = GainSmoother::new(0.3, 0.05);
s.smooth(0.5);
s.reset(1.0);
assert_eq!(s.current(), 1.0);
}
#[test]
fn identity_when_target_equals_current() {
let mut s = GainSmoother::new(0.3, 0.05);
let result = s.smooth(1.0);
assert_eq!(result, 1.0);
}
#[test]
fn params_default() {
let p = GainSmootherParams::default();
assert_eq!(p.attack, 0.3);
assert_eq!(p.release, 0.05);
assert!(p.validate().is_ok());
}
#[test]
fn params_validate_rejects_out_of_range() {
let p = GainSmootherParams {
attack: 1.5,
release: 0.05,
};
assert!(p.validate().is_err());
let p = GainSmootherParams {
attack: 0.3,
release: -0.1,
};
assert!(p.validate().is_err());
}
#[test]
fn serde_roundtrip() {
let p = GainSmootherParams {
attack: 0.2,
release: 0.1,
};
let json = serde_json::to_string(&p).unwrap();
let back: GainSmootherParams = serde_json::from_str(&json).unwrap();
assert_eq!(back.attack, p.attack);
assert_eq!(back.release, p.release);
}
}