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,197 @@
|
|
|
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("Willison Amplitude (WAMP)", () => {
|
|
8
|
+
let pipeline: DspProcessor;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
pipeline = createDspPipeline();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("should count amplitude changes exceeding threshold in sliding window", async () => {
|
|
15
|
+
pipeline.WillisonAmplitude({ windowSize: 5, threshold: 1.0 });
|
|
16
|
+
|
|
17
|
+
// Signal: [0, 0.5, 2.0, 2.5, 1.0, 3.0]
|
|
18
|
+
// Differences: 0.5, 1.5, 0.5, -1.5, 2.0
|
|
19
|
+
// Exceeding threshold (1.0): at indices 2(1.5), 4(-1.5), 5(2.0)
|
|
20
|
+
// Window counts (window size = 5):
|
|
21
|
+
const buffer = new Float32Array([0, 0.5, 2.0, 2.5, 1.0, 3.0]);
|
|
22
|
+
await pipeline.process(buffer, DEFAULT_OPTIONS);
|
|
23
|
+
|
|
24
|
+
assert.strictEqual(buffer[0], 0); // First sample, no previous
|
|
25
|
+
assert.strictEqual(buffer[1], 0); // |0.5| < 1.0
|
|
26
|
+
assert.strictEqual(buffer[2], 1); // |1.5| > 1.0, window count = 1
|
|
27
|
+
assert.strictEqual(buffer[3], 1); // |0.5| < 1.0, window still has 1
|
|
28
|
+
assert.strictEqual(buffer[4], 1); // |-1.5| > 1.0, but only within last 5 samples
|
|
29
|
+
assert.strictEqual(buffer[5], 1); // |2.0| > 1.0, window count = 1
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("should handle zero threshold", async () => {
|
|
33
|
+
pipeline.WillisonAmplitude({ windowSize: 4, threshold: 0 });
|
|
34
|
+
|
|
35
|
+
// All differences > 0 should be counted
|
|
36
|
+
const buffer = new Float32Array([1, 2, 3, 2, 3]);
|
|
37
|
+
await pipeline.process(buffer, DEFAULT_OPTIONS);
|
|
38
|
+
|
|
39
|
+
assert.strictEqual(buffer[0], 0);
|
|
40
|
+
assert.strictEqual(buffer[1], 1); // |1| > 0
|
|
41
|
+
assert.strictEqual(buffer[2], 1); // |1| > 0, window=[1,1]
|
|
42
|
+
assert.strictEqual(buffer[3], 1); // |-1| > 0, window=[1,1,-1]
|
|
43
|
+
assert.strictEqual(buffer[4], 1); // |1| > 0, window=[1,-1,1] (4 samples max)
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("should handle multi-channel WAMP", async () => {
|
|
47
|
+
pipeline.WillisonAmplitude({ windowSize: 3, threshold: 0.5 });
|
|
48
|
+
|
|
49
|
+
// 2 channels
|
|
50
|
+
// Ch0: [0, 1, 2, 1] - diffs: 1, 1, -1 (all exceed 0.5)
|
|
51
|
+
// Ch1: [0, 0.3, 1.0, 1.2] - diffs: 0.3, 0.7, 0.2
|
|
52
|
+
const buffer = new Float32Array([
|
|
53
|
+
0,
|
|
54
|
+
0, // Sample 0
|
|
55
|
+
1,
|
|
56
|
+
0.3, // Sample 1
|
|
57
|
+
2,
|
|
58
|
+
1.0, // Sample 2
|
|
59
|
+
1,
|
|
60
|
+
1.2, // Sample 3
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
await pipeline.process(buffer, { channels: 2, sampleRate: 44100 });
|
|
64
|
+
|
|
65
|
+
// Channel 0: all diffs exceed threshold
|
|
66
|
+
assert.strictEqual(buffer[0], 0);
|
|
67
|
+
assert.strictEqual(buffer[2], 1);
|
|
68
|
+
assert.strictEqual(buffer[4], 1); // Window count within last 3
|
|
69
|
+
assert.strictEqual(buffer[6], 1);
|
|
70
|
+
|
|
71
|
+
// Channel 1: only diff of 0.7 exceeds threshold
|
|
72
|
+
assert.strictEqual(buffer[1], 0);
|
|
73
|
+
assert.strictEqual(buffer[3], 0); // |0.3| < 0.5
|
|
74
|
+
assert.strictEqual(buffer[5], 1); // |0.7| > 0.5
|
|
75
|
+
assert.strictEqual(buffer[7], 1); // Window still has the 0.7 change
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("should handle constant signal (no amplitude changes)", async () => {
|
|
79
|
+
pipeline.WillisonAmplitude({ windowSize: 5, threshold: 0 });
|
|
80
|
+
|
|
81
|
+
const buffer = new Float32Array([5, 5, 5, 5, 5]);
|
|
82
|
+
await pipeline.process(buffer, DEFAULT_OPTIONS);
|
|
83
|
+
|
|
84
|
+
// All differences are 0, none exceed threshold
|
|
85
|
+
assert.strictEqual(buffer[0], 0);
|
|
86
|
+
assert.strictEqual(buffer[1], 0);
|
|
87
|
+
assert.strictEqual(buffer[2], 0);
|
|
88
|
+
assert.strictEqual(buffer[3], 0);
|
|
89
|
+
assert.strictEqual(buffer[4], 0);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("should handle negative values correctly", async () => {
|
|
93
|
+
pipeline.WillisonAmplitude({ windowSize: 4, threshold: 1.0 });
|
|
94
|
+
|
|
95
|
+
// Signal: [-2, -4, -1, -3]
|
|
96
|
+
// Absolute differences: |-2| = 2, |3| = 3, |-2| = 2
|
|
97
|
+
const buffer = new Float32Array([-2, -4, -1, -3]);
|
|
98
|
+
await pipeline.process(buffer, DEFAULT_OPTIONS);
|
|
99
|
+
|
|
100
|
+
assert.strictEqual(buffer[0], 0);
|
|
101
|
+
assert.strictEqual(buffer[1], 1); // |-2| > 1.0
|
|
102
|
+
assert.strictEqual(buffer[2], 1); // |3| > 1.0, window count
|
|
103
|
+
assert.strictEqual(buffer[3], 1); // |-2| > 1.0, window count
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("should apply sliding window correctly", async () => {
|
|
107
|
+
pipeline.WillisonAmplitude({ windowSize: 2, threshold: 0.5 });
|
|
108
|
+
|
|
109
|
+
// Signal: [0, 2, 3, 3.3, 3.5]
|
|
110
|
+
// Diffs: 2, 1, 0.3, 0.2
|
|
111
|
+
// Exceeding: indices 1(2), 2(1)
|
|
112
|
+
const buffer = new Float32Array([0, 2, 3, 3.3, 3.5]);
|
|
113
|
+
await pipeline.process(buffer, DEFAULT_OPTIONS);
|
|
114
|
+
|
|
115
|
+
assert.strictEqual(buffer[0], 0);
|
|
116
|
+
assert.strictEqual(buffer[1], 1); // |2| > 0.5
|
|
117
|
+
assert.strictEqual(buffer[2], 1); // |1| > 0.5, window=[true, true] but size=2
|
|
118
|
+
assert.strictEqual(buffer[3], 1); // |0.3| < 0.5, window=[true, false]
|
|
119
|
+
assert.strictEqual(buffer[4], 0); // |0.2| < 0.5, window=[false, false]
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("should reset state correctly", async () => {
|
|
123
|
+
pipeline.WillisonAmplitude({ windowSize: 3, threshold: 1.0 });
|
|
124
|
+
|
|
125
|
+
const buffer1 = new Float32Array([0, 2, 4, 3]);
|
|
126
|
+
await pipeline.process(buffer1, DEFAULT_OPTIONS);
|
|
127
|
+
|
|
128
|
+
pipeline.clearState();
|
|
129
|
+
|
|
130
|
+
const buffer2 = new Float32Array([0, 2, 4, 3]);
|
|
131
|
+
await pipeline.process(buffer2, DEFAULT_OPTIONS);
|
|
132
|
+
|
|
133
|
+
// After reset, should get same results
|
|
134
|
+
for (let i = 0; i < buffer1.length; i++) {
|
|
135
|
+
assert.strictEqual(buffer1[i], buffer2[i]);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("should serialize and deserialize state", async () => {
|
|
140
|
+
pipeline.WillisonAmplitude({ windowSize: 3, threshold: 1.0 });
|
|
141
|
+
|
|
142
|
+
const buffer = new Float32Array([0, 2, 4, 3]);
|
|
143
|
+
await pipeline.process(buffer, DEFAULT_OPTIONS);
|
|
144
|
+
|
|
145
|
+
const state = await pipeline.saveState();
|
|
146
|
+
|
|
147
|
+
const newPipeline = createDspPipeline();
|
|
148
|
+
newPipeline.WillisonAmplitude({ windowSize: 3, threshold: 1.0 }); // Must match original
|
|
149
|
+
await newPipeline.loadState(state);
|
|
150
|
+
|
|
151
|
+
const buffer2 = new Float32Array([5, 6]);
|
|
152
|
+
await newPipeline.process(buffer2, DEFAULT_OPTIONS);
|
|
153
|
+
|
|
154
|
+
// Should continue from where we left off
|
|
155
|
+
assert.ok(buffer2[0] > 0); // |5-3| = 2 > 1.0
|
|
156
|
+
assert.ok(buffer2[1] > 0); // |6-5| = 1 > 1.0 (barely)
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("should throw error for invalid window size", () => {
|
|
160
|
+
assert.throws(() => {
|
|
161
|
+
pipeline.WillisonAmplitude({ windowSize: 0, threshold: 1.0 });
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("should throw error for missing window size", () => {
|
|
166
|
+
assert.throws(() => {
|
|
167
|
+
// @ts-expect-error - Testing missing windowSize
|
|
168
|
+
pipeline.WillisonAmplitude({ threshold: 1.0 });
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("should default to zero threshold when not specified", () => {
|
|
173
|
+
assert.doesNotThrow(() => {
|
|
174
|
+
pipeline.WillisonAmplitude({ windowSize: 5 });
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("should handle high-frequency signal", async () => {
|
|
179
|
+
pipeline.WillisonAmplitude({ windowSize: 10, threshold: 0.5 });
|
|
180
|
+
|
|
181
|
+
// Sine-like signal with varying amplitudes
|
|
182
|
+
const buffer = new Float32Array(20);
|
|
183
|
+
for (let i = 0; i < 20; i++) {
|
|
184
|
+
buffer[i] = Math.sin(i * 0.5) * 2;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
await pipeline.process(buffer, DEFAULT_OPTIONS);
|
|
188
|
+
|
|
189
|
+
// Should detect amplitude changes in the sine wave
|
|
190
|
+
let countedChanges = 0;
|
|
191
|
+
for (let i = 1; i < buffer.length; i++) {
|
|
192
|
+
if (buffer[i] > 0) countedChanges++;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
assert.ok(countedChanges > 0);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
import { describe, test } from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import { createDspPipeline } from "../bindings.js";
|
|
4
|
+
|
|
5
|
+
describe("Z-Score Normalize Filter", () => {
|
|
6
|
+
describe("Batch Mode (Stateless)", () => {
|
|
7
|
+
test("should normalize to mean 0 and stddev ~1 for batch mode", async () => {
|
|
8
|
+
const pipeline = createDspPipeline();
|
|
9
|
+
pipeline.ZScoreNormalize({ mode: "batch" });
|
|
10
|
+
|
|
11
|
+
// Input: [1, 2, 3, 4, 5] with mean=3, stddev≈1.414
|
|
12
|
+
const input = new Float32Array([1, 2, 3, 4, 5]);
|
|
13
|
+
const output = await pipeline.process(input, {
|
|
14
|
+
sampleRate: 1000,
|
|
15
|
+
channels: 1,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// Calculate mean (should be ~0)
|
|
19
|
+
const mean = output.reduce((sum, val) => sum + val, 0) / output.length;
|
|
20
|
+
assert.ok(Math.abs(mean) < 0.0001, `Mean should be ~0, got ${mean}`);
|
|
21
|
+
|
|
22
|
+
// Calculate variance (should be ~1)
|
|
23
|
+
const variance =
|
|
24
|
+
output.reduce((sum, val) => sum + val * val, 0) / output.length;
|
|
25
|
+
assert.ok(
|
|
26
|
+
Math.abs(variance - 1.0) < 0.01,
|
|
27
|
+
`Variance should be ~1, got ${variance}`
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
// Expected normalized values: [-1.414, -0.707, 0, 0.707, 1.414]
|
|
31
|
+
assert.ok(Math.abs(output[0] - -1.414) < 0.01);
|
|
32
|
+
assert.ok(Math.abs(output[2] - 0) < 0.01); // Middle value should be ~0
|
|
33
|
+
assert.ok(Math.abs(output[4] - 1.414) < 0.01);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("should handle constant signal (zero stddev) with epsilon", async () => {
|
|
37
|
+
const pipeline = createDspPipeline();
|
|
38
|
+
pipeline.ZScoreNormalize({ mode: "batch", epsilon: 1e-6 });
|
|
39
|
+
|
|
40
|
+
const input = new Float32Array([5, 5, 5, 5, 5]);
|
|
41
|
+
const output = await pipeline.process(input, {
|
|
42
|
+
sampleRate: 1000,
|
|
43
|
+
channels: 1,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// When stddev is 0, all values should be 0 (since they all equal the mean)
|
|
47
|
+
output.forEach((val, i) => {
|
|
48
|
+
assert.ok(
|
|
49
|
+
Math.abs(val) < 0.0001,
|
|
50
|
+
`Expected 0 for constant signal, got ${val} at index ${i}`
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("should handle negative values correctly", async () => {
|
|
56
|
+
const pipeline = createDspPipeline();
|
|
57
|
+
pipeline.ZScoreNormalize({ mode: "batch" });
|
|
58
|
+
|
|
59
|
+
const input = new Float32Array([-2, -1, 0, 1, 2]);
|
|
60
|
+
const output = await pipeline.process(input, {
|
|
61
|
+
sampleRate: 1000,
|
|
62
|
+
channels: 1,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Mean should be ~0
|
|
66
|
+
const mean = output.reduce((sum, val) => sum + val, 0) / output.length;
|
|
67
|
+
assert.ok(Math.abs(mean) < 0.0001);
|
|
68
|
+
|
|
69
|
+
// Variance should be ~1
|
|
70
|
+
const variance =
|
|
71
|
+
output.reduce((sum, val) => sum + val * val, 0) / output.length;
|
|
72
|
+
assert.ok(Math.abs(variance - 1.0) < 0.01);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("should be stateless between calls", async () => {
|
|
76
|
+
const pipeline = createDspPipeline();
|
|
77
|
+
pipeline.ZScoreNormalize({ mode: "batch" });
|
|
78
|
+
|
|
79
|
+
const input1 = new Float32Array([1, 2, 3, 4, 5]);
|
|
80
|
+
const output1 = await pipeline.process(input1, {
|
|
81
|
+
sampleRate: 1000,
|
|
82
|
+
channels: 1,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const input2 = new Float32Array([1, 2, 3, 4, 5]);
|
|
86
|
+
const output2 = await pipeline.process(input2, {
|
|
87
|
+
sampleRate: 1000,
|
|
88
|
+
channels: 1,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Both outputs should be identical (stateless)
|
|
92
|
+
for (let i = 0; i < output1.length; i++) {
|
|
93
|
+
assert.strictEqual(output1[i], output2[i]);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("should handle multi-channel data independently", async () => {
|
|
98
|
+
const pipeline = createDspPipeline();
|
|
99
|
+
pipeline.ZScoreNormalize({ mode: "batch" });
|
|
100
|
+
|
|
101
|
+
// 2 channels, 5 samples per channel
|
|
102
|
+
// Channel 0: [1, 3, 5, 7, 9] (interleaved at indices 0,2,4,6,8)
|
|
103
|
+
// Channel 1: [10, 20, 30, 40, 50] (interleaved at indices 1,3,5,7,9)
|
|
104
|
+
const input = new Float32Array([1, 10, 3, 20, 5, 30, 7, 40, 9, 50]);
|
|
105
|
+
const output = await pipeline.process(input, {
|
|
106
|
+
sampleRate: 1000,
|
|
107
|
+
channels: 2,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Extract channels
|
|
111
|
+
const ch0 = [];
|
|
112
|
+
const ch1 = [];
|
|
113
|
+
for (let i = 0; i < output.length; i++) {
|
|
114
|
+
if (i % 2 === 0) ch0.push(output[i]);
|
|
115
|
+
else ch1.push(output[i]);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Each channel should have mean ~0 and variance ~1
|
|
119
|
+
const mean0 = ch0.reduce((s, v) => s + v, 0) / ch0.length;
|
|
120
|
+
const mean1 = ch1.reduce((s, v) => s + v, 0) / ch1.length;
|
|
121
|
+
|
|
122
|
+
assert.ok(
|
|
123
|
+
Math.abs(mean0) < 0.0001,
|
|
124
|
+
`Ch0 mean should be ~0, got ${mean0}`
|
|
125
|
+
);
|
|
126
|
+
assert.ok(
|
|
127
|
+
Math.abs(mean1) < 0.0001,
|
|
128
|
+
`Ch1 mean should be ~0, got ${mean1}`
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe("Moving Mode (Stateful)", () => {
|
|
134
|
+
test("should compute moving Z-Score with window size 3", async () => {
|
|
135
|
+
const pipeline = createDspPipeline();
|
|
136
|
+
pipeline.ZScoreNormalize({ mode: "moving", windowSize: 3 });
|
|
137
|
+
|
|
138
|
+
const input = new Float32Array([1, 2, 3, 4, 5]);
|
|
139
|
+
const output = await pipeline.process(input, {
|
|
140
|
+
sampleRate: 1000,
|
|
141
|
+
channels: 1,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Window [1]: mean=1, stddev=0 → z-score=0
|
|
145
|
+
assert.ok(Math.abs(output[0] - 0) < 0.01);
|
|
146
|
+
|
|
147
|
+
// Window [1,2]: mean=1.5, stddev=0.5 → z-score for 2 = (2-1.5)/0.5 = 1.0
|
|
148
|
+
assert.ok(Math.abs(output[1] - 1.0) < 0.01);
|
|
149
|
+
|
|
150
|
+
// Window [1,2,3]: mean=2, stddev≈0.816 → z-score for 3 = (3-2)/0.816 ≈ 1.225
|
|
151
|
+
assert.ok(Math.abs(output[2] - 1.225) < 0.05);
|
|
152
|
+
|
|
153
|
+
// Window [2,3,4]: mean=3, stddev≈0.816 → z-score for 4 = (4-3)/0.816 ≈ 1.225
|
|
154
|
+
assert.ok(Math.abs(output[3] - 1.225) < 0.05);
|
|
155
|
+
|
|
156
|
+
// Window [3,4,5]: mean=4, stddev≈0.816 → z-score for 5 = (5-4)/0.816 ≈ 1.225
|
|
157
|
+
assert.ok(Math.abs(output[4] - 1.225) < 0.05);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("should maintain state across multiple process calls", async () => {
|
|
161
|
+
const pipeline = createDspPipeline();
|
|
162
|
+
pipeline.ZScoreNormalize({ mode: "moving", windowSize: 3 });
|
|
163
|
+
|
|
164
|
+
// First batch: [1, 2, 3]
|
|
165
|
+
const input1 = new Float32Array([1, 2, 3]);
|
|
166
|
+
const output1 = await pipeline.process(input1, {
|
|
167
|
+
sampleRate: 1000,
|
|
168
|
+
channels: 1,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// After first batch, buffer contains [1, 2, 3]
|
|
172
|
+
// Second batch: [4, 5]
|
|
173
|
+
const input2 = new Float32Array([4, 5]);
|
|
174
|
+
const output2 = await pipeline.process(input2, {
|
|
175
|
+
sampleRate: 1000,
|
|
176
|
+
channels: 1,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// For value 4: window is [2, 3, 4], mean=3, stddev≈0.816
|
|
180
|
+
// Z-score for 4 = (4-3)/0.816 ≈ 1.225
|
|
181
|
+
assert.ok(Math.abs(output2[0] - 1.225) < 0.05);
|
|
182
|
+
|
|
183
|
+
// For value 5: window is [3, 4, 5], mean=4, stddev≈0.816
|
|
184
|
+
// Z-score for 5 = (5-4)/0.816 ≈ 1.225
|
|
185
|
+
assert.ok(Math.abs(output2[1] - 1.225) < 0.05);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("should handle window size of 1", async () => {
|
|
189
|
+
const pipeline = createDspPipeline();
|
|
190
|
+
pipeline.ZScoreNormalize({ mode: "moving", windowSize: 1 });
|
|
191
|
+
|
|
192
|
+
const input = new Float32Array([10, 20, 30]);
|
|
193
|
+
const output = await pipeline.process(input, {
|
|
194
|
+
sampleRate: 1000,
|
|
195
|
+
channels: 1,
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// With window size 1, stddev is always 0, so all values should be 0
|
|
199
|
+
output.forEach((val) => {
|
|
200
|
+
assert.ok(Math.abs(val) < 0.0001, `Expected 0, got ${val}`);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("should throw error for moving mode without window size", () => {
|
|
205
|
+
const pipeline = createDspPipeline();
|
|
206
|
+
|
|
207
|
+
assert.throws(() => {
|
|
208
|
+
pipeline.ZScoreNormalize({ mode: "moving" } as any);
|
|
209
|
+
}, /either windowSize or windowDuration must be specified/);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("should throw error for invalid window size", () => {
|
|
213
|
+
const pipeline = createDspPipeline();
|
|
214
|
+
|
|
215
|
+
assert.throws(() => {
|
|
216
|
+
pipeline.ZScoreNormalize({ mode: "moving", windowSize: 0 });
|
|
217
|
+
}, /windowSize must be a positive integer/);
|
|
218
|
+
|
|
219
|
+
assert.throws(() => {
|
|
220
|
+
pipeline.ZScoreNormalize({ mode: "moving", windowSize: -5 });
|
|
221
|
+
}, /windowSize must be a positive integer/);
|
|
222
|
+
|
|
223
|
+
assert.throws(() => {
|
|
224
|
+
pipeline.ZScoreNormalize({ mode: "moving", windowSize: 3.14 });
|
|
225
|
+
}, /windowSize must be a positive integer/);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("should process multi-channel data with independent state", async () => {
|
|
229
|
+
const pipeline = createDspPipeline();
|
|
230
|
+
pipeline.ZScoreNormalize({ mode: "moving", windowSize: 3 });
|
|
231
|
+
|
|
232
|
+
// 2 channels, 5 samples per channel (interleaved)
|
|
233
|
+
// Channel 0: [1, 2, 3, 4, 5]
|
|
234
|
+
// Channel 1: [10, 20, 30, 40, 50]
|
|
235
|
+
const input = new Float32Array([1, 10, 2, 20, 3, 30, 4, 40, 5, 50]);
|
|
236
|
+
const output = await pipeline.process(input, {
|
|
237
|
+
sampleRate: 1000,
|
|
238
|
+
channels: 2,
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// Each channel should have independent sliding window state
|
|
242
|
+
// Values should be normalized based on their own channel's statistics
|
|
243
|
+
assert.ok(output.length === 10);
|
|
244
|
+
|
|
245
|
+
// Extract channels for verification
|
|
246
|
+
const ch0 = [];
|
|
247
|
+
const ch1 = [];
|
|
248
|
+
for (let i = 0; i < output.length; i++) {
|
|
249
|
+
if (i % 2 === 0) ch0.push(output[i]);
|
|
250
|
+
else ch1.push(output[i]);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Both channels should show similar z-score patterns (since they have similar distributions)
|
|
254
|
+
// Last value in each channel (window of [3,4,5] or [30,40,50]) should be ~1.225
|
|
255
|
+
assert.ok(Math.abs(ch0[4] - 1.225) < 0.05);
|
|
256
|
+
assert.ok(Math.abs(ch1[4] - 1.225) < 0.05);
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
describe("State Management", () => {
|
|
261
|
+
test("should serialize and deserialize state correctly for moving mode", async () => {
|
|
262
|
+
const pipeline = createDspPipeline();
|
|
263
|
+
pipeline.ZScoreNormalize({ mode: "moving", windowSize: 5 });
|
|
264
|
+
|
|
265
|
+
// Process some data to build state
|
|
266
|
+
const input1 = new Float32Array([1, 2, 3, 4, 5]);
|
|
267
|
+
await pipeline.process(input1, { sampleRate: 1000, channels: 1 });
|
|
268
|
+
|
|
269
|
+
// Save state
|
|
270
|
+
const stateJson = await pipeline.saveState();
|
|
271
|
+
const state = JSON.parse(stateJson);
|
|
272
|
+
|
|
273
|
+
// Verify state structure
|
|
274
|
+
assert.strictEqual(state.stages.length, 1);
|
|
275
|
+
assert.strictEqual(state.stages[0].type, "zScoreNormalize");
|
|
276
|
+
assert.strictEqual(state.stages[0].state.mode, "moving");
|
|
277
|
+
assert.strictEqual(state.stages[0].state.windowSize, 5);
|
|
278
|
+
assert.strictEqual(state.stages[0].state.numChannels, 1);
|
|
279
|
+
|
|
280
|
+
// Verify buffer state
|
|
281
|
+
const channelState = state.stages[0].state.channels[0];
|
|
282
|
+
assert.ok(Array.isArray(channelState.buffer));
|
|
283
|
+
assert.strictEqual(channelState.buffer.length, 5);
|
|
284
|
+
assert.ok(typeof channelState.runningSum === "number");
|
|
285
|
+
assert.ok(typeof channelState.runningSumOfSquares === "number");
|
|
286
|
+
|
|
287
|
+
// Restore state in a new pipeline
|
|
288
|
+
const pipeline2 = createDspPipeline();
|
|
289
|
+
pipeline2.ZScoreNormalize({ mode: "moving", windowSize: 5 });
|
|
290
|
+
await pipeline2.loadState(stateJson);
|
|
291
|
+
|
|
292
|
+
// Continue processing
|
|
293
|
+
const input2 = new Float32Array([6, 7, 8]);
|
|
294
|
+
const output1 = await pipeline.process(input2.slice(), {
|
|
295
|
+
sampleRate: 1000,
|
|
296
|
+
channels: 1,
|
|
297
|
+
});
|
|
298
|
+
const output2 = await pipeline2.process(input2.slice(), {
|
|
299
|
+
sampleRate: 1000,
|
|
300
|
+
channels: 1,
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// Both outputs should be identical
|
|
304
|
+
for (let i = 0; i < output1.length; i++) {
|
|
305
|
+
assert.strictEqual(output1[i], output2[i]);
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test("should reset state correctly", async () => {
|
|
310
|
+
const pipeline = createDspPipeline();
|
|
311
|
+
pipeline.ZScoreNormalize({ mode: "moving", windowSize: 3 });
|
|
312
|
+
|
|
313
|
+
// Process some data
|
|
314
|
+
const input1 = new Float32Array([10, 20, 30, 40, 50]);
|
|
315
|
+
const output1 = await pipeline.process(input1, {
|
|
316
|
+
sampleRate: 1000,
|
|
317
|
+
channels: 1,
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// Reset state
|
|
321
|
+
pipeline.clearState();
|
|
322
|
+
|
|
323
|
+
// Process the same data again
|
|
324
|
+
const input2 = new Float32Array([10, 20, 30, 40, 50]);
|
|
325
|
+
const output2 = await pipeline.process(input2, {
|
|
326
|
+
sampleRate: 1000,
|
|
327
|
+
channels: 1,
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// First few samples should match (since state was reset)
|
|
331
|
+
assert.strictEqual(output1[0], output2[0]);
|
|
332
|
+
assert.strictEqual(output1[1], output2[1]);
|
|
333
|
+
assert.strictEqual(output1[2], output2[2]);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
test("should handle empty input", async () => {
|
|
337
|
+
const pipeline = createDspPipeline();
|
|
338
|
+
pipeline.ZScoreNormalize({ mode: "moving", windowSize: 3 });
|
|
339
|
+
|
|
340
|
+
const input = new Float32Array([]);
|
|
341
|
+
const output = await pipeline.process(input, {
|
|
342
|
+
sampleRate: 1000,
|
|
343
|
+
channels: 1,
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
assert.strictEqual(output.length, 0);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
test("should validate running sums on state load", async () => {
|
|
350
|
+
const pipeline1 = createDspPipeline();
|
|
351
|
+
pipeline1.ZScoreNormalize({ mode: "moving", windowSize: 3 });
|
|
352
|
+
|
|
353
|
+
const input = new Float32Array([1, 2, 3]);
|
|
354
|
+
await pipeline1.process(input, { sampleRate: 1000, channels: 1 });
|
|
355
|
+
|
|
356
|
+
const stateJson = await pipeline1.saveState();
|
|
357
|
+
const state = JSON.parse(stateJson);
|
|
358
|
+
|
|
359
|
+
// Corrupt the running sum
|
|
360
|
+
state.stages[0].state.channels[0].runningSum = 999;
|
|
361
|
+
|
|
362
|
+
const pipeline2 = createDspPipeline();
|
|
363
|
+
pipeline2.ZScoreNormalize({ mode: "moving", windowSize: 3 });
|
|
364
|
+
|
|
365
|
+
// Should throw validation error
|
|
366
|
+
await assert.rejects(
|
|
367
|
+
async () => await pipeline2.loadState(JSON.stringify(state)),
|
|
368
|
+
/Running sum validation failed/
|
|
369
|
+
);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
test("should validate window size on state load", async () => {
|
|
373
|
+
const pipeline1 = createDspPipeline();
|
|
374
|
+
pipeline1.ZScoreNormalize({ mode: "moving", windowSize: 5 });
|
|
375
|
+
|
|
376
|
+
const input = new Float32Array([1, 2, 3, 4, 5]);
|
|
377
|
+
await pipeline1.process(input, { sampleRate: 1000, channels: 1 });
|
|
378
|
+
|
|
379
|
+
const stateJson = await pipeline1.saveState();
|
|
380
|
+
const state = JSON.parse(stateJson);
|
|
381
|
+
|
|
382
|
+
// Corrupt the window size
|
|
383
|
+
state.stages[0].state.windowSize = 10;
|
|
384
|
+
|
|
385
|
+
const pipeline2 = createDspPipeline();
|
|
386
|
+
pipeline2.ZScoreNormalize({ mode: "moving", windowSize: 5 });
|
|
387
|
+
|
|
388
|
+
// Should throw validation error
|
|
389
|
+
await assert.rejects(
|
|
390
|
+
async () => await pipeline2.loadState(JSON.stringify(state)),
|
|
391
|
+
/Window size mismatch/
|
|
392
|
+
);
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
describe("Edge Cases", () => {
|
|
397
|
+
test("should handle single sample", async () => {
|
|
398
|
+
const pipeline = createDspPipeline();
|
|
399
|
+
pipeline.ZScoreNormalize({ mode: "batch" });
|
|
400
|
+
|
|
401
|
+
const input = new Float32Array([42]);
|
|
402
|
+
const output = await pipeline.process(input, {
|
|
403
|
+
sampleRate: 1000,
|
|
404
|
+
channels: 1,
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
// With single value, stddev is 0, so output should be 0
|
|
408
|
+
assert.ok(Math.abs(output[0]) < 0.0001);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
test("should handle very small values", async () => {
|
|
412
|
+
const pipeline = createDspPipeline();
|
|
413
|
+
pipeline.ZScoreNormalize({ mode: "batch" });
|
|
414
|
+
|
|
415
|
+
const input = new Float32Array([1e-10, 2e-10, 3e-10, 4e-10, 5e-10]);
|
|
416
|
+
const output = await pipeline.process(input, {
|
|
417
|
+
sampleRate: 1000,
|
|
418
|
+
channels: 1,
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
// Should still normalize correctly despite small values
|
|
422
|
+
const mean = output.reduce((sum, val) => sum + val, 0) / output.length;
|
|
423
|
+
assert.ok(Math.abs(mean) < 0.0001);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
test("should handle very large values", async () => {
|
|
427
|
+
const pipeline = createDspPipeline();
|
|
428
|
+
pipeline.ZScoreNormalize({ mode: "batch" });
|
|
429
|
+
|
|
430
|
+
const input = new Float32Array([1e6, 2e6, 3e6, 4e6, 5e6]);
|
|
431
|
+
const output = await pipeline.process(input, {
|
|
432
|
+
sampleRate: 1000,
|
|
433
|
+
channels: 1,
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
// Should normalize correctly despite large values
|
|
437
|
+
const mean = output.reduce((sum, val) => sum + val, 0) / output.length;
|
|
438
|
+
assert.ok(Math.abs(mean) < 0.0001);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
test("should respect custom epsilon value", async () => {
|
|
442
|
+
const pipeline = createDspPipeline();
|
|
443
|
+
pipeline.ZScoreNormalize({ mode: "batch", epsilon: 0.1 });
|
|
444
|
+
|
|
445
|
+
// Create signal with very small stddev
|
|
446
|
+
const input = new Float32Array([5.0, 5.001, 4.999, 5.0, 5.001]);
|
|
447
|
+
const output = await pipeline.process(input, {
|
|
448
|
+
sampleRate: 1000,
|
|
449
|
+
channels: 1,
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
// With epsilon=0.1, small stddev (~0.001) should be treated as ~0
|
|
453
|
+
// So all values should be close to 0
|
|
454
|
+
output.forEach((val) => {
|
|
455
|
+
assert.ok(Math.abs(val) < 0.1, `Expected small value, got ${val}`);
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
});
|
|
459
|
+
});
|