wavesurfer.js 7.0.0-beta.5 → 7.0.0-beta.6
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 +1 -5
- package/dist/cypress/support/commands.d.ts +1 -0
- package/dist/cypress/support/commands.js +39 -0
- package/dist/cypress/support/e2e.d.ts +1 -0
- package/dist/cypress/support/e2e.js +18 -0
- package/dist/cypress.config.d.ts +2 -0
- package/dist/cypress.config.js +10 -0
- package/dist/decoder.d.ts +1 -1
- package/dist/draggable.d.ts +1 -0
- package/dist/draggable.js +57 -0
- package/dist/plugins/envelope.js +4 -25
- package/dist/plugins/envelope.min.js +1 -1
- package/dist/plugins/minimap.min.js +1 -1
- package/dist/plugins/regions.js +13 -51
- package/dist/plugins/regions.min.js +1 -1
- package/dist/plugins/timeline.min.js +1 -1
- package/dist/renderer.d.ts +1 -0
- package/dist/renderer.js +64 -66
- package/dist/src/base-plugin.d.ts +13 -0
- package/dist/src/base-plugin.js +22 -0
- package/dist/src/decoder.d.ts +9 -0
- package/dist/src/decoder.js +48 -0
- package/dist/src/event-emitter.d.ts +19 -0
- package/dist/src/event-emitter.js +45 -0
- package/dist/src/fetcher.d.ts +5 -0
- package/dist/src/fetcher.js +7 -0
- package/dist/src/player.d.ts +45 -0
- package/dist/src/player.js +114 -0
- package/dist/src/plugins/envelope.d.ts +71 -0
- package/dist/src/plugins/envelope.js +350 -0
- package/dist/src/plugins/minimap.d.ts +39 -0
- package/dist/src/plugins/minimap.js +117 -0
- package/dist/src/plugins/record.d.ts +26 -0
- package/dist/src/plugins/record.js +124 -0
- package/dist/src/plugins/regions.d.ts +93 -0
- package/dist/src/plugins/regions.js +395 -0
- package/dist/src/plugins/spectrogram-fft.d.ts +9 -0
- package/dist/src/plugins/spectrogram-fft.js +150 -0
- package/dist/src/plugins/spectrogram.d.ts +69 -0
- package/dist/src/plugins/spectrogram.js +340 -0
- package/dist/src/plugins/timeline.d.ts +45 -0
- package/dist/src/plugins/timeline.js +162 -0
- package/dist/src/renderer.d.ts +41 -0
- package/dist/src/renderer.js +420 -0
- package/dist/src/timer.d.ts +11 -0
- package/dist/src/timer.js +19 -0
- package/dist/src/wavesurfer.d.ts +149 -0
- package/dist/src/wavesurfer.js +229 -0
- package/dist/wavesurfer.d.ts +5 -1
- package/dist/wavesurfer.js +2 -8
- package/dist/wavesurfer.min.js +1 -1
- package/package.json +4 -1
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Envelope is a visual UI for controlling the audio volume and add fade-in and fade-out effects.
|
|
3
|
+
*/
|
|
4
|
+
import BasePlugin from '../base-plugin.js';
|
|
5
|
+
import EventEmitter from '../event-emitter.js';
|
|
6
|
+
const defaultOptions = {
|
|
7
|
+
fadeInStart: 0,
|
|
8
|
+
fadeOutEnd: 0,
|
|
9
|
+
fadeInEnd: 0,
|
|
10
|
+
fadeOutStart: 0,
|
|
11
|
+
lineWidth: 4,
|
|
12
|
+
lineColor: 'rgba(0, 0, 255, 0.5)',
|
|
13
|
+
dragPointSize: 10,
|
|
14
|
+
dragPointFill: 'rgba(255, 255, 255, 0.8)',
|
|
15
|
+
dragPointStroke: 'rgba(255, 255, 255, 0.8)',
|
|
16
|
+
};
|
|
17
|
+
class Polyline extends EventEmitter {
|
|
18
|
+
svg;
|
|
19
|
+
padding;
|
|
20
|
+
top = 0;
|
|
21
|
+
constructor(options, wrapper) {
|
|
22
|
+
super();
|
|
23
|
+
// An padding to make the envelope fit into the SVG
|
|
24
|
+
this.padding = options.dragPointSize / 2 + 1;
|
|
25
|
+
// SVG element
|
|
26
|
+
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
27
|
+
svg.setAttribute('width', '100%');
|
|
28
|
+
svg.setAttribute('height', '100%');
|
|
29
|
+
svg.setAttribute('viewBox', '0 0 0 0');
|
|
30
|
+
svg.setAttribute('preserveAspectRatio', 'none');
|
|
31
|
+
svg.setAttribute('style', 'position: absolute; left: 0; top: 0; z-index: 4; pointer-events: none;');
|
|
32
|
+
svg.setAttribute('part', 'envelope');
|
|
33
|
+
this.svg = svg;
|
|
34
|
+
// A polyline representing the envelope
|
|
35
|
+
const polyline = document.createElementNS('http://www.w3.org/2000/svg', 'polyline');
|
|
36
|
+
polyline.setAttribute('points', '0,0 0,0 0,0 0,0');
|
|
37
|
+
polyline.setAttribute('stroke', options.lineColor);
|
|
38
|
+
polyline.setAttribute('stroke-width', options.lineWidth);
|
|
39
|
+
polyline.setAttribute('fill', 'none');
|
|
40
|
+
polyline.setAttribute('style', 'pointer-events: none;');
|
|
41
|
+
polyline.setAttribute('part', 'polyline');
|
|
42
|
+
svg.appendChild(polyline);
|
|
43
|
+
// Draggable top line
|
|
44
|
+
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
|
45
|
+
line.setAttribute('stroke', 'transparent');
|
|
46
|
+
line.setAttribute('stroke-width', (options.lineWidth * 3).toString());
|
|
47
|
+
line.setAttribute('style', 'cursor: ns-resize; pointer-events: all;');
|
|
48
|
+
line.setAttribute('part', 'line');
|
|
49
|
+
svg.appendChild(line);
|
|
50
|
+
[0, 1].forEach(() => {
|
|
51
|
+
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
|
52
|
+
circle.setAttribute('r', (options.dragPointSize / 2).toString());
|
|
53
|
+
circle.setAttribute('fill', options.dragPointFill);
|
|
54
|
+
circle.setAttribute('stroke', options.dragPointStroke || options.dragPointFill);
|
|
55
|
+
circle.setAttribute('stroke-width', '2');
|
|
56
|
+
circle.setAttribute('style', 'cursor: ew-resize; pointer-events: all;');
|
|
57
|
+
circle.setAttribute('part', 'circle');
|
|
58
|
+
svg.appendChild(circle);
|
|
59
|
+
});
|
|
60
|
+
wrapper.appendChild(svg);
|
|
61
|
+
// Init dtagging
|
|
62
|
+
{
|
|
63
|
+
// On top line drag
|
|
64
|
+
const onDragY = (dy) => {
|
|
65
|
+
const newTop = this.top + dy;
|
|
66
|
+
const { height } = svg.viewBox.baseVal;
|
|
67
|
+
if (newTop < -0.5 || newTop > height)
|
|
68
|
+
return;
|
|
69
|
+
const relativeY = Math.min(1, Math.max(0, (height - newTop) / height));
|
|
70
|
+
this.emit('line-move', relativeY);
|
|
71
|
+
};
|
|
72
|
+
// On points drag
|
|
73
|
+
const onDragX = (index, dx) => {
|
|
74
|
+
const point = polyline.points.getItem(index);
|
|
75
|
+
const newX = point.x + dx;
|
|
76
|
+
const { width } = svg.viewBox.baseVal;
|
|
77
|
+
this.emit('point-move', index, newX / width);
|
|
78
|
+
};
|
|
79
|
+
// Draggable top line of the polyline
|
|
80
|
+
this.makeDraggable(line, (_, dy) => onDragY(dy));
|
|
81
|
+
// Make each point draggable
|
|
82
|
+
const draggables = this.svg.querySelectorAll('circle');
|
|
83
|
+
Array.from(draggables).forEach((draggable, index) => {
|
|
84
|
+
this.makeDraggable(draggable, (dx) => onDragX(index + 1, dx));
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
makeDraggable(draggable, onDrag) {
|
|
89
|
+
draggable.addEventListener('click', (e) => {
|
|
90
|
+
e.preventDefault();
|
|
91
|
+
e.stopPropagation();
|
|
92
|
+
});
|
|
93
|
+
draggable.addEventListener('mousedown', (e) => {
|
|
94
|
+
e.preventDefault();
|
|
95
|
+
e.stopPropagation();
|
|
96
|
+
let x = e.clientX;
|
|
97
|
+
let y = e.clientY;
|
|
98
|
+
const move = (e) => {
|
|
99
|
+
const dx = e.clientX - x;
|
|
100
|
+
const dy = e.clientY - y;
|
|
101
|
+
x = e.clientX;
|
|
102
|
+
y = e.clientY;
|
|
103
|
+
onDrag(dx, dy);
|
|
104
|
+
};
|
|
105
|
+
const up = () => {
|
|
106
|
+
document.removeEventListener('mousemove', move);
|
|
107
|
+
document.removeEventListener('mouseup', up);
|
|
108
|
+
};
|
|
109
|
+
document.addEventListener('mousemove', move);
|
|
110
|
+
document.addEventListener('mouseup', up);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
update({ x1, x2, x3, x4, y }) {
|
|
114
|
+
const width = this.svg.clientWidth;
|
|
115
|
+
const height = this.svg.clientHeight;
|
|
116
|
+
this.top = height - y * height;
|
|
117
|
+
const paddedTop = Math.max(this.padding, Math.min(this.top, height - this.padding));
|
|
118
|
+
this.svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
|
|
119
|
+
const polyline = this.svg.querySelector('polyline');
|
|
120
|
+
const { points } = polyline;
|
|
121
|
+
points.getItem(0).x = x1 * width;
|
|
122
|
+
points.getItem(0).y = height;
|
|
123
|
+
points.getItem(1).x = x2 * width;
|
|
124
|
+
points.getItem(1).y = paddedTop;
|
|
125
|
+
points.getItem(2).x = x3 * width;
|
|
126
|
+
points.getItem(2).y = paddedTop;
|
|
127
|
+
points.getItem(3).x = x4 * width;
|
|
128
|
+
points.getItem(3).y = height;
|
|
129
|
+
const line = this.svg.querySelector('line');
|
|
130
|
+
line.setAttribute('x1', points.getItem(1).x.toString());
|
|
131
|
+
line.setAttribute('x2', points.getItem(2).x.toString());
|
|
132
|
+
line.setAttribute('y1', paddedTop.toString());
|
|
133
|
+
line.setAttribute('y2', paddedTop.toString());
|
|
134
|
+
const circles = this.svg.querySelectorAll('circle');
|
|
135
|
+
Array.from(circles).forEach((circle, i) => {
|
|
136
|
+
const point = points.getItem(i + 1);
|
|
137
|
+
circle.setAttribute('cx', point.x.toString());
|
|
138
|
+
circle.setAttribute('cy', point.y.toString());
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
destroy() {
|
|
142
|
+
this.svg.remove();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
class EnvelopePlugin extends BasePlugin {
|
|
146
|
+
options;
|
|
147
|
+
polyline = null;
|
|
148
|
+
audioContext = null;
|
|
149
|
+
gainNode = null;
|
|
150
|
+
volume = 1;
|
|
151
|
+
isFadingIn = false;
|
|
152
|
+
isFadingOut = false;
|
|
153
|
+
// Adjust the exponent to change the curve of the volume control
|
|
154
|
+
naturalVolumeExponent = 1.5;
|
|
155
|
+
constructor(options) {
|
|
156
|
+
super(options);
|
|
157
|
+
this.options = Object.assign({}, defaultOptions, options);
|
|
158
|
+
this.options.lineColor = this.options.lineColor || defaultOptions.lineColor;
|
|
159
|
+
this.options.dragPointFill = this.options.dragPointFill || defaultOptions.dragPointFill;
|
|
160
|
+
this.options.dragPointStroke = this.options.dragPointStroke || defaultOptions.dragPointStroke;
|
|
161
|
+
this.volume = this.options.volume ?? 1;
|
|
162
|
+
}
|
|
163
|
+
static create(options) {
|
|
164
|
+
return new EnvelopePlugin(options);
|
|
165
|
+
}
|
|
166
|
+
destroy() {
|
|
167
|
+
this.polyline?.destroy();
|
|
168
|
+
super.destroy();
|
|
169
|
+
}
|
|
170
|
+
/** Called by wavesurfer, don't call manually */
|
|
171
|
+
onInit() {
|
|
172
|
+
if (!this.wavesurfer) {
|
|
173
|
+
throw Error('WaveSurfer is not initialized');
|
|
174
|
+
}
|
|
175
|
+
this.initWebAudio();
|
|
176
|
+
this.initSvg();
|
|
177
|
+
this.initFadeEffects();
|
|
178
|
+
this.subscriptions.push(this.wavesurfer.on('redraw', () => {
|
|
179
|
+
const duration = this.wavesurfer?.getDuration();
|
|
180
|
+
if (!duration)
|
|
181
|
+
return;
|
|
182
|
+
this.options.fadeInStart = this.options.fadeInStart || 0;
|
|
183
|
+
this.options.fadeOutEnd = this.options.fadeOutEnd || duration;
|
|
184
|
+
this.options.fadeInEnd = this.options.fadeInEnd || this.options.fadeInStart;
|
|
185
|
+
this.options.fadeOutStart = this.options.fadeOutStart || this.options.fadeOutEnd;
|
|
186
|
+
this.renderPolyline();
|
|
187
|
+
}));
|
|
188
|
+
}
|
|
189
|
+
initSvg() {
|
|
190
|
+
if (!this.wavesurfer)
|
|
191
|
+
return;
|
|
192
|
+
const wrapper = this.wavesurfer.getWrapper();
|
|
193
|
+
this.polyline = new Polyline(this.options, wrapper);
|
|
194
|
+
this.subscriptions.push(this.polyline.on('line-move', (relativeY) => {
|
|
195
|
+
this.setVolume(this.naturalVolume(relativeY));
|
|
196
|
+
}), this.polyline.on('point-move', (index, relativeX) => {
|
|
197
|
+
const duration = this.wavesurfer?.getDuration() || 0;
|
|
198
|
+
const newTime = relativeX * duration;
|
|
199
|
+
// Fade-in end point
|
|
200
|
+
if (index === 1) {
|
|
201
|
+
if (newTime < this.options.fadeInStart || newTime > this.options.fadeOutStart)
|
|
202
|
+
return;
|
|
203
|
+
this.options.fadeInEnd = newTime;
|
|
204
|
+
this.emit('fade-in-change', newTime);
|
|
205
|
+
}
|
|
206
|
+
else if (index === 2) {
|
|
207
|
+
// Fade-out start point
|
|
208
|
+
if (newTime > this.options.fadeOutEnd || newTime < this.options.fadeInEnd)
|
|
209
|
+
return;
|
|
210
|
+
this.options.fadeOutStart = newTime;
|
|
211
|
+
this.emit('fade-out-change', newTime);
|
|
212
|
+
}
|
|
213
|
+
this.renderPolyline();
|
|
214
|
+
}));
|
|
215
|
+
}
|
|
216
|
+
renderPolyline() {
|
|
217
|
+
if (!this.polyline || !this.wavesurfer)
|
|
218
|
+
return;
|
|
219
|
+
const duration = this.wavesurfer.getDuration();
|
|
220
|
+
if (!duration)
|
|
221
|
+
return;
|
|
222
|
+
this.polyline.update({
|
|
223
|
+
x1: this.options.fadeInStart / duration,
|
|
224
|
+
x2: this.options.fadeInEnd / duration,
|
|
225
|
+
x3: this.options.fadeOutStart / duration,
|
|
226
|
+
x4: this.options.fadeOutEnd / duration,
|
|
227
|
+
y: this.invertNaturalVolume(this.volume),
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
initWebAudio() {
|
|
231
|
+
const audio = this.wavesurfer?.getMediaElement();
|
|
232
|
+
if (!audio)
|
|
233
|
+
return null;
|
|
234
|
+
this.volume = this.options.volume ?? audio.volume;
|
|
235
|
+
// Create an AudioContext
|
|
236
|
+
const audioContext = new window.AudioContext();
|
|
237
|
+
// Create a GainNode for controlling the volume
|
|
238
|
+
this.gainNode = audioContext.createGain();
|
|
239
|
+
this.setGainValue();
|
|
240
|
+
// Create a MediaElementAudioSourceNode using the audio element
|
|
241
|
+
const source = audioContext.createMediaElementSource(audio);
|
|
242
|
+
// Connect the source to the GainNode, and the GainNode to the destination (speakers)
|
|
243
|
+
source.connect(this.gainNode);
|
|
244
|
+
this.gainNode.connect(audioContext.destination);
|
|
245
|
+
this.audioContext = audioContext;
|
|
246
|
+
}
|
|
247
|
+
invertNaturalVolume(value) {
|
|
248
|
+
const minValue = 0.0001;
|
|
249
|
+
const maxValue = 1;
|
|
250
|
+
const interpolatedValue = Math.pow((value - minValue) / (maxValue - minValue), 1 / this.naturalVolumeExponent);
|
|
251
|
+
return interpolatedValue;
|
|
252
|
+
}
|
|
253
|
+
naturalVolume(value) {
|
|
254
|
+
const minValue = 0.0001;
|
|
255
|
+
const maxValue = 1;
|
|
256
|
+
const interpolatedValue = minValue + (maxValue - minValue) * Math.pow(value, this.naturalVolumeExponent);
|
|
257
|
+
return interpolatedValue;
|
|
258
|
+
}
|
|
259
|
+
setGainValue() {
|
|
260
|
+
if (this.gainNode) {
|
|
261
|
+
this.gainNode.gain.value = this.volume;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
initFadeEffects() {
|
|
265
|
+
if (!this.audioContext || !this.wavesurfer)
|
|
266
|
+
return;
|
|
267
|
+
const unsub = this.wavesurfer.on('timeupdate', (currentTime) => {
|
|
268
|
+
if (!this.audioContext || !this.gainNode)
|
|
269
|
+
return;
|
|
270
|
+
if (!this.wavesurfer?.isPlaying())
|
|
271
|
+
return;
|
|
272
|
+
if (this.audioContext.state === 'suspended') {
|
|
273
|
+
this.audioContext.resume();
|
|
274
|
+
}
|
|
275
|
+
// Fade in
|
|
276
|
+
if (!this.isFadingIn && currentTime >= this.options.fadeInStart && currentTime <= this.options.fadeInEnd) {
|
|
277
|
+
this.isFadingIn = true;
|
|
278
|
+
// Set the initial gain (volume) to 0 (silent)
|
|
279
|
+
this.gainNode.gain.setValueAtTime(0, this.audioContext.currentTime);
|
|
280
|
+
// Set the target gain (volume) to 1 (full volume) over N seconds
|
|
281
|
+
this.gainNode.gain.linearRampToValueAtTime(this.volume, this.audioContext.currentTime + (this.options.fadeInEnd - currentTime));
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
// Fade out
|
|
285
|
+
if (!this.isFadingOut && currentTime >= this.options.fadeOutStart && currentTime <= this.options.fadeOutEnd) {
|
|
286
|
+
this.isFadingOut = true;
|
|
287
|
+
/**
|
|
288
|
+
* Set the gain at this point in time to the current volume, otherwise
|
|
289
|
+
* the audio will start fading out from the fade-in point.
|
|
290
|
+
*/
|
|
291
|
+
this.gainNode.gain.setValueAtTime(this.volume, this.audioContext.currentTime);
|
|
292
|
+
// Set the target gain (volume) to 0 (silent) over N seconds
|
|
293
|
+
this.gainNode.gain.linearRampToValueAtTime(0, this.audioContext.currentTime + (this.options.fadeOutEnd - currentTime));
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
// Reset fade in/out
|
|
297
|
+
let cancelRamp = false;
|
|
298
|
+
if (this.isFadingIn && (currentTime < this.options.fadeInStart || currentTime > this.options.fadeInEnd)) {
|
|
299
|
+
this.isFadingIn = false;
|
|
300
|
+
cancelRamp = true;
|
|
301
|
+
}
|
|
302
|
+
if (this.isFadingOut && (currentTime < this.options.fadeOutStart || currentTime >= this.options.fadeOutEnd)) {
|
|
303
|
+
this.isFadingOut = false;
|
|
304
|
+
cancelRamp = true;
|
|
305
|
+
}
|
|
306
|
+
if (cancelRamp) {
|
|
307
|
+
this.gainNode.gain.cancelScheduledValues(this.audioContext.currentTime);
|
|
308
|
+
this.setGainValue();
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
this.subscriptions.push(unsub);
|
|
312
|
+
}
|
|
313
|
+
/** Get the current audio volume */
|
|
314
|
+
getCurrentVolume() {
|
|
315
|
+
return this.gainNode ? this.gainNode.gain.value : this.volume;
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Set the fade-in start time.
|
|
319
|
+
* @param time The time (in seconds) to set the fade-in start time to
|
|
320
|
+
* @param moveFadeInEnd Whether to move the drag point to the new time (default: false)
|
|
321
|
+
*/
|
|
322
|
+
setStartTime(time, moveFadeInEnd = false) {
|
|
323
|
+
if (moveFadeInEnd) {
|
|
324
|
+
const rampLength = this.options.fadeInEnd - this.options.fadeInStart;
|
|
325
|
+
this.options.fadeInEnd = time + rampLength;
|
|
326
|
+
}
|
|
327
|
+
this.options.fadeInStart = time;
|
|
328
|
+
this.renderPolyline();
|
|
329
|
+
}
|
|
330
|
+
/** Set the fade-in end time.
|
|
331
|
+
* @param time The time (in seconds) to set the fade-in end time to
|
|
332
|
+
* @param moveFadeOutStart Whether to move the drag point to the new time (default: false)
|
|
333
|
+
*/
|
|
334
|
+
setEndTime(time, moveFadeOutStart = false) {
|
|
335
|
+
if (moveFadeOutStart) {
|
|
336
|
+
const rampLength = this.options.fadeOutEnd - this.options.fadeOutStart;
|
|
337
|
+
this.options.fadeOutStart = time - rampLength;
|
|
338
|
+
}
|
|
339
|
+
this.options.fadeOutEnd = time;
|
|
340
|
+
this.renderPolyline();
|
|
341
|
+
}
|
|
342
|
+
/** Set the volume of the audio */
|
|
343
|
+
setVolume(volume) {
|
|
344
|
+
this.volume = volume;
|
|
345
|
+
this.setGainValue();
|
|
346
|
+
this.renderPolyline();
|
|
347
|
+
this.emit('volume-change', volume);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
export default EnvelopePlugin;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimap is a tiny copy of the main waveform serving as a navigation tool.
|
|
3
|
+
*/
|
|
4
|
+
import BasePlugin from '../base-plugin.js';
|
|
5
|
+
import { type WaveSurferOptions } from '../wavesurfer.js';
|
|
6
|
+
export type MinimapPluginOptions = {
|
|
7
|
+
overlayColor?: string;
|
|
8
|
+
insertPosition?: InsertPosition;
|
|
9
|
+
} & WaveSurferOptions;
|
|
10
|
+
declare const defaultOptions: {
|
|
11
|
+
height: number;
|
|
12
|
+
overlayColor: string;
|
|
13
|
+
insertPosition: string;
|
|
14
|
+
};
|
|
15
|
+
export type MinimapPluginEvents = {
|
|
16
|
+
ready: [];
|
|
17
|
+
interaction: [];
|
|
18
|
+
};
|
|
19
|
+
declare class MinimapPlugin extends BasePlugin<MinimapPluginEvents, MinimapPluginOptions> {
|
|
20
|
+
protected options: MinimapPluginOptions & typeof defaultOptions;
|
|
21
|
+
private minimapWrapper;
|
|
22
|
+
private miniWavesurfer;
|
|
23
|
+
private overlay;
|
|
24
|
+
private container;
|
|
25
|
+
constructor(options: MinimapPluginOptions);
|
|
26
|
+
static create(options: MinimapPluginOptions): MinimapPlugin;
|
|
27
|
+
/** Called by wavesurfer, don't call manually */
|
|
28
|
+
onInit(): void;
|
|
29
|
+
private initMinimapWrapper;
|
|
30
|
+
private initOverlay;
|
|
31
|
+
private initMinimap;
|
|
32
|
+
private getOverlayWidth;
|
|
33
|
+
private onRedraw;
|
|
34
|
+
private onScroll;
|
|
35
|
+
private initWaveSurferEvents;
|
|
36
|
+
/** Unmount */
|
|
37
|
+
destroy(): void;
|
|
38
|
+
}
|
|
39
|
+
export default MinimapPlugin;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimap is a tiny copy of the main waveform serving as a navigation tool.
|
|
3
|
+
*/
|
|
4
|
+
import BasePlugin from '../base-plugin.js';
|
|
5
|
+
import WaveSurfer from '../wavesurfer.js';
|
|
6
|
+
const defaultOptions = {
|
|
7
|
+
height: 50,
|
|
8
|
+
overlayColor: 'rgba(100, 100, 100, 0.1)',
|
|
9
|
+
insertPosition: 'afterend',
|
|
10
|
+
};
|
|
11
|
+
class MinimapPlugin extends BasePlugin {
|
|
12
|
+
options;
|
|
13
|
+
minimapWrapper;
|
|
14
|
+
miniWavesurfer = null;
|
|
15
|
+
overlay;
|
|
16
|
+
container = null;
|
|
17
|
+
constructor(options) {
|
|
18
|
+
super(options);
|
|
19
|
+
this.options = Object.assign({}, defaultOptions, options);
|
|
20
|
+
this.minimapWrapper = this.initMinimapWrapper();
|
|
21
|
+
this.overlay = this.initOverlay();
|
|
22
|
+
}
|
|
23
|
+
static create(options) {
|
|
24
|
+
return new MinimapPlugin(options);
|
|
25
|
+
}
|
|
26
|
+
/** Called by wavesurfer, don't call manually */
|
|
27
|
+
onInit() {
|
|
28
|
+
if (!this.wavesurfer) {
|
|
29
|
+
throw Error('WaveSurfer is not initialized');
|
|
30
|
+
}
|
|
31
|
+
if (this.options.container) {
|
|
32
|
+
if (typeof this.options.container === 'string') {
|
|
33
|
+
this.container = document.querySelector(this.options.container);
|
|
34
|
+
}
|
|
35
|
+
else if (this.options.container instanceof HTMLElement) {
|
|
36
|
+
this.container = this.options.container;
|
|
37
|
+
}
|
|
38
|
+
this.container?.appendChild(this.minimapWrapper);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
this.container = this.wavesurfer.getWrapper().parentElement;
|
|
42
|
+
this.container?.insertAdjacentElement(this.options.insertPosition, this.minimapWrapper);
|
|
43
|
+
}
|
|
44
|
+
this.initWaveSurferEvents();
|
|
45
|
+
}
|
|
46
|
+
initMinimapWrapper() {
|
|
47
|
+
const div = document.createElement('div');
|
|
48
|
+
div.style.position = 'relative';
|
|
49
|
+
div.setAttribute('part', 'minimap');
|
|
50
|
+
return div;
|
|
51
|
+
}
|
|
52
|
+
initOverlay() {
|
|
53
|
+
const div = document.createElement('div');
|
|
54
|
+
div.setAttribute('style', 'position: absolute; z-index: 2; left: 0; top: 0; bottom: 0; transition: left 100ms ease-out; pointer-events: none;');
|
|
55
|
+
div.style.backgroundColor = this.options.overlayColor;
|
|
56
|
+
this.minimapWrapper.appendChild(div);
|
|
57
|
+
return div;
|
|
58
|
+
}
|
|
59
|
+
initMinimap() {
|
|
60
|
+
if (this.miniWavesurfer) {
|
|
61
|
+
this.miniWavesurfer.destroy();
|
|
62
|
+
this.miniWavesurfer = null;
|
|
63
|
+
}
|
|
64
|
+
if (!this.wavesurfer)
|
|
65
|
+
return;
|
|
66
|
+
const data = this.wavesurfer.getDecodedData();
|
|
67
|
+
const media = this.wavesurfer.getMediaElement();
|
|
68
|
+
if (!data || !media)
|
|
69
|
+
return;
|
|
70
|
+
this.miniWavesurfer = WaveSurfer.create({
|
|
71
|
+
...this.options,
|
|
72
|
+
container: this.minimapWrapper,
|
|
73
|
+
minPxPerSec: 0,
|
|
74
|
+
fillParent: true,
|
|
75
|
+
media,
|
|
76
|
+
peaks: [data.getChannelData(0)],
|
|
77
|
+
duration: data.duration,
|
|
78
|
+
});
|
|
79
|
+
this.subscriptions.push(this.miniWavesurfer.on('ready', () => {
|
|
80
|
+
this.emit('ready');
|
|
81
|
+
}), this.miniWavesurfer.on('interaction', () => {
|
|
82
|
+
this.emit('interaction');
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
getOverlayWidth() {
|
|
86
|
+
const waveformWidth = this.wavesurfer?.getWrapper().clientWidth || 1;
|
|
87
|
+
return Math.round((this.minimapWrapper.clientWidth / waveformWidth) * 100);
|
|
88
|
+
}
|
|
89
|
+
onRedraw() {
|
|
90
|
+
const overlayWidth = this.getOverlayWidth();
|
|
91
|
+
this.overlay.style.width = `${overlayWidth}%`;
|
|
92
|
+
}
|
|
93
|
+
onScroll(startTime) {
|
|
94
|
+
if (!this.wavesurfer)
|
|
95
|
+
return;
|
|
96
|
+
const duration = this.wavesurfer.getDuration();
|
|
97
|
+
this.overlay.style.left = `${(startTime / duration) * 100}%`;
|
|
98
|
+
}
|
|
99
|
+
initWaveSurferEvents() {
|
|
100
|
+
if (!this.wavesurfer)
|
|
101
|
+
return;
|
|
102
|
+
this.subscriptions.push(this.wavesurfer.on('decode', () => {
|
|
103
|
+
this.initMinimap();
|
|
104
|
+
}), this.wavesurfer.on('scroll', (startTime) => {
|
|
105
|
+
this.onScroll(startTime);
|
|
106
|
+
}), this.wavesurfer.on('redraw', () => {
|
|
107
|
+
this.onRedraw();
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
/** Unmount */
|
|
111
|
+
destroy() {
|
|
112
|
+
this.miniWavesurfer?.destroy();
|
|
113
|
+
this.minimapWrapper.remove();
|
|
114
|
+
super.destroy();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
export default MinimapPlugin;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Record audio from the microphone, render a waveform and download the audio.
|
|
3
|
+
*/
|
|
4
|
+
import BasePlugin from '../base-plugin.js';
|
|
5
|
+
export type RecordPluginOptions = {
|
|
6
|
+
realtimeWaveColor?: string;
|
|
7
|
+
lineWidth?: number;
|
|
8
|
+
};
|
|
9
|
+
export type RecordPluginEvents = {
|
|
10
|
+
startRecording: [];
|
|
11
|
+
stopRecording: [];
|
|
12
|
+
};
|
|
13
|
+
declare class RecordPlugin extends BasePlugin<RecordPluginEvents, RecordPluginOptions> {
|
|
14
|
+
private mediaRecorder;
|
|
15
|
+
private recordedUrl;
|
|
16
|
+
static create(options?: RecordPluginOptions): RecordPlugin;
|
|
17
|
+
private loadBlob;
|
|
18
|
+
render(stream: MediaStream): () => void;
|
|
19
|
+
private cleanUp;
|
|
20
|
+
startRecording(): Promise<void>;
|
|
21
|
+
isRecording(): boolean;
|
|
22
|
+
stopRecording(): void;
|
|
23
|
+
getRecordedUrl(): string;
|
|
24
|
+
destroy(): void;
|
|
25
|
+
}
|
|
26
|
+
export default RecordPlugin;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Record audio from the microphone, render a waveform and download the audio.
|
|
3
|
+
*/
|
|
4
|
+
import BasePlugin from '../base-plugin.js';
|
|
5
|
+
class RecordPlugin extends BasePlugin {
|
|
6
|
+
mediaRecorder = null;
|
|
7
|
+
recordedUrl = '';
|
|
8
|
+
static create(options) {
|
|
9
|
+
return new RecordPlugin(options || {});
|
|
10
|
+
}
|
|
11
|
+
loadBlob(data) {
|
|
12
|
+
const blob = new Blob(data, { type: 'audio/webm' });
|
|
13
|
+
this.recordedUrl = URL.createObjectURL(blob);
|
|
14
|
+
this.wavesurfer?.load(this.recordedUrl);
|
|
15
|
+
}
|
|
16
|
+
render(stream) {
|
|
17
|
+
if (!this.wavesurfer)
|
|
18
|
+
return () => undefined;
|
|
19
|
+
const container = this.wavesurfer.getWrapper();
|
|
20
|
+
const canvas = document.createElement('canvas');
|
|
21
|
+
canvas.width = container.clientWidth;
|
|
22
|
+
canvas.height = container.clientHeight;
|
|
23
|
+
canvas.style.zIndex = '10';
|
|
24
|
+
container.appendChild(canvas);
|
|
25
|
+
const canvasCtx = canvas.getContext('2d');
|
|
26
|
+
const audioContext = new AudioContext();
|
|
27
|
+
const source = audioContext.createMediaStreamSource(stream);
|
|
28
|
+
const analyser = audioContext.createAnalyser();
|
|
29
|
+
source.connect(analyser);
|
|
30
|
+
let animationId;
|
|
31
|
+
const drawWaveform = () => {
|
|
32
|
+
if (!canvasCtx)
|
|
33
|
+
return;
|
|
34
|
+
canvasCtx.clearRect(0, 0, canvas.width, canvas.height);
|
|
35
|
+
const bufferLength = analyser.frequencyBinCount;
|
|
36
|
+
const dataArray = new Uint8Array(bufferLength);
|
|
37
|
+
analyser.getByteTimeDomainData(dataArray);
|
|
38
|
+
canvasCtx.lineWidth = this.options.lineWidth || 2;
|
|
39
|
+
const color = this.options.realtimeWaveColor || this.wavesurfer?.options.waveColor || '';
|
|
40
|
+
canvasCtx.strokeStyle = Array.isArray(color) ? color[0] : color;
|
|
41
|
+
canvasCtx.beginPath();
|
|
42
|
+
const sliceWidth = (canvas.width * 1.0) / bufferLength;
|
|
43
|
+
let x = 0;
|
|
44
|
+
for (let i = 0; i < bufferLength; i++) {
|
|
45
|
+
const v = dataArray[i] / 128.0;
|
|
46
|
+
const y = (v * canvas.height) / 2;
|
|
47
|
+
if (i === 0) {
|
|
48
|
+
canvasCtx.moveTo(x, y);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
canvasCtx.lineTo(x, y);
|
|
52
|
+
}
|
|
53
|
+
x += sliceWidth;
|
|
54
|
+
}
|
|
55
|
+
canvasCtx.lineTo(canvas.width, canvas.height / 2);
|
|
56
|
+
canvasCtx.stroke();
|
|
57
|
+
animationId = requestAnimationFrame(drawWaveform);
|
|
58
|
+
};
|
|
59
|
+
drawWaveform();
|
|
60
|
+
return () => {
|
|
61
|
+
if (animationId) {
|
|
62
|
+
cancelAnimationFrame(animationId);
|
|
63
|
+
}
|
|
64
|
+
if (source) {
|
|
65
|
+
source.disconnect();
|
|
66
|
+
source.mediaStream.getTracks().forEach((track) => track.stop());
|
|
67
|
+
}
|
|
68
|
+
if (audioContext) {
|
|
69
|
+
audioContext.close();
|
|
70
|
+
}
|
|
71
|
+
canvas?.remove();
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
cleanUp() {
|
|
75
|
+
this.stopRecording();
|
|
76
|
+
this.wavesurfer?.empty();
|
|
77
|
+
if (this.recordedUrl) {
|
|
78
|
+
URL.revokeObjectURL(this.recordedUrl);
|
|
79
|
+
this.recordedUrl = '';
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async startRecording() {
|
|
83
|
+
this.cleanUp();
|
|
84
|
+
let stream;
|
|
85
|
+
try {
|
|
86
|
+
stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
throw new Error('Error accessing the microphone: ' + err.message);
|
|
90
|
+
}
|
|
91
|
+
const onStop = this.render(stream);
|
|
92
|
+
const mediaRecorder = new MediaRecorder(stream);
|
|
93
|
+
const recordedChunks = [];
|
|
94
|
+
mediaRecorder.addEventListener('dataavailable', (event) => {
|
|
95
|
+
if (event.data.size > 0) {
|
|
96
|
+
recordedChunks.push(event.data);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
mediaRecorder.addEventListener('stop', () => {
|
|
100
|
+
onStop();
|
|
101
|
+
this.loadBlob(recordedChunks);
|
|
102
|
+
this.emit('stopRecording');
|
|
103
|
+
});
|
|
104
|
+
mediaRecorder.start();
|
|
105
|
+
this.emit('startRecording');
|
|
106
|
+
this.mediaRecorder = mediaRecorder;
|
|
107
|
+
}
|
|
108
|
+
isRecording() {
|
|
109
|
+
return this.mediaRecorder?.state === 'recording';
|
|
110
|
+
}
|
|
111
|
+
stopRecording() {
|
|
112
|
+
if (this.isRecording()) {
|
|
113
|
+
this.mediaRecorder?.stop();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
getRecordedUrl() {
|
|
117
|
+
return this.recordedUrl;
|
|
118
|
+
}
|
|
119
|
+
destroy() {
|
|
120
|
+
super.destroy();
|
|
121
|
+
this.cleanUp();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
export default RecordPlugin;
|