dspx 0.1.1-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/.github/workflows/ci.yml +185 -0
  2. package/.vscode/c_cpp_properties.json +17 -0
  3. package/.vscode/settings.json +68 -0
  4. package/.vscode/tasks.json +28 -0
  5. package/DISCLAIMER.md +32 -0
  6. package/LICENSE +21 -0
  7. package/README.md +1803 -0
  8. package/ROADMAP.md +192 -0
  9. package/TECHNICAL_DEBT.md +165 -0
  10. package/binding.gyp +65 -0
  11. package/docs/ADVANCED_LOGGER_FEATURES.md +598 -0
  12. package/docs/AUTHENTICATION_SECURITY.md +396 -0
  13. package/docs/BACKEND_IMPROVEMENTS.md +399 -0
  14. package/docs/CHEBYSHEV_BIQUAD_EQ_IMPLEMENTATION.md +405 -0
  15. package/docs/FFT_IMPLEMENTATION.md +490 -0
  16. package/docs/FFT_IMPROVEMENTS_SUMMARY.md +387 -0
  17. package/docs/FFT_USER_GUIDE.md +494 -0
  18. package/docs/FILTERS_IMPLEMENTATION.md +260 -0
  19. package/docs/FILTER_API_GUIDE.md +418 -0
  20. package/docs/FIR_SIMD_OPTIMIZATION.md +175 -0
  21. package/docs/LOGGER_API_REFERENCE.md +350 -0
  22. package/docs/NOTCH_FILTER_QUICK_REF.md +121 -0
  23. package/docs/PHASE2_TESTS_AND_NOTCH_FILTER.md +341 -0
  24. package/docs/PHASES_5_7_SUMMARY.md +403 -0
  25. package/docs/PIPELINE_FILTER_INTEGRATION.md +446 -0
  26. package/docs/SIMD_OPTIMIZATIONS.md +211 -0
  27. package/docs/TEST_MIGRATION_SUMMARY.md +173 -0
  28. package/docs/TIMESERIES_IMPLEMENTATION_SUMMARY.md +322 -0
  29. package/docs/TIMESERIES_QUICK_REF.md +85 -0
  30. package/docs/advanced.md +559 -0
  31. package/docs/time-series-guide.md +617 -0
  32. package/docs/time-series-migration.md +376 -0
  33. package/jest.config.js +37 -0
  34. package/package.json +42 -0
  35. package/prebuilds/linux-x64/dsp-ts-redis.node +0 -0
  36. package/prebuilds/win32-x64/dsp-ts-redis.node +0 -0
  37. package/scripts/test.js +24 -0
  38. package/src/build/dsp-ts-redis.node +0 -0
  39. package/src/native/DspPipeline.cc +675 -0
  40. package/src/native/DspPipeline.h +44 -0
  41. package/src/native/FftBindings.cc +817 -0
  42. package/src/native/FilterBindings.cc +1001 -0
  43. package/src/native/IDspStage.h +53 -0
  44. package/src/native/adapters/InterpolatorStage.h +201 -0
  45. package/src/native/adapters/MeanAbsoluteValueStage.h +289 -0
  46. package/src/native/adapters/MovingAverageStage.h +306 -0
  47. package/src/native/adapters/RectifyStage.h +88 -0
  48. package/src/native/adapters/ResamplerStage.h +238 -0
  49. package/src/native/adapters/RmsStage.h +299 -0
  50. package/src/native/adapters/SscStage.h +121 -0
  51. package/src/native/adapters/VarianceStage.h +307 -0
  52. package/src/native/adapters/WampStage.h +114 -0
  53. package/src/native/adapters/WaveformLengthStage.h +115 -0
  54. package/src/native/adapters/ZScoreNormalizeStage.h +326 -0
  55. package/src/native/core/FftEngine.cc +441 -0
  56. package/src/native/core/FftEngine.h +224 -0
  57. package/src/native/core/FirFilter.cc +324 -0
  58. package/src/native/core/FirFilter.h +149 -0
  59. package/src/native/core/IirFilter.cc +576 -0
  60. package/src/native/core/IirFilter.h +210 -0
  61. package/src/native/core/MovingAbsoluteValueFilter.cc +17 -0
  62. package/src/native/core/MovingAbsoluteValueFilter.h +135 -0
  63. package/src/native/core/MovingAverageFilter.cc +18 -0
  64. package/src/native/core/MovingAverageFilter.h +135 -0
  65. package/src/native/core/MovingFftFilter.cc +291 -0
  66. package/src/native/core/MovingFftFilter.h +203 -0
  67. package/src/native/core/MovingVarianceFilter.cc +194 -0
  68. package/src/native/core/MovingVarianceFilter.h +114 -0
  69. package/src/native/core/MovingZScoreFilter.cc +215 -0
  70. package/src/native/core/MovingZScoreFilter.h +113 -0
  71. package/src/native/core/Policies.h +352 -0
  72. package/src/native/core/RmsFilter.cc +18 -0
  73. package/src/native/core/RmsFilter.h +131 -0
  74. package/src/native/core/SscFilter.cc +16 -0
  75. package/src/native/core/SscFilter.h +137 -0
  76. package/src/native/core/WampFilter.cc +16 -0
  77. package/src/native/core/WampFilter.h +101 -0
  78. package/src/native/core/WaveformLengthFilter.cc +17 -0
  79. package/src/native/core/WaveformLengthFilter.h +98 -0
  80. package/src/native/utils/CircularBufferArray.cc +336 -0
  81. package/src/native/utils/CircularBufferArray.h +62 -0
  82. package/src/native/utils/CircularBufferVector.cc +145 -0
  83. package/src/native/utils/CircularBufferVector.h +45 -0
  84. package/src/native/utils/NapiUtils.cc +53 -0
  85. package/src/native/utils/NapiUtils.h +21 -0
  86. package/src/native/utils/SimdOps.h +870 -0
  87. package/src/native/utils/SlidingWindowFilter.cc +239 -0
  88. package/src/native/utils/SlidingWindowFilter.h +159 -0
  89. package/src/native/utils/TimeSeriesBuffer.cc +205 -0
  90. package/src/native/utils/TimeSeriesBuffer.h +140 -0
  91. package/src/ts/CircularLogBuffer.ts +87 -0
  92. package/src/ts/DriftDetector.ts +331 -0
  93. package/src/ts/TopicRouter.ts +428 -0
  94. package/src/ts/__tests__/AdvancedDsp.test.ts +585 -0
  95. package/src/ts/__tests__/AuthAndEdgeCases.test.ts +241 -0
  96. package/src/ts/__tests__/Chaining.test.ts +387 -0
  97. package/src/ts/__tests__/ChebyshevBiquad.test.ts +229 -0
  98. package/src/ts/__tests__/CircularLogBuffer.test.ts +158 -0
  99. package/src/ts/__tests__/DriftDetector.test.ts +389 -0
  100. package/src/ts/__tests__/Fft.test.ts +484 -0
  101. package/src/ts/__tests__/ListState.test.ts +153 -0
  102. package/src/ts/__tests__/Logger.test.ts +208 -0
  103. package/src/ts/__tests__/LoggerAdvanced.test.ts +319 -0
  104. package/src/ts/__tests__/LoggerMinor.test.ts +247 -0
  105. package/src/ts/__tests__/MeanAbsoluteValue.test.ts +398 -0
  106. package/src/ts/__tests__/MovingAverage.test.ts +322 -0
  107. package/src/ts/__tests__/RMS.test.ts +315 -0
  108. package/src/ts/__tests__/Rectify.test.ts +272 -0
  109. package/src/ts/__tests__/Redis.test.ts +456 -0
  110. package/src/ts/__tests__/SlopeSignChange.test.ts +166 -0
  111. package/src/ts/__tests__/Tap.test.ts +164 -0
  112. package/src/ts/__tests__/TimeBasedExpiration.test.ts +124 -0
  113. package/src/ts/__tests__/TimeBasedRmsAndMav.test.ts +231 -0
  114. package/src/ts/__tests__/TimeBasedVarianceAndZScore.test.ts +284 -0
  115. package/src/ts/__tests__/TimeSeries.test.ts +254 -0
  116. package/src/ts/__tests__/TopicRouter.test.ts +332 -0
  117. package/src/ts/__tests__/TopicRouterAdvanced.test.ts +483 -0
  118. package/src/ts/__tests__/TopicRouterPriority.test.ts +487 -0
  119. package/src/ts/__tests__/Variance.test.ts +509 -0
  120. package/src/ts/__tests__/WaveformLength.test.ts +147 -0
  121. package/src/ts/__tests__/WillisonAmplitude.test.ts +197 -0
  122. package/src/ts/__tests__/ZScoreNormalize.test.ts +459 -0
  123. package/src/ts/advanced-dsp.ts +566 -0
  124. package/src/ts/backends.ts +1137 -0
  125. package/src/ts/bindings.ts +1225 -0
  126. package/src/ts/easter-egg.ts +42 -0
  127. package/src/ts/examples/MeanAbsoluteValue/test-state.ts +99 -0
  128. package/src/ts/examples/MeanAbsoluteValue/test-streaming.ts +269 -0
  129. package/src/ts/examples/MovingAverage/test-state.ts +85 -0
  130. package/src/ts/examples/MovingAverage/test-streaming.ts +188 -0
  131. package/src/ts/examples/RMS/test-state.ts +97 -0
  132. package/src/ts/examples/RMS/test-streaming.ts +253 -0
  133. package/src/ts/examples/Rectify/test-state.ts +107 -0
  134. package/src/ts/examples/Rectify/test-streaming.ts +242 -0
  135. package/src/ts/examples/Variance/test-state.ts +195 -0
  136. package/src/ts/examples/Variance/test-streaming.ts +260 -0
  137. package/src/ts/examples/ZScoreNormalize/test-state.ts +277 -0
  138. package/src/ts/examples/ZScoreNormalize/test-streaming.ts +306 -0
  139. package/src/ts/examples/advanced-dsp-examples.ts +397 -0
  140. package/src/ts/examples/callbacks/advanced-router-features.ts +326 -0
  141. package/src/ts/examples/callbacks/benchmark-circular-buffer.ts +109 -0
  142. package/src/ts/examples/callbacks/monitoring-example.ts +265 -0
  143. package/src/ts/examples/callbacks/pipeline-callbacks-example.ts +137 -0
  144. package/src/ts/examples/callbacks/pooled-callbacks-example.ts +274 -0
  145. package/src/ts/examples/callbacks/priority-routing-example.ts +277 -0
  146. package/src/ts/examples/callbacks/production-topic-router.ts +214 -0
  147. package/src/ts/examples/callbacks/topic-based-logging.ts +161 -0
  148. package/src/ts/examples/chaining/test-chaining-redis.ts +113 -0
  149. package/src/ts/examples/chaining/test-chaining.ts +52 -0
  150. package/src/ts/examples/emg-features-example.ts +284 -0
  151. package/src/ts/examples/fft-example.ts +309 -0
  152. package/src/ts/examples/fft-examples.ts +349 -0
  153. package/src/ts/examples/filter-examples.ts +320 -0
  154. package/src/ts/examples/list-state-example.ts +131 -0
  155. package/src/ts/examples/logger-example.ts +91 -0
  156. package/src/ts/examples/notch-filter-examples.ts +243 -0
  157. package/src/ts/examples/phase5/drift-detection-example.ts +290 -0
  158. package/src/ts/examples/phase6-7/production-observability.ts +476 -0
  159. package/src/ts/examples/phase6-7/redis-timeseries-integration.ts +446 -0
  160. package/src/ts/examples/redis/redis-example.ts +202 -0
  161. package/src/ts/examples/redis-example.ts +202 -0
  162. package/src/ts/examples/simd-benchmark.ts +126 -0
  163. package/src/ts/examples/tap-debugging.ts +230 -0
  164. package/src/ts/examples/timeseries/comparison-example.ts +290 -0
  165. package/src/ts/examples/timeseries/iot-sensor-example.ts +143 -0
  166. package/src/ts/examples/timeseries/redis-streaming-example.ts +233 -0
  167. package/src/ts/examples/waveform-length-example.ts +139 -0
  168. package/src/ts/fft.ts +722 -0
  169. package/src/ts/filters.ts +1078 -0
  170. package/src/ts/index.ts +120 -0
  171. package/src/ts/types.ts +589 -0
  172. package/tsconfig.json +15 -0
