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: 5000.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
132/// A minimal oscillator that produces multiple waveforms.
133#[derive(Default)]
134pub struct Oscillator {
135    /// Current sample rate provided by the host.
136    sample_rate: f32,
137    /// Phase position within one cycle (0.0 – 1.0).
138    phase: f32,
139}
140
141impl Processor for Oscillator {
142    type Params = OscillatorParams;
143
144    fn set_sample_rate(&mut self, sample_rate: f32) {
145        self.sample_rate = sample_rate;
146    }
147
148    fn process(
149        &mut self,
150        buffer: &mut [&mut [f32]],
151        _transport: &Transport,
152        params: &Self::Params,
153    ) {
154        if !params.enabled {
155            for channel in buffer.iter_mut() {
156                channel.fill(0.0);
157            }
158            return;
159        }
160
161        // Guard: if set_sample_rate() hasn't been called yet, leave buffer unchanged.
162        if self.sample_rate == 0.0 {
163            return;
164        }
165
166        let waveform = Waveform::from_index(params.waveform);
167
168        // How far the phase advances per sample.
169        let phase_delta = params.frequency / self.sample_rate;
170
171        // Save the starting phase so every channel receives the same waveform.
172        let start_phase = self.phase;
173
174        for channel in buffer.iter_mut() {
175            self.phase = start_phase;
176            for sample in channel.iter_mut() {
177                *sample = generate_waveform_sample(waveform, self.phase) * params.level;
178
179                // Advance phase, wrapping at 1.0 to avoid floating-point drift.
180                self.phase += phase_delta;
181                if self.phase >= 1.0 {
182                    self.phase -= 1.0;
183                }
184            }
185        }
186    }
187
188    fn reset(&mut self) {
189        self.phase = 0.0;
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196
197    fn test_params(enabled: bool) -> OscillatorParams {
198        OscillatorParams {
199            enabled,
200            waveform: 0.0,
201            frequency: 440.0,
202            level: 0.5,
203        }
204    }
205
206    fn test_params_with_waveform(enabled: bool, waveform: f32) -> OscillatorParams {
207        OscillatorParams {
208            enabled,
209            waveform,
210            frequency: 440.0,
211            level: 0.5,
212        }
213    }
214
215    #[test]
216    fn waveform_from_index_maps_correctly() {
217        assert_eq!(Waveform::from_index(0.0), Waveform::Sine);
218        assert_eq!(Waveform::from_index(1.0), Waveform::Square);
219        assert_eq!(Waveform::from_index(2.0), Waveform::Saw);
220        assert_eq!(Waveform::from_index(3.0), Waveform::Triangle);
221    }
222
223    #[test]
224    fn waveform_from_index_out_of_range_defaults_to_sine() {
225        assert_eq!(Waveform::from_index(-1.0), Waveform::Sine);
226        assert_eq!(Waveform::from_index(4.0), Waveform::Sine);
227        assert_eq!(Waveform::from_index(100.0), Waveform::Sine);
228    }
229
230    #[test]
231    fn waveform_from_index_rounds_floats() {
232        assert_eq!(Waveform::from_index(0.4), Waveform::Sine);
233        assert_eq!(Waveform::from_index(0.6), Waveform::Square);
234        assert_eq!(Waveform::from_index(1.5), Waveform::Saw);
235        assert_eq!(Waveform::from_index(2.7), Waveform::Triangle);
236    }
237
238    #[test]
239    fn sine_wave_zero_crossing_and_peak() {
240        assert!((generate_waveform_sample(Waveform::Sine, 0.0)).abs() < 1e-5);
241        assert!((generate_waveform_sample(Waveform::Sine, 0.25) - 1.0).abs() < 1e-5);
242        assert!((generate_waveform_sample(Waveform::Sine, 0.5)).abs() < 1e-5);
243        assert!((generate_waveform_sample(Waveform::Sine, 0.75) + 1.0).abs() < 1e-5);
244    }
245
246    #[test]
247    fn square_wave_values() {
248        assert_eq!(generate_waveform_sample(Waveform::Square, 0.0), 1.0);
249        assert_eq!(generate_waveform_sample(Waveform::Square, 0.25), 1.0);
250        assert_eq!(generate_waveform_sample(Waveform::Square, 0.5), -1.0);
251        assert_eq!(generate_waveform_sample(Waveform::Square, 0.75), -1.0);
252    }
253
254    #[test]
255    fn saw_wave_values() {
256        assert!((generate_waveform_sample(Waveform::Saw, 0.0) + 1.0).abs() < 1e-5);
257        assert!((generate_waveform_sample(Waveform::Saw, 0.5)).abs() < 1e-5);
258        assert!((generate_waveform_sample(Waveform::Saw, 1.0) - 1.0).abs() < 1e-5);
259    }
260
261    #[test]
262    fn triangle_wave_values() {
263        assert!((generate_waveform_sample(Waveform::Triangle, 0.0) + 1.0).abs() < 1e-5);
264        assert!((generate_waveform_sample(Waveform::Triangle, 0.25)).abs() < 1e-5);
265        assert!((generate_waveform_sample(Waveform::Triangle, 0.5) - 1.0).abs() < 1e-5);
266        assert!((generate_waveform_sample(Waveform::Triangle, 0.75)).abs() < 1e-5);
267    }
268
269    #[test]
270    fn oscillator_outputs_silence_when_disabled() {
271        let mut osc = Oscillator::default();
272        osc.set_sample_rate(48_000.0);
273
274        let mut left = [1.0_f32; 64];
275        let mut right = [1.0_f32; 64];
276        let mut buffer = [&mut left[..], &mut right[..]];
277
278        osc.process(&mut buffer, &Transport::default(), &test_params(false));
279
280        assert!(left.iter().all(|s| s.abs() <= f32::EPSILON));
281        assert!(right.iter().all(|s| s.abs() <= f32::EPSILON));
282    }
283
284    #[test]
285    fn oscillator_outputs_signal_when_enabled() {
286        let mut osc = Oscillator::default();
287        osc.set_sample_rate(48_000.0);
288
289        let mut left = [0.0_f32; 128];
290        let mut right = [0.0_f32; 128];
291        let mut buffer = [&mut left[..], &mut right[..]];
292
293        osc.process(&mut buffer, &Transport::default(), &test_params(true));
294
295        let peak_left = left
296            .iter()
297            .fold(0.0_f32, |acc, sample| acc.max(sample.abs()));
298        let peak_right = right
299            .iter()
300            .fold(0.0_f32, |acc, sample| acc.max(sample.abs()));
301
302        assert!(
303            peak_left > 0.01,
304            "expected audible oscillator output on left"
305        );
306        assert!(
307            peak_right > 0.01,
308            "expected audible oscillator output on right"
309        );
310    }
311
312    #[test]
313    fn all_waveforms_produce_signal_when_enabled() {
314        for waveform_index in 0..4 {
315            let mut osc = Oscillator::default();
316            osc.set_sample_rate(48_000.0);
317
318            let mut left = [0.0_f32; 128];
319            let mut right = [0.0_f32; 128];
320            let mut buffer = [&mut left[..], &mut right[..]];
321
322            osc.process(
323                &mut buffer,
324                &Transport::default(),
325                &test_params_with_waveform(true, waveform_index as f32),
326            );
327
328            let peak = left
329                .iter()
330                .fold(0.0_f32, |acc, sample| acc.max(sample.abs()));
331            assert!(
332                peak > 0.01,
333                "waveform index {waveform_index} should produce signal"
334            );
335        }
336    }
337}