@libraz/libsonare 1.5.4 → 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 +873 -63
- package/dist/index.js +1037 -265
- 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 +596 -86
- package/dist/worklet.js +1658 -235
- package/dist/worklet.js.map +1 -1
- package/package.json +23 -11
- package/src/_chain_config.ts +5 -23
- package/src/analysis.ts +137 -0
- package/src/analysis_helpers.ts +29 -0
- package/src/audio.ts +5 -3
- package/src/clip_page_streamer.ts +62 -8
- package/src/codes.ts +51 -64
- package/src/effects_mastering.ts +1 -0
- package/src/effects_transform.ts +12 -6
- package/src/errors.ts +1 -0
- package/src/feature_core.ts +159 -3
- package/src/feature_music.ts +35 -16
- package/src/feature_pitch.ts +75 -6
- package/src/feature_spectral.ts +234 -8
- package/src/feature_spectrogram.ts +186 -5
- package/src/features.ts +16 -1
- package/src/index.ts +104 -7
- package/src/mastering_chain.ts +20 -8
- package/src/mastering_core.ts +7 -0
- package/src/mastering_dynamics.ts +7 -1
- package/src/metering.ts +51 -5
- package/src/mixer.ts +14 -1
- package/src/opfs_clip_pages.ts +26 -1
- package/src/project.ts +7 -0
- package/src/project_class.ts +74 -13
- 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 +28 -0
- package/src/public_types_mixing.ts +11 -2
- package/src/public_types_music.ts +13 -0
- package/src/public_types_spectral.ts +43 -3
- package/src/quick_analysis.ts +48 -12
- package/src/realtime_engine.ts +162 -4
- package/src/realtime_voice_changer.ts +51 -77
- package/src/sonare-analysis.js.d.ts +8 -0
- package/src/sonare.js.d.ts +305 -9
- package/src/streaming_processors.ts +22 -1
- package/src/validation.ts +6 -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 +141 -9
- package/src/worklet/protocol.ts +303 -17
- package/src/worklet/voice-changer-processor.ts +12 -15
- package/src/worklet.ts +20 -1
package/src/worklet/messages.ts
CHANGED
|
@@ -6,11 +6,13 @@ import type {
|
|
|
6
6
|
EngineMarker,
|
|
7
7
|
EngineMetronomeConfig,
|
|
8
8
|
EngineMidiClipSchedule,
|
|
9
|
+
EngineParameterInfo,
|
|
9
10
|
EngineTempoSegment,
|
|
10
11
|
EngineTimeSignatureSegment,
|
|
11
12
|
EngineTrackLane,
|
|
12
13
|
EngineTransportState,
|
|
13
14
|
RealtimeVoiceChangerConfigInput,
|
|
15
|
+
RealtimeVoiceChangerPodConfig,
|
|
14
16
|
} from '../index';
|
|
15
17
|
import type { AutomationCurve } from '../public_types';
|
|
16
18
|
import type {
|
|
@@ -55,6 +57,16 @@ export interface SonareRealtimeEngineWorkletProcessorOptions {
|
|
|
55
57
|
scopeBands?: number;
|
|
56
58
|
scopeSharedBuffer?: SharedArrayBuffer;
|
|
57
59
|
scopeRingCapacity?: number;
|
|
60
|
+
/**
|
|
61
|
+
* Lock-free SPSC queue for clip-page cache misses. Supplying this is required
|
|
62
|
+
* for realtime-safe OPFS streaming: it avoids both embind object creation and
|
|
63
|
+
* postMessage structured cloning from AudioWorklet process().
|
|
64
|
+
*/
|
|
65
|
+
clipPageRequestSharedBuffer?: SharedArrayBuffer;
|
|
66
|
+
clipPageRequestRingCapacity?: number;
|
|
67
|
+
/** Lock-free worklet-to-main-thread MIDI-1 output ring. */
|
|
68
|
+
externalMidiSharedBuffer?: SharedArrayBuffer;
|
|
69
|
+
externalMidiRingCapacity?: number;
|
|
58
70
|
}
|
|
59
71
|
|
|
60
72
|
export interface SonareRealtimeVoiceChangerWorkletProcessorOptions {
|
|
@@ -66,7 +78,8 @@ export interface SonareRealtimeVoiceChangerWorkletProcessorOptions {
|
|
|
66
78
|
|
|
67
79
|
export interface SonareRealtimeVoiceChangerSetConfigMessage {
|
|
68
80
|
type: 'setConfig';
|
|
69
|
-
|
|
81
|
+
/** Pre-normalized by the main thread; never JSON parsed in the worklet. */
|
|
82
|
+
config: RealtimeVoiceChangerPodConfig;
|
|
70
83
|
}
|
|
71
84
|
|
|
72
85
|
export interface SonareRealtimeVoiceChangerResetMessage {
|
|
@@ -88,6 +101,10 @@ export interface SonareRealtimeEngineNodeCapabilities {
|
|
|
88
101
|
sharedArrayBuffer: boolean;
|
|
89
102
|
atomics: boolean;
|
|
90
103
|
audioWorklet: boolean;
|
|
104
|
+
/** True only when clip-page misses use the bounded SAB ring. */
|
|
105
|
+
clipPageRequestsRealtimeSafe: boolean;
|
|
106
|
+
/** True when external MIDI uses the SAB output ring rather than postMessage. */
|
|
107
|
+
externalMidiRealtimeSafe: boolean;
|
|
91
108
|
engineAbiVersion?: number;
|
|
92
109
|
expectedEngineAbiVersion?: number;
|
|
93
110
|
abiCompatible?: boolean;
|
|
@@ -161,10 +178,18 @@ export interface SonareWorkletExternalMidiMessage {
|
|
|
161
178
|
events: SonareWorkletExternalMidiEvent[];
|
|
162
179
|
}
|
|
163
180
|
|
|
181
|
+
/** A control-plane sync message that the worklet rejected without crashing. */
|
|
182
|
+
export interface SonareEngineSyncErrorMessage {
|
|
183
|
+
type: 'syncError';
|
|
184
|
+
syncType: SonareEngineSyncMessage['type'];
|
|
185
|
+
message: string;
|
|
186
|
+
}
|
|
187
|
+
|
|
164
188
|
export type SonareWorkletTransportMessage =
|
|
165
189
|
| SonareWorkletMeterSnapshot
|
|
166
190
|
| SonareWorkletSpectrumSnapshot
|
|
167
191
|
| SonareWorkletExternalMidiMessage
|
|
192
|
+
| SonareEngineClipPageRequestMessage
|
|
168
193
|
| SonareEngineTelemetryRecord;
|
|
169
194
|
|
|
170
195
|
export interface WorkletTransport {
|
|
@@ -172,7 +197,8 @@ export interface WorkletTransport {
|
|
|
172
197
|
message:
|
|
173
198
|
| SonareWorkletTransportMessage
|
|
174
199
|
| SonareEngineCaptureResponseMessage
|
|
175
|
-
| SonareEngineTransportResponseMessage
|
|
200
|
+
| SonareEngineTransportResponseMessage
|
|
201
|
+
| SonareEngineSyncErrorMessage,
|
|
176
202
|
transfer?: Transferable[],
|
|
177
203
|
) => void;
|
|
178
204
|
onMeter?: (meter: SonareWorkletMeterSnapshot) => void;
|
|
@@ -183,6 +209,7 @@ export interface ResolvedMetronomeConfig {
|
|
|
183
209
|
beatGain: number;
|
|
184
210
|
accentGain: number;
|
|
185
211
|
clickSamples: number;
|
|
212
|
+
clickSeconds: number;
|
|
186
213
|
}
|
|
187
214
|
|
|
188
215
|
// Fallback metronome gains/click length used by the worklet consumer until the
|
|
@@ -191,23 +218,42 @@ export interface ResolvedMetronomeConfig {
|
|
|
191
218
|
export const DEFAULT_METRONOME_CONFIG: ResolvedMetronomeConfig = {
|
|
192
219
|
beatGain: 0.35,
|
|
193
220
|
accentGain: 0.7,
|
|
194
|
-
clickSamples:
|
|
221
|
+
clickSamples: 0,
|
|
222
|
+
clickSeconds: 0,
|
|
195
223
|
};
|
|
196
224
|
|
|
197
225
|
export function resolveMetronomeConfig(config: EngineMetronomeConfig): ResolvedMetronomeConfig {
|
|
198
|
-
|
|
226
|
+
const resolved = {
|
|
199
227
|
beatGain: config.beatGain ?? DEFAULT_METRONOME_CONFIG.beatGain,
|
|
200
228
|
accentGain: config.accentGain ?? DEFAULT_METRONOME_CONFIG.accentGain,
|
|
201
229
|
clickSamples: config.clickSamples ?? DEFAULT_METRONOME_CONFIG.clickSamples,
|
|
230
|
+
clickSeconds: config.clickSeconds ?? DEFAULT_METRONOME_CONFIG.clickSeconds,
|
|
202
231
|
};
|
|
232
|
+
if (
|
|
233
|
+
!Number.isFinite(resolved.beatGain) ||
|
|
234
|
+
resolved.beatGain < 0 ||
|
|
235
|
+
!Number.isFinite(resolved.accentGain) ||
|
|
236
|
+
resolved.accentGain < 0 ||
|
|
237
|
+
!Number.isInteger(resolved.clickSamples) ||
|
|
238
|
+
resolved.clickSamples < 0 ||
|
|
239
|
+
!Number.isFinite(resolved.clickSeconds) ||
|
|
240
|
+
resolved.clickSeconds < 0 ||
|
|
241
|
+
resolved.clickSamples > 384000 ||
|
|
242
|
+
(config.clickSeconds !== undefined &&
|
|
243
|
+
(!Number.isFinite(config.clickSeconds) || config.clickSeconds < 0 || config.clickSeconds > 1))
|
|
244
|
+
) {
|
|
245
|
+
throw new RangeError('invalid metronome gains or click length');
|
|
246
|
+
}
|
|
247
|
+
return resolved;
|
|
203
248
|
}
|
|
204
249
|
|
|
205
250
|
// Out-of-band control messages posted from the main-thread SonareEngine facade
|
|
206
251
|
// to the worklet engine processor over node.port. Unlike SonareEngineCommandRecord
|
|
207
252
|
// (a small POD POSTed/ringed every block) these carry bulk/structured payloads
|
|
208
253
|
// (clip audio buffers, marker lists, metronome config) that cannot fit the
|
|
209
|
-
// fixed-size SAB command record
|
|
210
|
-
//
|
|
254
|
+
// fixed-size SAB command record. Their port handlers still execute on the
|
|
255
|
+
// AudioWorklet rendering thread, so consumers must validate them without
|
|
256
|
+
// allocation and publish bounded snapshots for process() to consume.
|
|
211
257
|
export interface SonareEngineSyncClipsMessage {
|
|
212
258
|
type: 'syncClips';
|
|
213
259
|
clips: EngineClip[];
|
|
@@ -223,7 +269,8 @@ export interface SonareEngineSyncClipsDeltaMessage {
|
|
|
223
269
|
export interface SonareEngineSyncClipPageProviderMessage {
|
|
224
270
|
type: 'syncClipPageProvider';
|
|
225
271
|
clipId: number;
|
|
226
|
-
clip
|
|
272
|
+
/** Omitted when an OPFS stream is primed before its clip is scheduled. */
|
|
273
|
+
clip?: EngineClip;
|
|
227
274
|
numChannels: number;
|
|
228
275
|
numSamples: number;
|
|
229
276
|
pageFrames: number;
|
|
@@ -237,10 +284,41 @@ export interface SonareEngineSyncClipPageMessage {
|
|
|
237
284
|
channels: Float32Array[];
|
|
238
285
|
}
|
|
239
286
|
|
|
287
|
+
/** Evicts one page after the main-thread sliding window advances. */
|
|
288
|
+
export interface SonareEngineSyncClipPageClearMessage {
|
|
289
|
+
type: 'syncClipPageClear';
|
|
290
|
+
clipId: number;
|
|
291
|
+
pageIndex: number;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Releases a provider whose clip was removed or whose stream was closed. */
|
|
295
|
+
export interface SonareEngineSyncClipPageDestroyMessage {
|
|
296
|
+
type: 'syncClipPageDestroy';
|
|
297
|
+
clipId: number;
|
|
298
|
+
}
|
|
299
|
+
|
|
240
300
|
/** Makes a fully supplied paged clip visible to the audio engine. */
|
|
241
301
|
export interface SonareEngineSyncClipPageCommitMessage {
|
|
242
302
|
type: 'syncClipPageCommit';
|
|
243
303
|
clipId: number;
|
|
304
|
+
/**
|
|
305
|
+
* Clip schedule to publish with a provider that was primed before its
|
|
306
|
+
* schedule was known. The original pre-baked push path supplies it on the
|
|
307
|
+
* provider message instead.
|
|
308
|
+
*/
|
|
309
|
+
clip?: EngineClip;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* A bounded batch of page misses emitted by the AudioWorklet. The main thread
|
|
314
|
+
* resolves these from OPFS and responds with `syncClipPage`; a missing page is
|
|
315
|
+
* silent until that response arrives.
|
|
316
|
+
*/
|
|
317
|
+
export interface SonareEngineClipPageRequestMessage {
|
|
318
|
+
type: 'clipPageRequest';
|
|
319
|
+
requests: Array<{ clipId: number; pageIndex: number }>;
|
|
320
|
+
/** Number of misses dropped because a bounded native or SAB request queue was full. */
|
|
321
|
+
dropped?: number;
|
|
244
322
|
}
|
|
245
323
|
|
|
246
324
|
export interface SonareEngineSyncMidiClipsMessage {
|
|
@@ -264,6 +342,12 @@ export interface SonareEngineSyncAutomationMessage {
|
|
|
264
342
|
points: EngineAutomationPoint[];
|
|
265
343
|
}
|
|
266
344
|
|
|
345
|
+
/** Replaces the live engine's registered custom-parameter set. */
|
|
346
|
+
export interface SonareEngineSyncParametersMessage {
|
|
347
|
+
type: 'syncParameters';
|
|
348
|
+
parameters: EngineParameterInfo[];
|
|
349
|
+
}
|
|
350
|
+
|
|
267
351
|
export interface SonareEngineSyncTempoMessage {
|
|
268
352
|
type: 'syncTempo';
|
|
269
353
|
bpm: number;
|
|
@@ -389,7 +473,12 @@ export interface SonareEngineSyncSynthInstrumentMessage {
|
|
|
389
473
|
export interface SonareEngineSyncSf2InstrumentMessage {
|
|
390
474
|
type: 'syncSf2Instrument';
|
|
391
475
|
destinationId: number;
|
|
392
|
-
config: {
|
|
476
|
+
config: {
|
|
477
|
+
destinationId?: number;
|
|
478
|
+
gain?: number;
|
|
479
|
+
polyphony?: number;
|
|
480
|
+
preferModelForModeledFamilies?: boolean;
|
|
481
|
+
};
|
|
393
482
|
}
|
|
394
483
|
|
|
395
484
|
export interface SonareEngineSyncLoadSoundFontMessage {
|
|
@@ -424,6 +513,13 @@ export interface SonareEngineSyncMidiCcMessage {
|
|
|
424
513
|
renderFrame: number;
|
|
425
514
|
}
|
|
426
515
|
|
|
516
|
+
export interface SonareEngineSyncMidiUmpMessage {
|
|
517
|
+
type: 'syncMidiUmp';
|
|
518
|
+
destinationId: number;
|
|
519
|
+
word0: number;
|
|
520
|
+
renderFrame: number;
|
|
521
|
+
}
|
|
522
|
+
|
|
427
523
|
export interface SonareEngineSyncMidiSysexMessage {
|
|
428
524
|
type: 'syncMidiSysex';
|
|
429
525
|
destinationId: number;
|
|
@@ -447,6 +543,34 @@ export interface SonareEngineSyncExternalMidiClockMessage {
|
|
|
447
543
|
enabled: boolean;
|
|
448
544
|
}
|
|
449
545
|
|
|
546
|
+
export interface SonareEngineSyncMidiInputSourceMessage {
|
|
547
|
+
type: 'syncMidiInputSource' | 'syncClearMidiInputSource';
|
|
548
|
+
destinationId?: number;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
export interface SonareEngineSyncMidiCcBindingMessage {
|
|
552
|
+
type: 'syncMidiCcBinding';
|
|
553
|
+
channel: number;
|
|
554
|
+
controller: number;
|
|
555
|
+
paramId: number;
|
|
556
|
+
minValue: number;
|
|
557
|
+
maxValue: number;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
export interface SonareEngineSyncMidiInputEventMessage {
|
|
561
|
+
type: 'syncMidiInputNoteOn' | 'syncMidiInputNoteOff' | 'syncMidiInputCc';
|
|
562
|
+
group: number;
|
|
563
|
+
channel: number;
|
|
564
|
+
data0: number;
|
|
565
|
+
data1: number;
|
|
566
|
+
portTimeSamples: number;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/** Releases the realtime engine and all worklet-owned clip buffers. */
|
|
570
|
+
export interface SonareEngineDestroyMessage {
|
|
571
|
+
type: 'destroy';
|
|
572
|
+
}
|
|
573
|
+
|
|
450
574
|
export type SonareEngineInstrumentSyncMessage =
|
|
451
575
|
| SonareEngineSyncBuiltinInstrumentMessage
|
|
452
576
|
| SonareEngineSyncSynthInstrumentMessage
|
|
@@ -459,11 +583,14 @@ export type SonareEngineSyncMessage =
|
|
|
459
583
|
| SonareEngineSyncClipsDeltaMessage
|
|
460
584
|
| SonareEngineSyncClipPageProviderMessage
|
|
461
585
|
| SonareEngineSyncClipPageMessage
|
|
586
|
+
| SonareEngineSyncClipPageClearMessage
|
|
462
587
|
| SonareEngineSyncClipPageCommitMessage
|
|
588
|
+
| SonareEngineSyncClipPageDestroyMessage
|
|
463
589
|
| SonareEngineSyncMidiClipsMessage
|
|
464
590
|
| SonareEngineSyncMarkersMessage
|
|
465
591
|
| SonareEngineSyncMetronomeMessage
|
|
466
592
|
| SonareEngineSyncAutomationMessage
|
|
593
|
+
| SonareEngineSyncParametersMessage
|
|
467
594
|
| SonareEngineSyncTempoMessage
|
|
468
595
|
| SonareEngineSyncMixerMessage
|
|
469
596
|
| SonareEngineSyncCaptureMessage
|
|
@@ -486,10 +613,15 @@ export type SonareEngineSyncMessage =
|
|
|
486
613
|
| SonareEngineSyncMidiFxMessage
|
|
487
614
|
| SonareEngineSyncMidiNoteMessage
|
|
488
615
|
| SonareEngineSyncMidiCcMessage
|
|
616
|
+
| SonareEngineSyncMidiUmpMessage
|
|
489
617
|
| SonareEngineSyncMidiSysexMessage
|
|
490
618
|
| SonareEngineSyncMidiPanicMessage
|
|
491
619
|
| SonareEngineSyncMidiDestinationExternalMessage
|
|
492
|
-
| SonareEngineSyncExternalMidiClockMessage
|
|
620
|
+
| SonareEngineSyncExternalMidiClockMessage
|
|
621
|
+
| SonareEngineSyncMidiInputSourceMessage
|
|
622
|
+
| SonareEngineSyncMidiCcBindingMessage
|
|
623
|
+
| SonareEngineSyncMidiInputEventMessage
|
|
624
|
+
| SonareEngineDestroyMessage;
|
|
493
625
|
|
|
494
626
|
export interface WorkletPort {
|
|
495
627
|
postMessage?: (message: unknown, transfer?: Transferable[]) => void;
|
package/src/worklet/protocol.ts
CHANGED
|
@@ -77,6 +77,17 @@ export function decodeFrame(lo: number, hi: number): number {
|
|
|
77
77
|
export const SONARE_ENGINE_RING_HEADER_INTS = 5;
|
|
78
78
|
export const SONARE_ENGINE_COMMAND_RECORD_BYTES = 32;
|
|
79
79
|
export const SONARE_ENGINE_TELEMETRY_RECORD_BYTES = 48;
|
|
80
|
+
// Clip-page request ring header: [writeIndex, readIndex, capacity,
|
|
81
|
+
// recordUint32s, dropped]. Records are [clipId, pageIndex]. This is an SPSC
|
|
82
|
+
// queue: the AudioWorklet is the sole writer and the main thread is the sole
|
|
83
|
+
// reader. Unlike the telemetry rings, it must not overwrite unread requests:
|
|
84
|
+
// the producer increments `dropped` when the fixed queue is full.
|
|
85
|
+
export const SONARE_CLIP_PAGE_REQUEST_RING_HEADER_INTS = 5;
|
|
86
|
+
export const SONARE_CLIP_PAGE_REQUEST_RING_RECORD_UINT32S = 2;
|
|
87
|
+
// External MIDI records: destination ID, render-frame offset, packed MIDI-1
|
|
88
|
+
// bytes (little-endian), and byte count. SPSC worklet -> main-thread ring.
|
|
89
|
+
export const SONARE_EXTERNAL_MIDI_RING_HEADER_INTS = 5;
|
|
90
|
+
export const SONARE_EXTERNAL_MIDI_RING_RECORD_UINT32S = 4;
|
|
80
91
|
|
|
81
92
|
export enum SonareEngineCommandType {
|
|
82
93
|
SetParam = 0,
|
|
@@ -122,6 +133,9 @@ export enum SonareEngineTelemetryError {
|
|
|
122
133
|
CommandBacklogDeferred = 14,
|
|
123
134
|
ClipPageUnderrun = 15,
|
|
124
135
|
InsertAutomationOverflow = 16,
|
|
136
|
+
MidiClockOverflow = 17,
|
|
137
|
+
MetronomeOverflow = 18,
|
|
138
|
+
InvalidCommand = 19,
|
|
125
139
|
}
|
|
126
140
|
|
|
127
141
|
export interface SonareMeterRingBuffer {
|
|
@@ -180,6 +194,31 @@ export interface SonareScopeRingReadResult {
|
|
|
180
194
|
scopes: SonareWorkletScopeSnapshot[];
|
|
181
195
|
}
|
|
182
196
|
|
|
197
|
+
export interface SonareExternalMidiRingEvent {
|
|
198
|
+
destinationId: number;
|
|
199
|
+
renderFrame: number;
|
|
200
|
+
byteWord: number;
|
|
201
|
+
byteCount: number;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export interface SonareExternalMidiRingBuffer {
|
|
205
|
+
sharedBuffer: SharedArrayBuffer;
|
|
206
|
+
header: Int32Array;
|
|
207
|
+
records: Uint32Array;
|
|
208
|
+
capacity: number;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export interface SharedExternalMidiRingWriter {
|
|
212
|
+
header: Int32Array;
|
|
213
|
+
records: Uint32Array;
|
|
214
|
+
capacity: number;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export interface SonareExternalMidiRingReadResult {
|
|
218
|
+
events: SonareExternalMidiRingEvent[];
|
|
219
|
+
dropped: number;
|
|
220
|
+
}
|
|
221
|
+
|
|
183
222
|
export interface SharedScopeRingWriter {
|
|
184
223
|
header: Int32Array;
|
|
185
224
|
records: Float32Array;
|
|
@@ -226,6 +265,30 @@ export interface SonareEngineTelemetryRingReadResult {
|
|
|
226
265
|
telemetry: SonareEngineTelemetryRecord[];
|
|
227
266
|
}
|
|
228
267
|
|
|
268
|
+
export interface SonareClipPageRequest {
|
|
269
|
+
clipId: number;
|
|
270
|
+
pageIndex: number;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export interface SonareClipPageRequestRingBuffer {
|
|
274
|
+
sharedBuffer: SharedArrayBuffer;
|
|
275
|
+
header: Int32Array;
|
|
276
|
+
records: Uint32Array;
|
|
277
|
+
capacity: number;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export interface SharedClipPageRequestRingWriter {
|
|
281
|
+
header: Int32Array;
|
|
282
|
+
records: Uint32Array;
|
|
283
|
+
capacity: number;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export interface SonareClipPageRequestRingReadResult {
|
|
287
|
+
requests: SonareClipPageRequest[];
|
|
288
|
+
/** Cumulative uint32 drop count for native and SAB-ring overflows. */
|
|
289
|
+
dropped: number;
|
|
290
|
+
}
|
|
291
|
+
|
|
229
292
|
export interface SharedMeterRingWriter {
|
|
230
293
|
header: Int32Array;
|
|
231
294
|
records: Float32Array;
|
|
@@ -495,6 +558,190 @@ export function sonareEngineTelemetryRingBufferByteLength(capacity: number): num
|
|
|
495
558
|
);
|
|
496
559
|
}
|
|
497
560
|
|
|
561
|
+
export function sonareClipPageRequestRingBufferByteLength(capacity: number): number {
|
|
562
|
+
const clampedCapacity = Math.max(1, Math.floor(capacity));
|
|
563
|
+
return (
|
|
564
|
+
SONARE_CLIP_PAGE_REQUEST_RING_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT +
|
|
565
|
+
clampedCapacity * SONARE_CLIP_PAGE_REQUEST_RING_RECORD_UINT32S * Uint32Array.BYTES_PER_ELEMENT
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
export function sonareExternalMidiRingBufferByteLength(capacity: number): number {
|
|
570
|
+
const clampedCapacity = Math.max(1, Math.floor(capacity));
|
|
571
|
+
return (
|
|
572
|
+
SONARE_EXTERNAL_MIDI_RING_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT +
|
|
573
|
+
clampedCapacity * SONARE_EXTERNAL_MIDI_RING_RECORD_UINT32S * Uint32Array.BYTES_PER_ELEMENT
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
export function externalMidiRingFromSharedBuffer(
|
|
578
|
+
sharedBuffer: SharedArrayBuffer,
|
|
579
|
+
fallbackCapacity?: number,
|
|
580
|
+
): SharedExternalMidiRingWriter {
|
|
581
|
+
const headerBytes = SONARE_EXTERNAL_MIDI_RING_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT;
|
|
582
|
+
const header = new Int32Array(sharedBuffer, 0, SONARE_EXTERNAL_MIDI_RING_HEADER_INTS);
|
|
583
|
+
const existingCapacity = Atomics.load(header, 2);
|
|
584
|
+
const capacity = Math.max(1, Math.floor(existingCapacity || fallbackCapacity || 1));
|
|
585
|
+
if (sharedBuffer.byteLength < sonareExternalMidiRingBufferByteLength(capacity)) {
|
|
586
|
+
throw new Error('externalMidiSharedBuffer is too small for the requested ring capacity.');
|
|
587
|
+
}
|
|
588
|
+
Atomics.store(header, 2, capacity);
|
|
589
|
+
Atomics.store(header, 3, SONARE_EXTERNAL_MIDI_RING_RECORD_UINT32S);
|
|
590
|
+
return {
|
|
591
|
+
header,
|
|
592
|
+
records: new Uint32Array(
|
|
593
|
+
sharedBuffer,
|
|
594
|
+
headerBytes,
|
|
595
|
+
capacity * SONARE_EXTERNAL_MIDI_RING_RECORD_UINT32S,
|
|
596
|
+
),
|
|
597
|
+
capacity,
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
export function createSonareExternalMidiRingBuffer(capacity = 256): SonareExternalMidiRingBuffer {
|
|
602
|
+
const clampedCapacity = Math.max(1, Math.floor(capacity));
|
|
603
|
+
const sharedBuffer = new SharedArrayBuffer(
|
|
604
|
+
sonareExternalMidiRingBufferByteLength(clampedCapacity),
|
|
605
|
+
);
|
|
606
|
+
const ring = externalMidiRingFromSharedBuffer(sharedBuffer, clampedCapacity);
|
|
607
|
+
Atomics.store(ring.header, 0, 0);
|
|
608
|
+
Atomics.store(ring.header, 1, 0);
|
|
609
|
+
Atomics.store(ring.header, 4, 0);
|
|
610
|
+
return { sharedBuffer, header: ring.header, records: ring.records, capacity: ring.capacity };
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/** Write one lowered MIDI-1 event without allocations from the audio worklet. */
|
|
614
|
+
export function pushSonareExternalMidiRingBuffer(
|
|
615
|
+
ring: SharedExternalMidiRingWriter,
|
|
616
|
+
destinationId: number,
|
|
617
|
+
renderFrame: number,
|
|
618
|
+
byteWord: number,
|
|
619
|
+
byteCount: number,
|
|
620
|
+
): boolean {
|
|
621
|
+
// Emscripten can expose an int64 render-frame scalar as bigint when the
|
|
622
|
+
// host enables WASM_BIGINT. Normalize at this fixed-record boundary.
|
|
623
|
+
const destination = Number(destinationId);
|
|
624
|
+
const frame = Number(renderFrame);
|
|
625
|
+
const packedBytes = Number(byteWord);
|
|
626
|
+
const count = Number(byteCount);
|
|
627
|
+
if (
|
|
628
|
+
!Number.isSafeInteger(destination) ||
|
|
629
|
+
!Number.isSafeInteger(frame) ||
|
|
630
|
+
frame < 0 ||
|
|
631
|
+
!Number.isSafeInteger(packedBytes) ||
|
|
632
|
+
packedBytes < 0 ||
|
|
633
|
+
packedBytes > 0xff_ffff ||
|
|
634
|
+
!Number.isSafeInteger(count) ||
|
|
635
|
+
count < 1 ||
|
|
636
|
+
count > 3
|
|
637
|
+
) {
|
|
638
|
+
Atomics.add(ring.header, 4, 1);
|
|
639
|
+
return false;
|
|
640
|
+
}
|
|
641
|
+
const writeIndex = Atomics.load(ring.header, 0);
|
|
642
|
+
const readIndex = Atomics.load(ring.header, 1);
|
|
643
|
+
if (writeIndex - readIndex >= ring.capacity) {
|
|
644
|
+
Atomics.add(ring.header, 4, 1);
|
|
645
|
+
return false;
|
|
646
|
+
}
|
|
647
|
+
const offset = (writeIndex % ring.capacity) * SONARE_EXTERNAL_MIDI_RING_RECORD_UINT32S;
|
|
648
|
+
Atomics.store(ring.records, offset, destination);
|
|
649
|
+
Atomics.store(ring.records, offset + 1, frame);
|
|
650
|
+
Atomics.store(ring.records, offset + 2, packedBytes);
|
|
651
|
+
Atomics.store(ring.records, offset + 3, count);
|
|
652
|
+
Atomics.store(ring.header, 0, writeIndex + 1);
|
|
653
|
+
return true;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/** Main-thread drain. It is intentionally the only place MIDI byte arrays are built. */
|
|
657
|
+
export function readSonareExternalMidiRingBuffer(
|
|
658
|
+
ring: SonareExternalMidiRingBuffer,
|
|
659
|
+
): SonareExternalMidiRingReadResult {
|
|
660
|
+
const readIndex = Atomics.load(ring.header, 1);
|
|
661
|
+
const writeIndex = Atomics.load(ring.header, 0);
|
|
662
|
+
const events: SonareExternalMidiRingEvent[] = [];
|
|
663
|
+
for (let index = readIndex; index < writeIndex; index++) {
|
|
664
|
+
const offset = (index % ring.capacity) * SONARE_EXTERNAL_MIDI_RING_RECORD_UINT32S;
|
|
665
|
+
events.push({
|
|
666
|
+
destinationId: Atomics.load(ring.records, offset) >>> 0,
|
|
667
|
+
renderFrame: Atomics.load(ring.records, offset + 1) >>> 0,
|
|
668
|
+
byteWord: Atomics.load(ring.records, offset + 2) >>> 0,
|
|
669
|
+
byteCount: Atomics.load(ring.records, offset + 3) >>> 0,
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
Atomics.store(ring.header, 1, writeIndex);
|
|
673
|
+
return { events, dropped: Atomics.load(ring.header, 4) >>> 0 };
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
export function createSonareClipPageRequestRingBuffer(
|
|
677
|
+
capacity = 128,
|
|
678
|
+
): SonareClipPageRequestRingBuffer {
|
|
679
|
+
const clampedCapacity = Math.max(1, Math.floor(capacity));
|
|
680
|
+
const sharedBuffer = new SharedArrayBuffer(
|
|
681
|
+
sonareClipPageRequestRingBufferByteLength(clampedCapacity),
|
|
682
|
+
);
|
|
683
|
+
const ring = clipPageRequestRingFromSharedBuffer(sharedBuffer, clampedCapacity);
|
|
684
|
+
Atomics.store(ring.header, 0, 0);
|
|
685
|
+
Atomics.store(ring.header, 1, 0);
|
|
686
|
+
Atomics.store(ring.header, 2, ring.capacity);
|
|
687
|
+
Atomics.store(ring.header, 3, SONARE_CLIP_PAGE_REQUEST_RING_RECORD_UINT32S);
|
|
688
|
+
Atomics.store(ring.header, 4, 0);
|
|
689
|
+
return { sharedBuffer, header: ring.header, records: ring.records, capacity: ring.capacity };
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* Writes one request without allocating. Invalid uint32 coordinates and a full
|
|
694
|
+
* ring are counted as drops so the main thread can surface a bounded-loss
|
|
695
|
+
* diagnostic without receiving a worklet postMessage.
|
|
696
|
+
*/
|
|
697
|
+
export function pushSonareClipPageRequestRingBuffer(
|
|
698
|
+
ring: SharedClipPageRequestRingWriter,
|
|
699
|
+
clipId: number,
|
|
700
|
+
pageIndex: number,
|
|
701
|
+
): boolean {
|
|
702
|
+
if (
|
|
703
|
+
!Number.isSafeInteger(clipId) ||
|
|
704
|
+
clipId < 0 ||
|
|
705
|
+
clipId > 0xffff_ffff ||
|
|
706
|
+
!Number.isSafeInteger(pageIndex) ||
|
|
707
|
+
pageIndex < 0 ||
|
|
708
|
+
pageIndex > 0xffff_ffff
|
|
709
|
+
) {
|
|
710
|
+
Atomics.add(ring.header, 4, 1);
|
|
711
|
+
return false;
|
|
712
|
+
}
|
|
713
|
+
const writeIndex = Atomics.load(ring.header, 0);
|
|
714
|
+
const readIndex = Atomics.load(ring.header, 1);
|
|
715
|
+
if (writeIndex - readIndex >= ring.capacity) {
|
|
716
|
+
Atomics.add(ring.header, 4, 1);
|
|
717
|
+
return false;
|
|
718
|
+
}
|
|
719
|
+
const offset = (writeIndex % ring.capacity) * SONARE_CLIP_PAGE_REQUEST_RING_RECORD_UINT32S;
|
|
720
|
+
Atomics.store(ring.records, offset, clipId);
|
|
721
|
+
Atomics.store(ring.records, offset + 1, pageIndex);
|
|
722
|
+
// Publish only after both scalar fields have been written.
|
|
723
|
+
Atomics.store(ring.header, 0, writeIndex + 1);
|
|
724
|
+
return true;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/** Drains the SPSC request ring on the main thread. Object creation is here, never in process(). */
|
|
728
|
+
export function readSonareClipPageRequestRingBuffer(
|
|
729
|
+
ring: SonareClipPageRequestRingBuffer,
|
|
730
|
+
): SonareClipPageRequestRingReadResult {
|
|
731
|
+
const readIndex = Atomics.load(ring.header, 1);
|
|
732
|
+
const writeIndex = Atomics.load(ring.header, 0);
|
|
733
|
+
const requests: SonareClipPageRequest[] = [];
|
|
734
|
+
for (let index = readIndex; index < writeIndex; index++) {
|
|
735
|
+
const offset = (index % ring.capacity) * SONARE_CLIP_PAGE_REQUEST_RING_RECORD_UINT32S;
|
|
736
|
+
requests.push({
|
|
737
|
+
clipId: Atomics.load(ring.records, offset),
|
|
738
|
+
pageIndex: Atomics.load(ring.records, offset + 1),
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
Atomics.store(ring.header, 1, writeIndex);
|
|
742
|
+
return { requests, dropped: Atomics.load(ring.header, 4) >>> 0 };
|
|
743
|
+
}
|
|
744
|
+
|
|
498
745
|
export function createSonareEngineCommandRingBuffer(capacity = 128): SonareEngineCommandRingBuffer {
|
|
499
746
|
const clampedCapacity = Math.max(1, Math.floor(capacity));
|
|
500
747
|
const sharedBuffer = new SharedArrayBuffer(
|
|
@@ -668,18 +915,56 @@ export function engineRingFromSharedBuffer(
|
|
|
668
915
|
};
|
|
669
916
|
}
|
|
670
917
|
|
|
918
|
+
export function clipPageRequestRingFromSharedBuffer(
|
|
919
|
+
sharedBuffer: SharedArrayBuffer,
|
|
920
|
+
fallbackCapacity?: number,
|
|
921
|
+
): SharedClipPageRequestRingWriter {
|
|
922
|
+
const headerBytes = SONARE_CLIP_PAGE_REQUEST_RING_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT;
|
|
923
|
+
const header = new Int32Array(sharedBuffer, 0, SONARE_CLIP_PAGE_REQUEST_RING_HEADER_INTS);
|
|
924
|
+
const existingCapacity = Atomics.load(header, 2);
|
|
925
|
+
const capacity = Math.max(1, Math.floor(existingCapacity || fallbackCapacity || 1));
|
|
926
|
+
const minBytes = sonareClipPageRequestRingBufferByteLength(capacity);
|
|
927
|
+
if (sharedBuffer.byteLength < minBytes) {
|
|
928
|
+
throw new Error('clipPageRequestSharedBuffer is too small for the requested ring capacity.');
|
|
929
|
+
}
|
|
930
|
+
Atomics.store(header, 2, capacity);
|
|
931
|
+
Atomics.store(header, 3, SONARE_CLIP_PAGE_REQUEST_RING_RECORD_UINT32S);
|
|
932
|
+
return {
|
|
933
|
+
header,
|
|
934
|
+
records: new Uint32Array(
|
|
935
|
+
sharedBuffer,
|
|
936
|
+
headerBytes,
|
|
937
|
+
capacity * SONARE_CLIP_PAGE_REQUEST_RING_RECORD_UINT32S,
|
|
938
|
+
),
|
|
939
|
+
capacity,
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
|
|
671
943
|
function recordOffset(index: number, capacity: number, recordBytes: number): number {
|
|
672
944
|
return (index % capacity) * recordBytes;
|
|
673
945
|
}
|
|
674
946
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
947
|
+
function toSafeInteger(value: number | bigint | undefined, fallback: number): number {
|
|
948
|
+
const resolved = typeof value === 'bigint' ? Number(value) : value;
|
|
949
|
+
if (resolved === undefined) {
|
|
950
|
+
return fallback;
|
|
678
951
|
}
|
|
679
|
-
if (
|
|
680
|
-
|
|
952
|
+
if (!Number.isSafeInteger(resolved)) {
|
|
953
|
+
throw new RangeError('64-bit ring values must be safe integers');
|
|
681
954
|
}
|
|
682
|
-
return
|
|
955
|
+
return resolved;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
/** Store a signed 64-bit safe integer as two 32-bit words without BigInt. */
|
|
959
|
+
export function writeInt64Words(view: DataView, offset: number, value: number): void {
|
|
960
|
+
const integer = toSafeInteger(value, 0);
|
|
961
|
+
view.setUint32(offset, integer >>> 0, true);
|
|
962
|
+
view.setInt32(offset + 4, Math.floor(integer / 0x100000000), true);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
/** Reconstruct a signed 64-bit safe integer stored by {@link writeInt64Words}. */
|
|
966
|
+
export function readInt64Words(view: DataView, offset: number): number {
|
|
967
|
+
return view.getInt32(offset + 4, true) * 0x100000000 + view.getUint32(offset, true);
|
|
683
968
|
}
|
|
684
969
|
|
|
685
970
|
function writeEngineCommandRecord(
|
|
@@ -689,21 +974,21 @@ function writeEngineCommandRecord(
|
|
|
689
974
|
): void {
|
|
690
975
|
view.setUint32(offset, command.type, true);
|
|
691
976
|
view.setUint32(offset + 4, command.targetId ?? 0, true);
|
|
692
|
-
view
|
|
977
|
+
writeInt64Words(view, offset + 8, toSafeInteger(command.sampleTime, -1));
|
|
693
978
|
// argFloat occupies a full 8-byte Float64 slot (replacing the old Float32 +
|
|
694
979
|
// 4-byte pad) so PPQ scalars carried here keep full double precision over the
|
|
695
980
|
// SAB transport, matching the engine's double-precision seek/loop contract.
|
|
696
981
|
view.setFloat64(offset + 16, command.argFloat ?? 0, true);
|
|
697
|
-
view
|
|
982
|
+
writeInt64Words(view, offset + 24, toSafeInteger(command.argInt, 0));
|
|
698
983
|
}
|
|
699
984
|
|
|
700
985
|
function readEngineCommandRecord(view: DataView, offset: number): SonareEngineCommandRecord {
|
|
701
986
|
return {
|
|
702
987
|
type: view.getUint32(offset, true),
|
|
703
988
|
targetId: view.getUint32(offset + 4, true),
|
|
704
|
-
sampleTime:
|
|
989
|
+
sampleTime: readInt64Words(view, offset + 8),
|
|
705
990
|
argFloat: view.getFloat64(offset + 16, true),
|
|
706
|
-
argInt:
|
|
991
|
+
argInt: readInt64Words(view, offset + 24),
|
|
707
992
|
};
|
|
708
993
|
}
|
|
709
994
|
|
|
@@ -714,21 +999,22 @@ function writeEngineTelemetryRecord(
|
|
|
714
999
|
): void {
|
|
715
1000
|
view.setUint32(offset, telemetry.type, true);
|
|
716
1001
|
view.setUint32(offset + 4, telemetry.error, true);
|
|
717
|
-
view
|
|
718
|
-
view
|
|
719
|
-
view
|
|
1002
|
+
writeInt64Words(view, offset + 8, telemetry.renderFrame);
|
|
1003
|
+
writeInt64Words(view, offset + 16, telemetry.timelineSample);
|
|
1004
|
+
writeInt64Words(view, offset + 24, telemetry.audibleTimelineSample);
|
|
720
1005
|
view.setInt32(offset + 32, telemetry.graphLatencySamplesQ8, true);
|
|
721
1006
|
view.setUint32(offset + 36, telemetry.value, true);
|
|
722
|
-
view.
|
|
1007
|
+
view.setUint32(offset + 40, 0, true);
|
|
1008
|
+
view.setUint32(offset + 44, 0, true);
|
|
723
1009
|
}
|
|
724
1010
|
|
|
725
1011
|
function readEngineTelemetryRecord(view: DataView, offset: number): SonareEngineTelemetryRecord {
|
|
726
1012
|
return {
|
|
727
1013
|
type: view.getUint32(offset, true),
|
|
728
1014
|
error: view.getUint32(offset + 4, true),
|
|
729
|
-
renderFrame:
|
|
730
|
-
timelineSample:
|
|
731
|
-
audibleTimelineSample:
|
|
1015
|
+
renderFrame: readInt64Words(view, offset + 8),
|
|
1016
|
+
timelineSample: readInt64Words(view, offset + 16),
|
|
1017
|
+
audibleTimelineSample: readInt64Words(view, offset + 24),
|
|
732
1018
|
graphLatencySamplesQ8: view.getInt32(offset + 32, true),
|
|
733
1019
|
value: view.getUint32(offset + 36, true),
|
|
734
1020
|
};
|