@waveform-playlist/browser 15.0.0 → 15.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/tone.js CHANGED
@@ -68,12 +68,15 @@ var __async = (__this, __arguments, generator) => {
68
68
  var tone_exports = {};
69
69
  __export(tone_exports, {
70
70
  ExportWavButton: () => ExportWavButton,
71
+ WamEffectGui: () => WamEffectGui,
71
72
  createEffectChain: () => createEffectChain,
72
73
  createEffectInstance: () => createEffectInstance,
74
+ createWamEffectInstance: () => createWamEffectInstance,
73
75
  effectCategories: () => effectCategories,
74
76
  effectDefinitions: () => effectDefinitions,
75
77
  getEffectDefinition: () => getEffectDefinition,
76
78
  getEffectsByCategory: () => getEffectsByCategory,
79
+ loadWamModule: () => loadWamModule,
77
80
  useAudioTracks: () => useAudioTracks,
78
81
  useDynamicEffects: () => useDynamicEffects,
79
82
  useDynamicTracks: () => useDynamicTracks,
@@ -301,6 +304,7 @@ var useMasterAnalyser = (fftSize = 256) => {
301
304
 
302
305
  // src/hooks/useDynamicEffects.ts
303
306
  var import_react3 = require("react");
307
+ var import_playout = require("@waveform-playlist/playout");
304
308
 
305
309
  // src/effects/effectDefinitions.ts
306
310
  var effectDefinitions = [
@@ -903,8 +907,7 @@ function createEffectInstance(definition, initialParams) {
903
907
  effect.dispose();
904
908
  } catch (e) {
905
909
  console.warn(
906
- `[waveform-playlist] Error disposing effect "${definition.id}" (${instanceId}):`,
907
- e
910
+ `[waveform-playlist] Error disposing effect "${definition.id}" (${instanceId}): ${e instanceof Error ? e.message : String(e)}`
908
911
  );
909
912
  }
910
913
  },
@@ -949,8 +952,7 @@ function createEffectInstance(definition, initialParams) {
949
952
  effect.disconnect();
950
953
  } catch (e) {
951
954
  console.warn(
952
- `[waveform-playlist] Error disconnecting effect "${definition.id}" (${instanceId}):`,
953
- e
955
+ `[waveform-playlist] Error disconnecting effect "${definition.id}" (${instanceId}): ${e instanceof Error ? e.message : String(e)}`
954
956
  );
955
957
  }
956
958
  }
@@ -961,7 +963,7 @@ function createEffectChain(effects) {
961
963
  throw new Error("Cannot create effect chain with no effects");
962
964
  }
963
965
  for (let i = 0; i < effects.length - 1; i++) {
964
- effects[i].effect.connect(effects[i + 1].effect);
966
+ (0, import_tone2.connect)(effects[i].effect, effects[i + 1].effect);
965
967
  }
966
968
  return {
967
969
  input: effects[0].effect,
@@ -972,13 +974,105 @@ function createEffectChain(effects) {
972
974
  };
973
975
  }
974
976
 
975
- // src/hooks/useDynamicEffects.ts
977
+ // src/effects/loadWam.ts
978
+ function loadWamModule() {
979
+ return __async(this, null, function* () {
980
+ try {
981
+ return yield import("@dawcore/wam");
982
+ } catch (err) {
983
+ const detail = err instanceof Error ? err.message : String(err);
984
+ throw new Error(
985
+ "[waveform-playlist] WAM plugin support requires the optional '@dawcore/wam' package.\nInstall it with: npm install @dawcore/wam\nOriginal error: " + detail
986
+ );
987
+ }
988
+ });
989
+ }
990
+
991
+ // src/effects/wamEffectFactory.ts
976
992
  var import_tone3 = require("tone");
993
+ var wamInstanceCounter = 0;
994
+ function createWamEffectInstance(plugin) {
995
+ var _a, _b, _c;
996
+ const node = plugin.audioNode;
997
+ const instanceId = "wam_" + ++wamInstanceCounter;
998
+ return {
999
+ kind: "wam",
1000
+ plugin,
1001
+ url: plugin.url,
1002
+ effect: node,
1003
+ id: "wam:" + ((_c = (_b = (_a = plugin.descriptor) == null ? void 0 : _a.name) != null ? _b : plugin.url) != null ? _c : "plugin"),
1004
+ instanceId,
1005
+ dispose: () => {
1006
+ try {
1007
+ plugin.destroy();
1008
+ } catch (err) {
1009
+ console.warn(
1010
+ "[waveform-playlist] Error destroying WAM plugin: " + (err instanceof Error ? err.message : String(err))
1011
+ );
1012
+ }
1013
+ },
1014
+ setParameter: (name, value) => {
1015
+ var _a2;
1016
+ if (typeof value !== "number") return;
1017
+ const target = plugin.audioNode;
1018
+ void ((_a2 = target.setParameterValues) == null ? void 0 : _a2.call(target, { [name]: { id: name, value, normalized: false } }));
1019
+ },
1020
+ getParameter: () => void 0,
1021
+ // WAM params are async; read via the plugin GUI
1022
+ connect: (destination) => (0, import_tone3.connect)(node, destination),
1023
+ disconnect: () => (0, import_tone3.disconnect)(node)
1024
+ };
1025
+ }
1026
+
1027
+ // src/effects/offlineChain.ts
1028
+ var import_tone4 = require("tone");
1029
+ function buildOfflineChain(entries, getLivePlugin, rawContext) {
1030
+ return __async(this, null, function* () {
1031
+ const instances = [];
1032
+ const dispose = () => {
1033
+ instances.forEach((inst) => inst.dispose());
1034
+ };
1035
+ try {
1036
+ for (const entry of entries) {
1037
+ if (entry.kind === "native") {
1038
+ instances.push(createEffectInstance(entry.definition, entry.params));
1039
+ continue;
1040
+ }
1041
+ const livePlugin = getLivePlugin(entry.instanceId);
1042
+ if (!livePlugin) {
1043
+ throw new Error(
1044
+ '[waveform-playlist] WAV export: no live WAM plugin found for "' + entry.definition.name + '" (' + entry.instanceId + ") \u2014 cannot re-instantiate it offline."
1045
+ );
1046
+ }
1047
+ const wam = yield loadWamModule();
1048
+ const { hostGroupId } = yield wam.ensureWamHost(rawContext);
1049
+ const clone = yield wam.cloneInstanceInto(livePlugin, rawContext, hostGroupId);
1050
+ instances.push(createWamEffectInstance(clone));
1051
+ }
1052
+ } catch (err) {
1053
+ dispose();
1054
+ throw err;
1055
+ }
1056
+ return { instances, dispose };
1057
+ });
1058
+ }
1059
+ function connectOfflineChain(from, instances, to) {
1060
+ let current = from;
1061
+ for (const inst of instances) {
1062
+ (0, import_tone4.connect)(current, inst.effect);
1063
+ current = inst.effect;
1064
+ }
1065
+ (0, import_tone4.connect)(current, to);
1066
+ }
1067
+
1068
+ // src/hooks/useDynamicEffects.ts
1069
+ var import_tone5 = require("tone");
977
1070
  function useDynamicEffects(fftSize = 256) {
978
1071
  const [activeEffects, setActiveEffects] = (0, import_react3.useState)([]);
979
1072
  const activeEffectsRef = (0, import_react3.useRef)(activeEffects);
980
1073
  activeEffectsRef.current = activeEffects;
981
1074
  const effectInstancesRef = (0, import_react3.useRef)(/* @__PURE__ */ new Map());
1075
+ const isMountedRef = (0, import_react3.useRef)(true);
982
1076
  const analyserRef = (0, import_react3.useRef)(null);
983
1077
  const graphNodesRef = (0, import_react3.useRef)(null);
984
1078
  const rebuildChain = (0, import_react3.useCallback)((effects) => {
@@ -990,7 +1084,8 @@ function useDynamicEffects(fftSize = 256) {
990
1084
  } catch (e) {
991
1085
  console.warn("[waveform-playlist] Error disconnecting master effects chain:", e);
992
1086
  }
993
- const instances = effects.map((ae) => effectInstancesRef.current.get(ae.instanceId)).filter((inst) => inst !== void 0);
1087
+ const audible = effects.filter((ae) => !(ae.kind === "wam" && ae.bypassed));
1088
+ const instances = audible.map((ae) => effectInstancesRef.current.get(ae.instanceId)).filter((inst) => inst !== void 0);
994
1089
  if (instances.length === 0) {
995
1090
  masterGainNode.connect(analyserNode);
996
1091
  analyserNode.connect(destination);
@@ -1002,10 +1097,10 @@ function useDynamicEffects(fftSize = 256) {
1002
1097
  } catch (e) {
1003
1098
  console.warn(`[waveform-playlist] Error disconnecting effect "${inst.id}":`, e);
1004
1099
  }
1005
- currentNode.connect(inst.effect);
1100
+ (0, import_tone5.connect)(currentNode, inst.effect);
1006
1101
  currentNode = inst.effect;
1007
1102
  });
1008
- currentNode.connect(analyserNode);
1103
+ (0, import_tone5.connect)(currentNode, analyserNode);
1009
1104
  analyserNode.connect(destination);
1010
1105
  }
1011
1106
  }, []);
@@ -1024,12 +1119,68 @@ function useDynamicEffects(fftSize = 256) {
1024
1119
  const newActiveEffect = {
1025
1120
  instanceId: instance.instanceId,
1026
1121
  effectId: definition.id,
1122
+ kind: "native",
1027
1123
  definition,
1028
1124
  params,
1029
1125
  bypassed: false
1030
1126
  };
1031
1127
  setActiveEffects((prev) => [...prev, newActiveEffect]);
1032
1128
  }, []);
1129
+ const addWamEffect = (0, import_react3.useCallback)((url, initialState) => __async(null, null, function* () {
1130
+ var _a, _b;
1131
+ if (!(0, import_playout.isNativeGlobalContext)()) {
1132
+ throw new Error(
1133
+ "[waveform-playlist] WAM plugins require a native AudioContext. Call configureGlobalContext({ nativeAudioContext: true }) from @waveform-playlist/playout before any audio initialization."
1134
+ );
1135
+ }
1136
+ const wam = yield loadWamModule();
1137
+ const ctx = (0, import_playout.getGlobalAudioContext)();
1138
+ const { hostGroupId } = yield wam.ensureWamHost(ctx);
1139
+ const plugin = yield wam.createWamInstance(
1140
+ url,
1141
+ ctx,
1142
+ hostGroupId,
1143
+ initialState !== void 0 ? { initialState } : void 0
1144
+ );
1145
+ if (!isMountedRef.current) {
1146
+ try {
1147
+ plugin.destroy();
1148
+ } catch (err) {
1149
+ console.warn(
1150
+ "[waveform-playlist] Error destroying WAM plugin after unmount: " + (err instanceof Error ? err.message : String(err))
1151
+ );
1152
+ }
1153
+ throw new Error(
1154
+ "[waveform-playlist] addWamEffect aborted: hook unmounted before the plugin finished loading."
1155
+ );
1156
+ }
1157
+ const instance = createWamEffectInstance(plugin);
1158
+ effectInstancesRef.current.set(instance.instanceId, instance);
1159
+ const definition = {
1160
+ id: instance.id,
1161
+ name: (_b = (_a = plugin.descriptor) == null ? void 0 : _a.name) != null ? _b : url,
1162
+ category: "wam",
1163
+ description: "WAM plugin",
1164
+ parameters: []
1165
+ };
1166
+ setActiveEffects((prev) => [
1167
+ ...prev,
1168
+ {
1169
+ instanceId: instance.instanceId,
1170
+ effectId: instance.id,
1171
+ kind: "wam",
1172
+ url,
1173
+ definition,
1174
+ params: {},
1175
+ bypassed: false
1176
+ }
1177
+ ]);
1178
+ return instance.instanceId;
1179
+ }), []);
1180
+ const getWamPlugin = (0, import_react3.useCallback)((instanceId) => {
1181
+ const inst = effectInstancesRef.current.get(instanceId);
1182
+ return (inst == null ? void 0 : inst.kind) === "wam" ? inst.plugin : void 0;
1183
+ }, []);
1033
1184
  const removeEffect = (0, import_react3.useCallback)((instanceId) => {
1034
1185
  const instance = effectInstancesRef.current.get(instanceId);
1035
1186
  if (instance) {
@@ -1057,6 +1208,12 @@ function useDynamicEffects(fftSize = 256) {
1057
1208
  const effect = activeEffectsRef.current.find((e) => e.instanceId === instanceId);
1058
1209
  if (!effect) return;
1059
1210
  const newBypassed = !effect.bypassed;
1211
+ if (effect.kind === "wam") {
1212
+ setActiveEffects(
1213
+ (prev) => prev.map((e) => e.instanceId === instanceId ? __spreadProps(__spreadValues({}, e), { bypassed: newBypassed }) : e)
1214
+ );
1215
+ return;
1216
+ }
1060
1217
  const instance = effectInstancesRef.current.get(instanceId);
1061
1218
  if (instance) {
1062
1219
  const originalWet = (_a = effect.params.wet) != null ? _a : 1;
@@ -1084,7 +1241,7 @@ function useDynamicEffects(fftSize = 256) {
1084
1241
  }, [activeEffects, rebuildChain]);
1085
1242
  const masterEffects = (0, import_react3.useCallback)(
1086
1243
  (masterGainNode, destination, _isOffline) => {
1087
- const analyserNode = new import_tone3.Analyser("fft", fftSize);
1244
+ const analyserNode = new import_tone5.Analyser("fft", fftSize);
1088
1245
  analyserRef.current = analyserNode;
1089
1246
  graphNodesRef.current = {
1090
1247
  masterGainNode,
@@ -1092,17 +1249,18 @@ function useDynamicEffects(fftSize = 256) {
1092
1249
  analyserNode
1093
1250
  };
1094
1251
  const effects = activeEffectsRef.current;
1095
- const instances = effects.map((ae) => effectInstancesRef.current.get(ae.instanceId)).filter((inst) => inst !== void 0);
1252
+ const audible = effects.filter((ae) => !(ae.kind === "wam" && ae.bypassed));
1253
+ const instances = audible.map((ae) => effectInstancesRef.current.get(ae.instanceId)).filter((inst) => inst !== void 0);
1096
1254
  if (instances.length === 0) {
1097
1255
  masterGainNode.connect(analyserNode);
1098
1256
  analyserNode.connect(destination);
1099
1257
  } else {
1100
1258
  let currentNode = masterGainNode;
1101
1259
  instances.forEach((inst) => {
1102
- currentNode.connect(inst.effect);
1260
+ (0, import_tone5.connect)(currentNode, inst.effect);
1103
1261
  currentNode = inst.effect;
1104
1262
  });
1105
- currentNode.connect(analyserNode);
1263
+ (0, import_tone5.connect)(currentNode, analyserNode);
1106
1264
  analyserNode.connect(destination);
1107
1265
  }
1108
1266
  return function cleanup() {
@@ -1115,42 +1273,45 @@ function useDynamicEffects(fftSize = 256) {
1115
1273
  // Only fftSize - reads effects from ref
1116
1274
  );
1117
1275
  (0, import_react3.useEffect)(() => {
1276
+ isMountedRef.current = true;
1118
1277
  const effectInstances = effectInstancesRef.current;
1119
1278
  return () => {
1279
+ isMountedRef.current = false;
1120
1280
  effectInstances.forEach((inst) => inst.dispose());
1121
1281
  effectInstances.clear();
1122
1282
  };
1123
1283
  }, []);
1124
1284
  const createOfflineEffectsFunction = (0, import_react3.useCallback)(() => {
1125
- const nonBypassedEffects = activeEffects.filter((e) => !e.bypassed);
1126
- if (nonBypassedEffects.length === 0) {
1285
+ const nonBypassed = activeEffects.filter((e) => !e.bypassed);
1286
+ if (nonBypassed.length === 0) {
1127
1287
  return void 0;
1128
1288
  }
1129
- return (masterGainNode, destination, _isOffline) => {
1130
- const offlineInstances = [];
1131
- for (const activeEffect of nonBypassedEffects) {
1132
- const instance = createEffectInstance(activeEffect.definition, activeEffect.params);
1133
- offlineInstances.push(instance);
1134
- }
1135
- if (offlineInstances.length === 0) {
1136
- masterGainNode.connect(destination);
1137
- } else {
1138
- let currentNode = masterGainNode;
1139
- offlineInstances.forEach((inst) => {
1140
- currentNode.connect(inst.effect);
1141
- currentNode = inst.effect;
1142
- });
1143
- currentNode.connect(destination);
1289
+ const hasWam = nonBypassed.some((e) => e.kind === "wam");
1290
+ return (masterGainNode, destination, _isOffline) => __async(null, null, function* () {
1291
+ if (hasWam && !(0, import_playout.isNativeGlobalContext)()) {
1292
+ throw new Error(
1293
+ "[waveform-playlist] WAV export with WAM effects requires a native AudioContext. Call configureGlobalContext({ nativeAudioContext: true }) from @waveform-playlist/playout before any audio initialization."
1294
+ );
1144
1295
  }
1145
- return function cleanup() {
1146
- offlineInstances.forEach((inst) => inst.dispose());
1147
- };
1148
- };
1296
+ const rawContext = masterGainNode.context.rawContext;
1297
+ const { instances, dispose } = yield buildOfflineChain(
1298
+ nonBypassed,
1299
+ (instanceId) => {
1300
+ const inst = effectInstancesRef.current.get(instanceId);
1301
+ return (inst == null ? void 0 : inst.kind) === "wam" ? inst.plugin : void 0;
1302
+ },
1303
+ rawContext
1304
+ );
1305
+ connectOfflineChain(masterGainNode, instances, destination);
1306
+ return dispose;
1307
+ });
1149
1308
  }, [activeEffects]);
1150
1309
  return {
1151
1310
  activeEffects,
1152
1311
  availableEffects: effectDefinitions,
1153
1312
  addEffect,
1313
+ addWamEffect,
1314
+ getWamPlugin,
1154
1315
  removeEffect,
1155
1316
  updateParameter,
1156
1317
  toggleBypass,
@@ -1164,11 +1325,14 @@ function useDynamicEffects(fftSize = 256) {
1164
1325
 
1165
1326
  // src/hooks/useTrackDynamicEffects.ts
1166
1327
  var import_react4 = require("react");
1328
+ var import_playout2 = require("@waveform-playlist/playout");
1329
+ var import_tone6 = require("tone");
1167
1330
  function useTrackDynamicEffects() {
1168
1331
  const [trackEffectsState, setTrackEffectsState] = (0, import_react4.useState)(
1169
1332
  /* @__PURE__ */ new Map()
1170
1333
  );
1171
1334
  const trackEffectInstancesRef = (0, import_react4.useRef)(/* @__PURE__ */ new Map());
1335
+ const isMountedRef = (0, import_react4.useRef)(true);
1172
1336
  const trackGraphNodesRef = (0, import_react4.useRef)(/* @__PURE__ */ new Map());
1173
1337
  const rebuildTrackChain = (0, import_react4.useCallback)((trackId, trackEffects) => {
1174
1338
  const nodes = trackGraphNodesRef.current.get(trackId);
@@ -1180,7 +1344,8 @@ function useTrackDynamicEffects() {
1180
1344
  } catch (e) {
1181
1345
  console.warn(`[waveform-playlist] Error disconnecting track "${trackId}" effect chain:`, e);
1182
1346
  }
1183
- const instances = trackEffects.map((ae) => instancesMap == null ? void 0 : instancesMap.get(ae.instanceId)).filter((inst) => inst !== void 0);
1347
+ const audible = trackEffects.filter((ae) => !(ae.kind === "wam" && ae.bypassed));
1348
+ const instances = audible.map((ae) => instancesMap == null ? void 0 : instancesMap.get(ae.instanceId)).filter((inst) => inst !== void 0);
1184
1349
  if (instances.length === 0) {
1185
1350
  graphEnd.connect(masterGainNode);
1186
1351
  } else {
@@ -1194,10 +1359,10 @@ function useTrackDynamicEffects() {
1194
1359
  e
1195
1360
  );
1196
1361
  }
1197
- currentNode.connect(inst.effect);
1362
+ (0, import_tone6.connect)(currentNode, inst.effect);
1198
1363
  currentNode = inst.effect;
1199
1364
  });
1200
- currentNode.connect(masterGainNode);
1365
+ (0, import_tone6.connect)(currentNode, masterGainNode);
1201
1366
  }
1202
1367
  }, []);
1203
1368
  const addEffectToTrack = (0, import_react4.useCallback)((trackId, effectId) => {
@@ -1218,6 +1383,7 @@ function useTrackDynamicEffects() {
1218
1383
  const newActiveEffect = {
1219
1384
  instanceId: instance.instanceId,
1220
1385
  effectId: definition.id,
1386
+ kind: "native",
1221
1387
  definition,
1222
1388
  params,
1223
1389
  bypassed: false
@@ -1229,6 +1395,76 @@ function useTrackDynamicEffects() {
1229
1395
  return newState;
1230
1396
  });
1231
1397
  }, []);
1398
+ const addWamEffectToTrack = (0, import_react4.useCallback)(
1399
+ (trackId, url, initialState) => __async(null, null, function* () {
1400
+ var _a, _b;
1401
+ if (!(0, import_playout2.isNativeGlobalContext)()) {
1402
+ throw new Error(
1403
+ "[waveform-playlist] WAM plugins require a native AudioContext. Call configureGlobalContext({ nativeAudioContext: true }) from @waveform-playlist/playout before any audio initialization."
1404
+ );
1405
+ }
1406
+ const wam = yield loadWamModule();
1407
+ const ctx = (0, import_playout2.getGlobalAudioContext)();
1408
+ const { hostGroupId } = yield wam.ensureWamHost(ctx);
1409
+ const plugin = yield wam.createWamInstance(
1410
+ url,
1411
+ ctx,
1412
+ hostGroupId,
1413
+ initialState !== void 0 ? { initialState } : void 0
1414
+ );
1415
+ if (!isMountedRef.current) {
1416
+ try {
1417
+ plugin.destroy();
1418
+ } catch (err) {
1419
+ console.warn(
1420
+ "[waveform-playlist] Error destroying WAM plugin after unmount: " + (err instanceof Error ? err.message : String(err))
1421
+ );
1422
+ }
1423
+ throw new Error(
1424
+ "[waveform-playlist] addWamEffectToTrack aborted: hook unmounted before the plugin finished loading."
1425
+ );
1426
+ }
1427
+ const instance = createWamEffectInstance(plugin);
1428
+ if (!trackEffectInstancesRef.current.has(trackId)) {
1429
+ trackEffectInstancesRef.current.set(trackId, /* @__PURE__ */ new Map());
1430
+ }
1431
+ trackEffectInstancesRef.current.get(trackId).set(instance.instanceId, instance);
1432
+ const definition = {
1433
+ id: instance.id,
1434
+ name: (_b = (_a = plugin.descriptor) == null ? void 0 : _a.name) != null ? _b : url,
1435
+ category: "wam",
1436
+ description: "WAM plugin",
1437
+ parameters: []
1438
+ };
1439
+ setTrackEffectsState((prev) => {
1440
+ const newState = new Map(prev);
1441
+ const existing = newState.get(trackId) || [];
1442
+ newState.set(trackId, [
1443
+ ...existing,
1444
+ {
1445
+ instanceId: instance.instanceId,
1446
+ effectId: instance.id,
1447
+ kind: "wam",
1448
+ url,
1449
+ definition,
1450
+ params: {},
1451
+ bypassed: false
1452
+ }
1453
+ ]);
1454
+ return newState;
1455
+ });
1456
+ return instance.instanceId;
1457
+ }),
1458
+ []
1459
+ );
1460
+ const getTrackWamPlugin = (0, import_react4.useCallback)(
1461
+ (trackId, instanceId) => {
1462
+ const instancesMap = trackEffectInstancesRef.current.get(trackId);
1463
+ const inst = instancesMap == null ? void 0 : instancesMap.get(instanceId);
1464
+ return (inst == null ? void 0 : inst.kind) === "wam" ? inst.plugin : void 0;
1465
+ },
1466
+ []
1467
+ );
1232
1468
  const removeEffectFromTrack = (0, import_react4.useCallback)((trackId, instanceId) => {
1233
1469
  const instancesMap = trackEffectInstancesRef.current.get(trackId);
1234
1470
  const instance = instancesMap == null ? void 0 : instancesMap.get(instanceId);
@@ -1273,6 +1509,18 @@ function useTrackDynamicEffects() {
1273
1509
  const effect = trackEffects.find((e) => e.instanceId === instanceId);
1274
1510
  if (!effect) return;
1275
1511
  const newBypassed = !effect.bypassed;
1512
+ if (effect.kind === "wam") {
1513
+ setTrackEffectsState((prev) => {
1514
+ const newState = new Map(prev);
1515
+ const existing = newState.get(trackId) || [];
1516
+ newState.set(
1517
+ trackId,
1518
+ existing.map((e) => e.instanceId === instanceId ? __spreadProps(__spreadValues({}, e), { bypassed: newBypassed }) : e)
1519
+ );
1520
+ return newState;
1521
+ });
1522
+ return;
1523
+ }
1276
1524
  const instancesMap = trackEffectInstancesRef.current.get(trackId);
1277
1525
  const instance = instancesMap == null ? void 0 : instancesMap.get(instanceId);
1278
1526
  if (instance) {
@@ -1312,16 +1560,17 @@ function useTrackDynamicEffects() {
1312
1560
  });
1313
1561
  const trackEffects = trackEffectsStateRef.current.get(trackId) || [];
1314
1562
  const instancesMap = trackEffectInstancesRef.current.get(trackId);
1315
- const instances = trackEffects.map((ae) => instancesMap == null ? void 0 : instancesMap.get(ae.instanceId)).filter((inst) => inst !== void 0);
1563
+ const audible = trackEffects.filter((ae) => !(ae.kind === "wam" && ae.bypassed));
1564
+ const instances = audible.map((ae) => instancesMap == null ? void 0 : instancesMap.get(ae.instanceId)).filter((inst) => inst !== void 0);
1316
1565
  if (instances.length === 0) {
1317
1566
  graphEnd.connect(masterGainNode);
1318
1567
  } else {
1319
1568
  let currentNode = graphEnd;
1320
1569
  instances.forEach((inst) => {
1321
- currentNode.connect(inst.effect);
1570
+ (0, import_tone6.connect)(currentNode, inst.effect);
1322
1571
  currentNode = inst.effect;
1323
1572
  });
1324
- currentNode.connect(masterGainNode);
1573
+ (0, import_tone6.connect)(currentNode, masterGainNode);
1325
1574
  }
1326
1575
  return function cleanup() {
1327
1576
  trackGraphNodesRef.current.delete(trackId);
@@ -1337,8 +1586,10 @@ function useTrackDynamicEffects() {
1337
1586
  });
1338
1587
  }, [trackEffectsState, rebuildTrackChain]);
1339
1588
  (0, import_react4.useEffect)(() => {
1589
+ isMountedRef.current = true;
1340
1590
  const trackEffectInstances = trackEffectInstancesRef.current;
1341
1591
  return () => {
1592
+ isMountedRef.current = false;
1342
1593
  trackEffectInstances.forEach((instancesMap) => {
1343
1594
  instancesMap.forEach((inst) => inst.dispose());
1344
1595
  instancesMap.clear();
@@ -1349,36 +1600,38 @@ function useTrackDynamicEffects() {
1349
1600
  const createOfflineTrackEffectsFunction = (0, import_react4.useCallback)(
1350
1601
  (trackId) => {
1351
1602
  const trackEffects = trackEffectsState.get(trackId) || [];
1352
- const nonBypassedEffects = trackEffects.filter((e) => !e.bypassed);
1353
- if (nonBypassedEffects.length === 0) {
1603
+ const nonBypassed = trackEffects.filter((e) => !e.bypassed);
1604
+ if (nonBypassed.length === 0) {
1354
1605
  return void 0;
1355
1606
  }
1356
- return (graphEnd, masterGainNode, _isOffline) => {
1357
- const offlineInstances = [];
1358
- for (const activeEffect of nonBypassedEffects) {
1359
- const instance = createEffectInstance(activeEffect.definition, activeEffect.params);
1360
- offlineInstances.push(instance);
1361
- }
1362
- if (offlineInstances.length === 0) {
1363
- graphEnd.connect(masterGainNode);
1364
- } else {
1365
- let currentNode = graphEnd;
1366
- offlineInstances.forEach((inst) => {
1367
- currentNode.connect(inst.effect);
1368
- currentNode = inst.effect;
1369
- });
1370
- currentNode.connect(masterGainNode);
1607
+ const hasWam = nonBypassed.some((e) => e.kind === "wam");
1608
+ return (graphEnd, masterGainNode, _isOffline) => __async(null, null, function* () {
1609
+ if (hasWam && !(0, import_playout2.isNativeGlobalContext)()) {
1610
+ throw new Error(
1611
+ "[waveform-playlist] WAV export with WAM effects requires a native AudioContext. Call configureGlobalContext({ nativeAudioContext: true }) from @waveform-playlist/playout before any audio initialization."
1612
+ );
1371
1613
  }
1372
- return function cleanup() {
1373
- offlineInstances.forEach((inst) => inst.dispose());
1374
- };
1375
- };
1614
+ const rawContext = graphEnd.context.rawContext;
1615
+ const { instances, dispose } = yield buildOfflineChain(
1616
+ nonBypassed,
1617
+ (instanceId) => {
1618
+ var _a;
1619
+ const inst = (_a = trackEffectInstancesRef.current.get(trackId)) == null ? void 0 : _a.get(instanceId);
1620
+ return (inst == null ? void 0 : inst.kind) === "wam" ? inst.plugin : void 0;
1621
+ },
1622
+ rawContext
1623
+ );
1624
+ connectOfflineChain(graphEnd, instances, masterGainNode);
1625
+ return dispose;
1626
+ });
1376
1627
  },
1377
1628
  [trackEffectsState]
1378
1629
  );
1379
1630
  return {
1380
1631
  trackEffectsState,
1381
1632
  addEffectToTrack,
1633
+ addWamEffectToTrack,
1634
+ getTrackWamPlugin,
1382
1635
  removeEffectFromTrack,
1383
1636
  updateTrackEffectParameter,
1384
1637
  toggleBypass,
@@ -1392,7 +1645,45 @@ function useTrackDynamicEffects() {
1392
1645
  // src/hooks/useExportWav.ts
1393
1646
  var import_react5 = require("react");
1394
1647
  var import_core2 = require("@waveform-playlist/core");
1395
- var import_playout = require("@waveform-playlist/playout");
1648
+ var import_playout4 = require("@waveform-playlist/playout");
1649
+
1650
+ // src/utils/renderToneOffline.ts
1651
+ var import_tone7 = require("tone");
1652
+ var import_playout3 = require("@waveform-playlist/playout");
1653
+ var renderQueue = Promise.resolve();
1654
+ function renderToneOffline(build, duration, channels, sampleRate) {
1655
+ return __async(this, null, function* () {
1656
+ const run = renderQueue.then(
1657
+ () => renderToneOfflineSerial(build, duration, channels, sampleRate)
1658
+ );
1659
+ renderQueue = run.catch(() => void 0);
1660
+ return run;
1661
+ });
1662
+ }
1663
+ function renderToneOfflineSerial(build, duration, channels, sampleRate) {
1664
+ return __async(this, null, function* () {
1665
+ const offlineContext = (0, import_playout3.isNativeGlobalContext)() ? new import_tone7.OfflineContext(
1666
+ new OfflineAudioContext({
1667
+ numberOfChannels: channels,
1668
+ length: Math.round(duration * sampleRate),
1669
+ sampleRate
1670
+ })
1671
+ ) : new import_tone7.OfflineContext(channels, duration, sampleRate);
1672
+ const previousContext = (0, import_tone7.getContext)();
1673
+ (0, import_tone7.setContext)(offlineContext);
1674
+ try {
1675
+ yield build(offlineContext);
1676
+ } finally {
1677
+ (0, import_tone7.setContext)(previousContext);
1678
+ }
1679
+ const toneBuffer = yield offlineContext.render();
1680
+ const audioBuffer = toneBuffer.get();
1681
+ if (!audioBuffer) {
1682
+ throw new Error("Offline rendering produced no audio buffer");
1683
+ }
1684
+ return audioBuffer;
1685
+ });
1686
+ }
1396
1687
 
1397
1688
  // src/utils/wavEncoder.ts
1398
1689
  function encodeWav(audioBuffer, options = {}) {
@@ -1491,7 +1782,7 @@ function useExportWav() {
1491
1782
  if (mode === "individual" && (trackIndex === void 0 || trackIndex < 0 || trackIndex >= tracks.length)) {
1492
1783
  throw new Error("Invalid track index for individual export");
1493
1784
  }
1494
- const sampleRate = (0, import_playout.getGlobalAudioContext)().sampleRate;
1785
+ const sampleRate = (0, import_playout4.getGlobalAudioContext)().sampleRate;
1495
1786
  let totalDurationSamples = 0;
1496
1787
  for (const track of tracks) {
1497
1788
  for (const clip of track.clips) {
@@ -1548,7 +1839,7 @@ function useExportWav() {
1548
1839
  }
1549
1840
  function renderOffline(tracksToRender, hasSolo, duration, sampleRate, applyEffects, effectsFunction, createOfflineTrackEffects, onProgress) {
1550
1841
  return __async(this, null, function* () {
1551
- const { Offline, Volume: Volume2, Gain, Panner, Player, ToneAudioBuffer } = yield import("tone");
1842
+ const { Volume: Volume2, Gain: Gain2, Panner, Player, ToneAudioBuffer } = yield import("tone");
1552
1843
  onProgress(0.1);
1553
1844
  const audibleTracks = tracksToRender.filter(({ state }) => {
1554
1845
  if (state.muted && !state.soloed) return false;
@@ -1559,23 +1850,25 @@ function renderOffline(tracksToRender, hasSolo, duration, sampleRate, applyEffec
1559
1850
  (max, { track }) => Math.max(max, (0, import_core2.trackChannelCount)(track)),
1560
1851
  1
1561
1852
  );
1562
- let buffer;
1853
+ const cleanups = [];
1563
1854
  try {
1564
- buffer = yield Offline(
1565
- (_0) => __async(null, [_0], function* ({ transport, destination }) {
1855
+ const audioBuffer = yield renderToneOffline(
1856
+ (context) => __async(null, null, function* () {
1566
1857
  const masterVolume = new Volume2(0);
1567
1858
  if (effectsFunction && applyEffects) {
1568
- effectsFunction(masterVolume, destination, true);
1859
+ const cleanup = yield effectsFunction(masterVolume, context.destination, true);
1860
+ if (cleanup) cleanups.push(cleanup);
1569
1861
  } else {
1570
- masterVolume.connect(destination);
1862
+ masterVolume.connect(context.destination);
1571
1863
  }
1572
1864
  for (const { track, state } of audibleTracks) {
1573
1865
  const trackVolume = new Volume2((0, import_core2.gainToDb)(state.volume));
1574
1866
  const trackPan = new Panner({ pan: state.pan, channelCount: (0, import_core2.trackChannelCount)(track) });
1575
- const trackMute = new Gain(state.muted ? 0 : 1);
1867
+ const trackMute = new Gain2(state.muted ? 0 : 1);
1576
1868
  const trackEffects = createOfflineTrackEffects == null ? void 0 : createOfflineTrackEffects(track.id);
1577
1869
  if (trackEffects && applyEffects) {
1578
- trackEffects(trackMute, masterVolume, true);
1870
+ const cleanup = yield trackEffects(trackMute, masterVolume, true);
1871
+ if (cleanup) cleanups.push(cleanup);
1579
1872
  } else {
1580
1873
  trackMute.connect(masterVolume);
1581
1874
  }
@@ -1583,7 +1876,7 @@ function renderOffline(tracksToRender, hasSolo, duration, sampleRate, applyEffec
1583
1876
  trackVolume.connect(trackPan);
1584
1877
  for (const clip of track.clips) {
1585
1878
  const {
1586
- audioBuffer,
1879
+ audioBuffer: clipBuffer,
1587
1880
  startSample,
1588
1881
  durationSamples,
1589
1882
  offsetSamples,
@@ -1591,7 +1884,7 @@ function renderOffline(tracksToRender, hasSolo, duration, sampleRate, applyEffec
1591
1884
  fadeIn,
1592
1885
  fadeOut
1593
1886
  } = clip;
1594
- if (!audioBuffer) {
1887
+ if (!clipBuffer) {
1595
1888
  console.warn(
1596
1889
  '[waveform-playlist] Skipping clip "' + (clip.name || clip.id) + '" - no audioBuffer for export'
1597
1890
  );
@@ -1600,13 +1893,13 @@ function renderOffline(tracksToRender, hasSolo, duration, sampleRate, applyEffec
1600
1893
  const startTime = startSample / sampleRate;
1601
1894
  const clipDuration = durationSamples / sampleRate;
1602
1895
  const offset = offsetSamples / sampleRate;
1603
- const toneBuffer = new ToneAudioBuffer(audioBuffer);
1896
+ const toneBuffer = new ToneAudioBuffer(clipBuffer);
1604
1897
  const player = new Player(toneBuffer);
1605
- const fadeGain = new Gain(clipGain);
1898
+ const fadeGain = new Gain2(clipGain);
1606
1899
  player.connect(fadeGain);
1607
1900
  fadeGain.connect(trackVolume);
1608
1901
  if (applyEffects) {
1609
- const audioParam = (0, import_playout.getUnderlyingAudioParam)(fadeGain.gain);
1902
+ const audioParam = (0, import_playout4.getUnderlyingAudioParam)(fadeGain.gain);
1610
1903
  if (audioParam) {
1611
1904
  applyClipFades(audioParam, clipGain, startTime, clipDuration, fadeIn, fadeOut);
1612
1905
  } else if (fadeIn || fadeOut) {
@@ -1618,25 +1911,29 @@ function renderOffline(tracksToRender, hasSolo, duration, sampleRate, applyEffec
1618
1911
  player.start(startTime, offset, clipDuration);
1619
1912
  }
1620
1913
  }
1621
- transport.start(0);
1914
+ context.transport.start(0);
1622
1915
  }),
1623
1916
  duration,
1624
1917
  outputChannels,
1625
1918
  sampleRate
1626
1919
  );
1920
+ return audioBuffer;
1627
1921
  } catch (err) {
1628
1922
  if (err instanceof Error) {
1629
1923
  throw err;
1630
- } else {
1631
- throw new Error("Tone.Offline rendering failed: " + String(err));
1924
+ }
1925
+ throw new Error("Offline rendering failed: " + String(err));
1926
+ } finally {
1927
+ for (const cleanup of cleanups) {
1928
+ try {
1929
+ cleanup();
1930
+ } catch (err) {
1931
+ console.warn(
1932
+ "[waveform-playlist] Export cleanup error: " + (err instanceof Error ? err.message : String(err))
1933
+ );
1934
+ }
1632
1935
  }
1633
1936
  }
1634
- onProgress(0.9);
1635
- const result = buffer.get();
1636
- if (!result) {
1637
- throw new Error("Offline rendering produced no audio buffer");
1638
- }
1639
- return result;
1640
1937
  });
1641
1938
  }
1642
1939
  function applyClipFades(gainParam, clipGain, startTime, clipDuration, fadeIn, fadeOut) {
@@ -1660,7 +1957,7 @@ function applyClipFades(gainParam, clipGain, startTime, clipDuration, fadeIn, fa
1660
1957
  // src/hooks/useDynamicTracks.ts
1661
1958
  var import_react6 = require("react");
1662
1959
  var import_core3 = require("@waveform-playlist/core");
1663
- var import_playout2 = require("@waveform-playlist/playout");
1960
+ var import_playout5 = require("@waveform-playlist/playout");
1664
1961
  function getSourceName(source) {
1665
1962
  var _a, _b, _c, _d, _e;
1666
1963
  if (source instanceof File) {
@@ -1711,7 +2008,7 @@ function useDynamicTracks() {
1711
2008
  }, []);
1712
2009
  const addTracks = (0, import_react6.useCallback)((sources) => {
1713
2010
  if (sources.length === 0) return;
1714
- const audioContext = (0, import_playout2.getGlobalAudioContext)();
2011
+ const audioContext = (0, import_playout5.getGlobalAudioContext)();
1715
2012
  const placeholders = sources.map((source) => ({
1716
2013
  track: (0, import_core3.createTrack)({ name: `${getSourceName(source)} (loading...)`, clips: [] }),
1717
2014
  source
@@ -1782,7 +2079,7 @@ function useDynamicTracks() {
1782
2079
 
1783
2080
  // src/hooks/useOutputMeter.ts
1784
2081
  var import_react7 = require("react");
1785
- var import_playout3 = require("@waveform-playlist/playout");
2082
+ var import_playout6 = require("@waveform-playlist/playout");
1786
2083
  var import_core4 = require("@waveform-playlist/core");
1787
2084
  var import_worklets = require("@waveform-playlist/worklets");
1788
2085
  var PEAK_DECAY = 0.98;
@@ -1810,7 +2107,7 @@ function useOutputMeter(options = {}) {
1810
2107
  (0, import_react7.useEffect)(() => {
1811
2108
  let isMounted = true;
1812
2109
  const setup = () => __async(null, null, function* () {
1813
- const context = (0, import_playout3.getGlobalContext)();
2110
+ const context = (0, import_playout6.getGlobalContext)();
1814
2111
  const rawCtx = context.rawContext;
1815
2112
  yield rawCtx.audioWorklet.addModule(import_worklets.meterProcessorUrl);
1816
2113
  if (!isMounted) return;
@@ -1859,7 +2156,7 @@ function useOutputMeter(options = {}) {
1859
2156
  isMounted = false;
1860
2157
  if (workletNodeRef.current) {
1861
2158
  try {
1862
- const context = (0, import_playout3.getGlobalContext)();
2159
+ const context = (0, import_playout6.getGlobalContext)();
1863
2160
  context.destination.chain();
1864
2161
  } catch (err) {
1865
2162
  console.warn("[waveform-playlist] Failed to restore destination chain:", String(err));
@@ -1943,15 +2240,78 @@ var ExportWavButton = ({
1943
2240
  }
1944
2241
  );
1945
2242
  };
2243
+
2244
+ // src/components/WamEffectGui.tsx
2245
+ var import_react9 = require("react");
2246
+ var import_jsx_runtime3 = require("react/jsx-runtime");
2247
+ var WamEffectGui = ({ plugin, className }) => {
2248
+ const hostRef = (0, import_react9.useRef)(null);
2249
+ (0, import_react9.useEffect)(() => {
2250
+ const host = hostRef.current;
2251
+ if (!plugin || !host) return;
2252
+ let cancelled = false;
2253
+ let gui = null;
2254
+ (() => __async(null, null, function* () {
2255
+ var _a;
2256
+ try {
2257
+ if (plugin.createGui) {
2258
+ gui = yield plugin.createGui();
2259
+ } else {
2260
+ const wam = yield loadWamModule();
2261
+ gui = yield wam.createWamParameterPanel(
2262
+ plugin.audioNode
2263
+ );
2264
+ }
2265
+ if (cancelled) {
2266
+ if (gui) {
2267
+ try {
2268
+ (_a = plugin.destroyGui) == null ? void 0 : _a.call(plugin, gui);
2269
+ } catch (destroyErr) {
2270
+ console.warn(
2271
+ "[waveform-playlist] Error destroying WAM GUI: " + (destroyErr instanceof Error ? destroyErr.message : String(destroyErr))
2272
+ );
2273
+ }
2274
+ }
2275
+ gui = null;
2276
+ return;
2277
+ }
2278
+ host.innerHTML = "";
2279
+ host.appendChild(gui);
2280
+ } catch (err) {
2281
+ console.warn(
2282
+ "[waveform-playlist] Failed to create WAM GUI: " + (err instanceof Error ? err.message : String(err))
2283
+ );
2284
+ }
2285
+ }))();
2286
+ return () => {
2287
+ var _a;
2288
+ cancelled = true;
2289
+ if (gui) {
2290
+ gui.remove();
2291
+ try {
2292
+ (_a = plugin.destroyGui) == null ? void 0 : _a.call(plugin, gui);
2293
+ } catch (destroyErr) {
2294
+ console.warn(
2295
+ "[waveform-playlist] Error destroying WAM GUI: " + (destroyErr instanceof Error ? destroyErr.message : String(destroyErr))
2296
+ );
2297
+ }
2298
+ }
2299
+ };
2300
+ }, [plugin]);
2301
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { ref: hostRef, className: className != null ? className : "wam-effect-gui" });
2302
+ };
1946
2303
  // Annotate the CommonJS export names for ESM import in node:
1947
2304
  0 && (module.exports = {
1948
2305
  ExportWavButton,
2306
+ WamEffectGui,
1949
2307
  createEffectChain,
1950
2308
  createEffectInstance,
2309
+ createWamEffectInstance,
1951
2310
  effectCategories,
1952
2311
  effectDefinitions,
1953
2312
  getEffectDefinition,
1954
2313
  getEffectsByCategory,
2314
+ loadWamModule,
1955
2315
  useAudioTracks,
1956
2316
  useDynamicEffects,
1957
2317
  useDynamicTracks,