@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.
- package/README.md +51 -20
- package/dist/index.d.ts +5416 -1
- package/dist/index.js +938 -583
- package/dist/index.js.map +1 -1
- package/dist/sonare.js +2 -2
- package/dist/sonare.wasm +0 -0
- package/dist/worklet.d.ts +1083 -5227
- package/dist/worklet.js +2683 -2451
- package/dist/worklet.js.map +1 -1
- package/package.json +4 -9
- package/src/clip_page_streamer.ts +298 -0
- package/src/effects_mastering.ts +85 -1089
- package/src/effects_transform.ts +286 -0
- package/src/effects_voice_change.ts +118 -0
- package/src/feature_music.ts +5 -2
- package/src/feature_spectrogram.ts +42 -2
- package/src/features.ts +1 -0
- package/src/index.ts +23 -0
- package/src/mastering_chain.ts +200 -0
- package/src/mastering_core.ts +248 -0
- package/src/mastering_dynamics.ts +105 -0
- package/src/mastering_repair.ts +161 -0
- package/src/mixer.ts +8 -0
- package/src/mixing_oneshot.ts +54 -0
- package/src/module_state.ts +1 -2
- package/src/project.ts +71 -1712
- package/src/project_class.ts +871 -0
- package/src/project_internal.ts +333 -0
- package/src/project_synth.ts +43 -0
- package/src/project_types.ts +570 -0
- package/src/public_types.ts +6 -1221
- package/src/public_types_acoustic.ts +115 -0
- package/src/public_types_mastering.ts +333 -0
- package/src/public_types_mixing.ts +97 -0
- package/src/public_types_music.ts +352 -0
- package/src/public_types_realtime.ts +163 -0
- package/src/public_types_spectral.ts +194 -0
- package/src/realtime_engine.ts +94 -0
- package/src/sonare.js.d.ts +117 -38
- package/src/stream_analyzer.ts +3 -0
- package/src/stream_types.ts +4 -0
- package/src/worklet/engine-automation.ts +73 -0
- package/src/worklet/engine-capture-facade.ts +80 -0
- package/src/worklet/engine-clips.ts +71 -0
- package/src/worklet/engine-markers.ts +93 -0
- package/src/worklet/engine-mixer-facade.ts +186 -0
- package/src/worklet/engine-node.ts +451 -0
- package/src/worklet/engine-offline.ts +162 -0
- package/src/worklet/engine-options.ts +13 -0
- package/src/worklet/engine-parameter-facade.ts +172 -0
- package/src/worklet/engine-processor.ts +764 -0
- package/src/worklet/engine-register.ts +136 -0
- package/src/worklet/engine-strips.ts +315 -0
- package/src/worklet/engine-sync.ts +94 -0
- package/src/worklet/engine-tempo-facade.ts +141 -0
- package/src/worklet/engine.ts +998 -0
- package/src/worklet/guards.ts +14 -1
- package/src/worklet/messages.ts +60 -20
- package/src/worklet/mixer-processor.ts +368 -0
- package/src/worklet/protocol.ts +3 -0
- package/src/worklet/voice-changer-processor.ts +246 -0
- package/src/worklet.ts +20 -3549
- package/dist/sonare-rt-module.js +0 -2
- package/dist/sonare-rt.js +0 -2
- package/dist/sonare-rt.wasm +0 -0
- 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
|
+
}
|