wavesurfer.js 7.0.0-beta.4 → 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 +22 -6
- 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/multitrack.min.js +1 -1
- package/dist/plugins/record.d.ts +1 -1
- package/dist/plugins/record.js +2 -1
- package/dist/plugins/record.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 +9 -21
- package/dist/renderer.js +185 -132
- 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 +13 -7
- package/dist/wavesurfer.js +3 -9
- package/dist/wavesurfer.min.js +1 -1
- package/package.json +4 -1
package/dist/renderer.js
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
|
+
import { makeDraggable } from './draggable.js';
|
|
1
2
|
import EventEmitter from './event-emitter.js';
|
|
2
3
|
class Renderer extends EventEmitter {
|
|
3
|
-
constructor(
|
|
4
|
+
constructor(options) {
|
|
4
5
|
super();
|
|
5
|
-
this.
|
|
6
|
-
height: 0,
|
|
7
|
-
};
|
|
8
|
-
this.timeout = null;
|
|
6
|
+
this.timeouts = [];
|
|
9
7
|
this.isScrolling = false;
|
|
10
8
|
this.audioData = null;
|
|
11
9
|
this.resizeObserver = null;
|
|
12
10
|
this.isDragging = false;
|
|
13
|
-
this.options =
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
this.options = options;
|
|
12
|
+
let container;
|
|
13
|
+
if (typeof options.container === 'string') {
|
|
14
|
+
container = document.querySelector(options.container);
|
|
15
|
+
}
|
|
16
|
+
else if (options.container instanceof HTMLElement) {
|
|
17
|
+
container = options.container;
|
|
16
18
|
}
|
|
17
19
|
if (!container) {
|
|
18
20
|
throw new Error('Container not found');
|
|
@@ -45,31 +47,22 @@ class Renderer extends EventEmitter {
|
|
|
45
47
|
this.emit('scroll', startX, endX);
|
|
46
48
|
});
|
|
47
49
|
// Re-render the waveform on container resize
|
|
50
|
+
const delay = this.createDelay(100);
|
|
48
51
|
this.resizeObserver = new ResizeObserver(() => {
|
|
49
|
-
|
|
52
|
+
delay(() => this.reRender());
|
|
50
53
|
});
|
|
51
54
|
this.resizeObserver.observe(this.scrollContainer);
|
|
52
55
|
}
|
|
53
56
|
initDrag() {
|
|
54
|
-
this.wrapper
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
};
|
|
65
|
-
const up = () => {
|
|
66
|
-
document.removeEventListener('mousemove', move);
|
|
67
|
-
document.removeEventListener('mouseup', up);
|
|
68
|
-
this.isDragging = false;
|
|
69
|
-
};
|
|
70
|
-
document.addEventListener('mousemove', move);
|
|
71
|
-
document.addEventListener('mouseup', up);
|
|
72
|
-
});
|
|
57
|
+
makeDraggable(this.wrapper,
|
|
58
|
+
// On drag
|
|
59
|
+
(_, __, x) => {
|
|
60
|
+
this.emit('drag', Math.max(0, Math.min(1, x / this.wrapper.clientWidth)));
|
|
61
|
+
},
|
|
62
|
+
// On start drag
|
|
63
|
+
() => (this.isDragging = true),
|
|
64
|
+
// On end drag
|
|
65
|
+
() => (this.isDragging = false));
|
|
73
66
|
}
|
|
74
67
|
initHtml() {
|
|
75
68
|
const div = document.createElement('div');
|
|
@@ -99,15 +92,16 @@ class Renderer extends EventEmitter {
|
|
|
99
92
|
z-index: 2;
|
|
100
93
|
}
|
|
101
94
|
:host .canvases {
|
|
95
|
+
min-height: ${this.options.height}px;
|
|
96
|
+
}
|
|
97
|
+
:host .canvases > div {
|
|
102
98
|
position: relative;
|
|
103
|
-
height: ${this.options.height}px;
|
|
104
99
|
}
|
|
105
100
|
:host canvas {
|
|
106
101
|
display: block;
|
|
107
102
|
position: absolute;
|
|
108
103
|
top: 0;
|
|
109
104
|
image-rendering: pixelated;
|
|
110
|
-
height: ${this.options.height}px;
|
|
111
105
|
}
|
|
112
106
|
:host .progress {
|
|
113
107
|
pointer-events: none;
|
|
@@ -119,6 +113,9 @@ class Renderer extends EventEmitter {
|
|
|
119
113
|
height: 100%;
|
|
120
114
|
overflow: hidden;
|
|
121
115
|
}
|
|
116
|
+
:host .progress > div {
|
|
117
|
+
position: relative;
|
|
118
|
+
}
|
|
122
119
|
:host .cursor {
|
|
123
120
|
pointer-events: none;
|
|
124
121
|
position: absolute;
|
|
@@ -127,8 +124,6 @@ class Renderer extends EventEmitter {
|
|
|
127
124
|
left: 0;
|
|
128
125
|
height: 100%;
|
|
129
126
|
border-radius: 2px;
|
|
130
|
-
width: ${this.options.cursorWidth}px;
|
|
131
|
-
background-color: ${this.options.cursorColor || this.options.progressColor};
|
|
132
127
|
}
|
|
133
128
|
</style>
|
|
134
129
|
|
|
@@ -157,116 +152,160 @@ class Renderer extends EventEmitter {
|
|
|
157
152
|
this.container.remove();
|
|
158
153
|
this.resizeObserver?.disconnect();
|
|
159
154
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
155
|
+
createDelay(delayMs = 10) {
|
|
156
|
+
const context = {};
|
|
157
|
+
this.timeouts.push(context);
|
|
158
|
+
return (callback) => {
|
|
159
|
+
context.timeout && clearTimeout(context.timeout);
|
|
160
|
+
context.timeout = setTimeout(callback, delayMs);
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
// Convert array of color values to linear gradient
|
|
164
|
+
convertColorValues(color) {
|
|
165
|
+
if (!Array.isArray(color))
|
|
166
|
+
return color || '';
|
|
167
|
+
if (color.length < 2)
|
|
168
|
+
return color.length === 1 ? color[0] : '';
|
|
169
|
+
const canvasElement = document.createElement('canvas');
|
|
170
|
+
const ctx = canvasElement.getContext('2d');
|
|
171
|
+
const gradient = ctx.createLinearGradient(0, 0, 0, canvasElement.height);
|
|
172
|
+
const colorStopPercentage = 1 / (color.length - 1);
|
|
173
|
+
color.forEach((color, index) => {
|
|
174
|
+
const offset = index * colorStopPercentage;
|
|
175
|
+
gradient.addColorStop(offset, color);
|
|
168
176
|
});
|
|
177
|
+
return gradient;
|
|
169
178
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const
|
|
178
|
-
const
|
|
179
|
-
const
|
|
180
|
-
const
|
|
181
|
-
const
|
|
182
|
-
const barIndexScale = barCount / len;
|
|
179
|
+
renderBars(channelData, options, ctx) {
|
|
180
|
+
ctx.fillStyle = this.convertColorValues(options.waveColor);
|
|
181
|
+
// Custom rendering function
|
|
182
|
+
if (options.renderFunction) {
|
|
183
|
+
options.renderFunction(channelData, ctx);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const topChannel = channelData[0];
|
|
187
|
+
const bottomChannel = channelData[1] || channelData[0];
|
|
188
|
+
const length = topChannel.length;
|
|
189
|
+
const pixelRatio = window.devicePixelRatio || 1;
|
|
190
|
+
const { width, height } = ctx.canvas;
|
|
183
191
|
const halfHeight = height / 2;
|
|
184
|
-
const
|
|
185
|
-
const
|
|
186
|
-
const
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
canvas.style.left = `${Math.floor((start * width) / pixelRatio / len)}px`;
|
|
197
|
-
this.canvasWrapper.appendChild(canvas);
|
|
198
|
-
const ctx = canvas.getContext('2d', {
|
|
199
|
-
desynchronized: true,
|
|
200
|
-
});
|
|
201
|
-
ctx.beginPath();
|
|
202
|
-
ctx.fillStyle = this.options.waveColor ?? '';
|
|
203
|
-
// Firefox shim until 2023.04.11
|
|
204
|
-
if (!ctx.roundRect)
|
|
205
|
-
ctx.roundRect = ctx.fillRect;
|
|
206
|
-
for (let i = start; i < end; i++) {
|
|
207
|
-
const barIndex = Math.round((i - start) * barIndexScale);
|
|
208
|
-
if (barIndex > prevX) {
|
|
209
|
-
const leftBarHeight = Math.round(prevLeft * halfHeight * scaleY);
|
|
210
|
-
const rightBarHeight = Math.round(prevRight * halfHeight * scaleY);
|
|
211
|
-
ctx.roundRect(prevX * (barWidth + barGap), halfHeight - leftBarHeight, barWidth, leftBarHeight + (rightBarHeight || 1), barRadius);
|
|
212
|
-
prevX = barIndex;
|
|
213
|
-
prevLeft = 0;
|
|
214
|
-
prevRight = 0;
|
|
215
|
-
}
|
|
216
|
-
const leftValue = useNegative ? leftChannel[i] : Math.abs(leftChannel[i]);
|
|
217
|
-
const rightValue = useNegative ? rightChannel[i] : Math.abs(rightChannel[i]);
|
|
218
|
-
if (leftValue > prevLeft) {
|
|
219
|
-
prevLeft = leftValue;
|
|
220
|
-
}
|
|
221
|
-
// If stereo, both channels are drawn as max values
|
|
222
|
-
// If mono with negative values, the bottom channel will be the min negative values
|
|
223
|
-
if (useNegative ? rightValue < -prevRight : rightValue > prevRight) {
|
|
224
|
-
prevRight = rightValue < 0 ? -rightValue : rightValue;
|
|
225
|
-
}
|
|
192
|
+
const barHeight = options.barHeight || 1;
|
|
193
|
+
const barWidth = options.barWidth ? options.barWidth * pixelRatio : 1;
|
|
194
|
+
const barGap = options.barGap ? options.barGap * pixelRatio : options.barWidth ? barWidth / 2 : 0;
|
|
195
|
+
const barRadius = options.barRadius || 0;
|
|
196
|
+
const barIndexScale = width / (barWidth + barGap) / length;
|
|
197
|
+
let max = 1;
|
|
198
|
+
if (options.normalize) {
|
|
199
|
+
max = 0;
|
|
200
|
+
for (let i = 0; i < length; i++) {
|
|
201
|
+
const value = Math.abs(topChannel[i]);
|
|
202
|
+
if (value > max)
|
|
203
|
+
max = value;
|
|
226
204
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
if (
|
|
236
|
-
|
|
205
|
+
}
|
|
206
|
+
const vScale = (halfHeight / max) * barHeight;
|
|
207
|
+
ctx.beginPath();
|
|
208
|
+
let prevX = 0;
|
|
209
|
+
let maxTop = 0;
|
|
210
|
+
let maxBottom = 0;
|
|
211
|
+
for (let i = 0; i <= length; i++) {
|
|
212
|
+
const x = Math.round(i * barIndexScale);
|
|
213
|
+
if (x > prevX) {
|
|
214
|
+
const leftBarHeight = Math.round(maxTop * vScale);
|
|
215
|
+
const rightBarHeight = Math.round(maxBottom * vScale);
|
|
216
|
+
ctx.roundRect(prevX * (barWidth + barGap), halfHeight - leftBarHeight, barWidth, leftBarHeight + rightBarHeight || 1, barRadius);
|
|
217
|
+
prevX = x;
|
|
218
|
+
maxTop = 0;
|
|
219
|
+
maxBottom = 0;
|
|
237
220
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
221
|
+
const magnitudeTop = Math.abs(topChannel[i] || 0);
|
|
222
|
+
const magnitudeBottom = Math.abs(bottomChannel[i] || 0);
|
|
223
|
+
if (magnitudeTop > maxTop)
|
|
224
|
+
maxTop = magnitudeTop;
|
|
225
|
+
if (magnitudeBottom > maxBottom)
|
|
226
|
+
maxBottom = magnitudeBottom;
|
|
227
|
+
}
|
|
228
|
+
ctx.fill();
|
|
229
|
+
ctx.closePath();
|
|
230
|
+
}
|
|
231
|
+
renderSingleCanvas(channelData, options, width, start, end, canvasContainer, progressContainer) {
|
|
232
|
+
const pixelRatio = window.devicePixelRatio || 1;
|
|
233
|
+
const height = options.height || 0;
|
|
234
|
+
const canvas = document.createElement('canvas');
|
|
235
|
+
const length = channelData[0].length;
|
|
236
|
+
canvas.width = Math.round((width * (end - start)) / length);
|
|
237
|
+
canvas.height = height;
|
|
238
|
+
canvas.style.width = `${Math.floor(canvas.width / pixelRatio)}px`;
|
|
239
|
+
canvas.style.height = `${options.height}px`;
|
|
240
|
+
canvas.style.left = `${Math.floor((start * width) / pixelRatio / length)}px`;
|
|
241
|
+
canvasContainer.appendChild(canvas);
|
|
242
|
+
const ctx = canvas.getContext('2d', {
|
|
243
|
+
desynchronized: true,
|
|
244
|
+
});
|
|
245
|
+
this.renderBars(channelData.map((channel) => channel.slice(start, end)), options, ctx);
|
|
246
|
+
// Draw a progress canvas
|
|
247
|
+
const progressCanvas = canvas.cloneNode();
|
|
248
|
+
progressContainer.appendChild(progressCanvas);
|
|
249
|
+
const progressCtx = progressCanvas.getContext('2d', {
|
|
250
|
+
desynchronized: true,
|
|
251
|
+
});
|
|
252
|
+
if (canvas.width > 0 && canvas.height > 0) {
|
|
253
|
+
progressCtx.drawImage(canvas, 0, 0);
|
|
254
|
+
}
|
|
255
|
+
// Set the composition method to draw only where the waveform is drawn
|
|
256
|
+
progressCtx.globalCompositeOperation = 'source-in';
|
|
257
|
+
progressCtx.fillStyle = this.convertColorValues(options.progressColor);
|
|
258
|
+
// This rectangle acts as a mask thanks to the composition method
|
|
259
|
+
progressCtx.fillRect(0, 0, canvas.width, canvas.height);
|
|
260
|
+
}
|
|
261
|
+
renderWaveform(channelData, options, width) {
|
|
262
|
+
// A container for canvases
|
|
263
|
+
const canvasContainer = document.createElement('div');
|
|
264
|
+
canvasContainer.style.height = `${options.height}px`;
|
|
265
|
+
this.canvasWrapper.appendChild(canvasContainer);
|
|
266
|
+
// A container for progress canvases
|
|
267
|
+
const progressContainer = canvasContainer.cloneNode();
|
|
268
|
+
this.progressWrapper.appendChild(progressContainer);
|
|
247
269
|
// Determine the currently visible part of the waveform
|
|
248
270
|
const { scrollLeft, scrollWidth, clientWidth } = this.scrollContainer;
|
|
271
|
+
const len = channelData[0].length;
|
|
249
272
|
const scale = len / scrollWidth;
|
|
250
|
-
|
|
251
|
-
viewportWidth -= viewportWidth % ((barWidth + barGap) / pixelRatio);
|
|
273
|
+
const viewportWidth = Math.min(Renderer.MAX_CANVAS_WIDTH, clientWidth);
|
|
252
274
|
const start = Math.floor(Math.abs(scrollLeft) * scale);
|
|
253
275
|
const end = Math.ceil(start + viewportWidth * scale);
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
276
|
+
const viewportLen = end - start;
|
|
277
|
+
// Draw a portion of the waveform from start peak to end peak
|
|
278
|
+
const draw = (start, end) => {
|
|
279
|
+
this.renderSingleCanvas(channelData, options, width, Math.max(0, start), Math.min(end, len), canvasContainer, progressContainer);
|
|
280
|
+
};
|
|
281
|
+
// Draw the waveform in viewport chunks, each with a delay
|
|
282
|
+
const headDelay = this.createDelay();
|
|
283
|
+
const tailDelay = this.createDelay();
|
|
284
|
+
const renderHead = (fromIndex, toIndex) => {
|
|
285
|
+
draw(fromIndex, toIndex);
|
|
286
|
+
if (fromIndex > 0) {
|
|
287
|
+
headDelay(() => {
|
|
288
|
+
renderHead(fromIndex - viewportLen, toIndex - viewportLen);
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
const renderTail = (fromIndex, toIndex) => {
|
|
293
|
+
draw(fromIndex, toIndex);
|
|
294
|
+
if (toIndex < len) {
|
|
295
|
+
tailDelay(() => {
|
|
296
|
+
renderTail(fromIndex + viewportLen, toIndex + viewportLen);
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
renderHead(start, end);
|
|
301
|
+
if (end < len) {
|
|
302
|
+
renderTail(end, end + viewportLen);
|
|
267
303
|
}
|
|
268
304
|
}
|
|
269
305
|
render(audioData) {
|
|
306
|
+
// Clear previous timeouts
|
|
307
|
+
this.timeouts.forEach((context) => context.timeout && clearTimeout(context.timeout));
|
|
308
|
+
this.timeouts = [];
|
|
270
309
|
// Determine the width of the waveform
|
|
271
310
|
const pixelRatio = window.devicePixelRatio || 1;
|
|
272
311
|
const parentWidth = this.scrollContainer.clientWidth;
|
|
@@ -276,7 +315,6 @@ class Renderer extends EventEmitter {
|
|
|
276
315
|
const useParentWidth = this.options.fillParent && !this.isScrolling;
|
|
277
316
|
// Width and height of the waveform in pixels
|
|
278
317
|
const width = (useParentWidth ? parentWidth : scrollWidth) * pixelRatio;
|
|
279
|
-
const { height } = this.options;
|
|
280
318
|
// Set the width of the wrapper
|
|
281
319
|
this.wrapper.style.width = useParentWidth ? '100%' : `${scrollWidth}px`;
|
|
282
320
|
// Set additional styles
|
|
@@ -284,9 +322,24 @@ class Renderer extends EventEmitter {
|
|
|
284
322
|
this.scrollContainer.classList.toggle('noScrollbar', !!this.options.hideScrollbar);
|
|
285
323
|
this.cursor.style.backgroundColor = `${this.options.cursorColor || this.options.progressColor}`;
|
|
286
324
|
this.cursor.style.width = `${this.options.cursorWidth}px`;
|
|
287
|
-
|
|
325
|
+
// Clear the canvases
|
|
326
|
+
this.canvasWrapper.innerHTML = '';
|
|
327
|
+
this.progressWrapper.innerHTML = '';
|
|
288
328
|
// Render the waveform
|
|
289
|
-
this.
|
|
329
|
+
if (this.options.splitChannels) {
|
|
330
|
+
// Render a waveform for each channel
|
|
331
|
+
for (let i = 0; i < audioData.numberOfChannels; i++) {
|
|
332
|
+
const options = { ...this.options, ...this.options.splitChannels[i] };
|
|
333
|
+
this.renderWaveform([audioData.getChannelData(i)], options, width);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
// Render a single waveform for the first two channels (left and right)
|
|
338
|
+
const channels = [audioData.getChannelData(0)];
|
|
339
|
+
if (audioData.numberOfChannels > 1)
|
|
340
|
+
channels.push(audioData.getChannelData(1));
|
|
341
|
+
this.renderWaveform(channels, this.options, width);
|
|
342
|
+
}
|
|
290
343
|
this.audioData = audioData;
|
|
291
344
|
this.emit('render');
|
|
292
345
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import EventEmitter, { type GeneralEventTypes } from './event-emitter.js';
|
|
2
|
+
import type WaveSurfer from './wavesurfer.js';
|
|
3
|
+
export type GenericPlugin = BasePlugin<GeneralEventTypes, unknown>;
|
|
4
|
+
export declare class BasePlugin<EventTypes extends GeneralEventTypes, Options> extends EventEmitter<EventTypes> {
|
|
5
|
+
protected wavesurfer?: WaveSurfer;
|
|
6
|
+
protected subscriptions: (() => void)[];
|
|
7
|
+
protected options: Options;
|
|
8
|
+
constructor(options: Options);
|
|
9
|
+
onInit(): void;
|
|
10
|
+
init(wavesurfer: WaveSurfer): void;
|
|
11
|
+
destroy(): void;
|
|
12
|
+
}
|
|
13
|
+
export default BasePlugin;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import EventEmitter from './event-emitter.js';
|
|
2
|
+
export class BasePlugin extends EventEmitter {
|
|
3
|
+
wavesurfer;
|
|
4
|
+
subscriptions = [];
|
|
5
|
+
options;
|
|
6
|
+
constructor(options) {
|
|
7
|
+
super();
|
|
8
|
+
this.options = options;
|
|
9
|
+
}
|
|
10
|
+
onInit() {
|
|
11
|
+
// Overridden in plugin definition
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
init(wavesurfer) {
|
|
15
|
+
this.wavesurfer = wavesurfer;
|
|
16
|
+
this.onInit();
|
|
17
|
+
}
|
|
18
|
+
destroy() {
|
|
19
|
+
this.subscriptions.forEach((unsubscribe) => unsubscribe());
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export default BasePlugin;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Decode an array buffer into an audio buffer */
|
|
2
|
+
declare function decode(audioData: ArrayBuffer, sampleRate: number): Promise<AudioBuffer>;
|
|
3
|
+
/** Create an audio buffer from pre-decoded audio data */
|
|
4
|
+
declare function createBuffer(channelData: Float32Array[] | Array<number[]>, duration: number): AudioBuffer;
|
|
5
|
+
declare const Decoder: {
|
|
6
|
+
decode: typeof decode;
|
|
7
|
+
createBuffer: typeof createBuffer;
|
|
8
|
+
};
|
|
9
|
+
export default Decoder;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** Decode an array buffer into an audio buffer */
|
|
2
|
+
async function decode(audioData, sampleRate) {
|
|
3
|
+
const audioCtx = new AudioContext({ sampleRate });
|
|
4
|
+
const decode = audioCtx.decodeAudioData(audioData);
|
|
5
|
+
decode.finally(() => audioCtx.close());
|
|
6
|
+
return decode;
|
|
7
|
+
}
|
|
8
|
+
/** Normalize peaks to -1..1 */
|
|
9
|
+
function normalize(channelData) {
|
|
10
|
+
const firstChannel = channelData[0];
|
|
11
|
+
if (firstChannel.some((n) => n > 1 || n < -1)) {
|
|
12
|
+
const length = firstChannel.length;
|
|
13
|
+
let max = 0;
|
|
14
|
+
for (let i = 0; i < length; i++) {
|
|
15
|
+
const absN = Math.abs(firstChannel[i]);
|
|
16
|
+
if (absN > max)
|
|
17
|
+
max = absN;
|
|
18
|
+
}
|
|
19
|
+
for (const channel of channelData) {
|
|
20
|
+
for (let i = 0; i < length; i++) {
|
|
21
|
+
channel[i] /= max;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return channelData;
|
|
26
|
+
}
|
|
27
|
+
/** Create an audio buffer from pre-decoded audio data */
|
|
28
|
+
function createBuffer(channelData, duration) {
|
|
29
|
+
// If a single array of numbers is passed, make it an array of arrays
|
|
30
|
+
if (typeof channelData[0] === 'number')
|
|
31
|
+
channelData = [channelData];
|
|
32
|
+
// Normalize to -1..1
|
|
33
|
+
normalize(channelData);
|
|
34
|
+
return {
|
|
35
|
+
duration,
|
|
36
|
+
length: channelData[0].length,
|
|
37
|
+
sampleRate: channelData[0].length / duration,
|
|
38
|
+
numberOfChannels: channelData.length,
|
|
39
|
+
getChannelData: (i) => channelData?.[i],
|
|
40
|
+
copyFromChannel: AudioBuffer.prototype.copyFromChannel,
|
|
41
|
+
copyToChannel: AudioBuffer.prototype.copyToChannel,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const Decoder = {
|
|
45
|
+
decode,
|
|
46
|
+
createBuffer,
|
|
47
|
+
};
|
|
48
|
+
export default Decoder;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type GeneralEventTypes = {
|
|
2
|
+
[EventName: string]: any[];
|
|
3
|
+
};
|
|
4
|
+
type EventListener<EventTypes extends GeneralEventTypes, EventName extends keyof EventTypes> = (...args: EventTypes[EventName]) => void;
|
|
5
|
+
/** A simple event emitter that can be used to listen to and emit events. */
|
|
6
|
+
declare class EventEmitter<EventTypes extends GeneralEventTypes> {
|
|
7
|
+
private listeners;
|
|
8
|
+
/** Subscribe to an event. Returns an unsubscribe function. */
|
|
9
|
+
on<EventName extends keyof EventTypes>(eventName: EventName, listener: EventListener<EventTypes, EventName>): () => void;
|
|
10
|
+
/** Subscribe to an event only once */
|
|
11
|
+
once<EventName extends keyof EventTypes>(eventName: EventName, listener: EventListener<EventTypes, EventName>): () => void;
|
|
12
|
+
/** Unsubscribe from an event */
|
|
13
|
+
un<EventName extends keyof EventTypes>(eventName: EventName, listener: EventListener<EventTypes, EventName>): void;
|
|
14
|
+
/** Clear all events */
|
|
15
|
+
unAll(): void;
|
|
16
|
+
/** Emit an event */
|
|
17
|
+
protected emit<EventName extends keyof EventTypes>(eventName: EventName, ...args: EventTypes[EventName]): void;
|
|
18
|
+
}
|
|
19
|
+
export default EventEmitter;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** A simple event emitter that can be used to listen to and emit events. */
|
|
2
|
+
class EventEmitter {
|
|
3
|
+
listeners = {};
|
|
4
|
+
/** Subscribe to an event. Returns an unsubscribe function. */
|
|
5
|
+
on(eventName, listener) {
|
|
6
|
+
if (!this.listeners[eventName]) {
|
|
7
|
+
this.listeners[eventName] = new Set();
|
|
8
|
+
}
|
|
9
|
+
this.listeners[eventName].add(listener);
|
|
10
|
+
return () => this.un(eventName, listener);
|
|
11
|
+
}
|
|
12
|
+
/** Subscribe to an event only once */
|
|
13
|
+
once(eventName, listener) {
|
|
14
|
+
// The actual subscription
|
|
15
|
+
const unsubscribe = this.on(eventName, listener);
|
|
16
|
+
// Another subscription that will unsubscribe the actual subscription and itself after the first event
|
|
17
|
+
const unsubscribeOnce = this.on(eventName, () => {
|
|
18
|
+
unsubscribe();
|
|
19
|
+
unsubscribeOnce();
|
|
20
|
+
});
|
|
21
|
+
return unsubscribe;
|
|
22
|
+
}
|
|
23
|
+
/** Unsubscribe from an event */
|
|
24
|
+
un(eventName, listener) {
|
|
25
|
+
if (this.listeners[eventName]) {
|
|
26
|
+
if (listener) {
|
|
27
|
+
this.listeners[eventName].delete(listener);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
delete this.listeners[eventName];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** Clear all events */
|
|
35
|
+
unAll() {
|
|
36
|
+
this.listeners = {};
|
|
37
|
+
}
|
|
38
|
+
/** Emit an event */
|
|
39
|
+
emit(eventName, ...args) {
|
|
40
|
+
if (this.listeners[eventName]) {
|
|
41
|
+
this.listeners[eventName].forEach((listener) => listener(...args));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export default EventEmitter;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import EventEmitter, { type GeneralEventTypes } from './event-emitter.js';
|
|
2
|
+
type PlayerOptions = {
|
|
3
|
+
media?: HTMLMediaElement;
|
|
4
|
+
autoplay?: boolean;
|
|
5
|
+
playbackRate?: number;
|
|
6
|
+
};
|
|
7
|
+
declare class Player<T extends GeneralEventTypes> extends EventEmitter<T> {
|
|
8
|
+
protected media: HTMLMediaElement;
|
|
9
|
+
private isExternalMedia;
|
|
10
|
+
constructor(options: PlayerOptions);
|
|
11
|
+
protected onMediaEvent(event: keyof HTMLMediaElementEventMap, callback: () => void, options?: AddEventListenerOptions): () => void;
|
|
12
|
+
protected onceMediaEvent(event: keyof HTMLMediaElementEventMap, callback: () => void): () => void;
|
|
13
|
+
private revokeSrc;
|
|
14
|
+
protected setSrc(url: string, arrayBuffer?: ArrayBuffer): void;
|
|
15
|
+
destroy(): void;
|
|
16
|
+
/** Start playing the audio */
|
|
17
|
+
play(): Promise<void>;
|
|
18
|
+
/** Pause the audio */
|
|
19
|
+
pause(): void;
|
|
20
|
+
/** Check if the audio is playing */
|
|
21
|
+
isPlaying(): boolean;
|
|
22
|
+
/** Jumpt to a specific time in the audio (in seconds) */
|
|
23
|
+
setTime(time: number): void;
|
|
24
|
+
/** Get the duration of the audio in seconds */
|
|
25
|
+
getDuration(): number;
|
|
26
|
+
/** Get the current audio position in seconds */
|
|
27
|
+
getCurrentTime(): number;
|
|
28
|
+
/** Get the audio volume */
|
|
29
|
+
getVolume(): number;
|
|
30
|
+
/** Set the audio volume */
|
|
31
|
+
setVolume(volume: number): void;
|
|
32
|
+
/** Get the audio muted state */
|
|
33
|
+
getMuted(): boolean;
|
|
34
|
+
/** Mute or unmute the audio */
|
|
35
|
+
setMuted(muted: boolean): void;
|
|
36
|
+
/** Get the playback speed */
|
|
37
|
+
getPlaybackRate(): number;
|
|
38
|
+
/** Set the playback speed, pass an optional false to NOT preserve the pitch */
|
|
39
|
+
setPlaybackRate(rate: number, preservePitch?: boolean): void;
|
|
40
|
+
/** Get the HTML media element */
|
|
41
|
+
getMediaElement(): HTMLMediaElement;
|
|
42
|
+
/** Set a sink id to change the audio output device */
|
|
43
|
+
setSinkId(sinkId: string): Promise<void>;
|
|
44
|
+
}
|
|
45
|
+
export default Player;
|