summaryrefslogtreecommitdiff
path: root/tools/mq_editor/index.html
diff options
context:
space:
mode:
authorskal <pascal.massimino@gmail.com>2026-02-17 20:54:15 +0100
committerskal <pascal.massimino@gmail.com>2026-02-17 20:54:15 +0100
commitcfcd238044c7ce06dfdf1f9e08c3842bfa07979b (patch)
tree12cd6da868c43af9c054bc44705326c1dae5f6de /tools/mq_editor/index.html
parentd151eb48b2c55d16a1d9caa6a7affb3e0793c3e7 (diff)
feat(mq_editor): Complete Phase 2 - JS synthesizer with STFT cache
Phase 2 - JS Synthesizer: - Created mq_synth.js with replica oscillator bank - Bezier curve evaluation (cubic De Casteljau algorithm) - Replica synthesis: frequency spread, amplitude decay, phase jitter - PCM buffer generation from extracted MQ partials - Normalization to prevent clipping - Key '1' plays synthesized audio, key '2' plays original - Playback comparison with animated playhead STFT Cache Optimization: - Created STFTCache class in fft.js for pre-computed windowed FFT frames - Clean interface: getFFT(t), getMagnitudeDB(t, freq), setHopSize() - Pre-computes all frames on WAV load (eliminates redundant FFT calls) - Dynamic cache update when hop size changes - Shared across spectrogram, tooltip, and mini-spectrum viewer - Significant performance improvement Mini-Spectrum Viewer: - Bottom-right overlay (200x100) matching spectral_editor style - Real-time FFT display at playhead or mouse position - 100-bar visualization with cyan-to-yellow gradient - Updates during playback or mouse hover Files: - tools/mq_editor/mq_synth.js (new) - tools/mq_editor/fft.js (STFTCache class added) - tools/mq_editor/index.html (synthesis playback, cache integration) - tools/mq_editor/viewer.js (cache-based rendering, spectrum viewer) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Diffstat (limited to 'tools/mq_editor/index.html')
-rw-r--r--tools/mq_editor/index.html91
1 files changed, 85 insertions, 6 deletions
diff --git a/tools/mq_editor/index.html b/tools/mq_editor/index.html
index 1a07b61..84abd1c 100644
--- a/tools/mq_editor/index.html
+++ b/tools/mq_editor/index.html
@@ -86,7 +86,14 @@
</div>
</div>
- <canvas id="canvas" width="1400" height="600"></canvas>
+ <div style="position: relative;">
+ <canvas id="canvas" width="1400" height="600"></canvas>
+
+ <!-- Mini spectrum viewer (bottom-right overlay) -->
+ <div id="spectrumViewer" style="position: absolute; bottom: 10px; right: 10px; width: 200px; height: 100px; background: rgba(30, 30, 30, 0.9); border: 1px solid #555; border-radius: 3px; pointer-events: none;">
+ <canvas id="spectrumCanvas" width="200" height="100"></canvas>
+ </div>
+ </div>
<div id="tooltip" style="position: fixed; display: none; background: #2a2a2a; padding: 4px 8px; border: 1px solid #555; border-radius: 3px; pointer-events: none; font-size: 11px; z-index: 1000;"></div>
@@ -94,12 +101,15 @@
<script src="fft.js"></script>
<script src="mq_extract.js"></script>
+ <script src="mq_synth.js"></script>
<script src="viewer.js"></script>
<script>
let audioBuffer = null;
let viewer = null;
let audioContext = null;
let currentSource = null;
+ let extractedPartials = null;
+ let stftCache = null;
const wavFile = document.getElementById('wavFile');
const extractBtn = document.getElementById('extractBtn');
@@ -133,16 +143,36 @@
initAudioContext();
extractBtn.disabled = false;
playBtn.disabled = false;
- setStatus(`Loaded: ${audioBuffer.duration.toFixed(2)}s, ${audioBuffer.sampleRate}Hz, ${audioBuffer.numberOfChannels}ch`, 'info');
+ setStatus('Computing STFT cache...', 'info');
- // Create viewer
- viewer = new SpectrogramViewer(canvas, audioBuffer);
+ // Compute STFT cache
+ setTimeout(() => {
+ const signal = audioBuffer.getChannelData(0);
+ stftCache = new STFTCache(signal, audioBuffer.sampleRate, fftSize, parseInt(hopSize.value));
+
+ setStatus(`Loaded: ${audioBuffer.duration.toFixed(2)}s, ${audioBuffer.sampleRate}Hz, ${audioBuffer.numberOfChannels}ch (${stftCache.getNumFrames()} frames cached)`, 'info');
+
+ // Create viewer with cache
+ viewer = new SpectrogramViewer(canvas, audioBuffer, stftCache);
+ }, 10);
} catch (err) {
setStatus('Error loading WAV: ' + err.message, 'error');
console.error(err);
}
});
+ // Update cache when hop size changes
+ hopSize.addEventListener('change', () => {
+ if (stftCache) {
+ setStatus('Updating STFT cache...', 'info');
+ setTimeout(() => {
+ stftCache.setHopSize(parseInt(hopSize.value));
+ setStatus(`Cache updated (${stftCache.getNumFrames()} frames)`, 'info');
+ if (viewer) viewer.render();
+ }, 10);
+ }
+ });
+
// Extract partials
extractBtn.addEventListener('click', () => {
if (!audioBuffer) return;
@@ -161,6 +191,7 @@
const partials = extractPartials(audioBuffer, params);
+ extractedPartials = partials;
setStatus(`Extracted ${partials.length} partials`, 'info');
// Update viewer
@@ -235,12 +266,60 @@
status.className = type;
}
+ // Play synthesized audio
+ function playSynthesized() {
+ if (!extractedPartials || extractedPartials.length === 0) {
+ setStatus('No partials extracted yet', 'warn');
+ return;
+ }
+ if (!audioBuffer || !audioContext) return;
+
+ stopAudio();
+
+ setStatus('Synthesizing...', 'info');
+
+ // Synthesize PCM from partials
+ const sampleRate = audioBuffer.sampleRate;
+ const duration = audioBuffer.duration;
+ const pcm = synthesizeMQ(extractedPartials, sampleRate, duration);
+
+ // Create audio buffer
+ const synthBuffer = audioContext.createBuffer(1, pcm.length, sampleRate);
+ synthBuffer.getChannelData(0).set(pcm);
+
+ const startTime = audioContext.currentTime;
+ currentSource = audioContext.createBufferSource();
+ currentSource.buffer = synthBuffer;
+ currentSource.connect(audioContext.destination);
+ currentSource.start();
+
+ currentSource.onended = () => {
+ currentSource = null;
+ playBtn.disabled = false;
+ stopBtn.disabled = true;
+ viewer.setPlayheadTime(-1);
+ setStatus('Stopped', 'info');
+ };
+
+ playBtn.disabled = true;
+ stopBtn.disabled = false;
+ setStatus('Playing synthesized...', 'info');
+
+ // Animate playhead
+ function updatePlayhead() {
+ if (!currentSource) return;
+ const elapsed = audioContext.currentTime - startTime;
+ viewer.setPlayheadTime(elapsed);
+ requestAnimationFrame(updatePlayhead);
+ }
+ updatePlayhead();
+ }
+
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
if (e.code === 'Digit1') {
e.preventDefault();
- // TODO: Play synthesized (Phase 2)
- setStatus('Synthesized playback not yet implemented', 'warn');
+ playSynthesized();
} else if (e.code === 'Digit2') {
e.preventDefault();
if (!playBtn.disabled) {