Skip to main content

prime_osc/
lib.rs

1//! prime-osc — Oscillators and envelopes.
2//!
3//! All public functions are LOAD + COMPUTE. No STORE. No JUMP.
4//! State threads forward as an explicit parameter. Same inputs → same output.
5
6use std::f32::consts::TAU;
7
8// ── LFO shape functions ──────────────────────────────────────────────────────
9
10/// Sine wave at normalised phase.
11///
12/// # Math
13///   y = sin(phase × 2π)
14///
15/// # Arguments
16/// * `phase` - Normalised phase in [0, 1). Wraps automatically.
17///
18/// # Returns
19/// Value in [-1, 1].
20///
21/// # Example
22/// ```rust
23/// let y = prime_osc::lfo_sine(0.25); // ≈ 1.0 — quarter cycle
24/// assert!((y - 1.0).abs() < 1e-5);
25/// ```
26pub fn lfo_sine(phase: f32) -> f32 {
27    (phase * TAU).sin()
28}
29
30/// Triangle wave at normalised phase. Phase-aligned with lfo_sine (peak at 0.25).
31///
32/// # Math
33///   p = frac(phase + 0.75)
34///   y = 2 × |2p − 1| − 1
35///
36/// # Arguments
37/// * `phase` - Normalised phase in [0, 1). Wraps automatically.
38///
39/// # Returns
40/// Value in [-1, 1]. Zero at phase=0, peaks at phase=0.25, troughs at phase=0.75.
41///
42/// # Example
43/// ```rust
44/// let y = prime_osc::lfo_triangle(0.25); // 1.0 — peak
45/// assert!((y - 1.0).abs() < 1e-5);
46/// ```
47pub fn lfo_triangle(phase: f32) -> f32 {
48    let p = (phase + 0.75) - (phase + 0.75).floor();
49    2.0 * (2.0 * p - 1.0).abs() - 1.0
50}
51
52/// Sawtooth wave at normalised phase (rising).
53///
54/// # Math
55///   y = 2 × frac(phase) − 1
56///
57/// # Arguments
58/// * `phase` - Normalised phase in [0, 1). Wraps automatically.
59///
60/// # Returns
61/// Value in [-1, 1). Rises from -1 to +1, resets at each cycle.
62///
63/// # Example
64/// ```rust
65/// let y = prime_osc::lfo_sawtooth(0.0); // -1.0 — start of cycle
66/// assert!((y + 1.0).abs() < 1e-5);
67/// ```
68pub fn lfo_sawtooth(phase: f32) -> f32 {
69    let p = phase - phase.floor();
70    2.0 * p - 1.0
71}
72
73/// Cosine LFO. `phase` in [0, 1] maps to one full cycle.
74///
75/// # Math
76///   y = cos(phase × 2π)
77///
78/// # Example
79/// ```rust
80/// # use prime_osc::lfo_cosine;
81/// assert!((lfo_cosine(0.0) - 1.0).abs() < 1e-5);
82/// assert!((lfo_cosine(0.5) - (-1.0)).abs() < 1e-5);
83/// ```
84pub fn lfo_cosine(phase: f32) -> f32 {
85    (phase * TAU).cos()
86}
87
88/// Square wave at normalised phase.
89///
90/// # Math
91///   y = +1 if frac(phase) < width, else -1
92///
93/// # Arguments
94/// * `phase` - Normalised phase in [0, 1).
95/// * `width` - Duty cycle in (0, 1). 0.5 = 50% square. Clamped to (0.001, 0.999).
96///
97/// # Returns
98/// Value in {-1, +1}.
99///
100/// # Edge cases
101/// * `width=0.5` → symmetric square wave
102///
103/// # Example
104/// ```rust
105/// let y = prime_osc::lfo_square(0.1, 0.5); // 1.0 — first half of cycle
106/// assert!((y - 1.0).abs() < 1e-5);
107/// ```
108pub fn lfo_square(phase: f32, width: f32) -> f32 {
109    let p = phase - phase.floor();
110    let w = width.clamp(0.001, 0.999);
111    if p < w { 1.0 } else { -1.0 }
112}
113
114// ── Oscillator step ──────────────────────────────────────────────────────────
115
116/// Advance oscillator phase by one sample and return (sample, new_phase).
117///
118/// # Math
119///   new_phase = frac(phase + freq / sample_rate)
120///   y = shape(new_phase)
121///
122/// # Arguments
123/// * `phase` - Current phase in [0, 1).
124/// * `freq` - Frequency in Hz.
125/// * `sample_rate` - Sample rate in Hz (e.g. 44100.0).
126/// * `shape` - Shape function — lfo_sine, lfo_triangle, etc.
127///
128/// # Returns
129/// `(sample, new_phase)` — thread `new_phase` into the next call.
130///
131/// # Example
132/// ```rust
133/// let (y, next) = prime_osc::osc_step(0.0, 440.0, 44100.0, prime_osc::lfo_sine);
134/// ```
135pub fn osc_step(phase: f32, freq: f32, sample_rate: f32, shape: fn(f32) -> f32) -> (f32, f32) {
136    let sr = sample_rate.max(1.0);
137    let new_phase = (phase + freq / sr).fract();
138    (shape(new_phase), new_phase)
139}
140
141// ── ADSR envelope ────────────────────────────────────────────────────────────
142
143/// ADSR envelope parameters. All times in seconds.
144#[derive(Clone, Copy, Debug, PartialEq)]
145pub struct AdsrParams {
146    /// Attack time (0 → peak in seconds)
147    pub attack: f32,
148    /// Decay time (peak → sustain in seconds)
149    pub decay: f32,
150    /// Sustain level [0, 1]
151    pub sustain: f32,
152    /// Release time (sustain → 0 in seconds)
153    pub release: f32,
154}
155
156/// ADSR envelope stage.
157#[derive(Clone, Copy, Debug, PartialEq, Eq)]
158pub enum AdsrStage {
159    Attack,
160    Decay,
161    Sustain,
162    Release,
163    Done,
164}
165
166/// ADSR envelope state. Thread forward with each call to `adsr_step`.
167#[derive(Clone, Copy, Debug, PartialEq)]
168pub struct AdsrState {
169    /// Current envelope stage.
170    pub stage: AdsrStage,
171    /// Current envelope value in [0, 1].
172    pub value: f32,
173    /// Time elapsed in the current stage (seconds).
174    pub elapsed: f32,
175}
176
177impl AdsrState {
178    /// Initial state — envelope at rest (gate off).
179    pub const IDLE: Self = Self {
180        stage: AdsrStage::Done,
181        value: 0.0,
182        elapsed: 0.0,
183    };
184}
185
186/// Advance ADSR envelope by one time step — pure LOAD + COMPUTE.
187///
188/// # Math
189///   Attack:  value += dt / attack
190///   Decay:   value = lerp(1, sustain, elapsed / decay)
191///   Sustain: value = sustain
192///   Release: value = sustain_at_release × (1 − elapsed / release)
193///   Done:    value = 0
194///
195/// # Arguments
196/// * `state` - Current envelope state.
197/// * `params` - ADSR parameters.
198/// * `gate` - true = note on, false = note off (trigger release).
199/// * `dt` - Delta time in seconds.
200///
201/// # Returns
202/// `(value, new_state)` — thread `new_state` forward.
203///
204/// # Edge cases
205/// * Gate goes false mid-attack → transitions immediately to release.
206/// * Zero attack time → jumps directly to decay in one step.
207///
208/// # Example
209/// ```rust
210/// use prime_osc::{AdsrState, AdsrParams, adsr_step};
211/// let params = AdsrParams { attack: 0.1, decay: 0.1, sustain: 0.7, release: 0.2 };
212/// let (v, s1) = adsr_step(AdsrState::IDLE, &params, true, 0.016);
213/// assert!(v > 0.0);
214/// ```
215pub fn adsr_step(state: AdsrState, params: &AdsrParams, gate: bool, dt: f32) -> (f32, AdsrState) {
216    let attack = params.attack.max(1e-4);
217    let decay = params.decay.max(1e-4);
218    let release = params.release.max(1e-4);
219    let sustain = params.sustain.clamp(0.0, 1.0);
220
221    match (state.stage, gate) {
222        // Gate off → begin or continue release from current level
223        (AdsrStage::Done, false) => (0.0, AdsrState::IDLE),
224
225        (_, false) if state.stage != AdsrStage::Release && state.stage != AdsrStage::Done => {
226            // Transition into release — preserve current value as start point
227            let new_state = AdsrState {
228                stage: AdsrStage::Release,
229                value: state.value,
230                elapsed: 0.0,
231            };
232            (state.value, new_state)
233        }
234
235        (AdsrStage::Release, false) => {
236            let t = (state.elapsed / release).min(1.0);
237            let new_val = state.value * (1.0 - t);
238            let new_elapsed = state.elapsed + dt;
239            if new_elapsed >= release {
240                (0.0, AdsrState::IDLE)
241            } else {
242                (new_val, AdsrState { stage: AdsrStage::Release, value: state.value, elapsed: new_elapsed })
243            }
244        }
245
246        // Gate on
247        (AdsrStage::Done, true) | (AdsrStage::Release, true) => {
248            // (Re-)trigger: restart from attack
249            let new_elapsed = state.elapsed + dt;
250            let new_val = (new_elapsed / attack).min(1.0);
251            if new_elapsed >= attack {
252                (1.0, AdsrState { stage: AdsrStage::Decay, value: 1.0, elapsed: 0.0 })
253            } else {
254                (new_val, AdsrState { stage: AdsrStage::Attack, value: new_val, elapsed: new_elapsed })
255            }
256        }
257
258        (AdsrStage::Attack, true) => {
259            let new_elapsed = state.elapsed + dt;
260            let new_val = (new_elapsed / attack).min(1.0);
261            if new_elapsed >= attack {
262                (1.0, AdsrState { stage: AdsrStage::Decay, value: 1.0, elapsed: 0.0 })
263            } else {
264                (new_val, AdsrState { stage: AdsrStage::Attack, value: new_val, elapsed: new_elapsed })
265            }
266        }
267
268        (AdsrStage::Decay, true) => {
269            let new_elapsed = state.elapsed + dt;
270            let t = (new_elapsed / decay).min(1.0);
271            let new_val = 1.0 + (sustain - 1.0) * t;
272            if new_elapsed >= decay {
273                (sustain, AdsrState { stage: AdsrStage::Sustain, value: sustain, elapsed: 0.0 })
274            } else {
275                (new_val, AdsrState { stage: AdsrStage::Decay, value: new_val, elapsed: new_elapsed })
276            }
277        }
278
279        (AdsrStage::Sustain, true) => {
280            (sustain, AdsrState { stage: AdsrStage::Sustain, value: sustain, elapsed: state.elapsed + dt })
281        }
282
283        // Catch-all (shouldn't be reachable)
284        _ => (0.0, AdsrState::IDLE),
285    }
286}
287
288// ── Tests ────────────────────────────────────────────────────────────────────
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293    const EPS: f32 = 1e-5;
294
295    // lfo_sine
296    #[test]
297    fn sine_zero_phase() { assert!((lfo_sine(0.0)).abs() < EPS); }
298    #[test]
299    fn sine_quarter_phase() { assert!((lfo_sine(0.25) - 1.0).abs() < EPS); }
300    #[test]
301    fn sine_half_phase() { assert!((lfo_sine(0.5)).abs() < EPS); }
302    #[test]
303    fn sine_three_quarter_phase() { assert!((lfo_sine(0.75) + 1.0).abs() < EPS); }
304    #[test]
305    fn sine_range() {
306        (0..1000).map(|i| lfo_sine(i as f32 / 1000.0))
307            .for_each(|v| { assert!(v >= -1.0 - EPS && v <= 1.0 + EPS); });
308    }
309
310    // lfo_triangle
311    #[test]
312    fn triangle_zero() { assert!((lfo_triangle(0.0)).abs() < EPS); }
313    #[test]
314    fn triangle_quarter() { assert!((lfo_triangle(0.25) - 1.0).abs() < EPS); }
315    #[test]
316    fn triangle_half() { assert!((lfo_triangle(0.5)).abs() < EPS); }
317    #[test]
318    fn triangle_three_quarter() { assert!((lfo_triangle(0.75) + 1.0).abs() < EPS); }
319    #[test]
320    fn triangle_range() {
321        (0..1000).map(|i| lfo_triangle(i as f32 / 1000.0))
322            .for_each(|v| { assert!(v >= -1.0 - EPS && v <= 1.0 + EPS); });
323    }
324
325    // lfo_sawtooth
326    #[test]
327    fn sawtooth_zero() { assert!((lfo_sawtooth(0.0) + 1.0).abs() < EPS); }
328    #[test]
329    fn sawtooth_half() { assert!((lfo_sawtooth(0.5)).abs() < EPS); }
330    #[test]
331    fn sawtooth_range() {
332        (0..1000).map(|i| lfo_sawtooth(i as f32 / 1000.0))
333            .for_each(|v| { assert!(v >= -1.0 - EPS && v <= 1.0 + EPS); });
334    }
335
336    // lfo_cosine
337    #[test]
338    fn cosine_zero_phase() { assert!((lfo_cosine(0.0) - 1.0).abs() < EPS); }
339    #[test]
340    fn cosine_quarter_phase() { assert!((lfo_cosine(0.25)).abs() < EPS); }
341    #[test]
342    fn cosine_half_phase() { assert!((lfo_cosine(0.5) + 1.0).abs() < EPS); }
343    #[test]
344    fn cosine_three_quarter_phase() { assert!((lfo_cosine(0.75)).abs() < EPS); }
345    #[test]
346    fn cosine_range() {
347        (0..1000).map(|i| lfo_cosine(i as f32 / 1000.0))
348            .for_each(|v| { assert!(v >= -1.0 - EPS && v <= 1.0 + EPS); });
349    }
350
351    // lfo_square
352    #[test]
353    fn square_first_half() { assert!((lfo_square(0.1, 0.5) - 1.0).abs() < EPS); }
354    #[test]
355    fn square_second_half() { assert!((lfo_square(0.6, 0.5) + 1.0).abs() < EPS); }
356    #[test]
357    fn square_narrow_duty() {
358        // 10% duty: phase 0.05 → high, phase 0.15 → low
359        assert!((lfo_square(0.05, 0.1) - 1.0).abs() < EPS);
360        assert!((lfo_square(0.15, 0.1) + 1.0).abs() < EPS);
361    }
362
363    // osc_step
364    #[test]
365    fn osc_phase_advances() {
366        let (_, p1) = osc_step(0.0, 440.0, 44100.0, lfo_sine);
367        assert!((p1 - 440.0 / 44100.0).abs() < EPS);
368    }
369    #[test]
370    fn osc_phase_wraps() {
371        let (_, p1) = osc_step(0.99, 440.0, 44100.0, lfo_sine);
372        assert!(p1 < 1.0);
373    }
374    #[test]
375    fn osc_deterministic() {
376        let (y1, _) = osc_step(0.25, 440.0, 44100.0, lfo_sine);
377        let (y2, _) = osc_step(0.25, 440.0, 44100.0, lfo_sine);
378        assert_eq!(y1, y2);
379    }
380
381    // adsr_step
382    #[test]
383    fn adsr_starts_at_zero() {
384        let params = AdsrParams { attack: 0.1, decay: 0.1, sustain: 0.7, release: 0.2 };
385        let (v, _) = adsr_step(AdsrState::IDLE, &params, false, 0.016);
386        assert!((v).abs() < EPS);
387    }
388    #[test]
389    fn adsr_gate_on_rises() {
390        let params = AdsrParams { attack: 0.1, decay: 0.1, sustain: 0.7, release: 0.2 };
391        let (v, _) = adsr_step(AdsrState::IDLE, &params, true, 0.016);
392        assert!(v > 0.0);
393    }
394    #[test]
395    fn adsr_reaches_sustain() {
396        let params = AdsrParams { attack: 0.01, decay: 0.01, sustain: 0.7, release: 0.2 };
397        let final_state = (0..500).fold(
398            (0.0_f32, AdsrState::IDLE),
399            |(_, s), _| adsr_step(s, &params, true, 0.016),
400        );
401        assert!((final_state.0 - 0.7).abs() < 0.01);
402    }
403    #[test]
404    fn adsr_release_decays_to_zero() {
405        let params = AdsrParams { attack: 0.01, decay: 0.01, sustain: 0.7, release: 0.1 };
406        // Reach sustain first
407        let (_, sustain_state) = (0..200).fold(
408            (0.0_f32, AdsrState::IDLE),
409            |(_, s), _| adsr_step(s, &params, true, 0.016),
410        );
411        // Then release
412        let (v, _) = (0..500).fold(
413            (0.0_f32, sustain_state),
414            |(_, s), _| adsr_step(s, &params, false, 0.016),
415        );
416        assert!(v.abs() < 0.01);
417    }
418}