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.
Files changed (172) hide show
  1. package/.github/workflows/ci.yml +185 -0
  2. package/.vscode/c_cpp_properties.json +17 -0
  3. package/.vscode/settings.json +68 -0
  4. package/.vscode/tasks.json +28 -0
  5. package/DISCLAIMER.md +32 -0
  6. package/LICENSE +21 -0
  7. package/README.md +1803 -0
  8. package/ROADMAP.md +192 -0
  9. package/TECHNICAL_DEBT.md +165 -0
  10. package/binding.gyp +65 -0
  11. package/docs/ADVANCED_LOGGER_FEATURES.md +598 -0
  12. package/docs/AUTHENTICATION_SECURITY.md +396 -0
  13. package/docs/BACKEND_IMPROVEMENTS.md +399 -0
  14. package/docs/CHEBYSHEV_BIQUAD_EQ_IMPLEMENTATION.md +405 -0
  15. package/docs/FFT_IMPLEMENTATION.md +490 -0
  16. package/docs/FFT_IMPROVEMENTS_SUMMARY.md +387 -0
  17. package/docs/FFT_USER_GUIDE.md +494 -0
  18. package/docs/FILTERS_IMPLEMENTATION.md +260 -0
  19. package/docs/FILTER_API_GUIDE.md +418 -0
  20. package/docs/FIR_SIMD_OPTIMIZATION.md +175 -0
  21. package/docs/LOGGER_API_REFERENCE.md +350 -0
  22. package/docs/NOTCH_FILTER_QUICK_REF.md +121 -0
  23. package/docs/PHASE2_TESTS_AND_NOTCH_FILTER.md +341 -0
  24. package/docs/PHASES_5_7_SUMMARY.md +403 -0
  25. package/docs/PIPELINE_FILTER_INTEGRATION.md +446 -0
  26. package/docs/SIMD_OPTIMIZATIONS.md +211 -0
  27. package/docs/TEST_MIGRATION_SUMMARY.md +173 -0
  28. package/docs/TIMESERIES_IMPLEMENTATION_SUMMARY.md +322 -0
  29. package/docs/TIMESERIES_QUICK_REF.md +85 -0
  30. package/docs/advanced.md +559 -0
  31. package/docs/time-series-guide.md +617 -0
  32. package/docs/time-series-migration.md +376 -0
  33. package/jest.config.js +37 -0
  34. package/package.json +42 -0
  35. package/prebuilds/linux-x64/dsp-ts-redis.node +0 -0
  36. package/prebuilds/win32-x64/dsp-ts-redis.node +0 -0
  37. package/scripts/test.js +24 -0
  38. package/src/build/dsp-ts-redis.node +0 -0
  39. package/src/native/DspPipeline.cc +675 -0
  40. package/src/native/DspPipeline.h +44 -0
  41. package/src/native/FftBindings.cc +817 -0
  42. package/src/native/FilterBindings.cc +1001 -0
  43. package/src/native/IDspStage.h +53 -0
  44. package/src/native/adapters/InterpolatorStage.h +201 -0
  45. package/src/native/adapters/MeanAbsoluteValueStage.h +289 -0
  46. package/src/native/adapters/MovingAverageStage.h +306 -0
  47. package/src/native/adapters/RectifyStage.h +88 -0
  48. package/src/native/adapters/ResamplerStage.h +238 -0
  49. package/src/native/adapters/RmsStage.h +299 -0
  50. package/src/native/adapters/SscStage.h +121 -0
  51. package/src/native/adapters/VarianceStage.h +307 -0
  52. package/src/native/adapters/WampStage.h +114 -0
  53. package/src/native/adapters/WaveformLengthStage.h +115 -0
  54. package/src/native/adapters/ZScoreNormalizeStage.h +326 -0
  55. package/src/native/core/FftEngine.cc +441 -0
  56. package/src/native/core/FftEngine.h +224 -0
  57. package/src/native/core/FirFilter.cc +324 -0
  58. package/src/native/core/FirFilter.h +149 -0
  59. package/src/native/core/IirFilter.cc +576 -0
  60. package/src/native/core/IirFilter.h +210 -0
  61. package/src/native/core/MovingAbsoluteValueFilter.cc +17 -0
  62. package/src/native/core/MovingAbsoluteValueFilter.h +135 -0
  63. package/src/native/core/MovingAverageFilter.cc +18 -0
  64. package/src/native/core/MovingAverageFilter.h +135 -0
  65. package/src/native/core/MovingFftFilter.cc +291 -0
  66. package/src/native/core/MovingFftFilter.h +203 -0
  67. package/src/native/core/MovingVarianceFilter.cc +194 -0
  68. package/src/native/core/MovingVarianceFilter.h +114 -0
  69. package/src/native/core/MovingZScoreFilter.cc +215 -0
  70. package/src/native/core/MovingZScoreFilter.h +113 -0
  71. package/src/native/core/Policies.h +352 -0
  72. package/src/native/core/RmsFilter.cc +18 -0
  73. package/src/native/core/RmsFilter.h +131 -0
  74. package/src/native/core/SscFilter.cc +16 -0
  75. package/src/native/core/SscFilter.h +137 -0
  76. package/src/native/core/WampFilter.cc +16 -0
  77. package/src/native/core/WampFilter.h +101 -0
  78. package/src/native/core/WaveformLengthFilter.cc +17 -0
  79. package/src/native/core/WaveformLengthFilter.h +98 -0
  80. package/src/native/utils/CircularBufferArray.cc +336 -0
  81. package/src/native/utils/CircularBufferArray.h +62 -0
  82. package/src/native/utils/CircularBufferVector.cc +145 -0
  83. package/src/native/utils/CircularBufferVector.h +45 -0
  84. package/src/native/utils/NapiUtils.cc +53 -0
  85. package/src/native/utils/NapiUtils.h +21 -0
  86. package/src/native/utils/SimdOps.h +870 -0
  87. package/src/native/utils/SlidingWindowFilter.cc +239 -0
  88. package/src/native/utils/SlidingWindowFilter.h +159 -0
  89. package/src/native/utils/TimeSeriesBuffer.cc +205 -0
  90. package/src/native/utils/TimeSeriesBuffer.h +140 -0
  91. package/src/ts/CircularLogBuffer.ts +87 -0
  92. package/src/ts/DriftDetector.ts +331 -0
  93. package/src/ts/TopicRouter.ts +428 -0
  94. package/src/ts/__tests__/AdvancedDsp.test.ts +585 -0
  95. package/src/ts/__tests__/AuthAndEdgeCases.test.ts +241 -0
  96. package/src/ts/__tests__/Chaining.test.ts +387 -0
  97. package/src/ts/__tests__/ChebyshevBiquad.test.ts +229 -0
  98. package/src/ts/__tests__/CircularLogBuffer.test.ts +158 -0
  99. package/src/ts/__tests__/DriftDetector.test.ts +389 -0
  100. package/src/ts/__tests__/Fft.test.ts +484 -0
  101. package/src/ts/__tests__/ListState.test.ts +153 -0
  102. package/src/ts/__tests__/Logger.test.ts +208 -0
  103. package/src/ts/__tests__/LoggerAdvanced.test.ts +319 -0
  104. package/src/ts/__tests__/LoggerMinor.test.ts +247 -0
  105. package/src/ts/__tests__/MeanAbsoluteValue.test.ts +398 -0
  106. package/src/ts/__tests__/MovingAverage.test.ts +322 -0
  107. package/src/ts/__tests__/RMS.test.ts +315 -0
  108. package/src/ts/__tests__/Rectify.test.ts +272 -0
  109. package/src/ts/__tests__/Redis.test.ts +456 -0
  110. package/src/ts/__tests__/SlopeSignChange.test.ts +166 -0
  111. package/src/ts/__tests__/Tap.test.ts +164 -0
  112. package/src/ts/__tests__/TimeBasedExpiration.test.ts +124 -0
  113. package/src/ts/__tests__/TimeBasedRmsAndMav.test.ts +231 -0
  114. package/src/ts/__tests__/TimeBasedVarianceAndZScore.test.ts +284 -0
  115. package/src/ts/__tests__/TimeSeries.test.ts +254 -0
  116. package/src/ts/__tests__/TopicRouter.test.ts +332 -0
  117. package/src/ts/__tests__/TopicRouterAdvanced.test.ts +483 -0
  118. package/src/ts/__tests__/TopicRouterPriority.test.ts +487 -0
  119. package/src/ts/__tests__/Variance.test.ts +509 -0
  120. package/src/ts/__tests__/WaveformLength.test.ts +147 -0
  121. package/src/ts/__tests__/WillisonAmplitude.test.ts +197 -0
  122. package/src/ts/__tests__/ZScoreNormalize.test.ts +459 -0
  123. package/src/ts/advanced-dsp.ts +566 -0
  124. package/src/ts/backends.ts +1137 -0
  125. package/src/ts/bindings.ts +1225 -0
  126. package/src/ts/easter-egg.ts +42 -0
  127. package/src/ts/examples/MeanAbsoluteValue/test-state.ts +99 -0
  128. package/src/ts/examples/MeanAbsoluteValue/test-streaming.ts +269 -0
  129. package/src/ts/examples/MovingAverage/test-state.ts +85 -0
  130. package/src/ts/examples/MovingAverage/test-streaming.ts +188 -0
  131. package/src/ts/examples/RMS/test-state.ts +97 -0
  132. package/src/ts/examples/RMS/test-streaming.ts +253 -0
  133. package/src/ts/examples/Rectify/test-state.ts +107 -0
  134. package/src/ts/examples/Rectify/test-streaming.ts +242 -0
  135. package/src/ts/examples/Variance/test-state.ts +195 -0
  136. package/src/ts/examples/Variance/test-streaming.ts +260 -0
  137. package/src/ts/examples/ZScoreNormalize/test-state.ts +277 -0
  138. package/src/ts/examples/ZScoreNormalize/test-streaming.ts +306 -0
  139. package/src/ts/examples/advanced-dsp-examples.ts +397 -0
  140. package/src/ts/examples/callbacks/advanced-router-features.ts +326 -0
  141. package/src/ts/examples/callbacks/benchmark-circular-buffer.ts +109 -0
  142. package/src/ts/examples/callbacks/monitoring-example.ts +265 -0
  143. package/src/ts/examples/callbacks/pipeline-callbacks-example.ts +137 -0
  144. package/src/ts/examples/callbacks/pooled-callbacks-example.ts +274 -0
  145. package/src/ts/examples/callbacks/priority-routing-example.ts +277 -0
  146. package/src/ts/examples/callbacks/production-topic-router.ts +214 -0
  147. package/src/ts/examples/callbacks/topic-based-logging.ts +161 -0
  148. package/src/ts/examples/chaining/test-chaining-redis.ts +113 -0
  149. package/src/ts/examples/chaining/test-chaining.ts +52 -0
  150. package/src/ts/examples/emg-features-example.ts +284 -0
  151. package/src/ts/examples/fft-example.ts +309 -0
  152. package/src/ts/examples/fft-examples.ts +349 -0
  153. package/src/ts/examples/filter-examples.ts +320 -0
  154. package/src/ts/examples/list-state-example.ts +131 -0
  155. package/src/ts/examples/logger-example.ts +91 -0
  156. package/src/ts/examples/notch-filter-examples.ts +243 -0
  157. package/src/ts/examples/phase5/drift-detection-example.ts +290 -0
  158. package/src/ts/examples/phase6-7/production-observability.ts +476 -0
  159. package/src/ts/examples/phase6-7/redis-timeseries-integration.ts +446 -0
  160. package/src/ts/examples/redis/redis-example.ts +202 -0
  161. package/src/ts/examples/redis-example.ts +202 -0
  162. package/src/ts/examples/simd-benchmark.ts +126 -0
  163. package/src/ts/examples/tap-debugging.ts +230 -0
  164. package/src/ts/examples/timeseries/comparison-example.ts +290 -0
  165. package/src/ts/examples/timeseries/iot-sensor-example.ts +143 -0
  166. package/src/ts/examples/timeseries/redis-streaming-example.ts +233 -0
  167. package/src/ts/examples/waveform-length-example.ts +139 -0
  168. package/src/ts/fft.ts +722 -0
  169. package/src/ts/filters.ts +1078 -0
  170. package/src/ts/index.ts +120 -0
  171. package/src/ts/types.ts +589 -0
  172. 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
+ });