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,1225 @@
1
+ import { createRequire } from "node:module";
2
+ import { fileURLToPath } from "node:url";
3
+ import { dirname, join } from "node:path";
4
+ import type {
5
+ ProcessOptions,
6
+ RedisConfig,
7
+ MovingAverageParams,
8
+ RmsParams,
9
+ RectifyParams,
10
+ VarianceParams,
11
+ ZScoreNormalizeParams,
12
+ MeanAbsoluteValueParams,
13
+ WaveformLengthParams,
14
+ SlopeSignChangeParams,
15
+ WillisonAmplitudeParams,
16
+ PipelineCallbacks,
17
+ LogEntry,
18
+ SampleBatch,
19
+ TapCallback,
20
+ PipelineStateSummary,
21
+ } from "./types.js";
22
+ import { CircularLogBuffer } from "./CircularLogBuffer.js";
23
+ import { DriftDetector } from "./DriftDetector.js";
24
+ import {
25
+ FirFilter,
26
+ IirFilter,
27
+ type FilterOptions,
28
+ type FilterType,
29
+ type FilterMode,
30
+ } from "./filters.js";
31
+
32
+ const __filename = fileURLToPath(import.meta.url);
33
+ const __dirname = dirname(__filename);
34
+ const require = createRequire(import.meta.url);
35
+
36
+ // Try multiple paths to find the native module
37
+ let DspAddon: any;
38
+ const possiblePaths = [
39
+ join(__dirname, "../build/dspx.node"),
40
+ join(__dirname, "../../build/Release/dspx.node"),
41
+ join(process.cwd(), "build/Release/dspx.node"),
42
+ join(process.cwd(), "src/build/dspx.node"),
43
+ ];
44
+
45
+ const errors: Array<{ path: string; error: string }> = [];
46
+
47
+ for (const path of possiblePaths) {
48
+ try {
49
+ DspAddon = require(path);
50
+ break;
51
+ } catch (err: any) {
52
+ errors.push({ path, error: err.message });
53
+ // Try next path
54
+ }
55
+ }
56
+
57
+ if (!DspAddon) {
58
+ console.error("❌ Failed to load native module. Tried paths:");
59
+ errors.forEach(({ path, error }) => {
60
+ console.error(` - ${path}`);
61
+ console.error(` Error: ${error}`);
62
+ });
63
+ throw new Error(
64
+ `Could not load native module. Tried ${possiblePaths.length} paths.`
65
+ );
66
+ }
67
+
68
+ /**
69
+ * DSP Processor class that wraps the native C++ DspPipeline
70
+ * Provides a fluent API for building and processing DSP pipelines
71
+ */
72
+ class DspProcessor {
73
+ private stages: string[] = [];
74
+ private callbacks?: PipelineCallbacks;
75
+ private logBuffer: CircularLogBuffer;
76
+ private tapCallbacks: Array<{ stageName: string; callback: TapCallback }> =
77
+ [];
78
+ private driftDetector: DriftDetector | null = null;
79
+
80
+ constructor(private nativeInstance: any) {
81
+ // Initialize circular buffer with capacity for typical log volume
82
+ // (2-3 logs per process call, supports bursts up to 32)
83
+ this.logBuffer = new CircularLogBuffer(32);
84
+ }
85
+
86
+ /**
87
+ * Generate a Kafka-style topic for a log entry
88
+ */
89
+ private generateLogTopic(
90
+ level: "debug" | "info" | "warn" | "error",
91
+ context?: any
92
+ ): string {
93
+ const stage = context?.stage;
94
+ const category = context?.category || level;
95
+
96
+ if (stage) {
97
+ // Stage-specific topic: pipeline.stage.<stageName>.<category>
98
+ return `pipeline.stage.${stage}.${category}`;
99
+ } else {
100
+ // General topic: pipeline.<level>
101
+ return `pipeline.${level}`;
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Check if a topic matches the configured topic filter
107
+ */
108
+ private matchesTopicFilter(topic: string): boolean {
109
+ const filter = this.callbacks?.topicFilter;
110
+ if (!filter) {
111
+ return true; // No filter, accept all
112
+ }
113
+
114
+ const filters = Array.isArray(filter) ? filter : [filter];
115
+
116
+ for (const pattern of filters) {
117
+ // Convert wildcard pattern to regex
118
+ // pipeline.stage.* -> ^pipeline\.stage\.[^.]+$
119
+ // pipeline.*.error -> ^pipeline\.[^.]+\.error$
120
+ const regexPattern = pattern
121
+ .replace(/\./g, "\\.")
122
+ .replace(/\*/g, "[^.]+");
123
+ const regex = new RegExp(`^${regexPattern}$`);
124
+
125
+ if (regex.test(topic)) {
126
+ return true;
127
+ }
128
+ }
129
+
130
+ return false;
131
+ }
132
+
133
+ /**
134
+ * Add a log entry to the circular buffer for batched processing
135
+ */
136
+ /**
137
+ * Map log level to default priority
138
+ * debug: 2, info: 5, warn: 7, error: 9
139
+ */
140
+ private getDefaultPriority(
141
+ level: "debug" | "info" | "warn" | "error"
142
+ ): 2 | 5 | 7 | 9 {
143
+ switch (level) {
144
+ case "debug":
145
+ return 2;
146
+ case "info":
147
+ return 5;
148
+ case "warn":
149
+ return 7;
150
+ case "error":
151
+ return 9;
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Pool a log entry in the circular buffer for batch delivery
157
+ */
158
+ private poolLog(
159
+ level: "debug" | "info" | "warn" | "error",
160
+ message: string,
161
+ context?: any
162
+ ): void {
163
+ const topic = this.generateLogTopic(level, context);
164
+
165
+ // If onLogBatch is configured, pool the log in circular buffer (with topic filtering)
166
+ if (this.callbacks?.onLogBatch && this.matchesTopicFilter(topic)) {
167
+ this.logBuffer.push({
168
+ topic,
169
+ level,
170
+ message,
171
+ context,
172
+ timestamp: performance.now(),
173
+ priority: this.getDefaultPriority(level),
174
+ });
175
+ }
176
+
177
+ // If onLog is also configured, call it immediately (backwards compatible, with topic filtering)
178
+ if (this.callbacks?.onLog && this.matchesTopicFilter(topic)) {
179
+ this.callbacks.onLog(topic, level, message, context);
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Flush all pooled logs from circular buffer to the onLogBatch callback
185
+ */
186
+ private flushLogs(): void {
187
+ if (this.callbacks?.onLogBatch && this.logBuffer.hasEntries()) {
188
+ const logs = this.logBuffer.flush();
189
+ this.callbacks.onLogBatch(logs);
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Add a moving average filter stage to the pipeline
195
+ * @param params - Configuration for the moving average filter
196
+ * @param params.mode - "batch" for stateless averaging (all samples → single average), "moving" for windowed averaging
197
+ * @param params.windowSize - Required for "moving" mode when using sample-based processing
198
+ * @param params.windowDuration - Required for "moving" mode when using time-based processing (milliseconds)
199
+ * @returns this instance for method chaining
200
+ *
201
+ * @example
202
+ * // Batch mode (stateless)
203
+ * pipeline.MovingAverage({ mode: "batch" });
204
+ *
205
+ * @example
206
+ * // Moving mode with sample window (legacy)
207
+ * pipeline.MovingAverage({ mode: "moving", windowSize: 10 });
208
+ *
209
+ * @example
210
+ * // Moving mode with time window (recommended)
211
+ * pipeline.MovingAverage({ mode: "moving", windowDuration: 5000 }); // 5 seconds
212
+ */
213
+ MovingAverage(params: MovingAverageParams): this {
214
+ if (params.mode === "moving") {
215
+ if (
216
+ params.windowSize === undefined &&
217
+ params.windowDuration === undefined
218
+ ) {
219
+ throw new TypeError(
220
+ `MovingAverage: either windowSize or windowDuration must be specified for "moving" mode`
221
+ );
222
+ }
223
+ if (
224
+ params.windowSize !== undefined &&
225
+ (params.windowSize <= 0 || !Number.isInteger(params.windowSize))
226
+ ) {
227
+ throw new TypeError(
228
+ `MovingAverage: windowSize must be a positive integer for "moving" mode, got ${params.windowSize}`
229
+ );
230
+ }
231
+ if (params.windowDuration !== undefined && params.windowDuration <= 0) {
232
+ throw new TypeError(
233
+ `MovingAverage: windowDuration must be positive, got ${params.windowDuration}`
234
+ );
235
+ }
236
+ }
237
+ this.nativeInstance.addStage("movingAverage", params);
238
+ this.stages.push(`movingAverage:${params.mode}`);
239
+ return this;
240
+ }
241
+
242
+ /**
243
+ * Add a RMS (root mean square) stage to the pipeline
244
+ * @param params - Configuration for the RMS filter
245
+ * @param params.mode - "batch" for stateless RMS (all samples → single RMS), "moving" for windowed RMS
246
+ * @param params.windowSize - Required for "moving" mode when using sample-based processing
247
+ * @param params.windowDuration - Required for "moving" mode when using time-based processing (milliseconds)
248
+ * @returns this instance for method chaining
249
+ *
250
+ * @example
251
+ * // Batch mode (stateless)
252
+ * pipeline.Rms({ mode: "batch" });
253
+ *
254
+ * @example
255
+ * // Moving mode with sample window (legacy)
256
+ * pipeline.Rms({ mode: "moving", windowSize: 50 });
257
+ *
258
+ * @example
259
+ * // Moving mode with time window (recommended)
260
+ * pipeline.Rms({ mode: "moving", windowDuration: 10000 }); // 10 seconds
261
+ */
262
+ Rms(params: RmsParams): this {
263
+ if (params.mode === "moving") {
264
+ if (
265
+ params.windowSize === undefined &&
266
+ params.windowDuration === undefined
267
+ ) {
268
+ throw new TypeError(
269
+ `RMS: either windowSize or windowDuration must be specified for "moving" mode`
270
+ );
271
+ }
272
+ if (
273
+ params.windowSize !== undefined &&
274
+ (params.windowSize <= 0 || !Number.isInteger(params.windowSize))
275
+ ) {
276
+ throw new TypeError(
277
+ `RMS: windowSize must be a positive integer for "moving" mode, got ${params.windowSize}`
278
+ );
279
+ }
280
+ if (params.windowDuration !== undefined && params.windowDuration <= 0) {
281
+ throw new TypeError(
282
+ `RMS: windowDuration must be positive, got ${params.windowDuration}`
283
+ );
284
+ }
285
+ }
286
+ this.nativeInstance.addStage("rms", params);
287
+ this.stages.push(`rms:${params.mode}`);
288
+ return this;
289
+ }
290
+
291
+ /**
292
+ * Add a rectify stage to the pipeline
293
+ * @param params - Configuration for the rectify filter (optional)
294
+ * @returns this instance for method chaining
295
+ */
296
+ Rectify(params?: RectifyParams): this {
297
+ this.nativeInstance.addStage("rectify", params || { mode: "full" });
298
+ this.stages.push(`rectify:${params?.mode || "full"}`);
299
+ return this;
300
+ }
301
+
302
+ /**
303
+ * Add a variance stage to the pipeline
304
+ * Variance measures the spread of data around the mean
305
+ *
306
+ * @param params - Configuration for the variance filter
307
+ * @param params.mode - "batch" for stateless variance (all samples → single value), "moving" for windowed variance
308
+ * @param params.windowSize - Required for "moving" mode when using sample-based processing
309
+ * @param params.windowDuration - Required for "moving" mode when using time-based processing (milliseconds)
310
+ * @returns this instance for method chaining
311
+ *
312
+ * @example
313
+ * // Batch variance (stateless)
314
+ * pipeline.Variance({ mode: "batch" });
315
+ *
316
+ * @example
317
+ * // Moving variance with sample window (legacy)
318
+ * pipeline.Variance({ mode: "moving", windowSize: 100 });
319
+ *
320
+ * @example
321
+ * // Moving variance with time window (recommended)
322
+ * pipeline.Variance({ mode: "moving", windowDuration: 60000 }); // 1 minute
323
+ */
324
+ Variance(params: VarianceParams): this {
325
+ if (params.mode === "moving") {
326
+ if (
327
+ params.windowSize === undefined &&
328
+ params.windowDuration === undefined
329
+ ) {
330
+ throw new TypeError(
331
+ `Variance: either windowSize or windowDuration must be specified for "moving" mode`
332
+ );
333
+ }
334
+ if (
335
+ params.windowSize !== undefined &&
336
+ (params.windowSize <= 0 || !Number.isInteger(params.windowSize))
337
+ ) {
338
+ throw new TypeError(
339
+ `Variance: windowSize must be a positive integer for "moving" mode, got ${params.windowSize}`
340
+ );
341
+ }
342
+ if (params.windowDuration !== undefined && params.windowDuration <= 0) {
343
+ throw new TypeError(
344
+ `Variance: windowDuration must be positive, got ${params.windowDuration}`
345
+ );
346
+ }
347
+ }
348
+ this.nativeInstance.addStage("variance", params);
349
+ this.stages.push(`variance:${params.mode}`);
350
+ return this;
351
+ }
352
+
353
+ /**
354
+ * Add a Z-Score Normalization stage to the pipeline
355
+ * Z-Score Normalization standardizes data to have mean 0 and standard deviation 1
356
+ * @param params - Configuration for the Z-Score Normalization filter
357
+ * @param params.mode - "batch" for stateless normalization, "moving" for windowed normalization
358
+ * @param params.windowSize - Required for "moving" mode when using sample-based processing
359
+ * @param params.windowDuration - Required for "moving" mode when using time-based processing (milliseconds)
360
+ * @return this instance for method chaining
361
+ * @example
362
+ * // Batch Z-Score Normalization (stateless)
363
+ * pipeline.ZScoreNormalize({ mode: "batch" });
364
+ * @example
365
+ * // Moving Z-Score Normalization with sample window (legacy)
366
+ * pipeline.ZScoreNormalize({ mode: "moving", windowSize: 100 });
367
+ * @example
368
+ * // Moving Z-Score Normalization with time window (recommended)
369
+ * pipeline.ZScoreNormalize({ mode: "moving", windowDuration: 30000 }); // 30 seconds
370
+ */
371
+ ZScoreNormalize(params: ZScoreNormalizeParams): this {
372
+ if (params.mode === "moving") {
373
+ if (
374
+ params.windowSize === undefined &&
375
+ params.windowDuration === undefined
376
+ ) {
377
+ throw new TypeError(
378
+ `Z-Score Normalize: either windowSize or windowDuration must be specified for "moving" mode`
379
+ );
380
+ }
381
+ if (
382
+ params.windowSize !== undefined &&
383
+ (params.windowSize <= 0 || !Number.isInteger(params.windowSize))
384
+ ) {
385
+ throw new TypeError(
386
+ `Z-Score Normalize: windowSize must be a positive integer for "moving" mode, got ${params.windowSize}`
387
+ );
388
+ }
389
+ if (params.windowDuration !== undefined && params.windowDuration <= 0) {
390
+ throw new TypeError(
391
+ `Z-Score Normalize: windowDuration must be positive, got ${params.windowDuration}`
392
+ );
393
+ }
394
+ }
395
+ this.nativeInstance.addStage("zScoreNormalize", params);
396
+ this.stages.push(`zScoreNormalize:${params.mode}`);
397
+ return this;
398
+ }
399
+
400
+ /**
401
+ * Add a Mean Absolute Value (MAV) stage to the pipeline
402
+ * Mean Absolute Value computes the average of the absolute values of the samples
403
+ * @param params - Configuration for the MAV filter
404
+ * @param params.mode - "batch" for stateless MAV (all samples → single value), "moving" for windowed MAV
405
+ * @param params.windowSize - Required for "moving" mode when using sample-based processing
406
+ * @param params.windowDuration - Required for "moving" mode when using time-based processing (milliseconds)
407
+ * @return this instance for method chaining
408
+ * @example
409
+ * // Batch MAV (stateless)
410
+ * pipeline.MeanAbsoluteValue({ mode: "batch" });
411
+ * @example
412
+ * // Moving MAV with sample window (legacy)
413
+ * pipeline.MeanAbsoluteValue({ mode: "moving", windowSize: 50 });
414
+ * @example
415
+ * // Moving MAV with time window (recommended)
416
+ * pipeline.MeanAbsoluteValue({ mode: "moving", windowDuration: 2000 }); // 2 seconds
417
+ */
418
+ MeanAbsoluteValue(params: MeanAbsoluteValueParams): this {
419
+ if (params.mode === "moving") {
420
+ if (
421
+ params.windowSize === undefined &&
422
+ params.windowDuration === undefined
423
+ ) {
424
+ throw new TypeError(
425
+ `Mean Absolute Value: either windowSize or windowDuration must be specified for "moving" mode`
426
+ );
427
+ }
428
+ if (
429
+ params.windowSize !== undefined &&
430
+ (params.windowSize <= 0 || !Number.isInteger(params.windowSize))
431
+ ) {
432
+ throw new TypeError(
433
+ `Mean Absolute Value: windowSize must be a positive integer for "moving" mode, got ${params.windowSize}`
434
+ );
435
+ }
436
+ if (params.windowDuration !== undefined && params.windowDuration <= 0) {
437
+ throw new TypeError(
438
+ `Mean Absolute Value: windowDuration must be positive, got ${params.windowDuration}`
439
+ );
440
+ }
441
+ }
442
+ this.nativeInstance.addStage("meanAbsoluteValue", params);
443
+ this.stages.push(`meanAbsoluteValue:${params.mode}`);
444
+ return this;
445
+ }
446
+
447
+ /**
448
+ * Add a Waveform Length stage to the pipeline
449
+ * Computes the cumulative length of the signal path (sum of absolute differences between consecutive samples)
450
+ * Useful for EMG activity detection and signal complexity analysis
451
+ *
452
+ * @param params - Configuration for the waveform length filter
453
+ * @param params.windowSize - Number of samples in the sliding window
454
+ * @returns this instance for method chaining
455
+ *
456
+ * @example
457
+ * // Basic waveform length calculation
458
+ * pipeline.WaveformLength({ windowSize: 100 });
459
+ *
460
+ * @example
461
+ * // Multi-stage EMG pipeline
462
+ * pipeline
463
+ * .Rectify({ mode: "full" })
464
+ * .WaveformLength({ windowSize: 50 })
465
+ * .tap((samples) => console.log('WL:', samples[0]));
466
+ */
467
+ WaveformLength(params: WaveformLengthParams): this {
468
+ if (params.windowSize <= 0 || !Number.isInteger(params.windowSize)) {
469
+ throw new TypeError(
470
+ `WaveformLength: windowSize must be a positive integer, got ${params.windowSize}`
471
+ );
472
+ }
473
+ this.nativeInstance.addStage("waveformLength", params);
474
+ this.stages.push(`waveformLength:${params.windowSize}`);
475
+ return this;
476
+ }
477
+
478
+ /**
479
+ * Add a Slope Sign Change (SSC) stage to the pipeline
480
+ * Counts the number of times the signal slope changes sign within a window
481
+ * Useful for EMG frequency content analysis and muscle fatigue detection
482
+ *
483
+ * @param params - Configuration for the SSC filter
484
+ * @param params.windowSize - Number of samples in the sliding window
485
+ * @param params.threshold - Noise suppression threshold (default: 0.0)
486
+ * @returns this instance for method chaining
487
+ *
488
+ * @example
489
+ * // Basic SSC with no threshold
490
+ * pipeline.SlopeSignChange({ windowSize: 100 });
491
+ *
492
+ * @example
493
+ * // SSC with noise threshold
494
+ * pipeline.SlopeSignChange({ windowSize: 100, threshold: 0.01 });
495
+ *
496
+ * @example
497
+ * // EMG frequency analysis pipeline
498
+ * pipeline
499
+ * .Rectify({ mode: "full" })
500
+ * .SlopeSignChange({ windowSize: 50, threshold: 0.005 })
501
+ * .tap((samples) => console.log('SSC count:', samples[0]));
502
+ */
503
+ SlopeSignChange(params: SlopeSignChangeParams): this {
504
+ if (params.windowSize <= 0 || !Number.isInteger(params.windowSize)) {
505
+ throw new TypeError(
506
+ `SlopeSignChange: windowSize must be a positive integer, got ${params.windowSize}`
507
+ );
508
+ }
509
+ if (params.threshold !== undefined && params.threshold < 0) {
510
+ throw new TypeError(
511
+ `SlopeSignChange: threshold must be non-negative, got ${params.threshold}`
512
+ );
513
+ }
514
+ this.nativeInstance.addStage("slopeSignChange", params);
515
+ this.stages.push(`slopeSignChange:${params.windowSize}`);
516
+ return this;
517
+ }
518
+
519
+ /**
520
+ * Add a Willison Amplitude (WAMP) stage to the pipeline
521
+ * Counts the number of times consecutive samples differ by more than a threshold
522
+ * Useful for EMG burst detection and muscle activity classification
523
+ *
524
+ * @param params - Configuration for the WAMP filter
525
+ * @param params.windowSize - Number of samples in the sliding window
526
+ * @param params.threshold - Difference threshold for counting (default: 0.0)
527
+ * @returns this instance for method chaining
528
+ *
529
+ * @example
530
+ * // Basic WAMP with no threshold
531
+ * pipeline.WillisonAmplitude({ windowSize: 100 });
532
+ *
533
+ * @example
534
+ * // WAMP with threshold for burst detection
535
+ * pipeline.WillisonAmplitude({ windowSize: 100, threshold: 0.05 });
536
+ *
537
+ * @example
538
+ * // EMG burst detection pipeline
539
+ * pipeline
540
+ * .Rectify({ mode: "full" })
541
+ * .WillisonAmplitude({ windowSize: 50, threshold: 0.02 })
542
+ * .tap((samples) => console.log('WAMP count:', samples[0]));
543
+ */
544
+ WillisonAmplitude(params: WillisonAmplitudeParams): this {
545
+ if (params.windowSize <= 0 || !Number.isInteger(params.windowSize)) {
546
+ throw new TypeError(
547
+ `WillisonAmplitude: windowSize must be a positive integer, got ${params.windowSize}`
548
+ );
549
+ }
550
+ if (params.threshold !== undefined && params.threshold < 0) {
551
+ throw new TypeError(
552
+ `WillisonAmplitude: threshold must be non-negative, got ${params.threshold}`
553
+ );
554
+ }
555
+ this.nativeInstance.addStage("willisonAmplitude", params);
556
+ this.stages.push(`willisonAmplitude:${params.windowSize}`);
557
+ return this;
558
+ }
559
+
560
+ /**
561
+ * Tap into the pipeline for debugging and inspection
562
+ * The callback is executed synchronously after processing, allowing you to inspect
563
+ * intermediate results without modifying the data flow
564
+ *
565
+ * @param callback - Function to inspect samples (receives Float32Array view and stage name)
566
+ * @returns this instance for method chaining
567
+ *
568
+ * @example
569
+ * pipeline
570
+ * .MovingAverage({ mode: "moving", windowSize: 10 })
571
+ * .tap((samples, stage) => console.log(`After ${stage}:`, samples.slice(0, 5)))
572
+ * .Rectify()
573
+ * .tap((samples) => logger.debug('After rectify:', samples.slice(0, 5)))
574
+ * .Rms({ mode: "moving", windowSize: 5 });
575
+ *
576
+ * @example
577
+ * // Conditional logging
578
+ * pipeline
579
+ * .MovingAverage({ mode: "moving", windowSize: 100 })
580
+ * .tap((samples, stage) => {
581
+ * const max = Math.max(...samples);
582
+ * if (max > THRESHOLD) {
583
+ * console.warn(`High amplitude detected at ${stage}: ${max}`);
584
+ * }
585
+ * });
586
+ */
587
+ tap(callback: TapCallback): this {
588
+ const currentStageName = this.stages.join(" → ") || "start";
589
+ this.tapCallbacks.push({ stageName: currentStageName, callback });
590
+ return this;
591
+ }
592
+
593
+ /**
594
+ * Add a filter stage to the pipeline
595
+ * Supports FIR and IIR filters with various designs (Butterworth, Chebyshev, etc.)
596
+ *
597
+ * @param options - Filter configuration options
598
+ * @returns this instance for method chaining
599
+ *
600
+ * @example
601
+ * // FIR low-pass filter
602
+ * pipeline.filter({
603
+ * type: "fir",
604
+ * mode: "lowpass",
605
+ * cutoffFrequency: 1000,
606
+ * sampleRate: 8000,
607
+ * order: 51
608
+ * });
609
+ *
610
+ * @example
611
+ * // Butterworth band-pass filter
612
+ * pipeline.filter({
613
+ * type: "butterworth",
614
+ * mode: "bandpass",
615
+ * lowCutoffFrequency: 300,
616
+ * highCutoffFrequency: 3000,
617
+ * sampleRate: 8000,
618
+ * order: 4
619
+ * });
620
+ *
621
+ * @example
622
+ * // Chebyshev low-pass with ripple
623
+ * pipeline.filter({
624
+ * type: "chebyshev",
625
+ * mode: "lowpass",
626
+ * cutoffFrequency: 1000,
627
+ * sampleRate: 8000,
628
+ * order: 2,
629
+ * ripple: 0.5
630
+ * });
631
+ *
632
+ * @example
633
+ * // Peaking EQ (biquad)
634
+ * pipeline.filter({
635
+ * type: "biquad",
636
+ * mode: "peak",
637
+ * centerFrequency: 1000,
638
+ * sampleRate: 8000,
639
+ * q: 2.0,
640
+ * gain: 6.0
641
+ * });
642
+ */
643
+ filter(options: FilterOptions): this {
644
+ // Create the appropriate filter based on type
645
+ let filterInstance: FirFilter | IirFilter;
646
+
647
+ switch (options.type) {
648
+ case "fir":
649
+ filterInstance = this.createFirFilter(options);
650
+ break;
651
+
652
+ case "butterworth":
653
+ filterInstance = this.createButterworthFilter(options);
654
+ break;
655
+
656
+ case "chebyshev":
657
+ filterInstance = this.createChebyshevFilter(options);
658
+ break;
659
+
660
+ case "biquad":
661
+ filterInstance = this.createBiquadFilter(options);
662
+ break;
663
+
664
+ case "iir":
665
+ default:
666
+ throw new Error(
667
+ `Filter type "${options.type}" not yet implemented for pipeline chaining. Use standalone filter methods instead.`
668
+ );
669
+ }
670
+
671
+ // Store the filter instance for processing
672
+ // Note: We'll need to process through this filter during the process() call
673
+ // For now, this is a placeholder - we need C++ support for filter stages in pipeline
674
+
675
+ throw new Error(
676
+ "Filter stages in pipeline not yet implemented in C++ layer. Use standalone filters with manual chaining instead:\n" +
677
+ " const filter = IirFilter.createButterworthLowPass({...});\n" +
678
+ " const output1 = await pipeline.process(samples);\n" +
679
+ " const output2 = filter.process(output1);"
680
+ );
681
+
682
+ // Future: Once C++ support is added, this would be:
683
+ // this.nativeInstance.addFilterStage(filterInstance.getNative());
684
+ // this.stages.push(`filter:${options.type}:${options.mode}`);
685
+ // return this;
686
+ }
687
+
688
+ /**
689
+ * Helper to create FIR filter from options
690
+ */
691
+ private createFirFilter(options: FilterOptions & { type: "fir" }): FirFilter {
692
+ const { mode, cutoffFrequency, lowCutoffFrequency, highCutoffFrequency } =
693
+ options;
694
+
695
+ switch (mode) {
696
+ case "lowpass":
697
+ if (!cutoffFrequency) {
698
+ throw new Error("cutoffFrequency required for lowpass filter");
699
+ }
700
+ return FirFilter.createLowPass(options as any);
701
+
702
+ case "highpass":
703
+ if (!cutoffFrequency) {
704
+ throw new Error("cutoffFrequency required for highpass filter");
705
+ }
706
+ return FirFilter.createHighPass(options as any);
707
+
708
+ case "bandpass":
709
+ if (!lowCutoffFrequency || !highCutoffFrequency) {
710
+ throw new Error(
711
+ "lowCutoffFrequency and highCutoffFrequency required for bandpass filter"
712
+ );
713
+ }
714
+ return FirFilter.createBandPass(options as any);
715
+
716
+ case "bandstop":
717
+ case "notch":
718
+ if (!lowCutoffFrequency || !highCutoffFrequency) {
719
+ throw new Error(
720
+ "lowCutoffFrequency and highCutoffFrequency required for bandstop filter"
721
+ );
722
+ }
723
+ return FirFilter.createBandStop(options as any);
724
+
725
+ default:
726
+ throw new Error(`Unsupported FIR filter mode: ${mode}`);
727
+ }
728
+ }
729
+
730
+ /**
731
+ * Helper to create Butterworth filter from options
732
+ */
733
+ private createButterworthFilter(
734
+ options: FilterOptions & { type: "butterworth" }
735
+ ): IirFilter {
736
+ const { mode, cutoffFrequency, lowCutoffFrequency, highCutoffFrequency } =
737
+ options;
738
+
739
+ switch (mode) {
740
+ case "lowpass":
741
+ if (!cutoffFrequency) {
742
+ throw new Error("cutoffFrequency required for lowpass filter");
743
+ }
744
+ return IirFilter.createButterworthLowPass(options as any);
745
+
746
+ case "highpass":
747
+ if (!cutoffFrequency) {
748
+ throw new Error("cutoffFrequency required for highpass filter");
749
+ }
750
+ return IirFilter.createButterworthHighPass(options as any);
751
+
752
+ case "bandpass":
753
+ if (!lowCutoffFrequency || !highCutoffFrequency) {
754
+ throw new Error(
755
+ "lowCutoffFrequency and highCutoffFrequency required for bandpass filter"
756
+ );
757
+ }
758
+ return IirFilter.createButterworthBandPass(options as any);
759
+
760
+ default:
761
+ throw new Error(`Unsupported Butterworth filter mode: ${mode}`);
762
+ }
763
+ }
764
+
765
+ /**
766
+ * Helper to create Chebyshev filter from options
767
+ */
768
+ private createChebyshevFilter(
769
+ options: FilterOptions & { type: "chebyshev" }
770
+ ): IirFilter {
771
+ const {
772
+ mode,
773
+ cutoffFrequency,
774
+ lowCutoffFrequency,
775
+ highCutoffFrequency,
776
+ ripple = 0.5,
777
+ } = options as any;
778
+
779
+ switch (mode) {
780
+ case "lowpass":
781
+ if (!cutoffFrequency) {
782
+ throw new Error("cutoffFrequency required for lowpass filter");
783
+ }
784
+ return IirFilter.createChebyshevLowPass({
785
+ cutoffFrequency,
786
+ sampleRate: options.sampleRate,
787
+ order: options.order,
788
+ rippleDb: ripple,
789
+ });
790
+
791
+ case "highpass":
792
+ if (!cutoffFrequency) {
793
+ throw new Error("cutoffFrequency required for highpass filter");
794
+ }
795
+ return IirFilter.createChebyshevHighPass({
796
+ cutoffFrequency,
797
+ sampleRate: options.sampleRate,
798
+ order: options.order,
799
+ rippleDb: ripple,
800
+ });
801
+
802
+ case "bandpass":
803
+ if (!lowCutoffFrequency || !highCutoffFrequency) {
804
+ throw new Error(
805
+ "lowCutoffFrequency and highCutoffFrequency required for bandpass filter"
806
+ );
807
+ }
808
+ return IirFilter.createChebyshevBandPass({
809
+ lowCutoffFrequency,
810
+ highCutoffFrequency,
811
+ sampleRate: options.sampleRate,
812
+ order: options.order,
813
+ rippleDb: ripple,
814
+ });
815
+
816
+ default:
817
+ throw new Error(`Unsupported Chebyshev filter mode: ${mode}`);
818
+ }
819
+ }
820
+
821
+ /**
822
+ * Helper to create Biquad filter from options
823
+ */
824
+ private createBiquadFilter(
825
+ options: FilterOptions & { type: "biquad" }
826
+ ): IirFilter {
827
+ const { mode, cutoffFrequency, q = 0.707, gain = 0 } = options as any;
828
+ const { sampleRate } = options;
829
+
830
+ switch (mode) {
831
+ case "peak":
832
+ case "peaking":
833
+ if (!cutoffFrequency) {
834
+ throw new Error("cutoffFrequency (center frequency) required");
835
+ }
836
+ return IirFilter.createPeakingEQ({
837
+ centerFrequency: cutoffFrequency,
838
+ sampleRate,
839
+ Q: q,
840
+ gainDb: gain,
841
+ });
842
+
843
+ case "lowshelf":
844
+ if (!cutoffFrequency) {
845
+ throw new Error("cutoffFrequency required for low-shelf filter");
846
+ }
847
+ return IirFilter.createLowShelf({
848
+ cutoffFrequency,
849
+ sampleRate,
850
+ gainDb: gain,
851
+ Q: q,
852
+ });
853
+
854
+ case "highshelf":
855
+ if (!cutoffFrequency) {
856
+ throw new Error("cutoffFrequency required for high-shelf filter");
857
+ }
858
+ return IirFilter.createHighShelf({
859
+ cutoffFrequency,
860
+ sampleRate,
861
+ gainDb: gain,
862
+ Q: q,
863
+ });
864
+
865
+ case "lowpass":
866
+ if (!cutoffFrequency) {
867
+ throw new Error("cutoffFrequency required");
868
+ }
869
+ // Use Butterworth for biquad lowpass (2nd order)
870
+ return IirFilter.createButterworthLowPass({
871
+ cutoffFrequency,
872
+ sampleRate,
873
+ order: 2,
874
+ });
875
+
876
+ case "highpass":
877
+ if (!cutoffFrequency) {
878
+ throw new Error("cutoffFrequency required");
879
+ }
880
+ // Use Butterworth for biquad highpass (2nd order)
881
+ return IirFilter.createButterworthHighPass({
882
+ cutoffFrequency,
883
+ sampleRate,
884
+ order: 2,
885
+ });
886
+
887
+ case "bandpass":
888
+ case "bandstop":
889
+ case "notch":
890
+ throw new Error(
891
+ `Biquad ${mode} filters not yet implemented. Use Butterworth or Chebyshev filters instead.`
892
+ );
893
+
894
+ default:
895
+ throw new Error(`Unsupported Biquad filter mode: ${mode}`);
896
+ }
897
+ }
898
+
899
+ /**
900
+ * Configure pipeline callbacks for monitoring and observability
901
+ * @param callbacks - Object containing callback functions
902
+ * @returns this instance for method chaining
903
+ *
904
+ * @example
905
+ * pipeline
906
+ * .pipeline({
907
+ * onSample: (value, i, stage) => {
908
+ * if (value > THRESHOLD) triggerAlert(i, stage);
909
+ * },
910
+ * onStageComplete: (stage, durationMs) => {
911
+ * metrics.record(`dsp.${stage}.duration`, durationMs);
912
+ * },
913
+ * onError: (stage, err) => {
914
+ * logger.error(`Stage ${stage} failed`, err);
915
+ * },
916
+ * onLog: (level, msg, ctx) => {
917
+ * if (level === "debug") return;
918
+ * console.log(`[${level}] ${msg}`, ctx);
919
+ * },
920
+ * })
921
+ * .MovingAverage({ mode: "moving", windowSize: 10 })
922
+ * .Rectify();
923
+ */
924
+ pipeline(callbacks: PipelineCallbacks): this {
925
+ this.callbacks = callbacks;
926
+ return this;
927
+ }
928
+
929
+ /**
930
+ * Process data through the DSP pipeline
931
+ * The native process method uses Napi::AsyncWorker and runs on a background thread
932
+ * to avoid blocking the Node.js event loop
933
+ *
934
+ * Supports three modes:
935
+ * 1. Legacy sample-based: process(samples, { sampleRate: 100, channels: 1 })
936
+ * 2. Time-based with timestamps: process(samples, timestamps, { channels: 1 })
937
+ * 3. Auto-generated timestamps: process(samples, { channels: 1 }) - generates [0, 1, 2, ...]
938
+ *
939
+ * IMPORTANT: This method modifies the input buffer in-place for performance.
940
+ * If you need to preserve the original input, pass a copy instead.
941
+ *
942
+ * @param input - Float32Array containing interleaved samples (will be modified in-place)
943
+ * @param timestampsOrOptions - Either timestamps (Float32Array) or ProcessOptions
944
+ * @param optionsIfTimestamps - ProcessOptions if second argument is timestamps
945
+ * @returns Promise that resolves to the processed Float32Array (same reference as input)
946
+ */
947
+ async process(
948
+ input: Float32Array,
949
+ timestampsOrOptions: Float32Array | ProcessOptions,
950
+ optionsIfTimestamps?: ProcessOptions
951
+ ): Promise<Float32Array> {
952
+ let timestamps: Float32Array | undefined;
953
+ let options: ProcessOptions;
954
+
955
+ // Detect which overload was called
956
+ if (timestampsOrOptions instanceof Float32Array) {
957
+ // Time-based mode: process(samples, timestamps, options)
958
+ timestamps = timestampsOrOptions;
959
+ options = { channels: 1, ...optionsIfTimestamps };
960
+
961
+ if (timestamps.length !== input.length) {
962
+ throw new Error(
963
+ `Timestamps length (${timestamps.length}) must match samples length (${input.length})`
964
+ );
965
+ }
966
+ } else {
967
+ // Sample-based mode or auto-timestamps: process(samples, options)
968
+ options = { channels: 1, ...timestampsOrOptions };
969
+
970
+ if (options.sampleRate) {
971
+ // Legacy sample-based mode: auto-generate timestamps from sampleRate
972
+ const dt = 1000 / options.sampleRate; // milliseconds per sample
973
+ timestamps = new Float32Array(input.length);
974
+ for (let i = 0; i < input.length; i++) {
975
+ timestamps[i] = i * dt;
976
+ }
977
+ } else {
978
+ // Auto-generate sequential timestamps [0, 1, 2, ...]
979
+ timestamps = new Float32Array(input.length);
980
+ for (let i = 0; i < input.length; i++) {
981
+ timestamps[i] = i;
982
+ }
983
+ }
984
+ }
985
+
986
+ const startTime = performance.now();
987
+
988
+ // Initialize drift detection if enabled
989
+ if (options.enableDriftDetection && timestamps && options.sampleRate) {
990
+ if (
991
+ !this.driftDetector ||
992
+ this.driftDetector.getExpectedSampleRate() !== options.sampleRate
993
+ ) {
994
+ // Create new detector if it doesn't exist or sample rate changed
995
+ this.driftDetector = new DriftDetector({
996
+ expectedSampleRate: options.sampleRate,
997
+ driftThreshold: options.driftThreshold ?? 10,
998
+ onDriftDetected: options.onDriftDetected,
999
+ });
1000
+ }
1001
+ // Process timestamps to detect drift
1002
+ this.driftDetector.processBatch(timestamps);
1003
+ }
1004
+
1005
+ try {
1006
+ // Pool the start log
1007
+ this.poolLog("debug", "Starting pipeline processing", {
1008
+ sampleCount: input.length,
1009
+ channels: options.channels,
1010
+ stages: this.stages.length,
1011
+ mode: options.sampleRate ? "sample-based" : "time-based",
1012
+ });
1013
+
1014
+ // Call native process with timestamps
1015
+ // Note: The input buffer is modified in-place for zero-copy performance
1016
+ const result = await this.nativeInstance.process(
1017
+ input,
1018
+ timestamps,
1019
+ options
1020
+ );
1021
+
1022
+ // Execute tap callbacks for debugging/inspection
1023
+ if (this.tapCallbacks.length > 0) {
1024
+ for (const { stageName, callback } of this.tapCallbacks) {
1025
+ try {
1026
+ callback(result, stageName);
1027
+ } catch (tapError) {
1028
+ // Don't let tap errors break the pipeline
1029
+ console.error(`Tap callback error at ${stageName}:`, tapError);
1030
+ }
1031
+ }
1032
+ }
1033
+
1034
+ // Execute onBatch callback (efficient - one call per process)
1035
+ if (this.callbacks?.onBatch) {
1036
+ const stageName = this.stages.join(" → ") || "pipeline";
1037
+ const batch: SampleBatch = {
1038
+ stage: stageName,
1039
+ samples: result,
1040
+ startIndex: 0,
1041
+ count: result.length,
1042
+ };
1043
+ this.callbacks.onBatch(batch);
1044
+ }
1045
+
1046
+ // Execute onSample callbacks if provided (LEGACY - expensive)
1047
+ // WARNING: This can be expensive for large buffers
1048
+ if (this.callbacks?.onSample) {
1049
+ const stageName = this.stages.join(" → ") || "pipeline";
1050
+ for (let i = 0; i < result.length; i++) {
1051
+ this.callbacks.onSample(result[i], i, stageName);
1052
+ }
1053
+ }
1054
+
1055
+ // Execute onStageComplete callback
1056
+ if (this.callbacks?.onStageComplete) {
1057
+ const duration = performance.now() - startTime;
1058
+ const pipelineName = this.stages.join(" → ") || "pipeline";
1059
+ this.callbacks.onStageComplete(pipelineName, duration);
1060
+ }
1061
+
1062
+ // Pool the completion log
1063
+ const duration = performance.now() - startTime;
1064
+ this.poolLog("info", "Pipeline processing completed", {
1065
+ durationMs: duration,
1066
+ sampleCount: result.length,
1067
+ });
1068
+
1069
+ // Flush all pooled logs at the end
1070
+ this.flushLogs();
1071
+
1072
+ return result;
1073
+ } catch (error) {
1074
+ const err = error as Error;
1075
+
1076
+ // Execute onError callback
1077
+ if (this.callbacks?.onError) {
1078
+ const pipelineName = this.stages.join(" → ") || "pipeline";
1079
+ this.callbacks.onError(pipelineName, err);
1080
+ }
1081
+
1082
+ // Pool the error log
1083
+ this.poolLog("error", "Pipeline processing failed", {
1084
+ error: err.message,
1085
+ stack: err.stack,
1086
+ });
1087
+
1088
+ // Flush logs even on error
1089
+ this.flushLogs();
1090
+
1091
+ throw error;
1092
+ }
1093
+ }
1094
+
1095
+ /**
1096
+ * Process a copy of the audio data through the DSP pipeline
1097
+ * This method creates a copy of the input, so the original is preserved
1098
+ *
1099
+ * @param input - Float32Array containing interleaved audio samples (original is preserved)
1100
+ * @param timestampsOrOptions - Either timestamps array or processing options (sample rate and channel count)
1101
+ * @param optionsIfTimestamps - Processing options if timestamps were provided in second parameter
1102
+ * @returns Promise that resolves to a new Float32Array with the processed data
1103
+ *
1104
+ * @example
1105
+ * // Legacy sample-based (original preserved)
1106
+ * const output = await pipeline.processCopy(samples, { sampleRate: 100, channels: 1 });
1107
+ *
1108
+ * @example
1109
+ * // Time-based with explicit timestamps (original preserved)
1110
+ * const output = await pipeline.processCopy(samples, timestamps, { channels: 1 });
1111
+ */
1112
+ async processCopy(
1113
+ input: Float32Array,
1114
+ timestampsOrOptions: Float32Array | ProcessOptions,
1115
+ optionsIfTimestamps?: ProcessOptions
1116
+ ): Promise<Float32Array> {
1117
+ // Create a copy to preserve the original
1118
+ const copy = new Float32Array(input);
1119
+
1120
+ // Handle both overloaded signatures by delegating to process()
1121
+ if (timestampsOrOptions instanceof Float32Array) {
1122
+ // Time-based mode: process(samples, timestamps, options)
1123
+ const timestampsCopy = new Float32Array(timestampsOrOptions);
1124
+ return await this.process(copy, timestampsCopy, optionsIfTimestamps!);
1125
+ } else {
1126
+ // Legacy mode: process(samples, options)
1127
+ return await this.process(copy, timestampsOrOptions);
1128
+ }
1129
+ }
1130
+
1131
+ /**
1132
+ * Save the current pipeline state as a JSON string
1133
+ * TypeScript can then store this in Redis or other persistent storage
1134
+ *
1135
+ * @returns JSON string containing the pipeline state
1136
+ *
1137
+ * @example
1138
+ * const stateJson = await pipeline.saveState();
1139
+ * await redis.set('dsp:state', stateJson);
1140
+ */
1141
+ async saveState(): Promise<string> {
1142
+ return this.nativeInstance.saveState();
1143
+ }
1144
+
1145
+ /**
1146
+ * Load pipeline state from a JSON string
1147
+ * TypeScript retrieves this from Redis and passes it to restore state
1148
+ *
1149
+ * @param stateJson - JSON string containing the pipeline state
1150
+ * @returns Promise that resolves to true if successful
1151
+ *
1152
+ * @example
1153
+ * const stateJson = await redis.get('dsp:state');
1154
+ * if (stateJson) {
1155
+ * await pipeline.loadState(stateJson);
1156
+ * }
1157
+ */
1158
+ async loadState(stateJson: string): Promise<boolean> {
1159
+ return this.nativeInstance.loadState(stateJson);
1160
+ }
1161
+
1162
+ /**
1163
+ * Clear all pipeline state (reset all filters to initial state)
1164
+ * This resets filter buffers without removing the stages
1165
+ *
1166
+ * @example
1167
+ * pipeline.clearState(); // Reset all filters
1168
+ */
1169
+ clearState(): void {
1170
+ this.nativeInstance.clearState();
1171
+ }
1172
+
1173
+ /**
1174
+ * List current pipeline state summary
1175
+ * Returns a lightweight view of the pipeline configuration without full state data.
1176
+ * Useful for debugging, monitoring, and inspecting pipeline structure.
1177
+ *
1178
+ * @returns Object containing pipeline summary with stage info
1179
+ *
1180
+ * @example
1181
+ * const pipeline = createDspPipeline()
1182
+ * .MovingAverage({ mode: "moving", windowSize: 100 })
1183
+ * .Rectify({ mode: 'full' })
1184
+ * .Rms({ mode: "moving", windowSize: 50 });
1185
+ *
1186
+ * const summary = pipeline.listState();
1187
+ * console.log(summary);
1188
+ * // {
1189
+ * // stageCount: 3,
1190
+ * // timestamp: 1761234567,
1191
+ * // stages: [
1192
+ * // { index: 0, type: 'movingAverage', windowSize: 100, numChannels: 1, bufferSize: 100, channelCount: 1 },
1193
+ * // { index: 1, type: 'rectify', mode: 'full', numChannels: 1 },
1194
+ * // { index: 2, type: 'rms', windowSize: 50, numChannels: 1, bufferSize: 50, channelCount: 1 }
1195
+ * // ]
1196
+ * // }
1197
+ */
1198
+ listState(): PipelineStateSummary {
1199
+ return this.nativeInstance.listState();
1200
+ }
1201
+ }
1202
+
1203
+ /**
1204
+ * Create a new DSP pipeline builder
1205
+ * @param config - Optional Redis configuration for state persistence
1206
+ * @returns A new DspProcessor instance
1207
+ *
1208
+ * @example
1209
+ * // Create pipeline with Redis state persistence
1210
+ * const pipeline = createDspPipeline({
1211
+ * redisHost: 'localhost',
1212
+ * redisPort: 6379,
1213
+ * stateKey: 'dsp:channel1'
1214
+ * });
1215
+ *
1216
+ * @example
1217
+ * // Create pipeline without Redis (state is not persisted)
1218
+ * const pipeline = createDspPipeline();
1219
+ */
1220
+ export function createDspPipeline(config?: RedisConfig): DspProcessor {
1221
+ const nativeInstance = new DspAddon.DspPipeline(config);
1222
+ return new DspProcessor(nativeInstance);
1223
+ }
1224
+
1225
+ export { DspProcessor };