@libraz/libsonare 1.3.3 → 1.4.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.
package/src/worklet.ts CHANGED
@@ -1,14 +1,14 @@
1
+ import { panLawCode, panModeCode } from './codes';
1
2
  import type {
2
3
  EngineAutomationPoint,
3
4
  EngineBus,
4
5
  EngineCaptureStatus,
5
6
  EngineClip,
6
7
  EngineMarker,
7
- EngineMeterTelemetry,
8
8
  EngineMetronomeConfig,
9
9
  EngineMidiClipSchedule,
10
10
  EngineParameterInfo,
11
- EngineTelemetry,
11
+ EngineScopeTelemetry,
12
12
  EngineTempoSegment,
13
13
  EngineTimeSignatureSegment,
14
14
  EngineTrackLane,
@@ -16,7 +16,8 @@ import type {
16
16
  EngineTransportState,
17
17
  EqBand,
18
18
  MixerRealtimeBuffer,
19
- RealtimeVoiceChangerConfigInput,
19
+ PanLaw,
20
+ PanMode,
20
21
  } from './index';
21
22
  import {
22
23
  engineCapabilities,
@@ -26,7 +27,6 @@ import {
26
27
  RealtimeEngine,
27
28
  RealtimeVoiceChanger,
28
29
  } from './index';
29
- import type { AutomationCurve } from './public_types';
30
30
  import type { SonareModule } from './sonare.js';
31
31
  import type { SonareRtModule } from './sonare-rt';
32
32
 
@@ -37,104 +37,182 @@ import type { SonareRtModule } from './sonare-rt';
37
37
  // `index` module.
38
38
  export { init, isInitialized } from './index';
39
39
 
40
- export interface SonareWorkletProcessorOptions {
41
- sceneJson: string;
42
- sampleRate?: number;
43
- blockSize?: number;
44
- stripCount?: number;
45
- meterIntervalFrames?: number;
46
- meterSharedBuffer?: SharedArrayBuffer;
47
- meterRingCapacity?: number;
48
- spectrumIntervalFrames?: number;
49
- spectrumBands?: number;
50
- spectrumSharedBuffer?: SharedArrayBuffer;
51
- spectrumRingCapacity?: number;
52
- }
53
-
54
- export interface SonareRealtimeEngineWorkletProcessorOptions {
55
- runtimeTarget?: 'embind' | 'sonare-rt';
56
- rtModuleUrl?: string;
57
- rtWasmBinary?: ArrayBuffer | Uint8Array;
58
- wasmBinary?: ArrayBuffer | Uint8Array;
59
- initialSyncMessages?: SonareEngineSyncMessage[];
60
- initialCommands?: SonareEngineCommandRecord[];
61
- sampleRate?: number;
62
- blockSize?: number;
63
- channelCount?: number;
64
- meterIntervalFrames?: number;
65
- commandSharedBuffer?: SharedArrayBuffer;
66
- commandRingCapacity?: number;
67
- telemetrySharedBuffer?: SharedArrayBuffer;
68
- telemetryRingCapacity?: number;
69
- meterSharedBuffer?: SharedArrayBuffer;
70
- meterRingCapacity?: number;
71
- }
72
-
73
- export interface SonareRealtimeVoiceChangerWorkletProcessorOptions {
74
- preset?: RealtimeVoiceChangerConfigInput;
75
- sampleRate?: number;
76
- blockSize?: number;
77
- channelCount?: number;
78
- }
79
-
80
- export interface SonareRealtimeVoiceChangerSetConfigMessage {
81
- type: 'setConfig';
82
- preset: RealtimeVoiceChangerConfigInput;
83
- }
84
-
85
- export interface SonareRealtimeVoiceChangerResetMessage {
86
- type: 'reset';
87
- }
88
-
89
- export interface SonareRealtimeVoiceChangerDestroyMessage {
90
- type: 'destroy';
91
- }
92
-
93
- export type SonareRealtimeVoiceChangerMessage =
94
- | SonareRealtimeVoiceChangerSetConfigMessage
95
- | SonareRealtimeVoiceChangerResetMessage
96
- | SonareRealtimeVoiceChangerDestroyMessage;
97
-
98
- export interface SonareRealtimeEngineNodeCapabilities {
99
- mode: 'sab' | 'postMessage';
100
- runtimeTarget: 'embind' | 'sonare-rt';
101
- sharedArrayBuffer: boolean;
102
- atomics: boolean;
103
- audioWorklet: boolean;
104
- engineAbiVersion?: number;
105
- expectedEngineAbiVersion?: number;
106
- abiCompatible?: boolean;
107
- degradedReason?: string;
108
- readyMessage?: boolean;
109
- }
110
-
111
- export interface SonareRealtimeEngineNodeOptions
112
- extends SonareRealtimeEngineWorkletProcessorOptions {
113
- processorName?: string;
114
- moduleUrl?: string | URL;
115
- rtModuleUrl?: string;
116
- mode?: 'auto' | 'sab' | 'postMessage';
117
- engineAbiVersion?: number;
118
- expectedEngineAbiVersion?: number;
119
- requireAbiCompatible?: boolean;
120
- nodeFactory?: (
121
- context: BaseAudioContext,
122
- processorName: string,
123
- options: AudioWorkletNodeOptions,
124
- ) => AudioWorkletNode;
125
- }
126
-
127
- export interface SonareRtRealtimeEngineRuntimeOptions {
128
- module: SonareRtModule;
129
- memory: WebAssembly.Memory;
130
- sampleRate?: number;
131
- blockSize?: number;
132
- channelCount?: number;
133
- commandSharedBuffer?: SharedArrayBuffer;
134
- commandRingCapacity?: number;
135
- telemetrySharedBuffer?: SharedArrayBuffer;
136
- telemetryRingCapacity?: number;
137
- }
40
+ import type { WorkletInput, WorkletOutput } from './worklet/audio_types';
41
+ import {
42
+ isEngineCaptureRequestMessage,
43
+ isEngineCaptureResponseMessage,
44
+ isEngineCommandRecord,
45
+ isEngineSyncMessage,
46
+ isEngineTelemetryRecord,
47
+ isEngineTransportRequestMessage,
48
+ isEngineTransportResponseMessage,
49
+ isMeterSnapshot,
50
+ isRealtimeVoiceChangerMessage,
51
+ isWorkletMessage,
52
+ } from './worklet/guards';
53
+ import {
54
+ DEFAULT_METRONOME_CONFIG,
55
+ type ResolvedMetronomeConfig,
56
+ resolveMetronomeConfig,
57
+ type SonareEngineCaptureRequestMessage,
58
+ type SonareEngineCaptureResponseMessage,
59
+ type SonareEngineInstrumentSyncMessage,
60
+ type SonareEngineSyncCaptureMessage,
61
+ type SonareEngineSyncMessage,
62
+ type SonareEngineTransportFacade,
63
+ type SonareEngineTransportRequestMessage,
64
+ type SonareEngineTransportResponseMessage,
65
+ type SonareRealtimeEngineNodeCapabilities,
66
+ type SonareRealtimeEngineNodeOptions,
67
+ type SonareRealtimeEngineWorkletProcessorOptions,
68
+ type SonareRealtimeVoiceChangerMessage,
69
+ type SonareRealtimeVoiceChangerWorkletProcessorOptions,
70
+ type SonareRtRealtimeEngineRuntimeOptions,
71
+ type SonareWorkletMessage,
72
+ type SonareWorkletProcessorOptions,
73
+ type WorkletPort,
74
+ type WorkletTransport,
75
+ } from './worklet/messages';
76
+ // --- internal modules (split out of this file; bundled back into a single
77
+ // dist/worklet.js by tsup, so the public surface is unchanged) ---
78
+ import {
79
+ createSonareEngineCommandRingBuffer,
80
+ createSonareEngineTelemetryRingBuffer,
81
+ createSonareMeterRingBuffer,
82
+ createSonareScopeRingBuffer,
83
+ ENGINE_MIXER_PARAM_FADER_DB,
84
+ ENGINE_MIXER_PARAM_PAN,
85
+ encodeFrameHi,
86
+ encodeFrameLo,
87
+ engineMixerBusTarget,
88
+ engineMixerLaneTarget,
89
+ engineMixerMasterTarget,
90
+ engineRingFromSharedBuffer,
91
+ isRecord,
92
+ magnitudeToDb,
93
+ meterFromEngine,
94
+ meterRingFromSharedBuffer,
95
+ popSonareEngineCommandRingBuffer,
96
+ pushSonareEngineCommandRingBuffer,
97
+ readSonareEngineTelemetryRingBuffer,
98
+ readSonareMeterRingBuffer,
99
+ readSonareScopeRingBuffer,
100
+ type SharedMeterRingWriter,
101
+ type SharedScopeRingWriter,
102
+ type SharedSpectrumRingWriter,
103
+ SONARE_ENGINE_COMMAND_RECORD_BYTES,
104
+ SONARE_ENGINE_TELEMETRY_RECORD_BYTES,
105
+ SONARE_METER_RING_RECORD_FLOATS,
106
+ SONARE_SCOPE_RING_RECORD_PREFIX_FLOATS,
107
+ type SonareEngineCommandRecord,
108
+ type SonareEngineCommandRingBuffer,
109
+ SonareEngineCommandType,
110
+ SonareEngineTelemetryError,
111
+ type SonareEngineTelemetryRecord,
112
+ type SonareEngineTelemetryRingBuffer,
113
+ SonareEngineTelemetryType,
114
+ type SonareMeterRingBuffer,
115
+ type SonareScopeRingBuffer,
116
+ type SonareWorkletMeterSnapshot,
117
+ type SonareWorkletScopeSnapshot,
118
+ type SonareWorkletSpectrumSnapshot,
119
+ scopeRingFromSharedBuffer,
120
+ spectrumRingFromSharedBuffer,
121
+ telemetryFromEngine,
122
+ toBigInt64,
123
+ toDb,
124
+ writeSonareEngineTelemetryRingBuffer,
125
+ } from './worklet/protocol';
126
+
127
+ export type {
128
+ SonareEngineCaptureRequestMessage,
129
+ SonareEngineCaptureResponseMessage,
130
+ SonareEngineSyncAutomationMessage,
131
+ SonareEngineSyncBuiltinInstrumentMessage,
132
+ SonareEngineSyncCaptureMessage,
133
+ SonareEngineSyncClipsDeltaMessage,
134
+ SonareEngineSyncClipsMessage,
135
+ SonareEngineSyncLoadSoundFontMessage,
136
+ SonareEngineSyncMarkersMessage,
137
+ SonareEngineSyncMasterStripEqBandMessage,
138
+ SonareEngineSyncMasterStripInsertBypassedMessage,
139
+ SonareEngineSyncMessage,
140
+ SonareEngineSyncMetronomeMessage,
141
+ SonareEngineSyncMidiCcMessage,
142
+ SonareEngineSyncMidiClipsMessage,
143
+ SonareEngineSyncMidiNoteMessage,
144
+ SonareEngineSyncMidiPanicMessage,
145
+ SonareEngineSyncMixerMessage,
146
+ SonareEngineSyncSf2InstrumentMessage,
147
+ SonareEngineSyncSynthInstrumentMessage,
148
+ SonareEngineSyncTempoMessage,
149
+ SonareEngineSyncTrackStripEqBandMessage,
150
+ SonareEngineSyncTrackStripInsertBypassedMessage,
151
+ SonareEngineTransportFacade,
152
+ SonareEngineTransportRequestMessage,
153
+ SonareEngineTransportResponseMessage,
154
+ SonareRealtimeEngineNodeCapabilities,
155
+ SonareRealtimeEngineNodeOptions,
156
+ SonareRealtimeEngineWorkletProcessorOptions,
157
+ SonareRealtimeVoiceChangerDestroyMessage,
158
+ SonareRealtimeVoiceChangerMessage,
159
+ SonareRealtimeVoiceChangerResetMessage,
160
+ SonareRealtimeVoiceChangerSetConfigMessage,
161
+ SonareRealtimeVoiceChangerWorkletProcessorOptions,
162
+ SonareRtRealtimeEngineRuntimeOptions,
163
+ SonareWorkletDestroyMessage,
164
+ SonareWorkletMessage,
165
+ SonareWorkletProcessorOptions,
166
+ SonareWorkletScheduleInsertAutomationMessage,
167
+ SonareWorkletSetMeterIntervalMessage,
168
+ SonareWorkletTransportMessage,
169
+ } from './worklet/messages';
170
+ export {
171
+ createSonareEngineCommandRingBuffer,
172
+ createSonareEngineTelemetryRingBuffer,
173
+ createSonareMeterRingBuffer,
174
+ createSonareScopeRingBuffer,
175
+ createSonareSpectrumRingBuffer,
176
+ decodeFrame,
177
+ encodeFrameHi,
178
+ encodeFrameLo,
179
+ popSonareEngineCommandRingBuffer,
180
+ pushSonareEngineCommandRingBuffer,
181
+ readSonareEngineTelemetryRingBuffer,
182
+ readSonareMeterRingBuffer,
183
+ readSonareScopeRingBuffer,
184
+ readSonareSpectrumRingBuffer,
185
+ SONARE_ENGINE_COMMAND_RECORD_BYTES,
186
+ SONARE_ENGINE_RING_HEADER_INTS,
187
+ SONARE_ENGINE_TELEMETRY_RECORD_BYTES,
188
+ SONARE_METER_RING_HEADER_INTS,
189
+ SONARE_METER_RING_RECORD_FLOATS,
190
+ SONARE_SCOPE_RING_HEADER_INTS,
191
+ SONARE_SPECTRUM_RING_HEADER_INTS,
192
+ type SonareEngineCommandRecord,
193
+ type SonareEngineCommandRingBuffer,
194
+ SonareEngineCommandType,
195
+ SonareEngineTelemetryError,
196
+ type SonareEngineTelemetryRecord,
197
+ type SonareEngineTelemetryRingBuffer,
198
+ type SonareEngineTelemetryRingReadResult,
199
+ SonareEngineTelemetryType,
200
+ type SonareMeterRingBuffer,
201
+ type SonareMeterRingReadResult,
202
+ type SonareScopeRingBuffer,
203
+ type SonareScopeRingReadResult,
204
+ type SonareSpectrumRingBuffer,
205
+ type SonareSpectrumRingReadResult,
206
+ type SonareWorkletMeterSnapshot,
207
+ type SonareWorkletScopeSnapshot,
208
+ type SonareWorkletSpectrumSnapshot,
209
+ sonareEngineCommandRingBufferByteLength,
210
+ sonareEngineTelemetryRingBufferByteLength,
211
+ sonareMeterRingBufferByteLength,
212
+ sonareScopeRingBufferByteLength,
213
+ sonareSpectrumRingBufferByteLength,
214
+ writeSonareEngineTelemetryRingBuffer,
215
+ } from './worklet/protocol';
138
216
 
139
217
  export interface SonareEngineOptions extends SonareRealtimeEngineNodeOptions {
140
218
  offlineEngine?: RealtimeEngine;
@@ -142,1010 +220,11 @@ export interface SonareEngineOptions extends SonareRealtimeEngineNodeOptions {
142
220
  offlineChannelCount?: number;
143
221
  }
144
222
 
145
- export interface SonareEngineTransportFacade {
146
- play(sampleTime?: number): boolean;
147
- stop(sampleTime?: number): boolean;
148
- seekPpq(ppq: number, sampleTime?: number): boolean;
149
- seekSeconds(seconds: number, sampleTime?: number): boolean;
150
- setTempo(bpm: number): void;
151
- setTempoSegments(segments: readonly EngineTempoSegment[]): void;
152
- setLoop(startPpq: number, endPpq: number, enabled?: boolean): boolean;
153
- }
154
-
155
223
  type SuspendableAudioContext = BaseAudioContext & {
156
224
  suspend?: () => Promise<void>;
157
225
  resume?: () => Promise<void>;
158
226
  };
159
227
 
160
- const ENGINE_MIXER_TARGET_BASE = 0x4d580000;
161
- const ENGINE_MIXER_PARAM_FADER_DB = 1;
162
- const ENGINE_MIXER_PARAM_PAN = 2;
163
-
164
- function engineMixerLaneTarget(laneIndex: number, paramKind: number): number {
165
- return ENGINE_MIXER_TARGET_BASE | ((laneIndex & 0xff) << 8) | (paramKind & 0xff);
166
- }
167
-
168
- function engineMixerBusTarget(busIndex: number, paramKind: number): number {
169
- return ENGINE_MIXER_TARGET_BASE | (((0xfe - busIndex) & 0xff) << 8) | (paramKind & 0xff);
170
- }
171
-
172
- function engineMixerMasterTarget(paramKind: number): number {
173
- return ENGINE_MIXER_TARGET_BASE | (0xff << 8) | (paramKind & 0xff);
174
- }
175
-
176
- type WorkletInput = readonly (readonly Float32Array[])[];
177
- type WorkletOutput = Float32Array[][];
178
-
179
- export interface SonareWorkletScheduleInsertAutomationMessage {
180
- type: 'scheduleInsertAutomation';
181
- stripIndex: number;
182
- insertIndex: number;
183
- paramId: number;
184
- value: number;
185
- samplePos?: number;
186
- curve?: AutomationCurve;
187
- }
188
-
189
- export interface SonareWorkletSetMeterIntervalMessage {
190
- type: 'setMeterInterval';
191
- frames: number;
192
- }
193
-
194
- export interface SonareWorkletDestroyMessage {
195
- type: 'destroy';
196
- }
197
-
198
- export type SonareWorkletMessage =
199
- | SonareWorkletScheduleInsertAutomationMessage
200
- | SonareWorkletSetMeterIntervalMessage
201
- | SonareWorkletDestroyMessage;
202
-
203
- export interface SonareWorkletMeterSnapshot {
204
- type: 'meter';
205
- targetId: number;
206
- frame: number;
207
- peakDbL: number;
208
- peakDbR: number;
209
- rmsDbL: number;
210
- rmsDbR: number;
211
- correlation: number;
212
- truePeakDbL: number;
213
- truePeakDbR: number;
214
- momentaryLufs: number;
215
- shortTermLufs: number;
216
- integratedLufs: number;
217
- gainReductionDb: number;
218
- }
219
-
220
- export interface SonareWorkletSpectrumSnapshot {
221
- type: 'spectrum';
222
- frame: number;
223
- bands: Float32Array;
224
- }
225
-
226
- export type SonareWorkletTransportMessage =
227
- | SonareWorkletMeterSnapshot
228
- | SonareWorkletSpectrumSnapshot
229
- | SonareEngineTelemetryRecord;
230
-
231
- export const SONARE_METER_RING_HEADER_INTS = 4;
232
- // Record layout: [frameLo, frameHi, targetId, peakDbL, peakDbR, rmsDbL, rmsDbR,
233
- // correlation, truePeakDbL, truePeakDbR, momentaryLufs, shortTermLufs,
234
- // integratedLufs, gainReductionDb].
235
- // The sample-frame index is monotonically increasing and quickly exceeds the
236
- // 2^24 exact-integer range of a single Float32 slot (~349 s at 48 kHz), so it is
237
- // stored split across two Float32 lanes (low 24 bits + high bits) for exact
238
- // reconstruction. See encodeFrameLo/encodeFrameHi/decodeFrame.
239
- export const SONARE_METER_RING_RECORD_FLOATS = 14;
240
- export const SONARE_SPECTRUM_RING_HEADER_INTS = 5;
241
-
242
- /** Base for splitting a frame index into two exactly-representable Float32 lanes. */
243
- const SONARE_FRAME_LANE_BASE = 0x1000000; // 2^24
244
-
245
- /** Low 24 bits of a frame index (exact in Float32). */
246
- export function encodeFrameLo(frame: number): number {
247
- const f = Math.max(0, Math.floor(frame));
248
- return f % SONARE_FRAME_LANE_BASE;
249
- }
250
-
251
- /** High bits of a frame index above 2^24 (exact in Float32 up to ~2^48). */
252
- export function encodeFrameHi(frame: number): number {
253
- const f = Math.max(0, Math.floor(frame));
254
- return Math.floor(f / SONARE_FRAME_LANE_BASE);
255
- }
256
-
257
- /** Reconstruct a frame index from its low/high Float32 lanes. */
258
- export function decodeFrame(lo: number, hi: number): number {
259
- return hi * SONARE_FRAME_LANE_BASE + lo;
260
- }
261
- export const SONARE_ENGINE_RING_HEADER_INTS = 5;
262
- export const SONARE_ENGINE_COMMAND_RECORD_BYTES = 32;
263
- export const SONARE_ENGINE_TELEMETRY_RECORD_BYTES = 48;
264
-
265
- export enum SonareEngineCommandType {
266
- SetParam = 0,
267
- SetParamSmoothed = 1,
268
- TransportPlay = 2,
269
- TransportStop = 3,
270
- TransportSeekSample = 4,
271
- TransportSeekPpq = 5,
272
- SetTempoMap = 6,
273
- SetLoop = 7,
274
- SwapGraph = 8,
275
- SwapAutomation = 9,
276
- SetSoloMute = 10,
277
- AddClip = 11,
278
- RemoveClip = 12,
279
- ArmRecord = 13,
280
- Punch = 14,
281
- SetMetronome = 15,
282
- SetMarker = 16,
283
- SeekMarker = 17,
284
- }
285
-
286
- export enum SonareEngineTelemetryType {
287
- ProcessBlock = 0,
288
- Error = 1,
289
- }
290
-
291
- export enum SonareEngineTelemetryError {
292
- None = 0,
293
- CommandQueueOverflow = 1,
294
- PendingCommandOverflow = 2,
295
- BoundaryOverflow = 3,
296
- TelemetryOverflow = 4,
297
- CaptureOverflow = 5,
298
- MaxBlockExceeded = 6,
299
- UnknownTarget = 7,
300
- NonRealtimeSafeParameter = 8,
301
- NotPrepared = 9,
302
- NonQueueableCommand = 10,
303
- AutomationBindTargetOverflow = 11,
304
- StaleAutomationLanes = 12,
305
- SmoothedParameterCapacity = 13,
306
- }
307
-
308
- interface WorkletTransport {
309
- postMessage?: (
310
- message:
311
- | SonareWorkletTransportMessage
312
- | SonareEngineCaptureResponseMessage
313
- | SonareEngineTransportResponseMessage,
314
- transfer?: Transferable[],
315
- ) => void;
316
- onMeter?: (meter: SonareWorkletMeterSnapshot) => void;
317
- onSpectrum?: (spectrum: SonareWorkletSpectrumSnapshot) => void;
318
- }
319
-
320
- interface ResolvedMetronomeConfig {
321
- beatGain: number;
322
- accentGain: number;
323
- clickSamples: number;
324
- }
325
-
326
- // Fallback metronome gains/click length used by the worklet consumer until the
327
- // host posts a 'syncMetronome' config. Aligned with the embind setMetronome
328
- // defaults (src/wasm/bindings.cpp) so offline and realtime metronomes match.
329
- const DEFAULT_METRONOME_CONFIG: ResolvedMetronomeConfig = {
330
- beatGain: 0.35,
331
- accentGain: 0.7,
332
- clickSamples: 96,
333
- };
334
-
335
- function resolveMetronomeConfig(config: EngineMetronomeConfig): ResolvedMetronomeConfig {
336
- return {
337
- beatGain: config.beatGain ?? DEFAULT_METRONOME_CONFIG.beatGain,
338
- accentGain: config.accentGain ?? DEFAULT_METRONOME_CONFIG.accentGain,
339
- clickSamples: config.clickSamples ?? DEFAULT_METRONOME_CONFIG.clickSamples,
340
- };
341
- }
342
-
343
- export interface SonareMeterRingBuffer {
344
- sharedBuffer: SharedArrayBuffer;
345
- header: Int32Array;
346
- records: Float32Array;
347
- capacity: number;
348
- }
349
-
350
- export interface SonareMeterRingReadResult {
351
- nextReadIndex: number;
352
- meters: SonareWorkletMeterSnapshot[];
353
- }
354
-
355
- export interface SonareSpectrumRingBuffer {
356
- sharedBuffer: SharedArrayBuffer;
357
- header: Int32Array;
358
- records: Float32Array;
359
- capacity: number;
360
- bands: number;
361
- }
362
-
363
- export interface SonareSpectrumRingReadResult {
364
- nextReadIndex: number;
365
- spectra: SonareWorkletSpectrumSnapshot[];
366
- }
367
-
368
- export interface SonareEngineCommandRecord {
369
- type: SonareEngineCommandType | number;
370
- targetId?: number;
371
- sampleTime?: number | bigint;
372
- argFloat?: number;
373
- argInt?: number | bigint;
374
- }
375
-
376
- // Out-of-band control messages posted from the main-thread SonareEngine facade
377
- // to the worklet engine processor over node.port. Unlike SonareEngineCommandRecord
378
- // (a small POD POSTed/ringed every block) these carry bulk/structured payloads
379
- // (clip audio buffers, marker lists, metronome config) that cannot fit the
380
- // fixed-size SAB command record, so they are applied OUTSIDE process() — the
381
- // audio-thread equivalent of the engine's control-thread RtPublisher setters.
382
- export interface SonareEngineSyncClipsMessage {
383
- type: 'syncClips';
384
- clips: EngineClip[];
385
- }
386
-
387
- export interface SonareEngineSyncClipsDeltaMessage {
388
- type: 'syncClipsDelta';
389
- upserts: EngineClip[];
390
- removeIds: number[];
391
- }
392
-
393
- export interface SonareEngineSyncMidiClipsMessage {
394
- type: 'syncMidiClips';
395
- clips: EngineMidiClipSchedule[];
396
- }
397
-
398
- export interface SonareEngineSyncMarkersMessage {
399
- type: 'syncMarkers';
400
- markers: EngineMarker[];
401
- }
402
-
403
- export interface SonareEngineSyncMetronomeMessage {
404
- type: 'syncMetronome';
405
- config: EngineMetronomeConfig;
406
- }
407
-
408
- export interface SonareEngineSyncAutomationMessage {
409
- type: 'syncAutomation';
410
- paramId: number;
411
- points: EngineAutomationPoint[];
412
- }
413
-
414
- export interface SonareEngineSyncTempoMessage {
415
- type: 'syncTempo';
416
- bpm: number;
417
- timeSignature: { numerator: number; denominator: number };
418
- tempoSegments?: EngineTempoSegment[];
419
- timeSignatureSegments?: EngineTimeSignatureSegment[];
420
- }
421
-
422
- export interface SonareEngineSyncMixerMessage {
423
- type: 'syncMixer';
424
- lanes: EngineTrackLane[];
425
- buses?: EngineBus[];
426
- trackStrips?: Array<{ trackId: number; sceneJson: string }>;
427
- busStrips?: Array<{ busId: number; sceneJson: string }>;
428
- masterStripJson?: string;
429
- }
430
-
431
- export interface SonareEngineSyncCaptureMessage {
432
- type: 'syncCapture';
433
- bufferFrames: number;
434
- channels: number;
435
- source: EngineCaptureStatus['source'];
436
- recordOffsetSamples: number;
437
- inputMonitor: { enabled: boolean; gain: number };
438
- }
439
-
440
- export interface SonareEngineSyncTrackStripEqBandMessage {
441
- type: 'syncTrackStripEqBand';
442
- trackId: number;
443
- bandIndex: number;
444
- bandJson: string;
445
- }
446
-
447
- export interface SonareEngineSyncMasterStripEqBandMessage {
448
- type: 'syncMasterStripEqBand';
449
- bandIndex: number;
450
- bandJson: string;
451
- }
452
-
453
- export interface SonareEngineSyncTrackStripInsertBypassedMessage {
454
- type: 'syncTrackStripInsertBypassed';
455
- trackId: number;
456
- insertIndex: number;
457
- bypassed: boolean;
458
- resetOnBypass: boolean;
459
- }
460
-
461
- export interface SonareEngineSyncMasterStripInsertBypassedMessage {
462
- type: 'syncMasterStripInsertBypassed';
463
- insertIndex: number;
464
- bypassed: boolean;
465
- resetOnBypass: boolean;
466
- }
467
-
468
- export interface SonareEngineSyncBuiltinInstrumentMessage {
469
- type: 'syncBuiltinInstrument';
470
- destinationId: number;
471
- config: { destinationId?: number } & Record<string, unknown>;
472
- }
473
-
474
- export interface SonareEngineSyncSynthInstrumentMessage {
475
- type: 'syncSynthInstrument';
476
- destinationId: number;
477
- patch: Record<string, unknown> | string;
478
- }
479
-
480
- export interface SonareEngineSyncSf2InstrumentMessage {
481
- type: 'syncSf2Instrument';
482
- destinationId: number;
483
- config: { destinationId?: number; gain?: number; polyphony?: number };
484
- }
485
-
486
- export interface SonareEngineSyncLoadSoundFontMessage {
487
- type: 'syncLoadSoundFont';
488
- data: Uint8Array;
489
- }
490
-
491
- export interface SonareEngineSyncMidiNoteMessage {
492
- type: 'syncMidiNoteOn' | 'syncMidiNoteOff';
493
- destinationId: number;
494
- group: number;
495
- channel: number;
496
- note: number;
497
- velocity: number;
498
- renderFrame: number;
499
- }
500
-
501
- export interface SonareEngineSyncMidiCcMessage {
502
- type: 'syncMidiCc';
503
- destinationId: number;
504
- group: number;
505
- channel: number;
506
- controller: number;
507
- value: number;
508
- renderFrame: number;
509
- }
510
-
511
- export interface SonareEngineSyncMidiPanicMessage {
512
- type: 'syncMidiPanic';
513
- renderFrame: number;
514
- }
515
-
516
- type SonareEngineInstrumentSyncMessage =
517
- | SonareEngineSyncBuiltinInstrumentMessage
518
- | SonareEngineSyncSynthInstrumentMessage
519
- | SonareEngineSyncSf2InstrumentMessage
520
- | SonareEngineSyncLoadSoundFontMessage;
521
-
522
- export type SonareEngineSyncMessage =
523
- | SonareEngineSyncClipsMessage
524
- | SonareEngineSyncClipsDeltaMessage
525
- | SonareEngineSyncMidiClipsMessage
526
- | SonareEngineSyncMarkersMessage
527
- | SonareEngineSyncMetronomeMessage
528
- | SonareEngineSyncAutomationMessage
529
- | SonareEngineSyncTempoMessage
530
- | SonareEngineSyncMixerMessage
531
- | SonareEngineSyncCaptureMessage
532
- | SonareEngineSyncTrackStripEqBandMessage
533
- | SonareEngineSyncMasterStripEqBandMessage
534
- | SonareEngineSyncTrackStripInsertBypassedMessage
535
- | SonareEngineSyncMasterStripInsertBypassedMessage
536
- | SonareEngineSyncBuiltinInstrumentMessage
537
- | SonareEngineSyncSynthInstrumentMessage
538
- | SonareEngineSyncSf2InstrumentMessage
539
- | SonareEngineSyncLoadSoundFontMessage
540
- | SonareEngineSyncMidiNoteMessage
541
- | SonareEngineSyncMidiCcMessage
542
- | SonareEngineSyncMidiPanicMessage;
543
-
544
- export interface SonareEngineTelemetryRecord {
545
- type: SonareEngineTelemetryType | number;
546
- error: SonareEngineTelemetryError | number;
547
- renderFrame: number;
548
- timelineSample: number;
549
- audibleTimelineSample: number;
550
- graphLatencySamplesQ8: number;
551
- value: number;
552
- }
553
-
554
- export interface SonareEngineCommandRingBuffer {
555
- sharedBuffer: SharedArrayBuffer;
556
- header: Int32Array;
557
- view: DataView;
558
- capacity: number;
559
- }
560
-
561
- export interface SonareEngineTelemetryRingBuffer {
562
- sharedBuffer: SharedArrayBuffer;
563
- header: Int32Array;
564
- view: DataView;
565
- capacity: number;
566
- }
567
-
568
- export interface SonareEngineTelemetryRingReadResult {
569
- nextReadIndex: number;
570
- telemetry: SonareEngineTelemetryRecord[];
571
- }
572
-
573
- interface SharedMeterRingWriter {
574
- header: Int32Array;
575
- records: Float32Array;
576
- capacity: number;
577
- }
578
-
579
- interface SharedSpectrumRingWriter {
580
- header: Int32Array;
581
- records: Float32Array;
582
- capacity: number;
583
- bands: number;
584
- recordFloats: number;
585
- }
586
-
587
- interface WorkletPort {
588
- postMessage?: (message: unknown, transfer?: Transferable[]) => void;
589
- onmessage?: (event: { data: unknown }) => void;
590
- addEventListener?: (type: 'message', listener: (event: { data: unknown }) => void) => void;
591
- start?: () => void;
592
- }
593
-
594
- export interface SonareEngineCaptureRequestMessage {
595
- type: 'captureRequest';
596
- requestId: number;
597
- op: 'status' | 'read' | 'reset';
598
- }
599
-
600
- export interface SonareEngineCaptureResponseMessage {
601
- type: 'captureResponse';
602
- requestId: number;
603
- ok: boolean;
604
- status?: EngineCaptureStatus;
605
- channels?: Float32Array[] | number[][];
606
- error?: string;
607
- }
608
-
609
- export interface SonareEngineTransportRequestMessage {
610
- type: 'transportRequest';
611
- requestId: number;
612
- op: 'state';
613
- }
614
-
615
- export interface SonareEngineTransportResponseMessage {
616
- type: 'transportResponse';
617
- requestId: number;
618
- ok: boolean;
619
- state?: EngineTransportState;
620
- error?: string;
621
- }
622
-
623
- function toDb(value: number): number {
624
- return value > 0 ? 20 * Math.log10(value) : Number.NEGATIVE_INFINITY;
625
- }
626
-
627
- function isRecord(value: unknown): value is Record<string, unknown> {
628
- return typeof value === 'object' && value !== null;
629
- }
630
-
631
- function isWorkletMessage(value: unknown): value is SonareWorkletMessage {
632
- if (!isRecord(value) || typeof value.type !== 'string') {
633
- return false;
634
- }
635
- return (
636
- value.type === 'scheduleInsertAutomation' ||
637
- value.type === 'setMeterInterval' ||
638
- value.type === 'destroy'
639
- );
640
- }
641
-
642
- function isEngineCommandRecord(value: unknown): value is SonareEngineCommandRecord {
643
- return isRecord(value) && typeof value.type === 'number';
644
- }
645
-
646
- function isEngineSyncMessage(value: unknown): value is SonareEngineSyncMessage {
647
- if (!isRecord(value) || typeof value.type !== 'string') {
648
- return false;
649
- }
650
- return (
651
- value.type === 'syncClips' ||
652
- value.type === 'syncClipsDelta' ||
653
- value.type === 'syncMidiClips' ||
654
- value.type === 'syncMarkers' ||
655
- value.type === 'syncMetronome' ||
656
- value.type === 'syncAutomation' ||
657
- value.type === 'syncTempo' ||
658
- value.type === 'syncMixer' ||
659
- value.type === 'syncCapture' ||
660
- value.type === 'syncTrackStripEqBand' ||
661
- value.type === 'syncMasterStripEqBand' ||
662
- value.type === 'syncTrackStripInsertBypassed' ||
663
- value.type === 'syncMasterStripInsertBypassed' ||
664
- value.type === 'syncBuiltinInstrument' ||
665
- value.type === 'syncSynthInstrument' ||
666
- value.type === 'syncSf2Instrument' ||
667
- value.type === 'syncLoadSoundFont' ||
668
- value.type === 'syncMidiNoteOn' ||
669
- value.type === 'syncMidiNoteOff' ||
670
- value.type === 'syncMidiCc' ||
671
- value.type === 'syncMidiPanic'
672
- );
673
- }
674
-
675
- function isEngineCaptureRequestMessage(value: unknown): value is SonareEngineCaptureRequestMessage {
676
- return (
677
- isRecord(value) &&
678
- value.type === 'captureRequest' &&
679
- typeof value.requestId === 'number' &&
680
- (value.op === 'status' || value.op === 'read' || value.op === 'reset')
681
- );
682
- }
683
-
684
- function isEngineCaptureResponseMessage(
685
- value: unknown,
686
- ): value is SonareEngineCaptureResponseMessage {
687
- return (
688
- isRecord(value) &&
689
- value.type === 'captureResponse' &&
690
- typeof value.requestId === 'number' &&
691
- typeof value.ok === 'boolean'
692
- );
693
- }
694
-
695
- function isEngineTransportRequestMessage(
696
- value: unknown,
697
- ): value is SonareEngineTransportRequestMessage {
698
- return (
699
- isRecord(value) &&
700
- value.type === 'transportRequest' &&
701
- typeof value.requestId === 'number' &&
702
- value.op === 'state'
703
- );
704
- }
705
-
706
- function isEngineTransportResponseMessage(
707
- value: unknown,
708
- ): value is SonareEngineTransportResponseMessage {
709
- return (
710
- isRecord(value) &&
711
- value.type === 'transportResponse' &&
712
- typeof value.requestId === 'number' &&
713
- typeof value.ok === 'boolean'
714
- );
715
- }
716
-
717
- function isRealtimeVoiceChangerMessage(value: unknown): value is SonareRealtimeVoiceChangerMessage {
718
- if (!isRecord(value) || typeof value.type !== 'string') {
719
- return false;
720
- }
721
- return value.type === 'setConfig' || value.type === 'reset' || value.type === 'destroy';
722
- }
723
-
724
- function isEngineTelemetryRecord(value: unknown): value is SonareEngineTelemetryRecord {
725
- return (
726
- isRecord(value) &&
727
- typeof value.type === 'number' &&
728
- typeof value.error === 'number' &&
729
- typeof value.renderFrame === 'number' &&
730
- typeof value.timelineSample === 'number' &&
731
- typeof value.audibleTimelineSample === 'number' &&
732
- typeof value.graphLatencySamplesQ8 === 'number' &&
733
- typeof value.value === 'number'
734
- );
735
- }
736
-
737
- function isMeterSnapshot(value: unknown): value is SonareWorkletMeterSnapshot {
738
- return (
739
- isRecord(value) &&
740
- value.type === 'meter' &&
741
- typeof value.frame === 'number' &&
742
- typeof value.peakDbL === 'number' &&
743
- typeof value.peakDbR === 'number' &&
744
- typeof value.rmsDbL === 'number' &&
745
- typeof value.rmsDbR === 'number' &&
746
- typeof value.correlation === 'number' &&
747
- (typeof value.targetId === 'number' || value.targetId === undefined)
748
- );
749
- }
750
-
751
- export function sonareMeterRingBufferByteLength(capacity: number): number {
752
- const clampedCapacity = Math.max(1, Math.floor(capacity));
753
- return (
754
- SONARE_METER_RING_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT +
755
- clampedCapacity * SONARE_METER_RING_RECORD_FLOATS * Float32Array.BYTES_PER_ELEMENT
756
- );
757
- }
758
-
759
- export function createSonareMeterRingBuffer(capacity = 128): SonareMeterRingBuffer {
760
- const clampedCapacity = Math.max(1, Math.floor(capacity));
761
- const sharedBuffer = new SharedArrayBuffer(sonareMeterRingBufferByteLength(clampedCapacity));
762
- const ring = meterRingFromSharedBuffer(sharedBuffer, clampedCapacity);
763
- Atomics.store(ring.header, 0, 0);
764
- Atomics.store(ring.header, 1, clampedCapacity);
765
- Atomics.store(ring.header, 2, SONARE_METER_RING_RECORD_FLOATS);
766
- Atomics.store(ring.header, 3, 0);
767
- return { sharedBuffer, header: ring.header, records: ring.records, capacity: ring.capacity };
768
- }
769
-
770
- export function readSonareMeterRingBuffer(
771
- ring: SonareMeterRingBuffer,
772
- readIndex = 0,
773
- ): SonareMeterRingReadResult {
774
- const writeIndex = Atomics.load(ring.header, 0);
775
- const recordFloats = Atomics.load(ring.header, 2) || SONARE_METER_RING_RECORD_FLOATS;
776
- const nextReadIndex = Math.max(0, Math.min(readIndex, writeIndex));
777
- const firstReadable = Math.max(nextReadIndex, writeIndex - ring.capacity);
778
- const meters: SonareWorkletMeterSnapshot[] = [];
779
- for (let index = firstReadable; index < writeIndex; index++) {
780
- const offset = (index % ring.capacity) * recordFloats;
781
- meters.push({
782
- type: 'meter',
783
- frame: decodeFrame(ring.records[offset], ring.records[offset + 1]),
784
- targetId: ring.records[offset + 2],
785
- peakDbL: ring.records[offset + 3],
786
- peakDbR: ring.records[offset + 4],
787
- rmsDbL: ring.records[offset + 5],
788
- rmsDbR: ring.records[offset + 6],
789
- correlation: ring.records[offset + 7],
790
- truePeakDbL: ring.records[offset + 8],
791
- truePeakDbR: ring.records[offset + 9],
792
- momentaryLufs: ring.records[offset + 10],
793
- shortTermLufs: ring.records[offset + 11],
794
- integratedLufs: ring.records[offset + 12],
795
- gainReductionDb: ring.records[offset + 13],
796
- });
797
- }
798
- return { nextReadIndex: writeIndex, meters };
799
- }
800
-
801
- export function sonareSpectrumRingBufferByteLength(capacity: number, bands = 16): number {
802
- const clampedCapacity = Math.max(1, Math.floor(capacity));
803
- const clampedBands = Math.max(1, Math.floor(bands));
804
- // Record layout: [frameLo, frameHi, bandCount, band0, band1, ...]. frame is
805
- // split across two Float32 lanes for exact reconstruction beyond 2^24.
806
- return (
807
- SONARE_SPECTRUM_RING_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT +
808
- clampedCapacity * (3 + clampedBands) * Float32Array.BYTES_PER_ELEMENT
809
- );
810
- }
811
-
812
- export function createSonareSpectrumRingBuffer(
813
- capacity = 128,
814
- bands = 16,
815
- ): SonareSpectrumRingBuffer {
816
- const clampedCapacity = Math.max(1, Math.floor(capacity));
817
- const clampedBands = Math.max(1, Math.floor(bands));
818
- const sharedBuffer = new SharedArrayBuffer(
819
- sonareSpectrumRingBufferByteLength(clampedCapacity, clampedBands),
820
- );
821
- const ring = spectrumRingFromSharedBuffer(sharedBuffer, clampedCapacity, clampedBands);
822
- Atomics.store(ring.header, 0, 0);
823
- Atomics.store(ring.header, 1, clampedCapacity);
824
- Atomics.store(ring.header, 2, ring.recordFloats);
825
- Atomics.store(ring.header, 3, clampedBands);
826
- Atomics.store(ring.header, 4, 0);
827
- return {
828
- sharedBuffer,
829
- header: ring.header,
830
- records: ring.records,
831
- capacity: ring.capacity,
832
- bands: ring.bands,
833
- };
834
- }
835
-
836
- export function readSonareSpectrumRingBuffer(
837
- ring: SonareSpectrumRingBuffer,
838
- readIndex = 0,
839
- ): SonareSpectrumRingReadResult {
840
- const writeIndex = Atomics.load(ring.header, 0);
841
- const recordFloats = Atomics.load(ring.header, 2) || 3 + ring.bands;
842
- const bands = Atomics.load(ring.header, 3) || ring.bands;
843
- const nextReadIndex = Math.max(0, Math.min(readIndex, writeIndex));
844
- const firstReadable = Math.max(nextReadIndex, writeIndex - ring.capacity);
845
- const spectra: SonareWorkletSpectrumSnapshot[] = [];
846
- for (let index = firstReadable; index < writeIndex; index++) {
847
- const offset = (index % ring.capacity) * recordFloats;
848
- const values = new Float32Array(bands);
849
- values.set(ring.records.subarray(offset + 3, offset + 3 + bands));
850
- spectra.push({
851
- type: 'spectrum',
852
- frame: decodeFrame(ring.records[offset], ring.records[offset + 1]),
853
- bands: values,
854
- });
855
- }
856
- return { nextReadIndex: writeIndex, spectra };
857
- }
858
-
859
- export function sonareEngineCommandRingBufferByteLength(capacity: number): number {
860
- const clampedCapacity = Math.max(1, Math.floor(capacity));
861
- return (
862
- SONARE_ENGINE_RING_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT +
863
- clampedCapacity * SONARE_ENGINE_COMMAND_RECORD_BYTES
864
- );
865
- }
866
-
867
- export function sonareEngineTelemetryRingBufferByteLength(capacity: number): number {
868
- const clampedCapacity = Math.max(1, Math.floor(capacity));
869
- return (
870
- SONARE_ENGINE_RING_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT +
871
- clampedCapacity * SONARE_ENGINE_TELEMETRY_RECORD_BYTES
872
- );
873
- }
874
-
875
- export function createSonareEngineCommandRingBuffer(capacity = 128): SonareEngineCommandRingBuffer {
876
- const clampedCapacity = Math.max(1, Math.floor(capacity));
877
- const sharedBuffer = new SharedArrayBuffer(
878
- sonareEngineCommandRingBufferByteLength(clampedCapacity),
879
- );
880
- const ring = engineRingFromSharedBuffer(
881
- sharedBuffer,
882
- SONARE_ENGINE_COMMAND_RECORD_BYTES,
883
- clampedCapacity,
884
- );
885
- return { sharedBuffer, header: ring.header, view: ring.view, capacity: ring.capacity };
886
- }
887
-
888
- export function createSonareEngineTelemetryRingBuffer(
889
- capacity = 128,
890
- ): SonareEngineTelemetryRingBuffer {
891
- const clampedCapacity = Math.max(1, Math.floor(capacity));
892
- const sharedBuffer = new SharedArrayBuffer(
893
- sonareEngineTelemetryRingBufferByteLength(clampedCapacity),
894
- );
895
- const ring = engineRingFromSharedBuffer(
896
- sharedBuffer,
897
- SONARE_ENGINE_TELEMETRY_RECORD_BYTES,
898
- clampedCapacity,
899
- );
900
- return { sharedBuffer, header: ring.header, view: ring.view, capacity: ring.capacity };
901
- }
902
-
903
- export function pushSonareEngineCommandRingBuffer(
904
- ring: SonareEngineCommandRingBuffer,
905
- command: SonareEngineCommandRecord,
906
- ): boolean {
907
- const writeIndex = Atomics.load(ring.header, 0);
908
- const readIndex = Atomics.load(ring.header, 1);
909
- if (writeIndex - readIndex >= ring.capacity) {
910
- Atomics.add(ring.header, 4, 1);
911
- return false;
912
- }
913
- writeEngineCommandRecord(
914
- ring.view,
915
- recordOffset(writeIndex, ring.capacity, SONARE_ENGINE_COMMAND_RECORD_BYTES),
916
- command,
917
- );
918
- Atomics.store(ring.header, 0, writeIndex + 1);
919
- return true;
920
- }
921
-
922
- export function popSonareEngineCommandRingBuffer(
923
- ring: SonareEngineCommandRingBuffer,
924
- ): SonareEngineCommandRecord | null {
925
- const readIndex = Atomics.load(ring.header, 1);
926
- const writeIndex = Atomics.load(ring.header, 0);
927
- if (readIndex >= writeIndex) {
928
- return null;
929
- }
930
- const command = readEngineCommandRecord(
931
- ring.view,
932
- recordOffset(readIndex, ring.capacity, SONARE_ENGINE_COMMAND_RECORD_BYTES),
933
- );
934
- Atomics.store(ring.header, 1, readIndex + 1);
935
- return command;
936
- }
937
-
938
- export function writeSonareEngineTelemetryRingBuffer(
939
- ring: SonareEngineTelemetryRingBuffer,
940
- telemetry: SonareEngineTelemetryRecord,
941
- ): void {
942
- const writeIndex = Atomics.load(ring.header, 0);
943
- writeEngineTelemetryRecord(
944
- ring.view,
945
- recordOffset(writeIndex, ring.capacity, SONARE_ENGINE_TELEMETRY_RECORD_BYTES),
946
- telemetry,
947
- );
948
- Atomics.store(ring.header, 0, writeIndex + 1);
949
- if (writeIndex + 1 > ring.capacity) {
950
- Atomics.store(ring.header, 4, writeIndex + 1 - ring.capacity);
951
- }
952
- }
953
-
954
- export function readSonareEngineTelemetryRingBuffer(
955
- ring: SonareEngineTelemetryRingBuffer,
956
- readIndex = 0,
957
- ): SonareEngineTelemetryRingReadResult {
958
- const writeIndex = Atomics.load(ring.header, 0);
959
- const nextReadIndex = Math.max(0, Math.min(readIndex, writeIndex));
960
- const firstReadable = Math.max(nextReadIndex, writeIndex - ring.capacity);
961
- const telemetry: SonareEngineTelemetryRecord[] = [];
962
- for (let index = firstReadable; index < writeIndex; index++) {
963
- telemetry.push(
964
- readEngineTelemetryRecord(
965
- ring.view,
966
- recordOffset(index, ring.capacity, SONARE_ENGINE_TELEMETRY_RECORD_BYTES),
967
- ),
968
- );
969
- }
970
- return { nextReadIndex: writeIndex, telemetry };
971
- }
972
-
973
- function meterRingFromSharedBuffer(
974
- sharedBuffer: SharedArrayBuffer,
975
- fallbackCapacity?: number,
976
- ): SharedMeterRingWriter {
977
- const headerBytes = SONARE_METER_RING_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT;
978
- const header = new Int32Array(sharedBuffer, 0, SONARE_METER_RING_HEADER_INTS);
979
- const existingCapacity = Atomics.load(header, 1);
980
- const capacity = Math.max(1, Math.floor(existingCapacity || fallbackCapacity || 1));
981
- const minBytes = sonareMeterRingBufferByteLength(capacity);
982
- if (sharedBuffer.byteLength < minBytes) {
983
- throw new Error('meterSharedBuffer is too small for the requested ring capacity.');
984
- }
985
- Atomics.store(header, 1, capacity);
986
- Atomics.store(header, 2, SONARE_METER_RING_RECORD_FLOATS);
987
- return {
988
- header,
989
- records: new Float32Array(
990
- sharedBuffer,
991
- headerBytes,
992
- capacity * SONARE_METER_RING_RECORD_FLOATS,
993
- ),
994
- capacity,
995
- };
996
- }
997
-
998
- function spectrumRingFromSharedBuffer(
999
- sharedBuffer: SharedArrayBuffer,
1000
- fallbackCapacity?: number,
1001
- fallbackBands?: number,
1002
- ): SharedSpectrumRingWriter {
1003
- const headerBytes = SONARE_SPECTRUM_RING_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT;
1004
- const header = new Int32Array(sharedBuffer, 0, SONARE_SPECTRUM_RING_HEADER_INTS);
1005
- const existingCapacity = Atomics.load(header, 1);
1006
- const existingBands = Atomics.load(header, 3);
1007
- const capacity = Math.max(1, Math.floor(existingCapacity || fallbackCapacity || 1));
1008
- const bands = Math.max(1, Math.floor(existingBands || fallbackBands || 16));
1009
- const recordFloats = 3 + bands;
1010
- const minBytes = sonareSpectrumRingBufferByteLength(capacity, bands);
1011
- if (sharedBuffer.byteLength < minBytes) {
1012
- throw new Error('spectrumSharedBuffer is too small for the requested ring capacity.');
1013
- }
1014
- Atomics.store(header, 1, capacity);
1015
- Atomics.store(header, 2, recordFloats);
1016
- Atomics.store(header, 3, bands);
1017
- return {
1018
- header,
1019
- records: new Float32Array(sharedBuffer, headerBytes, capacity * recordFloats),
1020
- capacity,
1021
- bands,
1022
- recordFloats,
1023
- };
1024
- }
1025
-
1026
- function engineRingFromSharedBuffer(
1027
- sharedBuffer: SharedArrayBuffer,
1028
- recordBytes: number,
1029
- fallbackCapacity?: number,
1030
- ): { header: Int32Array; view: DataView; capacity: number } {
1031
- const headerBytes = SONARE_ENGINE_RING_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT;
1032
- const header = new Int32Array(sharedBuffer, 0, SONARE_ENGINE_RING_HEADER_INTS);
1033
- const existingCapacity = Atomics.load(header, 2);
1034
- const capacity = Math.max(1, Math.floor(existingCapacity || fallbackCapacity || 1));
1035
- const minBytes = headerBytes + capacity * recordBytes;
1036
- if (sharedBuffer.byteLength < minBytes) {
1037
- throw new Error('engine SharedArrayBuffer is too small for the requested ring capacity.');
1038
- }
1039
- Atomics.store(header, 2, capacity);
1040
- Atomics.store(header, 3, recordBytes);
1041
- return {
1042
- header,
1043
- view: new DataView(sharedBuffer, headerBytes, capacity * recordBytes),
1044
- capacity,
1045
- };
1046
- }
1047
-
1048
- function recordOffset(index: number, capacity: number, recordBytes: number): number {
1049
- return (index % capacity) * recordBytes;
1050
- }
1051
-
1052
- function toBigInt64(value: number | bigint | undefined, fallback: bigint): bigint {
1053
- if (typeof value === 'bigint') {
1054
- return value;
1055
- }
1056
- if (typeof value === 'number') {
1057
- return BigInt(Math.trunc(value));
1058
- }
1059
- return fallback;
1060
- }
1061
-
1062
- function writeEngineCommandRecord(
1063
- view: DataView,
1064
- offset: number,
1065
- command: SonareEngineCommandRecord,
1066
- ): void {
1067
- view.setUint32(offset, command.type, true);
1068
- view.setUint32(offset + 4, command.targetId ?? 0, true);
1069
- view.setBigInt64(offset + 8, toBigInt64(command.sampleTime, -1n), true);
1070
- // argFloat occupies a full 8-byte Float64 slot (replacing the old Float32 +
1071
- // 4-byte pad) so PPQ scalars carried here keep full double precision over the
1072
- // SAB transport, matching the engine's double-precision seek/loop contract.
1073
- view.setFloat64(offset + 16, command.argFloat ?? 0, true);
1074
- view.setBigInt64(offset + 24, toBigInt64(command.argInt, 0n), true);
1075
- }
1076
-
1077
- function readEngineCommandRecord(view: DataView, offset: number): SonareEngineCommandRecord {
1078
- return {
1079
- type: view.getUint32(offset, true),
1080
- targetId: view.getUint32(offset + 4, true),
1081
- sampleTime: Number(view.getBigInt64(offset + 8, true)),
1082
- argFloat: view.getFloat64(offset + 16, true),
1083
- argInt: Number(view.getBigInt64(offset + 24, true)),
1084
- };
1085
- }
1086
-
1087
- function writeEngineTelemetryRecord(
1088
- view: DataView,
1089
- offset: number,
1090
- telemetry: SonareEngineTelemetryRecord,
1091
- ): void {
1092
- view.setUint32(offset, telemetry.type, true);
1093
- view.setUint32(offset + 4, telemetry.error, true);
1094
- view.setBigInt64(offset + 8, BigInt(Math.trunc(telemetry.renderFrame)), true);
1095
- view.setBigInt64(offset + 16, BigInt(Math.trunc(telemetry.timelineSample)), true);
1096
- view.setBigInt64(offset + 24, BigInt(Math.trunc(telemetry.audibleTimelineSample)), true);
1097
- view.setInt32(offset + 32, telemetry.graphLatencySamplesQ8, true);
1098
- view.setUint32(offset + 36, telemetry.value, true);
1099
- view.setBigInt64(offset + 40, 0n, true);
1100
- }
1101
-
1102
- function readEngineTelemetryRecord(view: DataView, offset: number): SonareEngineTelemetryRecord {
1103
- return {
1104
- type: view.getUint32(offset, true),
1105
- error: view.getUint32(offset + 4, true),
1106
- renderFrame: Number(view.getBigInt64(offset + 8, true)),
1107
- timelineSample: Number(view.getBigInt64(offset + 16, true)),
1108
- audibleTimelineSample: Number(view.getBigInt64(offset + 24, true)),
1109
- graphLatencySamplesQ8: view.getInt32(offset + 32, true),
1110
- value: view.getUint32(offset + 36, true),
1111
- };
1112
- }
1113
-
1114
- function telemetryFromEngine(telemetry: EngineTelemetry): SonareEngineTelemetryRecord {
1115
- return {
1116
- type: telemetry.type,
1117
- error: telemetry.error,
1118
- renderFrame: telemetry.renderFrame,
1119
- timelineSample: telemetry.timelineSample,
1120
- audibleTimelineSample: telemetry.audibleTimelineSample,
1121
- graphLatencySamplesQ8: telemetry.graphLatencySamplesQ8,
1122
- value: telemetry.value,
1123
- };
1124
- }
1125
-
1126
- function meterFromEngine(meter: EngineMeterTelemetry): SonareWorkletMeterSnapshot {
1127
- return {
1128
- type: 'meter',
1129
- targetId: meter.targetId,
1130
- frame: meter.renderFrame,
1131
- peakDbL: meter.peakDbL,
1132
- peakDbR: meter.peakDbR,
1133
- rmsDbL: meter.rmsDbL,
1134
- rmsDbR: meter.rmsDbR,
1135
- correlation: meter.correlation,
1136
- truePeakDbL: meter.truePeakDbL,
1137
- truePeakDbR: meter.truePeakDbR,
1138
- momentaryLufs: meter.momentaryLufs,
1139
- shortTermLufs: meter.shortTermLufs,
1140
- integratedLufs: meter.integratedLufs,
1141
- gainReductionDb: meter.gainReductionDb,
1142
- };
1143
- }
1144
-
1145
- function magnitudeToDb(value: number): number {
1146
- return value > 1.0e-12 ? 20 * Math.log10(value) : -120;
1147
- }
1148
-
1149
228
  /**
1150
229
  * AudioWorklet-style mixer bridge backed by the package's single `sonare.wasm`.
1151
230
  *
@@ -1471,6 +550,7 @@ export class SonareRealtimeEngineWorkletProcessor {
1471
550
  private commandRing?: SonareEngineCommandRingBuffer;
1472
551
  private telemetryRing?: SonareEngineTelemetryRingBuffer;
1473
552
  private meterRing?: SharedMeterRingWriter;
553
+ private scopeRing?: SharedScopeRingWriter;
1474
554
  private transport?: WorkletTransport;
1475
555
  private meterIntervalFrames: number;
1476
556
  private lastMeterFrame = Number.NEGATIVE_INFINITY;
@@ -1514,6 +594,13 @@ export class SonareRealtimeEngineWorkletProcessor {
1514
594
  this.meterRing = options.meterSharedBuffer
1515
595
  ? meterRingFromSharedBuffer(options.meterSharedBuffer, options.meterRingCapacity)
1516
596
  : undefined;
597
+ this.scopeRing = options.scopeSharedBuffer
598
+ ? scopeRingFromSharedBuffer(
599
+ options.scopeSharedBuffer,
600
+ options.scopeRingCapacity,
601
+ options.scopeBands,
602
+ )
603
+ : undefined;
1517
604
  this.engine = new RealtimeEngine(this.sampleRate, this.blockSize);
1518
605
  // Allocate persistent WASM-heap scratch (worst case: channelCount channels x
1519
606
  // blockSize frames) and acquire the per-channel heap views once.
@@ -1522,6 +609,13 @@ export class SonareRealtimeEngineWorkletProcessor {
1522
609
  for (let ch = 0; ch < this.channelCount; ch++) {
1523
610
  this.channelBuffers[ch] = this.engine.getChannelBuffer(ch, this.blockSize);
1524
611
  }
612
+ // Arm the engine's scope producer only when a scope ring was provided. The
613
+ // band count follows the ring's record layout so writeScopeRing never
614
+ // overruns its slot.
615
+ if (this.scopeRing) {
616
+ const interval = Math.max(1, Math.floor(options.scopeIntervalFrames ?? this.blockSize));
617
+ this.engine.configureScopeTelemetry(interval, this.scopeRing.bands);
618
+ }
1525
619
  }
1526
620
 
1527
621
  process(inputs: WorkletInput, outputs: WorkletOutput): boolean {
@@ -1599,6 +693,7 @@ export class SonareRealtimeEngineWorkletProcessor {
1599
693
  }
1600
694
  this.publishTelemetry();
1601
695
  this.publishMeters();
696
+ this.publishScope();
1602
697
  return true;
1603
698
  }
1604
699
 
@@ -1686,6 +781,9 @@ export class SonareRealtimeEngineWorkletProcessor {
1686
781
  if (message.masterStripJson) {
1687
782
  this.engine.setMasterStripJson(message.masterStripJson);
1688
783
  }
784
+ for (const binding of message.laneSidechains ?? []) {
785
+ this.engine.setLaneSidechain(binding.trackId, binding.insertIndex, binding.sourceTrackId);
786
+ }
1689
787
  break;
1690
788
  case 'syncCapture':
1691
789
  this.engine.setCaptureBuffer(message.channels, message.bufferFrames);
@@ -1714,6 +812,36 @@ export class SonareRealtimeEngineWorkletProcessor {
1714
812
  message.resetOnBypass,
1715
813
  );
1716
814
  break;
815
+ case 'syncTrackStripInsertParamByName':
816
+ this.engine.setTrackStripInsertParamByName(
817
+ message.trackId,
818
+ message.insertIndex,
819
+ message.paramName,
820
+ message.value,
821
+ );
822
+ break;
823
+ case 'syncMasterStripInsertParamByName':
824
+ this.engine.setMasterStripInsertParamByName(
825
+ message.insertIndex,
826
+ message.paramName,
827
+ message.value,
828
+ );
829
+ break;
830
+ case 'syncTrackStripPan':
831
+ this.engine.setTrackStripPan(message.trackId, message.pan);
832
+ break;
833
+ case 'syncTrackStripPanLaw':
834
+ this.engine.setTrackStripPanLaw(message.trackId, message.panLaw);
835
+ break;
836
+ case 'syncTrackStripPanMode':
837
+ this.engine.setTrackStripPanMode(message.trackId, message.panMode);
838
+ break;
839
+ case 'syncTrackStripDualPan':
840
+ this.engine.setTrackStripDualPan(message.trackId, message.leftPan, message.rightPan);
841
+ break;
842
+ case 'syncTrackStripChannelDelaySamples':
843
+ this.engine.setTrackStripChannelDelaySamples(message.trackId, message.delaySamples);
844
+ break;
1717
845
  case 'syncBuiltinInstrument':
1718
846
  this.engine.setBuiltinInstrument(message.config, message.destinationId);
1719
847
  break;
@@ -1966,6 +1094,17 @@ export class SonareRealtimeEngineWorkletProcessor {
1966
1094
  this.transport?.postMessage?.(record);
1967
1095
  }
1968
1096
 
1097
+ // Drains the engine meter telemetry queue into the stereo meter ring / transport.
1098
+ //
1099
+ // Shared-queue contract: `drainMeterTelemetry` and `drainMeterTelemetryWide`
1100
+ // pop the SAME single-consumer telemetry queue, so exactly ONE of them may run
1101
+ // per engine. The live worklet path owns the queue via the stereo drain below;
1102
+ // the worklet meter ring (SONARE_METER_RING_RECORD_FLOATS) is a fixed stereo
1103
+ // layout carrying planes 0/1 plus the correlation/LUFS summary. Per-plane
1104
+ // surround meters are NOT delivered over the live worklet ring — a host that
1105
+ // needs them must use the offline `drainMeterTelemetryWide()` API on a
1106
+ // non-worklet engine instance (do not also call it on a worklet-driven engine,
1107
+ // or the two drains will starve each other).
1969
1108
  private publishMeters(): void {
1970
1109
  if (this.meterIntervalFrames <= 0 || (!this.transport && !this.meterRing)) {
1971
1110
  return;
@@ -2023,6 +1162,46 @@ export class SonareRealtimeEngineWorkletProcessor {
2023
1162
  // writeIndex - capacity), so header slot 3 is left at its initial 0.
2024
1163
  }
2025
1164
 
1165
+ // Drains the engine's scope producer (FFT spectrum + goniometer points) into
1166
+ // the lock-free SAB scope ring. Only the embind runtime publishes scope
1167
+ // telemetry; the sonare-rt runtime owns its own transport. No allocation on
1168
+ // the render path: records are written field-by-field into the ring.
1169
+ private publishScope(): void {
1170
+ const ring = this.scopeRing;
1171
+ if (!ring) {
1172
+ return;
1173
+ }
1174
+ for (const item of this.engine.drainScopeTelemetry(64)) {
1175
+ this.writeScopeRing(ring, item);
1176
+ }
1177
+ }
1178
+
1179
+ private writeScopeRing(ring: SharedScopeRingWriter, record: EngineScopeTelemetry): void {
1180
+ const writeIndex = Atomics.load(ring.header, 0);
1181
+ const base = (writeIndex % ring.capacity) * ring.recordFloats;
1182
+ ring.records[base] = encodeFrameLo(record.renderFrame);
1183
+ ring.records[base + 1] = encodeFrameHi(record.renderFrame);
1184
+ ring.records[base + 2] = record.targetId;
1185
+ const bandCount = Math.min(ring.bands, record.bands.length);
1186
+ ring.records[base + 3] = bandCount;
1187
+ const pointCount = Math.min(ring.maxPoints, record.points.length);
1188
+ ring.records[base + 4] = pointCount;
1189
+ const bandsBase = base + SONARE_SCOPE_RING_RECORD_PREFIX_FLOATS;
1190
+ for (let i = 0; i < bandCount; i++) {
1191
+ ring.records[bandsBase + i] = record.bands[i];
1192
+ }
1193
+ const pointsBase = bandsBase + ring.bands;
1194
+ for (let i = 0; i < pointCount; i++) {
1195
+ const point = record.points[i];
1196
+ ring.records[pointsBase + 2 * i] = point.left;
1197
+ ring.records[pointsBase + 2 * i + 1] = point.right;
1198
+ }
1199
+ Atomics.store(ring.header, 0, writeIndex + 1);
1200
+ // Like writeMeterRing, writeIndex is a free-running monotonic counter; the
1201
+ // reader detects silent overrun via firstReadable, so the overflow slot
1202
+ // (header[5]) stays at its initial 0.
1203
+ }
1204
+
2026
1205
  private commandRingFromSharedBuffer(
2027
1206
  sharedBuffer: SharedArrayBuffer,
2028
1207
  fallbackCapacity?: number,
@@ -2446,11 +1625,14 @@ export class SonareRealtimeEngineNode {
2446
1625
  readonly commandRing?: SonareEngineCommandRingBuffer;
2447
1626
  readonly telemetryRing?: SonareEngineTelemetryRingBuffer;
2448
1627
  readonly meterRing?: SonareMeterRingBuffer;
1628
+ readonly scopeRing?: SonareScopeRingBuffer;
2449
1629
  readonly ready: Promise<void>;
2450
1630
  private telemetryReadIndex = 0;
2451
1631
  private meterReadIndex = 0;
1632
+ private scopeReadIndex = 0;
2452
1633
  private telemetryListeners = new Set<(telemetry: SonareEngineTelemetryRecord) => void>();
2453
1634
  private meterListeners = new Set<(meter: SonareWorkletMeterSnapshot) => void>();
1635
+ private scopeListeners = new Set<(scope: SonareWorkletScopeSnapshot) => void>();
2454
1636
  private captureRequestId = 1;
2455
1637
  private readonly captureRequests = new Map<
2456
1638
  number,
@@ -2477,12 +1659,14 @@ export class SonareRealtimeEngineNode {
2477
1659
  commandRing?: SonareEngineCommandRingBuffer,
2478
1660
  telemetryRing?: SonareEngineTelemetryRingBuffer,
2479
1661
  meterRing?: SonareMeterRingBuffer,
1662
+ scopeRing?: SonareScopeRingBuffer,
2480
1663
  ) {
2481
1664
  this.node = node;
2482
1665
  this.capabilities = capabilities;
2483
1666
  this.commandRing = commandRing;
2484
1667
  this.telemetryRing = telemetryRing;
2485
1668
  this.meterRing = meterRing;
1669
+ this.scopeRing = scopeRing;
2486
1670
  this.ready = new Promise((resolve, reject) => {
2487
1671
  this.resolveReady = resolve;
2488
1672
  this.rejectReady = reject;
@@ -2581,6 +1765,14 @@ export class SonareRealtimeEngineNode {
2581
1765
  mode === 'sab' && runtimeTarget === 'embind'
2582
1766
  ? createSonareMeterRingBuffer(options.meterRingCapacity ?? 128)
2583
1767
  : undefined;
1768
+ // Scope ring (FFT spectrum + goniometer): opt-in, embind-only. The
1769
+ // per-block FFT is heavier than the meter path, so it is created only when
1770
+ // the caller requests scope telemetry via scopeIntervalFrames > 0.
1771
+ const scopeIntervalFrames = Math.max(0, Math.floor(options.scopeIntervalFrames ?? 0));
1772
+ const scopeRing =
1773
+ mode === 'sab' && runtimeTarget === 'embind' && scopeIntervalFrames > 0
1774
+ ? createSonareScopeRingBuffer(options.scopeRingCapacity ?? 64, options.scopeBands ?? 48)
1775
+ : undefined;
2584
1776
  const channelCount = Math.max(1, Math.floor(options.channelCount ?? 2));
2585
1777
  const processorOptions: SonareRealtimeEngineWorkletProcessorOptions = {
2586
1778
  runtimeTarget,
@@ -2595,6 +1787,10 @@ export class SonareRealtimeEngineNode {
2595
1787
  telemetryRingCapacity: telemetryRing?.capacity,
2596
1788
  meterSharedBuffer: meterRing?.sharedBuffer,
2597
1789
  meterRingCapacity: meterRing?.capacity,
1790
+ scopeSharedBuffer: scopeRing?.sharedBuffer,
1791
+ scopeRingCapacity: scopeRing?.capacity,
1792
+ scopeBands: scopeRing?.bands,
1793
+ scopeIntervalFrames: scopeRing ? scopeIntervalFrames : undefined,
2598
1794
  wasmBinary: options.wasmBinary,
2599
1795
  initialSyncMessages: options.initialSyncMessages,
2600
1796
  initialCommands: options.initialCommands,
@@ -2628,6 +1824,7 @@ export class SonareRealtimeEngineNode {
2628
1824
  commandRing,
2629
1825
  telemetryRing,
2630
1826
  meterRing,
1827
+ scopeRing,
2631
1828
  );
2632
1829
  }
2633
1830
 
@@ -2723,6 +1920,21 @@ export class SonareRealtimeEngineNode {
2723
1920
  return read.meters;
2724
1921
  }
2725
1922
 
1923
+ // Drains scope telemetry (FFT spectrum + goniometer points) published into the
1924
+ // SAB scope ring and forwards each record to onScope listeners. A no-op unless
1925
+ // the node was created with scopeIntervalFrames > 0 (embind SAB mode).
1926
+ pollScope(): SonareWorkletScopeSnapshot[] {
1927
+ if (!this.scopeRing) {
1928
+ return [];
1929
+ }
1930
+ const read = readSonareScopeRingBuffer(this.scopeRing, this.scopeReadIndex);
1931
+ this.scopeReadIndex = read.nextReadIndex;
1932
+ for (const scope of read.scopes) {
1933
+ this.emitScope(scope);
1934
+ }
1935
+ return read.scopes;
1936
+ }
1937
+
2726
1938
  onTelemetry(callback: (telemetry: SonareEngineTelemetryRecord) => void): () => void {
2727
1939
  this.telemetryListeners.add(callback);
2728
1940
  return () => {
@@ -2737,6 +1949,13 @@ export class SonareRealtimeEngineNode {
2737
1949
  };
2738
1950
  }
2739
1951
 
1952
+ onScope(callback: (scope: SonareWorkletScopeSnapshot) => void): () => void {
1953
+ this.scopeListeners.add(callback);
1954
+ return () => {
1955
+ this.scopeListeners.delete(callback);
1956
+ };
1957
+ }
1958
+
2740
1959
  destroy(): void {
2741
1960
  if (this.destroyed) {
2742
1961
  return;
@@ -2754,6 +1973,7 @@ export class SonareRealtimeEngineNode {
2754
1973
  this.transportRequests.clear();
2755
1974
  this.telemetryListeners.clear();
2756
1975
  this.meterListeners.clear();
1976
+ this.scopeListeners.clear();
2757
1977
  }
2758
1978
 
2759
1979
  private emitTelemetry(telemetry: SonareEngineTelemetryRecord): void {
@@ -2768,6 +1988,12 @@ export class SonareRealtimeEngineNode {
2768
1988
  }
2769
1989
  }
2770
1990
 
1991
+ private emitScope(scope: SonareWorkletScopeSnapshot): void {
1992
+ for (const listener of this.scopeListeners) {
1993
+ listener(scope);
1994
+ }
1995
+ }
1996
+
2771
1997
  private sendCaptureRequest(
2772
1998
  op: SonareEngineCaptureRequestMessage['op'],
2773
1999
  ): Promise<SonareEngineCaptureResponseMessage> {
@@ -2811,6 +2037,11 @@ export class SonareEngine {
2811
2037
  private readonly markers = new Map<number, EngineMarker>();
2812
2038
  private readonly trackLaneIds: number[] = [];
2813
2039
  private readonly trackSends = new Map<number, EngineTrackSend[]>();
2040
+ private readonly trackOutputBus = new Map<number, number>();
2041
+ private readonly laneSidechains = new Map<
2042
+ string,
2043
+ { trackId: number; insertIndex: number; sourceTrackId: number }
2044
+ >();
2814
2045
  private readonly buses: EngineBus[] = [];
2815
2046
  private readonly trackStripJson = new Map<number, string>();
2816
2047
  private readonly busStripJson = new Map<number, string>();
@@ -3025,6 +2256,68 @@ export class SonareEngine {
3025
2256
  this.scheduleParam('', laneId, ppq, value, curve);
3026
2257
  }
3027
2258
 
2259
+ /**
2260
+ * Replaces the automation lane for `paramId` with the given breakpoints.
2261
+ *
2262
+ * Unlike scheduleParam (which appends a single point), this sets the whole
2263
+ * lane at once; an empty array clears the lane. The points are defensively
2264
+ * copied and sorted by ppq before being mirrored to the offline engine and
2265
+ * the live worklet engine.
2266
+ *
2267
+ * @param paramId Automation target id (registered parameter or a reserved
2268
+ * engine mixer target from automationParamId/busAutomationParamId).
2269
+ * @param points Lane breakpoints; order does not matter.
2270
+ */
2271
+ setAutomationLane(paramId: number, points: ReadonlyArray<EngineAutomationPoint>): void {
2272
+ const sorted = points.map((point) => ({ ...point })).sort((a, b) => a.ppq - b.ppq);
2273
+ if (sorted.length === 0) {
2274
+ this.automationLanes.delete(paramId);
2275
+ } else {
2276
+ this.automationLanes.set(paramId, sorted);
2277
+ }
2278
+ this.offlineEngine.setAutomationLane(paramId, sorted);
2279
+ this.postSync({ type: 'syncAutomation', paramId, points: sorted });
2280
+ }
2281
+
2282
+ /**
2283
+ * Returns the automation target id for a mixer strip parameter.
2284
+ *
2285
+ * The id addresses the engine's reserved mixer namespace, so it can be fed
2286
+ * straight to setAutomationLane to automate a fader or pan without
2287
+ * registering a parameter.
2288
+ *
2289
+ * @param target Track id (declares a mixer lane on first use) or 'master'.
2290
+ * @param kind Strip parameter to address.
2291
+ * @returns Reserved engine parameter id for the strip parameter.
2292
+ */
2293
+ automationParamId(target: string | number, kind: 'faderDb' | 'pan'): number {
2294
+ const paramKind = kind === 'pan' ? ENGINE_MIXER_PARAM_PAN : ENGINE_MIXER_PARAM_FADER_DB;
2295
+ if (target === 'master') {
2296
+ return engineMixerMasterTarget(paramKind);
2297
+ }
2298
+ return engineMixerLaneTarget(this.ensureTrackLane(target), paramKind);
2299
+ }
2300
+
2301
+ /**
2302
+ * Returns the automation target id for a bus fader.
2303
+ *
2304
+ * @param busId Bus id (declares the mixer bus on first use).
2305
+ * @returns Reserved engine parameter id for the bus fader gain (dB).
2306
+ */
2307
+ busAutomationParamId(busId: number): number {
2308
+ return engineMixerBusTarget(this.ensureBus(busId), ENGINE_MIXER_PARAM_FADER_DB);
2309
+ }
2310
+
2311
+ /**
2312
+ * Returns the number of automation lanes installed on the engine, including
2313
+ * lanes whose breakpoint list is currently empty.
2314
+ *
2315
+ * @returns Engine-side automation lane count.
2316
+ */
2317
+ automationLaneCount(): number {
2318
+ return this.offlineEngine.automationLaneCount();
2319
+ }
2320
+
3028
2321
  listParameters(): EngineParameterInfo[] {
3029
2322
  const parameters: EngineParameterInfo[] = [];
3030
2323
  for (let index = 0; index < this.offlineEngine.parameterCount(); index++) {
@@ -3125,11 +2418,64 @@ export class SonareEngine {
3125
2418
  entry.sends.map((send) => ({ ...send })),
3126
2419
  );
3127
2420
  }
2421
+ if (entry.outputBusId !== undefined) {
2422
+ if (entry.outputBusId === 0) {
2423
+ this.trackOutputBus.delete(entry.trackId);
2424
+ } else {
2425
+ this.trackOutputBus.set(entry.trackId, entry.outputBusId);
2426
+ }
2427
+ }
3128
2428
  }
3129
2429
  this.trackLaneIds.splice(0, this.trackLaneIds.length, ...ids);
3130
2430
  this.syncMixer();
3131
2431
  }
3132
2432
 
2433
+ /**
2434
+ * Routes a track lane's post-fader output into a declared bus instead of
2435
+ * the master mix (group/folder routing); busId 0 restores the master mix.
2436
+ */
2437
+ setTrackOutputBus(target: string | number, busId: number): void {
2438
+ const laneIndex = this.ensureTrackLane(target);
2439
+ const trackId = this.trackLaneIds[laneIndex];
2440
+ if (busId === 0) {
2441
+ this.trackOutputBus.delete(trackId);
2442
+ } else {
2443
+ this.trackOutputBus.set(trackId, busId);
2444
+ }
2445
+ this.syncMixer();
2446
+ }
2447
+
2448
+ /**
2449
+ * Keys one insert of a lane strip from another lane's post-strip pre-fader
2450
+ * audio (ducking/sidechainRouter inserts). sourceTarget null removes the
2451
+ * binding.
2452
+ */
2453
+ setLaneSidechain(
2454
+ target: string | number,
2455
+ insertIndex: number,
2456
+ sourceTarget: string | number | null,
2457
+ ): void {
2458
+ const laneIndex = this.ensureTrackLane(target);
2459
+ const trackId = this.trackLaneIds[laneIndex];
2460
+ const key = `${trackId}:${insertIndex}`;
2461
+ let sourceTrackId = 0;
2462
+ if (sourceTarget !== null) {
2463
+ const sourceIndex = this.ensureTrackLane(sourceTarget);
2464
+ sourceTrackId = this.trackLaneIds[sourceIndex];
2465
+ }
2466
+ if (sourceTrackId === 0) {
2467
+ this.laneSidechains.delete(key);
2468
+ } else {
2469
+ this.laneSidechains.set(key, { trackId, insertIndex, sourceTrackId });
2470
+ }
2471
+ this.offlineEngine.setLaneSidechain(trackId, insertIndex, sourceTrackId);
2472
+ this.postSync({
2473
+ type: 'syncMixer',
2474
+ lanes: this.mixerLanes(),
2475
+ laneSidechains: [{ trackId, insertIndex, sourceTrackId }],
2476
+ });
2477
+ }
2478
+
3133
2479
  setSends(target: string | number, sends: EngineTrackSend[]): void {
3134
2480
  const laneIndex = this.ensureTrackLane(target);
3135
2481
  const trackId = this.trackLaneIds[laneIndex];
@@ -3193,6 +2539,56 @@ export class SonareEngine {
3193
2539
  });
3194
2540
  }
3195
2541
 
2542
+ setTrackStripInsertParamByName(
2543
+ target: string | number,
2544
+ insertIndex: number,
2545
+ paramName: string,
2546
+ value: number,
2547
+ ): void {
2548
+ const laneIndex = this.ensureTrackLane(target);
2549
+ const trackId = this.trackLaneIds[laneIndex];
2550
+ this.offlineEngine.setTrackStripInsertParamByName(trackId, insertIndex, paramName, value);
2551
+ this.postSync({
2552
+ type: 'syncTrackStripInsertParamByName',
2553
+ trackId,
2554
+ insertIndex,
2555
+ paramName,
2556
+ value,
2557
+ });
2558
+ }
2559
+
2560
+ setTrackStripPan(target: string | number, pan: number): void {
2561
+ const trackId = this.trackLaneIds[this.ensureTrackLane(target)];
2562
+ this.offlineEngine.setTrackStripPan(trackId, pan);
2563
+ this.postSync({ type: 'syncTrackStripPan', trackId, pan });
2564
+ }
2565
+
2566
+ setTrackStripPanLaw(target: string | number, panLaw: PanLaw | number): void {
2567
+ const trackId = this.trackLaneIds[this.ensureTrackLane(target)];
2568
+ const code = panLawCode(panLaw);
2569
+ this.offlineEngine.setTrackStripPanLaw(trackId, code);
2570
+ this.postSync({ type: 'syncTrackStripPanLaw', trackId, panLaw: code });
2571
+ }
2572
+
2573
+ setTrackStripPanMode(target: string | number, panMode: PanMode | number): void {
2574
+ const trackId = this.trackLaneIds[this.ensureTrackLane(target)];
2575
+ const code = panModeCode(panMode);
2576
+ this.offlineEngine.setTrackStripPanMode(trackId, code);
2577
+ this.postSync({ type: 'syncTrackStripPanMode', trackId, panMode: code });
2578
+ }
2579
+
2580
+ setTrackStripDualPan(target: string | number, leftPan: number, rightPan: number): void {
2581
+ const trackId = this.trackLaneIds[this.ensureTrackLane(target)];
2582
+ this.offlineEngine.setTrackStripDualPan(trackId, leftPan, rightPan);
2583
+ this.postSync({ type: 'syncTrackStripDualPan', trackId, leftPan, rightPan });
2584
+ }
2585
+
2586
+ setTrackStripChannelDelaySamples(target: string | number, delaySamples: number): void {
2587
+ const trackId = this.trackLaneIds[this.ensureTrackLane(target)];
2588
+ this.offlineEngine.setTrackStripChannelDelaySamples(trackId, delaySamples);
2589
+ this.postSync({ type: 'syncTrackStripChannelDelaySamples', trackId, delaySamples });
2590
+ }
2591
+
3196
2592
  setStripEq(target: string | number, bandIndex: number, band: EqBand | string): void {
3197
2593
  if (target === 'master') {
3198
2594
  this.setMasterStripEqBand(bandIndex, band);
@@ -3255,6 +2651,29 @@ export class SonareEngine {
3255
2651
  });
3256
2652
  }
3257
2653
 
2654
+ setMasterStripInsertParamByName(insertIndex: number, paramName: string, value: number): void {
2655
+ this.offlineEngine.setMasterStripInsertParamByName(insertIndex, paramName, value);
2656
+ this.postSync({
2657
+ type: 'syncMasterStripInsertParamByName',
2658
+ insertIndex,
2659
+ paramName,
2660
+ value,
2661
+ });
2662
+ }
2663
+
2664
+ setStripInsertParamByName(
2665
+ target: string | number,
2666
+ insertIndex: number,
2667
+ paramName: string,
2668
+ value: number,
2669
+ ): void {
2670
+ if (target === 'master') {
2671
+ this.setMasterStripInsertParamByName(insertIndex, paramName, value);
2672
+ return;
2673
+ }
2674
+ this.setTrackStripInsertParamByName(target, insertIndex, paramName, value);
2675
+ }
2676
+
3258
2677
  setMasterChain(sceneJson: string): void {
3259
2678
  this.setMasterStripJson(sceneJson);
3260
2679
  }
@@ -3562,6 +2981,10 @@ export class SonareEngine {
3562
2981
  return this.realtimeNode.onMeter(callback);
3563
2982
  }
3564
2983
 
2984
+ onScope(callback: (scope: SonareWorkletScopeSnapshot) => void): () => void {
2985
+ return this.realtimeNode.onScope(callback);
2986
+ }
2987
+
3565
2988
  onTelemetry(callback: (telemetry: SonareEngineTelemetryRecord) => void): () => void {
3566
2989
  return this.realtimeNode.onTelemetry(callback);
3567
2990
  }
@@ -3574,6 +2997,10 @@ export class SonareEngine {
3574
2997
  return this.realtimeNode.pollMeters();
3575
2998
  }
3576
2999
 
3000
+ pollScope(): SonareWorkletScopeSnapshot[] {
3001
+ return this.realtimeNode.pollScope();
3002
+ }
3003
+
3577
3004
  destroy(): void {
3578
3005
  if (this.destroyed) {
3579
3006
  return;
@@ -3601,13 +3028,20 @@ export class SonareEngine {
3601
3028
  this.postSync({ type: 'syncMidiClips', clips });
3602
3029
  }
3603
3030
 
3604
- private syncMixer(): void {
3605
- const lanes = this.trackLaneIds.map((trackId) => {
3031
+ private mixerLanes(): EngineTrackLane[] {
3032
+ return this.trackLaneIds.map((trackId) => {
3606
3033
  const sends = this.trackSends.get(trackId);
3607
- return sends && sends.length > 0
3608
- ? { trackId, sends: sends.map((send) => ({ ...send })) }
3609
- : { trackId };
3034
+ const outputBusId = this.trackOutputBus.get(trackId);
3035
+ return {
3036
+ trackId,
3037
+ ...(sends && sends.length > 0 ? { sends: sends.map((send) => ({ ...send })) } : {}),
3038
+ ...(outputBusId !== undefined ? { outputBusId } : {}),
3039
+ };
3610
3040
  });
3041
+ }
3042
+
3043
+ private syncMixer(): void {
3044
+ const lanes = this.mixerLanes();
3611
3045
  const buses = this.buses.map((bus) => ({ ...bus }));
3612
3046
  this.offlineEngine.setTrackBuses(buses);
3613
3047
  if (lanes.length > 0) {
@@ -3626,6 +3060,7 @@ export class SonareEngine {
3626
3060
  lanes,
3627
3061
  buses,
3628
3062
  trackStrips,
3063
+ laneSidechains: Array.from(this.laneSidechains.values()),
3629
3064
  busStrips,
3630
3065
  masterStripJson: this.masterStripJson,
3631
3066
  });