@buffered-audio/nodes 0.15.2 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import path, { extname, join } from 'path';
2
2
  import { randomUUID } from 'crypto';
3
3
  import { createWriteStream, createReadStream } from 'fs';
4
- import { open, stat, unlink, readFile, mkdtemp, writeFile, rm } from 'fs/promises';
4
+ import { open, stat, readFile, unlink, mkdtemp, writeFile, rm } from 'fs/promises';
5
5
  import { tmpdir } from 'os';
6
+ import { Readable } from 'stream';
6
7
  import { EventEmitter } from 'events';
7
8
  import { spawn } from 'child_process';
8
9
  import { createRequire } from 'module';
@@ -15575,6 +15576,66 @@ var __export2 = (target, all) => {
15575
15576
  __defProp2(target, name, { get: all[name], enumerable: true });
15576
15577
  };
15577
15578
  var HIGH_WATER_MARK = 10 * 1024 * 1024;
15579
+ var REVERSE_STRIPE_BYTES = 10 * 1024 * 1024;
15580
+ function deinterleave(buf, channels) {
15581
+ const bytesPerFrame = channels * 4;
15582
+ const frames = Math.floor(buf.length / bytesPerFrame);
15583
+ const interleaved = new Float32Array(buf.buffer, buf.byteOffset, frames * channels);
15584
+ const out = [];
15585
+ for (let ch = 0; ch < channels; ch++) out.push(new Float32Array(frames));
15586
+ for (let frame = 0; frame < frames; frame++) {
15587
+ const base = frame * channels;
15588
+ for (let ch = 0; ch < channels; ch++) {
15589
+ out[ch][frame] = interleaved[base + ch];
15590
+ }
15591
+ }
15592
+ return out;
15593
+ }
15594
+ function buildAudioChunk(samples, offset, sampleRate, bitDepth) {
15595
+ return { samples, offset, sampleRate: sampleRate ?? 0, bitDepth: bitDepth ?? 0 };
15596
+ }
15597
+ async function pullBytes(rs, isEnded, bytesNeeded) {
15598
+ const chunks = [];
15599
+ let collected = 0;
15600
+ while (collected < bytesNeeded) {
15601
+ const chunk = rs.read();
15602
+ if (chunk !== null) {
15603
+ const remaining = bytesNeeded - collected;
15604
+ if (chunk.length <= remaining) {
15605
+ chunks.push(chunk);
15606
+ collected += chunk.length;
15607
+ } else {
15608
+ chunks.push(chunk.subarray(0, remaining));
15609
+ collected += remaining;
15610
+ rs.unshift(chunk.subarray(remaining));
15611
+ }
15612
+ continue;
15613
+ }
15614
+ if (isEnded()) break;
15615
+ await new Promise((resolve) => {
15616
+ const wake = () => {
15617
+ rs.off("readable", wake);
15618
+ rs.off("end", wake);
15619
+ rs.off("error", wake);
15620
+ rs.off("close", wake);
15621
+ resolve();
15622
+ };
15623
+ rs.once("readable", wake);
15624
+ rs.once("end", wake);
15625
+ rs.once("error", wake);
15626
+ rs.once("close", wake);
15627
+ });
15628
+ }
15629
+ if (chunks.length === 0) return Buffer.alloc(0);
15630
+ if (chunks.length === 1) return chunks[0];
15631
+ return Buffer.concat(chunks);
15632
+ }
15633
+ async function awaitStreamClose(stream) {
15634
+ if (stream.closed) return;
15635
+ await new Promise((resolve) => {
15636
+ stream.once("close", () => resolve());
15637
+ });
15638
+ }
15578
15639
  var ChunkBuffer = class {
15579
15640
  constructor() {
15580
15641
  this._frames = 0;
@@ -15583,6 +15644,7 @@ var ChunkBuffer = class {
15583
15644
  this.writePositionByte = 0;
15584
15645
  this.readStreamEnded = false;
15585
15646
  this.framesReadInSession = 0;
15647
+ this.reverseReaders = /* @__PURE__ */ new Set();
15586
15648
  }
15587
15649
  get frames() {
15588
15650
  return this._frames;
@@ -15628,28 +15690,47 @@ var ChunkBuffer = class {
15628
15690
  async flushWrites() {
15629
15691
  await this.endWriteStream();
15630
15692
  }
15693
+ // Opens a read-only reverse view over this buffer's temp file. Flushes pending writes first, then
15694
+ // snapshots frames/channels/sampleRate/bitDepth so the reader is stable against later reads on the
15695
+ // source. The reader is registered here and closed by clear()/close() (Windows blocks unlink while a
15696
+ // handle is open — same EBUSY guard the forward read stream uses in endReadStream). See the
15697
+ // ReverseChunkReader class comment for the full borrow contract.
15698
+ async openReverseReader() {
15699
+ await this.flushWrites();
15700
+ const reader = new ReverseChunkReader(
15701
+ this.tempPath,
15702
+ { frames: this._frames, channels: this._channels, sampleRate: this._sampleRate, bitDepth: this._bitDepth },
15703
+ REVERSE_STRIPE_BYTES,
15704
+ this
15705
+ );
15706
+ this.reverseReaders.add(reader);
15707
+ return reader;
15708
+ }
15709
+ // Called by a factory-created reader from its close() so it deregisters itself. Idempotent.
15710
+ deregisterReverseReader(reader) {
15711
+ this.reverseReaders.delete(reader);
15712
+ }
15713
+ async closeReverseReaders() {
15714
+ const readers = [...this.reverseReaders];
15715
+ this.reverseReaders.clear();
15716
+ for (const reader of readers) {
15717
+ await reader.close().catch(() => void 0);
15718
+ }
15719
+ }
15631
15720
  async read(frames) {
15632
15721
  const channels = this._channels;
15633
15722
  const startFrame = this.framesReadInSession;
15634
15723
  if (channels === 0 || frames <= 0 || this._frames === 0) {
15635
- return this.buildAudioChunk([], startFrame);
15724
+ return buildAudioChunk([], startFrame, this._sampleRate, this._bitDepth);
15636
15725
  }
15637
15726
  const bytesPerFrame = channels * 4;
15638
- const bytesNeeded = frames * bytesPerFrame;
15639
- const buf = await this.pullBytes(bytesNeeded);
15727
+ const rs = this.ensureReadStream();
15728
+ const buf = await pullBytes(rs, () => rs.destroyed || this.readStreamEnded, frames * bytesPerFrame);
15640
15729
  const actualFrames = Math.floor(buf.length / bytesPerFrame);
15641
- if (actualFrames <= 0) return this.buildAudioChunk([], startFrame);
15730
+ if (actualFrames <= 0) return buildAudioChunk([], startFrame, this._sampleRate, this._bitDepth);
15642
15731
  this.framesReadInSession += actualFrames;
15643
- const interleaved = new Float32Array(buf.buffer, buf.byteOffset, actualFrames * channels);
15644
- const out = [];
15645
- for (let ch = 0; ch < channels; ch++) out.push(new Float32Array(actualFrames));
15646
- for (let frame = 0; frame < actualFrames; frame++) {
15647
- const base = frame * channels;
15648
- for (let ch = 0; ch < channels; ch++) {
15649
- out[ch][frame] = interleaved[base + ch];
15650
- }
15651
- }
15652
- return this.buildAudioChunk(out, startFrame);
15732
+ const out = deinterleave(buf, channels);
15733
+ return buildAudioChunk(out, startFrame, this._sampleRate, this._bitDepth);
15653
15734
  }
15654
15735
  async reset() {
15655
15736
  await this.endWriteStream();
@@ -15659,6 +15740,7 @@ var ChunkBuffer = class {
15659
15740
  async clear() {
15660
15741
  await this.endWriteStream();
15661
15742
  await this.endReadStream();
15743
+ await this.closeReverseReaders();
15662
15744
  if (this.tempPath) {
15663
15745
  await unlink(this.tempPath).catch(() => void 0);
15664
15746
  this.tempPath = void 0;
@@ -15670,6 +15752,7 @@ var ChunkBuffer = class {
15670
15752
  async close() {
15671
15753
  await this.endWriteStream();
15672
15754
  await this.endReadStream();
15755
+ await this.closeReverseReaders();
15673
15756
  if (this.tempPath) {
15674
15757
  await unlink(this.tempPath).catch(() => void 0);
15675
15758
  this.tempPath = void 0;
@@ -15679,9 +15762,6 @@ var ChunkBuffer = class {
15679
15762
  this._frames = 0;
15680
15763
  this._channels = 0;
15681
15764
  }
15682
- tempFilePath() {
15683
- return this.tempPath;
15684
- }
15685
15765
  validateAndSetMetadata(sampleRate, bitDepth) {
15686
15766
  if (sampleRate !== void 0) {
15687
15767
  if (this._sampleRate === void 0) {
@@ -15705,9 +15785,6 @@ var ChunkBuffer = class {
15705
15785
  throw new Error(`ChunkBuffer: channel count mismatch \u2014 buffer has ${String(this._channels)}, write supplied ${String(target)}`);
15706
15786
  }
15707
15787
  }
15708
- buildAudioChunk(samples, offset) {
15709
- return { samples, offset, sampleRate: this._sampleRate ?? 0, bitDepth: this._bitDepth ?? 0 };
15710
- }
15711
15788
  ensureTempPath() {
15712
15789
  this.tempPath ?? (this.tempPath = join(tmpdir(), `chunk-buffer-${randomUUID()}.bin`));
15713
15790
  return this.tempPath;
@@ -15760,47 +15837,140 @@ var ChunkBuffer = class {
15760
15837
  this.readStreamEnded = false;
15761
15838
  this.framesReadInSession = 0;
15762
15839
  rs.destroy();
15763
- if (!rs.closed) {
15764
- await new Promise((resolve) => {
15765
- rs.once("close", () => resolve());
15766
- });
15840
+ await awaitStreamClose(rs);
15841
+ }
15842
+ };
15843
+ var ReverseChunkReader = class {
15844
+ constructor(path2, meta32, stripeBytes = REVERSE_STRIPE_BYTES, parent) {
15845
+ this.framesReturned = 0;
15846
+ this.closed = false;
15847
+ this.path = path2;
15848
+ this.frames = path2 === void 0 ? 0 : meta32.frames;
15849
+ this.channels = meta32.channels;
15850
+ this.sampleRate = meta32.sampleRate;
15851
+ this.bitDepth = meta32.bitDepth;
15852
+ this.bytesPerFrame = meta32.channels * 4;
15853
+ this.windowBytes = meta32.channels === 0 ? stripeBytes : Math.max(this.bytesPerFrame, Math.floor(stripeBytes / this.bytesPerFrame) * this.bytesPerFrame);
15854
+ this.parent = parent;
15855
+ }
15856
+ async read(frames) {
15857
+ if (this.closed) {
15858
+ throw new Error("ReverseChunkReader: read() after close()");
15859
+ }
15860
+ const offset = this.framesReturned;
15861
+ const remaining = this.frames - this.framesReturned;
15862
+ if (this.path === void 0 || this.channels === 0 || frames <= 0 || remaining <= 0) {
15863
+ return buildAudioChunk([], offset, this.sampleRate, this.bitDepth);
15767
15864
  }
15865
+ const count = Math.min(frames, remaining);
15866
+ const rs = this.ensureStream();
15867
+ const buf = await pullBytes(rs, () => rs.destroyed || rs.readableEnded, count * this.bytesPerFrame);
15868
+ const actualFrames = Math.floor(buf.length / this.bytesPerFrame);
15869
+ if (actualFrames < count) {
15870
+ throw this.streamError ?? new Error("ReverseChunkReader: unexpected end of reverse stream");
15871
+ }
15872
+ this.framesReturned += actualFrames;
15873
+ const out = deinterleave(buf, this.channels);
15874
+ return buildAudioChunk(out, offset, this.sampleRate, this.bitDepth);
15768
15875
  }
15769
- async pullBytes(bytesNeeded) {
15770
- const rs = this.ensureReadStream();
15771
- const chunks = [];
15772
- let collected = 0;
15773
- while (collected < bytesNeeded) {
15774
- const chunk = rs.read();
15775
- if (chunk !== null) {
15776
- const remaining = bytesNeeded - collected;
15777
- if (chunk.length <= remaining) {
15778
- chunks.push(chunk);
15779
- collected += chunk.length;
15780
- } else {
15781
- chunks.push(chunk.subarray(0, remaining));
15782
- collected += remaining;
15783
- rs.unshift(chunk.subarray(remaining));
15784
- }
15785
- continue;
15876
+ async close() {
15877
+ if (this.closed) return;
15878
+ this.closed = true;
15879
+ const stream = this.stream;
15880
+ this.stream = void 0;
15881
+ if (stream) {
15882
+ stream.destroy();
15883
+ await awaitStreamClose(stream);
15884
+ }
15885
+ this.parent?.deregisterReverseReader(this);
15886
+ }
15887
+ ensureStream() {
15888
+ if (this.stream) return this.stream;
15889
+ if (this.path === void 0) {
15890
+ throw new Error("ReverseChunkReader: no source file");
15891
+ }
15892
+ const totalBytes = this.frames * this.bytesPerFrame;
15893
+ const stream = new ReverseReadable(this.path, totalBytes, this.bytesPerFrame, this.windowBytes);
15894
+ stream.once("error", (error482) => {
15895
+ this.streamError = error482;
15896
+ });
15897
+ this.stream = stream;
15898
+ return stream;
15899
+ }
15900
+ };
15901
+ var ReverseReadable = class extends Readable {
15902
+ constructor(path2, totalBytes, bytesPerFrame, windowBytes) {
15903
+ super({ highWaterMark: windowBytes });
15904
+ this.path = path2;
15905
+ this.bytesPerFrame = bytesPerFrame;
15906
+ this.windowBytes = windowBytes;
15907
+ this.pos = totalBytes;
15908
+ }
15909
+ // Emits the next backward window [max(0, pos - windowBytes), pos), with the frame order reversed in
15910
+ // place (channel order INSIDE each frame preserved — interleaved layout intact), then walks pos toward
15911
+ // 0 and pushes null at start-of-file. Node does not consume a rejected _read promise — an uncaught
15912
+ // throw becomes an unhandled rejection and the stream never errors — so any failure is routed to
15913
+ // destroy(err), which fires the 'error'/'close' events pullBytes waits on.
15914
+ _read() {
15915
+ void this.readWindow();
15916
+ }
15917
+ async readWindow() {
15918
+ try {
15919
+ if (this.pos <= 0) {
15920
+ this.push(null);
15921
+ return;
15786
15922
  }
15787
- if (this.readStreamEnded) break;
15788
- await new Promise((resolve) => {
15789
- const onReadable = () => {
15790
- rs.off("end", onEnd);
15791
- resolve();
15792
- };
15793
- const onEnd = () => {
15794
- rs.off("readable", onReadable);
15795
- resolve();
15796
- };
15797
- rs.once("readable", onReadable);
15798
- rs.once("end", onEnd);
15799
- });
15923
+ const startByte = Math.max(0, this.pos - this.windowBytes);
15924
+ const length = this.pos - startByte;
15925
+ const buf = Buffer.alloc(length);
15926
+ await this.readFully(buf, startByte);
15927
+ this.reverseFramesInPlace(buf);
15928
+ this.pos = startByte;
15929
+ this.push(buf);
15930
+ } catch (error482) {
15931
+ this.destroy(error482);
15932
+ }
15933
+ }
15934
+ // Reverse the frame order within the window in place: frame i swaps with frame (frameCount - 1 - i),
15935
+ // each frame being one bytesPerFrame-wide slice. Channel order inside a frame is untouched, so the
15936
+ // interleaved layout survives — only the time order flips.
15937
+ reverseFramesInPlace(buf) {
15938
+ const frameCount = Math.floor(buf.length / this.bytesPerFrame);
15939
+ const scratch = Buffer.alloc(this.bytesPerFrame);
15940
+ for (let low = 0, high = frameCount - 1; low < high; low++, high--) {
15941
+ const lowByte = low * this.bytesPerFrame;
15942
+ const highByte = high * this.bytesPerFrame;
15943
+ buf.copy(scratch, 0, lowByte, lowByte + this.bytesPerFrame);
15944
+ buf.copy(buf, lowByte, highByte, highByte + this.bytesPerFrame);
15945
+ scratch.copy(buf, highByte, 0, this.bytesPerFrame);
15946
+ }
15947
+ }
15948
+ // The one centralized positioned read-fully helper. Loops on handle.read and throws on a zero-byte
15949
+ // read (unexpected EOF) — the shared guard against the former unchecked-bytesRead short-read bug.
15950
+ async readFully(target, position) {
15951
+ const handle = await this.ensureHandle();
15952
+ let filled = 0;
15953
+ while (filled < target.length) {
15954
+ const { bytesRead } = await handle.read(target, filled, target.length - filled, position + filled);
15955
+ if (bytesRead === 0) {
15956
+ throw new Error(`ReverseReadable: unexpected EOF at byte ${position + filled}`);
15957
+ }
15958
+ filled += bytesRead;
15959
+ }
15960
+ }
15961
+ async ensureHandle() {
15962
+ if (this.handle) return this.handle;
15963
+ this.handle = await open(this.path, "r");
15964
+ return this.handle;
15965
+ }
15966
+ _destroy(error482, callback) {
15967
+ const handle = this.handle;
15968
+ this.handle = void 0;
15969
+ if (!handle) {
15970
+ callback(error482);
15971
+ return;
15800
15972
  }
15801
- if (chunks.length === 0) return Buffer.alloc(0);
15802
- if (chunks.length === 1) return chunks[0];
15803
- return Buffer.concat(chunks);
15973
+ handle.close().then(() => callback(error482)).catch((closeError) => callback(error482 ?? closeError));
15804
15974
  }
15805
15975
  };
15806
15976
  var external_exports2 = {};
@@ -29413,6 +29583,54 @@ function date42(params) {
29413
29583
  return /* @__PURE__ */ _coercedDate2(ZodDate2, params);
29414
29584
  }
29415
29585
  config2(en_default2());
29586
+ var UNKNOWN_TOTAL_QUANTUM_FRAMES = 48e4;
29587
+ var DEFAULT_PROGRESS_QUANTUM = 0.1;
29588
+ var BufferedStream = class {
29589
+ constructor(properties) {
29590
+ this.events = new EventEmitter();
29591
+ this.quantumFraction = DEFAULT_PROGRESS_QUANTUM;
29592
+ this.lastBoundaryByPhase = /* @__PURE__ */ new Map();
29593
+ this.properties = properties;
29594
+ }
29595
+ emitProgress(phase2, framesDone, framesTotal, options) {
29596
+ const total = framesTotal !== void 0 && framesTotal > 0 ? framesTotal : void 0;
29597
+ if (options?.force) {
29598
+ this.lastBoundaryByPhase.set(phase2, framesDone);
29599
+ this.events.emit("progress", { phase: phase2, framesDone, framesTotal: total });
29600
+ return;
29601
+ }
29602
+ const quantum = total ? Math.max(1, Math.floor(total * this.quantumFraction)) : UNKNOWN_TOTAL_QUANTUM_FRAMES;
29603
+ const boundary = Math.floor(framesDone / quantum) * quantum;
29604
+ const last = this.lastBoundaryByPhase.get(phase2);
29605
+ if (last !== void 0 && boundary <= last) return;
29606
+ this.lastBoundaryByPhase.set(phase2, boundary);
29607
+ this.events.emit("progress", { phase: phase2, framesDone, framesTotal: total });
29608
+ }
29609
+ log(message, data, level = "info") {
29610
+ this.events.emit("log", { level, message, data });
29611
+ }
29612
+ progress(framesDone, framesTotal) {
29613
+ this.emitProgress("process", framesDone, framesTotal);
29614
+ }
29615
+ async teardown() {
29616
+ await this._teardown();
29617
+ }
29618
+ _teardown() {
29619
+ return;
29620
+ }
29621
+ };
29622
+ function wireStreamEvents(stream, identity, onEvent) {
29623
+ stream.events.on("started", () => onEvent(identity, { kind: "started" }));
29624
+ stream.events.on("finished", (payload) => onEvent(identity, { kind: "finished", ...payload }));
29625
+ stream.events.on("progress", (payload) => onEvent(identity, { kind: "progress", ...payload }));
29626
+ stream.events.on("log", (payload) => onEvent(identity, { kind: "log", ...payload }));
29627
+ }
29628
+ function wireStream(node, stream, context) {
29629
+ stream.quantumFraction = context.progressQuantum ?? DEFAULT_PROGRESS_QUANTUM;
29630
+ if (!context.onEvent) return;
29631
+ const identity = { nodeName: node.constructor.nodeName, id: node.id, type: node.type };
29632
+ wireStreamEvents(stream, identity, context.onEvent);
29633
+ }
29416
29634
  var BufferedAudioNode = class {
29417
29635
  constructor(properties) {
29418
29636
  this.streams = [];
@@ -29462,20 +29680,8 @@ var BufferedAudioNode = class {
29462
29680
  }
29463
29681
  };
29464
29682
  BufferedAudioNode.packageVersion = "0.0.0";
29465
- BufferedAudioNode.moduleDescription = "";
29683
+ BufferedAudioNode.nodeDescription = "";
29466
29684
  BufferedAudioNode.schema = external_exports2.object({});
29467
- var BufferedStream = class {
29468
- constructor(properties) {
29469
- this.events = new EventEmitter();
29470
- this.properties = properties;
29471
- }
29472
- async teardown() {
29473
- await this._teardown();
29474
- }
29475
- _teardown() {
29476
- return;
29477
- }
29478
- };
29479
29685
  var BufferedTargetStream = class extends BufferedStream {
29480
29686
  constructor() {
29481
29687
  super(...arguments);
@@ -29500,11 +29706,12 @@ var BufferedTargetStream = class extends BufferedStream {
29500
29706
  }
29501
29707
  await this._write(chunk);
29502
29708
  this.framesWritten += chunk.samples[0]?.length ?? 0;
29503
- this.events.emit("progress", { framesProcessed: this.framesWritten, sourceTotalFrames: this.sourceTotalFrames });
29709
+ this.emitProgress("write", this.framesWritten, this.sourceTotalFrames);
29504
29710
  },
29505
29711
  close: async () => {
29506
29712
  await this._close();
29507
- this.events.emit("finished");
29713
+ this.emitProgress("write", this.framesWritten, this.sourceTotalFrames, { force: true });
29714
+ this.events.emit("finished", { framesDone: this.framesWritten });
29508
29715
  }
29509
29716
  });
29510
29717
  }
@@ -29516,6 +29723,7 @@ var TargetNode = class extends BufferedAudioNode {
29516
29723
  setup(readable, context) {
29517
29724
  const stream = this.createStream();
29518
29725
  this.streams.push(stream);
29726
+ wireStream(this, stream, context);
29519
29727
  return Promise.resolve([stream.setup(readable, context)]);
29520
29728
  }
29521
29729
  };
@@ -29539,6 +29747,8 @@ var BufferedTransformStream = class extends BufferedStream {
29539
29747
  super(properties);
29540
29748
  this.processingMs = 0;
29541
29749
  this.framesProcessed = 0;
29750
+ this.framesBuffered = 0;
29751
+ this.framesEmitted = 0;
29542
29752
  this.bufferOffset = 0;
29543
29753
  this.hasStarted = false;
29544
29754
  this.bufferSize = properties.bufferSize ?? 0;
@@ -29589,7 +29799,8 @@ var BufferedTransformStream = class extends BufferedStream {
29589
29799
  }
29590
29800
  this.processingMs += performance.now() - start;
29591
29801
  this.framesProcessed += samplesIn;
29592
- this.events.emit("progress", { framesProcessed: this.framesProcessed, sourceTotalFrames: this.sourceTotalFrames });
29802
+ this.framesBuffered += samplesIn;
29803
+ this.emitProgress("buffer", this.framesBuffered, this.sourceTotalFrames);
29593
29804
  return;
29594
29805
  }
29595
29806
  let offset = 0;
@@ -29610,16 +29821,22 @@ var BufferedTransformStream = class extends BufferedStream {
29610
29821
  }
29611
29822
  this.processingMs += performance.now() - start;
29612
29823
  this.framesProcessed += samplesIn;
29613
- this.events.emit("progress", { framesProcessed: this.framesProcessed, sourceTotalFrames: this.sourceTotalFrames });
29824
+ this.framesBuffered += samplesIn;
29825
+ this.emitProgress("buffer", this.framesBuffered, this.sourceTotalFrames);
29614
29826
  }
29615
29827
  async handleFlush(controller) {
29828
+ this.emitProgress("buffer", this.framesBuffered, this.sourceTotalFrames, { force: true });
29616
29829
  if (!this.chunkBuffer || this.chunkBuffer.frames === 0) {
29617
- this.events.emit("finished");
29830
+ await this.emitFlushChunks(controller);
29831
+ this.emitProgress("emit", this.framesEmitted, this.sourceTotalFrames, { force: true });
29832
+ this.events.emit("finished", { framesDone: this.framesBuffered, processingMs: this.processingMs });
29618
29833
  return;
29619
29834
  }
29620
29835
  if (this.bufferSize === 0) {
29621
29836
  await this.chunkBuffer.close();
29622
- this.events.emit("finished");
29837
+ await this.emitFlushChunks(controller);
29838
+ this.emitProgress("emit", this.framesEmitted, this.sourceTotalFrames, { force: true });
29839
+ this.events.emit("finished", { framesDone: this.framesBuffered, processingMs: this.processingMs });
29623
29840
  return;
29624
29841
  }
29625
29842
  try {
@@ -29628,14 +29845,27 @@ var BufferedTransformStream = class extends BufferedStream {
29628
29845
  await this.chunkBuffer.close();
29629
29846
  this.chunkBuffer = void 0;
29630
29847
  }
29631
- this.events.emit("finished");
29848
+ await this.emitFlushChunks(controller);
29849
+ this.emitProgress("emit", this.framesEmitted, this.sourceTotalFrames, { force: true });
29850
+ this.events.emit("finished", { framesDone: this.framesBuffered, processingMs: this.processingMs });
29851
+ }
29852
+ async emitFlushChunks(controller) {
29853
+ const chunks = await this._flush();
29854
+ if (!chunks) return;
29855
+ for (const chunk of chunks) {
29856
+ controller.enqueue(chunk);
29857
+ this.framesEmitted += chunk.samples[0]?.length ?? 0;
29858
+ }
29632
29859
  }
29633
29860
  async processAndEmit(controller) {
29634
29861
  if (!this.chunkBuffer) return;
29635
29862
  const samplesBeforeProcess = this.chunkBuffer.frames;
29636
29863
  const start = performance.now();
29864
+ const wholeFile = this.bufferSize === WHOLE_FILE;
29637
29865
  await this.chunkBuffer.flushWrites();
29866
+ if (wholeFile) this.emitProgress("process", 0, void 0, { force: true });
29638
29867
  await this._process(this.chunkBuffer);
29868
+ if (wholeFile) this.emitProgress("process", samplesBeforeProcess, samplesBeforeProcess, { force: true });
29639
29869
  await this.emitBuffer(controller);
29640
29870
  this.processingMs += performance.now() - start;
29641
29871
  this.framesProcessed += samplesBeforeProcess;
@@ -29664,6 +29894,8 @@ var BufferedTransformStream = class extends BufferedStream {
29664
29894
  };
29665
29895
  const result = await this._unbuffer(adjusted);
29666
29896
  if (result) controller.enqueue(result);
29897
+ this.framesEmitted += chunkFrames;
29898
+ this.emitProgress("emit", this.framesEmitted, this.sourceTotalFrames);
29667
29899
  if (overlapScratch) {
29668
29900
  if (chunkFrames >= overlap) {
29669
29901
  for (let ch = 0; ch < channels; ch++) {
@@ -29714,6 +29946,9 @@ var BufferedTransformStream = class extends BufferedStream {
29714
29946
  _unbuffer(chunk) {
29715
29947
  return chunk;
29716
29948
  }
29949
+ _flush() {
29950
+ return void 0;
29951
+ }
29717
29952
  };
29718
29953
  var TransformNode = class _TransformNode extends BufferedAudioNode {
29719
29954
  static is(value) {
@@ -29725,6 +29960,7 @@ var TransformNode = class _TransformNode extends BufferedAudioNode {
29725
29960
  async setup(readable, context) {
29726
29961
  const stream = this.createStream();
29727
29962
  this.streams.push(stream);
29963
+ wireStream(this, stream, context);
29728
29964
  const output = await stream.setup(readable, context);
29729
29965
  return this.setupChildren(output, context);
29730
29966
  }
@@ -29746,6 +29982,7 @@ var BufferedSourceStream = class extends BufferedStream {
29746
29982
  constructor() {
29747
29983
  super(...arguments);
29748
29984
  this.framesRead = 0;
29985
+ this.hasStarted = false;
29749
29986
  }
29750
29987
  setup(context) {
29751
29988
  return this._setup(context);
@@ -29754,6 +29991,7 @@ var BufferedSourceStream = class extends BufferedStream {
29754
29991
  async _setup(context) {
29755
29992
  let done = false;
29756
29993
  this.framesRead = 0;
29994
+ this.hasStarted = false;
29757
29995
  const { signal, durationFrames: sourceTotalFrames, highWaterMark } = context;
29758
29996
  return new ReadableStream(
29759
29997
  {
@@ -29765,16 +30003,22 @@ var BufferedSourceStream = class extends BufferedStream {
29765
30003
  return;
29766
30004
  }
29767
30005
  try {
30006
+ if (!this.hasStarted) {
30007
+ this.hasStarted = true;
30008
+ this.events.emit("started");
30009
+ }
29768
30010
  const chunk = await this._read();
29769
30011
  if (!chunk) {
29770
30012
  done = true;
30013
+ this.emitProgress("read", this.framesRead, sourceTotalFrames, { force: true });
29771
30014
  await this._flush();
30015
+ this.events.emit("finished", { framesDone: this.framesRead });
29772
30016
  controller.close();
29773
30017
  return;
29774
30018
  }
29775
30019
  this.framesRead += chunk.samples[0]?.length ?? 0;
29776
30020
  controller.enqueue(chunk);
29777
- this.events.emit("progress", { framesProcessed: this.framesRead, sourceTotalFrames });
30021
+ this.emitProgress("read", this.framesRead, sourceTotalFrames);
29778
30022
  } catch (error482) {
29779
30023
  done = true;
29780
30024
  controller.error(error482);
@@ -29805,6 +30049,7 @@ var SourceNode = class extends BufferedAudioNode {
29805
30049
  async setup(context) {
29806
30050
  const stream = this.createStream();
29807
30051
  this.streams.push(stream);
30052
+ wireStream(this, stream, context);
29808
30053
  const readable = await stream.setup(context);
29809
30054
  const promises = await this.setupChildren(readable, context);
29810
30055
  await Promise.all(promises);
@@ -29836,7 +30081,9 @@ var SourceNode = class extends BufferedAudioNode {
29836
30081
  durationFrames: meta32.durationFrames,
29837
30082
  highWaterMark: options?.highWaterMark ?? computedHighWaterMark,
29838
30083
  signal: options?.signal,
29839
- visited: /* @__PURE__ */ new Set()
30084
+ visited: /* @__PURE__ */ new Set(),
30085
+ onEvent: options?.onEvent,
30086
+ progressQuantum: options?.progressQuantum
29840
30087
  };
29841
30088
  const start = performance.now();
29842
30089
  try {
@@ -29905,39 +30152,24 @@ external_exports2.object({
29905
30152
  });
29906
30153
  var STRIPE_BYTES = 10 * 1024 * 1024;
29907
30154
  async function reverseBuffer(source, dest) {
29908
- await source.flushWrites();
29909
30155
  const out = dest ?? new ChunkBuffer();
29910
30156
  const channels = source.channels;
29911
30157
  const totalFrames = source.frames;
29912
30158
  if (channels === 0 || totalFrames === 0) return out;
29913
- const sourcePath = source.tempFilePath();
29914
- if (!sourcePath) return out;
29915
30159
  const sampleRate = source.sampleRate;
29916
30160
  const bitDepth = source.bitDepth;
29917
30161
  const bytesPerFrame = channels * 4;
29918
30162
  const stripeFramesCap = Math.max(1, Math.floor(STRIPE_BYTES / bytesPerFrame));
29919
- const handle = await open(sourcePath, "r");
30163
+ const reader = await source.openReverseReader();
29920
30164
  try {
29921
- let endFrame = totalFrames;
29922
- while (endFrame > 0) {
29923
- const stripeFrames = Math.min(stripeFramesCap, endFrame);
29924
- const startFrame = endFrame - stripeFrames;
29925
- const buf = Buffer.alloc(stripeFrames * bytesPerFrame);
29926
- await handle.read(buf, 0, buf.length, startFrame * bytesPerFrame);
29927
- const interleaved = new Float32Array(buf.buffer, buf.byteOffset, stripeFrames * channels);
29928
- const reversed = [];
29929
- for (let ch = 0; ch < channels; ch++) reversed.push(new Float32Array(stripeFrames));
29930
- for (let frame = 0; frame < stripeFrames; frame++) {
29931
- const sourceBase = (stripeFrames - 1 - frame) * channels;
29932
- for (let ch = 0; ch < channels; ch++) {
29933
- reversed[ch][frame] = interleaved[sourceBase + ch];
29934
- }
29935
- }
29936
- await out.write(reversed, sampleRate, bitDepth);
29937
- endFrame = startFrame;
30165
+ for (; ; ) {
30166
+ const chunk = await reader.read(stripeFramesCap);
30167
+ const frames = chunk.samples[0]?.length ?? 0;
30168
+ if (frames === 0) break;
30169
+ await out.write(chunk.samples, sampleRate, bitDepth);
29938
30170
  }
29939
30171
  } finally {
29940
- await handle.close();
30172
+ await reader.close();
29941
30173
  }
29942
30174
  await out.flushWrites();
29943
30175
  return out;
@@ -29946,7 +30178,7 @@ async function reverseBuffer(source, dest) {
29946
30178
  // package.json
29947
30179
  var package_default = {
29948
30180
  name: "@buffered-audio/nodes",
29949
- version: "0.15.2"};
30181
+ version: "0.17.0"};
29950
30182
 
29951
30183
  // src/package-metadata.ts
29952
30184
  var PACKAGE_NAME = package_default.name;
@@ -29963,11 +30195,7 @@ var AmplitudeHistogramAccumulator = class {
29963
30195
  this.bucketCount = bucketCount;
29964
30196
  this.buckets = new Uint32Array(bucketCount);
29965
30197
  }
29966
- /**
29967
- * Consume `frames` samples from each channel. Throws if any channel
29968
- * has fewer than `frames` samples or if {@link finalize} has already
29969
- * been called.
29970
- */
30198
+ // Consumes `frames` samples per channel; throws if any channel has fewer than `frames` samples or if `finalize` was already called.
29971
30199
  push(channels, frames) {
29972
30200
  if (this.finalized) {
29973
30201
  throw new Error("AmplitudeHistogramAccumulator: push() called after finalize()");
@@ -30008,13 +30236,6 @@ var AmplitudeHistogramAccumulator = class {
30008
30236
  }
30009
30237
  }
30010
30238
  }
30011
- /**
30012
- * Returns the histogram with linearly-interpolated median. Idempotent
30013
- * — subsequent calls return the same cached result object.
30014
- *
30015
- * Empty / all-zero input yields `bucketMax = 0`, `median = 0`, and an
30016
- * all-zero `buckets` array, matching the one-shot utility's edge case.
30017
- */
30018
30239
  finalize() {
30019
30240
  if (this.cachedResult !== void 0) return this.cachedResult;
30020
30241
  this.finalized = true;
@@ -30039,17 +30260,6 @@ var AmplitudeHistogramAccumulator = class {
30039
30260
  this.cachedResult = { buckets: this.buckets, bucketMax: this.bucketMax, median };
30040
30261
  return this.cachedResult;
30041
30262
  }
30042
- /**
30043
- * Rebin existing counts into a wider range. Each old bucket's count is
30044
- * deposited entirely into the new bucket whose interval contains the
30045
- * old bucket's center. Faster than a proportional split and accuracy
30046
- * difference is sub-bucket — with 1024 buckets the resolution is
30047
- * already finer than any practical percentile precision requirement.
30048
- *
30049
- * Sample-count invariant: `sum(newBuckets) === sum(oldBuckets)`. This
30050
- * follows trivially from each old bucket being deposited into exactly
30051
- * one new bucket.
30052
- */
30053
30263
  rebucket(newMax) {
30054
30264
  if (this.bucketMax === 0) {
30055
30265
  if (this.pendingZeros > 0) {
@@ -30182,11 +30392,6 @@ var BidirectionalIir = class {
30182
30392
  const tauCausal = this.smoothingMs / 1e3;
30183
30393
  this.alphaCausal = tauCausal > 0 ? 1 - Math.exp(-samplePeriod / tauCausal) : 1;
30184
30394
  }
30185
- /**
30186
- * Bidirectional one-pole IIR with sqrt(2)*tau compensation.
30187
- * Returns a fresh array; input is not mutated. Identity when
30188
- * smoothingMs <= 0.
30189
- */
30190
30395
  applyBidirectional(input) {
30191
30396
  const output = Float32Array.from(input);
30192
30397
  if (this.smoothingMs <= 0) return output;
@@ -30206,13 +30411,6 @@ var BidirectionalIir = class {
30206
30411
  }
30207
30412
  return output;
30208
30413
  }
30209
- /**
30210
- * Forward-only one-pole IIR, single pass, no compensation. State is
30211
- * carried by the caller via `{ value: number }` so chunked use can
30212
- * be continuous. Identity when smoothingMs <= 0 (the input is
30213
- * returned as a fresh copy so callers can rely on a fresh buffer
30214
- * regardless).
30215
- */
30216
30414
  applyCausal(input, state) {
30217
30415
  const output = Float32Array.from(input);
30218
30416
  if (this.smoothingMs <= 0) return output;
@@ -30227,22 +30425,6 @@ var BidirectionalIir = class {
30227
30425
  state.value = y;
30228
30426
  return output;
30229
30427
  }
30230
- /**
30231
- * Forward HALF of the bidirectional cascade, chunked. Same loop body
30232
- * as `applyCausal`, but uses `alphaBidirectional` (the sqrt(2)-
30233
- * compensated alpha) so the output, after a subsequent
30234
- * `applyBackwardPassInPlace` on the concatenated forward result,
30235
- * matches `applyBidirectional`'s output byte-for-byte.
30236
- *
30237
- * State is carried by the caller via `{ value: number }`. For the
30238
- * very first chunk the caller must seed `state.value = input[0]`
30239
- * (matching `applyBidirectional`'s "init from first sample" rule);
30240
- * for subsequent chunks `state.value` carries the previous chunk's
30241
- * trailing forward state.
30242
- *
30243
- * Identity when smoothingMs <= 0 (returns a fresh copy of the
30244
- * input).
30245
- */
30246
30428
  applyForwardPass(input, state) {
30247
30429
  const output = Float32Array.from(input);
30248
30430
  if (this.smoothingMs <= 0) return output;
@@ -30257,21 +30439,6 @@ var BidirectionalIir = class {
30257
30439
  state.value = y;
30258
30440
  return output;
30259
30441
  }
30260
- /**
30261
- * Backward HALF of the bidirectional cascade, run IN PLACE on a
30262
- * whole-array buffer. The buffer is expected to hold the result of
30263
- * the forward HALF (e.g. produced by chunked `applyForwardPass`
30264
- * calls concatenated together). Overwrites `buffer` with the final
30265
- * smoothed result.
30266
- *
30267
- * Init from `buffer[buffer.length - 1]`, matching the second pass of
30268
- * `applyBidirectional`. Uses `alphaBidirectional`.
30269
- *
30270
- * Whole-array (not chunked) — backward IIR cannot stream forward
30271
- * because it must walk end-to-start.
30272
- *
30273
- * Identity (no-op) when smoothingMs <= 0.
30274
- */
30275
30442
  applyBackwardPassInPlace(buffer) {
30276
30443
  if (this.smoothingMs <= 0) return;
30277
30444
  if (buffer.length === 0) return;
@@ -30304,12 +30471,7 @@ var BlockSumAccumulator = class {
30304
30471
  this.ringSize = computeRingSize(blockSize, blockStep);
30305
30472
  this.activeBlockSums = new Float64Array(this.ringSize);
30306
30473
  }
30307
- /**
30308
- * Consume `frames` per-frame sums starting at `perFrameSums[0]`.
30309
- * `perFrameSums.length` must be at least `frames`. Block boundary
30310
- * accounting advances exactly as if these values were appended to a
30311
- * single contiguous stream.
30312
- */
30474
+ // Consumes `frames` per-frame sums from `perFrameSums[0]` (needs length >= `frames`); block-boundary accounting advances as if appended to one contiguous stream.
30313
30475
  push(perFrameSums, frames) {
30314
30476
  if (this.finalized) {
30315
30477
  throw new Error("BlockSumAccumulator: push() called after finalize()");
@@ -30322,34 +30484,35 @@ var BlockSumAccumulator = class {
30322
30484
  const blockStep = this.blockStep;
30323
30485
  const ringSize = this.ringSize;
30324
30486
  const activeBlockSums = this.activeBlockSums;
30487
+ let samplesProcessed = this.samplesProcessed;
30488
+ let nextBlockToOpen = this.nextBlockToOpen;
30489
+ let nextBlockToClose = this.nextBlockToClose;
30490
+ let nextOpenAt = nextBlockToOpen * blockStep;
30491
+ let nextCloseAt = nextBlockToClose * blockStep + blockSize;
30325
30492
  for (let frameIndex = 0; frameIndex < frames; frameIndex++) {
30326
- const globalSampleIndex = this.samplesProcessed;
30327
30493
  const sampleContribution = perFrameSums[frameIndex] ?? 0;
30328
- const rawMinBlock = Math.ceil((globalSampleIndex - blockSize + 1) / blockStep);
30329
- const minBlock = rawMinBlock < 0 ? 0 : rawMinBlock;
30330
- const maxBlock = Math.floor(globalSampleIndex / blockStep);
30331
- while (this.nextBlockToOpen <= maxBlock) {
30332
- activeBlockSums[this.nextBlockToOpen % ringSize] = 0;
30333
- this.nextBlockToOpen++;
30334
- }
30335
- for (let blockIndex = minBlock; blockIndex <= maxBlock; blockIndex++) {
30336
- const slot = blockIndex % ringSize;
30494
+ while (samplesProcessed >= nextOpenAt) {
30495
+ activeBlockSums[nextBlockToOpen % ringSize] = 0;
30496
+ nextBlockToOpen++;
30497
+ nextOpenAt += blockStep;
30498
+ }
30499
+ let slot = nextBlockToClose % ringSize;
30500
+ for (let blockIndex = nextBlockToClose; blockIndex < nextBlockToOpen; blockIndex++) {
30337
30501
  activeBlockSums[slot] = (activeBlockSums[slot] ?? 0) + sampleContribution;
30502
+ slot++;
30503
+ if (slot === ringSize) slot = 0;
30338
30504
  }
30339
- this.samplesProcessed = globalSampleIndex + 1;
30340
- while (this.samplesProcessed >= this.nextBlockToClose * blockStep + blockSize) {
30341
- const closingIndex = this.nextBlockToClose;
30342
- const slot = closingIndex % ringSize;
30343
- this.closedBlockSums.push(activeBlockSums[slot] ?? 0);
30344
- this.nextBlockToClose++;
30505
+ samplesProcessed++;
30506
+ while (samplesProcessed >= nextCloseAt) {
30507
+ this.closedBlockSums.push(activeBlockSums[nextBlockToClose % ringSize] ?? 0);
30508
+ nextBlockToClose++;
30509
+ nextCloseAt += blockStep;
30345
30510
  }
30346
30511
  }
30512
+ this.samplesProcessed = samplesProcessed;
30513
+ this.nextBlockToOpen = nextBlockToOpen;
30514
+ this.nextBlockToClose = nextBlockToClose;
30347
30515
  }
30348
- /**
30349
- * Return the raw closed block sums in block-index order. Idempotent —
30350
- * additional calls return the same array. Subsequent {@link push}
30351
- * calls throw.
30352
- */
30353
30516
  finalize() {
30354
30517
  this.finalized = true;
30355
30518
  return this.closedBlockSums;
@@ -30475,14 +30638,7 @@ var KWeightedSquaredSum = class {
30475
30638
  this.weights[channelIndex] = weights[channelIndex] ?? 1;
30476
30639
  }
30477
30640
  }
30478
- /**
30479
- * Consume `frames` of audio. `channels[c]` must have at least `frames`
30480
- * valid samples starting at index 0; oversized buffers are fine.
30481
- * `output` must have at least `frames` entries — `output[i]` receives
30482
- * the K-weighted, channel-weighted, squared sum at frame `i`. Biquad
30483
- * state advances exactly as if the samples were appended to a single
30484
- * contiguous buffer.
30485
- */
30641
+ // `channels[c]` and `output` need >= `frames` entries from index 0 (oversized OK); `output[i]` gets the K-weighted channel-weighted squared sum at frame `i`; biquad state advances as if appended contiguously.
30486
30642
  push(channels, frames, output) {
30487
30643
  if (channels.length !== this.channelCount) {
30488
30644
  throw new Error(`KWeightedSquaredSum: push got ${channels.length} channels, expected ${this.channelCount}`);
@@ -30509,40 +30665,56 @@ var KWeightedSquaredSum = class {
30509
30665
  const rlbB2 = this.rlbB2;
30510
30666
  const rlbA1 = this.rlbA1;
30511
30667
  const rlbA2 = this.rlbA2;
30512
- const preX1 = this.preX1;
30513
- const preX2 = this.preX2;
30514
- const preY1 = this.preY1;
30515
- const preY2 = this.preY2;
30516
- const rlbX1 = this.rlbX1;
30517
- const rlbX2 = this.rlbX2;
30518
- const rlbY1 = this.rlbY1;
30519
- const rlbY2 = this.rlbY2;
30520
- for (let frameIndex = 0; frameIndex < frames; frameIndex++) {
30521
- let sampleContribution = 0;
30522
- for (let channelIndex = 0; channelIndex < channelCount; channelIndex++) {
30523
- const channel = channels[channelIndex] ?? channels[0] ?? new Float32Array(0);
30524
- const x0 = channel[frameIndex] ?? 0;
30525
- const px1 = preX1[channelIndex] ?? 0;
30526
- const px2 = preX2[channelIndex] ?? 0;
30527
- const py1 = preY1[channelIndex] ?? 0;
30528
- const py2 = preY2[channelIndex] ?? 0;
30529
- const preY = preB0 * x0 + preB1 * px1 + preB2 * px2 - preA1 * py1 - preA2 * py2;
30530
- preX2[channelIndex] = px1;
30531
- preX1[channelIndex] = x0;
30532
- preY2[channelIndex] = py1;
30533
- preY1[channelIndex] = preY;
30534
- const rx1 = rlbX1[channelIndex] ?? 0;
30535
- const rx2 = rlbX2[channelIndex] ?? 0;
30536
- const ry1 = rlbY1[channelIndex] ?? 0;
30537
- const ry2 = rlbY2[channelIndex] ?? 0;
30538
- const rlbY = rlbB0 * preY + rlbB1 * rx1 + rlbB2 * rx2 - rlbA1 * ry1 - rlbA2 * ry2;
30539
- rlbX2[channelIndex] = rx1;
30540
- rlbX1[channelIndex] = preY;
30541
- rlbY2[channelIndex] = ry1;
30542
- rlbY1[channelIndex] = rlbY;
30543
- sampleContribution += (weights[channelIndex] ?? 1) * rlbY * rlbY;
30544
- }
30545
- output[frameIndex] = sampleContribution;
30668
+ for (let channelIndex = 0; channelIndex < channelCount; channelIndex++) {
30669
+ const channel = channels[channelIndex] ?? channels[0] ?? new Float32Array(0);
30670
+ const weight = weights[channelIndex] ?? 1;
30671
+ let px1 = this.preX1[channelIndex] ?? 0;
30672
+ let px2 = this.preX2[channelIndex] ?? 0;
30673
+ let py1 = this.preY1[channelIndex] ?? 0;
30674
+ let py2 = this.preY2[channelIndex] ?? 0;
30675
+ let rx1 = this.rlbX1[channelIndex] ?? 0;
30676
+ let rx2 = this.rlbX2[channelIndex] ?? 0;
30677
+ let ry1 = this.rlbY1[channelIndex] ?? 0;
30678
+ let ry2 = this.rlbY2[channelIndex] ?? 0;
30679
+ if (channelIndex === 0) {
30680
+ for (let frameIndex = 0; frameIndex < frames; frameIndex++) {
30681
+ const x0 = channel[frameIndex] ?? 0;
30682
+ const preY = preB0 * x0 + preB1 * px1 + preB2 * px2 - preA1 * py1 - preA2 * py2;
30683
+ px2 = px1;
30684
+ px1 = x0;
30685
+ py2 = py1;
30686
+ py1 = preY;
30687
+ const rlbY = rlbB0 * preY + rlbB1 * rx1 + rlbB2 * rx2 - rlbA1 * ry1 - rlbA2 * ry2;
30688
+ rx2 = rx1;
30689
+ rx1 = preY;
30690
+ ry2 = ry1;
30691
+ ry1 = rlbY;
30692
+ output[frameIndex] = weight * rlbY * rlbY;
30693
+ }
30694
+ } else {
30695
+ for (let frameIndex = 0; frameIndex < frames; frameIndex++) {
30696
+ const x0 = channel[frameIndex] ?? 0;
30697
+ const preY = preB0 * x0 + preB1 * px1 + preB2 * px2 - preA1 * py1 - preA2 * py2;
30698
+ px2 = px1;
30699
+ px1 = x0;
30700
+ py2 = py1;
30701
+ py1 = preY;
30702
+ const rlbY = rlbB0 * preY + rlbB1 * rx1 + rlbB2 * rx2 - rlbA1 * ry1 - rlbA2 * ry2;
30703
+ rx2 = rx1;
30704
+ rx1 = preY;
30705
+ ry2 = ry1;
30706
+ ry1 = rlbY;
30707
+ output[frameIndex] = (output[frameIndex] ?? 0) + weight * rlbY * rlbY;
30708
+ }
30709
+ }
30710
+ this.preX1[channelIndex] = px1;
30711
+ this.preX2[channelIndex] = px2;
30712
+ this.preY1[channelIndex] = py1;
30713
+ this.preY2[channelIndex] = py2;
30714
+ this.rlbX1[channelIndex] = rx1;
30715
+ this.rlbX2[channelIndex] = rx2;
30716
+ this.rlbY1[channelIndex] = ry1;
30717
+ this.rlbY2[channelIndex] = ry2;
30546
30718
  }
30547
30719
  }
30548
30720
  };
@@ -30602,13 +30774,7 @@ var IntegratedLufsAccumulator = class {
30602
30774
  this.kw = new KWeightedSquaredSum(sampleRate, channelCount, channelWeights);
30603
30775
  this.blocks = new BlockSumAccumulator(this.blockSize, blockStep);
30604
30776
  }
30605
- /**
30606
- * Consume `frames` of audio. `channels[c]` must have at least
30607
- * `frames` valid samples starting at index 0; oversized buffers are
30608
- * fine and avoid the need for caller-side slicing. The accumulator
30609
- * advances biquad state and block accounting exactly as if these
30610
- * samples were appended to a single contiguous buffer.
30611
- */
30777
+ // `channels[c]` needs >= `frames` valid samples from index 0 (oversized OK); state advances as if appended to one contiguous buffer.
30612
30778
  push(channels, frames) {
30613
30779
  if (this.finalized) {
30614
30780
  throw new Error("IntegratedLufsAccumulator: push() called after finalize()");
@@ -30620,12 +30786,6 @@ var IntegratedLufsAccumulator = class {
30620
30786
  this.kw.push(channels, frames, this.outputBuffer);
30621
30787
  this.blocks.push(this.outputBuffer, frames);
30622
30788
  }
30623
- /**
30624
- * Apply the BS.1770 two-stage gating to all completed blocks and
30625
- * return integrated LUFS. Returns -Infinity if no blocks completed
30626
- * (signal shorter than one 400 ms block) or if every block fails the
30627
- * absolute or relative gate.
30628
- */
30629
30789
  finalize() {
30630
30790
  this.finalized = true;
30631
30791
  return applyBs1770Gating(this.blocks.finalize(), this.blockSize);
@@ -30709,11 +30869,6 @@ var LoudnessAccumulator = class {
30709
30869
  this.blocks400 = new BlockSumAccumulator(this.blockSize400, blockStep);
30710
30870
  this.blocks3s = new BlockSumAccumulator(this.blockSize3s, blockStep);
30711
30871
  }
30712
- /**
30713
- * Consume `frames` of audio, advancing the K-weighting state and both
30714
- * block-sum accumulators in lockstep. Identical chunk-boundary
30715
- * semantics to {@link IntegratedLufsAccumulator.push}.
30716
- */
30717
30872
  push(channels, frames) {
30718
30873
  if (this.finalized) {
30719
30874
  throw new Error("LoudnessAccumulator: push() called after finalize()");
@@ -30726,11 +30881,6 @@ var LoudnessAccumulator = class {
30726
30881
  this.blocks400.push(this.outputBuffer, frames);
30727
30882
  this.blocks3s.push(this.outputBuffer, frames);
30728
30883
  }
30729
- /**
30730
- * Finalize and return all four metrics. Idempotent — additional calls
30731
- * return the same cached result object reference. Subsequent
30732
- * {@link push} calls throw.
30733
- */
30734
30884
  finalize() {
30735
30885
  if (this.cachedResult !== void 0) return this.cachedResult;
30736
30886
  this.finalized = true;
@@ -31193,20 +31343,6 @@ var SlidingWindowMaxStream = class {
31193
31343
  this.lookAhead = new Float32Array(ringCapacity);
31194
31344
  this.deque = new Int32Array(ringCapacity);
31195
31345
  }
31196
- /**
31197
- * Ingest the next chunk of input. Returns the next chunk of output
31198
- * (deferred by `halfWidth` on the leading edge). When `isFinal` is
31199
- * `true`, the trailing-edge outputs are flushed with the right-edge
31200
- * clamped to the last ingested input index.
31201
- *
31202
- * The return value's length depends on the leading-edge fill state:
31203
- * - Until total ingested ≥ `halfWidth + 1`, output length is 0
31204
- * unless `isFinal` is true.
31205
- * - Steady state: output length equals input chunk length.
31206
- * - On the final chunk: output length equals input chunk length
31207
- * plus any trailing-edge outputs not yet emitted (so total
31208
- * across all calls equals total input length).
31209
- */
31210
31346
  push(chunk, isFinal) {
31211
31347
  const chunkLength = chunk.length;
31212
31348
  const halfWidth = this.halfWidth;
@@ -31276,12 +31412,6 @@ var SlidingWindowMinStream = class {
31276
31412
  this.lookAhead = new Float32Array(ringCapacity);
31277
31413
  this.deque = new Int32Array(ringCapacity);
31278
31414
  }
31279
- /**
31280
- * Ingest the next chunk of input. Returns the next chunk of output
31281
- * (deferred by `halfWidth` on the leading edge). When `isFinal` is
31282
- * `true`, the trailing-edge outputs are flushed with the right-edge
31283
- * clamped to the last ingested input index.
31284
- */
31285
31415
  push(chunk, isFinal) {
31286
31416
  const chunkLength = chunk.length;
31287
31417
  const halfWidth = this.halfWidth;
@@ -31578,119 +31708,144 @@ function butterflyStages(re, im, size) {
31578
31708
  }
31579
31709
  var TAPS_PER_PHASE_4X = 12;
31580
31710
  var HISTORY_LENGTH_4X = TAPS_PER_PHASE_4X;
31581
- var COEFFICIENTS_4X = [
31582
- // Phase 0 — identity tap (output sample = input sample).
31583
- [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
31584
- // Phase 1
31585
- [
31586
- 0.001708984375,
31587
- 0.010986328125,
31588
- -0.0196533203125,
31589
- 0.033203125,
31590
- -0.0594482421875,
31591
- 0.1373291015625,
31592
- 0.97216796875,
31593
- -0.102294921875,
31594
- 0.047607421875,
31595
- -0.026611328125,
31596
- 0.014892578125,
31597
- -0.00830078125
31598
- ],
31599
- // Phase 2
31600
- [
31601
- -0.0291748046875,
31602
- 0.029296875,
31603
- -0.0517578125,
31604
- 0.089111328125,
31605
- -0.16650390625,
31606
- 0.465087890625,
31607
- 0.77978515625,
31608
- -0.2003173828125,
31609
- 0.1015625,
31610
- -0.0582275390625,
31611
- 0.0330810546875,
31612
- -0.0189208984375
31613
- ],
31614
- // Phase 3
31615
- [
31616
- -0.0189208984375,
31617
- 0.0330810546875,
31618
- -0.0582275390625,
31619
- 0.1015625,
31620
- -0.2003173828125,
31621
- 0.77978515625,
31622
- 0.465087890625,
31623
- -0.16650390625,
31624
- 0.089111328125,
31625
- -0.0517578125,
31626
- 0.029296875,
31627
- -0.0291748046875
31628
- ]
31629
- ];
31711
+ var P1T0 = 0.001708984375;
31712
+ var P1T1 = 0.010986328125;
31713
+ var P1T2 = -0.0196533203125;
31714
+ var P1T3 = 0.033203125;
31715
+ var P1T4 = -0.0594482421875;
31716
+ var P1T5 = 0.1373291015625;
31717
+ var P1T6 = 0.97216796875;
31718
+ var P1T7 = -0.102294921875;
31719
+ var P1T8 = 0.047607421875;
31720
+ var P1T9 = -0.026611328125;
31721
+ var P1T10 = 0.014892578125;
31722
+ var P1T11 = -0.00830078125;
31723
+ var P2T0 = -0.0291748046875;
31724
+ var P2T1 = 0.029296875;
31725
+ var P2T2 = -0.0517578125;
31726
+ var P2T3 = 0.089111328125;
31727
+ var P2T4 = -0.16650390625;
31728
+ var P2T5 = 0.465087890625;
31729
+ var P2T6 = 0.77978515625;
31730
+ var P2T7 = -0.2003173828125;
31731
+ var P2T8 = 0.1015625;
31732
+ var P2T9 = -0.0582275390625;
31733
+ var P2T10 = 0.0330810546875;
31734
+ var P2T11 = -0.0189208984375;
31735
+ var P3T0 = -0.0189208984375;
31736
+ var P3T1 = 0.0330810546875;
31737
+ var P3T2 = -0.0582275390625;
31738
+ var P3T3 = 0.1015625;
31739
+ var P3T4 = -0.2003173828125;
31740
+ var P3T5 = 0.77978515625;
31741
+ var P3T6 = 0.465087890625;
31742
+ var P3T7 = -0.16650390625;
31743
+ var P3T8 = 0.089111328125;
31744
+ var P3T9 = -0.0517578125;
31745
+ var P3T10 = 0.029296875;
31746
+ var P3T11 = -0.0291748046875;
31630
31747
  var TruePeakUpsampler = class {
31631
31748
  constructor(factor = 4) {
31632
- this.writeIndex = 0;
31749
+ this.work = new Float64Array(0);
31633
31750
  if (factor !== 4) {
31634
31751
  throw new Error(`TruePeakUpsampler: factor ${factor} is not yet implemented; only 4\xD7 (BS.1770-4 Annex 1) is supported`);
31635
31752
  }
31636
31753
  this.factor = factor;
31637
31754
  this.history = new Float64Array(HISTORY_LENGTH_4X);
31638
31755
  }
31639
- /**
31640
- * Push one chunk of input samples and return the upsampled chunk.
31641
- * Output length is `input.length * factor`. State carries across
31642
- * calls — feeding the same source in any chunk pattern produces
31643
- * the same upsampled samples (modulo float ordering, which is
31644
- * deterministic).
31645
- *
31646
- * The first 11 output frames after a {@link reset} (or after
31647
- * construction) are coloured by the cold filter history (zeros).
31648
- * For true-peak measurement this is harmless — the max-tracking
31649
- * downstream sees only values ≤ the actual settled peak during
31650
- * that ramp-up.
31651
- */
31652
- upsample(input) {
31756
+ // `outputScratch` (>= input.length × factor) avoids the per-call allocation; the returned view aliases it.
31757
+ upsample(input, outputScratch) {
31653
31758
  const factor = this.factor;
31654
31759
  const inputLength = input.length;
31655
- const output = new Float32Array(inputLength * factor);
31760
+ const outputLength = inputLength * factor;
31761
+ const output = outputScratch !== void 0 && outputScratch.length >= outputLength ? outputScratch.subarray(0, outputLength) : new Float32Array(outputLength);
31656
31762
  const history = this.history;
31657
31763
  const historyLength = HISTORY_LENGTH_4X;
31658
- let writeIndex = this.writeIndex;
31764
+ const workLength = historyLength + inputLength;
31765
+ if (this.work.length < workLength) {
31766
+ this.work = new Float64Array(workLength);
31767
+ }
31768
+ const work = this.work;
31769
+ work.set(history, 0);
31770
+ for (let inIdx = 0; inIdx < inputLength; inIdx++) {
31771
+ work[historyLength + inIdx] = input[inIdx] ?? 0;
31772
+ }
31659
31773
  for (let inIdx = 0; inIdx < inputLength; inIdx++) {
31660
- const sample = input[inIdx] ?? 0;
31661
- history[writeIndex] = sample;
31662
- writeIndex = (writeIndex + 1) % historyLength;
31774
+ const currentIdx = historyLength + inIdx;
31663
31775
  const outOffset = inIdx * factor;
31664
- output[outOffset] = sample;
31665
- for (let phase2 = 1; phase2 < factor; phase2++) {
31666
- const taps = COEFFICIENTS_4X[phase2];
31667
- if (taps === void 0) continue;
31668
- let acc = 0;
31669
- let readIndex = writeIndex - 1;
31670
- if (readIndex < 0) readIndex += historyLength;
31671
- for (let tap = 0; tap < historyLength; tap++) {
31672
- acc += (taps[tap] ?? 0) * (history[readIndex] ?? 0);
31673
- readIndex -= 1;
31674
- if (readIndex < 0) readIndex += historyLength;
31675
- }
31676
- output[outOffset + phase2] = acc;
31677
- }
31678
- }
31679
- this.writeIndex = writeIndex;
31776
+ output[outOffset] = input[inIdx] ?? 0;
31777
+ const v0 = work[currentIdx] ?? 0;
31778
+ const v1 = work[currentIdx - 1] ?? 0;
31779
+ const v2 = work[currentIdx - 2] ?? 0;
31780
+ const v3 = work[currentIdx - 3] ?? 0;
31781
+ const v4 = work[currentIdx - 4] ?? 0;
31782
+ const v5 = work[currentIdx - 5] ?? 0;
31783
+ const v6 = work[currentIdx - 6] ?? 0;
31784
+ const v7 = work[currentIdx - 7] ?? 0;
31785
+ const v8 = work[currentIdx - 8] ?? 0;
31786
+ const v9 = work[currentIdx - 9] ?? 0;
31787
+ const v10 = work[currentIdx - 10] ?? 0;
31788
+ const v11 = work[currentIdx - 11] ?? 0;
31789
+ let acc1 = 0;
31790
+ acc1 += P1T0 * v0;
31791
+ acc1 += P1T1 * v1;
31792
+ acc1 += P1T2 * v2;
31793
+ acc1 += P1T3 * v3;
31794
+ acc1 += P1T4 * v4;
31795
+ acc1 += P1T5 * v5;
31796
+ acc1 += P1T6 * v6;
31797
+ acc1 += P1T7 * v7;
31798
+ acc1 += P1T8 * v8;
31799
+ acc1 += P1T9 * v9;
31800
+ acc1 += P1T10 * v10;
31801
+ acc1 += P1T11 * v11;
31802
+ let acc2 = 0;
31803
+ acc2 += P2T0 * v0;
31804
+ acc2 += P2T1 * v1;
31805
+ acc2 += P2T2 * v2;
31806
+ acc2 += P2T3 * v3;
31807
+ acc2 += P2T4 * v4;
31808
+ acc2 += P2T5 * v5;
31809
+ acc2 += P2T6 * v6;
31810
+ acc2 += P2T7 * v7;
31811
+ acc2 += P2T8 * v8;
31812
+ acc2 += P2T9 * v9;
31813
+ acc2 += P2T10 * v10;
31814
+ acc2 += P2T11 * v11;
31815
+ let acc3 = 0;
31816
+ acc3 += P3T0 * v0;
31817
+ acc3 += P3T1 * v1;
31818
+ acc3 += P3T2 * v2;
31819
+ acc3 += P3T3 * v3;
31820
+ acc3 += P3T4 * v4;
31821
+ acc3 += P3T5 * v5;
31822
+ acc3 += P3T6 * v6;
31823
+ acc3 += P3T7 * v7;
31824
+ acc3 += P3T8 * v8;
31825
+ acc3 += P3T9 * v9;
31826
+ acc3 += P3T10 * v10;
31827
+ acc3 += P3T11 * v11;
31828
+ output[outOffset + 1] = acc1;
31829
+ output[outOffset + 2] = acc2;
31830
+ output[outOffset + 3] = acc3;
31831
+ }
31832
+ if (inputLength >= historyLength) {
31833
+ history.set(work.subarray(workLength - historyLength, workLength), 0);
31834
+ } else if (inputLength > 0) {
31835
+ history.copyWithin(0, inputLength);
31836
+ history.set(work.subarray(historyLength, workLength), historyLength - inputLength);
31837
+ }
31680
31838
  return output;
31681
31839
  }
31682
- /**
31683
- * Reset the 12-sample input history to zeros. Use when starting a
31684
- * new render / stream / measurement on the same instance.
31685
- */
31686
31840
  reset() {
31687
31841
  this.history.fill(0);
31688
- this.writeIndex = 0;
31842
+ this.work = new Float64Array(0);
31689
31843
  }
31690
31844
  };
31691
31845
  var DEFAULT_OVERSAMPLE_FACTOR = 4;
31692
31846
  var TruePeakAccumulator = class {
31693
31847
  constructor(_sampleRate, channelCount, oversampleFactor = DEFAULT_OVERSAMPLE_FACTOR) {
31848
+ this.upsampleScratch = new Float32Array(0);
31694
31849
  this.runningMax = 0;
31695
31850
  if (channelCount <= 0) {
31696
31851
  throw new Error(`TruePeakAccumulator: channelCount must be positive, got ${channelCount}`);
@@ -31702,18 +31857,7 @@ var TruePeakAccumulator = class {
31702
31857
  }
31703
31858
  this.upsamplers = upsamplers;
31704
31859
  }
31705
- /**
31706
- * Consume `frames` of audio. `channels[c]` must have at least
31707
- * `frames` valid samples starting at index 0; oversized buffers are
31708
- * fine and avoid the need for caller-side slicing. The accumulator
31709
- * advances per-channel upsampler state exactly as if the samples
31710
- * were appended to a single contiguous buffer.
31711
- *
31712
- * The `frames` argument is accepted for symmetry with
31713
- * {@link IntegratedLufsAccumulator} even though
31714
- * {@link TruePeakUpsampler.upsample} derives length from the input
31715
- * slice.
31716
- */
31860
+ // `channels[c]` needs >= `frames` valid samples from index 0 (oversized OK); advances per-channel upsampler state as if appended contiguously.
31717
31861
  push(channels, frames) {
31718
31862
  if (channels.length !== this.channelCount) {
31719
31863
  throw new Error(`TruePeakAccumulator: push got ${channels.length} channels, expected ${this.channelCount}`);
@@ -31729,7 +31873,10 @@ var TruePeakAccumulator = class {
31729
31873
  throw new Error(`TruePeakAccumulator: missing upsampler for channel ${channelIndex}`);
31730
31874
  }
31731
31875
  const slice = channel.length === frames ? channel : channel.subarray(0, frames);
31732
- const upsampled = upsampler.upsample(slice);
31876
+ if (this.upsampleScratch.length < frames * upsampler.factor) {
31877
+ this.upsampleScratch = new Float32Array(frames * upsampler.factor);
31878
+ }
31879
+ const upsampled = upsampler.upsample(slice, this.upsampleScratch);
31733
31880
  for (let index = 0; index < upsampled.length; index++) {
31734
31881
  const sample = upsampled[index] ?? 0;
31735
31882
  const magnitude = sample < 0 ? -sample : sample;
@@ -31737,11 +31884,6 @@ var TruePeakAccumulator = class {
31737
31884
  }
31738
31885
  }
31739
31886
  }
31740
- /**
31741
- * Return the linear amplitude max(|x|) across all upsampled samples
31742
- * and all channels. Idempotent — additional calls return the same
31743
- * value. Returns 0 when no samples have been pushed.
31744
- */
31745
31887
  finalize() {
31746
31888
  return this.runningMax;
31747
31889
  }
@@ -32244,10 +32386,10 @@ var _ReadFfmpegNode = class _ReadFfmpegNode extends SourceNode {
32244
32386
  return new _ReadFfmpegNode({ ...this.properties, previousProperties: this.properties, ...overrides });
32245
32387
  }
32246
32388
  };
32247
- _ReadFfmpegNode.moduleName = "Read FFmpeg";
32389
+ _ReadFfmpegNode.nodeName = "Read FFmpeg";
32248
32390
  _ReadFfmpegNode.packageName = PACKAGE_NAME;
32249
32391
  _ReadFfmpegNode.packageVersion = PACKAGE_VERSION;
32250
- _ReadFfmpegNode.moduleDescription = "Read audio from a file using FFmpeg";
32392
+ _ReadFfmpegNode.nodeDescription = "Read audio from a file using FFmpeg";
32251
32393
  _ReadFfmpegNode.schema = ffmpegSchema;
32252
32394
  var ReadFfmpegNode = _ReadFfmpegNode;
32253
32395
  function readFfmpeg(path2, options) {
@@ -32427,10 +32569,10 @@ var _ReadWavNode = class _ReadWavNode extends SourceNode {
32427
32569
  return new _ReadWavNode({ ...this.properties, previousProperties: this.properties, ...overrides });
32428
32570
  }
32429
32571
  };
32430
- _ReadWavNode.moduleName = "Read WAV";
32572
+ _ReadWavNode.nodeName = "Read WAV";
32431
32573
  _ReadWavNode.packageName = PACKAGE_NAME;
32432
32574
  _ReadWavNode.packageVersion = PACKAGE_VERSION;
32433
- _ReadWavNode.moduleDescription = "Read audio from a WAV file";
32575
+ _ReadWavNode.nodeDescription = "Read audio from a WAV file";
32434
32576
  _ReadWavNode.schema = wavSchema;
32435
32577
  var ReadWavNode = _ReadWavNode;
32436
32578
  function readWav(path2, options) {
@@ -32462,10 +32604,10 @@ var _ReadNode = class _ReadNode extends SourceNode {
32462
32604
  return new _ReadNode({ ...this.properties, previousProperties: this.properties, ...overrides });
32463
32605
  }
32464
32606
  };
32465
- _ReadNode.moduleName = "Read";
32607
+ _ReadNode.nodeName = "Read";
32466
32608
  _ReadNode.packageName = PACKAGE_NAME;
32467
32609
  _ReadNode.packageVersion = PACKAGE_VERSION;
32468
- _ReadNode.moduleDescription = "Read audio from a file";
32610
+ _ReadNode.nodeDescription = "Read audio from a file";
32469
32611
  _ReadNode.schema = schema;
32470
32612
  var ReadNode = _ReadNode;
32471
32613
  function read(path2, options) {
@@ -32599,10 +32741,10 @@ var _LoudnessStatsNode = class _LoudnessStatsNode extends TargetNode {
32599
32741
  return new _LoudnessStatsNode({ ...this.properties, previousProperties: this.properties, ...overrides });
32600
32742
  }
32601
32743
  };
32602
- _LoudnessStatsNode.moduleName = "Loudness Stats";
32744
+ _LoudnessStatsNode.nodeName = "Loudness Stats";
32603
32745
  _LoudnessStatsNode.packageName = PACKAGE_NAME;
32604
32746
  _LoudnessStatsNode.packageVersion = PACKAGE_VERSION;
32605
- _LoudnessStatsNode.moduleDescription = "Measure integrated loudness, true peak, and loudness range per EBU R128, plus an amplitude-distribution histogram";
32747
+ _LoudnessStatsNode.nodeDescription = "Measure integrated loudness, true peak, and loudness range per EBU R128, plus an amplitude-distribution histogram";
32606
32748
  _LoudnessStatsNode.schema = schema2;
32607
32749
  var LoudnessStatsNode = _LoudnessStatsNode;
32608
32750
  function loudnessStats(options) {
@@ -32942,10 +33084,10 @@ var _SpectrogramNode = class _SpectrogramNode extends TargetNode {
32942
33084
  return new _SpectrogramNode({ ...this.properties, previousProperties: this.properties, ...overrides });
32943
33085
  }
32944
33086
  };
32945
- _SpectrogramNode.moduleName = "Spectrogram";
33087
+ _SpectrogramNode.nodeName = "Spectrogram";
32946
33088
  _SpectrogramNode.packageName = PACKAGE_NAME;
32947
33089
  _SpectrogramNode.packageVersion = PACKAGE_VERSION;
32948
- _SpectrogramNode.moduleDescription = "Generate spectrogram visualization data";
33090
+ _SpectrogramNode.nodeDescription = "Generate spectrogram visualization data";
32949
33091
  _SpectrogramNode.schema = schema3;
32950
33092
  var SpectrogramNode = _SpectrogramNode;
32951
33093
  function spectrogram(outputPath, options) {
@@ -33093,10 +33235,10 @@ var _WaveformNode = class _WaveformNode extends TargetNode {
33093
33235
  return new _WaveformNode({ ...this.properties, previousProperties: this.properties, ...overrides });
33094
33236
  }
33095
33237
  };
33096
- _WaveformNode.moduleName = "Waveform";
33238
+ _WaveformNode.nodeName = "Waveform";
33097
33239
  _WaveformNode.packageName = PACKAGE_NAME;
33098
33240
  _WaveformNode.packageVersion = PACKAGE_VERSION;
33099
- _WaveformNode.moduleDescription = "Generate waveform visualization data";
33241
+ _WaveformNode.nodeDescription = "Generate waveform visualization data";
33100
33242
  _WaveformNode.schema = schema4;
33101
33243
  var WaveformNode = _WaveformNode;
33102
33244
  function waveform(outputPath, options) {
@@ -33411,10 +33553,10 @@ var _WriteNode = class _WriteNode extends TargetNode {
33411
33553
  return new _WriteNode({ ...this.properties, previousProperties: this.properties, ...overrides });
33412
33554
  }
33413
33555
  };
33414
- _WriteNode.moduleName = "Write";
33556
+ _WriteNode.nodeName = "Write";
33415
33557
  _WriteNode.packageName = PACKAGE_NAME;
33416
33558
  _WriteNode.packageVersion = PACKAGE_VERSION;
33417
- _WriteNode.moduleDescription = "Write audio to a file";
33559
+ _WriteNode.nodeDescription = "Write audio to a file";
33418
33560
  _WriteNode.schema = schema5;
33419
33561
  var WriteNode = _WriteNode;
33420
33562
  function write(path2, options) {
@@ -33502,10 +33644,10 @@ var _CutNode = class _CutNode extends TransformNode {
33502
33644
  return new _CutNode({ ...this.properties, previousProperties: this.properties, ...overrides });
33503
33645
  }
33504
33646
  };
33505
- _CutNode.moduleName = "Cut";
33647
+ _CutNode.nodeName = "Cut";
33506
33648
  _CutNode.packageName = PACKAGE_NAME;
33507
33649
  _CutNode.packageVersion = PACKAGE_VERSION;
33508
- _CutNode.moduleDescription = "Remove a region of audio";
33650
+ _CutNode.nodeDescription = "Remove a region of audio";
33509
33651
  _CutNode.schema = schema6;
33510
33652
  var CutNode = _CutNode;
33511
33653
  function cut(regions, options) {
@@ -33567,10 +33709,10 @@ var _DitherNode = class _DitherNode extends TransformNode {
33567
33709
  return new _DitherNode({ ...this.properties, previousProperties: this.properties, ...overrides });
33568
33710
  }
33569
33711
  };
33570
- _DitherNode.moduleName = "Dither";
33712
+ _DitherNode.nodeName = "Dither";
33571
33713
  _DitherNode.packageName = PACKAGE_NAME;
33572
33714
  _DitherNode.packageVersion = PACKAGE_VERSION;
33573
- _DitherNode.moduleDescription = "Add shaped noise to reduce quantization distortion";
33715
+ _DitherNode.nodeDescription = "Add shaped noise to reduce quantization distortion";
33574
33716
  _DitherNode.schema = schema7;
33575
33717
  var DitherNode = _DitherNode;
33576
33718
  function dither(bitDepth, options) {
@@ -33629,10 +33771,10 @@ var _NormalizeNode = class _NormalizeNode extends TransformNode {
33629
33771
  return new _NormalizeNode({ ...this.properties, previousProperties: this.properties, ...overrides });
33630
33772
  }
33631
33773
  };
33632
- _NormalizeNode.moduleName = "Normalize";
33774
+ _NormalizeNode.nodeName = "Normalize";
33633
33775
  _NormalizeNode.packageName = PACKAGE_NAME;
33634
33776
  _NormalizeNode.packageVersion = PACKAGE_VERSION;
33635
- _NormalizeNode.moduleDescription = "Adjust peak or loudness level to a target ceiling";
33777
+ _NormalizeNode.nodeDescription = "Adjust peak or loudness level to a target ceiling";
33636
33778
  _NormalizeNode.schema = schema8;
33637
33779
  var NormalizeNode = _NormalizeNode;
33638
33780
  function normalize(options) {
@@ -33646,53 +33788,57 @@ var schema9 = external_exports.object({
33646
33788
  after: external_exports.number().min(0).multipleOf(1e-3).default(0).describe("After")
33647
33789
  });
33648
33790
  var PadStream = class extends BufferedTransformStream {
33649
- async _process(buffer) {
33650
- const { before, after } = this.properties;
33651
- const channels = buffer.channels;
33652
- if (channels === 0) return;
33653
- const sr = buffer.sampleRate ?? 44100;
33654
- const bd = buffer.bitDepth;
33655
- const leading = Math.round(before * sr);
33656
- const trailing = Math.round(after * sr);
33657
- if (leading === 0 && trailing === 0) return;
33658
- const output = new ChunkBuffer();
33659
- try {
33660
- await writeSilence(output, leading, channels, CHUNK_FRAMES, sr, bd);
33661
- for (; ; ) {
33662
- const chunk = await buffer.read(CHUNK_FRAMES);
33663
- const chunkFrames = chunk.samples[0]?.length ?? 0;
33664
- if (chunkFrames === 0) break;
33665
- await output.write(chunk.samples, sr, bd);
33666
- if (chunkFrames < CHUNK_FRAMES) break;
33667
- }
33668
- await writeSilence(output, trailing, channels, CHUNK_FRAMES, sr, bd);
33669
- await buffer.clear();
33670
- await output.reset();
33671
- for (; ; ) {
33672
- const chunk = await output.read(CHUNK_FRAMES);
33673
- const chunkFrames = chunk.samples[0]?.length ?? 0;
33674
- if (chunkFrames === 0) break;
33675
- await buffer.write(chunk.samples, sr, bd);
33676
- if (chunkFrames < CHUNK_FRAMES) break;
33791
+ constructor() {
33792
+ super(...arguments);
33793
+ this.seenChunk = false;
33794
+ this.capturedSampleRate = 44100;
33795
+ this.capturedBitDepth = 32;
33796
+ this.capturedChannels = 0;
33797
+ this.outputOffset = 0;
33798
+ }
33799
+ _unbuffer(chunk) {
33800
+ const frames = chunk.samples[0]?.length ?? 0;
33801
+ if (!this.seenChunk) {
33802
+ this.seenChunk = true;
33803
+ this.capturedSampleRate = chunk.sampleRate;
33804
+ this.capturedBitDepth = chunk.bitDepth;
33805
+ this.capturedChannels = chunk.samples.length;
33806
+ const leading = Math.round(this.properties.before * chunk.sampleRate);
33807
+ if (leading > 0) {
33808
+ const samples = chunk.samples.map((channel) => {
33809
+ const padded = new Float32Array(leading + frames);
33810
+ padded.set(channel, leading);
33811
+ return padded;
33812
+ });
33813
+ const offset2 = this.outputOffset;
33814
+ this.outputOffset += leading + frames;
33815
+ return { samples, offset: offset2, sampleRate: chunk.sampleRate, bitDepth: chunk.bitDepth };
33677
33816
  }
33678
- } finally {
33679
- await output.close();
33680
33817
  }
33818
+ const offset = this.outputOffset;
33819
+ this.outputOffset += frames;
33820
+ return { samples: chunk.samples, offset, sampleRate: chunk.sampleRate, bitDepth: chunk.bitDepth };
33681
33821
  }
33682
- };
33683
- async function writeSilence(target, frames, channels, chunkSize, sampleRate, bitDepth) {
33684
- let remaining = frames;
33685
- while (remaining > 0) {
33686
- const take = Math.min(chunkSize, remaining);
33687
- const silence = [];
33688
- for (let ch = 0; ch < channels; ch++) silence.push(new Float32Array(take));
33689
- await target.write(silence, sampleRate, bitDepth);
33690
- remaining -= take;
33822
+ _flush() {
33823
+ if (!this.seenChunk) return void 0;
33824
+ const trailing = Math.round(this.properties.after * this.capturedSampleRate);
33825
+ if (trailing === 0) return void 0;
33826
+ const chunks = [];
33827
+ let remaining = trailing;
33828
+ while (remaining > 0) {
33829
+ const take = Math.min(CHUNK_FRAMES, remaining);
33830
+ const samples = Array.from({ length: this.capturedChannels }, () => new Float32Array(take));
33831
+ const offset = this.outputOffset;
33832
+ this.outputOffset += take;
33833
+ chunks.push({ samples, offset, sampleRate: this.capturedSampleRate, bitDepth: this.capturedBitDepth });
33834
+ remaining -= take;
33835
+ }
33836
+ return chunks;
33691
33837
  }
33692
- }
33838
+ };
33693
33839
  var _PadNode = class _PadNode extends TransformNode {
33694
33840
  constructor(properties) {
33695
- super({ bufferSize: WHOLE_FILE, latency: WHOLE_FILE, ...properties });
33841
+ super({ bufferSize: 0, latency: 0, ...properties });
33696
33842
  this.type = ["buffered-audio-node", "transform", "pad"];
33697
33843
  }
33698
33844
  static is(value) {
@@ -33705,10 +33851,10 @@ var _PadNode = class _PadNode extends TransformNode {
33705
33851
  return new _PadNode({ ...this.properties, previousProperties: this.properties, ...overrides });
33706
33852
  }
33707
33853
  };
33708
- _PadNode.moduleName = "Pad";
33854
+ _PadNode.nodeName = "Pad";
33709
33855
  _PadNode.packageName = PACKAGE_NAME;
33710
33856
  _PadNode.packageVersion = PACKAGE_VERSION;
33711
- _PadNode.moduleDescription = "Add silence to start or end of audio";
33857
+ _PadNode.nodeDescription = "Add silence to start or end of audio";
33712
33858
  _PadNode.schema = schema9;
33713
33859
  var PadNode = _PadNode;
33714
33860
  function pad(options) {
@@ -33782,10 +33928,10 @@ var _PhaseNode = class _PhaseNode extends TransformNode {
33782
33928
  return new _PhaseNode({ ...this.properties, previousProperties: this.properties, ...overrides });
33783
33929
  }
33784
33930
  };
33785
- _PhaseNode.moduleName = "Phase";
33931
+ _PhaseNode.nodeName = "Phase";
33786
33932
  _PhaseNode.packageName = PACKAGE_NAME;
33787
33933
  _PhaseNode.packageVersion = PACKAGE_VERSION;
33788
- _PhaseNode.moduleDescription = "Invert or rotate signal phase";
33934
+ _PhaseNode.nodeDescription = "Invert or rotate signal phase";
33789
33935
  _PhaseNode.schema = schema10;
33790
33936
  var PhaseNode = _PhaseNode;
33791
33937
  function phase(options) {
@@ -33839,10 +33985,10 @@ var _ReverseNode = class _ReverseNode extends TransformNode {
33839
33985
  return new _ReverseNode({ ...this.properties, previousProperties: this.properties, ...overrides });
33840
33986
  }
33841
33987
  };
33842
- _ReverseNode.moduleName = "Reverse";
33988
+ _ReverseNode.nodeName = "Reverse";
33843
33989
  _ReverseNode.packageName = PACKAGE_NAME;
33844
33990
  _ReverseNode.packageVersion = PACKAGE_VERSION;
33845
- _ReverseNode.moduleDescription = "Reverse audio playback direction";
33991
+ _ReverseNode.nodeDescription = "Reverse audio playback direction";
33846
33992
  _ReverseNode.schema = schema11;
33847
33993
  var ReverseNode = _ReverseNode;
33848
33994
  function reverse(options) {
@@ -33970,10 +34116,10 @@ var _SpliceNode = class _SpliceNode extends TransformNode {
33970
34116
  return new _SpliceNode({ ...this.properties, previousProperties: this.properties, ...overrides });
33971
34117
  }
33972
34118
  };
33973
- _SpliceNode.moduleName = "Splice";
34119
+ _SpliceNode.nodeName = "Splice";
33974
34120
  _SpliceNode.packageName = PACKAGE_NAME;
33975
34121
  _SpliceNode.packageVersion = PACKAGE_VERSION;
33976
- _SpliceNode.moduleDescription = "Replace a region of audio with processed content";
34122
+ _SpliceNode.nodeDescription = "Replace a region of audio with processed content";
33977
34123
  _SpliceNode.schema = schema12;
33978
34124
  var SpliceNode = _SpliceNode;
33979
34125
  function splice(insertPath, insertAt, options) {
@@ -34003,7 +34149,6 @@ function findLastAbove(samples, frames, threshold) {
34003
34149
  }
34004
34150
 
34005
34151
  // src/transforms/trim/index.ts
34006
- var CHUNK_FRAMES3 = 44100;
34007
34152
  var schema13 = external_exports.object({
34008
34153
  threshold: external_exports.number().min(0).max(1).multipleOf(1e-3).default(1e-3).describe("Threshold"),
34009
34154
  margin: external_exports.number().min(0).max(1).multipleOf(1e-3).default(0.01).describe("Margin"),
@@ -34011,93 +34156,66 @@ var schema13 = external_exports.object({
34011
34156
  end: external_exports.boolean().default(true).describe("End")
34012
34157
  });
34013
34158
  var TrimStream = class extends BufferedTransformStream {
34159
+ constructor() {
34160
+ super(...arguments);
34161
+ this.firstAbove = Infinity;
34162
+ this.lastAbove = -1;
34163
+ this.scanOffset = 0;
34164
+ this.startFrame = 0;
34165
+ this.endFrame = 0;
34166
+ }
34167
+ async _buffer(chunk, buffer) {
34168
+ await super._buffer(chunk, buffer);
34169
+ const chunkFrames = chunk.samples[0]?.length ?? 0;
34170
+ if (chunkFrames === 0) return;
34171
+ const threshold = this.properties.threshold;
34172
+ const localFirst = findFirstAbove(chunk.samples, chunkFrames, threshold);
34173
+ if (localFirst < chunkFrames) {
34174
+ const abs = this.scanOffset + localFirst;
34175
+ if (abs < this.firstAbove) this.firstAbove = abs;
34176
+ this.lastAbove = Math.max(this.lastAbove, this.scanOffset + findLastAbove(chunk.samples, chunkFrames, threshold));
34177
+ }
34178
+ this.scanOffset += chunkFrames;
34179
+ }
34014
34180
  async _process(buffer) {
34015
34181
  const frames = buffer.frames;
34016
34182
  const channels = buffer.channels;
34017
34183
  if (channels === 0 || frames === 0) return;
34018
- const threshold = this.properties.threshold;
34019
- const sr = buffer.sampleRate ?? 44100;
34020
- const bd = buffer.bitDepth;
34021
- const marginFrames = Math.round(this.properties.margin * sr);
34022
- const trimStart = this.properties.start;
34023
- const trimEnd = this.properties.end;
34024
- await buffer.reset();
34025
- let firstAbove = frames;
34026
- let lastAbove = -1;
34027
- let scanOffset = 0;
34028
- for (; ; ) {
34029
- const chunk = await buffer.read(CHUNK_FRAMES3);
34030
- const chunkFrames = chunk.samples[0]?.length ?? 0;
34031
- if (chunkFrames === 0) break;
34032
- const localFirst = findFirstAbove(chunk.samples, chunkFrames, threshold);
34033
- if (localFirst < chunkFrames) {
34034
- const abs = scanOffset + localFirst;
34035
- if (abs < firstAbove) firstAbove = abs;
34036
- lastAbove = Math.max(lastAbove, scanOffset + findLastAbove(chunk.samples, chunkFrames, threshold));
34037
- }
34038
- scanOffset += chunkFrames;
34039
- if (chunkFrames < CHUNK_FRAMES3) break;
34040
- }
34041
- if (firstAbove >= frames) {
34184
+ if (this.firstAbove >= frames) {
34042
34185
  await buffer.clear();
34043
34186
  return;
34044
34187
  }
34188
+ const sr = buffer.sampleRate ?? 44100;
34189
+ const marginFrames = Math.round(this.properties.margin * sr);
34045
34190
  let startFrame = 0;
34046
34191
  let endFrame = frames;
34047
- if (trimStart) {
34048
- startFrame = Math.max(0, firstAbove - marginFrames);
34049
- }
34050
- if (trimEnd) {
34051
- endFrame = Math.min(frames, lastAbove + 1 + marginFrames);
34052
- }
34192
+ if (this.properties.start) startFrame = Math.max(0, this.firstAbove - marginFrames);
34193
+ if (this.properties.end) endFrame = Math.min(frames, this.lastAbove + 1 + marginFrames);
34053
34194
  if (startFrame >= endFrame) {
34054
34195
  await buffer.clear();
34055
34196
  return;
34056
34197
  }
34057
- if (startFrame === 0 && endFrame === frames) return;
34058
- const output = new ChunkBuffer();
34059
- try {
34060
- await buffer.reset();
34061
- let copyOffset = 0;
34062
- for (; ; ) {
34063
- const chunk = await buffer.read(CHUNK_FRAMES3);
34064
- const chunkFrames = chunk.samples[0]?.length ?? 0;
34065
- if (chunkFrames === 0) break;
34066
- const chunkStart = copyOffset;
34067
- const chunkEnd = copyOffset + chunkFrames;
34068
- const overlapStart = Math.max(chunkStart, startFrame);
34069
- const overlapEnd = Math.min(chunkEnd, endFrame);
34070
- if (overlapEnd > overlapStart) {
34071
- const sliceStart = overlapStart - chunkStart;
34072
- const sliceEnd = overlapEnd - chunkStart;
34073
- if (sliceStart === 0 && sliceEnd === chunkFrames) {
34074
- await output.write(chunk.samples, sr, bd);
34075
- } else {
34076
- const sliced = [];
34077
- for (let ch = 0; ch < channels; ch++) {
34078
- const source = chunk.samples[ch];
34079
- if (source) sliced.push(source.subarray(sliceStart, sliceEnd));
34080
- else sliced.push(new Float32Array(sliceEnd - sliceStart));
34081
- }
34082
- await output.write(sliced, sr, bd);
34083
- }
34084
- }
34085
- copyOffset = chunkEnd;
34086
- if (copyOffset >= endFrame) break;
34087
- if (chunkFrames < CHUNK_FRAMES3) break;
34088
- }
34089
- await buffer.clear();
34090
- await output.reset();
34091
- for (; ; ) {
34092
- const chunk = await output.read(CHUNK_FRAMES3);
34093
- const chunkFrames = chunk.samples[0]?.length ?? 0;
34094
- if (chunkFrames === 0) break;
34095
- await buffer.write(chunk.samples, sr, bd);
34096
- if (chunkFrames < CHUNK_FRAMES3) break;
34097
- }
34098
- } finally {
34099
- await output.close();
34100
- }
34198
+ this.startFrame = startFrame;
34199
+ this.endFrame = endFrame;
34200
+ }
34201
+ _unbuffer(chunk) {
34202
+ const frames = chunk.samples[0]?.length ?? 0;
34203
+ const chunkStart = chunk.offset;
34204
+ const chunkEnd = chunkStart + frames;
34205
+ const overlapStart = Math.max(chunkStart, this.startFrame);
34206
+ const overlapEnd = Math.min(chunkEnd, this.endFrame);
34207
+ if (overlapEnd <= overlapStart) return void 0;
34208
+ if (overlapStart === chunkStart && overlapEnd === chunkEnd) {
34209
+ return { samples: chunk.samples, offset: chunkStart - this.startFrame, sampleRate: chunk.sampleRate, bitDepth: chunk.bitDepth };
34210
+ }
34211
+ const sliceStart = overlapStart - chunkStart;
34212
+ const sliceEnd = overlapEnd - chunkStart;
34213
+ return {
34214
+ samples: chunk.samples.map((channel) => channel.subarray(sliceStart, sliceEnd)),
34215
+ offset: overlapStart - this.startFrame,
34216
+ sampleRate: chunk.sampleRate,
34217
+ bitDepth: chunk.bitDepth
34218
+ };
34101
34219
  }
34102
34220
  };
34103
34221
  var _TrimNode = class _TrimNode extends TransformNode {
@@ -34115,10 +34233,10 @@ var _TrimNode = class _TrimNode extends TransformNode {
34115
34233
  return new _TrimNode({ ...this.properties, previousProperties: this.properties, ...overrides });
34116
34234
  }
34117
34235
  };
34118
- _TrimNode.moduleName = "Trim";
34236
+ _TrimNode.nodeName = "Trim";
34119
34237
  _TrimNode.packageName = PACKAGE_NAME;
34120
34238
  _TrimNode.packageVersion = PACKAGE_VERSION;
34121
- _TrimNode.moduleDescription = "Remove silence from start and end";
34239
+ _TrimNode.nodeDescription = "Remove silence from start and end";
34122
34240
  _TrimNode.schema = schema13;
34123
34241
  var TrimNode = _TrimNode;
34124
34242
  function trim(options) {
@@ -34160,10 +34278,10 @@ var _DownmixMonoNode = class _DownmixMonoNode extends TransformNode {
34160
34278
  return new _DownmixMonoNode({ ...this.properties, previousProperties: this.properties, ...overrides });
34161
34279
  }
34162
34280
  };
34163
- _DownmixMonoNode.moduleName = "Downmix Mono";
34281
+ _DownmixMonoNode.nodeName = "Downmix Mono";
34164
34282
  _DownmixMonoNode.packageName = PACKAGE_NAME;
34165
34283
  _DownmixMonoNode.packageVersion = PACKAGE_VERSION;
34166
- _DownmixMonoNode.moduleDescription = "Mix all input channels to a single mono channel by averaging";
34284
+ _DownmixMonoNode.nodeDescription = "Mix all input channels to a single mono channel by averaging";
34167
34285
  _DownmixMonoNode.schema = schema14;
34168
34286
  var DownmixMonoNode = _DownmixMonoNode;
34169
34287
  function downmixMono(options) {
@@ -34204,10 +34322,10 @@ var _DuplicateChannelsNode = class _DuplicateChannelsNode extends TransformNode
34204
34322
  return new _DuplicateChannelsNode({ ...this.properties, previousProperties: this.properties, ...overrides });
34205
34323
  }
34206
34324
  };
34207
- _DuplicateChannelsNode.moduleName = "Duplicate Channels";
34325
+ _DuplicateChannelsNode.nodeName = "Duplicate Channels";
34208
34326
  _DuplicateChannelsNode.packageName = PACKAGE_NAME;
34209
34327
  _DuplicateChannelsNode.packageVersion = PACKAGE_VERSION;
34210
- _DuplicateChannelsNode.moduleDescription = "Duplicate a mono signal into multiple identical output channels";
34328
+ _DuplicateChannelsNode.nodeDescription = "Duplicate a mono signal into multiple identical output channels; requires exactly 1 input channel, throws otherwise";
34211
34329
  _DuplicateChannelsNode.schema = schema15;
34212
34330
  var DuplicateChannelsNode = _DuplicateChannelsNode;
34213
34331
  function duplicateChannels(options) {
@@ -34248,10 +34366,10 @@ var _GainNode = class _GainNode extends TransformNode {
34248
34366
  return new _GainNode({ ...this.properties, previousProperties: this.properties, ...overrides });
34249
34367
  }
34250
34368
  };
34251
- _GainNode.moduleName = "Gain";
34369
+ _GainNode.nodeName = "Gain";
34252
34370
  _GainNode.packageName = PACKAGE_NAME;
34253
34371
  _GainNode.packageVersion = PACKAGE_VERSION;
34254
- _GainNode.moduleDescription = "Adjust signal level by a fixed amount in dB";
34372
+ _GainNode.nodeDescription = "Adjust signal level by a fixed amount in dB";
34255
34373
  _GainNode.schema = schema16;
34256
34374
  var GainNode = _GainNode;
34257
34375
  function gain(options) {
@@ -34314,10 +34432,10 @@ var _PanNode = class _PanNode extends TransformNode {
34314
34432
  return new _PanNode({ ...this.properties, previousProperties: this.properties, ...overrides });
34315
34433
  }
34316
34434
  };
34317
- _PanNode.moduleName = "Pan";
34435
+ _PanNode.nodeName = "Pan";
34318
34436
  _PanNode.packageName = PACKAGE_NAME;
34319
34437
  _PanNode.packageVersion = PACKAGE_VERSION;
34320
- _PanNode.moduleDescription = "Position mono signal in stereo field or adjust stereo balance";
34438
+ _PanNode.nodeDescription = "Position mono signal in stereo field or adjust stereo balance; throws for inputs with more than 2 channels";
34321
34439
  _PanNode.schema = schema17;
34322
34440
  var PanNode = _PanNode;
34323
34441
  function pan(options) {
@@ -34434,6 +34552,7 @@ var FfmpegStream = class extends BufferedTransformStream {
34434
34552
  };
34435
34553
  controller.enqueue(audioChunk);
34436
34554
  this.outputOffset += frameCount;
34555
+ this.emitProgress("emit", this.outputOffset);
34437
34556
  this.stdoutStash = merged.subarray(completeBytes);
34438
34557
  }
34439
34558
  async handleChunk(chunk, controller) {
@@ -34464,12 +34583,14 @@ var FfmpegStream = class extends BufferedTransformStream {
34464
34583
  });
34465
34584
  }
34466
34585
  this.framesProcessed += frames;
34467
- this.events.emit("progress", { framesProcessed: this.framesProcessed, sourceTotalFrames: this._sourceTotalFrames });
34586
+ this.emitProgress("buffer", this.framesProcessed, this._sourceTotalFrames);
34468
34587
  }
34469
34588
  async handleFlushStream(controller) {
34470
34589
  const child = this.child;
34471
34590
  if (!child) {
34472
- this.events.emit("finished");
34591
+ this.emitProgress("buffer", this.framesProcessed, this._sourceTotalFrames, { force: true });
34592
+ this.emitProgress("emit", this.outputOffset, void 0, { force: true });
34593
+ this.events.emit("finished", { framesDone: this.framesProcessed });
34473
34594
  return;
34474
34595
  }
34475
34596
  if (this.pendingDrain) {
@@ -34486,7 +34607,9 @@ var FfmpegStream = class extends BufferedTransformStream {
34486
34607
  if (this.stdoutStash.length >= this.inputChannels * 4) {
34487
34608
  this.handleStdoutBytes(Buffer.alloc(0), controller);
34488
34609
  }
34489
- this.events.emit("finished");
34610
+ this.emitProgress("buffer", this.framesProcessed, this._sourceTotalFrames, { force: true });
34611
+ this.emitProgress("emit", this.outputOffset, void 0, { force: true });
34612
+ this.events.emit("finished", { framesDone: this.framesProcessed });
34490
34613
  }
34491
34614
  async _teardown() {
34492
34615
  const child = this.child;
@@ -34516,10 +34639,10 @@ var _FfmpegNode = class _FfmpegNode extends TransformNode {
34516
34639
  return new _FfmpegNode({ ...this.properties, previousProperties: this.properties, ...overrides });
34517
34640
  }
34518
34641
  };
34519
- _FfmpegNode.moduleName = "FFmpeg";
34642
+ _FfmpegNode.nodeName = "FFmpeg";
34520
34643
  _FfmpegNode.packageName = PACKAGE_NAME;
34521
34644
  _FfmpegNode.packageVersion = PACKAGE_VERSION;
34522
- _FfmpegNode.moduleDescription = "Process audio through FFmpeg filters";
34645
+ _FfmpegNode.nodeDescription = "Process audio through FFmpeg filters";
34523
34646
  _FfmpegNode.schema = schema18;
34524
34647
  var FfmpegNode = _FfmpegNode;
34525
34648
  function ffmpeg(options) {
@@ -34581,22 +34704,6 @@ function applyBaseRateChunk(args) {
34581
34704
  return output;
34582
34705
  }
34583
34706
 
34584
- // src/transforms/loudness-target/utils/curve.ts
34585
- function gainDbAt(absXDb, anchors) {
34586
- const { floorDb, pivotDb, limitDb, B: boost, peakGainDb } = anchors;
34587
- if (floorDb !== null && absXDb <= floorDb) return 0;
34588
- if (absXDb < pivotDb) {
34589
- if (floorDb === null) return boost;
34590
- const position = (absXDb - floorDb) / (pivotDb - floorDb);
34591
- return position * boost;
34592
- }
34593
- if (absXDb < limitDb) {
34594
- const position = (absXDb - pivotDb) / (limitDb - pivotDb);
34595
- return boost + position * (peakGainDb - boost);
34596
- }
34597
- return limitDb + peakGainDb - absXDb;
34598
- }
34599
-
34600
34707
  // src/transforms/loudness-target/utils/envelope.ts
34601
34708
  function windowSamplesFromMs(smoothingMs, sampleRate) {
34602
34709
  return Math.max(1, Math.round(smoothingMs * sampleRate / 1e3));
@@ -34615,61 +34722,75 @@ async function applyBackwardPassOverChunkBuffer(args) {
34615
34722
  }
34616
34723
  const sr = sourceBuffer.sampleRate;
34617
34724
  const bd = sourceBuffer.bitDepth;
34618
- const reversedSource = await reverseBuffer(sourceBuffer);
34619
34725
  const filteredReversed = new ChunkBuffer();
34620
- const iirForwardOrder = minHeldBuffer === void 0 ? void 0 : new ChunkBuffer();
34621
34726
  try {
34622
- await reversedSource.reset();
34623
- const seedChunk = await reversedSource.read(1);
34624
- const backwardState = { value: seedChunk.samples[0]?.[0] ?? 0 };
34625
- await reversedSource.reset();
34626
- for (; ; ) {
34627
- const chunk = await reversedSource.read(chunkSize);
34628
- const data = chunk.samples[0];
34629
- const chunkLength = data?.length ?? 0;
34630
- if (data === void 0 || chunkLength === 0) break;
34631
- const filtered = iir.applyForwardPass(data, backwardState);
34632
- await filteredReversed.write([filtered], sr, bd);
34633
- if (chunkLength < chunkSize) break;
34634
- }
34635
- await filteredReversed.flushWrites();
34636
- if (iirForwardOrder === void 0) {
34637
- await reverseBuffer(filteredReversed, destBuffer);
34638
- } else {
34639
- await reverseBuffer(filteredReversed, iirForwardOrder);
34640
- await iirForwardOrder.flushWrites();
34641
- await iirForwardOrder.reset();
34642
- await minHeldBuffer.reset();
34727
+ const backwardState = { value: 0 };
34728
+ let seeded = false;
34729
+ const sourceReader = await sourceBuffer.openReverseReader();
34730
+ try {
34643
34731
  for (; ; ) {
34644
- const iirChunk = await iirForwardOrder.read(chunkSize);
34645
- const iirData = iirChunk.samples[0];
34646
- const chunkLength = iirData?.length ?? 0;
34647
- if (iirData === void 0 || chunkLength === 0) break;
34648
- const minChunk = await minHeldBuffer.read(chunkLength);
34649
- const minData = minChunk.samples[0];
34650
- if (minData?.length !== chunkLength) {
34651
- throw new Error(
34652
- `applyBackwardPassOverChunkBuffer: minHeldBuffer returned ${minData?.length ?? 0} samples; expected ${chunkLength}`
34653
- );
34732
+ const chunk = await sourceReader.read(chunkSize);
34733
+ const reversed = chunk.samples[0];
34734
+ if (reversed === void 0 || reversed.length === 0) break;
34735
+ if (!seeded) {
34736
+ backwardState.value = reversed[0] ?? 0;
34737
+ seeded = true;
34654
34738
  }
34655
- const clamped = new Float32Array(chunkLength);
34656
- for (let sampleIdx = 0; sampleIdx < chunkLength; sampleIdx++) {
34657
- const iirValue = iirData[sampleIdx] ?? 0;
34658
- const minValue = minData[sampleIdx] ?? 0;
34659
- clamped[sampleIdx] = iirValue < minValue ? iirValue : minValue;
34739
+ const filtered = iir.applyForwardPass(reversed, backwardState);
34740
+ await filteredReversed.write([filtered], sr, bd);
34741
+ }
34742
+ } finally {
34743
+ await sourceReader.close();
34744
+ }
34745
+ if (minHeldBuffer !== void 0) await minHeldBuffer.reset();
34746
+ const filteredReader = await filteredReversed.openReverseReader();
34747
+ try {
34748
+ for (; ; ) {
34749
+ const chunk = await filteredReader.read(chunkSize);
34750
+ const forwardOrder = chunk.samples[0];
34751
+ if (forwardOrder === void 0 || forwardOrder.length === 0) break;
34752
+ const stripeFrames = forwardOrder.length;
34753
+ if (minHeldBuffer !== void 0) {
34754
+ const minChunk = await minHeldBuffer.read(stripeFrames);
34755
+ const minData = minChunk.samples[0];
34756
+ if (minData?.length !== stripeFrames) {
34757
+ throw new Error(
34758
+ `applyBackwardPassOverChunkBuffer: minHeldBuffer returned ${minData?.length ?? 0} samples; expected ${stripeFrames}`
34759
+ );
34760
+ }
34761
+ for (let sampleIdx = 0; sampleIdx < stripeFrames; sampleIdx++) {
34762
+ const iirValue = forwardOrder[sampleIdx] ?? 0;
34763
+ const minValue = minData[sampleIdx] ?? 0;
34764
+ forwardOrder[sampleIdx] = iirValue < minValue ? iirValue : minValue;
34765
+ }
34660
34766
  }
34661
- await destBuffer.write([clamped], sr, bd);
34662
- if (chunkLength < chunkSize) break;
34767
+ await destBuffer.write([forwardOrder], sr, bd);
34663
34768
  }
34664
- await destBuffer.flushWrites();
34769
+ } finally {
34770
+ await filteredReader.close();
34665
34771
  }
34772
+ await destBuffer.flushWrites();
34666
34773
  } finally {
34667
- await reversedSource.close();
34668
34774
  await filteredReversed.close();
34669
- if (iirForwardOrder !== void 0) await iirForwardOrder.close();
34670
34775
  }
34671
34776
  }
34672
34777
 
34778
+ // src/transforms/loudness-target/utils/curve.ts
34779
+ function gainDbAt(absXDb, anchors) {
34780
+ const { floorDb, pivotDb, limitDb, B: boost, peakGainDb } = anchors;
34781
+ if (floorDb !== null && absXDb <= floorDb) return 0;
34782
+ if (absXDb < pivotDb) {
34783
+ if (floorDb === null) return boost;
34784
+ const position = (absXDb - floorDb) / (pivotDb - floorDb);
34785
+ return position * boost;
34786
+ }
34787
+ if (absXDb < limitDb) {
34788
+ const position = (absXDb - pivotDb) / (limitDb - pivotDb);
34789
+ return boost + position * (peakGainDb - boost);
34790
+ }
34791
+ return limitDb + peakGainDb - absXDb;
34792
+ }
34793
+
34673
34794
  // src/transforms/loudness-target/utils/source-caches.ts
34674
34795
  async function buildBaseRateDetectionCache(args) {
34675
34796
  const { buffer, sampleRate, channelCount, frames, halfWidth } = args;
@@ -34683,12 +34804,14 @@ async function buildBaseRateDetectionCache(args) {
34683
34804
  upsamplers.push(new TruePeakUpsampler(OVERSAMPLE_FACTOR));
34684
34805
  }
34685
34806
  const slidingWindow = new SlidingWindowMaxStream(halfWidth);
34686
- const detectScratch4x = new Float32Array(CHUNK_FRAMES4 * OVERSAMPLE_FACTOR);
34687
- const detectScratchBase = new Float32Array(CHUNK_FRAMES4);
34807
+ const detectScratch4x = new Float32Array(CHUNK_FRAMES3 * OVERSAMPLE_FACTOR);
34808
+ const detectScratchBase = new Float32Array(CHUNK_FRAMES3);
34809
+ let dbScratch = null;
34810
+ const upsampleScratches = [];
34688
34811
  let consumedBaseFrames = 0;
34689
34812
  await buffer.reset();
34690
34813
  for (; ; ) {
34691
- const chunk = await buffer.read(CHUNK_FRAMES4);
34814
+ const chunk = await buffer.read(CHUNK_FRAMES3);
34692
34815
  const channels = chunk.samples;
34693
34816
  const chunkFrames = channels[0]?.length ?? 0;
34694
34817
  if (chunkFrames === 0) break;
@@ -34702,7 +34825,12 @@ async function buildBaseRateDetectionCache(args) {
34702
34825
  continue;
34703
34826
  }
34704
34827
  const slice = channel.length === chunkFrames ? channel : channel.subarray(0, chunkFrames);
34705
- upChannels.push(upsampler.upsample(slice));
34828
+ let scratch = upsampleScratches[channelIdx];
34829
+ if (scratch === void 0 || scratch.length < chunkFrames * OVERSAMPLE_FACTOR) {
34830
+ scratch = new Float32Array(chunkFrames * OVERSAMPLE_FACTOR);
34831
+ upsampleScratches[channelIdx] = scratch;
34832
+ }
34833
+ upChannels.push(upsampler.upsample(slice, scratch));
34706
34834
  }
34707
34835
  const detect4xChunk = detectScratch4x.subarray(0, upChunkLength);
34708
34836
  for (let upIdx = 0; upIdx < upChunkLength; upIdx++) {
@@ -34729,9 +34857,16 @@ async function buildBaseRateDetectionCache(args) {
34729
34857
  const isFinal = consumedBaseFrames >= frames;
34730
34858
  const pooled = slidingWindow.push(detectBaseChunk, isFinal);
34731
34859
  if (pooled.length > 0) {
34732
- await detectionEnvelope.write([pooled], sampleRate, sourceBitDepth);
34860
+ if (dbScratch === null || dbScratch.length < pooled.length) {
34861
+ dbScratch = new Float32Array(pooled.length);
34862
+ }
34863
+ const dbChunk = dbScratch.subarray(0, pooled.length);
34864
+ for (let sampleIdx = 0; sampleIdx < pooled.length; sampleIdx++) {
34865
+ dbChunk[sampleIdx] = linearToDb(pooled[sampleIdx] ?? 0);
34866
+ }
34867
+ await detectionEnvelope.write([dbChunk], sampleRate, sourceBitDepth);
34733
34868
  }
34734
- if (chunkFrames < CHUNK_FRAMES4) break;
34869
+ if (chunkFrames < CHUNK_FRAMES3) break;
34735
34870
  }
34736
34871
  await detectionEnvelope.flushWrites();
34737
34872
  return detectionEnvelope;
@@ -34739,7 +34874,7 @@ async function buildBaseRateDetectionCache(args) {
34739
34874
 
34740
34875
  // src/transforms/loudness-target/utils/iterate.ts
34741
34876
  var OVERSAMPLE_FACTOR = 4;
34742
- var CHUNK_FRAMES4 = 44100;
34877
+ var CHUNK_FRAMES3 = 44100;
34743
34878
  var BOOST_LOWER_BOUND = -30;
34744
34879
  var BOOST_UPPER_BOUND = 30;
34745
34880
  var LIMIT_EPSILON_DB = 0.01;
@@ -34748,6 +34883,30 @@ var PEAK_GAIN_DB_FLOOR = -60;
34748
34883
  var MIN_SECANT_SLOPE = 0.05;
34749
34884
  var DEFAULT_MAX_ATTEMPTS = 10;
34750
34885
  var DEFAULT_TOLERANCE = 0.5;
34886
+ var GAIN_LUT_MIN_DB = -80;
34887
+ var GAIN_LUT_MAX_DB = 40;
34888
+ var GAIN_LUT_STEP_DB = 0.01;
34889
+ var GAIN_LUT_INV_STEP = 1 / GAIN_LUT_STEP_DB;
34890
+ var GAIN_LUT_SIZE = Math.round((GAIN_LUT_MAX_DB - GAIN_LUT_MIN_DB) / GAIN_LUT_STEP_DB) + 1;
34891
+ var GAIN_LUT_STORED_SILENCE_DB = -200;
34892
+ function buildGainLut() {
34893
+ const lut = new Float64Array(GAIN_LUT_SIZE);
34894
+ for (let entryIdx = 0; entryIdx < GAIN_LUT_SIZE; entryIdx++) {
34895
+ lut[entryIdx] = Math.pow(10, (GAIN_LUT_MIN_DB + entryIdx * GAIN_LUT_STEP_DB) / 20);
34896
+ }
34897
+ return lut;
34898
+ }
34899
+ function gainLutLerp(lut, gainDb) {
34900
+ if (gainDb < GAIN_LUT_MIN_DB || gainDb >= GAIN_LUT_MAX_DB) {
34901
+ return Math.pow(10, gainDb / 20);
34902
+ }
34903
+ const pos = (gainDb - GAIN_LUT_MIN_DB) * GAIN_LUT_INV_STEP;
34904
+ const lutIdx = pos | 0;
34905
+ const frac = pos - lutIdx;
34906
+ const lo = lut[lutIdx] ?? 0;
34907
+ const hi = lut[lutIdx + 1] ?? 0;
34908
+ return lo + (hi - lo) * frac;
34909
+ }
34751
34910
  async function iterateForTargets(args) {
34752
34911
  const {
34753
34912
  buffer,
@@ -34768,13 +34927,18 @@ async function iterateForTargets(args) {
34768
34927
  const channelCount = buffer.channels;
34769
34928
  const frames = buffer.frames;
34770
34929
  if (channelCount === 0 || frames === 0) {
34930
+ if (args.detectionEnvelope !== void 0) await args.detectionEnvelope.close();
34771
34931
  return {
34772
34932
  bestSmoothedEnvelopeBuffer: new ChunkBuffer(),
34773
34933
  bestB: 0,
34774
34934
  bestLimitDb: sourcePeakDb,
34775
34935
  bestPeakGainDb: 0,
34776
34936
  attempts: [],
34777
- converged: false
34937
+ converged: false,
34938
+ detectionCacheBuildMs: 0,
34939
+ winnerOutputLufs: null,
34940
+ winnerOutputTruePeakDb: null,
34941
+ winnerOutputLra: null
34778
34942
  };
34779
34943
  }
34780
34944
  const effectiveTargetTp = targetTp ?? sourcePeakDb;
@@ -34789,13 +34953,15 @@ async function iterateForTargets(args) {
34789
34953
  let currentPeakGainDb = effectiveTargetTp - currentLimit;
34790
34954
  const halfWidth = windowSamplesFromMs(smoothingMs, sampleRate);
34791
34955
  const iir = new BidirectionalIir({ smoothingMs, sampleRate });
34792
- const detectionEnvelope = await buildBaseRateDetectionCache({
34956
+ const tCacheBuild0 = Date.now();
34957
+ const detectionEnvelope = args.detectionEnvelope ?? await buildBaseRateDetectionCache({
34793
34958
  buffer,
34794
34959
  sampleRate,
34795
34960
  channelCount,
34796
34961
  frames,
34797
34962
  halfWidth
34798
34963
  });
34964
+ const detectionCacheBuildMs = args.detectionEnvelope !== void 0 ? 0 : Date.now() - tCacheBuild0;
34799
34965
  const forwardEnvelopeBuffer = new ChunkBuffer();
34800
34966
  const minHeldEnvelopeBuffer = new ChunkBuffer();
34801
34967
  const activeBufferA = new ChunkBuffer();
@@ -34812,8 +34978,14 @@ async function iterateForTargets(args) {
34812
34978
  let bestBoost = currentBoost;
34813
34979
  let bestPeakGainDb = currentPeakGainDb;
34814
34980
  let bestScore = Infinity;
34981
+ let winnerOutputLufs = null;
34982
+ let winnerOutputTruePeakDb = null;
34983
+ let winnerOutputLra = null;
34984
+ let winnerLufsErr = Infinity;
34985
+ let winnerPeakErr = Infinity;
34815
34986
  let previousStepMagnitude = Infinity;
34816
34987
  for (let attemptIdx = 0; attemptIdx < maxAttempts; attemptIdx++) {
34988
+ const tAttempt0 = Date.now();
34817
34989
  const anchors = {
34818
34990
  floorDb: anchorBase.floorDb,
34819
34991
  pivotDb: anchorBase.pivotDb,
@@ -34833,31 +35005,35 @@ async function iterateForTargets(args) {
34833
35005
  sourceBuffer: forwardEnvelopeBuffer,
34834
35006
  destBuffer: activeRef,
34835
35007
  iir,
34836
- chunkSize: CHUNK_FRAMES4,
35008
+ chunkSize: CHUNK_FRAMES3,
34837
35009
  minHeldBuffer: minHeldEnvelopeBuffer
34838
35010
  });
34839
- const measured = await measureAttemptOutput({
34840
- source: buffer,
34841
- sampleRate,
34842
- channelCount,
34843
- gSmoothed: activeRef
34844
- });
35011
+ const measured = await measureAttemptOutput({ source: buffer, sampleRate, channelCount, gSmoothed: activeRef });
34845
35012
  const lufsErr = measured.outputLufs - targetLufs;
34846
35013
  const peakErr = measured.outputTruePeakDb - effectiveTargetTp;
34847
35014
  attempts.push({
34848
35015
  boost: currentBoost,
34849
35016
  limitDb: currentLimit,
34850
35017
  lufsErr,
35018
+ outputLufs: measured.outputLufs,
35019
+ outputTruePeakDb: measured.outputTruePeakDb,
34851
35020
  outputLra: measured.outputLra,
34852
35021
  peakGainDb: currentPeakGainDb,
34853
- peakErr
35022
+ peakErr,
35023
+ elapsedMs: Date.now() - tAttempt0
34854
35024
  });
34855
- const peakScoreTerm = skipPeak ? 0 : peakErr * peakErr;
34856
- const score = Math.sqrt(lufsErr * lufsErr + peakScoreTerm);
35025
+ const lufsScoreTerm = Math.abs(lufsErr) / tolerance;
35026
+ const peakScoreTerm = skipPeak ? 0 : Math.abs(peakErr) / peakTolerance;
35027
+ const score = Math.max(lufsScoreTerm, peakScoreTerm);
34857
35028
  if (score < bestScore) {
34858
35029
  bestScore = score;
34859
35030
  bestBoost = currentBoost;
34860
35031
  bestPeakGainDb = currentPeakGainDb;
35032
+ winnerOutputLufs = measured.outputLufs;
35033
+ winnerOutputTruePeakDb = measured.outputTruePeakDb;
35034
+ winnerOutputLra = measured.outputLra;
35035
+ winnerLufsErr = lufsErr;
35036
+ winnerPeakErr = peakErr;
34861
35037
  const previousActive = activeRef;
34862
35038
  activeRef = winningRef;
34863
35039
  winningRef = previousActive;
@@ -34869,28 +35045,9 @@ async function iterateForTargets(args) {
34869
35045
  await forwardEnvelopeBuffer.clear();
34870
35046
  await minHeldEnvelopeBuffer.clear();
34871
35047
  const matchesToTwoDp = Math.round(Math.abs(lufsErr) * 100) === 0 && (skipPeak || Math.round(Math.abs(peakErr) * 100) === 0);
34872
- if (matchesToTwoDp) {
34873
- return {
34874
- bestSmoothedEnvelopeBuffer: winningRef,
34875
- bestB: bestBoost,
34876
- bestLimitDb: currentLimit,
34877
- bestPeakGainDb,
34878
- attempts,
34879
- converged: true
34880
- };
34881
- }
34882
35048
  const lufsConverged = Math.abs(lufsErr) < tolerance;
34883
35049
  const peakConverged = skipPeak || Math.abs(peakErr) < peakTolerance;
34884
- if (lufsConverged && peakConverged) {
34885
- return {
34886
- bestSmoothedEnvelopeBuffer: winningRef,
34887
- bestB: bestBoost,
34888
- bestLimitDb: currentLimit,
34889
- bestPeakGainDb,
34890
- attempts,
34891
- converged: true
34892
- };
34893
- }
35050
+ if (matchesToTwoDp || lufsConverged && peakConverged) break;
34894
35051
  if (attemptIdx === maxAttempts - 1) break;
34895
35052
  const next = computeBoostStep(attempts, previousStepMagnitude);
34896
35053
  currentBoost = clampBoost(next.boost);
@@ -34902,13 +35059,18 @@ async function iterateForTargets(args) {
34902
35059
  );
34903
35060
  }
34904
35061
  }
35062
+ const converged = winningPopulated && (Math.round(Math.abs(winnerLufsErr) * 100) === 0 && (skipPeak || Math.round(Math.abs(winnerPeakErr) * 100) === 0) || Math.abs(winnerLufsErr) < tolerance && (skipPeak || Math.abs(winnerPeakErr) < peakTolerance));
34905
35063
  return {
34906
35064
  bestSmoothedEnvelopeBuffer: winningRef,
34907
35065
  bestB: bestBoost,
34908
35066
  bestLimitDb: currentLimit,
34909
35067
  bestPeakGainDb,
34910
35068
  attempts,
34911
- converged: false
35069
+ converged,
35070
+ detectionCacheBuildMs,
35071
+ winnerOutputLufs,
35072
+ winnerOutputTruePeakDb,
35073
+ winnerOutputLra
34912
35074
  };
34913
35075
  } finally {
34914
35076
  await detectionEnvelope.close();
@@ -34932,20 +35094,21 @@ async function streamCurveAndForwardIir(args) {
34932
35094
  const forwardState = { value: 0 };
34933
35095
  let forwardSeeded = false;
34934
35096
  const minStream = new SlidingWindowMinStream(halfWidth);
34935
- const gWindowScratch = new Float32Array(CHUNK_FRAMES4);
35097
+ const gWindowScratch = new Float32Array(CHUNK_FRAMES3);
35098
+ const gainLut = buildGainLut();
34936
35099
  const detectionSampleRate = detectionEnvelope.sampleRate;
34937
35100
  const detectionBitDepth = detectionEnvelope.bitDepth;
34938
35101
  let consumedFrames = 0;
34939
35102
  for (; ; ) {
34940
- const chunk = await detectionEnvelope.read(CHUNK_FRAMES4);
35103
+ const chunk = await detectionEnvelope.read(CHUNK_FRAMES3);
34941
35104
  const windowChunk = chunk.samples[0];
34942
35105
  const chunkLength = windowChunk?.length ?? 0;
34943
35106
  if (windowChunk === void 0 || chunkLength === 0) break;
34944
35107
  const gWindowChunk = gWindowScratch.subarray(0, chunkLength);
34945
35108
  for (let outputIdx = 0; outputIdx < chunkLength; outputIdx++) {
34946
- const levelDb = linearToDb(windowChunk[outputIdx] ?? 0);
35109
+ const levelDb = windowChunk[outputIdx] ?? GAIN_LUT_STORED_SILENCE_DB;
34947
35110
  const gainDb = gainDbAt(levelDb, anchors);
34948
- gWindowChunk[outputIdx] = Math.pow(10, gainDb / 20);
35111
+ gWindowChunk[outputIdx] = gainLutLerp(gainLut, gainDb);
34949
35112
  }
34950
35113
  consumedFrames += chunkLength;
34951
35114
  const isFinal = consumedFrames >= totalFrames;
@@ -34959,7 +35122,7 @@ async function streamCurveAndForwardIir(args) {
34959
35122
  await forwardEnvelopeBuffer.write([forwardChunk], detectionSampleRate, detectionBitDepth);
34960
35123
  await minHeldEnvelopeBuffer.write([minHeldChunk], detectionSampleRate, detectionBitDepth);
34961
35124
  }
34962
- if (chunkLength < CHUNK_FRAMES4) break;
35125
+ if (chunkLength < CHUNK_FRAMES3) break;
34963
35126
  }
34964
35127
  await forwardEnvelopeBuffer.flushWrites();
34965
35128
  await minHeldEnvelopeBuffer.flushWrites();
@@ -34970,12 +35133,12 @@ async function measureAttemptOutput(args) {
34970
35133
  const truePeakAccumulator = new TruePeakAccumulator(sampleRate, channelCount);
34971
35134
  const applyOutputScratch = [];
34972
35135
  for (let channelIdx = 0; channelIdx < channelCount; channelIdx++) {
34973
- applyOutputScratch.push(new Float32Array(CHUNK_FRAMES4));
35136
+ applyOutputScratch.push(new Float32Array(CHUNK_FRAMES3));
34974
35137
  }
34975
35138
  await source.reset();
34976
35139
  await gSmoothed.reset();
34977
35140
  for (; ; ) {
34978
- const sourceChunk = await source.read(CHUNK_FRAMES4);
35141
+ const sourceChunk = await source.read(CHUNK_FRAMES3);
34979
35142
  const chunkFrames = sourceChunk.samples[0]?.length ?? 0;
34980
35143
  if (chunkFrames === 0) break;
34981
35144
  const envelopeChunk = await gSmoothed.read(chunkFrames);
@@ -34995,7 +35158,7 @@ async function measureAttemptOutput(args) {
34995
35158
  });
34996
35159
  accumulator.push(transformed, chunkFrames);
34997
35160
  truePeakAccumulator.push(transformed, chunkFrames);
34998
- if (chunkFrames < CHUNK_FRAMES4) break;
35161
+ if (chunkFrames < CHUNK_FRAMES3) break;
34999
35162
  }
35000
35163
  const result = accumulator.finalize();
35001
35164
  const truePeakLinear = truePeakAccumulator.finalize();
@@ -35042,60 +35205,70 @@ function clampLimit(limitDb, pivotDb, sourcePeakDb) {
35042
35205
  }
35043
35206
 
35044
35207
  // src/transforms/loudness-target/utils/measurement.ts
35045
- var CHUNK_FRAMES5 = 44100;
35208
+ var CHUNK_FRAMES4 = 44100;
35046
35209
  var HISTOGRAM_BUCKETS = 1024;
35047
35210
  var PIVOT_FALLBACK_DB = -40;
35048
- async function measureSource(buffer, sampleRate, limitPercentile, halfWidth) {
35049
- const frames = buffer.frames;
35050
- const channelCount = buffer.channels;
35051
- if (frames === 0 || channelCount === 0) {
35052
- return {
35053
- integratedLufs: -Infinity,
35054
- lra: 0,
35055
- truePeakDb: -Infinity,
35056
- pivotAutoDb: Number.POSITIVE_INFINITY,
35057
- floorAutoDb: Number.POSITIVE_INFINITY,
35058
- limitAutoDb: Number.POSITIVE_INFINITY,
35059
- shortTermLufs: [],
35060
- detectionHistogram: { buckets: new Uint32Array(0), bucketMax: 0, totalSamples: 0 }
35061
- };
35062
- }
35063
- const loudness = new LoudnessAccumulator(sampleRate, channelCount);
35064
- const truePeak = new TruePeakAccumulator(sampleRate, channelCount);
35065
- const detectionHistogram = new AmplitudeHistogramAccumulator(HISTOGRAM_BUCKETS);
35066
- const upsamplers = [];
35067
- for (let channelIdx = 0; channelIdx < channelCount; channelIdx++) {
35068
- upsamplers.push(new TruePeakUpsampler(OVERSAMPLE_FACTOR));
35069
- }
35070
- const slidingWindow = new SlidingWindowMaxStream(halfWidth);
35071
- let levelsScratch = null;
35072
- let baseScratch = null;
35073
- await buffer.reset();
35074
- for (; ; ) {
35075
- const chunk = await buffer.read(CHUNK_FRAMES5);
35076
- const chunkFrames = chunk.samples[0]?.length ?? 0;
35077
- if (chunkFrames === 0) break;
35078
- loudness.push(chunk.samples, chunkFrames);
35079
- truePeak.push(chunk.samples, chunkFrames);
35211
+ function emptyMeasurement() {
35212
+ return {
35213
+ integratedLufs: -Infinity,
35214
+ lra: 0,
35215
+ truePeakDb: -Infinity,
35216
+ pivotAutoDb: Number.POSITIVE_INFINITY,
35217
+ floorAutoDb: Number.POSITIVE_INFINITY,
35218
+ limitAutoDb: Number.POSITIVE_INFINITY,
35219
+ shortTermLufs: [],
35220
+ detectionHistogram: { buckets: new Uint32Array(0), bucketMax: 0, totalSamples: 0 }
35221
+ };
35222
+ }
35223
+ var SourceMeasurementAccumulator = class {
35224
+ constructor(sampleRate, channelCount, limitPercentile, halfWidth, detectionEnvelope = null, persistBitDepth) {
35225
+ this.levelsScratch = null;
35226
+ this.baseScratch = null;
35227
+ this.dbScratch = null;
35228
+ // see toDbScratch
35229
+ this.upsampleScratches = [];
35230
+ this.pushedFrames = 0;
35231
+ this.limitPercentile = limitPercentile;
35232
+ this.sampleRate = sampleRate;
35233
+ this.loudness = new LoudnessAccumulator(sampleRate, channelCount);
35234
+ this.truePeak = new TruePeakAccumulator(sampleRate, channelCount);
35235
+ this.detectionHistogram = new AmplitudeHistogramAccumulator(HISTOGRAM_BUCKETS);
35236
+ this.upsamplers = [];
35237
+ for (let channelIdx = 0; channelIdx < channelCount; channelIdx++) {
35238
+ this.upsamplers.push(new TruePeakUpsampler(OVERSAMPLE_FACTOR));
35239
+ }
35240
+ this.slidingWindow = new SlidingWindowMaxStream(halfWidth);
35241
+ this.detectionEnvelope = detectionEnvelope;
35242
+ this.persistBitDepth = persistBitDepth;
35243
+ }
35244
+ async push(channels, frames) {
35245
+ if (frames === 0) return;
35246
+ this.loudness.push(channels, frames);
35247
+ this.truePeak.push(channels, frames);
35080
35248
  const upChannels = [];
35081
- for (let channelIdx = 0; channelIdx < chunk.samples.length; channelIdx++) {
35082
- const channel = chunk.samples[channelIdx];
35083
- const upsampler = upsamplers[channelIdx];
35249
+ for (let channelIdx = 0; channelIdx < channels.length; channelIdx++) {
35250
+ const channel = channels[channelIdx];
35251
+ const upsampler = this.upsamplers[channelIdx];
35084
35252
  if (channel === void 0 || upsampler === void 0) {
35085
- upChannels.push(new Float32Array(chunkFrames * OVERSAMPLE_FACTOR));
35253
+ upChannels.push(new Float32Array(frames * OVERSAMPLE_FACTOR));
35086
35254
  continue;
35087
35255
  }
35088
- const slice = channel.length === chunkFrames ? channel : channel.subarray(0, chunkFrames);
35089
- upChannels.push(upsampler.upsample(slice));
35256
+ const slice = channel.length === frames ? channel : channel.subarray(0, frames);
35257
+ let scratch = this.upsampleScratches[channelIdx];
35258
+ if (scratch === void 0 || scratch.length < frames * OVERSAMPLE_FACTOR) {
35259
+ scratch = new Float32Array(frames * OVERSAMPLE_FACTOR);
35260
+ this.upsampleScratches[channelIdx] = scratch;
35261
+ }
35262
+ upChannels.push(upsampler.upsample(slice, scratch));
35090
35263
  }
35091
- const upChunkLength = chunkFrames * OVERSAMPLE_FACTOR;
35092
- if (levelsScratch === null || levelsScratch.length < upChunkLength) {
35093
- levelsScratch = new Float32Array(upChunkLength);
35264
+ const upChunkLength = frames * OVERSAMPLE_FACTOR;
35265
+ if (this.levelsScratch === null || this.levelsScratch.length < upChunkLength) {
35266
+ this.levelsScratch = new Float32Array(upChunkLength);
35094
35267
  }
35095
- if (baseScratch === null || baseScratch.length < chunkFrames) {
35096
- baseScratch = new Float32Array(chunkFrames);
35268
+ if (this.baseScratch === null || this.baseScratch.length < frames) {
35269
+ this.baseScratch = new Float32Array(frames);
35097
35270
  }
35098
- const levels = levelsScratch;
35271
+ const levels = this.levelsScratch;
35099
35272
  for (let upIdx = 0; upIdx < upChunkLength; upIdx++) {
35100
35273
  let max = 0;
35101
35274
  for (let channelIdx = 0; channelIdx < upChannels.length; channelIdx++) {
@@ -35105,8 +35278,8 @@ async function measureSource(buffer, sampleRate, limitPercentile, halfWidth) {
35105
35278
  }
35106
35279
  levels[upIdx] = max;
35107
35280
  }
35108
- const baseChunk = baseScratch.subarray(0, chunkFrames);
35109
- for (let baseIdx = 0; baseIdx < chunkFrames; baseIdx++) {
35281
+ const baseChunk = this.baseScratch.subarray(0, frames);
35282
+ for (let baseIdx = 0; baseIdx < frames; baseIdx++) {
35110
35283
  const upOffset = baseIdx * OVERSAMPLE_FACTOR;
35111
35284
  const s0 = levels[upOffset] ?? 0;
35112
35285
  const s1 = levels[upOffset + 1] ?? 0;
@@ -35116,43 +35289,79 @@ async function measureSource(buffer, sampleRate, limitPercentile, halfWidth) {
35116
35289
  const m23 = s2 > s3 ? s2 : s3;
35117
35290
  baseChunk[baseIdx] = m01 > m23 ? m01 : m23;
35118
35291
  }
35119
- const isLastChunk = chunkFrames < CHUNK_FRAMES5;
35120
- const pooled = slidingWindow.push(baseChunk, isLastChunk);
35292
+ this.pushedFrames += frames;
35293
+ const pooled = this.slidingWindow.push(baseChunk, false);
35121
35294
  if (pooled.length > 0) {
35122
- detectionHistogram.push([pooled], pooled.length);
35295
+ this.detectionHistogram.push([pooled], pooled.length);
35296
+ if (this.detectionEnvelope !== null) {
35297
+ await this.detectionEnvelope.write([this.toDbScratch(pooled)], this.sampleRate, this.persistBitDepth);
35298
+ }
35123
35299
  }
35124
- if (isLastChunk) break;
35125
35300
  }
35126
- const loudnessResult = loudness.finalize();
35127
- const truePeakLin = truePeak.finalize();
35128
- const histogramResult = detectionHistogram.finalize();
35129
- const stats = getLraConsideredStats(loudnessResult.shortTerm);
35130
- const limitAutoDb = computeLimitAutoDb(histogramResult.buckets, histogramResult.bucketMax, stats.median, limitPercentile);
35131
- let totalSamples = 0;
35132
- for (let bucketIdx = 0; bucketIdx < histogramResult.buckets.length; bucketIdx++) {
35133
- totalSamples += histogramResult.buckets[bucketIdx] ?? 0;
35301
+ // LINEAR pooled slider output -> dB, into a reused scratch. The histogram keeps the linear axis; only the
35302
+ // detection-envelope buffer stores dB (never converts `pooled`/`trailing` in place — an axis mixup silently
35303
+ // corrupts limitAutoDb / the predictor).
35304
+ toDbScratch(linear) {
35305
+ if (this.dbScratch === null || this.dbScratch.length < linear.length) {
35306
+ this.dbScratch = new Float32Array(linear.length);
35307
+ }
35308
+ const out = this.dbScratch.subarray(0, linear.length);
35309
+ for (let sampleIdx = 0; sampleIdx < linear.length; sampleIdx++) {
35310
+ out[sampleIdx] = linearToDb(linear[sampleIdx] ?? 0);
35311
+ }
35312
+ return out;
35134
35313
  }
35135
- return {
35136
- integratedLufs: loudnessResult.integrated,
35137
- lra: loudnessResult.range,
35138
- truePeakDb: linearToDb(truePeakLin),
35139
- pivotAutoDb: stats.median,
35140
- floorAutoDb: stats.min,
35141
- limitAutoDb,
35142
- shortTermLufs: loudnessResult.shortTerm,
35143
- // Pooled base-rate detection histogram — same axis the apply
35144
- // path's `buildBaseRateDetectionCache` produces, feeding the
35145
- // curve's per-sample `gainDbAt`. `limitAutoDb`'s percentile
35146
- // walk and `predictOutputLufs` both consume this histogram
35147
- // (per the 2026-05-13 histogram-axis fix; the prior raw-4× axis
35148
- // under-estimated typical curve-input levels and produced an
35149
- // over-aggressive `limitAutoDb`).
35150
- detectionHistogram: {
35151
- buckets: histogramResult.buckets,
35152
- bucketMax: histogramResult.bucketMax,
35153
- totalSamples
35314
+ async finalize() {
35315
+ if (this.pushedFrames === 0) return emptyMeasurement();
35316
+ const trailing = this.slidingWindow.push(new Float32Array(0), true);
35317
+ if (trailing.length > 0) {
35318
+ this.detectionHistogram.push([trailing], trailing.length);
35319
+ if (this.detectionEnvelope !== null) {
35320
+ await this.detectionEnvelope.write([this.toDbScratch(trailing)], this.sampleRate, this.persistBitDepth);
35321
+ }
35154
35322
  }
35155
- };
35323
+ if (this.detectionEnvelope !== null) {
35324
+ await this.detectionEnvelope.flushWrites();
35325
+ }
35326
+ const loudnessResult = this.loudness.finalize();
35327
+ const truePeakLin = this.truePeak.finalize();
35328
+ const histogramResult = this.detectionHistogram.finalize();
35329
+ const stats = getLraConsideredStats(loudnessResult.shortTerm);
35330
+ const limitAutoDb = computeLimitAutoDb(histogramResult.buckets, histogramResult.bucketMax, stats.median, this.limitPercentile);
35331
+ let totalSamples = 0;
35332
+ for (let bucketIdx = 0; bucketIdx < histogramResult.buckets.length; bucketIdx++) {
35333
+ totalSamples += histogramResult.buckets[bucketIdx] ?? 0;
35334
+ }
35335
+ return {
35336
+ integratedLufs: loudnessResult.integrated,
35337
+ lra: loudnessResult.range,
35338
+ truePeakDb: linearToDb(truePeakLin),
35339
+ pivotAutoDb: stats.median,
35340
+ floorAutoDb: stats.min,
35341
+ limitAutoDb,
35342
+ shortTermLufs: loudnessResult.shortTerm,
35343
+ detectionHistogram: {
35344
+ buckets: histogramResult.buckets,
35345
+ bucketMax: histogramResult.bucketMax,
35346
+ totalSamples
35347
+ }
35348
+ };
35349
+ }
35350
+ };
35351
+ async function measureSource(buffer, sampleRate, limitPercentile, halfWidth) {
35352
+ const frames = buffer.frames;
35353
+ const channelCount = buffer.channels;
35354
+ if (frames === 0 || channelCount === 0) return emptyMeasurement();
35355
+ const accumulator = new SourceMeasurementAccumulator(sampleRate, channelCount, limitPercentile, halfWidth);
35356
+ await buffer.reset();
35357
+ for (; ; ) {
35358
+ const chunk = await buffer.read(CHUNK_FRAMES4);
35359
+ const chunkFrames = chunk.samples[0]?.length ?? 0;
35360
+ if (chunkFrames === 0) break;
35361
+ await accumulator.push(chunk.samples, chunkFrames);
35362
+ if (chunkFrames < CHUNK_FRAMES4) break;
35363
+ }
35364
+ return await accumulator.finalize();
35156
35365
  }
35157
35366
  function computeLimitAutoDb(buckets, bucketMax, pivotAutoDb, limitPercentile) {
35158
35367
  if (bucketMax === 0) return Number.POSITIVE_INFINITY;
@@ -35287,84 +35496,63 @@ var schema19 = external_exports.object({
35287
35496
  var LoudnessTargetStream = class extends BufferedTransformStream {
35288
35497
  constructor() {
35289
35498
  super(...arguments);
35290
- /**
35291
- * BASE-rate peak-respecting smoothed gain envelope produced by the
35292
- * winning iteration attempt, held as a disk-backed single-channel
35293
- * `ChunkBuffer` of size `frames` (post the 2026-05-13 base-rate-
35294
- * downstream rewrite; no `× OVERSAMPLE_FACTOR` factor). The
35295
- * envelope is read chunk-by-chunk in `_unbuffer` rather than held
35296
- * as a flat `Float32Array` — keeps RAM at ~10 MB regardless of
35297
- * source length. `null` when the stream passes through (silent /
35298
- * sub-block-length source).
35299
- */
35300
35499
  this.winningSmoothedEnvelopeBuffer = null;
35301
- /**
35302
- * Body gain `B` from the winning iteration attempt. `null` when the
35303
- * stream passes through (no curve was learned). Diagnostic only —
35304
- * the apply pass uses the materialised envelope, not `B` directly.
35305
- */
35306
35500
  this.winningB = null;
35307
- /**
35308
- * Limit anchor `limitDb` used for this source — constant across
35309
- * iteration attempts. Sourced from `limitDbOverride` when explicit,
35310
- * else from the percentile-derived `limitAutoDb` on the source's
35311
- * detection-envelope histogram, else `sourcePeakDb` (no limiting).
35312
- * `null` when the stream passes through. Diagnostic only.
35313
- */
35314
35501
  this.winningLimitDb = null;
35315
- /**
35316
- * `peakGainDb` from the winning attempt — the upper-segment right-
35317
- * endpoint anchor gain. Starts at the closed-form
35318
- * `effectiveTargetTp − limitDb` and adjusts via proportional
35319
- * feedback on observed signed TP error. `null` when the stream
35320
- * passes through.
35321
- */
35322
35502
  this.winningPeakGainDb = null;
35323
- /**
35324
- * Set to `true` by the first `_unbuffer` call so the
35325
- * `winningSmoothedEnvelopeBuffer`'s read cursor is rewound exactly
35326
- * once. The envelope is read-only in `_unbuffer` and consumed
35327
- * forward in chunk-cadence lockstep with upstream chunks; this
35328
- * lazy-reset pattern keeps the cursor management out of `_setup`.
35329
- * Post the 2026-05-13 base-rate-downstream rewrite the
35330
- * upsampled-source cache no longer exists — `_unbuffer` reads
35331
- * source samples directly from the framework-provided chunk.
35332
- */
35333
35503
  this.unbufferCursorsReady = false;
35334
- /**
35335
- * Per-chunk wall-clock time spent in `_unbuffer`. Post the
35336
- * 2026-05-13 base-rate-downstream rewrite, `_unbuffer` is a pure
35337
- * base-rate multiply per chunk (no upsample, no downsample, no
35338
- * AA filter — the smoothed envelope is bandlimited far below
35339
- * base-rate Nyquist, so the multiply introduces no high-frequency
35340
- * content). Accumulated across all `_unbuffer` calls.
35341
- */
35504
+ this.capturedDetectionEnvelope = null;
35342
35505
  this.unbufferElapsedMs = 0;
35343
- /**
35344
- * Wall-clock breakdown of the learn pass. `iteration` is the wall
35345
- * time spent inside `iterateForTargets`. `detection` stays at 0 for
35346
- * QA-driver log parity — detection-envelope build cost is folded
35347
- * into `iteration`.
35348
- */
35349
35506
  this.learnTimingMs = {
35350
35507
  sourceMeasurement: 0,
35351
35508
  detection: 0,
35352
35509
  iteration: 0
35353
35510
  };
35354
35511
  }
35512
+ async _buffer(chunk, buffer) {
35513
+ await super._buffer(chunk, buffer);
35514
+ const frames = chunk.samples[0]?.length ?? 0;
35515
+ const channelCount = chunk.samples.length;
35516
+ if (frames === 0 || channelCount === 0) return;
35517
+ const tPush0 = Date.now();
35518
+ if (this.measurementAccumulator === void 0) {
35519
+ this.capturedDetectionEnvelope = new ChunkBuffer();
35520
+ this.measurementAccumulator = new SourceMeasurementAccumulator(
35521
+ chunk.sampleRate,
35522
+ channelCount,
35523
+ this.properties.limitPercentile,
35524
+ windowSamplesFromMs(this.properties.smoothing, chunk.sampleRate),
35525
+ this.capturedDetectionEnvelope,
35526
+ chunk.bitDepth
35527
+ );
35528
+ }
35529
+ await this.measurementAccumulator.push(chunk.samples, frames);
35530
+ this.learnTimingMs.sourceMeasurement += Date.now() - tPush0;
35531
+ }
35355
35532
  async _process(buffer) {
35356
35533
  const frames = buffer.frames;
35357
35534
  const channelCount = buffer.channels;
35358
35535
  const sampleRate = buffer.sampleRate ?? this.sampleRate ?? 44100;
35359
35536
  if (frames === 0 || channelCount === 0) return;
35360
35537
  const { targetLufs, targetTp, limitDb: limitDbOverride, limitPercentile, smoothing, tolerance, peakTolerance, maxAttempts } = this.properties;
35361
- const measurementHalfWidth = windowSamplesFromMs(smoothing, sampleRate);
35362
35538
  const tMeasure0 = Date.now();
35363
- const measurement = await measureSource(buffer, sampleRate, limitPercentile, measurementHalfWidth);
35364
- this.learnTimingMs.sourceMeasurement = Date.now() - tMeasure0;
35539
+ const measurement = this.measurementAccumulator !== void 0 ? await this.measurementAccumulator.finalize() : await measureSource(buffer, sampleRate, limitPercentile, windowSamplesFromMs(smoothing, sampleRate));
35540
+ this.learnTimingMs.sourceMeasurement += Date.now() - tMeasure0;
35541
+ const capturedDetectionEnvelope = this.capturedDetectionEnvelope;
35542
+ this.capturedDetectionEnvelope = null;
35543
+ const detectionEnvelope = capturedDetectionEnvelope !== null && capturedDetectionEnvelope.frames === frames ? capturedDetectionEnvelope : void 0;
35544
+ if (detectionEnvelope === void 0 && capturedDetectionEnvelope !== null) {
35545
+ this.log(
35546
+ "captured detection envelope frames mismatch; rebuilding at barrier",
35547
+ { capturedFrames: capturedDetectionEnvelope.frames, bufferFrames: frames },
35548
+ "warn"
35549
+ );
35550
+ await capturedDetectionEnvelope.close();
35551
+ }
35365
35552
  const { integratedLufs: sourceLufs, lra: sourceLra, truePeakDb: sourcePeakDb } = measurement;
35366
35553
  if (!Number.isFinite(sourceLufs)) {
35367
- console.log(`[loudness-target] source has no measurable loudness (LUFS=${String(sourceLufs)}); pass-through.`);
35554
+ if (detectionEnvelope !== void 0) await detectionEnvelope.close();
35555
+ this.log("source has no measurable loudness; pass-through", { sourceLufs });
35368
35556
  return;
35369
35557
  }
35370
35558
  const userPivot = this.properties.pivot;
@@ -35373,13 +35561,13 @@ var LoudnessTargetStream = class extends BufferedTransformStream {
35373
35561
  effectivePivotDb = userPivot;
35374
35562
  } else if (Number.isFinite(measurement.pivotAutoDb)) {
35375
35563
  effectivePivotDb = measurement.pivotAutoDb;
35376
- console.log(
35377
- `[loudness-target] pivot auto-derived to ${effectivePivotDb.toFixed(2)} dBFS (median(considered LRA blocks))`
35378
- );
35564
+ this.log("pivot auto-derived (median(considered LRA blocks))", { pivotDb: effectivePivotDb });
35379
35565
  } else {
35380
35566
  effectivePivotDb = -40;
35381
- console.warn(
35382
- `[loudness-target] pivot auto-derivation produced no considered LRA blocks; falling back to ${effectivePivotDb} dBFS. Supply 'pivot' explicitly for tighter control on short or near-silent sources.`
35567
+ this.log(
35568
+ "pivot auto-derivation produced no considered LRA blocks; falling back. Supply 'pivot' explicitly for tighter control on short or near-silent sources.",
35569
+ { fallbackPivotDb: effectivePivotDb },
35570
+ "warn"
35383
35571
  );
35384
35572
  }
35385
35573
  const userFloor = this.properties.floor;
@@ -35388,16 +35576,15 @@ var LoudnessTargetStream = class extends BufferedTransformStream {
35388
35576
  effectiveFloorDb = userFloor;
35389
35577
  } else if (Number.isFinite(measurement.floorAutoDb)) {
35390
35578
  effectiveFloorDb = measurement.floorAutoDb;
35391
- console.log(
35392
- `[loudness-target] floor auto-derived to ${effectiveFloorDb.toFixed(2)} dBFS (min(considered LRA blocks))`
35393
- );
35579
+ this.log("floor auto-derived (min(considered LRA blocks))", { floorDb: effectiveFloorDb });
35394
35580
  } else {
35395
35581
  effectiveFloorDb = null;
35396
35582
  }
35397
35583
  if (effectiveFloorDb !== null && effectiveFloorDb >= effectivePivotDb) {
35398
35584
  const clampedFloorDb = effectivePivotDb - FLOOR_PIVOT_EPSILON_DB;
35399
- console.log(
35400
- `[loudness-target] floor (${effectiveFloorDb.toFixed(2)} dBFS) >= pivot (${effectivePivotDb.toFixed(2)} dBFS); clamping floor to ${clampedFloorDb.toFixed(3)} dBFS (pivot - ${FLOOR_PIVOT_EPSILON_DB} dB).`
35585
+ this.log(
35586
+ "floor >= pivot; clamping floor to (pivot - epsilon)",
35587
+ { floorDb: effectiveFloorDb, pivotDb: effectivePivotDb, clampedFloorDb, epsilonDb: FLOOR_PIVOT_EPSILON_DB }
35401
35588
  );
35402
35589
  effectiveFloorDb = clampedFloorDb;
35403
35590
  }
@@ -35436,19 +35623,20 @@ var LoudnessTargetStream = class extends BufferedTransformStream {
35436
35623
  maxAttempts,
35437
35624
  tolerance,
35438
35625
  peakTolerance,
35439
- seedB
35626
+ seedB,
35627
+ detectionEnvelope
35440
35628
  });
35441
35629
  this.learnTimingMs.iteration = Date.now() - tIterate0;
35630
+ this.learnTimingMs.detection = result.detectionCacheBuildMs;
35442
35631
  this.winningSmoothedEnvelopeBuffer = result.bestSmoothedEnvelopeBuffer;
35443
35632
  this.winningB = result.bestB;
35444
35633
  this.winningLimitDb = result.bestLimitDb;
35445
35634
  this.winningPeakGainDb = result.bestPeakGainDb;
35446
- const lastAttempt = result.attempts[result.attempts.length - 1];
35447
- const outputLufsRepr = lastAttempt ? (targetLufs + lastAttempt.lufsErr).toFixed(2) : "n/a";
35448
- const outputLraRepr = lastAttempt ? lastAttempt.outputLra.toFixed(2) : "n/a";
35449
- const lufsDeltaRepr = lastAttempt ? lastAttempt.lufsErr.toFixed(2) : "n/a";
35450
- const outputTruePeakRepr = lastAttempt ? (effectiveTargetTp + lastAttempt.peakErr).toFixed(2) : "n/a";
35451
- const peakDeltaRepr = lastAttempt ? lastAttempt.peakErr.toFixed(2) : "n/a";
35635
+ const outputLufsRepr = result.winnerOutputLufs !== null ? result.winnerOutputLufs.toFixed(2) : "n/a";
35636
+ const outputLraRepr = result.winnerOutputLra !== null ? result.winnerOutputLra.toFixed(2) : "n/a";
35637
+ const lufsDeltaRepr = result.winnerOutputLufs !== null ? (result.winnerOutputLufs - targetLufs).toFixed(2) : "n/a";
35638
+ const outputTruePeakRepr = result.winnerOutputTruePeakDb !== null ? result.winnerOutputTruePeakDb.toFixed(2) : "n/a";
35639
+ const peakDeltaRepr = result.winnerOutputTruePeakDb !== null ? (result.winnerOutputTruePeakDb - effectiveTargetTp).toFixed(2) : "n/a";
35452
35640
  const bestPeakGainDbRepr = result.bestPeakGainDb.toFixed(4);
35453
35641
  const bestLimitDbRepr = result.bestLimitDb.toFixed(4);
35454
35642
  const pivotRepr = userPivot === void 0 ? `${effectivePivotDb.toFixed(2)} (auto)` : String(userPivot);
@@ -35463,21 +35651,53 @@ var LoudnessTargetStream = class extends BufferedTransformStream {
35463
35651
  }
35464
35652
  const limitDbRepr = `${bestLimitDbRepr} (${limitDbSource})`;
35465
35653
  const expansiveGeometry = result.bestPeakGainDb > result.bestB;
35466
- const expansionSuffix = expansiveGeometry ? " EXPANSIVE_UPPER_SEGMENT" : "";
35467
35654
  for (let attemptIdx = 0; attemptIdx < result.attempts.length; attemptIdx++) {
35468
35655
  const attempt = result.attempts[attemptIdx];
35469
35656
  if (attempt === void 0) continue;
35470
- console.log(
35471
- `[loudness-target] attempt ${(attemptIdx + 1).toString().padStart(2)}: B=${attempt.boost.toFixed(4).padStart(9)} peakGainDb=${attempt.peakGainDb.toFixed(4).padStart(9)} lufsErr=${attempt.lufsErr.toFixed(4).padStart(8)} peakErr=${attempt.peakErr.toFixed(4).padStart(8)} outputLra=${attempt.outputLra.toFixed(4).padStart(7)}`
35472
- );
35657
+ this.log("attempt", {
35658
+ attempt: attemptIdx + 1,
35659
+ B: attempt.boost,
35660
+ peakGainDb: attempt.peakGainDb,
35661
+ lufsErr: attempt.lufsErr,
35662
+ peakErr: attempt.peakErr,
35663
+ outputLra: attempt.outputLra,
35664
+ elapsedMs: attempt.elapsedMs
35665
+ });
35666
+ this.progress(attemptIdx + 1, maxAttempts);
35473
35667
  }
35474
35668
  const fmt = (x) => x === void 0 ? "off" : String(x);
35475
- console.log(
35476
- `[loudness-target] iteration: attempts=${result.attempts.length} converged=${String(result.converged)} seedB=${seedB.toFixed(4)} bestB=${result.bestB.toFixed(4)} bestLimitDb=${bestLimitDbRepr} bestPeakGainDb=${bestPeakGainDbRepr} outputLufs=${outputLufsRepr} (\u0394${lufsDeltaRepr}) outputLra=${outputLraRepr} outputTruePeakDb=${outputTruePeakRepr} (\u0394${peakDeltaRepr}) targetLufs=${targetLufs.toFixed(2)} targetTp=${targetTp === void 0 ? "source" : String(targetTp)} limitDb=${limitDbRepr} limitPercentile=${limitPercentile} sourceLufs=${sourceLufs.toFixed(2)} sourcePeakDb=${sourcePeakDb.toFixed(2)} sourceLra=${sourceLra.toFixed(2)} pivot=${pivotRepr} floor=${floorRepr} smoothing=${smoothing} tolerance=${fmt(tolerance)} peakTolerance=${fmt(peakTolerance)} maxAttempts=${fmt(maxAttempts)}` + expansionSuffix
35477
- );
35669
+ this.log("iteration", {
35670
+ attempts: result.attempts.length,
35671
+ converged: result.converged,
35672
+ seedB,
35673
+ bestB: result.bestB,
35674
+ bestLimitDb: bestLimitDbRepr,
35675
+ bestPeakGainDb: bestPeakGainDbRepr,
35676
+ outputLufs: outputLufsRepr,
35677
+ lufsDelta: lufsDeltaRepr,
35678
+ outputLra: outputLraRepr,
35679
+ outputTruePeakDb: outputTruePeakRepr,
35680
+ peakDelta: peakDeltaRepr,
35681
+ targetLufs,
35682
+ targetTp: targetTp === void 0 ? "source" : String(targetTp),
35683
+ limitDb: limitDbRepr,
35684
+ limitPercentile,
35685
+ sourceLufs,
35686
+ sourcePeakDb,
35687
+ sourceLra,
35688
+ pivot: pivotRepr,
35689
+ floor: floorRepr,
35690
+ smoothing,
35691
+ tolerance: fmt(tolerance),
35692
+ peakTolerance: fmt(peakTolerance),
35693
+ maxAttempts: fmt(maxAttempts),
35694
+ expansiveUpperSegment: expansiveGeometry
35695
+ });
35478
35696
  if (expansiveGeometry) {
35479
- console.warn(
35480
- `[loudness-target] peakGainDb (${bestPeakGainDbRepr}) > B (${result.bestB.toFixed(4)}); upper segment of curve is expansive between pivot and limit. Brick-wall above limit still caps output at targetTp \u2014 note for listening QA.`
35697
+ this.log(
35698
+ "peakGainDb > B; upper segment of curve is expansive between pivot and limit. Brick-wall above limit still caps output at targetTp \u2014 note for listening QA.",
35699
+ { peakGainDb: bestPeakGainDbRepr, B: result.bestB },
35700
+ "warn"
35481
35701
  );
35482
35702
  }
35483
35703
  }
@@ -35487,14 +35707,25 @@ var LoudnessTargetStream = class extends BufferedTransformStream {
35487
35707
  const bRepr = this.winningB === null ? "n/a" : this.winningB.toFixed(4);
35488
35708
  const limitDbRepr = this.winningLimitDb === null ? "n/a" : this.winningLimitDb.toFixed(4);
35489
35709
  const peakGainDbRepr = this.winningPeakGainDb === null ? "n/a" : this.winningPeakGainDb.toFixed(4);
35490
- console.log(
35491
- `[loudness-target timing] sourceMeasurement=${this.learnTimingMs.sourceMeasurement}ms detection=${this.learnTimingMs.detection}ms iteration=${this.learnTimingMs.iteration}ms unbufferApply=${this.unbufferElapsedMs}ms total=${total}ms winningB=${bRepr} winningLimitDb=${limitDbRepr} winningPeakGainDb=${peakGainDbRepr}`
35492
- );
35710
+ this.log("timing", {
35711
+ sourceMeasurementMs: this.learnTimingMs.sourceMeasurement,
35712
+ detectionMs: this.learnTimingMs.detection,
35713
+ iterationMs: this.learnTimingMs.iteration,
35714
+ unbufferApplyMs: this.unbufferElapsedMs,
35715
+ totalMs: total,
35716
+ winningB: bRepr,
35717
+ winningLimitDb: limitDbRepr,
35718
+ winningPeakGainDb: peakGainDbRepr
35719
+ });
35493
35720
  }
35494
35721
  if (this.winningSmoothedEnvelopeBuffer !== null) {
35495
35722
  await this.winningSmoothedEnvelopeBuffer.close();
35496
35723
  this.winningSmoothedEnvelopeBuffer = null;
35497
35724
  }
35725
+ if (this.capturedDetectionEnvelope !== null) {
35726
+ await this.capturedDetectionEnvelope.close();
35727
+ this.capturedDetectionEnvelope = null;
35728
+ }
35498
35729
  }
35499
35730
  async _unbuffer(chunk) {
35500
35731
  const envelopeBuffer = this.winningSmoothedEnvelopeBuffer;
@@ -35541,10 +35772,10 @@ var _LoudnessTargetNode = class _LoudnessTargetNode extends TransformNode {
35541
35772
  return new _LoudnessTargetNode({ ...this.properties, previousProperties: this.properties, ...overrides });
35542
35773
  }
35543
35774
  };
35544
- _LoudnessTargetNode.moduleName = "Loudness Target";
35775
+ _LoudnessTargetNode.nodeName = "Loudness Target";
35545
35776
  _LoudnessTargetNode.packageName = PACKAGE_NAME;
35546
35777
  _LoudnessTargetNode.packageVersion = PACKAGE_VERSION;
35547
- _LoudnessTargetNode.moduleDescription = "Peak-aware content-adaptive curve fitting (LUFS, true-peak, LRA) via a single combined gain envelope with a peak-respecting two-stage smoother. The upper-arm peak anchor jointly iterates with the body gain to land both LUFS and true-peak targets in one envelope.";
35778
+ _LoudnessTargetNode.nodeDescription = "Peak-aware content-adaptive curve fitting (LUFS, true-peak, LRA) via a single combined gain envelope with a peak-respecting two-stage smoother. The upper-arm peak anchor jointly iterates with the body gain to land both LUFS and true-peak targets in one envelope.";
35548
35779
  _LoudnessTargetNode.schema = schema19;
35549
35780
  var LoudnessTargetNode = _LoudnessTargetNode;
35550
35781
  function loudnessTarget(options) {
@@ -35552,24 +35783,6 @@ function loudnessTarget(options) {
35552
35783
  return new LoudnessTargetNode({ ...parsed, id: options.id });
35553
35784
  }
35554
35785
 
35555
- // src/transforms/loudness-normalize/utils/measurement.ts
35556
- var CHUNK_FRAMES6 = 44100;
35557
- async function measureIntegratedLufs(buffer, sampleRate) {
35558
- const frames = buffer.frames;
35559
- const channelCount = buffer.channels;
35560
- if (frames === 0 || channelCount === 0) return -Infinity;
35561
- const accumulator = new IntegratedLufsAccumulator(sampleRate, channelCount);
35562
- await buffer.reset();
35563
- for (; ; ) {
35564
- const chunk = await buffer.read(CHUNK_FRAMES6);
35565
- const chunkFrames = chunk.samples[0]?.length ?? 0;
35566
- if (chunkFrames === 0) break;
35567
- accumulator.push(chunk.samples, chunkFrames);
35568
- if (chunkFrames < CHUNK_FRAMES6) break;
35569
- }
35570
- return accumulator.finalize();
35571
- }
35572
-
35573
35786
  // src/transforms/loudness-normalize/index.ts
35574
35787
  var schema20 = external_exports.object({
35575
35788
  target: external_exports.number().min(-50).max(0).multipleOf(0.1).default(-16).describe("Target integrated loudness (LUFS)")
@@ -35579,9 +35792,16 @@ var LoudnessNormalizeStream = class extends BufferedTransformStream {
35579
35792
  super(...arguments);
35580
35793
  this.gain = 1;
35581
35794
  }
35582
- async _process(buffer) {
35583
- const sampleRate = buffer.sampleRate ?? this.sampleRate ?? 44100;
35584
- const integrated = await measureIntegratedLufs(buffer, sampleRate);
35795
+ async _buffer(chunk, buffer) {
35796
+ await super._buffer(chunk, buffer);
35797
+ const frames = chunk.samples[0]?.length ?? 0;
35798
+ const channelCount = chunk.samples.length;
35799
+ if (frames === 0 || channelCount === 0) return;
35800
+ this.accumulator ?? (this.accumulator = new IntegratedLufsAccumulator(chunk.sampleRate, channelCount));
35801
+ this.accumulator.push(chunk.samples, frames);
35802
+ }
35803
+ _process(_buffer) {
35804
+ const integrated = this.accumulator === void 0 ? -Infinity : this.accumulator.finalize();
35585
35805
  if (!Number.isFinite(integrated)) {
35586
35806
  this.gain = 1;
35587
35807
  return;
@@ -35616,10 +35836,10 @@ var _LoudnessNormalizeNode = class _LoudnessNormalizeNode extends TransformNode
35616
35836
  return new _LoudnessNormalizeNode({ ...this.properties, previousProperties: this.properties, ...overrides });
35617
35837
  }
35618
35838
  };
35619
- _LoudnessNormalizeNode.moduleName = "Loudness Normalize";
35839
+ _LoudnessNormalizeNode.nodeName = "Loudness Normalize";
35620
35840
  _LoudnessNormalizeNode.packageName = PACKAGE_NAME;
35621
35841
  _LoudnessNormalizeNode.packageVersion = PACKAGE_VERSION;
35622
- _LoudnessNormalizeNode.moduleDescription = "Measure integrated loudness (BS.1770) and apply a single linear gain to hit a target LUFS \u2014 no limiting, no dynamics";
35842
+ _LoudnessNormalizeNode.nodeDescription = "Measure integrated loudness (BS.1770) and apply a single linear gain to hit a target LUFS \u2014 no limiting, no dynamics";
35623
35843
  _LoudnessNormalizeNode.schema = schema20;
35624
35844
  var LoudnessNormalizeNode = _LoudnessNormalizeNode;
35625
35845
  function loudnessNormalize(options) {
@@ -35628,7 +35848,6 @@ function loudnessNormalize(options) {
35628
35848
  }
35629
35849
 
35630
35850
  // src/transforms/true-peak-normalize/index.ts
35631
- var CHUNK_FRAMES7 = 44100;
35632
35851
  var schema21 = external_exports.object({
35633
35852
  target: external_exports.number().lt(0).default(-1).describe("Target true peak (dBTP). Must be < 0.")
35634
35853
  });
@@ -35637,32 +35856,28 @@ var TruePeakNormalizeStream = class extends BufferedTransformStream {
35637
35856
  super(...arguments);
35638
35857
  this.gain = 1;
35639
35858
  }
35640
- async _process(buffer) {
35641
- const sampleRate = buffer.sampleRate ?? this.sampleRate ?? 44100;
35642
- const channelCount = buffer.channels;
35643
- const frames = buffer.frames;
35644
- if (frames === 0 || channelCount === 0) {
35859
+ async _buffer(chunk, buffer) {
35860
+ await super._buffer(chunk, buffer);
35861
+ const frames = chunk.samples[0]?.length ?? 0;
35862
+ const channelCount = chunk.samples.length;
35863
+ if (frames === 0 || channelCount === 0) return;
35864
+ this.accumulator ?? (this.accumulator = new TruePeakAccumulator(chunk.sampleRate, channelCount));
35865
+ this.accumulator.push(chunk.samples, frames);
35866
+ }
35867
+ _process(_buffer) {
35868
+ if (this.accumulator === void 0) {
35645
35869
  this.gain = 1;
35646
35870
  return;
35647
35871
  }
35648
- const accumulator = new TruePeakAccumulator(sampleRate, channelCount);
35649
- await buffer.reset();
35650
- for (; ; ) {
35651
- const chunk = await buffer.read(CHUNK_FRAMES7);
35652
- const chunkFrames = chunk.samples[0]?.length ?? 0;
35653
- if (chunkFrames === 0) break;
35654
- accumulator.push(chunk.samples, chunkFrames);
35655
- if (chunkFrames < CHUNK_FRAMES7) break;
35656
- }
35657
- const sourcePeakLinear = accumulator.finalize();
35872
+ const sourcePeakLinear = this.accumulator.finalize();
35658
35873
  if (sourcePeakLinear <= 0) {
35659
35874
  this.gain = 1;
35660
- console.log(`[true-peak-normalize] sourceTP=-Infinity target=${this.properties.target} gain=1`);
35875
+ this.log("true peak measured", { sourceTpDb: -Infinity, targetDb: this.properties.target, gain: 1 });
35661
35876
  return;
35662
35877
  }
35663
35878
  const sourcePeakDb = 20 * Math.log10(sourcePeakLinear);
35664
35879
  this.gain = Math.pow(10, (this.properties.target - sourcePeakDb) / 20);
35665
- console.log(`[true-peak-normalize] sourceTP=${sourcePeakDb} target=${this.properties.target} gain=${this.gain}`);
35880
+ this.log("true peak measured", { sourceTpDb: sourcePeakDb, targetDb: this.properties.target, gain: this.gain });
35666
35881
  }
35667
35882
  _unbuffer(chunk) {
35668
35883
  const gain2 = this.gain;
@@ -35692,10 +35907,10 @@ var _TruePeakNormalizeNode = class _TruePeakNormalizeNode extends TransformNode
35692
35907
  return new _TruePeakNormalizeNode({ ...this.properties, previousProperties: this.properties, ...overrides });
35693
35908
  }
35694
35909
  };
35695
- _TruePeakNormalizeNode.moduleName = "True Peak Normalize";
35910
+ _TruePeakNormalizeNode.nodeName = "True Peak Normalize";
35696
35911
  _TruePeakNormalizeNode.packageName = PACKAGE_NAME;
35697
35912
  _TruePeakNormalizeNode.packageVersion = PACKAGE_VERSION;
35698
- _TruePeakNormalizeNode.moduleDescription = "Measure source true peak (4\xD7 upsampled, BS.1770-4 style) and apply a single linear gain to hit a target dBTP";
35913
+ _TruePeakNormalizeNode.nodeDescription = "Measure source true peak (4\xD7 upsampled, BS.1770-4 style) and apply a single linear gain to hit a target dBTP";
35699
35914
  _TruePeakNormalizeNode.schema = schema21;
35700
35915
  var TruePeakNormalizeNode = _TruePeakNormalizeNode;
35701
35916
  function truePeakNormalize(options) {
@@ -35745,8 +35960,8 @@ function betaForBand(_bandIndex) {
35745
35960
  }
35746
35961
  function schroederTargetToDelay(magnitude, amount = 1) {
35747
35962
  const binCount = magnitude.length;
35748
- const delay = new Float32Array(binCount);
35749
- if (binCount < 2) return delay;
35963
+ const delay2 = new Float32Array(binCount);
35964
+ if (binCount < 2) return delay2;
35750
35965
  const phase2 = schroederTargetPhase(magnitude);
35751
35966
  const dOmega = Math.PI / (binCount - 1);
35752
35967
  const scale = Math.max(0, Math.min(1, amount));
@@ -35756,9 +35971,9 @@ function schroederTargetToDelay(magnitude, amount = 1) {
35756
35971
  const dPhi = (phase2[hi] ?? 0) - (phase2[lo] ?? 0);
35757
35972
  const span = (hi - lo) * dOmega;
35758
35973
  const tau = span > 0 ? -dPhi / span : 0;
35759
- delay[bin] = scale * Math.max(0, tau);
35974
+ delay2[bin] = scale * Math.max(0, tau);
35760
35975
  }
35761
- return delay;
35976
+ return delay2;
35762
35977
  }
35763
35978
  function poleRadius(halfWidth, beta) {
35764
35979
  const delta = Math.max(0, halfWidth);
@@ -35773,14 +35988,14 @@ function poleRadius(halfWidth, beta) {
35773
35988
  if (!Number.isFinite(rho)) return 0;
35774
35989
  return Math.max(0, Math.min(MAX_POLE_RADIUS, rho));
35775
35990
  }
35776
- function designDispersionAllpass(delay, order) {
35777
- const binCount = delay.length;
35991
+ function designDispersionAllpass(delay2, order) {
35992
+ const binCount = delay2.length;
35778
35993
  const poles = [];
35779
35994
  if (binCount < 2 || order <= 0) return { denominator: Float32Array.from([1]), poles };
35780
35995
  const dOmega = Math.PI / (binCount - 1);
35781
35996
  let rawHalfArea = 0;
35782
35997
  for (let bin = 0; bin < binCount - 1; bin++) {
35783
- rawHalfArea += 0.5 * ((delay[bin] ?? 0) + (delay[bin + 1] ?? 0)) * dOmega;
35998
+ rawHalfArea += 0.5 * ((delay2[bin] ?? 0) + (delay2[bin + 1] ?? 0)) * dOmega;
35784
35999
  }
35785
36000
  let naturalOrder = Math.round(rawHalfArea / Math.PI);
35786
36001
  if (naturalOrder < 0) naturalOrder = 0;
@@ -35792,9 +36007,9 @@ function designDispersionAllpass(delay, order) {
35792
36007
  }
35793
36008
  if (rawHalfArea > bandOrder * Math.PI && rawHalfArea > 0) {
35794
36009
  const areaScale = bandOrder * Math.PI / rawHalfArea;
35795
- for (let bin = 0; bin < binCount; bin++) scaled[bin] = areaScale * (delay[bin] ?? 0);
36010
+ for (let bin = 0; bin < binCount; bin++) scaled[bin] = areaScale * (delay2[bin] ?? 0);
35796
36011
  } else {
35797
- for (let bin = 0; bin < binCount; bin++) scaled[bin] = delay[bin] ?? 0;
36012
+ for (let bin = 0; bin < binCount; bin++) scaled[bin] = delay2[bin] ?? 0;
35798
36013
  constantDelay = Math.max(0, (bandOrder * Math.PI - rawHalfArea) / Math.PI);
35799
36014
  }
35800
36015
  const omegaAt = (bin) => bin * dOmega;
@@ -35968,9 +36183,6 @@ function searchBindingPeak(channelWindows, reflectionRow, order, lambda, targetP
35968
36183
  if (identityPower <= targetPeakPower || lambda <= 0) {
35969
36184
  return {
35970
36185
  scale: 0,
35971
- // One objective evaluation was performed (the identity / c=0
35972
- // `truePeakPower4x` above) — `iterations` is the deterministic
35973
- // evaluation count, so the skip path is 1, not 0.
35974
36186
  iterations: 1,
35975
36187
  committedPeakPower: identityPower,
35976
36188
  identityPeakPower: identityPower,
@@ -36125,42 +36337,41 @@ function stftFrameCount(signalLength, frameSize, hopSize) {
36125
36337
  if (signalLength < frameSize || hopSize <= 0) return 0;
36126
36338
  return Math.floor((signalLength - frameSize) / hopSize) + 1;
36127
36339
  }
36128
- async function measureBufferTruePeakWithArgmax(buffer, _sampleRate) {
36129
- const channelCount = buffer.channels;
36130
- const totalFrames = buffer.frames;
36131
- if (channelCount === 0 || totalFrames === 0) return { db: linearToDb(0), peakInputSample: 0 };
36132
- await buffer.reset();
36133
- const upsamplers = [];
36134
- for (let channel = 0; channel < channelCount; channel++) upsamplers.push(new TruePeakUpsampler(4));
36135
- let runningMax = 0;
36136
- let peakInputSample = 0;
36137
- let inputBase = 0;
36138
- let toRead = totalFrames;
36139
- while (toRead > 0) {
36140
- const want = Math.min(WALK_CHUNK_FRAMES, toRead);
36141
- const chunk = await buffer.read(want);
36142
- const got = chunk.samples[0]?.length ?? 0;
36143
- if (got === 0) break;
36144
- for (let channel = 0; channel < channelCount; channel++) {
36145
- const samples = chunk.samples[channel];
36146
- const upsampler = upsamplers[channel];
36340
+ var TruePeakArgmaxAccumulator = class {
36341
+ constructor(channelCount, _sampleRate) {
36342
+ this.channelCount = channelCount;
36343
+ this.runningMax = 0;
36344
+ this.peakInputSample = 0;
36345
+ this.inputBase = 0;
36346
+ this.upsamplers = [];
36347
+ for (let channel = 0; channel < channelCount; channel++) this.upsamplers.push(new TruePeakUpsampler(4));
36348
+ }
36349
+ push(channels, frames) {
36350
+ if (frames <= 0) {
36351
+ this.inputBase += frames > 0 ? frames : 0;
36352
+ return;
36353
+ }
36354
+ for (let channel = 0; channel < this.channelCount; channel++) {
36355
+ const samples = channels[channel];
36356
+ const upsampler = this.upsamplers[channel];
36147
36357
  if (samples === void 0 || upsampler === void 0) continue;
36148
- const slice = samples.length === got ? samples : samples.subarray(0, got);
36358
+ const slice = samples.length === frames ? samples : samples.subarray(0, frames);
36149
36359
  const upsampled = upsampler.upsample(slice);
36150
36360
  for (let index = 0; index < upsampled.length; index++) {
36151
36361
  const value = upsampled[index] ?? 0;
36152
36362
  const magnitude = value < 0 ? -value : value;
36153
- if (magnitude > runningMax) {
36154
- runningMax = magnitude;
36155
- peakInputSample = inputBase + Math.floor(index / 4);
36363
+ if (magnitude > this.runningMax) {
36364
+ this.runningMax = magnitude;
36365
+ this.peakInputSample = this.inputBase + Math.floor(index / 4);
36156
36366
  }
36157
36367
  }
36158
36368
  }
36159
- inputBase += got;
36160
- toRead -= got;
36369
+ this.inputBase += frames;
36161
36370
  }
36162
- return { db: linearToDb(runningMax), peakInputSample };
36163
- }
36371
+ finalize() {
36372
+ return { db: linearToDb(this.runningMax), peakInputSample: this.peakInputSample };
36373
+ }
36374
+ };
36164
36375
  async function streamLatticeTrajectory(buffer, frameSize, hopSize, backend, addonOptions, search) {
36165
36376
  const channelCount = buffer.channels;
36166
36377
  const signalLength = buffer.frames;
@@ -36243,8 +36454,8 @@ async function streamLatticeTrajectory(buffer, frameSize, hopSize, backend, addo
36243
36454
  }
36244
36455
  windowPeaks[nextFrame] = { peakMagnitude: windowPeak, headroom: amount };
36245
36456
  peakSampleIndex[nextFrame] = start + peakPos;
36246
- const delay = schroederTargetToDelay(sumMagnitude, amount);
36247
- const { denominator } = designDispersionAllpass(delay, order);
36457
+ const delay2 = schroederTargetToDelay(sumMagnitude, amount);
36458
+ const { denominator } = designDispersionAllpass(delay2, order);
36248
36459
  const reflection = stepDownToReflection(denominator);
36249
36460
  const row = new Float32Array(order);
36250
36461
  for (let section = 0; section < order; section++) row[section] = reflection[section] ?? 0;
@@ -36277,40 +36488,40 @@ async function streamLatticeTrajectory(buffer, frameSize, hopSize, backend, addo
36277
36488
  }
36278
36489
  return { trajectory, frameCount, signalLength, windowPeaks, bindingMask };
36279
36490
  }
36280
- async function streamLatticeApply(buffer, smoothedTrajectory, scale, order, hopSize, onChunk) {
36281
- const channelCount = buffer.channels;
36282
- const signalLength = buffer.frames;
36283
- if (channelCount === 0 || signalLength === 0) return;
36284
- const rows = smoothedTrajectory.rows;
36285
- const frameCount = rows.length;
36286
- const state = Array.from({ length: channelCount }, () => new Float32Array(order));
36287
- await buffer.reset();
36288
- let sample = 0;
36289
- let toRead = signalLength;
36290
- const MAX_REFLECTION2 = 0.95;
36291
- while (toRead > 0) {
36292
- const want = Math.min(WALK_CHUNK_FRAMES, toRead);
36293
- const chunk = await buffer.read(want);
36294
- const got = chunk.samples[0]?.length ?? 0;
36295
- if (got === 0) break;
36296
- const out = Array.from({ length: channelCount }, () => new Float32Array(got));
36297
- for (let index = 0; index < got; index++) {
36298
- const framePos = hopSize > 0 ? sample / hopSize : 0;
36491
+ var _LatticeApplyState = class _LatticeApplyState {
36492
+ constructor(trajectory, order, hopSize, channelCount) {
36493
+ this.trajectory = trajectory;
36494
+ this.order = order;
36495
+ this.hopSize = hopSize;
36496
+ this.sample = 0;
36497
+ this.rows = trajectory.rows;
36498
+ this.frameCount = this.rows.length;
36499
+ this.state = Array.from({ length: channelCount }, () => new Float32Array(order));
36500
+ }
36501
+ apply(channels, frames) {
36502
+ const channelCount = this.state.length;
36503
+ const order = this.order;
36504
+ const hopSize = this.hopSize;
36505
+ const frameCount = this.frameCount;
36506
+ const rows = this.rows;
36507
+ const out = Array.from({ length: channelCount }, () => new Float32Array(frames));
36508
+ for (let index = 0; index < frames; index++) {
36509
+ const framePos = hopSize > 0 ? this.sample / hopSize : 0;
36299
36510
  const frame0 = Math.min(frameCount - 1, Math.max(0, Math.floor(framePos)));
36300
36511
  const frame1 = Math.min(frameCount - 1, frame0 + 1);
36301
36512
  const fraction = framePos - frame0;
36302
- const row0 = rows[frame0] ?? smoothedTrajectory.identity;
36303
- const row1 = rows[frame1] ?? smoothedTrajectory.identity;
36513
+ const row0 = rows[frame0] ?? this.trajectory.identity;
36514
+ const row1 = rows[frame1] ?? this.trajectory.identity;
36304
36515
  for (let ch = 0; ch < channelCount; ch++) {
36305
- const inputValue = chunk.samples[ch]?.[index] ?? 0;
36516
+ const inputValue = channels[ch]?.[index] ?? 0;
36306
36517
  const outChannel = out[ch];
36307
- const chState = state[ch] ?? new Float32Array(order);
36518
+ const chState = this.state[ch] ?? new Float32Array(order);
36308
36519
  let signalValue = inputValue;
36309
36520
  for (let section = 0; section < order; section++) {
36310
36521
  const interpolated = (row0[section] ?? 0) + fraction * ((row1[section] ?? 0) - (row0[section] ?? 0));
36311
- let kCoeff = scale * interpolated;
36312
- if (kCoeff > MAX_REFLECTION2) kCoeff = MAX_REFLECTION2;
36313
- else if (kCoeff < -MAX_REFLECTION2) kCoeff = -MAX_REFLECTION2;
36522
+ let kCoeff = _LatticeApplyState.SCALE * interpolated;
36523
+ if (kCoeff > _LatticeApplyState.MAX_REFLECTION) kCoeff = _LatticeApplyState.MAX_REFLECTION;
36524
+ else if (kCoeff < -_LatticeApplyState.MAX_REFLECTION) kCoeff = -_LatticeApplyState.MAX_REFLECTION;
36314
36525
  const cCoeff = Math.sqrt(Math.max(0, 1 - kCoeff * kCoeff));
36315
36526
  const delayed = chState[section] ?? 0;
36316
36527
  const toDelay = cCoeff * signalValue + kCoeff * delayed;
@@ -36320,12 +36531,15 @@ async function streamLatticeApply(buffer, smoothedTrajectory, scale, order, hopS
36320
36531
  }
36321
36532
  if (outChannel) outChannel[index] = signalValue;
36322
36533
  }
36323
- sample += 1;
36534
+ this.sample += 1;
36324
36535
  }
36325
- await onChunk(out, got);
36326
- toRead -= got;
36536
+ return out;
36327
36537
  }
36328
- }
36538
+ };
36539
+ // Mirrors lattice.ts MAX_REFLECTION (module-private there); both sides must clamp identically.
36540
+ _LatticeApplyState.MAX_REFLECTION = 0.95;
36541
+ _LatticeApplyState.SCALE = 1;
36542
+ var LatticeApplyState = _LatticeApplyState;
36329
36543
 
36330
36544
  // src/transforms/crest-reduce/index.ts
36331
36545
  function isPowerOfTwo(value) {
@@ -36342,28 +36556,8 @@ var schema22 = external_exports.object({
36342
36556
  var CrestReduceStream = class extends BufferedTransformStream {
36343
36557
  constructor() {
36344
36558
  super(...arguments);
36345
- /**
36346
- * Whole-file transformed output as a node-owned, disk-backed
36347
- * `ChunkBuffer` (the `loudnessTarget` `winningSmoothedEnvelopeBuffer`
36348
- * precedent — NOT a stream-resident full-length `Float32Array`, which
36349
- * was the design-streaming.md materialization anti-pattern Phase 2
36350
- * removes). `_process` writes the produced output here at flush;
36351
- * `_unbuffer` serves it chunk-by-chunk. `null` when the stream passes
36352
- * through (no audio or a sub-frame input — there is no `strength`
36353
- * bypass after the 2026-05-17 keystone; the node always runs the
36354
- * gate/search/lattice path), in which case `_unbuffer` emits the
36355
- * input verbatim (the length-preserving passthrough fallback).
36356
- */
36357
- this.outputBuffer = null;
36358
- /**
36359
- * Set `true` by the first `_unbuffer` call so `outputBuffer`'s read
36360
- * cursor is rewound exactly once (it is at end-of-buffer after
36361
- * `_process`'s streaming production write). Rewound lazily in
36362
- * `_unbuffer` — after `_process` finished — not eagerly, so a stable
36363
- * cursor state exists (the `loudnessTarget` `unbufferCursorsReady`
36364
- * precedent).
36365
- */
36366
- this.unbufferCursorReady = false;
36559
+ this.applyOrder = LATTICE_ORDER;
36560
+ this.applyHopSize = 0;
36367
36561
  }
36368
36562
  async _setup(input, context) {
36369
36563
  const fft2 = initFftBackend(context.executionProviders, this.properties);
@@ -36374,98 +36568,13 @@ var CrestReduceStream = class extends BufferedTransformStream {
36374
36568
  get hopSize() {
36375
36569
  return this.properties.frameSize / 4;
36376
36570
  }
36377
- /**
36378
- * Whole-file processing at flush (the `loudnessTarget` accumulate-then-
36379
- * process precedent `bufferSize === WHOLE_FILE`, so the framework hands
36380
- * the ENTIRE accumulated signal in one `_process` call). The SINGLE
36381
- * realization (2026-05-16 FUNDAMENTAL REFRAME the normalized
36382
- * Gray–Markel lossless-lattice phase rotator; `spectral` and the
36383
- * `realization` parameter removed):
36384
- *
36385
- * The 2026-05-17 KEYSTONE rework — SINGLE-PASS, per-binding-peak
36386
- * Item-7 search over a TP-DRIVEN gate and a TP-DRIVEN commit
36387
- * objective; NO `strength` (the node always applies the optimal
36388
- * value); NO whole-signal never-worsen veto (the only never-worsen
36389
- * is the per-window commit-only-if-better intrinsic to the Item-7
36390
- * search):
36391
- *
36392
- * 1. WHOLE-SIGNAL 4× true peak + its input-sample index measured
36393
- * ONCE — a SEQUENTIAL CHUNKED WALK driving per-channel BS.1770-4
36394
- * 4× `TruePeakUpsampler`s directly
36395
- * (`measureBufferTruePeakWithArgmax`; 12-tap history carries
36396
- * across `upsample` so chunk boundaries are invisible). The dBTP
36397
- * is the gate's proximity reference; `peakInputSample`
36398
- * force-binds the analysis frame containing the file's global
36399
- * 4× true peak.
36400
- * 2. EXTRACT + TP-DRIVEN GATE + ITEM-7 SEARCH, one streaming
36401
- * sliding-window pass — `streamLatticeTrajectory(buffer, …,
36402
- * { globalTruePeakDb, peakInputSample, sampleRate, lambda })`: a
36403
- * `frameSize` ring of the linked-stereo sum + ONE per channel,
36404
- * computed on the fly (NO whole-signal array). Per hop the
36405
- * grounded Abel & Smith (Item 9) + RMV §III step-down (Item 8)
36406
- * reflection row is fitted VERBATIM; the frame is classified by
36407
- * the TP-DRIVEN gate predicate (`isBindingPeak` —
36408
- * `peakPriorityAmount` headroom AND ( the window's OWN
36409
- * per-channel 4× true peak within BINDING_DELTA_DB of the global
36410
- * 4× true peak, SAME true-peak domain — OR the global-4×-TP
36411
- * frame, force-bound ); `binding.ts` UNFROZEN); an ACTIVE-band
36412
- * peak frame runs the §Algorithm Specification Item-7 search
36413
- * (`searchBindingPeak`, Hong/Kim/Har 2011 §3; `search.ts`
36414
- * UNFROZEN — its COMMIT OBJECTIVE is the cross-channel 4×
36415
- * true-peak power, the Eq. 7–9 φ′ raw-sample recurrence kept as
36416
- * the candidate-proposal direction only) over the PER-CHANNEL
36417
- * windows with the FULL group-delay-ceiling λ — its committed
36418
- * scale (commit-only-if-better on the 4× TP power; identity
36419
- * `c=0` is the guaranteed floor) is the per-active-peak OPTIMAL
36420
- * decorrelation value; a NON-active frame's envelope value is 0.
36421
- * There is NO `strength` (λ is ALWAYS the full ceiling,
36422
- * `groupDelayLambda`). The O(frames) trajectory IS the
36423
- * decorrelation envelope (the ONLY resident product besides
36424
- * bounded scratch).
36425
- * 2b. COMBINE the envelope (the 2026-05-17 PER-PEAK-EXACT model):
36426
- * `smoothControlTrajectory` = `max` of a per-peak EXACT-OPTIMAL
36427
- * flat hold centered on the PEAK SAMPLE `n_i` (half-width
36428
- * `exactHoldHalfWidthFrames(sampleRate, hopSize)` — the
36429
- * group-delay span, NEVER `smoothing`) that pins the EXACT
36430
- * Item-7 optimal at every binding peak so whole-signal
36431
- * true-peak reduction is `smoothing`-INVARIANT, and a
36432
- * `smoothing`-driven bidirectional zero-phase SPILL
36433
- * (transient-asymmetry pullback pre-pass THEN forward+backward
36434
- * `BidirectionalIir` on the trajectory's FRAME-RATE axis,
36435
- * τ = (ms/1000)·√2 internal to `BidirectionalIir`) whose ONLY
36436
- * effect is decorrelation spill into the gated gaps + easing
36437
- * between active values. HARD RULE (design §Rejected
36438
- * Approaches): both passes are on the CONTROL trajectory ONLY —
36439
- * NEVER the audio.
36440
- * 3. ONE streaming PRODUCTION pass over the committed
36441
- * per-peak-exact + bidirectionally-SMOOTHED decorrelation
36442
- * envelope. `streamLatticeApply` is the byte-faithful
36443
- * `processLatticeChannel` transcription (lattice.ts BYTE-FROZEN)
36444
- * and is called with the literal `1` for its internal
36445
- * transcribed scalar (an identity no-op at the call site — an
36446
- * internal arg of the verbatim transcription, NOT a public
36447
- * surface; there is no `strength`). There is NO whole-signal
36448
- * never-worsen veto (removed — the per-window
36449
- * commit-only-if-better in the Item-7 search is the only
36450
- * never-worsen) and NO per-sample binding gate / edge crossfade:
36451
- * a non-active segment is the lattice at eased-to-zero
36452
- * coefficients (a crest-invariant pure delay). Each output chunk
36453
- * is written straight into the node-owned output `ChunkBuffer`
36454
- * (served chunk-by-chunk by `_unbuffer`). The streamed lattice
36455
- * output is BIT-IDENTICAL to processing one contiguous array.
36456
- *
36457
- * NO whole-signal `Float32Array` exists anywhere — the
36458
- * design-streaming.md materialization anti-pattern is GENUINELY
36459
- * eliminated (not relocated). Resident state: the O(frames) trajectory
36460
- * + mask + bounded ring (sum + per-channel) / accumulator / chunk
36461
- * scratch.
36462
- *
36463
- * There is NO node-level bypass (the `strength` parameter and its
36464
- * `strength <= 0` early-return were removed by the 2026-05-17
36465
- * keystone). The only passthrough is the length-preserving fallback
36466
- * for no audio / a sub-frame input (no analysis frame fits) —
36467
- * `_unbuffer` then emits the input verbatim.
36468
- */
36571
+ async _buffer(chunk, buffer) {
36572
+ await super._buffer(chunk, buffer);
36573
+ const frames = chunk.samples[0]?.length ?? 0;
36574
+ if (frames === 0 || chunk.samples.length === 0) return;
36575
+ this.truePeakAccumulator ?? (this.truePeakAccumulator = new TruePeakArgmaxAccumulator(chunk.samples.length, chunk.sampleRate));
36576
+ this.truePeakAccumulator.push(chunk.samples, frames);
36577
+ }
36469
36578
  async _process(buffer) {
36470
36579
  const { frameSize, smoothing } = this.properties;
36471
36580
  const channelCount = buffer.channels;
@@ -36474,7 +36583,7 @@ var CrestReduceStream = class extends BufferedTransformStream {
36474
36583
  const sampleRate = buffer.sampleRate ?? this.sampleRate ?? 48e3;
36475
36584
  const order = LATTICE_ORDER;
36476
36585
  const hopSize = this.hopSize;
36477
- const { db: inputTpDb, peakInputSample } = await measureBufferTruePeakWithArgmax(buffer);
36586
+ const { db: inputTpDb, peakInputSample } = this.truePeakAccumulator?.finalize() ?? { db: linearToDb(0), peakInputSample: 0 };
36478
36587
  const lambda = groupDelayLambda(sampleRate, order);
36479
36588
  const { trajectory, frameCount } = await streamLatticeTrajectory(buffer, frameSize, hopSize, this.fftBackend, this.fftAddonOptions, {
36480
36589
  globalTruePeakDb: inputTpDb,
@@ -36482,58 +36591,21 @@ var CrestReduceStream = class extends BufferedTransformStream {
36482
36591
  sampleRate,
36483
36592
  lambda
36484
36593
  });
36485
- if (frameCount === 0) {
36486
- return;
36487
- }
36488
- const smoothed = smoothControlTrajectory(trajectory, smoothing, trajectoryFrameRate(sampleRate, hopSize), exactHoldHalfWidthFrames(sampleRate, hopSize), hopSize);
36489
- const out = new ChunkBuffer();
36490
- await streamLatticeApply(buffer, smoothed, 1, order, hopSize, async (channels, got) => {
36491
- await out.write(
36492
- channels.map((channel) => channel.length === got ? channel : channel.subarray(0, got)),
36493
- sampleRate
36494
- );
36495
- });
36496
- await out.flushWrites();
36497
- if (out.frames > 0) this.outputBuffer = out;
36498
- else await out.close();
36594
+ if (frameCount === 0) return;
36595
+ this.smoothedTrajectory = smoothControlTrajectory(trajectory, smoothing, trajectoryFrameRate(sampleRate, hopSize), exactHoldHalfWidthFrames(sampleRate, hopSize), hopSize);
36596
+ this.applyOrder = order;
36597
+ this.applyHopSize = hopSize;
36499
36598
  }
36500
- /**
36501
- * Serve the whole-file transformed output sequentially in chunk cadence
36502
- * from the node-owned disk-backed `ChunkBuffer` (the `loudnessTarget`
36503
- * `_unbuffer` precedent — read forward in chunk-cadence lockstep with
36504
- * upstream chunks; NO overlap re-feed since `bufferSize === WHOLE_FILE`).
36505
- * When no transform was produced (no audio or a sub-frame input —
36506
- * there is no `strength` bypass after the 2026-05-17 keystone) the
36507
- * input chunk is emitted VERBATIM — the length-preserving passthrough
36508
- * fallback (nothing mutated, same backing arrays).
36509
- */
36510
- async _unbuffer(chunk) {
36511
- const output = this.outputBuffer;
36512
- const length = chunk.samples[0]?.length ?? 0;
36513
- if (output === null || output.frames === 0 || length === 0) return chunk;
36514
- if (!this.unbufferCursorReady) {
36515
- await output.reset();
36516
- this.unbufferCursorReady = true;
36517
- }
36518
- const transformedChunk = await output.read(length);
36519
- const transformed = transformedChunk.samples;
36599
+ _unbuffer(chunk) {
36600
+ const trajectory = this.smoothedTrajectory;
36601
+ const frames = chunk.samples[0]?.length ?? 0;
36602
+ const channelCount = chunk.samples.length;
36603
+ if (trajectory === void 0 || frames === 0 || channelCount === 0) return chunk;
36604
+ this.applyState ?? (this.applyState = new LatticeApplyState(trajectory, this.applyOrder, this.applyHopSize, channelCount));
36605
+ const transformed = this.applyState.apply(chunk.samples, frames);
36520
36606
  const samples = chunk.samples.map((inputChannel, ch) => transformed[ch] ?? inputChannel);
36521
36607
  return { samples, offset: chunk.offset, sampleRate: chunk.sampleRate, bitDepth: chunk.bitDepth };
36522
36608
  }
36523
- /**
36524
- * Idempotent cleanup of the node-owned output `ChunkBuffer` so its
36525
- * backing temp file is released on EVERY exit path — not only graceful
36526
- * end-of-stream (the 2026-05-12 `_teardown`-guaranteed-cleanup
36527
- * Decision; the `loudnessTarget` precedent). `BufferedTransformStream`
36528
- * already closes its own accumulation `chunkBuffer` in `teardown()`;
36529
- * this node-owned extra buffer needs its own release.
36530
- */
36531
- async _teardown() {
36532
- if (this.outputBuffer !== null) {
36533
- await this.outputBuffer.close();
36534
- this.outputBuffer = null;
36535
- }
36536
- }
36537
36609
  };
36538
36610
  var _CrestReduceNode = class _CrestReduceNode extends TransformNode {
36539
36611
  constructor(properties) {
@@ -36550,10 +36622,10 @@ var _CrestReduceNode = class _CrestReduceNode extends TransformNode {
36550
36622
  return new _CrestReduceNode({ ...this.properties, previousProperties: this.properties, ...overrides });
36551
36623
  }
36552
36624
  };
36553
- _CrestReduceNode.moduleName = "Crest Reduce";
36625
+ _CrestReduceNode.nodeName = "Crest Reduce";
36554
36626
  _CrestReduceNode.packageName = PACKAGE_NAME;
36555
36627
  _CrestReduceNode.packageVersion = PACKAGE_VERSION;
36556
- _CrestReduceNode.moduleDescription = "Content-adaptive, magnitude-preserving, phase-only crest-factor reducer \u2014 a pre-limiter headroom stage that rearranges signal phase to flatten true-peak excursions without changing the magnitude spectrum, never increasing crest factor";
36628
+ _CrestReduceNode.nodeDescription = "Content-adaptive, magnitude-preserving, phase-only crest-factor reducer \u2014 a pre-limiter headroom stage that rearranges signal phase to flatten true-peak excursions without changing the magnitude spectrum, never increasing crest factor";
36557
36629
  _CrestReduceNode.schema = schema22;
36558
36630
  var CrestReduceNode = _CrestReduceNode;
36559
36631
  function crestReduce(options) {
@@ -37045,7 +37117,6 @@ var WindowReader = class {
37045
37117
  getScratch() {
37046
37118
  return this.scratch;
37047
37119
  }
37048
- /** Fill the leading `edgePadSamples` of the virtual signal with zeros and load the rest of the first window from `buffer`. */
37049
37120
  async preload(buffer, edgePadSamples) {
37050
37121
  for (let ch = 0; ch < this.channels; ch++) this.scratch[ch].fill(0);
37051
37122
  this.virtualCursor = 0;
@@ -37055,7 +37126,6 @@ var WindowReader = class {
37055
37126
  if (remainingInWindow > 0) await this.readInto(buffer, headPad, remainingInWindow);
37056
37127
  this.virtualCursor = this.windowSamples;
37057
37128
  }
37058
- /** Slide scratch left by `step` samples; append `step` new samples from `buffer` (zero-filled past end). */
37059
37129
  async advance(buffer, step) {
37060
37130
  if (step <= 0) return;
37061
37131
  const keep = this.windowSamples - step;
@@ -37169,20 +37239,6 @@ var DeBleedStream = class extends BufferedTransformStream {
37169
37239
  }
37170
37240
  this.referenceBuffers = [];
37171
37241
  }
37172
- /**
37173
- * Run the first-N-seconds warm-up scan across all target channels and all
37174
- * references in a single sequential pass over each buffer.
37175
- *
37176
- * Each buffer is rewound to its start, then sequentially read for
37177
- * `warmupSamples` real samples. STFTs are computed once per buffer (one
37178
- * STFT per channel for the multi-channel target, one STFT per reference
37179
- * for the references). Per-channel cross-spectral accumulators then
37180
- * consume the pre-computed STFTs.
37181
- *
37182
- * Returns a `channels × refCount` array of `TransferFunction` seeds:
37183
- * `seedsByChannel[ch][refIndex]`. Degeneracy validation and cold-start
37184
- * fallback per `validateTransferSeed`.
37185
- */
37186
37242
  async warmupSeedsAllChannels(buffer, channels, warmupFrames, fftSize, hopSize) {
37187
37243
  const { numBins, referenceBuffers } = this;
37188
37244
  const refCount = referenceBuffers.length;
@@ -37229,7 +37285,7 @@ var DeBleedStream = class extends BufferedTransformStream {
37229
37285
  const validated = seeds.map((seed) => {
37230
37286
  const validation = validateTransferSeed(seed);
37231
37287
  if (validation.degenerate) {
37232
- console.warn(`de-bleed: warm-up seed degenerate (${validation.reason}); falling back to cold-start \u0124(\u2113=0) = 0.`);
37288
+ this.log("warm-up seed degenerate; falling back to cold-start \u0124(\u2113=0) = 0", { reason: validation.reason }, "warn");
37233
37289
  return coldStartSeed(numBins);
37234
37290
  }
37235
37291
  return seed;
@@ -37493,17 +37549,18 @@ var DeBleedStream = class extends BufferedTransformStream {
37493
37549
  if (profileEnabled) {
37494
37550
  const total = profileMs.warmup + profileMs.stftRead + profileMs.msad + profileMs.kalman + profileMs.mwf + profileMs.nlm + profileMs.dftt + profileMs.applyMaskIstft + profileMs.write;
37495
37551
  const pct = (key) => `${(profileMs[key] / 1e3).toFixed(2)}s (${(profileMs[key] / total * 100).toFixed(1)}%)`;
37496
- console.log(`[deBleed profile]
37497
- warmup : ${pct("warmup")}
37498
- stft+read : ${pct("stftRead")}
37499
- msad : ${pct("msad")}
37500
- kalman+isp : ${pct("kalman")}
37501
- mwf : ${pct("mwf")}
37502
- nlm : ${pct("nlm")}
37503
- dftt : ${pct("dftt")}
37504
- applyMask+istft: ${pct("applyMaskIstft")}
37505
- write : ${pct("write")}
37506
- TOTAL : ${(total / 1e3).toFixed(2)}s`);
37552
+ this.log("profile", {
37553
+ warmup: pct("warmup"),
37554
+ stftRead: pct("stftRead"),
37555
+ msad: pct("msad"),
37556
+ kalman: pct("kalman"),
37557
+ mwf: pct("mwf"),
37558
+ nlm: pct("nlm"),
37559
+ dftt: pct("dftt"),
37560
+ applyMaskIstft: pct("applyMaskIstft"),
37561
+ write: pct("write"),
37562
+ totalS: (total / 1e3).toFixed(2)
37563
+ });
37507
37564
  }
37508
37565
  }
37509
37566
  };
@@ -37522,10 +37579,10 @@ var _DeBleedNode = class _DeBleedNode extends TransformNode {
37522
37579
  return new _DeBleedNode({ ...this.properties, previousProperties: this.properties, ...overrides });
37523
37580
  }
37524
37581
  };
37525
- _DeBleedNode.moduleName = "De-Bleed Adaptive";
37582
+ _DeBleedNode.nodeName = "De-Bleed Adaptive";
37526
37583
  _DeBleedNode.packageName = PACKAGE_NAME;
37527
37584
  _DeBleedNode.packageVersion = PACKAGE_VERSION;
37528
- _DeBleedNode.moduleDescription = "Adaptive (MEF FDAF Kalman + MWF + MSAD) reference-based microphone bleed reduction. Stages 1+2 are MEF Meyer-Elshamy-Fingscheidt 2020; Stage 3 is Lukin-Todd 2D NLM+DFTT post-filter.";
37585
+ _DeBleedNode.nodeDescription = "Adaptive (MEF FDAF Kalman + MWF + MSAD) reference-based microphone bleed reduction. Stages 1+2 are MEF Meyer-Elshamy-Fingscheidt 2020; Stage 3 is Lukin-Todd 2D NLM+DFTT post-filter.";
37529
37586
  _DeBleedNode.schema = schema23;
37530
37587
  var DeBleedNode = _DeBleedNode;
37531
37588
  function deBleed(references, options) {
@@ -37544,7 +37601,7 @@ function deBleed(references, options) {
37544
37601
  });
37545
37602
  }
37546
37603
  var require3 = createRequire(import.meta.url);
37547
- function createOnnxSession(addonPath, modelPath, options) {
37604
+ function createOnnxSession(addonPath, modelPath, options, log) {
37548
37605
  let addon;
37549
37606
  try {
37550
37607
  addon = require3(addonPath);
@@ -37566,7 +37623,7 @@ function createOnnxSession(addonPath, modelPath, options) {
37566
37623
  } catch (error50) {
37567
37624
  provider = `<unknown> (getProvider() threw: ${error50 instanceof Error ? error50.message : String(error50)})`;
37568
37625
  }
37569
- console.log(`[onnx-runtime] session created for ${modelName} using ${provider}`);
37626
+ log?.("onnx session created", { modelName, provider });
37570
37627
  return {
37571
37628
  run(inputs) {
37572
37629
  return session.run(inputs);
@@ -37638,14 +37695,10 @@ var schema24 = external_exports.object({
37638
37695
  var DeepFilterNet3Stream = class extends BufferedTransformStream {
37639
37696
  constructor() {
37640
37697
  super(...arguments);
37641
- // One DfnState per channel — DFN3's recurrent state is per-source, so stereo input
37642
- // needs two independent states (matches DTLN's per-channel handling).
37643
- // Allocated lazily on the first `_process` call so we can size to the actual chunk
37644
- // channel count rather than guessing.
37645
37698
  this.dfnStates = [];
37646
37699
  }
37647
37700
  async _setup(input, context) {
37648
- this.session = createOnnxSession(this.properties.onnxAddonPath, this.properties.modelPath, { executionProviders: ["cpu"] });
37701
+ this.session = createOnnxSession(this.properties.onnxAddonPath, this.properties.modelPath, { executionProviders: ["cpu"] }, (message, data) => this.log(message, data));
37649
37702
  const sourceRate = this.properties.sampleRate;
37650
37703
  if (sourceRate === DFN3_SAMPLE_RATE) {
37651
37704
  return super._setup(input, context);
@@ -37719,10 +37772,10 @@ var _DeepFilterNet3Node = class _DeepFilterNet3Node extends TransformNode {
37719
37772
  return new _DeepFilterNet3Node({ ...this.properties, previousProperties: this.properties, ...overrides });
37720
37773
  }
37721
37774
  };
37722
- _DeepFilterNet3Node.moduleName = "DeepFilterNet3 (Denoiser)";
37775
+ _DeepFilterNet3Node.nodeName = "DeepFilterNet3 (Denoiser)";
37723
37776
  _DeepFilterNet3Node.packageName = PACKAGE_NAME;
37724
37777
  _DeepFilterNet3Node.packageVersion = PACKAGE_VERSION;
37725
- _DeepFilterNet3Node.moduleDescription = "Remove background noise from speech using DeepFilterNet3 (48 kHz full-band CRN)";
37778
+ _DeepFilterNet3Node.nodeDescription = "Remove background noise from speech using DeepFilterNet3 (48 kHz full-band CRN)";
37726
37779
  _DeepFilterNet3Node.schema = schema24;
37727
37780
  var DeepFilterNet3Node = _DeepFilterNet3Node;
37728
37781
  function deepFilterNet3(options) {
@@ -37791,11 +37844,6 @@ var DtlnBlockStream = class {
37791
37844
  };
37792
37845
  this.stftOutput = { real: this.stftRealScratch, imag: this.stftImagScratch };
37793
37846
  }
37794
- /**
37795
- * Feed `BLOCK_SHIFT` samples; return `BLOCK_SHIFT` samples of output (from the
37796
- * stable left edge of the OLA accumulator). The first `WARMUP_SHIFTS` calls
37797
- * return zeros (the sliding window is not yet full of real samples).
37798
- */
37799
37847
  step(inputBlock) {
37800
37848
  if (inputBlock.length !== BLOCK_SHIFT) {
37801
37849
  throw new Error(`DtlnBlockStream.step: expected ${String(BLOCK_SHIFT)} samples, got ${String(inputBlock.length)}`);
@@ -37817,11 +37865,6 @@ var DtlnBlockStream = class {
37817
37865
  this.olaScratch.fill(0, BLOCK_LEN - BLOCK_SHIFT, BLOCK_LEN);
37818
37866
  return out;
37819
37867
  }
37820
- /**
37821
- * Drain the OLA accumulator's remaining stable samples
37822
- * (`BLOCK_LEN - BLOCK_SHIFT = 384` samples). After this, the stream's state
37823
- * is exhausted but no new inference has run.
37824
- */
37825
37868
  flush() {
37826
37869
  const remaining = BLOCK_LEN - BLOCK_SHIFT;
37827
37870
  const out = new Float32Array(remaining);
@@ -37872,15 +37915,15 @@ var schema25 = external_exports.object({
37872
37915
  fftwAddonPath: external_exports.string().default("").meta({ input: "file", mode: "open", binary: "fftw-addon", download: "https://github.com/visionsofparadise/fftw-addon" }).describe("FFTW native addon \u2014 CPU FFT acceleration")
37873
37916
  });
37874
37917
  var DTLN_SAMPLE_RATE = 16e3;
37875
- var CHUNK_FRAMES8 = 16e3;
37918
+ var CHUNK_FRAMES5 = 16e3;
37876
37919
  var RESAMPLE_DRAIN_CHUNK = 16384;
37877
37920
  var STEP_BATCH_SIZE = 16e3;
37878
37921
  var WARMUP_SAMPLES = WARMUP_SHIFTS * BLOCK_SHIFT;
37879
37922
  var DtlnStream = class extends BufferedTransformStream {
37880
37923
  async _setup(input, context) {
37881
37924
  const onnxProviders = filterOnnxProviders(context.executionProviders);
37882
- this.session1 = createOnnxSession(this.properties.onnxAddonPath, this.properties.modelPath1, { executionProviders: onnxProviders });
37883
- this.session2 = createOnnxSession(this.properties.onnxAddonPath, this.properties.modelPath2, { executionProviders: onnxProviders });
37925
+ this.session1 = createOnnxSession(this.properties.onnxAddonPath, this.properties.modelPath1, { executionProviders: onnxProviders }, (message, data) => this.log(message, data));
37926
+ this.session2 = createOnnxSession(this.properties.onnxAddonPath, this.properties.modelPath2, { executionProviders: onnxProviders }, (message, data) => this.log(message, data));
37884
37927
  const cpuProviders = context.executionProviders.filter((ep) => ep !== "gpu");
37885
37928
  const fft2 = initFftBackend(cpuProviders.length > 0 ? cpuProviders : ["cpu"], this.properties);
37886
37929
  this.fftBackend = fft2.backend;
@@ -37924,11 +37967,11 @@ var DtlnStream = class extends BufferedTransformStream {
37924
37967
  await buffer.clear();
37925
37968
  await output.reset();
37926
37969
  for (; ; ) {
37927
- const chunk = await output.read(CHUNK_FRAMES8);
37970
+ const chunk = await output.read(CHUNK_FRAMES5);
37928
37971
  const got = chunk.samples[0]?.length ?? 0;
37929
37972
  if (got === 0) break;
37930
37973
  await buffer.write(chunk.samples, sourceRate, bitDepth);
37931
- if (got < CHUNK_FRAMES8) break;
37974
+ if (got < CHUNK_FRAMES5) break;
37932
37975
  }
37933
37976
  } finally {
37934
37977
  if (pair) {
@@ -37952,10 +37995,10 @@ var DtlnStream = class extends BufferedTransformStream {
37952
37995
  let samplesFed = 0;
37953
37996
  let warmupRemaining = WARMUP_SAMPLES;
37954
37997
  const writerState = { written: 0 };
37955
- const pumpDone = pair !== void 0 ? pumpSourceToResampleIn({ buffer, resampleIn: pair.resampleIn, channels, chunkFrames: CHUNK_FRAMES8 }) : Promise.resolve();
37998
+ const pumpDone = pair !== void 0 ? pumpSourceToResampleIn({ buffer, resampleIn: pair.resampleIn, channels, chunkFrames: CHUNK_FRAMES5 }) : Promise.resolve();
37956
37999
  const drainerDone = pair !== void 0 ? drainResampleOutToBuffer({ resampleOut: pair.resampleOut, output, channels, sourceRate, bitDepth, originalFrames, writerState }) : Promise.resolve();
37957
38000
  for (; ; ) {
37958
- const got16k = await pullNextChunkAt16k({ buffer, pair, channels, frames: CHUNK_FRAMES8 });
38001
+ const got16k = await pullNextChunkAt16k({ buffer, pair, channels, frames: CHUNK_FRAMES5 });
37959
38002
  if (got16k === void 0) break;
37960
38003
  const firstChannel = got16k[0];
37961
38004
  const chunkFrames = firstChannel?.length ?? 0;
@@ -38168,10 +38211,10 @@ var _DtlnNode = class _DtlnNode extends TransformNode {
38168
38211
  return new _DtlnNode({ ...this.properties, previousProperties: this.properties, ...overrides });
38169
38212
  }
38170
38213
  };
38171
- _DtlnNode.moduleName = "DTLN (Denoiser)";
38214
+ _DtlnNode.nodeName = "DTLN (Denoiser)";
38172
38215
  _DtlnNode.packageName = PACKAGE_NAME;
38173
38216
  _DtlnNode.packageVersion = PACKAGE_VERSION;
38174
- _DtlnNode.moduleDescription = "Remove background noise from speech using DTLN neural network";
38217
+ _DtlnNode.nodeDescription = "Remove background noise from speech using DTLN neural network";
38175
38218
  _DtlnNode.schema = schema25;
38176
38219
  var DtlnNode = _DtlnNode;
38177
38220
  function dtln(options) {
@@ -38315,12 +38358,12 @@ var HOP_SIZE2 = 1024;
38315
38358
  var SEGMENT_SAMPLES = 343980;
38316
38359
  var OVERLAP = 0.25;
38317
38360
  var TRANSITION_POWER = 1;
38318
- var CHUNK_FRAMES9 = 44100;
38361
+ var CHUNK_FRAMES6 = 44100;
38319
38362
  var RESAMPLE_DRAIN_CHUNK2 = 16384;
38320
38363
  var STEM_OUTPUTS = 4 * 2;
38321
38364
  var HtdemucsStream = class extends BufferedTransformStream {
38322
38365
  async _setup(input, context) {
38323
- this.session = createOnnxSession(this.properties.onnxAddonPath, this.properties.modelPath, { executionProviders: ["cpu"] });
38366
+ this.session = createOnnxSession(this.properties.onnxAddonPath, this.properties.modelPath, { executionProviders: ["cpu"] }, (message, data) => this.log(message, data));
38324
38367
  return super._setup(input, context);
38325
38368
  }
38326
38369
  async _process(buffer) {
@@ -38362,11 +38405,11 @@ var HtdemucsStream = class extends BufferedTransformStream {
38362
38405
  await buffer.clear();
38363
38406
  await output.reset();
38364
38407
  for (; ; ) {
38365
- const chunk = await output.read(CHUNK_FRAMES9);
38408
+ const chunk = await output.read(CHUNK_FRAMES6);
38366
38409
  const got = chunk.samples[0]?.length ?? 0;
38367
38410
  if (got === 0) break;
38368
38411
  await buffer.write(chunk.samples, sourceRate, bitDepth);
38369
- if (got < CHUNK_FRAMES9) break;
38412
+ if (got < CHUNK_FRAMES6) break;
38370
38413
  }
38371
38414
  } finally {
38372
38415
  if (pair) {
@@ -38379,7 +38422,7 @@ var HtdemucsStream = class extends BufferedTransformStream {
38379
38422
  const { buffer, output, channels, originalFrames, sourceRate, bitDepth, stats, pair } = args;
38380
38423
  const stride = Math.round((1 - OVERLAP) * SEGMENT_SAMPLES);
38381
38424
  const writerState = { written: 0 };
38382
- const pumpDone = pair !== void 0 ? pumpSourceToResampleIn2({ buffer, resampleIn: pair.resampleIn, channels, chunkFrames: CHUNK_FRAMES9 }) : Promise.resolve();
38425
+ const pumpDone = pair !== void 0 ? pumpSourceToResampleIn2({ buffer, resampleIn: pair.resampleIn, channels, chunkFrames: CHUNK_FRAMES6 }) : Promise.resolve();
38383
38426
  const drainerDone = pair !== void 0 ? drainResampleOutToBuffer2({ resampleOut: pair.resampleOut, output, channels, sourceRate, bitDepth, originalFrames, writerState }) : Promise.resolve();
38384
38427
  const weight = new Float32Array(SEGMENT_SAMPLES);
38385
38428
  const half = SEGMENT_SAMPLES / 2;
@@ -38425,7 +38468,7 @@ var HtdemucsStream = class extends BufferedTransformStream {
38425
38468
  if (!inputExhausted) {
38426
38469
  while (segFilled < SEGMENT_SAMPLES) {
38427
38470
  const need = SEGMENT_SAMPLES - segFilled;
38428
- const got = await pullNextChunkAt441({ buffer, pair, channels, frames: Math.min(need, CHUNK_FRAMES9) });
38471
+ const got = await pullNextChunkAt441({ buffer, pair, channels, frames: Math.min(need, CHUNK_FRAMES6) });
38429
38472
  if (got === void 0 || got[0].length === 0) {
38430
38473
  inputExhausted = true;
38431
38474
  break;
@@ -38540,7 +38583,7 @@ async function computeStreamingStats(buffer, channels) {
38540
38583
  let sum = 0;
38541
38584
  let count = 0;
38542
38585
  for (; ; ) {
38543
- const chunk = await buffer.read(CHUNK_FRAMES9);
38586
+ const chunk = await buffer.read(CHUNK_FRAMES6);
38544
38587
  const frames = chunk.samples[0]?.length ?? 0;
38545
38588
  if (frames === 0) break;
38546
38589
  const left = chunk.samples[0];
@@ -38553,14 +38596,14 @@ async function computeStreamingStats(buffer, channels) {
38553
38596
  for (let index = 0; index < frames; index++) sum += right[index] ?? 0;
38554
38597
  count += frames;
38555
38598
  }
38556
- if (frames < CHUNK_FRAMES9) break;
38599
+ if (frames < CHUNK_FRAMES6) break;
38557
38600
  }
38558
38601
  const mean = count > 0 ? sum / count : 0;
38559
38602
  await buffer.reset();
38560
38603
  let variance = 0;
38561
38604
  let varCount = 0;
38562
38605
  for (; ; ) {
38563
- const chunk = await buffer.read(CHUNK_FRAMES9);
38606
+ const chunk = await buffer.read(CHUNK_FRAMES6);
38564
38607
  const frames = chunk.samples[0]?.length ?? 0;
38565
38608
  if (frames === 0) break;
38566
38609
  const left = chunk.samples[0];
@@ -38579,7 +38622,7 @@ async function computeStreamingStats(buffer, channels) {
38579
38622
  }
38580
38623
  varCount += frames;
38581
38624
  }
38582
- if (frames < CHUNK_FRAMES9) break;
38625
+ if (frames < CHUNK_FRAMES6) break;
38583
38626
  }
38584
38627
  const std = varCount > 0 ? Math.sqrt(variance / varCount) || 1 : 1;
38585
38628
  return { mean, std };
@@ -38671,10 +38714,10 @@ var _HtdemucsNode = class _HtdemucsNode extends TransformNode {
38671
38714
  return new _HtdemucsNode({ ...this.properties, previousProperties: this.properties, ...overrides });
38672
38715
  }
38673
38716
  };
38674
- _HtdemucsNode.moduleName = "HTDemucs (Stem Separator)";
38717
+ _HtdemucsNode.nodeName = "HTDemucs (Stem Separator)";
38675
38718
  _HtdemucsNode.packageName = PACKAGE_NAME;
38676
38719
  _HtdemucsNode.packageVersion = PACKAGE_VERSION;
38677
- _HtdemucsNode.moduleDescription = "Rebalance stem volumes using HTDemucs source separation";
38720
+ _HtdemucsNode.nodeDescription = "Rebalance stem volumes using HTDemucs source separation";
38678
38721
  _HtdemucsNode.schema = schema26;
38679
38722
  var HtdemucsNode = _HtdemucsNode;
38680
38723
  function htdemucs(modelPath, stems, options) {
@@ -38830,7 +38873,7 @@ var COMPENSATE = 1.009;
38830
38873
  var SEGMENT_SAMPLES2 = N_FFT2 + (DIM_T3 - 1) * HOP_SIZE4;
38831
38874
  var OVERLAP2 = 0.25;
38832
38875
  var TRANSITION_POWER2 = 1;
38833
- var CHUNK_FRAMES10 = 44100;
38876
+ var CHUNK_FRAMES7 = 44100;
38834
38877
  var RESAMPLE_DRAIN_CHUNK3 = 16384;
38835
38878
  var KimVocal2Stream = class extends BufferedTransformStream {
38836
38879
  constructor(properties) {
@@ -38838,7 +38881,7 @@ var KimVocal2Stream = class extends BufferedTransformStream {
38838
38881
  this.fftInstance = new MixedRadixFft(N_FFT2);
38839
38882
  }
38840
38883
  async _setup(input, context) {
38841
- this.session = createOnnxSession(this.properties.onnxAddonPath, this.properties.modelPath, { executionProviders: filterOnnxProviders(context.executionProviders) });
38884
+ this.session = createOnnxSession(this.properties.onnxAddonPath, this.properties.modelPath, { executionProviders: filterOnnxProviders(context.executionProviders) }, (message, data) => this.log(message, data));
38842
38885
  return super._setup(input, context);
38843
38886
  }
38844
38887
  async _process(buffer) {
@@ -38878,11 +38921,11 @@ var KimVocal2Stream = class extends BufferedTransformStream {
38878
38921
  await buffer.clear();
38879
38922
  await output.reset();
38880
38923
  for (; ; ) {
38881
- const chunk = await output.read(CHUNK_FRAMES10);
38924
+ const chunk = await output.read(CHUNK_FRAMES7);
38882
38925
  const got = chunk.samples[0]?.length ?? 0;
38883
38926
  if (got === 0) break;
38884
38927
  await buffer.write(chunk.samples, sourceRate, bitDepth);
38885
- if (got < CHUNK_FRAMES10) break;
38928
+ if (got < CHUNK_FRAMES7) break;
38886
38929
  }
38887
38930
  } finally {
38888
38931
  if (pair) {
@@ -38896,7 +38939,7 @@ var KimVocal2Stream = class extends BufferedTransformStream {
38896
38939
  const stride = Math.round((1 - OVERLAP2) * SEGMENT_SAMPLES2);
38897
38940
  const isMono = channels < 2;
38898
38941
  const writerState = { written: 0 };
38899
- const pumpDone = pair !== void 0 ? pumpSourceToResampleIn3({ buffer, resampleIn: pair.resampleIn, channels, chunkFrames: CHUNK_FRAMES10 }) : Promise.resolve();
38942
+ const pumpDone = pair !== void 0 ? pumpSourceToResampleIn3({ buffer, resampleIn: pair.resampleIn, channels, chunkFrames: CHUNK_FRAMES7 }) : Promise.resolve();
38900
38943
  const drainerDone = pair !== void 0 ? drainResampleOutToBuffer3({ resampleOut: pair.resampleOut, output, channels, sourceRate, bitDepth, originalFrames, writerState }) : Promise.resolve();
38901
38944
  const weight = buildTransitionWindow(SEGMENT_SAMPLES2, TRANSITION_POWER2);
38902
38945
  const workspace = createSegmentWorkspace(SEGMENT_SAMPLES2);
@@ -38911,7 +38954,7 @@ var KimVocal2Stream = class extends BufferedTransformStream {
38911
38954
  if (!inputExhausted) {
38912
38955
  while (segFilled < SEGMENT_SAMPLES2) {
38913
38956
  const need = SEGMENT_SAMPLES2 - segFilled;
38914
- const got = await pullNextChunkAt4412({ buffer, pair, channels, frames: Math.min(need, CHUNK_FRAMES10) });
38957
+ const got = await pullNextChunkAt4412({ buffer, pair, channels, frames: Math.min(need, CHUNK_FRAMES7) });
38915
38958
  if (got === void 0 || got[0].length === 0) {
38916
38959
  inputExhausted = true;
38917
38960
  break;
@@ -39087,10 +39130,10 @@ var _KimVocal2Node = class _KimVocal2Node extends TransformNode {
39087
39130
  return new _KimVocal2Node({ ...this.properties, previousProperties: this.properties, ...overrides });
39088
39131
  }
39089
39132
  };
39090
- _KimVocal2Node.moduleName = "Kim Vocal 2 (Stem Separator)";
39133
+ _KimVocal2Node.nodeName = "Kim Vocal 2 (Stem Separator)";
39091
39134
  _KimVocal2Node.packageName = PACKAGE_NAME;
39092
39135
  _KimVocal2Node.packageVersion = PACKAGE_VERSION;
39093
- _KimVocal2Node.moduleDescription = "Isolate dialogue from background using MDX-Net vocal separation";
39136
+ _KimVocal2Node.nodeDescription = "Isolate dialogue from background using MDX-Net vocal separation";
39094
39137
  _KimVocal2Node.schema = schema27;
39095
39138
  var KimVocal2Node = _KimVocal2Node;
39096
39139
  function kimVocal2(options) {
@@ -39103,9 +39146,17 @@ function kimVocal2(options) {
39103
39146
  id: options.id
39104
39147
  });
39105
39148
  }
39106
- var CHUNK_FRAMES11 = 48e3;
39149
+ var CHUNK_FRAMES8 = 48e3;
39107
39150
  var READY_LINE = "READY\n";
39108
39151
  var READY_TIMEOUT_MS = 3e5;
39152
+ var VstHostExitedBeforeReadyError = class extends Error {
39153
+ constructor(code, stderr) {
39154
+ super(`vst-host exited before READY (code ${code ?? "null"}): ${stderr}`);
39155
+ this.name = "VstHostExitedBeforeReadyError";
39156
+ this.code = code;
39157
+ this.stderr = stderr;
39158
+ }
39159
+ };
39109
39160
  function spawnVstHost(binaryPath, args) {
39110
39161
  const proc = spawn(binaryPath, [...args], {
39111
39162
  stdio: ["pipe", "pipe", "pipe"]
@@ -39151,7 +39202,7 @@ function spawnVstHost(binaryPath, args) {
39151
39202
  };
39152
39203
  const onClose = (code) => {
39153
39204
  const stderrOutput = Buffer.concat(stderrChunks).toString();
39154
- fail(new Error(`vst-host exited before READY (code ${code ?? "null"}): ${stderrOutput}`));
39205
+ fail(new VstHostExitedBeforeReadyError(code, stderrOutput));
39155
39206
  };
39156
39207
  const timer = setTimeout(() => {
39157
39208
  fail(new Error(`vst-host did not emit READY within ${READY_TIMEOUT_MS}ms`));
@@ -39164,6 +39215,28 @@ function spawnVstHost(binaryPath, args) {
39164
39215
  });
39165
39216
  return { proc, stdin, stdout, stderr, ready, stderrChunks };
39166
39217
  }
39218
+ var CLEAN_WRAPPER_EXIT_CODES = /* @__PURE__ */ new Set([0, 1, 2]);
39219
+ function isRetryableInitCrash(error50) {
39220
+ return error50 instanceof VstHostExitedBeforeReadyError && !CLEAN_WRAPPER_EXIT_CODES.has(error50.code ?? -1);
39221
+ }
39222
+ var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
39223
+ async function spawnVstHostReady(binaryPath, args, options = {}) {
39224
+ const maxAttempts = options.maxAttempts ?? 5;
39225
+ const backoffMs = options.backoffMs ?? 750;
39226
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
39227
+ const handle = spawnVstHost(binaryPath, args);
39228
+ try {
39229
+ await handle.ready;
39230
+ return handle;
39231
+ } catch (error50) {
39232
+ handle.proc.kill();
39233
+ if (attempt >= maxAttempts || !isRetryableInitCrash(error50)) throw error50;
39234
+ options.onRetry?.(attempt, error50);
39235
+ await delay(backoffMs);
39236
+ }
39237
+ }
39238
+ throw new Error(`spawnVstHostReady: exhausted ${maxAttempts} attempts without a result`);
39239
+ }
39167
39240
  async function writeStagesJson(stages) {
39168
39241
  const dir = await mkdtemp(join(tmpdir(), "vst-host-stages-"));
39169
39242
  const path2 = join(dir, "stages.json");
@@ -39186,7 +39259,7 @@ async function processStreamingThroughVstHost(handle, buffer, channelCount, samp
39186
39259
  });
39187
39260
  await buffer.reset();
39188
39261
  for (; ; ) {
39189
- const chunk = await buffer.read(CHUNK_FRAMES11);
39262
+ const chunk = await buffer.read(CHUNK_FRAMES8);
39190
39263
  const chunkFrames = chunk.samples[0]?.length ?? 0;
39191
39264
  if (chunkFrames === 0) break;
39192
39265
  const channelArrays = [];
@@ -39199,7 +39272,7 @@ async function processStreamingThroughVstHost(handle, buffer, channelCount, samp
39199
39272
  if (!canWrite) {
39200
39273
  await waitForDrain(handle.proc, handle.stdin);
39201
39274
  }
39202
- if (chunkFrames < CHUNK_FRAMES11) break;
39275
+ if (chunkFrames < CHUNK_FRAMES8) break;
39203
39276
  }
39204
39277
  handle.stdin.end();
39205
39278
  await buffer.reset();
@@ -39247,8 +39320,7 @@ async function processStreamingThroughVstHost(handle, buffer, channelCount, samp
39247
39320
  var stageSchema = external_exports.object({
39248
39321
  pluginPath: external_exports.string().meta({ input: "file", mode: "open", accept: ".vst3" }).describe("VST3 plugin file or bundle"),
39249
39322
  pluginName: external_exports.string().optional().describe("Sub-plugin name when pluginPath is a multi-plugin shell (e.g. WaveShell)"),
39250
- presetPath: external_exports.string().optional().meta({ input: "file", mode: "open", accept: ".vstpreset" }).describe("Optional .vstpreset state file applied after the plugin loads"),
39251
- parameters: external_exports.record(external_exports.string(), external_exports.union([external_exports.number(), external_exports.string(), external_exports.boolean()])).optional().describe("Optional parameter overrides applied after presetPath. Keys map to Pedalboard parameter names exposed by the plugin.")
39323
+ presetPath: external_exports.string().optional().meta({ input: "file", mode: "open", accept: ".vstpreset" }).describe("Optional .vstpreset state file applied after the plugin loads")
39252
39324
  });
39253
39325
  var schema28 = external_exports.object({
39254
39326
  vstHostPath: external_exports.string().default("").meta({ input: "file", mode: "open", binary: "vst-host", download: "https://github.com/visionsofparadise/vst-host" }).describe("vst-host \u2014 Pedalboard-based VST3 host CLI"),
@@ -39283,13 +39355,11 @@ var Vst3Stream = class extends BufferedTransformStream {
39283
39355
  "--channels",
39284
39356
  String(channels)
39285
39357
  ];
39286
- const handle = spawnVstHost(this.properties.vstHostPath, args);
39287
- try {
39288
- await handle.ready;
39289
- } catch (error50) {
39290
- handle.proc.kill();
39291
- throw error50;
39292
- }
39358
+ const handle = await spawnVstHostReady(this.properties.vstHostPath, args, {
39359
+ onRetry: (failedAttempt, error50) => {
39360
+ this.log("vst-host init crash, retrying", { attempt: failedAttempt, error: error50.message }, "warn");
39361
+ }
39362
+ });
39293
39363
  await processStreamingThroughVstHost(handle, buffer, channels, sampleRate, bd);
39294
39364
  }
39295
39365
  async _teardown() {
@@ -39323,10 +39393,10 @@ var _Vst3Node = class _Vst3Node extends TransformNode {
39323
39393
  return new _Vst3Node({ ...this.properties, previousProperties: this.properties, ...overrides });
39324
39394
  }
39325
39395
  };
39326
- _Vst3Node.moduleName = "VST3";
39396
+ _Vst3Node.nodeName = "VST3";
39327
39397
  _Vst3Node.packageName = PACKAGE_NAME;
39328
39398
  _Vst3Node.packageVersion = PACKAGE_VERSION;
39329
- _Vst3Node.moduleDescription = "Host a chain of VST3 effect plugins via Pedalboard (whole-file offline mode)";
39399
+ _Vst3Node.nodeDescription = "Host a chain of VST3 effect plugins via Pedalboard (whole-file offline mode)";
39330
39400
  _Vst3Node.schema = schema28;
39331
39401
  var Vst3Node = _Vst3Node;
39332
39402
  function vst3(options) {