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,164 @@
1
+ /**
2
+ * Unit tests for .tap() method
3
+ */
4
+
5
+ import { describe, it } from "node:test";
6
+ import assert from "node:assert";
7
+ import { createDspPipeline } from "../bindings.js";
8
+
9
+ describe("Tap Method", () => {
10
+ it("should execute tap callback after processing", async () => {
11
+ let tapCalled = false;
12
+ let tappedSamples: Float32Array | null = null;
13
+ let tappedStage = "";
14
+
15
+ const pipeline = createDspPipeline()
16
+ .MovingAverage({ mode: "moving", windowSize: 3 })
17
+ .tap((samples, stage) => {
18
+ tapCalled = true;
19
+ tappedSamples = samples;
20
+ tappedStage = stage;
21
+ });
22
+
23
+ const input = new Float32Array([1, 2, 3, 4, 5]);
24
+ await pipeline.process(input, { sampleRate: 1000 });
25
+
26
+ assert.strictEqual(tapCalled, true);
27
+ assert.notStrictEqual(tappedSamples, null);
28
+ assert.strictEqual(tappedStage, "movingAverage:moving");
29
+ });
30
+
31
+ it("should support multiple tap calls in chain", async () => {
32
+ const tapLog: Array<{ stage: string; firstValue: number }> = [];
33
+
34
+ const pipeline = createDspPipeline()
35
+ .MovingAverage({ mode: "moving", windowSize: 2 })
36
+ .tap((samples, stage) => {
37
+ tapLog.push({ stage, firstValue: samples[0] });
38
+ })
39
+ .Rectify({ mode: "full" })
40
+ .tap((samples, stage) => {
41
+ tapLog.push({ stage, firstValue: samples[0] });
42
+ })
43
+ .Rms({ mode: "moving", windowSize: 2 })
44
+ .tap((samples, stage) => {
45
+ tapLog.push({ stage, firstValue: samples[0] });
46
+ });
47
+
48
+ const input = new Float32Array([1, -2, 3, -4, 5]);
49
+ await pipeline.process(input, { sampleRate: 1000 });
50
+
51
+ assert.strictEqual(tapLog.length, 3);
52
+ assert.strictEqual(tapLog[0].stage, "movingAverage:moving");
53
+ assert.strictEqual(tapLog[1].stage, "movingAverage:moving → rectify:full");
54
+ assert.strictEqual(
55
+ tapLog[2].stage,
56
+ "movingAverage:moving → rectify:full → rms:moving"
57
+ );
58
+ });
59
+
60
+ it("should not modify the data in tap callback", async () => {
61
+ const pipeline = createDspPipeline()
62
+ .MovingAverage({ mode: "moving", windowSize: 2 })
63
+ .tap((samples) => {
64
+ // Try to modify (should not affect final result since it's after processing)
65
+ samples[0] = 999;
66
+ });
67
+
68
+ const input = new Float32Array([1, 2, 3, 4, 5]);
69
+ const result = await pipeline.process(input, { sampleRate: 1000 });
70
+
71
+ // Since tap is called after native processing completes, the modification
72
+ // will actually persist (samples reference the result buffer)
73
+ assert.strictEqual(result[0], 999);
74
+ });
75
+
76
+ it("should handle errors in tap callback gracefully", async () => {
77
+ let processCompleted = false;
78
+
79
+ const pipeline = createDspPipeline()
80
+ .MovingAverage({ mode: "moving", windowSize: 2 })
81
+ .tap(() => {
82
+ throw new Error("Tap error!");
83
+ });
84
+
85
+ const input = new Float32Array([1, 2, 3, 4, 5]);
86
+
87
+ // Should not throw, error is caught and logged
88
+ const result = await pipeline.process(input, { sampleRate: 1000 });
89
+ processCompleted = true;
90
+
91
+ assert.strictEqual(processCompleted, true);
92
+ assert.notStrictEqual(result, null);
93
+ });
94
+
95
+ it("should work with empty pipeline (no stages)", async () => {
96
+ let tapCalled = false;
97
+
98
+ const pipeline = createDspPipeline().tap((samples, stage) => {
99
+ tapCalled = true;
100
+ assert.strictEqual(stage, "start");
101
+ });
102
+
103
+ const input = new Float32Array([1, 2, 3]);
104
+ await pipeline.process(input, { sampleRate: 1000 });
105
+
106
+ assert.strictEqual(tapCalled, true);
107
+ });
108
+
109
+ it("should receive a view of the actual result buffer", async () => {
110
+ let tappedBuffer: Float32Array | null = null;
111
+
112
+ const pipeline = createDspPipeline()
113
+ .MovingAverage({ mode: "moving", windowSize: 2 })
114
+ .tap((samples) => {
115
+ tappedBuffer = samples;
116
+ });
117
+
118
+ const input = new Float32Array([1, 2, 3, 4, 5]);
119
+ const result = await pipeline.process(input, { sampleRate: 1000 });
120
+
121
+ // Should be the same reference
122
+ assert.strictEqual(tappedBuffer, result);
123
+ });
124
+
125
+ it("should support inspection of sample slices", async () => {
126
+ const inspectedSlices: number[][] = [];
127
+
128
+ const pipeline = createDspPipeline()
129
+ .MovingAverage({ mode: "moving", windowSize: 3 })
130
+ .tap((samples) => {
131
+ // Common pattern: inspect first few samples
132
+ inspectedSlices.push(Array.from(samples.slice(0, 3)));
133
+ })
134
+ .Rectify();
135
+
136
+ const input = new Float32Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
137
+ await pipeline.process(input, { sampleRate: 1000 });
138
+
139
+ assert.strictEqual(inspectedSlices.length, 1);
140
+ assert.strictEqual(inspectedSlices[0].length, 3);
141
+ });
142
+
143
+ it("should work with pipeline callbacks simultaneously", async () => {
144
+ let tapCalled = false;
145
+ let onBatchCalled = false;
146
+
147
+ const pipeline = createDspPipeline()
148
+ .pipeline({
149
+ onBatch: () => {
150
+ onBatchCalled = true;
151
+ },
152
+ })
153
+ .MovingAverage({ mode: "moving", windowSize: 2 })
154
+ .tap(() => {
155
+ tapCalled = true;
156
+ });
157
+
158
+ const input = new Float32Array([1, 2, 3]);
159
+ await pipeline.process(input, { sampleRate: 1000 });
160
+
161
+ assert.strictEqual(tapCalled, true);
162
+ assert.strictEqual(onBatchCalled, true);
163
+ });
164
+ });
@@ -0,0 +1,124 @@
1
+ import { describe, test } from "node:test";
2
+ import assert from "node:assert";
3
+ import { createDspPipeline } from "../index.js";
4
+
5
+ describe("True Time-Based Expiration", () => {
6
+ test("should expire samples based on age, not count", async () => {
7
+ const pipeline = createDspPipeline();
8
+ // 1 second window with timestamps
9
+ pipeline.MovingAverage({ mode: "moving", windowDuration: 1000 });
10
+
11
+ // Add 3 samples within 500ms
12
+ const batch1 = new Float32Array([10, 20, 30]);
13
+ const timestamps1 = new Float32Array([0, 100, 200]);
14
+
15
+ const result1 = await pipeline.process(batch1, timestamps1, {
16
+ channels: 1,
17
+ });
18
+
19
+ // Average of [10, 20, 30] = 20
20
+ assert.ok(Math.abs(result1[2] - 20) < 0.01, "Expected average of ~20");
21
+
22
+ // Add a sample 2 seconds later - should expire all previous samples
23
+ const batch2 = new Float32Array([100]);
24
+ const timestamps2 = new Float32Array([2200]); // 2.2 seconds from start
25
+
26
+ const result2 = await pipeline.process(batch2, timestamps2, {
27
+ channels: 1,
28
+ });
29
+
30
+ // Should only have the new sample (100), since all previous ones are > 1 second old
31
+ assert.ok(
32
+ Math.abs(result2[0] - 100) < 0.01,
33
+ `Expected only new sample (100), got ${result2[0]}`
34
+ );
35
+ });
36
+
37
+ test("should work with irregular sampling", async () => {
38
+ const pipeline = createDspPipeline();
39
+ // 500ms window
40
+ pipeline.MovingAverage({ mode: "moving", windowDuration: 500 });
41
+
42
+ // Irregular timestamps: 0ms, 50ms, 600ms, 650ms
43
+ const samples = new Float32Array([10, 20, 30, 40]);
44
+ const timestamps = new Float32Array([0, 50, 600, 650]);
45
+
46
+ const result = await pipeline.process(samples, timestamps, {
47
+ channels: 1,
48
+ });
49
+
50
+ // At timestamp 0: avg([10]) = 10
51
+ assert.ok(Math.abs(result[0] - 10) < 0.01, "First sample should be 10");
52
+
53
+ // At timestamp 50: avg([10, 20]) = 15
54
+ assert.ok(
55
+ Math.abs(result[1] - 15) < 0.01,
56
+ "Second should be avg(10,20)=15"
57
+ );
58
+
59
+ // At timestamp 600: samples at 0 and 50 are expired (>500ms old)
60
+ // Only sample at 600ms, so avg([30]) = 30
61
+ assert.ok(
62
+ Math.abs(result[2] - 30) < 0.01,
63
+ `Third should be 30 (old samples expired), got ${result[2]}`
64
+ );
65
+
66
+ // At timestamp 650: samples at 600 and 650 are within 500ms
67
+ // avg([30, 40]) = 35
68
+ assert.ok(
69
+ Math.abs(result[3] - 35) < 0.01,
70
+ "Fourth should be avg(30,40)=35"
71
+ );
72
+ });
73
+
74
+ test("should handle streaming with time-based windows", async () => {
75
+ const pipeline = createDspPipeline();
76
+ // 300ms window
77
+ pipeline.MovingAverage({ mode: "moving", windowDuration: 300 });
78
+
79
+ // First chunk: samples at 0, 100, 200ms
80
+ const chunk1 = new Float32Array([10, 20, 30]);
81
+ const ts1 = new Float32Array([0, 100, 200]);
82
+ const result1 = await pipeline.process(chunk1, ts1, { channels: 1 });
83
+
84
+ // At 200ms: avg([10, 20, 30]) = 20
85
+ assert.ok(Math.abs(result1[2] - 20) < 0.01, "First chunk avg should be 20");
86
+
87
+ // Second chunk: samples at 250, 600ms
88
+ // At 250ms: all previous samples still valid (within 300ms)
89
+ // At 600ms: samples at 0, 100, 200, 250 all expired (>300ms old)
90
+ const chunk2 = new Float32Array([40, 100]);
91
+ const ts2 = new Float32Array([250, 600]);
92
+ const result2 = await pipeline.process(chunk2, ts2, { channels: 1 });
93
+
94
+ // At 250ms: avg([10, 20, 30, 40]) = 25
95
+ assert.ok(
96
+ Math.abs(result2[0] - 25) < 0.01,
97
+ `At 250ms expected avg(10,20,30,40)=25, got ${result2[0]}`
98
+ );
99
+
100
+ // At 600ms: only sample at 600ms remains, avg([100]) = 100
101
+ assert.ok(
102
+ Math.abs(result2[1] - 100) < 0.01,
103
+ `At 600ms expected 100 (all previous expired), got ${result2[1]}`
104
+ );
105
+ });
106
+
107
+ test("should only use time-based expiration when timestamps provided", async () => {
108
+ const pipeline = createDspPipeline();
109
+ // Both windowSize and windowDuration specified
110
+ pipeline.MovingAverage({ mode: "moving", windowDuration: 1000 });
111
+
112
+ // Process without timestamps - should use sample-count mode
113
+ const samples1 = new Float32Array([1, 2, 3, 4, 5]);
114
+
115
+ // This should work without timestamps (falls back to sample-count mode)
116
+ const result1 = await pipeline.process(samples1, {
117
+ channels: 1,
118
+ sampleRate: 1000, // Required when no timestamps
119
+ });
120
+
121
+ assert.ok(result1 instanceof Float32Array);
122
+ assert.strictEqual(result1.length, 5);
123
+ });
124
+ });
@@ -0,0 +1,231 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert";
3
+ import { createDspPipeline } from "../index.js";
4
+
5
+ test("Time-Based RMS", async (t) => {
6
+ await t.test("should expire samples based on age for RMS", async () => {
7
+ const pipeline = createDspPipeline();
8
+ // 1 second window
9
+ pipeline.Rms({ 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: RMS([1, 2, 3]) = sqrt((1 + 4 + 9) / 3) = sqrt(14/3) ≈ 2.16
17
+ assert.ok(
18
+ Math.abs(result1[2] - Math.sqrt(14 / 3)) < 0.01,
19
+ `Expected RMS ≈ 2.16, got ${result1[2]}`
20
+ );
21
+
22
+ // Sample at 2200ms - 2.2 seconds later (>1s window)
23
+ // Samples at 0, 100, 200ms should be EXPIRED
24
+ const chunk2 = new Float32Array([4]);
25
+ const ts2 = new Float32Array([2200]);
26
+ const result2 = await pipeline.process(chunk2, ts2, { channels: 1 });
27
+
28
+ // At 2200ms: only sample at 2200ms remains, RMS([4]) = 4
29
+ assert.ok(
30
+ Math.abs(result2[0] - 4.0) < 0.01,
31
+ `Expected RMS = 4.0, got ${result2[0]}`
32
+ );
33
+ });
34
+
35
+ await t.test("should work with irregular sampling for RMS", async () => {
36
+ const pipeline = createDspPipeline();
37
+ // 500ms window
38
+ pipeline.Rms({ mode: "moving", windowDuration: 500 });
39
+
40
+ const samples = new Float32Array([2, 4, 6, 8]);
41
+ const timestamps = new Float32Array([
42
+ 0, // Sample at 0ms
43
+ 50, // Sample at 50ms
44
+ 600, // Sample at 600ms (550ms gap!)
45
+ 650, // Sample at 650ms
46
+ ]);
47
+
48
+ const result = await pipeline.process(samples, timestamps, { channels: 1 });
49
+
50
+ // At 650ms with 500ms window:
51
+ // - Sample at 0ms EXPIRED (650 - 0 = 650ms > 500ms)
52
+ // - Sample at 50ms EXPIRED (650 - 50 = 600ms > 500ms)
53
+ // - Sample at 600ms KEPT (650 - 600 = 50ms < 500ms)
54
+ // - Sample at 650ms KEPT (current)
55
+ // RMS([6, 8]) = sqrt((36 + 64) / 2) = sqrt(50) ≈ 7.07
56
+ assert.ok(
57
+ Math.abs(result[3] - Math.sqrt(50)) < 0.01,
58
+ `Expected RMS ≈ 7.07, got ${result[3]}`
59
+ );
60
+ });
61
+
62
+ await t.test(
63
+ "should handle streaming with time-based windows for RMS",
64
+ async () => {
65
+ const pipeline = createDspPipeline();
66
+ // 300ms window
67
+ pipeline.Rms({ mode: "moving", windowDuration: 300 });
68
+
69
+ // First chunk: samples at 0, 100, 200ms
70
+ const chunk1 = new Float32Array([1, 2, 3]);
71
+ const ts1 = new Float32Array([0, 100, 200]);
72
+ const result1 = await pipeline.process(chunk1, ts1, { channels: 1 });
73
+
74
+ // At 200ms: RMS([1, 2, 3]) = sqrt(14/3) ≈ 2.16
75
+ assert.ok(
76
+ Math.abs(result1[2] - Math.sqrt(14 / 3)) < 0.01,
77
+ "First chunk RMS should be sqrt(14/3)"
78
+ );
79
+
80
+ // Second chunk: samples at 250, 600ms
81
+ const chunk2 = new Float32Array([4, 10]);
82
+ const ts2 = new Float32Array([250, 600]);
83
+ const result2 = await pipeline.process(chunk2, ts2, { channels: 1 });
84
+
85
+ // At 250ms: all previous samples still valid (within 300ms)
86
+ // RMS([1, 2, 3, 4]) = sqrt((1 + 4 + 9 + 16) / 4) = sqrt(7.5) ≈ 2.74
87
+ assert.ok(
88
+ Math.abs(result2[0] - Math.sqrt(7.5)) < 0.01,
89
+ `At 250ms expected RMS ≈ 2.74, got ${result2[0]}`
90
+ );
91
+
92
+ // At 600ms: samples at 0, 100, 200, 250 all expired (>300ms old)
93
+ // RMS([10]) = 10
94
+ assert.ok(
95
+ Math.abs(result2[1] - 10.0) < 0.01,
96
+ `At 600ms expected RMS = 10, got ${result2[1]}`
97
+ );
98
+ }
99
+ );
100
+ });
101
+
102
+ test("Time-Based Mean Absolute Value", async (t) => {
103
+ await t.test("should expire samples based on age for MAV", async () => {
104
+ const pipeline = createDspPipeline();
105
+ // 1 second window
106
+ pipeline.MeanAbsoluteValue({ mode: "moving", windowDuration: 1000 });
107
+
108
+ // First 3 samples at 0, 100, 200ms - values -1, 2, -3
109
+ const chunk1 = new Float32Array([-1, 2, -3]);
110
+ const ts1 = new Float32Array([0, 100, 200]);
111
+ const result1 = await pipeline.process(chunk1, ts1, { channels: 1 });
112
+
113
+ // At 200ms: MAV([-1, 2, -3]) = (1 + 2 + 3) / 3 = 2
114
+ assert.ok(
115
+ Math.abs(result1[2] - 2.0) < 0.01,
116
+ `Expected MAV = 2.0, got ${result1[2]}`
117
+ );
118
+
119
+ // Sample at 2200ms - 2.2 seconds later (>1s window)
120
+ // Samples at 0, 100, 200ms should be EXPIRED
121
+ const chunk2 = new Float32Array([-5]);
122
+ const ts2 = new Float32Array([2200]);
123
+ const result2 = await pipeline.process(chunk2, ts2, { channels: 1 });
124
+
125
+ // At 2200ms: only sample at 2200ms remains, MAV([-5]) = 5
126
+ assert.ok(
127
+ Math.abs(result2[0] - 5.0) < 0.01,
128
+ `Expected MAV = 5.0, got ${result2[0]}`
129
+ );
130
+ });
131
+
132
+ await t.test("should work with irregular sampling for MAV", async () => {
133
+ const pipeline = createDspPipeline();
134
+ // 500ms window
135
+ pipeline.MeanAbsoluteValue({ mode: "moving", windowDuration: 500 });
136
+
137
+ const samples = new Float32Array([-2, 4, -6, 8]);
138
+ const timestamps = new Float32Array([
139
+ 0, // Sample at 0ms
140
+ 50, // Sample at 50ms
141
+ 600, // Sample at 600ms (550ms gap!)
142
+ 650, // Sample at 650ms
143
+ ]);
144
+
145
+ const result = await pipeline.process(samples, timestamps, { channels: 1 });
146
+
147
+ // At 650ms with 500ms window:
148
+ // - Sample at 0ms EXPIRED (650 - 0 = 650ms > 500ms)
149
+ // - Sample at 50ms EXPIRED (650 - 50 = 600ms > 500ms)
150
+ // - Sample at 600ms KEPT (650 - 600 = 50ms < 500ms)
151
+ // - Sample at 650ms KEPT (current)
152
+ // MAV([-6, 8]) = (6 + 8) / 2 = 7
153
+ assert.ok(
154
+ Math.abs(result[3] - 7.0) < 0.01,
155
+ `Expected MAV = 7.0, got ${result[3]}`
156
+ );
157
+ });
158
+
159
+ await t.test(
160
+ "should handle streaming with time-based windows for MAV",
161
+ async () => {
162
+ const pipeline = createDspPipeline();
163
+ // 300ms window
164
+ pipeline.MeanAbsoluteValue({ mode: "moving", windowDuration: 300 });
165
+
166
+ // First chunk: samples at 0, 100, 200ms
167
+ const chunk1 = new Float32Array([-1, -2, 3]);
168
+ const ts1 = new Float32Array([0, 100, 200]);
169
+ const result1 = await pipeline.process(chunk1, ts1, { channels: 1 });
170
+
171
+ // At 200ms: MAV([-1, -2, 3]) = (1 + 2 + 3) / 3 = 2
172
+ assert.ok(
173
+ Math.abs(result1[2] - 2.0) < 0.01,
174
+ "First chunk MAV should be 2.0"
175
+ );
176
+
177
+ // Second chunk: samples at 250, 600ms
178
+ const chunk2 = new Float32Array([-4, 10]);
179
+ const ts2 = new Float32Array([250, 600]);
180
+ const result2 = await pipeline.process(chunk2, ts2, { channels: 1 });
181
+
182
+ // At 250ms: all previous samples still valid (within 300ms)
183
+ // MAV([-1, -2, 3, -4]) = (1 + 2 + 3 + 4) / 4 = 2.5
184
+ assert.ok(
185
+ Math.abs(result2[0] - 2.5) < 0.01,
186
+ `At 250ms expected MAV = 2.5, got ${result2[0]}`
187
+ );
188
+
189
+ // At 600ms: samples at 0, 100, 200, 250 all expired (>300ms old)
190
+ // MAV([10]) = 10
191
+ assert.ok(
192
+ Math.abs(result2[1] - 10.0) < 0.01,
193
+ `At 600ms expected MAV = 10, got ${result2[1]}`
194
+ );
195
+ }
196
+ );
197
+ });
198
+
199
+ test("Backward Compatibility", async (t) => {
200
+ await t.test("RMS should work without timestamps", async () => {
201
+ const pipeline = createDspPipeline();
202
+ pipeline.Rms({ mode: "moving", windowSize: 3 });
203
+
204
+ const samples = new Float32Array([1, 2, 3, 4]);
205
+ const result = await pipeline.process(samples, { channels: 1 });
206
+
207
+ // Should use sample-count mode (last 3 samples)
208
+ // At sample 4: RMS([2, 3, 4]) = sqrt((4 + 9 + 16) / 3) = sqrt(29/3) ≈ 3.11
209
+ assert.ok(
210
+ Math.abs(result[3] - Math.sqrt(29 / 3)) < 0.01,
211
+ "RMS should work in sample-count mode"
212
+ );
213
+ });
214
+
215
+ await t.test("MAV should work without timestamps", async () => {
216
+ const pipeline = createDspPipeline();
217
+ pipeline.MeanAbsoluteValue({ mode: "moving", windowSize: 3 });
218
+
219
+ const samples = new Float32Array([-1, -2, 3, -4]);
220
+ const result = await pipeline.process(samples, { channels: 1 });
221
+
222
+ // Should use sample-count mode (last 3 samples)
223
+ // At sample 4: MAV([3, -4]) = (3 + 4) / 2 = 3.5
224
+ // Wait, with windowSize=3, at index 3 we have [2, 3, 4] in the window
225
+ // MAV([-2, 3, -4]) = (2 + 3 + 4) / 3 = 3
226
+ assert.ok(
227
+ Math.abs(result[3] - 3.0) < 0.01,
228
+ "MAV should work in sample-count mode"
229
+ );
230
+ });
231
+ });