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.
- package/README.md +40 -78
- package/binding.gyp +10 -0
- package/dist/FilterBankDesign.d.ts +233 -0
- package/dist/FilterBankDesign.d.ts.map +1 -0
- package/dist/FilterBankDesign.js +247 -0
- package/dist/FilterBankDesign.js.map +1 -0
- package/dist/advanced-dsp.d.ts +6 -6
- package/dist/advanced-dsp.d.ts.map +1 -1
- package/dist/advanced-dsp.js +35 -12
- package/dist/advanced-dsp.js.map +1 -1
- package/dist/backends.d.ts +0 -103
- package/dist/backends.d.ts.map +1 -1
- package/dist/backends.js +0 -217
- package/dist/backends.js.map +1 -1
- package/dist/bindings.d.ts +270 -17
- package/dist/bindings.d.ts.map +1 -1
- package/dist/bindings.js +566 -43
- package/dist/bindings.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +67 -8
- package/dist/types.d.ts.map +1 -1
- package/dist/utils.d.ts +38 -8
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +84 -26
- package/dist/utils.js.map +1 -1
- package/package.json +1 -2
- package/prebuilds/win32-x64/dspx.node +0 -0
- package/scripts/add-dispose-to-tests.js +145 -0
- package/src/native/DspPipeline.cc +699 -126
- package/src/native/DspPipeline.h +13 -0
- package/src/native/FilterBankDesignBindings.cc +241 -0
- package/src/native/IDspStage.h +24 -0
- package/src/native/UtilityBindings.cc +130 -0
- package/src/native/adapters/AmplifyStage.h +148 -0
- package/src/native/adapters/ClipDetectionStage.h +15 -4
- package/src/native/adapters/ConvolutionStage.h +101 -0
- package/src/native/adapters/CumulativeMovingAverageStage.h +264 -0
- package/src/native/adapters/DecimatorStage.h +80 -0
- package/src/native/adapters/DifferentiatorStage.h +13 -0
- package/src/native/adapters/ExponentialMovingAverageStage.h +290 -0
- package/src/native/adapters/FilterBankStage.cc +336 -0
- package/src/native/adapters/FilterBankStage.h +170 -0
- package/src/native/adapters/FilterStage.cc +122 -0
- package/src/native/adapters/FilterStage.h +4 -0
- package/src/native/adapters/HilbertEnvelopeStage.h +55 -0
- package/src/native/adapters/IntegratorStage.h +15 -0
- package/src/native/adapters/InterpolatorStage.h +51 -0
- package/src/native/adapters/LinearRegressionStage.h +40 -0
- package/src/native/adapters/LmsStage.h +63 -0
- package/src/native/adapters/MeanAbsoluteValueStage.h +76 -0
- package/src/native/adapters/MovingAverageStage.h +119 -0
- package/src/native/adapters/PeakDetectionStage.h +53 -0
- package/src/native/adapters/RectifyStage.h +14 -0
- package/src/native/adapters/ResamplerStage.h +67 -0
- package/src/native/adapters/RlsStage.h +76 -0
- package/src/native/adapters/RmsStage.h +72 -0
- package/src/native/adapters/SnrStage.h +45 -0
- package/src/native/adapters/SquareStage.h +78 -0
- package/src/native/adapters/SscStage.h +65 -0
- package/src/native/adapters/StftStage.h +62 -0
- package/src/native/adapters/VarianceStage.h +59 -0
- package/src/native/adapters/WampStage.h +59 -0
- package/src/native/adapters/WaveformLengthStage.h +51 -0
- package/src/native/adapters/ZScoreNormalizeStage.h +64 -0
- package/src/native/core/CumulativeMovingAverageFilter.h +123 -0
- package/src/native/core/ExponentialMovingAverageFilter.h +129 -0
- package/src/native/core/FilterBankDesign.h +266 -0
- package/src/native/core/Policies.h +124 -0
- package/src/native/utils/CircularBufferArray.cc +2 -1
- package/src/native/utils/SimdOps.h +67 -0
- 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
|
-
|
|
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 !==
|
|
2564
|
-
throw new Error(`Timestamps length (${timestamps.length}) must match samples 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
|
|
2569
|
-
options
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
*
|
|
2709
|
-
*
|
|
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
|
-
* @
|
|
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
|
|
2722
|
-
*
|
|
3239
|
+
* Load pipeline state from JSON string or TOON binary Buffer
|
|
3240
|
+
* Auto-detects format: Buffer → TOON, string → JSON
|
|
2723
3241
|
*
|
|
2724
|
-
* @param
|
|
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(
|
|
2734
|
-
return this.nativeInstance.loadState(
|
|
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(
|
|
2793
|
-
const nativeInstance = new DspAddon.DspPipeline(
|
|
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
|