@libraz/libsonare 1.7.0 → 1.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/worklet.js CHANGED
@@ -544,6 +544,25 @@ function getSonareModule() {
544
544
  return wrappedModule;
545
545
  }
546
546
 
547
+ // src/_chain_config.ts
548
+ function flattenChainConfig(config) {
549
+ const out = {};
550
+ const walk = (node, prefix) => {
551
+ for (const [key, value] of Object.entries(node)) {
552
+ const path = prefix ? `${prefix}.${key}` : key;
553
+ if (typeof value === "number" || typeof value === "boolean") {
554
+ out[path] = value;
555
+ } else if (value !== null && typeof value === "object") {
556
+ walk(value, path);
557
+ } else if (value !== void 0) {
558
+ throw new TypeError(`Mastering override '${path}' must be a number or boolean.`);
559
+ }
560
+ }
561
+ };
562
+ walk(config, "");
563
+ return out;
564
+ }
565
+
547
566
  // src/codes.ts
548
567
  function resolveOrdinalInRange(value, min, max, enumName) {
549
568
  if (typeof value !== "number" || !Number.isSafeInteger(value) || value < min || value > max) {
@@ -1111,6 +1130,15 @@ var RealtimeEngine = class {
1111
1130
  * Returns `-1` when the track, insert, or name is unknown. (The Python binding
1112
1131
  * raises a `SonareError` for an unknown id where Node/WASM return the `-1`
1113
1132
  * sentinel.)
1133
+ *
1134
+ * This trio is how a mastering processor gets time-varying automation: the
1135
+ * `eq.*`, `dynamics.*`, `saturation.*`, `spectral.*`, `stereo.*`,
1136
+ * `maximizer.*` and `multiband.*` processors are all available as strip
1137
+ * inserts, so placing one on a strip and resolving its parameter here drives
1138
+ * it at audio-block precision, live and offline alike. The whole-signal
1139
+ * stages of the offline mastering chain (`repair.*`, `loudness`, and the
1140
+ * match stages) have no insert form and no automation id: they buffer the
1141
+ * entire signal by construction and do not run on the realtime path.
1114
1142
  */
1115
1143
  resolveTrackInsertAutomationId(trackId, insertIndex, paramName) {
1116
1144
  return this.native.resolveTrackInsertAutomationId(trackId, insertIndex, paramName);
@@ -1238,6 +1266,33 @@ var RealtimeEngine = class {
1238
1266
  processPrepared(numFrames) {
1239
1267
  this.native.processPrepared(numFrames);
1240
1268
  }
1269
+ /**
1270
+ * Allocates the cue-bus counterpart of {@link prepareChannels}. Needed only
1271
+ * when PFL/AFL monitoring must reach a separate output: `processPrepared`
1272
+ * folds the cue bus into the program output, while
1273
+ * {@link processPreparedWithMonitor} keeps the two apart. Call once, off the
1274
+ * audio thread, with at least as many channels as `prepareChannels` got.
1275
+ */
1276
+ prepareMonitorChannels(numChannels, maxFrames) {
1277
+ this.native.prepareMonitorChannels(numChannels, maxFrames);
1278
+ }
1279
+ /**
1280
+ * Returns a Float32Array view onto the persistent cue-bus scratch for one
1281
+ * channel (valid for up to `numFrames`). Read it after
1282
+ * {@link processPreparedWithMonitor}. Re-acquire after WASM memory growth.
1283
+ */
1284
+ getMonitorChannelBuffer(channel, numFrames) {
1285
+ return this.native.getMonitorChannelBuffer(channel, numFrames);
1286
+ }
1287
+ /**
1288
+ * Runs the engine in place over the prepared scratch, writing the cue bus to
1289
+ * the monitor scratch instead of folding it into the program output.
1290
+ * Allocation-free: safe on the AudioWorklet render thread after
1291
+ * `prepareChannels` and `prepareMonitorChannels`.
1292
+ */
1293
+ processPreparedWithMonitor(numFrames) {
1294
+ this.native.processPreparedWithMonitor(numFrames);
1295
+ }
1241
1296
  processWithMonitor(channels) {
1242
1297
  return this.native.processWithMonitor(channels);
1243
1298
  }
@@ -2005,6 +2060,73 @@ var RealtimeVoiceChanger = class {
2005
2060
  }
2006
2061
  };
2007
2062
 
2063
+ // src/streaming_processors.ts
2064
+ var StreamingMasteringChain = class {
2065
+ constructor(config) {
2066
+ const module2 = getSonareModule();
2067
+ const { loudnessStaticGainDb, loudnessStaticGainPeakDb, ...chainConfig } = config;
2068
+ this.chain = module2.createStreamingMasteringChain({
2069
+ __flatParams: flattenChainConfig(chainConfig),
2070
+ loudnessStaticGainDb,
2071
+ loudnessStaticGainPeakDb
2072
+ });
2073
+ }
2074
+ /**
2075
+ * Initialize processors for the given sample rate and block layout.
2076
+ *
2077
+ * @param sampleRate - Sample rate in Hz
2078
+ * @param maxBlockSize - Maximum block size per process call
2079
+ * @param numChannels - 1 (mono) or 2 (stereo)
2080
+ */
2081
+ prepare(sampleRate, maxBlockSize, numChannels) {
2082
+ this.chain.prepare(sampleRate, maxBlockSize, numChannels);
2083
+ }
2084
+ /**
2085
+ * Process one mono block, returning the processed samples (same length).
2086
+ */
2087
+ processMono(samples) {
2088
+ return this.chain.processMono(samples);
2089
+ }
2090
+ /**
2091
+ * Process one stereo block, returning the processed channels.
2092
+ */
2093
+ processStereo(left, right) {
2094
+ if (left.length !== right.length) {
2095
+ throw new Error("Stereo channel lengths must match.");
2096
+ }
2097
+ return this.chain.processStereo(left, right);
2098
+ }
2099
+ /**
2100
+ * Emit delayed audio and finite processor tails after the final mono block.
2101
+ * Call until this returns an empty array. The initial `latencySamples()`
2102
+ * samples of the concatenated stream are delayed and should be discarded for
2103
+ * time-aligned output.
2104
+ */
2105
+ flushMono() {
2106
+ return this.chain.flushMono();
2107
+ }
2108
+ /** Stereo counterpart of {@link flushMono}. */
2109
+ flushStereo() {
2110
+ return this.chain.flushStereo();
2111
+ }
2112
+ /** Reset all processor state without rebuilding. */
2113
+ reset() {
2114
+ this.chain.reset();
2115
+ }
2116
+ /** Total reported latency in samples across all active processors. */
2117
+ latencySamples() {
2118
+ return this.chain.latencySamples();
2119
+ }
2120
+ /** Ordered stage names that will run (e.g. `"eq.tilt"`). */
2121
+ stageNames() {
2122
+ return this.chain.stageNames();
2123
+ }
2124
+ /** Release the underlying WASM object. Safe to call only once. */
2125
+ delete() {
2126
+ this.chain.delete();
2127
+ }
2128
+ };
2129
+
2008
2130
  // src/index.ts
2009
2131
  var module = null;
2010
2132
  var initPromise = null;
@@ -3407,10 +3529,12 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
3407
3529
  const clipPageRequestRing = mode === "sab" ? createSonareClipPageRequestRingBuffer(options.clipPageRequestRingCapacity ?? 128) : void 0;
3408
3530
  const externalMidiRing = mode === "sab" ? createSonareExternalMidiRingBuffer(options.externalMidiRingCapacity ?? 256) : void 0;
3409
3531
  const channelCount = Math.max(1, Math.floor(options.channelCount ?? 2));
3532
+ const cueOutput = options.cueOutput === true;
3410
3533
  const processorOptions = {
3411
3534
  sampleRate: options.sampleRate ?? context.sampleRate,
3412
3535
  blockSize,
3413
3536
  channelCount,
3537
+ cueOutput,
3414
3538
  commandSharedBuffer: commandRing?.sharedBuffer,
3415
3539
  commandRingCapacity: commandRing?.capacity,
3416
3540
  telemetrySharedBuffer: telemetryRing?.sharedBuffer,
@@ -3432,8 +3556,10 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
3432
3556
  const factory = options.nodeFactory ?? ((ctx, name, nodeOptions) => new AudioWorkletNode(ctx, name, nodeOptions));
3433
3557
  const node = factory(context, processorName, {
3434
3558
  numberOfInputs: 1,
3435
- numberOfOutputs: 1,
3436
- outputChannelCount: [channelCount],
3559
+ // The cue bus needs its own output; a single-output node keeps the
3560
+ // historical mix where process() folds the cue into the program.
3561
+ numberOfOutputs: cueOutput ? 2 : 1,
3562
+ outputChannelCount: cueOutput ? [channelCount, channelCount] : [channelCount],
3437
3563
  processorOptions
3438
3564
  });
3439
3565
  return new _SonareRealtimeEngineNode(
@@ -3446,6 +3572,7 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
3446
3572
  audioWorklet,
3447
3573
  clipPageRequestsRealtimeSafe: mode === "sab",
3448
3574
  externalMidiRealtimeSafe: mode === "sab",
3575
+ cueOutput,
3449
3576
  engineAbiVersion: detectedCapabilities?.engineAbiVersion,
3450
3577
  expectedEngineAbiVersion: detectedCapabilities?.expectedEngineAbiVersion,
3451
3578
  abiCompatible: detectedCapabilities?.abiCompatible,
@@ -5093,6 +5220,20 @@ function resolveMetronomeConfig(config) {
5093
5220
  }
5094
5221
 
5095
5222
  // src/worklet/engine-processor.ts
5223
+ function copyPlanesToOutput(output, planes, frames) {
5224
+ for (let ch = 0; ch < output.length; ch++) {
5225
+ const target = output[ch];
5226
+ const source = planes[ch] ?? planes[0];
5227
+ if (source) {
5228
+ target.set(source.subarray(0, Math.min(target.length, frames)));
5229
+ if (target.length > frames) {
5230
+ target.fill(0, frames);
5231
+ }
5232
+ } else {
5233
+ target.fill(0);
5234
+ }
5235
+ }
5236
+ }
5096
5237
  function captureTransferList(channels) {
5097
5238
  const transfers = [];
5098
5239
  const seen = /* @__PURE__ */ new Set();
@@ -5115,6 +5256,10 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
5115
5256
  // Latest metronome gains/click length pushed via 'syncMetronome'. The
5116
5257
  // SetMetronome command only toggles enabled state; the config arrives here.
5117
5258
  this.metronomeConfig = { ...DEFAULT_METRONOME_CONFIG };
5259
+ // Cue-bus plane, allocated only when the host asked for a separate PFL/AFL
5260
+ // output. Empty otherwise, so a single-output host pays no heap and keeps the
5261
+ // historical behaviour where process() folds the cue into the program mix.
5262
+ this.monitorBuffers = [];
5118
5263
  this.liveClips = /* @__PURE__ */ new Map();
5119
5264
  this.pagedClipProviders = /* @__PURE__ */ new Map();
5120
5265
  this.pagedClipPageFrames = /* @__PURE__ */ new Map();
@@ -5162,6 +5307,14 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
5162
5307
  for (let ch = 0; ch < this.channelCount; ch++) {
5163
5308
  this.channelBuffers[ch] = this.engine.getChannelBuffer(ch, this.blockSize);
5164
5309
  }
5310
+ this.cueOutput = options.cueOutput === true;
5311
+ if (this.cueOutput) {
5312
+ this.engine.prepareMonitorChannels(this.channelCount, this.blockSize);
5313
+ this.monitorBuffers = new Array(this.channelCount);
5314
+ for (let ch = 0; ch < this.channelCount; ch++) {
5315
+ this.monitorBuffers[ch] = this.engine.getMonitorChannelBuffer(ch, this.blockSize);
5316
+ }
5317
+ }
5165
5318
  if (this.scopeRing) {
5166
5319
  const interval = Math.max(1, Math.floor(options.scopeIntervalFrames ?? this.blockSize));
5167
5320
  this.engine.configureScopeTelemetry(interval, this.scopeRing.bands);
@@ -5198,6 +5351,9 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
5198
5351
  if ((this.channelBuffers[0]?.byteLength ?? 0) === 0) {
5199
5352
  this.reacquireChannelBuffers();
5200
5353
  }
5354
+ if (this.cueOutput && (this.monitorBuffers[0]?.byteLength ?? 0) === 0) {
5355
+ this.reacquireMonitorBuffers();
5356
+ }
5201
5357
  const input = inputs[0];
5202
5358
  for (let ch = 0; ch < this.channelCount; ch++) {
5203
5359
  const dst = this.channelBuffers[ch];
@@ -5208,17 +5364,16 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
5208
5364
  dst.fill(0, 0, usableFrames);
5209
5365
  }
5210
5366
  }
5211
- this.engine.processPrepared(usableFrames);
5212
- for (let ch = 0; ch < output.length; ch++) {
5213
- const target = output[ch];
5214
- const source = this.channelBuffers[ch] ?? this.channelBuffers[0];
5215
- if (source) {
5216
- target.set(source.subarray(0, Math.min(target.length, usableFrames)));
5217
- if (target.length > usableFrames) {
5218
- target.fill(0, usableFrames);
5219
- }
5220
- } else {
5221
- target.fill(0);
5367
+ if (this.cueOutput) {
5368
+ this.engine.processPreparedWithMonitor(usableFrames);
5369
+ } else {
5370
+ this.engine.processPrepared(usableFrames);
5371
+ }
5372
+ copyPlanesToOutput(output, this.channelBuffers, usableFrames);
5373
+ if (this.cueOutput) {
5374
+ const cue = outputs[1];
5375
+ if (cue) {
5376
+ copyPlanesToOutput(cue, this.monitorBuffers, usableFrames);
5222
5377
  }
5223
5378
  }
5224
5379
  this.publishClipPageRequests();
@@ -5233,6 +5388,11 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
5233
5388
  this.channelBuffers[ch] = this.engine.getChannelBuffer(ch, this.blockSize);
5234
5389
  }
5235
5390
  }
5391
+ reacquireMonitorBuffers() {
5392
+ for (let ch = 0; ch < this.channelCount; ch++) {
5393
+ this.monitorBuffers[ch] = this.engine.getMonitorChannelBuffer(ch, this.blockSize);
5394
+ }
5395
+ }
5236
5396
  receiveCommand(command) {
5237
5397
  if (!this.closed) {
5238
5398
  this.safeApplyCommand(command);
@@ -6645,6 +6805,7 @@ export {
6645
6805
  SonareRealtimeEngineWorkletProcessor,
6646
6806
  SonareRealtimeVoiceChangerWorkletProcessor,
6647
6807
  SonareWorkletProcessor,
6808
+ StreamingMasteringChain,
6648
6809
  attachOpfsClipStream,
6649
6810
  createSonareClipPageRequestRingBuffer,
6650
6811
  createSonareEngineCommandRingBuffer,