1use std::sync::Arc;
22
23use rustfft::num_complex::Complex64;
24use rustfft::{Fft, FftPlanner};
25
26use super::Model;
27use super::convert::PEDAL_TAPS;
28
29pub const SR: usize = 48000;
31
32const F_KNOTS: [f64; 17] =
37 [-32.0, -16.0, -8.0, -4.0, -2.0, -1.0, -0.5, -0.25, 0.0, 0.25, 0.5, 1.0, 2.0, 4.0, 8.0, 16.0, 32.0];
38const F_VALUES: [f64; 17] = [
41 -0.159341381678132,
42 -0.1515523482954498,
43 -0.12383923234960825,
44 -0.07909273657985298,
45 -0.04498259976139617,
46 -0.02245628731142803,
47 -0.011406034112795504,
48 -0.005583136803729503,
49 0.00018537827202383975,
50 0.005986027774350994,
51 0.01180106412522064,
52 0.02312483036632483,
53 0.045952939777459255,
54 0.08342816833097066,
55 0.13236822043215563,
56 0.15836762403716023,
57 0.16086942240926744,
58];
59
60fn knot_interval(u: f64) -> Option<usize> {
63 if u <= F_KNOTS[0] || u > F_KNOTS[F_KNOTS.len() - 1] {
64 return None;
65 }
66 Some(F_KNOTS.partition_point(|&k| k < u) - 1)
67}
68
69pub fn saturation(u: f64) -> f64 {
71 match knot_interval(u) {
72 None if u <= F_KNOTS[0] => F_VALUES[0],
73 None => F_VALUES[F_VALUES.len() - 1],
74 Some(i) => {
75 let t = (u - F_KNOTS[i]) / (F_KNOTS[i + 1] - F_KNOTS[i]);
76 F_VALUES[i] * (1.0 - t) + F_VALUES[i + 1] * t
77 }
78 }
79}
80
81fn saturation_slope(u: f64) -> f64 {
83 knot_interval(u).map_or(0.0, |i| (F_VALUES[i + 1] - F_VALUES[i]) / (F_KNOTS[i + 1] - F_KNOTS[i]))
84}
85
86fn plan(n: usize) -> (Arc<dyn Fft<f64>>, Arc<dyn Fft<f64>>) {
90 let mut p = FftPlanner::new();
91 (p.plan_fft_forward(n), p.plan_fft_inverse(n))
92}
93
94fn complex(x: &[f64], n: usize) -> Vec<Complex64> {
95 let mut v: Vec<Complex64> = x.iter().take(n).map(|&r| Complex64::new(r, 0.0)).collect();
96 v.resize(n, Complex64::default());
97 v
98}
99
100struct Conv {
103 fwd: Arc<dyn Fft<f64>>,
104 inv: Arc<dyn Fft<f64>>,
105}
106
107const BLOCK: usize = 8192;
109const HIST: usize = PEDAL_TAPS - 1;
111const HOP: usize = BLOCK - HIST;
113
114impl Conv {
115 fn new() -> Self {
116 let (fwd, inv) = plan(BLOCK);
117 Conv { fwd, inv }
118 }
119
120 fn blocks(len: usize) -> usize {
121 len.div_ceil(HOP)
122 }
123
124 fn filter(&self, h: &[f64]) -> Vec<Complex64> {
126 assert!(h.len() <= PEDAL_TAPS);
127 let mut v = complex(h, BLOCK);
128 self.fwd.process(&mut v);
129 v
130 }
131
132 fn signal(&self, x: &[f64]) -> Vec<Vec<Complex64>> {
135 par_chunks(Self::blocks(x.len()), |blocks| {
136 blocks
137 .map(|b| {
138 let s = (b * HOP) as isize - HIST as isize;
139 let mut v: Vec<Complex64> = (0..BLOCK as isize)
140 .map(|j| {
141 let i = s + j;
142 Complex64::new(if i >= 0 && (i as usize) < x.len() { x[i as usize] } else { 0.0 }, 0.0)
143 })
144 .collect();
145 self.fwd.process(&mut v);
146 v
147 })
148 .collect::<Vec<_>>()
149 })
150 .concat()
151 }
152
153 fn apply(&self, blocks: &[Vec<Complex64>], h: &[Complex64], len: usize) -> Vec<f64> {
155 let mut y = par_chunks(blocks.len(), |range| {
156 let mut out = Vec::with_capacity(range.len() * HOP);
157 let mut buf = vec![Complex64::default(); BLOCK];
158 for xb in &blocks[range] {
159 for ((o, a), c) in buf.iter_mut().zip(xb).zip(h) {
160 *o = a * c;
161 }
162 self.inv.process(&mut buf);
163 out.extend(buf[HIST..].iter().map(|v| v.re / BLOCK as f64));
164 }
165 out
166 })
167 .concat();
168 y.truncate(len);
169 y
170 }
171
172 fn correlate(&self, g: &[f64], h: &[Complex64]) -> Vec<f64> {
174 let mut gx = par_chunks(Self::blocks(g.len()), |range| {
175 let mut out = Vec::with_capacity(range.len() * HOP);
176 let mut buf = vec![Complex64::default(); BLOCK];
177 for b in range {
178 let s = b * HOP;
179 for (j, o) in buf.iter_mut().enumerate() {
180 *o = Complex64::new(g.get(s + j).copied().unwrap_or(0.0), 0.0);
181 }
182 self.fwd.process(&mut buf);
183 for (o, c) in buf.iter_mut().zip(h) {
184 *o *= c.conj();
185 }
186 self.inv.process(&mut buf);
187 out.extend(buf[..HOP].iter().map(|v| v.re / BLOCK as f64));
188 }
189 out
190 })
191 .concat();
192 gx.truncate(g.len());
193 gx
194 }
195
196 fn filter_grad(&self, blocks: &[Vec<Complex64>], g: &[f64]) -> Vec<f64> {
199 let parts = par_chunks(blocks.len(), |range| {
200 let mut acc = vec![Complex64::default(); BLOCK];
201 let mut buf = vec![Complex64::default(); BLOCK];
202 for b in range {
203 let s = b * HOP;
204 buf.fill(Complex64::default());
205 for (j, &v) in g[s..(s + HOP).min(g.len())].iter().enumerate() {
206 buf[HIST + j].re = v;
207 }
208 self.fwd.process(&mut buf);
209 for ((a, gb), xc) in acc.iter_mut().zip(&buf).zip(&blocks[b]) {
210 *a += gb * xc.conj();
211 }
212 }
213 acc
214 });
215 let mut acc = vec![Complex64::default(); BLOCK];
216 for p in parts {
217 acc.iter_mut().zip(p).for_each(|(a, v)| *a += v);
218 }
219 self.inv.process(&mut acc);
220 acc[..PEDAL_TAPS].iter().map(|c| c.re / BLOCK as f64).collect()
221 }
222}
223
224fn par_chunks<T: Send>(n: usize, f: impl Fn(std::ops::Range<usize>) -> T + Sync) -> Vec<T> {
226 let threads = std::thread::available_parallelism().map_or(1, |t| t.get()).clamp(1, 16).min(n.max(1));
227 let per = n.div_ceil(threads);
228 if threads == 1 {
229 return vec![f(0..n)];
230 }
231 std::thread::scope(|s| {
232 let f = &f;
233 let handles: Vec<_> =
234 (0..threads).map(|t| s.spawn(move || f((t * per).min(n)..((t + 1) * per).min(n)))).collect();
235 handles.into_iter().map(|h| h.join().expect("optimiser thread")).collect()
236 })
237}
238
239fn par_map<T: Send>(n: usize, f: impl Fn(usize) -> T + Sync) -> Vec<T> {
241 use std::sync::atomic::{AtomicUsize, Ordering};
242 let next = AtomicUsize::new(0);
243 let threads = std::thread::available_parallelism().map_or(1, |t| t.get()).clamp(1, 16).min(n.max(1));
244 let mut done: Vec<(usize, T)> = std::thread::scope(|s| {
245 let (f, next) = (&f, &next);
246 let handles: Vec<_> = (0..threads)
247 .map(|_| {
248 s.spawn(move || {
249 let mut v = Vec::new();
250 loop {
251 let i = next.fetch_add(1, Ordering::Relaxed);
252 if i >= n {
253 return v;
254 }
255 v.push((i, f(i)));
256 }
257 })
258 })
259 .collect();
260 handles.into_iter().flat_map(|h| h.join().expect("optimiser thread")).collect()
261 });
262 done.sort_by_key(|(i, _)| *i);
263 done.into_iter().map(|(_, t)| t).collect()
264}
265
266#[derive(Clone, Debug, PartialEq)]
271pub struct Segment {
272 pub name: String,
273 pub start: usize,
274 pub len: usize,
275 pub train: bool,
277}
278
279#[derive(Clone, Debug)]
281pub struct TestSignal {
282 pub x: Vec<f64>,
283 pub segments: Vec<Segment>,
284}
285
286struct Rng(u64);
288
289impl Rng {
290 fn uniform(&mut self) -> f64 {
291 self.0 ^= self.0 >> 12;
292 self.0 ^= self.0 << 25;
293 self.0 ^= self.0 >> 27;
294 ((self.0.wrapping_mul(0x2545_f491_4f6c_dd1d) >> 11) as f64 + 0.5) / (1u64 << 53) as f64
295 }
296
297 fn normal(&mut self) -> f64 {
298 let (a, b) = (self.uniform(), self.uniform());
299 (-2.0 * a.ln()).sqrt() * (std::f64::consts::TAU * b).cos()
300 }
301}
302
303fn pink(n: usize, seed: u64, weight: impl Fn(usize) -> Option<f64>) -> Vec<f64> {
306 let mut rng = Rng(seed);
307 let mut v: Vec<Complex64> = (0..n).map(|_| Complex64::new(rng.normal(), 0.0)).collect();
308 let (fwd, inv) = plan(n);
309 fwd.process(&mut v);
310 for k in 0..=n / 2 {
311 let g = weight(k).map_or(0.0, |w| 1.0 / w.sqrt());
312 v[k] *= g;
313 if k != 0 && k != n - k {
314 v[n - k] *= g;
315 }
316 }
317 inv.process(&mut v);
318 let x: Vec<f64> = v.iter().map(|c| c.re).collect();
319 let rms = (x.iter().map(|a| a * a).sum::<f64>() / n as f64).sqrt();
320 x.iter().map(|a| a / rms).collect()
321}
322
323pub fn test_signal() -> TestSignal {
327 let mut x: Vec<f64> = Vec::new();
328 let mut segments = Vec::new();
329 let mut add = |name: String, mut s: Vec<f64>, gap: usize, fade: bool, train: bool, x: &mut Vec<f64>| {
330 if fade {
331 let n = 480;
332 for i in 0..n {
333 let g = i as f64 / (n - 1) as f64;
334 s[i] *= g;
335 let j = s.len() - n + i;
336 s[j] *= 1.0 - g;
337 }
338 s.iter_mut().for_each(|v| *v = *v as f32 as f64);
340 }
341 segments.push(Segment { name, start: x.len(), len: s.len(), train });
342 x.extend_from_slice(&s);
343 x.resize(x.len() + gap, 0.0);
344 };
345 let db = |d: f64| 10f64.powf(d / 20.0);
346
347 let (t_len, f1, f2): (f64, f64, f64) = (4.0, 20.0, 20000.0);
349 let l = t_len / (f2 / f1).ln();
350 let sweep: Vec<f64> = (0..(t_len * SR as f64) as usize)
351 .map(|i| (std::f64::consts::TAU * f1 * l * ((i as f64 / SR as f64 / l).exp() - 1.0)).sin())
352 .collect();
353 for d in [-36.0, -24.0, -12.0, -6.0] {
354 add(format!("sweep {d}"), sweep.iter().map(|v| v * db(d)).collect(), SR * 6 / 10, true, true, &mut x);
355 }
356 for f in [41.2, 55.0, 73.4, 98.0, 196.0] {
358 for d in [-24.0, -12.0, -6.0] {
359 let tone = (0..SR).map(|i| (std::f64::consts::TAU * f * (i as f64 / SR as f64)).sin() * db(d)).collect();
360 add(format!("tone {f:.1} {d}"), tone, SR * 3 / 10, true, true, &mut x);
361 }
362 }
363 let p = pink(3 * SR, 1, |k| Some(k.max(1) as f64));
365 add("pink -18".into(), p.iter().map(|v| v * db(-18.0)).collect(), SR * 6 / 10, true, false, &mut x);
366 for (i, d) in [-30.0, -18.0, -6.0].into_iter().enumerate() {
368 x.resize(x.len() + SR / 2, 0.0);
369 let n = 3 * SR;
370 let p = pink(n, 1000 + i as u64, |k| {
371 let f = k as f64 * SR as f64 / n as f64;
372 (f >= 20.0).then_some(f)
373 });
374 add(format!("synth pink {}", d - 6.0), p.iter().map(|v| v * db(d) * 0.5).collect(), 0, false, true, &mut x);
376 }
377 x.resize(x.len() + SR / 2, 0.0);
378 TestSignal { x, segments }
379}
380
381const TARGET_GAIN_IN_DB: f64 = -12.0;
387const TARGET_GAIN_OUT: f64 = 0.09316022181625924;
388
389pub fn render_target(model: &mut Model, x: &[f64]) -> Vec<f64> {
392 let gin = 10f64.powf(TARGET_GAIN_IN_DB / 20.0);
393 let input: Vec<f32> = x.iter().map(|v| (v * gin) as f32).collect();
394 let mut out = vec![0.0f32; x.len()];
395 model.reset();
396 model.process(&input, &mut out);
397 const B: [f64; 3] = [0.9953827, -1.990765, 0.9953827];
399 const A: [f64; 2] = [-1.990744, 0.9907867];
400 let (mut x1, mut x2, mut y1, mut y2) = (0.0, 0.0, 0.0, 0.0);
401 out.iter()
402 .map(|&v| {
403 let x0 = v as f64;
404 let y0 = B[0] * x0 + B[1] * x1 + B[2] * x2 - A[0] * y1 - A[1] * y2;
405 (x2, x1, y2, y1) = (x1, x0, y1, y0);
406 y0 * TARGET_GAIN_OUT
407 })
408 .collect()
409}
410
411pub struct Simulator {
416 conv: Conv,
417 len: usize,
418 x: Vec<Vec<Complex64>>,
419}
420
421struct Run {
423 u: Vec<f64>,
425 v: Vec<Vec<Complex64>>,
427 y: Vec<f64>,
429}
430
431impl Simulator {
432 pub fn new(x: &[f64]) -> Self {
433 let conv = Conv::new();
434 let blocks = conv.signal(x);
435 Simulator { conv, len: x.len(), x: blocks }
436 }
437
438 fn run(&self, h1: &[f64], h2: &[f64]) -> Run {
439 let u = self.conv.apply(&self.x, &self.conv.filter(h1), self.len);
440 let fu: Vec<f64> = u.iter().map(|&a| saturation(a)).collect();
441 let v = self.conv.signal(&fu);
442 let y = self.conv.apply(&v, &self.conv.filter(h2), self.len);
443 Run { u, v, y }
444 }
445
446 fn backward(&self, run: &Run, h2: &[f64], gy: &[f64]) -> (Vec<f64>, Vec<f64>) {
448 let gh2 = self.conv.filter_grad(&run.v, gy);
449 let gv = self.conv.correlate(gy, &self.conv.filter(h2));
450 let gu: Vec<f64> = gv.iter().zip(&run.u).map(|(g, &u)| g * saturation_slope(u) + penalty_grad(u, self.len)).collect();
451 let gh1 = self.conv.filter_grad(&self.x, &gu);
452 (gh1, gh2)
453 }
454
455 pub fn output(&self, h1: &[f64], h2: &[f64]) -> Vec<f64> {
457 self.run(h1, h2).y
458 }
459}
460
461const PENALTY_LIMIT: f64 = 100.0;
464const PENALTY_WEIGHT: f64 = 1e-3;
465
466fn penalty(u: &[f64]) -> f64 {
467 PENALTY_WEIGHT * u.iter().map(|v| (v.abs() - PENALTY_LIMIT).max(0.0).powi(2)).sum::<f64>() / u.len() as f64
468}
469
470fn penalty_grad(u: f64, len: usize) -> f64 {
471 PENALTY_WEIGHT * 2.0 * (u.abs() - PENALTY_LIMIT).max(0.0) * u.signum() / len as f64
472}
473
474pub fn simulate(data: &[f32], x: &[f64]) -> Vec<f64> {
476 let (h1, h2) = super::convert::pedal_irs(data);
477 Simulator::new(x).output(&h1, &h2)
478}
479
480pub const CURVE_POINTS: usize = 41;
485const MP_N: usize = 8192;
487const MP_BINS: usize = MP_N / 2 + 1;
488
489fn control_points() -> [f64; CURVE_POINTS] {
491 std::array::from_fn(|i| 20f64.log10() + i as f64 * 2f64.log10() / 4.0)
492}
493
494fn bin_log_freqs() -> Vec<f64> {
496 (0..MP_BINS).map(|k| (k as f64 * SR as f64 / MP_N as f64).max(1e-3).log10()).collect()
497}
498
499struct Curves {
501 interp: Vec<(usize, f64)>,
504 fwd: Arc<dyn Fft<f64>>,
505 inv: Arc<dyn Fft<f64>>,
506}
507
508struct Design {
510 e: Vec<Complex64>,
512 h: Vec<f64>,
513}
514
515impl Curves {
516 fn new() -> Self {
517 let cp = control_points();
518 let interp = bin_log_freqs()
519 .iter()
520 .map(|&lf| {
521 let i = (cp.partition_point(|&c| c < lf).max(1) - 1).min(CURVE_POINTS - 2);
522 (i, ((lf - cp[i]) / (cp[i + 1] - cp[i])).clamp(0.0, 1.0))
523 })
524 .collect();
525 let (fwd, inv) = plan(MP_N);
526 Curves { interp, fwd, inv }
527 }
528
529 fn fold(n: usize) -> f64 {
531 match n {
532 0 => 1.0,
533 n if n < MP_N / 2 => 2.0,
534 n if n == MP_N / 2 => 1.0,
535 _ => 0.0,
536 }
537 }
538
539 fn design(&self, curve: &[f64]) -> Design {
542 let l: Vec<f64> = self.interp.iter().map(|&(i, t)| curve[i] * (1.0 - t) + curve[i + 1] * t).collect();
543 let mut c: Vec<Complex64> = (0..MP_N).map(|k| Complex64::new(l[k.min(MP_N - k)], 0.0)).collect();
545 self.inv.process(&mut c);
546 for (n, v) in c.iter_mut().enumerate() {
547 *v = Complex64::new(v.re / MP_N as f64 * Self::fold(n), 0.0);
548 }
549 self.fwd.process(&mut c);
550 let e: Vec<Complex64> = c[..MP_BINS].iter().map(|v| v.exp()).collect();
551 let mut z = vec![Complex64::default(); MP_N];
553 z[0] = Complex64::new(e[0].re, 0.0);
554 z[MP_N / 2] = Complex64::new(e[MP_N / 2].re, 0.0);
555 for k in 1..MP_N / 2 {
556 z[k] = e[k];
557 z[MP_N - k] = e[k].conj();
558 }
559 self.inv.process(&mut z);
560 let h = z[..PEDAL_TAPS].iter().map(|v| v.re / MP_N as f64).collect();
561 Design { e, h }
562 }
563
564 fn backward(&self, design: &Design, gh: &[f64]) -> Vec<f64> {
566 let n = MP_N as f64;
567 let mut g = complex(gh, MP_N);
569 self.fwd.process(&mut g);
570 let mut z = vec![Complex64::default(); MP_N];
571 for k in 0..MP_BINS {
572 let ge = if k == 0 || k == MP_N / 2 { Complex64::new(g[k].re / n, 0.0) } else { g[k] * (2.0 / n) };
573 z[k] = ge * design.e[k].conj();
575 }
576 self.inv.process(&mut z);
578 let mut gc: Vec<Complex64> = z.iter().enumerate().map(|(i, v)| Complex64::new(v.re * Self::fold(i), 0.0)).collect();
579 self.fwd.process(&mut gc);
580 let mut out = vec![0.0; CURVE_POINTS];
581 for (k, &(i, t)) in self.interp.iter().enumerate() {
582 let gl = gc[k].re * if k == 0 || k == MP_N / 2 { 1.0 } else { 2.0 } / n;
583 out[i] += gl * (1.0 - t);
584 out[i + 1] += gl * t;
585 }
586 out
587 }
588
589 fn of(&self, h: &[f64]) -> Vec<f64> {
592 let mut s = complex(h, MP_N);
593 self.fwd.process(&mut s);
594 let lf = bin_log_freqs();
595 control_points()
596 .iter()
597 .map(|&c| {
598 let (sum, n) = (0..MP_BINS)
599 .filter(|&k| lf[k] >= c - 0.0625 && lf[k] < c + 0.0625)
600 .fold((0.0, 0), |(a, n), k| (a + (s[k].norm() + 1e-9).ln(), n + 1));
601 sum / n as f64
602 })
603 .collect()
604 }
605}
606
607const RESOLUTIONS: [(usize, usize); 3] = [(4096, 1024), (1024, 256), (256, 64)];
612const TRIM: usize = 2400;
614
615struct Resolution {
616 n: usize,
617 hop: usize,
618 window: Vec<f64>,
619 fwd: Arc<dyn Fft<f64>>,
620 inv: Arc<dyn Fft<f64>>,
621}
622
623impl Resolution {
624 fn new(n: usize, hop: usize) -> Self {
625 let window = (0..n).map(|j| 0.5 - 0.5 * (std::f64::consts::TAU * j as f64 / n as f64).cos()).collect();
626 let (fwd, inv) = plan(n);
627 Resolution { n, hop, window, fwd, inv }
628 }
629
630 fn frames(&self, len: usize) -> usize {
631 1 + len / self.hop
632 }
633
634 fn source(&self, i: usize, len: usize) -> usize {
636 let pad = self.n / 2;
637 if i < pad {
638 pad - i
639 } else if i < pad + len {
640 i - pad
641 } else {
642 2 * (len - 1) + pad - i
643 }
644 }
645
646 fn stft(&self, x: &[f64]) -> Vec<Complex64> {
648 let (len, bins) = (x.len(), self.n / 2 + 1);
649 let mut out = Vec::with_capacity(self.frames(len) * bins);
650 let mut buf = vec![Complex64::default(); self.n];
651 for f in 0..self.frames(len) {
652 for (j, b) in buf.iter_mut().enumerate() {
653 *b = Complex64::new(self.window[j] * x[self.source(f * self.hop + j, len)], 0.0);
654 }
655 self.fwd.process(&mut buf);
656 out.extend_from_slice(&buf[..bins]);
657 }
658 out
659 }
660
661 fn stft_adjoint(&self, gs: &[Complex64], gx: &mut [f64]) {
663 let (len, bins) = (gx.len(), self.n / 2 + 1);
664 let mut buf = vec![Complex64::default(); self.n];
665 for (f, g) in gs.chunks_exact(bins).enumerate() {
666 buf.fill(Complex64::default());
667 buf[..bins].copy_from_slice(g);
668 self.inv.process(&mut buf);
669 for (j, b) in buf.iter().enumerate() {
670 gx[self.source(f * self.hop + j, len)] += self.window[j] * b.re;
671 }
672 }
673 }
674
675 fn magnitude(&self, x: &[f64]) -> Vec<f64> {
678 self.stft(x).iter().map(|c| c.norm()).collect()
679 }
680}
681
682#[derive(Clone, Copy, Debug, PartialEq, Eq)]
684pub enum Weighting {
685 Flat,
688 Octave,
691}
692
693impl Weighting {
694 fn bins(self, n: usize) -> Vec<f64> {
696 let f = |k: usize| k as f64 * SR as f64 / n as f64;
697 let w: Vec<f64> = match self {
698 Weighting::Flat => vec![1.0; n / 2 + 1],
699 Weighting::Octave => (0..=n / 2).map(|k| if f(k) < 20.0 { 0.0 } else { 1.0 / f(k).max(40.0) }).collect(),
700 };
701 let mean = w.iter().sum::<f64>() / w.len() as f64;
702 w.iter().map(|v| v / mean).collect()
703 }
704}
705
706struct SpecLoss {
708 res: Vec<Resolution>,
709 weights: Vec<Vec<f64>>,
711 parts: Vec<(usize, usize, f64, Vec<Vec<f64>>)>,
714}
715
716impl SpecLoss {
717 fn new(target: &[f64], slices: &[(usize, usize)], weighting: Weighting) -> Self {
718 let res: Vec<Resolution> = RESOLUTIONS.iter().map(|&(n, h)| Resolution::new(n, h)).collect();
719 let weights = res.iter().map(|r| weighting.bins(r.n)).collect();
720 let parts = slices
721 .iter()
722 .map(|&(a, b)| {
723 let t = &target[a..b];
724 let eps = 1e-3 * (t.iter().map(|v| v * v).sum::<f64>() / t.len() as f64).sqrt();
725 let logs = res
726 .iter()
727 .map(|r| {
728 let e = eps * (r.n as f64).sqrt();
729 r.magnitude(t).iter().map(|m| (m + e).ln()).collect()
730 })
731 .collect();
732 (a, b, eps, logs)
733 })
734 .collect();
735 SpecLoss { res, weights, parts }
736 }
737
738 fn value_grad(&self, y: &[f64], gy: &mut [f64]) -> f64 {
740 let scale = 1.0 / (self.parts.len() * self.res.len()) as f64;
741 let tasks: Vec<(usize, usize)> = (0..self.parts.len()).flat_map(|p| (0..self.res.len()).map(move |r| (p, r))).collect();
743 let results = par_map(tasks.len(), |i| {
744 let (p, r) = tasks[i];
745 let ((a, b, eps, logs), res) = (&self.parts[p], &self.res[r]);
746 let e = eps * (res.n as f64).sqrt();
747 let mut s = res.stft(&y[*a..*b]);
748 let k = scale / s.len() as f64;
749 let w = &self.weights[r];
750 let mut sum = 0.0;
751 for (i, (c, t)) in s.iter_mut().zip(&logs[r]).enumerate() {
752 let wi = w[i % w.len()];
753 let m = c.norm();
754 let d = (m + e).ln() - t;
755 sum += wi * d.abs();
756 *c = if m > 0.0 { *c * (k * wi * d.signum() / ((m + e) * m)) } else { Complex64::default() };
758 }
759 let mut g = vec![0.0; b - a];
760 res.stft_adjoint(&s, &mut g);
761 (sum / s.len() as f64, g)
762 });
763 let mut tot = 0.0;
764 for ((p, _), (v, g)) in tasks.iter().zip(results) {
765 tot += v;
766 let a = self.parts[*p].0;
767 gy[a..a + g.len()].iter_mut().zip(g).for_each(|(o, v)| *o += v);
768 }
769 tot * scale
770 }
771
772 #[cfg(test)]
773 fn value(&self, y: &[f64]) -> f64 {
775 let mut tot = 0.0;
776 for (a, b, eps, logs) in &self.parts {
777 for ((r, t), w) in self.res.iter().zip(logs).zip(&self.weights) {
778 let e = eps * (r.n as f64).sqrt();
779 let m = r.magnitude(&y[*a..*b]);
780 tot += m.iter().zip(t).enumerate().map(|(i, (m, t))| w[i % w.len()] * ((m + e).ln() - t).abs()).sum::<f64>()
781 / m.len() as f64;
782 }
783 }
784 tot / (self.parts.len() * self.res.len()) as f64
785 }
786}
787
788fn train_slices(segments: &[Segment]) -> Vec<(usize, usize)> {
790 segments.iter().filter(|s| s.train).map(|s| (s.start + TRIM, s.start + s.len - TRIM)).collect()
791}
792
793#[derive(Clone, Debug)]
798pub struct Options {
799 pub steps: usize,
801 pub learning_rate: f64,
804 pub warmup: usize,
805 pub smoothness: f64,
807 pub weighting: Weighting,
809}
810
811impl Default for Options {
812 fn default() -> Self {
813 Options { steps: 300, learning_rate: 0.05, warmup: 10, smoothness: 1e-3, weighting: Weighting::Octave }
814 }
815}
816
817#[derive(Clone, Debug)]
819pub struct Optimised {
820 pub data: Vec<f32>,
822 pub g1_ir: Vec<f64>,
824 pub g2_ir: Vec<f64>,
825 pub g1_curve: Vec<f64>,
827 pub g2_curve: Vec<f64>,
828 pub initial_loss: f64,
830 pub loss: Vec<f64>,
831}
832
833type Pair = [Vec<f64>; 2];
835
836struct Problem {
838 sim: Simulator,
839 loss: SpecLoss,
840 curves: Curves,
841}
842
843struct Eval {
845 value: f64,
846 grads: [Vec<f64>; 2],
847 irs: [Vec<f64>; 2],
848}
849
850impl Problem {
851 fn new(x: &[f64], target: &[f64], slices: &[(usize, usize)], weighting: Weighting) -> Self {
852 Problem { sim: Simulator::new(x), loss: SpecLoss::new(target, slices, weighting), curves: Curves::new() }
853 }
854
855 fn evaluate(&self, c: &[Vec<f64>; 2], smoothness: f64) -> Eval {
857 let d = [self.curves.design(&c[0]), self.curves.design(&c[1])];
858 let run = self.sim.run(&d[0].h, &d[1].h);
859 let mut gy = vec![0.0; run.y.len()];
860 let mut value = self.loss.value_grad(&run.y, &mut gy) + penalty(&run.u);
861 let (gh1, gh2) = self.sim.backward(&run, &d[1].h, &gy);
862 let mut grads = [self.curves.backward(&d[0], &gh1), self.curves.backward(&d[1], &gh2)];
863 for (cv, g) in c.iter().zip(&mut grads) {
864 let n = (cv.len() - 2) as f64;
865 for i in 0..cv.len() - 2 {
866 let dd = cv[i] - 2.0 * cv[i + 1] + cv[i + 2];
867 value += smoothness * dd * dd / n;
868 let k = smoothness * 2.0 * dd / n;
869 g[i] += k;
870 g[i + 1] -= 2.0 * k;
871 g[i + 2] += k;
872 }
873 }
874 let [Design { h: h1, .. }, Design { h: h2, .. }] = d;
875 Eval { value, grads, irs: [h1, h2] }
876 }
877
878 fn fit(&self, mut c: Pair, o: &Options, on_step: &mut dyn FnMut(usize, usize) -> bool) -> (Pair, Pair, Vec<f64>) {
881 let (b1, b2, eps) = (0.9, 0.999, 1e-8);
882 let mut m = [vec![0.0; CURVE_POINTS], vec![0.0; CURVE_POINTS]];
883 let mut v = m.clone();
884 let mut history = Vec::with_capacity(o.steps);
885 let mut best: Option<(f64, Pair, Pair)> = None;
886 for it in 0..o.steps {
887 let lr = o.learning_rate
888 * ((it + 1) as f64 / o.warmup.max(1) as f64).min(1.0)
889 * 0.5
890 * (1.0 + (std::f64::consts::PI * it as f64 / o.steps as f64).cos());
891 let e = self.evaluate(&c, o.smoothness);
892 history.push(e.value);
893 if best.as_ref().is_none_or(|b| e.value < b.0) {
894 best = Some((e.value, c.clone(), e.irs));
895 }
896 let t = (it + 1) as i32;
897 let (bc1, bc2) = (1.0 - f64::powi(b1, t), 1.0 - f64::powi(b2, t));
898 for j in 0..2 {
899 for i in 0..CURVE_POINTS {
900 let g = e.grads[j][i];
901 m[j][i] = b1 * m[j][i] + (1.0 - b1) * g;
902 v[j][i] = b2 * v[j][i] + (1.0 - b2) * g * g;
903 c[j][i] -= lr / bc1 * m[j][i] / ((v[j][i] / bc2).sqrt() + eps);
904 }
905 }
906 if !on_step(it + 1, o.steps) {
907 break;
908 }
909 }
910 let (_, bc, birs) = best.unwrap_or_else(|| {
911 let e = self.evaluate(&c, o.smoothness);
912 (e.value, c.clone(), e.irs)
913 });
914 (bc, birs, history)
915 }
916}
917
918pub fn optimise(model: &mut Model, init: &[f32], options: &Options) -> Optimised {
923 optimise_with_progress(model, init, options, &mut |_, _| true)
924}
925
926pub fn optimise_with_progress(
929 model: &mut Model,
930 init: &[f32],
931 options: &Options,
932 on_step: &mut dyn FnMut(usize, usize) -> bool,
933) -> Optimised {
934 let signal = test_signal();
935 let target = render_target(model, &signal.x);
936 let problem = Problem::new(&signal.x, &target, &train_slices(&signal.segments), options.weighting);
937 let (h1, h2) = super::convert::pedal_irs(init);
938 let c0 = [problem.curves.of(&h1), problem.curves.of(&h2)];
939 let initial_loss = problem.evaluate(&c0, options.smoothness).value;
940 let ([g1_curve, g2_curve], [g1_ir, g2_ir], loss) = problem.fit(c0, options, on_step);
941 Optimised { data: super::convert::upload_data(&g1_ir, &g2_ir), g1_ir, g2_ir, g1_curve, g2_curve, initial_loss, loss }
942}
943
944#[cfg(test)]
945mod tests {
946 use super::*;
947
948 fn golden(name: &str) -> Vec<f64> {
949 let p = format!("{}/tests/data/optim/{name}.bin", env!("CARGO_MANIFEST_DIR"));
950 std::fs::read(&p)
951 .unwrap_or_else(|e| panic!("{p}: {e}"))
952 .as_chunks::<8>()
953 .0
954 .iter()
955 .map(|c| f64::from_le_bytes(*c))
956 .collect()
957 }
958
959 fn manifest() -> serde_json::Value {
960 let p = format!("{}/tests/data/optim/golden.json", env!("CARGO_MANIFEST_DIR"));
961 serde_json::from_str(&std::fs::read_to_string(p).unwrap()).unwrap()
962 }
963
964 fn rel_err(a: &[f64], b: &[f64]) -> f64 {
966 assert_eq!(a.len(), b.len());
967 let scale = b.iter().fold(0f64, |m, v| m.max(v.abs()));
968 a.iter().zip(b).fold(0f64, |m, (x, y)| m.max((x - y).abs())) / scale
969 }
970
971 #[test]
972 fn saturation_matches_reference() {
973 let (u, v) = (golden("f_u"), golden("f_v"));
974 let ours: Vec<f64> = u.iter().map(|&a| saturation(a)).collect();
975 assert!(rel_err(&ours, &v) < 1e-12);
976 }
977
978 #[test]
979 fn simulator_matches_reference() {
980 let x = golden("sim_x");
981 let run = Simulator::new(&x).run(&golden("sim_h1"), &golden("sim_h2"));
982 assert!(rel_err(&run.u, &golden("sim_u")) < 1e-10, "u {}", rel_err(&run.u, &golden("sim_u")));
983 assert!(rel_err(&run.y, &golden("sim_y")) < 1e-10, "y {}", rel_err(&run.y, &golden("sim_y")));
984 }
985
986 #[test]
987 fn loss_matches_reference() {
988 let m = manifest();
989 let slices: Vec<(usize, usize)> = m["sim_segs"]
990 .as_array()
991 .unwrap()
992 .iter()
993 .map(|s| {
994 let (a, l) = (s[0].as_u64().unwrap() as usize, s[1].as_u64().unwrap() as usize);
995 (a + TRIM, a + l - TRIM)
996 })
997 .collect();
998 let loss = SpecLoss::new(&golden("sim_tgt"), &slices, Weighting::Flat);
999 let v = loss.value(&golden("sim_y"));
1000 let want = m["loss"].as_f64().unwrap();
1001 assert!((v - want).abs() < 1e-10 * want, "{v} vs {want}");
1002 }
1003
1004 fn golden_slices() -> Vec<(usize, usize)> {
1005 manifest()["sim_segs"]
1006 .as_array()
1007 .unwrap()
1008 .iter()
1009 .map(|s| {
1010 let (a, l) = (s[0].as_u64().unwrap() as usize, s[1].as_u64().unwrap() as usize);
1011 (a + TRIM, a + l - TRIM)
1012 })
1013 .collect()
1014 }
1015
1016 #[test]
1017 fn curve_of_matches_reference() {
1018 let c = Curves::new();
1019 for (h, want) in [("co_h1", "co_c1"), ("co_h2", "co_c2")] {
1020 let ours = c.of(&golden(h));
1021 assert!(rel_err(&ours, &golden(want)) < 1e-10, "{h}: {}", rel_err(&ours, &golden(want)));
1022 }
1023 }
1024
1025 #[test]
1026 fn min_phase_design_and_gradient_match_reference() {
1027 let c = Curves::new();
1028 let d = c.design(&golden("mp_c"));
1029 assert!(rel_err(&d.h, &golden("mp_h")) < 1e-10, "h {}", rel_err(&d.h, &golden("mp_h")));
1030 let g = c.backward(&d, &golden("mp_gh"));
1031 assert!(rel_err(&g, &golden("mp_gc")) < 1e-9, "grad {}", rel_err(&g, &golden("mp_gc")));
1032 }
1033
1034 #[test]
1035 fn min_phase_gradient_is_numerically_right() {
1036 let c = Curves::new();
1037 let curve: Vec<f64> = (0..CURVE_POINTS).map(|i| (i as f64 * 0.7).sin() - 2.0).collect();
1038 let gh: Vec<f64> = (0..PEDAL_TAPS).map(|k| ((k * 37 % 101) as f64 / 50.0 - 1.0) / (1.0 + k as f64 / 20.0)).collect();
1039 let f = |cv: &[f64]| c.design(cv).h.iter().zip(&gh).map(|(a, b)| a * b).sum::<f64>();
1040 let g = c.backward(&c.design(&curve), &gh);
1041 for i in [0, 1, 7, 20, 33, 40] {
1042 let (mut p, mut m) = (curve.clone(), curve.clone());
1043 p[i] += 1e-6;
1044 m[i] -= 1e-6;
1045 let num = (f(&p) - f(&m)) / 2e-6;
1046 assert!((num - g[i]).abs() < 1e-6 * g.iter().fold(0f64, |a, v| a.max(v.abs())), "point {i}: {num} vs {}", g[i]);
1047 }
1048 }
1049
1050 #[test]
1051 fn simulator_gradients_match_reference() {
1052 let sim = Simulator::new(&golden("sim_x"));
1053 let (h1, h2) = (golden("sim_h1"), golden("sim_h2"));
1054 let run = sim.run(&h1, &h2);
1055 let pen = penalty(&run.u);
1056 let want = manifest()["sim_pen"].as_f64().unwrap();
1057 assert!((pen - want).abs() < 1e-10 * want, "penalty {pen} vs {want}");
1058 let (g1, g2) = sim.backward(&run, &h2, &golden("sim_gy"));
1059 assert!(rel_err(&g1, &golden("sim_gh1")) < 1e-9, "h1 {}", rel_err(&g1, &golden("sim_gh1")));
1060 assert!(rel_err(&g2, &golden("sim_gh2")) < 1e-9, "h2 {}", rel_err(&g2, &golden("sim_gh2")));
1061 }
1062
1063 #[test]
1064 fn simulator_gradients_are_numerically_right() {
1065 let x: Vec<f64> = (0..30000).map(|n| 300.0 * (n as f64 * 0.013).sin() * (n as f64 / 30000.0)).collect();
1067 let h1: Vec<f64> = (0..PEDAL_TAPS).map(|k| 0.5 * (-(k as f64) / 30.0).exp() * (k as f64 * 0.3).cos()).collect();
1068 let h2: Vec<f64> = (0..PEDAL_TAPS).map(|k| (-(k as f64) / 50.0).exp() * (k as f64 * 0.1).sin()).collect();
1069 let gy: Vec<f64> = (0..x.len()).map(|n| (n * 7919 % 1000) as f64 / 500.0 - 1.0).collect();
1070 let sim = Simulator::new(&x);
1071 let obj = |a: &[f64], b: &[f64]| {
1072 let r = sim.run(a, b);
1073 r.y.iter().zip(&gy).map(|(y, g)| y * g).sum::<f64>() + penalty(&r.u)
1074 };
1075 let run = sim.run(&h1, &h2);
1076 assert!(run.u.iter().any(|u| u.abs() > PENALTY_LIMIT));
1077 let (g1, g2) = sim.backward(&run, &h2, &gy);
1078 let d = 1e-7;
1079 for k in [0, 3, 100, 511] {
1080 let (mut p, mut m) = (h1.clone(), h1.clone());
1081 p[k] += d;
1082 m[k] -= d;
1083 let num = (obj(&p, &h2) - obj(&m, &h2)) / (2.0 * d);
1084 assert!((num - g1[k]).abs() < 1e-4 * num.abs().max(1.0), "h1[{k}]: {num} vs {}", g1[k]);
1085 let (mut p, mut m) = (h2.clone(), h2.clone());
1086 p[k] += d;
1087 m[k] -= d;
1088 let num = (obj(&h1, &p) - obj(&h1, &m)) / (2.0 * d);
1089 assert!((num - g2[k]).abs() < 1e-4 * num.abs().max(1.0), "h2[{k}]: {num} vs {}", g2[k]);
1090 }
1091 }
1092
1093 #[test]
1094 fn loss_gradient_matches_reference() {
1095 let loss = SpecLoss::new(&golden("sim_tgt"), &golden_slices(), Weighting::Flat);
1096 let y = golden("sim_y");
1097 let mut gy = vec![0.0; y.len()];
1098 let v = loss.value_grad(&y, &mut gy);
1099 assert!((v - loss.value(&y)).abs() < 1e-12);
1100 assert!(rel_err(&gy, &golden("loss_gy")) < 1e-9, "{}", rel_err(&gy, &golden("loss_gy")));
1101 }
1102
1103 #[test]
1104 fn loss_gradient_is_numerically_right() {
1105 let slices = golden_slices();
1106 let loss = SpecLoss::new(&golden("sim_tgt"), &slices, Weighting::Flat);
1107 let y = golden("sim_y");
1108 let mut gy = vec![0.0; y.len()];
1109 loss.value_grad(&y, &mut gy);
1110 for i in [slices[0].0, slices[0].0 + 1, slices[0].0 + 1000, slices[0].1 - 1, slices[1].0 + 3, 100] {
1112 let d = 1e-6 * y[i].abs().max(1e-3);
1113 let (mut p, mut m) = (y.clone(), y.clone());
1114 p[i] += d;
1115 m[i] -= d;
1116 let num = (loss.value(&p) - loss.value(&m)) / (2.0 * d);
1117 assert!((num - gy[i]).abs() < 1e-4 * num.abs().max(1e-6), "sample {i}: {num} vs {}", gy[i]);
1118 }
1119 }
1120
1121 #[test]
1122 fn octave_weights() {
1123 assert!(Weighting::Flat.bins(256).iter().all(|&w| w == 1.0));
1124 let n = 4096;
1125 let w = Weighting::Octave.bins(n);
1126 let bin = |f: f64| (f * n as f64 / SR as f64).round() as usize;
1127 assert!((w.iter().sum::<f64>() / w.len() as f64 - 1.0).abs() < 1e-12);
1128 assert_eq!((w[0], w[bin(10.0)]), (0.0, 0.0));
1129 assert!((w[bin(25.0)] - w[bin(35.0)]).abs() < 1e-12, "flat below 40 Hz");
1130 let (lo, hi) = (bin(100.0), bin(1000.0));
1131 let ratio = w[lo] / w[hi];
1132 assert!((ratio - hi as f64 / lo as f64).abs() < 1e-12, "1/f: {ratio}");
1133 }
1134
1135 #[test]
1136 fn weighted_loss_gradient_is_numerically_right() {
1137 let slices = golden_slices();
1138 let loss = SpecLoss::new(&golden("sim_tgt"), &slices, Weighting::Octave);
1139 let y = golden("sim_y");
1140 let mut gy = vec![0.0; y.len()];
1141 let v = loss.value_grad(&y, &mut gy);
1142 assert!((v - loss.value(&y)).abs() < 1e-12 * v);
1143 for i in [slices[0].0, slices[0].0 + 1000, slices[0].1 - 1, slices[1].0 + 3] {
1144 let d = 1e-6 * y[i].abs().max(1e-3);
1145 let (mut p, mut m) = (y.clone(), y.clone());
1146 p[i] += d;
1147 m[i] -= d;
1148 let num = (loss.value(&p) - loss.value(&m)) / (2.0 * d);
1149 assert!((num - gy[i]).abs() < 1e-4 * num.abs().max(1e-6), "sample {i}: {num} vs {}", gy[i]);
1150 }
1151 }
1152
1153 #[test]
1154 fn optimiser_steps_match_reference() {
1155 let problem = Problem::new(&golden("sim_x"), &golden("sim_tgt"), &golden_slices(), Weighting::Flat);
1156 let c0 = golden("opt_c0");
1157 let want = golden("opt_loss");
1158 let o = Options { steps: want.len(), weighting: Weighting::Flat, ..Options::default() };
1159 let c = [c0[..CURVE_POINTS].to_vec(), c0[CURVE_POINTS..].to_vec()];
1160 let (_, _, history) = problem.fit(c, &o, &mut |_, _| true);
1162 assert!(rel_err(&history, &want) < 1e-9, "{history:?} vs {want:?}");
1163 }
1164
1165 #[test]
1166 fn optimiser_reports_progress_and_stops_early() {
1167 let problem = Problem::new(&golden("sim_x"), &golden("sim_tgt"), &golden_slices(), Weighting::Flat);
1168 let c0 = golden("opt_c0");
1169 let c = [c0[..CURVE_POINTS].to_vec(), c0[CURVE_POINTS..].to_vec()];
1170 let mut seen = vec![];
1171 let (_, irs, history) = problem.fit(c, &Options { steps: 8, ..Options::default() }, &mut |d, t| {
1172 seen.push((d, t));
1173 d < 3
1174 });
1175 assert_eq!((seen, history.len(), irs[0].len()), (vec![(1, 8), (2, 8), (3, 8)], 3, PEDAL_TAPS));
1176 }
1177
1178 #[test]
1179 fn target_matches_reference() {
1180 let mut model = Model::load(format!("{}/tests/data/nam/wavenet.nam", env!("CARGO_MANIFEST_DIR"))).unwrap();
1181 let y = render_target(&mut model, &golden("tgt_x"));
1182 assert!(rel_err(&y, &golden("tgt_y")) < 1e-5, "{}", rel_err(&y, &golden("tgt_y")));
1183 }
1184
1185 #[test]
1186 fn test_signal_matches_reference() {
1187 let m = manifest();
1188 let s = test_signal();
1189 let segs = m["sig_segs"].as_array().unwrap();
1190 for (ours, theirs) in s.segments.iter().zip(segs) {
1191 assert_eq!(ours.name, theirs["name"].as_str().unwrap());
1192 assert_eq!((ours.start, ours.len), (theirs["start"].as_u64().unwrap() as usize, theirs["len"].as_u64().unwrap() as usize));
1193 }
1194 let re_amp = m["sig_len"].as_u64().unwrap() as usize;
1196 assert_eq!(s.x.len(), re_amp + 3 * (SR / 2 + 3 * SR) + SR / 2);
1197 let (idx, v) = (golden("sig_idx"), golden("sig_v"));
1198 for (&i, &want) in idx.iter().zip(&v) {
1199 assert!((s.x[i as usize] - want).abs() < 1e-6, "sample {i}: {} vs {want}", s.x[i as usize]);
1200 }
1201 assert_eq!(s.segments.iter().filter(|s| s.train).count(), 22);
1202 let p = s.segments.iter().find(|s| s.name == "synth pink -12").unwrap();
1204 let rms = (s.x[p.start..p.start + p.len].iter().map(|v| v * v).sum::<f64>() / p.len as f64).sqrt();
1205 assert!((rms - 0.5 * 10f64.powf(-6.0 / 20.0)).abs() < 1e-12);
1206 }
1207}