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
@@ -0,0 +1,484 @@
1
+ /**
2
+ * Tests for FFT/DFT Engine
3
+ */
4
+
5
+ import { describe, it } from "node:test";
6
+ import assert from "node:assert";
7
+ import {
8
+ FftProcessor,
9
+ MovingFftProcessor,
10
+ FftUtils,
11
+ type ComplexArray,
12
+ } from "../fft.js";
13
+
14
+ describe("FftProcessor - Basic Transforms", () => {
15
+ it("should create FFT processor with power-of-2 size", () => {
16
+ const fft = new FftProcessor(256);
17
+ assert.strictEqual(fft.getSize(), 256);
18
+ assert.strictEqual(fft.getHalfSize(), 129);
19
+ assert.strictEqual(fft.isPowerOfTwo(), true);
20
+ });
21
+
22
+ it("should create DFT processor with non-power-of-2 size", () => {
23
+ const fft = new FftProcessor(100);
24
+ assert.strictEqual(fft.getSize(), 100);
25
+ assert.strictEqual(fft.isPowerOfTwo(), false);
26
+ });
27
+
28
+ it("should compute RFFT for real signal", () => {
29
+ const size = 256;
30
+ const fft = new FftProcessor(size);
31
+
32
+ // Generate sine wave at bin 10 (10 * sampleRate / 256)
33
+ const signal = new Float32Array(size);
34
+ const freq = 10;
35
+ for (let i = 0; i < size; i++) {
36
+ signal[i] = Math.sin((2 * Math.PI * freq * i) / size);
37
+ }
38
+
39
+ const spectrum = fft.rfft(signal);
40
+
41
+ assert.ok(spectrum.real instanceof Float32Array);
42
+ assert.ok(spectrum.imag instanceof Float32Array);
43
+ assert.strictEqual(spectrum.real.length, fft.getHalfSize());
44
+ assert.strictEqual(spectrum.imag.length, fft.getHalfSize());
45
+
46
+ // Check peak is at bin 10
47
+ const magnitudes = fft.getMagnitude(spectrum);
48
+ let peakBin = 0;
49
+ let peakValue = 0;
50
+
51
+ for (let i = 0; i < magnitudes.length; i++) {
52
+ if (magnitudes[i] > peakValue) {
53
+ peakValue = magnitudes[i];
54
+ peakBin = i;
55
+ }
56
+ }
57
+
58
+ assert.strictEqual(peakBin, freq);
59
+ });
60
+
61
+ it("should reconstruct signal with IRFFT", () => {
62
+ const size = 128;
63
+ const fft = new FftProcessor(size);
64
+
65
+ // Original signal
66
+ const original = new Float32Array(size);
67
+ for (let i = 0; i < size; i++) {
68
+ original[i] = Math.sin((2 * Math.PI * 5 * i) / size);
69
+ }
70
+
71
+ // Forward -> Inverse
72
+ const spectrum = fft.rfft(original);
73
+ const reconstructed = fft.irfft(spectrum);
74
+
75
+ // Check reconstruction accuracy
76
+ for (let i = 0; i < size; i++) {
77
+ assert.ok(Math.abs(reconstructed[i] - original[i]) < 1e-5);
78
+ }
79
+ });
80
+
81
+ it("should compute DFT for non-power-of-2 sizes", () => {
82
+ const size = 100;
83
+ const fft = new FftProcessor(size);
84
+
85
+ const signal = new Float32Array(size);
86
+ for (let i = 0; i < size; i++) {
87
+ signal[i] = Math.cos((2 * Math.PI * 7 * i) / size);
88
+ }
89
+
90
+ const spectrum = fft.rdft(signal);
91
+
92
+ assert.strictEqual(spectrum.real.length, 51); // 100/2 + 1
93
+ assert.ok(spectrum.real instanceof Float32Array);
94
+ });
95
+ });
96
+
97
+ describe("FftProcessor - Complex Transforms", () => {
98
+ it("should compute FFT for complex signal", () => {
99
+ const size = 64;
100
+ const fft = new FftProcessor(size);
101
+
102
+ const input: ComplexArray = {
103
+ real: new Float32Array(size),
104
+ imag: new Float32Array(size),
105
+ };
106
+
107
+ // Create complex exponential: e^(j2πk/N)
108
+ const k = 5;
109
+ for (let n = 0; n < size; n++) {
110
+ input.real[n] = Math.cos((2 * Math.PI * k * n) / size);
111
+ input.imag[n] = Math.sin((2 * Math.PI * k * n) / size);
112
+ }
113
+
114
+ const spectrum = fft.fft(input);
115
+
116
+ assert.strictEqual(spectrum.real.length, size);
117
+ assert.strictEqual(spectrum.imag.length, size);
118
+
119
+ // Peak should be at bin k
120
+ const magnitudes = fft.getMagnitude(spectrum);
121
+ let peakBin = 0;
122
+ let peakValue = 0;
123
+
124
+ for (let i = 0; i < magnitudes.length; i++) {
125
+ if (magnitudes[i] > peakValue) {
126
+ peakValue = magnitudes[i];
127
+ peakBin = i;
128
+ }
129
+ }
130
+
131
+ assert.strictEqual(peakBin, k);
132
+ });
133
+
134
+ it("should reconstruct complex signal with IFFT", () => {
135
+ const size = 32;
136
+ const fft = new FftProcessor(size);
137
+
138
+ const original: ComplexArray = {
139
+ real: new Float32Array(size),
140
+ imag: new Float32Array(size),
141
+ };
142
+
143
+ for (let i = 0; i < size; i++) {
144
+ original.real[i] = Math.sin((2 * Math.PI * 3 * i) / size);
145
+ original.imag[i] = Math.cos((2 * Math.PI * 3 * i) / size);
146
+ }
147
+
148
+ const spectrum = fft.fft(original);
149
+ const reconstructed = fft.ifft(spectrum);
150
+
151
+ for (let i = 0; i < size; i++) {
152
+ assert.ok(Math.abs(reconstructed.real[i] - original.real[i]) < 1e-5);
153
+ assert.ok(Math.abs(reconstructed.imag[i] - original.imag[i]) < 1e-5);
154
+ }
155
+ });
156
+ });
157
+
158
+ describe("FftProcessor - Spectral Analysis", () => {
159
+ it("should compute magnitude spectrum", () => {
160
+ const size = 128;
161
+ const fft = new FftProcessor(size);
162
+
163
+ const signal = new Float32Array(size);
164
+ for (let i = 0; i < size; i++) {
165
+ signal[i] = Math.sin((2 * Math.PI * 8 * i) / size);
166
+ }
167
+
168
+ const spectrum = fft.rfft(signal);
169
+ const magnitudes = fft.getMagnitude(spectrum);
170
+
171
+ assert.strictEqual(magnitudes.length, fft.getHalfSize());
172
+ assert.ok(magnitudes.every((m) => m >= 0)); // Magnitudes always positive
173
+ });
174
+
175
+ it("should compute phase spectrum", () => {
176
+ const size = 64;
177
+ const fft = new FftProcessor(size);
178
+
179
+ const signal = new Float32Array(size);
180
+ for (let i = 0; i < size; i++) {
181
+ signal[i] = Math.cos((2 * Math.PI * 4 * i) / size);
182
+ }
183
+
184
+ const spectrum = fft.rfft(signal);
185
+ const phases = fft.getPhase(spectrum);
186
+
187
+ assert.strictEqual(phases.length, fft.getHalfSize());
188
+ // Phase should be near 0 for cosine (even function)
189
+ assert.ok(Math.abs(phases[4]) < 0.1);
190
+ });
191
+
192
+ it("should compute power spectrum", () => {
193
+ const size = 256;
194
+ const fft = new FftProcessor(size);
195
+
196
+ const signal = new Float32Array(size);
197
+ for (let i = 0; i < size; i++) {
198
+ signal[i] = Math.sin((2 * Math.PI * 10 * i) / size);
199
+ }
200
+
201
+ const spectrum = fft.rfft(signal);
202
+ const power = fft.getPower(spectrum);
203
+
204
+ assert.strictEqual(power.length, fft.getHalfSize());
205
+ assert.ok(power.every((p) => p >= 0));
206
+
207
+ // Power = magnitude²
208
+ const magnitudes = fft.getMagnitude(spectrum);
209
+ for (let i = 0; i < power.length; i++) {
210
+ assert.ok(Math.abs(power[i] - magnitudes[i] ** 2) < 1e-4);
211
+ }
212
+ });
213
+
214
+ it("should compute frequency bins correctly", () => {
215
+ const size = 1024;
216
+ const sampleRate = 44100;
217
+ const fft = new FftProcessor(size);
218
+
219
+ const freqs = fft.getFrequencyBins(sampleRate);
220
+
221
+ assert.strictEqual(freqs.length, fft.getHalfSize());
222
+ assert.strictEqual(freqs[0], 0); // DC
223
+ assert.ok(Math.abs(freqs[freqs.length - 1] - sampleRate / 2) < 1); // Nyquist
224
+ });
225
+ });
226
+
227
+ describe("FftProcessor - Parseval's Theorem", () => {
228
+ it("should conserve energy (Parseval)", () => {
229
+ const size = 256;
230
+ const fft = new FftProcessor(size);
231
+
232
+ const signal = new Float32Array(size);
233
+ for (let i = 0; i < size; i++) {
234
+ signal[i] = Math.random() * 2 - 1; // Random signal
235
+ }
236
+
237
+ // Time-domain energy
238
+ let timeEnergy = 0;
239
+ for (let i = 0; i < size; i++) {
240
+ timeEnergy += signal[i] * signal[i];
241
+ }
242
+
243
+ // Frequency-domain energy
244
+ const spectrum = fft.rfft(signal);
245
+ const power = fft.getPower(spectrum);
246
+
247
+ let freqEnergy = 0;
248
+ freqEnergy += power[0]; // DC
249
+ for (let i = 1; i < power.length - 1; i++) {
250
+ freqEnergy += 2 * power[i]; // Account for negative frequencies
251
+ }
252
+ freqEnergy += power[power.length - 1]; // Nyquist
253
+ freqEnergy /= size; // Normalize
254
+
255
+ // Check energy conservation
256
+ assert.ok(Math.abs(timeEnergy - freqEnergy) / timeEnergy < 0.01);
257
+ });
258
+ });
259
+
260
+ describe("MovingFftProcessor - Batched Mode", () => {
261
+ it("should process batched FFT with hop size", () => {
262
+ const fftSize = 128;
263
+ const hopSize = 64;
264
+
265
+ const movingFft = new MovingFftProcessor({
266
+ fftSize,
267
+ hopSize,
268
+ mode: "batched",
269
+ windowType: "hann",
270
+ });
271
+
272
+ const samples = new Float32Array(256);
273
+ for (let i = 0; i < samples.length; i++) {
274
+ samples[i] = Math.sin((2 * Math.PI * 10 * i) / fftSize);
275
+ }
276
+
277
+ let spectrumCount = 0;
278
+ movingFft.addSamples(samples, (spectrum, size) => {
279
+ assert.strictEqual(size, fftSize / 2 + 1);
280
+ spectrumCount++;
281
+ });
282
+
283
+ // Should produce 3 spectra: at samples 128, 192, 256
284
+ assert.strictEqual(spectrumCount, 3);
285
+ });
286
+
287
+ it("should apply windowing correctly", () => {
288
+ const fftSize = 64;
289
+
290
+ const movingFft = new MovingFftProcessor({
291
+ fftSize,
292
+ mode: "batched",
293
+ windowType: "hann",
294
+ });
295
+
296
+ // Fill buffer
297
+ const samples = new Float32Array(fftSize);
298
+ samples.fill(1.0); // Constant signal
299
+
300
+ movingFft.addSamples(samples);
301
+ const spectrum = movingFft.computeSpectrum();
302
+
303
+ // Windowing should reduce spectral leakage
304
+ assert.ok(spectrum.real[0] > 0); // DC component preserved
305
+ });
306
+
307
+ it("should reset state correctly", () => {
308
+ const movingFft = new MovingFftProcessor({
309
+ fftSize: 64,
310
+ hopSize: 32,
311
+ });
312
+
313
+ const samples = new Float32Array(128);
314
+ movingFft.addSamples(samples);
315
+ movingFft.reset();
316
+
317
+ // After reset, should need full buffer again
318
+ const samples2 = new Float32Array(32);
319
+ let called = false;
320
+ movingFft.addSamples(samples2, () => {
321
+ called = true;
322
+ });
323
+
324
+ assert.strictEqual(called, false); // Not enough samples yet
325
+ });
326
+ });
327
+
328
+ describe("FftUtils - Helper Functions", () => {
329
+ it("should find peak frequency", () => {
330
+ const fftSize = 1024;
331
+ const sampleRate = 44100;
332
+ const targetFreq = 440; // A4
333
+
334
+ const fft = new FftProcessor(fftSize);
335
+
336
+ const signal = new Float32Array(fftSize);
337
+ for (let i = 0; i < fftSize; i++) {
338
+ signal[i] = Math.sin((2 * Math.PI * targetFreq * i) / sampleRate);
339
+ }
340
+
341
+ const spectrum = fft.rfft(signal);
342
+ const magnitudes = fft.getMagnitude(spectrum);
343
+
344
+ const peakFreq = FftUtils.findPeakFrequency(
345
+ magnitudes,
346
+ sampleRate,
347
+ fftSize
348
+ );
349
+
350
+ assert.ok(Math.abs(peakFreq - targetFreq) < sampleRate / fftSize);
351
+ });
352
+
353
+ it("should convert to decibels", () => {
354
+ const magnitudes = new Float32Array([1.0, 0.5, 0.1, 0.01]);
355
+ const db = FftUtils.toDecibels(magnitudes);
356
+
357
+ assert.ok(Math.abs(db[0] - 0) < 0.1); // 1.0 = 0 dB
358
+ assert.ok(Math.abs(db[1] - -6.02) < 0.1); // 0.5 ≈ -6 dB
359
+ assert.ok(Math.abs(db[2] - -20) < 0.1); // 0.1 = -20 dB
360
+ assert.ok(Math.abs(db[3] - -40) < 0.1); // 0.01 = -40 dB
361
+ });
362
+
363
+ it("should compute next power of 2", () => {
364
+ assert.strictEqual(FftUtils.nextPowerOfTwo(100), 128);
365
+ assert.strictEqual(FftUtils.nextPowerOfTwo(256), 256);
366
+ assert.strictEqual(FftUtils.nextPowerOfTwo(1000), 1024);
367
+ assert.strictEqual(FftUtils.nextPowerOfTwo(2048), 2048);
368
+ });
369
+
370
+ it("should zero-pad signal", () => {
371
+ const signal = new Float32Array([1, 2, 3, 4, 5]);
372
+ const padded = FftUtils.zeroPad(signal, 10);
373
+
374
+ assert.strictEqual(padded.length, 10);
375
+ assert.strictEqual(padded[0], 1);
376
+ assert.strictEqual(padded[4], 5);
377
+ assert.strictEqual(padded[5], 0);
378
+ assert.strictEqual(padded[9], 0);
379
+ });
380
+ });
381
+
382
+ describe("FFT - Edge Cases", () => {
383
+ it("should handle DC-only signal", () => {
384
+ const size = 64;
385
+ const fft = new FftProcessor(size);
386
+
387
+ const signal = new Float32Array(size);
388
+ signal.fill(1.0); // DC only
389
+
390
+ const spectrum = fft.rfft(signal);
391
+ const magnitudes = fft.getMagnitude(spectrum);
392
+
393
+ // All energy should be in DC bin
394
+ assert.ok(magnitudes[0] > size * 0.9);
395
+ for (let i = 1; i < magnitudes.length; i++) {
396
+ assert.ok(magnitudes[i] < 0.1);
397
+ }
398
+ });
399
+
400
+ it("should handle Nyquist frequency", () => {
401
+ const size = 128;
402
+ const fft = new FftProcessor(size);
403
+
404
+ // Alternating +1, -1 = Nyquist frequency
405
+ const signal = new Float32Array(size);
406
+ for (let i = 0; i < size; i++) {
407
+ signal[i] = i % 2 === 0 ? 1 : -1;
408
+ }
409
+
410
+ const spectrum = fft.rfft(signal);
411
+ const magnitudes = fft.getMagnitude(spectrum);
412
+
413
+ // Peak should be at Nyquist bin (last bin)
414
+ let peakBin = 0;
415
+ let peakValue = 0;
416
+ for (let i = 0; i < magnitudes.length; i++) {
417
+ if (magnitudes[i] > peakValue) {
418
+ peakValue = magnitudes[i];
419
+ peakBin = i;
420
+ }
421
+ }
422
+
423
+ assert.strictEqual(peakBin, magnitudes.length - 1);
424
+ });
425
+
426
+ it("should handle zero signal", () => {
427
+ const size = 256;
428
+ const fft = new FftProcessor(size);
429
+
430
+ const signal = new Float32Array(size);
431
+ // All zeros
432
+
433
+ const spectrum = fft.rfft(signal);
434
+ const magnitudes = fft.getMagnitude(spectrum);
435
+
436
+ for (let i = 0; i < magnitudes.length; i++) {
437
+ assert.ok(Math.abs(magnitudes[i]) < 1e-6);
438
+ }
439
+ });
440
+ });
441
+
442
+ describe("FFT - Hermitian Symmetry", () => {
443
+ it("should exhibit Hermitian symmetry for real inputs", () => {
444
+ const size = 256;
445
+ const fft = new FftProcessor(size);
446
+
447
+ const signal = new Float32Array(size);
448
+ for (let i = 0; i < size; i++) {
449
+ signal[i] =
450
+ Math.sin((2 * Math.PI * 7 * i) / size) +
451
+ Math.cos((2 * Math.PI * 13 * i) / size);
452
+ }
453
+
454
+ // Full complex FFT
455
+ const complexInput: ComplexArray = {
456
+ real: signal,
457
+ imag: new Float32Array(size),
458
+ };
459
+
460
+ const fullSpectrum = fft.fft(complexInput);
461
+
462
+ // Check X[k] = conj(X[N-k])
463
+ // Hermitian symmetry: Real[k] = Real[N-k], Imag[k] = -Imag[N-k]
464
+ for (let k = 1; k < size / 2; k++) {
465
+ const realDiff = Math.abs(
466
+ fullSpectrum.real[k] - fullSpectrum.real[size - k]
467
+ );
468
+ const imagDiff = Math.abs(
469
+ fullSpectrum.imag[k] + fullSpectrum.imag[size - k]
470
+ );
471
+
472
+ // Use relative tolerance for better numerical stability
473
+ const tolerance = 1e-4;
474
+ assert.ok(
475
+ realDiff < tolerance,
476
+ `Real part symmetry at k=${k}: diff=${realDiff}`
477
+ );
478
+ assert.ok(
479
+ imagDiff < tolerance,
480
+ `Imag part symmetry at k=${k}: diff=${imagDiff}`
481
+ );
482
+ }
483
+ });
484
+ });
@@ -0,0 +1,153 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert";
3
+ import { createDspPipeline } from "../bindings.js";
4
+
5
+ describe("listState Method", () => {
6
+ it("should return basic pipeline summary before processing", () => {
7
+ const pipeline = createDspPipeline()
8
+ .MovingAverage({ mode: "moving", windowSize: 10 })
9
+ .Rms({ mode: "moving", windowSize: 5 });
10
+
11
+ const summary = pipeline.listState();
12
+
13
+ assert.strictEqual(summary.stageCount, 2);
14
+ assert.ok(summary.timestamp > 0);
15
+ assert.strictEqual(summary.stages.length, 2);
16
+
17
+ // Check first stage
18
+ assert.strictEqual(summary.stages[0].index, 0);
19
+ assert.strictEqual(summary.stages[0].type, "movingAverage");
20
+ assert.strictEqual(summary.stages[0].windowSize, 10);
21
+
22
+ // Check second stage
23
+ assert.strictEqual(summary.stages[1].index, 1);
24
+ assert.strictEqual(summary.stages[1].type, "rms");
25
+ assert.strictEqual(summary.stages[1].windowSize, 5);
26
+ });
27
+
28
+ it("should include channel info after processing", async () => {
29
+ const pipeline = createDspPipeline().MovingAverage({ mode: "moving", windowSize: 5 });
30
+
31
+ const input = new Float32Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
32
+ await pipeline.process(input, { sampleRate: 1000, channels: 1 });
33
+
34
+ const summary = pipeline.listState();
35
+
36
+ assert.strictEqual(summary.stages[0].numChannels, 1);
37
+ assert.strictEqual(summary.stages[0].bufferSize, 5);
38
+ assert.strictEqual(summary.stages[0].channelCount, 1);
39
+ });
40
+
41
+ it("should show correct channel count for multi-channel processing", async () => {
42
+ const pipeline = createDspPipeline()
43
+ .MovingAverage({ mode: "moving", windowSize: 10 })
44
+ .Rms({ mode: "moving", windowSize: 5 });
45
+
46
+ // 4-channel interleaved data
47
+ const input = new Float32Array(400).map((_, i) => Math.sin(i * 0.1));
48
+ await pipeline.process(input, { sampleRate: 2000, channels: 4 });
49
+
50
+ const summary = pipeline.listState();
51
+
52
+ assert.strictEqual(summary.stages[0].channelCount, 4);
53
+ assert.strictEqual(summary.stages[1].channelCount, 4);
54
+ });
55
+
56
+ it("should include mode for rectify stage", () => {
57
+ const pipeline = createDspPipeline()
58
+ .Rectify({ mode: "half" })
59
+ .Rms({ mode: "moving", windowSize: 10 });
60
+
61
+ const summary = pipeline.listState();
62
+
63
+ assert.strictEqual(summary.stages[0].type, "rectify");
64
+ assert.strictEqual(summary.stages[0].mode, "half");
65
+ });
66
+
67
+ it("should not include buffer data (unlike saveState)", async () => {
68
+ const pipeline = createDspPipeline().MovingAverage({ mode: "moving", windowSize: 5 });
69
+
70
+ const input = new Float32Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
71
+ await pipeline.process(input, { sampleRate: 1000, channels: 1 });
72
+
73
+ const summary = pipeline.listState();
74
+ const summaryJson = JSON.stringify(summary);
75
+
76
+ // Should not contain actual buffer values
77
+ assert.ok(!summaryJson.includes('"buffer":['));
78
+
79
+ // Should not contain running sum
80
+ assert.ok(!summaryJson.includes('"runningSum"'));
81
+ });
82
+
83
+ it("should be smaller than saveState", async () => {
84
+ const pipeline = createDspPipeline()
85
+ .MovingAverage({ mode: "moving", windowSize: 100 })
86
+ .Rms({ mode: "moving", windowSize: 50 });
87
+
88
+ const input = new Float32Array(1000).map((_, i) => Math.sin(i * 0.1));
89
+ await pipeline.process(input, { sampleRate: 1000, channels: 1 });
90
+
91
+ const summary = pipeline.listState();
92
+ const fullState = await pipeline.saveState();
93
+
94
+ const summarySize = JSON.stringify(summary).length;
95
+ const fullStateSize = fullState.length;
96
+
97
+ // listState should be significantly smaller
98
+ assert.ok(
99
+ summarySize < fullStateSize,
100
+ `Summary size (${summarySize}) should be less than full state (${fullStateSize})`
101
+ );
102
+
103
+ // Should be at least 50% smaller for this case
104
+ const reduction = 1 - summarySize / fullStateSize;
105
+ assert.ok(
106
+ reduction > 0.5,
107
+ `Reduction should be > 50%, got ${(reduction * 100).toFixed(1)}%`
108
+ );
109
+ });
110
+
111
+ it("should work with complex pipeline", async () => {
112
+ const pipeline = createDspPipeline()
113
+ .MovingAverage({ mode: "moving", windowSize: 20 })
114
+ .Rectify({ mode: "full" })
115
+ .Rms({ mode: "moving", windowSize: 10 })
116
+ .MovingAverage({ mode: "moving", windowSize: 5 });
117
+
118
+ const input = new Float32Array(100).map((_, i) => Math.sin(i * 0.1));
119
+ await pipeline.process(input, { sampleRate: 1000, channels: 1 });
120
+
121
+ const summary = pipeline.listState();
122
+
123
+ assert.strictEqual(summary.stageCount, 4);
124
+ assert.strictEqual(summary.stages[0].type, "movingAverage");
125
+ assert.strictEqual(summary.stages[1].type, "rectify");
126
+ assert.strictEqual(summary.stages[2].type, "rms");
127
+ assert.strictEqual(summary.stages[3].type, "movingAverage");
128
+
129
+ // Verify each stage has expected properties
130
+ assert.strictEqual(summary.stages[0].windowSize, 20);
131
+ assert.strictEqual(summary.stages[1].mode, "full");
132
+ assert.strictEqual(summary.stages[2].windowSize, 10);
133
+ assert.strictEqual(summary.stages[3].windowSize, 5);
134
+ });
135
+
136
+ it("should update timestamp on each call", async () => {
137
+ const pipeline = createDspPipeline().MovingAverage({ mode: "moving", windowSize: 5 });
138
+
139
+ const summary1 = pipeline.listState();
140
+ const timestamp1 = summary1.timestamp;
141
+
142
+ // Wait a moment
143
+ await new Promise((resolve) => setTimeout(resolve, 100));
144
+
145
+ const summary2 = pipeline.listState();
146
+ const timestamp2 = summary2.timestamp;
147
+
148
+ assert.ok(
149
+ timestamp2 >= timestamp1,
150
+ "Second timestamp should be >= first timestamp"
151
+ );
152
+ });
153
+ });