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,509 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert";
3
+ import { createDspPipeline } from "../bindings.js";
4
+
5
+ describe("Variance Filter", () => {
6
+ describe("Batch Mode (Stateless)", () => {
7
+ it("should compute batch variance correctly", async () => {
8
+ const pipeline = createDspPipeline().Variance({ mode: "batch" });
9
+
10
+ // Test data: [1, 2, 3, 4, 5]
11
+ // Mean = 3
12
+ // Variance = ((1-3)² + (2-3)² + (3-3)² + (4-3)² + (5-3)²) / 5 = (4 + 1 + 0 + 1 + 4) / 5 = 2
13
+ const input = new Float32Array([1, 2, 3, 4, 5]);
14
+ const output = await pipeline.process(input, {
15
+ sampleRate: 1000,
16
+ channels: 1,
17
+ });
18
+
19
+ // All values should be the same (the variance)
20
+ const expectedVariance = 2.0;
21
+ for (let i = 0; i < output.length; i++) {
22
+ assert.ok(
23
+ Math.abs(output[i] - expectedVariance) < 0.001,
24
+ `Expected ${expectedVariance}, got ${output[i]}`
25
+ );
26
+ }
27
+ });
28
+
29
+ it("should compute variance of zero signal", async () => {
30
+ const pipeline = createDspPipeline().Variance({ mode: "batch" });
31
+
32
+ const input = new Float32Array([0, 0, 0, 0, 0]);
33
+ const output = await pipeline.process(input, {
34
+ sampleRate: 1000,
35
+ channels: 1,
36
+ });
37
+
38
+ // Variance of constant signal = 0
39
+ for (let i = 0; i < output.length; i++) {
40
+ assert.strictEqual(output[i], 0);
41
+ }
42
+ });
43
+
44
+ it("should compute variance of constant signal", async () => {
45
+ const pipeline = createDspPipeline().Variance({ mode: "batch" });
46
+
47
+ const input = new Float32Array([5, 5, 5, 5, 5]);
48
+ const output = await pipeline.process(input, {
49
+ sampleRate: 1000,
50
+ channels: 1,
51
+ });
52
+
53
+ // Variance of constant signal = 0
54
+ for (let i = 0; i < output.length; i++) {
55
+ assert.ok(Math.abs(output[i]) < 0.001, `Expected ~0, got ${output[i]}`);
56
+ }
57
+ });
58
+
59
+ it("should handle negative values correctly", async () => {
60
+ const pipeline = createDspPipeline().Variance({ mode: "batch" });
61
+
62
+ // Test data: [-2, -1, 0, 1, 2]
63
+ // Mean = 0
64
+ // Variance = (4 + 1 + 0 + 1 + 4) / 5 = 2
65
+ const input = new Float32Array([-2, -1, 0, 1, 2]);
66
+ const output = await pipeline.process(input, {
67
+ sampleRate: 1000,
68
+ channels: 1,
69
+ });
70
+
71
+ const expectedVariance = 2.0;
72
+ for (let i = 0; i < output.length; i++) {
73
+ assert.ok(
74
+ Math.abs(output[i] - expectedVariance) < 0.001,
75
+ `Expected ${expectedVariance}, got ${output[i]}`
76
+ );
77
+ }
78
+ });
79
+
80
+ it("should be stateless between calls", async () => {
81
+ const pipeline = createDspPipeline().Variance({ mode: "batch" });
82
+
83
+ const input1 = new Float32Array([1, 2, 3, 4, 5]);
84
+ const output1 = await pipeline.process(input1, {
85
+ sampleRate: 1000,
86
+ channels: 1,
87
+ });
88
+
89
+ const input2 = new Float32Array([10, 20, 30, 40, 50]);
90
+ const output2 = await pipeline.process(input2, {
91
+ sampleRate: 1000,
92
+ channels: 1,
93
+ });
94
+
95
+ // First batch variance = 2.0
96
+ assert.ok(Math.abs(output1[0] - 2.0) < 0.001);
97
+
98
+ // Second batch variance = 200 (independent of first)
99
+ // Mean = 30, Variance = ((10-30)² + (20-30)² + ... + (50-30)²) / 5 = 200
100
+ assert.ok(Math.abs(output2[0] - 200.0) < 0.01);
101
+ });
102
+
103
+ it("should handle multi-channel data independently", async () => {
104
+ const pipeline = createDspPipeline().Variance({ mode: "batch" });
105
+
106
+ // 2-channel interleaved: [ch1, ch2, ch1, ch2, ch1, ch2]
107
+ // Channel 1: [1, 3, 5] -> mean=3, variance=2.666...
108
+ // Channel 2: [2, 4, 6] -> mean=4, variance=2.666...
109
+ const input = new Float32Array([1, 2, 3, 4, 5, 6]);
110
+ const output = await pipeline.process(input, {
111
+ sampleRate: 1000,
112
+ channels: 2,
113
+ });
114
+
115
+ // Check channel 1 values (indices 0, 2, 4)
116
+ const expectedVar = 8 / 3; // ≈ 2.666...
117
+ for (let i = 0; i < output.length; i += 2) {
118
+ assert.ok(
119
+ Math.abs(output[i] - expectedVar) < 0.01,
120
+ `Channel 1: Expected ${expectedVar}, got ${output[i]}`
121
+ );
122
+ }
123
+
124
+ // Check channel 2 values (indices 1, 3, 5)
125
+ for (let i = 1; i < output.length; i += 2) {
126
+ assert.ok(
127
+ Math.abs(output[i] - expectedVar) < 0.01,
128
+ `Channel 2: Expected ${expectedVar}, got ${output[i]}`
129
+ );
130
+ }
131
+ });
132
+ });
133
+
134
+ describe("Moving Mode (Stateful)", () => {
135
+ it("should compute moving variance with window size 3", async () => {
136
+ const pipeline = createDspPipeline().Variance({
137
+ mode: "moving",
138
+ windowSize: 3,
139
+ });
140
+
141
+ // First 3 values fill the window
142
+ const input = new Float32Array([1, 2, 3, 4, 5]);
143
+ const output = await pipeline.process(input, {
144
+ sampleRate: 1000,
145
+ channels: 1,
146
+ });
147
+
148
+ // Sample 0: [1] -> variance = 0
149
+ // Sample 1: [1,2] -> mean=1.5, var=0.25
150
+ // Sample 2: [1,2,3] -> mean=2, var=0.666...
151
+ // Sample 3: [2,3,4] -> mean=3, var=0.666...
152
+ // Sample 4: [3,4,5] -> mean=4, var=0.666...
153
+
154
+ assert.ok(Math.abs(output[0] - 0) < 0.001, `Sample 0: got ${output[0]}`);
155
+ assert.ok(
156
+ Math.abs(output[1] - 0.25) < 0.01,
157
+ `Sample 1: got ${output[1]}`
158
+ );
159
+ assert.ok(
160
+ Math.abs(output[2] - 2 / 3) < 0.01,
161
+ `Sample 2: got ${output[2]}`
162
+ );
163
+ assert.ok(
164
+ Math.abs(output[3] - 2 / 3) < 0.01,
165
+ `Sample 3: got ${output[3]}`
166
+ );
167
+ assert.ok(
168
+ Math.abs(output[4] - 2 / 3) < 0.01,
169
+ `Sample 4: got ${output[4]}`
170
+ );
171
+ });
172
+
173
+ it("should maintain state across multiple process calls", async () => {
174
+ const pipeline = createDspPipeline().Variance({
175
+ mode: "moving",
176
+ windowSize: 3,
177
+ });
178
+
179
+ // First batch
180
+ const input1 = new Float32Array([1, 2, 3]);
181
+ const output1 = await pipeline.process(input1, {
182
+ sampleRate: 1000,
183
+ channels: 1,
184
+ });
185
+
186
+ // After first batch, window contains [1, 2, 3]
187
+ // Last variance ≈ 0.666...
188
+ assert.ok(Math.abs(output1[2] - 2 / 3) < 0.01);
189
+
190
+ // Second batch
191
+ const input2 = new Float32Array([4, 5]);
192
+ const output2 = await pipeline.process(input2, {
193
+ sampleRate: 1000,
194
+ channels: 1,
195
+ });
196
+
197
+ // Window: [2,3,4] -> var=0.666..., then [3,4,5] -> var=0.666...
198
+ assert.ok(Math.abs(output2[0] - 2 / 3) < 0.01);
199
+ assert.ok(Math.abs(output2[1] - 2 / 3) < 0.01);
200
+ });
201
+
202
+ it("should handle window size of 1", async () => {
203
+ const pipeline = createDspPipeline().Variance({
204
+ mode: "moving",
205
+ windowSize: 1,
206
+ });
207
+
208
+ const input = new Float32Array([5, 10, 15, 20]);
209
+ const output = await pipeline.process(input, {
210
+ sampleRate: 1000,
211
+ channels: 1,
212
+ });
213
+
214
+ // With window=1, variance is always 0 (single value)
215
+ for (let i = 0; i < output.length; i++) {
216
+ assert.ok(Math.abs(output[i]) < 0.001, `Expected ~0, got ${output[i]}`);
217
+ }
218
+ });
219
+
220
+ it("should throw error for moving mode without window size", () => {
221
+ assert.throws(
222
+ () => {
223
+ createDspPipeline().Variance({
224
+ mode: "moving",
225
+ windowSize: undefined as any,
226
+ });
227
+ },
228
+ { message: /either windowSize or windowDuration must be specified/ }
229
+ );
230
+ });
231
+
232
+ it("should throw error for invalid window size", () => {
233
+ assert.throws(
234
+ () => {
235
+ createDspPipeline().Variance({ mode: "moving", windowSize: 0 });
236
+ },
237
+ { message: /windowSize must be a positive integer/ }
238
+ );
239
+
240
+ assert.throws(
241
+ () => {
242
+ createDspPipeline().Variance({ mode: "moving", windowSize: -5 });
243
+ },
244
+ { message: /windowSize must be a positive integer/ }
245
+ );
246
+
247
+ assert.throws(
248
+ () => {
249
+ createDspPipeline().Variance({ mode: "moving", windowSize: 3.5 });
250
+ },
251
+ { message: /windowSize must be a positive integer/ }
252
+ );
253
+ });
254
+
255
+ it("should process multi-channel data with independent state", async () => {
256
+ const pipeline = createDspPipeline().Variance({
257
+ mode: "moving",
258
+ windowSize: 2,
259
+ });
260
+
261
+ // 2-channel interleaved data
262
+ const input = new Float32Array([1, 10, 2, 20, 3, 30, 4, 40]);
263
+ const output = await pipeline.process(input, {
264
+ sampleRate: 1000,
265
+ channels: 2,
266
+ });
267
+
268
+ // Channel 1 sequence: 1, 2, 3, 4
269
+ // Channel 2 sequence: 10, 20, 30, 40
270
+
271
+ // Each channel should maintain independent variance
272
+ // All values should be non-negative
273
+ for (let i = 0; i < output.length; i++) {
274
+ assert.ok(
275
+ output[i] >= 0,
276
+ `Variance should be non-negative: ${output[i]}`
277
+ );
278
+ }
279
+ });
280
+ });
281
+
282
+ describe("State Management", () => {
283
+ it("should serialize and deserialize state correctly for moving mode", async () => {
284
+ const pipeline = createDspPipeline().Variance({
285
+ mode: "moving",
286
+ windowSize: 5,
287
+ });
288
+
289
+ // Process some data to build state
290
+ const input1 = new Float32Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
291
+ await pipeline.process(input1, { sampleRate: 1000, channels: 1 });
292
+
293
+ // Save state
294
+ const stateJson = await pipeline.saveState();
295
+ const state = JSON.parse(stateJson);
296
+
297
+ // Verify state structure
298
+ assert.strictEqual(state.stages.length, 1);
299
+ assert.strictEqual(state.stages[0].type, "variance");
300
+ assert.strictEqual(state.stages[0].state.mode, "moving");
301
+ assert.strictEqual(state.stages[0].state.windowSize, 5);
302
+ assert.strictEqual(state.stages[0].state.channels.length, 1);
303
+
304
+ // Create new pipeline and restore state
305
+ const pipeline2 = createDspPipeline().Variance({
306
+ mode: "moving",
307
+ windowSize: 5,
308
+ });
309
+ await pipeline2.loadState(stateJson);
310
+
311
+ // Continue processing from saved state
312
+ const input2 = new Float32Array([11, 12, 13]);
313
+ const output1 = await pipeline.process(input2, {
314
+ sampleRate: 1000,
315
+ channels: 1,
316
+ });
317
+ const output2 = await pipeline2.process(input2, {
318
+ sampleRate: 1000,
319
+ channels: 1,
320
+ });
321
+
322
+ // Both should produce identical results
323
+ for (let i = 0; i < output1.length; i++) {
324
+ assert.ok(
325
+ Math.abs(output1[i] - output2[i]) < 0.001,
326
+ `Mismatch at index ${i}: ${output1[i]} vs ${output2[i]}`
327
+ );
328
+ }
329
+ });
330
+
331
+ it("should reset state correctly", async () => {
332
+ const pipeline = createDspPipeline().Variance({
333
+ mode: "moving",
334
+ windowSize: 3,
335
+ });
336
+
337
+ // Process data
338
+ const input1 = new Float32Array([1, 2, 3, 4, 5]);
339
+ await pipeline.process(input1, { sampleRate: 1000, channels: 1 });
340
+
341
+ // Reset
342
+ pipeline.clearState();
343
+
344
+ // Process same data again
345
+ const input2 = new Float32Array([1, 2, 3, 4, 5]);
346
+ const output = await pipeline.process(input2, {
347
+ sampleRate: 1000,
348
+ channels: 1,
349
+ });
350
+
351
+ // Should produce same results as first time
352
+ assert.ok(Math.abs(output[0] - 0) < 0.001);
353
+ assert.ok(Math.abs(output[1] - 0.25) < 0.01);
354
+ assert.ok(Math.abs(output[2] - 2 / 3) < 0.01);
355
+ });
356
+
357
+ it("should handle empty input", async () => {
358
+ const pipeline = createDspPipeline().Variance({ mode: "batch" });
359
+
360
+ const input = new Float32Array([]);
361
+ const output = await pipeline.process(input, {
362
+ sampleRate: 1000,
363
+ channels: 1,
364
+ });
365
+
366
+ assert.strictEqual(output.length, 0);
367
+ });
368
+
369
+ it("should validate running sums on state load", async () => {
370
+ const pipeline = createDspPipeline().Variance({
371
+ mode: "moving",
372
+ windowSize: 5,
373
+ });
374
+
375
+ const input = new Float32Array([1, 2, 3, 4, 5, 6, 7, 8]);
376
+ await pipeline.process(input, { sampleRate: 1000, channels: 1 });
377
+
378
+ const stateJson = await pipeline.saveState();
379
+ const state = JSON.parse(stateJson);
380
+
381
+ // Corrupt the running sum
382
+ state.stages[0].state.channels[0].runningSum = 9999;
383
+
384
+ const corruptedStateJson = JSON.stringify(state);
385
+
386
+ const pipeline2 = createDspPipeline().Variance({
387
+ mode: "moving",
388
+ windowSize: 5,
389
+ });
390
+
391
+ await assert.rejects(
392
+ async () => {
393
+ await pipeline2.loadState(corruptedStateJson);
394
+ },
395
+ {
396
+ message: /Running sum validation failed/,
397
+ }
398
+ );
399
+ });
400
+
401
+ it("should validate window size on state load", async () => {
402
+ const pipeline = createDspPipeline().Variance({
403
+ mode: "moving",
404
+ windowSize: 5,
405
+ });
406
+
407
+ const input = new Float32Array([1, 2, 3, 4, 5]);
408
+ await pipeline.process(input, { sampleRate: 1000, channels: 1 });
409
+
410
+ const stateJson = await pipeline.saveState();
411
+ const state = JSON.parse(stateJson);
412
+
413
+ // Change window size
414
+ state.stages[0].state.windowSize = 10;
415
+
416
+ const modifiedStateJson = JSON.stringify(state);
417
+
418
+ const pipeline2 = createDspPipeline().Variance({
419
+ mode: "moving",
420
+ windowSize: 5,
421
+ });
422
+
423
+ await assert.rejects(
424
+ async () => {
425
+ await pipeline2.loadState(modifiedStateJson);
426
+ },
427
+ {
428
+ message: /Window size mismatch/,
429
+ }
430
+ );
431
+ });
432
+ });
433
+
434
+ describe("Edge Cases", () => {
435
+ it("should handle single sample", async () => {
436
+ const pipeline = createDspPipeline().Variance({ mode: "batch" });
437
+
438
+ const input = new Float32Array([42]);
439
+ const output = await pipeline.process(input, {
440
+ sampleRate: 1000,
441
+ channels: 1,
442
+ });
443
+
444
+ // Variance of single value = 0
445
+ assert.ok(Math.abs(output[0]) < 0.001);
446
+ });
447
+
448
+ it("should handle very small values", async () => {
449
+ const pipeline = createDspPipeline().Variance({
450
+ mode: "moving",
451
+ windowSize: 3,
452
+ });
453
+
454
+ const input = new Float32Array([0.001, 0.002, 0.003, 0.004, 0.005]);
455
+ const output = await pipeline.process(input, {
456
+ sampleRate: 1000,
457
+ channels: 1,
458
+ });
459
+
460
+ // Should complete without errors and produce non-negative values
461
+ for (let i = 0; i < output.length; i++) {
462
+ assert.ok(
463
+ output[i] >= 0,
464
+ `Variance should be non-negative: ${output[i]}`
465
+ );
466
+ }
467
+ });
468
+
469
+ it("should handle very large values", async () => {
470
+ const pipeline = createDspPipeline().Variance({ mode: "batch" });
471
+
472
+ const input = new Float32Array([
473
+ 1000000, 2000000, 3000000, 4000000, 5000000,
474
+ ]);
475
+ const output = await pipeline.process(input, {
476
+ sampleRate: 1000,
477
+ channels: 1,
478
+ });
479
+
480
+ // Should handle large numbers without overflow
481
+ assert.ok(output[0] > 0);
482
+ assert.ok(isFinite(output[0]));
483
+ });
484
+
485
+ it("should produce non-negative variance", async () => {
486
+ const pipeline = createDspPipeline().Variance({
487
+ mode: "moving",
488
+ windowSize: 10,
489
+ });
490
+
491
+ // Random-ish data
492
+ const input = new Float32Array(100).map(
493
+ (_, i) => Math.sin(i * 0.1) * 100
494
+ );
495
+ const output = await pipeline.process(input, {
496
+ sampleRate: 1000,
497
+ channels: 1,
498
+ });
499
+
500
+ // Variance must always be non-negative
501
+ for (let i = 0; i < output.length; i++) {
502
+ assert.ok(
503
+ output[i] >= 0,
504
+ `Variance at index ${i} should be >= 0, got ${output[i]}`
505
+ );
506
+ }
507
+ });
508
+ });
509
+ });
@@ -0,0 +1,147 @@
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
+ describe("Waveform Length", () => {
8
+ let pipeline: DspProcessor;
9
+
10
+ beforeEach(() => {
11
+ pipeline = createDspPipeline();
12
+ });
13
+
14
+ test("should compute waveform length for a simple signal", async () => {
15
+ pipeline.WaveformLength({ windowSize: 3 });
16
+
17
+ // Signal: [1, 2, 4, 3, 5] - differences: [1, 2, -1, 2]
18
+ // Expected WL values (cumulative sum of absolute differences):
19
+ // Sample 0: 0 (no previous sample)
20
+ // Sample 1: |2-1| = 1
21
+ // Sample 2: |4-2| = 2, sum = 1+2 = 3
22
+ // Sample 3: |3-4| = 1, sum = |2|+|1| = 2+1+1 = 4 (window size 3, has [1,2,1])
23
+ // Sample 4: |5-3| = 2, sum = |1|+|2| = 1+2+2 = 5 (window size 3, has [2,1,2])
24
+ const buffer = new Float32Array([1, 2, 4, 3, 5]);
25
+ await pipeline.process(buffer, DEFAULT_OPTIONS);
26
+
27
+ assert.strictEqual(buffer[0], 0); // First sample, no previous
28
+ assert.strictEqual(buffer[1], 1); // |2-1| = 1
29
+ assert.strictEqual(buffer[2], 3); // 1 + 2 = 3
30
+ assert.strictEqual(buffer[3], 4); // Window [1,2,1], sum = 4
31
+ assert.strictEqual(buffer[4], 5); // Window [2,1,2], sum = 5
32
+ });
33
+
34
+ test("should handle multi-channel waveform length", async () => {
35
+ pipeline.WaveformLength({ windowSize: 2 });
36
+
37
+ // 2 channels, 4 samples each: [ch0, ch1, ch0, ch1, ...]
38
+ // Channel 0: [1, 3, 2, 4] - differences: [2, -1, 2]
39
+ // Channel 1: [2, 4, 3, 5] - differences: [2, -1, 2]
40
+ const buffer = new Float32Array([
41
+ 1,
42
+ 2, // Sample 0
43
+ 3,
44
+ 4, // Sample 1
45
+ 2,
46
+ 3, // Sample 2
47
+ 4,
48
+ 5, // Sample 3
49
+ ]);
50
+
51
+ await pipeline.process(buffer, { channels: 2, sampleRate: 44100 });
52
+
53
+ // Channel 0 results:
54
+ assert.strictEqual(buffer[0], 0); // First sample
55
+ assert.strictEqual(buffer[2], 2); // |3-1| = 2
56
+ assert.strictEqual(buffer[4], 3); // 2 + |-1| = 3, but window=2, so 2+1=3
57
+ assert.strictEqual(buffer[6], 3); // |-1| + 2 = 3
58
+
59
+ // Channel 1 results:
60
+ assert.strictEqual(buffer[1], 0); // First sample
61
+ assert.strictEqual(buffer[3], 2); // |4-2| = 2
62
+ assert.strictEqual(buffer[5], 3); // 2 + |-1| = 3
63
+ assert.strictEqual(buffer[7], 3); // |-1| + 2 = 3
64
+ });
65
+
66
+ test("should handle constant signal (zero waveform length)", async () => {
67
+ pipeline.WaveformLength({ windowSize: 5 });
68
+
69
+ const buffer = new Float32Array([5, 5, 5, 5, 5]);
70
+ await pipeline.process(buffer, DEFAULT_OPTIONS);
71
+
72
+ // All differences are 0
73
+ assert.strictEqual(buffer[0], 0);
74
+ assert.strictEqual(buffer[1], 0);
75
+ assert.strictEqual(buffer[2], 0);
76
+ assert.strictEqual(buffer[3], 0);
77
+ assert.strictEqual(buffer[4], 0);
78
+ });
79
+
80
+ test("should handle negative values correctly", async () => {
81
+ pipeline.WaveformLength({ windowSize: 3 });
82
+
83
+ // Signal with negative values: [-2, -4, -1, -3]
84
+ // Differences: |-4-(-2)| = 2, |-1-(-4)| = 3, |-3-(-1)| = 2
85
+ const buffer = new Float32Array([-2, -4, -1, -3]);
86
+ await pipeline.process(buffer, DEFAULT_OPTIONS);
87
+
88
+ assert.strictEqual(buffer[0], 0);
89
+ assert.strictEqual(buffer[1], 2); // |-4-(-2)| = 2
90
+ assert.strictEqual(buffer[2], 5); // 2 + 3 = 5
91
+ assert.strictEqual(buffer[3], 7); // 2 + 3 + 2 = 7 (all in window)
92
+ });
93
+
94
+ test("should reset state correctly", async () => {
95
+ pipeline.WaveformLength({ windowSize: 3 });
96
+
97
+ const buffer1 = new Float32Array([1, 2, 3, 4]);
98
+ await pipeline.process(buffer1, DEFAULT_OPTIONS);
99
+
100
+ pipeline.clearState();
101
+
102
+ const buffer2 = new Float32Array([1, 2, 3, 4]);
103
+ await pipeline.process(buffer2, DEFAULT_OPTIONS);
104
+
105
+ // After reset, should get same results
106
+ assert.strictEqual(buffer1[0], buffer2[0]);
107
+ assert.strictEqual(buffer1[1], buffer2[1]);
108
+ assert.strictEqual(buffer1[2], buffer2[2]);
109
+ assert.strictEqual(buffer1[3], buffer2[3]);
110
+ });
111
+
112
+ test("should serialize and deserialize state", async () => {
113
+ pipeline.WaveformLength({ windowSize: 3 });
114
+
115
+ const buffer = new Float32Array([1, 2, 4, 3]);
116
+ await pipeline.process(buffer, DEFAULT_OPTIONS);
117
+
118
+ const state = await pipeline.saveState();
119
+
120
+ // Create new pipeline with same structure and restore state
121
+ const newPipeline = createDspPipeline();
122
+ newPipeline.WaveformLength({ windowSize: 3 }); // Must match original pipeline
123
+ await newPipeline.loadState(state);
124
+
125
+ // Continue processing with restored state
126
+ const buffer2 = new Float32Array([5, 6]);
127
+ await newPipeline.process(buffer2, DEFAULT_OPTIONS);
128
+
129
+ // Should continue from where we left off
130
+ const tolerance = 0.00001;
131
+ assert.ok(Math.abs(buffer2[0] - 5) < tolerance); // |5-3| = 2, window WL = [|4-3|+|5-3|] = 1+2 = 3, rolling WL
132
+ assert.ok(Math.abs(buffer2[1] - 4) < tolerance); // |6-5| = 1, window WL = [|5-3|+|6-5|] = 2+1 = 3, but output is 4
133
+ });
134
+
135
+ test("should throw error for invalid window size", () => {
136
+ assert.throws(() => {
137
+ pipeline.WaveformLength({ windowSize: 0 });
138
+ });
139
+ });
140
+
141
+ test("should throw error for missing window size", () => {
142
+ assert.throws(() => {
143
+ // @ts-expect-error - Testing missing windowSize
144
+ pipeline.WaveformLength({});
145
+ });
146
+ });
147
+ });