Bin
2025-12-17 d616898802dfe7e5dd648bcf53c6d1f86b6d3642
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
import webfft from "webfft";
import { applyWindowFunction, type WindowFunctionType } from "../Visual/WindowFunctions";
import { MelBanks } from "./MelBanks";
import { SPECTROGRAM_DEFAULTS } from "../Visual/constants";
 
export type SpectrogramScale = "linear" | "log" | "mel";
 
export interface FFTProcessorOptions {
  fftSamples: number;
  windowingFunction: WindowFunctionType;
  sampleRate?: number;
}
 
/**
 * Handles the core FFT calculations, windowing, and Mel scale conversion.
 */
export class FFTProcessor {
  private options: FFTProcessorOptions;
  private webfftInstance: webfft | null = null;
 
  // Added a cache for MelBanks instances to avoid recreating them constantly
  private melBanksCache: MelBanks | null = null;
  private melBanksCacheKey: string | null = null;
 
  // Persistent buffers for performance
  private fftInputBuffer: Float32Array | null = null;
  private fftInterleavedInputBuffer: Float32Array | null = null;
 
  constructor(options: FFTProcessorOptions) {
    this.options = {
      ...options,
      fftSamples: options.fftSamples || SPECTROGRAM_DEFAULTS.FFT_SAMPLES,
      windowingFunction: options.windowingFunction || "hann",
    };
    this.initialize();
  }
 
  private initialize() {
    try {
      this.webfftInstance = new webfft(this.options.fftSamples);
      // Run profiling immediately after initialization
      (this.webfftInstance as any).profile();
 
      // Pre-allocate buffers
      this.fftInputBuffer = new Float32Array(this.options.fftSamples);
      // webfft might need interleaved input (real, imag, real, imag, ...)
      this.fftInterleavedInputBuffer = new Float32Array(this.options.fftSamples * 2);
    } catch (_error) {
      this.webfftInstance = null;
      this.fftInputBuffer = null;
      this.fftInterleavedInputBuffer = null;
    }
  }
 
  /**
   * Updates FFT parameters. Re-initializes FFT instance and Mel filterbank if necessary.
   */
  updateParameters(newOptions: Partial<FFTProcessorOptions>) {
    const needsReinitialization = newOptions.fftSamples && newOptions.fftSamples !== this.options.fftSamples;
    // Check if the sampleRate changed, as it affects MelBanks
    const needsMelCacheClear = newOptions.sampleRate && newOptions.sampleRate !== this.options.sampleRate;
 
    this.options = { ...this.options, ...newOptions };
 
    if (needsReinitialization) {
      this.webfftInstance?.dispose(); // Clean up old instance if exists
      this.initialize(); // Re-initialize with a new size
      this.melBanksCache = null; // Clear MelBanks cache if FFT size changes
      this.melBanksCacheKey = null;
    } else if (needsMelCacheClear) {
      this.melBanksCache = null; // Clear MelBanks cache if the sample rate changes
      this.melBanksCacheKey = null;
    }
  }
 
