@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
package/src/worklet.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
+
import { panLawCode, panModeCode } from './codes';
|
|
1
2
|
import type {
|
|
2
3
|
EngineAutomationPoint,
|
|
3
4
|
EngineBus,
|
|
4
5
|
EngineCaptureStatus,
|
|
5
6
|
EngineClip,
|
|
6
7
|
EngineMarker,
|
|
7
|
-
EngineMeterTelemetry,
|
|
8
8
|
EngineMetronomeConfig,
|
|
9
9
|
EngineMidiClipSchedule,
|
|
10
10
|
EngineParameterInfo,
|
|
11
|
-
|
|
11
|
+
EngineScopeTelemetry,
|
|
12
12
|
EngineTempoSegment,
|
|
13
13
|
EngineTimeSignatureSegment,
|
|
14
14
|
EngineTrackLane,
|
|
@@ -16,7 +16,8 @@ import type {
|
|
|
16
16
|
EngineTransportState,
|
|
17
17
|
EqBand,
|
|
18
18
|
MixerRealtimeBuffer,
|
|
19
|
-
|
|
19
|
+
PanLaw,
|
|
20
|
+
PanMode,
|
|
20
21
|
} from './index';
|
|
21
22
|
import {
|
|
22
23
|
engineCapabilities,
|
|
@@ -26,7 +27,6 @@ import {
|
|
|
26
27
|
RealtimeEngine,
|
|
27
28
|
RealtimeVoiceChanger,
|
|
28
29
|
} from './index';
|
|
29
|
-
import type { AutomationCurve } from './public_types';
|
|
30
30
|
import type { SonareModule } from './sonare.js';
|
|
31
31
|
import type { SonareRtModule } from './sonare-rt';
|
|
32
32
|
|
|
@@ -37,104 +37,182 @@ import type { SonareRtModule } from './sonare-rt';
|
|
|
37
37
|
// `index` module.
|
|
38
38
|
export { init, isInitialized } from './index';
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
40
|
+
import type { WorkletInput, WorkletOutput } from './worklet/audio_types';
|
|
41
|
+
import {
|
|
42
|
+
isEngineCaptureRequestMessage,
|
|
43
|
+
isEngineCaptureResponseMessage,
|
|
44
|
+
isEngineCommandRecord,
|
|
45
|
+
isEngineSyncMessage,
|
|
46
|
+
isEngineTelemetryRecord,
|
|
47
|
+
isEngineTransportRequestMessage,
|
|
48
|
+
isEngineTransportResponseMessage,
|
|
49
|
+
isMeterSnapshot,
|
|
50
|
+
isRealtimeVoiceChangerMessage,
|
|
51
|
+
isWorkletMessage,
|
|
52
|
+
} from './worklet/guards';
|
|
53
|
+
import {
|
|
54
|
+
DEFAULT_METRONOME_CONFIG,
|
|
55
|
+
type ResolvedMetronomeConfig,
|
|
56
|
+
resolveMetronomeConfig,
|
|
57
|
+
type SonareEngineCaptureRequestMessage,
|
|
58
|
+
type SonareEngineCaptureResponseMessage,
|
|
59
|
+
type SonareEngineInstrumentSyncMessage,
|
|
60
|
+
type SonareEngineSyncCaptureMessage,
|
|
61
|
+
type SonareEngineSyncMessage,
|
|
62
|
+
type SonareEngineTransportFacade,
|
|
63
|
+
type SonareEngineTransportRequestMessage,
|
|
64
|
+
type SonareEngineTransportResponseMessage,
|
|
65
|
+
type SonareRealtimeEngineNodeCapabilities,
|
|
66
|
+
type SonareRealtimeEngineNodeOptions,
|
|
67
|
+
type SonareRealtimeEngineWorkletProcessorOptions,
|
|
68
|
+
type SonareRealtimeVoiceChangerMessage,
|
|
69
|
+
type SonareRealtimeVoiceChangerWorkletProcessorOptions,
|
|
70
|
+
type SonareRtRealtimeEngineRuntimeOptions,
|
|
71
|
+
type SonareWorkletMessage,
|
|
72
|
+
type SonareWorkletProcessorOptions,
|
|
73
|
+
type WorkletPort,
|
|
74
|
+
type WorkletTransport,
|
|
75
|
+
} from './worklet/messages';
|
|
76
|
+
// --- internal modules (split out of this file; bundled back into a single
|
|
77
|
+
// dist/worklet.js by tsup, so the public surface is unchanged) ---
|
|
78
|
+
import {
|
|
79
|
+
createSonareEngineCommandRingBuffer,
|
|
80
|
+
createSonareEngineTelemetryRingBuffer,
|
|
81
|
+
createSonareMeterRingBuffer,
|
|
82
|
+
createSonareScopeRingBuffer,
|
|
83
|
+
ENGINE_MIXER_PARAM_FADER_DB,
|
|
84
|
+
ENGINE_MIXER_PARAM_PAN,
|
|
85
|
+
encodeFrameHi,
|
|
86
|
+
encodeFrameLo,
|
|
87
|
+
engineMixerBusTarget,
|
|
88
|
+
engineMixerLaneTarget,
|
|
89
|
+
engineMixerMasterTarget,
|
|
90
|
+
engineRingFromSharedBuffer,
|
|
91
|
+
isRecord,
|
|
92
|
+
magnitudeToDb,
|
|
93
|
+
meterFromEngine,
|
|
94
|
+
meterRingFromSharedBuffer,
|
|
95
|
+
popSonareEngineCommandRingBuffer,
|
|
96
|
+
pushSonareEngineCommandRingBuffer,
|
|
97
|
+
readSonareEngineTelemetryRingBuffer,
|
|
98
|
+
readSonareMeterRingBuffer,
|
|
99
|
+
readSonareScopeRingBuffer,
|
|
100
|
+
type SharedMeterRingWriter,
|
|
101
|
+
type SharedScopeRingWriter,
|
|
102
|
+
type SharedSpectrumRingWriter,
|
|
103
|
+
SONARE_ENGINE_COMMAND_RECORD_BYTES,
|
|
104
|
+
SONARE_ENGINE_TELEMETRY_RECORD_BYTES,
|
|
105
|
+
SONARE_METER_RING_RECORD_FLOATS,
|
|
106
|
+
SONARE_SCOPE_RING_RECORD_PREFIX_FLOATS,
|
|
107
|
+
type SonareEngineCommandRecord,
|
|
108
|
+
type SonareEngineCommandRingBuffer,
|
|
109
|
+
SonareEngineCommandType,
|
|
110
|
+
SonareEngineTelemetryError,
|
|
111
|
+
type SonareEngineTelemetryRecord,
|
|
112
|
+
type SonareEngineTelemetryRingBuffer,
|
|
113
|
+
SonareEngineTelemetryType,
|
|
114
|
+
type SonareMeterRingBuffer,
|
|
115
|
+
type SonareScopeRingBuffer,
|
|
116
|
+
type SonareWorkletMeterSnapshot,
|
|
117
|
+
type SonareWorkletScopeSnapshot,
|
|
118
|
+
type SonareWorkletSpectrumSnapshot,
|
|
119
|
+
scopeRingFromSharedBuffer,
|
|
120
|
+
spectrumRingFromSharedBuffer,
|
|
121
|
+
telemetryFromEngine,
|
|
122
|
+
toBigInt64,
|
|
123
|
+
toDb,
|
|
124
|
+
writeSonareEngineTelemetryRingBuffer,
|
|
125
|
+
} from './worklet/protocol';
|
|
126
|
+
|
|
127
|
+
export type {
|
|
128
|
+
SonareEngineCaptureRequestMessage,
|
|
129
|
+
SonareEngineCaptureResponseMessage,
|
|
130
|
+
SonareEngineSyncAutomationMessage,
|
|
131
|
+
SonareEngineSyncBuiltinInstrumentMessage,
|
|
132
|
+
SonareEngineSyncCaptureMessage,
|
|
133
|
+
SonareEngineSyncClipsDeltaMessage,
|
|
134
|
+
SonareEngineSyncClipsMessage,
|
|
135
|
+
SonareEngineSyncLoadSoundFontMessage,
|
|
136
|
+
SonareEngineSyncMarkersMessage,
|
|
137
|
+
SonareEngineSyncMasterStripEqBandMessage,
|
|
138
|
+
SonareEngineSyncMasterStripInsertBypassedMessage,
|
|
139
|
+
SonareEngineSyncMessage,
|
|
140
|
+
SonareEngineSyncMetronomeMessage,
|
|
141
|
+
SonareEngineSyncMidiCcMessage,
|
|
142
|
+
SonareEngineSyncMidiClipsMessage,
|
|
143
|
+
SonareEngineSyncMidiNoteMessage,
|
|
144
|
+
SonareEngineSyncMidiPanicMessage,
|
|
145
|
+
SonareEngineSyncMixerMessage,
|
|
146
|
+
SonareEngineSyncSf2InstrumentMessage,
|
|
147
|
+
SonareEngineSyncSynthInstrumentMessage,
|
|
148
|
+
SonareEngineSyncTempoMessage,
|
|
149
|
+
SonareEngineSyncTrackStripEqBandMessage,
|
|
150
|
+
SonareEngineSyncTrackStripInsertBypassedMessage,
|
|
151
|
+
SonareEngineTransportFacade,
|
|
152
|
+
SonareEngineTransportRequestMessage,
|
|
153
|
+
SonareEngineTransportResponseMessage,
|
|
154
|
+
SonareRealtimeEngineNodeCapabilities,
|
|
155
|
+
SonareRealtimeEngineNodeOptions,
|
|
156
|
+
SonareRealtimeEngineWorkletProcessorOptions,
|
|
157
|
+
SonareRealtimeVoiceChangerDestroyMessage,
|
|
158
|
+
SonareRealtimeVoiceChangerMessage,
|
|
159
|
+
SonareRealtimeVoiceChangerResetMessage,
|
|
160
|
+
SonareRealtimeVoiceChangerSetConfigMessage,
|
|
161
|
+
SonareRealtimeVoiceChangerWorkletProcessorOptions,
|
|
162
|
+
SonareRtRealtimeEngineRuntimeOptions,
|
|
163
|
+
SonareWorkletDestroyMessage,
|
|
164
|
+
SonareWorkletMessage,
|
|
165
|
+
SonareWorkletProcessorOptions,
|
|
166
|
+
SonareWorkletScheduleInsertAutomationMessage,
|
|
167
|
+
SonareWorkletSetMeterIntervalMessage,
|
|
168
|
+
SonareWorkletTransportMessage,
|
|
169
|
+
} from './worklet/messages';
|
|
170
|
+
export {
|
|
171
|
+
createSonareEngineCommandRingBuffer,
|
|
172
|
+
createSonareEngineTelemetryRingBuffer,
|
|
173
|
+
createSonareMeterRingBuffer,
|
|
174
|
+
createSonareScopeRingBuffer,
|
|
175
|
+
createSonareSpectrumRingBuffer,
|
|
176
|
+
decodeFrame,
|
|
177
|
+
encodeFrameHi,
|
|
178
|
+
encodeFrameLo,
|
|
179
|
+
popSonareEngineCommandRingBuffer,
|
|
180
|
+
pushSonareEngineCommandRingBuffer,
|
|
181
|
+
readSonareEngineTelemetryRingBuffer,
|
|
182
|
+
readSonareMeterRingBuffer,
|
|
183
|
+
readSonareScopeRingBuffer,
|
|
184
|
+
readSonareSpectrumRingBuffer,
|
|
185
|
+
SONARE_ENGINE_COMMAND_RECORD_BYTES,
|
|
186
|
+
SONARE_ENGINE_RING_HEADER_INTS,
|
|
187
|
+
SONARE_ENGINE_TELEMETRY_RECORD_BYTES,
|
|
188
|
+
SONARE_METER_RING_HEADER_INTS,
|
|
189
|
+
SONARE_METER_RING_RECORD_FLOATS,
|
|
190
|
+
SONARE_SCOPE_RING_HEADER_INTS,
|
|
191
|
+
SONARE_SPECTRUM_RING_HEADER_INTS,
|
|
192
|
+
type SonareEngineCommandRecord,
|
|
193
|
+
type SonareEngineCommandRingBuffer,
|
|
194
|
+
SonareEngineCommandType,
|
|
195
|
+
SonareEngineTelemetryError,
|
|
196
|
+
type SonareEngineTelemetryRecord,
|
|
197
|
+
type SonareEngineTelemetryRingBuffer,
|
|
198
|
+
type SonareEngineTelemetryRingReadResult,
|
|
199
|
+
SonareEngineTelemetryType,
|
|
200
|
+
type SonareMeterRingBuffer,
|
|
201
|
+
type SonareMeterRingReadResult,
|
|
202
|
+
type SonareScopeRingBuffer,
|
|
203
|
+
type SonareScopeRingReadResult,
|
|
204
|
+
type SonareSpectrumRingBuffer,
|
|
205
|
+
type SonareSpectrumRingReadResult,
|
|
206
|
+
type SonareWorkletMeterSnapshot,
|
|
207
|
+
type SonareWorkletScopeSnapshot,
|
|
208
|
+
type SonareWorkletSpectrumSnapshot,
|
|
209
|
+
sonareEngineCommandRingBufferByteLength,
|
|
210
|
+
sonareEngineTelemetryRingBufferByteLength,
|
|
211
|
+
sonareMeterRingBufferByteLength,
|
|
212
|
+
sonareScopeRingBufferByteLength,
|
|
213
|
+
sonareSpectrumRingBufferByteLength,
|
|
214
|
+
writeSonareEngineTelemetryRingBuffer,
|
|
215
|
+
} from './worklet/protocol';
|
|
138
216
|
|
|
139
217
|
export interface SonareEngineOptions extends SonareRealtimeEngineNodeOptions {
|
|
140
218
|
offlineEngine?: RealtimeEngine;
|
|
@@ -142,1010 +220,11 @@ export interface SonareEngineOptions extends SonareRealtimeEngineNodeOptions {
|
|
|
142
220
|
offlineChannelCount?: number;
|
|
143
221
|
}
|
|
144
222
|
|
|
145
|
-
export interface SonareEngineTransportFacade {
|
|
146
|
-
play(sampleTime?: number): boolean;
|
|
147
|
-
stop(sampleTime?: number): boolean;
|
|
148
|
-
seekPpq(ppq: number, sampleTime?: number): boolean;
|
|
149
|
-
seekSeconds(seconds: number, sampleTime?: number): boolean;
|
|
150
|
-
setTempo(bpm: number): void;
|
|
151
|
-
setTempoSegments(segments: readonly EngineTempoSegment[]): void;
|
|
152
|
-
setLoop(startPpq: number, endPpq: number, enabled?: boolean): boolean;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
223
|
type SuspendableAudioContext = BaseAudioContext & {
|
|
156
224
|
suspend?: () => Promise<void>;
|
|
157
225
|
resume?: () => Promise<void>;
|
|
158
226
|
};
|
|
159
227
|
|
|
160
|
-
const ENGINE_MIXER_TARGET_BASE = 0x4d580000;
|
|
161
|
-
const ENGINE_MIXER_PARAM_FADER_DB = 1;
|
|
162
|
-
const ENGINE_MIXER_PARAM_PAN = 2;
|
|
163
|
-
|
|
164
|
-
function engineMixerLaneTarget(laneIndex: number, paramKind: number): number {
|
|
165
|
-
return ENGINE_MIXER_TARGET_BASE | ((laneIndex & 0xff) << 8) | (paramKind & 0xff);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function engineMixerBusTarget(busIndex: number, paramKind: number): number {
|
|
169
|
-
return ENGINE_MIXER_TARGET_BASE | (((0xfe - busIndex) & 0xff) << 8) | (paramKind & 0xff);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function engineMixerMasterTarget(paramKind: number): number {
|
|
173
|
-
return ENGINE_MIXER_TARGET_BASE | (0xff << 8) | (paramKind & 0xff);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
type WorkletInput = readonly (readonly Float32Array[])[];
|
|
177
|
-
type WorkletOutput = Float32Array[][];
|
|
178
|
-
|
|
179
|
-
export interface SonareWorkletScheduleInsertAutomationMessage {
|
|
180
|
-
type: 'scheduleInsertAutomation';
|
|
181
|
-
stripIndex: number;
|
|
182
|
-
insertIndex: number;
|
|
183
|
-
paramId: number;
|
|
184
|
-
value: number;
|
|
185
|
-
samplePos?: number;
|
|
186
|
-
curve?: AutomationCurve;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
export interface SonareWorkletSetMeterIntervalMessage {
|
|
190
|
-
type: 'setMeterInterval';
|
|
191
|
-
frames: number;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
export interface SonareWorkletDestroyMessage {
|
|
195
|
-
type: 'destroy';
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
export type SonareWorkletMessage =
|
|
199
|
-
| SonareWorkletScheduleInsertAutomationMessage
|
|
200
|
-
| SonareWorkletSetMeterIntervalMessage
|
|
201
|
-
| SonareWorkletDestroyMessage;
|
|
202
|
-
|
|
203
|
-
export interface SonareWorkletMeterSnapshot {
|
|
204
|
-
type: 'meter';
|
|
205
|
-
targetId: number;
|
|
206
|
-
frame: number;
|
|
207
|
-
peakDbL: number;
|
|
208
|
-
peakDbR: number;
|
|
209
|
-
rmsDbL: number;
|
|
210
|
-
rmsDbR: number;
|
|
211
|
-
correlation: number;
|
|
212
|
-
truePeakDbL: number;
|
|
213
|
-
truePeakDbR: number;
|
|
214
|
-
momentaryLufs: number;
|
|
215
|
-
shortTermLufs: number;
|
|
216
|
-
integratedLufs: number;
|
|
217
|
-
gainReductionDb: number;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
export interface SonareWorkletSpectrumSnapshot {
|
|
221
|
-
type: 'spectrum';
|
|
222
|
-
frame: number;
|
|
223
|
-
bands: Float32Array;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
export type SonareWorkletTransportMessage =
|
|
227
|
-
| SonareWorkletMeterSnapshot
|
|
228
|
-
| SonareWorkletSpectrumSnapshot
|
|
229
|
-
| SonareEngineTelemetryRecord;
|
|
230
|
-
|
|
231
|
-
export const SONARE_METER_RING_HEADER_INTS = 4;
|
|
232
|
-
// Record layout: [frameLo, frameHi, targetId, peakDbL, peakDbR, rmsDbL, rmsDbR,
|
|
233
|
-
// correlation, truePeakDbL, truePeakDbR, momentaryLufs, shortTermLufs,
|
|
234
|
-
// integratedLufs, gainReductionDb].
|
|
235
|
-
// The sample-frame index is monotonically increasing and quickly exceeds the
|
|
236
|
-
// 2^24 exact-integer range of a single Float32 slot (~349 s at 48 kHz), so it is
|
|
237
|
-
// stored split across two Float32 lanes (low 24 bits + high bits) for exact
|
|
238
|
-
// reconstruction. See encodeFrameLo/encodeFrameHi/decodeFrame.
|
|
239
|
-
export const SONARE_METER_RING_RECORD_FLOATS = 14;
|
|
240
|
-
export const SONARE_SPECTRUM_RING_HEADER_INTS = 5;
|
|
241
|
-
|
|
242
|
-
/** Base for splitting a frame index into two exactly-representable Float32 lanes. */
|
|
243
|
-
const SONARE_FRAME_LANE_BASE = 0x1000000; // 2^24
|
|
244
|
-
|
|
245
|
-
/** Low 24 bits of a frame index (exact in Float32). */
|
|
246
|
-
export function encodeFrameLo(frame: number): number {
|
|
247
|
-
const f = Math.max(0, Math.floor(frame));
|
|
248
|
-
return f % SONARE_FRAME_LANE_BASE;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/** High bits of a frame index above 2^24 (exact in Float32 up to ~2^48). */
|
|
252
|
-
export function encodeFrameHi(frame: number): number {
|
|
253
|
-
const f = Math.max(0, Math.floor(frame));
|
|
254
|
-
return Math.floor(f / SONARE_FRAME_LANE_BASE);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/** Reconstruct a frame index from its low/high Float32 lanes. */
|
|
258
|
-
export function decodeFrame(lo: number, hi: number): number {
|
|
259
|
-
return hi * SONARE_FRAME_LANE_BASE + lo;
|
|
260
|
-
}
|
|
261
|
-
export const SONARE_ENGINE_RING_HEADER_INTS = 5;
|
|
262
|
-
export const SONARE_ENGINE_COMMAND_RECORD_BYTES = 32;
|
|
263
|
-
export const SONARE_ENGINE_TELEMETRY_RECORD_BYTES = 48;
|
|
264
|
-
|
|
265
|
-
export enum SonareEngineCommandType {
|
|
266
|
-
SetParam = 0,
|
|
267
|
-
SetParamSmoothed = 1,
|
|
268
|
-
TransportPlay = 2,
|
|
269
|
-
TransportStop = 3,
|
|
270
|
-
TransportSeekSample = 4,
|
|
271
|
-
TransportSeekPpq = 5,
|
|
272
|
-
SetTempoMap = 6,
|
|
273
|
-
SetLoop = 7,
|
|
274
|
-
SwapGraph = 8,
|
|
275
|
-
SwapAutomation = 9,
|
|
276
|
-
SetSoloMute = 10,
|
|
277
|
-
AddClip = 11,
|
|
278
|
-
RemoveClip = 12,
|
|
279
|
-
ArmRecord = 13,
|
|
280
|
-
Punch = 14,
|
|
281
|
-
SetMetronome = 15,
|
|
282
|
-
SetMarker = 16,
|
|
283
|
-
SeekMarker = 17,
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
export enum SonareEngineTelemetryType {
|
|
287
|
-
ProcessBlock = 0,
|
|
288
|
-
Error = 1,
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
export enum SonareEngineTelemetryError {
|
|
292
|
-
None = 0,
|
|
293
|
-
CommandQueueOverflow = 1,
|
|
294
|
-
PendingCommandOverflow = 2,
|
|
295
|
-
BoundaryOverflow = 3,
|
|
296
|
-
TelemetryOverflow = 4,
|
|
297
|
-
CaptureOverflow = 5,
|
|
298
|
-
MaxBlockExceeded = 6,
|
|
299
|
-
UnknownTarget = 7,
|
|
300
|
-
NonRealtimeSafeParameter = 8,
|
|
301
|
-
NotPrepared = 9,
|
|
302
|
-
NonQueueableCommand = 10,
|
|
303
|
-
AutomationBindTargetOverflow = 11,
|
|
304
|
-
StaleAutomationLanes = 12,
|
|
305
|
-
SmoothedParameterCapacity = 13,
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
interface WorkletTransport {
|
|
309
|
-
postMessage?: (
|
|
310
|
-
message:
|
|
311
|
-
| SonareWorkletTransportMessage
|
|
312
|
-
| SonareEngineCaptureResponseMessage
|
|
313
|
-
| SonareEngineTransportResponseMessage,
|
|
314
|
-
transfer?: Transferable[],
|
|
315
|
-
) => void;
|
|
316
|
-
onMeter?: (meter: SonareWorkletMeterSnapshot) => void;
|
|
317
|
-
onSpectrum?: (spectrum: SonareWorkletSpectrumSnapshot) => void;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
interface ResolvedMetronomeConfig {
|
|
321
|
-
beatGain: number;
|
|
322
|
-
accentGain: number;
|
|
323
|
-
clickSamples: number;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// Fallback metronome gains/click length used by the worklet consumer until the
|
|
327
|
-
// host posts a 'syncMetronome' config. Aligned with the embind setMetronome
|
|
328
|
-
// defaults (src/wasm/bindings.cpp) so offline and realtime metronomes match.
|
|
329
|
-
const DEFAULT_METRONOME_CONFIG: ResolvedMetronomeConfig = {
|
|
330
|
-
beatGain: 0.35,
|
|
331
|
-
accentGain: 0.7,
|
|
332
|
-
clickSamples: 96,
|
|
333
|
-
};
|
|
334
|
-
|
|
335
|
-
function resolveMetronomeConfig(config: EngineMetronomeConfig): ResolvedMetronomeConfig {
|
|
336
|
-
return {
|
|
337
|
-
beatGain: config.beatGain ?? DEFAULT_METRONOME_CONFIG.beatGain,
|
|
338
|
-
accentGain: config.accentGain ?? DEFAULT_METRONOME_CONFIG.accentGain,
|
|
339
|
-
clickSamples: config.clickSamples ?? DEFAULT_METRONOME_CONFIG.clickSamples,
|
|
340
|
-
};
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
export interface SonareMeterRingBuffer {
|
|
344
|
-
sharedBuffer: SharedArrayBuffer;
|
|
345
|
-
header: Int32Array;
|
|
346
|
-
records: Float32Array;
|
|
347
|
-
capacity: number;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
export interface SonareMeterRingReadResult {
|
|
351
|
-
nextReadIndex: number;
|
|
352
|
-
meters: SonareWorkletMeterSnapshot[];
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
export interface SonareSpectrumRingBuffer {
|
|
356
|
-
sharedBuffer: SharedArrayBuffer;
|
|
357
|
-
header: Int32Array;
|
|
358
|
-
records: Float32Array;
|
|
359
|
-
capacity: number;
|
|
360
|
-
bands: number;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
export interface SonareSpectrumRingReadResult {
|
|
364
|
-
nextReadIndex: number;
|
|
365
|
-
spectra: SonareWorkletSpectrumSnapshot[];
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
export interface SonareEngineCommandRecord {
|
|
369
|
-
type: SonareEngineCommandType | number;
|
|
370
|
-
targetId?: number;
|
|
371
|
-
sampleTime?: number | bigint;
|
|
372
|
-
argFloat?: number;
|
|
373
|
-
argInt?: number | bigint;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
// Out-of-band control messages posted from the main-thread SonareEngine facade
|
|
377
|
-
// to the worklet engine processor over node.port. Unlike SonareEngineCommandRecord
|
|
378
|
-
// (a small POD POSTed/ringed every block) these carry bulk/structured payloads
|
|
379
|
-
// (clip audio buffers, marker lists, metronome config) that cannot fit the
|
|
380
|
-
// fixed-size SAB command record, so they are applied OUTSIDE process() — the
|
|
381
|
-
// audio-thread equivalent of the engine's control-thread RtPublisher setters.
|
|
382
|
-
export interface SonareEngineSyncClipsMessage {
|
|
383
|
-
type: 'syncClips';
|
|
384
|
-
clips: EngineClip[];
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
export interface SonareEngineSyncClipsDeltaMessage {
|
|
388
|
-
type: 'syncClipsDelta';
|
|
389
|
-
upserts: EngineClip[];
|
|
390
|
-
removeIds: number[];
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
export interface SonareEngineSyncMidiClipsMessage {
|
|
394
|
-
type: 'syncMidiClips';
|
|
395
|
-
clips: EngineMidiClipSchedule[];
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
export interface SonareEngineSyncMarkersMessage {
|
|
399
|
-
type: 'syncMarkers';
|
|
400
|
-
markers: EngineMarker[];
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
export interface SonareEngineSyncMetronomeMessage {
|
|
404
|
-
type: 'syncMetronome';
|
|
405
|
-
config: EngineMetronomeConfig;
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
export interface SonareEngineSyncAutomationMessage {
|
|
409
|
-
type: 'syncAutomation';
|
|
410
|
-
paramId: number;
|
|
411
|
-
points: EngineAutomationPoint[];
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
export interface SonareEngineSyncTempoMessage {
|
|
415
|
-
type: 'syncTempo';
|
|
416
|
-
bpm: number;
|
|
417
|
-
timeSignature: { numerator: number; denominator: number };
|
|
418
|
-
tempoSegments?: EngineTempoSegment[];
|
|
419
|
-
timeSignatureSegments?: EngineTimeSignatureSegment[];
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
export interface SonareEngineSyncMixerMessage {
|
|
423
|
-
type: 'syncMixer';
|
|
424
|
-
lanes: EngineTrackLane[];
|
|
425
|
-
buses?: EngineBus[];
|
|
426
|
-
trackStrips?: Array<{ trackId: number; sceneJson: string }>;
|
|
427
|
-
busStrips?: Array<{ busId: number; sceneJson: string }>;
|
|
428
|
-
masterStripJson?: string;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
export interface SonareEngineSyncCaptureMessage {
|
|
432
|
-
type: 'syncCapture';
|
|
433
|
-
bufferFrames: number;
|
|
434
|
-
channels: number;
|
|
435
|
-
source: EngineCaptureStatus['source'];
|
|
436
|
-
recordOffsetSamples: number;
|
|
437
|
-
inputMonitor: { enabled: boolean; gain: number };
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
export interface SonareEngineSyncTrackStripEqBandMessage {
|
|
441
|
-
type: 'syncTrackStripEqBand';
|
|
442
|
-
trackId: number;
|
|
443
|
-
bandIndex: number;
|
|
444
|
-
bandJson: string;
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
export interface SonareEngineSyncMasterStripEqBandMessage {
|
|
448
|
-
type: 'syncMasterStripEqBand';
|
|
449
|
-
bandIndex: number;
|
|
450
|
-
bandJson: string;
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
export interface SonareEngineSyncTrackStripInsertBypassedMessage {
|
|
454
|
-
type: 'syncTrackStripInsertBypassed';
|
|
455
|
-
trackId: number;
|
|
456
|
-
insertIndex: number;
|
|
457
|
-
bypassed: boolean;
|
|
458
|
-
resetOnBypass: boolean;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
export interface SonareEngineSyncMasterStripInsertBypassedMessage {
|
|
462
|
-
type: 'syncMasterStripInsertBypassed';
|
|
463
|
-
insertIndex: number;
|
|
464
|
-
bypassed: boolean;
|
|
465
|
-
resetOnBypass: boolean;
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
export interface SonareEngineSyncBuiltinInstrumentMessage {
|
|
469
|
-
type: 'syncBuiltinInstrument';
|
|
470
|
-
destinationId: number;
|
|
471
|
-
config: { destinationId?: number } & Record<string, unknown>;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
export interface SonareEngineSyncSynthInstrumentMessage {
|
|
475
|
-
type: 'syncSynthInstrument';
|
|
476
|
-
destinationId: number;
|
|
477
|
-
patch: Record<string, unknown> | string;
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
export interface SonareEngineSyncSf2InstrumentMessage {
|
|
481
|
-
type: 'syncSf2Instrument';
|
|
482
|
-
destinationId: number;
|
|
483
|
-
config: { destinationId?: number; gain?: number; polyphony?: number };
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
export interface SonareEngineSyncLoadSoundFontMessage {
|
|
487
|
-
type: 'syncLoadSoundFont';
|
|
488
|
-
data: Uint8Array;
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
export interface SonareEngineSyncMidiNoteMessage {
|
|
492
|
-
type: 'syncMidiNoteOn' | 'syncMidiNoteOff';
|
|
493
|
-
destinationId: number;
|
|
494
|
-
group: number;
|
|
495
|
-
channel: number;
|
|
496
|
-
note: number;
|
|
497
|
-
velocity: number;
|
|
498
|
-
renderFrame: number;
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
export interface SonareEngineSyncMidiCcMessage {
|
|
502
|
-
type: 'syncMidiCc';
|
|
503
|
-
destinationId: number;
|
|
504
|
-
group: number;
|
|
505
|
-
channel: number;
|
|
506
|
-
controller: number;
|
|
507
|
-
value: number;
|
|
508
|
-
renderFrame: number;
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
export interface SonareEngineSyncMidiPanicMessage {
|
|
512
|
-
type: 'syncMidiPanic';
|
|
513
|
-
renderFrame: number;
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
type SonareEngineInstrumentSyncMessage =
|
|
517
|
-
| SonareEngineSyncBuiltinInstrumentMessage
|
|
518
|
-
| SonareEngineSyncSynthInstrumentMessage
|
|
519
|
-
| SonareEngineSyncSf2InstrumentMessage
|
|
520
|
-
| SonareEngineSyncLoadSoundFontMessage;
|
|
521
|
-
|
|
522
|
-
export type SonareEngineSyncMessage =
|
|
523
|
-
| SonareEngineSyncClipsMessage
|
|
524
|
-
| SonareEngineSyncClipsDeltaMessage
|
|
525
|
-
| SonareEngineSyncMidiClipsMessage
|
|
526
|
-
| SonareEngineSyncMarkersMessage
|
|
527
|
-
| SonareEngineSyncMetronomeMessage
|
|
528
|
-
| SonareEngineSyncAutomationMessage
|
|
529
|
-
| SonareEngineSyncTempoMessage
|
|
530
|
-
| SonareEngineSyncMixerMessage
|
|
531
|
-
| SonareEngineSyncCaptureMessage
|
|
532
|
-
| SonareEngineSyncTrackStripEqBandMessage
|
|
533
|
-
| SonareEngineSyncMasterStripEqBandMessage
|
|
534
|
-
| SonareEngineSyncTrackStripInsertBypassedMessage
|
|
535
|
-
| SonareEngineSyncMasterStripInsertBypassedMessage
|
|
536
|
-
| SonareEngineSyncBuiltinInstrumentMessage
|
|
537
|
-
| SonareEngineSyncSynthInstrumentMessage
|
|
538
|
-
| SonareEngineSyncSf2InstrumentMessage
|
|
539
|
-
| SonareEngineSyncLoadSoundFontMessage
|
|
540
|
-
| SonareEngineSyncMidiNoteMessage
|
|
541
|
-
| SonareEngineSyncMidiCcMessage
|
|
542
|
-
| SonareEngineSyncMidiPanicMessage;
|
|
543
|
-
|
|
544
|
-
export interface SonareEngineTelemetryRecord {
|
|
545
|
-
type: SonareEngineTelemetryType | number;
|
|
546
|
-
error: SonareEngineTelemetryError | number;
|
|
547
|
-
renderFrame: number;
|
|
548
|
-
timelineSample: number;
|
|
549
|
-
audibleTimelineSample: number;
|
|
550
|
-
graphLatencySamplesQ8: number;
|
|
551
|
-
value: number;
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
export interface SonareEngineCommandRingBuffer {
|
|
555
|
-
sharedBuffer: SharedArrayBuffer;
|
|
556
|
-
header: Int32Array;
|
|
557
|
-
view: DataView;
|
|
558
|
-
capacity: number;
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
export interface SonareEngineTelemetryRingBuffer {
|
|
562
|
-
sharedBuffer: SharedArrayBuffer;
|
|
563
|
-
header: Int32Array;
|
|
564
|
-
view: DataView;
|
|
565
|
-
capacity: number;
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
export interface SonareEngineTelemetryRingReadResult {
|
|
569
|
-
nextReadIndex: number;
|
|
570
|
-
telemetry: SonareEngineTelemetryRecord[];
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
interface SharedMeterRingWriter {
|
|
574
|
-
header: Int32Array;
|
|
575
|
-
records: Float32Array;
|
|
576
|
-
capacity: number;
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
interface SharedSpectrumRingWriter {
|
|
580
|
-
header: Int32Array;
|
|
581
|
-
records: Float32Array;
|
|
582
|
-
capacity: number;
|
|
583
|
-
bands: number;
|
|
584
|
-
recordFloats: number;
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
interface WorkletPort {
|
|
588
|
-
postMessage?: (message: unknown, transfer?: Transferable[]) => void;
|
|
589
|
-
onmessage?: (event: { data: unknown }) => void;
|
|
590
|
-
addEventListener?: (type: 'message', listener: (event: { data: unknown }) => void) => void;
|
|
591
|
-
start?: () => void;
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
export interface SonareEngineCaptureRequestMessage {
|
|
595
|
-
type: 'captureRequest';
|
|
596
|
-
requestId: number;
|
|
597
|
-
op: 'status' | 'read' | 'reset';
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
export interface SonareEngineCaptureResponseMessage {
|
|
601
|
-
type: 'captureResponse';
|
|
602
|
-
requestId: number;
|
|
603
|
-
ok: boolean;
|
|
604
|
-
status?: EngineCaptureStatus;
|
|
605
|
-
channels?: Float32Array[] | number[][];
|
|
606
|
-
error?: string;
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
export interface SonareEngineTransportRequestMessage {
|
|
610
|
-
type: 'transportRequest';
|
|
611
|
-
requestId: number;
|
|
612
|
-
op: 'state';
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
export interface SonareEngineTransportResponseMessage {
|
|
616
|
-
type: 'transportResponse';
|
|
617
|
-
requestId: number;
|
|
618
|
-
ok: boolean;
|
|
619
|
-
state?: EngineTransportState;
|
|
620
|
-
error?: string;
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
function toDb(value: number): number {
|
|
624
|
-
return value > 0 ? 20 * Math.log10(value) : Number.NEGATIVE_INFINITY;
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
628
|
-
return typeof value === 'object' && value !== null;
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
function isWorkletMessage(value: unknown): value is SonareWorkletMessage {
|
|
632
|
-
if (!isRecord(value) || typeof value.type !== 'string') {
|
|
633
|
-
return false;
|
|
634
|
-
}
|
|
635
|
-
return (
|
|
636
|
-
value.type === 'scheduleInsertAutomation' ||
|
|
637
|
-
value.type === 'setMeterInterval' ||
|
|
638
|
-
value.type === 'destroy'
|
|
639
|
-
);
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
function isEngineCommandRecord(value: unknown): value is SonareEngineCommandRecord {
|
|
643
|
-
return isRecord(value) && typeof value.type === 'number';
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
function isEngineSyncMessage(value: unknown): value is SonareEngineSyncMessage {
|
|
647
|
-
if (!isRecord(value) || typeof value.type !== 'string') {
|
|
648
|
-
return false;
|
|
649
|
-
}
|
|
650
|
-
return (
|
|
651
|
-
value.type === 'syncClips' ||
|
|
652
|
-
value.type === 'syncClipsDelta' ||
|
|
653
|
-
value.type === 'syncMidiClips' ||
|
|
654
|
-
value.type === 'syncMarkers' ||
|
|
655
|
-
value.type === 'syncMetronome' ||
|
|
656
|
-
value.type === 'syncAutomation' ||
|
|
657
|
-
value.type === 'syncTempo' ||
|
|
658
|
-
value.type === 'syncMixer' ||
|
|
659
|
-
value.type === 'syncCapture' ||
|
|
660
|
-
value.type === 'syncTrackStripEqBand' ||
|
|
661
|
-
value.type === 'syncMasterStripEqBand' ||
|
|
662
|
-
value.type === 'syncTrackStripInsertBypassed' ||
|
|
663
|
-
value.type === 'syncMasterStripInsertBypassed' ||
|
|
664
|
-
value.type === 'syncBuiltinInstrument' ||
|
|
665
|
-
value.type === 'syncSynthInstrument' ||
|
|
666
|
-
value.type === 'syncSf2Instrument' ||
|
|
667
|
-
value.type === 'syncLoadSoundFont' ||
|
|
668
|
-
value.type === 'syncMidiNoteOn' ||
|
|
669
|
-
value.type === 'syncMidiNoteOff' ||
|
|
670
|
-
value.type === 'syncMidiCc' ||
|
|
671
|
-
value.type === 'syncMidiPanic'
|
|
672
|
-
);
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
function isEngineCaptureRequestMessage(value: unknown): value is SonareEngineCaptureRequestMessage {
|
|
676
|
-
return (
|
|
677
|
-
isRecord(value) &&
|
|
678
|
-
value.type === 'captureRequest' &&
|
|
679
|
-
typeof value.requestId === 'number' &&
|
|
680
|
-
(value.op === 'status' || value.op === 'read' || value.op === 'reset')
|
|
681
|
-
);
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
function isEngineCaptureResponseMessage(
|
|
685
|
-
value: unknown,
|
|
686
|
-
): value is SonareEngineCaptureResponseMessage {
|
|
687
|
-
return (
|
|
688
|
-
isRecord(value) &&
|
|
689
|
-
value.type === 'captureResponse' &&
|
|
690
|
-
typeof value.requestId === 'number' &&
|
|
691
|
-
typeof value.ok === 'boolean'
|
|
692
|
-
);
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
function isEngineTransportRequestMessage(
|
|
696
|
-
value: unknown,
|
|
697
|
-
): value is SonareEngineTransportRequestMessage {
|
|
698
|
-
return (
|
|
699
|
-
isRecord(value) &&
|
|
700
|
-
value.type === 'transportRequest' &&
|
|
701
|
-
typeof value.requestId === 'number' &&
|
|
702
|
-
value.op === 'state'
|
|
703
|
-
);
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
function isEngineTransportResponseMessage(
|
|
707
|
-
value: unknown,
|
|
708
|
-
): value is SonareEngineTransportResponseMessage {
|
|
709
|
-
return (
|
|
710
|
-
isRecord(value) &&
|
|
711
|
-
value.type === 'transportResponse' &&
|
|
712
|
-
typeof value.requestId === 'number' &&
|
|
713
|
-
typeof value.ok === 'boolean'
|
|
714
|
-
);
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
function isRealtimeVoiceChangerMessage(value: unknown): value is SonareRealtimeVoiceChangerMessage {
|
|
718
|
-
if (!isRecord(value) || typeof value.type !== 'string') {
|
|
719
|
-
return false;
|
|
720
|
-
}
|
|
721
|
-
return value.type === 'setConfig' || value.type === 'reset' || value.type === 'destroy';
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
function isEngineTelemetryRecord(value: unknown): value is SonareEngineTelemetryRecord {
|
|
725
|
-
return (
|
|
726
|
-
isRecord(value) &&
|
|
727
|
-
typeof value.type === 'number' &&
|
|
728
|
-
typeof value.error === 'number' &&
|
|
729
|
-
typeof value.renderFrame === 'number' &&
|
|
730
|
-
typeof value.timelineSample === 'number' &&
|
|
731
|
-
typeof value.audibleTimelineSample === 'number' &&
|
|
732
|
-
typeof value.graphLatencySamplesQ8 === 'number' &&
|
|
733
|
-
typeof value.value === 'number'
|
|
734
|
-
);
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
function isMeterSnapshot(value: unknown): value is SonareWorkletMeterSnapshot {
|
|
738
|
-
return (
|
|
739
|
-
isRecord(value) &&
|
|
740
|
-
value.type === 'meter' &&
|
|
741
|
-
typeof value.frame === 'number' &&
|
|
742
|
-
typeof value.peakDbL === 'number' &&
|
|
743
|
-
typeof value.peakDbR === 'number' &&
|
|
744
|
-
typeof value.rmsDbL === 'number' &&
|
|
745
|
-
typeof value.rmsDbR === 'number' &&
|
|
746
|
-
typeof value.correlation === 'number' &&
|
|
747
|
-
(typeof value.targetId === 'number' || value.targetId === undefined)
|
|
748
|
-
);
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
export function sonareMeterRingBufferByteLength(capacity: number): number {
|
|
752
|
-
const clampedCapacity = Math.max(1, Math.floor(capacity));
|
|
753
|
-
return (
|
|
754
|
-
SONARE_METER_RING_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT +
|
|
755
|
-
clampedCapacity * SONARE_METER_RING_RECORD_FLOATS * Float32Array.BYTES_PER_ELEMENT
|
|
756
|
-
);
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
export function createSonareMeterRingBuffer(capacity = 128): SonareMeterRingBuffer {
|
|
760
|
-
const clampedCapacity = Math.max(1, Math.floor(capacity));
|
|
761
|
-
const sharedBuffer = new SharedArrayBuffer(sonareMeterRingBufferByteLength(clampedCapacity));
|
|
762
|
-
const ring = meterRingFromSharedBuffer(sharedBuffer, clampedCapacity);
|
|
763
|
-
Atomics.store(ring.header, 0, 0);
|
|
764
|
-
Atomics.store(ring.header, 1, clampedCapacity);
|
|
765
|
-
Atomics.store(ring.header, 2, SONARE_METER_RING_RECORD_FLOATS);
|
|
766
|
-
Atomics.store(ring.header, 3, 0);
|
|
767
|
-
return { sharedBuffer, header: ring.header, records: ring.records, capacity: ring.capacity };
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
export function readSonareMeterRingBuffer(
|
|
771
|
-
ring: SonareMeterRingBuffer,
|
|
772
|
-
readIndex = 0,
|
|
773
|
-
): SonareMeterRingReadResult {
|
|
774
|
-
const writeIndex = Atomics.load(ring.header, 0);
|
|
775
|
-
const recordFloats = Atomics.load(ring.header, 2) || SONARE_METER_RING_RECORD_FLOATS;
|
|
776
|
-
const nextReadIndex = Math.max(0, Math.min(readIndex, writeIndex));
|
|
777
|
-
const firstReadable = Math.max(nextReadIndex, writeIndex - ring.capacity);
|
|
778
|
-
const meters: SonareWorkletMeterSnapshot[] = [];
|
|
779
|
-
for (let index = firstReadable; index < writeIndex; index++) {
|
|
780
|
-
const offset = (index % ring.capacity) * recordFloats;
|
|
781
|
-
meters.push({
|
|
782
|
-
type: 'meter',
|
|
783
|
-
frame: decodeFrame(ring.records[offset], ring.records[offset + 1]),
|
|
784
|
-
targetId: ring.records[offset + 2],
|
|
785
|
-
peakDbL: ring.records[offset + 3],
|
|
786
|
-
peakDbR: ring.records[offset + 4],
|
|
787
|
-
rmsDbL: ring.records[offset + 5],
|
|
788
|
-
rmsDbR: ring.records[offset + 6],
|
|
789
|
-
correlation: ring.records[offset + 7],
|
|
790
|
-
truePeakDbL: ring.records[offset + 8],
|
|
791
|
-
truePeakDbR: ring.records[offset + 9],
|
|
792
|
-
momentaryLufs: ring.records[offset + 10],
|
|
793
|
-
shortTermLufs: ring.records[offset + 11],
|
|
794
|
-
integratedLufs: ring.records[offset + 12],
|
|
795
|
-
gainReductionDb: ring.records[offset + 13],
|
|
796
|
-
});
|
|
797
|
-
}
|
|
798
|
-
return { nextReadIndex: writeIndex, meters };
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
export function sonareSpectrumRingBufferByteLength(capacity: number, bands = 16): number {
|
|
802
|
-
const clampedCapacity = Math.max(1, Math.floor(capacity));
|
|
803
|
-
const clampedBands = Math.max(1, Math.floor(bands));
|
|
804
|
-
// Record layout: [frameLo, frameHi, bandCount, band0, band1, ...]. frame is
|
|
805
|
-
// split across two Float32 lanes for exact reconstruction beyond 2^24.
|
|
806
|
-
return (
|
|
807
|
-
SONARE_SPECTRUM_RING_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT +
|
|
808
|
-
clampedCapacity * (3 + clampedBands) * Float32Array.BYTES_PER_ELEMENT
|
|
809
|
-
);
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
export function createSonareSpectrumRingBuffer(
|
|
813
|
-
capacity = 128,
|
|
814
|
-
bands = 16,
|
|
815
|
-
): SonareSpectrumRingBuffer {
|
|
816
|
-
const clampedCapacity = Math.max(1, Math.floor(capacity));
|
|
817
|
-
const clampedBands = Math.max(1, Math.floor(bands));
|
|
818
|
-
const sharedBuffer = new SharedArrayBuffer(
|
|
819
|
-
sonareSpectrumRingBufferByteLength(clampedCapacity, clampedBands),
|
|
820
|
-
);
|
|
821
|
-
const ring = spectrumRingFromSharedBuffer(sharedBuffer, clampedCapacity, clampedBands);
|
|
822
|
-
Atomics.store(ring.header, 0, 0);
|
|
823
|
-
Atomics.store(ring.header, 1, clampedCapacity);
|
|
824
|
-
Atomics.store(ring.header, 2, ring.recordFloats);
|
|
825
|
-
Atomics.store(ring.header, 3, clampedBands);
|
|
826
|
-
Atomics.store(ring.header, 4, 0);
|
|
827
|
-
return {
|
|
828
|
-
sharedBuffer,
|
|
829
|
-
header: ring.header,
|
|
830
|
-
records: ring.records,
|
|
831
|
-
capacity: ring.capacity,
|
|
832
|
-
bands: ring.bands,
|
|
833
|
-
};
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
export function readSonareSpectrumRingBuffer(
|
|
837
|
-
ring: SonareSpectrumRingBuffer,
|
|
838
|
-
readIndex = 0,
|
|
839
|
-
): SonareSpectrumRingReadResult {
|
|
840
|
-
const writeIndex = Atomics.load(ring.header, 0);
|
|
841
|
-
const recordFloats = Atomics.load(ring.header, 2) || 3 + ring.bands;
|
|
842
|
-
const bands = Atomics.load(ring.header, 3) || ring.bands;
|
|
843
|
-
const nextReadIndex = Math.max(0, Math.min(readIndex, writeIndex));
|
|
844
|
-
const firstReadable = Math.max(nextReadIndex, writeIndex - ring.capacity);
|
|
845
|
-
const spectra: SonareWorkletSpectrumSnapshot[] = [];
|
|
846
|
-
for (let index = firstReadable; index < writeIndex; index++) {
|
|
847
|
-
const offset = (index % ring.capacity) * recordFloats;
|
|
848
|
-
const values = new Float32Array(bands);
|
|
849
|
-
values.set(ring.records.subarray(offset + 3, offset + 3 + bands));
|
|
850
|
-
spectra.push({
|
|
851
|
-
type: 'spectrum',
|
|
852
|
-
frame: decodeFrame(ring.records[offset], ring.records[offset + 1]),
|
|
853
|
-
bands: values,
|
|
854
|
-
});
|
|
855
|
-
}
|
|
856
|
-
return { nextReadIndex: writeIndex, spectra };
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
export function sonareEngineCommandRingBufferByteLength(capacity: number): number {
|
|
860
|
-
const clampedCapacity = Math.max(1, Math.floor(capacity));
|
|
861
|
-
return (
|
|
862
|
-
SONARE_ENGINE_RING_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT +
|
|
863
|
-
clampedCapacity * SONARE_ENGINE_COMMAND_RECORD_BYTES
|
|
864
|
-
);
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
export function sonareEngineTelemetryRingBufferByteLength(capacity: number): number {
|
|
868
|
-
const clampedCapacity = Math.max(1, Math.floor(capacity));
|
|
869
|
-
return (
|
|
870
|
-
SONARE_ENGINE_RING_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT +
|
|
871
|
-
clampedCapacity * SONARE_ENGINE_TELEMETRY_RECORD_BYTES
|
|
872
|
-
);
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
export function createSonareEngineCommandRingBuffer(capacity = 128): SonareEngineCommandRingBuffer {
|
|
876
|
-
const clampedCapacity = Math.max(1, Math.floor(capacity));
|
|
877
|
-
const sharedBuffer = new SharedArrayBuffer(
|
|
878
|
-
sonareEngineCommandRingBufferByteLength(clampedCapacity),
|
|
879
|
-
);
|
|
880
|
-
const ring = engineRingFromSharedBuffer(
|
|
881
|
-
sharedBuffer,
|
|
882
|
-
SONARE_ENGINE_COMMAND_RECORD_BYTES,
|
|
883
|
-
clampedCapacity,
|
|
884
|
-
);
|
|
885
|
-
return { sharedBuffer, header: ring.header, view: ring.view, capacity: ring.capacity };
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
export function createSonareEngineTelemetryRingBuffer(
|
|
889
|
-
capacity = 128,
|
|
890
|
-
): SonareEngineTelemetryRingBuffer {
|
|
891
|
-
const clampedCapacity = Math.max(1, Math.floor(capacity));
|
|
892
|
-
const sharedBuffer = new SharedArrayBuffer(
|
|
893
|
-
sonareEngineTelemetryRingBufferByteLength(clampedCapacity),
|
|
894
|
-
);
|
|
895
|
-
const ring = engineRingFromSharedBuffer(
|
|
896
|
-
sharedBuffer,
|
|
897
|
-
SONARE_ENGINE_TELEMETRY_RECORD_BYTES,
|
|
898
|
-
clampedCapacity,
|
|
899
|
-
);
|
|
900
|
-
return { sharedBuffer, header: ring.header, view: ring.view, capacity: ring.capacity };
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
export function pushSonareEngineCommandRingBuffer(
|
|
904
|
-
ring: SonareEngineCommandRingBuffer,
|
|
905
|
-
command: SonareEngineCommandRecord,
|
|
906
|
-
): boolean {
|
|
907
|
-
const writeIndex = Atomics.load(ring.header, 0);
|
|
908
|
-
const readIndex = Atomics.load(ring.header, 1);
|
|
909
|
-
if (writeIndex - readIndex >= ring.capacity) {
|
|
910
|
-
Atomics.add(ring.header, 4, 1);
|
|
911
|
-
return false;
|
|
912
|
-
}
|
|
913
|
-
writeEngineCommandRecord(
|
|
914
|
-
ring.view,
|
|
915
|
-
recordOffset(writeIndex, ring.capacity, SONARE_ENGINE_COMMAND_RECORD_BYTES),
|
|
916
|
-
command,
|
|
917
|
-
);
|
|
918
|
-
Atomics.store(ring.header, 0, writeIndex + 1);
|
|
919
|
-
return true;
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
export function popSonareEngineCommandRingBuffer(
|
|
923
|
-
ring: SonareEngineCommandRingBuffer,
|
|
924
|
-
): SonareEngineCommandRecord | null {
|
|
925
|
-
const readIndex = Atomics.load(ring.header, 1);
|
|
926
|
-
const writeIndex = Atomics.load(ring.header, 0);
|
|
927
|
-
if (readIndex >= writeIndex) {
|
|
928
|
-
return null;
|
|
929
|
-
}
|
|
930
|
-
const command = readEngineCommandRecord(
|
|
931
|
-
ring.view,
|
|
932
|
-
recordOffset(readIndex, ring.capacity, SONARE_ENGINE_COMMAND_RECORD_BYTES),
|
|
933
|
-
);
|
|
934
|
-
Atomics.store(ring.header, 1, readIndex + 1);
|
|
935
|
-
return command;
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
export function writeSonareEngineTelemetryRingBuffer(
|
|
939
|
-
ring: SonareEngineTelemetryRingBuffer,
|
|
940
|
-
telemetry: SonareEngineTelemetryRecord,
|
|
941
|
-
): void {
|
|
942
|
-
const writeIndex = Atomics.load(ring.header, 0);
|
|
943
|
-
writeEngineTelemetryRecord(
|
|
944
|
-
ring.view,
|
|
945
|
-
recordOffset(writeIndex, ring.capacity, SONARE_ENGINE_TELEMETRY_RECORD_BYTES),
|
|
946
|
-
telemetry,
|
|
947
|
-
);
|
|
948
|
-
Atomics.store(ring.header, 0, writeIndex + 1);
|
|
949
|
-
if (writeIndex + 1 > ring.capacity) {
|
|
950
|
-
Atomics.store(ring.header, 4, writeIndex + 1 - ring.capacity);
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
export function readSonareEngineTelemetryRingBuffer(
|
|
955
|
-
ring: SonareEngineTelemetryRingBuffer,
|
|
956
|
-
readIndex = 0,
|
|
957
|
-
): SonareEngineTelemetryRingReadResult {
|
|
958
|
-
const writeIndex = Atomics.load(ring.header, 0);
|
|
959
|
-
const nextReadIndex = Math.max(0, Math.min(readIndex, writeIndex));
|
|
960
|
-
const firstReadable = Math.max(nextReadIndex, writeIndex - ring.capacity);
|
|
961
|
-
const telemetry: SonareEngineTelemetryRecord[] = [];
|
|
962
|
-
for (let index = firstReadable; index < writeIndex; index++) {
|
|
963
|
-
telemetry.push(
|
|
964
|
-
readEngineTelemetryRecord(
|
|
965
|
-
ring.view,
|
|
966
|
-
recordOffset(index, ring.capacity, SONARE_ENGINE_TELEMETRY_RECORD_BYTES),
|
|
967
|
-
),
|
|
968
|
-
);
|
|
969
|
-
}
|
|
970
|
-
return { nextReadIndex: writeIndex, telemetry };
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
function meterRingFromSharedBuffer(
|
|
974
|
-
sharedBuffer: SharedArrayBuffer,
|
|
975
|
-
fallbackCapacity?: number,
|
|
976
|
-
): SharedMeterRingWriter {
|
|
977
|
-
const headerBytes = SONARE_METER_RING_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT;
|
|
978
|
-
const header = new Int32Array(sharedBuffer, 0, SONARE_METER_RING_HEADER_INTS);
|
|
979
|
-
const existingCapacity = Atomics.load(header, 1);
|
|
980
|
-
const capacity = Math.max(1, Math.floor(existingCapacity || fallbackCapacity || 1));
|
|
981
|
-
const minBytes = sonareMeterRingBufferByteLength(capacity);
|
|
982
|
-
if (sharedBuffer.byteLength < minBytes) {
|
|
983
|
-
throw new Error('meterSharedBuffer is too small for the requested ring capacity.');
|
|
984
|
-
}
|
|
985
|
-
Atomics.store(header, 1, capacity);
|
|
986
|
-
Atomics.store(header, 2, SONARE_METER_RING_RECORD_FLOATS);
|
|
987
|
-
return {
|
|
988
|
-
header,
|
|
989
|
-
records: new Float32Array(
|
|
990
|
-
sharedBuffer,
|
|
991
|
-
headerBytes,
|
|
992
|
-
capacity * SONARE_METER_RING_RECORD_FLOATS,
|
|
993
|
-
),
|
|
994
|
-
capacity,
|
|
995
|
-
};
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
function spectrumRingFromSharedBuffer(
|
|
999
|
-
sharedBuffer: SharedArrayBuffer,
|
|
1000
|
-
fallbackCapacity?: number,
|
|
1001
|
-
fallbackBands?: number,
|
|
1002
|
-
): SharedSpectrumRingWriter {
|
|
1003
|
-
const headerBytes = SONARE_SPECTRUM_RING_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT;
|
|
1004
|
-
const header = new Int32Array(sharedBuffer, 0, SONARE_SPECTRUM_RING_HEADER_INTS);
|
|
1005
|
-
const existingCapacity = Atomics.load(header, 1);
|
|
1006
|
-
const existingBands = Atomics.load(header, 3);
|
|
1007
|
-
const capacity = Math.max(1, Math.floor(existingCapacity || fallbackCapacity || 1));
|
|
1008
|
-
const bands = Math.max(1, Math.floor(existingBands || fallbackBands || 16));
|
|
1009
|
-
const recordFloats = 3 + bands;
|
|
1010
|
-
const minBytes = sonareSpectrumRingBufferByteLength(capacity, bands);
|
|
1011
|
-
if (sharedBuffer.byteLength < minBytes) {
|
|
1012
|
-
throw new Error('spectrumSharedBuffer is too small for the requested ring capacity.');
|
|
1013
|
-
}
|
|
1014
|
-
Atomics.store(header, 1, capacity);
|
|
1015
|
-
Atomics.store(header, 2, recordFloats);
|
|
1016
|
-
Atomics.store(header, 3, bands);
|
|
1017
|
-
return {
|
|
1018
|
-
header,
|
|
1019
|
-
records: new Float32Array(sharedBuffer, headerBytes, capacity * recordFloats),
|
|
1020
|
-
capacity,
|
|
1021
|
-
bands,
|
|
1022
|
-
recordFloats,
|
|
1023
|
-
};
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
function engineRingFromSharedBuffer(
|
|
1027
|
-
sharedBuffer: SharedArrayBuffer,
|
|
1028
|
-
recordBytes: number,
|
|
1029
|
-
fallbackCapacity?: number,
|
|
1030
|
-
): { header: Int32Array; view: DataView; capacity: number } {
|
|
1031
|
-
const headerBytes = SONARE_ENGINE_RING_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT;
|
|
1032
|
-
const header = new Int32Array(sharedBuffer, 0, SONARE_ENGINE_RING_HEADER_INTS);
|
|
1033
|
-
const existingCapacity = Atomics.load(header, 2);
|
|
1034
|
-
const capacity = Math.max(1, Math.floor(existingCapacity || fallbackCapacity || 1));
|
|
1035
|
-
const minBytes = headerBytes + capacity * recordBytes;
|
|
1036
|
-
if (sharedBuffer.byteLength < minBytes) {
|
|
1037
|
-
throw new Error('engine SharedArrayBuffer is too small for the requested ring capacity.');
|
|
1038
|
-
}
|
|
1039
|
-
Atomics.store(header, 2, capacity);
|
|
1040
|
-
Atomics.store(header, 3, recordBytes);
|
|
1041
|
-
return {
|
|
1042
|
-
header,
|
|
1043
|
-
view: new DataView(sharedBuffer, headerBytes, capacity * recordBytes),
|
|
1044
|
-
capacity,
|
|
1045
|
-
};
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
function recordOffset(index: number, capacity: number, recordBytes: number): number {
|
|
1049
|
-
return (index % capacity) * recordBytes;
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
|
-
function toBigInt64(value: number | bigint | undefined, fallback: bigint): bigint {
|
|
1053
|
-
if (typeof value === 'bigint') {
|
|
1054
|
-
return value;
|
|
1055
|
-
}
|
|
1056
|
-
if (typeof value === 'number') {
|
|
1057
|
-
return BigInt(Math.trunc(value));
|
|
1058
|
-
}
|
|
1059
|
-
return fallback;
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
function writeEngineCommandRecord(
|
|
1063
|
-
view: DataView,
|
|
1064
|
-
offset: number,
|
|
1065
|
-
command: SonareEngineCommandRecord,
|
|
1066
|
-
): void {
|
|
1067
|
-
view.setUint32(offset, command.type, true);
|
|
1068
|
-
view.setUint32(offset + 4, command.targetId ?? 0, true);
|
|
1069
|
-
view.setBigInt64(offset + 8, toBigInt64(command.sampleTime, -1n), true);
|
|
1070
|
-
// argFloat occupies a full 8-byte Float64 slot (replacing the old Float32 +
|
|
1071
|
-
// 4-byte pad) so PPQ scalars carried here keep full double precision over the
|
|
1072
|
-
// SAB transport, matching the engine's double-precision seek/loop contract.
|
|
1073
|
-
view.setFloat64(offset + 16, command.argFloat ?? 0, true);
|
|
1074
|
-
view.setBigInt64(offset + 24, toBigInt64(command.argInt, 0n), true);
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
function readEngineCommandRecord(view: DataView, offset: number): SonareEngineCommandRecord {
|
|
1078
|
-
return {
|
|
1079
|
-
type: view.getUint32(offset, true),
|
|
1080
|
-
targetId: view.getUint32(offset + 4, true),
|
|
1081
|
-
sampleTime: Number(view.getBigInt64(offset + 8, true)),
|
|
1082
|
-
argFloat: view.getFloat64(offset + 16, true),
|
|
1083
|
-
argInt: Number(view.getBigInt64(offset + 24, true)),
|
|
1084
|
-
};
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
|
-
function writeEngineTelemetryRecord(
|
|
1088
|
-
view: DataView,
|
|
1089
|
-
offset: number,
|
|
1090
|
-
telemetry: SonareEngineTelemetryRecord,
|
|
1091
|
-
): void {
|
|
1092
|
-
view.setUint32(offset, telemetry.type, true);
|
|
1093
|
-
view.setUint32(offset + 4, telemetry.error, true);
|
|
1094
|
-
view.setBigInt64(offset + 8, BigInt(Math.trunc(telemetry.renderFrame)), true);
|
|
1095
|
-
view.setBigInt64(offset + 16, BigInt(Math.trunc(telemetry.timelineSample)), true);
|
|
1096
|
-
view.setBigInt64(offset + 24, BigInt(Math.trunc(telemetry.audibleTimelineSample)), true);
|
|
1097
|
-
view.setInt32(offset + 32, telemetry.graphLatencySamplesQ8, true);
|
|
1098
|
-
view.setUint32(offset + 36, telemetry.value, true);
|
|
1099
|
-
view.setBigInt64(offset + 40, 0n, true);
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
function readEngineTelemetryRecord(view: DataView, offset: number): SonareEngineTelemetryRecord {
|
|
1103
|
-
return {
|
|
1104
|
-
type: view.getUint32(offset, true),
|
|
1105
|
-
error: view.getUint32(offset + 4, true),
|
|
1106
|
-
renderFrame: Number(view.getBigInt64(offset + 8, true)),
|
|
1107
|
-
timelineSample: Number(view.getBigInt64(offset + 16, true)),
|
|
1108
|
-
audibleTimelineSample: Number(view.getBigInt64(offset + 24, true)),
|
|
1109
|
-
graphLatencySamplesQ8: view.getInt32(offset + 32, true),
|
|
1110
|
-
value: view.getUint32(offset + 36, true),
|
|
1111
|
-
};
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
|
-
function telemetryFromEngine(telemetry: EngineTelemetry): SonareEngineTelemetryRecord {
|
|
1115
|
-
return {
|
|
1116
|
-
type: telemetry.type,
|
|
1117
|
-
error: telemetry.error,
|
|
1118
|
-
renderFrame: telemetry.renderFrame,
|
|
1119
|
-
timelineSample: telemetry.timelineSample,
|
|
1120
|
-
audibleTimelineSample: telemetry.audibleTimelineSample,
|
|
1121
|
-
graphLatencySamplesQ8: telemetry.graphLatencySamplesQ8,
|
|
1122
|
-
value: telemetry.value,
|
|
1123
|
-
};
|
|
1124
|
-
}
|
|
1125
|
-
|
|
1126
|
-
function meterFromEngine(meter: EngineMeterTelemetry): SonareWorkletMeterSnapshot {
|
|
1127
|
-
return {
|
|
1128
|
-
type: 'meter',
|
|
1129
|
-
targetId: meter.targetId,
|
|
1130
|
-
frame: meter.renderFrame,
|
|
1131
|
-
peakDbL: meter.peakDbL,
|
|
1132
|
-
peakDbR: meter.peakDbR,
|
|
1133
|
-
rmsDbL: meter.rmsDbL,
|
|
1134
|
-
rmsDbR: meter.rmsDbR,
|
|
1135
|
-
correlation: meter.correlation,
|
|
1136
|
-
truePeakDbL: meter.truePeakDbL,
|
|
1137
|
-
truePeakDbR: meter.truePeakDbR,
|
|
1138
|
-
momentaryLufs: meter.momentaryLufs,
|
|
1139
|
-
shortTermLufs: meter.shortTermLufs,
|
|
1140
|
-
integratedLufs: meter.integratedLufs,
|
|
1141
|
-
gainReductionDb: meter.gainReductionDb,
|
|
1142
|
-
};
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
|
-
function magnitudeToDb(value: number): number {
|
|
1146
|
-
return value > 1.0e-12 ? 20 * Math.log10(value) : -120;
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
228
|
/**
|
|
1150
229
|
* AudioWorklet-style mixer bridge backed by the package's single `sonare.wasm`.
|
|
1151
230
|
*
|
|
@@ -1471,6 +550,7 @@ export class SonareRealtimeEngineWorkletProcessor {
|
|
|
1471
550
|
private commandRing?: SonareEngineCommandRingBuffer;
|
|
1472
551
|
private telemetryRing?: SonareEngineTelemetryRingBuffer;
|
|
1473
552
|
private meterRing?: SharedMeterRingWriter;
|
|
553
|
+
private scopeRing?: SharedScopeRingWriter;
|
|
1474
554
|
private transport?: WorkletTransport;
|
|
1475
555
|
private meterIntervalFrames: number;
|
|
1476
556
|
private lastMeterFrame = Number.NEGATIVE_INFINITY;
|
|
@@ -1514,6 +594,13 @@ export class SonareRealtimeEngineWorkletProcessor {
|
|
|
1514
594
|
this.meterRing = options.meterSharedBuffer
|
|
1515
595
|
? meterRingFromSharedBuffer(options.meterSharedBuffer, options.meterRingCapacity)
|
|
1516
596
|
: undefined;
|
|
597
|
+
this.scopeRing = options.scopeSharedBuffer
|
|
598
|
+
? scopeRingFromSharedBuffer(
|
|
599
|
+
options.scopeSharedBuffer,
|
|
600
|
+
options.scopeRingCapacity,
|
|
601
|
+
options.scopeBands,
|
|
602
|
+
)
|
|
603
|
+
: undefined;
|
|
1517
604
|
this.engine = new RealtimeEngine(this.sampleRate, this.blockSize);
|
|
1518
605
|
// Allocate persistent WASM-heap scratch (worst case: channelCount channels x
|
|
1519
606
|
// blockSize frames) and acquire the per-channel heap views once.
|
|
@@ -1522,6 +609,13 @@ export class SonareRealtimeEngineWorkletProcessor {
|
|
|
1522
609
|
for (let ch = 0; ch < this.channelCount; ch++) {
|
|
1523
610
|
this.channelBuffers[ch] = this.engine.getChannelBuffer(ch, this.blockSize);
|
|
1524
611
|
}
|
|
612
|
+
// Arm the engine's scope producer only when a scope ring was provided. The
|
|
613
|
+
// band count follows the ring's record layout so writeScopeRing never
|
|
614
|
+
// overruns its slot.
|
|
615
|
+
if (this.scopeRing) {
|
|
616
|
+
const interval = Math.max(1, Math.floor(options.scopeIntervalFrames ?? this.blockSize));
|
|
617
|
+
this.engine.configureScopeTelemetry(interval, this.scopeRing.bands);
|
|
618
|
+
}
|
|
1525
619
|
}
|
|
1526
620
|
|
|
1527
621
|
process(inputs: WorkletInput, outputs: WorkletOutput): boolean {
|
|
@@ -1599,6 +693,7 @@ export class SonareRealtimeEngineWorkletProcessor {
|
|
|
1599
693
|
}
|
|
1600
694
|
this.publishTelemetry();
|
|
1601
695
|
this.publishMeters();
|
|
696
|
+
this.publishScope();
|
|
1602
697
|
return true;
|
|
1603
698
|
}
|
|
1604
699
|
|
|
@@ -1686,6 +781,9 @@ export class SonareRealtimeEngineWorkletProcessor {
|
|
|
1686
781
|
if (message.masterStripJson) {
|
|
1687
782
|
this.engine.setMasterStripJson(message.masterStripJson);
|
|
1688
783
|
}
|
|
784
|
+
for (const binding of message.laneSidechains ?? []) {
|
|
785
|
+
this.engine.setLaneSidechain(binding.trackId, binding.insertIndex, binding.sourceTrackId);
|
|
786
|
+
}
|
|
1689
787
|
break;
|
|
1690
788
|
case 'syncCapture':
|
|
1691
789
|
this.engine.setCaptureBuffer(message.channels, message.bufferFrames);
|
|
@@ -1714,6 +812,36 @@ export class SonareRealtimeEngineWorkletProcessor {
|
|
|
1714
812
|
message.resetOnBypass,
|
|
1715
813
|
);
|
|
1716
814
|
break;
|
|
815
|
+
case 'syncTrackStripInsertParamByName':
|
|
816
|
+
this.engine.setTrackStripInsertParamByName(
|
|
817
|
+
message.trackId,
|
|
818
|
+
message.insertIndex,
|
|
819
|
+
message.paramName,
|
|
820
|
+
message.value,
|
|
821
|
+
);
|
|
822
|
+
break;
|
|
823
|
+
case 'syncMasterStripInsertParamByName':
|
|
824
|
+
this.engine.setMasterStripInsertParamByName(
|
|
825
|
+
message.insertIndex,
|
|
826
|
+
message.paramName,
|
|
827
|
+
message.value,
|
|
828
|
+
);
|
|
829
|
+
break;
|
|
830
|
+
case 'syncTrackStripPan':
|
|
831
|
+
this.engine.setTrackStripPan(message.trackId, message.pan);
|
|
832
|
+
break;
|
|
833
|
+
case 'syncTrackStripPanLaw':
|
|
834
|
+
this.engine.setTrackStripPanLaw(message.trackId, message.panLaw);
|
|
835
|
+
break;
|
|
836
|
+
case 'syncTrackStripPanMode':
|
|
837
|
+
this.engine.setTrackStripPanMode(message.trackId, message.panMode);
|
|
838
|
+
break;
|
|
839
|
+
case 'syncTrackStripDualPan':
|
|
840
|
+
this.engine.setTrackStripDualPan(message.trackId, message.leftPan, message.rightPan);
|
|
841
|
+
break;
|
|
842
|
+
case 'syncTrackStripChannelDelaySamples':
|
|
843
|
+
this.engine.setTrackStripChannelDelaySamples(message.trackId, message.delaySamples);
|
|
844
|
+
break;
|
|
1717
845
|
case 'syncBuiltinInstrument':
|
|
1718
846
|
this.engine.setBuiltinInstrument(message.config, message.destinationId);
|
|
1719
847
|
break;
|
|
@@ -1966,6 +1094,17 @@ export class SonareRealtimeEngineWorkletProcessor {
|
|
|
1966
1094
|
this.transport?.postMessage?.(record);
|
|
1967
1095
|
}
|
|
1968
1096
|
|
|
1097
|
+
// Drains the engine meter telemetry queue into the stereo meter ring / transport.
|
|
1098
|
+
//
|
|
1099
|
+
// Shared-queue contract: `drainMeterTelemetry` and `drainMeterTelemetryWide`
|
|
1100
|
+
// pop the SAME single-consumer telemetry queue, so exactly ONE of them may run
|
|
1101
|
+
// per engine. The live worklet path owns the queue via the stereo drain below;
|
|
1102
|
+
// the worklet meter ring (SONARE_METER_RING_RECORD_FLOATS) is a fixed stereo
|
|
1103
|
+
// layout carrying planes 0/1 plus the correlation/LUFS summary. Per-plane
|
|
1104
|
+
// surround meters are NOT delivered over the live worklet ring — a host that
|
|
1105
|
+
// needs them must use the offline `drainMeterTelemetryWide()` API on a
|
|
1106
|
+
// non-worklet engine instance (do not also call it on a worklet-driven engine,
|
|
1107
|
+
// or the two drains will starve each other).
|
|
1969
1108
|
private publishMeters(): void {
|
|
1970
1109
|
if (this.meterIntervalFrames <= 0 || (!this.transport && !this.meterRing)) {
|
|
1971
1110
|
return;
|
|
@@ -2023,6 +1162,46 @@ export class SonareRealtimeEngineWorkletProcessor {
|
|
|
2023
1162
|
// writeIndex - capacity), so header slot 3 is left at its initial 0.
|
|
2024
1163
|
}
|
|
2025
1164
|
|
|
1165
|
+
// Drains the engine's scope producer (FFT spectrum + goniometer points) into
|
|
1166
|
+
// the lock-free SAB scope ring. Only the embind runtime publishes scope
|
|
1167
|
+
// telemetry; the sonare-rt runtime owns its own transport. No allocation on
|
|
1168
|
+
// the render path: records are written field-by-field into the ring.
|
|
1169
|
+
private publishScope(): void {
|
|
1170
|
+
const ring = this.scopeRing;
|
|
1171
|
+
if (!ring) {
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1174
|
+
for (const item of this.engine.drainScopeTelemetry(64)) {
|
|
1175
|
+
this.writeScopeRing(ring, item);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
private writeScopeRing(ring: SharedScopeRingWriter, record: EngineScopeTelemetry): void {
|
|
1180
|
+
const writeIndex = Atomics.load(ring.header, 0);
|
|
1181
|
+
const base = (writeIndex % ring.capacity) * ring.recordFloats;
|
|
1182
|
+
ring.records[base] = encodeFrameLo(record.renderFrame);
|
|
1183
|
+
ring.records[base + 1] = encodeFrameHi(record.renderFrame);
|
|
1184
|
+
ring.records[base + 2] = record.targetId;
|
|
1185
|
+
const bandCount = Math.min(ring.bands, record.bands.length);
|
|
1186
|
+
ring.records[base + 3] = bandCount;
|
|
1187
|
+
const pointCount = Math.min(ring.maxPoints, record.points.length);
|
|
1188
|
+
ring.records[base + 4] = pointCount;
|
|
1189
|
+
const bandsBase = base + SONARE_SCOPE_RING_RECORD_PREFIX_FLOATS;
|
|
1190
|
+
for (let i = 0; i < bandCount; i++) {
|
|
1191
|
+
ring.records[bandsBase + i] = record.bands[i];
|
|
1192
|
+
}
|
|
1193
|
+
const pointsBase = bandsBase + ring.bands;
|
|
1194
|
+
for (let i = 0; i < pointCount; i++) {
|
|
1195
|
+
const point = record.points[i];
|
|
1196
|
+
ring.records[pointsBase + 2 * i] = point.left;
|
|
1197
|
+
ring.records[pointsBase + 2 * i + 1] = point.right;
|
|
1198
|
+
}
|
|
1199
|
+
Atomics.store(ring.header, 0, writeIndex + 1);
|
|
1200
|
+
// Like writeMeterRing, writeIndex is a free-running monotonic counter; the
|
|
1201
|
+
// reader detects silent overrun via firstReadable, so the overflow slot
|
|
1202
|
+
// (header[5]) stays at its initial 0.
|
|
1203
|
+
}
|
|
1204
|
+
|
|
2026
1205
|
private commandRingFromSharedBuffer(
|
|
2027
1206
|
sharedBuffer: SharedArrayBuffer,
|
|
2028
1207
|
fallbackCapacity?: number,
|
|
@@ -2446,11 +1625,14 @@ export class SonareRealtimeEngineNode {
|
|
|
2446
1625
|
readonly commandRing?: SonareEngineCommandRingBuffer;
|
|
2447
1626
|
readonly telemetryRing?: SonareEngineTelemetryRingBuffer;
|
|
2448
1627
|
readonly meterRing?: SonareMeterRingBuffer;
|
|
1628
|
+
readonly scopeRing?: SonareScopeRingBuffer;
|
|
2449
1629
|
readonly ready: Promise<void>;
|
|
2450
1630
|
private telemetryReadIndex = 0;
|
|
2451
1631
|
private meterReadIndex = 0;
|
|
1632
|
+
private scopeReadIndex = 0;
|
|
2452
1633
|
private telemetryListeners = new Set<(telemetry: SonareEngineTelemetryRecord) => void>();
|
|
2453
1634
|
private meterListeners = new Set<(meter: SonareWorkletMeterSnapshot) => void>();
|
|
1635
|
+
private scopeListeners = new Set<(scope: SonareWorkletScopeSnapshot) => void>();
|
|
2454
1636
|
private captureRequestId = 1;
|
|
2455
1637
|
private readonly captureRequests = new Map<
|
|
2456
1638
|
number,
|
|
@@ -2477,12 +1659,14 @@ export class SonareRealtimeEngineNode {
|
|
|
2477
1659
|
commandRing?: SonareEngineCommandRingBuffer,
|
|
2478
1660
|
telemetryRing?: SonareEngineTelemetryRingBuffer,
|
|
2479
1661
|
meterRing?: SonareMeterRingBuffer,
|
|
1662
|
+
scopeRing?: SonareScopeRingBuffer,
|
|
2480
1663
|
) {
|
|
2481
1664
|
this.node = node;
|
|
2482
1665
|
this.capabilities = capabilities;
|
|
2483
1666
|
this.commandRing = commandRing;
|
|
2484
1667
|
this.telemetryRing = telemetryRing;
|
|
2485
1668
|
this.meterRing = meterRing;
|
|
1669
|
+
this.scopeRing = scopeRing;
|
|
2486
1670
|
this.ready = new Promise((resolve, reject) => {
|
|
2487
1671
|
this.resolveReady = resolve;
|
|
2488
1672
|
this.rejectReady = reject;
|
|
@@ -2581,6 +1765,14 @@ export class SonareRealtimeEngineNode {
|
|
|
2581
1765
|
mode === 'sab' && runtimeTarget === 'embind'
|
|
2582
1766
|
? createSonareMeterRingBuffer(options.meterRingCapacity ?? 128)
|
|
2583
1767
|
: undefined;
|
|
1768
|
+
// Scope ring (FFT spectrum + goniometer): opt-in, embind-only. The
|
|
1769
|
+
// per-block FFT is heavier than the meter path, so it is created only when
|
|
1770
|
+
// the caller requests scope telemetry via scopeIntervalFrames > 0.
|
|
1771
|
+
const scopeIntervalFrames = Math.max(0, Math.floor(options.scopeIntervalFrames ?? 0));
|
|
1772
|
+
const scopeRing =
|
|
1773
|
+
mode === 'sab' && runtimeTarget === 'embind' && scopeIntervalFrames > 0
|
|
1774
|
+
? createSonareScopeRingBuffer(options.scopeRingCapacity ?? 64, options.scopeBands ?? 48)
|
|
1775
|
+
: undefined;
|
|
2584
1776
|
const channelCount = Math.max(1, Math.floor(options.channelCount ?? 2));
|
|
2585
1777
|
const processorOptions: SonareRealtimeEngineWorkletProcessorOptions = {
|
|
2586
1778
|
runtimeTarget,
|
|
@@ -2595,6 +1787,10 @@ export class SonareRealtimeEngineNode {
|
|
|
2595
1787
|
telemetryRingCapacity: telemetryRing?.capacity,
|
|
2596
1788
|
meterSharedBuffer: meterRing?.sharedBuffer,
|
|
2597
1789
|
meterRingCapacity: meterRing?.capacity,
|
|
1790
|
+
scopeSharedBuffer: scopeRing?.sharedBuffer,
|
|
1791
|
+
scopeRingCapacity: scopeRing?.capacity,
|
|
1792
|
+
scopeBands: scopeRing?.bands,
|
|
1793
|
+
scopeIntervalFrames: scopeRing ? scopeIntervalFrames : undefined,
|
|
2598
1794
|
wasmBinary: options.wasmBinary,
|
|
2599
1795
|
initialSyncMessages: options.initialSyncMessages,
|
|
2600
1796
|
initialCommands: options.initialCommands,
|
|
@@ -2628,6 +1824,7 @@ export class SonareRealtimeEngineNode {
|
|
|
2628
1824
|
commandRing,
|
|
2629
1825
|
telemetryRing,
|
|
2630
1826
|
meterRing,
|
|
1827
|
+
scopeRing,
|
|
2631
1828
|
);
|
|
2632
1829
|
}
|
|
2633
1830
|
|
|
@@ -2723,6 +1920,21 @@ export class SonareRealtimeEngineNode {
|
|
|
2723
1920
|
return read.meters;
|
|
2724
1921
|
}
|
|
2725
1922
|
|
|
1923
|
+
// Drains scope telemetry (FFT spectrum + goniometer points) published into the
|
|
1924
|
+
// SAB scope ring and forwards each record to onScope listeners. A no-op unless
|
|
1925
|
+
// the node was created with scopeIntervalFrames > 0 (embind SAB mode).
|
|
1926
|
+
pollScope(): SonareWorkletScopeSnapshot[] {
|
|
1927
|
+
if (!this.scopeRing) {
|
|
1928
|
+
return [];
|
|
1929
|
+
}
|
|
1930
|
+
const read = readSonareScopeRingBuffer(this.scopeRing, this.scopeReadIndex);
|
|
1931
|
+
this.scopeReadIndex = read.nextReadIndex;
|
|
1932
|
+
for (const scope of read.scopes) {
|
|
1933
|
+
this.emitScope(scope);
|
|
1934
|
+
}
|
|
1935
|
+
return read.scopes;
|
|
1936
|
+
}
|
|
1937
|
+
|
|
2726
1938
|
onTelemetry(callback: (telemetry: SonareEngineTelemetryRecord) => void): () => void {
|
|
2727
1939
|
this.telemetryListeners.add(callback);
|
|
2728
1940
|
return () => {
|
|
@@ -2737,6 +1949,13 @@ export class SonareRealtimeEngineNode {
|
|
|
2737
1949
|
};
|
|
2738
1950
|
}
|
|
2739
1951
|
|
|
1952
|
+
onScope(callback: (scope: SonareWorkletScopeSnapshot) => void): () => void {
|
|
1953
|
+
this.scopeListeners.add(callback);
|
|
1954
|
+
return () => {
|
|
1955
|
+
this.scopeListeners.delete(callback);
|
|
1956
|
+
};
|
|
1957
|
+
}
|
|
1958
|
+
|
|
2740
1959
|
destroy(): void {
|
|
2741
1960
|
if (this.destroyed) {
|
|
2742
1961
|
return;
|
|
@@ -2754,6 +1973,7 @@ export class SonareRealtimeEngineNode {
|
|
|
2754
1973
|
this.transportRequests.clear();
|
|
2755
1974
|
this.telemetryListeners.clear();
|
|
2756
1975
|
this.meterListeners.clear();
|
|
1976
|
+
this.scopeListeners.clear();
|
|
2757
1977
|
}
|
|
2758
1978
|
|
|
2759
1979
|
private emitTelemetry(telemetry: SonareEngineTelemetryRecord): void {
|
|
@@ -2768,6 +1988,12 @@ export class SonareRealtimeEngineNode {
|
|
|
2768
1988
|
}
|
|
2769
1989
|
}
|
|
2770
1990
|
|
|
1991
|
+
private emitScope(scope: SonareWorkletScopeSnapshot): void {
|
|
1992
|
+
for (const listener of this.scopeListeners) {
|
|
1993
|
+
listener(scope);
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
|
|
2771
1997
|
private sendCaptureRequest(
|
|
2772
1998
|
op: SonareEngineCaptureRequestMessage['op'],
|
|
2773
1999
|
): Promise<SonareEngineCaptureResponseMessage> {
|
|
@@ -2811,6 +2037,11 @@ export class SonareEngine {
|
|
|
2811
2037
|
private readonly markers = new Map<number, EngineMarker>();
|
|
2812
2038
|
private readonly trackLaneIds: number[] = [];
|
|
2813
2039
|
private readonly trackSends = new Map<number, EngineTrackSend[]>();
|
|
2040
|
+
private readonly trackOutputBus = new Map<number, number>();
|
|
2041
|
+
private readonly laneSidechains = new Map<
|
|
2042
|
+
string,
|
|
2043
|
+
{ trackId: number; insertIndex: number; sourceTrackId: number }
|
|
2044
|
+
>();
|
|
2814
2045
|
private readonly buses: EngineBus[] = [];
|
|
2815
2046
|
private readonly trackStripJson = new Map<number, string>();
|
|
2816
2047
|
private readonly busStripJson = new Map<number, string>();
|
|
@@ -3025,6 +2256,68 @@ export class SonareEngine {
|
|
|
3025
2256
|
this.scheduleParam('', laneId, ppq, value, curve);
|
|
3026
2257
|
}
|
|
3027
2258
|
|
|
2259
|
+
/**
|
|
2260
|
+
* Replaces the automation lane for `paramId` with the given breakpoints.
|
|
2261
|
+
*
|
|
2262
|
+
* Unlike scheduleParam (which appends a single point), this sets the whole
|
|
2263
|
+
* lane at once; an empty array clears the lane. The points are defensively
|
|
2264
|
+
* copied and sorted by ppq before being mirrored to the offline engine and
|
|
2265
|
+
* the live worklet engine.
|
|
2266
|
+
*
|
|
2267
|
+
* @param paramId Automation target id (registered parameter or a reserved
|
|
2268
|
+
* engine mixer target from automationParamId/busAutomationParamId).
|
|
2269
|
+
* @param points Lane breakpoints; order does not matter.
|
|
2270
|
+
*/
|
|
2271
|
+
setAutomationLane(paramId: number, points: ReadonlyArray<EngineAutomationPoint>): void {
|
|
2272
|
+
const sorted = points.map((point) => ({ ...point })).sort((a, b) => a.ppq - b.ppq);
|
|
2273
|
+
if (sorted.length === 0) {
|
|
2274
|
+
this.automationLanes.delete(paramId);
|
|
2275
|
+
} else {
|
|
2276
|
+
this.automationLanes.set(paramId, sorted);
|
|
2277
|
+
}
|
|
2278
|
+
this.offlineEngine.setAutomationLane(paramId, sorted);
|
|
2279
|
+
this.postSync({ type: 'syncAutomation', paramId, points: sorted });
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
/**
|
|
2283
|
+
* Returns the automation target id for a mixer strip parameter.
|
|
2284
|
+
*
|
|
2285
|
+
* The id addresses the engine's reserved mixer namespace, so it can be fed
|
|
2286
|
+
* straight to setAutomationLane to automate a fader or pan without
|
|
2287
|
+
* registering a parameter.
|
|
2288
|
+
*
|
|
2289
|
+
* @param target Track id (declares a mixer lane on first use) or 'master'.
|
|
2290
|
+
* @param kind Strip parameter to address.
|
|
2291
|
+
* @returns Reserved engine parameter id for the strip parameter.
|
|
2292
|
+
*/
|
|
2293
|
+
automationParamId(target: string | number, kind: 'faderDb' | 'pan'): number {
|
|
2294
|
+
const paramKind = kind === 'pan' ? ENGINE_MIXER_PARAM_PAN : ENGINE_MIXER_PARAM_FADER_DB;
|
|
2295
|
+
if (target === 'master') {
|
|
2296
|
+
return engineMixerMasterTarget(paramKind);
|
|
2297
|
+
}
|
|
2298
|
+
return engineMixerLaneTarget(this.ensureTrackLane(target), paramKind);
|
|
2299
|
+
}
|
|
2300
|
+
|
|
2301
|
+
/**
|
|
2302
|
+
* Returns the automation target id for a bus fader.
|
|
2303
|
+
*
|
|
2304
|
+
* @param busId Bus id (declares the mixer bus on first use).
|
|
2305
|
+
* @returns Reserved engine parameter id for the bus fader gain (dB).
|
|
2306
|
+
*/
|
|
2307
|
+
busAutomationParamId(busId: number): number {
|
|
2308
|
+
return engineMixerBusTarget(this.ensureBus(busId), ENGINE_MIXER_PARAM_FADER_DB);
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
/**
|
|
2312
|
+
* Returns the number of automation lanes installed on the engine, including
|
|
2313
|
+
* lanes whose breakpoint list is currently empty.
|
|
2314
|
+
*
|
|
2315
|
+
* @returns Engine-side automation lane count.
|
|
2316
|
+
*/
|
|
2317
|
+
automationLaneCount(): number {
|
|
2318
|
+
return this.offlineEngine.automationLaneCount();
|
|
2319
|
+
}
|
|
2320
|
+
|
|
3028
2321
|
listParameters(): EngineParameterInfo[] {
|
|
3029
2322
|
const parameters: EngineParameterInfo[] = [];
|
|
3030
2323
|
for (let index = 0; index < this.offlineEngine.parameterCount(); index++) {
|
|
@@ -3125,11 +2418,64 @@ export class SonareEngine {
|
|
|
3125
2418
|
entry.sends.map((send) => ({ ...send })),
|
|
3126
2419
|
);
|
|
3127
2420
|
}
|
|
2421
|
+
if (entry.outputBusId !== undefined) {
|
|
2422
|
+
if (entry.outputBusId === 0) {
|
|
2423
|
+
this.trackOutputBus.delete(entry.trackId);
|
|
2424
|
+
} else {
|
|
2425
|
+
this.trackOutputBus.set(entry.trackId, entry.outputBusId);
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
3128
2428
|
}
|
|
3129
2429
|
this.trackLaneIds.splice(0, this.trackLaneIds.length, ...ids);
|
|
3130
2430
|
this.syncMixer();
|
|
3131
2431
|
}
|
|
3132
2432
|
|
|
2433
|
+
/**
|
|
2434
|
+
* Routes a track lane's post-fader output into a declared bus instead of
|
|
2435
|
+
* the master mix (group/folder routing); busId 0 restores the master mix.
|
|
2436
|
+
*/
|
|
2437
|
+
setTrackOutputBus(target: string | number, busId: number): void {
|
|
2438
|
+
const laneIndex = this.ensureTrackLane(target);
|
|
2439
|
+
const trackId = this.trackLaneIds[laneIndex];
|
|
2440
|
+
if (busId === 0) {
|
|
2441
|
+
this.trackOutputBus.delete(trackId);
|
|
2442
|
+
} else {
|
|
2443
|
+
this.trackOutputBus.set(trackId, busId);
|
|
2444
|
+
}
|
|
2445
|
+
this.syncMixer();
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
/**
|
|
2449
|
+
* Keys one insert of a lane strip from another lane's post-strip pre-fader
|
|
2450
|
+
* audio (ducking/sidechainRouter inserts). sourceTarget null removes the
|
|
2451
|
+
* binding.
|
|
2452
|
+
*/
|
|
2453
|
+
setLaneSidechain(
|
|
2454
|
+
target: string | number,
|
|
2455
|
+
insertIndex: number,
|
|
2456
|
+
sourceTarget: string | number | null,
|
|
2457
|
+
): void {
|
|
2458
|
+
const laneIndex = this.ensureTrackLane(target);
|
|
2459
|
+
const trackId = this.trackLaneIds[laneIndex];
|
|
2460
|
+
const key = `${trackId}:${insertIndex}`;
|
|
2461
|
+
let sourceTrackId = 0;
|
|
2462
|
+
if (sourceTarget !== null) {
|
|
2463
|
+
const sourceIndex = this.ensureTrackLane(sourceTarget);
|
|
2464
|
+
sourceTrackId = this.trackLaneIds[sourceIndex];
|
|
2465
|
+
}
|
|
2466
|
+
if (sourceTrackId === 0) {
|
|
2467
|
+
this.laneSidechains.delete(key);
|
|
2468
|
+
} else {
|
|
2469
|
+
this.laneSidechains.set(key, { trackId, insertIndex, sourceTrackId });
|
|
2470
|
+
}
|
|
2471
|
+
this.offlineEngine.setLaneSidechain(trackId, insertIndex, sourceTrackId);
|
|
2472
|
+
this.postSync({
|
|
2473
|
+
type: 'syncMixer',
|
|
2474
|
+
lanes: this.mixerLanes(),
|
|
2475
|
+
laneSidechains: [{ trackId, insertIndex, sourceTrackId }],
|
|
2476
|
+
});
|
|
2477
|
+
}
|
|
2478
|
+
|
|
3133
2479
|
setSends(target: string | number, sends: EngineTrackSend[]): void {
|
|
3134
2480
|
const laneIndex = this.ensureTrackLane(target);
|
|
3135
2481
|
const trackId = this.trackLaneIds[laneIndex];
|
|
@@ -3193,6 +2539,56 @@ export class SonareEngine {
|
|
|
3193
2539
|
});
|
|
3194
2540
|
}
|
|
3195
2541
|
|
|
2542
|
+
setTrackStripInsertParamByName(
|
|
2543
|
+
target: string | number,
|
|
2544
|
+
insertIndex: number,
|
|
2545
|
+
paramName: string,
|
|
2546
|
+
value: number,
|
|
2547
|
+
): void {
|
|
2548
|
+
const laneIndex = this.ensureTrackLane(target);
|
|
2549
|
+
const trackId = this.trackLaneIds[laneIndex];
|
|
2550
|
+
this.offlineEngine.setTrackStripInsertParamByName(trackId, insertIndex, paramName, value);
|
|
2551
|
+
this.postSync({
|
|
2552
|
+
type: 'syncTrackStripInsertParamByName',
|
|
2553
|
+
trackId,
|
|
2554
|
+
insertIndex,
|
|
2555
|
+
paramName,
|
|
2556
|
+
value,
|
|
2557
|
+
});
|
|
2558
|
+
}
|
|
2559
|
+
|
|
2560
|
+
setTrackStripPan(target: string | number, pan: number): void {
|
|
2561
|
+
const trackId = this.trackLaneIds[this.ensureTrackLane(target)];
|
|
2562
|
+
this.offlineEngine.setTrackStripPan(trackId, pan);
|
|
2563
|
+
this.postSync({ type: 'syncTrackStripPan', trackId, pan });
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
setTrackStripPanLaw(target: string | number, panLaw: PanLaw | number): void {
|
|
2567
|
+
const trackId = this.trackLaneIds[this.ensureTrackLane(target)];
|
|
2568
|
+
const code = panLawCode(panLaw);
|
|
2569
|
+
this.offlineEngine.setTrackStripPanLaw(trackId, code);
|
|
2570
|
+
this.postSync({ type: 'syncTrackStripPanLaw', trackId, panLaw: code });
|
|
2571
|
+
}
|
|
2572
|
+
|
|
2573
|
+
setTrackStripPanMode(target: string | number, panMode: PanMode | number): void {
|
|
2574
|
+
const trackId = this.trackLaneIds[this.ensureTrackLane(target)];
|
|
2575
|
+
const code = panModeCode(panMode);
|
|
2576
|
+
this.offlineEngine.setTrackStripPanMode(trackId, code);
|
|
2577
|
+
this.postSync({ type: 'syncTrackStripPanMode', trackId, panMode: code });
|
|
2578
|
+
}
|
|
2579
|
+
|
|
2580
|
+
setTrackStripDualPan(target: string | number, leftPan: number, rightPan: number): void {
|
|
2581
|
+
const trackId = this.trackLaneIds[this.ensureTrackLane(target)];
|
|
2582
|
+
this.offlineEngine.setTrackStripDualPan(trackId, leftPan, rightPan);
|
|
2583
|
+
this.postSync({ type: 'syncTrackStripDualPan', trackId, leftPan, rightPan });
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2586
|
+
setTrackStripChannelDelaySamples(target: string | number, delaySamples: number): void {
|
|
2587
|
+
const trackId = this.trackLaneIds[this.ensureTrackLane(target)];
|
|
2588
|
+
this.offlineEngine.setTrackStripChannelDelaySamples(trackId, delaySamples);
|
|
2589
|
+
this.postSync({ type: 'syncTrackStripChannelDelaySamples', trackId, delaySamples });
|
|
2590
|
+
}
|
|
2591
|
+
|
|
3196
2592
|
setStripEq(target: string | number, bandIndex: number, band: EqBand | string): void {
|
|
3197
2593
|
if (target === 'master') {
|
|
3198
2594
|
this.setMasterStripEqBand(bandIndex, band);
|
|
@@ -3255,6 +2651,29 @@ export class SonareEngine {
|
|
|
3255
2651
|
});
|
|
3256
2652
|
}
|
|
3257
2653
|
|
|
2654
|
+
setMasterStripInsertParamByName(insertIndex: number, paramName: string, value: number): void {
|
|
2655
|
+
this.offlineEngine.setMasterStripInsertParamByName(insertIndex, paramName, value);
|
|
2656
|
+
this.postSync({
|
|
2657
|
+
type: 'syncMasterStripInsertParamByName',
|
|
2658
|
+
insertIndex,
|
|
2659
|
+
paramName,
|
|
2660
|
+
value,
|
|
2661
|
+
});
|
|
2662
|
+
}
|
|
2663
|
+
|
|
2664
|
+
setStripInsertParamByName(
|
|
2665
|
+
target: string | number,
|
|
2666
|
+
insertIndex: number,
|
|
2667
|
+
paramName: string,
|
|
2668
|
+
value: number,
|
|
2669
|
+
): void {
|
|
2670
|
+
if (target === 'master') {
|
|
2671
|
+
this.setMasterStripInsertParamByName(insertIndex, paramName, value);
|
|
2672
|
+
return;
|
|
2673
|
+
}
|
|
2674
|
+
this.setTrackStripInsertParamByName(target, insertIndex, paramName, value);
|
|
2675
|
+
}
|
|
2676
|
+
|
|
3258
2677
|
setMasterChain(sceneJson: string): void {
|
|
3259
2678
|
this.setMasterStripJson(sceneJson);
|
|
3260
2679
|
}
|
|
@@ -3562,6 +2981,10 @@ export class SonareEngine {
|
|
|
3562
2981
|
return this.realtimeNode.onMeter(callback);
|
|
3563
2982
|
}
|
|
3564
2983
|
|
|
2984
|
+
onScope(callback: (scope: SonareWorkletScopeSnapshot) => void): () => void {
|
|
2985
|
+
return this.realtimeNode.onScope(callback);
|
|
2986
|
+
}
|
|
2987
|
+
|
|
3565
2988
|
onTelemetry(callback: (telemetry: SonareEngineTelemetryRecord) => void): () => void {
|
|
3566
2989
|
return this.realtimeNode.onTelemetry(callback);
|
|
3567
2990
|
}
|
|
@@ -3574,6 +2997,10 @@ export class SonareEngine {
|
|
|
3574
2997
|
return this.realtimeNode.pollMeters();
|
|
3575
2998
|
}
|
|
3576
2999
|
|
|
3000
|
+
pollScope(): SonareWorkletScopeSnapshot[] {
|
|
3001
|
+
return this.realtimeNode.pollScope();
|
|
3002
|
+
}
|
|
3003
|
+
|
|
3577
3004
|
destroy(): void {
|
|
3578
3005
|
if (this.destroyed) {
|
|
3579
3006
|
return;
|
|
@@ -3601,13 +3028,20 @@ export class SonareEngine {
|
|
|
3601
3028
|
this.postSync({ type: 'syncMidiClips', clips });
|
|
3602
3029
|
}
|
|
3603
3030
|
|
|
3604
|
-
private
|
|
3605
|
-
|
|
3031
|
+
private mixerLanes(): EngineTrackLane[] {
|
|
3032
|
+
return this.trackLaneIds.map((trackId) => {
|
|
3606
3033
|
const sends = this.trackSends.get(trackId);
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3034
|
+
const outputBusId = this.trackOutputBus.get(trackId);
|
|
3035
|
+
return {
|
|
3036
|
+
trackId,
|
|
3037
|
+
...(sends && sends.length > 0 ? { sends: sends.map((send) => ({ ...send })) } : {}),
|
|
3038
|
+
...(outputBusId !== undefined ? { outputBusId } : {}),
|
|
3039
|
+
};
|
|
3610
3040
|
});
|
|
3041
|
+
}
|
|
3042
|
+
|
|
3043
|
+
private syncMixer(): void {
|
|
3044
|
+
const lanes = this.mixerLanes();
|
|
3611
3045
|
const buses = this.buses.map((bus) => ({ ...bus }));
|
|
3612
3046
|
this.offlineEngine.setTrackBuses(buses);
|
|
3613
3047
|
if (lanes.length > 0) {
|
|
@@ -3626,6 +3060,7 @@ export class SonareEngine {
|
|
|
3626
3060
|
lanes,
|
|
3627
3061
|
buses,
|
|
3628
3062
|
trackStrips,
|
|
3063
|
+
laneSidechains: Array.from(this.laneSidechains.values()),
|
|
3629
3064
|
busStrips,
|
|
3630
3065
|
masterStripJson: this.masterStripJson,
|
|
3631
3066
|
});
|