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