@libraz/libsonare 1.8.0 → 1.8.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.
Files changed (138) hide show
  1. package/README.md +5 -5
  2. package/dist/_effects_common.d.ts +2 -1
  3. package/dist/_effects_common.d.ts.map +1 -1
  4. package/dist/analysis.js +3 -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 +11 -5
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +2098 -155
  17. package/dist/index.js.map +1 -1
  18. package/dist/instrument_types.d.ts +27 -3
  19. package/dist/instrument_types.d.ts.map +1 -1
  20. package/dist/mastering_core.d.ts +30 -3
  21. package/dist/mastering_core.d.ts.map +1 -1
  22. package/dist/mixer.d.ts +2 -2
  23. package/dist/mixer.d.ts.map +1 -1
  24. package/dist/project.d.ts +5 -4
  25. package/dist/project.d.ts.map +1 -1
  26. package/dist/project_class.d.ts +57 -3
  27. package/dist/project_class.d.ts.map +1 -1
  28. package/dist/project_internal.d.ts +35 -1
  29. package/dist/project_internal.d.ts.map +1 -1
  30. package/dist/project_types.d.ts +17 -6
  31. package/dist/project_types.d.ts.map +1 -1
  32. package/dist/public_types.d.ts +1 -0
  33. package/dist/public_types.d.ts.map +1 -1
  34. package/dist/public_types_mastering.d.ts +19 -1
  35. package/dist/public_types_mastering.d.ts.map +1 -1
  36. package/dist/public_types_playback.d.ts +1 -0
  37. package/dist/public_types_playback.d.ts.map +1 -1
  38. package/dist/public_types_spectral.d.ts +5 -5
  39. package/dist/public_types_spectral.d.ts.map +1 -1
  40. package/dist/public_types_vocal_edit.d.ts +250 -0
  41. package/dist/public_types_vocal_edit.d.ts.map +1 -0
  42. package/dist/public_types_vocal_project.d.ts +63 -0
  43. package/dist/public_types_vocal_project.d.ts.map +1 -0
  44. package/dist/realtime_engine.d.ts +106 -16
  45. package/dist/realtime_engine.d.ts.map +1 -1
  46. package/dist/repair_dereverb.d.ts +1 -0
  47. package/dist/repair_dereverb.d.ts.map +1 -1
  48. package/dist/repair_noise.d.ts +1 -0
  49. package/dist/repair_noise.d.ts.map +1 -1
  50. package/dist/sonare-analysis.wasm +0 -0
  51. package/dist/sonare.d.ts +89 -0
  52. package/dist/sonare.js +1 -1
  53. package/dist/sonare.wasm +0 -0
  54. package/dist/streaming_processors.d.ts +5 -0
  55. package/dist/streaming_processors.d.ts.map +1 -1
  56. package/dist/vocal_edit.d.ts +131 -0
  57. package/dist/vocal_edit.d.ts.map +1 -0
  58. package/dist/vocal_edit_worker.d.ts +51 -0
  59. package/dist/vocal_edit_worker.d.ts.map +1 -0
  60. package/dist/vocal_edit_worker.js +1462 -0
  61. package/dist/vocal_edit_worker.js.map +1 -0
  62. package/dist/vocal_edit_worker_client.d.ts +118 -0
  63. package/dist/vocal_edit_worker_client.d.ts.map +1 -0
  64. package/dist/vocal_edit_worker_protocol.d.ts +139 -0
  65. package/dist/vocal_edit_worker_protocol.d.ts.map +1 -0
  66. package/dist/vocal_project.d.ts +17 -0
  67. package/dist/vocal_project.d.ts.map +1 -0
  68. package/dist/web_midi.d.ts +4 -0
  69. package/dist/web_midi.d.ts.map +1 -1
  70. package/dist/worker.js.map +1 -1
  71. package/dist/worklet/engine-mixer-facade.d.ts +42 -5
  72. package/dist/worklet/engine-mixer-facade.d.ts.map +1 -1
  73. package/dist/worklet/engine-node.d.ts +10 -0
  74. package/dist/worklet/engine-node.d.ts.map +1 -1
  75. package/dist/worklet/engine-processor.d.ts +6 -1
  76. package/dist/worklet/engine-processor.d.ts.map +1 -1
  77. package/dist/worklet/engine-register.d.ts.map +1 -1
  78. package/dist/worklet/engine-strips.d.ts +8 -4
  79. package/dist/worklet/engine-strips.d.ts.map +1 -1
  80. package/dist/worklet/engine-sync.d.ts +4 -2
  81. package/dist/worklet/engine-sync.d.ts.map +1 -1
  82. package/dist/worklet/engine.d.ts +42 -9
  83. package/dist/worklet/engine.d.ts.map +1 -1
  84. package/dist/worklet/guards.d.ts +10 -2
  85. package/dist/worklet/guards.d.ts.map +1 -1
  86. package/dist/worklet/messages.d.ts +54 -9
  87. package/dist/worklet/messages.d.ts.map +1 -1
  88. package/dist/worklet/mixer-processor.d.ts +2 -2
  89. package/dist/worklet/mixer-processor.d.ts.map +1 -1
  90. package/dist/worklet/protocol.d.ts +20 -2
  91. package/dist/worklet/protocol.d.ts.map +1 -1
  92. package/dist/worklet.d.ts +1 -1
  93. package/dist/worklet.d.ts.map +1 -1
  94. package/dist/worklet.js +808 -204
  95. package/dist/worklet.js.map +1 -1
  96. package/package.json +6 -1
  97. package/src/_effects_common.ts +36 -6
  98. package/src/effects_mastering.ts +4 -0
  99. package/src/effects_note_ops.ts +92 -44
  100. package/src/effects_timepitch.ts +42 -31
  101. package/src/feature_decompose.ts +2 -2
  102. package/src/index.ts +39 -0
  103. package/src/instrument_types.ts +29 -3
  104. package/src/mastering_core.ts +98 -2
  105. package/src/mixer.ts +41 -21
  106. package/src/project.ts +8 -1
  107. package/src/project_class.ts +136 -1
  108. package/src/project_internal.ts +90 -0
  109. package/src/project_types.ts +18 -6
  110. package/src/public_types.ts +1 -0
  111. package/src/public_types_mastering.ts +23 -0
  112. package/src/public_types_playback.ts +1 -0
  113. package/src/public_types_spectral.ts +5 -5
  114. package/src/public_types_vocal_edit.ts +289 -0
  115. package/src/public_types_vocal_project.ts +69 -0
  116. package/src/realtime_engine.ts +157 -24
  117. package/src/repair_dereverb.ts +1 -0
  118. package/src/repair_noise.ts +1 -0
  119. package/src/sonare.js.d.ts +89 -0
  120. package/src/streaming_processors.ts +8 -0
  121. package/src/vocal_edit.ts +885 -0
  122. package/src/vocal_edit_worker.ts +832 -0
  123. package/src/vocal_edit_worker_client.ts +812 -0
  124. package/src/vocal_edit_worker_protocol.ts +206 -0
  125. package/src/vocal_project.ts +327 -0
  126. package/src/web_midi.ts +100 -5
  127. package/src/worklet/engine-mixer-facade.ts +422 -87
  128. package/src/worklet/engine-node.ts +69 -4
  129. package/src/worklet/engine-processor.ts +129 -34
  130. package/src/worklet/engine-register.ts +3 -0
  131. package/src/worklet/engine-strips.ts +36 -14
  132. package/src/worklet/engine-sync.ts +6 -1
  133. package/src/worklet/engine.ts +76 -40
  134. package/src/worklet/guards.ts +81 -0
  135. package/src/worklet/messages.ts +63 -4
  136. package/src/worklet/mixer-processor.ts +10 -3
  137. package/src/worklet/protocol.ts +39 -8
  138. package/src/worklet.ts +2 -0
