/ proof-of-concept / midiboot
Sign in
proof-of-concept / midiboot
Test the percussion drivers' geometric pitch scaling
Renders the woodblock an octave apart and checks the dominant spectral peak (Goertzel past the attack) doubles; reference pitches move the existing woodblock checks to MIDI 76.
Jeremy Magland <jmagland@flatironinstitute.org> committed commit 1f214a61feea parent fa5c12e Browse files
1 changed file+37−3
scripts/test-sim.tsmodified+37−3View file
@@ -35,6 +35,31 @@ function finite(x: Float32Array): boolean {
3535 return true;
3636 }
3737
38+/** Goertzel magnitude at one frequency over a window into the ring. */
39+function goertzel(y: Float32Array, from: number, len: number, f: number, sr: number): number {
40+ const w = (2 * Math.PI * f) / sr;
41+ const c = 2 * Math.cos(w);
42+ let s1 = 0, s2 = 0;
43+ for (let i = from; i < from + len && i < y.length; i++) {
44+ const s0 = y[i] + c * s1 - s2;
45+ s2 = s1;
46+ s1 = s0;
47+ }
48+ return Math.sqrt(s1 * s1 + s2 * s2 - c * s1 * s2);
49+}
50+
51+/** The strongest spectral peak in [lo, hi], 2 Hz grid, past the attack. */
52+function peakFreq(y: Float32Array, sr: number, lo: number, hi: number): number {
53+ const from = Math.floor(y.length * 0.3);
54+ const len = Math.min(8192, y.length - from);
55+ let best = lo, bm = -1;
56+ for (let f = lo; f <= hi; f += 2) {
57+ const m = goertzel(y, from, len, f, sr);
58+ if (m > bm) { bm = m; best = f; }
59+ }
60+ return best;
61+}
62+
3863 if (!existsSync(ROOT)) {
3964 console.log(`skipped: ${ROOT} not found`);
4065 process.exit(0);
@@ -46,20 +71,29 @@ const SR = 44100;
4671 const b = loadBundle('woodblock', 'woodblock');
4772 const t0 = performance.now();
4873 const [soft, hard] = renderNoteBatch(b, [
49- { pitch: 60, velocity: 0.25, durationSec: 0.25 },
50- { pitch: 60, velocity: 1.0, durationSec: 0.25 },
74+ { pitch: 76, velocity: 0.25, durationSec: 0.25 },
75+ { pitch: 76, velocity: 1.0, durationSec: 0.25 },
5176 ], { sampleRate: SR, quality: 'preview', settings: {} });
5277 console.log(`woodblock: two strikes in ${((performance.now() - t0) / 1000).toFixed(1)} s`);
5378 check(soft.length > SR * 0.2, `woodblock trace has audio length (${soft.length})`);
5479 check(finite(soft) && finite(hard), 'woodblock samples finite');
5580 check(peak(soft) > 0, 'woodblock soft strike sounds');
5681 check(peak(hard) > peak(soft) * 1.5, `harder mallet is louder (${peak(hard).toExponential(2)} vs ${peak(soft).toExponential(2)})`);
57- const [moved] = renderNoteBatch(b, [{ pitch: 60, velocity: 1.0, durationSec: 0.25 }], {
82+ const [moved] = renderNoteBatch(b, [{ pitch: 76, velocity: 1.0, durationSec: 0.25 }], {
5883 sampleRate: SR, quality: 'preview', settings: { strike_position: 0.25 },
5984 });
6085 let differs = moved.length !== hard.length;
6186 for (let i = 0; i < Math.min(moved.length, hard.length) && !differs; i++) if (moved[i] !== hard[i]) differs = true;
6287 check(differs, 'strike position knob changes the sound');
88+ // Pitch by geometric scaling: an octave down halves the dominant mode.
89+ const [lo, hi] = renderNoteBatch(b, [
90+ { pitch: 64, velocity: 0.8, durationSec: 0.25 },
91+ { pitch: 76, velocity: 0.8, durationSec: 0.25 },
92+ ], { sampleRate: SR, quality: 'preview', settings: {} });
93+ const fLo = peakFreq(lo, SR, 300, 4000);
94+ const fHi = peakFreq(hi, SR, 600, 8000);
95+ const ratio = fHi / fLo;
96+ check(ratio > 1.94 && ratio < 2.06, `octave apart in pitch is an octave apart in frequency (${fLo} Hz -> ${fHi} Hz)`);
6397 }
6498
6599 {
↑↓ move↵openescclose