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/fft.js | |
| 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/fft.js')
| -rw-r--r-- | tools/mq_editor/fft.js | 86 |
1 files changed, 86 insertions, 0 deletions
diff --git a/tools/mq_editor/fft.js b/tools/mq_editor/fft.js index 8610222..0668906 100644 --- a/tools/mq_editor/fft.js +++ b/tools/mq_editor/fft.js @@ -101,3 +101,89 @@ function realFFT(signal) { return spectrum; } + +// STFT Cache - Pre-computes and caches windowed FFT frames +class STFTCache { + constructor(signal, sampleRate, fftSize, hopSize) { + this.signal = signal; + this.sampleRate = sampleRate; + this.fftSize = fftSize; + this.hopSize = hopSize; + this.frames = []; // Array of {time, offset, spectrum} + this.compute(); + } + + compute() { + this.frames = []; + const numFrames = Math.floor((this.signal.length - this.fftSize) / this.hopSize); + + for (let frameIdx = 0; frameIdx < numFrames; ++frameIdx) { + const offset = frameIdx * this.hopSize; + const time = offset / this.sampleRate; + + // Extract frame + const frame = this.signal.slice(offset, offset + this.fftSize); + + // Apply Hann window + const windowed = new Float32Array(this.fftSize); + for (let i = 0; i < this.fftSize; ++i) { + const w = 0.5 - 0.5 * Math.cos(2 * Math.PI * i / this.fftSize); + windowed[i] = frame[i] * w; + } + + // Compute FFT + const spectrum = realFFT(windowed); + + this.frames.push({time, offset, spectrum}); + } + } + + setHopSize(hopSize) { + if (hopSize === this.hopSize) return; + this.hopSize = hopSize; + this.compute(); + } + + setFFTSize(fftSize) { + if (fftSize === this.fftSize) return; + this.fftSize = fftSize; + this.compute(); + } + + getNumFrames() { + return this.frames.length; + } + + getFrameAtIndex(frameIdx) { + if (frameIdx < 0 || frameIdx >= this.frames.length) return null; + return this.frames[frameIdx]; + } + + getFrameAtTime(t) { + if (this.frames.length === 0) return null; + + // Find closest frame + const frameIdx = Math.floor(t * this.sampleRate / this.hopSize); + return this.getFrameAtIndex(frameIdx); + } + + getFFT(t) { + const frame = this.getFrameAtTime(t); + return frame ? frame.spectrum : null; + } + + // Get magnitude in dB at specific time and frequency + getMagnitudeDB(t, freq) { + const spectrum = this.getFFT(t); + if (!spectrum) return -80; + + const bin = Math.round(freq * this.fftSize / this.sampleRate); + if (bin < 0 || bin >= this.fftSize / 2) return -80; + + const re = spectrum[bin * 2]; + const im = spectrum[bin * 2 + 1]; + const mag = Math.sqrt(re * re + im * im); + return 20 * Math.log10(Math.max(mag, 1e-10)); + } +} + |
