@libraz/libsonare 1.5.5 → 1.6.0
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 +109 -20
- package/dist/analysis.d.ts +5730 -0
- package/dist/analysis.js +2464 -0
- package/dist/analysis.js.map +1 -0
- package/dist/index.d.ts +759 -35
- package/dist/index.js +904 -209
- package/dist/index.js.map +1 -1
- package/dist/schemas/realtime-voice-changer-preset-pack.schema.json +27 -0
- package/dist/schemas/realtime-voice-changer-preset.schema.json +140 -0
- package/dist/sonare-analysis.js +2 -0
- package/dist/sonare-analysis.wasm +0 -0
- package/dist/sonare.js +1 -1
- package/dist/sonare.wasm +0 -0
- package/dist/worker.d.ts +65 -0
- package/dist/worker.js +707 -0
- package/dist/worker.js.map +1 -0
- package/dist/worklet.d.ts +574 -86
- package/dist/worklet.js +1634 -235
- package/dist/worklet.js.map +1 -1
- package/package.json +23 -11
- package/src/_chain_config.ts +5 -3
- package/src/analysis.ts +137 -0
- package/src/analysis_helpers.ts +6 -0
- package/src/clip_page_streamer.ts +62 -8
- package/src/codes.ts +51 -64
- package/src/effects_mastering.ts +1 -0
- package/src/errors.ts +1 -0
- package/src/feature_core.ts +140 -2
- package/src/feature_music.ts +17 -2
- package/src/feature_pitch.ts +70 -1
- package/src/feature_spectral.ts +231 -5
- package/src/feature_spectrogram.ts +159 -0
- package/src/features.ts +16 -1
- package/src/index.ts +101 -7
- package/src/mastering_chain.ts +20 -8
- package/src/mastering_core.ts +5 -0
- package/src/mastering_dynamics.ts +7 -1
- package/src/metering.ts +6 -1
- package/src/opfs_clip_pages.ts +26 -1
- package/src/project.ts +7 -0
- package/src/project_class.ts +62 -8
- package/src/project_internal.ts +24 -29
- package/src/project_types.ts +69 -10
- package/src/public_types.ts +68 -0
- package/src/public_types_mastering.ts +22 -0
- package/src/public_types_mixing.ts +11 -2
- package/src/public_types_music.ts +9 -0
- package/src/public_types_spectral.ts +33 -0
- package/src/quick_analysis.ts +25 -7
- package/src/realtime_engine.ts +153 -3
- package/src/realtime_voice_changer.ts +51 -77
- package/src/sonare-analysis.js.d.ts +8 -0
- package/src/sonare.js.d.ts +276 -6
- package/src/streaming_processors.ts +15 -0
- package/src/web_midi.ts +36 -4
- package/src/worker.ts +196 -0
- package/src/worker_client.ts +352 -0
- package/src/worker_protocol.ts +59 -0
- package/src/worklet/engine-automation.ts +2 -1
- package/src/worklet/engine-capture-facade.ts +13 -4
- package/src/worklet/engine-clips.ts +12 -3
- package/src/worklet/engine-node.ts +212 -4
- package/src/worklet/engine-offline.ts +7 -2
- package/src/worklet/engine-parameter-facade.ts +26 -5
- package/src/worklet/engine-processor.ts +383 -42
- package/src/worklet/engine-register.ts +8 -0
- package/src/worklet/engine-strips.ts +17 -1
- package/src/worklet/engine-sync.ts +2 -6
- package/src/worklet/engine-tempo-facade.ts +8 -6
- package/src/worklet/engine.ts +380 -13
- package/src/worklet/guards.ts +22 -1
- package/src/worklet/messages.ts +125 -7
- package/src/worklet/protocol.ts +301 -17
- package/src/worklet/voice-changer-processor.ts +12 -15
- package/src/worklet.ts +20 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { EngineClip
|
|
1
|
+
import type { EngineClip } from '../index';
|
|
2
2
|
import { RealtimeEngine } from '../index';
|
|
3
3
|
import type { WorkletInput, WorkletOutput } from './audio_types';
|
|
4
4
|
import {
|
|
@@ -14,12 +14,18 @@ import {
|
|
|
14
14
|
type WorkletTransport,
|
|
15
15
|
} from './messages';
|
|
16
16
|
import {
|
|
17
|
+
clipPageRequestRingFromSharedBuffer,
|
|
17
18
|
encodeFrameHi,
|
|
18
19
|
encodeFrameLo,
|
|
19
20
|
engineRingFromSharedBuffer,
|
|
21
|
+
externalMidiRingFromSharedBuffer,
|
|
20
22
|
meterFromEngine,
|
|
21
23
|
meterRingFromSharedBuffer,
|
|
22
24
|
popSonareEngineCommandRingBuffer,
|
|
25
|
+
pushSonareClipPageRequestRingBuffer,
|
|
26
|
+
pushSonareExternalMidiRingBuffer,
|
|
27
|
+
type SharedClipPageRequestRingWriter,
|
|
28
|
+
type SharedExternalMidiRingWriter,
|
|
23
29
|
type SharedMeterRingWriter,
|
|
24
30
|
type SharedScopeRingWriter,
|
|
25
31
|
SONARE_ENGINE_COMMAND_RECORD_BYTES,
|
|
@@ -36,6 +42,7 @@ import {
|
|
|
36
42
|
type SonareWorkletMeterSnapshot,
|
|
37
43
|
scopeRingFromSharedBuffer,
|
|
38
44
|
telemetryFromEngine,
|
|
45
|
+
writeInt64Words,
|
|
39
46
|
writeSonareEngineTelemetryRingBuffer,
|
|
40
47
|
} from './protocol';
|
|
41
48
|
|
|
@@ -55,6 +62,8 @@ export class SonareRealtimeEngineWorkletProcessor {
|
|
|
55
62
|
private telemetryRing?: SonareEngineTelemetryRingBuffer;
|
|
56
63
|
private meterRing?: SharedMeterRingWriter;
|
|
57
64
|
private scopeRing?: SharedScopeRingWriter;
|
|
65
|
+
private clipPageRequestRing?: SharedClipPageRequestRingWriter;
|
|
66
|
+
private externalMidiRing?: SharedExternalMidiRingWriter;
|
|
58
67
|
private transport?: WorkletTransport;
|
|
59
68
|
private meterIntervalFrames: number;
|
|
60
69
|
private lastMeterFrame = Number.NEGATIVE_INFINITY;
|
|
@@ -71,7 +80,15 @@ export class SonareRealtimeEngineWorkletProcessor {
|
|
|
71
80
|
private channelBuffers: Float32Array[];
|
|
72
81
|
private readonly liveClips = new Map<number, EngineClip>();
|
|
73
82
|
private readonly pagedClipProviders = new Map<number, number>();
|
|
83
|
+
private readonly pagedClipPageFrames = new Map<number, number>();
|
|
74
84
|
private readonly pendingPagedClips = new Map<number, EngineClip>();
|
|
85
|
+
// The worklet drains at most this many distinct page misses per render
|
|
86
|
+
// quantum. The fixed scalar buffers avoid retaining an unbounded queue on
|
|
87
|
+
// the audio thread; any remaining native requests stay queued for the next
|
|
88
|
+
// quantum and render silence meanwhile.
|
|
89
|
+
private readonly clipPageRequestClipIds = new Float64Array(64);
|
|
90
|
+
private readonly clipPageRequestPageIndices = new Float64Array(64);
|
|
91
|
+
private clipPageRequestOverflowReported = 0;
|
|
75
92
|
|
|
76
93
|
constructor(
|
|
77
94
|
options: SonareRealtimeEngineWorkletProcessorOptions = {},
|
|
@@ -101,7 +118,25 @@ export class SonareRealtimeEngineWorkletProcessor {
|
|
|
101
118
|
options.scopeBands,
|
|
102
119
|
)
|
|
103
120
|
: undefined;
|
|
104
|
-
this.
|
|
121
|
+
this.clipPageRequestRing = options.clipPageRequestSharedBuffer
|
|
122
|
+
? clipPageRequestRingFromSharedBuffer(
|
|
123
|
+
options.clipPageRequestSharedBuffer,
|
|
124
|
+
options.clipPageRequestRingCapacity,
|
|
125
|
+
)
|
|
126
|
+
: undefined;
|
|
127
|
+
this.externalMidiRing = options.externalMidiSharedBuffer
|
|
128
|
+
? externalMidiRingFromSharedBuffer(
|
|
129
|
+
options.externalMidiSharedBuffer,
|
|
130
|
+
options.externalMidiRingCapacity,
|
|
131
|
+
)
|
|
132
|
+
: undefined;
|
|
133
|
+
this.engine = new RealtimeEngine(
|
|
134
|
+
this.sampleRate,
|
|
135
|
+
this.blockSize,
|
|
136
|
+
1024,
|
|
137
|
+
1024,
|
|
138
|
+
this.channelCount,
|
|
139
|
+
);
|
|
105
140
|
// Allocate persistent WASM-heap scratch (worst case: channelCount channels x
|
|
106
141
|
// blockSize frames) and acquire the per-channel heap views once.
|
|
107
142
|
this.engine.prepareChannels(this.channelCount, this.blockSize);
|
|
@@ -191,6 +226,7 @@ export class SonareRealtimeEngineWorkletProcessor {
|
|
|
191
226
|
target.fill(0);
|
|
192
227
|
}
|
|
193
228
|
}
|
|
229
|
+
this.publishClipPageRequests();
|
|
194
230
|
this.publishTelemetry();
|
|
195
231
|
this.publishMeters();
|
|
196
232
|
this.publishScope();
|
|
@@ -206,7 +242,7 @@ export class SonareRealtimeEngineWorkletProcessor {
|
|
|
206
242
|
|
|
207
243
|
receiveCommand(command: SonareEngineCommandRecord): void {
|
|
208
244
|
if (!this.closed) {
|
|
209
|
-
this.
|
|
245
|
+
this.safeApplyCommand(command);
|
|
210
246
|
}
|
|
211
247
|
}
|
|
212
248
|
|
|
@@ -218,35 +254,68 @@ export class SonareRealtimeEngineWorkletProcessor {
|
|
|
218
254
|
if (this.closed) {
|
|
219
255
|
return;
|
|
220
256
|
}
|
|
257
|
+
try {
|
|
258
|
+
this.applySync(message);
|
|
259
|
+
} catch (error) {
|
|
260
|
+
this.transport?.postMessage?.({
|
|
261
|
+
type: 'syncError',
|
|
262
|
+
syncType: message.type,
|
|
263
|
+
message: error instanceof Error ? error.message : String(error),
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
private applySync(message: SonareEngineSyncMessage): void {
|
|
221
269
|
switch (message.type) {
|
|
222
|
-
case '
|
|
270
|
+
case 'destroy':
|
|
271
|
+
this.destroy();
|
|
272
|
+
return;
|
|
273
|
+
case 'syncClips': {
|
|
274
|
+
// Commit native state before touching the worklet-side mirrors. A
|
|
275
|
+
// rejected embind call must leave a later delta based on the previous
|
|
276
|
+
// clip set, not on a half-applied replacement.
|
|
277
|
+
this.engine.setClips(message.clips);
|
|
278
|
+
this.clearPagedClipProviders();
|
|
223
279
|
this.liveClips.clear();
|
|
224
280
|
for (const clip of message.clips) {
|
|
225
281
|
if (clip.id !== undefined) {
|
|
226
282
|
this.liveClips.set(clip.id, clip);
|
|
227
283
|
}
|
|
228
284
|
}
|
|
229
|
-
this.engine.setClips(message.clips);
|
|
230
285
|
break;
|
|
231
|
-
|
|
286
|
+
}
|
|
287
|
+
case 'syncClipsDelta': {
|
|
288
|
+
const nextClips = new Map(this.liveClips);
|
|
232
289
|
for (const clipId of message.removeIds) {
|
|
233
|
-
|
|
290
|
+
nextClips.delete(clipId);
|
|
234
291
|
}
|
|
235
292
|
for (const clip of message.upserts) {
|
|
236
293
|
if (clip.id !== undefined) {
|
|
237
|
-
|
|
294
|
+
nextClips.set(clip.id, clip);
|
|
238
295
|
}
|
|
239
296
|
}
|
|
240
|
-
this.engine.setClips(Array.from(
|
|
297
|
+
this.engine.setClips(Array.from(nextClips.values()));
|
|
298
|
+
for (const clipId of message.removeIds) {
|
|
299
|
+
this.removePagedClipProvider(clipId);
|
|
300
|
+
}
|
|
301
|
+
this.liveClips.clear();
|
|
302
|
+
for (const [clipId, clip] of nextClips) {
|
|
303
|
+
this.liveClips.set(clipId, clip);
|
|
304
|
+
}
|
|
241
305
|
break;
|
|
306
|
+
}
|
|
242
307
|
case 'syncClipPageProvider': {
|
|
243
308
|
const provider = this.engine.createClipPageProvider(
|
|
244
309
|
message.numChannels,
|
|
245
310
|
message.numSamples,
|
|
246
311
|
message.pageFrames,
|
|
247
312
|
);
|
|
313
|
+
this.removePagedClipProvider(message.clipId);
|
|
248
314
|
this.pagedClipProviders.set(message.clipId, provider.id);
|
|
249
|
-
this.
|
|
315
|
+
this.pagedClipPageFrames.set(message.clipId, message.pageFrames);
|
|
316
|
+
if (message.clip) {
|
|
317
|
+
this.pendingPagedClips.set(message.clipId, message.clip);
|
|
318
|
+
}
|
|
250
319
|
break;
|
|
251
320
|
}
|
|
252
321
|
case 'syncClipPage': {
|
|
@@ -256,16 +325,34 @@ export class SonareRealtimeEngineWorkletProcessor {
|
|
|
256
325
|
}
|
|
257
326
|
break;
|
|
258
327
|
}
|
|
328
|
+
case 'syncClipPageClear': {
|
|
329
|
+
const providerId = this.pagedClipProviders.get(message.clipId);
|
|
330
|
+
if (providerId !== undefined) {
|
|
331
|
+
this.engine.clearClipPage(providerId, message.pageIndex);
|
|
332
|
+
}
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
259
335
|
case 'syncClipPageCommit': {
|
|
260
336
|
const providerId = this.pagedClipProviders.get(message.clipId);
|
|
261
|
-
const clip = this.pendingPagedClips.get(message.clipId);
|
|
337
|
+
const clip = message.clip ?? this.pendingPagedClips.get(message.clipId);
|
|
262
338
|
if (providerId !== undefined && clip) {
|
|
263
|
-
|
|
339
|
+
const nextClip = { ...clip, pageProvider: providerId };
|
|
340
|
+
const nextClips = new Map(this.liveClips);
|
|
341
|
+
nextClips.set(message.clipId, nextClip);
|
|
342
|
+
this.engine.setClips(Array.from(nextClips.values()));
|
|
343
|
+
this.liveClips.set(message.clipId, nextClip);
|
|
264
344
|
this.pendingPagedClips.delete(message.clipId);
|
|
265
|
-
this.engine.setClips(Array.from(this.liveClips.values()));
|
|
266
345
|
}
|
|
267
346
|
break;
|
|
268
347
|
}
|
|
348
|
+
case 'syncClipPageDestroy': {
|
|
349
|
+
const nextClips = new Map(this.liveClips);
|
|
350
|
+
nextClips.delete(message.clipId);
|
|
351
|
+
this.engine.setClips(Array.from(nextClips.values()));
|
|
352
|
+
this.liveClips.delete(message.clipId);
|
|
353
|
+
this.removePagedClipProvider(message.clipId);
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
269
356
|
case 'syncMidiClips':
|
|
270
357
|
this.engine.setMidiClips(message.clips);
|
|
271
358
|
break;
|
|
@@ -273,12 +360,23 @@ export class SonareRealtimeEngineWorkletProcessor {
|
|
|
273
360
|
this.engine.setMarkers(message.markers);
|
|
274
361
|
break;
|
|
275
362
|
case 'syncMetronome':
|
|
276
|
-
|
|
277
|
-
|
|
363
|
+
// Do not publish the cached config until the native engine accepted it.
|
|
364
|
+
// Otherwise a rejected sync would corrupt the command-side fallback.
|
|
365
|
+
{
|
|
366
|
+
const config = resolveMetronomeConfig(message.config);
|
|
367
|
+
this.engine.setMetronome(message.config);
|
|
368
|
+
this.metronomeConfig = config;
|
|
369
|
+
}
|
|
278
370
|
break;
|
|
279
371
|
case 'syncAutomation':
|
|
280
372
|
this.engine.setAutomationLane(message.paramId, message.points);
|
|
281
373
|
break;
|
|
374
|
+
case 'syncParameters':
|
|
375
|
+
this.engine.clearParameters();
|
|
376
|
+
for (const info of message.parameters) {
|
|
377
|
+
this.engine.addParameter(info);
|
|
378
|
+
}
|
|
379
|
+
break;
|
|
282
380
|
case 'syncTempo':
|
|
283
381
|
if (message.tempoSegments) {
|
|
284
382
|
this.engine.setTempoSegments(message.tempoSegments);
|
|
@@ -425,6 +523,9 @@ export class SonareRealtimeEngineWorkletProcessor {
|
|
|
425
523
|
message.renderFrame,
|
|
426
524
|
);
|
|
427
525
|
break;
|
|
526
|
+
case 'syncMidiUmp':
|
|
527
|
+
this.engine.pushMidiUmp(message.destinationId, message.word0, message.renderFrame);
|
|
528
|
+
break;
|
|
428
529
|
case 'syncMidiSysex':
|
|
429
530
|
this.engine.pushMidiSysex(message.destinationId, message.data, message.renderFrame);
|
|
430
531
|
break;
|
|
@@ -437,6 +538,45 @@ export class SonareRealtimeEngineWorkletProcessor {
|
|
|
437
538
|
case 'syncExternalMidiClock':
|
|
438
539
|
this.engine.setExternalMidiClockEnabled(message.enabled);
|
|
439
540
|
break;
|
|
541
|
+
case 'syncMidiInputSource':
|
|
542
|
+
this.engine.setMidiInputSource(message.destinationId ?? 0);
|
|
543
|
+
break;
|
|
544
|
+
case 'syncClearMidiInputSource':
|
|
545
|
+
this.engine.clearMidiInputSource();
|
|
546
|
+
break;
|
|
547
|
+
case 'syncMidiCcBinding':
|
|
548
|
+
this.engine.bindMidiCc(message.channel, message.controller, message.paramId, {
|
|
549
|
+
minValue: message.minValue,
|
|
550
|
+
maxValue: message.maxValue,
|
|
551
|
+
});
|
|
552
|
+
break;
|
|
553
|
+
case 'syncMidiInputNoteOn':
|
|
554
|
+
this.engine.pushMidiInputNoteOn(
|
|
555
|
+
message.group,
|
|
556
|
+
message.channel,
|
|
557
|
+
message.data0,
|
|
558
|
+
message.data1,
|
|
559
|
+
message.portTimeSamples,
|
|
560
|
+
);
|
|
561
|
+
break;
|
|
562
|
+
case 'syncMidiInputNoteOff':
|
|
563
|
+
this.engine.pushMidiInputNoteOff(
|
|
564
|
+
message.group,
|
|
565
|
+
message.channel,
|
|
566
|
+
message.data0,
|
|
567
|
+
message.data1,
|
|
568
|
+
message.portTimeSamples,
|
|
569
|
+
);
|
|
570
|
+
break;
|
|
571
|
+
case 'syncMidiInputCc':
|
|
572
|
+
this.engine.pushMidiInputCc(
|
|
573
|
+
message.group,
|
|
574
|
+
message.channel,
|
|
575
|
+
message.data0,
|
|
576
|
+
message.data1,
|
|
577
|
+
message.portTimeSamples,
|
|
578
|
+
);
|
|
579
|
+
break;
|
|
440
580
|
}
|
|
441
581
|
}
|
|
442
582
|
|
|
@@ -520,6 +660,7 @@ export class SonareRealtimeEngineWorkletProcessor {
|
|
|
520
660
|
|
|
521
661
|
destroy(): void {
|
|
522
662
|
if (!this.closed) {
|
|
663
|
+
this.clearPagedClipProviders();
|
|
523
664
|
this.engine.destroy();
|
|
524
665
|
this.closed = true;
|
|
525
666
|
}
|
|
@@ -534,7 +675,25 @@ export class SonareRealtimeEngineWorkletProcessor {
|
|
|
534
675
|
if (!command) {
|
|
535
676
|
return;
|
|
536
677
|
}
|
|
678
|
+
this.safeApplyCommand(command);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
private safeApplyCommand(command: SonareEngineCommandRecord): void {
|
|
683
|
+
try {
|
|
537
684
|
this.applyCommand(command);
|
|
685
|
+
} catch {
|
|
686
|
+
// A malformed control command must never escape AudioWorklet.process():
|
|
687
|
+
// Web Audio permanently stops invoking a processor that throws there.
|
|
688
|
+
this.publishTelemetryRecord({
|
|
689
|
+
type: SonareEngineTelemetryType.Error,
|
|
690
|
+
error: SonareEngineTelemetryError.InvalidCommand,
|
|
691
|
+
renderFrame: 0,
|
|
692
|
+
timelineSample: 0,
|
|
693
|
+
audibleTimelineSample: 0,
|
|
694
|
+
graphLatencySamplesQ8: 0,
|
|
695
|
+
value: Number(command.type),
|
|
696
|
+
});
|
|
538
697
|
}
|
|
539
698
|
}
|
|
540
699
|
|
|
@@ -569,9 +728,6 @@ export class SonareRealtimeEngineWorkletProcessor {
|
|
|
569
728
|
case SonareEngineCommandType.TransportSeekPpq:
|
|
570
729
|
this.engine.seekPpq(Number(command.argFloat ?? 0), sampleTime);
|
|
571
730
|
break;
|
|
572
|
-
case SonareEngineCommandType.SetTempoMap:
|
|
573
|
-
this.engine.setTempo(Number(command.argFloat ?? 120));
|
|
574
|
-
break;
|
|
575
731
|
case SonareEngineCommandType.SetLoop:
|
|
576
732
|
this.engine.setLoop(
|
|
577
733
|
Number(command.argFloat ?? 0),
|
|
@@ -601,6 +757,7 @@ export class SonareRealtimeEngineWorkletProcessor {
|
|
|
601
757
|
beatGain: this.metronomeConfig.beatGain,
|
|
602
758
|
accentGain: this.metronomeConfig.accentGain,
|
|
603
759
|
clickSamples: this.metronomeConfig.clickSamples,
|
|
760
|
+
clickSeconds: this.metronomeConfig.clickSeconds,
|
|
604
761
|
});
|
|
605
762
|
break;
|
|
606
763
|
case SonareEngineCommandType.SeekMarker:
|
|
@@ -631,11 +788,36 @@ export class SonareRealtimeEngineWorkletProcessor {
|
|
|
631
788
|
}
|
|
632
789
|
|
|
633
790
|
private publishTelemetry(): void {
|
|
791
|
+
const ring = this.telemetryRing;
|
|
792
|
+
if (ring) {
|
|
793
|
+
for (let count = 0; count < 64 && this.engine.popTelemetryToScratch(); count++) {
|
|
794
|
+
this.writeTelemetryScratch(ring);
|
|
795
|
+
}
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
634
798
|
for (const item of this.engine.drainTelemetry(64)) {
|
|
635
799
|
this.publishTelemetryRecord(telemetryFromEngine(item));
|
|
636
800
|
}
|
|
637
801
|
}
|
|
638
802
|
|
|
803
|
+
private writeTelemetryScratch(ring: SonareEngineTelemetryRingBuffer): void {
|
|
804
|
+
const writeIndex = Atomics.load(ring.header, 0);
|
|
805
|
+
const offset = (writeIndex % ring.capacity) * SONARE_ENGINE_TELEMETRY_RECORD_BYTES;
|
|
806
|
+
ring.view.setUint32(offset, this.engine.telemetryScratchType(), true);
|
|
807
|
+
ring.view.setUint32(offset + 4, this.engine.telemetryScratchError(), true);
|
|
808
|
+
writeInt64Words(ring.view, offset + 8, this.engine.telemetryScratchRenderFrame());
|
|
809
|
+
writeInt64Words(ring.view, offset + 16, this.engine.telemetryScratchTimelineSample());
|
|
810
|
+
writeInt64Words(ring.view, offset + 24, this.engine.telemetryScratchAudibleTimelineSample());
|
|
811
|
+
ring.view.setInt32(offset + 32, this.engine.telemetryScratchGraphLatencySamplesQ8(), true);
|
|
812
|
+
ring.view.setUint32(offset + 36, this.engine.telemetryScratchValue(), true);
|
|
813
|
+
ring.view.setUint32(offset + 40, 0, true);
|
|
814
|
+
ring.view.setUint32(offset + 44, 0, true);
|
|
815
|
+
Atomics.store(ring.header, 0, writeIndex + 1);
|
|
816
|
+
if (writeIndex + 1 > ring.capacity) {
|
|
817
|
+
Atomics.store(ring.header, 4, writeIndex + 1 - ring.capacity);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
639
821
|
private publishTelemetryRecord(record: SonareEngineTelemetryRecord): void {
|
|
640
822
|
if (this.telemetryRing) {
|
|
641
823
|
writeSonareEngineTelemetryRingBuffer(this.telemetryRing, record);
|
|
@@ -659,6 +841,22 @@ export class SonareRealtimeEngineWorkletProcessor {
|
|
|
659
841
|
if (this.meterIntervalFrames <= 0 || (!this.transport && !this.meterRing)) {
|
|
660
842
|
return;
|
|
661
843
|
}
|
|
844
|
+
if (this.meterRing) {
|
|
845
|
+
for (let count = 0; count < 64 && this.engine.popMeterTelemetryToScratch(); count++) {
|
|
846
|
+
const frame = this.engine.meterScratchRenderFrame();
|
|
847
|
+
if (
|
|
848
|
+
frame !== this.lastMeterFrame &&
|
|
849
|
+
frame - this.lastMeterFrame < this.meterIntervalFrames
|
|
850
|
+
) {
|
|
851
|
+
continue;
|
|
852
|
+
}
|
|
853
|
+
if (frame !== this.lastMeterFrame) {
|
|
854
|
+
this.lastMeterFrame = frame;
|
|
855
|
+
}
|
|
856
|
+
this.writeMeterScratch(this.meterRing);
|
|
857
|
+
}
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
662
860
|
for (const item of this.engine.drainMeterTelemetry(64)) {
|
|
663
861
|
const meter = meterFromEngine(item);
|
|
664
862
|
if (
|
|
@@ -683,6 +881,19 @@ export class SonareRealtimeEngineWorkletProcessor {
|
|
|
683
881
|
}
|
|
684
882
|
}
|
|
685
883
|
|
|
884
|
+
private writeMeterScratch(ring: SharedMeterRingWriter): void {
|
|
885
|
+
const writeIndex = Atomics.load(ring.header, 0);
|
|
886
|
+
const offset = (writeIndex % ring.capacity) * SONARE_METER_RING_RECORD_FLOATS;
|
|
887
|
+
const frame = Number(this.engine.meterScratchRenderFrame());
|
|
888
|
+
ring.records[offset] = encodeFrameLo(frame);
|
|
889
|
+
ring.records[offset + 1] = encodeFrameHi(frame);
|
|
890
|
+
ring.records[offset + 2] = this.engine.meterScratchTargetId();
|
|
891
|
+
for (let field = 0; field < 11; field++) {
|
|
892
|
+
ring.records[offset + 3 + field] = this.engine.meterScratchValue(field);
|
|
893
|
+
}
|
|
894
|
+
Atomics.store(ring.header, 0, writeIndex + 1);
|
|
895
|
+
}
|
|
896
|
+
|
|
686
897
|
private writeMeterRing(meter: SonareWorkletMeterSnapshot): void {
|
|
687
898
|
const ring = this.meterRing;
|
|
688
899
|
if (!ring) {
|
|
@@ -720,9 +931,32 @@ export class SonareRealtimeEngineWorkletProcessor {
|
|
|
720
931
|
if (!ring) {
|
|
721
932
|
return;
|
|
722
933
|
}
|
|
723
|
-
for (
|
|
724
|
-
this.
|
|
934
|
+
for (let count = 0; count < 64 && this.engine.popScopeTelemetryToScratch(); count++) {
|
|
935
|
+
this.writeScopeScratch(ring);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
private writeScopeScratch(ring: SharedScopeRingWriter): void {
|
|
940
|
+
const writeIndex = Atomics.load(ring.header, 0);
|
|
941
|
+
const base = (writeIndex % ring.capacity) * ring.recordFloats;
|
|
942
|
+
const frame = Number(this.engine.scopeScratchRenderFrame());
|
|
943
|
+
ring.records[base] = encodeFrameLo(frame);
|
|
944
|
+
ring.records[base + 1] = encodeFrameHi(frame);
|
|
945
|
+
ring.records[base + 2] = this.engine.scopeScratchTargetId();
|
|
946
|
+
const bandCount = Math.min(ring.bands, this.engine.scopeScratchBandCount());
|
|
947
|
+
ring.records[base + 3] = bandCount;
|
|
948
|
+
const pointCount = Math.min(ring.maxPoints, this.engine.scopeScratchPointCount());
|
|
949
|
+
ring.records[base + 4] = pointCount;
|
|
950
|
+
const bandsBase = base + SONARE_SCOPE_RING_RECORD_PREFIX_FLOATS;
|
|
951
|
+
for (let i = 0; i < bandCount; i++) {
|
|
952
|
+
ring.records[bandsBase + i] = this.engine.scopeScratchBand(i);
|
|
953
|
+
}
|
|
954
|
+
const pointsBase = bandsBase + ring.bands;
|
|
955
|
+
for (let i = 0; i < pointCount; i++) {
|
|
956
|
+
ring.records[pointsBase + 2 * i] = this.engine.scopeScratchPointLeft(i);
|
|
957
|
+
ring.records[pointsBase + 2 * i + 1] = this.engine.scopeScratchPointRight(i);
|
|
725
958
|
}
|
|
959
|
+
Atomics.store(ring.header, 0, writeIndex + 1);
|
|
726
960
|
}
|
|
727
961
|
|
|
728
962
|
// Drains queued external-MIDI events (already lowered to MIDI 1.0 bytes) and
|
|
@@ -730,9 +964,26 @@ export class SonareRealtimeEngineWorkletProcessor {
|
|
|
730
964
|
// One batch per render block; skipped entirely when nothing is queued, so an
|
|
731
965
|
// all-internal project never allocates or posts here.
|
|
732
966
|
private publishExternalMidi(): void {
|
|
967
|
+
const ring = this.externalMidiRing;
|
|
968
|
+
if (ring) {
|
|
969
|
+
for (let count = 0; count < 256 && this.engine.popExternalMidiToScratch(); count++) {
|
|
970
|
+
pushSonareExternalMidiRingBuffer(
|
|
971
|
+
ring,
|
|
972
|
+
this.engine.externalMidiScratchDestinationId(),
|
|
973
|
+
this.engine.externalMidiScratchRenderFrame(),
|
|
974
|
+
this.engine.externalMidiScratchByteWord(),
|
|
975
|
+
this.engine.externalMidiScratchByteCount(),
|
|
976
|
+
);
|
|
977
|
+
this.engine.consumeExternalMidiScratch();
|
|
978
|
+
}
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
733
981
|
if (!this.transport?.postMessage) {
|
|
734
982
|
return;
|
|
735
983
|
}
|
|
984
|
+
if (this.engine.externalMidiPendingCount() === 0) {
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
736
987
|
const events = this.engine.drainExternalMidi(256);
|
|
737
988
|
if (events.length === 0) {
|
|
738
989
|
return;
|
|
@@ -740,30 +991,120 @@ export class SonareRealtimeEngineWorkletProcessor {
|
|
|
740
991
|
this.transport.postMessage({ type: 'externalMidi', events });
|
|
741
992
|
}
|
|
742
993
|
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
const
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
const bandsBase = base + SONARE_SCOPE_RING_RECORD_PREFIX_FLOATS;
|
|
754
|
-
for (let i = 0; i < bandCount; i++) {
|
|
755
|
-
ring.records[bandsBase + i] = record.bands[i];
|
|
994
|
+
/**
|
|
995
|
+
* Forward a bounded batch of native page misses to the main thread. This
|
|
996
|
+
* deliberately runs after audio rendering: a cache miss is silence for this
|
|
997
|
+
* block, and OPFS I/O must never delay `process()`.
|
|
998
|
+
*/
|
|
999
|
+
private publishClipPageRequests(): void {
|
|
1000
|
+
const ring = this.clipPageRequestRing;
|
|
1001
|
+
const transport = this.transport;
|
|
1002
|
+
if (!ring && !transport?.postMessage) {
|
|
1003
|
+
return;
|
|
756
1004
|
}
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
1005
|
+
let count = 0;
|
|
1006
|
+
let drained = 0;
|
|
1007
|
+
while (drained < this.clipPageRequestClipIds.length) {
|
|
1008
|
+
drained += 1;
|
|
1009
|
+
let clipId: number;
|
|
1010
|
+
let sample: number;
|
|
1011
|
+
if (ring) {
|
|
1012
|
+
// This scalar scratch API is intentionally used only on the SAB path:
|
|
1013
|
+
// it avoids embind materialising one JS object for every page miss in
|
|
1014
|
+
// AudioWorklet process().
|
|
1015
|
+
if (!this.engine.popClipPageRequestToScratch()) {
|
|
1016
|
+
break;
|
|
1017
|
+
}
|
|
1018
|
+
clipId = this.engine.clipPageRequestScratchClipId();
|
|
1019
|
+
sample = this.engine.clipPageRequestScratchSample();
|
|
1020
|
+
} else {
|
|
1021
|
+
// postMessage fallback is explicitly non-RT-safe; retain the public
|
|
1022
|
+
// object-returning API only for that degraded control-plane path.
|
|
1023
|
+
const request = this.engine.popClipPageRequest();
|
|
1024
|
+
if (!request) {
|
|
1025
|
+
break;
|
|
1026
|
+
}
|
|
1027
|
+
clipId = request.clipId;
|
|
1028
|
+
sample = request.sample;
|
|
1029
|
+
}
|
|
1030
|
+
const pageFrames = this.pagedClipPageFrames.get(clipId);
|
|
1031
|
+
if (!pageFrames) {
|
|
1032
|
+
continue;
|
|
1033
|
+
}
|
|
1034
|
+
const pageIndex = Math.floor(sample / pageFrames);
|
|
1035
|
+
let duplicate = false;
|
|
1036
|
+
for (let i = 0; i < count; ++i) {
|
|
1037
|
+
if (
|
|
1038
|
+
this.clipPageRequestClipIds[i] === clipId &&
|
|
1039
|
+
this.clipPageRequestPageIndices[i] === pageIndex
|
|
1040
|
+
) {
|
|
1041
|
+
duplicate = true;
|
|
1042
|
+
break;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
if (!duplicate) {
|
|
1046
|
+
this.clipPageRequestClipIds[count] = clipId;
|
|
1047
|
+
this.clipPageRequestPageIndices[count] = pageIndex;
|
|
1048
|
+
count += 1;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
const overflowCount = this.engine.clipPageRequestOverflowCount();
|
|
1052
|
+
// The native counter is uint32_t; preserve a correct delta across its
|
|
1053
|
+
// defined unsigned wraparound instead of emitting a negative count.
|
|
1054
|
+
const dropped = (overflowCount - this.clipPageRequestOverflowReported) >>> 0;
|
|
1055
|
+
this.clipPageRequestOverflowReported = overflowCount;
|
|
1056
|
+
if (ring) {
|
|
1057
|
+
// Preserve native-queue loss in the same cumulative counter used for a
|
|
1058
|
+
// full SAB request ring. No object, structured clone, or postMessage is
|
|
1059
|
+
// created on this path.
|
|
1060
|
+
if (dropped > 0) {
|
|
1061
|
+
Atomics.add(ring.header, 4, dropped);
|
|
1062
|
+
}
|
|
1063
|
+
for (let i = 0; i < count; ++i) {
|
|
1064
|
+
pushSonareClipPageRequestRingBuffer(
|
|
1065
|
+
ring,
|
|
1066
|
+
this.clipPageRequestClipIds[i],
|
|
1067
|
+
this.clipPageRequestPageIndices[i],
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
if (count === 0 && dropped === 0) {
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
if (!transport?.postMessage) {
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
// A message is necessarily allocated by postMessage; the hot path's
|
|
1079
|
+
// resident queue remains fixed-size and no message is built without a miss.
|
|
1080
|
+
const requests = new Array<{ clipId: number; pageIndex: number }>(count);
|
|
1081
|
+
for (let i = 0; i < count; ++i) {
|
|
1082
|
+
requests[i] = {
|
|
1083
|
+
clipId: this.clipPageRequestClipIds[i],
|
|
1084
|
+
pageIndex: this.clipPageRequestPageIndices[i],
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
transport.postMessage({
|
|
1088
|
+
type: 'clipPageRequest',
|
|
1089
|
+
requests,
|
|
1090
|
+
...(dropped > 0 ? { dropped } : {}),
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
private removePagedClipProvider(clipId: number): void {
|
|
1095
|
+
const providerId = this.pagedClipProviders.get(clipId);
|
|
1096
|
+
if (providerId !== undefined) {
|
|
1097
|
+
this.engine.destroyClipPageProvider(providerId);
|
|
1098
|
+
}
|
|
1099
|
+
this.pagedClipProviders.delete(clipId);
|
|
1100
|
+
this.pagedClipPageFrames.delete(clipId);
|
|
1101
|
+
this.pendingPagedClips.delete(clipId);
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
private clearPagedClipProviders(): void {
|
|
1105
|
+
for (const clipId of this.pagedClipProviders.keys()) {
|
|
1106
|
+
this.removePagedClipProvider(clipId);
|
|
762
1107
|
}
|
|
763
|
-
Atomics.store(ring.header, 0, writeIndex + 1);
|
|
764
|
-
// Like writeMeterRing, writeIndex is a free-running monotonic counter; the
|
|
765
|
-
// reader detects silent overrun via firstReadable, so the overflow slot
|
|
766
|
-
// (header[5]) stays at its initial 0.
|
|
767
1108
|
}
|
|
768
1109
|
|
|
769
1110
|
private commandRingFromSharedBuffer(
|
|
@@ -24,6 +24,7 @@ export function registerSonareRealtimeEngineWorkletProcessor(
|
|
|
24
24
|
class RegisteredSonareRealtimeEngineWorkletProcessor extends Base {
|
|
25
25
|
private bridge?: SonareRealtimeEngineWorkletProcessor;
|
|
26
26
|
private readonly pendingMessages: unknown[] = [];
|
|
27
|
+
private pendingMessagesOverflowed = false;
|
|
27
28
|
readonly port?: WorkletPort;
|
|
28
29
|
|
|
29
30
|
constructor(options?: { processorOptions?: SonareRealtimeEngineWorkletProcessorOptions }) {
|
|
@@ -35,6 +36,10 @@ export function registerSonareRealtimeEngineWorkletProcessor(
|
|
|
35
36
|
if (!this.bridge) {
|
|
36
37
|
if (this.pendingMessages.length < 1024) {
|
|
37
38
|
this.pendingMessages.push(event.data);
|
|
39
|
+
} else {
|
|
40
|
+
// A long sync before embind initialization must fail visibly rather
|
|
41
|
+
// than drop a provider/page/commit message and leave silent audio.
|
|
42
|
+
this.pendingMessagesOverflowed = true;
|
|
38
43
|
}
|
|
39
44
|
return;
|
|
40
45
|
}
|
|
@@ -112,6 +117,9 @@ export function registerSonareRealtimeEngineWorkletProcessor(
|
|
|
112
117
|
moduleFactory,
|
|
113
118
|
});
|
|
114
119
|
}
|
|
120
|
+
if (this.pendingMessagesOverflowed) {
|
|
121
|
+
throw new Error('AudioWorklet initialization message buffer overflowed.');
|
|
122
|
+
}
|
|
115
123
|
this.bridge = new SonareRealtimeEngineWorkletProcessor(options, {
|
|
116
124
|
postMessage: (message) => port?.postMessage?.(message),
|
|
117
125
|
onMeter: (meter) => port?.postMessage?.(meter),
|
|
@@ -232,6 +232,17 @@ export function pushMidiCc(
|
|
|
232
232
|
});
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
+
export function pushMidiUmp(
|
|
236
|
+
ctx: EngineStripContext,
|
|
237
|
+
trackId: string | number,
|
|
238
|
+
word0: number,
|
|
239
|
+
renderFrame: number,
|
|
240
|
+
): void {
|
|
241
|
+
const destinationId = ctx.resolveTargetId(trackId);
|
|
242
|
+
ctx.offlineEngine.pushMidiUmp(destinationId, word0, renderFrame);
|
|
243
|
+
ctx.postSync({ type: 'syncMidiUmp', destinationId, word0, renderFrame });
|
|
244
|
+
}
|
|
245
|
+
|
|
235
246
|
export function setBuiltinInstrument(
|
|
236
247
|
ctx: EngineStripContext,
|
|
237
248
|
trackId: string | number,
|
|
@@ -260,7 +271,12 @@ export function loadSoundFont(ctx: EngineStripContext, data: Uint8Array): void {
|
|
|
260
271
|
export function setSf2Instrument(
|
|
261
272
|
ctx: EngineStripContext,
|
|
262
273
|
trackId: string | number,
|
|
263
|
-
config: {
|
|
274
|
+
config: {
|
|
275
|
+
destinationId?: number;
|
|
276
|
+
gain?: number;
|
|
277
|
+
polyphony?: number;
|
|
278
|
+
preferModelForModeledFamilies?: boolean;
|
|
279
|
+
},
|
|
264
280
|
): void {
|
|
265
281
|
const destinationId = ctx.resolveTargetId(trackId);
|
|
266
282
|
ctx.offlineEngine.setSf2Instrument(config, destinationId);
|