@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.js
CHANGED
|
@@ -51,7 +51,7 @@ function getUnderlyingAudioParam(signal) {
|
|
|
51
51
|
if (!param && !hasWarned) {
|
|
52
52
|
hasWarned = true;
|
|
53
53
|
console.warn(
|
|
54
|
-
"[waveform-playlist] Unable to access Tone.js internal _param. This likely means the Tone.js version is incompatible.
|
|
54
|
+
"[waveform-playlist] Unable to access Tone.js internal _param. This likely means the Tone.js version is incompatible. Mute scheduling may not work correctly."
|
|
55
55
|
);
|
|
56
56
|
}
|
|
57
57
|
return param;
|
|
@@ -145,13 +145,20 @@ function applyFadeOut(param, startTime, duration, type = "linear", startValue =
|
|
|
145
145
|
|
|
146
146
|
// src/ToneTrack.ts
|
|
147
147
|
var ToneTrack = class {
|
|
148
|
-
// Count of currently playing clips
|
|
149
148
|
constructor(options) {
|
|
150
|
-
this.
|
|
149
|
+
this.activeSources = /* @__PURE__ */ new Set();
|
|
150
|
+
// Guard against ghost tick schedule callbacks. After stop/start cycles with
|
|
151
|
+
// loops, stale Clock._lastUpdate causes ticks from the previous cycle to fire
|
|
152
|
+
// Transport.schedule() callbacks at past positions (e.g., time 0 clips fire
|
|
153
|
+
// when starting at offset 5s). Clips before this offset are handled by
|
|
154
|
+
// startMidClipSources(); schedule callbacks should only create sources for
|
|
155
|
+
// clips at/after this offset.
|
|
156
|
+
this._scheduleGuardOffset = 0;
|
|
151
157
|
this.track = options.track;
|
|
152
158
|
this.volumeNode = new import_tone.Volume(this.gainToDb(options.track.gain));
|
|
153
159
|
this.panNode = new import_tone.Panner(options.track.stereoPan);
|
|
154
160
|
this.muteGain = new import_tone.Gain(options.track.muted ? 0 : 1);
|
|
161
|
+
this.volumeNode.chain(this.panNode, this.muteGain);
|
|
155
162
|
const destination = options.destination || (0, import_tone.getDestination)();
|
|
156
163
|
if (options.effects) {
|
|
157
164
|
const cleanup = options.effects(this.muteGain, destination, false);
|
|
@@ -165,45 +172,112 @@ var ToneTrack = class {
|
|
|
165
172
|
{
|
|
166
173
|
buffer: options.buffer,
|
|
167
174
|
startTime: 0,
|
|
168
|
-
// Legacy: single buffer starts at timeline position 0
|
|
169
175
|
duration: options.buffer.duration,
|
|
170
|
-
// Legacy: play full buffer duration
|
|
171
176
|
offset: 0,
|
|
172
177
|
fadeIn: options.track.fadeIn,
|
|
173
178
|
fadeOut: options.track.fadeOut,
|
|
174
179
|
gain: 1
|
|
175
180
|
}
|
|
176
181
|
] : []);
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
182
|
+
const transport = (0, import_tone.getTransport)();
|
|
183
|
+
const rawContext = (0, import_tone.getContext)().rawContext;
|
|
184
|
+
const volumeNativeInput = this.volumeNode.input.input;
|
|
185
|
+
this.scheduledClips = clipInfos.map((clipInfo) => {
|
|
186
|
+
const fadeGainNode = rawContext.createGain();
|
|
187
|
+
fadeGainNode.gain.value = clipInfo.gain;
|
|
188
|
+
fadeGainNode.connect(volumeNativeInput);
|
|
189
|
+
const absTransportTime = this.track.startTime + clipInfo.startTime;
|
|
190
|
+
const scheduleId = transport.schedule((audioContextTime) => {
|
|
191
|
+
if (absTransportTime < this._scheduleGuardOffset) {
|
|
192
|
+
return;
|
|
186
193
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
fadeGain.chain(this.volumeNode, this.panNode, this.muteGain);
|
|
191
|
-
return {
|
|
192
|
-
player,
|
|
193
|
-
clipInfo,
|
|
194
|
-
fadeGain,
|
|
195
|
-
pausedPosition: 0,
|
|
196
|
-
playStartTime: 0
|
|
197
|
-
};
|
|
194
|
+
this.startClipSource(clipInfo, fadeGainNode, audioContextTime);
|
|
195
|
+
}, absTransportTime);
|
|
196
|
+
return { clipInfo, fadeGainNode, scheduleId };
|
|
198
197
|
});
|
|
199
198
|
}
|
|
200
199
|
/**
|
|
201
|
-
*
|
|
200
|
+
* Create and start an AudioBufferSourceNode for a clip.
|
|
201
|
+
* Sources are one-shot: each play or loop iteration creates a fresh one.
|
|
202
202
|
*/
|
|
203
|
-
|
|
204
|
-
const
|
|
205
|
-
const
|
|
206
|
-
|
|
203
|
+
startClipSource(clipInfo, fadeGainNode, audioContextTime, bufferOffset, playDuration) {
|
|
204
|
+
const rawContext = (0, import_tone.getContext)().rawContext;
|
|
205
|
+
const source = rawContext.createBufferSource();
|
|
206
|
+
source.buffer = clipInfo.buffer;
|
|
207
|
+
source.connect(fadeGainNode);
|
|
208
|
+
const offset = bufferOffset ?? clipInfo.offset;
|
|
209
|
+
const duration = playDuration ?? clipInfo.duration;
|
|
210
|
+
try {
|
|
211
|
+
source.start(audioContextTime, offset, duration);
|
|
212
|
+
} catch (err) {
|
|
213
|
+
console.warn(
|
|
214
|
+
`[waveform-playlist] Failed to start source on track "${this.id}" (time=${audioContextTime}, offset=${offset}, duration=${duration}):`,
|
|
215
|
+
err
|
|
216
|
+
);
|
|
217
|
+
source.disconnect();
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
this.activeSources.add(source);
|
|
221
|
+
source.onended = () => {
|
|
222
|
+
this.activeSources.delete(source);
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Set the schedule guard offset. Schedule callbacks for clips before this
|
|
227
|
+
* offset are suppressed (already handled by startMidClipSources).
|
|
228
|
+
* Must be called before transport.start() and in the loop handler.
|
|
229
|
+
*/
|
|
230
|
+
setScheduleGuardOffset(offset) {
|
|
231
|
+
this._scheduleGuardOffset = offset;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Start sources for clips that span the given Transport position.
|
|
235
|
+
* Used for mid-playback seeking and loop boundary handling where
|
|
236
|
+
* Transport.schedule() callbacks have already passed.
|
|
237
|
+
*
|
|
238
|
+
* Uses strict < for absClipStart to avoid double-creation with
|
|
239
|
+
* schedule callbacks at exact Transport position (e.g., loopStart).
|
|
240
|
+
*/
|
|
241
|
+
startMidClipSources(transportOffset, audioContextTime) {
|
|
242
|
+
for (const { clipInfo, fadeGainNode } of this.scheduledClips) {
|
|
243
|
+
const absClipStart = this.track.startTime + clipInfo.startTime;
|
|
244
|
+
const absClipEnd = absClipStart + clipInfo.duration;
|
|
245
|
+
if (absClipStart < transportOffset && absClipEnd > transportOffset) {
|
|
246
|
+
const elapsed = transportOffset - absClipStart;
|
|
247
|
+
const adjustedOffset = clipInfo.offset + elapsed;
|
|
248
|
+
const remainingDuration = clipInfo.duration - elapsed;
|
|
249
|
+
this.startClipSource(
|
|
250
|
+
clipInfo,
|
|
251
|
+
fadeGainNode,
|
|
252
|
+
audioContextTime,
|
|
253
|
+
adjustedOffset,
|
|
254
|
+
remainingDuration
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Stop all active AudioBufferSourceNodes and clear the set.
|
|
261
|
+
* Native AudioBufferSourceNodes ignore Transport state changes —
|
|
262
|
+
* they must be explicitly stopped.
|
|
263
|
+
*/
|
|
264
|
+
stopAllSources() {
|
|
265
|
+
this.activeSources.forEach((source) => {
|
|
266
|
+
try {
|
|
267
|
+
source.stop();
|
|
268
|
+
} catch (err) {
|
|
269
|
+
console.warn(`[waveform-playlist] Error stopping source on track "${this.id}":`, err);
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
this.activeSources.clear();
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Schedule fade envelopes for a clip at the given AudioContext time.
|
|
276
|
+
* Uses native GainNode.gain (AudioParam) directly — no _param workaround needed.
|
|
277
|
+
*/
|
|
278
|
+
scheduleFades(scheduled, clipStartTime2, clipOffset = 0) {
|
|
279
|
+
const { clipInfo, fadeGainNode } = scheduled;
|
|
280
|
+
const audioParam = fadeGainNode.gain;
|
|
207
281
|
audioParam.cancelScheduledValues(0);
|
|
208
282
|
const skipTime = clipOffset - clipInfo.offset;
|
|
209
283
|
if (clipInfo.fadeIn && skipTime < clipInfo.fadeIn.duration) {
|
|
@@ -262,6 +336,35 @@ var ToneTrack = class {
|
|
|
262
336
|
}
|
|
263
337
|
}
|
|
264
338
|
}
|
|
339
|
+
/**
|
|
340
|
+
* Prepare fade envelopes for all clips based on Transport offset.
|
|
341
|
+
* Called before Transport.start() to schedule fades at correct AudioContext times.
|
|
342
|
+
*/
|
|
343
|
+
prepareFades(when, transportOffset) {
|
|
344
|
+
this.scheduledClips.forEach((scheduled) => {
|
|
345
|
+
const absClipStart = this.track.startTime + scheduled.clipInfo.startTime;
|
|
346
|
+
const absClipEnd = absClipStart + scheduled.clipInfo.duration;
|
|
347
|
+
if (transportOffset >= absClipEnd) return;
|
|
348
|
+
if (transportOffset >= absClipStart) {
|
|
349
|
+
const clipOffset = transportOffset - absClipStart + scheduled.clipInfo.offset;
|
|
350
|
+
this.scheduleFades(scheduled, when, clipOffset);
|
|
351
|
+
} else {
|
|
352
|
+
const delay = absClipStart - transportOffset;
|
|
353
|
+
this.scheduleFades(scheduled, when + delay, scheduled.clipInfo.offset);
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Cancel all scheduled fade automation and reset to nominal gain.
|
|
359
|
+
* Called on pause/stop to prevent stale fade envelopes.
|
|
360
|
+
*/
|
|
361
|
+
cancelFades() {
|
|
362
|
+
this.scheduledClips.forEach(({ fadeGainNode, clipInfo }) => {
|
|
363
|
+
const audioParam = fadeGainNode.gain;
|
|
364
|
+
audioParam.cancelScheduledValues(0);
|
|
365
|
+
audioParam.setValueAtTime(clipInfo.gain, 0);
|
|
366
|
+
});
|
|
367
|
+
}
|
|
265
368
|
gainToDb(gain) {
|
|
266
369
|
return 20 * Math.log10(gain);
|
|
267
370
|
}
|
|
@@ -283,99 +386,60 @@ var ToneTrack = class {
|
|
|
283
386
|
setSolo(soloed) {
|
|
284
387
|
this.track.soloed = soloed;
|
|
285
388
|
}
|
|
286
|
-
play(when, offset = 0, duration) {
|
|
287
|
-
this.clips.forEach((clipPlayer) => {
|
|
288
|
-
clipPlayer.player.stop();
|
|
289
|
-
clipPlayer.player.disconnect();
|
|
290
|
-
clipPlayer.player.dispose();
|
|
291
|
-
const newPlayer = new import_tone.Player({
|
|
292
|
-
url: clipPlayer.clipInfo.buffer,
|
|
293
|
-
loop: false,
|
|
294
|
-
onstop: () => {
|
|
295
|
-
this.activePlayers--;
|
|
296
|
-
if (this.activePlayers === 0 && this.onStopCallback) {
|
|
297
|
-
this.onStopCallback();
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
});
|
|
301
|
-
newPlayer.connect(clipPlayer.fadeGain);
|
|
302
|
-
clipPlayer.player = newPlayer;
|
|
303
|
-
clipPlayer.pausedPosition = 0;
|
|
304
|
-
});
|
|
305
|
-
this.activePlayers = 0;
|
|
306
|
-
this.clips.forEach((clipPlayer) => {
|
|
307
|
-
const { player, clipInfo } = clipPlayer;
|
|
308
|
-
const playbackPosition = offset;
|
|
309
|
-
const clipStart = clipInfo.startTime;
|
|
310
|
-
const clipEnd = clipInfo.startTime + clipInfo.duration;
|
|
311
|
-
if (playbackPosition < clipEnd) {
|
|
312
|
-
this.activePlayers++;
|
|
313
|
-
const currentTime = when ?? (0, import_tone.now)();
|
|
314
|
-
clipPlayer.playStartTime = currentTime;
|
|
315
|
-
if (playbackPosition >= clipStart) {
|
|
316
|
-
const clipOffset = playbackPosition - clipStart + clipInfo.offset;
|
|
317
|
-
const remainingDuration = clipInfo.duration - (playbackPosition - clipStart);
|
|
318
|
-
const clipDuration = duration ? Math.min(duration, remainingDuration) : remainingDuration;
|
|
319
|
-
clipPlayer.pausedPosition = clipOffset;
|
|
320
|
-
this.scheduleFades(clipPlayer, currentTime, clipOffset);
|
|
321
|
-
player.start(currentTime, clipOffset, clipDuration);
|
|
322
|
-
} else {
|
|
323
|
-
const delay = clipStart - playbackPosition;
|
|
324
|
-
const clipDuration = duration ? Math.min(duration - delay, clipInfo.duration) : clipInfo.duration;
|
|
325
|
-
if (delay < (duration ?? Infinity)) {
|
|
326
|
-
clipPlayer.pausedPosition = clipInfo.offset;
|
|
327
|
-
this.scheduleFades(clipPlayer, currentTime + delay, clipInfo.offset);
|
|
328
|
-
player.start(currentTime + delay, clipInfo.offset, clipDuration);
|
|
329
|
-
} else {
|
|
330
|
-
this.activePlayers--;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
});
|
|
335
|
-
}
|
|
336
|
-
pause() {
|
|
337
|
-
this.clips.forEach((clipPlayer) => {
|
|
338
|
-
if (clipPlayer.player.state === "started") {
|
|
339
|
-
const elapsed = ((0, import_tone.now)() - clipPlayer.playStartTime) * clipPlayer.player.playbackRate;
|
|
340
|
-
clipPlayer.pausedPosition = clipPlayer.pausedPosition + elapsed;
|
|
341
|
-
}
|
|
342
|
-
clipPlayer.player.stop();
|
|
343
|
-
});
|
|
344
|
-
this.activePlayers = 0;
|
|
345
|
-
}
|
|
346
|
-
stop(when) {
|
|
347
|
-
const stopWhen = when ?? (0, import_tone.now)();
|
|
348
|
-
this.clips.forEach((clipPlayer) => {
|
|
349
|
-
clipPlayer.player.stop(stopWhen);
|
|
350
|
-
clipPlayer.pausedPosition = 0;
|
|
351
|
-
});
|
|
352
|
-
this.activePlayers = 0;
|
|
353
|
-
}
|
|
354
389
|
dispose() {
|
|
390
|
+
const transport = (0, import_tone.getTransport)();
|
|
355
391
|
if (this.effectsCleanup) {
|
|
356
|
-
|
|
392
|
+
try {
|
|
393
|
+
this.effectsCleanup();
|
|
394
|
+
} catch (err) {
|
|
395
|
+
console.warn(`[waveform-playlist] Error during track "${this.id}" effects cleanup:`, err);
|
|
396
|
+
}
|
|
357
397
|
}
|
|
358
|
-
this.
|
|
359
|
-
|
|
360
|
-
|
|
398
|
+
this.stopAllSources();
|
|
399
|
+
this.scheduledClips.forEach((scheduled, index) => {
|
|
400
|
+
try {
|
|
401
|
+
transport.clear(scheduled.scheduleId);
|
|
402
|
+
} catch (err) {
|
|
403
|
+
console.warn(
|
|
404
|
+
`[waveform-playlist] Error clearing schedule ${index} on track "${this.id}":`,
|
|
405
|
+
err
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
try {
|
|
409
|
+
scheduled.fadeGainNode.disconnect();
|
|
410
|
+
} catch (err) {
|
|
411
|
+
console.warn(
|
|
412
|
+
`[waveform-playlist] Error disconnecting fadeGain ${index} on track "${this.id}":`,
|
|
413
|
+
err
|
|
414
|
+
);
|
|
415
|
+
}
|
|
361
416
|
});
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
417
|
+
try {
|
|
418
|
+
this.volumeNode.dispose();
|
|
419
|
+
} catch (err) {
|
|
420
|
+
console.warn(`[waveform-playlist] Error disposing volumeNode on track "${this.id}":`, err);
|
|
421
|
+
}
|
|
422
|
+
try {
|
|
423
|
+
this.panNode.dispose();
|
|
424
|
+
} catch (err) {
|
|
425
|
+
console.warn(`[waveform-playlist] Error disposing panNode on track "${this.id}":`, err);
|
|
426
|
+
}
|
|
427
|
+
try {
|
|
428
|
+
this.muteGain.dispose();
|
|
429
|
+
} catch (err) {
|
|
430
|
+
console.warn(`[waveform-playlist] Error disposing muteGain on track "${this.id}":`, err);
|
|
431
|
+
}
|
|
365
432
|
}
|
|
366
433
|
get id() {
|
|
367
434
|
return this.track.id;
|
|
368
435
|
}
|
|
369
436
|
get duration() {
|
|
370
|
-
if (this.
|
|
371
|
-
const lastClip = this.
|
|
437
|
+
if (this.scheduledClips.length === 0) return 0;
|
|
438
|
+
const lastClip = this.scheduledClips[this.scheduledClips.length - 1];
|
|
372
439
|
return lastClip.clipInfo.startTime + lastClip.clipInfo.duration;
|
|
373
440
|
}
|
|
374
441
|
get buffer() {
|
|
375
|
-
return this.
|
|
376
|
-
}
|
|
377
|
-
get isPlaying() {
|
|
378
|
-
return this.clips.some((clipPlayer) => clipPlayer.player.state === "started");
|
|
442
|
+
return this.scheduledClips[0]?.clipInfo.buffer;
|
|
379
443
|
}
|
|
380
444
|
get muted() {
|
|
381
445
|
return this.track.muted;
|
|
@@ -383,9 +447,6 @@ var ToneTrack = class {
|
|
|
383
447
|
get startTime() {
|
|
384
448
|
return this.track.startTime;
|
|
385
449
|
}
|
|
386
|
-
setOnStopCallback(callback) {
|
|
387
|
-
this.onStopCallback = callback;
|
|
388
|
-
}
|
|
389
450
|
};
|
|
390
451
|
|
|
391
452
|
// src/TonePlayout.ts
|
|
@@ -395,9 +456,11 @@ var TonePlayout = class {
|
|
|
395
456
|
this.isInitialized = false;
|
|
396
457
|
this.soloedTracks = /* @__PURE__ */ new Set();
|
|
397
458
|
this.manualMuteState = /* @__PURE__ */ new Map();
|
|
398
|
-
this.
|
|
399
|
-
|
|
400
|
-
this.
|
|
459
|
+
this._completionEventId = null;
|
|
460
|
+
this._loopHandler = null;
|
|
461
|
+
this._loopEnabled = false;
|
|
462
|
+
this._loopStart = 0;
|
|
463
|
+
this._loopEnd = 0;
|
|
401
464
|
this.masterVolume = new import_tone2.Volume(this.gainToDb(options.masterGain ?? 1));
|
|
402
465
|
if (options.effects) {
|
|
403
466
|
const cleanup = options.effects(this.masterVolume, (0, import_tone2.getDestination)(), false);
|
|
@@ -417,6 +480,16 @@ var TonePlayout = class {
|
|
|
417
480
|
gainToDb(gain) {
|
|
418
481
|
return 20 * Math.log10(gain);
|
|
419
482
|
}
|
|
483
|
+
clearCompletionEvent() {
|
|
484
|
+
if (this._completionEventId !== null) {
|
|
485
|
+
try {
|
|
486
|
+
(0, import_tone2.getTransport)().clear(this._completionEventId);
|
|
487
|
+
} catch (err) {
|
|
488
|
+
console.warn("[waveform-playlist] Error clearing Transport completion event:", err);
|
|
489
|
+
}
|
|
490
|
+
this._completionEventId = null;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
420
493
|
async init() {
|
|
421
494
|
if (this.isInitialized) return;
|
|
422
495
|
await (0, import_tone2.start)();
|
|
@@ -456,63 +529,75 @@ var TonePlayout = class {
|
|
|
456
529
|
}
|
|
457
530
|
play(when, offset, duration) {
|
|
458
531
|
if (!this.isInitialized) {
|
|
459
|
-
|
|
460
|
-
return;
|
|
532
|
+
throw new Error("[waveform-playlist] TonePlayout not initialized. Call init() first.");
|
|
461
533
|
}
|
|
462
534
|
const startTime = when ?? (0, import_tone2.now)();
|
|
463
|
-
const
|
|
464
|
-
this.
|
|
465
|
-
const
|
|
466
|
-
this.
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
this.onPlaybackCompleteCallback();
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
});
|
|
535
|
+
const transport = (0, import_tone2.getTransport)();
|
|
536
|
+
this.clearCompletionEvent();
|
|
537
|
+
const transportOffset = offset ?? 0;
|
|
538
|
+
this.tracks.forEach((track) => {
|
|
539
|
+
track.cancelFades();
|
|
540
|
+
track.prepareFades(startTime, transportOffset);
|
|
541
|
+
});
|
|
542
|
+
if (duration !== void 0) {
|
|
543
|
+
this._completionEventId = transport.scheduleOnce(() => {
|
|
544
|
+
this._completionEventId = null;
|
|
545
|
+
try {
|
|
546
|
+
this.onPlaybackCompleteCallback?.();
|
|
547
|
+
} catch (err) {
|
|
548
|
+
console.warn("[waveform-playlist] Error in playback completion callback:", err);
|
|
481
549
|
}
|
|
482
|
-
|
|
550
|
+
}, transportOffset + duration);
|
|
551
|
+
}
|
|
552
|
+
try {
|
|
553
|
+
if (transport.state !== "stopped") {
|
|
554
|
+
transport.stop();
|
|
555
|
+
}
|
|
556
|
+
this.tracks.forEach((track) => track.stopAllSources());
|
|
557
|
+
transport.loopStart = this._loopStart;
|
|
558
|
+
transport.loopEnd = this._loopEnd;
|
|
559
|
+
transport.loop = this._loopEnabled;
|
|
560
|
+
this.tracks.forEach((track) => track.setScheduleGuardOffset(transportOffset));
|
|
561
|
+
if (offset !== void 0) {
|
|
562
|
+
transport.start(startTime, offset);
|
|
483
563
|
} else {
|
|
484
|
-
|
|
485
|
-
if (duration !== void 0) {
|
|
486
|
-
this.activeTracks.set(toneTrack.id, currentSessionId);
|
|
487
|
-
toneTrack.setOnStopCallback(() => {
|
|
488
|
-
if (this.activeTracks.get(toneTrack.id) === currentSessionId) {
|
|
489
|
-
this.activeTracks.delete(toneTrack.id);
|
|
490
|
-
if (this.activeTracks.size === 0 && this.onPlaybackCompleteCallback) {
|
|
491
|
-
this.onPlaybackCompleteCallback();
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
});
|
|
495
|
-
}
|
|
496
|
-
toneTrack.play(startTime + delay, 0, duration);
|
|
564
|
+
transport.start(startTime);
|
|
497
565
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
566
|
+
transport._clock._lastUpdate = startTime;
|
|
567
|
+
this.tracks.forEach((track) => {
|
|
568
|
+
track.startMidClipSources(transportOffset, startTime);
|
|
569
|
+
});
|
|
570
|
+
} catch (err) {
|
|
571
|
+
this.clearCompletionEvent();
|
|
572
|
+
this.tracks.forEach((track) => track.cancelFades());
|
|
573
|
+
console.warn(
|
|
574
|
+
"[waveform-playlist] Transport.start() failed. Audio playback could not begin.",
|
|
575
|
+
err
|
|
576
|
+
);
|
|
577
|
+
throw err;
|
|
503
578
|
}
|
|
504
579
|
}
|
|
505
580
|
pause() {
|
|
506
|
-
(0, import_tone2.getTransport)()
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
})
|
|
581
|
+
const transport = (0, import_tone2.getTransport)();
|
|
582
|
+
try {
|
|
583
|
+
transport.pause();
|
|
584
|
+
} catch (err) {
|
|
585
|
+
console.warn("[waveform-playlist] Transport.pause() failed:", err);
|
|
586
|
+
}
|
|
587
|
+
this.tracks.forEach((track) => track.stopAllSources());
|
|
588
|
+
this.tracks.forEach((track) => track.cancelFades());
|
|
589
|
+
this.clearCompletionEvent();
|
|
510
590
|
}
|
|
511
591
|
stop() {
|
|
512
|
-
(0, import_tone2.getTransport)()
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
})
|
|
592
|
+
const transport = (0, import_tone2.getTransport)();
|
|
593
|
+
try {
|
|
594
|
+
transport.stop();
|
|
595
|
+
} catch (err) {
|
|
596
|
+
console.warn("[waveform-playlist] Transport.stop() failed:", err);
|
|
597
|
+
}
|
|
598
|
+
this.tracks.forEach((track) => track.stopAllSources());
|
|
599
|
+
this.tracks.forEach((track) => track.cancelFades());
|
|
600
|
+
this.clearCompletionEvent();
|
|
516
601
|
}
|
|
517
602
|
setMasterGain(gain) {
|
|
518
603
|
this.masterVolume.volume.value = this.gainToDb(gain);
|
|
@@ -552,6 +637,43 @@ var TonePlayout = class {
|
|
|
552
637
|
track.setMute(muted);
|
|
553
638
|
}
|
|
554
639
|
}
|
|
640
|
+
setLoop(enabled, loopStart, loopEnd) {
|
|
641
|
+
this._loopEnabled = enabled;
|
|
642
|
+
this._loopStart = loopStart;
|
|
643
|
+
this._loopEnd = loopEnd;
|
|
644
|
+
const transport = (0, import_tone2.getTransport)();
|
|
645
|
+
try {
|
|
646
|
+
transport.loopStart = loopStart;
|
|
647
|
+
transport.loopEnd = loopEnd;
|
|
648
|
+
transport.loop = enabled;
|
|
649
|
+
} catch (err) {
|
|
650
|
+
console.warn("[waveform-playlist] Error configuring Transport loop:", err);
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
if (enabled && !this._loopHandler) {
|
|
654
|
+
this._loopHandler = () => {
|
|
655
|
+
const currentTime = (0, import_tone2.now)();
|
|
656
|
+
this.tracks.forEach((track) => {
|
|
657
|
+
try {
|
|
658
|
+
track.stopAllSources();
|
|
659
|
+
track.cancelFades();
|
|
660
|
+
track.setScheduleGuardOffset(this._loopStart);
|
|
661
|
+
track.startMidClipSources(this._loopStart, currentTime);
|
|
662
|
+
track.prepareFades(currentTime, this._loopStart);
|
|
663
|
+
} catch (err) {
|
|
664
|
+
console.warn(
|
|
665
|
+
`[waveform-playlist] Error re-scheduling track "${track.id}" on loop:`,
|
|
666
|
+
err
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
});
|
|
670
|
+
};
|
|
671
|
+
transport.on("loop", this._loopHandler);
|
|
672
|
+
} else if (!enabled && this._loopHandler) {
|
|
673
|
+
transport.off("loop", this._loopHandler);
|
|
674
|
+
this._loopHandler = null;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
555
677
|
getCurrentTime() {
|
|
556
678
|
return (0, import_tone2.getTransport)().seconds;
|
|
557
679
|
}
|
|
@@ -559,14 +681,35 @@ var TonePlayout = class {
|
|
|
559
681
|
(0, import_tone2.getTransport)().seconds = time;
|
|
560
682
|
}
|
|
561
683
|
dispose() {
|
|
684
|
+
this.clearCompletionEvent();
|
|
685
|
+
if (this._loopHandler) {
|
|
686
|
+
try {
|
|
687
|
+
(0, import_tone2.getTransport)().off("loop", this._loopHandler);
|
|
688
|
+
} catch (err) {
|
|
689
|
+
console.warn("[waveform-playlist] Error removing Transport loop handler:", err);
|
|
690
|
+
}
|
|
691
|
+
this._loopHandler = null;
|
|
692
|
+
}
|
|
562
693
|
this.tracks.forEach((track) => {
|
|
563
|
-
|
|
694
|
+
try {
|
|
695
|
+
track.dispose();
|
|
696
|
+
} catch (err) {
|
|
697
|
+
console.warn(`[waveform-playlist] Error disposing track "${track.id}":`, err);
|
|
698
|
+
}
|
|
564
699
|
});
|
|
565
700
|
this.tracks.clear();
|
|
566
701
|
if (this.effectsCleanup) {
|
|
567
|
-
|
|
702
|
+
try {
|
|
703
|
+
this.effectsCleanup();
|
|
704
|
+
} catch (err) {
|
|
705
|
+
console.warn("[waveform-playlist] Error during master effects cleanup:", err);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
try {
|
|
709
|
+
this.masterVolume.dispose();
|
|
710
|
+
} catch (err) {
|
|
711
|
+
console.warn("[waveform-playlist] Error disposing master volume:", err);
|
|
568
712
|
}
|
|
569
|
-
this.masterVolume.dispose();
|
|
570
713
|
}
|
|
571
714
|
get context() {
|
|
572
715
|
return (0, import_tone2.getContext)();
|
|
@@ -651,15 +794,33 @@ function createToneAdapter(options) {
|
|
|
651
794
|
let playout = null;
|
|
652
795
|
let _isPlaying = false;
|
|
653
796
|
let _playoutGeneration = 0;
|
|
797
|
+
let _loopEnabled = false;
|
|
798
|
+
let _loopStart = 0;
|
|
799
|
+
let _loopEnd = 0;
|
|
800
|
+
let _audioInitialized = false;
|
|
654
801
|
function buildPlayout(tracks) {
|
|
655
802
|
if (playout) {
|
|
656
|
-
|
|
803
|
+
try {
|
|
804
|
+
playout.dispose();
|
|
805
|
+
} catch (err) {
|
|
806
|
+
console.warn("[waveform-playlist] Error disposing previous playout during rebuild:", err);
|
|
807
|
+
}
|
|
808
|
+
playout = null;
|
|
657
809
|
}
|
|
658
810
|
_playoutGeneration++;
|
|
659
811
|
const generation = _playoutGeneration;
|
|
660
812
|
playout = new TonePlayout({
|
|
661
813
|
effects: options?.effects
|
|
662
814
|
});
|
|
815
|
+
if (_audioInitialized) {
|
|
816
|
+
playout.init().catch((err) => {
|
|
817
|
+
console.warn(
|
|
818
|
+
"[waveform-playlist] Failed to re-initialize playout after rebuild. Audio playback will require another user gesture.",
|
|
819
|
+
err
|
|
820
|
+
);
|
|
821
|
+
_audioInitialized = false;
|
|
822
|
+
});
|
|
823
|
+
}
|
|
663
824
|
for (const track of tracks) {
|
|
664
825
|
const playableClips = track.clips.filter((c) => c.audioBuffer);
|
|
665
826
|
if (playableClips.length === 0) continue;
|
|
@@ -691,6 +852,7 @@ function createToneAdapter(options) {
|
|
|
691
852
|
});
|
|
692
853
|
}
|
|
693
854
|
playout.applyInitialSoloState();
|
|
855
|
+
playout.setLoop(_loopEnabled, _loopStart, _loopEnd);
|
|
694
856
|
playout.setOnPlaybackComplete(() => {
|
|
695
857
|
if (generation === _playoutGeneration) {
|
|
696
858
|
_isPlaying = false;
|
|
@@ -701,14 +863,19 @@ function createToneAdapter(options) {
|
|
|
701
863
|
async init() {
|
|
702
864
|
if (playout) {
|
|
703
865
|
await playout.init();
|
|
866
|
+
_audioInitialized = true;
|
|
704
867
|
}
|
|
705
868
|
},
|
|
706
869
|
setTracks(tracks) {
|
|
707
870
|
buildPlayout(tracks);
|
|
708
871
|
},
|
|
709
|
-
|
|
710
|
-
if (!playout)
|
|
711
|
-
|
|
872
|
+
play(startTime, endTime) {
|
|
873
|
+
if (!playout) {
|
|
874
|
+
console.warn(
|
|
875
|
+
"[waveform-playlist] adapter.play() called but no playout is available. Tracks may not have been set, or the adapter was disposed."
|
|
876
|
+
);
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
712
879
|
const duration = endTime !== void 0 ? endTime - startTime : void 0;
|
|
713
880
|
playout.play((0, import_tone5.now)(), startTime, duration);
|
|
714
881
|
_isPlaying = true;
|
|
@@ -745,8 +912,18 @@ function createToneAdapter(options) {
|
|
|
745
912
|
setTrackPan(trackId, pan) {
|
|
746
913
|
playout?.getTrack(trackId)?.setPan(pan);
|
|
747
914
|
},
|
|
915
|
+
setLoop(enabled, start2, end) {
|
|
916
|
+
_loopEnabled = enabled;
|
|
917
|
+
_loopStart = start2;
|
|
918
|
+
_loopEnd = end;
|
|
919
|
+
playout?.setLoop(enabled, start2, end);
|
|
920
|
+
},
|
|
748
921
|
dispose() {
|
|
749
|
-
|
|
922
|
+
try {
|
|
923
|
+
playout?.dispose();
|
|
924
|
+
} catch (err) {
|
|
925
|
+
console.warn("[waveform-playlist] Error disposing playout:", err);
|
|
926
|
+
}
|
|
750
927
|
playout = null;
|
|
751
928
|
_isPlaying = false;
|
|
752
929
|
}
|