summaryrefslogtreecommitdiff
path: root/tools/mq_editor/fft.js
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/fft.js
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/fft.js')
-rw-r--r--tools/mq_editor/fft.js86
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));
+ }
+}
+