wavesurfer.js 7.0.0-alpha.2 → 7.0.0-alpha.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -15
- package/dist/base-plugin.d.ts +6 -4
- package/dist/base-plugin.js +19 -0
- package/dist/decoder.d.ts +2 -4
- package/dist/decoder.js +31 -0
- package/dist/event-emitter.d.ts +1 -1
- package/dist/event-emitter.js +26 -0
- package/dist/fetcher.js +6 -0
- package/dist/index.d.ts +66 -20
- package/dist/index.js +238 -0
- package/dist/player.d.ts +13 -2
- package/dist/player.js +90 -0
- package/dist/plugins/envelope.d.ts +58 -0
- package/dist/plugins/envelope.js +292 -0
- package/dist/plugins/minimap.d.ts +25 -0
- package/dist/plugins/minimap.js +63 -0
- package/dist/plugins/multitrack.d.ts +111 -0
- package/dist/plugins/multitrack.js +489 -0
- package/dist/plugins/regions.d.ts +14 -9
- package/dist/plugins/regions.js +231 -0
- package/dist/plugins/timeline.d.ts +36 -0
- package/dist/plugins/timeline.js +125 -0
- package/dist/renderer.d.ts +16 -10
- package/dist/renderer.js +239 -0
- package/dist/timer.d.ts +2 -1
- package/dist/timer.js +19 -0
- package/dist/wavesurfer.Minimap.min.js +1 -0
- package/dist/wavesurfer.Multitrack.min.js +1 -0
- package/dist/wavesurfer.Regions.min.js +1 -1
- package/dist/wavesurfer.Timeline.min.js +1 -0
- package/dist/wavesurfer.min.js +1 -1
- package/package.json +32 -20
- package/dist/player-webaudio.d.ts +0 -8
package/README.md
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
|
-
# wavesurfer.ts
|
|
1
|
+
# <img src="https://user-images.githubusercontent.com/381895/226091100-f5567a28-7736-4d37-8f84-e08f297b7e1a.png" alt="logo" height="60" valign="middle" /> wavesurfer.ts
|
|
2
2
|
|
|
3
3
|
An experimental rewrite of [wavesufer.js](https://github.com/wavesurfer-js/wavesurfer.js) to play around with new ideas.
|
|
4
4
|
|
|
5
|
+
<img alt="screenshot" src="https://user-images.githubusercontent.com/381895/225539680-fc724acd-8657-458e-a558-ff1c6758ba30.png" width="800" />
|
|
6
|
+
|
|
7
|
+
Try it out:
|
|
8
|
+
```
|
|
9
|
+
npm install --save wavesurfer.js@alpha
|
|
10
|
+
```
|
|
11
|
+
|
|
5
12
|
## Goals
|
|
6
13
|
|
|
7
14
|
* TypeScript API
|
|
@@ -17,11 +24,9 @@ Keeping backwards compatibility with earlier versions of wavesurfer.js.
|
|
|
17
24
|
|
|
18
25
|
Principles:
|
|
19
26
|
* Modular and event-driven
|
|
20
|
-
*
|
|
27
|
+
* Flexible (e.g. allow custom media elements and user-defined Web Audio graphs)
|
|
21
28
|
* Extensible with plugins
|
|
22
29
|
|
|
23
|
-

|
|
24
|
-
|
|
25
30
|
## Development
|
|
26
31
|
|
|
27
32
|
Install dev dependencies:
|
|
@@ -30,21 +35,14 @@ Install dev dependencies:
|
|
|
30
35
|
yarn
|
|
31
36
|
```
|
|
32
37
|
|
|
33
|
-
Start the TypeScript compiler in watch mode:
|
|
34
|
-
|
|
35
|
-
```
|
|
36
|
-
yarn build:dev
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
Run an HTTP server to view the examples:
|
|
38
|
+
Start the TypeScript compiler in watch mode and an HTTP server:
|
|
40
39
|
|
|
41
40
|
```
|
|
42
|
-
|
|
41
|
+
yarn start
|
|
43
42
|
```
|
|
44
43
|
|
|
45
|
-
|
|
46
|
-
There's no hot reload yet, so you'll need to reload the page manually on every change.
|
|
44
|
+
This will open http://localhost:9090/tutorial in your browser with live reload.
|
|
47
45
|
|
|
48
46
|
## Feedback
|
|
49
47
|
|
|
50
|
-
|
|
48
|
+
Your feedback is very welcome here: https://github.com/wavesurfer-js/wavesurfer.js/discussions/2684
|
package/dist/base-plugin.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import EventEmitter, { type GeneralEventTypes } from './event-emitter.js';
|
|
2
|
-
import
|
|
3
|
-
export declare class BasePlugin<EventTypes extends GeneralEventTypes> extends EventEmitter<EventTypes> {
|
|
2
|
+
import WaveSurfer, { type WaveSurferPluginParams } from './index.js';
|
|
3
|
+
export declare class BasePlugin<EventTypes extends GeneralEventTypes, Options> extends EventEmitter<EventTypes> {
|
|
4
4
|
protected wavesurfer: WaveSurfer;
|
|
5
|
-
protected
|
|
5
|
+
protected container: HTMLElement;
|
|
6
|
+
protected wrapper: HTMLElement;
|
|
6
7
|
protected subscriptions: (() => void)[];
|
|
7
|
-
|
|
8
|
+
protected options: Options;
|
|
9
|
+
constructor(params: WaveSurferPluginParams, options: Options);
|
|
8
10
|
destroy(): void;
|
|
9
11
|
}
|
|
10
12
|
export default BasePlugin;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import EventEmitter from './event-emitter.js';
|
|
2
|
+
export class BasePlugin extends EventEmitter {
|
|
3
|
+
wavesurfer;
|
|
4
|
+
container;
|
|
5
|
+
wrapper;
|
|
6
|
+
subscriptions = [];
|
|
7
|
+
options;
|
|
8
|
+
constructor(params, options) {
|
|
9
|
+
super();
|
|
10
|
+
this.wavesurfer = params.wavesurfer;
|
|
11
|
+
this.container = params.container;
|
|
12
|
+
this.wrapper = params.wrapper;
|
|
13
|
+
this.options = options;
|
|
14
|
+
}
|
|
15
|
+
destroy() {
|
|
16
|
+
this.subscriptions.forEach((unsubscribe) => unsubscribe());
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export default BasePlugin;
|
package/dist/decoder.d.ts
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
declare class Decoder {
|
|
2
2
|
audioCtx: AudioContext | null;
|
|
3
|
+
private initAudioContext;
|
|
3
4
|
constructor();
|
|
4
|
-
decode(audioData: ArrayBuffer): Promise<
|
|
5
|
-
duration: number;
|
|
6
|
-
channelData: Float32Array[];
|
|
7
|
-
}>;
|
|
5
|
+
decode(audioData: ArrayBuffer): Promise<AudioBuffer>;
|
|
8
6
|
destroy(): void;
|
|
9
7
|
}
|
|
10
8
|
export default Decoder;
|
package/dist/decoder.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
class Decoder {
|
|
2
|
+
audioCtx = null;
|
|
3
|
+
initAudioContext(sampleRate) {
|
|
4
|
+
this.audioCtx = new AudioContext({
|
|
5
|
+
latencyHint: 'playback',
|
|
6
|
+
sampleRate,
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
constructor() {
|
|
10
|
+
// Minimum sample rate supported by Web Audio API
|
|
11
|
+
const DEFAULT_SAMPLE_RATE = 3000; // Chrome, Safari
|
|
12
|
+
const FALLBACK_SAMPLE_RATE = 8000; // Firefox
|
|
13
|
+
try {
|
|
14
|
+
this.initAudioContext(DEFAULT_SAMPLE_RATE);
|
|
15
|
+
}
|
|
16
|
+
catch (e) {
|
|
17
|
+
this.initAudioContext(FALLBACK_SAMPLE_RATE);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
async decode(audioData) {
|
|
21
|
+
if (!this.audioCtx) {
|
|
22
|
+
throw new Error('AudioContext is not initialized');
|
|
23
|
+
}
|
|
24
|
+
return this.audioCtx.decodeAudioData(audioData);
|
|
25
|
+
}
|
|
26
|
+
destroy() {
|
|
27
|
+
this.audioCtx?.close();
|
|
28
|
+
this.audioCtx = null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export default Decoder;
|
package/dist/event-emitter.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ declare class EventEmitter<EventTypes extends GeneralEventTypes> {
|
|
|
6
6
|
constructor();
|
|
7
7
|
protected emit<T extends keyof EventTypes>(eventType: T, detail?: EventTypes[T]): void;
|
|
8
8
|
/** Subscribe to an event and return a function to unsubscribe */
|
|
9
|
-
on<T extends keyof EventTypes>(eventType: T, callback: (detail: EventTypes[T]) => void): () => void;
|
|
9
|
+
on<T extends keyof EventTypes>(eventType: T, callback: (detail: EventTypes[T]) => void, once?: boolean): () => void;
|
|
10
10
|
/** Subscribe to an event once and return a function to unsubscribe */
|
|
11
11
|
once<T extends keyof EventTypes>(eventType: T, callback: (detail: EventTypes[T]) => void): () => void;
|
|
12
12
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
class EventEmitter {
|
|
2
|
+
eventTarget;
|
|
3
|
+
constructor() {
|
|
4
|
+
this.eventTarget = new EventTarget();
|
|
5
|
+
}
|
|
6
|
+
emit(eventType, detail) {
|
|
7
|
+
const e = new CustomEvent(String(eventType), { detail });
|
|
8
|
+
this.eventTarget.dispatchEvent(e);
|
|
9
|
+
}
|
|
10
|
+
/** Subscribe to an event and return a function to unsubscribe */
|
|
11
|
+
on(eventType, callback, once) {
|
|
12
|
+
const handler = (e) => {
|
|
13
|
+
if (e instanceof CustomEvent) {
|
|
14
|
+
callback(e.detail);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
const eventName = String(eventType);
|
|
18
|
+
this.eventTarget.addEventListener(eventName, handler, { once });
|
|
19
|
+
return () => this.eventTarget.removeEventListener(eventName, handler);
|
|
20
|
+
}
|
|
21
|
+
/** Subscribe to an event once and return a function to unsubscribe */
|
|
22
|
+
once(eventType, callback) {
|
|
23
|
+
return this.on(eventType, callback, true);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export default EventEmitter;
|
package/dist/fetcher.js
ADDED
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
import EventEmitter, { type GeneralEventTypes } from './event-emitter.js';
|
|
2
2
|
import BasePlugin from './base-plugin.js';
|
|
3
|
-
export declare enum PlayerType {
|
|
4
|
-
WebAudio = "WebAudio",
|
|
5
|
-
MediaElement = "MediaElement"
|
|
6
|
-
}
|
|
7
3
|
export type WaveSurferOptions = {
|
|
8
4
|
/** HTML element or CSS selector */
|
|
9
5
|
container: HTMLElement | string | null;
|
|
@@ -13,6 +9,10 @@ export type WaveSurferOptions = {
|
|
|
13
9
|
waveColor?: string;
|
|
14
10
|
/** The color of the progress mask */
|
|
15
11
|
progressColor?: string;
|
|
12
|
+
/** The color of the playpack cursor */
|
|
13
|
+
cursorColor?: string;
|
|
14
|
+
/** The cursor with */
|
|
15
|
+
cursorWidth?: number;
|
|
16
16
|
/** If set, the waveform will be rendered in bars like so: ▁ ▂ ▇ ▃ ▅ ▂ */
|
|
17
17
|
barWidth?: number;
|
|
18
18
|
/** Spacing between bars in pixels */
|
|
@@ -21,37 +21,65 @@ export type WaveSurferOptions = {
|
|
|
21
21
|
barRadius?: number;
|
|
22
22
|
/** Minimum pixels per second of audio (zoom) */
|
|
23
23
|
minPxPerSec?: number;
|
|
24
|
+
/** Stretch the waveform to fill the container, true by default */
|
|
25
|
+
fillParent?: boolean;
|
|
24
26
|
/** Audio URL */
|
|
25
27
|
url?: string;
|
|
26
28
|
/** Pre-computed audio data */
|
|
27
|
-
|
|
29
|
+
peaks?: Float32Array[];
|
|
28
30
|
/** Pre-computed duration */
|
|
29
31
|
duration?: number;
|
|
30
|
-
/** Player "backend", the default is MediaElement */
|
|
31
|
-
backend?: PlayerType;
|
|
32
32
|
/** Use an existing media element instead of creating one */
|
|
33
33
|
media?: HTMLMediaElement;
|
|
34
|
+
/** Play the audio on load */
|
|
35
|
+
autoplay?: boolean;
|
|
36
|
+
/** Is the waveform clickable? */
|
|
37
|
+
interactive?: boolean;
|
|
38
|
+
/** Hide scrollbar **/
|
|
39
|
+
noScrollbar?: boolean;
|
|
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;
|
|
34
49
|
};
|
|
35
50
|
export type WaveSurferEvents = {
|
|
51
|
+
decode: {
|
|
52
|
+
duration: number;
|
|
53
|
+
};
|
|
54
|
+
canplay: {
|
|
55
|
+
duration: number;
|
|
56
|
+
};
|
|
36
57
|
ready: {
|
|
37
58
|
duration: number;
|
|
38
59
|
};
|
|
39
|
-
canplay: void;
|
|
40
60
|
play: void;
|
|
41
61
|
pause: void;
|
|
42
|
-
|
|
62
|
+
timeupdate: {
|
|
63
|
+
currentTime: number;
|
|
64
|
+
};
|
|
65
|
+
seeking: {
|
|
66
|
+
currentTime: number;
|
|
67
|
+
};
|
|
68
|
+
seekClick: {
|
|
43
69
|
currentTime: number;
|
|
44
70
|
};
|
|
45
|
-
|
|
46
|
-
|
|
71
|
+
zoom: {
|
|
72
|
+
minPxPerSec: number;
|
|
47
73
|
};
|
|
74
|
+
destroy: void;
|
|
48
75
|
};
|
|
49
76
|
export type WaveSurferPluginParams = {
|
|
50
77
|
wavesurfer: WaveSurfer;
|
|
51
|
-
|
|
78
|
+
container: HTMLElement;
|
|
79
|
+
wrapper: HTMLElement;
|
|
52
80
|
};
|
|
53
|
-
|
|
54
|
-
|
|
81
|
+
declare class WaveSurfer extends EventEmitter<WaveSurferEvents> {
|
|
82
|
+
options: WaveSurferOptions & typeof defaultOptions;
|
|
55
83
|
private fetcher;
|
|
56
84
|
private decoder;
|
|
57
85
|
private renderer;
|
|
@@ -59,8 +87,7 @@ export declare class WaveSurfer extends EventEmitter<WaveSurferEvents> {
|
|
|
59
87
|
private timer;
|
|
60
88
|
private plugins;
|
|
61
89
|
private subscriptions;
|
|
62
|
-
private
|
|
63
|
-
private duration;
|
|
90
|
+
private decodedData;
|
|
64
91
|
/** Create a new WaveSurfer instance */
|
|
65
92
|
static create(options: WaveSurferOptions): WaveSurfer;
|
|
66
93
|
/** Create a new WaveSurfer instance */
|
|
@@ -68,9 +95,8 @@ export declare class WaveSurfer extends EventEmitter<WaveSurferEvents> {
|
|
|
68
95
|
private initPlayerEvents;
|
|
69
96
|
private initRendererEvents;
|
|
70
97
|
private initTimerEvents;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
/** Load an audio file by URL */
|
|
98
|
+
private initReadyEvent;
|
|
99
|
+
/** Load an audio file by URL, with optional pre-decoded audio data */
|
|
74
100
|
load(url: string, channelData?: Float32Array[], duration?: number): Promise<void>;
|
|
75
101
|
/** Zoom in or out */
|
|
76
102
|
zoom(minPxPerSec: number): void;
|
|
@@ -86,7 +112,27 @@ export declare class WaveSurfer extends EventEmitter<WaveSurferEvents> {
|
|
|
86
112
|
getDuration(): number;
|
|
87
113
|
/** Get the current audio position in seconds */
|
|
88
114
|
getCurrentTime(): number;
|
|
115
|
+
/** Get the audio volume */
|
|
116
|
+
getVolume(): number;
|
|
117
|
+
/** Set the audio volume */
|
|
118
|
+
setVolume(volume: number): void;
|
|
119
|
+
/** Get the audio muted state */
|
|
120
|
+
getMuted(): boolean;
|
|
121
|
+
/** Mute or unmute the audio */
|
|
122
|
+
setMuted(muted: boolean): void;
|
|
123
|
+
/** Get playback rate */
|
|
124
|
+
getPlaybackRate(): number;
|
|
125
|
+
/** Set playback rate, with an optional parameter to NOT preserve the pitch if false */
|
|
126
|
+
setPlaybackRate(rate: number, preservePitch?: boolean): void;
|
|
89
127
|
/** Register and initialize a plugin */
|
|
90
|
-
registerPlugin<T extends BasePlugin<GeneralEventTypes
|
|
128
|
+
registerPlugin<T extends BasePlugin<GeneralEventTypes, Options>, Options>(CustomPlugin: new (params: WaveSurferPluginParams, options: Options) => T, options: Options): T;
|
|
129
|
+
/** Get the decoded audio data */
|
|
130
|
+
getDecodedData(): AudioBuffer | null;
|
|
131
|
+
/** Get the raw media element */
|
|
132
|
+
getMediaElement(): HTMLMediaElement | null;
|
|
133
|
+
/** Toggle if the waveform should react to clicks */
|
|
134
|
+
toggleInteractive(isInteractive: boolean): void;
|
|
135
|
+
/** Unmount wavesurfer */
|
|
136
|
+
destroy(): void;
|
|
91
137
|
}
|
|
92
138
|
export default WaveSurfer;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import Fetcher from './fetcher.js';
|
|
2
|
+
import Decoder from './decoder.js';
|
|
3
|
+
import Renderer from './renderer.js';
|
|
4
|
+
import Player from './player.js';
|
|
5
|
+
import EventEmitter from './event-emitter.js';
|
|
6
|
+
import Timer from './timer.js';
|
|
7
|
+
const defaultOptions = {
|
|
8
|
+
height: 128,
|
|
9
|
+
waveColor: '#999',
|
|
10
|
+
progressColor: '#555',
|
|
11
|
+
cursorWidth: 1,
|
|
12
|
+
minPxPerSec: 0,
|
|
13
|
+
fillParent: true,
|
|
14
|
+
interactive: true,
|
|
15
|
+
};
|
|
16
|
+
class WaveSurfer extends EventEmitter {
|
|
17
|
+
options;
|
|
18
|
+
fetcher;
|
|
19
|
+
decoder;
|
|
20
|
+
renderer;
|
|
21
|
+
player;
|
|
22
|
+
timer;
|
|
23
|
+
plugins = [];
|
|
24
|
+
subscriptions = [];
|
|
25
|
+
decodedData = null;
|
|
26
|
+
/** Create a new WaveSurfer instance */
|
|
27
|
+
static create(options) {
|
|
28
|
+
return new WaveSurfer(options);
|
|
29
|
+
}
|
|
30
|
+
/** Create a new WaveSurfer instance */
|
|
31
|
+
constructor(options) {
|
|
32
|
+
super();
|
|
33
|
+
this.options = Object.assign({}, defaultOptions, options);
|
|
34
|
+
this.fetcher = new Fetcher();
|
|
35
|
+
this.decoder = new Decoder();
|
|
36
|
+
this.timer = new Timer();
|
|
37
|
+
this.player = new Player({
|
|
38
|
+
media: this.options.media,
|
|
39
|
+
autoplay: this.options.autoplay,
|
|
40
|
+
});
|
|
41
|
+
this.renderer = new Renderer({
|
|
42
|
+
container: this.options.container,
|
|
43
|
+
height: this.options.height,
|
|
44
|
+
waveColor: this.options.waveColor,
|
|
45
|
+
progressColor: this.options.progressColor,
|
|
46
|
+
cursorColor: this.options.cursorColor,
|
|
47
|
+
cursorWidth: this.options.cursorWidth,
|
|
48
|
+
minPxPerSec: this.options.minPxPerSec,
|
|
49
|
+
fillParent: this.options.fillParent,
|
|
50
|
+
barWidth: this.options.barWidth,
|
|
51
|
+
barGap: this.options.barGap,
|
|
52
|
+
barRadius: this.options.barRadius,
|
|
53
|
+
noScrollbar: this.options.noScrollbar,
|
|
54
|
+
});
|
|
55
|
+
this.initPlayerEvents();
|
|
56
|
+
this.initRendererEvents();
|
|
57
|
+
this.initTimerEvents();
|
|
58
|
+
this.initReadyEvent();
|
|
59
|
+
const url = this.options.url || this.options.media?.src;
|
|
60
|
+
if (url) {
|
|
61
|
+
this.load(url, this.options.peaks, this.options.duration);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
initPlayerEvents() {
|
|
65
|
+
this.subscriptions.push(this.player.on('timeupdate', () => {
|
|
66
|
+
const currentTime = this.getCurrentTime();
|
|
67
|
+
this.renderer.renderProgress(currentTime / this.getDuration(), this.isPlaying());
|
|
68
|
+
this.emit('timeupdate', { currentTime });
|
|
69
|
+
}), this.player.on('play', () => {
|
|
70
|
+
this.emit('play');
|
|
71
|
+
this.timer.start();
|
|
72
|
+
}), this.player.on('pause', () => {
|
|
73
|
+
this.emit('pause');
|
|
74
|
+
this.timer.stop();
|
|
75
|
+
}), this.player.on('canplay', () => {
|
|
76
|
+
this.emit('canplay', { duration: this.getDuration() });
|
|
77
|
+
}), this.player.on('seeking', () => {
|
|
78
|
+
this.emit('seeking', { currentTime: this.getCurrentTime() });
|
|
79
|
+
}));
|
|
80
|
+
}
|
|
81
|
+
initRendererEvents() {
|
|
82
|
+
// Seek on click
|
|
83
|
+
this.subscriptions.push(this.renderer.on('click', ({ relativeX }) => {
|
|
84
|
+
if (this.options.interactive) {
|
|
85
|
+
const time = this.getDuration() * relativeX;
|
|
86
|
+
this.seekTo(time);
|
|
87
|
+
this.emit('seekClick', { currentTime: this.getCurrentTime() });
|
|
88
|
+
}
|
|
89
|
+
}));
|
|
90
|
+
}
|
|
91
|
+
initTimerEvents() {
|
|
92
|
+
// The timer fires every 16ms for a smooth progress animation
|
|
93
|
+
this.subscriptions.push(this.timer.on('tick', () => {
|
|
94
|
+
const currentTime = this.getCurrentTime();
|
|
95
|
+
this.renderer.renderProgress(currentTime / this.getDuration(), true);
|
|
96
|
+
this.emit('timeupdate', { currentTime });
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
initReadyEvent() {
|
|
100
|
+
let isDecoded = false;
|
|
101
|
+
let isPlayable = false;
|
|
102
|
+
const emitReady = () => {
|
|
103
|
+
if (isDecoded && isPlayable) {
|
|
104
|
+
this.emit('ready', { duration: this.getDuration() });
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
this.subscriptions.push(this.on('decode', () => {
|
|
108
|
+
isDecoded = true;
|
|
109
|
+
emitReady();
|
|
110
|
+
}), this.on('canplay', () => {
|
|
111
|
+
isPlayable = true;
|
|
112
|
+
emitReady();
|
|
113
|
+
}), this.player.on('waiting', () => {
|
|
114
|
+
isPlayable = false;
|
|
115
|
+
isDecoded = false;
|
|
116
|
+
}));
|
|
117
|
+
}
|
|
118
|
+
/** Load an audio file by URL, with optional pre-decoded audio data */
|
|
119
|
+
async load(url, channelData, duration) {
|
|
120
|
+
this.player.loadUrl(url);
|
|
121
|
+
// Fetch and decode the audio of no pre-computed audio data is provided
|
|
122
|
+
if (channelData == null) {
|
|
123
|
+
const audio = await this.fetcher.load(url);
|
|
124
|
+
const data = await this.decoder.decode(audio);
|
|
125
|
+
this.decodedData = data;
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
if (!duration) {
|
|
129
|
+
duration =
|
|
130
|
+
(await new Promise((resolve) => {
|
|
131
|
+
this.player.on('canplay', () => resolve(this.getDuration()), {
|
|
132
|
+
once: true,
|
|
133
|
+
});
|
|
134
|
+
})) || 0;
|
|
135
|
+
}
|
|
136
|
+
this.decodedData = {
|
|
137
|
+
duration,
|
|
138
|
+
numberOfChannels: channelData.length,
|
|
139
|
+
sampleRate: channelData[0].length / duration,
|
|
140
|
+
getChannelData: (i) => channelData[i],
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
this.renderer.render(this.decodedData);
|
|
144
|
+
this.emit('decode', { duration: this.decodedData.duration });
|
|
145
|
+
}
|
|
146
|
+
/** Zoom in or out */
|
|
147
|
+
zoom(minPxPerSec) {
|
|
148
|
+
if (!this.decodedData) {
|
|
149
|
+
throw new Error('No audio loaded');
|
|
150
|
+
}
|
|
151
|
+
this.renderer.zoom(this.decodedData, minPxPerSec);
|
|
152
|
+
this.emit('zoom', { minPxPerSec });
|
|
153
|
+
}
|
|
154
|
+
/** Start playing the audio */
|
|
155
|
+
play() {
|
|
156
|
+
this.player.play();
|
|
157
|
+
}
|
|
158
|
+
/** Pause the audio */
|
|
159
|
+
pause() {
|
|
160
|
+
this.player.pause();
|
|
161
|
+
}
|
|
162
|
+
/** Skip to a time position in seconds */
|
|
163
|
+
seekTo(time) {
|
|
164
|
+
this.player.seekTo(time);
|
|
165
|
+
}
|
|
166
|
+
/** Check if the audio is playing */
|
|
167
|
+
isPlaying() {
|
|
168
|
+
return this.player.isPlaying();
|
|
169
|
+
}
|
|
170
|
+
/** Get the duration of the audio in seconds */
|
|
171
|
+
getDuration() {
|
|
172
|
+
return this.player.getDuration() || this.decodedData?.duration || 0;
|
|
173
|
+
}
|
|
174
|
+
/** Get the current audio position in seconds */
|
|
175
|
+
getCurrentTime() {
|
|
176
|
+
return this.player.getCurrentTime();
|
|
177
|
+
}
|
|
178
|
+
/** Get the audio volume */
|
|
179
|
+
getVolume() {
|
|
180
|
+
return this.player.getVolume();
|
|
181
|
+
}
|
|
182
|
+
/** Set the audio volume */
|
|
183
|
+
setVolume(volume) {
|
|
184
|
+
this.player.setVolume(volume);
|
|
185
|
+
}
|
|
186
|
+
/** Get the audio muted state */
|
|
187
|
+
getMuted() {
|
|
188
|
+
return this.player.getMuted();
|
|
189
|
+
}
|
|
190
|
+
/** Mute or unmute the audio */
|
|
191
|
+
setMuted(muted) {
|
|
192
|
+
this.player.setMuted(muted);
|
|
193
|
+
}
|
|
194
|
+
/** Get playback rate */
|
|
195
|
+
getPlaybackRate() {
|
|
196
|
+
return this.player.getPlaybackRate();
|
|
197
|
+
}
|
|
198
|
+
/** Set playback rate, with an optional parameter to NOT preserve the pitch if false */
|
|
199
|
+
setPlaybackRate(rate, preservePitch) {
|
|
200
|
+
this.player.setPlaybackRate(rate, preservePitch);
|
|
201
|
+
}
|
|
202
|
+
/** Register and initialize a plugin */
|
|
203
|
+
registerPlugin(CustomPlugin, options) {
|
|
204
|
+
const plugin = new CustomPlugin({
|
|
205
|
+
wavesurfer: this,
|
|
206
|
+
container: this.renderer.getContainer(),
|
|
207
|
+
wrapper: this.renderer.getWrapper(),
|
|
208
|
+
}, options);
|
|
209
|
+
this.plugins.push(plugin);
|
|
210
|
+
plugin.once('destroy', () => {
|
|
211
|
+
this.plugins = this.plugins.filter((p) => p !== plugin);
|
|
212
|
+
});
|
|
213
|
+
return plugin;
|
|
214
|
+
}
|
|
215
|
+
/** Get the decoded audio data */
|
|
216
|
+
getDecodedData() {
|
|
217
|
+
return this.decodedData;
|
|
218
|
+
}
|
|
219
|
+
/** Get the raw media element */
|
|
220
|
+
getMediaElement() {
|
|
221
|
+
return this.player.getMediaElement();
|
|
222
|
+
}
|
|
223
|
+
/** Toggle if the waveform should react to clicks */
|
|
224
|
+
toggleInteractive(isInteractive) {
|
|
225
|
+
this.options.interactive = isInteractive;
|
|
226
|
+
}
|
|
227
|
+
/** Unmount wavesurfer */
|
|
228
|
+
destroy() {
|
|
229
|
+
this.emit('destroy');
|
|
230
|
+
this.subscriptions.forEach((unsubscribe) => unsubscribe());
|
|
231
|
+
this.plugins.forEach((plugin) => plugin.destroy());
|
|
232
|
+
this.timer.destroy();
|
|
233
|
+
this.player.destroy();
|
|
234
|
+
this.decoder.destroy();
|
|
235
|
+
this.renderer.destroy();
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
export default WaveSurfer;
|
package/dist/player.d.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
declare class Player {
|
|
2
2
|
protected media: HTMLMediaElement;
|
|
3
3
|
private isExternalMedia;
|
|
4
|
-
|
|
4
|
+
private hasPlayedOnce;
|
|
5
|
+
private subscriptions;
|
|
6
|
+
constructor({ media, autoplay }: {
|
|
5
7
|
media?: HTMLMediaElement;
|
|
8
|
+
autoplay?: boolean;
|
|
6
9
|
});
|
|
7
|
-
on(event: keyof HTMLMediaElementEventMap, callback: () => void): () => void;
|
|
10
|
+
on(event: keyof HTMLMediaElementEventMap, callback: () => void, options?: AddEventListenerOptions): () => void;
|
|
8
11
|
destroy(): void;
|
|
9
12
|
loadUrl(src: string): void;
|
|
10
13
|
getCurrentTime(): number;
|
|
@@ -12,5 +15,13 @@ declare class Player {
|
|
|
12
15
|
pause(): void;
|
|
13
16
|
isPlaying(): boolean;
|
|
14
17
|
seekTo(time: number): void;
|
|
18
|
+
getDuration(): number;
|
|
19
|
+
getVolume(): number;
|
|
20
|
+
setVolume(volume: number): void;
|
|
21
|
+
getMuted(): boolean;
|
|
22
|
+
setMuted(muted: boolean): void;
|
|
23
|
+
getPlaybackRate(): number;
|
|
24
|
+
setPlaybackRate(rate: number, preservePitch?: boolean): void;
|
|
25
|
+
getMediaElement(): HTMLMediaElement;
|
|
15
26
|
}
|
|
16
27
|
export default Player;
|
package/dist/player.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
class Player {
|
|
2
|
+
media;
|
|
3
|
+
isExternalMedia = false;
|
|
4
|
+
hasPlayedOnce = false;
|
|
5
|
+
subscriptions = [];
|
|
6
|
+
constructor({ media, autoplay }) {
|
|
7
|
+
if (media) {
|
|
8
|
+
this.media = media;
|
|
9
|
+
this.isExternalMedia = true;
|
|
10
|
+
}
|
|
11
|
+
else {
|
|
12
|
+
this.media = document.createElement('audio');
|
|
13
|
+
}
|
|
14
|
+
this.subscriptions.push(
|
|
15
|
+
// Track the first play() call
|
|
16
|
+
this.on('play', () => {
|
|
17
|
+
this.hasPlayedOnce = true;
|
|
18
|
+
}, { once: true }));
|
|
19
|
+
// Autoplay
|
|
20
|
+
if (autoplay) {
|
|
21
|
+
this.media.autoplay = true;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
on(event, callback, options) {
|
|
25
|
+
this.media.addEventListener(event, callback, options);
|
|
26
|
+
return () => this.media.removeEventListener(event, callback);
|
|
27
|
+
}
|
|
28
|
+
destroy() {
|
|
29
|
+
this.subscriptions.forEach((unsubscribe) => {
|
|
30
|
+
unsubscribe();
|
|
31
|
+
});
|
|
32
|
+
this.media.pause();
|
|
33
|
+
if (!this.isExternalMedia) {
|
|
34
|
+
this.media.remove();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
loadUrl(src) {
|
|
38
|
+
this.media.src = src;
|
|
39
|
+
}
|
|
40
|
+
getCurrentTime() {
|
|
41
|
+
return this.media.currentTime;
|
|
42
|
+
}
|
|
43
|
+
play() {
|
|
44
|
+
this.media.play();
|
|
45
|
+
}
|
|
46
|
+
pause() {
|
|
47
|
+
this.media.pause();
|
|
48
|
+
}
|
|
49
|
+
isPlaying() {
|
|
50
|
+
return this.media.currentTime > 0 && !this.media.paused && !this.media.ended;
|
|
51
|
+
}
|
|
52
|
+
seekTo(time) {
|
|
53
|
+
// iOS Safari requires a play() call before seeking
|
|
54
|
+
if (!this.hasPlayedOnce && navigator.userAgent.match(/(iPad|iPhone|iPod)/g)) {
|
|
55
|
+
this.media.play()?.then?.(() => {
|
|
56
|
+
setTimeout(() => this.media.pause(), 10);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
this.media.currentTime = time;
|
|
60
|
+
}
|
|
61
|
+
getDuration() {
|
|
62
|
+
return this.media.duration;
|
|
63
|
+
}
|
|
64
|
+
getVolume() {
|
|
65
|
+
return this.media.volume;
|
|
66
|
+
}
|
|
67
|
+
setVolume(volume) {
|
|
68
|
+
this.media.volume = volume;
|
|
69
|
+
}
|
|
70
|
+
getMuted() {
|
|
71
|
+
return this.media.muted;
|
|
72
|
+
}
|
|
73
|
+
setMuted(muted) {
|
|
74
|
+
this.media.muted = muted;
|
|
75
|
+
}
|
|
76
|
+
getPlaybackRate() {
|
|
77
|
+
return this.media.playbackRate;
|
|
78
|
+
}
|
|
79
|
+
setPlaybackRate(rate, preservePitch) {
|
|
80
|
+
// preservePitch is true by default in most browsers
|
|
81
|
+
if (preservePitch != null) {
|
|
82
|
+
this.media.preservesPitch = preservePitch;
|
|
83
|
+
}
|
|
84
|
+
this.media.playbackRate = rate;
|
|
85
|
+
}
|
|
86
|
+
getMediaElement() {
|
|
87
|
+
return this.media;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
export default Player;
|