@waveform-playlist/browser 13.1.3 → 14.0.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/index.d.mts +34 -433
- package/dist/index.d.ts +34 -433
- package/dist/index.js +610 -2371
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +507 -2291
- package/dist/index.mjs.map +1 -1
- package/dist/tone.d.mts +429 -0
- package/dist/tone.d.ts +429 -0
- package/dist/tone.js +1963 -0
- package/dist/tone.js.map +1 -0
- package/dist/tone.mjs +1958 -0
- package/dist/tone.mjs.map +1 -0
- package/package.json +23 -8
package/dist/tone.js
ADDED
|
@@ -0,0 +1,1963 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __defProps = Object.defineProperties;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
|
|
7
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
8
|
+
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
9
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
10
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
11
|
+
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
12
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
13
|
+
var __spreadValues = (a, b) => {
|
|
14
|
+
for (var prop in b || (b = {}))
|
|
15
|
+
if (__hasOwnProp.call(b, prop))
|
|
16
|
+
__defNormalProp(a, prop, b[prop]);
|
|
17
|
+
if (__getOwnPropSymbols)
|
|
18
|
+
for (var prop of __getOwnPropSymbols(b)) {
|
|
19
|
+
if (__propIsEnum.call(b, prop))
|
|
20
|
+
__defNormalProp(a, prop, b[prop]);
|
|
21
|
+
}
|
|
22
|
+
return a;
|
|
23
|
+
};
|
|
24
|
+
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
|
|
25
|
+
var __export = (target, all) => {
|
|
26
|
+
for (var name in all)
|
|
27
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
28
|
+
};
|
|
29
|
+
var __copyProps = (to, from, except, desc) => {
|
|
30
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
31
|
+
for (let key of __getOwnPropNames(from))
|
|
32
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
33
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
34
|
+
}
|
|
35
|
+
return to;
|
|
36
|
+
};
|
|
37
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
38
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
39
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
40
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
41
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
42
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
43
|
+
mod
|
|
44
|
+
));
|
|
45
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
46
|
+
var __async = (__this, __arguments, generator) => {
|
|
47
|
+
return new Promise((resolve, reject) => {
|
|
48
|
+
var fulfilled = (value) => {
|
|
49
|
+
try {
|
|
50
|
+
step(generator.next(value));
|
|
51
|
+
} catch (e) {
|
|
52
|
+
reject(e);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
var rejected = (value) => {
|
|
56
|
+
try {
|
|
57
|
+
step(generator.throw(value));
|
|
58
|
+
} catch (e) {
|
|
59
|
+
reject(e);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
|
|
63
|
+
step((generator = generator.apply(__this, __arguments)).next());
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// src/tone.ts
|
|
68
|
+
var tone_exports = {};
|
|
69
|
+
__export(tone_exports, {
|
|
70
|
+
ExportWavButton: () => ExportWavButton,
|
|
71
|
+
createEffectChain: () => createEffectChain,
|
|
72
|
+
createEffectInstance: () => createEffectInstance,
|
|
73
|
+
effectCategories: () => effectCategories,
|
|
74
|
+
effectDefinitions: () => effectDefinitions,
|
|
75
|
+
getEffectDefinition: () => getEffectDefinition,
|
|
76
|
+
getEffectsByCategory: () => getEffectsByCategory,
|
|
77
|
+
useAudioTracks: () => useAudioTracks,
|
|
78
|
+
useDynamicEffects: () => useDynamicEffects,
|
|
79
|
+
useDynamicTracks: () => useDynamicTracks,
|
|
80
|
+
useExportWav: () => useExportWav,
|
|
81
|
+
useMasterAnalyser: () => useMasterAnalyser,
|
|
82
|
+
useOutputMeter: () => useOutputMeter,
|
|
83
|
+
useTrackDynamicEffects: () => useTrackDynamicEffects
|
|
84
|
+
});
|
|
85
|
+
module.exports = __toCommonJS(tone_exports);
|
|
86
|
+
|
|
87
|
+
// src/hooks/useAudioTracks.ts
|
|
88
|
+
var import_react = require("react");
|
|
89
|
+
var import_core = require("@waveform-playlist/core");
|
|
90
|
+
var Tone = __toESM(require("tone"));
|
|
91
|
+
function buildTrackFromConfig(config, index, audioBuffer, stableIds, contextSampleRate = 48e3) {
|
|
92
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p;
|
|
93
|
+
const buffer = audioBuffer != null ? audioBuffer : config.audioBuffer;
|
|
94
|
+
const sampleRate = (_c = (_b = buffer == null ? void 0 : buffer.sampleRate) != null ? _b : (_a = config.waveformData) == null ? void 0 : _a.sample_rate) != null ? _c : contextSampleRate;
|
|
95
|
+
const sourceDuration = (_g = (_e = buffer == null ? void 0 : buffer.duration) != null ? _e : (_d = config.waveformData) == null ? void 0 : _d.duration) != null ? _g : config.duration != null ? config.duration + ((_f = config.offset) != null ? _f : 0) : void 0;
|
|
96
|
+
if (sourceDuration === void 0) {
|
|
97
|
+
console.warn(
|
|
98
|
+
`[waveform-playlist] Track ${index + 1} ("${(_h = config.name) != null ? _h : "unnamed"}"): Cannot create track \u2014 provide duration, audioBuffer, or waveformData with duration.`
|
|
99
|
+
);
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
const clip = (0, import_core.createClipFromSeconds)({
|
|
103
|
+
audioBuffer: buffer,
|
|
104
|
+
sampleRate,
|
|
105
|
+
sourceDuration,
|
|
106
|
+
startTime: (_i = config.startTime) != null ? _i : 0,
|
|
107
|
+
duration: (_j = config.duration) != null ? _j : sourceDuration,
|
|
108
|
+
offset: (_k = config.offset) != null ? _k : 0,
|
|
109
|
+
name: config.name || `Track ${index + 1}`,
|
|
110
|
+
fadeIn: config.fadeIn,
|
|
111
|
+
fadeOut: config.fadeOut,
|
|
112
|
+
waveformData: config.waveformData
|
|
113
|
+
});
|
|
114
|
+
if (isNaN(clip.startSample) || isNaN(clip.durationSamples) || isNaN(clip.offsetSamples)) {
|
|
115
|
+
console.error(
|
|
116
|
+
`[waveform-playlist] Invalid clip values for track ${index + 1} ("${(_l = config.name) != null ? _l : "unnamed"}"): startSample=${clip.startSample}, durationSamples=${clip.durationSamples}, offsetSamples=${clip.offsetSamples}`
|
|
117
|
+
);
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
const track = __spreadProps(__spreadValues({}, (0, import_core.createTrack)({
|
|
121
|
+
name: config.name || `Track ${index + 1}`,
|
|
122
|
+
clips: [clip],
|
|
123
|
+
muted: (_m = config.muted) != null ? _m : false,
|
|
124
|
+
soloed: (_n = config.soloed) != null ? _n : false,
|
|
125
|
+
volume: (_o = config.volume) != null ? _o : 1,
|
|
126
|
+
pan: (_p = config.pan) != null ? _p : 0,
|
|
127
|
+
color: config.color
|
|
128
|
+
})), {
|
|
129
|
+
effects: config.effects,
|
|
130
|
+
renderMode: config.renderMode,
|
|
131
|
+
spectrogramConfig: config.spectrogramConfig,
|
|
132
|
+
spectrogramColorMap: config.spectrogramColorMap
|
|
133
|
+
});
|
|
134
|
+
const existingIds = stableIds.get(index);
|
|
135
|
+
if (existingIds) {
|
|
136
|
+
track.id = existingIds.trackId;
|
|
137
|
+
track.clips[0] = __spreadProps(__spreadValues({}, track.clips[0]), { id: existingIds.clipId });
|
|
138
|
+
} else {
|
|
139
|
+
stableIds.set(index, { trackId: track.id, clipId: track.clips[0].id });
|
|
140
|
+
}
|
|
141
|
+
return track;
|
|
142
|
+
}
|
|
143
|
+
function useAudioTracks(configs, options = {}) {
|
|
144
|
+
const { immediate = false, progressive = false } = options;
|
|
145
|
+
const isImmediate = immediate || progressive;
|
|
146
|
+
const [loading, setLoading] = (0, import_react.useState)(true);
|
|
147
|
+
const [error, setError] = (0, import_react.useState)(null);
|
|
148
|
+
const [loadedCount, setLoadedCount] = (0, import_react.useState)(0);
|
|
149
|
+
const totalCount = configs.length;
|
|
150
|
+
const [loadedBuffers, setLoadedBuffers] = (0, import_react.useState)(/* @__PURE__ */ new Map());
|
|
151
|
+
const stableIdsRef = (0, import_react.useRef)(/* @__PURE__ */ new Map());
|
|
152
|
+
const contextSampleRateRef = (0, import_react.useRef)(48e3);
|
|
153
|
+
const derivedTracks = (0, import_react.useMemo)(() => {
|
|
154
|
+
if (!isImmediate) return null;
|
|
155
|
+
const result = [];
|
|
156
|
+
for (let i = 0; i < configs.length; i++) {
|
|
157
|
+
const track = buildTrackFromConfig(
|
|
158
|
+
configs[i],
|
|
159
|
+
i,
|
|
160
|
+
loadedBuffers.get(i),
|
|
161
|
+
stableIdsRef.current,
|
|
162
|
+
contextSampleRateRef.current
|
|
163
|
+
);
|
|
164
|
+
if (track) result.push(track);
|
|
165
|
+
}
|
|
166
|
+
return result;
|
|
167
|
+
}, [isImmediate, configs, loadedBuffers]);
|
|
168
|
+
const [tracks, setTracks] = (0, import_react.useState)(derivedTracks != null ? derivedTracks : []);
|
|
169
|
+
const prevDerivedRef = (0, import_react.useRef)(derivedTracks);
|
|
170
|
+
if (derivedTracks !== prevDerivedRef.current) {
|
|
171
|
+
prevDerivedRef.current = derivedTracks;
|
|
172
|
+
if (derivedTracks) setTracks(derivedTracks);
|
|
173
|
+
}
|
|
174
|
+
(0, import_react.useEffect)(() => {
|
|
175
|
+
if (configs.length === 0) {
|
|
176
|
+
setTracks([]);
|
|
177
|
+
setLoading(false);
|
|
178
|
+
setLoadedCount(0);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
let cancelled = false;
|
|
182
|
+
const abortController = new AbortController();
|
|
183
|
+
const loadTracks = () => __async(null, null, function* () {
|
|
184
|
+
try {
|
|
185
|
+
setLoading(true);
|
|
186
|
+
setError(null);
|
|
187
|
+
setLoadedCount(0);
|
|
188
|
+
if (isImmediate) {
|
|
189
|
+
setLoadedBuffers(/* @__PURE__ */ new Map());
|
|
190
|
+
}
|
|
191
|
+
const audioContext = Tone.getContext().rawContext;
|
|
192
|
+
contextSampleRateRef.current = audioContext.sampleRate;
|
|
193
|
+
const loadPromises = configs.map((config, index) => __async(null, null, function* () {
|
|
194
|
+
if (config.audioBuffer) {
|
|
195
|
+
if (isImmediate && !cancelled) {
|
|
196
|
+
setLoadedBuffers((prev) => {
|
|
197
|
+
const next = new Map(prev);
|
|
198
|
+
next.set(index, config.audioBuffer);
|
|
199
|
+
return next;
|
|
200
|
+
});
|
|
201
|
+
setLoadedCount((prev) => prev + 1);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
return buildTrackFromConfig(
|
|
205
|
+
config,
|
|
206
|
+
index,
|
|
207
|
+
config.audioBuffer,
|
|
208
|
+
stableIdsRef.current,
|
|
209
|
+
audioContext.sampleRate
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
if (!config.src && config.waveformData) {
|
|
213
|
+
if (isImmediate && !cancelled) {
|
|
214
|
+
setLoadedCount((prev) => prev + 1);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
return buildTrackFromConfig(
|
|
218
|
+
config,
|
|
219
|
+
index,
|
|
220
|
+
void 0,
|
|
221
|
+
stableIdsRef.current,
|
|
222
|
+
audioContext.sampleRate
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
if (!config.src) {
|
|
226
|
+
throw new Error(`Track ${index + 1}: Must provide src, audioBuffer, or waveformData`);
|
|
227
|
+
}
|
|
228
|
+
const response = yield fetch(config.src, { signal: abortController.signal });
|
|
229
|
+
if (!response.ok) {
|
|
230
|
+
throw new Error(`Failed to fetch ${config.src}: ${response.statusText}`);
|
|
231
|
+
}
|
|
232
|
+
const arrayBuffer = yield response.arrayBuffer();
|
|
233
|
+
const audioBuffer = yield audioContext.decodeAudioData(arrayBuffer);
|
|
234
|
+
if (!audioBuffer || !audioBuffer.sampleRate || !audioBuffer.duration) {
|
|
235
|
+
throw new Error(`Invalid audio buffer for ${config.src}`);
|
|
236
|
+
}
|
|
237
|
+
if (isImmediate && !cancelled) {
|
|
238
|
+
setLoadedBuffers((prev) => {
|
|
239
|
+
const next = new Map(prev);
|
|
240
|
+
next.set(index, audioBuffer);
|
|
241
|
+
return next;
|
|
242
|
+
});
|
|
243
|
+
setLoadedCount((prev) => prev + 1);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
return buildTrackFromConfig(
|
|
247
|
+
config,
|
|
248
|
+
index,
|
|
249
|
+
audioBuffer,
|
|
250
|
+
stableIdsRef.current,
|
|
251
|
+
audioContext.sampleRate
|
|
252
|
+
);
|
|
253
|
+
}));
|
|
254
|
+
const loadedTracks = yield Promise.all(loadPromises);
|
|
255
|
+
if (!cancelled) {
|
|
256
|
+
if (!isImmediate) {
|
|
257
|
+
const validTracks = loadedTracks.filter((t) => t != null);
|
|
258
|
+
setTracks(validTracks);
|
|
259
|
+
setLoadedCount(validTracks.length);
|
|
260
|
+
}
|
|
261
|
+
setLoading(false);
|
|
262
|
+
}
|
|
263
|
+
} catch (err) {
|
|
264
|
+
if (!cancelled) {
|
|
265
|
+
const errorMessage = err instanceof Error ? err.message : "Unknown error loading audio";
|
|
266
|
+
setError(errorMessage);
|
|
267
|
+
setLoading(false);
|
|
268
|
+
console.error(`[waveform-playlist] Error loading audio tracks: ${errorMessage}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
loadTracks();
|
|
273
|
+
return () => {
|
|
274
|
+
cancelled = true;
|
|
275
|
+
abortController.abort();
|
|
276
|
+
};
|
|
277
|
+
}, [configs, isImmediate]);
|
|
278
|
+
return { tracks, loading, error, loadedCount, totalCount };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// src/hooks/useAudioEffects.ts
|
|
282
|
+
var import_react2 = require("react");
|
|
283
|
+
var import_tone = require("tone");
|
|
284
|
+
var useMasterAnalyser = (fftSize = 256) => {
|
|
285
|
+
const analyserRef = (0, import_react2.useRef)(null);
|
|
286
|
+
const masterEffects = (0, import_react2.useCallback)(
|
|
287
|
+
(masterGainNode, destination, _isOffline) => {
|
|
288
|
+
const analyserNode = new import_tone.Analyser("fft", fftSize);
|
|
289
|
+
masterGainNode.connect(analyserNode);
|
|
290
|
+
masterGainNode.connect(destination);
|
|
291
|
+
analyserRef.current = analyserNode;
|
|
292
|
+
return function cleanup() {
|
|
293
|
+
analyserNode.dispose();
|
|
294
|
+
analyserRef.current = null;
|
|
295
|
+
};
|
|
296
|
+
},
|
|
297
|
+
[fftSize]
|
|
298
|
+
);
|
|
299
|
+
return { analyserRef, masterEffects };
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
// src/hooks/useDynamicEffects.ts
|
|
303
|
+
var import_react3 = require("react");
|
|
304
|
+
|
|
305
|
+
// src/effects/effectDefinitions.ts
|
|
306
|
+
var effectDefinitions = [
|
|
307
|
+
// === REVERB EFFECTS ===
|
|
308
|
+
{
|
|
309
|
+
id: "reverb",
|
|
310
|
+
name: "Reverb",
|
|
311
|
+
category: "reverb",
|
|
312
|
+
description: "Simple convolution reverb with adjustable decay time",
|
|
313
|
+
parameters: [
|
|
314
|
+
{
|
|
315
|
+
name: "decay",
|
|
316
|
+
label: "Decay",
|
|
317
|
+
type: "number",
|
|
318
|
+
min: 0.1,
|
|
319
|
+
max: 10,
|
|
320
|
+
step: 0.1,
|
|
321
|
+
default: 1.5,
|
|
322
|
+
unit: "s"
|
|
323
|
+
},
|
|
324
|
+
{ name: "wet", label: "Mix", type: "number", min: 0, max: 1, step: 0.01, default: 0.5 }
|
|
325
|
+
]
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
id: "freeverb",
|
|
329
|
+
name: "Freeverb",
|
|
330
|
+
category: "reverb",
|
|
331
|
+
description: "Classic Schroeder/Moorer reverb with room size and dampening",
|
|
332
|
+
parameters: [
|
|
333
|
+
{
|
|
334
|
+
name: "roomSize",
|
|
335
|
+
label: "Room Size",
|
|
336
|
+
type: "number",
|
|
337
|
+
min: 0,
|
|
338
|
+
max: 1,
|
|
339
|
+
step: 0.01,
|
|
340
|
+
default: 0.7
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
name: "dampening",
|
|
344
|
+
label: "Dampening",
|
|
345
|
+
type: "number",
|
|
346
|
+
min: 0,
|
|
347
|
+
max: 1e4,
|
|
348
|
+
step: 100,
|
|
349
|
+
default: 3e3,
|
|
350
|
+
unit: "Hz"
|
|
351
|
+
},
|
|
352
|
+
{ name: "wet", label: "Mix", type: "number", min: 0, max: 1, step: 0.01, default: 0.5 }
|
|
353
|
+
]
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
id: "jcReverb",
|
|
357
|
+
name: "JC Reverb",
|
|
358
|
+
category: "reverb",
|
|
359
|
+
description: "Attempt at Roland JC-120 chorus reverb emulation",
|
|
360
|
+
parameters: [
|
|
361
|
+
{
|
|
362
|
+
name: "roomSize",
|
|
363
|
+
label: "Room Size",
|
|
364
|
+
type: "number",
|
|
365
|
+
min: 0,
|
|
366
|
+
max: 1,
|
|
367
|
+
step: 0.01,
|
|
368
|
+
default: 0.5
|
|
369
|
+
},
|
|
370
|
+
{ name: "wet", label: "Mix", type: "number", min: 0, max: 1, step: 0.01, default: 0.5 }
|
|
371
|
+
]
|
|
372
|
+
},
|
|
373
|
+
// === DELAY EFFECTS ===
|
|
374
|
+
{
|
|
375
|
+
id: "feedbackDelay",
|
|
376
|
+
name: "Feedback Delay",
|
|
377
|
+
category: "delay",
|
|
378
|
+
description: "Delay line with feedback for echo effects",
|
|
379
|
+
parameters: [
|
|
380
|
+
{
|
|
381
|
+
name: "delayTime",
|
|
382
|
+
label: "Delay Time",
|
|
383
|
+
type: "number",
|
|
384
|
+
min: 0,
|
|
385
|
+
max: 1,
|
|
386
|
+
step: 0.01,
|
|
387
|
+
default: 0.25,
|
|
388
|
+
unit: "s"
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
name: "feedback",
|
|
392
|
+
label: "Feedback",
|
|
393
|
+
type: "number",
|
|
394
|
+
min: 0,
|
|
395
|
+
max: 0.95,
|
|
396
|
+
step: 0.01,
|
|
397
|
+
default: 0.5
|
|
398
|
+
},
|
|
399
|
+
{ name: "wet", label: "Mix", type: "number", min: 0, max: 1, step: 0.01, default: 0.5 }
|
|
400
|
+
]
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
id: "pingPongDelay",
|
|
404
|
+
name: "Ping Pong Delay",
|
|
405
|
+
category: "delay",
|
|
406
|
+
description: "Stereo delay bouncing between left and right channels",
|
|
407
|
+
parameters: [
|
|
408
|
+
{
|
|
409
|
+
name: "delayTime",
|
|
410
|
+
label: "Delay Time",
|
|
411
|
+
type: "number",
|
|
412
|
+
min: 0,
|
|
413
|
+
max: 1,
|
|
414
|
+
step: 0.01,
|
|
415
|
+
default: 0.25,
|
|
416
|
+
unit: "s"
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
name: "feedback",
|
|
420
|
+
label: "Feedback",
|
|
421
|
+
type: "number",
|
|
422
|
+
min: 0,
|
|
423
|
+
max: 0.95,
|
|
424
|
+
step: 0.01,
|
|
425
|
+
default: 0.5
|
|
426
|
+
},
|
|
427
|
+
{ name: "wet", label: "Mix", type: "number", min: 0, max: 1, step: 0.01, default: 0.5 }
|
|
428
|
+
]
|
|
429
|
+
},
|
|
430
|
+
// === MODULATION EFFECTS ===
|
|
431
|
+
{
|
|
432
|
+
id: "chorus",
|
|
433
|
+
name: "Chorus",
|
|
434
|
+
category: "modulation",
|
|
435
|
+
description: "Creates thickness by layering detuned copies of the signal",
|
|
436
|
+
parameters: [
|
|
437
|
+
{
|
|
438
|
+
name: "frequency",
|
|
439
|
+
label: "Rate",
|
|
440
|
+
type: "number",
|
|
441
|
+
min: 0.1,
|
|
442
|
+
max: 10,
|
|
443
|
+
step: 0.1,
|
|
444
|
+
default: 1.5,
|
|
445
|
+
unit: "Hz"
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
name: "delayTime",
|
|
449
|
+
label: "Delay",
|
|
450
|
+
type: "number",
|
|
451
|
+
min: 0,
|
|
452
|
+
max: 20,
|
|
453
|
+
step: 0.5,
|
|
454
|
+
default: 3.5,
|
|
455
|
+
unit: "ms"
|
|
456
|
+
},
|
|
457
|
+
{ name: "depth", label: "Depth", type: "number", min: 0, max: 1, step: 0.01, default: 0.7 },
|
|
458
|
+
{ name: "wet", label: "Mix", type: "number", min: 0, max: 1, step: 0.01, default: 0.5 }
|
|
459
|
+
]
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
id: "phaser",
|
|
463
|
+
name: "Phaser",
|
|
464
|
+
category: "modulation",
|
|
465
|
+
description: "Classic phaser effect using allpass filters",
|
|
466
|
+
parameters: [
|
|
467
|
+
{
|
|
468
|
+
name: "frequency",
|
|
469
|
+
label: "Rate",
|
|
470
|
+
type: "number",
|
|
471
|
+
min: 0.1,
|
|
472
|
+
max: 10,
|
|
473
|
+
step: 0.1,
|
|
474
|
+
default: 0.5,
|
|
475
|
+
unit: "Hz"
|
|
476
|
+
},
|
|
477
|
+
{ name: "octaves", label: "Octaves", type: "number", min: 1, max: 6, step: 1, default: 3 },
|
|
478
|
+
{
|
|
479
|
+
name: "baseFrequency",
|
|
480
|
+
label: "Base Freq",
|
|
481
|
+
type: "number",
|
|
482
|
+
min: 100,
|
|
483
|
+
max: 2e3,
|
|
484
|
+
step: 10,
|
|
485
|
+
default: 350,
|
|
486
|
+
unit: "Hz"
|
|
487
|
+
},
|
|
488
|
+
{ name: "wet", label: "Mix", type: "number", min: 0, max: 1, step: 0.01, default: 0.5 }
|
|
489
|
+
]
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
id: "tremolo",
|
|
493
|
+
name: "Tremolo",
|
|
494
|
+
category: "modulation",
|
|
495
|
+
description: "Rhythmic volume modulation",
|
|
496
|
+
parameters: [
|
|
497
|
+
{
|
|
498
|
+
name: "frequency",
|
|
499
|
+
label: "Rate",
|
|
500
|
+
type: "number",
|
|
501
|
+
min: 0.1,
|
|
502
|
+
max: 20,
|
|
503
|
+
step: 0.1,
|
|
504
|
+
default: 4,
|
|
505
|
+
unit: "Hz"
|
|
506
|
+
},
|
|
507
|
+
{ name: "depth", label: "Depth", type: "number", min: 0, max: 1, step: 0.01, default: 0.5 },
|
|
508
|
+
{ name: "wet", label: "Mix", type: "number", min: 0, max: 1, step: 0.01, default: 1 }
|
|
509
|
+
]
|
|
510
|
+
},
|
|
511
|
+
{
|
|
512
|
+
id: "vibrato",
|
|
513
|
+
name: "Vibrato",
|
|
514
|
+
category: "modulation",
|
|
515
|
+
description: "Pitch modulation effect",
|
|
516
|
+
parameters: [
|
|
517
|
+
{
|
|
518
|
+
name: "frequency",
|
|
519
|
+
label: "Rate",
|
|
520
|
+
type: "number",
|
|
521
|
+
min: 0.1,
|
|
522
|
+
max: 20,
|
|
523
|
+
step: 0.1,
|
|
524
|
+
default: 5,
|
|
525
|
+
unit: "Hz"
|
|
526
|
+
},
|
|
527
|
+
{ name: "depth", label: "Depth", type: "number", min: 0, max: 1, step: 0.01, default: 0.1 },
|
|
528
|
+
{ name: "wet", label: "Mix", type: "number", min: 0, max: 1, step: 0.01, default: 1 }
|
|
529
|
+
]
|
|
530
|
+
},
|
|
531
|
+
{
|
|
532
|
+
id: "autoPanner",
|
|
533
|
+
name: "Auto Panner",
|
|
534
|
+
category: "modulation",
|
|
535
|
+
description: "Automatic left-right panning",
|
|
536
|
+
parameters: [
|
|
537
|
+
{
|
|
538
|
+
name: "frequency",
|
|
539
|
+
label: "Rate",
|
|
540
|
+
type: "number",
|
|
541
|
+
min: 0.1,
|
|
542
|
+
max: 10,
|
|
543
|
+
step: 0.1,
|
|
544
|
+
default: 1,
|
|
545
|
+
unit: "Hz"
|
|
546
|
+
},
|
|
547
|
+
{ name: "depth", label: "Depth", type: "number", min: 0, max: 1, step: 0.01, default: 1 },
|
|
548
|
+
{ name: "wet", label: "Mix", type: "number", min: 0, max: 1, step: 0.01, default: 1 }
|
|
549
|
+
]
|
|
550
|
+
},
|
|
551
|
+
// === FILTER EFFECTS ===
|
|
552
|
+
{
|
|
553
|
+
id: "autoFilter",
|
|
554
|
+
name: "Auto Filter",
|
|
555
|
+
category: "filter",
|
|
556
|
+
description: "Automated filter sweep with LFO",
|
|
557
|
+
parameters: [
|
|
558
|
+
{
|
|
559
|
+
name: "frequency",
|
|
560
|
+
label: "Rate",
|
|
561
|
+
type: "number",
|
|
562
|
+
min: 0.1,
|
|
563
|
+
max: 10,
|
|
564
|
+
step: 0.1,
|
|
565
|
+
default: 1,
|
|
566
|
+
unit: "Hz"
|
|
567
|
+
},
|
|
568
|
+
{
|
|
569
|
+
name: "baseFrequency",
|
|
570
|
+
label: "Base Freq",
|
|
571
|
+
type: "number",
|
|
572
|
+
min: 20,
|
|
573
|
+
max: 2e3,
|
|
574
|
+
step: 10,
|
|
575
|
+
default: 200,
|
|
576
|
+
unit: "Hz"
|
|
577
|
+
},
|
|
578
|
+
{
|
|
579
|
+
name: "octaves",
|
|
580
|
+
label: "Octaves",
|
|
581
|
+
type: "number",
|
|
582
|
+
min: 0.5,
|
|
583
|
+
max: 8,
|
|
584
|
+
step: 0.5,
|
|
585
|
+
default: 2.6
|
|
586
|
+
},
|
|
587
|
+
{ name: "depth", label: "Depth", type: "number", min: 0, max: 1, step: 0.01, default: 1 },
|
|
588
|
+
{ name: "wet", label: "Mix", type: "number", min: 0, max: 1, step: 0.01, default: 1 }
|
|
589
|
+
]
|
|
590
|
+
},
|
|
591
|
+
{
|
|
592
|
+
id: "autoWah",
|
|
593
|
+
name: "Auto Wah",
|
|
594
|
+
category: "filter",
|
|
595
|
+
description: "Envelope follower filter effect",
|
|
596
|
+
parameters: [
|
|
597
|
+
{
|
|
598
|
+
name: "baseFrequency",
|
|
599
|
+
label: "Base Freq",
|
|
600
|
+
type: "number",
|
|
601
|
+
min: 20,
|
|
602
|
+
max: 500,
|
|
603
|
+
step: 10,
|
|
604
|
+
default: 100,
|
|
605
|
+
unit: "Hz"
|
|
606
|
+
},
|
|
607
|
+
{ name: "octaves", label: "Octaves", type: "number", min: 1, max: 8, step: 1, default: 6 },
|
|
608
|
+
{
|
|
609
|
+
name: "sensitivity",
|
|
610
|
+
label: "Sensitivity",
|
|
611
|
+
type: "number",
|
|
612
|
+
min: -40,
|
|
613
|
+
max: 0,
|
|
614
|
+
step: 1,
|
|
615
|
+
default: 0,
|
|
616
|
+
unit: "dB"
|
|
617
|
+
},
|
|
618
|
+
{ name: "wet", label: "Mix", type: "number", min: 0, max: 1, step: 0.01, default: 1 }
|
|
619
|
+
]
|
|
620
|
+
},
|
|
621
|
+
{
|
|
622
|
+
id: "eq3",
|
|
623
|
+
name: "3-Band EQ",
|
|
624
|
+
category: "filter",
|
|
625
|
+
description: "Three band equalizer with low, mid, and high controls",
|
|
626
|
+
parameters: [
|
|
627
|
+
{
|
|
628
|
+
name: "low",
|
|
629
|
+
label: "Low",
|
|
630
|
+
type: "number",
|
|
631
|
+
min: -24,
|
|
632
|
+
max: 24,
|
|
633
|
+
step: 0.5,
|
|
634
|
+
default: 0,
|
|
635
|
+
unit: "dB"
|
|
636
|
+
},
|
|
637
|
+
{
|
|
638
|
+
name: "mid",
|
|
639
|
+
label: "Mid",
|
|
640
|
+
type: "number",
|
|
641
|
+
min: -24,
|
|
642
|
+
max: 24,
|
|
643
|
+
step: 0.5,
|
|
644
|
+
default: 0,
|
|
645
|
+
unit: "dB"
|
|
646
|
+
},
|
|
647
|
+
{
|
|
648
|
+
name: "high",
|
|
649
|
+
label: "High",
|
|
650
|
+
type: "number",
|
|
651
|
+
min: -24,
|
|
652
|
+
max: 24,
|
|
653
|
+
step: 0.5,
|
|
654
|
+
default: 0,
|
|
655
|
+
unit: "dB"
|
|
656
|
+
},
|
|
657
|
+
{
|
|
658
|
+
name: "lowFrequency",
|
|
659
|
+
label: "Low Freq",
|
|
660
|
+
type: "number",
|
|
661
|
+
min: 20,
|
|
662
|
+
max: 500,
|
|
663
|
+
step: 10,
|
|
664
|
+
default: 400,
|
|
665
|
+
unit: "Hz"
|
|
666
|
+
},
|
|
667
|
+
{
|
|
668
|
+
name: "highFrequency",
|
|
669
|
+
label: "High Freq",
|
|
670
|
+
type: "number",
|
|
671
|
+
min: 1e3,
|
|
672
|
+
max: 1e4,
|
|
673
|
+
step: 100,
|
|
674
|
+
default: 2500,
|
|
675
|
+
unit: "Hz"
|
|
676
|
+
}
|
|
677
|
+
]
|
|
678
|
+
},
|
|
679
|
+
// === DISTORTION EFFECTS ===
|
|
680
|
+
{
|
|
681
|
+
id: "distortion",
|
|
682
|
+
name: "Distortion",
|
|
683
|
+
category: "distortion",
|
|
684
|
+
description: "Wave shaping distortion effect",
|
|
685
|
+
parameters: [
|
|
686
|
+
{
|
|
687
|
+
name: "distortion",
|
|
688
|
+
label: "Drive",
|
|
689
|
+
type: "number",
|
|
690
|
+
min: 0,
|
|
691
|
+
max: 1,
|
|
692
|
+
step: 0.01,
|
|
693
|
+
default: 0.4
|
|
694
|
+
},
|
|
695
|
+
{ name: "wet", label: "Mix", type: "number", min: 0, max: 1, step: 0.01, default: 1 }
|
|
696
|
+
]
|
|
697
|
+
},
|
|
698
|
+
{
|
|
699
|
+
id: "bitCrusher",
|
|
700
|
+
name: "Bit Crusher",
|
|
701
|
+
category: "distortion",
|
|
702
|
+
description: "Reduces bit depth for lo-fi digital texture",
|
|
703
|
+
parameters: [
|
|
704
|
+
{ name: "bits", label: "Bits", type: "number", min: 1, max: 16, step: 1, default: 4 },
|
|
705
|
+
{ name: "wet", label: "Mix", type: "number", min: 0, max: 1, step: 0.01, default: 1 }
|
|
706
|
+
]
|
|
707
|
+
},
|
|
708
|
+
{
|
|
709
|
+
id: "chebyshev",
|
|
710
|
+
name: "Chebyshev",
|
|
711
|
+
category: "distortion",
|
|
712
|
+
description: "Waveshaping distortion using Chebyshev polynomials",
|
|
713
|
+
parameters: [
|
|
714
|
+
{ name: "order", label: "Order", type: "number", min: 1, max: 100, step: 1, default: 50 },
|
|
715
|
+
{ name: "wet", label: "Mix", type: "number", min: 0, max: 1, step: 0.01, default: 1 }
|
|
716
|
+
]
|
|
717
|
+
},
|
|
718
|
+
// === DYNAMICS EFFECTS ===
|
|
719
|
+
{
|
|
720
|
+
id: "compressor",
|
|
721
|
+
name: "Compressor",
|
|
722
|
+
category: "dynamics",
|
|
723
|
+
description: "Dynamic range compressor",
|
|
724
|
+
parameters: [
|
|
725
|
+
{
|
|
726
|
+
name: "threshold",
|
|
727
|
+
label: "Threshold",
|
|
728
|
+
type: "number",
|
|
729
|
+
min: -60,
|
|
730
|
+
max: 0,
|
|
731
|
+
step: 1,
|
|
732
|
+
default: -24,
|
|
733
|
+
unit: "dB"
|
|
734
|
+
},
|
|
735
|
+
{ name: "ratio", label: "Ratio", type: "number", min: 1, max: 20, step: 0.5, default: 4 },
|
|
736
|
+
{
|
|
737
|
+
name: "attack",
|
|
738
|
+
label: "Attack",
|
|
739
|
+
type: "number",
|
|
740
|
+
min: 0,
|
|
741
|
+
max: 1,
|
|
742
|
+
step: 1e-3,
|
|
743
|
+
default: 3e-3,
|
|
744
|
+
unit: "s"
|
|
745
|
+
},
|
|
746
|
+
{
|
|
747
|
+
name: "release",
|
|
748
|
+
label: "Release",
|
|
749
|
+
type: "number",
|
|
750
|
+
min: 0,
|
|
751
|
+
max: 1,
|
|
752
|
+
step: 0.01,
|
|
753
|
+
default: 0.25,
|
|
754
|
+
unit: "s"
|
|
755
|
+
},
|
|
756
|
+
{
|
|
757
|
+
name: "knee",
|
|
758
|
+
label: "Knee",
|
|
759
|
+
type: "number",
|
|
760
|
+
min: 0,
|
|
761
|
+
max: 40,
|
|
762
|
+
step: 1,
|
|
763
|
+
default: 30,
|
|
764
|
+
unit: "dB"
|
|
765
|
+
}
|
|
766
|
+
]
|
|
767
|
+
},
|
|
768
|
+
{
|
|
769
|
+
id: "limiter",
|
|
770
|
+
name: "Limiter",
|
|
771
|
+
category: "dynamics",
|
|
772
|
+
description: "Hard limiter to prevent clipping",
|
|
773
|
+
parameters: [
|
|
774
|
+
{
|
|
775
|
+
name: "threshold",
|
|
776
|
+
label: "Threshold",
|
|
777
|
+
type: "number",
|
|
778
|
+
min: -12,
|
|
779
|
+
max: 0,
|
|
780
|
+
step: 0.5,
|
|
781
|
+
default: -6,
|
|
782
|
+
unit: "dB"
|
|
783
|
+
}
|
|
784
|
+
]
|
|
785
|
+
},
|
|
786
|
+
{
|
|
787
|
+
id: "gate",
|
|
788
|
+
name: "Gate",
|
|
789
|
+
category: "dynamics",
|
|
790
|
+
description: "Noise gate to silence signal below threshold",
|
|
791
|
+
parameters: [
|
|
792
|
+
{
|
|
793
|
+
name: "threshold",
|
|
794
|
+
label: "Threshold",
|
|
795
|
+
type: "number",
|
|
796
|
+
min: -100,
|
|
797
|
+
max: 0,
|
|
798
|
+
step: 1,
|
|
799
|
+
default: -40,
|
|
800
|
+
unit: "dB"
|
|
801
|
+
},
|
|
802
|
+
{
|
|
803
|
+
name: "attack",
|
|
804
|
+
label: "Attack",
|
|
805
|
+
type: "number",
|
|
806
|
+
min: 0,
|
|
807
|
+
max: 0.3,
|
|
808
|
+
step: 1e-3,
|
|
809
|
+
default: 1e-3,
|
|
810
|
+
unit: "s"
|
|
811
|
+
},
|
|
812
|
+
{
|
|
813
|
+
name: "release",
|
|
814
|
+
label: "Release",
|
|
815
|
+
type: "number",
|
|
816
|
+
min: 0,
|
|
817
|
+
max: 0.5,
|
|
818
|
+
step: 0.01,
|
|
819
|
+
default: 0.1,
|
|
820
|
+
unit: "s"
|
|
821
|
+
}
|
|
822
|
+
]
|
|
823
|
+
},
|
|
824
|
+
// === SPATIAL EFFECTS ===
|
|
825
|
+
{
|
|
826
|
+
id: "stereoWidener",
|
|
827
|
+
name: "Stereo Widener",
|
|
828
|
+
category: "spatial",
|
|
829
|
+
description: "Expands or narrows the stereo image",
|
|
830
|
+
parameters: [
|
|
831
|
+
{ name: "width", label: "Width", type: "number", min: 0, max: 1, step: 0.01, default: 0.5 }
|
|
832
|
+
]
|
|
833
|
+
}
|
|
834
|
+
];
|
|
835
|
+
var getEffectDefinition = (id) => {
|
|
836
|
+
return effectDefinitions.find((def) => def.id === id);
|
|
837
|
+
};
|
|
838
|
+
var getEffectsByCategory = (category) => {
|
|
839
|
+
return effectDefinitions.filter((def) => def.category === category);
|
|
840
|
+
};
|
|
841
|
+
var effectCategories = [
|
|
842
|
+
{ id: "reverb", name: "Reverb" },
|
|
843
|
+
{ id: "delay", name: "Delay" },
|
|
844
|
+
{ id: "modulation", name: "Modulation" },
|
|
845
|
+
{ id: "filter", name: "Filter" },
|
|
846
|
+
{ id: "distortion", name: "Distortion" },
|
|
847
|
+
{ id: "dynamics", name: "Dynamics" },
|
|
848
|
+
{ id: "spatial", name: "Spatial" }
|
|
849
|
+
];
|
|
850
|
+
|
|
851
|
+
// src/effects/effectFactory.ts
|
|
852
|
+
var import_tone2 = require("tone");
|
|
853
|
+
function asEffectConstructor(ctor) {
|
|
854
|
+
return ctor;
|
|
855
|
+
}
|
|
856
|
+
var effectConstructors = {
|
|
857
|
+
reverb: asEffectConstructor(import_tone2.Reverb),
|
|
858
|
+
freeverb: asEffectConstructor(import_tone2.Freeverb),
|
|
859
|
+
jcReverb: asEffectConstructor(import_tone2.JCReverb),
|
|
860
|
+
feedbackDelay: asEffectConstructor(import_tone2.FeedbackDelay),
|
|
861
|
+
pingPongDelay: asEffectConstructor(import_tone2.PingPongDelay),
|
|
862
|
+
chorus: asEffectConstructor(import_tone2.Chorus),
|
|
863
|
+
phaser: asEffectConstructor(import_tone2.Phaser),
|
|
864
|
+
tremolo: asEffectConstructor(import_tone2.Tremolo),
|
|
865
|
+
vibrato: asEffectConstructor(import_tone2.Vibrato),
|
|
866
|
+
autoPanner: asEffectConstructor(import_tone2.AutoPanner),
|
|
867
|
+
autoFilter: asEffectConstructor(import_tone2.AutoFilter),
|
|
868
|
+
autoWah: asEffectConstructor(import_tone2.AutoWah),
|
|
869
|
+
eq3: asEffectConstructor(import_tone2.EQ3),
|
|
870
|
+
distortion: asEffectConstructor(import_tone2.Distortion),
|
|
871
|
+
bitCrusher: asEffectConstructor(import_tone2.BitCrusher),
|
|
872
|
+
chebyshev: asEffectConstructor(import_tone2.Chebyshev),
|
|
873
|
+
compressor: asEffectConstructor(import_tone2.Compressor),
|
|
874
|
+
limiter: asEffectConstructor(import_tone2.Limiter),
|
|
875
|
+
gate: asEffectConstructor(import_tone2.Gate),
|
|
876
|
+
stereoWidener: asEffectConstructor(import_tone2.StereoWidener)
|
|
877
|
+
};
|
|
878
|
+
var instanceCounter = 0;
|
|
879
|
+
var generateInstanceId = () => {
|
|
880
|
+
return `effect_${Date.now()}_${++instanceCounter}`;
|
|
881
|
+
};
|
|
882
|
+
function createEffectInstance(definition, initialParams) {
|
|
883
|
+
const Constructor = effectConstructors[definition.id];
|
|
884
|
+
if (!Constructor) {
|
|
885
|
+
throw new Error(`Unknown effect type: ${definition.id}`);
|
|
886
|
+
}
|
|
887
|
+
const options = {};
|
|
888
|
+
definition.parameters.forEach((param) => {
|
|
889
|
+
var _a;
|
|
890
|
+
const value = (_a = initialParams == null ? void 0 : initialParams[param.name]) != null ? _a : param.default;
|
|
891
|
+
options[param.name] = value;
|
|
892
|
+
});
|
|
893
|
+
const effect = new Constructor(options);
|
|
894
|
+
const instanceId = generateInstanceId();
|
|
895
|
+
const effectRecord = effect;
|
|
896
|
+
return {
|
|
897
|
+
effect,
|
|
898
|
+
id: definition.id,
|
|
899
|
+
instanceId,
|
|
900
|
+
dispose() {
|
|
901
|
+
try {
|
|
902
|
+
effect.disconnect();
|
|
903
|
+
effect.dispose();
|
|
904
|
+
} catch (e) {
|
|
905
|
+
console.warn(
|
|
906
|
+
`[waveform-playlist] Error disposing effect "${definition.id}" (${instanceId}):`,
|
|
907
|
+
e
|
|
908
|
+
);
|
|
909
|
+
}
|
|
910
|
+
},
|
|
911
|
+
setParameter(name, value) {
|
|
912
|
+
const prop = effectRecord[name];
|
|
913
|
+
if (name === "wet") {
|
|
914
|
+
const wetProp = effectRecord["wet"];
|
|
915
|
+
if (wetProp && typeof wetProp === "object" && "value" in wetProp) {
|
|
916
|
+
wetProp.value = value;
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
if (prop !== void 0) {
|
|
921
|
+
if (prop && typeof prop === "object" && "value" in prop) {
|
|
922
|
+
prop.value = value;
|
|
923
|
+
} else {
|
|
924
|
+
effectRecord[name] = value;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
},
|
|
928
|
+
getParameter(name) {
|
|
929
|
+
if (name === "wet") {
|
|
930
|
+
const wetProp = effectRecord["wet"];
|
|
931
|
+
if (wetProp && typeof wetProp === "object" && "value" in wetProp) {
|
|
932
|
+
return wetProp.value;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
const prop = effectRecord[name];
|
|
936
|
+
if (prop !== void 0) {
|
|
937
|
+
if (prop && typeof prop === "object" && "value" in prop) {
|
|
938
|
+
return prop.value;
|
|
939
|
+
}
|
|
940
|
+
return prop;
|
|
941
|
+
}
|
|
942
|
+
return void 0;
|
|
943
|
+
},
|
|
944
|
+
connect(destination) {
|
|
945
|
+
effect.connect(destination);
|
|
946
|
+
},
|
|
947
|
+
disconnect() {
|
|
948
|
+
try {
|
|
949
|
+
effect.disconnect();
|
|
950
|
+
} catch (e) {
|
|
951
|
+
console.warn(
|
|
952
|
+
`[waveform-playlist] Error disconnecting effect "${definition.id}" (${instanceId}):`,
|
|
953
|
+
e
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
function createEffectChain(effects) {
|
|
960
|
+
if (effects.length === 0) {
|
|
961
|
+
throw new Error("Cannot create effect chain with no effects");
|
|
962
|
+
}
|
|
963
|
+
for (let i = 0; i < effects.length - 1; i++) {
|
|
964
|
+
effects[i].effect.connect(effects[i + 1].effect);
|
|
965
|
+
}
|
|
966
|
+
return {
|
|
967
|
+
input: effects[0].effect,
|
|
968
|
+
output: effects[effects.length - 1].effect,
|
|
969
|
+
dispose() {
|
|
970
|
+
effects.forEach((e) => e.dispose());
|
|
971
|
+
}
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// src/hooks/useDynamicEffects.ts
|
|
976
|
+
var import_tone3 = require("tone");
|
|
977
|
+
function useDynamicEffects(fftSize = 256) {
|
|
978
|
+
const [activeEffects, setActiveEffects] = (0, import_react3.useState)([]);
|
|
979
|
+
const activeEffectsRef = (0, import_react3.useRef)(activeEffects);
|
|
980
|
+
activeEffectsRef.current = activeEffects;
|
|
981
|
+
const effectInstancesRef = (0, import_react3.useRef)(/* @__PURE__ */ new Map());
|
|
982
|
+
const analyserRef = (0, import_react3.useRef)(null);
|
|
983
|
+
const graphNodesRef = (0, import_react3.useRef)(null);
|
|
984
|
+
const rebuildChain = (0, import_react3.useCallback)((effects) => {
|
|
985
|
+
const nodes = graphNodesRef.current;
|
|
986
|
+
if (!nodes) return;
|
|
987
|
+
const { masterGainNode, destination, analyserNode } = nodes;
|
|
988
|
+
try {
|
|
989
|
+
masterGainNode.disconnect();
|
|
990
|
+
} catch (e) {
|
|
991
|
+
console.warn("[waveform-playlist] Error disconnecting master effects chain:", e);
|
|
992
|
+
}
|
|
993
|
+
const instances = effects.map((ae) => effectInstancesRef.current.get(ae.instanceId)).filter((inst) => inst !== void 0);
|
|
994
|
+
if (instances.length === 0) {
|
|
995
|
+
masterGainNode.connect(analyserNode);
|
|
996
|
+
analyserNode.connect(destination);
|
|
997
|
+
} else {
|
|
998
|
+
let currentNode = masterGainNode;
|
|
999
|
+
instances.forEach((inst) => {
|
|
1000
|
+
try {
|
|
1001
|
+
inst.disconnect();
|
|
1002
|
+
} catch (e) {
|
|
1003
|
+
console.warn(`[waveform-playlist] Error disconnecting effect "${inst.id}":`, e);
|
|
1004
|
+
}
|
|
1005
|
+
currentNode.connect(inst.effect);
|
|
1006
|
+
currentNode = inst.effect;
|
|
1007
|
+
});
|
|
1008
|
+
currentNode.connect(analyserNode);
|
|
1009
|
+
analyserNode.connect(destination);
|
|
1010
|
+
}
|
|
1011
|
+
}, []);
|
|
1012
|
+
const addEffect = (0, import_react3.useCallback)((effectId) => {
|
|
1013
|
+
const definition = getEffectDefinition(effectId);
|
|
1014
|
+
if (!definition) {
|
|
1015
|
+
console.error(`Unknown effect: ${effectId}`);
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
const params = {};
|
|
1019
|
+
definition.parameters.forEach((p) => {
|
|
1020
|
+
params[p.name] = p.default;
|
|
1021
|
+
});
|
|
1022
|
+
const instance = createEffectInstance(definition, params);
|
|
1023
|
+
effectInstancesRef.current.set(instance.instanceId, instance);
|
|
1024
|
+
const newActiveEffect = {
|
|
1025
|
+
instanceId: instance.instanceId,
|
|
1026
|
+
effectId: definition.id,
|
|
1027
|
+
definition,
|
|
1028
|
+
params,
|
|
1029
|
+
bypassed: false
|
|
1030
|
+
};
|
|
1031
|
+
setActiveEffects((prev) => [...prev, newActiveEffect]);
|
|
1032
|
+
}, []);
|
|
1033
|
+
const removeEffect = (0, import_react3.useCallback)((instanceId) => {
|
|
1034
|
+
const instance = effectInstancesRef.current.get(instanceId);
|
|
1035
|
+
if (instance) {
|
|
1036
|
+
instance.dispose();
|
|
1037
|
+
effectInstancesRef.current.delete(instanceId);
|
|
1038
|
+
}
|
|
1039
|
+
setActiveEffects((prev) => prev.filter((e) => e.instanceId !== instanceId));
|
|
1040
|
+
}, []);
|
|
1041
|
+
const updateParameter = (0, import_react3.useCallback)(
|
|
1042
|
+
(instanceId, paramName, value) => {
|
|
1043
|
+
const instance = effectInstancesRef.current.get(instanceId);
|
|
1044
|
+
if (instance) {
|
|
1045
|
+
instance.setParameter(paramName, value);
|
|
1046
|
+
}
|
|
1047
|
+
setActiveEffects(
|
|
1048
|
+
(prev) => prev.map(
|
|
1049
|
+
(e) => e.instanceId === instanceId ? __spreadProps(__spreadValues({}, e), { params: __spreadProps(__spreadValues({}, e.params), { [paramName]: value }) }) : e
|
|
1050
|
+
)
|
|
1051
|
+
);
|
|
1052
|
+
},
|
|
1053
|
+
[]
|
|
1054
|
+
);
|
|
1055
|
+
const toggleBypass = (0, import_react3.useCallback)((instanceId) => {
|
|
1056
|
+
var _a;
|
|
1057
|
+
const effect = activeEffectsRef.current.find((e) => e.instanceId === instanceId);
|
|
1058
|
+
if (!effect) return;
|
|
1059
|
+
const newBypassed = !effect.bypassed;
|
|
1060
|
+
const instance = effectInstancesRef.current.get(instanceId);
|
|
1061
|
+
if (instance) {
|
|
1062
|
+
const originalWet = (_a = effect.params.wet) != null ? _a : 1;
|
|
1063
|
+
instance.setParameter("wet", newBypassed ? 0 : originalWet);
|
|
1064
|
+
}
|
|
1065
|
+
setActiveEffects(
|
|
1066
|
+
(prev) => prev.map((e) => e.instanceId === instanceId ? __spreadProps(__spreadValues({}, e), { bypassed: newBypassed }) : e)
|
|
1067
|
+
);
|
|
1068
|
+
}, []);
|
|
1069
|
+
const reorderEffects = (0, import_react3.useCallback)((fromIndex, toIndex) => {
|
|
1070
|
+
setActiveEffects((prev) => {
|
|
1071
|
+
const newEffects = [...prev];
|
|
1072
|
+
const [removed] = newEffects.splice(fromIndex, 1);
|
|
1073
|
+
newEffects.splice(toIndex, 0, removed);
|
|
1074
|
+
return newEffects;
|
|
1075
|
+
});
|
|
1076
|
+
}, []);
|
|
1077
|
+
const clearAllEffects = (0, import_react3.useCallback)(() => {
|
|
1078
|
+
effectInstancesRef.current.forEach((inst) => inst.dispose());
|
|
1079
|
+
effectInstancesRef.current.clear();
|
|
1080
|
+
setActiveEffects([]);
|
|
1081
|
+
}, []);
|
|
1082
|
+
(0, import_react3.useEffect)(() => {
|
|
1083
|
+
rebuildChain(activeEffects);
|
|
1084
|
+
}, [activeEffects, rebuildChain]);
|
|
1085
|
+
const masterEffects = (0, import_react3.useCallback)(
|
|
1086
|
+
(masterGainNode, destination, _isOffline) => {
|
|
1087
|
+
const analyserNode = new import_tone3.Analyser("fft", fftSize);
|
|
1088
|
+
analyserRef.current = analyserNode;
|
|
1089
|
+
graphNodesRef.current = {
|
|
1090
|
+
masterGainNode,
|
|
1091
|
+
destination,
|
|
1092
|
+
analyserNode
|
|
1093
|
+
};
|
|
1094
|
+
const effects = activeEffectsRef.current;
|
|
1095
|
+
const instances = effects.map((ae) => effectInstancesRef.current.get(ae.instanceId)).filter((inst) => inst !== void 0);
|
|
1096
|
+
if (instances.length === 0) {
|
|
1097
|
+
masterGainNode.connect(analyserNode);
|
|
1098
|
+
analyserNode.connect(destination);
|
|
1099
|
+
} else {
|
|
1100
|
+
let currentNode = masterGainNode;
|
|
1101
|
+
instances.forEach((inst) => {
|
|
1102
|
+
currentNode.connect(inst.effect);
|
|
1103
|
+
currentNode = inst.effect;
|
|
1104
|
+
});
|
|
1105
|
+
currentNode.connect(analyserNode);
|
|
1106
|
+
analyserNode.connect(destination);
|
|
1107
|
+
}
|
|
1108
|
+
return function cleanup() {
|
|
1109
|
+
analyserNode.dispose();
|
|
1110
|
+
analyserRef.current = null;
|
|
1111
|
+
graphNodesRef.current = null;
|
|
1112
|
+
};
|
|
1113
|
+
},
|
|
1114
|
+
[fftSize]
|
|
1115
|
+
// Only fftSize - reads effects from ref
|
|
1116
|
+
);
|
|
1117
|
+
(0, import_react3.useEffect)(() => {
|
|
1118
|
+
const effectInstances = effectInstancesRef.current;
|
|
1119
|
+
return () => {
|
|
1120
|
+
effectInstances.forEach((inst) => inst.dispose());
|
|
1121
|
+
effectInstances.clear();
|
|
1122
|
+
};
|
|
1123
|
+
}, []);
|
|
1124
|
+
const createOfflineEffectsFunction = (0, import_react3.useCallback)(() => {
|
|
1125
|
+
const nonBypassedEffects = activeEffects.filter((e) => !e.bypassed);
|
|
1126
|
+
if (nonBypassedEffects.length === 0) {
|
|
1127
|
+
return void 0;
|
|
1128
|
+
}
|
|
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);
|
|
1144
|
+
}
|
|
1145
|
+
return function cleanup() {
|
|
1146
|
+
offlineInstances.forEach((inst) => inst.dispose());
|
|
1147
|
+
};
|
|
1148
|
+
};
|
|
1149
|
+
}, [activeEffects]);
|
|
1150
|
+
return {
|
|
1151
|
+
activeEffects,
|
|
1152
|
+
availableEffects: effectDefinitions,
|
|
1153
|
+
addEffect,
|
|
1154
|
+
removeEffect,
|
|
1155
|
+
updateParameter,
|
|
1156
|
+
toggleBypass,
|
|
1157
|
+
reorderEffects,
|
|
1158
|
+
clearAllEffects,
|
|
1159
|
+
masterEffects,
|
|
1160
|
+
createOfflineEffectsFunction,
|
|
1161
|
+
analyserRef
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// src/hooks/useTrackDynamicEffects.ts
|
|
1166
|
+
var import_react4 = require("react");
|
|
1167
|
+
function useTrackDynamicEffects() {
|
|
1168
|
+
const [trackEffectsState, setTrackEffectsState] = (0, import_react4.useState)(
|
|
1169
|
+
/* @__PURE__ */ new Map()
|
|
1170
|
+
);
|
|
1171
|
+
const trackEffectInstancesRef = (0, import_react4.useRef)(/* @__PURE__ */ new Map());
|
|
1172
|
+
const trackGraphNodesRef = (0, import_react4.useRef)(/* @__PURE__ */ new Map());
|
|
1173
|
+
const rebuildTrackChain = (0, import_react4.useCallback)((trackId, trackEffects) => {
|
|
1174
|
+
const nodes = trackGraphNodesRef.current.get(trackId);
|
|
1175
|
+
if (!nodes) return;
|
|
1176
|
+
const { graphEnd, masterGainNode } = nodes;
|
|
1177
|
+
const instancesMap = trackEffectInstancesRef.current.get(trackId);
|
|
1178
|
+
try {
|
|
1179
|
+
graphEnd.disconnect();
|
|
1180
|
+
} catch (e) {
|
|
1181
|
+
console.warn(`[waveform-playlist] Error disconnecting track "${trackId}" effect chain:`, e);
|
|
1182
|
+
}
|
|
1183
|
+
const instances = trackEffects.map((ae) => instancesMap == null ? void 0 : instancesMap.get(ae.instanceId)).filter((inst) => inst !== void 0);
|
|
1184
|
+
if (instances.length === 0) {
|
|
1185
|
+
graphEnd.connect(masterGainNode);
|
|
1186
|
+
} else {
|
|
1187
|
+
let currentNode = graphEnd;
|
|
1188
|
+
instances.forEach((inst) => {
|
|
1189
|
+
try {
|
|
1190
|
+
inst.disconnect();
|
|
1191
|
+
} catch (e) {
|
|
1192
|
+
console.warn(
|
|
1193
|
+
`[waveform-playlist] Error disconnecting effect "${inst.id}" on track "${trackId}":`,
|
|
1194
|
+
e
|
|
1195
|
+
);
|
|
1196
|
+
}
|
|
1197
|
+
currentNode.connect(inst.effect);
|
|
1198
|
+
currentNode = inst.effect;
|
|
1199
|
+
});
|
|
1200
|
+
currentNode.connect(masterGainNode);
|
|
1201
|
+
}
|
|
1202
|
+
}, []);
|
|
1203
|
+
const addEffectToTrack = (0, import_react4.useCallback)((trackId, effectId) => {
|
|
1204
|
+
const definition = getEffectDefinition(effectId);
|
|
1205
|
+
if (!definition) {
|
|
1206
|
+
console.error(`Unknown effect: ${effectId}`);
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
const params = {};
|
|
1210
|
+
definition.parameters.forEach((p) => {
|
|
1211
|
+
params[p.name] = p.default;
|
|
1212
|
+
});
|
|
1213
|
+
const instance = createEffectInstance(definition, params);
|
|
1214
|
+
if (!trackEffectInstancesRef.current.has(trackId)) {
|
|
1215
|
+
trackEffectInstancesRef.current.set(trackId, /* @__PURE__ */ new Map());
|
|
1216
|
+
}
|
|
1217
|
+
trackEffectInstancesRef.current.get(trackId).set(instance.instanceId, instance);
|
|
1218
|
+
const newActiveEffect = {
|
|
1219
|
+
instanceId: instance.instanceId,
|
|
1220
|
+
effectId: definition.id,
|
|
1221
|
+
definition,
|
|
1222
|
+
params,
|
|
1223
|
+
bypassed: false
|
|
1224
|
+
};
|
|
1225
|
+
setTrackEffectsState((prev) => {
|
|
1226
|
+
const newState = new Map(prev);
|
|
1227
|
+
const existing = newState.get(trackId) || [];
|
|
1228
|
+
newState.set(trackId, [...existing, newActiveEffect]);
|
|
1229
|
+
return newState;
|
|
1230
|
+
});
|
|
1231
|
+
}, []);
|
|
1232
|
+
const removeEffectFromTrack = (0, import_react4.useCallback)((trackId, instanceId) => {
|
|
1233
|
+
const instancesMap = trackEffectInstancesRef.current.get(trackId);
|
|
1234
|
+
const instance = instancesMap == null ? void 0 : instancesMap.get(instanceId);
|
|
1235
|
+
if (instance) {
|
|
1236
|
+
instance.dispose();
|
|
1237
|
+
instancesMap == null ? void 0 : instancesMap.delete(instanceId);
|
|
1238
|
+
}
|
|
1239
|
+
setTrackEffectsState((prev) => {
|
|
1240
|
+
const newState = new Map(prev);
|
|
1241
|
+
const existing = newState.get(trackId) || [];
|
|
1242
|
+
newState.set(
|
|
1243
|
+
trackId,
|
|
1244
|
+
existing.filter((e) => e.instanceId !== instanceId)
|
|
1245
|
+
);
|
|
1246
|
+
return newState;
|
|
1247
|
+
});
|
|
1248
|
+
}, []);
|
|
1249
|
+
const updateTrackEffectParameter = (0, import_react4.useCallback)(
|
|
1250
|
+
(trackId, instanceId, paramName, value) => {
|
|
1251
|
+
const instancesMap = trackEffectInstancesRef.current.get(trackId);
|
|
1252
|
+
const instance = instancesMap == null ? void 0 : instancesMap.get(instanceId);
|
|
1253
|
+
if (instance) {
|
|
1254
|
+
instance.setParameter(paramName, value);
|
|
1255
|
+
}
|
|
1256
|
+
setTrackEffectsState((prev) => {
|
|
1257
|
+
const newState = new Map(prev);
|
|
1258
|
+
const existing = newState.get(trackId) || [];
|
|
1259
|
+
newState.set(
|
|
1260
|
+
trackId,
|
|
1261
|
+
existing.map(
|
|
1262
|
+
(e) => e.instanceId === instanceId ? __spreadProps(__spreadValues({}, e), { params: __spreadProps(__spreadValues({}, e.params), { [paramName]: value }) }) : e
|
|
1263
|
+
)
|
|
1264
|
+
);
|
|
1265
|
+
return newState;
|
|
1266
|
+
});
|
|
1267
|
+
},
|
|
1268
|
+
[]
|
|
1269
|
+
);
|
|
1270
|
+
const toggleBypass = (0, import_react4.useCallback)((trackId, instanceId) => {
|
|
1271
|
+
var _a;
|
|
1272
|
+
const trackEffects = trackEffectsStateRef.current.get(trackId) || [];
|
|
1273
|
+
const effect = trackEffects.find((e) => e.instanceId === instanceId);
|
|
1274
|
+
if (!effect) return;
|
|
1275
|
+
const newBypassed = !effect.bypassed;
|
|
1276
|
+
const instancesMap = trackEffectInstancesRef.current.get(trackId);
|
|
1277
|
+
const instance = instancesMap == null ? void 0 : instancesMap.get(instanceId);
|
|
1278
|
+
if (instance) {
|
|
1279
|
+
const originalWet = (_a = effect.params.wet) != null ? _a : 1;
|
|
1280
|
+
instance.setParameter("wet", newBypassed ? 0 : originalWet);
|
|
1281
|
+
}
|
|
1282
|
+
setTrackEffectsState((prev) => {
|
|
1283
|
+
const newState = new Map(prev);
|
|
1284
|
+
const existing = newState.get(trackId) || [];
|
|
1285
|
+
newState.set(
|
|
1286
|
+
trackId,
|
|
1287
|
+
existing.map((e) => e.instanceId === instanceId ? __spreadProps(__spreadValues({}, e), { bypassed: newBypassed }) : e)
|
|
1288
|
+
);
|
|
1289
|
+
return newState;
|
|
1290
|
+
});
|
|
1291
|
+
}, []);
|
|
1292
|
+
const clearTrackEffects = (0, import_react4.useCallback)((trackId) => {
|
|
1293
|
+
const instancesMap = trackEffectInstancesRef.current.get(trackId);
|
|
1294
|
+
if (instancesMap) {
|
|
1295
|
+
instancesMap.forEach((inst) => inst.dispose());
|
|
1296
|
+
instancesMap.clear();
|
|
1297
|
+
}
|
|
1298
|
+
setTrackEffectsState((prev) => {
|
|
1299
|
+
const newState = new Map(prev);
|
|
1300
|
+
newState.set(trackId, []);
|
|
1301
|
+
return newState;
|
|
1302
|
+
});
|
|
1303
|
+
}, []);
|
|
1304
|
+
const trackEffectsStateRef = (0, import_react4.useRef)(trackEffectsState);
|
|
1305
|
+
trackEffectsStateRef.current = trackEffectsState;
|
|
1306
|
+
const getTrackEffectsFunction = (0, import_react4.useCallback)(
|
|
1307
|
+
(trackId) => {
|
|
1308
|
+
return (graphEnd, masterGainNode, _isOffline) => {
|
|
1309
|
+
trackGraphNodesRef.current.set(trackId, {
|
|
1310
|
+
graphEnd,
|
|
1311
|
+
masterGainNode
|
|
1312
|
+
});
|
|
1313
|
+
const trackEffects = trackEffectsStateRef.current.get(trackId) || [];
|
|
1314
|
+
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);
|
|
1316
|
+
if (instances.length === 0) {
|
|
1317
|
+
graphEnd.connect(masterGainNode);
|
|
1318
|
+
} else {
|
|
1319
|
+
let currentNode = graphEnd;
|
|
1320
|
+
instances.forEach((inst) => {
|
|
1321
|
+
currentNode.connect(inst.effect);
|
|
1322
|
+
currentNode = inst.effect;
|
|
1323
|
+
});
|
|
1324
|
+
currentNode.connect(masterGainNode);
|
|
1325
|
+
}
|
|
1326
|
+
return function cleanup() {
|
|
1327
|
+
trackGraphNodesRef.current.delete(trackId);
|
|
1328
|
+
};
|
|
1329
|
+
};
|
|
1330
|
+
},
|
|
1331
|
+
[]
|
|
1332
|
+
// No dependencies - stable function that reads from refs
|
|
1333
|
+
);
|
|
1334
|
+
(0, import_react4.useEffect)(() => {
|
|
1335
|
+
trackEffectsState.forEach((effects, trackId) => {
|
|
1336
|
+
rebuildTrackChain(trackId, effects);
|
|
1337
|
+
});
|
|
1338
|
+
}, [trackEffectsState, rebuildTrackChain]);
|
|
1339
|
+
(0, import_react4.useEffect)(() => {
|
|
1340
|
+
const trackEffectInstances = trackEffectInstancesRef.current;
|
|
1341
|
+
return () => {
|
|
1342
|
+
trackEffectInstances.forEach((instancesMap) => {
|
|
1343
|
+
instancesMap.forEach((inst) => inst.dispose());
|
|
1344
|
+
instancesMap.clear();
|
|
1345
|
+
});
|
|
1346
|
+
trackEffectInstances.clear();
|
|
1347
|
+
};
|
|
1348
|
+
}, []);
|
|
1349
|
+
const createOfflineTrackEffectsFunction = (0, import_react4.useCallback)(
|
|
1350
|
+
(trackId) => {
|
|
1351
|
+
const trackEffects = trackEffectsState.get(trackId) || [];
|
|
1352
|
+
const nonBypassedEffects = trackEffects.filter((e) => !e.bypassed);
|
|
1353
|
+
if (nonBypassedEffects.length === 0) {
|
|
1354
|
+
return void 0;
|
|
1355
|
+
}
|
|
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);
|
|
1371
|
+
}
|
|
1372
|
+
return function cleanup() {
|
|
1373
|
+
offlineInstances.forEach((inst) => inst.dispose());
|
|
1374
|
+
};
|
|
1375
|
+
};
|
|
1376
|
+
},
|
|
1377
|
+
[trackEffectsState]
|
|
1378
|
+
);
|
|
1379
|
+
return {
|
|
1380
|
+
trackEffectsState,
|
|
1381
|
+
addEffectToTrack,
|
|
1382
|
+
removeEffectFromTrack,
|
|
1383
|
+
updateTrackEffectParameter,
|
|
1384
|
+
toggleBypass,
|
|
1385
|
+
clearTrackEffects,
|
|
1386
|
+
getTrackEffectsFunction,
|
|
1387
|
+
createOfflineTrackEffectsFunction,
|
|
1388
|
+
availableEffects: effectDefinitions
|
|
1389
|
+
};
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
// src/hooks/useExportWav.ts
|
|
1393
|
+
var import_react5 = require("react");
|
|
1394
|
+
var import_core2 = require("@waveform-playlist/core");
|
|
1395
|
+
var import_playout = require("@waveform-playlist/playout");
|
|
1396
|
+
|
|
1397
|
+
// src/utils/wavEncoder.ts
|
|
1398
|
+
function encodeWav(audioBuffer, options = {}) {
|
|
1399
|
+
const { bitDepth = 16 } = options;
|
|
1400
|
+
const numChannels = audioBuffer.numberOfChannels;
|
|
1401
|
+
const sampleRate = audioBuffer.sampleRate;
|
|
1402
|
+
const numSamples = audioBuffer.length;
|
|
1403
|
+
const bytesPerSample = bitDepth / 8;
|
|
1404
|
+
const blockAlign = numChannels * bytesPerSample;
|
|
1405
|
+
const byteRate = sampleRate * blockAlign;
|
|
1406
|
+
const dataSize = numSamples * blockAlign;
|
|
1407
|
+
const headerSize = 44;
|
|
1408
|
+
const totalSize = headerSize + dataSize;
|
|
1409
|
+
const buffer = new ArrayBuffer(totalSize);
|
|
1410
|
+
const view = new DataView(buffer);
|
|
1411
|
+
writeString(view, 0, "RIFF");
|
|
1412
|
+
view.setUint32(4, totalSize - 8, true);
|
|
1413
|
+
writeString(view, 8, "WAVE");
|
|
1414
|
+
writeString(view, 12, "fmt ");
|
|
1415
|
+
view.setUint32(16, 16, true);
|
|
1416
|
+
view.setUint16(20, bitDepth === 32 ? 3 : 1, true);
|
|
1417
|
+
view.setUint16(22, numChannels, true);
|
|
1418
|
+
view.setUint32(24, sampleRate, true);
|
|
1419
|
+
view.setUint32(28, byteRate, true);
|
|
1420
|
+
view.setUint16(32, blockAlign, true);
|
|
1421
|
+
view.setUint16(34, bitDepth, true);
|
|
1422
|
+
writeString(view, 36, "data");
|
|
1423
|
+
view.setUint32(40, dataSize, true);
|
|
1424
|
+
const channelData = [];
|
|
1425
|
+
for (let ch = 0; ch < numChannels; ch++) {
|
|
1426
|
+
channelData.push(audioBuffer.getChannelData(ch));
|
|
1427
|
+
}
|
|
1428
|
+
let offset = headerSize;
|
|
1429
|
+
if (bitDepth === 16) {
|
|
1430
|
+
for (let i = 0; i < numSamples; i++) {
|
|
1431
|
+
for (let ch = 0; ch < numChannels; ch++) {
|
|
1432
|
+
const sample = channelData[ch][i];
|
|
1433
|
+
const clampedSample = Math.max(-1, Math.min(1, sample));
|
|
1434
|
+
const intSample = clampedSample < 0 ? clampedSample * 32768 : clampedSample * 32767;
|
|
1435
|
+
view.setInt16(offset, intSample, true);
|
|
1436
|
+
offset += 2;
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
} else {
|
|
1440
|
+
for (let i = 0; i < numSamples; i++) {
|
|
1441
|
+
for (let ch = 0; ch < numChannels; ch++) {
|
|
1442
|
+
view.setFloat32(offset, channelData[ch][i], true);
|
|
1443
|
+
offset += 4;
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
return new Blob([buffer], { type: "audio/wav" });
|
|
1448
|
+
}
|
|
1449
|
+
function writeString(view, offset, str) {
|
|
1450
|
+
for (let i = 0; i < str.length; i++) {
|
|
1451
|
+
view.setUint8(offset + i, str.charCodeAt(i));
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
function downloadBlob(blob, filename) {
|
|
1455
|
+
const url = URL.createObjectURL(blob);
|
|
1456
|
+
const a = document.createElement("a");
|
|
1457
|
+
a.href = url;
|
|
1458
|
+
a.download = filename;
|
|
1459
|
+
a.style.display = "none";
|
|
1460
|
+
document.body.appendChild(a);
|
|
1461
|
+
a.click();
|
|
1462
|
+
document.body.removeChild(a);
|
|
1463
|
+
URL.revokeObjectURL(url);
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
// src/hooks/useExportWav.ts
|
|
1467
|
+
function useExportWav() {
|
|
1468
|
+
const [isExporting, setIsExporting] = (0, import_react5.useState)(false);
|
|
1469
|
+
const [progress, setProgress] = (0, import_react5.useState)(0);
|
|
1470
|
+
const [error, setError] = (0, import_react5.useState)(null);
|
|
1471
|
+
const exportWav = (0, import_react5.useCallback)(
|
|
1472
|
+
(_0, _1, ..._2) => __async(null, [_0, _1, ..._2], function* (tracks, trackStates, options = {}) {
|
|
1473
|
+
const {
|
|
1474
|
+
filename = "export",
|
|
1475
|
+
mode = "master",
|
|
1476
|
+
trackIndex,
|
|
1477
|
+
autoDownload = true,
|
|
1478
|
+
applyEffects = true,
|
|
1479
|
+
effectsFunction,
|
|
1480
|
+
createOfflineTrackEffects,
|
|
1481
|
+
bitDepth = 16,
|
|
1482
|
+
onProgress
|
|
1483
|
+
} = options;
|
|
1484
|
+
setIsExporting(true);
|
|
1485
|
+
setProgress(0);
|
|
1486
|
+
setError(null);
|
|
1487
|
+
try {
|
|
1488
|
+
if (tracks.length === 0) {
|
|
1489
|
+
throw new Error("No tracks to export");
|
|
1490
|
+
}
|
|
1491
|
+
if (mode === "individual" && (trackIndex === void 0 || trackIndex < 0 || trackIndex >= tracks.length)) {
|
|
1492
|
+
throw new Error("Invalid track index for individual export");
|
|
1493
|
+
}
|
|
1494
|
+
const sampleRate = (0, import_playout.getGlobalAudioContext)().sampleRate;
|
|
1495
|
+
let totalDurationSamples = 0;
|
|
1496
|
+
for (const track of tracks) {
|
|
1497
|
+
for (const clip of track.clips) {
|
|
1498
|
+
const clipEndSample = clip.startSample + clip.durationSamples;
|
|
1499
|
+
totalDurationSamples = Math.max(totalDurationSamples, clipEndSample);
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
totalDurationSamples += Math.round(sampleRate * 0.1);
|
|
1503
|
+
const duration = totalDurationSamples / sampleRate;
|
|
1504
|
+
const tracksToRender = mode === "individual" ? [{ track: tracks[trackIndex], state: trackStates[trackIndex], index: trackIndex }] : tracks.map((track, index) => ({ track, state: trackStates[index], index }));
|
|
1505
|
+
const hasSolo = mode === "master" && trackStates.some((state) => state.soloed);
|
|
1506
|
+
const reportProgress = (p) => {
|
|
1507
|
+
setProgress(p);
|
|
1508
|
+
onProgress == null ? void 0 : onProgress(p);
|
|
1509
|
+
};
|
|
1510
|
+
const renderedBuffer = yield renderOffline(
|
|
1511
|
+
tracksToRender,
|
|
1512
|
+
hasSolo,
|
|
1513
|
+
duration,
|
|
1514
|
+
sampleRate,
|
|
1515
|
+
applyEffects,
|
|
1516
|
+
effectsFunction,
|
|
1517
|
+
createOfflineTrackEffects,
|
|
1518
|
+
reportProgress
|
|
1519
|
+
);
|
|
1520
|
+
reportProgress(0.9);
|
|
1521
|
+
const blob = encodeWav(renderedBuffer, { bitDepth });
|
|
1522
|
+
reportProgress(1);
|
|
1523
|
+
if (autoDownload) {
|
|
1524
|
+
const exportFilename = mode === "individual" ? `${filename}_${tracks[trackIndex].name}` : filename;
|
|
1525
|
+
downloadBlob(blob, `${exportFilename}.wav`);
|
|
1526
|
+
}
|
|
1527
|
+
return {
|
|
1528
|
+
audioBuffer: renderedBuffer,
|
|
1529
|
+
blob,
|
|
1530
|
+
duration
|
|
1531
|
+
};
|
|
1532
|
+
} catch (err) {
|
|
1533
|
+
const message = err instanceof Error ? err.message : "Export failed";
|
|
1534
|
+
setError(message);
|
|
1535
|
+
throw err;
|
|
1536
|
+
} finally {
|
|
1537
|
+
setIsExporting(false);
|
|
1538
|
+
}
|
|
1539
|
+
}),
|
|
1540
|
+
[]
|
|
1541
|
+
);
|
|
1542
|
+
return {
|
|
1543
|
+
exportWav,
|
|
1544
|
+
isExporting,
|
|
1545
|
+
progress,
|
|
1546
|
+
error
|
|
1547
|
+
};
|
|
1548
|
+
}
|
|
1549
|
+
function renderOffline(tracksToRender, hasSolo, duration, sampleRate, applyEffects, effectsFunction, createOfflineTrackEffects, onProgress) {
|
|
1550
|
+
return __async(this, null, function* () {
|
|
1551
|
+
const { Offline, Volume: Volume2, Gain, Panner, Player, ToneAudioBuffer } = yield import("tone");
|
|
1552
|
+
onProgress(0.1);
|
|
1553
|
+
const audibleTracks = tracksToRender.filter(({ state }) => {
|
|
1554
|
+
if (state.muted && !state.soloed) return false;
|
|
1555
|
+
if (hasSolo && !state.soloed) return false;
|
|
1556
|
+
return true;
|
|
1557
|
+
});
|
|
1558
|
+
const outputChannels = audibleTracks.reduce(
|
|
1559
|
+
(max, { track }) => Math.max(max, (0, import_core2.trackChannelCount)(track)),
|
|
1560
|
+
1
|
|
1561
|
+
);
|
|
1562
|
+
let buffer;
|
|
1563
|
+
try {
|
|
1564
|
+
buffer = yield Offline(
|
|
1565
|
+
(_0) => __async(null, [_0], function* ({ transport, destination }) {
|
|
1566
|
+
const masterVolume = new Volume2(0);
|
|
1567
|
+
if (effectsFunction && applyEffects) {
|
|
1568
|
+
effectsFunction(masterVolume, destination, true);
|
|
1569
|
+
} else {
|
|
1570
|
+
masterVolume.connect(destination);
|
|
1571
|
+
}
|
|
1572
|
+
for (const { track, state } of audibleTracks) {
|
|
1573
|
+
const trackVolume = new Volume2((0, import_core2.gainToDb)(state.volume));
|
|
1574
|
+
const trackPan = new Panner({ pan: state.pan, channelCount: (0, import_core2.trackChannelCount)(track) });
|
|
1575
|
+
const trackMute = new Gain(state.muted ? 0 : 1);
|
|
1576
|
+
const trackEffects = createOfflineTrackEffects == null ? void 0 : createOfflineTrackEffects(track.id);
|
|
1577
|
+
if (trackEffects && applyEffects) {
|
|
1578
|
+
trackEffects(trackMute, masterVolume, true);
|
|
1579
|
+
} else {
|
|
1580
|
+
trackMute.connect(masterVolume);
|
|
1581
|
+
}
|
|
1582
|
+
trackPan.connect(trackMute);
|
|
1583
|
+
trackVolume.connect(trackPan);
|
|
1584
|
+
for (const clip of track.clips) {
|
|
1585
|
+
const {
|
|
1586
|
+
audioBuffer,
|
|
1587
|
+
startSample,
|
|
1588
|
+
durationSamples,
|
|
1589
|
+
offsetSamples,
|
|
1590
|
+
gain: clipGain,
|
|
1591
|
+
fadeIn,
|
|
1592
|
+
fadeOut
|
|
1593
|
+
} = clip;
|
|
1594
|
+
if (!audioBuffer) {
|
|
1595
|
+
console.warn(
|
|
1596
|
+
'[waveform-playlist] Skipping clip "' + (clip.name || clip.id) + '" - no audioBuffer for export'
|
|
1597
|
+
);
|
|
1598
|
+
continue;
|
|
1599
|
+
}
|
|
1600
|
+
const startTime = startSample / sampleRate;
|
|
1601
|
+
const clipDuration = durationSamples / sampleRate;
|
|
1602
|
+
const offset = offsetSamples / sampleRate;
|
|
1603
|
+
const toneBuffer = new ToneAudioBuffer(audioBuffer);
|
|
1604
|
+
const player = new Player(toneBuffer);
|
|
1605
|
+
const fadeGain = new Gain(clipGain);
|
|
1606
|
+
player.connect(fadeGain);
|
|
1607
|
+
fadeGain.connect(trackVolume);
|
|
1608
|
+
if (applyEffects) {
|
|
1609
|
+
const audioParam = (0, import_playout.getUnderlyingAudioParam)(fadeGain.gain);
|
|
1610
|
+
if (audioParam) {
|
|
1611
|
+
applyClipFades(audioParam, clipGain, startTime, clipDuration, fadeIn, fadeOut);
|
|
1612
|
+
} else if (fadeIn || fadeOut) {
|
|
1613
|
+
console.warn(
|
|
1614
|
+
'[waveform-playlist] Cannot apply fades for clip "' + (clip.name || clip.id) + '" - AudioParam not accessible'
|
|
1615
|
+
);
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
player.start(startTime, offset, clipDuration);
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
transport.start(0);
|
|
1622
|
+
}),
|
|
1623
|
+
duration,
|
|
1624
|
+
outputChannels,
|
|
1625
|
+
sampleRate
|
|
1626
|
+
);
|
|
1627
|
+
} catch (err) {
|
|
1628
|
+
if (err instanceof Error) {
|
|
1629
|
+
throw err;
|
|
1630
|
+
} else {
|
|
1631
|
+
throw new Error("Tone.Offline rendering failed: " + String(err));
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
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
|
+
});
|
|
1641
|
+
}
|
|
1642
|
+
function applyClipFades(gainParam, clipGain, startTime, clipDuration, fadeIn, fadeOut) {
|
|
1643
|
+
if (fadeIn) {
|
|
1644
|
+
gainParam.setValueAtTime(0, startTime);
|
|
1645
|
+
} else {
|
|
1646
|
+
gainParam.setValueAtTime(clipGain, startTime);
|
|
1647
|
+
}
|
|
1648
|
+
if (fadeIn) {
|
|
1649
|
+
(0, import_core2.applyFadeIn)(gainParam, startTime, fadeIn.duration, fadeIn.type || "linear", 0, clipGain);
|
|
1650
|
+
}
|
|
1651
|
+
if (fadeOut) {
|
|
1652
|
+
const fadeOutStart = startTime + clipDuration - fadeOut.duration;
|
|
1653
|
+
if (!fadeIn || fadeIn.duration < clipDuration - fadeOut.duration) {
|
|
1654
|
+
gainParam.setValueAtTime(clipGain, fadeOutStart);
|
|
1655
|
+
}
|
|
1656
|
+
(0, import_core2.applyFadeOut)(gainParam, fadeOutStart, fadeOut.duration, fadeOut.type || "linear", clipGain, 0);
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
// src/hooks/useDynamicTracks.ts
|
|
1661
|
+
var import_react6 = require("react");
|
|
1662
|
+
var import_core3 = require("@waveform-playlist/core");
|
|
1663
|
+
var import_playout2 = require("@waveform-playlist/playout");
|
|
1664
|
+
function getSourceName(source) {
|
|
1665
|
+
var _a, _b, _c, _d, _e;
|
|
1666
|
+
if (source instanceof File) {
|
|
1667
|
+
return source.name.replace(/\.[^/.]+$/, "");
|
|
1668
|
+
}
|
|
1669
|
+
if (source instanceof Blob) {
|
|
1670
|
+
return "Untitled";
|
|
1671
|
+
}
|
|
1672
|
+
if (typeof source === "string") {
|
|
1673
|
+
return (_b = (_a = source.split("/").pop()) == null ? void 0 : _a.replace(/\.[^/.]+$/, "")) != null ? _b : "Untitled";
|
|
1674
|
+
}
|
|
1675
|
+
return (_e = (_d = source.name) != null ? _d : (_c = source.src.split("/").pop()) == null ? void 0 : _c.replace(/\.[^/.]+$/, "")) != null ? _e : "Untitled";
|
|
1676
|
+
}
|
|
1677
|
+
function decodeSource(source, audioContext, signal) {
|
|
1678
|
+
return __async(this, null, function* () {
|
|
1679
|
+
const name = getSourceName(source);
|
|
1680
|
+
if (source instanceof Blob) {
|
|
1681
|
+
const arrayBuffer2 = yield source.arrayBuffer();
|
|
1682
|
+
signal == null ? void 0 : signal.throwIfAborted();
|
|
1683
|
+
const audioBuffer2 = yield audioContext.decodeAudioData(arrayBuffer2);
|
|
1684
|
+
return { audioBuffer: audioBuffer2, name };
|
|
1685
|
+
}
|
|
1686
|
+
const url = typeof source === "string" ? source : source.src;
|
|
1687
|
+
const response = yield fetch(url, { signal });
|
|
1688
|
+
if (!response.ok) throw new Error(`Failed to fetch ${url}: ${response.statusText}`);
|
|
1689
|
+
const arrayBuffer = yield response.arrayBuffer();
|
|
1690
|
+
signal == null ? void 0 : signal.throwIfAborted();
|
|
1691
|
+
const audioBuffer = yield audioContext.decodeAudioData(arrayBuffer);
|
|
1692
|
+
return { audioBuffer, name };
|
|
1693
|
+
});
|
|
1694
|
+
}
|
|
1695
|
+
function useDynamicTracks() {
|
|
1696
|
+
const [tracks, setTracks] = (0, import_react6.useState)([]);
|
|
1697
|
+
const [loadingCount, setLoadingCount] = (0, import_react6.useState)(0);
|
|
1698
|
+
const [errors, setErrors] = (0, import_react6.useState)([]);
|
|
1699
|
+
const cancelledRef = (0, import_react6.useRef)(false);
|
|
1700
|
+
const loadingIdsRef = (0, import_react6.useRef)(/* @__PURE__ */ new Set());
|
|
1701
|
+
const abortControllersRef = (0, import_react6.useRef)(/* @__PURE__ */ new Map());
|
|
1702
|
+
(0, import_react6.useEffect)(() => {
|
|
1703
|
+
const controllers = abortControllersRef.current;
|
|
1704
|
+
return () => {
|
|
1705
|
+
cancelledRef.current = true;
|
|
1706
|
+
for (const controller of controllers.values()) {
|
|
1707
|
+
controller.abort();
|
|
1708
|
+
}
|
|
1709
|
+
controllers.clear();
|
|
1710
|
+
};
|
|
1711
|
+
}, []);
|
|
1712
|
+
const addTracks = (0, import_react6.useCallback)((sources) => {
|
|
1713
|
+
if (sources.length === 0) return;
|
|
1714
|
+
const audioContext = (0, import_playout2.getGlobalAudioContext)();
|
|
1715
|
+
const placeholders = sources.map((source) => ({
|
|
1716
|
+
track: (0, import_core3.createTrack)({ name: `${getSourceName(source)} (loading...)`, clips: [] }),
|
|
1717
|
+
source
|
|
1718
|
+
}));
|
|
1719
|
+
setTracks((prev) => [...prev, ...placeholders.map((p) => p.track)]);
|
|
1720
|
+
setLoadingCount((prev) => prev + sources.length);
|
|
1721
|
+
for (const { track, source } of placeholders) {
|
|
1722
|
+
loadingIdsRef.current.add(track.id);
|
|
1723
|
+
const controller = new AbortController();
|
|
1724
|
+
abortControllersRef.current.set(track.id, controller);
|
|
1725
|
+
(() => __async(null, null, function* () {
|
|
1726
|
+
try {
|
|
1727
|
+
const { audioBuffer, name } = yield decodeSource(source, audioContext, controller.signal);
|
|
1728
|
+
const clip = (0, import_core3.createClipFromSeconds)({
|
|
1729
|
+
audioBuffer,
|
|
1730
|
+
startTime: 0,
|
|
1731
|
+
duration: audioBuffer.duration,
|
|
1732
|
+
offset: 0,
|
|
1733
|
+
name
|
|
1734
|
+
});
|
|
1735
|
+
if (!cancelledRef.current && loadingIdsRef.current.has(track.id)) {
|
|
1736
|
+
setTracks(
|
|
1737
|
+
(prev) => prev.map((t) => t.id === track.id ? __spreadProps(__spreadValues({}, t), { name, clips: [clip] }) : t)
|
|
1738
|
+
);
|
|
1739
|
+
}
|
|
1740
|
+
} catch (error) {
|
|
1741
|
+
if (error instanceof DOMException && error.name === "AbortError") return;
|
|
1742
|
+
console.warn("[waveform-playlist] Error loading audio:", error);
|
|
1743
|
+
if (!cancelledRef.current && loadingIdsRef.current.has(track.id)) {
|
|
1744
|
+
setTracks((prev) => prev.filter((t) => t.id !== track.id));
|
|
1745
|
+
setErrors((prev) => [
|
|
1746
|
+
...prev,
|
|
1747
|
+
{
|
|
1748
|
+
name: getSourceName(source),
|
|
1749
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
1750
|
+
}
|
|
1751
|
+
]);
|
|
1752
|
+
}
|
|
1753
|
+
} finally {
|
|
1754
|
+
abortControllersRef.current.delete(track.id);
|
|
1755
|
+
if (!cancelledRef.current && loadingIdsRef.current.delete(track.id)) {
|
|
1756
|
+
setLoadingCount((prev) => prev - 1);
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
}))();
|
|
1760
|
+
}
|
|
1761
|
+
}, []);
|
|
1762
|
+
const removeTrack = (0, import_react6.useCallback)((trackId) => {
|
|
1763
|
+
setTracks((prev) => prev.filter((t) => t.id !== trackId));
|
|
1764
|
+
const controller = abortControllersRef.current.get(trackId);
|
|
1765
|
+
if (controller) {
|
|
1766
|
+
controller.abort();
|
|
1767
|
+
abortControllersRef.current.delete(trackId);
|
|
1768
|
+
}
|
|
1769
|
+
if (loadingIdsRef.current.delete(trackId)) {
|
|
1770
|
+
setLoadingCount((prev) => prev - 1);
|
|
1771
|
+
}
|
|
1772
|
+
}, []);
|
|
1773
|
+
return {
|
|
1774
|
+
tracks,
|
|
1775
|
+
addTracks,
|
|
1776
|
+
removeTrack,
|
|
1777
|
+
loadingCount,
|
|
1778
|
+
isLoading: loadingCount > 0,
|
|
1779
|
+
errors
|
|
1780
|
+
};
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
// src/hooks/useOutputMeter.ts
|
|
1784
|
+
var import_react7 = require("react");
|
|
1785
|
+
var import_playout3 = require("@waveform-playlist/playout");
|
|
1786
|
+
var import_core4 = require("@waveform-playlist/core");
|
|
1787
|
+
var import_worklets = require("@waveform-playlist/worklets");
|
|
1788
|
+
var PEAK_DECAY = 0.98;
|
|
1789
|
+
function useOutputMeter(options = {}) {
|
|
1790
|
+
const { channelCount = 2, updateRate = 60, isPlaying = false } = options;
|
|
1791
|
+
const [levels, setLevels] = (0, import_react7.useState)(() => new Array(channelCount).fill(0));
|
|
1792
|
+
const [peakLevels, setPeakLevels] = (0, import_react7.useState)(() => new Array(channelCount).fill(0));
|
|
1793
|
+
const [rmsLevels, setRmsLevels] = (0, import_react7.useState)(() => new Array(channelCount).fill(0));
|
|
1794
|
+
const workletNodeRef = (0, import_react7.useRef)(null);
|
|
1795
|
+
const smoothedPeakRef = (0, import_react7.useRef)(new Array(channelCount).fill(0));
|
|
1796
|
+
const [meterError, setMeterError] = (0, import_react7.useState)(null);
|
|
1797
|
+
const resetPeak = (0, import_react7.useCallback)(
|
|
1798
|
+
() => setPeakLevels(new Array(channelCount).fill(0)),
|
|
1799
|
+
[channelCount]
|
|
1800
|
+
);
|
|
1801
|
+
(0, import_react7.useEffect)(() => {
|
|
1802
|
+
if (!isPlaying) {
|
|
1803
|
+
const zeros = new Array(channelCount).fill(0);
|
|
1804
|
+
smoothedPeakRef.current = new Array(channelCount).fill(0);
|
|
1805
|
+
setLevels(zeros);
|
|
1806
|
+
setRmsLevels(zeros);
|
|
1807
|
+
setPeakLevels(zeros);
|
|
1808
|
+
}
|
|
1809
|
+
}, [isPlaying, channelCount]);
|
|
1810
|
+
(0, import_react7.useEffect)(() => {
|
|
1811
|
+
let isMounted = true;
|
|
1812
|
+
const setup = () => __async(null, null, function* () {
|
|
1813
|
+
const context = (0, import_playout3.getGlobalContext)();
|
|
1814
|
+
const rawCtx = context.rawContext;
|
|
1815
|
+
yield rawCtx.audioWorklet.addModule(import_worklets.meterProcessorUrl);
|
|
1816
|
+
if (!isMounted) return;
|
|
1817
|
+
const workletNode = context.createAudioWorkletNode("meter-processor", {
|
|
1818
|
+
channelCount,
|
|
1819
|
+
channelCountMode: "explicit",
|
|
1820
|
+
processorOptions: {
|
|
1821
|
+
numberOfChannels: channelCount,
|
|
1822
|
+
updateRate
|
|
1823
|
+
}
|
|
1824
|
+
});
|
|
1825
|
+
workletNodeRef.current = workletNode;
|
|
1826
|
+
workletNode.onprocessorerror = (event) => {
|
|
1827
|
+
console.warn("[waveform-playlist] Output meter worklet processor error:", String(event));
|
|
1828
|
+
};
|
|
1829
|
+
const destination = context.destination;
|
|
1830
|
+
destination.chain(workletNode);
|
|
1831
|
+
smoothedPeakRef.current = new Array(channelCount).fill(0);
|
|
1832
|
+
workletNode.port.onmessage = (event) => {
|
|
1833
|
+
var _a;
|
|
1834
|
+
if (!isMounted) return;
|
|
1835
|
+
const { peak, rms } = event.data;
|
|
1836
|
+
const smoothed = smoothedPeakRef.current;
|
|
1837
|
+
const peakValues = [];
|
|
1838
|
+
const rmsValues = [];
|
|
1839
|
+
for (let ch = 0; ch < peak.length; ch++) {
|
|
1840
|
+
smoothed[ch] = Math.max(peak[ch], ((_a = smoothed[ch]) != null ? _a : 0) * PEAK_DECAY);
|
|
1841
|
+
peakValues.push((0, import_core4.gainToNormalized)(smoothed[ch]));
|
|
1842
|
+
rmsValues.push((0, import_core4.gainToNormalized)(rms[ch]));
|
|
1843
|
+
}
|
|
1844
|
+
setLevels(peakValues);
|
|
1845
|
+
setRmsLevels(rmsValues);
|
|
1846
|
+
setPeakLevels((prev) => peakValues.map((val, i) => {
|
|
1847
|
+
var _a2;
|
|
1848
|
+
return Math.max((_a2 = prev[i]) != null ? _a2 : 0, val);
|
|
1849
|
+
}));
|
|
1850
|
+
};
|
|
1851
|
+
});
|
|
1852
|
+
setup().catch((err) => {
|
|
1853
|
+
console.warn("[waveform-playlist] Failed to set up output meter:", String(err));
|
|
1854
|
+
if (isMounted) {
|
|
1855
|
+
setMeterError(err instanceof Error ? err : new Error(String(err)));
|
|
1856
|
+
}
|
|
1857
|
+
});
|
|
1858
|
+
return () => {
|
|
1859
|
+
isMounted = false;
|
|
1860
|
+
if (workletNodeRef.current) {
|
|
1861
|
+
try {
|
|
1862
|
+
const context = (0, import_playout3.getGlobalContext)();
|
|
1863
|
+
context.destination.chain();
|
|
1864
|
+
} catch (err) {
|
|
1865
|
+
console.warn("[waveform-playlist] Failed to restore destination chain:", String(err));
|
|
1866
|
+
}
|
|
1867
|
+
try {
|
|
1868
|
+
workletNodeRef.current.disconnect();
|
|
1869
|
+
workletNodeRef.current.port.close();
|
|
1870
|
+
} catch (err) {
|
|
1871
|
+
console.warn("[waveform-playlist] Output meter disconnect during cleanup:", String(err));
|
|
1872
|
+
}
|
|
1873
|
+
workletNodeRef.current = null;
|
|
1874
|
+
}
|
|
1875
|
+
};
|
|
1876
|
+
}, [channelCount, updateRate]);
|
|
1877
|
+
return { levels, peakLevels, rmsLevels, resetPeak, error: meterError };
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
// src/components/ExportControls.tsx
|
|
1881
|
+
var import_ui_components2 = require("@waveform-playlist/ui-components");
|
|
1882
|
+
|
|
1883
|
+
// src/WaveformPlaylistContext.tsx
|
|
1884
|
+
var import_react8 = require("react");
|
|
1885
|
+
var import_styled_components = require("styled-components");
|
|
1886
|
+
var import_engine = require("@waveform-playlist/engine");
|
|
1887
|
+
var import_ui_components = require("@waveform-playlist/ui-components");
|
|
1888
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
1889
|
+
var PlaybackAnimationContext = (0, import_react8.createContext)(null);
|
|
1890
|
+
var PlaylistStateContext = (0, import_react8.createContext)(null);
|
|
1891
|
+
var PlaylistControlsContext = (0, import_react8.createContext)(null);
|
|
1892
|
+
var PlaylistDataContext = (0, import_react8.createContext)(null);
|
|
1893
|
+
var usePlaylistData = () => {
|
|
1894
|
+
const context = (0, import_react8.useContext)(PlaylistDataContext);
|
|
1895
|
+
if (!context) {
|
|
1896
|
+
throw new Error("usePlaylistData must be used within WaveformPlaylistProvider");
|
|
1897
|
+
}
|
|
1898
|
+
return context;
|
|
1899
|
+
};
|
|
1900
|
+
|
|
1901
|
+
// src/components/ExportControls.tsx
|
|
1902
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
1903
|
+
var ExportWavButton = ({
|
|
1904
|
+
label = "Export WAV",
|
|
1905
|
+
filename = "export",
|
|
1906
|
+
mode = "master",
|
|
1907
|
+
trackIndex,
|
|
1908
|
+
bitDepth = 16,
|
|
1909
|
+
applyEffects = true,
|
|
1910
|
+
effectsFunction,
|
|
1911
|
+
createOfflineTrackEffects,
|
|
1912
|
+
className,
|
|
1913
|
+
onExportComplete,
|
|
1914
|
+
onExportError
|
|
1915
|
+
}) => {
|
|
1916
|
+
const { tracks, trackStates } = usePlaylistData();
|
|
1917
|
+
const { exportWav, isExporting, progress } = useExportWav();
|
|
1918
|
+
const handleExport = () => __async(null, null, function* () {
|
|
1919
|
+
try {
|
|
1920
|
+
const result = yield exportWav(tracks, trackStates, {
|
|
1921
|
+
filename,
|
|
1922
|
+
mode,
|
|
1923
|
+
trackIndex,
|
|
1924
|
+
bitDepth,
|
|
1925
|
+
applyEffects,
|
|
1926
|
+
effectsFunction,
|
|
1927
|
+
createOfflineTrackEffects,
|
|
1928
|
+
autoDownload: true
|
|
1929
|
+
});
|
|
1930
|
+
onExportComplete == null ? void 0 : onExportComplete(result.blob);
|
|
1931
|
+
} catch (error) {
|
|
1932
|
+
onExportError == null ? void 0 : onExportError(error instanceof Error ? error : new Error("Export failed"));
|
|
1933
|
+
}
|
|
1934
|
+
});
|
|
1935
|
+
const buttonLabel = isExporting ? `Exporting ${Math.round(progress * 100)}%` : label;
|
|
1936
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1937
|
+
import_ui_components2.BaseControlButton,
|
|
1938
|
+
{
|
|
1939
|
+
onClick: handleExport,
|
|
1940
|
+
disabled: isExporting || tracks.length === 0,
|
|
1941
|
+
className,
|
|
1942
|
+
children: buttonLabel
|
|
1943
|
+
}
|
|
1944
|
+
);
|
|
1945
|
+
};
|
|
1946
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1947
|
+
0 && (module.exports = {
|
|
1948
|
+
ExportWavButton,
|
|
1949
|
+
createEffectChain,
|
|
1950
|
+
createEffectInstance,
|
|
1951
|
+
effectCategories,
|
|
1952
|
+
effectDefinitions,
|
|
1953
|
+
getEffectDefinition,
|
|
1954
|
+
getEffectsByCategory,
|
|
1955
|
+
useAudioTracks,
|
|
1956
|
+
useDynamicEffects,
|
|
1957
|
+
useDynamicTracks,
|
|
1958
|
+
useExportWav,
|
|
1959
|
+
useMasterAnalyser,
|
|
1960
|
+
useOutputMeter,
|
|
1961
|
+
useTrackDynamicEffects
|
|
1962
|
+
});
|
|
1963
|
+
//# sourceMappingURL=tone.js.map
|