@libraz/libsonare 1.8.0 → 1.8.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 +2 -2
- package/dist/_effects_common.d.ts +2 -1
- package/dist/_effects_common.d.ts.map +1 -1
- package/dist/analysis.js +1 -0
- package/dist/analysis.js.map +1 -1
- package/dist/effects_mastering.d.ts +2 -2
- package/dist/effects_mastering.d.ts.map +1 -1
- package/dist/effects_note_ops.d.ts +55 -30
- package/dist/effects_note_ops.d.ts.map +1 -1
- package/dist/effects_timepitch.d.ts +17 -12
- package/dist/effects_timepitch.d.ts.map +1 -1
- package/dist/feature_decompose.d.ts +1 -1
- package/dist/feature_decompose.d.ts.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +216 -60
- package/dist/index.js.map +1 -1
- package/dist/mastering_core.d.ts +27 -1
- package/dist/mastering_core.d.ts.map +1 -1
- package/dist/mixer.d.ts.map +1 -1
- package/dist/project_types.d.ts +7 -5
- package/dist/project_types.d.ts.map +1 -1
- package/dist/public_types_mastering.d.ts +19 -1
- package/dist/public_types_mastering.d.ts.map +1 -1
- package/dist/public_types_playback.d.ts +1 -0
- package/dist/public_types_playback.d.ts.map +1 -1
- package/dist/public_types_spectral.d.ts +5 -5
- package/dist/public_types_spectral.d.ts.map +1 -1
- package/dist/realtime_engine.d.ts +49 -6
- package/dist/realtime_engine.d.ts.map +1 -1
- package/dist/sonare-analysis.wasm +0 -0
- package/dist/sonare.d.ts +36 -0
- package/dist/sonare.js +1 -1
- package/dist/sonare.wasm +0 -0
- package/dist/streaming_processors.d.ts +5 -0
- package/dist/streaming_processors.d.ts.map +1 -1
- package/dist/worker.js.map +1 -1
- package/dist/worklet/engine-mixer-facade.d.ts +40 -4
- package/dist/worklet/engine-mixer-facade.d.ts.map +1 -1
- package/dist/worklet/engine-node.d.ts +2 -0
- package/dist/worklet/engine-node.d.ts.map +1 -1
- package/dist/worklet/engine-processor.d.ts +4 -0
- package/dist/worklet/engine-processor.d.ts.map +1 -1
- package/dist/worklet/engine-strips.d.ts +4 -2
- package/dist/worklet/engine-strips.d.ts.map +1 -1
- package/dist/worklet/engine-sync.d.ts +4 -2
- package/dist/worklet/engine-sync.d.ts.map +1 -1
- package/dist/worklet/engine.d.ts +27 -6
- package/dist/worklet/engine.d.ts.map +1 -1
- package/dist/worklet/guards.d.ts +7 -1
- package/dist/worklet/guards.d.ts.map +1 -1
- package/dist/worklet/messages.d.ts +4 -4
- package/dist/worklet/messages.d.ts.map +1 -1
- package/dist/worklet/mixer-processor.d.ts +2 -2
- package/dist/worklet/mixer-processor.d.ts.map +1 -1
- package/dist/worklet/protocol.d.ts +10 -2
- package/dist/worklet/protocol.d.ts.map +1 -1
- package/dist/worklet.js +585 -188
- package/dist/worklet.js.map +1 -1
- package/package.json +1 -1
- package/src/_effects_common.ts +36 -6
- package/src/effects_mastering.ts +4 -0
- package/src/effects_note_ops.ts +92 -44
- package/src/effects_timepitch.ts +42 -31
- package/src/feature_decompose.ts +2 -2
- package/src/index.ts +6 -0
- package/src/mastering_core.ts +95 -0
- package/src/mixer.ts +39 -19
- package/src/project_types.ts +7 -5
- package/src/public_types_mastering.ts +21 -0
- package/src/public_types_playback.ts +1 -0
- package/src/public_types_spectral.ts +5 -5
- package/src/realtime_engine.ts +71 -15
- package/src/sonare.js.d.ts +36 -0
- package/src/streaming_processors.ts +8 -0
- package/src/worklet/engine-mixer-facade.ts +420 -86
- package/src/worklet/engine-node.ts +22 -4
- package/src/worklet/engine-processor.ts +83 -33
- package/src/worklet/engine-strips.ts +5 -10
- package/src/worklet/engine-sync.ts +6 -1
- package/src/worklet/engine.ts +49 -37
- package/src/worklet/guards.ts +55 -0
- package/src/worklet/messages.ts +4 -2
- package/src/worklet/mixer-processor.ts +10 -3
- package/src/worklet/protocol.ts +29 -8
|
@@ -9,8 +9,10 @@ import {
|
|
|
9
9
|
isEngineTransportResponseMessage,
|
|
10
10
|
isExternalMidiBatchMessage,
|
|
11
11
|
isMeterSnapshot,
|
|
12
|
+
isScopeSnapshot,
|
|
12
13
|
requireChannelCount,
|
|
13
14
|
requireIntegerOption,
|
|
15
|
+
resolveScopeBandCount,
|
|
14
16
|
} from './guards';
|
|
15
17
|
import type {
|
|
16
18
|
SonareEngineCaptureRequestMessage,
|
|
@@ -72,7 +74,8 @@ function isValidCommandRecord(command: SonareEngineCommandRecord): boolean {
|
|
|
72
74
|
!Number.isSafeInteger(type) ||
|
|
73
75
|
type < SonareEngineCommandType.SetParam ||
|
|
74
76
|
(type > SonareEngineCommandType.SeekMarker &&
|
|
75
|
-
type !== SonareEngineCommandType.SetTrackMonitorMode
|
|
77
|
+
type !== SonareEngineCommandType.SetTrackMonitorMode &&
|
|
78
|
+
type !== SonareEngineCommandType.ResetMasterLoudnessMeter)
|
|
76
79
|
) {
|
|
77
80
|
return false;
|
|
78
81
|
}
|
|
@@ -206,6 +209,8 @@ export class SonareRealtimeEngineNode {
|
|
|
206
209
|
this.emitTelemetry(event.data);
|
|
207
210
|
} else if (isMeterSnapshot(event.data)) {
|
|
208
211
|
this.emitMeter(event.data);
|
|
212
|
+
} else if (isScopeSnapshot(event.data)) {
|
|
213
|
+
this.emitScope(event.data);
|
|
209
214
|
} else if (isExternalMidiBatchMessage(event.data)) {
|
|
210
215
|
this.emitMidiOut(event.data.events);
|
|
211
216
|
} else if (isClipPageRequestMessage(event.data)) {
|
|
@@ -273,6 +278,13 @@ export class SonareRealtimeEngineNode {
|
|
|
273
278
|
);
|
|
274
279
|
}
|
|
275
280
|
|
|
281
|
+
const meterIntervalFrames = requireIntegerOption(
|
|
282
|
+
options.meterIntervalFrames,
|
|
283
|
+
2048,
|
|
284
|
+
'meterIntervalFrames',
|
|
285
|
+
0,
|
|
286
|
+
);
|
|
287
|
+
|
|
276
288
|
// Resolved before the transport branch, so a capacity the ring could not
|
|
277
289
|
// hold is refused by name whichever transport the caller ends up with.
|
|
278
290
|
const commandRingCapacity = requireIntegerOption(
|
|
@@ -302,7 +314,7 @@ export class SonareRealtimeEngineNode {
|
|
|
302
314
|
'scopeRingCapacity',
|
|
303
315
|
1,
|
|
304
316
|
);
|
|
305
|
-
const scopeBands =
|
|
317
|
+
const scopeBands = resolveScopeBandCount(options.scopeBands);
|
|
306
318
|
const clipPageRequestRingCapacity = requireIntegerOption(
|
|
307
319
|
options.clipPageRequestRingCapacity,
|
|
308
320
|
128,
|
|
@@ -352,6 +364,7 @@ export class SonareRealtimeEngineNode {
|
|
|
352
364
|
sampleRate: options.sampleRate ?? context.sampleRate,
|
|
353
365
|
blockSize,
|
|
354
366
|
channelCount,
|
|
367
|
+
meterIntervalFrames,
|
|
355
368
|
cueOutput,
|
|
356
369
|
commandSharedBuffer: commandRing?.sharedBuffer,
|
|
357
370
|
commandRingCapacity: commandRing?.capacity,
|
|
@@ -361,8 +374,8 @@ export class SonareRealtimeEngineNode {
|
|
|
361
374
|
meterRingCapacity: meterRing?.capacity,
|
|
362
375
|
scopeSharedBuffer: scopeRing?.sharedBuffer,
|
|
363
376
|
scopeRingCapacity: scopeRing?.capacity,
|
|
364
|
-
scopeBands: scopeRing?.bands,
|
|
365
|
-
scopeIntervalFrames:
|
|
377
|
+
scopeBands: scopeIntervalFrames > 0 ? (scopeRing?.bands ?? scopeBands) : undefined,
|
|
378
|
+
scopeIntervalFrames: scopeIntervalFrames > 0 ? scopeIntervalFrames : undefined,
|
|
366
379
|
clipPageRequestSharedBuffer: clipPageRequestRing?.sharedBuffer,
|
|
367
380
|
clipPageRequestRingCapacity: clipPageRequestRing?.capacity,
|
|
368
381
|
externalMidiSharedBuffer: externalMidiRing?.sharedBuffer,
|
|
@@ -416,6 +429,11 @@ export class SonareRealtimeEngineNode {
|
|
|
416
429
|
return this.sendCommand({ type: SonareEngineCommandType.TransportPlay, sampleTime });
|
|
417
430
|
}
|
|
418
431
|
|
|
432
|
+
/** Queues a reset of the master's integrated-loudness accumulator. */
|
|
433
|
+
resetMasterLoudnessMeter(sampleTime = -1): boolean {
|
|
434
|
+
return this.sendCommand({ type: SonareEngineCommandType.ResetMasterLoudnessMeter, sampleTime });
|
|
435
|
+
}
|
|
436
|
+
|
|
419
437
|
stop(sampleTime = -1): boolean {
|
|
420
438
|
return this.sendCommand({ type: SonareEngineCommandType.TransportStop, sampleTime });
|
|
421
439
|
}
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import type { EngineClip } from '../index';
|
|
2
2
|
import { RealtimeEngine } from '../index';
|
|
3
3
|
import { copyPlanesToOutput, type WorkletInput, type WorkletOutput } from './audio_types';
|
|
4
|
-
import {
|
|
5
|
-
|
|
4
|
+
import {
|
|
5
|
+
requireChannelCount,
|
|
6
|
+
requireIntegerOption,
|
|
7
|
+
resolveScopeBandCount,
|
|
8
|
+
SONARE_SCOPE_MAX_BANDS,
|
|
9
|
+
SONARE_SCOPE_MAX_POINTS,
|
|
10
|
+
} from './guards';
|
|
6
11
|
import {
|
|
7
12
|
DEFAULT_METRONOME_CONFIG,
|
|
8
13
|
type ResolvedMetronomeConfig,
|
|
@@ -41,6 +46,7 @@ import {
|
|
|
41
46
|
type SonareEngineTelemetryRecord,
|
|
42
47
|
type SonareEngineTelemetryRingBuffer,
|
|
43
48
|
SonareEngineTelemetryType,
|
|
49
|
+
type SonareWorkletScopeSnapshot,
|
|
44
50
|
scopeRingFromSharedBuffer,
|
|
45
51
|
telemetryFromEngine,
|
|
46
52
|
writeInt64Words,
|
|
@@ -84,6 +90,9 @@ export class SonareRealtimeEngineWorkletProcessor {
|
|
|
84
90
|
private transport?: WorkletTransport;
|
|
85
91
|
private meterIntervalFrames: number;
|
|
86
92
|
private lastMeterFrame = Number.NEGATIVE_INFINITY;
|
|
93
|
+
private scopeIntervalFrames: number;
|
|
94
|
+
private scopeEnabled: boolean;
|
|
95
|
+
private scopeBands: number;
|
|
87
96
|
// Latest metronome gains/click length pushed via 'syncMetronome'. The
|
|
88
97
|
// SetMetronome command only toggles enabled state; the config arrives here.
|
|
89
98
|
private metronomeConfig: ResolvedMetronomeConfig = { ...DEFAULT_METRONOME_CONFIG };
|
|
@@ -146,6 +155,16 @@ export class SonareRealtimeEngineWorkletProcessor {
|
|
|
146
155
|
options.scopeBands,
|
|
147
156
|
)
|
|
148
157
|
: undefined;
|
|
158
|
+
this.scopeIntervalFrames = requireIntegerOption(
|
|
159
|
+
options.scopeIntervalFrames,
|
|
160
|
+
this.scopeRing ? this.blockSize : 0,
|
|
161
|
+
'scopeIntervalFrames',
|
|
162
|
+
0,
|
|
163
|
+
);
|
|
164
|
+
this.scopeBands = resolveScopeBandCount(options.scopeBands);
|
|
165
|
+
this.scopeEnabled =
|
|
166
|
+
this.scopeIntervalFrames > 0 &&
|
|
167
|
+
(this.scopeRing !== undefined || typeof this.transport?.postMessage === 'function');
|
|
149
168
|
this.clipPageRequestRing = options.clipPageRequestSharedBuffer
|
|
150
169
|
? clipPageRequestRingFromSharedBuffer(
|
|
151
170
|
options.clipPageRequestSharedBuffer,
|
|
@@ -183,18 +202,12 @@ export class SonareRealtimeEngineWorkletProcessor {
|
|
|
183
202
|
this.monitorBuffers[ch] = this.engine.getMonitorChannelBuffer(ch, this.blockSize);
|
|
184
203
|
}
|
|
185
204
|
}
|
|
186
|
-
// Arm the
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const interval = requireIntegerOption(
|
|
192
|
-
options.scopeIntervalFrames,
|
|
193
|
-
this.blockSize,
|
|
194
|
-
'scopeIntervalFrames',
|
|
195
|
-
0,
|
|
205
|
+
// Arm the native scope producer only when a SAB ring or postMessage sink exists.
|
|
206
|
+
if (this.scopeEnabled) {
|
|
207
|
+
this.engine.configureScopeTelemetry(
|
|
208
|
+
this.scopeIntervalFrames,
|
|
209
|
+
Math.min(this.scopeRing?.bands ?? this.scopeBands, SONARE_SCOPE_MAX_BANDS),
|
|
196
210
|
);
|
|
197
|
-
this.engine.configureScopeTelemetry(interval, this.scopeRing.bands);
|
|
198
211
|
}
|
|
199
212
|
}
|
|
200
213
|
|
|
@@ -496,37 +509,32 @@ export class SonareRealtimeEngineWorkletProcessor {
|
|
|
496
509
|
// Complete earlier immediate edits in message order. Scheduled
|
|
497
510
|
// commands remain queued for their render frame.
|
|
498
511
|
this.engine.applyCommandsDueNowPreservingFuture();
|
|
499
|
-
this.engine.settleInsertParameters();
|
|
500
512
|
if (message.buses) {
|
|
501
513
|
this.engine.setTrackBuses(message.buses);
|
|
502
514
|
}
|
|
503
515
|
this.engine.setTrackLanes(message.lanes);
|
|
504
|
-
for (const
|
|
505
|
-
|
|
516
|
+
for (const strip of message.trackStrips ?? []) {
|
|
517
|
+
this.engine.setTrackStripJson(strip.trackId, strip.sceneJson);
|
|
518
|
+
}
|
|
519
|
+
for (const strip of message.busStrips ?? []) {
|
|
520
|
+
this.engine.setBusStripJson(strip.busId, strip.sceneJson);
|
|
521
|
+
}
|
|
522
|
+
if (message.masterStripJson) {
|
|
523
|
+
this.engine.setMasterStripJson(message.masterStripJson);
|
|
524
|
+
}
|
|
525
|
+
for (const target of message.insertBaseResets ?? []) {
|
|
506
526
|
switch (target.kind) {
|
|
507
527
|
case 'track':
|
|
508
528
|
this.engine.clearTrackInsertParameterBases(target.trackId);
|
|
509
|
-
this.engine.setTrackStripJson(target.trackId, emptyJson);
|
|
510
529
|
break;
|
|
511
530
|
case 'bus':
|
|
512
531
|
this.engine.clearBusInsertParameterBases(target.busId);
|
|
513
|
-
this.engine.setBusStripJson(target.busId, emptyJson);
|
|
514
532
|
break;
|
|
515
533
|
case 'master':
|
|
516
534
|
this.engine.clearMasterInsertParameterBases();
|
|
517
|
-
this.engine.setMasterStripJson(emptyJson);
|
|
518
535
|
break;
|
|
519
536
|
}
|
|
520
537
|
}
|
|
521
|
-
for (const strip of message.trackStrips ?? []) {
|
|
522
|
-
this.engine.setTrackStripJson(strip.trackId, strip.sceneJson);
|
|
523
|
-
}
|
|
524
|
-
for (const strip of message.busStrips ?? []) {
|
|
525
|
-
this.engine.setBusStripJson(strip.busId, strip.sceneJson);
|
|
526
|
-
}
|
|
527
|
-
if (message.masterStripJson) {
|
|
528
|
-
this.engine.setMasterStripJson(message.masterStripJson);
|
|
529
|
-
}
|
|
530
538
|
for (const binding of message.laneSidechains ?? []) {
|
|
531
539
|
this.engine.setLaneSidechain(binding.trackId, binding.insertIndex, binding.sourceTrackId);
|
|
532
540
|
}
|
|
@@ -1056,6 +1064,9 @@ export class SonareRealtimeEngineWorkletProcessor {
|
|
|
1056
1064
|
this.engine.setTrackMonitorMode(laneIndex, mode as 0 | 1 | 2, sampleTime);
|
|
1057
1065
|
break;
|
|
1058
1066
|
}
|
|
1067
|
+
case SonareEngineCommandType.ResetMasterLoudnessMeter:
|
|
1068
|
+
this.engine.resetMasterLoudnessMeter(sampleTime);
|
|
1069
|
+
break;
|
|
1059
1070
|
default:
|
|
1060
1071
|
this.publishTelemetryRecord({
|
|
1061
1072
|
type: SonareEngineTelemetryType.Error,
|
|
@@ -1175,6 +1186,8 @@ export class SonareRealtimeEngineWorkletProcessor {
|
|
|
1175
1186
|
for (let field = 0; field < 11; field++) {
|
|
1176
1187
|
ring.records[offset + 3 + field] = this.engine.meterScratchValue(field);
|
|
1177
1188
|
}
|
|
1189
|
+
ring.records[offset + 14] = this.engine.meterScratchInputPeakDbL();
|
|
1190
|
+
ring.records[offset + 15] = this.engine.meterScratchInputPeakDbR();
|
|
1178
1191
|
Atomics.store(ring.header, 0, writeIndex + 1);
|
|
1179
1192
|
}
|
|
1180
1193
|
|
|
@@ -1192,16 +1205,53 @@ export class SonareRealtimeEngineWorkletProcessor {
|
|
|
1192
1205
|
// so header slot 3 is left at its initial 0.
|
|
1193
1206
|
|
|
1194
1207
|
// Drains the engine's scope producer (FFT spectrum + goniometer points) into
|
|
1195
|
-
// the lock-free SAB scope ring
|
|
1196
|
-
//
|
|
1208
|
+
// the lock-free SAB scope ring or the bounded postMessage fallback. The SAB
|
|
1209
|
+
// path remains allocation-free; postMessage materialises only records that
|
|
1210
|
+
// are ready to publish.
|
|
1197
1211
|
private publishScope(): void {
|
|
1198
|
-
|
|
1199
|
-
if (!ring) {
|
|
1212
|
+
if (!this.scopeEnabled) {
|
|
1200
1213
|
return;
|
|
1201
1214
|
}
|
|
1215
|
+
const ring = this.scopeRing;
|
|
1216
|
+
const transport = this.transport;
|
|
1202
1217
|
for (let count = 0; count < 64 && this.engine.popScopeTelemetryToScratch(); count++) {
|
|
1203
|
-
|
|
1218
|
+
if (ring) {
|
|
1219
|
+
this.writeScopeScratch(ring);
|
|
1220
|
+
} else {
|
|
1221
|
+
this.postScopeScratch(transport);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
private postScopeScratch(transport: WorkletTransport | undefined): void {
|
|
1227
|
+
if (!transport?.postMessage) {
|
|
1228
|
+
return;
|
|
1229
|
+
}
|
|
1230
|
+
const bandCount = Math.min(
|
|
1231
|
+
SONARE_SCOPE_MAX_BANDS,
|
|
1232
|
+
Math.max(0, Math.trunc(Number(this.engine.scopeScratchBandCount()))),
|
|
1233
|
+
);
|
|
1234
|
+
const pointCount = Math.min(
|
|
1235
|
+
SONARE_SCOPE_MAX_POINTS,
|
|
1236
|
+
Math.max(0, Math.trunc(Number(this.engine.scopeScratchPointCount()))),
|
|
1237
|
+
);
|
|
1238
|
+
const bands = new Float32Array(bandCount);
|
|
1239
|
+
for (let i = 0; i < bandCount; i++) {
|
|
1240
|
+
bands[i] = this.engine.scopeScratchBand(i);
|
|
1241
|
+
}
|
|
1242
|
+
const points = new Float32Array(pointCount * 2);
|
|
1243
|
+
for (let i = 0; i < pointCount; i++) {
|
|
1244
|
+
points[2 * i] = this.engine.scopeScratchPointLeft(i);
|
|
1245
|
+
points[2 * i + 1] = this.engine.scopeScratchPointRight(i);
|
|
1204
1246
|
}
|
|
1247
|
+
const scope: SonareWorkletScopeSnapshot = {
|
|
1248
|
+
type: 'scope',
|
|
1249
|
+
targetId: this.engine.scopeScratchTargetId(),
|
|
1250
|
+
frame: Number(this.engine.scopeScratchRenderFrame()),
|
|
1251
|
+
bands,
|
|
1252
|
+
points,
|
|
1253
|
+
};
|
|
1254
|
+
transport.postMessage(scope, [bands.buffer, points.buffer]);
|
|
1205
1255
|
}
|
|
1206
1256
|
|
|
1207
1257
|
private writeScopeScratch(ring: SharedScopeRingWriter): void {
|
|
@@ -2,12 +2,10 @@ import { panLawCode, panModeCode } from '../codes';
|
|
|
2
2
|
import type { EqBand, PanLawInput, PanMode, RealtimeEngine } from '../index';
|
|
3
3
|
import type { InsertParamOverrideMap } from './engine-mixer-facade';
|
|
4
4
|
import {
|
|
5
|
-
applyFullStripJson,
|
|
6
|
-
clearInsertParamOverrides,
|
|
7
5
|
emptyStripJson,
|
|
8
|
-
hasInsertParamOverrides,
|
|
9
6
|
type InsertParamOverride,
|
|
10
7
|
insertParamOverrideKey,
|
|
8
|
+
replaceStripScene,
|
|
11
9
|
type StripJsonTarget,
|
|
12
10
|
} from './engine-mixer-facade';
|
|
13
11
|
import type { SonareEngineInstrumentSyncMessage, SonareEngineSyncMessage } from './messages';
|
|
@@ -27,6 +25,7 @@ export interface EngineStripContext {
|
|
|
27
25
|
readStripJson(target: StripJsonTarget): string | undefined;
|
|
28
26
|
writeStripJson(target: StripJsonTarget, sceneJson: string): void;
|
|
29
27
|
readonly insertParamOverrides: InsertParamOverrideMap;
|
|
28
|
+
clearInsertAutomationLanes(target: StripJsonTarget): void;
|
|
30
29
|
}
|
|
31
30
|
|
|
32
31
|
function trackIdFor(ctx: EngineStripContext, target: string | number): number {
|
|
@@ -92,15 +91,11 @@ export function setTrackStripJson(
|
|
|
92
91
|
trackId: number,
|
|
93
92
|
sceneJson: string,
|
|
94
93
|
trackStripJson: Map<number, string>,
|
|
95
|
-
): {
|
|
94
|
+
): { insertBaseResets: StripJsonTarget[]; constructionOverrides: InsertParamOverride[] } {
|
|
96
95
|
const target: StripJsonTarget = { kind: 'track', trackId };
|
|
97
|
-
const
|
|
98
|
-
(trackStripJson.has(trackId) && trackStripJson.get(trackId) !== sceneJson) ||
|
|
99
|
-
hasInsertParamOverrides(ctx.insertParamOverrides, target);
|
|
100
|
-
applyFullStripJson(ctx.offlineEngine, target, sceneJson, resetInserts);
|
|
101
|
-
clearInsertParamOverrides(ctx.insertParamOverrides, target);
|
|
96
|
+
const result = replaceStripScene(ctx, target, trackStripJson.get(trackId), sceneJson);
|
|
102
97
|
trackStripJson.set(trackId, sceneJson);
|
|
103
|
-
return
|
|
98
|
+
return result;
|
|
104
99
|
}
|
|
105
100
|
|
|
106
101
|
export function setTrackStripEqBand(
|
|
@@ -11,25 +11,30 @@ import type { SonareEngineSyncTempoMessage } from './messages';
|
|
|
11
11
|
* Builds the ordered mixer-lane descriptors for a sync message.
|
|
12
12
|
*
|
|
13
13
|
* Each declared track id is paired with its current send list and output bus
|
|
14
|
-
* (both omitted when absent), defensively copying the
|
|
14
|
+
* (both omitted when absent) and source channel layout, defensively copying the
|
|
15
|
+
* send entries.
|
|
15
16
|
*
|
|
16
17
|
* @param trackLaneIds Lane order (track ids) declared on the engine.
|
|
17
18
|
* @param trackSends Per-track send lists.
|
|
18
19
|
* @param trackOutputBus Per-track output bus routing.
|
|
20
|
+
* @param trackSourceChannelLayout Per-track source channel layouts.
|
|
19
21
|
* @returns Lane descriptors in lane order.
|
|
20
22
|
*/
|
|
21
23
|
export function buildMixerLanes(
|
|
22
24
|
trackLaneIds: readonly number[],
|
|
23
25
|
trackSends: ReadonlyMap<number, EngineTrackSend[]>,
|
|
24
26
|
trackOutputBus: ReadonlyMap<number, number>,
|
|
27
|
+
trackSourceChannelLayout: ReadonlyMap<number, number>,
|
|
25
28
|
): EngineTrackLane[] {
|
|
26
29
|
return trackLaneIds.map((trackId) => {
|
|
27
30
|
const sends = trackSends.get(trackId);
|
|
28
31
|
const outputBusId = trackOutputBus.get(trackId);
|
|
32
|
+
const sourceChannelLayout = trackSourceChannelLayout.get(trackId);
|
|
29
33
|
return {
|
|
30
34
|
trackId,
|
|
31
35
|
...(sends && sends.length > 0 ? { sends: sends.map((send) => ({ ...send })) } : {}),
|
|
32
36
|
...(outputBusId !== undefined ? { outputBusId } : {}),
|
|
37
|
+
...(sourceChannelLayout !== undefined ? { sourceChannelLayout } : {}),
|
|
33
38
|
};
|
|
34
39
|
});
|
|
35
40
|
}
|
package/src/worklet/engine.ts
CHANGED
|
@@ -102,6 +102,7 @@ export class SonareEngine {
|
|
|
102
102
|
private readonly trackLaneIds: number[] = [];
|
|
103
103
|
private readonly trackSends = new Map<number, EngineTrackSend[]>();
|
|
104
104
|
private readonly trackOutputBus = new Map<number, number>();
|
|
105
|
+
private readonly trackSourceChannelLayout = new Map<number, number>();
|
|
105
106
|
private readonly laneSidechains = new Map<
|
|
106
107
|
string,
|
|
107
108
|
{ trackId: number; insertIndex: number; sourceTrackId: number }
|
|
@@ -242,6 +243,11 @@ export class SonareEngine {
|
|
|
242
243
|
tempo.setTimeSignatureSegments(this.tempoContext, segments);
|
|
243
244
|
}
|
|
244
245
|
|
|
246
|
+
/**
|
|
247
|
+
* A loop wrap, seek or stop sends note-offs plus CC64=0, CC121, CC123 and a
|
|
248
|
+
* centred pitch bend on every channel played since the last reset, so
|
|
249
|
+
* controller values set before a loop region are not restored at the wrap.
|
|
250
|
+
*/
|
|
245
251
|
setLoop(startPpq: number, endPpq: number, enabled = true): boolean {
|
|
246
252
|
return tempo.setLoop(this.tempoContext, startPpq, endPpq, enabled);
|
|
247
253
|
}
|
|
@@ -321,6 +327,15 @@ export class SonareEngine {
|
|
|
321
327
|
* mixer lane first (like automationParamId) so the offline engine resolves the
|
|
322
328
|
* same strip selector the realtime engine uses.
|
|
323
329
|
*
|
|
330
|
+
* The returned id uses the track's current positional lane selector. When
|
|
331
|
+
* setTrackLanes successfully changes lane order or membership, the engine
|
|
332
|
+
* remaps already queued and published track automation by track id, but it
|
|
333
|
+
* cannot update a numeric id retained by the caller. Re-resolve every track
|
|
334
|
+
* insert id after such a topology change before passing it to
|
|
335
|
+
* setAutomationLane. Use setTrackStripInsertParamByName when the operation
|
|
336
|
+
* needs a stable track identity. Master and bus insert ids are separate and
|
|
337
|
+
* are not invalidated by track-lane changes.
|
|
338
|
+
*
|
|
324
339
|
* @param target Track id (declares a mixer lane on first use).
|
|
325
340
|
* @param insertIndex Index into the strip's combined insert sequence.
|
|
326
341
|
* @param paramName Processor JSON-key parameter name.
|
|
@@ -454,14 +469,24 @@ export class SonareEngine {
|
|
|
454
469
|
return sent;
|
|
455
470
|
}
|
|
456
471
|
|
|
472
|
+
/** Resets the master's integrated loudness accumulator in both engine mirrors. */
|
|
473
|
+
resetMasterLoudnessMeter(renderFrame = -1): boolean {
|
|
474
|
+
this.offlineEngine.resetMasterLoudnessMeter(renderFrame);
|
|
475
|
+
return this.sendMirroredCommand({
|
|
476
|
+
type: SonareEngineCommandType.ResetMasterLoudnessMeter,
|
|
477
|
+
sampleTime: renderFrame,
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
|
|
457
481
|
/**
|
|
458
482
|
* Declares the mixer track lanes in an explicit order.
|
|
459
483
|
*
|
|
460
|
-
*
|
|
461
|
-
*
|
|
462
|
-
*
|
|
463
|
-
*
|
|
464
|
-
* track's send list; entries without `sends` leave
|
|
484
|
+
* A successful call may reorder existing track ids, remove track ids, or add
|
|
485
|
+
* new ones. Existing queued and published track automation follows the track
|
|
486
|
+
* id, while numeric track-insert automation ids retained by the caller do
|
|
487
|
+
* not; re-resolve those ids after every topology change. Entries carrying
|
|
488
|
+
* `sends` replace that track's send list; entries without `sends` leave
|
|
489
|
+
* existing sends untouched.
|
|
465
490
|
*
|
|
466
491
|
* @param lanes Track ids or lane descriptors in the desired lane order.
|
|
467
492
|
*/
|
|
@@ -528,19 +553,18 @@ export class SonareEngine {
|
|
|
528
553
|
}
|
|
529
554
|
|
|
530
555
|
setTrackStripJson(target: string | number, sceneJson: string): void {
|
|
556
|
+
// Parse before declaring a new lane so malformed text cannot declare one.
|
|
557
|
+
mixer.decodedInsertSignature(sceneJson, { kind: 'track', trackId: 0 });
|
|
531
558
|
const laneIndex = this.ensureTrackLane(target);
|
|
532
559
|
const trackId = this.trackLaneIds[laneIndex];
|
|
533
|
-
const {
|
|
560
|
+
const { insertBaseResets, constructionOverrides } = strips.setTrackStripJson(
|
|
534
561
|
this.stripContext,
|
|
535
562
|
trackId,
|
|
536
563
|
sceneJson,
|
|
537
564
|
this.trackStripJson,
|
|
538
565
|
);
|
|
539
|
-
if (resetInserts) {
|
|
540
|
-
this.clearInsertAutomationLanes({ kind: 'track', trackId });
|
|
541
|
-
}
|
|
542
566
|
mixer.pruneStripSidechains(this.mixerContext, { kind: 'track', trackId }, sceneJson);
|
|
543
|
-
this.syncMixer(
|
|
567
|
+
this.syncMixer(insertBaseResets, constructionOverrides);
|
|
544
568
|
}
|
|
545
569
|
|
|
546
570
|
setTrackStripEqBand(target: string | number, bandIndex: number, band: EqBand | string): void {
|
|
@@ -625,18 +649,7 @@ export class SonareEngine {
|
|
|
625
649
|
}
|
|
626
650
|
|
|
627
651
|
setMasterStripJson(sceneJson: string): void {
|
|
628
|
-
|
|
629
|
-
const resetInserts =
|
|
630
|
-
(this.masterStripJson !== undefined && this.masterStripJson !== sceneJson) ||
|
|
631
|
-
mixer.hasInsertParamOverrides(this.insertParamOverrides, target);
|
|
632
|
-
mixer.applyFullStripJson(this.offlineEngine, target, sceneJson, resetInserts);
|
|
633
|
-
if (resetInserts) {
|
|
634
|
-
this.clearInsertAutomationLanes(target);
|
|
635
|
-
}
|
|
636
|
-
mixer.pruneStripSidechains(this.mixerContext, target, sceneJson);
|
|
637
|
-
mixer.clearInsertParamOverrides(this.insertParamOverrides, target);
|
|
638
|
-
this.masterStripJson = sceneJson;
|
|
639
|
-
this.syncMixer(resetInserts ? [target] : []);
|
|
652
|
+
mixer.setMasterStripJson(this.mixerContext, sceneJson);
|
|
640
653
|
}
|
|
641
654
|
|
|
642
655
|
setMasterStripEqBand(bandIndex: number, band: EqBand | string): void {
|
|
@@ -904,7 +917,10 @@ export class SonareEngine {
|
|
|
904
917
|
/**
|
|
905
918
|
* Route a track's MIDI to the external output (drained via {@link onMidiOut})
|
|
906
919
|
* instead of an internal instrument, so the track plays an external device.
|
|
907
|
-
* Pass `external=false` to restore internal-synth playback.
|
|
920
|
+
* Pass `external=false` to restore internal-synth playback. The change takes
|
|
921
|
+
* effect at the next processed block; switching a destination's route first
|
|
922
|
+
* releases its notes and resets its controllers through the old route, and
|
|
923
|
+
* drops its pending MIDI-FX events.
|
|
908
924
|
*/
|
|
909
925
|
setMidiDestinationExternal(trackId: string | number, external: boolean): void {
|
|
910
926
|
strips.setMidiDestinationExternal(this.stripContext, trackId, external);
|
|
@@ -1397,8 +1413,11 @@ export class SonareEngine {
|
|
|
1397
1413
|
return mixer.mixerLanes(this.mixerContext);
|
|
1398
1414
|
}
|
|
1399
1415
|
|
|
1400
|
-
private syncMixer(
|
|
1401
|
-
mixer.
|
|
1416
|
+
private syncMixer(
|
|
1417
|
+
insertBaseResets: mixer.StripJsonTarget[] = [],
|
|
1418
|
+
oneShotInsertParamOverrides: mixer.InsertParamOverride[] = [],
|
|
1419
|
+
): void {
|
|
1420
|
+
mixer.syncMixer(this.mixerContext, false, insertBaseResets, oneShotInsertParamOverrides);
|
|
1402
1421
|
}
|
|
1403
1422
|
|
|
1404
1423
|
private insertAutomationTargetKey(target: mixer.StripJsonTarget): string {
|
|
@@ -1494,6 +1513,7 @@ export class SonareEngine {
|
|
|
1494
1513
|
trackLaneIds: this.trackLaneIds,
|
|
1495
1514
|
trackSends: this.trackSends,
|
|
1496
1515
|
trackOutputBus: this.trackOutputBus,
|
|
1516
|
+
trackSourceChannelLayout: this.trackSourceChannelLayout,
|
|
1497
1517
|
laneSidechains: this.laneSidechains,
|
|
1498
1518
|
busSidechains: this.busSidechains,
|
|
1499
1519
|
masterSidechains: this.masterSidechains,
|
|
@@ -1506,7 +1526,8 @@ export class SonareEngine {
|
|
|
1506
1526
|
ensureTrackLane: (target) => this.ensureTrackLane(target),
|
|
1507
1527
|
ensureBus: (busId) => this.ensureBus(busId),
|
|
1508
1528
|
mixerLanes: () => this.mixerLanes(),
|
|
1509
|
-
syncMixer: (
|
|
1529
|
+
syncMixer: (insertBaseResets, oneShotInsertParamOverrides) =>
|
|
1530
|
+
this.syncMixer(insertBaseResets, oneShotInsertParamOverrides),
|
|
1510
1531
|
clearInsertAutomationLanes: (target, forgetResolvedIds) =>
|
|
1511
1532
|
this.clearInsertAutomationLanes(target, forgetResolvedIds),
|
|
1512
1533
|
sendSmoothedParam: (paramId, value) => this.sendSmoothedParam(paramId, value),
|
|
@@ -1532,6 +1553,7 @@ export class SonareEngine {
|
|
|
1532
1553
|
writeStripJson: (target, sceneJson) =>
|
|
1533
1554
|
mixer.cacheStripJson(this.mixerContext, target, sceneJson),
|
|
1534
1555
|
insertParamOverrides: this.insertParamOverrides,
|
|
1556
|
+
clearInsertAutomationLanes: (target) => this.clearInsertAutomationLanes(target),
|
|
1535
1557
|
};
|
|
1536
1558
|
}
|
|
1537
1559
|
|
|
@@ -1683,17 +1705,7 @@ export class SonareEngine {
|
|
|
1683
1705
|
}
|
|
1684
1706
|
|
|
1685
1707
|
private ensureTrackLane(target: string | number): number {
|
|
1686
|
-
|
|
1687
|
-
if (!Number.isInteger(trackId) || trackId <= 0) {
|
|
1688
|
-
throw new RangeError(`Invalid track id for mixer lane: ${String(target)}`);
|
|
1689
|
-
}
|
|
1690
|
-
const existing = this.trackLaneIds.indexOf(trackId);
|
|
1691
|
-
if (existing >= 0) {
|
|
1692
|
-
return existing;
|
|
1693
|
-
}
|
|
1694
|
-
this.trackLaneIds.push(trackId);
|
|
1695
|
-
this.syncMixer();
|
|
1696
|
-
return this.trackLaneIds.length - 1;
|
|
1708
|
+
return mixer.ensureTrackLane(this.mixerContext, target);
|
|
1697
1709
|
}
|
|
1698
1710
|
|
|
1699
1711
|
private ensureBus(busId: number): number {
|
package/src/worklet/guards.ts
CHANGED
|
@@ -13,9 +13,11 @@ import type {
|
|
|
13
13
|
} from './messages';
|
|
14
14
|
import {
|
|
15
15
|
isRecord,
|
|
16
|
+
SONARE_SCOPE_DEFAULT_BANDS,
|
|
16
17
|
type SonareEngineCommandRecord,
|
|
17
18
|
type SonareEngineTelemetryRecord,
|
|
18
19
|
type SonareWorkletMeterSnapshot,
|
|
20
|
+
type SonareWorkletScopeSnapshot,
|
|
19
21
|
} from './protocol';
|
|
20
22
|
|
|
21
23
|
export function isWorkletMessage(value: unknown): value is SonareWorkletMessage {
|
|
@@ -331,10 +333,55 @@ export function isMeterSnapshot(value: unknown): value is SonareWorkletMeterSnap
|
|
|
331
333
|
typeof value.rmsDbL === 'number' &&
|
|
332
334
|
typeof value.rmsDbR === 'number' &&
|
|
333
335
|
typeof value.correlation === 'number' &&
|
|
336
|
+
Number.isFinite(value.inputPeakDbL) &&
|
|
337
|
+
Number.isFinite(value.inputPeakDbR) &&
|
|
334
338
|
(typeof value.targetId === 'number' || value.targetId === undefined)
|
|
335
339
|
);
|
|
336
340
|
}
|
|
337
341
|
|
|
342
|
+
export const SONARE_SCOPE_MAX_BANDS = 64;
|
|
343
|
+
export const SONARE_SCOPE_MAX_POINTS = 32;
|
|
344
|
+
|
|
345
|
+
function isPlainFloat32Array(
|
|
346
|
+
value: unknown,
|
|
347
|
+
maxLength: number,
|
|
348
|
+
evenLength = false,
|
|
349
|
+
): value is Float32Array {
|
|
350
|
+
if (
|
|
351
|
+
!(value instanceof Float32Array) ||
|
|
352
|
+
typeof ArrayBuffer === 'undefined' ||
|
|
353
|
+
!(value.buffer instanceof ArrayBuffer) ||
|
|
354
|
+
value.length > maxLength ||
|
|
355
|
+
(evenLength && value.length % 2 !== 0)
|
|
356
|
+
) {
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
for (const sample of value) {
|
|
360
|
+
if (!Number.isFinite(sample)) {
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return true;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** Validates the bounded postMessage representation of a scope snapshot. */
|
|
368
|
+
export function isScopeSnapshot(value: unknown): value is SonareWorkletScopeSnapshot {
|
|
369
|
+
return (
|
|
370
|
+
isRecord(value) &&
|
|
371
|
+
value.type === 'scope' &&
|
|
372
|
+
typeof value.targetId === 'number' &&
|
|
373
|
+
Number.isSafeInteger(value.targetId) &&
|
|
374
|
+
value.targetId >= 0 &&
|
|
375
|
+
value.targetId <= 0xffff_ffff &&
|
|
376
|
+
typeof value.frame === 'number' &&
|
|
377
|
+
Number.isSafeInteger(value.frame) &&
|
|
378
|
+
value.frame >= 0 &&
|
|
379
|
+
isPlainFloat32Array(value.bands, SONARE_SCOPE_MAX_BANDS) &&
|
|
380
|
+
value.bands.length > 0 &&
|
|
381
|
+
isPlainFloat32Array(value.points, SONARE_SCOPE_MAX_POINTS * 2, true)
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
338
385
|
/**
|
|
339
386
|
* Resolves an integer option, refusing anything the field's own domain cannot
|
|
340
387
|
* hold instead of rounding it into range.
|
|
@@ -356,6 +403,14 @@ export function requireIntegerOption(
|
|
|
356
403
|
return resolved;
|
|
357
404
|
}
|
|
358
405
|
|
|
406
|
+
/** Resolves scope bands and applies the native tap's fixed 64-band ceiling. */
|
|
407
|
+
export function resolveScopeBandCount(
|
|
408
|
+
value: number | undefined,
|
|
409
|
+
fallback = SONARE_SCOPE_DEFAULT_BANDS,
|
|
410
|
+
): number {
|
|
411
|
+
return Math.min(requireIntegerOption(value, fallback, 'scopeBands', 1), SONARE_SCOPE_MAX_BANDS);
|
|
412
|
+
}
|
|
413
|
+
|
|
359
414
|
/** As {@link requireIntegerOption}, for a field whose domain admits either sign. */
|
|
360
415
|
export function requireInteger(value: number | undefined, fallback: number, name: string): number {
|
|
361
416
|
const resolved = value ?? fallback;
|
package/src/worklet/messages.ts
CHANGED
|
@@ -21,6 +21,7 @@ import type {
|
|
|
21
21
|
SonareEngineCommandRecord,
|
|
22
22
|
SonareEngineTelemetryRecord,
|
|
23
23
|
SonareWorkletMeterSnapshot,
|
|
24
|
+
SonareWorkletScopeSnapshot,
|
|
24
25
|
SonareWorkletSpectrumSnapshot,
|
|
25
26
|
} from './protocol';
|
|
26
27
|
|
|
@@ -288,6 +289,7 @@ export interface SonareEngineSyncErrorMessage {
|
|
|
288
289
|
|
|
289
290
|
export type SonareWorkletTransportMessage =
|
|
290
291
|
| SonareWorkletMeterSnapshot
|
|
292
|
+
| SonareWorkletScopeSnapshot
|
|
291
293
|
| SonareWorkletSpectrumSnapshot
|
|
292
294
|
| SonareWorkletExternalMidiMessage
|
|
293
295
|
| SonareEngineClipPageRequestMessage
|
|
@@ -511,8 +513,8 @@ export interface SonareEngineSyncMixerMessage {
|
|
|
511
513
|
masterStripJson?: string;
|
|
512
514
|
/** Retained by-name insert values, applied after strip and sidechain replay. */
|
|
513
515
|
insertParamOverrides?: SonareEngineSyncMixerInsertParamOverride[];
|
|
514
|
-
/**
|
|
515
|
-
|
|
516
|
+
/** Strips whose manual insert bases are cleared after the strip replay, before insertParamOverrides. */
|
|
517
|
+
insertBaseResets?: Array<
|
|
516
518
|
{ kind: 'track'; trackId: number } | { kind: 'bus'; busId: number } | { kind: 'master' }
|
|
517
519
|
>;
|
|
518
520
|
/** Lane insert sidechain bindings (replayed after lanes/strips). */
|