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