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.
- package/.github/workflows/ci.yml +185 -0
- package/.vscode/c_cpp_properties.json +17 -0
- package/.vscode/settings.json +68 -0
- package/.vscode/tasks.json +28 -0
- package/DISCLAIMER.md +32 -0
- package/LICENSE +21 -0
- package/README.md +1803 -0
- package/ROADMAP.md +192 -0
- package/TECHNICAL_DEBT.md +165 -0
- package/binding.gyp +65 -0
- package/docs/ADVANCED_LOGGER_FEATURES.md +598 -0
- package/docs/AUTHENTICATION_SECURITY.md +396 -0
- package/docs/BACKEND_IMPROVEMENTS.md +399 -0
- package/docs/CHEBYSHEV_BIQUAD_EQ_IMPLEMENTATION.md +405 -0
- package/docs/FFT_IMPLEMENTATION.md +490 -0
- package/docs/FFT_IMPROVEMENTS_SUMMARY.md +387 -0
- package/docs/FFT_USER_GUIDE.md +494 -0
- package/docs/FILTERS_IMPLEMENTATION.md +260 -0
- package/docs/FILTER_API_GUIDE.md +418 -0
- package/docs/FIR_SIMD_OPTIMIZATION.md +175 -0
- package/docs/LOGGER_API_REFERENCE.md +350 -0
- package/docs/NOTCH_FILTER_QUICK_REF.md +121 -0
- package/docs/PHASE2_TESTS_AND_NOTCH_FILTER.md +341 -0
- package/docs/PHASES_5_7_SUMMARY.md +403 -0
- package/docs/PIPELINE_FILTER_INTEGRATION.md +446 -0
- package/docs/SIMD_OPTIMIZATIONS.md +211 -0
- package/docs/TEST_MIGRATION_SUMMARY.md +173 -0
- package/docs/TIMESERIES_IMPLEMENTATION_SUMMARY.md +322 -0
- package/docs/TIMESERIES_QUICK_REF.md +85 -0
- package/docs/advanced.md +559 -0
- package/docs/time-series-guide.md +617 -0
- package/docs/time-series-migration.md +376 -0
- package/jest.config.js +37 -0
- package/package.json +42 -0
- package/prebuilds/linux-x64/dsp-ts-redis.node +0 -0
- package/prebuilds/win32-x64/dsp-ts-redis.node +0 -0
- package/scripts/test.js +24 -0
- package/src/build/dsp-ts-redis.node +0 -0
- package/src/native/DspPipeline.cc +675 -0
- package/src/native/DspPipeline.h +44 -0
- package/src/native/FftBindings.cc +817 -0
- package/src/native/FilterBindings.cc +1001 -0
- package/src/native/IDspStage.h +53 -0
- package/src/native/adapters/InterpolatorStage.h +201 -0
- package/src/native/adapters/MeanAbsoluteValueStage.h +289 -0
- package/src/native/adapters/MovingAverageStage.h +306 -0
- package/src/native/adapters/RectifyStage.h +88 -0
- package/src/native/adapters/ResamplerStage.h +238 -0
- package/src/native/adapters/RmsStage.h +299 -0
- package/src/native/adapters/SscStage.h +121 -0
- package/src/native/adapters/VarianceStage.h +307 -0
- package/src/native/adapters/WampStage.h +114 -0
- package/src/native/adapters/WaveformLengthStage.h +115 -0
- package/src/native/adapters/ZScoreNormalizeStage.h +326 -0
- package/src/native/core/FftEngine.cc +441 -0
- package/src/native/core/FftEngine.h +224 -0
- package/src/native/core/FirFilter.cc +324 -0
- package/src/native/core/FirFilter.h +149 -0
- package/src/native/core/IirFilter.cc +576 -0
- package/src/native/core/IirFilter.h +210 -0
- package/src/native/core/MovingAbsoluteValueFilter.cc +17 -0
- package/src/native/core/MovingAbsoluteValueFilter.h +135 -0
- package/src/native/core/MovingAverageFilter.cc +18 -0
- package/src/native/core/MovingAverageFilter.h +135 -0
- package/src/native/core/MovingFftFilter.cc +291 -0
- package/src/native/core/MovingFftFilter.h +203 -0
- package/src/native/core/MovingVarianceFilter.cc +194 -0
- package/src/native/core/MovingVarianceFilter.h +114 -0
- package/src/native/core/MovingZScoreFilter.cc +215 -0
- package/src/native/core/MovingZScoreFilter.h +113 -0
- package/src/native/core/Policies.h +352 -0
- package/src/native/core/RmsFilter.cc +18 -0
- package/src/native/core/RmsFilter.h +131 -0
- package/src/native/core/SscFilter.cc +16 -0
- package/src/native/core/SscFilter.h +137 -0
- package/src/native/core/WampFilter.cc +16 -0
- package/src/native/core/WampFilter.h +101 -0
- package/src/native/core/WaveformLengthFilter.cc +17 -0
- package/src/native/core/WaveformLengthFilter.h +98 -0
- package/src/native/utils/CircularBufferArray.cc +336 -0
- package/src/native/utils/CircularBufferArray.h +62 -0
- package/src/native/utils/CircularBufferVector.cc +145 -0
- package/src/native/utils/CircularBufferVector.h +45 -0
- package/src/native/utils/NapiUtils.cc +53 -0
- package/src/native/utils/NapiUtils.h +21 -0
- package/src/native/utils/SimdOps.h +870 -0
- package/src/native/utils/SlidingWindowFilter.cc +239 -0
- package/src/native/utils/SlidingWindowFilter.h +159 -0
- package/src/native/utils/TimeSeriesBuffer.cc +205 -0
- package/src/native/utils/TimeSeriesBuffer.h +140 -0
- package/src/ts/CircularLogBuffer.ts +87 -0
- package/src/ts/DriftDetector.ts +331 -0
- package/src/ts/TopicRouter.ts +428 -0
- package/src/ts/__tests__/AdvancedDsp.test.ts +585 -0
- package/src/ts/__tests__/AuthAndEdgeCases.test.ts +241 -0
- package/src/ts/__tests__/Chaining.test.ts +387 -0
- package/src/ts/__tests__/ChebyshevBiquad.test.ts +229 -0
- package/src/ts/__tests__/CircularLogBuffer.test.ts +158 -0
- package/src/ts/__tests__/DriftDetector.test.ts +389 -0
- package/src/ts/__tests__/Fft.test.ts +484 -0
- package/src/ts/__tests__/ListState.test.ts +153 -0
- package/src/ts/__tests__/Logger.test.ts +208 -0
- package/src/ts/__tests__/LoggerAdvanced.test.ts +319 -0
- package/src/ts/__tests__/LoggerMinor.test.ts +247 -0
- package/src/ts/__tests__/MeanAbsoluteValue.test.ts +398 -0
- package/src/ts/__tests__/MovingAverage.test.ts +322 -0
- package/src/ts/__tests__/RMS.test.ts +315 -0
- package/src/ts/__tests__/Rectify.test.ts +272 -0
- package/src/ts/__tests__/Redis.test.ts +456 -0
- package/src/ts/__tests__/SlopeSignChange.test.ts +166 -0
- package/src/ts/__tests__/Tap.test.ts +164 -0
- package/src/ts/__tests__/TimeBasedExpiration.test.ts +124 -0
- package/src/ts/__tests__/TimeBasedRmsAndMav.test.ts +231 -0
- package/src/ts/__tests__/TimeBasedVarianceAndZScore.test.ts +284 -0
- package/src/ts/__tests__/TimeSeries.test.ts +254 -0
- package/src/ts/__tests__/TopicRouter.test.ts +332 -0
- package/src/ts/__tests__/TopicRouterAdvanced.test.ts +483 -0
- package/src/ts/__tests__/TopicRouterPriority.test.ts +487 -0
- package/src/ts/__tests__/Variance.test.ts +509 -0
- package/src/ts/__tests__/WaveformLength.test.ts +147 -0
- package/src/ts/__tests__/WillisonAmplitude.test.ts +197 -0
- package/src/ts/__tests__/ZScoreNormalize.test.ts +459 -0
- package/src/ts/advanced-dsp.ts +566 -0
- package/src/ts/backends.ts +1137 -0
- package/src/ts/bindings.ts +1225 -0
- package/src/ts/easter-egg.ts +42 -0
- package/src/ts/examples/MeanAbsoluteValue/test-state.ts +99 -0
- package/src/ts/examples/MeanAbsoluteValue/test-streaming.ts +269 -0
- package/src/ts/examples/MovingAverage/test-state.ts +85 -0
- package/src/ts/examples/MovingAverage/test-streaming.ts +188 -0
- package/src/ts/examples/RMS/test-state.ts +97 -0
- package/src/ts/examples/RMS/test-streaming.ts +253 -0
- package/src/ts/examples/Rectify/test-state.ts +107 -0
- package/src/ts/examples/Rectify/test-streaming.ts +242 -0
- package/src/ts/examples/Variance/test-state.ts +195 -0
- package/src/ts/examples/Variance/test-streaming.ts +260 -0
- package/src/ts/examples/ZScoreNormalize/test-state.ts +277 -0
- package/src/ts/examples/ZScoreNormalize/test-streaming.ts +306 -0
- package/src/ts/examples/advanced-dsp-examples.ts +397 -0
- package/src/ts/examples/callbacks/advanced-router-features.ts +326 -0
- package/src/ts/examples/callbacks/benchmark-circular-buffer.ts +109 -0
- package/src/ts/examples/callbacks/monitoring-example.ts +265 -0
- package/src/ts/examples/callbacks/pipeline-callbacks-example.ts +137 -0
- package/src/ts/examples/callbacks/pooled-callbacks-example.ts +274 -0
- package/src/ts/examples/callbacks/priority-routing-example.ts +277 -0
- package/src/ts/examples/callbacks/production-topic-router.ts +214 -0
- package/src/ts/examples/callbacks/topic-based-logging.ts +161 -0
- package/src/ts/examples/chaining/test-chaining-redis.ts +113 -0
- package/src/ts/examples/chaining/test-chaining.ts +52 -0
- package/src/ts/examples/emg-features-example.ts +284 -0
- package/src/ts/examples/fft-example.ts +309 -0
- package/src/ts/examples/fft-examples.ts +349 -0
- package/src/ts/examples/filter-examples.ts +320 -0
- package/src/ts/examples/list-state-example.ts +131 -0
- package/src/ts/examples/logger-example.ts +91 -0
- package/src/ts/examples/notch-filter-examples.ts +243 -0
- package/src/ts/examples/phase5/drift-detection-example.ts +290 -0
- package/src/ts/examples/phase6-7/production-observability.ts +476 -0
- package/src/ts/examples/phase6-7/redis-timeseries-integration.ts +446 -0
- package/src/ts/examples/redis/redis-example.ts +202 -0
- package/src/ts/examples/redis-example.ts +202 -0
- package/src/ts/examples/simd-benchmark.ts +126 -0
- package/src/ts/examples/tap-debugging.ts +230 -0
- package/src/ts/examples/timeseries/comparison-example.ts +290 -0
- package/src/ts/examples/timeseries/iot-sensor-example.ts +143 -0
- package/src/ts/examples/timeseries/redis-streaming-example.ts +233 -0
- package/src/ts/examples/waveform-length-example.ts +139 -0
- package/src/ts/fft.ts +722 -0
- package/src/ts/filters.ts +1078 -0
- package/src/ts/index.ts +120 -0
- package/src/ts/types.ts +589 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { describe, test, beforeEach } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { createDspPipeline, DspProcessor } from "../bindings.js";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_OPTIONS = { channels: 1, sampleRate: 44100 };
|
|
6
|
+
|
|
7
|
+
function assertCloseTo(actual: number, expected: number, precision = 5) {
|
|
8
|
+
const tolerance = Math.pow(10, -precision);
|
|
9
|
+
assert.ok(
|
|
10
|
+
Math.abs(actual - expected) < tolerance,
|
|
11
|
+
`Expected ${actual} to be close to ${expected} (tolerance: ${tolerance})`
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe("MovingAverage Filter", () => {
|
|
16
|
+
let processor: DspProcessor;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
processor = createDspPipeline();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe("Batch Mode (Stateless)", () => {
|
|
23
|
+
test("should compute batch average correctly", async () => {
|
|
24
|
+
processor.MovingAverage({ mode: "batch" });
|
|
25
|
+
|
|
26
|
+
const input = new Float32Array([1, 2, 3, 4, 5]);
|
|
27
|
+
const output = await processor.process(input, DEFAULT_OPTIONS);
|
|
28
|
+
|
|
29
|
+
// Mean of [1,2,3,4,5] = 3
|
|
30
|
+
// All samples should be replaced with the average
|
|
31
|
+
assert.equal(output.length, 5);
|
|
32
|
+
output.forEach((val) => assertCloseTo(val, 3));
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("should compute average of zero signal", async () => {
|
|
36
|
+
processor.MovingAverage({ mode: "batch" });
|
|
37
|
+
|
|
38
|
+
const input = new Float32Array([0, 0, 0, 0]);
|
|
39
|
+
const output = await processor.process(input, DEFAULT_OPTIONS);
|
|
40
|
+
|
|
41
|
+
output.forEach((val) => assertCloseTo(val, 0));
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("should compute average of constant signal", async () => {
|
|
45
|
+
processor.MovingAverage({ mode: "batch" });
|
|
46
|
+
|
|
47
|
+
const input = new Float32Array([5, 5, 5, 5, 5]);
|
|
48
|
+
const output = await processor.process(input, DEFAULT_OPTIONS);
|
|
49
|
+
|
|
50
|
+
output.forEach((val) => assertCloseTo(val, 5));
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("should handle negative values correctly", async () => {
|
|
54
|
+
processor.MovingAverage({ mode: "batch" });
|
|
55
|
+
|
|
56
|
+
const input = new Float32Array([-2, -1, 0, 1, 2]);
|
|
57
|
+
const output = await processor.process(input, DEFAULT_OPTIONS);
|
|
58
|
+
|
|
59
|
+
// Mean = 0
|
|
60
|
+
output.forEach((val) => assertCloseTo(val, 0));
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("should be stateless between calls", async () => {
|
|
64
|
+
processor.MovingAverage({ mode: "batch" });
|
|
65
|
+
|
|
66
|
+
const input1 = new Float32Array([1, 2, 3, 4, 5]);
|
|
67
|
+
const output1 = await processor.process(input1, DEFAULT_OPTIONS);
|
|
68
|
+
|
|
69
|
+
const input2 = new Float32Array([10, 20, 30]);
|
|
70
|
+
const output2 = await processor.process(input2, DEFAULT_OPTIONS);
|
|
71
|
+
|
|
72
|
+
// First batch: mean = 3
|
|
73
|
+
output1.forEach((val) => assertCloseTo(val, 3));
|
|
74
|
+
|
|
75
|
+
// Second batch: mean = 20 (independent of first)
|
|
76
|
+
output2.forEach((val) => assertCloseTo(val, 20));
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("should handle multi-channel data independently", async () => {
|
|
80
|
+
processor.MovingAverage({ mode: "batch" });
|
|
81
|
+
|
|
82
|
+
// 2 channels, 5 samples per channel
|
|
83
|
+
// Channel 0: [1, 3, 5, 7, 9] → mean = 5
|
|
84
|
+
// Channel 1: [10, 20, 30, 40, 50] → mean = 30
|
|
85
|
+
const input = new Float32Array([1, 10, 3, 20, 5, 30, 7, 40, 9, 50]);
|
|
86
|
+
const output = await processor.process(input, {
|
|
87
|
+
sampleRate: 1000,
|
|
88
|
+
channels: 2,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Extract channels
|
|
92
|
+
const ch0 = [];
|
|
93
|
+
const ch1 = [];
|
|
94
|
+
for (let i = 0; i < output.length; i++) {
|
|
95
|
+
if (i % 2 === 0) ch0.push(output[i]);
|
|
96
|
+
else ch1.push(output[i]);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
ch0.forEach((val) => assertCloseTo(val, 5));
|
|
100
|
+
ch1.forEach((val) => assertCloseTo(val, 30));
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe("Moving Mode (Stateful)", () => {
|
|
105
|
+
test("should compute moving average with window size 3", async () => {
|
|
106
|
+
processor.MovingAverage({ mode: "moving", windowSize: 3 });
|
|
107
|
+
|
|
108
|
+
const input = new Float32Array([1, 2, 3, 4, 5]);
|
|
109
|
+
const output = await processor.process(input, DEFAULT_OPTIONS);
|
|
110
|
+
|
|
111
|
+
// First value: [1] → avg = 1
|
|
112
|
+
// Second value: [1, 2] → avg = 1.5
|
|
113
|
+
// Third value: [1, 2, 3] → avg = 2
|
|
114
|
+
// Fourth value: [2, 3, 4] → avg = 3
|
|
115
|
+
// Fifth value: [3, 4, 5] → avg = 4
|
|
116
|
+
assert.equal(output.length, 5);
|
|
117
|
+
assertCloseTo(output[0], 1);
|
|
118
|
+
assertCloseTo(output[1], 1.5);
|
|
119
|
+
assertCloseTo(output[2], 2);
|
|
120
|
+
assertCloseTo(output[3], 3);
|
|
121
|
+
assertCloseTo(output[4], 4);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("should handle single sample window", async () => {
|
|
125
|
+
processor.MovingAverage({ mode: "moving", windowSize: 1 });
|
|
126
|
+
|
|
127
|
+
const input = new Float32Array([10, 20, 30]);
|
|
128
|
+
const output = await processor.process(input, DEFAULT_OPTIONS);
|
|
129
|
+
|
|
130
|
+
assert.deepEqual(Array.from(output), [10, 20, 30]); // No smoothing with window size 1
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("should handle negative values", async () => {
|
|
134
|
+
processor.MovingAverage({ mode: "moving", windowSize: 2 });
|
|
135
|
+
|
|
136
|
+
const input = new Float32Array([-5, 5, -10, 10]);
|
|
137
|
+
const output = await processor.process(input, DEFAULT_OPTIONS);
|
|
138
|
+
|
|
139
|
+
assertCloseTo(output[0], -5);
|
|
140
|
+
assertCloseTo(output[1], 0); // (-5 + 5) / 2
|
|
141
|
+
assertCloseTo(output[2], -2.5); // (5 + -10) / 2
|
|
142
|
+
assertCloseTo(output[3], 0); // (-10 + 10) / 2
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("should maintain state across multiple process calls", async () => {
|
|
146
|
+
processor.MovingAverage({ mode: "moving", windowSize: 3 });
|
|
147
|
+
|
|
148
|
+
// Process first chunk
|
|
149
|
+
const output1 = await processor.process(
|
|
150
|
+
new Float32Array([1, 2]),
|
|
151
|
+
DEFAULT_OPTIONS
|
|
152
|
+
);
|
|
153
|
+
assertCloseTo(output1[0], 1);
|
|
154
|
+
assertCloseTo(output1[1], 1.5);
|
|
155
|
+
|
|
156
|
+
// Process second chunk - should continue from previous state
|
|
157
|
+
const output2 = await processor.process(
|
|
158
|
+
new Float32Array([3, 4]),
|
|
159
|
+
DEFAULT_OPTIONS
|
|
160
|
+
);
|
|
161
|
+
assertCloseTo(output2[0], 2); // (1 + 2 + 3) / 3
|
|
162
|
+
assertCloseTo(output2[1], 3); // (2 + 3 + 4) / 3
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("should throw error for moving mode without window size", () => {
|
|
166
|
+
assert.throws(() => {
|
|
167
|
+
processor.MovingAverage({ mode: "moving" } as any);
|
|
168
|
+
}, /either windowSize or windowDuration must be specified/);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("should throw error for invalid window size", () => {
|
|
172
|
+
assert.throws(() => {
|
|
173
|
+
processor.MovingAverage({ mode: "moving", windowSize: 0 });
|
|
174
|
+
}, /windowSize must be a positive integer/);
|
|
175
|
+
|
|
176
|
+
assert.throws(() => {
|
|
177
|
+
processor.MovingAverage({ mode: "moving", windowSize: -5 });
|
|
178
|
+
}, /windowSize must be a positive integer/);
|
|
179
|
+
|
|
180
|
+
assert.throws(() => {
|
|
181
|
+
processor.MovingAverage({ mode: "moving", windowSize: 3.14 });
|
|
182
|
+
}, /windowSize must be a positive integer/);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
describe("State Management", () => {
|
|
187
|
+
test("should serialize and deserialize state correctly", async () => {
|
|
188
|
+
processor.MovingAverage({ mode: "moving", windowSize: 3 });
|
|
189
|
+
|
|
190
|
+
// Process some data to build state
|
|
191
|
+
await processor.process(
|
|
192
|
+
new Float32Array([1, 2, 3, 4, 5]),
|
|
193
|
+
DEFAULT_OPTIONS
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
// Save state
|
|
197
|
+
const stateJson = await processor.saveState();
|
|
198
|
+
const state = JSON.parse(stateJson);
|
|
199
|
+
|
|
200
|
+
assert.ok(state);
|
|
201
|
+
assert.ok(state.timestamp);
|
|
202
|
+
assert.equal(state.stages.length, 1);
|
|
203
|
+
assert.equal(state.stages[0].type, "movingAverage");
|
|
204
|
+
|
|
205
|
+
// Create new processor with same pipeline structure and load state
|
|
206
|
+
const processor2 = createDspPipeline();
|
|
207
|
+
processor2.MovingAverage({ mode: "moving", windowSize: 3 }); // Must match original pipeline
|
|
208
|
+
await processor2.loadState(stateJson);
|
|
209
|
+
|
|
210
|
+
// Process should continue from saved state
|
|
211
|
+
const output1 = await processor.process(
|
|
212
|
+
new Float32Array([6]),
|
|
213
|
+
DEFAULT_OPTIONS
|
|
214
|
+
);
|
|
215
|
+
const output2 = await processor2.process(
|
|
216
|
+
new Float32Array([6]),
|
|
217
|
+
DEFAULT_OPTIONS
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
assertCloseTo(output2[0], output1[0]);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("should reset state correctly", async () => {
|
|
224
|
+
processor.MovingAverage({ mode: "moving", windowSize: 3 });
|
|
225
|
+
|
|
226
|
+
// Build up state
|
|
227
|
+
await processor.process(new Float32Array([1, 2, 3]), DEFAULT_OPTIONS);
|
|
228
|
+
|
|
229
|
+
// Reset
|
|
230
|
+
processor.clearState();
|
|
231
|
+
|
|
232
|
+
// Should start fresh
|
|
233
|
+
const output = await processor.process(
|
|
234
|
+
new Float32Array([10]),
|
|
235
|
+
DEFAULT_OPTIONS
|
|
236
|
+
);
|
|
237
|
+
assertCloseTo(output[0], 10); // Only 10 in buffer
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("should validate runningSum on state load", async () => {
|
|
241
|
+
processor.MovingAverage({ mode: "moving", windowSize: 3 });
|
|
242
|
+
await processor.process(new Float32Array([1, 2, 3]), DEFAULT_OPTIONS);
|
|
243
|
+
|
|
244
|
+
const stateJson = await processor.saveState();
|
|
245
|
+
const state = JSON.parse(stateJson);
|
|
246
|
+
|
|
247
|
+
// Corrupt the runningSum (note: it's in state.stages[0].state.channels)
|
|
248
|
+
if (state.stages[0].state.channels && state.stages[0].state.channels[0]) {
|
|
249
|
+
state.stages[0].state.channels[0].runningSum = 9999;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Should throw when loading corrupted state
|
|
253
|
+
const processor2 = createDspPipeline();
|
|
254
|
+
processor2.MovingAverage({ mode: "moving", windowSize: 3 }); // Must match original pipeline
|
|
255
|
+
await assert.rejects(
|
|
256
|
+
async () => await processor2.loadState(JSON.stringify(state)),
|
|
257
|
+
/Running sum validation failed/
|
|
258
|
+
);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test("should validate window size on state load", async () => {
|
|
262
|
+
processor.MovingAverage({ mode: "moving", windowSize: 3 });
|
|
263
|
+
await processor.process(new Float32Array([1, 2, 3]), DEFAULT_OPTIONS);
|
|
264
|
+
|
|
265
|
+
const stateJson = await processor.saveState();
|
|
266
|
+
const state = JSON.parse(stateJson);
|
|
267
|
+
|
|
268
|
+
// Corrupt the window size parameter itself
|
|
269
|
+
if (state.stages[0].state) {
|
|
270
|
+
state.stages[0].state.windowSize = 5; // Change from 3 to 5
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Should throw when loading corrupted state
|
|
274
|
+
const processor2 = createDspPipeline();
|
|
275
|
+
processor2.MovingAverage({ mode: "moving", windowSize: 3 }); // Original window size
|
|
276
|
+
await assert.rejects(
|
|
277
|
+
async () => await processor2.loadState(JSON.stringify(state)),
|
|
278
|
+
/Window size mismatch/
|
|
279
|
+
);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test("should handle empty input", async () => {
|
|
283
|
+
processor.MovingAverage({ mode: "moving", windowSize: 3 });
|
|
284
|
+
const output = await processor.process(
|
|
285
|
+
new Float32Array([]),
|
|
286
|
+
DEFAULT_OPTIONS
|
|
287
|
+
);
|
|
288
|
+
assert.equal(output.length, 0);
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
describe("Multi-channel Processing", () => {
|
|
293
|
+
test("should process data with stateful continuity", async () => {
|
|
294
|
+
processor.MovingAverage({ mode: "moving", windowSize: 2 });
|
|
295
|
+
|
|
296
|
+
// Process first batch
|
|
297
|
+
const output1 = await processor.process(
|
|
298
|
+
new Float32Array([1, 2, 3]),
|
|
299
|
+
DEFAULT_OPTIONS
|
|
300
|
+
);
|
|
301
|
+
assertCloseTo(output1[0], 1); // [1]
|
|
302
|
+
assertCloseTo(output1[1], 1.5); // [1, 2]
|
|
303
|
+
assertCloseTo(output1[2], 2.5); // [2, 3]
|
|
304
|
+
|
|
305
|
+
// Process second batch - state continues from previous
|
|
306
|
+
const output2 = await processor.process(
|
|
307
|
+
new Float32Array([10, 20, 30]),
|
|
308
|
+
DEFAULT_OPTIONS
|
|
309
|
+
);
|
|
310
|
+
assertCloseTo(output2[0], 6.5); // [3, 10]
|
|
311
|
+
assertCloseTo(output2[1], 15); // [10, 20]
|
|
312
|
+
assertCloseTo(output2[2], 25); // [20, 30]
|
|
313
|
+
|
|
314
|
+
// Process third batch - state continues
|
|
315
|
+
const output3 = await processor.process(
|
|
316
|
+
new Float32Array([4]),
|
|
317
|
+
DEFAULT_OPTIONS
|
|
318
|
+
);
|
|
319
|
+
assertCloseTo(output3[0], 17); // [30, 4]
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
});
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import { describe, test, beforeEach } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { createDspPipeline, DspProcessor } from "../bindings.js";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_OPTIONS = { channels: 1, sampleRate: 44100 };
|
|
6
|
+
|
|
7
|
+
function assertCloseTo(actual: number, expected: number, precision = 5) {
|
|
8
|
+
const tolerance = Math.pow(10, -precision);
|
|
9
|
+
assert.ok(
|
|
10
|
+
Math.abs(actual - expected) < tolerance,
|
|
11
|
+
`Expected ${actual} to be close to ${expected} (tolerance: ${tolerance})`
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe("RMS Filter", () => {
|
|
16
|
+
let processor: DspProcessor;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
processor = createDspPipeline();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe("Basic Functionality", () => {
|
|
23
|
+
test("should compute RMS with window size 3", async () => {
|
|
24
|
+
processor.Rms({ mode: "moving", windowSize: 3 });
|
|
25
|
+
|
|
26
|
+
const input = new Float32Array([3, 4, 0, 6, 8]);
|
|
27
|
+
const output = await processor.process(input, DEFAULT_OPTIONS);
|
|
28
|
+
|
|
29
|
+
// First value: sqrt(3²) = 3
|
|
30
|
+
// Second value: sqrt((3² + 4²) / 2) = sqrt(25 / 2) = 3.5355...
|
|
31
|
+
// Third value: sqrt((3² + 4² + 0²) / 3) = sqrt(25 / 3) = 2.8867...
|
|
32
|
+
// Fourth value: sqrt((4² + 0² + 6²) / 3) = sqrt(52 / 3) = 4.1633...
|
|
33
|
+
// Fifth value: sqrt((0² + 6² + 8²) / 3) = sqrt(100 / 3) = 5.7735...
|
|
34
|
+
assert.equal(output.length, 5);
|
|
35
|
+
assertCloseTo(output[0], 3);
|
|
36
|
+
assertCloseTo(output[1], 3.5355, 4);
|
|
37
|
+
assertCloseTo(output[2], 2.8867, 4);
|
|
38
|
+
assertCloseTo(output[3], 4.1633, 4);
|
|
39
|
+
assertCloseTo(output[4], 5.7735, 4);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("should handle single sample window", async () => {
|
|
43
|
+
processor.Rms({ mode: "moving", windowSize: 1 });
|
|
44
|
+
|
|
45
|
+
const input = new Float32Array([3, 4, 5]);
|
|
46
|
+
const output = await processor.process(input, DEFAULT_OPTIONS);
|
|
47
|
+
|
|
48
|
+
// RMS of single sample is just the absolute value
|
|
49
|
+
assertCloseTo(output[0], 3);
|
|
50
|
+
assertCloseTo(output[1], 4);
|
|
51
|
+
assertCloseTo(output[2], 5);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("should compute RMS correctly for negative values", async () => {
|
|
55
|
+
processor.Rms({ mode: "moving", windowSize: 2 });
|
|
56
|
+
|
|
57
|
+
const input = new Float32Array([-3, 4, -5, 12]);
|
|
58
|
+
const output = await processor.process(input, DEFAULT_OPTIONS);
|
|
59
|
+
|
|
60
|
+
// First: sqrt((-3)²) = 3
|
|
61
|
+
// Second: sqrt((9 + 16) / 2) = sqrt(12.5) = 3.5355...
|
|
62
|
+
// Third: sqrt((16 + 25) / 2) = sqrt(20.5) = 4.5277...
|
|
63
|
+
// Fourth: sqrt((25 + 144) / 2) = sqrt(84.5) = 9.1924...
|
|
64
|
+
assertCloseTo(output[0], 3);
|
|
65
|
+
assertCloseTo(output[1], 3.5355, 4);
|
|
66
|
+
assertCloseTo(output[2], 4.5277, 4);
|
|
67
|
+
assertCloseTo(output[3], 9.1924, 4);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("should maintain state across multiple process calls", async () => {
|
|
71
|
+
processor.Rms({ mode: "moving", windowSize: 3 });
|
|
72
|
+
|
|
73
|
+
// First batch: [3, 4]
|
|
74
|
+
const output1 = await processor.process(
|
|
75
|
+
new Float32Array([3, 4]),
|
|
76
|
+
DEFAULT_OPTIONS
|
|
77
|
+
);
|
|
78
|
+
assertCloseTo(output1[0], 3);
|
|
79
|
+
assertCloseTo(output1[1], 3.5355, 4);
|
|
80
|
+
|
|
81
|
+
// Second batch: [5] - should use [3, 4, 5] for RMS
|
|
82
|
+
const output2 = await processor.process(
|
|
83
|
+
new Float32Array([5]),
|
|
84
|
+
DEFAULT_OPTIONS
|
|
85
|
+
);
|
|
86
|
+
// sqrt((3² + 4² + 5²) / 3) = sqrt(50 / 3) = 4.0824...
|
|
87
|
+
assertCloseTo(output2[0], 4.0824, 4);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe("State Management", () => {
|
|
92
|
+
test("should serialize and deserialize state correctly", async () => {
|
|
93
|
+
processor.Rms({ mode: "moving", windowSize: 3 });
|
|
94
|
+
|
|
95
|
+
// Build state
|
|
96
|
+
await processor.process(new Float32Array([3, 4, 5]), DEFAULT_OPTIONS);
|
|
97
|
+
|
|
98
|
+
const stateJson = await processor.saveState();
|
|
99
|
+
const state = JSON.parse(stateJson);
|
|
100
|
+
|
|
101
|
+
assert.ok(state);
|
|
102
|
+
assert.ok(state.timestamp);
|
|
103
|
+
assert.equal(state.stages.length, 1);
|
|
104
|
+
assert.equal(state.stages[0].type, "rms");
|
|
105
|
+
assert.equal(state.stages[0].state.windowSize, 3);
|
|
106
|
+
assert.ok(state.stages[0].state.channels);
|
|
107
|
+
|
|
108
|
+
// Create new processor and load state
|
|
109
|
+
const processor2 = createDspPipeline();
|
|
110
|
+
processor2.Rms({ mode: "moving", windowSize: 3 });
|
|
111
|
+
await processor2.loadState(stateJson);
|
|
112
|
+
|
|
113
|
+
// Both should produce same output for next sample
|
|
114
|
+
const output1 = await processor.process(
|
|
115
|
+
new Float32Array([6]),
|
|
116
|
+
DEFAULT_OPTIONS
|
|
117
|
+
);
|
|
118
|
+
const output2 = await processor2.process(
|
|
119
|
+
new Float32Array([6]),
|
|
120
|
+
DEFAULT_OPTIONS
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
assertCloseTo(output2[0], output1[0]);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("should reset state correctly", async () => {
|
|
127
|
+
processor.Rms({ mode: "moving", windowSize: 3 });
|
|
128
|
+
|
|
129
|
+
// Build state
|
|
130
|
+
await processor.process(new Float32Array([3, 4, 5]), DEFAULT_OPTIONS);
|
|
131
|
+
|
|
132
|
+
// Reset
|
|
133
|
+
processor.clearState();
|
|
134
|
+
|
|
135
|
+
// Should start fresh
|
|
136
|
+
const output = await processor.process(
|
|
137
|
+
new Float32Array([10]),
|
|
138
|
+
DEFAULT_OPTIONS
|
|
139
|
+
);
|
|
140
|
+
assertCloseTo(output[0], 10); // RMS of single value
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("should validate runningSumOfSquares on state load", async () => {
|
|
144
|
+
processor.Rms({ mode: "moving", windowSize: 3 });
|
|
145
|
+
await processor.process(new Float32Array([3, 4, 5]), DEFAULT_OPTIONS);
|
|
146
|
+
|
|
147
|
+
const stateJson = await processor.saveState();
|
|
148
|
+
const state = JSON.parse(stateJson);
|
|
149
|
+
|
|
150
|
+
// Corrupt runningSumOfSquares
|
|
151
|
+
if (state.stages[0].state.channels && state.stages[0].state.channels[0]) {
|
|
152
|
+
state.stages[0].state.channels[0].runningSumOfSquares = 9999;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Should throw when loading corrupted state
|
|
156
|
+
const processor2 = createDspPipeline();
|
|
157
|
+
processor2.Rms({ mode: "moving", windowSize: 3 });
|
|
158
|
+
await assert.rejects(
|
|
159
|
+
async () => await processor2.loadState(JSON.stringify(state)),
|
|
160
|
+
/Running sum of squares validation failed/
|
|
161
|
+
);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("should validate window size on state load", async () => {
|
|
165
|
+
processor.Rms({ mode: "moving", windowSize: 3 });
|
|
166
|
+
await processor.process(new Float32Array([3, 4, 5]), DEFAULT_OPTIONS);
|
|
167
|
+
|
|
168
|
+
const stateJson = await processor.saveState();
|
|
169
|
+
const state = JSON.parse(stateJson);
|
|
170
|
+
|
|
171
|
+
// Corrupt window size
|
|
172
|
+
if (state.stages[0].state) {
|
|
173
|
+
state.stages[0].state.windowSize = 5;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Should throw when loading corrupted state
|
|
177
|
+
const processor2 = createDspPipeline();
|
|
178
|
+
processor2.Rms({ mode: "moving", windowSize: 3 });
|
|
179
|
+
await assert.rejects(
|
|
180
|
+
async () => await processor2.loadState(JSON.stringify(state)),
|
|
181
|
+
/Window size mismatch/
|
|
182
|
+
);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
describe("Mathematical Properties", () => {
|
|
187
|
+
test("should compute RMS of constant signal correctly", async () => {
|
|
188
|
+
processor.Rms({ mode: "moving", windowSize: 4 });
|
|
189
|
+
|
|
190
|
+
const input = new Float32Array([5, 5, 5, 5]);
|
|
191
|
+
const output = await processor.process(input, DEFAULT_OPTIONS);
|
|
192
|
+
|
|
193
|
+
// RMS of constant signal equals the constant
|
|
194
|
+
output.forEach((val) => assertCloseTo(val, 5));
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("should compute RMS of zero signal", async () => {
|
|
198
|
+
processor.Rms({ mode: "moving", windowSize: 3 });
|
|
199
|
+
|
|
200
|
+
const input = new Float32Array([0, 0, 0, 0]);
|
|
201
|
+
const output = await processor.process(input, DEFAULT_OPTIONS);
|
|
202
|
+
|
|
203
|
+
output.forEach((val) => assertCloseTo(val, 0));
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("should handle alternating positive/negative", async () => {
|
|
207
|
+
processor.Rms({ mode: "moving", windowSize: 2 });
|
|
208
|
+
|
|
209
|
+
const input = new Float32Array([3, -3, 4, -4]);
|
|
210
|
+
const output = await processor.process(input, DEFAULT_OPTIONS);
|
|
211
|
+
|
|
212
|
+
// First: sqrt(9) = 3
|
|
213
|
+
// Second: sqrt((9 + 9) / 2) = 3
|
|
214
|
+
// Third: sqrt((9 + 16) / 2) = sqrt(12.5) = 3.5355...
|
|
215
|
+
// Fourth: sqrt((16 + 16) / 2) = 4
|
|
216
|
+
assertCloseTo(output[0], 3);
|
|
217
|
+
assertCloseTo(output[1], 3);
|
|
218
|
+
assertCloseTo(output[2], 3.5355, 4);
|
|
219
|
+
assertCloseTo(output[3], 4);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("should produce value equal to or less than max absolute value", async () => {
|
|
223
|
+
processor.Rms({ mode: "moving", windowSize: 3 });
|
|
224
|
+
|
|
225
|
+
const input = new Float32Array([1, 5, 2]);
|
|
226
|
+
const output = await processor.process(input, DEFAULT_OPTIONS);
|
|
227
|
+
|
|
228
|
+
// RMS should be <= max absolute value for each window
|
|
229
|
+
assert.ok(output[0] <= 1);
|
|
230
|
+
assert.ok(output[1] <= 5);
|
|
231
|
+
assert.ok(output[2] <= 5);
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe("Edge Cases", () => {
|
|
236
|
+
test("should handle empty input array", async () => {
|
|
237
|
+
processor.Rms({ mode: "moving", windowSize: 3 });
|
|
238
|
+
|
|
239
|
+
const output = await processor.process(
|
|
240
|
+
new Float32Array([]),
|
|
241
|
+
DEFAULT_OPTIONS
|
|
242
|
+
);
|
|
243
|
+
assert.equal(output.length, 0);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("should handle very small values", async () => {
|
|
247
|
+
processor.Rms({ mode: "moving", windowSize: 2 });
|
|
248
|
+
|
|
249
|
+
const input = new Float32Array([0.0001, 0.0002, 0.0001]);
|
|
250
|
+
const output = await processor.process(input, DEFAULT_OPTIONS);
|
|
251
|
+
|
|
252
|
+
assert.ok(output.every((v) => v > 0 && v < 0.001));
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test("should handle very large values", async () => {
|
|
256
|
+
processor.Rms({ mode: "moving", windowSize: 2 });
|
|
257
|
+
|
|
258
|
+
const input = new Float32Array([1e6, 1e6]);
|
|
259
|
+
const output = await processor.process(input, DEFAULT_OPTIONS);
|
|
260
|
+
|
|
261
|
+
assertCloseTo(output[0], 1e6);
|
|
262
|
+
assertCloseTo(output[1], 1e6);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("should handle mixed magnitude ranges", async () => {
|
|
266
|
+
processor.Rms({ mode: "moving", windowSize: 3 });
|
|
267
|
+
|
|
268
|
+
const input = new Float32Array([0.001, 1000, 0.001]);
|
|
269
|
+
const output = await processor.process(input, DEFAULT_OPTIONS);
|
|
270
|
+
|
|
271
|
+
assert.ok(output.every((v) => v >= 0));
|
|
272
|
+
assert.ok(output.some((v) => v > 100)); // Large value influences RMS
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
describe("Multi-channel Processing", () => {
|
|
277
|
+
test("should process data with stateful continuity", async () => {
|
|
278
|
+
processor.Rms({ mode: "moving", windowSize: 2 });
|
|
279
|
+
|
|
280
|
+
// First batch
|
|
281
|
+
const output1 = await processor.process(
|
|
282
|
+
new Float32Array([3, 4]),
|
|
283
|
+
DEFAULT_OPTIONS
|
|
284
|
+
);
|
|
285
|
+
assertCloseTo(output1[0], 3);
|
|
286
|
+
assertCloseTo(output1[1], 3.5355, 4);
|
|
287
|
+
|
|
288
|
+
// Second batch - continues from previous
|
|
289
|
+
const output2 = await processor.process(
|
|
290
|
+
new Float32Array([5, 0]),
|
|
291
|
+
DEFAULT_OPTIONS
|
|
292
|
+
);
|
|
293
|
+
// sqrt((16 + 25) / 2) = sqrt(20.5) = 4.5277...
|
|
294
|
+
assertCloseTo(output2[0], 4.5277, 4);
|
|
295
|
+
// sqrt((25 + 0) / 2) = sqrt(12.5) = 3.5355...
|
|
296
|
+
assertCloseTo(output2[1], 3.5355, 4);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test("should maintain separate state across multiple batches", async () => {
|
|
300
|
+
processor.Rms({ mode: "moving", windowSize: 2 });
|
|
301
|
+
|
|
302
|
+
const batches = [
|
|
303
|
+
new Float32Array([1, 2]),
|
|
304
|
+
new Float32Array([3, 4]),
|
|
305
|
+
new Float32Array([5, 6]),
|
|
306
|
+
];
|
|
307
|
+
|
|
308
|
+
for (const batch of batches) {
|
|
309
|
+
const output = await processor.process(batch, DEFAULT_OPTIONS);
|
|
310
|
+
assert.equal(output.length, batch.length);
|
|
311
|
+
assert.ok(output.every((v) => v > 0));
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
});
|