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