summaryrefslogtreecommitdiff
path: root/tools/mq_editor/fft.js
diff options
context:
space:
mode:
authorskal <pascal.massimino@gmail.com>2026-02-17 16:12:21 +0100
committerskal <pascal.massimino@gmail.com>2026-02-17 16:12:21 +0100
commit03579c4a33ab3955ff9924a6dcd882fe91dd9aaa (patch)
treebe458d2ac4bc0d7160be8a18526b4e9157af33a5 /tools/mq_editor/fft.js
parente3f0b002c0998c8553e782273b254869107ffc0f (diff)
feat(mq_editor): Phase 1 - MQ extraction and visualization (SPECTRAL_BRUSH_2)
Implement McAulay-Quatieri sinusoidal analysis tool for audio compression. New files: - doc/SPECTRAL_BRUSH_2.md: Complete design doc (MQ algorithm, data format, synthesis, roadmap) - tools/mq_editor/index.html: Web UI (file loader, params, canvas) - tools/mq_editor/fft.js: Radix-2 Cooley-Tukey FFT (from spectral_editor) - tools/mq_editor/mq_extract.js: MQ algorithm (peak detection, tracking, bezier fitting) - tools/mq_editor/viewer.js: Visualization (spectrogram, partials, zoom, axes) - tools/mq_editor/README.md: Usage and implementation status Features: - Load WAV → extract sinusoidal partials → fit cubic bezier curves - Time-frequency spectrogram with hot colormap (0-16 kHz) - Horizontal zoom (mousewheel) around mouse position - Axis ticks with labels (time: seconds, freq: Hz/kHz) - Mouse tooltip showing time/frequency coordinates - Real-time adjustable MQ parameters (FFT size, hop, threshold) Algorithm: - STFT with Hann windows (2048 FFT, 512 hop) - Peak detection with parabolic interpolation - Birth/death/continuation tracking (50 Hz tolerance) - Cubic bezier fitting (4 control points per trajectory) Next: Phase 2 (JS synthesizer for audio preview) handoff(Claude): MQ editor Phase 1 complete. Ready for synthesis implementation.
Diffstat (limited to 'tools/mq_editor/fft.js')
-rw-r--r--tools/mq_editor/fft.js103
1 files changed, 103 insertions, 0 deletions
diff --git a/tools/mq_editor/fft.js b/tools/mq_editor/fft.js
new file mode 100644
index 0000000..8610222
--- /dev/null
+++ b/tools/mq_editor/fft.js
@@ -0,0 +1,103 @@
+// Fast Fourier Transform (adapted from spectral_editor/dct.js)
+// Radix-2 Cooley-Tukey algorithm
+
+// Bit-reversal permutation (in-place)
+function bitReversePermute(real, imag, N) {
+ let temp_bits = N;
+ let num_bits = 0;
+ while (temp_bits > 1) {
+ temp_bits >>= 1;
+ num_bits++;
+ }
+
+ for (let i = 0; i < N; ++i) {
+ let j = 0;
+ let temp = i;
+ for (let b = 0; b < num_bits; ++b) {
+ j = (j << 1) | (temp & 1);
+ temp >>= 1;
+ }
+
+ if (j > i) {
+ const tmp_real = real[i];
+ const tmp_imag = imag[i];
+ real[i] = real[j];
+ imag[i] = imag[j];
+ real[j] = tmp_real;
+ imag[j] = tmp_imag;
+ }
+ }
+}
+
+// In-place radix-2 FFT
+// direction: +1 for forward, -1 for inverse
+function fftRadix2(real, imag, N, direction) {
+ const PI = Math.PI;
+
+ for (let stage_size = 2; stage_size <= N; stage_size *= 2) {
+ const half_stage = stage_size / 2;
+ const angle = direction * 2.0 * PI / stage_size;
+
+ let wr = 1.0;
+ let wi = 0.0;
+ const wr_delta = Math.cos(angle);
+ const wi_delta = Math.sin(angle);
+
+ for (let k = 0; k < half_stage; ++k) {
+ for (let group_start = k; group_start < N; group_start += stage_size) {
+ const i = group_start;
+ const j = group_start + half_stage;
+
+ const temp_real = real[j] * wr - imag[j] * wi;
+ const temp_imag = real[j] * wi + imag[j] * wr;
+
+ real[j] = real[i] - temp_real;
+ imag[j] = imag[i] - temp_imag;
+ real[i] = real[i] + temp_real;
+ imag[i] = imag[i] + temp_imag;
+ }
+
+ const wr_old = wr;
+ wr = wr_old * wr_delta - wi * wi_delta;
+ wi = wr_old * wi_delta + wi * wr_delta;
+ }
+ }
+}
+
+// Forward FFT: Time domain → Frequency domain
+function fftForward(real, imag, N) {
+ bitReversePermute(real, imag, N);
+ fftRadix2(real, imag, N, +1);
+}
+
+// Real FFT wrapper for MQ extraction
+// Input: Float32Array (time-domain signal)
+// Output: Float32Array (interleaved [re0, im0, re1, im1, ...])
+function realFFT(signal) {
+ const N = signal.length;
+
+ // Must be power of 2
+ if ((N & (N - 1)) !== 0) {
+ throw new Error('FFT size must be power of 2');
+ }
+
+ const real = new Float32Array(N);
+ const imag = new Float32Array(N);
+
+ // Copy input to real part
+ for (let i = 0; i < N; ++i) {
+ real[i] = signal[i];
+ }
+
+ // Compute FFT
+ fftForward(real, imag, N);
+
+ // Interleave output
+ const spectrum = new Float32Array(N * 2);
+ for (let i = 0; i < N; ++i) {
+ spectrum[i * 2] = real[i];
+ spectrum[i * 2 + 1] = imag[i];
+ }
+
+ return spectrum;
+}