Skip to main content

wavecraft_processors/
test_tone_processor.rs

1//! Test tone processor — a simple sine-wave tone generator.
2
3use wavecraft_dsp::{ParamRange, ParamSpec, Processor, ProcessorParams, Transport};
4
5/// Generate a single sine-wave sample for the given phase (0.0–1.0).
6#[inline]
7fn generate_sine_sample(phase: f32) -> f32 {
8    (phase * std::f32::consts::TAU).sin()
9}
10
11#[inline]
12fn advance_phase(phase: &mut f32, phase_delta: f32) {
13    *phase += phase_delta;
14    if *phase >= 1.0 {
15        *phase -= 1.0;
16    }
17}
18
19/// Test tone processor parameters.
20#[derive(Clone)]
21pub struct TestToneProcessorParams {
22    /// Enable/disable test tone generation.
23    pub enabled: bool,
24
25    /// Frequency in Hz. `factor = 2.5` gives a logarithmic feel in the UI.
26    pub frequency: f32,
27
28    /// Output level as normalized amplitude (0.0 – 1.0).
29    pub level: f32,
30}
31
32impl Default for TestToneProcessorParams {
33    fn default() -> Self {
34        Self {
35            enabled: false,
36            frequency: 440.0,
37            level: 0.5,
38        }
39    }
40}
41
42impl ProcessorParams for TestToneProcessorParams {
43    fn param_specs() -> &'static [ParamSpec] {
44        static SPECS: [ParamSpec; 3] = [
45            ParamSpec {
46                name: "Enabled",
47                id_suffix: "enabled",
48                range: ParamRange::Stepped { min: 0, max: 1 },
49                default: 0.0,
50                unit: "",
51                group: None,
52            },
53            ParamSpec {
54                name: "Frequency",
55                id_suffix: "frequency",
56                range: ParamRange::Skewed {
57                    min: 20.0,
58                    max: 20_000.0,
59                    factor: 2.5,
60                },
61                default: 440.0,
62                unit: "Hz",
63                group: None,
64            },
65            ParamSpec {
66                name: "Level",
67                id_suffix: "level",
68                range: ParamRange::Linear { min: 0.0, max: 1.0 },
69                default: 0.5,
70                unit: "%",
71                group: None,
72            },
73        ];
74
75        &SPECS
76    }
77
78    fn from_param_defaults() -> Self {
79        Self::default()
80    }
81
82    fn apply_plain_values(&mut self, values: &[f32]) {
83        if let Some(enabled) = values.first() {
84            self.enabled = *enabled >= 0.5;
85        }
86        if let Some(frequency) = values.get(1) {
87            self.frequency = *frequency;
88        }
89        if let Some(level) = values.get(2) {
90            self.level = *level;
91        }
92    }
93}
94
95/// A minimal test tone source that produces a sine wave.
96#[derive(Default)]
97pub struct TestToneProcessor {
98    /// Current sample rate provided by the host.
99    sample_rate: f32,
100    /// Phase position within one cycle (0.0 – 1.0).
101    phase: f32,
102}
103
104impl Processor for TestToneProcessor {
105    type Params = TestToneProcessorParams;
106
107    fn set_sample_rate(&mut self, sample_rate: f32) {
108        self.sample_rate = sample_rate;
109    }
110
111    fn process(
112        &mut self,
113        buffer: &mut [&mut [f32]],
114        _transport: &Transport,
115        params: &Self::Params,
116    ) {
117        if !params.enabled {
118            return;
119        }
120
121        if self.sample_rate == 0.0 {
122            return;
123        }
124
125        let phase_delta = params.frequency / self.sample_rate;
126        let start_phase = self.phase;
127
128        for channel in buffer.iter_mut() {
129            self.phase = start_phase;
130            for sample in channel.iter_mut() {
131                *sample += generate_sine_sample(self.phase) * params.level;
132                advance_phase(&mut self.phase, phase_delta);
133            }
134        }
135    }
136
137    fn reset(&mut self) {
138        self.phase = 0.0;
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145    use wavecraft_dsp::Bypassed;
146
147    fn test_params() -> TestToneProcessorParams {
148        TestToneProcessorParams {
149            enabled: true,
150            frequency: 440.0,
151            level: 0.5,
152        }
153    }
154
155    fn test_params_with_level(level: f32) -> TestToneProcessorParams {
156        TestToneProcessorParams {
157            enabled: true,
158            frequency: 440.0,
159            level,
160        }
161    }
162
163    #[test]
164    fn sine_wave_zero_crossing_and_peak() {
165        assert!((generate_sine_sample(0.0)).abs() < 1e-5);
166        assert!((generate_sine_sample(0.25) - 1.0).abs() < 1e-5);
167        assert!((generate_sine_sample(0.5)).abs() < 1e-5);
168        assert!((generate_sine_sample(0.75) + 1.0).abs() < 1e-5);
169    }
170
171    #[test]
172    fn test_tone_processor_preserves_passthrough_when_level_is_zero() {
173        let mut test_tone = TestToneProcessor::default();
174        test_tone.set_sample_rate(48_000.0);
175
176        let mut left = [0.25_f32; 64];
177        let mut right = [-0.5_f32; 64];
178        let left_in = left;
179        let right_in = right;
180        let mut buffer = [&mut left[..], &mut right[..]];
181
182        test_tone.process(
183            &mut buffer,
184            &Transport::default(),
185            &test_params_with_level(0.0),
186        );
187
188        for (actual, expected) in left.iter().zip(left_in.iter()) {
189            assert!((actual - expected).abs() <= f32::EPSILON);
190        }
191
192        for (actual, expected) in right.iter().zip(right_in.iter()) {
193            assert!((actual - expected).abs() <= f32::EPSILON);
194        }
195    }
196
197    #[test]
198    fn test_tone_processor_generates_signal_on_silent_input() {
199        let mut test_tone = TestToneProcessor::default();
200        test_tone.set_sample_rate(48_000.0);
201
202        let mut left = [0.0_f32; 128];
203        let mut right = [0.0_f32; 128];
204        let mut buffer = [&mut left[..], &mut right[..]];
205
206        test_tone.process(&mut buffer, &Transport::default(), &test_params());
207
208        let peak_left = left
209            .iter()
210            .fold(0.0_f32, |acc, sample| acc.max(sample.abs()));
211        let peak_right = right
212            .iter()
213            .fold(0.0_f32, |acc, sample| acc.max(sample.abs()));
214
215        assert!(
216            peak_left > 0.01,
217            "expected audible test tone output on left"
218        );
219        assert!(
220            peak_right > 0.01,
221            "expected audible test tone output on right"
222        );
223    }
224
225    #[test]
226    fn test_tone_processor_adds_signal_without_removing_input() {
227        let mut test_tone_mixed = TestToneProcessor::default();
228        test_tone_mixed.set_sample_rate(48_000.0);
229
230        let mut left_mixed = [0.2_f32; 128];
231        let mut right_mixed = [-0.15_f32; 128];
232        let left_input = left_mixed;
233        let right_input = right_mixed;
234        let mut mixed_buffer = [&mut left_mixed[..], &mut right_mixed[..]];
235
236        test_tone_mixed.process(&mut mixed_buffer, &Transport::default(), &test_params());
237
238        let mut test_tone_only = TestToneProcessor::default();
239        test_tone_only.set_sample_rate(48_000.0);
240
241        let mut left_tone_only = [0.0_f32; 128];
242        let mut right_tone_only = [0.0_f32; 128];
243        let mut tone_only_buffer = [&mut left_tone_only[..], &mut right_tone_only[..]];
244
245        test_tone_only.process(&mut tone_only_buffer, &Transport::default(), &test_params());
246
247        for i in 0..left_mixed.len() {
248            let additive_component_left = left_mixed[i] - left_input[i];
249            let additive_component_right = right_mixed[i] - right_input[i];
250
251            assert!((additive_component_left - left_tone_only[i]).abs() < 1e-6);
252            assert!((additive_component_right - right_tone_only[i]).abs() < 1e-6);
253        }
254    }
255
256    #[test]
257    fn test_tone_processor_bypass_wrapper_mutes_generator_output() {
258        let mut wrapped = Bypassed::new(TestToneProcessor::default());
259        wrapped.set_sample_rate(48_000.0);
260
261        type WrappedParams = <Bypassed<TestToneProcessor> as Processor>::Params;
262        let bypassed_params = WrappedParams {
263            inner: test_params(),
264            bypassed: true,
265        };
266
267        for _ in 0..4 {
268            let mut left = [0.0_f32; 128];
269            let mut right = [0.0_f32; 128];
270            let mut buffer = [&mut left[..], &mut right[..]];
271
272            wrapped.process(&mut buffer, &Transport::default(), &bypassed_params);
273        }
274
275        let mut left = [0.0_f32; 128];
276        let mut right = [0.0_f32; 128];
277        let mut buffer = [&mut left[..], &mut right[..]];
278        wrapped.process(&mut buffer, &Transport::default(), &bypassed_params);
279
280        let peak_left = left
281            .iter()
282            .fold(0.0_f32, |acc, sample| acc.max(sample.abs()));
283        let peak_right = right
284            .iter()
285            .fold(0.0_f32, |acc, sample| acc.max(sample.abs()));
286
287        assert!(
288            peak_left <= 1e-6,
289            "expected bypassed test tone to contribute no left-channel signal"
290        );
291        assert!(
292            peak_right <= 1e-6,
293            "expected bypassed test tone to contribute no right-channel signal"
294        );
295    }
296
297    #[test]
298    fn apply_plain_values_updates_all_fields() {
299        let mut params = TestToneProcessorParams::default();
300        params.apply_plain_values(&[1.0, 1760.0, 0.9]);
301
302        assert!(params.enabled);
303        assert!((params.frequency - 1760.0).abs() < f32::EPSILON);
304        assert!((params.level - 0.9).abs() < f32::EPSILON);
305    }
306
307    #[test]
308    fn test_tone_processor_disabled_by_default() {
309        let mut test_tone = TestToneProcessor::default();
310        test_tone.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        let params = TestToneProcessorParams::default();
317        test_tone.process(&mut buffer, &Transport::default(), &params);
318
319        assert!(left.iter().all(|sample| sample.abs() <= f32::EPSILON));
320        assert!(right.iter().all(|sample| sample.abs() <= f32::EPSILON));
321    }
322
323    #[test]
324    fn frequency_param_uses_full_audible_range() {
325        let specs = TestToneProcessorParams::param_specs();
326        let frequency = specs
327            .iter()
328            .find(|spec| spec.id_suffix == "frequency")
329            .expect("frequency spec should exist");
330
331        match frequency.range {
332            ParamRange::Skewed { min, max, .. } => {
333                assert!((min - 20.0).abs() < f64::EPSILON);
334                assert!((max - 20_000.0).abs() < f64::EPSILON);
335            }
336            _ => panic!("frequency should use a skewed range"),
337        }
338    }
339}