@libraz/libsonare 1.4.1 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -20
- package/dist/index.d.ts +5416 -1
- package/dist/index.js +938 -583
- package/dist/index.js.map +1 -1
- package/dist/sonare.js +2 -2
- package/dist/sonare.wasm +0 -0
- package/dist/worklet.d.ts +1083 -5227
- package/dist/worklet.js +2683 -2451
- package/dist/worklet.js.map +1 -1
- package/package.json +4 -9
- package/src/clip_page_streamer.ts +298 -0
- package/src/effects_mastering.ts +85 -1089
- package/src/effects_transform.ts +286 -0
- package/src/effects_voice_change.ts +118 -0
- package/src/feature_music.ts +5 -2
- package/src/feature_spectrogram.ts +42 -2
- package/src/features.ts +1 -0
- package/src/index.ts +23 -0
- package/src/mastering_chain.ts +200 -0
- package/src/mastering_core.ts +248 -0
- package/src/mastering_dynamics.ts +105 -0
- package/src/mastering_repair.ts +161 -0
- package/src/mixer.ts +8 -0
- package/src/mixing_oneshot.ts +54 -0
- package/src/module_state.ts +1 -2
- package/src/project.ts +71 -1712
- package/src/project_class.ts +871 -0
- package/src/project_internal.ts +333 -0
- package/src/project_synth.ts +43 -0
- package/src/project_types.ts +570 -0
- package/src/public_types.ts +6 -1221
- package/src/public_types_acoustic.ts +115 -0
- package/src/public_types_mastering.ts +333 -0
- package/src/public_types_mixing.ts +97 -0
- package/src/public_types_music.ts +352 -0
- package/src/public_types_realtime.ts +163 -0
- package/src/public_types_spectral.ts +194 -0
- package/src/realtime_engine.ts +94 -0
- package/src/sonare.js.d.ts +117 -38
- package/src/stream_analyzer.ts +3 -0
- package/src/stream_types.ts +4 -0
- package/src/worklet/engine-automation.ts +73 -0
- package/src/worklet/engine-capture-facade.ts +80 -0
- package/src/worklet/engine-clips.ts +71 -0
- package/src/worklet/engine-markers.ts +93 -0
- package/src/worklet/engine-mixer-facade.ts +186 -0
- package/src/worklet/engine-node.ts +451 -0
- package/src/worklet/engine-offline.ts +162 -0
- package/src/worklet/engine-options.ts +13 -0
- package/src/worklet/engine-parameter-facade.ts +172 -0
- package/src/worklet/engine-processor.ts +764 -0
- package/src/worklet/engine-register.ts +136 -0
- package/src/worklet/engine-strips.ts +315 -0
- package/src/worklet/engine-sync.ts +94 -0
- package/src/worklet/engine-tempo-facade.ts +141 -0
- package/src/worklet/engine.ts +998 -0
- package/src/worklet/guards.ts +14 -1
- package/src/worklet/messages.ts +60 -20
- package/src/worklet/mixer-processor.ts +368 -0
- package/src/worklet/protocol.ts +3 -0
- package/src/worklet/voice-changer-processor.ts +246 -0
- package/src/worklet.ts +20 -3549
- package/dist/sonare-rt-module.js +0 -2
- package/dist/sonare-rt.js +0 -2
- package/dist/sonare-rt.wasm +0 -0
- package/src/sonare-rt.d.ts +0 -93
package/src/worklet/guards.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
SonareEngineTransportRequestMessage,
|
|
6
6
|
SonareEngineTransportResponseMessage,
|
|
7
7
|
SonareRealtimeVoiceChangerMessage,
|
|
8
|
+
SonareWorkletExternalMidiMessage,
|
|
8
9
|
SonareWorkletMessage,
|
|
9
10
|
} from './messages';
|
|
10
11
|
import {
|
|
@@ -49,6 +50,7 @@ export function isEngineSyncMessage(value: unknown): value is SonareEngineSyncMe
|
|
|
49
50
|
value.type === 'syncMasterStripInsertBypassed' ||
|
|
50
51
|
value.type === 'syncTrackStripInsertParamByName' ||
|
|
51
52
|
value.type === 'syncMasterStripInsertParamByName' ||
|
|
53
|
+
value.type === 'syncBusStripInsertParamByName' ||
|
|
52
54
|
value.type === 'syncTrackStripPan' ||
|
|
53
55
|
value.type === 'syncTrackStripPanLaw' ||
|
|
54
56
|
value.type === 'syncTrackStripPanMode' ||
|
|
@@ -58,10 +60,15 @@ export function isEngineSyncMessage(value: unknown): value is SonareEngineSyncMe
|
|
|
58
60
|
value.type === 'syncSynthInstrument' ||
|
|
59
61
|
value.type === 'syncSf2Instrument' ||
|
|
60
62
|
value.type === 'syncLoadSoundFont' ||
|
|
63
|
+
value.type === 'syncMidiFx' ||
|
|
64
|
+
value.type === 'syncClearMidiFx' ||
|
|
61
65
|
value.type === 'syncMidiNoteOn' ||
|
|
62
66
|
value.type === 'syncMidiNoteOff' ||
|
|
63
67
|
value.type === 'syncMidiCc' ||
|
|
64
|
-
value.type === '
|
|
68
|
+
value.type === 'syncMidiSysex' ||
|
|
69
|
+
value.type === 'syncMidiPanic' ||
|
|
70
|
+
value.type === 'syncMidiDestinationExternal' ||
|
|
71
|
+
value.type === 'syncExternalMidiClock'
|
|
65
72
|
);
|
|
66
73
|
}
|
|
67
74
|
|
|
@@ -131,6 +138,12 @@ export function isEngineTelemetryRecord(value: unknown): value is SonareEngineTe
|
|
|
131
138
|
);
|
|
132
139
|
}
|
|
133
140
|
|
|
141
|
+
export function isExternalMidiBatchMessage(
|
|
142
|
+
value: unknown,
|
|
143
|
+
): value is SonareWorkletExternalMidiMessage {
|
|
144
|
+
return isRecord(value) && value.type === 'externalMidi' && Array.isArray(value.events);
|
|
145
|
+
}
|
|
146
|
+
|
|
134
147
|
export function isMeterSnapshot(value: unknown): value is SonareWorkletMeterSnapshot {
|
|
135
148
|
return (
|
|
136
149
|
isRecord(value) &&
|
package/src/worklet/messages.ts
CHANGED
|
@@ -13,7 +13,6 @@ import type {
|
|
|
13
13
|
RealtimeVoiceChangerConfigInput,
|
|
14
14
|
} from '../index';
|
|
15
15
|
import type { AutomationCurve } from '../public_types';
|
|
16
|
-
import type { SonareRtModule } from '../sonare-rt';
|
|
17
16
|
import type {
|
|
18
17
|
SonareEngineCommandRecord,
|
|
19
18
|
SonareEngineTelemetryRecord,
|
|
@@ -36,9 +35,6 @@ export interface SonareWorkletProcessorOptions {
|
|
|
36
35
|
}
|
|
37
36
|
|
|
38
37
|
export interface SonareRealtimeEngineWorkletProcessorOptions {
|
|
39
|
-
runtimeTarget?: 'embind' | 'sonare-rt';
|
|
40
|
-
rtModuleUrl?: string;
|
|
41
|
-
rtWasmBinary?: ArrayBuffer | Uint8Array;
|
|
42
38
|
wasmBinary?: ArrayBuffer | Uint8Array;
|
|
43
39
|
initialSyncMessages?: SonareEngineSyncMessage[];
|
|
44
40
|
initialCommands?: SonareEngineCommandRecord[];
|
|
@@ -88,7 +84,7 @@ export type SonareRealtimeVoiceChangerMessage =
|
|
|
88
84
|
|
|
89
85
|
export interface SonareRealtimeEngineNodeCapabilities {
|
|
90
86
|
mode: 'sab' | 'postMessage';
|
|
91
|
-
runtimeTarget: 'embind'
|
|
87
|
+
runtimeTarget: 'embind';
|
|
92
88
|
sharedArrayBuffer: boolean;
|
|
93
89
|
atomics: boolean;
|
|
94
90
|
audioWorklet: boolean;
|
|
@@ -103,7 +99,6 @@ export interface SonareRealtimeEngineNodeOptions
|
|
|
103
99
|
extends SonareRealtimeEngineWorkletProcessorOptions {
|
|
104
100
|
processorName?: string;
|
|
105
101
|
moduleUrl?: string | URL;
|
|
106
|
-
rtModuleUrl?: string;
|
|
107
102
|
mode?: 'auto' | 'sab' | 'postMessage';
|
|
108
103
|
engineAbiVersion?: number;
|
|
109
104
|
expectedEngineAbiVersion?: number;
|
|
@@ -115,18 +110,6 @@ export interface SonareRealtimeEngineNodeOptions
|
|
|
115
110
|
) => AudioWorkletNode;
|
|
116
111
|
}
|
|
117
112
|
|
|
118
|
-
export interface SonareRtRealtimeEngineRuntimeOptions {
|
|
119
|
-
module: SonareRtModule;
|
|
120
|
-
memory: WebAssembly.Memory;
|
|
121
|
-
sampleRate?: number;
|
|
122
|
-
blockSize?: number;
|
|
123
|
-
channelCount?: number;
|
|
124
|
-
commandSharedBuffer?: SharedArrayBuffer;
|
|
125
|
-
commandRingCapacity?: number;
|
|
126
|
-
telemetrySharedBuffer?: SharedArrayBuffer;
|
|
127
|
-
telemetryRingCapacity?: number;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
113
|
export interface SonareEngineTransportFacade {
|
|
131
114
|
play(sampleTime?: number): boolean;
|
|
132
115
|
stop(sampleTime?: number): boolean;
|
|
@@ -161,9 +144,27 @@ export type SonareWorkletMessage =
|
|
|
161
144
|
| SonareWorkletSetMeterIntervalMessage
|
|
162
145
|
| SonareWorkletDestroyMessage;
|
|
163
146
|
|
|
147
|
+
/** One external-MIDI event delivered to the main thread, lowered to MIDI 1.0
|
|
148
|
+
* bytes ready for a Web MIDI output port. */
|
|
149
|
+
export interface SonareWorkletExternalMidiEvent {
|
|
150
|
+
/** Originating track lane, or 0xFFFFFFFF for transport/clock bytes. */
|
|
151
|
+
destinationId: number;
|
|
152
|
+
/** Sample position within the producing block. */
|
|
153
|
+
renderFrame: number;
|
|
154
|
+
/** MIDI 1.0 status + data bytes (1..3 entries). */
|
|
155
|
+
bytes: number[];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Batch of external-MIDI events posted from the worklet once per render block. */
|
|
159
|
+
export interface SonareWorkletExternalMidiMessage {
|
|
160
|
+
type: 'externalMidi';
|
|
161
|
+
events: SonareWorkletExternalMidiEvent[];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
164
|
export type SonareWorkletTransportMessage =
|
|
165
165
|
| SonareWorkletMeterSnapshot
|
|
166
166
|
| SonareWorkletSpectrumSnapshot
|
|
167
|
+
| SonareWorkletExternalMidiMessage
|
|
167
168
|
| SonareEngineTelemetryRecord;
|
|
168
169
|
|
|
169
170
|
export interface WorkletTransport {
|
|
@@ -310,6 +311,14 @@ export interface SonareEngineSyncMasterStripInsertParamByNameMessage {
|
|
|
310
311
|
value: number;
|
|
311
312
|
}
|
|
312
313
|
|
|
314
|
+
export interface SonareEngineSyncBusStripInsertParamByNameMessage {
|
|
315
|
+
type: 'syncBusStripInsertParamByName';
|
|
316
|
+
busId: number;
|
|
317
|
+
insertIndex: number;
|
|
318
|
+
paramName: string;
|
|
319
|
+
value: number;
|
|
320
|
+
}
|
|
321
|
+
|
|
313
322
|
export interface SonareEngineSyncTrackStripPanMessage {
|
|
314
323
|
type: 'syncTrackStripPan';
|
|
315
324
|
trackId: number;
|
|
@@ -364,6 +373,13 @@ export interface SonareEngineSyncLoadSoundFontMessage {
|
|
|
364
373
|
data: Uint8Array;
|
|
365
374
|
}
|
|
366
375
|
|
|
376
|
+
export interface SonareEngineSyncMidiFxMessage {
|
|
377
|
+
type: 'syncMidiFx' | 'syncClearMidiFx';
|
|
378
|
+
destinationId: number;
|
|
379
|
+
/** Engine MIDI-FX config JSON; present for 'syncMidiFx', absent for a clear. */
|
|
380
|
+
configJson?: string;
|
|
381
|
+
}
|
|
382
|
+
|
|
367
383
|
export interface SonareEngineSyncMidiNoteMessage {
|
|
368
384
|
type: 'syncMidiNoteOn' | 'syncMidiNoteOff';
|
|
369
385
|
destinationId: number;
|
|
@@ -384,16 +400,35 @@ export interface SonareEngineSyncMidiCcMessage {
|
|
|
384
400
|
renderFrame: number;
|
|
385
401
|
}
|
|
386
402
|
|
|
403
|
+
export interface SonareEngineSyncMidiSysexMessage {
|
|
404
|
+
type: 'syncMidiSysex';
|
|
405
|
+
destinationId: number;
|
|
406
|
+
data: Uint8Array;
|
|
407
|
+
renderFrame: number;
|
|
408
|
+
}
|
|
409
|
+
|
|
387
410
|
export interface SonareEngineSyncMidiPanicMessage {
|
|
388
411
|
type: 'syncMidiPanic';
|
|
389
412
|
renderFrame: number;
|
|
390
413
|
}
|
|
391
414
|
|
|
415
|
+
export interface SonareEngineSyncMidiDestinationExternalMessage {
|
|
416
|
+
type: 'syncMidiDestinationExternal';
|
|
417
|
+
destinationId: number;
|
|
418
|
+
external: boolean;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
export interface SonareEngineSyncExternalMidiClockMessage {
|
|
422
|
+
type: 'syncExternalMidiClock';
|
|
423
|
+
enabled: boolean;
|
|
424
|
+
}
|
|
425
|
+
|
|
392
426
|
export type SonareEngineInstrumentSyncMessage =
|
|
393
427
|
| SonareEngineSyncBuiltinInstrumentMessage
|
|
394
428
|
| SonareEngineSyncSynthInstrumentMessage
|
|
395
429
|
| SonareEngineSyncSf2InstrumentMessage
|
|
396
|
-
| SonareEngineSyncLoadSoundFontMessage
|
|
430
|
+
| SonareEngineSyncLoadSoundFontMessage
|
|
431
|
+
| SonareEngineSyncMidiFxMessage;
|
|
397
432
|
|
|
398
433
|
export type SonareEngineSyncMessage =
|
|
399
434
|
| SonareEngineSyncClipsMessage
|
|
@@ -411,6 +446,7 @@ export type SonareEngineSyncMessage =
|
|
|
411
446
|
| SonareEngineSyncMasterStripInsertBypassedMessage
|
|
412
447
|
| SonareEngineSyncTrackStripInsertParamByNameMessage
|
|
413
448
|
| SonareEngineSyncMasterStripInsertParamByNameMessage
|
|
449
|
+
| SonareEngineSyncBusStripInsertParamByNameMessage
|
|
414
450
|
| SonareEngineSyncTrackStripPanMessage
|
|
415
451
|
| SonareEngineSyncTrackStripPanLawMessage
|
|
416
452
|
| SonareEngineSyncTrackStripPanModeMessage
|
|
@@ -420,9 +456,13 @@ export type SonareEngineSyncMessage =
|
|
|
420
456
|
| SonareEngineSyncSynthInstrumentMessage
|
|
421
457
|
| SonareEngineSyncSf2InstrumentMessage
|
|
422
458
|
| SonareEngineSyncLoadSoundFontMessage
|
|
459
|
+
| SonareEngineSyncMidiFxMessage
|
|
423
460
|
| SonareEngineSyncMidiNoteMessage
|
|
424
461
|
| SonareEngineSyncMidiCcMessage
|
|
425
|
-
|
|
|
462
|
+
| SonareEngineSyncMidiSysexMessage
|
|
463
|
+
| SonareEngineSyncMidiPanicMessage
|
|
464
|
+
| SonareEngineSyncMidiDestinationExternalMessage
|
|
465
|
+
| SonareEngineSyncExternalMidiClockMessage;
|
|
426
466
|
|
|
427
467
|
export interface WorkletPort {
|
|
428
468
|
postMessage?: (message: unknown, transfer?: Transferable[]) => void;
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import type { MixerRealtimeBuffer } from '../index';
|
|
2
|
+
import { Mixer } from '../index';
|
|
3
|
+
import type { WorkletInput, WorkletOutput } from './audio_types';
|
|
4
|
+
import { isWorkletMessage } from './guards';
|
|
5
|
+
import type {
|
|
6
|
+
SonareWorkletMessage,
|
|
7
|
+
SonareWorkletProcessorOptions,
|
|
8
|
+
WorkletPort,
|
|
9
|
+
WorkletTransport,
|
|
10
|
+
} from './messages';
|
|
11
|
+
import {
|
|
12
|
+
encodeFrameHi,
|
|
13
|
+
encodeFrameLo,
|
|
14
|
+
magnitudeToDb,
|
|
15
|
+
meterRingFromSharedBuffer,
|
|
16
|
+
type SharedMeterRingWriter,
|
|
17
|
+
type SharedSpectrumRingWriter,
|
|
18
|
+
SONARE_METER_RING_RECORD_FLOATS,
|
|
19
|
+
type SonareWorkletMeterSnapshot,
|
|
20
|
+
type SonareWorkletSpectrumSnapshot,
|
|
21
|
+
spectrumRingFromSharedBuffer,
|
|
22
|
+
toDb,
|
|
23
|
+
} from './protocol';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* AudioWorklet-style mixer bridge backed by the package's single `sonare.wasm`.
|
|
27
|
+
*
|
|
28
|
+
* The WASM module must already be initialized via `init()` before constructing
|
|
29
|
+
* this bridge. Each AudioWorklet input is treated as one stereo strip:
|
|
30
|
+
* `inputs[strip][0]` is left and `inputs[strip][1]` is right. Missing channels
|
|
31
|
+
* are replaced with preallocated silence.
|
|
32
|
+
*/
|
|
33
|
+
export class SonareWorkletProcessor {
|
|
34
|
+
readonly sampleRate: number;
|
|
35
|
+
readonly blockSize: number;
|
|
36
|
+
private mixer: Mixer;
|
|
37
|
+
private realtime: MixerRealtimeBuffer;
|
|
38
|
+
private closed = false;
|
|
39
|
+
private processedFrames = 0;
|
|
40
|
+
private lastMeterFrame = 0;
|
|
41
|
+
private meterIntervalFrames: number;
|
|
42
|
+
private spectrumIntervalFrames: number;
|
|
43
|
+
private lastSpectrumFrame = 0;
|
|
44
|
+
private transport?: WorkletTransport;
|
|
45
|
+
private meterRing?: SharedMeterRingWriter;
|
|
46
|
+
private spectrumRing?: SharedSpectrumRingWriter;
|
|
47
|
+
private spectrumBands: Float32Array;
|
|
48
|
+
|
|
49
|
+
constructor(options: SonareWorkletProcessorOptions, transport?: WorkletTransport) {
|
|
50
|
+
if (!options.sceneJson) {
|
|
51
|
+
throw new Error('sceneJson is required.');
|
|
52
|
+
}
|
|
53
|
+
this.sampleRate = options.sampleRate ?? 48000;
|
|
54
|
+
this.blockSize = options.blockSize ?? 128;
|
|
55
|
+
this.meterIntervalFrames = Math.max(0, Math.floor(options.meterIntervalFrames ?? 2048));
|
|
56
|
+
this.spectrumIntervalFrames = Math.max(0, Math.floor(options.spectrumIntervalFrames ?? 0));
|
|
57
|
+
this.transport = transport;
|
|
58
|
+
this.meterIntervalFrames = Math.max(0, Math.floor(options.meterIntervalFrames ?? 2048));
|
|
59
|
+
this.meterRing = options.meterSharedBuffer
|
|
60
|
+
? meterRingFromSharedBuffer(options.meterSharedBuffer, options.meterRingCapacity)
|
|
61
|
+
: undefined;
|
|
62
|
+
this.spectrumRing = options.spectrumSharedBuffer
|
|
63
|
+
? spectrumRingFromSharedBuffer(
|
|
64
|
+
options.spectrumSharedBuffer,
|
|
65
|
+
options.spectrumRingCapacity,
|
|
66
|
+
options.spectrumBands,
|
|
67
|
+
)
|
|
68
|
+
: undefined;
|
|
69
|
+
const spectrumBandCount = this.spectrumRing?.bands ?? Math.max(1, options.spectrumBands ?? 16);
|
|
70
|
+
this.spectrumBands = new Float32Array(spectrumBandCount);
|
|
71
|
+
this.mixer = Mixer.fromSceneJson(options.sceneJson, this.sampleRate, this.blockSize);
|
|
72
|
+
this.mixer.compile();
|
|
73
|
+
const sceneStripCount = this.mixer.stripCount();
|
|
74
|
+
const stripCount = options.stripCount ?? sceneStripCount;
|
|
75
|
+
if (stripCount !== sceneStripCount) {
|
|
76
|
+
throw new Error('stripCount must match the scene strip count.');
|
|
77
|
+
}
|
|
78
|
+
this.realtime = this.mixer.createRealtimeBuffer();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
process(inputs: WorkletInput, outputs: WorkletOutput): boolean {
|
|
82
|
+
if (this.closed) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
const output = outputs[0];
|
|
86
|
+
const leftOut = output?.[0];
|
|
87
|
+
const rightOut = output?.[1];
|
|
88
|
+
if (!leftOut) {
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
const frames = leftOut.length;
|
|
92
|
+
// The mixer's realtime heap buffers are sized to blockSize. A render quantum
|
|
93
|
+
// that differs from blockSize (e.g. a future browser using a quantum other
|
|
94
|
+
// than 128, or a misconfigured blockSize) must NOT return false here:
|
|
95
|
+
// returning false permanently terminates the AudioWorkletProcessor and
|
|
96
|
+
// silently kills the node mid-stream. Instead degrade gracefully by
|
|
97
|
+
// processing min(frames, blockSize) and zero-filling any remainder.
|
|
98
|
+
const usable = Math.min(frames, this.blockSize);
|
|
99
|
+
|
|
100
|
+
for (let strip = 0; strip < this.realtime.leftInputs.length; strip++) {
|
|
101
|
+
const input = inputs[strip];
|
|
102
|
+
const left = input?.[0];
|
|
103
|
+
const right = input?.[1];
|
|
104
|
+
const leftTarget = this.realtime.leftInputs[strip];
|
|
105
|
+
const rightTarget = this.realtime.rightInputs[strip];
|
|
106
|
+
if (left && left.length >= usable) {
|
|
107
|
+
leftTarget.set(left.subarray(0, usable));
|
|
108
|
+
if (right && right.length >= usable) {
|
|
109
|
+
rightTarget.set(right.subarray(0, usable));
|
|
110
|
+
} else {
|
|
111
|
+
rightTarget.set(left.subarray(0, usable));
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
leftTarget.fill(0);
|
|
115
|
+
rightTarget.fill(0);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
this.realtime.process(usable);
|
|
120
|
+
if (usable === frames) {
|
|
121
|
+
leftOut.set(this.realtime.outLeft.subarray(0, usable));
|
|
122
|
+
if (rightOut) {
|
|
123
|
+
rightOut.set(this.realtime.outRight.subarray(0, usable));
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
// frames > blockSize: fill the produced part and zero the remaining tail.
|
|
127
|
+
leftOut.fill(0);
|
|
128
|
+
leftOut.set(this.realtime.outLeft.subarray(0, usable));
|
|
129
|
+
if (rightOut) {
|
|
130
|
+
rightOut.fill(0);
|
|
131
|
+
rightOut.set(this.realtime.outRight.subarray(0, usable));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
this.processedFrames += usable;
|
|
135
|
+
this.publishMeter(
|
|
136
|
+
this.realtime.outLeft.subarray(0, usable),
|
|
137
|
+
this.realtime.outRight.subarray(0, usable),
|
|
138
|
+
);
|
|
139
|
+
this.publishSpectrum(
|
|
140
|
+
this.realtime.outLeft.subarray(0, usable),
|
|
141
|
+
this.realtime.outRight.subarray(0, usable),
|
|
142
|
+
);
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
receiveMessage(message: SonareWorkletMessage): void {
|
|
147
|
+
if (this.closed) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (message.type === 'destroy') {
|
|
151
|
+
this.destroy();
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (message.type === 'setMeterInterval') {
|
|
155
|
+
this.meterIntervalFrames = Math.max(0, Math.floor(message.frames));
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (message.type === 'scheduleInsertAutomation') {
|
|
159
|
+
this.mixer.scheduleInsertAutomation(
|
|
160
|
+
message.stripIndex,
|
|
161
|
+
message.insertIndex,
|
|
162
|
+
message.paramId,
|
|
163
|
+
message.samplePos ?? this.processedFrames,
|
|
164
|
+
message.value,
|
|
165
|
+
message.curve ?? 'linear',
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
destroy(): void {
|
|
171
|
+
if (!this.closed) {
|
|
172
|
+
this.mixer.delete();
|
|
173
|
+
this.closed = true;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private publishMeter(left: Float32Array, right: Float32Array): void {
|
|
178
|
+
if (!this.transport || this.meterIntervalFrames <= 0) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (this.processedFrames - this.lastMeterFrame < this.meterIntervalFrames) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
this.lastMeterFrame = this.processedFrames;
|
|
185
|
+
|
|
186
|
+
let peakL = 0;
|
|
187
|
+
let peakR = 0;
|
|
188
|
+
let sumL = 0;
|
|
189
|
+
let sumR = 0;
|
|
190
|
+
let sumLR = 0;
|
|
191
|
+
for (let i = 0; i < left.length; i++) {
|
|
192
|
+
const l = left[i] ?? 0;
|
|
193
|
+
const r = right[i] ?? 0;
|
|
194
|
+
const absL = Math.abs(l);
|
|
195
|
+
const absR = Math.abs(r);
|
|
196
|
+
if (absL > peakL) {
|
|
197
|
+
peakL = absL;
|
|
198
|
+
}
|
|
199
|
+
if (absR > peakR) {
|
|
200
|
+
peakR = absR;
|
|
201
|
+
}
|
|
202
|
+
sumL += l * l;
|
|
203
|
+
sumR += r * r;
|
|
204
|
+
sumLR += l * r;
|
|
205
|
+
}
|
|
206
|
+
const rmsL = Math.sqrt(sumL / Math.max(1, left.length));
|
|
207
|
+
const rmsR = Math.sqrt(sumR / Math.max(1, right.length));
|
|
208
|
+
const denominator = Math.sqrt(sumL * sumR);
|
|
209
|
+
const meter: SonareWorkletMeterSnapshot = {
|
|
210
|
+
type: 'meter',
|
|
211
|
+
targetId: 0,
|
|
212
|
+
frame: this.processedFrames,
|
|
213
|
+
peakDbL: toDb(peakL),
|
|
214
|
+
peakDbR: toDb(peakR),
|
|
215
|
+
rmsDbL: toDb(rmsL),
|
|
216
|
+
rmsDbR: toDb(rmsR),
|
|
217
|
+
correlation: denominator > 0 ? sumLR / denominator : 0,
|
|
218
|
+
truePeakDbL: toDb(peakL),
|
|
219
|
+
truePeakDbR: toDb(peakR),
|
|
220
|
+
momentaryLufs: Number.NaN,
|
|
221
|
+
shortTermLufs: Number.NaN,
|
|
222
|
+
integratedLufs: Number.NaN,
|
|
223
|
+
gainReductionDb: Number.NaN,
|
|
224
|
+
};
|
|
225
|
+
this.transport.onMeter?.(meter);
|
|
226
|
+
if (this.meterRing) {
|
|
227
|
+
this.writeMeterRing(meter);
|
|
228
|
+
} else {
|
|
229
|
+
this.transport.postMessage?.(meter);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private writeMeterRing(meter: SonareWorkletMeterSnapshot): void {
|
|
234
|
+
const ring = this.meterRing;
|
|
235
|
+
if (!ring) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const writeIndex = Atomics.load(ring.header, 0);
|
|
239
|
+
const offset = (writeIndex % ring.capacity) * SONARE_METER_RING_RECORD_FLOATS;
|
|
240
|
+
ring.records[offset] = encodeFrameLo(meter.frame);
|
|
241
|
+
ring.records[offset + 1] = encodeFrameHi(meter.frame);
|
|
242
|
+
ring.records[offset + 2] = meter.targetId;
|
|
243
|
+
ring.records[offset + 3] = meter.peakDbL;
|
|
244
|
+
ring.records[offset + 4] = meter.peakDbR;
|
|
245
|
+
ring.records[offset + 5] = meter.rmsDbL;
|
|
246
|
+
ring.records[offset + 6] = meter.rmsDbR;
|
|
247
|
+
ring.records[offset + 7] = meter.correlation;
|
|
248
|
+
ring.records[offset + 8] = meter.truePeakDbL;
|
|
249
|
+
ring.records[offset + 9] = meter.truePeakDbR;
|
|
250
|
+
ring.records[offset + 10] = meter.momentaryLufs;
|
|
251
|
+
ring.records[offset + 11] = meter.shortTermLufs;
|
|
252
|
+
ring.records[offset + 12] = meter.integratedLufs;
|
|
253
|
+
ring.records[offset + 13] = meter.gainReductionDb;
|
|
254
|
+
Atomics.store(ring.header, 0, writeIndex + 1);
|
|
255
|
+
// writeIndex is a free-running monotonic counter, so an overflow guard here
|
|
256
|
+
// would fire on essentially every write past the first `capacity` records
|
|
257
|
+
// and store an ever-growing value, not a dropped-record count. Readers
|
|
258
|
+
// already detect silent overrun via firstReadable = max(readIndex,
|
|
259
|
+
// writeIndex - capacity), so header slot 3 is left at its initial 0.
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
private publishSpectrum(left: Float32Array, right: Float32Array): void {
|
|
263
|
+
if (this.spectrumIntervalFrames <= 0) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (this.processedFrames - this.lastSpectrumFrame < this.spectrumIntervalFrames) {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
this.lastSpectrumFrame = this.processedFrames;
|
|
270
|
+
this.computeSpectrum(left, right);
|
|
271
|
+
if (this.spectrumRing) {
|
|
272
|
+
this.writeSpectrumRing(this.processedFrames, this.spectrumBands);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const spectrum: SonareWorkletSpectrumSnapshot = {
|
|
276
|
+
type: 'spectrum',
|
|
277
|
+
frame: this.processedFrames,
|
|
278
|
+
bands: new Float32Array(this.spectrumBands),
|
|
279
|
+
};
|
|
280
|
+
this.transport?.onSpectrum?.(spectrum);
|
|
281
|
+
this.transport?.postMessage?.(spectrum);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
private computeSpectrum(left: Float32Array, right: Float32Array): void {
|
|
285
|
+
// Coarse per-render-quantum band energy, NOT a full FFT analyzer: each band
|
|
286
|
+
// is a single-bin DFT (bin = band + 1) evaluated over the current block of n
|
|
287
|
+
// samples. Bins at or above the block Nyquist (band + 1 > floor(n / 2))
|
|
288
|
+
// alias, so the evaluated band count is clamped to floor(n / 2) and any
|
|
289
|
+
// higher bands are pinned to the silence floor. Bin resolution is therefore
|
|
290
|
+
// tied to the render quantum (typically 128 samples); treat the output as a
|
|
291
|
+
// rough spectral tilt, not a precise spectrum.
|
|
292
|
+
const n = Math.max(1, Math.min(left.length, right.length));
|
|
293
|
+
const maxBand = Math.floor(n / 2);
|
|
294
|
+
for (let band = 0; band < this.spectrumBands.length; band++) {
|
|
295
|
+
if (band >= maxBand) {
|
|
296
|
+
this.spectrumBands[band] = magnitudeToDb(0);
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
const bin = band + 1;
|
|
300
|
+
let real = 0;
|
|
301
|
+
let imag = 0;
|
|
302
|
+
for (let i = 0; i < n; i++) {
|
|
303
|
+
const sample = 0.5 * ((left[i] ?? 0) + (right[i] ?? 0));
|
|
304
|
+
const phase = (-2 * Math.PI * bin * i) / n;
|
|
305
|
+
real += sample * Math.cos(phase);
|
|
306
|
+
imag += sample * Math.sin(phase);
|
|
307
|
+
}
|
|
308
|
+
this.spectrumBands[band] = magnitudeToDb((2 * Math.hypot(real, imag)) / n);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
private writeSpectrumRing(frame: number, bands: Float32Array): void {
|
|
313
|
+
const ring = this.spectrumRing;
|
|
314
|
+
if (!ring) {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const writeIndex = Atomics.load(ring.header, 0);
|
|
318
|
+
const offset = (writeIndex % ring.capacity) * ring.recordFloats;
|
|
319
|
+
ring.records[offset] = encodeFrameLo(frame);
|
|
320
|
+
ring.records[offset + 1] = encodeFrameHi(frame);
|
|
321
|
+
ring.records[offset + 2] = bands.length;
|
|
322
|
+
ring.records.set(bands.subarray(0, ring.bands), offset + 3);
|
|
323
|
+
Atomics.store(ring.header, 0, writeIndex + 1);
|
|
324
|
+
// See writeMeterRing: header slot 4 (the spectrum-ring overflow slot) is
|
|
325
|
+
// left at its initial 0; readers detect silent overrun via the
|
|
326
|
+
// firstReadable = max(readIndex, writeIndex - capacity) clamp. (Slot 3 here
|
|
327
|
+
// holds the band count and is still written at ring creation.)
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export function registerSonareWorkletProcessor(name = 'sonare-worklet-processor'): void {
|
|
332
|
+
const scope = globalThis as unknown as {
|
|
333
|
+
AudioWorkletProcessor?: new () => object;
|
|
334
|
+
registerProcessor?: (processorName: string, processorCtor: unknown) => void;
|
|
335
|
+
};
|
|
336
|
+
if (!scope.AudioWorkletProcessor || !scope.registerProcessor) {
|
|
337
|
+
throw new Error('AudioWorkletProcessor is not available in this context.');
|
|
338
|
+
}
|
|
339
|
+
const Base = scope.AudioWorkletProcessor;
|
|
340
|
+
class RegisteredSonareWorkletProcessor extends Base {
|
|
341
|
+
private bridge: SonareWorkletProcessor;
|
|
342
|
+
readonly port?: WorkletPort;
|
|
343
|
+
|
|
344
|
+
constructor(options?: { processorOptions?: SonareWorkletProcessorOptions }) {
|
|
345
|
+
super();
|
|
346
|
+
const port = this.port;
|
|
347
|
+
this.bridge = new SonareWorkletProcessor(options?.processorOptions ?? { sceneJson: '' }, {
|
|
348
|
+
postMessage: (message) => port?.postMessage?.(message),
|
|
349
|
+
});
|
|
350
|
+
const onMessage = (event: { data: unknown }) => {
|
|
351
|
+
if (isWorkletMessage(event.data)) {
|
|
352
|
+
this.bridge.receiveMessage(event.data);
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
if (port?.addEventListener) {
|
|
356
|
+
port.addEventListener('message', onMessage);
|
|
357
|
+
port.start?.();
|
|
358
|
+
} else if (port) {
|
|
359
|
+
port.onmessage = onMessage;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
process(inputs: WorkletInput, outputs: WorkletOutput): boolean {
|
|
364
|
+
return this.bridge.process(inputs, outputs);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
scope.registerProcessor(name, RegisteredSonareWorkletProcessor);
|
|
368
|
+
}
|
package/src/worklet/protocol.ts
CHANGED
|
@@ -119,6 +119,9 @@ export enum SonareEngineTelemetryError {
|
|
|
119
119
|
AutomationBindTargetOverflow = 11,
|
|
120
120
|
StaleAutomationLanes = 12,
|
|
121
121
|
SmoothedParameterCapacity = 13,
|
|
122
|
+
CommandBacklogDeferred = 14,
|
|
123
|
+
ClipPageUnderrun = 15,
|
|
124
|
+
InsertAutomationOverflow = 16,
|
|
122
125
|
}
|
|
123
126
|
|
|
124
127
|
export interface SonareMeterRingBuffer {
|