@libraz/libsonare 1.2.2 → 1.3.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 +38 -1
- package/dist/index.d.ts +1 -2722
- package/dist/index.js +3659 -1896
- package/dist/index.js.map +1 -1
- package/dist/sonare-rt-module.js +1 -1
- package/dist/sonare-rt.js +1 -1
- package/dist/sonare-rt.wasm +0 -0
- package/dist/sonare.js +1 -1
- package/dist/sonare.wasm +0 -0
- package/dist/worklet.d.ts +4827 -455
- package/dist/worklet.js +1076 -494
- package/dist/worklet.js.map +1 -1
- package/package.json +2 -1
- package/src/analysis_helpers.ts +152 -0
- package/src/audio.ts +493 -0
- package/src/codes.ts +56 -0
- package/src/effects_mastering.ts +964 -0
- package/src/feature_core.ts +248 -0
- package/src/feature_music.ts +419 -0
- package/src/feature_pitch.ts +80 -0
- package/src/feature_resample.ts +21 -0
- package/src/feature_spectral.ts +330 -0
- package/src/feature_spectrogram.ts +454 -0
- package/src/features.ts +84 -0
- package/src/index.ts +352 -4793
- package/src/live_audio.ts +45 -0
- package/src/metering.ts +380 -0
- package/src/mixer.ts +523 -0
- package/src/module_state.ts +14 -0
- package/src/opfs_clip_pages.ts +188 -0
- package/src/project.ts +1614 -0
- package/src/public_types.ts +244 -2
- package/src/quick_analysis.ts +508 -0
- package/src/realtime_engine.ts +667 -0
- package/src/realtime_voice_changer.ts +275 -0
- package/src/scale.ts +42 -0
- package/src/sonare.js.d.ts +386 -4
- package/src/stream_analyzer.ts +275 -0
- package/src/stream_types.ts +29 -1
- package/src/streaming_mixing.ts +18 -0
- package/src/streaming_processors.ts +335 -0
- package/src/validation.ts +82 -0
- package/src/web_midi.ts +367 -0
- package/src/worklet.ts +525 -81
package/dist/worklet.js
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
var
|
|
1
|
+
// src/module_state.ts
|
|
2
|
+
var wasmModule = null;
|
|
3
|
+
function setSonareModule(module2) {
|
|
4
|
+
wasmModule = module2;
|
|
5
|
+
}
|
|
6
|
+
function getSonareModule() {
|
|
7
|
+
if (!wasmModule) {
|
|
8
|
+
throw new Error("Module not initialized. Call init() first.");
|
|
9
|
+
}
|
|
10
|
+
return wasmModule;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// src/codes.ts
|
|
3
14
|
function automationCurveCode(curve) {
|
|
4
15
|
switch (curve) {
|
|
5
16
|
case "linear":
|
|
@@ -50,213 +61,396 @@ function meterTapCode(tap) {
|
|
|
50
61
|
function sendTimingCode(timing) {
|
|
51
62
|
return timing === "preFader" || timing === 0 ? 0 : 1;
|
|
52
63
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
64
|
+
|
|
65
|
+
// src/mixer.ts
|
|
66
|
+
var Mixer = class _Mixer {
|
|
67
|
+
constructor(mixer) {
|
|
68
|
+
this.mixer = mixer;
|
|
58
69
|
}
|
|
59
|
-
|
|
60
|
-
|
|
70
|
+
/**
|
|
71
|
+
* Build a mixer from a scene JSON string.
|
|
72
|
+
*
|
|
73
|
+
* @param json - Scene JSON (strips, buses, sends, connections, inserts)
|
|
74
|
+
* @param sampleRate - Sample rate in Hz (default: 48000)
|
|
75
|
+
* @param blockSize - Maximum block size per {@link processStereo} call (default: 512)
|
|
76
|
+
*/
|
|
77
|
+
static fromSceneJson(json, sampleRate = 48e3, blockSize = 512) {
|
|
78
|
+
const module2 = getSonareModule();
|
|
79
|
+
return new _Mixer(module2.createMixerFromSceneJson(json, sampleRate, blockSize));
|
|
61
80
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
81
|
+
/** Rebuild and compile the routing graph from the current scene topology. */
|
|
82
|
+
compile() {
|
|
83
|
+
this.mixer.compile();
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Mix one block of per-strip stereo audio into the stereo master.
|
|
87
|
+
*
|
|
88
|
+
* @param leftChannels - `leftChannels[i]` is the left channel of strip `i`
|
|
89
|
+
* @param rightChannels - `rightChannels[i]` is the right channel of strip `i`
|
|
90
|
+
* @returns Mixed stereo master (`left`, `right`, `sampleRate`)
|
|
91
|
+
*/
|
|
92
|
+
processStereo(leftChannels, rightChannels) {
|
|
93
|
+
if (leftChannels.length !== rightChannels.length) {
|
|
94
|
+
throw new Error("leftChannels and rightChannels must have the same length.");
|
|
69
95
|
}
|
|
70
|
-
|
|
71
|
-
return initPromise;
|
|
72
|
-
}
|
|
73
|
-
function isInitialized() {
|
|
74
|
-
return module !== null;
|
|
75
|
-
}
|
|
76
|
-
function engineAbiVersion() {
|
|
77
|
-
if (!module) {
|
|
78
|
-
throw new Error("Module not initialized. Call init() first.");
|
|
96
|
+
return this.mixer.processStereo(leftChannels, rightChannels);
|
|
79
97
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
abiCompatible: abiVersion === EXPECTED_ENGINE_ABI_VERSION,
|
|
91
|
-
sharedArrayBuffer,
|
|
92
|
-
atomics,
|
|
93
|
-
audioWorklet,
|
|
94
|
-
mode: sharedArrayBuffer && atomics ? "sab" : "postMessage"
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
var RealtimeEngine = class {
|
|
98
|
-
constructor(sampleRate = 48e3, maxBlockSize = 128, commandCapacity = 1024, telemetryCapacity = 1024) {
|
|
99
|
-
if (!module) {
|
|
100
|
-
throw new Error("Module not initialized. Call init() first.");
|
|
98
|
+
/**
|
|
99
|
+
* Mix one block into caller-owned output arrays.
|
|
100
|
+
*
|
|
101
|
+
* This avoids allocating the result object and result `Float32Array`s. It is
|
|
102
|
+
* intended for realtime bridges such as AudioWorklet; the input channel count
|
|
103
|
+
* must match the scene strip count and all arrays must have the same length.
|
|
104
|
+
*/
|
|
105
|
+
processStereoInto(leftChannels, rightChannels, outLeft, outRight) {
|
|
106
|
+
if (leftChannels.length !== rightChannels.length) {
|
|
107
|
+
throw new Error("leftChannels and rightChannels must have the same length.");
|
|
101
108
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
throw new Error(
|
|
105
|
-
`Engine ABI mismatch: wasm=${capabilities.engineAbiVersion}, expected=${capabilities.expectedEngineAbiVersion}`
|
|
106
|
-
);
|
|
109
|
+
if (outLeft.length !== outRight.length) {
|
|
110
|
+
throw new Error("outLeft and outRight must have the same length.");
|
|
107
111
|
}
|
|
108
|
-
this.
|
|
109
|
-
sampleRate,
|
|
110
|
-
maxBlockSize,
|
|
111
|
-
commandCapacity,
|
|
112
|
-
telemetryCapacity
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
prepare(sampleRate, maxBlockSize, commandCapacity = 1024, telemetryCapacity = 1024) {
|
|
116
|
-
this.native.prepare(sampleRate, maxBlockSize, commandCapacity, telemetryCapacity);
|
|
117
|
-
}
|
|
118
|
-
/** Queue a sample-accurate parameter change (engine kSetParam). */
|
|
119
|
-
setParameter(paramId, value, renderFrame = -1) {
|
|
120
|
-
this.native.setParameter(paramId, value, renderFrame);
|
|
112
|
+
this.mixer.processStereoInto(leftChannels, rightChannels, outLeft, outRight);
|
|
121
113
|
}
|
|
122
|
-
/**
|
|
123
|
-
|
|
124
|
-
|
|
114
|
+
/**
|
|
115
|
+
* Create reusable WASM-heap input/output views for realtime-style processing.
|
|
116
|
+
*
|
|
117
|
+
* Fill `leftInputs[i]` / `rightInputs[i]`, call `process()`, then read
|
|
118
|
+
* `outLeft` / `outRight`. The views are owned by this mixer and become invalid
|
|
119
|
+
* after {@link delete}.
|
|
120
|
+
*/
|
|
121
|
+
createRealtimeBuffer() {
|
|
122
|
+
const stripCount = this.stripCount();
|
|
123
|
+
let leftInputs = [];
|
|
124
|
+
let rightInputs = [];
|
|
125
|
+
let outLeft = this.mixer.outputLeftView();
|
|
126
|
+
let outRight = this.mixer.outputRightView();
|
|
127
|
+
const acquire = () => {
|
|
128
|
+
leftInputs = [];
|
|
129
|
+
rightInputs = [];
|
|
130
|
+
for (let index = 0; index < stripCount; index++) {
|
|
131
|
+
leftInputs.push(this.mixer.inputLeftView(index));
|
|
132
|
+
rightInputs.push(this.mixer.inputRightView(index));
|
|
133
|
+
}
|
|
134
|
+
outLeft = this.mixer.outputLeftView();
|
|
135
|
+
outRight = this.mixer.outputRightView();
|
|
136
|
+
};
|
|
137
|
+
acquire();
|
|
138
|
+
const reacquireIfDetached = () => {
|
|
139
|
+
if (outLeft.byteLength === 0 || (leftInputs[0]?.byteLength ?? 1) === 0) {
|
|
140
|
+
acquire();
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
return {
|
|
144
|
+
get leftInputs() {
|
|
145
|
+
reacquireIfDetached();
|
|
146
|
+
return leftInputs;
|
|
147
|
+
},
|
|
148
|
+
get rightInputs() {
|
|
149
|
+
reacquireIfDetached();
|
|
150
|
+
return rightInputs;
|
|
151
|
+
},
|
|
152
|
+
get outLeft() {
|
|
153
|
+
reacquireIfDetached();
|
|
154
|
+
return outLeft;
|
|
155
|
+
},
|
|
156
|
+
get outRight() {
|
|
157
|
+
reacquireIfDetached();
|
|
158
|
+
return outRight;
|
|
159
|
+
},
|
|
160
|
+
process: (numSamples = outLeft.length) => {
|
|
161
|
+
reacquireIfDetached();
|
|
162
|
+
this.mixer.processPreparedStereo(numSamples);
|
|
163
|
+
}
|
|
164
|
+
};
|
|
125
165
|
}
|
|
126
|
-
/**
|
|
127
|
-
|
|
128
|
-
return this.
|
|
166
|
+
/** Number of strips in the mixer (e.g. strips loaded from the scene). */
|
|
167
|
+
stripCount() {
|
|
168
|
+
return this.mixer.stripCount();
|
|
129
169
|
}
|
|
130
|
-
|
|
131
|
-
|
|
170
|
+
/**
|
|
171
|
+
* Schedule sample-accurate insert-parameter automation on a strip's insert.
|
|
172
|
+
*
|
|
173
|
+
* @param stripIndex - Strip index in `[0, stripCount())`
|
|
174
|
+
* @param insertIndex - Index into the strip's combined insert sequence
|
|
175
|
+
* (`[pre-inserts... post-inserts...]`)
|
|
176
|
+
* @param paramId - Processor-specific parameter id
|
|
177
|
+
* @param samplePos - Absolute samples from the start of processing (the mixer
|
|
178
|
+
* advances an internal position from 0 on the first {@link processStereo}
|
|
179
|
+
* call; recompiling resets it to 0)
|
|
180
|
+
* @param value - Target parameter value
|
|
181
|
+
* @param curve - Interpolation curve (default: `'linear'`)
|
|
182
|
+
* @throws If the strip index is out of range or the schedule call fails
|
|
183
|
+
* (unknown curve, out-of-range insert index, or full event lane)
|
|
184
|
+
*/
|
|
185
|
+
scheduleInsertAutomation(stripIndex, insertIndex, paramId, samplePos, value, curve = "linear") {
|
|
186
|
+
this.mixer.scheduleInsertAutomation(
|
|
187
|
+
stripIndex,
|
|
188
|
+
insertIndex,
|
|
189
|
+
paramId,
|
|
190
|
+
samplePos,
|
|
191
|
+
value,
|
|
192
|
+
automationCurveCode(curve)
|
|
193
|
+
);
|
|
132
194
|
}
|
|
133
|
-
|
|
134
|
-
|
|
195
|
+
/**
|
|
196
|
+
* Resolve a strip's index in `[0, stripCount())` from its scene id, or `null`
|
|
197
|
+
* when no strip with that id exists (matches the Node binding's `number | null`).
|
|
198
|
+
*/
|
|
199
|
+
stripById(id) {
|
|
200
|
+
const index = this.mixer.stripById(id);
|
|
201
|
+
return index < 0 ? null : index;
|
|
135
202
|
}
|
|
136
|
-
|
|
137
|
-
|
|
203
|
+
/**
|
|
204
|
+
* Add a bus to the mixer topology. `role` is one of `'master'`, `'aux'`, or
|
|
205
|
+
* `'submix'` (defaults to `'aux'`). Marks the routing graph dirty; call
|
|
206
|
+
* {@link compile} (or {@link processStereo}) to rebuild.
|
|
207
|
+
*/
|
|
208
|
+
addBus(id, role = "aux") {
|
|
209
|
+
this.mixer.addBus(id, role);
|
|
138
210
|
}
|
|
139
|
-
|
|
140
|
-
|
|
211
|
+
/** Remove a bus by id. Marks the routing graph dirty. */
|
|
212
|
+
removeBus(id) {
|
|
213
|
+
this.mixer.removeBus(id);
|
|
141
214
|
}
|
|
142
|
-
|
|
143
|
-
|
|
215
|
+
/** Number of buses in the mixer topology. */
|
|
216
|
+
busCount() {
|
|
217
|
+
return this.mixer.busCount();
|
|
144
218
|
}
|
|
145
|
-
|
|
146
|
-
|
|
219
|
+
/**
|
|
220
|
+
* Add a VCA group with the given gain offset (dB). `members` is a list of
|
|
221
|
+
* strip ids governed by the group (may be empty).
|
|
222
|
+
*/
|
|
223
|
+
addVcaGroup(id, gainDb = 0, members = []) {
|
|
224
|
+
this.mixer.addVcaGroup(id, gainDb, members);
|
|
147
225
|
}
|
|
148
|
-
|
|
149
|
-
|
|
226
|
+
/** Set an existing VCA group's gain in dB. */
|
|
227
|
+
setVcaGroupGainDb(id, gainDb) {
|
|
228
|
+
this.mixer.setVcaGroupGainDb(id, gainDb);
|
|
150
229
|
}
|
|
151
|
-
|
|
152
|
-
|
|
230
|
+
/** Remove a VCA group by id. */
|
|
231
|
+
removeVcaGroup(id) {
|
|
232
|
+
this.mixer.removeVcaGroup(id);
|
|
153
233
|
}
|
|
154
|
-
|
|
155
|
-
|
|
234
|
+
/** Number of VCA groups in the mixer topology. */
|
|
235
|
+
vcaGroupCount() {
|
|
236
|
+
return this.mixer.vcaGroupCount();
|
|
156
237
|
}
|
|
157
|
-
|
|
158
|
-
|
|
238
|
+
/** Set the strip's input trim in dB. */
|
|
239
|
+
setInputTrimDb(stripIndex, db) {
|
|
240
|
+
this.mixer.setInputTrimDb(stripIndex, db);
|
|
159
241
|
}
|
|
160
|
-
|
|
161
|
-
|
|
242
|
+
/** Set the strip's fader level in dB. */
|
|
243
|
+
setFaderDb(stripIndex, db) {
|
|
244
|
+
this.mixer.setFaderDb(stripIndex, db);
|
|
162
245
|
}
|
|
163
|
-
|
|
164
|
-
|
|
246
|
+
/**
|
|
247
|
+
* Set the strip's pan position.
|
|
248
|
+
*
|
|
249
|
+
* @param stripIndex - Strip index in `[0, stripCount())`
|
|
250
|
+
* @param pan - Pan position in `[-1, 1]`
|
|
251
|
+
* @param panMode - Optional pan mode. When omitted the strip's current pan
|
|
252
|
+
* mode is kept (passes `SONARE_PAN_MODE_KEEP`), so a plain pan nudge does
|
|
253
|
+
* not reset a scene-defined `'stereoPan'` / `'dualPan'` mode back to
|
|
254
|
+
* balance. Pass `'balance'` (or `0`) explicitly to force balance mode.
|
|
255
|
+
*/
|
|
256
|
+
setPan(stripIndex, pan, panMode) {
|
|
257
|
+
const mode = panMode === void 0 ? -1 : panModeCode(panMode);
|
|
258
|
+
this.mixer.setPan(stripIndex, pan, mode);
|
|
165
259
|
}
|
|
166
|
-
|
|
167
|
-
|
|
260
|
+
/** Set the strip's stereo width. */
|
|
261
|
+
setWidth(stripIndex, width) {
|
|
262
|
+
this.mixer.setWidth(stripIndex, width);
|
|
168
263
|
}
|
|
169
|
-
|
|
170
|
-
|
|
264
|
+
/** Set the strip's mute state. */
|
|
265
|
+
setMuted(stripIndex, muted) {
|
|
266
|
+
this.mixer.setMuted(stripIndex, muted);
|
|
171
267
|
}
|
|
172
|
-
|
|
173
|
-
|
|
268
|
+
/**
|
|
269
|
+
* Set a strip's solo state. Takes effect on the next process without a
|
|
270
|
+
* graph recompile.
|
|
271
|
+
*/
|
|
272
|
+
setSoloed(stripIndex, soloed) {
|
|
273
|
+
this.mixer.setSoloed(stripIndex, soloed);
|
|
174
274
|
}
|
|
175
|
-
|
|
176
|
-
|
|
275
|
+
/**
|
|
276
|
+
* Mark a strip solo-safe so it is never implied-muted by another strip's
|
|
277
|
+
* solo. Takes effect on the next process without a graph recompile.
|
|
278
|
+
*/
|
|
279
|
+
setSoloSafe(stripIndex, soloSafe) {
|
|
280
|
+
this.mixer.setSoloSafe(stripIndex, soloSafe);
|
|
177
281
|
}
|
|
178
|
-
|
|
179
|
-
|
|
282
|
+
/** Invert the polarity of the left and/or right channel of a strip. */
|
|
283
|
+
setPolarityInvert(stripIndex, invertLeft, invertRight) {
|
|
284
|
+
this.mixer.setPolarityInvert(stripIndex, invertLeft, invertRight);
|
|
180
285
|
}
|
|
181
|
-
|
|
182
|
-
|
|
286
|
+
/** Set the strip's pan law. */
|
|
287
|
+
setPanLaw(stripIndex, panLaw) {
|
|
288
|
+
this.mixer.setPanLaw(stripIndex, panLawCode(panLaw));
|
|
183
289
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
metronome() {
|
|
191
|
-
return this.native.metronome();
|
|
192
|
-
}
|
|
193
|
-
countInEndSample(startSample, bars) {
|
|
194
|
-
return Number(this.native.countInEndSample(startSample, bars));
|
|
195
|
-
}
|
|
196
|
-
setGraph(spec) {
|
|
197
|
-
this.native.setGraph(spec);
|
|
198
|
-
}
|
|
199
|
-
graphNodeCount() {
|
|
200
|
-
return this.native.graphNodeCount();
|
|
290
|
+
/**
|
|
291
|
+
* Set a per-strip channel delay in samples. This changes the strip's reported
|
|
292
|
+
* latency; recompile to re-run latency compensation.
|
|
293
|
+
*/
|
|
294
|
+
setChannelDelaySamples(stripIndex, delaySamples) {
|
|
295
|
+
this.mixer.setChannelDelaySamples(stripIndex, delaySamples);
|
|
201
296
|
}
|
|
202
|
-
|
|
203
|
-
|
|
297
|
+
/** Set the strip's live VCA gain offset in dB (not persisted to the scene). */
|
|
298
|
+
setVcaOffsetDb(stripIndex, offsetDb) {
|
|
299
|
+
this.mixer.setVcaOffsetDb(stripIndex, offsetDb);
|
|
204
300
|
}
|
|
205
|
-
|
|
206
|
-
|
|
301
|
+
/** Set independent left/right pan positions (dual-pan mode). */
|
|
302
|
+
setDualPan(stripIndex, leftPan, rightPan) {
|
|
303
|
+
this.mixer.setDualPan(stripIndex, leftPan, rightPan);
|
|
207
304
|
}
|
|
208
|
-
|
|
209
|
-
|
|
305
|
+
/**
|
|
306
|
+
* Add a send to a strip after construction.
|
|
307
|
+
*
|
|
308
|
+
* @param stripIndex - Strip index in `[0, stripCount())`
|
|
309
|
+
* @param id - Send id
|
|
310
|
+
* @param destinationBusId - Destination bus id
|
|
311
|
+
* @param sendDb - Initial send level in dB
|
|
312
|
+
* @param timing - `'preFader'` or `'postFader'` (default: `'postFader'`)
|
|
313
|
+
* @returns The new send's index
|
|
314
|
+
*/
|
|
315
|
+
addSend(stripIndex, id, destinationBusId, sendDb = 0, timing = "postFader") {
|
|
316
|
+
return this.mixer.addSend(stripIndex, id, destinationBusId, sendDb, sendTimingCode(timing));
|
|
210
317
|
}
|
|
211
|
-
|
|
212
|
-
|
|
318
|
+
/** Set the send level (in dB) for an existing send by index. */
|
|
319
|
+
setSendDb(stripIndex, sendIndex, sendDb) {
|
|
320
|
+
this.mixer.setSendDb(stripIndex, sendIndex, sendDb);
|
|
213
321
|
}
|
|
214
|
-
|
|
215
|
-
|
|
322
|
+
/**
|
|
323
|
+
* Remove an existing send from a strip by index.
|
|
324
|
+
*
|
|
325
|
+
* Sends are addressed in add order. After removal, sends with a higher index
|
|
326
|
+
* than `sendIndex` shift down by one. Recompile (or process) before reading
|
|
327
|
+
* results so the routing graph rebuilds.
|
|
328
|
+
*
|
|
329
|
+
* @param stripIndex - Strip index in `[0, stripCount())`
|
|
330
|
+
* @param sendIndex - Send index in add order
|
|
331
|
+
*/
|
|
332
|
+
removeSend(stripIndex, sendIndex) {
|
|
333
|
+
this.mixer.removeSend(stripIndex, sendIndex);
|
|
216
334
|
}
|
|
217
|
-
|
|
218
|
-
|
|
335
|
+
/**
|
|
336
|
+
* Read a strip's meter snapshot at the given tap point.
|
|
337
|
+
*
|
|
338
|
+
* @param stripIndex - Strip index in `[0, stripCount())`
|
|
339
|
+
* @param tap - `'preFader'` or `'postFader'` (default: `'postFader'`)
|
|
340
|
+
*/
|
|
341
|
+
meterTap(stripIndex, tap = "postFader") {
|
|
342
|
+
return this.mixer.meterTap(stripIndex, meterTapCode(tap));
|
|
219
343
|
}
|
|
220
|
-
|
|
221
|
-
|
|
344
|
+
/**
|
|
345
|
+
* Read a strip's meter snapshot.
|
|
346
|
+
*
|
|
347
|
+
* With no `tap` argument this reads the strip's own (post-fader) meter,
|
|
348
|
+
* matching the Node/Python tap-less `stripMeter` contract. Pass an optional
|
|
349
|
+
* `tap` (`'preFader'` / `'postFader'`) to read the tap-selectable snapshot
|
|
350
|
+
* instead — the same backing call as {@link meterTap}.
|
|
351
|
+
*
|
|
352
|
+
* @param stripIndex - Strip index in `[0, stripCount())`
|
|
353
|
+
* @param tap - Optional tap point (`'preFader'` / `'postFader'`); when omitted
|
|
354
|
+
* the tap-less post-fader strip meter is read.
|
|
355
|
+
*/
|
|
356
|
+
stripMeter(stripIndex, tap) {
|
|
357
|
+
if (tap === void 0) {
|
|
358
|
+
return this.mixer.stripMeter(stripIndex);
|
|
359
|
+
}
|
|
360
|
+
return this.mixer.meterTap(stripIndex, meterTapCode(tap));
|
|
222
361
|
}
|
|
223
|
-
|
|
224
|
-
|
|
362
|
+
/**
|
|
363
|
+
* Schedule sample-accurate fader automation on a strip.
|
|
364
|
+
*
|
|
365
|
+
* @param stripIndex - Strip index in `[0, stripCount())`
|
|
366
|
+
* @param samplePos - Absolute samples from the start of processing
|
|
367
|
+
* @param faderDb - Target fader level in dB
|
|
368
|
+
* @param curve - Interpolation curve (default: `'linear'`)
|
|
369
|
+
*/
|
|
370
|
+
scheduleFaderAutomation(stripIndex, samplePos, faderDb, curve = "linear") {
|
|
371
|
+
this.mixer.scheduleFaderAutomation(stripIndex, samplePos, faderDb, automationCurveCode(curve));
|
|
225
372
|
}
|
|
226
|
-
|
|
227
|
-
|
|
373
|
+
/**
|
|
374
|
+
* Schedule sample-accurate pan automation on a strip.
|
|
375
|
+
*
|
|
376
|
+
* @param stripIndex - Strip index in `[0, stripCount())`
|
|
377
|
+
* @param samplePos - Absolute samples from the start of processing
|
|
378
|
+
* @param pan - Target pan position
|
|
379
|
+
* @param curve - Interpolation curve (default: `'linear'`)
|
|
380
|
+
*/
|
|
381
|
+
schedulePanAutomation(stripIndex, samplePos, pan, curve = "linear") {
|
|
382
|
+
this.mixer.schedulePanAutomation(stripIndex, samplePos, pan, automationCurveCode(curve));
|
|
228
383
|
}
|
|
229
|
-
|
|
230
|
-
|
|
384
|
+
/**
|
|
385
|
+
* Schedule sample-accurate width automation on a strip.
|
|
386
|
+
*
|
|
387
|
+
* @param stripIndex - Strip index in `[0, stripCount())`
|
|
388
|
+
* @param samplePos - Absolute samples from the start of processing
|
|
389
|
+
* @param width - Target stereo width
|
|
390
|
+
* @param curve - Interpolation curve (default: `'linear'`)
|
|
391
|
+
*/
|
|
392
|
+
scheduleWidthAutomation(stripIndex, samplePos, width, curve = "linear") {
|
|
393
|
+
this.mixer.scheduleWidthAutomation(stripIndex, samplePos, width, automationCurveCode(curve));
|
|
231
394
|
}
|
|
232
|
-
|
|
233
|
-
|
|
395
|
+
/**
|
|
396
|
+
* Schedule sample-accurate send-level automation on a strip's send.
|
|
397
|
+
*
|
|
398
|
+
* @param stripIndex - Strip index in `[0, stripCount())`
|
|
399
|
+
* @param sendIndex - Send index in the strip's add order
|
|
400
|
+
* @param samplePos - Absolute samples from the start of processing
|
|
401
|
+
* @param db - Target send level in dB
|
|
402
|
+
* @param curve - Interpolation curve (default: `'linear'`)
|
|
403
|
+
*/
|
|
404
|
+
scheduleSendAutomation(stripIndex, sendIndex, samplePos, db, curve = "linear") {
|
|
405
|
+
this.mixer.scheduleSendAutomation(
|
|
406
|
+
stripIndex,
|
|
407
|
+
sendIndex,
|
|
408
|
+
samplePos,
|
|
409
|
+
db,
|
|
410
|
+
automationCurveCode(curve)
|
|
411
|
+
);
|
|
234
412
|
}
|
|
235
|
-
|
|
236
|
-
|
|
413
|
+
/**
|
|
414
|
+
* Read up to `maxPoints` of a strip's most recent goniometer samples
|
|
415
|
+
* (oldest to newest).
|
|
416
|
+
*/
|
|
417
|
+
readGoniometerLatest(stripIndex, maxPoints) {
|
|
418
|
+
return this.mixer.readGoniometerLatest(stripIndex, maxPoints);
|
|
237
419
|
}
|
|
238
|
-
|
|
239
|
-
|
|
420
|
+
/** Serialize the current scene (strips, buses, sends, connections) to JSON. */
|
|
421
|
+
toSceneJson() {
|
|
422
|
+
return this.mixer.toSceneJson();
|
|
240
423
|
}
|
|
241
|
-
|
|
242
|
-
|
|
424
|
+
/**
|
|
425
|
+
* Maximum processor tail length (samples) in the compiled mixer graph. Lazily
|
|
426
|
+
* compiles the routing graph if the topology is dirty.
|
|
427
|
+
*/
|
|
428
|
+
tailSamples() {
|
|
429
|
+
return this.mixer.tailSamples();
|
|
243
430
|
}
|
|
244
|
-
|
|
245
|
-
|
|
431
|
+
/**
|
|
432
|
+
* Drain delayed / tail audio by processing a zero-input block of `numSamples`
|
|
433
|
+
* frames after the host stops feeding strip inputs. Returns the mixed stereo
|
|
434
|
+
* master (`left`, `right`, `sampleRate`).
|
|
435
|
+
*/
|
|
436
|
+
drainTailStereo(numSamples) {
|
|
437
|
+
return this.mixer.drainTailStereo(numSamples);
|
|
246
438
|
}
|
|
247
|
-
|
|
248
|
-
|
|
439
|
+
/** Release the underlying WASM object. Safe to call only once. */
|
|
440
|
+
delete() {
|
|
441
|
+
this.mixer.delete();
|
|
249
442
|
}
|
|
443
|
+
/** Alias for {@link delete}, provided for cross-binding (Node) compatibility. */
|
|
250
444
|
destroy() {
|
|
251
|
-
this.
|
|
445
|
+
this.delete();
|
|
252
446
|
}
|
|
253
447
|
};
|
|
448
|
+
|
|
449
|
+
// src/realtime_voice_changer.ts
|
|
254
450
|
var RealtimeVoiceChanger = class {
|
|
255
451
|
constructor(config = "neutral-monitor") {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
}
|
|
259
|
-
this.changer = module.createRealtimeVoiceChanger(config);
|
|
452
|
+
const module2 = getSonareModule();
|
|
453
|
+
this.changer = module2.createRealtimeVoiceChanger(config);
|
|
260
454
|
}
|
|
261
455
|
prepare(sampleRate, maxBlockSize = 128, channels = 1) {
|
|
262
456
|
this.changer.prepare(sampleRate, maxBlockSize, channels);
|
|
@@ -347,23 +541,53 @@ var RealtimeVoiceChanger = class {
|
|
|
347
541
|
* views are reused across calls and become invalid after {@link delete}.
|
|
348
542
|
*/
|
|
349
543
|
createRealtimeMonoBuffer(numSamples) {
|
|
350
|
-
|
|
351
|
-
|
|
544
|
+
let input = this.getMonoInputBuffer(numSamples);
|
|
545
|
+
let output = this.getMonoOutputBuffer(numSamples);
|
|
546
|
+
const reacquireIfDetached = () => {
|
|
547
|
+
if (input.byteLength === 0 || output.byteLength === 0) {
|
|
548
|
+
input = this.getMonoInputBuffer(numSamples);
|
|
549
|
+
output = this.getMonoOutputBuffer(numSamples);
|
|
550
|
+
}
|
|
551
|
+
};
|
|
352
552
|
return {
|
|
353
|
-
input
|
|
354
|
-
|
|
355
|
-
|
|
553
|
+
get input() {
|
|
554
|
+
reacquireIfDetached();
|
|
555
|
+
return input;
|
|
556
|
+
},
|
|
557
|
+
get output() {
|
|
558
|
+
reacquireIfDetached();
|
|
559
|
+
return output;
|
|
560
|
+
},
|
|
561
|
+
process: () => {
|
|
562
|
+
reacquireIfDetached();
|
|
563
|
+
this.processPreparedMono(numSamples);
|
|
564
|
+
}
|
|
356
565
|
};
|
|
357
566
|
}
|
|
358
567
|
/** Same as {@link createRealtimeMonoBuffer} but for interleaved I/O. */
|
|
359
568
|
createRealtimeInterleavedBuffer(numFrames, numChannels) {
|
|
360
|
-
|
|
361
|
-
|
|
569
|
+
let input = this.getInterleavedInputBuffer(numFrames, numChannels);
|
|
570
|
+
let output = this.getInterleavedOutputBuffer(numFrames, numChannels);
|
|
571
|
+
const reacquireIfDetached = () => {
|
|
572
|
+
if (input.byteLength === 0 || output.byteLength === 0) {
|
|
573
|
+
input = this.getInterleavedInputBuffer(numFrames, numChannels);
|
|
574
|
+
output = this.getInterleavedOutputBuffer(numFrames, numChannels);
|
|
575
|
+
}
|
|
576
|
+
};
|
|
362
577
|
return {
|
|
363
|
-
input
|
|
364
|
-
|
|
578
|
+
get input() {
|
|
579
|
+
reacquireIfDetached();
|
|
580
|
+
return input;
|
|
581
|
+
},
|
|
582
|
+
get output() {
|
|
583
|
+
reacquireIfDetached();
|
|
584
|
+
return output;
|
|
585
|
+
},
|
|
365
586
|
channels: numChannels,
|
|
366
|
-
process: () =>
|
|
587
|
+
process: () => {
|
|
588
|
+
reacquireIfDetached();
|
|
589
|
+
this.processPreparedInterleaved(numFrames, numChannels);
|
|
590
|
+
}
|
|
367
591
|
};
|
|
368
592
|
}
|
|
369
593
|
/**
|
|
@@ -373,331 +597,451 @@ var RealtimeVoiceChanger = class {
|
|
|
373
597
|
* become invalid after {@link delete}.
|
|
374
598
|
*/
|
|
375
599
|
createRealtimePlanarBuffer(numFrames, numChannels) {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
channels
|
|
379
|
-
|
|
600
|
+
let channels = [];
|
|
601
|
+
const acquire = () => {
|
|
602
|
+
channels = [];
|
|
603
|
+
for (let ch = 0; ch < numChannels; ch++) {
|
|
604
|
+
channels.push(this.getPlanarChannelBuffer(ch, numFrames));
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
acquire();
|
|
608
|
+
const reacquireIfDetached = () => {
|
|
609
|
+
if ((channels[0]?.byteLength ?? 0) === 0) {
|
|
610
|
+
acquire();
|
|
611
|
+
}
|
|
612
|
+
};
|
|
380
613
|
return {
|
|
381
|
-
channels
|
|
382
|
-
|
|
614
|
+
get channels() {
|
|
615
|
+
reacquireIfDetached();
|
|
616
|
+
return channels;
|
|
617
|
+
},
|
|
618
|
+
process: () => {
|
|
619
|
+
reacquireIfDetached();
|
|
620
|
+
this.processPreparedPlanar(numFrames);
|
|
621
|
+
}
|
|
383
622
|
};
|
|
384
623
|
}
|
|
385
624
|
delete() {
|
|
386
625
|
this.changer.delete();
|
|
387
626
|
}
|
|
388
627
|
};
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
628
|
+
|
|
629
|
+
// src/realtime_engine.ts
|
|
630
|
+
var EXPECTED_ENGINE_ABI_VERSION = 3;
|
|
631
|
+
function engineCapabilities() {
|
|
632
|
+
const abiVersion = getSonareModule().engineAbiVersion();
|
|
633
|
+
const sharedArrayBuffer = typeof globalThis.SharedArrayBuffer === "function";
|
|
634
|
+
const atomics = typeof globalThis.Atomics === "object";
|
|
635
|
+
const audioWorklet = typeof AudioWorkletNode !== "undefined" || typeof globalThis.AudioWorkletProcessor !== "undefined";
|
|
636
|
+
return {
|
|
637
|
+
engineAbiVersion: abiVersion,
|
|
638
|
+
expectedEngineAbiVersion: EXPECTED_ENGINE_ABI_VERSION,
|
|
639
|
+
abiCompatible: abiVersion === EXPECTED_ENGINE_ABI_VERSION,
|
|
640
|
+
sharedArrayBuffer,
|
|
641
|
+
atomics,
|
|
642
|
+
audioWorklet,
|
|
643
|
+
mode: sharedArrayBuffer && atomics ? "sab" : "postMessage"
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
var RealtimeEngine = class {
|
|
647
|
+
nativeExt() {
|
|
648
|
+
return this.native;
|
|
392
649
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
static fromSceneJson(json, sampleRate = 48e3, blockSize = 512) {
|
|
401
|
-
if (!module) {
|
|
402
|
-
throw new Error("Module not initialized. Call init() first.");
|
|
650
|
+
constructor(sampleRate = 48e3, maxBlockSize = 128, commandCapacity = 1024, telemetryCapacity = 1024) {
|
|
651
|
+
const module2 = getSonareModule();
|
|
652
|
+
const capabilities = engineCapabilities();
|
|
653
|
+
if (!capabilities.abiCompatible) {
|
|
654
|
+
throw new Error(
|
|
655
|
+
`Engine ABI mismatch: wasm=${capabilities.engineAbiVersion}, expected=${capabilities.expectedEngineAbiVersion}`
|
|
656
|
+
);
|
|
403
657
|
}
|
|
404
|
-
|
|
658
|
+
this.native = new module2.RealtimeEngine(
|
|
659
|
+
sampleRate,
|
|
660
|
+
maxBlockSize,
|
|
661
|
+
commandCapacity,
|
|
662
|
+
telemetryCapacity
|
|
663
|
+
);
|
|
405
664
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
665
|
+
prepare(sampleRate, maxBlockSize, commandCapacity = 1024, telemetryCapacity = 1024) {
|
|
666
|
+
this.native.prepare(sampleRate, maxBlockSize, commandCapacity, telemetryCapacity);
|
|
667
|
+
}
|
|
668
|
+
/** Queue a sample-accurate parameter change (engine kSetParam). */
|
|
669
|
+
setParameter(paramId, value, renderFrame = -1) {
|
|
670
|
+
this.native.setParameter(paramId, value, renderFrame);
|
|
671
|
+
}
|
|
672
|
+
/** Queue a smoothed parameter change (engine kSetParamSmoothed). */
|
|
673
|
+
setParameterSmoothed(paramId, value, renderFrame = -1) {
|
|
674
|
+
this.native.setParameterSmoothed(paramId, value, renderFrame);
|
|
675
|
+
}
|
|
676
|
+
setBuiltinInstrument(config = {}, destinationId = config.destinationId ?? 0) {
|
|
677
|
+
this.nativeExt().setBuiltinInstrument(destinationId, config);
|
|
409
678
|
}
|
|
410
679
|
/**
|
|
411
|
-
*
|
|
412
|
-
*
|
|
413
|
-
*
|
|
414
|
-
* @
|
|
415
|
-
*
|
|
680
|
+
* Bind the patch-driven NativeSynth to a realtime MIDI destination. `patch`
|
|
681
|
+
* is a {@link SynthPatch} or a preset-name string (`'saw-lead'` /
|
|
682
|
+
* `'va:saw-lead'`; see {@link synthPresetNames}), resolving exactly like
|
|
683
|
+
* {@link Project.bounceWithSynthInstrument}. Live note/CC commands and
|
|
684
|
+
* scheduled MIDI clips routed to that destination render through the synth.
|
|
685
|
+
* Unknown preset names throw. An object patch's `destinationId` is a JS
|
|
686
|
+
* binding convenience, not part of the NativeSynth patch itself.
|
|
416
687
|
*/
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
throw new Error("leftChannels and rightChannels must have the same length.");
|
|
420
|
-
}
|
|
421
|
-
return this.mixer.processStereo(leftChannels, rightChannels);
|
|
688
|
+
setSynthInstrument(patch = {}, destinationId = (typeof patch === "object" ? patch.destinationId : void 0) ?? 0) {
|
|
689
|
+
this.nativeExt().setSynthInstrument(destinationId, patch);
|
|
422
690
|
}
|
|
423
691
|
/**
|
|
424
|
-
*
|
|
425
|
-
*
|
|
426
|
-
*
|
|
427
|
-
*
|
|
428
|
-
* must match the scene strip count and all arrays must have the same length.
|
|
692
|
+
* Load (parse) SoundFont 2 bytes into the engine so SF2 instruments can be
|
|
693
|
+
* bound with {@link setSf2Instrument}. The host fetches the `.sf2` and
|
|
694
|
+
* passes the raw bytes; they are copied into linear memory for the call and
|
|
695
|
+
* not referenced afterwards. Replaces any previously loaded SoundFont.
|
|
429
696
|
*/
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
throw new Error("leftChannels and rightChannels must have the same length.");
|
|
433
|
-
}
|
|
434
|
-
if (outLeft.length !== outRight.length) {
|
|
435
|
-
throw new Error("outLeft and outRight must have the same length.");
|
|
436
|
-
}
|
|
437
|
-
this.mixer.processStereoInto(leftChannels, rightChannels, outLeft, outRight);
|
|
697
|
+
loadSoundFont(data) {
|
|
698
|
+
this.nativeExt().loadSoundFont(data);
|
|
438
699
|
}
|
|
439
700
|
/**
|
|
440
|
-
*
|
|
441
|
-
*
|
|
442
|
-
*
|
|
443
|
-
*
|
|
444
|
-
*
|
|
701
|
+
* Bind a GS-compatible SoundFont player to a realtime MIDI destination, fed
|
|
702
|
+
* by the engine's loaded SoundFont ({@link loadSoundFont}). Live note/CC
|
|
703
|
+
* commands and scheduled MIDI clips routed to that destination render
|
|
704
|
+
* through the player (16 MIDI channels, channel 10 drums, GS NRPN part
|
|
705
|
+
* edits, GS/GM SysEx resets). Without a loaded SoundFont — or for programs
|
|
706
|
+
* the SoundFont does not cover — notes play through the built-in
|
|
707
|
+
* synthesizer GM fallback bank (the data-free floor).
|
|
445
708
|
*/
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
const leftInputs = [];
|
|
449
|
-
const rightInputs = [];
|
|
450
|
-
for (let index = 0; index < stripCount; index++) {
|
|
451
|
-
leftInputs.push(this.mixer.inputLeftView(index));
|
|
452
|
-
rightInputs.push(this.mixer.inputRightView(index));
|
|
453
|
-
}
|
|
454
|
-
const outLeft = this.mixer.outputLeftView();
|
|
455
|
-
const outRight = this.mixer.outputRightView();
|
|
456
|
-
return {
|
|
457
|
-
leftInputs,
|
|
458
|
-
rightInputs,
|
|
459
|
-
outLeft,
|
|
460
|
-
outRight,
|
|
461
|
-
process: (numSamples = outLeft.length) => this.mixer.processPreparedStereo(numSamples)
|
|
462
|
-
};
|
|
709
|
+
setSf2Instrument(config = {}, destinationId = config.destinationId ?? 0) {
|
|
710
|
+
this.nativeExt().setSf2Instrument(destinationId, config);
|
|
463
711
|
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
712
|
+
clearMidiInstrument(destinationId = 0) {
|
|
713
|
+
this.nativeExt().clearMidiInstrument(destinationId);
|
|
714
|
+
}
|
|
715
|
+
midiInstrumentCount() {
|
|
716
|
+
return this.nativeExt().midiInstrumentCount();
|
|
467
717
|
}
|
|
468
718
|
/**
|
|
469
|
-
*
|
|
470
|
-
*
|
|
471
|
-
*
|
|
472
|
-
* @param insertIndex - Index into the strip's combined insert sequence
|
|
473
|
-
* (`[pre-inserts... post-inserts...]`)
|
|
474
|
-
* @param paramId - Processor-specific parameter id
|
|
475
|
-
* @param samplePos - Absolute samples from the start of processing (the mixer
|
|
476
|
-
* advances an internal position from 0 on the first {@link processStereo}
|
|
477
|
-
* call; recompiling resets it to 0)
|
|
478
|
-
* @param value - Target parameter value
|
|
479
|
-
* @param curve - Interpolation curve (default: `'linear'`)
|
|
480
|
-
* @throws If the strip index is out of range or the schedule call fails
|
|
481
|
-
* (unknown curve, out-of-range insert index, or full event lane)
|
|
719
|
+
* Bind a live MIDI CC to an engine automation parameter. The MIDI event still
|
|
720
|
+
* reaches the destination instrument; when bound, its 7-bit value is also
|
|
721
|
+
* mapped into [minValue, maxValue] for `paramId`.
|
|
482
722
|
*/
|
|
483
|
-
|
|
484
|
-
this.
|
|
485
|
-
|
|
486
|
-
|
|
723
|
+
bindMidiCc(channel, controller, paramId, options = {}) {
|
|
724
|
+
this.nativeExt().bindMidiCc(
|
|
725
|
+
channel,
|
|
726
|
+
controller,
|
|
487
727
|
paramId,
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
automationCurveCode(curve)
|
|
728
|
+
options.minValue ?? 0,
|
|
729
|
+
options.maxValue ?? 1
|
|
491
730
|
);
|
|
492
731
|
}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
* when no strip with that id exists (matches the Node binding's `number | null`).
|
|
496
|
-
*/
|
|
497
|
-
stripById(id) {
|
|
498
|
-
const index = this.mixer.stripById(id);
|
|
499
|
-
return index < 0 ? null : index;
|
|
732
|
+
clearMidiCcBindings() {
|
|
733
|
+
this.nativeExt().clearMidiCcBindings();
|
|
500
734
|
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
* `'submix'` (defaults to `'aux'`). Marks the routing graph dirty; call
|
|
504
|
-
* {@link compile} (or {@link processStereo}) to rebuild.
|
|
505
|
-
*/
|
|
506
|
-
addBus(id, role = "aux") {
|
|
507
|
-
this.mixer.addBus(id, role);
|
|
735
|
+
midiCcBindingCount() {
|
|
736
|
+
return this.nativeExt().midiCcBindingCount();
|
|
508
737
|
}
|
|
509
|
-
/**
|
|
510
|
-
|
|
511
|
-
this.
|
|
738
|
+
/** Install/replace a live non-destructive MIDI-FX insert for one destination. */
|
|
739
|
+
setMidiFx(destinationId, configJson) {
|
|
740
|
+
this.nativeExt().setMidiFx(destinationId, configJson);
|
|
512
741
|
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
return this.mixer.busCount();
|
|
742
|
+
clearMidiFx(destinationId = 0) {
|
|
743
|
+
this.nativeExt().clearMidiFx(destinationId);
|
|
516
744
|
}
|
|
517
|
-
/**
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
*/
|
|
521
|
-
addVcaGroup(id, gainDb = 0, members = []) {
|
|
522
|
-
this.mixer.addVcaGroup(id, gainDb, members);
|
|
745
|
+
/** Enable the engine-owned live MIDI input source for a destination. */
|
|
746
|
+
setMidiInputSource(destinationId = 0) {
|
|
747
|
+
this.nativeExt().setMidiInputSource(destinationId);
|
|
523
748
|
}
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
this.mixer.removeVcaGroup(id);
|
|
749
|
+
clearMidiInputSource() {
|
|
750
|
+
this.nativeExt().clearMidiInputSource();
|
|
527
751
|
}
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
return this.mixer.vcaGroupCount();
|
|
752
|
+
midiInputPendingCount() {
|
|
753
|
+
return this.nativeExt().midiInputPendingCount();
|
|
531
754
|
}
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
this.mixer.setInputTrimDb(stripIndex, db);
|
|
755
|
+
pushMidiInputNoteOn(group, channel, note, velocity, portTimeSamples = 0) {
|
|
756
|
+
this.nativeExt().pushMidiInputNoteOn(group, channel, note, velocity, portTimeSamples);
|
|
535
757
|
}
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
this.mixer.setFaderDb(stripIndex, db);
|
|
758
|
+
pushMidiInputNoteOff(group, channel, note, velocity = 0, portTimeSamples = 0) {
|
|
759
|
+
this.nativeExt().pushMidiInputNoteOff(group, channel, note, velocity, portTimeSamples);
|
|
539
760
|
}
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
this.mixer.setPan(stripIndex, pan, panModeCode(panMode));
|
|
761
|
+
pushMidiInputCc(group, channel, controller, value, portTimeSamples = 0) {
|
|
762
|
+
this.nativeExt().pushMidiInputCc(group, channel, controller, value, portTimeSamples);
|
|
543
763
|
}
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
this.mixer.setWidth(stripIndex, width);
|
|
764
|
+
pushMidiNoteOn(destinationId, group, channel, note, velocity, renderFrame = -1) {
|
|
765
|
+
this.nativeExt().pushMidiNoteOn(destinationId, group, channel, note, velocity, renderFrame);
|
|
547
766
|
}
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
this.mixer.setMuted(stripIndex, muted);
|
|
767
|
+
pushMidiNoteOff(destinationId, group, channel, note, velocity = 0, renderFrame = -1) {
|
|
768
|
+
this.nativeExt().pushMidiNoteOff(destinationId, group, channel, note, velocity, renderFrame);
|
|
551
769
|
}
|
|
552
770
|
/**
|
|
553
|
-
*
|
|
554
|
-
*
|
|
771
|
+
* Queue an immediate (live) MIDI control change to a MIDI destination
|
|
772
|
+
* (engine kMidiCcImmediate). `group`/`channel` are 0..15; `controller`/`value`
|
|
773
|
+
* are 7-bit (0..127). `renderFrame` is the frame to fire at, or -1 for
|
|
774
|
+
* immediate. Mirrors the Node/Python/C-ABI `pushMidiCc`.
|
|
555
775
|
*/
|
|
556
|
-
|
|
557
|
-
this.
|
|
776
|
+
pushMidiCc(destinationId, group, channel, controller, value, renderFrame = -1) {
|
|
777
|
+
this.nativeExt().pushMidiCc(destinationId, group, channel, controller, value, renderFrame);
|
|
558
778
|
}
|
|
559
779
|
/**
|
|
560
|
-
*
|
|
561
|
-
*
|
|
780
|
+
* Queue a MIDI panic (all-notes-off) releasing every sounding note at
|
|
781
|
+
* `renderFrame` (-1 = immediate). Mirrors the C-ABI `pushMidiPanic`.
|
|
562
782
|
*/
|
|
563
|
-
|
|
564
|
-
this.
|
|
565
|
-
}
|
|
566
|
-
/** Invert the polarity of the left and/or right channel of a strip. */
|
|
567
|
-
setPolarityInvert(stripIndex, invertLeft, invertRight) {
|
|
568
|
-
this.mixer.setPolarityInvert(stripIndex, invertLeft, invertRight);
|
|
569
|
-
}
|
|
570
|
-
/** Set the strip's pan law. */
|
|
571
|
-
setPanLaw(stripIndex, panLaw) {
|
|
572
|
-
this.mixer.setPanLaw(stripIndex, panLawCode(panLaw));
|
|
783
|
+
pushMidiPanic(renderFrame = -1) {
|
|
784
|
+
this.nativeExt().pushMidiPanic(renderFrame);
|
|
573
785
|
}
|
|
574
786
|
/**
|
|
575
|
-
*
|
|
576
|
-
*
|
|
787
|
+
* Remove all registered parameters (and their automation lanes). Control-thread
|
|
788
|
+
* only; not realtime-safe. Mirrors the C-ABI `clearParameters`.
|
|
577
789
|
*/
|
|
578
|
-
|
|
579
|
-
this.
|
|
790
|
+
clearParameters() {
|
|
791
|
+
this.nativeExt().clearParameters();
|
|
580
792
|
}
|
|
581
|
-
/**
|
|
582
|
-
|
|
583
|
-
this.
|
|
793
|
+
/** Read back the current transport state snapshot. */
|
|
794
|
+
getTransportState() {
|
|
795
|
+
return this.native.getTransportState();
|
|
584
796
|
}
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
this.mixer.setDualPan(stripIndex, leftPan, rightPan);
|
|
797
|
+
play(renderFrame = -1) {
|
|
798
|
+
this.native.play(renderFrame);
|
|
588
799
|
}
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
800
|
+
stop(renderFrame = -1) {
|
|
801
|
+
this.native.stop(renderFrame);
|
|
802
|
+
}
|
|
803
|
+
seekSample(timelineSample, renderFrame = -1) {
|
|
804
|
+
this.native.seekSample(timelineSample, renderFrame);
|
|
805
|
+
}
|
|
806
|
+
seekPpq(ppq, renderFrame = -1) {
|
|
807
|
+
this.native.seekPpq(ppq, renderFrame);
|
|
808
|
+
}
|
|
809
|
+
setTempo(bpm) {
|
|
810
|
+
this.native.setTempo(bpm);
|
|
811
|
+
}
|
|
812
|
+
setTimeSignature(numerator, denominator) {
|
|
813
|
+
this.native.setTimeSignature(numerator, denominator);
|
|
814
|
+
}
|
|
815
|
+
setLoop(startPpq, endPpq, enabled = true) {
|
|
816
|
+
this.native.setLoop(startPpq, endPpq, enabled);
|
|
817
|
+
}
|
|
818
|
+
addParameter(info) {
|
|
819
|
+
this.native.addParameter(info);
|
|
820
|
+
}
|
|
821
|
+
parameterCount() {
|
|
822
|
+
return this.native.parameterCount();
|
|
823
|
+
}
|
|
824
|
+
parameterInfoByIndex(index) {
|
|
825
|
+
return this.native.parameterInfoByIndex(index);
|
|
826
|
+
}
|
|
827
|
+
parameterInfo(id) {
|
|
828
|
+
return this.native.parameterInfo(id);
|
|
829
|
+
}
|
|
830
|
+
setAutomationLane(paramId, points) {
|
|
831
|
+
this.native.setAutomationLane(paramId, points);
|
|
832
|
+
}
|
|
833
|
+
automationLaneCount() {
|
|
834
|
+
return this.native.automationLaneCount();
|
|
835
|
+
}
|
|
836
|
+
setMarkers(markers) {
|
|
837
|
+
this.native.setMarkers(markers);
|
|
838
|
+
}
|
|
839
|
+
markerCount() {
|
|
840
|
+
return this.native.markerCount();
|
|
841
|
+
}
|
|
842
|
+
markerByIndex(index) {
|
|
843
|
+
return this.native.markerByIndex(index);
|
|
844
|
+
}
|
|
845
|
+
marker(id) {
|
|
846
|
+
return this.native.marker(id);
|
|
847
|
+
}
|
|
848
|
+
seekMarker(markerId, renderFrame = -1) {
|
|
849
|
+
this.native.seekMarker(markerId, renderFrame);
|
|
850
|
+
}
|
|
851
|
+
setLoopFromMarkers(startMarkerId, endMarkerId) {
|
|
852
|
+
this.native.setLoopFromMarkers(startMarkerId, endMarkerId);
|
|
853
|
+
}
|
|
854
|
+
setMetronome(config) {
|
|
855
|
+
this.native.setMetronome(config);
|
|
856
|
+
}
|
|
857
|
+
metronome() {
|
|
858
|
+
return this.native.metronome();
|
|
859
|
+
}
|
|
860
|
+
countInEndSample(startSample, bars) {
|
|
861
|
+
return Number(this.native.countInEndSample(startSample, bars));
|
|
862
|
+
}
|
|
863
|
+
setGraph(spec) {
|
|
864
|
+
this.native.setGraph(spec);
|
|
865
|
+
}
|
|
866
|
+
graphNodeCount() {
|
|
867
|
+
return this.native.graphNodeCount();
|
|
868
|
+
}
|
|
869
|
+
graphConnectionCount() {
|
|
870
|
+
return this.native.graphConnectionCount();
|
|
871
|
+
}
|
|
872
|
+
setClips(clips) {
|
|
873
|
+
this.native.setClips(
|
|
874
|
+
clips.map((clip) => ({
|
|
875
|
+
...clip,
|
|
876
|
+
pageProvider: typeof clip.pageProvider === "object" && clip.pageProvider !== null ? clip.pageProvider.id : clip.pageProvider
|
|
877
|
+
}))
|
|
878
|
+
);
|
|
879
|
+
}
|
|
880
|
+
clipCount() {
|
|
881
|
+
return this.native.clipCount();
|
|
882
|
+
}
|
|
883
|
+
createClipPageProvider(numChannels, numSamples, pageFrames) {
|
|
884
|
+
const id = this.nativeExt().createClipPageProvider(numChannels, numSamples, pageFrames);
|
|
885
|
+
return new ClipPageProvider(this, id);
|
|
886
|
+
}
|
|
887
|
+
supplyClipPage(providerId, pageIndex, channels) {
|
|
888
|
+
this.nativeExt().supplyClipPage(providerId, pageIndex, channels);
|
|
889
|
+
}
|
|
890
|
+
clearClipPage(providerId, pageIndex) {
|
|
891
|
+
this.nativeExt().clearClipPage(providerId, pageIndex);
|
|
892
|
+
}
|
|
893
|
+
destroyClipPageProvider(providerId) {
|
|
894
|
+
this.nativeExt().destroyClipPageProvider(providerId);
|
|
895
|
+
}
|
|
896
|
+
popClipPageRequest() {
|
|
897
|
+
return this.nativeExt().popClipPageRequest();
|
|
898
|
+
}
|
|
899
|
+
setCaptureBuffer(numChannels, capacityFrames) {
|
|
900
|
+
this.native.setCaptureBuffer(numChannels, capacityFrames);
|
|
901
|
+
}
|
|
902
|
+
armCapture(armed = true) {
|
|
903
|
+
this.native.armCapture(armed);
|
|
904
|
+
}
|
|
905
|
+
setCapturePunch(startSample, endSample, enabled = true) {
|
|
906
|
+
this.native.setCapturePunch(startSample, endSample, enabled);
|
|
907
|
+
}
|
|
908
|
+
setCaptureSource(source) {
|
|
909
|
+
this.native.setCaptureSource(source);
|
|
910
|
+
}
|
|
911
|
+
setRecordOffsetSamples(offsetSamples) {
|
|
912
|
+
this.native.setRecordOffsetSamples(offsetSamples);
|
|
913
|
+
}
|
|
914
|
+
setInputMonitor(enabled, gain = 1) {
|
|
915
|
+
this.native.setInputMonitor(enabled, gain);
|
|
916
|
+
}
|
|
917
|
+
resetCapture() {
|
|
918
|
+
this.native.resetCapture();
|
|
919
|
+
}
|
|
920
|
+
captureStatus() {
|
|
921
|
+
return this.native.captureStatus();
|
|
601
922
|
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
this.mixer.setSendDb(stripIndex, sendIndex, sendDb);
|
|
923
|
+
capturedAudio() {
|
|
924
|
+
return this.native.capturedAudio();
|
|
605
925
|
}
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
*
|
|
609
|
-
* @param stripIndex - Strip index in `[0, stripCount())`
|
|
610
|
-
* @param tap - `'preFader'` or `'postFader'` (default: `'postFader'`)
|
|
611
|
-
*/
|
|
612
|
-
meterTap(stripIndex, tap = "postFader") {
|
|
613
|
-
return this.mixer.meterTap(stripIndex, meterTapCode(tap));
|
|
926
|
+
process(channels) {
|
|
927
|
+
return this.native.process(channels);
|
|
614
928
|
}
|
|
615
929
|
/**
|
|
616
|
-
*
|
|
617
|
-
*
|
|
618
|
-
*
|
|
619
|
-
*
|
|
620
|
-
* @param tap - `'preFader'` or `'postFader'` (default: `'postFader'`)
|
|
930
|
+
* Allocates persistent per-channel WASM-heap scratch for the zero-copy
|
|
931
|
+
* `getChannelBuffer` / `processPrepared` realtime path. Call once (off the
|
|
932
|
+
* audio thread) before driving `processPrepared` from an AudioWorklet so the
|
|
933
|
+
* render callback never allocates on the C++/JS heap.
|
|
621
934
|
*/
|
|
622
|
-
|
|
623
|
-
|
|
935
|
+
prepareChannels(numChannels, maxFrames) {
|
|
936
|
+
this.native.prepareChannels(numChannels, maxFrames);
|
|
624
937
|
}
|
|
625
938
|
/**
|
|
626
|
-
*
|
|
627
|
-
*
|
|
628
|
-
*
|
|
629
|
-
* @param samplePos - Absolute samples from the start of processing
|
|
630
|
-
* @param faderDb - Target fader level in dB
|
|
631
|
-
* @param curve - Interpolation curve (default: `'linear'`)
|
|
939
|
+
* Returns a Float32Array view onto the persistent WASM-heap scratch for one
|
|
940
|
+
* channel (valid for up to `numFrames`). Fill it, call `processPrepared`, then
|
|
941
|
+
* read the same view back. Re-acquire after WASM memory growth.
|
|
632
942
|
*/
|
|
633
|
-
|
|
634
|
-
this.
|
|
943
|
+
getChannelBuffer(channel, numFrames) {
|
|
944
|
+
return this.native.getChannelBuffer(channel, numFrames);
|
|
635
945
|
}
|
|
636
946
|
/**
|
|
637
|
-
*
|
|
638
|
-
*
|
|
639
|
-
*
|
|
640
|
-
* @param samplePos - Absolute samples from the start of processing
|
|
641
|
-
* @param pan - Target pan position
|
|
642
|
-
* @param curve - Interpolation curve (default: `'linear'`)
|
|
947
|
+
* Runs the engine in place over the prepared per-channel scratch buffers.
|
|
948
|
+
* Allocation-free: safe to call on the AudioWorklet render thread after
|
|
949
|
+
* `prepareChannels`.
|
|
643
950
|
*/
|
|
644
|
-
|
|
645
|
-
this.
|
|
951
|
+
processPrepared(numFrames) {
|
|
952
|
+
this.native.processPrepared(numFrames);
|
|
646
953
|
}
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
*
|
|
650
|
-
* @param stripIndex - Strip index in `[0, stripCount())`
|
|
651
|
-
* @param samplePos - Absolute samples from the start of processing
|
|
652
|
-
* @param width - Target stereo width
|
|
653
|
-
* @param curve - Interpolation curve (default: `'linear'`)
|
|
654
|
-
*/
|
|
655
|
-
scheduleWidthAutomation(stripIndex, samplePos, width, curve = "linear") {
|
|
656
|
-
this.mixer.scheduleWidthAutomation(stripIndex, samplePos, width, automationCurveCode(curve));
|
|
954
|
+
processWithMonitor(channels) {
|
|
955
|
+
return this.native.processWithMonitor(channels);
|
|
657
956
|
}
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
*
|
|
661
|
-
* @param stripIndex - Strip index in `[0, stripCount())`
|
|
662
|
-
* @param sendIndex - Send index in the strip's add order
|
|
663
|
-
* @param samplePos - Absolute samples from the start of processing
|
|
664
|
-
* @param db - Target send level in dB
|
|
665
|
-
* @param curve - Interpolation curve (default: `'linear'`)
|
|
666
|
-
*/
|
|
667
|
-
scheduleSendAutomation(stripIndex, sendIndex, samplePos, db, curve = "linear") {
|
|
668
|
-
this.mixer.scheduleSendAutomation(
|
|
669
|
-
stripIndex,
|
|
670
|
-
sendIndex,
|
|
671
|
-
samplePos,
|
|
672
|
-
db,
|
|
673
|
-
automationCurveCode(curve)
|
|
674
|
-
);
|
|
957
|
+
renderOffline(channels, blockSize = 128) {
|
|
958
|
+
return this.native.renderOffline(channels, blockSize);
|
|
675
959
|
}
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
* (oldest to newest).
|
|
679
|
-
*/
|
|
680
|
-
readGoniometerLatest(stripIndex, maxPoints) {
|
|
681
|
-
return this.mixer.readGoniometerLatest(stripIndex, maxPoints);
|
|
960
|
+
bounceOffline(options) {
|
|
961
|
+
return this.native.bounceOffline(options);
|
|
682
962
|
}
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
return this.mixer.toSceneJson();
|
|
963
|
+
freezeOffline(options) {
|
|
964
|
+
return this.native.freezeOffline(options);
|
|
686
965
|
}
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
966
|
+
drainTelemetry(maxRecords = 1024) {
|
|
967
|
+
return this.native.drainTelemetry(maxRecords);
|
|
968
|
+
}
|
|
969
|
+
drainMeterTelemetry(maxRecords = 1024) {
|
|
970
|
+
return this.native.drainMeterTelemetry(maxRecords);
|
|
690
971
|
}
|
|
691
|
-
/** Alias for {@link delete}, provided for cross-binding (Node) compatibility. */
|
|
692
972
|
destroy() {
|
|
693
|
-
this.delete();
|
|
973
|
+
this.native.delete();
|
|
974
|
+
}
|
|
975
|
+
};
|
|
976
|
+
var ClipPageProvider = class {
|
|
977
|
+
constructor(engine, id) {
|
|
978
|
+
this.engine = engine;
|
|
979
|
+
this.id = id;
|
|
980
|
+
this.disposed = false;
|
|
981
|
+
}
|
|
982
|
+
supply(pageIndex, channels) {
|
|
983
|
+
if (this.disposed) {
|
|
984
|
+
throw new Error("ClipPageProvider is destroyed");
|
|
985
|
+
}
|
|
986
|
+
this.engine.supplyClipPage(this.id, pageIndex, channels);
|
|
987
|
+
}
|
|
988
|
+
clear(pageIndex) {
|
|
989
|
+
if (this.disposed) {
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
this.engine.clearClipPage(this.id, pageIndex);
|
|
993
|
+
}
|
|
994
|
+
destroy() {
|
|
995
|
+
if (this.disposed) {
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
this.disposed = true;
|
|
999
|
+
this.engine.destroyClipPageProvider(this.id);
|
|
694
1000
|
}
|
|
695
1001
|
};
|
|
696
1002
|
|
|
1003
|
+
// src/index.ts
|
|
1004
|
+
var module = null;
|
|
1005
|
+
var initPromise = null;
|
|
1006
|
+
async function init(options) {
|
|
1007
|
+
if (module) {
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
if (initPromise) {
|
|
1011
|
+
return initPromise;
|
|
1012
|
+
}
|
|
1013
|
+
initPromise = (async () => {
|
|
1014
|
+
try {
|
|
1015
|
+
const createModule = (await import("./sonare.js")).default;
|
|
1016
|
+
module = await createModule(options);
|
|
1017
|
+
setSonareModule(module);
|
|
1018
|
+
} catch (error) {
|
|
1019
|
+
initPromise = null;
|
|
1020
|
+
throw error;
|
|
1021
|
+
}
|
|
1022
|
+
})();
|
|
1023
|
+
return initPromise;
|
|
1024
|
+
}
|
|
1025
|
+
function isInitialized() {
|
|
1026
|
+
return module !== null;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
697
1029
|
// src/worklet.ts
|
|
698
1030
|
var SONARE_METER_RING_HEADER_INTS = 4;
|
|
699
|
-
var SONARE_METER_RING_RECORD_FLOATS =
|
|
1031
|
+
var SONARE_METER_RING_RECORD_FLOATS = 7;
|
|
700
1032
|
var SONARE_SPECTRUM_RING_HEADER_INTS = 5;
|
|
1033
|
+
var SONARE_FRAME_LANE_BASE = 16777216;
|
|
1034
|
+
function encodeFrameLo(frame) {
|
|
1035
|
+
const f = Math.max(0, Math.floor(frame));
|
|
1036
|
+
return f % SONARE_FRAME_LANE_BASE;
|
|
1037
|
+
}
|
|
1038
|
+
function encodeFrameHi(frame) {
|
|
1039
|
+
const f = Math.max(0, Math.floor(frame));
|
|
1040
|
+
return Math.floor(f / SONARE_FRAME_LANE_BASE);
|
|
1041
|
+
}
|
|
1042
|
+
function decodeFrame(lo, hi) {
|
|
1043
|
+
return hi * SONARE_FRAME_LANE_BASE + lo;
|
|
1044
|
+
}
|
|
701
1045
|
var SONARE_ENGINE_RING_HEADER_INTS = 5;
|
|
702
1046
|
var SONARE_ENGINE_COMMAND_RECORD_BYTES = 32;
|
|
703
1047
|
var SONARE_ENGINE_TELEMETRY_RECORD_BYTES = 48;
|
|
@@ -744,6 +1088,18 @@ var SonareEngineTelemetryError = /* @__PURE__ */ ((SonareEngineTelemetryError2)
|
|
|
744
1088
|
SonareEngineTelemetryError2[SonareEngineTelemetryError2["SmoothedParameterCapacity"] = 13] = "SmoothedParameterCapacity";
|
|
745
1089
|
return SonareEngineTelemetryError2;
|
|
746
1090
|
})(SonareEngineTelemetryError || {});
|
|
1091
|
+
var DEFAULT_METRONOME_CONFIG = {
|
|
1092
|
+
beatGain: 0.35,
|
|
1093
|
+
accentGain: 0.7,
|
|
1094
|
+
clickSamples: 96
|
|
1095
|
+
};
|
|
1096
|
+
function resolveMetronomeConfig(config) {
|
|
1097
|
+
return {
|
|
1098
|
+
beatGain: config.beatGain ?? DEFAULT_METRONOME_CONFIG.beatGain,
|
|
1099
|
+
accentGain: config.accentGain ?? DEFAULT_METRONOME_CONFIG.accentGain,
|
|
1100
|
+
clickSamples: config.clickSamples ?? DEFAULT_METRONOME_CONFIG.clickSamples
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
747
1103
|
function toDb(value) {
|
|
748
1104
|
return value > 0 ? 20 * Math.log10(value) : Number.NEGATIVE_INFINITY;
|
|
749
1105
|
}
|
|
@@ -759,6 +1115,12 @@ function isWorkletMessage(value) {
|
|
|
759
1115
|
function isEngineCommandRecord(value) {
|
|
760
1116
|
return isRecord(value) && typeof value.type === "number";
|
|
761
1117
|
}
|
|
1118
|
+
function isEngineSyncMessage(value) {
|
|
1119
|
+
if (!isRecord(value) || typeof value.type !== "string") {
|
|
1120
|
+
return false;
|
|
1121
|
+
}
|
|
1122
|
+
return value.type === "syncClips" || value.type === "syncMarkers" || value.type === "syncMetronome" || value.type === "syncAutomation";
|
|
1123
|
+
}
|
|
762
1124
|
function isRealtimeVoiceChangerMessage(value) {
|
|
763
1125
|
if (!isRecord(value) || typeof value.type !== "string") {
|
|
764
1126
|
return false;
|
|
@@ -794,7 +1156,7 @@ function readSonareMeterRingBuffer(ring, readIndex = 0) {
|
|
|
794
1156
|
const offset = index % ring.capacity * SONARE_METER_RING_RECORD_FLOATS;
|
|
795
1157
|
meters.push({
|
|
796
1158
|
type: "meter",
|
|
797
|
-
frame: ring.records[offset],
|
|
1159
|
+
frame: decodeFrame(ring.records[offset], ring.records[offset + 6]),
|
|
798
1160
|
peakDbL: ring.records[offset + 1],
|
|
799
1161
|
peakDbR: ring.records[offset + 2],
|
|
800
1162
|
rmsDbL: ring.records[offset + 3],
|
|
@@ -807,7 +1169,7 @@ function readSonareMeterRingBuffer(ring, readIndex = 0) {
|
|
|
807
1169
|
function sonareSpectrumRingBufferByteLength(capacity, bands = 16) {
|
|
808
1170
|
const clampedCapacity = Math.max(1, Math.floor(capacity));
|
|
809
1171
|
const clampedBands = Math.max(1, Math.floor(bands));
|
|
810
|
-
return SONARE_SPECTRUM_RING_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT + clampedCapacity * (
|
|
1172
|
+
return SONARE_SPECTRUM_RING_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT + clampedCapacity * (3 + clampedBands) * Float32Array.BYTES_PER_ELEMENT;
|
|
811
1173
|
}
|
|
812
1174
|
function createSonareSpectrumRingBuffer(capacity = 128, bands = 16) {
|
|
813
1175
|
const clampedCapacity = Math.max(1, Math.floor(capacity));
|
|
@@ -831,7 +1193,7 @@ function createSonareSpectrumRingBuffer(capacity = 128, bands = 16) {
|
|
|
831
1193
|
}
|
|
832
1194
|
function readSonareSpectrumRingBuffer(ring, readIndex = 0) {
|
|
833
1195
|
const writeIndex = Atomics.load(ring.header, 0);
|
|
834
|
-
const recordFloats = Atomics.load(ring.header, 2) ||
|
|
1196
|
+
const recordFloats = Atomics.load(ring.header, 2) || 3 + ring.bands;
|
|
835
1197
|
const bands = Atomics.load(ring.header, 3) || ring.bands;
|
|
836
1198
|
const nextReadIndex = Math.max(0, Math.min(readIndex, writeIndex));
|
|
837
1199
|
const firstReadable = Math.max(nextReadIndex, writeIndex - ring.capacity);
|
|
@@ -839,8 +1201,12 @@ function readSonareSpectrumRingBuffer(ring, readIndex = 0) {
|
|
|
839
1201
|
for (let index = firstReadable; index < writeIndex; index++) {
|
|
840
1202
|
const offset = index % ring.capacity * recordFloats;
|
|
841
1203
|
const values = new Float32Array(bands);
|
|
842
|
-
values.set(ring.records.subarray(offset +
|
|
843
|
-
spectra.push({
|
|
1204
|
+
values.set(ring.records.subarray(offset + 3, offset + 3 + bands));
|
|
1205
|
+
spectra.push({
|
|
1206
|
+
type: "spectrum",
|
|
1207
|
+
frame: decodeFrame(ring.records[offset], ring.records[offset + 1]),
|
|
1208
|
+
bands: values
|
|
1209
|
+
});
|
|
844
1210
|
}
|
|
845
1211
|
return { nextReadIndex: writeIndex, spectra };
|
|
846
1212
|
}
|
|
@@ -959,7 +1325,7 @@ function spectrumRingFromSharedBuffer(sharedBuffer, fallbackCapacity, fallbackBa
|
|
|
959
1325
|
const existingBands = Atomics.load(header, 3);
|
|
960
1326
|
const capacity = Math.max(1, Math.floor(existingCapacity || fallbackCapacity || 1));
|
|
961
1327
|
const bands = Math.max(1, Math.floor(existingBands || fallbackBands || 16));
|
|
962
|
-
const recordFloats =
|
|
1328
|
+
const recordFloats = 3 + bands;
|
|
963
1329
|
const minBytes = sonareSpectrumRingBufferByteLength(capacity, bands);
|
|
964
1330
|
if (sharedBuffer.byteLength < minBytes) {
|
|
965
1331
|
throw new Error("spectrumSharedBuffer is too small for the requested ring capacity.");
|
|
@@ -1008,8 +1374,7 @@ function writeEngineCommandRecord(view, offset, command) {
|
|
|
1008
1374
|
view.setUint32(offset, command.type, true);
|
|
1009
1375
|
view.setUint32(offset + 4, command.targetId ?? 0, true);
|
|
1010
1376
|
view.setBigInt64(offset + 8, toBigInt64(command.sampleTime, -1n), true);
|
|
1011
|
-
view.
|
|
1012
|
-
view.setUint32(offset + 20, 0, true);
|
|
1377
|
+
view.setFloat64(offset + 16, command.argFloat ?? 0, true);
|
|
1013
1378
|
view.setBigInt64(offset + 24, toBigInt64(command.argInt, 0n), true);
|
|
1014
1379
|
}
|
|
1015
1380
|
function readEngineCommandRecord(view, offset) {
|
|
@@ -1017,7 +1382,7 @@ function readEngineCommandRecord(view, offset) {
|
|
|
1017
1382
|
type: view.getUint32(offset, true),
|
|
1018
1383
|
targetId: view.getUint32(offset + 4, true),
|
|
1019
1384
|
sampleTime: Number(view.getBigInt64(offset + 8, true)),
|
|
1020
|
-
argFloat: view.
|
|
1385
|
+
argFloat: view.getFloat64(offset + 16, true),
|
|
1021
1386
|
argInt: Number(view.getBigInt64(offset + 24, true))
|
|
1022
1387
|
};
|
|
1023
1388
|
}
|
|
@@ -1110,35 +1475,48 @@ var SonareWorkletProcessor = class {
|
|
|
1110
1475
|
return true;
|
|
1111
1476
|
}
|
|
1112
1477
|
const frames = leftOut.length;
|
|
1113
|
-
|
|
1114
|
-
return false;
|
|
1115
|
-
}
|
|
1478
|
+
const usable = Math.min(frames, this.blockSize);
|
|
1116
1479
|
for (let strip = 0; strip < this.realtime.leftInputs.length; strip++) {
|
|
1117
1480
|
const input = inputs[strip];
|
|
1118
1481
|
const left = input?.[0];
|
|
1119
1482
|
const right = input?.[1];
|
|
1120
1483
|
const leftTarget = this.realtime.leftInputs[strip];
|
|
1121
1484
|
const rightTarget = this.realtime.rightInputs[strip];
|
|
1122
|
-
if (left && left.length
|
|
1123
|
-
leftTarget.set(left);
|
|
1124
|
-
if (right && right.length
|
|
1125
|
-
rightTarget.set(right);
|
|
1485
|
+
if (left && left.length >= usable) {
|
|
1486
|
+
leftTarget.set(left.subarray(0, usable));
|
|
1487
|
+
if (right && right.length >= usable) {
|
|
1488
|
+
rightTarget.set(right.subarray(0, usable));
|
|
1126
1489
|
} else {
|
|
1127
|
-
rightTarget.set(left);
|
|
1490
|
+
rightTarget.set(left.subarray(0, usable));
|
|
1128
1491
|
}
|
|
1129
1492
|
} else {
|
|
1130
1493
|
leftTarget.fill(0);
|
|
1131
1494
|
rightTarget.fill(0);
|
|
1132
1495
|
}
|
|
1133
1496
|
}
|
|
1134
|
-
this.realtime.process(
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
rightOut
|
|
1497
|
+
this.realtime.process(usable);
|
|
1498
|
+
if (usable === frames) {
|
|
1499
|
+
leftOut.set(this.realtime.outLeft.subarray(0, usable));
|
|
1500
|
+
if (rightOut) {
|
|
1501
|
+
rightOut.set(this.realtime.outRight.subarray(0, usable));
|
|
1502
|
+
}
|
|
1503
|
+
} else {
|
|
1504
|
+
leftOut.fill(0);
|
|
1505
|
+
leftOut.set(this.realtime.outLeft.subarray(0, usable));
|
|
1506
|
+
if (rightOut) {
|
|
1507
|
+
rightOut.fill(0);
|
|
1508
|
+
rightOut.set(this.realtime.outRight.subarray(0, usable));
|
|
1509
|
+
}
|
|
1138
1510
|
}
|
|
1139
|
-
this.processedFrames +=
|
|
1140
|
-
this.publishMeter(
|
|
1141
|
-
|
|
1511
|
+
this.processedFrames += usable;
|
|
1512
|
+
this.publishMeter(
|
|
1513
|
+
this.realtime.outLeft.subarray(0, usable),
|
|
1514
|
+
this.realtime.outRight.subarray(0, usable)
|
|
1515
|
+
);
|
|
1516
|
+
this.publishSpectrum(
|
|
1517
|
+
this.realtime.outLeft.subarray(0, usable),
|
|
1518
|
+
this.realtime.outRight.subarray(0, usable)
|
|
1519
|
+
);
|
|
1142
1520
|
return true;
|
|
1143
1521
|
}
|
|
1144
1522
|
receiveMessage(message) {
|
|
@@ -1224,16 +1602,14 @@ var SonareWorkletProcessor = class {
|
|
|
1224
1602
|
}
|
|
1225
1603
|
const writeIndex = Atomics.load(ring.header, 0);
|
|
1226
1604
|
const offset = writeIndex % ring.capacity * SONARE_METER_RING_RECORD_FLOATS;
|
|
1227
|
-
ring.records[offset] = meter.frame;
|
|
1605
|
+
ring.records[offset] = encodeFrameLo(meter.frame);
|
|
1228
1606
|
ring.records[offset + 1] = meter.peakDbL;
|
|
1229
1607
|
ring.records[offset + 2] = meter.peakDbR;
|
|
1230
1608
|
ring.records[offset + 3] = meter.rmsDbL;
|
|
1231
1609
|
ring.records[offset + 4] = meter.rmsDbR;
|
|
1232
1610
|
ring.records[offset + 5] = meter.correlation;
|
|
1611
|
+
ring.records[offset + 6] = encodeFrameHi(meter.frame);
|
|
1233
1612
|
Atomics.store(ring.header, 0, writeIndex + 1);
|
|
1234
|
-
if (writeIndex + 1 > ring.capacity) {
|
|
1235
|
-
Atomics.store(ring.header, 3, writeIndex + 1 - ring.capacity);
|
|
1236
|
-
}
|
|
1237
1613
|
}
|
|
1238
1614
|
publishSpectrum(left, right) {
|
|
1239
1615
|
if (this.spectrumIntervalFrames <= 0) {
|
|
@@ -1258,7 +1634,12 @@ var SonareWorkletProcessor = class {
|
|
|
1258
1634
|
}
|
|
1259
1635
|
computeSpectrum(left, right) {
|
|
1260
1636
|
const n = Math.max(1, Math.min(left.length, right.length));
|
|
1637
|
+
const maxBand = Math.floor(n / 2);
|
|
1261
1638
|
for (let band = 0; band < this.spectrumBands.length; band++) {
|
|
1639
|
+
if (band >= maxBand) {
|
|
1640
|
+
this.spectrumBands[band] = magnitudeToDb(0);
|
|
1641
|
+
continue;
|
|
1642
|
+
}
|
|
1262
1643
|
const bin = band + 1;
|
|
1263
1644
|
let real = 0;
|
|
1264
1645
|
let imag = 0;
|
|
@@ -1278,19 +1659,20 @@ var SonareWorkletProcessor = class {
|
|
|
1278
1659
|
}
|
|
1279
1660
|
const writeIndex = Atomics.load(ring.header, 0);
|
|
1280
1661
|
const offset = writeIndex % ring.capacity * ring.recordFloats;
|
|
1281
|
-
ring.records[offset] = frame;
|
|
1282
|
-
ring.records[offset + 1] =
|
|
1283
|
-
ring.records
|
|
1662
|
+
ring.records[offset] = encodeFrameLo(frame);
|
|
1663
|
+
ring.records[offset + 1] = encodeFrameHi(frame);
|
|
1664
|
+
ring.records[offset + 2] = bands.length;
|
|
1665
|
+
ring.records.set(bands.subarray(0, ring.bands), offset + 3);
|
|
1284
1666
|
Atomics.store(ring.header, 0, writeIndex + 1);
|
|
1285
|
-
if (writeIndex + 1 > ring.capacity) {
|
|
1286
|
-
Atomics.store(ring.header, 4, writeIndex + 1 - ring.capacity);
|
|
1287
|
-
}
|
|
1288
1667
|
}
|
|
1289
1668
|
};
|
|
1290
1669
|
var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletProcessor {
|
|
1291
1670
|
constructor(options = {}, transport) {
|
|
1292
1671
|
this.closed = false;
|
|
1293
1672
|
this.lastMeterFrame = Number.NEGATIVE_INFINITY;
|
|
1673
|
+
// Latest metronome gains/click length pushed via 'syncMetronome'. The
|
|
1674
|
+
// SetMetronome command only toggles enabled state; the config arrives here.
|
|
1675
|
+
this.metronomeConfig = { ...DEFAULT_METRONOME_CONFIG };
|
|
1294
1676
|
this.sampleRate = options.sampleRate ?? 48e3;
|
|
1295
1677
|
this.blockSize = options.blockSize ?? 128;
|
|
1296
1678
|
this.channelCount = Math.max(1, Math.floor(options.channelCount ?? 2));
|
|
@@ -1307,12 +1689,12 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
|
|
|
1307
1689
|
options.telemetrySharedBuffer,
|
|
1308
1690
|
options.telemetryRingCapacity
|
|
1309
1691
|
) : void 0;
|
|
1692
|
+
this.meterRing = options.meterSharedBuffer ? meterRingFromSharedBuffer(options.meterSharedBuffer, options.meterRingCapacity) : void 0;
|
|
1310
1693
|
this.engine = new RealtimeEngine(this.sampleRate, this.blockSize);
|
|
1311
|
-
this.
|
|
1312
|
-
this.
|
|
1694
|
+
this.engine.prepareChannels(this.channelCount, this.blockSize);
|
|
1695
|
+
this.channelBuffers = new Array(this.channelCount);
|
|
1313
1696
|
for (let ch = 0; ch < this.channelCount; ch++) {
|
|
1314
|
-
this.
|
|
1315
|
-
this.channelScratchViews[ch] = this.channelScratch[ch];
|
|
1697
|
+
this.channelBuffers[ch] = this.engine.getChannelBuffer(ch, this.blockSize);
|
|
1316
1698
|
}
|
|
1317
1699
|
}
|
|
1318
1700
|
process(inputs, outputs) {
|
|
@@ -1333,34 +1715,38 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
|
|
|
1333
1715
|
return true;
|
|
1334
1716
|
}
|
|
1335
1717
|
this.drainCommands();
|
|
1336
|
-
const scratchCapacity = this.channelScratch[0]?.length ?? 0;
|
|
1337
1718
|
let usableFrames = frames;
|
|
1338
|
-
if (usableFrames >
|
|
1719
|
+
if (usableFrames > this.blockSize) {
|
|
1339
1720
|
if (!_SonareRealtimeEngineWorkletProcessor.warnedChannelScratchOverflow) {
|
|
1340
1721
|
_SonareRealtimeEngineWorkletProcessor.warnedChannelScratchOverflow = true;
|
|
1341
1722
|
console.warn(
|
|
1342
|
-
`SonareRealtimeEngineWorkletProcessor: requested ${usableFrames} frames exceeds pre-allocated capacity ${
|
|
1723
|
+
`SonareRealtimeEngineWorkletProcessor: requested ${usableFrames} frames exceeds pre-allocated capacity ${this.blockSize}; clamping.`
|
|
1343
1724
|
);
|
|
1344
1725
|
}
|
|
1345
|
-
usableFrames =
|
|
1726
|
+
usableFrames = this.blockSize;
|
|
1727
|
+
}
|
|
1728
|
+
if ((this.channelBuffers[0]?.byteLength ?? 0) === 0) {
|
|
1729
|
+
this.reacquireChannelBuffers();
|
|
1346
1730
|
}
|
|
1347
1731
|
const input = inputs[0];
|
|
1348
1732
|
for (let ch = 0; ch < this.channelCount; ch++) {
|
|
1349
|
-
const
|
|
1733
|
+
const dst = this.channelBuffers[ch];
|
|
1350
1734
|
const source = input?.[ch];
|
|
1351
1735
|
if (source && source.length === usableFrames) {
|
|
1352
|
-
|
|
1736
|
+
dst.set(source.subarray(0, usableFrames));
|
|
1353
1737
|
} else {
|
|
1354
|
-
|
|
1738
|
+
dst.fill(0, 0, usableFrames);
|
|
1355
1739
|
}
|
|
1356
|
-
this.channelScratchViews[ch] = scratch.subarray(0, usableFrames);
|
|
1357
1740
|
}
|
|
1358
|
-
|
|
1741
|
+
this.engine.processPrepared(usableFrames);
|
|
1359
1742
|
for (let ch = 0; ch < output.length; ch++) {
|
|
1360
1743
|
const target = output[ch];
|
|
1361
|
-
const source =
|
|
1744
|
+
const source = this.channelBuffers[ch] ?? this.channelBuffers[0];
|
|
1362
1745
|
if (source) {
|
|
1363
|
-
target.set(source.subarray(0, target.length));
|
|
1746
|
+
target.set(source.subarray(0, Math.min(target.length, usableFrames)));
|
|
1747
|
+
if (target.length > usableFrames) {
|
|
1748
|
+
target.fill(0, usableFrames);
|
|
1749
|
+
}
|
|
1364
1750
|
} else {
|
|
1365
1751
|
target.fill(0);
|
|
1366
1752
|
}
|
|
@@ -1369,11 +1755,41 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
|
|
|
1369
1755
|
this.publishMeters();
|
|
1370
1756
|
return true;
|
|
1371
1757
|
}
|
|
1758
|
+
reacquireChannelBuffers() {
|
|
1759
|
+
for (let ch = 0; ch < this.channelCount; ch++) {
|
|
1760
|
+
this.channelBuffers[ch] = this.engine.getChannelBuffer(ch, this.blockSize);
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1372
1763
|
receiveCommand(command) {
|
|
1373
1764
|
if (!this.closed) {
|
|
1374
1765
|
this.applyCommand(command);
|
|
1375
1766
|
}
|
|
1376
1767
|
}
|
|
1768
|
+
// Applies an out-of-band control-plane sync message. Runs on the AudioWorklet
|
|
1769
|
+
// global scope but OUTSIDE process() (the message-port callback), so the
|
|
1770
|
+
// bulk/allocating engine setters (setClips/setMarkers) are safe here — they
|
|
1771
|
+
// never run on the realtime render path. This is the audio-thread equivalent
|
|
1772
|
+
// of the engine's control-thread RtPublisher setters.
|
|
1773
|
+
receiveSync(message) {
|
|
1774
|
+
if (this.closed) {
|
|
1775
|
+
return;
|
|
1776
|
+
}
|
|
1777
|
+
switch (message.type) {
|
|
1778
|
+
case "syncClips":
|
|
1779
|
+
this.engine.setClips(message.clips);
|
|
1780
|
+
break;
|
|
1781
|
+
case "syncMarkers":
|
|
1782
|
+
this.engine.setMarkers(message.markers);
|
|
1783
|
+
break;
|
|
1784
|
+
case "syncMetronome":
|
|
1785
|
+
this.metronomeConfig = resolveMetronomeConfig(message.config);
|
|
1786
|
+
this.engine.setMetronome(message.config);
|
|
1787
|
+
break;
|
|
1788
|
+
case "syncAutomation":
|
|
1789
|
+
this.engine.setAutomationLane(message.paramId, message.points);
|
|
1790
|
+
break;
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1377
1793
|
destroy() {
|
|
1378
1794
|
if (!this.closed) {
|
|
1379
1795
|
this.engine.destroy();
|
|
@@ -1395,6 +1811,20 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
|
|
|
1395
1811
|
applyCommand(command) {
|
|
1396
1812
|
const sampleTime = Number(command.sampleTime ?? -1);
|
|
1397
1813
|
switch (command.type) {
|
|
1814
|
+
case 0 /* SetParam */:
|
|
1815
|
+
this.engine.setParameter(
|
|
1816
|
+
Math.trunc(Number(command.targetId ?? 0)),
|
|
1817
|
+
Number(command.argFloat ?? 0),
|
|
1818
|
+
sampleTime
|
|
1819
|
+
);
|
|
1820
|
+
break;
|
|
1821
|
+
case 1 /* SetParamSmoothed */:
|
|
1822
|
+
this.engine.setParameterSmoothed(
|
|
1823
|
+
Math.trunc(Number(command.targetId ?? 0)),
|
|
1824
|
+
Number(command.argFloat ?? 0),
|
|
1825
|
+
sampleTime
|
|
1826
|
+
);
|
|
1827
|
+
break;
|
|
1398
1828
|
case 2 /* TransportPlay */:
|
|
1399
1829
|
this.engine.play(sampleTime);
|
|
1400
1830
|
break;
|
|
@@ -1423,18 +1853,21 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
|
|
|
1423
1853
|
case 14 /* Punch */:
|
|
1424
1854
|
this.engine.setCapturePunch(
|
|
1425
1855
|
Number(command.argInt ?? 0),
|
|
1426
|
-
Math.max(0, Math.round(Number(command.argFloat ?? 0)
|
|
1856
|
+
Math.max(0, Math.round(Number(command.argFloat ?? 0))),
|
|
1427
1857
|
true
|
|
1428
1858
|
);
|
|
1429
1859
|
break;
|
|
1430
1860
|
case 15 /* SetMetronome */:
|
|
1431
1861
|
this.engine.setMetronome({
|
|
1432
1862
|
enabled: Boolean(command.argInt),
|
|
1433
|
-
beatGain:
|
|
1434
|
-
accentGain:
|
|
1435
|
-
clickSamples:
|
|
1863
|
+
beatGain: this.metronomeConfig.beatGain,
|
|
1864
|
+
accentGain: this.metronomeConfig.accentGain,
|
|
1865
|
+
clickSamples: this.metronomeConfig.clickSamples
|
|
1436
1866
|
});
|
|
1437
1867
|
break;
|
|
1868
|
+
case 17 /* SeekMarker */:
|
|
1869
|
+
this.engine.seekMarker(Math.trunc(Number(command.targetId ?? 0)), sampleTime);
|
|
1870
|
+
break;
|
|
1438
1871
|
default:
|
|
1439
1872
|
this.publishTelemetryRecord({
|
|
1440
1873
|
type: 1 /* Error */,
|
|
@@ -1461,7 +1894,7 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
|
|
|
1461
1894
|
this.transport?.postMessage?.(record);
|
|
1462
1895
|
}
|
|
1463
1896
|
publishMeters() {
|
|
1464
|
-
if (
|
|
1897
|
+
if (this.meterIntervalFrames <= 0 || !this.transport && !this.meterRing) {
|
|
1465
1898
|
return;
|
|
1466
1899
|
}
|
|
1467
1900
|
for (const item of this.engine.drainMeterTelemetry(64)) {
|
|
@@ -1470,9 +1903,29 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
|
|
|
1470
1903
|
continue;
|
|
1471
1904
|
}
|
|
1472
1905
|
this.lastMeterFrame = meter.frame;
|
|
1473
|
-
this.
|
|
1474
|
-
|
|
1906
|
+
if (this.meterRing) {
|
|
1907
|
+
this.writeMeterRing(meter);
|
|
1908
|
+
} else {
|
|
1909
|
+
this.transport?.onMeter?.(meter);
|
|
1910
|
+
this.transport?.postMessage?.(meter);
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
writeMeterRing(meter) {
|
|
1915
|
+
const ring = this.meterRing;
|
|
1916
|
+
if (!ring) {
|
|
1917
|
+
return;
|
|
1475
1918
|
}
|
|
1919
|
+
const writeIndex = Atomics.load(ring.header, 0);
|
|
1920
|
+
const offset = writeIndex % ring.capacity * SONARE_METER_RING_RECORD_FLOATS;
|
|
1921
|
+
ring.records[offset] = encodeFrameLo(meter.frame);
|
|
1922
|
+
ring.records[offset + 1] = meter.peakDbL;
|
|
1923
|
+
ring.records[offset + 2] = meter.peakDbR;
|
|
1924
|
+
ring.records[offset + 3] = meter.rmsDbL;
|
|
1925
|
+
ring.records[offset + 4] = meter.rmsDbR;
|
|
1926
|
+
ring.records[offset + 5] = meter.correlation;
|
|
1927
|
+
ring.records[offset + 6] = encodeFrameHi(meter.frame);
|
|
1928
|
+
Atomics.store(ring.header, 0, writeIndex + 1);
|
|
1476
1929
|
}
|
|
1477
1930
|
commandRingFromSharedBuffer(sharedBuffer, fallbackCapacity) {
|
|
1478
1931
|
const ring = engineRingFromSharedBuffer(
|
|
@@ -1495,6 +1948,7 @@ _SonareRealtimeEngineWorkletProcessor.warnedChannelScratchOverflow = false;
|
|
|
1495
1948
|
var SonareRealtimeEngineWorkletProcessor = _SonareRealtimeEngineWorkletProcessor;
|
|
1496
1949
|
var SonareRtRealtimeEngineRuntime = class {
|
|
1497
1950
|
constructor(options) {
|
|
1951
|
+
this.metronomeConfig = { ...DEFAULT_METRONOME_CONFIG };
|
|
1498
1952
|
this.closed = false;
|
|
1499
1953
|
this.module = options.module;
|
|
1500
1954
|
this.memory = options.memory;
|
|
@@ -1576,6 +2030,49 @@ var SonareRtRealtimeEngineRuntime = class {
|
|
|
1576
2030
|
this.publishTelemetry();
|
|
1577
2031
|
return true;
|
|
1578
2032
|
}
|
|
2033
|
+
receiveCommand(command) {
|
|
2034
|
+
if (!this.closed) {
|
|
2035
|
+
this.applyCommand(command);
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
// Out-of-band control sync for the sonare-rt runtime. The sonare-rt C ABI
|
|
2039
|
+
// (src/wasm/rt_bindings.cpp) exposes set_metronome_enabled and seek_marker but
|
|
2040
|
+
// NOT set_clips / set_markers, so clip/marker mutations cannot be applied to a
|
|
2041
|
+
// live sonare-rt engine. We honor the metronome config and surface a clear
|
|
2042
|
+
// telemetry error for the unsupported clip/marker paths instead of silently
|
|
2043
|
+
// dropping them. The default 'embind' runtime wires all three fully.
|
|
2044
|
+
receiveSync(message) {
|
|
2045
|
+
if (this.closed) {
|
|
2046
|
+
return;
|
|
2047
|
+
}
|
|
2048
|
+
switch (message.type) {
|
|
2049
|
+
case "syncMetronome":
|
|
2050
|
+
this.metronomeConfig = resolveMetronomeConfig(message.config);
|
|
2051
|
+
this.module._sonare_rt_engine_set_metronome_enabled(
|
|
2052
|
+
this.engine,
|
|
2053
|
+
message.config.enabled ? 1 : 0,
|
|
2054
|
+
this.metronomeConfig.beatGain,
|
|
2055
|
+
this.metronomeConfig.accentGain,
|
|
2056
|
+
this.metronomeConfig.clickSamples
|
|
2057
|
+
);
|
|
2058
|
+
break;
|
|
2059
|
+
case "syncClips":
|
|
2060
|
+
case "syncMarkers":
|
|
2061
|
+
case "syncAutomation":
|
|
2062
|
+
if (this.telemetryRing) {
|
|
2063
|
+
writeSonareEngineTelemetryRingBuffer(this.telemetryRing, {
|
|
2064
|
+
type: 1 /* Error */,
|
|
2065
|
+
error: 7 /* UnknownTarget */,
|
|
2066
|
+
renderFrame: 0,
|
|
2067
|
+
timelineSample: 0,
|
|
2068
|
+
audibleTimelineSample: 0,
|
|
2069
|
+
graphLatencySamplesQ8: 0,
|
|
2070
|
+
value: 0
|
|
2071
|
+
});
|
|
2072
|
+
}
|
|
2073
|
+
break;
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
1579
2076
|
destroy() {
|
|
1580
2077
|
if (this.closed) {
|
|
1581
2078
|
return;
|
|
@@ -1611,6 +2108,20 @@ var SonareRtRealtimeEngineRuntime = class {
|
|
|
1611
2108
|
applyCommand(command) {
|
|
1612
2109
|
const sampleTime = toBigInt64(command.sampleTime, -1n);
|
|
1613
2110
|
switch (command.type) {
|
|
2111
|
+
case 0 /* SetParam */:
|
|
2112
|
+
case 1 /* SetParamSmoothed */:
|
|
2113
|
+
if (this.telemetryRing) {
|
|
2114
|
+
writeSonareEngineTelemetryRingBuffer(this.telemetryRing, {
|
|
2115
|
+
type: 1 /* Error */,
|
|
2116
|
+
error: 7 /* UnknownTarget */,
|
|
2117
|
+
renderFrame: 0,
|
|
2118
|
+
timelineSample: 0,
|
|
2119
|
+
audibleTimelineSample: 0,
|
|
2120
|
+
graphLatencySamplesQ8: 0,
|
|
2121
|
+
value: Number(command.type)
|
|
2122
|
+
});
|
|
2123
|
+
}
|
|
2124
|
+
break;
|
|
1614
2125
|
case 2 /* TransportPlay */:
|
|
1615
2126
|
this.module._sonare_rt_engine_play(this.engine, sampleTime);
|
|
1616
2127
|
break;
|
|
@@ -1649,7 +2160,7 @@ var SonareRtRealtimeEngineRuntime = class {
|
|
|
1649
2160
|
this.module._sonare_rt_engine_set_capture_punch(
|
|
1650
2161
|
this.engine,
|
|
1651
2162
|
toBigInt64(command.argInt, 0n),
|
|
1652
|
-
BigInt(Math.
|
|
2163
|
+
BigInt(Math.max(0, Math.round(Number(command.argFloat ?? 0)))),
|
|
1653
2164
|
1
|
|
1654
2165
|
);
|
|
1655
2166
|
break;
|
|
@@ -1657,9 +2168,9 @@ var SonareRtRealtimeEngineRuntime = class {
|
|
|
1657
2168
|
this.module._sonare_rt_engine_set_metronome_enabled(
|
|
1658
2169
|
this.engine,
|
|
1659
2170
|
command.argInt ? 1 : 0,
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
2171
|
+
this.metronomeConfig.beatGain,
|
|
2172
|
+
this.metronomeConfig.accentGain,
|
|
2173
|
+
this.metronomeConfig.clickSamples
|
|
1663
2174
|
);
|
|
1664
2175
|
break;
|
|
1665
2176
|
case 17 /* SeekMarker */:
|
|
@@ -1734,8 +2245,9 @@ var SonareRtRealtimeEngineRuntime = class {
|
|
|
1734
2245
|
}
|
|
1735
2246
|
};
|
|
1736
2247
|
var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
|
|
1737
|
-
constructor(node, capabilities, commandRing, telemetryRing) {
|
|
2248
|
+
constructor(node, capabilities, commandRing, telemetryRing, meterRing) {
|
|
1738
2249
|
this.telemetryReadIndex = 0;
|
|
2250
|
+
this.meterReadIndex = 0;
|
|
1739
2251
|
this.telemetryListeners = /* @__PURE__ */ new Set();
|
|
1740
2252
|
this.meterListeners = /* @__PURE__ */ new Set();
|
|
1741
2253
|
this.destroyed = false;
|
|
@@ -1743,6 +2255,7 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
|
|
|
1743
2255
|
this.capabilities = capabilities;
|
|
1744
2256
|
this.commandRing = commandRing;
|
|
1745
2257
|
this.telemetryRing = telemetryRing;
|
|
2258
|
+
this.meterRing = meterRing;
|
|
1746
2259
|
this.ready = new Promise((resolve, reject) => {
|
|
1747
2260
|
this.resolveReady = resolve;
|
|
1748
2261
|
this.rejectReady = reject;
|
|
@@ -1791,6 +2304,7 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
|
|
|
1791
2304
|
}
|
|
1792
2305
|
const commandRing = mode === "sab" ? createSonareEngineCommandRingBuffer(options.commandRingCapacity ?? 128) : void 0;
|
|
1793
2306
|
const telemetryRing = mode === "sab" ? createSonareEngineTelemetryRingBuffer(options.telemetryRingCapacity ?? 128) : void 0;
|
|
2307
|
+
const meterRing = mode === "sab" && runtimeTarget === "embind" ? createSonareMeterRingBuffer(options.meterRingCapacity ?? 128) : void 0;
|
|
1794
2308
|
const channelCount = Math.max(1, Math.floor(options.channelCount ?? 2));
|
|
1795
2309
|
const processorOptions = {
|
|
1796
2310
|
runtimeTarget,
|
|
@@ -1802,7 +2316,9 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
|
|
|
1802
2316
|
commandSharedBuffer: commandRing?.sharedBuffer,
|
|
1803
2317
|
commandRingCapacity: commandRing?.capacity,
|
|
1804
2318
|
telemetrySharedBuffer: telemetryRing?.sharedBuffer,
|
|
1805
|
-
telemetryRingCapacity: telemetryRing?.capacity
|
|
2319
|
+
telemetryRingCapacity: telemetryRing?.capacity,
|
|
2320
|
+
meterSharedBuffer: meterRing?.sharedBuffer,
|
|
2321
|
+
meterRingCapacity: meterRing?.capacity
|
|
1806
2322
|
};
|
|
1807
2323
|
const factory = options.nodeFactory ?? ((ctx, name, nodeOptions) => new AudioWorkletNode(ctx, name, nodeOptions));
|
|
1808
2324
|
const node = factory(context, processorName, {
|
|
@@ -1825,7 +2341,8 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
|
|
|
1825
2341
|
degradedReason
|
|
1826
2342
|
},
|
|
1827
2343
|
commandRing,
|
|
1828
|
-
telemetryRing
|
|
2344
|
+
telemetryRing,
|
|
2345
|
+
meterRing
|
|
1829
2346
|
);
|
|
1830
2347
|
}
|
|
1831
2348
|
play(sampleTime = -1) {
|
|
@@ -1869,6 +2386,20 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
|
|
|
1869
2386
|
}
|
|
1870
2387
|
return read.telemetry;
|
|
1871
2388
|
}
|
|
2389
|
+
// Drains any meters published into the SAB meter ring (embind SAB mode) and
|
|
2390
|
+
// forwards them to onMeter listeners. In postMessage mode meters arrive via
|
|
2391
|
+
// node.port.onmessage instead, so this is a no-op then.
|
|
2392
|
+
pollMeters() {
|
|
2393
|
+
if (!this.meterRing) {
|
|
2394
|
+
return [];
|
|
2395
|
+
}
|
|
2396
|
+
const read = readSonareMeterRingBuffer(this.meterRing, this.meterReadIndex);
|
|
2397
|
+
this.meterReadIndex = read.nextReadIndex;
|
|
2398
|
+
for (const meter of read.meters) {
|
|
2399
|
+
this.emitMeter(meter);
|
|
2400
|
+
}
|
|
2401
|
+
return read.meters;
|
|
2402
|
+
}
|
|
1872
2403
|
onTelemetry(callback) {
|
|
1873
2404
|
this.telemetryListeners.add(callback);
|
|
1874
2405
|
return () => {
|
|
@@ -1983,10 +2514,14 @@ var SonareEngine = class _SonareEngine {
|
|
|
1983
2514
|
});
|
|
1984
2515
|
}
|
|
1985
2516
|
setParam(nodeId, param, value) {
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
2517
|
+
const paramId = this.resolveParamId(nodeId, param);
|
|
2518
|
+
this.offlineEngine.setParameter(paramId, value);
|
|
2519
|
+
return this.realtimeNode.sendCommand({
|
|
2520
|
+
type: 0 /* SetParam */,
|
|
2521
|
+
targetId: paramId,
|
|
2522
|
+
sampleTime: -1,
|
|
2523
|
+
argFloat: value
|
|
2524
|
+
});
|
|
1990
2525
|
}
|
|
1991
2526
|
scheduleParam(nodeId, param, ppq, value, curve = "linear") {
|
|
1992
2527
|
const paramId = this.resolveParamId(nodeId, param);
|
|
@@ -1995,6 +2530,7 @@ var SonareEngine = class _SonareEngine {
|
|
|
1995
2530
|
lane.sort((a, b) => a.ppq - b.ppq);
|
|
1996
2531
|
this.automationLanes.set(paramId, lane);
|
|
1997
2532
|
this.offlineEngine.setAutomationLane(paramId, lane);
|
|
2533
|
+
this.postSync({ type: "syncAutomation", paramId, points: lane });
|
|
1998
2534
|
}
|
|
1999
2535
|
addAutomationPoint(laneId, ppq, value, curve = "linear") {
|
|
2000
2536
|
this.scheduleParam("", laneId, ppq, value, curve);
|
|
@@ -2010,7 +2546,9 @@ var SonareEngine = class _SonareEngine {
|
|
|
2010
2546
|
void target;
|
|
2011
2547
|
void solo;
|
|
2012
2548
|
void mute;
|
|
2013
|
-
|
|
2549
|
+
throw new Error(
|
|
2550
|
+
"SonareEngine.setSoloMute is not supported: solo/mute is a Mixer feature; use Mixer.setSoloed(stripIndex, ...) / Mixer.setMuted(stripIndex, ...) instead."
|
|
2551
|
+
);
|
|
2014
2552
|
}
|
|
2015
2553
|
addClip(trackId, buffer, startPpq, opts = {}) {
|
|
2016
2554
|
const id = opts.id ?? this.nextClipId++;
|
|
@@ -2046,11 +2584,12 @@ var SonareEngine = class _SonareEngine {
|
|
|
2046
2584
|
type: 14 /* Punch */,
|
|
2047
2585
|
sampleTime: -1,
|
|
2048
2586
|
argInt: inSample,
|
|
2049
|
-
argFloat:
|
|
2587
|
+
argFloat: outSample
|
|
2050
2588
|
});
|
|
2051
2589
|
}
|
|
2052
2590
|
setMetronome(opts) {
|
|
2053
2591
|
this.offlineEngine.setMetronome(opts);
|
|
2592
|
+
this.postSync({ type: "syncMetronome", config: opts });
|
|
2054
2593
|
this.realtimeNode.sendCommand({
|
|
2055
2594
|
type: 15 /* SetMetronome */,
|
|
2056
2595
|
sampleTime: -1,
|
|
@@ -2065,7 +2604,11 @@ var SonareEngine = class _SonareEngine {
|
|
|
2065
2604
|
}
|
|
2066
2605
|
seekMarker(markerId) {
|
|
2067
2606
|
this.offlineEngine.seekMarker(markerId);
|
|
2068
|
-
return
|
|
2607
|
+
return this.realtimeNode.sendCommand({
|
|
2608
|
+
type: 17 /* SeekMarker */,
|
|
2609
|
+
targetId: markerId,
|
|
2610
|
+
sampleTime: -1
|
|
2611
|
+
});
|
|
2069
2612
|
}
|
|
2070
2613
|
async renderOffline(totalFrames) {
|
|
2071
2614
|
const frames = Math.max(0, Math.floor(totalFrames));
|
|
@@ -2084,6 +2627,9 @@ var SonareEngine = class _SonareEngine {
|
|
|
2084
2627
|
pollTelemetry() {
|
|
2085
2628
|
return this.realtimeNode.pollTelemetry();
|
|
2086
2629
|
}
|
|
2630
|
+
pollMeters() {
|
|
2631
|
+
return this.realtimeNode.pollMeters();
|
|
2632
|
+
}
|
|
2087
2633
|
destroy() {
|
|
2088
2634
|
if (this.destroyed) {
|
|
2089
2635
|
return;
|
|
@@ -2095,10 +2641,23 @@ var SonareEngine = class _SonareEngine {
|
|
|
2095
2641
|
this.offlineEngine.destroy();
|
|
2096
2642
|
}
|
|
2097
2643
|
syncClips() {
|
|
2098
|
-
|
|
2644
|
+
const clips = Array.from(this.clips.values());
|
|
2645
|
+
this.offlineEngine.setClips(clips);
|
|
2646
|
+
this.postSync({ type: "syncClips", clips });
|
|
2099
2647
|
}
|
|
2100
2648
|
syncMarkers() {
|
|
2101
|
-
|
|
2649
|
+
const markers = Array.from(this.markers.values()).sort((a, b) => a.ppq - b.ppq);
|
|
2650
|
+
this.offlineEngine.setMarkers(markers);
|
|
2651
|
+
this.postSync({ type: "syncMarkers", markers });
|
|
2652
|
+
}
|
|
2653
|
+
// Posts an out-of-band control-sync message to the worklet engine processor.
|
|
2654
|
+
// Sync messages use a string `type` so the worklet's message handler routes
|
|
2655
|
+
// them to receiveSync() (numeric `type` is reserved for SonareEngineCommandRecord).
|
|
2656
|
+
postSync(message) {
|
|
2657
|
+
if (this.destroyed) {
|
|
2658
|
+
return;
|
|
2659
|
+
}
|
|
2660
|
+
this.realtimeNode.node.port.postMessage(message);
|
|
2102
2661
|
}
|
|
2103
2662
|
resolveParamId(nodeId, param) {
|
|
2104
2663
|
if (typeof param === "number") {
|
|
@@ -2169,6 +2728,9 @@ var _SonareRealtimeVoiceChangerWorkletProcessor = class _SonareRealtimeVoiceChan
|
|
|
2169
2728
|
if (this.destroyed || !output || output.length === 0) {
|
|
2170
2729
|
return !this.destroyed;
|
|
2171
2730
|
}
|
|
2731
|
+
if (this.monoInput.byteLength === 0) {
|
|
2732
|
+
this.reacquireBuffers();
|
|
2733
|
+
}
|
|
2172
2734
|
const input = inputs[0];
|
|
2173
2735
|
const requestedFrames = output[0]?.length ?? 0;
|
|
2174
2736
|
const requestedChannels = Math.min(this.channelCount, output.length);
|
|
@@ -2220,6 +2782,19 @@ var _SonareRealtimeVoiceChangerWorkletProcessor = class _SonareRealtimeVoiceChan
|
|
|
2220
2782
|
this.destroyed = true;
|
|
2221
2783
|
this.changer.delete();
|
|
2222
2784
|
}
|
|
2785
|
+
// Re-acquires the cached WASM-heap views after a memory-growth detachment.
|
|
2786
|
+
// The underlying C++ vectors are pre-warmed (ensure_*_capacity ran at prepare
|
|
2787
|
+
// time), so getMono*/getPlanar* return fresh views onto the SAME storage
|
|
2788
|
+
// without reallocating it.
|
|
2789
|
+
reacquireBuffers() {
|
|
2790
|
+
this.monoInput = this.changer.getMonoInputBuffer(this.blockSize);
|
|
2791
|
+
this.monoOutput = this.changer.getMonoOutputBuffer(this.blockSize);
|
|
2792
|
+
if (this.channelCount > 1) {
|
|
2793
|
+
for (let ch = 0; ch < this.channelCount; ch++) {
|
|
2794
|
+
this.planarChannels[ch] = this.changer.getPlanarChannelBuffer(ch, this.blockSize);
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2223
2798
|
/**
|
|
2224
2799
|
* Returns the number of frames we can actually process given the
|
|
2225
2800
|
* pre-allocated capacity. If the host requests more frames than the
|
|
@@ -2343,6 +2918,10 @@ function registerSonareRealtimeEngineWorkletProcessor(name = "sonare-realtime-en
|
|
|
2343
2918
|
const onMessage = (event) => {
|
|
2344
2919
|
if (isEngineCommandRecord(event.data)) {
|
|
2345
2920
|
this.bridge?.receiveCommand(event.data);
|
|
2921
|
+
this.rtBridge?.receiveCommand(event.data);
|
|
2922
|
+
} else if (isEngineSyncMessage(event.data)) {
|
|
2923
|
+
this.bridge?.receiveSync(event.data);
|
|
2924
|
+
this.rtBridge?.receiveSync(event.data);
|
|
2346
2925
|
}
|
|
2347
2926
|
};
|
|
2348
2927
|
if (port?.addEventListener) {
|
|
@@ -2421,6 +3000,9 @@ export {
|
|
|
2421
3000
|
createSonareEngineTelemetryRingBuffer,
|
|
2422
3001
|
createSonareMeterRingBuffer,
|
|
2423
3002
|
createSonareSpectrumRingBuffer,
|
|
3003
|
+
decodeFrame,
|
|
3004
|
+
encodeFrameHi,
|
|
3005
|
+
encodeFrameLo,
|
|
2424
3006
|
init,
|
|
2425
3007
|
isInitialized,
|
|
2426
3008
|
popSonareEngineCommandRingBuffer,
|