summaryrefslogtreecommitdiff
path: root/tools
diff options
context:
space:
mode:
authorskal <pascal.massimino@gmail.com>2026-02-06 13:50:56 +0100
committerskal <pascal.massimino@gmail.com>2026-02-06 13:50:56 +0100
commitb00d1cd351ec6c960ef957950e95930344f75dcc (patch)
tree44c3c9903500569aa162377f982d41338b9c0f2d /tools
parenta0888c1afa8bf178b7a57d4e80373ad867a3474a (diff)
feat(audio): FFT implementation Phase 1 - Infrastructure and foundation
Phase 1 Complete: Robust FFT infrastructure for future DCT optimization Current production code continues using O(N²) DCT/IDCT (perfectly accurate) FFT Infrastructure Implemented: ================================ Core FFT Engine: - Radix-2 Cooley-Tukey algorithm (power-of-2 sizes) - Bit-reversal permutation with in-place reordering - Butterfly operations with twiddle factor rotation - Forward FFT (time → frequency domain) - Inverse FFT (frequency → time domain, scaled by 1/N) Files Created: - src/audio/fft.{h,cc} - C++ implementation (~180 lines) - tools/spectral_editor/dct.js - Matching JavaScript implementation (~190 lines) - src/tests/test_fft.cc - Comprehensive test suite (~220 lines) Matching C++/JavaScript Implementation: - Identical algorithm structure in both languages - Same constant values (π, scaling factors) - Same floating-point operations for consistency - Enables spectral editor to match demo output exactly DCT-II via FFT (Experimental): - Double-and-mirror method implemented - dct_fft() and idct_fft() functions created - Works but accumulates numerical error (~1e-3 vs 1e-4 for direct method) - IDCT round-trip has ~3.6% error - needs algorithm refinement Build System Integration: - Added src/audio/fft.cc to AUDIO_SOURCES - Created test_fft target with comprehensive tests - Tests verify FFT correctness against reference O(N²) DCT Current Status: =============== Production Code: - Demo continues using existing O(N²) DCT/IDCT (fdct.cc, idct.cc) - Perfectly accurate, no changes to audio output - Zero risk to existing functionality FFT Infrastructure: - Core FFT engine verified correct (forward/inverse tested) - Provides foundation for future optimization - C++/JavaScript parity ensures editor consistency Known Issues: - DCT-via-FFT has small numerical errors (tolerance 1e-3 vs 1e-4) - IDCT-via-FFT round-trip error ~3.6% (hermitian symmetry needs work) - Double-and-mirror algorithm sensitive to implementation details Phase 2 TODO (Future Optimization): ==================================== Algorithm Refinement: 1. Research alternative DCT-via-FFT algorithms (FFTW, scipy, Numerical Recipes) 2. Fix IDCT hermitian symmetry packing for correct round-trip 3. Add reference value tests (compare against known good outputs) 4. Minimize error accumulation (currently ~10× higher than direct method) Performance Validation: 5. Benchmark O(N log N) FFT-based DCT vs O(N²) direct DCT 6. Confirm speedup justifies complexity (for N=512: 512² vs 512×log₂(512) = 262,144 vs 4,608) 7. Measure actual performance gain in spectral editor (JavaScript) Integration: 8. Replace fdct.cc/idct.cc with fft.cc once algorithms perfected 9. Update spectral editor to use FFT-based DCT by default 10. Remove old O(N²) implementations (size optimization) Technical Details: ================== FFT Complexity: O(N log N) where N = 512 - Radix-2 requires log₂(N) = 9 stages - Each stage: N/2 butterfly operations - Total: 9 × 256 = 2,304 complex multiplications DCT-II via FFT Complexity: O(N log N) + O(N) preprocessing - Theoretical speedup: 262,144 / 4,608 ≈ 57× faster - Actual speedup depends on constant factors and cache behavior Algorithm Used (Double-and-Mirror): 1. Extend signal to 2N by mirroring: [x₀, x₁, ..., x_{N-1}, x_{N-1}, ..., x₁] 2. Apply 2N-point FFT 3. Extract DCT coefficients: DCT[k] = Re{FFT[k] × exp(-jπk/(2N))} / 2 4. Apply DCT-II normalization: √(1/N) for k=0, √(2/N) otherwise References: - Numerical Recipes (Press et al.) - FFT algorithms - "A Fast Cosine Transform" (Chen, Smith, Fralick, 1977) - FFTW documentation - DCT implementation strategies Size Impact: - Added ~600 lines of code (fft.cc + fft.h + tests) - Test code stripped in final build (STRIP_ALL) - Core FFT: ~180 lines, will replace ~200 lines of O(N²) DCT when ready - Net size impact: Minimal (similar code size, better performance) Next Steps: =========== 1. Continue development with existing O(N²) DCT (stable, accurate) 2. Phase 2: Refine FFT-based DCT algorithm when time permits 3. Integrate once numerical accuracy matches reference (< 1e-4 tolerance) handoff(Claude): FFT Phase 1 complete. Infrastructure ready for Phase 2 refinement. Current production code unchanged (zero risk). Next: Algorithm debugging or other tasks. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Diffstat (limited to 'tools')
-rw-r--r--tools/spectral_editor/dct.js201
1 files changed, 201 insertions, 0 deletions
diff --git a/tools/spectral_editor/dct.js b/tools/spectral_editor/dct.js
index e48ce2b..deff8a9 100644
--- a/tools/spectral_editor/dct.js
+++ b/tools/spectral_editor/dct.js
@@ -29,3 +29,204 @@ function hanningWindow(size) {
const hanningWindowArray = hanningWindow(dctSize); // Pre-calculate window
+// ============================================================================
+// FFT-based DCT/IDCT Implementation
+// ============================================================================
+// Fast Fourier Transform using Radix-2 Cooley-Tukey algorithm
+// This implementation MUST match the C++ version in src/audio/fft.cc exactly
+
+// Bit-reversal permutation (in-place)
+// Reorders array elements by reversing their binary indices
+function bitReversePermute(real, imag, N) {
+ // Calculate number of bits needed
+ let temp_bits = N;
+ let num_bits = 0;
+ while (temp_bits > 1) {
+ temp_bits >>= 1;
+ num_bits++;
+ }
+
+ for (let i = 0; i < N; i++) {
+ // Compute bit-reversed index
+ let j = 0;
+ let temp = i;
+ for (let b = 0; b < num_bits; b++) {
+ j = (j << 1) | (temp & 1);
+ temp >>= 1;
+ }
+
+ // Swap if j > i (to avoid swapping twice)
+ 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 (after bit-reversal)
+// direction: +1 for forward FFT, -1 for inverse FFT
+function fftRadix2(real, imag, N, direction) {
+ const PI = Math.PI;
+
+ // Butterfly operations
+ 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;
+
+ // Precompute twiddle factors for this stage
+ 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++) {
+ // Apply butterfly to all groups at this stage
+ for (let group_start = k; group_start < N; group_start += stage_size) {
+ const i = group_start;
+ const j = group_start + half_stage;
+
+ // Complex multiplication: (real[j] + i*imag[j]) * (wr + i*wi)
+ const temp_real = real[j] * wr - imag[j] * wi;
+ const temp_imag = real[j] * wi + imag[j] * wr;
+
+ // Butterfly operation
+ real[j] = real[i] - temp_real;
+ imag[j] = imag[i] - temp_imag;
+ real[i] = real[i] + temp_real;
+ imag[i] = imag[i] + temp_imag;
+ }
+
+ // Update twiddle factor for next k (rotation)
+ 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);
+}
+
+// Inverse FFT: Frequency domain → Time domain
+function fftInverse(real, imag, N) {
+ bitReversePermute(real, imag, N);
+ fftRadix2(real, imag, N, -1);
+
+ // Scale by 1/N
+ const scale = 1.0 / N;
+ for (let i = 0; i < N; i++) {
+ real[i] *= scale;
+ imag[i] *= scale;
+ }
+}
+
+// DCT-II via FFT using double-and-mirror method (matches C++ dct_fft)
+// This is a more robust algorithm that avoids reordering issues
+function javascript_dct_fft(input, N) {
+ const PI = Math.PI;
+
+ // Allocate arrays for 2N-point FFT
+ const M = 2 * N;
+ const real = new Float32Array(M);
+ const imag = new Float32Array(M);
+
+ // Pack input: [x[0], x[1], ..., x[N-1], x[N-1], x[N-2], ..., x[1]]
+ // This creates even symmetry for real-valued DCT
+ for (let i = 0; i < N; i++) {
+ real[i] = input[i];
+ }
+ for (let i = 0; i < N; i++) {
+ real[N + i] = input[N - 1 - i];
+ }
+ // imag is already zeros (Float32Array default)
+
+ // Apply 2N-point FFT
+ fftForward(real, imag, M);
+
+ // Extract DCT coefficients
+ // DCT[k] = Re{FFT[k] * exp(-j*pi*k/(2*N))} * normalization
+ // Note: Need to divide by 2 because we doubled the signal length
+ const output = new Float32Array(N);
+ for (let k = 0; k < N; k++) {
+ const angle = -PI * k / (2.0 * N);
+ const wr = Math.cos(angle);
+ const wi = Math.sin(angle);
+
+ // Complex multiplication: (real + j*imag) * (wr + j*wi)
+ // Real part: real*wr - imag*wi
+ const dct_value = real[k] * wr - imag[k] * wi;
+
+ // Apply DCT-II normalization (divide by 2 for double-length FFT)
+ if (k === 0) {
+ output[k] = dct_value * Math.sqrt(1.0 / N) / 2.0;
+ } else {
+ output[k] = dct_value * Math.sqrt(2.0 / N) / 2.0;
+ }
+ }
+
+ return output;
+}
+
+// IDCT (Inverse DCT-II) via FFT using double-and-mirror method (matches C++ idct_fft)
+function javascript_idct_fft(input, N) {
+ const PI = Math.PI;
+
+ // Allocate arrays for 2N-point FFT
+ const M = 2 * N;
+ const real = new Float32Array(M);
+ const imag = new Float32Array(M);
+
+ // Prepare FFT input from DCT coefficients
+ // IDCT = Re{IFFT[DCT * exp(j*pi*k/(2*N))]} * 2
+ for (let k = 0; k < N; k++) {
+ const angle = PI * k / (2.0 * N); // Positive for inverse
+ const wr = Math.cos(angle);
+ const wi = Math.sin(angle);
+
+ // Apply inverse normalization
+ let scaled_input;
+ if (k === 0) {
+ scaled_input = input[k] * Math.sqrt(N) * 2.0;
+ } else {
+ scaled_input = input[k] * Math.sqrt(N / 2.0) * 2.0;
+ }
+
+ // Complex multiplication: DCT[k] * exp(j*theta)
+ real[k] = scaled_input * wr;
+ imag[k] = scaled_input * wi;
+ }
+
+ // Fill second half with conjugate symmetry (for real output)
+ for (let k = 1; k < N; k++) {
+ real[M - k] = real[k];
+ imag[M - k] = -imag[k];
+ }
+
+ // Apply inverse FFT
+ fftInverse(real, imag, M);
+
+ // Extract first N samples (real part only, imag should be ~0)
+ const output = new Float32Array(N);
+ for (let i = 0; i < N; i++) {
+ output[i] = real[i];
+ }
+
+ return output;
+}
+
+// Convenience wrappers for dctSize = 512 (backward compatible)
+function javascript_dct_512_fft(input) {
+ return javascript_dct_fft(input, dctSize);
+}
+
+function javascript_idct_512_fft(input) {
+ return javascript_idct_fft(input, dctSize);
+}
+