@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.
Files changed (66) hide show
  1. package/README.md +51 -20
  2. package/dist/index.d.ts +5416 -1
  3. package/dist/index.js +938 -583
  4. package/dist/index.js.map +1 -1
  5. package/dist/sonare.js +2 -2
  6. package/dist/sonare.wasm +0 -0
  7. package/dist/worklet.d.ts +1083 -5227
  8. package/dist/worklet.js +2683 -2451
  9. package/dist/worklet.js.map +1 -1
  10. package/package.json +4 -9
  11. package/src/clip_page_streamer.ts +298 -0
  12. package/src/effects_mastering.ts +85 -1089
  13. package/src/effects_transform.ts +286 -0
  14. package/src/effects_voice_change.ts +118 -0
  15. package/src/feature_music.ts +5 -2
  16. package/src/feature_spectrogram.ts +42 -2
  17. package/src/features.ts +1 -0
  18. package/src/index.ts +23 -0
  19. package/src/mastering_chain.ts +200 -0
  20. package/src/mastering_core.ts +248 -0
  21. package/src/mastering_dynamics.ts +105 -0
  22. package/src/mastering_repair.ts +161 -0
  23. package/src/mixer.ts +8 -0
  24. package/src/mixing_oneshot.ts +54 -0
  25. package/src/module_state.ts +1 -2
  26. package/src/project.ts +71 -1712
  27. package/src/project_class.ts +871 -0
  28. package/src/project_internal.ts +333 -0
  29. package/src/project_synth.ts +43 -0
  30. package/src/project_types.ts +570 -0
  31. package/src/public_types.ts +6 -1221
  32. package/src/public_types_acoustic.ts +115 -0
  33. package/src/public_types_mastering.ts +333 -0
  34. package/src/public_types_mixing.ts +97 -0
  35. package/src/public_types_music.ts +352 -0
  36. package/src/public_types_realtime.ts +163 -0
  37. package/src/public_types_spectral.ts +194 -0
  38. package/src/realtime_engine.ts +94 -0
  39. package/src/sonare.js.d.ts +117 -38
  40. package/src/stream_analyzer.ts +3 -0
  41. package/src/stream_types.ts +4 -0
  42. package/src/worklet/engine-automation.ts +73 -0
  43. package/src/worklet/engine-capture-facade.ts +80 -0
  44. package/src/worklet/engine-clips.ts +71 -0
  45. package/src/worklet/engine-markers.ts +93 -0
  46. package/src/worklet/engine-mixer-facade.ts +186 -0
  47. package/src/worklet/engine-node.ts +451 -0
  48. package/src/worklet/engine-offline.ts +162 -0
  49. package/src/worklet/engine-options.ts +13 -0
  50. package/src/worklet/engine-parameter-facade.ts +172 -0
  51. package/src/worklet/engine-processor.ts +764 -0
  52. package/src/worklet/engine-register.ts +136 -0
  53. package/src/worklet/engine-strips.ts +315 -0
  54. package/src/worklet/engine-sync.ts +94 -0
  55. package/src/worklet/engine-tempo-facade.ts +141 -0
  56. package/src/worklet/engine.ts +998 -0
  57. package/src/worklet/guards.ts +14 -1
  58. package/src/worklet/messages.ts +60 -20
  59. package/src/worklet/mixer-processor.ts +368 -0
  60. package/src/worklet/protocol.ts +3 -0
  61. package/src/worklet/voice-changer-processor.ts +246 -0
  62. package/src/worklet.ts +20 -3549
  63. package/dist/sonare-rt-module.js +0 -2
  64. package/dist/sonare-rt.js +0 -2
  65. package/dist/sonare-rt.wasm +0 -0
  66. package/src/sonare-rt.d.ts +0 -93
@@ -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 === 'syncMidiPanic'
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) &&
@@ -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' | 'sonare-rt';
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
- | SonareEngineSyncMidiPanicMessage;
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
+ }
@@ -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 {