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.
Files changed (56) hide show
  1. package/README.md +22 -6
  2. package/dist/cypress/support/commands.d.ts +1 -0
  3. package/dist/cypress/support/commands.js +39 -0
  4. package/dist/cypress/support/e2e.d.ts +1 -0
  5. package/dist/cypress/support/e2e.js +18 -0
  6. package/dist/cypress.config.d.ts +2 -0
  7. package/dist/cypress.config.js +10 -0
  8. package/dist/decoder.d.ts +1 -1
  9. package/dist/draggable.d.ts +1 -0
  10. package/dist/draggable.js +57 -0
  11. package/dist/plugins/envelope.js +4 -25
  12. package/dist/plugins/envelope.min.js +1 -1
  13. package/dist/plugins/minimap.min.js +1 -1
  14. package/dist/plugins/multitrack.min.js +1 -1
  15. package/dist/plugins/record.d.ts +1 -1
  16. package/dist/plugins/record.js +2 -1
  17. package/dist/plugins/record.min.js +1 -1
  18. package/dist/plugins/regions.js +13 -51
  19. package/dist/plugins/regions.min.js +1 -1
  20. package/dist/plugins/timeline.min.js +1 -1
  21. package/dist/renderer.d.ts +9 -21
  22. package/dist/renderer.js +185 -132
  23. package/dist/src/base-plugin.d.ts +13 -0
  24. package/dist/src/base-plugin.js +22 -0
  25. package/dist/src/decoder.d.ts +9 -0
  26. package/dist/src/decoder.js +48 -0
  27. package/dist/src/event-emitter.d.ts +19 -0
  28. package/dist/src/event-emitter.js +45 -0
  29. package/dist/src/fetcher.d.ts +5 -0
  30. package/dist/src/fetcher.js +7 -0
  31. package/dist/src/player.d.ts +45 -0
  32. package/dist/src/player.js +114 -0
  33. package/dist/src/plugins/envelope.d.ts +71 -0
  34. package/dist/src/plugins/envelope.js +350 -0
  35. package/dist/src/plugins/minimap.d.ts +39 -0
  36. package/dist/src/plugins/minimap.js +117 -0
  37. package/dist/src/plugins/record.d.ts +26 -0
  38. package/dist/src/plugins/record.js +124 -0
  39. package/dist/src/plugins/regions.d.ts +93 -0
  40. package/dist/src/plugins/regions.js +395 -0
  41. package/dist/src/plugins/spectrogram-fft.d.ts +9 -0
  42. package/dist/src/plugins/spectrogram-fft.js +150 -0
  43. package/dist/src/plugins/spectrogram.d.ts +69 -0
  44. package/dist/src/plugins/spectrogram.js +340 -0
  45. package/dist/src/plugins/timeline.d.ts +45 -0
  46. package/dist/src/plugins/timeline.js +162 -0
  47. package/dist/src/renderer.d.ts +41 -0
  48. package/dist/src/renderer.js +420 -0
  49. package/dist/src/timer.d.ts +11 -0
  50. package/dist/src/timer.js +19 -0
  51. package/dist/src/wavesurfer.d.ts +149 -0
  52. package/dist/src/wavesurfer.js +229 -0
  53. package/dist/wavesurfer.d.ts +13 -7
  54. package/dist/wavesurfer.js +3 -9
  55. package/dist/wavesurfer.min.js +1 -1
  56. 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(container, options) {
4
+ constructor(options) {
4
5
  super();
5
- this.options = {
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 = { ...options };
14
- if (typeof container === 'string') {
15
- container = document.querySelector(container);
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
- this.delay(() => this.reRender(), 100);
52
+ delay(() => this.reRender());
50
53
  });
51
54
  this.resizeObserver.observe(this.scrollContainer);
52
55
  }
53
56
  initDrag() {
54
- this.wrapper.addEventListener('mousedown', (e) => {
55
- const minDx = 5;
56
- const x = e.clientX;
57
- const move = (e) => {
58
- const diff = Math.abs(e.clientX - x);
59
- if (diff >= minDx) {
60
- this.isDragging = true;
61
- const rect = this.wrapper.getBoundingClientRect();
62
- this.emit('drag', Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width)));
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
- delay(fn, delayMs = 10) {
161
- if (this.timeout) {
162
- clearTimeout(this.timeout);
163
- }
164
- return new Promise((resolve) => {
165
- this.timeout = setTimeout(() => {
166
- resolve(fn());
167
- }, delayMs);
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
- async renderPeaks(audioData, width, height, pixelRatio) {
171
- const barWidth = this.options.barWidth != null && !isNaN(this.options.barWidth) ? this.options.barWidth * pixelRatio : 1;
172
- const barGap = this.options.barGap != null && !isNaN(this.options.barGap)
173
- ? this.options.barGap * pixelRatio
174
- : this.options.barWidth
175
- ? barWidth / 2
176
- : 0;
177
- const barRadius = this.options.barRadius || 0;
178
- const scaleY = this.options.barHeight || 1;
179
- const leftChannel = audioData.getChannelData(0);
180
- const len = leftChannel.length;
181
- const barCount = Math.floor(width / (barWidth + barGap));
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 isMono = audioData.numberOfChannels === 1;
185
- const rightChannel = isMono ? leftChannel : audioData.getChannelData(1);
186
- const useNegative = isMono && rightChannel.some((v) => v < 0);
187
- const draw = (start, end) => {
188
- let prevX = 0;
189
- let prevLeft = 0;
190
- let prevRight = 0;
191
- const canvas = document.createElement('canvas');
192
- canvas.width = Math.round((width * (end - start)) / len);
193
- canvas.height = this.options.height;
194
- canvas.style.width = `${Math.floor(canvas.width / pixelRatio)}px`;
195
- canvas.style.height = `${this.options.height}px`;
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
- ctx.fill();
228
- ctx.closePath();
229
- // Draw a progress canvas
230
- const progressCanvas = canvas.cloneNode();
231
- this.progressWrapper.appendChild(progressCanvas);
232
- const progressCtx = progressCanvas.getContext('2d', {
233
- desynchronized: true,
234
- });
235
- if (canvas.width > 0 && canvas.height > 0) {
236
- progressCtx.drawImage(canvas, 0, 0);
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
- // Set the composition method to draw only where the waveform is drawn
239
- progressCtx.globalCompositeOperation = 'source-in';
240
- progressCtx.fillStyle = this.options.progressColor ?? '';
241
- // This rectangle acts as a mask thanks to the composition method
242
- progressCtx.fillRect(0, 0, canvas.width, canvas.height);
243
- };
244
- // Clear the canvas
245
- this.canvasWrapper.innerHTML = '';
246
- this.progressWrapper.innerHTML = '';
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
- let viewportWidth = Math.min(Renderer.MAX_CANVAS_WIDTH, clientWidth);
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
- // Draw the visible portion of the waveform
255
- draw(start, end);
256
- // Draw the rest of the waveform with a timeout for better performance
257
- const step = end - start;
258
- for (let i = end; i < len; i += step) {
259
- await this.delay(() => {
260
- draw(i, Math.min(len, i + step));
261
- });
262
- }
263
- for (let i = start - 1; i >= 0; i -= step) {
264
- await this.delay(() => {
265
- draw(Math.max(0, i - step), i);
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
- this.canvasWrapper.style.height = `${this.options.height}px`;
325
+ // Clear the canvases
326
+ this.canvasWrapper.innerHTML = '';
327
+ this.progressWrapper.innerHTML = '';
288
328
  // Render the waveform
289
- this.renderPeaks(audioData, width, height, pixelRatio);
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,5 @@
1
+ declare function fetchArrayBuffer(url: string): Promise<ArrayBuffer>;
2
+ declare const Fetcher: {
3
+ fetchArrayBuffer: typeof fetchArrayBuffer;
4
+ };
5
+ export default Fetcher;
@@ -0,0 +1,7 @@
1
+ async function fetchArrayBuffer(url) {
2
+ return fetch(url).then((response) => response.arrayBuffer());
3
+ }
4
+ const Fetcher = {
5
+ fetchArrayBuffer,
6
+ };
7
+ export default Fetcher;
@@ -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;