@libraz/libsonare 1.2.3 → 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 -2842
- package/dist/index.js +3602 -1934
- 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 +4816 -483
- package/dist/worklet.js +747 -440
- 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 +341 -4963
- 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 +177 -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 +302 -4
- package/src/stream_analyzer.ts +275 -0
- package/src/stream_types.ts +26 -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/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,238 +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();
|
|
201
|
-
}
|
|
202
|
-
graphConnectionCount() {
|
|
203
|
-
return this.native.graphConnectionCount();
|
|
204
|
-
}
|
|
205
|
-
setClips(clips) {
|
|
206
|
-
this.native.setClips(clips);
|
|
207
|
-
}
|
|
208
|
-
clipCount() {
|
|
209
|
-
return this.native.clipCount();
|
|
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);
|
|
210
296
|
}
|
|
211
|
-
|
|
212
|
-
|
|
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);
|
|
213
300
|
}
|
|
214
|
-
|
|
215
|
-
|
|
301
|
+
/** Set independent left/right pan positions (dual-pan mode). */
|
|
302
|
+
setDualPan(stripIndex, leftPan, rightPan) {
|
|
303
|
+
this.mixer.setDualPan(stripIndex, leftPan, rightPan);
|
|
216
304
|
}
|
|
217
|
-
|
|
218
|
-
|
|
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));
|
|
219
317
|
}
|
|
220
|
-
|
|
221
|
-
|
|
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);
|
|
222
321
|
}
|
|
223
|
-
|
|
224
|
-
|
|
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);
|
|
225
334
|
}
|
|
226
|
-
|
|
227
|
-
|
|
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));
|
|
228
343
|
}
|
|
229
|
-
|
|
230
|
-
|
|
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));
|
|
231
361
|
}
|
|
232
362
|
/**
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
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'`)
|
|
237
369
|
*/
|
|
238
|
-
|
|
239
|
-
this.
|
|
370
|
+
scheduleFaderAutomation(stripIndex, samplePos, faderDb, curve = "linear") {
|
|
371
|
+
this.mixer.scheduleFaderAutomation(stripIndex, samplePos, faderDb, automationCurveCode(curve));
|
|
240
372
|
}
|
|
241
373
|
/**
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
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'`)
|
|
245
380
|
*/
|
|
246
|
-
|
|
247
|
-
|
|
381
|
+
schedulePanAutomation(stripIndex, samplePos, pan, curve = "linear") {
|
|
382
|
+
this.mixer.schedulePanAutomation(stripIndex, samplePos, pan, automationCurveCode(curve));
|
|
248
383
|
}
|
|
249
384
|
/**
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
* `
|
|
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'`)
|
|
253
391
|
*/
|
|
254
|
-
|
|
255
|
-
this.
|
|
392
|
+
scheduleWidthAutomation(stripIndex, samplePos, width, curve = "linear") {
|
|
393
|
+
this.mixer.scheduleWidthAutomation(stripIndex, samplePos, width, automationCurveCode(curve));
|
|
256
394
|
}
|
|
257
|
-
|
|
258
|
-
|
|
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
|
+
);
|
|
259
412
|
}
|
|
260
|
-
|
|
261
|
-
|
|
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);
|
|
262
419
|
}
|
|
263
|
-
|
|
264
|
-
|
|
420
|
+
/** Serialize the current scene (strips, buses, sends, connections) to JSON. */
|
|
421
|
+
toSceneJson() {
|
|
422
|
+
return this.mixer.toSceneJson();
|
|
265
423
|
}
|
|
266
|
-
|
|
267
|
-
|
|
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();
|
|
268
430
|
}
|
|
269
|
-
|
|
270
|
-
|
|
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);
|
|
271
438
|
}
|
|
272
|
-
|
|
273
|
-
|
|
439
|
+
/** Release the underlying WASM object. Safe to call only once. */
|
|
440
|
+
delete() {
|
|
441
|
+
this.mixer.delete();
|
|
274
442
|
}
|
|
443
|
+
/** Alias for {@link delete}, provided for cross-binding (Node) compatibility. */
|
|
275
444
|
destroy() {
|
|
276
|
-
this.
|
|
445
|
+
this.delete();
|
|
277
446
|
}
|
|
278
447
|
};
|
|
448
|
+
|
|
449
|
+
// src/realtime_voice_changer.ts
|
|
279
450
|
var RealtimeVoiceChanger = class {
|
|
280
451
|
constructor(config = "neutral-monitor") {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
}
|
|
284
|
-
this.changer = module.createRealtimeVoiceChanger(config);
|
|
452
|
+
const module2 = getSonareModule();
|
|
453
|
+
this.changer = module2.createRealtimeVoiceChanger(config);
|
|
285
454
|
}
|
|
286
455
|
prepare(sampleRate, maxBlockSize = 128, channels = 1) {
|
|
287
456
|
this.changer.prepare(sampleRate, maxBlockSize, channels);
|
|
@@ -372,23 +541,53 @@ var RealtimeVoiceChanger = class {
|
|
|
372
541
|
* views are reused across calls and become invalid after {@link delete}.
|
|
373
542
|
*/
|
|
374
543
|
createRealtimeMonoBuffer(numSamples) {
|
|
375
|
-
|
|
376
|
-
|
|
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
|
+
};
|
|
377
552
|
return {
|
|
378
|
-
input
|
|
379
|
-
|
|
380
|
-
|
|
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
|
+
}
|
|
381
565
|
};
|
|
382
566
|
}
|
|
383
567
|
/** Same as {@link createRealtimeMonoBuffer} but for interleaved I/O. */
|
|
384
568
|
createRealtimeInterleavedBuffer(numFrames, numChannels) {
|
|
385
|
-
|
|
386
|
-
|
|
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
|
+
};
|
|
387
577
|
return {
|
|
388
|
-
input
|
|
389
|
-
|
|
578
|
+
get input() {
|
|
579
|
+
reacquireIfDetached();
|
|
580
|
+
return input;
|
|
581
|
+
},
|
|
582
|
+
get output() {
|
|
583
|
+
reacquireIfDetached();
|
|
584
|
+
return output;
|
|
585
|
+
},
|
|
390
586
|
channels: numChannels,
|
|
391
|
-
process: () =>
|
|
587
|
+
process: () => {
|
|
588
|
+
reacquireIfDetached();
|
|
589
|
+
this.processPreparedInterleaved(numFrames, numChannels);
|
|
590
|
+
}
|
|
392
591
|
};
|
|
393
592
|
}
|
|
394
593
|
/**
|
|
@@ -398,326 +597,434 @@ var RealtimeVoiceChanger = class {
|
|
|
398
597
|
* become invalid after {@link delete}.
|
|
399
598
|
*/
|
|
400
599
|
createRealtimePlanarBuffer(numFrames, numChannels) {
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
channels
|
|
404
|
-
|
|
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
|
+
};
|
|
405
613
|
return {
|
|
406
|
-
channels
|
|
407
|
-
|
|
614
|
+
get channels() {
|
|
615
|
+
reacquireIfDetached();
|
|
616
|
+
return channels;
|
|
617
|
+
},
|
|
618
|
+
process: () => {
|
|
619
|
+
reacquireIfDetached();
|
|
620
|
+
this.processPreparedPlanar(numFrames);
|
|
621
|
+
}
|
|
408
622
|
};
|
|
409
623
|
}
|
|
410
624
|
delete() {
|
|
411
625
|
this.changer.delete();
|
|
412
626
|
}
|
|
413
627
|
};
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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;
|
|
417
649
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
static fromSceneJson(json, sampleRate = 48e3, blockSize = 512) {
|
|
426
|
-
if (!module) {
|
|
427
|
-
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
|
+
);
|
|
428
657
|
}
|
|
429
|
-
|
|
658
|
+
this.native = new module2.RealtimeEngine(
|
|
659
|
+
sampleRate,
|
|
660
|
+
maxBlockSize,
|
|
661
|
+
commandCapacity,
|
|
662
|
+
telemetryCapacity
|
|
663
|
+
);
|
|
430
664
|
}
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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);
|
|
434
678
|
}
|
|
435
679
|
/**
|
|
436
|
-
*
|
|
437
|
-
*
|
|
438
|
-
*
|
|
439
|
-
* @
|
|
440
|
-
*
|
|
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.
|
|
441
687
|
*/
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
throw new Error("leftChannels and rightChannels must have the same length.");
|
|
445
|
-
}
|
|
446
|
-
return this.mixer.processStereo(leftChannels, rightChannels);
|
|
688
|
+
setSynthInstrument(patch = {}, destinationId = (typeof patch === "object" ? patch.destinationId : void 0) ?? 0) {
|
|
689
|
+
this.nativeExt().setSynthInstrument(destinationId, patch);
|
|
447
690
|
}
|
|
448
691
|
/**
|
|
449
|
-
*
|
|
450
|
-
*
|
|
451
|
-
*
|
|
452
|
-
*
|
|
453
|
-
* 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.
|
|
454
696
|
*/
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
throw new Error("leftChannels and rightChannels must have the same length.");
|
|
458
|
-
}
|
|
459
|
-
if (outLeft.length !== outRight.length) {
|
|
460
|
-
throw new Error("outLeft and outRight must have the same length.");
|
|
461
|
-
}
|
|
462
|
-
this.mixer.processStereoInto(leftChannels, rightChannels, outLeft, outRight);
|
|
697
|
+
loadSoundFont(data) {
|
|
698
|
+
this.nativeExt().loadSoundFont(data);
|
|
463
699
|
}
|
|
464
700
|
/**
|
|
465
|
-
*
|
|
466
|
-
*
|
|
467
|
-
*
|
|
468
|
-
*
|
|
469
|
-
*
|
|
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).
|
|
470
708
|
*/
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
const leftInputs = [];
|
|
474
|
-
const rightInputs = [];
|
|
475
|
-
for (let index = 0; index < stripCount; index++) {
|
|
476
|
-
leftInputs.push(this.mixer.inputLeftView(index));
|
|
477
|
-
rightInputs.push(this.mixer.inputRightView(index));
|
|
478
|
-
}
|
|
479
|
-
const outLeft = this.mixer.outputLeftView();
|
|
480
|
-
const outRight = this.mixer.outputRightView();
|
|
481
|
-
return {
|
|
482
|
-
leftInputs,
|
|
483
|
-
rightInputs,
|
|
484
|
-
outLeft,
|
|
485
|
-
outRight,
|
|
486
|
-
process: (numSamples = outLeft.length) => this.mixer.processPreparedStereo(numSamples)
|
|
487
|
-
};
|
|
709
|
+
setSf2Instrument(config = {}, destinationId = config.destinationId ?? 0) {
|
|
710
|
+
this.nativeExt().setSf2Instrument(destinationId, config);
|
|
488
711
|
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
712
|
+
clearMidiInstrument(destinationId = 0) {
|
|
713
|
+
this.nativeExt().clearMidiInstrument(destinationId);
|
|
714
|
+
}
|
|
715
|
+
midiInstrumentCount() {
|
|
716
|
+
return this.nativeExt().midiInstrumentCount();
|
|
492
717
|
}
|
|
493
718
|
/**
|
|
494
|
-
*
|
|
495
|
-
*
|
|
496
|
-
*
|
|
497
|
-
* @param insertIndex - Index into the strip's combined insert sequence
|
|
498
|
-
* (`[pre-inserts... post-inserts...]`)
|
|
499
|
-
* @param paramId - Processor-specific parameter id
|
|
500
|
-
* @param samplePos - Absolute samples from the start of processing (the mixer
|
|
501
|
-
* advances an internal position from 0 on the first {@link processStereo}
|
|
502
|
-
* call; recompiling resets it to 0)
|
|
503
|
-
* @param value - Target parameter value
|
|
504
|
-
* @param curve - Interpolation curve (default: `'linear'`)
|
|
505
|
-
* @throws If the strip index is out of range or the schedule call fails
|
|
506
|
-
* (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`.
|
|
507
722
|
*/
|
|
508
|
-
|
|
509
|
-
this.
|
|
510
|
-
|
|
511
|
-
|
|
723
|
+
bindMidiCc(channel, controller, paramId, options = {}) {
|
|
724
|
+
this.nativeExt().bindMidiCc(
|
|
725
|
+
channel,
|
|
726
|
+
controller,
|
|
512
727
|
paramId,
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
automationCurveCode(curve)
|
|
728
|
+
options.minValue ?? 0,
|
|
729
|
+
options.maxValue ?? 1
|
|
516
730
|
);
|
|
517
731
|
}
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
* when no strip with that id exists (matches the Node binding's `number | null`).
|
|
521
|
-
*/
|
|
522
|
-
stripById(id) {
|
|
523
|
-
const index = this.mixer.stripById(id);
|
|
524
|
-
return index < 0 ? null : index;
|
|
732
|
+
clearMidiCcBindings() {
|
|
733
|
+
this.nativeExt().clearMidiCcBindings();
|
|
525
734
|
}
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
* `'submix'` (defaults to `'aux'`). Marks the routing graph dirty; call
|
|
529
|
-
* {@link compile} (or {@link processStereo}) to rebuild.
|
|
530
|
-
*/
|
|
531
|
-
addBus(id, role = "aux") {
|
|
532
|
-
this.mixer.addBus(id, role);
|
|
735
|
+
midiCcBindingCount() {
|
|
736
|
+
return this.nativeExt().midiCcBindingCount();
|
|
533
737
|
}
|
|
534
|
-
/**
|
|
535
|
-
|
|
536
|
-
this.
|
|
738
|
+
/** Install/replace a live non-destructive MIDI-FX insert for one destination. */
|
|
739
|
+
setMidiFx(destinationId, configJson) {
|
|
740
|
+
this.nativeExt().setMidiFx(destinationId, configJson);
|
|
537
741
|
}
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
return this.mixer.busCount();
|
|
742
|
+
clearMidiFx(destinationId = 0) {
|
|
743
|
+
this.nativeExt().clearMidiFx(destinationId);
|
|
541
744
|
}
|
|
542
|
-
/**
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
*/
|
|
546
|
-
addVcaGroup(id, gainDb = 0, members = []) {
|
|
547
|
-
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);
|
|
548
748
|
}
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
this.mixer.removeVcaGroup(id);
|
|
749
|
+
clearMidiInputSource() {
|
|
750
|
+
this.nativeExt().clearMidiInputSource();
|
|
552
751
|
}
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
return this.mixer.vcaGroupCount();
|
|
752
|
+
midiInputPendingCount() {
|
|
753
|
+
return this.nativeExt().midiInputPendingCount();
|
|
556
754
|
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
this.mixer.setInputTrimDb(stripIndex, db);
|
|
755
|
+
pushMidiInputNoteOn(group, channel, note, velocity, portTimeSamples = 0) {
|
|
756
|
+
this.nativeExt().pushMidiInputNoteOn(group, channel, note, velocity, portTimeSamples);
|
|
560
757
|
}
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
this.mixer.setFaderDb(stripIndex, db);
|
|
758
|
+
pushMidiInputNoteOff(group, channel, note, velocity = 0, portTimeSamples = 0) {
|
|
759
|
+
this.nativeExt().pushMidiInputNoteOff(group, channel, note, velocity, portTimeSamples);
|
|
564
760
|
}
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
this.mixer.setPan(stripIndex, pan, panModeCode(panMode));
|
|
761
|
+
pushMidiInputCc(group, channel, controller, value, portTimeSamples = 0) {
|
|
762
|
+
this.nativeExt().pushMidiInputCc(group, channel, controller, value, portTimeSamples);
|
|
568
763
|
}
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
this.mixer.setWidth(stripIndex, width);
|
|
764
|
+
pushMidiNoteOn(destinationId, group, channel, note, velocity, renderFrame = -1) {
|
|
765
|
+
this.nativeExt().pushMidiNoteOn(destinationId, group, channel, note, velocity, renderFrame);
|
|
572
766
|
}
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
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);
|
|
576
769
|
}
|
|
577
770
|
/**
|
|
578
|
-
*
|
|
579
|
-
*
|
|
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`.
|
|
580
775
|
*/
|
|
581
|
-
|
|
582
|
-
this.
|
|
776
|
+
pushMidiCc(destinationId, group, channel, controller, value, renderFrame = -1) {
|
|
777
|
+
this.nativeExt().pushMidiCc(destinationId, group, channel, controller, value, renderFrame);
|
|
583
778
|
}
|
|
584
779
|
/**
|
|
585
|
-
*
|
|
586
|
-
*
|
|
780
|
+
* Queue a MIDI panic (all-notes-off) releasing every sounding note at
|
|
781
|
+
* `renderFrame` (-1 = immediate). Mirrors the C-ABI `pushMidiPanic`.
|
|
587
782
|
*/
|
|
588
|
-
|
|
589
|
-
this.
|
|
590
|
-
}
|
|
591
|
-
/** Invert the polarity of the left and/or right channel of a strip. */
|
|
592
|
-
setPolarityInvert(stripIndex, invertLeft, invertRight) {
|
|
593
|
-
this.mixer.setPolarityInvert(stripIndex, invertLeft, invertRight);
|
|
594
|
-
}
|
|
595
|
-
/** Set the strip's pan law. */
|
|
596
|
-
setPanLaw(stripIndex, panLaw) {
|
|
597
|
-
this.mixer.setPanLaw(stripIndex, panLawCode(panLaw));
|
|
783
|
+
pushMidiPanic(renderFrame = -1) {
|
|
784
|
+
this.nativeExt().pushMidiPanic(renderFrame);
|
|
598
785
|
}
|
|
599
786
|
/**
|
|
600
|
-
*
|
|
601
|
-
*
|
|
787
|
+
* Remove all registered parameters (and their automation lanes). Control-thread
|
|
788
|
+
* only; not realtime-safe. Mirrors the C-ABI `clearParameters`.
|
|
602
789
|
*/
|
|
603
|
-
|
|
604
|
-
this.
|
|
790
|
+
clearParameters() {
|
|
791
|
+
this.nativeExt().clearParameters();
|
|
605
792
|
}
|
|
606
|
-
/**
|
|
607
|
-
|
|
608
|
-
this.
|
|
793
|
+
/** Read back the current transport state snapshot. */
|
|
794
|
+
getTransportState() {
|
|
795
|
+
return this.native.getTransportState();
|
|
609
796
|
}
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
this.mixer.setDualPan(stripIndex, leftPan, rightPan);
|
|
797
|
+
play(renderFrame = -1) {
|
|
798
|
+
this.native.play(renderFrame);
|
|
613
799
|
}
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
*
|
|
617
|
-
* @param stripIndex - Strip index in `[0, stripCount())`
|
|
618
|
-
* @param id - Send id
|
|
619
|
-
* @param destinationBusId - Destination bus id
|
|
620
|
-
* @param sendDb - Initial send level in dB
|
|
621
|
-
* @param timing - `'preFader'` or `'postFader'` (default: `'postFader'`)
|
|
622
|
-
* @returns The new send's index
|
|
623
|
-
*/
|
|
624
|
-
addSend(stripIndex, id, destinationBusId, sendDb, timing = "postFader") {
|
|
625
|
-
return this.mixer.addSend(stripIndex, id, destinationBusId, sendDb, sendTimingCode(timing));
|
|
800
|
+
stop(renderFrame = -1) {
|
|
801
|
+
this.native.stop(renderFrame);
|
|
626
802
|
}
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
this.mixer.setSendDb(stripIndex, sendIndex, sendDb);
|
|
803
|
+
seekSample(timelineSample, renderFrame = -1) {
|
|
804
|
+
this.native.seekSample(timelineSample, renderFrame);
|
|
630
805
|
}
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
*
|
|
634
|
-
* @param stripIndex - Strip index in `[0, stripCount())`
|
|
635
|
-
* @param tap - `'preFader'` or `'postFader'` (default: `'postFader'`)
|
|
636
|
-
*/
|
|
637
|
-
meterTap(stripIndex, tap = "postFader") {
|
|
638
|
-
return this.mixer.meterTap(stripIndex, meterTapCode(tap));
|
|
806
|
+
seekPpq(ppq, renderFrame = -1) {
|
|
807
|
+
this.native.seekPpq(ppq, renderFrame);
|
|
639
808
|
}
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
* cross-binding (Node/Python) parity.
|
|
643
|
-
*
|
|
644
|
-
* @param stripIndex - Strip index in `[0, stripCount())`
|
|
645
|
-
* @param tap - `'preFader'` or `'postFader'` (default: `'postFader'`)
|
|
646
|
-
*/
|
|
647
|
-
stripMeter(stripIndex, tap = "postFader") {
|
|
648
|
-
return this.mixer.stripMeter(stripIndex, meterTapCode(tap));
|
|
809
|
+
setTempo(bpm) {
|
|
810
|
+
this.native.setTempo(bpm);
|
|
649
811
|
}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
*
|
|
653
|
-
* @param stripIndex - Strip index in `[0, stripCount())`
|
|
654
|
-
* @param samplePos - Absolute samples from the start of processing
|
|
655
|
-
* @param faderDb - Target fader level in dB
|
|
656
|
-
* @param curve - Interpolation curve (default: `'linear'`)
|
|
657
|
-
*/
|
|
658
|
-
scheduleFaderAutomation(stripIndex, samplePos, faderDb, curve = "linear") {
|
|
659
|
-
this.mixer.scheduleFaderAutomation(stripIndex, samplePos, faderDb, automationCurveCode(curve));
|
|
812
|
+
setTimeSignature(numerator, denominator) {
|
|
813
|
+
this.native.setTimeSignature(numerator, denominator);
|
|
660
814
|
}
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
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();
|
|
922
|
+
}
|
|
923
|
+
capturedAudio() {
|
|
924
|
+
return this.native.capturedAudio();
|
|
925
|
+
}
|
|
926
|
+
process(channels) {
|
|
927
|
+
return this.native.process(channels);
|
|
671
928
|
}
|
|
672
929
|
/**
|
|
673
|
-
*
|
|
674
|
-
*
|
|
675
|
-
*
|
|
676
|
-
*
|
|
677
|
-
* @param width - Target stereo width
|
|
678
|
-
* @param curve - Interpolation curve (default: `'linear'`)
|
|
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.
|
|
679
934
|
*/
|
|
680
|
-
|
|
681
|
-
this.
|
|
935
|
+
prepareChannels(numChannels, maxFrames) {
|
|
936
|
+
this.native.prepareChannels(numChannels, maxFrames);
|
|
682
937
|
}
|
|
683
938
|
/**
|
|
684
|
-
*
|
|
685
|
-
*
|
|
686
|
-
*
|
|
687
|
-
* @param sendIndex - Send index in the strip's add order
|
|
688
|
-
* @param samplePos - Absolute samples from the start of processing
|
|
689
|
-
* @param db - Target send level in dB
|
|
690
|
-
* @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.
|
|
691
942
|
*/
|
|
692
|
-
|
|
693
|
-
this.
|
|
694
|
-
stripIndex,
|
|
695
|
-
sendIndex,
|
|
696
|
-
samplePos,
|
|
697
|
-
db,
|
|
698
|
-
automationCurveCode(curve)
|
|
699
|
-
);
|
|
943
|
+
getChannelBuffer(channel, numFrames) {
|
|
944
|
+
return this.native.getChannelBuffer(channel, numFrames);
|
|
700
945
|
}
|
|
701
946
|
/**
|
|
702
|
-
*
|
|
703
|
-
*
|
|
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`.
|
|
704
950
|
*/
|
|
705
|
-
|
|
706
|
-
|
|
951
|
+
processPrepared(numFrames) {
|
|
952
|
+
this.native.processPrepared(numFrames);
|
|
707
953
|
}
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
return this.mixer.toSceneJson();
|
|
954
|
+
processWithMonitor(channels) {
|
|
955
|
+
return this.native.processWithMonitor(channels);
|
|
711
956
|
}
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
957
|
+
renderOffline(channels, blockSize = 128) {
|
|
958
|
+
return this.native.renderOffline(channels, blockSize);
|
|
959
|
+
}
|
|
960
|
+
bounceOffline(options) {
|
|
961
|
+
return this.native.bounceOffline(options);
|
|
962
|
+
}
|
|
963
|
+
freezeOffline(options) {
|
|
964
|
+
return this.native.freezeOffline(options);
|
|
965
|
+
}
|
|
966
|
+
drainTelemetry(maxRecords = 1024) {
|
|
967
|
+
return this.native.drainTelemetry(maxRecords);
|
|
968
|
+
}
|
|
969
|
+
drainMeterTelemetry(maxRecords = 1024) {
|
|
970
|
+
return this.native.drainMeterTelemetry(maxRecords);
|
|
715
971
|
}
|
|
716
|
-
/** Alias for {@link delete}, provided for cross-binding (Node) compatibility. */
|
|
717
972
|
destroy() {
|
|
718
|
-
this.delete();
|
|
973
|
+
this.native.delete();
|
|
719
974
|
}
|
|
720
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);
|
|
1000
|
+
}
|
|
1001
|
+
};
|
|
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
|
+
}
|
|
721
1028
|
|
|
722
1029
|
// src/worklet.ts
|
|
723
1030
|
var SONARE_METER_RING_HEADER_INTS = 4;
|