@libraz/libsonare 1.4.1 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +51 -20
  2. package/dist/index.d.ts +5416 -1
  3. package/dist/index.js +938 -583
  4. package/dist/index.js.map +1 -1
  5. package/dist/sonare.js +2 -2
  6. package/dist/sonare.wasm +0 -0
  7. package/dist/worklet.d.ts +1083 -5227
  8. package/dist/worklet.js +2683 -2451
  9. package/dist/worklet.js.map +1 -1
  10. package/package.json +4 -9
  11. package/src/clip_page_streamer.ts +298 -0
  12. package/src/effects_mastering.ts +85 -1089
  13. package/src/effects_transform.ts +286 -0
  14. package/src/effects_voice_change.ts +118 -0
  15. package/src/feature_music.ts +5 -2
  16. package/src/feature_spectrogram.ts +42 -2
  17. package/src/features.ts +1 -0
  18. package/src/index.ts +23 -0
  19. package/src/mastering_chain.ts +200 -0
  20. package/src/mastering_core.ts +248 -0
  21. package/src/mastering_dynamics.ts +105 -0
  22. package/src/mastering_repair.ts +161 -0
  23. package/src/mixer.ts +8 -0
  24. package/src/mixing_oneshot.ts +54 -0
  25. package/src/module_state.ts +1 -2
  26. package/src/project.ts +71 -1712
  27. package/src/project_class.ts +871 -0
  28. package/src/project_internal.ts +333 -0
  29. package/src/project_synth.ts +43 -0
  30. package/src/project_types.ts +570 -0
  31. package/src/public_types.ts +6 -1221
  32. package/src/public_types_acoustic.ts +115 -0
  33. package/src/public_types_mastering.ts +333 -0
  34. package/src/public_types_mixing.ts +97 -0
  35. package/src/public_types_music.ts +352 -0
  36. package/src/public_types_realtime.ts +163 -0
  37. package/src/public_types_spectral.ts +194 -0
  38. package/src/realtime_engine.ts +94 -0
  39. package/src/sonare.js.d.ts +117 -38
  40. package/src/stream_analyzer.ts +3 -0
  41. package/src/stream_types.ts +4 -0
  42. package/src/worklet/engine-automation.ts +73 -0
  43. package/src/worklet/engine-capture-facade.ts +80 -0
  44. package/src/worklet/engine-clips.ts +71 -0
  45. package/src/worklet/engine-markers.ts +93 -0
  46. package/src/worklet/engine-mixer-facade.ts +186 -0
  47. package/src/worklet/engine-node.ts +451 -0
  48. package/src/worklet/engine-offline.ts +162 -0
  49. package/src/worklet/engine-options.ts +13 -0
  50. package/src/worklet/engine-parameter-facade.ts +172 -0
  51. package/src/worklet/engine-processor.ts +764 -0
  52. package/src/worklet/engine-register.ts +136 -0
  53. package/src/worklet/engine-strips.ts +315 -0
  54. package/src/worklet/engine-sync.ts +94 -0
  55. package/src/worklet/engine-tempo-facade.ts +141 -0
  56. package/src/worklet/engine.ts +998 -0
  57. package/src/worklet/guards.ts +14 -1
  58. package/src/worklet/messages.ts +60 -20
  59. package/src/worklet/mixer-processor.ts +368 -0
  60. package/src/worklet/protocol.ts +3 -0
  61. package/src/worklet/voice-changer-processor.ts +246 -0
  62. package/src/worklet.ts +20 -3549
  63. package/dist/sonare-rt-module.js +0 -2
  64. package/dist/sonare-rt.js +0 -2
  65. package/dist/sonare-rt.wasm +0 -0
  66. package/src/sonare-rt.d.ts +0 -93
