@libraz/libsonare 1.8.0 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/README.md +2 -2
  2. package/dist/_effects_common.d.ts +2 -1
  3. package/dist/_effects_common.d.ts.map +1 -1
  4. package/dist/analysis.js +1 -0
  5. package/dist/analysis.js.map +1 -1
  6. package/dist/effects_mastering.d.ts +2 -2
  7. package/dist/effects_mastering.d.ts.map +1 -1
  8. package/dist/effects_note_ops.d.ts +55 -30
  9. package/dist/effects_note_ops.d.ts.map +1 -1
  10. package/dist/effects_timepitch.d.ts +17 -12
  11. package/dist/effects_timepitch.d.ts.map +1 -1
  12. package/dist/feature_decompose.d.ts +1 -1
  13. package/dist/feature_decompose.d.ts.map +1 -1
  14. package/dist/index.d.ts +3 -3
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +216 -60
  17. package/dist/index.js.map +1 -1
  18. package/dist/mastering_core.d.ts +27 -1
  19. package/dist/mastering_core.d.ts.map +1 -1
  20. package/dist/mixer.d.ts.map +1 -1
  21. package/dist/project_types.d.ts +7 -5
  22. package/dist/project_types.d.ts.map +1 -1
  23. package/dist/public_types_mastering.d.ts +19 -1
  24. package/dist/public_types_mastering.d.ts.map +1 -1
  25. package/dist/public_types_playback.d.ts +1 -0
  26. package/dist/public_types_playback.d.ts.map +1 -1
  27. package/dist/public_types_spectral.d.ts +5 -5
  28. package/dist/public_types_spectral.d.ts.map +1 -1
  29. package/dist/realtime_engine.d.ts +49 -6
  30. package/dist/realtime_engine.d.ts.map +1 -1
  31. package/dist/sonare-analysis.wasm +0 -0
  32. package/dist/sonare.d.ts +36 -0
  33. package/dist/sonare.js +1 -1
  34. package/dist/sonare.wasm +0 -0
  35. package/dist/streaming_processors.d.ts +5 -0
  36. package/dist/streaming_processors.d.ts.map +1 -1
  37. package/dist/worker.js.map +1 -1
  38. package/dist/worklet/engine-mixer-facade.d.ts +40 -4
  39. package/dist/worklet/engine-mixer-facade.d.ts.map +1 -1
  40. package/dist/worklet/engine-node.d.ts +2 -0
  41. package/dist/worklet/engine-node.d.ts.map +1 -1
  42. package/dist/worklet/engine-processor.d.ts +4 -0
  43. package/dist/worklet/engine-processor.d.ts.map +1 -1
  44. package/dist/worklet/engine-strips.d.ts +4 -2
  45. package/dist/worklet/engine-strips.d.ts.map +1 -1
  46. package/dist/worklet/engine-sync.d.ts +4 -2
  47. package/dist/worklet/engine-sync.d.ts.map +1 -1
  48. package/dist/worklet/engine.d.ts +27 -6
  49. package/dist/worklet/engine.d.ts.map +1 -1
  50. package/dist/worklet/guards.d.ts +7 -1
  51. package/dist/worklet/guards.d.ts.map +1 -1
  52. package/dist/worklet/messages.d.ts +4 -4
  53. package/dist/worklet/messages.d.ts.map +1 -1
  54. package/dist/worklet/mixer-processor.d.ts +2 -2
  55. package/dist/worklet/mixer-processor.d.ts.map +1 -1
  56. package/dist/worklet/protocol.d.ts +10 -2
  57. package/dist/worklet/protocol.d.ts.map +1 -1
  58. package/dist/worklet.js +585 -188
  59. package/dist/worklet.js.map +1 -1
  60. package/package.json +1 -1
  61. package/src/_effects_common.ts +36 -6
  62. package/src/effects_mastering.ts +4 -0
  63. package/src/effects_note_ops.ts +92 -44
  64. package/src/effects_timepitch.ts +42 -31
  65. package/src/feature_decompose.ts +2 -2
  66. package/src/index.ts +6 -0
  67. package/src/mastering_core.ts +95 -0
  68. package/src/mixer.ts +39 -19
  69. package/src/project_types.ts +7 -5
  70. package/src/public_types_mastering.ts +21 -0
  71. package/src/public_types_playback.ts +1 -0
  72. package/src/public_types_spectral.ts +5 -5
  73. package/src/realtime_engine.ts +71 -15
  74. package/src/sonare.js.d.ts +36 -0
  75. package/src/streaming_processors.ts +8 -0
  76. package/src/worklet/engine-mixer-facade.ts +420 -86
  77. package/src/worklet/engine-node.ts +22 -4
  78. package/src/worklet/engine-processor.ts +83 -33
  79. package/src/worklet/engine-strips.ts +5 -10
  80. package/src/worklet/engine-sync.ts +6 -1
  81. package/src/worklet/engine.ts +49 -37
  82. package/src/worklet/guards.ts +55 -0
  83. package/src/worklet/messages.ts +4 -2
  84. package/src/worklet/mixer-processor.ts +10 -3
  85. package/src/worklet/protocol.ts +29 -8
