summaryrefslogtreecommitdiff
path: root/tools/mq_editor/index.html
diff options
context:
space:
mode:
authorskal <pascal.massimino@gmail.com>2026-02-18 06:02:32 +0100
committerskal <pascal.massimino@gmail.com>2026-02-18 06:02:32 +0100
commitbf3929220be7eddf32cebe12573b870fc9b54997 (patch)
tree27318902f41b0e4f5deb73831d9cfc03ca37b229 /tools/mq_editor/index.html
parent14c3bfe09f906e9b80d6100b126e59c9a88ac976 (diff)
fix(mq_editor): mini-spectrum and spectrogram display improvements
- Fix mini-spectrum: log-scale frequency axis, per-pixel bin averaging, red peak bars after extraction - Fix key handlers swallowing digits in input fields - Clamp hop size to min 64 to prevent hang - Store maxDB in STFTCache, use it to normalize both mini-spectrum and main spectrogram (fixes clipping at 0dB for pure tones) - Add Test WAV console validation for 440/660Hz peaks - Grayscale colormap for main spectrogram handoff(Gemini): mq_editor display fixes complete, tests not affected
Diffstat (limited to 'tools/mq_editor/index.html')
-rw-r--r--tools/mq_editor/index.html74
1 files changed, 72 insertions, 2 deletions
diff --git a/tools/mq_editor/index.html b/tools/mq_editor/index.html
index 1ebc0d3..22f8ff9 100644
--- a/tools/mq_editor/index.html
+++ b/tools/mq_editor/index.html
@@ -153,9 +153,10 @@
setTimeout(() => {
const signal = audioBuffer.getChannelData(0);
- stftCache = new STFTCache(signal, audioBuffer.sampleRate, fftSize, parseInt(hopSize.value));
+ stftCache = new STFTCache(signal, audioBuffer.sampleRate, fftSize, Math.max(64, parseInt(hopSize.value) || 64));
setStatus(`${label} — ${audioBuffer.duration.toFixed(2)}s, ${audioBuffer.sampleRate}Hz, ${audioBuffer.numberOfChannels}ch (${stftCache.getNumFrames()} frames cached)`, 'info');
viewer = new SpectrogramViewer(canvas, audioBuffer, stftCache);
+ if (label.startsWith('Test WAV')) validateTestWAVPeaks(stftCache);
}, 10);
}
@@ -196,10 +197,12 @@
// Update cache when hop size changes
hopSize.addEventListener('change', () => {
+ const val = Math.max(64, parseInt(hopSize.value) || 64);
+ hopSize.value = val;
if (stftCache) {
setStatus('Updating STFT cache...', 'info');
setTimeout(() => {
- stftCache.setHopSize(parseInt(hopSize.value));
+ stftCache.setHopSize(val);
setStatus(`Cache updated (${stftCache.getNumFrames()} frames)`, 'info');
if (viewer) viewer.render();
}, 10);
@@ -375,6 +378,7 @@
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
+ if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
if (e.code === 'Digit1') {
e.preventDefault();
playSynthesized();
@@ -394,6 +398,72 @@
}
}
});
+
+ // --- Test WAV peak validation ---
+ function validateTestWAVPeaks(cache) {
+ const SR = cache.sampleRate;
+ const N = cache.fftSize;
+ const binWidth = SR / N; // Hz per bin
+ const numBins = N / 2;
+ const numBars = 100; // mini-spectrum bar count
+
+ // Use a mid-signal frame (avoid edge effects)
+ const midFrame = cache.frames[Math.floor(cache.frames.length / 2)];
+ if (!midFrame) { console.error('[TestWAV] No frames computed'); return; }
+ const sq = midFrame.squaredAmplitude;
+ const t = midFrame.time;
+
+ console.group('[TestWAV] Peak validation @ t=' + t.toFixed(3) + 's');
+
+ // Top 5 bins by magnitude
+ const ranked = Array.from(sq)
+ .map((v, i) => ({ bin: i, freq: i * binWidth, db: 10 * Math.log10(Math.max(v, 1e-20)) }))
+ .sort((a, b) => b.db - a.db);
+ console.log('Top 5 FFT bins:');
+ ranked.slice(0, 5).forEach(x =>
+ console.log(` bin ${x.bin.toString().padStart(3)}: ${x.freq.toFixed(1).padStart(7)}Hz ${x.db.toFixed(1)}dB`));
+
+ // Expected bins for 440/660 Hz
+ const bin440 = Math.round(440 / binWidth);
+ const bin660 = Math.round(660 / binWidth);
+ const db440 = 10 * Math.log10(Math.max(sq[bin440], 1e-20));
+ const db660 = 10 * Math.log10(Math.max(sq[bin660], 1e-20));
+ console.log(`440Hz → bin ${bin440} (${(bin440 * binWidth).toFixed(1)}Hz): ${db440.toFixed(1)}dB`);
+ console.log(`660Hz → bin ${bin660} (${(bin660 * binWidth).toFixed(1)}Hz): ${db660.toFixed(1)}dB`);
+
+ // Validate: 440/660 Hz must be in top-10
+ const top10Freqs = ranked.slice(0, 10).map(x => x.freq);
+ const pass440 = top10Freqs.some(f => Math.abs(f - 440) < binWidth * 2);
+ const pass660 = top10Freqs.some(f => Math.abs(f - 660) < binWidth * 2);
+ console.log('Peak check: 440Hz ' + (pass440 ? 'PASS ✓' : 'FAIL ✗') +
+ ', 660Hz ' + (pass660 ? 'PASS ✓' : 'FAIL ✗'));
+
+ // Mini-spectrum: which bar do these peaks land in?
+ const bar440 = Math.floor(bin440 * numBars / numBins);
+ const bar660 = Math.floor(bin660 * numBars / numBins);
+ const sampledBin440 = Math.floor(bar440 * numBins / numBars);
+ const sampledBin660 = Math.floor(bar660 * numBars / numBars);
+ console.log('Mini-spectrum (linear scale, 100 bars):');
+ console.log(` 440Hz (bin ${bin440}) → bar ${bar440}/100 [bar samples bin ${sampledBin440} = ${(sampledBin440 * binWidth).toFixed(1)}Hz]`);
+ console.log(` 660Hz (bin ${bin660}) → bar ${bar660}/100 [bar samples bin ${Math.floor(bar660 * numBins / numBars)} = ${(Math.floor(bar660 * numBins / numBars) * binWidth).toFixed(1)}Hz]`);
+ if (bar440 < 5 || bar660 < 5) {
+ console.warn(' ⚠ BUG: peaks fall in bars ' + bar440 + ' and ' + bar660 +
+ ' (leftmost ~' + Math.max(bar440, bar660) * 2 + 'px of 200px canvas)' +
+ ' — linear scale hides low-frequency peaks. Need log-scale bar mapping.');
+ }
+
+ // Main spectrogram: confirm bins are in draw range
+ const mainFreqStart = 20, mainFreqEnd = 16000;
+ const inRange440 = 440 >= mainFreqStart && 440 <= mainFreqEnd;
+ const inRange660 = 660 >= mainFreqStart && 660 <= mainFreqEnd;
+ const norm440 = (Math.log2(440) - Math.log2(mainFreqStart)) / (Math.log2(mainFreqEnd) - Math.log2(mainFreqStart));
+ const norm660 = (Math.log2(660) - Math.log2(mainFreqStart)) / (Math.log2(mainFreqEnd) - Math.log2(mainFreqStart));
+ console.log('Main spectrogram (log Y-axis, 600px):');
+ console.log(` 440Hz: in range=${inRange440}, y=${Math.round(600 * (1 - norm440))}px, db=${db440.toFixed(1)}dB → intensity=${Math.min(1, Math.pow(Math.max(0, (db440 + 80) / 80), 2)).toFixed(2)}`);
+ console.log(` 660Hz: in range=${inRange660}, y=${Math.round(600 * (1 - norm660))}px, db=${db660.toFixed(1)}dB → intensity=${Math.min(1, Math.pow(Math.max(0, (db660 + 80) / 80), 2)).toFixed(2)}`);
+
+ console.groupEnd();
+ }
</script>
</body>
</html>