@dawcore/components 0.0.23 → 0.0.24
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 +155 -16
- package/dist/index.d.mts +248 -1
- package/dist/index.d.ts +248 -1
- package/dist/index.js +1269 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1266 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +17 -7
package/dist/index.mjs
CHANGED
|
@@ -201,6 +201,68 @@ var DawTrackElement = class extends LitElement2 {
|
|
|
201
201
|
createRenderRoot() {
|
|
202
202
|
return this;
|
|
203
203
|
}
|
|
204
|
+
// --- Effects API (delegates to the owning <daw-editor>) ---
|
|
205
|
+
addEffect(type, params) {
|
|
206
|
+
return this._effectsEditor()._trackAddEffect(this.trackId, this, type, params);
|
|
207
|
+
}
|
|
208
|
+
/** Load a WAM plugin (via the optional @dawcore/wam peer) into this track's chain. */
|
|
209
|
+
addWamPlugin(url, initialState) {
|
|
210
|
+
return this._effectsEditor()._trackAddWamPlugin(this.trackId, this, url, initialState);
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Compile Faust DSP source in the browser (via the optional @dawcore/faust
|
|
214
|
+
* peer) and add the resulting WAM to this track's chain. Compile errors
|
|
215
|
+
* keep their Faust line/column diagnostics and leave the chain untouched.
|
|
216
|
+
*/
|
|
217
|
+
addFaustEffect(dspCode, options) {
|
|
218
|
+
return this._effectsEditor()._trackAddFaustEffect(this.trackId, this, dspCode, options);
|
|
219
|
+
}
|
|
220
|
+
/** Snapshot this track's chain in its persisted form (see dawcore README). */
|
|
221
|
+
getEffectsState() {
|
|
222
|
+
const editor = this.closest("daw-editor");
|
|
223
|
+
return editor?._trackGetEffectsState(this.trackId) ?? Promise.resolve([]);
|
|
224
|
+
}
|
|
225
|
+
/** Replace this track's chain with a persisted snapshot. */
|
|
226
|
+
setEffectsState(entries) {
|
|
227
|
+
return this._effectsEditor()._trackSetEffectsState(this.trackId, this, entries);
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Open (lazily creating) the GUI for one of this track's effects into a
|
|
231
|
+
* consumer-provided container. Closing hides without interrupting audio;
|
|
232
|
+
* the element is cached for reopen. See <daw-editor>.openEffectGui.
|
|
233
|
+
*/
|
|
234
|
+
openEffectGui(effectId, container) {
|
|
235
|
+
return this._effectsEditor()._trackOpenEffectGui(this.trackId, this, effectId, container);
|
|
236
|
+
}
|
|
237
|
+
/** Hide an effect's GUI (cached for reopen — never destroys). */
|
|
238
|
+
closeEffectGui(effectId) {
|
|
239
|
+
this._effectsEditor()._trackCloseEffectGui(this.trackId, effectId);
|
|
240
|
+
}
|
|
241
|
+
removeEffect(effectId) {
|
|
242
|
+
this._effectsEditor()._trackEffectOp(this.trackId, this, "remove", effectId);
|
|
243
|
+
}
|
|
244
|
+
setEffectParams(effectId, params) {
|
|
245
|
+
this._effectsEditor()._trackEffectOp(this.trackId, this, "setParams", effectId, params);
|
|
246
|
+
}
|
|
247
|
+
setEffectBypassed(effectId, bypassed) {
|
|
248
|
+
this._effectsEditor()._trackEffectOp(this.trackId, this, "setBypassed", effectId, bypassed);
|
|
249
|
+
}
|
|
250
|
+
moveEffect(effectId, newIndex) {
|
|
251
|
+
this._effectsEditor()._trackEffectOp(this.trackId, this, "move", effectId, newIndex);
|
|
252
|
+
}
|
|
253
|
+
get effects() {
|
|
254
|
+
const editor = this.closest("daw-editor");
|
|
255
|
+
return editor?._trackEffects(this.trackId) ?? [];
|
|
256
|
+
}
|
|
257
|
+
_effectsEditor() {
|
|
258
|
+
const editor = this.closest("daw-editor");
|
|
259
|
+
if (!editor) {
|
|
260
|
+
throw new Error(
|
|
261
|
+
"[waveform-playlist] <daw-track> effects API requires the track to be inside a <daw-editor>"
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
return editor;
|
|
265
|
+
}
|
|
204
266
|
connectedCallback() {
|
|
205
267
|
super.connectedCallback();
|
|
206
268
|
setTimeout(() => {
|
|
@@ -1386,7 +1448,7 @@ function createPeaksWorker() {
|
|
|
1386
1448
|
}
|
|
1387
1449
|
const pending = /* @__PURE__ */ new Map();
|
|
1388
1450
|
let terminated = false;
|
|
1389
|
-
let
|
|
1451
|
+
let idCounter2 = 0;
|
|
1390
1452
|
worker.onmessage = (e) => {
|
|
1391
1453
|
const msg = e.data;
|
|
1392
1454
|
const entry = pending.get(msg.id);
|
|
@@ -1419,7 +1481,7 @@ function createPeaksWorker() {
|
|
|
1419
1481
|
return {
|
|
1420
1482
|
generate(params) {
|
|
1421
1483
|
if (terminated) return Promise.reject(new Error("Worker terminated"));
|
|
1422
|
-
const messageId = String(++
|
|
1484
|
+
const messageId = String(++idCounter2);
|
|
1423
1485
|
return new Promise((resolve, reject) => {
|
|
1424
1486
|
pending.set(messageId, { resolve, reject });
|
|
1425
1487
|
worker.postMessage(
|
|
@@ -4096,6 +4158,1088 @@ function findAudioBufferForClip(host, clip, track) {
|
|
|
4096
4158
|
return null;
|
|
4097
4159
|
}
|
|
4098
4160
|
|
|
4161
|
+
// src/effects/effects-chain-controller.ts
|
|
4162
|
+
var idCounter = 0;
|
|
4163
|
+
var EffectsChainController = class {
|
|
4164
|
+
constructor(audioContext) {
|
|
4165
|
+
this._entries = [];
|
|
4166
|
+
this._disposed = false;
|
|
4167
|
+
this._input = audioContext.createGain();
|
|
4168
|
+
this._output = audioContext.createGain();
|
|
4169
|
+
this._input.connect(this._output);
|
|
4170
|
+
}
|
|
4171
|
+
/** Connect the upstream source (track mute node, master gain) into this. */
|
|
4172
|
+
get input() {
|
|
4173
|
+
return this._input;
|
|
4174
|
+
}
|
|
4175
|
+
/** Route this onward (to the master bus / destination). */
|
|
4176
|
+
get output() {
|
|
4177
|
+
return this._output;
|
|
4178
|
+
}
|
|
4179
|
+
get entries() {
|
|
4180
|
+
return this._entries.map((entry) => ({
|
|
4181
|
+
id: entry.id,
|
|
4182
|
+
kind: entry.kind,
|
|
4183
|
+
type: entry.type,
|
|
4184
|
+
params: { ...entry.params },
|
|
4185
|
+
bypassed: entry.bypassed,
|
|
4186
|
+
...entry.url !== void 0 ? { url: entry.url } : {},
|
|
4187
|
+
...entry.source !== void 0 ? { source: { ...entry.source } } : {},
|
|
4188
|
+
...entry.label !== void 0 ? { label: entry.label } : {},
|
|
4189
|
+
...entry.error !== void 0 ? { error: entry.error } : {}
|
|
4190
|
+
}));
|
|
4191
|
+
}
|
|
4192
|
+
/** Snapshot the chain in its persisted form. WAM entries are asked for
|
|
4193
|
+
* their live state; placeholders re-emit the state they were saved with. */
|
|
4194
|
+
serialize() {
|
|
4195
|
+
return Promise.all(
|
|
4196
|
+
this._entries.map(async (entry) => {
|
|
4197
|
+
if (entry.kind === "wam") {
|
|
4198
|
+
const sourceFields = entry.source?.faust !== void 0 ? {
|
|
4199
|
+
faustDsp: entry.source.faust,
|
|
4200
|
+
...entry.label !== void 0 ? { faustName: entry.label } : {}
|
|
4201
|
+
} : { url: entry.url ?? "" };
|
|
4202
|
+
if (entry.placeholder) {
|
|
4203
|
+
return {
|
|
4204
|
+
kind: "wam",
|
|
4205
|
+
...sourceFields,
|
|
4206
|
+
bypassed: entry.placeholder.bypassed,
|
|
4207
|
+
...entry.placeholder.state !== void 0 ? { state: entry.placeholder.state } : {}
|
|
4208
|
+
};
|
|
4209
|
+
}
|
|
4210
|
+
let state5;
|
|
4211
|
+
try {
|
|
4212
|
+
state5 = await entry.instance.getState?.();
|
|
4213
|
+
} catch (err) {
|
|
4214
|
+
console.warn(
|
|
4215
|
+
'[waveform-playlist] serialize: plugin "' + (entry.url ?? entry.label ?? entry.type) + '" getState failed: ' + String(err)
|
|
4216
|
+
);
|
|
4217
|
+
}
|
|
4218
|
+
return {
|
|
4219
|
+
kind: "wam",
|
|
4220
|
+
...sourceFields,
|
|
4221
|
+
bypassed: entry.bypassed,
|
|
4222
|
+
...state5 !== void 0 ? { state: state5 } : {}
|
|
4223
|
+
};
|
|
4224
|
+
}
|
|
4225
|
+
return {
|
|
4226
|
+
kind: "native",
|
|
4227
|
+
type: entry.type,
|
|
4228
|
+
params: { ...entry.params },
|
|
4229
|
+
bypassed: entry.bypassed
|
|
4230
|
+
};
|
|
4231
|
+
})
|
|
4232
|
+
);
|
|
4233
|
+
}
|
|
4234
|
+
get disposed() {
|
|
4235
|
+
return this._disposed;
|
|
4236
|
+
}
|
|
4237
|
+
/** Internal (manager-facing): the live entry — including its instance —
|
|
4238
|
+
* for GUI wiring. Returns undefined (no warning) when the id is unknown. */
|
|
4239
|
+
getEntry(effectId) {
|
|
4240
|
+
return this._entries.find((e) => e.id === effectId);
|
|
4241
|
+
}
|
|
4242
|
+
add(item, index) {
|
|
4243
|
+
if (this._disposed) {
|
|
4244
|
+
throw new Error(
|
|
4245
|
+
"[waveform-playlist] EffectsChainController.add: chain is disposed \u2014 entries cannot be added"
|
|
4246
|
+
);
|
|
4247
|
+
}
|
|
4248
|
+
const id = "effect-" + ++idCounter;
|
|
4249
|
+
const entry = { ...item, params: { ...item.params }, id, bypassed: false };
|
|
4250
|
+
const at = index === void 0 ? this._entries.length : Math.max(0, Math.min(index, this._entries.length));
|
|
4251
|
+
this._entries = [...this._entries.slice(0, at), entry, ...this._entries.slice(at)];
|
|
4252
|
+
this._rebuild();
|
|
4253
|
+
return id;
|
|
4254
|
+
}
|
|
4255
|
+
remove(effectId) {
|
|
4256
|
+
const entry = this._find("remove", effectId);
|
|
4257
|
+
if (!entry) return;
|
|
4258
|
+
this._entries = this._entries.filter((e) => e.id !== effectId);
|
|
4259
|
+
entry.instance.output.disconnect();
|
|
4260
|
+
entry.instance.dispose?.();
|
|
4261
|
+
this._rebuild();
|
|
4262
|
+
}
|
|
4263
|
+
move(effectId, newIndex) {
|
|
4264
|
+
const entry = this._find("move", effectId);
|
|
4265
|
+
if (!entry) return;
|
|
4266
|
+
const without = this._entries.filter((e) => e.id !== effectId);
|
|
4267
|
+
const at = Math.max(0, Math.min(newIndex, without.length));
|
|
4268
|
+
this._entries = [...without.slice(0, at), entry, ...without.slice(at)];
|
|
4269
|
+
this._rebuild();
|
|
4270
|
+
}
|
|
4271
|
+
setParams(effectId, params) {
|
|
4272
|
+
const entry = this._find("setParams", effectId);
|
|
4273
|
+
if (!entry) return;
|
|
4274
|
+
entry.params = { ...entry.params, ...params };
|
|
4275
|
+
if (entry.bypassed && entry.wetParam && entry.wetParam in params) {
|
|
4276
|
+
const { [entry.wetParam]: _stored, ...rest } = params;
|
|
4277
|
+
if (Object.keys(rest).length > 0) {
|
|
4278
|
+
entry.instance.applyParams(rest);
|
|
4279
|
+
}
|
|
4280
|
+
return;
|
|
4281
|
+
}
|
|
4282
|
+
entry.instance.applyParams(params);
|
|
4283
|
+
}
|
|
4284
|
+
setBypassed(effectId, bypassed) {
|
|
4285
|
+
const entry = this._find("setBypassed", effectId);
|
|
4286
|
+
if (!entry || entry.bypassed === bypassed) return;
|
|
4287
|
+
entry.bypassed = bypassed;
|
|
4288
|
+
if (entry.wetParam) {
|
|
4289
|
+
const wet = bypassed ? 0 : entry.params[entry.wetParam] ?? 0;
|
|
4290
|
+
entry.instance.applyParams({ [entry.wetParam]: wet });
|
|
4291
|
+
return;
|
|
4292
|
+
}
|
|
4293
|
+
this._rebuild();
|
|
4294
|
+
}
|
|
4295
|
+
dispose() {
|
|
4296
|
+
if (this._disposed) return;
|
|
4297
|
+
this._disposed = true;
|
|
4298
|
+
this._severOwnEdges();
|
|
4299
|
+
for (const entry of this._entries) {
|
|
4300
|
+
entry.instance.dispose?.();
|
|
4301
|
+
}
|
|
4302
|
+
this._entries = [];
|
|
4303
|
+
try {
|
|
4304
|
+
this._output.disconnect();
|
|
4305
|
+
} catch {
|
|
4306
|
+
}
|
|
4307
|
+
}
|
|
4308
|
+
_find(op, effectId) {
|
|
4309
|
+
const entry = this._entries.find((e) => e.id === effectId);
|
|
4310
|
+
if (!entry) {
|
|
4311
|
+
console.warn(
|
|
4312
|
+
"[waveform-playlist] EffectsChainController." + op + ': unknown effectId "' + effectId + '"'
|
|
4313
|
+
);
|
|
4314
|
+
}
|
|
4315
|
+
return entry;
|
|
4316
|
+
}
|
|
4317
|
+
/** Sever only this chain's outgoing edges — never the consumer's. */
|
|
4318
|
+
_severOwnEdges() {
|
|
4319
|
+
this._input.disconnect();
|
|
4320
|
+
for (const entry of this._entries) {
|
|
4321
|
+
entry.instance.output.disconnect();
|
|
4322
|
+
}
|
|
4323
|
+
}
|
|
4324
|
+
_rebuild() {
|
|
4325
|
+
this._severOwnEdges();
|
|
4326
|
+
let previous = this._input;
|
|
4327
|
+
for (const entry of this._entries) {
|
|
4328
|
+
if (entry.bypassed && !entry.wetParam) {
|
|
4329
|
+
continue;
|
|
4330
|
+
}
|
|
4331
|
+
previous.connect(entry.instance.input);
|
|
4332
|
+
previous = entry.instance.output;
|
|
4333
|
+
}
|
|
4334
|
+
previous.connect(this._output);
|
|
4335
|
+
}
|
|
4336
|
+
};
|
|
4337
|
+
|
|
4338
|
+
// src/effects/optional-modules.ts
|
|
4339
|
+
var PREFIX = "[waveform-playlist] ";
|
|
4340
|
+
async function loadOptionalModule(importer, packageName, feature) {
|
|
4341
|
+
try {
|
|
4342
|
+
return await importer();
|
|
4343
|
+
} catch (originalErr) {
|
|
4344
|
+
console.warn(PREFIX + packageName + " dynamic import failed: " + String(originalErr));
|
|
4345
|
+
throw new Error(
|
|
4346
|
+
PREFIX + packageName + " is required for " + feature + ". Install with: npm install " + packageName + " (import failed: " + String(originalErr) + ")"
|
|
4347
|
+
);
|
|
4348
|
+
}
|
|
4349
|
+
}
|
|
4350
|
+
function loadWamModule(feature) {
|
|
4351
|
+
return loadOptionalModule(() => import("@dawcore/wam"), "@dawcore/wam", feature);
|
|
4352
|
+
}
|
|
4353
|
+
function loadFaustModule(feature) {
|
|
4354
|
+
return loadOptionalModule(() => import("@dawcore/faust"), "@dawcore/faust", feature);
|
|
4355
|
+
}
|
|
4356
|
+
|
|
4357
|
+
// src/effects/effect-registry.ts
|
|
4358
|
+
var MAX_DELAY_SECONDS = 10;
|
|
4359
|
+
var registry = /* @__PURE__ */ new Map();
|
|
4360
|
+
function registerEffect(type, definition) {
|
|
4361
|
+
if (registry.has(type)) {
|
|
4362
|
+
console.warn(
|
|
4363
|
+
'[waveform-playlist] registerEffect: overwriting existing effect type "' + type + '"'
|
|
4364
|
+
);
|
|
4365
|
+
}
|
|
4366
|
+
registry.set(type, definition);
|
|
4367
|
+
}
|
|
4368
|
+
function getEffectDefinitions() {
|
|
4369
|
+
return new Map(registry);
|
|
4370
|
+
}
|
|
4371
|
+
function createEffectInstance(type, audioContext, params = {}) {
|
|
4372
|
+
const definition = registry.get(type);
|
|
4373
|
+
if (!definition) {
|
|
4374
|
+
throw new Error(
|
|
4375
|
+
'[waveform-playlist] createEffectInstance: unknown effect type "' + type + '". Available types: ' + [...registry.keys()].join(", ")
|
|
4376
|
+
);
|
|
4377
|
+
}
|
|
4378
|
+
const merged = { ...definition.defaults, ...params };
|
|
4379
|
+
const instance = definition.create(audioContext, merged);
|
|
4380
|
+
instance.applyParams(merged);
|
|
4381
|
+
return { instance, params: merged, wetParam: definition.wetParam };
|
|
4382
|
+
}
|
|
4383
|
+
function singleNode(node, applyParams) {
|
|
4384
|
+
return { input: node, output: node, applyParams };
|
|
4385
|
+
}
|
|
4386
|
+
function registerBuiltIns() {
|
|
4387
|
+
registry.set("native-gain", {
|
|
4388
|
+
label: "Gain",
|
|
4389
|
+
category: "dynamics",
|
|
4390
|
+
defaults: { gain: 1 },
|
|
4391
|
+
params: { gain: { min: 0, max: 2, step: 0.01 } },
|
|
4392
|
+
create: (ctx) => {
|
|
4393
|
+
const node = ctx.createGain();
|
|
4394
|
+
return singleNode(node, (p) => {
|
|
4395
|
+
if (p.gain !== void 0) node.gain.value = p.gain;
|
|
4396
|
+
});
|
|
4397
|
+
}
|
|
4398
|
+
});
|
|
4399
|
+
registry.set("native-filter", {
|
|
4400
|
+
label: "Lowpass Filter",
|
|
4401
|
+
category: "filter",
|
|
4402
|
+
defaults: { frequency: 1e3, q: 1 },
|
|
4403
|
+
params: {
|
|
4404
|
+
frequency: { min: 20, max: 2e4, step: 1, unit: "Hz" },
|
|
4405
|
+
q: { min: 0.1, max: 20, step: 0.1 }
|
|
4406
|
+
},
|
|
4407
|
+
create: (ctx) => {
|
|
4408
|
+
const node = ctx.createBiquadFilter();
|
|
4409
|
+
node.type = "lowpass";
|
|
4410
|
+
return singleNode(node, (p) => {
|
|
4411
|
+
if (p.frequency !== void 0) node.frequency.value = p.frequency;
|
|
4412
|
+
if (p.q !== void 0) node.Q.value = p.q;
|
|
4413
|
+
});
|
|
4414
|
+
}
|
|
4415
|
+
});
|
|
4416
|
+
registry.set("native-compressor", {
|
|
4417
|
+
label: "Compressor",
|
|
4418
|
+
category: "dynamics",
|
|
4419
|
+
defaults: { threshold: -24, knee: 30, ratio: 12, attack: 3e-3, release: 0.25 },
|
|
4420
|
+
params: {
|
|
4421
|
+
threshold: { min: -60, max: 0, step: 1, unit: "dB" },
|
|
4422
|
+
knee: { min: 0, max: 40, step: 1, unit: "dB" },
|
|
4423
|
+
ratio: { min: 1, max: 20, step: 0.5 },
|
|
4424
|
+
attack: { min: 0, max: 1, step: 1e-3, unit: "s" },
|
|
4425
|
+
release: { min: 0, max: 1, step: 0.01, unit: "s" }
|
|
4426
|
+
},
|
|
4427
|
+
create: (ctx) => {
|
|
4428
|
+
const node = ctx.createDynamicsCompressor();
|
|
4429
|
+
return singleNode(node, (p) => {
|
|
4430
|
+
if (p.threshold !== void 0) node.threshold.value = p.threshold;
|
|
4431
|
+
if (p.knee !== void 0) node.knee.value = p.knee;
|
|
4432
|
+
if (p.ratio !== void 0) node.ratio.value = p.ratio;
|
|
4433
|
+
if (p.attack !== void 0) node.attack.value = p.attack;
|
|
4434
|
+
if (p.release !== void 0) node.release.value = p.release;
|
|
4435
|
+
});
|
|
4436
|
+
}
|
|
4437
|
+
});
|
|
4438
|
+
registry.set("native-stereo-panner", {
|
|
4439
|
+
label: "Stereo Panner",
|
|
4440
|
+
category: "spatial",
|
|
4441
|
+
defaults: { pan: 0 },
|
|
4442
|
+
params: { pan: { min: -1, max: 1, step: 0.01 } },
|
|
4443
|
+
create: (ctx) => {
|
|
4444
|
+
const node = ctx.createStereoPanner();
|
|
4445
|
+
return singleNode(node, (p) => {
|
|
4446
|
+
if (p.pan !== void 0) node.pan.value = p.pan;
|
|
4447
|
+
});
|
|
4448
|
+
}
|
|
4449
|
+
});
|
|
4450
|
+
registry.set("native-delay", {
|
|
4451
|
+
label: "Delay",
|
|
4452
|
+
category: "delay",
|
|
4453
|
+
defaults: { delayTime: 0.25, feedback: 0.4, wet: 0.35 },
|
|
4454
|
+
params: {
|
|
4455
|
+
delayTime: { min: 0, max: MAX_DELAY_SECONDS, step: 0.01, unit: "s" },
|
|
4456
|
+
feedback: { min: 0, max: 0.95, step: 0.01 },
|
|
4457
|
+
wet: { min: 0, max: 1, step: 0.01 }
|
|
4458
|
+
},
|
|
4459
|
+
wetParam: "wet",
|
|
4460
|
+
create: (ctx) => {
|
|
4461
|
+
const input = ctx.createGain();
|
|
4462
|
+
const output = ctx.createGain();
|
|
4463
|
+
const delay = ctx.createDelay(MAX_DELAY_SECONDS);
|
|
4464
|
+
const feedback = ctx.createGain();
|
|
4465
|
+
const wet = ctx.createGain();
|
|
4466
|
+
const dry = ctx.createGain();
|
|
4467
|
+
input.connect(dry);
|
|
4468
|
+
dry.connect(output);
|
|
4469
|
+
input.connect(delay);
|
|
4470
|
+
delay.connect(feedback);
|
|
4471
|
+
feedback.connect(delay);
|
|
4472
|
+
delay.connect(wet);
|
|
4473
|
+
wet.connect(output);
|
|
4474
|
+
return {
|
|
4475
|
+
input,
|
|
4476
|
+
output,
|
|
4477
|
+
applyParams: (p) => {
|
|
4478
|
+
if (p.delayTime !== void 0) delay.delayTime.value = p.delayTime;
|
|
4479
|
+
if (p.feedback !== void 0) feedback.gain.value = p.feedback;
|
|
4480
|
+
if (p.wet !== void 0) {
|
|
4481
|
+
wet.gain.value = p.wet;
|
|
4482
|
+
dry.gain.value = 1 - p.wet;
|
|
4483
|
+
}
|
|
4484
|
+
}
|
|
4485
|
+
};
|
|
4486
|
+
}
|
|
4487
|
+
});
|
|
4488
|
+
}
|
|
4489
|
+
registerBuiltIns();
|
|
4490
|
+
|
|
4491
|
+
// src/effects/effects-manager.ts
|
|
4492
|
+
var PREFIX2 = "[waveform-playlist] ";
|
|
4493
|
+
function makeGuiRecord(element, destroyImpl) {
|
|
4494
|
+
let destroyed = false;
|
|
4495
|
+
return {
|
|
4496
|
+
element,
|
|
4497
|
+
destroy: () => {
|
|
4498
|
+
if (destroyed) return;
|
|
4499
|
+
destroyed = true;
|
|
4500
|
+
destroyImpl();
|
|
4501
|
+
}
|
|
4502
|
+
};
|
|
4503
|
+
}
|
|
4504
|
+
var EffectsManager = class {
|
|
4505
|
+
constructor(getAdapter, masterEventTarget) {
|
|
4506
|
+
this._masterChain = null;
|
|
4507
|
+
this._trackChains = /* @__PURE__ */ new Map();
|
|
4508
|
+
/** Per-chain restore ownership — a newer setEffectsState supersedes a stale in-flight one. */
|
|
4509
|
+
this._restoreTokens = /* @__PURE__ */ new WeakMap();
|
|
4510
|
+
/** Cached GUI elements by effectId — close hides, only removal destroys. */
|
|
4511
|
+
this._guis = /* @__PURE__ */ new Map();
|
|
4512
|
+
/** In-flight GUI creation by effectId — concurrent opens share one build. */
|
|
4513
|
+
this._guiPending = /* @__PURE__ */ new Map();
|
|
4514
|
+
/** Live WAM plugin nodes across all chains, fed to the wam-transport bridge. */
|
|
4515
|
+
this._wamNodes = /* @__PURE__ */ new Set();
|
|
4516
|
+
this._transportBridge = null;
|
|
4517
|
+
this._getAdapter = getAdapter;
|
|
4518
|
+
this._masterTarget = masterEventTarget;
|
|
4519
|
+
}
|
|
4520
|
+
// --- Master chain ---
|
|
4521
|
+
addMasterEffect(type, params) {
|
|
4522
|
+
const chain = this._ensureMasterChain();
|
|
4523
|
+
return this._addToChain(chain, this._masterTarget, type, params);
|
|
4524
|
+
}
|
|
4525
|
+
masterEffects() {
|
|
4526
|
+
return this._masterChain?.entries ?? [];
|
|
4527
|
+
}
|
|
4528
|
+
masterOp(op, effectId, arg) {
|
|
4529
|
+
this._runOp(this._masterChain, this._masterTarget, op, effectId, arg);
|
|
4530
|
+
}
|
|
4531
|
+
addMasterWamPlugin(url, initialState) {
|
|
4532
|
+
const chain = this._ensureMasterChain();
|
|
4533
|
+
return this._addWamToChain(chain, this._masterTarget, url, initialState);
|
|
4534
|
+
}
|
|
4535
|
+
addMasterFaustEffect(dspCode, options) {
|
|
4536
|
+
const chain = this._ensureMasterChain();
|
|
4537
|
+
return this._addFaustToChain(chain, this._masterTarget, dspCode, { name: options?.name });
|
|
4538
|
+
}
|
|
4539
|
+
getMasterEffectsState() {
|
|
4540
|
+
return this._masterChain?.serialize() ?? Promise.resolve([]);
|
|
4541
|
+
}
|
|
4542
|
+
async setMasterEffectsState(entries) {
|
|
4543
|
+
validateSerializedEntries(entries);
|
|
4544
|
+
await this._restoreChain(this._ensureMasterChain(), this._masterTarget, entries);
|
|
4545
|
+
}
|
|
4546
|
+
// --- Track chains ---
|
|
4547
|
+
addTrackEffect(trackId, target, type, params) {
|
|
4548
|
+
const chain = this._ensureTrackChain(trackId);
|
|
4549
|
+
return this._addToChain(chain, target, type, params);
|
|
4550
|
+
}
|
|
4551
|
+
addTrackWamPlugin(trackId, target, url, initialState) {
|
|
4552
|
+
const chain = this._ensureTrackChain(trackId);
|
|
4553
|
+
return this._addWamToChain(chain, target, url, initialState);
|
|
4554
|
+
}
|
|
4555
|
+
addTrackFaustEffect(trackId, target, dspCode, options) {
|
|
4556
|
+
const chain = this._ensureTrackChain(trackId);
|
|
4557
|
+
return this._addFaustToChain(chain, target, dspCode, { name: options?.name });
|
|
4558
|
+
}
|
|
4559
|
+
trackEffects(trackId) {
|
|
4560
|
+
return this._trackChains.get(trackId)?.entries ?? [];
|
|
4561
|
+
}
|
|
4562
|
+
getTrackEffectsState(trackId) {
|
|
4563
|
+
return this._trackChains.get(trackId)?.serialize() ?? Promise.resolve([]);
|
|
4564
|
+
}
|
|
4565
|
+
async setTrackEffectsState(trackId, target, entries) {
|
|
4566
|
+
validateSerializedEntries(entries);
|
|
4567
|
+
await this._restoreChain(this._ensureTrackChain(trackId), target, entries);
|
|
4568
|
+
}
|
|
4569
|
+
trackOp(trackId, target, op, effectId, arg) {
|
|
4570
|
+
this._runOp(this._trackChains.get(trackId) ?? null, target, op, effectId, arg);
|
|
4571
|
+
}
|
|
4572
|
+
// --- Effect GUIs ---
|
|
4573
|
+
/** Open (lazily creating) the GUI for a master-chain effect. */
|
|
4574
|
+
openMasterEffectGui(effectId, container) {
|
|
4575
|
+
return this._openGui(this._masterChain, this._masterTarget, effectId, container);
|
|
4576
|
+
}
|
|
4577
|
+
/** Open (lazily creating) the GUI for a track-chain effect. */
|
|
4578
|
+
openTrackEffectGui(trackId, target, effectId, container) {
|
|
4579
|
+
return this._openGui(this._trackChains.get(trackId) ?? null, target, effectId, container);
|
|
4580
|
+
}
|
|
4581
|
+
/** Hide an open GUI. The element stays cached so reopen is instant —
|
|
4582
|
+
* audio processing is never interrupted. */
|
|
4583
|
+
closeEffectGui(effectId) {
|
|
4584
|
+
const record = this._guis.get(effectId);
|
|
4585
|
+
if (!record) {
|
|
4586
|
+
console.warn(PREFIX2 + 'closeEffectGui: no open GUI for effectId "' + effectId + '"');
|
|
4587
|
+
return;
|
|
4588
|
+
}
|
|
4589
|
+
record.element.remove();
|
|
4590
|
+
}
|
|
4591
|
+
// --- Lifecycle ---
|
|
4592
|
+
/** Transport setTracks rebuilds TrackNodes, severing chain hookups — re-wire. */
|
|
4593
|
+
rewireTrackChains() {
|
|
4594
|
+
const transport = this._getAdapter()?.transport;
|
|
4595
|
+
if (!transport) return;
|
|
4596
|
+
for (const [trackId, chain] of this._trackChains) {
|
|
4597
|
+
transport.connectTrackOutput(trackId, chain.input);
|
|
4598
|
+
chain.output.connect(transport.masterOutputNode);
|
|
4599
|
+
}
|
|
4600
|
+
}
|
|
4601
|
+
disposeTrackChain(trackId) {
|
|
4602
|
+
const chain = this._trackChains.get(trackId);
|
|
4603
|
+
if (!chain) return;
|
|
4604
|
+
this._trackChains.delete(trackId);
|
|
4605
|
+
for (const entry of chain.entries) {
|
|
4606
|
+
this._destroyGui(entry.id);
|
|
4607
|
+
}
|
|
4608
|
+
try {
|
|
4609
|
+
this._getAdapter()?.transport?.disconnectTrackOutput(trackId);
|
|
4610
|
+
} catch (err) {
|
|
4611
|
+
console.warn(PREFIX2 + "EffectsManager: error disconnecting track output: " + String(err));
|
|
4612
|
+
}
|
|
4613
|
+
chain.dispose();
|
|
4614
|
+
}
|
|
4615
|
+
disposeAll() {
|
|
4616
|
+
this._transportBridge?.dispose();
|
|
4617
|
+
this._transportBridge = null;
|
|
4618
|
+
this._wamNodes.clear();
|
|
4619
|
+
for (const trackId of [...this._trackChains.keys()]) {
|
|
4620
|
+
this.disposeTrackChain(trackId);
|
|
4621
|
+
}
|
|
4622
|
+
if (this._masterChain) {
|
|
4623
|
+
for (const entry of this._masterChain.entries) {
|
|
4624
|
+
this._destroyGui(entry.id);
|
|
4625
|
+
}
|
|
4626
|
+
try {
|
|
4627
|
+
this._getAdapter()?.transport?.disconnectMasterOutput();
|
|
4628
|
+
} catch (err) {
|
|
4629
|
+
console.warn(PREFIX2 + "EffectsManager: error disconnecting master output: " + String(err));
|
|
4630
|
+
}
|
|
4631
|
+
this._masterChain.dispose();
|
|
4632
|
+
this._masterChain = null;
|
|
4633
|
+
}
|
|
4634
|
+
}
|
|
4635
|
+
// --- Private ---
|
|
4636
|
+
_requireWiring() {
|
|
4637
|
+
const adapter = this._getAdapter();
|
|
4638
|
+
if (!adapter) {
|
|
4639
|
+
throw new Error(
|
|
4640
|
+
PREFIX2 + "effects require an adapter \u2014 set editor.adapter before adding effects."
|
|
4641
|
+
);
|
|
4642
|
+
}
|
|
4643
|
+
const { audioContext, transport } = adapter;
|
|
4644
|
+
if (!audioContext || !transport || typeof transport.connectTrackOutput !== "function") {
|
|
4645
|
+
throw new Error(
|
|
4646
|
+
PREFIX2 + "the current adapter does not expose effects hooks (transport.connectTrackOutput / connectMasterOutput)."
|
|
4647
|
+
);
|
|
4648
|
+
}
|
|
4649
|
+
return { audioContext, transport };
|
|
4650
|
+
}
|
|
4651
|
+
/** Dynamic-import the optional @dawcore/wam peer with an actionable error. */
|
|
4652
|
+
_loadWamModule(feature) {
|
|
4653
|
+
return loadWamModule(feature);
|
|
4654
|
+
}
|
|
4655
|
+
/** Dynamic-import the optional @dawcore/faust peer with an actionable error. */
|
|
4656
|
+
_loadFaustModule(feature) {
|
|
4657
|
+
return loadFaustModule(feature);
|
|
4658
|
+
}
|
|
4659
|
+
async _openGui(chain, target, effectId, container) {
|
|
4660
|
+
if (!container || typeof container.appendChild !== "function") {
|
|
4661
|
+
throw new Error(PREFIX2 + "openEffectGui: container must be a DOM element");
|
|
4662
|
+
}
|
|
4663
|
+
const entry = chain?.getEntry(effectId);
|
|
4664
|
+
if (!chain || !entry) {
|
|
4665
|
+
throw new Error(PREFIX2 + 'openEffectGui: unknown effectId "' + effectId + '"');
|
|
4666
|
+
}
|
|
4667
|
+
if (entry.error !== void 0) {
|
|
4668
|
+
throw new Error(
|
|
4669
|
+
PREFIX2 + 'openEffectGui: effect "' + effectId + '" is a failed-plugin placeholder (' + entry.error + ") \u2014 no GUI is available. Remove it or retry the restore."
|
|
4670
|
+
);
|
|
4671
|
+
}
|
|
4672
|
+
const cached = this._guis.get(effectId);
|
|
4673
|
+
if (cached) {
|
|
4674
|
+
container.appendChild(cached.element);
|
|
4675
|
+
return cached.element;
|
|
4676
|
+
}
|
|
4677
|
+
let pending = this._guiPending.get(effectId);
|
|
4678
|
+
if (!pending) {
|
|
4679
|
+
pending = this._createGuiRecord(chain, target, entry, effectId).finally(() => {
|
|
4680
|
+
this._guiPending.delete(effectId);
|
|
4681
|
+
});
|
|
4682
|
+
this._guiPending.set(effectId, pending);
|
|
4683
|
+
}
|
|
4684
|
+
const record = await pending;
|
|
4685
|
+
if (chain.disposed || !chain.getEntry(effectId)) {
|
|
4686
|
+
this._guis.delete(effectId);
|
|
4687
|
+
record.element.remove();
|
|
4688
|
+
try {
|
|
4689
|
+
record.destroy();
|
|
4690
|
+
} catch (err) {
|
|
4691
|
+
console.warn(
|
|
4692
|
+
PREFIX2 + 'openEffectGui: destroying a late GUI for "' + effectId + '" failed: ' + String(err)
|
|
4693
|
+
);
|
|
4694
|
+
}
|
|
4695
|
+
throw new Error(
|
|
4696
|
+
PREFIX2 + 'openEffectGui: effect "' + effectId + '" was removed while its GUI was loading; the GUI was discarded.'
|
|
4697
|
+
);
|
|
4698
|
+
}
|
|
4699
|
+
this._guis.set(effectId, record);
|
|
4700
|
+
container.appendChild(record.element);
|
|
4701
|
+
return record.element;
|
|
4702
|
+
}
|
|
4703
|
+
/** Build a GUI record: the plugin's own GUI when available, otherwise the
|
|
4704
|
+
* generic parameter panel from @dawcore/wam. */
|
|
4705
|
+
async _createGuiRecord(chain, target, entry, effectId) {
|
|
4706
|
+
const { instance } = entry;
|
|
4707
|
+
if (typeof instance.createGui === "function") {
|
|
4708
|
+
try {
|
|
4709
|
+
const element2 = await instance.createGui();
|
|
4710
|
+
return makeGuiRecord(element2, () => instance.destroyGui?.(element2));
|
|
4711
|
+
} catch (err) {
|
|
4712
|
+
console.warn(
|
|
4713
|
+
PREFIX2 + 'openEffectGui: plugin createGui failed for "' + effectId + '" \u2014 falling back to the generic parameter panel: ' + String(err)
|
|
4714
|
+
);
|
|
4715
|
+
}
|
|
4716
|
+
}
|
|
4717
|
+
const element = await this._createFallbackPanel(chain, target, entry, effectId);
|
|
4718
|
+
return makeGuiRecord(element, () => {
|
|
4719
|
+
});
|
|
4720
|
+
}
|
|
4721
|
+
/** The generic parameter panel — one code path for "no custom GUI":
|
|
4722
|
+
* native entries render from the registry's params metadata, WAM entries
|
|
4723
|
+
* from getParameterInfo(). Edits route through the regular setParams op so
|
|
4724
|
+
* they hit the audio (applyParams → setParameterValues for WAM) AND
|
|
4725
|
+
* dispatch daw-effect-change like any other parameter edit. */
|
|
4726
|
+
async _createFallbackPanel(chain, target, entry, effectId) {
|
|
4727
|
+
const wamModule = await this._loadWamModule("openEffectGui() parameter panels");
|
|
4728
|
+
const onChange = (paramId, value) => {
|
|
4729
|
+
this._runOp(chain, target, "setParams", effectId, { [paramId]: value });
|
|
4730
|
+
};
|
|
4731
|
+
if (entry.kind === "native") {
|
|
4732
|
+
const definition = getEffectDefinitions().get(entry.type);
|
|
4733
|
+
if (!definition) {
|
|
4734
|
+
throw new Error(
|
|
4735
|
+
PREFIX2 + 'openEffectGui: no registry definition for effect type "' + entry.type + '"'
|
|
4736
|
+
);
|
|
4737
|
+
}
|
|
4738
|
+
const params = Object.entries(definition.params).map(([id, def]) => ({
|
|
4739
|
+
id,
|
|
4740
|
+
min: def.min,
|
|
4741
|
+
max: def.max,
|
|
4742
|
+
...def.step !== void 0 ? { step: def.step } : {},
|
|
4743
|
+
...def.unit !== void 0 ? { unit: def.unit } : {},
|
|
4744
|
+
value: entry.params[id] ?? definition.defaults[id]
|
|
4745
|
+
}));
|
|
4746
|
+
return wamModule.createParameterPanel(params, onChange);
|
|
4747
|
+
}
|
|
4748
|
+
if (typeof entry.instance.getParameterInfo !== "function") {
|
|
4749
|
+
throw new Error(
|
|
4750
|
+
PREFIX2 + 'openEffectGui: effect "' + effectId + '" has no GUI and exposes no parameter info \u2014 nothing to render.'
|
|
4751
|
+
);
|
|
4752
|
+
}
|
|
4753
|
+
return wamModule.createWamParameterPanel(
|
|
4754
|
+
{ getParameterInfo: () => entry.instance.getParameterInfo() },
|
|
4755
|
+
{ onParamChange: onChange }
|
|
4756
|
+
);
|
|
4757
|
+
}
|
|
4758
|
+
/** Detach + destroy a cached GUI. Called only from removal paths — close
|
|
4759
|
+
* never destroys. Safe when no GUI was ever opened. */
|
|
4760
|
+
_destroyGui(effectId) {
|
|
4761
|
+
const record = this._guis.get(effectId);
|
|
4762
|
+
if (!record) return;
|
|
4763
|
+
this._guis.delete(effectId);
|
|
4764
|
+
record.element.remove();
|
|
4765
|
+
try {
|
|
4766
|
+
record.destroy();
|
|
4767
|
+
} catch (err) {
|
|
4768
|
+
console.warn(PREFIX2 + 'destroyGui failed for effect "' + effectId + '": ' + String(err));
|
|
4769
|
+
}
|
|
4770
|
+
}
|
|
4771
|
+
_ensureMasterChain() {
|
|
4772
|
+
if (this._masterChain) return this._masterChain;
|
|
4773
|
+
const { audioContext, transport } = this._requireWiring();
|
|
4774
|
+
const chain = new EffectsChainController(audioContext);
|
|
4775
|
+
transport.connectMasterOutput(chain.input);
|
|
4776
|
+
chain.output.connect(audioContext.destination);
|
|
4777
|
+
this._masterChain = chain;
|
|
4778
|
+
return chain;
|
|
4779
|
+
}
|
|
4780
|
+
_ensureTrackChain(trackId) {
|
|
4781
|
+
const existing = this._trackChains.get(trackId);
|
|
4782
|
+
if (existing) return existing;
|
|
4783
|
+
const { audioContext, transport } = this._requireWiring();
|
|
4784
|
+
const chain = new EffectsChainController(audioContext);
|
|
4785
|
+
transport.connectTrackOutput(trackId, chain.input);
|
|
4786
|
+
chain.output.connect(transport.masterOutputNode);
|
|
4787
|
+
this._trackChains.set(trackId, chain);
|
|
4788
|
+
return chain;
|
|
4789
|
+
}
|
|
4790
|
+
_addToChain(chain, target, type, params) {
|
|
4791
|
+
const audioContext = this._requireWiring().audioContext;
|
|
4792
|
+
const created = createEffectInstance(type, audioContext, params);
|
|
4793
|
+
const effectId = chain.add({
|
|
4794
|
+
kind: "native",
|
|
4795
|
+
type,
|
|
4796
|
+
instance: created.instance,
|
|
4797
|
+
params: created.params,
|
|
4798
|
+
wetParam: created.wetParam
|
|
4799
|
+
});
|
|
4800
|
+
const index = chain.entries.findIndex((e) => e.id === effectId);
|
|
4801
|
+
this._dispatch(target, "daw-effect-add", {
|
|
4802
|
+
effectId,
|
|
4803
|
+
kind: "native",
|
|
4804
|
+
type,
|
|
4805
|
+
params: { ...created.params },
|
|
4806
|
+
index
|
|
4807
|
+
});
|
|
4808
|
+
return effectId;
|
|
4809
|
+
}
|
|
4810
|
+
/**
|
|
4811
|
+
* Load a WAM plugin (via the optional @dawcore/wam peer dep) and add it to
|
|
4812
|
+
* a chain as a kind:'wam' entry. WAM entries participate in every chain
|
|
4813
|
+
* operation with no special-casing: remove destroys the plugin (via the
|
|
4814
|
+
* entry's dispose), bypass uses disconnection semantics (no wet param),
|
|
4815
|
+
* and setParams maps onto the plugin's setParameterValues.
|
|
4816
|
+
*/
|
|
4817
|
+
async _addWamToChain(chain, target, url, initialState) {
|
|
4818
|
+
const { audioContext } = this._requireWiring();
|
|
4819
|
+
const wamModule = await this._loadWamModule("addWamPlugin()");
|
|
4820
|
+
const { hostGroupId } = await wamModule.ensureWamHost(audioContext);
|
|
4821
|
+
const plugin = await wamModule.createWamInstance(url, audioContext, hostGroupId, {
|
|
4822
|
+
initialState
|
|
4823
|
+
});
|
|
4824
|
+
return this._insertWamPlugin(chain, target, wamModule, plugin, { url });
|
|
4825
|
+
}
|
|
4826
|
+
/**
|
|
4827
|
+
* Compile Faust DSP source in the browser (via the optional @dawcore/faust
|
|
4828
|
+
* peer) and add the resulting WAM to a chain. Compilation happens BEFORE
|
|
4829
|
+
* any chain work, so a Faust error (with its line/column diagnostics intact)
|
|
4830
|
+
* leaves the chain untouched. The entry lands as kind:'wam' with a
|
|
4831
|
+
* `source: { faust }` marker so persistence recompiles instead of fetching.
|
|
4832
|
+
*/
|
|
4833
|
+
async _addFaustToChain(chain, target, dspCode, opts) {
|
|
4834
|
+
if (typeof dspCode !== "string" || dspCode.trim().length === 0) {
|
|
4835
|
+
throw new Error(PREFIX2 + "addFaustEffect: dspCode must be a non-empty string");
|
|
4836
|
+
}
|
|
4837
|
+
this._requireWiring();
|
|
4838
|
+
const faustModule = await this._loadFaustModule("addFaustEffect()");
|
|
4839
|
+
const compiled = await faustModule.compileFaustToWam(dspCode, { name: opts.name });
|
|
4840
|
+
const { audioContext } = this._requireWiring();
|
|
4841
|
+
const wamModule = await this._loadWamModule("addFaustEffect()");
|
|
4842
|
+
const { hostGroupId } = await wamModule.ensureWamHost(audioContext);
|
|
4843
|
+
const plugin = await wamModule.createWamInstanceFromFactory(
|
|
4844
|
+
compiled.factory,
|
|
4845
|
+
audioContext,
|
|
4846
|
+
hostGroupId,
|
|
4847
|
+
{ initialState: opts.initialState, label: compiled.name }
|
|
4848
|
+
);
|
|
4849
|
+
return this._insertWamPlugin(chain, target, wamModule, plugin, {
|
|
4850
|
+
source: { faust: compiled.dspCode }
|
|
4851
|
+
});
|
|
4852
|
+
}
|
|
4853
|
+
/**
|
|
4854
|
+
* Wire a live WAM plugin instance into a chain as a kind:'wam' entry —
|
|
4855
|
+
* shared by the url path (addWamPlugin) and the Faust path (addFaustEffect).
|
|
4856
|
+
* WAM entries participate in every chain operation with no special-casing:
|
|
4857
|
+
* remove destroys the plugin (via the entry's dispose), bypass uses
|
|
4858
|
+
* disconnection semantics (no wet param), and setParams maps onto the
|
|
4859
|
+
* plugin's setParameterValues.
|
|
4860
|
+
*/
|
|
4861
|
+
_insertWamPlugin(chain, target, wamModule, plugin, meta) {
|
|
4862
|
+
const node = plugin.audioNode;
|
|
4863
|
+
const label = plugin.descriptor.name;
|
|
4864
|
+
if (chain.disposed) {
|
|
4865
|
+
plugin.destroy();
|
|
4866
|
+
throw new Error(
|
|
4867
|
+
PREFIX2 + 'addWamPlugin: the effects chain was disposed while "' + (meta.url ?? label) + '" was loading; the plugin was discarded.'
|
|
4868
|
+
);
|
|
4869
|
+
}
|
|
4870
|
+
let effectId;
|
|
4871
|
+
try {
|
|
4872
|
+
effectId = chain.add({
|
|
4873
|
+
kind: "wam",
|
|
4874
|
+
type: "wam",
|
|
4875
|
+
...meta.url !== void 0 ? { url: meta.url } : {},
|
|
4876
|
+
...meta.source !== void 0 ? { source: meta.source } : {},
|
|
4877
|
+
label,
|
|
4878
|
+
instance: {
|
|
4879
|
+
input: node,
|
|
4880
|
+
output: node,
|
|
4881
|
+
applyParams: (params) => {
|
|
4882
|
+
node.setParameterValues?.(toWamParameterMap(params))?.catch((err) => {
|
|
4883
|
+
console.warn(PREFIX2 + "WAM setParameterValues failed: " + String(err));
|
|
4884
|
+
});
|
|
4885
|
+
},
|
|
4886
|
+
dispose: () => {
|
|
4887
|
+
this._wamNodes.delete(node);
|
|
4888
|
+
plugin.destroy();
|
|
4889
|
+
},
|
|
4890
|
+
getState: () => plugin.getState(),
|
|
4891
|
+
getParameterInfo: () => plugin.getParameterInfo(),
|
|
4892
|
+
...plugin.createGui ? { createGui: () => plugin.createGui() } : {},
|
|
4893
|
+
...plugin.destroyGui ? { destroyGui: (gui) => plugin.destroyGui(gui) } : {}
|
|
4894
|
+
},
|
|
4895
|
+
params: {}
|
|
4896
|
+
});
|
|
4897
|
+
} catch (err) {
|
|
4898
|
+
try {
|
|
4899
|
+
plugin.destroy();
|
|
4900
|
+
} catch (destroyErr) {
|
|
4901
|
+
console.warn(
|
|
4902
|
+
PREFIX2 + "addWamPlugin: cleanup after failed insertion also failed: " + String(destroyErr)
|
|
4903
|
+
);
|
|
4904
|
+
}
|
|
4905
|
+
throw err;
|
|
4906
|
+
}
|
|
4907
|
+
this._wamNodes.add(node);
|
|
4908
|
+
this._ensureTransportBridge(wamModule)?.notifyNodeAdded(node);
|
|
4909
|
+
const index = chain.entries.findIndex((e) => e.id === effectId);
|
|
4910
|
+
this._dispatch(target, "daw-effect-add", {
|
|
4911
|
+
effectId,
|
|
4912
|
+
kind: "wam",
|
|
4913
|
+
type: "wam",
|
|
4914
|
+
...meta.url !== void 0 ? { url: meta.url } : {},
|
|
4915
|
+
...meta.source !== void 0 ? { source: meta.source } : {},
|
|
4916
|
+
params: {},
|
|
4917
|
+
index
|
|
4918
|
+
});
|
|
4919
|
+
return effectId;
|
|
4920
|
+
}
|
|
4921
|
+
/**
|
|
4922
|
+
* Lazily create the wam-transport bridge so tempo-synced plugins lock to
|
|
4923
|
+
* the timeline. Skipped (not an error) when the adapter's transport lacks
|
|
4924
|
+
* the query/event surface — the bridge is an enhancement, not a
|
|
4925
|
+
* requirement for audio processing.
|
|
4926
|
+
*/
|
|
4927
|
+
_ensureTransportBridge(wamModule) {
|
|
4928
|
+
if (this._transportBridge) return this._transportBridge;
|
|
4929
|
+
if (typeof wamModule.createWamTransportBridge !== "function") return null;
|
|
4930
|
+
const transport = this._getAdapter()?.transport;
|
|
4931
|
+
if (!transport || typeof transport.on !== "function" || typeof transport.getTempo !== "function" || typeof transport.tickToBar !== "function" || typeof transport.timeToTick !== "function") {
|
|
4932
|
+
return null;
|
|
4933
|
+
}
|
|
4934
|
+
this._transportBridge = wamModule.createWamTransportBridge(transport, () => [
|
|
4935
|
+
...this._wamNodes
|
|
4936
|
+
]);
|
|
4937
|
+
return this._transportBridge;
|
|
4938
|
+
}
|
|
4939
|
+
/**
|
|
4940
|
+
* Replace a chain's contents with a persisted snapshot. Entries restore
|
|
4941
|
+
* sequentially so chain order survives async WAM loads. A WAM url that
|
|
4942
|
+
* fails to load becomes a bypassed passthrough placeholder at its saved
|
|
4943
|
+
* position — the restore continues, a daw-effect-error fires, and the
|
|
4944
|
+
* saved state is retained so a later snapshot/retry round-trips it.
|
|
4945
|
+
*/
|
|
4946
|
+
async _restoreChain(chain, target, entries) {
|
|
4947
|
+
const token = /* @__PURE__ */ Symbol("restore");
|
|
4948
|
+
this._restoreTokens.set(chain, token);
|
|
4949
|
+
const superseded = () => this._restoreTokens.get(chain) !== token;
|
|
4950
|
+
for (const existing of chain.entries) {
|
|
4951
|
+
this._runOp(chain, target, "remove", existing.id);
|
|
4952
|
+
}
|
|
4953
|
+
for (const entry of entries) {
|
|
4954
|
+
if (superseded()) return;
|
|
4955
|
+
if (entry.kind === "native") {
|
|
4956
|
+
const id = this._addToChain(chain, target, entry.type, entry.params);
|
|
4957
|
+
if (entry.bypassed) {
|
|
4958
|
+
this._runOp(chain, target, "setBypassed", id, true);
|
|
4959
|
+
}
|
|
4960
|
+
continue;
|
|
4961
|
+
}
|
|
4962
|
+
try {
|
|
4963
|
+
const id = entry.faustDsp !== void 0 ? await this._addFaustToChain(chain, target, entry.faustDsp, {
|
|
4964
|
+
name: entry.faustName,
|
|
4965
|
+
initialState: entry.state
|
|
4966
|
+
}) : await this._addWamToChain(chain, target, entry.url ?? "", entry.state);
|
|
4967
|
+
if (superseded()) {
|
|
4968
|
+
this._runOp(chain, target, "remove", id);
|
|
4969
|
+
return;
|
|
4970
|
+
}
|
|
4971
|
+
if (entry.bypassed) {
|
|
4972
|
+
this._runOp(chain, target, "setBypassed", id, true);
|
|
4973
|
+
}
|
|
4974
|
+
} catch (err) {
|
|
4975
|
+
if (superseded()) return;
|
|
4976
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4977
|
+
const sourceLabel = entry.url ?? entry.faustName ?? "Faust effect";
|
|
4978
|
+
console.warn(
|
|
4979
|
+
PREFIX2 + 'setEffectsState: plugin "' + sourceLabel + '" failed to restore: ' + message
|
|
4980
|
+
);
|
|
4981
|
+
const effectId = this._addWamPlaceholder(chain, entry, message);
|
|
4982
|
+
this._dispatch(target, "daw-effect-error", {
|
|
4983
|
+
effectId,
|
|
4984
|
+
...entry.url !== void 0 ? { url: entry.url } : {},
|
|
4985
|
+
...entry.faustDsp !== void 0 ? { source: { faust: entry.faustDsp } } : {},
|
|
4986
|
+
message
|
|
4987
|
+
});
|
|
4988
|
+
}
|
|
4989
|
+
}
|
|
4990
|
+
}
|
|
4991
|
+
/** A silent passthrough occupying the failed plugin's chain position. */
|
|
4992
|
+
_addWamPlaceholder(chain, entry, message) {
|
|
4993
|
+
const { audioContext } = this._requireWiring();
|
|
4994
|
+
const node = audioContext.createGain();
|
|
4995
|
+
const effectId = chain.add({
|
|
4996
|
+
kind: "wam",
|
|
4997
|
+
type: "wam",
|
|
4998
|
+
...entry.url !== void 0 ? { url: entry.url } : {},
|
|
4999
|
+
...entry.faustDsp !== void 0 ? { source: { faust: entry.faustDsp } } : {},
|
|
5000
|
+
label: entry.url ?? entry.faustName ?? "Faust effect",
|
|
5001
|
+
error: message,
|
|
5002
|
+
placeholder: { state: entry.state, bypassed: entry.bypassed },
|
|
5003
|
+
instance: { input: node, output: node, applyParams: () => {
|
|
5004
|
+
} },
|
|
5005
|
+
params: {}
|
|
5006
|
+
});
|
|
5007
|
+
chain.setBypassed(effectId, true);
|
|
5008
|
+
return effectId;
|
|
5009
|
+
}
|
|
5010
|
+
_runOp(chain, target, op, effectId, arg) {
|
|
5011
|
+
const entries = chain?.entries ?? [];
|
|
5012
|
+
const fromIndex = entries.findIndex((e) => e.id === effectId);
|
|
5013
|
+
if (!chain || fromIndex === -1) {
|
|
5014
|
+
console.warn(PREFIX2 + "effects." + op + ': unknown effectId "' + effectId + '"');
|
|
5015
|
+
return;
|
|
5016
|
+
}
|
|
5017
|
+
if (entries[fromIndex].error !== void 0 && (op === "setParams" || op === "setBypassed")) {
|
|
5018
|
+
console.warn(
|
|
5019
|
+
PREFIX2 + "effects." + op + ': effect "' + effectId + '" is a failed-plugin placeholder (' + entries[fromIndex].error + ") \u2014 edit ignored. Remove it or retry the restore."
|
|
5020
|
+
);
|
|
5021
|
+
return;
|
|
5022
|
+
}
|
|
5023
|
+
switch (op) {
|
|
5024
|
+
case "remove":
|
|
5025
|
+
this._destroyGui(effectId);
|
|
5026
|
+
chain.remove(effectId);
|
|
5027
|
+
this._dispatch(target, "daw-effect-remove", { effectId });
|
|
5028
|
+
break;
|
|
5029
|
+
case "setParams":
|
|
5030
|
+
chain.setParams(effectId, arg);
|
|
5031
|
+
this._dispatch(target, "daw-effect-change", { effectId, params: { ...arg } });
|
|
5032
|
+
break;
|
|
5033
|
+
case "setBypassed":
|
|
5034
|
+
chain.setBypassed(effectId, arg);
|
|
5035
|
+
this._dispatch(target, "daw-effect-bypass", { effectId, bypassed: arg });
|
|
5036
|
+
break;
|
|
5037
|
+
case "move":
|
|
5038
|
+
chain.move(effectId, arg);
|
|
5039
|
+
this._dispatch(target, "daw-effect-reorder", { effectId, fromIndex, toIndex: arg });
|
|
5040
|
+
break;
|
|
5041
|
+
}
|
|
5042
|
+
}
|
|
5043
|
+
_dispatch(target, name, detail) {
|
|
5044
|
+
target.dispatchEvent(new CustomEvent(name, { bubbles: true, composed: true, detail }));
|
|
5045
|
+
}
|
|
5046
|
+
};
|
|
5047
|
+
function validateSerializedEntries(entries) {
|
|
5048
|
+
if (!Array.isArray(entries)) {
|
|
5049
|
+
throw new Error(PREFIX2 + "setEffectsState: expected an array of serialized effect entries");
|
|
5050
|
+
}
|
|
5051
|
+
entries.forEach((entry, i) => {
|
|
5052
|
+
const at = " (entry " + i + ")";
|
|
5053
|
+
if (entry === null || typeof entry !== "object") {
|
|
5054
|
+
throw new Error(PREFIX2 + "setEffectsState: entry must be an object" + at);
|
|
5055
|
+
}
|
|
5056
|
+
const e = entry;
|
|
5057
|
+
if (e.kind === "native") {
|
|
5058
|
+
if (typeof e.type !== "string" || e.type.length === 0) {
|
|
5059
|
+
throw new Error(PREFIX2 + "setEffectsState: native entry requires a type string" + at);
|
|
5060
|
+
}
|
|
5061
|
+
if (e.params === null || typeof e.params !== "object") {
|
|
5062
|
+
throw new Error(PREFIX2 + "setEffectsState: native entry requires a params object" + at);
|
|
5063
|
+
}
|
|
5064
|
+
} else if (e.kind === "wam") {
|
|
5065
|
+
const hasUrl = typeof e.url === "string" && e.url.length > 0;
|
|
5066
|
+
const hasFaustDsp = typeof e.faustDsp === "string" && e.faustDsp.trim().length > 0;
|
|
5067
|
+
if (!hasUrl && !hasFaustDsp) {
|
|
5068
|
+
throw new Error(
|
|
5069
|
+
PREFIX2 + "setEffectsState: wam entry requires a url string or a faustDsp source string" + at
|
|
5070
|
+
);
|
|
5071
|
+
}
|
|
5072
|
+
if (e.faustName !== void 0 && typeof e.faustName !== "string") {
|
|
5073
|
+
throw new Error(PREFIX2 + "setEffectsState: faustName must be a string when provided" + at);
|
|
5074
|
+
}
|
|
5075
|
+
} else {
|
|
5076
|
+
throw new Error(PREFIX2 + 'setEffectsState: unknown entry kind "' + String(e.kind) + '"' + at);
|
|
5077
|
+
}
|
|
5078
|
+
if (typeof e.bypassed !== "boolean") {
|
|
5079
|
+
throw new Error(PREFIX2 + "setEffectsState: entry requires a boolean bypassed flag" + at);
|
|
5080
|
+
}
|
|
5081
|
+
});
|
|
5082
|
+
}
|
|
5083
|
+
function toWamParameterMap(params) {
|
|
5084
|
+
const map = {};
|
|
5085
|
+
for (const [id, value] of Object.entries(params)) {
|
|
5086
|
+
map[id] = { id, value, normalized: false };
|
|
5087
|
+
}
|
|
5088
|
+
return map;
|
|
5089
|
+
}
|
|
5090
|
+
|
|
5091
|
+
// src/interactions/export-audio.ts
|
|
5092
|
+
var PREFIX3 = "[waveform-playlist] ";
|
|
5093
|
+
async function exportAudioImpl(host, options = {}) {
|
|
5094
|
+
const sampleRate = options.sampleRate ?? host.effectiveSampleRate;
|
|
5095
|
+
const startTime = options.startTime ?? 0;
|
|
5096
|
+
const duration = options.duration ?? Math.max(0, host.duration - startTime);
|
|
5097
|
+
const channels = options.channels ?? 2;
|
|
5098
|
+
if (!Number.isFinite(sampleRate) || sampleRate <= 0) {
|
|
5099
|
+
throw new Error(PREFIX3 + "exportAudio: invalid sampleRate " + String(sampleRate));
|
|
5100
|
+
}
|
|
5101
|
+
if (!Number.isFinite(duration) || duration <= 0) {
|
|
5102
|
+
throw new Error(PREFIX3 + "exportAudio: nothing to render (duration " + String(duration) + ")");
|
|
5103
|
+
}
|
|
5104
|
+
const ctx = new OfflineAudioContext({
|
|
5105
|
+
numberOfChannels: channels,
|
|
5106
|
+
length: Math.round(duration * sampleRate),
|
|
5107
|
+
sampleRate
|
|
5108
|
+
});
|
|
5109
|
+
const cleanups = [];
|
|
5110
|
+
try {
|
|
5111
|
+
const masterChain = await buildOfflineChain(ctx, await host.getMasterEffectsState());
|
|
5112
|
+
cleanups.push(masterChain.dispose);
|
|
5113
|
+
masterChain.output.connect(ctx.destination);
|
|
5114
|
+
const anySoloed = host.tracks.some((t) => t.soloed);
|
|
5115
|
+
for (const track of host.tracks) {
|
|
5116
|
+
if (track.muted || anySoloed && !track.soloed) {
|
|
5117
|
+
continue;
|
|
5118
|
+
}
|
|
5119
|
+
const chain = await buildOfflineChain(ctx, await host.getTrackEffectsState(track.id));
|
|
5120
|
+
cleanups.push(chain.dispose);
|
|
5121
|
+
const volume = ctx.createGain();
|
|
5122
|
+
volume.gain.value = track.volume;
|
|
5123
|
+
const panner = ctx.createStereoPanner();
|
|
5124
|
+
panner.pan.value = track.pan;
|
|
5125
|
+
volume.connect(panner);
|
|
5126
|
+
panner.connect(chain.input);
|
|
5127
|
+
chain.output.connect(masterChain.input);
|
|
5128
|
+
for (const clip of track.clips) {
|
|
5129
|
+
scheduleClip(ctx, clip, startTime, duration, volume);
|
|
5130
|
+
}
|
|
5131
|
+
}
|
|
5132
|
+
return await ctx.startRendering();
|
|
5133
|
+
} finally {
|
|
5134
|
+
for (const dispose of cleanups) {
|
|
5135
|
+
try {
|
|
5136
|
+
dispose();
|
|
5137
|
+
} catch (err) {
|
|
5138
|
+
console.warn(PREFIX3 + "exportAudio: cleanup error: " + String(err));
|
|
5139
|
+
}
|
|
5140
|
+
}
|
|
5141
|
+
}
|
|
5142
|
+
}
|
|
5143
|
+
function scheduleClip(ctx, clip, windowStart, windowDuration, destination) {
|
|
5144
|
+
if (!clip.audioBuffer || clip.durationSamples <= 0) {
|
|
5145
|
+
return;
|
|
5146
|
+
}
|
|
5147
|
+
const clipRate = clip.sampleRate;
|
|
5148
|
+
const clipStart = clip.startSample / clipRate;
|
|
5149
|
+
const clipDuration = clip.durationSamples / clipRate;
|
|
5150
|
+
let offset = clip.offsetSamples / clipRate;
|
|
5151
|
+
let when = clipStart - windowStart;
|
|
5152
|
+
let remaining = clipDuration;
|
|
5153
|
+
if (when < 0) {
|
|
5154
|
+
offset += -when;
|
|
5155
|
+
remaining += when;
|
|
5156
|
+
when = 0;
|
|
5157
|
+
}
|
|
5158
|
+
remaining = Math.min(remaining, windowDuration - when);
|
|
5159
|
+
if (remaining <= 0) {
|
|
5160
|
+
return;
|
|
5161
|
+
}
|
|
5162
|
+
const source = ctx.createBufferSource();
|
|
5163
|
+
source.buffer = clip.audioBuffer;
|
|
5164
|
+
let out = source;
|
|
5165
|
+
if (clip.gain !== void 0 && clip.gain !== 1) {
|
|
5166
|
+
const gainNode = ctx.createGain();
|
|
5167
|
+
gainNode.gain.value = clip.gain;
|
|
5168
|
+
out.connect(gainNode);
|
|
5169
|
+
out = gainNode;
|
|
5170
|
+
}
|
|
5171
|
+
out.connect(destination);
|
|
5172
|
+
source.start(when, offset, remaining);
|
|
5173
|
+
}
|
|
5174
|
+
async function buildOfflineChain(ctx, entries) {
|
|
5175
|
+
const input = ctx.createGain();
|
|
5176
|
+
const output = ctx.createGain();
|
|
5177
|
+
const cleanups = [];
|
|
5178
|
+
const dispose = () => {
|
|
5179
|
+
for (const cleanup of cleanups) {
|
|
5180
|
+
try {
|
|
5181
|
+
cleanup();
|
|
5182
|
+
} catch (err) {
|
|
5183
|
+
console.warn(PREFIX3 + "exportAudio: chain cleanup error: " + String(err));
|
|
5184
|
+
}
|
|
5185
|
+
}
|
|
5186
|
+
};
|
|
5187
|
+
let previous = input;
|
|
5188
|
+
try {
|
|
5189
|
+
await wireEntries();
|
|
5190
|
+
} catch (err) {
|
|
5191
|
+
dispose();
|
|
5192
|
+
throw err;
|
|
5193
|
+
}
|
|
5194
|
+
previous.connect(output);
|
|
5195
|
+
return { input, output, dispose };
|
|
5196
|
+
async function wireEntries() {
|
|
5197
|
+
for (const entry of entries) {
|
|
5198
|
+
if (entry.kind === "native") {
|
|
5199
|
+
const created = createEffectInstance(entry.type, ctx, entry.params);
|
|
5200
|
+
if (entry.bypassed) {
|
|
5201
|
+
if (!created.wetParam) {
|
|
5202
|
+
created.instance.dispose?.();
|
|
5203
|
+
continue;
|
|
5204
|
+
}
|
|
5205
|
+
created.instance.applyParams({ [created.wetParam]: 0 });
|
|
5206
|
+
}
|
|
5207
|
+
if (created.instance.dispose) {
|
|
5208
|
+
cleanups.push(created.instance.dispose);
|
|
5209
|
+
}
|
|
5210
|
+
previous.connect(created.instance.input);
|
|
5211
|
+
previous = created.instance.output;
|
|
5212
|
+
continue;
|
|
5213
|
+
}
|
|
5214
|
+
if (entry.bypassed) {
|
|
5215
|
+
continue;
|
|
5216
|
+
}
|
|
5217
|
+
const wamModule = await loadWamModule("exportAudio() with WAM effects");
|
|
5218
|
+
const { hostGroupId } = await wamModule.ensureWamHost(ctx);
|
|
5219
|
+
let plugin;
|
|
5220
|
+
if (entry.faustDsp !== void 0) {
|
|
5221
|
+
const faustModule = await loadFaustModule("exportAudio() with Faust effects");
|
|
5222
|
+
const compiled = await faustModule.compileFaustToWam(entry.faustDsp, {
|
|
5223
|
+
name: entry.faustName
|
|
5224
|
+
});
|
|
5225
|
+
plugin = await wamModule.createWamInstanceFromFactory(
|
|
5226
|
+
compiled.factory,
|
|
5227
|
+
ctx,
|
|
5228
|
+
hostGroupId,
|
|
5229
|
+
{ initialState: entry.state, label: compiled.name }
|
|
5230
|
+
);
|
|
5231
|
+
} else {
|
|
5232
|
+
plugin = await wamModule.createWamInstance(entry.url ?? "", ctx, hostGroupId, {
|
|
5233
|
+
initialState: entry.state
|
|
5234
|
+
});
|
|
5235
|
+
}
|
|
5236
|
+
cleanups.push(() => plugin.destroy());
|
|
5237
|
+
previous.connect(plugin.audioNode);
|
|
5238
|
+
previous = plugin.audioNode;
|
|
5239
|
+
}
|
|
5240
|
+
}
|
|
5241
|
+
}
|
|
5242
|
+
|
|
4099
5243
|
// src/interactions/peaks-loader.ts
|
|
4100
5244
|
import WaveformData2 from "waveform-data";
|
|
4101
5245
|
async function loadWaveformDataFromUrl(src) {
|
|
@@ -4286,6 +5430,8 @@ var DawEditorElement = class extends LitElement11 {
|
|
|
4286
5430
|
this._selectionEndTime = 0;
|
|
4287
5431
|
this._currentTime = 0;
|
|
4288
5432
|
this._externalAdapter = null;
|
|
5433
|
+
// --- Effects (master chain; per-track chains delegated from <daw-track>) ---
|
|
5434
|
+
this._effectsManager = null;
|
|
4289
5435
|
this._engine = null;
|
|
4290
5436
|
this._warnedMissingTicksToSeconds = false;
|
|
4291
5437
|
this._warnedMissingSecondsToTicks = false;
|
|
@@ -4643,11 +5789,122 @@ var DawEditorElement = class extends LitElement11 {
|
|
|
4643
5789
|
"[dawcore] adapter set after engine is built. The engine will continue using the previous adapter."
|
|
4644
5790
|
);
|
|
4645
5791
|
}
|
|
5792
|
+
if (this._effectsManager) {
|
|
5793
|
+
console.warn(
|
|
5794
|
+
"[dawcore] adapter replaced \u2014 existing effect chains were disposed. Re-add effects on the new adapter."
|
|
5795
|
+
);
|
|
5796
|
+
this._effectsManager.disposeAll();
|
|
5797
|
+
this._effectsManager = null;
|
|
5798
|
+
}
|
|
4646
5799
|
this._externalAdapter = value;
|
|
4647
5800
|
}
|
|
4648
5801
|
get adapter() {
|
|
4649
5802
|
return this._externalAdapter;
|
|
4650
5803
|
}
|
|
5804
|
+
get _effects() {
|
|
5805
|
+
if (!this._effectsManager) {
|
|
5806
|
+
this._effectsManager = new EffectsManager(() => this._externalAdapter, this);
|
|
5807
|
+
}
|
|
5808
|
+
return this._effectsManager;
|
|
5809
|
+
}
|
|
5810
|
+
/** Master effects chain — inserted between the master bus and the destination. */
|
|
5811
|
+
addEffect(type, params) {
|
|
5812
|
+
return this._effects.addMasterEffect(type, params);
|
|
5813
|
+
}
|
|
5814
|
+
removeEffect(effectId) {
|
|
5815
|
+
this._effects.masterOp("remove", effectId);
|
|
5816
|
+
}
|
|
5817
|
+
setEffectParams(effectId, params) {
|
|
5818
|
+
this._effects.masterOp("setParams", effectId, params);
|
|
5819
|
+
}
|
|
5820
|
+
setEffectBypassed(effectId, bypassed) {
|
|
5821
|
+
this._effects.masterOp("setBypassed", effectId, bypassed);
|
|
5822
|
+
}
|
|
5823
|
+
moveEffect(effectId, newIndex) {
|
|
5824
|
+
this._effects.masterOp("move", effectId, newIndex);
|
|
5825
|
+
}
|
|
5826
|
+
get effects() {
|
|
5827
|
+
return this._effectsManager?.masterEffects() ?? [];
|
|
5828
|
+
}
|
|
5829
|
+
/** Load a WAM plugin (via the optional @dawcore/wam peer) into the master chain. */
|
|
5830
|
+
addWamPlugin(url, initialState) {
|
|
5831
|
+
return this._effects.addMasterWamPlugin(url, initialState);
|
|
5832
|
+
}
|
|
5833
|
+
/**
|
|
5834
|
+
* Compile Faust DSP source in the browser (via the optional @dawcore/faust
|
|
5835
|
+
* peer) and add the resulting WAM to the master chain. Compile errors keep
|
|
5836
|
+
* their Faust line/column diagnostics and leave the chain untouched.
|
|
5837
|
+
*/
|
|
5838
|
+
addFaustEffect(dspCode, options) {
|
|
5839
|
+
return this._effects.addMasterFaustEffect(dspCode, options);
|
|
5840
|
+
}
|
|
5841
|
+
/**
|
|
5842
|
+
* Open (lazily creating) the GUI for a master-chain effect into a
|
|
5843
|
+
* consumer-provided container. WAM plugins mount their own GUI; plugins
|
|
5844
|
+
* without one — and native effects — get the generic parameter panel from
|
|
5845
|
+
* @dawcore/wam. The element is cached: closeEffectGui hides it without
|
|
5846
|
+
* interrupting audio, reopening remounts the same element.
|
|
5847
|
+
*/
|
|
5848
|
+
openEffectGui(effectId, container) {
|
|
5849
|
+
return this._effects.openMasterEffectGui(effectId, container);
|
|
5850
|
+
}
|
|
5851
|
+
/** Hide a master-chain effect's GUI (cached for reopen — never destroys). */
|
|
5852
|
+
closeEffectGui(effectId) {
|
|
5853
|
+
this._effects.closeEffectGui(effectId);
|
|
5854
|
+
}
|
|
5855
|
+
/** Snapshot the master chain in its persisted form (see dawcore README). */
|
|
5856
|
+
getEffectsState() {
|
|
5857
|
+
return this._effectsManager?.getMasterEffectsState() ?? Promise.resolve([]);
|
|
5858
|
+
}
|
|
5859
|
+
/** Replace the master chain with a persisted snapshot. */
|
|
5860
|
+
setEffectsState(entries) {
|
|
5861
|
+
return this._effects.setMasterEffectsState(entries);
|
|
5862
|
+
}
|
|
5863
|
+
/**
|
|
5864
|
+
* Render the session offline through all effect chains (per-track +
|
|
5865
|
+
* master), including WAM plugins (re-instantiated on the offline context
|
|
5866
|
+
* with their live state). Returns the rendered AudioBuffer.
|
|
5867
|
+
*/
|
|
5868
|
+
exportAudio(options) {
|
|
5869
|
+
return exportAudioImpl(
|
|
5870
|
+
{
|
|
5871
|
+
effectiveSampleRate: this.effectiveSampleRate,
|
|
5872
|
+
duration: this._duration,
|
|
5873
|
+
tracks: [...this._engineTracks.values()],
|
|
5874
|
+
getMasterEffectsState: () => this.getEffectsState(),
|
|
5875
|
+
getTrackEffectsState: (trackId) => this._trackGetEffectsState(trackId)
|
|
5876
|
+
},
|
|
5877
|
+
options
|
|
5878
|
+
);
|
|
5879
|
+
}
|
|
5880
|
+
/** Internal — <daw-track> effects API delegates here (dawcore-internal contract). */
|
|
5881
|
+
_trackAddEffect(trackId, target, type, params) {
|
|
5882
|
+
return this._effects.addTrackEffect(trackId, target, type, params);
|
|
5883
|
+
}
|
|
5884
|
+
_trackEffectOp(trackId, target, op, effectId, arg) {
|
|
5885
|
+
this._effects.trackOp(trackId, target, op, effectId, arg);
|
|
5886
|
+
}
|
|
5887
|
+
_trackEffects(trackId) {
|
|
5888
|
+
return this._effectsManager?.trackEffects(trackId) ?? [];
|
|
5889
|
+
}
|
|
5890
|
+
_trackAddWamPlugin(trackId, target, url, initialState) {
|
|
5891
|
+
return this._effects.addTrackWamPlugin(trackId, target, url, initialState);
|
|
5892
|
+
}
|
|
5893
|
+
_trackAddFaustEffect(trackId, target, dspCode, options) {
|
|
5894
|
+
return this._effects.addTrackFaustEffect(trackId, target, dspCode, options);
|
|
5895
|
+
}
|
|
5896
|
+
_trackOpenEffectGui(trackId, target, effectId, container) {
|
|
5897
|
+
return this._effects.openTrackEffectGui(trackId, target, effectId, container);
|
|
5898
|
+
}
|
|
5899
|
+
_trackCloseEffectGui(_trackId, effectId) {
|
|
5900
|
+
this._effects.closeEffectGui(effectId);
|
|
5901
|
+
}
|
|
5902
|
+
_trackGetEffectsState(trackId) {
|
|
5903
|
+
return this._effectsManager?.getTrackEffectsState(trackId) ?? Promise.resolve([]);
|
|
5904
|
+
}
|
|
5905
|
+
_trackSetEffectsState(trackId, target, entries) {
|
|
5906
|
+
return this._effects.setTrackEffectsState(trackId, target, entries);
|
|
5907
|
+
}
|
|
4651
5908
|
get audioContext() {
|
|
4652
5909
|
if (!this._externalAdapter) {
|
|
4653
5910
|
throw new Error(NO_ADAPTER_ERROR);
|
|
@@ -4819,6 +6076,8 @@ var DawEditorElement = class extends LitElement11 {
|
|
|
4819
6076
|
}
|
|
4820
6077
|
disconnectedCallback() {
|
|
4821
6078
|
super.disconnectedCallback();
|
|
6079
|
+
this._effectsManager?.disposeAll();
|
|
6080
|
+
this._effectsManager = null;
|
|
4822
6081
|
this.removeEventListener("daw-track-connected", this._onTrackConnected);
|
|
4823
6082
|
this.removeEventListener("daw-track-update", this._onTrackUpdate);
|
|
4824
6083
|
this.removeEventListener("daw-track-control", this._onTrackControl);
|
|
@@ -4894,6 +6153,7 @@ var DawEditorElement = class extends LitElement11 {
|
|
|
4894
6153
|
}
|
|
4895
6154
|
_onTrackRemoved(trackId) {
|
|
4896
6155
|
this._trackElements.delete(trackId);
|
|
6156
|
+
this._effectsManager?.disposeTrackChain(trackId);
|
|
4897
6157
|
const removedTrack = this._engineTracks.get(trackId);
|
|
4898
6158
|
if (removedTrack) {
|
|
4899
6159
|
const nextPeaks = new Map(this._peaksData);
|
|
@@ -5544,6 +6804,7 @@ var DawEditorElement = class extends LitElement11 {
|
|
|
5544
6804
|
nextTracks.set(track.id, track);
|
|
5545
6805
|
}
|
|
5546
6806
|
this._engineTracks = nextTracks;
|
|
6807
|
+
this._effectsManager?.rewireTrackChains();
|
|
5547
6808
|
const audioTracks = engineState.tracks.filter((t) => {
|
|
5548
6809
|
const desc = this._tracks.get(t.id);
|
|
5549
6810
|
return desc?.renderMode !== "piano-roll";
|
|
@@ -7019,7 +8280,10 @@ export {
|
|
|
7019
8280
|
DawWaveformElement,
|
|
7020
8281
|
RecordingController,
|
|
7021
8282
|
SpectrogramController,
|
|
8283
|
+
createEffectInstance,
|
|
8284
|
+
getEffectDefinitions,
|
|
7022
8285
|
isDomClip,
|
|
8286
|
+
registerEffect,
|
|
7023
8287
|
splitAtPlayhead
|
|
7024
8288
|
};
|
|
7025
8289
|
//# sourceMappingURL=index.mjs.map
|