1use wavecraft_dsp::{ParamRange, ParamSpec, Processor, ProcessorParams, Transport};
4
5#[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#[derive(Clone)]
21pub struct TestToneProcessorParams {
22 pub enabled: bool,
24
25 pub frequency: f32,
27
28 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#[derive(Default)]
97pub struct TestToneProcessor {
98 sample_rate: f32,
100 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(), ¶ms);
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}