@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.js
CHANGED
|
@@ -60,7 +60,10 @@ __export(index_exports, {
|
|
|
60
60
|
DawWaveformElement: () => DawWaveformElement,
|
|
61
61
|
RecordingController: () => RecordingController,
|
|
62
62
|
SpectrogramController: () => SpectrogramController,
|
|
63
|
+
createEffectInstance: () => createEffectInstance,
|
|
64
|
+
getEffectDefinitions: () => getEffectDefinitions,
|
|
63
65
|
isDomClip: () => isDomClip,
|
|
66
|
+
registerEffect: () => registerEffect,
|
|
64
67
|
splitAtPlayhead: () => splitAtPlayhead
|
|
65
68
|
});
|
|
66
69
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -257,6 +260,68 @@ var DawTrackElement = class extends import_lit2.LitElement {
|
|
|
257
260
|
createRenderRoot() {
|
|
258
261
|
return this;
|
|
259
262
|
}
|
|
263
|
+
// --- Effects API (delegates to the owning <daw-editor>) ---
|
|
264
|
+
addEffect(type, params) {
|
|
265
|
+
return this._effectsEditor()._trackAddEffect(this.trackId, this, type, params);
|
|
266
|
+
}
|
|
267
|
+
/** Load a WAM plugin (via the optional @dawcore/wam peer) into this track's chain. */
|
|
268
|
+
addWamPlugin(url, initialState) {
|
|
269
|
+
return this._effectsEditor()._trackAddWamPlugin(this.trackId, this, url, initialState);
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Compile Faust DSP source in the browser (via the optional @dawcore/faust
|
|
273
|
+
* peer) and add the resulting WAM to this track's chain. Compile errors
|
|
274
|
+
* keep their Faust line/column diagnostics and leave the chain untouched.
|
|
275
|
+
*/
|
|
276
|
+
addFaustEffect(dspCode, options) {
|
|
277
|
+
return this._effectsEditor()._trackAddFaustEffect(this.trackId, this, dspCode, options);
|
|
278
|
+
}
|
|
279
|
+
/** Snapshot this track's chain in its persisted form (see dawcore README). */
|
|
280
|
+
getEffectsState() {
|
|
281
|
+
const editor = this.closest("daw-editor");
|
|
282
|
+
return editor?._trackGetEffectsState(this.trackId) ?? Promise.resolve([]);
|
|
283
|
+
}
|
|
284
|
+
/** Replace this track's chain with a persisted snapshot. */
|
|
285
|
+
setEffectsState(entries) {
|
|
286
|
+
return this._effectsEditor()._trackSetEffectsState(this.trackId, this, entries);
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Open (lazily creating) the GUI for one of this track's effects into a
|
|
290
|
+
* consumer-provided container. Closing hides without interrupting audio;
|
|
291
|
+
* the element is cached for reopen. See <daw-editor>.openEffectGui.
|
|
292
|
+
*/
|
|
293
|
+
openEffectGui(effectId, container) {
|
|
294
|
+
return this._effectsEditor()._trackOpenEffectGui(this.trackId, this, effectId, container);
|
|
295
|
+
}
|
|
296
|
+
/** Hide an effect's GUI (cached for reopen — never destroys). */
|
|
297
|
+
closeEffectGui(effectId) {
|
|
298
|
+
this._effectsEditor()._trackCloseEffectGui(this.trackId, effectId);
|
|
299
|
+
}
|
|
300
|
+
removeEffect(effectId) {
|
|
301
|
+
this._effectsEditor()._trackEffectOp(this.trackId, this, "remove", effectId);
|
|
302
|
+
}
|
|
303
|
+
setEffectParams(effectId, params) {
|
|
304
|
+
this._effectsEditor()._trackEffectOp(this.trackId, this, "setParams", effectId, params);
|
|
305
|
+
}
|
|
306
|
+
setEffectBypassed(effectId, bypassed) {
|
|
307
|
+
this._effectsEditor()._trackEffectOp(this.trackId, this, "setBypassed", effectId, bypassed);
|
|
308
|
+
}
|
|
309
|
+
moveEffect(effectId, newIndex) {
|
|
310
|
+
this._effectsEditor()._trackEffectOp(this.trackId, this, "move", effectId, newIndex);
|
|
311
|
+
}
|
|
312
|
+
get effects() {
|
|
313
|
+
const editor = this.closest("daw-editor");
|
|
314
|
+
return editor?._trackEffects(this.trackId) ?? [];
|
|
315
|
+
}
|
|
316
|
+
_effectsEditor() {
|
|
317
|
+
const editor = this.closest("daw-editor");
|
|
318
|
+
if (!editor) {
|
|
319
|
+
throw new Error(
|
|
320
|
+
"[waveform-playlist] <daw-track> effects API requires the track to be inside a <daw-editor>"
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
return editor;
|
|
324
|
+
}
|
|
260
325
|
connectedCallback() {
|
|
261
326
|
super.connectedCallback();
|
|
262
327
|
setTimeout(() => {
|
|
@@ -1437,7 +1502,7 @@ function createPeaksWorker() {
|
|
|
1437
1502
|
}
|
|
1438
1503
|
const pending = /* @__PURE__ */ new Map();
|
|
1439
1504
|
let terminated = false;
|
|
1440
|
-
let
|
|
1505
|
+
let idCounter2 = 0;
|
|
1441
1506
|
worker.onmessage = (e) => {
|
|
1442
1507
|
const msg = e.data;
|
|
1443
1508
|
const entry = pending.get(msg.id);
|
|
@@ -1470,7 +1535,7 @@ function createPeaksWorker() {
|
|
|
1470
1535
|
return {
|
|
1471
1536
|
generate(params) {
|
|
1472
1537
|
if (terminated) return Promise.reject(new Error("Worker terminated"));
|
|
1473
|
-
const messageId = String(++
|
|
1538
|
+
const messageId = String(++idCounter2);
|
|
1474
1539
|
return new Promise((resolve, reject) => {
|
|
1475
1540
|
pending.set(messageId, { resolve, reject });
|
|
1476
1541
|
worker.postMessage(
|
|
@@ -4142,6 +4207,1088 @@ function findAudioBufferForClip(host, clip, track) {
|
|
|
4142
4207
|
return null;
|
|
4143
4208
|
}
|
|
4144
4209
|
|
|
4210
|
+
// src/effects/effects-chain-controller.ts
|
|
4211
|
+
var idCounter = 0;
|
|
4212
|
+
var EffectsChainController = class {
|
|
4213
|
+
constructor(audioContext) {
|
|
4214
|
+
this._entries = [];
|
|
4215
|
+
this._disposed = false;
|
|
4216
|
+
this._input = audioContext.createGain();
|
|
4217
|
+
this._output = audioContext.createGain();
|
|
4218
|
+
this._input.connect(this._output);
|
|
4219
|
+
}
|
|
4220
|
+
/** Connect the upstream source (track mute node, master gain) into this. */
|
|
4221
|
+
get input() {
|
|
4222
|
+
return this._input;
|
|
4223
|
+
}
|
|
4224
|
+
/** Route this onward (to the master bus / destination). */
|
|
4225
|
+
get output() {
|
|
4226
|
+
return this._output;
|
|
4227
|
+
}
|
|
4228
|
+
get entries() {
|
|
4229
|
+
return this._entries.map((entry) => ({
|
|
4230
|
+
id: entry.id,
|
|
4231
|
+
kind: entry.kind,
|
|
4232
|
+
type: entry.type,
|
|
4233
|
+
params: { ...entry.params },
|
|
4234
|
+
bypassed: entry.bypassed,
|
|
4235
|
+
...entry.url !== void 0 ? { url: entry.url } : {},
|
|
4236
|
+
...entry.source !== void 0 ? { source: { ...entry.source } } : {},
|
|
4237
|
+
...entry.label !== void 0 ? { label: entry.label } : {},
|
|
4238
|
+
...entry.error !== void 0 ? { error: entry.error } : {}
|
|
4239
|
+
}));
|
|
4240
|
+
}
|
|
4241
|
+
/** Snapshot the chain in its persisted form. WAM entries are asked for
|
|
4242
|
+
* their live state; placeholders re-emit the state they were saved with. */
|
|
4243
|
+
serialize() {
|
|
4244
|
+
return Promise.all(
|
|
4245
|
+
this._entries.map(async (entry) => {
|
|
4246
|
+
if (entry.kind === "wam") {
|
|
4247
|
+
const sourceFields = entry.source?.faust !== void 0 ? {
|
|
4248
|
+
faustDsp: entry.source.faust,
|
|
4249
|
+
...entry.label !== void 0 ? { faustName: entry.label } : {}
|
|
4250
|
+
} : { url: entry.url ?? "" };
|
|
4251
|
+
if (entry.placeholder) {
|
|
4252
|
+
return {
|
|
4253
|
+
kind: "wam",
|
|
4254
|
+
...sourceFields,
|
|
4255
|
+
bypassed: entry.placeholder.bypassed,
|
|
4256
|
+
...entry.placeholder.state !== void 0 ? { state: entry.placeholder.state } : {}
|
|
4257
|
+
};
|
|
4258
|
+
}
|
|
4259
|
+
let state5;
|
|
4260
|
+
try {
|
|
4261
|
+
state5 = await entry.instance.getState?.();
|
|
4262
|
+
} catch (err) {
|
|
4263
|
+
console.warn(
|
|
4264
|
+
'[waveform-playlist] serialize: plugin "' + (entry.url ?? entry.label ?? entry.type) + '" getState failed: ' + String(err)
|
|
4265
|
+
);
|
|
4266
|
+
}
|
|
4267
|
+
return {
|
|
4268
|
+
kind: "wam",
|
|
4269
|
+
...sourceFields,
|
|
4270
|
+
bypassed: entry.bypassed,
|
|
4271
|
+
...state5 !== void 0 ? { state: state5 } : {}
|
|
4272
|
+
};
|
|
4273
|
+
}
|
|
4274
|
+
return {
|
|
4275
|
+
kind: "native",
|
|
4276
|
+
type: entry.type,
|
|
4277
|
+
params: { ...entry.params },
|
|
4278
|
+
bypassed: entry.bypassed
|
|
4279
|
+
};
|
|
4280
|
+
})
|
|
4281
|
+
);
|
|
4282
|
+
}
|
|
4283
|
+
get disposed() {
|
|
4284
|
+
return this._disposed;
|
|
4285
|
+
}
|
|
4286
|
+
/** Internal (manager-facing): the live entry — including its instance —
|
|
4287
|
+
* for GUI wiring. Returns undefined (no warning) when the id is unknown. */
|
|
4288
|
+
getEntry(effectId) {
|
|
4289
|
+
return this._entries.find((e) => e.id === effectId);
|
|
4290
|
+
}
|
|
4291
|
+
add(item, index) {
|
|
4292
|
+
if (this._disposed) {
|
|
4293
|
+
throw new Error(
|
|
4294
|
+
"[waveform-playlist] EffectsChainController.add: chain is disposed \u2014 entries cannot be added"
|
|
4295
|
+
);
|
|
4296
|
+
}
|
|
4297
|
+
const id = "effect-" + ++idCounter;
|
|
4298
|
+
const entry = { ...item, params: { ...item.params }, id, bypassed: false };
|
|
4299
|
+
const at = index === void 0 ? this._entries.length : Math.max(0, Math.min(index, this._entries.length));
|
|
4300
|
+
this._entries = [...this._entries.slice(0, at), entry, ...this._entries.slice(at)];
|
|
4301
|
+
this._rebuild();
|
|
4302
|
+
return id;
|
|
4303
|
+
}
|
|
4304
|
+
remove(effectId) {
|
|
4305
|
+
const entry = this._find("remove", effectId);
|
|
4306
|
+
if (!entry) return;
|
|
4307
|
+
this._entries = this._entries.filter((e) => e.id !== effectId);
|
|
4308
|
+
entry.instance.output.disconnect();
|
|
4309
|
+
entry.instance.dispose?.();
|
|
4310
|
+
this._rebuild();
|
|
4311
|
+
}
|
|
4312
|
+
move(effectId, newIndex) {
|
|
4313
|
+
const entry = this._find("move", effectId);
|
|
4314
|
+
if (!entry) return;
|
|
4315
|
+
const without = this._entries.filter((e) => e.id !== effectId);
|
|
4316
|
+
const at = Math.max(0, Math.min(newIndex, without.length));
|
|
4317
|
+
this._entries = [...without.slice(0, at), entry, ...without.slice(at)];
|
|
4318
|
+
this._rebuild();
|
|
4319
|
+
}
|
|
4320
|
+
setParams(effectId, params) {
|
|
4321
|
+
const entry = this._find("setParams", effectId);
|
|
4322
|
+
if (!entry) return;
|
|
4323
|
+
entry.params = { ...entry.params, ...params };
|
|
4324
|
+
if (entry.bypassed && entry.wetParam && entry.wetParam in params) {
|
|
4325
|
+
const { [entry.wetParam]: _stored, ...rest } = params;
|
|
4326
|
+
if (Object.keys(rest).length > 0) {
|
|
4327
|
+
entry.instance.applyParams(rest);
|
|
4328
|
+
}
|
|
4329
|
+
return;
|
|
4330
|
+
}
|
|
4331
|
+
entry.instance.applyParams(params);
|
|
4332
|
+
}
|
|
4333
|
+
setBypassed(effectId, bypassed) {
|
|
4334
|
+
const entry = this._find("setBypassed", effectId);
|
|
4335
|
+
if (!entry || entry.bypassed === bypassed) return;
|
|
4336
|
+
entry.bypassed = bypassed;
|
|
4337
|
+
if (entry.wetParam) {
|
|
4338
|
+
const wet = bypassed ? 0 : entry.params[entry.wetParam] ?? 0;
|
|
4339
|
+
entry.instance.applyParams({ [entry.wetParam]: wet });
|
|
4340
|
+
return;
|
|
4341
|
+
}
|
|
4342
|
+
this._rebuild();
|
|
4343
|
+
}
|
|
4344
|
+
dispose() {
|
|
4345
|
+
if (this._disposed) return;
|
|
4346
|
+
this._disposed = true;
|
|
4347
|
+
this._severOwnEdges();
|
|
4348
|
+
for (const entry of this._entries) {
|
|
4349
|
+
entry.instance.dispose?.();
|
|
4350
|
+
}
|
|
4351
|
+
this._entries = [];
|
|
4352
|
+
try {
|
|
4353
|
+
this._output.disconnect();
|
|
4354
|
+
} catch {
|
|
4355
|
+
}
|
|
4356
|
+
}
|
|
4357
|
+
_find(op, effectId) {
|
|
4358
|
+
const entry = this._entries.find((e) => e.id === effectId);
|
|
4359
|
+
if (!entry) {
|
|
4360
|
+
console.warn(
|
|
4361
|
+
"[waveform-playlist] EffectsChainController." + op + ': unknown effectId "' + effectId + '"'
|
|
4362
|
+
);
|
|
4363
|
+
}
|
|
4364
|
+
return entry;
|
|
4365
|
+
}
|
|
4366
|
+
/** Sever only this chain's outgoing edges — never the consumer's. */
|
|
4367
|
+
_severOwnEdges() {
|
|
4368
|
+
this._input.disconnect();
|
|
4369
|
+
for (const entry of this._entries) {
|
|
4370
|
+
entry.instance.output.disconnect();
|
|
4371
|
+
}
|
|
4372
|
+
}
|
|
4373
|
+
_rebuild() {
|
|
4374
|
+
this._severOwnEdges();
|
|
4375
|
+
let previous = this._input;
|
|
4376
|
+
for (const entry of this._entries) {
|
|
4377
|
+
if (entry.bypassed && !entry.wetParam) {
|
|
4378
|
+
continue;
|
|
4379
|
+
}
|
|
4380
|
+
previous.connect(entry.instance.input);
|
|
4381
|
+
previous = entry.instance.output;
|
|
4382
|
+
}
|
|
4383
|
+
previous.connect(this._output);
|
|
4384
|
+
}
|
|
4385
|
+
};
|
|
4386
|
+
|
|
4387
|
+
// src/effects/optional-modules.ts
|
|
4388
|
+
var PREFIX = "[waveform-playlist] ";
|
|
4389
|
+
async function loadOptionalModule(importer, packageName, feature) {
|
|
4390
|
+
try {
|
|
4391
|
+
return await importer();
|
|
4392
|
+
} catch (originalErr) {
|
|
4393
|
+
console.warn(PREFIX + packageName + " dynamic import failed: " + String(originalErr));
|
|
4394
|
+
throw new Error(
|
|
4395
|
+
PREFIX + packageName + " is required for " + feature + ". Install with: npm install " + packageName + " (import failed: " + String(originalErr) + ")"
|
|
4396
|
+
);
|
|
4397
|
+
}
|
|
4398
|
+
}
|
|
4399
|
+
function loadWamModule(feature) {
|
|
4400
|
+
return loadOptionalModule(() => import("@dawcore/wam"), "@dawcore/wam", feature);
|
|
4401
|
+
}
|
|
4402
|
+
function loadFaustModule(feature) {
|
|
4403
|
+
return loadOptionalModule(() => import("@dawcore/faust"), "@dawcore/faust", feature);
|
|
4404
|
+
}
|
|
4405
|
+
|
|
4406
|
+
// src/effects/effect-registry.ts
|
|
4407
|
+
var MAX_DELAY_SECONDS = 10;
|
|
4408
|
+
var registry = /* @__PURE__ */ new Map();
|
|
4409
|
+
function registerEffect(type, definition) {
|
|
4410
|
+
if (registry.has(type)) {
|
|
4411
|
+
console.warn(
|
|
4412
|
+
'[waveform-playlist] registerEffect: overwriting existing effect type "' + type + '"'
|
|
4413
|
+
);
|
|
4414
|
+
}
|
|
4415
|
+
registry.set(type, definition);
|
|
4416
|
+
}
|
|
4417
|
+
function getEffectDefinitions() {
|
|
4418
|
+
return new Map(registry);
|
|
4419
|
+
}
|
|
4420
|
+
function createEffectInstance(type, audioContext, params = {}) {
|
|
4421
|
+
const definition = registry.get(type);
|
|
4422
|
+
if (!definition) {
|
|
4423
|
+
throw new Error(
|
|
4424
|
+
'[waveform-playlist] createEffectInstance: unknown effect type "' + type + '". Available types: ' + [...registry.keys()].join(", ")
|
|
4425
|
+
);
|
|
4426
|
+
}
|
|
4427
|
+
const merged = { ...definition.defaults, ...params };
|
|
4428
|
+
const instance = definition.create(audioContext, merged);
|
|
4429
|
+
instance.applyParams(merged);
|
|
4430
|
+
return { instance, params: merged, wetParam: definition.wetParam };
|
|
4431
|
+
}
|
|
4432
|
+
function singleNode(node, applyParams) {
|
|
4433
|
+
return { input: node, output: node, applyParams };
|
|
4434
|
+
}
|
|
4435
|
+
function registerBuiltIns() {
|
|
4436
|
+
registry.set("native-gain", {
|
|
4437
|
+
label: "Gain",
|
|
4438
|
+
category: "dynamics",
|
|
4439
|
+
defaults: { gain: 1 },
|
|
4440
|
+
params: { gain: { min: 0, max: 2, step: 0.01 } },
|
|
4441
|
+
create: (ctx) => {
|
|
4442
|
+
const node = ctx.createGain();
|
|
4443
|
+
return singleNode(node, (p) => {
|
|
4444
|
+
if (p.gain !== void 0) node.gain.value = p.gain;
|
|
4445
|
+
});
|
|
4446
|
+
}
|
|
4447
|
+
});
|
|
4448
|
+
registry.set("native-filter", {
|
|
4449
|
+
label: "Lowpass Filter",
|
|
4450
|
+
category: "filter",
|
|
4451
|
+
defaults: { frequency: 1e3, q: 1 },
|
|
4452
|
+
params: {
|
|
4453
|
+
frequency: { min: 20, max: 2e4, step: 1, unit: "Hz" },
|
|
4454
|
+
q: { min: 0.1, max: 20, step: 0.1 }
|
|
4455
|
+
},
|
|
4456
|
+
create: (ctx) => {
|
|
4457
|
+
const node = ctx.createBiquadFilter();
|
|
4458
|
+
node.type = "lowpass";
|
|
4459
|
+
return singleNode(node, (p) => {
|
|
4460
|
+
if (p.frequency !== void 0) node.frequency.value = p.frequency;
|
|
4461
|
+
if (p.q !== void 0) node.Q.value = p.q;
|
|
4462
|
+
});
|
|
4463
|
+
}
|
|
4464
|
+
});
|
|
4465
|
+
registry.set("native-compressor", {
|
|
4466
|
+
label: "Compressor",
|
|
4467
|
+
category: "dynamics",
|
|
4468
|
+
defaults: { threshold: -24, knee: 30, ratio: 12, attack: 3e-3, release: 0.25 },
|
|
4469
|
+
params: {
|
|
4470
|
+
threshold: { min: -60, max: 0, step: 1, unit: "dB" },
|
|
4471
|
+
knee: { min: 0, max: 40, step: 1, unit: "dB" },
|
|
4472
|
+
ratio: { min: 1, max: 20, step: 0.5 },
|
|
4473
|
+
attack: { min: 0, max: 1, step: 1e-3, unit: "s" },
|
|
4474
|
+
release: { min: 0, max: 1, step: 0.01, unit: "s" }
|
|
4475
|
+
},
|
|
4476
|
+
create: (ctx) => {
|
|
4477
|
+
const node = ctx.createDynamicsCompressor();
|
|
4478
|
+
return singleNode(node, (p) => {
|
|
4479
|
+
if (p.threshold !== void 0) node.threshold.value = p.threshold;
|
|
4480
|
+
if (p.knee !== void 0) node.knee.value = p.knee;
|
|
4481
|
+
if (p.ratio !== void 0) node.ratio.value = p.ratio;
|
|
4482
|
+
if (p.attack !== void 0) node.attack.value = p.attack;
|
|
4483
|
+
if (p.release !== void 0) node.release.value = p.release;
|
|
4484
|
+
});
|
|
4485
|
+
}
|
|
4486
|
+
});
|
|
4487
|
+
registry.set("native-stereo-panner", {
|
|
4488
|
+
label: "Stereo Panner",
|
|
4489
|
+
category: "spatial",
|
|
4490
|
+
defaults: { pan: 0 },
|
|
4491
|
+
params: { pan: { min: -1, max: 1, step: 0.01 } },
|
|
4492
|
+
create: (ctx) => {
|
|
4493
|
+
const node = ctx.createStereoPanner();
|
|
4494
|
+
return singleNode(node, (p) => {
|
|
4495
|
+
if (p.pan !== void 0) node.pan.value = p.pan;
|
|
4496
|
+
});
|
|
4497
|
+
}
|
|
4498
|
+
});
|
|
4499
|
+
registry.set("native-delay", {
|
|
4500
|
+
label: "Delay",
|
|
4501
|
+
category: "delay",
|
|
4502
|
+
defaults: { delayTime: 0.25, feedback: 0.4, wet: 0.35 },
|
|
4503
|
+
params: {
|
|
4504
|
+
delayTime: { min: 0, max: MAX_DELAY_SECONDS, step: 0.01, unit: "s" },
|
|
4505
|
+
feedback: { min: 0, max: 0.95, step: 0.01 },
|
|
4506
|
+
wet: { min: 0, max: 1, step: 0.01 }
|
|
4507
|
+
},
|
|
4508
|
+
wetParam: "wet",
|
|
4509
|
+
create: (ctx) => {
|
|
4510
|
+
const input = ctx.createGain();
|
|
4511
|
+
const output = ctx.createGain();
|
|
4512
|
+
const delay = ctx.createDelay(MAX_DELAY_SECONDS);
|
|
4513
|
+
const feedback = ctx.createGain();
|
|
4514
|
+
const wet = ctx.createGain();
|
|
4515
|
+
const dry = ctx.createGain();
|
|
4516
|
+
input.connect(dry);
|
|
4517
|
+
dry.connect(output);
|
|
4518
|
+
input.connect(delay);
|
|
4519
|
+
delay.connect(feedback);
|
|
4520
|
+
feedback.connect(delay);
|
|
4521
|
+
delay.connect(wet);
|
|
4522
|
+
wet.connect(output);
|
|
4523
|
+
return {
|
|
4524
|
+
input,
|
|
4525
|
+
output,
|
|
4526
|
+
applyParams: (p) => {
|
|
4527
|
+
if (p.delayTime !== void 0) delay.delayTime.value = p.delayTime;
|
|
4528
|
+
if (p.feedback !== void 0) feedback.gain.value = p.feedback;
|
|
4529
|
+
if (p.wet !== void 0) {
|
|
4530
|
+
wet.gain.value = p.wet;
|
|
4531
|
+
dry.gain.value = 1 - p.wet;
|
|
4532
|
+
}
|
|
4533
|
+
}
|
|
4534
|
+
};
|
|
4535
|
+
}
|
|
4536
|
+
});
|
|
4537
|
+
}
|
|
4538
|
+
registerBuiltIns();
|
|
4539
|
+
|
|
4540
|
+
// src/effects/effects-manager.ts
|
|
4541
|
+
var PREFIX2 = "[waveform-playlist] ";
|
|
4542
|
+
function makeGuiRecord(element, destroyImpl) {
|
|
4543
|
+
let destroyed = false;
|
|
4544
|
+
return {
|
|
4545
|
+
element,
|
|
4546
|
+
destroy: () => {
|
|
4547
|
+
if (destroyed) return;
|
|
4548
|
+
destroyed = true;
|
|
4549
|
+
destroyImpl();
|
|
4550
|
+
}
|
|
4551
|
+
};
|
|
4552
|
+
}
|
|
4553
|
+
var EffectsManager = class {
|
|
4554
|
+
constructor(getAdapter, masterEventTarget) {
|
|
4555
|
+
this._masterChain = null;
|
|
4556
|
+
this._trackChains = /* @__PURE__ */ new Map();
|
|
4557
|
+
/** Per-chain restore ownership — a newer setEffectsState supersedes a stale in-flight one. */
|
|
4558
|
+
this._restoreTokens = /* @__PURE__ */ new WeakMap();
|
|
4559
|
+
/** Cached GUI elements by effectId — close hides, only removal destroys. */
|
|
4560
|
+
this._guis = /* @__PURE__ */ new Map();
|
|
4561
|
+
/** In-flight GUI creation by effectId — concurrent opens share one build. */
|
|
4562
|
+
this._guiPending = /* @__PURE__ */ new Map();
|
|
4563
|
+
/** Live WAM plugin nodes across all chains, fed to the wam-transport bridge. */
|
|
4564
|
+
this._wamNodes = /* @__PURE__ */ new Set();
|
|
4565
|
+
this._transportBridge = null;
|
|
4566
|
+
this._getAdapter = getAdapter;
|
|
4567
|
+
this._masterTarget = masterEventTarget;
|
|
4568
|
+
}
|
|
4569
|
+
// --- Master chain ---
|
|
4570
|
+
addMasterEffect(type, params) {
|
|
4571
|
+
const chain = this._ensureMasterChain();
|
|
4572
|
+
return this._addToChain(chain, this._masterTarget, type, params);
|
|
4573
|
+
}
|
|
4574
|
+
masterEffects() {
|
|
4575
|
+
return this._masterChain?.entries ?? [];
|
|
4576
|
+
}
|
|
4577
|
+
masterOp(op, effectId, arg) {
|
|
4578
|
+
this._runOp(this._masterChain, this._masterTarget, op, effectId, arg);
|
|
4579
|
+
}
|
|
4580
|
+
addMasterWamPlugin(url, initialState) {
|
|
4581
|
+
const chain = this._ensureMasterChain();
|
|
4582
|
+
return this._addWamToChain(chain, this._masterTarget, url, initialState);
|
|
4583
|
+
}
|
|
4584
|
+
addMasterFaustEffect(dspCode, options) {
|
|
4585
|
+
const chain = this._ensureMasterChain();
|
|
4586
|
+
return this._addFaustToChain(chain, this._masterTarget, dspCode, { name: options?.name });
|
|
4587
|
+
}
|
|
4588
|
+
getMasterEffectsState() {
|
|
4589
|
+
return this._masterChain?.serialize() ?? Promise.resolve([]);
|
|
4590
|
+
}
|
|
4591
|
+
async setMasterEffectsState(entries) {
|
|
4592
|
+
validateSerializedEntries(entries);
|
|
4593
|
+
await this._restoreChain(this._ensureMasterChain(), this._masterTarget, entries);
|
|
4594
|
+
}
|
|
4595
|
+
// --- Track chains ---
|
|
4596
|
+
addTrackEffect(trackId, target, type, params) {
|
|
4597
|
+
const chain = this._ensureTrackChain(trackId);
|
|
4598
|
+
return this._addToChain(chain, target, type, params);
|
|
4599
|
+
}
|
|
4600
|
+
addTrackWamPlugin(trackId, target, url, initialState) {
|
|
4601
|
+
const chain = this._ensureTrackChain(trackId);
|
|
4602
|
+
return this._addWamToChain(chain, target, url, initialState);
|
|
4603
|
+
}
|
|
4604
|
+
addTrackFaustEffect(trackId, target, dspCode, options) {
|
|
4605
|
+
const chain = this._ensureTrackChain(trackId);
|
|
4606
|
+
return this._addFaustToChain(chain, target, dspCode, { name: options?.name });
|
|
4607
|
+
}
|
|
4608
|
+
trackEffects(trackId) {
|
|
4609
|
+
return this._trackChains.get(trackId)?.entries ?? [];
|
|
4610
|
+
}
|
|
4611
|
+
getTrackEffectsState(trackId) {
|
|
4612
|
+
return this._trackChains.get(trackId)?.serialize() ?? Promise.resolve([]);
|
|
4613
|
+
}
|
|
4614
|
+
async setTrackEffectsState(trackId, target, entries) {
|
|
4615
|
+
validateSerializedEntries(entries);
|
|
4616
|
+
await this._restoreChain(this._ensureTrackChain(trackId), target, entries);
|
|
4617
|
+
}
|
|
4618
|
+
trackOp(trackId, target, op, effectId, arg) {
|
|
4619
|
+
this._runOp(this._trackChains.get(trackId) ?? null, target, op, effectId, arg);
|
|
4620
|
+
}
|
|
4621
|
+
// --- Effect GUIs ---
|
|
4622
|
+
/** Open (lazily creating) the GUI for a master-chain effect. */
|
|
4623
|
+
openMasterEffectGui(effectId, container) {
|
|
4624
|
+
return this._openGui(this._masterChain, this._masterTarget, effectId, container);
|
|
4625
|
+
}
|
|
4626
|
+
/** Open (lazily creating) the GUI for a track-chain effect. */
|
|
4627
|
+
openTrackEffectGui(trackId, target, effectId, container) {
|
|
4628
|
+
return this._openGui(this._trackChains.get(trackId) ?? null, target, effectId, container);
|
|
4629
|
+
}
|
|
4630
|
+
/** Hide an open GUI. The element stays cached so reopen is instant —
|
|
4631
|
+
* audio processing is never interrupted. */
|
|
4632
|
+
closeEffectGui(effectId) {
|
|
4633
|
+
const record = this._guis.get(effectId);
|
|
4634
|
+
if (!record) {
|
|
4635
|
+
console.warn(PREFIX2 + 'closeEffectGui: no open GUI for effectId "' + effectId + '"');
|
|
4636
|
+
return;
|
|
4637
|
+
}
|
|
4638
|
+
record.element.remove();
|
|
4639
|
+
}
|
|
4640
|
+
// --- Lifecycle ---
|
|
4641
|
+
/** Transport setTracks rebuilds TrackNodes, severing chain hookups — re-wire. */
|
|
4642
|
+
rewireTrackChains() {
|
|
4643
|
+
const transport = this._getAdapter()?.transport;
|
|
4644
|
+
if (!transport) return;
|
|
4645
|
+
for (const [trackId, chain] of this._trackChains) {
|
|
4646
|
+
transport.connectTrackOutput(trackId, chain.input);
|
|
4647
|
+
chain.output.connect(transport.masterOutputNode);
|
|
4648
|
+
}
|
|
4649
|
+
}
|
|
4650
|
+
disposeTrackChain(trackId) {
|
|
4651
|
+
const chain = this._trackChains.get(trackId);
|
|
4652
|
+
if (!chain) return;
|
|
4653
|
+
this._trackChains.delete(trackId);
|
|
4654
|
+
for (const entry of chain.entries) {
|
|
4655
|
+
this._destroyGui(entry.id);
|
|
4656
|
+
}
|
|
4657
|
+
try {
|
|
4658
|
+
this._getAdapter()?.transport?.disconnectTrackOutput(trackId);
|
|
4659
|
+
} catch (err) {
|
|
4660
|
+
console.warn(PREFIX2 + "EffectsManager: error disconnecting track output: " + String(err));
|
|
4661
|
+
}
|
|
4662
|
+
chain.dispose();
|
|
4663
|
+
}
|
|
4664
|
+
disposeAll() {
|
|
4665
|
+
this._transportBridge?.dispose();
|
|
4666
|
+
this._transportBridge = null;
|
|
4667
|
+
this._wamNodes.clear();
|
|
4668
|
+
for (const trackId of [...this._trackChains.keys()]) {
|
|
4669
|
+
this.disposeTrackChain(trackId);
|
|
4670
|
+
}
|
|
4671
|
+
if (this._masterChain) {
|
|
4672
|
+
for (const entry of this._masterChain.entries) {
|
|
4673
|
+
this._destroyGui(entry.id);
|
|
4674
|
+
}
|
|
4675
|
+
try {
|
|
4676
|
+
this._getAdapter()?.transport?.disconnectMasterOutput();
|
|
4677
|
+
} catch (err) {
|
|
4678
|
+
console.warn(PREFIX2 + "EffectsManager: error disconnecting master output: " + String(err));
|
|
4679
|
+
}
|
|
4680
|
+
this._masterChain.dispose();
|
|
4681
|
+
this._masterChain = null;
|
|
4682
|
+
}
|
|
4683
|
+
}
|
|
4684
|
+
// --- Private ---
|
|
4685
|
+
_requireWiring() {
|
|
4686
|
+
const adapter = this._getAdapter();
|
|
4687
|
+
if (!adapter) {
|
|
4688
|
+
throw new Error(
|
|
4689
|
+
PREFIX2 + "effects require an adapter \u2014 set editor.adapter before adding effects."
|
|
4690
|
+
);
|
|
4691
|
+
}
|
|
4692
|
+
const { audioContext, transport } = adapter;
|
|
4693
|
+
if (!audioContext || !transport || typeof transport.connectTrackOutput !== "function") {
|
|
4694
|
+
throw new Error(
|
|
4695
|
+
PREFIX2 + "the current adapter does not expose effects hooks (transport.connectTrackOutput / connectMasterOutput)."
|
|
4696
|
+
);
|
|
4697
|
+
}
|
|
4698
|
+
return { audioContext, transport };
|
|
4699
|
+
}
|
|
4700
|
+
/** Dynamic-import the optional @dawcore/wam peer with an actionable error. */
|
|
4701
|
+
_loadWamModule(feature) {
|
|
4702
|
+
return loadWamModule(feature);
|
|
4703
|
+
}
|
|
4704
|
+
/** Dynamic-import the optional @dawcore/faust peer with an actionable error. */
|
|
4705
|
+
_loadFaustModule(feature) {
|
|
4706
|
+
return loadFaustModule(feature);
|
|
4707
|
+
}
|
|
4708
|
+
async _openGui(chain, target, effectId, container) {
|
|
4709
|
+
if (!container || typeof container.appendChild !== "function") {
|
|
4710
|
+
throw new Error(PREFIX2 + "openEffectGui: container must be a DOM element");
|
|
4711
|
+
}
|
|
4712
|
+
const entry = chain?.getEntry(effectId);
|
|
4713
|
+
if (!chain || !entry) {
|
|
4714
|
+
throw new Error(PREFIX2 + 'openEffectGui: unknown effectId "' + effectId + '"');
|
|
4715
|
+
}
|
|
4716
|
+
if (entry.error !== void 0) {
|
|
4717
|
+
throw new Error(
|
|
4718
|
+
PREFIX2 + 'openEffectGui: effect "' + effectId + '" is a failed-plugin placeholder (' + entry.error + ") \u2014 no GUI is available. Remove it or retry the restore."
|
|
4719
|
+
);
|
|
4720
|
+
}
|
|
4721
|
+
const cached = this._guis.get(effectId);
|
|
4722
|
+
if (cached) {
|
|
4723
|
+
container.appendChild(cached.element);
|
|
4724
|
+
return cached.element;
|
|
4725
|
+
}
|
|
4726
|
+
let pending = this._guiPending.get(effectId);
|
|
4727
|
+
if (!pending) {
|
|
4728
|
+
pending = this._createGuiRecord(chain, target, entry, effectId).finally(() => {
|
|
4729
|
+
this._guiPending.delete(effectId);
|
|
4730
|
+
});
|
|
4731
|
+
this._guiPending.set(effectId, pending);
|
|
4732
|
+
}
|
|
4733
|
+
const record = await pending;
|
|
4734
|
+
if (chain.disposed || !chain.getEntry(effectId)) {
|
|
4735
|
+
this._guis.delete(effectId);
|
|
4736
|
+
record.element.remove();
|
|
4737
|
+
try {
|
|
4738
|
+
record.destroy();
|
|
4739
|
+
} catch (err) {
|
|
4740
|
+
console.warn(
|
|
4741
|
+
PREFIX2 + 'openEffectGui: destroying a late GUI for "' + effectId + '" failed: ' + String(err)
|
|
4742
|
+
);
|
|
4743
|
+
}
|
|
4744
|
+
throw new Error(
|
|
4745
|
+
PREFIX2 + 'openEffectGui: effect "' + effectId + '" was removed while its GUI was loading; the GUI was discarded.'
|
|
4746
|
+
);
|
|
4747
|
+
}
|
|
4748
|
+
this._guis.set(effectId, record);
|
|
4749
|
+
container.appendChild(record.element);
|
|
4750
|
+
return record.element;
|
|
4751
|
+
}
|
|
4752
|
+
/** Build a GUI record: the plugin's own GUI when available, otherwise the
|
|
4753
|
+
* generic parameter panel from @dawcore/wam. */
|
|
4754
|
+
async _createGuiRecord(chain, target, entry, effectId) {
|
|
4755
|
+
const { instance } = entry;
|
|
4756
|
+
if (typeof instance.createGui === "function") {
|
|
4757
|
+
try {
|
|
4758
|
+
const element2 = await instance.createGui();
|
|
4759
|
+
return makeGuiRecord(element2, () => instance.destroyGui?.(element2));
|
|
4760
|
+
} catch (err) {
|
|
4761
|
+
console.warn(
|
|
4762
|
+
PREFIX2 + 'openEffectGui: plugin createGui failed for "' + effectId + '" \u2014 falling back to the generic parameter panel: ' + String(err)
|
|
4763
|
+
);
|
|
4764
|
+
}
|
|
4765
|
+
}
|
|
4766
|
+
const element = await this._createFallbackPanel(chain, target, entry, effectId);
|
|
4767
|
+
return makeGuiRecord(element, () => {
|
|
4768
|
+
});
|
|
4769
|
+
}
|
|
4770
|
+
/** The generic parameter panel — one code path for "no custom GUI":
|
|
4771
|
+
* native entries render from the registry's params metadata, WAM entries
|
|
4772
|
+
* from getParameterInfo(). Edits route through the regular setParams op so
|
|
4773
|
+
* they hit the audio (applyParams → setParameterValues for WAM) AND
|
|
4774
|
+
* dispatch daw-effect-change like any other parameter edit. */
|
|
4775
|
+
async _createFallbackPanel(chain, target, entry, effectId) {
|
|
4776
|
+
const wamModule = await this._loadWamModule("openEffectGui() parameter panels");
|
|
4777
|
+
const onChange = (paramId, value) => {
|
|
4778
|
+
this._runOp(chain, target, "setParams", effectId, { [paramId]: value });
|
|
4779
|
+
};
|
|
4780
|
+
if (entry.kind === "native") {
|
|
4781
|
+
const definition = getEffectDefinitions().get(entry.type);
|
|
4782
|
+
if (!definition) {
|
|
4783
|
+
throw new Error(
|
|
4784
|
+
PREFIX2 + 'openEffectGui: no registry definition for effect type "' + entry.type + '"'
|
|
4785
|
+
);
|
|
4786
|
+
}
|
|
4787
|
+
const params = Object.entries(definition.params).map(([id, def]) => ({
|
|
4788
|
+
id,
|
|
4789
|
+
min: def.min,
|
|
4790
|
+
max: def.max,
|
|
4791
|
+
...def.step !== void 0 ? { step: def.step } : {},
|
|
4792
|
+
...def.unit !== void 0 ? { unit: def.unit } : {},
|
|
4793
|
+
value: entry.params[id] ?? definition.defaults[id]
|
|
4794
|
+
}));
|
|
4795
|
+
return wamModule.createParameterPanel(params, onChange);
|
|
4796
|
+
}
|
|
4797
|
+
if (typeof entry.instance.getParameterInfo !== "function") {
|
|
4798
|
+
throw new Error(
|
|
4799
|
+
PREFIX2 + 'openEffectGui: effect "' + effectId + '" has no GUI and exposes no parameter info \u2014 nothing to render.'
|
|
4800
|
+
);
|
|
4801
|
+
}
|
|
4802
|
+
return wamModule.createWamParameterPanel(
|
|
4803
|
+
{ getParameterInfo: () => entry.instance.getParameterInfo() },
|
|
4804
|
+
{ onParamChange: onChange }
|
|
4805
|
+
);
|
|
4806
|
+
}
|
|
4807
|
+
/** Detach + destroy a cached GUI. Called only from removal paths — close
|
|
4808
|
+
* never destroys. Safe when no GUI was ever opened. */
|
|
4809
|
+
_destroyGui(effectId) {
|
|
4810
|
+
const record = this._guis.get(effectId);
|
|
4811
|
+
if (!record) return;
|
|
4812
|
+
this._guis.delete(effectId);
|
|
4813
|
+
record.element.remove();
|
|
4814
|
+
try {
|
|
4815
|
+
record.destroy();
|
|
4816
|
+
} catch (err) {
|
|
4817
|
+
console.warn(PREFIX2 + 'destroyGui failed for effect "' + effectId + '": ' + String(err));
|
|
4818
|
+
}
|
|
4819
|
+
}
|
|
4820
|
+
_ensureMasterChain() {
|
|
4821
|
+
if (this._masterChain) return this._masterChain;
|
|
4822
|
+
const { audioContext, transport } = this._requireWiring();
|
|
4823
|
+
const chain = new EffectsChainController(audioContext);
|
|
4824
|
+
transport.connectMasterOutput(chain.input);
|
|
4825
|
+
chain.output.connect(audioContext.destination);
|
|
4826
|
+
this._masterChain = chain;
|
|
4827
|
+
return chain;
|
|
4828
|
+
}
|
|
4829
|
+
_ensureTrackChain(trackId) {
|
|
4830
|
+
const existing = this._trackChains.get(trackId);
|
|
4831
|
+
if (existing) return existing;
|
|
4832
|
+
const { audioContext, transport } = this._requireWiring();
|
|
4833
|
+
const chain = new EffectsChainController(audioContext);
|
|
4834
|
+
transport.connectTrackOutput(trackId, chain.input);
|
|
4835
|
+
chain.output.connect(transport.masterOutputNode);
|
|
4836
|
+
this._trackChains.set(trackId, chain);
|
|
4837
|
+
return chain;
|
|
4838
|
+
}
|
|
4839
|
+
_addToChain(chain, target, type, params) {
|
|
4840
|
+
const audioContext = this._requireWiring().audioContext;
|
|
4841
|
+
const created = createEffectInstance(type, audioContext, params);
|
|
4842
|
+
const effectId = chain.add({
|
|
4843
|
+
kind: "native",
|
|
4844
|
+
type,
|
|
4845
|
+
instance: created.instance,
|
|
4846
|
+
params: created.params,
|
|
4847
|
+
wetParam: created.wetParam
|
|
4848
|
+
});
|
|
4849
|
+
const index = chain.entries.findIndex((e) => e.id === effectId);
|
|
4850
|
+
this._dispatch(target, "daw-effect-add", {
|
|
4851
|
+
effectId,
|
|
4852
|
+
kind: "native",
|
|
4853
|
+
type,
|
|
4854
|
+
params: { ...created.params },
|
|
4855
|
+
index
|
|
4856
|
+
});
|
|
4857
|
+
return effectId;
|
|
4858
|
+
}
|
|
4859
|
+
/**
|
|
4860
|
+
* Load a WAM plugin (via the optional @dawcore/wam peer dep) and add it to
|
|
4861
|
+
* a chain as a kind:'wam' entry. WAM entries participate in every chain
|
|
4862
|
+
* operation with no special-casing: remove destroys the plugin (via the
|
|
4863
|
+
* entry's dispose), bypass uses disconnection semantics (no wet param),
|
|
4864
|
+
* and setParams maps onto the plugin's setParameterValues.
|
|
4865
|
+
*/
|
|
4866
|
+
async _addWamToChain(chain, target, url, initialState) {
|
|
4867
|
+
const { audioContext } = this._requireWiring();
|
|
4868
|
+
const wamModule = await this._loadWamModule("addWamPlugin()");
|
|
4869
|
+
const { hostGroupId } = await wamModule.ensureWamHost(audioContext);
|
|
4870
|
+
const plugin = await wamModule.createWamInstance(url, audioContext, hostGroupId, {
|
|
4871
|
+
initialState
|
|
4872
|
+
});
|
|
4873
|
+
return this._insertWamPlugin(chain, target, wamModule, plugin, { url });
|
|
4874
|
+
}
|
|
4875
|
+
/**
|
|
4876
|
+
* Compile Faust DSP source in the browser (via the optional @dawcore/faust
|
|
4877
|
+
* peer) and add the resulting WAM to a chain. Compilation happens BEFORE
|
|
4878
|
+
* any chain work, so a Faust error (with its line/column diagnostics intact)
|
|
4879
|
+
* leaves the chain untouched. The entry lands as kind:'wam' with a
|
|
4880
|
+
* `source: { faust }` marker so persistence recompiles instead of fetching.
|
|
4881
|
+
*/
|
|
4882
|
+
async _addFaustToChain(chain, target, dspCode, opts) {
|
|
4883
|
+
if (typeof dspCode !== "string" || dspCode.trim().length === 0) {
|
|
4884
|
+
throw new Error(PREFIX2 + "addFaustEffect: dspCode must be a non-empty string");
|
|
4885
|
+
}
|
|
4886
|
+
this._requireWiring();
|
|
4887
|
+
const faustModule = await this._loadFaustModule("addFaustEffect()");
|
|
4888
|
+
const compiled = await faustModule.compileFaustToWam(dspCode, { name: opts.name });
|
|
4889
|
+
const { audioContext } = this._requireWiring();
|
|
4890
|
+
const wamModule = await this._loadWamModule("addFaustEffect()");
|
|
4891
|
+
const { hostGroupId } = await wamModule.ensureWamHost(audioContext);
|
|
4892
|
+
const plugin = await wamModule.createWamInstanceFromFactory(
|
|
4893
|
+
compiled.factory,
|
|
4894
|
+
audioContext,
|
|
4895
|
+
hostGroupId,
|
|
4896
|
+
{ initialState: opts.initialState, label: compiled.name }
|
|
4897
|
+
);
|
|
4898
|
+
return this._insertWamPlugin(chain, target, wamModule, plugin, {
|
|
4899
|
+
source: { faust: compiled.dspCode }
|
|
4900
|
+
});
|
|
4901
|
+
}
|
|
4902
|
+
/**
|
|
4903
|
+
* Wire a live WAM plugin instance into a chain as a kind:'wam' entry —
|
|
4904
|
+
* shared by the url path (addWamPlugin) and the Faust path (addFaustEffect).
|
|
4905
|
+
* WAM entries participate in every chain operation with no special-casing:
|
|
4906
|
+
* remove destroys the plugin (via the entry's dispose), bypass uses
|
|
4907
|
+
* disconnection semantics (no wet param), and setParams maps onto the
|
|
4908
|
+
* plugin's setParameterValues.
|
|
4909
|
+
*/
|
|
4910
|
+
_insertWamPlugin(chain, target, wamModule, plugin, meta) {
|
|
4911
|
+
const node = plugin.audioNode;
|
|
4912
|
+
const label = plugin.descriptor.name;
|
|
4913
|
+
if (chain.disposed) {
|
|
4914
|
+
plugin.destroy();
|
|
4915
|
+
throw new Error(
|
|
4916
|
+
PREFIX2 + 'addWamPlugin: the effects chain was disposed while "' + (meta.url ?? label) + '" was loading; the plugin was discarded.'
|
|
4917
|
+
);
|
|
4918
|
+
}
|
|
4919
|
+
let effectId;
|
|
4920
|
+
try {
|
|
4921
|
+
effectId = chain.add({
|
|
4922
|
+
kind: "wam",
|
|
4923
|
+
type: "wam",
|
|
4924
|
+
...meta.url !== void 0 ? { url: meta.url } : {},
|
|
4925
|
+
...meta.source !== void 0 ? { source: meta.source } : {},
|
|
4926
|
+
label,
|
|
4927
|
+
instance: {
|
|
4928
|
+
input: node,
|
|
4929
|
+
output: node,
|
|
4930
|
+
applyParams: (params) => {
|
|
4931
|
+
node.setParameterValues?.(toWamParameterMap(params))?.catch((err) => {
|
|
4932
|
+
console.warn(PREFIX2 + "WAM setParameterValues failed: " + String(err));
|
|
4933
|
+
});
|
|
4934
|
+
},
|
|
4935
|
+
dispose: () => {
|
|
4936
|
+
this._wamNodes.delete(node);
|
|
4937
|
+
plugin.destroy();
|
|
4938
|
+
},
|
|
4939
|
+
getState: () => plugin.getState(),
|
|
4940
|
+
getParameterInfo: () => plugin.getParameterInfo(),
|
|
4941
|
+
...plugin.createGui ? { createGui: () => plugin.createGui() } : {},
|
|
4942
|
+
...plugin.destroyGui ? { destroyGui: (gui) => plugin.destroyGui(gui) } : {}
|
|
4943
|
+
},
|
|
4944
|
+
params: {}
|
|
4945
|
+
});
|
|
4946
|
+
} catch (err) {
|
|
4947
|
+
try {
|
|
4948
|
+
plugin.destroy();
|
|
4949
|
+
} catch (destroyErr) {
|
|
4950
|
+
console.warn(
|
|
4951
|
+
PREFIX2 + "addWamPlugin: cleanup after failed insertion also failed: " + String(destroyErr)
|
|
4952
|
+
);
|
|
4953
|
+
}
|
|
4954
|
+
throw err;
|
|
4955
|
+
}
|
|
4956
|
+
this._wamNodes.add(node);
|
|
4957
|
+
this._ensureTransportBridge(wamModule)?.notifyNodeAdded(node);
|
|
4958
|
+
const index = chain.entries.findIndex((e) => e.id === effectId);
|
|
4959
|
+
this._dispatch(target, "daw-effect-add", {
|
|
4960
|
+
effectId,
|
|
4961
|
+
kind: "wam",
|
|
4962
|
+
type: "wam",
|
|
4963
|
+
...meta.url !== void 0 ? { url: meta.url } : {},
|
|
4964
|
+
...meta.source !== void 0 ? { source: meta.source } : {},
|
|
4965
|
+
params: {},
|
|
4966
|
+
index
|
|
4967
|
+
});
|
|
4968
|
+
return effectId;
|
|
4969
|
+
}
|
|
4970
|
+
/**
|
|
4971
|
+
* Lazily create the wam-transport bridge so tempo-synced plugins lock to
|
|
4972
|
+
* the timeline. Skipped (not an error) when the adapter's transport lacks
|
|
4973
|
+
* the query/event surface — the bridge is an enhancement, not a
|
|
4974
|
+
* requirement for audio processing.
|
|
4975
|
+
*/
|
|
4976
|
+
_ensureTransportBridge(wamModule) {
|
|
4977
|
+
if (this._transportBridge) return this._transportBridge;
|
|
4978
|
+
if (typeof wamModule.createWamTransportBridge !== "function") return null;
|
|
4979
|
+
const transport = this._getAdapter()?.transport;
|
|
4980
|
+
if (!transport || typeof transport.on !== "function" || typeof transport.getTempo !== "function" || typeof transport.tickToBar !== "function" || typeof transport.timeToTick !== "function") {
|
|
4981
|
+
return null;
|
|
4982
|
+
}
|
|
4983
|
+
this._transportBridge = wamModule.createWamTransportBridge(transport, () => [
|
|
4984
|
+
...this._wamNodes
|
|
4985
|
+
]);
|
|
4986
|
+
return this._transportBridge;
|
|
4987
|
+
}
|
|
4988
|
+
/**
|
|
4989
|
+
* Replace a chain's contents with a persisted snapshot. Entries restore
|
|
4990
|
+
* sequentially so chain order survives async WAM loads. A WAM url that
|
|
4991
|
+
* fails to load becomes a bypassed passthrough placeholder at its saved
|
|
4992
|
+
* position — the restore continues, a daw-effect-error fires, and the
|
|
4993
|
+
* saved state is retained so a later snapshot/retry round-trips it.
|
|
4994
|
+
*/
|
|
4995
|
+
async _restoreChain(chain, target, entries) {
|
|
4996
|
+
const token = /* @__PURE__ */ Symbol("restore");
|
|
4997
|
+
this._restoreTokens.set(chain, token);
|
|
4998
|
+
const superseded = () => this._restoreTokens.get(chain) !== token;
|
|
4999
|
+
for (const existing of chain.entries) {
|
|
5000
|
+
this._runOp(chain, target, "remove", existing.id);
|
|
5001
|
+
}
|
|
5002
|
+
for (const entry of entries) {
|
|
5003
|
+
if (superseded()) return;
|
|
5004
|
+
if (entry.kind === "native") {
|
|
5005
|
+
const id = this._addToChain(chain, target, entry.type, entry.params);
|
|
5006
|
+
if (entry.bypassed) {
|
|
5007
|
+
this._runOp(chain, target, "setBypassed", id, true);
|
|
5008
|
+
}
|
|
5009
|
+
continue;
|
|
5010
|
+
}
|
|
5011
|
+
try {
|
|
5012
|
+
const id = entry.faustDsp !== void 0 ? await this._addFaustToChain(chain, target, entry.faustDsp, {
|
|
5013
|
+
name: entry.faustName,
|
|
5014
|
+
initialState: entry.state
|
|
5015
|
+
}) : await this._addWamToChain(chain, target, entry.url ?? "", entry.state);
|
|
5016
|
+
if (superseded()) {
|
|
5017
|
+
this._runOp(chain, target, "remove", id);
|
|
5018
|
+
return;
|
|
5019
|
+
}
|
|
5020
|
+
if (entry.bypassed) {
|
|
5021
|
+
this._runOp(chain, target, "setBypassed", id, true);
|
|
5022
|
+
}
|
|
5023
|
+
} catch (err) {
|
|
5024
|
+
if (superseded()) return;
|
|
5025
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
5026
|
+
const sourceLabel = entry.url ?? entry.faustName ?? "Faust effect";
|
|
5027
|
+
console.warn(
|
|
5028
|
+
PREFIX2 + 'setEffectsState: plugin "' + sourceLabel + '" failed to restore: ' + message
|
|
5029
|
+
);
|
|
5030
|
+
const effectId = this._addWamPlaceholder(chain, entry, message);
|
|
5031
|
+
this._dispatch(target, "daw-effect-error", {
|
|
5032
|
+
effectId,
|
|
5033
|
+
...entry.url !== void 0 ? { url: entry.url } : {},
|
|
5034
|
+
...entry.faustDsp !== void 0 ? { source: { faust: entry.faustDsp } } : {},
|
|
5035
|
+
message
|
|
5036
|
+
});
|
|
5037
|
+
}
|
|
5038
|
+
}
|
|
5039
|
+
}
|
|
5040
|
+
/** A silent passthrough occupying the failed plugin's chain position. */
|
|
5041
|
+
_addWamPlaceholder(chain, entry, message) {
|
|
5042
|
+
const { audioContext } = this._requireWiring();
|
|
5043
|
+
const node = audioContext.createGain();
|
|
5044
|
+
const effectId = chain.add({
|
|
5045
|
+
kind: "wam",
|
|
5046
|
+
type: "wam",
|
|
5047
|
+
...entry.url !== void 0 ? { url: entry.url } : {},
|
|
5048
|
+
...entry.faustDsp !== void 0 ? { source: { faust: entry.faustDsp } } : {},
|
|
5049
|
+
label: entry.url ?? entry.faustName ?? "Faust effect",
|
|
5050
|
+
error: message,
|
|
5051
|
+
placeholder: { state: entry.state, bypassed: entry.bypassed },
|
|
5052
|
+
instance: { input: node, output: node, applyParams: () => {
|
|
5053
|
+
} },
|
|
5054
|
+
params: {}
|
|
5055
|
+
});
|
|
5056
|
+
chain.setBypassed(effectId, true);
|
|
5057
|
+
return effectId;
|
|
5058
|
+
}
|
|
5059
|
+
_runOp(chain, target, op, effectId, arg) {
|
|
5060
|
+
const entries = chain?.entries ?? [];
|
|
5061
|
+
const fromIndex = entries.findIndex((e) => e.id === effectId);
|
|
5062
|
+
if (!chain || fromIndex === -1) {
|
|
5063
|
+
console.warn(PREFIX2 + "effects." + op + ': unknown effectId "' + effectId + '"');
|
|
5064
|
+
return;
|
|
5065
|
+
}
|
|
5066
|
+
if (entries[fromIndex].error !== void 0 && (op === "setParams" || op === "setBypassed")) {
|
|
5067
|
+
console.warn(
|
|
5068
|
+
PREFIX2 + "effects." + op + ': effect "' + effectId + '" is a failed-plugin placeholder (' + entries[fromIndex].error + ") \u2014 edit ignored. Remove it or retry the restore."
|
|
5069
|
+
);
|
|
5070
|
+
return;
|
|
5071
|
+
}
|
|
5072
|
+
switch (op) {
|
|
5073
|
+
case "remove":
|
|
5074
|
+
this._destroyGui(effectId);
|
|
5075
|
+
chain.remove(effectId);
|
|
5076
|
+
this._dispatch(target, "daw-effect-remove", { effectId });
|
|
5077
|
+
break;
|
|
5078
|
+
case "setParams":
|
|
5079
|
+
chain.setParams(effectId, arg);
|
|
5080
|
+
this._dispatch(target, "daw-effect-change", { effectId, params: { ...arg } });
|
|
5081
|
+
break;
|
|
5082
|
+
case "setBypassed":
|
|
5083
|
+
chain.setBypassed(effectId, arg);
|
|
5084
|
+
this._dispatch(target, "daw-effect-bypass", { effectId, bypassed: arg });
|
|
5085
|
+
break;
|
|
5086
|
+
case "move":
|
|
5087
|
+
chain.move(effectId, arg);
|
|
5088
|
+
this._dispatch(target, "daw-effect-reorder", { effectId, fromIndex, toIndex: arg });
|
|
5089
|
+
break;
|
|
5090
|
+
}
|
|
5091
|
+
}
|
|
5092
|
+
_dispatch(target, name, detail) {
|
|
5093
|
+
target.dispatchEvent(new CustomEvent(name, { bubbles: true, composed: true, detail }));
|
|
5094
|
+
}
|
|
5095
|
+
};
|
|
5096
|
+
function validateSerializedEntries(entries) {
|
|
5097
|
+
if (!Array.isArray(entries)) {
|
|
5098
|
+
throw new Error(PREFIX2 + "setEffectsState: expected an array of serialized effect entries");
|
|
5099
|
+
}
|
|
5100
|
+
entries.forEach((entry, i) => {
|
|
5101
|
+
const at = " (entry " + i + ")";
|
|
5102
|
+
if (entry === null || typeof entry !== "object") {
|
|
5103
|
+
throw new Error(PREFIX2 + "setEffectsState: entry must be an object" + at);
|
|
5104
|
+
}
|
|
5105
|
+
const e = entry;
|
|
5106
|
+
if (e.kind === "native") {
|
|
5107
|
+
if (typeof e.type !== "string" || e.type.length === 0) {
|
|
5108
|
+
throw new Error(PREFIX2 + "setEffectsState: native entry requires a type string" + at);
|
|
5109
|
+
}
|
|
5110
|
+
if (e.params === null || typeof e.params !== "object") {
|
|
5111
|
+
throw new Error(PREFIX2 + "setEffectsState: native entry requires a params object" + at);
|
|
5112
|
+
}
|
|
5113
|
+
} else if (e.kind === "wam") {
|
|
5114
|
+
const hasUrl = typeof e.url === "string" && e.url.length > 0;
|
|
5115
|
+
const hasFaustDsp = typeof e.faustDsp === "string" && e.faustDsp.trim().length > 0;
|
|
5116
|
+
if (!hasUrl && !hasFaustDsp) {
|
|
5117
|
+
throw new Error(
|
|
5118
|
+
PREFIX2 + "setEffectsState: wam entry requires a url string or a faustDsp source string" + at
|
|
5119
|
+
);
|
|
5120
|
+
}
|
|
5121
|
+
if (e.faustName !== void 0 && typeof e.faustName !== "string") {
|
|
5122
|
+
throw new Error(PREFIX2 + "setEffectsState: faustName must be a string when provided" + at);
|
|
5123
|
+
}
|
|
5124
|
+
} else {
|
|
5125
|
+
throw new Error(PREFIX2 + 'setEffectsState: unknown entry kind "' + String(e.kind) + '"' + at);
|
|
5126
|
+
}
|
|
5127
|
+
if (typeof e.bypassed !== "boolean") {
|
|
5128
|
+
throw new Error(PREFIX2 + "setEffectsState: entry requires a boolean bypassed flag" + at);
|
|
5129
|
+
}
|
|
5130
|
+
});
|
|
5131
|
+
}
|
|
5132
|
+
function toWamParameterMap(params) {
|
|
5133
|
+
const map = {};
|
|
5134
|
+
for (const [id, value] of Object.entries(params)) {
|
|
5135
|
+
map[id] = { id, value, normalized: false };
|
|
5136
|
+
}
|
|
5137
|
+
return map;
|
|
5138
|
+
}
|
|
5139
|
+
|
|
5140
|
+
// src/interactions/export-audio.ts
|
|
5141
|
+
var PREFIX3 = "[waveform-playlist] ";
|
|
5142
|
+
async function exportAudioImpl(host, options = {}) {
|
|
5143
|
+
const sampleRate = options.sampleRate ?? host.effectiveSampleRate;
|
|
5144
|
+
const startTime = options.startTime ?? 0;
|
|
5145
|
+
const duration = options.duration ?? Math.max(0, host.duration - startTime);
|
|
5146
|
+
const channels = options.channels ?? 2;
|
|
5147
|
+
if (!Number.isFinite(sampleRate) || sampleRate <= 0) {
|
|
5148
|
+
throw new Error(PREFIX3 + "exportAudio: invalid sampleRate " + String(sampleRate));
|
|
5149
|
+
}
|
|
5150
|
+
if (!Number.isFinite(duration) || duration <= 0) {
|
|
5151
|
+
throw new Error(PREFIX3 + "exportAudio: nothing to render (duration " + String(duration) + ")");
|
|
5152
|
+
}
|
|
5153
|
+
const ctx = new OfflineAudioContext({
|
|
5154
|
+
numberOfChannels: channels,
|
|
5155
|
+
length: Math.round(duration * sampleRate),
|
|
5156
|
+
sampleRate
|
|
5157
|
+
});
|
|
5158
|
+
const cleanups = [];
|
|
5159
|
+
try {
|
|
5160
|
+
const masterChain = await buildOfflineChain(ctx, await host.getMasterEffectsState());
|
|
5161
|
+
cleanups.push(masterChain.dispose);
|
|
5162
|
+
masterChain.output.connect(ctx.destination);
|
|
5163
|
+
const anySoloed = host.tracks.some((t) => t.soloed);
|
|
5164
|
+
for (const track of host.tracks) {
|
|
5165
|
+
if (track.muted || anySoloed && !track.soloed) {
|
|
5166
|
+
continue;
|
|
5167
|
+
}
|
|
5168
|
+
const chain = await buildOfflineChain(ctx, await host.getTrackEffectsState(track.id));
|
|
5169
|
+
cleanups.push(chain.dispose);
|
|
5170
|
+
const volume = ctx.createGain();
|
|
5171
|
+
volume.gain.value = track.volume;
|
|
5172
|
+
const panner = ctx.createStereoPanner();
|
|
5173
|
+
panner.pan.value = track.pan;
|
|
5174
|
+
volume.connect(panner);
|
|
5175
|
+
panner.connect(chain.input);
|
|
5176
|
+
chain.output.connect(masterChain.input);
|
|
5177
|
+
for (const clip of track.clips) {
|
|
5178
|
+
scheduleClip(ctx, clip, startTime, duration, volume);
|
|
5179
|
+
}
|
|
5180
|
+
}
|
|
5181
|
+
return await ctx.startRendering();
|
|
5182
|
+
} finally {
|
|
5183
|
+
for (const dispose of cleanups) {
|
|
5184
|
+
try {
|
|
5185
|
+
dispose();
|
|
5186
|
+
} catch (err) {
|
|
5187
|
+
console.warn(PREFIX3 + "exportAudio: cleanup error: " + String(err));
|
|
5188
|
+
}
|
|
5189
|
+
}
|
|
5190
|
+
}
|
|
5191
|
+
}
|
|
5192
|
+
function scheduleClip(ctx, clip, windowStart, windowDuration, destination) {
|
|
5193
|
+
if (!clip.audioBuffer || clip.durationSamples <= 0) {
|
|
5194
|
+
return;
|
|
5195
|
+
}
|
|
5196
|
+
const clipRate = clip.sampleRate;
|
|
5197
|
+
const clipStart = clip.startSample / clipRate;
|
|
5198
|
+
const clipDuration = clip.durationSamples / clipRate;
|
|
5199
|
+
let offset = clip.offsetSamples / clipRate;
|
|
5200
|
+
let when = clipStart - windowStart;
|
|
5201
|
+
let remaining = clipDuration;
|
|
5202
|
+
if (when < 0) {
|
|
5203
|
+
offset += -when;
|
|
5204
|
+
remaining += when;
|
|
5205
|
+
when = 0;
|
|
5206
|
+
}
|
|
5207
|
+
remaining = Math.min(remaining, windowDuration - when);
|
|
5208
|
+
if (remaining <= 0) {
|
|
5209
|
+
return;
|
|
5210
|
+
}
|
|
5211
|
+
const source = ctx.createBufferSource();
|
|
5212
|
+
source.buffer = clip.audioBuffer;
|
|
5213
|
+
let out = source;
|
|
5214
|
+
if (clip.gain !== void 0 && clip.gain !== 1) {
|
|
5215
|
+
const gainNode = ctx.createGain();
|
|
5216
|
+
gainNode.gain.value = clip.gain;
|
|
5217
|
+
out.connect(gainNode);
|
|
5218
|
+
out = gainNode;
|
|
5219
|
+
}
|
|
5220
|
+
out.connect(destination);
|
|
5221
|
+
source.start(when, offset, remaining);
|
|
5222
|
+
}
|
|
5223
|
+
async function buildOfflineChain(ctx, entries) {
|
|
5224
|
+
const input = ctx.createGain();
|
|
5225
|
+
const output = ctx.createGain();
|
|
5226
|
+
const cleanups = [];
|
|
5227
|
+
const dispose = () => {
|
|
5228
|
+
for (const cleanup of cleanups) {
|
|
5229
|
+
try {
|
|
5230
|
+
cleanup();
|
|
5231
|
+
} catch (err) {
|
|
5232
|
+
console.warn(PREFIX3 + "exportAudio: chain cleanup error: " + String(err));
|
|
5233
|
+
}
|
|
5234
|
+
}
|
|
5235
|
+
};
|
|
5236
|
+
let previous = input;
|
|
5237
|
+
try {
|
|
5238
|
+
await wireEntries();
|
|
5239
|
+
} catch (err) {
|
|
5240
|
+
dispose();
|
|
5241
|
+
throw err;
|
|
5242
|
+
}
|
|
5243
|
+
previous.connect(output);
|
|
5244
|
+
return { input, output, dispose };
|
|
5245
|
+
async function wireEntries() {
|
|
5246
|
+
for (const entry of entries) {
|
|
5247
|
+
if (entry.kind === "native") {
|
|
5248
|
+
const created = createEffectInstance(entry.type, ctx, entry.params);
|
|
5249
|
+
if (entry.bypassed) {
|
|
5250
|
+
if (!created.wetParam) {
|
|
5251
|
+
created.instance.dispose?.();
|
|
5252
|
+
continue;
|
|
5253
|
+
}
|
|
5254
|
+
created.instance.applyParams({ [created.wetParam]: 0 });
|
|
5255
|
+
}
|
|
5256
|
+
if (created.instance.dispose) {
|
|
5257
|
+
cleanups.push(created.instance.dispose);
|
|
5258
|
+
}
|
|
5259
|
+
previous.connect(created.instance.input);
|
|
5260
|
+
previous = created.instance.output;
|
|
5261
|
+
continue;
|
|
5262
|
+
}
|
|
5263
|
+
if (entry.bypassed) {
|
|
5264
|
+
continue;
|
|
5265
|
+
}
|
|
5266
|
+
const wamModule = await loadWamModule("exportAudio() with WAM effects");
|
|
5267
|
+
const { hostGroupId } = await wamModule.ensureWamHost(ctx);
|
|
5268
|
+
let plugin;
|
|
5269
|
+
if (entry.faustDsp !== void 0) {
|
|
5270
|
+
const faustModule = await loadFaustModule("exportAudio() with Faust effects");
|
|
5271
|
+
const compiled = await faustModule.compileFaustToWam(entry.faustDsp, {
|
|
5272
|
+
name: entry.faustName
|
|
5273
|
+
});
|
|
5274
|
+
plugin = await wamModule.createWamInstanceFromFactory(
|
|
5275
|
+
compiled.factory,
|
|
5276
|
+
ctx,
|
|
5277
|
+
hostGroupId,
|
|
5278
|
+
{ initialState: entry.state, label: compiled.name }
|
|
5279
|
+
);
|
|
5280
|
+
} else {
|
|
5281
|
+
plugin = await wamModule.createWamInstance(entry.url ?? "", ctx, hostGroupId, {
|
|
5282
|
+
initialState: entry.state
|
|
5283
|
+
});
|
|
5284
|
+
}
|
|
5285
|
+
cleanups.push(() => plugin.destroy());
|
|
5286
|
+
previous.connect(plugin.audioNode);
|
|
5287
|
+
previous = plugin.audioNode;
|
|
5288
|
+
}
|
|
5289
|
+
}
|
|
5290
|
+
}
|
|
5291
|
+
|
|
4145
5292
|
// src/interactions/peaks-loader.ts
|
|
4146
5293
|
var import_waveform_data2 = __toESM(require("waveform-data"));
|
|
4147
5294
|
async function loadWaveformDataFromUrl(src) {
|
|
@@ -4333,6 +5480,8 @@ var DawEditorElement = class extends import_lit15.LitElement {
|
|
|
4333
5480
|
this._selectionEndTime = 0;
|
|
4334
5481
|
this._currentTime = 0;
|
|
4335
5482
|
this._externalAdapter = null;
|
|
5483
|
+
// --- Effects (master chain; per-track chains delegated from <daw-track>) ---
|
|
5484
|
+
this._effectsManager = null;
|
|
4336
5485
|
this._engine = null;
|
|
4337
5486
|
this._warnedMissingTicksToSeconds = false;
|
|
4338
5487
|
this._warnedMissingSecondsToTicks = false;
|
|
@@ -4690,11 +5839,122 @@ var DawEditorElement = class extends import_lit15.LitElement {
|
|
|
4690
5839
|
"[dawcore] adapter set after engine is built. The engine will continue using the previous adapter."
|
|
4691
5840
|
);
|
|
4692
5841
|
}
|
|
5842
|
+
if (this._effectsManager) {
|
|
5843
|
+
console.warn(
|
|
5844
|
+
"[dawcore] adapter replaced \u2014 existing effect chains were disposed. Re-add effects on the new adapter."
|
|
5845
|
+
);
|
|
5846
|
+
this._effectsManager.disposeAll();
|
|
5847
|
+
this._effectsManager = null;
|
|
5848
|
+
}
|
|
4693
5849
|
this._externalAdapter = value;
|
|
4694
5850
|
}
|
|
4695
5851
|
get adapter() {
|
|
4696
5852
|
return this._externalAdapter;
|
|
4697
5853
|
}
|
|
5854
|
+
get _effects() {
|
|
5855
|
+
if (!this._effectsManager) {
|
|
5856
|
+
this._effectsManager = new EffectsManager(() => this._externalAdapter, this);
|
|
5857
|
+
}
|
|
5858
|
+
return this._effectsManager;
|
|
5859
|
+
}
|
|
5860
|
+
/** Master effects chain — inserted between the master bus and the destination. */
|
|
5861
|
+
addEffect(type, params) {
|
|
5862
|
+
return this._effects.addMasterEffect(type, params);
|
|
5863
|
+
}
|
|
5864
|
+
removeEffect(effectId) {
|
|
5865
|
+
this._effects.masterOp("remove", effectId);
|
|
5866
|
+
}
|
|
5867
|
+
setEffectParams(effectId, params) {
|
|
5868
|
+
this._effects.masterOp("setParams", effectId, params);
|
|
5869
|
+
}
|
|
5870
|
+
setEffectBypassed(effectId, bypassed) {
|
|
5871
|
+
this._effects.masterOp("setBypassed", effectId, bypassed);
|
|
5872
|
+
}
|
|
5873
|
+
moveEffect(effectId, newIndex) {
|
|
5874
|
+
this._effects.masterOp("move", effectId, newIndex);
|
|
5875
|
+
}
|
|
5876
|
+
get effects() {
|
|
5877
|
+
return this._effectsManager?.masterEffects() ?? [];
|
|
5878
|
+
}
|
|
5879
|
+
/** Load a WAM plugin (via the optional @dawcore/wam peer) into the master chain. */
|
|
5880
|
+
addWamPlugin(url, initialState) {
|
|
5881
|
+
return this._effects.addMasterWamPlugin(url, initialState);
|
|
5882
|
+
}
|
|
5883
|
+
/**
|
|
5884
|
+
* Compile Faust DSP source in the browser (via the optional @dawcore/faust
|
|
5885
|
+
* peer) and add the resulting WAM to the master chain. Compile errors keep
|
|
5886
|
+
* their Faust line/column diagnostics and leave the chain untouched.
|
|
5887
|
+
*/
|
|
5888
|
+
addFaustEffect(dspCode, options) {
|
|
5889
|
+
return this._effects.addMasterFaustEffect(dspCode, options);
|
|
5890
|
+
}
|
|
5891
|
+
/**
|
|
5892
|
+
* Open (lazily creating) the GUI for a master-chain effect into a
|
|
5893
|
+
* consumer-provided container. WAM plugins mount their own GUI; plugins
|
|
5894
|
+
* without one — and native effects — get the generic parameter panel from
|
|
5895
|
+
* @dawcore/wam. The element is cached: closeEffectGui hides it without
|
|
5896
|
+
* interrupting audio, reopening remounts the same element.
|
|
5897
|
+
*/
|
|
5898
|
+
openEffectGui(effectId, container) {
|
|
5899
|
+
return this._effects.openMasterEffectGui(effectId, container);
|
|
5900
|
+
}
|
|
5901
|
+
/** Hide a master-chain effect's GUI (cached for reopen — never destroys). */
|
|
5902
|
+
closeEffectGui(effectId) {
|
|
5903
|
+
this._effects.closeEffectGui(effectId);
|
|
5904
|
+
}
|
|
5905
|
+
/** Snapshot the master chain in its persisted form (see dawcore README). */
|
|
5906
|
+
getEffectsState() {
|
|
5907
|
+
return this._effectsManager?.getMasterEffectsState() ?? Promise.resolve([]);
|
|
5908
|
+
}
|
|
5909
|
+
/** Replace the master chain with a persisted snapshot. */
|
|
5910
|
+
setEffectsState(entries) {
|
|
5911
|
+
return this._effects.setMasterEffectsState(entries);
|
|
5912
|
+
}
|
|
5913
|
+
/**
|
|
5914
|
+
* Render the session offline through all effect chains (per-track +
|
|
5915
|
+
* master), including WAM plugins (re-instantiated on the offline context
|
|
5916
|
+
* with their live state). Returns the rendered AudioBuffer.
|
|
5917
|
+
*/
|
|
5918
|
+
exportAudio(options) {
|
|
5919
|
+
return exportAudioImpl(
|
|
5920
|
+
{
|
|
5921
|
+
effectiveSampleRate: this.effectiveSampleRate,
|
|
5922
|
+
duration: this._duration,
|
|
5923
|
+
tracks: [...this._engineTracks.values()],
|
|
5924
|
+
getMasterEffectsState: () => this.getEffectsState(),
|
|
5925
|
+
getTrackEffectsState: (trackId) => this._trackGetEffectsState(trackId)
|
|
5926
|
+
},
|
|
5927
|
+
options
|
|
5928
|
+
);
|
|
5929
|
+
}
|
|
5930
|
+
/** Internal — <daw-track> effects API delegates here (dawcore-internal contract). */
|
|
5931
|
+
_trackAddEffect(trackId, target, type, params) {
|
|
5932
|
+
return this._effects.addTrackEffect(trackId, target, type, params);
|
|
5933
|
+
}
|
|
5934
|
+
_trackEffectOp(trackId, target, op, effectId, arg) {
|
|
5935
|
+
this._effects.trackOp(trackId, target, op, effectId, arg);
|
|
5936
|
+
}
|
|
5937
|
+
_trackEffects(trackId) {
|
|
5938
|
+
return this._effectsManager?.trackEffects(trackId) ?? [];
|
|
5939
|
+
}
|
|
5940
|
+
_trackAddWamPlugin(trackId, target, url, initialState) {
|
|
5941
|
+
return this._effects.addTrackWamPlugin(trackId, target, url, initialState);
|
|
5942
|
+
}
|
|
5943
|
+
_trackAddFaustEffect(trackId, target, dspCode, options) {
|
|
5944
|
+
return this._effects.addTrackFaustEffect(trackId, target, dspCode, options);
|
|
5945
|
+
}
|
|
5946
|
+
_trackOpenEffectGui(trackId, target, effectId, container) {
|
|
5947
|
+
return this._effects.openTrackEffectGui(trackId, target, effectId, container);
|
|
5948
|
+
}
|
|
5949
|
+
_trackCloseEffectGui(_trackId, effectId) {
|
|
5950
|
+
this._effects.closeEffectGui(effectId);
|
|
5951
|
+
}
|
|
5952
|
+
_trackGetEffectsState(trackId) {
|
|
5953
|
+
return this._effectsManager?.getTrackEffectsState(trackId) ?? Promise.resolve([]);
|
|
5954
|
+
}
|
|
5955
|
+
_trackSetEffectsState(trackId, target, entries) {
|
|
5956
|
+
return this._effects.setTrackEffectsState(trackId, target, entries);
|
|
5957
|
+
}
|
|
4698
5958
|
get audioContext() {
|
|
4699
5959
|
if (!this._externalAdapter) {
|
|
4700
5960
|
throw new Error(NO_ADAPTER_ERROR);
|
|
@@ -4866,6 +6126,8 @@ var DawEditorElement = class extends import_lit15.LitElement {
|
|
|
4866
6126
|
}
|
|
4867
6127
|
disconnectedCallback() {
|
|
4868
6128
|
super.disconnectedCallback();
|
|
6129
|
+
this._effectsManager?.disposeAll();
|
|
6130
|
+
this._effectsManager = null;
|
|
4869
6131
|
this.removeEventListener("daw-track-connected", this._onTrackConnected);
|
|
4870
6132
|
this.removeEventListener("daw-track-update", this._onTrackUpdate);
|
|
4871
6133
|
this.removeEventListener("daw-track-control", this._onTrackControl);
|
|
@@ -4941,6 +6203,7 @@ var DawEditorElement = class extends import_lit15.LitElement {
|
|
|
4941
6203
|
}
|
|
4942
6204
|
_onTrackRemoved(trackId) {
|
|
4943
6205
|
this._trackElements.delete(trackId);
|
|
6206
|
+
this._effectsManager?.disposeTrackChain(trackId);
|
|
4944
6207
|
const removedTrack = this._engineTracks.get(trackId);
|
|
4945
6208
|
if (removedTrack) {
|
|
4946
6209
|
const nextPeaks = new Map(this._peaksData);
|
|
@@ -5591,6 +6854,7 @@ var DawEditorElement = class extends import_lit15.LitElement {
|
|
|
5591
6854
|
nextTracks.set(track.id, track);
|
|
5592
6855
|
}
|
|
5593
6856
|
this._engineTracks = nextTracks;
|
|
6857
|
+
this._effectsManager?.rewireTrackChains();
|
|
5594
6858
|
const audioTracks = engineState.tracks.filter((t) => {
|
|
5595
6859
|
const desc = this._tracks.get(t.id);
|
|
5596
6860
|
return desc?.renderMode !== "piano-roll";
|
|
@@ -7067,7 +8331,10 @@ DawSpectrogramElement = __decorateClass([
|
|
|
7067
8331
|
DawWaveformElement,
|
|
7068
8332
|
RecordingController,
|
|
7069
8333
|
SpectrogramController,
|
|
8334
|
+
createEffectInstance,
|
|
8335
|
+
getEffectDefinitions,
|
|
7070
8336
|
isDomClip,
|
|
8337
|
+
registerEffect,
|
|
7071
8338
|
splitAtPlayhead
|
|
7072
8339
|
});
|
|
7073
8340
|
//# sourceMappingURL=index.js.map
|