diff options
| author | skal <pascal.massimino@gmail.com> | 2026-02-17 20:54:15 +0100 |
|---|---|---|
| committer | skal <pascal.massimino@gmail.com> | 2026-02-17 20:54:15 +0100 |
| commit | cfcd238044c7ce06dfdf1f9e08c3842bfa07979b (patch) | |
| tree | 12cd6da868c43af9c054bc44705326c1dae5f6de /tools/mq_editor/index.html | |
| parent | d151eb48b2c55d16a1d9caa6a7affb3e0793c3e7 (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.html | 91 |
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) { |
