dspx 1.2.4 → 1.3.1

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 (74) 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 +270 -17
  16. package/dist/bindings.d.ts.map +1 -1
  17. package/dist/bindings.js +566 -43
  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 +699 -126
  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/AmplifyStage.h +148 -0
  38. package/src/native/adapters/ClipDetectionStage.h +15 -4
  39. package/src/native/adapters/ConvolutionStage.h +101 -0
  40. package/src/native/adapters/CumulativeMovingAverageStage.h +264 -0
  41. package/src/native/adapters/DecimatorStage.h +80 -0
  42. package/src/native/adapters/DifferentiatorStage.h +13 -0
  43. package/src/native/adapters/ExponentialMovingAverageStage.h +290 -0
  44. package/src/native/adapters/FilterBankStage.cc +336 -0
  45. package/src/native/adapters/FilterBankStage.h +170 -0
  46. package/src/native/adapters/FilterStage.cc +122 -0
  47. package/src/native/adapters/FilterStage.h +4 -0
  48. package/src/native/adapters/HilbertEnvelopeStage.h +55 -0
  49. package/src/native/adapters/IntegratorStage.h +15 -0
  50. package/src/native/adapters/InterpolatorStage.h +51 -0
  51. package/src/native/adapters/LinearRegressionStage.h +40 -0
  52. package/src/native/adapters/LmsStage.h +63 -0
  53. package/src/native/adapters/MeanAbsoluteValueStage.h +76 -0
  54. package/src/native/adapters/MovingAverageStage.h +119 -0
  55. package/src/native/adapters/PeakDetectionStage.h +53 -0
  56. package/src/native/adapters/RectifyStage.h +14 -0
  57. package/src/native/adapters/ResamplerStage.h +67 -0
  58. package/src/native/adapters/RlsStage.h +76 -0
  59. package/src/native/adapters/RmsStage.h +72 -0
  60. package/src/native/adapters/SnrStage.h +45 -0
  61. package/src/native/adapters/SquareStage.h +78 -0
  62. package/src/native/adapters/SscStage.h +65 -0
  63. package/src/native/adapters/StftStage.h +62 -0
  64. package/src/native/adapters/VarianceStage.h +59 -0
  65. package/src/native/adapters/WampStage.h +59 -0
  66. package/src/native/adapters/WaveformLengthStage.h +51 -0
  67. package/src/native/adapters/ZScoreNormalizeStage.h +64 -0
  68. package/src/native/core/CumulativeMovingAverageFilter.h +123 -0
  69. package/src/native/core/ExponentialMovingAverageFilter.h +129 -0
  70. package/src/native/core/FilterBankDesign.h +266 -0
  71. package/src/native/core/Policies.h +124 -0
  72. package/src/native/utils/CircularBufferArray.cc +2 -1
  73. package/src/native/utils/SimdOps.h +67 -0
  74. 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
  *
@@ -1401,6 +1557,69 @@ class DspProcessor {
1401
1557
  this.stages.push("differentiator");
1402
1558
  return this;
1403
1559
  }
