const SNAP_THRESHOLD: f64 = 1e-8;
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum SmoothingStyle {
#[default]
None,
Linear(f64),
Exponential(f64),
Logarithmic(f64),
}
#[derive(Debug, Clone)]
pub struct Smoother {
style: SmoothingStyle,
sample_rate: f64,
current: f64,
target: f64,
coefficient: f64, step_size: f64, steps_remaining: u32, }
impl Smoother {
pub fn new(style: SmoothingStyle) -> Self {
Self {
style,
sample_rate: 0.0,
current: 0.0,
target: 0.0,
coefficient: 0.0,
step_size: 0.0,
steps_remaining: 0,
}
}
pub fn none() -> Self {
Self::new(SmoothingStyle::None)
}
pub fn style(&self) -> SmoothingStyle {
self.style
}
pub fn set_sample_rate(&mut self, sample_rate: f64) {
self.sample_rate = sample_rate;
self.recompute_coefficients();
}
pub fn set_target(&mut self, target: f64) {
if (self.target - target).abs() < 1e-10 {
return;
}
self.target = target;
match self.style {
SmoothingStyle::None => {
self.current = target;
}
SmoothingStyle::Linear(ms) => {
let samples = (ms * self.sample_rate / 1000.0) as u32;
self.steps_remaining = samples.max(1);
self.step_size = (target - self.current) / self.steps_remaining as f64;
}
SmoothingStyle::Exponential(_) | SmoothingStyle::Logarithmic(_) => {
}
}
}
pub fn reset(&mut self, value: f64) {
self.current = value;
self.target = value;
self.steps_remaining = 0;
self.step_size = 0.0;
}
#[inline]
pub fn tick(&mut self) -> f64 {
match self.style {
SmoothingStyle::None => self.target,
SmoothingStyle::Linear(_) => {
if self.steps_remaining > 0 {
self.current += self.step_size;
self.steps_remaining -= 1;
if self.steps_remaining == 0 {
self.current = self.target;
}
}
self.current
}
SmoothingStyle::Exponential(_) => {
self.current += self.coefficient * (self.target - self.current);
if (self.current - self.target).abs() < SNAP_THRESHOLD {
self.current = self.target;
}
self.current
}
SmoothingStyle::Logarithmic(_) => {
if self.target > 0.0 && self.current > 0.0 {
let log_current = self.current.ln();
let log_target = self.target.ln();
let log_next = log_current + self.coefficient * (log_target - log_current);
self.current = log_next.exp();
if (self.current - self.target).abs() < SNAP_THRESHOLD {
self.current = self.target;
}
} else {
self.current = self.target;
}
self.current
}
}
}
#[inline]
pub fn current(&self) -> f64 {
match self.style {
SmoothingStyle::None => self.target,
_ => self.current,
}
}
#[inline]
pub fn target(&self) -> f64 {
self.target
}
pub fn skip(&mut self, samples: usize) {
match self.style {
SmoothingStyle::None => {}
SmoothingStyle::Linear(_) => {
let skip_count = (samples as u32).min(self.steps_remaining);
if skip_count > 0 {
self.current += self.step_size * skip_count as f64;
self.steps_remaining -= skip_count;
if self.steps_remaining == 0 {
self.current = self.target;
}
}
}
SmoothingStyle::Exponential(_) => {
let decay = (1.0 - self.coefficient).powi(samples as i32);
self.current = self.target + (self.current - self.target) * decay;
if (self.current - self.target).abs() < SNAP_THRESHOLD {
self.current = self.target;
}
}
SmoothingStyle::Logarithmic(_) => {
if self.target > 0.0 && self.current > 0.0 {
let log_current = self.current.ln();
let log_target = self.target.ln();
let decay = (1.0 - self.coefficient).powi(samples as i32);
let log_result = log_target + (log_current - log_target) * decay;
self.current = log_result.exp();
if (self.current - self.target).abs() < SNAP_THRESHOLD {
self.current = self.target;
}
} else {
self.current = self.target;
}
}
}
}
pub fn fill(&mut self, buffer: &mut [f64]) {
for sample in buffer.iter_mut() {
*sample = self.tick();
}
}
pub fn fill_f32(&mut self, buffer: &mut [f32]) {
for sample in buffer.iter_mut() {
*sample = self.tick() as f32;
}
}
#[inline]
pub fn is_smoothing(&self) -> bool {
match self.style {
SmoothingStyle::None => false,
SmoothingStyle::Linear(_) => self.steps_remaining > 0,
SmoothingStyle::Exponential(_) | SmoothingStyle::Logarithmic(_) => {
(self.current - self.target).abs() > SNAP_THRESHOLD
}
}
}
fn recompute_coefficients(&mut self) {
if self.sample_rate <= 0.0 {
return;
}
match self.style {
SmoothingStyle::None => {}
SmoothingStyle::Linear(_) => {
}
SmoothingStyle::Exponential(ms) | SmoothingStyle::Logarithmic(ms) => {
let tau = ms / 1000.0;
let samples_per_tau = tau * self.sample_rate;
if samples_per_tau > 0.0 {
self.coefficient = 1.0 - (-1.0 / samples_per_tau).exp();
} else {
self.coefficient = 1.0; }
}
}
}
}
impl Default for Smoother {
fn default() -> Self {
Self::none()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_no_smoothing() {
let mut s = Smoother::new(SmoothingStyle::None);
s.set_sample_rate(44100.0);
s.reset(0.0);
s.set_target(1.0);
assert!((s.tick() - 1.0).abs() < 1e-10);
assert!(!s.is_smoothing());
}
#[test]
fn test_linear_reaches_target() {
let mut s = Smoother::new(SmoothingStyle::Linear(10.0)); s.set_sample_rate(1000.0); s.reset(0.0);
s.set_target(1.0);
for _ in 0..10 {
s.tick();
}
assert!((s.current() - 1.0).abs() < 1e-10);
assert!(!s.is_smoothing());
}
#[test]
fn test_exponential_approaches_target() {
let mut s = Smoother::new(SmoothingStyle::Exponential(5.0)); s.set_sample_rate(44100.0);
s.reset(0.0);
s.set_target(1.0);
for _ in 0..10000 {
s.tick();
}
assert!((s.current() - 1.0).abs() < 1e-6);
}
#[test]
fn test_skip_linear() {
let mut s = Smoother::new(SmoothingStyle::Linear(10.0));
s.set_sample_rate(1000.0);
s.reset(0.0);
s.set_target(1.0);
s.skip(5);
assert!((s.current() - 0.5).abs() < 1e-10);
s.skip(5);
assert!((s.current() - 1.0).abs() < 1e-10);
}
#[test]
fn test_fill_f32() {
let mut s = Smoother::new(SmoothingStyle::Linear(10.0));
s.set_sample_rate(1000.0);
s.reset(0.0);
s.set_target(1.0);
let mut buffer = [0.0f32; 10];
s.fill_f32(&mut buffer);
assert!(buffer[0] > 0.0);
assert!((buffer[9] - 1.0).abs() < 1e-5);
}
}