@waveform-playlist/playout 7.1.3 → 8.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +48 -11
- package/dist/index.d.ts +48 -11
- package/dist/index.js +349 -172
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +369 -185
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
package/dist/index.mjs
CHANGED
|
@@ -3,13 +3,20 @@ import {
|
|
|
3
3
|
Volume as Volume2,
|
|
4
4
|
getDestination as getDestination2,
|
|
5
5
|
start,
|
|
6
|
-
now
|
|
7
|
-
getTransport,
|
|
8
|
-
getContext
|
|
6
|
+
now,
|
|
7
|
+
getTransport as getTransport2,
|
|
8
|
+
getContext as getContext2
|
|
9
9
|
} from "tone";
|
|
10
10
|
|
|
11
11
|
// src/ToneTrack.ts
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
Volume,
|
|
14
|
+
Gain,
|
|
15
|
+
Panner,
|
|
16
|
+
getDestination,
|
|
17
|
+
getTransport,
|
|
18
|
+
getContext
|
|
19
|
+
} from "tone";
|
|
13
20
|
|
|
14
21
|
// src/fades.ts
|
|
15
22
|
var hasWarned = false;
|
|
@@ -18,7 +25,7 @@ function getUnderlyingAudioParam(signal) {
|
|
|
18
25
|
if (!param && !hasWarned) {
|
|
19
26
|
hasWarned = true;
|
|
20
27
|
console.warn(
|
|
21
|
-
"[waveform-playlist] Unable to access Tone.js internal _param. This likely means the Tone.js version is incompatible.
|
|
28
|
+
"[waveform-playlist] Unable to access Tone.js internal _param. This likely means the Tone.js version is incompatible. Mute scheduling may not work correctly."
|
|
22
29
|
);
|
|
23
30
|
}
|
|
24
31
|
return param;
|
|
@@ -112,13 +119,20 @@ function applyFadeOut(param, startTime, duration, type = "linear", startValue =
|
|
|
112
119
|
|
|
113
120
|
// src/ToneTrack.ts
|
|
114
121
|
var ToneTrack = class {
|
|
115
|
-
// Count of currently playing clips
|
|
116
122
|
constructor(options) {
|
|
117
|
-
this.
|
|
123
|
+
this.activeSources = /* @__PURE__ */ new Set();
|
|
124
|
+
// Guard against ghost tick schedule callbacks. After stop/start cycles with
|
|
125
|
+
// loops, stale Clock._lastUpdate causes ticks from the previous cycle to fire
|
|
126
|
+
// Transport.schedule() callbacks at past positions (e.g., time 0 clips fire
|
|
127
|
+
// when starting at offset 5s). Clips before this offset are handled by
|
|
128
|
+
// startMidClipSources(); schedule callbacks should only create sources for
|
|
129
|
+
// clips at/after this offset.
|
|
130
|
+
this._scheduleGuardOffset = 0;
|
|
118
131
|
this.track = options.track;
|
|
119
132
|
this.volumeNode = new Volume(this.gainToDb(options.track.gain));
|
|
120
133
|
this.panNode = new Panner(options.track.stereoPan);
|
|
121
134
|
this.muteGain = new Gain(options.track.muted ? 0 : 1);
|
|
135
|
+
this.volumeNode.chain(this.panNode, this.muteGain);
|
|
122
136
|
const destination = options.destination || getDestination();
|
|
123
137
|
if (options.effects) {
|
|
124
138
|
const cleanup = options.effects(this.muteGain, destination, false);
|
|
@@ -132,45 +146,112 @@ var ToneTrack = class {
|
|
|
132
146
|
{
|
|
133
147
|
buffer: options.buffer,
|
|
134
148
|
startTime: 0,
|
|
135
|
-
// Legacy: single buffer starts at timeline position 0
|
|
136
149
|
duration: options.buffer.duration,
|
|
137
|
-
// Legacy: play full buffer duration
|
|
138
150
|
offset: 0,
|
|
139
151
|
fadeIn: options.track.fadeIn,
|
|
140
152
|
fadeOut: options.track.fadeOut,
|
|
141
153
|
gain: 1
|
|
142
154
|
}
|
|
143
155
|
] : []);
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
156
|
+
const transport = getTransport();
|
|
157
|
+
const rawContext = getContext().rawContext;
|
|
158
|
+
const volumeNativeInput = this.volumeNode.input.input;
|
|
159
|
+
this.scheduledClips = clipInfos.map((clipInfo) => {
|
|
160
|
+
const fadeGainNode = rawContext.createGain();
|
|
161
|
+
fadeGainNode.gain.value = clipInfo.gain;
|
|
162
|
+
fadeGainNode.connect(volumeNativeInput);
|
|
163
|
+
const absTransportTime = this.track.startTime + clipInfo.startTime;
|
|
164
|
+
const scheduleId = transport.schedule((audioContextTime) => {
|
|
165
|
+
if (absTransportTime < this._scheduleGuardOffset) {
|
|
166
|
+
return;
|
|
153
167
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
168
|
+
this.startClipSource(clipInfo, fadeGainNode, audioContextTime);
|
|
169
|
+
}, absTransportTime);
|
|
170
|
+
return { clipInfo, fadeGainNode, scheduleId };
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Create and start an AudioBufferSourceNode for a clip.
|
|
175
|
+
* Sources are one-shot: each play or loop iteration creates a fresh one.
|
|
176
|
+
*/
|
|
177
|
+
startClipSource(clipInfo, fadeGainNode, audioContextTime, bufferOffset, playDuration) {
|
|
178
|
+
const rawContext = getContext().rawContext;
|
|
179
|
+
const source = rawContext.createBufferSource();
|
|
180
|
+
source.buffer = clipInfo.buffer;
|
|
181
|
+
source.connect(fadeGainNode);
|
|
182
|
+
const offset = bufferOffset ?? clipInfo.offset;
|
|
183
|
+
const duration = playDuration ?? clipInfo.duration;
|
|
184
|
+
try {
|
|
185
|
+
source.start(audioContextTime, offset, duration);
|
|
186
|
+
} catch (err) {
|
|
187
|
+
console.warn(
|
|
188
|
+
`[waveform-playlist] Failed to start source on track "${this.id}" (time=${audioContextTime}, offset=${offset}, duration=${duration}):`,
|
|
189
|
+
err
|
|
190
|
+
);
|
|
191
|
+
source.disconnect();
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
this.activeSources.add(source);
|
|
195
|
+
source.onended = () => {
|
|
196
|
+
this.activeSources.delete(source);
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Set the schedule guard offset. Schedule callbacks for clips before this
|
|
201
|
+
* offset are suppressed (already handled by startMidClipSources).
|
|
202
|
+
* Must be called before transport.start() and in the loop handler.
|
|
203
|
+
*/
|
|
204
|
+
setScheduleGuardOffset(offset) {
|
|
205
|
+
this._scheduleGuardOffset = offset;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Start sources for clips that span the given Transport position.
|
|
209
|
+
* Used for mid-playback seeking and loop boundary handling where
|
|
210
|
+
* Transport.schedule() callbacks have already passed.
|
|
211
|
+
*
|
|
212
|
+
* Uses strict < for absClipStart to avoid double-creation with
|
|
213
|
+
* schedule callbacks at exact Transport position (e.g., loopStart).
|
|
214
|
+
*/
|
|
215
|
+
startMidClipSources(transportOffset, audioContextTime) {
|
|
216
|
+
for (const { clipInfo, fadeGainNode } of this.scheduledClips) {
|
|
217
|
+
const absClipStart = this.track.startTime + clipInfo.startTime;
|
|
218
|
+
const absClipEnd = absClipStart + clipInfo.duration;
|
|
219
|
+
if (absClipStart < transportOffset && absClipEnd > transportOffset) {
|
|
220
|
+
const elapsed = transportOffset - absClipStart;
|
|
221
|
+
const adjustedOffset = clipInfo.offset + elapsed;
|
|
222
|
+
const remainingDuration = clipInfo.duration - elapsed;
|
|
223
|
+
this.startClipSource(
|
|
224
|
+
clipInfo,
|
|
225
|
+
fadeGainNode,
|
|
226
|
+
audioContextTime,
|
|
227
|
+
adjustedOffset,
|
|
228
|
+
remainingDuration
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Stop all active AudioBufferSourceNodes and clear the set.
|
|
235
|
+
* Native AudioBufferSourceNodes ignore Transport state changes —
|
|
236
|
+
* they must be explicitly stopped.
|
|
237
|
+
*/
|
|
238
|
+
stopAllSources() {
|
|
239
|
+
this.activeSources.forEach((source) => {
|
|
240
|
+
try {
|
|
241
|
+
source.stop();
|
|
242
|
+
} catch (err) {
|
|
243
|
+
console.warn(`[waveform-playlist] Error stopping source on track "${this.id}":`, err);
|
|
244
|
+
}
|
|
165
245
|
});
|
|
246
|
+
this.activeSources.clear();
|
|
166
247
|
}
|
|
167
248
|
/**
|
|
168
|
-
* Schedule fade envelopes for a clip at the given
|
|
249
|
+
* Schedule fade envelopes for a clip at the given AudioContext time.
|
|
250
|
+
* Uses native GainNode.gain (AudioParam) directly — no _param workaround needed.
|
|
169
251
|
*/
|
|
170
|
-
scheduleFades(
|
|
171
|
-
const { clipInfo,
|
|
172
|
-
const audioParam =
|
|
173
|
-
if (!audioParam) return;
|
|
252
|
+
scheduleFades(scheduled, clipStartTime2, clipOffset = 0) {
|
|
253
|
+
const { clipInfo, fadeGainNode } = scheduled;
|
|
254
|
+
const audioParam = fadeGainNode.gain;
|
|
174
255
|
audioParam.cancelScheduledValues(0);
|
|
175
256
|
const skipTime = clipOffset - clipInfo.offset;
|
|
176
257
|
if (clipInfo.fadeIn && skipTime < clipInfo.fadeIn.duration) {
|
|
@@ -229,6 +310,35 @@ var ToneTrack = class {
|
|
|
229
310
|
}
|
|
230
311
|
}
|
|
231
312
|
}
|
|
313
|
+
/**
|
|
314
|
+
* Prepare fade envelopes for all clips based on Transport offset.
|
|
315
|
+
* Called before Transport.start() to schedule fades at correct AudioContext times.
|
|
316
|
+
*/
|
|
317
|
+
prepareFades(when, transportOffset) {
|
|
318
|
+
this.scheduledClips.forEach((scheduled) => {
|
|
319
|
+
const absClipStart = this.track.startTime + scheduled.clipInfo.startTime;
|
|
320
|
+
const absClipEnd = absClipStart + scheduled.clipInfo.duration;
|
|
321
|
+
if (transportOffset >= absClipEnd) return;
|
|
322
|
+
if (transportOffset >= absClipStart) {
|
|
323
|
+
const clipOffset = transportOffset - absClipStart + scheduled.clipInfo.offset;
|
|
324
|
+
this.scheduleFades(scheduled, when, clipOffset);
|
|
325
|
+
} else {
|
|
326
|
+
const delay = absClipStart - transportOffset;
|
|
327
|
+
this.scheduleFades(scheduled, when + delay, scheduled.clipInfo.offset);
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Cancel all scheduled fade automation and reset to nominal gain.
|
|
333
|
+
* Called on pause/stop to prevent stale fade envelopes.
|
|
334
|
+
*/
|
|
335
|
+
cancelFades() {
|
|
336
|
+
this.scheduledClips.forEach(({ fadeGainNode, clipInfo }) => {
|
|
337
|
+
const audioParam = fadeGainNode.gain;
|
|
338
|
+
audioParam.cancelScheduledValues(0);
|
|
339
|
+
audioParam.setValueAtTime(clipInfo.gain, 0);
|
|
340
|
+
});
|
|
341
|
+
}
|
|
232
342
|
gainToDb(gain) {
|
|
233
343
|
return 20 * Math.log10(gain);
|
|
234
344
|
}
|
|
@@ -250,99 +360,60 @@ var ToneTrack = class {
|
|
|
250
360
|
setSolo(soloed) {
|
|
251
361
|
this.track.soloed = soloed;
|
|
252
362
|
}
|
|
253
|
-
play(when, offset = 0, duration) {
|
|
254
|
-
this.clips.forEach((clipPlayer) => {
|
|
255
|
-
clipPlayer.player.stop();
|
|
256
|
-
clipPlayer.player.disconnect();
|
|
257
|
-
clipPlayer.player.dispose();
|
|
258
|
-
const newPlayer = new Player({
|
|
259
|
-
url: clipPlayer.clipInfo.buffer,
|
|
260
|
-
loop: false,
|
|
261
|
-
onstop: () => {
|
|
262
|
-
this.activePlayers--;
|
|
263
|
-
if (this.activePlayers === 0 && this.onStopCallback) {
|
|
264
|
-
this.onStopCallback();
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
});
|
|
268
|
-
newPlayer.connect(clipPlayer.fadeGain);
|
|
269
|
-
clipPlayer.player = newPlayer;
|
|
270
|
-
clipPlayer.pausedPosition = 0;
|
|
271
|
-
});
|
|
272
|
-
this.activePlayers = 0;
|
|
273
|
-
this.clips.forEach((clipPlayer) => {
|
|
274
|
-
const { player, clipInfo } = clipPlayer;
|
|
275
|
-
const playbackPosition = offset;
|
|
276
|
-
const clipStart = clipInfo.startTime;
|
|
277
|
-
const clipEnd = clipInfo.startTime + clipInfo.duration;
|
|
278
|
-
if (playbackPosition < clipEnd) {
|
|
279
|
-
this.activePlayers++;
|
|
280
|
-
const currentTime = when ?? now();
|
|
281
|
-
clipPlayer.playStartTime = currentTime;
|
|
282
|
-
if (playbackPosition >= clipStart) {
|
|
283
|
-
const clipOffset = playbackPosition - clipStart + clipInfo.offset;
|
|
284
|
-
const remainingDuration = clipInfo.duration - (playbackPosition - clipStart);
|
|
285
|
-
const clipDuration = duration ? Math.min(duration, remainingDuration) : remainingDuration;
|
|
286
|
-
clipPlayer.pausedPosition = clipOffset;
|
|
287
|
-
this.scheduleFades(clipPlayer, currentTime, clipOffset);
|
|
288
|
-
player.start(currentTime, clipOffset, clipDuration);
|
|
289
|
-
} else {
|
|
290
|
-
const delay = clipStart - playbackPosition;
|
|
291
|
-
const clipDuration = duration ? Math.min(duration - delay, clipInfo.duration) : clipInfo.duration;
|
|
292
|
-
if (delay < (duration ?? Infinity)) {
|
|
293
|
-
clipPlayer.pausedPosition = clipInfo.offset;
|
|
294
|
-
this.scheduleFades(clipPlayer, currentTime + delay, clipInfo.offset);
|
|
295
|
-
player.start(currentTime + delay, clipInfo.offset, clipDuration);
|
|
296
|
-
} else {
|
|
297
|
-
this.activePlayers--;
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
});
|
|
302
|
-
}
|
|
303
|
-
pause() {
|
|
304
|
-
this.clips.forEach((clipPlayer) => {
|
|
305
|
-
if (clipPlayer.player.state === "started") {
|
|
306
|
-
const elapsed = (now() - clipPlayer.playStartTime) * clipPlayer.player.playbackRate;
|
|
307
|
-
clipPlayer.pausedPosition = clipPlayer.pausedPosition + elapsed;
|
|
308
|
-
}
|
|
309
|
-
clipPlayer.player.stop();
|
|
310
|
-
});
|
|
311
|
-
this.activePlayers = 0;
|
|
312
|
-
}
|
|
313
|
-
stop(when) {
|
|
314
|
-
const stopWhen = when ?? now();
|
|
315
|
-
this.clips.forEach((clipPlayer) => {
|
|
316
|
-
clipPlayer.player.stop(stopWhen);
|
|
317
|
-
clipPlayer.pausedPosition = 0;
|
|
318
|
-
});
|
|
319
|
-
this.activePlayers = 0;
|
|
320
|
-
}
|
|
321
363
|
dispose() {
|
|
364
|
+
const transport = getTransport();
|
|
322
365
|
if (this.effectsCleanup) {
|
|
323
|
-
|
|
366
|
+
try {
|
|
367
|
+
this.effectsCleanup();
|
|
368
|
+
} catch (err) {
|
|
369
|
+
console.warn(`[waveform-playlist] Error during track "${this.id}" effects cleanup:`, err);
|
|
370
|
+
}
|
|
324
371
|
}
|
|
325
|
-
this.
|
|
326
|
-
|
|
327
|
-
|
|
372
|
+
this.stopAllSources();
|
|
373
|
+
this.scheduledClips.forEach((scheduled, index) => {
|
|
374
|
+
try {
|
|
375
|
+
transport.clear(scheduled.scheduleId);
|
|
376
|
+
} catch (err) {
|
|
377
|
+
console.warn(
|
|
378
|
+
`[waveform-playlist] Error clearing schedule ${index} on track "${this.id}":`,
|
|
379
|
+
err
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
try {
|
|
383
|
+
scheduled.fadeGainNode.disconnect();
|
|
384
|
+
} catch (err) {
|
|
385
|
+
console.warn(
|
|
386
|
+
`[waveform-playlist] Error disconnecting fadeGain ${index} on track "${this.id}":`,
|
|
387
|
+
err
|
|
388
|
+
);
|
|
389
|
+
}
|
|
328
390
|
});
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
391
|
+
try {
|
|
392
|
+
this.volumeNode.dispose();
|
|
393
|
+
} catch (err) {
|
|
394
|
+
console.warn(`[waveform-playlist] Error disposing volumeNode on track "${this.id}":`, err);
|
|
395
|
+
}
|
|
396
|
+
try {
|
|
397
|
+
this.panNode.dispose();
|
|
398
|
+
} catch (err) {
|
|
399
|
+
console.warn(`[waveform-playlist] Error disposing panNode on track "${this.id}":`, err);
|
|
400
|
+
}
|
|
401
|
+
try {
|
|
402
|
+
this.muteGain.dispose();
|
|
403
|
+
} catch (err) {
|
|
404
|
+
console.warn(`[waveform-playlist] Error disposing muteGain on track "${this.id}":`, err);
|
|
405
|
+
}
|
|
332
406
|
}
|
|
333
407
|
get id() {
|
|
334
408
|
return this.track.id;
|
|
335
409
|
}
|
|
336
410
|
get duration() {
|
|
337
|
-
if (this.
|
|
338
|
-
const lastClip = this.
|
|
411
|
+
if (this.scheduledClips.length === 0) return 0;
|
|
412
|
+
const lastClip = this.scheduledClips[this.scheduledClips.length - 1];
|
|
339
413
|
return lastClip.clipInfo.startTime + lastClip.clipInfo.duration;
|
|
340
414
|
}
|
|
341
415
|
get buffer() {
|
|
342
|
-
return this.
|
|
343
|
-
}
|
|
344
|
-
get isPlaying() {
|
|
345
|
-
return this.clips.some((clipPlayer) => clipPlayer.player.state === "started");
|
|
416
|
+
return this.scheduledClips[0]?.clipInfo.buffer;
|
|
346
417
|
}
|
|
347
418
|
get muted() {
|
|
348
419
|
return this.track.muted;
|
|
@@ -350,9 +421,6 @@ var ToneTrack = class {
|
|
|
350
421
|
get startTime() {
|
|
351
422
|
return this.track.startTime;
|
|
352
423
|
}
|
|
353
|
-
setOnStopCallback(callback) {
|
|
354
|
-
this.onStopCallback = callback;
|
|
355
|
-
}
|
|
356
424
|
};
|
|
357
425
|
|
|
358
426
|
// src/TonePlayout.ts
|
|
@@ -362,9 +430,11 @@ var TonePlayout = class {
|
|
|
362
430
|
this.isInitialized = false;
|
|
363
431
|
this.soloedTracks = /* @__PURE__ */ new Set();
|
|
364
432
|
this.manualMuteState = /* @__PURE__ */ new Map();
|
|
365
|
-
this.
|
|
366
|
-
|
|
367
|
-
this.
|
|
433
|
+
this._completionEventId = null;
|
|
434
|
+
this._loopHandler = null;
|
|
435
|
+
this._loopEnabled = false;
|
|
436
|
+
this._loopStart = 0;
|
|
437
|
+
this._loopEnd = 0;
|
|
368
438
|
this.masterVolume = new Volume2(this.gainToDb(options.masterGain ?? 1));
|
|
369
439
|
if (options.effects) {
|
|
370
440
|
const cleanup = options.effects(this.masterVolume, getDestination2(), false);
|
|
@@ -384,6 +454,16 @@ var TonePlayout = class {
|
|
|
384
454
|
gainToDb(gain) {
|
|
385
455
|
return 20 * Math.log10(gain);
|
|
386
456
|
}
|
|
457
|
+
clearCompletionEvent() {
|
|
458
|
+
if (this._completionEventId !== null) {
|
|
459
|
+
try {
|
|
460
|
+
getTransport2().clear(this._completionEventId);
|
|
461
|
+
} catch (err) {
|
|
462
|
+
console.warn("[waveform-playlist] Error clearing Transport completion event:", err);
|
|
463
|
+
}
|
|
464
|
+
this._completionEventId = null;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
387
467
|
async init() {
|
|
388
468
|
if (this.isInitialized) return;
|
|
389
469
|
await start();
|
|
@@ -423,63 +503,75 @@ var TonePlayout = class {
|
|
|
423
503
|
}
|
|
424
504
|
play(when, offset, duration) {
|
|
425
505
|
if (!this.isInitialized) {
|
|
426
|
-
|
|
427
|
-
return;
|
|
506
|
+
throw new Error("[waveform-playlist] TonePlayout not initialized. Call init() first.");
|
|
428
507
|
}
|
|
429
|
-
const startTime = when ??
|
|
430
|
-
const
|
|
431
|
-
this.
|
|
432
|
-
const
|
|
433
|
-
this.
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
this.onPlaybackCompleteCallback();
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
});
|
|
508
|
+
const startTime = when ?? now();
|
|
509
|
+
const transport = getTransport2();
|
|
510
|
+
this.clearCompletionEvent();
|
|
511
|
+
const transportOffset = offset ?? 0;
|
|
512
|
+
this.tracks.forEach((track) => {
|
|
513
|
+
track.cancelFades();
|
|
514
|
+
track.prepareFades(startTime, transportOffset);
|
|
515
|
+
});
|
|
516
|
+
if (duration !== void 0) {
|
|
517
|
+
this._completionEventId = transport.scheduleOnce(() => {
|
|
518
|
+
this._completionEventId = null;
|
|
519
|
+
try {
|
|
520
|
+
this.onPlaybackCompleteCallback?.();
|
|
521
|
+
} catch (err) {
|
|
522
|
+
console.warn("[waveform-playlist] Error in playback completion callback:", err);
|
|
448
523
|
}
|
|
449
|
-
|
|
524
|
+
}, transportOffset + duration);
|
|
525
|
+
}
|
|
526
|
+
try {
|
|
527
|
+
if (transport.state !== "stopped") {
|
|
528
|
+
transport.stop();
|
|
529
|
+
}
|
|
530
|
+
this.tracks.forEach((track) => track.stopAllSources());
|
|
531
|
+
transport.loopStart = this._loopStart;
|
|
532
|
+
transport.loopEnd = this._loopEnd;
|
|
533
|
+
transport.loop = this._loopEnabled;
|
|
534
|
+
this.tracks.forEach((track) => track.setScheduleGuardOffset(transportOffset));
|
|
535
|
+
if (offset !== void 0) {
|
|
536
|
+
transport.start(startTime, offset);
|
|
450
537
|
} else {
|
|
451
|
-
|
|
452
|
-
if (duration !== void 0) {
|
|
453
|
-
this.activeTracks.set(toneTrack.id, currentSessionId);
|
|
454
|
-
toneTrack.setOnStopCallback(() => {
|
|
455
|
-
if (this.activeTracks.get(toneTrack.id) === currentSessionId) {
|
|
456
|
-
this.activeTracks.delete(toneTrack.id);
|
|
457
|
-
if (this.activeTracks.size === 0 && this.onPlaybackCompleteCallback) {
|
|
458
|
-
this.onPlaybackCompleteCallback();
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
});
|
|
462
|
-
}
|
|
463
|
-
toneTrack.play(startTime + delay, 0, duration);
|
|
538
|
+
transport.start(startTime);
|
|
464
539
|
}
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
540
|
+
transport._clock._lastUpdate = startTime;
|
|
541
|
+
this.tracks.forEach((track) => {
|
|
542
|
+
track.startMidClipSources(transportOffset, startTime);
|
|
543
|
+
});
|
|
544
|
+
} catch (err) {
|
|
545
|
+
this.clearCompletionEvent();
|
|
546
|
+
this.tracks.forEach((track) => track.cancelFades());
|
|
547
|
+
console.warn(
|
|
548
|
+
"[waveform-playlist] Transport.start() failed. Audio playback could not begin.",
|
|
549
|
+
err
|
|
550
|
+
);
|
|
551
|
+
throw err;
|
|
470
552
|
}
|
|
471
553
|
}
|
|
472
554
|
pause() {
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
})
|
|
555
|
+
const transport = getTransport2();
|
|
556
|
+
try {
|
|
557
|
+
transport.pause();
|
|
558
|
+
} catch (err) {
|
|
559
|
+
console.warn("[waveform-playlist] Transport.pause() failed:", err);
|
|
560
|
+
}
|
|
561
|
+
this.tracks.forEach((track) => track.stopAllSources());
|
|
562
|
+
this.tracks.forEach((track) => track.cancelFades());
|
|
563
|
+
this.clearCompletionEvent();
|
|
477
564
|
}
|
|
478
565
|
stop() {
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
})
|
|
566
|
+
const transport = getTransport2();
|
|
567
|
+
try {
|
|
568
|
+
transport.stop();
|
|
569
|
+
} catch (err) {
|
|
570
|
+
console.warn("[waveform-playlist] Transport.stop() failed:", err);
|
|
571
|
+
}
|
|
572
|
+
this.tracks.forEach((track) => track.stopAllSources());
|
|
573
|
+
this.tracks.forEach((track) => track.cancelFades());
|
|
574
|
+
this.clearCompletionEvent();
|
|
483
575
|
}
|
|
484
576
|
setMasterGain(gain) {
|
|
485
577
|
this.masterVolume.volume.value = this.gainToDb(gain);
|
|
@@ -519,27 +611,85 @@ var TonePlayout = class {
|
|
|
519
611
|
track.setMute(muted);
|
|
520
612
|
}
|
|
521
613
|
}
|
|
614
|
+
setLoop(enabled, loopStart, loopEnd) {
|
|
615
|
+
this._loopEnabled = enabled;
|
|
616
|
+
this._loopStart = loopStart;
|
|
617
|
+
this._loopEnd = loopEnd;
|
|
618
|
+
const transport = getTransport2();
|
|
619
|
+
try {
|
|
620
|
+
transport.loopStart = loopStart;
|
|
621
|
+
transport.loopEnd = loopEnd;
|
|
622
|
+
transport.loop = enabled;
|
|
623
|
+
} catch (err) {
|
|
624
|
+
console.warn("[waveform-playlist] Error configuring Transport loop:", err);
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
if (enabled && !this._loopHandler) {
|
|
628
|
+
this._loopHandler = () => {
|
|
629
|
+
const currentTime = now();
|
|
630
|
+
this.tracks.forEach((track) => {
|
|
631
|
+
try {
|
|
632
|
+
track.stopAllSources();
|
|
633
|
+
track.cancelFades();
|
|
634
|
+
track.setScheduleGuardOffset(this._loopStart);
|
|
635
|
+
track.startMidClipSources(this._loopStart, currentTime);
|
|
636
|
+
track.prepareFades(currentTime, this._loopStart);
|
|
637
|
+
} catch (err) {
|
|
638
|
+
console.warn(
|
|
639
|
+
`[waveform-playlist] Error re-scheduling track "${track.id}" on loop:`,
|
|
640
|
+
err
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
});
|
|
644
|
+
};
|
|
645
|
+
transport.on("loop", this._loopHandler);
|
|
646
|
+
} else if (!enabled && this._loopHandler) {
|
|
647
|
+
transport.off("loop", this._loopHandler);
|
|
648
|
+
this._loopHandler = null;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
522
651
|
getCurrentTime() {
|
|
523
|
-
return
|
|
652
|
+
return getTransport2().seconds;
|
|
524
653
|
}
|
|
525
654
|
seekTo(time) {
|
|
526
|
-
|
|
655
|
+
getTransport2().seconds = time;
|
|
527
656
|
}
|
|
528
657
|
dispose() {
|
|
658
|
+
this.clearCompletionEvent();
|
|
659
|
+
if (this._loopHandler) {
|
|
660
|
+
try {
|
|
661
|
+
getTransport2().off("loop", this._loopHandler);
|
|
662
|
+
} catch (err) {
|
|
663
|
+
console.warn("[waveform-playlist] Error removing Transport loop handler:", err);
|
|
664
|
+
}
|
|
665
|
+
this._loopHandler = null;
|
|
666
|
+
}
|
|
529
667
|
this.tracks.forEach((track) => {
|
|
530
|
-
|
|
668
|
+
try {
|
|
669
|
+
track.dispose();
|
|
670
|
+
} catch (err) {
|
|
671
|
+
console.warn(`[waveform-playlist] Error disposing track "${track.id}":`, err);
|
|
672
|
+
}
|
|
531
673
|
});
|
|
532
674
|
this.tracks.clear();
|
|
533
675
|
if (this.effectsCleanup) {
|
|
534
|
-
|
|
676
|
+
try {
|
|
677
|
+
this.effectsCleanup();
|
|
678
|
+
} catch (err) {
|
|
679
|
+
console.warn("[waveform-playlist] Error during master effects cleanup:", err);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
try {
|
|
683
|
+
this.masterVolume.dispose();
|
|
684
|
+
} catch (err) {
|
|
685
|
+
console.warn("[waveform-playlist] Error disposing master volume:", err);
|
|
535
686
|
}
|
|
536
|
-
this.masterVolume.dispose();
|
|
537
687
|
}
|
|
538
688
|
get context() {
|
|
539
|
-
return
|
|
689
|
+
return getContext2();
|
|
540
690
|
}
|
|
541
691
|
get sampleRate() {
|
|
542
|
-
return
|
|
692
|
+
return getContext2().sampleRate;
|
|
543
693
|
}
|
|
544
694
|
setOnPlaybackComplete(callback) {
|
|
545
695
|
this.onPlaybackCompleteCallback = callback;
|
|
@@ -579,14 +729,14 @@ async function closeGlobalAudioContext() {
|
|
|
579
729
|
}
|
|
580
730
|
|
|
581
731
|
// src/mediaStreamSourceManager.ts
|
|
582
|
-
import { getContext as
|
|
732
|
+
import { getContext as getContext3 } from "tone";
|
|
583
733
|
var streamSources = /* @__PURE__ */ new Map();
|
|
584
734
|
var streamCleanupHandlers = /* @__PURE__ */ new Map();
|
|
585
735
|
function getMediaStreamSource(stream) {
|
|
586
736
|
if (streamSources.has(stream)) {
|
|
587
737
|
return streamSources.get(stream);
|
|
588
738
|
}
|
|
589
|
-
const context =
|
|
739
|
+
const context = getContext3();
|
|
590
740
|
const source = context.createMediaStreamSource(stream);
|
|
591
741
|
streamSources.set(stream, source);
|
|
592
742
|
const cleanup = () => {
|
|
@@ -618,20 +768,38 @@ import {
|
|
|
618
768
|
clipOffsetTime,
|
|
619
769
|
clipDurationTime
|
|
620
770
|
} from "@waveform-playlist/core";
|
|
621
|
-
import { now as
|
|
771
|
+
import { now as now2 } from "tone";
|
|
622
772
|
function createToneAdapter(options) {
|
|
623
773
|
let playout = null;
|
|
624
774
|
let _isPlaying = false;
|
|
625
775
|
let _playoutGeneration = 0;
|
|
776
|
+
let _loopEnabled = false;
|
|
777
|
+
let _loopStart = 0;
|
|
778
|
+
let _loopEnd = 0;
|
|
779
|
+
let _audioInitialized = false;
|
|
626
780
|
function buildPlayout(tracks) {
|
|
627
781
|
if (playout) {
|
|
628
|
-
|
|
782
|
+
try {
|
|
783
|
+
playout.dispose();
|
|
784
|
+
} catch (err) {
|
|
785
|
+
console.warn("[waveform-playlist] Error disposing previous playout during rebuild:", err);
|
|
786
|
+
}
|
|
787
|
+
playout = null;
|
|
629
788
|
}
|
|
630
789
|
_playoutGeneration++;
|
|
631
790
|
const generation = _playoutGeneration;
|
|
632
791
|
playout = new TonePlayout({
|
|
633
792
|
effects: options?.effects
|
|
634
793
|
});
|
|
794
|
+
if (_audioInitialized) {
|
|
795
|
+
playout.init().catch((err) => {
|
|
796
|
+
console.warn(
|
|
797
|
+
"[waveform-playlist] Failed to re-initialize playout after rebuild. Audio playback will require another user gesture.",
|
|
798
|
+
err
|
|
799
|
+
);
|
|
800
|
+
_audioInitialized = false;
|
|
801
|
+
});
|
|
802
|
+
}
|
|
635
803
|
for (const track of tracks) {
|
|
636
804
|
const playableClips = track.clips.filter((c) => c.audioBuffer);
|
|
637
805
|
if (playableClips.length === 0) continue;
|
|
@@ -663,6 +831,7 @@ function createToneAdapter(options) {
|
|
|
663
831
|
});
|
|
664
832
|
}
|
|
665
833
|
playout.applyInitialSoloState();
|
|
834
|
+
playout.setLoop(_loopEnabled, _loopStart, _loopEnd);
|
|
666
835
|
playout.setOnPlaybackComplete(() => {
|
|
667
836
|
if (generation === _playoutGeneration) {
|
|
668
837
|
_isPlaying = false;
|
|
@@ -673,16 +842,21 @@ function createToneAdapter(options) {
|
|
|
673
842
|
async init() {
|
|
674
843
|
if (playout) {
|
|
675
844
|
await playout.init();
|
|
845
|
+
_audioInitialized = true;
|
|
676
846
|
}
|
|
677
847
|
},
|
|
678
848
|
setTracks(tracks) {
|
|
679
849
|
buildPlayout(tracks);
|
|
680
850
|
},
|
|
681
|
-
|
|
682
|
-
if (!playout)
|
|
683
|
-
|
|
851
|
+
play(startTime, endTime) {
|
|
852
|
+
if (!playout) {
|
|
853
|
+
console.warn(
|
|
854
|
+
"[waveform-playlist] adapter.play() called but no playout is available. Tracks may not have been set, or the adapter was disposed."
|
|
855
|
+
);
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
684
858
|
const duration = endTime !== void 0 ? endTime - startTime : void 0;
|
|
685
|
-
playout.play(
|
|
859
|
+
playout.play(now2(), startTime, duration);
|
|
686
860
|
_isPlaying = true;
|
|
687
861
|
},
|
|
688
862
|
pause() {
|
|
@@ -717,8 +891,18 @@ function createToneAdapter(options) {
|
|
|
717
891
|
setTrackPan(trackId, pan) {
|
|
718
892
|
playout?.getTrack(trackId)?.setPan(pan);
|
|
719
893
|
},
|
|
894
|
+
setLoop(enabled, start2, end) {
|
|
895
|
+
_loopEnabled = enabled;
|
|
896
|
+
_loopStart = start2;
|
|
897
|
+
_loopEnd = end;
|
|
898
|
+
playout?.setLoop(enabled, start2, end);
|
|
899
|
+
},
|
|
720
900
|
dispose() {
|
|
721
|
-
|
|
901
|
+
try {
|
|
902
|
+
playout?.dispose();
|
|
903
|
+
} catch (err) {
|
|
904
|
+
console.warn("[waveform-playlist] Error disposing playout:", err);
|
|
905
|
+
}
|
|
722
906
|
playout = null;
|
|
723
907
|
_isPlaying = false;
|
|
724
908
|
}
|