Skip to main content

rust_synth/math/
sigmoid.rs

1//! Shaping functions for smooth modulation in [0.0, 1.0].
2//!
3//! All time-domain functions use `f64` so they stay precise after hours
4//! of playback (FunDSP's `hacker` module is double-precision).
5
6/// Logistic sigmoid centered at `x0` with slope `k`.
7#[inline]
8pub fn sigmoid(t: f64, k: f64, x0: f64) -> f64 {
9    1.0 / (1.0 + (-k * (t - x0)).exp())
10}
11
12/// Hermite smoothstep — zero-derivative ends, used for seamless loops.
13#[inline]
14pub fn smoothstep(t: f64, a: f64, b: f64) -> f64 {
15    let x = ((t - a) / (b - a)).clamp(0.0, 1.0);
16    x * x * (3.0 - 2.0 * x)
17}
18
19/// Cubic ease-in-out on [0, 1] — softer than smoothstep.
20#[inline]
21pub fn ease_in_out(t: f64) -> f64 {
22    let x = t.clamp(0.0, 1.0);
23    if x < 0.5 {
24        4.0 * x * x * x
25    } else {
26        let f = -2.0 * x + 2.0;
27        1.0 - f * f * f / 2.0
28    }
29}
30
31/// Soft exponential — `rate` controls curvature, 0 → linear, 5 → steep.
32#[inline]
33pub fn softexp(t: f64, rate: f64) -> f64 {
34    let x = t.clamp(0.0, 1.0);
35    if rate.abs() < 1e-6 {
36        x
37    } else {
38        (rate * x).exp_m1() / rate.exp_m1()
39    }
40}
41
42#[inline]
43pub fn lerp(a: f64, b: f64, t: f64) -> f64 {
44    a + (b - a) * t
45}
46
47#[cfg(test)]
48mod tests {
49    use super::*;
50    use approx::assert_relative_eq;
51
52    #[test]
53    fn sigmoid_midpoint_is_half() {
54        assert_relative_eq!(sigmoid(5.0, 1.0, 5.0), 0.5, epsilon = 1e-12);
55    }
56
57    #[test]
58    fn smoothstep_endpoints() {
59        assert_eq!(smoothstep(-1.0, 0.0, 1.0), 0.0);
60        assert_eq!(smoothstep(2.0, 0.0, 1.0), 1.0);
61    }
62
63    #[test]
64    fn ease_is_monotone() {
65        let samples: Vec<f64> = (0..=100).map(|i| ease_in_out(i as f64 / 100.0)).collect();
66        for w in samples.windows(2) {
67            assert!(w[1] >= w[0] - 1e-12);
68        }
69    }
70}