wavesurfer.js 7.0.0-alpha.9 → 7.0.0-beta.1
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 +90 -18
- package/dist/base-plugin.d.ts +6 -4
- package/dist/base-plugin.js +9 -3
- package/dist/decoder.d.ts +8 -7
- package/dist/decoder.js +43 -38
- package/dist/event-emitter.d.ts +16 -10
- package/dist/event-emitter.js +38 -16
- package/dist/fetcher.d.ts +4 -3
- package/dist/fetcher.js +5 -15
- package/dist/player.d.ts +31 -11
- package/dist/player.js +58 -31
- package/dist/plugins/envelope.d.ts +71 -0
- package/dist/plugins/envelope.js +347 -0
- package/dist/plugins/envelope.min.js +1 -0
- package/dist/plugins/minimap.d.ts +39 -0
- package/dist/plugins/minimap.js +117 -0
- package/dist/plugins/minimap.min.js +1 -0
- package/dist/plugins/multitrack.d.ts +85 -12
- package/dist/plugins/multitrack.js +331 -84
- package/dist/plugins/multitrack.min.js +1 -0
- package/dist/plugins/record.d.ts +26 -0
- package/dist/plugins/record.js +126 -0
- package/dist/plugins/record.min.js +1 -0
- package/dist/plugins/regions.d.ts +83 -43
- package/dist/plugins/regions.js +362 -192
- package/dist/plugins/regions.min.js +1 -0
- package/dist/plugins/spectrogram.d.ts +69 -0
- package/dist/plugins/spectrogram.js +340 -0
- package/dist/plugins/spectrogram.min.js +1 -0
- package/dist/plugins/timeline.d.ts +25 -9
- package/dist/plugins/timeline.js +65 -24
- package/dist/plugins/timeline.min.js +1 -0
- package/dist/renderer.d.ts +29 -13
- package/dist/renderer.js +280 -161
- package/dist/timer.d.ts +1 -1
- package/dist/wavesurfer.d.ts +151 -0
- package/dist/wavesurfer.js +241 -0
- package/dist/wavesurfer.min.js +1 -1
- package/package.json +54 -27
- package/dist/index.d.ts +0 -117
- package/dist/index.js +0 -227
- package/dist/player-webaudio.d.ts +0 -8
- package/dist/player-webaudio.js +0 -32
- package/dist/plugins/xmultitrack.d.ts +0 -44
- package/dist/plugins/xmultitrack.js +0 -260
- package/dist/react/useWavesurfer.d.ts +0 -5
- package/dist/react/useWavesurfer.js +0 -20
- package/dist/wavesurfer.Multitrack.min.js +0 -1
- package/dist/wavesurfer.Regions.min.js +0 -1
- package/dist/wavesurfer.Timeline.min.js +0 -1
|
@@ -1,24 +1,80 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Multitrack isn't a plugin, but rather a helper class for creating a multitrack audio player.
|
|
3
|
+
* Individual tracks are synced and played together. They can be dragged to set their start position.
|
|
4
|
+
*/
|
|
5
|
+
import { type WaveSurferOptions } from '../wavesurfer.js';
|
|
6
|
+
import { type EnvelopePluginOptions } from './envelope.js';
|
|
7
|
+
import EventEmitter from '../event-emitter.js';
|
|
8
|
+
export type TrackId = string | number;
|
|
9
|
+
export type TrackOptions = {
|
|
10
|
+
id: TrackId;
|
|
4
11
|
url?: string;
|
|
5
12
|
peaks?: WaveSurferOptions['peaks'];
|
|
6
13
|
draggable?: boolean;
|
|
7
14
|
startPosition: number;
|
|
8
15
|
startCue?: number;
|
|
9
16
|
endCue?: number;
|
|
17
|
+
fadeInEnd?: number;
|
|
18
|
+
fadeOutStart?: number;
|
|
19
|
+
volume?: number;
|
|
10
20
|
markers?: Array<{
|
|
11
|
-
id: string | number;
|
|
12
21
|
time: number;
|
|
13
22
|
label?: string;
|
|
14
23
|
color?: string;
|
|
15
24
|
}>;
|
|
16
|
-
|
|
17
|
-
|
|
25
|
+
intro?: {
|
|
26
|
+
endTime: number;
|
|
27
|
+
label?: string;
|
|
28
|
+
color?: string;
|
|
29
|
+
};
|
|
30
|
+
options?: WaveSurferOptions;
|
|
31
|
+
};
|
|
32
|
+
export type MultitrackOptions = {
|
|
18
33
|
container: HTMLElement;
|
|
19
|
-
minPxPerSec
|
|
20
|
-
|
|
21
|
-
|
|
34
|
+
minPxPerSec?: number;
|
|
35
|
+
cursorColor?: string;
|
|
36
|
+
cursorWidth?: number;
|
|
37
|
+
trackBackground?: string;
|
|
38
|
+
trackBorderColor?: string;
|
|
39
|
+
rightButtonDrag?: boolean;
|
|
40
|
+
envelopeOptions?: EnvelopePluginOptions;
|
|
41
|
+
};
|
|
42
|
+
export type MultitrackEvents = {
|
|
43
|
+
canplay: [];
|
|
44
|
+
'start-position-change': [{
|
|
45
|
+
id: TrackId;
|
|
46
|
+
startPosition: number;
|
|
47
|
+
}];
|
|
48
|
+
'start-cue-change': [{
|
|
49
|
+
id: TrackId;
|
|
50
|
+
startCue: number;
|
|
51
|
+
}];
|
|
52
|
+
'end-cue-change': [{
|
|
53
|
+
id: TrackId;
|
|
54
|
+
endCue: number;
|
|
55
|
+
}];
|
|
56
|
+
'fade-in-change': [{
|
|
57
|
+
id: TrackId;
|
|
58
|
+
fadeInEnd: number;
|
|
59
|
+
}];
|
|
60
|
+
'fade-out-change': [{
|
|
61
|
+
id: TrackId;
|
|
62
|
+
fadeOutStart: number;
|
|
63
|
+
}];
|
|
64
|
+
'volume-change': [{
|
|
65
|
+
id: TrackId;
|
|
66
|
+
volume: number;
|
|
67
|
+
}];
|
|
68
|
+
'intro-end-change': [{
|
|
69
|
+
id: TrackId;
|
|
70
|
+
endTime: number;
|
|
71
|
+
}];
|
|
72
|
+
drop: [{
|
|
73
|
+
id: TrackId;
|
|
74
|
+
}];
|
|
75
|
+
};
|
|
76
|
+
export type MultitrackTracks = Array<TrackOptions>;
|
|
77
|
+
declare class MultiTrack extends EventEmitter<MultitrackEvents> {
|
|
22
78
|
private tracks;
|
|
23
79
|
private options;
|
|
24
80
|
private audios;
|
|
@@ -28,17 +84,34 @@ declare class MultiTrack {
|
|
|
28
84
|
private maxDuration;
|
|
29
85
|
private rendering;
|
|
30
86
|
private isDragging;
|
|
87
|
+
private frameRequest;
|
|
88
|
+
private timer;
|
|
89
|
+
private subscriptions;
|
|
90
|
+
private timeline;
|
|
31
91
|
static create(tracks: MultitrackTracks, options: MultitrackOptions): MultiTrack;
|
|
32
92
|
constructor(tracks: MultitrackTracks, options: MultitrackOptions);
|
|
33
|
-
private
|
|
34
|
-
private
|
|
93
|
+
private initDurations;
|
|
94
|
+
private initAudio;
|
|
95
|
+
private initAllAudios;
|
|
96
|
+
private initWavesurfer;
|
|
97
|
+
private initAllWavesurfers;
|
|
98
|
+
private initTimeline;
|
|
35
99
|
private updatePosition;
|
|
36
|
-
private
|
|
100
|
+
private setIsDragging;
|
|
37
101
|
private onDrag;
|
|
38
102
|
private findCurrentTracks;
|
|
103
|
+
private startSync;
|
|
39
104
|
play(): void;
|
|
40
105
|
pause(): void;
|
|
41
106
|
isPlaying(): boolean;
|
|
107
|
+
getCurrentTime(): number;
|
|
108
|
+
/** Position percentage from 0 to 1 */
|
|
109
|
+
seekTo(position: number): void;
|
|
110
|
+
/** Set time in seconds */
|
|
111
|
+
setTime(time: number): void;
|
|
112
|
+
zoom(pxPerSec: number): void;
|
|
113
|
+
addTrack(track: TrackOptions): void;
|
|
42
114
|
destroy(): void;
|
|
115
|
+
setSinkId(sinkId: string): Promise<void[]>;
|
|
43
116
|
}
|
|
44
117
|
export default MultiTrack;
|
|
@@ -1,86 +1,201 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Multitrack isn't a plugin, but rather a helper class for creating a multitrack audio player.
|
|
3
|
+
* Individual tracks are synced and played together. They can be dragged to set their start position.
|
|
4
|
+
*/
|
|
5
|
+
import WaveSurfer from '../wavesurfer.js';
|
|
2
6
|
import RegionsPlugin from './regions.js';
|
|
3
|
-
|
|
7
|
+
import TimelinePlugin from './timeline.js';
|
|
8
|
+
import EnvelopePlugin from './envelope.js';
|
|
9
|
+
import EventEmitter from '../event-emitter.js';
|
|
10
|
+
class MultiTrack extends EventEmitter {
|
|
4
11
|
static create(tracks, options) {
|
|
5
12
|
return new MultiTrack(tracks, options);
|
|
6
13
|
}
|
|
7
14
|
constructor(tracks, options) {
|
|
15
|
+
super();
|
|
8
16
|
this.audios = [];
|
|
9
17
|
this.wavesurfers = [];
|
|
10
18
|
this.durations = [];
|
|
11
19
|
this.currentTime = 0;
|
|
12
20
|
this.maxDuration = 0;
|
|
13
21
|
this.isDragging = false;
|
|
14
|
-
this.
|
|
22
|
+
this.frameRequest = null;
|
|
23
|
+
this.timer = null;
|
|
24
|
+
this.subscriptions = [];
|
|
25
|
+
this.timeline = null;
|
|
26
|
+
this.tracks = tracks.map((track) => ({
|
|
27
|
+
...track,
|
|
28
|
+
startPosition: track.startPosition || 0,
|
|
29
|
+
peaks: track.peaks || (track.url ? undefined : [new Float32Array()]),
|
|
30
|
+
}));
|
|
15
31
|
this.options = options;
|
|
16
32
|
this.rendering = initRendering(this.tracks, this.options);
|
|
17
|
-
this.
|
|
18
|
-
this.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
this.
|
|
33
|
+
this.rendering.addDropHandler((trackId) => {
|
|
34
|
+
this.emit('drop', { id: trackId });
|
|
35
|
+
});
|
|
36
|
+
this.initAllAudios().then((durations) => {
|
|
37
|
+
this.initDurations(durations);
|
|
38
|
+
this.initAllWavesurfers();
|
|
39
|
+
this.rendering.containers.forEach((container, index) => {
|
|
40
|
+
const drag = initDragging(container, (delta) => this.onDrag(index, delta), options.rightButtonDrag);
|
|
41
|
+
this.wavesurfers[index].once('destroy', () => drag?.destroy());
|
|
42
|
+
});
|
|
23
43
|
this.rendering.addClickHandler((position) => {
|
|
24
|
-
this.
|
|
44
|
+
if (this.isDragging)
|
|
45
|
+
return;
|
|
46
|
+
this.seekTo(position);
|
|
25
47
|
});
|
|
26
|
-
this.
|
|
48
|
+
this.emit('canplay');
|
|
27
49
|
});
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
50
|
+
}
|
|
51
|
+
initDurations(durations) {
|
|
52
|
+
this.durations = durations;
|
|
53
|
+
this.maxDuration = this.tracks.reduce((max, track, index) => {
|
|
54
|
+
return Math.max(max, track.startPosition + durations[index]);
|
|
55
|
+
}, 0);
|
|
56
|
+
this.rendering.setMainWidth(durations, this.maxDuration);
|
|
57
|
+
}
|
|
58
|
+
initAudio(track) {
|
|
59
|
+
const audio = new Audio(track.url);
|
|
60
|
+
return new Promise((resolve) => {
|
|
61
|
+
if (!audio.src)
|
|
62
|
+
return resolve(audio);
|
|
63
|
+
audio.addEventListener('loadedmetadata', () => resolve(audio), { once: true });
|
|
34
64
|
});
|
|
35
65
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
this.audios
|
|
39
|
-
return Promise.all(this.audios.map((audio) => {
|
|
40
|
-
return new Promise((resolve) => {
|
|
41
|
-
audio.addEventListener('canplay', () => {
|
|
42
|
-
resolve(audio.duration);
|
|
43
|
-
}, { once: true });
|
|
44
|
-
});
|
|
45
|
-
}));
|
|
66
|
+
async initAllAudios() {
|
|
67
|
+
this.audios = await Promise.all(this.tracks.map((track) => this.initAudio(track)));
|
|
68
|
+
return this.audios.map((a) => (a.src ? a.duration : 0));
|
|
46
69
|
}
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
70
|
+
initWavesurfer(track, index) {
|
|
71
|
+
const container = this.rendering.containers[index];
|
|
72
|
+
// Create a wavesurfer instance
|
|
73
|
+
const ws = WaveSurfer.create({
|
|
74
|
+
...track.options,
|
|
75
|
+
container,
|
|
76
|
+
minPxPerSec: 0,
|
|
77
|
+
media: this.audios[index],
|
|
78
|
+
peaks: track.peaks,
|
|
79
|
+
cursorColor: 'transparent',
|
|
80
|
+
cursorWidth: 0,
|
|
81
|
+
interact: false,
|
|
82
|
+
hideScrollbar: true,
|
|
83
|
+
});
|
|
84
|
+
// Regions and markers
|
|
85
|
+
const wsRegions = RegionsPlugin.create();
|
|
86
|
+
ws.registerPlugin(wsRegions);
|
|
87
|
+
this.subscriptions.push(ws.once('decode', () => {
|
|
88
|
+
// Start and end cues
|
|
89
|
+
if (track.startCue != null || track.endCue != null) {
|
|
90
|
+
const { startCue = 0, endCue = this.durations[index] } = track;
|
|
91
|
+
const startCueRegion = wsRegions.addRegion({
|
|
92
|
+
start: 0,
|
|
93
|
+
end: startCue,
|
|
94
|
+
color: 'rgba(0, 0, 0, 0.7)',
|
|
95
|
+
drag: false,
|
|
96
|
+
});
|
|
97
|
+
const endCueRegion = wsRegions.addRegion({
|
|
98
|
+
start: endCue,
|
|
99
|
+
end: endCue + this.durations[index],
|
|
100
|
+
color: 'rgba(0, 0, 0, 0.7)',
|
|
101
|
+
drag: false,
|
|
102
|
+
});
|
|
103
|
+
// Allow resizing only from one side
|
|
104
|
+
startCueRegion.element.firstElementChild?.remove();
|
|
105
|
+
endCueRegion.element.lastChild?.remove();
|
|
106
|
+
// Prevent clicks when dragging
|
|
107
|
+
// Update the start and end cues on resize
|
|
108
|
+
this.subscriptions.push(startCueRegion.on('update-end', () => {
|
|
109
|
+
track.startCue = startCueRegion.end;
|
|
110
|
+
this.emit('start-cue-change', { id: track.id, startCue: track.startCue });
|
|
111
|
+
}), endCueRegion.on('update-end', () => {
|
|
112
|
+
track.endCue = endCueRegion.start;
|
|
113
|
+
this.emit('end-cue-change', { id: track.id, endCue: track.endCue });
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
116
|
+
// Intro
|
|
117
|
+
if (track.intro) {
|
|
118
|
+
const introRegion = wsRegions.addRegion({
|
|
119
|
+
start: 0,
|
|
120
|
+
end: track.intro.endTime,
|
|
121
|
+
content: track.intro.label,
|
|
122
|
+
color: this.options.trackBackground,
|
|
123
|
+
drag: false,
|
|
124
|
+
});
|
|
125
|
+
introRegion.element.querySelector('[data-resize="left"]')?.remove();
|
|
126
|
+
introRegion.element.parentElement.style.mixBlendMode = 'plus-lighter';
|
|
127
|
+
if (track.intro.color) {
|
|
128
|
+
;
|
|
129
|
+
introRegion.element.querySelector('[data-resize="right"]').style.borderColor =
|
|
130
|
+
track.intro.color;
|
|
68
131
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
132
|
+
this.subscriptions.push(introRegion.on('update-end', () => {
|
|
133
|
+
this.emit('intro-end-change', { id: track.id, endTime: introRegion.end });
|
|
134
|
+
}));
|
|
135
|
+
}
|
|
136
|
+
// Render markers
|
|
137
|
+
if (track.markers) {
|
|
138
|
+
track.markers.forEach((marker) => {
|
|
139
|
+
wsRegions.addRegion({
|
|
140
|
+
start: marker.time,
|
|
141
|
+
content: marker.label,
|
|
142
|
+
color: marker.color,
|
|
143
|
+
resize: false,
|
|
144
|
+
});
|
|
73
145
|
});
|
|
74
|
-
}
|
|
75
|
-
|
|
146
|
+
}
|
|
147
|
+
}));
|
|
148
|
+
// Envelope
|
|
149
|
+
const envelope = ws.registerPlugin(EnvelopePlugin.create({
|
|
150
|
+
...this.options.envelopeOptions,
|
|
151
|
+
fadeInStart: track.startCue,
|
|
152
|
+
fadeInEnd: track.fadeInEnd,
|
|
153
|
+
fadeOutStart: track.fadeOutStart,
|
|
154
|
+
fadeOutEnd: track.endCue,
|
|
155
|
+
volume: track.volume,
|
|
156
|
+
}));
|
|
157
|
+
this.subscriptions.push(envelope.on('volume-change', (volume) => {
|
|
158
|
+
this.setIsDragging();
|
|
159
|
+
this.emit('volume-change', { id: track.id, volume });
|
|
160
|
+
}), envelope.on('fade-in-change', (time) => {
|
|
161
|
+
this.setIsDragging();
|
|
162
|
+
this.emit('fade-in-change', { id: track.id, fadeInEnd: time });
|
|
163
|
+
}), envelope.on('fade-out-change', (time) => {
|
|
164
|
+
this.setIsDragging();
|
|
165
|
+
this.emit('fade-out-change', { id: track.id, fadeOutStart: time });
|
|
166
|
+
}), this.on('start-cue-change', ({ id, startCue }) => {
|
|
167
|
+
if (id === track.id) {
|
|
168
|
+
envelope.setStartTime(startCue);
|
|
169
|
+
}
|
|
170
|
+
}), this.on('end-cue-change', ({ id, endCue }) => {
|
|
171
|
+
if (id === track.id) {
|
|
172
|
+
envelope.setEndTime(endCue);
|
|
173
|
+
}
|
|
174
|
+
}));
|
|
175
|
+
return ws;
|
|
176
|
+
}
|
|
177
|
+
initAllWavesurfers() {
|
|
178
|
+
const wavesurfers = this.tracks.map((track, index) => {
|
|
179
|
+
return this.initWavesurfer(track, index);
|
|
76
180
|
});
|
|
77
181
|
this.wavesurfers = wavesurfers;
|
|
182
|
+
this.initTimeline();
|
|
183
|
+
}
|
|
184
|
+
initTimeline() {
|
|
185
|
+
if (this.timeline)
|
|
186
|
+
this.timeline.destroy();
|
|
187
|
+
this.timeline = this.wavesurfers[0].registerPlugin(TimelinePlugin.create({
|
|
188
|
+
duration: this.maxDuration,
|
|
189
|
+
container: this.rendering.containers[0].parentElement,
|
|
190
|
+
}));
|
|
78
191
|
}
|
|
79
|
-
updatePosition(time) {
|
|
192
|
+
updatePosition(time, autoCenter = false) {
|
|
80
193
|
const precisionSeconds = 0.3;
|
|
81
|
-
this.currentTime = time;
|
|
82
|
-
this.rendering.updateCursor(time / this.maxDuration);
|
|
83
194
|
const isPaused = !this.isPlaying();
|
|
195
|
+
if (time !== this.currentTime) {
|
|
196
|
+
this.currentTime = time;
|
|
197
|
+
this.rendering.updateCursor(time / this.maxDuration, autoCenter);
|
|
198
|
+
}
|
|
84
199
|
// Update the current time of each audio
|
|
85
200
|
this.tracks.forEach((track, index) => {
|
|
86
201
|
const audio = this.audios[index];
|
|
@@ -103,23 +218,30 @@ class MultiTrack {
|
|
|
103
218
|
audio.volume = newVolume;
|
|
104
219
|
});
|
|
105
220
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
this.play();
|
|
221
|
+
setIsDragging() {
|
|
222
|
+
// Prevent click events when dragging
|
|
223
|
+
this.isDragging = true;
|
|
224
|
+
if (this.timer)
|
|
225
|
+
clearTimeout(this.timer);
|
|
226
|
+
this.timer = setTimeout(() => (this.isDragging = false), 300);
|
|
113
227
|
}
|
|
114
228
|
onDrag(index, delta) {
|
|
229
|
+
this.setIsDragging();
|
|
115
230
|
const track = this.tracks[index];
|
|
116
231
|
if (!track.draggable)
|
|
117
232
|
return;
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
this.
|
|
233
|
+
const newStartPosition = track.startPosition + delta * this.maxDuration;
|
|
234
|
+
const mainIndex = this.tracks.findIndex((item) => item.url && !item.draggable);
|
|
235
|
+
const mainTrack = this.tracks[mainIndex];
|
|
236
|
+
const minStart = (mainTrack ? mainTrack.startPosition : 0) - this.durations[index];
|
|
237
|
+
const maxStart = mainTrack ? mainTrack.startPosition + this.durations[mainIndex] : this.maxDuration;
|
|
238
|
+
if (newStartPosition >= minStart && newStartPosition <= maxStart) {
|
|
239
|
+
track.startPosition = newStartPosition;
|
|
240
|
+
this.initDurations(this.durations);
|
|
241
|
+
this.rendering.setContainerOffsets();
|
|
242
|
+
this.updatePosition(this.currentTime);
|
|
243
|
+
this.emit('start-position-change', { id: track.id, startPosition: newStartPosition });
|
|
244
|
+
}
|
|
123
245
|
}
|
|
124
246
|
findCurrentTracks() {
|
|
125
247
|
// Find the audios at the current time
|
|
@@ -137,11 +259,26 @@ class MultiTrack {
|
|
|
137
259
|
}
|
|
138
260
|
return indexes;
|
|
139
261
|
}
|
|
262
|
+
startSync() {
|
|
263
|
+
const onFrame = () => {
|
|
264
|
+
const position = this.audios.reduce((pos, audio, index) => {
|
|
265
|
+
if (!audio.paused) {
|
|
266
|
+
pos = Math.max(pos, audio.currentTime + this.tracks[index].startPosition);
|
|
267
|
+
}
|
|
268
|
+
return pos;
|
|
269
|
+
}, this.currentTime);
|
|
270
|
+
if (position > this.currentTime) {
|
|
271
|
+
this.updatePosition(position, true);
|
|
272
|
+
}
|
|
273
|
+
this.frameRequest = requestAnimationFrame(onFrame);
|
|
274
|
+
};
|
|
275
|
+
onFrame();
|
|
276
|
+
}
|
|
140
277
|
play() {
|
|
278
|
+
this.startSync();
|
|
141
279
|
const indexes = this.findCurrentTracks();
|
|
142
280
|
indexes.forEach((index) => {
|
|
143
|
-
|
|
144
|
-
(_a = this.audios[index]) === null || _a === void 0 ? void 0 : _a.play();
|
|
281
|
+
this.audios[index]?.play();
|
|
145
282
|
});
|
|
146
283
|
}
|
|
147
284
|
pause() {
|
|
@@ -150,7 +287,51 @@ class MultiTrack {
|
|
|
150
287
|
isPlaying() {
|
|
151
288
|
return this.audios.some((audio) => !audio.paused);
|
|
152
289
|
}
|
|
290
|
+
getCurrentTime() {
|
|
291
|
+
return this.currentTime;
|
|
292
|
+
}
|
|
293
|
+
/** Position percentage from 0 to 1 */
|
|
294
|
+
seekTo(position) {
|
|
295
|
+
const wasPlaying = this.isPlaying();
|
|
296
|
+
this.updatePosition(position * this.maxDuration);
|
|
297
|
+
if (wasPlaying)
|
|
298
|
+
this.play();
|
|
299
|
+
}
|
|
300
|
+
/** Set time in seconds */
|
|
301
|
+
setTime(time) {
|
|
302
|
+
const wasPlaying = this.isPlaying();
|
|
303
|
+
this.updatePosition(time);
|
|
304
|
+
if (wasPlaying)
|
|
305
|
+
this.play();
|
|
306
|
+
}
|
|
307
|
+
zoom(pxPerSec) {
|
|
308
|
+
this.options.minPxPerSec = pxPerSec;
|
|
309
|
+
this.wavesurfers.forEach((ws, index) => this.tracks[index].url && ws.zoom(pxPerSec));
|
|
310
|
+
this.rendering.setMainWidth(this.durations, this.maxDuration);
|
|
311
|
+
this.rendering.setContainerOffsets();
|
|
312
|
+
}
|
|
313
|
+
addTrack(track) {
|
|
314
|
+
const index = this.tracks.findIndex((t) => t.id === track.id);
|
|
315
|
+
if (index !== -1) {
|
|
316
|
+
this.tracks[index] = track;
|
|
317
|
+
this.initAudio(track).then((audio) => {
|
|
318
|
+
this.audios[index] = audio;
|
|
319
|
+
this.durations[index] = audio.duration;
|
|
320
|
+
this.initDurations(this.durations);
|
|
321
|
+
const container = this.rendering.containers[index];
|
|
322
|
+
container.innerHTML = '';
|
|
323
|
+
this.wavesurfers[index].destroy();
|
|
324
|
+
this.wavesurfers[index] = this.initWavesurfer(track, index);
|
|
325
|
+
const drag = initDragging(container, (delta) => this.onDrag(index, delta), this.options.rightButtonDrag);
|
|
326
|
+
this.wavesurfers[index].once('destroy', () => drag?.destroy());
|
|
327
|
+
this.initTimeline();
|
|
328
|
+
this.emit('canplay');
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
}
|
|
153
332
|
destroy() {
|
|
333
|
+
if (this.frameRequest)
|
|
334
|
+
cancelAnimationFrame(this.frameRequest);
|
|
154
335
|
this.rendering.destroy();
|
|
155
336
|
this.audios.forEach((audio) => {
|
|
156
337
|
audio.pause();
|
|
@@ -160,10 +341,17 @@ class MultiTrack {
|
|
|
160
341
|
ws.destroy();
|
|
161
342
|
});
|
|
162
343
|
}
|
|
344
|
+
// See https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/setSinkId
|
|
345
|
+
setSinkId(sinkId) {
|
|
346
|
+
return Promise.all(this.wavesurfers.map((ws) => ws.setSinkId(sinkId)));
|
|
347
|
+
}
|
|
163
348
|
}
|
|
164
349
|
function initRendering(tracks, options) {
|
|
350
|
+
let pxPerSec = 0;
|
|
351
|
+
let durations = [];
|
|
352
|
+
let mainWidth = 0;
|
|
165
353
|
// Create a common container for all tracks
|
|
166
|
-
|
|
354
|
+
const scroll = document.createElement('div');
|
|
167
355
|
scroll.setAttribute('style', 'width: 100%; overflow-x: scroll; overflow-y: hidden; user-select: none;');
|
|
168
356
|
const wrapper = document.createElement('div');
|
|
169
357
|
wrapper.style.position = 'relative';
|
|
@@ -171,37 +359,77 @@ function initRendering(tracks, options) {
|
|
|
171
359
|
options.container.appendChild(scroll);
|
|
172
360
|
// Create a common cursor
|
|
173
361
|
const cursor = document.createElement('div');
|
|
174
|
-
cursor.setAttribute('style', '
|
|
175
|
-
cursor.style.backgroundColor = options.cursorColor ||
|
|
362
|
+
cursor.setAttribute('style', 'height: 100%; position: absolute; z-index: 10; top: 0; left: 0');
|
|
363
|
+
cursor.style.backgroundColor = options.cursorColor || '#000';
|
|
364
|
+
cursor.style.width = `${options.cursorWidth ?? 1}px`;
|
|
176
365
|
wrapper.appendChild(cursor);
|
|
366
|
+
const { clientWidth } = wrapper;
|
|
177
367
|
// Create containers for each track
|
|
178
|
-
const containers = tracks.map(() => {
|
|
368
|
+
const containers = tracks.map((track, index) => {
|
|
179
369
|
const container = document.createElement('div');
|
|
180
|
-
container.style.
|
|
370
|
+
container.style.position = 'relative';
|
|
371
|
+
if (options.trackBorderColor && index > 0) {
|
|
372
|
+
const borderDiv = document.createElement('div');
|
|
373
|
+
borderDiv.setAttribute('style', `width: 100%; height: 2px; background-color: ${options.trackBorderColor}`);
|
|
374
|
+
wrapper.appendChild(borderDiv);
|
|
375
|
+
}
|
|
376
|
+
if (options.trackBackground && track.url) {
|
|
377
|
+
container.style.background = options.trackBackground;
|
|
378
|
+
}
|
|
379
|
+
// No audio on this track, so make it droppable
|
|
380
|
+
if (!track.url) {
|
|
381
|
+
const dropArea = document.createElement('div');
|
|
382
|
+
dropArea.setAttribute('style', `position: absolute; z-index: 10; left: 10px; top: 10px; right: 10px; bottom: 10px; border: 2px dashed ${options.trackBorderColor};`);
|
|
383
|
+
dropArea.addEventListener('dragover', (e) => {
|
|
384
|
+
e.preventDefault();
|
|
385
|
+
dropArea.style.background = options.trackBackground || '';
|
|
386
|
+
});
|
|
387
|
+
dropArea.addEventListener('dragleave', (e) => {
|
|
388
|
+
e.preventDefault();
|
|
389
|
+
dropArea.style.background = '';
|
|
390
|
+
});
|
|
391
|
+
dropArea.addEventListener('drop', (e) => {
|
|
392
|
+
e.preventDefault();
|
|
393
|
+
dropArea.style.background = '';
|
|
394
|
+
});
|
|
395
|
+
container.appendChild(dropArea);
|
|
396
|
+
}
|
|
181
397
|
wrapper.appendChild(container);
|
|
182
398
|
return container;
|
|
183
399
|
});
|
|
184
400
|
// Set the positions of each container
|
|
185
401
|
const setContainerOffsets = () => {
|
|
186
402
|
containers.forEach((container, i) => {
|
|
187
|
-
const offset =
|
|
403
|
+
const offset = tracks[i].startPosition * pxPerSec;
|
|
404
|
+
if (durations[i]) {
|
|
405
|
+
container.style.width = `${durations[i] * pxPerSec}px`;
|
|
406
|
+
}
|
|
188
407
|
container.style.transform = `translateX(${offset}px)`;
|
|
189
|
-
if (tracks[i].draggable)
|
|
190
|
-
container.style.cursor = 'ew-resize';
|
|
191
408
|
});
|
|
192
409
|
};
|
|
193
|
-
setContainerOffsets();
|
|
194
410
|
return {
|
|
195
411
|
containers,
|
|
196
412
|
// Set the start offset
|
|
197
413
|
setContainerOffsets,
|
|
198
414
|
// Set the container width
|
|
199
|
-
setMainWidth: (
|
|
200
|
-
|
|
415
|
+
setMainWidth: (trackDurations, maxDuration) => {
|
|
416
|
+
durations = trackDurations;
|
|
417
|
+
pxPerSec = Math.max(options.minPxPerSec || 0, clientWidth / maxDuration);
|
|
418
|
+
mainWidth = pxPerSec * maxDuration;
|
|
419
|
+
wrapper.style.width = `${mainWidth}px`;
|
|
420
|
+
setContainerOffsets();
|
|
201
421
|
},
|
|
202
422
|
// Update cursor position
|
|
203
|
-
updateCursor: (position) => {
|
|
423
|
+
updateCursor: (position, autoCenter) => {
|
|
204
424
|
cursor.style.left = `${Math.min(100, position * 100)}%`;
|
|
425
|
+
// Update scroll
|
|
426
|
+
const { clientWidth, scrollLeft } = scroll;
|
|
427
|
+
const center = clientWidth / 2;
|
|
428
|
+
const minScroll = autoCenter ? center : clientWidth;
|
|
429
|
+
const pos = position * mainWidth;
|
|
430
|
+
if (pos > scrollLeft + minScroll || pos < scrollLeft) {
|
|
431
|
+
scroll.scrollLeft = pos - center;
|
|
432
|
+
}
|
|
205
433
|
},
|
|
206
434
|
// Click to seek
|
|
207
435
|
addClickHandler: (onClick) => {
|
|
@@ -216,24 +444,43 @@ function initRendering(tracks, options) {
|
|
|
216
444
|
destroy: () => {
|
|
217
445
|
scroll.remove();
|
|
218
446
|
},
|
|
447
|
+
// Do something on drop
|
|
448
|
+
addDropHandler: (onDrop) => {
|
|
449
|
+
tracks.forEach((track, index) => {
|
|
450
|
+
if (!track.url) {
|
|
451
|
+
const droppable = containers[index].querySelector('div');
|
|
452
|
+
droppable?.addEventListener('drop', (e) => {
|
|
453
|
+
e.preventDefault();
|
|
454
|
+
onDrop(track.id);
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
},
|
|
219
459
|
};
|
|
220
460
|
}
|
|
221
|
-
function initDragging(container, onDrag) {
|
|
461
|
+
function initDragging(container, onDrag, rightButtonDrag = false) {
|
|
222
462
|
const wrapper = container.parentElement;
|
|
223
463
|
if (!wrapper)
|
|
224
464
|
return;
|
|
225
465
|
// Dragging tracks to set position
|
|
226
466
|
let dragStart = null;
|
|
467
|
+
container.addEventListener('contextmenu', (e) => {
|
|
468
|
+
rightButtonDrag && e.preventDefault();
|
|
469
|
+
});
|
|
227
470
|
// Drag start
|
|
228
471
|
container.addEventListener('mousedown', (e) => {
|
|
472
|
+
if (rightButtonDrag && e.button !== 2)
|
|
473
|
+
return;
|
|
229
474
|
const rect = wrapper.getBoundingClientRect();
|
|
230
475
|
dragStart = e.clientX - rect.left;
|
|
476
|
+
container.style.cursor = 'grabbing';
|
|
231
477
|
});
|
|
232
478
|
// Drag end
|
|
233
479
|
const onMouseUp = (e) => {
|
|
234
480
|
if (dragStart != null) {
|
|
235
481
|
e.stopPropagation();
|
|
236
482
|
dragStart = null;
|
|
483
|
+
container.style.cursor = '';
|
|
237
484
|
}
|
|
238
485
|
};
|
|
239
486
|
// Drag move
|