@waveform-playlist/browser 15.0.0 → 15.1.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.mjs CHANGED
@@ -258,6 +258,10 @@ var useMasterAnalyser = (fftSize = 256) => {
258
258
 
259
259
  // src/hooks/useDynamicEffects.ts
260
260
  import { useState as useState2, useCallback as useCallback2, useRef as useRef3, useEffect as useEffect2 } from "react";
261
+ import {
262
+ isNativeGlobalContext,
263
+ getGlobalAudioContext
264
+ } from "@waveform-playlist/playout";
261
265
 
262
266
  // src/effects/effectDefinitions.ts
263
267
  var effectDefinitions = [
@@ -826,7 +830,8 @@ import {
826
830
  Compressor,
827
831
  Limiter,
828
832
  Gate,
829
- StereoWidener
833
+ StereoWidener,
834
+ connect
830
835
  } from "tone";
831
836
  function asEffectConstructor(ctor) {
832
837
  return ctor;
@@ -881,8 +886,7 @@ function createEffectInstance(definition, initialParams) {
881
886
  effect.dispose();
882
887
  } catch (e) {
883
888
  console.warn(
884
- `[waveform-playlist] Error disposing effect "${definition.id}" (${instanceId}):`,
885
- e
889
+ `[waveform-playlist] Error disposing effect "${definition.id}" (${instanceId}): ${e instanceof Error ? e.message : String(e)}`
886
890
  );
887
891
  }
888
892
  },
@@ -927,8 +931,7 @@ function createEffectInstance(definition, initialParams) {
927
931
  effect.disconnect();
928
932
  } catch (e) {
929
933
  console.warn(
930
- `[waveform-playlist] Error disconnecting effect "${definition.id}" (${instanceId}):`,
931
- e
934
+ `[waveform-playlist] Error disconnecting effect "${definition.id}" (${instanceId}): ${e instanceof Error ? e.message : String(e)}`
932
935
  );
933
936
  }
934
937
  }
@@ -939,7 +942,7 @@ function createEffectChain(effects) {
939
942
  throw new Error("Cannot create effect chain with no effects");
940
943
  }
941
944
  for (let i = 0; i < effects.length - 1; i++) {
942
- effects[i].effect.connect(effects[i + 1].effect);
945
+ connect(effects[i].effect, effects[i + 1].effect);
943
946
  }
944
947
  return {
945
948
  input: effects[0].effect,
@@ -950,13 +953,105 @@ function createEffectChain(effects) {
950
953
  };
951
954
  }
952
955
 
956
+ // src/effects/loadWam.ts
957
+ function loadWamModule() {
958
+ return __async(this, null, function* () {
959
+ try {
960
+ return yield import("@dawcore/wam");
961
+ } catch (err) {
962
+ const detail = err instanceof Error ? err.message : String(err);
963
+ throw new Error(
964
+ "[waveform-playlist] WAM plugin support requires the optional '@dawcore/wam' package.\nInstall it with: npm install @dawcore/wam\nOriginal error: " + detail
965
+ );
966
+ }
967
+ });
968
+ }
969
+
970
+ // src/effects/wamEffectFactory.ts
971
+ import { connect as connect2, disconnect } from "tone";
972
+ var wamInstanceCounter = 0;
973
+ function createWamEffectInstance(plugin) {
974
+ var _a, _b, _c;
975
+ const node = plugin.audioNode;
976
+ const instanceId = "wam_" + ++wamInstanceCounter;
977
+ return {
978
+ kind: "wam",
979
+ plugin,
980
+ url: plugin.url,
981
+ effect: node,
982
+ id: "wam:" + ((_c = (_b = (_a = plugin.descriptor) == null ? void 0 : _a.name) != null ? _b : plugin.url) != null ? _c : "plugin"),
983
+ instanceId,
984
+ dispose: () => {
985
+ try {
986
+ plugin.destroy();
987
+ } catch (err) {
988
+ console.warn(
989
+ "[waveform-playlist] Error destroying WAM plugin: " + (err instanceof Error ? err.message : String(err))
990
+ );
991
+ }
992
+ },
993
+ setParameter: (name, value) => {
994
+ var _a2;
995
+ if (typeof value !== "number") return;
996
+ const target = plugin.audioNode;
997
+ void ((_a2 = target.setParameterValues) == null ? void 0 : _a2.call(target, { [name]: { id: name, value, normalized: false } }));
998
+ },
999
+ getParameter: () => void 0,
1000
+ // WAM params are async; read via the plugin GUI
1001
+ connect: (destination) => connect2(node, destination),
1002
+ disconnect: () => disconnect(node)
1003
+ };
1004
+ }
1005
+
1006
+ // src/effects/offlineChain.ts
1007
+ import { connect as connect3 } from "tone";
1008
+ function buildOfflineChain(entries, getLivePlugin, rawContext) {
1009
+ return __async(this, null, function* () {
1010
+ const instances = [];
1011
+ const dispose = () => {
1012
+ instances.forEach((inst) => inst.dispose());
1013
+ };
1014
+ try {
1015
+ for (const entry of entries) {
1016
+ if (entry.kind === "native") {
1017
+ instances.push(createEffectInstance(entry.definition, entry.params));
1018
+ continue;
1019
+ }
1020
+ const livePlugin = getLivePlugin(entry.instanceId);
1021
+ if (!livePlugin) {
1022
+ throw new Error(
1023
+ '[waveform-playlist] WAV export: no live WAM plugin found for "' + entry.definition.name + '" (' + entry.instanceId + ") \u2014 cannot re-instantiate it offline."
1024
+ );
1025
+ }
1026
+ const wam = yield loadWamModule();
1027
+ const { hostGroupId } = yield wam.ensureWamHost(rawContext);
1028
+ const clone = yield wam.cloneInstanceInto(livePlugin, rawContext, hostGroupId);
1029
+ instances.push(createWamEffectInstance(clone));
1030
+ }
1031
+ } catch (err) {
1032
+ dispose();
1033
+ throw err;
1034
+ }
1035
+ return { instances, dispose };
1036
+ });
1037
+ }
1038
+ function connectOfflineChain(from, instances, to) {
1039
+ let current = from;
1040
+ for (const inst of instances) {
1041
+ connect3(current, inst.effect);
1042
+ current = inst.effect;
1043
+ }
1044
+ connect3(current, to);
1045
+ }
1046
+
953
1047
  // src/hooks/useDynamicEffects.ts
954
- import { Analyser as Analyser2 } from "tone";
1048
+ import { Analyser as Analyser2, connect as connect4 } from "tone";
955
1049
  function useDynamicEffects(fftSize = 256) {
956
1050
  const [activeEffects, setActiveEffects] = useState2([]);
957
1051
  const activeEffectsRef = useRef3(activeEffects);
958
1052
  activeEffectsRef.current = activeEffects;
959
1053
  const effectInstancesRef = useRef3(/* @__PURE__ */ new Map());
1054
+ const isMountedRef = useRef3(true);
960
1055
  const analyserRef = useRef3(null);
961
1056
  const graphNodesRef = useRef3(null);
962
1057
  const rebuildChain = useCallback2((effects) => {
@@ -968,7 +1063,8 @@ function useDynamicEffects(fftSize = 256) {
968
1063
  } catch (e) {
969
1064
  console.warn("[waveform-playlist] Error disconnecting master effects chain:", e);
970
1065
  }
971
- const instances = effects.map((ae) => effectInstancesRef.current.get(ae.instanceId)).filter((inst) => inst !== void 0);
1066
+ const audible = effects.filter((ae) => !(ae.kind === "wam" && ae.bypassed));
1067
+ const instances = audible.map((ae) => effectInstancesRef.current.get(ae.instanceId)).filter((inst) => inst !== void 0);
972
1068
  if (instances.length === 0) {
973
1069
  masterGainNode.connect(analyserNode);
974
1070
  analyserNode.connect(destination);
@@ -980,10 +1076,10 @@ function useDynamicEffects(fftSize = 256) {
980
1076
  } catch (e) {
981
1077
  console.warn(`[waveform-playlist] Error disconnecting effect "${inst.id}":`, e);
982
1078
  }
983
- currentNode.connect(inst.effect);
1079
+ connect4(currentNode, inst.effect);
984
1080
  currentNode = inst.effect;
985
1081
  });
986
- currentNode.connect(analyserNode);
1082
+ connect4(currentNode, analyserNode);
987
1083
  analyserNode.connect(destination);
988
1084
  }
989
1085
  }, []);
