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 {
48            phase: 0.0,
49            tuning: None,
50        }
51    }
52
53    /// Load a tuning table into this oscillator.
54    /// After loading, param 3 (midi_note) drives the frequency.
55    pub fn set_tuning(&mut self, frequencies: [f32; 128]) {
56        self.tuning = Some(Box::new(frequencies));
57    }
58
59    pub fn clear_tuning(&mut self) {
60        self.tuning = None;
61    }
62}
63
64/// Polynomial Band-Limited Step (BLEP) correction.
65/// Reduces aliasing at waveform discontinuities.
66/// `t` = current phase, `dt` = phase increment per sample.
67#[inline(always)]
68fn blep(t: f32, dt: f32) -> f32 {
69    if t < dt {
70        let t = t / dt;
71        2.0 * t - t * t - 1.0
72    } else if t > 1.0 - dt {
73        let t = (t - 1.0) / dt;
74        t * t + 2.0 * t + 1.0
75    } else {
76        0.0
77    }
78}
79
80impl Default for Oscillator {
81    fn default() -> Self {
82        Self::new()
83    }
84}
85
86impl DspNode for Oscillator {
87    fn process(
88        &mut self,
89        _inputs: &[Option<&[f32; BUFFER_SIZE]>; MAX_INPUTS],
90        output: &mut [f32; BUFFER_SIZE],
91        params: &mut ParamBlock,
92        sample_rate: f32,
93    ) {
94        // Snapshot params once — they are stable or slowly ramping.
95        // Reading inside the loop would prevent auto-vectorization.
96        let amp = params.get(1).current.clamp(0.0, 1.0);
97        let wave = params.get(2).current as u32;
98
99        let freq = if let Some(ref tuning) = self.tuning {
100            let midi = params.get(3).current;
101            if midi >= 0.0 {
102                let note = (midi as usize).min(127);
103                tuning[note].max(0.01)
104            } else {
105                params.get(0).current.max(0.01)
106            }
107        } else {
108            params.get(0).current.max(0.01)
109        };
110
111        let phase_inc = freq / sample_rate;
112
113        match wave {
114            0 => {
115                // ── Sine: vectorizable loop ───────────────────────────────
116                // LLVM will auto-vectorize this into 4-wide SSE/AVX lanes
117                // because fast_sin_norm is a pure polynomial with no branches.
118                let mut phase = self.phase;
119                for out in output.iter_mut() {
120                    *out = fast_sin_norm(phase) * amp;
121                    phase = (phase + phase_inc).fract();
122                }
123                self.phase = phase;
124            }
125            1 => {
126                // ── Sawtooth with BLEP ────────────────────────────────────
127                let mut phase = self.phase;
128                for out in output.iter_mut() {
129                    let mut saw = 2.0 * phase - 1.0;
130                    saw -= blep(phase, phase_inc);
131                    *out = saw * amp;
132                    phase = (phase + phase_inc).fract();
133                }
134                self.phase = phase;
135            }
136            2 => {
137                // ── Square with BLEP ──────────────────────────────────────
138                let mut phase = self.phase;
139                for out in output.iter_mut() {
140                    let mut sq = if phase < 0.5 { 1.0f32 } else { -1.0f32 };
141                    sq += blep(phase, phase_inc);
142                    sq -= blep((phase + 0.5).fract(), phase_inc);
143                    *out = sq * amp;
144                    phase = (phase + phase_inc).fract();
145                }
146                self.phase = phase;
147            }
148            _ => {
149                // ── Triangle: vectorizable (no discontinuities) ───────────
150                let mut phase = self.phase;
151                for out in output.iter_mut() {
152                    let tri = if phase < 0.5 {
153                        4.0 * phase - 1.0
154                    } else {
155                        3.0 - 4.0 * phase
156                    };
157                    *out = tri * amp;
158                    phase = (phase + phase_inc).fract();
159                }
160                self.phase = phase;
161            }
162        }
163
164        // Advance params once per buffer (not per sample) when not ramping.
165        // This is correct because we snapshotted the values above.
166        // If params are ramping, tick them per-sample for accuracy.
167        if params.params[..params.count].iter().any(|p| p.step != 0.0) {
168            for _ in 0..BUFFER_SIZE {
169                params.tick_all();
170            }
171        }
172    }
173
174    fn capture_state(&self) -> StateBlob {
175        StateBlob::from_value(&OscState { phase: self.phase })
176    }
177
178    fn restore_state(&mut self, state: StateBlob) {
179        let s: OscState = state.to_value();
180        self.phase = s.phase;
181    }
182
183    fn type_name(&self) -> &'static str {
184        "Oscillator"
185    }
186}