@@ -0,0 +1,998 @@
1
+ import type {
2
+ EngineAutomationPoint,
3
+ EngineBus,
4
+ EngineCaptureStatus,
5
+ EngineClip,
6
+ EngineMarker,
7
+ EngineMetronomeConfig,
8
+ EngineMidiClipSchedule,
9
+ EngineParameterInfo,
10
+ EngineTempoSegment,
11
+ EngineTimeSignatureSegment,
12
+ EngineTrackLane,
13
+ EngineTrackSend,
14
+ EngineTransportState,
15
+ EqBand,
16
+ PanLaw,
17
+ PanMode,
18
+ } from '../index';
19
+ import { RealtimeEngine } from '../index';
20
+ import type { EngineAutomationContext } from './engine-automation';
21
+ import * as automation from './engine-automation';
22
+ import type { EngineCaptureContext } from './engine-capture-facade';
23
+ import * as capture from './engine-capture-facade';
24
+ import type { EngineClipContext } from './engine-clips';
25
+ import * as clips from './engine-clips';
26
+ import type { EngineMarkerContext } from './engine-markers';
27
+ import * as markers from './engine-markers';
28
+ import type { EngineMixerContext } from './engine-mixer-facade';
29
+ import * as mixer from './engine-mixer-facade';
30
+ import { SonareRealtimeEngineNode } from './engine-node';
31
+ import { buildTransportFacade, type CaptureOptions } from './engine-offline';
32
+ import type { SonareEngineOptions, SuspendableAudioContext } from './engine-options';
33
+ import type { EngineParameterContext } from './engine-parameter-facade';
34
+ import * as parameter from './engine-parameter-facade';
35
+ import type { EngineStripContext } from './engine-strips';
36
+ import * as strips from './engine-strips';
37
+ import { resolveParamId, resolveTargetId } from './engine-sync';
38
+ import type { EngineTempoContext } from './engine-tempo-facade';
39
+ import * as tempo from './engine-tempo-facade';
40
+ import type {
41
+ SonareEngineInstrumentSyncMessage,
42
+ SonareEngineSyncCaptureMessage,
43
+ SonareEngineSyncMessage,
44
+ SonareEngineTransportFacade,
45
+ SonareRealtimeEngineNodeCapabilities,
46
+ SonareWorkletExternalMidiEvent,
47
+ } from './messages';
48
+ import {
49
+ ENGINE_MIXER_PARAM_FADER_DB,
50
+ ENGINE_MIXER_PARAM_PAN,
51
+ engineMixerLaneTarget,
52
+ engineMixerMasterTarget,
53
+ SonareEngineCommandType,
54
+ type SonareEngineTelemetryRecord,
55
+ type SonareWorkletMeterSnapshot,
56
+ type SonareWorkletScopeSnapshot,
57
+ } from './protocol';
58
+
59
+ export class SonareEngine {
60
+ readonly node: AudioWorkletNode;
61
+ readonly capabilities: SonareRealtimeEngineNodeCapabilities;
62
+ readonly transport: SonareEngineTransportFacade;
63
+ private readonly realtimeNode: SonareRealtimeEngineNode;
64
+ private readonly offlineEngine: RealtimeEngine;
65
+ private readonly context: SuspendableAudioContext;
66
+ private readonly sampleRate: number;
67
+ private readonly offlineBlockSize: number;
68
+ private readonly offlineChannelCount: number;
69
+ private readonly automationLanes = new Map<number, EngineAutomationPoint[]>();
70
+ private readonly clips = new Map<number, EngineClip>();
71
+ private readonly midiClips = new Map<number, EngineMidiClipSchedule>();
72
+ private readonly markers = new Map<number, EngineMarker>();
73
+ private readonly trackLaneIds: number[] = [];
74
+ private readonly trackSends = new Map<number, EngineTrackSend[]>();
75
+ private readonly trackOutputBus = new Map<number, number>();
76
+ private readonly laneSidechains = new Map<
77
+ string,
78
+ { trackId: number; insertIndex: number; sourceTrackId: number }
79
+ >();
80
+ private readonly buses: EngineBus[] = [];
81
+ private readonly trackStripJson = new Map<number, string>();
82
+ private readonly busStripJson = new Map<number, string>();
83
+ private masterStripJson: string | undefined;
84
+ private captureConfig: Omit<SonareEngineSyncCaptureMessage, 'type'> | undefined;
85
+ private tempoBpm = 120;
86
+ private timeSignature = { numerator: 4, denominator: 4 };
87
+ private tempoSegments: EngineTempoSegment[] = [{ startPpq: 0, bpm: 120 }];
88
+ private timeSignatureSegments: EngineTimeSignatureSegment[] = [
89
+ { startPpq: 0, numerator: 4, denominator: 4 },
90
+ ];
91
+ private latestTransportState: EngineTransportState | undefined;
92
+ private nextClipId = 1;
93
+ private nextMarkerId = 1;
94
+ private transportPlaying = false;
95
+ private readonly pendingInstrumentSync: SonareEngineInstrumentSyncMessage[] = [];
96
+ private destroyed = false;
97
+
98
+ private constructor(
99
+ context: BaseAudioContext,
100
+ realtimeNode: SonareRealtimeEngineNode,
101
+ offlineEngine: RealtimeEngine,
102
+ sampleRate: number,
103
+ offlineBlockSize: number,
104
+ offlineChannelCount: number,
105
+ ) {
106
+ this.context = context;
107
+ this.realtimeNode = realtimeNode;
108
+ this.offlineEngine = offlineEngine;
109
+ this.node = realtimeNode.node;
110
+ this.capabilities = realtimeNode.capabilities;
111
+ this.sampleRate = sampleRate;
112
+ this.offlineBlockSize = offlineBlockSize;
113
+ this.offlineChannelCount = offlineChannelCount;
114
+ this.transport = buildTransportFacade({
115
+ sampleRate: this.sampleRate,
116
+ realtimeNode: this.realtimeNode,
117
+ offlineEngine: this.offlineEngine,
118
+ setTransportPlaying: (playing) => {
119
+ this.transportPlaying = playing;
120
+ },
121
+ flushPendingInstrumentSync: () => this.flushPendingInstrumentSync(),
122
+ setTempo: (bpm) => this.setTempo(bpm),
123
+ setTempoSegments: (segments) => this.setTempoSegments(segments),
124
+ setLoop: (startPpq, endPpq, enabled) => this.setLoop(startPpq, endPpq, enabled),
125
+ });
126
+ }
127
+
128
+ static async create(
129
+ context: BaseAudioContext,
130
+ options: SonareEngineOptions = {},
131
+ ): Promise<SonareEngine> {
132
+ const sampleRate = options.sampleRate ?? context.sampleRate;
133
+ const blockSize = options.offlineBlockSize ?? options.blockSize ?? 128;
134
+ const channelCount = Math.max(
135
+ 1,
136
+ Math.floor(options.offlineChannelCount ?? options.channelCount ?? 2),
137
+ );
138
+ const realtimeNode = await SonareRealtimeEngineNode.create(context, options);
139
+ const offlineEngine = options.offlineEngine ?? new RealtimeEngine(sampleRate, blockSize);
140
+ return new SonareEngine(
141
+ context,
142
+ realtimeNode,
143
+ offlineEngine,
144
+ sampleRate,
145
+ blockSize,
146
+ channelCount,
147
+ );
148
+ }
149
+
150
+ async suspend(): Promise<void> {
151
+ if (this.destroyed) {
152
+ return;
153
+ }
154
+ await this.context.suspend?.();
155
+ }
156
+
157
+ async resume(): Promise<void> {
158
+ if (this.destroyed) {
159
+ return;
160
+ }
161
+ await this.context.resume?.();
162
+ }
163
+
164
+ setTempo(bpm: number): void {
165
+ tempo.setTempo(this.tempoContext, bpm);
166
+ }
167
+
168
+ setTempoSegments(segments: readonly EngineTempoSegment[]): void {
169
+ tempo.setTempoSegments(this.tempoContext, segments);
170
+ }
171
+
172
+ setTimeSignature(numerator: number, denominator: number): void {
173
+ tempo.setTimeSignature(this.tempoContext, numerator, denominator);
174
+ }
175
+
176
+ setTimeSignatureSegments(segments: readonly EngineTimeSignatureSegment[]): void {
177
+ tempo.setTimeSignatureSegments(this.tempoContext, segments);
178
+ }
179
+
180
+ setLoop(startPpq: number, endPpq: number, enabled = true): boolean {
181
+ return tempo.setLoop(this.tempoContext, startPpq, endPpq, enabled);
182
+ }
183
+
184
+ countInEndSample(startSample: number, bars: number): number {
185
+ return tempo.countInEndSample(this.tempoContext, startSample, bars);
186
+ }
187
+
188
+ getTransportState(): Promise<EngineTransportState> {
189
+ return tempo.getTransportState(this.tempoContext);
190
+ }
191
+
192
+ cachedTransportState(): EngineTransportState | undefined {
193
+ return tempo.cachedTransportState(this.tempoContext);
194
+ }
195
+
196
+ setParam(nodeId: string, param: string | number, value: number): boolean {
197
+ return parameter.setParam(this.parameterContext, nodeId, param, value);
198
+ }
199
+
200
+ scheduleParam(
201
+ nodeId: string,
202
+ param: string | number,
203
+ ppq: number,
204
+ value: number,
205
+ curve: number | 'linear' | 'exponential' = 'linear',
206
+ ): void {
207
+ automation.scheduleParam(this.automationContext, nodeId, param, ppq, value, curve);
208
+ }
209
+
210
+ addAutomationPoint(
211
+ laneId: string | number,
212
+ ppq: number,
213
+ value: number,
214
+ curve: number | 'linear' | 'exponential' = 'linear',
215
+ ): void {
216
+ automation.addAutomationPoint(this.automationContext, laneId, ppq, value, curve);
217
+ }
218
+
219
+ /**
220
+ * Replaces the automation lane for `paramId` with the given breakpoints. An
221
+ * empty array clears the lane; the points are defensively copied and sorted
222
+ * by ppq before mirroring to the offline and live worklet engines.
223
+ */
224
+ setAutomationLane(paramId: number, points: ReadonlyArray<EngineAutomationPoint>): void {
225
+ automation.setAutomationLane(this.automationContext, paramId, points);
226
+ }
227
+
228
+ /**
229
+ * Returns the automation target id for a mixer strip parameter.
230
+ *
231
+ * The id addresses the engine's reserved mixer namespace, so it can be fed
232
+ * straight to setAutomationLane to automate a fader or pan without
233
+ * registering a parameter.
234
+ *
235
+ * @param target Track id (declares a mixer lane on first use) or 'master'.
236
+ * @param kind Strip parameter to address.
237
+ * @returns Reserved engine parameter id for the strip parameter.
238
+ */
239
+ automationParamId(target: string | number, kind: 'faderDb' | 'pan'): number {
240
+ return parameter.automationParamId(this.parameterContext, target, kind);
241
+ }
242
+
243
+ /**
244
+ * Returns the automation target id for a bus fader.
245
+ *
246
+ * @param busId Bus id (declares the mixer bus on first use).
247
+ * @returns Reserved engine parameter id for the bus fader gain (dB).
248
+ */
249
+ busAutomationParamId(busId: number): number {
250
+ return parameter.busAutomationParamId(this.parameterContext, busId);
251
+ }
252
+
253
+ /**
254
+ * Resolves a track-lane insert parameter (JSON-key name) to the reserved
255
+ * insert-automation id fed straight to setAutomationLane. Declares the track's
256
+ * mixer lane first (like automationParamId) so the offline engine resolves the
257
+ * same strip selector the realtime engine uses.
258
+ *
259
+ * @param target Track id (declares a mixer lane on first use).
260
+ * @param insertIndex Index into the strip's combined insert sequence.
261
+ * @param paramName Processor JSON-key parameter name.
262
+ * @returns Reserved insert-automation id, or -1 when strip/insert/key unknown.
263
+ */
264
+ resolveTrackInsertAutomationId(
265
+ target: string | number,
266
+ insertIndex: number,
267
+ paramName: string,
268
+ ): number {
269
+ return parameter.resolveTrackInsertAutomationId(
270
+ this.parameterContext,
271
+ target,
272
+ insertIndex,
273
+ paramName,
274
+ );
275
+ }
276
+
277
+ /**
278
+ * Resolves a master-strip insert parameter to its reserved insert-automation
279
+ * id.
280
+ *
281
+ * @param insertIndex Index into the master strip's insert sequence.
282
+ * @param paramName Processor JSON-key parameter name.
283
+ * @returns Reserved insert-automation id, or -1 when insert/key unknown.
284
+ */
285
+ resolveMasterInsertAutomationId(insertIndex: number, paramName: string): number {
286
+ return parameter.resolveMasterInsertAutomationId(this.parameterContext, insertIndex, paramName);
287
+ }
288
+
289
+ /**
290
+ * Resolves a bus-strip insert parameter to its reserved insert-automation id.
291
+ * Declares the mixer bus first so the offline engine resolves the same bus
292
+ * selector.
293
+ *
294
+ * @param busId Bus id (declares the mixer bus on first use).
295
+ * @param insertIndex Index into the bus strip's insert sequence.
296
+ * @param paramName Processor JSON-key parameter name.
297
+ * @returns Reserved insert-automation id, or -1 when bus/insert/key unknown.
298
+ */
299
+ resolveBusInsertAutomationId(busId: number, insertIndex: number, paramName: string): number {
300
+ return parameter.resolveBusInsertAutomationId(
301
+ this.parameterContext,
302
+ busId,
303
+ insertIndex,
304
+ paramName,
305
+ );
306
+ }
307
+
308
+ /**
309
+ * Returns the number of automation lanes installed on the engine, including
310
+ * lanes whose breakpoint list is currently empty.
311
+ *
312
+ * @returns Engine-side automation lane count.
313
+ */
314
+ automationLaneCount(): number {
315
+ return parameter.automationLaneCount(this.parameterContext);
316
+ }
317
+
318
+ listParameters(): EngineParameterInfo[] {
319
+ return parameter.listParameters(this.parameterContext);
320
+ }
321
+
322
+ setSoloMute(target: string | number, solo: boolean, mute: boolean): boolean {
323
+ return parameter.setSoloMute(this.parameterContext, target, solo, mute);
324
+ }
325
+
326
+ setStripGain(target: string | number, db: number): boolean {
327
+ return this.sendSmoothedParam(this.stripParamId(target, ENGINE_MIXER_PARAM_FADER_DB), db);
328
+ }
329
+
330
+ setStripPan(target: string | number, pan: number): boolean {
331
+ return this.sendSmoothedParam(this.stripParamId(target, ENGINE_MIXER_PARAM_PAN), pan);
332
+ }
333
+
334
+ /**
335
+ * Declares the mixer track lanes in an explicit order.
336
+ *
337
+ * Lane indices are append-only: once a track id occupies a lane, its index
338
+ * stays fixed for the engine's lifetime. The given list must therefore start
339
+ * with the already-declared lane ids in their current order and may only
340
+ * append new track ids after them. Entries carrying `sends` replace that
341
+ * track's send list; entries without `sends` leave existing sends untouched.
342
+ *
343
+ * @param lanes Track ids or lane descriptors in the desired lane order.
344
+ */
345
+ setTrackLanes(lanes: ReadonlyArray<number | EngineTrackLane>): void {
346
+ mixer.setTrackLanes(this.mixerContext, lanes);
347
+ }
348
+
349
+ /**
350
+ * Routes a track lane's post-fader output into a declared bus instead of
351
+ * the master mix (group/folder routing); busId 0 restores the master mix.
352
+ */
353
+ setTrackOutputBus(target: string | number, busId: number): void {
354
+ mixer.setTrackOutputBus(this.mixerContext, target, busId);
355
+ }
356
+
357
+ /**
358
+ * Keys one insert of a lane strip from another lane's post-strip pre-fader
359
+ * audio (ducking/sidechainRouter inserts). sourceTarget null removes the
360
+ * binding.
361
+ */
362
+ setLaneSidechain(
363
+ target: string | number,
364
+ insertIndex: number,
365
+ sourceTarget: string | number | null,
366
+ ): void {
367
+ mixer.setLaneSidechain(this.mixerContext, target, insertIndex, sourceTarget);
368
+ }
369
+
370
+ setSends(target: string | number, sends: EngineTrackSend[]): void {
371
+ mixer.setSends(this.mixerContext, target, sends);
372
+ }
373
+
374
+ setTrackBuses(buses: EngineBus[]): void {
375
+ mixer.setTrackBuses(this.mixerContext, buses);
376
+ }
377
+
378
+ setBusGain(busId: number, db: number): boolean {
379
+ return mixer.setBusGain(this.mixerContext, busId, db);
380
+ }
381
+
382
+ setTrackStripJson(target: string | number, sceneJson: string): void {
383
+ const laneIndex = this.ensureTrackLane(target);
384
+ const trackId = this.trackLaneIds[laneIndex];
385
+ strips.setTrackStripJson(this.stripContext, trackId, sceneJson, this.trackStripJson);
386
+ this.syncMixer();
387
+ }
388
+
389
+ setTrackStripEqBand(target: string | number, bandIndex: number, band: EqBand | string): void {
390
+ strips.setTrackStripEqBand(this.stripContext, target, bandIndex, band);
391
+ }
392
+
393
+ setTrackStripInsertBypassed(
394
+ target: string | number,
395
+ insertIndex: number,
396
+ bypassed: boolean,
397
+ resetOnBypass = false,
398
+ ): void {
399
+ strips.setTrackStripInsertBypassed(
400
+ this.stripContext,
401
+ target,
402
+ insertIndex,
403
+ bypassed,
404
+ resetOnBypass,
405
+ );
406
+ }
407
+
408
+ setTrackStripInsertParamByName(
409
+ target: string | number,
410
+ insertIndex: number,
411
+ paramName: string,
412
+ value: number,
413
+ ): void {
414
+ strips.setTrackStripInsertParamByName(this.stripContext, target, insertIndex, paramName, value);
415
+ }
416
+
417
+ setTrackStripPan(target: string | number, pan: number): void {
418
+ strips.setTrackStripPan(this.stripContext, target, pan);
419
+ }
420
+
421
+ setTrackStripPanLaw(target: string | number, panLaw: PanLaw | number): void {
422
+ strips.setTrackStripPanLaw(this.stripContext, target, panLaw);
423
+ }
424
+
425
+ setTrackStripPanMode(target: string | number, panMode: PanMode | number): void {
426
+ strips.setTrackStripPanMode(this.stripContext, target, panMode);
427
+ }
428
+
429
+ setTrackStripDualPan(target: string | number, leftPan: number, rightPan: number): void {
430
+ strips.setTrackStripDualPan(this.stripContext, target, leftPan, rightPan);
431
+ }
432
+
433
+ setTrackStripChannelDelaySamples(target: string | number, delaySamples: number): void {
434
+ strips.setTrackStripChannelDelaySamples(this.stripContext, target, delaySamples);
435
+ }
436
+
437
+ setStripEq(target: string | number, bandIndex: number, band: EqBand | string): void {
438
+ if (target === 'master') {
439
+ this.setMasterStripEqBand(bandIndex, band);
440
+ return;
441
+ }
442
+ this.setTrackStripEqBand(target, bandIndex, band);
443
+ }
444
+
445
+ setStripInsertBypassed(
446
+ target: string | number,
447
+ insertIndex: number,
448
+ bypassed: boolean,
449
+ resetOnBypass = false,
450
+ ): void {
451
+ if (target === 'master') {
452
+ this.setMasterStripInsertBypassed(insertIndex, bypassed, resetOnBypass);
453
+ return;
454
+ }
455
+ this.setTrackStripInsertBypassed(target, insertIndex, bypassed, resetOnBypass);
456
+ }
457
+
458
+ setStripInserts(target: string | number, sceneJson: string): void {
459
+ if (target === 'master') {
460
+ this.setMasterStripJson(sceneJson);
461
+ return;
462
+ }
463
+ this.setTrackStripJson(target, sceneJson);
464
+ }
465
+
466
+ setBusStripJson(busId: number, sceneJson: string): void {
467
+ mixer.setBusStripJson(this.mixerContext, busId, sceneJson);
468
+ }
469
+
470
+ setMasterStripJson(sceneJson: string): void {
471
+ this.offlineEngine.setMasterStripJson(sceneJson);
472
+ this.masterStripJson = sceneJson;
473
+ this.syncMixer();
474
+ }
475
+
476
+ setMasterStripEqBand(bandIndex: number, band: EqBand | string): void {
477
+ strips.setMasterStripEqBand(this.stripContext, bandIndex, band);
478
+ }
479
+
480
+ setMasterStripInsertBypassed(
481
+ insertIndex: number,
482
+ bypassed: boolean,
483
+ resetOnBypass = false,
484
+ ): void {
485
+ strips.setMasterStripInsertBypassed(this.stripContext, insertIndex, bypassed, resetOnBypass);
486
+ }
487
+
488
+ setMasterStripInsertParamByName(insertIndex: number, paramName: string, value: number): void {
489
+ strips.setMasterStripInsertParamByName(this.stripContext, insertIndex, paramName, value);
490
+ }
491
+
492
+ setBusStripInsertParamByName(
493
+ busId: number,
494
+ insertIndex: number,
495
+ paramName: string,
496
+ value: number,
497
+ ): void {
498
+ this.ensureBus(busId);
499
+ strips.setBusStripInsertParamByName(this.stripContext, busId, insertIndex, paramName, value);
500
+ }
501
+
502
+ setStripInsertParamByName(
503
+ target: string | number,
504
+ insertIndex: number,
505
+ paramName: string,
506
+ value: number,
507
+ ): void {
508
+ if (target === 'master') {
509
+ this.setMasterStripInsertParamByName(insertIndex, paramName, value);
510
+ return;
511
+ }
512
+ this.setTrackStripInsertParamByName(target, insertIndex, paramName, value);
513
+ }
514
+
515
+ setMasterChain(sceneJson: string): void {
516
+ this.setMasterStripJson(sceneJson);
517
+ }
518
+
519
+ addClip(
520
+ trackId: string | number,
521
+ buffer: Float32Array[],
522
+ startPpq: number,
523
+ opts: Partial<Omit<EngineClip, 'channels' | 'startPpq'>> = {},
524
+ ): number {
525
+ return clips.addClip(this.clipContext, trackId, buffer, startPpq, opts);
526
+ }
527
+
528
+ removeClip(clipId: number): void {
529
+ clips.removeClip(this.clipContext, clipId);
530
+ }
531
+
532
+ setMidiClips(schedules: readonly EngineMidiClipSchedule[]): void {
533
+ clips.setMidiClips(this.clipContext, schedules);
534
+ }
535
+
536
+ setBuiltinInstrument(
537
+ trackId: string | number,
538
+ config: { destinationId?: number } & Record<string, unknown> = {},
539
+ ): void {
540
+ strips.setBuiltinInstrument(this.stripContext, trackId, config);
541
+ }
542
+
543
+ setSynthInstrument(trackId: string | number, patch: Record<string, unknown> | string = {}): void {
544
+ strips.setSynthInstrument(this.stripContext, trackId, patch);
545
+ }
546
+
547
+ loadSoundFont(data: Uint8Array): void {
548
+ strips.loadSoundFont(this.stripContext, data);
549
+ }
550
+
551
+ setSf2Instrument(
552
+ trackId: string | number,
553
+ config: { destinationId?: number; gain?: number; polyphony?: number } = {},
554
+ ): void {
555
+ strips.setSf2Instrument(this.stripContext, trackId, config);
556
+ }
557
+
558
+ /**
559
+ * Route a track's MIDI to the external output (drained via {@link onMidiOut})
560
+ * instead of an internal instrument, so the track plays an external device.
561
+ * Pass `external=false` to restore internal-synth playback.
562
+ */
563
+ setMidiDestinationExternal(trackId: string | number, external: boolean): void {
564
+ strips.setMidiDestinationExternal(this.stripContext, trackId, external);
565
+ }
566
+
567
+ /**
568
+ * Enable/disable forwarding MIDI clock + transport (start/continue/stop) to
569
+ * the external output so external gear tracks the transport tempo. The bytes
570
+ * arrive through {@link onMidiOut} tagged with the transport destination.
571
+ */
572
+ setExternalMidiClockEnabled(enabled: boolean): void {
573
+ strips.setExternalMidiClockEnabled(this.stripContext, enabled);
574
+ }
575
+
576
+ /**
577
+ * Install or replace a live, non-destructive MIDI-FX insert for one
578
+ * destination. The insert transforms the destination's MIDI before
579
+ * synthesis (transpose, quantize, velocity shaping, humanize, harmonize,
580
+ * arpeggiate) without rewriting any stored notes, so it can be bypassed by
581
+ * {@link clearMidiFx}. The config JSON is the flat object the engine's
582
+ * MIDI-FX accepts (the same schema as the offline `Project.bakeMidiFx`).
583
+ */
584
+ setMidiFx(trackId: string | number, configJson: string): void {
585
+ strips.setMidiFx(this.stripContext, trackId, configJson);
586
+ }
587
+
588
+ /** Remove the live MIDI-FX insert from one destination (a no-op when none). */
589
+ clearMidiFx(trackId: string | number): void {
590
+ strips.clearMidiFx(this.stripContext, trackId);
591
+ }
592
+
593
+ pushMidiNoteOn(
594
+ trackId: string | number,
595
+ group: number,
596
+ channel: number,
597
+ note: number,
598
+ velocity: number,
599
+ renderFrame = -1,
600
+ ): void {
601
+ strips.pushMidiNoteOn(this.stripContext, trackId, group, channel, note, velocity, renderFrame);
602
+ }
603
+
604
+ pushMidiNoteOff(
605
+ trackId: string | number,
606
+ group: number,
607
+ channel: number,
608
+ note: number,
609
+ velocity = 0,
610
+ renderFrame = -1,
611
+ ): void {
612
+ strips.pushMidiNoteOff(this.stripContext, trackId, group, channel, note, velocity, renderFrame);
613
+ }
614
+
615
+ pushMidiCc(
616
+ trackId: string | number,
617
+ group: number,
618
+ channel: number,
619
+ controller: number,
620
+ value: number,
621
+ renderFrame = -1,
622
+ ): void {
623
+ strips.pushMidiCc(this.stripContext, trackId, group, channel, controller, value, renderFrame);
624
+ }
625
+
626
+ pushMidiSysex(trackId: string | number, data: Uint8Array, renderFrame = -1): void {
627
+ strips.pushMidiSysex(this.stripContext, trackId, data, renderFrame);
628
+ }
629
+
630
+ pushMidiPanic(renderFrame = -1): void {
631
+ this.offlineEngine.pushMidiPanic(renderFrame);
632
+ this.postSync({ type: 'syncMidiPanic', renderFrame });
633
+ }
634
+
635
+ configureCapture(options: CaptureOptions): void {
636
+ capture.configureCapture(this.captureContext, options);
637
+ }
638
+
639
+ armRecord(trackId: string | number, enabled: boolean): boolean {
640
+ return capture.armRecord(this.captureContext, trackId, enabled);
641
+ }
642
+
643
+ punch(inPpq: number, outPpq: number): boolean {
644
+ return capture.punch(this.captureContext, inPpq, outPpq);
645
+ }
646
+
647
+ captureStatus(): Promise<EngineCaptureStatus> {
648
+ return capture.captureStatus(this.captureContext);
649
+ }
650
+
651
+ capturedAudio(): Promise<Float32Array[]> {
652
+ return capture.capturedAudio(this.captureContext);
653
+ }
654
+
655
+ async resetCapture(): Promise<void> {
656
+ return capture.resetCapture(this.captureContext);
657
+ }
658
+
659
+ setMetronome(opts: EngineMetronomeConfig): void {
660
+ this.offlineEngine.setMetronome(opts);
661
+ // The full config (beatGain/accentGain/clickSamples/clickSeconds) cannot fit
662
+ // the fixed-size SAB command record, so it is delivered out-of-band; the
663
+ // SetMetronome command then toggles enabled state on the audio thread.
664
+ this.postSync({ type: 'syncMetronome', config: opts });
665
+ this.realtimeNode.sendCommand({
666
+ type: SonareEngineCommandType.SetMetronome,
667
+ sampleTime: -1,
668
+ argInt: opts.enabled ? 1 : 0,
669
+ });
670
+ }
671
+
672
+ addMarker(ppq: number, name = ''): number {
673
+ return markers.addMarker(this.markerContext, ppq, name);
674
+ }
675
+
676
+ /**
677
+ * Replaces the whole marker set in one call. Entries without an `id` are
678
+ * assigned fresh ids; entries carrying an `id` keep it. Returns the resolved
679
+ * markers in the order given.
680
+ */
681
+ setMarkers(entries: ReadonlyArray<{ ppq: number; name?: string; id?: number }>): EngineMarker[] {
682
+ return markers.setMarkers(this.markerContext, entries);
683
+ }
684
+
685
+ markerCount(): number {
686
+ return markers.markerCount(this.markerContext);
687
+ }
688
+
689
+ markerByIndex(index: number): EngineMarker {
690
+ return markers.markerByIndex(this.markerContext, index);
691
+ }
692
+
693
+ marker(markerId: number): EngineMarker {
694
+ return markers.marker(this.markerContext, markerId);
695
+ }
696
+
697
+ seekMarker(markerId: number): boolean {
698
+ return markers.seekMarker(this.markerContext, markerId);
699
+ }
700
+
701
+ setLoopFromMarkers(startMarkerId: number, endMarkerId: number): boolean {
702
+ return markers.setLoopFromMarkers(this.markerContext, startMarkerId, endMarkerId);
703
+ }
704
+
705
+ async renderOffline(totalFrames: number): Promise<Float32Array[]> {
706
+ const frames = Math.max(0, Math.floor(totalFrames));
707
+ const inputs: Float32Array[] = [];
708
+ for (let ch = 0; ch < this.offlineChannelCount; ch++) {
709
+ inputs.push(new Float32Array(frames));
710
+ }
711
+ return this.offlineEngine.renderOffline(inputs, this.offlineBlockSize);
712
+ }
713
+
714
+ /**
715
+ * Subscribe to external-MIDI batches (already lowered to MIDI 1.0 bytes) for
716
+ * delivery to Web MIDI output ports. Fires once per render block that
717
+ * produced events. Returns an unsubscribe function.
718
+ */
719
+ onMidiOut(callback: (events: SonareWorkletExternalMidiEvent[]) => void): () => void {
720
+ return this.realtimeNode.onMidiOut(callback);
721
+ }
722
+
723
+ onMeter(callback: (meter: SonareWorkletMeterSnapshot) => void): () => void {
724
+ return this.realtimeNode.onMeter(callback);
725
+ }
726
+
727
+ onScope(callback: (scope: SonareWorkletScopeSnapshot) => void): () => void {
728
+ return this.realtimeNode.onScope(callback);
729
+ }
730
+
731
+ onTelemetry(callback: (telemetry: SonareEngineTelemetryRecord) => void): () => void {
732
+ return this.realtimeNode.onTelemetry(callback);
733
+ }
734
+
735
+ pollTelemetry(): SonareEngineTelemetryRecord[] {
736
+ return this.realtimeNode.pollTelemetry();
737
+ }
738
+
739
+ pollMeters(): SonareWorkletMeterSnapshot[] {
740
+ return this.realtimeNode.pollMeters();
741
+ }
742
+
743
+ pollScope(): SonareWorkletScopeSnapshot[] {
744
+ return this.realtimeNode.pollScope();
745
+ }
746
+
747
+ destroy(): void {
748
+ if (this.destroyed) {
749
+ return;
750
+ }
751
+ this.destroyed = true;
752
+ this.transport.stop();
753
+ this.realtimeNode.pollTelemetry();
754
+ this.realtimeNode.destroy();
755
+ this.offlineEngine.destroy();
756
+ }
757
+
758
+ private mixerLanes(): EngineTrackLane[] {
759
+ return mixer.mixerLanes(this.mixerContext);
760
+ }
761
+
762
+ private syncMixer(): void {
763
+ mixer.syncMixer(this.mixerContext);
764
+ }
765
+
766
+ private postInstrumentSync(message: SonareEngineInstrumentSyncMessage): void {
767
+ if (this.destroyed) {
768
+ return;
769
+ }
770
+ if (this.transportPlaying) {
771
+ this.pendingInstrumentSync.push(message);
772
+ return;
773
+ }
774
+ this.postSync(message);
775
+ }
776
+
777
+ private flushPendingInstrumentSync(): void {
778
+ if (this.destroyed || this.pendingInstrumentSync.length === 0) {
779
+ return;
780
+ }
781
+ const pending = this.pendingInstrumentSync.splice(0);
782
+ for (const message of pending) {
783
+ this.postSync(message);
784
+ }
785
+ }
786
+
787
+ // Posts an out-of-band control-sync message to the worklet engine processor.
788
+ // Sync messages use a string `type` so the worklet's message handler routes
789
+ // them to receiveSync() (numeric `type` is reserved for SonareEngineCommandRecord).
790
+ private postSync(message: SonareEngineSyncMessage): void {
791
+ if (this.destroyed) {
792
+ return;
793
+ }
794
+ this.realtimeNode.node.port.postMessage(message);
795
+ }
796
+
797
+ // Collaborator surface handed to the mixer/routing free functions so they can
798
+ // mutate the routing stores (held by reference), mirror into the offline
799
+ // engine, post mixer-sync messages, and declare lanes/buses without a
800
+ // back-reference to the whole engine.
801
+ private get mixerContext(): EngineMixerContext {
802
+ return {
803
+ offlineEngine: this.offlineEngine,
804
+ trackLaneIds: this.trackLaneIds,
805
+ trackSends: this.trackSends,
806
+ trackOutputBus: this.trackOutputBus,
807
+ laneSidechains: this.laneSidechains,
808
+ buses: this.buses,
809
+ trackStripJson: this.trackStripJson,
810
+ busStripJson: this.busStripJson,
811
+ postSync: (message) => this.postSync(message),
812
+ ensureTrackLane: (target) => this.ensureTrackLane(target),
813
+ ensureBus: (busId) => this.ensureBus(busId),
814
+ mixerLanes: () => this.mixerLanes(),
815
+ syncMixer: () => this.syncMixer(),
816
+ sendSmoothedParam: (paramId, value) => this.sendSmoothedParam(paramId, value),
817
+ getMasterStripJson: () => this.masterStripJson,
818
+ };
819
+ }
820
+
821
+ // Collaborator surface handed to the strip/pan/EQ/insert/MIDI free functions
822
+ // so they can mirror into the offline engine, post sync messages, and resolve
823
+ // lanes without each holding a back-reference to the whole engine.
824
+ private get stripContext(): EngineStripContext {
825
+ return {
826
+ offlineEngine: this.offlineEngine,
827
+ trackLaneIds: this.trackLaneIds,
828
+ postSync: (message) => this.postSync(message),
829
+ postInstrumentSync: (message) => this.postInstrumentSync(message),
830
+ ensureTrackLane: (target) => this.ensureTrackLane(target),
831
+ resolveTargetId: (target) => this.resolveTargetId(target),
832
+ };
833
+ }
834
+
835
+ // Collaborator surface handed to the automation-lane free functions so they
836
+ // can mutate the lane store, mirror into the offline engine, and post
837
+ // automation-sync messages without holding a back-reference.
838
+ private get automationContext(): EngineAutomationContext {
839
+ return {
840
+ offlineEngine: this.offlineEngine,
841
+ automationLanes: this.automationLanes,
842
+ postSync: (message) => this.postSync(message),
843
+ resolveParamId: (nodeId, param) => this.resolveParamId(nodeId, param),
844
+ };
845
+ }
846
+
847
+ // Collaborator surface handed to the capture/record/punch free functions so
848
+ // they can mirror into and query the offline engine, command the realtime
849
+ // node, and read/write the capture config without a back-reference.
850
+ private get captureContext(): EngineCaptureContext {
851
+ return {
852
+ offlineEngine: this.offlineEngine,
853
+ realtimeNode: this.realtimeNode,
854
+ offlineChannelCount: this.offlineChannelCount,
855
+ postSync: (message) => this.postSync(message),
856
+ getCaptureConfig: () => this.captureConfig,
857
+ setCaptureConfig: (config) => {
858
+ this.captureConfig = config;
859
+ },
860
+ resolveTargetId: (target) => this.resolveTargetId(target),
861
+ };
862
+ }
863
+
864
+ // Collaborator surface handed to the parameter / automation-id resolution
865
+ // free functions so they can mirror into and query the offline engine,
866
+ // command the realtime node, and declare lanes/buses without holding a
867
+ // back-reference to the whole engine.
868
+ private get parameterContext(): EngineParameterContext {
869
+ return {
870
+ offlineEngine: this.offlineEngine,
871
+ realtimeNode: this.realtimeNode,
872
+ trackLaneIds: this.trackLaneIds,
873
+ resolveParamId: (nodeId, param) => this.resolveParamId(nodeId, param),
874
+ ensureTrackLane: (target) => this.ensureTrackLane(target),
875
+ ensureBus: (busId) => this.ensureBus(busId),
876
+ };
877
+ }
878
+
879
+ // Collaborator surface handed to the tempo / time-signature free functions so
880
+ // they can mirror into the offline engine, command the realtime node, post
881
+ // tempo-sync messages, and mutate the engine's tempo-map state by reference.
882
+ private get tempoContext(): EngineTempoContext {
883
+ return {
884
+ offlineEngine: this.offlineEngine,
885
+ realtimeNode: this.realtimeNode,
886
+ postSync: (message) => this.postSync(message),
887
+ getTempoBpm: () => this.tempoBpm,
888
+ setTempoBpm: (bpm) => {
889
+ this.tempoBpm = bpm;
890
+ },
891
+ getTimeSignature: () => this.timeSignature,
892
+ setTimeSignature: (signature) => {
893
+ this.timeSignature = signature;
894
+ },
895
+ getTempoSegments: () => this.tempoSegments,
896
+ setTempoSegments: (segments) => {
897
+ this.tempoSegments = segments;
898
+ },
899
+ getTimeSignatureSegments: () => this.timeSignatureSegments,
900
+ setTimeSignatureSegments: (segments) => {
901
+ this.timeSignatureSegments = segments;
902
+ },
903
+ setLatestTransportState: (state) => {
904
+ this.latestTransportState = state;
905
+ },
906
+ getLatestTransportState: () => this.latestTransportState,
907
+ };
908
+ }
909
+
910
+ // Collaborator surface handed to the audio/MIDI clip scheduling free
911
+ // functions so they can mutate the clip stores, mirror into the offline
912
+ // engine, and post clip-sync messages without holding a back-reference.
913
+ private get clipContext(): EngineClipContext {
914
+ return {
915
+ offlineEngine: this.offlineEngine,
916
+ clips: this.clips,
917
+ midiClips: this.midiClips,
918
+ allocateClipId: () => this.nextClipId++,
919
+ postSync: (message) => this.postSync(message),
920
+ ensureTrackLane: (target) => this.ensureTrackLane(target),
921
+ resolveTargetId: (target) => this.resolveTargetId(target),
922
+ };
923
+ }
924
+
925
+ // Collaborator surface handed to the marker free functions so they can mutate
926
+ // the marker store and id counter, mirror into the offline engine, post
927
+ // marker-sync messages, and drive transport without a back-reference.
928
+ private get markerContext(): EngineMarkerContext {
929
+ return {
930
+ offlineEngine: this.offlineEngine,
931
+ markers: this.markers,
932
+ getNextMarkerId: () => this.nextMarkerId,
933
+ setNextMarkerId: (value) => {
934
+ this.nextMarkerId = value;
935
+ },
936
+ postSync: (message) => this.postSync(message),
937
+ sendCommand: (command) => this.realtimeNode.sendCommand(command),
938
+ setLoop: (startPpq, endPpq, enabled) => this.setLoop(startPpq, endPpq, enabled),
939
+ };
940
+ }
941
+
942
+ // Resolves the reserved mixer parameter id for a fader/pan target, declaring a
943
+ // track lane on first use; 'master' addresses the master strip namespace.
944
+ private stripParamId(target: string | number, paramKind: number): number {
945
+ if (target === 'master') {
946
+ return engineMixerMasterTarget(paramKind);
947
+ }
948
+ return engineMixerLaneTarget(this.ensureTrackLane(target), paramKind);
949
+ }
950
+
951
+ // Mirrors a smoothed parameter into the offline engine and pushes a
952
+ // sample-accurate smoothed-param command to the realtime runtime.
953
+ private sendSmoothedParam(paramId: number, value: number): boolean {
954
+ this.offlineEngine.setParameter(paramId, value);
955
+ return this.realtimeNode.sendCommand({
956
+ type: SonareEngineCommandType.SetParamSmoothed,
957
+ targetId: paramId,
958
+ sampleTime: -1,
959
+ argFloat: value,
960
+ });
961
+ }
962
+
963
+ private resolveParamId(nodeId: string, param: string | number): number {
964
+ return resolveParamId(this.listParameters(), nodeId, param);
965
+ }
966
+
967
+ private resolveTargetId(target: string | number): number {
968
+ return resolveTargetId(target);
969
+ }
970
+
971
+ private ensureTrackLane(target: string | number): number {
972
+ const trackId = this.resolveTargetId(target);
973
+ if (!Number.isInteger(trackId) || trackId <= 0) {
974
+ throw new Error(`Invalid track id for mixer lane: ${String(target)}`);
975
+ }
976
+ const existing = this.trackLaneIds.indexOf(trackId);
977
+ if (existing >= 0) {
978
+ return existing;
979
+ }
980
+ this.trackLaneIds.push(trackId);
981
+ this.syncMixer();
982
+ return this.trackLaneIds.length - 1;
983
+ }
984
+
985
+ private ensureBus(busId: number): number {
986
+ const resolved = Math.trunc(busId);
987
+ if (!Number.isInteger(resolved) || resolved <= 0) {
988
+ throw new Error(`Invalid bus id for mixer bus: ${String(busId)}`);
989
+ }
990
+ const existing = this.buses.findIndex((bus) => bus.busId === resolved);
991
+ if (existing >= 0) {
992
+ return existing;
993
+ }
994
+ this.buses.push({ busId: resolved });
995
+ this.syncMixer();
996
+ return this.buses.length - 1;
997
+ }
998
+ }