@libraz/libsonare 1.3.1 → 1.3.3
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/README.md +277 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +333 -55
- package/dist/index.js.map +1 -1
- package/dist/sonare-rt-module.js +1 -1
- package/dist/sonare-rt.js +1 -1
- package/dist/sonare-rt.wasm +0 -0
- package/dist/sonare.js +1 -1
- package/dist/sonare.wasm +0 -0
- package/dist/worklet.d.ts +397 -9
- package/dist/worklet.js +1259 -87
- package/dist/worklet.js.map +1 -1
- package/package.json +1 -1
- package/src/effects_mastering.ts +16 -0
- package/src/errors.ts +44 -0
- package/src/index.ts +15 -1
- package/src/mixer.ts +11 -0
- package/src/module_state.ts +180 -4
- package/src/opfs_clip_pages.ts +43 -9
- package/src/realtime_engine.ts +174 -109
- package/src/sonare.js.d.ts +65 -0
- package/src/web_midi.ts +15 -11
- package/src/worklet.ts +1402 -66
package/src/worklet.ts
CHANGED
|
@@ -1,16 +1,33 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
EngineAutomationPoint,
|
|
3
|
+
EngineBus,
|
|
4
|
+
EngineCaptureStatus,
|
|
3
5
|
EngineClip,
|
|
4
6
|
EngineMarker,
|
|
5
7
|
EngineMeterTelemetry,
|
|
6
8
|
EngineMetronomeConfig,
|
|
9
|
+
EngineMidiClipSchedule,
|
|
7
10
|
EngineParameterInfo,
|
|
8
11
|
EngineTelemetry,
|
|
12
|
+
EngineTempoSegment,
|
|
13
|
+
EngineTimeSignatureSegment,
|
|
14
|
+
EngineTrackLane,
|
|
15
|
+
EngineTrackSend,
|
|
16
|
+
EngineTransportState,
|
|
17
|
+
EqBand,
|
|
9
18
|
MixerRealtimeBuffer,
|
|
10
19
|
RealtimeVoiceChangerConfigInput,
|
|
11
20
|
} from './index';
|
|
12
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
engineCapabilities,
|
|
23
|
+
init as initSonareModule,
|
|
24
|
+
isInitialized,
|
|
25
|
+
Mixer,
|
|
26
|
+
RealtimeEngine,
|
|
27
|
+
RealtimeVoiceChanger,
|
|
28
|
+
} from './index';
|
|
13
29
|
import type { AutomationCurve } from './public_types';
|
|
30
|
+
import type { SonareModule } from './sonare.js';
|
|
14
31
|
import type { SonareRtModule } from './sonare-rt';
|
|
15
32
|
|
|
16
33
|
// With code-splitting disabled, the worklet bundle carries its own copy of the
|
|
@@ -38,6 +55,9 @@ export interface SonareRealtimeEngineWorkletProcessorOptions {
|
|
|
38
55
|
runtimeTarget?: 'embind' | 'sonare-rt';
|
|
39
56
|
rtModuleUrl?: string;
|
|
40
57
|
rtWasmBinary?: ArrayBuffer | Uint8Array;
|
|
58
|
+
wasmBinary?: ArrayBuffer | Uint8Array;
|
|
59
|
+
initialSyncMessages?: SonareEngineSyncMessage[];
|
|
60
|
+
initialCommands?: SonareEngineCommandRecord[];
|
|
41
61
|
sampleRate?: number;
|
|
42
62
|
blockSize?: number;
|
|
43
63
|
channelCount?: number;
|
|
@@ -85,6 +105,7 @@ export interface SonareRealtimeEngineNodeCapabilities {
|
|
|
85
105
|
expectedEngineAbiVersion?: number;
|
|
86
106
|
abiCompatible?: boolean;
|
|
87
107
|
degradedReason?: string;
|
|
108
|
+
readyMessage?: boolean;
|
|
88
109
|
}
|
|
89
110
|
|
|
90
111
|
export interface SonareRealtimeEngineNodeOptions
|
|
@@ -127,6 +148,7 @@ export interface SonareEngineTransportFacade {
|
|
|
127
148
|
seekPpq(ppq: number, sampleTime?: number): boolean;
|
|
128
149
|
seekSeconds(seconds: number, sampleTime?: number): boolean;
|
|
129
150
|
setTempo(bpm: number): void;
|
|
151
|
+
setTempoSegments(segments: readonly EngineTempoSegment[]): void;
|
|
130
152
|
setLoop(startPpq: number, endPpq: number, enabled?: boolean): boolean;
|
|
131
153
|
}
|
|
132
154
|
|
|
@@ -135,6 +157,22 @@ type SuspendableAudioContext = BaseAudioContext & {
|
|
|
135
157
|
resume?: () => Promise<void>;
|
|
136
158
|
};
|
|
137
159
|
|
|
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
|
+
|
|
138
176
|
type WorkletInput = readonly (readonly Float32Array[])[];
|
|
139
177
|
type WorkletOutput = Float32Array[][];
|
|
140
178
|
|
|
@@ -164,12 +202,19 @@ export type SonareWorkletMessage =
|
|
|
164
202
|
|
|
165
203
|
export interface SonareWorkletMeterSnapshot {
|
|
166
204
|
type: 'meter';
|
|
205
|
+
targetId: number;
|
|
167
206
|
frame: number;
|
|
168
207
|
peakDbL: number;
|
|
169
208
|
peakDbR: number;
|
|
170
209
|
rmsDbL: number;
|
|
171
210
|
rmsDbR: number;
|
|
172
211
|
correlation: number;
|
|
212
|
+
truePeakDbL: number;
|
|
213
|
+
truePeakDbR: number;
|
|
214
|
+
momentaryLufs: number;
|
|
215
|
+
shortTermLufs: number;
|
|
216
|
+
integratedLufs: number;
|
|
217
|
+
gainReductionDb: number;
|
|
173
218
|
}
|
|
174
219
|
|
|
175
220
|
export interface SonareWorkletSpectrumSnapshot {
|
|
@@ -184,12 +229,14 @@ export type SonareWorkletTransportMessage =
|
|
|
184
229
|
| SonareEngineTelemetryRecord;
|
|
185
230
|
|
|
186
231
|
export const SONARE_METER_RING_HEADER_INTS = 4;
|
|
187
|
-
// Record layout: [frameLo, peakDbL, peakDbR, rmsDbL, rmsDbR,
|
|
232
|
+
// Record layout: [frameLo, frameHi, targetId, peakDbL, peakDbR, rmsDbL, rmsDbR,
|
|
233
|
+
// correlation, truePeakDbL, truePeakDbR, momentaryLufs, shortTermLufs,
|
|
234
|
+
// integratedLufs, gainReductionDb].
|
|
188
235
|
// The sample-frame index is monotonically increasing and quickly exceeds the
|
|
189
236
|
// 2^24 exact-integer range of a single Float32 slot (~349 s at 48 kHz), so it is
|
|
190
237
|
// stored split across two Float32 lanes (low 24 bits + high bits) for exact
|
|
191
238
|
// reconstruction. See encodeFrameLo/encodeFrameHi/decodeFrame.
|
|
192
|
-
export const SONARE_METER_RING_RECORD_FLOATS =
|
|
239
|
+
export const SONARE_METER_RING_RECORD_FLOATS = 14;
|
|
193
240
|
export const SONARE_SPECTRUM_RING_HEADER_INTS = 5;
|
|
194
241
|
|
|
195
242
|
/** Base for splitting a frame index into two exactly-representable Float32 lanes. */
|
|
@@ -259,7 +306,13 @@ export enum SonareEngineTelemetryError {
|
|
|
259
306
|
}
|
|
260
307
|
|
|
261
308
|
interface WorkletTransport {
|
|
262
|
-
postMessage?: (
|
|
309
|
+
postMessage?: (
|
|
310
|
+
message:
|
|
311
|
+
| SonareWorkletTransportMessage
|
|
312
|
+
| SonareEngineCaptureResponseMessage
|
|
313
|
+
| SonareEngineTransportResponseMessage,
|
|
314
|
+
transfer?: Transferable[],
|
|
315
|
+
) => void;
|
|
263
316
|
onMeter?: (meter: SonareWorkletMeterSnapshot) => void;
|
|
264
317
|
onSpectrum?: (spectrum: SonareWorkletSpectrumSnapshot) => void;
|
|
265
318
|
}
|
|
@@ -331,6 +384,17 @@ export interface SonareEngineSyncClipsMessage {
|
|
|
331
384
|
clips: EngineClip[];
|
|
332
385
|
}
|
|
333
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
|
+
|
|
334
398
|
export interface SonareEngineSyncMarkersMessage {
|
|
335
399
|
type: 'syncMarkers';
|
|
336
400
|
markers: EngineMarker[];
|
|
@@ -347,11 +411,135 @@ export interface SonareEngineSyncAutomationMessage {
|
|
|
347
411
|
points: EngineAutomationPoint[];
|
|
348
412
|
}
|
|
349
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
|
+
|
|
350
522
|
export type SonareEngineSyncMessage =
|
|
351
523
|
| SonareEngineSyncClipsMessage
|
|
524
|
+
| SonareEngineSyncClipsDeltaMessage
|
|
525
|
+
| SonareEngineSyncMidiClipsMessage
|
|
352
526
|
| SonareEngineSyncMarkersMessage
|
|
353
527
|
| SonareEngineSyncMetronomeMessage
|
|
354
|
-
| SonareEngineSyncAutomationMessage
|
|
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;
|
|
355
543
|
|
|
356
544
|
export interface SonareEngineTelemetryRecord {
|
|
357
545
|
type: SonareEngineTelemetryType | number;
|
|
@@ -397,12 +585,41 @@ interface SharedSpectrumRingWriter {
|
|
|
397
585
|
}
|
|
398
586
|
|
|
399
587
|
interface WorkletPort {
|
|
400
|
-
postMessage?: (message: unknown) => void;
|
|
588
|
+
postMessage?: (message: unknown, transfer?: Transferable[]) => void;
|
|
401
589
|
onmessage?: (event: { data: unknown }) => void;
|
|
402
590
|
addEventListener?: (type: 'message', listener: (event: { data: unknown }) => void) => void;
|
|
403
591
|
start?: () => void;
|
|
404
592
|
}
|
|
405
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
|
+
|
|
406
623
|
function toDb(value: number): number {
|
|
407
624
|
return value > 0 ? 20 * Math.log10(value) : Number.NEGATIVE_INFINITY;
|
|
408
625
|
}
|
|
@@ -432,9 +649,68 @@ function isEngineSyncMessage(value: unknown): value is SonareEngineSyncMessage {
|
|
|
432
649
|
}
|
|
433
650
|
return (
|
|
434
651
|
value.type === 'syncClips' ||
|
|
652
|
+
value.type === 'syncClipsDelta' ||
|
|
653
|
+
value.type === 'syncMidiClips' ||
|
|
435
654
|
value.type === 'syncMarkers' ||
|
|
436
655
|
value.type === 'syncMetronome' ||
|
|
437
|
-
value.type === 'syncAutomation'
|
|
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'
|
|
438
714
|
);
|
|
439
715
|
}
|
|
440
716
|
|
|
@@ -467,7 +743,8 @@ function isMeterSnapshot(value: unknown): value is SonareWorkletMeterSnapshot {
|
|
|
467
743
|
typeof value.peakDbR === 'number' &&
|
|
468
744
|
typeof value.rmsDbL === 'number' &&
|
|
469
745
|
typeof value.rmsDbR === 'number' &&
|
|
470
|
-
typeof value.correlation === 'number'
|
|
746
|
+
typeof value.correlation === 'number' &&
|
|
747
|
+
(typeof value.targetId === 'number' || value.targetId === undefined)
|
|
471
748
|
);
|
|
472
749
|
}
|
|
473
750
|
|
|
@@ -495,19 +772,27 @@ export function readSonareMeterRingBuffer(
|
|
|
495
772
|
readIndex = 0,
|
|
496
773
|
): SonareMeterRingReadResult {
|
|
497
774
|
const writeIndex = Atomics.load(ring.header, 0);
|
|
775
|
+
const recordFloats = Atomics.load(ring.header, 2) || SONARE_METER_RING_RECORD_FLOATS;
|
|
498
776
|
const nextReadIndex = Math.max(0, Math.min(readIndex, writeIndex));
|
|
499
777
|
const firstReadable = Math.max(nextReadIndex, writeIndex - ring.capacity);
|
|
500
778
|
const meters: SonareWorkletMeterSnapshot[] = [];
|
|
501
779
|
for (let index = firstReadable; index < writeIndex; index++) {
|
|
502
|
-
const offset = (index % ring.capacity) *
|
|
780
|
+
const offset = (index % ring.capacity) * recordFloats;
|
|
503
781
|
meters.push({
|
|
504
782
|
type: 'meter',
|
|
505
|
-
frame: decodeFrame(ring.records[offset], ring.records[offset +
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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],
|
|
511
796
|
});
|
|
512
797
|
}
|
|
513
798
|
return { nextReadIndex: writeIndex, meters };
|
|
@@ -841,12 +1126,19 @@ function telemetryFromEngine(telemetry: EngineTelemetry): SonareEngineTelemetryR
|
|
|
841
1126
|
function meterFromEngine(meter: EngineMeterTelemetry): SonareWorkletMeterSnapshot {
|
|
842
1127
|
return {
|
|
843
1128
|
type: 'meter',
|
|
1129
|
+
targetId: meter.targetId,
|
|
844
1130
|
frame: meter.renderFrame,
|
|
845
1131
|
peakDbL: meter.peakDbL,
|
|
846
1132
|
peakDbR: meter.peakDbR,
|
|
847
1133
|
rmsDbL: meter.rmsDbL,
|
|
848
1134
|
rmsDbR: meter.rmsDbR,
|
|
849
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,
|
|
850
1142
|
};
|
|
851
1143
|
}
|
|
852
1144
|
|
|
@@ -1041,12 +1333,19 @@ export class SonareWorkletProcessor {
|
|
|
1041
1333
|
const denominator = Math.sqrt(sumL * sumR);
|
|
1042
1334
|
const meter: SonareWorkletMeterSnapshot = {
|
|
1043
1335
|
type: 'meter',
|
|
1336
|
+
targetId: 0,
|
|
1044
1337
|
frame: this.processedFrames,
|
|
1045
1338
|
peakDbL: toDb(peakL),
|
|
1046
1339
|
peakDbR: toDb(peakR),
|
|
1047
1340
|
rmsDbL: toDb(rmsL),
|
|
1048
1341
|
rmsDbR: toDb(rmsR),
|
|
1049
1342
|
correlation: denominator > 0 ? sumLR / denominator : 0,
|
|
1343
|
+
truePeakDbL: toDb(peakL),
|
|
1344
|
+
truePeakDbR: toDb(peakR),
|
|
1345
|
+
momentaryLufs: Number.NaN,
|
|
1346
|
+
shortTermLufs: Number.NaN,
|
|
1347
|
+
integratedLufs: Number.NaN,
|
|
1348
|
+
gainReductionDb: Number.NaN,
|
|
1050
1349
|
};
|
|
1051
1350
|
this.transport.onMeter?.(meter);
|
|
1052
1351
|
if (this.meterRing) {
|
|
@@ -1064,12 +1363,19 @@ export class SonareWorkletProcessor {
|
|
|
1064
1363
|
const writeIndex = Atomics.load(ring.header, 0);
|
|
1065
1364
|
const offset = (writeIndex % ring.capacity) * SONARE_METER_RING_RECORD_FLOATS;
|
|
1066
1365
|
ring.records[offset] = encodeFrameLo(meter.frame);
|
|
1067
|
-
ring.records[offset + 1] = meter.
|
|
1068
|
-
ring.records[offset + 2] = meter.
|
|
1069
|
-
ring.records[offset + 3] = meter.
|
|
1070
|
-
ring.records[offset + 4] = meter.
|
|
1071
|
-
ring.records[offset + 5] = meter.
|
|
1072
|
-
ring.records[offset + 6] =
|
|
1366
|
+
ring.records[offset + 1] = encodeFrameHi(meter.frame);
|
|
1367
|
+
ring.records[offset + 2] = meter.targetId;
|
|
1368
|
+
ring.records[offset + 3] = meter.peakDbL;
|
|
1369
|
+
ring.records[offset + 4] = meter.peakDbR;
|
|
1370
|
+
ring.records[offset + 5] = meter.rmsDbL;
|
|
1371
|
+
ring.records[offset + 6] = meter.rmsDbR;
|
|
1372
|
+
ring.records[offset + 7] = meter.correlation;
|
|
1373
|
+
ring.records[offset + 8] = meter.truePeakDbL;
|
|
1374
|
+
ring.records[offset + 9] = meter.truePeakDbR;
|
|
1375
|
+
ring.records[offset + 10] = meter.momentaryLufs;
|
|
1376
|
+
ring.records[offset + 11] = meter.shortTermLufs;
|
|
1377
|
+
ring.records[offset + 12] = meter.integratedLufs;
|
|
1378
|
+
ring.records[offset + 13] = meter.gainReductionDb;
|
|
1073
1379
|
Atomics.store(ring.header, 0, writeIndex + 1);
|
|
1074
1380
|
// writeIndex is a free-running monotonic counter, so an overflow guard here
|
|
1075
1381
|
// would fire on essentially every write past the first `capacity` records
|
|
@@ -1179,6 +1485,7 @@ export class SonareRealtimeEngineWorkletProcessor {
|
|
|
1179
1485
|
// allocated per render quantum (the old engine.process() round-tripped fresh
|
|
1180
1486
|
// arrays on both heaps every block, an RT-safety hazard).
|
|
1181
1487
|
private channelBuffers: Float32Array[];
|
|
1488
|
+
private readonly liveClips = new Map<number, EngineClip>();
|
|
1182
1489
|
|
|
1183
1490
|
constructor(
|
|
1184
1491
|
options: SonareRealtimeEngineWorkletProcessorOptions = {},
|
|
@@ -1318,8 +1625,28 @@ export class SonareRealtimeEngineWorkletProcessor {
|
|
|
1318
1625
|
}
|
|
1319
1626
|
switch (message.type) {
|
|
1320
1627
|
case 'syncClips':
|
|
1628
|
+
this.liveClips.clear();
|
|
1629
|
+
for (const clip of message.clips) {
|
|
1630
|
+
if (clip.id !== undefined) {
|
|
1631
|
+
this.liveClips.set(clip.id, clip);
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1321
1634
|
this.engine.setClips(message.clips);
|
|
1322
1635
|
break;
|
|
1636
|
+
case 'syncClipsDelta':
|
|
1637
|
+
for (const clipId of message.removeIds) {
|
|
1638
|
+
this.liveClips.delete(clipId);
|
|
1639
|
+
}
|
|
1640
|
+
for (const clip of message.upserts) {
|
|
1641
|
+
if (clip.id !== undefined) {
|
|
1642
|
+
this.liveClips.set(clip.id, clip);
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
this.engine.setClips(Array.from(this.liveClips.values()));
|
|
1646
|
+
break;
|
|
1647
|
+
case 'syncMidiClips':
|
|
1648
|
+
this.engine.setMidiClips(message.clips);
|
|
1649
|
+
break;
|
|
1323
1650
|
case 'syncMarkers':
|
|
1324
1651
|
this.engine.setMarkers(message.markers);
|
|
1325
1652
|
break;
|
|
@@ -1330,6 +1657,186 @@ export class SonareRealtimeEngineWorkletProcessor {
|
|
|
1330
1657
|
case 'syncAutomation':
|
|
1331
1658
|
this.engine.setAutomationLane(message.paramId, message.points);
|
|
1332
1659
|
break;
|
|
1660
|
+
case 'syncTempo':
|
|
1661
|
+
if (message.tempoSegments) {
|
|
1662
|
+
this.engine.setTempoSegments(message.tempoSegments);
|
|
1663
|
+
} else {
|
|
1664
|
+
this.engine.setTempo(message.bpm);
|
|
1665
|
+
}
|
|
1666
|
+
if (message.timeSignatureSegments) {
|
|
1667
|
+
this.engine.setTimeSignatureSegments(message.timeSignatureSegments);
|
|
1668
|
+
} else {
|
|
1669
|
+
this.engine.setTimeSignature(
|
|
1670
|
+
message.timeSignature.numerator,
|
|
1671
|
+
message.timeSignature.denominator,
|
|
1672
|
+
);
|
|
1673
|
+
}
|
|
1674
|
+
break;
|
|
1675
|
+
case 'syncMixer':
|
|
1676
|
+
if (message.buses) {
|
|
1677
|
+
this.engine.setTrackBuses(message.buses);
|
|
1678
|
+
}
|
|
1679
|
+
this.engine.setTrackLanes(message.lanes);
|
|
1680
|
+
for (const strip of message.trackStrips ?? []) {
|
|
1681
|
+
this.engine.setTrackStripJson(strip.trackId, strip.sceneJson);
|
|
1682
|
+
}
|
|
1683
|
+
for (const strip of message.busStrips ?? []) {
|
|
1684
|
+
this.engine.setBusStripJson(strip.busId, strip.sceneJson);
|
|
1685
|
+
}
|
|
1686
|
+
if (message.masterStripJson) {
|
|
1687
|
+
this.engine.setMasterStripJson(message.masterStripJson);
|
|
1688
|
+
}
|
|
1689
|
+
break;
|
|
1690
|
+
case 'syncCapture':
|
|
1691
|
+
this.engine.setCaptureBuffer(message.channels, message.bufferFrames);
|
|
1692
|
+
this.engine.setCaptureSource(message.source);
|
|
1693
|
+
this.engine.setRecordOffsetSamples(message.recordOffsetSamples);
|
|
1694
|
+
this.engine.setInputMonitor(message.inputMonitor.enabled, message.inputMonitor.gain);
|
|
1695
|
+
break;
|
|
1696
|
+
case 'syncTrackStripEqBand':
|
|
1697
|
+
this.engine.setTrackStripEqBandJson(message.trackId, message.bandIndex, message.bandJson);
|
|
1698
|
+
break;
|
|
1699
|
+
case 'syncMasterStripEqBand':
|
|
1700
|
+
this.engine.setMasterStripEqBandJson(message.bandIndex, message.bandJson);
|
|
1701
|
+
break;
|
|
1702
|
+
case 'syncTrackStripInsertBypassed':
|
|
1703
|
+
this.engine.setTrackStripInsertBypassed(
|
|
1704
|
+
message.trackId,
|
|
1705
|
+
message.insertIndex,
|
|
1706
|
+
message.bypassed,
|
|
1707
|
+
message.resetOnBypass,
|
|
1708
|
+
);
|
|
1709
|
+
break;
|
|
1710
|
+
case 'syncMasterStripInsertBypassed':
|
|
1711
|
+
this.engine.setMasterStripInsertBypassed(
|
|
1712
|
+
message.insertIndex,
|
|
1713
|
+
message.bypassed,
|
|
1714
|
+
message.resetOnBypass,
|
|
1715
|
+
);
|
|
1716
|
+
break;
|
|
1717
|
+
case 'syncBuiltinInstrument':
|
|
1718
|
+
this.engine.setBuiltinInstrument(message.config, message.destinationId);
|
|
1719
|
+
break;
|
|
1720
|
+
case 'syncSynthInstrument':
|
|
1721
|
+
this.engine.setSynthInstrument(message.patch, message.destinationId);
|
|
1722
|
+
break;
|
|
1723
|
+
case 'syncLoadSoundFont':
|
|
1724
|
+
this.engine.loadSoundFont(message.data);
|
|
1725
|
+
break;
|
|
1726
|
+
case 'syncSf2Instrument':
|
|
1727
|
+
this.engine.setSf2Instrument(message.config, message.destinationId);
|
|
1728
|
+
break;
|
|
1729
|
+
case 'syncMidiNoteOn':
|
|
1730
|
+
this.engine.pushMidiNoteOn(
|
|
1731
|
+
message.destinationId,
|
|
1732
|
+
message.group,
|
|
1733
|
+
message.channel,
|
|
1734
|
+
message.note,
|
|
1735
|
+
message.velocity,
|
|
1736
|
+
message.renderFrame,
|
|
1737
|
+
);
|
|
1738
|
+
break;
|
|
1739
|
+
case 'syncMidiNoteOff':
|
|
1740
|
+
this.engine.pushMidiNoteOff(
|
|
1741
|
+
message.destinationId,
|
|
1742
|
+
message.group,
|
|
1743
|
+
message.channel,
|
|
1744
|
+
message.note,
|
|
1745
|
+
message.velocity,
|
|
1746
|
+
message.renderFrame,
|
|
1747
|
+
);
|
|
1748
|
+
break;
|
|
1749
|
+
case 'syncMidiCc':
|
|
1750
|
+
this.engine.pushMidiCc(
|
|
1751
|
+
message.destinationId,
|
|
1752
|
+
message.group,
|
|
1753
|
+
message.channel,
|
|
1754
|
+
message.controller,
|
|
1755
|
+
message.value,
|
|
1756
|
+
message.renderFrame,
|
|
1757
|
+
);
|
|
1758
|
+
break;
|
|
1759
|
+
case 'syncMidiPanic':
|
|
1760
|
+
this.engine.pushMidiPanic(message.renderFrame);
|
|
1761
|
+
break;
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
receiveCaptureRequest(message: SonareEngineCaptureRequestMessage): void {
|
|
1766
|
+
if (this.closed) {
|
|
1767
|
+
return;
|
|
1768
|
+
}
|
|
1769
|
+
try {
|
|
1770
|
+
if (message.op === 'status') {
|
|
1771
|
+
const status = this.engine.captureStatus();
|
|
1772
|
+
this.transport?.postMessage?.({
|
|
1773
|
+
type: 'captureResponse',
|
|
1774
|
+
requestId: message.requestId,
|
|
1775
|
+
ok: true,
|
|
1776
|
+
status: {
|
|
1777
|
+
capturedFrames: status.capturedFrames,
|
|
1778
|
+
overflowCount: status.overflowCount,
|
|
1779
|
+
armed: status.armed,
|
|
1780
|
+
punchEnabled: status.punchEnabled,
|
|
1781
|
+
source: status.source,
|
|
1782
|
+
recordOffsetSamples: status.recordOffsetSamples,
|
|
1783
|
+
},
|
|
1784
|
+
} satisfies SonareEngineCaptureResponseMessage);
|
|
1785
|
+
return;
|
|
1786
|
+
}
|
|
1787
|
+
if (message.op === 'read') {
|
|
1788
|
+
const captured = this.engine.capturedAudio();
|
|
1789
|
+
const channels: number[][] = [];
|
|
1790
|
+
for (let ch = 0; ch < captured.length; ch++) {
|
|
1791
|
+
const source = captured[ch];
|
|
1792
|
+
const copy: number[] = [];
|
|
1793
|
+
for (let i = 0; i < source.length; i++) {
|
|
1794
|
+
copy.push(Number(source[i]));
|
|
1795
|
+
}
|
|
1796
|
+
channels.push(copy);
|
|
1797
|
+
}
|
|
1798
|
+
this.transport?.postMessage?.({
|
|
1799
|
+
type: 'captureResponse',
|
|
1800
|
+
requestId: message.requestId,
|
|
1801
|
+
ok: true,
|
|
1802
|
+
channels,
|
|
1803
|
+
} satisfies SonareEngineCaptureResponseMessage);
|
|
1804
|
+
return;
|
|
1805
|
+
}
|
|
1806
|
+
this.engine.resetCapture();
|
|
1807
|
+
this.transport?.postMessage?.({
|
|
1808
|
+
type: 'captureResponse',
|
|
1809
|
+
requestId: message.requestId,
|
|
1810
|
+
ok: true,
|
|
1811
|
+
} satisfies SonareEngineCaptureResponseMessage);
|
|
1812
|
+
} catch (error) {
|
|
1813
|
+
this.transport?.postMessage?.({
|
|
1814
|
+
type: 'captureResponse',
|
|
1815
|
+
requestId: message.requestId,
|
|
1816
|
+
ok: false,
|
|
1817
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1818
|
+
} satisfies SonareEngineCaptureResponseMessage);
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
receiveTransportRequest(message: SonareEngineTransportRequestMessage): void {
|
|
1823
|
+
if (this.closed) {
|
|
1824
|
+
return;
|
|
1825
|
+
}
|
|
1826
|
+
try {
|
|
1827
|
+
this.transport?.postMessage?.({
|
|
1828
|
+
type: 'transportResponse',
|
|
1829
|
+
requestId: message.requestId,
|
|
1830
|
+
ok: true,
|
|
1831
|
+
state: this.engine.getTransportState(),
|
|
1832
|
+
} satisfies SonareEngineTransportResponseMessage);
|
|
1833
|
+
} catch (error) {
|
|
1834
|
+
this.transport?.postMessage?.({
|
|
1835
|
+
type: 'transportResponse',
|
|
1836
|
+
requestId: message.requestId,
|
|
1837
|
+
ok: false,
|
|
1838
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1839
|
+
} satisfies SonareEngineTransportResponseMessage);
|
|
1333
1840
|
}
|
|
1334
1841
|
}
|
|
1335
1842
|
|
|
@@ -1423,6 +1930,14 @@ export class SonareRealtimeEngineWorkletProcessor {
|
|
|
1423
1930
|
// (RtPublisher-style swap), so a queued kSeekMarker resolves correctly.
|
|
1424
1931
|
this.engine.seekMarker(Math.trunc(Number(command.targetId ?? 0)), sampleTime);
|
|
1425
1932
|
break;
|
|
1933
|
+
case SonareEngineCommandType.SetSoloMute:
|
|
1934
|
+
this.engine.setSoloMute(
|
|
1935
|
+
Math.trunc(Number(command.targetId ?? 0)),
|
|
1936
|
+
Boolean((Number(command.argInt ?? 0) & 0x2) !== 0),
|
|
1937
|
+
Boolean((Number(command.argInt ?? 0) & 0x1) !== 0),
|
|
1938
|
+
sampleTime,
|
|
1939
|
+
);
|
|
1940
|
+
break;
|
|
1426
1941
|
default:
|
|
1427
1942
|
this.publishTelemetryRecord({
|
|
1428
1943
|
type: SonareEngineTelemetryType.Error,
|
|
@@ -1457,10 +1972,15 @@ export class SonareRealtimeEngineWorkletProcessor {
|
|
|
1457
1972
|
}
|
|
1458
1973
|
for (const item of this.engine.drainMeterTelemetry(64)) {
|
|
1459
1974
|
const meter = meterFromEngine(item);
|
|
1460
|
-
if (
|
|
1975
|
+
if (
|
|
1976
|
+
meter.frame !== this.lastMeterFrame &&
|
|
1977
|
+
meter.frame - this.lastMeterFrame < this.meterIntervalFrames
|
|
1978
|
+
) {
|
|
1461
1979
|
continue;
|
|
1462
1980
|
}
|
|
1463
|
-
this.lastMeterFrame
|
|
1981
|
+
if (meter.frame !== this.lastMeterFrame) {
|
|
1982
|
+
this.lastMeterFrame = meter.frame;
|
|
1983
|
+
}
|
|
1464
1984
|
// Prefer the lock-free SAB meter ring (matching the telemetry path and
|
|
1465
1985
|
// SonareWorkletProcessor); only fall back to structured-clone postMessage
|
|
1466
1986
|
// when no ring was provided, so we do not allocate/post from the audio
|
|
@@ -1482,12 +2002,19 @@ export class SonareRealtimeEngineWorkletProcessor {
|
|
|
1482
2002
|
const writeIndex = Atomics.load(ring.header, 0);
|
|
1483
2003
|
const offset = (writeIndex % ring.capacity) * SONARE_METER_RING_RECORD_FLOATS;
|
|
1484
2004
|
ring.records[offset] = encodeFrameLo(meter.frame);
|
|
1485
|
-
ring.records[offset + 1] = meter.
|
|
1486
|
-
ring.records[offset + 2] = meter.
|
|
1487
|
-
ring.records[offset + 3] = meter.
|
|
1488
|
-
ring.records[offset + 4] = meter.
|
|
1489
|
-
ring.records[offset + 5] = meter.
|
|
1490
|
-
ring.records[offset + 6] =
|
|
2005
|
+
ring.records[offset + 1] = encodeFrameHi(meter.frame);
|
|
2006
|
+
ring.records[offset + 2] = meter.targetId;
|
|
2007
|
+
ring.records[offset + 3] = meter.peakDbL;
|
|
2008
|
+
ring.records[offset + 4] = meter.peakDbR;
|
|
2009
|
+
ring.records[offset + 5] = meter.rmsDbL;
|
|
2010
|
+
ring.records[offset + 6] = meter.rmsDbR;
|
|
2011
|
+
ring.records[offset + 7] = meter.correlation;
|
|
2012
|
+
ring.records[offset + 8] = meter.truePeakDbL;
|
|
2013
|
+
ring.records[offset + 9] = meter.truePeakDbR;
|
|
2014
|
+
ring.records[offset + 10] = meter.momentaryLufs;
|
|
2015
|
+
ring.records[offset + 11] = meter.shortTermLufs;
|
|
2016
|
+
ring.records[offset + 12] = meter.integratedLufs;
|
|
2017
|
+
ring.records[offset + 13] = meter.gainReductionDb;
|
|
1491
2018
|
Atomics.store(ring.header, 0, writeIndex + 1);
|
|
1492
2019
|
// writeIndex is a free-running monotonic counter, so an overflow guard here
|
|
1493
2020
|
// would fire on essentially every write past the first `capacity` records
|
|
@@ -1657,12 +2184,32 @@ export class SonareRtRealtimeEngineRuntime {
|
|
|
1657
2184
|
this.metronomeConfig.clickSamples,
|
|
1658
2185
|
);
|
|
1659
2186
|
break;
|
|
2187
|
+
case 'syncTempo':
|
|
2188
|
+
this.module._sonare_rt_engine_set_tempo(this.engine, message.bpm);
|
|
2189
|
+
break;
|
|
1660
2190
|
case 'syncClips':
|
|
2191
|
+
case 'syncClipsDelta':
|
|
2192
|
+
case 'syncMidiClips':
|
|
1661
2193
|
case 'syncMarkers':
|
|
1662
2194
|
case 'syncAutomation':
|
|
2195
|
+
case 'syncMixer':
|
|
2196
|
+
case 'syncCapture':
|
|
2197
|
+
case 'syncTrackStripEqBand':
|
|
2198
|
+
case 'syncMasterStripEqBand':
|
|
2199
|
+
case 'syncTrackStripInsertBypassed':
|
|
2200
|
+
case 'syncMasterStripInsertBypassed':
|
|
2201
|
+
case 'syncBuiltinInstrument':
|
|
2202
|
+
case 'syncSynthInstrument':
|
|
2203
|
+
case 'syncSf2Instrument':
|
|
2204
|
+
case 'syncLoadSoundFont':
|
|
2205
|
+
case 'syncMidiNoteOn':
|
|
2206
|
+
case 'syncMidiNoteOff':
|
|
2207
|
+
case 'syncMidiCc':
|
|
2208
|
+
case 'syncMidiPanic':
|
|
1663
2209
|
// The sonare-rt C ABI exposes no set_clips / set_markers /
|
|
1664
|
-
// set_automation_lane, so these mutations cannot
|
|
1665
|
-
// engine. Surface a clear telemetry error rather
|
|
2210
|
+
// set_automation_lane / set_track_lanes, so these mutations cannot
|
|
2211
|
+
// reach a live sonare-rt engine. Surface a clear telemetry error rather
|
|
2212
|
+
// than silently dropping.
|
|
1666
2213
|
if (this.telemetryRing) {
|
|
1667
2214
|
writeSonareEngineTelemetryRingBuffer(this.telemetryRing, {
|
|
1668
2215
|
type: SonareEngineTelemetryType.Error,
|
|
@@ -1678,6 +2225,30 @@ export class SonareRtRealtimeEngineRuntime {
|
|
|
1678
2225
|
}
|
|
1679
2226
|
}
|
|
1680
2227
|
|
|
2228
|
+
receiveCaptureRequest(message: SonareEngineCaptureRequestMessage, port?: WorkletPort): void {
|
|
2229
|
+
if (this.closed) {
|
|
2230
|
+
return;
|
|
2231
|
+
}
|
|
2232
|
+
port?.postMessage?.({
|
|
2233
|
+
type: 'captureResponse',
|
|
2234
|
+
requestId: message.requestId,
|
|
2235
|
+
ok: false,
|
|
2236
|
+
error: 'Capture read-back is not supported by the sonare-rt runtime.',
|
|
2237
|
+
} satisfies SonareEngineCaptureResponseMessage);
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
receiveTransportRequest(message: SonareEngineTransportRequestMessage, port?: WorkletPort): void {
|
|
2241
|
+
if (this.closed) {
|
|
2242
|
+
return;
|
|
2243
|
+
}
|
|
2244
|
+
port?.postMessage?.({
|
|
2245
|
+
type: 'transportResponse',
|
|
2246
|
+
requestId: message.requestId,
|
|
2247
|
+
ok: false,
|
|
2248
|
+
error: 'Transport state read-back is not supported by the sonare-rt runtime.',
|
|
2249
|
+
} satisfies SonareEngineTransportResponseMessage);
|
|
2250
|
+
}
|
|
2251
|
+
|
|
1681
2252
|
destroy(): void {
|
|
1682
2253
|
if (this.closed) {
|
|
1683
2254
|
return;
|
|
@@ -1880,6 +2451,22 @@ export class SonareRealtimeEngineNode {
|
|
|
1880
2451
|
private meterReadIndex = 0;
|
|
1881
2452
|
private telemetryListeners = new Set<(telemetry: SonareEngineTelemetryRecord) => void>();
|
|
1882
2453
|
private meterListeners = new Set<(meter: SonareWorkletMeterSnapshot) => void>();
|
|
2454
|
+
private captureRequestId = 1;
|
|
2455
|
+
private readonly captureRequests = new Map<
|
|
2456
|
+
number,
|
|
2457
|
+
{
|
|
2458
|
+
resolve: (response: SonareEngineCaptureResponseMessage) => void;
|
|
2459
|
+
reject: (reason?: unknown) => void;
|
|
2460
|
+
}
|
|
2461
|
+
>();
|
|
2462
|
+
private transportRequestId = 1;
|
|
2463
|
+
private readonly transportRequests = new Map<
|
|
2464
|
+
number,
|
|
2465
|
+
{
|
|
2466
|
+
resolve: (response: SonareEngineTransportResponseMessage) => void;
|
|
2467
|
+
reject: (reason?: unknown) => void;
|
|
2468
|
+
}
|
|
2469
|
+
>();
|
|
1883
2470
|
private resolveReady!: () => void;
|
|
1884
2471
|
private rejectReady!: (reason?: unknown) => void;
|
|
1885
2472
|
private destroyed = false;
|
|
@@ -1900,11 +2487,31 @@ export class SonareRealtimeEngineNode {
|
|
|
1900
2487
|
this.resolveReady = resolve;
|
|
1901
2488
|
this.rejectReady = reject;
|
|
1902
2489
|
});
|
|
1903
|
-
if (capabilities.
|
|
2490
|
+
if (!capabilities.readyMessage) {
|
|
1904
2491
|
this.resolveReady();
|
|
1905
2492
|
}
|
|
1906
2493
|
this.node.port.onmessage = (event: MessageEvent<unknown>) => {
|
|
1907
|
-
if (
|
|
2494
|
+
if (isEngineCaptureResponseMessage(event.data)) {
|
|
2495
|
+
const pending = this.captureRequests.get(event.data.requestId);
|
|
2496
|
+
if (pending) {
|
|
2497
|
+
this.captureRequests.delete(event.data.requestId);
|
|
2498
|
+
if (event.data.ok) {
|
|
2499
|
+
pending.resolve(event.data);
|
|
2500
|
+
} else {
|
|
2501
|
+
pending.reject(new Error(event.data.error ?? 'Capture request failed'));
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
} else if (isEngineTransportResponseMessage(event.data)) {
|
|
2505
|
+
const pending = this.transportRequests.get(event.data.requestId);
|
|
2506
|
+
if (pending) {
|
|
2507
|
+
this.transportRequests.delete(event.data.requestId);
|
|
2508
|
+
if (event.data.ok) {
|
|
2509
|
+
pending.resolve(event.data);
|
|
2510
|
+
} else {
|
|
2511
|
+
pending.reject(new Error(event.data.error ?? 'Transport request failed'));
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
} else if (isEngineTelemetryRecord(event.data)) {
|
|
1908
2515
|
this.emitTelemetry(event.data);
|
|
1909
2516
|
} else if (isMeterSnapshot(event.data)) {
|
|
1910
2517
|
this.emitMeter(event.data);
|
|
@@ -1988,6 +2595,9 @@ export class SonareRealtimeEngineNode {
|
|
|
1988
2595
|
telemetryRingCapacity: telemetryRing?.capacity,
|
|
1989
2596
|
meterSharedBuffer: meterRing?.sharedBuffer,
|
|
1990
2597
|
meterRingCapacity: meterRing?.capacity,
|
|
2598
|
+
wasmBinary: options.wasmBinary,
|
|
2599
|
+
initialSyncMessages: options.initialSyncMessages,
|
|
2600
|
+
initialCommands: options.initialCommands,
|
|
1991
2601
|
};
|
|
1992
2602
|
const factory =
|
|
1993
2603
|
options.nodeFactory ??
|
|
@@ -2011,6 +2621,9 @@ export class SonareRealtimeEngineNode {
|
|
|
2011
2621
|
expectedEngineAbiVersion: detectedCapabilities?.expectedEngineAbiVersion,
|
|
2012
2622
|
abiCompatible: detectedCapabilities?.abiCompatible,
|
|
2013
2623
|
degradedReason,
|
|
2624
|
+
readyMessage:
|
|
2625
|
+
runtimeTarget === 'sonare-rt' ||
|
|
2626
|
+
(runtimeTarget === 'embind' && moduleUrl !== undefined && !options.nodeFactory),
|
|
2014
2627
|
},
|
|
2015
2628
|
commandRing,
|
|
2016
2629
|
telemetryRing,
|
|
@@ -2053,6 +2666,36 @@ export class SonareRealtimeEngineNode {
|
|
|
2053
2666
|
return true;
|
|
2054
2667
|
}
|
|
2055
2668
|
|
|
2669
|
+
requestCaptureStatus(): Promise<EngineCaptureStatus> {
|
|
2670
|
+
return this.sendCaptureRequest('status').then((response) => {
|
|
2671
|
+
if (!response.status) {
|
|
2672
|
+
throw new Error('Capture status response is missing status.');
|
|
2673
|
+
}
|
|
2674
|
+
return response.status;
|
|
2675
|
+
});
|
|
2676
|
+
}
|
|
2677
|
+
|
|
2678
|
+
requestCapturedAudio(): Promise<Float32Array[]> {
|
|
2679
|
+
return this.sendCaptureRequest('read').then((response) =>
|
|
2680
|
+
(response.channels ?? []).map((channel) =>
|
|
2681
|
+
channel instanceof Float32Array ? channel : new Float32Array(channel),
|
|
2682
|
+
),
|
|
2683
|
+
);
|
|
2684
|
+
}
|
|
2685
|
+
|
|
2686
|
+
requestCaptureReset(): Promise<void> {
|
|
2687
|
+
return this.sendCaptureRequest('reset').then(() => undefined);
|
|
2688
|
+
}
|
|
2689
|
+
|
|
2690
|
+
requestTransportState(): Promise<EngineTransportState> {
|
|
2691
|
+
return this.sendTransportRequest().then((response) => {
|
|
2692
|
+
if (!response.state) {
|
|
2693
|
+
throw new Error('Transport state response is missing state.');
|
|
2694
|
+
}
|
|
2695
|
+
return response.state;
|
|
2696
|
+
});
|
|
2697
|
+
}
|
|
2698
|
+
|
|
2056
2699
|
pollTelemetry(): SonareEngineTelemetryRecord[] {
|
|
2057
2700
|
if (!this.telemetryRing) {
|
|
2058
2701
|
return [];
|
|
@@ -2101,6 +2744,14 @@ export class SonareRealtimeEngineNode {
|
|
|
2101
2744
|
this.destroyed = true;
|
|
2102
2745
|
this.node.port.postMessage({ type: SonareEngineCommandType.TransportStop, sampleTime: -1 });
|
|
2103
2746
|
this.node.disconnect();
|
|
2747
|
+
for (const pending of this.captureRequests.values()) {
|
|
2748
|
+
pending.reject(new Error('Realtime engine node is destroyed.'));
|
|
2749
|
+
}
|
|
2750
|
+
this.captureRequests.clear();
|
|
2751
|
+
for (const pending of this.transportRequests.values()) {
|
|
2752
|
+
pending.reject(new Error('Realtime engine node is destroyed.'));
|
|
2753
|
+
}
|
|
2754
|
+
this.transportRequests.clear();
|
|
2104
2755
|
this.telemetryListeners.clear();
|
|
2105
2756
|
this.meterListeners.clear();
|
|
2106
2757
|
}
|
|
@@ -2116,6 +2767,32 @@ export class SonareRealtimeEngineNode {
|
|
|
2116
2767
|
listener(meter);
|
|
2117
2768
|
}
|
|
2118
2769
|
}
|
|
2770
|
+
|
|
2771
|
+
private sendCaptureRequest(
|
|
2772
|
+
op: SonareEngineCaptureRequestMessage['op'],
|
|
2773
|
+
): Promise<SonareEngineCaptureResponseMessage> {
|
|
2774
|
+
if (this.destroyed) {
|
|
2775
|
+
return Promise.reject(new Error('Realtime engine node is destroyed.'));
|
|
2776
|
+
}
|
|
2777
|
+
const requestId = this.captureRequestId++;
|
|
2778
|
+
const promise = new Promise<SonareEngineCaptureResponseMessage>((resolve, reject) => {
|
|
2779
|
+
this.captureRequests.set(requestId, { resolve, reject });
|
|
2780
|
+
});
|
|
2781
|
+
this.node.port.postMessage({ type: 'captureRequest', requestId, op });
|
|
2782
|
+
return promise;
|
|
2783
|
+
}
|
|
2784
|
+
|
|
2785
|
+
private sendTransportRequest(): Promise<SonareEngineTransportResponseMessage> {
|
|
2786
|
+
if (this.destroyed) {
|
|
2787
|
+
return Promise.reject(new Error('Realtime engine node is destroyed.'));
|
|
2788
|
+
}
|
|
2789
|
+
const requestId = this.transportRequestId++;
|
|
2790
|
+
const promise = new Promise<SonareEngineTransportResponseMessage>((resolve, reject) => {
|
|
2791
|
+
this.transportRequests.set(requestId, { resolve, reject });
|
|
2792
|
+
});
|
|
2793
|
+
this.node.port.postMessage({ type: 'transportRequest', requestId, op: 'state' });
|
|
2794
|
+
return promise;
|
|
2795
|
+
}
|
|
2119
2796
|
}
|
|
2120
2797
|
|
|
2121
2798
|
export class SonareEngine {
|
|
@@ -2130,9 +2807,26 @@ export class SonareEngine {
|
|
|
2130
2807
|
private readonly offlineChannelCount: number;
|
|
2131
2808
|
private readonly automationLanes = new Map<number, EngineAutomationPoint[]>();
|
|
2132
2809
|
private readonly clips = new Map<number, EngineClip>();
|
|
2810
|
+
private readonly midiClips = new Map<number, EngineMidiClipSchedule>();
|
|
2133
2811
|
private readonly markers = new Map<number, EngineMarker>();
|
|
2812
|
+
private readonly trackLaneIds: number[] = [];
|
|
2813
|
+
private readonly trackSends = new Map<number, EngineTrackSend[]>();
|
|
2814
|
+
private readonly buses: EngineBus[] = [];
|
|
2815
|
+
private readonly trackStripJson = new Map<number, string>();
|
|
2816
|
+
private readonly busStripJson = new Map<number, string>();
|
|
2817
|
+
private masterStripJson: string | undefined;
|
|
2818
|
+
private captureConfig: Omit<SonareEngineSyncCaptureMessage, 'type'> | undefined;
|
|
2819
|
+
private tempoBpm = 120;
|
|
2820
|
+
private timeSignature = { numerator: 4, denominator: 4 };
|
|
2821
|
+
private tempoSegments: EngineTempoSegment[] = [{ startPpq: 0, bpm: 120 }];
|
|
2822
|
+
private timeSignatureSegments: EngineTimeSignatureSegment[] = [
|
|
2823
|
+
{ startPpq: 0, numerator: 4, denominator: 4 },
|
|
2824
|
+
];
|
|
2825
|
+
private latestTransportState: EngineTransportState | undefined;
|
|
2134
2826
|
private nextClipId = 1;
|
|
2135
2827
|
private nextMarkerId = 1;
|
|
2828
|
+
private transportPlaying = false;
|
|
2829
|
+
private readonly pendingInstrumentSync: SonareEngineInstrumentSyncMessage[] = [];
|
|
2136
2830
|
private destroyed = false;
|
|
2137
2831
|
|
|
2138
2832
|
private constructor(
|
|
@@ -2152,8 +2846,21 @@ export class SonareEngine {
|
|
|
2152
2846
|
this.offlineBlockSize = offlineBlockSize;
|
|
2153
2847
|
this.offlineChannelCount = offlineChannelCount;
|
|
2154
2848
|
this.transport = {
|
|
2155
|
-
play: (sampleTime = -1) =>
|
|
2156
|
-
|
|
2849
|
+
play: (sampleTime = -1) => {
|
|
2850
|
+
const ok = this.realtimeNode.play(sampleTime);
|
|
2851
|
+
if (ok) {
|
|
2852
|
+
this.transportPlaying = true;
|
|
2853
|
+
}
|
|
2854
|
+
return ok;
|
|
2855
|
+
},
|
|
2856
|
+
stop: (sampleTime = -1) => {
|
|
2857
|
+
const ok = this.realtimeNode.stop(sampleTime);
|
|
2858
|
+
if (ok) {
|
|
2859
|
+
this.transportPlaying = false;
|
|
2860
|
+
this.flushPendingInstrumentSync();
|
|
2861
|
+
}
|
|
2862
|
+
return ok;
|
|
2863
|
+
},
|
|
2157
2864
|
seekPpq: (ppq, sampleTime = -1) => {
|
|
2158
2865
|
this.offlineEngine.seekPpq(ppq, sampleTime);
|
|
2159
2866
|
return this.realtimeNode.seekPpq(ppq, sampleTime);
|
|
@@ -2164,6 +2871,7 @@ export class SonareEngine {
|
|
|
2164
2871
|
return this.realtimeNode.seekSample(timelineSample, sampleTime);
|
|
2165
2872
|
},
|
|
2166
2873
|
setTempo: (bpm) => this.setTempo(bpm),
|
|
2874
|
+
setTempoSegments: (segments) => this.setTempoSegments(segments),
|
|
2167
2875
|
setLoop: (startPpq, endPpq, enabled = true) => this.setLoop(startPpq, endPpq, enabled),
|
|
2168
2876
|
};
|
|
2169
2877
|
}
|
|
@@ -2205,7 +2913,10 @@ export class SonareEngine {
|
|
|
2205
2913
|
}
|
|
2206
2914
|
|
|
2207
2915
|
setTempo(bpm: number): void {
|
|
2916
|
+
this.tempoBpm = bpm;
|
|
2917
|
+
this.tempoSegments = [{ startPpq: 0, bpm }];
|
|
2208
2918
|
this.offlineEngine.setTempo(bpm);
|
|
2919
|
+
this.postTempoSync();
|
|
2209
2920
|
this.realtimeNode.sendCommand({
|
|
2210
2921
|
type: SonareEngineCommandType.SetTempoMap,
|
|
2211
2922
|
sampleTime: -1,
|
|
@@ -2213,6 +2924,30 @@ export class SonareEngine {
|
|
|
2213
2924
|
});
|
|
2214
2925
|
}
|
|
2215
2926
|
|
|
2927
|
+
setTempoSegments(segments: readonly EngineTempoSegment[]): void {
|
|
2928
|
+
this.tempoSegments = segments.map((segment) => ({ ...segment }));
|
|
2929
|
+
this.tempoBpm = this.tempoSegments[0]?.bpm ?? this.tempoBpm;
|
|
2930
|
+
this.offlineEngine.setTempoSegments(this.tempoSegments);
|
|
2931
|
+
this.postTempoSync();
|
|
2932
|
+
}
|
|
2933
|
+
|
|
2934
|
+
setTimeSignature(numerator: number, denominator: number): void {
|
|
2935
|
+
this.timeSignature = { numerator, denominator };
|
|
2936
|
+
this.timeSignatureSegments = [{ startPpq: 0, numerator, denominator }];
|
|
2937
|
+
this.offlineEngine.setTimeSignature(numerator, denominator);
|
|
2938
|
+
this.postTempoSync();
|
|
2939
|
+
}
|
|
2940
|
+
|
|
2941
|
+
setTimeSignatureSegments(segments: readonly EngineTimeSignatureSegment[]): void {
|
|
2942
|
+
this.timeSignatureSegments = segments.map((segment) => ({ ...segment }));
|
|
2943
|
+
const first = this.timeSignatureSegments[0];
|
|
2944
|
+
if (first) {
|
|
2945
|
+
this.timeSignature = { numerator: first.numerator, denominator: first.denominator };
|
|
2946
|
+
}
|
|
2947
|
+
this.offlineEngine.setTimeSignatureSegments(this.timeSignatureSegments);
|
|
2948
|
+
this.postTempoSync();
|
|
2949
|
+
}
|
|
2950
|
+
|
|
2216
2951
|
setLoop(startPpq: number, endPpq: number, enabled = true): boolean {
|
|
2217
2952
|
this.offlineEngine.setLoop(startPpq, endPpq, enabled);
|
|
2218
2953
|
// Transport precision contract: the SAB command record carries exactly one
|
|
@@ -2233,6 +2968,20 @@ export class SonareEngine {
|
|
|
2233
2968
|
});
|
|
2234
2969
|
}
|
|
2235
2970
|
|
|
2971
|
+
countInEndSample(startSample: number, bars: number): number {
|
|
2972
|
+
return this.offlineEngine.countInEndSample(startSample, bars);
|
|
2973
|
+
}
|
|
2974
|
+
|
|
2975
|
+
async getTransportState(): Promise<EngineTransportState> {
|
|
2976
|
+
const state = await this.realtimeNode.requestTransportState();
|
|
2977
|
+
this.latestTransportState = state;
|
|
2978
|
+
return state;
|
|
2979
|
+
}
|
|
2980
|
+
|
|
2981
|
+
cachedTransportState(): EngineTransportState | undefined {
|
|
2982
|
+
return this.latestTransportState;
|
|
2983
|
+
}
|
|
2984
|
+
|
|
2236
2985
|
setParam(nodeId: string, param: string | number, value: number): boolean {
|
|
2237
2986
|
const paramId = this.resolveParamId(nodeId, param);
|
|
2238
2987
|
// Mirror the change into the offline engine so a subsequent offline render
|
|
@@ -2285,19 +3034,229 @@ export class SonareEngine {
|
|
|
2285
3034
|
}
|
|
2286
3035
|
|
|
2287
3036
|
setSoloMute(target: string | number, solo: boolean, mute: boolean): boolean {
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
3037
|
+
const laneIndex = this.ensureTrackLane(target);
|
|
3038
|
+
this.offlineEngine.setSoloMute(laneIndex, solo, mute);
|
|
3039
|
+
return this.realtimeNode.sendCommand({
|
|
3040
|
+
type: SonareEngineCommandType.SetSoloMute,
|
|
3041
|
+
targetId: laneIndex,
|
|
3042
|
+
sampleTime: -1,
|
|
3043
|
+
argInt: (mute ? 0x1 : 0) | (solo ? 0x2 : 0),
|
|
3044
|
+
});
|
|
3045
|
+
}
|
|
3046
|
+
|
|
3047
|
+
setStripGain(target: string | number, db: number): boolean {
|
|
3048
|
+
if (target === 'master') {
|
|
3049
|
+
const paramId = engineMixerMasterTarget(ENGINE_MIXER_PARAM_FADER_DB);
|
|
3050
|
+
this.offlineEngine.setParameter(paramId, db);
|
|
3051
|
+
return this.realtimeNode.sendCommand({
|
|
3052
|
+
type: SonareEngineCommandType.SetParamSmoothed,
|
|
3053
|
+
targetId: paramId,
|
|
3054
|
+
sampleTime: -1,
|
|
3055
|
+
argFloat: db,
|
|
3056
|
+
});
|
|
3057
|
+
}
|
|
3058
|
+
const laneIndex = this.ensureTrackLane(target);
|
|
3059
|
+
const paramId = engineMixerLaneTarget(laneIndex, ENGINE_MIXER_PARAM_FADER_DB);
|
|
3060
|
+
this.offlineEngine.setParameter(paramId, db);
|
|
3061
|
+
return this.realtimeNode.sendCommand({
|
|
3062
|
+
type: SonareEngineCommandType.SetParamSmoothed,
|
|
3063
|
+
targetId: paramId,
|
|
3064
|
+
sampleTime: -1,
|
|
3065
|
+
argFloat: db,
|
|
3066
|
+
});
|
|
3067
|
+
}
|
|
3068
|
+
|
|
3069
|
+
setStripPan(target: string | number, pan: number): boolean {
|
|
3070
|
+
if (target === 'master') {
|
|
3071
|
+
const paramId = engineMixerMasterTarget(ENGINE_MIXER_PARAM_PAN);
|
|
3072
|
+
this.offlineEngine.setParameter(paramId, pan);
|
|
3073
|
+
return this.realtimeNode.sendCommand({
|
|
3074
|
+
type: SonareEngineCommandType.SetParamSmoothed,
|
|
3075
|
+
targetId: paramId,
|
|
3076
|
+
sampleTime: -1,
|
|
3077
|
+
argFloat: pan,
|
|
3078
|
+
});
|
|
3079
|
+
}
|
|
3080
|
+
const laneIndex = this.ensureTrackLane(target);
|
|
3081
|
+
const paramId = engineMixerLaneTarget(laneIndex, ENGINE_MIXER_PARAM_PAN);
|
|
3082
|
+
this.offlineEngine.setParameter(paramId, pan);
|
|
3083
|
+
return this.realtimeNode.sendCommand({
|
|
3084
|
+
type: SonareEngineCommandType.SetParamSmoothed,
|
|
3085
|
+
targetId: paramId,
|
|
3086
|
+
sampleTime: -1,
|
|
3087
|
+
argFloat: pan,
|
|
3088
|
+
});
|
|
3089
|
+
}
|
|
3090
|
+
|
|
3091
|
+
/**
|
|
3092
|
+
* Declares the mixer track lanes in an explicit order.
|
|
3093
|
+
*
|
|
3094
|
+
* Lane indices are append-only: once a track id occupies a lane, its index
|
|
3095
|
+
* stays fixed for the engine's lifetime. The given list must therefore start
|
|
3096
|
+
* with the already-declared lane ids in their current order and may only
|
|
3097
|
+
* append new track ids after them. Entries carrying `sends` replace that
|
|
3098
|
+
* track's send list; entries without `sends` leave existing sends untouched.
|
|
3099
|
+
*
|
|
3100
|
+
* @param lanes Track ids or lane descriptors in the desired lane order.
|
|
3101
|
+
*/
|
|
3102
|
+
setTrackLanes(lanes: ReadonlyArray<number | EngineTrackLane>): void {
|
|
3103
|
+
const entries = lanes.map((lane) => (typeof lane === 'number' ? { trackId: lane } : lane));
|
|
3104
|
+
const ids: number[] = [];
|
|
3105
|
+
for (const entry of entries) {
|
|
3106
|
+
if (!Number.isInteger(entry.trackId) || entry.trackId <= 0) {
|
|
3107
|
+
throw new Error(`Invalid track id for mixer lane: ${String(entry.trackId)}`);
|
|
3108
|
+
}
|
|
3109
|
+
ids.push(entry.trackId);
|
|
3110
|
+
}
|
|
3111
|
+
if (new Set(ids).size !== ids.length) {
|
|
3112
|
+
throw new Error('Duplicate track id in mixer lane list');
|
|
3113
|
+
}
|
|
3114
|
+
for (let index = 0; index < this.trackLaneIds.length; index++) {
|
|
3115
|
+
if (ids[index] !== this.trackLaneIds[index]) {
|
|
3116
|
+
throw new Error(
|
|
3117
|
+
'Mixer lanes are append-only: keep existing lanes in order and only append new track ids',
|
|
3118
|
+
);
|
|
3119
|
+
}
|
|
3120
|
+
}
|
|
3121
|
+
for (const entry of entries) {
|
|
3122
|
+
if (entry.sends) {
|
|
3123
|
+
this.trackSends.set(
|
|
3124
|
+
entry.trackId,
|
|
3125
|
+
entry.sends.map((send) => ({ ...send })),
|
|
3126
|
+
);
|
|
3127
|
+
}
|
|
3128
|
+
}
|
|
3129
|
+
this.trackLaneIds.splice(0, this.trackLaneIds.length, ...ids);
|
|
3130
|
+
this.syncMixer();
|
|
3131
|
+
}
|
|
3132
|
+
|
|
3133
|
+
setSends(target: string | number, sends: EngineTrackSend[]): void {
|
|
3134
|
+
const laneIndex = this.ensureTrackLane(target);
|
|
3135
|
+
const trackId = this.trackLaneIds[laneIndex];
|
|
3136
|
+
this.trackSends.set(
|
|
3137
|
+
trackId,
|
|
3138
|
+
sends.map((send) => ({ ...send })),
|
|
2300
3139
|
);
|
|
3140
|
+
this.syncMixer();
|
|
3141
|
+
}
|
|
3142
|
+
|
|
3143
|
+
setTrackBuses(buses: EngineBus[]): void {
|
|
3144
|
+
this.buses.splice(0, this.buses.length, ...buses.map((bus) => ({ ...bus })));
|
|
3145
|
+
this.syncMixer();
|
|
3146
|
+
}
|
|
3147
|
+
|
|
3148
|
+
setBusGain(busId: number, db: number): boolean {
|
|
3149
|
+
const busIndex = this.ensureBus(busId);
|
|
3150
|
+
this.buses[busIndex] = { ...this.buses[busIndex], busId, gainDb: db };
|
|
3151
|
+
this.offlineEngine.setTrackBuses(this.buses);
|
|
3152
|
+
const paramId = engineMixerBusTarget(busIndex, ENGINE_MIXER_PARAM_FADER_DB);
|
|
3153
|
+
this.offlineEngine.setParameter(paramId, db);
|
|
3154
|
+
return this.realtimeNode.sendCommand({
|
|
3155
|
+
type: SonareEngineCommandType.SetParamSmoothed,
|
|
3156
|
+
targetId: paramId,
|
|
3157
|
+
sampleTime: -1,
|
|
3158
|
+
argFloat: db,
|
|
3159
|
+
});
|
|
3160
|
+
}
|
|
3161
|
+
|
|
3162
|
+
setTrackStripJson(target: string | number, sceneJson: string): void {
|
|
3163
|
+
const laneIndex = this.ensureTrackLane(target);
|
|
3164
|
+
const trackId = this.trackLaneIds[laneIndex];
|
|
3165
|
+
this.offlineEngine.setTrackStripJson(trackId, sceneJson);
|
|
3166
|
+
this.trackStripJson.set(trackId, sceneJson);
|
|
3167
|
+
this.syncMixer();
|
|
3168
|
+
}
|
|
3169
|
+
|
|
3170
|
+
setTrackStripEqBand(target: string | number, bandIndex: number, band: EqBand | string): void {
|
|
3171
|
+
const laneIndex = this.ensureTrackLane(target);
|
|
3172
|
+
const trackId = this.trackLaneIds[laneIndex];
|
|
3173
|
+
const bandJson = typeof band === 'string' ? band : JSON.stringify(band);
|
|
3174
|
+
this.offlineEngine.setTrackStripEqBandJson(trackId, bandIndex, bandJson);
|
|
3175
|
+
this.postSync({ type: 'syncTrackStripEqBand', trackId, bandIndex, bandJson });
|
|
3176
|
+
}
|
|
3177
|
+
|
|
3178
|
+
setTrackStripInsertBypassed(
|
|
3179
|
+
target: string | number,
|
|
3180
|
+
insertIndex: number,
|
|
3181
|
+
bypassed: boolean,
|
|
3182
|
+
resetOnBypass = false,
|
|
3183
|
+
): void {
|
|
3184
|
+
const laneIndex = this.ensureTrackLane(target);
|
|
3185
|
+
const trackId = this.trackLaneIds[laneIndex];
|
|
3186
|
+
this.offlineEngine.setTrackStripInsertBypassed(trackId, insertIndex, bypassed, resetOnBypass);
|
|
3187
|
+
this.postSync({
|
|
3188
|
+
type: 'syncTrackStripInsertBypassed',
|
|
3189
|
+
trackId,
|
|
3190
|
+
insertIndex,
|
|
3191
|
+
bypassed,
|
|
3192
|
+
resetOnBypass,
|
|
3193
|
+
});
|
|
3194
|
+
}
|
|
3195
|
+
|
|
3196
|
+
setStripEq(target: string | number, bandIndex: number, band: EqBand | string): void {
|
|
3197
|
+
if (target === 'master') {
|
|
3198
|
+
this.setMasterStripEqBand(bandIndex, band);
|
|
3199
|
+
return;
|
|
3200
|
+
}
|
|
3201
|
+
this.setTrackStripEqBand(target, bandIndex, band);
|
|
3202
|
+
}
|
|
3203
|
+
|
|
3204
|
+
setStripInsertBypassed(
|
|
3205
|
+
target: string | number,
|
|
3206
|
+
insertIndex: number,
|
|
3207
|
+
bypassed: boolean,
|
|
3208
|
+
resetOnBypass = false,
|
|
3209
|
+
): void {
|
|
3210
|
+
if (target === 'master') {
|
|
3211
|
+
this.setMasterStripInsertBypassed(insertIndex, bypassed, resetOnBypass);
|
|
3212
|
+
return;
|
|
3213
|
+
}
|
|
3214
|
+
this.setTrackStripInsertBypassed(target, insertIndex, bypassed, resetOnBypass);
|
|
3215
|
+
}
|
|
3216
|
+
|
|
3217
|
+
setStripInserts(target: string | number, sceneJson: string): void {
|
|
3218
|
+
if (target === 'master') {
|
|
3219
|
+
this.setMasterStripJson(sceneJson);
|
|
3220
|
+
return;
|
|
3221
|
+
}
|
|
3222
|
+
this.setTrackStripJson(target, sceneJson);
|
|
3223
|
+
}
|
|
3224
|
+
|
|
3225
|
+
setBusStripJson(busId: number, sceneJson: string): void {
|
|
3226
|
+
this.ensureBus(busId);
|
|
3227
|
+
this.offlineEngine.setBusStripJson(busId, sceneJson);
|
|
3228
|
+
this.busStripJson.set(busId, sceneJson);
|
|
3229
|
+
this.syncMixer();
|
|
3230
|
+
}
|
|
3231
|
+
|
|
3232
|
+
setMasterStripJson(sceneJson: string): void {
|
|
3233
|
+
this.offlineEngine.setMasterStripJson(sceneJson);
|
|
3234
|
+
this.masterStripJson = sceneJson;
|
|
3235
|
+
this.syncMixer();
|
|
3236
|
+
}
|
|
3237
|
+
|
|
3238
|
+
setMasterStripEqBand(bandIndex: number, band: EqBand | string): void {
|
|
3239
|
+
const bandJson = typeof band === 'string' ? band : JSON.stringify(band);
|
|
3240
|
+
this.offlineEngine.setMasterStripEqBandJson(bandIndex, bandJson);
|
|
3241
|
+
this.postSync({ type: 'syncMasterStripEqBand', bandIndex, bandJson });
|
|
3242
|
+
}
|
|
3243
|
+
|
|
3244
|
+
setMasterStripInsertBypassed(
|
|
3245
|
+
insertIndex: number,
|
|
3246
|
+
bypassed: boolean,
|
|
3247
|
+
resetOnBypass = false,
|
|
3248
|
+
): void {
|
|
3249
|
+
this.offlineEngine.setMasterStripInsertBypassed(insertIndex, bypassed, resetOnBypass);
|
|
3250
|
+
this.postSync({
|
|
3251
|
+
type: 'syncMasterStripInsertBypassed',
|
|
3252
|
+
insertIndex,
|
|
3253
|
+
bypassed,
|
|
3254
|
+
resetOnBypass,
|
|
3255
|
+
});
|
|
3256
|
+
}
|
|
3257
|
+
|
|
3258
|
+
setMasterChain(sceneJson: string): void {
|
|
3259
|
+
this.setMasterStripJson(sceneJson);
|
|
2301
3260
|
}
|
|
2302
3261
|
|
|
2303
3262
|
addClip(
|
|
@@ -2312,19 +3271,152 @@ export class SonareEngine {
|
|
|
2312
3271
|
id,
|
|
2313
3272
|
channels: buffer,
|
|
2314
3273
|
startPpq,
|
|
3274
|
+
trackId: this.resolveTargetId(trackId),
|
|
2315
3275
|
};
|
|
3276
|
+
this.ensureTrackLane(trackId);
|
|
2316
3277
|
this.clips.set(id, clip);
|
|
2317
|
-
this.
|
|
2318
|
-
void trackId;
|
|
3278
|
+
this.syncClipsDelta([clip], []);
|
|
2319
3279
|
return id;
|
|
2320
3280
|
}
|
|
2321
3281
|
|
|
2322
3282
|
removeClip(clipId: number): void {
|
|
2323
3283
|
this.clips.delete(clipId);
|
|
2324
|
-
this.
|
|
3284
|
+
this.syncClipsDelta([], [clipId]);
|
|
3285
|
+
}
|
|
3286
|
+
|
|
3287
|
+
setMidiClips(clips: readonly EngineMidiClipSchedule[]): void {
|
|
3288
|
+
this.midiClips.clear();
|
|
3289
|
+
for (const clip of clips) {
|
|
3290
|
+
const id = clip.id ?? this.nextClipId++;
|
|
3291
|
+
this.midiClips.set(id, { ...clip, id, events: clip.events.map((event) => ({ ...event })) });
|
|
3292
|
+
}
|
|
3293
|
+
this.syncMidiClips();
|
|
3294
|
+
}
|
|
3295
|
+
|
|
3296
|
+
setBuiltinInstrument(
|
|
3297
|
+
trackId: string | number,
|
|
3298
|
+
config: { destinationId?: number } & Record<string, unknown> = {},
|
|
3299
|
+
): void {
|
|
3300
|
+
const destinationId = this.resolveTargetId(trackId);
|
|
3301
|
+
this.offlineEngine.setBuiltinInstrument(config, destinationId);
|
|
3302
|
+
this.postInstrumentSync({ type: 'syncBuiltinInstrument', destinationId, config });
|
|
3303
|
+
}
|
|
3304
|
+
|
|
3305
|
+
setSynthInstrument(trackId: string | number, patch: Record<string, unknown> | string = {}): void {
|
|
3306
|
+
const destinationId = this.resolveTargetId(trackId);
|
|
3307
|
+
this.offlineEngine.setSynthInstrument(patch, destinationId);
|
|
3308
|
+
this.postInstrumentSync({ type: 'syncSynthInstrument', destinationId, patch });
|
|
3309
|
+
}
|
|
3310
|
+
|
|
3311
|
+
loadSoundFont(data: Uint8Array): void {
|
|
3312
|
+
this.offlineEngine.loadSoundFont(data);
|
|
3313
|
+
this.postInstrumentSync({ type: 'syncLoadSoundFont', data });
|
|
3314
|
+
}
|
|
3315
|
+
|
|
3316
|
+
setSf2Instrument(
|
|
3317
|
+
trackId: string | number,
|
|
3318
|
+
config: { destinationId?: number; gain?: number; polyphony?: number } = {},
|
|
3319
|
+
): void {
|
|
3320
|
+
const destinationId = this.resolveTargetId(trackId);
|
|
3321
|
+
this.offlineEngine.setSf2Instrument(config, destinationId);
|
|
3322
|
+
this.postInstrumentSync({ type: 'syncSf2Instrument', destinationId, config });
|
|
3323
|
+
}
|
|
3324
|
+
|
|
3325
|
+
pushMidiNoteOn(
|
|
3326
|
+
trackId: string | number,
|
|
3327
|
+
group: number,
|
|
3328
|
+
channel: number,
|
|
3329
|
+
note: number,
|
|
3330
|
+
velocity: number,
|
|
3331
|
+
renderFrame = -1,
|
|
3332
|
+
): void {
|
|
3333
|
+
const destinationId = this.resolveTargetId(trackId);
|
|
3334
|
+
this.offlineEngine.pushMidiNoteOn(destinationId, group, channel, note, velocity, renderFrame);
|
|
3335
|
+
this.postSync({
|
|
3336
|
+
type: 'syncMidiNoteOn',
|
|
3337
|
+
destinationId,
|
|
3338
|
+
group,
|
|
3339
|
+
channel,
|
|
3340
|
+
note,
|
|
3341
|
+
velocity,
|
|
3342
|
+
renderFrame,
|
|
3343
|
+
});
|
|
3344
|
+
}
|
|
3345
|
+
|
|
3346
|
+
pushMidiNoteOff(
|
|
3347
|
+
trackId: string | number,
|
|
3348
|
+
group: number,
|
|
3349
|
+
channel: number,
|
|
3350
|
+
note: number,
|
|
3351
|
+
velocity = 0,
|
|
3352
|
+
renderFrame = -1,
|
|
3353
|
+
): void {
|
|
3354
|
+
const destinationId = this.resolveTargetId(trackId);
|
|
3355
|
+
this.offlineEngine.pushMidiNoteOff(destinationId, group, channel, note, velocity, renderFrame);
|
|
3356
|
+
this.postSync({
|
|
3357
|
+
type: 'syncMidiNoteOff',
|
|
3358
|
+
destinationId,
|
|
3359
|
+
group,
|
|
3360
|
+
channel,
|
|
3361
|
+
note,
|
|
3362
|
+
velocity,
|
|
3363
|
+
renderFrame,
|
|
3364
|
+
});
|
|
3365
|
+
}
|
|
3366
|
+
|
|
3367
|
+
pushMidiCc(
|
|
3368
|
+
trackId: string | number,
|
|
3369
|
+
group: number,
|
|
3370
|
+
channel: number,
|
|
3371
|
+
controller: number,
|
|
3372
|
+
value: number,
|
|
3373
|
+
renderFrame = -1,
|
|
3374
|
+
): void {
|
|
3375
|
+
const destinationId = this.resolveTargetId(trackId);
|
|
3376
|
+
this.offlineEngine.pushMidiCc(destinationId, group, channel, controller, value, renderFrame);
|
|
3377
|
+
this.postSync({
|
|
3378
|
+
type: 'syncMidiCc',
|
|
3379
|
+
destinationId,
|
|
3380
|
+
group,
|
|
3381
|
+
channel,
|
|
3382
|
+
controller,
|
|
3383
|
+
value,
|
|
3384
|
+
renderFrame,
|
|
3385
|
+
});
|
|
3386
|
+
}
|
|
3387
|
+
|
|
3388
|
+
pushMidiPanic(renderFrame = -1): void {
|
|
3389
|
+
this.offlineEngine.pushMidiPanic(renderFrame);
|
|
3390
|
+
this.postSync({ type: 'syncMidiPanic', renderFrame });
|
|
3391
|
+
}
|
|
3392
|
+
|
|
3393
|
+
configureCapture(options: {
|
|
3394
|
+
bufferFrames: number;
|
|
3395
|
+
channels?: number;
|
|
3396
|
+
source?: EngineCaptureStatus['source'];
|
|
3397
|
+
recordOffsetSamples?: number;
|
|
3398
|
+
inputMonitor?: { enabled: boolean; gain?: number };
|
|
3399
|
+
}): void {
|
|
3400
|
+
const bufferFrames = Math.trunc(options.bufferFrames);
|
|
3401
|
+
const channels = Math.trunc(options.channels ?? this.offlineChannelCount);
|
|
3402
|
+
const source = options.source ?? 'output';
|
|
3403
|
+
const recordOffsetSamples = Math.trunc(options.recordOffsetSamples ?? 0);
|
|
3404
|
+
const inputMonitor = {
|
|
3405
|
+
enabled: Boolean(options.inputMonitor?.enabled),
|
|
3406
|
+
gain: options.inputMonitor?.gain ?? 1,
|
|
3407
|
+
};
|
|
3408
|
+
this.offlineEngine.setCaptureBuffer(channels, bufferFrames);
|
|
3409
|
+
this.offlineEngine.setCaptureSource(source);
|
|
3410
|
+
this.offlineEngine.setRecordOffsetSamples(recordOffsetSamples);
|
|
3411
|
+
this.offlineEngine.setInputMonitor(inputMonitor.enabled, inputMonitor.gain);
|
|
3412
|
+
this.captureConfig = { bufferFrames, channels, source, recordOffsetSamples, inputMonitor };
|
|
3413
|
+
this.postSync({ type: 'syncCapture', ...this.captureConfig });
|
|
2325
3414
|
}
|
|
2326
3415
|
|
|
2327
3416
|
armRecord(trackId: string | number, enabled: boolean): boolean {
|
|
3417
|
+
if (enabled && !this.captureConfig) {
|
|
3418
|
+
throw new Error('Capture buffer is not configured');
|
|
3419
|
+
}
|
|
2328
3420
|
this.offlineEngine.armCapture(enabled);
|
|
2329
3421
|
return this.realtimeNode.sendCommand({
|
|
2330
3422
|
type: SonareEngineCommandType.ArmRecord,
|
|
@@ -2335,8 +3427,8 @@ export class SonareEngine {
|
|
|
2335
3427
|
}
|
|
2336
3428
|
|
|
2337
3429
|
punch(inPpq: number, outPpq: number): boolean {
|
|
2338
|
-
const inSample = this.
|
|
2339
|
-
const outSample = this.
|
|
3430
|
+
const inSample = this.offlineEngine.sampleAtPpq(inPpq);
|
|
3431
|
+
const outSample = this.offlineEngine.sampleAtPpq(outPpq);
|
|
2340
3432
|
this.offlineEngine.setCapturePunch(inSample, outSample, true);
|
|
2341
3433
|
// Carry BOTH endpoints as already-converted SAMPLES so the realtime engine
|
|
2342
3434
|
// agrees with the offline engine. The previous code sent the raw PPQ out
|
|
@@ -2351,6 +3443,19 @@ export class SonareEngine {
|
|
|
2351
3443
|
});
|
|
2352
3444
|
}
|
|
2353
3445
|
|
|
3446
|
+
captureStatus(): Promise<EngineCaptureStatus> {
|
|
3447
|
+
return this.realtimeNode.requestCaptureStatus();
|
|
3448
|
+
}
|
|
3449
|
+
|
|
3450
|
+
capturedAudio(): Promise<Float32Array[]> {
|
|
3451
|
+
return this.realtimeNode.requestCapturedAudio();
|
|
3452
|
+
}
|
|
3453
|
+
|
|
3454
|
+
async resetCapture(): Promise<void> {
|
|
3455
|
+
this.offlineEngine.resetCapture();
|
|
3456
|
+
await this.realtimeNode.requestCaptureReset();
|
|
3457
|
+
}
|
|
3458
|
+
|
|
2354
3459
|
setMetronome(opts: EngineMetronomeConfig): void {
|
|
2355
3460
|
this.offlineEngine.setMetronome(opts);
|
|
2356
3461
|
// The full config (beatGain/accentGain/clickSamples/clickSeconds) cannot fit
|
|
@@ -2371,6 +3476,59 @@ export class SonareEngine {
|
|
|
2371
3476
|
return id;
|
|
2372
3477
|
}
|
|
2373
3478
|
|
|
3479
|
+
/**
|
|
3480
|
+
* Replaces the whole marker set in one call.
|
|
3481
|
+
*
|
|
3482
|
+
* Entries without an `id` are assigned fresh ids; entries carrying an `id`
|
|
3483
|
+
* keep it (ids must be positive and unique within the list). Returns the
|
|
3484
|
+
* resolved markers in the order given, so a caller can map its own marker
|
|
3485
|
+
* identities to the engine ids used by `seekMarker`/`setLoopFromMarkers`.
|
|
3486
|
+
*
|
|
3487
|
+
* @param markers The full marker list (an empty list clears all markers).
|
|
3488
|
+
* @returns The markers with their resolved engine ids.
|
|
3489
|
+
*/
|
|
3490
|
+
setMarkers(markers: ReadonlyArray<{ ppq: number; name?: string; id?: number }>): EngineMarker[] {
|
|
3491
|
+
const resolved: EngineMarker[] = [];
|
|
3492
|
+
const seen = new Set<number>();
|
|
3493
|
+
for (const marker of markers) {
|
|
3494
|
+
if (!Number.isFinite(marker.ppq)) {
|
|
3495
|
+
throw new Error(`Invalid marker ppq: ${String(marker.ppq)}`);
|
|
3496
|
+
}
|
|
3497
|
+
if (marker.id !== undefined) {
|
|
3498
|
+
if (!Number.isInteger(marker.id) || marker.id <= 0) {
|
|
3499
|
+
throw new Error(`Invalid marker id: ${String(marker.id)}`);
|
|
3500
|
+
}
|
|
3501
|
+
if (seen.has(marker.id)) {
|
|
3502
|
+
throw new Error(`Duplicate marker id: ${marker.id}`);
|
|
3503
|
+
}
|
|
3504
|
+
}
|
|
3505
|
+
const id = marker.id ?? this.nextMarkerId++;
|
|
3506
|
+
seen.add(id);
|
|
3507
|
+
if (id >= this.nextMarkerId) {
|
|
3508
|
+
this.nextMarkerId = id + 1;
|
|
3509
|
+
}
|
|
3510
|
+
resolved.push({ id, ppq: marker.ppq, name: marker.name ?? '' });
|
|
3511
|
+
}
|
|
3512
|
+
this.markers.clear();
|
|
3513
|
+
for (const marker of resolved) {
|
|
3514
|
+
this.markers.set(marker.id, marker);
|
|
3515
|
+
}
|
|
3516
|
+
this.syncMarkers();
|
|
3517
|
+
return resolved.map((marker) => ({ ...marker }));
|
|
3518
|
+
}
|
|
3519
|
+
|
|
3520
|
+
markerCount(): number {
|
|
3521
|
+
return this.offlineEngine.markerCount();
|
|
3522
|
+
}
|
|
3523
|
+
|
|
3524
|
+
markerByIndex(index: number): EngineMarker {
|
|
3525
|
+
return this.offlineEngine.markerByIndex(index);
|
|
3526
|
+
}
|
|
3527
|
+
|
|
3528
|
+
marker(markerId: number): EngineMarker {
|
|
3529
|
+
return this.offlineEngine.marker(markerId);
|
|
3530
|
+
}
|
|
3531
|
+
|
|
2374
3532
|
seekMarker(markerId: number): boolean {
|
|
2375
3533
|
this.offlineEngine.seekMarker(markerId);
|
|
2376
3534
|
// Forward to the live worklet engine. Its marker set is kept in sync via the
|
|
@@ -2384,6 +3542,13 @@ export class SonareEngine {
|
|
|
2384
3542
|
});
|
|
2385
3543
|
}
|
|
2386
3544
|
|
|
3545
|
+
setLoopFromMarkers(startMarkerId: number, endMarkerId: number): boolean {
|
|
3546
|
+
this.offlineEngine.setLoopFromMarkers(startMarkerId, endMarkerId);
|
|
3547
|
+
const start = this.offlineEngine.marker(startMarkerId);
|
|
3548
|
+
const end = this.offlineEngine.marker(endMarkerId);
|
|
3549
|
+
return this.setLoop(start.ppq, end.ppq, true);
|
|
3550
|
+
}
|
|
3551
|
+
|
|
2387
3552
|
async renderOffline(totalFrames: number): Promise<Float32Array[]> {
|
|
2388
3553
|
const frames = Math.max(0, Math.floor(totalFrames));
|
|
2389
3554
|
const inputs: Float32Array[] = [];
|
|
@@ -2420,14 +3585,50 @@ export class SonareEngine {
|
|
|
2420
3585
|
this.offlineEngine.destroy();
|
|
2421
3586
|
}
|
|
2422
3587
|
|
|
2423
|
-
private
|
|
3588
|
+
private syncClipsDelta(upserts: EngineClip[], removeIds: number[]): void {
|
|
2424
3589
|
const clips = Array.from(this.clips.values());
|
|
2425
3590
|
this.offlineEngine.setClips(clips);
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
3591
|
+
this.postSync({
|
|
3592
|
+
type: 'syncClipsDelta',
|
|
3593
|
+
upserts,
|
|
3594
|
+
removeIds,
|
|
3595
|
+
});
|
|
3596
|
+
}
|
|
3597
|
+
|
|
3598
|
+
private syncMidiClips(): void {
|
|
3599
|
+
const clips = Array.from(this.midiClips.values());
|
|
3600
|
+
this.offlineEngine.setMidiClips(clips);
|
|
3601
|
+
this.postSync({ type: 'syncMidiClips', clips });
|
|
3602
|
+
}
|
|
3603
|
+
|
|
3604
|
+
private syncMixer(): void {
|
|
3605
|
+
const lanes = this.trackLaneIds.map((trackId) => {
|
|
3606
|
+
const sends = this.trackSends.get(trackId);
|
|
3607
|
+
return sends && sends.length > 0
|
|
3608
|
+
? { trackId, sends: sends.map((send) => ({ ...send })) }
|
|
3609
|
+
: { trackId };
|
|
3610
|
+
});
|
|
3611
|
+
const buses = this.buses.map((bus) => ({ ...bus }));
|
|
3612
|
+
this.offlineEngine.setTrackBuses(buses);
|
|
3613
|
+
if (lanes.length > 0) {
|
|
3614
|
+
this.offlineEngine.setTrackLanes(lanes);
|
|
3615
|
+
}
|
|
3616
|
+
const trackStrips = Array.from(this.trackStripJson, ([trackId, sceneJson]) => ({
|
|
3617
|
+
trackId,
|
|
3618
|
+
sceneJson,
|
|
3619
|
+
}));
|
|
3620
|
+
const busStrips = Array.from(this.busStripJson, ([busId, sceneJson]) => ({
|
|
3621
|
+
busId,
|
|
3622
|
+
sceneJson,
|
|
3623
|
+
}));
|
|
3624
|
+
this.postSync({
|
|
3625
|
+
type: 'syncMixer',
|
|
3626
|
+
lanes,
|
|
3627
|
+
buses,
|
|
3628
|
+
trackStrips,
|
|
3629
|
+
busStrips,
|
|
3630
|
+
masterStripJson: this.masterStripJson,
|
|
3631
|
+
});
|
|
2431
3632
|
}
|
|
2432
3633
|
|
|
2433
3634
|
private syncMarkers(): void {
|
|
@@ -2436,6 +3637,37 @@ export class SonareEngine {
|
|
|
2436
3637
|
this.postSync({ type: 'syncMarkers', markers });
|
|
2437
3638
|
}
|
|
2438
3639
|
|
|
3640
|
+
private postInstrumentSync(message: SonareEngineInstrumentSyncMessage): void {
|
|
3641
|
+
if (this.destroyed) {
|
|
3642
|
+
return;
|
|
3643
|
+
}
|
|
3644
|
+
if (this.transportPlaying) {
|
|
3645
|
+
this.pendingInstrumentSync.push(message);
|
|
3646
|
+
return;
|
|
3647
|
+
}
|
|
3648
|
+
this.postSync(message);
|
|
3649
|
+
}
|
|
3650
|
+
|
|
3651
|
+
private flushPendingInstrumentSync(): void {
|
|
3652
|
+
if (this.destroyed || this.pendingInstrumentSync.length === 0) {
|
|
3653
|
+
return;
|
|
3654
|
+
}
|
|
3655
|
+
const pending = this.pendingInstrumentSync.splice(0);
|
|
3656
|
+
for (const message of pending) {
|
|
3657
|
+
this.postSync(message);
|
|
3658
|
+
}
|
|
3659
|
+
}
|
|
3660
|
+
|
|
3661
|
+
private postTempoSync(): void {
|
|
3662
|
+
this.postSync({
|
|
3663
|
+
type: 'syncTempo',
|
|
3664
|
+
bpm: this.tempoBpm,
|
|
3665
|
+
timeSignature: { ...this.timeSignature },
|
|
3666
|
+
tempoSegments: this.tempoSegments.map((segment) => ({ ...segment })),
|
|
3667
|
+
timeSignatureSegments: this.timeSignatureSegments.map((segment) => ({ ...segment })),
|
|
3668
|
+
});
|
|
3669
|
+
}
|
|
3670
|
+
|
|
2439
3671
|
// Posts an out-of-band control-sync message to the worklet engine processor.
|
|
2440
3672
|
// Sync messages use a string `type` so the worklet's message handler routes
|
|
2441
3673
|
// them to receiveSync() (numeric `type` is reserved for SonareEngineCommandRecord).
|
|
@@ -2465,16 +3697,40 @@ export class SonareEngine {
|
|
|
2465
3697
|
return Number.isFinite(parsed) ? parsed : 0;
|
|
2466
3698
|
}
|
|
2467
3699
|
|
|
3700
|
+
private ensureTrackLane(target: string | number): number {
|
|
3701
|
+
const trackId = this.resolveTargetId(target);
|
|
3702
|
+
if (!Number.isInteger(trackId) || trackId <= 0) {
|
|
3703
|
+
throw new Error(`Invalid track id for mixer lane: ${String(target)}`);
|
|
3704
|
+
}
|
|
3705
|
+
const existing = this.trackLaneIds.indexOf(trackId);
|
|
3706
|
+
if (existing >= 0) {
|
|
3707
|
+
return existing;
|
|
3708
|
+
}
|
|
3709
|
+
this.trackLaneIds.push(trackId);
|
|
3710
|
+
this.syncMixer();
|
|
3711
|
+
return this.trackLaneIds.length - 1;
|
|
3712
|
+
}
|
|
3713
|
+
|
|
3714
|
+
private ensureBus(busId: number): number {
|
|
3715
|
+
const resolved = Math.trunc(busId);
|
|
3716
|
+
if (!Number.isInteger(resolved) || resolved <= 0) {
|
|
3717
|
+
throw new Error(`Invalid bus id for mixer bus: ${String(busId)}`);
|
|
3718
|
+
}
|
|
3719
|
+
const existing = this.buses.findIndex((bus) => bus.busId === resolved);
|
|
3720
|
+
if (existing >= 0) {
|
|
3721
|
+
return existing;
|
|
3722
|
+
}
|
|
3723
|
+
this.buses.push({ busId: resolved });
|
|
3724
|
+
this.syncMixer();
|
|
3725
|
+
return this.buses.length - 1;
|
|
3726
|
+
}
|
|
3727
|
+
|
|
2468
3728
|
private curveCode(curve: number | 'linear' | 'exponential'): number {
|
|
2469
3729
|
if (typeof curve === 'number') {
|
|
2470
3730
|
return curve;
|
|
2471
3731
|
}
|
|
2472
3732
|
return curve === 'exponential' ? 1 : 0;
|
|
2473
3733
|
}
|
|
2474
|
-
|
|
2475
|
-
private ppqToApproxSample(ppq: number): number {
|
|
2476
|
-
return Math.max(0, Math.round(((ppq * 60) / 120) * this.sampleRate));
|
|
2477
|
-
}
|
|
2478
3734
|
}
|
|
2479
3735
|
|
|
2480
3736
|
export class SonareRealtimeVoiceChangerWorkletProcessor {
|
|
@@ -2768,6 +4024,7 @@ export function registerSonareRealtimeEngineWorkletProcessor(
|
|
|
2768
4024
|
class RegisteredSonareRealtimeEngineWorkletProcessor extends Base {
|
|
2769
4025
|
private bridge?: SonareRealtimeEngineWorkletProcessor;
|
|
2770
4026
|
private rtBridge?: SonareRtRealtimeEngineRuntime;
|
|
4027
|
+
private readonly pendingMessages: unknown[] = [];
|
|
2771
4028
|
readonly port?: WorkletPort;
|
|
2772
4029
|
|
|
2773
4030
|
constructor(options?: { processorOptions?: SonareRealtimeEngineWorkletProcessorOptions }) {
|
|
@@ -2777,18 +4034,27 @@ export function registerSonareRealtimeEngineWorkletProcessor(
|
|
|
2777
4034
|
if (processorOptions.runtimeTarget === 'sonare-rt') {
|
|
2778
4035
|
void this.initializeSonareRt(processorOptions, port);
|
|
2779
4036
|
} else {
|
|
2780
|
-
this.
|
|
2781
|
-
postMessage: (message) => port?.postMessage?.(message),
|
|
2782
|
-
onMeter: (meter) => port?.postMessage?.(meter),
|
|
2783
|
-
});
|
|
4037
|
+
void this.initializeEmbind(processorOptions, port);
|
|
2784
4038
|
}
|
|
2785
4039
|
const onMessage = (event: { data: unknown }) => {
|
|
4040
|
+
if (!this.bridge && !this.rtBridge) {
|
|
4041
|
+
if (this.pendingMessages.length < 1024) {
|
|
4042
|
+
this.pendingMessages.push(event.data);
|
|
4043
|
+
}
|
|
4044
|
+
return;
|
|
4045
|
+
}
|
|
2786
4046
|
if (isEngineCommandRecord(event.data)) {
|
|
2787
4047
|
this.bridge?.receiveCommand(event.data);
|
|
2788
4048
|
this.rtBridge?.receiveCommand(event.data);
|
|
2789
4049
|
} else if (isEngineSyncMessage(event.data)) {
|
|
2790
4050
|
this.bridge?.receiveSync(event.data);
|
|
2791
4051
|
this.rtBridge?.receiveSync(event.data);
|
|
4052
|
+
} else if (isEngineCaptureRequestMessage(event.data)) {
|
|
4053
|
+
this.bridge?.receiveCaptureRequest(event.data);
|
|
4054
|
+
this.rtBridge?.receiveCaptureRequest(event.data, port);
|
|
4055
|
+
} else if (isEngineTransportRequestMessage(event.data)) {
|
|
4056
|
+
this.bridge?.receiveTransportRequest(event.data);
|
|
4057
|
+
this.rtBridge?.receiveTransportRequest(event.data, port);
|
|
2792
4058
|
}
|
|
2793
4059
|
};
|
|
2794
4060
|
if (port?.addEventListener) {
|
|
@@ -2813,6 +4079,75 @@ export function registerSonareRealtimeEngineWorkletProcessor(
|
|
|
2813
4079
|
return true;
|
|
2814
4080
|
}
|
|
2815
4081
|
|
|
4082
|
+
private replayPendingMessages(port?: WorkletPort): void {
|
|
4083
|
+
const messages = this.pendingMessages.splice(0);
|
|
4084
|
+
for (const data of messages) {
|
|
4085
|
+
if (isEngineCommandRecord(data)) {
|
|
4086
|
+
this.bridge?.receiveCommand(data);
|
|
4087
|
+
this.rtBridge?.receiveCommand(data);
|
|
4088
|
+
} else if (isEngineSyncMessage(data)) {
|
|
4089
|
+
this.bridge?.receiveSync(data);
|
|
4090
|
+
this.rtBridge?.receiveSync(data);
|
|
4091
|
+
} else if (isEngineCaptureRequestMessage(data)) {
|
|
4092
|
+
this.bridge?.receiveCaptureRequest(data);
|
|
4093
|
+
this.rtBridge?.receiveCaptureRequest(data, port);
|
|
4094
|
+
} else if (isEngineTransportRequestMessage(data)) {
|
|
4095
|
+
this.bridge?.receiveTransportRequest(data);
|
|
4096
|
+
this.rtBridge?.receiveTransportRequest(data, port);
|
|
4097
|
+
}
|
|
4098
|
+
}
|
|
4099
|
+
}
|
|
4100
|
+
|
|
4101
|
+
private async initializeEmbind(
|
|
4102
|
+
options: SonareRealtimeEngineWorkletProcessorOptions,
|
|
4103
|
+
port?: WorkletPort,
|
|
4104
|
+
): Promise<void> {
|
|
4105
|
+
try {
|
|
4106
|
+
const initPromise = (
|
|
4107
|
+
globalThis as typeof globalThis & { SonareEmbindInitPromise?: Promise<void> }
|
|
4108
|
+
).SonareEmbindInitPromise;
|
|
4109
|
+
if (initPromise) {
|
|
4110
|
+
await initPromise;
|
|
4111
|
+
}
|
|
4112
|
+
if (!isInitialized()) {
|
|
4113
|
+
type EmbindModuleFactory = (options?: {
|
|
4114
|
+
locateFile?: (path: string, prefix: string) => string;
|
|
4115
|
+
wasmBinary?: ArrayBuffer | Uint8Array;
|
|
4116
|
+
}) => Promise<SonareModule>;
|
|
4117
|
+
const moduleFactory = (
|
|
4118
|
+
globalThis as typeof globalThis & {
|
|
4119
|
+
SonareEmbindModuleFactory?: EmbindModuleFactory;
|
|
4120
|
+
}
|
|
4121
|
+
).SonareEmbindModuleFactory;
|
|
4122
|
+
if (!moduleFactory) {
|
|
4123
|
+
throw new Error('embind realtime engine module is not initialized.');
|
|
4124
|
+
}
|
|
4125
|
+
await initSonareModule({
|
|
4126
|
+
locateFile: (path) => path,
|
|
4127
|
+
wasmBinary: options.wasmBinary,
|
|
4128
|
+
moduleFactory,
|
|
4129
|
+
});
|
|
4130
|
+
}
|
|
4131
|
+
this.bridge = new SonareRealtimeEngineWorkletProcessor(options, {
|
|
4132
|
+
postMessage: (message) => port?.postMessage?.(message),
|
|
4133
|
+
onMeter: (meter) => port?.postMessage?.(meter),
|
|
4134
|
+
});
|
|
4135
|
+
for (const message of options.initialSyncMessages ?? []) {
|
|
4136
|
+
this.bridge.receiveSync(message);
|
|
4137
|
+
}
|
|
4138
|
+
for (const command of options.initialCommands ?? []) {
|
|
4139
|
+
this.bridge.receiveCommand(command);
|
|
4140
|
+
}
|
|
4141
|
+
this.replayPendingMessages(port);
|
|
4142
|
+
port?.postMessage?.({ type: 'ready', runtimeTarget: 'embind' });
|
|
4143
|
+
} catch (error) {
|
|
4144
|
+
port?.postMessage?.({
|
|
4145
|
+
type: 'error',
|
|
4146
|
+
message: error instanceof Error ? error.message : String(error),
|
|
4147
|
+
});
|
|
4148
|
+
}
|
|
4149
|
+
}
|
|
4150
|
+
|
|
2816
4151
|
private async initializeSonareRt(
|
|
2817
4152
|
options: SonareRealtimeEngineWorkletProcessorOptions,
|
|
2818
4153
|
port?: WorkletPort,
|
|
@@ -2857,6 +4192,7 @@ export function registerSonareRealtimeEngineWorkletProcessor(
|
|
|
2857
4192
|
telemetrySharedBuffer: options.telemetrySharedBuffer,
|
|
2858
4193
|
telemetryRingCapacity: options.telemetryRingCapacity,
|
|
2859
4194
|
});
|
|
4195
|
+
this.replayPendingMessages(port);
|
|
2860
4196
|
port?.postMessage?.({ type: 'ready', runtimeTarget: 'sonare-rt' });
|
|
2861
4197
|
} catch (error) {
|
|
2862
4198
|
port?.postMessage?.({
|