Skip to main content

wavecraft_processors/
oscillator.rs

1//! Oscillator — a simple sine-wave generator.
2
3use wavecraft_dsp::{ParamRange, ParamSpec, Processor, ProcessorParams, Transport};
4
5/// Available oscillator waveform shapes.
6#[derive(Debug, Clone, Copy, Default, PartialEq)]
7pub enum Waveform {
8    #[default]
9    Sine,
10    Square,
11    Saw,
12    Triangle,
13}
14
15impl Waveform {
16    /// Variant labels in declaration order (must match enum discriminant order).
17    pub const VARIANTS: &'static [&'static str] = &["Sine", "Square", "Saw", "Triangle"];
18
19    /// Convert a 0-based index to a `Waveform`.
20    /// Out-of-range values default to `Sine`.
21    pub fn from_index(index: f32) -> Self {
22        match index.round() as u32 {
23            0 => Self::Sine,
24            1 => Self::Square,
25            2 => Self::Saw,
26            3 => Self::Triangle,
27            _ => Self::Sine,
28        }
29    }
30}
31
32/// Generate a single sample for the given waveform at the given phase (0.0–1.0).
33pub fn generate_waveform_sample(waveform: Waveform, phase: f32) -> f32 {
34    match waveform {
35        Waveform::Sine => (phase * std::f32::consts::TAU).sin(),
36        Waveform::Square => {
37            if phase < 0.5 {
38                1.0
39            } else {
40                -1.0
41            }
42        }
43        Waveform::Saw => 2.0 * phase - 1.0,
44        Waveform::Triangle => {
45            if phase < 0.5 {
46                4.0 * phase - 1.0
47            } else {
48                -4.0 * phase + 3.0
49            }
50        }
51    }
52}
53
54#[inline]
55fn advance_phase(phase: &mut f32, phase_delta: f32) {
56    // Advance phase, wrapping at 1.0 to avoid floating-point drift.
57    *phase += phase_delta;
58    if *phase >= 1.0 {
59        *phase -= 1.0;
60    }
61}
62
63/// Oscillator parameters.
64#[derive(Clone)]
65pub struct OscillatorParams {
66    /// Enable/disable oscillator output.
67    pub enabled: bool,
68
69    /// Waveform index mapped through [`Waveform::from_index`].
70    pub waveform: f32,
71
72    /// Frequency in Hz. `factor = 2.5` gives a logarithmic feel in the UI.
73    pub frequency: f32,
74
75    /// Output level as normalized amplitude (0.0 – 1.0).
76    pub level: f32,
77}
78
79impl Default for OscillatorParams {
80    fn default() -> Self {
81        Self {
82            enabled: false,
83            waveform: 0.0,
84            frequency: 440.0,
85            level: 0.5,
86        }
87    }
88}
89
90impl ProcessorParams for OscillatorParams {
91    fn param_specs() -> &'static [ParamSpec] {
92        static SPECS: [ParamSpec; 4] = [
93            ParamSpec {
94                name: "Enabled",
95                id_suffix: "enabled",
96                range: ParamRange::Stepped { min: 0, max: 1 },
97                default: 0.0,
98                unit: "",
99                group: None,
100            },
101            ParamSpec {
102                name: "Waveform",
103                id_suffix: "waveform",
104                range: ParamRange::Enum {
105                    variants: Waveform::VARIANTS,
106                },
107                default: 0.0,
108                unit: "",
109                group: None,
110            },
111            ParamSpec {
112                name: "Frequency",
113                id_suffix: "frequency",
114                range: ParamRange::Skewed {
115                    min: 20.0,
116                    max: 20_000.0,
117                    factor: 2.5,
118                },
119                default: 440.0,
120                unit: "Hz",
121                group: None,
122            },
123            ParamSpec {
124                name: "Level",
125                id_suffix: "level",
126                range: ParamRange::Linear { min: 0.0, max: 1.0 },
127                default: 0.5,
128                unit: "%",
129                group: None,
130            },
131        ];
132
133        &SPECS
134    }
135
136    fn from_param_defaults() -> Self {
137        Self::default()
138    }
139
140    fn apply_plain_values(&mut self, values: &[f32]) {
141        if let Some(enabled) = values.first() {
142            self.enabled = *enabled >= 0.5;
143        }
144        if let Some(waveform) = values.get(1) {
145            self.waveform = *waveform;
146        }
147        if let Some(frequency) = values.get(2) {
148            self.frequency = *frequency;
149        }
150        if let Some(level) = values.get(3) {
151            self.level = *level;
152        }
153    }
154}
155
156/// A minimal oscillator that produces multiple waveforms.
157#[derive(Default)]
158pub struct Oscillator {
159    /// Current sample rate provided by the host.
160    sample_rate: f32,
161    /// Phase position within one cycle (0.0 – 1.0).
162    phase: f32,
163}
164
165impl Processor for Oscillator {
166    type Params = OscillatorParams;
167
168    fn set_sample_rate(&mut self, sample_rate: f32) {
169        self.sample_rate = sample_rate;
170    }
171
172    fn process(
173        &mut self,
174        buffer: &mut [&mut [f32]],
175        _transport: &Transport,
176        params: &Self::Params,
177    ) {
178        if !params.enabled {
179            return;
180        }
181
182        // Guard: if set_sample_rate() hasn't been called yet, leave buffer unchanged.
183        if self.sample_rate == 0.0 {
184            return;
185        }
186
187        let waveform = Waveform::from_index(params.waveform);
188
189        // How far the phase advances per sample.
190        let phase_delta = params.frequency / self.sample_rate;
191
192        // Save the starting phase so every channel receives the same waveform.
193        let start_phase = self.phase;
194
195        for channel in buffer.iter_mut() {
196            self.phase = start_phase;
197            for sample in channel.iter_mut() {
198                *sample += generate_waveform_sample(waveform, self.phase) * params.level;
199                advance_phase(&mut self.phase, phase_delta);
200            }
201        }
202    }
203
204    fn reset(&mut self) {
205        self.phase = 0.0;
206    }
207}
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212
213    fn test_params(enabled: bool) -> OscillatorParams {
214        OscillatorParams {
215            enabled,
216            waveform: 0.0,
217            frequency: 440.0,
218            level: 0.5,
219        }
220    }
221
222    fn test_params_with_waveform(enabled: bool, waveform: f32) -> OscillatorParams {
223        OscillatorParams {
224            enabled,
225            waveform,
226            frequency: 440.0,
227            level: 0.5,
228        }
229    }
230
231    #[test]
232    fn waveform_from_index_maps_correctly() {
233        assert_eq!(Waveform::from_index(0.0), Waveform::Sine);
234        assert_eq!(Waveform::from_index(1.0), Waveform::Square);
235        assert_eq!(Waveform::from_index(2.0), Waveform::Saw);
236        assert_eq!(Waveform::from_index(3.0), Waveform::Triangle);
237    }
238
239    #[test]
240    fn waveform_from_index_out_of_range_defaults_to_sine() {
241        assert_eq!(Waveform::from_index(-1.0), Waveform::Sine);
242        assert_eq!(Waveform::from_index(4.0), Waveform::Sine);
243        assert_eq!(Waveform::from_index(100.0), Waveform::Sine);
244    }
245
246    #[test]
247    fn waveform_from_index_rounds_floats() {
248        assert_eq!(Waveform::from_index(0.4), Waveform::Sine);
249        assert_eq!(Waveform::from_index(0.6), Waveform::Square);
250        assert_eq!(Waveform::from_index(1.5), Waveform::Saw);
251        assert_eq!(Waveform::from_index(2.7), Waveform::Triangle);
252    }
253
254    #[test]
255    fn sine_wave_zero_crossing_and_peak() {
256        assert!((generate_waveform_sample(Waveform::Sine, 0.0)).abs() < 1e-5);
257        assert!((generate_waveform_sample(Waveform::Sine, 0.25) - 1.0).abs() < 1e-5);
258        assert!((generate_waveform_sample(Waveform::Sine, 0.5)).abs() < 1e-5);
259        assert!((generate_waveform_sample(Waveform::Sine, 0.75) + 1.0).abs() < 1e-5);
260    }
261
262    #[test]
263    fn square_wave_values() {
264        assert_eq!(generate_waveform_sample(Waveform::Square, 0.0), 1.0);
265        assert_eq!(generate_waveform_sample(Waveform::Square, 0.25), 1.0);
266        assert_eq!(generate_waveform_sample(Waveform::Square, 0.5), -1.0);
267        assert_eq!(generate_waveform_sample(Waveform::Square, 0.75), -1.0);
268    }
269
270    #[test]
271    fn saw_wave_values() {
272        assert!((generate_waveform_sample(Waveform::Saw, 0.0) + 1.0).abs() < 1e-5);
273        assert!((generate_waveform_sample(Waveform::Saw, 0.5)).abs() < 1e-5);
274        assert!((generate_waveform_sample(Waveform::Saw, 1.0) - 1.0).abs() < 1e-5);
275    }
276
277    #[test]
278    fn triangle_wave_values() {
279        assert!((generate_waveform_sample(Waveform::Triangle, 0.0) + 1.0).abs() < 1e-5);
280        assert!((generate_waveform_sample(Waveform::Triangle, 0.25)).abs() < 1e-5);
281        assert!((generate_waveform_sample(Waveform::Triangle, 0.5) - 1.0).abs() < 1e-5);
282        assert!((generate_waveform_sample(Waveform::Triangle, 0.75)).abs() < 1e-5);
283    }
284
285    #[test]
286    fn oscillator_preserves_passthrough_when_disabled() {
287        let mut osc = Oscillator::default();
288        osc.set_sample_rate(48_000.0);
289
290        let mut left = [0.25_f32; 64];
291        let mut right = [-0.5_f32; 64];
292        let left_in = left;
293        let right_in = right;
294        let mut buffer = [&mut left[..], &mut right[..]];
295
296        osc.process(&mut buffer, &Transport::default(), &test_params(false));
297
298        for (actual, expected) in left.iter().zip(left_in.iter()) {
299            assert!((actual - expected).abs() <= f32::EPSILON);
300        }
301
302        for (actual, expected) in right.iter().zip(right_in.iter()) {
303            assert!((actual - expected).abs() <= f32::EPSILON);
304        }
305    }
306
307    #[test]
308    fn oscillator_generates_signal_when_enabled_on_silent_input() {
309        let mut osc = Oscillator::default();
310        osc.set_sample_rate(48_000.0);
311
312        let mut left = [0.0_f32; 128];
313        let mut right = [0.0_f32; 128];
314        let mut buffer = [&mut left[..], &mut right[..]];
315
316        osc.process(&mut buffer, &Transport::default(), &test_params(true));
317
318        let peak_left = left
319            .iter()
320            .fold(0.0_f32, |acc, sample| acc.max(sample.abs()));
321        let peak_right = right
322            .iter()
323            .fold(0.0_f32, |acc, sample| acc.max(sample.abs()));
324
325        assert!(
326            peak_left > 0.01,
327            "expected audible oscillator output on left"
328        );
329        assert!(
330            peak_right > 0.01,
331            "expected audible oscillator output on right"
332        );
333    }
334
335    #[test]
336    fn oscillator_enabled_adds_signal_without_removing_input() {
337        let mut osc_mixed = Oscillator::default();
338        osc_mixed.set_sample_rate(48_000.0);
339
340        let mut left_mixed = [0.2_f32; 128];
341        let mut right_mixed = [-0.15_f32; 128];
342        let left_input = left_mixed;
343        let right_input = right_mixed;
344        let mut mixed_buffer = [&mut left_mixed[..], &mut right_mixed[..]];
345
346        osc_mixed.process(&mut mixed_buffer, &Transport::default(), &test_params(true));
347
348        let mut osc_only = Oscillator::default();
349        osc_only.set_sample_rate(48_000.0);
350
351        let mut left_osc_only = [0.0_f32; 128];
352        let mut right_osc_only = [0.0_f32; 128];
353        let mut osc_only_buffer = [&mut left_osc_only[..], &mut right_osc_only[..]];
354
355        osc_only.process(
356            &mut osc_only_buffer,
357            &Transport::default(),
358            &test_params(true),
359        );
360
361        for i in 0..left_mixed.len() {
362            let additive_component_left = left_mixed[i] - left_input[i];
363            let additive_component_right = right_mixed[i] - right_input[i];
364
365            assert!((additive_component_left - left_osc_only[i]).abs() < 1e-6);
366            assert!((additive_component_right - right_osc_only[i]).abs() < 1e-6);
367        }
368    }
369
370    #[test]
371    fn all_waveforms_produce_signal_when_enabled() {
372        for waveform_index in 0..4 {
373            let mut osc = Oscillator::default();
374            osc.set_sample_rate(48_000.0);
375
376            let mut left = [0.0_f32; 128];
377            let mut right = [0.0_f32; 128];
378            let mut buffer = [&mut left[..], &mut right[..]];
379
380            osc.process(
381                &mut buffer,
382                &Transport::default(),
383                &test_params_with_waveform(true, waveform_index as f32),
384            );
385
386            let peak = left
387                .iter()
388                .fold(0.0_f32, |acc, sample| acc.max(sample.abs()));
389            assert!(
390                peak > 0.01,
391                "waveform index {waveform_index} should produce signal"
392            );
393        }
394    }
395
396    #[test]
397    fn apply_plain_values_updates_all_fields() {
398        let mut params = OscillatorParams::default();
399        params.apply_plain_values(&[1.0, 2.0, 1760.0, 0.9]);
400
401        assert!(params.enabled);
402        assert!((params.waveform - 2.0).abs() < f32::EPSILON);
403        assert!((params.frequency - 1760.0).abs() < f32::EPSILON);
404        assert!((params.level - 0.9).abs() < f32::EPSILON);
405    }
406
407    #[test]
408    fn frequency_param_uses_full_audible_range() {
409        let specs = OscillatorParams::param_specs();
410        let frequency = specs
411            .iter()
412            .find(|spec| spec.id_suffix == "frequency")
413            .expect("frequency spec should exist");
414
415        match frequency.range {
416            ParamRange::Skewed { min, max, .. } => {
417                assert!((min - 20.0).abs() < f64::EPSILON);
418                assert!((max - 20_000.0).abs() < f64::EPSILON);
419            }
420            _ => panic!("frequency should use a skewed range"),
421        }
422    }
423}