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,322 @@
1
+ import { describe, test, beforeEach } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { createDspPipeline, DspProcessor } from "../bindings.js";
4
+
5
+ const DEFAULT_OPTIONS = { channels: 1, sampleRate: 44100 };
6
+
7
+ function assertCloseTo(actual: number, expected: number, precision = 5) {
8
+ const tolerance = Math.pow(10, -precision);
9
+ assert.ok(
10
+ Math.abs(actual - expected) < tolerance,
11
+ `Expected ${actual} to be close to ${expected} (tolerance: ${tolerance})`
12
+ );
13
+ }
14
+
15
+ describe("MovingAverage Filter", () => {
16
+ let processor: DspProcessor;
17
+
18
+ beforeEach(() => {
19
+ processor = createDspPipeline();
20
+ });
21
+
22
+ describe("Batch Mode (Stateless)", () => {
23
+ test("should compute batch average correctly", async () => {
24
+ processor.MovingAverage({ mode: "batch" });
25
+
26
+ const input = new Float32Array([1, 2, 3, 4, 5]);
27
+ const output = await processor.process(input, DEFAULT_OPTIONS);
28
+
29
+ // Mean of [1,2,3,4,5] = 3
30
+ // All samples should be replaced with the average
31
+ assert.equal(output.length, 5);
32
+ output.forEach((val) => assertCloseTo(val, 3));
33
+ });
34
+
35
+ test("should compute average of zero signal", async () => {
36
+ processor.MovingAverage({ mode: "batch" });
37
+
38
+ const input = new Float32Array([0, 0, 0, 0]);
39
+ const output = await processor.process(input, DEFAULT_OPTIONS);
40
+
41
+ output.forEach((val) => assertCloseTo(val, 0));
42
+ });
43
+
44
+ test("should compute average of constant signal", async () => {
45
+ processor.MovingAverage({ mode: "batch" });
46
+
47
+ const input = new Float32Array([5, 5, 5, 5, 5]);
48
+ const output = await processor.process(input, DEFAULT_OPTIONS);
49
+
50
+ output.forEach((val) => assertCloseTo(val, 5));
51
+ });
52
+
53
+ test("should handle negative values correctly", async () => {
54
+ processor.MovingAverage({ mode: "batch" });
55
+
56
+ const input = new Float32Array([-2, -1, 0, 1, 2]);
57
+ const output = await processor.process(input, DEFAULT_OPTIONS);
58
+
59
+ // Mean = 0
60
+ output.forEach((val) => assertCloseTo(val, 0));
61
+ });
62
+
63
+ test("should be stateless between calls", async () => {
64
+ processor.MovingAverage({ mode: "batch" });
65
+
66
+ const input1 = new Float32Array([1, 2, 3, 4, 5]);
67
+ const output1 = await processor.process(input1, DEFAULT_OPTIONS);
68
+
69
+ const input2 = new Float32Array([10, 20, 30]);
70
+ const output2 = await processor.process(input2, DEFAULT_OPTIONS);
71
+
72
+ // First batch: mean = 3
73
+ output1.forEach((val) => assertCloseTo(val, 3));
74
+
75
+ // Second batch: mean = 20 (independent of first)
76
+ output2.forEach((val) => assertCloseTo(val, 20));
77
+ });
78
+
79
+ test("should handle multi-channel data independently", async () => {
80
+ processor.MovingAverage({ mode: "batch" });
81
+
82
+ // 2 channels, 5 samples per channel
83
+ // Channel 0: [1, 3, 5, 7, 9] → mean = 5
84
+ // Channel 1: [10, 20, 30, 40, 50] → mean = 30
85
+ const input = new Float32Array([1, 10, 3, 20, 5, 30, 7, 40, 9, 50]);
86
+ const output = await processor.process(input, {
87
+ sampleRate: 1000,
88
+ channels: 2,
89
+ });
90
+
91
+ // Extract channels
92
+ const ch0 = [];
93
+ const ch1 = [];
94
+ for (let i = 0; i < output.length; i++) {
95
+ if (i % 2 === 0) ch0.push(output[i]);
96
+ else ch1.push(output[i]);
97
+ }
98
+
99
+ ch0.forEach((val) => assertCloseTo(val, 5));
100
+ ch1.forEach((val) => assertCloseTo(val, 30));
101
+ });
102
+ });
103
+
104
+ describe("Moving Mode (Stateful)", () => {
105
+ test("should compute moving average with window size 3", async () => {
106
+ processor.MovingAverage({ mode: "moving", windowSize: 3 });
107
+
108
+ const input = new Float32Array([1, 2, 3, 4, 5]);
109
+ const output = await processor.process(input, DEFAULT_OPTIONS);
110
+
111
+ // First value: [1] → avg = 1
112
+ // Second value: [1, 2] → avg = 1.5
113
+ // Third value: [1, 2, 3] → avg = 2
114
+ // Fourth value: [2, 3, 4] → avg = 3
115
+ // Fifth value: [3, 4, 5] → avg = 4
116
+ assert.equal(output.length, 5);
117
+ assertCloseTo(output[0], 1);
118
+ assertCloseTo(output[1], 1.5);
119
+ assertCloseTo(output[2], 2);
120
+ assertCloseTo(output[3], 3);
121
+ assertCloseTo(output[4], 4);
122
+ });
123
+
124
+ test("should handle single sample window", async () => {
125
+ processor.MovingAverage({ mode: "moving", windowSize: 1 });
126
+
127
+ const input = new Float32Array([10, 20, 30]);
128
+ const output = await processor.process(input, DEFAULT_OPTIONS);
129
+
130
+ assert.deepEqual(Array.from(output), [10, 20, 30]); // No smoothing with window size 1
131
+ });
132
+
133
+ test("should handle negative values", async () => {
134
+ processor.MovingAverage({ mode: "moving", windowSize: 2 });
135
+
136
+ const input = new Float32Array([-5, 5, -10, 10]);
137
+ const output = await processor.process(input, DEFAULT_OPTIONS);
138
+
139
+ assertCloseTo(output[0], -5);
140
+ assertCloseTo(output[1], 0); // (-5 + 5) / 2
141
+ assertCloseTo(output[2], -2.5); // (5 + -10) / 2
142
+ assertCloseTo(output[3], 0); // (-10 + 10) / 2
143
+ });
144
+
145
+ test("should maintain state across multiple process calls", async () => {
146
+ processor.MovingAverage({ mode: "moving", windowSize: 3 });
147
+
148
+ // Process first chunk
149
+ const output1 = await processor.process(
150
+ new Float32Array([1, 2]),
151
+ DEFAULT_OPTIONS
152
+ );
153
+ assertCloseTo(output1[0], 1);
154
+ assertCloseTo(output1[1], 1.5);
155
+
156
+ // Process second chunk - should continue from previous state
157
+ const output2 = await processor.process(
158
+ new Float32Array([3, 4]),
159
+ DEFAULT_OPTIONS
160
+ );
161
+ assertCloseTo(output2[0], 2); // (1 + 2 + 3) / 3
162
+ assertCloseTo(output2[1], 3); // (2 + 3 + 4) / 3
163
+ });
164
+
165
+ test("should throw error for moving mode without window size", () => {
166
+ assert.throws(() => {
167
+ processor.MovingAverage({ mode: "moving" } as any);
168
+ }, /either windowSize or windowDuration must be specified/);
169
+ });
170
+
171
+ test("should throw error for invalid window size", () => {
172
+ assert.throws(() => {
173
+ processor.MovingAverage({ mode: "moving", windowSize: 0 });
174
+ }, /windowSize must be a positive integer/);
175
+
176
+ assert.throws(() => {
177
+ processor.MovingAverage({ mode: "moving", windowSize: -5 });
178
+ }, /windowSize must be a positive integer/);
179
+
180
+ assert.throws(() => {
181
+ processor.MovingAverage({ mode: "moving", windowSize: 3.14 });
182
+ }, /windowSize must be a positive integer/);
183
+ });
184
+ });
185
+
186
+ describe("State Management", () => {
187
+ test("should serialize and deserialize state correctly", async () => {
188
+ processor.MovingAverage({ mode: "moving", windowSize: 3 });
189
+
190
+ // Process some data to build state
191
+ await processor.process(
192
+ new Float32Array([1, 2, 3, 4, 5]),
193
+ DEFAULT_OPTIONS
194
+ );
195
+
196
+ // Save state
197
+ const stateJson = await processor.saveState();
198
+ const state = JSON.parse(stateJson);
199
+
200
+ assert.ok(state);
201
+ assert.ok(state.timestamp);
202
+ assert.equal(state.stages.length, 1);
203
+ assert.equal(state.stages[0].type, "movingAverage");
204
+
205
+ // Create new processor with same pipeline structure and load state
206
+ const processor2 = createDspPipeline();
207
+ processor2.MovingAverage({ mode: "moving", windowSize: 3 }); // Must match original pipeline
208
+ await processor2.loadState(stateJson);
209
+
210
+ // Process should continue from saved state
211
+ const output1 = await processor.process(
212
+ new Float32Array([6]),
213
+ DEFAULT_OPTIONS
214
+ );
215
+ const output2 = await processor2.process(
216
+ new Float32Array([6]),
217
+ DEFAULT_OPTIONS
218
+ );
219
+
220
+ assertCloseTo(output2[0], output1[0]);
221
+ });
222
+
223
+ test("should reset state correctly", async () => {
224
+ processor.MovingAverage({ mode: "moving", windowSize: 3 });
225
+
226
+ // Build up state
227
+ await processor.process(new Float32Array([1, 2, 3]), DEFAULT_OPTIONS);
228
+
229
+ // Reset
230
+ processor.clearState();
231
+
232
+ // Should start fresh
233
+ const output = await processor.process(
234
+ new Float32Array([10]),
235
+ DEFAULT_OPTIONS
236
+ );
237
+ assertCloseTo(output[0], 10); // Only 10 in buffer
238
+ });
239
+
240
+ test("should validate runningSum on state load", async () => {
241
+ processor.MovingAverage({ mode: "moving", windowSize: 3 });
242
+ await processor.process(new Float32Array([1, 2, 3]), DEFAULT_OPTIONS);
243
+
244
+ const stateJson = await processor.saveState();
245
+ const state = JSON.parse(stateJson);
246
+
247
+ // Corrupt the runningSum (note: it's in state.stages[0].state.channels)
248
+ if (state.stages[0].state.channels && state.stages[0].state.channels[0]) {
249
+ state.stages[0].state.channels[0].runningSum = 9999;
250
+ }
251
+
252
+ // Should throw when loading corrupted state
253
+ const processor2 = createDspPipeline();
254
+ processor2.MovingAverage({ mode: "moving", windowSize: 3 }); // Must match original pipeline
255
+ await assert.rejects(
256
+ async () => await processor2.loadState(JSON.stringify(state)),
257
+ /Running sum validation failed/
258
+ );
259
+ });
260
+
261
+ test("should validate window size on state load", async () => {
262
+ processor.MovingAverage({ mode: "moving", windowSize: 3 });
263
+ await processor.process(new Float32Array([1, 2, 3]), DEFAULT_OPTIONS);
264
+
265
+ const stateJson = await processor.saveState();
266
+ const state = JSON.parse(stateJson);
267
+
268
+ // Corrupt the window size parameter itself
269
+ if (state.stages[0].state) {
270
+ state.stages[0].state.windowSize = 5; // Change from 3 to 5
271
+ }
272
+
273
+ // Should throw when loading corrupted state
274
+ const processor2 = createDspPipeline();
275
+ processor2.MovingAverage({ mode: "moving", windowSize: 3 }); // Original window size
276
+ await assert.rejects(
277
+ async () => await processor2.loadState(JSON.stringify(state)),
278
+ /Window size mismatch/
279
+ );
280
+ });
281
+
282
+ test("should handle empty input", async () => {
283
+ processor.MovingAverage({ mode: "moving", windowSize: 3 });
284
+ const output = await processor.process(
285
+ new Float32Array([]),
286
+ DEFAULT_OPTIONS
287
+ );
288
+ assert.equal(output.length, 0);
289
+ });
290
+ });
291
+
292
+ describe("Multi-channel Processing", () => {
293
+ test("should process data with stateful continuity", async () => {
294
+ processor.MovingAverage({ mode: "moving", windowSize: 2 });
295
+
296
+ // Process first batch
297
+ const output1 = await processor.process(
298
+ new Float32Array([1, 2, 3]),
299
+ DEFAULT_OPTIONS
300
+ );
301
+ assertCloseTo(output1[0], 1); // [1]
302
+ assertCloseTo(output1[1], 1.5); // [1, 2]
303
+ assertCloseTo(output1[2], 2.5); // [2, 3]
304
+
305
+ // Process second batch - state continues from previous
306
+ const output2 = await processor.process(
307
+ new Float32Array([10, 20, 30]),
308
+ DEFAULT_OPTIONS
309
+ );
310
+ assertCloseTo(output2[0], 6.5); // [3, 10]
311
+ assertCloseTo(output2[1], 15); // [10, 20]
312
+ assertCloseTo(output2[2], 25); // [20, 30]
313
+
314
+ // Process third batch - state continues
315
+ const output3 = await processor.process(
316
+ new Float32Array([4]),
317
+ DEFAULT_OPTIONS
318
+ );
319
+ assertCloseTo(output3[0], 17); // [30, 4]
320
+ });
321
+ });
322
+ });
@@ -0,0 +1,315 @@
1
+ import { describe, test, beforeEach } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { createDspPipeline, DspProcessor } from "../bindings.js";
4
+
5
+ const DEFAULT_OPTIONS = { channels: 1, sampleRate: 44100 };
6
+
7
+ function assertCloseTo(actual: number, expected: number, precision = 5) {
8
+ const tolerance = Math.pow(10, -precision);
9
+ assert.ok(
10
+ Math.abs(actual - expected) < tolerance,
11
+ `Expected ${actual} to be close to ${expected} (tolerance: ${tolerance})`
12
+ );
13
+ }
14
+
15
+ describe("RMS Filter", () => {
16
+ let processor: DspProcessor;
17
+
18
+ beforeEach(() => {
19
+ processor = createDspPipeline();
20
+ });
21
+
22
+ describe("Basic Functionality", () => {
23
+ test("should compute RMS with window size 3", async () => {
24
+ processor.Rms({ mode: "moving", windowSize: 3 });
25
+
26
+ const input = new Float32Array([3, 4, 0, 6, 8]);
27
+ const output = await processor.process(input, DEFAULT_OPTIONS);
28
+
29
+ // First value: sqrt(3²) = 3
30
+ // Second value: sqrt((3² + 4²) / 2) = sqrt(25 / 2) = 3.5355...
31
+ // Third value: sqrt((3² + 4² + 0²) / 3) = sqrt(25 / 3) = 2.8867...
32
+ // Fourth value: sqrt((4² + 0² + 6²) / 3) = sqrt(52 / 3) = 4.1633...
33
+ // Fifth value: sqrt((0² + 6² + 8²) / 3) = sqrt(100 / 3) = 5.7735...
34
+ assert.equal(output.length, 5);
35
+ assertCloseTo(output[0], 3);
36
+ assertCloseTo(output[1], 3.5355, 4);
37
+ assertCloseTo(output[2], 2.8867, 4);
38
+ assertCloseTo(output[3], 4.1633, 4);
39
+ assertCloseTo(output[4], 5.7735, 4);
40
+ });
41
+
42
+ test("should handle single sample window", async () => {
43
+ processor.Rms({ mode: "moving", windowSize: 1 });
44
+
45
+ const input = new Float32Array([3, 4, 5]);
46
+ const output = await processor.process(input, DEFAULT_OPTIONS);
47
+
48
+ // RMS of single sample is just the absolute value
49
+ assertCloseTo(output[0], 3);
50
+ assertCloseTo(output[1], 4);
51
+ assertCloseTo(output[2], 5);
52
+ });
53
+
54
+ test("should compute RMS correctly for negative values", async () => {
55
+ processor.Rms({ mode: "moving", windowSize: 2 });
56
+
57
+ const input = new Float32Array([-3, 4, -5, 12]);
58
+ const output = await processor.process(input, DEFAULT_OPTIONS);
59
+
60
+ // First: sqrt((-3)²) = 3
61
+ // Second: sqrt((9 + 16) / 2) = sqrt(12.5) = 3.5355...
62
+ // Third: sqrt((16 + 25) / 2) = sqrt(20.5) = 4.5277...
63
+ // Fourth: sqrt((25 + 144) / 2) = sqrt(84.5) = 9.1924...
64
+ assertCloseTo(output[0], 3);
65
+ assertCloseTo(output[1], 3.5355, 4);
66
+ assertCloseTo(output[2], 4.5277, 4);
67
+ assertCloseTo(output[3], 9.1924, 4);
68
+ });
69
+
70
+ test("should maintain state across multiple process calls", async () => {
71
+ processor.Rms({ mode: "moving", windowSize: 3 });
72
+
73
+ // First batch: [3, 4]
74
+ const output1 = await processor.process(
75
+ new Float32Array([3, 4]),
76
+ DEFAULT_OPTIONS
77
+ );
78
+ assertCloseTo(output1[0], 3);
79
+ assertCloseTo(output1[1], 3.5355, 4);
80
+
81
+ // Second batch: [5] - should use [3, 4, 5] for RMS
82
+ const output2 = await processor.process(
83
+ new Float32Array([5]),
84
+ DEFAULT_OPTIONS
85
+ );
86
+ // sqrt((3² + 4² + 5²) / 3) = sqrt(50 / 3) = 4.0824...
87
+ assertCloseTo(output2[0], 4.0824, 4);
88
+ });
89
+ });
90
+
91
+ describe("State Management", () => {
92
+ test("should serialize and deserialize state correctly", async () => {
93
+ processor.Rms({ mode: "moving", windowSize: 3 });
94
+
95
+ // Build state
96
+ await processor.process(new Float32Array([3, 4, 5]), DEFAULT_OPTIONS);
97
+
98
+ const stateJson = await processor.saveState();
99
+ const state = JSON.parse(stateJson);
100
+
101
+ assert.ok(state);
102
+ assert.ok(state.timestamp);
103
+ assert.equal(state.stages.length, 1);
104
+ assert.equal(state.stages[0].type, "rms");
105
+ assert.equal(state.stages[0].state.windowSize, 3);
106
+ assert.ok(state.stages[0].state.channels);
107
+
108
+ // Create new processor and load state
109
+ const processor2 = createDspPipeline();
110
+ processor2.Rms({ mode: "moving", windowSize: 3 });
111
+ await processor2.loadState(stateJson);
112
+
113
+ // Both should produce same output for next sample
114
+ const output1 = await processor.process(
115
+ new Float32Array([6]),
116
+ DEFAULT_OPTIONS
117
+ );
118
+ const output2 = await processor2.process(
119
+ new Float32Array([6]),
120
+ DEFAULT_OPTIONS
121
+ );
122
+
123
+ assertCloseTo(output2[0], output1[0]);
124
+ });
125
+
126
+ test("should reset state correctly", async () => {
127
+ processor.Rms({ mode: "moving", windowSize: 3 });
128
+
129
+ // Build state
130
+ await processor.process(new Float32Array([3, 4, 5]), DEFAULT_OPTIONS);
131
+
132
+ // Reset
133
+ processor.clearState();
134
+
135
+ // Should start fresh
136
+ const output = await processor.process(
137
+ new Float32Array([10]),
138
+ DEFAULT_OPTIONS
139
+ );
140
+ assertCloseTo(output[0], 10); // RMS of single value
141
+ });
142
+
143
+ test("should validate runningSumOfSquares on state load", async () => {
144
+ processor.Rms({ mode: "moving", windowSize: 3 });
145
+ await processor.process(new Float32Array([3, 4, 5]), DEFAULT_OPTIONS);
146
+
147
+ const stateJson = await processor.saveState();
148
+ const state = JSON.parse(stateJson);
149
+
150
+ // Corrupt runningSumOfSquares
151
+ if (state.stages[0].state.channels && state.stages[0].state.channels[0]) {
152
+ state.stages[0].state.channels[0].runningSumOfSquares = 9999;
153
+ }
154
+
155
+ // Should throw when loading corrupted state
156
+ const processor2 = createDspPipeline();
157
+ processor2.Rms({ mode: "moving", windowSize: 3 });
158
+ await assert.rejects(
159
+ async () => await processor2.loadState(JSON.stringify(state)),
160
+ /Running sum of squares validation failed/
161
+ );
162
+ });
163
+
164
+ test("should validate window size on state load", async () => {
165
+ processor.Rms({ mode: "moving", windowSize: 3 });
166
+ await processor.process(new Float32Array([3, 4, 5]), DEFAULT_OPTIONS);
167
+
168
+ const stateJson = await processor.saveState();
169
+ const state = JSON.parse(stateJson);
170
+
171
+ // Corrupt window size
172
+ if (state.stages[0].state) {
173
+ state.stages[0].state.windowSize = 5;
174
+ }
175
+
176
+ // Should throw when loading corrupted state
177
+ const processor2 = createDspPipeline();
178
+ processor2.Rms({ mode: "moving", windowSize: 3 });
179
+ await assert.rejects(
180
+ async () => await processor2.loadState(JSON.stringify(state)),
181
+ /Window size mismatch/
182
+ );
183
+ });
184
+ });
185
+
186
+ describe("Mathematical Properties", () => {
187
+ test("should compute RMS of constant signal correctly", async () => {
188
+ processor.Rms({ mode: "moving", windowSize: 4 });
189
+
190
+ const input = new Float32Array([5, 5, 5, 5]);
191
+ const output = await processor.process(input, DEFAULT_OPTIONS);
192
+
193
+ // RMS of constant signal equals the constant
194
+ output.forEach((val) => assertCloseTo(val, 5));
195
+ });
196
+
197
+ test("should compute RMS of zero signal", async () => {
198
+ processor.Rms({ mode: "moving", windowSize: 3 });
199
+
200
+ const input = new Float32Array([0, 0, 0, 0]);
201
+ const output = await processor.process(input, DEFAULT_OPTIONS);
202
+
203
+ output.forEach((val) => assertCloseTo(val, 0));
204
+ });
205
+
206
+ test("should handle alternating positive/negative", async () => {
207
+ processor.Rms({ mode: "moving", windowSize: 2 });
208
+
209
+ const input = new Float32Array([3, -3, 4, -4]);
210
+ const output = await processor.process(input, DEFAULT_OPTIONS);
211
+
212
+ // First: sqrt(9) = 3
213
+ // Second: sqrt((9 + 9) / 2) = 3
214
+ // Third: sqrt((9 + 16) / 2) = sqrt(12.5) = 3.5355...
215
+ // Fourth: sqrt((16 + 16) / 2) = 4
216
+ assertCloseTo(output[0], 3);
217
+ assertCloseTo(output[1], 3);
218
+ assertCloseTo(output[2], 3.5355, 4);
219
+ assertCloseTo(output[3], 4);
220
+ });
221
+
222
+ test("should produce value equal to or less than max absolute value", async () => {
223
+ processor.Rms({ mode: "moving", windowSize: 3 });
224
+
225
+ const input = new Float32Array([1, 5, 2]);
226
+ const output = await processor.process(input, DEFAULT_OPTIONS);
227
+
228
+ // RMS should be <= max absolute value for each window
229
+ assert.ok(output[0] <= 1);
230
+ assert.ok(output[1] <= 5);
231
+ assert.ok(output[2] <= 5);
232
+ });
233
+ });
234
+
235
+ describe("Edge Cases", () => {
236
+ test("should handle empty input array", async () => {
237
+ processor.Rms({ mode: "moving", windowSize: 3 });
238
+
239
+ const output = await processor.process(
240
+ new Float32Array([]),
241
+ DEFAULT_OPTIONS
242
+ );
243
+ assert.equal(output.length, 0);
244
+ });
245
+
246
+ test("should handle very small values", async () => {
247
+ processor.Rms({ mode: "moving", windowSize: 2 });
248
+
249
+ const input = new Float32Array([0.0001, 0.0002, 0.0001]);
250
+ const output = await processor.process(input, DEFAULT_OPTIONS);
251
+
252
+ assert.ok(output.every((v) => v > 0 && v < 0.001));
253
+ });
254
+
255
+ test("should handle very large values", async () => {
256
+ processor.Rms({ mode: "moving", windowSize: 2 });
257
+
258
+ const input = new Float32Array([1e6, 1e6]);
259
+ const output = await processor.process(input, DEFAULT_OPTIONS);
260
+
261
+ assertCloseTo(output[0], 1e6);
262
+ assertCloseTo(output[1], 1e6);
263
+ });
264
+
265
+ test("should handle mixed magnitude ranges", async () => {
266
+ processor.Rms({ mode: "moving", windowSize: 3 });
267
+
268
+ const input = new Float32Array([0.001, 1000, 0.001]);
269
+ const output = await processor.process(input, DEFAULT_OPTIONS);
270
+
271
+ assert.ok(output.every((v) => v >= 0));
272
+ assert.ok(output.some((v) => v > 100)); // Large value influences RMS
273
+ });
274
+ });
275
+
276
+ describe("Multi-channel Processing", () => {
277
+ test("should process data with stateful continuity", async () => {
278
+ processor.Rms({ mode: "moving", windowSize: 2 });
279
+
280
+ // First batch
281
+ const output1 = await processor.process(
282
+ new Float32Array([3, 4]),
283
+ DEFAULT_OPTIONS
284
+ );
285
+ assertCloseTo(output1[0], 3);
286
+ assertCloseTo(output1[1], 3.5355, 4);
287
+
288
+ // Second batch - continues from previous
289
+ const output2 = await processor.process(
290
+ new Float32Array([5, 0]),
291
+ DEFAULT_OPTIONS
292
+ );
293
+ // sqrt((16 + 25) / 2) = sqrt(20.5) = 4.5277...
294
+ assertCloseTo(output2[0], 4.5277, 4);
295
+ // sqrt((25 + 0) / 2) = sqrt(12.5) = 3.5355...
296
+ assertCloseTo(output2[1], 3.5355, 4);
297
+ });
298
+
299
+ test("should maintain separate state across multiple batches", async () => {
300
+ processor.Rms({ mode: "moving", windowSize: 2 });
301
+
302
+ const batches = [
303
+ new Float32Array([1, 2]),
304
+ new Float32Array([3, 4]),
305
+ new Float32Array([5, 6]),
306
+ ];
307
+
308
+ for (const batch of batches) {
309
+ const output = await processor.process(batch, DEFAULT_OPTIONS);
310
+ assert.equal(output.length, batch.length);
311
+ assert.ok(output.every((v) => v > 0));
312
+ }
313
+ });
314
+ });
315
+ });