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
9use aether_core::{node::DspNode, param::ParamBlock, state::StateBlob, BUFFER_SIZE, MAX_INPUTS};
10use std::f32::consts::TAU;
11
12#[derive(Clone, Copy)]
13struct OscState {
14    phase: f32,
15}
16
17pub struct Oscillator {
18    phase: f32,
19    /// Optional tuning table — when set, MIDI note param drives frequency.
20    /// Stored as 128 f32 values (one per MIDI note).
21    tuning: Option<Box<[f32; 128]>>,
22}
23
24impl Oscillator {
25    pub fn new() -> Self {
26        Self { phase: 0.0, tuning: None }
27    }
28
29    /// Load a tuning table into this oscillator.
30    /// After loading, param 3 (midi_note) drives the frequency.
31    pub fn set_tuning(&mut self, frequencies: [f32; 128]) {
32        self.tuning = Some(Box::new(frequencies));
33    }
34
35    pub fn clear_tuning(&mut self) {
36        self.tuning = None;
37    }
38
39    #[inline(always)]
40    fn generate_sample(&mut self, freq: f32, amp: f32, waveform: f32, sr: f32) -> f32 {
41        let phase_inc = freq / sr;
42
43        // BLEP anti-aliasing for discontinuous waveforms
44        let sample = match waveform as u32 {
45            0 => (self.phase * TAU).sin(),
46            1 => {
47                // Band-limited sawtooth using BLEP
48                let mut saw = 2.0 * self.phase - 1.0;
49                saw -= blep(self.phase, phase_inc);
50                saw
51            }
52            2 => {
53                // Band-limited square using BLEP
54                let mut sq = if self.phase < 0.5 { 1.0f32 } else { -1.0f32 };
55                sq += blep(self.phase, phase_inc);
56                sq -= blep((self.phase + 0.5).fract(), phase_inc);
57                sq
58            }
59            _ => {
60                // Triangle (integrated square — already band-limited)
61                if self.phase < 0.5 {
62                    4.0 * self.phase - 1.0
63                } else {
64                    3.0 - 4.0 * self.phase
65                }
66            }
67        };
68        self.phase = (self.phase + phase_inc).fract();
69        sample * amp
70    }
71}
72
73/// Polynomial Band-Limited Step (BLEP) correction.
74/// Reduces aliasing at waveform discontinuities.
75/// `t` = current phase, `dt` = phase increment per sample.
76#[inline(always)]
77fn blep(t: f32, dt: f32) -> f32 {
78    if t < dt {
79        let t = t / dt;
80        2.0 * t - t * t - 1.0
81    } else if t > 1.0 - dt {
82        let t = (t - 1.0) / dt;
83        t * t + 2.0 * t + 1.0
84    } else {
85        0.0
86    }
87}
88
89impl Default for Oscillator {
90    fn default() -> Self {
91        Self::new()
92    }
93}
94
95impl DspNode for Oscillator {
96    fn process(
97        &mut self,
98        _inputs: &[Option<&[f32; BUFFER_SIZE]>; MAX_INPUTS],
99        output: &mut [f32; BUFFER_SIZE],
100        params: &mut ParamBlock,
101        sample_rate: f32,
102    ) {
103        for sample in output.iter_mut() {
104            let amp  = params.get(1).current.clamp(0.0, 1.0);
105            let wave = params.get(2).current;
106
107            // Frequency: use tuning table if available and midi_note param is set
108            let freq = if let Some(ref tuning) = self.tuning {
109                let midi = params.get(3).current;
110                if midi >= 0.0 {
111                    let note = (midi as usize).min(127);
112                    tuning[note].max(0.01)
113                } else {
114                    params.get(0).current.max(0.01)
115                }
116            } else {
117                params.get(0).current.max(0.01)
118            };
119
120            *sample = self.generate_sample(freq, amp, wave, sample_rate);
121            params.tick_all();
122        }
123    }
124
125    fn capture_state(&self) -> StateBlob {
126        StateBlob::from_value(&OscState { phase: self.phase })
127    }
128
129    fn restore_state(&mut self, state: StateBlob) {
130        let s: OscState = state.to_value();
131        self.phase = s.phase;
132    }
133
134    fn type_name(&self) -> &'static str {
135        "Oscillator"
136    }
137}