@@ -1002,12 +1098,68 @@ function useDynamicEffects(fftSize = 256) {
1002
1098
  const newActiveEffect = {
1003
1099
  instanceId: instance.instanceId,
1004
1100
  effectId: definition.id,
1101
+ kind: "native",
1005
1102
  definition,
1006
1103
  params,
1007
1104
  bypassed: false
1008
1105
  };
1009
1106
  setActiveEffects((prev) => [...prev, newActiveEffect]);
1010
1107
  }, []);
1108
+ const addWamEffect = useCallback2((url, initialState) => __async(null, null, function* () {
1109
+ var _a, _b;
1110
+ if (!isNativeGlobalContext()) {
1111
+ throw new Error(
1112
+ "[waveform-playlist] WAM plugins require a native AudioContext. Call configureGlobalContext({ nativeAudioContext: true }) from @waveform-playlist/playout before any audio initialization."
1113
+ );
1114
+ }
1115
+ const wam = yield loadWamModule();
1116
+ const ctx = getGlobalAudioContext();
1117
+ const { hostGroupId } = yield wam.ensureWamHost(ctx);
1118
+ const plugin = yield wam.createWamInstance(
1119
+ url,
1120
+ ctx,
1121
+ hostGroupId,
1122
+ initialState !== void 0 ? { initialState } : void 0
1123
+ );
1124
+ if (!isMountedRef.current) {
1125
+ try {
1126
+ plugin.destroy();
1127
+ } catch (err) {
1128
+ console.warn(
1129
+ "[waveform-playlist] Error destroying WAM plugin after unmount: " + (err instanceof Error ? err.message : String(err))
1130
+ );
1131
+ }
1132
+ throw new Error(
1133
+ "[waveform-playlist] addWamEffect aborted: hook unmounted before the plugin finished loading."
1134
+ );
1135
+ }
1136
+ const instance = createWamEffectInstance(plugin);
1137
+ effectInstancesRef.current.set(instance.instanceId, instance);
1138
+ const definition = {
1139
+ id: instance.id,
1140
+ name: (_b = (_a = plugin.descriptor) == null ? void 0 : _a.name) != null ? _b : url,
1141
+ category: "wam",
1142
+ description: "WAM plugin",
1143
+ parameters: []
1144
+ };
1145
+ setActiveEffects((prev) => [
1146
+ ...prev,
1147
+ {
1148
+ instanceId: instance.instanceId,
1149
+ effectId: instance.id,
1150
+ kind: "wam",
1151
+ url,
1152
+ definition,
1153
+ params: {},
1154
+ bypassed: false
1155
+ }
1156
+ ]);
1157
+ return instance.instanceId;
1158
+ }), []);
1159
+ const getWamPlugin = useCallback2((instanceId) => {
1160
+ const inst = effectInstancesRef.current.get(instanceId);
1161
+ return (inst == null ? void 0 : inst.kind) === "wam" ? inst.plugin : void 0;
1162
+ }, []);
1011
1163
  const removeEffect = useCallback2((instanceId) => {
1012
1164
  const instance = effectInstancesRef.current.get(instanceId);
1013
1165
  if (instance) {
@@ -1035,6 +1187,12 @@ function useDynamicEffects(fftSize = 256) {
1035
1187
  const effect = activeEffectsRef.current.find((e) => e.instanceId === instanceId);
1036
1188
  if (!effect) return;
1037
1189
  const newBypassed = !effect.bypassed;
1190
+ if (effect.kind === "wam") {
1191
+ setActiveEffects(
1192
+ (prev) => prev.map((e) => e.instanceId === instanceId ? __spreadProps(__spreadValues({}, e), { bypassed: newBypassed }) : e)
1193
+ );
1194
+ return;
1195
+ }
1038
1196
  const instance = effectInstancesRef.current.get(instanceId);
1039
1197
  if (instance) {
1040
1198
  const originalWet = (_a = effect.params.wet) != null ? _a : 1;
@@ -1070,17 +1228,18 @@ function useDynamicEffects(fftSize = 256) {
1070
1228
  analyserNode
1071
1229
  };
1072
1230
  const effects = activeEffectsRef.current;
1073
- const instances = effects.map((ae) => effectInstancesRef.current.get(ae.instanceId)).filter((inst) => inst !== void 0);
1231
+ const audible = effects.filter((ae) => !(ae.kind === "wam" && ae.bypassed));
1232
+ const instances = audible.map((ae) => effectInstancesRef.current.get(ae.instanceId)).filter((inst) => inst !== void 0);
1074
1233
  if (instances.length === 0) {
1075
1234
  masterGainNode.connect(analyserNode);
1076
1235
  analyserNode.connect(destination);
1077
1236
  } else {
1078
1237
  let currentNode = masterGainNode;
1079
1238
  instances.forEach((inst) => {
1080
- currentNode.connect(inst.effect);
1239
+ connect4(currentNode, inst.effect);
1081
1240
  currentNode = inst.effect;
1082
1241
  });
1083
- currentNode.connect(analyserNode);
1242
+ connect4(currentNode, analyserNode);
1084
1243
  analyserNode.connect(destination);
1085
1244
  }
1086
1245
  return function cleanup() {
@@ -1093,42 +1252,45 @@ function useDynamicEffects(fftSize = 256) {
1093
1252
  // Only fftSize - reads effects from ref
1094
1253
  );
1095
1254
  useEffect2(() => {
1255
+ isMountedRef.current = true;
1096
1256
  const effectInstances = effectInstancesRef.current;
1097
1257
  return () => {
1258
+ isMountedRef.current = false;
1098
1259
  effectInstances.forEach((inst) => inst.dispose());
1099
1260
  effectInstances.clear();
1100
1261
  };
1101
1262
  }, []);
1102
1263
  const createOfflineEffectsFunction = useCallback2(() => {
1103
- const nonBypassedEffects = activeEffects.filter((e) => !e.bypassed);
1104
- if (nonBypassedEffects.length === 0) {
1264
+ const nonBypassed = activeEffects.filter((e) => !e.bypassed);
1265
+ if (nonBypassed.length === 0) {
1105
1266
  return void 0;
1106
1267
  }
1107
- return (masterGainNode, destination, _isOffline) => {
1108
- const offlineInstances = [];
1109
- for (const activeEffect of nonBypassedEffects) {
1110
- const instance = createEffectInstance(activeEffect.definition, activeEffect.params);
1111
- offlineInstances.push(instance);
1112
- }
1113
- if (offlineInstances.length === 0) {
1114
- masterGainNode.connect(destination);
1115
- } else {
1116
- let currentNode = masterGainNode;
1117
- offlineInstances.forEach((inst) => {
1118
- currentNode.connect(inst.effect);
1119
- currentNode = inst.effect;
1120
- });
1121
- currentNode.connect(destination);
1268
+ const hasWam = nonBypassed.some((e) => e.kind === "wam");
1269
+ return (masterGainNode, destination, _isOffline) => __async(null, null, function* () {
1270
+ if (hasWam && !isNativeGlobalContext()) {
1271
+ throw new Error(
1272
+ "[waveform-playlist] WAV export with WAM effects requires a native AudioContext. Call configureGlobalContext({ nativeAudioContext: true }) from @waveform-playlist/playout before any audio initialization."
1273
+ );
1122
1274
  }
1123
- return function cleanup() {
1124
- offlineInstances.forEach((inst) => inst.dispose());
1125
- };
1126
- };
1275
+ const rawContext = masterGainNode.context.rawContext;
1276
+ const { instances, dispose } = yield buildOfflineChain(
1277
+ nonBypassed,
1278
+ (instanceId) => {
1279
+ const inst = effectInstancesRef.current.get(instanceId);
1280
+ return (inst == null ? void 0 : inst.kind) === "wam" ? inst.plugin : void 0;
1281
+ },
1282
+ rawContext
1283
+ );
1284
+ connectOfflineChain(masterGainNode, instances, destination);
1285
+ return dispose;
1286
+ });
1127
1287
  }, [activeEffects]);
1128
1288
  return {
1129
1289
  activeEffects,
1130
1290
  availableEffects: effectDefinitions,
1131
1291
  addEffect,
1292
+ addWamEffect,
1293
+ getWamPlugin,
1132
1294
  removeEffect,
1133
1295
  updateParameter,
1134
1296
  toggleBypass,
@@ -1142,11 +1304,14 @@ function useDynamicEffects(fftSize = 256) {
1142
1304
 
1143
1305
  // src/hooks/useTrackDynamicEffects.ts
1144
1306
  import { useState as useState3, useCallback as useCallback3, useRef as useRef4, useEffect as useEffect3 } from "react";
1307
+ import { isNativeGlobalContext as isNativeGlobalContext2, getGlobalAudioContext as getGlobalAudioContext2 } from "@waveform-playlist/playout";
1308
+ import { connect as connect5 } from "tone";
1145
1309
  function useTrackDynamicEffects() {
1146
1310
  const [trackEffectsState, setTrackEffectsState] = useState3(
1147
1311
  /* @__PURE__ */ new Map()
1148
1312
  );
1149
1313
  const trackEffectInstancesRef = useRef4(/* @__PURE__ */ new Map());
1314
+ const isMountedRef = useRef4(true);
1150
1315
  const trackGraphNodesRef = useRef4(/* @__PURE__ */ new Map());
1151
1316
  const rebuildTrackChain = useCallback3((trackId, trackEffects) => {
1152
1317
  const nodes = trackGraphNodesRef.current.get(trackId);
@@ -1158,7 +1323,8 @@ function useTrackDynamicEffects() {
1158
1323
  } catch (e) {
1159
1324
  console.warn(`[waveform-playlist] Error disconnecting track "${trackId}" effect chain:`, e);
1160
1325
  }
1161
- const instances = trackEffects.map((ae) => instancesMap == null ? void 0 : instancesMap.get(ae.instanceId)).filter((inst) => inst !== void 0);
1326
+ const audible = trackEffects.filter((ae) => !(ae.kind === "wam" && ae.bypassed));
1327
+ const instances = audible.map((ae) => instancesMap == null ? void 0 : instancesMap.get(ae.instanceId)).filter((inst) => inst !== void 0);
1162
1328
  if (instances.length === 0) {
1163
1329
  graphEnd.connect(masterGainNode);
1164
1330
  } else {
@@ -1172,10 +1338,10 @@ function useTrackDynamicEffects() {
1172
1338
  e
1173
1339
  );
1174
1340
  }
1175
- currentNode.connect(inst.effect);
1341
+ connect5(currentNode, inst.effect);
1176
1342
  currentNode = inst.effect;
1177
1343
  });
1178
- currentNode.connect(masterGainNode);
1344
+ connect5(currentNode, masterGainNode);
1179
1345
  }
1180
1346
  }, []);
1181
1347
  const addEffectToTrack = useCallback3((trackId, effectId) => {
@@ -1196,6 +1362,7 @@ function useTrackDynamicEffects() {
1196
1362
  const newActiveEffect = {
1197
1363
  instanceId: instance.instanceId,
1198
1364
  effectId: definition.id,
1365
+ kind: "native",
1199
1366
  definition,
1200
1367
  params,
1201
1368
  bypassed: false
@@ -1207,6 +1374,76 @@ function useTrackDynamicEffects() {
1207
1374
  return newState;
1208
1375
  });
1209
1376
  }, []);
1377
+ const addWamEffectToTrack = useCallback3(
1378
+ (trackId, url, initialState) => __async(null, null, function* () {
1379
+ var _a, _b;
1380
+ if (!isNativeGlobalContext2()) {
1381
+ throw new Error(
1382
+ "[waveform-playlist] WAM plugins require a native AudioContext. Call configureGlobalContext({ nativeAudioContext: true }) from @waveform-playlist/playout before any audio initialization."
1383
+ );
1384
+ }
1385
+ const wam = yield loadWamModule();
1386
+ const ctx = getGlobalAudioContext2();
1387
+ const { hostGroupId } = yield wam.ensureWamHost(ctx);
1388
+ const plugin = yield wam.createWamInstance(
1389
+ url,
1390
+ ctx,
1391
+ hostGroupId,
1392
+ initialState !== void 0 ? { initialState } : void 0
1393
+ );
1394
+ if (!isMountedRef.current) {
1395
+ try {
1396
+ plugin.destroy();
1397
+ } catch (err) {
1398
+ console.warn(
1399
+ "[waveform-playlist] Error destroying WAM plugin after unmount: " + (err instanceof Error ? err.message : String(err))
1400
+ );
1401
+ }
1402
+ throw new Error(
1403
+ "[waveform-playlist] addWamEffectToTrack aborted: hook unmounted before the plugin finished loading."
1404
+ );
1405
+ }
1406
+ const instance = createWamEffectInstance(plugin);
1407
+ if (!trackEffectInstancesRef.current.has(trackId)) {
1408
+ trackEffectInstancesRef.current.set(trackId, /* @__PURE__ */ new Map());
1409
+ }
1410
+ trackEffectInstancesRef.current.get(trackId).set(instance.instanceId, instance);
1411
+ const definition = {
1412
+ id: instance.id,
1413
+ name: (_b = (_a = plugin.descriptor) == null ? void 0 : _a.name) != null ? _b : url,
1414
+ category: "wam",
1415
+ description: "WAM plugin",
1416
+ parameters: []
1417
+ };
1418
+ setTrackEffectsState((prev) => {
1419
+ const newState = new Map(prev);
1420
+ const existing = newState.get(trackId) || [];
1421
+ newState.set(trackId, [
1422
+ ...existing,
1423
+ {
1424
+ instanceId: instance.instanceId,
1425
+ effectId: instance.id,
1426
+ kind: "wam",
1427
+ url,
1428
+ definition,
1429
+ params: {},
1430
+ bypassed: false
1431
+ }
1432
+ ]);
1433
+ return newState;
1434
+ });
1435
+ return instance.instanceId;
1436
+ }),
1437
+ []
1438
+ );
1439
+ const getTrackWamPlugin = useCallback3(
1440
+ (trackId, instanceId) => {
1441
+ const instancesMap = trackEffectInstancesRef.current.get(trackId);
1442
+ const inst = instancesMap == null ? void 0 : instancesMap.get(instanceId);
1443
+ return (inst == null ? void 0 : inst.kind) === "wam" ? inst.plugin : void 0;
1444
+ },
1445
+ []
1446
+ );
1210
1447
  const removeEffectFromTrack = useCallback3((trackId, instanceId) => {
1211
1448
  const instancesMap = trackEffectInstancesRef.current.get(trackId);
1212
1449
  const instance = instancesMap == null ? void 0 : instancesMap.get(instanceId);
@@ -1251,6 +1488,18 @@ function useTrackDynamicEffects() {
1251
1488
  const effect = trackEffects.find((e) => e.instanceId === instanceId);
1252
1489
  if (!effect) return;
1253
1490
  const newBypassed = !effect.bypassed;
1491
+ if (effect.kind === "wam") {
1492
+ setTrackEffectsState((prev) => {
1493
+ const newState = new Map(prev);
1494
+ const existing = newState.get(trackId) || [];
1495
+ newState.set(
1496
+ trackId,
1497
+ existing.map((e) => e.instanceId === instanceId ? __spreadProps(__spreadValues({}, e), { bypassed: newBypassed }) : e)
1498
+ );
1499
+ return newState;
1500
+ });
1501
+ return;
1502
+ }
1254
1503
  const instancesMap = trackEffectInstancesRef.current.get(trackId);
1255
1504
  const instance = instancesMap == null ? void 0 : instancesMap.get(instanceId);
1256
1505
  if (instance) {
@@ -1290,16 +1539,17 @@ function useTrackDynamicEffects() {
1290
1539
  });
1291
1540
  const trackEffects = trackEffectsStateRef.current.get(trackId) || [];
1292
1541
  const instancesMap = trackEffectInstancesRef.current.get(trackId);
1293
- const instances = trackEffects.map((ae) => instancesMap == null ? void 0 : instancesMap.get(ae.instanceId)).filter((inst) => inst !== void 0);
1542
+ const audible = trackEffects.filter((ae) => !(ae.kind === "wam" && ae.bypassed));
1543
+ const instances = audible.map((ae) => instancesMap == null ? void 0 : instancesMap.get(ae.instanceId)).filter((inst) => inst !== void 0);
1294
1544
  if (instances.length === 0) {
1295
1545
  graphEnd.connect(masterGainNode);
1296
1546
  } else {
1297
1547
  let currentNode = graphEnd;
1298
1548
  instances.forEach((inst) => {
1299
- currentNode.connect(inst.effect);
1549
+ connect5(currentNode, inst.effect);
1300
1550
  currentNode = inst.effect;
1301
1551
  });
1302
- currentNode.connect(masterGainNode);
1552
+ connect5(currentNode, masterGainNode);
1303
1553
  }
1304
1554
  return function cleanup() {
1305
1555
  trackGraphNodesRef.current.delete(trackId);
@@ -1315,8 +1565,10 @@ function useTrackDynamicEffects() {
1315
1565
  });
1316
1566
  }, [trackEffectsState, rebuildTrackChain]);
1317
1567
  useEffect3(() => {
1568
+ isMountedRef.current = true;
1318
1569
  const trackEffectInstances = trackEffectInstancesRef.current;
1319
1570
  return () => {
1571
+ isMountedRef.current = false;
1320
1572
  trackEffectInstances.forEach((instancesMap) => {
1321
1573
  instancesMap.forEach((inst) => inst.dispose());
1322
1574
  instancesMap.clear();
@@ -1327,36 +1579,38 @@ function useTrackDynamicEffects() {
1327
1579
  const createOfflineTrackEffectsFunction = useCallback3(
1328
1580
  (trackId) => {
1329
1581
  const trackEffects = trackEffectsState.get(trackId) || [];
1330
- const nonBypassedEffects = trackEffects.filter((e) => !e.bypassed);
1331
- if (nonBypassedEffects.length === 0) {
1582
+ const nonBypassed = trackEffects.filter((e) => !e.bypassed);
1583
+ if (nonBypassed.length === 0) {
1332
1584
  return void 0;
1333
1585
  }
1334
- return (graphEnd, masterGainNode, _isOffline) => {
1335
- const offlineInstances = [];
1336
- for (const activeEffect of nonBypassedEffects) {
1337
- const instance = createEffectInstance(activeEffect.definition, activeEffect.params);
1338
- offlineInstances.push(instance);
1339
- }
1340
- if (offlineInstances.length === 0) {
1341
- graphEnd.connect(masterGainNode);
1342
- } else {
1343
- let currentNode = graphEnd;
1344
- offlineInstances.forEach((inst) => {
1345
- currentNode.connect(inst.effect);
1346
- currentNode = inst.effect;
1347
- });
1348
- currentNode.connect(masterGainNode);
1586
+ const hasWam = nonBypassed.some((e) => e.kind === "wam");
1587
+ return (graphEnd, masterGainNode, _isOffline) => __async(null, null, function* () {
1588
+ if (hasWam && !isNativeGlobalContext2()) {
1589
+ throw new Error(
1590
+ "[waveform-playlist] WAV export with WAM effects requires a native AudioContext. Call configureGlobalContext({ nativeAudioContext: true }) from @waveform-playlist/playout before any audio initialization."
1591
+ );
1349
1592
  }
1350
- return function cleanup() {
1351
- offlineInstances.forEach((inst) => inst.dispose());
1352
- };
1353
- };
1593
+ const rawContext = graphEnd.context.rawContext;
1594
+ const { instances, dispose } = yield buildOfflineChain(
1595
+ nonBypassed,
1596
+ (instanceId) => {
1597
+ var _a;
1598
+ const inst = (_a = trackEffectInstancesRef.current.get(trackId)) == null ? void 0 : _a.get(instanceId);
1599
+ return (inst == null ? void 0 : inst.kind) === "wam" ? inst.plugin : void 0;
1600
+ },
1601
+ rawContext
1602
+ );
1603
+ connectOfflineChain(graphEnd, instances, masterGainNode);
1604
+ return dispose;
1605
+ });
1354
1606
  },
1355
1607
  [trackEffectsState]
1356
1608
  );
1357
1609
  return {
1358
1610
  trackEffectsState,
1359
1611
  addEffectToTrack,
1612
+ addWamEffectToTrack,
1613
+ getTrackWamPlugin,
1360
1614
  removeEffectFromTrack,
1361
1615
  updateTrackEffectParameter,
1362
1616
  toggleBypass,
@@ -1375,10 +1629,45 @@ import {
1375
1629
  applyFadeIn,
1376
1630
  applyFadeOut
1377
1631
  } from "@waveform-playlist/core";
1378
- import {
1379
- getUnderlyingAudioParam,
1380
- getGlobalAudioContext
1381
- } from "@waveform-playlist/playout";
1632
+ import { getUnderlyingAudioParam, getGlobalAudioContext as getGlobalAudioContext3 } from "@waveform-playlist/playout";
1633
+
1634
+ // src/utils/renderToneOffline.ts
1635
+ import { getContext as getContext2, setContext, OfflineContext } from "tone";
1636
+ import { isNativeGlobalContext as isNativeGlobalContext3 } from "@waveform-playlist/playout";
1637
+ var renderQueue = Promise.resolve();
1638
+ function renderToneOffline(build, duration, channels, sampleRate) {
1639
+ return __async(this, null, function* () {
1640
+ const run = renderQueue.then(
1641
+ () => renderToneOfflineSerial(build, duration, channels, sampleRate)
1642
+ );
1643
+ renderQueue = run.catch(() => void 0);
1644
+ return run;
1645
+ });
1646
+ }
1647
+ function renderToneOfflineSerial(build, duration, channels, sampleRate) {
1648
+ return __async(this, null, function* () {
1649
+ const offlineContext = isNativeGlobalContext3() ? new OfflineContext(
1650
+ new OfflineAudioContext({
1651
+ numberOfChannels: channels,
1652
+ length: Math.round(duration * sampleRate),
1653
+ sampleRate
1654
+ })
1655
+ ) : new OfflineContext(channels, duration, sampleRate);
1656
+ const previousContext = getContext2();
1657
+ setContext(offlineContext);
1658
+ try {
1659
+ yield build(offlineContext);
1660
+ } finally {
1661
+ setContext(previousContext);
1662
+ }
1663
+ const toneBuffer = yield offlineContext.render();
1664
+ const audioBuffer = toneBuffer.get();
1665
+ if (!audioBuffer) {
1666
+ throw new Error("Offline rendering produced no audio buffer");
1667
+ }
1668
+ return audioBuffer;
1669
+ });
1670
+ }
1382
1671
 
1383
1672
  // src/utils/wavEncoder.ts
1384
1673
  function encodeWav(audioBuffer, options = {}) {
@@ -1477,7 +1766,7 @@ function useExportWav() {
1477
1766
  if (mode === "individual" && (trackIndex === void 0 || trackIndex < 0 || trackIndex >= tracks.length)) {
1478
1767
  throw new Error("Invalid track index for individual export");
1479
1768
  }
1480
- const sampleRate = getGlobalAudioContext().sampleRate;
1769
+ const sampleRate = getGlobalAudioContext3().sampleRate;
1481
1770
  let totalDurationSamples = 0;
1482
1771
  for (const track of tracks) {
1483
1772
  for (const clip of track.clips) {
@@ -1534,7 +1823,7 @@ function useExportWav() {
1534
1823
  }
1535
1824
  function renderOffline(tracksToRender, hasSolo, duration, sampleRate, applyEffects, effectsFunction, createOfflineTrackEffects, onProgress) {
1536
1825
  return __async(this, null, function* () {
1537
- const { Offline, Volume: Volume2, Gain, Panner, Player, ToneAudioBuffer } = yield import("tone");
1826
+ const { Volume: Volume2, Gain: Gain2, Panner, Player, ToneAudioBuffer } = yield import("tone");
1538
1827
  onProgress(0.1);
1539
1828
  const audibleTracks = tracksToRender.filter(({ state }) => {
1540
1829
  if (state.muted && !state.soloed) return false;
@@ -1545,23 +1834,25 @@ function renderOffline(tracksToRender, hasSolo, duration, sampleRate, applyEffec
1545
1834
  (max, { track }) => Math.max(max, trackChannelCount(track)),
1546
1835
  1
1547
1836
  );
1548
- let buffer;
1837
+ const cleanups = [];
1549
1838
  try {
1550
- buffer = yield Offline(
1551
- (_0) => __async(null, [_0], function* ({ transport, destination }) {
1839
+ const audioBuffer = yield renderToneOffline(
1840
+ (context) => __async(null, null, function* () {
1552
1841
  const masterVolume = new Volume2(0);
1553
1842
  if (effectsFunction && applyEffects) {
1554
- effectsFunction(masterVolume, destination, true);
1843
+ const cleanup = yield effectsFunction(masterVolume, context.destination, true);
1844
+ if (cleanup) cleanups.push(cleanup);
1555
1845
  } else {
1556
- masterVolume.connect(destination);
1846
+ masterVolume.connect(context.destination);
1557
1847
  }
1558
1848
  for (const { track, state } of audibleTracks) {
1559
1849
  const trackVolume = new Volume2(gainToDb(state.volume));
1560
1850
  const trackPan = new Panner({ pan: state.pan, channelCount: trackChannelCount(track) });
1561
- const trackMute = new Gain(state.muted ? 0 : 1);
1851
+ const trackMute = new Gain2(state.muted ? 0 : 1);
1562
1852
  const trackEffects = createOfflineTrackEffects == null ? void 0 : createOfflineTrackEffects(track.id);
1563
1853
  if (trackEffects && applyEffects) {
1564
- trackEffects(trackMute, masterVolume, true);
1854
+ const cleanup = yield trackEffects(trackMute, masterVolume, true);
1855
+ if (cleanup) cleanups.push(cleanup);
1565
1856
  } else {
1566
1857
  trackMute.connect(masterVolume);
1567
1858
  }
@@ -1569,7 +1860,7 @@ function renderOffline(tracksToRender, hasSolo, duration, sampleRate, applyEffec
1569
1860
  trackVolume.connect(trackPan);
1570
1861
  for (const clip of track.clips) {
1571
1862
  const {
1572
- audioBuffer,
1863
+ audioBuffer: clipBuffer,
1573
1864
  startSample,
1574
1865
  durationSamples,
1575
1866
  offsetSamples,
@@ -1577,7 +1868,7 @@ function renderOffline(tracksToRender, hasSolo, duration, sampleRate, applyEffec
1577
1868
  fadeIn,
1578
1869
  fadeOut
1579
1870
  } = clip;
1580
- if (!audioBuffer) {
1871
+ if (!clipBuffer) {
1581
1872
  console.warn(
1582
1873
  '[waveform-playlist] Skipping clip "' + (clip.name || clip.id) + '" - no audioBuffer for export'
1583
1874
  );
@@ -1586,9 +1877,9 @@ function renderOffline(tracksToRender, hasSolo, duration, sampleRate, applyEffec
1586
1877
  const startTime = startSample / sampleRate;
1587
1878
  const clipDuration = durationSamples / sampleRate;
1588
1879
  const offset = offsetSamples / sampleRate;
1589
- const toneBuffer = new ToneAudioBuffer(audioBuffer);
1880
+ const toneBuffer = new ToneAudioBuffer(clipBuffer);
1590
1881
  const player = new Player(toneBuffer);
1591
- const fadeGain = new Gain(clipGain);
1882
+ const fadeGain = new Gain2(clipGain);
1592
1883
  player.connect(fadeGain);
1593
1884
  fadeGain.connect(trackVolume);
1594
1885
  if (applyEffects) {
@@ -1604,25 +1895,29 @@ function renderOffline(tracksToRender, hasSolo, duration, sampleRate, applyEffec
1604
1895
  player.start(startTime, offset, clipDuration);
1605
1896
  }
1606
1897
  }
1607
- transport.start(0);
1898
+ context.transport.start(0);
1608
1899
  }),
1609
1900
  duration,
1610
1901
  outputChannels,
1611
1902
  sampleRate
1612
1903
  );
1904
+ return audioBuffer;
1613
1905
  } catch (err) {
1614
1906
  if (err instanceof Error) {
1615
1907
  throw err;
1616
- } else {
1617
- throw new Error("Tone.Offline rendering failed: " + String(err));
1908
+ }
1909
+ throw new Error("Offline rendering failed: " + String(err));
1910
+ } finally {
1911
+ for (const cleanup of cleanups) {
1912
+ try {
1913
+ cleanup();
1914
+ } catch (err) {
1915
+ console.warn(
1916
+ "[waveform-playlist] Export cleanup error: " + (err instanceof Error ? err.message : String(err))
1917
+ );
1918
+ }
1618
1919
  }
1619
1920
  }
1620
- onProgress(0.9);
1621
- const result = buffer.get();
1622
- if (!result) {
1623
- throw new Error("Offline rendering produced no audio buffer");
1624
- }
1625
- return result;
1626
1921
  });
1627
1922
  }
1628
1923
  function applyClipFades(gainParam, clipGain, startTime, clipDuration, fadeIn, fadeOut) {
@@ -1646,7 +1941,7 @@ function applyClipFades(gainParam, clipGain, startTime, clipDuration, fadeIn, fa
1646
1941
  // src/hooks/useDynamicTracks.ts
1647
1942
  import { useState as useState5, useCallback as useCallback5, useRef as useRef5, useEffect as useEffect4 } from "react";
1648
1943
  import { createTrack as createTrack2, createClipFromSeconds as createClipFromSeconds2 } from "@waveform-playlist/core";
1649
- import { getGlobalAudioContext as getGlobalAudioContext2 } from "@waveform-playlist/playout";
1944
+ import { getGlobalAudioContext as getGlobalAudioContext4 } from "@waveform-playlist/playout";
1650
1945
  function getSourceName(source) {
1651
1946
  var _a, _b, _c, _d, _e;
1652
1947
  if (source instanceof File) {
@@ -1697,7 +1992,7 @@ function useDynamicTracks() {
1697
1992
  }, []);
1698
1993
  const addTracks = useCallback5((sources) => {
1699
1994
  if (sources.length === 0) return;
1700
- const audioContext = getGlobalAudioContext2();
1995
+ const audioContext = getGlobalAudioContext4();
1701
1996
  const placeholders = sources.map((source) => ({
1702
1997
  track: createTrack2({ name: `${getSourceName(source)} (loading...)`, clips: [] }),
1703
1998
  source
@@ -1939,14 +2234,77 @@ var ExportWavButton = ({
1939
2234
  }
1940
2235
  );
1941
2236
  };
2237
+
2238
+ // src/components/WamEffectGui.tsx
2239
+ import { useEffect as useEffect7, useRef as useRef8 } from "react";
2240
+ import { jsx as jsx3 } from "react/jsx-runtime";
2241
+ var WamEffectGui = ({ plugin, className }) => {
2242
+ const hostRef = useRef8(null);
2243
+ useEffect7(() => {
2244
+ const host = hostRef.current;
2245
+ if (!plugin || !host) return;
2246
+ let cancelled = false;
2247
+ let gui = null;
2248
+ (() => __async(null, null, function* () {
2249
+ var _a;
2250
+ try {
2251
+ if (plugin.createGui) {
2252
+ gui = yield plugin.createGui();
2253
+ } else {
2254
+ const wam = yield loadWamModule();
2255
+ gui = yield wam.createWamParameterPanel(
2256
+ plugin.audioNode
2257
+ );
2258
+ }
2259
+ if (cancelled) {
2260
+ if (gui) {
2261
+ try {
2262
+ (_a = plugin.destroyGui) == null ? void 0 : _a.call(plugin, gui);
2263
+ } catch (destroyErr) {
2264
+ console.warn(
2265
+ "[waveform-playlist] Error destroying WAM GUI: " + (destroyErr instanceof Error ? destroyErr.message : String(destroyErr))
2266
+ );
2267
+ }
2268
+ }
2269
+ gui = null;
2270
+ return;
2271
+ }
2272
+ host.innerHTML = "";
2273
+ host.appendChild(gui);
2274
+ } catch (err) {
2275
+ console.warn(
2276
+ "[waveform-playlist] Failed to create WAM GUI: " + (err instanceof Error ? err.message : String(err))
2277
+ );
2278
+ }
2279
+ }))();
2280
+ return () => {
2281
+ var _a;
2282
+ cancelled = true;
2283
+ if (gui) {
2284
+ gui.remove();
2285
+ try {
2286
+ (_a = plugin.destroyGui) == null ? void 0 : _a.call(plugin, gui);
2287
+ } catch (destroyErr) {
2288
+ console.warn(
2289
+ "[waveform-playlist] Error destroying WAM GUI: " + (destroyErr instanceof Error ? destroyErr.message : String(destroyErr))
2290
+ );
2291
+ }
2292
+ }
2293
+ };
2294
+ }, [plugin]);
2295
+ return /* @__PURE__ */ jsx3("div", { ref: hostRef, className: className != null ? className : "wam-effect-gui" });
2296
+ };
1942
2297
  export {
1943
2298
  ExportWavButton,
2299
+ WamEffectGui,
1944
2300
  createEffectChain,
1945
2301
  createEffectInstance,
2302
+ createWamEffectInstance,
1946
2303
  effectCategories,
1947
2304
  effectDefinitions,
1948
2305
  getEffectDefinition,
1949
2306
  getEffectsByCategory,
2307
+ loadWamModule,
1950
2308
  useAudioTracks,
1951
2309
  useDynamicEffects,
1952
2310
  useDynamicTracks,