gapless 4.0.12 → 4.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/README.md +58 -2
- package/dist/index.d.ts +38 -8
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/Queue.ts +44 -14
- package/src/Track.ts +38 -11
- package/src/machines/track.machine.ts +37 -9
- package/src/types.ts +24 -3
package/src/Queue.ts
CHANGED
|
@@ -14,10 +14,7 @@ import { createQueueMachine } from './machines/queue.machine';
|
|
|
14
14
|
import { Track } from './Track';
|
|
15
15
|
import type { TrackQueueRef } from './Track';
|
|
16
16
|
import { throttle } from './utils/throttle';
|
|
17
|
-
import type { GaplessOptions, AddTrackOptions, TrackInfo, TrackMetadata } from './types';
|
|
18
|
-
|
|
19
|
-
/** Maximum number of tracks to preload ahead of the current track. */
|
|
20
|
-
const PRELOAD_AHEAD = 2;
|
|
17
|
+
import type { GaplessOptions, AddTrackOptions, TrackInfo, TrackMetadata, PlaybackMethod } from './types';
|
|
21
18
|
|
|
22
19
|
export class Queue implements TrackQueueRef {
|
|
23
20
|
private _tracks: Track[] = [];
|
|
@@ -32,16 +29,18 @@ export class Queue implements TrackQueueRef {
|
|
|
32
29
|
private readonly _onPlayBlocked?: () => void;
|
|
33
30
|
private readonly _onDebug?: (msg: string) => void;
|
|
34
31
|
|
|
35
|
-
readonly
|
|
32
|
+
readonly playbackMethod: PlaybackMethod;
|
|
36
33
|
|
|
37
34
|
private _volume: number;
|
|
35
|
+
private _preloadNumTracks: number;
|
|
36
|
+
private _playbackRate: number;
|
|
38
37
|
|
|
39
38
|
/** Index of the next track with a pre-scheduled gapless start, or null. */
|
|
40
39
|
private _scheduledNextIndex: number | null = null;
|
|
41
40
|
|
|
42
41
|
private _throttledUpdatePositionState = throttle(
|
|
43
|
-
(duration: number, currentTime: number) =>
|
|
44
|
-
updateMediaSessionPositionState(duration, currentTime),
|
|
42
|
+
(duration: number, currentTime: number, playbackRate: number) =>
|
|
43
|
+
updateMediaSessionPositionState(duration, currentTime, playbackRate),
|
|
45
44
|
1000,
|
|
46
45
|
);
|
|
47
46
|
|
|
@@ -56,13 +55,17 @@ export class Queue implements TrackQueueRef {
|
|
|
56
55
|
onError,
|
|
57
56
|
onPlayBlocked,
|
|
58
57
|
onDebug,
|
|
59
|
-
|
|
58
|
+
playbackMethod = 'HYBRID',
|
|
60
59
|
trackMetadata = [],
|
|
61
60
|
volume: initialVolume = 1,
|
|
61
|
+
preloadNumTracks = 2,
|
|
62
|
+
playbackRate: initialPlaybackRate = 1,
|
|
62
63
|
} = options;
|
|
63
64
|
|
|
64
65
|
this._volume = Math.min(1, Math.max(0, initialVolume));
|
|
65
|
-
this.
|
|
66
|
+
this._preloadNumTracks = Math.max(0, preloadNumTracks);
|
|
67
|
+
this._playbackRate = Math.min(4, Math.max(0.25, initialPlaybackRate));
|
|
68
|
+
this.playbackMethod = playbackMethod;
|
|
66
69
|
this._onProgress = onProgress;
|
|
67
70
|
this._onEnded = onEnded;
|
|
68
71
|
this._onPlayNextTrack = onPlayNextTrack;
|
|
@@ -246,6 +249,17 @@ export class Queue implements TrackQueueRef {
|
|
|
246
249
|
for (const track of this._tracks) track.setVolume(clamped);
|
|
247
250
|
}
|
|
248
251
|
|
|
252
|
+
setPlaybackRate(rate: number): void {
|
|
253
|
+
const clamped = Math.min(4, Math.max(0.25, rate));
|
|
254
|
+
this._playbackRate = clamped;
|
|
255
|
+
this._currentTrack?.setPlaybackRate(clamped);
|
|
256
|
+
this._cancelScheduledGapless();
|
|
257
|
+
const snap = this._actor.getSnapshot();
|
|
258
|
+
if (snap.value === 'playing') {
|
|
259
|
+
this._tryScheduleGapless(snap.context.currentTrackIndex);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
249
263
|
addTrack(url: string, options: AddTrackOptions = {}): void {
|
|
250
264
|
const index = this._tracks.length;
|
|
251
265
|
const metadata = options.metadata ?? ({} as TrackMetadata);
|
|
@@ -314,6 +328,22 @@ export class Queue implements TrackQueueRef {
|
|
|
314
328
|
return this._volume;
|
|
315
329
|
}
|
|
316
330
|
|
|
331
|
+
get preloadNumTracks(): number {
|
|
332
|
+
return this._preloadNumTracks;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
set preloadNumTracks(value: number) {
|
|
336
|
+
this._preloadNumTracks = Math.max(0, value);
|
|
337
|
+
const snap = this._actor.getSnapshot();
|
|
338
|
+
if (snap.value === 'playing') {
|
|
339
|
+
this._preloadAhead(snap.context.currentTrackIndex);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
get playbackRate(): number {
|
|
344
|
+
return this._playbackRate;
|
|
345
|
+
}
|
|
346
|
+
|
|
317
347
|
/** Snapshot of the queue state machine (state name + context). For debugging. */
|
|
318
348
|
get queueSnapshot(): { state: string; context: { currentTrackIndex: number; trackCount: number } } {
|
|
319
349
|
const snap = this._actor.getSnapshot();
|
|
@@ -347,7 +377,7 @@ export class Queue implements TrackQueueRef {
|
|
|
347
377
|
onProgress(info: TrackInfo): void {
|
|
348
378
|
if (info.index !== this._actor.getSnapshot().context.currentTrackIndex) return;
|
|
349
379
|
if (!isNaN(info.duration)) {
|
|
350
|
-
this._throttledUpdatePositionState(info.duration, info.currentTime);
|
|
380
|
+
this._throttledUpdatePositionState(info.duration, info.currentTime, this._playbackRate);
|
|
351
381
|
}
|
|
352
382
|
this._onProgress?.(info);
|
|
353
383
|
}
|
|
@@ -393,7 +423,7 @@ export class Queue implements TrackQueueRef {
|
|
|
393
423
|
return;
|
|
394
424
|
}
|
|
395
425
|
}
|
|
396
|
-
const limit = fromIndex +
|
|
426
|
+
const limit = fromIndex + this._preloadNumTracks + 1;
|
|
397
427
|
this.onDebug(`_preloadAhead(${fromIndex}) limit=${limit} trackCount=${this._tracks.length}`);
|
|
398
428
|
for (let i = fromIndex + 1; i < this._tracks.length && i < limit; i++) {
|
|
399
429
|
const t = this._tracks[i];
|
|
@@ -419,7 +449,7 @@ export class Queue implements TrackQueueRef {
|
|
|
419
449
|
|
|
420
450
|
private _tryScheduleGapless(curIndex: number): void {
|
|
421
451
|
const ctx = getAudioContext();
|
|
422
|
-
if (!ctx || this.
|
|
452
|
+
if (!ctx || this.playbackMethod === 'HTML5_ONLY') return;
|
|
423
453
|
|
|
424
454
|
const nextIndex = curIndex + 1;
|
|
425
455
|
if (nextIndex >= this._tracks.length) return;
|
|
@@ -451,10 +481,10 @@ export class Queue implements TrackQueueRef {
|
|
|
451
481
|
if (isNaN(duration)) return null;
|
|
452
482
|
|
|
453
483
|
if (track.scheduledStartContextTime !== null) {
|
|
454
|
-
return track.scheduledStartContextTime + duration;
|
|
484
|
+
return track.scheduledStartContextTime + duration / this._playbackRate;
|
|
455
485
|
}
|
|
456
486
|
|
|
457
|
-
const remaining = duration - track.currentTime;
|
|
487
|
+
const remaining = (duration - track.currentTime) / this._playbackRate;
|
|
458
488
|
if (remaining <= 0) return null;
|
|
459
489
|
return ctx.currentTime + remaining;
|
|
460
490
|
}
|
package/src/Track.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { getAudioContext, resumeAudioContext } from './utils/audioContext';
|
|
|
7
7
|
import { createTrackMachine } from './machines/track.machine';
|
|
8
8
|
import { fetchDecodeMachine } from './machines/fetchDecode.machine';
|
|
9
9
|
import type { TrackContext } from './machines/track.machine';
|
|
10
|
-
import type { TrackInfo, TrackMetadata, WebAudioLoadingState, PlaybackType } from './types';
|
|
10
|
+
import type { TrackInfo, TrackMetadata, WebAudioLoadingState, PlaybackType, PlaybackMethod } from './types';
|
|
11
11
|
|
|
12
12
|
export interface TrackQueueRef {
|
|
13
13
|
onTrackEnded(track: Track): void;
|
|
@@ -18,7 +18,8 @@ export interface TrackQueueRef {
|
|
|
18
18
|
onPlayBlocked(): void;
|
|
19
19
|
onDebug(msg: string): void;
|
|
20
20
|
readonly volume: number;
|
|
21
|
-
readonly
|
|
21
|
+
readonly playbackMethod: PlaybackMethod;
|
|
22
|
+
readonly playbackRate: number;
|
|
22
23
|
readonly currentTrackIndex: number;
|
|
23
24
|
}
|
|
24
25
|
|
|
@@ -42,10 +43,10 @@ export class Track {
|
|
|
42
43
|
readonly audio: HTMLAudioElement;
|
|
43
44
|
|
|
44
45
|
// ---- Web Audio nodes -----------------------------------------------------
|
|
45
|
-
private readonly
|
|
46
|
+
private readonly _playbackMethod: PlaybackMethod;
|
|
46
47
|
|
|
47
48
|
private get ctx(): AudioContext | null {
|
|
48
|
-
if (this.
|
|
49
|
+
if (this._playbackMethod === 'HTML5_ONLY') return null;
|
|
49
50
|
const context = getAudioContext();
|
|
50
51
|
if (context && !this.gainNode) {
|
|
51
52
|
this.gainNode = context.createGain();
|
|
@@ -57,8 +58,10 @@ export class Track {
|
|
|
57
58
|
private gainNode: GainNode | null = null;
|
|
58
59
|
private sourceNode: AudioBufferSourceNode | null = null;
|
|
59
60
|
audioBuffer: AudioBuffer | null = null;
|
|
60
|
-
/** AudioContext.currentTime
|
|
61
|
-
private
|
|
61
|
+
/** AudioContext.currentTime at the start of the current playback segment. */
|
|
62
|
+
private _waRefCtxTime = 0;
|
|
63
|
+
/** Track position (seconds) at the start of the current playback segment. */
|
|
64
|
+
private _waRefTrackTime = 0;
|
|
62
65
|
/** Track-time (seconds) frozen at the moment of the most recent pause. */
|
|
63
66
|
private pausedAtTrackTime = 0;
|
|
64
67
|
// ---- FSM -----------------------------------------------------------------
|
|
@@ -104,7 +107,7 @@ export class Track {
|
|
|
104
107
|
this._actor.send({ type: 'HTML5_ENDED' });
|
|
105
108
|
};
|
|
106
109
|
|
|
107
|
-
this.
|
|
110
|
+
this._playbackMethod = opts.queue.playbackMethod;
|
|
108
111
|
|
|
109
112
|
const initialContext: TrackContext = {
|
|
110
113
|
trackUrl: this._trackUrl,
|
|
@@ -116,10 +119,12 @@ export class Track {
|
|
|
116
119
|
scheduledStartContextTime: null,
|
|
117
120
|
notifiedLookahead: false,
|
|
118
121
|
fetchStarted: false,
|
|
122
|
+
pendingPlay: false,
|
|
119
123
|
};
|
|
120
124
|
const machine = createTrackMachine(initialContext).provide({
|
|
121
125
|
guards: {
|
|
122
126
|
canPlayWebAudio: () => !!(this.ctx && this.audioBuffer && this.gainNode),
|
|
127
|
+
isWebAudioOnly: () => this._playbackMethod === 'WEBAUDIO_ONLY',
|
|
123
128
|
},
|
|
124
129
|
actors: {
|
|
125
130
|
fetchDecode: fetchDecodeMachine.provide({
|
|
@@ -150,6 +155,10 @@ export class Track {
|
|
|
150
155
|
}),
|
|
151
156
|
},
|
|
152
157
|
actions: {
|
|
158
|
+
triggerFetchForPendingPlay: () => {
|
|
159
|
+
this.preload();
|
|
160
|
+
resumeAudioContext();
|
|
161
|
+
},
|
|
153
162
|
playHtml5: () => this._playHtml5(),
|
|
154
163
|
startSourceNode: () => {
|
|
155
164
|
this._startSourceNode(this.pausedAtTrackTime);
|
|
@@ -160,11 +169,13 @@ export class Track {
|
|
|
160
169
|
this._stopSourceNode();
|
|
161
170
|
this.sourceNode = this.ctx.createBufferSource();
|
|
162
171
|
this.sourceNode.buffer = this.audioBuffer;
|
|
172
|
+
this.sourceNode.playbackRate.value = this.queueRef.playbackRate;
|
|
163
173
|
this.sourceNode.connect(this.gainNode);
|
|
164
174
|
this.gainNode.connect(this.ctx.destination);
|
|
165
175
|
this.sourceNode.onended = this._handleWebAudioEnded;
|
|
166
176
|
this.sourceNode.start(when, 0);
|
|
167
|
-
this.
|
|
177
|
+
this._waRefCtxTime = when;
|
|
178
|
+
this._waRefTrackTime = 0;
|
|
168
179
|
this.queueRef.onDebug(
|
|
169
180
|
`startScheduledSourceNode track=${this.index} when=${when.toFixed(3)} ctxNow=${this.ctx.currentTime.toFixed(3)} delta=${(when - this.ctx.currentTime).toFixed(3)}s`
|
|
170
181
|
);
|
|
@@ -187,7 +198,8 @@ export class Track {
|
|
|
187
198
|
this.audio.currentTime = 0;
|
|
188
199
|
},
|
|
189
200
|
resetTiming: () => {
|
|
190
|
-
this.
|
|
201
|
+
this._waRefCtxTime = 0;
|
|
202
|
+
this._waRefTrackTime = 0;
|
|
191
203
|
this.pausedAtTrackTime = 0;
|
|
192
204
|
},
|
|
193
205
|
notifyTrackEnded: () => {
|
|
@@ -228,6 +240,17 @@ export class Track {
|
|
|
228
240
|
this._actor.send({ type: 'SET_VOLUME', volume: vol });
|
|
229
241
|
}
|
|
230
242
|
|
|
243
|
+
setPlaybackRate(rate: number): void {
|
|
244
|
+
// Freeze current track position at the old rate before switching
|
|
245
|
+
if (this.ctx && this.sourceNode && this._actor.getSnapshot().context.isPlaying) {
|
|
246
|
+
const oldRate = this.sourceNode.playbackRate.value;
|
|
247
|
+
this._waRefTrackTime = this._waRefTrackTime + (this.ctx.currentTime - this._waRefCtxTime) * oldRate;
|
|
248
|
+
this._waRefCtxTime = this.ctx.currentTime;
|
|
249
|
+
}
|
|
250
|
+
this.audio.playbackRate = rate;
|
|
251
|
+
if (this.sourceNode) this.sourceNode.playbackRate.value = rate;
|
|
252
|
+
}
|
|
253
|
+
|
|
231
254
|
preload(): void {
|
|
232
255
|
this.queueRef.onDebug(
|
|
233
256
|
`preload() track=${this.index} state=${this._actor.getSnapshot().value} hasBuffer=${!!this.audioBuffer} hasCtx=${!!this.ctx}`
|
|
@@ -296,7 +319,7 @@ export class Track {
|
|
|
296
319
|
if (snap.value === 'webaudio') {
|
|
297
320
|
if (!snap.context.isPlaying) return this.pausedAtTrackTime;
|
|
298
321
|
if (!this.ctx) return 0;
|
|
299
|
-
return Math.max(0, this.ctx.currentTime - this.
|
|
322
|
+
return Math.max(0, this._waRefTrackTime + (this.ctx.currentTime - this._waRefCtxTime) * this.queueRef.playbackRate);
|
|
300
323
|
}
|
|
301
324
|
return this.audio.currentTime;
|
|
302
325
|
}
|
|
@@ -356,6 +379,7 @@ export class Track {
|
|
|
356
379
|
playbackType: this.playbackType,
|
|
357
380
|
webAudioLoadingState: this.webAudioLoadingState,
|
|
358
381
|
metadata: this.metadata,
|
|
382
|
+
playbackRate: this.queueRef.playbackRate,
|
|
359
383
|
machineState: this.machineState,
|
|
360
384
|
};
|
|
361
385
|
}
|
|
@@ -366,6 +390,7 @@ export class Track {
|
|
|
366
390
|
|
|
367
391
|
private _playHtml5(): void {
|
|
368
392
|
if (this.audio.preload !== 'auto') this.audio.preload = 'auto';
|
|
393
|
+
this.audio.playbackRate = this.queueRef.playbackRate;
|
|
369
394
|
const promise = this.audio.play();
|
|
370
395
|
if (promise) {
|
|
371
396
|
promise.catch((err: unknown) => {
|
|
@@ -414,11 +439,13 @@ export class Track {
|
|
|
414
439
|
|
|
415
440
|
this.sourceNode = this.ctx.createBufferSource();
|
|
416
441
|
this.sourceNode.buffer = this.audioBuffer;
|
|
442
|
+
this.sourceNode.playbackRate.value = this.queueRef.playbackRate;
|
|
417
443
|
this.sourceNode.connect(this.gainNode);
|
|
418
444
|
this.gainNode.connect(this.ctx.destination);
|
|
419
445
|
this.sourceNode.onended = this._handleWebAudioEnded;
|
|
420
446
|
|
|
421
|
-
this.
|
|
447
|
+
this._waRefCtxTime = this.ctx.currentTime;
|
|
448
|
+
this._waRefTrackTime = offset;
|
|
422
449
|
this.sourceNode.start(0, offset);
|
|
423
450
|
}
|
|
424
451
|
|
|
@@ -39,6 +39,8 @@ export interface TrackContext {
|
|
|
39
39
|
scheduledStartContextTime: number | null;
|
|
40
40
|
notifiedLookahead: boolean;
|
|
41
41
|
fetchStarted: boolean;
|
|
42
|
+
/** True when PLAY was received in webAudioOnly mode before buffer is ready. */
|
|
43
|
+
pendingPlay: boolean;
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
// ---- Events ----------------------------------------------------------------
|
|
@@ -76,6 +78,7 @@ export function createTrackMachine(initialContext: TrackContext) {
|
|
|
76
78
|
},
|
|
77
79
|
guards: {
|
|
78
80
|
canPlayWebAudio: () => false,
|
|
81
|
+
isWebAudioOnly: () => false,
|
|
79
82
|
canStartFetch: ({ context }) => context.webAudioLoadingState === 'NONE' && !context.fetchStarted,
|
|
80
83
|
},
|
|
81
84
|
actions: {
|
|
@@ -94,6 +97,9 @@ export function createTrackMachine(initialContext: TrackContext) {
|
|
|
94
97
|
resetHtml5Element: () => {},
|
|
95
98
|
resetTiming: () => {},
|
|
96
99
|
notifyTrackEnded: () => {},
|
|
100
|
+
triggerFetchForPendingPlay: () => {},
|
|
101
|
+
setPendingPlay: assign({ pendingPlay: () => true }),
|
|
102
|
+
clearPendingPlay: assign({ pendingPlay: () => false }),
|
|
97
103
|
setIsPlaying: assign({ isPlaying: () => true }),
|
|
98
104
|
clearIsPlaying: assign({ isPlaying: () => false }),
|
|
99
105
|
setLoadingState: assign({ webAudioLoadingState: () => 'LOADING' as WebAudioLoadingState }),
|
|
@@ -186,6 +192,10 @@ export function createTrackMachine(initialContext: TrackContext) {
|
|
|
186
192
|
'startProgressLoop',
|
|
187
193
|
],
|
|
188
194
|
},
|
|
195
|
+
{
|
|
196
|
+
guard: 'isWebAudioOnly',
|
|
197
|
+
actions: ['setPendingPlay', 'triggerFetchForPendingPlay'],
|
|
198
|
+
},
|
|
189
199
|
{
|
|
190
200
|
target: 'html5',
|
|
191
201
|
actions: ['setIsPlaying', 'playHtml5', 'startProgressLoop'],
|
|
@@ -210,11 +220,18 @@ export function createTrackMachine(initialContext: TrackContext) {
|
|
|
210
220
|
BUFFER_LOADING: {
|
|
211
221
|
actions: 'setLoadingState',
|
|
212
222
|
},
|
|
213
|
-
BUFFER_READY:
|
|
214
|
-
|
|
215
|
-
|
|
223
|
+
BUFFER_READY: [
|
|
224
|
+
{
|
|
225
|
+
guard: ({ context }: { context: TrackContext }) => context.pendingPlay,
|
|
226
|
+
target: 'webaudio',
|
|
227
|
+
actions: ['clearPendingPlay', 'setPlayingWebAudio', 'startSourceNode', 'startProgressLoop'],
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
actions: 'setLoadedState',
|
|
231
|
+
},
|
|
232
|
+
],
|
|
216
233
|
BUFFER_ERROR: {
|
|
217
|
-
actions: 'setErrorState',
|
|
234
|
+
actions: ['setErrorState', 'clearPendingPlay'],
|
|
218
235
|
},
|
|
219
236
|
URL_RESOLVED: {
|
|
220
237
|
actions: 'setResolvedUrl',
|
|
@@ -289,13 +306,20 @@ export function createTrackMachine(initialContext: TrackContext) {
|
|
|
289
306
|
BUFFER_LOADING: {
|
|
290
307
|
actions: 'setLoadingState',
|
|
291
308
|
},
|
|
292
|
-
BUFFER_READY:
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
309
|
+
BUFFER_READY: [
|
|
310
|
+
{
|
|
311
|
+
guard: ({ context }: { context: TrackContext }) => context.pendingPlay,
|
|
312
|
+
target: 'webaudio',
|
|
313
|
+
actions: ['clearPendingPlay', 'setPlayingWebAudio', 'startSourceNode', 'startProgressLoop'],
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
target: 'idle',
|
|
317
|
+
actions: 'setLoadedState',
|
|
318
|
+
},
|
|
319
|
+
],
|
|
296
320
|
BUFFER_ERROR: {
|
|
297
321
|
target: 'idle',
|
|
298
|
-
actions: 'setErrorState',
|
|
322
|
+
actions: ['setErrorState', 'clearPendingPlay'],
|
|
299
323
|
},
|
|
300
324
|
PLAY: [
|
|
301
325
|
{
|
|
@@ -307,6 +331,10 @@ export function createTrackMachine(initialContext: TrackContext) {
|
|
|
307
331
|
'startProgressLoop',
|
|
308
332
|
],
|
|
309
333
|
},
|
|
334
|
+
{
|
|
335
|
+
guard: 'isWebAudioOnly',
|
|
336
|
+
actions: ['setPendingPlay', 'triggerFetchForPendingPlay'],
|
|
337
|
+
},
|
|
310
338
|
{
|
|
311
339
|
target: 'html5',
|
|
312
340
|
actions: ['setIsPlaying', 'playHtml5', 'startProgressLoop'],
|
package/src/types.ts
CHANGED
|
@@ -4,6 +4,14 @@
|
|
|
4
4
|
|
|
5
5
|
export type PlaybackType = 'HTML5' | 'WEBAUDIO';
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Controls how audio is rendered.
|
|
9
|
+
* - `'HYBRID'` (default): Starts with HTML5 audio, then switches to Web Audio after decode. Best for remote files.
|
|
10
|
+
* - `'HTML5_ONLY'`: Uses HTML5 audio exclusively. Gapless playback is not available.
|
|
11
|
+
* - `'WEBAUDIO_ONLY'`: Uses Web Audio API exclusively. Audio must fully buffer before playing — only use for very small or local files.
|
|
12
|
+
*/
|
|
13
|
+
export type PlaybackMethod = 'HYBRID' | 'HTML5_ONLY' | 'WEBAUDIO_ONLY';
|
|
14
|
+
|
|
7
15
|
export type WebAudioLoadingState = 'NONE' | 'LOADING' | 'LOADED' | 'ERROR';
|
|
8
16
|
|
|
9
17
|
/** Metadata attached to a track (arbitrary user data). */
|
|
@@ -36,14 +44,25 @@ export interface GaplessOptions {
|
|
|
36
44
|
/** Called when autoplay is blocked by the browser. */
|
|
37
45
|
onPlayBlocked?: () => void;
|
|
38
46
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
47
|
+
* Controls how audio is rendered.
|
|
48
|
+
* - `'HYBRID'` (default): Starts with HTML5 audio, switches to Web Audio after decode. Best for remote files.
|
|
49
|
+
* - `'HTML5_ONLY'`: HTML5 audio only. Gapless playback is not available.
|
|
50
|
+
* - `'WEBAUDIO_ONLY'`: Web Audio API only. Audio must fully buffer before playing.
|
|
41
51
|
*/
|
|
42
|
-
|
|
52
|
+
playbackMethod?: PlaybackMethod;
|
|
43
53
|
/** Per-track metadata (aligned to the tracks array by index). */
|
|
44
54
|
trackMetadata?: TrackMetadata[];
|
|
45
55
|
/** Initial volume, 0.0–1.0. Defaults to 1. */
|
|
46
56
|
volume?: number;
|
|
57
|
+
/**
|
|
58
|
+
* Number of tracks to preload ahead of the current track.
|
|
59
|
+
* Defaults to 2. Set to 0 to disable preloading.
|
|
60
|
+
*/
|
|
61
|
+
preloadNumTracks?: number;
|
|
62
|
+
/**
|
|
63
|
+
* Initial playback rate, 0.25–4.0. Defaults to 1.
|
|
64
|
+
*/
|
|
65
|
+
playbackRate?: number;
|
|
47
66
|
}
|
|
48
67
|
|
|
49
68
|
/** Options for dynamically adding a track. */
|
|
@@ -82,6 +101,8 @@ export interface TrackInfo {
|
|
|
82
101
|
webAudioLoadingState: WebAudioLoadingState;
|
|
83
102
|
/** Arbitrary metadata supplied when the track was added. */
|
|
84
103
|
metadata?: TrackMetadata;
|
|
104
|
+
/** Current playback rate. */
|
|
105
|
+
playbackRate: number;
|
|
85
106
|
/** Current xstate machine state for this track (e.g. 'idle', 'html5', 'webaudio'). */
|
|
86
107
|
machineState: string;
|
|
87
108
|
}
|