wavesurfer.js 7.0.0-alpha.2 → 7.0.0-alpha.21

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.
@@ -0,0 +1,58 @@
1
+ import BasePlugin from '../base-plugin.js';
2
+ import type { WaveSurferPluginParams } from '../index.js';
3
+ export type EnvelopePluginOptions = {
4
+ startTime?: number;
5
+ endTime?: number;
6
+ fadeInEnd?: number;
7
+ fadeOutStart?: number;
8
+ volume?: number;
9
+ lineWidth?: string;
10
+ lineColor?: string;
11
+ dragPointSize?: number;
12
+ dragPointFill?: string;
13
+ dragPointStroke?: string;
14
+ };
15
+ declare const defaultOptions: {
16
+ startTime: number;
17
+ endTime: number;
18
+ fadeInEnd: number;
19
+ fadeOutStart: number;
20
+ lineWidth: number;
21
+ lineColor: string;
22
+ dragPointSize: number;
23
+ dragPointFill: string;
24
+ dragPointStroke: string;
25
+ };
26
+ export type EnvelopePluginEvents = {
27
+ 'fade-in-change': {
28
+ time: number;
29
+ };
30
+ 'fade-out-change': {
31
+ time: number;
32
+ };
33
+ 'volume-change': {
34
+ volume: number;
35
+ };
36
+ };
37
+ declare class EnvelopePlugin extends BasePlugin<EnvelopePluginEvents, EnvelopePluginOptions> {
38
+ protected options: EnvelopePluginOptions & typeof defaultOptions;
39
+ private svg;
40
+ private audioContext;
41
+ private gainNode;
42
+ private volume;
43
+ private isFadingIn;
44
+ private isFadingOut;
45
+ constructor(params: WaveSurferPluginParams, options: EnvelopePluginOptions);
46
+ private makeDraggable;
47
+ private renderPolyline;
48
+ private initSvg;
49
+ destroy(): void;
50
+ private initWebAudio;
51
+ private naturalVolume;
52
+ private onVolumeChange;
53
+ private initFadeEffects;
54
+ getCurrentVolume(): number;
55
+ setStartTime(time: number): void;
56
+ setEndTime(time: number): void;
57
+ }
58
+ export default EnvelopePlugin;
@@ -0,0 +1,292 @@
1
+ import BasePlugin from '../base-plugin.js';
2
+ const defaultOptions = {
3
+ startTime: 0,
4
+ endTime: 0,
5
+ fadeInEnd: 0,
6
+ fadeOutStart: 0,
7
+ lineWidth: 4,
8
+ lineColor: 'rgba(0, 0, 255, 0.5)',
9
+ dragPointSize: 10,
10
+ dragPointFill: 'rgba(255, 255, 255, 0.8)',
11
+ dragPointStroke: 'rgba(255, 255, 255, 0.8)',
12
+ };
13
+ class EnvelopePlugin extends BasePlugin {
14
+ options;
15
+ svg = null;
16
+ audioContext = null;
17
+ gainNode = null;
18
+ volume = 1;
19
+ isFadingIn = false;
20
+ isFadingOut = false;
21
+ constructor(params, options) {
22
+ super(params, options);
23
+ this.options = Object.assign({}, defaultOptions, options);
24
+ this.options.lineColor = this.options.lineColor || defaultOptions.lineColor;
25
+ this.options.dragPointFill = this.options.dragPointFill || defaultOptions.dragPointFill;
26
+ this.options.dragPointStroke = this.options.dragPointStroke || defaultOptions.dragPointStroke;
27
+ this.volume = this.options.volume ?? 1;
28
+ this.subscriptions.push(this.wavesurfer.once('decode', ({ duration }) => {
29
+ this.options.startTime = this.options.startTime || 0;
30
+ this.options.endTime = this.options.endTime || duration;
31
+ this.options.fadeInEnd = this.options.fadeInEnd || this.options.startTime;
32
+ this.options.fadeOutStart = this.options.fadeOutStart || this.options.endTime;
33
+ this.initWebAudio();
34
+ this.initSvg();
35
+ this.initFadeEffects();
36
+ }));
37
+ let delay;
38
+ this.subscriptions.push(this.wavesurfer.on('zoom', () => {
39
+ if (delay)
40
+ clearTimeout(delay);
41
+ delay = setTimeout(() => {
42
+ this.svg?.remove();
43
+ this.initSvg();
44
+ }, 100);
45
+ }));
46
+ }
47
+ makeDraggable(draggable, onDrag) {
48
+ draggable.addEventListener('mousedown', (e) => {
49
+ let x = e.clientX;
50
+ let y = e.clientY;
51
+ const wasInteractive = this.wavesurfer.options.interactive;
52
+ let delay;
53
+ // Make the wavesurfer ignore clicks when we're dragging
54
+ this.wavesurfer.toggleInteractive(false);
55
+ const move = (e) => {
56
+ const dx = e.clientX - x;
57
+ const dy = e.clientY - y;
58
+ x = e.clientX;
59
+ y = e.clientY;
60
+ onDrag(dx, dy);
61
+ };
62
+ const up = () => {
63
+ document.removeEventListener('mousemove', move);
64
+ document.removeEventListener('mouseup', up);
65
+ // Restore interactive state
66
+ if (delay)
67
+ clearTimeout(delay);
68
+ delay = setTimeout(() => {
69
+ this.wavesurfer.toggleInteractive(wasInteractive);
70
+ }, 100);
71
+ };
72
+ document.addEventListener('mousemove', move);
73
+ document.addEventListener('mouseup', up);
74
+ e.preventDefault();
75
+ e.stopPropagation();
76
+ });
77
+ }
78
+ renderPolyline() {
79
+ if (!this.svg)
80
+ return;
81
+ const polyline = this.svg.querySelector('polyline');
82
+ const points = polyline.points;
83
+ const top = points.getItem(1).y;
84
+ const width = this.wrapper.clientWidth;
85
+ const duration = this.wavesurfer.getDuration();
86
+ points.getItem(0).x = (this.options.startTime / duration) * width;
87
+ points.getItem(3).x = (this.options.endTime / duration) * width;
88
+ const line = this.svg.querySelector('line');
89
+ line.setAttribute('x1', points.getItem(1).x.toString());
90
+ line.setAttribute('x2', points.getItem(2).x.toString());
91
+ line.setAttribute('y1', top.toString());
92
+ line.setAttribute('y2', top.toString());
93
+ const circles = this.svg.querySelectorAll('circle');
94
+ for (let i = 0; i < circles.length; i++) {
95
+ const circle = circles[i];
96
+ const point = polyline.points.getItem(i + 1);
97
+ circle.setAttribute('cx', point.x.toString());
98
+ circle.setAttribute('cy', top.toString());
99
+ }
100
+ }
101
+ initSvg() {
102
+ const width = this.wrapper.clientWidth;
103
+ const height = this.wrapper.clientHeight;
104
+ const duration = this.wavesurfer.getDuration();
105
+ // SVG element
106
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
107
+ svg.setAttribute('width', '100%');
108
+ svg.setAttribute('height', '100%');
109
+ svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
110
+ svg.setAttribute('preserveAspectRatio', 'none');
111
+ svg.setAttribute('style', 'position: absolute; left: 0; top: 0; z-index: 4; pointer-events: none;');
112
+ this.svg = svg;
113
+ // A polyline representing the envelope
114
+ const polyline = document.createElementNS('http://www.w3.org/2000/svg', 'polyline');
115
+ polyline.setAttribute('points', '0,0 0,0 0,0 0,0');
116
+ polyline.setAttribute('stroke', this.options.lineColor);
117
+ polyline.setAttribute('stroke-width', this.options.lineWidth);
118
+ polyline.setAttribute('fill', 'none');
119
+ polyline.setAttribute('style', 'pointer-events: none');
120
+ svg.appendChild(polyline);
121
+ // Draggable top line
122
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
123
+ line.setAttribute('stroke', 'none');
124
+ line.setAttribute('stroke-width', (this.options.lineWidth * 3).toString());
125
+ line.setAttribute('style', 'cursor: ns-resize; pointer-events: all;');
126
+ svg.appendChild(line);
127
+ const points = polyline.points;
128
+ const offset = this.options.dragPointSize / 2;
129
+ const top = height - this.volume * height + offset;
130
+ points.getItem(0).x = (this.options.startTime / duration) * width;
131
+ points.getItem(0).y = height;
132
+ points.getItem(1).x = (this.options.fadeInEnd / duration) * width;
133
+ points.getItem(1).y = top;
134
+ points.getItem(2).x = (this.options.fadeOutStart / duration) * width;
135
+ points.getItem(2).y = top;
136
+ points.getItem(3).x = (this.options.endTime / duration) * width;
137
+ points.getItem(3).y = height;
138
+ // Drag points
139
+ const dragPoints = [1, 2];
140
+ dragPoints.forEach(() => {
141
+ const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
142
+ circle.setAttribute('r', (this.options.dragPointSize / 2).toString());
143
+ circle.setAttribute('fill', this.options.dragPointFill);
144
+ circle.setAttribute('stroke', this.options.dragPointStroke || this.options.dragPointFill);
145
+ circle.setAttribute('stroke-width', '2');
146
+ circle.setAttribute('style', 'cursor: ew-resize; pointer-events: all;');
147
+ svg.appendChild(circle);
148
+ });
149
+ this.wrapper.appendChild(svg);
150
+ // Initial polyline
151
+ this.renderPolyline();
152
+ // On top line drag
153
+ const onDragY = (dy) => {
154
+ const newTop = points.getItem(1).y + dy - offset;
155
+ if (newTop < -0.5 || newTop > height)
156
+ return;
157
+ points.getItem(1).y = newTop + offset;
158
+ points.getItem(2).y = newTop + offset;
159
+ this.renderPolyline();
160
+ const newVolume = Math.min(1, Math.max(0, (height - newTop) / height));
161
+ this.onVolumeChange(newVolume);
162
+ this.renderPolyline();
163
+ };
164
+ // On points drag
165
+ const onDragX = (dx, dy, index) => {
166
+ const point = polyline.points.getItem(index);
167
+ const newX = point.x + dx;
168
+ const newTime = (newX / width) * duration;
169
+ if ((index === 1 && newTime > this.options.fadeOutStart) || newTime < this.options.startTime)
170
+ return;
171
+ if ((index === 2 && newTime < this.options.fadeInEnd) || newTime > this.options.endTime)
172
+ return;
173
+ point.x = newX;
174
+ if (index === 1) {
175
+ this.options.fadeInEnd = newTime;
176
+ this.emit('fade-in-change', { time: newTime });
177
+ }
178
+ else if (index === 2) {
179
+ this.options.fadeOutStart = newTime;
180
+ this.emit('fade-out-change', { time: newTime });
181
+ }
182
+ // Also allow dragging points vertically
183
+ if (dy > 1) {
184
+ onDragY(dy);
185
+ }
186
+ else {
187
+ this.renderPolyline();
188
+ }
189
+ };
190
+ // Draggable top line of the polyline
191
+ this.makeDraggable(line, (_, dy) => onDragY(dy));
192
+ // Make each point draggable
193
+ const draggables = svg.querySelectorAll('circle');
194
+ for (let i = 0; i < draggables.length; i++) {
195
+ const index = i + 1;
196
+ this.makeDraggable(draggables[i], (dx, dy) => onDragX(dx, dy, index));
197
+ }
198
+ }
199
+ destroy() {
200
+ this.svg?.remove();
201
+ super.destroy();
202
+ }
203
+ initWebAudio() {
204
+ const audio = this.wavesurfer.getMediaElement();
205
+ if (!audio)
206
+ return null;
207
+ this.volume = this.options.volume ?? audio.volume;
208
+ // Create an AudioContext
209
+ const audioContext = new window.AudioContext();
210
+ // Create a GainNode for controlling the volume
211
+ this.gainNode = audioContext.createGain();
212
+ this.gainNode.gain.value = this.volume;
213
+ // Create a MediaElementAudioSourceNode using the audio element
214
+ const source = audioContext.createMediaElementSource(audio);
215
+ // Connect the source to the GainNode, and the GainNode to the destination (speakers)
216
+ source.connect(this.gainNode);
217
+ this.gainNode.connect(audioContext.destination);
218
+ this.audioContext = audioContext;
219
+ }
220
+ naturalVolume(value) {
221
+ const minValue = 0.0001;
222
+ const maxValue = 1;
223
+ const exponent = 3; // Adjust the exponent to change the curve of the volume control
224
+ const interpolatedValue = minValue + (maxValue - minValue) * Math.pow(value, exponent);
225
+ return interpolatedValue;
226
+ }
227
+ onVolumeChange(volume) {
228
+ volume = this.naturalVolume(volume);
229
+ this.volume = volume;
230
+ this.emit('volume-change', { volume });
231
+ if (!this.gainNode)
232
+ return;
233
+ this.gainNode.gain.value = volume;
234
+ }
235
+ initFadeEffects() {
236
+ if (!this.audioContext)
237
+ return;
238
+ const unsub = this.wavesurfer.on('timeupdate', ({ currentTime }) => {
239
+ if (!this.audioContext || !this.gainNode)
240
+ return;
241
+ if (!this.wavesurfer.isPlaying())
242
+ return;
243
+ if (this.audioContext.state === 'suspended') {
244
+ this.audioContext.resume();
245
+ }
246
+ // Fade in
247
+ if (!this.isFadingIn && currentTime >= this.options.startTime && currentTime <= this.options.fadeInEnd) {
248
+ this.isFadingIn = true;
249
+ // Set the initial gain (volume) to 0 (silent)
250
+ this.gainNode.gain.setValueAtTime(0, this.audioContext.currentTime);
251
+ // Set the target gain (volume) to 1 (full volume) over N seconds
252
+ this.gainNode.gain.linearRampToValueAtTime(this.volume, this.audioContext.currentTime + (this.options.fadeInEnd - currentTime));
253
+ return;
254
+ }
255
+ // Fade out
256
+ if (!this.isFadingOut && currentTime >= this.options.fadeOutStart && currentTime <= this.options.endTime) {
257
+ this.isFadingOut = true;
258
+ // Set the target gain (volume) to 0 (silent) over N seconds
259
+ this.gainNode.gain.linearRampToValueAtTime(0, this.audioContext.currentTime + (this.options.endTime - currentTime));
260
+ return;
261
+ }
262
+ // Reset fade in/out
263
+ let cancelRamp = false;
264
+ if (this.isFadingIn && (currentTime < this.options.startTime || currentTime > this.options.fadeInEnd)) {
265
+ this.isFadingIn = false;
266
+ cancelRamp = true;
267
+ }
268
+ if (this.isFadingOut && (currentTime < this.options.fadeOutStart || currentTime >= this.options.endTime)) {
269
+ this.isFadingOut = false;
270
+ cancelRamp = true;
271
+ }
272
+ if (cancelRamp) {
273
+ this.gainNode.gain.cancelScheduledValues(this.audioContext.currentTime);
274
+ this.gainNode.gain.value = this.volume;
275
+ }
276
+ });
277
+ this.subscriptions.push(unsub);
278
+ }
279
+ getCurrentVolume() {
280
+ return this.gainNode ? this.gainNode.gain.value : this.volume;
281
+ }
282
+ setStartTime(time) {
283
+ this.options.startTime = time;
284
+ console.log(time);
285
+ this.renderPolyline();
286
+ }
287
+ setEndTime(time) {
288
+ this.options.endTime = time;
289
+ this.renderPolyline();
290
+ }
291
+ }
292
+ export default EnvelopePlugin;
@@ -0,0 +1,25 @@
1
+ import BasePlugin from '../base-plugin.js';
2
+ import { type WaveSurferPluginParams, type WaveSurferOptions } from '../index.js';
3
+ export type MinimapPluginOptions = {
4
+ overlayColor?: string;
5
+ } & WaveSurferOptions;
6
+ declare const defaultOptions: {
7
+ height: number;
8
+ overlayColor: string;
9
+ };
10
+ export type MinimapPluginEvents = {
11
+ ready: void;
12
+ };
13
+ declare class TimelinePlugin extends BasePlugin<MinimapPluginEvents, MinimapPluginOptions> {
14
+ protected options: MinimapPluginOptions & typeof defaultOptions;
15
+ private minimapWrapper;
16
+ private miniWavesurfer;
17
+ private overlay;
18
+ constructor(params: WaveSurferPluginParams, options: MinimapPluginOptions);
19
+ private initMinimapWrapper;
20
+ private initOverlay;
21
+ private initMinimap;
22
+ /** Unmount */
23
+ destroy(): void;
24
+ }
25
+ export default TimelinePlugin;
@@ -0,0 +1,63 @@
1
+ import BasePlugin from '../base-plugin.js';
2
+ import WaveSurfer from '../index.js';
3
+ const defaultOptions = {
4
+ height: 50,
5
+ overlayColor: 'rgba(100, 100, 100, 0.1)',
6
+ };
7
+ class TimelinePlugin extends BasePlugin {
8
+ options;
9
+ minimapWrapper;
10
+ miniWavesurfer = null;
11
+ overlay;
12
+ constructor(params, options) {
13
+ super(params, options);
14
+ this.options = Object.assign({}, defaultOptions, options);
15
+ this.minimapWrapper = this.initMinimapWrapper();
16
+ this.overlay = this.initOverlay();
17
+ this.subscriptions.push(this.wavesurfer.on('decode', () => {
18
+ this.initMinimap();
19
+ }));
20
+ }
21
+ initMinimapWrapper() {
22
+ const div = document.createElement('div');
23
+ div.style.position = 'relative';
24
+ this.container.insertAdjacentElement('afterend', div);
25
+ return div;
26
+ }
27
+ initOverlay() {
28
+ const div = document.createElement('div');
29
+ div.setAttribute('style', 'position: absolute; z-index: 2; left: 0; top: 0; bottom: 0;');
30
+ div.style.backgroundColor = this.options.overlayColor;
31
+ this.minimapWrapper.appendChild(div);
32
+ return div;
33
+ }
34
+ initMinimap() {
35
+ const data = this.wavesurfer.getDecodedData();
36
+ const media = this.wavesurfer.getMediaElement();
37
+ if (!data || !media)
38
+ return;
39
+ this.miniWavesurfer = WaveSurfer.create({
40
+ ...this.options,
41
+ container: this.minimapWrapper,
42
+ minPxPerSec: 1,
43
+ fillParent: true,
44
+ media,
45
+ url: media.src,
46
+ peaks: [data.getChannelData(0)],
47
+ duration: data.duration,
48
+ });
49
+ const overlayWidth = Math.round((this.minimapWrapper.clientWidth / this.wrapper.clientWidth) * 100);
50
+ this.overlay.style.width = `${overlayWidth}%`;
51
+ this.wavesurfer.on('timeupdate', ({ currentTime }) => {
52
+ const offset = Math.max(0, Math.min((currentTime / this.wavesurfer.getDuration()) * 100 - overlayWidth / 2, 100 - overlayWidth)).toFixed(2);
53
+ this.overlay.style.left = `${offset}%`;
54
+ });
55
+ }
56
+ /** Unmount */
57
+ destroy() {
58
+ this.miniWavesurfer?.destroy();
59
+ this.minimapWrapper.remove();
60
+ super.destroy();
61
+ }
62
+ }
63
+ export default TimelinePlugin;
@@ -0,0 +1,111 @@
1
+ import { type WaveSurferOptions } from '../index.js';
2
+ import { type EnvelopePluginOptions } from './envelope.js';
3
+ import EventEmitter from '../event-emitter.js';
4
+ export type TrackId = string | number;
5
+ export type TrackOptions = {
6
+ id: TrackId;
7
+ url?: string;
8
+ peaks?: WaveSurferOptions['peaks'];
9
+ draggable?: boolean;
10
+ startPosition: number;
11
+ startCue?: number;
12
+ endCue?: number;
13
+ fadeInEnd?: number;
14
+ fadeOutStart?: number;
15
+ volume?: number;
16
+ markers?: Array<{
17
+ time: number;
18
+ label?: string;
19
+ color?: string;
20
+ }>;
21
+ intro?: {
22
+ endTime: number;
23
+ label?: string;
24
+ color?: string;
25
+ };
26
+ options?: WaveSurferOptions;
27
+ };
28
+ export type MultitrackOptions = {
29
+ container: HTMLElement;
30
+ minPxPerSec?: number;
31
+ cursorColor?: string;
32
+ cursorWidth?: number;
33
+ trackBackground?: string;
34
+ trackBorderColor?: string;
35
+ rightButtonDrag?: boolean;
36
+ envelopeOptions?: EnvelopePluginOptions;
37
+ };
38
+ export type MultitrackEvents = {
39
+ canplay: void;
40
+ 'start-position-change': {
41
+ id: TrackId;
42
+ startPosition: number;
43
+ };
44
+ 'start-cue-change': {
45
+ id: TrackId;
46
+ startCue: number;
47
+ };
48
+ 'end-cue-change': {
49
+ id: TrackId;
50
+ endCue: number;
51
+ };
52
+ 'fade-in-change': {
53
+ id: TrackId;
54
+ fadeInEnd: number;
55
+ };
56
+ 'fade-out-change': {
57
+ id: TrackId;
58
+ fadeOutStart: number;
59
+ };
60
+ 'volume-change': {
61
+ id: TrackId;
62
+ volume: number;
63
+ };
64
+ 'intro-end-change': {
65
+ id: TrackId;
66
+ endTime: number;
67
+ };
68
+ drop: {
69
+ id: TrackId;
70
+ };
71
+ };
72
+ export type MultitrackTracks = Array<TrackOptions>;
73
+ declare class MultiTrack extends EventEmitter<MultitrackEvents> {
74
+ private tracks;
75
+ private options;
76
+ private audios;
77
+ private wavesurfers;
78
+ private durations;
79
+ private currentTime;
80
+ private maxDuration;
81
+ private rendering;
82
+ private isDragging;
83
+ private frameRequest;
84
+ private timer;
85
+ private subscriptions;
86
+ private timeline;
87
+ static create(tracks: MultitrackTracks, options: MultitrackOptions): MultiTrack;
88
+ constructor(tracks: MultitrackTracks, options: MultitrackOptions);
89
+ private initDurations;
90
+ private initAudio;
91
+ private initAllAudios;
92
+ private initWavesurfer;
93
+ private initAllWavesurfers;
94
+ private initTimeline;
95
+ private updatePosition;
96
+ private setIsDragging;
97
+ private onDrag;
98
+ private onMove;
99
+ private findCurrentTracks;
100
+ private startSync;
101
+ play(): void;
102
+ pause(): void;
103
+ isPlaying(): boolean;
104
+ getCurrentTime(): number;
105
+ seekTo(time: number): void;
106
+ zoom(pxPerSec: number): void;
107
+ addTrack(track: TrackOptions): void;
108
+ destroy(): void;
109
+ setSinkId(sinkId: string): Promise<void[]>;
110
+ }
111
+ export default MultiTrack;