1560
+ /**
1561
+ * Apply element-wise squaring to the signal.
1562
+ *
1563
+ * Implements: y[n] = x[n]²
1564
+ *
1565
+ * Squaring is commonly used for:
1566
+ * - Energy/power calculation
1567
+ * - Non-linear signal transformation
1568
+ * - Envelope detection
1569
+ * - Part of Pan-Tompkins QRS detection algorithm (ECG)
1570
+ *
1571
+ * **Note:** Amplifies large values and suppresses small ones.
1572
+ * This stage is stateless - no mode selection needed.
1573
+ *
1574
+ * @returns this instance for method chaining
1575
+ *
1576
+ * @example
1577
+ * // Calculate signal power
1578
+ * pipeline.Square();
1579
+ *
1580
+ * @example
1581
+ * // Pan-Tompkins QRS detection
1582
+ * pipeline
1583
+ * .ButterworthBandpass({ lowCutoff: 5, highCutoff: 15, order: 2, sampleRate: 360 })
1584
+ * .Differentiator()
1585
+ * .Square()
1586
+ * .MovingAverage({ mode: "moving", windowSize: 30 });
1587
+ */
1588
+ Square() {
1589
+ this.nativeInstance.addStage("square", {});
1590
+ this.stages.push("square");
1591
+ return this;
1592
+ }
1593
+ /**
1594
+ * Amplify (scale) the signal by a constant gain factor.
1595
+ *
1596
+ * Multiplies all samples by the gain value: y[n] = gain * x[n]
1597
+ *
1598
+ * Use cases:
1599
+ * - Scale signals to appropriate amplitude ranges for peak detection
1600
+ * - Compensate for sensor attenuation
1601
+ * - Normalize signal levels between processing stages
1602
+ *
1603
+ * @param params - Amplification parameters
1604
+ * @param params.gain - Gain factor (must be positive). Default: 1.0
1605
+ * @returns this (for method chaining)
1606
+ *
1607
+ * @example
1608
+ * // Amplify signal by 1000x for better peak detection sensitivity
1609
+ * pipeline.Amplify({ gain: 1000 });
1610
+ *
1611
+ * @example
1612
+ * // Attenuate signal by 50%
1613
+ * pipeline.Amplify({ gain: 0.5 });
1614
+ */
1615
+ Amplify(params) {
1616
+ if (params.gain <= 0) {
1617
+ throw new Error("Amplify gain must be positive");
1618
+ }
1619
+ this.nativeInstance.addStage("amplify", params);
1620
+ this.stages.push(`amplify:${params.gain}`);
1621
+ return this;
1622
+ }
1404
1623
  /**
1405
1624
  * Apply leaky integration (accumulation) using IIR filter.
1406
1625
  *
@@ -2230,6 +2449,16 @@ class DspProcessor {
2230
2449
  * });
2231
2450
  *
2232
2451
  * @example
2452
+ * // Generic IIR filter (uses Butterworth internally)
2453
+ * pipeline.filter({
2454
+ * type: "iir",
2455
+ * mode: "highpass",
2456
+ * cutoffFrequency: 500,
2457
+ * sampleRate: 8000,
2458
+ * order: 2
2459
+ * });
2460
+ *
2461
+ * @example
2233
2462
  * // Butterworth band-pass filter
2234
2463
  * pipeline.filter({
2235
2464
  * type: "butterworth",
@@ -2292,9 +2521,10 @@ class DspProcessor {
2292
2521
  filterInstance = this.createBiquadFilter(options);
2293
2522
  break;
2294
2523
  case "iir":
2524
+ filterInstance = this.createIirFilter(options);
2525
+ break;
2295
2526
  default:
2296
- //TODO: Implement this
2297
- throw new Error(`Filter type "${options.type}" not yet implemented for pipeline chaining. Use standalone filter methods instead.`);
2527
+ throw new Error(`Filter type not supported. Valid types: 'fir', 'iir', 'butterworth', 'chebyshev', 'biquad'`);
2298
2528
  }
2299
2529
  }
2300
2530
  catch (error) {
@@ -2439,6 +2669,76 @@ class DspProcessor {
2439
2669
  throw new Error(`Unsupported Chebyshev filter mode: ${mode}`);
2440
2670
  }
2441
2671
  }
2672
+ /**
2673
+ * Helper to create generic IIR filter from options
2674
+ * Uses first-order filters for order=1, Butterworth otherwise
2675
+ */
2676
+ createIirFilter(options) {
2677
+ const { mode, cutoffFrequency, lowCutoffFrequency, highCutoffFrequency, order, } = options;
2678
+ // Validate order
2679
+ if (!order || order < 1) {
2680
+ throw new Error("IIR filter requires order >= 1");
2681
+ }
2682
+ // For first-order filters, use optimized implementations
2683
+ if (order === 1) {
2684
+ switch (mode) {
2685
+ case "lowpass":
2686
+ if (!cutoffFrequency) {
2687
+ throw new Error("cutoffFrequency required for lowpass filter");
2688
+ }
2689
+ return IirFilter.createFirstOrderLowPass({
2690
+ cutoffFrequency,
2691
+ sampleRate: options.sampleRate,
2692
+ });
2693
+ case "highpass":
2694
+ if (!cutoffFrequency) {
2695
+ throw new Error("cutoffFrequency required for highpass filter");
2696
+ }
2697
+ return IirFilter.createFirstOrderHighPass({
2698
+ cutoffFrequency,
2699
+ sampleRate: options.sampleRate,
2700
+ });
2701
+ default:
2702
+ throw new Error(`First-order IIR filter only supports lowpass and highpass modes. For ${mode}, use order > 1 or a different filter type.`);
2703
+ }
2704
+ }
2705
+ // For higher-order filters, delegate to Butterworth (maximally flat response)
2706
+ switch (mode) {
2707
+ case "lowpass":
2708
+ if (!cutoffFrequency) {
2709
+ throw new Error("cutoffFrequency required for lowpass filter");
2710
+ }
2711
+ return IirFilter.createButterworthLowPass({
2712
+ cutoffFrequency,
2713
+ sampleRate: options.sampleRate,
2714
+ order,
2715
+ });
2716
+ case "highpass":
2717
+ if (!cutoffFrequency) {
2718
+ throw new Error("cutoffFrequency required for highpass filter");
2719
+ }
2720
+ return IirFilter.createButterworthHighPass({
2721
+ cutoffFrequency,
2722
+ sampleRate: options.sampleRate,
2723
+ order,
2724
+ });
2725
+ case "bandpass":
2726
+ if (!lowCutoffFrequency || !highCutoffFrequency) {
2727
+ throw new Error("lowCutoffFrequency and highCutoffFrequency required for bandpass filter");
2728
+ }
2729
+ return IirFilter.createButterworthBandPass({
2730
+ lowCutoffFrequency,
2731
+ highCutoffFrequency,
2732
+ sampleRate: options.sampleRate,
2733
+ order,
2734
+ });
2735
+ case "bandstop":
2736
+ case "notch":
2737
+ throw new Error(`IIR bandstop/notch filters not yet implemented. Use 'butterworth' or 'chebyshev' type instead, or use biquad notch filter.`);
2738
+ default:
2739
+ throw new Error(`Unsupported IIR filter mode: ${mode}`);
2740
+ }
2741
+ }
2442
2742
  /**
2443
2743
  * Helper to create Biquad filter from options
2444
2744
  */
