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,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
|
+
});
|