1pub struct ToneFeedback {
55 rf: f64,
57 ri: f64,
58 c_tone: f64,
59 r_shelf: f64,
60 max_pot_r: f64,
61 sample_rate: f64,
62
63 pub b0: f64,
65 pub b1: f64,
66 pub a1: f64,
67 pub dc_gain: f64,
68
69 x1: f64,
71 y1: f64,
72}
73
74impl ToneFeedback {
75 #[allow(clippy::too_many_arguments)]
87 pub fn new(
88 rf: f64,
89 ri: f64,
90 c_tone: f64,
91 r_shelf: f64,
92 max_pot_r: f64,
93 _pot_id: String,
94 sample_rate: f64,
95 pot_pos: f64,
96 ) -> Self {
97 let mut tf = ToneFeedback {
98 rf,
99 ri,
100 c_tone,
101 r_shelf,
102 max_pot_r,
103 sample_rate,
104 b0: 0.0,
105 b1: 0.0,
106 a1: 0.0,
107 dc_gain: 0.0,
108 x1: 0.0,
109 y1: 0.0,
110 };
111 tf.recompute(pot_pos);
112 tf
113 }
114
115 pub fn recompute(&mut self, pot_pos: f64) {
120 let rf = self.rf;
121 let ri = self.ri;
122 let c = self.c_tone;
123 let rs = self.r_shelf;
124 let rp = self.max_pot_r * pot_pos.clamp(0.0, 1.0);
125
126 let num1 = c * (rf * (rs + rp) + rs * rp);
135 let num0 = rf + rs;
136 let den1 = (rs + rp) * c;
137
138 let k = 2.0 * self.sample_rate;
143
144 let n0 = num1 * k + num0;
152 let n1 = -num1 * k + num0;
153 let d0 = den1 * k + 1.0;
154 let d1 = -den1 * k + 1.0;
155
156 let gain = -1.0 / ri;
158 self.b0 = gain * n0 / d0;
159 self.b1 = gain * n1 / d0;
160 self.a1 = d1 / d0;
161
162 self.dc_gain = -(rf + rs) / ri;
164 }
165
166 #[inline]
168 pub fn process(&mut self, x: f64) -> f64 {
169 let y = self.b0 * x + self.b1 * self.x1 - self.a1 * self.y1;
170 self.x1 = x;
171 self.y1 = y;
172 y
173 }
174
175 pub fn reset(&mut self) {
177 self.x1 = 0.0;
178 self.y1 = 0.0;
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185 use std::f64::consts::PI;
186
187 #[test]
189 fn dc_gain_matches_analytical() {
190 let tf = ToneFeedback::new(
191 1800.0,
192 1800.0,
193 3.9e-9,
194 4700.0,
195 10000.0,
196 "Treble".into(),
197 48000.0,
198 0.5,
199 );
200 let expected_dc = -(1800.0 + 4700.0) / 1800.0;
201 assert!(
202 (tf.dc_gain - expected_dc).abs() < 1e-10,
203 "DC gain mismatch: got {}, expected {}",
204 tf.dc_gain,
205 expected_dc
206 );
207 }
208
209 #[test]
211 fn coefficients_vary_with_pot() {
212 let tf0 = ToneFeedback::new(
213 1800.0,
214 1800.0,
215 3.9e-9,
216 4700.0,
217 10000.0,
218 "Treble".into(),
219 48000.0,
220 0.0,
221 );
222 let tf1 = ToneFeedback::new(
223 1800.0,
224 1800.0,
225 3.9e-9,
226 4700.0,
227 10000.0,
228 "Treble".into(),
229 48000.0,
230 1.0,
231 );
232
233 assert!((tf0.dc_gain - tf1.dc_gain).abs() < 1e-10);
235
236 assert!(
238 (tf0.b0 - tf1.b0).abs() > 1e-6,
239 "b0 should differ: {} vs {}",
240 tf0.b0,
241 tf1.b0
242 );
243 }
244
245 #[test]
247 fn frequency_response_shelf() {
248 let sr = 48000.0;
249 let freq = 10000.0;
250 let n = 4096usize;
251
252 let mut rms_by_pos = Vec::new();
253
254 for &pos in &[0.0, 0.5, 1.0] {
255 let mut tf = ToneFeedback::new(
256 1800.0,
257 1800.0,
258 3.9e-9,
259 4700.0,
260 10000.0,
261 "Treble".into(),
262 sr,
263 pos,
264 );
265
266 for i in 0..2048 {
268 let x = 0.1 * (2.0 * PI * freq * i as f64 / sr).sin();
269 tf.process(x);
270 }
271
272 let mut sum_sq = 0.0;
274 for i in 0..n {
275 let x = 0.1 * (2.0 * PI * freq * (i + 2048) as f64 / sr).sin();
276 let y = tf.process(x);
277 sum_sq += y * y;
278 }
279 rms_by_pos.push((sum_sq / n as f64).sqrt());
280 }
281
282 assert!(
285 rms_by_pos[0] < rms_by_pos[2],
286 "Treble pot should boost HF: pos=0 rms={:.6}, pos=1 rms={:.6}",
287 rms_by_pos[0],
288 rms_by_pos[2]
289 );
290 }
291
292 #[test]
294 fn stability() {
295 let mut tf = ToneFeedback::new(
296 1800.0,
297 1800.0,
298 3.9e-9,
299 4700.0,
300 10000.0,
301 "Treble".into(),
302 48000.0,
303 0.5,
304 );
305
306 for i in 0..10000 {
307 let x = 0.5 * (2.0 * PI * 1000.0 * i as f64 / 48000.0).sin();
308 let y = tf.process(x);
309 assert!(y.is_finite(), "Output became non-finite at sample {}", i);
310 }
311 }
312
313 use crate::compiler::dyn_node::DynNode;
318 use crate::compiler::stage::OpAmpWdfAdaptor;
319 use crate::dsl::PotTaper;
320 use crate::elements::nonlinear::{OpAmpModel, OpAmpRoot};
321
322 fn build_klon_zf(sr: f64, pot_pos: f64) -> DynNode {
325 let r_tone_fb = DynNode::Resistor(Some("R_tone_fb".into()), 1800.0);
326 let r_shelf = DynNode::Resistor(Some("R_tone_shelf".into()), 4700.0);
327
328 let max_pot_r = 10000.0;
329 let treble_wb = DynNode::Pot("Treble__wb".into(), max_pot_r, 1.0 - pot_pos, PotTaper::B);
330 let c_tone_rp = 1.0 / (2.0 * sr * 3.9e-9);
331 let c_tone = DynNode::Capacitor(Some("C_tone".into()), 3.9e-9, c_tone_rp);
332 let treble_aw = DynNode::Pot("Treble__aw".into(), max_pot_r, pot_pos, PotTaper::B);
333
334 let inner_par = DynNode::Parallel(Box::new(r_shelf), Box::new(treble_wb));
336 let inner_ser = DynNode::Series(Box::new(c_tone), Box::new(treble_aw));
338 let outer_ser = DynNode::Series(Box::new(inner_par), Box::new(inner_ser));
340 DynNode::Parallel(Box::new(r_tone_fb), Box::new(outer_ser))
342 }
343
344 fn build_klon_adaptor(sr: f64, pot_pos: f64) -> OpAmpWdfAdaptor {
346 let ri = 1800.0;
347 let zi = DynNode::VoltageSource(0.0, ri);
348 let zf = build_klon_zf(sr, pot_pos);
349
350 let model = OpAmpModel::tl072();
351 let gain = 1800.0 / ri; let mut opamp = OpAmpRoot::new_inverting(model, 1.0);
353 opamp.set_sample_rate(sr);
354 opamp.set_gbw_gain(gain);
355 opamp.set_v_max(3.0); OpAmpWdfAdaptor::new(zi, zf, true, opamp, Some("Treble".into()))
358 }
359
360 fn measure_rms(adaptor: &mut OpAmpWdfAdaptor, freq: f64, sr: f64, amplitude: f64) -> f64 {
362 let warmup = 4096;
363 let n = 4096;
364
365 for i in 0..warmup {
367 let x = amplitude * (2.0 * PI * freq * i as f64 / sr).sin();
368 adaptor.process(0.0, x); }
370 let mut sum_sq = 0.0;
372 for i in 0..n {
373 let x = amplitude * (2.0 * PI * freq * (i + warmup) as f64 / sr).sin();
374 let y = adaptor.process(0.0, x);
375 sum_sq += y * y;
376 }
377 (sum_sq / n as f64).sqrt()
378 }
379
380 #[test]
381 fn wdf_adaptor_produces_signal() {
382 let sr = 48000.0;
383 let mut adaptor = build_klon_adaptor(sr, 0.5);
384 let rms = measure_rms(&mut adaptor, 1000.0, sr, 0.1);
385 eprintln!("WDF adaptor 1kHz RMS = {:.6}", rms);
386 assert!(
387 rms > 0.01,
388 "Adaptor should produce audible output, got {:.6}",
389 rms
390 );
391 }
392
393 #[test]
394 fn wdf_adaptor_stable() {
395 let sr = 48000.0;
396 let mut adaptor = build_klon_adaptor(sr, 0.5);
397 for i in 0..20000 {
398 let x = 0.5 * (2.0 * PI * 1000.0 * i as f64 / sr).sin();
399 let y = adaptor.process(0.0, x);
400 assert!(y.is_finite(), "Output NaN/Inf at sample {}", i);
401 assert!(
402 y.abs() < 100.0,
403 "Output diverging at sample {}: {:.2}",
404 i,
405 y
406 );
407 }
408 }
409
410 #[test]
411 fn wdf_adaptor_dc_gain_inverting() {
412 let sr = 48000.0;
415 let mut adaptor = build_klon_adaptor(sr, 0.5);
416
417 let dc_in = 0.1;
419 let mut last = 0.0;
420 for _ in 0..10000 {
421 last = adaptor.process(0.0, dc_in);
422 }
423 let dc_gain = last / dc_in;
424 eprintln!("WDF adaptor DC gain = {:.4}", dc_gain);
425 let gain_mag = dc_gain.abs();
429 assert!(
430 gain_mag > 0.3 && gain_mag < 15.0,
431 "DC gain magnitude should be reasonable, got {:.4} (mag={:.4})",
432 dc_gain,
433 gain_mag
434 );
435 }
436
437 #[test]
438 fn wdf_adaptor_treble_changes_spectrum() {
439 let sr = 48000.0;
440 let freq = 10000.0; let mut adaptor_dark = build_klon_adaptor(sr, 0.0);
443 let mut adaptor_bright = build_klon_adaptor(sr, 1.0);
444
445 let rms_dark = measure_rms(&mut adaptor_dark, freq, sr, 0.1);
446 let rms_bright = measure_rms(&mut adaptor_bright, freq, sr, 0.1);
447
448 let ratio_db = 20.0 * (rms_bright / rms_dark).log10();
449 eprintln!(
450 "WDF adaptor treble at {}Hz: dark={:.6}, bright={:.6}, ratio={:.3}x, dB={:.2}",
451 freq,
452 rms_dark,
453 rms_bright,
454 rms_bright / rms_dark,
455 ratio_db
456 );
457
458 assert!(
459 ratio_db.abs() > 0.5,
460 "Treble pot should affect 10kHz output by ≥0.5dB, got {:.2}dB",
461 ratio_db
462 );
463 }
464
465 #[test]
466 fn wdf_adaptor_frequency_response_shape() {
467 let sr = 48000.0;
470
471 let mut adaptor_dark = build_klon_adaptor(sr, 0.0);
472 let mut adaptor_bright = build_klon_adaptor(sr, 1.0);
473
474 let freqs = [100.0, 1000.0, 5000.0, 10000.0];
475 let mut ratios = Vec::new();
476
477 for &freq in &freqs {
478 let mut ad = build_klon_adaptor(sr, 0.0);
480 let mut ab = build_klon_adaptor(sr, 1.0);
481 let rms_d = measure_rms(&mut ad, freq, sr, 0.1);
482 let rms_b = measure_rms(&mut ab, freq, sr, 0.1);
483 let ratio = if rms_d > 1e-10 { rms_b / rms_d } else { 1.0 };
484 let db = 20.0 * ratio.log10();
485 eprintln!(
486 " {}Hz: dark={:.6} bright={:.6} ratio={:.4} ({:.2}dB)",
487 freq, rms_d, rms_b, ratio, db
488 );
489 ratios.push(db);
490 }
491
492 let hf_lf_diff = (ratios[3] - ratios[0]).abs();
496 eprintln!("HF-LF pot effect difference: {:.2}dB", hf_lf_diff);
497 assert!(
498 hf_lf_diff > 0.1,
499 "Shelving EQ should affect HF more than LF, diff={:.2}dB",
500 hf_lf_diff
501 );
502 }
503}