dspx 1.2.3 → 1.3.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 (71) hide show
  1. package/README.md +40 -78
  2. package/binding.gyp +10 -0
  3. package/dist/FilterBankDesign.d.ts +233 -0
  4. package/dist/FilterBankDesign.d.ts.map +1 -0
  5. package/dist/FilterBankDesign.js +247 -0
  6. package/dist/FilterBankDesign.js.map +1 -0
  7. package/dist/advanced-dsp.d.ts +6 -6
  8. package/dist/advanced-dsp.d.ts.map +1 -1
  9. package/dist/advanced-dsp.js +35 -12
  10. package/dist/advanced-dsp.js.map +1 -1
  11. package/dist/backends.d.ts +0 -103
  12. package/dist/backends.d.ts.map +1 -1
  13. package/dist/backends.js +0 -217
  14. package/dist/backends.js.map +1 -1
  15. package/dist/bindings.d.ts +216 -17
  16. package/dist/bindings.d.ts.map +1 -1
  17. package/dist/bindings.js +503 -42
  18. package/dist/bindings.js.map +1 -1
  19. package/dist/index.d.ts +4 -2
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +2 -1
  22. package/dist/index.js.map +1 -1
  23. package/dist/types.d.ts +67 -8
  24. package/dist/types.d.ts.map +1 -1
  25. package/dist/utils.d.ts +38 -8
  26. package/dist/utils.d.ts.map +1 -1
  27. package/dist/utils.js +84 -26
  28. package/dist/utils.js.map +1 -1
  29. package/package.json +1 -2
  30. package/prebuilds/win32-x64/dspx.node +0 -0
  31. package/scripts/add-dispose-to-tests.js +145 -0
  32. package/src/native/DspPipeline.cc +777 -143
  33. package/src/native/DspPipeline.h +13 -0
  34. package/src/native/FilterBankDesignBindings.cc +241 -0
  35. package/src/native/IDspStage.h +24 -0
  36. package/src/native/UtilityBindings.cc +130 -0
  37. package/src/native/adapters/ClipDetectionStage.h +15 -4
  38. package/src/native/adapters/ConvolutionStage.h +101 -0
  39. package/src/native/adapters/CumulativeMovingAverageStage.h +264 -0
  40. package/src/native/adapters/DecimatorStage.h +80 -0
  41. package/src/native/adapters/DifferentiatorStage.h +13 -0
  42. package/src/native/adapters/ExponentialMovingAverageStage.h +290 -0
  43. package/src/native/adapters/FilterBankStage.cc +336 -0
  44. package/src/native/adapters/FilterBankStage.h +170 -0
  45. package/src/native/adapters/FilterStage.cc +139 -1
  46. package/src/native/adapters/FilterStage.h +4 -0
  47. package/src/native/adapters/HilbertEnvelopeStage.h +55 -0
  48. package/src/native/adapters/IntegratorStage.h +15 -0
  49. package/src/native/adapters/InterpolatorStage.h +51 -0
  50. package/src/native/adapters/LinearRegressionStage.h +40 -0
  51. package/src/native/adapters/LmsStage.h +63 -0
  52. package/src/native/adapters/MeanAbsoluteValueStage.h +76 -0
  53. package/src/native/adapters/MovingAverageStage.h +119 -0
  54. package/src/native/adapters/PeakDetectionStage.h +53 -0
  55. package/src/native/adapters/RectifyStage.h +14 -0
  56. package/src/native/adapters/ResamplerStage.h +67 -0
  57. package/src/native/adapters/RlsStage.h +76 -0
  58. package/src/native/adapters/RmsStage.h +73 -1
  59. package/src/native/adapters/SnrStage.h +45 -0
  60. package/src/native/adapters/SscStage.h +65 -0
  61. package/src/native/adapters/StftStage.h +62 -0
  62. package/src/native/adapters/VarianceStage.h +60 -1
  63. package/src/native/adapters/WampStage.h +59 -0
  64. package/src/native/adapters/WaveformLengthStage.h +51 -0
  65. package/src/native/adapters/ZScoreNormalizeStage.h +65 -1
  66. package/src/native/core/CumulativeMovingAverageFilter.h +123 -0
  67. package/src/native/core/ExponentialMovingAverageFilter.h +129 -0
  68. package/src/native/core/FilterBankDesign.h +266 -0
  69. package/src/native/core/Policies.h +124 -0
  70. package/src/native/utils/CircularBufferArray.cc +2 -1
  71. package/src/native/utils/Toon.h +195 -0
package/dist/bindings.js CHANGED
@@ -4,6 +4,7 @@ import { dirname, join } from "node:path";
4
4
  import { CircularLogBuffer } from "./CircularLogBuffer.js";
5
5
  import { DriftDetector } from "./DriftDetector.js";
