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,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for minor enhancements: sampling, dynamic levels, retry stats, formatter, traceparent
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { test } from "node:test";
|
|
6
|
+
import assert from "node:assert";
|
|
7
|
+
import {
|
|
8
|
+
Logger,
|
|
9
|
+
createMockHandler,
|
|
10
|
+
JSONFormatter,
|
|
11
|
+
TextFormatter,
|
|
12
|
+
generateTraceparent,
|
|
13
|
+
type LogEntry,
|
|
14
|
+
} from "../index.js";
|
|
15
|
+
|
|
16
|
+
test("Sampling - logs sampled at configured rate", async () => {
|
|
17
|
+
const mock = createMockHandler();
|
|
18
|
+
const logger = new Logger([mock.handler], {
|
|
19
|
+
sampling: {
|
|
20
|
+
trace: 0, // Never log trace
|
|
21
|
+
debug: 1, // Always log debug
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Log 100 trace messages - should all be dropped
|
|
26
|
+
for (let i = 0; i < 100; i++) {
|
|
27
|
+
await logger.trace("Trace message");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Log 10 debug messages - should all pass
|
|
31
|
+
for (let i = 0; i < 10; i++) {
|
|
32
|
+
await logger.debug("Debug message");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const logs = mock.getLogs();
|
|
36
|
+
assert.strictEqual(logs.length, 10, "Only debug messages should pass");
|
|
37
|
+
assert.ok(logs.every((l) => l.level === "debug"));
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("Dynamic level control - setMinLevel filters logs", async () => {
|
|
41
|
+
const mock = createMockHandler();
|
|
42
|
+
const logger = new Logger([mock.handler], { minLevel: "debug" });
|
|
43
|
+
|
|
44
|
+
await logger.trace("Should be filtered");
|
|
45
|
+
await logger.debug("Should pass");
|
|
46
|
+
await logger.info("Should pass");
|
|
47
|
+
|
|
48
|
+
assert.strictEqual(mock.getLogs().length, 2);
|
|
49
|
+
|
|
50
|
+
// Change to info level
|
|
51
|
+
logger.setMinLevel("info");
|
|
52
|
+
mock.clear();
|
|
53
|
+
|
|
54
|
+
await logger.trace("Should be filtered");
|
|
55
|
+
await logger.debug("Should be filtered");
|
|
56
|
+
await logger.info("Should pass");
|
|
57
|
+
await logger.warn("Should pass");
|
|
58
|
+
|
|
59
|
+
assert.strictEqual(mock.getLogs().length, 2);
|
|
60
|
+
assert.strictEqual(logger.getMinLevel(), "info");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("Dynamic level control - getMinLevel returns current level", () => {
|
|
64
|
+
const logger = new Logger([], { minLevel: "warn" });
|
|
65
|
+
assert.strictEqual(logger.getMinLevel(), "warn");
|
|
66
|
+
|
|
67
|
+
logger.setMinLevel("error");
|
|
68
|
+
assert.strictEqual(logger.getMinLevel(), "error");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("Formatter - JSONFormatter formats as object", () => {
|
|
72
|
+
const formatter = new JSONFormatter();
|
|
73
|
+
const log: LogEntry = {
|
|
74
|
+
level: "info",
|
|
75
|
+
message: "Test",
|
|
76
|
+
timestamp: Date.now(),
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const result = formatter.format(log);
|
|
80
|
+
assert.deepStrictEqual(result, log);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("Formatter - TextFormatter formats as string", () => {
|
|
84
|
+
const formatter = new TextFormatter();
|
|
85
|
+
const log: LogEntry = {
|
|
86
|
+
level: "info",
|
|
87
|
+
message: "Test message",
|
|
88
|
+
topic: "test.topic",
|
|
89
|
+
timestamp: Date.now(),
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const result = formatter.format(log);
|
|
93
|
+
assert.strictEqual(typeof result, "string");
|
|
94
|
+
assert.ok(result.includes("INFO"));
|
|
95
|
+
assert.ok(result.includes("test.topic"));
|
|
96
|
+
assert.ok(result.includes("Test message"));
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("Formatter - TextFormatter includes trace ID", () => {
|
|
100
|
+
const formatter = new TextFormatter();
|
|
101
|
+
const log: LogEntry = {
|
|
102
|
+
level: "info",
|
|
103
|
+
message: "Test",
|
|
104
|
+
topic: "test",
|
|
105
|
+
timestamp: Date.now(),
|
|
106
|
+
traceId: "abc123def456",
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const result = formatter.format(log);
|
|
110
|
+
assert.ok(result.includes("[trace:abc123de]"));
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("Formatter - custom formatter in Logger", async () => {
|
|
114
|
+
const formattedLogs: string[] = [];
|
|
115
|
+
const customHandler = (log: any) => {
|
|
116
|
+
formattedLogs.push(log);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const logger = new Logger([customHandler], {
|
|
120
|
+
formatter: new TextFormatter(),
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
await logger.info("Test message");
|
|
124
|
+
|
|
125
|
+
assert.strictEqual(formattedLogs.length, 1);
|
|
126
|
+
assert.strictEqual(typeof formattedLogs[0], "string");
|
|
127
|
+
assert.ok(formattedLogs[0].includes("Test message"));
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("Traceparent - generates valid W3C format", () => {
|
|
131
|
+
const traceId = "0af7651916cd43dd8448eb211c80319c";
|
|
132
|
+
const spanId = "b7ad6b7169203331";
|
|
133
|
+
|
|
134
|
+
const result = generateTraceparent(traceId, spanId);
|
|
135
|
+
assert.strictEqual(
|
|
136
|
+
result,
|
|
137
|
+
"00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("Traceparent - returns undefined for missing IDs", () => {
|
|
142
|
+
assert.strictEqual(generateTraceparent(undefined, "span123"), undefined);
|
|
143
|
+
assert.strictEqual(generateTraceparent("trace123", undefined), undefined);
|
|
144
|
+
assert.strictEqual(generateTraceparent(undefined, undefined), undefined);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("Auto-shutdown - registers signal handlers", () => {
|
|
148
|
+
// Note: Can't fully test without actually sending signals
|
|
149
|
+
// This just verifies construction doesn't throw
|
|
150
|
+
const logger = new Logger([], { autoShutdown: true });
|
|
151
|
+
assert.ok(logger);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("Child logger - preserves sampling config", async () => {
|
|
155
|
+
const mock = createMockHandler();
|
|
156
|
+
const parentLogger = new Logger([mock.handler], {
|
|
157
|
+
sampling: { trace: 0 },
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const childLogger = parentLogger.child("child");
|
|
161
|
+
|
|
162
|
+
await childLogger.trace("Should be filtered by sampling");
|
|
163
|
+
await childLogger.info("Should pass");
|
|
164
|
+
|
|
165
|
+
const logs = mock.getLogs();
|
|
166
|
+
assert.strictEqual(logs.length, 1);
|
|
167
|
+
assert.strictEqual(logs[0].level, "info");
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("Child logger - preserves min level", async () => {
|
|
171
|
+
const mock = createMockHandler();
|
|
172
|
+
const parentLogger = new Logger([mock.handler], { minLevel: "warn" });
|
|
173
|
+
|
|
174
|
+
const childLogger = parentLogger.child("child");
|
|
175
|
+
|
|
176
|
+
await childLogger.debug("Should be filtered");
|
|
177
|
+
await childLogger.warn("Should pass");
|
|
178
|
+
|
|
179
|
+
const logs = mock.getLogs();
|
|
180
|
+
assert.strictEqual(logs.length, 1);
|
|
181
|
+
assert.strictEqual(logs[0].level, "warn");
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("Child logger - preserves formatter", async () => {
|
|
185
|
+
const formattedLogs: any[] = [];
|
|
186
|
+
const customHandler = (log: any) => {
|
|
187
|
+
formattedLogs.push(log);
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const parentLogger = new Logger([customHandler], {
|
|
191
|
+
formatter: new TextFormatter(),
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const childLogger = parentLogger.child("child");
|
|
195
|
+
|
|
196
|
+
await childLogger.info("Test");
|
|
197
|
+
|
|
198
|
+
// Child inherits formatter from parent, so output should be formatted
|
|
199
|
+
assert.strictEqual(formattedLogs.length, 1);
|
|
200
|
+
assert.strictEqual(typeof formattedLogs[0], "string");
|
|
201
|
+
assert.ok(formattedLogs[0].includes("child.default")); // Topic prefix added
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("Metrics - incrementRetries can be called", () => {
|
|
205
|
+
const logger = new Logger([], { enableMetrics: true });
|
|
206
|
+
|
|
207
|
+
logger.incrementRetries();
|
|
208
|
+
logger.incrementRetries();
|
|
209
|
+
logger.incrementRetries();
|
|
210
|
+
|
|
211
|
+
const metrics = logger.getMetrics();
|
|
212
|
+
assert.strictEqual(metrics.totalRetries, 3);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("Sampling - info level without sampling config passes through", async () => {
|
|
216
|
+
const mock = createMockHandler();
|
|
217
|
+
const logger = new Logger([mock.handler], {
|
|
218
|
+
sampling: { trace: 0.1 }, // Only sample trace
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
await logger.info("Should always pass");
|
|
222
|
+
await logger.info("Should always pass");
|
|
223
|
+
|
|
224
|
+
const logs = mock.getLogs();
|
|
225
|
+
assert.strictEqual(logs.length, 2);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("Min level - defaults to trace when not specified", async () => {
|
|
229
|
+
const mock = createMockHandler();
|
|
230
|
+
const logger = new Logger([mock.handler]);
|
|
231
|
+
|
|
232
|
+
await logger.trace("Should pass");
|
|
233
|
+
assert.strictEqual(mock.getLogs().length, 1);
|
|
234
|
+
assert.strictEqual(logger.getMinLevel(), "trace");
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test("Min level - fatal is highest priority", async () => {
|
|
238
|
+
const mock = createMockHandler();
|
|
239
|
+
const logger = new Logger([mock.handler], { minLevel: "fatal" });
|
|
240
|
+
|
|
241
|
+
await logger.error("Should be filtered");
|
|
242
|
+
await logger.fatal("Should pass");
|
|
243
|
+
|
|
244
|
+
const logs = mock.getLogs();
|
|
245
|
+
assert.strictEqual(logs.length, 1);
|
|
246
|
+
assert.strictEqual(logs[0].level, "fatal");
|
|
247
|
+
});
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import { describe, test, beforeEach } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { createDspPipeline, DspProcessor } from "../bindings.js";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_OPTIONS = { channels: 1, sampleRate: 44100 };
|
|
6
|
+
|
|
7
|
+
function assertCloseTo(actual: number, expected: number, precision = 5) {
|
|
8
|
+
const tolerance = Math.pow(10, -precision);
|
|
9
|
+
assert.ok(
|
|
10
|
+
Math.abs(actual - expected) < tolerance,
|
|
11
|
+
`Expected ${actual} to be close to ${expected} (tolerance: ${tolerance})`
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe("Mean Absolute Value Filter", () => {
|
|
16
|
+
let processor: DspProcessor;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
processor = createDspPipeline();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe("Basic Functionality", () => {
|
|
23
|
+
test("should compute MAV with window size 3", async () => {
|
|
24
|
+
processor.MeanAbsoluteValue({ mode: "moving", windowSize: 3 });
|
|
25
|
+
|
|
26
|
+
const input = new Float32Array([1, -2, 3, -4, 5]);
|
|
27
|
+
const output = await processor.process(input, DEFAULT_OPTIONS);
|
|
28
|
+
|
|
29
|
+
// First value: |1| = 1
|
|
30
|
+
// Second value: (|1| + |-2|) / 2 = 1.5
|
|
31
|
+
// Third value: (|1| + |-2| + |3|) / 3 = 2.0
|
|
32
|
+
// Fourth value: (|-2| + |3| + |-4|) / 3 = 3.0
|
|
33
|
+
// Fifth value: (|3| + |-4| + |5|) / 3 = 4.0
|
|
34
|
+
assert.equal(output.length, 5);
|
|
35
|
+
assertCloseTo(output[0], 1.0);
|
|
36
|
+
assertCloseTo(output[1], 1.5);
|
|
37
|
+
assertCloseTo(output[2], 2.0);
|
|
38
|
+
assertCloseTo(output[3], 3.0);
|
|
39
|
+
assertCloseTo(output[4], 4.0);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("should handle single sample window", async () => {
|
|
43
|
+
processor.MeanAbsoluteValue({ mode: "moving", windowSize: 1 });
|
|
44
|
+
|
|
45
|
+
const input = new Float32Array([3, -4, 5]);
|
|
46
|
+
const output = await processor.process(input, DEFAULT_OPTIONS);
|
|
47
|
+
|
|
48
|
+
// MAV of single sample is just the absolute value
|
|
49
|
+
assertCloseTo(output[0], 3);
|
|
50
|
+
assertCloseTo(output[1], 4);
|
|
51
|
+
assertCloseTo(output[2], 5);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("should compute MAV correctly for all negative values", async () => {
|
|
55
|
+
processor.MeanAbsoluteValue({ mode: "moving", windowSize: 2 });
|
|
56
|
+
|
|
57
|
+
const input = new Float32Array([-3, -4, -5, -12]);
|
|
58
|
+
const output = await processor.process(input, DEFAULT_OPTIONS);
|
|
59
|
+
|
|
60
|
+
// First: |-3| = 3
|
|
61
|
+
// Second: (|-3| + |-4|) / 2 = 3.5
|
|
62
|
+
// Third: (|-4| + |-5|) / 2 = 4.5
|
|
63
|
+
// Fourth: (|-5| + |-12|) / 2 = 8.5
|
|
64
|
+
assertCloseTo(output[0], 3.0);
|
|
65
|
+
assertCloseTo(output[1], 3.5);
|
|
66
|
+
assertCloseTo(output[2], 4.5);
|
|
67
|
+
assertCloseTo(output[3], 8.5);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("should maintain state across multiple process calls", async () => {
|
|
71
|
+
processor.MeanAbsoluteValue({ mode: "moving", windowSize: 3 });
|
|
72
|
+
|
|
73
|
+
// First batch: [3, -4]
|
|
74
|
+
const output1 = await processor.process(
|
|
75
|
+
new Float32Array([3, -4]),
|
|
76
|
+
DEFAULT_OPTIONS
|
|
77
|
+
);
|
|
78
|
+
assertCloseTo(output1[0], 3);
|
|
79
|
+
assertCloseTo(output1[1], 3.5);
|
|
80
|
+
|
|
81
|
+
// Second batch: [5] - should use [3, -4, 5] for MAV
|
|
82
|
+
const output2 = await processor.process(
|
|
83
|
+
new Float32Array([5]),
|
|
84
|
+
DEFAULT_OPTIONS
|
|
85
|
+
);
|
|
86
|
+
// (|3| + |-4| + |5|) / 3 = 4.0
|
|
87
|
+
assertCloseTo(output2[0], 4.0);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("should handle zero values correctly", async () => {
|
|
91
|
+
processor.MeanAbsoluteValue({ mode: "moving", windowSize: 3 });
|
|
92
|
+
|
|
93
|
+
const input = new Float32Array([0, 0, 0, 5, 0]);
|
|
94
|
+
const output = await processor.process(input, DEFAULT_OPTIONS);
|
|
95
|
+
|
|
96
|
+
assertCloseTo(output[0], 0);
|
|
97
|
+
assertCloseTo(output[1], 0);
|
|
98
|
+
assertCloseTo(output[2], 0);
|
|
99
|
+
assertCloseTo(output[3], 5 / 3);
|
|
100
|
+
assertCloseTo(output[4], 5 / 3);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe("State Management", () => {
|
|
105
|
+
test("should serialize and deserialize state correctly", async () => {
|
|
106
|
+
processor.MeanAbsoluteValue({ mode: "moving", windowSize: 3 });
|
|
107
|
+
|
|
108
|
+
// Build state
|
|
109
|
+
await processor.process(new Float32Array([1, -2, 3]), DEFAULT_OPTIONS);
|
|
110
|
+
|
|
111
|
+
const stateJson = await processor.saveState();
|
|
112
|
+
const state = JSON.parse(stateJson);
|
|
113
|
+
|
|
114
|
+
assert.ok(state);
|
|
115
|
+
assert.ok(state.timestamp);
|
|
116
|
+
assert.equal(state.stages.length, 1);
|
|
117
|
+
assert.equal(state.stages[0].type, "meanAbsoluteValue");
|
|
118
|
+
assert.equal(state.stages[0].state.windowSize, 3);
|
|
119
|
+
assert.ok(state.stages[0].state.channels);
|
|
120
|
+
|
|
121
|
+
// Create new processor and load state
|
|
122
|
+
const processor2 = createDspPipeline();
|
|
123
|
+
processor2.MeanAbsoluteValue({ mode: "moving", windowSize: 3 });
|
|
124
|
+
await processor2.loadState(stateJson);
|
|
125
|
+
|
|
126
|
+
// Both should produce same output for next sample
|
|
127
|
+
const output1 = await processor.process(
|
|
128
|
+
new Float32Array([-4, 5]),
|
|
129
|
+
DEFAULT_OPTIONS
|
|
130
|
+
);
|
|
131
|
+
const output2 = await processor2.process(
|
|
132
|
+
new Float32Array([-4, 5]),
|
|
133
|
+
DEFAULT_OPTIONS
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
assertCloseTo(output2[0], output1[0]);
|
|
137
|
+
assertCloseTo(output2[1], output1[1]);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("should reset state correctly", async () => {
|
|
141
|
+
processor.MeanAbsoluteValue({ mode: "moving", windowSize: 3 });
|
|
142
|
+
|
|
143
|
+
// Build state
|
|
144
|
+
await processor.process(
|
|
145
|
+
new Float32Array([1, -2, 3, -4]),
|
|
146
|
+
DEFAULT_OPTIONS
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
// Reset
|
|
150
|
+
processor.clearState();
|
|
151
|
+
|
|
152
|
+
// Should start fresh
|
|
153
|
+
const output = await processor.process(
|
|
154
|
+
new Float32Array([10]),
|
|
155
|
+
DEFAULT_OPTIONS
|
|
156
|
+
);
|
|
157
|
+
assertCloseTo(output[0], 10); // MAV of single value
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("should validate state on load", async () => {
|
|
161
|
+
processor.MeanAbsoluteValue({ mode: "moving", windowSize: 3 });
|
|
162
|
+
await processor.process(new Float32Array([1, -2, 3]), DEFAULT_OPTIONS);
|
|
163
|
+
|
|
164
|
+
const stateJson = await processor.saveState();
|
|
165
|
+
const state = JSON.parse(stateJson);
|
|
166
|
+
|
|
167
|
+
// Corrupt the buffer
|
|
168
|
+
if (state.stages[0].state.channels && state.stages[0].state.channels[0]) {
|
|
169
|
+
state.stages[0].state.channels[0].buffer = null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
await assert.rejects(
|
|
173
|
+
async () => processor.loadState(JSON.stringify(state)),
|
|
174
|
+
/array/i
|
|
175
|
+
);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe("Mathematical Properties", () => {
|
|
180
|
+
test("should always be non-negative", async () => {
|
|
181
|
+
processor.MeanAbsoluteValue({ mode: "moving", windowSize: 2 });
|
|
182
|
+
|
|
183
|
+
const input = new Float32Array([-10, -20, -30, -40]);
|
|
184
|
+
const output = await processor.process(input, DEFAULT_OPTIONS);
|
|
185
|
+
|
|
186
|
+
for (let i = 0; i < output.length; i++) {
|
|
187
|
+
assert.ok(
|
|
188
|
+
output[i] >= 0,
|
|
189
|
+
`Output at index ${i} should be non-negative`
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("should be scale-invariant", async () => {
|
|
195
|
+
const windowSize = 3;
|
|
196
|
+
const input = new Float32Array([2, 4, 6]);
|
|
197
|
+
|
|
198
|
+
// Test with original values
|
|
199
|
+
const processor1 = createDspPipeline();
|
|
200
|
+
processor1.MeanAbsoluteValue({ mode: "moving", windowSize });
|
|
201
|
+
const output1 = await processor1.process(input, DEFAULT_OPTIONS);
|
|
202
|
+
|
|
203
|
+
// Test with scaled values (multiply by 2)
|
|
204
|
+
const processor2 = createDspPipeline();
|
|
205
|
+
processor2.MeanAbsoluteValue({ mode: "moving", windowSize });
|
|
206
|
+
const scaledInput = new Float32Array([4, 8, 12]);
|
|
207
|
+
const output2 = await processor2.process(scaledInput, DEFAULT_OPTIONS);
|
|
208
|
+
|
|
209
|
+
// MAV should scale linearly
|
|
210
|
+
for (let i = 0; i < output1.length; i++) {
|
|
211
|
+
assertCloseTo(output2[i], output1[i] * 2);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("should be equal to mean for all positive values", async () => {
|
|
216
|
+
processor.MeanAbsoluteValue({ mode: "moving", windowSize: 3 });
|
|
217
|
+
|
|
218
|
+
const input = new Float32Array([2, 4, 6, 8]);
|
|
219
|
+
const output = await processor.process(input, DEFAULT_OPTIONS);
|
|
220
|
+
|
|
221
|
+
// For all positive values, MAV = mean
|
|
222
|
+
assertCloseTo(output[0], 2);
|
|
223
|
+
assertCloseTo(output[1], 3);
|
|
224
|
+
assertCloseTo(output[2], 4);
|
|
225
|
+
assertCloseTo(output[3], 6);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("should be bounded by max absolute value", async () => {
|
|
229
|
+
processor.MeanAbsoluteValue({ mode: "moving", windowSize: 3 });
|
|
230
|
+
|
|
231
|
+
const input = new Float32Array([1, -10, 2, -15, 3]);
|
|
232
|
+
const output = await processor.process(input, DEFAULT_OPTIONS);
|
|
233
|
+
|
|
234
|
+
for (let i = 0; i < output.length; i++) {
|
|
235
|
+
const windowStart = Math.max(0, i - 2);
|
|
236
|
+
const windowValues = input.slice(windowStart, i + 1);
|
|
237
|
+
const maxAbs = Math.max(...Array.from(windowValues).map(Math.abs));
|
|
238
|
+
assert.ok(
|
|
239
|
+
output[i] <= maxAbs,
|
|
240
|
+
`MAV at ${i} (${output[i]}) should be <= max absolute value (${maxAbs})`
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
describe("Edge Cases", () => {
|
|
247
|
+
test("should handle very large window sizes", async () => {
|
|
248
|
+
processor.MeanAbsoluteValue({ mode: "moving", windowSize: 1000 });
|
|
249
|
+
|
|
250
|
+
const input = new Float32Array([1, -2, 3, -4, 5]);
|
|
251
|
+
const output = await processor.process(input, DEFAULT_OPTIONS);
|
|
252
|
+
|
|
253
|
+
// With window larger than input, each sample averages all previous samples
|
|
254
|
+
assert.equal(output.length, 5);
|
|
255
|
+
assertCloseTo(output[0], 1);
|
|
256
|
+
assertCloseTo(output[1], 1.5);
|
|
257
|
+
assertCloseTo(output[2], 2.0);
|
|
258
|
+
assertCloseTo(output[3], 2.5);
|
|
259
|
+
assertCloseTo(output[4], 3.0);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test("should handle very small values", async () => {
|
|
263
|
+
processor.MeanAbsoluteValue({ mode: "moving", windowSize: 2 });
|
|
264
|
+
|
|
265
|
+
const input = new Float32Array([1e-10, -2e-10, 3e-10]);
|
|
266
|
+
const output = await processor.process(input, DEFAULT_OPTIONS);
|
|
267
|
+
|
|
268
|
+
assert.ok(output.every((v) => !isNaN(v) && isFinite(v)));
|
|
269
|
+
assertCloseTo(output[0], 1e-10);
|
|
270
|
+
assertCloseTo(output[1], 1.5e-10);
|
|
271
|
+
assertCloseTo(output[2], 2.5e-10);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test("should handle empty input", async () => {
|
|
275
|
+
processor.MeanAbsoluteValue({ mode: "moving", windowSize: 3 });
|
|
276
|
+
|
|
277
|
+
const input = new Float32Array([]);
|
|
278
|
+
const output = await processor.process(input, DEFAULT_OPTIONS);
|
|
279
|
+
|
|
280
|
+
assert.equal(output.length, 0);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test("should handle single sample input", async () => {
|
|
284
|
+
processor.MeanAbsoluteValue({ mode: "moving", windowSize: 5 });
|
|
285
|
+
|
|
286
|
+
const input = new Float32Array([-7]);
|
|
287
|
+
const output = await processor.process(input, DEFAULT_OPTIONS);
|
|
288
|
+
|
|
289
|
+
assert.equal(output.length, 1);
|
|
290
|
+
assertCloseTo(output[0], 7);
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
describe("Multi-channel Processing", () => {
|
|
295
|
+
test("should process multiple channels independently", async () => {
|
|
296
|
+
processor.MeanAbsoluteValue({ mode: "moving", windowSize: 2 });
|
|
297
|
+
|
|
298
|
+
// Interleaved: [L1, R1, L2, R2, L3, R3]
|
|
299
|
+
const input = new Float32Array([1, -10, -2, 20, 3, -30]);
|
|
300
|
+
const output = await processor.process(input, {
|
|
301
|
+
channels: 2,
|
|
302
|
+
sampleRate: 44100,
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// Channel 0 (L): 1, -2, 3
|
|
306
|
+
// Channel 1 (R): -10, 20, -30
|
|
307
|
+
assert.equal(output.length, 6);
|
|
308
|
+
|
|
309
|
+
// Left channel
|
|
310
|
+
assertCloseTo(output[0], 1); // |1|
|
|
311
|
+
assertCloseTo(output[2], 1.5); // (|1| + |-2|) / 2
|
|
312
|
+
assertCloseTo(output[4], 2.5); // (|-2| + |3|) / 2
|
|
313
|
+
|
|
314
|
+
// Right channel
|
|
315
|
+
assertCloseTo(output[1], 10); // |-10|
|
|
316
|
+
assertCloseTo(output[3], 15); // (|-10| + |20|) / 2
|
|
317
|
+
assertCloseTo(output[5], 25); // (|20| + |-30|) / 2
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test("should maintain separate state per channel", async () => {
|
|
321
|
+
processor.MeanAbsoluteValue({ mode: "moving", windowSize: 3 });
|
|
322
|
+
|
|
323
|
+
// First batch
|
|
324
|
+
await processor.process(new Float32Array([1, -10, -2, 20]), {
|
|
325
|
+
channels: 2,
|
|
326
|
+
sampleRate: 44100,
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// Second batch - should maintain separate windows
|
|
330
|
+
const output = await processor.process(new Float32Array([3, -30]), {
|
|
331
|
+
channels: 2,
|
|
332
|
+
sampleRate: 44100,
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// Left: (|1| + |-2| + |3|) / 3
|
|
336
|
+
// Right: (|-10| + |20| + |-30|) / 3
|
|
337
|
+
assertCloseTo(output[0], 2.0);
|
|
338
|
+
assertCloseTo(output[1], 20.0);
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
describe("Error Handling", () => {
|
|
343
|
+
test("should require window size parameter", async () => {
|
|
344
|
+
assert.throws(() => {
|
|
345
|
+
processor.MeanAbsoluteValue({ mode: "moving" });
|
|
346
|
+
}, /windowSize/i);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
test("should reject invalid window size", async () => {
|
|
350
|
+
assert.throws(() => {
|
|
351
|
+
processor.MeanAbsoluteValue({ mode: "moving", windowSize: 0 });
|
|
352
|
+
}, /windowSize/i);
|
|
353
|
+
|
|
354
|
+
assert.throws(() => {
|
|
355
|
+
processor.MeanAbsoluteValue({ mode: "moving", windowSize: -1 });
|
|
356
|
+
}, /windowSize/i);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
test("should reject non-integer window size", async () => {
|
|
360
|
+
assert.throws(() => {
|
|
361
|
+
processor.MeanAbsoluteValue({ mode: "moving", windowSize: 2.5 });
|
|
362
|
+
}, /integer/i);
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
describe("Batch Mode", () => {
|
|
367
|
+
test("should compute MAV over entire input in batch mode", async () => {
|
|
368
|
+
processor.MeanAbsoluteValue({ mode: "batch" });
|
|
369
|
+
|
|
370
|
+
const input = new Float32Array([1, -2, 3, -4, 5]);
|
|
371
|
+
const output = await processor.process(input, DEFAULT_OPTIONS);
|
|
372
|
+
|
|
373
|
+
// All outputs should be the same: (|1| + |-2| + |3| + |-4| + |5|) / 5 = 3.0
|
|
374
|
+
assert.equal(output.length, 5);
|
|
375
|
+
for (let i = 0; i < output.length; i++) {
|
|
376
|
+
assertCloseTo(output[i], 3.0);
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
test("should recompute each time in batch mode", async () => {
|
|
381
|
+
processor.MeanAbsoluteValue({ mode: "batch" });
|
|
382
|
+
|
|
383
|
+
// First batch
|
|
384
|
+
const output1 = await processor.process(
|
|
385
|
+
new Float32Array([2, -4, 6]),
|
|
386
|
+
DEFAULT_OPTIONS
|
|
387
|
+
);
|
|
388
|
+
assertCloseTo(output1[0], 4.0); // (2 + 4 + 6) / 3
|
|
389
|
+
|
|
390
|
+
// Second batch - should not use state from first
|
|
391
|
+
const output2 = await processor.process(
|
|
392
|
+
new Float32Array([10, -10]),
|
|
393
|
+
DEFAULT_OPTIONS
|
|
394
|
+
);
|
|
395
|
+
assertCloseTo(output2[0], 10.0); // (10 + 10) / 2
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
});
|