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.
- package/.github/workflows/ci.yml +185 -0
- package/.vscode/c_cpp_properties.json +17 -0
- package/.vscode/settings.json +68 -0
- package/.vscode/tasks.json +28 -0
- package/DISCLAIMER.md +32 -0
- package/LICENSE +21 -0
- package/README.md +1803 -0
- package/ROADMAP.md +192 -0
- package/TECHNICAL_DEBT.md +165 -0
- package/binding.gyp +65 -0
- package/docs/ADVANCED_LOGGER_FEATURES.md +598 -0
- package/docs/AUTHENTICATION_SECURITY.md +396 -0
- package/docs/BACKEND_IMPROVEMENTS.md +399 -0
- package/docs/CHEBYSHEV_BIQUAD_EQ_IMPLEMENTATION.md +405 -0
- package/docs/FFT_IMPLEMENTATION.md +490 -0
- package/docs/FFT_IMPROVEMENTS_SUMMARY.md +387 -0
- package/docs/FFT_USER_GUIDE.md +494 -0
- package/docs/FILTERS_IMPLEMENTATION.md +260 -0
- package/docs/FILTER_API_GUIDE.md +418 -0
- package/docs/FIR_SIMD_OPTIMIZATION.md +175 -0
- package/docs/LOGGER_API_REFERENCE.md +350 -0
- package/docs/NOTCH_FILTER_QUICK_REF.md +121 -0
- package/docs/PHASE2_TESTS_AND_NOTCH_FILTER.md +341 -0
- package/docs/PHASES_5_7_SUMMARY.md +403 -0
- package/docs/PIPELINE_FILTER_INTEGRATION.md +446 -0
- package/docs/SIMD_OPTIMIZATIONS.md +211 -0
- package/docs/TEST_MIGRATION_SUMMARY.md +173 -0
- package/docs/TIMESERIES_IMPLEMENTATION_SUMMARY.md +322 -0
- package/docs/TIMESERIES_QUICK_REF.md +85 -0
- package/docs/advanced.md +559 -0
- package/docs/time-series-guide.md +617 -0
- package/docs/time-series-migration.md +376 -0
- package/jest.config.js +37 -0
- package/package.json +42 -0
- package/prebuilds/linux-x64/dsp-ts-redis.node +0 -0
- package/prebuilds/win32-x64/dsp-ts-redis.node +0 -0
- package/scripts/test.js +24 -0
- package/src/build/dsp-ts-redis.node +0 -0
- package/src/native/DspPipeline.cc +675 -0
- package/src/native/DspPipeline.h +44 -0
- package/src/native/FftBindings.cc +817 -0
- package/src/native/FilterBindings.cc +1001 -0
- package/src/native/IDspStage.h +53 -0
- package/src/native/adapters/InterpolatorStage.h +201 -0
- package/src/native/adapters/MeanAbsoluteValueStage.h +289 -0
- package/src/native/adapters/MovingAverageStage.h +306 -0
- package/src/native/adapters/RectifyStage.h +88 -0
- package/src/native/adapters/ResamplerStage.h +238 -0
- package/src/native/adapters/RmsStage.h +299 -0
- package/src/native/adapters/SscStage.h +121 -0
- package/src/native/adapters/VarianceStage.h +307 -0
- package/src/native/adapters/WampStage.h +114 -0
- package/src/native/adapters/WaveformLengthStage.h +115 -0
- package/src/native/adapters/ZScoreNormalizeStage.h +326 -0
- package/src/native/core/FftEngine.cc +441 -0
- package/src/native/core/FftEngine.h +224 -0
- package/src/native/core/FirFilter.cc +324 -0
- package/src/native/core/FirFilter.h +149 -0
- package/src/native/core/IirFilter.cc +576 -0
- package/src/native/core/IirFilter.h +210 -0
- package/src/native/core/MovingAbsoluteValueFilter.cc +17 -0
- package/src/native/core/MovingAbsoluteValueFilter.h +135 -0
- package/src/native/core/MovingAverageFilter.cc +18 -0
- package/src/native/core/MovingAverageFilter.h +135 -0
- package/src/native/core/MovingFftFilter.cc +291 -0
- package/src/native/core/MovingFftFilter.h +203 -0
- package/src/native/core/MovingVarianceFilter.cc +194 -0
- package/src/native/core/MovingVarianceFilter.h +114 -0
- package/src/native/core/MovingZScoreFilter.cc +215 -0
- package/src/native/core/MovingZScoreFilter.h +113 -0
- package/src/native/core/Policies.h +352 -0
- package/src/native/core/RmsFilter.cc +18 -0
- package/src/native/core/RmsFilter.h +131 -0
- package/src/native/core/SscFilter.cc +16 -0
- package/src/native/core/SscFilter.h +137 -0
- package/src/native/core/WampFilter.cc +16 -0
- package/src/native/core/WampFilter.h +101 -0
- package/src/native/core/WaveformLengthFilter.cc +17 -0
- package/src/native/core/WaveformLengthFilter.h +98 -0
- package/src/native/utils/CircularBufferArray.cc +336 -0
- package/src/native/utils/CircularBufferArray.h +62 -0
- package/src/native/utils/CircularBufferVector.cc +145 -0
- package/src/native/utils/CircularBufferVector.h +45 -0
- package/src/native/utils/NapiUtils.cc +53 -0
- package/src/native/utils/NapiUtils.h +21 -0
- package/src/native/utils/SimdOps.h +870 -0
- package/src/native/utils/SlidingWindowFilter.cc +239 -0
- package/src/native/utils/SlidingWindowFilter.h +159 -0
- package/src/native/utils/TimeSeriesBuffer.cc +205 -0
- package/src/native/utils/TimeSeriesBuffer.h +140 -0
- package/src/ts/CircularLogBuffer.ts +87 -0
- package/src/ts/DriftDetector.ts +331 -0
- package/src/ts/TopicRouter.ts +428 -0
- package/src/ts/__tests__/AdvancedDsp.test.ts +585 -0
- package/src/ts/__tests__/AuthAndEdgeCases.test.ts +241 -0
- package/src/ts/__tests__/Chaining.test.ts +387 -0
- package/src/ts/__tests__/ChebyshevBiquad.test.ts +229 -0
- package/src/ts/__tests__/CircularLogBuffer.test.ts +158 -0
- package/src/ts/__tests__/DriftDetector.test.ts +389 -0
- package/src/ts/__tests__/Fft.test.ts +484 -0
- package/src/ts/__tests__/ListState.test.ts +153 -0
- package/src/ts/__tests__/Logger.test.ts +208 -0
- package/src/ts/__tests__/LoggerAdvanced.test.ts +319 -0
- package/src/ts/__tests__/LoggerMinor.test.ts +247 -0
- package/src/ts/__tests__/MeanAbsoluteValue.test.ts +398 -0
- package/src/ts/__tests__/MovingAverage.test.ts +322 -0
- package/src/ts/__tests__/RMS.test.ts +315 -0
- package/src/ts/__tests__/Rectify.test.ts +272 -0
- package/src/ts/__tests__/Redis.test.ts +456 -0
- package/src/ts/__tests__/SlopeSignChange.test.ts +166 -0
- package/src/ts/__tests__/Tap.test.ts +164 -0
- package/src/ts/__tests__/TimeBasedExpiration.test.ts +124 -0
- package/src/ts/__tests__/TimeBasedRmsAndMav.test.ts +231 -0
- package/src/ts/__tests__/TimeBasedVarianceAndZScore.test.ts +284 -0
- package/src/ts/__tests__/TimeSeries.test.ts +254 -0
- package/src/ts/__tests__/TopicRouter.test.ts +332 -0
- package/src/ts/__tests__/TopicRouterAdvanced.test.ts +483 -0
- package/src/ts/__tests__/TopicRouterPriority.test.ts +487 -0
- package/src/ts/__tests__/Variance.test.ts +509 -0
- package/src/ts/__tests__/WaveformLength.test.ts +147 -0
- package/src/ts/__tests__/WillisonAmplitude.test.ts +197 -0
- package/src/ts/__tests__/ZScoreNormalize.test.ts +459 -0
- package/src/ts/advanced-dsp.ts +566 -0
- package/src/ts/backends.ts +1137 -0
- package/src/ts/bindings.ts +1225 -0
- package/src/ts/easter-egg.ts +42 -0
- package/src/ts/examples/MeanAbsoluteValue/test-state.ts +99 -0
- package/src/ts/examples/MeanAbsoluteValue/test-streaming.ts +269 -0
- package/src/ts/examples/MovingAverage/test-state.ts +85 -0
- package/src/ts/examples/MovingAverage/test-streaming.ts +188 -0
- package/src/ts/examples/RMS/test-state.ts +97 -0
- package/src/ts/examples/RMS/test-streaming.ts +253 -0
- package/src/ts/examples/Rectify/test-state.ts +107 -0
- package/src/ts/examples/Rectify/test-streaming.ts +242 -0
- package/src/ts/examples/Variance/test-state.ts +195 -0
- package/src/ts/examples/Variance/test-streaming.ts +260 -0
- package/src/ts/examples/ZScoreNormalize/test-state.ts +277 -0
- package/src/ts/examples/ZScoreNormalize/test-streaming.ts +306 -0
- package/src/ts/examples/advanced-dsp-examples.ts +397 -0
- package/src/ts/examples/callbacks/advanced-router-features.ts +326 -0
- package/src/ts/examples/callbacks/benchmark-circular-buffer.ts +109 -0
- package/src/ts/examples/callbacks/monitoring-example.ts +265 -0
- package/src/ts/examples/callbacks/pipeline-callbacks-example.ts +137 -0
- package/src/ts/examples/callbacks/pooled-callbacks-example.ts +274 -0
- package/src/ts/examples/callbacks/priority-routing-example.ts +277 -0
- package/src/ts/examples/callbacks/production-topic-router.ts +214 -0
- package/src/ts/examples/callbacks/topic-based-logging.ts +161 -0
- package/src/ts/examples/chaining/test-chaining-redis.ts +113 -0
- package/src/ts/examples/chaining/test-chaining.ts +52 -0
- package/src/ts/examples/emg-features-example.ts +284 -0
- package/src/ts/examples/fft-example.ts +309 -0
- package/src/ts/examples/fft-examples.ts +349 -0
- package/src/ts/examples/filter-examples.ts +320 -0
- package/src/ts/examples/list-state-example.ts +131 -0
- package/src/ts/examples/logger-example.ts +91 -0
- package/src/ts/examples/notch-filter-examples.ts +243 -0
- package/src/ts/examples/phase5/drift-detection-example.ts +290 -0
- package/src/ts/examples/phase6-7/production-observability.ts +476 -0
- package/src/ts/examples/phase6-7/redis-timeseries-integration.ts +446 -0
- package/src/ts/examples/redis/redis-example.ts +202 -0
- package/src/ts/examples/redis-example.ts +202 -0
- package/src/ts/examples/simd-benchmark.ts +126 -0
- package/src/ts/examples/tap-debugging.ts +230 -0
- package/src/ts/examples/timeseries/comparison-example.ts +290 -0
- package/src/ts/examples/timeseries/iot-sensor-example.ts +143 -0
- package/src/ts/examples/timeseries/redis-streaming-example.ts +233 -0
- package/src/ts/examples/waveform-length-example.ts +139 -0
- package/src/ts/fft.ts +722 -0
- package/src/ts/filters.ts +1078 -0
- package/src/ts/index.ts +120 -0
- package/src/ts/types.ts +589 -0
- 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
|
+
}
|