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    use wavecraft_dsp::Bypassed;
213
214    fn test_params(enabled: bool) -> OscillatorParams {
215        OscillatorParams {
216            enabled,
217            waveform: 0.0,
218            frequency: 440.0,
219            level: 0.5,
220        }
221    }
222
223    fn test_params_with_waveform(enabled: bool, waveform: f32) -> OscillatorParams {
224        OscillatorParams {
225            enabled,
226            waveform,
227            frequency: 440.0,
228            level: 0.5,
229        }
230    }
231
232    #[test]
233    fn waveform_from_index_maps_correctly() {
234        assert_eq!(Waveform::from_index(0.0), Waveform::Sine);
235        assert_eq!(Waveform::from_index(1.0), Waveform::Square);
236        assert_eq!(Waveform::from_index(2.0), Waveform::Saw);
237        assert_eq!(Waveform::from_index(3.0), Waveform::Triangle);
238    }
239
240    #[test]
241    fn waveform_from_index_out_of_range_defaults_to_sine() {
242        assert_eq!(Waveform::from_index(-1.0), Waveform::Sine);
243        assert_eq!(Waveform::from_index(4.0), Waveform::Sine);
244        assert_eq!(Waveform::from_index(100.0), Waveform::Sine);
245    }
246
247    #[test]
248    fn waveform_from_index_rounds_floats() {
249        assert_eq!(Waveform::from_index(0.4), Waveform::Sine);
250        assert_eq!(Waveform::from_index(0.6), Waveform::Square);
251        assert_eq!(Waveform::from_index(1.5), Waveform::Saw);
252        assert_eq!(Waveform::from_index(2.7), Waveform::Triangle);
253    }
254
255    #[test]
256    fn sine_wave_zero_crossing_and_peak() {
257        assert!((generate_waveform_sample(Waveform::Sine, 0.0)).abs() < 1e-5);
258        assert!((generate_waveform_sample(Waveform::Sine, 0.25) - 1.0).abs() < 1e-5);
259        assert!((generate_waveform_sample(Waveform::Sine, 0.5)).abs() < 1e-5);
260        assert!((generate_waveform_sample(Waveform::Sine, 0.75) + 1.0).abs() < 1e-5);
261    }
262
263    #[test]
264    fn square_wave_values() {
265        assert_eq!(generate_waveform_sample(Waveform::Square, 0.0), 1.0);
266        assert_eq!(generate_waveform_sample(Waveform::Square, 0.25), 1.0);
267        assert_eq!(generate_waveform_sample(Waveform::Square, 0.5), -1.0);
268        assert_eq!(generate_waveform_sample(Waveform::Square, 0.75), -1.0);
269    }
270
271    #[test]
272    fn saw_wave_values() {
273        assert!((generate_waveform_sample(Waveform::Saw, 0.0) + 1.0).abs() < 1e-5);
274        assert!((generate_waveform_sample(Waveform::Saw, 0.5)).abs() < 1e-5);
275        assert!((generate_waveform_sample(Waveform::Saw, 1.0) - 1.0).abs() < 1e-5);
276    }
277
278    #[test]
279    fn triangle_wave_values() {
280        assert!((generate_waveform_sample(Waveform::Triangle, 0.0) + 1.0).abs() < 1e-5);
281        assert!((generate_waveform_sample(Waveform::Triangle, 0.25)).abs() < 1e-5);
282        assert!((generate_waveform_sample(Waveform::Triangle, 0.5) - 1.0).abs() < 1e-5);
283        assert!((generate_waveform_sample(Waveform::Triangle, 0.75)).abs() < 1e-5);
284    }
285
286    #[test]
287    fn oscillator_preserves_passthrough_when_disabled() {
288        let mut osc = Oscillator::default();
289        osc.set_sample_rate(48_000.0);
290
291        let mut left = [0.25_f32; 64];
292        let mut right = [-0.5_f32; 64];
293        let left_in = left;
294        let right_in = right;
295        let mut buffer = [&mut left[..], &mut right[..]];
296
297        osc.process(&mut buffer, &Transport::default(), &test_params(false));
298
299        for (actual, expected) in left.iter().zip(left_in.iter()) {
300            assert!((actual - expected).abs() <= f32::EPSILON);
301        }
302
303        for (actual, expected) in right.iter().zip(right_in.iter()) {
304            assert!((actual - expected).abs() <= f32::EPSILON);
305        }
306    }
307
308    #[test]
309    fn oscillator_generates_signal_when_enabled_on_silent_input() {
310        let mut osc = Oscillator::default();
311        osc.set_sample_rate(48_000.0);
312
313        let mut left = [0.0_f32; 128];
314        let mut right = [0.0_f32; 128];
315        let mut buffer = [&mut left[..], &mut right[..]];
316
317        osc.process(&mut buffer, &Transport::default(), &test_params(true));
318
319        let peak_left = left
320            .iter()
321            .fold(0.0_f32, |acc, sample| acc.max(sample.abs()));
322        let peak_right = right
323            .iter()
324            .fold(0.0_f32, |acc, sample| acc.max(sample.abs()));
325
326        assert!(
327            peak_left > 0.01,
328            "expected audible oscillator output on left"
329        );
330        assert!(
331            peak_right > 0.01,
332            "expected audible oscillator output on right"
333        );
334    }
335
336    #[test]
337    fn oscillator_enabled_adds_signal_without_removing_input() {
338        let mut osc_mixed = Oscillator::default();
339        osc_mixed.set_sample_rate(48_000.0);
340
341        let mut left_mixed = [0.2_f32; 128];
342        let mut right_mixed = [-0.15_f32; 128];
343        let left_input = left_mixed;
344        let right_input = right_mixed;
345        let mut mixed_buffer = [&mut left_mixed[..], &mut right_mixed[..]];
346
347        osc_mixed.process(&mut mixed_buffer, &Transport::default(), &test_params(true));
348
349        let mut osc_only = Oscillator::default();
350        osc_only.set_sample_rate(48_000.0);
351
352        let mut left_osc_only = [0.0_f32; 128];
353        let mut right_osc_only = [0.0_f32; 128];
354        let mut osc_only_buffer = [&mut left_osc_only[..], &mut right_osc_only[..]];
355
356        osc_only.process(
357            &mut osc_only_buffer,
358            &Transport::default(),
359            &test_params(true),
360        );
361
362        for i in 0..left_mixed.len() {
363            let additive_component_left = left_mixed[i] - left_input[i];
364            let additive_component_right = right_mixed[i] - right_input[i];
365
366            assert!((additive_component_left - left_osc_only[i]).abs() < 1e-6);
367            assert!((additive_component_right - right_osc_only[i]).abs() < 1e-6);
368        }
369    }
370
371    #[test]
372    fn oscillator_bypass_wrapper_mutes_generator_output() {
373        let mut wrapped = Bypassed::new(Oscillator::default());
374        wrapped.set_sample_rate(48_000.0);
375
376        type WrappedParams = <Bypassed<Oscillator> as Processor>::Params;
377        let bypassed_params = WrappedParams {
378            inner: test_params(true),
379            bypassed: true,
380        };
381
382        // Allow bypass transition to settle to a stable bypassed state.
383        for _ in 0..4 {
384            let mut left = [0.0_f32; 128];
385            let mut right = [0.0_f32; 128];
386            let mut buffer = [&mut left[..], &mut right[..]];
387
388            wrapped.process(&mut buffer, &Transport::default(), &bypassed_params);
389        }
390
391        let mut left = [0.0_f32; 128];
392        let mut right = [0.0_f32; 128];
393        let mut buffer = [&mut left[..], &mut right[..]];
394        wrapped.process(&mut buffer, &Transport::default(), &bypassed_params);
395
396        let peak_left = left
397            .iter()
398            .fold(0.0_f32, |acc, sample| acc.max(sample.abs()));
399        let peak_right = right
400            .iter()
401            .fold(0.0_f32, |acc, sample| acc.max(sample.abs()));
402
403        assert!(
404            peak_left <= 1e-6,
405            "expected bypassed oscillator to contribute no left-channel signal"
406        );
407        assert!(
408            peak_right <= 1e-6,
409            "expected bypassed oscillator to contribute no right-channel signal"
410        );
411    }
412
413    #[test]
414    fn all_waveforms_produce_signal_when_enabled() {
415        for waveform_index in 0..4 {
416            let mut osc = Oscillator::default();
417            osc.set_sample_rate(48_000.0);
418
419            let mut left = [0.0_f32; 128];
420            let mut right = [0.0_f32; 128];
421            let mut buffer = [&mut left[..], &mut right[..]];
422
423            osc.process(
424                &mut buffer,
425                &Transport::default(),
426                &test_params_with_waveform(true, waveform_index as f32),
427            );
428
429            let peak = left
430                .iter()
431                .fold(0.0_f32, |acc, sample| acc.max(sample.abs()));
432            assert!(
433                peak > 0.01,
434                "waveform index {waveform_index} should produce signal"
435            );
436        }
437    }
438
439    #[test]
440    fn apply_plain_values_updates_all_fields() {
441        let mut params = OscillatorParams::default();
442        params.apply_plain_values(&[1.0, 2.0, 1760.0, 0.9]);
443
444        assert!(params.enabled);
445        assert!((params.waveform - 2.0).abs() < f32::EPSILON);
446        assert!((params.frequency - 1760.0).abs() < f32::EPSILON);
447        assert!((params.level - 0.9).abs() < f32::EPSILON);
448    }
449
450    #[test]
451    fn frequency_param_uses_full_audible_range() {
452        let specs = OscillatorParams::param_specs();
453        let frequency = specs
454            .iter()
455            .find(|spec| spec.id_suffix == "frequency")
456            .expect("frequency spec should exist");
457
458        match frequency.range {
459            ParamRange::Skewed { min, max, .. } => {
460                assert!((min - 20.0).abs() < f64::EPSILON);
461                assert!((max - 20_000.0).abs() < f64::EPSILON);
462            }
463            _ => panic!("frequency should use a skewed range"),
464        }
465    }
466}