@buffered-audio/nodes 0.23.0 → 0.24.2
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 +3 -4
- package/dist/{chunk-YJWZP2OD.js → chunk-BKEMKXGG.js} +672 -531
- package/dist/index.d.ts +25 -10
- package/dist/index.js +1002 -927
- package/dist/nlm-worker.js +1 -1
- package/package.json +3 -3
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { createRequire } from 'module';
|
|
2
|
-
import { spawn } from 'child_process';
|
|
3
2
|
|
|
4
3
|
var __create = Object.create;
|
|
5
4
|
var __defProp = Object.defineProperty;
|
|
@@ -229,15 +228,30 @@ function bandpass(channels, sampleRate, highPass, lowPass) {
|
|
|
229
228
|
}
|
|
230
229
|
}
|
|
231
230
|
}
|
|
231
|
+
function getBidirectionalIirAlphas(sampleRate, smoothingMs) {
|
|
232
|
+
if (!Number.isFinite(sampleRate) || sampleRate <= 0) {
|
|
233
|
+
throw new Error(`BidirectionalIir: sampleRate must be positive and finite, got ${sampleRate}`);
|
|
234
|
+
}
|
|
235
|
+
if (!Number.isFinite(smoothingMs)) {
|
|
236
|
+
throw new Error(`BidirectionalIir: smoothingMs must be finite, got ${smoothingMs}`);
|
|
237
|
+
}
|
|
238
|
+
if (smoothingMs <= 0) return { causal: 1, bidirectional: 1 };
|
|
239
|
+
const ratio = 1e3 / sampleRate / smoothingMs;
|
|
240
|
+
const causalPole = Math.exp(-ratio);
|
|
241
|
+
const causal = -Math.expm1(-ratio);
|
|
242
|
+
const omega = Math.min(ratio, Math.PI);
|
|
243
|
+
const sinHalf = Math.sin(omega / 2);
|
|
244
|
+
const causalMagnitude = causal / Math.hypot(causal, 2 * Math.sqrt(causalPole) * sinHalf);
|
|
245
|
+
const transformedFrequency = 2 * sinHalf * Math.sqrt(causalMagnitude / (1 - causalMagnitude));
|
|
246
|
+
const bidirectional = -Math.expm1(-2 * Math.asinh(transformedFrequency / 2));
|
|
247
|
+
return { causal, bidirectional };
|
|
248
|
+
}
|
|
232
249
|
var BidirectionalIir = class {
|
|
233
250
|
constructor(options) {
|
|
234
251
|
this.smoothingMs = options.smoothingMs;
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
this.alphaBidirectional = tauBidirectional > 0 ? 1 - Math.exp(-samplePeriod / tauBidirectional) : 1;
|
|
239
|
-
const tauCausal = this.smoothingMs / 1e3;
|
|
240
|
-
this.alphaCausal = tauCausal > 0 ? 1 - Math.exp(-samplePeriod / tauCausal) : 1;
|
|
252
|
+
const alphas = getBidirectionalIirAlphas(options.sampleRate, options.smoothingMs);
|
|
253
|
+
this.alphaBidirectional = alphas.bidirectional;
|
|
254
|
+
this.alphaCausal = alphas.causal;
|
|
241
255
|
}
|
|
242
256
|
applyBidirectional(input) {
|
|
243
257
|
const output = Float32Array.from(input);
|
|
@@ -318,7 +332,6 @@ var BlockSumAccumulator = class {
|
|
|
318
332
|
this.ringSize = computeRingSize(blockSize, blockStep);
|
|
319
333
|
this.activeBlockSums = new Float64Array(this.ringSize);
|
|
320
334
|
}
|
|
321
|
-
// Consumes `frames` per-frame sums from `perFrameSums[0]` (needs length >= `frames`); block-boundary accounting advances as if appended to one contiguous stream.
|
|
322
335
|
push(perFrameSums, frames) {
|
|
323
336
|
if (this.finalized) {
|
|
324
337
|
throw new Error("BlockSumAccumulator: push() called after finalize()");
|
|
@@ -425,8 +438,8 @@ function getFftAddon(backend, options) {
|
|
|
425
438
|
function interleave(samples, frames, channels) {
|
|
426
439
|
const interleaved = new Float32Array(frames * channels);
|
|
427
440
|
for (let frame = 0; frame < frames; frame++) {
|
|
428
|
-
for (let
|
|
429
|
-
interleaved[frame * channels +
|
|
441
|
+
for (let channel = 0; channel < channels; channel++) {
|
|
442
|
+
interleaved[frame * channels + channel] = samples[channel]?.[frame] ?? 0;
|
|
430
443
|
}
|
|
431
444
|
}
|
|
432
445
|
return interleaved;
|
|
@@ -435,14 +448,14 @@ function deinterleaveBuffer(buffer, channels) {
|
|
|
435
448
|
const totalSamples = buffer.length / 4;
|
|
436
449
|
const frames = Math.floor(totalSamples / channels);
|
|
437
450
|
const result = [];
|
|
438
|
-
for (let
|
|
451
|
+
for (let channel = 0; channel < channels; channel++) {
|
|
439
452
|
result.push(new Float32Array(frames));
|
|
440
453
|
}
|
|
441
454
|
const view = new Float32Array(buffer.buffer, buffer.byteOffset, totalSamples);
|
|
442
455
|
for (let frame = 0; frame < frames; frame++) {
|
|
443
|
-
for (let
|
|
444
|
-
const channelArray = result[
|
|
445
|
-
const value = view[frame * channels +
|
|
456
|
+
for (let channel = 0; channel < channels; channel++) {
|
|
457
|
+
const channelArray = result[channel];
|
|
458
|
+
const value = view[frame * channels + channel];
|
|
446
459
|
if (channelArray && value !== void 0) {
|
|
447
460
|
channelArray[frame] = value;
|
|
448
461
|
}
|
|
@@ -565,21 +578,61 @@ var KWeightedSquaredSum = class {
|
|
|
565
578
|
}
|
|
566
579
|
}
|
|
567
580
|
};
|
|
568
|
-
var LUFS_OFFSET = -0.691;
|
|
569
581
|
var ABSOLUTE_GATE_LUFS = -70;
|
|
570
|
-
var RELATIVE_GATE_OFFSET_LU = -
|
|
582
|
+
var RELATIVE_GATE_OFFSET_LU = -20;
|
|
583
|
+
var LOW_PERCENTILE = 0.1;
|
|
584
|
+
var HIGH_PERCENTILE = 0.95;
|
|
585
|
+
function getConsideredLoudness(shortTerm) {
|
|
586
|
+
const absoluteGated = [];
|
|
587
|
+
for (let index = 0; index < shortTerm.length; index++) {
|
|
588
|
+
const value = shortTerm[index] ?? 0;
|
|
589
|
+
if (value >= ABSOLUTE_GATE_LUFS) absoluteGated.push(value);
|
|
590
|
+
}
|
|
591
|
+
if (absoluteGated.length === 0) return [];
|
|
592
|
+
let absoluteSum = 0;
|
|
593
|
+
for (let index = 0; index < absoluteGated.length; index++) {
|
|
594
|
+
absoluteSum += Math.pow(10, (absoluteGated[index] ?? 0) / 10);
|
|
595
|
+
}
|
|
596
|
+
const relativeThreshold = 10 * Math.log10(absoluteSum / absoluteGated.length) + RELATIVE_GATE_OFFSET_LU;
|
|
597
|
+
const considered = [];
|
|
598
|
+
for (let index = 0; index < absoluteGated.length; index++) {
|
|
599
|
+
const value = absoluteGated[index] ?? 0;
|
|
600
|
+
if (value >= relativeThreshold) considered.push(value);
|
|
601
|
+
}
|
|
602
|
+
return considered;
|
|
603
|
+
}
|
|
604
|
+
function computeLoudnessRange(shortTerm) {
|
|
605
|
+
const considered = getConsideredLoudness(shortTerm);
|
|
606
|
+
if (considered.length < 2) return 0;
|
|
607
|
+
considered.sort((left, right) => left - right);
|
|
608
|
+
const lowIndex = Math.round((considered.length - 1) * LOW_PERCENTILE);
|
|
609
|
+
const highIndex = Math.round((considered.length - 1) * HIGH_PERCENTILE);
|
|
610
|
+
return (considered[highIndex] ?? 0) - (considered[lowIndex] ?? 0);
|
|
611
|
+
}
|
|
612
|
+
function getLraConsideredStats(shortTerm) {
|
|
613
|
+
const considered = getConsideredLoudness(shortTerm);
|
|
614
|
+
if (considered.length === 0) {
|
|
615
|
+
return { min: Number.POSITIVE_INFINITY, median: Number.POSITIVE_INFINITY };
|
|
616
|
+
}
|
|
617
|
+
considered.sort((left, right) => left - right);
|
|
618
|
+
const min = considered[0] ?? Number.POSITIVE_INFINITY;
|
|
619
|
+
const middleIndex = considered.length >> 1;
|
|
620
|
+
const median = considered.length % 2 === 1 ? considered[middleIndex] ?? Number.POSITIVE_INFINITY : ((considered[middleIndex - 1] ?? 0) + (considered[middleIndex] ?? 0)) / 2;
|
|
621
|
+
return { min, median };
|
|
622
|
+
}
|
|
623
|
+
var LUFS_OFFSET = -0.691;
|
|
624
|
+
var ABSOLUTE_GATE_LUFS2 = -70;
|
|
625
|
+
var RELATIVE_GATE_OFFSET_LU2 = -10;
|
|
571
626
|
var BLOCK_DURATION_SECONDS = 0.4;
|
|
572
627
|
var BLOCK_STEP_SECONDS = 0.1;
|
|
573
628
|
var SHORT_TERM_BLOCK_DURATION_SECONDS = 3;
|
|
629
|
+
var FILE_LRA_TAIL_SECONDS = 1.5;
|
|
630
|
+
var FILE_LRA_TAIL_CHUNK_FRAMES = 8192;
|
|
574
631
|
var POWER_FLOOR = 1e-10;
|
|
575
|
-
var LRA_ABSOLUTE_GATE_LUFS = -70;
|
|
576
|
-
var LRA_RELATIVE_GATE_OFFSET_LU = -20;
|
|
577
|
-
var LRA_LOW_PERCENTILE = 0.1;
|
|
578
|
-
var LRA_HIGH_PERCENTILE = 0.95;
|
|
579
632
|
function applyBs1770Gating(closedBlockSums, blockSize) {
|
|
580
633
|
const blockCount = closedBlockSums.length;
|
|
581
634
|
if (blockCount === 0) return -Infinity;
|
|
582
|
-
const absoluteThresholdPower = Math.pow(10, (
|
|
635
|
+
const absoluteThresholdPower = Math.pow(10, (ABSOLUTE_GATE_LUFS2 - LUFS_OFFSET) / 10);
|
|
583
636
|
let absoluteSurvivorCount = 0;
|
|
584
637
|
let absoluteSum = 0;
|
|
585
638
|
for (let blockIndex = 0; blockIndex < blockCount; blockIndex++) {
|
|
@@ -591,7 +644,7 @@ function applyBs1770Gating(closedBlockSums, blockSize) {
|
|
|
591
644
|
}
|
|
592
645
|
if (absoluteSurvivorCount === 0) return -Infinity;
|
|
593
646
|
const absoluteMean = absoluteSum / absoluteSurvivorCount;
|
|
594
|
-
const relativeThresholdLufs = LUFS_OFFSET + 10 * Math.log10(absoluteMean) +
|
|
647
|
+
const relativeThresholdLufs = LUFS_OFFSET + 10 * Math.log10(absoluteMean) + RELATIVE_GATE_OFFSET_LU2;
|
|
595
648
|
const relativeThresholdPower = Math.pow(10, (relativeThresholdLufs - LUFS_OFFSET) / 10);
|
|
596
649
|
let relativeSurvivorCount = 0;
|
|
597
650
|
let relativeSum = 0;
|
|
@@ -638,71 +691,11 @@ var IntegratedLufsAccumulator = class {
|
|
|
638
691
|
return applyBs1770Gating(this.blocks.finalize(), this.blockSize);
|
|
639
692
|
}
|
|
640
693
|
};
|
|
641
|
-
function computeLraFromShortTerm(shortTermLoudness) {
|
|
642
|
-
const absoluteGated = [];
|
|
643
|
-
for (let index = 0; index < shortTermLoudness.length; index++) {
|
|
644
|
-
const value = shortTermLoudness[index] ?? 0;
|
|
645
|
-
if (value > LRA_ABSOLUTE_GATE_LUFS) {
|
|
646
|
-
absoluteGated.push(value);
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
if (absoluteGated.length < 2) return 0;
|
|
650
|
-
let absoluteSum = 0;
|
|
651
|
-
for (let index = 0; index < absoluteGated.length; index++) {
|
|
652
|
-
absoluteSum += Math.pow(10, (absoluteGated[index] ?? 0) / 10);
|
|
653
|
-
}
|
|
654
|
-
const absoluteMean = absoluteSum / absoluteGated.length;
|
|
655
|
-
const relativeThreshold = 10 * Math.log10(absoluteMean) + LRA_RELATIVE_GATE_OFFSET_LU;
|
|
656
|
-
const relativeGated = [];
|
|
657
|
-
for (let index = 0; index < absoluteGated.length; index++) {
|
|
658
|
-
const value = absoluteGated[index] ?? 0;
|
|
659
|
-
if (value > relativeThreshold) {
|
|
660
|
-
relativeGated.push(value);
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
if (relativeGated.length < 2) return 0;
|
|
664
|
-
relativeGated.sort((lhs, rhs) => lhs - rhs);
|
|
665
|
-
const lowIndex = Math.floor(relativeGated.length * LRA_LOW_PERCENTILE);
|
|
666
|
-
const highIndex = Math.min(Math.ceil(relativeGated.length * LRA_HIGH_PERCENTILE) - 1, relativeGated.length - 1);
|
|
667
|
-
return (relativeGated[highIndex] ?? 0) - (relativeGated[lowIndex] ?? 0);
|
|
668
|
-
}
|
|
669
|
-
function getLraConsideredStats(shortTerm) {
|
|
670
|
-
const aboveAbsolute = [];
|
|
671
|
-
for (let index = 0; index < shortTerm.length; index++) {
|
|
672
|
-
const lufs = shortTerm[index] ?? 0;
|
|
673
|
-
if (lufs > LRA_ABSOLUTE_GATE_LUFS) {
|
|
674
|
-
aboveAbsolute.push(lufs);
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
if (aboveAbsolute.length === 0) {
|
|
678
|
-
return { min: Number.POSITIVE_INFINITY, median: Number.POSITIVE_INFINITY };
|
|
679
|
-
}
|
|
680
|
-
let absoluteSum = 0;
|
|
681
|
-
for (let index = 0; index < aboveAbsolute.length; index++) {
|
|
682
|
-
absoluteSum += Math.pow(10, (aboveAbsolute[index] ?? 0) / 10);
|
|
683
|
-
}
|
|
684
|
-
const absoluteMean = absoluteSum / aboveAbsolute.length;
|
|
685
|
-
const relativeThreshold = 10 * Math.log10(absoluteMean) + LRA_RELATIVE_GATE_OFFSET_LU;
|
|
686
|
-
const considered = [];
|
|
687
|
-
for (let index = 0; index < aboveAbsolute.length; index++) {
|
|
688
|
-
const lufs = aboveAbsolute[index] ?? 0;
|
|
689
|
-
if (lufs > relativeThreshold) {
|
|
690
|
-
considered.push(lufs);
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
if (considered.length === 0) {
|
|
694
|
-
return { min: Number.POSITIVE_INFINITY, median: Number.POSITIVE_INFINITY };
|
|
695
|
-
}
|
|
696
|
-
considered.sort((left, right) => left - right);
|
|
697
|
-
const min = considered[0] ?? Number.POSITIVE_INFINITY;
|
|
698
|
-
const middleIndex = considered.length >> 1;
|
|
699
|
-
const median = considered.length % 2 === 1 ? considered[middleIndex] ?? Number.POSITIVE_INFINITY : ((considered[middleIndex - 1] ?? 0) + (considered[middleIndex] ?? 0)) / 2;
|
|
700
|
-
return { min, median };
|
|
701
|
-
}
|
|
702
694
|
var LoudnessAccumulator = class {
|
|
703
695
|
constructor(sampleRate, channelCount, channelWeights) {
|
|
704
696
|
this.outputBuffer = new Float64Array(0);
|
|
705
697
|
this.finalized = false;
|
|
698
|
+
this.sourceFrames = 0;
|
|
706
699
|
if (channelCount <= 0) {
|
|
707
700
|
throw new Error(`LoudnessAccumulator: channelCount must be positive, got ${channelCount}`);
|
|
708
701
|
}
|
|
@@ -711,10 +704,12 @@ var LoudnessAccumulator = class {
|
|
|
711
704
|
}
|
|
712
705
|
this.blockSize400 = Math.round(BLOCK_DURATION_SECONDS * sampleRate);
|
|
713
706
|
this.blockSize3s = Math.round(SHORT_TERM_BLOCK_DURATION_SECONDS * sampleRate);
|
|
714
|
-
|
|
707
|
+
this.blockStep = Math.round(BLOCK_STEP_SECONDS * sampleRate);
|
|
708
|
+
this.sampleRate = sampleRate;
|
|
709
|
+
this.channelCount = channelCount;
|
|
715
710
|
this.kw = new KWeightedSquaredSum(sampleRate, channelCount, channelWeights);
|
|
716
|
-
this.blocks400 = new BlockSumAccumulator(this.blockSize400, blockStep);
|
|
717
|
-
this.blocks3s = new BlockSumAccumulator(this.blockSize3s, blockStep);
|
|
711
|
+
this.blocks400 = new BlockSumAccumulator(this.blockSize400, this.blockStep);
|
|
712
|
+
this.blocks3s = new BlockSumAccumulator(this.blockSize3s, this.blockStep);
|
|
718
713
|
}
|
|
719
714
|
push(channels, frames) {
|
|
720
715
|
if (this.finalized) {
|
|
@@ -727,10 +722,23 @@ var LoudnessAccumulator = class {
|
|
|
727
722
|
this.kw.push(channels, frames, this.outputBuffer);
|
|
728
723
|
this.blocks400.push(this.outputBuffer, frames);
|
|
729
724
|
this.blocks3s.push(this.outputBuffer, frames);
|
|
725
|
+
this.sourceFrames += frames;
|
|
730
726
|
}
|
|
731
727
|
finalize() {
|
|
732
728
|
if (this.cachedResult !== void 0) return this.cachedResult;
|
|
733
729
|
this.finalized = true;
|
|
730
|
+
const sourceShortTermCount = this.sourceFrames < this.blockSize3s ? 0 : Math.floor((this.sourceFrames - this.blockSize3s) / this.blockStep) + 1;
|
|
731
|
+
const tailFrames = Math.round(FILE_LRA_TAIL_SECONDS * this.sampleRate);
|
|
732
|
+
const tailChunkFrames = Math.min(FILE_LRA_TAIL_CHUNK_FRAMES, tailFrames);
|
|
733
|
+
const zeroChannels = Array.from({ length: this.channelCount }, () => new Float32Array(tailChunkFrames));
|
|
734
|
+
let remainingTailFrames = tailFrames;
|
|
735
|
+
while (remainingTailFrames > 0) {
|
|
736
|
+
const frames = Math.min(remainingTailFrames, tailChunkFrames);
|
|
737
|
+
if (this.outputBuffer.length < frames) this.outputBuffer = new Float64Array(frames);
|
|
738
|
+
this.kw.push(zeroChannels, frames, this.outputBuffer);
|
|
739
|
+
this.blocks3s.push(this.outputBuffer, frames);
|
|
740
|
+
remainingTailFrames -= frames;
|
|
741
|
+
}
|
|
734
742
|
const closed400 = this.blocks400.finalize();
|
|
735
743
|
const closed3s = this.blocks3s.finalize();
|
|
736
744
|
const blockSize400 = this.blockSize400;
|
|
@@ -740,19 +748,23 @@ var LoudnessAccumulator = class {
|
|
|
740
748
|
const sum = closed400[index] ?? 0;
|
|
741
749
|
momentary[index] = LUFS_OFFSET + 10 * Math.log10(Math.max(sum / blockSize400, POWER_FLOOR));
|
|
742
750
|
}
|
|
743
|
-
const
|
|
751
|
+
const fileShortTerm = new Array(closed3s.length);
|
|
744
752
|
for (let index = 0; index < closed3s.length; index++) {
|
|
745
753
|
const sum = closed3s[index] ?? 0;
|
|
746
|
-
|
|
754
|
+
fileShortTerm[index] = LUFS_OFFSET + 10 * Math.log10(Math.max(sum / blockSize3s, POWER_FLOOR));
|
|
747
755
|
}
|
|
756
|
+
const shortTerm = fileShortTerm.slice(0, sourceShortTermCount);
|
|
748
757
|
const integrated = applyBs1770Gating(closed400, blockSize400);
|
|
749
|
-
const range =
|
|
758
|
+
const range = computeLoudnessRange(fileShortTerm);
|
|
750
759
|
this.cachedResult = { integrated, momentary, shortTerm, range };
|
|
751
760
|
return this.cachedResult;
|
|
752
761
|
}
|
|
753
762
|
};
|
|
754
763
|
var MixedRadixFft = class {
|
|
755
764
|
constructor(size) {
|
|
765
|
+
if (!Number.isSafeInteger(size) || size <= 0) {
|
|
766
|
+
throw new Error(`MixedRadixFft: size must be a positive integer, got ${size}`);
|
|
767
|
+
}
|
|
756
768
|
this.size = size;
|
|
757
769
|
this.radices = factorize(size);
|
|
758
770
|
this.frameRe = new Float32Array(size);
|
|
@@ -766,6 +778,7 @@ var MixedRadixFft = class {
|
|
|
766
778
|
this.twiddleIm = twiddleIm;
|
|
767
779
|
}
|
|
768
780
|
fft(xRe, xIm, outRe, outIm) {
|
|
781
|
+
this.assertArrayCapacities(xRe, xIm, outRe, outIm);
|
|
769
782
|
const perm = this.permutation;
|
|
770
783
|
const nn = this.size;
|
|
771
784
|
for (let index = 0; index < nn; index++) {
|
|
@@ -788,6 +801,7 @@ var MixedRadixFft = class {
|
|
|
788
801
|
}
|
|
789
802
|
}
|
|
790
803
|
ifft(xRe, xIm, outRe, outIm) {
|
|
804
|
+
this.assertArrayCapacities(xRe, xIm, outRe, outIm);
|
|
791
805
|
const auxIm = this.auxIm;
|
|
792
806
|
const nn = this.size;
|
|
793
807
|
for (let index = 0; index < nn; index++) {
|
|
@@ -799,6 +813,17 @@ var MixedRadixFft = class {
|
|
|
799
813
|
outIm[index] = -(outIm[index] ?? 0) / nn;
|
|
800
814
|
}
|
|
801
815
|
}
|
|
816
|
+
assertArrayCapacities(xRe, xIm, outRe, outIm) {
|
|
817
|
+
this.assertArrayCapacity("xRe", xRe);
|
|
818
|
+
this.assertArrayCapacity("xIm", xIm);
|
|
819
|
+
this.assertArrayCapacity("outRe", outRe);
|
|
820
|
+
this.assertArrayCapacity("outIm", outIm);
|
|
821
|
+
}
|
|
822
|
+
assertArrayCapacity(name, values) {
|
|
823
|
+
if (values.length < this.size) {
|
|
824
|
+
throw new Error(`MixedRadixFft: ${name} capacity must be at least ${this.size}, got ${values.length}`);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
802
827
|
radix2(outRe, outIm, nn, groupSize, subSize, twOffset) {
|
|
803
828
|
for (let group = 0; group < nn; group += groupSize) {
|
|
804
829
|
for (let ni = 0; ni < subSize; ni++) {
|
|
@@ -936,11 +961,11 @@ function factorize(size) {
|
|
|
936
961
|
if (remaining !== 1) {
|
|
937
962
|
throw new Error(`MixedRadixFft: size ${size} has unsupported prime factor ${remaining} (only 2, 3, 5 supported)`);
|
|
938
963
|
}
|
|
939
|
-
factors.sort((
|
|
964
|
+
factors.sort((firstFactor, secondFactor) => firstFactor - secondFactor);
|
|
940
965
|
return factors;
|
|
941
966
|
}
|
|
942
967
|
function computePermutation(size, radices) {
|
|
943
|
-
const permutation = new
|
|
968
|
+
const permutation = new Uint32Array(size);
|
|
944
969
|
for (let index = 0; index < size; index++) {
|
|
945
970
|
let remainder = index;
|
|
946
971
|
let permuted = 0;
|
|
@@ -980,202 +1005,6 @@ function computeTwiddles(radices) {
|
|
|
980
1005
|
}
|
|
981
1006
|
return { twiddleRe, twiddleIm };
|
|
982
1007
|
}
|
|
983
|
-
var STDERR_CAP_BYTES = 64 * 1024;
|
|
984
|
-
var ResampleStream = class {
|
|
985
|
-
constructor(ffmpegPath, options) {
|
|
986
|
-
this.chunks = [];
|
|
987
|
-
this.chunkedBytes = 0;
|
|
988
|
-
this.stdoutEnded = false;
|
|
989
|
-
this.exited = false;
|
|
990
|
-
this.stderr = "";
|
|
991
|
-
this.closed = false;
|
|
992
|
-
const { sourceSampleRate, targetSampleRate, channels } = options;
|
|
993
|
-
if (channels <= 0) throw new Error(`ResampleStream: channels must be > 0, got ${String(channels)}`);
|
|
994
|
-
if (sourceSampleRate <= 0) throw new Error(`ResampleStream: sourceSampleRate must be > 0, got ${String(sourceSampleRate)}`);
|
|
995
|
-
if (targetSampleRate <= 0) throw new Error(`ResampleStream: targetSampleRate must be > 0, got ${String(targetSampleRate)}`);
|
|
996
|
-
this.channels = channels;
|
|
997
|
-
this.bytesPerFrame = channels * 4;
|
|
998
|
-
const args = [
|
|
999
|
-
"-f",
|
|
1000
|
-
"f32le",
|
|
1001
|
-
"-ar",
|
|
1002
|
-
String(sourceSampleRate),
|
|
1003
|
-
"-ac",
|
|
1004
|
-
String(channels),
|
|
1005
|
-
"-i",
|
|
1006
|
-
"pipe:0",
|
|
1007
|
-
"-af",
|
|
1008
|
-
`aresample=${String(targetSampleRate)}:resampler=soxr:dither_method=triangular`,
|
|
1009
|
-
"-f",
|
|
1010
|
-
"f32le",
|
|
1011
|
-
"-ar",
|
|
1012
|
-
String(targetSampleRate),
|
|
1013
|
-
"-ac",
|
|
1014
|
-
String(channels),
|
|
1015
|
-
"pipe:1"
|
|
1016
|
-
];
|
|
1017
|
-
this.child = spawn(ffmpegPath, args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
1018
|
-
this.child.stdout.on("data", (bytes) => this.onStdoutData(bytes));
|
|
1019
|
-
this.child.stdout.once("end", () => this.onStdoutEnd());
|
|
1020
|
-
this.child.stderr.on("data", (bytes) => this.onStderrData(bytes));
|
|
1021
|
-
this.child.on("error", (error) => this.onExit(error));
|
|
1022
|
-
this.child.once("exit", (code) => {
|
|
1023
|
-
if (code !== null && code !== 0) {
|
|
1024
|
-
const detail = this.stderr ? `: ${this.stderr.slice(0, 1024)}` : "";
|
|
1025
|
-
this.onExit(new Error(`ffmpeg exited ${String(code)}${detail}`));
|
|
1026
|
-
return;
|
|
1027
|
-
}
|
|
1028
|
-
this.onExit();
|
|
1029
|
-
});
|
|
1030
|
-
this.child.stdin.on("error", (error) => {
|
|
1031
|
-
if (error.code === "EPIPE") return;
|
|
1032
|
-
this.exitError ?? (this.exitError = error);
|
|
1033
|
-
});
|
|
1034
|
-
}
|
|
1035
|
-
async write(samples) {
|
|
1036
|
-
if (this.closed) throw new Error("ResampleStream: write after close");
|
|
1037
|
-
if (this.exitError) throw this.exitError;
|
|
1038
|
-
const frames = samples[0]?.length ?? 0;
|
|
1039
|
-
if (frames === 0) return;
|
|
1040
|
-
const interleaved = interleave(samples, frames, this.channels);
|
|
1041
|
-
const buf = Buffer.from(interleaved.buffer, interleaved.byteOffset, interleaved.byteLength);
|
|
1042
|
-
if (this.pendingDrain) await this.pendingDrain;
|
|
1043
|
-
const ok = this.child.stdin.write(buf);
|
|
1044
|
-
if (!ok) {
|
|
1045
|
-
this.pendingDrain = new Promise((resolve) => {
|
|
1046
|
-
this.child.stdin.once("drain", () => {
|
|
1047
|
-
this.pendingDrain = void 0;
|
|
1048
|
-
resolve();
|
|
1049
|
-
});
|
|
1050
|
-
});
|
|
1051
|
-
}
|
|
1052
|
-
}
|
|
1053
|
-
async read(frames) {
|
|
1054
|
-
if (this.closed) throw new Error("ResampleStream: read after close");
|
|
1055
|
-
if (frames <= 0) return this.emptyChannels();
|
|
1056
|
-
if (this.pendingRead) throw new Error("ResampleStream: concurrent read");
|
|
1057
|
-
if (this.chunkedBytes >= this.bytesPerFrame) return this.drainOutput(frames);
|
|
1058
|
-
if (this.exitError) throw this.exitError;
|
|
1059
|
-
if (this.stdoutEnded && this.exited) return this.emptyChannels();
|
|
1060
|
-
return new Promise((resolve, reject) => {
|
|
1061
|
-
this.pendingRead = { resolve, reject, frames };
|
|
1062
|
-
this.maybeSatisfyPendingRead();
|
|
1063
|
-
});
|
|
1064
|
-
}
|
|
1065
|
-
async end() {
|
|
1066
|
-
if (this.closed) return;
|
|
1067
|
-
if (this.pendingDrain) await this.pendingDrain;
|
|
1068
|
-
if (!this.child.stdin.writableEnded) this.child.stdin.end();
|
|
1069
|
-
}
|
|
1070
|
-
async close() {
|
|
1071
|
-
if (this.closed) return;
|
|
1072
|
-
this.closed = true;
|
|
1073
|
-
if (this.pendingRead) {
|
|
1074
|
-
this.pendingRead.reject(new Error("ResampleStream: close while read pending"));
|
|
1075
|
-
this.pendingRead = void 0;
|
|
1076
|
-
}
|
|
1077
|
-
try {
|
|
1078
|
-
if (!this.child.stdin.writableEnded) {
|
|
1079
|
-
try {
|
|
1080
|
-
this.child.stdin.end();
|
|
1081
|
-
} catch {
|
|
1082
|
-
}
|
|
1083
|
-
}
|
|
1084
|
-
} finally {
|
|
1085
|
-
if (!this.exited && this.child.exitCode === null && !this.child.killed) {
|
|
1086
|
-
this.child.kill("SIGTERM");
|
|
1087
|
-
}
|
|
1088
|
-
}
|
|
1089
|
-
if (!this.exited) {
|
|
1090
|
-
await new Promise((resolve) => {
|
|
1091
|
-
let settled = false;
|
|
1092
|
-
const settle = () => {
|
|
1093
|
-
if (!settled) {
|
|
1094
|
-
settled = true;
|
|
1095
|
-
resolve();
|
|
1096
|
-
}
|
|
1097
|
-
};
|
|
1098
|
-
const timeout = setTimeout(() => {
|
|
1099
|
-
this.child.kill("SIGKILL");
|
|
1100
|
-
settle();
|
|
1101
|
-
}, 5e3);
|
|
1102
|
-
this.child.once("exit", () => {
|
|
1103
|
-
clearTimeout(timeout);
|
|
1104
|
-
settle();
|
|
1105
|
-
});
|
|
1106
|
-
});
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1109
|
-
onStdoutData(bytes) {
|
|
1110
|
-
if (bytes.length > 0) {
|
|
1111
|
-
this.chunks.push(bytes);
|
|
1112
|
-
this.chunkedBytes += bytes.length;
|
|
1113
|
-
}
|
|
1114
|
-
this.maybeSatisfyPendingRead();
|
|
1115
|
-
}
|
|
1116
|
-
onStdoutEnd() {
|
|
1117
|
-
this.stdoutEnded = true;
|
|
1118
|
-
this.maybeSatisfyPendingRead();
|
|
1119
|
-
}
|
|
1120
|
-
onStderrData(bytes) {
|
|
1121
|
-
if (this.stderr.length >= STDERR_CAP_BYTES) return;
|
|
1122
|
-
const remaining = STDERR_CAP_BYTES - this.stderr.length;
|
|
1123
|
-
const text = bytes.toString("utf8");
|
|
1124
|
-
this.stderr += text.length > remaining ? text.slice(0, remaining) : text;
|
|
1125
|
-
}
|
|
1126
|
-
onExit(error) {
|
|
1127
|
-
this.exited = true;
|
|
1128
|
-
if (error) this.exitError ?? (this.exitError = error);
|
|
1129
|
-
this.maybeSatisfyPendingRead();
|
|
1130
|
-
}
|
|
1131
|
-
maybeSatisfyPendingRead() {
|
|
1132
|
-
if (!this.pendingRead) return;
|
|
1133
|
-
const { frames, resolve, reject } = this.pendingRead;
|
|
1134
|
-
if (this.chunkedBytes >= this.bytesPerFrame) {
|
|
1135
|
-
this.pendingRead = void 0;
|
|
1136
|
-
resolve(this.drainOutput(frames));
|
|
1137
|
-
return;
|
|
1138
|
-
}
|
|
1139
|
-
if (this.stdoutEnded && this.exited) {
|
|
1140
|
-
this.pendingRead = void 0;
|
|
1141
|
-
if (this.exitError) {
|
|
1142
|
-
reject(this.exitError);
|
|
1143
|
-
return;
|
|
1144
|
-
}
|
|
1145
|
-
resolve(this.emptyChannels());
|
|
1146
|
-
}
|
|
1147
|
-
}
|
|
1148
|
-
drainOutput(frames) {
|
|
1149
|
-
const wantBytes = frames * this.bytesPerFrame;
|
|
1150
|
-
const available = Math.min(this.chunkedBytes, wantBytes);
|
|
1151
|
-
const completeFrames = Math.floor(available / this.bytesPerFrame);
|
|
1152
|
-
if (completeFrames === 0) return this.emptyChannels();
|
|
1153
|
-
const completeBytes = completeFrames * this.bytesPerFrame;
|
|
1154
|
-
const aligned = Buffer.allocUnsafe(completeBytes);
|
|
1155
|
-
let written = 0;
|
|
1156
|
-
while (written < completeBytes) {
|
|
1157
|
-
const head = this.chunks[0];
|
|
1158
|
-
if (!head) break;
|
|
1159
|
-
const remaining = completeBytes - written;
|
|
1160
|
-
if (head.length <= remaining) {
|
|
1161
|
-
head.copy(aligned, written, 0, head.length);
|
|
1162
|
-
written += head.length;
|
|
1163
|
-
this.chunks.shift();
|
|
1164
|
-
continue;
|
|
1165
|
-
}
|
|
1166
|
-
head.copy(aligned, written, 0, remaining);
|
|
1167
|
-
this.chunks[0] = head.subarray(remaining);
|
|
1168
|
-
written += remaining;
|
|
1169
|
-
}
|
|
1170
|
-
this.chunkedBytes -= completeBytes;
|
|
1171
|
-
return deinterleaveBuffer(aligned, this.channels);
|
|
1172
|
-
}
|
|
1173
|
-
emptyChannels() {
|
|
1174
|
-
const out = [];
|
|
1175
|
-
for (let ch = 0; ch < this.channels; ch++) out.push(new Float32Array(0));
|
|
1176
|
-
return out;
|
|
1177
|
-
}
|
|
1178
|
-
};
|
|
1179
1008
|
var SlidingWindowMaxStream = class {
|
|
1180
1009
|
constructor(halfWidth) {
|
|
1181
1010
|
this.dequeHead = 0;
|
|
@@ -1314,48 +1143,111 @@ var SlidingWindowMinStream = class {
|
|
|
1314
1143
|
return output;
|
|
1315
1144
|
}
|
|
1316
1145
|
};
|
|
1317
|
-
var
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1146
|
+
var ByteBoundedCache = class {
|
|
1147
|
+
constructor(maxBytes) {
|
|
1148
|
+
this.maxBytes = maxBytes;
|
|
1149
|
+
this.entries = /* @__PURE__ */ new Map();
|
|
1150
|
+
this.currentBytes = 0;
|
|
1151
|
+
assertPositiveInteger(maxBytes, "ByteBoundedCache maxBytes");
|
|
1152
|
+
}
|
|
1153
|
+
get bytes() {
|
|
1154
|
+
return this.currentBytes;
|
|
1155
|
+
}
|
|
1156
|
+
get size() {
|
|
1157
|
+
return this.entries.size;
|
|
1158
|
+
}
|
|
1159
|
+
get(key) {
|
|
1160
|
+
return this.entries.get(key)?.value;
|
|
1161
|
+
}
|
|
1162
|
+
set(key, value, bytes) {
|
|
1163
|
+
assertPositiveInteger(bytes, "ByteBoundedCache entry bytes");
|
|
1164
|
+
const replaced = this.entries.get(key);
|
|
1165
|
+
if (replaced !== void 0) {
|
|
1166
|
+
this.entries.delete(key);
|
|
1167
|
+
this.currentBytes -= replaced.bytes;
|
|
1168
|
+
}
|
|
1169
|
+
if (bytes > this.maxBytes) return;
|
|
1170
|
+
while (this.currentBytes + bytes > this.maxBytes) {
|
|
1171
|
+
const oldest = this.entries.entries().next();
|
|
1172
|
+
if (oldest.done) break;
|
|
1173
|
+
const [oldestKey, oldestEntry] = oldest.value;
|
|
1174
|
+
this.entries.delete(oldestKey);
|
|
1175
|
+
this.currentBytes -= oldestEntry.bytes;
|
|
1176
|
+
}
|
|
1177
|
+
this.entries.set(key, { value, bytes });
|
|
1178
|
+
this.currentBytes += bytes;
|
|
1179
|
+
}
|
|
1180
|
+
};
|
|
1181
|
+
function assertPositiveInteger(value, name) {
|
|
1182
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
1183
|
+
throw new Error(`${name} must be a positive finite integer, got ${value}`);
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
function assertRadix2Size(size) {
|
|
1187
|
+
if (!Number.isSafeInteger(size) || size <= 0 || !Number.isInteger(Math.log2(size))) {
|
|
1188
|
+
throw new Error(`FFT size must be a positive power-of-two integer, got ${size}`);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
function assertPositiveInteger2(value, name) {
|
|
1192
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
1193
|
+
throw new Error(`${name} must be a positive integer, got ${value}`);
|
|
1194
|
+
}
|
|
1325
1195
|
}
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
if (cached && cached.length >= needed) return cached;
|
|
1331
|
-
const grown = new Float32Array(needed);
|
|
1332
|
-
batchTimeCache.set(fftSize, grown);
|
|
1333
|
-
return grown;
|
|
1196
|
+
function assertNonnegativeInteger(value, name) {
|
|
1197
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
1198
|
+
throw new Error(`${name} must be a nonnegative integer, got ${value}`);
|
|
1199
|
+
}
|
|
1334
1200
|
}
|
|
1201
|
+
var STFT_BATCH_SCRATCH_BYTES = 8 * 1024 * 1024;
|
|
1202
|
+
var HANN_WINDOW_CACHE_BYTES = 1024 * 1024;
|
|
1203
|
+
var TWIDDLE_CACHE_BYTES = 8 * 1024 * 1024;
|
|
1335
1204
|
function stft(signal, fftSize, hopSize, output, backend, fftAddonOptions) {
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
const
|
|
1339
|
-
const
|
|
1340
|
-
const
|
|
1341
|
-
|
|
1205
|
+
assertRadix2Size(fftSize);
|
|
1206
|
+
assertPositiveInteger2(hopSize, "STFT hopSize");
|
|
1207
|
+
const numFrames = signal.length < fftSize ? 0 : Math.floor((signal.length - fftSize) / hopSize) + 1;
|
|
1208
|
+
const halfSize = Math.floor(fftSize / 2) + 1;
|
|
1209
|
+
const outputLength = halfSize * numFrames;
|
|
1210
|
+
if (output !== void 0 && (output.real.length < outputLength || output.imag.length < outputLength)) {
|
|
1211
|
+
throw new Error(`STFT output capacity must be at least ${outputLength} values per component`);
|
|
1212
|
+
}
|
|
1342
1213
|
if (numFrames <= 0) {
|
|
1343
|
-
|
|
1214
|
+
const real2 = output?.real ?? new Float32Array(0);
|
|
1215
|
+
const imag2 = output?.imag ?? new Float32Array(0);
|
|
1216
|
+
return { real: real2, imag: imag2, frames: 0, fftSize };
|
|
1344
1217
|
}
|
|
1218
|
+
const addon = backend ? getFftAddon(backend, fftAddonOptions) : null;
|
|
1219
|
+
let slabFrameCapacity = 0;
|
|
1345
1220
|
if (addon) {
|
|
1346
|
-
const
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
for (let index = 0; index < fftSize; index++) {
|
|
1350
|
-
batchInput[frame * fftSize + index] = (signal[offset + index] ?? 0) * (window[index] ?? 0);
|
|
1351
|
-
}
|
|
1221
|
+
const bytesPerFrame = Float32Array.BYTES_PER_ELEMENT * (fftSize + 2 * halfSize);
|
|
1222
|
+
if (bytesPerFrame > STFT_BATCH_SCRATCH_BYTES) {
|
|
1223
|
+
throw new Error(`STFT fftSize ${fftSize} exceeds the ${STFT_BATCH_SCRATCH_BYTES}-byte native scratch budget`);
|
|
1352
1224
|
}
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1225
|
+
slabFrameCapacity = Math.max(1, Math.floor(STFT_BATCH_SCRATCH_BYTES / bytesPerFrame));
|
|
1226
|
+
}
|
|
1227
|
+
const real = output?.real ?? new Float32Array(outputLength);
|
|
1228
|
+
const imag = output?.imag ?? new Float32Array(outputLength);
|
|
1229
|
+
const window = hanningWindow(fftSize);
|
|
1230
|
+
if (addon) {
|
|
1231
|
+
for (let firstFrame = 0; firstFrame < numFrames; firstFrame += slabFrameCapacity) {
|
|
1232
|
+
const slabFrames = Math.min(slabFrameCapacity, numFrames - firstFrame);
|
|
1233
|
+
const batchInput = new Float32Array(fftSize * slabFrames);
|
|
1234
|
+
for (let slabFrame = 0; slabFrame < slabFrames; slabFrame++) {
|
|
1235
|
+
const offset = (firstFrame + slabFrame) * hopSize;
|
|
1236
|
+
for (let index = 0; index < fftSize; index++) {
|
|
1237
|
+
batchInput[slabFrame * fftSize + index] = (signal[offset + index] ?? 0) * (window[index] ?? 0);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
const outputStart = firstFrame * halfSize;
|
|
1241
|
+
const outputEnd = outputStart + slabFrames * halfSize;
|
|
1242
|
+
const realSlab = real.subarray(outputStart, outputEnd);
|
|
1243
|
+
const imagSlab = imag.subarray(outputStart, outputEnd);
|
|
1244
|
+
if (typeof addon.batchFftInto === "function") {
|
|
1245
|
+
addon.batchFftInto(batchInput, realSlab, imagSlab, fftSize, slabFrames);
|
|
1246
|
+
} else {
|
|
1247
|
+
const { re: batchReal, im: batchImag } = addon.batchFft(batchInput, fftSize, slabFrames);
|
|
1248
|
+
realSlab.set(batchReal.subarray(0, realSlab.length));
|
|
1249
|
+
imagSlab.set(batchImag.subarray(0, imagSlab.length));
|
|
1250
|
+
}
|
|
1359
1251
|
}
|
|
1360
1252
|
return { real, imag, frames: numFrames, fftSize };
|
|
1361
1253
|
}
|
|
@@ -1377,29 +1269,49 @@ function stft(signal, fftSize, hopSize, output, backend, fftAddonOptions) {
|
|
|
1377
1269
|
}
|
|
1378
1270
|
function istft(result, hopSize, outputLength, backend, fftAddonOptions) {
|
|
1379
1271
|
const { real, imag, frames, fftSize } = result;
|
|
1272
|
+
assertRadix2Size(fftSize);
|
|
1273
|
+
assertPositiveInteger2(hopSize, "ISTFT hopSize");
|
|
1274
|
+
assertNonnegativeInteger(outputLength, "ISTFT outputLength");
|
|
1275
|
+
assertNonnegativeInteger(frames, "ISTFT frames");
|
|
1276
|
+
const halfSize = Math.floor(fftSize / 2) + 1;
|
|
1277
|
+
const spectrumLength = halfSize * frames;
|
|
1278
|
+
if (!Number.isSafeInteger(spectrumLength) || real.length < spectrumLength || imag.length < spectrumLength) {
|
|
1279
|
+
throw new Error(`ISTFT spectrum capacity must be at least ${spectrumLength} values per component`);
|
|
1280
|
+
}
|
|
1281
|
+
const addon = backend ? getFftAddon(backend, fftAddonOptions) : null;
|
|
1282
|
+
let slabFrameCapacity = 0;
|
|
1283
|
+
if (addon && frames > 0) {
|
|
1284
|
+
const bytesPerFrame = Float32Array.BYTES_PER_ELEMENT * fftSize;
|
|
1285
|
+
if (bytesPerFrame > STFT_BATCH_SCRATCH_BYTES) {
|
|
1286
|
+
throw new Error(`ISTFT fftSize ${fftSize} exceeds the ${STFT_BATCH_SCRATCH_BYTES}-byte native scratch budget`);
|
|
1287
|
+
}
|
|
1288
|
+
slabFrameCapacity = Math.max(1, Math.floor(STFT_BATCH_SCRATCH_BYTES / bytesPerFrame));
|
|
1289
|
+
}
|
|
1380
1290
|
const window = hanningWindow(fftSize);
|
|
1381
1291
|
const output = new Float32Array(outputLength);
|
|
1382
1292
|
const windowSum = new Float32Array(outputLength);
|
|
1383
|
-
const halfSize = fftSize / 2 + 1;
|
|
1384
|
-
const addon = backend ? getFftAddon(backend, fftAddonOptions) : null;
|
|
1385
1293
|
if (addon && frames > 0) {
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
const
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1294
|
+
for (let firstFrame = 0; firstFrame < frames; firstFrame += slabFrameCapacity) {
|
|
1295
|
+
const slabFrames = Math.min(slabFrameCapacity, frames - firstFrame);
|
|
1296
|
+
const spectrumStart = firstFrame * halfSize;
|
|
1297
|
+
const spectrumEnd = spectrumStart + slabFrames * halfSize;
|
|
1298
|
+
const realSlab = real.subarray(spectrumStart, spectrumEnd);
|
|
1299
|
+
const imagSlab = imag.subarray(spectrumStart, spectrumEnd);
|
|
1300
|
+
let timeDomainSlab;
|
|
1301
|
+
if (typeof addon.batchIfftInto === "function") {
|
|
1302
|
+
timeDomainSlab = new Float32Array(fftSize * slabFrames);
|
|
1303
|
+
addon.batchIfftInto(realSlab, imagSlab, timeDomainSlab, fftSize, slabFrames);
|
|
1304
|
+
} else {
|
|
1305
|
+
timeDomainSlab = addon.batchIfft(realSlab, imagSlab, fftSize, slabFrames);
|
|
1306
|
+
}
|
|
1307
|
+
for (let slabFrame = 0; slabFrame < slabFrames; slabFrame++) {
|
|
1308
|
+
const offset = (firstFrame + slabFrame) * hopSize;
|
|
1309
|
+
for (let index = 0; index < fftSize; index++) {
|
|
1310
|
+
const position = offset + index;
|
|
1311
|
+
if (position < outputLength) {
|
|
1312
|
+
output[position] = (output[position] ?? 0) + (timeDomainSlab[slabFrame * fftSize + index] ?? 0) * (window[index] ?? 0);
|
|
1313
|
+
windowSum[position] = (windowSum[position] ?? 0) + (window[index] ?? 0) * (window[index] ?? 0);
|
|
1314
|
+
}
|
|
1403
1315
|
}
|
|
1404
1316
|
}
|
|
1405
1317
|
}
|
|
@@ -1438,20 +1350,27 @@ function istft(result, hopSize, outputLength, backend, fftAddonOptions) {
|
|
|
1438
1350
|
}
|
|
1439
1351
|
return output;
|
|
1440
1352
|
}
|
|
1441
|
-
var hanningWindowCache =
|
|
1353
|
+
var hanningWindowCache = new ByteBoundedCache(HANN_WINDOW_CACHE_BYTES);
|
|
1442
1354
|
function hanningWindow(size, periodic = true) {
|
|
1355
|
+
assertPositiveInteger2(size, "Hann window size");
|
|
1443
1356
|
const key = `${size}:${periodic ? "p" : "s"}`;
|
|
1444
1357
|
const cached = hanningWindowCache.get(key);
|
|
1445
1358
|
if (cached) return cached;
|
|
1446
1359
|
const window = new Float32Array(size);
|
|
1360
|
+
if (size === 1) {
|
|
1361
|
+
window[0] = 1;
|
|
1362
|
+
hanningWindowCache.set(key, window, window.byteLength);
|
|
1363
|
+
return window;
|
|
1364
|
+
}
|
|
1447
1365
|
const denominator = periodic ? size : size - 1;
|
|
1448
1366
|
for (let index = 0; index < size; index++) {
|
|
1449
1367
|
window[index] = 0.5 * (1 - Math.cos(2 * Math.PI * index / denominator));
|
|
1450
1368
|
}
|
|
1451
|
-
hanningWindowCache.set(key, window);
|
|
1369
|
+
hanningWindowCache.set(key, window, window.byteLength);
|
|
1452
1370
|
return window;
|
|
1453
1371
|
}
|
|
1454
1372
|
function createFftWorkspace(size) {
|
|
1373
|
+
assertRadix2Size(size);
|
|
1455
1374
|
return {
|
|
1456
1375
|
re: new Float32Array(size),
|
|
1457
1376
|
im: new Float32Array(size),
|
|
@@ -1461,6 +1380,8 @@ function createFftWorkspace(size) {
|
|
|
1461
1380
|
}
|
|
1462
1381
|
function fft(input, workspace) {
|
|
1463
1382
|
const size = input.length;
|
|
1383
|
+
assertRadix2Size(size);
|
|
1384
|
+
if (workspace !== void 0) assertWorkspaceCapacity(workspace, size);
|
|
1464
1385
|
const re = workspace ? workspace.re : new Float32Array(size);
|
|
1465
1386
|
const im = workspace ? workspace.im : new Float32Array(size);
|
|
1466
1387
|
re.set(input);
|
|
@@ -1472,6 +1393,9 @@ function fft(input, workspace) {
|
|
|
1472
1393
|
}
|
|
1473
1394
|
function ifft(re, im, workspace) {
|
|
1474
1395
|
const size = re.length;
|
|
1396
|
+
assertRadix2Size(size);
|
|
1397
|
+
if (im.length !== size) throw new Error(`IFFT real and imaginary lengths must match, got ${size} and ${im.length}`);
|
|
1398
|
+
if (workspace !== void 0) assertWorkspaceCapacity(workspace, size);
|
|
1475
1399
|
const outRe = workspace ? workspace.outRe : Float32Array.from(re);
|
|
1476
1400
|
const outIm = workspace ? workspace.outIm : new Float32Array(size);
|
|
1477
1401
|
if (workspace) outRe.set(re);
|
|
@@ -1486,6 +1410,8 @@ function ifft(re, im, workspace) {
|
|
|
1486
1410
|
return outRe;
|
|
1487
1411
|
}
|
|
1488
1412
|
function bitReverse(re, im, size) {
|
|
1413
|
+
assertRadix2Size(size);
|
|
1414
|
+
assertComplexCapacity(re, im, size, "bitReverse");
|
|
1489
1415
|
let rev = 0;
|
|
1490
1416
|
for (let index = 0; index < size - 1; index++) {
|
|
1491
1417
|
if (index < rev) {
|
|
@@ -1504,11 +1430,11 @@ function bitReverse(re, im, size) {
|
|
|
1504
1430
|
rev += bit;
|
|
1505
1431
|
}
|
|
1506
1432
|
}
|
|
1507
|
-
var twiddleCache =
|
|
1433
|
+
var twiddleCache = new ByteBoundedCache(TWIDDLE_CACHE_BYTES);
|
|
1508
1434
|
function getTwiddleFactors(size) {
|
|
1509
1435
|
let cached = twiddleCache.get(size);
|
|
1510
1436
|
if (cached) return cached;
|
|
1511
|
-
const totalFactors = size
|
|
1437
|
+
const totalFactors = size - 1;
|
|
1512
1438
|
const twRe = new Float32Array(totalFactors);
|
|
1513
1439
|
const twIm = new Float32Array(totalFactors);
|
|
1514
1440
|
let offset = 0;
|
|
@@ -1522,10 +1448,14 @@ function getTwiddleFactors(size) {
|
|
|
1522
1448
|
offset += halfStep;
|
|
1523
1449
|
}
|
|
1524
1450
|
cached = { re: twRe, im: twIm };
|
|
1525
|
-
|
|
1451
|
+
if (twRe.byteLength > 0) {
|
|
1452
|
+
twiddleCache.set(size, cached, twRe.byteLength + twIm.byteLength);
|
|
1453
|
+
}
|
|
1526
1454
|
return cached;
|
|
1527
1455
|
}
|
|
1528
1456
|
function butterflyStages(re, im, size) {
|
|
1457
|
+
assertRadix2Size(size);
|
|
1458
|
+
assertComplexCapacity(re, im, size, "butterflyStages");
|
|
1529
1459
|
const twiddle = getTwiddleFactors(size);
|
|
1530
1460
|
const twRe = twiddle.re;
|
|
1531
1461
|
const twIm = twiddle.im;
|
|
@@ -1553,55 +1483,99 @@ function butterflyStages(re, im, size) {
|
|
|
1553
1483
|
twOffset += halfStep;
|
|
1554
1484
|
}
|
|
1555
1485
|
}
|
|
1486
|
+
function assertWorkspaceCapacity(workspace, size) {
|
|
1487
|
+
assertWorkspaceArrayCapacity("re", workspace.re, size);
|
|
1488
|
+
assertWorkspaceArrayCapacity("im", workspace.im, size);
|
|
1489
|
+
assertWorkspaceArrayCapacity("outRe", workspace.outRe, size);
|
|
1490
|
+
assertWorkspaceArrayCapacity("outIm", workspace.outIm, size);
|
|
1491
|
+
}
|
|
1492
|
+
function assertWorkspaceArrayCapacity(name, values, size) {
|
|
1493
|
+
if (values.length < size) {
|
|
1494
|
+
throw new Error(`FFT workspace ${name} capacity must be at least ${size}, got ${values.length}`);
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
function assertComplexCapacity(re, im, size, operation) {
|
|
1498
|
+
if (re.length < size || im.length < size) {
|
|
1499
|
+
throw new Error(`${operation} complex-array capacity must be at least ${size}`);
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1556
1502
|
var TAPS_PER_PHASE_4X = 12;
|
|
1557
1503
|
var HISTORY_LENGTH_4X = TAPS_PER_PHASE_4X;
|
|
1558
|
-
var
|
|
1559
|
-
var
|
|
1560
|
-
var
|
|
1561
|
-
var
|
|
1562
|
-
var
|
|
1563
|
-
var
|
|
1564
|
-
var
|
|
1565
|
-
var
|
|
1566
|
-
var
|
|
1567
|
-
var
|
|
1568
|
-
var
|
|
1569
|
-
var
|
|
1570
|
-
var
|
|
1571
|
-
var
|
|
1572
|
-
var
|
|
1573
|
-
var
|
|
1574
|
-
var
|
|
1575
|
-
var
|
|
1576
|
-
var
|
|
1577
|
-
var
|
|
1578
|
-
var
|
|
1579
|
-
var
|
|
1580
|
-
var
|
|
1581
|
-
var
|
|
1582
|
-
var
|
|
1583
|
-
var
|
|
1584
|
-
var
|
|
1585
|
-
var
|
|
1586
|
-
var
|
|
1587
|
-
var
|
|
1588
|
-
var
|
|
1589
|
-
var
|
|
1590
|
-
var
|
|
1591
|
-
var
|
|
1592
|
-
var
|
|
1593
|
-
var
|
|
1504
|
+
var FLUSH_INPUT_4X = new Float32Array(TAPS_PER_PHASE_4X - 1);
|
|
1505
|
+
var P0T0 = 0.001708984375;
|
|
1506
|
+
var P0T1 = 0.010986328125;
|
|
1507
|
+
var P0T2 = -0.0196533203125;
|
|
1508
|
+
var P0T3 = 0.033203125;
|
|
1509
|
+
var P0T4 = -0.0594482421875;
|
|
1510
|
+
var P0T5 = 0.1373291015625;
|
|
1511
|
+
var P0T6 = 0.97216796875;
|
|
1512
|
+
var P0T7 = -0.102294921875;
|
|
1513
|
+
var P0T8 = 0.047607421875;
|
|
1514
|
+
var P0T9 = -0.026611328125;
|
|
1515
|
+
var P0T10 = 0.014892578125;
|
|
1516
|
+
var P0T11 = -0.00830078125;
|
|
1517
|
+
var P1T0 = -0.0291748046875;
|
|
1518
|
+
var P1T1 = 0.029296875;
|
|
1519
|
+
var P1T2 = -0.0517578125;
|
|
1520
|
+
var P1T3 = 0.089111328125;
|
|
1521
|
+
var P1T4 = -0.16650390625;
|
|
1522
|
+
var P1T5 = 0.465087890625;
|
|
1523
|
+
var P1T6 = 0.77978515625;
|
|
1524
|
+
var P1T7 = -0.2003173828125;
|
|
1525
|
+
var P1T8 = 0.1015625;
|
|
1526
|
+
var P1T9 = -0.0582275390625;
|
|
1527
|
+
var P1T10 = 0.0330810546875;
|
|
1528
|
+
var P1T11 = -0.0189208984375;
|
|
1529
|
+
var P2T0 = -0.0189208984375;
|
|
1530
|
+
var P2T1 = 0.0330810546875;
|
|
1531
|
+
var P2T2 = -0.0582275390625;
|
|
1532
|
+
var P2T3 = 0.1015625;
|
|
1533
|
+
var P2T4 = -0.2003173828125;
|
|
1534
|
+
var P2T5 = 0.77978515625;
|
|
1535
|
+
var P2T6 = 0.465087890625;
|
|
1536
|
+
var P2T7 = -0.16650390625;
|
|
1537
|
+
var P2T8 = 0.089111328125;
|
|
1538
|
+
var P2T9 = -0.0517578125;
|
|
1539
|
+
var P2T10 = 0.029296875;
|
|
1540
|
+
var P2T11 = -0.0291748046875;
|
|
1541
|
+
var P3T0 = -0.00830078125;
|
|
1542
|
+
var P3T1 = 0.014892578125;
|
|
1543
|
+
var P3T2 = -0.026611328125;
|
|
1544
|
+
var P3T3 = 0.047607421875;
|
|
1545
|
+
var P3T4 = -0.102294921875;
|
|
1546
|
+
var P3T5 = 0.97216796875;
|
|
1547
|
+
var P3T6 = 0.1373291015625;
|
|
1548
|
+
var P3T7 = -0.0594482421875;
|
|
1549
|
+
var P3T8 = 0.033203125;
|
|
1550
|
+
var P3T9 = -0.0196533203125;
|
|
1551
|
+
var P3T10 = 0.010986328125;
|
|
1552
|
+
var P3T11 = 0.001708984375;
|
|
1594
1553
|
var TruePeakUpsampler = class {
|
|
1595
1554
|
constructor(factor = 4) {
|
|
1596
1555
|
this.work = new Float64Array(0);
|
|
1556
|
+
this.flushed = false;
|
|
1597
1557
|
if (factor !== 4) {
|
|
1598
|
-
throw new Error(`TruePeakUpsampler: factor ${factor} is not yet implemented; only 4\xD7 (BS.1770-
|
|
1558
|
+
throw new Error(`TruePeakUpsampler: factor ${factor} is not yet implemented; only 4\xD7 (BS.1770-5 Annex 2) is supported`);
|
|
1599
1559
|
}
|
|
1600
1560
|
this.factor = factor;
|
|
1601
1561
|
this.history = new Float64Array(HISTORY_LENGTH_4X);
|
|
1602
1562
|
}
|
|
1603
|
-
// `outputScratch` (>= input.length × factor) avoids the per-call allocation; the returned view aliases it.
|
|
1604
1563
|
upsample(input, outputScratch) {
|
|
1564
|
+
if (this.flushed) throw new Error("TruePeakUpsampler: upsample after flush; call reset() first");
|
|
1565
|
+
return this.process(input, outputScratch);
|
|
1566
|
+
}
|
|
1567
|
+
flush(outputScratch) {
|
|
1568
|
+
if (this.flushed) return new Float32Array(0);
|
|
1569
|
+
const output = this.process(FLUSH_INPUT_4X, outputScratch);
|
|
1570
|
+
this.flushed = true;
|
|
1571
|
+
return output;
|
|
1572
|
+
}
|
|
1573
|
+
reset() {
|
|
1574
|
+
this.history.fill(0);
|
|
1575
|
+
this.work = new Float64Array(0);
|
|
1576
|
+
this.flushed = false;
|
|
1577
|
+
}
|
|
1578
|
+
process(input, outputScratch) {
|
|
1605
1579
|
const factor = this.factor;
|
|
1606
1580
|
const inputLength = input.length;
|
|
1607
1581
|
const outputLength = inputLength * factor;
|
|
@@ -1614,67 +1588,80 @@ var TruePeakUpsampler = class {
|
|
|
1614
1588
|
}
|
|
1615
1589
|
const work = this.work;
|
|
1616
1590
|
work.set(history, 0);
|
|
1617
|
-
for (let
|
|
1618
|
-
work[historyLength +
|
|
1619
|
-
}
|
|
1620
|
-
for (let
|
|
1621
|
-
const
|
|
1622
|
-
const
|
|
1623
|
-
|
|
1624
|
-
const
|
|
1625
|
-
const
|
|
1626
|
-
const
|
|
1627
|
-
const
|
|
1628
|
-
const
|
|
1629
|
-
const
|
|
1630
|
-
const
|
|
1631
|
-
const
|
|
1632
|
-
const
|
|
1633
|
-
const
|
|
1634
|
-
const
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1591
|
+
for (let inputIndex = 0; inputIndex < inputLength; inputIndex++) {
|
|
1592
|
+
work[historyLength + inputIndex] = input[inputIndex] ?? 0;
|
|
1593
|
+
}
|
|
1594
|
+
for (let inputIndex = 0; inputIndex < inputLength; inputIndex++) {
|
|
1595
|
+
const currentIndex = historyLength + inputIndex;
|
|
1596
|
+
const outputOffset = inputIndex * factor;
|
|
1597
|
+
const value0 = work[currentIndex] ?? 0;
|
|
1598
|
+
const value1 = work[currentIndex - 1] ?? 0;
|
|
1599
|
+
const value2 = work[currentIndex - 2] ?? 0;
|
|
1600
|
+
const value3 = work[currentIndex - 3] ?? 0;
|
|
1601
|
+
const value4 = work[currentIndex - 4] ?? 0;
|
|
1602
|
+
const value5 = work[currentIndex - 5] ?? 0;
|
|
1603
|
+
const value6 = work[currentIndex - 6] ?? 0;
|
|
1604
|
+
const value7 = work[currentIndex - 7] ?? 0;
|
|
1605
|
+
const value8 = work[currentIndex - 8] ?? 0;
|
|
1606
|
+
const value9 = work[currentIndex - 9] ?? 0;
|
|
1607
|
+
const value10 = work[currentIndex - 10] ?? 0;
|
|
1608
|
+
const value11 = work[currentIndex - 11] ?? 0;
|
|
1609
|
+
let phase0 = 0;
|
|
1610
|
+
phase0 += P0T0 * value0;
|
|
1611
|
+
phase0 += P0T1 * value1;
|
|
1612
|
+
phase0 += P0T2 * value2;
|
|
1613
|
+
phase0 += P0T3 * value3;
|
|
1614
|
+
phase0 += P0T4 * value4;
|
|
1615
|
+
phase0 += P0T5 * value5;
|
|
1616
|
+
phase0 += P0T6 * value6;
|
|
1617
|
+
phase0 += P0T7 * value7;
|
|
1618
|
+
phase0 += P0T8 * value8;
|
|
1619
|
+
phase0 += P0T9 * value9;
|
|
1620
|
+
phase0 += P0T10 * value10;
|
|
1621
|
+
phase0 += P0T11 * value11;
|
|
1622
|
+
let phase1 = 0;
|
|
1623
|
+
phase1 += P1T0 * value0;
|
|
1624
|
+
phase1 += P1T1 * value1;
|
|
1625
|
+
phase1 += P1T2 * value2;
|
|
1626
|
+
phase1 += P1T3 * value3;
|
|
1627
|
+
phase1 += P1T4 * value4;
|
|
1628
|
+
phase1 += P1T5 * value5;
|
|
1629
|
+
phase1 += P1T6 * value6;
|
|
1630
|
+
phase1 += P1T7 * value7;
|
|
1631
|
+
phase1 += P1T8 * value8;
|
|
1632
|
+
phase1 += P1T9 * value9;
|
|
1633
|
+
phase1 += P1T10 * value10;
|
|
1634
|
+
phase1 += P1T11 * value11;
|
|
1635
|
+
let phase2 = 0;
|
|
1636
|
+
phase2 += P2T0 * value0;
|
|
1637
|
+
phase2 += P2T1 * value1;
|
|
1638
|
+
phase2 += P2T2 * value2;
|
|
1639
|
+
phase2 += P2T3 * value3;
|
|
1640
|
+
phase2 += P2T4 * value4;
|
|
1641
|
+
phase2 += P2T5 * value5;
|
|
1642
|
+
phase2 += P2T6 * value6;
|
|
1643
|
+
phase2 += P2T7 * value7;
|
|
1644
|
+
phase2 += P2T8 * value8;
|
|
1645
|
+
phase2 += P2T9 * value9;
|
|
1646
|
+
phase2 += P2T10 * value10;
|
|
1647
|
+
phase2 += P2T11 * value11;
|
|
1648
|
+
let phase3 = 0;
|
|
1649
|
+
phase3 += P3T0 * value0;
|
|
1650
|
+
phase3 += P3T1 * value1;
|
|
1651
|
+
phase3 += P3T2 * value2;
|
|
1652
|
+
phase3 += P3T3 * value3;
|
|
1653
|
+
phase3 += P3T4 * value4;
|
|
1654
|
+
phase3 += P3T5 * value5;
|
|
1655
|
+
phase3 += P3T6 * value6;
|
|
1656
|
+
phase3 += P3T7 * value7;
|
|
1657
|
+
phase3 += P3T8 * value8;
|
|
1658
|
+
phase3 += P3T9 * value9;
|
|
1659
|
+
phase3 += P3T10 * value10;
|
|
1660
|
+
phase3 += P3T11 * value11;
|
|
1661
|
+
output[outputOffset] = phase0;
|
|
1662
|
+
output[outputOffset + 1] = phase1;
|
|
1663
|
+
output[outputOffset + 2] = phase2;
|
|
1664
|
+
output[outputOffset + 3] = phase3;
|
|
1678
1665
|
}
|
|
1679
1666
|
if (inputLength >= historyLength) {
|
|
1680
1667
|
history.set(work.subarray(workLength - historyLength, workLength), 0);
|
|
@@ -1684,16 +1671,13 @@ var TruePeakUpsampler = class {
|
|
|
1684
1671
|
}
|
|
1685
1672
|
return output;
|
|
1686
1673
|
}
|
|
1687
|
-
reset() {
|
|
1688
|
-
this.history.fill(0);
|
|
1689
|
-
this.work = new Float64Array(0);
|
|
1690
|
-
}
|
|
1691
1674
|
};
|
|
1692
1675
|
var DEFAULT_OVERSAMPLE_FACTOR = 4;
|
|
1693
1676
|
var TruePeakAccumulator = class {
|
|
1694
1677
|
constructor(_sampleRate, channelCount, oversampleFactor = DEFAULT_OVERSAMPLE_FACTOR) {
|
|
1695
1678
|
this.upsampleScratch = new Float32Array(0);
|
|
1696
|
-
this.
|
|
1679
|
+
this.interpolatedMax = 0;
|
|
1680
|
+
this.rawMax = 0;
|
|
1697
1681
|
if (channelCount <= 0) {
|
|
1698
1682
|
throw new Error(`TruePeakAccumulator: channelCount must be positive, got ${channelCount}`);
|
|
1699
1683
|
}
|
|
@@ -1706,6 +1690,7 @@ var TruePeakAccumulator = class {
|
|
|
1706
1690
|
}
|
|
1707
1691
|
// `channels[c]` needs >= `frames` valid samples from index 0 (oversized OK); advances per-channel upsampler state as if appended contiguously.
|
|
1708
1692
|
push(channels, frames) {
|
|
1693
|
+
if (this.finalizedResult !== void 0) throw new Error("TruePeakAccumulator: push after finalize");
|
|
1709
1694
|
if (channels.length !== this.channelCount) {
|
|
1710
1695
|
throw new Error(`TruePeakAccumulator: push got ${channels.length} channels, expected ${this.channelCount}`);
|
|
1711
1696
|
}
|
|
@@ -1720,6 +1705,11 @@ var TruePeakAccumulator = class {
|
|
|
1720
1705
|
throw new Error(`TruePeakAccumulator: missing upsampler for channel ${channelIndex}`);
|
|
1721
1706
|
}
|
|
1722
1707
|
const slice = channel.length === frames ? channel : channel.subarray(0, frames);
|
|
1708
|
+
for (let index = 0; index < slice.length; index++) {
|
|
1709
|
+
const sample = slice[index] ?? 0;
|
|
1710
|
+
const magnitude = sample < 0 ? -sample : sample;
|
|
1711
|
+
if (magnitude > this.rawMax) this.rawMax = magnitude;
|
|
1712
|
+
}
|
|
1723
1713
|
if (this.upsampleScratch.length < frames * upsampler.factor) {
|
|
1724
1714
|
this.upsampleScratch = new Float32Array(frames * upsampler.factor);
|
|
1725
1715
|
}
|
|
@@ -1727,14 +1717,46 @@ var TruePeakAccumulator = class {
|
|
|
1727
1717
|
for (let index = 0; index < upsampled.length; index++) {
|
|
1728
1718
|
const sample = upsampled[index] ?? 0;
|
|
1729
1719
|
const magnitude = sample < 0 ? -sample : sample;
|
|
1730
|
-
if (magnitude > this.
|
|
1720
|
+
if (magnitude > this.interpolatedMax) this.interpolatedMax = magnitude;
|
|
1731
1721
|
}
|
|
1732
1722
|
}
|
|
1733
1723
|
}
|
|
1734
1724
|
finalize() {
|
|
1735
|
-
return this.
|
|
1725
|
+
if (this.finalizedResult !== void 0) return this.finalizedResult;
|
|
1726
|
+
for (const upsampler of this.upsamplers) {
|
|
1727
|
+
const flushLength = 11 * upsampler.factor;
|
|
1728
|
+
if (this.upsampleScratch.length < flushLength) {
|
|
1729
|
+
this.upsampleScratch = new Float32Array(flushLength);
|
|
1730
|
+
}
|
|
1731
|
+
const tail = upsampler.flush(this.upsampleScratch);
|
|
1732
|
+
for (let index = 0; index < tail.length; index++) {
|
|
1733
|
+
const sample = tail[index] ?? 0;
|
|
1734
|
+
const magnitude = sample < 0 ? -sample : sample;
|
|
1735
|
+
if (magnitude > this.interpolatedMax) this.interpolatedMax = magnitude;
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
this.finalizedResult = Math.max(this.rawMax, this.interpolatedMax);
|
|
1739
|
+
return this.finalizedResult;
|
|
1736
1740
|
}
|
|
1737
1741
|
};
|
|
1742
|
+
var MAX_DFTT_BATCH_BYTES = 32 * 1024 * 1024;
|
|
1743
|
+
function getDfttBatchBlockCount(blockSize, complexBlockSize, maxBatchBytes) {
|
|
1744
|
+
if (!Number.isSafeInteger(blockSize) || blockSize <= 0 || !Number.isSafeInteger(complexBlockSize) || complexBlockSize <= 0) {
|
|
1745
|
+
throw new Error("DFTT block sizes must be positive integers");
|
|
1746
|
+
}
|
|
1747
|
+
if (!Number.isFinite(maxBatchBytes) || maxBatchBytes <= 0 || maxBatchBytes > MAX_DFTT_BATCH_BYTES) {
|
|
1748
|
+
throw new Error(`DFTT maxBatchBytes must be finite and in (0, ${MAX_DFTT_BATCH_BYTES}]`);
|
|
1749
|
+
}
|
|
1750
|
+
const bytesPerBlock = Float32Array.BYTES_PER_ELEMENT * (3 * blockSize + 4 * complexBlockSize);
|
|
1751
|
+
if (!Number.isSafeInteger(bytesPerBlock)) {
|
|
1752
|
+
throw new Error("DFTT block geometry exceeds the safe integer range");
|
|
1753
|
+
}
|
|
1754
|
+
const blockCount = Math.floor(maxBatchBytes / bytesPerBlock);
|
|
1755
|
+
if (blockCount < 1) {
|
|
1756
|
+
throw new Error(`One DFTT block requires ${bytesPerBlock} bytes, exceeding the ${maxBatchBytes}-byte batch budget`);
|
|
1757
|
+
}
|
|
1758
|
+
return blockCount;
|
|
1759
|
+
}
|
|
1738
1760
|
function complexFft(inRe, inIm, outRe, outIm, workspaceA, workspaceB) {
|
|
1739
1761
|
const { re: reOfRe, im: imOfRe } = fft(inRe, workspaceA);
|
|
1740
1762
|
const { re: reOfIm, im: imOfIm } = fft(inIm, workspaceB);
|
|
@@ -1744,9 +1766,51 @@ function complexFft(inRe, inIm, outRe, outIm, workspaceA, workspaceB) {
|
|
|
1744
1766
|
outIm[ii] = imOfRe[ii] + reOfIm[ii];
|
|
1745
1767
|
}
|
|
1746
1768
|
}
|
|
1747
|
-
function
|
|
1769
|
+
function complexIfft(inRe, inIm, outRe, outIm, negativeReal, workspaceA, workspaceB) {
|
|
1770
|
+
const realResult = ifft(inRe, inIm, workspaceA);
|
|
1771
|
+
outRe.set(realResult);
|
|
1772
|
+
for (let index = 0; index < inRe.length; index++) negativeReal[index] = -inRe[index];
|
|
1773
|
+
const imaginaryResult = ifft(inIm, negativeReal, workspaceB);
|
|
1774
|
+
outIm.set(imaginaryResult);
|
|
1775
|
+
}
|
|
1776
|
+
function getWienerGain(signalMagnitudeSquared, noiseMagnitudeSquared) {
|
|
1777
|
+
if (noiseMagnitudeSquared === 0) return signalMagnitudeSquared === 0 ? 0 : 1;
|
|
1778
|
+
return signalMagnitudeSquared / (signalMagnitudeSquared + noiseMagnitudeSquared);
|
|
1779
|
+
}
|
|
1780
|
+
function applyDfttSmoothing(nlmSmoothed, rawMask, numFrames, numBins, dfttOptions, output, fftBackend, fftAddonOptions, profileMs, executionOptions) {
|
|
1781
|
+
const { blockFreq, blockTime, hopFreq, hopTime, threshold } = dfttOptions;
|
|
1782
|
+
const maskLength = numFrames * numBins;
|
|
1783
|
+
assertPositiveSafeInteger(numFrames, "DFTT numFrames");
|
|
1784
|
+
assertPositiveSafeInteger(numBins, "DFTT numBins");
|
|
1785
|
+
if (!Number.isSafeInteger(maskLength)) {
|
|
1786
|
+
throw new Error("DFTT mask dimensions exceed the safe integer range");
|
|
1787
|
+
}
|
|
1788
|
+
if (nlmSmoothed.length !== maskLength || rawMask.length !== maskLength || output.length !== maskLength) {
|
|
1789
|
+
throw new Error(`DFTT input and output lengths must equal ${maskLength}`);
|
|
1790
|
+
}
|
|
1791
|
+
assertPowerOfTwo(blockFreq, "DFTT blockFreq");
|
|
1792
|
+
assertPowerOfTwo(blockTime, "DFTT blockTime");
|
|
1793
|
+
assertPositiveSafeInteger(hopFreq, "DFTT hopFreq");
|
|
1794
|
+
assertPositiveSafeInteger(hopTime, "DFTT hopTime");
|
|
1795
|
+
if (hopFreq > blockFreq || hopTime > blockTime) {
|
|
1796
|
+
throw new Error("DFTT hops must be no larger than their block dimensions");
|
|
1797
|
+
}
|
|
1798
|
+
if (!Number.isFinite(threshold) || threshold < 0) {
|
|
1799
|
+
throw new Error(`DFTT threshold must be finite and nonnegative, got ${threshold}`);
|
|
1800
|
+
}
|
|
1801
|
+
const blockSize = blockTime * blockFreq;
|
|
1802
|
+
const complexBlockSize = blockTime * (Math.floor(blockFreq / 2) + 1);
|
|
1803
|
+
const maxBatchBytes = executionOptions?.maxBatchBytes ?? MAX_DFTT_BATCH_BYTES;
|
|
1804
|
+
const batchBlockCapacity = getDfttBatchBlockCount(blockSize, complexBlockSize, maxBatchBytes);
|
|
1805
|
+
if (threshold === 0) {
|
|
1806
|
+
output.set(rawMask);
|
|
1807
|
+
return;
|
|
1808
|
+
}
|
|
1809
|
+
if (typedArraysOverlap(output, rawMask) || typedArraysOverlap(output, nlmSmoothed)) {
|
|
1810
|
+
throw new Error("DFTT output must not overlap either input for non-bypass processing");
|
|
1811
|
+
}
|
|
1748
1812
|
const addon = fftBackend ? getFftAddon(fftBackend, fftAddonOptions) : null;
|
|
1749
|
-
if (!addon || typeof addon.batchFft2D !== "function") {
|
|
1813
|
+
if (!addon || typeof addon.batchFft2D !== "function" || typeof addon.batchIfft2D !== "function") {
|
|
1750
1814
|
applyDfttSmoothingJs(nlmSmoothed, rawMask, numFrames, numBins, dfttOptions, output);
|
|
1751
1815
|
return;
|
|
1752
1816
|
}
|
|
@@ -1757,10 +1821,9 @@ function applyDfttSmoothing(nlmSmoothed, rawMask, numFrames, numBins, dfttOption
|
|
|
1757
1821
|
profileMs[key] += now - profileMark;
|
|
1758
1822
|
profileMark = now;
|
|
1759
1823
|
};
|
|
1760
|
-
const { blockFreq, blockTime, hopFreq, hopTime, threshold } = dfttOptions;
|
|
1761
1824
|
const winFreq = hanningWindow(blockFreq, false);
|
|
1762
1825
|
const winTime = hanningWindow(blockTime, false);
|
|
1763
|
-
const win2d = new Float32Array(
|
|
1826
|
+
const win2d = new Float32Array(blockSize);
|
|
1764
1827
|
for (let tf = 0; tf < blockTime; tf++) {
|
|
1765
1828
|
for (let bf = 0; bf < blockFreq; bf++) {
|
|
1766
1829
|
win2d[tf * blockFreq + bf] = winTime[tf] * winFreq[bf];
|
|
@@ -1769,82 +1832,83 @@ function applyDfttSmoothing(nlmSmoothed, rawMask, numFrames, numBins, dfttOption
|
|
|
1769
1832
|
const blocksPerFrame = Math.ceil(numFrames / hopTime);
|
|
1770
1833
|
const blocksPerBin = Math.ceil(numBins / hopFreq);
|
|
1771
1834
|
const totalBlocks = blocksPerFrame * blocksPerBin;
|
|
1772
|
-
const
|
|
1773
|
-
const
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
const
|
|
1779
|
-
for (let
|
|
1780
|
-
const
|
|
1781
|
-
const
|
|
1782
|
-
const
|
|
1835
|
+
const sigmaSq = threshold * threshold;
|
|
1836
|
+
const windowSumSq = new Float32Array(maskLength);
|
|
1837
|
+
output.fill(0);
|
|
1838
|
+
for (let firstGlobalBlock = 0; firstGlobalBlock < totalBlocks; firstGlobalBlock += batchBlockCapacity) {
|
|
1839
|
+
const batchCount = Math.min(batchBlockCapacity, totalBlocks - firstGlobalBlock);
|
|
1840
|
+
const rawBatch = new Float32Array(batchCount * blockSize);
|
|
1841
|
+
const nlmBatch = new Float32Array(batchCount * blockSize);
|
|
1842
|
+
for (let localBlock = 0; localBlock < batchCount; localBlock++) {
|
|
1843
|
+
const globalBlock = firstGlobalBlock + localBlock;
|
|
1844
|
+
const frameIndex = Math.floor(globalBlock / blocksPerBin);
|
|
1845
|
+
const binIndex = globalBlock % blocksPerBin;
|
|
1846
|
+
const frameStart = frameIndex * hopTime;
|
|
1847
|
+
const binStart = binIndex * hopFreq;
|
|
1848
|
+
const blockOffset = localBlock * blockSize;
|
|
1783
1849
|
for (let tf = 0; tf < blockTime; tf++) {
|
|
1784
|
-
const srcFrame = frameStart + tf
|
|
1850
|
+
const srcFrame = Math.min(frameStart + tf, numFrames - 1);
|
|
1785
1851
|
for (let bf = 0; bf < blockFreq; bf++) {
|
|
1786
|
-
const srcBin = binStart + bf
|
|
1787
|
-
const
|
|
1788
|
-
const
|
|
1789
|
-
const
|
|
1790
|
-
rawBatch[
|
|
1791
|
-
nlmBatch[
|
|
1852
|
+
const srcBin = Math.min(binStart + bf, numBins - 1);
|
|
1853
|
+
const windowValue = win2d[tf * blockFreq + bf];
|
|
1854
|
+
const sourcePosition = srcFrame * numBins + srcBin;
|
|
1855
|
+
const batchPosition = blockOffset + tf * blockFreq + bf;
|
|
1856
|
+
rawBatch[batchPosition] = rawMask[sourcePosition] * windowValue;
|
|
1857
|
+
nlmBatch[batchPosition] = nlmSmoothed[sourcePosition] * windowValue;
|
|
1792
1858
|
}
|
|
1793
1859
|
}
|
|
1794
1860
|
}
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
const
|
|
1823
|
-
const blockIdx = frameIdx * blocksPerBin + binIdx;
|
|
1824
|
-
const blockOffset = blockIdx * blockSize;
|
|
1861
|
+
profileAdd("fill");
|
|
1862
|
+
const rawFft = addon.batchFft2D(rawBatch, blockTime, blockFreq, batchCount);
|
|
1863
|
+
const nlmFft = addon.batchFft2D(nlmBatch, blockTime, blockFreq, batchCount);
|
|
1864
|
+
const batchComplexLength = batchCount * complexBlockSize;
|
|
1865
|
+
assertArrayLength(rawFft.re, batchComplexLength, "DFTT addon raw real output");
|
|
1866
|
+
assertArrayLength(rawFft.im, batchComplexLength, "DFTT addon raw imaginary output");
|
|
1867
|
+
assertArrayLength(nlmFft.re, batchComplexLength, "DFTT addon NLM real output");
|
|
1868
|
+
assertArrayLength(nlmFft.im, batchComplexLength, "DFTT addon NLM imaginary output");
|
|
1869
|
+
profileAdd("forward");
|
|
1870
|
+
for (let flatIndex = 0; flatIndex < batchComplexLength; flatIndex++) {
|
|
1871
|
+
const nlmReal = nlmFft.re[flatIndex];
|
|
1872
|
+
const nlmImaginary = nlmFft.im[flatIndex];
|
|
1873
|
+
const nlmMagnitudeSquared = nlmReal * nlmReal + nlmImaginary * nlmImaginary;
|
|
1874
|
+
const gain = getWienerGain(nlmMagnitudeSquared, sigmaSq);
|
|
1875
|
+
rawFft.re[flatIndex] = rawFft.re[flatIndex] * gain;
|
|
1876
|
+
rawFft.im[flatIndex] = rawFft.im[flatIndex] * gain;
|
|
1877
|
+
}
|
|
1878
|
+
profileAdd("gain");
|
|
1879
|
+
const synth = addon.batchIfft2D(rawFft.re, rawFft.im, blockTime, blockFreq, batchCount);
|
|
1880
|
+
assertArrayLength(synth, batchCount * blockSize, "DFTT addon inverse output");
|
|
1881
|
+
profileAdd("inverse");
|
|
1882
|
+
for (let localBlock = 0; localBlock < batchCount; localBlock++) {
|
|
1883
|
+
const globalBlock = firstGlobalBlock + localBlock;
|
|
1884
|
+
const frameIndex = Math.floor(globalBlock / blocksPerBin);
|
|
1885
|
+
const binIndex = globalBlock % blocksPerBin;
|
|
1886
|
+
const frameStart = frameIndex * hopTime;
|
|
1887
|
+
const binStart = binIndex * hopFreq;
|
|
1888
|
+
const blockOffset = localBlock * blockSize;
|
|
1825
1889
|
for (let tf = 0; tf < blockTime; tf++) {
|
|
1826
|
-
const
|
|
1827
|
-
if (
|
|
1890
|
+
const destinationFrame = frameStart + tf;
|
|
1891
|
+
if (destinationFrame >= numFrames) break;
|
|
1828
1892
|
for (let bf = 0; bf < blockFreq; bf++) {
|
|
1829
|
-
const
|
|
1830
|
-
if (
|
|
1831
|
-
const
|
|
1832
|
-
const
|
|
1833
|
-
const
|
|
1834
|
-
|
|
1835
|
-
windowSumSq[
|
|
1893
|
+
const destinationBin = binStart + bf;
|
|
1894
|
+
if (destinationBin >= numBins) break;
|
|
1895
|
+
const windowValue = win2d[tf * blockFreq + bf];
|
|
1896
|
+
const destinationPosition = destinationFrame * numBins + destinationBin;
|
|
1897
|
+
const sourceValue = synth[blockOffset + tf * blockFreq + bf];
|
|
1898
|
+
output[destinationPosition] = output[destinationPosition] + sourceValue * windowValue;
|
|
1899
|
+
windowSumSq[destinationPosition] = windowSumSq[destinationPosition] + windowValue * windowValue;
|
|
1836
1900
|
}
|
|
1837
1901
|
}
|
|
1838
1902
|
}
|
|
1903
|
+
profileAdd("ola");
|
|
1839
1904
|
}
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
output[flatIdx] = normalisedVal < 0 ? 0 : normalisedVal > 1 ? 1 : normalisedVal;
|
|
1905
|
+
for (let flatIndex = 0; flatIndex < maskLength; flatIndex++) {
|
|
1906
|
+
const windowWeight = windowSumSq[flatIndex];
|
|
1907
|
+
if (windowWeight > 1e-8) {
|
|
1908
|
+
const normalizedValue = output[flatIndex] / windowWeight;
|
|
1909
|
+
output[flatIndex] = normalizedValue < 0 ? 0 : normalizedValue > 1 ? 1 : normalizedValue;
|
|
1846
1910
|
} else {
|
|
1847
|
-
output[
|
|
1911
|
+
output[flatIndex] = rawMask[flatIndex];
|
|
1848
1912
|
}
|
|
1849
1913
|
}
|
|
1850
1914
|
profileAdd("normalize");
|
|
@@ -1859,8 +1923,8 @@ function applyDfttSmoothingJs(nlmSmoothed, rawMask, numFrames, numBins, dfttOpti
|
|
|
1859
1923
|
win2d[tf * blockFreq + bf] = winTime[tf] * winFreq[bf];
|
|
1860
1924
|
}
|
|
1861
1925
|
}
|
|
1862
|
-
const accumulator = new Float32Array(numFrames * numBins);
|
|
1863
1926
|
const windowSumSq = new Float32Array(numFrames * numBins);
|
|
1927
|
+
output.fill(0);
|
|
1864
1928
|
const blockRaw = new Float32Array(blockTime * blockFreq);
|
|
1865
1929
|
const blockNlm = new Float32Array(blockTime * blockFreq);
|
|
1866
1930
|
const rawRowRe = new Float32Array(blockTime * blockFreq);
|
|
@@ -1879,6 +1943,7 @@ function applyDfttSmoothingJs(nlmSmoothed, rawMask, numFrames, numBins, dfttOpti
|
|
|
1879
1943
|
const scratchIm = new Float32Array(blockTime);
|
|
1880
1944
|
const scratchOutRe = new Float32Array(blockTime);
|
|
1881
1945
|
const scratchOutIm = new Float32Array(blockTime);
|
|
1946
|
+
const scratchNegativeReal = new Float32Array(blockTime);
|
|
1882
1947
|
const rowScratch = new Float32Array(blockFreq);
|
|
1883
1948
|
const rowScratchRe = new Float32Array(blockFreq);
|
|
1884
1949
|
const rowScratchIm = new Float32Array(blockFreq);
|
|
@@ -1886,7 +1951,8 @@ function applyDfttSmoothingJs(nlmSmoothed, rawMask, numFrames, numBins, dfttOpti
|
|
|
1886
1951
|
const rowFwdWorkspace = createFftWorkspace(blockFreq);
|
|
1887
1952
|
const colFwdWorkspaceA = createFftWorkspace(blockTime);
|
|
1888
1953
|
const colFwdWorkspaceB = createFftWorkspace(blockTime);
|
|
1889
|
-
const
|
|
1954
|
+
const colInvWorkspaceA = createFftWorkspace(blockTime);
|
|
1955
|
+
const colInvWorkspaceB = createFftWorkspace(blockTime);
|
|
1890
1956
|
const rowInvWorkspace = createFftWorkspace(blockFreq);
|
|
1891
1957
|
for (let frameStart = 0; frameStart < numFrames; frameStart += hopTime) {
|
|
1892
1958
|
for (let binStart = 0; binStart < numBins; binStart += hopFreq) {
|
|
@@ -1961,7 +2027,7 @@ function applyDfttSmoothingJs(nlmSmoothed, rawMask, numFrames, numBins, dfttOpti
|
|
|
1961
2027
|
const nlmRe = nlmColRe[flatIdx];
|
|
1962
2028
|
const nlmIm = nlmColIm[flatIdx];
|
|
1963
2029
|
const nlmMagSq = nlmRe * nlmRe + nlmIm * nlmIm;
|
|
1964
|
-
const gain =
|
|
2030
|
+
const gain = getWienerGain(nlmMagSq, sigmaSq);
|
|
1965
2031
|
gainColRe[flatIdx] = rawColRe[flatIdx] * gain;
|
|
1966
2032
|
gainColIm[flatIdx] = rawColIm[flatIdx] * gain;
|
|
1967
2033
|
}
|
|
@@ -1971,15 +2037,24 @@ function applyDfttSmoothingJs(nlmSmoothed, rawMask, numFrames, numBins, dfttOpti
|
|
|
1971
2037
|
scratchRe[tf] = gainColRe[bf * blockTime + tf];
|
|
1972
2038
|
scratchIm[tf] = gainColIm[bf * blockTime + tf];
|
|
1973
2039
|
}
|
|
1974
|
-
|
|
2040
|
+
complexIfft(
|
|
2041
|
+
scratchRe,
|
|
2042
|
+
scratchIm,
|
|
2043
|
+
scratchOutRe,
|
|
2044
|
+
scratchOutIm,
|
|
2045
|
+
scratchNegativeReal,
|
|
2046
|
+
colInvWorkspaceA,
|
|
2047
|
+
colInvWorkspaceB
|
|
2048
|
+
);
|
|
1975
2049
|
for (let tf = 0; tf < blockTime; tf++) {
|
|
1976
|
-
colInRe[bf * blockTime + tf] =
|
|
2050
|
+
colInRe[bf * blockTime + tf] = scratchOutRe[tf];
|
|
2051
|
+
colInIm[bf * blockTime + tf] = scratchOutIm[tf];
|
|
1977
2052
|
}
|
|
1978
2053
|
}
|
|
1979
2054
|
for (let tf = 0; tf < blockTime; tf++) {
|
|
1980
2055
|
for (let bf = 0; bf < blockFreq; bf++) {
|
|
1981
2056
|
rowScratchRe[bf] = colInRe[bf * blockTime + tf];
|
|
1982
|
-
rowScratchIm[bf] =
|
|
2057
|
+
rowScratchIm[bf] = colInIm[bf * blockTime + tf];
|
|
1983
2058
|
}
|
|
1984
2059
|
const irowResult = ifft(rowScratchRe, rowScratchIm, rowInvWorkspace);
|
|
1985
2060
|
for (let bf = 0; bf < blockFreq; bf++) {
|
|
@@ -1994,7 +2069,7 @@ function applyDfttSmoothingJs(nlmSmoothed, rawMask, numFrames, numBins, dfttOpti
|
|
|
1994
2069
|
if (destBin >= numBins) break;
|
|
1995
2070
|
const winVal = win2d[tf * blockFreq + bf];
|
|
1996
2071
|
const destPos = destFrame * numBins + destBin;
|
|
1997
|
-
|
|
2072
|
+
output[destPos] = output[destPos] + synthBlock[tf * blockFreq + bf] * winVal;
|
|
1998
2073
|
windowSumSq[destPos] = windowSumSq[destPos] + winVal * winVal;
|
|
1999
2074
|
}
|
|
2000
2075
|
}
|
|
@@ -2003,15 +2078,73 @@ function applyDfttSmoothingJs(nlmSmoothed, rawMask, numFrames, numBins, dfttOpti
|
|
|
2003
2078
|
for (let flatIdx = 0; flatIdx < numFrames * numBins; flatIdx++) {
|
|
2004
2079
|
const ws = windowSumSq[flatIdx];
|
|
2005
2080
|
if (ws > 1e-8) {
|
|
2006
|
-
const normalisedVal =
|
|
2081
|
+
const normalisedVal = output[flatIdx] / ws;
|
|
2007
2082
|
output[flatIdx] = normalisedVal < 0 ? 0 : normalisedVal > 1 ? 1 : normalisedVal;
|
|
2008
2083
|
} else {
|
|
2009
2084
|
output[flatIdx] = rawMask[flatIdx];
|
|
2010
2085
|
}
|
|
2011
2086
|
}
|
|
2012
2087
|
}
|
|
2088
|
+
function assertPositiveSafeInteger(value, name) {
|
|
2089
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
2090
|
+
throw new Error(`${name} must be a positive integer, got ${value}`);
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
function assertPowerOfTwo(value, name) {
|
|
2094
|
+
assertPositiveSafeInteger(value, name);
|
|
2095
|
+
if (!Number.isInteger(Math.log2(value))) {
|
|
2096
|
+
throw new Error(`${name} must be a power of two, got ${value}`);
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
function typedArraysOverlap(left, right) {
|
|
2100
|
+
if (left.buffer !== right.buffer) return false;
|
|
2101
|
+
const leftEnd = left.byteOffset + left.byteLength;
|
|
2102
|
+
const rightEnd = right.byteOffset + right.byteLength;
|
|
2103
|
+
return left.byteOffset < rightEnd && right.byteOffset < leftEnd;
|
|
2104
|
+
}
|
|
2105
|
+
function assertArrayLength(values, expectedLength, name) {
|
|
2106
|
+
if (values.length !== expectedLength) {
|
|
2107
|
+
throw new Error(`${name} length must equal ${expectedLength}, got ${values.length}`);
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2013
2110
|
function applyNlmSmoothingRange(mask, numFrames, numBins, nlmOptions, output, blockFrameStart, blockFrameEnd) {
|
|
2014
2111
|
const { patchSize, searchFreqRadius, searchTimePre, searchTimePost, pasteBlockSize, threshold } = nlmOptions;
|
|
2112
|
+
const maskLength = numFrames * numBins;
|
|
2113
|
+
assertNonnegativeSafeInteger(numFrames, "NLM numFrames");
|
|
2114
|
+
assertNonnegativeSafeInteger(numBins, "NLM numBins");
|
|
2115
|
+
if (!Number.isSafeInteger(maskLength)) {
|
|
2116
|
+
throw new Error("NLM mask dimensions exceed the safe integer range");
|
|
2117
|
+
}
|
|
2118
|
+
if (mask.length !== maskLength || output.length !== maskLength) {
|
|
2119
|
+
throw new Error(`NLM mask and output lengths must equal ${maskLength}`);
|
|
2120
|
+
}
|
|
2121
|
+
if (!Number.isSafeInteger(patchSize) || patchSize <= 0 || patchSize % 2 !== 0) {
|
|
2122
|
+
throw new Error(`NLM patchSize must be a positive even integer, got ${patchSize}`);
|
|
2123
|
+
}
|
|
2124
|
+
assertNonnegativeSafeInteger(searchFreqRadius, "NLM searchFreqRadius");
|
|
2125
|
+
assertNonnegativeSafeInteger(searchTimePre, "NLM searchTimePre");
|
|
2126
|
+
assertNonnegativeSafeInteger(searchTimePost, "NLM searchTimePost");
|
|
2127
|
+
if (!Number.isSafeInteger(pasteBlockSize) || pasteBlockSize <= 0) {
|
|
2128
|
+
throw new Error(`NLM pasteBlockSize must be a positive integer, got ${pasteBlockSize}`);
|
|
2129
|
+
}
|
|
2130
|
+
if (!Number.isFinite(threshold) || threshold < 0) {
|
|
2131
|
+
throw new Error(`NLM threshold must be finite and nonnegative, got ${threshold}`);
|
|
2132
|
+
}
|
|
2133
|
+
assertNonnegativeSafeInteger(blockFrameStart, "NLM blockFrameStart");
|
|
2134
|
+
assertNonnegativeSafeInteger(blockFrameEnd, "NLM blockFrameEnd");
|
|
2135
|
+
if (blockFrameStart > blockFrameEnd || blockFrameEnd > numFrames || blockFrameStart % pasteBlockSize !== 0 || blockFrameEnd !== numFrames && blockFrameEnd % pasteBlockSize !== 0) {
|
|
2136
|
+
throw new Error("NLM frame range must be ordered, in bounds, and aligned to pasteBlockSize");
|
|
2137
|
+
}
|
|
2138
|
+
if (threshold === 0) {
|
|
2139
|
+
if (blockFrameStart === 0 && blockFrameEnd === numFrames) {
|
|
2140
|
+
output.set(mask);
|
|
2141
|
+
} else {
|
|
2142
|
+
const outputStart = blockFrameStart * numBins;
|
|
2143
|
+
const outputEnd = blockFrameEnd * numBins;
|
|
2144
|
+
output.set(mask.subarray(outputStart, outputEnd), outputStart);
|
|
2145
|
+
}
|
|
2146
|
+
return;
|
|
2147
|
+
}
|
|
2015
2148
|
const hSq = threshold * threshold;
|
|
2016
2149
|
const halfPatch = Math.floor(patchSize / 2);
|
|
2017
2150
|
for (let blockFrame = blockFrameStart; blockFrame < blockFrameEnd; blockFrame += pasteBlockSize) {
|
|
@@ -2050,18 +2183,26 @@ function applyNlmSmoothingRange(mask, numFrames, numBins, nlmOptions, output, bl
|
|
|
2050
2183
|
valueSum += weight * mask[clampedCandFrame * numBins + clampedCandBin];
|
|
2051
2184
|
}
|
|
2052
2185
|
}
|
|
2053
|
-
const
|
|
2186
|
+
const clampedCentreFrame = Math.min(centreFrame, numFrames - 1);
|
|
2187
|
+
const clampedCentreBin = Math.min(centreBin, numBins - 1);
|
|
2188
|
+
const smoothed = weightSum > 0 ? valueSum / weightSum : mask[clampedCentreFrame * numBins + clampedCentreBin];
|
|
2189
|
+
const clampedSmoothed = smoothed < 0 ? 0 : smoothed > 1 ? 1 : smoothed;
|
|
2054
2190
|
for (let pf = 0; pf < pasteBlockSize; pf++) {
|
|
2055
2191
|
const outFrame = blockFrame + pf;
|
|
2056
2192
|
if (outFrame >= numFrames) break;
|
|
2057
2193
|
for (let pb = 0; pb < pasteBlockSize; pb++) {
|
|
2058
2194
|
const outBin = blockBin + pb;
|
|
2059
2195
|
if (outBin >= numBins) break;
|
|
2060
|
-
output[outFrame * numBins + outBin] =
|
|
2196
|
+
output[outFrame * numBins + outBin] = clampedSmoothed;
|
|
2061
2197
|
}
|
|
2062
2198
|
}
|
|
2063
2199
|
}
|
|
2064
2200
|
}
|
|
2065
2201
|
}
|
|
2202
|
+
function assertNonnegativeSafeInteger(value, name) {
|
|
2203
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
2204
|
+
throw new Error(`${name} must be a nonnegative integer, got ${value}`);
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2066
2207
|
|
|
2067
|
-
export { AmplitudeHistogramAccumulator, BidirectionalIir, IntegratedLufsAccumulator, LoudnessAccumulator, MixedRadixFft,
|
|
2208
|
+
export { AmplitudeHistogramAccumulator, BidirectionalIir, IntegratedLufsAccumulator, LoudnessAccumulator, MixedRadixFft, SlidingWindowMaxStream, SlidingWindowMinStream, TruePeakAccumulator, TruePeakUpsampler, __commonJS, __export, __toESM, applyDfttSmoothing, applyNlmSmoothingRange, bandpass, createFftWorkspace, dbToLinear, deinterleaveBuffer, detectFftBackend, fft, getFftAddon, getLraConsideredStats, hanningWindow, initFftBackend, interleave, istft, linearToDb, stft };
|