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,456 @@
1
+ import {
2
+ describe,
3
+ test,
4
+ before,
5
+ after,
6
+ beforeEach,
7
+ afterEach,
8
+ } from "node:test";
9
+ import assert from "node:assert/strict";
10
+ import { createDspPipeline, DspProcessor } from "../bindings.js";
11
+ import { createClient } from "redis";
12
+ import type { RedisClientType } from "redis";
13
+
14
+ const DEFAULT_OPTIONS = { channels: 1, sampleRate: 44100 };
15
+
16
+ function assertCloseTo(actual: number, expected: number, precision = 5) {
17
+ const tolerance = Math.pow(10, -precision);
18
+ assert.ok(
19
+ Math.abs(actual - expected) < tolerance,
20
+ `Expected ${actual} to be close to ${expected} (tolerance: ${tolerance})`
21
+ );
22
+ }
23
+
24
+ // Helper to check if Redis is available
25
+ async function isRedisAvailable(): Promise<boolean> {
26
+ try {
27
+ const client = createClient({
28
+ url: "redis://localhost:6379",
29
+ socket: { connectTimeout: 1000 },
30
+ });
31
+ await client.connect();
32
+ await client.disconnect();
33
+ return true;
34
+ } catch {
35
+ return false;
36
+ }
37
+ }
38
+
39
+ describe("Redis State Persistence", () => {
40
+ let redis: RedisClientType;
41
+ let redisAvailable: boolean;
42
+
43
+ before(async () => {
44
+ redisAvailable = await isRedisAvailable();
45
+ if (!redisAvailable) {
46
+ console.log(
47
+ "\n⚠️ Redis not available - skipping Redis integration tests"
48
+ );
49
+ console.log(
50
+ " To run these tests, start Redis: docker run -p 6379:6379 redis\n"
51
+ );
52
+ }
53
+ });
54
+
55
+ beforeEach(async () => {
56
+ if (!redisAvailable) return;
57
+
58
+ redis = createClient({ url: "redis://localhost:6379" });
59
+ await redis.connect();
60
+
61
+ // Clean up test keys
62
+ const keys = await redis.keys("test:dsp:*");
63
+ if (keys.length > 0) {
64
+ await redis.del(keys);
65
+ }
66
+ });
67
+
68
+ afterEach(async () => {
69
+ if (redis && redis.isOpen) {
70
+ await redis.disconnect();
71
+ }
72
+ });
73
+
74
+ describe("Basic State Persistence", () => {
75
+ test("should save and restore state from Redis", async () => {
76
+ if (!redisAvailable) return;
77
+
78
+ const stateKey = "test:dsp:state:1";
79
+ const processor = createDspPipeline();
80
+ processor.MovingAverage({ mode: "moving", windowSize: 3 });
81
+
82
+ // Build state
83
+ await processor.process(
84
+ new Float32Array([1, 2, 3, 4, 5]),
85
+ DEFAULT_OPTIONS
86
+ );
87
+
88
+ // Save to Redis
89
+ const stateJson = await processor.saveState();
90
+ await redis.set(stateKey, stateJson);
91
+
92
+ // Verify state was saved
93
+ const savedState = await redis.get(stateKey);
94
+ assert.ok(savedState);
95
+ assert.equal(savedState, stateJson);
96
+
97
+ // Create new processor and restore
98
+ const processor2 = createDspPipeline();
99
+ processor2.MovingAverage({ mode: "moving", windowSize: 3 });
100
+
101
+ const restoredState = await redis.get(stateKey);
102
+ assert.ok(restoredState);
103
+ await processor2.loadState(restoredState);
104
+
105
+ // Verify continuity
106
+ const output1 = await processor.process(
107
+ new Float32Array([6]),
108
+ DEFAULT_OPTIONS
109
+ );
110
+ const output2 = await processor2.process(
111
+ new Float32Array([6]),
112
+ DEFAULT_OPTIONS
113
+ );
114
+
115
+ assertCloseTo(output1[0], output2[0]);
116
+ });
117
+
118
+ test("should handle missing state key gracefully", async () => {
119
+ if (!redisAvailable) return;
120
+
121
+ const stateKey = "test:dsp:state:nonexistent";
122
+ const state = await redis.get(stateKey);
123
+
124
+ assert.equal(state, null);
125
+ });
126
+
127
+ test("should update state in Redis after processing", async () => {
128
+ if (!redisAvailable) return;
129
+
130
+ const stateKey = "test:dsp:state:2";
131
+ const processor = createDspPipeline();
132
+ processor.MovingAverage({ mode: "moving", windowSize: 2 });
133
+
134
+ // Initial state
135
+ await processor.process(new Float32Array([1, 2]), DEFAULT_OPTIONS);
136
+ const state1 = await processor.saveState();
137
+ await redis.set(stateKey, state1);
138
+
139
+ // Process more data
140
+ await processor.process(new Float32Array([3, 4]), DEFAULT_OPTIONS);
141
+ const state2 = await processor.saveState();
142
+ await redis.set(stateKey, state2);
143
+
144
+ // States should be different
145
+ assert.notEqual(state1, state2);
146
+
147
+ const savedState = await redis.get(stateKey);
148
+ assert.equal(savedState, state2);
149
+ });
150
+ });
151
+
152
+ describe("Multi-Stage Pipeline Persistence", () => {
153
+ test("should persist and restore complex pipeline", async () => {
154
+ if (!redisAvailable) return;
155
+
156
+ const stateKey = "test:dsp:state:complex";
157
+ const processor = createDspPipeline();
158
+ processor
159
+ .MovingAverage({ mode: "moving", windowSize: 3 })
160
+ .Rms({ mode: "moving", windowSize: 2 })
161
+ .Rectify({ mode: "full" });
162
+
163
+ // Build state
164
+ await processor.process(
165
+ new Float32Array([1, -2, 3, -4, 5]),
166
+ DEFAULT_OPTIONS
167
+ );
168
+
169
+ // Save to Redis
170
+ const stateJson = await processor.saveState();
171
+ await redis.set(stateKey, stateJson);
172
+
173
+ const state = JSON.parse(stateJson);
174
+ assert.equal(state.stages.length, 3);
175
+
176
+ // Restore in new processor
177
+ const processor2 = createDspPipeline();
178
+ processor2
179
+ .MovingAverage({ mode: "moving", windowSize: 3 })
180
+ .Rms({ mode: "moving", windowSize: 2 })
181
+ .Rectify({ mode: "full" });
182
+
183
+ const restoredState = await redis.get(stateKey);
184
+ assert.ok(restoredState);
185
+ await processor2.loadState(restoredState);
186
+
187
+ // Verify continuity
188
+ const output1 = await processor.process(
189
+ new Float32Array([6]),
190
+ DEFAULT_OPTIONS
191
+ );
192
+ const output2 = await processor2.process(
193
+ new Float32Array([6]),
194
+ DEFAULT_OPTIONS
195
+ );
196
+
197
+ assertCloseTo(output1[0], output2[0]);
198
+ });
199
+ });
200
+
201
+ describe("Streaming Scenario", () => {
202
+ test("should maintain state across simulated stream chunks", async () => {
203
+ if (!redisAvailable) return;
204
+
205
+ const stateKey = "test:dsp:state:stream";
206
+ const processor = createDspPipeline();
207
+ processor.MovingAverage({ mode: "moving", windowSize: 5 });
208
+
209
+ const chunks = [
210
+ new Float32Array([1, 2, 3]),
211
+ new Float32Array([4, 5, 6]),
212
+ new Float32Array([7, 8, 9]),
213
+ ];
214
+
215
+ const outputs: Float32Array[] = [];
216
+
217
+ for (const chunk of chunks) {
218
+ const output = await processor.process(
219
+ new Float32Array(chunk),
220
+ DEFAULT_OPTIONS
221
+ );
222
+ outputs.push(output);
223
+
224
+ // Save state after each chunk
225
+ const state = await processor.saveState();
226
+ await redis.set(stateKey, state);
227
+ }
228
+
229
+ // Verify all chunks processed
230
+ assert.equal(outputs.length, 3);
231
+
232
+ // Simulate restart and continue processing
233
+ const processor2 = createDspPipeline();
234
+ processor2.MovingAverage({ mode: "moving", windowSize: 5 });
235
+
236
+ const savedState = await redis.get(stateKey);
237
+ assert.ok(savedState);
238
+ await processor2.loadState(savedState);
239
+
240
+ // Continue with new chunk
241
+ const output1 = await processor.process(
242
+ new Float32Array([10]),
243
+ DEFAULT_OPTIONS
244
+ );
245
+ const output2 = await processor2.process(
246
+ new Float32Array([10]),
247
+ DEFAULT_OPTIONS
248
+ );
249
+
250
+ assertCloseTo(output1[0], output2[0]);
251
+ });
252
+
253
+ test("should handle rapid save/restore cycles", async () => {
254
+ if (!redisAvailable) return;
255
+
256
+ const stateKey = "test:dsp:state:rapid";
257
+ const processor = createDspPipeline();
258
+ processor.Rms({ mode: "moving", windowSize: 3 });
259
+
260
+ // Rapidly process and save
261
+ for (let i = 0; i < 10; i++) {
262
+ await processor.process(new Float32Array([i]), DEFAULT_OPTIONS);
263
+ const state = await processor.saveState();
264
+ await redis.set(stateKey, state);
265
+ }
266
+
267
+ // Verify final state
268
+ const finalState = await redis.get(stateKey);
269
+ assert.ok(finalState);
270
+
271
+ const processor2 = createDspPipeline();
272
+ processor2.Rms({ mode: "moving", windowSize: 3 });
273
+ await processor2.loadState(finalState);
274
+
275
+ // Both should be in sync
276
+ const output1 = await processor.process(
277
+ new Float32Array([100]),
278
+ DEFAULT_OPTIONS
279
+ );
280
+ const output2 = await processor2.process(
281
+ new Float32Array([100]),
282
+ DEFAULT_OPTIONS
283
+ );
284
+
285
+ assertCloseTo(output1[0], output2[0]);
286
+ });
287
+ });
288
+
289
+ describe("Multi-Channel Scenarios", () => {
290
+ test("should persist state for multiple channels", async () => {
291
+ if (!redisAvailable) return;
292
+
293
+ const channel1Key = "test:dsp:state:ch1";
294
+ const channel2Key = "test:dsp:state:ch2";
295
+
296
+ // Channel 1
297
+ const processor1 = createDspPipeline();
298
+ processor1.MovingAverage({ mode: "moving", windowSize: 2 });
299
+ await processor1.process(new Float32Array([1, 2]), DEFAULT_OPTIONS);
300
+ await redis.set(channel1Key, await processor1.saveState());
301
+
302
+ // Channel 2
303
+ const processor2 = createDspPipeline();
304
+ processor2.MovingAverage({ mode: "moving", windowSize: 3 });
305
+ await processor2.process(new Float32Array([3, 4, 5]), DEFAULT_OPTIONS);
306
+ await redis.set(channel2Key, await processor2.saveState());
307
+
308
+ // Verify both states exist and are different
309
+ const state1 = await redis.get(channel1Key);
310
+ const state2 = await redis.get(channel2Key);
311
+
312
+ assert.ok(state1);
313
+ assert.ok(state2);
314
+ assert.notEqual(state1, state2);
315
+
316
+ // Verify different window sizes
317
+ const parsed1 = JSON.parse(state1);
318
+ const parsed2 = JSON.parse(state2);
319
+
320
+ assert.equal(parsed1.stages[0].state.windowSize, 2);
321
+ assert.equal(parsed2.stages[0].state.windowSize, 3);
322
+ });
323
+ });
324
+
325
+ describe("State Expiration", () => {
326
+ test("should set TTL on state keys for automatic cleanup", async () => {
327
+ if (!redisAvailable) return;
328
+
329
+ const stateKey = "test:dsp:state:ttl";
330
+ const processor = createDspPipeline();
331
+ processor.MovingAverage({ mode: "moving", windowSize: 2 });
332
+
333
+ await processor.process(new Float32Array([1, 2]), DEFAULT_OPTIONS);
334
+ const state = await processor.saveState();
335
+
336
+ // Save with 60 second TTL
337
+ await redis.setEx(stateKey, 60, state);
338
+
339
+ // Verify TTL is set
340
+ const ttl = await redis.ttl(stateKey);
341
+ assert.ok(ttl > 0 && ttl <= 60);
342
+
343
+ // Verify state can be retrieved
344
+ const retrieved = await redis.get(stateKey);
345
+ assert.equal(retrieved, state);
346
+ });
347
+ });
348
+
349
+ describe("Error Handling", () => {
350
+ test("should handle corrupted state in Redis", async () => {
351
+ if (!redisAvailable) return;
352
+
353
+ const stateKey = "test:dsp:state:corrupted";
354
+ const processor = createDspPipeline();
355
+ processor.MovingAverage({ mode: "moving", windowSize: 3 });
356
+
357
+ // Save corrupted JSON
358
+ await redis.set(stateKey, "{ invalid json }");
359
+
360
+ // Should throw when trying to load
361
+ await assert.rejects(async () => {
362
+ const state = await redis.get(stateKey);
363
+ if (state) {
364
+ JSON.parse(state); // This should throw
365
+ }
366
+ });
367
+ });
368
+
369
+ test("should handle state validation failure", async () => {
370
+ if (!redisAvailable) return;
371
+
372
+ const stateKey = "test:dsp:state:invalid";
373
+ const processor = createDspPipeline();
374
+ processor.MovingAverage({ mode: "moving", windowSize: 3 });
375
+
376
+ await processor.process(new Float32Array([1, 2, 3]), DEFAULT_OPTIONS);
377
+ const stateJson = await processor.saveState();
378
+ const state = JSON.parse(stateJson);
379
+
380
+ // Corrupt the state
381
+ if (state.stages[0].state.channels && state.stages[0].state.channels[0]) {
382
+ state.stages[0].state.channels[0].runningSum = 9999;
383
+ }
384
+
385
+ await redis.set(stateKey, JSON.stringify(state));
386
+
387
+ // Should throw validation error
388
+ const processor2 = createDspPipeline();
389
+ processor2.MovingAverage({ mode: "moving", windowSize: 3 });
390
+
391
+ const corruptedState = await redis.get(stateKey);
392
+ assert.ok(corruptedState);
393
+
394
+ await assert.rejects(
395
+ async () => await processor2.loadState(corruptedState),
396
+ /Running sum validation failed/
397
+ );
398
+ });
399
+ });
400
+
401
+ describe("State Metadata", () => {
402
+ test("should include timestamp in saved state", async () => {
403
+ if (!redisAvailable) return;
404
+
405
+ const stateKey = "test:dsp:state:timestamp";
406
+ const processor = createDspPipeline();
407
+ processor.MovingAverage({ mode: "moving", windowSize: 2 });
408
+
409
+ await processor.process(new Float32Array([1, 2]), DEFAULT_OPTIONS);
410
+ const stateJson = await processor.saveState();
411
+ await redis.set(stateKey, stateJson);
412
+
413
+ const state = JSON.parse(stateJson);
414
+ assert.ok(state.timestamp);
415
+ assert.ok(new Date(state.timestamp).getTime() > 0);
416
+ });
417
+
418
+ test("should track state version across updates", async () => {
419
+ if (!redisAvailable) return;
420
+
421
+ const stateKey = "test:dsp:state:version";
422
+ const metadataKey = "test:dsp:state:version:meta";
423
+ const processor = createDspPipeline();
424
+ processor.MovingAverage({ mode: "moving", windowSize: 2 });
425
+
426
+ // Save multiple versions
427
+ const versions: string[] = [];
428
+ const states: string[] = [];
429
+
430
+ for (let i = 0; i < 3; i++) {
431
+ await processor.process(new Float32Array([i]), DEFAULT_OPTIONS);
432
+ const state = await processor.saveState();
433
+ await redis.set(stateKey, state);
434
+ versions.push(JSON.parse(state).timestamp);
435
+ states.push(state);
436
+
437
+ // Track version count
438
+ await redis.incr(metadataKey);
439
+
440
+ // Small delay to ensure different timestamps
441
+ await new Promise((resolve) => setTimeout(resolve, 10));
442
+ }
443
+
444
+ const versionCount = await redis.get(metadataKey);
445
+ assert.equal(versionCount, "3");
446
+
447
+ // States should be different (different buffer contents)
448
+ assert.notEqual(states[0], states[1]);
449
+ assert.notEqual(states[1], states[2]);
450
+
451
+ // At least some timestamps should be different
452
+ const uniqueTimestamps = new Set(versions);
453
+ assert.ok(uniqueTimestamps.size >= 1, "Should have valid timestamps");
454
+ });
455
+ });
456
+ });
@@ -0,0 +1,166 @@
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("Slope Sign Change (SSC)", () => {
8
+ let pipeline: DspProcessor;
9
+
10
+ beforeEach(() => {
11
+ pipeline = createDspPipeline();
12
+ });
13
+
14
+ test("should count slope sign changes with zero threshold", async () => {
15
+ pipeline.SlopeSignChange({ windowSize: 5, threshold: 0 });
16
+
17
+ // Signal: [1, 3, 2, 4, 3, 5]
18
+ // Slopes: +, -, +, -, +
19
+ // Sign changes: at indices 2, 3, 4, 5
20
+ // Windowed counting: returns count WITHIN last 5 samples
21
+ const buffer = new Float32Array([1, 3, 2, 4, 3, 5]);
22
+ await pipeline.process(buffer, DEFAULT_OPTIONS);
23
+
24
+ assert.strictEqual(buffer[0], 0); // Need 2 previous samples
25
+ assert.strictEqual(buffer[1], 0); // Need 1 more previous sample
26
+ assert.strictEqual(buffer[2], 0); // First sign change, but window filling
27
+ assert.strictEqual(buffer[3], 1); // Sign change detected in window
28
+ assert.strictEqual(buffer[4], 1); // Count within sliding window (window size 5)
29
+ assert.strictEqual(buffer[5], 1); // Count within sliding window
30
+ });
31
+
32
+ test("should apply threshold correctly", async () => {
33
+ pipeline.SlopeSignChange({ windowSize: 4, threshold: 1.0 });
34
+
35
+ // Signal: [0, 0.5, 1.0, 0.5, 1.5, 0.5]
36
+ // Differences: 0.5, 0.5, -0.5, 1.0, -1.0
37
+ // With threshold 1.0, the PRODUCT of consecutive differences must exceed threshold
38
+ // SSC checks: (diff1 * diff2) > threshold
39
+ // Windowed counting: returns count WITHIN last 4 samples
40
+ const buffer = new Float32Array([0, 0.5, 1.0, 0.5, 1.5, 0.5]);
41
+ await pipeline.process(buffer, DEFAULT_OPTIONS);
42
+
43
+ assert.strictEqual(buffer[0], 0);
44
+ assert.strictEqual(buffer[1], 0);
45
+ assert.strictEqual(buffer[2], 0); // Product: 0.5*0.5 = 0.25 < 1.0
46
+ assert.strictEqual(buffer[3], 0); // Product: 0.5*-0.5 = -0.25 < 1.0
47
+ assert.strictEqual(buffer[4], 0); // Product: -0.5*1.0 = -0.5 < 1.0
48
+ assert.strictEqual(buffer[5], 0); // Product: 1.0*-1.0 = -1.0 < 1.0
49
+ });
50
+
51
+ test("should handle multi-channel SSC", async () => {
52
+ pipeline.SlopeSignChange({ windowSize: 3, threshold: 0 });
53
+
54
+ // 2 channels
55
+ // Ch0: [1, 2, 1, 2] - slopes: +, -, +
56
+ // Ch1: [2, 1, 2, 1] - slopes: -, +, -
57
+ // Windowed counting: returns count WITHIN last 3 samples
58
+ const buffer = new Float32Array([
59
+ 1,
60
+ 2, // Sample 0
61
+ 2,
62
+ 1, // Sample 1
63
+ 1,
64
+ 2, // Sample 2
65
+ 2,
66
+ 1, // Sample 3
67
+ ]);
68
+
69
+ await pipeline.process(buffer, { channels: 2, sampleRate: 44100 });
70
+
71
+ // Channel 0
72
+ assert.strictEqual(buffer[0], 0);
73
+ assert.strictEqual(buffer[2], 0);
74
+ assert.strictEqual(buffer[4], 0); // First sign change, but window filling
75
+ assert.strictEqual(buffer[6], 1); // Count within sliding window (window size 3)
76
+
77
+ // Channel 1
78
+ assert.strictEqual(buffer[1], 0);
79
+ assert.strictEqual(buffer[3], 0);
80
+ assert.strictEqual(buffer[5], 0); // First sign change, but window filling
81
+ assert.strictEqual(buffer[7], 1); // Count within sliding window
82
+ });
83
+
84
+ test("should handle monotonic signal (no sign changes)", async () => {
85
+ pipeline.SlopeSignChange({ windowSize: 5, threshold: 0 });
86
+
87
+ const buffer = new Float32Array([1, 2, 3, 4, 5]);
88
+ await pipeline.process(buffer, DEFAULT_OPTIONS);
89
+
90
+ // All slopes are positive, no sign changes
91
+ assert.strictEqual(buffer[0], 0);
92
+ assert.strictEqual(buffer[1], 0);
93
+ assert.strictEqual(buffer[2], 0);
94
+ assert.strictEqual(buffer[3], 0);
95
+ assert.strictEqual(buffer[4], 0);
96
+ });
97
+
98
+ test("should handle constant signal", async () => {
99
+ pipeline.SlopeSignChange({ windowSize: 4, threshold: 0 });
100
+
101
+ const buffer = new Float32Array([5, 5, 5, 5]);
102
+ await pipeline.process(buffer, DEFAULT_OPTIONS);
103
+
104
+ // No slopes, no sign changes
105
+ assert.strictEqual(buffer[0], 0);
106
+ assert.strictEqual(buffer[1], 0);
107
+ assert.strictEqual(buffer[2], 0);
108
+ assert.strictEqual(buffer[3], 0);
109
+ });
110
+
111
+ test("should reset state correctly", async () => {
112
+ pipeline.SlopeSignChange({ windowSize: 3, threshold: 0 });
113
+
114
+ const buffer1 = new Float32Array([1, 3, 2, 4]);
115
+ await pipeline.process(buffer1, DEFAULT_OPTIONS);
116
+
117
+ pipeline.clearState();
118
+
119
+ const buffer2 = new Float32Array([1, 3, 2, 4]);
120
+ await pipeline.process(buffer2, DEFAULT_OPTIONS);
121
+
122
+ // After reset, should get same results
123
+ for (let i = 0; i < buffer1.length; i++) {
124
+ assert.strictEqual(buffer1[i], buffer2[i]);
125
+ }
126
+ });
127
+
128
+ test("should serialize and deserialize state", async () => {
129
+ pipeline.SlopeSignChange({ windowSize: 4, threshold: 0 });
130
+
131
+ const buffer = new Float32Array([1, 3, 2, 4, 3]);
132
+ await pipeline.process(buffer, DEFAULT_OPTIONS);
133
+
134
+ const state = await pipeline.saveState();
135
+
136
+ const newPipeline = createDspPipeline();
137
+ newPipeline.SlopeSignChange({ windowSize: 4, threshold: 0 }); // Must match original pipeline
138
+ await newPipeline.loadState(state);
139
+
140
+ const buffer2 = new Float32Array([5, 4]);
141
+ await newPipeline.process(buffer2, DEFAULT_OPTIONS);
142
+
143
+ // Should continue counting from where we left off
144
+ assert.ok(buffer2[0] > 0);
145
+ assert.ok(buffer2[1] > 0);
146
+ });
147
+
148
+ test("should throw error for invalid window size", () => {
149
+ assert.throws(() => {
150
+ pipeline.SlopeSignChange({ windowSize: 0, threshold: 0 });
151
+ });
152
+ });
153
+
154
+ test("should throw error for missing window size", () => {
155
+ assert.throws(() => {
156
+ // @ts-expect-error - Testing missing windowSize
157
+ pipeline.SlopeSignChange({ threshold: 0 });
158
+ });
159
+ });
160
+
161
+ test("should default to zero threshold when not specified", async () => {
162
+ assert.doesNotThrow(() => {
163
+ pipeline.SlopeSignChange({ windowSize: 5 });
164
+ });
165
+ });
166
+ });