@libraz/libsonare 1.3.2 → 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/README.md +45 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +403 -70
- 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 +907 -144
- package/dist/worklet.js +1803 -207
- 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 +27 -1
- package/src/mixer.ts +9 -0
- package/src/module_state.ts +82 -17
- package/src/opfs_clip_pages.ts +43 -9
- package/src/project.ts +74 -0
- package/src/public_types.ts +52 -0
- package/src/realtime_engine.ts +313 -109
- package/src/sonare.js.d.ts +140 -0
- package/src/stream_types.ts +7 -0
- package/src/validation.ts +7 -0
- package/src/web_midi.ts +15 -11
- 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 +1659 -888
package/src/worklet.ts
CHANGED
|
@@ -1,16 +1,33 @@
|
|
|
1
|
+
import { panLawCode, panModeCode } from './codes';
|
|
1
2
|
import type {
|
|
2
3
|
EngineAutomationPoint,
|
|
4
|
+
EngineBus,
|
|
5
|
+
EngineCaptureStatus,
|
|
3
6
|
EngineClip,
|
|
4
7
|
EngineMarker,
|
|
5
|
-
EngineMeterTelemetry,
|
|
6
8
|
EngineMetronomeConfig,
|
|
9
|
+
EngineMidiClipSchedule,
|
|
7
10
|
EngineParameterInfo,
|
|
8
|
-
|
|
11
|
+
EngineScopeTelemetry,
|
|
12
|
+
EngineTempoSegment,
|
|
13
|
+
EngineTimeSignatureSegment,
|
|
14
|
+
EngineTrackLane,
|
|
15
|
+
EngineTrackSend,
|
|
16
|
+
EngineTransportState,
|
|
17
|
+
EqBand,
|
|
9
18
|
MixerRealtimeBuffer,
|
|
10
|
-
|
|
19
|
+
PanLaw,
|
|
20
|
+
PanMode,
|
|
11
21
|
} from './index';
|
|
12
|
-
import {
|
|
13
|
-
|
|
22
|
+
import {
|
|
23
|
+
engineCapabilities,
|
|
24
|
+
init as initSonareModule,
|
|
25
|
+
isInitialized,
|
|
26
|
+
Mixer,
|
|
27
|
+
RealtimeEngine,
|
|
28
|
+
RealtimeVoiceChanger,
|
|
29
|
+
} from './index';
|
|
30
|
+
import type { SonareModule } from './sonare.js';
|
|
14
31
|
import type { SonareRtModule } from './sonare-rt';
|
|
15
32
|
|
|
16
33
|
// With code-splitting disabled, the worklet bundle carries its own copy of the
|
|
@@ -20,100 +37,182 @@ import type { SonareRtModule } from './sonare-rt';
|
|
|
20
37
|
// `index` module.
|
|
21
38
|
export { init, isInitialized } from './index';
|
|
22
39
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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';
|
|
117
216
|
|
|
118
217
|
export interface SonareEngineOptions extends SonareRealtimeEngineNodeOptions {
|
|
119
218
|
offlineEngine?: RealtimeEngine;
|
|
@@ -121,739 +220,11 @@ export interface SonareEngineOptions extends SonareRealtimeEngineNodeOptions {
|
|
|
121
220
|
offlineChannelCount?: number;
|
|
122
221
|
}
|
|
123
222
|
|
|
124
|
-
export interface SonareEngineTransportFacade {
|
|
125
|
-
play(sampleTime?: number): boolean;
|
|
126
|
-
stop(sampleTime?: number): boolean;
|
|
127
|
-
seekPpq(ppq: number, sampleTime?: number): boolean;
|
|
128
|
-
seekSeconds(seconds: number, sampleTime?: number): boolean;
|
|
129
|
-
setTempo(bpm: number): void;
|
|
130
|
-
setLoop(startPpq: number, endPpq: number, enabled?: boolean): boolean;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
223
|
type SuspendableAudioContext = BaseAudioContext & {
|
|
134
224
|
suspend?: () => Promise<void>;
|
|
135
225
|
resume?: () => Promise<void>;
|
|
136
226
|
};
|
|
137
227
|
|
|
138
|
-
type WorkletInput = readonly (readonly Float32Array[])[];
|
|
139
|
-
type WorkletOutput = Float32Array[][];
|
|
140
|
-
|
|
141
|
-
export interface SonareWorkletScheduleInsertAutomationMessage {
|
|
142
|
-
type: 'scheduleInsertAutomation';
|
|
143
|
-
stripIndex: number;
|
|
144
|
-
insertIndex: number;
|
|
145
|
-
paramId: number;
|
|
146
|
-
value: number;
|
|
147
|
-
samplePos?: number;
|
|
148
|
-
curve?: AutomationCurve;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
export interface SonareWorkletSetMeterIntervalMessage {
|
|
152
|
-
type: 'setMeterInterval';
|
|
153
|
-
frames: number;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
export interface SonareWorkletDestroyMessage {
|
|
157
|
-
type: 'destroy';
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
export type SonareWorkletMessage =
|
|
161
|
-
| SonareWorkletScheduleInsertAutomationMessage
|
|
162
|
-
| SonareWorkletSetMeterIntervalMessage
|
|
163
|
-
| SonareWorkletDestroyMessage;
|
|
164
|
-
|
|
165
|
-
export interface SonareWorkletMeterSnapshot {
|
|
166
|
-
type: 'meter';
|
|
167
|
-
frame: number;
|
|
168
|
-
peakDbL: number;
|
|
169
|
-
peakDbR: number;
|
|
170
|
-
rmsDbL: number;
|
|
171
|
-
rmsDbR: number;
|
|
172
|
-
correlation: number;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
export interface SonareWorkletSpectrumSnapshot {
|
|
176
|
-
type: 'spectrum';
|
|
177
|
-
frame: number;
|
|
178
|
-
bands: Float32Array;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
export type SonareWorkletTransportMessage =
|
|
182
|
-
| SonareWorkletMeterSnapshot
|
|
183
|
-
| SonareWorkletSpectrumSnapshot
|
|
184
|
-
| SonareEngineTelemetryRecord;
|
|
185
|
-
|
|
186
|
-
export const SONARE_METER_RING_HEADER_INTS = 4;
|
|
187
|
-
// Record layout: [frameLo, peakDbL, peakDbR, rmsDbL, rmsDbR, correlation, frameHi].
|
|
188
|
-
// The sample-frame index is monotonically increasing and quickly exceeds the
|
|
189
|
-
// 2^24 exact-integer range of a single Float32 slot (~349 s at 48 kHz), so it is
|
|
190
|
-
// stored split across two Float32 lanes (low 24 bits + high bits) for exact
|
|
191
|
-
// reconstruction. See encodeFrameLo/encodeFrameHi/decodeFrame.
|
|
192
|
-
export const SONARE_METER_RING_RECORD_FLOATS = 7;
|
|
193
|
-
export const SONARE_SPECTRUM_RING_HEADER_INTS = 5;
|
|
194
|
-
|
|
195
|
-
/** Base for splitting a frame index into two exactly-representable Float32 lanes. */
|
|
196
|
-
const SONARE_FRAME_LANE_BASE = 0x1000000; // 2^24
|
|
197
|
-
|
|
198
|
-
/** Low 24 bits of a frame index (exact in Float32). */
|
|
199
|
-
export function encodeFrameLo(frame: number): number {
|
|
200
|
-
const f = Math.max(0, Math.floor(frame));
|
|
201
|
-
return f % SONARE_FRAME_LANE_BASE;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/** High bits of a frame index above 2^24 (exact in Float32 up to ~2^48). */
|
|
205
|
-
export function encodeFrameHi(frame: number): number {
|
|
206
|
-
const f = Math.max(0, Math.floor(frame));
|
|
207
|
-
return Math.floor(f / SONARE_FRAME_LANE_BASE);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/** Reconstruct a frame index from its low/high Float32 lanes. */
|
|
211
|
-
export function decodeFrame(lo: number, hi: number): number {
|
|
212
|
-
return hi * SONARE_FRAME_LANE_BASE + lo;
|
|
213
|
-
}
|
|
214
|
-
export const SONARE_ENGINE_RING_HEADER_INTS = 5;
|
|
215
|
-
export const SONARE_ENGINE_COMMAND_RECORD_BYTES = 32;
|
|
216
|
-
export const SONARE_ENGINE_TELEMETRY_RECORD_BYTES = 48;
|
|
217
|
-
|
|
218
|
-
export enum SonareEngineCommandType {
|
|
219
|
-
SetParam = 0,
|
|
220
|
-
SetParamSmoothed = 1,
|
|
221
|
-
TransportPlay = 2,
|
|
222
|
-
TransportStop = 3,
|
|
223
|
-
TransportSeekSample = 4,
|
|
224
|
-
TransportSeekPpq = 5,
|
|
225
|
-
SetTempoMap = 6,
|
|
226
|
-
SetLoop = 7,
|
|
227
|
-
SwapGraph = 8,
|
|
228
|
-
SwapAutomation = 9,
|
|
229
|
-
SetSoloMute = 10,
|
|
230
|
-
AddClip = 11,
|
|
231
|
-
RemoveClip = 12,
|
|
232
|
-
ArmRecord = 13,
|
|
233
|
-
Punch = 14,
|
|
234
|
-
SetMetronome = 15,
|
|
235
|
-
SetMarker = 16,
|
|
236
|
-
SeekMarker = 17,
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
export enum SonareEngineTelemetryType {
|
|
240
|
-
ProcessBlock = 0,
|
|
241
|
-
Error = 1,
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
export enum SonareEngineTelemetryError {
|
|
245
|
-
None = 0,
|
|
246
|
-
CommandQueueOverflow = 1,
|
|
247
|
-
PendingCommandOverflow = 2,
|
|
248
|
-
BoundaryOverflow = 3,
|
|
249
|
-
TelemetryOverflow = 4,
|
|
250
|
-
CaptureOverflow = 5,
|
|
251
|
-
MaxBlockExceeded = 6,
|
|
252
|
-
UnknownTarget = 7,
|
|
253
|
-
NonRealtimeSafeParameter = 8,
|
|
254
|
-
NotPrepared = 9,
|
|
255
|
-
NonQueueableCommand = 10,
|
|
256
|
-
AutomationBindTargetOverflow = 11,
|
|
257
|
-
StaleAutomationLanes = 12,
|
|
258
|
-
SmoothedParameterCapacity = 13,
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
interface WorkletTransport {
|
|
262
|
-
postMessage?: (message: SonareWorkletTransportMessage) => void;
|
|
263
|
-
onMeter?: (meter: SonareWorkletMeterSnapshot) => void;
|
|
264
|
-
onSpectrum?: (spectrum: SonareWorkletSpectrumSnapshot) => void;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
interface ResolvedMetronomeConfig {
|
|
268
|
-
beatGain: number;
|
|
269
|
-
accentGain: number;
|
|
270
|
-
clickSamples: number;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// Fallback metronome gains/click length used by the worklet consumer until the
|
|
274
|
-
// host posts a 'syncMetronome' config. Aligned with the embind setMetronome
|
|
275
|
-
// defaults (src/wasm/bindings.cpp) so offline and realtime metronomes match.
|
|
276
|
-
const DEFAULT_METRONOME_CONFIG: ResolvedMetronomeConfig = {
|
|
277
|
-
beatGain: 0.35,
|
|
278
|
-
accentGain: 0.7,
|
|
279
|
-
clickSamples: 96,
|
|
280
|
-
};
|
|
281
|
-
|
|
282
|
-
function resolveMetronomeConfig(config: EngineMetronomeConfig): ResolvedMetronomeConfig {
|
|
283
|
-
return {
|
|
284
|
-
beatGain: config.beatGain ?? DEFAULT_METRONOME_CONFIG.beatGain,
|
|
285
|
-
accentGain: config.accentGain ?? DEFAULT_METRONOME_CONFIG.accentGain,
|
|
286
|
-
clickSamples: config.clickSamples ?? DEFAULT_METRONOME_CONFIG.clickSamples,
|
|
287
|
-
};
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
export interface SonareMeterRingBuffer {
|
|
291
|
-
sharedBuffer: SharedArrayBuffer;
|
|
292
|
-
header: Int32Array;
|
|
293
|
-
records: Float32Array;
|
|
294
|
-
capacity: number;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
export interface SonareMeterRingReadResult {
|
|
298
|
-
nextReadIndex: number;
|
|
299
|
-
meters: SonareWorkletMeterSnapshot[];
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
export interface SonareSpectrumRingBuffer {
|
|
303
|
-
sharedBuffer: SharedArrayBuffer;
|
|
304
|
-
header: Int32Array;
|
|
305
|
-
records: Float32Array;
|
|
306
|
-
capacity: number;
|
|
307
|
-
bands: number;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
export interface SonareSpectrumRingReadResult {
|
|
311
|
-
nextReadIndex: number;
|
|
312
|
-
spectra: SonareWorkletSpectrumSnapshot[];
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
export interface SonareEngineCommandRecord {
|
|
316
|
-
type: SonareEngineCommandType | number;
|
|
317
|
-
targetId?: number;
|
|
318
|
-
sampleTime?: number | bigint;
|
|
319
|
-
argFloat?: number;
|
|
320
|
-
argInt?: number | bigint;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
// Out-of-band control messages posted from the main-thread SonareEngine facade
|
|
324
|
-
// to the worklet engine processor over node.port. Unlike SonareEngineCommandRecord
|
|
325
|
-
// (a small POD POSTed/ringed every block) these carry bulk/structured payloads
|
|
326
|
-
// (clip audio buffers, marker lists, metronome config) that cannot fit the
|
|
327
|
-
// fixed-size SAB command record, so they are applied OUTSIDE process() — the
|
|
328
|
-
// audio-thread equivalent of the engine's control-thread RtPublisher setters.
|
|
329
|
-
export interface SonareEngineSyncClipsMessage {
|
|
330
|
-
type: 'syncClips';
|
|
331
|
-
clips: EngineClip[];
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
export interface SonareEngineSyncMarkersMessage {
|
|
335
|
-
type: 'syncMarkers';
|
|
336
|
-
markers: EngineMarker[];
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
export interface SonareEngineSyncMetronomeMessage {
|
|
340
|
-
type: 'syncMetronome';
|
|
341
|
-
config: EngineMetronomeConfig;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
export interface SonareEngineSyncAutomationMessage {
|
|
345
|
-
type: 'syncAutomation';
|
|
346
|
-
paramId: number;
|
|
347
|
-
points: EngineAutomationPoint[];
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
export type SonareEngineSyncMessage =
|
|
351
|
-
| SonareEngineSyncClipsMessage
|
|
352
|
-
| SonareEngineSyncMarkersMessage
|
|
353
|
-
| SonareEngineSyncMetronomeMessage
|
|
354
|
-
| SonareEngineSyncAutomationMessage;
|
|
355
|
-
|
|
356
|
-
export interface SonareEngineTelemetryRecord {
|
|
357
|
-
type: SonareEngineTelemetryType | number;
|
|
358
|
-
error: SonareEngineTelemetryError | number;
|
|
359
|
-
renderFrame: number;
|
|
360
|
-
timelineSample: number;
|
|
361
|
-
audibleTimelineSample: number;
|
|
362
|
-
graphLatencySamplesQ8: number;
|
|
363
|
-
value: number;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
export interface SonareEngineCommandRingBuffer {
|
|
367
|
-
sharedBuffer: SharedArrayBuffer;
|
|
368
|
-
header: Int32Array;
|
|
369
|
-
view: DataView;
|
|
370
|
-
capacity: number;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
export interface SonareEngineTelemetryRingBuffer {
|
|
374
|
-
sharedBuffer: SharedArrayBuffer;
|
|
375
|
-
header: Int32Array;
|
|
376
|
-
view: DataView;
|
|
377
|
-
capacity: number;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
export interface SonareEngineTelemetryRingReadResult {
|
|
381
|
-
nextReadIndex: number;
|
|
382
|
-
telemetry: SonareEngineTelemetryRecord[];
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
interface SharedMeterRingWriter {
|
|
386
|
-
header: Int32Array;
|
|
387
|
-
records: Float32Array;
|
|
388
|
-
capacity: number;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
interface SharedSpectrumRingWriter {
|
|
392
|
-
header: Int32Array;
|
|
393
|
-
records: Float32Array;
|
|
394
|
-
capacity: number;
|
|
395
|
-
bands: number;
|
|
396
|
-
recordFloats: number;
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
interface WorkletPort {
|
|
400
|
-
postMessage?: (message: unknown) => void;
|
|
401
|
-
onmessage?: (event: { data: unknown }) => void;
|
|
402
|
-
addEventListener?: (type: 'message', listener: (event: { data: unknown }) => void) => void;
|
|
403
|
-
start?: () => void;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
function toDb(value: number): number {
|
|
407
|
-
return value > 0 ? 20 * Math.log10(value) : Number.NEGATIVE_INFINITY;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
411
|
-
return typeof value === 'object' && value !== null;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
function isWorkletMessage(value: unknown): value is SonareWorkletMessage {
|
|
415
|
-
if (!isRecord(value) || typeof value.type !== 'string') {
|
|
416
|
-
return false;
|
|
417
|
-
}
|
|
418
|
-
return (
|
|
419
|
-
value.type === 'scheduleInsertAutomation' ||
|
|
420
|
-
value.type === 'setMeterInterval' ||
|
|
421
|
-
value.type === 'destroy'
|
|
422
|
-
);
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
function isEngineCommandRecord(value: unknown): value is SonareEngineCommandRecord {
|
|
426
|
-
return isRecord(value) && typeof value.type === 'number';
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
function isEngineSyncMessage(value: unknown): value is SonareEngineSyncMessage {
|
|
430
|
-
if (!isRecord(value) || typeof value.type !== 'string') {
|
|
431
|
-
return false;
|
|
432
|
-
}
|
|
433
|
-
return (
|
|
434
|
-
value.type === 'syncClips' ||
|
|
435
|
-
value.type === 'syncMarkers' ||
|
|
436
|
-
value.type === 'syncMetronome' ||
|
|
437
|
-
value.type === 'syncAutomation'
|
|
438
|
-
);
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
function isRealtimeVoiceChangerMessage(value: unknown): value is SonareRealtimeVoiceChangerMessage {
|
|
442
|
-
if (!isRecord(value) || typeof value.type !== 'string') {
|
|
443
|
-
return false;
|
|
444
|
-
}
|
|
445
|
-
return value.type === 'setConfig' || value.type === 'reset' || value.type === 'destroy';
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
function isEngineTelemetryRecord(value: unknown): value is SonareEngineTelemetryRecord {
|
|
449
|
-
return (
|
|
450
|
-
isRecord(value) &&
|
|
451
|
-
typeof value.type === 'number' &&
|
|
452
|
-
typeof value.error === 'number' &&
|
|
453
|
-
typeof value.renderFrame === 'number' &&
|
|
454
|
-
typeof value.timelineSample === 'number' &&
|
|
455
|
-
typeof value.audibleTimelineSample === 'number' &&
|
|
456
|
-
typeof value.graphLatencySamplesQ8 === 'number' &&
|
|
457
|
-
typeof value.value === 'number'
|
|
458
|
-
);
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
function isMeterSnapshot(value: unknown): value is SonareWorkletMeterSnapshot {
|
|
462
|
-
return (
|
|
463
|
-
isRecord(value) &&
|
|
464
|
-
value.type === 'meter' &&
|
|
465
|
-
typeof value.frame === 'number' &&
|
|
466
|
-
typeof value.peakDbL === 'number' &&
|
|
467
|
-
typeof value.peakDbR === 'number' &&
|
|
468
|
-
typeof value.rmsDbL === 'number' &&
|
|
469
|
-
typeof value.rmsDbR === 'number' &&
|
|
470
|
-
typeof value.correlation === 'number'
|
|
471
|
-
);
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
export function sonareMeterRingBufferByteLength(capacity: number): number {
|
|
475
|
-
const clampedCapacity = Math.max(1, Math.floor(capacity));
|
|
476
|
-
return (
|
|
477
|
-
SONARE_METER_RING_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT +
|
|
478
|
-
clampedCapacity * SONARE_METER_RING_RECORD_FLOATS * Float32Array.BYTES_PER_ELEMENT
|
|
479
|
-
);
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
export function createSonareMeterRingBuffer(capacity = 128): SonareMeterRingBuffer {
|
|
483
|
-
const clampedCapacity = Math.max(1, Math.floor(capacity));
|
|
484
|
-
const sharedBuffer = new SharedArrayBuffer(sonareMeterRingBufferByteLength(clampedCapacity));
|
|
485
|
-
const ring = meterRingFromSharedBuffer(sharedBuffer, clampedCapacity);
|
|
486
|
-
Atomics.store(ring.header, 0, 0);
|
|
487
|
-
Atomics.store(ring.header, 1, clampedCapacity);
|
|
488
|
-
Atomics.store(ring.header, 2, SONARE_METER_RING_RECORD_FLOATS);
|
|
489
|
-
Atomics.store(ring.header, 3, 0);
|
|
490
|
-
return { sharedBuffer, header: ring.header, records: ring.records, capacity: ring.capacity };
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
export function readSonareMeterRingBuffer(
|
|
494
|
-
ring: SonareMeterRingBuffer,
|
|
495
|
-
readIndex = 0,
|
|
496
|
-
): SonareMeterRingReadResult {
|
|
497
|
-
const writeIndex = Atomics.load(ring.header, 0);
|
|
498
|
-
const nextReadIndex = Math.max(0, Math.min(readIndex, writeIndex));
|
|
499
|
-
const firstReadable = Math.max(nextReadIndex, writeIndex - ring.capacity);
|
|
500
|
-
const meters: SonareWorkletMeterSnapshot[] = [];
|
|
501
|
-
for (let index = firstReadable; index < writeIndex; index++) {
|
|
502
|
-
const offset = (index % ring.capacity) * SONARE_METER_RING_RECORD_FLOATS;
|
|
503
|
-
meters.push({
|
|
504
|
-
type: 'meter',
|
|
505
|
-
frame: decodeFrame(ring.records[offset], ring.records[offset + 6]),
|
|
506
|
-
peakDbL: ring.records[offset + 1],
|
|
507
|
-
peakDbR: ring.records[offset + 2],
|
|
508
|
-
rmsDbL: ring.records[offset + 3],
|
|
509
|
-
rmsDbR: ring.records[offset + 4],
|
|
510
|
-
correlation: ring.records[offset + 5],
|
|
511
|
-
});
|
|
512
|
-
}
|
|
513
|
-
return { nextReadIndex: writeIndex, meters };
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
export function sonareSpectrumRingBufferByteLength(capacity: number, bands = 16): number {
|
|
517
|
-
const clampedCapacity = Math.max(1, Math.floor(capacity));
|
|
518
|
-
const clampedBands = Math.max(1, Math.floor(bands));
|
|
519
|
-
// Record layout: [frameLo, frameHi, bandCount, band0, band1, ...]. frame is
|
|
520
|
-
// split across two Float32 lanes for exact reconstruction beyond 2^24.
|
|
521
|
-
return (
|
|
522
|
-
SONARE_SPECTRUM_RING_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT +
|
|
523
|
-
clampedCapacity * (3 + clampedBands) * Float32Array.BYTES_PER_ELEMENT
|
|
524
|
-
);
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
export function createSonareSpectrumRingBuffer(
|
|
528
|
-
capacity = 128,
|
|
529
|
-
bands = 16,
|
|
530
|
-
): SonareSpectrumRingBuffer {
|
|
531
|
-
const clampedCapacity = Math.max(1, Math.floor(capacity));
|
|
532
|
-
const clampedBands = Math.max(1, Math.floor(bands));
|
|
533
|
-
const sharedBuffer = new SharedArrayBuffer(
|
|
534
|
-
sonareSpectrumRingBufferByteLength(clampedCapacity, clampedBands),
|
|
535
|
-
);
|
|
536
|
-
const ring = spectrumRingFromSharedBuffer(sharedBuffer, clampedCapacity, clampedBands);
|
|
537
|
-
Atomics.store(ring.header, 0, 0);
|
|
538
|
-
Atomics.store(ring.header, 1, clampedCapacity);
|
|
539
|
-
Atomics.store(ring.header, 2, ring.recordFloats);
|
|
540
|
-
Atomics.store(ring.header, 3, clampedBands);
|
|
541
|
-
Atomics.store(ring.header, 4, 0);
|
|
542
|
-
return {
|
|
543
|
-
sharedBuffer,
|
|
544
|
-
header: ring.header,
|
|
545
|
-
records: ring.records,
|
|
546
|
-
capacity: ring.capacity,
|
|
547
|
-
bands: ring.bands,
|
|
548
|
-
};
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
export function readSonareSpectrumRingBuffer(
|
|
552
|
-
ring: SonareSpectrumRingBuffer,
|
|
553
|
-
readIndex = 0,
|
|
554
|
-
): SonareSpectrumRingReadResult {
|
|
555
|
-
const writeIndex = Atomics.load(ring.header, 0);
|
|
556
|
-
const recordFloats = Atomics.load(ring.header, 2) || 3 + ring.bands;
|
|
557
|
-
const bands = Atomics.load(ring.header, 3) || ring.bands;
|
|
558
|
-
const nextReadIndex = Math.max(0, Math.min(readIndex, writeIndex));
|
|
559
|
-
const firstReadable = Math.max(nextReadIndex, writeIndex - ring.capacity);
|
|
560
|
-
const spectra: SonareWorkletSpectrumSnapshot[] = [];
|
|
561
|
-
for (let index = firstReadable; index < writeIndex; index++) {
|
|
562
|
-
const offset = (index % ring.capacity) * recordFloats;
|
|
563
|
-
const values = new Float32Array(bands);
|
|
564
|
-
values.set(ring.records.subarray(offset + 3, offset + 3 + bands));
|
|
565
|
-
spectra.push({
|
|
566
|
-
type: 'spectrum',
|
|
567
|
-
frame: decodeFrame(ring.records[offset], ring.records[offset + 1]),
|
|
568
|
-
bands: values,
|
|
569
|
-
});
|
|
570
|
-
}
|
|
571
|
-
return { nextReadIndex: writeIndex, spectra };
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
export function sonareEngineCommandRingBufferByteLength(capacity: number): number {
|
|
575
|
-
const clampedCapacity = Math.max(1, Math.floor(capacity));
|
|
576
|
-
return (
|
|
577
|
-
SONARE_ENGINE_RING_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT +
|
|
578
|
-
clampedCapacity * SONARE_ENGINE_COMMAND_RECORD_BYTES
|
|
579
|
-
);
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
export function sonareEngineTelemetryRingBufferByteLength(capacity: number): number {
|
|
583
|
-
const clampedCapacity = Math.max(1, Math.floor(capacity));
|
|
584
|
-
return (
|
|
585
|
-
SONARE_ENGINE_RING_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT +
|
|
586
|
-
clampedCapacity * SONARE_ENGINE_TELEMETRY_RECORD_BYTES
|
|
587
|
-
);
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
export function createSonareEngineCommandRingBuffer(capacity = 128): SonareEngineCommandRingBuffer {
|
|
591
|
-
const clampedCapacity = Math.max(1, Math.floor(capacity));
|
|
592
|
-
const sharedBuffer = new SharedArrayBuffer(
|
|
593
|
-
sonareEngineCommandRingBufferByteLength(clampedCapacity),
|
|
594
|
-
);
|
|
595
|
-
const ring = engineRingFromSharedBuffer(
|
|
596
|
-
sharedBuffer,
|
|
597
|
-
SONARE_ENGINE_COMMAND_RECORD_BYTES,
|
|
598
|
-
clampedCapacity,
|
|
599
|
-
);
|
|
600
|
-
return { sharedBuffer, header: ring.header, view: ring.view, capacity: ring.capacity };
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
export function createSonareEngineTelemetryRingBuffer(
|
|
604
|
-
capacity = 128,
|
|
605
|
-
): SonareEngineTelemetryRingBuffer {
|
|
606
|
-
const clampedCapacity = Math.max(1, Math.floor(capacity));
|
|
607
|
-
const sharedBuffer = new SharedArrayBuffer(
|
|
608
|
-
sonareEngineTelemetryRingBufferByteLength(clampedCapacity),
|
|
609
|
-
);
|
|
610
|
-
const ring = engineRingFromSharedBuffer(
|
|
611
|
-
sharedBuffer,
|
|
612
|
-
SONARE_ENGINE_TELEMETRY_RECORD_BYTES,
|
|
613
|
-
clampedCapacity,
|
|
614
|
-
);
|
|
615
|
-
return { sharedBuffer, header: ring.header, view: ring.view, capacity: ring.capacity };
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
export function pushSonareEngineCommandRingBuffer(
|
|
619
|
-
ring: SonareEngineCommandRingBuffer,
|
|
620
|
-
command: SonareEngineCommandRecord,
|
|
621
|
-
): boolean {
|
|
622
|
-
const writeIndex = Atomics.load(ring.header, 0);
|
|
623
|
-
const readIndex = Atomics.load(ring.header, 1);
|
|
624
|
-
if (writeIndex - readIndex >= ring.capacity) {
|
|
625
|
-
Atomics.add(ring.header, 4, 1);
|
|
626
|
-
return false;
|
|
627
|
-
}
|
|
628
|
-
writeEngineCommandRecord(
|
|
629
|
-
ring.view,
|
|
630
|
-
recordOffset(writeIndex, ring.capacity, SONARE_ENGINE_COMMAND_RECORD_BYTES),
|
|
631
|
-
command,
|
|
632
|
-
);
|
|
633
|
-
Atomics.store(ring.header, 0, writeIndex + 1);
|
|
634
|
-
return true;
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
export function popSonareEngineCommandRingBuffer(
|
|
638
|
-
ring: SonareEngineCommandRingBuffer,
|
|
639
|
-
): SonareEngineCommandRecord | null {
|
|
640
|
-
const readIndex = Atomics.load(ring.header, 1);
|
|
641
|
-
const writeIndex = Atomics.load(ring.header, 0);
|
|
642
|
-
if (readIndex >= writeIndex) {
|
|
643
|
-
return null;
|
|
644
|
-
}
|
|
645
|
-
const command = readEngineCommandRecord(
|
|
646
|
-
ring.view,
|
|
647
|
-
recordOffset(readIndex, ring.capacity, SONARE_ENGINE_COMMAND_RECORD_BYTES),
|
|
648
|
-
);
|
|
649
|
-
Atomics.store(ring.header, 1, readIndex + 1);
|
|
650
|
-
return command;
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
export function writeSonareEngineTelemetryRingBuffer(
|
|
654
|
-
ring: SonareEngineTelemetryRingBuffer,
|
|
655
|
-
telemetry: SonareEngineTelemetryRecord,
|
|
656
|
-
): void {
|
|
657
|
-
const writeIndex = Atomics.load(ring.header, 0);
|
|
658
|
-
writeEngineTelemetryRecord(
|
|
659
|
-
ring.view,
|
|
660
|
-
recordOffset(writeIndex, ring.capacity, SONARE_ENGINE_TELEMETRY_RECORD_BYTES),
|
|
661
|
-
telemetry,
|
|
662
|
-
);
|
|
663
|
-
Atomics.store(ring.header, 0, writeIndex + 1);
|
|
664
|
-
if (writeIndex + 1 > ring.capacity) {
|
|
665
|
-
Atomics.store(ring.header, 4, writeIndex + 1 - ring.capacity);
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
export function readSonareEngineTelemetryRingBuffer(
|
|
670
|
-
ring: SonareEngineTelemetryRingBuffer,
|
|
671
|
-
readIndex = 0,
|
|
672
|
-
): SonareEngineTelemetryRingReadResult {
|
|
673
|
-
const writeIndex = Atomics.load(ring.header, 0);
|
|
674
|
-
const nextReadIndex = Math.max(0, Math.min(readIndex, writeIndex));
|
|
675
|
-
const firstReadable = Math.max(nextReadIndex, writeIndex - ring.capacity);
|
|
676
|
-
const telemetry: SonareEngineTelemetryRecord[] = [];
|
|
677
|
-
for (let index = firstReadable; index < writeIndex; index++) {
|
|
678
|
-
telemetry.push(
|
|
679
|
-
readEngineTelemetryRecord(
|
|
680
|
-
ring.view,
|
|
681
|
-
recordOffset(index, ring.capacity, SONARE_ENGINE_TELEMETRY_RECORD_BYTES),
|
|
682
|
-
),
|
|
683
|
-
);
|
|
684
|
-
}
|
|
685
|
-
return { nextReadIndex: writeIndex, telemetry };
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
function meterRingFromSharedBuffer(
|
|
689
|
-
sharedBuffer: SharedArrayBuffer,
|
|
690
|
-
fallbackCapacity?: number,
|
|
691
|
-
): SharedMeterRingWriter {
|
|
692
|
-
const headerBytes = SONARE_METER_RING_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT;
|
|
693
|
-
const header = new Int32Array(sharedBuffer, 0, SONARE_METER_RING_HEADER_INTS);
|
|
694
|
-
const existingCapacity = Atomics.load(header, 1);
|
|
695
|
-
const capacity = Math.max(1, Math.floor(existingCapacity || fallbackCapacity || 1));
|
|
696
|
-
const minBytes = sonareMeterRingBufferByteLength(capacity);
|
|
697
|
-
if (sharedBuffer.byteLength < minBytes) {
|
|
698
|
-
throw new Error('meterSharedBuffer is too small for the requested ring capacity.');
|
|
699
|
-
}
|
|
700
|
-
Atomics.store(header, 1, capacity);
|
|
701
|
-
Atomics.store(header, 2, SONARE_METER_RING_RECORD_FLOATS);
|
|
702
|
-
return {
|
|
703
|
-
header,
|
|
704
|
-
records: new Float32Array(
|
|
705
|
-
sharedBuffer,
|
|
706
|
-
headerBytes,
|
|
707
|
-
capacity * SONARE_METER_RING_RECORD_FLOATS,
|
|
708
|
-
),
|
|
709
|
-
capacity,
|
|
710
|
-
};
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
function spectrumRingFromSharedBuffer(
|
|
714
|
-
sharedBuffer: SharedArrayBuffer,
|
|
715
|
-
fallbackCapacity?: number,
|
|
716
|
-
fallbackBands?: number,
|
|
717
|
-
): SharedSpectrumRingWriter {
|
|
718
|
-
const headerBytes = SONARE_SPECTRUM_RING_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT;
|
|
719
|
-
const header = new Int32Array(sharedBuffer, 0, SONARE_SPECTRUM_RING_HEADER_INTS);
|
|
720
|
-
const existingCapacity = Atomics.load(header, 1);
|
|
721
|
-
const existingBands = Atomics.load(header, 3);
|
|
722
|
-
const capacity = Math.max(1, Math.floor(existingCapacity || fallbackCapacity || 1));
|
|
723
|
-
const bands = Math.max(1, Math.floor(existingBands || fallbackBands || 16));
|
|
724
|
-
const recordFloats = 3 + bands;
|
|
725
|
-
const minBytes = sonareSpectrumRingBufferByteLength(capacity, bands);
|
|
726
|
-
if (sharedBuffer.byteLength < minBytes) {
|
|
727
|
-
throw new Error('spectrumSharedBuffer is too small for the requested ring capacity.');
|
|
728
|
-
}
|
|
729
|
-
Atomics.store(header, 1, capacity);
|
|
730
|
-
Atomics.store(header, 2, recordFloats);
|
|
731
|
-
Atomics.store(header, 3, bands);
|
|
732
|
-
return {
|
|
733
|
-
header,
|
|
734
|
-
records: new Float32Array(sharedBuffer, headerBytes, capacity * recordFloats),
|
|
735
|
-
capacity,
|
|
736
|
-
bands,
|
|
737
|
-
recordFloats,
|
|
738
|
-
};
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
function engineRingFromSharedBuffer(
|
|
742
|
-
sharedBuffer: SharedArrayBuffer,
|
|
743
|
-
recordBytes: number,
|
|
744
|
-
fallbackCapacity?: number,
|
|
745
|
-
): { header: Int32Array; view: DataView; capacity: number } {
|
|
746
|
-
const headerBytes = SONARE_ENGINE_RING_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT;
|
|
747
|
-
const header = new Int32Array(sharedBuffer, 0, SONARE_ENGINE_RING_HEADER_INTS);
|
|
748
|
-
const existingCapacity = Atomics.load(header, 2);
|
|
749
|
-
const capacity = Math.max(1, Math.floor(existingCapacity || fallbackCapacity || 1));
|
|
750
|
-
const minBytes = headerBytes + capacity * recordBytes;
|
|
751
|
-
if (sharedBuffer.byteLength < minBytes) {
|
|
752
|
-
throw new Error('engine SharedArrayBuffer is too small for the requested ring capacity.');
|
|
753
|
-
}
|
|
754
|
-
Atomics.store(header, 2, capacity);
|
|
755
|
-
Atomics.store(header, 3, recordBytes);
|
|
756
|
-
return {
|
|
757
|
-
header,
|
|
758
|
-
view: new DataView(sharedBuffer, headerBytes, capacity * recordBytes),
|
|
759
|
-
capacity,
|
|
760
|
-
};
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
function recordOffset(index: number, capacity: number, recordBytes: number): number {
|
|
764
|
-
return (index % capacity) * recordBytes;
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
function toBigInt64(value: number | bigint | undefined, fallback: bigint): bigint {
|
|
768
|
-
if (typeof value === 'bigint') {
|
|
769
|
-
return value;
|
|
770
|
-
}
|
|
771
|
-
if (typeof value === 'number') {
|
|
772
|
-
return BigInt(Math.trunc(value));
|
|
773
|
-
}
|
|
774
|
-
return fallback;
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
function writeEngineCommandRecord(
|
|
778
|
-
view: DataView,
|
|
779
|
-
offset: number,
|
|
780
|
-
command: SonareEngineCommandRecord,
|
|
781
|
-
): void {
|
|
782
|
-
view.setUint32(offset, command.type, true);
|
|
783
|
-
view.setUint32(offset + 4, command.targetId ?? 0, true);
|
|
784
|
-
view.setBigInt64(offset + 8, toBigInt64(command.sampleTime, -1n), true);
|
|
785
|
-
// argFloat occupies a full 8-byte Float64 slot (replacing the old Float32 +
|
|
786
|
-
// 4-byte pad) so PPQ scalars carried here keep full double precision over the
|
|
787
|
-
// SAB transport, matching the engine's double-precision seek/loop contract.
|
|
788
|
-
view.setFloat64(offset + 16, command.argFloat ?? 0, true);
|
|
789
|
-
view.setBigInt64(offset + 24, toBigInt64(command.argInt, 0n), true);
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
function readEngineCommandRecord(view: DataView, offset: number): SonareEngineCommandRecord {
|
|
793
|
-
return {
|
|
794
|
-
type: view.getUint32(offset, true),
|
|
795
|
-
targetId: view.getUint32(offset + 4, true),
|
|
796
|
-
sampleTime: Number(view.getBigInt64(offset + 8, true)),
|
|
797
|
-
argFloat: view.getFloat64(offset + 16, true),
|
|
798
|
-
argInt: Number(view.getBigInt64(offset + 24, true)),
|
|
799
|
-
};
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
function writeEngineTelemetryRecord(
|
|
803
|
-
view: DataView,
|
|
804
|
-
offset: number,
|
|
805
|
-
telemetry: SonareEngineTelemetryRecord,
|
|
806
|
-
): void {
|
|
807
|
-
view.setUint32(offset, telemetry.type, true);
|
|
808
|
-
view.setUint32(offset + 4, telemetry.error, true);
|
|
809
|
-
view.setBigInt64(offset + 8, BigInt(Math.trunc(telemetry.renderFrame)), true);
|
|
810
|
-
view.setBigInt64(offset + 16, BigInt(Math.trunc(telemetry.timelineSample)), true);
|
|
811
|
-
view.setBigInt64(offset + 24, BigInt(Math.trunc(telemetry.audibleTimelineSample)), true);
|
|
812
|
-
view.setInt32(offset + 32, telemetry.graphLatencySamplesQ8, true);
|
|
813
|
-
view.setUint32(offset + 36, telemetry.value, true);
|
|
814
|
-
view.setBigInt64(offset + 40, 0n, true);
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
function readEngineTelemetryRecord(view: DataView, offset: number): SonareEngineTelemetryRecord {
|
|
818
|
-
return {
|
|
819
|
-
type: view.getUint32(offset, true),
|
|
820
|
-
error: view.getUint32(offset + 4, true),
|
|
821
|
-
renderFrame: Number(view.getBigInt64(offset + 8, true)),
|
|
822
|
-
timelineSample: Number(view.getBigInt64(offset + 16, true)),
|
|
823
|
-
audibleTimelineSample: Number(view.getBigInt64(offset + 24, true)),
|
|
824
|
-
graphLatencySamplesQ8: view.getInt32(offset + 32, true),
|
|
825
|
-
value: view.getUint32(offset + 36, true),
|
|
826
|
-
};
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
function telemetryFromEngine(telemetry: EngineTelemetry): SonareEngineTelemetryRecord {
|
|
830
|
-
return {
|
|
831
|
-
type: telemetry.type,
|
|
832
|
-
error: telemetry.error,
|
|
833
|
-
renderFrame: telemetry.renderFrame,
|
|
834
|
-
timelineSample: telemetry.timelineSample,
|
|
835
|
-
audibleTimelineSample: telemetry.audibleTimelineSample,
|
|
836
|
-
graphLatencySamplesQ8: telemetry.graphLatencySamplesQ8,
|
|
837
|
-
value: telemetry.value,
|
|
838
|
-
};
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
function meterFromEngine(meter: EngineMeterTelemetry): SonareWorkletMeterSnapshot {
|
|
842
|
-
return {
|
|
843
|
-
type: 'meter',
|
|
844
|
-
frame: meter.renderFrame,
|
|
845
|
-
peakDbL: meter.peakDbL,
|
|
846
|
-
peakDbR: meter.peakDbR,
|
|
847
|
-
rmsDbL: meter.rmsDbL,
|
|
848
|
-
rmsDbR: meter.rmsDbR,
|
|
849
|
-
correlation: meter.correlation,
|
|
850
|
-
};
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
function magnitudeToDb(value: number): number {
|
|
854
|
-
return value > 1.0e-12 ? 20 * Math.log10(value) : -120;
|
|
855
|
-
}
|
|
856
|
-
|
|
857
228
|
/**
|
|
858
229
|
* AudioWorklet-style mixer bridge backed by the package's single `sonare.wasm`.
|
|
859
230
|
*
|
|
@@ -1041,12 +412,19 @@ export class SonareWorkletProcessor {
|
|
|
1041
412
|
const denominator = Math.sqrt(sumL * sumR);
|
|
1042
413
|
const meter: SonareWorkletMeterSnapshot = {
|
|
1043
414
|
type: 'meter',
|
|
415
|
+
targetId: 0,
|
|
1044
416
|
frame: this.processedFrames,
|
|
1045
417
|
peakDbL: toDb(peakL),
|
|
1046
418
|
peakDbR: toDb(peakR),
|
|
1047
419
|
rmsDbL: toDb(rmsL),
|
|
1048
420
|
rmsDbR: toDb(rmsR),
|
|
1049
421
|
correlation: denominator > 0 ? sumLR / denominator : 0,
|
|
422
|
+
truePeakDbL: toDb(peakL),
|
|
423
|
+
truePeakDbR: toDb(peakR),
|
|
424
|
+
momentaryLufs: Number.NaN,
|
|
425
|
+
shortTermLufs: Number.NaN,
|
|
426
|
+
integratedLufs: Number.NaN,
|
|
427
|
+
gainReductionDb: Number.NaN,
|
|
1050
428
|
};
|
|
1051
429
|
this.transport.onMeter?.(meter);
|
|
1052
430
|
if (this.meterRing) {
|
|
@@ -1064,12 +442,19 @@ export class SonareWorkletProcessor {
|
|
|
1064
442
|
const writeIndex = Atomics.load(ring.header, 0);
|
|
1065
443
|
const offset = (writeIndex % ring.capacity) * SONARE_METER_RING_RECORD_FLOATS;
|
|
1066
444
|
ring.records[offset] = encodeFrameLo(meter.frame);
|
|
1067
|
-
ring.records[offset + 1] = meter.
|
|
1068
|
-
ring.records[offset + 2] = meter.
|
|
1069
|
-
ring.records[offset + 3] = meter.
|
|
1070
|
-
ring.records[offset + 4] = meter.
|
|
1071
|
-
ring.records[offset + 5] = meter.
|
|
1072
|
-
ring.records[offset + 6] =
|
|
445
|
+
ring.records[offset + 1] = encodeFrameHi(meter.frame);
|
|
446
|
+
ring.records[offset + 2] = meter.targetId;
|
|
447
|
+
ring.records[offset + 3] = meter.peakDbL;
|
|
448
|
+
ring.records[offset + 4] = meter.peakDbR;
|
|
449
|
+
ring.records[offset + 5] = meter.rmsDbL;
|
|
450
|
+
ring.records[offset + 6] = meter.rmsDbR;
|
|
451
|
+
ring.records[offset + 7] = meter.correlation;
|
|
452
|
+
ring.records[offset + 8] = meter.truePeakDbL;
|
|
453
|
+
ring.records[offset + 9] = meter.truePeakDbR;
|
|
454
|
+
ring.records[offset + 10] = meter.momentaryLufs;
|
|
455
|
+
ring.records[offset + 11] = meter.shortTermLufs;
|
|
456
|
+
ring.records[offset + 12] = meter.integratedLufs;
|
|
457
|
+
ring.records[offset + 13] = meter.gainReductionDb;
|
|
1073
458
|
Atomics.store(ring.header, 0, writeIndex + 1);
|
|
1074
459
|
// writeIndex is a free-running monotonic counter, so an overflow guard here
|
|
1075
460
|
// would fire on essentially every write past the first `capacity` records
|
|
@@ -1165,6 +550,7 @@ export class SonareRealtimeEngineWorkletProcessor {
|
|
|
1165
550
|
private commandRing?: SonareEngineCommandRingBuffer;
|
|
1166
551
|
private telemetryRing?: SonareEngineTelemetryRingBuffer;
|
|
1167
552
|
private meterRing?: SharedMeterRingWriter;
|
|
553
|
+
private scopeRing?: SharedScopeRingWriter;
|
|
1168
554
|
private transport?: WorkletTransport;
|
|
1169
555
|
private meterIntervalFrames: number;
|
|
1170
556
|
private lastMeterFrame = Number.NEGATIVE_INFINITY;
|
|
@@ -1179,6 +565,7 @@ export class SonareRealtimeEngineWorkletProcessor {
|
|
|
1179
565
|
// allocated per render quantum (the old engine.process() round-tripped fresh
|
|
1180
566
|
// arrays on both heaps every block, an RT-safety hazard).
|
|
1181
567
|
private channelBuffers: Float32Array[];
|
|
568
|
+
private readonly liveClips = new Map<number, EngineClip>();
|
|
1182
569
|
|
|
1183
570
|
constructor(
|
|
1184
571
|
options: SonareRealtimeEngineWorkletProcessorOptions = {},
|
|
@@ -1207,6 +594,13 @@ export class SonareRealtimeEngineWorkletProcessor {
|
|
|
1207
594
|
this.meterRing = options.meterSharedBuffer
|
|
1208
595
|
? meterRingFromSharedBuffer(options.meterSharedBuffer, options.meterRingCapacity)
|
|
1209
596
|
: undefined;
|
|
597
|
+
this.scopeRing = options.scopeSharedBuffer
|
|
598
|
+
? scopeRingFromSharedBuffer(
|
|
599
|
+
options.scopeSharedBuffer,
|
|
600
|
+
options.scopeRingCapacity,
|
|
601
|
+
options.scopeBands,
|
|
602
|
+
)
|
|
603
|
+
: undefined;
|
|
1210
604
|
this.engine = new RealtimeEngine(this.sampleRate, this.blockSize);
|
|
1211
605
|
// Allocate persistent WASM-heap scratch (worst case: channelCount channels x
|
|
1212
606
|
// blockSize frames) and acquire the per-channel heap views once.
|
|
@@ -1215,6 +609,13 @@ export class SonareRealtimeEngineWorkletProcessor {
|
|
|
1215
609
|
for (let ch = 0; ch < this.channelCount; ch++) {
|
|
1216
610
|
this.channelBuffers[ch] = this.engine.getChannelBuffer(ch, this.blockSize);
|
|
1217
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
|
+
}
|
|
1218
619
|
}
|
|
1219
620
|
|
|
1220
621
|
process(inputs: WorkletInput, outputs: WorkletOutput): boolean {
|
|
@@ -1292,6 +693,7 @@ export class SonareRealtimeEngineWorkletProcessor {
|
|
|
1292
693
|
}
|
|
1293
694
|
this.publishTelemetry();
|
|
1294
695
|
this.publishMeters();
|
|
696
|
+
this.publishScope();
|
|
1295
697
|
return true;
|
|
1296
698
|
}
|
|
1297
699
|
|
|
@@ -1318,8 +720,28 @@ export class SonareRealtimeEngineWorkletProcessor {
|
|
|
1318
720
|
}
|
|
1319
721
|
switch (message.type) {
|
|
1320
722
|
case 'syncClips':
|
|
723
|
+
this.liveClips.clear();
|
|
724
|
+
for (const clip of message.clips) {
|
|
725
|
+
if (clip.id !== undefined) {
|
|
726
|
+
this.liveClips.set(clip.id, clip);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
1321
729
|
this.engine.setClips(message.clips);
|
|
1322
730
|
break;
|
|
731
|
+
case 'syncClipsDelta':
|
|
732
|
+
for (const clipId of message.removeIds) {
|
|
733
|
+
this.liveClips.delete(clipId);
|
|
734
|
+
}
|
|
735
|
+
for (const clip of message.upserts) {
|
|
736
|
+
if (clip.id !== undefined) {
|
|
737
|
+
this.liveClips.set(clip.id, clip);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
this.engine.setClips(Array.from(this.liveClips.values()));
|
|
741
|
+
break;
|
|
742
|
+
case 'syncMidiClips':
|
|
743
|
+
this.engine.setMidiClips(message.clips);
|
|
744
|
+
break;
|
|
1323
745
|
case 'syncMarkers':
|
|
1324
746
|
this.engine.setMarkers(message.markers);
|
|
1325
747
|
break;
|
|
@@ -1330,6 +752,219 @@ export class SonareRealtimeEngineWorkletProcessor {
|
|
|
1330
752
|
case 'syncAutomation':
|
|
1331
753
|
this.engine.setAutomationLane(message.paramId, message.points);
|
|
1332
754
|
break;
|
|
755
|
+
case 'syncTempo':
|
|
756
|
+
if (message.tempoSegments) {
|
|
757
|
+
this.engine.setTempoSegments(message.tempoSegments);
|
|
758
|
+
} else {
|
|
759
|
+
this.engine.setTempo(message.bpm);
|
|
760
|
+
}
|
|
761
|
+
if (message.timeSignatureSegments) {
|
|
762
|
+
this.engine.setTimeSignatureSegments(message.timeSignatureSegments);
|
|
763
|
+
} else {
|
|
764
|
+
this.engine.setTimeSignature(
|
|
765
|
+
message.timeSignature.numerator,
|
|
766
|
+
message.timeSignature.denominator,
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
break;
|
|
770
|
+
case 'syncMixer':
|
|
771
|
+
if (message.buses) {
|
|
772
|
+
this.engine.setTrackBuses(message.buses);
|
|
773
|
+
}
|
|
774
|
+
this.engine.setTrackLanes(message.lanes);
|
|
775
|
+
for (const strip of message.trackStrips ?? []) {
|
|
776
|
+
this.engine.setTrackStripJson(strip.trackId, strip.sceneJson);
|
|
777
|
+
}
|
|
778
|
+
for (const strip of message.busStrips ?? []) {
|
|
779
|
+
this.engine.setBusStripJson(strip.busId, strip.sceneJson);
|
|
780
|
+
}
|
|
781
|
+
if (message.masterStripJson) {
|
|
782
|
+
this.engine.setMasterStripJson(message.masterStripJson);
|
|
783
|
+
}
|
|
784
|
+
for (const binding of message.laneSidechains ?? []) {
|
|
785
|
+
this.engine.setLaneSidechain(binding.trackId, binding.insertIndex, binding.sourceTrackId);
|
|
786
|
+
}
|
|
787
|
+
break;
|
|
788
|
+
case 'syncCapture':
|
|
789
|
+
this.engine.setCaptureBuffer(message.channels, message.bufferFrames);
|
|
790
|
+
this.engine.setCaptureSource(message.source);
|
|
791
|
+
this.engine.setRecordOffsetSamples(message.recordOffsetSamples);
|
|
792
|
+
this.engine.setInputMonitor(message.inputMonitor.enabled, message.inputMonitor.gain);
|
|
793
|
+
break;
|
|
794
|
+
case 'syncTrackStripEqBand':
|
|
795
|
+
this.engine.setTrackStripEqBandJson(message.trackId, message.bandIndex, message.bandJson);
|
|
796
|
+
break;
|
|
797
|
+
case 'syncMasterStripEqBand':
|
|
798
|
+
this.engine.setMasterStripEqBandJson(message.bandIndex, message.bandJson);
|
|
799
|
+
break;
|
|
800
|
+
case 'syncTrackStripInsertBypassed':
|
|
801
|
+
this.engine.setTrackStripInsertBypassed(
|
|
802
|
+
message.trackId,
|
|
803
|
+
message.insertIndex,
|
|
804
|
+
message.bypassed,
|
|
805
|
+
message.resetOnBypass,
|
|
806
|
+
);
|
|
807
|
+
break;
|
|
808
|
+
case 'syncMasterStripInsertBypassed':
|
|
809
|
+
this.engine.setMasterStripInsertBypassed(
|
|
810
|
+
message.insertIndex,
|
|
811
|
+
message.bypassed,
|
|
812
|
+
message.resetOnBypass,
|
|
813
|
+
);
|
|
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;
|
|
845
|
+
case 'syncBuiltinInstrument':
|
|
846
|
+
this.engine.setBuiltinInstrument(message.config, message.destinationId);
|
|
847
|
+
break;
|
|
848
|
+
case 'syncSynthInstrument':
|
|
849
|
+
this.engine.setSynthInstrument(message.patch, message.destinationId);
|
|
850
|
+
break;
|
|
851
|
+
case 'syncLoadSoundFont':
|
|
852
|
+
this.engine.loadSoundFont(message.data);
|
|
853
|
+
break;
|
|
854
|
+
case 'syncSf2Instrument':
|
|
855
|
+
this.engine.setSf2Instrument(message.config, message.destinationId);
|
|
856
|
+
break;
|
|
857
|
+
case 'syncMidiNoteOn':
|
|
858
|
+
this.engine.pushMidiNoteOn(
|
|
859
|
+
message.destinationId,
|
|
860
|
+
message.group,
|
|
861
|
+
message.channel,
|
|
862
|
+
message.note,
|
|
863
|
+
message.velocity,
|
|
864
|
+
message.renderFrame,
|
|
865
|
+
);
|
|
866
|
+
break;
|
|
867
|
+
case 'syncMidiNoteOff':
|
|
868
|
+
this.engine.pushMidiNoteOff(
|
|
869
|
+
message.destinationId,
|
|
870
|
+
message.group,
|
|
871
|
+
message.channel,
|
|
872
|
+
message.note,
|
|
873
|
+
message.velocity,
|
|
874
|
+
message.renderFrame,
|
|
875
|
+
);
|
|
876
|
+
break;
|
|
877
|
+
case 'syncMidiCc':
|
|
878
|
+
this.engine.pushMidiCc(
|
|
879
|
+
message.destinationId,
|
|
880
|
+
message.group,
|
|
881
|
+
message.channel,
|
|
882
|
+
message.controller,
|
|
883
|
+
message.value,
|
|
884
|
+
message.renderFrame,
|
|
885
|
+
);
|
|
886
|
+
break;
|
|
887
|
+
case 'syncMidiPanic':
|
|
888
|
+
this.engine.pushMidiPanic(message.renderFrame);
|
|
889
|
+
break;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
receiveCaptureRequest(message: SonareEngineCaptureRequestMessage): void {
|
|
894
|
+
if (this.closed) {
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
try {
|
|
898
|
+
if (message.op === 'status') {
|
|
899
|
+
const status = this.engine.captureStatus();
|
|
900
|
+
this.transport?.postMessage?.({
|
|
901
|
+
type: 'captureResponse',
|
|
902
|
+
requestId: message.requestId,
|
|
903
|
+
ok: true,
|
|
904
|
+
status: {
|
|
905
|
+
capturedFrames: status.capturedFrames,
|
|
906
|
+
overflowCount: status.overflowCount,
|
|
907
|
+
armed: status.armed,
|
|
908
|
+
punchEnabled: status.punchEnabled,
|
|
909
|
+
source: status.source,
|
|
910
|
+
recordOffsetSamples: status.recordOffsetSamples,
|
|
911
|
+
},
|
|
912
|
+
} satisfies SonareEngineCaptureResponseMessage);
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
if (message.op === 'read') {
|
|
916
|
+
const captured = this.engine.capturedAudio();
|
|
917
|
+
const channels: number[][] = [];
|
|
918
|
+
for (let ch = 0; ch < captured.length; ch++) {
|
|
919
|
+
const source = captured[ch];
|
|
920
|
+
const copy: number[] = [];
|
|
921
|
+
for (let i = 0; i < source.length; i++) {
|
|
922
|
+
copy.push(Number(source[i]));
|
|
923
|
+
}
|
|
924
|
+
channels.push(copy);
|
|
925
|
+
}
|
|
926
|
+
this.transport?.postMessage?.({
|
|
927
|
+
type: 'captureResponse',
|
|
928
|
+
requestId: message.requestId,
|
|
929
|
+
ok: true,
|
|
930
|
+
channels,
|
|
931
|
+
} satisfies SonareEngineCaptureResponseMessage);
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
this.engine.resetCapture();
|
|
935
|
+
this.transport?.postMessage?.({
|
|
936
|
+
type: 'captureResponse',
|
|
937
|
+
requestId: message.requestId,
|
|
938
|
+
ok: true,
|
|
939
|
+
} satisfies SonareEngineCaptureResponseMessage);
|
|
940
|
+
} catch (error) {
|
|
941
|
+
this.transport?.postMessage?.({
|
|
942
|
+
type: 'captureResponse',
|
|
943
|
+
requestId: message.requestId,
|
|
944
|
+
ok: false,
|
|
945
|
+
error: error instanceof Error ? error.message : String(error),
|
|
946
|
+
} satisfies SonareEngineCaptureResponseMessage);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
receiveTransportRequest(message: SonareEngineTransportRequestMessage): void {
|
|
951
|
+
if (this.closed) {
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
try {
|
|
955
|
+
this.transport?.postMessage?.({
|
|
956
|
+
type: 'transportResponse',
|
|
957
|
+
requestId: message.requestId,
|
|
958
|
+
ok: true,
|
|
959
|
+
state: this.engine.getTransportState(),
|
|
960
|
+
} satisfies SonareEngineTransportResponseMessage);
|
|
961
|
+
} catch (error) {
|
|
962
|
+
this.transport?.postMessage?.({
|
|
963
|
+
type: 'transportResponse',
|
|
964
|
+
requestId: message.requestId,
|
|
965
|
+
ok: false,
|
|
966
|
+
error: error instanceof Error ? error.message : String(error),
|
|
967
|
+
} satisfies SonareEngineTransportResponseMessage);
|
|
1333
968
|
}
|
|
1334
969
|
}
|
|
1335
970
|
|
|
@@ -1423,6 +1058,14 @@ export class SonareRealtimeEngineWorkletProcessor {
|
|
|
1423
1058
|
// (RtPublisher-style swap), so a queued kSeekMarker resolves correctly.
|
|
1424
1059
|
this.engine.seekMarker(Math.trunc(Number(command.targetId ?? 0)), sampleTime);
|
|
1425
1060
|
break;
|
|
1061
|
+
case SonareEngineCommandType.SetSoloMute:
|
|
1062
|
+
this.engine.setSoloMute(
|
|
1063
|
+
Math.trunc(Number(command.targetId ?? 0)),
|
|
1064
|
+
Boolean((Number(command.argInt ?? 0) & 0x2) !== 0),
|
|
1065
|
+
Boolean((Number(command.argInt ?? 0) & 0x1) !== 0),
|
|
1066
|
+
sampleTime,
|
|
1067
|
+
);
|
|
1068
|
+
break;
|
|
1426
1069
|
default:
|
|
1427
1070
|
this.publishTelemetryRecord({
|
|
1428
1071
|
type: SonareEngineTelemetryType.Error,
|
|
@@ -1451,16 +1094,32 @@ export class SonareRealtimeEngineWorkletProcessor {
|
|
|
1451
1094
|
this.transport?.postMessage?.(record);
|
|
1452
1095
|
}
|
|
1453
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).
|
|
1454
1108
|
private publishMeters(): void {
|
|
1455
1109
|
if (this.meterIntervalFrames <= 0 || (!this.transport && !this.meterRing)) {
|
|
1456
1110
|
return;
|
|
1457
1111
|
}
|
|
1458
1112
|
for (const item of this.engine.drainMeterTelemetry(64)) {
|
|
1459
1113
|
const meter = meterFromEngine(item);
|
|
1460
|
-
if (
|
|
1114
|
+
if (
|
|
1115
|
+
meter.frame !== this.lastMeterFrame &&
|
|
1116
|
+
meter.frame - this.lastMeterFrame < this.meterIntervalFrames
|
|
1117
|
+
) {
|
|
1461
1118
|
continue;
|
|
1462
1119
|
}
|
|
1463
|
-
this.lastMeterFrame
|
|
1120
|
+
if (meter.frame !== this.lastMeterFrame) {
|
|
1121
|
+
this.lastMeterFrame = meter.frame;
|
|
1122
|
+
}
|
|
1464
1123
|
// Prefer the lock-free SAB meter ring (matching the telemetry path and
|
|
1465
1124
|
// SonareWorkletProcessor); only fall back to structured-clone postMessage
|
|
1466
1125
|
// when no ring was provided, so we do not allocate/post from the audio
|
|
@@ -1482,12 +1141,19 @@ export class SonareRealtimeEngineWorkletProcessor {
|
|
|
1482
1141
|
const writeIndex = Atomics.load(ring.header, 0);
|
|
1483
1142
|
const offset = (writeIndex % ring.capacity) * SONARE_METER_RING_RECORD_FLOATS;
|
|
1484
1143
|
ring.records[offset] = encodeFrameLo(meter.frame);
|
|
1485
|
-
ring.records[offset + 1] = meter.
|
|
1486
|
-
ring.records[offset + 2] = meter.
|
|
1487
|
-
ring.records[offset + 3] = meter.
|
|
1488
|
-
ring.records[offset + 4] = meter.
|
|
1489
|
-
ring.records[offset + 5] = meter.
|
|
1490
|
-
ring.records[offset + 6] =
|
|
1144
|
+
ring.records[offset + 1] = encodeFrameHi(meter.frame);
|
|
1145
|
+
ring.records[offset + 2] = meter.targetId;
|
|
1146
|
+
ring.records[offset + 3] = meter.peakDbL;
|
|
1147
|
+
ring.records[offset + 4] = meter.peakDbR;
|
|
1148
|
+
ring.records[offset + 5] = meter.rmsDbL;
|
|
1149
|
+
ring.records[offset + 6] = meter.rmsDbR;
|
|
1150
|
+
ring.records[offset + 7] = meter.correlation;
|
|
1151
|
+
ring.records[offset + 8] = meter.truePeakDbL;
|
|
1152
|
+
ring.records[offset + 9] = meter.truePeakDbR;
|
|
1153
|
+
ring.records[offset + 10] = meter.momentaryLufs;
|
|
1154
|
+
ring.records[offset + 11] = meter.shortTermLufs;
|
|
1155
|
+
ring.records[offset + 12] = meter.integratedLufs;
|
|
1156
|
+
ring.records[offset + 13] = meter.gainReductionDb;
|
|
1491
1157
|
Atomics.store(ring.header, 0, writeIndex + 1);
|
|
1492
1158
|
// writeIndex is a free-running monotonic counter, so an overflow guard here
|
|
1493
1159
|
// would fire on essentially every write past the first `capacity` records
|
|
@@ -1496,16 +1162,56 @@ export class SonareRealtimeEngineWorkletProcessor {
|
|
|
1496
1162
|
// writeIndex - capacity), so header slot 3 is left at its initial 0.
|
|
1497
1163
|
}
|
|
1498
1164
|
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
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
|
+
|
|
1205
|
+
private commandRingFromSharedBuffer(
|
|
1206
|
+
sharedBuffer: SharedArrayBuffer,
|
|
1207
|
+
fallbackCapacity?: number,
|
|
1208
|
+
): SonareEngineCommandRingBuffer {
|
|
1209
|
+
const ring = engineRingFromSharedBuffer(
|
|
1210
|
+
sharedBuffer,
|
|
1211
|
+
SONARE_ENGINE_COMMAND_RECORD_BYTES,
|
|
1212
|
+
fallbackCapacity,
|
|
1213
|
+
);
|
|
1214
|
+
return { sharedBuffer, header: ring.header, view: ring.view, capacity: ring.capacity };
|
|
1509
1215
|
}
|
|
1510
1216
|
|
|
1511
1217
|
private telemetryRingFromSharedBuffer(
|
|
@@ -1657,12 +1363,32 @@ export class SonareRtRealtimeEngineRuntime {
|
|
|
1657
1363
|
this.metronomeConfig.clickSamples,
|
|
1658
1364
|
);
|
|
1659
1365
|
break;
|
|
1366
|
+
case 'syncTempo':
|
|
1367
|
+
this.module._sonare_rt_engine_set_tempo(this.engine, message.bpm);
|
|
1368
|
+
break;
|
|
1660
1369
|
case 'syncClips':
|
|
1370
|
+
case 'syncClipsDelta':
|
|
1371
|
+
case 'syncMidiClips':
|
|
1661
1372
|
case 'syncMarkers':
|
|
1662
1373
|
case 'syncAutomation':
|
|
1374
|
+
case 'syncMixer':
|
|
1375
|
+
case 'syncCapture':
|
|
1376
|
+
case 'syncTrackStripEqBand':
|
|
1377
|
+
case 'syncMasterStripEqBand':
|
|
1378
|
+
case 'syncTrackStripInsertBypassed':
|
|
1379
|
+
case 'syncMasterStripInsertBypassed':
|
|
1380
|
+
case 'syncBuiltinInstrument':
|
|
1381
|
+
case 'syncSynthInstrument':
|
|
1382
|
+
case 'syncSf2Instrument':
|
|
1383
|
+
case 'syncLoadSoundFont':
|
|
1384
|
+
case 'syncMidiNoteOn':
|
|
1385
|
+
case 'syncMidiNoteOff':
|
|
1386
|
+
case 'syncMidiCc':
|
|
1387
|
+
case 'syncMidiPanic':
|
|
1663
1388
|
// The sonare-rt C ABI exposes no set_clips / set_markers /
|
|
1664
|
-
// set_automation_lane, so these mutations cannot
|
|
1665
|
-
// engine. Surface a clear telemetry error rather
|
|
1389
|
+
// set_automation_lane / set_track_lanes, so these mutations cannot
|
|
1390
|
+
// reach a live sonare-rt engine. Surface a clear telemetry error rather
|
|
1391
|
+
// than silently dropping.
|
|
1666
1392
|
if (this.telemetryRing) {
|
|
1667
1393
|
writeSonareEngineTelemetryRingBuffer(this.telemetryRing, {
|
|
1668
1394
|
type: SonareEngineTelemetryType.Error,
|
|
@@ -1678,6 +1404,30 @@ export class SonareRtRealtimeEngineRuntime {
|
|
|
1678
1404
|
}
|
|
1679
1405
|
}
|
|
1680
1406
|
|
|
1407
|
+
receiveCaptureRequest(message: SonareEngineCaptureRequestMessage, port?: WorkletPort): void {
|
|
1408
|
+
if (this.closed) {
|
|
1409
|
+
return;
|
|
1410
|
+
}
|
|
1411
|
+
port?.postMessage?.({
|
|
1412
|
+
type: 'captureResponse',
|
|
1413
|
+
requestId: message.requestId,
|
|
1414
|
+
ok: false,
|
|
1415
|
+
error: 'Capture read-back is not supported by the sonare-rt runtime.',
|
|
1416
|
+
} satisfies SonareEngineCaptureResponseMessage);
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
receiveTransportRequest(message: SonareEngineTransportRequestMessage, port?: WorkletPort): void {
|
|
1420
|
+
if (this.closed) {
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
port?.postMessage?.({
|
|
1424
|
+
type: 'transportResponse',
|
|
1425
|
+
requestId: message.requestId,
|
|
1426
|
+
ok: false,
|
|
1427
|
+
error: 'Transport state read-back is not supported by the sonare-rt runtime.',
|
|
1428
|
+
} satisfies SonareEngineTransportResponseMessage);
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1681
1431
|
destroy(): void {
|
|
1682
1432
|
if (this.closed) {
|
|
1683
1433
|
return;
|
|
@@ -1875,11 +1625,30 @@ export class SonareRealtimeEngineNode {
|
|
|
1875
1625
|
readonly commandRing?: SonareEngineCommandRingBuffer;
|
|
1876
1626
|
readonly telemetryRing?: SonareEngineTelemetryRingBuffer;
|
|
1877
1627
|
readonly meterRing?: SonareMeterRingBuffer;
|
|
1628
|
+
readonly scopeRing?: SonareScopeRingBuffer;
|
|
1878
1629
|
readonly ready: Promise<void>;
|
|
1879
1630
|
private telemetryReadIndex = 0;
|
|
1880
1631
|
private meterReadIndex = 0;
|
|
1632
|
+
private scopeReadIndex = 0;
|
|
1881
1633
|
private telemetryListeners = new Set<(telemetry: SonareEngineTelemetryRecord) => void>();
|
|
1882
1634
|
private meterListeners = new Set<(meter: SonareWorkletMeterSnapshot) => void>();
|
|
1635
|
+
private scopeListeners = new Set<(scope: SonareWorkletScopeSnapshot) => void>();
|
|
1636
|
+
private captureRequestId = 1;
|
|
1637
|
+
private readonly captureRequests = new Map<
|
|
1638
|
+
number,
|
|
1639
|
+
{
|
|
1640
|
+
resolve: (response: SonareEngineCaptureResponseMessage) => void;
|
|
1641
|
+
reject: (reason?: unknown) => void;
|
|
1642
|
+
}
|
|
1643
|
+
>();
|
|
1644
|
+
private transportRequestId = 1;
|
|
1645
|
+
private readonly transportRequests = new Map<
|
|
1646
|
+
number,
|
|
1647
|
+
{
|
|
1648
|
+
resolve: (response: SonareEngineTransportResponseMessage) => void;
|
|
1649
|
+
reject: (reason?: unknown) => void;
|
|
1650
|
+
}
|
|
1651
|
+
>();
|
|
1883
1652
|
private resolveReady!: () => void;
|
|
1884
1653
|
private rejectReady!: (reason?: unknown) => void;
|
|
1885
1654
|
private destroyed = false;
|
|
@@ -1890,21 +1659,43 @@ export class SonareRealtimeEngineNode {
|
|
|
1890
1659
|
commandRing?: SonareEngineCommandRingBuffer,
|
|
1891
1660
|
telemetryRing?: SonareEngineTelemetryRingBuffer,
|
|
1892
1661
|
meterRing?: SonareMeterRingBuffer,
|
|
1662
|
+
scopeRing?: SonareScopeRingBuffer,
|
|
1893
1663
|
) {
|
|
1894
1664
|
this.node = node;
|
|
1895
1665
|
this.capabilities = capabilities;
|
|
1896
1666
|
this.commandRing = commandRing;
|
|
1897
1667
|
this.telemetryRing = telemetryRing;
|
|
1898
1668
|
this.meterRing = meterRing;
|
|
1669
|
+
this.scopeRing = scopeRing;
|
|
1899
1670
|
this.ready = new Promise((resolve, reject) => {
|
|
1900
1671
|
this.resolveReady = resolve;
|
|
1901
1672
|
this.rejectReady = reject;
|
|
1902
1673
|
});
|
|
1903
|
-
if (capabilities.
|
|
1674
|
+
if (!capabilities.readyMessage) {
|
|
1904
1675
|
this.resolveReady();
|
|
1905
1676
|
}
|
|
1906
1677
|
this.node.port.onmessage = (event: MessageEvent<unknown>) => {
|
|
1907
|
-
if (
|
|
1678
|
+
if (isEngineCaptureResponseMessage(event.data)) {
|
|
1679
|
+
const pending = this.captureRequests.get(event.data.requestId);
|
|
1680
|
+
if (pending) {
|
|
1681
|
+
this.captureRequests.delete(event.data.requestId);
|
|
1682
|
+
if (event.data.ok) {
|
|
1683
|
+
pending.resolve(event.data);
|
|
1684
|
+
} else {
|
|
1685
|
+
pending.reject(new Error(event.data.error ?? 'Capture request failed'));
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
} else if (isEngineTransportResponseMessage(event.data)) {
|
|
1689
|
+
const pending = this.transportRequests.get(event.data.requestId);
|
|
1690
|
+
if (pending) {
|
|
1691
|
+
this.transportRequests.delete(event.data.requestId);
|
|
1692
|
+
if (event.data.ok) {
|
|
1693
|
+
pending.resolve(event.data);
|
|
1694
|
+
} else {
|
|
1695
|
+
pending.reject(new Error(event.data.error ?? 'Transport request failed'));
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
} else if (isEngineTelemetryRecord(event.data)) {
|
|
1908
1699
|
this.emitTelemetry(event.data);
|
|
1909
1700
|
} else if (isMeterSnapshot(event.data)) {
|
|
1910
1701
|
this.emitMeter(event.data);
|
|
@@ -1974,6 +1765,14 @@ export class SonareRealtimeEngineNode {
|
|
|
1974
1765
|
mode === 'sab' && runtimeTarget === 'embind'
|
|
1975
1766
|
? createSonareMeterRingBuffer(options.meterRingCapacity ?? 128)
|
|
1976
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;
|
|
1977
1776
|
const channelCount = Math.max(1, Math.floor(options.channelCount ?? 2));
|
|
1978
1777
|
const processorOptions: SonareRealtimeEngineWorkletProcessorOptions = {
|
|
1979
1778
|
runtimeTarget,
|
|
@@ -1988,6 +1787,13 @@ export class SonareRealtimeEngineNode {
|
|
|
1988
1787
|
telemetryRingCapacity: telemetryRing?.capacity,
|
|
1989
1788
|
meterSharedBuffer: meterRing?.sharedBuffer,
|
|
1990
1789
|
meterRingCapacity: meterRing?.capacity,
|
|
1790
|
+
scopeSharedBuffer: scopeRing?.sharedBuffer,
|
|
1791
|
+
scopeRingCapacity: scopeRing?.capacity,
|
|
1792
|
+
scopeBands: scopeRing?.bands,
|
|
1793
|
+
scopeIntervalFrames: scopeRing ? scopeIntervalFrames : undefined,
|
|
1794
|
+
wasmBinary: options.wasmBinary,
|
|
1795
|
+
initialSyncMessages: options.initialSyncMessages,
|
|
1796
|
+
initialCommands: options.initialCommands,
|
|
1991
1797
|
};
|
|
1992
1798
|
const factory =
|
|
1993
1799
|
options.nodeFactory ??
|
|
@@ -2011,10 +1817,14 @@ export class SonareRealtimeEngineNode {
|
|
|
2011
1817
|
expectedEngineAbiVersion: detectedCapabilities?.expectedEngineAbiVersion,
|
|
2012
1818
|
abiCompatible: detectedCapabilities?.abiCompatible,
|
|
2013
1819
|
degradedReason,
|
|
1820
|
+
readyMessage:
|
|
1821
|
+
runtimeTarget === 'sonare-rt' ||
|
|
1822
|
+
(runtimeTarget === 'embind' && moduleUrl !== undefined && !options.nodeFactory),
|
|
2014
1823
|
},
|
|
2015
1824
|
commandRing,
|
|
2016
1825
|
telemetryRing,
|
|
2017
1826
|
meterRing,
|
|
1827
|
+
scopeRing,
|
|
2018
1828
|
);
|
|
2019
1829
|
}
|
|
2020
1830
|
|
|
@@ -2053,6 +1863,36 @@ export class SonareRealtimeEngineNode {
|
|
|
2053
1863
|
return true;
|
|
2054
1864
|
}
|
|
2055
1865
|
|
|
1866
|
+
requestCaptureStatus(): Promise<EngineCaptureStatus> {
|
|
1867
|
+
return this.sendCaptureRequest('status').then((response) => {
|
|
1868
|
+
if (!response.status) {
|
|
1869
|
+
throw new Error('Capture status response is missing status.');
|
|
1870
|
+
}
|
|
1871
|
+
return response.status;
|
|
1872
|
+
});
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
requestCapturedAudio(): Promise<Float32Array[]> {
|
|
1876
|
+
return this.sendCaptureRequest('read').then((response) =>
|
|
1877
|
+
(response.channels ?? []).map((channel) =>
|
|
1878
|
+
channel instanceof Float32Array ? channel : new Float32Array(channel),
|
|
1879
|
+
),
|
|
1880
|
+
);
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
requestCaptureReset(): Promise<void> {
|
|
1884
|
+
return this.sendCaptureRequest('reset').then(() => undefined);
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
requestTransportState(): Promise<EngineTransportState> {
|
|
1888
|
+
return this.sendTransportRequest().then((response) => {
|
|
1889
|
+
if (!response.state) {
|
|
1890
|
+
throw new Error('Transport state response is missing state.');
|
|
1891
|
+
}
|
|
1892
|
+
return response.state;
|
|
1893
|
+
});
|
|
1894
|
+
}
|
|
1895
|
+
|
|
2056
1896
|
pollTelemetry(): SonareEngineTelemetryRecord[] {
|
|
2057
1897
|
if (!this.telemetryRing) {
|
|
2058
1898
|
return [];
|
|
@@ -2080,6 +1920,21 @@ export class SonareRealtimeEngineNode {
|
|
|
2080
1920
|
return read.meters;
|
|
2081
1921
|
}
|
|
2082
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
|
+
|
|
2083
1938
|
onTelemetry(callback: (telemetry: SonareEngineTelemetryRecord) => void): () => void {
|
|
2084
1939
|
this.telemetryListeners.add(callback);
|
|
2085
1940
|
return () => {
|
|
@@ -2094,6 +1949,13 @@ export class SonareRealtimeEngineNode {
|
|
|
2094
1949
|
};
|
|
2095
1950
|
}
|
|
2096
1951
|
|
|
1952
|
+
onScope(callback: (scope: SonareWorkletScopeSnapshot) => void): () => void {
|
|
1953
|
+
this.scopeListeners.add(callback);
|
|
1954
|
+
return () => {
|
|
1955
|
+
this.scopeListeners.delete(callback);
|
|
1956
|
+
};
|
|
1957
|
+
}
|
|
1958
|
+
|
|
2097
1959
|
destroy(): void {
|
|
2098
1960
|
if (this.destroyed) {
|
|
2099
1961
|
return;
|
|
@@ -2101,8 +1963,17 @@ export class SonareRealtimeEngineNode {
|
|
|
2101
1963
|
this.destroyed = true;
|
|
2102
1964
|
this.node.port.postMessage({ type: SonareEngineCommandType.TransportStop, sampleTime: -1 });
|
|
2103
1965
|
this.node.disconnect();
|
|
1966
|
+
for (const pending of this.captureRequests.values()) {
|
|
1967
|
+
pending.reject(new Error('Realtime engine node is destroyed.'));
|
|
1968
|
+
}
|
|
1969
|
+
this.captureRequests.clear();
|
|
1970
|
+
for (const pending of this.transportRequests.values()) {
|
|
1971
|
+
pending.reject(new Error('Realtime engine node is destroyed.'));
|
|
1972
|
+
}
|
|
1973
|
+
this.transportRequests.clear();
|
|
2104
1974
|
this.telemetryListeners.clear();
|
|
2105
1975
|
this.meterListeners.clear();
|
|
1976
|
+
this.scopeListeners.clear();
|
|
2106
1977
|
}
|
|
2107
1978
|
|
|
2108
1979
|
private emitTelemetry(telemetry: SonareEngineTelemetryRecord): void {
|
|
@@ -2116,6 +1987,38 @@ export class SonareRealtimeEngineNode {
|
|
|
2116
1987
|
listener(meter);
|
|
2117
1988
|
}
|
|
2118
1989
|
}
|
|
1990
|
+
|
|
1991
|
+
private emitScope(scope: SonareWorkletScopeSnapshot): void {
|
|
1992
|
+
for (const listener of this.scopeListeners) {
|
|
1993
|
+
listener(scope);
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
private sendCaptureRequest(
|
|
1998
|
+
op: SonareEngineCaptureRequestMessage['op'],
|
|
1999
|
+
): Promise<SonareEngineCaptureResponseMessage> {
|
|
2000
|
+
if (this.destroyed) {
|
|
2001
|
+
return Promise.reject(new Error('Realtime engine node is destroyed.'));
|
|
2002
|
+
}
|
|
2003
|
+
const requestId = this.captureRequestId++;
|
|
2004
|
+
const promise = new Promise<SonareEngineCaptureResponseMessage>((resolve, reject) => {
|
|
2005
|
+
this.captureRequests.set(requestId, { resolve, reject });
|
|
2006
|
+
});
|
|
2007
|
+
this.node.port.postMessage({ type: 'captureRequest', requestId, op });
|
|
2008
|
+
return promise;
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
private sendTransportRequest(): Promise<SonareEngineTransportResponseMessage> {
|
|
2012
|
+
if (this.destroyed) {
|
|
2013
|
+
return Promise.reject(new Error('Realtime engine node is destroyed.'));
|
|
2014
|
+
}
|
|
2015
|
+
const requestId = this.transportRequestId++;
|
|
2016
|
+
const promise = new Promise<SonareEngineTransportResponseMessage>((resolve, reject) => {
|
|
2017
|
+
this.transportRequests.set(requestId, { resolve, reject });
|
|
2018
|
+
});
|
|
2019
|
+
this.node.port.postMessage({ type: 'transportRequest', requestId, op: 'state' });
|
|
2020
|
+
return promise;
|
|
2021
|
+
}
|
|
2119
2022
|
}
|
|
2120
2023
|
|
|
2121
2024
|
export class SonareEngine {
|
|
@@ -2130,9 +2033,31 @@ export class SonareEngine {
|
|
|
2130
2033
|
private readonly offlineChannelCount: number;
|
|
2131
2034
|
private readonly automationLanes = new Map<number, EngineAutomationPoint[]>();
|
|
2132
2035
|
private readonly clips = new Map<number, EngineClip>();
|
|
2036
|
+
private readonly midiClips = new Map<number, EngineMidiClipSchedule>();
|
|
2133
2037
|
private readonly markers = new Map<number, EngineMarker>();
|
|
2038
|
+
private readonly trackLaneIds: number[] = [];
|
|
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
|
+
>();
|
|
2045
|
+
private readonly buses: EngineBus[] = [];
|
|
2046
|
+
private readonly trackStripJson = new Map<number, string>();
|
|
2047
|
+
private readonly busStripJson = new Map<number, string>();
|
|
2048
|
+
private masterStripJson: string | undefined;
|
|
2049
|
+
private captureConfig: Omit<SonareEngineSyncCaptureMessage, 'type'> | undefined;
|
|
2050
|
+
private tempoBpm = 120;
|
|
2051
|
+
private timeSignature = { numerator: 4, denominator: 4 };
|
|
2052
|
+
private tempoSegments: EngineTempoSegment[] = [{ startPpq: 0, bpm: 120 }];
|
|
2053
|
+
private timeSignatureSegments: EngineTimeSignatureSegment[] = [
|
|
2054
|
+
{ startPpq: 0, numerator: 4, denominator: 4 },
|
|
2055
|
+
];
|
|
2056
|
+
private latestTransportState: EngineTransportState | undefined;
|
|
2134
2057
|
private nextClipId = 1;
|
|
2135
2058
|
private nextMarkerId = 1;
|
|
2059
|
+
private transportPlaying = false;
|
|
2060
|
+
private readonly pendingInstrumentSync: SonareEngineInstrumentSyncMessage[] = [];
|
|
2136
2061
|
private destroyed = false;
|
|
2137
2062
|
|
|
2138
2063
|
private constructor(
|
|
@@ -2152,8 +2077,21 @@ export class SonareEngine {
|
|
|
2152
2077
|
this.offlineBlockSize = offlineBlockSize;
|
|
2153
2078
|
this.offlineChannelCount = offlineChannelCount;
|
|
2154
2079
|
this.transport = {
|
|
2155
|
-
play: (sampleTime = -1) =>
|
|
2156
|
-
|
|
2080
|
+
play: (sampleTime = -1) => {
|
|
2081
|
+
const ok = this.realtimeNode.play(sampleTime);
|
|
2082
|
+
if (ok) {
|
|
2083
|
+
this.transportPlaying = true;
|
|
2084
|
+
}
|
|
2085
|
+
return ok;
|
|
2086
|
+
},
|
|
2087
|
+
stop: (sampleTime = -1) => {
|
|
2088
|
+
const ok = this.realtimeNode.stop(sampleTime);
|
|
2089
|
+
if (ok) {
|
|
2090
|
+
this.transportPlaying = false;
|
|
2091
|
+
this.flushPendingInstrumentSync();
|
|
2092
|
+
}
|
|
2093
|
+
return ok;
|
|
2094
|
+
},
|
|
2157
2095
|
seekPpq: (ppq, sampleTime = -1) => {
|
|
2158
2096
|
this.offlineEngine.seekPpq(ppq, sampleTime);
|
|
2159
2097
|
return this.realtimeNode.seekPpq(ppq, sampleTime);
|
|
@@ -2164,6 +2102,7 @@ export class SonareEngine {
|
|
|
2164
2102
|
return this.realtimeNode.seekSample(timelineSample, sampleTime);
|
|
2165
2103
|
},
|
|
2166
2104
|
setTempo: (bpm) => this.setTempo(bpm),
|
|
2105
|
+
setTempoSegments: (segments) => this.setTempoSegments(segments),
|
|
2167
2106
|
setLoop: (startPpq, endPpq, enabled = true) => this.setLoop(startPpq, endPpq, enabled),
|
|
2168
2107
|
};
|
|
2169
2108
|
}
|
|
@@ -2205,7 +2144,10 @@ export class SonareEngine {
|
|
|
2205
2144
|
}
|
|
2206
2145
|
|
|
2207
2146
|
setTempo(bpm: number): void {
|
|
2147
|
+
this.tempoBpm = bpm;
|
|
2148
|
+
this.tempoSegments = [{ startPpq: 0, bpm }];
|
|
2208
2149
|
this.offlineEngine.setTempo(bpm);
|
|
2150
|
+
this.postTempoSync();
|
|
2209
2151
|
this.realtimeNode.sendCommand({
|
|
2210
2152
|
type: SonareEngineCommandType.SetTempoMap,
|
|
2211
2153
|
sampleTime: -1,
|
|
@@ -2213,6 +2155,30 @@ export class SonareEngine {
|
|
|
2213
2155
|
});
|
|
2214
2156
|
}
|
|
2215
2157
|
|
|
2158
|
+
setTempoSegments(segments: readonly EngineTempoSegment[]): void {
|
|
2159
|
+
this.tempoSegments = segments.map((segment) => ({ ...segment }));
|
|
2160
|
+
this.tempoBpm = this.tempoSegments[0]?.bpm ?? this.tempoBpm;
|
|
2161
|
+
this.offlineEngine.setTempoSegments(this.tempoSegments);
|
|
2162
|
+
this.postTempoSync();
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
setTimeSignature(numerator: number, denominator: number): void {
|
|
2166
|
+
this.timeSignature = { numerator, denominator };
|
|
2167
|
+
this.timeSignatureSegments = [{ startPpq: 0, numerator, denominator }];
|
|
2168
|
+
this.offlineEngine.setTimeSignature(numerator, denominator);
|
|
2169
|
+
this.postTempoSync();
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
setTimeSignatureSegments(segments: readonly EngineTimeSignatureSegment[]): void {
|
|
2173
|
+
this.timeSignatureSegments = segments.map((segment) => ({ ...segment }));
|
|
2174
|
+
const first = this.timeSignatureSegments[0];
|
|
2175
|
+
if (first) {
|
|
2176
|
+
this.timeSignature = { numerator: first.numerator, denominator: first.denominator };
|
|
2177
|
+
}
|
|
2178
|
+
this.offlineEngine.setTimeSignatureSegments(this.timeSignatureSegments);
|
|
2179
|
+
this.postTempoSync();
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2216
2182
|
setLoop(startPpq: number, endPpq: number, enabled = true): boolean {
|
|
2217
2183
|
this.offlineEngine.setLoop(startPpq, endPpq, enabled);
|
|
2218
2184
|
// Transport precision contract: the SAB command record carries exactly one
|
|
@@ -2233,6 +2199,20 @@ export class SonareEngine {
|
|
|
2233
2199
|
});
|
|
2234
2200
|
}
|
|
2235
2201
|
|
|
2202
|
+
countInEndSample(startSample: number, bars: number): number {
|
|
2203
|
+
return this.offlineEngine.countInEndSample(startSample, bars);
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
async getTransportState(): Promise<EngineTransportState> {
|
|
2207
|
+
const state = await this.realtimeNode.requestTransportState();
|
|
2208
|
+
this.latestTransportState = state;
|
|
2209
|
+
return state;
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
cachedTransportState(): EngineTransportState | undefined {
|
|
2213
|
+
return this.latestTransportState;
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2236
2216
|
setParam(nodeId: string, param: string | number, value: number): boolean {
|
|
2237
2217
|
const paramId = this.resolveParamId(nodeId, param);
|
|
2238
2218
|
// Mirror the change into the offline engine so a subsequent offline render
|
|
@@ -2276,6 +2256,68 @@ export class SonareEngine {
|
|
|
2276
2256
|
this.scheduleParam('', laneId, ppq, value, curve);
|
|
2277
2257
|
}
|
|
2278
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
|
+
|
|
2279
2321
|
listParameters(): EngineParameterInfo[] {
|
|
2280
2322
|
const parameters: EngineParameterInfo[] = [];
|
|
2281
2323
|
for (let index = 0; index < this.offlineEngine.parameterCount(); index++) {
|
|
@@ -2285,19 +2327,355 @@ export class SonareEngine {
|
|
|
2285
2327
|
}
|
|
2286
2328
|
|
|
2287
2329
|
setSoloMute(target: string | number, solo: boolean, mute: boolean): boolean {
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2330
|
+
const laneIndex = this.ensureTrackLane(target);
|
|
2331
|
+
this.offlineEngine.setSoloMute(laneIndex, solo, mute);
|
|
2332
|
+
return this.realtimeNode.sendCommand({
|
|
2333
|
+
type: SonareEngineCommandType.SetSoloMute,
|
|
2334
|
+
targetId: laneIndex,
|
|
2335
|
+
sampleTime: -1,
|
|
2336
|
+
argInt: (mute ? 0x1 : 0) | (solo ? 0x2 : 0),
|
|
2337
|
+
});
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
setStripGain(target: string | number, db: number): boolean {
|
|
2341
|
+
if (target === 'master') {
|
|
2342
|
+
const paramId = engineMixerMasterTarget(ENGINE_MIXER_PARAM_FADER_DB);
|
|
2343
|
+
this.offlineEngine.setParameter(paramId, db);
|
|
2344
|
+
return this.realtimeNode.sendCommand({
|
|
2345
|
+
type: SonareEngineCommandType.SetParamSmoothed,
|
|
2346
|
+
targetId: paramId,
|
|
2347
|
+
sampleTime: -1,
|
|
2348
|
+
argFloat: db,
|
|
2349
|
+
});
|
|
2350
|
+
}
|
|
2351
|
+
const laneIndex = this.ensureTrackLane(target);
|
|
2352
|
+
const paramId = engineMixerLaneTarget(laneIndex, ENGINE_MIXER_PARAM_FADER_DB);
|
|
2353
|
+
this.offlineEngine.setParameter(paramId, db);
|
|
2354
|
+
return this.realtimeNode.sendCommand({
|
|
2355
|
+
type: SonareEngineCommandType.SetParamSmoothed,
|
|
2356
|
+
targetId: paramId,
|
|
2357
|
+
sampleTime: -1,
|
|
2358
|
+
argFloat: db,
|
|
2359
|
+
});
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
setStripPan(target: string | number, pan: number): boolean {
|
|
2363
|
+
if (target === 'master') {
|
|
2364
|
+
const paramId = engineMixerMasterTarget(ENGINE_MIXER_PARAM_PAN);
|
|
2365
|
+
this.offlineEngine.setParameter(paramId, pan);
|
|
2366
|
+
return this.realtimeNode.sendCommand({
|
|
2367
|
+
type: SonareEngineCommandType.SetParamSmoothed,
|
|
2368
|
+
targetId: paramId,
|
|
2369
|
+
sampleTime: -1,
|
|
2370
|
+
argFloat: pan,
|
|
2371
|
+
});
|
|
2372
|
+
}
|
|
2373
|
+
const laneIndex = this.ensureTrackLane(target);
|
|
2374
|
+
const paramId = engineMixerLaneTarget(laneIndex, ENGINE_MIXER_PARAM_PAN);
|
|
2375
|
+
this.offlineEngine.setParameter(paramId, pan);
|
|
2376
|
+
return this.realtimeNode.sendCommand({
|
|
2377
|
+
type: SonareEngineCommandType.SetParamSmoothed,
|
|
2378
|
+
targetId: paramId,
|
|
2379
|
+
sampleTime: -1,
|
|
2380
|
+
argFloat: pan,
|
|
2381
|
+
});
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
/**
|
|
2385
|
+
* Declares the mixer track lanes in an explicit order.
|
|
2386
|
+
*
|
|
2387
|
+
* Lane indices are append-only: once a track id occupies a lane, its index
|
|
2388
|
+
* stays fixed for the engine's lifetime. The given list must therefore start
|
|
2389
|
+
* with the already-declared lane ids in their current order and may only
|
|
2390
|
+
* append new track ids after them. Entries carrying `sends` replace that
|
|
2391
|
+
* track's send list; entries without `sends` leave existing sends untouched.
|
|
2392
|
+
*
|
|
2393
|
+
* @param lanes Track ids or lane descriptors in the desired lane order.
|
|
2394
|
+
*/
|
|
2395
|
+
setTrackLanes(lanes: ReadonlyArray<number | EngineTrackLane>): void {
|
|
2396
|
+
const entries = lanes.map((lane) => (typeof lane === 'number' ? { trackId: lane } : lane));
|
|
2397
|
+
const ids: number[] = [];
|
|
2398
|
+
for (const entry of entries) {
|
|
2399
|
+
if (!Number.isInteger(entry.trackId) || entry.trackId <= 0) {
|
|
2400
|
+
throw new Error(`Invalid track id for mixer lane: ${String(entry.trackId)}`);
|
|
2401
|
+
}
|
|
2402
|
+
ids.push(entry.trackId);
|
|
2403
|
+
}
|
|
2404
|
+
if (new Set(ids).size !== ids.length) {
|
|
2405
|
+
throw new Error('Duplicate track id in mixer lane list');
|
|
2406
|
+
}
|
|
2407
|
+
for (let index = 0; index < this.trackLaneIds.length; index++) {
|
|
2408
|
+
if (ids[index] !== this.trackLaneIds[index]) {
|
|
2409
|
+
throw new Error(
|
|
2410
|
+
'Mixer lanes are append-only: keep existing lanes in order and only append new track ids',
|
|
2411
|
+
);
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
for (const entry of entries) {
|
|
2415
|
+
if (entry.sends) {
|
|
2416
|
+
this.trackSends.set(
|
|
2417
|
+
entry.trackId,
|
|
2418
|
+
entry.sends.map((send) => ({ ...send })),
|
|
2419
|
+
);
|
|
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
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
this.trackLaneIds.splice(0, this.trackLaneIds.length, ...ids);
|
|
2430
|
+
this.syncMixer();
|
|
2431
|
+
}
|
|
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
|
+
|
|
2479
|
+
setSends(target: string | number, sends: EngineTrackSend[]): void {
|
|
2480
|
+
const laneIndex = this.ensureTrackLane(target);
|
|
2481
|
+
const trackId = this.trackLaneIds[laneIndex];
|
|
2482
|
+
this.trackSends.set(
|
|
2483
|
+
trackId,
|
|
2484
|
+
sends.map((send) => ({ ...send })),
|
|
2300
2485
|
);
|
|
2486
|
+
this.syncMixer();
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2489
|
+
setTrackBuses(buses: EngineBus[]): void {
|
|
2490
|
+
this.buses.splice(0, this.buses.length, ...buses.map((bus) => ({ ...bus })));
|
|
2491
|
+
this.syncMixer();
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
setBusGain(busId: number, db: number): boolean {
|
|
2495
|
+
const busIndex = this.ensureBus(busId);
|
|
2496
|
+
this.buses[busIndex] = { ...this.buses[busIndex], busId, gainDb: db };
|
|
2497
|
+
this.offlineEngine.setTrackBuses(this.buses);
|
|
2498
|
+
const paramId = engineMixerBusTarget(busIndex, ENGINE_MIXER_PARAM_FADER_DB);
|
|
2499
|
+
this.offlineEngine.setParameter(paramId, db);
|
|
2500
|
+
return this.realtimeNode.sendCommand({
|
|
2501
|
+
type: SonareEngineCommandType.SetParamSmoothed,
|
|
2502
|
+
targetId: paramId,
|
|
2503
|
+
sampleTime: -1,
|
|
2504
|
+
argFloat: db,
|
|
2505
|
+
});
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2508
|
+
setTrackStripJson(target: string | number, sceneJson: string): void {
|
|
2509
|
+
const laneIndex = this.ensureTrackLane(target);
|
|
2510
|
+
const trackId = this.trackLaneIds[laneIndex];
|
|
2511
|
+
this.offlineEngine.setTrackStripJson(trackId, sceneJson);
|
|
2512
|
+
this.trackStripJson.set(trackId, sceneJson);
|
|
2513
|
+
this.syncMixer();
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
setTrackStripEqBand(target: string | number, bandIndex: number, band: EqBand | string): void {
|
|
2517
|
+
const laneIndex = this.ensureTrackLane(target);
|
|
2518
|
+
const trackId = this.trackLaneIds[laneIndex];
|
|
2519
|
+
const bandJson = typeof band === 'string' ? band : JSON.stringify(band);
|
|
2520
|
+
this.offlineEngine.setTrackStripEqBandJson(trackId, bandIndex, bandJson);
|
|
2521
|
+
this.postSync({ type: 'syncTrackStripEqBand', trackId, bandIndex, bandJson });
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2524
|
+
setTrackStripInsertBypassed(
|
|
2525
|
+
target: string | number,
|
|
2526
|
+
insertIndex: number,
|
|
2527
|
+
bypassed: boolean,
|
|
2528
|
+
resetOnBypass = false,
|
|
2529
|
+
): void {
|
|
2530
|
+
const laneIndex = this.ensureTrackLane(target);
|
|
2531
|
+
const trackId = this.trackLaneIds[laneIndex];
|
|
2532
|
+
this.offlineEngine.setTrackStripInsertBypassed(trackId, insertIndex, bypassed, resetOnBypass);
|
|
2533
|
+
this.postSync({
|
|
2534
|
+
type: 'syncTrackStripInsertBypassed',
|
|
2535
|
+
trackId,
|
|
2536
|
+
insertIndex,
|
|
2537
|
+
bypassed,
|
|
2538
|
+
resetOnBypass,
|
|
2539
|
+
});
|
|
2540
|
+
}
|
|
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
|
+
|
|
2592
|
+
setStripEq(target: string | number, bandIndex: number, band: EqBand | string): void {
|
|
2593
|
+
if (target === 'master') {
|
|
2594
|
+
this.setMasterStripEqBand(bandIndex, band);
|
|
2595
|
+
return;
|
|
2596
|
+
}
|
|
2597
|
+
this.setTrackStripEqBand(target, bandIndex, band);
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2600
|
+
setStripInsertBypassed(
|
|
2601
|
+
target: string | number,
|
|
2602
|
+
insertIndex: number,
|
|
2603
|
+
bypassed: boolean,
|
|
2604
|
+
resetOnBypass = false,
|
|
2605
|
+
): void {
|
|
2606
|
+
if (target === 'master') {
|
|
2607
|
+
this.setMasterStripInsertBypassed(insertIndex, bypassed, resetOnBypass);
|
|
2608
|
+
return;
|
|
2609
|
+
}
|
|
2610
|
+
this.setTrackStripInsertBypassed(target, insertIndex, bypassed, resetOnBypass);
|
|
2611
|
+
}
|
|
2612
|
+
|
|
2613
|
+
setStripInserts(target: string | number, sceneJson: string): void {
|
|
2614
|
+
if (target === 'master') {
|
|
2615
|
+
this.setMasterStripJson(sceneJson);
|
|
2616
|
+
return;
|
|
2617
|
+
}
|
|
2618
|
+
this.setTrackStripJson(target, sceneJson);
|
|
2619
|
+
}
|
|
2620
|
+
|
|
2621
|
+
setBusStripJson(busId: number, sceneJson: string): void {
|
|
2622
|
+
this.ensureBus(busId);
|
|
2623
|
+
this.offlineEngine.setBusStripJson(busId, sceneJson);
|
|
2624
|
+
this.busStripJson.set(busId, sceneJson);
|
|
2625
|
+
this.syncMixer();
|
|
2626
|
+
}
|
|
2627
|
+
|
|
2628
|
+
setMasterStripJson(sceneJson: string): void {
|
|
2629
|
+
this.offlineEngine.setMasterStripJson(sceneJson);
|
|
2630
|
+
this.masterStripJson = sceneJson;
|
|
2631
|
+
this.syncMixer();
|
|
2632
|
+
}
|
|
2633
|
+
|
|
2634
|
+
setMasterStripEqBand(bandIndex: number, band: EqBand | string): void {
|
|
2635
|
+
const bandJson = typeof band === 'string' ? band : JSON.stringify(band);
|
|
2636
|
+
this.offlineEngine.setMasterStripEqBandJson(bandIndex, bandJson);
|
|
2637
|
+
this.postSync({ type: 'syncMasterStripEqBand', bandIndex, bandJson });
|
|
2638
|
+
}
|
|
2639
|
+
|
|
2640
|
+
setMasterStripInsertBypassed(
|
|
2641
|
+
insertIndex: number,
|
|
2642
|
+
bypassed: boolean,
|
|
2643
|
+
resetOnBypass = false,
|
|
2644
|
+
): void {
|
|
2645
|
+
this.offlineEngine.setMasterStripInsertBypassed(insertIndex, bypassed, resetOnBypass);
|
|
2646
|
+
this.postSync({
|
|
2647
|
+
type: 'syncMasterStripInsertBypassed',
|
|
2648
|
+
insertIndex,
|
|
2649
|
+
bypassed,
|
|
2650
|
+
resetOnBypass,
|
|
2651
|
+
});
|
|
2652
|
+
}
|
|
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
|
+
|
|
2677
|
+
setMasterChain(sceneJson: string): void {
|
|
2678
|
+
this.setMasterStripJson(sceneJson);
|
|
2301
2679
|
}
|
|
2302
2680
|
|
|
2303
2681
|
addClip(
|
|
@@ -2312,19 +2690,152 @@ export class SonareEngine {
|
|
|
2312
2690
|
id,
|
|
2313
2691
|
channels: buffer,
|
|
2314
2692
|
startPpq,
|
|
2693
|
+
trackId: this.resolveTargetId(trackId),
|
|
2315
2694
|
};
|
|
2695
|
+
this.ensureTrackLane(trackId);
|
|
2316
2696
|
this.clips.set(id, clip);
|
|
2317
|
-
this.
|
|
2318
|
-
void trackId;
|
|
2697
|
+
this.syncClipsDelta([clip], []);
|
|
2319
2698
|
return id;
|
|
2320
2699
|
}
|
|
2321
2700
|
|
|
2322
2701
|
removeClip(clipId: number): void {
|
|
2323
2702
|
this.clips.delete(clipId);
|
|
2324
|
-
this.
|
|
2703
|
+
this.syncClipsDelta([], [clipId]);
|
|
2704
|
+
}
|
|
2705
|
+
|
|
2706
|
+
setMidiClips(clips: readonly EngineMidiClipSchedule[]): void {
|
|
2707
|
+
this.midiClips.clear();
|
|
2708
|
+
for (const clip of clips) {
|
|
2709
|
+
const id = clip.id ?? this.nextClipId++;
|
|
2710
|
+
this.midiClips.set(id, { ...clip, id, events: clip.events.map((event) => ({ ...event })) });
|
|
2711
|
+
}
|
|
2712
|
+
this.syncMidiClips();
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2715
|
+
setBuiltinInstrument(
|
|
2716
|
+
trackId: string | number,
|
|
2717
|
+
config: { destinationId?: number } & Record<string, unknown> = {},
|
|
2718
|
+
): void {
|
|
2719
|
+
const destinationId = this.resolveTargetId(trackId);
|
|
2720
|
+
this.offlineEngine.setBuiltinInstrument(config, destinationId);
|
|
2721
|
+
this.postInstrumentSync({ type: 'syncBuiltinInstrument', destinationId, config });
|
|
2722
|
+
}
|
|
2723
|
+
|
|
2724
|
+
setSynthInstrument(trackId: string | number, patch: Record<string, unknown> | string = {}): void {
|
|
2725
|
+
const destinationId = this.resolveTargetId(trackId);
|
|
2726
|
+
this.offlineEngine.setSynthInstrument(patch, destinationId);
|
|
2727
|
+
this.postInstrumentSync({ type: 'syncSynthInstrument', destinationId, patch });
|
|
2728
|
+
}
|
|
2729
|
+
|
|
2730
|
+
loadSoundFont(data: Uint8Array): void {
|
|
2731
|
+
this.offlineEngine.loadSoundFont(data);
|
|
2732
|
+
this.postInstrumentSync({ type: 'syncLoadSoundFont', data });
|
|
2733
|
+
}
|
|
2734
|
+
|
|
2735
|
+
setSf2Instrument(
|
|
2736
|
+
trackId: string | number,
|
|
2737
|
+
config: { destinationId?: number; gain?: number; polyphony?: number } = {},
|
|
2738
|
+
): void {
|
|
2739
|
+
const destinationId = this.resolveTargetId(trackId);
|
|
2740
|
+
this.offlineEngine.setSf2Instrument(config, destinationId);
|
|
2741
|
+
this.postInstrumentSync({ type: 'syncSf2Instrument', destinationId, config });
|
|
2742
|
+
}
|
|
2743
|
+
|
|
2744
|
+
pushMidiNoteOn(
|
|
2745
|
+
trackId: string | number,
|
|
2746
|
+
group: number,
|
|
2747
|
+
channel: number,
|
|
2748
|
+
note: number,
|
|
2749
|
+
velocity: number,
|
|
2750
|
+
renderFrame = -1,
|
|
2751
|
+
): void {
|
|
2752
|
+
const destinationId = this.resolveTargetId(trackId);
|
|
2753
|
+
this.offlineEngine.pushMidiNoteOn(destinationId, group, channel, note, velocity, renderFrame);
|
|
2754
|
+
this.postSync({
|
|
2755
|
+
type: 'syncMidiNoteOn',
|
|
2756
|
+
destinationId,
|
|
2757
|
+
group,
|
|
2758
|
+
channel,
|
|
2759
|
+
note,
|
|
2760
|
+
velocity,
|
|
2761
|
+
renderFrame,
|
|
2762
|
+
});
|
|
2763
|
+
}
|
|
2764
|
+
|
|
2765
|
+
pushMidiNoteOff(
|
|
2766
|
+
trackId: string | number,
|
|
2767
|
+
group: number,
|
|
2768
|
+
channel: number,
|
|
2769
|
+
note: number,
|
|
2770
|
+
velocity = 0,
|
|
2771
|
+
renderFrame = -1,
|
|
2772
|
+
): void {
|
|
2773
|
+
const destinationId = this.resolveTargetId(trackId);
|
|
2774
|
+
this.offlineEngine.pushMidiNoteOff(destinationId, group, channel, note, velocity, renderFrame);
|
|
2775
|
+
this.postSync({
|
|
2776
|
+
type: 'syncMidiNoteOff',
|
|
2777
|
+
destinationId,
|
|
2778
|
+
group,
|
|
2779
|
+
channel,
|
|
2780
|
+
note,
|
|
2781
|
+
velocity,
|
|
2782
|
+
renderFrame,
|
|
2783
|
+
});
|
|
2784
|
+
}
|
|
2785
|
+
|
|
2786
|
+
pushMidiCc(
|
|
2787
|
+
trackId: string | number,
|
|
2788
|
+
group: number,
|
|
2789
|
+
channel: number,
|
|
2790
|
+
controller: number,
|
|
2791
|
+
value: number,
|
|
2792
|
+
renderFrame = -1,
|
|
2793
|
+
): void {
|
|
2794
|
+
const destinationId = this.resolveTargetId(trackId);
|
|
2795
|
+
this.offlineEngine.pushMidiCc(destinationId, group, channel, controller, value, renderFrame);
|
|
2796
|
+
this.postSync({
|
|
2797
|
+
type: 'syncMidiCc',
|
|
2798
|
+
destinationId,
|
|
2799
|
+
group,
|
|
2800
|
+
channel,
|
|
2801
|
+
controller,
|
|
2802
|
+
value,
|
|
2803
|
+
renderFrame,
|
|
2804
|
+
});
|
|
2805
|
+
}
|
|
2806
|
+
|
|
2807
|
+
pushMidiPanic(renderFrame = -1): void {
|
|
2808
|
+
this.offlineEngine.pushMidiPanic(renderFrame);
|
|
2809
|
+
this.postSync({ type: 'syncMidiPanic', renderFrame });
|
|
2810
|
+
}
|
|
2811
|
+
|
|
2812
|
+
configureCapture(options: {
|
|
2813
|
+
bufferFrames: number;
|
|
2814
|
+
channels?: number;
|
|
2815
|
+
source?: EngineCaptureStatus['source'];
|
|
2816
|
+
recordOffsetSamples?: number;
|
|
2817
|
+
inputMonitor?: { enabled: boolean; gain?: number };
|
|
2818
|
+
}): void {
|
|
2819
|
+
const bufferFrames = Math.trunc(options.bufferFrames);
|
|
2820
|
+
const channels = Math.trunc(options.channels ?? this.offlineChannelCount);
|
|
2821
|
+
const source = options.source ?? 'output';
|
|
2822
|
+
const recordOffsetSamples = Math.trunc(options.recordOffsetSamples ?? 0);
|
|
2823
|
+
const inputMonitor = {
|
|
2824
|
+
enabled: Boolean(options.inputMonitor?.enabled),
|
|
2825
|
+
gain: options.inputMonitor?.gain ?? 1,
|
|
2826
|
+
};
|
|
2827
|
+
this.offlineEngine.setCaptureBuffer(channels, bufferFrames);
|
|
2828
|
+
this.offlineEngine.setCaptureSource(source);
|
|
2829
|
+
this.offlineEngine.setRecordOffsetSamples(recordOffsetSamples);
|
|
2830
|
+
this.offlineEngine.setInputMonitor(inputMonitor.enabled, inputMonitor.gain);
|
|
2831
|
+
this.captureConfig = { bufferFrames, channels, source, recordOffsetSamples, inputMonitor };
|
|
2832
|
+
this.postSync({ type: 'syncCapture', ...this.captureConfig });
|
|
2325
2833
|
}
|
|
2326
2834
|
|
|
2327
2835
|
armRecord(trackId: string | number, enabled: boolean): boolean {
|
|
2836
|
+
if (enabled && !this.captureConfig) {
|
|
2837
|
+
throw new Error('Capture buffer is not configured');
|
|
2838
|
+
}
|
|
2328
2839
|
this.offlineEngine.armCapture(enabled);
|
|
2329
2840
|
return this.realtimeNode.sendCommand({
|
|
2330
2841
|
type: SonareEngineCommandType.ArmRecord,
|
|
@@ -2335,8 +2846,8 @@ export class SonareEngine {
|
|
|
2335
2846
|
}
|
|
2336
2847
|
|
|
2337
2848
|
punch(inPpq: number, outPpq: number): boolean {
|
|
2338
|
-
const inSample = this.
|
|
2339
|
-
const outSample = this.
|
|
2849
|
+
const inSample = this.offlineEngine.sampleAtPpq(inPpq);
|
|
2850
|
+
const outSample = this.offlineEngine.sampleAtPpq(outPpq);
|
|
2340
2851
|
this.offlineEngine.setCapturePunch(inSample, outSample, true);
|
|
2341
2852
|
// Carry BOTH endpoints as already-converted SAMPLES so the realtime engine
|
|
2342
2853
|
// agrees with the offline engine. The previous code sent the raw PPQ out
|
|
@@ -2351,6 +2862,19 @@ export class SonareEngine {
|
|
|
2351
2862
|
});
|
|
2352
2863
|
}
|
|
2353
2864
|
|
|
2865
|
+
captureStatus(): Promise<EngineCaptureStatus> {
|
|
2866
|
+
return this.realtimeNode.requestCaptureStatus();
|
|
2867
|
+
}
|
|
2868
|
+
|
|
2869
|
+
capturedAudio(): Promise<Float32Array[]> {
|
|
2870
|
+
return this.realtimeNode.requestCapturedAudio();
|
|
2871
|
+
}
|
|
2872
|
+
|
|
2873
|
+
async resetCapture(): Promise<void> {
|
|
2874
|
+
this.offlineEngine.resetCapture();
|
|
2875
|
+
await this.realtimeNode.requestCaptureReset();
|
|
2876
|
+
}
|
|
2877
|
+
|
|
2354
2878
|
setMetronome(opts: EngineMetronomeConfig): void {
|
|
2355
2879
|
this.offlineEngine.setMetronome(opts);
|
|
2356
2880
|
// The full config (beatGain/accentGain/clickSamples/clickSeconds) cannot fit
|
|
@@ -2371,6 +2895,59 @@ export class SonareEngine {
|
|
|
2371
2895
|
return id;
|
|
2372
2896
|
}
|
|
2373
2897
|
|
|
2898
|
+
/**
|
|
2899
|
+
* Replaces the whole marker set in one call.
|
|
2900
|
+
*
|
|
2901
|
+
* Entries without an `id` are assigned fresh ids; entries carrying an `id`
|
|
2902
|
+
* keep it (ids must be positive and unique within the list). Returns the
|
|
2903
|
+
* resolved markers in the order given, so a caller can map its own marker
|
|
2904
|
+
* identities to the engine ids used by `seekMarker`/`setLoopFromMarkers`.
|
|
2905
|
+
*
|
|
2906
|
+
* @param markers The full marker list (an empty list clears all markers).
|
|
2907
|
+
* @returns The markers with their resolved engine ids.
|
|
2908
|
+
*/
|
|
2909
|
+
setMarkers(markers: ReadonlyArray<{ ppq: number; name?: string; id?: number }>): EngineMarker[] {
|
|
2910
|
+
const resolved: EngineMarker[] = [];
|
|
2911
|
+
const seen = new Set<number>();
|
|
2912
|
+
for (const marker of markers) {
|
|
2913
|
+
if (!Number.isFinite(marker.ppq)) {
|
|
2914
|
+
throw new Error(`Invalid marker ppq: ${String(marker.ppq)}`);
|
|
2915
|
+
}
|
|
2916
|
+
if (marker.id !== undefined) {
|
|
2917
|
+
if (!Number.isInteger(marker.id) || marker.id <= 0) {
|
|
2918
|
+
throw new Error(`Invalid marker id: ${String(marker.id)}`);
|
|
2919
|
+
}
|
|
2920
|
+
if (seen.has(marker.id)) {
|
|
2921
|
+
throw new Error(`Duplicate marker id: ${marker.id}`);
|
|
2922
|
+
}
|
|
2923
|
+
}
|
|
2924
|
+
const id = marker.id ?? this.nextMarkerId++;
|
|
2925
|
+
seen.add(id);
|
|
2926
|
+
if (id >= this.nextMarkerId) {
|
|
2927
|
+
this.nextMarkerId = id + 1;
|
|
2928
|
+
}
|
|
2929
|
+
resolved.push({ id, ppq: marker.ppq, name: marker.name ?? '' });
|
|
2930
|
+
}
|
|
2931
|
+
this.markers.clear();
|
|
2932
|
+
for (const marker of resolved) {
|
|
2933
|
+
this.markers.set(marker.id, marker);
|
|
2934
|
+
}
|
|
2935
|
+
this.syncMarkers();
|
|
2936
|
+
return resolved.map((marker) => ({ ...marker }));
|
|
2937
|
+
}
|
|
2938
|
+
|
|
2939
|
+
markerCount(): number {
|
|
2940
|
+
return this.offlineEngine.markerCount();
|
|
2941
|
+
}
|
|
2942
|
+
|
|
2943
|
+
markerByIndex(index: number): EngineMarker {
|
|
2944
|
+
return this.offlineEngine.markerByIndex(index);
|
|
2945
|
+
}
|
|
2946
|
+
|
|
2947
|
+
marker(markerId: number): EngineMarker {
|
|
2948
|
+
return this.offlineEngine.marker(markerId);
|
|
2949
|
+
}
|
|
2950
|
+
|
|
2374
2951
|
seekMarker(markerId: number): boolean {
|
|
2375
2952
|
this.offlineEngine.seekMarker(markerId);
|
|
2376
2953
|
// Forward to the live worklet engine. Its marker set is kept in sync via the
|
|
@@ -2384,6 +2961,13 @@ export class SonareEngine {
|
|
|
2384
2961
|
});
|
|
2385
2962
|
}
|
|
2386
2963
|
|
|
2964
|
+
setLoopFromMarkers(startMarkerId: number, endMarkerId: number): boolean {
|
|
2965
|
+
this.offlineEngine.setLoopFromMarkers(startMarkerId, endMarkerId);
|
|
2966
|
+
const start = this.offlineEngine.marker(startMarkerId);
|
|
2967
|
+
const end = this.offlineEngine.marker(endMarkerId);
|
|
2968
|
+
return this.setLoop(start.ppq, end.ppq, true);
|
|
2969
|
+
}
|
|
2970
|
+
|
|
2387
2971
|
async renderOffline(totalFrames: number): Promise<Float32Array[]> {
|
|
2388
2972
|
const frames = Math.max(0, Math.floor(totalFrames));
|
|
2389
2973
|
const inputs: Float32Array[] = [];
|
|
@@ -2397,6 +2981,10 @@ export class SonareEngine {
|
|
|
2397
2981
|
return this.realtimeNode.onMeter(callback);
|
|
2398
2982
|
}
|
|
2399
2983
|
|
|
2984
|
+
onScope(callback: (scope: SonareWorkletScopeSnapshot) => void): () => void {
|
|
2985
|
+
return this.realtimeNode.onScope(callback);
|
|
2986
|
+
}
|
|
2987
|
+
|
|
2400
2988
|
onTelemetry(callback: (telemetry: SonareEngineTelemetryRecord) => void): () => void {
|
|
2401
2989
|
return this.realtimeNode.onTelemetry(callback);
|
|
2402
2990
|
}
|
|
@@ -2409,6 +2997,10 @@ export class SonareEngine {
|
|
|
2409
2997
|
return this.realtimeNode.pollMeters();
|
|
2410
2998
|
}
|
|
2411
2999
|
|
|
3000
|
+
pollScope(): SonareWorkletScopeSnapshot[] {
|
|
3001
|
+
return this.realtimeNode.pollScope();
|
|
3002
|
+
}
|
|
3003
|
+
|
|
2412
3004
|
destroy(): void {
|
|
2413
3005
|
if (this.destroyed) {
|
|
2414
3006
|
return;
|
|
@@ -2420,14 +3012,58 @@ export class SonareEngine {
|
|
|
2420
3012
|
this.offlineEngine.destroy();
|
|
2421
3013
|
}
|
|
2422
3014
|
|
|
2423
|
-
private
|
|
3015
|
+
private syncClipsDelta(upserts: EngineClip[], removeIds: number[]): void {
|
|
2424
3016
|
const clips = Array.from(this.clips.values());
|
|
2425
3017
|
this.offlineEngine.setClips(clips);
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
3018
|
+
this.postSync({
|
|
3019
|
+
type: 'syncClipsDelta',
|
|
3020
|
+
upserts,
|
|
3021
|
+
removeIds,
|
|
3022
|
+
});
|
|
3023
|
+
}
|
|
3024
|
+
|
|
3025
|
+
private syncMidiClips(): void {
|
|
3026
|
+
const clips = Array.from(this.midiClips.values());
|
|
3027
|
+
this.offlineEngine.setMidiClips(clips);
|
|
3028
|
+
this.postSync({ type: 'syncMidiClips', clips });
|
|
3029
|
+
}
|
|
3030
|
+
|
|
3031
|
+
private mixerLanes(): EngineTrackLane[] {
|
|
3032
|
+
return this.trackLaneIds.map((trackId) => {
|
|
3033
|
+
const sends = this.trackSends.get(trackId);
|
|
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
|
+
};
|
|
3040
|
+
});
|
|
3041
|
+
}
|
|
3042
|
+
|
|
3043
|
+
private syncMixer(): void {
|
|
3044
|
+
const lanes = this.mixerLanes();
|
|
3045
|
+
const buses = this.buses.map((bus) => ({ ...bus }));
|
|
3046
|
+
this.offlineEngine.setTrackBuses(buses);
|
|
3047
|
+
if (lanes.length > 0) {
|
|
3048
|
+
this.offlineEngine.setTrackLanes(lanes);
|
|
3049
|
+
}
|
|
3050
|
+
const trackStrips = Array.from(this.trackStripJson, ([trackId, sceneJson]) => ({
|
|
3051
|
+
trackId,
|
|
3052
|
+
sceneJson,
|
|
3053
|
+
}));
|
|
3054
|
+
const busStrips = Array.from(this.busStripJson, ([busId, sceneJson]) => ({
|
|
3055
|
+
busId,
|
|
3056
|
+
sceneJson,
|
|
3057
|
+
}));
|
|
3058
|
+
this.postSync({
|
|
3059
|
+
type: 'syncMixer',
|
|
3060
|
+
lanes,
|
|
3061
|
+
buses,
|
|
3062
|
+
trackStrips,
|
|
3063
|
+
laneSidechains: Array.from(this.laneSidechains.values()),
|
|
3064
|
+
busStrips,
|
|
3065
|
+
masterStripJson: this.masterStripJson,
|
|
3066
|
+
});
|
|
2431
3067
|
}
|
|
2432
3068
|
|
|
2433
3069
|
private syncMarkers(): void {
|
|
@@ -2436,6 +3072,37 @@ export class SonareEngine {
|
|
|
2436
3072
|
this.postSync({ type: 'syncMarkers', markers });
|
|
2437
3073
|
}
|
|
2438
3074
|
|
|
3075
|
+
private postInstrumentSync(message: SonareEngineInstrumentSyncMessage): void {
|
|
3076
|
+
if (this.destroyed) {
|
|
3077
|
+
return;
|
|
3078
|
+
}
|
|
3079
|
+
if (this.transportPlaying) {
|
|
3080
|
+
this.pendingInstrumentSync.push(message);
|
|
3081
|
+
return;
|
|
3082
|
+
}
|
|
3083
|
+
this.postSync(message);
|
|
3084
|
+
}
|
|
3085
|
+
|
|
3086
|
+
private flushPendingInstrumentSync(): void {
|
|
3087
|
+
if (this.destroyed || this.pendingInstrumentSync.length === 0) {
|
|
3088
|
+
return;
|
|
3089
|
+
}
|
|
3090
|
+
const pending = this.pendingInstrumentSync.splice(0);
|
|
3091
|
+
for (const message of pending) {
|
|
3092
|
+
this.postSync(message);
|
|
3093
|
+
}
|
|
3094
|
+
}
|
|
3095
|
+
|
|
3096
|
+
private postTempoSync(): void {
|
|
3097
|
+
this.postSync({
|
|
3098
|
+
type: 'syncTempo',
|
|
3099
|
+
bpm: this.tempoBpm,
|
|
3100
|
+
timeSignature: { ...this.timeSignature },
|
|
3101
|
+
tempoSegments: this.tempoSegments.map((segment) => ({ ...segment })),
|
|
3102
|
+
timeSignatureSegments: this.timeSignatureSegments.map((segment) => ({ ...segment })),
|
|
3103
|
+
});
|
|
3104
|
+
}
|
|
3105
|
+
|
|
2439
3106
|
// Posts an out-of-band control-sync message to the worklet engine processor.
|
|
2440
3107
|
// Sync messages use a string `type` so the worklet's message handler routes
|
|
2441
3108
|
// them to receiveSync() (numeric `type` is reserved for SonareEngineCommandRecord).
|
|
@@ -2465,16 +3132,40 @@ export class SonareEngine {
|
|
|
2465
3132
|
return Number.isFinite(parsed) ? parsed : 0;
|
|
2466
3133
|
}
|
|
2467
3134
|
|
|
3135
|
+
private ensureTrackLane(target: string | number): number {
|
|
3136
|
+
const trackId = this.resolveTargetId(target);
|
|
3137
|
+
if (!Number.isInteger(trackId) || trackId <= 0) {
|
|
3138
|
+
throw new Error(`Invalid track id for mixer lane: ${String(target)}`);
|
|
3139
|
+
}
|
|
3140
|
+
const existing = this.trackLaneIds.indexOf(trackId);
|
|
3141
|
+
if (existing >= 0) {
|
|
3142
|
+
return existing;
|
|
3143
|
+
}
|
|
3144
|
+
this.trackLaneIds.push(trackId);
|
|
3145
|
+
this.syncMixer();
|
|
3146
|
+
return this.trackLaneIds.length - 1;
|
|
3147
|
+
}
|
|
3148
|
+
|
|
3149
|
+
private ensureBus(busId: number): number {
|
|
3150
|
+
const resolved = Math.trunc(busId);
|
|
3151
|
+
if (!Number.isInteger(resolved) || resolved <= 0) {
|
|
3152
|
+
throw new Error(`Invalid bus id for mixer bus: ${String(busId)}`);
|
|
3153
|
+
}
|
|
3154
|
+
const existing = this.buses.findIndex((bus) => bus.busId === resolved);
|
|
3155
|
+
if (existing >= 0) {
|
|
3156
|
+
return existing;
|
|
3157
|
+
}
|
|
3158
|
+
this.buses.push({ busId: resolved });
|
|
3159
|
+
this.syncMixer();
|
|
3160
|
+
return this.buses.length - 1;
|
|
3161
|
+
}
|
|
3162
|
+
|
|
2468
3163
|
private curveCode(curve: number | 'linear' | 'exponential'): number {
|
|
2469
3164
|
if (typeof curve === 'number') {
|
|
2470
3165
|
return curve;
|
|
2471
3166
|
}
|
|
2472
3167
|
return curve === 'exponential' ? 1 : 0;
|
|
2473
3168
|
}
|
|
2474
|
-
|
|
2475
|
-
private ppqToApproxSample(ppq: number): number {
|
|
2476
|
-
return Math.max(0, Math.round(((ppq * 60) / 120) * this.sampleRate));
|
|
2477
|
-
}
|
|
2478
3169
|
}
|
|
2479
3170
|
|
|
2480
3171
|
export class SonareRealtimeVoiceChangerWorkletProcessor {
|
|
@@ -2768,6 +3459,7 @@ export function registerSonareRealtimeEngineWorkletProcessor(
|
|
|
2768
3459
|
class RegisteredSonareRealtimeEngineWorkletProcessor extends Base {
|
|
2769
3460
|
private bridge?: SonareRealtimeEngineWorkletProcessor;
|
|
2770
3461
|
private rtBridge?: SonareRtRealtimeEngineRuntime;
|
|
3462
|
+
private readonly pendingMessages: unknown[] = [];
|
|
2771
3463
|
readonly port?: WorkletPort;
|
|
2772
3464
|
|
|
2773
3465
|
constructor(options?: { processorOptions?: SonareRealtimeEngineWorkletProcessorOptions }) {
|
|
@@ -2777,18 +3469,27 @@ export function registerSonareRealtimeEngineWorkletProcessor(
|
|
|
2777
3469
|
if (processorOptions.runtimeTarget === 'sonare-rt') {
|
|
2778
3470
|
void this.initializeSonareRt(processorOptions, port);
|
|
2779
3471
|
} else {
|
|
2780
|
-
this.
|
|
2781
|
-
postMessage: (message) => port?.postMessage?.(message),
|
|
2782
|
-
onMeter: (meter) => port?.postMessage?.(meter),
|
|
2783
|
-
});
|
|
3472
|
+
void this.initializeEmbind(processorOptions, port);
|
|
2784
3473
|
}
|
|
2785
3474
|
const onMessage = (event: { data: unknown }) => {
|
|
3475
|
+
if (!this.bridge && !this.rtBridge) {
|
|
3476
|
+
if (this.pendingMessages.length < 1024) {
|
|
3477
|
+
this.pendingMessages.push(event.data);
|
|
3478
|
+
}
|
|
3479
|
+
return;
|
|
3480
|
+
}
|
|
2786
3481
|
if (isEngineCommandRecord(event.data)) {
|
|
2787
3482
|
this.bridge?.receiveCommand(event.data);
|
|
2788
3483
|
this.rtBridge?.receiveCommand(event.data);
|
|
2789
3484
|
} else if (isEngineSyncMessage(event.data)) {
|
|
2790
3485
|
this.bridge?.receiveSync(event.data);
|
|
2791
3486
|
this.rtBridge?.receiveSync(event.data);
|
|
3487
|
+
} else if (isEngineCaptureRequestMessage(event.data)) {
|
|
3488
|
+
this.bridge?.receiveCaptureRequest(event.data);
|
|
3489
|
+
this.rtBridge?.receiveCaptureRequest(event.data, port);
|
|
3490
|
+
} else if (isEngineTransportRequestMessage(event.data)) {
|
|
3491
|
+
this.bridge?.receiveTransportRequest(event.data);
|
|
3492
|
+
this.rtBridge?.receiveTransportRequest(event.data, port);
|
|
2792
3493
|
}
|
|
2793
3494
|
};
|
|
2794
3495
|
if (port?.addEventListener) {
|
|
@@ -2813,6 +3514,75 @@ export function registerSonareRealtimeEngineWorkletProcessor(
|
|
|
2813
3514
|
return true;
|
|
2814
3515
|
}
|
|
2815
3516
|
|
|
3517
|
+
private replayPendingMessages(port?: WorkletPort): void {
|
|
3518
|
+
const messages = this.pendingMessages.splice(0);
|
|
3519
|
+
for (const data of messages) {
|
|
3520
|
+
if (isEngineCommandRecord(data)) {
|
|
3521
|
+
this.bridge?.receiveCommand(data);
|
|
3522
|
+
this.rtBridge?.receiveCommand(data);
|
|
3523
|
+
} else if (isEngineSyncMessage(data)) {
|
|
3524
|
+
this.bridge?.receiveSync(data);
|
|
3525
|
+
this.rtBridge?.receiveSync(data);
|
|
3526
|
+
} else if (isEngineCaptureRequestMessage(data)) {
|
|
3527
|
+
this.bridge?.receiveCaptureRequest(data);
|
|
3528
|
+
this.rtBridge?.receiveCaptureRequest(data, port);
|
|
3529
|
+
} else if (isEngineTransportRequestMessage(data)) {
|
|
3530
|
+
this.bridge?.receiveTransportRequest(data);
|
|
3531
|
+
this.rtBridge?.receiveTransportRequest(data, port);
|
|
3532
|
+
}
|
|
3533
|
+
}
|
|
3534
|
+
}
|
|
3535
|
+
|
|
3536
|
+
private async initializeEmbind(
|
|
3537
|
+
options: SonareRealtimeEngineWorkletProcessorOptions,
|
|
3538
|
+
port?: WorkletPort,
|
|
3539
|
+
): Promise<void> {
|
|
3540
|
+
try {
|
|
3541
|
+
const initPromise = (
|
|
3542
|
+
globalThis as typeof globalThis & { SonareEmbindInitPromise?: Promise<void> }
|
|
3543
|
+
).SonareEmbindInitPromise;
|
|
3544
|
+
if (initPromise) {
|
|
3545
|
+
await initPromise;
|
|
3546
|
+
}
|
|
3547
|
+
if (!isInitialized()) {
|
|
3548
|
+
type EmbindModuleFactory = (options?: {
|
|
3549
|
+
locateFile?: (path: string, prefix: string) => string;
|
|
3550
|
+
wasmBinary?: ArrayBuffer | Uint8Array;
|
|
3551
|
+
}) => Promise<SonareModule>;
|
|
3552
|
+
const moduleFactory = (
|
|
3553
|
+
globalThis as typeof globalThis & {
|
|
3554
|
+
SonareEmbindModuleFactory?: EmbindModuleFactory;
|
|
3555
|
+
}
|
|
3556
|
+
).SonareEmbindModuleFactory;
|
|
3557
|
+
if (!moduleFactory) {
|
|
3558
|
+
throw new Error('embind realtime engine module is not initialized.');
|
|
3559
|
+
}
|
|
3560
|
+
await initSonareModule({
|
|
3561
|
+
locateFile: (path) => path,
|
|
3562
|
+
wasmBinary: options.wasmBinary,
|
|
3563
|
+
moduleFactory,
|
|
3564
|
+
});
|
|
3565
|
+
}
|
|
3566
|
+
this.bridge = new SonareRealtimeEngineWorkletProcessor(options, {
|
|
3567
|
+
postMessage: (message) => port?.postMessage?.(message),
|
|
3568
|
+
onMeter: (meter) => port?.postMessage?.(meter),
|
|
3569
|
+
});
|
|
3570
|
+
for (const message of options.initialSyncMessages ?? []) {
|
|
3571
|
+
this.bridge.receiveSync(message);
|
|
3572
|
+
}
|
|
3573
|
+
for (const command of options.initialCommands ?? []) {
|
|
3574
|
+
this.bridge.receiveCommand(command);
|
|
3575
|
+
}
|
|
3576
|
+
this.replayPendingMessages(port);
|
|
3577
|
+
port?.postMessage?.({ type: 'ready', runtimeTarget: 'embind' });
|
|
3578
|
+
} catch (error) {
|
|
3579
|
+
port?.postMessage?.({
|
|
3580
|
+
type: 'error',
|
|
3581
|
+
message: error instanceof Error ? error.message : String(error),
|
|
3582
|
+
});
|
|
3583
|
+
}
|
|
3584
|
+
}
|
|
3585
|
+
|
|
2816
3586
|
private async initializeSonareRt(
|
|
2817
3587
|
options: SonareRealtimeEngineWorkletProcessorOptions,
|
|
2818
3588
|
port?: WorkletPort,
|
|
@@ -2857,6 +3627,7 @@ export function registerSonareRealtimeEngineWorkletProcessor(
|
|
|
2857
3627
|
telemetrySharedBuffer: options.telemetrySharedBuffer,
|
|
2858
3628
|
telemetryRingCapacity: options.telemetryRingCapacity,
|
|
2859
3629
|
});
|
|
3630
|
+
this.replayPendingMessages(port);
|
|
2860
3631
|
port?.postMessage?.({ type: 'ready', runtimeTarget: 'sonare-rt' });
|
|
2861
3632
|
} catch (error) {
|
|
2862
3633
|
port?.postMessage?.({
|