wavesurfer.js 7.0.0-alpha.19 → 7.0.0-alpha.20

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