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