Skip to main content

aether_nodes/
oscillator.rs

1//! Band-limited wavetable oscillator with tuning table support.
2//!
3//! Param layout:
4//!   0 = frequency (Hz) — overridden by tuning table when MIDI note is set
5//!   1 = amplitude (0..1)
6//!   2 = waveform  (0=sine, 1=saw, 2=square, 3=triangle)
7//!   3 = midi_note (0..127, -1 = use frequency param directly)
8//!
9//! SIMD strategy: the sine path uses a minimax polynomial approximation
10//! that the compiler can auto-vectorize across 4-wide f32 lanes.
11//! Saw/square/triangle use scalar BLEP (discontinuity correction requires
12//! per-sample phase tracking that defeats vectorization).
13
14use aether_core::{node::DspNode, param::ParamBlock, state::StateBlob, BUFFER_SIZE, MAX_INPUTS};
15use std::f32::consts::TAU;
16
17// ── Fast sine approximation (minimax polynomial, error < 1.5e-4) ─────────────
18// Accurate enough for audio; ~4× faster than libm sin() on x86.
19// Maps input in [0, 1) (normalized phase) to sin(phase * 2π).
20//
21// Algorithm: Bhaskara I approximation refined with a 5th-order minimax fit.
22// Reference: "Approximations for Digital Oscillators" — Välimäki & Pakarinen 2012.
23#[inline(always)]
24fn fast_sin_norm(phase: f32) -> f32 {
25    // Map [0,1) → [-π, π)
26    let x = (phase - 0.5) * TAU;
27    // 5th-order minimax polynomial for sin(x) on [-π, π]
28    // Coefficients from Remez algorithm fit
29    let x2 = x * x;
30    x * (0.999_999_4 + x2 * (-0.166_666_58 + x2 * (0.008_333_331 - x2 * 0.000_198_409)))
31}
32
33#[derive(Clone, Copy)]
34struct OscState {
35    phase: f32,
36}
37
38pub struct Oscillator {
39    phase: f32,
40    /// Optional tuning table — when set, MIDI note param drives frequency.
41    /// Stored as 128 f32 values (one per MIDI note).
42    tuning: Option<Box<[f32; 128]>>,
43}
44
45impl Oscillator {
46    pub fn new() -> Self {
47        Self { phase: 0.0, tuning: None }
48    }
49
50    /// Load a tuning table into this oscillator.
51    /// After loading, param 3 (midi_note) drives the frequency.
52    pub fn set_tuning(&mut self, frequencies: [f32; 128]) {
53        self.tuning = Some(Box::new(frequencies));
54    }
55
56    pub fn clear_tuning(&mut self) {
57        self.tuning = None;
58    }
59}
60
61/// Polynomial Band-Limited Step (BLEP) correction.
62/// Reduces aliasing at waveform discontinuities.
63/// `t` = current phase, `dt` = phase increment per sample.
64#[inline(always)]
65fn blep(t: f32, dt: f32) -> f32 {
66    if t < dt {
67        let t = t / dt;
68        2.0 * t - t * t - 1.0
69    } else if t > 1.0 - dt {
70        let t = (t - 1.0) / dt;
71        t * t + 2.0 * t + 1.0
72    } else {
73        0.0
74    }
75}
76
77impl Default for Oscillator {
78    fn default() -> Self {
79        Self::new()
80    }
81}
82
83impl DspNode for Oscillator {
84    fn process(
85        &mut self,
86        _inputs: &[Option<&[f32; BUFFER_SIZE]>; MAX_INPUTS],
87        output: &mut [f32; BUFFER_SIZE],
88        params: &mut ParamBlock,
89        sample_rate: f32,
90    ) {
91        // Snapshot params once — they are stable or slowly ramping.
92        // Reading inside the loop would prevent auto-vectorization.
93        let amp  = params.get(1).current.clamp(0.0, 1.0);
94        let wave = params.get(2).current as u32;
95
96        let freq = if let Some(ref tuning) = self.tuning {
97            let midi = params.get(3).current;
98            if midi >= 0.0 {
99                let note = (midi as usize).min(127);
100                tuning[note].max(0.01)
101            } else {
102                params.get(0).current.max(0.01)
103            }
104        } else {
105            params.get(0).current.max(0.01)
106        };
107
108        let phase_inc = freq / sample_rate;
109
110        match wave {
111            0 => {
112                // ── Sine: vectorizable loop ───────────────────────────────
113                // LLVM will auto-vectorize this into 4-wide SSE/AVX lanes
114                // because fast_sin_norm is a pure polynomial with no branches.
115                let mut phase = self.phase;
116                for out in output.iter_mut() {
117                    *out = fast_sin_norm(phase) * amp;
118                    phase = (phase + phase_inc).fract();
119                }
120                self.phase = phase;
121            }
122            1 => {
123                // ── Sawtooth with BLEP ────────────────────────────────────
124                let mut phase = self.phase;
125                for out in output.iter_mut() {
126                    let mut saw = 2.0 * phase - 1.0;
127                    saw -= blep(phase, phase_inc);
128                    *out = saw * amp;
129                    phase = (phase + phase_inc).fract();
130                }
131                self.phase = phase;
132            }
133            2 => {
134                // ── Square with BLEP ──────────────────────────────────────
135                let mut phase = self.phase;
136                for out in output.iter_mut() {
137                    let mut sq = if phase < 0.5 { 1.0f32 } else { -1.0f32 };
138                    sq += blep(phase, phase_inc);
139                    sq -= blep((phase + 0.5).fract(), phase_inc);
140                    *out = sq * amp;
141                    phase = (phase + phase_inc).fract();
142                }
143                self.phase = phase;
144            }
145            _ => {
146                // ── Triangle: vectorizable (no discontinuities) ───────────
147                let mut phase = self.phase;
148                for out in output.iter_mut() {
149                    let tri = if phase < 0.5 {
150                        4.0 * phase - 1.0
151                    } else {
152                        3.0 - 4.0 * phase
153                    };
154                    *out = tri * amp;
155                    phase = (phase + phase_inc).fract();
156                }
157                self.phase = phase;
158            }
159        }
160
161        // Advance params once per buffer (not per sample) when not ramping.
162        // This is correct because we snapshotted the values above.
163        // If params are ramping, tick them per-sample for accuracy.
164        if params.params[..params.count].iter().any(|p| p.step != 0.0) {
165            for _ in 0..BUFFER_SIZE {
166                params.tick_all();
167            }
168        }
169    }
170
171    fn capture_state(&self) -> StateBlob {
172        StateBlob::from_value(&OscState { phase: self.phase })
173    }
174
175    fn restore_state(&mut self, state: StateBlob) {
176        let s: OscState = state.to_value();
177        self.phase = s.phase;
178    }
179
180    fn type_name(&self) -> &'static str {
181        "Oscillator"
182    }
183}