6
6
  import { FirFilter, IirFilter, } from "./filters.js";
7
+ import { DspUtils } from "./utils.js";
7
8
  // Get the directory of the current file
8
9
  const __filename = fileURLToPath(import.meta.url);
9
10
  const __dirname = dirname(__filename);
@@ -166,6 +167,97 @@ class DspProcessor {
166
167
  this.stages.push(`movingAverage:${params.mode}`);
167
168
  return this;
168
169
  }
170
+ /**
171
+ * Add an Exponential Moving Average (EMA) stage to the pipeline
172
+ *
173
+ * EMA gives exponentially decaying weight to older samples, providing adaptive smoothing.
174
+ * Formula: EMA(t) = α * value(t) + (1 - α) * EMA(t-1)
175
+ *
176
+ * @param params - Configuration for the EMA filter
177
+ * @param params.mode - "batch" for stateless EMA (resets per chunk), "moving" for stateful with continuity
178
+ * @param params.alpha - Smoothing factor (0 < α ≤ 1)
179
+ * - α close to 1: Fast response to changes (less smoothing)
180
+ * - α close to 0: Slow response to changes (more smoothing)
181
+ * - From N-period SMA: α = 2 / (N + 1)
182
+ * @returns this instance for method chaining
183
+ *
184
+ * @example
185
+ * // Batch mode: Reset EMA for each process() call
186
+ * pipeline.ExponentialMovingAverage({ mode: "batch", alpha: 0.3 });
187
+ *
188
+ * @example
189
+ * // Moving mode: EMA continues across process() calls
190
+ * pipeline.ExponentialMovingAverage({ mode: "moving", alpha: 0.1 });
191
+ *
192
+ * @example
193
+ * // Fast adaptive filter (equivalent to ~10-sample SMA)
194
+ * pipeline.ExponentialMovingAverage({ mode: "moving", alpha: 2 / (10 + 1) });
195
+ *
196
+ * @example
197
+ * // Chain with other stages for advanced processing
198
+ * pipeline
199
+ * .ExponentialMovingAverage({ mode: "moving", alpha: 0.2 })
200
+ * .Rectify({ mode: "full" })
201
+ * .Rms({ mode: "moving", windowSize: 50 });
202
+ */
203
+ ExponentialMovingAverage(params) {
204
+ // Validate alpha parameter
205
+ if (params.alpha === undefined) {
206
+ throw new TypeError(`ExponentialMovingAverage: alpha is required`);
207
+ }
208
+ if (params.alpha <= 0 || params.alpha > 1) {
209
+ throw new TypeError(`ExponentialMovingAverage: alpha must be in range (0, 1], got ${params.alpha}`);
210
+ }
211
+ this.nativeInstance.addStage("exponentialMovingAverage", params);
212
+ this.stages.push(`exponentialMovingAverage:${params.mode}:α=${params.alpha}`);
213
+ return this;
214
+ }
215
+ /**
216
+ * Add a Cumulative Moving Average (CMA) stage to the pipeline
217
+ *
218
+ * CMA is the average of ALL samples seen since initialization (not just a window).
219
+ * Formula: CMA(n) = (sum of all values) / n
220
+ *
221
+ * Unlike simple moving average (SMA), CMA considers entire history:
222
+ * - SMA: Uses fixed window of recent N samples
223
+ * - CMA: Uses all samples from start to current
224
+ *
225
+ * Memory-efficient: only stores running sum and count.
226
+ *
227
+ * @param params - Configuration for the CMA filter
228
+ * @param params.mode - "batch" for stateless CMA (resets per chunk), "moving" for stateful with continuity
229
+ * @returns this instance for method chaining
230
+ *
231
+ * @example
232
+ * // Batch mode: CMA resets for each process() call
233
+ * pipeline.CumulativeMovingAverage({ mode: "batch" });
234
+ *
235
+ * @example
236
+ * // Moving mode: CMA accumulates across all process() calls
237
+ * pipeline.CumulativeMovingAverage({ mode: "moving" });
238
+ *
239
+ * @example
240
+ * // Use case: Long-term baseline estimation from calibration data
241
+ * const calibrationPipeline = createDspPipeline();
242
+ * calibrationPipeline.CumulativeMovingAverage({ mode: "moving" });
243
+ *
244
+ * // Process calibration chunks
245
+ * await calibrationPipeline.process(chunk1, { channels: 1 });
246
+ * await calibrationPipeline.process(chunk2, { channels: 1 });
247
+ * await calibrationPipeline.process(chunk3, { channels: 1 });
248
+ * // Final output contains cumulative average across all chunks
249
+ *
250
+ * @example
251
+ * // Chain with variance for online statistics
252
+ * pipeline
253
+ * .CumulativeMovingAverage({ mode: "moving" })
254
+ * .tap((samples) => console.log('Running mean:', samples[samples.length - 1]));
255
+ */
256
+ CumulativeMovingAverage(params) {
257
+ this.nativeInstance.addStage("cumulativeMovingAverage", params);
258
+ this.stages.push(`cumulativeMovingAverage:${params.mode}`);
259
+ return this;
260
+ }
169
261
  /**
170
262
  * Add a RMS (root mean square) stage to the pipeline
171
263
  * @param params - Configuration for the RMS filter
@@ -1244,6 +1336,70 @@ class DspProcessor {
1244
1336
  this.stages.push(`channelMerge:${params.numInputChannels}ch→${params.mapping.length}ch`);
1245
1337
  return this;
1246
1338
  }
1339
+ /**
1340
+ * Apply a bank of IIR filters to split each input channel into multiple frequency bands.
1341
+ *
1342
+ * Implements frequency decomposition for:
1343
+ * - Speech recognition: Mel-scale filter banks (20-40 bands)
1344
+ * - Audio compression: Bark-scale psychoacoustic analysis
1345
+ * - Musical analysis: Octave bands (log scale)
1346
+ * - Research: Linear-scale frequency analysis
1347
+ *
1348
+ * Architecture:
1349
+ * - Input: N channels (interleaved)
1350
+ * - Output: N × M channels (interleaved, channel-major layout)
1351
+ * - Layout: All bands for channel 1, then all bands for channel 2, etc.
1352
+ *
1353
+ * @param params - Filter bank parameters
1354
+ * @param params.definitions - Array of filter definitions (one per band)
1355
+ * @param params.inputChannels - Number of input channels
1356
+ * @returns this instance for method chaining
1357
+ *
1358
+ * @throws {TypeError} If definitions or inputChannels are missing
1359
+ * @throws {RangeError} If inputChannels <= 0 or definitions is empty
1360
+ *
1361
+ * @example
1362
+ * // Design a 24-band Mel-scale filter bank for speech
1363
+ * import { FilterBankDesign } from 'dspx';
1364
+ *
1365
+ * const melBank = FilterBankDesign.createMel(24, 16000, [100, 8000]);
1366
+ * pipeline.FilterBank({
1367
+ * definitions: melBank,
1368
+ * inputChannels: 1
1369
+ * });
1370
+ * // Output: 24 channels (one per Mel band)
1371
+ *
1372
+ * @example
1373
+ * // Custom 10-band octave filter bank for stereo input
1374
+ * const octaveBank = FilterBankDesign.createLog(10, 44100, [20, 20000]);
1375
+ * pipeline.FilterBank({
1376
+ * definitions: octaveBank,
1377
+ * inputChannels: 2
1378
+ * });
1379
+ * // Output: 20 channels (10 bands × 2 input channels)
1380
+ */
1381
+ FilterBank(params) {
1382
+ if (!Array.isArray(params.definitions) || params.definitions.length === 0) {
1383
+ throw new TypeError("FilterBank: definitions must be a non-empty array");
1384
+ }
1385
+ if (!Number.isInteger(params.inputChannels) || params.inputChannels <= 0) {
1386
+ throw new RangeError("FilterBank: inputChannels must be a positive integer");
1387
+ }
1388
+ // Validate each filter definition
1389
+ for (let i = 0; i < params.definitions.length; i++) {
1390
+ const def = params.definitions[i];
1391
+ if (!Array.isArray(def.b) || def.b.length === 0) {
1392
+ throw new TypeError(`FilterBank: definition[${i}].b must be a non-empty array`);
1393
+ }
1394
+ if (!Array.isArray(def.a) || def.a.length === 0) {
1395
+ throw new TypeError(`FilterBank: definition[${i}].a must be a non-empty array`);
1396
+ }
1397
+ }
1398
+ this.nativeInstance.addStage("filterBank", params);
1399
+ const outputChannels = params.inputChannels * params.definitions.length;
1400
+ this.stages.push(`filterBank:${params.definitions.length}bands×${params.inputChannels}ch=${outputChannels}ch`);
1401
+ return this;
1402
+ }
1247
1403
  /**
1248
1404
  * Detect clipping (signal saturation) in the input stream.
1249
1405
  *
@@ -2230,6 +2386,16 @@ class DspProcessor {
2230
2386
  * });
2231
2387
  *
2232
2388
  * @example
2389
+ * // Generic IIR filter (uses Butterworth internally)
2390
+ * pipeline.filter({
2391
+ * type: "iir",
2392
+ * mode: "highpass",
2393
+ * cutoffFrequency: 500,
2394
+ * sampleRate: 8000,
2395
+ * order: 2
2396
+ * });
2397
+ *
2398
+ * @example
2233
2399
  * // Butterworth band-pass filter
2234
2400
  * pipeline.filter({
2235
2401
  * type: "butterworth",
@@ -2292,8 +2458,10 @@ class DspProcessor {
2292
2458
  filterInstance = this.createBiquadFilter(options);
2293
2459
  break;
2294
2460
  case "iir":
2461
+ filterInstance = this.createIirFilter(options);
2462
+ break;
2295
2463
  default:
2296
- throw new Error(`Filter type "${options.type}" not yet implemented for pipeline chaining. Use standalone filter methods instead.`);
2464
+ throw new Error(`Filter type not supported. Valid types: 'fir', 'iir', 'butterworth', 'chebyshev', 'biquad'`);
2297
2465
  }
2298
2466
  }
2299
2467
  catch (error) {
@@ -2438,6 +2606,76 @@ class DspProcessor {
2438
2606
  throw new Error(`Unsupported Chebyshev filter mode: ${mode}`);
2439
2607
  }
2440
2608
  }
2609
+ /**
2610
+ * Helper to create generic IIR filter from options
2611
+ * Uses first-order filters for order=1, Butterworth otherwise
2612
+ */
2613
+ createIirFilter(options) {
2614
+ const { mode, cutoffFrequency, lowCutoffFrequency, highCutoffFrequency, order, } = options;
2615
+ // Validate order
2616
+ if (!order || order < 1) {
2617
+ throw new Error("IIR filter requires order >= 1");
2618
+ }
2619
+ // For first-order filters, use optimized implementations
2620
+ if (order === 1) {
2621
+ switch (mode) {
2622
+ case "lowpass":
2623
+ if (!cutoffFrequency) {
2624
+ throw new Error("cutoffFrequency required for lowpass filter");
2625
+ }
2626
+ return IirFilter.createFirstOrderLowPass({
2627
+ cutoffFrequency,
2628
+ sampleRate: options.sampleRate,
2629
+ });
2630
+ case "highpass":
2631
+ if (!cutoffFrequency) {
2632
+ throw new Error("cutoffFrequency required for highpass filter");
2633
+ }
2634
+ return IirFilter.createFirstOrderHighPass({
2635
+ cutoffFrequency,
2636
+ sampleRate: options.sampleRate,
2637
+ });
2638
+ default:
2639
+ throw new Error(`First-order IIR filter only supports lowpass and highpass modes. For ${mode}, use order > 1 or a different filter type.`);
2640
+ }
2641
+ }
2642
+ // For higher-order filters, delegate to Butterworth (maximally flat response)
2643
+ switch (mode) {
2644
+ case "lowpass":
2645
+ if (!cutoffFrequency) {
2646
+ throw new Error("cutoffFrequency required for lowpass filter");
2647
+ }
2648
+ return IirFilter.createButterworthLowPass({
2649
+ cutoffFrequency,
2650
+ sampleRate: options.sampleRate,
2651
+ order,
2652
+ });
2653
+ case "highpass":
2654
+ if (!cutoffFrequency) {
2655
+ throw new Error("cutoffFrequency required for highpass filter");
2656
+ }
2657
+ return IirFilter.createButterworthHighPass({
2658
+ cutoffFrequency,
2659
+ sampleRate: options.sampleRate,
2660
+ order,
2661
+ });
2662
+ case "bandpass":
2663
+ if (!lowCutoffFrequency || !highCutoffFrequency) {
2664
+ throw new Error("lowCutoffFrequency and highCutoffFrequency required for bandpass filter");
2665
+ }
2666
+ return IirFilter.createButterworthBandPass({
2667
+ lowCutoffFrequency,
2668
+ highCutoffFrequency,
2669
+ sampleRate: options.sampleRate,
2670
+ order,
2671
+ });
2672
+ case "bandstop":
2673
+ case "notch":
2674
+ throw new Error(`IIR bandstop/notch filters not yet implemented. Use 'butterworth' or 'chebyshev' type instead, or use biquad notch filter.`);
2675
+ default:
2676
+ throw new Error(`Unsupported IIR filter mode: ${mode}`);
2677
+ }
2678
+ }
2441
2679
  /**
2442
2680
  * Helper to create Biquad filter from options
2443
2681
  */
