@libraz/libsonare 1.3.3 → 1.4.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/dist/index.d.ts +1 -1
- package/dist/index.js +168 -3
- package/dist/index.js.map +1 -1
- package/dist/sonare-rt-module.js +2 -2
- package/dist/sonare-rt.js +2 -2
- package/dist/sonare-rt.wasm +0 -0
- package/dist/sonare.js +2 -2
- package/dist/sonare.wasm +0 -0
- package/dist/worklet.d.ts +579 -154
- package/dist/worklet.js +622 -110
- package/dist/worklet.js.map +1 -1
- package/package.json +1 -1
- package/src/codes.ts +6 -1
- package/src/effects_mastering.ts +103 -1
- package/src/feature_music.ts +18 -4
- package/src/feature_spectral.ts +7 -1
- package/src/index.ts +14 -0
- package/src/mixer.ts +9 -0
- package/src/project.ts +74 -0
- package/src/public_types.ts +52 -0
- package/src/realtime_engine.ts +141 -2
- package/src/sonare.js.d.ts +81 -0
- package/src/stream_types.ts +7 -0
- package/src/validation.ts +7 -0
- package/src/worklet/audio_types.ts +2 -0
- package/src/worklet/guards.ts +146 -0
- package/src/worklet/messages.ts +461 -0
- package/src/worklet/protocol.ts +767 -0
- package/src/worklet.ts +541 -1106
|
@@ -0,0 +1,767 @@
|
|
|
1
|
+
import type { EngineMeterTelemetry, EngineTelemetry } from '../index';
|
|
2
|
+
|
|
3
|
+
const ENGINE_MIXER_TARGET_BASE = 0x4d580000;
|
|
4
|
+
export const ENGINE_MIXER_PARAM_FADER_DB = 1;
|
|
5
|
+
export const ENGINE_MIXER_PARAM_PAN = 2;
|
|
6
|
+
|
|
7
|
+
export function engineMixerLaneTarget(laneIndex: number, paramKind: number): number {
|
|
8
|
+
return ENGINE_MIXER_TARGET_BASE | ((laneIndex & 0xff) << 8) | (paramKind & 0xff);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function engineMixerBusTarget(busIndex: number, paramKind: number): number {
|
|
12
|
+
return ENGINE_MIXER_TARGET_BASE | (((0xfe - busIndex) & 0xff) << 8) | (paramKind & 0xff);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function engineMixerMasterTarget(paramKind: number): number {
|
|
16
|
+
return ENGINE_MIXER_TARGET_BASE | (0xff << 8) | (paramKind & 0xff);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface SonareWorkletMeterSnapshot {
|
|
20
|
+
type: 'meter';
|
|
21
|
+
targetId: number;
|
|
22
|
+
frame: number;
|
|
23
|
+
peakDbL: number;
|
|
24
|
+
peakDbR: number;
|
|
25
|
+
rmsDbL: number;
|
|
26
|
+
rmsDbR: number;
|
|
27
|
+
correlation: number;
|
|
28
|
+
truePeakDbL: number;
|
|
29
|
+
truePeakDbR: number;
|
|
30
|
+
momentaryLufs: number;
|
|
31
|
+
shortTermLufs: number;
|
|
32
|
+
integratedLufs: number;
|
|
33
|
+
gainReductionDb: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface SonareWorkletSpectrumSnapshot {
|
|
37
|
+
type: 'spectrum';
|
|
38
|
+
frame: number;
|
|
39
|
+
bands: Float32Array;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const SONARE_METER_RING_HEADER_INTS = 4;
|
|
43
|
+
// Record layout: [frameLo, frameHi, targetId, peakDbL, peakDbR, rmsDbL, rmsDbR,
|
|
44
|
+
// correlation, truePeakDbL, truePeakDbR, momentaryLufs, shortTermLufs,
|
|
45
|
+
// integratedLufs, gainReductionDb].
|
|
46
|
+
// The sample-frame index is monotonically increasing and quickly exceeds the
|
|
47
|
+
// 2^24 exact-integer range of a single Float32 slot (~349 s at 48 kHz), so it is
|
|
48
|
+
// stored split across two Float32 lanes (low 24 bits + high bits) for exact
|
|
49
|
+
// reconstruction. See encodeFrameLo/encodeFrameHi/decodeFrame.
|
|
50
|
+
export const SONARE_METER_RING_RECORD_FLOATS = 14;
|
|
51
|
+
export const SONARE_SPECTRUM_RING_HEADER_INTS = 5;
|
|
52
|
+
// Scope ring header: [writeIndex, capacity, recordFloats, bands, maxPoints,
|
|
53
|
+
// reserved]. Record layout: [frameLo, frameHi, targetId, bandCount, pointCount,
|
|
54
|
+
// band0..band(bands-1), l0, r0, l1, r1, ... (maxPoints stereo pairs)].
|
|
55
|
+
export const SONARE_SCOPE_RING_HEADER_INTS = 6;
|
|
56
|
+
export const SONARE_SCOPE_RING_RECORD_PREFIX_FLOATS = 5;
|
|
57
|
+
|
|
58
|
+
/** Base for splitting a frame index into two exactly-representable Float32 lanes. */
|
|
59
|
+
const SONARE_FRAME_LANE_BASE = 0x1000000; // 2^24
|
|
60
|
+
|
|
61
|
+
/** Low 24 bits of a frame index (exact in Float32). */
|
|
62
|
+
export function encodeFrameLo(frame: number): number {
|
|
63
|
+
const f = Math.max(0, Math.floor(frame));
|
|
64
|
+
return f % SONARE_FRAME_LANE_BASE;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** High bits of a frame index above 2^24 (exact in Float32 up to ~2^48). */
|
|
68
|
+
export function encodeFrameHi(frame: number): number {
|
|
69
|
+
const f = Math.max(0, Math.floor(frame));
|
|
70
|
+
return Math.floor(f / SONARE_FRAME_LANE_BASE);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Reconstruct a frame index from its low/high Float32 lanes. */
|
|
74
|
+
export function decodeFrame(lo: number, hi: number): number {
|
|
75
|
+
return hi * SONARE_FRAME_LANE_BASE + lo;
|
|
76
|
+
}
|
|
77
|
+
export const SONARE_ENGINE_RING_HEADER_INTS = 5;
|
|
78
|
+
export const SONARE_ENGINE_COMMAND_RECORD_BYTES = 32;
|
|
79
|
+
export const SONARE_ENGINE_TELEMETRY_RECORD_BYTES = 48;
|
|
80
|
+
|
|
81
|
+
export enum SonareEngineCommandType {
|
|
82
|
+
SetParam = 0,
|
|
83
|
+
SetParamSmoothed = 1,
|
|
84
|
+
TransportPlay = 2,
|
|
85
|
+
TransportStop = 3,
|
|
86
|
+
TransportSeekSample = 4,
|
|
87
|
+
TransportSeekPpq = 5,
|
|
88
|
+
SetTempoMap = 6,
|
|
89
|
+
SetLoop = 7,
|
|
90
|
+
SwapGraph = 8,
|
|
91
|
+
SwapAutomation = 9,
|
|
92
|
+
SetSoloMute = 10,
|
|
93
|
+
AddClip = 11,
|
|
94
|
+
RemoveClip = 12,
|
|
95
|
+
ArmRecord = 13,
|
|
96
|
+
Punch = 14,
|
|
97
|
+
SetMetronome = 15,
|
|
98
|
+
SetMarker = 16,
|
|
99
|
+
SeekMarker = 17,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export enum SonareEngineTelemetryType {
|
|
103
|
+
ProcessBlock = 0,
|
|
104
|
+
Error = 1,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export enum SonareEngineTelemetryError {
|
|
108
|
+
None = 0,
|
|
109
|
+
CommandQueueOverflow = 1,
|
|
110
|
+
PendingCommandOverflow = 2,
|
|
111
|
+
BoundaryOverflow = 3,
|
|
112
|
+
TelemetryOverflow = 4,
|
|
113
|
+
CaptureOverflow = 5,
|
|
114
|
+
MaxBlockExceeded = 6,
|
|
115
|
+
UnknownTarget = 7,
|
|
116
|
+
NonRealtimeSafeParameter = 8,
|
|
117
|
+
NotPrepared = 9,
|
|
118
|
+
NonQueueableCommand = 10,
|
|
119
|
+
AutomationBindTargetOverflow = 11,
|
|
120
|
+
StaleAutomationLanes = 12,
|
|
121
|
+
SmoothedParameterCapacity = 13,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface SonareMeterRingBuffer {
|
|
125
|
+
sharedBuffer: SharedArrayBuffer;
|
|
126
|
+
header: Int32Array;
|
|
127
|
+
records: Float32Array;
|
|
128
|
+
capacity: number;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface SonareMeterRingReadResult {
|
|
132
|
+
nextReadIndex: number;
|
|
133
|
+
meters: SonareWorkletMeterSnapshot[];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface SonareSpectrumRingBuffer {
|
|
137
|
+
sharedBuffer: SharedArrayBuffer;
|
|
138
|
+
header: Int32Array;
|
|
139
|
+
records: Float32Array;
|
|
140
|
+
capacity: number;
|
|
141
|
+
bands: number;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface SonareSpectrumRingReadResult {
|
|
145
|
+
nextReadIndex: number;
|
|
146
|
+
spectra: SonareWorkletSpectrumSnapshot[];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* A single target-addressed scope record drained from the realtime engine's
|
|
151
|
+
* scope ring: an FFT magnitude spectrum plus a decimated goniometer/vectorscope
|
|
152
|
+
* point cloud. Unlike {@link SonareWorkletSpectrumSnapshot} (the legacy
|
|
153
|
+
* coarse-DFT meter spectrum), this carries a `targetId` (master/lane/bus) and a
|
|
154
|
+
* stereo point cloud, mirroring the engine's `ScopeTelemetryRecord`.
|
|
155
|
+
*/
|
|
156
|
+
export interface SonareWorkletScopeSnapshot {
|
|
157
|
+
type: 'scope';
|
|
158
|
+
targetId: number;
|
|
159
|
+
frame: number;
|
|
160
|
+
/** Linear-band magnitudes in dB (length = the configured band count). */
|
|
161
|
+
bands: Float32Array;
|
|
162
|
+
/** Interleaved stereo goniometer points: [l0, r0, l1, r1, ...]. */
|
|
163
|
+
points: Float32Array;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export interface SonareScopeRingBuffer {
|
|
167
|
+
sharedBuffer: SharedArrayBuffer;
|
|
168
|
+
header: Int32Array;
|
|
169
|
+
records: Float32Array;
|
|
170
|
+
capacity: number;
|
|
171
|
+
bands: number;
|
|
172
|
+
maxPoints: number;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export interface SonareScopeRingReadResult {
|
|
176
|
+
nextReadIndex: number;
|
|
177
|
+
scopes: SonareWorkletScopeSnapshot[];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export interface SharedScopeRingWriter {
|
|
181
|
+
header: Int32Array;
|
|
182
|
+
records: Float32Array;
|
|
183
|
+
capacity: number;
|
|
184
|
+
bands: number;
|
|
185
|
+
maxPoints: number;
|
|
186
|
+
recordFloats: number;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export interface SonareEngineCommandRecord {
|
|
190
|
+
type: SonareEngineCommandType | number;
|
|
191
|
+
targetId?: number;
|
|
192
|
+
sampleTime?: number | bigint;
|
|
193
|
+
argFloat?: number;
|
|
194
|
+
argInt?: number | bigint;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export interface SonareEngineTelemetryRecord {
|
|
198
|
+
type: SonareEngineTelemetryType | number;
|
|
199
|
+
error: SonareEngineTelemetryError | number;
|
|
200
|
+
renderFrame: number;
|
|
201
|
+
timelineSample: number;
|
|
202
|
+
audibleTimelineSample: number;
|
|
203
|
+
graphLatencySamplesQ8: number;
|
|
204
|
+
value: number;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export interface SonareEngineCommandRingBuffer {
|
|
208
|
+
sharedBuffer: SharedArrayBuffer;
|
|
209
|
+
header: Int32Array;
|
|
210
|
+
view: DataView;
|
|
211
|
+
capacity: number;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export interface SonareEngineTelemetryRingBuffer {
|
|
215
|
+
sharedBuffer: SharedArrayBuffer;
|
|
216
|
+
header: Int32Array;
|
|
217
|
+
view: DataView;
|
|
218
|
+
capacity: number;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export interface SonareEngineTelemetryRingReadResult {
|
|
222
|
+
nextReadIndex: number;
|
|
223
|
+
telemetry: SonareEngineTelemetryRecord[];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export interface SharedMeterRingWriter {
|
|
227
|
+
header: Int32Array;
|
|
228
|
+
records: Float32Array;
|
|
229
|
+
capacity: number;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export interface SharedSpectrumRingWriter {
|
|
233
|
+
header: Int32Array;
|
|
234
|
+
records: Float32Array;
|
|
235
|
+
capacity: number;
|
|
236
|
+
bands: number;
|
|
237
|
+
recordFloats: number;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function toDb(value: number): number {
|
|
241
|
+
return value > 0 ? 20 * Math.log10(value) : Number.NEGATIVE_INFINITY;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
245
|
+
return typeof value === 'object' && value !== null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function sonareMeterRingBufferByteLength(capacity: number): number {
|
|
249
|
+
const clampedCapacity = Math.max(1, Math.floor(capacity));
|
|
250
|
+
return (
|
|
251
|
+
SONARE_METER_RING_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT +
|
|
252
|
+
clampedCapacity * SONARE_METER_RING_RECORD_FLOATS * Float32Array.BYTES_PER_ELEMENT
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function createSonareMeterRingBuffer(capacity = 128): SonareMeterRingBuffer {
|
|
257
|
+
const clampedCapacity = Math.max(1, Math.floor(capacity));
|
|
258
|
+
const sharedBuffer = new SharedArrayBuffer(sonareMeterRingBufferByteLength(clampedCapacity));
|
|
259
|
+
const ring = meterRingFromSharedBuffer(sharedBuffer, clampedCapacity);
|
|
260
|
+
Atomics.store(ring.header, 0, 0);
|
|
261
|
+
Atomics.store(ring.header, 1, clampedCapacity);
|
|
262
|
+
Atomics.store(ring.header, 2, SONARE_METER_RING_RECORD_FLOATS);
|
|
263
|
+
Atomics.store(ring.header, 3, 0);
|
|
264
|
+
return { sharedBuffer, header: ring.header, records: ring.records, capacity: ring.capacity };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function readSonareMeterRingBuffer(
|
|
268
|
+
ring: SonareMeterRingBuffer,
|
|
269
|
+
readIndex = 0,
|
|
270
|
+
): SonareMeterRingReadResult {
|
|
271
|
+
const writeIndex = Atomics.load(ring.header, 0);
|
|
272
|
+
const recordFloats = Atomics.load(ring.header, 2) || SONARE_METER_RING_RECORD_FLOATS;
|
|
273
|
+
const nextReadIndex = Math.max(0, Math.min(readIndex, writeIndex));
|
|
274
|
+
const firstReadable = Math.max(nextReadIndex, writeIndex - ring.capacity);
|
|
275
|
+
const meters: SonareWorkletMeterSnapshot[] = [];
|
|
276
|
+
for (let index = firstReadable; index < writeIndex; index++) {
|
|
277
|
+
const offset = (index % ring.capacity) * recordFloats;
|
|
278
|
+
meters.push({
|
|
279
|
+
type: 'meter',
|
|
280
|
+
frame: decodeFrame(ring.records[offset], ring.records[offset + 1]),
|
|
281
|
+
targetId: ring.records[offset + 2],
|
|
282
|
+
peakDbL: ring.records[offset + 3],
|
|
283
|
+
peakDbR: ring.records[offset + 4],
|
|
284
|
+
rmsDbL: ring.records[offset + 5],
|
|
285
|
+
rmsDbR: ring.records[offset + 6],
|
|
286
|
+
correlation: ring.records[offset + 7],
|
|
287
|
+
truePeakDbL: ring.records[offset + 8],
|
|
288
|
+
truePeakDbR: ring.records[offset + 9],
|
|
289
|
+
momentaryLufs: ring.records[offset + 10],
|
|
290
|
+
shortTermLufs: ring.records[offset + 11],
|
|
291
|
+
integratedLufs: ring.records[offset + 12],
|
|
292
|
+
gainReductionDb: ring.records[offset + 13],
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
return { nextReadIndex: writeIndex, meters };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function sonareSpectrumRingBufferByteLength(capacity: number, bands = 16): number {
|
|
299
|
+
const clampedCapacity = Math.max(1, Math.floor(capacity));
|
|
300
|
+
const clampedBands = Math.max(1, Math.floor(bands));
|
|
301
|
+
// Record layout: [frameLo, frameHi, bandCount, band0, band1, ...]. frame is
|
|
302
|
+
// split across two Float32 lanes for exact reconstruction beyond 2^24.
|
|
303
|
+
return (
|
|
304
|
+
SONARE_SPECTRUM_RING_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT +
|
|
305
|
+
clampedCapacity * (3 + clampedBands) * Float32Array.BYTES_PER_ELEMENT
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export function createSonareSpectrumRingBuffer(
|
|
310
|
+
capacity = 128,
|
|
311
|
+
bands = 16,
|
|
312
|
+
): SonareSpectrumRingBuffer {
|
|
313
|
+
const clampedCapacity = Math.max(1, Math.floor(capacity));
|
|
314
|
+
const clampedBands = Math.max(1, Math.floor(bands));
|
|
315
|
+
const sharedBuffer = new SharedArrayBuffer(
|
|
316
|
+
sonareSpectrumRingBufferByteLength(clampedCapacity, clampedBands),
|
|
317
|
+
);
|
|
318
|
+
const ring = spectrumRingFromSharedBuffer(sharedBuffer, clampedCapacity, clampedBands);
|
|
319
|
+
Atomics.store(ring.header, 0, 0);
|
|
320
|
+
Atomics.store(ring.header, 1, clampedCapacity);
|
|
321
|
+
Atomics.store(ring.header, 2, ring.recordFloats);
|
|
322
|
+
Atomics.store(ring.header, 3, clampedBands);
|
|
323
|
+
Atomics.store(ring.header, 4, 0);
|
|
324
|
+
return {
|
|
325
|
+
sharedBuffer,
|
|
326
|
+
header: ring.header,
|
|
327
|
+
records: ring.records,
|
|
328
|
+
capacity: ring.capacity,
|
|
329
|
+
bands: ring.bands,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function readSonareSpectrumRingBuffer(
|
|
334
|
+
ring: SonareSpectrumRingBuffer,
|
|
335
|
+
readIndex = 0,
|
|
336
|
+
): SonareSpectrumRingReadResult {
|
|
337
|
+
const writeIndex = Atomics.load(ring.header, 0);
|
|
338
|
+
const recordFloats = Atomics.load(ring.header, 2) || 3 + ring.bands;
|
|
339
|
+
const bands = Atomics.load(ring.header, 3) || ring.bands;
|
|
340
|
+
const nextReadIndex = Math.max(0, Math.min(readIndex, writeIndex));
|
|
341
|
+
const firstReadable = Math.max(nextReadIndex, writeIndex - ring.capacity);
|
|
342
|
+
const spectra: SonareWorkletSpectrumSnapshot[] = [];
|
|
343
|
+
for (let index = firstReadable; index < writeIndex; index++) {
|
|
344
|
+
const offset = (index % ring.capacity) * recordFloats;
|
|
345
|
+
const values = new Float32Array(bands);
|
|
346
|
+
values.set(ring.records.subarray(offset + 3, offset + 3 + bands));
|
|
347
|
+
spectra.push({
|
|
348
|
+
type: 'spectrum',
|
|
349
|
+
frame: decodeFrame(ring.records[offset], ring.records[offset + 1]),
|
|
350
|
+
bands: values,
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
return { nextReadIndex: writeIndex, spectra };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export function sonareScopeRingRecordFloats(bands: number, maxPoints: number): number {
|
|
357
|
+
return SONARE_SCOPE_RING_RECORD_PREFIX_FLOATS + bands + 2 * maxPoints;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export function sonareScopeRingBufferByteLength(
|
|
361
|
+
capacity: number,
|
|
362
|
+
bands = 48,
|
|
363
|
+
maxPoints = 32,
|
|
364
|
+
): number {
|
|
365
|
+
const clampedCapacity = Math.max(1, Math.floor(capacity));
|
|
366
|
+
const clampedBands = Math.max(1, Math.floor(bands));
|
|
367
|
+
const clampedPoints = Math.max(0, Math.floor(maxPoints));
|
|
368
|
+
return (
|
|
369
|
+
SONARE_SCOPE_RING_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT +
|
|
370
|
+
clampedCapacity *
|
|
371
|
+
sonareScopeRingRecordFloats(clampedBands, clampedPoints) *
|
|
372
|
+
Float32Array.BYTES_PER_ELEMENT
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export function createSonareScopeRingBuffer(
|
|
377
|
+
capacity = 64,
|
|
378
|
+
bands = 48,
|
|
379
|
+
maxPoints = 32,
|
|
380
|
+
): SonareScopeRingBuffer {
|
|
381
|
+
const clampedCapacity = Math.max(1, Math.floor(capacity));
|
|
382
|
+
const clampedBands = Math.max(1, Math.floor(bands));
|
|
383
|
+
const clampedPoints = Math.max(0, Math.floor(maxPoints));
|
|
384
|
+
const sharedBuffer = new SharedArrayBuffer(
|
|
385
|
+
sonareScopeRingBufferByteLength(clampedCapacity, clampedBands, clampedPoints),
|
|
386
|
+
);
|
|
387
|
+
const ring = scopeRingFromSharedBuffer(
|
|
388
|
+
sharedBuffer,
|
|
389
|
+
clampedCapacity,
|
|
390
|
+
clampedBands,
|
|
391
|
+
clampedPoints,
|
|
392
|
+
);
|
|
393
|
+
Atomics.store(ring.header, 0, 0);
|
|
394
|
+
Atomics.store(ring.header, 1, clampedCapacity);
|
|
395
|
+
Atomics.store(ring.header, 2, ring.recordFloats);
|
|
396
|
+
Atomics.store(ring.header, 3, clampedBands);
|
|
397
|
+
Atomics.store(ring.header, 4, clampedPoints);
|
|
398
|
+
Atomics.store(ring.header, 5, 0);
|
|
399
|
+
return {
|
|
400
|
+
sharedBuffer,
|
|
401
|
+
header: ring.header,
|
|
402
|
+
records: ring.records,
|
|
403
|
+
capacity: ring.capacity,
|
|
404
|
+
bands: ring.bands,
|
|
405
|
+
maxPoints: ring.maxPoints,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export function readSonareScopeRingBuffer(
|
|
410
|
+
ring: SonareScopeRingBuffer,
|
|
411
|
+
readIndex = 0,
|
|
412
|
+
): SonareScopeRingReadResult {
|
|
413
|
+
const writeIndex = Atomics.load(ring.header, 0);
|
|
414
|
+
const bands = Atomics.load(ring.header, 3) || ring.bands;
|
|
415
|
+
const maxPoints = Atomics.load(ring.header, 4);
|
|
416
|
+
const recordFloats =
|
|
417
|
+
Atomics.load(ring.header, 2) || sonareScopeRingRecordFloats(bands, maxPoints);
|
|
418
|
+
const nextReadIndex = Math.max(0, Math.min(readIndex, writeIndex));
|
|
419
|
+
const firstReadable = Math.max(nextReadIndex, writeIndex - ring.capacity);
|
|
420
|
+
const scopes: SonareWorkletScopeSnapshot[] = [];
|
|
421
|
+
for (let index = firstReadable; index < writeIndex; index++) {
|
|
422
|
+
const offset = (index % ring.capacity) * recordFloats;
|
|
423
|
+
const bandCount = Math.min(bands, Math.max(0, ring.records[offset + 3]));
|
|
424
|
+
const pointCount = Math.min(maxPoints, Math.max(0, ring.records[offset + 4]));
|
|
425
|
+
const bandsView = new Float32Array(bandCount);
|
|
426
|
+
bandsView.set(
|
|
427
|
+
ring.records.subarray(
|
|
428
|
+
offset + SONARE_SCOPE_RING_RECORD_PREFIX_FLOATS,
|
|
429
|
+
offset + SONARE_SCOPE_RING_RECORD_PREFIX_FLOATS + bandCount,
|
|
430
|
+
),
|
|
431
|
+
);
|
|
432
|
+
const pointsBase = offset + SONARE_SCOPE_RING_RECORD_PREFIX_FLOATS + bands;
|
|
433
|
+
const pointsView = new Float32Array(pointCount * 2);
|
|
434
|
+
pointsView.set(ring.records.subarray(pointsBase, pointsBase + pointCount * 2));
|
|
435
|
+
scopes.push({
|
|
436
|
+
type: 'scope',
|
|
437
|
+
frame: decodeFrame(ring.records[offset], ring.records[offset + 1]),
|
|
438
|
+
targetId: ring.records[offset + 2],
|
|
439
|
+
bands: bandsView,
|
|
440
|
+
points: pointsView,
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
return { nextReadIndex: writeIndex, scopes };
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export function scopeRingFromSharedBuffer(
|
|
447
|
+
sharedBuffer: SharedArrayBuffer,
|
|
448
|
+
fallbackCapacity?: number,
|
|
449
|
+
fallbackBands?: number,
|
|
450
|
+
fallbackMaxPoints?: number,
|
|
451
|
+
): SharedScopeRingWriter {
|
|
452
|
+
const headerBytes = SONARE_SCOPE_RING_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT;
|
|
453
|
+
const header = new Int32Array(sharedBuffer, 0, SONARE_SCOPE_RING_HEADER_INTS);
|
|
454
|
+
const existingCapacity = Atomics.load(header, 1);
|
|
455
|
+
const existingBands = Atomics.load(header, 3);
|
|
456
|
+
const existingMaxPoints = Atomics.load(header, 4);
|
|
457
|
+
const capacity = Math.max(1, Math.floor(existingCapacity || fallbackCapacity || 1));
|
|
458
|
+
const bands = Math.max(1, Math.floor(existingBands || fallbackBands || 48));
|
|
459
|
+
const maxPoints = Math.max(0, Math.floor(existingMaxPoints || (fallbackMaxPoints ?? 32)));
|
|
460
|
+
const recordFloats = sonareScopeRingRecordFloats(bands, maxPoints);
|
|
461
|
+
const minBytes = sonareScopeRingBufferByteLength(capacity, bands, maxPoints);
|
|
462
|
+
if (sharedBuffer.byteLength < minBytes) {
|
|
463
|
+
throw new Error('scopeSharedBuffer is too small for the requested ring capacity.');
|
|
464
|
+
}
|
|
465
|
+
Atomics.store(header, 1, capacity);
|
|
466
|
+
Atomics.store(header, 2, recordFloats);
|
|
467
|
+
Atomics.store(header, 3, bands);
|
|
468
|
+
Atomics.store(header, 4, maxPoints);
|
|
469
|
+
return {
|
|
470
|
+
header,
|
|
471
|
+
records: new Float32Array(sharedBuffer, headerBytes, capacity * recordFloats),
|
|
472
|
+
capacity,
|
|
473
|
+
bands,
|
|
474
|
+
maxPoints,
|
|
475
|
+
recordFloats,
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
export function sonareEngineCommandRingBufferByteLength(capacity: number): number {
|
|
480
|
+
const clampedCapacity = Math.max(1, Math.floor(capacity));
|
|
481
|
+
return (
|
|
482
|
+
SONARE_ENGINE_RING_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT +
|
|
483
|
+
clampedCapacity * SONARE_ENGINE_COMMAND_RECORD_BYTES
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
export function sonareEngineTelemetryRingBufferByteLength(capacity: number): number {
|
|
488
|
+
const clampedCapacity = Math.max(1, Math.floor(capacity));
|
|
489
|
+
return (
|
|
490
|
+
SONARE_ENGINE_RING_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT +
|
|
491
|
+
clampedCapacity * SONARE_ENGINE_TELEMETRY_RECORD_BYTES
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
export function createSonareEngineCommandRingBuffer(capacity = 128): SonareEngineCommandRingBuffer {
|
|
496
|
+
const clampedCapacity = Math.max(1, Math.floor(capacity));
|
|
497
|
+
const sharedBuffer = new SharedArrayBuffer(
|
|
498
|
+
sonareEngineCommandRingBufferByteLength(clampedCapacity),
|
|
499
|
+
);
|
|
500
|
+
const ring = engineRingFromSharedBuffer(
|
|
501
|
+
sharedBuffer,
|
|
502
|
+
SONARE_ENGINE_COMMAND_RECORD_BYTES,
|
|
503
|
+
clampedCapacity,
|
|
504
|
+
);
|
|
505
|
+
return { sharedBuffer, header: ring.header, view: ring.view, capacity: ring.capacity };
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
export function createSonareEngineTelemetryRingBuffer(
|
|
509
|
+
capacity = 128,
|
|
510
|
+
): SonareEngineTelemetryRingBuffer {
|
|
511
|
+
const clampedCapacity = Math.max(1, Math.floor(capacity));
|
|
512
|
+
const sharedBuffer = new SharedArrayBuffer(
|
|
513
|
+
sonareEngineTelemetryRingBufferByteLength(clampedCapacity),
|
|
514
|
+
);
|
|
515
|
+
const ring = engineRingFromSharedBuffer(
|
|
516
|
+
sharedBuffer,
|
|
517
|
+
SONARE_ENGINE_TELEMETRY_RECORD_BYTES,
|
|
518
|
+
clampedCapacity,
|
|
519
|
+
);
|
|
520
|
+
return { sharedBuffer, header: ring.header, view: ring.view, capacity: ring.capacity };
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
export function pushSonareEngineCommandRingBuffer(
|
|
524
|
+
ring: SonareEngineCommandRingBuffer,
|
|
525
|
+
command: SonareEngineCommandRecord,
|
|
526
|
+
): boolean {
|
|
527
|
+
const writeIndex = Atomics.load(ring.header, 0);
|
|
528
|
+
const readIndex = Atomics.load(ring.header, 1);
|
|
529
|
+
if (writeIndex - readIndex >= ring.capacity) {
|
|
530
|
+
Atomics.add(ring.header, 4, 1);
|
|
531
|
+
return false;
|
|
532
|
+
}
|
|
533
|
+
writeEngineCommandRecord(
|
|
534
|
+
ring.view,
|
|
535
|
+
recordOffset(writeIndex, ring.capacity, SONARE_ENGINE_COMMAND_RECORD_BYTES),
|
|
536
|
+
command,
|
|
537
|
+
);
|
|
538
|
+
Atomics.store(ring.header, 0, writeIndex + 1);
|
|
539
|
+
return true;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
export function popSonareEngineCommandRingBuffer(
|
|
543
|
+
ring: SonareEngineCommandRingBuffer,
|
|
544
|
+
): SonareEngineCommandRecord | null {
|
|
545
|
+
const readIndex = Atomics.load(ring.header, 1);
|
|
546
|
+
const writeIndex = Atomics.load(ring.header, 0);
|
|
547
|
+
if (readIndex >= writeIndex) {
|
|
548
|
+
return null;
|
|
549
|
+
}
|
|
550
|
+
const command = readEngineCommandRecord(
|
|
551
|
+
ring.view,
|
|
552
|
+
recordOffset(readIndex, ring.capacity, SONARE_ENGINE_COMMAND_RECORD_BYTES),
|
|
553
|
+
);
|
|
554
|
+
Atomics.store(ring.header, 1, readIndex + 1);
|
|
555
|
+
return command;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
export function writeSonareEngineTelemetryRingBuffer(
|
|
559
|
+
ring: SonareEngineTelemetryRingBuffer,
|
|
560
|
+
telemetry: SonareEngineTelemetryRecord,
|
|
561
|
+
): void {
|
|
562
|
+
const writeIndex = Atomics.load(ring.header, 0);
|
|
563
|
+
writeEngineTelemetryRecord(
|
|
564
|
+
ring.view,
|
|
565
|
+
recordOffset(writeIndex, ring.capacity, SONARE_ENGINE_TELEMETRY_RECORD_BYTES),
|
|
566
|
+
telemetry,
|
|
567
|
+
);
|
|
568
|
+
Atomics.store(ring.header, 0, writeIndex + 1);
|
|
569
|
+
if (writeIndex + 1 > ring.capacity) {
|
|
570
|
+
Atomics.store(ring.header, 4, writeIndex + 1 - ring.capacity);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
export function readSonareEngineTelemetryRingBuffer(
|
|
575
|
+
ring: SonareEngineTelemetryRingBuffer,
|
|
576
|
+
readIndex = 0,
|
|
577
|
+
): SonareEngineTelemetryRingReadResult {
|
|
578
|
+
const writeIndex = Atomics.load(ring.header, 0);
|
|
579
|
+
const nextReadIndex = Math.max(0, Math.min(readIndex, writeIndex));
|
|
580
|
+
const firstReadable = Math.max(nextReadIndex, writeIndex - ring.capacity);
|
|
581
|
+
const telemetry: SonareEngineTelemetryRecord[] = [];
|
|
582
|
+
for (let index = firstReadable; index < writeIndex; index++) {
|
|
583
|
+
telemetry.push(
|
|
584
|
+
readEngineTelemetryRecord(
|
|
585
|
+
ring.view,
|
|
586
|
+
recordOffset(index, ring.capacity, SONARE_ENGINE_TELEMETRY_RECORD_BYTES),
|
|
587
|
+
),
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
return { nextReadIndex: writeIndex, telemetry };
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
export function meterRingFromSharedBuffer(
|
|
594
|
+
sharedBuffer: SharedArrayBuffer,
|
|
595
|
+
fallbackCapacity?: number,
|
|
596
|
+
): SharedMeterRingWriter {
|
|
597
|
+
const headerBytes = SONARE_METER_RING_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT;
|
|
598
|
+
const header = new Int32Array(sharedBuffer, 0, SONARE_METER_RING_HEADER_INTS);
|
|
599
|
+
const existingCapacity = Atomics.load(header, 1);
|
|
600
|
+
const capacity = Math.max(1, Math.floor(existingCapacity || fallbackCapacity || 1));
|
|
601
|
+
const minBytes = sonareMeterRingBufferByteLength(capacity);
|
|
602
|
+
if (sharedBuffer.byteLength < minBytes) {
|
|
603
|
+
throw new Error('meterSharedBuffer is too small for the requested ring capacity.');
|
|
604
|
+
}
|
|
605
|
+
Atomics.store(header, 1, capacity);
|
|
606
|
+
Atomics.store(header, 2, SONARE_METER_RING_RECORD_FLOATS);
|
|
607
|
+
return {
|
|
608
|
+
header,
|
|
609
|
+
records: new Float32Array(
|
|
610
|
+
sharedBuffer,
|
|
611
|
+
headerBytes,
|
|
612
|
+
capacity * SONARE_METER_RING_RECORD_FLOATS,
|
|
613
|
+
),
|
|
614
|
+
capacity,
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
export function spectrumRingFromSharedBuffer(
|
|
619
|
+
sharedBuffer: SharedArrayBuffer,
|
|
620
|
+
fallbackCapacity?: number,
|
|
621
|
+
fallbackBands?: number,
|
|
622
|
+
): SharedSpectrumRingWriter {
|
|
623
|
+
const headerBytes = SONARE_SPECTRUM_RING_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT;
|
|
624
|
+
const header = new Int32Array(sharedBuffer, 0, SONARE_SPECTRUM_RING_HEADER_INTS);
|
|
625
|
+
const existingCapacity = Atomics.load(header, 1);
|
|
626
|
+
const existingBands = Atomics.load(header, 3);
|
|
627
|
+
const capacity = Math.max(1, Math.floor(existingCapacity || fallbackCapacity || 1));
|
|
628
|
+
const bands = Math.max(1, Math.floor(existingBands || fallbackBands || 16));
|
|
629
|
+
const recordFloats = 3 + bands;
|
|
630
|
+
const minBytes = sonareSpectrumRingBufferByteLength(capacity, bands);
|
|
631
|
+
if (sharedBuffer.byteLength < minBytes) {
|
|
632
|
+
throw new Error('spectrumSharedBuffer is too small for the requested ring capacity.');
|
|
633
|
+
}
|
|
634
|
+
Atomics.store(header, 1, capacity);
|
|
635
|
+
Atomics.store(header, 2, recordFloats);
|
|
636
|
+
Atomics.store(header, 3, bands);
|
|
637
|
+
return {
|
|
638
|
+
header,
|
|
639
|
+
records: new Float32Array(sharedBuffer, headerBytes, capacity * recordFloats),
|
|
640
|
+
capacity,
|
|
641
|
+
bands,
|
|
642
|
+
recordFloats,
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
export function engineRingFromSharedBuffer(
|
|
647
|
+
sharedBuffer: SharedArrayBuffer,
|
|
648
|
+
recordBytes: number,
|
|
649
|
+
fallbackCapacity?: number,
|
|
650
|
+
): { header: Int32Array; view: DataView; capacity: number } {
|
|
651
|
+
const headerBytes = SONARE_ENGINE_RING_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT;
|
|
652
|
+
const header = new Int32Array(sharedBuffer, 0, SONARE_ENGINE_RING_HEADER_INTS);
|
|
653
|
+
const existingCapacity = Atomics.load(header, 2);
|
|
654
|
+
const capacity = Math.max(1, Math.floor(existingCapacity || fallbackCapacity || 1));
|
|
655
|
+
const minBytes = headerBytes + capacity * recordBytes;
|
|
656
|
+
if (sharedBuffer.byteLength < minBytes) {
|
|
657
|
+
throw new Error('engine SharedArrayBuffer is too small for the requested ring capacity.');
|
|
658
|
+
}
|
|
659
|
+
Atomics.store(header, 2, capacity);
|
|
660
|
+
Atomics.store(header, 3, recordBytes);
|
|
661
|
+
return {
|
|
662
|
+
header,
|
|
663
|
+
view: new DataView(sharedBuffer, headerBytes, capacity * recordBytes),
|
|
664
|
+
capacity,
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function recordOffset(index: number, capacity: number, recordBytes: number): number {
|
|
669
|
+
return (index % capacity) * recordBytes;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
export function toBigInt64(value: number | bigint | undefined, fallback: bigint): bigint {
|
|
673
|
+
if (typeof value === 'bigint') {
|
|
674
|
+
return value;
|
|
675
|
+
}
|
|
676
|
+
if (typeof value === 'number') {
|
|
677
|
+
return BigInt(Math.trunc(value));
|
|
678
|
+
}
|
|
679
|
+
return fallback;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function writeEngineCommandRecord(
|
|
683
|
+
view: DataView,
|
|
684
|
+
offset: number,
|
|
685
|
+
command: SonareEngineCommandRecord,
|
|
686
|
+
): void {
|
|
687
|
+
view.setUint32(offset, command.type, true);
|
|
688
|
+
view.setUint32(offset + 4, command.targetId ?? 0, true);
|
|
689
|
+
view.setBigInt64(offset + 8, toBigInt64(command.sampleTime, -1n), true);
|
|
690
|
+
// argFloat occupies a full 8-byte Float64 slot (replacing the old Float32 +
|
|
691
|
+
// 4-byte pad) so PPQ scalars carried here keep full double precision over the
|
|
692
|
+
// SAB transport, matching the engine's double-precision seek/loop contract.
|
|
693
|
+
view.setFloat64(offset + 16, command.argFloat ?? 0, true);
|
|
694
|
+
view.setBigInt64(offset + 24, toBigInt64(command.argInt, 0n), true);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function readEngineCommandRecord(view: DataView, offset: number): SonareEngineCommandRecord {
|
|
698
|
+
return {
|
|
699
|
+
type: view.getUint32(offset, true),
|
|
700
|
+
targetId: view.getUint32(offset + 4, true),
|
|
701
|
+
sampleTime: Number(view.getBigInt64(offset + 8, true)),
|
|
702
|
+
argFloat: view.getFloat64(offset + 16, true),
|
|
703
|
+
argInt: Number(view.getBigInt64(offset + 24, true)),
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function writeEngineTelemetryRecord(
|
|
708
|
+
view: DataView,
|
|
709
|
+
offset: number,
|
|
710
|
+
telemetry: SonareEngineTelemetryRecord,
|
|
711
|
+
): void {
|
|
712
|
+
view.setUint32(offset, telemetry.type, true);
|
|
713
|
+
view.setUint32(offset + 4, telemetry.error, true);
|
|
714
|
+
view.setBigInt64(offset + 8, BigInt(Math.trunc(telemetry.renderFrame)), true);
|
|
715
|
+
view.setBigInt64(offset + 16, BigInt(Math.trunc(telemetry.timelineSample)), true);
|
|
716
|
+
view.setBigInt64(offset + 24, BigInt(Math.trunc(telemetry.audibleTimelineSample)), true);
|
|
717
|
+
view.setInt32(offset + 32, telemetry.graphLatencySamplesQ8, true);
|
|
718
|
+
view.setUint32(offset + 36, telemetry.value, true);
|
|
719
|
+
view.setBigInt64(offset + 40, 0n, true);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function readEngineTelemetryRecord(view: DataView, offset: number): SonareEngineTelemetryRecord {
|
|
723
|
+
return {
|
|
724
|
+
type: view.getUint32(offset, true),
|
|
725
|
+
error: view.getUint32(offset + 4, true),
|
|
726
|
+
renderFrame: Number(view.getBigInt64(offset + 8, true)),
|
|
727
|
+
timelineSample: Number(view.getBigInt64(offset + 16, true)),
|
|
728
|
+
audibleTimelineSample: Number(view.getBigInt64(offset + 24, true)),
|
|
729
|
+
graphLatencySamplesQ8: view.getInt32(offset + 32, true),
|
|
730
|
+
value: view.getUint32(offset + 36, true),
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
export function telemetryFromEngine(telemetry: EngineTelemetry): SonareEngineTelemetryRecord {
|
|
735
|
+
return {
|
|
736
|
+
type: telemetry.type,
|
|
737
|
+
error: telemetry.error,
|
|
738
|
+
renderFrame: telemetry.renderFrame,
|
|
739
|
+
timelineSample: telemetry.timelineSample,
|
|
740
|
+
audibleTimelineSample: telemetry.audibleTimelineSample,
|
|
741
|
+
graphLatencySamplesQ8: telemetry.graphLatencySamplesQ8,
|
|
742
|
+
value: telemetry.value,
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
export function meterFromEngine(meter: EngineMeterTelemetry): SonareWorkletMeterSnapshot {
|
|
747
|
+
return {
|
|
748
|
+
type: 'meter',
|
|
749
|
+
targetId: meter.targetId,
|
|
750
|
+
frame: meter.renderFrame,
|
|
751
|
+
peakDbL: meter.peakDbL,
|
|
752
|
+
peakDbR: meter.peakDbR,
|
|
753
|
+
rmsDbL: meter.rmsDbL,
|
|
754
|
+
rmsDbR: meter.rmsDbR,
|
|
755
|
+
correlation: meter.correlation,
|
|
756
|
+
truePeakDbL: meter.truePeakDbL,
|
|
757
|
+
truePeakDbR: meter.truePeakDbR,
|
|
758
|
+
momentaryLufs: meter.momentaryLufs,
|
|
759
|
+
shortTermLufs: meter.shortTermLufs,
|
|
760
|
+
integratedLufs: meter.integratedLufs,
|
|
761
|
+
gainReductionDb: meter.gainReductionDb,
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
export function magnitudeToDb(value: number): number {
|
|
766
|
+
return value > 1.0e-12 ? 20 * Math.log10(value) : -120;
|
|
767
|
+
}
|