@buffered-audio/nodes 0.22.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.
@@ -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
- this.sampleRate = options.sampleRate;
236
- const samplePeriod = 1 / this.sampleRate;
237
- const tauBidirectional = this.smoothingMs / 1e3 * Math.SQRT2;
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 ch = 0; ch < channels; ch++) {
429
- interleaved[frame * channels + ch] = samples[ch]?.[frame] ?? 0;
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 ch = 0; ch < channels; ch++) {
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 ch = 0; ch < channels; ch++) {
444
- const channelArray = result[ch];
445
- const value = view[frame * channels + ch];
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 = -10;
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, (ABSOLUTE_GATE_LUFS - LUFS_OFFSET) / 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) + RELATIVE_GATE_OFFSET_LU;
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
- const blockStep = Math.round(BLOCK_STEP_SECONDS * sampleRate);
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 shortTerm = new Array(closed3s.length);
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
- shortTerm[index] = LUFS_OFFSET + 10 * Math.log10(Math.max(sum / blockSize3s, POWER_FLOOR));
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 = computeLraFromShortTerm(shortTerm);
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((lhs, rhs) => lhs - rhs);
964
+ factors.sort((firstFactor, secondFactor) => firstFactor - secondFactor);
940
965
  return factors;
941
966
  }
942
967
  function computePermutation(size, radices) {
943
- const permutation = new Uint16Array(size);
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 batchInputCache = /* @__PURE__ */ new Map();
1318
- function getBatchInput(fftSize, numFrames) {
1319
- const needed = fftSize * numFrames;
1320
- const cached = batchInputCache.get(fftSize);
1321
- if (cached && cached.length >= needed) return cached;
1322
- const grown = new Float32Array(needed);
1323
- batchInputCache.set(fftSize, grown);
1324
- return grown;
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
- var batchTimeCache = /* @__PURE__ */ new Map();
1327
- function getBatchTime(fftSize, numFrames) {
1328
- const needed = fftSize * numFrames;
1329
- const cached = batchTimeCache.get(fftSize);
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
- const window = hanningWindow(fftSize);
1337
- const numFrames = Math.floor((signal.length - fftSize) / hopSize) + 1;
1338
- const halfSize = fftSize / 2 + 1;
1339
- const addon = backend ? getFftAddon(backend, fftAddonOptions) : null;
1340
- const real = output?.real ?? (numFrames > 0 ? new Float32Array(halfSize * numFrames) : new Float32Array(0));
1341
- const imag = output?.imag ?? (numFrames > 0 ? new Float32Array(halfSize * numFrames) : new Float32Array(0));
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
- return { real, imag, frames: 0, fftSize };
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 batchInput = getBatchInput(fftSize, numFrames);
1347
- for (let frame = 0; frame < numFrames; frame++) {
1348
- const offset = frame * hopSize;
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
- if (typeof addon.batchFftInto === "function") {
1354
- addon.batchFftInto(batchInput.subarray(0, fftSize * numFrames), real.subarray(0, halfSize * numFrames), imag.subarray(0, halfSize * numFrames), fftSize, numFrames);
1355
- } else {
1356
- const { re: batchRe, im: batchIm } = addon.batchFft(batchInput.subarray(0, fftSize * numFrames), fftSize, numFrames);
1357
- real.set(batchRe.subarray(0, halfSize * numFrames));
1358
- imag.set(batchIm.subarray(0, halfSize * numFrames));
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
- const reView = real.subarray(0, halfSize * frames);
1387
- const imView = imag.subarray(0, halfSize * frames);
1388
- let timeDomainBatch;
1389
- if (typeof addon.batchIfftInto === "function") {
1390
- const batchTime = getBatchTime(fftSize, frames);
1391
- addon.batchIfftInto(reView, imView, batchTime.subarray(0, fftSize * frames), fftSize, frames);
1392
- timeDomainBatch = batchTime;
1393
- } else {
1394
- timeDomainBatch = addon.batchIfft(reView, imView, fftSize, frames);
1395
- }
1396
- for (let frame = 0; frame < frames; frame++) {
1397
- const offset = frame * hopSize;
1398
- for (let index = 0; index < fftSize; index++) {
1399
- const pos = offset + index;
1400
- if (pos < outputLength) {
1401
- output[pos] = (output[pos] ?? 0) + (timeDomainBatch[frame * fftSize + index] ?? 0) * (window[index] ?? 0);
1402
- windowSum[pos] = (windowSum[pos] ?? 0) + (window[index] ?? 0) * (window[index] ?? 0);
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 = /* @__PURE__ */ new Map();
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 = /* @__PURE__ */ new Map();
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 / 2 * Math.log2(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
- twiddleCache.set(size, cached);
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 P1T0 = 0.001708984375;
1559
- var P1T1 = 0.010986328125;
1560
- var P1T2 = -0.0196533203125;
1561
- var P1T3 = 0.033203125;
1562
- var P1T4 = -0.0594482421875;
1563
- var P1T5 = 0.1373291015625;
1564
- var P1T6 = 0.97216796875;
1565
- var P1T7 = -0.102294921875;
1566
- var P1T8 = 0.047607421875;
1567
- var P1T9 = -0.026611328125;
1568
- var P1T10 = 0.014892578125;
1569
- var P1T11 = -0.00830078125;
1570
- var P2T0 = -0.0291748046875;
1571
- var P2T1 = 0.029296875;
1572
- var P2T2 = -0.0517578125;
1573
- var P2T3 = 0.089111328125;
1574
- var P2T4 = -0.16650390625;
1575
- var P2T5 = 0.465087890625;
1576
- var P2T6 = 0.77978515625;
1577
- var P2T7 = -0.2003173828125;
1578
- var P2T8 = 0.1015625;
1579
- var P2T9 = -0.0582275390625;
1580
- var P2T10 = 0.0330810546875;
1581
- var P2T11 = -0.0189208984375;
1582
- var P3T0 = -0.0189208984375;
1583
- var P3T1 = 0.0330810546875;
1584
- var P3T2 = -0.0582275390625;
1585
- var P3T3 = 0.1015625;
1586
- var P3T4 = -0.2003173828125;
1587
- var P3T5 = 0.77978515625;
1588
- var P3T6 = 0.465087890625;
1589
- var P3T7 = -0.16650390625;
1590
- var P3T8 = 0.089111328125;
1591
- var P3T9 = -0.0517578125;
1592
- var P3T10 = 0.029296875;
1593
- var P3T11 = -0.0291748046875;
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-4 Annex 1) is supported`);
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 inIdx = 0; inIdx < inputLength; inIdx++) {
1618
- work[historyLength + inIdx] = input[inIdx] ?? 0;
1619
- }
1620
- for (let inIdx = 0; inIdx < inputLength; inIdx++) {
1621
- const currentIdx = historyLength + inIdx;
1622
- const outOffset = inIdx * factor;
1623
- output[outOffset] = input[inIdx] ?? 0;
1624
- const v0 = work[currentIdx] ?? 0;
1625
- const v1 = work[currentIdx - 1] ?? 0;
1626
- const v2 = work[currentIdx - 2] ?? 0;
1627
- const v3 = work[currentIdx - 3] ?? 0;
1628
- const v4 = work[currentIdx - 4] ?? 0;
1629
- const v5 = work[currentIdx - 5] ?? 0;
1630
- const v6 = work[currentIdx - 6] ?? 0;
1631
- const v7 = work[currentIdx - 7] ?? 0;
1632
- const v8 = work[currentIdx - 8] ?? 0;
1633
- const v9 = work[currentIdx - 9] ?? 0;
1634
- const v10 = work[currentIdx - 10] ?? 0;
1635
- const v11 = work[currentIdx - 11] ?? 0;
1636
- let acc1 = 0;
1637
- acc1 += P1T0 * v0;
1638
- acc1 += P1T1 * v1;
1639
- acc1 += P1T2 * v2;
1640
- acc1 += P1T3 * v3;
1641
- acc1 += P1T4 * v4;
1642
- acc1 += P1T5 * v5;
1643
- acc1 += P1T6 * v6;
1644
- acc1 += P1T7 * v7;
1645
- acc1 += P1T8 * v8;
1646
- acc1 += P1T9 * v9;
1647
- acc1 += P1T10 * v10;
1648
- acc1 += P1T11 * v11;
1649
- let acc2 = 0;
1650
- acc2 += P2T0 * v0;
1651
- acc2 += P2T1 * v1;
1652
- acc2 += P2T2 * v2;
1653
- acc2 += P2T3 * v3;
1654
- acc2 += P2T4 * v4;
1655
- acc2 += P2T5 * v5;
1656
- acc2 += P2T6 * v6;
1657
- acc2 += P2T7 * v7;
1658
- acc2 += P2T8 * v8;
1659
- acc2 += P2T9 * v9;
1660
- acc2 += P2T10 * v10;
1661
- acc2 += P2T11 * v11;
1662
- let acc3 = 0;
1663
- acc3 += P3T0 * v0;
1664
- acc3 += P3T1 * v1;
1665
- acc3 += P3T2 * v2;
1666
- acc3 += P3T3 * v3;
1667
- acc3 += P3T4 * v4;
1668
- acc3 += P3T5 * v5;
1669
- acc3 += P3T6 * v6;
1670
- acc3 += P3T7 * v7;
1671
- acc3 += P3T8 * v8;
1672
- acc3 += P3T9 * v9;
1673
- acc3 += P3T10 * v10;
1674
- acc3 += P3T11 * v11;
1675
- output[outOffset + 1] = acc1;
1676
- output[outOffset + 2] = acc2;
1677
- output[outOffset + 3] = acc3;
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.runningMax = 0;
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.runningMax) this.runningMax = magnitude;
1720
+ if (magnitude > this.interpolatedMax) this.interpolatedMax = magnitude;
1731
1721
  }
1732
1722
  }
1733
1723
  }
1734
1724
  finalize() {
1735
- return this.runningMax;
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 applyDfttSmoothing(nlmSmoothed, rawMask, numFrames, numBins, dfttOptions, output, fftBackend, fftAddonOptions, profileMs) {
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(blockTime * blockFreq);
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 blockSize = blockTime * blockFreq;
1773
- const complexBinsPerRow = blockFreq / 2 + 1;
1774
- const complexBlockSize = blockTime * complexBinsPerRow;
1775
- const rawBatch = new Float32Array(totalBlocks * blockSize);
1776
- const nlmBatch = new Float32Array(totalBlocks * blockSize);
1777
- for (let frameIdx = 0; frameIdx < blocksPerFrame; frameIdx++) {
1778
- const frameStart = frameIdx * hopTime;
1779
- for (let binIdx = 0; binIdx < blocksPerBin; binIdx++) {
1780
- const binStart = binIdx * hopFreq;
1781
- const blockIdx = frameIdx * blocksPerBin + binIdx;
1782
- const blockOffset = blockIdx * blockSize;
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 < numFrames ? frameStart + tf : numFrames - 1;
1850
+ const srcFrame = Math.min(frameStart + tf, numFrames - 1);
1785
1851
  for (let bf = 0; bf < blockFreq; bf++) {
1786
- const srcBin = binStart + bf < numBins ? binStart + bf : numBins - 1;
1787
- const winVal = win2d[tf * blockFreq + bf];
1788
- const srcPos = srcFrame * numBins + srcBin;
1789
- const dstPos = blockOffset + tf * blockFreq + bf;
1790
- rawBatch[dstPos] = rawMask[srcPos] * winVal;
1791
- nlmBatch[dstPos] = nlmSmoothed[srcPos] * winVal;
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
- profileAdd("fill");
1797
- const rawFft = addon.batchFft2D(rawBatch, blockTime, blockFreq, totalBlocks);
1798
- const nlmFft = addon.batchFft2D(nlmBatch, blockTime, blockFreq, totalBlocks);
1799
- profileAdd("forward");
1800
- const sigmaSq = threshold * threshold;
1801
- const totalComplex = totalBlocks * complexBlockSize;
1802
- const rawRe = rawFft.re;
1803
- const rawIm = rawFft.im;
1804
- const nlmRe = nlmFft.re;
1805
- const nlmIm = nlmFft.im;
1806
- for (let flatIdx = 0; flatIdx < totalComplex; flatIdx++) {
1807
- const nRe = nlmRe[flatIdx];
1808
- const nIm = nlmIm[flatIdx];
1809
- const nMagSq = nRe * nRe + nIm * nIm;
1810
- const gain = nMagSq / (nMagSq + sigmaSq);
1811
- rawRe[flatIdx] = rawRe[flatIdx] * gain;
1812
- rawIm[flatIdx] = rawIm[flatIdx] * gain;
1813
- }
1814
- profileAdd("gain");
1815
- const synth = addon.batchIfft2D(rawRe, rawIm, blockTime, blockFreq, totalBlocks);
1816
- profileAdd("inverse");
1817
- const accumulator = new Float32Array(numFrames * numBins);
1818
- const windowSumSq = new Float32Array(numFrames * numBins);
1819
- for (let frameIdx = 0; frameIdx < blocksPerFrame; frameIdx++) {
1820
- const frameStart = frameIdx * hopTime;
1821
- for (let binIdx = 0; binIdx < blocksPerBin; binIdx++) {
1822
- const binStart = binIdx * hopFreq;
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 destFrame = frameStart + tf;
1827
- if (destFrame >= numFrames) break;
1890
+ const destinationFrame = frameStart + tf;
1891
+ if (destinationFrame >= numFrames) break;
1828
1892
  for (let bf = 0; bf < blockFreq; bf++) {
1829
- const destBin = binStart + bf;
1830
- if (destBin >= numBins) break;
1831
- const winVal = win2d[tf * blockFreq + bf];
1832
- const destPos = destFrame * numBins + destBin;
1833
- const srcVal = synth[blockOffset + tf * blockFreq + bf];
1834
- accumulator[destPos] = accumulator[destPos] + srcVal * winVal;
1835
- windowSumSq[destPos] = windowSumSq[destPos] + winVal * winVal;
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
- profileAdd("ola");
1841
- for (let flatIdx = 0; flatIdx < numFrames * numBins; flatIdx++) {
1842
- const ws = windowSumSq[flatIdx];
1843
- if (ws > 1e-8) {
1844
- const normalisedVal = accumulator[flatIdx] / ws;
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[flatIdx] = rawMask[flatIdx];
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 colInvWorkspace = createFftWorkspace(blockTime);
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 = nlmMagSq / (nlmMagSq + sigmaSq);
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
- const icolResult = ifft(scratchRe, scratchIm, colInvWorkspace);
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] = icolResult[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] = 0;
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
- accumulator[destPos] = accumulator[destPos] + synthBlock[tf * blockFreq + bf] * winVal;
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 = accumulator[flatIdx] / ws;
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 smoothed = weightSum > 0 ? valueSum / weightSum : mask[centreFrame * numBins + centreBin];
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] = smoothed;
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, ResampleStream, SlidingWindowMaxStream, SlidingWindowMinStream, TruePeakAccumulator, TruePeakUpsampler, __commonJS, __export, __toESM, applyDfttSmoothing, applyNlmSmoothingRange, bandpass, createFftWorkspace, dbToLinear, deinterleaveBuffer, detectFftBackend, fft, getFftAddon, getLraConsideredStats, hanningWindow, initFftBackend, interleave, istft, linearToDb, stft };
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 };