@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
@@ -5,12 +5,15 @@ import {
5
5
  isClipPageRequestMessage,
6
6
  isEngineCaptureResponseForOperation,
7
7
  isEngineCaptureResponseMessage,
8
+ isEngineInsertGainReductionResponseMessage,
8
9
  isEngineTelemetryRecord,
9
10
  isEngineTransportResponseMessage,
10
11
  isExternalMidiBatchMessage,
11
12
  isMeterSnapshot,
13
+ isScopeSnapshot,
12
14
  requireChannelCount,
13
15
  requireIntegerOption,
16
+ resolveScopeBandCount,
14
17
  } from './guards';
15
18
  import type {
16
19
  SonareEngineCaptureRequestMessage,
@@ -72,7 +75,8 @@ function isValidCommandRecord(command: SonareEngineCommandRecord): boolean {
72
75
  !Number.isSafeInteger(type) ||
73
76
  type < SonareEngineCommandType.SetParam ||
74
77
  (type > SonareEngineCommandType.SeekMarker &&
75
- type !== SonareEngineCommandType.SetTrackMonitorMode)
78
+ type !== SonareEngineCommandType.SetTrackMonitorMode &&
79
+ type !== SonareEngineCommandType.ResetMasterLoudnessMeter)
76
80
  ) {
77
81
  return false;
78
82
  }
@@ -141,6 +145,14 @@ export class SonareRealtimeEngineNode {
141
145
  reject: (reason?: unknown) => void;
142
146
  }
143
147
  >();
148
+ private insertGainReductionRequestId = 1;
149
+ private readonly insertGainReductionRequests = new Map<
150
+ number,
151
+ {
152
+ resolve: (values: number[]) => void;
153
+ reject: (reason?: unknown) => void;
154
+ }
155
+ >();
144
156
  private resolveReady!: () => void;
145
157
  private rejectReady!: (reason?: unknown) => void;
146
158
  private destroyed = false;
@@ -202,10 +214,22 @@ export class SonareRealtimeEngineNode {
202
214
  pending.reject(new Error(event.data.error ?? 'Transport request failed'));
203
215
  }
204
216
  }
217
+ } else if (isEngineInsertGainReductionResponseMessage(event.data)) {
218
+ const pending = this.insertGainReductionRequests.get(event.data.requestId);
219
+ if (pending) {
220
+ this.insertGainReductionRequests.delete(event.data.requestId);
221
+ if (event.data.ok && Array.isArray(event.data.values)) {
222
+ pending.resolve(event.data.values);
223
+ } else {
224
+ pending.reject(new Error(event.data.error ?? 'Insert gain reduction request failed'));
225
+ }
226
+ }
205
227
  } else if (isEngineTelemetryRecord(event.data)) {
206
228
  this.emitTelemetry(event.data);
207
229
  } else if (isMeterSnapshot(event.data)) {
208
230
  this.emitMeter(event.data);
231
+ } else if (isScopeSnapshot(event.data)) {
232
+ this.emitScope(event.data);
209
233
  } else if (isExternalMidiBatchMessage(event.data)) {
210
234
  this.emitMidiOut(event.data.events);
211
235
  } else if (isClipPageRequestMessage(event.data)) {
@@ -273,6 +297,13 @@ export class SonareRealtimeEngineNode {
273
297
  );
274
298
  }
275
299
 
300
+ const meterIntervalFrames = requireIntegerOption(
301
+ options.meterIntervalFrames,
302
+ 2048,
303
+ 'meterIntervalFrames',
304
+ 0,
305
+ );
306
+
276
307
  // Resolved before the transport branch, so a capacity the ring could not
277
308
  // hold is refused by name whichever transport the caller ends up with.
