1use rustfft::FftPlanner;
25use rustfft::num_complex::Complex64;
26
27use super::Model;
28
29pub const N: usize = 8192;
31const HALF: usize = N / 2;
32const TONE_LEN: usize = 10240;
34const TONE_SETTLE: usize = 2048;
35
36pub const TONE_BINS: [usize; 94] = [
38 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 23, 24, 25, 27, 28, 30, 32, 34, 36, 38, 41, 43, 46, 48, 51, 54,
39 58, 61, 65, 69, 73, 77, 82, 86, 92, 97, 103, 109, 116, 123, 130, 138, 146, 155, 164, 174, 184, 195, 207, 219, 232,
40 246, 260, 276, 292, 310, 328, 348, 369, 391, 414, 439, 465, 492, 522, 553, 586, 621, 658, 697, 738, 782, 829, 878,
41 931, 986, 1045, 1107, 1173, 1243, 1317, 1395, 1478, 1566, 1660, 1758, 1863, 1974, 2092, 2216, 2348, 2488,
42];
43pub const PEDAL_TAPS: usize = 512;
48const THIRD_TONES: usize = 83;
50
51#[derive(Clone, Debug)]
53pub struct Converted {
54 pub data: Vec<f32>,
56 pub factor: f32,
58 pub levels: Vec<f64>,
60 pub levels3: Vec<f64>,
62 pub bla_db: Vec<f64>,
64 pub g1_gains_db: Vec<f64>,
66 pub g1_ir: Vec<f64>,
69 pub g2_ir: Vec<f64>,
70}
71
72pub fn upload_data(g1_ir: &[f64], g2_ir: &[f64]) -> Vec<f32> {
75 let block = |ir: &[f64]| {
76 let mut b = vec![0.0f32; 1024];
77 for (k, &v) in ir.iter().take(PEDAL_TAPS).enumerate() {
78 b[1023 - k] = v as f32;
79 }
80 b
81 };
82 [block(g1_ir), block(g2_ir)].concat()
83}
84
85pub fn pedal_irs(data: &[f32]) -> (Vec<f64>, Vec<f64>) {
88 assert_eq!(data.len(), 2048, "upload data is 2048 floats");
89 let ir = |b: &[f32]| (0..PEDAL_TAPS).map(|k| b[1023 - k] as f64).collect();
90 (ir(&data[..1024]), ir(&data[1024..]))
91}
92
93#[derive(Default)]
95struct DcBlocker {
96 x1: f32,
97 x2: f32,
98 y1: f32,
99 y2: f32,
100}
101
102impl DcBlocker {
103 fn run(&mut self, x: &mut [f32]) {
104 const B0: f32 = 0.995_382_7;
105 const B1: f32 = -1.990_765;
106 const B2: f32 = 0.995_382_7;
107 const A1: f32 = -1.990_744;
108 const A2: f32 = 0.990_786_7;
109 for v in x {
110 let x0 = *v;
111 let y0 = (self.x1 * B1 + x0 * B0 + self.x2 * B2) - (self.y2 * A2 + self.y1 * A1);
112 *v = y0;
113 (self.x2, self.x1, self.y2, self.y1) = (self.x1, x0, self.y1, y0);
114 }
115 }
116}
117
118struct Fft {
119 fwd: std::sync::Arc<dyn rustfft::Fft<f64>>,
120 inv: std::sync::Arc<dyn rustfft::Fft<f64>>,
121}
122
123impl Fft {
124 fn new() -> Self {
125 let mut p = FftPlanner::new();
126 Fft { fwd: p.plan_fft_forward(N), inv: p.plan_fft_inverse(N) }
127 }
128
129 fn power(&self, x: &[f32]) -> Vec<f64> {
131 let mut buf: Vec<Complex64> = x.iter().map(|&v| Complex64::new(v as f64, 0.0)).collect();
132 buf.resize(N, Complex64::default());
133 self.fwd.process(&mut buf);
134 buf[..HALF].iter().map(|c| c.norm_sqr()).collect()
135 }
136
137 fn min_phase(&self, db: &[f64]) -> Vec<f64> {
139 let m: Vec<f64> = (0..N).map(|i| 10f64.powf(db[if i < HALF { i } else { N - 1 - i }] / 20.0)).collect();
141 let mut c: Vec<Complex64> =
142 m.iter().map(|&v| Complex64::new(if v == 0.0 { -30.0 } else { v.ln() }, 0.0)).collect();
143 self.fwd.process(&mut c);
145 for (i, v) in c.iter_mut().enumerate() {
146 *v *= match i {
147 0 => 1.0,
148 i if i == HALF => 1.0,
149 i if i < HALF => 2.0,
150 _ => 0.0,
151 } / N as f64;
152 }
153 self.inv.process(&mut c);
154 let mut spec: Vec<Complex64> = m.iter().zip(&c).map(|(&mag, a)| Complex64::from_polar(mag, -a.im)).collect();
155 self.inv.process(&mut spec);
156 spec.iter().map(|v| v.re / N as f64).collect()
157 }
158}
159
160fn smooth(h: &[f64]) -> Vec<f64> {
162 let r = 1.014_545_3_f32; let mut out = vec![0.0; HALF];
164 for (k, o) in out.iter_mut().enumerate().skip(2) {
165 let lo = ((k as f32 / r).floor() as usize).max(2);
166 let hi = ((k as f32 * r).ceil() as usize).clamp(2, HALF - 1).max(lo);
167 let m = k.max(lo);
168 let (lm, llo, lhi) = ((m as f64).log10(), ((lo - 1) as f64).log10(), ((hi + 1) as f64).log10());
169 let (mut sw, mut sp) = (0.0, 0.0);
170 for (j, hj) in h.iter().enumerate().take(hi + 1).skip(lo) {
171 let lj = (j as f64).log10();
172 let w = if j > m {
173 if lhi != lm { (lhi - lj) / (lhi - lm) } else { 1.0 }
174 } else {
175 (lj - llo) / (lm - llo)
176 };
177 sw += w;
178 sp += w * hj * hj;
179 }
180 *o = sp / sw;
181 }
182 out[0] = out[2];
183 out[1] = out[2];
184 out.iter().map(|p| p.sqrt()).collect()
185}
186
187fn interp_linear(values: &[f64], bins: &[usize], below: f64) -> Vec<f64> {
189 let mut out = vec![below; HALF];
190 for i in 0..bins.len() - 1 {
191 for (k, o) in out.iter_mut().enumerate().take(bins[i + 1]).skip(bins[i]) {
192 let t = (k - bins[i]) as f64 / (bins[i + 1] - bins[i]) as f64;
193 *o = t * values[i + 1] + (1.0 - t) * values[i];
194 }
195 }
196 let last = *bins.last().expect("bins");
197 out[last..].fill(*values.last().expect("values"));
198 out
199}
200
201fn interp_log(values: &[f64], bins: &[usize]) -> Vec<f64> {
203 let mut out = vec![values[0]; HALF];
204 for i in 0..bins.len() - 1 {
205 let (l0, l1) = ((bins[i] as f64).log10(), (bins[i + 1] as f64).log10());
206 for (k, o) in out.iter_mut().enumerate().take(bins[i + 1]).skip(bins[i]) {
207 let t = ((k as f64).log10() - l0) / (l1 - l0);
208 *o = t * values[i + 1] + (1.0 - t) * values[i];
209 }
210 }
211 let last = *bins.last().expect("bins");
212 out[last..].fill(*values.last().expect("values"));
213 out
214}
215
216const OFFICIAL_PREFIX: [u8; 8] = [0xcf, 0xc0, 0x91, 0x37, 0x11, 0x79, 0x17, 0x37];
220const OFFICIAL_FNV1A64: u64 = 0x4aa0_4ced_5cff_86d2;
221
222fn fnv1a64(b: &[u8]) -> u64 {
223 b.iter().fold(0xcbf2_9ce4_8422_2325, |h, &c| (h ^ c as u64).wrapping_mul(0x0100_0000_01b3))
224}
225
226fn find_official(bytes: &[u8]) -> Option<Vec<f32>> {
228 let len = N * 4;
229 let mut i = 0;
230 while i + len <= bytes.len() {
231 let at = i + bytes[i..bytes.len() - len + OFFICIAL_PREFIX.len()].windows(8).position(|w| w == OFFICIAL_PREFIX)?;
232 let table = &bytes[at..at + len];
233 if fnv1a64(table) == OFFICIAL_FNV1A64 {
234 return Some(table.as_chunks::<4>().0.iter().map(|c| f32::from_le_bytes(*c)).collect());
235 }
236 i = at + 1;
237 }
238 None
239}
240
241pub fn official_excitation(path: impl AsRef<std::path::Path>) -> Result<Vec<f32>, super::NamError> {
249 fn search(p: &std::path::Path, depth: usize) -> Option<Vec<f32>> {
250 let meta = std::fs::symlink_metadata(p).ok()?;
251 if meta.is_file() {
252 return (meta.len() < 200 << 20).then(|| std::fs::read(p).ok()).flatten().and_then(|b| find_official(&b));
254 }
255 if meta.is_dir() && depth < 12 {
256 let mut entries: Vec<_> = std::fs::read_dir(p).ok()?.filter_map(|e| e.ok().map(|e| e.path())).collect();
257 entries.sort_by_key(|e| {
259 let n = e.to_string_lossy().to_lowercase();
260 !(n.contains("imagelib") || n.contains("frameworks") || n.ends_with(".dll"))
261 });
262 return entries.iter().find_map(|e| search(e, depth + 1));
263 }
264 None
265 }
266 let p = path.as_ref();
267 search(p, 0).ok_or_else(|| {
268 super::NamError::Format(format!("no Stomp Manager BLA excitation found in {}", p.display()))
269 })
270}
271
272pub fn official_app_locations() -> Vec<std::path::PathBuf> {
274 let mut v = vec![std::path::PathBuf::from("/Applications/Stomp Manager.app")];
275 if let Some(home) = std::env::var_os("HOME") {
276 v.push(std::path::Path::new(&home).join("Applications/Stomp Manager.app"));
277 }
278 for var in ["ProgramFiles", "ProgramFiles(x86)", "LOCALAPPDATA"] {
279 if let Some(dir) = std::env::var_os(var) {
280 v.push(std::path::Path::new(&dir).join("Stomp Manager"));
281 }
282 }
283 v
284}
285
286pub fn find_official_excitation() -> Option<Vec<f32>> {
288 official_app_locations().iter().filter(|p| p.exists()).find_map(|p| official_excitation(p).ok())
289}
290
291pub fn excitation() -> Vec<f32> {
294 let mut seed: u64 = 0x4f50_454e_5354_4f4d; let mut next = || {
296 seed ^= seed >> 12;
298 seed ^= seed << 25;
299 seed ^= seed >> 27;
300 (seed.wrapping_mul(0x2545_f491_4f6c_dd1d) >> 11) as f64 / (1u64 << 53) as f64
301 };
302 let mut spec = vec![Complex64::default(); N];
303 for (k, s) in spec.iter_mut().enumerate().take(HALF).skip(1) {
304 let f = k as f64 * 48000.0 / N as f64;
305 let a = (1.0 / f.max(40.0)).sqrt();
306 *s = Complex64::from_polar(N as f64 / 2.0 * a, next() * std::f64::consts::TAU - std::f64::consts::FRAC_PI_2);
308 }
309 spec[0] = Complex64::new(N as f64 / 2.0 * (1.0f64 / 40.0).sqrt(), 0.0);
311 for k in 1..HALF {
312 spec[N - k] = spec[k].conj();
313 }
314 FftPlanner::new().plan_fft_inverse(N).process(&mut spec);
315 spec.iter().map(|c| (c.re / N as f64 * 1e-5) as f32).collect()
316}
317
318#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
320pub enum Measure {
321 App,
325 Clean,
329 #[default]
334 Optimised,
335}
336
337pub fn convert(model: &mut Model) -> Converted {
341 convert_with(model, &excitation())
342}
343
344pub fn convert_with(model: &mut Model, bla_x: &[f32]) -> Converted {
350 convert_measured(model, bla_x, Measure::App)
351}
352
353pub fn convert_optimised(model: &mut Model, bla_x: &[f32], on_step: &mut dyn FnMut(usize, usize) -> bool) -> Converted {
357 let mut c = convert_measured(model, bla_x, Measure::Clean);
358 let o = super::optimise::optimise_with_progress(model, &c.data, &super::optimise::Options::default(), on_step);
359 (c.data, c.g1_ir, c.g2_ir) = (o.data, o.g1_ir, o.g2_ir);
360 c
361}
362
363pub const NORMALISED_LEVEL_DB: f64 = -32.5;
368
369pub fn pedal_level_db(model: &mut Model) -> f64 {
372 let sig = super::optimise::test_signal();
373 let seg = sig.segments.iter().find(|s| s.name == "pink -18").expect("the test signal has its pink segment");
374 let y = super::optimise::render_target(model, &sig.x[seg.start..seg.start + seg.len]);
375 let y = &y[4800..];
377 10.0 * (y.iter().map(|v| v * v).sum::<f64>() / y.len() as f64).log10()
378}
379
380pub fn normalise_level(c: &mut Converted, model: &mut Model) -> f64 {
385 let gain_db = NORMALISED_LEVEL_DB - pedal_level_db(model);
386 let g = 10f64.powf(gain_db / 20.0);
387 for v in &mut c.data[1024..] {
388 *v = (*v as f64 * g) as f32;
389 }
390 for v in &mut c.g2_ir {
391 *v *= g;
392 }
393 gain_db
394}
395
396pub fn convert_measured(model: &mut Model, bla_x: &[f32], measure: Measure) -> Converted {
398 if measure == Measure::Optimised {
399 return convert_optimised(model, bla_x, &mut |_, _| true);
400 }
401 assert_eq!(bla_x.len(), N, "the excitation is {N} samples");
402 let fft = Fft::new();
403 let clean_h = (measure == Measure::Clean).then(|| {
404 model.reset();
405 let x2 = [bla_x, bla_x].concat();
406 let mut y2 = vec![0.0f32; 2 * N];
407 model.process(&x2, &mut y2);
408 DcBlocker::default().run(&mut y2);
409 let (py, px) = (fft.power(&y2[N..]), fft.power(bla_x));
410 py.iter().zip(&px).map(|(a, b)| a.sqrt() / b.sqrt()).collect::<Vec<f64>>()
411 });
412 model.reset();
413 let mut dc = DcBlocker::default();
414
415 let mut y = vec![0.0f32; N];
418 model.process(bla_x, &mut y);
419 dc.run(&mut y);
420 let h: Vec<f64> = match clean_h {
421 Some(h) => h,
422 None => {
423 let (py, px) = (fft.power(&y), fft.power(bla_x));
424 py.iter().zip(&px).map(|(a, b)| a.sqrt() / b.sqrt()).collect()
425 }
426 };
427 let h_db: Vec<f64> = h.iter().map(|v| 20.0 * v.log10()).collect();
428 let hs = smooth(&h);
429
430 let (mut levels, mut levels3) = (Vec::with_capacity(94), Vec::with_capacity(THIRD_TONES));
432 let mut x = vec![0.0f32; TONE_LEN];
433 let mut out = vec![0.0f32; TONE_LEN];
434 for (i, &b) in TONE_BINS.iter().enumerate() {
435 let w = b as f32 * 6.283_185_5 * (1.0 / N as f32);
436 for (n, v) in x.iter_mut().enumerate() {
437 *v = (n as f32 * w).sin();
438 }
439 model.process(&x, &mut out);
440 dc.run(&mut out);
441 let p = fft.power(&out[TONE_SETTLE..]);
442 levels.push(p[b].sqrt() * std::f64::consts::FRAC_PI_4 / 4096.0);
443 if i < THIRD_TONES {
444 levels3.push(p[3 * b].sqrt() * 3.0 * std::f64::consts::FRAC_PI_4 / 4096.0);
445 }
446 }
447
448 let bins3: Vec<usize> = TONE_BINS[..THIRD_TONES].iter().map(|b| 3 * b).collect();
450 let f = interp_linear(&levels, &TONE_BINS, levels[0]);
451 let t = interp_linear(&levels3, &bins3, 0.0);
452 let g1_gains_db: Vec<f64> = TONE_BINS.iter().map(|&b| 20.0 * (hs[b] / f[b].max(t[b])).log10()).collect();
453
454 let g1_db = interp_log(&g1_gains_db, &TONE_BINS);
456 let g2_db: Vec<f64> = h_db.iter().zip(&g1_db).map(|(a, b)| a - b).collect();
457 let (g1_ir, g2_ir) = (fft.min_phase(&g1_db), fft.min_phase(&g2_db));
458 let data = match measure {
459 Measure::App => g1_ir[..1024].iter().rev().chain(g2_ir[..1024].iter().rev()).map(|&v| v as f32).collect(),
461 Measure::Clean | Measure::Optimised => upload_data(&g1_ir, &g2_ir),
462 };
463 Converted { data, factor: 1.0, levels, levels3, bla_db: h_db, g1_gains_db, g1_ir, g2_ir }
464}
465
466#[cfg(test)]
467mod tests {
468 use super::*;
469
470 #[test]
471 fn min_phase_reproduces_magnitude() {
472 let fft = Fft::new();
473 let db: Vec<f64> = (0..HALF).map(|k| 6.0 - 12.0 * k as f64 / HALF as f64).collect();
475 let ir = fft.min_phase(&db);
476 let mut c: Vec<Complex64> = ir.iter().map(|&v| Complex64::new(v, 0.0)).collect();
477 fft.fwd.process(&mut c);
478 for k in [1, 10, 100, 1000, 4000] {
479 assert!((20.0 * c[k].norm().log10() - db[k]).abs() < 0.01, "bin {k}");
480 }
481 let e: f64 = ir.iter().map(|v| v * v).sum();
483 assert!(ir[..64].iter().map(|v| v * v).sum::<f64>() > 0.99 * e);
484 }
485
486 #[test]
487 fn flat_curve_is_an_impulse() {
488 let ir = Fft::new().min_phase(&vec![0.0; HALF]);
489 assert!((ir[0] - 1.0).abs() < 1e-9 && ir[1..].iter().all(|v| v.abs() < 1e-9));
490 }
491
492 #[test]
493 fn smoothing_keeps_a_constant() {
494 let s = smooth(&vec![3.0; HALF]);
495 assert!(s.iter().all(|v| (v - 3.0).abs() < 1e-9));
496 }
497
498 #[test]
499 fn interpolation() {
500 let bins = [10, 20, 40];
501 let l = interp_linear(&[1.0, 2.0, 4.0], &bins, 0.5);
502 assert_eq!((l[0], l[10], l[15], l[20], l[30], l[4000]), (0.5, 1.0, 1.5, 2.0, 3.0, 4.0));
503 let g = interp_log(&[0.0, 6.0, 12.0], &bins);
504 assert!((g[14] - 6.0 * (14f64 / 10.0).log10() / 2f64.log10()).abs() < 1e-12);
505 assert_eq!((g[0], g[4095]), (0.0, 12.0));
506 }
507
508 #[test]
509 fn upload_layout() {
510 let g1: Vec<f64> = (0..2000).map(|k| k as f64 + 1.0).collect();
511 let g2: Vec<f64> = (0..2000).map(|k| -(k as f64) - 1.0).collect();
512 let d = upload_data(&g1, &g2);
513 assert_eq!(d.len(), 2048);
514 assert!(d[..512].iter().chain(&d[1024..1536]).all(|&v| v == 0.0));
515 assert_eq!((d[1023], d[512], d[2047], d[1536]), (1.0, 512.0, -1.0, -512.0));
516 let (h1, h2) = pedal_irs(&d);
517 assert_eq!((h1.len(), h2.len()), (PEDAL_TAPS, PEDAL_TAPS));
518 assert!(h1.iter().zip(&g1).chain(h2.iter().zip(&g2)).all(|(a, b)| a == b));
519 }
520
521 #[test]
522 fn official_fingerprint_search() {
523 let mut fake = vec![0u8; 100];
525 fake.extend_from_slice(&OFFICIAL_PREFIX);
526 fake.resize(100 + N * 4 + 50, 7);
527 assert!(find_official(&fake).is_none());
528 assert!(find_official(&[1, 2, 3]).is_none());
529 assert_eq!(fnv1a64(b""), 0xcbf2_9ce4_8422_2325);
530 }
531
532 #[test]
533 fn excitation_spectrum() {
534 let x = excitation();
535 let fft = Fft::new();
536 let p = fft.power(&x);
537 for k in [10usize, 100, 1000, 4000] {
538 let want = N as f64 / 2.0 * (1.0 / (k as f64 * 48000.0 / N as f64)).sqrt() * 1e-5;
539 assert!((p[k].sqrt() / want - 1.0).abs() < 1e-3, "bin {k}");
540 }
541 let peak = x.iter().fold(0f32, |m, v| m.max(v.abs()));
542 assert!(peak > 1e-5 && peak < 1e-4, "about -90 dBFS: {peak}");
543 }
544}