@@ -23,9 +23,11 @@ import type {
23
23
  PanLaw,
24
24
  PanMode,
25
25
  SidechainSourceKind,
26
+ UmpWords,
26
27
  } from '../index';
27
28
  import { RealtimeEngine } from '../index';
28
29
  import { createOpfsClipPageProvider, type OpfsClipPageProviderBinding } from '../opfs_clip_pages';
30
+ import type { SurroundPan } from '../public_types';
29
31
  import type { ClipPageProvider } from '../realtime_engine';
30
32
  import type { EngineAutomationContext } from './engine-automation';
31
33
  import * as automation from './engine-automation';
@@ -102,6 +104,7 @@ export class SonareEngine {
102
104
  private readonly trackLaneIds: number[] = [];
103
105
  private readonly trackSends = new Map<number, EngineTrackSend[]>();
104
106
  private readonly trackOutputBus = new Map<number, number>();
107
+ private readonly trackSourceChannelLayout = new Map<number, number>();
105
108
  private readonly laneSidechains = new Map<
106
109
  string,
107
110
  { trackId: number; insertIndex: number; sourceTrackId: number }
@@ -242,6 +245,11 @@ export class SonareEngine {
242
245
  tempo.setTimeSignatureSegments(this.tempoContext, segments);
243
246
  }
244
247
 
248
+ /**
249
+ * A loop wrap, seek or stop sends note-offs plus CC64=0, CC121, CC123 and a
250
+ * centred pitch bend on every channel played since the last reset, so
251
+ * controller values set before a loop region are not restored at the wrap.
252
+ */
245
253
  setLoop(startPpq: number, endPpq: number, enabled = true): boolean {
246
254
  return tempo.setLoop(this.tempoContext, startPpq, endPpq, enabled);
247
255
  }
@@ -321,6 +329,15 @@ export class SonareEngine {
321
329
  * mixer lane first (like automationParamId) so the offline engine resolves the
322
330
  * same strip selector the realtime engine uses.
323
331
  *
332
+ * The returned id uses the track's current positional lane selector. When
333
+ * setTrackLanes successfully changes lane order or membership, the engine
334
+ * remaps already queued and published track automation by track id, but it
335
+ * cannot update a numeric id retained by the caller. Re-resolve every track
336
+ * insert id after such a topology change before passing it to
337
+ * setAutomationLane. Use setTrackStripInsertParamByName when the operation
338
+ * needs a stable track identity. Master and bus insert ids are separate and
339
+ * are not invalidated by track-lane changes.
340
+ *
324
341
  * @param target Track id (declares a mixer lane on first use).
325
342
  * @param insertIndex Index into the strip's combined insert sequence.
326
343
  * @param paramName Processor JSON-key parameter name.
@@ -454,14 +471,24 @@ export class SonareEngine {
454
471
  return sent;
455
472
  }
456
473
 
474
+ /** Resets the master's integrated loudness accumulator in both engine mirrors. */
475
+ resetMasterLoudnessMeter(renderFrame = -1): boolean {
476
+ this.offlineEngine.resetMasterLoudnessMeter(renderFrame);
477
+ return this.sendMirroredCommand({
478
+ type: SonareEngineCommandType.ResetMasterLoudnessMeter,
479
+ sampleTime: renderFrame,
480
+ });
481
+ }
482
+
457
483
  /**
458
484
  * Declares the mixer track lanes in an explicit order.
459
485
  *
460
- * Lane indices are append-only: once a track id occupies a lane, its index
461
- * stays fixed for the engine's lifetime. The given list must therefore start
462
- * with the already-declared lane ids in their current order and may only
463
- * append new track ids after them. Entries carrying `sends` replace that
464
- * track's send list; entries without `sends` leave existing sends untouched.
486
+ * A successful call may reorder existing track ids, remove track ids, or add
487
+ * new ones. Existing queued and published track automation follows the track
488
+ * id, while numeric track-insert automation ids retained by the caller do
489
+ * not; re-resolve those ids after every topology change. Entries carrying
490
+ * `sends` replace that track's send list; entries without `sends` leave
491
+ * existing sends untouched.
465
492
  *
466
493
  * @param lanes Track ids or lane descriptors in the desired lane order.
467
494
  */
@@ -480,7 +507,9 @@ export class SonareEngine {
480
507
  /**
481
508
  * Keys one insert of a lane strip from another lane's post-strip pre-fader
482
509
  * audio (ducking/sidechainRouter inserts). sourceTarget null removes the
483
- * binding.
510
+ * binding. Throws, leaving the existing bindings unchanged, for a lane keying
511
+ * itself or a binding that would close a cycle over the lane bindings; a
512
+ * refused binding is never recorded, so it is not replayed on resync.
484
513
  */
485
514
  setLaneSidechain(
486
515
  target: string | number,
@@ -528,19 +557,18 @@ export class SonareEngine {
528
557
  }
529
558
 
530
559
  setTrackStripJson(target: string | number, sceneJson: string): void {
560
+ // Parse before declaring a new lane so malformed text cannot declare one.
561
+ mixer.decodedInsertSignature(sceneJson, { kind: 'track', trackId: 0 });
531
562
  const laneIndex = this.ensureTrackLane(target);
532
563
  const trackId = this.trackLaneIds[laneIndex];
533
- const { resetInserts } = strips.setTrackStripJson(
564
+ const { insertBaseResets, constructionOverrides } = strips.setTrackStripJson(
534
565
  this.stripContext,
535
566
  trackId,
536
567
  sceneJson,
537
568
  this.trackStripJson,
538
569
  );
539
- if (resetInserts) {
540
- this.clearInsertAutomationLanes({ kind: 'track', trackId });
541
- }
542
570
  mixer.pruneStripSidechains(this.mixerContext, { kind: 'track', trackId }, sceneJson);
543
- this.syncMixer(resetInserts ? [{ kind: 'track', trackId }] : []);
571
+ this.syncMixer(insertBaseResets, constructionOverrides);
544
572
  }
545
573
 
546
574
  setTrackStripEqBand(target: string | number, bandIndex: number, band: EqBand | string): void {
@@ -587,6 +615,15 @@ export class SonareEngine {
587
615
  strips.setTrackStripDualPan(this.stripContext, target, leftPan, rightPan);
588
616
  }
589
617
 
618
+ /**
619
+ * Sets a track lane strip's surround placement (glitch-free, 5 ms placement
620
+ * glide). Takes effect on a destination wider than stereo, so set `channelCount`
621
+ * to the layout's plane count. Omitted fields default to a centered point source.
622
+ */
623
+ setTrackStripSurroundPan(target: string | number, pan: SurroundPan): void {
624
+ strips.setTrackStripSurroundPan(this.stripContext, target, pan);
625
+ }
626
+
590
627
  setTrackStripChannelDelaySamples(target: string | number, delaySamples: number): void {
591
628
  strips.setTrackStripChannelDelaySamples(this.stripContext, target, delaySamples);
592
629
  }
@@ -625,18 +662,7 @@ export class SonareEngine {
625
662
  }
626
663
 
627
664
  setMasterStripJson(sceneJson: string): void {
628
- const target = { kind: 'master' } as const;
629
- const resetInserts =
630
- (this.masterStripJson !== undefined && this.masterStripJson !== sceneJson) ||
631
- mixer.hasInsertParamOverrides(this.insertParamOverrides, target);
632
- mixer.applyFullStripJson(this.offlineEngine, target, sceneJson, resetInserts);
633
- if (resetInserts) {
634
- this.clearInsertAutomationLanes(target);
635
- }
636
- mixer.pruneStripSidechains(this.mixerContext, target, sceneJson);
637
- mixer.clearInsertParamOverrides(this.insertParamOverrides, target);
638
- this.masterStripJson = sceneJson;
639
- this.syncMixer(resetInserts ? [target] : []);
665
+ mixer.setMasterStripJson(this.mixerContext, sceneJson);
640
666
  }
641
667
 
642
668
  setMasterStripEqBand(bandIndex: number, band: EqBand | string): void {
@@ -904,7 +930,10 @@ export class SonareEngine {
904
930
  /**
905
931
  * Route a track's MIDI to the external output (drained via {@link onMidiOut})
906
932
  * instead of an internal instrument, so the track plays an external device.
907
- * Pass `external=false` to restore internal-synth playback.
933
+ * Pass `external=false` to restore internal-synth playback. The change takes
934
+ * effect at the next processed block; switching a destination's route first
935
+ * releases its notes and resets its controllers through the old route, and
936
+ * drops its pending MIDI-FX events.
908
937
  */
909
938
  setMidiDestinationExternal(trackId: string | number, external: boolean): void {
910
939
  strips.setMidiDestinationExternal(this.stripContext, trackId, external);
@@ -1015,8 +1044,8 @@ export class SonareEngine {
1015
1044
  );
1016
1045
  }
1017
1046
 
1018
- pushMidiUmp(trackId: string | number, word0: number, renderFrame = -1): void {
1019
- strips.pushMidiUmp(this.stripContext, trackId, word0, renderFrame);
1047
+ pushMidiUmp(trackId: string | number, words: number | UmpWords, renderFrame = -1): void {
1048
+ strips.pushMidiUmp(this.stripContext, trackId, words, renderFrame);
1020
1049
  }
1021
1050
 
1022
1051
  pushMidiSysex(trackId: string | number, data: Uint8Array, renderFrame = -1): void {
@@ -1151,6 +1180,12 @@ export class SonareEngine {
1151
1180
  });
1152
1181
  }
1153
1182
 
1183
+ pushMidiInputUmp(words: UmpWords, portTimeSamples = 0): void {
1184
+ const copiedWords = words instanceof Uint32Array ? new Uint32Array(words) : words.slice();
1185
+ this.offlineEngine.pushMidiInputUmp(copiedWords, portTimeSamples);
1186
+ this.postSync({ type: 'syncMidiInputUmp', words: copiedWords, portTimeSamples });
1187
+ }
1188
+
1154
1189
  pushMidiPanic(renderFrame = -1): void {
1155
1190
  this.offlineEngine.pushMidiPanic(renderFrame);
1156
1191
  this.postSync({ type: 'syncMidiPanic', renderFrame });
@@ -1272,6 +1307,11 @@ export class SonareEngine {
1272
1307
  return this.realtimeNode.pollScope();
1273
1308
  }
1274
1309
 
1310
+ /** Per-insert gain reduction for a meter target; see {@link SonareRealtimeEngineNode.pollInsertGainReduction}. */
1311
+ pollInsertGainReduction(targetId: number): Promise<number[]> {
1312
+ return this.realtimeNode.pollInsertGainReduction(targetId);
1313
+ }
1314
+
1275
1315
  destroy(): void {
1276
1316
  if (this.destroyed) {
1277
1317
  return;
@@ -1397,8 +1437,11 @@ export class SonareEngine {
1397
1437
  return mixer.mixerLanes(this.mixerContext);
1398
1438
  }
1399
1439
 
1400
- private syncMixer(forceInsertResets: mixer.StripJsonTarget[] = []): void {
1401
- mixer.syncMixer(this.mixerContext, false, forceInsertResets);
1440
+ private syncMixer(
1441
+ insertBaseResets: mixer.StripJsonTarget[] = [],
1442
+ oneShotInsertParamOverrides: mixer.InsertParamOverride[] = [],
1443
+ ): void {
1444
+ mixer.syncMixer(this.mixerContext, false, insertBaseResets, oneShotInsertParamOverrides);
1402
1445
  }
1403
1446
 
1404
1447
  private insertAutomationTargetKey(target: mixer.StripJsonTarget): string {
@@ -1494,6 +1537,7 @@ export class SonareEngine {
1494
1537
  trackLaneIds: this.trackLaneIds,
1495
1538
  trackSends: this.trackSends,
1496
1539
  trackOutputBus: this.trackOutputBus,
1540
+ trackSourceChannelLayout: this.trackSourceChannelLayout,
1497
1541
  laneSidechains: this.laneSidechains,
1498
1542
  busSidechains: this.busSidechains,
1499
1543
  masterSidechains: this.masterSidechains,
@@ -1506,7 +1550,8 @@ export class SonareEngine {
1506
1550
  ensureTrackLane: (target) => this.ensureTrackLane(target),
1507
1551
  ensureBus: (busId) => this.ensureBus(busId),
1508
1552
  mixerLanes: () => this.mixerLanes(),
1509
- syncMixer: (forceInsertResets) => this.syncMixer(forceInsertResets),
1553
+ syncMixer: (insertBaseResets, oneShotInsertParamOverrides) =>
1554
+ this.syncMixer(insertBaseResets, oneShotInsertParamOverrides),
1510
1555
  clearInsertAutomationLanes: (target, forgetResolvedIds) =>
1511
1556
  this.clearInsertAutomationLanes(target, forgetResolvedIds),
1512
1557
  sendSmoothedParam: (paramId, value) => this.sendSmoothedParam(paramId, value),
@@ -1532,6 +1577,7 @@ export class SonareEngine {
1532
1577
  writeStripJson: (target, sceneJson) =>
1533
1578
  mixer.cacheStripJson(this.mixerContext, target, sceneJson),
1534
1579
  insertParamOverrides: this.insertParamOverrides,
1580
+ clearInsertAutomationLanes: (target) => this.clearInsertAutomationLanes(target),
1535
1581
  };
1536
1582
  }
1537
1583
 
@@ -1683,17 +1729,7 @@ export class SonareEngine {
1683
1729
  }
1684
1730
 
1685
1731
  private ensureTrackLane(target: string | number): number {
1686
- const trackId = this.resolveTargetId(target);
1687
- if (!Number.isInteger(trackId) || trackId <= 0) {
1688
- throw new RangeError(`Invalid track id for mixer lane: ${String(target)}`);
1689
- }
1690
- const existing = this.trackLaneIds.indexOf(trackId);
1691
- if (existing >= 0) {
1692
- return existing;
1693
- }
1694
- this.trackLaneIds.push(trackId);
1695
- this.syncMixer();
1696
- return this.trackLaneIds.length - 1;
1732
+ return mixer.ensureTrackLane(this.mixerContext, target);
1697
1733
  }
1698
1734
 
1699
1735
  private ensureBus(busId: number): number {
@@ -3,6 +3,8 @@ import type {
3
3
  SonareEngineCaptureRequestMessage,
4
4
  SonareEngineCaptureResponseMessageInternal,
5
5
  SonareEngineClipPageRequestMessage,
6
+ SonareEngineInsertGainReductionRequestMessage,
7
+ SonareEngineInsertGainReductionResponseMessage,
6
8
  SonareEngineSyncMessage,
7
9
  SonareEngineTransportRequestMessage,
8
10
  SonareEngineTransportResponseMessage,
@@ -13,9 +15,11 @@ import type {
13
15
  } from './messages';
14
16
  import {
15
17
  isRecord,
18
+ SONARE_SCOPE_DEFAULT_BANDS,
16
19
  type SonareEngineCommandRecord,
17
20
  type SonareEngineTelemetryRecord,
18
21
  type SonareWorkletMeterSnapshot,
22
+ type SonareWorkletScopeSnapshot,
19
23
  } from './protocol';
20
24
 
21
25
  export function isWorkletMessage(value: unknown): value is SonareWorkletMessage {
@@ -84,6 +88,7 @@ export const ENGINE_SYNC_MESSAGE_TYPES: Record<SonareEngineSyncMessage['type'],
84
88
  syncMidiInputPitchBend: true,
85
89
  syncMidiInputPolyPressure: true,
86
90
  syncMidiInputSource: true,
91
+ syncMidiInputUmp: true,
87
92
  syncMidiNoteOff: true,
88
93
  syncMidiNoteOn: true,
89
94
  syncMidiPanic: true,
@@ -104,6 +109,7 @@ export const ENGINE_SYNC_MESSAGE_TYPES: Record<SonareEngineSyncMessage['type'],
104
109
  syncTrackStripPan: true,
105
110
  syncTrackStripPanLaw: true,
106
111
  syncTrackStripPanMode: true,
112
+ syncTrackStripSurroundPan: true,
107
113
  syncWarpVoiceCapacity: true,
108
114
  };
109
115
 
@@ -252,6 +258,28 @@ export function isEngineTransportResponseMessage(
252
258
  );
253
259
  }
254
260
 
261
+ export function isEngineInsertGainReductionRequestMessage(
262
+ value: unknown,
263
+ ): value is SonareEngineInsertGainReductionRequestMessage {
264
+ return (
265
+ isRecord(value) &&
266
+ value.type === 'insertGainReductionRequest' &&
267
+ typeof value.requestId === 'number' &&
268
+ typeof value.targetId === 'number'
269
+ );
270
+ }
271
+
272
+ export function isEngineInsertGainReductionResponseMessage(
273
+ value: unknown,
274
+ ): value is SonareEngineInsertGainReductionResponseMessage {
275
+ return (
276
+ isRecord(value) &&
277
+ value.type === 'insertGainReductionResponse' &&
278
+ typeof value.requestId === 'number' &&
279
+ typeof value.ok === 'boolean'
280
+ );
281
+ }
282
+
255
283
  export function isRealtimeVoiceChangerMessage(
256
284
  value: unknown,
257
285
  ): value is SonareRealtimeVoiceChangerMessage {
@@ -331,10 +359,55 @@ export function isMeterSnapshot(value: unknown): value is SonareWorkletMeterSnap
331
359
  typeof value.rmsDbL === 'number' &&
332
360
  typeof value.rmsDbR === 'number' &&
333
361
  typeof value.correlation === 'number' &&
362
+ Number.isFinite(value.inputPeakDbL) &&
363
+ Number.isFinite(value.inputPeakDbR) &&
334
364
  (typeof value.targetId === 'number' || value.targetId === undefined)
335
365
  );
336
366
  }
337
367
 
368
+ export const SONARE_SCOPE_MAX_BANDS = 64;
369
+ export const SONARE_SCOPE_MAX_POINTS = 32;
370
+
371
+ function isPlainFloat32Array(
372
+ value: unknown,
373
+ maxLength: number,
374
+ evenLength = false,
375
+ ): value is Float32Array {
376
+ if (
377
+ !(value instanceof Float32Array) ||
378
+ typeof ArrayBuffer === 'undefined' ||
379
+ !(value.buffer instanceof ArrayBuffer) ||
380
+ value.length > maxLength ||
381
+ (evenLength && value.length % 2 !== 0)
382
+ ) {
383
+ return false;
384
+ }
385
+ for (const sample of value) {
386
+ if (!Number.isFinite(sample)) {
387
+ return false;
388
+ }
389
+ }
390
+ return true;
391
+ }
392
+
393
+ /** Validates the bounded postMessage representation of a scope snapshot. */
394
+ export function isScopeSnapshot(value: unknown): value is SonareWorkletScopeSnapshot {
395
+ return (
396
+ isRecord(value) &&
397
+ value.type === 'scope' &&
398
+ typeof value.targetId === 'number' &&
399
+ Number.isSafeInteger(value.targetId) &&
400
+ value.targetId >= 0 &&
401
+ value.targetId <= 0xffff_ffff &&
402
+ typeof value.frame === 'number' &&
403
+ Number.isSafeInteger(value.frame) &&
404
+ value.frame >= 0 &&
405
+ isPlainFloat32Array(value.bands, SONARE_SCOPE_MAX_BANDS) &&
406
+ value.bands.length > 0 &&
407
+ isPlainFloat32Array(value.points, SONARE_SCOPE_MAX_POINTS * 2, true)
408
+ );
409
+ }
410
+
338
411
  /**
339
412
  * Resolves an integer option, refusing anything the field's own domain cannot
340
413
  * hold instead of rounding it into range.
@@ -356,6 +429,14 @@ export function requireIntegerOption(
356
429
  return resolved;
357
430
  }
358
431
 
432
+ /** Resolves scope bands and applies the native tap's fixed 64-band ceiling. */
433
+ export function resolveScopeBandCount(
434
+ value: number | undefined,
435
+ fallback = SONARE_SCOPE_DEFAULT_BANDS,
436
+ ): number {
437
+ return Math.min(requireIntegerOption(value, fallback, 'scopeBands', 1), SONARE_SCOPE_MAX_BANDS);
438
+ }
439
+
359
440
  /** As {@link requireIntegerOption}, for a field whose domain admits either sign. */
360
441
  export function requireInteger(value: number | undefined, fallback: number, name: string): number {
361
442
  const resolved = value ?? fallback;
@@ -15,12 +15,14 @@ import type {
15
15
  PlaybackRendererConfig,
16
16
  RealtimeVoiceChangerConfigInput,
17
17
  RealtimeVoiceChangerPodConfig,
18
+ UmpWords,
18
19
  } from '../index';
19
- import type { AutomationCurve } from '../public_types';
20
+ import type { AutomationCurve, SurroundPan } from '../public_types';
20
21
  import type {
21
22
  SonareEngineCommandRecord,
22
23
  SonareEngineTelemetryRecord,
23
24
  SonareWorkletMeterSnapshot,
25
+ SonareWorkletScopeSnapshot,
24
26
  SonareWorkletSpectrumSnapshot,
25
27
  } from './protocol';
26
28
 
@@ -44,6 +46,18 @@ export interface SonareRealtimeEngineWorkletProcessorOptions {
44
46
  initialCommands?: SonareEngineCommandRecord[];
45
47
  sampleRate?: number;
46
48
  blockSize?: number;
49
+ /**
50
+ * Output plane count (default 2). Planes follow the canonical order
51
+ * L R C LFE Ls Rs [Lss Rss] for 6 (5.1) and 8 (7.1) planes.
52
+ *
53
+ * The Web Audio default destination is 2-channel `'speakers'`, so a wider
54
+ * output is mixed down to stereo unless the app raises
55
+ * `audioContext.destination.channelCount` (up to its `maxChannelCount`) to
56
+ * match. Interpretation matters only while the counts differ: `'speakers'`
57
+ * applies the spec's down-mix for mono, stereo, quad and 5.1 sources only,
58
+ * and `'discrete'` copies the first planes and drops the rest. With equal
59
+ * counts the planes pass through unchanged in either mode.
60
+ */
47
61
  channelCount?: number;
48
62
  meterIntervalFrames?: number;
49
63
  commandSharedBuffer?: SharedArrayBuffer;
@@ -288,6 +302,7 @@ export interface SonareEngineSyncErrorMessage {
288
302
 
289
303
  export type SonareWorkletTransportMessage =
290
304
  | SonareWorkletMeterSnapshot
305
+ | SonareWorkletScopeSnapshot
291
306
  | SonareWorkletSpectrumSnapshot
292
307
  | SonareWorkletExternalMidiMessage
293
308
  | SonareEngineClipPageRequestMessage
@@ -299,6 +314,7 @@ export interface WorkletTransport {
299
314
  | SonareWorkletTransportMessage
300
315
  | SonareEngineCaptureResponseMessageInternal
301
316
  | SonareEngineTransportResponseMessage
317
+ | SonareEngineInsertGainReductionResponseMessage
302
318
  | SonareEngineSyncErrorMessage,
303
319
  transfer?: Transferable[],
304
320
  ) => void;
@@ -511,8 +527,8 @@ export interface SonareEngineSyncMixerMessage {
511
527
  masterStripJson?: string;
512
528
  /** Retained by-name insert values, applied after strip and sidechain replay. */
513
529
  insertParamOverrides?: SonareEngineSyncMixerInsertParamOverride[];
514
- /** Explicit full strip replacements that must reset retained insert state. */
515
- forceInsertResets?: Array<
530
+ /** Strips whose manual insert bases are cleared after the strip replay, before insertParamOverrides. */
531
+ insertBaseResets?: Array<
516
532
  { kind: 'track'; trackId: number } | { kind: 'bus'; busId: number } | { kind: 'master' }
517
533
  >;
518
534
  /** Lane insert sidechain bindings (replayed after lanes/strips). */
@@ -631,6 +647,12 @@ export interface SonareEngineSyncTrackStripDualPanMessage {
631
647
  rightPan: number;
632
648
  }
633
649
 
650
+ export interface SonareEngineSyncTrackStripSurroundPanMessage {
651
+ type: 'syncTrackStripSurroundPan';
652
+ trackId: number;
653
+ pan: Required<SurroundPan>;
654
+ }
655
+
634
656
  export interface SonareEngineSyncBusStripPanMessage {
635
657
  type: 'syncBusStripPan';
636
658
  busId: number;
@@ -719,13 +741,26 @@ export interface SonareEngineSyncMidiCcMessage {
719
741
  renderFrame: number;
720
742
  }
721
743
 
722
- export interface SonareEngineSyncMidiUmpMessage {
744
+ export interface SonareEngineSyncMidiUmpWordMessage {
723
745
  type: 'syncMidiUmp';
724
746
  destinationId: number;
725
747
  word0: number;
748
+ words?: never;
726
749
  renderFrame: number;
727
750
  }
728
751
 
752
+ export interface SonareEngineSyncMidiUmpWordsMessage {
753
+ type: 'syncMidiUmp';
754
+ destinationId: number;
755
+ word0?: never;
756
+ words: UmpWords;
757
+ renderFrame: number;
758
+ }
759
+
760
+ export type SonareEngineSyncMidiUmpMessage =
761
+ | SonareEngineSyncMidiUmpWordMessage
762
+ | SonareEngineSyncMidiUmpWordsMessage;
763
+
729
764
  /**
730
765
  * One of the three per-note expression dimensions, addressed to a destination.
731
766
  * `data0`/`data1` carry the dimension's own payload the way the live-input
@@ -794,6 +829,12 @@ export interface SonareEngineSyncMidiInputEventMessage {
794
829
  portTimeSamples: number;
795
830
  }
796
831
 
832
+ export interface SonareEngineSyncMidiInputUmpMessage {
833
+ type: 'syncMidiInputUmp';
834
+ words: UmpWords;
835
+ portTimeSamples: number;
836
+ }
837
+
797
838
  /** Releases the realtime engine and all worklet-owned clip buffers. */
798
839
  export interface SonareEngineDestroyMessage {
799
840
  type: 'destroy';
@@ -837,6 +878,7 @@ export type SonareEngineSyncMessage =
837
878
  | SonareEngineSyncTrackStripPanLawMessage
838
879
  | SonareEngineSyncTrackStripPanModeMessage
839
880
  | SonareEngineSyncTrackStripDualPanMessage
881
+ | SonareEngineSyncTrackStripSurroundPanMessage
840
882
  | SonareEngineSyncBusStripPanMessage
841
883
  | SonareEngineSyncBusStripPanLawMessage
842
884
  | SonareEngineSyncBusStripPanModeMessage
@@ -858,6 +900,7 @@ export type SonareEngineSyncMessage =
858
900
  | SonareEngineSyncMidiInputSourceMessage
859
901
  | SonareEngineSyncMidiCcBindingMessage
860
902
  | SonareEngineSyncMidiInputEventMessage
903
+ | SonareEngineSyncMidiInputUmpMessage
861
904
  | SonareEngineDestroyMessage;
862
905
 
863
906
  export interface WorkletPort {
@@ -935,3 +978,19 @@ export interface SonareEngineTransportResponseMessage {
935
978
  state?: EngineTransportState;
936
979
  error?: string;
937
980
  }
981
+
982
+ /** Asks the worklet for one target's per-insert gain reduction (any meter delivery mode). */
983
+ export interface SonareEngineInsertGainReductionRequestMessage {
984
+ type: 'insertGainReductionRequest';
985
+ requestId: number;
986
+ targetId: number;
987
+ }
988
+
989
+ /** Reply to {@link SonareEngineInsertGainReductionRequestMessage}; `values` is present when `ok`. */
990
+ export interface SonareEngineInsertGainReductionResponseMessage {
991
+ type: 'insertGainReductionResponse';
992
+ requestId: number;
993
+ ok: boolean;
994
+ values?: number[];
995
+ error?: string;
996
+ }
@@ -15,6 +15,7 @@ import {
15
15
  meterRingFromSharedBuffer,
16
16
  type SharedMeterRingWriter,
17
17
  type SharedSpectrumRingWriter,
18
+ SONARE_FLOOR_DB,
18
19
  SONARE_METER_RING_RECORD_FLOATS,
19
20
  type SonareWorkletMeterSnapshot,
20
21
  type SonareWorkletSpectrumSnapshot,
@@ -47,8 +48,8 @@ export class SonareWorkletProcessor {
47
48
  *
48
49
  * `targetId` is always the master and the four LUFS / gain-reduction fields
49
50
  * are always unavailable here — the mixer worklet does not run the
50
- * K-weighting filters, and a floor value would read as silence — so both are
51
- * set once rather than per interval.
51
+ * K-weighting filters or the engine's pre-trim input tap — so finite floor
52
+ * values are set once rather than per interval.
52
53
  */
53
54
  private readonly meterScratch: SonareWorkletMeterSnapshot = {
54
55
  type: 'meter',
@@ -65,6 +66,8 @@ export class SonareWorkletProcessor {
65
66
  shortTermLufs: Number.NaN,
66
67
  integratedLufs: Number.NaN,
67
68
  gainReductionDb: Number.NaN,
69
+ inputPeakDbL: SONARE_FLOOR_DB,
70
+ inputPeakDbR: SONARE_FLOOR_DB,
68
71
  };
69
72
  private spectrumRing?: SharedSpectrumRingWriter;
70
73
  private spectrumBands: Float32Array;
@@ -276,6 +279,8 @@ export class SonareWorkletProcessor {
276
279
  shortTermLufs: Number.NaN,
277
280
  integratedLufs: Number.NaN,
278
281
  gainReductionDb: Number.NaN,
282
+ inputPeakDbL: SONARE_FLOOR_DB,
283
+ inputPeakDbR: SONARE_FLOOR_DB,
279
284
  };
280
285
  // Alternative channels for one record, not a broadcast pair: a transport
281
286
  // that supplies both (the engine registration resolves both to
@@ -309,6 +314,8 @@ export class SonareWorkletProcessor {
309
314
  ring.records[offset + 11] = meter.shortTermLufs;
310
315
  ring.records[offset + 12] = meter.integratedLufs;
311
316
  ring.records[offset + 13] = meter.gainReductionDb;
317
+ ring.records[offset + 14] = meter.inputPeakDbL;
318
+ ring.records[offset + 15] = meter.inputPeakDbR;
312
319
  Atomics.store(ring.header, 0, writeIndex + 1);
313
320
  // writeIndex is a free-running monotonic counter, so an overflow guard here
314
321
  // would fire on essentially every write past the first `capacity` records
@@ -377,7 +384,7 @@ export class SonareWorkletProcessor {
377
384
  return;
378
385
  }
379
386
  const writeIndex = Atomics.load(ring.header, 0);
380
- const offset = (writeIndex % ring.capacity) * ring.recordFloats;
387
+ const offset = (writeIndex % ring.capacity) * SONARE_METER_RING_RECORD_FLOATS;
381
388
  ring.records[offset] = encodeFrameLo(frame);
382
389
  ring.records[offset + 1] = encodeFrameHi(frame);
383
390
  ring.records[offset + 2] = bands.length;