package/src/ts/fft.ts ADDED
@@ -0,0 +1,722 @@
1
+ /**
2
+ * FFT/DFT TypeScript Bindings
3
+ *
4
+ * Provides all 8 Fourier transforms with full type safety:
5
+ * - FFT/IFFT: Fast Fourier Transform (complex, O(N log N))
6
+ * - DFT/IDFT: Discrete Fourier Transform (complex, O(N²))
7
+ * - RFFT/IRFFT: Real-input FFT (outputs N/2+1 bins)
8
+ * - RDFT/IRDFT: Real-input DFT (outputs N/2+1 bins)
9
+ *
10
+ * Plus moving/batched FFT for streaming applications
11
+ */
12
+
13
+ import { createRequire } from "node:module";
14
+ import { fileURLToPath } from "node:url";
15
+ import { dirname, join } from "node:path";
16
+
17
+ const __filename = fileURLToPath(import.meta.url);
18
+ const __dirname = dirname(__filename);
19
+ const require = createRequire(import.meta.url);
20
+
21
+ // Try multiple paths to find the native module
22
+ let DspAddon: any;
23
+ const possiblePaths = [
24
+ join(__dirname, "../build/dspx.node"),
25
+ join(__dirname, "../../build/Release/dspx.node"),
26
+ join(__dirname, "../../prebuilds/win32-x64/dsp-js-native.node"),
27
+ ];
28
+
29
+ for (const path of possiblePaths) {
30
+ try {
31
+ DspAddon = require(path);
32
+ break;
33
+ } catch (err) {
34
+ // Continue to next path
35
+ }
36
+ }
37
+
38
+ if (!DspAddon) {
39
+ throw new Error(
40
+ "Could not load native module. Tried paths:\n" + possiblePaths.join("\n")
41
+ );
42
+ }
43
+
44
+ /**
45
+ * Complex number representation
46
+ */
47
+ export interface ComplexArray {
48
+ /** Real part */
49
+ real: Float32Array;
50
+ /** Imaginary part */
51
+ imag: Float32Array;
52
+ }
53
+
54
+ /**
55
+ * Window function types for spectral analysis
56
+ */
57
+ export type WindowType =
58
+ | "none" // Rectangular (no windowing)
59
+ | "hann" // Hann window (cosine taper)
60
+ | "hamming" // Hamming window
61
+ | "blackman" // Blackman window (better sidelobe rejection)
62
+ | "bartlett"; // Triangular window
63
+
64
+ /**
65
+ * FFT processing mode
66
+ */
67
+ export type FftMode =
68
+ | "moving" // Sliding window, updates on every sample
69
+ | "batched"; // Process complete frames
70
+
71
+ /**
72
+ * FFT Processor - Core transform engine
73
+ *
74
+ * **IMPORTANT: Radix-2 (Power-of-2) Requirement**
75
+ *
76
+ * The FFT/IFFT/RFFT/IRFFT transforms use the **Cooley-Tukey radix-2 algorithm**,
77
+ * which requires the input size to be a power of 2 (e.g., 64, 128, 256, 512, 1024, 2048, 4096, ...).
78
+ *
79
+ * If your data length is not a power of 2:
80
+ * 1. **Use DFT/IDFT/RDFT/IRDFT** - These work with any size but are slower (O(N²) vs O(N log N))
81
+ * 2. **Zero-pad your signal** - Use `FftUtils.padToPowerOfTwo()` to automatically pad to next power of 2
82
+ * 3. **Truncate or resample** - Adjust your signal to match a power-of-2 size
83
+ *
84
+ * @example
85
+ * ```ts
86
+ * // Example 1: Direct use with power-of-2 size
87
+ * const fft = new FftProcessor(512);
88
+ * const signal = new Float32Array(512);
89
+ * const spectrum = fft.rfft(signal); // Fast O(N log N)
90
+ *
91
+ * // Example 2: Auto-padding for non-power-of-2 signals
92
+ * const rawSignal = new Float32Array(1000); // Not power of 2!
93
+ * const padded = FftUtils.padToPowerOfTwo(rawSignal); // Pads to 1024
94
+ * const fft2 = new FftProcessor(padded.length);
95
+ * const spectrum2 = fft2.rfft(padded);
96
+ *
97
+ * // Example 3: Use DFT for arbitrary sizes
98
+ * const fft3 = new FftProcessor(1000); // Any size
99
+ * const complexIn = { real: new Float32Array(1000), imag: new Float32Array(1000) };
100
+ * const spectrum3 = fft3.rdft(rawSignal); // Slower O(N²) but no padding needed
101
+ *
102
+ * // Get magnitude spectrum
103
+ * const magnitudes = fft.getMagnitude(spectrum);
104
+ *
105
+ * // Inverse transform
106
+ * const reconstructed = fft.irfft(spectrum);
107
+ * ```
108
+ */
109
+ export class FftProcessor {
110
+ private native: any;
111
+
112
+ /**
113
+ * Create FFT processor
114
+ *
115
+ * @param size FFT size
116
+ * - For FFT/IFFT/RFFT/IRFFT: **MUST be power of 2** (64, 128, 256, 512, 1024, 2048, 4096, ...)
117
+ * - For DFT/IDFT/RDFT/IRDFT: Can be any positive integer
118
+ *
119
+ * @throws {Error} If size is not positive
120
+ * @throws {Error} If using FFT/RFFT methods with non-power-of-2 size
121
+ */
122
+ constructor(size: number) {
123
+ this.native = new DspAddon.FftProcessor(size);
124
+ }
125
+
126
+ // ========== Complex Transforms ==========
127
+
128
+ /**
129
+ * Forward FFT (complex -> complex)
130
+ *
131
+ * Computes: X[k] = Σ x[n] * e^(-j2πkn/N)
132
+ *
133
+ * Time complexity: O(N log N)
134
+ * Requires: size must be power of 2
135
+ *
136
+ * @param input Complex input signal { real, imag }
137
+ * @returns Complex frequency spectrum
138
+ */
139
+ fft(input: ComplexArray): ComplexArray {
140
+ return this.native.fft(input);
141
+ }
142
+
143
+ /**
144
+ * Inverse FFT (complex -> complex)
145
+ *
146
+ * Computes: x[n] = (1/N) * Σ X[k] * e^(j2πkn/N)
147
+ *
148
+ * @param spectrum Complex frequency spectrum
149
+ * @returns Complex time-domain signal
150
+ */
151
+ ifft(spectrum: ComplexArray): ComplexArray {
152
+ return this.native.ifft(spectrum);
153
+ }
154
+
155
+ /**
156
+ * Forward DFT (complex -> complex)
157
+ *
158
+ * Direct computation, slower but works for any size
159
+ * Time complexity: O(N²)
160
+ *
161
+ * @param input Complex input signal
162
+ * @returns Complex frequency spectrum
163
+ */
164
+ dft(input: ComplexArray): ComplexArray {
165
+ return this.native.dft(input);
166
+ }
167
+
168
+ /**
169
+ * Inverse DFT (complex -> complex)
170
+ *
171
+ * @param spectrum Complex frequency spectrum
172
+ * @returns Complex time-domain signal
173
+ */
174
+ idft(spectrum: ComplexArray): ComplexArray {
175
+ return this.native.idft(spectrum);
176
+ }
177
+
178
+ // ========== Real-Input Transforms ==========
179
+
180
+ /**
181
+ * Forward RFFT (real -> complex half-spectrum)
182
+ *
183
+ * Exploits Hermitian symmetry for real inputs: X[k] = X*[N-k]
184
+ * Returns only positive frequencies (N/2+1 bins)
185
+ *
186
+ * Time complexity: O(N log N)
187
+ * Output size: N/2 + 1 (includes DC and Nyquist)
188
+ *
189
+ * @param input Real input signal (size N)
190
+ * @returns Complex half-spectrum (size N/2+1)
191
+ *
192
+ * @example
193
+ * ```ts
194
+ * const fft = new FftProcessor(1024);
195
+ * const signal = new Float32Array(1024);
196
+ * const spectrum = fft.rfft(signal);
197
+ * // spectrum has 513 bins (DC + 512 positive frequencies)
198
+ * ```
199
+ */
200
+ rfft(input: Float32Array): ComplexArray {
201
+ return this.native.rfft(input);
202
+ }
203
+
204
+ /**
205
+ * Inverse RFFT (complex half-spectrum -> real)
206
+ *
207
+ * Reconstructs real signal from half spectrum using Hermitian symmetry
208
+ *
209
+ * @param spectrum Complex half-spectrum (size N/2+1)
210
+ * @returns Real time-domain signal (size N)
211
+ */
212
+ irfft(spectrum: ComplexArray): Float32Array {
213
+ return this.native.irfft(spectrum);
214
+ }
215
+
216
+ /**
217
+ * Forward RDFT (real -> complex half-spectrum)
218
+ *
219
+ * Direct computation version of RFFT
220
+ * Time complexity: O(N²)
221
+ *
222
+ * @param input Real input signal
223
+ * @returns Complex half-spectrum
224
+ */
225
+ rdft(input: Float32Array): ComplexArray {
226
+ return this.native.rdft(input);
227
+ }
228
+
229
+ /**
230
+ * Inverse RDFT (complex half-spectrum -> real)
231
+ *
232
+ * Direct computation version of IRFFT
233
+ *
234
+ * @param spectrum Complex half-spectrum
235
+ * @returns Real time-domain signal
236
+ */
237
+ irdft(spectrum: ComplexArray): Float32Array {
238
+ return this.native.irdft(spectrum);
239
+ }
240
+
241
+ // ========== Utility Methods ==========
242
+
243
+ /**
244
+ * Get FFT size
245
+ */
246
+ getSize(): number {
247
+ return this.native.getSize();
248
+ }
249
+
250
+ /**
251
+ * Get half-spectrum size (for real transforms)
252
+ * Returns N/2 + 1
253
+ */
254
+ getHalfSize(): number {
255
+ return this.native.getHalfSize();
256
+ }
257
+
258
+ /**
259
+ * Check if FFT size is power of 2
260
+ */
261
+ isPowerOfTwo(): boolean {
262
+ return this.native.isPowerOfTwo();
263
+ }
264
+
265
+ /**
266
+ * Get magnitude spectrum from complex spectrum
267
+ *
268
+ * Computes: |X[k]| = sqrt(Re²(X[k]) + Im²(X[k]))
269
+ *
270
+ * @param spectrum Complex spectrum
271
+ * @returns Magnitude array
272
+ */
273
+ getMagnitude(spectrum: ComplexArray): Float32Array {
274
+ return this.native.getMagnitude(spectrum);
275
+ }
276
+
277
+ /**
278
+ * Get phase spectrum from complex spectrum
279
+ *
280
+ * Computes: ∠X[k] = atan2(Im(X[k]), Re(X[k]))
281
+ *
282
+ * @param spectrum Complex spectrum
283
+ * @returns Phase array (radians, -π to π)
284
+ */
285
+ getPhase(spectrum: ComplexArray): Float32Array {
286
+ return this.native.getPhase(spectrum);
287
+ }
288
+
289
+ /**
290
+ * Get power spectrum (magnitude squared)
291
+ *
292
+ * Computes: P[k] = |X[k]|²
293
+ *
294
+ * @param spectrum Complex spectrum
295
+ * @returns Power array
296
+ */
297
+ getPower(spectrum: ComplexArray): Float32Array {
298
+ return this.native.getPower(spectrum);
299
+ }
300
+
301
+ /**
302
+ * Compute frequency bins for spectrum
303
+ *
304
+ * @param sampleRate Sample rate in Hz
305
+ * @returns Frequency array in Hz
306
+ *
307
+ * @example
308
+ * ```ts
309
+ * const fft = new FftProcessor(1024);
310
+ * const freqs = fft.getFrequencyBins(44100); // 44.1 kHz sample rate
311
+ * // freqs[0] = 0 Hz (DC)
312
+ * // freqs[1] = 43.07 Hz
313
+ * // freqs[512] = 22050 Hz (Nyquist)
314
+ * ```
315
+ */
316
+ getFrequencyBins(sampleRate: number): Float32Array {
317
+ const size = this.isPowerOfTwo() ? this.getHalfSize() : this.getSize();
318
+ const freqs = new Float32Array(size);
319
+ const binWidth = sampleRate / this.getSize();
320
+
321
+ for (let i = 0; i < size; i++) {
322
+ freqs[i] = i * binWidth;
323
+ }
324
+
325
+ return freqs;
326
+ }
327
+ }
328
+
329
+ /**
330
+ * Moving FFT Processor - Streaming/batched transforms
331
+ *
332
+ * Provides sliding-window and frame-based FFT processing:
333
+ * - Moving mode: Updates spectrum on every sample
334
+ * - Batched mode: Processes complete frames with hop size
335
+ * - **Automatic windowing** to reduce spectral leakage
336
+ * - Overlap-add support
337
+ *
338
+ * **Windowing for Spectral Leakage Reduction:**
339
+ *
340
+ * When performing FFT on finite-length signals, discontinuities at the boundaries
341
+ * cause **spectral leakage** - energy from one frequency bin "leaking" into others.
342
+ * Window functions taper the signal at the edges to reduce this effect.
343
+ *
344
+ * Available window types:
345
+ * - `none`: Rectangular (no windowing) - fastest but most leakage
346
+ * - `hann`: Hann window - good general-purpose choice (default for audio)
347
+ * - `hamming`: Hamming window - slightly better frequency resolution than Hann
348
+ * - `blackman`: Blackman window - best sidelobe rejection, wider main lobe
349
+ * - `bartlett`: Triangular window - simple linear taper
350
+ *
351
+ * **Choosing a window:**
352
+ * - Audio analysis: Use `hann` (most common)
353
+ * - Narrowband signals: Use `hamming`
354
+ * - Wideband signals with interfering tones: Use `blackman`
355
+ * - Quick testing: Use `none` (but expect leakage)
356
+ *
357
+ * Uses native C++ implementation for high performance.
358
+ *
359
+ * @example
360
+ * ```ts
361
+ * // Batched processing with 50% overlap and Hann windowing
362
+ * const movingFft = new MovingFftProcessor({
363
+ * fftSize: 2048,
364
+ * hopSize: 1024,
365
+ * mode: "batched",
366
+ * windowType: "hann" // Reduces spectral leakage!
367
+ * });
368
+ *
369
+ * // Stream audio samples
370
+ * const samples = new Float32Array(4096);
371
+ * movingFft.addSamples(samples, (spectrum, size) => {
372
+ * console.log(`Spectrum ready: ${size} bins`);
373
+ * });
374
+ *
375
+ * // Compare windowing effects
376
+ * const noWindow = new MovingFftProcessor({ fftSize: 1024, windowType: "none" });
377
+ * const hannWindow = new MovingFftProcessor({ fftSize: 1024, windowType: "hann" });
378
+ * // hannWindow will show much cleaner spectral peaks!
379
+ * ```
380
+ */
381
+ export class MovingFftProcessor {
382
+ private native: any;
383
+
384
+ /**
385
+ * Create Moving FFT processor
386
+ *
387
+ * @param options Configuration object
388
+ * @param options.fftSize FFT size (must be power of 2 for FFT, any size for DFT)
389
+ * @param options.hopSize Hop size in samples (default: fftSize, i.e., no overlap)
390
+ * @param options.mode Processing mode (default: "batched")
391
+ * @param options.windowType Window function (default: "hann" for spectral leakage reduction)
392
+ * @param options.realInput Use real-input transforms (default: true)
393
+ *
394
+ * @throws {Error} If fftSize is invalid
395
+ * @throws {Error} If hopSize > fftSize
396
+ *
397
+ * @example
398
+ * ```ts
399
+ * // Audio spectral analysis with 75% overlap
400
+ * const audioFFT = new MovingFftProcessor({
401
+ * fftSize: 2048,
402
+ * hopSize: 512, // 75% overlap
403
+ * windowType: "hann" // Reduce spectral leakage
404
+ * });
405
+ *
406
+ * // Vibration analysis with Blackman window
407
+ * const vibrationFFT = new MovingFftProcessor({
408
+ * fftSize: 4096,
409
+ * hopSize: 4096, // No overlap
410
+ * windowType: "blackman" // Best sidelobe rejection
411
+ * });
412
+ * ```
413
+ */
414
+ constructor(options: {
415
+ fftSize: number;
416
+ hopSize?: number;
417
+ mode?: FftMode;
418
+ windowType?: WindowType;
419
+ realInput?: boolean;
420
+ }) {
421
+ // Build options object with only defined properties
422
+ const nativeOptions: any = {
423
+ fftSize: options.fftSize,
424
+ realInput: options.realInput ?? true,
425
+ };
426
+
427
+ if (options.hopSize !== undefined) {
428
+ nativeOptions.hopSize = options.hopSize;
429
+ }
430
+
431
+ if (options.mode !== undefined) {
432
+ nativeOptions.mode = options.mode;
433
+ }
434
+
435
+ if (options.windowType !== undefined) {
436
+ nativeOptions.windowType = options.windowType;
437
+ }
438
+
439
+ this.native = new DspAddon.MovingFftProcessor(nativeOptions);
440
+ }
441
+
442
+ /**
443
+ * Add single sample and optionally compute FFT
444
+ *
445
+ * @param sample Input sample
446
+ * @returns Spectrum if computed, null otherwise
447
+ */
448
+ addSample(sample: number): ComplexArray | null {
449
+ return this.native.addSample(sample);
450
+ }
451
+
452
+ /**
453
+ * Add batch of samples
454
+ *
455
+ * @param samples Input samples
456
+ * @param callback Called for each computed spectrum
457
+ * @returns Number of spectra computed
458
+ */
459
+ addSamples(
460
+ samples: Float32Array,
461
+ callback?: (spectrum: ComplexArray, size: number) => void
462
+ ): number {
463
+ if (!callback) {
464
+ // If no callback, just process and return count
465
+ return this.native.addSamples(samples, () => {});
466
+ }
467
+ return this.native.addSamples(samples, callback);
468
+ }
469
+
470
+ /**
471
+ * Force compute spectrum from current buffer
472
+ */
473
+ computeSpectrum(): ComplexArray {
474
+ return this.native.computeSpectrum();
475
+ }
476
+
477
+ /**
478
+ * Reset processor state
479
+ */
480
+ reset(): void {
481
+ this.native.reset();
482
+ }
483
+
484
+ /**
485
+ * Get FFT size
486
+ */
487
+ getFftSize(): number {
488
+ return this.native.getFftSize();
489
+ }
490
+
491
+ /**
492
+ * Get spectrum size (N/2+1 for real, N for complex)
493
+ */
494
+ getSpectrumSize(): number {
495
+ return this.native.getSpectrumSize();
496
+ }
497
+
498
+ /**
499
+ * Get hop size
500
+ */
501
+ getHopSize(): number {
502
+ return this.native.getHopSize();
503
+ }
504
+
505
+ /**
506
+ * Get buffer fill level
507
+ */
508
+ getFillLevel(): number {
509
+ return this.native.getFillLevel();
510
+ }
511
+
512
+ /**
513
+ * Check if ready to compute FFT
514
+ */
515
+ isReady(): boolean {
516
+ return this.native.isReady();
517
+ }
518
+
519
+ /**
520
+ * Set window type
521
+ */
522
+ setWindowType(type: WindowType): void {
523
+ this.native.setWindowType(type);
524
+ }
525
+
526
+ /**
527
+ * Get magnitude spectrum
528
+ */
529
+ getMagnitudeSpectrum(): Float32Array {
530
+ return this.native.getMagnitudeSpectrum();
531
+ }
532
+
533
+ /**
534
+ * Get power spectrum
535
+ */
536
+ getPowerSpectrum(): Float32Array {
537
+ return this.native.getPowerSpectrum();
538
+ }
539
+
540
+ /**
541
+ * Get phase spectrum
542
+ */
543
+ getPhaseSpectrum(): Float32Array {
544
+ return this.native.getPhaseSpectrum();
545
+ }
546
+
547
+ /**
548
+ * Get frequency bins
549
+ */
550
+ getFrequencyBins(sampleRate: number): Float32Array {
551
+ return this.native.getFrequencyBins(sampleRate);
552
+ }
553
+ }
554
+
555
+ /**
556
+ * Helper functions for common FFT operations
557
+ */
558
+ export namespace FftUtils {
559
+ /**
560
+ * Pad signal to next power of 2 with zeros
561
+ *
562
+ * This is the recommended approach for using FFT with arbitrary-length signals.
563
+ * Zero-padding allows you to use the fast FFT algorithm (O(N log N)) instead of
564
+ * the slower DFT (O(N²)).
565
+ *
566
+ * **Note on spectral resolution:**
567
+ * - Zero-padding does NOT increase spectral resolution
568
+ * - It only increases the number of frequency bins (interpolation)
569
+ * - True resolution is still limited by original signal length
570
+ *
571
+ * @param signal Input signal (any length)
572
+ * @returns Zero-padded signal (power-of-2 length)
573
+ *
574
+ * @example
575
+ * ```ts
576
+ * const signal = new Float32Array(1000); // Not power of 2
577
+ * const padded = FftUtils.padToPowerOfTwo(signal); // 1024 samples
578
+ * const fft = new FftProcessor(padded.length);
579
+ * const spectrum = fft.rfft(padded);
580
+ * ```
581
+ */
582
+ export function padToPowerOfTwo(signal: Float32Array): Float32Array {
583
+ const nextPow2 = nextPowerOfTwo(signal.length);
584
+
585
+ if (nextPow2 === signal.length) {
586
+ // Already power of 2, return as-is
587
+ return signal;
588
+ }
589
+
590
+ // Create zero-padded array
591
+ const padded = new Float32Array(nextPow2);
592
+ padded.set(signal);
593
+
594
+ return padded;
595
+ }
596
+
597
+ /**
598
+ * Check if number is a power of 2
599
+ *
600
+ * @param n Number to check
601
+ * @returns True if n is a power of 2
602
+ *
603
+ * @example
604
+ * ```ts
605
+ * FftUtils.isPowerOfTwo(512); // true
606
+ * FftUtils.isPowerOfTwo(1000); // false
607
+ * FftUtils.isPowerOfTwo(1024); // true
608
+ * ```
609
+ */
610
+ export function isPowerOfTwo(n: number): boolean {
611
+ return n > 0 && (n & (n - 1)) === 0;
612
+ }
613
+
614
+ /**
615
+ * Find peak frequency in spectrum
616
+ *
617
+ * @param magnitudes Magnitude spectrum
618
+ * @param sampleRate Sample rate in Hz
619
+ * @param fftSize FFT size
620
+ * @returns Peak frequency in Hz
621
+ */
622
+ export function findPeakFrequency(
623
+ magnitudes: Float32Array,
624
+ sampleRate: number,
625
+ fftSize: number
626
+ ): number {
627
+ let maxIdx = 0;
628
+ let maxVal = magnitudes[0];
629
+
630
+ for (let i = 1; i < magnitudes.length; i++) {
631
+ if (magnitudes[i] > maxVal) {
632
+ maxVal = magnitudes[i];
633
+ maxIdx = i;
634
+ }
635
+ }
636
+
637
+ return (maxIdx * sampleRate) / fftSize;
638
+ }
639
+
640
+ /**
641
+ * Convert magnitude spectrum to decibels
642
+ *
643
+ * @param magnitudes Magnitude spectrum
644
+ * @param refLevel Reference level (default: 1.0)
645
+ * @returns Spectrum in dB
646
+ */
647
+ export function toDecibels(
648
+ magnitudes: Float32Array,
649
+ refLevel: number = 1.0
650
+ ): Float32Array {
651
+ const db = new Float32Array(magnitudes.length);
652
+
653
+ for (let i = 0; i < magnitudes.length; i++) {
654
+ db[i] = 20 * Math.log10(Math.max(magnitudes[i], 1e-10) / refLevel);
655
+ }
656
+
657
+ return db;
658
+ }
659
+
660
+ /**
661
+ * Apply A-weighting to frequency spectrum (perceptual audio)
662
+ *
663
+ * @param magnitudes Magnitude spectrum
664
+ * @param frequencies Frequency bins in Hz
665
+ * @returns A-weighted magnitudes
666
+ */
667
+ export function applyAWeighting(
668
+ magnitudes: Float32Array,
669
+ frequencies: Float32Array
670
+ ): Float32Array {
671
+ const weighted = new Float32Array(magnitudes.length);
672
+
673
+ for (let i = 0; i < magnitudes.length; i++) {
674
+ const f = frequencies[i];
675
+ const f2 = f * f;
676
+ const f4 = f2 * f2;
677
+
678
+ // A-weighting formula
679
+ const numerator = 12194 * 12194 * f4;
680
+ const denominator =
681
+ (f2 + 20.6 * 20.6) *
682
+ Math.sqrt((f2 + 107.7 * 107.7) * (f2 + 737.9 * 737.9)) *
683
+ (f2 + 12194 * 12194);
684
+
685
+ const weight = numerator / denominator;
686
+ weighted[i] = magnitudes[i] * weight;
687
+ }
688
+
689
+ return weighted;
690
+ }
691
+
692
+ /**
693
+ * Compute next power of 2
694
+ */
695
+ export function nextPowerOfTwo(n: number): number {
696
+ if (n <= 0) return 1;
697
+
698
+ let power = 1;
699
+ while (power < n) {
700
+ power *= 2;
701
+ }
702
+
703
+ return power;
704
+ }
705
+
706
+ /**
707
+ * Zero-pad signal to target length
708
+ */
709
+ export function zeroPad(
710
+ signal: Float32Array,
711
+ targetLength: number
712
+ ): Float32Array {
713
+ if (signal.length >= targetLength) {
714
+ return signal;
715
+ }
716
+
717
+ const padded = new Float32Array(targetLength);
718
+ padded.set(signal);
719
+
720
+ return padded;
721
+ }
722
+ }