package/dist/worklet.js CHANGED
@@ -1194,7 +1194,10 @@ var RealtimeEngine = class _RealtimeEngine {
1194
1194
  /**
1195
1195
  * Route a destination's (track lane's) MIDI to the external output queue
1196
1196
  * instead of the internal instrument rack, so the track plays an external
1197
- * device. Clearing it restores internal-synth playback.
1197
+ * device. Clearing it restores internal-synth playback. Control-thread only.
1198
+ * The change takes effect at the next processed block; switching a
1199
+ * destination's route first releases its notes and resets its controllers
1200
+ * through the old route, and drops its pending MIDI-FX events. Single writer.
1198
1201
  */
1199
1202
  setMidiDestinationExternal(destinationId, external) {
1200
1203
  this.native.setMidiDestinationExternal(destinationId, external);
@@ -1348,8 +1351,11 @@ var RealtimeEngine = class _RealtimeEngine {
1348
1351
  /**
1349
1352
  * Queue an immediate (live) MIDI SysEx frame to a MIDI destination. `data` is
1350
1353
  * the full message including the leading 0xF0 and trailing 0xF7 (1..512
1351
- * bytes). `renderFrame` is the frame to fire at, or -1 for immediate. Mirrors
1352
- * the Node/Python/C-ABI `pushMidiSysex`.
1354
+ * bytes). `renderFrame` is the frame to fire at, or -1 for immediate. Throws
1355
+ * `InvalidParameter` when the destination instrument cannot prepare
1356
+ * the SysEx (retrying cannot help), and `OutOfMemory` when the payload
1357
+ * slots or the command queue are full (retry after a processed block).
1358
+ * Mirrors the Node/Python/C-ABI `pushMidiSysex`.
1353
1359
  */
1354
1360
  pushMidiSysex(destinationId, data, renderFrame = -1) {
1355
1361
  this.native.pushMidiSysex(destinationId, data, renderFrame);
@@ -1372,12 +1378,30 @@ var RealtimeEngine = class _RealtimeEngine {
1372
1378
  getTransportState() {
1373
1379
  return this.native.getTransportState();
1374
1380
  }
1381
+ /** Queues an integrated-loudness reset; short-term and momentary windows are retained. */
1382
+ resetMasterLoudnessMeter(renderFrame = -1) {
1383
+ this.native.resetMasterLoudnessMeter(renderFrame);
1384
+ }
1385
+ /** Reads the immutable factory value for a resolved insert parameter id. */
1386
+ insertParameterConstructedValue(paramId) {
1387
+ return this.native.insertParameterConstructedValue(paramId);
1388
+ }
1375
1389
  play(renderFrame = -1) {
1376
1390
  this.native.play(renderFrame);
1377
1391
  }
1392
+ /**
1393
+ * A loop wrap, seek or stop sends note-offs plus CC64=0, CC121, CC123 and a
1394
+ * centred pitch bend on every channel played since the last reset, so
1395
+ * controller values set before a loop region are not restored at the wrap.
1396
+ */
1378
1397
  stop(renderFrame = -1) {
1379
1398
  this.native.stop(renderFrame);
1380
1399
  }
1400
+ /**
1401
+ * A loop wrap, seek or stop sends note-offs plus CC64=0, CC121, CC123 and a
1402
+ * centred pitch bend on every channel played since the last reset, so
1403
+ * controller values set before a loop region are not restored at the wrap.
1404
+ */
1381
1405
  seekSample(timelineSample, renderFrame = -1) {
1382
1406
  this.native.seekSample(timelineSample, renderFrame);
1383
1407
  }
@@ -1402,6 +1426,11 @@ var RealtimeEngine = class _RealtimeEngine {
1402
1426
  applyCommandsDueNowPreservingFuture() {
1403
1427
  this.native.applyCommandsDueNowPreservingFuture();
1404
1428
  }
1429
+ /**
1430
+ * A loop wrap, seek or stop sends note-offs plus CC64=0, CC121, CC123 and a
1431
+ * centred pitch bend on every channel played since the last reset, so
1432
+ * controller values set before a loop region are not restored at the wrap.
1433
+ */
1405
1434
  seekPpq(ppq, renderFrame = -1) {
1406
1435
  this.native.seekPpq(ppq, renderFrame);
1407
1436
  }
@@ -1421,6 +1450,11 @@ var RealtimeEngine = class _RealtimeEngine {
1421
1450
  sampleAtPpq(ppq) {
1422
1451
  return Number(this.native.sampleAtPpq(ppq));
1423
1452
  }
1453
+ /**
1454
+ * A loop wrap, seek or stop sends note-offs plus CC64=0, CC121, CC123 and a
1455
+ * centred pitch bend on every channel played since the last reset, so
1456
+ * controller values set before a loop region are not restored at the wrap.
1457
+ */
1424
1458
  setLoop(startPpq, endPpq, enabled = true) {
1425
1459
  this.native.setLoop(startPpq, endPpq, enabled);
1426
1460
  }
@@ -1515,10 +1549,10 @@ var RealtimeEngine = class _RealtimeEngine {
1515
1549
  if (typeof lane === "number") {
1516
1550
  return { trackId: lane };
1517
1551
  }
1518
- if (!lane.sends) {
1519
- return lane;
1520
- }
1521
- return { ...lane, sends: _RealtimeEngine.normalizeSends(lane.sends) };
1552
+ return {
1553
+ ...lane,
1554
+ sends: lane.sends ? _RealtimeEngine.normalizeSends(lane.sends) : void 0
1555
+ };
1522
1556
  })
1523
1557
  );
1524
1558
  }
@@ -1531,9 +1565,10 @@ var RealtimeEngine = class _RealtimeEngine {
1531
1565
  }
1532
1566
  setTrackBuses(buses) {
1533
1567
  this.native.setTrackBuses(
1534
- Array.isArray(buses) ? buses.map(
1535
- (bus) => bus.sends ? { ...bus, sends: _RealtimeEngine.normalizeSends(bus.sends) } : bus
1536
- ) : buses
1568
+ Array.isArray(buses) ? buses.map((bus) => ({
1569
+ ...bus,
1570
+ sends: bus.sends ? _RealtimeEngine.normalizeSends(bus.sends) : void 0
1571
+ })) : buses
1537
1572
  );
1538
1573
  }
1539
1574
  /**
@@ -1687,6 +1722,16 @@ var RealtimeEngine = class _RealtimeEngine {
1687
1722
  * stages of the offline mastering chain (`repair.*`, `loudness`, and the
1688
1723
  * match stages) have no insert form and no automation id: they buffer the
1689
1724
  * entire signal by construction and do not run on the realtime path.
1725
+ *
1726
+ * The returned id uses the track's current positional lane selector. When
1727
+ * `setTrackLanes` successfully changes lane order or membership, the engine
1728
+ * remaps already queued and published track automation by track id, but it
1729
+ * cannot update a numeric id retained by the caller. Re-resolve every track
1730
+ * insert id after such a topology change before passing it to
1731
+ * `setAutomationLane`, `setParameter`, or `setParameterSmoothed`. Use
1732
+ * `setTrackStripInsertParamByName` when the operation needs a stable track
1733
+ * identity. Master and bus insert ids are separate and are not invalidated by
1734
+ * track-lane changes.
1690
1735
  */
1691
1736
  resolveTrackInsertAutomationId(trackId, insertIndex, paramName) {
1692
1737
  return this.native.resolveTrackInsertAutomationId(trackId, insertIndex, paramName);
@@ -2013,6 +2058,12 @@ var RealtimeEngine = class _RealtimeEngine {
2013
2058
  meterScratchRenderFrame() {
2014
2059
  return Number(this.native.meterScratchRenderFrame());
2015
2060
  }
2061
+ meterScratchInputPeakDbL() {
2062
+ return this.native.meterScratchInputPeakDbL();
2063
+ }
2064
+ meterScratchInputPeakDbR() {
2065
+ return this.native.meterScratchInputPeakDbR();
2066
+ }
2016
2067
  meterScratchValue(field) {
2017
2068
  return this.native.meterScratchValue(field);
2018
2069
  }
@@ -2022,9 +2073,10 @@ var RealtimeEngine = class _RealtimeEngine {
2022
2073
  /**
2023
2074
  * Drains pending meter telemetry as per-plane (wide) records for a surround
2024
2075
  * target. Use this for a surround mix target; {@link drainMeterTelemetry}
2025
- * stays the stereo fast path. The two share one queue — call only one per
2026
- * target. The live AudioWorklet path owns the queue via the stereo drain, so
2027
- * this wide drain is for an offline (non-worklet) engine instance; per-plane
2076
+ * stays the stereo fast path. The two share one queue and each consumes every
2077
+ * target's records, so an engine uses only one of them. The live AudioWorklet
2078
+ * path owns the queue via the stereo drain, so this wide drain is for an
2079
+ * offline (non-worklet) engine instance; per-plane
2028
2080
  * surround meters are not delivered over the live worklet meter ring.
2029
2081
  */
2030
2082
  drainMeterTelemetryWide(maxRecords = 1024) {
@@ -2186,47 +2238,60 @@ var Mixer = class _Mixer {
2186
2238
  * after {@link delete}.
2187
2239
  */
2188
2240
  createRealtimeBuffer() {
2189
- const stripCount = this.stripCount();
2190
- let leftInputs = [];
2191
- let rightInputs = [];
2241
+ const leftInputs = [];
2242
+ const rightInputs = [];
2192
2243
  let outLeft = this.mixer.outputLeftView();
2193
2244
  let outRight = this.mixer.outputRightView();
2245
+ let acquiredStripCount = -1;
2246
+ const viewsDetached = () => outLeft.byteLength === 0 || outRight.byteLength === 0;
2194
2247
  const acquire = () => {
2195
- leftInputs = [];
2196
- rightInputs = [];
2197
- for (let index = 0; index < stripCount; index++) {
2248
+ const stripCount = this.stripCount();
2249
+ const detached = viewsDetached();
2250
+ if (detached) {
2251
+ leftInputs.length = 0;
2252
+ rightInputs.length = 0;
2253
+ } else {
2254
+ leftInputs.length = Math.min(leftInputs.length, stripCount);
2255
+ rightInputs.length = Math.min(rightInputs.length, stripCount);
2256
+ }
2257
+ for (let index = leftInputs.length; index < stripCount; index++) {
2198
2258
  leftInputs.push(this.mixer.inputLeftView(index));
2259
+ }
2260
+ for (let index = rightInputs.length; index < stripCount; index++) {
2199
2261
  rightInputs.push(this.mixer.inputRightView(index));
2200
2262
  }
2201
- outLeft = this.mixer.outputLeftView();
2202
- outRight = this.mixer.outputRightView();
2263
+ if (detached) {
2264
+ outLeft = this.mixer.outputLeftView();
2265
+ outRight = this.mixer.outputRightView();
2266
+ }
2267
+ acquiredStripCount = stripCount;
2203
2268
  };
2204
2269
  acquire();
2205
- const reacquireIfDetached = () => {
2206
- if (outLeft.byteLength === 0 || (leftInputs[0]?.byteLength ?? 1) === 0) {
2270
+ const acquireIfNeeded = () => {
2271
+ if (acquiredStripCount !== this.stripCount() || viewsDetached()) {
2207
2272
  acquire();
2208
2273
  }
2209
2274
  };
2210
2275
  return {
2211
2276
  get leftInputs() {
2212
- reacquireIfDetached();
2277
+ acquireIfNeeded();
2213
2278
  return leftInputs;
2214
2279
  },
2215
2280
  get rightInputs() {
2216
- reacquireIfDetached();
2281
+ acquireIfNeeded();
2217
2282
  return rightInputs;
2218
2283
  },
2219
2284
  get outLeft() {
2220
- reacquireIfDetached();
2285
+ acquireIfNeeded();
2221
2286
  return outLeft;
2222
2287
  },
2223
2288
  get outRight() {
2224
- reacquireIfDetached();
2289
+ acquireIfNeeded();
2225
2290
  return outRight;
2226
2291
  },
2227
- process: (numSamples = outLeft.length) => {
2228
- reacquireIfDetached();
2229
- this.mixer.processPreparedStereo(numSamples);
2292
+ process: (numSamples) => {
2293
+ acquireIfNeeded();
2294
+ this.mixer.processPreparedStereo(numSamples ?? outLeft.length);
2230
2295
  }
2231
2296
  };
2232
2297
  }
@@ -2999,6 +3064,13 @@ var StreamingMasteringChain = class {
2999
3064
  reset() {
3000
3065
  this.chain.reset();
3001
3066
  }
3067
+ /**
3068
+ * Set one realtime-safe full-chain parameter at a serialized block boundary.
3069
+ * Structural, disabled-stage and unknown parameters are rejected.
3070
+ */
3071
+ setParameter(key, value) {
3072
+ this.chain.setParameter(key, value);
3073
+ }
3002
3074
  /** Total reported latency in samples across all active processors. */
3003
3075
  latencySamples() {
3004
3076
  return this.chain.latencySamples();
@@ -3105,14 +3177,16 @@ function isInitialized() {
3105
3177
  }
3106
3178
 
3107
3179
  // src/worklet/engine-sync.ts
3108
- function buildMixerLanes(trackLaneIds, trackSends, trackOutputBus) {
3180
+ function buildMixerLanes(trackLaneIds, trackSends, trackOutputBus, trackSourceChannelLayout) {
3109
3181
  return trackLaneIds.map((trackId) => {
3110
3182
  const sends = trackSends.get(trackId);
3111
3183
  const outputBusId = trackOutputBus.get(trackId);
3184
+ const sourceChannelLayout = trackSourceChannelLayout.get(trackId);
3112
3185
  return {
3113
3186
  trackId,
3114
3187
  ...sends && sends.length > 0 ? { sends: sends.map((send) => ({ ...send })) } : {},
3115
- ...outputBusId !== void 0 ? { outputBusId } : {}
3188
+ ...outputBusId !== void 0 ? { outputBusId } : {},
3189
+ ...sourceChannelLayout !== void 0 ? { sourceChannelLayout } : {}
3116
3190
  };
3117
3191
  });
3118
3192
  }
@@ -3187,10 +3261,12 @@ function engineMixerMasterTarget(paramKind) {
3187
3261
  return ENGINE_MIXER_TARGET_BASE | 255 << 8 | paramKind & 255;
3188
3262
  }
3189
3263
  var SONARE_METER_RING_HEADER_INTS = 4;
3190
- var SONARE_METER_RING_RECORD_FLOATS = 14;
3264
+ var SONARE_METER_RING_RECORD_FLOATS = 16;
3191
3265
  var SONARE_SPECTRUM_RING_HEADER_INTS = 5;
3192
3266
  var SONARE_SCOPE_RING_HEADER_INTS = 6;
3193
3267
  var SONARE_SCOPE_RING_RECORD_PREFIX_FLOATS = 5;
3268
+ var SONARE_SCOPE_DEFAULT_BANDS = 48;
3269
+ var SONARE_SCOPE_DEFAULT_MAX_POINTS = 32;
3194
3270
  var SONARE_FRAME_LANE_BASE = 16777216;
3195
3271
  function encodeFrameLo(frame) {
3196
3272
  const f = Math.max(0, Math.floor(frame));
@@ -3230,6 +3306,7 @@ var SonareEngineCommandType = /* @__PURE__ */ ((SonareEngineCommandType2) => {
3230
3306
  SonareEngineCommandType2[SonareEngineCommandType2["SetMarker"] = 16] = "SetMarker";
3231
3307
  SonareEngineCommandType2[SonareEngineCommandType2["SeekMarker"] = 17] = "SeekMarker";
3232
3308
  SonareEngineCommandType2[SonareEngineCommandType2["SetTrackMonitorMode"] = 26] = "SetTrackMonitorMode";
3309
+ SonareEngineCommandType2[SonareEngineCommandType2["ResetMasterLoudnessMeter"] = 28] = "ResetMasterLoudnessMeter";
3233
3310
  return SonareEngineCommandType2;
3234
3311
  })(SonareEngineCommandType || {});
3235
3312
  var SonareEngineTelemetryType = /* @__PURE__ */ ((SonareEngineTelemetryType2) => {
@@ -3262,6 +3339,7 @@ var SonareEngineTelemetryError = /* @__PURE__ */ ((SonareEngineTelemetryError2)
3262
3339
  SonareEngineTelemetryError2[SonareEngineTelemetryError2["ParameterBaseOverflow"] = 21] = "ParameterBaseOverflow";
3263
3340
  return SonareEngineTelemetryError2;
3264
3341
  })(SonareEngineTelemetryError || {});
3342
+ var SONARE_FLOOR_DB = -120;
3265
3343
  function isRecord(value) {
3266
3344
  return typeof value === "object" && value !== null;
3267
3345
  }
@@ -3301,7 +3379,9 @@ function readSonareMeterRingBuffer(ring, readIndex = 0) {
3301
3379
  momentaryLufs: ring.records[offset + 10],
3302
3380
  shortTermLufs: ring.records[offset + 11],
3303
3381
  integratedLufs: ring.records[offset + 12],
3304
- gainReductionDb: ring.records[offset + 13]
3382
+ gainReductionDb: ring.records[offset + 13],
3383
+ inputPeakDbL: ring.records[offset + 14],
3384
+ inputPeakDbR: ring.records[offset + 15]
3305
3385
  });
3306
3386
  }
3307
3387
  return { nextReadIndex: writeIndex, meters };
@@ -3353,13 +3433,13 @@ function readSonareSpectrumRingBuffer(ring, readIndex = 0) {
3353
3433
  function sonareScopeRingRecordFloats(bands, maxPoints) {
3354
3434
  return SONARE_SCOPE_RING_RECORD_PREFIX_FLOATS + bands + 2 * maxPoints;
3355
3435
  }
3356
- function sonareScopeRingBufferByteLength(capacity, bands = 48, maxPoints = 32) {
3436
+ function sonareScopeRingBufferByteLength(capacity, bands = SONARE_SCOPE_DEFAULT_BANDS, maxPoints = SONARE_SCOPE_DEFAULT_MAX_POINTS) {
3357
3437
  const clampedCapacity = Math.max(1, Math.floor(capacity));
3358
3438
  const clampedBands = Math.max(1, Math.floor(bands));
3359
3439
  const clampedPoints = Math.max(0, Math.floor(maxPoints));
3360
3440
  return SONARE_SCOPE_RING_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT + clampedCapacity * sonareScopeRingRecordFloats(clampedBands, clampedPoints) * Float32Array.BYTES_PER_ELEMENT;
3361
3441
  }
3362
- function createSonareScopeRingBuffer(capacity = 64, bands = 48, maxPoints = 32) {
3442
+ function createSonareScopeRingBuffer(capacity = 64, bands = SONARE_SCOPE_DEFAULT_BANDS, maxPoints = SONARE_SCOPE_DEFAULT_MAX_POINTS) {
3363
3443
  const clampedCapacity = Math.max(1, Math.floor(capacity));
3364
3444
  const clampedBands = Math.max(1, Math.floor(bands));
3365
3445
  const clampedPoints = Math.max(0, Math.floor(maxPoints));
@@ -3426,8 +3506,14 @@ function scopeRingFromSharedBuffer(sharedBuffer, fallbackCapacity, fallbackBands
3426
3506
  const existingBands = Atomics.load(header, 3);
3427
3507
  const existingMaxPoints = Atomics.load(header, 4);
3428
3508
  const capacity = Math.max(1, Math.floor(existingCapacity || fallbackCapacity || 1));
3429
- const bands = Math.max(1, Math.floor(existingBands || fallbackBands || 48));
3430
- const maxPoints = Math.max(0, Math.floor(existingMaxPoints || (fallbackMaxPoints ?? 32)));
3509
+ const bands = Math.max(
3510
+ 1,
3511
+ Math.floor(existingBands || fallbackBands || SONARE_SCOPE_DEFAULT_BANDS)
3512
+ );
3513
+ const maxPoints = Math.max(
3514
+ 0,
3515
+ Math.floor(existingMaxPoints || (fallbackMaxPoints ?? SONARE_SCOPE_DEFAULT_MAX_POINTS))
3516
+ );
3431
3517
  const recordFloats = sonareScopeRingRecordFloats(bands, maxPoints);
3432
3518
  const minBytes = sonareScopeRingBufferByteLength(capacity, bands, maxPoints);
3433
3519
  if (sharedBuffer.byteLength < minBytes) {
@@ -3832,7 +3918,9 @@ function meterFromEngine(meter) {
3832
3918
  momentaryLufs: meter.momentaryLufs,
3833
3919
  shortTermLufs: meter.shortTermLufs,
3834
3920
  integratedLufs: meter.integratedLufs,
3835
- gainReductionDb: meter.gainReductionDb
3921
+ gainReductionDb: meter.gainReductionDb,
3922
+ inputPeakDbL: meter.inputPeakDbL,
3923
+ inputPeakDbR: meter.inputPeakDbR
3836
3924
  };
3837
3925
  }
3838
3926
  function magnitudeToDb(value) {
@@ -4017,7 +4105,23 @@ function isClipPageRequestMessage(value) {
4017
4105
  );
4018
4106
  }
4019
4107
  function isMeterSnapshot(value) {
4020
- return isRecord(value) && value.type === "meter" && typeof value.frame === "number" && typeof value.peakDbL === "number" && typeof value.peakDbR === "number" && typeof value.rmsDbL === "number" && typeof value.rmsDbR === "number" && typeof value.correlation === "number" && (typeof value.targetId === "number" || value.targetId === void 0);
4108
+ return isRecord(value) && value.type === "meter" && typeof value.frame === "number" && typeof value.peakDbL === "number" && typeof value.peakDbR === "number" && typeof value.rmsDbL === "number" && typeof value.rmsDbR === "number" && typeof value.correlation === "number" && Number.isFinite(value.inputPeakDbL) && Number.isFinite(value.inputPeakDbR) && (typeof value.targetId === "number" || value.targetId === void 0);
4109
+ }
4110
+ var SONARE_SCOPE_MAX_BANDS = 64;
4111
+ var SONARE_SCOPE_MAX_POINTS = 32;
4112
+ function isPlainFloat32Array(value, maxLength, evenLength = false) {
4113
+ if (!(value instanceof Float32Array) || typeof ArrayBuffer === "undefined" || !(value.buffer instanceof ArrayBuffer) || value.length > maxLength || evenLength && value.length % 2 !== 0) {
4114
+ return false;
4115
+ }
4116
+ for (const sample of value) {
4117
+ if (!Number.isFinite(sample)) {
4118
+ return false;
4119
+ }
4120
+ }
4121
+ return true;
4122
+ }
4123
+ function isScopeSnapshot(value) {
4124
+ return isRecord(value) && value.type === "scope" && typeof value.targetId === "number" && Number.isSafeInteger(value.targetId) && value.targetId >= 0 && value.targetId <= 4294967295 && typeof value.frame === "number" && Number.isSafeInteger(value.frame) && value.frame >= 0 && isPlainFloat32Array(value.bands, SONARE_SCOPE_MAX_BANDS) && value.bands.length > 0 && isPlainFloat32Array(value.points, SONARE_SCOPE_MAX_POINTS * 2, true);
4021
4125
  }
4022
4126
  function requireIntegerOption(value, fallback, name, minimum) {
4023
4127
  const resolved = value ?? fallback;
@@ -4026,6 +4130,9 @@ function requireIntegerOption(value, fallback, name, minimum) {
4026
4130
  }
4027
4131
  return resolved;
4028
4132
  }
4133
+ function resolveScopeBandCount(value, fallback = SONARE_SCOPE_DEFAULT_BANDS) {
4134
+ return Math.min(requireIntegerOption(value, fallback, "scopeBands", 1), SONARE_SCOPE_MAX_BANDS);
4135
+ }
4029
4136
  function requireInteger(value, fallback, name) {
4030
4137
  const resolved = value ?? fallback;
4031
4138
  if (!Number.isSafeInteger(resolved)) {
@@ -4325,6 +4432,156 @@ function syncMarkers(ctx) {
4325
4432
  }
4326
4433
 
4327
4434
  // src/worklet/engine-mixer-facade.ts
4435
+ function isSceneObject(value) {
4436
+ return value !== null && typeof value === "object" && !Array.isArray(value);
4437
+ }
4438
+ function hasOwn2(object, key) {
4439
+ return Object.getOwnPropertyDescriptor(object, key) !== void 0;
4440
+ }
4441
+ function canonicalJson(value) {
4442
+ if (Array.isArray(value)) {
4443
+ return `[${value.map((item) => canonicalJson(item)).join(",")}]`;
4444
+ }
4445
+ if (value !== null && typeof value === "object") {
4446
+ const object = value;
4447
+ return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(object[key])}`).join(",")}}`;
4448
+ }
4449
+ return JSON.stringify(value);
4450
+ }
4451
+ function aliasedValue(object, primary, legacy) {
4452
+ if (hasOwn2(object, primary)) {
4453
+ return { present: true, value: object[primary] };
4454
+ }
4455
+ if (hasOwn2(object, legacy)) {
4456
+ return { present: true, value: object[legacy] };
4457
+ }
4458
+ return { present: false, value: void 0 };
4459
+ }
4460
+ function insertSignature(insert) {
4461
+ const object = insert !== null && typeof insert === "object" ? insert : {};
4462
+ const slot = object.slot === void 0 ? "pre" : object.slot;
4463
+ const processor = aliasedValue(object, "processor", "processor_name");
4464
+ const params = aliasedValue(object, "params", "params_json");
4465
+ const sidechain = aliasedValue(object, "sidechainKey", "sidechain_key");
4466
+ const processorValue = typeof processor.value === "string" ? processor.value : "";
4467
+ const sidechainValue = typeof sidechain.value === "string" ? sidechain.value : "";
4468
+ let paramsValue;
4469
+ if (!params.present) {
4470
+ paramsValue = ["absent"];
4471
+ } else if (typeof params.value === "string") {
4472
+ if (params.value.length === 0) {
4473
+ paramsValue = ["absent"];
4474
+ } else {
4475
+ try {
4476
+ paramsValue = ["params", canonicalJson(JSON.parse(params.value))];
4477
+ } catch {
4478
+ paramsValue = ["params-invalid", params.value];
4479
+ }
4480
+ }
4481
+ } else {
4482
+ paramsValue = ["params", canonicalJson(params.value)];
4483
+ }
4484
+ return JSON.stringify([slot, processorValue, paramsValue, sidechainValue]);
4485
+ }
4486
+ function decodedInsertSignature(sceneJson, target) {
4487
+ const scene = JSON.parse(sceneJson);
4488
+ const entries = target.kind === "bus" ? scene.buses : scene.strips;
4489
+ const strip = Array.isArray(entries) ? entries.find(isSceneObject) : void 0;
4490
+ const object = isSceneObject(strip) ? strip : {};
4491
+ const inserts = Array.isArray(object.inserts) ? object.inserts.filter(isSceneObject) : [];
4492
+ const ordered = target.kind === "bus" ? inserts : [
4493
+ ...inserts.filter((insert) => insert.slot !== "post"),
4494
+ ...inserts.filter((insert) => insert.slot === "post")
4495
+ ];
4496
+ return JSON.stringify(ordered.map(insertSignature));
4497
+ }
4498
+ function constructionValue(engine, target, override) {
4499
+ let parameterId = -1;
4500
+ switch (target.kind) {
4501
+ case "track":
4502
+ parameterId = engine.resolveTrackInsertAutomationId(
4503
+ target.trackId,
4504
+ override.insertIndex,
4505
+ override.paramName
4506
+ );
4507
+ break;
4508
+ case "bus":
4509
+ parameterId = engine.resolveBusInsertAutomationId(
4510
+ target.busId,
4511
+ override.insertIndex,
4512
+ override.paramName
4513
+ );
4514
+ break;
4515
+ case "master":
4516
+ parameterId = engine.resolveMasterInsertAutomationId(
4517
+ override.insertIndex,
4518
+ override.paramName
4519
+ );
4520
+ break;
4521
+ }
4522
+ if (parameterId < 0) {
4523
+ return void 0;
4524
+ }
4525
+ return engine.insertParameterConstructedValue(parameterId);
4526
+ }
4527
+ function constructionInsertOverrides(engine, target, overrides) {
4528
+ const restored = [];
4529
+ for (const override of overrides.values()) {
4530
+ if (!sameStripTarget(override.target, target)) {
4531
+ continue;
4532
+ }
4533
+ const value = constructionValue(engine, target, override);
4534
+ if (value !== void 0) {
4535
+ restored.push({ ...override, value });
4536
+ }
4537
+ }
4538
+ return restored;
4539
+ }
4540
+ function applyInsertParamOverride(engine, override) {
4541
+ switch (override.target.kind) {
4542
+ case "track":
4543
+ engine.restoreTrackStripInsertParamByName(
4544
+ override.target.trackId,
4545
+ override.insertIndex,
4546
+ override.paramName,
4547
+ override.value
4548
+ );
4549
+ break;
4550
+ case "bus":
4551
+ engine.restoreBusStripInsertParamByName(
4552
+ override.target.busId,
4553
+ override.insertIndex,
4554
+ override.paramName,
4555
+ override.value
4556
+ );
4557
+ break;
4558
+ case "master":
4559
+ engine.restoreMasterStripInsertParamByName(
4560
+ override.insertIndex,
4561
+ override.paramName,
4562
+ override.value
4563
+ );
4564
+ break;
4565
+ }
4566
+ }
4567
+ function applyConstructionInsertOverrides(engine, overrides) {
4568
+ for (const override of overrides) {
4569
+ applyInsertParamOverride(engine, override);
4570
+ }
4571
+ }
4572
+ function clearInsertParameterBases(engine, target) {
4573
+ switch (target.kind) {
4574
+ case "track":
4575
+ engine.clearTrackInsertParameterBases(target.trackId);
4576
+ break;
4577
+ case "bus":
4578
+ engine.clearBusInsertParameterBases(target.busId);
4579
+ break;
4580
+ case "master":
4581
+ engine.clearMasterInsertParameterBases();
4582
+ break;
4583
+ }
4584
+ }
4328
4585
  function emptyStripJson(target) {
4329
4586
  switch (target.kind) {
4330
4587
  case "track":
@@ -4367,7 +4624,7 @@ function hasInsertParamOverrides(overrides, target) {
4367
4624
  }
4368
4625
  return false;
4369
4626
  }
4370
- function applyFullStripJson(engine, target, sceneJson, resetInserts) {
4627
+ function applyFullStripJson(engine, target, sceneJson) {
4371
4628
  const apply = (json) => {
4372
4629
  switch (target.kind) {
4373
4630
  case "track":
@@ -4383,21 +4640,6 @@ function applyFullStripJson(engine, target, sceneJson, resetInserts) {
4383
4640
  };
4384
4641
  engine.applyCommandsDueNowPreservingFuture();
4385
4642
  apply(sceneJson);
4386
- if (resetInserts) {
4387
- switch (target.kind) {
4388
- case "track":
4389
- engine.clearTrackInsertParameterBases(target.trackId);
4390
- break;
4391
- case "bus":
4392
- engine.clearBusInsertParameterBases(target.busId);
4393
- break;
4394
- case "master":
4395
- engine.clearMasterInsertParameterBases();
4396
- break;
4397
- }
4398
- apply(emptyStripJson(target));
4399
- apply(sceneJson);
4400
- }
4401
4643
  }
4402
4644
  function insertParamOverrideKey(override) {
4403
4645
  const targetId = override.target.kind === "track" ? override.target.trackId : override.target.kind === "bus" ? override.target.busId : "master";
@@ -4473,42 +4715,64 @@ function cacheStripJson(ctx, target, sceneJson) {
4473
4715
  }
4474
4716
  }
4475
4717
  function mixerLanes(ctx) {
4476
- return buildMixerLanes(ctx.trackLaneIds, ctx.trackSends, ctx.trackOutputBus);
4718
+ return buildMixerLanes(
4719
+ ctx.trackLaneIds,
4720
+ ctx.trackSends,
4721
+ ctx.trackOutputBus,
4722
+ ctx.trackSourceChannelLayout
4723
+ );
4724
+ }
4725
+ function cloneMixerRouting(ctx) {
4726
+ return {
4727
+ trackLaneIds: [...ctx.trackLaneIds],
4728
+ trackSends: new Map(
4729
+ Array.from(ctx.trackSends, ([trackId, sends]) => [
4730
+ trackId,
4731
+ sends.map((send) => ({ ...send }))
4732
+ ])
4733
+ ),
4734
+ trackOutputBus: new Map(ctx.trackOutputBus),
4735
+ trackSourceChannelLayout: new Map(ctx.trackSourceChannelLayout)
4736
+ };
4737
+ }
4738
+ function commitMixerRouting(ctx, draft) {
4739
+ ctx.trackLaneIds.splice(0, ctx.trackLaneIds.length, ...draft.trackLaneIds);
4740
+ ctx.trackSends.clear();
4741
+ for (const [trackId, sends] of draft.trackSends) {
4742
+ ctx.trackSends.set(
4743
+ trackId,
4744
+ sends.map((send) => ({ ...send }))
4745
+ );
4746
+ }
4747
+ ctx.trackOutputBus.clear();
4748
+ for (const [trackId, busId] of draft.trackOutputBus) {
4749
+ ctx.trackOutputBus.set(trackId, busId);
4750
+ }
4751
+ ctx.trackSourceChannelLayout.clear();
4752
+ for (const [trackId, layout] of draft.trackSourceChannelLayout) {
4753
+ ctx.trackSourceChannelLayout.set(trackId, layout);
4754
+ }
4755
+ }
4756
+ function buildDraftMixerLanes(draft) {
4757
+ return buildMixerLanes(
4758
+ draft.trackLaneIds,
4759
+ draft.trackSends,
4760
+ draft.trackOutputBus,
4761
+ draft.trackSourceChannelLayout
4762
+ );
4477
4763
  }
4478
- function syncMixer(ctx, busesAlreadyApplied = false, forceInsertResets = []) {
4479
- const lanes = mixerLanes(ctx);
4764
+ function applyMixerRouting(ctx, draft, busesAlreadyApplied) {
4765
+ const lanes = buildDraftMixerLanes(draft);
4480
4766
  const buses = ctx.buses.map((bus) => ({ ...bus }));
4481
4767
  if (!busesAlreadyApplied) {
4482
4768
  ctx.offlineEngine.setTrackBuses(buses);
4483
4769
  }
4484
4770
  if (lanes.length > 0) {
4485
- ctx.offlineEngine.settleInsertParameters();
4486
4771
  ctx.offlineEngine.setTrackLanes(lanes);
4487
4772
  }
4488
- if (forceInsertResets.length > 0) {
4489
- for (const binding of ctx.laneSidechains.values()) {
4490
- ctx.offlineEngine.setLaneSidechain(
4491
- binding.trackId,
4492
- binding.insertIndex,
4493
- binding.sourceTrackId
4494
- );
4495
- }
4496
- for (const binding of ctx.busSidechains.values()) {
4497
- ctx.offlineEngine.setBusSidechain(
4498
- binding.busId,
4499
- binding.insertIndex,
4500
- binding.sourceKind,
4501
- binding.sourceId
4502
- );
4503
- }
4504
- for (const binding of ctx.masterSidechains.values()) {
4505
- ctx.offlineEngine.setMasterSidechain(
4506
- binding.insertIndex,
4507
- binding.sourceKind,
4508
- binding.sourceId
4509
- );
4510
- }
4511
- }
4773
+ return { lanes, buses };
4774
+ }
4775
+ function postMixerSync(ctx, lanes, buses, insertBaseResets, oneShotInsertParamOverrides) {
4512
4776
  replayInsertParamOverrides(ctx, lanes, buses);
4513
4777
  const trackStrips = Array.from(ctx.trackStripJson, ([trackId, sceneJson]) => ({
4514
4778
  trackId,
@@ -4518,10 +4782,10 @@ function syncMixer(ctx, busesAlreadyApplied = false, forceInsertResets = []) {
4518
4782
  busId,
4519
4783
  sceneJson
4520
4784
  }));
4521
- const insertParamOverrides = Array.from(
4522
- ctx.insertParamOverrides.values(),
4523
- flattenInsertParamOverride
4524
- );
4785
+ const insertParamOverrides = [
4786
+ ...Array.from(ctx.insertParamOverrides.values(), flattenInsertParamOverride),
4787
+ ...oneShotInsertParamOverrides.map(flattenInsertParamOverride)
4788
+ ];
4525
4789
  ctx.postSync({
4526
4790
  type: "syncMixer",
4527
4791
  lanes,
@@ -4533,9 +4797,23 @@ function syncMixer(ctx, busesAlreadyApplied = false, forceInsertResets = []) {
4533
4797
  busSidechains: Array.from(ctx.busSidechains.values()),
4534
4798
  masterSidechains: Array.from(ctx.masterSidechains.values()),
4535
4799
  ...insertParamOverrides.length > 0 ? { insertParamOverrides } : {},
4536
- ...forceInsertResets.length > 0 ? { forceInsertResets } : {}
4800
+ ...insertBaseResets.length > 0 ? { insertBaseResets } : {}
4537
4801
  });
4538
4802
  }
4803
+ function syncMixerDraft(ctx, draft, busesAlreadyApplied = false, insertBaseResets = [], oneShotInsertParamOverrides = []) {
4804
+ const { lanes, buses } = applyMixerRouting(ctx, draft, busesAlreadyApplied);
4805
+ commitMixerRouting(ctx, draft);
4806
+ postMixerSync(ctx, lanes, buses, insertBaseResets, oneShotInsertParamOverrides);
4807
+ }
4808
+ function syncMixer(ctx, busesAlreadyApplied = false, insertBaseResets = [], oneShotInsertParamOverrides = []) {
4809
+ syncMixerDraft(
4810
+ ctx,
4811
+ cloneMixerRouting(ctx),
4812
+ busesAlreadyApplied,
4813
+ insertBaseResets,
4814
+ oneShotInsertParamOverrides
4815
+ );
4816
+ }
4539
4817
  function replayInsertParamOverrides(ctx, lanes = mixerLanes(ctx), buses = ctx.buses) {
4540
4818
  const activeTrackIds = new Set(lanes.map((lane) => lane.trackId));
4541
4819
  const activeBusIds = new Set(buses.map((bus) => bus.busId));
@@ -4573,34 +4851,59 @@ function replayInsertParamOverrides(ctx, lanes = mixerLanes(ctx), buses = ctx.bu
4573
4851
  }
4574
4852
  }
4575
4853
  function setTrackLanes(ctx, lanes) {
4576
- const { entries, ids } = normalizeTrackLanes(ctx.trackLaneIds, lanes);
4854
+ const draft = cloneMixerRouting(ctx);
4855
+ const { entries, ids } = normalizeTrackLanes(draft.trackLaneIds, lanes);
4577
4856
  for (const entry of entries) {
4578
4857
  if (entry.sends) {
4579
- ctx.trackSends.set(
4858
+ draft.trackSends.set(
4580
4859
  entry.trackId,
4581
4860
  entry.sends.map((send) => ({ ...send }))
4582
4861
  );
4583
4862
  }
4584
4863
  if (entry.outputBusId !== void 0) {
4585
4864
  if (entry.outputBusId === 0) {
4586
- ctx.trackOutputBus.delete(entry.trackId);
4865
+ draft.trackOutputBus.delete(entry.trackId);
4587
4866
  } else {
4588
- ctx.trackOutputBus.set(entry.trackId, entry.outputBusId);
4867
+ draft.trackOutputBus.set(entry.trackId, entry.outputBusId);
4589
4868
  }
4590
4869
  }
4870
+ if (entry.sourceChannelLayout !== void 0) {
4871
+ draft.trackSourceChannelLayout.set(entry.trackId, entry.sourceChannelLayout);
4872
+ }
4873
+ }
4874
+ draft.trackLaneIds = ids;
4875
+ syncMixerDraft(ctx, draft, true);
4876
+ }
4877
+ function ensureTrackLaneInDraft(draft, target) {
4878
+ const trackId = resolveTargetId(target);
4879
+ if (!Number.isInteger(trackId) || trackId <= 0) {
4880
+ throw new RangeError(`Invalid track id for mixer lane: ${String(target)}`);
4881
+ }
4882
+ const existing = draft.trackLaneIds.indexOf(trackId);
4883
+ if (existing >= 0) {
4884
+ return existing;
4885
+ }
4886
+ draft.trackLaneIds.push(trackId);
4887
+ return draft.trackLaneIds.length - 1;
4888
+ }
4889
+ function ensureTrackLane(ctx, target) {
4890
+ const draft = cloneMixerRouting(ctx);
4891
+ const laneIndex = ensureTrackLaneInDraft(draft, target);
4892
+ if (draft.trackLaneIds.length !== ctx.trackLaneIds.length) {
4893
+ syncMixerDraft(ctx, draft, true);
4591
4894
  }
4592
- ctx.trackLaneIds.splice(0, ctx.trackLaneIds.length, ...ids);
4593
- ctx.syncMixer();
4895
+ return laneIndex;
4594
4896
  }
4595
4897
  function setTrackOutputBus(ctx, target, busId) {
4596
- const laneIndex = ctx.ensureTrackLane(target);
4597
- const trackId = ctx.trackLaneIds[laneIndex];
4898
+ const draft = cloneMixerRouting(ctx);
4899
+ const laneIndex = ensureTrackLaneInDraft(draft, target);
4900
+ const trackId = draft.trackLaneIds[laneIndex];
4598
4901
  if (busId === 0) {
4599
- ctx.trackOutputBus.delete(trackId);
4902
+ draft.trackOutputBus.delete(trackId);
4600
4903
  } else {
4601
- ctx.trackOutputBus.set(trackId, busId);
4904
+ draft.trackOutputBus.set(trackId, busId);
4602
4905
  }
4603
- ctx.syncMixer();
4906
+ syncMixerDraft(ctx, draft, true);
4604
4907
  }
4605
4908
  function setLaneSidechain(ctx, target, insertIndex, sourceTarget) {
4606
4909
  const laneIndex = ctx.ensureTrackLane(target);
@@ -4669,13 +4972,14 @@ function ensureSidechainSource(ctx, sourceKind, sourceId) {
4669
4972
  }
4670
4973
  }
4671
4974
  function setSends(ctx, target, sends) {
4672
- const laneIndex = ctx.ensureTrackLane(target);
4673
- const trackId = ctx.trackLaneIds[laneIndex];
4674
- ctx.trackSends.set(
4975
+ const draft = cloneMixerRouting(ctx);
4976
+ const laneIndex = ensureTrackLaneInDraft(draft, target);
4977
+ const trackId = draft.trackLaneIds[laneIndex];
4978
+ draft.trackSends.set(
4675
4979
  trackId,
4676
4980
  sends.map((send) => ({ ...send }))
4677
4981
  );
4678
- ctx.syncMixer();
4982
+ syncMixerDraft(ctx, draft, true);
4679
4983
  }
4680
4984
  function setTrackBuses(ctx, buses) {
4681
4985
  ctx.offlineEngine.setTrackBuses(buses);
@@ -4709,18 +5013,45 @@ function setBusGain(ctx, busId, db) {
4709
5013
  ctx.buses[busIndex] = { ...ctx.buses[busIndex], busId, gainDb: db };
4710
5014
  return ctx.sendSmoothedParam(engineMixerBusTarget(busIndex, ENGINE_MIXER_PARAM_FADER_DB), db);
4711
5015
  }
5016
+ function replaceStripScene(ctx, target, previousJson, sceneJson) {
5017
+ const chainChanged = previousJson !== void 0 && decodedInsertSignature(previousJson, target) !== decodedInsertSignature(sceneJson, target);
5018
+ const hasOverrides = hasInsertParamOverrides(ctx.insertParamOverrides, target);
5019
+ applyFullStripJson(ctx.offlineEngine, target, sceneJson);
5020
+ if (!chainChanged && !hasOverrides) {
5021
+ return { insertBaseResets: [], constructionOverrides: [] };
5022
+ }
5023
+ const constructionOverrides = chainChanged ? [] : constructionInsertOverrides(ctx.offlineEngine, target, ctx.insertParamOverrides);
5024
+ ctx.clearInsertAutomationLanes(target);
5025
+ clearInsertParameterBases(ctx.offlineEngine, target);
5026
+ applyConstructionInsertOverrides(ctx.offlineEngine, constructionOverrides);
5027
+ clearInsertParamOverrides(ctx.insertParamOverrides, target);
5028
+ return { insertBaseResets: [target], constructionOverrides };
5029
+ }
4712
5030
  function setBusStripJson(ctx, busId, sceneJson) {
4713
- ctx.ensureBus(busId);
4714
5031
  const target = { kind: "bus", busId };
4715
- const resetInserts = ctx.busStripJson.has(busId) && ctx.busStripJson.get(busId) !== sceneJson || hasInsertParamOverrides(ctx.insertParamOverrides, target);
4716
- applyFullStripJson(ctx.offlineEngine, target, sceneJson, resetInserts);
4717
- if (resetInserts) {
4718
- ctx.clearInsertAutomationLanes(target);
4719
- }
5032
+ decodedInsertSignature(sceneJson, target);
5033
+ ctx.ensureBus(busId);
5034
+ const { insertBaseResets, constructionOverrides } = replaceStripScene(
5035
+ ctx,
5036
+ target,
5037
+ ctx.busStripJson.get(busId),
5038
+ sceneJson
5039
+ );
4720
5040
  pruneStripSidechains(ctx, target, sceneJson);
4721
5041
  ctx.busStripJson.set(busId, sceneJson);
4722
- clearInsertParamOverrides(ctx.insertParamOverrides, target);
4723
- ctx.syncMixer(resetInserts ? [target] : []);
5042
+ ctx.syncMixer(insertBaseResets, constructionOverrides);
5043
+ }
5044
+ function setMasterStripJson(ctx, sceneJson) {
5045
+ const target = { kind: "master" };
5046
+ const { insertBaseResets, constructionOverrides } = replaceStripScene(
5047
+ ctx,
5048
+ target,
5049
+ ctx.getMasterStripJson(),
5050
+ sceneJson
5051
+ );
5052
+ pruneStripSidechains(ctx, target, sceneJson);
5053
+ ctx.cacheMasterStripJson(sceneJson);
5054
+ ctx.syncMixer(insertBaseResets, constructionOverrides);
4724
5055
  }
4725
5056
 
4726
5057
  // src/worklet/engine-node.ts
@@ -4739,7 +5070,7 @@ function isTrackMonitorMode(value) {
4739
5070
  }
4740
5071
  function isValidCommandRecord(command) {
4741
5072
  const type = Number(command.type);
4742
- if (!Number.isSafeInteger(type) || type < 0 /* SetParam */ || type > 17 /* SeekMarker */ && type !== 26 /* SetTrackMonitorMode */) {
5073
+ if (!Number.isSafeInteger(type) || type < 0 /* SetParam */ || type > 17 /* SeekMarker */ && type !== 26 /* SetTrackMonitorMode */ && type !== 28 /* ResetMasterLoudnessMeter */) {
4743
5074
  return false;
4744
5075
  }
4745
5076
  return (
@@ -4824,6 +5155,8 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
4824
5155
  this.emitTelemetry(event.data);
4825
5156
  } else if (isMeterSnapshot(event.data)) {
4826
5157
  this.emitMeter(event.data);
5158
+ } else if (isScopeSnapshot(event.data)) {
5159
+ this.emitScope(event.data);
4827
5160
  } else if (isExternalMidiBatchMessage(event.data)) {
4828
5161
  this.emitMidiOut(event.data.events);
4829
5162
  } else if (isClipPageRequestMessage(event.data)) {
@@ -4876,6 +5209,12 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
4876
5209
  "SharedArrayBuffer mode requested but SharedArrayBuffer/Atomics are unavailable."
4877
5210
  );
4878
5211
  }
5212
+ const meterIntervalFrames = requireIntegerOption(
5213
+ options.meterIntervalFrames,
5214
+ 2048,
5215
+ "meterIntervalFrames",
5216
+ 0
5217
+ );
4879
5218
  const commandRingCapacity = requireIntegerOption(
4880
5219
  options.commandRingCapacity,
4881
5220
  128,
@@ -4903,7 +5242,7 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
4903
5242
  "scopeRingCapacity",
4904
5243
  1
4905
5244
  );
4906
- const scopeBands = requireIntegerOption(options.scopeBands, 48, "scopeBands", 1);
5245
+ const scopeBands = resolveScopeBandCount(options.scopeBands);
4907
5246
  const clipPageRequestRingCapacity = requireIntegerOption(
4908
5247
  options.clipPageRequestRingCapacity,
4909
5248
  128,
@@ -4934,6 +5273,7 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
4934
5273
  sampleRate: options.sampleRate ?? context.sampleRate,
4935
5274
  blockSize,
4936
5275
  channelCount,
5276
+ meterIntervalFrames,
4937
5277
  cueOutput,
4938
5278
  commandSharedBuffer: commandRing?.sharedBuffer,
4939
5279
  commandRingCapacity: commandRing?.capacity,
@@ -4943,8 +5283,8 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
4943
5283
  meterRingCapacity: meterRing?.capacity,
4944
5284
  scopeSharedBuffer: scopeRing?.sharedBuffer,
4945
5285
  scopeRingCapacity: scopeRing?.capacity,
4946
- scopeBands: scopeRing?.bands,
4947
- scopeIntervalFrames: scopeRing ? scopeIntervalFrames : void 0,
5286
+ scopeBands: scopeIntervalFrames > 0 ? scopeRing?.bands ?? scopeBands : void 0,
5287
+ scopeIntervalFrames: scopeIntervalFrames > 0 ? scopeIntervalFrames : void 0,
4948
5288
  clipPageRequestSharedBuffer: clipPageRequestRing?.sharedBuffer,
4949
5289
  clipPageRequestRingCapacity: clipPageRequestRing?.capacity,
4950
5290
  externalMidiSharedBuffer: externalMidiRing?.sharedBuffer,
@@ -4993,6 +5333,10 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
4993
5333
  play(sampleTime = -1) {
4994
5334
  return this.sendCommand({ type: 2 /* TransportPlay */, sampleTime });
4995
5335
  }
5336
+ /** Queues a reset of the master's integrated-loudness accumulator. */
5337
+ resetMasterLoudnessMeter(sampleTime = -1) {
5338
+ return this.sendCommand({ type: 28 /* ResetMasterLoudnessMeter */, sampleTime });
5339
+ }
4996
5340
  stop(sampleTime = -1) {
4997
5341
  return this.sendCommand({ type: 3 /* TransportStop */, sampleTime });
4998
5342
  }
@@ -5397,11 +5741,9 @@ function setInsertParamByName(ctx, target, insertIndex, paramName, value, applyN
5397
5741
  }
5398
5742
  function setTrackStripJson(ctx, trackId, sceneJson, trackStripJson) {
5399
5743
  const target = { kind: "track", trackId };
5400
- const resetInserts = trackStripJson.has(trackId) && trackStripJson.get(trackId) !== sceneJson || hasInsertParamOverrides(ctx.insertParamOverrides, target);
5401
- applyFullStripJson(ctx.offlineEngine, target, sceneJson, resetInserts);
5402
- clearInsertParamOverrides(ctx.insertParamOverrides, target);
5744
+ const result = replaceStripScene(ctx, target, trackStripJson.get(trackId), sceneJson);
5403
5745
  trackStripJson.set(trackId, sceneJson);
5404
- return { resetInserts };
5746
+ return result;
5405
5747
  }
5406
5748
  function setTrackStripEqBand(ctx, target, bandIndex, band) {
5407
5749
  const trackId = trackIdFor(ctx, target);
@@ -5774,6 +6116,7 @@ var SonareEngine = class _SonareEngine {
5774
6116
  this.trackLaneIds = [];
5775
6117
  this.trackSends = /* @__PURE__ */ new Map();
5776
6118
  this.trackOutputBus = /* @__PURE__ */ new Map();
6119
+ this.trackSourceChannelLayout = /* @__PURE__ */ new Map();
5777
6120
  this.laneSidechains = /* @__PURE__ */ new Map();
5778
6121
  this.busSidechains = /* @__PURE__ */ new Map();
5779
6122
  this.masterSidechains = /* @__PURE__ */ new Map();
@@ -5875,6 +6218,11 @@ var SonareEngine = class _SonareEngine {
5875
6218
  setTimeSignatureSegments(segments) {
5876
6219
  setTimeSignatureSegments(this.tempoContext, segments);
5877
6220
  }
6221
+ /**
6222
+ * A loop wrap, seek or stop sends note-offs plus CC64=0, CC121, CC123 and a
6223
+ * centred pitch bend on every channel played since the last reset, so
6224
+ * controller values set before a loop region are not restored at the wrap.
6225
+ */
5878
6226
  setLoop(startPpq, endPpq, enabled = true) {
5879
6227
  return setLoop(this.tempoContext, startPpq, endPpq, enabled);
5880
6228
  }
@@ -5933,6 +6281,15 @@ var SonareEngine = class _SonareEngine {
5933
6281
  * mixer lane first (like automationParamId) so the offline engine resolves the
5934
6282
  * same strip selector the realtime engine uses.
5935
6283
  *
6284
+ * The returned id uses the track's current positional lane selector. When
6285
+ * setTrackLanes successfully changes lane order or membership, the engine
6286
+ * remaps already queued and published track automation by track id, but it
6287
+ * cannot update a numeric id retained by the caller. Re-resolve every track
6288
+ * insert id after such a topology change before passing it to
6289
+ * setAutomationLane. Use setTrackStripInsertParamByName when the operation
6290
+ * needs a stable track identity. Master and bus insert ids are separate and
6291
+ * are not invalidated by track-lane changes.
6292
+ *
5936
6293
  * @param target Track id (declares a mixer lane on first use).
5937
6294
  * @param insertIndex Index into the strip's combined insert sequence.
5938
6295
  * @param paramName Processor JSON-key parameter name.
@@ -6046,14 +6403,23 @@ var SonareEngine = class _SonareEngine {
6046
6403
  }
6047
6404
  return sent;
6048
6405
  }
6406
+ /** Resets the master's integrated loudness accumulator in both engine mirrors. */
6407
+ resetMasterLoudnessMeter(renderFrame = -1) {
6408
+ this.offlineEngine.resetMasterLoudnessMeter(renderFrame);
6409
+ return this.sendMirroredCommand({
6410
+ type: 28 /* ResetMasterLoudnessMeter */,
6411
+ sampleTime: renderFrame
6412
+ });
6413
+ }
6049
6414
  /**
6050
6415
  * Declares the mixer track lanes in an explicit order.
6051
6416
  *
6052
- * Lane indices are append-only: once a track id occupies a lane, its index
6053
- * stays fixed for the engine's lifetime. The given list must therefore start
6054
- * with the already-declared lane ids in their current order and may only
6055
- * append new track ids after them. Entries carrying `sends` replace that
6056
- * track's send list; entries without `sends` leave existing sends untouched.
6417
+ * A successful call may reorder existing track ids, remove track ids, or add
6418
+ * new ones. Existing queued and published track automation follows the track
6419
+ * id, while numeric track-insert automation ids retained by the caller do
6420
+ * not; re-resolve those ids after every topology change. Entries carrying
6421
+ * `sends` replace that track's send list; entries without `sends` leave
6422
+ * existing sends untouched.
6057
6423
  *
6058
6424
  * @param lanes Track ids or lane descriptors in the desired lane order.
6059
6425
  */
@@ -6099,19 +6465,17 @@ var SonareEngine = class _SonareEngine {
6099
6465
  return setBusGain(this.mixerContext, busId, db);
6100
6466
  }
6101
6467
  setTrackStripJson(target, sceneJson) {
6468
+ decodedInsertSignature(sceneJson, { kind: "track", trackId: 0 });
6102
6469
  const laneIndex = this.ensureTrackLane(target);
6103
6470
  const trackId = this.trackLaneIds[laneIndex];
6104
- const { resetInserts } = setTrackStripJson(
6471
+ const { insertBaseResets, constructionOverrides } = setTrackStripJson(
6105
6472
  this.stripContext,
6106
6473
  trackId,
6107
6474
  sceneJson,
6108
6475
  this.trackStripJson
6109
6476
  );
6110
- if (resetInserts) {
6111
- this.clearInsertAutomationLanes({ kind: "track", trackId });
6112
- }
6113
6477
  pruneStripSidechains(this.mixerContext, { kind: "track", trackId }, sceneJson);
6114
- this.syncMixer(resetInserts ? [{ kind: "track", trackId }] : []);
6478
+ this.syncMixer(insertBaseResets, constructionOverrides);
6115
6479
  }
6116
6480
  setTrackStripEqBand(target, bandIndex, band) {
6117
6481
  setTrackStripEqBand(this.stripContext, target, bandIndex, band);
@@ -6168,16 +6532,7 @@ var SonareEngine = class _SonareEngine {
6168
6532
  setBusStripJson(this.mixerContext, busId, sceneJson);
6169
6533
  }
6170
6534
  setMasterStripJson(sceneJson) {
6171
- const target = { kind: "master" };
6172
- const resetInserts = this.masterStripJson !== void 0 && this.masterStripJson !== sceneJson || hasInsertParamOverrides(this.insertParamOverrides, target);
6173
- applyFullStripJson(this.offlineEngine, target, sceneJson, resetInserts);
6174
- if (resetInserts) {
6175
- this.clearInsertAutomationLanes(target);
6176
- }
6177
- pruneStripSidechains(this.mixerContext, target, sceneJson);
6178
- clearInsertParamOverrides(this.insertParamOverrides, target);
6179
- this.masterStripJson = sceneJson;
6180
- this.syncMixer(resetInserts ? [target] : []);
6535
+ setMasterStripJson(this.mixerContext, sceneJson);
6181
6536
  }
6182
6537
  setMasterStripEqBand(bandIndex, band) {
6183
6538
  setMasterStripEqBand(this.stripContext, bandIndex, band);
@@ -6380,7 +6735,10 @@ var SonareEngine = class _SonareEngine {
6380
6735
  /**
6381
6736
  * Route a track's MIDI to the external output (drained via {@link onMidiOut})
6382
6737
  * instead of an internal instrument, so the track plays an external device.
6383
- * Pass `external=false` to restore internal-synth playback.
6738
+ * Pass `external=false` to restore internal-synth playback. The change takes
6739
+ * effect at the next processed block; switching a destination's route first
6740
+ * releases its notes and resets its controllers through the old route, and
6741
+ * drops its pending MIDI-FX events.
6384
6742
  */
6385
6743
  setMidiDestinationExternal(trackId, external) {
6386
6744
  setMidiDestinationExternal(this.stripContext, trackId, external);
@@ -6724,8 +7082,8 @@ var SonareEngine = class _SonareEngine {
6724
7082
  mixerLanes() {
6725
7083
  return mixerLanes(this.mixerContext);
6726
7084
  }
6727
- syncMixer(forceInsertResets = []) {
6728
- syncMixer(this.mixerContext, false, forceInsertResets);
7085
+ syncMixer(insertBaseResets = [], oneShotInsertParamOverrides = []) {
7086
+ syncMixer(this.mixerContext, false, insertBaseResets, oneShotInsertParamOverrides);
6729
7087
  }
6730
7088
  insertAutomationTargetKey(target) {
6731
7089
  return target.kind === "track" ? `track:${target.trackId}` : target.kind === "bus" ? `bus:${target.busId}` : "master";
@@ -6803,6 +7161,7 @@ var SonareEngine = class _SonareEngine {
6803
7161
  trackLaneIds: this.trackLaneIds,
6804
7162
  trackSends: this.trackSends,
6805
7163
  trackOutputBus: this.trackOutputBus,
7164
+ trackSourceChannelLayout: this.trackSourceChannelLayout,
6806
7165
  laneSidechains: this.laneSidechains,
6807
7166
  busSidechains: this.busSidechains,
6808
7167
  masterSidechains: this.masterSidechains,
@@ -6815,7 +7174,7 @@ var SonareEngine = class _SonareEngine {
6815
7174
  ensureTrackLane: (target) => this.ensureTrackLane(target),
6816
7175
  ensureBus: (busId) => this.ensureBus(busId),
6817
7176
  mixerLanes: () => this.mixerLanes(),
6818
- syncMixer: (forceInsertResets) => this.syncMixer(forceInsertResets),
7177
+ syncMixer: (insertBaseResets, oneShotInsertParamOverrides) => this.syncMixer(insertBaseResets, oneShotInsertParamOverrides),
6819
7178
  clearInsertAutomationLanes: (target, forgetResolvedIds) => this.clearInsertAutomationLanes(target, forgetResolvedIds),
6820
7179
  sendSmoothedParam: (paramId, value) => this.sendSmoothedParam(paramId, value),
6821
7180
  getMasterStripJson: () => this.masterStripJson,
@@ -6837,7 +7196,8 @@ var SonareEngine = class _SonareEngine {
6837
7196
  resolveTargetId: (target) => this.resolveTargetId(target),
6838
7197
  readStripJson: (target) => cachedStripJson(this.mixerContext, target),
6839
7198
  writeStripJson: (target, sceneJson) => cacheStripJson(this.mixerContext, target, sceneJson),
6840
- insertParamOverrides: this.insertParamOverrides
7199
+ insertParamOverrides: this.insertParamOverrides,
7200
+ clearInsertAutomationLanes: (target) => this.clearInsertAutomationLanes(target)
6841
7201
  };
6842
7202
  }
6843
7203
  // Collaborator surface handed to the automation-lane free functions so they
@@ -6977,17 +7337,7 @@ var SonareEngine = class _SonareEngine {
6977
7337
  return resolveTargetId(target);
6978
7338
  }
6979
7339
  ensureTrackLane(target) {
6980
- const trackId = this.resolveTargetId(target);
6981
- if (!Number.isInteger(trackId) || trackId <= 0) {
6982
- throw new RangeError(`Invalid track id for mixer lane: ${String(target)}`);
6983
- }
6984
- const existing = this.trackLaneIds.indexOf(trackId);
6985
- if (existing >= 0) {
6986
- return existing;
6987
- }
6988
- this.trackLaneIds.push(trackId);
6989
- this.syncMixer();
6990
- return this.trackLaneIds.length - 1;
7340
+ return ensureTrackLane(this.mixerContext, target);
6991
7341
  }
6992
7342
  ensureBus(busId) {
6993
7343
  if (!Number.isInteger(busId) || busId <= 0) {
@@ -7099,6 +7449,14 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
7099
7449
  options.scopeRingCapacity,
7100
7450
  options.scopeBands
7101
7451
  ) : void 0;
7452
+ this.scopeIntervalFrames = requireIntegerOption(
7453
+ options.scopeIntervalFrames,
7454
+ this.scopeRing ? this.blockSize : 0,
7455
+ "scopeIntervalFrames",
7456
+ 0
7457
+ );
7458
+ this.scopeBands = resolveScopeBandCount(options.scopeBands);
7459
+ this.scopeEnabled = this.scopeIntervalFrames > 0 && (this.scopeRing !== void 0 || typeof this.transport?.postMessage === "function");
7102
7460
  this.clipPageRequestRing = options.clipPageRequestSharedBuffer ? clipPageRequestRingFromSharedBuffer(
7103
7461
  options.clipPageRequestSharedBuffer,
7104
7462
  options.clipPageRequestRingCapacity
@@ -7130,14 +7488,11 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
7130
7488
  this.monitorBuffers[ch] = this.engine.getMonitorChannelBuffer(ch, this.blockSize);
7131
7489
  }
7132
7490
  }
7133
- if (this.scopeRing) {
7134
- const interval = requireIntegerOption(
7135
- options.scopeIntervalFrames,
7136
- this.blockSize,
7137
- "scopeIntervalFrames",
7138
- 0
7491
+ if (this.scopeEnabled) {
7492
+ this.engine.configureScopeTelemetry(
7493
+ this.scopeIntervalFrames,
7494
+ Math.min(this.scopeRing?.bands ?? this.scopeBands, SONARE_SCOPE_MAX_BANDS)
7139
7495
  );
7140
- this.engine.configureScopeTelemetry(interval, this.scopeRing.bands);
7141
7496
  }
7142
7497
  }
7143
7498
  process(inputs, outputs) {
@@ -7398,37 +7753,32 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
7398
7753
  this.drainCommands(Math.min(this.commandRing.capacity, Math.max(0, write - read)));
7399
7754
  }
7400
7755
  this.engine.applyCommandsDueNowPreservingFuture();
7401
- this.engine.settleInsertParameters();
7402
7756
  if (message.buses) {
7403
7757
  this.engine.setTrackBuses(message.buses);
7404
7758
  }
7405
7759
  this.engine.setTrackLanes(message.lanes);
7406
- for (const target of message.forceInsertResets ?? []) {
7407
- const emptyJson = emptyStripJson(target);
7760
+ for (const strip of message.trackStrips ?? []) {
7761
+ this.engine.setTrackStripJson(strip.trackId, strip.sceneJson);
7762
+ }
7763
+ for (const strip of message.busStrips ?? []) {
7764
+ this.engine.setBusStripJson(strip.busId, strip.sceneJson);
7765
+ }
7766
+ if (message.masterStripJson) {
7767
+ this.engine.setMasterStripJson(message.masterStripJson);
7768
+ }
7769
+ for (const target of message.insertBaseResets ?? []) {
7408
7770
  switch (target.kind) {
7409
7771
  case "track":
7410
7772
  this.engine.clearTrackInsertParameterBases(target.trackId);
7411
- this.engine.setTrackStripJson(target.trackId, emptyJson);
7412
7773
  break;
7413
7774
  case "bus":
7414
7775
  this.engine.clearBusInsertParameterBases(target.busId);
7415
- this.engine.setBusStripJson(target.busId, emptyJson);
7416
7776
  break;
7417
7777
  case "master":
7418
7778
  this.engine.clearMasterInsertParameterBases();
7419
- this.engine.setMasterStripJson(emptyJson);
7420
7779
  break;
7421
7780
  }
7422
7781
  }
7423
- for (const strip of message.trackStrips ?? []) {
7424
- this.engine.setTrackStripJson(strip.trackId, strip.sceneJson);
7425
- }
7426
- for (const strip of message.busStrips ?? []) {
7427
- this.engine.setBusStripJson(strip.busId, strip.sceneJson);
7428
- }
7429
- if (message.masterStripJson) {
7430
- this.engine.setMasterStripJson(message.masterStripJson);
7431
- }
7432
7782
  for (const binding of message.laneSidechains ?? []) {
7433
7783
  this.engine.setLaneSidechain(binding.trackId, binding.insertIndex, binding.sourceTrackId);
7434
7784
  }
@@ -7924,6 +8274,9 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
7924
8274
  this.engine.setTrackMonitorMode(laneIndex, mode, sampleTime);
7925
8275
  break;
7926
8276
  }
8277
+ case 28 /* ResetMasterLoudnessMeter */:
8278
+ this.engine.resetMasterLoudnessMeter(sampleTime);
8279
+ break;
7927
8280
  default:
7928
8281
  this.publishTelemetryRecord({
7929
8282
  type: 1 /* Error */,
@@ -8026,6 +8379,8 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
8026
8379
  for (let field = 0; field < 11; field++) {
8027
8380
  ring.records[offset + 3 + field] = this.engine.meterScratchValue(field);
8028
8381
  }
8382
+ ring.records[offset + 14] = this.engine.meterScratchInputPeakDbL();
8383
+ ring.records[offset + 15] = this.engine.meterScratchInputPeakDbR();
8029
8384
  Atomics.store(ring.header, 0, writeIndex + 1);
8030
8385
  }
8031
8386
  // A snapshot-object ring writer used to live here as well. It was
@@ -8041,17 +8396,53 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
8041
8396
  // silent overrun via firstReadable = max(readIndex, writeIndex - capacity),
8042
8397
  // so header slot 3 is left at its initial 0.
8043
8398
  // Drains the engine's scope producer (FFT spectrum + goniometer points) into
8044
- // the lock-free SAB scope ring. No allocation on the render path: records are
8045
- // written field-by-field into the ring.
8399
+ // the lock-free SAB scope ring or the bounded postMessage fallback. The SAB
8400
+ // path remains allocation-free; postMessage materialises only records that
8401
+ // are ready to publish.
8046
8402
  publishScope() {
8047
- const ring = this.scopeRing;
8048
- if (!ring) {
8403
+ if (!this.scopeEnabled) {
8049
8404
  return;
8050
8405
  }
8406
+ const ring = this.scopeRing;
8407
+ const transport = this.transport;
8051
8408
  for (let count = 0; count < 64 && this.engine.popScopeTelemetryToScratch(); count++) {
8052
- this.writeScopeScratch(ring);
8409
+ if (ring) {
8410
+ this.writeScopeScratch(ring);
8411
+ } else {
8412
+ this.postScopeScratch(transport);
8413
+ }
8053
8414
  }
8054
8415
  }
8416
+ postScopeScratch(transport) {
8417
+ if (!transport?.postMessage) {
8418
+ return;
8419
+ }
8420
+ const bandCount = Math.min(
8421
+ SONARE_SCOPE_MAX_BANDS,
8422
+ Math.max(0, Math.trunc(Number(this.engine.scopeScratchBandCount())))
8423
+ );
8424
+ const pointCount = Math.min(
8425
+ SONARE_SCOPE_MAX_POINTS,
8426
+ Math.max(0, Math.trunc(Number(this.engine.scopeScratchPointCount())))
8427
+ );
8428
+ const bands = new Float32Array(bandCount);
8429
+ for (let i = 0; i < bandCount; i++) {
8430
+ bands[i] = this.engine.scopeScratchBand(i);
8431
+ }
8432
+ const points = new Float32Array(pointCount * 2);
8433
+ for (let i = 0; i < pointCount; i++) {
8434
+ points[2 * i] = this.engine.scopeScratchPointLeft(i);
8435
+ points[2 * i + 1] = this.engine.scopeScratchPointRight(i);
8436
+ }
8437
+ const scope = {
8438
+ type: "scope",
8439
+ targetId: this.engine.scopeScratchTargetId(),
8440
+ frame: Number(this.engine.scopeScratchRenderFrame()),
8441
+ bands,
8442
+ points
8443
+ };
8444
+ transport.postMessage(scope, [bands.buffer, points.buffer]);
8445
+ }
8055
8446
  writeScopeScratch(ring) {
8056
8447
  const writeIndex = Atomics.load(ring.header, 0);
8057
8448
  const base = writeIndex % ring.capacity * ring.recordFloats;
@@ -8352,8 +8743,8 @@ var SonareWorkletProcessor = class {
8352
8743
  *
8353
8744
  * `targetId` is always the master and the four LUFS / gain-reduction fields
8354
8745
  * are always unavailable here — the mixer worklet does not run the
8355
- * K-weighting filters, and a floor value would read as silence — so both are
8356
- * set once rather than per interval.
8746
+ * K-weighting filters or the engine's pre-trim input tap — so finite floor
8747
+ * values are set once rather than per interval.
8357
8748
  */
8358
8749
  this.meterScratch = {
8359
8750
  type: "meter",
@@ -8369,7 +8760,9 @@ var SonareWorkletProcessor = class {
8369
8760
  momentaryLufs: Number.NaN,
8370
8761
  shortTermLufs: Number.NaN,
8371
8762
  integratedLufs: Number.NaN,
8372
- gainReductionDb: Number.NaN
8763
+ gainReductionDb: Number.NaN,
8764
+ inputPeakDbL: SONARE_FLOOR_DB,
8765
+ inputPeakDbR: SONARE_FLOOR_DB
8373
8766
  };
8374
8767
  if (!options.sceneJson) {
8375
8768
  throw new Error("sceneJson is required.");
@@ -8532,7 +8925,9 @@ var SonareWorkletProcessor = class {
8532
8925
  momentaryLufs: Number.NaN,
8533
8926
  shortTermLufs: Number.NaN,
8534
8927
  integratedLufs: Number.NaN,
8535
- gainReductionDb: Number.NaN
8928
+ gainReductionDb: Number.NaN,
8929
+ inputPeakDbL: SONARE_FLOOR_DB,
8930
+ inputPeakDbR: SONARE_FLOOR_DB
8536
8931
  };
8537
8932
  if (this.transport?.onMeter) {
8538
8933
  this.transport.onMeter(meter);
@@ -8561,6 +8956,8 @@ var SonareWorkletProcessor = class {
8561
8956
  ring.records[offset + 11] = meter.shortTermLufs;
8562
8957
  ring.records[offset + 12] = meter.integratedLufs;
8563
8958
  ring.records[offset + 13] = meter.gainReductionDb;
8959
+ ring.records[offset + 14] = meter.inputPeakDbL;
8960
+ ring.records[offset + 15] = meter.inputPeakDbR;
8564
8961
  Atomics.store(ring.header, 0, writeIndex + 1);
8565
8962
  }
8566
8963
  publishSpectrum(left, right) {
@@ -8613,7 +9010,7 @@ var SonareWorkletProcessor = class {
8613
9010
  return;
8614
9011
  }
8615
9012
  const writeIndex = Atomics.load(ring.header, 0);
8616
- const offset = writeIndex % ring.capacity * ring.recordFloats;
9013
+ const offset = writeIndex % ring.capacity * SONARE_METER_RING_RECORD_FLOATS;
8617
9014
  ring.records[offset] = encodeFrameLo(frame);
8618
9015
  ring.records[offset + 1] = encodeFrameHi(frame);
8619
9016
  ring.records[offset + 2] = bands.length;