@@ -2552,38 +2790,42 @@ class DspProcessor {
2552
2790
  * @returns Promise that resolves to the processed Float32Array (same reference as input)
2553
2791
  */
2554
2792
  async process(input, timestampsOrOptions, optionsIfTimestamps) {
2793
+ let bufferToProcess;
2794
+ let inferredChannels;
2795
+ if (Array.isArray(input)) {
2796
+ inferredChannels = input.length;
2797
+ bufferToProcess = DspUtils.interleave(input);
2798
+ }
2799
+ else {
2800
+ bufferToProcess = input;
2801
+ }
2555
2802
  let timestamps;
2556
2803
  let options;
2804
+ if (!(bufferToProcess instanceof Float32Array)) {
2805
+ throw new TypeError(`Input samples must be a Float32Array, got ${typeof input}`);
2806
+ }
2557
2807
  // Detect which overload was called
2558
2808
  if (timestampsOrOptions instanceof Float32Array) {
2559
2809
  // Time-based mode: process(samples, timestamps, options)
2560
2810
  timestamps = timestampsOrOptions;
2561
2811
  options = { channels: 1, ...optionsIfTimestamps };
2562
- if (timestamps.length !== input.length) {
2563
- throw new Error(`Timestamps length (${timestamps.length}) must match samples length (${input.length})`);
2812
+ if (timestamps.length !== bufferToProcess.length) {
2813
+ throw new Error(`Timestamps length (${timestamps.length}) must match samples length (${bufferToProcess.length})`);
2564
2814
  }
2565
2815
  }
2566
2816
  else {
2567
- // Sample-based mode or auto-timestamps: process(samples, options)
2568
- options = { channels: 1, ...timestampsOrOptions };
2569
- if (options.sampleRate) {
2570
- // Legacy sample-based mode: auto-generate timestamps from sampleRate
2571
- const dt = 1000 / options.sampleRate; // milliseconds per sample
2572
- timestamps = new Float32Array(input.length);
2573
- for (let i = 0; i < input.length; i++) {
2574
- timestamps[i] = i * dt;
2575
- }
2576
- }
2577
- else {
2578
- // Auto-generate sequential timestamps [0, 1, 2, ...]
2579
- timestamps = new Float32Array(input.length);
2580
- for (let i = 0; i < input.length; i++) {
2581
- timestamps[i] = i;
2582
- }
2583
- }
2817
+ // Sample-based mode: process(samples, options)
2818
+ // Fix: ensure options is correctly formed even if arg is undefined/null
2819
+ options = { channels: 1, ...(timestampsOrOptions || {}) };
2820
+ // Optimization: Pass undefined timestamps, let C++ generate them
2821
+ }
2822
+ // If using planar input, ensure channels option is set correctly
2823
+ if (inferredChannels && options.channels === 1) {
2824
+ options.channels = inferredChannels;
2584
2825
  }
2585
2826
  const startTime = performance.now();
2586
2827
  // Initialize drift detection if enabled
2828
+ // Note: If timestamps are implicit (undefined), drift is 0 by definition, so we skip this.
2587
2829
  if (options.enableDriftDetection && timestamps && options.sampleRate) {
2588
2830
  if (!this.driftDetector ||
2589
2831
  this.driftDetector.getExpectedSampleRate() !== options.sampleRate) {
@@ -2600,14 +2842,22 @@ class DspProcessor {
2600
2842
  try {
2601
2843
  // Pool the start log
2602
2844
  this.poolLog("debug", "Starting pipeline processing", {
2603
- sampleCount: input.length,
2845
+ sampleCount: bufferToProcess.length,
2604
2846
  channels: options.channels,
2605
2847
  stages: this.stages.length,
2606
2848
  mode: options.sampleRate ? "sample-based" : "time-based",
2607
2849
  });
2608
2850
  // Call native process with timestamps
2609
2851
  // Note: The input buffer is modified in-place for zero-copy performance
2610
- const result = await this.nativeInstance.process(input, timestamps, options);
2852
+ let result;
2853
+ if (timestamps) {
2854
+ // Explicit timestamps provided
2855
+ result = await this.nativeInstance.process(bufferToProcess, timestamps, options);
2856
+ }
2857
+ else {
2858
+ // Implicit timestamps -> C++ generates them
2859
+ result = await this.nativeInstance.process(bufferToProcess, options);
2860
+ }
2611
2861
  // Execute tap callbacks for debugging/inspection
2612
2862
  if (this.tapCallbacks.length > 0) {
2613
2863
  for (const { stageName, callback } of this.tapCallbacks) {
@@ -2673,10 +2923,10 @@ class DspProcessor {
2673
2923
  }
2674
2924
  }
2675
2925
  /**
2676
- * Process a copy of the audio data through the DSP pipeline
2926
+ * Process a copy of the input data through the DSP pipeline
2677
2927
  * This method creates a copy of the input, so the original is preserved
2678
2928
  *
2679
- * @param input - Float32Array containing interleaved audio samples (original is preserved)
2929
+ * @param input - Float32Array containing interleaved input samples (original is preserved)
2680
2930
  * @param timestampsOrOptions - Either timestamps array or processing options (sample rate and channel count)
2681
2931
  * @param optionsIfTimestamps - Processing options if timestamps were provided in second parameter
2682
2932
  * @returns Promise that resolves to a new Float32Array with the processed data
@@ -2691,7 +2941,9 @@ class DspProcessor {
2691
2941
  */
2692
2942
  async processCopy(input, timestampsOrOptions, optionsIfTimestamps) {
2693
2943
  // Create a copy to preserve the original
2694
- const copy = new Float32Array(input);
2944
+ const copy = Array.isArray(input)
2945
+ ? DspUtils.interleave(input)
2946
+ : new Float32Array(input);
2695
2947
  // Handle both overloaded signatures by delegating to process()
2696
2948
  if (timestampsOrOptions instanceof Float32Array) {
2697
2949
  // Time-based mode: process(samples, timestamps, options)
@@ -2704,33 +2956,245 @@ class DspProcessor {
2704
2956
  }
2705
2957
  }
2706
2958
  /**
2707
- * Save the current pipeline state as a JSON string
2708
- * TypeScript can then store this in Redis or other persistent storage
2959
+ * Process data synchronously (BLOCKING).
2960
+ * * ⚠️ PERFORMANCE WARNING:
2961
+ * Only use this method inside Worker Threads or Cluster Workers.
2962
+ * Using this on the Main Thread of a server will block the Event Loop.
2963
+ * * This method bypasses the libuv thread pool, running DSP logic directly
2964
+ * on the calling thread. Ideal for high-frequency real-time audio loops.
2965
+ * * @param input - Float32Array containing samples (modified in-place if no resizing)
2966
+ * @param timestampsOrOptions - Timestamps array or options object
2967
+ * @param optionsIfTimestamps - Options object if timestamps provided
2968
+ */
2969
+ processSync(input, timestampsOrOptions, optionsIfTimestamps) {
2970
+ let bufferToProcess;
2971
+ let inferredChannels;
2972
+ if (Array.isArray(input)) {
2973
+ inferredChannels = input.length;
2974
+ bufferToProcess = DspUtils.interleave(input);
2975
+ }
2976
+ else {
2977
+ bufferToProcess = input;
2978
+ }
2979
+ let timestamps;
2980
+ let options;
2981
+ if (!(bufferToProcess instanceof Float32Array)) {
2982
+ throw new TypeError(`Input samples must be a Float32Array, got ${typeof input}`);
2983
+ }
2984
+ // Detect which overload was called
2985
+ if (timestampsOrOptions instanceof Float32Array) {
2986
+ // Time-based mode: process(samples, timestamps, options)
2987
+ timestamps = timestampsOrOptions;
2988
+ options = { channels: 1, ...optionsIfTimestamps };
2989
+ if (timestamps.length !== bufferToProcess.length) {
2990
+ throw new Error(`Timestamps length (${timestamps.length}) must match samples length (${bufferToProcess.length})`);
2991
+ }
2992
+ }
2993
+ else {
2994
+ // Sample-based mode: process(samples, options)
2995
+ // Fix: ensure options is correctly formed even if arg is undefined/null
2996
+ options = { channels: 1, ...(timestampsOrOptions || {}) };
2997
+ // Optimization: Pass undefined timestamps, let C++ generate them
2998
+ }
2999
+ // If using planar input, ensure channels option is set correctly
3000
+ if (inferredChannels && options.channels === 1) {
3001
+ options.channels = inferredChannels;
3002
+ }
3003
+ const startTime = performance.now();
3004
+ // Initialize drift detection if enabled
3005
+ // Note: If timestamps are implicit (undefined), drift is 0 by definition, so we skip this.
3006
+ if (options.enableDriftDetection && timestamps && options.sampleRate) {
3007
+ if (!this.driftDetector ||
3008
+ this.driftDetector.getExpectedSampleRate() !== options.sampleRate) {
3009
+ // Create new detector if it doesn't exist or sample rate changed
3010
+ this.driftDetector = new DriftDetector({
3011
+ expectedSampleRate: options.sampleRate,
3012
+ driftThreshold: options.driftThreshold ?? 10,
3013
+ onDriftDetected: options.onDriftDetected,
3014
+ });
3015
+ }
3016
+ // Process timestamps to detect drift
3017
+ this.driftDetector.processBatch(timestamps);
3018
+ }
3019
+ try {
3020
+ // Pool the start log
3021
+ this.poolLog("debug", "Starting pipeline processing", {
3022
+ sampleCount: input.length,
3023
+ channels: options.channels,
3024
+ stages: this.stages.length,
3025
+ mode: options.sampleRate ? "sample-based" : "time-based",
3026
+ });
3027
+ // Call native process with timestamps
3028
+ // Note: The input buffer is modified in-place for zero-copy performance
3029
+ let result;
3030
+ if (timestamps) {
3031
+ // Explicit timestamps provided
3032
+ result = this.nativeInstance.processSync(bufferToProcess, timestamps, options);
3033
+ }
3034
+ else {
3035
+ // Implicit timestamps -> C++ generates them
3036
+ result = this.nativeInstance.processSync(bufferToProcess, options);
3037
+ }
3038
+ // Execute tap callbacks for debugging/inspection
3039
+ if (this.tapCallbacks.length > 0) {
3040
+ for (const { stageName, callback } of this.tapCallbacks) {
3041
+ try {
3042
+ callback(result, stageName);
3043
+ }
3044
+ catch (tapError) {
3045
+ // Don't let tap errors break the pipeline
3046
+ console.error(`Tap callback error at ${stageName}:`, tapError);
3047
+ }
3048
+ }
3049
+ }
3050
+ // Execute onBatch callback (efficient - one call per process)
3051
+ if (this.callbacks?.onBatch) {
3052
+ const stageName = this.stages.join(" → ") || "pipeline";
3053
+ const batch = {
3054
+ stage: stageName,
3055
+ samples: result,
3056
+ startIndex: 0,
3057
+ count: result.length,
3058
+ };
3059
+ this.callbacks.onBatch(batch);
3060
+ }
3061
+ // Execute onSample callbacks if provided (LEGACY - expensive)
3062
+ // WARNING: This can be expensive for large buffers
3063
+ if (this.callbacks?.onSample) {
3064
+ const stageName = this.stages.join(" → ") || "pipeline";
3065
+ for (let i = 0; i < result.length; i++) {
3066
+ this.callbacks.onSample(result[i], i, stageName);
3067
+ }
3068
+ }
3069
+ // Execute onStageComplete callback
3070
+ if (this.callbacks?.onStageComplete) {
3071
+ const duration = performance.now() - startTime;
3072
+ const pipelineName = this.stages.join(" → ") || "pipeline";
3073
+ this.callbacks.onStageComplete(pipelineName, duration);
3074
+ }
3075
+ // Pool the completion log
3076
+ const duration = performance.now() - startTime;
3077
+ this.poolLog("info", "Pipeline processing completed", {
3078
+ durationMs: duration,
3079
+ sampleCount: result.length,
3080
+ });
3081
+ // Flush all pooled logs at the end
3082
+ this.flushLogs();
3083
+ return result;
3084
+ }
3085
+ catch (error) {
3086
+ const err = error;
3087
+ // Execute onError callback
3088
+ if (this.callbacks?.onError) {
3089
+ const pipelineName = this.stages.join(" → ") || "pipeline";
3090
+ this.callbacks.onError(pipelineName, err);
3091
+ }
3092
+ // Pool the error log
3093
+ this.poolLog("error", "Pipeline processing failed", {
3094
+ error: err.message,
3095
+ stack: err.stack,
3096
+ });
3097
+ // Flush logs even on error
3098
+ this.flushLogs();
3099
+ throw error;
3100
+ }
3101
+ }
3102
+ /**
3103
+ * Process a copy of the input data through the DSP pipeline
3104
+ * This method creates a copy of the input, so the original is preserved
3105
+ * * ⚠️ PERFORMANCE WARNING:
3106
+ * Only use this method inside Worker Threads or Cluster Workers.
3107
+ * Using this on the Main Thread of a server will block the Event Loop.
3108
+ * * This method bypasses the libuv thread pool, running DSP logic directly
3109
+ * on the calling thread. Ideal for high-frequency real-time audio loops.
3110
+ * @param input - Float32Array containing interleaved input samples (original is preserved)
3111
+ * @param timestampsOrOptions - Either timestamps array or processing options (sample rate and channel count)
3112
+ * @param optionsIfTimestamps - Processing options if timestamps were provided in second parameter
3113
+ * @returns Promise that resolves to a new Float32Array with the processed data
3114
+ *
3115
+ * @example
3116
+ * // Legacy sample-based (original preserved)
3117
+ * const output = await pipeline.processSyncCopy(samples, { sampleRate: 100, channels: 1 });
3118
+ *
3119
+ * @example
3120
+ * // Time-based with explicit timestamps (original preserved)
3121
+ * const output = await pipeline.processSyncCopy(samples, timestamps, { channels: 1 });
3122
+ */
3123
+ processSyncCopy(input, timestampsOrOptions, optionsIfTimestamps) {
3124
+ const copy = Array.isArray(input)
3125
+ ? DspUtils.interleave(input)
3126
+ : new Float32Array(input);
3127
+ // Handle both overloaded signatures by delegating to process()
3128
+ if (timestampsOrOptions instanceof Float32Array) {
3129
+ // Time-based mode: process(samples, timestamps, options)
3130
+ const timestampsCopy = new Float32Array(timestampsOrOptions);
3131
+ return this.processSync(copy, timestampsCopy, optionsIfTimestamps);
3132
+ }
3133
+ else {
3134
+ // Legacy mode: process(samples, options)
3135
+ return this.processSync(copy, timestampsOrOptions);
3136
+ }
3137
+ }
3138
+ /**
3139
+ * @brief Dispose of the DSP pipeline and free native resources
3140
+ * After calling this method, the instance should not be used again
3141
+ *
3142
+ * @example
3143
+ * const pipeline = createDspPipeline()
3144
+ * .MovingAverage({ mode: "moving", windowSize: 100 })
3145
+ * .Rectify({ mode: 'full' })
3146
+ * const output = await pipeline.process(samples, { sampleRate: 1000, channels: 1 });
3147
+ * pipeline.dispose();
3148
+ * // Free resources when done, pipeline cannot be used after this for processing the input signal since the stages have been disposed
3149
+ */
3150
+ dispose() {
3151
+ this.nativeInstance.dispose();
3152
+ }
3153
+ /**
3154
+ * Save the current pipeline state
3155
+ * Supports two formats:
3156
+ * - JSON (default): Returns a string for text-based storage
3157
+ * - TOON: Returns a Buffer for binary serialization (60-70% smaller, faster)
2709
3158
  *
2710
- * @returns JSON string containing the pipeline state
3159
+ * @param options - Optional configuration with format: 'json' | 'toon'
3160
+ * @returns JSON string (default) or Buffer (if format: 'toon')
2711
3161
  *
2712
3162
  * @example
3163
+ * // Default JSON format
2713
3164
  * const stateJson = await pipeline.saveState();
2714
3165
  * await redis.set('dsp:state', stateJson);
3166
+ *
3167
+ * @example
3168
+ * // TOON binary format (smaller, faster)
3169
+ * const stateBinary = await pipeline.saveState({ format: 'toon' });
3170
+ * await redis.set('dsp:state', stateBinary);
2715
3171
  */
2716
- async saveState() {
2717
- return this.nativeInstance.saveState();
3172
+ async saveState(options) {
3173
+ return this.nativeInstance.saveState(options);
2718
3174
  }
2719
3175
  /**
2720
- * Load pipeline state from a JSON string
2721
- * TypeScript retrieves this from Redis and passes it to restore state
3176
+ * Load pipeline state from JSON string or TOON binary Buffer
3177
+ * Auto-detects format: Buffer TOON, string JSON
2722
3178
  *
2723
- * @param stateJson - JSON string containing the pipeline state
3179
+ * @param state - JSON string or TOON Buffer containing the pipeline state
2724
3180
  * @returns Promise that resolves to true if successful
2725
3181
  *
2726
3182
  * @example
3183
+ * // Load JSON state
2727
3184
  * const stateJson = await redis.get('dsp:state');
2728
3185
  * if (stateJson) {
2729
3186
  * await pipeline.loadState(stateJson);
2730
3187
  * }
3188
+ *
3189
+ * @example
3190
+ * // Load TOON binary state (auto-detected)
3191
+ * const stateBinary = await redis.getBuffer('dsp:state');
3192
+ * if (stateBinary) {
3193
+ * await pipeline.loadState(stateBinary);
3194
+ * }
2731
3195
  */
2732
- async loadState(stateJson) {
2733
- return this.nativeInstance.loadState(stateJson);
3196
+ async loadState(state) {
3197
+ return this.nativeInstance.loadState(state);
2734
3198
  }
2735
3199
  /**
2736
3200
  * Clear all pipeline state (reset all filters to initial state)
@@ -2778,18 +3242,14 @@ class DspProcessor {
2778
3242
  *
2779
3243
  * @example
2780
3244
  * // Create pipeline with Redis state persistence
2781
- * const pipeline = createDspPipeline({
2782
- * redisHost: 'localhost',
2783
- * redisPort: 6379,
2784
- * stateKey: 'dsp:channel1'
2785
- * });
3245
+ * const pipeline = createDspPipeline();
2786
3246
  *
2787
3247
  * @example
2788
3248
  * // Create pipeline without Redis (state is not persisted)
2789
3249
  * const pipeline = createDspPipeline();
2790
3250
  */
2791
- export function createDspPipeline(config) {
2792
- const nativeInstance = new DspAddon.DspPipeline(config);
3251
+ export function createDspPipeline() {
3252
+ const nativeInstance = new DspAddon.DspPipeline();
2793
3253
  return new DspProcessor(nativeInstance);
2794
3254
  }
2795
3255
  export { DspProcessor };
@@ -3215,4 +3675,5 @@ export function calculateCommonSpatialPatterns(dataClass1, dataClass2, numChanne
3215
3675
  }
3216
3676
  return DspAddon.calculateCommonSpatialPatterns(dataClass1, dataClass2, numChannels, numFilters);
3217
3677
  }
3678
+ export { FilterBankDesign } from "./FilterBankDesign.js";
3218
3679
  //# sourceMappingURL=bindings.js.map