@libraz/libsonare 1.5.5 → 1.7.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 +5809 -0
- package/dist/analysis.js +2574 -0
- package/dist/analysis.js.map +1 -0
- package/dist/index.d.ts +884 -55
- package/dist/index.js +1302 -292
- 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 +734 -0
- package/dist/worker.js.map +1 -0
- package/dist/worklet.d.ts +628 -88
- package/dist/worklet.js +1816 -266
- 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 +40 -29
- package/src/audio.ts +7 -2
- package/src/clip_page_streamer.ts +62 -8
- package/src/codes.ts +87 -64
- package/src/effects_mastering.ts +1 -0
- package/src/effects_transform.ts +179 -10
- package/src/errors.ts +1 -0
- package/src/feature_core.ts +140 -2
- package/src/feature_music.ts +25 -3
- package/src/feature_pitch.ts +70 -1
- package/src/feature_spectral.ts +295 -7
- package/src/feature_spectrogram.ts +203 -3
- package/src/features.ts +16 -1
- package/src/index.ts +120 -8
- 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/mixer.ts +3 -3
- package/src/opfs_clip_pages.ts +26 -1
- package/src/project.ts +13 -0
- package/src/project_class.ts +204 -23
- package/src/project_internal.ts +38 -34
- package/src/project_types.ts +114 -11
- package/src/public_types.ts +68 -0
- package/src/public_types_mastering.ts +22 -0
- package/src/public_types_mixing.ts +27 -2
- package/src/public_types_music.ts +9 -0
- package/src/public_types_realtime.ts +23 -5
- package/src/public_types_spectral.ts +33 -0
- package/src/quick_analysis.ts +46 -10
- package/src/realtime_engine.ts +167 -6
- package/src/realtime_voice_changer.ts +51 -77
- package/src/sonare-analysis.js.d.ts +8 -0
- package/src/sonare.js.d.ts +340 -6
- package/src/stream_analyzer.ts +6 -1
- package/src/streaming_processors.ts +15 -0
- package/src/web_midi.ts +46 -11
- 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 +264 -21
- package/src/worklet/engine-offline.ts +7 -2
- package/src/worklet/engine-parameter-facade.ts +55 -5
- package/src/worklet/engine-processor.ts +435 -62
- package/src/worklet/engine-register.ts +9 -1
- package/src/worklet/engine-strips.ts +19 -3
- package/src/worklet/engine-sync.ts +2 -6
- package/src/worklet/engine-tempo-facade.ts +8 -6
- package/src/worklet/engine.ts +390 -13
- package/src/worklet/guards.ts +121 -8
- package/src/worklet/messages.ts +166 -8
- package/src/worklet/protocol.ts +305 -17
- package/src/worklet/voice-changer-processor.ts +12 -15
- package/src/worklet.ts +20 -1
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,
|
|
@@ -97,6 +108,9 @@ export enum SonareEngineCommandType {
|
|
|
97
108
|
SetMetronome = 15,
|
|
98
109
|
SetMarker = 16,
|
|
99
110
|
SeekMarker = 17,
|
|
111
|
+
// Queueable lane monitor-mode transition. The worklet command vocabulary
|
|
112
|
+
// deliberately leaves 18..25 unused; this is the stable ABI-v3 ordinal.
|
|
113
|
+
SetTrackMonitorMode = 26,
|
|
100
114
|
}
|
|
101
115
|
|
|
102
116
|
export enum SonareEngineTelemetryType {
|
|
@@ -124,6 +138,8 @@ export enum SonareEngineTelemetryError {
|
|
|
124
138
|
InsertAutomationOverflow = 16,
|
|
125
139
|
MidiClockOverflow = 17,
|
|
126
140
|
MetronomeOverflow = 18,
|
|
141
|
+
InvalidCommand = 19,
|
|
142
|
+
MaxChannelsExceeded = 20,
|
|
127
143
|
}
|
|
128
144
|
|
|
129
145
|
export interface SonareMeterRingBuffer {
|
|
@@ -182,6 +198,31 @@ export interface SonareScopeRingReadResult {
|
|
|
182
198
|
scopes: SonareWorkletScopeSnapshot[];
|
|
183
199
|
}
|
|
184
200
|
|
|
201
|
+
export interface SonareExternalMidiRingEvent {
|
|
202
|
+
destinationId: number;
|
|
203
|
+
renderFrame: number;
|
|
204
|
+
byteWord: number;
|
|
205
|
+
byteCount: number;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export interface SonareExternalMidiRingBuffer {
|
|
209
|
+
sharedBuffer: SharedArrayBuffer;
|
|
210
|
+
header: Int32Array;
|
|
211
|
+
records: Uint32Array;
|
|
212
|
+
capacity: number;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export interface SharedExternalMidiRingWriter {
|
|
216
|
+
header: Int32Array;
|
|
217
|
+
records: Uint32Array;
|
|
218
|
+
capacity: number;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export interface SonareExternalMidiRingReadResult {
|
|
222
|
+
events: SonareExternalMidiRingEvent[];
|
|
223
|
+
dropped: number;
|
|
224
|
+
}
|
|
225
|
+
|
|
185
226
|
export interface SharedScopeRingWriter {
|
|
186
227
|
header: Int32Array;
|
|
187
228
|
records: Float32Array;
|
|
@@ -228,6 +269,30 @@ export interface SonareEngineTelemetryRingReadResult {
|
|
|
228
269
|
telemetry: SonareEngineTelemetryRecord[];
|
|
229
270
|
}
|
|
230
271
|
|
|
272
|
+
export interface SonareClipPageRequest {
|
|
273
|
+
clipId: number;
|
|
274
|
+
pageIndex: number;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export interface SonareClipPageRequestRingBuffer {
|
|
278
|
+
sharedBuffer: SharedArrayBuffer;
|
|
279
|
+
header: Int32Array;
|
|
280
|
+
records: Uint32Array;
|
|
281
|
+
capacity: number;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export interface SharedClipPageRequestRingWriter {
|
|
285
|
+
header: Int32Array;
|
|
286
|
+
records: Uint32Array;
|
|
287
|
+
capacity: number;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export interface SonareClipPageRequestRingReadResult {
|
|
291
|
+
requests: SonareClipPageRequest[];
|
|
292
|
+
/** Cumulative uint32 drop count for native and SAB-ring overflows. */
|
|
293
|
+
dropped: number;
|
|
294
|
+
}
|
|
295
|
+
|
|
231
296
|
export interface SharedMeterRingWriter {
|
|
232
297
|
header: Int32Array;
|
|
233
298
|
records: Float32Array;
|
|
@@ -497,6 +562,190 @@ export function sonareEngineTelemetryRingBufferByteLength(capacity: number): num
|
|
|
497
562
|
);
|
|
498
563
|
}
|
|
499
564
|
|
|
565
|
+
export function sonareClipPageRequestRingBufferByteLength(capacity: number): number {
|
|
566
|
+
const clampedCapacity = Math.max(1, Math.floor(capacity));
|
|
567
|
+
return (
|
|
568
|
+
SONARE_CLIP_PAGE_REQUEST_RING_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT +
|
|
569
|
+
clampedCapacity * SONARE_CLIP_PAGE_REQUEST_RING_RECORD_UINT32S * Uint32Array.BYTES_PER_ELEMENT
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
export function sonareExternalMidiRingBufferByteLength(capacity: number): number {
|
|
574
|
+
const clampedCapacity = Math.max(1, Math.floor(capacity));
|
|
575
|
+
return (
|
|
576
|
+
SONARE_EXTERNAL_MIDI_RING_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT +
|
|
577
|
+
clampedCapacity * SONARE_EXTERNAL_MIDI_RING_RECORD_UINT32S * Uint32Array.BYTES_PER_ELEMENT
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
export function externalMidiRingFromSharedBuffer(
|
|
582
|
+
sharedBuffer: SharedArrayBuffer,
|
|
583
|
+
fallbackCapacity?: number,
|
|
584
|
+
): SharedExternalMidiRingWriter {
|
|
585
|
+
const headerBytes = SONARE_EXTERNAL_MIDI_RING_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT;
|
|
586
|
+
const header = new Int32Array(sharedBuffer, 0, SONARE_EXTERNAL_MIDI_RING_HEADER_INTS);
|
|
587
|
+
const existingCapacity = Atomics.load(header, 2);
|
|
588
|
+
const capacity = Math.max(1, Math.floor(existingCapacity || fallbackCapacity || 1));
|
|
589
|
+
if (sharedBuffer.byteLength < sonareExternalMidiRingBufferByteLength(capacity)) {
|
|
590
|
+
throw new Error('externalMidiSharedBuffer is too small for the requested ring capacity.');
|
|
591
|
+
}
|
|
592
|
+
Atomics.store(header, 2, capacity);
|
|
593
|
+
Atomics.store(header, 3, SONARE_EXTERNAL_MIDI_RING_RECORD_UINT32S);
|
|
594
|
+
return {
|
|
595
|
+
header,
|
|
596
|
+
records: new Uint32Array(
|
|
597
|
+
sharedBuffer,
|
|
598
|
+
headerBytes,
|
|
599
|
+
capacity * SONARE_EXTERNAL_MIDI_RING_RECORD_UINT32S,
|
|
600
|
+
),
|
|
601
|
+
capacity,
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
export function createSonareExternalMidiRingBuffer(capacity = 256): SonareExternalMidiRingBuffer {
|
|
606
|
+
const clampedCapacity = Math.max(1, Math.floor(capacity));
|
|
607
|
+
const sharedBuffer = new SharedArrayBuffer(
|
|
608
|
+
sonareExternalMidiRingBufferByteLength(clampedCapacity),
|
|
609
|
+
);
|
|
610
|
+
const ring = externalMidiRingFromSharedBuffer(sharedBuffer, clampedCapacity);
|
|
611
|
+
Atomics.store(ring.header, 0, 0);
|
|
612
|
+
Atomics.store(ring.header, 1, 0);
|
|
613
|
+
Atomics.store(ring.header, 4, 0);
|
|
614
|
+
return { sharedBuffer, header: ring.header, records: ring.records, capacity: ring.capacity };
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/** Write one lowered MIDI-1 event without allocations from the audio worklet. */
|
|
618
|
+
export function pushSonareExternalMidiRingBuffer(
|
|
619
|
+
ring: SharedExternalMidiRingWriter,
|
|
620
|
+
destinationId: number,
|
|
621
|
+
renderFrame: number,
|
|
622
|
+
byteWord: number,
|
|
623
|
+
byteCount: number,
|
|
624
|
+
): boolean {
|
|
625
|
+
// Emscripten can expose an int64 render-frame scalar as bigint when the
|
|
626
|
+
// host enables WASM_BIGINT. Normalize at this fixed-record boundary.
|
|
627
|
+
const destination = Number(destinationId);
|
|
628
|
+
const frame = Number(renderFrame);
|
|
629
|
+
const packedBytes = Number(byteWord);
|
|
630
|
+
const count = Number(byteCount);
|
|
631
|
+
if (
|
|
632
|
+
!Number.isSafeInteger(destination) ||
|
|
633
|
+
!Number.isSafeInteger(frame) ||
|
|
634
|
+
frame < 0 ||
|
|
635
|
+
!Number.isSafeInteger(packedBytes) ||
|
|
636
|
+
packedBytes < 0 ||
|
|
637
|
+
packedBytes > 0xff_ffff ||
|
|
638
|
+
!Number.isSafeInteger(count) ||
|
|
639
|
+
count < 1 ||
|
|
640
|
+
count > 3
|
|
641
|
+
) {
|
|
642
|
+
Atomics.add(ring.header, 4, 1);
|
|
643
|
+
return false;
|
|
644
|
+
}
|
|
645
|
+
const writeIndex = Atomics.load(ring.header, 0);
|
|
646
|
+
const readIndex = Atomics.load(ring.header, 1);
|
|
647
|
+
if (writeIndex - readIndex >= ring.capacity) {
|
|
648
|
+
Atomics.add(ring.header, 4, 1);
|
|
649
|
+
return false;
|
|
650
|
+
}
|
|
651
|
+
const offset = (writeIndex % ring.capacity) * SONARE_EXTERNAL_MIDI_RING_RECORD_UINT32S;
|
|
652
|
+
Atomics.store(ring.records, offset, destination);
|
|
653
|
+
Atomics.store(ring.records, offset + 1, frame);
|
|
654
|
+
Atomics.store(ring.records, offset + 2, packedBytes);
|
|
655
|
+
Atomics.store(ring.records, offset + 3, count);
|
|
656
|
+
Atomics.store(ring.header, 0, writeIndex + 1);
|
|
657
|
+
return true;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/** Main-thread drain. It is intentionally the only place MIDI byte arrays are built. */
|
|
661
|
+
export function readSonareExternalMidiRingBuffer(
|
|
662
|
+
ring: SonareExternalMidiRingBuffer,
|
|
663
|
+
): SonareExternalMidiRingReadResult {
|
|
664
|
+
const readIndex = Atomics.load(ring.header, 1);
|
|
665
|
+
const writeIndex = Atomics.load(ring.header, 0);
|
|
666
|
+
const events: SonareExternalMidiRingEvent[] = [];
|
|
667
|
+
for (let index = readIndex; index < writeIndex; index++) {
|
|
668
|
+
const offset = (index % ring.capacity) * SONARE_EXTERNAL_MIDI_RING_RECORD_UINT32S;
|
|
669
|
+
events.push({
|
|
670
|
+
destinationId: Atomics.load(ring.records, offset) >>> 0,
|
|
671
|
+
renderFrame: Atomics.load(ring.records, offset + 1) >>> 0,
|
|
672
|
+
byteWord: Atomics.load(ring.records, offset + 2) >>> 0,
|
|
673
|
+
byteCount: Atomics.load(ring.records, offset + 3) >>> 0,
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
Atomics.store(ring.header, 1, writeIndex);
|
|
677
|
+
return { events, dropped: Atomics.load(ring.header, 4) >>> 0 };
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
export function createSonareClipPageRequestRingBuffer(
|
|
681
|
+
capacity = 128,
|
|
682
|
+
): SonareClipPageRequestRingBuffer {
|
|
683
|
+
const clampedCapacity = Math.max(1, Math.floor(capacity));
|
|
684
|
+
const sharedBuffer = new SharedArrayBuffer(
|
|
685
|
+
sonareClipPageRequestRingBufferByteLength(clampedCapacity),
|
|
686
|
+
);
|
|
687
|
+
const ring = clipPageRequestRingFromSharedBuffer(sharedBuffer, clampedCapacity);
|
|
688
|
+
Atomics.store(ring.header, 0, 0);
|
|
689
|
+
Atomics.store(ring.header, 1, 0);
|
|
690
|
+
Atomics.store(ring.header, 2, ring.capacity);
|
|
691
|
+
Atomics.store(ring.header, 3, SONARE_CLIP_PAGE_REQUEST_RING_RECORD_UINT32S);
|
|
692
|
+
Atomics.store(ring.header, 4, 0);
|
|
693
|
+
return { sharedBuffer, header: ring.header, records: ring.records, capacity: ring.capacity };
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Writes one request without allocating. Invalid uint32 coordinates and a full
|
|
698
|
+
* ring are counted as drops so the main thread can surface a bounded-loss
|
|
699
|
+
* diagnostic without receiving a worklet postMessage.
|
|
700
|
+
*/
|
|
701
|
+
export function pushSonareClipPageRequestRingBuffer(
|
|
702
|
+
ring: SharedClipPageRequestRingWriter,
|
|
703
|
+
clipId: number,
|
|
704
|
+
pageIndex: number,
|
|
705
|
+
): boolean {
|
|
706
|
+
if (
|
|
707
|
+
!Number.isSafeInteger(clipId) ||
|
|
708
|
+
clipId < 0 ||
|
|
709
|
+
clipId > 0xffff_ffff ||
|
|
710
|
+
!Number.isSafeInteger(pageIndex) ||
|
|
711
|
+
pageIndex < 0 ||
|
|
712
|
+
pageIndex > 0xffff_ffff
|
|
713
|
+
) {
|
|
714
|
+
Atomics.add(ring.header, 4, 1);
|
|
715
|
+
return false;
|
|
716
|
+
}
|
|
717
|
+
const writeIndex = Atomics.load(ring.header, 0);
|
|
718
|
+
const readIndex = Atomics.load(ring.header, 1);
|
|
719
|
+
if (writeIndex - readIndex >= ring.capacity) {
|
|
720
|
+
Atomics.add(ring.header, 4, 1);
|
|
721
|
+
return false;
|
|
722
|
+
}
|
|
723
|
+
const offset = (writeIndex % ring.capacity) * SONARE_CLIP_PAGE_REQUEST_RING_RECORD_UINT32S;
|
|
724
|
+
Atomics.store(ring.records, offset, clipId);
|
|
725
|
+
Atomics.store(ring.records, offset + 1, pageIndex);
|
|
726
|
+
// Publish only after both scalar fields have been written.
|
|
727
|
+
Atomics.store(ring.header, 0, writeIndex + 1);
|
|
728
|
+
return true;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/** Drains the SPSC request ring on the main thread. Object creation is here, never in process(). */
|
|
732
|
+
export function readSonareClipPageRequestRingBuffer(
|
|
733
|
+
ring: SonareClipPageRequestRingBuffer,
|
|
734
|
+
): SonareClipPageRequestRingReadResult {
|
|
735
|
+
const readIndex = Atomics.load(ring.header, 1);
|
|
736
|
+
const writeIndex = Atomics.load(ring.header, 0);
|
|
737
|
+
const requests: SonareClipPageRequest[] = [];
|
|
738
|
+
for (let index = readIndex; index < writeIndex; index++) {
|
|
739
|
+
const offset = (index % ring.capacity) * SONARE_CLIP_PAGE_REQUEST_RING_RECORD_UINT32S;
|
|
740
|
+
requests.push({
|
|
741
|
+
clipId: Atomics.load(ring.records, offset),
|
|
742
|
+
pageIndex: Atomics.load(ring.records, offset + 1),
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
Atomics.store(ring.header, 1, writeIndex);
|
|
746
|
+
return { requests, dropped: Atomics.load(ring.header, 4) >>> 0 };
|
|
747
|
+
}
|
|
748
|
+
|
|
500
749
|
export function createSonareEngineCommandRingBuffer(capacity = 128): SonareEngineCommandRingBuffer {
|
|
501
750
|
const clampedCapacity = Math.max(1, Math.floor(capacity));
|
|
502
751
|
const sharedBuffer = new SharedArrayBuffer(
|
|
@@ -670,18 +919,56 @@ export function engineRingFromSharedBuffer(
|
|
|
670
919
|
};
|
|
671
920
|
}
|
|
672
921
|
|
|
922
|
+
export function clipPageRequestRingFromSharedBuffer(
|
|
923
|
+
sharedBuffer: SharedArrayBuffer,
|
|
924
|
+
fallbackCapacity?: number,
|
|
925
|
+
): SharedClipPageRequestRingWriter {
|
|
926
|
+
const headerBytes = SONARE_CLIP_PAGE_REQUEST_RING_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT;
|
|
927
|
+
const header = new Int32Array(sharedBuffer, 0, SONARE_CLIP_PAGE_REQUEST_RING_HEADER_INTS);
|
|
928
|
+
const existingCapacity = Atomics.load(header, 2);
|
|
929
|
+
const capacity = Math.max(1, Math.floor(existingCapacity || fallbackCapacity || 1));
|
|
930
|
+
const minBytes = sonareClipPageRequestRingBufferByteLength(capacity);
|
|
931
|
+
if (sharedBuffer.byteLength < minBytes) {
|
|
932
|
+
throw new Error('clipPageRequestSharedBuffer is too small for the requested ring capacity.');
|
|
933
|
+
}
|
|
934
|
+
Atomics.store(header, 2, capacity);
|
|
935
|
+
Atomics.store(header, 3, SONARE_CLIP_PAGE_REQUEST_RING_RECORD_UINT32S);
|
|
936
|
+
return {
|
|
937
|
+
header,
|
|
938
|
+
records: new Uint32Array(
|
|
939
|
+
sharedBuffer,
|
|
940
|
+
headerBytes,
|
|
941
|
+
capacity * SONARE_CLIP_PAGE_REQUEST_RING_RECORD_UINT32S,
|
|
942
|
+
),
|
|
943
|
+
capacity,
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
|
|
673
947
|
function recordOffset(index: number, capacity: number, recordBytes: number): number {
|
|
674
948
|
return (index % capacity) * recordBytes;
|
|
675
949
|
}
|
|
676
950
|
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
951
|
+
function toSafeInteger(value: number | bigint | undefined, fallback: number): number {
|
|
952
|
+
const resolved = typeof value === 'bigint' ? Number(value) : value;
|
|
953
|
+
if (resolved === undefined) {
|
|
954
|
+
return fallback;
|
|
680
955
|
}
|
|
681
|
-
if (
|
|
682
|
-
|
|
956
|
+
if (!Number.isSafeInteger(resolved)) {
|
|
957
|
+
throw new RangeError('64-bit ring values must be safe integers');
|
|
683
958
|
}
|
|
684
|
-
return
|
|
959
|
+
return resolved;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
/** Store a signed 64-bit safe integer as two 32-bit words without BigInt. */
|
|
963
|
+
export function writeInt64Words(view: DataView, offset: number, value: number): void {
|
|
964
|
+
const integer = toSafeInteger(value, 0);
|
|
965
|
+
view.setUint32(offset, integer >>> 0, true);
|
|
966
|
+
view.setInt32(offset + 4, Math.floor(integer / 0x100000000), true);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
/** Reconstruct a signed 64-bit safe integer stored by {@link writeInt64Words}. */
|
|
970
|
+
export function readInt64Words(view: DataView, offset: number): number {
|
|
971
|
+
return view.getInt32(offset + 4, true) * 0x100000000 + view.getUint32(offset, true);
|
|
685
972
|
}
|
|
686
973
|
|
|
687
974
|
function writeEngineCommandRecord(
|
|
@@ -691,21 +978,21 @@ function writeEngineCommandRecord(
|
|
|
691
978
|
): void {
|
|
692
979
|
view.setUint32(offset, command.type, true);
|
|
693
980
|
view.setUint32(offset + 4, command.targetId ?? 0, true);
|
|
694
|
-
view
|
|
981
|
+
writeInt64Words(view, offset + 8, toSafeInteger(command.sampleTime, -1));
|
|
695
982
|
// argFloat occupies a full 8-byte Float64 slot (replacing the old Float32 +
|
|
696
983
|
// 4-byte pad) so PPQ scalars carried here keep full double precision over the
|
|
697
984
|
// SAB transport, matching the engine's double-precision seek/loop contract.
|
|
698
985
|
view.setFloat64(offset + 16, command.argFloat ?? 0, true);
|
|
699
|
-
view
|
|
986
|
+
writeInt64Words(view, offset + 24, toSafeInteger(command.argInt, 0));
|
|
700
987
|
}
|
|
701
988
|
|
|
702
989
|
function readEngineCommandRecord(view: DataView, offset: number): SonareEngineCommandRecord {
|
|
703
990
|
return {
|
|
704
991
|
type: view.getUint32(offset, true),
|
|
705
992
|
targetId: view.getUint32(offset + 4, true),
|
|
706
|
-
sampleTime:
|
|
993
|
+
sampleTime: readInt64Words(view, offset + 8),
|
|
707
994
|
argFloat: view.getFloat64(offset + 16, true),
|
|
708
|
-
argInt:
|
|
995
|
+
argInt: readInt64Words(view, offset + 24),
|
|
709
996
|
};
|
|
710
997
|
}
|
|
711
998
|
|
|
@@ -716,21 +1003,22 @@ function writeEngineTelemetryRecord(
|
|
|
716
1003
|
): void {
|
|
717
1004
|
view.setUint32(offset, telemetry.type, true);
|
|
718
1005
|
view.setUint32(offset + 4, telemetry.error, true);
|
|
719
|
-
view
|
|
720
|
-
view
|
|
721
|
-
view
|
|
1006
|
+
writeInt64Words(view, offset + 8, telemetry.renderFrame);
|
|
1007
|
+
writeInt64Words(view, offset + 16, telemetry.timelineSample);
|
|
1008
|
+
writeInt64Words(view, offset + 24, telemetry.audibleTimelineSample);
|
|
722
1009
|
view.setInt32(offset + 32, telemetry.graphLatencySamplesQ8, true);
|
|
723
1010
|
view.setUint32(offset + 36, telemetry.value, true);
|
|
724
|
-
view.
|
|
1011
|
+
view.setUint32(offset + 40, 0, true);
|
|
1012
|
+
view.setUint32(offset + 44, 0, true);
|
|
725
1013
|
}
|
|
726
1014
|
|
|
727
1015
|
function readEngineTelemetryRecord(view: DataView, offset: number): SonareEngineTelemetryRecord {
|
|
728
1016
|
return {
|
|
729
1017
|
type: view.getUint32(offset, true),
|
|
730
1018
|
error: view.getUint32(offset + 4, true),
|
|
731
|
-
renderFrame:
|
|
732
|
-
timelineSample:
|
|
733
|
-
audibleTimelineSample:
|
|
1019
|
+
renderFrame: readInt64Words(view, offset + 8),
|
|
1020
|
+
timelineSample: readInt64Words(view, offset + 16),
|
|
1021
|
+
audibleTimelineSample: readInt64Words(view, offset + 24),
|
|
734
1022
|
graphLatencySamplesQ8: view.getInt32(offset + 32, true),
|
|
735
1023
|
value: view.getUint32(offset + 36, true),
|
|
736
1024
|
};
|
|
@@ -19,6 +19,7 @@ export class SonareRealtimeVoiceChangerWorkletProcessor {
|
|
|
19
19
|
// audio-thread process() never crosses an allocation boundary.
|
|
20
20
|
private monoInput: Float32Array;
|
|
21
21
|
private monoOutput: Float32Array;
|
|
22
|
+
private bufferGeneration: number;
|
|
22
23
|
// Planar heap-backed views (one Float32Array per channel) used by the
|
|
23
24
|
// multi-channel path. AudioWorklet inputs/outputs are already planar
|
|
24
25
|
// Float32Arrays, so this avoids the per-sample interleave/deinterleave
|
|
@@ -37,6 +38,7 @@ export class SonareRealtimeVoiceChangerWorkletProcessor {
|
|
|
37
38
|
// process() than blockSize, we clamp (see ensure*Capacity).
|
|
38
39
|
this.monoInput = this.changer.getMonoInputBuffer(this.blockSize);
|
|
39
40
|
this.monoOutput = this.changer.getMonoOutputBuffer(this.blockSize);
|
|
41
|
+
this.bufferGeneration = this.changer.bufferGeneration();
|
|
40
42
|
this.planarChannels = [];
|
|
41
43
|
if (this.channelCount > 1) {
|
|
42
44
|
for (let ch = 0; ch < this.channelCount; ch++) {
|
|
@@ -46,22 +48,16 @@ export class SonareRealtimeVoiceChangerWorkletProcessor {
|
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
/**
|
|
49
|
-
* Handles a control-plane message from the main thread.
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* DSP coefficient recomputation here. `setConfig` MUST NOT be deferred
|
|
53
|
-
* into `process()` because that would block the audio thread for longer
|
|
54
|
-
* than one render quantum (e.g. 128 samples / 44.1 kHz = ~2.9 ms).
|
|
51
|
+
* Handles a control-plane message from the main thread. AudioWorklet port
|
|
52
|
+
* handlers run on the same audio rendering thread as `process()`: this path
|
|
53
|
+
* therefore accepts only a pre-normalized POD and never parses JSON.
|
|
55
54
|
*/
|
|
56
55
|
receiveMessage(message: SonareRealtimeVoiceChangerMessage): void {
|
|
57
56
|
if (this.destroyed) {
|
|
58
57
|
return;
|
|
59
58
|
}
|
|
60
59
|
if (message.type === 'setConfig') {
|
|
61
|
-
|
|
62
|
-
// allocate and parse JSON internally; doing it here keeps `process()`
|
|
63
|
-
// realtime-safe.
|
|
64
|
-
this.changer.setConfig(message.preset);
|
|
60
|
+
this.changer.setPodConfig(message.config);
|
|
65
61
|
} else if (message.type === 'reset') {
|
|
66
62
|
this.changer.reset();
|
|
67
63
|
} else if (message.type === 'destroy') {
|
|
@@ -79,7 +75,10 @@ export class SonareRealtimeVoiceChangerWorkletProcessor {
|
|
|
79
75
|
// module is built ALLOW_MEMORY_GROWTH). Re-acquire them if detached
|
|
80
76
|
// (byteLength === 0) before touching them; in the common no-growth case this
|
|
81
77
|
// is a cheap branch with no allocation.
|
|
82
|
-
if (
|
|
78
|
+
if (
|
|
79
|
+
this.bufferGeneration !== this.changer.bufferGeneration() ||
|
|
80
|
+
this.monoInput.byteLength === 0
|
|
81
|
+
) {
|
|
83
82
|
this.reacquireBuffers();
|
|
84
83
|
}
|
|
85
84
|
|
|
@@ -100,10 +99,7 @@ export class SonareRealtimeVoiceChangerWorkletProcessor {
|
|
|
100
99
|
} else {
|
|
101
100
|
this.monoInput.fill(0, 0, frames);
|
|
102
101
|
}
|
|
103
|
-
this.changer.
|
|
104
|
-
this.monoInput.subarray(0, frames),
|
|
105
|
-
this.monoOutput.subarray(0, frames),
|
|
106
|
-
);
|
|
102
|
+
this.changer.processPreparedMono(frames);
|
|
107
103
|
output[0].set(this.monoOutput.subarray(0, frames));
|
|
108
104
|
return true;
|
|
109
105
|
}
|
|
@@ -156,6 +152,7 @@ export class SonareRealtimeVoiceChangerWorkletProcessor {
|
|
|
156
152
|
this.planarChannels[ch] = this.changer.getPlanarChannelBuffer(ch, this.blockSize);
|
|
157
153
|
}
|
|
158
154
|
}
|
|
155
|
+
this.bufferGeneration = this.changer.bufferGeneration();
|
|
159
156
|
}
|
|
160
157
|
|
|
161
158
|
/**
|
package/src/worklet.ts
CHANGED
|
@@ -4,19 +4,21 @@
|
|
|
4
4
|
// AudioWorkletGlobalScope cannot resolve sibling chunks), so the public surface
|
|
5
5
|
// is unchanged.
|
|
6
6
|
|
|
7
|
+
export type { OpfsClipStream, OpfsClipStreamOptions } from './clip_page_streamer';
|
|
8
|
+
export { attachOpfsClipStream } from './clip_page_streamer';
|
|
7
9
|
// With code-splitting disabled, the worklet bundle carries its own copy of the
|
|
8
10
|
// module singleton. Re-export the lifecycle so that realm can initialize its
|
|
9
11
|
// own wasm instance, independent of the main-thread `index` module.
|
|
10
12
|
export { init, isInitialized } from './index';
|
|
11
13
|
export { SonareEngine } from './worklet/engine';
|
|
12
14
|
export { SonareRealtimeEngineNode } from './worklet/engine-node';
|
|
13
|
-
|
|
14
15
|
export type { SonareEngineOptions } from './worklet/engine-options';
|
|
15
16
|
export { SonareRealtimeEngineWorkletProcessor } from './worklet/engine-processor';
|
|
16
17
|
export { registerSonareRealtimeEngineWorkletProcessor } from './worklet/engine-register';
|
|
17
18
|
export type {
|
|
18
19
|
SonareEngineCaptureRequestMessage,
|
|
19
20
|
SonareEngineCaptureResponseMessage,
|
|
21
|
+
SonareEngineClipPageRequestMessage,
|
|
20
22
|
SonareEngineSyncAutomationMessage,
|
|
21
23
|
SonareEngineSyncBuiltinInstrumentMessage,
|
|
22
24
|
SonareEngineSyncCaptureMessage,
|
|
@@ -61,8 +63,10 @@ export {
|
|
|
61
63
|
SonareWorkletProcessor,
|
|
62
64
|
} from './worklet/mixer-processor';
|
|
63
65
|
export {
|
|
66
|
+
createSonareClipPageRequestRingBuffer,
|
|
64
67
|
createSonareEngineCommandRingBuffer,
|
|
65
68
|
createSonareEngineTelemetryRingBuffer,
|
|
69
|
+
createSonareExternalMidiRingBuffer,
|
|
66
70
|
createSonareMeterRingBuffer,
|
|
67
71
|
createSonareScopeRingBuffer,
|
|
68
72
|
createSonareSpectrumRingBuffer,
|
|
@@ -70,18 +74,29 @@ export {
|
|
|
70
74
|
encodeFrameHi,
|
|
71
75
|
encodeFrameLo,
|
|
72
76
|
popSonareEngineCommandRingBuffer,
|
|
77
|
+
pushSonareClipPageRequestRingBuffer,
|
|
73
78
|
pushSonareEngineCommandRingBuffer,
|
|
79
|
+
pushSonareExternalMidiRingBuffer,
|
|
80
|
+
readSonareClipPageRequestRingBuffer,
|
|
74
81
|
readSonareEngineTelemetryRingBuffer,
|
|
82
|
+
readSonareExternalMidiRingBuffer,
|
|
75
83
|
readSonareMeterRingBuffer,
|
|
76
84
|
readSonareScopeRingBuffer,
|
|
77
85
|
readSonareSpectrumRingBuffer,
|
|
86
|
+
SONARE_CLIP_PAGE_REQUEST_RING_HEADER_INTS,
|
|
87
|
+
SONARE_CLIP_PAGE_REQUEST_RING_RECORD_UINT32S,
|
|
78
88
|
SONARE_ENGINE_COMMAND_RECORD_BYTES,
|
|
79
89
|
SONARE_ENGINE_RING_HEADER_INTS,
|
|
80
90
|
SONARE_ENGINE_TELEMETRY_RECORD_BYTES,
|
|
91
|
+
SONARE_EXTERNAL_MIDI_RING_HEADER_INTS,
|
|
92
|
+
SONARE_EXTERNAL_MIDI_RING_RECORD_UINT32S,
|
|
81
93
|
SONARE_METER_RING_HEADER_INTS,
|
|
82
94
|
SONARE_METER_RING_RECORD_FLOATS,
|
|
83
95
|
SONARE_SCOPE_RING_HEADER_INTS,
|
|
84
96
|
SONARE_SPECTRUM_RING_HEADER_INTS,
|
|
97
|
+
type SonareClipPageRequest,
|
|
98
|
+
type SonareClipPageRequestRingBuffer,
|
|
99
|
+
type SonareClipPageRequestRingReadResult,
|
|
85
100
|
type SonareEngineCommandRecord,
|
|
86
101
|
type SonareEngineCommandRingBuffer,
|
|
87
102
|
SonareEngineCommandType,
|
|
@@ -90,6 +105,8 @@ export {
|
|
|
90
105
|
type SonareEngineTelemetryRingBuffer,
|
|
91
106
|
type SonareEngineTelemetryRingReadResult,
|
|
92
107
|
SonareEngineTelemetryType,
|
|
108
|
+
type SonareExternalMidiRingBuffer,
|
|
109
|
+
type SonareExternalMidiRingReadResult,
|
|
93
110
|
type SonareMeterRingBuffer,
|
|
94
111
|
type SonareMeterRingReadResult,
|
|
95
112
|
type SonareScopeRingBuffer,
|
|
@@ -99,8 +116,10 @@ export {
|
|
|
99
116
|
type SonareWorkletMeterSnapshot,
|
|
100
117
|
type SonareWorkletScopeSnapshot,
|
|
101
118
|
type SonareWorkletSpectrumSnapshot,
|
|
119
|
+
sonareClipPageRequestRingBufferByteLength,
|
|
102
120
|
sonareEngineCommandRingBufferByteLength,
|
|
103
121
|
sonareEngineTelemetryRingBufferByteLength,
|
|
122
|
+
sonareExternalMidiRingBufferByteLength,
|
|
104
123
|
sonareMeterRingBufferByteLength,
|
|
105
124
|
sonareScopeRingBufferByteLength,
|
|
106
125
|
sonareSpectrumRingBufferByteLength,
|