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,284 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert";
3
+ import { createDspPipeline } from "../index.js";
4
+
5
+ test("Time-Based Variance", async (t) => {
6
+ await t.test("should expire samples based on age for Variance", async () => {
7
+ const pipeline = createDspPipeline();
8
+ // 1 second window
9
+ pipeline.Variance({ mode: "moving", windowDuration: 1000 });
10
+
11
+ // First 3 samples at 0, 100, 200ms - values 1, 2, 3
12
+ const chunk1 = new Float32Array([1, 2, 3]);
13
+ const ts1 = new Float32Array([0, 100, 200]);
14
+ const result1 = await pipeline.process(chunk1, ts1, { channels: 1 });
15
+
16
+ // At 200ms: Variance([1, 2, 3])
17
+ // Mean = 2, Variance = ((1-2)^2 + (2-2)^2 + (3-2)^2) / 3 = (1 + 0 + 1) / 3 = 0.667
18
+ const expectedVar1 = 2 / 3; // 0.667
19
+ assert.ok(
20
+ Math.abs(result1[2] - expectedVar1) < 0.01,
21
+ `Expected Variance ≈ 0.667, got ${result1[2]}`
22
+ );
23
+
24
+ // Sample at 2200ms - 2.2 seconds later (>1s window)
25
+ // Samples at 0, 100, 200ms should be EXPIRED
26
+ const chunk2 = new Float32Array([10]);
27
+ const ts2 = new Float32Array([2200]);
28
+ const result2 = await pipeline.process(chunk2, ts2, { channels: 1 });
29
+
30
+ // At 2200ms: only sample at 2200ms remains, Variance([10]) = 0
31
+ assert.ok(
32
+ Math.abs(result2[0] - 0.0) < 0.01,
33
+ `Expected Variance = 0.0, got ${result2[0]}`
34
+ );
35
+ });
36
+
37
+ await t.test("should work with irregular sampling for Variance", async () => {
38
+ const pipeline = createDspPipeline();
39
+ // 500ms window
40
+ pipeline.Variance({ mode: "moving", windowDuration: 500 });
41
+
42
+ const samples = new Float32Array([2, 4, 10, 12]);
43
+ const timestamps = new Float32Array([
44
+ 0, // Sample at 0ms
45
+ 50, // Sample at 50ms
46
+ 600, // Sample at 600ms (550ms gap!)
47
+ 650, // Sample at 650ms
48
+ ]);
49
+
50
+ const result = await pipeline.process(samples, timestamps, { channels: 1 });
51
+
52
+ // At 650ms with 500ms window:
53
+ // - Sample at 0ms EXPIRED (650 - 0 = 650ms > 500ms)
54
+ // - Sample at 50ms EXPIRED (650 - 50 = 600ms > 500ms)
55
+ // - Sample at 600ms KEPT (650 - 600 = 50ms < 500ms)
56
+ // - Sample at 650ms KEPT (current)
57
+ // Variance([10, 12]) = ((10-11)^2 + (12-11)^2) / 2 = (1 + 1) / 2 = 1
58
+ assert.ok(
59
+ Math.abs(result[3] - 1.0) < 0.01,
60
+ `Expected Variance = 1.0, got ${result[3]}`
61
+ );
62
+ });
63
+
64
+ await t.test(
65
+ "should handle streaming with time-based windows for Variance",
66
+ async () => {
67
+ const pipeline = createDspPipeline();
68
+ // 300ms window
69
+ pipeline.Variance({ mode: "moving", windowDuration: 300 });
70
+
71
+ // First chunk: samples at 0, 100, 200ms - values 2, 4, 6
72
+ const chunk1 = new Float32Array([2, 4, 6]);
73
+ const ts1 = new Float32Array([0, 100, 200]);
74
+ const result1 = await pipeline.process(chunk1, ts1, { channels: 1 });
75
+
76
+ // At 200ms: Variance([2, 4, 6])
77
+ // Mean = 4, Variance = ((2-4)^2 + (4-4)^2 + (6-4)^2) / 3 = (4 + 0 + 4) / 3 ≈ 2.667
78
+ assert.ok(
79
+ Math.abs(result1[2] - 8 / 3) < 0.01,
80
+ "First chunk Variance should be ~2.667"
81
+ );
82
+
83
+ // Second chunk: samples at 250, 600ms
84
+ const chunk2 = new Float32Array([8, 20]);
85
+ const ts2 = new Float32Array([250, 600]);
86
+ const result2 = await pipeline.process(chunk2, ts2, { channels: 1 });
87
+
88
+ // At 250ms: all previous samples still valid (within 300ms)
89
+ // Variance([2, 4, 6, 8])
90
+ // Mean = 5, Variance = ((2-5)^2 + (4-5)^2 + (6-5)^2 + (8-5)^2) / 4 = (9 + 1 + 1 + 9) / 4 = 5
91
+ assert.ok(
92
+ Math.abs(result2[0] - 5.0) < 0.01,
93
+ `At 250ms expected Variance = 5.0, got ${result2[0]}`
94
+ );
95
+
96
+ // At 600ms: samples at 0, 100, 200, 250 all expired (>300ms old)
97
+ // Variance([20]) = 0
98
+ assert.ok(
99
+ Math.abs(result2[1] - 0.0) < 0.01,
100
+ `At 600ms expected Variance = 0, got ${result2[1]}`
101
+ );
102
+ }
103
+ );
104
+ });
105
+
106
+ test("Time-Based Z-Score Normalization", async (t) => {
107
+ await t.test("should expire samples based on age for Z-Score", async () => {
108
+ const pipeline = createDspPipeline();
109
+ // 1 second window
110
+ pipeline.ZScoreNormalize({
111
+ mode: "moving",
112
+ windowDuration: 1000,
113
+ epsilon: 1e-6,
114
+ });
115
+
116
+ // First 3 samples at 0, 100, 200ms - values 10, 20, 30
117
+ const chunk1 = new Float32Array([10, 20, 30]);
118
+ const ts1 = new Float32Array([0, 100, 200]);
119
+ const result1 = await pipeline.process(chunk1, ts1, { channels: 1 });
120
+
121
+ // At 200ms: Z-Score of 30 given [10, 20, 30]
122
+ // Mean = 20, StdDev = sqrt(Variance) = sqrt(66.667) ≈ 8.165
123
+ // Z-Score = (30 - 20) / 8.165 ≈ 1.225
124
+ const mean1 = 20;
125
+ const variance1 = 200 / 3; // ((10-20)^2 + (20-20)^2 + (30-20)^2) / 3
126
+ const stddev1 = Math.sqrt(variance1);
127
+ const expectedZ1 = (30 - mean1) / stddev1;
128
+ assert.ok(
129
+ Math.abs(result1[2] - expectedZ1) < 0.01,
130
+ `Expected Z-Score ≈ ${expectedZ1.toFixed(3)}, got ${result1[2]}`
131
+ );
132
+
133
+ // Sample at 2200ms - 2.2 seconds later (>1s window)
134
+ // Samples at 0, 100, 200ms should be EXPIRED
135
+ const chunk2 = new Float32Array([50]);
136
+ const ts2 = new Float32Array([2200]);
137
+ const result2 = await pipeline.process(chunk2, ts2, { channels: 1 });
138
+
139
+ // At 2200ms: only sample at 2200ms remains
140
+ // Z-Score([50]) = 0 (stddev is 0, so returns 0)
141
+ assert.ok(
142
+ Math.abs(result2[0] - 0.0) < 0.01,
143
+ `Expected Z-Score = 0.0, got ${result2[0]}`
144
+ );
145
+ });
146
+
147
+ await t.test("should work with irregular sampling for Z-Score", async () => {
148
+ const pipeline = createDspPipeline();
149
+ // 500ms window
150
+ pipeline.ZScoreNormalize({
151
+ mode: "moving",
152
+ windowDuration: 500,
153
+ epsilon: 1e-6,
154
+ });
155
+
156
+ const samples = new Float32Array([10, 20, 100, 110]);
157
+ const timestamps = new Float32Array([
158
+ 0, // Sample at 0ms
159
+ 50, // Sample at 50ms
160
+ 600, // Sample at 600ms (550ms gap!)
161
+ 650, // Sample at 650ms
162
+ ]);
163
+
164
+ const result = await pipeline.process(samples, timestamps, {
165
+ channels: 1,
166
+ });
167
+
168
+ // At 650ms with 500ms window:
169
+ // - Sample at 0ms EXPIRED (650 - 0 = 650ms > 500ms)
170
+ // - Sample at 50ms EXPIRED (650 - 50 = 600ms > 500ms)
171
+ // - Sample at 600ms KEPT (650 - 600 = 50ms < 500ms)
172
+ // - Sample at 650ms KEPT (current)
173
+ // Z-Score of 110 given [100, 110]
174
+ // Mean = 105, StdDev = sqrt(25) = 5
175
+ // Z-Score = (110 - 105) / 5 = 1.0
176
+ assert.ok(
177
+ Math.abs(result[3] - 1.0) < 0.01,
178
+ `Expected Z-Score = 1.0, got ${result[3]}`
179
+ );
180
+ });
181
+
182
+ await t.test(
183
+ "should handle streaming with time-based windows for Z-Score",
184
+ async () => {
185
+ const pipeline = createDspPipeline();
186
+ // 300ms window
187
+ pipeline.ZScoreNormalize({
188
+ mode: "moving",
189
+ windowDuration: 300,
190
+ epsilon: 1e-6,
191
+ });
192
+
193
+ // First chunk: samples at 0, 100, 200ms - values 0, 10, 20
194
+ const chunk1 = new Float32Array([0, 10, 20]);
195
+ const ts1 = new Float32Array([0, 100, 200]);
196
+ const result1 = await pipeline.process(chunk1, ts1, { channels: 1 });
197
+
198
+ // At 200ms: Z-Score of 20 given [0, 10, 20]
199
+ // Mean = 10, Variance = 66.667, StdDev ≈ 8.165
200
+ // Z-Score = (20 - 10) / 8.165 ≈ 1.225
201
+ const mean1 = 10;
202
+ const variance1 = 200 / 3;
203
+ const stddev1 = Math.sqrt(variance1);
204
+ const expectedZ1 = (20 - mean1) / stddev1;
205
+ assert.ok(
206
+ Math.abs(result1[2] - expectedZ1) < 0.01,
207
+ "First chunk Z-Score should be ~1.225"
208
+ );
209
+
210
+ // Second chunk: samples at 250, 600ms
211
+ const chunk2 = new Float32Array([30, 100]);
212
+ const ts2 = new Float32Array([250, 600]);
213
+ const result2 = await pipeline.process(chunk2, ts2, { channels: 1 });
214
+
215
+ // At 250ms: all previous samples still valid (within 300ms)
216
+ // Z-Score of 30 given [0, 10, 20, 30]
217
+ // Mean = 15, Variance = 125, StdDev ≈ 11.18
218
+ // Z-Score = (30 - 15) / 11.18 ≈ 1.342
219
+ const mean2 = 15;
220
+ const variance2 = 125;
221
+ const stddev2 = Math.sqrt(variance2);
222
+ const expectedZ2 = (30 - mean2) / stddev2;
223
+ assert.ok(
224
+ Math.abs(result2[0] - expectedZ2) < 0.01,
225
+ `At 250ms expected Z-Score ≈ ${expectedZ2.toFixed(3)}, got ${
226
+ result2[0]
227
+ }`
228
+ );
229
+
230
+ // At 600ms: samples at 0, 100, 200, 250 all expired (>300ms old)
231
+ // Z-Score([100]) = 0 (single sample, stddev = 0)
232
+ assert.ok(
233
+ Math.abs(result2[1] - 0.0) < 0.01,
234
+ `At 600ms expected Z-Score = 0, got ${result2[1]}`
235
+ );
236
+ }
237
+ );
238
+ });
239
+
240
+ test("Backward Compatibility - Variance and Z-Score", async (t) => {
241
+ await t.test("Variance should work without timestamps", async () => {
242
+ const pipeline = createDspPipeline();
243
+ pipeline.Variance({ mode: "moving", windowSize: 3 });
244
+
245
+ const samples = new Float32Array([1, 2, 3, 10]);
246
+ const result = await pipeline.process(samples, { channels: 1 });
247
+
248
+ // Should use sample-count mode (last 3 samples)
249
+ // At sample 4: Variance([2, 3, 10])
250
+ // Mean = 5, Variance = ((2-5)^2 + (3-5)^2 + (10-5)^2) / 3 = (9 + 4 + 25) / 3 ≈ 12.667
251
+ assert.ok(
252
+ Math.abs(result[3] - 38 / 3) < 0.01,
253
+ "Variance should work in sample-count mode"
254
+ );
255
+ });
256
+
257
+ await t.test("Z-Score should work without timestamps", async () => {
258
+ const pipeline = createDspPipeline();
259
+ pipeline.ZScoreNormalize({
260
+ mode: "moving",
261
+ windowSize: 3,
262
+ epsilon: 1e-6,
263
+ });
264
+
265
+ const samples = new Float32Array([10, 20, 30, 100]);
266
+ const result = await pipeline.process(samples, { channels: 1 });
267
+
268
+ // Should use sample-count mode (last 3 samples)
269
+ // At sample 4: Z-Score of 100 given [20, 30, 100]
270
+ // Mean = 50, Variance = ((20-50)^2 + (30-50)^2 + (100-50)^2) / 3 = (900 + 400 + 2500) / 3 = 3800/3
271
+ // StdDev ≈ 35.59
272
+ // Z-Score = (100 - 50) / 35.59 ≈ 1.405
273
+ const mean = 50;
274
+ const variance = 3800 / 3;
275
+ const stddev = Math.sqrt(variance);
276
+ const expectedZ = (100 - mean) / stddev;
277
+ assert.ok(
278
+ Math.abs(result[3] - expectedZ) < 0.01,
279
+ `Z-Score should work in sample-count mode, expected ${expectedZ.toFixed(
280
+ 3
281
+ )}, got ${result[3]}`
282
+ );
283
+ });
284
+ });
@@ -0,0 +1,254 @@
1
+ import { describe, test } from "node:test";
2
+ import assert from "node:assert";
3
+ import { createDspPipeline } from "../index.js";
4
+
5
+ describe("Time-Series Processing", () => {
6
+ describe("Process with Timestamps", () => {
7
+ test("should accept timestamps array (legacy sample-based)", async () => {
8
+ const pipeline = createDspPipeline();
9
+ pipeline.MovingAverage({ mode: "moving", windowSize: 3 });
10
+
11
+ const samples = new Float32Array([1, 2, 3, 4, 5]);
12
+ const timestamps = new Float32Array([0, 1, 2, 3, 4]); // Sample indices
13
+
14
+ const output = await pipeline.process(samples, timestamps, {
15
+ channels: 1,
16
+ });
17
+
18
+ assert.ok(output instanceof Float32Array);
19
+ assert.strictEqual(output.length, 5);
20
+ });
21
+
22
+ test("should accept timestamps with milliseconds", async () => {
23
+ const pipeline = createDspPipeline();
24
+ pipeline.MovingAverage({ mode: "moving", windowSize: 3 });
25
+
26
+ const samples = new Float32Array([1, 2, 3, 4, 5]);
27
+ // Timestamps at 100ms intervals
28
+ const timestamps = new Float32Array([0, 100, 200, 300, 400]);
29
+
30
+ const output = await pipeline.process(samples, timestamps, {
31
+ channels: 1,
32
+ });
33
+
34
+ assert.ok(output instanceof Float32Array);
35
+ assert.strictEqual(output.length, 5);
36
+ });
37
+
38
+ test("should validate timestamp length matches sample length", async () => {
39
+ const pipeline = createDspPipeline();
40
+ pipeline.MovingAverage({ mode: "moving", windowSize: 3 });
41
+
42
+ const samples = new Float32Array([1, 2, 3, 4, 5]);
43
+ const timestamps = new Float32Array([0, 100, 200]); // Wrong length!
44
+
45
+ await assert.rejects(
46
+ async () => {
47
+ await pipeline.process(samples, timestamps, { channels: 1 });
48
+ },
49
+ {
50
+ message: /Timestamp.*length.*must match.*sample.*length/i,
51
+ }
52
+ );
53
+ });
54
+
55
+ test("should auto-generate timestamps from sampleRate", async () => {
56
+ const pipeline = createDspPipeline();
57
+ pipeline.MovingAverage({ mode: "moving", windowSize: 3 });
58
+
59
+ const samples = new Float32Array([1, 2, 3, 4, 5]);
60
+
61
+ // Legacy mode: auto-generates timestamps from sampleRate
62
+ const output = await pipeline.process(samples, {
63
+ sampleRate: 100, // 100 Hz = 10ms per sample
64
+ channels: 1,
65
+ });
66
+
67
+ assert.ok(output instanceof Float32Array);
68
+ assert.strictEqual(output.length, 5);
69
+ });
70
+
71
+ test("should work with windowDuration parameter", async () => {
72
+ const pipeline = createDspPipeline();
73
+ // Using windowDuration instead of windowSize
74
+ pipeline.MovingAverage({ mode: "moving", windowDuration: 5000 }); // 5 seconds
75
+
76
+ const samples = new Float32Array([1, 2, 3, 4, 5]);
77
+ const timestamps = new Float32Array([0, 1000, 2000, 3000, 4000]);
78
+
79
+ const output = await pipeline.process(samples, timestamps, {
80
+ channels: 1,
81
+ });
82
+
83
+ assert.ok(output instanceof Float32Array);
84
+ assert.strictEqual(output.length, 5);
85
+ });
86
+
87
+ test("should work with processCopy and timestamps", async () => {
88
+ const pipeline = createDspPipeline();
89
+ pipeline.MovingAverage({ mode: "moving", windowSize: 3 });
90
+
91
+ const samples = new Float32Array([1, 2, 3, 4, 5]);
92
+ const timestamps = new Float32Array([0, 100, 200, 300, 400]);
93
+
94
+ const output = await pipeline.processCopy(samples, timestamps, {
95
+ channels: 1,
96
+ });
97
+
98
+ // Original should be unchanged
99
+ assert.deepStrictEqual(
100
+ Array.from(samples),
101
+ [1, 2, 3, 4, 5],
102
+ "Original samples should be unchanged"
103
+ );
104
+
105
+ // Output should be different
106
+ assert.ok(output instanceof Float32Array);
107
+ assert.strictEqual(output.length, 5);
108
+ });
109
+
110
+ test("should support multi-channel with timestamps", async () => {
111
+ const pipeline = createDspPipeline();
112
+ pipeline.MovingAverage({ mode: "moving", windowSize: 3 });
113
+
114
+ // 2 channels, 3 samples per channel = 6 total samples (interleaved)
115
+ const samples = new Float32Array([1, 10, 2, 20, 3, 30]);
116
+ const timestamps = new Float32Array([0, 0, 100, 100, 200, 200]); // Timestamps per sample
117
+
118
+ const output = await pipeline.process(samples, timestamps, {
119
+ channels: 2,
120
+ });
121
+
122
+ assert.ok(output instanceof Float32Array);
123
+ assert.strictEqual(output.length, 6);
124
+ });
125
+ });
126
+
127
+ describe("Backwards Compatibility", () => {
128
+ test("should work without timestamps (legacy mode)", async () => {
129
+ const pipeline = createDspPipeline();
130
+ pipeline.MovingAverage({ mode: "moving", windowSize: 3 });
131
+
132
+ const samples = new Float32Array([1, 2, 3, 4, 5]);
133
+
134
+ // Legacy API: no timestamps
135
+ const output = await pipeline.process(samples, { channels: 1 });
136
+
137
+ assert.ok(output instanceof Float32Array);
138
+ assert.strictEqual(output.length, 5);
139
+ });
140
+
141
+ test("should maintain same results in legacy mode", async () => {
142
+ const pipeline1 = createDspPipeline();
143
+ pipeline1.MovingAverage({ mode: "moving", windowSize: 3 });
144
+
145
+ const pipeline2 = createDspPipeline();
146
+ pipeline2.MovingAverage({ mode: "moving", windowSize: 3 });
147
+
148
+ const samples1 = new Float32Array([1, 2, 3, 4, 5]);
149
+ const samples2 = new Float32Array([1, 2, 3, 4, 5]);
150
+
151
+ // Legacy mode
152
+ const output1 = await pipeline1.process(samples1, { channels: 1 });
153
+
154
+ // With explicit sequential timestamps (should behave the same)
155
+ const timestamps = new Float32Array([0, 1, 2, 3, 4]);
156
+ const output2 = await pipeline2.process(samples2, timestamps, {
157
+ channels: 1,
158
+ });
159
+
160
+ // Results should be identical
161
+ for (let i = 0; i < output1.length; i++) {
162
+ assert.ok(
163
+ Math.abs(output1[i] - output2[i]) < 0.0001,
164
+ `Expected outputs to match at index ${i}: ${output1[i]} vs ${output2[i]}`
165
+ );
166
+ }
167
+ });
168
+ });
169
+
170
+ describe("Filter Parameter Validation", () => {
171
+ test("should accept windowSize (legacy)", () => {
172
+ const pipeline = createDspPipeline();
173
+ assert.doesNotThrow(() => {
174
+ pipeline.MovingAverage({ mode: "moving", windowSize: 10 });
175
+ });
176
+ });
177
+
178
+ test("should accept windowDuration (new)", () => {
179
+ const pipeline = createDspPipeline();
180
+ assert.doesNotThrow(() => {
181
+ pipeline.MovingAverage({ mode: "moving", windowDuration: 5000 });
182
+ });
183
+ });
184
+
185
+ test("should accept both windowSize and windowDuration", () => {
186
+ const pipeline = createDspPipeline();
187
+ assert.doesNotThrow(() => {
188
+ pipeline.MovingAverage({
189
+ mode: "moving",
190
+ windowSize: 10,
191
+ windowDuration: 5000,
192
+ });
193
+ });
194
+ });
195
+
196
+ test("should reject neither windowSize nor windowDuration", () => {
197
+ const pipeline = createDspPipeline();
198
+ assert.throws(
199
+ () => {
200
+ pipeline.MovingAverage({ mode: "moving" } as any);
201
+ },
202
+ {
203
+ name: "TypeError",
204
+ message:
205
+ /either windowSize or windowDuration must be specified for "moving" mode/,
206
+ }
207
+ );
208
+ });
209
+
210
+ test("should reject invalid windowDuration", () => {
211
+ const pipeline = createDspPipeline();
212
+ assert.throws(
213
+ () => {
214
+ pipeline.MovingAverage({ mode: "moving", windowDuration: -100 });
215
+ },
216
+ {
217
+ name: "TypeError",
218
+ message: /windowDuration must be positive/,
219
+ }
220
+ );
221
+ });
222
+ });
223
+
224
+ describe("All Filters Support Time-Series", () => {
225
+ test("Rms should accept windowDuration", () => {
226
+ const pipeline = createDspPipeline();
227
+ assert.doesNotThrow(() => {
228
+ pipeline.Rms({ mode: "moving", windowDuration: 5000 });
229
+ });
230
+ });
231
+
232
+ test("Variance should accept windowDuration", () => {
233
+ const pipeline = createDspPipeline();
234
+ assert.doesNotThrow(() => {
235
+ pipeline.Variance({ mode: "moving", windowDuration: 5000 });
236
+ });
237
+ });
238
+
239
+ test("ZScoreNormalize should accept windowDuration", () => {
240
+ const pipeline = createDspPipeline();
241
+ assert.doesNotThrow(() => {
242
+ pipeline.ZScoreNormalize({ mode: "moving", windowDuration: 5000 });
243
+ });
244
+ });
245
+
246
+ test("MeanAbsoluteValue should accept windowDuration", () => {
247
+ const pipeline = createDspPipeline();
248
+ assert.doesNotThrow(() => {
249
+ pipeline.MeanAbsoluteValue({ mode: "moving", windowDuration: 5000 });
250
+ });
251
+ });
252
+ });
253
+ });
254
+