  /**
   * Calculates the power spectrum for a given audio buffer segment.
   * Applies windowing function before FFT.
   * Handles potential errors during FFT calculation.
   *
   * @param buffer The input audio data segment.
   * @returns The power spectrum (magnitude) or null if FFT failed.
   */
  calculatePowerSpectrum(buffer: Float32Array): Float32Array | null {
    if (!this.webfftInstance || !this.fftInputBuffer || !this.fftInterleavedInputBuffer || buffer.length === 0) {
      return this.handleFFTError();
    }
 
    // Ensure the input buffer has the correct data, applying windowing
    // Use slice(0, fftSamples) in case the input buffer is longer
    const inputSlice = buffer.slice(0, this.options.fftSamples);
 
    // Copy sliced data into the pre-allocated buffer
    // Pad with zeros if inputSlice is shorter than fftSamples
    this.fftInputBuffer.set(inputSlice);
    if (inputSlice.length < this.options.fftSamples) {
      this.fftInputBuffer.fill(0, inputSlice.length);
    }
 
    // Now apply the window function IN-PLACE to the fftInputBuffer
    applyWindowFunction(this.fftInputBuffer, this.options.windowingFunction);
 
    try {
      // Prepare interleaved input for webfft (assuming real input)
      for (let i = 0; i < this.options.fftSamples; i++) {
        this.fftInterleavedInputBuffer[2 * i] = this.fftInputBuffer[i]; // Real part (now correctly windowed)
        this.fftInterleavedInputBuffer[2 * i + 1] = 0; // Imaginary part
      }
 
      // Perform FFT
      const fftResult = this.webfftInstance.fft(this.fftInterleavedInputBuffer);
 
      // Add a check for a valid FFT result
      if (!fftResult) {
        console.error("WebFFT returned invalid result", fftResult);
        return this.handleFFTError();
      }
 
      // Cast the result after the check using any as a workaround
      const validFftResult = fftResult as any;
 
      // Calculate magnitude (power spectrum)
      // Output is complex (real, imag), we need sqrt(real^2 + imag^2)
      // Result is half the size + 1 (due to symmetry)
      const spectrumSize = this.options.fftSamples / 2 + 1;
      const powerSpectrum = new Float32Array(spectrumSize);
      const normFactor = this.options.fftSamples; // Normalization factor (FFT size)
 
      // Handle DC component (index 0)
      const dcReal = validFftResult[0];
      powerSpectrum[0] = Math.abs(dcReal) / normFactor;
 
      // Handle remaining bins up to Nyquist
      for (let i = 1; i < spectrumSize; i++) {
        const real = validFftResult[2 * i];
        const imag = validFftResult[2 * i + 1];
        // Normalize the magnitude
        // Note: Standard normalization often uses N for power, 2/N for amplitude spectrum (excluding DC/Nyquist)
        // We are calculating power (magnitude squared implicitly via dB conversion later),
        // but normalizing magnitude by N here is common before dB.
        powerSpectrum[i] = Math.sqrt(real * real + imag * imag) / normFactor;
      }
 
      return powerSpectrum;
    } catch (error) {
      console.error("Error during FFT calculation:", error);
      return this.handleFFTError();
    }
  }
 
  /**
   * Converts a linear power spectrum to the Mel scale using the MelBanks class.
   *
   * @param linearSpectrum The input power spectrum.
   * @param numberOfMelBands The desired number of Mel bands for this conversion.
   * @returns The Mel scaled spectrum or null if parameters are missing/invalid.
   */
  convertToMelScale(linearSpectrum: Float32Array, numberOfMelBands: number): Float32Array | null {
    if (!this.options.sampleRate) {
      console.warn("Sample rate required for Mel scale conversion.");
      return null;
    }
    if (numberOfMelBands <= 0) {
      console.warn("Number of Mel bands must be positive.");
      return null;
    }
 
    const linearBinCount = linearSpectrum.length;
    const currentKey = `${this.options.sampleRate}-${linearBinCount}-${numberOfMelBands}`;
 
    // Check cache
    if (!this.melBanksCache || this.melBanksCacheKey !== currentKey) {
      try {
        this.melBanksCache = new MelBanks(this.options.sampleRate, linearBinCount, numberOfMelBands);
        this.melBanksCacheKey = currentKey;
      } catch (error) {
        console.error("Failed to create MelBanks instance:", error);
        this.melBanksCache = null;
        this.melBanksCacheKey = null;
        return null;
      }
    }
 
    // Apply the filter bank using the cached instance
    try {
      return this.melBanksCache.applyFilterbank(linearSpectrum);
    } catch (error) {
      console.error("Error applying Mel filterbank:", error);
      return null;
    }
  }
 
  /**
   * Returns a fallback array when FFT calculation fails.
   */
  private handleFFTError(): Float32Array | null {
    // Return null to indicate failure, let the caller decide on fallback
    return null;
  }
 
  /**
   * Cleans up the WebFFT instance.
   */
  dispose() {
    this.webfftInstance?.dispose();
    this.webfftInstance = null;
    this.fftInputBuffer = null;
    this.fftInterleavedInputBuffer = null;
    this.melBanksCache = null; // Clear cache on dispose
    this.melBanksCacheKey = null;
  }
 
  // Getter for FFT samples size
  get fftSamples(): number {
    return this.options.fftSamples;
  }
}