@@ -2553,38 +2853,42 @@ class DspProcessor {
2553
2853
  * @returns Promise that resolves to the processed Float32Array (same reference as input)
2554
2854
  */
2555
2855
  async process(input, timestampsOrOptions, optionsIfTimestamps) {
2856
+ let bufferToProcess;
2857
+ let inferredChannels;
2858
+ if (Array.isArray(input)) {
2859
+ inferredChannels = input.length;
2860
+ bufferToProcess = DspUtils.interleave(input);
2861
+ }
2862
+ else {
2863
+ bufferToProcess = input;
2864
+ }
2556
2865
  let timestamps;
2557
2866
  let options;
2867
+ if (!(bufferToProcess instanceof Float32Array)) {
2868
+ throw new TypeError(`Input samples must be a Float32Array, got ${typeof input}`);
2869
+ }
2558
2870
  // Detect which overload was called
2559
2871
  if (timestampsOrOptions instanceof Float32Array) {
2560
2872
  // Time-based mode: process(samples, timestamps, options)
2561
2873
  timestamps = timestampsOrOptions;
2562
2874
  options = { channels: 1, ...optionsIfTimestamps };
2563
- if (timestamps.length !== input.length) {
2564
- throw new Error(`Timestamps length (${timestamps.length}) must match samples length (${input.length})`);
2875
+ if (timestamps.length !== bufferToProcess.length) {
2876
+ throw new Error(`Timestamps length (${timestamps.length}) must match samples length (${bufferToProcess.length})`);
2565
2877
  }
2566
2878
  }
2567
2879
  else {
2568
- // Sample-based mode or auto-timestamps: process(samples, options)
2569
- options = { channels: 1, ...timestampsOrOptions };
2570
- if (options.sampleRate) {
2571
- // Legacy sample-based mode: auto-generate timestamps from sampleRate
2572
- const dt = 1000 / options.sampleRate; // milliseconds per sample
2573
- timestamps = new Float32Array(input.length);
2574
- for (let i = 0; i < input.length; i++) {
2575
- timestamps[i] = i * dt;
2576
- }
2577
- }
2578
- else {
2579
- // Auto-generate sequential timestamps [0, 1, 2, ...]
2580
- timestamps = new Float32Array(input.length);
2581
- for (let i = 0; i < input.length; i++) {
2582
- timestamps[i] = i;
2583
- }
2584
- }
2880
+ // Sample-based mode: process(samples, options)
2881
+ // Fix: ensure options is correctly formed even if arg is undefined/null
2882
+ options = { channels: 1, ...(timestampsOrOptions || {}) };
2883
+ // Optimization: Pass undefined timestamps, let C++ generate them
2884
+ }
2885
+ // If using planar input, ensure channels option is set correctly
2886
+ if (inferredChannels && options.channels === 1) {
2887
+ options.channels = inferredChannels;
2585
2888
  }
2586
2889
  const startTime = performance.now();
2587
2890
  // Initialize drift detection if enabled
2891
+ // Note: If timestamps are implicit (undefined), drift is 0 by definition, so we skip this.
2588
2892
  if (options.enableDriftDetection && timestamps && options.sampleRate) {
2589
2893
  if (!this.driftDetector ||
2590
2894
  this.driftDetector.getExpectedSampleRate() !== options.sampleRate) {
@@ -2601,14 +2905,22 @@ class DspProcessor {
2601
2905
  try {
2602
2906
  // Pool the start log
2603
2907
  this.poolLog("debug", "Starting pipeline processing", {
2604
- sampleCount: input.length,
2908
+ sampleCount: bufferToProcess.length,
2605
2909
  channels: options.channels,
2606
2910
  stages: this.stages.length,
2607
2911
  mode: options.sampleRate ? "sample-based" : "time-based",
2608
2912
  });
2609
2913
  // Call native process with timestamps
2610
2914
  // Note: The input buffer is modified in-place for zero-copy performance
2611
- const result = await this.nativeInstance.process(input, timestamps, options);
2915
+ let result;
2916
+ if (timestamps) {
2917
+ // Explicit timestamps provided
2918
+ result = await this.nativeInstance.process(bufferToProcess, timestamps, options);
2919
+ }
2920
+ else {
2921
+ // Implicit timestamps -> C++ generates them
2922
+ result = await this.nativeInstance.process(bufferToProcess, options);
2923
+ }
2612
2924
  // Execute tap callbacks for debugging/inspection
2613
2925
  if (this.tapCallbacks.length > 0) {
2614
2926
  for (const { stageName, callback } of this.tapCallbacks) {
@@ -2674,10 +2986,10 @@ class DspProcessor {
2674
2986
  }
2675
2987
  }
2676
2988
  /**
2677
- * Process a copy of the audio data through the DSP pipeline
2989
+ * Process a copy of the input data through the DSP pipeline
2678
2990
  * This method creates a copy of the input, so the original is preserved
2679
2991
  *
2680
- * @param input - Float32Array containing interleaved audio samples (original is preserved)
2992
+ * @param input - Float32Array containing interleaved input samples (original is preserved)
2681
2993
  * @param timestampsOrOptions - Either timestamps array or processing options (sample rate and channel count)
2682
2994
  * @param optionsIfTimestamps - Processing options if timestamps were provided in second parameter
2683
2995
  * @returns Promise that resolves to a new Float32Array with the processed data
@@ -2692,7 +3004,9 @@ class DspProcessor {
2692
3004
  */
2693
3005
  async processCopy(input, timestampsOrOptions, optionsIfTimestamps) {
2694
3006
  // Create a copy to preserve the original
2695
- const copy = new Float32Array(input);
3007
+ const copy = Array.isArray(input)
3008
+ ? DspUtils.interleave(input)
3009
+ : new Float32Array(input);
2696
3010
  // Handle both overloaded signatures by delegating to process()
2697
3011
  if (timestampsOrOptions instanceof Float32Array) {
2698
3012
  // Time-based mode: process(samples, timestamps, options)
@@ -2705,33 +3019,245 @@ class DspProcessor {
2705
3019
  }
2706
3020
  }
2707
3021
  /**
2708
- * Save the current pipeline state as a JSON string
2709
- * TypeScript can then store this in Redis or other persistent storage
3022
+ * Process data synchronously (BLOCKING).
3023
+ * * ⚠️ PERFORMANCE WARNING:
3024
+ * Only use this method inside Worker Threads or Cluster Workers.
3025
+ * Using this on the Main Thread of a server will block the Event Loop.
3026
+ * * This method bypasses the libuv thread pool, running DSP logic directly
3027
+ * on the calling thread. Ideal for high-frequency real-time audio loops.
3028
+ * * @param input - Float32Array containing samples (modified in-place if no resizing)
3029
+ * @param timestampsOrOptions - Timestamps array or options object
3030
+ * @param optionsIfTimestamps - Options object if timestamps provided
3031
+ */
3032
+ processSync(input, timestampsOrOptions, optionsIfTimestamps) {
3033
+ let bufferToProcess;
3034
+ let inferredChannels;
3035
+ if (Array.isArray(input)) {
3036
+ inferredChannels = input.length;
3037
+ bufferToProcess = DspUtils.interleave(input);
3038
+ }
3039
+ else {
3040
+ bufferToProcess = input;
3041
+ }
3042
+ let timestamps;
3043
+ let options;
3044
+ if (!(bufferToProcess instanceof Float32Array)) {
3045
+ throw new TypeError(`Input samples must be a Float32Array, got ${typeof input}`);
3046
+ }
3047
+ // Detect which overload was called
3048
+ if (timestampsOrOptions instanceof Float32Array) {
3049
+ // Time-based mode: process(samples, timestamps, options)
3050
+ timestamps = timestampsOrOptions;
3051
+ options = { channels: 1, ...optionsIfTimestamps };
3052
+ if (timestamps.length !== bufferToProcess.length) {
3053
+ throw new Error(`Timestamps length (${timestamps.length}) must match samples length (${bufferToProcess.length})`);
3054
+ }
3055
+ }
3056
+ else {
3057
+ // Sample-based mode: process(samples, options)
3058
+ // Fix: ensure options is correctly formed even if arg is undefined/null
3059
+ options = { channels: 1, ...(timestampsOrOptions || {}) };
3060
+ // Optimization: Pass undefined timestamps, let C++ generate them
3061
+ }
3062
+ // If using planar input, ensure channels option is set correctly
3063
+ if (inferredChannels && options.channels === 1) {
3064
+ options.channels = inferredChannels;
3065
+ }
3066
+ const startTime = performance.now();
3067
+ // Initialize drift detection if enabled
3068
+ // Note: If timestamps are implicit (undefined), drift is 0 by definition, so we skip this.
3069
+ if (options.enableDriftDetection && timestamps && options.sampleRate) {
3070
+ if (!this.driftDetector ||
3071
+ this.driftDetector.getExpectedSampleRate() !== options.sampleRate) {
3072
+ // Create new detector if it doesn't exist or sample rate changed
3073
+ this.driftDetector = new DriftDetector({
3074
+ expectedSampleRate: options.sampleRate,
3075
+ driftThreshold: options.driftThreshold ?? 10,
3076
+ onDriftDetected: options.onDriftDetected,
3077
+ });
3078
+ }
3079
+ // Process timestamps to detect drift
3080
+ this.driftDetector.processBatch(timestamps);
3081
+ }
3082
+ try {
3083
+ // Pool the start log
3084
+ this.poolLog("debug", "Starting pipeline processing", {
3085
+ sampleCount: input.length,
3086
+ channels: options.channels,
3087
+ stages: this.stages.length,
3088
+ mode: options.sampleRate ? "sample-based" : "time-based",
3089
+ });
3090
+ // Call native process with timestamps
3091
+ // Note: The input buffer is modified in-place for zero-copy performance
3092
+ let result;
3093
+ if (timestamps) {
3094
+ // Explicit timestamps provided
3095
+ result = this.nativeInstance.processSync(bufferToProcess, timestamps, options);
3096
+ }
3097
+ else {
3098
+ // Implicit timestamps -> C++ generates them
3099
+ result = this.nativeInstance.processSync(bufferToProcess, options);
3100
+ }
3101
+ // Execute tap callbacks for debugging/inspection
3102
+ if (this.tapCallbacks.length > 0) {
3103
+ for (const { stageName, callback } of this.tapCallbacks) {
3104
+ try {
3105
+ callback(result, stageName);
3106
+ }
3107
+ catch (tapError) {
3108
+ // Don't let tap errors break the pipeline
3109
+ console.error(`Tap callback error at ${stageName}:`, tapError);
3110
+ }
3111
+ }
3112
+ }
3113
+ // Execute onBatch callback (efficient - one call per process)
3114
+ if (this.callbacks?.onBatch) {
3115
+ const stageName = this.stages.join(" → ") || "pipeline";
3116
+ const batch = {
3117
+ stage: stageName,
3118
+ samples: result,
3119
+ startIndex: 0,
3120
+ count: result.length,
3121
+ };
3122
+ this.callbacks.onBatch(batch);
3123
+ }
3124
+ // Execute onSample callbacks if provided (LEGACY - expensive)
3125
+ // WARNING: This can be expensive for large buffers
3126
+ if (this.callbacks?.onSample) {
3127
+ const stageName = this.stages.join(" → ") || "pipeline";
3128
+ for (let i = 0; i < result.length; i++) {
3129
+ this.callbacks.onSample(result[i], i, stageName);
3130
+ }
3131
+ }
3132
+ // Execute onStageComplete callback
3133
+ if (this.callbacks?.onStageComplete) {
3134
+ const duration = performance.now() - startTime;
3135
+ const pipelineName = this.stages.join(" → ") || "pipeline";
3136
+ this.callbacks.onStageComplete(pipelineName, duration);
3137
+ }
3138
+ // Pool the completion log
3139
+ const duration = performance.now() - startTime;
3140
+ this.poolLog("info", "Pipeline processing completed", {
3141
+ durationMs: duration,
3142
+ sampleCount: result.length,
3143
+ });
3144
+ // Flush all pooled logs at the end
3145
+ this.flushLogs();
3146
+ return result;
3147
+ }
3148
+ catch (error) {
3149
+ const err = error;
3150
+ // Execute onError callback
3151
+ if (this.callbacks?.onError) {
3152
+ const pipelineName = this.stages.join(" → ") || "pipeline";
3153
+ this.callbacks.onError(pipelineName, err);
3154
+ }
3155
+ // Pool the error log
3156
+ this.poolLog("error", "Pipeline processing failed", {
3157
+ error: err.message,
3158
+ stack: err.stack,
3159
+ });
3160
+ // Flush logs even on error
3161
+ this.flushLogs();
3162
+ throw error;
3163
+ }
3164
+ }
3165
+ /**
3166
+ * Process a copy of the input data through the DSP pipeline
3167
+ * This method creates a copy of the input, so the original is preserved
3168
+ * * ⚠️ PERFORMANCE WARNING:
3169
+ * Only use this method inside Worker Threads or Cluster Workers.
3170
+ * Using this on the Main Thread of a server will block the Event Loop.
3171
+ * * This method bypasses the libuv thread pool, running DSP logic directly
3172
+ * on the calling thread. Ideal for high-frequency real-time audio loops.
3173
+ * @param input - Float32Array containing interleaved input samples (original is preserved)
3174
+ * @param timestampsOrOptions - Either timestamps array or processing options (sample rate and channel count)
3175
+ * @param optionsIfTimestamps - Processing options if timestamps were provided in second parameter
3176
+ * @returns Promise that resolves to a new Float32Array with the processed data
2710
3177
  *
2711
- * @returns JSON string containing the pipeline state
3178
+ * @example
3179
+ * // Legacy sample-based (original preserved)
3180
+ * const output = await pipeline.processSyncCopy(samples, { sampleRate: 100, channels: 1 });
2712
3181
  *
2713
3182
  * @example
3183
+ * // Time-based with explicit timestamps (original preserved)
3184
+ * const output = await pipeline.processSyncCopy(samples, timestamps, { channels: 1 });
3185
+ */
3186
+ processSyncCopy(input, timestampsOrOptions, optionsIfTimestamps) {
3187
+ const copy = Array.isArray(input)
3188
+ ? DspUtils.interleave(input)
3189
+ : new Float32Array(input);
3190
+ // Handle both overloaded signatures by delegating to process()
3191
+ if (timestampsOrOptions instanceof Float32Array) {
3192
+ // Time-based mode: process(samples, timestamps, options)
3193
+ const timestampsCopy = new Float32Array(timestampsOrOptions);
3194
+ return this.processSync(copy, timestampsCopy, optionsIfTimestamps);
3195
+ }
3196
+ else {
3197
+ // Legacy mode: process(samples, options)
3198
+ return this.processSync(copy, timestampsOrOptions);
3199
+ }
3200
+ }
3201
+ /**
3202
+ * @brief Dispose of the DSP pipeline and free native resources
3203
+ * After calling this method, the instance should not be used again
3204
+ *
3205
+ * @example
3206
+ * const pipeline = createDspPipeline()
3207
+ * .MovingAverage({ mode: "moving", windowSize: 100 })
3208
+ * .Rectify({ mode: 'full' })
3209
+ * const output = await pipeline.process(samples, { sampleRate: 1000, channels: 1 });
3210
+ * pipeline.dispose();
3211
+ * // Free resources when done, pipeline cannot be used after this for processing the input signal since the stages have been disposed
3212
+ */
3213
+ dispose() {
3214
+ this.nativeInstance.dispose();
3215
+ }
3216
+ /**
3217
+ * Save the current pipeline state
3218
+ * Supports two formats:
3219
+ * - JSON (default): Returns a string for text-based storage
3220
+ * - TOON: Returns a Buffer for binary serialization (60-70% smaller, faster)
3221
+ *
3222
+ * @param options - Optional configuration with format: 'json' | 'toon'
3223
+ * @returns JSON string (default) or Buffer (if format: 'toon')
3224
+ *
3225
+ * @example
3226
+ * // Default JSON format
2714
3227
  * const stateJson = await pipeline.saveState();
2715
3228
  * await redis.set('dsp:state', stateJson);
3229
+ *
3230
+ * @example
3231
+ * // TOON binary format (smaller, faster)
3232
+ * const stateBinary = await pipeline.saveState({ format: 'toon' });
3233
+ * await redis.set('dsp:state', stateBinary);
2716
3234
  */
2717
- async saveState() {
2718
- return this.nativeInstance.saveState();
3235
+ async saveState(options) {
3236
+ return this.nativeInstance.saveState(options);
2719
3237
  }
2720
3238
  /**
2721
- * Load pipeline state from a JSON string
2722
- * TypeScript retrieves this from Redis and passes it to restore state
3239
+ * Load pipeline state from JSON string or TOON binary Buffer
3240
+ * Auto-detects format: Buffer TOON, string JSON
2723
3241
  *
2724
- * @param stateJson - JSON string containing the pipeline state
3242
+ * @param state - JSON string or TOON Buffer containing the pipeline state
2725
3243
  * @returns Promise that resolves to true if successful
2726
3244
  *
2727
3245
  * @example
3246
+ * // Load JSON state
2728
3247
  * const stateJson = await redis.get('dsp:state');
2729
3248
  * if (stateJson) {
2730
3249
  * await pipeline.loadState(stateJson);
2731
3250
  * }
3251
+ *
3252
+ * @example
3253
+ * // Load TOON binary state (auto-detected)
3254
+ * const stateBinary = await redis.getBuffer('dsp:state');
3255
+ * if (stateBinary) {
3256
+ * await pipeline.loadState(stateBinary);
3257
+ * }
2732
3258
  */
2733
- async loadState(stateJson) {
2734
- return this.nativeInstance.loadState(stateJson);
3259
+ async loadState(state) {
3260
+ return this.nativeInstance.loadState(state);
2735
3261
  }
2736
3262
  /**
2737
3263
  * Clear all pipeline state (reset all filters to initial state)
@@ -2779,18 +3305,14 @@ class DspProcessor {
2779
3305
  *
2780
3306
  * @example
2781
3307
  * // Create pipeline with Redis state persistence
2782
- * const pipeline = createDspPipeline({
2783
- * redisHost: 'localhost',
2784
- * redisPort: 6379,
2785
- * stateKey: 'dsp:channel1'
2786
- * });
3308
+ * const pipeline = createDspPipeline();
2787
3309
  *
2788
3310
  * @example
2789
3311
  * // Create pipeline without Redis (state is not persisted)
2790
3312
  * const pipeline = createDspPipeline();
2791
3313
  */
2792
- export function createDspPipeline(config) {
2793
- const nativeInstance = new DspAddon.DspPipeline(config);
3314
+ export function createDspPipeline() {
3315
+ const nativeInstance = new DspAddon.DspPipeline();
2794
3316
  return new DspProcessor(nativeInstance);
2795
3317
  }
2796
3318
  export { DspProcessor };
@@ -3216,4 +3738,5 @@ export function calculateCommonSpatialPatterns(dataClass1, dataClass2, numChanne
3216
3738
  }
3217
3739
  return DspAddon.calculateCommonSpatialPatterns(dataClass1, dataClass2, numChannels, numFilters);
3218
3740
  }
3741
+ export { FilterBankDesign } from "./FilterBankDesign.js";
3219
3742
  //# sourceMappingURL=bindings.js.map