278
309
  const commandRingCapacity = requireIntegerOption(
@@ -302,7 +333,7 @@ export class SonareRealtimeEngineNode {
302
333
  'scopeRingCapacity',
303
334
  1,
304
335
  );
305
- const scopeBands = requireIntegerOption(options.scopeBands, 48, 'scopeBands', 1);
336
+ const scopeBands = resolveScopeBandCount(options.scopeBands);
306
337
  const clipPageRequestRingCapacity = requireIntegerOption(
307
338
  options.clipPageRequestRingCapacity,
308
339
  128,
@@ -352,6 +383,7 @@ export class SonareRealtimeEngineNode {
352
383
  sampleRate: options.sampleRate ?? context.sampleRate,
353
384
  blockSize,
354
385
  channelCount,
386
+ meterIntervalFrames,
355
387
  cueOutput,
356
388
  commandSharedBuffer: commandRing?.sharedBuffer,
357
389
  commandRingCapacity: commandRing?.capacity,
@@ -361,8 +393,8 @@ export class SonareRealtimeEngineNode {
361
393
  meterRingCapacity: meterRing?.capacity,
362
394
  scopeSharedBuffer: scopeRing?.sharedBuffer,
363
395
  scopeRingCapacity: scopeRing?.capacity,
364
- scopeBands: scopeRing?.bands,
365
- scopeIntervalFrames: scopeRing ? scopeIntervalFrames : undefined,
396
+ scopeBands: scopeIntervalFrames > 0 ? (scopeRing?.bands ?? scopeBands) : undefined,
397
+ scopeIntervalFrames: scopeIntervalFrames > 0 ? scopeIntervalFrames : undefined,
366
398
  clipPageRequestSharedBuffer: clipPageRequestRing?.sharedBuffer,
367
399
  clipPageRequestRingCapacity: clipPageRequestRing?.capacity,
368
400
  externalMidiSharedBuffer: externalMidiRing?.sharedBuffer,
@@ -416,6 +448,11 @@ export class SonareRealtimeEngineNode {
416
448
  return this.sendCommand({ type: SonareEngineCommandType.TransportPlay, sampleTime });
417
449
  }
418
450
 
451
+ /** Queues a reset of the master's integrated-loudness accumulator. */
452
+ resetMasterLoudnessMeter(sampleTime = -1): boolean {
453
+ return this.sendCommand({ type: SonareEngineCommandType.ResetMasterLoudnessMeter, sampleTime });
454
+ }
455
+
419
456
  stop(sampleTime = -1): boolean {
420
457
  return this.sendCommand({ type: SonareEngineCommandType.TransportStop, sampleTime });
421
458
  }
@@ -641,6 +678,10 @@ export class SonareRealtimeEngineNode {
641
678
  pending.reject(new Error('Realtime engine node is destroyed.'));
642
679
  }
643
680
  this.transportRequests.clear();
681
+ for (const pending of this.insertGainReductionRequests.values()) {
682
+ pending.reject(new Error('Realtime engine node is destroyed.'));
683
+ }
684
+ this.insertGainReductionRequests.clear();
644
685
  this.telemetryListeners.clear();
645
686
  this.meterListeners.clear();
646
687
  this.scopeListeners.clear();
@@ -710,6 +751,30 @@ export class SonareRealtimeEngineNode {
710
751
  }
711
752
  }
712
753
 
754
+ /**
755
+ * Reads one target's per-insert gain reduction (dB, each <= 0, pre-to-post insert order)
756
+ * from the worklet. Works in both ring and postMessage meter modes; rejects when the
757
+ * target is invalid or mixing is not compiled in.
758
+ */
759
+ pollInsertGainReduction(targetId: number): Promise<number[]> {
760
+ if (this.destroyed) {
761
+ return Promise.reject(new Error('Realtime engine node is destroyed.'));
762
+ }
763
+ const requestId = this.insertGainReductionRequestId++;
764
+ let rejectRequest!: (reason?: unknown) => void;
765
+ const promise = new Promise<number[]>((resolve, reject) => {
766
+ rejectRequest = reject;
767
+ this.insertGainReductionRequests.set(requestId, { resolve, reject });
768
+ });
769
+ try {
770
+ this.node.port.postMessage({ type: 'insertGainReductionRequest', requestId, targetId });
771
+ } catch (error) {
772
+ this.insertGainReductionRequests.delete(requestId);
773
+ rejectRequest(error);
774
+ }
775
+ return promise;
776
+ }
777
+
713
778
  private emitScope(scope: SonareWorkletScopeSnapshot): void {
714
779
  for (const listener of this.scopeListeners) {
715
780
  listener(scope);
@@ -1,14 +1,21 @@
1
1
  import type { EngineClip } from '../index';
2
2
  import { RealtimeEngine } from '../index';
3
3
  import { copyPlanesToOutput, type WorkletInput, type WorkletOutput } from './audio_types';
4
- import { emptyStripJson } from './engine-mixer-facade';
5
- import { requireChannelCount, requireIntegerOption } from './guards';
4
+ import {
5
+ requireChannelCount,
6
+ requireIntegerOption,
7
+ resolveScopeBandCount,
8
+ SONARE_SCOPE_MAX_BANDS,
9
+ SONARE_SCOPE_MAX_POINTS,
10
+ } from './guards';
6
11
  import {
7
12
  DEFAULT_METRONOME_CONFIG,
8
13
  type ResolvedMetronomeConfig,
9
14
  resolveMetronomeConfig,
10
15
  type SonareEngineCaptureRequestMessage,
11
16
  type SonareEngineCaptureResponseMessageInternal,
17
+ type SonareEngineInsertGainReductionRequestMessage,
18
+ type SonareEngineInsertGainReductionResponseMessage,
12
19
  type SonareEngineSyncMessage,
13
20
  type SonareEngineTransportRequestMessage,
14
21
  type SonareEngineTransportResponseMessage,
@@ -41,6 +48,8 @@ import {
41
48
  type SonareEngineTelemetryRecord,
42
49
  type SonareEngineTelemetryRingBuffer,
43
50
  SonareEngineTelemetryType,
51
+ type SonareWorkletMeterSnapshot,
52
+ type SonareWorkletScopeSnapshot,
44
53
  scopeRingFromSharedBuffer,
45
54
  telemetryFromEngine,
46
55
  writeInt64Words,
@@ -84,6 +93,9 @@ export class SonareRealtimeEngineWorkletProcessor {
84
93
  private transport?: WorkletTransport;
85
94
  private meterIntervalFrames: number;
86
95
  private lastMeterFrame = Number.NEGATIVE_INFINITY;
96
+ private scopeIntervalFrames: number;
97
+ private scopeEnabled: boolean;
98
+ private scopeBands: number;
87
99
  // Latest metronome gains/click length pushed via 'syncMetronome'. The
88
100
  // SetMetronome command only toggles enabled state; the config arrives here.
89
101
  private metronomeConfig: ResolvedMetronomeConfig = { ...DEFAULT_METRONOME_CONFIG };
@@ -146,6 +158,16 @@ export class SonareRealtimeEngineWorkletProcessor {
146
158
  options.scopeBands,
147
159
  )
148
160
  : undefined;
161
+ this.scopeIntervalFrames = requireIntegerOption(
162
+ options.scopeIntervalFrames,
163
+ this.scopeRing ? this.blockSize : 0,
164
+ 'scopeIntervalFrames',
165
+ 0,
166
+ );
167
+ this.scopeBands = resolveScopeBandCount(options.scopeBands);
168
+ this.scopeEnabled =
169
+ this.scopeIntervalFrames > 0 &&
170
+ (this.scopeRing !== undefined || typeof this.transport?.postMessage === 'function');
149
171
  this.clipPageRequestRing = options.clipPageRequestSharedBuffer
150
172
  ? clipPageRequestRingFromSharedBuffer(
151
173
  options.clipPageRequestSharedBuffer,
@@ -183,18 +205,12 @@ export class SonareRealtimeEngineWorkletProcessor {
183
205
  this.monitorBuffers[ch] = this.engine.getMonitorChannelBuffer(ch, this.blockSize);
184
206
  }
185
207
  }
186
- // Arm the engine's scope producer only when a scope ring was provided. The
187
- // band count follows the ring's record layout so writeScopeRing never
188
- // overruns its slot.
189
- if (this.scopeRing) {
190
- // Zero is the engine's own off switch for the scope tap, so the floor is 0.
191
- const interval = requireIntegerOption(
192
- options.scopeIntervalFrames,
193
- this.blockSize,
194
- 'scopeIntervalFrames',
195
- 0,
208
+ // Arm the native scope producer only when a SAB ring or postMessage sink exists.
209
+ if (this.scopeEnabled) {
210
+ this.engine.configureScopeTelemetry(
211
+ this.scopeIntervalFrames,
212
+ Math.min(this.scopeRing?.bands ?? this.scopeBands, SONARE_SCOPE_MAX_BANDS),
196
213
  );
197
- this.engine.configureScopeTelemetry(interval, this.scopeRing.bands);
198
214
  }
199
215
  }
200
216
 
@@ -496,37 +512,32 @@ export class SonareRealtimeEngineWorkletProcessor {
496
512
  // Complete earlier immediate edits in message order. Scheduled
497
513
  // commands remain queued for their render frame.
498
514
  this.engine.applyCommandsDueNowPreservingFuture();
499
- this.engine.settleInsertParameters();
500
515
  if (message.buses) {
501
516
  this.engine.setTrackBuses(message.buses);
502
517
  }
503
518
  this.engine.setTrackLanes(message.lanes);
504
- for (const target of message.forceInsertResets ?? []) {
505
- const emptyJson = emptyStripJson(target);
519
+ for (const strip of message.trackStrips ?? []) {
520
+ this.engine.setTrackStripJson(strip.trackId, strip.sceneJson);
521
+ }
522
+ for (const strip of message.busStrips ?? []) {
523
+ this.engine.setBusStripJson(strip.busId, strip.sceneJson);
524
+ }
525
+ if (message.masterStripJson) {
526
+ this.engine.setMasterStripJson(message.masterStripJson);
527
+ }
528
+ for (const target of message.insertBaseResets ?? []) {
506
529
  switch (target.kind) {
507
530
  case 'track':
508
531
  this.engine.clearTrackInsertParameterBases(target.trackId);
509
- this.engine.setTrackStripJson(target.trackId, emptyJson);
510
532
  break;
511
533
  case 'bus':
512
534
  this.engine.clearBusInsertParameterBases(target.busId);
513
- this.engine.setBusStripJson(target.busId, emptyJson);
514
535
  break;
515
536
  case 'master':
516
537
  this.engine.clearMasterInsertParameterBases();
517
- this.engine.setMasterStripJson(emptyJson);
518
538
  break;
519
539
  }
520
540
  }
521
- for (const strip of message.trackStrips ?? []) {
522
- this.engine.setTrackStripJson(strip.trackId, strip.sceneJson);
523
- }
524
- for (const strip of message.busStrips ?? []) {
525
- this.engine.setBusStripJson(strip.busId, strip.sceneJson);
526
- }
527
- if (message.masterStripJson) {
528
- this.engine.setMasterStripJson(message.masterStripJson);
529
- }
530
541
  for (const binding of message.laneSidechains ?? []) {
531
542
  this.engine.setLaneSidechain(binding.trackId, binding.insertIndex, binding.sourceTrackId);
532
543
  }
@@ -674,6 +685,9 @@ export class SonareRealtimeEngineWorkletProcessor {
674
685
  case 'syncTrackStripDualPan':
675
686
  this.engine.setTrackStripDualPan(message.trackId, message.leftPan, message.rightPan);
676
687
  break;
688
+ case 'syncTrackStripSurroundPan':
689
+ this.engine.setTrackStripSurroundPan(message.trackId, message.pan);
690
+ break;
677
691
  case 'syncBusStripPan':
678
692
  this.engine.setBusStripPan(message.busId, message.pan);
679
693
  break;
@@ -766,7 +780,11 @@ export class SonareRealtimeEngineWorkletProcessor {
766
780
  );
767
781
  break;
768
782
  case 'syncMidiUmp':
769
- this.engine.pushMidiUmp(message.destinationId, [message.word0], message.renderFrame);
783
+ this.engine.pushMidiUmp(
784
+ message.destinationId,
785
+ message.words ?? [message.word0],
786
+ message.renderFrame,
787
+ );
770
788
  break;
771
789
  case 'syncMidiSysex':
772
790
  this.engine.pushMidiSysex(message.destinationId, message.data, message.renderFrame);
@@ -844,6 +862,9 @@ export class SonareRealtimeEngineWorkletProcessor {
844
862
  message.portTimeSamples,
845
863
  );
846
864
  break;
865
+ case 'syncMidiInputUmp':
866
+ this.engine.pushMidiInputUmp(message.words, message.portTimeSamples);
867
+ break;
847
868
  }
848
869
  }
849
870
 
@@ -923,6 +944,27 @@ export class SonareRealtimeEngineWorkletProcessor {
923
944
  }
924
945
  }
925
946
 
947
+ receiveInsertGainReductionRequest(message: SonareEngineInsertGainReductionRequestMessage): void {
948
+ if (this.closed) {
949
+ return;
950
+ }
951
+ try {
952
+ this.transport?.postMessage?.({
953
+ type: 'insertGainReductionResponse',
954
+ requestId: message.requestId,
955
+ ok: true,
956
+ values: this.engine.meterTargetInsertGainReduction(message.targetId),
957
+ } satisfies SonareEngineInsertGainReductionResponseMessage);
958
+ } catch (error) {
959
+ this.transport?.postMessage?.({
960
+ type: 'insertGainReductionResponse',
961
+ requestId: message.requestId,
962
+ ok: false,
963
+ error: error instanceof Error ? error.message : String(error),
964
+ } satisfies SonareEngineInsertGainReductionResponseMessage);
965
+ }
966
+ }
967
+
926
968
  destroy(): void {
927
969
  if (!this.closed) {
928
970
  this.clearPagedClipProviders();
@@ -1056,6 +1098,9 @@ export class SonareRealtimeEngineWorkletProcessor {
1056
1098
  this.engine.setTrackMonitorMode(laneIndex, mode as 0 | 1 | 2, sampleTime);
1057
1099
  break;
1058
1100
  }
1101
+ case SonareEngineCommandType.ResetMasterLoudnessMeter:
1102
+ this.engine.resetMasterLoudnessMeter(sampleTime);
1103
+ break;
1059
1104
  default:
1060
1105
  this.publishTelemetryRecord({
1061
1106
  type: SonareEngineTelemetryType.Error,
@@ -1140,6 +1185,8 @@ export class SonareRealtimeEngineWorkletProcessor {
1140
1185
  }
1141
1186
  return;
1142
1187
  }
1188
+ const delivered: SonareWorkletMeterSnapshot[] = [];
1189
+ const newestByTarget = new Map<number, SonareWorkletMeterSnapshot>();
1143
1190
  for (const item of this.engine.drainMeterTelemetry(64)) {
1144
1191
  const meter = meterFromEngine(item);
1145
1192
  if (
@@ -1151,6 +1198,15 @@ export class SonareRealtimeEngineWorkletProcessor {
1151
1198
  if (meter.frame !== this.lastMeterFrame) {
1152
1199
  this.lastMeterFrame = meter.frame;
1153
1200
  }
1201
+ delivered.push(meter);
1202
+ newestByTarget.set(meter.targetId, meter);
1203
+ }
1204
+ for (const meter of delivered) {
1205
+ // The board holds only the latest block, so only the newest record per
1206
+ // target in this drain can carry it; the ring layout is fixed and cannot.
1207
+ if (newestByTarget.get(meter.targetId) === meter) {
1208
+ meter.insertGainReductionDb = this.engine.meterTargetInsertGainReduction(meter.targetId);
1209
+ }
1154
1210
  // The ring branch returned above, so this is the structured-clone
1155
1211
  // fallback. `onMeter` and `postMessage` are alternative channels for the
1156
1212
  // same record, not a broadcast pair: the AudioWorklet registration
@@ -1175,6 +1231,8 @@ export class SonareRealtimeEngineWorkletProcessor {
1175
1231
  for (let field = 0; field < 11; field++) {
1176
1232
  ring.records[offset + 3 + field] = this.engine.meterScratchValue(field);
1177
1233
  }
1234
+ ring.records[offset + 14] = this.engine.meterScratchInputPeakDbL();
1235
+ ring.records[offset + 15] = this.engine.meterScratchInputPeakDbR();
1178
1236
  Atomics.store(ring.header, 0, writeIndex + 1);
1179
1237
  }
1180
1238
 
@@ -1192,16 +1250,53 @@ export class SonareRealtimeEngineWorkletProcessor {
1192
1250
  // so header slot 3 is left at its initial 0.
1193
1251
 
1194
1252
  // Drains the engine's scope producer (FFT spectrum + goniometer points) into
1195
- // the lock-free SAB scope ring. No allocation on the render path: records are
1196
- // written field-by-field into the ring.
1253
+ // the lock-free SAB scope ring or the bounded postMessage fallback. The SAB
1254
+ // path remains allocation-free; postMessage materialises only records that
1255
+ // are ready to publish.
1197
1256
  private publishScope(): void {
1198
- const ring = this.scopeRing;
1199
- if (!ring) {
1257
+ if (!this.scopeEnabled) {
1200
1258
  return;
1201
1259
  }
1260
+ const ring = this.scopeRing;
1261
+ const transport = this.transport;
1202
1262
  for (let count = 0; count < 64 && this.engine.popScopeTelemetryToScratch(); count++) {
1203
- this.writeScopeScratch(ring);
1263
+ if (ring) {
1264
+ this.writeScopeScratch(ring);
1265
+ } else {
1266
+ this.postScopeScratch(transport);
1267
+ }
1268
+ }
1269
+ }
1270
+
1271
+ private postScopeScratch(transport: WorkletTransport | undefined): void {
1272
+ if (!transport?.postMessage) {
1273
+ return;
1274
+ }
1275
+ const bandCount = Math.min(
1276
+ SONARE_SCOPE_MAX_BANDS,
1277
+ Math.max(0, Math.trunc(Number(this.engine.scopeScratchBandCount()))),
1278
+ );
1279
+ const pointCount = Math.min(
1280
+ SONARE_SCOPE_MAX_POINTS,
1281
+ Math.max(0, Math.trunc(Number(this.engine.scopeScratchPointCount()))),
1282
+ );
1283
+ const bands = new Float32Array(bandCount);
1284
+ for (let i = 0; i < bandCount; i++) {
1285
+ bands[i] = this.engine.scopeScratchBand(i);
1286
+ }
1287
+ const points = new Float32Array(pointCount * 2);
1288
+ for (let i = 0; i < pointCount; i++) {
1289
+ points[2 * i] = this.engine.scopeScratchPointLeft(i);
1290
+ points[2 * i + 1] = this.engine.scopeScratchPointRight(i);
1204
1291
  }
1292
+ const scope: SonareWorkletScopeSnapshot = {
1293
+ type: 'scope',
1294
+ targetId: this.engine.scopeScratchTargetId(),
1295
+ frame: Number(this.engine.scopeScratchRenderFrame()),
1296
+ bands,
1297
+ points,
1298
+ };
1299
+ transport.postMessage(scope, [bands.buffer, points.buffer]);
1205
1300
  }
1206
1301
 
1207
1302
  private writeScopeScratch(ring: SharedScopeRingWriter): void {
@@ -5,6 +5,7 @@ import { SonareRealtimeEngineWorkletProcessor } from './engine-processor';
5
5
  import {
6
6
  isEngineCaptureRequestMessage,
7
7
  isEngineCommandRecord,
8
+ isEngineInsertGainReductionRequestMessage,
8
9
  isEngineSyncMessage,
9
10
  isEngineTransportRequestMessage,
10
11
  } from './guards';
@@ -82,6 +83,8 @@ export function registerSonareRealtimeEngineWorkletProcessor(
82
83
  bridge.receiveCaptureRequest(data);
83
84
  } else if (isEngineTransportRequestMessage(data)) {
84
85
  bridge.receiveTransportRequest(data);
86
+ } else if (isEngineInsertGainReductionRequestMessage(data)) {
87
+ bridge.receiveInsertGainReductionRequest(data);
85
88
  } else if (isRecord(data) && typeof data.type === 'string' && data.type.startsWith('sync')) {
86
89
  // A sync the guard does not accept would otherwise vanish, leaving the
87
90
  // live engine out of step with the offline mirror and nothing to see.
@@ -1,13 +1,12 @@
1
1
  import { panLawCode, panModeCode } from '../codes';
2
- import type { EqBand, PanLawInput, PanMode, RealtimeEngine } from '../index';
2
+ import type { EqBand, PanLawInput, PanMode, RealtimeEngine, UmpWords } from '../index';
3
+ import type { SurroundPan } from '../public_types';
3
4
  import type { InsertParamOverrideMap } from './engine-mixer-facade';
4
5
  import {
5
- applyFullStripJson,
6
- clearInsertParamOverrides,
7
6
  emptyStripJson,
8
- hasInsertParamOverrides,
9
7
  type InsertParamOverride,
10
8
  insertParamOverrideKey,
9
+ replaceStripScene,
11
10
  type StripJsonTarget,
12
11
  } from './engine-mixer-facade';
13
12
  import type { SonareEngineInstrumentSyncMessage, SonareEngineSyncMessage } from './messages';
@@ -27,6 +26,7 @@ export interface EngineStripContext {
27
26
  readStripJson(target: StripJsonTarget): string | undefined;
28
27
  writeStripJson(target: StripJsonTarget, sceneJson: string): void;
29
28
  readonly insertParamOverrides: InsertParamOverrideMap;
29
+ clearInsertAutomationLanes(target: StripJsonTarget): void;
30
30
  }
31
31
 
32
32
  function trackIdFor(ctx: EngineStripContext, target: string | number): number {
@@ -92,15 +92,11 @@ export function setTrackStripJson(
92
92
  trackId: number,
93
93
  sceneJson: string,
94
94
  trackStripJson: Map<number, string>,
95
- ): { resetInserts: boolean } {
95
+ ): { insertBaseResets: StripJsonTarget[]; constructionOverrides: InsertParamOverride[] } {
96
96
  const target: StripJsonTarget = { kind: 'track', trackId };
97
- const resetInserts =
98
- (trackStripJson.has(trackId) && trackStripJson.get(trackId) !== sceneJson) ||
99
- hasInsertParamOverrides(ctx.insertParamOverrides, target);
100
- applyFullStripJson(ctx.offlineEngine, target, sceneJson, resetInserts);
101
- clearInsertParamOverrides(ctx.insertParamOverrides, target);
97
+ const result = replaceStripScene(ctx, target, trackStripJson.get(trackId), sceneJson);
102
98
  trackStripJson.set(trackId, sceneJson);
103
- return { resetInserts };
99
+ return result;
104
100
  }
105
101
 
106
102
  export function setTrackStripEqBand(
@@ -210,6 +206,26 @@ export function setTrackStripDualPan(
210
206
  ctx.postSync({ type: 'syncTrackStripDualPan', trackId, leftPan, rightPan });
211
207
  }
212
208
 
209
+ export function setTrackStripSurroundPan(
210
+ ctx: EngineStripContext,
211
+ target: string | number,
212
+ pan: SurroundPan,
213
+ ): void {
214
+ const trackId = trackIdFor(ctx, target);
215
+ ctx.offlineEngine.setTrackStripSurroundPan(trackId, pan);
216
+ const resolved = {
217
+ azimuth: pan.azimuth ?? 0,
218
+ elevation: pan.elevation ?? 0,
219
+ divergence: pan.divergence ?? 0,
220
+ lfe: pan.lfe ?? 0,
221
+ distance: pan.distance === undefined || pan.distance <= 0 ? 1 : pan.distance,
222
+ };
223
+ mergeStripJson(ctx, { kind: 'track', trackId }, (entry) => {
224
+ entry.surroundPan = resolved;
225
+ });
226
+ ctx.postSync({ type: 'syncTrackStripSurroundPan', trackId, pan: resolved });
227
+ }
228
+
213
229
  export function setTrackStripChannelDelaySamples(
214
230
  ctx: EngineStripContext,
215
231
  target: string | number,
@@ -504,12 +520,18 @@ export function pushMidiPolyPressure(
504
520
  export function pushMidiUmp(
505
521
  ctx: EngineStripContext,
506
522
  trackId: string | number,
507
- word0: number,
523
+ word0: number | UmpWords,
508
524
  renderFrame: number,
509
525
  ): void {
510
526
  const destinationId = ctx.resolveTargetId(trackId);
511
- ctx.offlineEngine.pushMidiUmp(destinationId, [word0], renderFrame);
512
- ctx.postSync({ type: 'syncMidiUmp', destinationId, word0, renderFrame });
527
+ if (typeof word0 === 'number') {
528
+ ctx.offlineEngine.pushMidiUmp(destinationId, word0, renderFrame);
529
+ ctx.postSync({ type: 'syncMidiUmp', destinationId, word0, renderFrame });
530
+ return;
531
+ }
532
+ const words = word0 instanceof Uint32Array ? new Uint32Array(word0) : word0.slice();
533
+ ctx.offlineEngine.pushMidiUmp(destinationId, words, renderFrame);
534
+ ctx.postSync({ type: 'syncMidiUmp', destinationId, words, renderFrame });
513
535
  }
514
536
 
515
537
  export function setBuiltinInstrument(
@@ -11,25 +11,30 @@ import type { SonareEngineSyncTempoMessage } from './messages';
11
11
  * Builds the ordered mixer-lane descriptors for a sync message.
12
12
  *
13
13
  * Each declared track id is paired with its current send list and output bus
14
- * (both omitted when absent), defensively copying the send entries.
14
+ * (both omitted when absent) and source channel layout, defensively copying the
15
+ * send entries.
15
16
  *
16
17
  * @param trackLaneIds Lane order (track ids) declared on the engine.
17
18
  * @param trackSends Per-track send lists.
18
19
  * @param trackOutputBus Per-track output bus routing.
20
+ * @param trackSourceChannelLayout Per-track source channel layouts.
19
21
  * @returns Lane descriptors in lane order.
20
22
  */
21
23
  export function buildMixerLanes(
22
24
  trackLaneIds: readonly number[],
23
25
  trackSends: ReadonlyMap<number, EngineTrackSend[]>,
24
26
  trackOutputBus: ReadonlyMap<number, number>,
27
+ trackSourceChannelLayout: ReadonlyMap<number, number>,
25
28
  ): EngineTrackLane[] {
26
29
  return trackLaneIds.map((trackId) => {
27
30
  const sends = trackSends.get(trackId);
28
31
  const outputBusId = trackOutputBus.get(trackId);
32
+ const sourceChannelLayout = trackSourceChannelLayout.get(trackId);
29
33
  return {
30
34
  trackId,
31
35
  ...(sends && sends.length > 0 ? { sends: sends.map((send) => ({ ...send })) } : {}),
32
36
  ...(outputBusId !== undefined ? { outputBusId } : {}),
37
+ ...(sourceChannelLayout !== undefined ? { sourceChannelLayout } : {}),
33
38
  };
34
39
  });
35
40
  }