wavesurfer.js 7.0.0-alpha.2 → 7.0.0-alpha.4
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 +8 -15
- package/dist/base-plugin.d.ts +1 -1
- package/dist/base-plugin.js +13 -0
- package/dist/decoder.d.ts +1 -0
- package/dist/decoder.js +51 -0
- package/dist/event-emitter.d.ts +1 -1
- package/dist/event-emitter.js +25 -0
- package/dist/fetcher.js +17 -0
- package/dist/index.d.ts +22 -10
- package/dist/index.js +188 -0
- package/dist/player-webaudio.js +32 -0
- package/dist/player.d.ts +9 -1
- package/dist/player.js +87 -0
- package/dist/plugins/regions.d.ts +3 -3
- package/dist/plugins/regions.js +193 -0
- package/dist/react/useWavesurfer.d.ts +5 -0
- package/dist/react/useWavesurfer.js +20 -0
- package/dist/renderer.d.ts +7 -5
- package/dist/renderer.js +234 -0
- package/dist/timer.d.ts +2 -1
- package/dist/timer.js +22 -0
- package/dist/wavesurfer.Regions.min.js +1 -1
- package/dist/wavesurfer.min.js +1 -1
- package/package.json +10 -4
package/README.md
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
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
|
+
|
|
5
7
|
## Goals
|
|
6
8
|
|
|
7
9
|
* TypeScript API
|
|
@@ -17,11 +19,9 @@ Keeping backwards compatibility with earlier versions of wavesurfer.js.
|
|
|
17
19
|
|
|
18
20
|
Principles:
|
|
19
21
|
* Modular and event-driven
|
|
20
|
-
*
|
|
22
|
+
* Flexible (e.g. allow custom media elements and user-defined Web Audio graphs)
|
|
21
23
|
* Extensible with plugins
|
|
22
24
|
|
|
23
|
-

|
|
24
|
-
|
|
25
25
|
## Development
|
|
26
26
|
|
|
27
27
|
Install dev dependencies:
|
|
@@ -30,21 +30,14 @@ Install dev dependencies:
|
|
|
30
30
|
yarn
|
|
31
31
|
```
|
|
32
32
|
|
|
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:
|
|
33
|
+
Start the TypeScript compiler in watch mode and an HTTP server:
|
|
40
34
|
|
|
41
35
|
```
|
|
42
|
-
|
|
36
|
+
yarn start
|
|
43
37
|
```
|
|
44
38
|
|
|
45
|
-
|
|
46
|
-
There's no hot reload yet, so you'll need to reload the page manually on every change.
|
|
39
|
+
This will open http://localhost:9090/tutorial in your browser with live reload.
|
|
47
40
|
|
|
48
41
|
## Feedback
|
|
49
42
|
|
|
50
|
-
|
|
43
|
+
Your feedback is very welcome here: https://github.com/wavesurfer-js/wavesurfer.js/discussions/2684
|
package/dist/base-plugin.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import EventEmitter, { type GeneralEventTypes } from './event-emitter.js';
|
|
|
2
2
|
import type { WaveSurfer, WaveSurferPluginParams } from './index.js';
|
|
3
3
|
export declare class BasePlugin<EventTypes extends GeneralEventTypes> extends EventEmitter<EventTypes> {
|
|
4
4
|
protected wavesurfer: WaveSurfer;
|
|
5
|
-
protected
|
|
5
|
+
protected container: HTMLElement;
|
|
6
6
|
protected subscriptions: (() => void)[];
|
|
7
7
|
constructor(params: WaveSurferPluginParams);
|
|
8
8
|
destroy(): void;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import EventEmitter from './event-emitter.js';
|
|
2
|
+
export class BasePlugin extends EventEmitter {
|
|
3
|
+
constructor(params) {
|
|
4
|
+
super();
|
|
5
|
+
this.subscriptions = [];
|
|
6
|
+
this.wavesurfer = params.wavesurfer;
|
|
7
|
+
this.container = params.container;
|
|
8
|
+
}
|
|
9
|
+
destroy() {
|
|
10
|
+
this.subscriptions.forEach((unsubscribe) => unsubscribe());
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export default BasePlugin;
|
package/dist/decoder.d.ts
CHANGED
package/dist/decoder.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
class Decoder {
|
|
11
|
+
initAudioContext(sampleRate) {
|
|
12
|
+
this.audioCtx = new AudioContext({
|
|
13
|
+
latencyHint: 'playback',
|
|
14
|
+
sampleRate,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
constructor() {
|
|
18
|
+
this.audioCtx = null;
|
|
19
|
+
// Minimum sample rate supported by Web Audio API
|
|
20
|
+
const DEFAULT_SAMPLE_RATE = 3000; // Chrome, Safari
|
|
21
|
+
const FALLBACK_SAMPLE_RATE = 8000; // Firefox
|
|
22
|
+
try {
|
|
23
|
+
this.initAudioContext(DEFAULT_SAMPLE_RATE);
|
|
24
|
+
}
|
|
25
|
+
catch (e) {
|
|
26
|
+
this.initAudioContext(FALLBACK_SAMPLE_RATE);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
decode(audioData) {
|
|
30
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
31
|
+
if (!this.audioCtx) {
|
|
32
|
+
throw new Error('AudioContext is not initialized');
|
|
33
|
+
}
|
|
34
|
+
const buffer = yield this.audioCtx.decodeAudioData(audioData);
|
|
35
|
+
const channelData = [buffer.getChannelData(0)];
|
|
36
|
+
if (buffer.numberOfChannels > 1) {
|
|
37
|
+
channelData.push(buffer.getChannelData(1));
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
duration: buffer.duration,
|
|
41
|
+
channelData,
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
destroy() {
|
|
46
|
+
var _a;
|
|
47
|
+
(_a = this.audioCtx) === null || _a === void 0 ? void 0 : _a.close();
|
|
48
|
+
this.audioCtx = null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
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,25 @@
|
|
|
1
|
+
class EventEmitter {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.eventTarget = new EventTarget();
|
|
4
|
+
}
|
|
5
|
+
emit(eventType, detail) {
|
|
6
|
+
const e = new CustomEvent(String(eventType), { detail });
|
|
7
|
+
this.eventTarget.dispatchEvent(e);
|
|
8
|
+
}
|
|
9
|
+
/** Subscribe to an event and return a function to unsubscribe */
|
|
10
|
+
on(eventType, callback, once) {
|
|
11
|
+
const handler = (e) => {
|
|
12
|
+
if (e instanceof CustomEvent) {
|
|
13
|
+
callback(e.detail);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
const eventName = String(eventType);
|
|
17
|
+
this.eventTarget.addEventListener(eventName, handler, { once });
|
|
18
|
+
return () => this.eventTarget.removeEventListener(eventName, handler);
|
|
19
|
+
}
|
|
20
|
+
/** Subscribe to an event once and return a function to unsubscribe */
|
|
21
|
+
once(eventType, callback) {
|
|
22
|
+
return this.on(eventType, callback, true);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export default EventEmitter;
|
package/dist/fetcher.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
class Fetcher {
|
|
11
|
+
load(url) {
|
|
12
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
13
|
+
return fetch(url).then((response) => response.arrayBuffer());
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export default Fetcher;
|
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;
|
|
@@ -27,16 +23,18 @@ export type WaveSurferOptions = {
|
|
|
27
23
|
channelData?: Float32Array[];
|
|
28
24
|
/** Pre-computed duration */
|
|
29
25
|
duration?: number;
|
|
30
|
-
/** Player "backend", the default is MediaElement */
|
|
31
|
-
backend?: PlayerType;
|
|
32
26
|
/** Use an existing media element instead of creating one */
|
|
33
27
|
media?: HTMLMediaElement;
|
|
28
|
+
/** Play the audio on load */
|
|
29
|
+
autoplay?: boolean;
|
|
34
30
|
};
|
|
35
31
|
export type WaveSurferEvents = {
|
|
36
|
-
|
|
32
|
+
decode: {
|
|
33
|
+
duration: number;
|
|
34
|
+
};
|
|
35
|
+
canplay: {
|
|
37
36
|
duration: number;
|
|
38
37
|
};
|
|
39
|
-
canplay: void;
|
|
40
38
|
play: void;
|
|
41
39
|
pause: void;
|
|
42
40
|
audioprocess: {
|
|
@@ -48,7 +46,7 @@ export type WaveSurferEvents = {
|
|
|
48
46
|
};
|
|
49
47
|
export type WaveSurferPluginParams = {
|
|
50
48
|
wavesurfer: WaveSurfer;
|
|
51
|
-
|
|
49
|
+
container: HTMLElement;
|
|
52
50
|
};
|
|
53
51
|
export declare class WaveSurfer extends EventEmitter<WaveSurferEvents> {
|
|
54
52
|
private options;
|
|
@@ -70,7 +68,7 @@ export declare class WaveSurfer extends EventEmitter<WaveSurferEvents> {
|
|
|
70
68
|
private initTimerEvents;
|
|
71
69
|
/** Unmount wavesurfer */
|
|
72
70
|
destroy(): void;
|
|
73
|
-
/** Load an audio file by URL */
|
|
71
|
+
/** Load an audio file by URL, with optional pre-decoded audio data */
|
|
74
72
|
load(url: string, channelData?: Float32Array[], duration?: number): Promise<void>;
|
|
75
73
|
/** Zoom in or out */
|
|
76
74
|
zoom(minPxPerSec: number): void;
|
|
@@ -86,7 +84,21 @@ export declare class WaveSurfer extends EventEmitter<WaveSurferEvents> {
|
|
|
86
84
|
getDuration(): number;
|
|
87
85
|
/** Get the current audio position in seconds */
|
|
88
86
|
getCurrentTime(): number;
|
|
87
|
+
/** Get the audio volume */
|
|
88
|
+
getVolume(): number;
|
|
89
|
+
/** Set the audio volume */
|
|
90
|
+
setVolume(volume: number): void;
|
|
91
|
+
/** Get the audio muted state */
|
|
92
|
+
getMuted(): boolean;
|
|
93
|
+
/** Mute or unmute the audio */
|
|
94
|
+
setMuted(muted: boolean): void;
|
|
95
|
+
/** Get playback rate */
|
|
96
|
+
getPlaybackRate(): number;
|
|
97
|
+
/** Set playback rate, with an optional parameter to NOT preserve the pitch if false */
|
|
98
|
+
setPlaybackRate(rate: number, preservePitch?: boolean): void;
|
|
89
99
|
/** Register and initialize a plugin */
|
|
90
100
|
registerPlugin<T extends BasePlugin<GeneralEventTypes>>(CustomPlugin: new (params: WaveSurferPluginParams) => T): T;
|
|
101
|
+
/** Get the decoded audio data */
|
|
102
|
+
getAudioData(): Float32Array[] | null;
|
|
91
103
|
}
|
|
92
104
|
export default WaveSurfer;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import Fetcher from './fetcher.js';
|
|
11
|
+
import Decoder from './decoder.js';
|
|
12
|
+
import Renderer from './renderer.js';
|
|
13
|
+
import Player from './player.js';
|
|
14
|
+
import EventEmitter from './event-emitter.js';
|
|
15
|
+
import Timer from './timer.js';
|
|
16
|
+
const defaultOptions = {
|
|
17
|
+
height: 128,
|
|
18
|
+
waveColor: '#999',
|
|
19
|
+
progressColor: '#555',
|
|
20
|
+
minPxPerSec: 0,
|
|
21
|
+
};
|
|
22
|
+
export class WaveSurfer extends EventEmitter {
|
|
23
|
+
/** Create a new WaveSurfer instance */
|
|
24
|
+
static create(options) {
|
|
25
|
+
return new WaveSurfer(options);
|
|
26
|
+
}
|
|
27
|
+
/** Create a new WaveSurfer instance */
|
|
28
|
+
constructor(options) {
|
|
29
|
+
var _a;
|
|
30
|
+
super();
|
|
31
|
+
this.plugins = [];
|
|
32
|
+
this.subscriptions = [];
|
|
33
|
+
this.channelData = null;
|
|
34
|
+
this.duration = 0;
|
|
35
|
+
this.options = Object.assign({}, defaultOptions, options);
|
|
36
|
+
this.fetcher = new Fetcher();
|
|
37
|
+
this.decoder = new Decoder();
|
|
38
|
+
this.timer = new Timer();
|
|
39
|
+
this.player = new Player({
|
|
40
|
+
media: this.options.media,
|
|
41
|
+
autoplay: this.options.autoplay,
|
|
42
|
+
});
|
|
43
|
+
this.renderer = new Renderer({
|
|
44
|
+
container: this.options.container,
|
|
45
|
+
height: this.options.height,
|
|
46
|
+
waveColor: this.options.waveColor,
|
|
47
|
+
progressColor: this.options.progressColor,
|
|
48
|
+
minPxPerSec: this.options.minPxPerSec,
|
|
49
|
+
barWidth: this.options.barWidth,
|
|
50
|
+
barGap: this.options.barGap,
|
|
51
|
+
barRadius: this.options.barRadius,
|
|
52
|
+
});
|
|
53
|
+
this.initPlayerEvents();
|
|
54
|
+
this.initRendererEvents();
|
|
55
|
+
this.initTimerEvents();
|
|
56
|
+
const url = this.options.url || ((_a = this.options.media) === null || _a === void 0 ? void 0 : _a.src);
|
|
57
|
+
if (url) {
|
|
58
|
+
this.load(url, this.options.channelData, this.options.duration);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
initPlayerEvents() {
|
|
62
|
+
this.subscriptions.push(this.player.on('timeupdate', () => {
|
|
63
|
+
const currentTime = this.getCurrentTime();
|
|
64
|
+
this.renderer.renderProgress(currentTime / this.duration, this.isPlaying());
|
|
65
|
+
this.emit('audioprocess', { currentTime });
|
|
66
|
+
}), this.player.on('play', () => {
|
|
67
|
+
this.emit('play');
|
|
68
|
+
this.timer.start();
|
|
69
|
+
}), this.player.on('pause', () => {
|
|
70
|
+
this.emit('pause');
|
|
71
|
+
this.timer.stop();
|
|
72
|
+
}), this.player.on('canplay', () => {
|
|
73
|
+
this.emit('canplay', { duration: this.getDuration() });
|
|
74
|
+
}), this.player.on('seeking', () => {
|
|
75
|
+
this.emit('seek', { time: this.getCurrentTime() });
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
initRendererEvents() {
|
|
79
|
+
// Seek on click
|
|
80
|
+
this.subscriptions.push(this.renderer.on('click', ({ relativeX }) => {
|
|
81
|
+
const time = this.getDuration() * relativeX;
|
|
82
|
+
this.seekTo(time);
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
initTimerEvents() {
|
|
86
|
+
// The timer fires every 16ms for a smooth progress animation
|
|
87
|
+
this.subscriptions.push(this.timer.on('tick', () => {
|
|
88
|
+
const currentTime = this.getCurrentTime();
|
|
89
|
+
this.renderer.renderProgress(currentTime / this.duration, true);
|
|
90
|
+
this.emit('audioprocess', { currentTime });
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
93
|
+
/** Unmount wavesurfer */
|
|
94
|
+
destroy() {
|
|
95
|
+
this.subscriptions.forEach((unsubscribe) => unsubscribe());
|
|
96
|
+
this.plugins.forEach((plugin) => plugin.destroy());
|
|
97
|
+
this.timer.destroy();
|
|
98
|
+
this.player.destroy();
|
|
99
|
+
this.decoder.destroy();
|
|
100
|
+
this.renderer.destroy();
|
|
101
|
+
}
|
|
102
|
+
/** Load an audio file by URL, with optional pre-decoded audio data */
|
|
103
|
+
load(url, channelData, duration) {
|
|
104
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
105
|
+
this.player.loadUrl(url);
|
|
106
|
+
// Fetch and decode the audio of no pre-computed audio data is provided
|
|
107
|
+
if (channelData == null || duration == null) {
|
|
108
|
+
const audio = yield this.fetcher.load(url);
|
|
109
|
+
const data = yield this.decoder.decode(audio);
|
|
110
|
+
channelData = data.channelData;
|
|
111
|
+
duration = data.duration;
|
|
112
|
+
}
|
|
113
|
+
this.channelData = channelData;
|
|
114
|
+
this.duration = duration;
|
|
115
|
+
this.renderer.render(this.channelData, this.duration);
|
|
116
|
+
this.emit('decode', { duration: this.duration });
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
/** Zoom in or out */
|
|
120
|
+
zoom(minPxPerSec) {
|
|
121
|
+
if (this.channelData == null || this.duration == null) {
|
|
122
|
+
throw new Error('No audio loaded');
|
|
123
|
+
}
|
|
124
|
+
this.renderer.zoom(this.channelData, this.duration, minPxPerSec);
|
|
125
|
+
}
|
|
126
|
+
/** Start playing the audio */
|
|
127
|
+
play() {
|
|
128
|
+
this.player.play();
|
|
129
|
+
}
|
|
130
|
+
/** Pause the audio */
|
|
131
|
+
pause() {
|
|
132
|
+
this.player.pause();
|
|
133
|
+
}
|
|
134
|
+
/** Skip to a time position in seconds */
|
|
135
|
+
seekTo(time) {
|
|
136
|
+
this.player.seekTo(time);
|
|
137
|
+
}
|
|
138
|
+
/** Check if the audio is playing */
|
|
139
|
+
isPlaying() {
|
|
140
|
+
return this.player.isPlaying();
|
|
141
|
+
}
|
|
142
|
+
/** Get the duration of the audio in seconds */
|
|
143
|
+
getDuration() {
|
|
144
|
+
return this.duration;
|
|
145
|
+
}
|
|
146
|
+
/** Get the current audio position in seconds */
|
|
147
|
+
getCurrentTime() {
|
|
148
|
+
return this.player.getCurrentTime();
|
|
149
|
+
}
|
|
150
|
+
/** Get the audio volume */
|
|
151
|
+
getVolume() {
|
|
152
|
+
return this.player.getVolume();
|
|
153
|
+
}
|
|
154
|
+
/** Set the audio volume */
|
|
155
|
+
setVolume(volume) {
|
|
156
|
+
this.player.setVolume(volume);
|
|
157
|
+
}
|
|
158
|
+
/** Get the audio muted state */
|
|
159
|
+
getMuted() {
|
|
160
|
+
return this.player.getMuted();
|
|
161
|
+
}
|
|
162
|
+
/** Mute or unmute the audio */
|
|
163
|
+
setMuted(muted) {
|
|
164
|
+
this.player.setMuted(muted);
|
|
165
|
+
}
|
|
166
|
+
/** Get playback rate */
|
|
167
|
+
getPlaybackRate() {
|
|
168
|
+
return this.player.getPlaybackRate();
|
|
169
|
+
}
|
|
170
|
+
/** Set playback rate, with an optional parameter to NOT preserve the pitch if false */
|
|
171
|
+
setPlaybackRate(rate, preservePitch) {
|
|
172
|
+
this.player.setPlaybackRate(rate, preservePitch);
|
|
173
|
+
}
|
|
174
|
+
/** Register and initialize a plugin */
|
|
175
|
+
registerPlugin(CustomPlugin) {
|
|
176
|
+
const plugin = new CustomPlugin({
|
|
177
|
+
wavesurfer: this,
|
|
178
|
+
container: this.renderer.getContainer(),
|
|
179
|
+
});
|
|
180
|
+
this.plugins.push(plugin);
|
|
181
|
+
return plugin;
|
|
182
|
+
}
|
|
183
|
+
/** Get the decoded audio data */
|
|
184
|
+
getAudioData() {
|
|
185
|
+
return this.channelData;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
export default WaveSurfer;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import Player from './player.js';
|
|
2
|
+
class WebAudioPlayer extends Player {
|
|
3
|
+
constructor() {
|
|
4
|
+
super(...arguments);
|
|
5
|
+
this.audioCtx = null;
|
|
6
|
+
this.sourceNode = null;
|
|
7
|
+
}
|
|
8
|
+
destroy() {
|
|
9
|
+
var _a, _b;
|
|
10
|
+
(_a = this.sourceNode) === null || _a === void 0 ? void 0 : _a.disconnect();
|
|
11
|
+
this.sourceNode = null;
|
|
12
|
+
(_b = this.audioCtx) === null || _b === void 0 ? void 0 : _b.close();
|
|
13
|
+
this.audioCtx = null;
|
|
14
|
+
super.destroy();
|
|
15
|
+
}
|
|
16
|
+
loadUrl(url) {
|
|
17
|
+
super.loadUrl(url);
|
|
18
|
+
if (!this.audioCtx) {
|
|
19
|
+
this.audioCtx = new AudioContext({
|
|
20
|
+
latencyHint: 'playback',
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
if (this.sourceNode) {
|
|
24
|
+
this.sourceNode.disconnect();
|
|
25
|
+
}
|
|
26
|
+
if (this.media) {
|
|
27
|
+
this.sourceNode = this.audioCtx.createMediaElementSource(this.media);
|
|
28
|
+
this.sourceNode.connect(this.audioCtx.destination);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export default WebAudioPlayer;
|
package/dist/player.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
declare class Player {
|
|
2
2
|
protected media: HTMLMediaElement;
|
|
3
3
|
private isExternalMedia;
|
|
4
|
-
|
|
4
|
+
private hasPlayedOnce;
|
|
5
|
+
constructor({ media, autoplay }: {
|
|
5
6
|
media?: HTMLMediaElement;
|
|
7
|
+
autoplay?: boolean;
|
|
6
8
|
});
|
|
7
9
|
on(event: keyof HTMLMediaElementEventMap, callback: () => void): () => void;
|
|
8
10
|
destroy(): void;
|
|
@@ -12,5 +14,11 @@ declare class Player {
|
|
|
12
14
|
pause(): void;
|
|
13
15
|
isPlaying(): boolean;
|
|
14
16
|
seekTo(time: number): void;
|
|
17
|
+
getVolume(): number;
|
|
18
|
+
setVolume(volume: number): void;
|
|
19
|
+
getMuted(): boolean;
|
|
20
|
+
setMuted(muted: boolean): void;
|
|
21
|
+
getPlaybackRate(): number;
|
|
22
|
+
setPlaybackRate(rate: number, preservePitch?: boolean): void;
|
|
15
23
|
}
|
|
16
24
|
export default Player;
|
package/dist/player.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
class Player {
|
|
2
|
+
constructor({ media, autoplay }) {
|
|
3
|
+
this.isExternalMedia = false;
|
|
4
|
+
this.hasPlayedOnce = false;
|
|
5
|
+
if (media) {
|
|
6
|
+
this.media = media;
|
|
7
|
+
this.isExternalMedia = true;
|
|
8
|
+
}
|
|
9
|
+
else {
|
|
10
|
+
this.media = document.createElement('audio');
|
|
11
|
+
}
|
|
12
|
+
// Track the first play() call
|
|
13
|
+
const unsubscribe = this.on('play', () => {
|
|
14
|
+
this.hasPlayedOnce = true;
|
|
15
|
+
unsubscribe();
|
|
16
|
+
});
|
|
17
|
+
// Autoplay
|
|
18
|
+
if (autoplay) {
|
|
19
|
+
this.media.autoplay = true;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
on(event, callback) {
|
|
23
|
+
var _a;
|
|
24
|
+
(_a = this.media) === null || _a === void 0 ? void 0 : _a.addEventListener(event, callback);
|
|
25
|
+
return () => { var _a; return (_a = this.media) === null || _a === void 0 ? void 0 : _a.removeEventListener(event, callback); };
|
|
26
|
+
}
|
|
27
|
+
destroy() {
|
|
28
|
+
var _a, _b;
|
|
29
|
+
(_a = this.media) === null || _a === void 0 ? void 0 : _a.pause();
|
|
30
|
+
if (!this.isExternalMedia) {
|
|
31
|
+
(_b = this.media) === null || _b === void 0 ? void 0 : _b.remove();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
loadUrl(src) {
|
|
35
|
+
if (this.media) {
|
|
36
|
+
this.media.src = src;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
getCurrentTime() {
|
|
40
|
+
return this.media.currentTime;
|
|
41
|
+
}
|
|
42
|
+
play() {
|
|
43
|
+
this.media.play();
|
|
44
|
+
}
|
|
45
|
+
pause() {
|
|
46
|
+
this.media.pause();
|
|
47
|
+
}
|
|
48
|
+
isPlaying() {
|
|
49
|
+
return this.media.currentTime > 0 && !this.media.paused && !this.media.ended;
|
|
50
|
+
}
|
|
51
|
+
seekTo(time) {
|
|
52
|
+
if (this.media) {
|
|
53
|
+
// iOS Safari requires a play() call before seeking
|
|
54
|
+
const { hasPlayedOnce } = this;
|
|
55
|
+
if (!hasPlayedOnce) {
|
|
56
|
+
this.media.play();
|
|
57
|
+
}
|
|
58
|
+
this.media.currentTime = time;
|
|
59
|
+
if (!hasPlayedOnce) {
|
|
60
|
+
this.media.pause();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
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
|
+
}
|
|
87
|
+
export default Player;
|
|
@@ -21,7 +21,7 @@ type RegionsPluginEvents = {
|
|
|
21
21
|
};
|
|
22
22
|
declare class RegionsPlugin extends BasePlugin<RegionsPluginEvents> {
|
|
23
23
|
private dragStart;
|
|
24
|
-
private
|
|
24
|
+
private wrapper;
|
|
25
25
|
private regions;
|
|
26
26
|
private createdRegion;
|
|
27
27
|
private modifiedRegion;
|
|
@@ -31,7 +31,7 @@ declare class RegionsPlugin extends BasePlugin<RegionsPluginEvents> {
|
|
|
31
31
|
constructor(params: WaveSurferPluginParams);
|
|
32
32
|
/** Unmounts the regions */
|
|
33
33
|
destroy(): void;
|
|
34
|
-
private
|
|
34
|
+
private initWrapper;
|
|
35
35
|
private handleMouseDown;
|
|
36
36
|
private handleMouseMove;
|
|
37
37
|
private handleMouseUp;
|
|
@@ -41,7 +41,7 @@ declare class RegionsPlugin extends BasePlugin<RegionsPluginEvents> {
|
|
|
41
41
|
private updateRegion;
|
|
42
42
|
private moveRegion;
|
|
43
43
|
/** Create a region at a given start and end time, with an optional title */
|
|
44
|
-
|
|
44
|
+
add(startTime: number, endTime: number, title?: string): Region;
|
|
45
45
|
/** Set the background color of a region */
|
|
46
46
|
setRegionColor(region: Region, color: string): void;
|
|
47
47
|
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import BasePlugin from '../base-plugin.js';
|
|
2
|
+
const MIN_WIDTH = 10;
|
|
3
|
+
const style = (element, styles) => {
|
|
4
|
+
for (const key in styles) {
|
|
5
|
+
element.style[key] = styles[key] || '';
|
|
6
|
+
}
|
|
7
|
+
};
|
|
8
|
+
const el = (tagName, css) => {
|
|
9
|
+
const element = document.createElement(tagName);
|
|
10
|
+
style(element, css);
|
|
11
|
+
return element;
|
|
12
|
+
};
|
|
13
|
+
class RegionsPlugin extends BasePlugin {
|
|
14
|
+
/** Create an instance of RegionsPlugin */
|
|
15
|
+
constructor(params) {
|
|
16
|
+
super(params);
|
|
17
|
+
this.dragStart = NaN;
|
|
18
|
+
this.regions = [];
|
|
19
|
+
this.createdRegion = null;
|
|
20
|
+
this.modifiedRegion = null;
|
|
21
|
+
this.isResizingLeft = false;
|
|
22
|
+
this.isMoving = false;
|
|
23
|
+
this.handleMouseDown = (e) => {
|
|
24
|
+
this.dragStart = e.clientX - this.container.getBoundingClientRect().left;
|
|
25
|
+
};
|
|
26
|
+
this.handleMouseMove = (e) => {
|
|
27
|
+
const dragEnd = e.clientX - this.container.getBoundingClientRect().left;
|
|
28
|
+
if (this.modifiedRegion && this.isMoving) {
|
|
29
|
+
this.moveRegion(this.modifiedRegion, dragEnd - this.dragStart);
|
|
30
|
+
this.dragStart = dragEnd;
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (this.modifiedRegion) {
|
|
34
|
+
this.updateRegion(this.modifiedRegion, this.isResizingLeft ? dragEnd : undefined, this.isResizingLeft ? undefined : dragEnd);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (!isNaN(this.dragStart)) {
|
|
38
|
+
const dragEnd = e.clientX - this.wrapper.getBoundingClientRect().left;
|
|
39
|
+
if (dragEnd - this.dragStart >= MIN_WIDTH) {
|
|
40
|
+
if (!this.createdRegion) {
|
|
41
|
+
this.container.style.pointerEvents = 'none';
|
|
42
|
+
this.createdRegion = this.createRegion(this.dragStart, dragEnd);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
this.updateRegion(this.createdRegion, this.dragStart, dragEnd);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
this.handleMouseUp = () => {
|
|
51
|
+
if (this.createdRegion) {
|
|
52
|
+
this.addRegion(this.createdRegion);
|
|
53
|
+
this.createdRegion = null;
|
|
54
|
+
}
|
|
55
|
+
this.modifiedRegion = null;
|
|
56
|
+
this.isMoving = false;
|
|
57
|
+
this.dragStart = NaN;
|
|
58
|
+
this.container.style.pointerEvents = '';
|
|
59
|
+
};
|
|
60
|
+
this.wrapper = this.initWrapper();
|
|
61
|
+
const unsubscribe = this.wavesurfer.on('decode', () => {
|
|
62
|
+
this.container.appendChild(this.wrapper);
|
|
63
|
+
unsubscribe();
|
|
64
|
+
});
|
|
65
|
+
this.subscriptions.push(unsubscribe);
|
|
66
|
+
this.container.addEventListener('mousedown', this.handleMouseDown);
|
|
67
|
+
document.addEventListener('mousemove', this.handleMouseMove);
|
|
68
|
+
document.addEventListener('mouseup', this.handleMouseUp);
|
|
69
|
+
}
|
|
70
|
+
/** Unmounts the regions */
|
|
71
|
+
destroy() {
|
|
72
|
+
this.container.removeEventListener('mousedown', this.handleMouseDown);
|
|
73
|
+
document.removeEventListener('mousemove', this.handleMouseMove);
|
|
74
|
+
document.removeEventListener('mouseup', this.handleMouseUp, true);
|
|
75
|
+
this.wrapper.remove();
|
|
76
|
+
super.destroy();
|
|
77
|
+
}
|
|
78
|
+
initWrapper() {
|
|
79
|
+
return el('div', {
|
|
80
|
+
position: 'absolute',
|
|
81
|
+
top: '0',
|
|
82
|
+
left: '0',
|
|
83
|
+
width: '100%',
|
|
84
|
+
height: '100%',
|
|
85
|
+
zIndex: '3',
|
|
86
|
+
pointerEvents: 'none',
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
createRegionElement(start, end, title = '') {
|
|
90
|
+
const div = el('div', {
|
|
91
|
+
position: 'absolute',
|
|
92
|
+
left: `${start}px`,
|
|
93
|
+
width: `${end - start}px`,
|
|
94
|
+
height: '100%',
|
|
95
|
+
backgroundColor: 'rgba(0, 0, 0, 0.1)',
|
|
96
|
+
transition: 'background-color 0.2s ease',
|
|
97
|
+
cursor: 'move',
|
|
98
|
+
pointerEvents: 'all',
|
|
99
|
+
});
|
|
100
|
+
div.title = title;
|
|
101
|
+
const leftHandle = el('div', {
|
|
102
|
+
position: 'absolute',
|
|
103
|
+
left: '0',
|
|
104
|
+
width: '6px',
|
|
105
|
+
height: '100%',
|
|
106
|
+
borderLeft: '2px solid rgba(0, 0, 0, 0.5)',
|
|
107
|
+
borderRadius: '2px 0 0 2px',
|
|
108
|
+
cursor: 'ew-resize',
|
|
109
|
+
pointerEvents: 'all',
|
|
110
|
+
});
|
|
111
|
+
div.appendChild(leftHandle);
|
|
112
|
+
const rightHandle = leftHandle.cloneNode();
|
|
113
|
+
style(rightHandle, {
|
|
114
|
+
left: '',
|
|
115
|
+
right: '0',
|
|
116
|
+
borderLeft: '',
|
|
117
|
+
borderRight: '2px solid rgba(0, 0, 0, 0.5)',
|
|
118
|
+
borderRadius: '0 2px 2px 0',
|
|
119
|
+
});
|
|
120
|
+
div.appendChild(rightHandle);
|
|
121
|
+
leftHandle.addEventListener('mousedown', (e) => {
|
|
122
|
+
e.stopPropagation();
|
|
123
|
+
this.modifiedRegion = this.regions.find((r) => r.element === div) || null;
|
|
124
|
+
this.isResizingLeft = true;
|
|
125
|
+
this.isMoving = false;
|
|
126
|
+
});
|
|
127
|
+
rightHandle.addEventListener('mousedown', (e) => {
|
|
128
|
+
e.stopPropagation();
|
|
129
|
+
this.modifiedRegion = this.regions.find((r) => r.element === div) || null;
|
|
130
|
+
this.isResizingLeft = false;
|
|
131
|
+
this.isMoving = false;
|
|
132
|
+
});
|
|
133
|
+
div.addEventListener('mousedown', () => {
|
|
134
|
+
this.modifiedRegion = this.regions.find((r) => r.element === div) || null;
|
|
135
|
+
this.isMoving = true;
|
|
136
|
+
});
|
|
137
|
+
div.addEventListener('click', () => {
|
|
138
|
+
const region = this.regions.find((r) => r.element === div);
|
|
139
|
+
if (region) {
|
|
140
|
+
this.emit('region-clicked', { region });
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
this.wrapper.appendChild(div);
|
|
144
|
+
return div;
|
|
145
|
+
}
|
|
146
|
+
createRegion(start, end, title = '') {
|
|
147
|
+
const duration = this.wavesurfer.getDuration();
|
|
148
|
+
const width = this.wrapper.clientWidth;
|
|
149
|
+
return {
|
|
150
|
+
element: this.createRegionElement(start, end, title),
|
|
151
|
+
start,
|
|
152
|
+
end,
|
|
153
|
+
startTime: (start / width) * duration,
|
|
154
|
+
endTime: (end / width) * duration,
|
|
155
|
+
title,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
addRegion(region) {
|
|
159
|
+
this.regions.push(region);
|
|
160
|
+
this.emit('region-created', { region });
|
|
161
|
+
}
|
|
162
|
+
updateRegion(region, start, end) {
|
|
163
|
+
if (start != null) {
|
|
164
|
+
region.start = start !== null && start !== void 0 ? start : region.start;
|
|
165
|
+
region.element.style.left = `${region.start}px`;
|
|
166
|
+
region.startTime = (start / this.wrapper.clientWidth) * this.wavesurfer.getDuration();
|
|
167
|
+
}
|
|
168
|
+
if (end != null) {
|
|
169
|
+
region.end = end;
|
|
170
|
+
region.element.style.width = `${region.end - region.start}px`;
|
|
171
|
+
region.endTime = (end / this.wrapper.clientWidth) * this.wavesurfer.getDuration();
|
|
172
|
+
}
|
|
173
|
+
this.emit('region-updated', { region });
|
|
174
|
+
}
|
|
175
|
+
moveRegion(region, delta) {
|
|
176
|
+
this.updateRegion(region, region.start + delta, region.end + delta);
|
|
177
|
+
}
|
|
178
|
+
/** Create a region at a given start and end time, with an optional title */
|
|
179
|
+
add(startTime, endTime, title = '') {
|
|
180
|
+
const duration = this.wavesurfer.getDuration();
|
|
181
|
+
const width = this.wrapper.clientWidth;
|
|
182
|
+
const start = (startTime / duration) * width;
|
|
183
|
+
const end = (endTime / duration) * width;
|
|
184
|
+
const region = this.createRegion(start, end, title);
|
|
185
|
+
this.addRegion(region);
|
|
186
|
+
return region;
|
|
187
|
+
}
|
|
188
|
+
/** Set the background color of a region */
|
|
189
|
+
setRegionColor(region, color) {
|
|
190
|
+
region.element.style.backgroundColor = color;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
export default RegionsPlugin;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
import WaveSurfer from '../index.js';
|
|
3
|
+
const { useEffect, useState } = React;
|
|
4
|
+
// A React hook to use WaveSurfer
|
|
5
|
+
export const useWavesurfer = (containerRef, options) => {
|
|
6
|
+
const [wavesurfer, setWavesurfer] = useState(null);
|
|
7
|
+
// Initialize wavesurfer when the container mounts
|
|
8
|
+
// or any of the props change
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
if (!containerRef.current)
|
|
11
|
+
return;
|
|
12
|
+
const ws = WaveSurfer.create(Object.assign(Object.assign({}, options), { container: containerRef.current }));
|
|
13
|
+
setWavesurfer(ws);
|
|
14
|
+
return () => {
|
|
15
|
+
ws.destroy();
|
|
16
|
+
};
|
|
17
|
+
}, [options, containerRef]);
|
|
18
|
+
return wavesurfer;
|
|
19
|
+
};
|
|
20
|
+
export default useWavesurfer;
|
package/dist/renderer.d.ts
CHANGED
|
@@ -17,18 +17,20 @@ type RendererEvents = {
|
|
|
17
17
|
declare class Renderer extends EventEmitter<RendererEvents> {
|
|
18
18
|
private options;
|
|
19
19
|
private container;
|
|
20
|
-
private
|
|
20
|
+
private scrollContainer;
|
|
21
21
|
private mainCanvas;
|
|
22
22
|
private progressCanvas;
|
|
23
23
|
private cursor;
|
|
24
24
|
private ctx;
|
|
25
|
+
private timeout;
|
|
25
26
|
constructor(options: RendererOptions);
|
|
27
|
+
getContainer(): HTMLElement;
|
|
26
28
|
destroy(): void;
|
|
27
|
-
private
|
|
28
|
-
private
|
|
29
|
-
render(channelData: Float32Array[], duration: number, minPxPerSec?: number): void;
|
|
29
|
+
private delay;
|
|
30
|
+
private renderPeaks;
|
|
30
31
|
private createProgressMask;
|
|
32
|
+
render(channelData: Float32Array[], duration: number): void;
|
|
33
|
+
zoom(channelData: Float32Array[], duration: number, minPxPerSec: number): void;
|
|
31
34
|
renderProgress(progress: number, autoCenter?: boolean): void;
|
|
32
|
-
getContainer(): HTMLElement;
|
|
33
35
|
}
|
|
34
36
|
export default Renderer;
|
package/dist/renderer.js
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import EventEmitter from './event-emitter.js';
|
|
11
|
+
class Renderer extends EventEmitter {
|
|
12
|
+
constructor(options) {
|
|
13
|
+
super();
|
|
14
|
+
this.timeout = null;
|
|
15
|
+
this.options = options;
|
|
16
|
+
let container = null;
|
|
17
|
+
if (typeof this.options.container === 'string') {
|
|
18
|
+
container = document.querySelector(this.options.container);
|
|
19
|
+
}
|
|
20
|
+
else if (this.options.container instanceof HTMLElement) {
|
|
21
|
+
container = this.options.container;
|
|
22
|
+
}
|
|
23
|
+
if (!container) {
|
|
24
|
+
throw new Error('Container not found');
|
|
25
|
+
}
|
|
26
|
+
const div = document.createElement('div');
|
|
27
|
+
const shadow = div.attachShadow({ mode: 'open' });
|
|
28
|
+
shadow.innerHTML = `
|
|
29
|
+
<style>
|
|
30
|
+
:host {
|
|
31
|
+
user-select: none;
|
|
32
|
+
}
|
|
33
|
+
:host .scroll {
|
|
34
|
+
overflow-x: auto;
|
|
35
|
+
overflow-y: visible;
|
|
36
|
+
width: 100%;
|
|
37
|
+
height: ${this.options.height}px;
|
|
38
|
+
position: relative;
|
|
39
|
+
}
|
|
40
|
+
:host .wrapper {
|
|
41
|
+
position: relative;
|
|
42
|
+
width: fit-content;
|
|
43
|
+
min-width: 100%;
|
|
44
|
+
height: 100%;
|
|
45
|
+
z-index: 2;
|
|
46
|
+
}
|
|
47
|
+
:host canvas {
|
|
48
|
+
display: block;
|
|
49
|
+
height: 100%;
|
|
50
|
+
min-width: 100%;
|
|
51
|
+
image-rendering: pixelated;
|
|
52
|
+
}
|
|
53
|
+
:host .progress {
|
|
54
|
+
position: absolute;
|
|
55
|
+
z-index: 2;
|
|
56
|
+
top: 0;
|
|
57
|
+
left: 0;
|
|
58
|
+
height: 100%;
|
|
59
|
+
pointer-events: none;
|
|
60
|
+
clip-path: inset(100%);
|
|
61
|
+
}
|
|
62
|
+
:host .cursor {
|
|
63
|
+
position: absolute;
|
|
64
|
+
z-index: 3;
|
|
65
|
+
top: 0;
|
|
66
|
+
left: 0;
|
|
67
|
+
height: 100%;
|
|
68
|
+
border-right: 1px solid ${this.options.progressColor};
|
|
69
|
+
margin-left: -1px;
|
|
70
|
+
}
|
|
71
|
+
</style>
|
|
72
|
+
|
|
73
|
+
<div class="scroll">
|
|
74
|
+
<div class="wrapper">
|
|
75
|
+
<canvas></canvas>
|
|
76
|
+
<canvas class="progress"></canvas>
|
|
77
|
+
<div class="cursor"></div>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
`;
|
|
81
|
+
this.container = div;
|
|
82
|
+
this.scrollContainer = shadow.querySelector('.scroll');
|
|
83
|
+
this.mainCanvas = shadow.querySelector('canvas');
|
|
84
|
+
this.ctx = this.mainCanvas.getContext('2d', { desynchronized: true });
|
|
85
|
+
this.progressCanvas = shadow.querySelector('.progress');
|
|
86
|
+
this.cursor = shadow.querySelector('.cursor');
|
|
87
|
+
container.appendChild(div);
|
|
88
|
+
this.mainCanvas.addEventListener('click', (e) => {
|
|
89
|
+
const rect = this.mainCanvas.getBoundingClientRect();
|
|
90
|
+
const x = e.clientX - rect.left;
|
|
91
|
+
const relativeX = x / rect.width;
|
|
92
|
+
this.emit('click', { relativeX });
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
getContainer() {
|
|
96
|
+
return this.scrollContainer.querySelector('.wrapper');
|
|
97
|
+
}
|
|
98
|
+
destroy() {
|
|
99
|
+
this.container.remove();
|
|
100
|
+
}
|
|
101
|
+
delay(fn, delayMs = 100) {
|
|
102
|
+
if (this.timeout) {
|
|
103
|
+
clearTimeout(this.timeout);
|
|
104
|
+
}
|
|
105
|
+
return new Promise((resolve) => {
|
|
106
|
+
this.timeout = setTimeout(() => {
|
|
107
|
+
resolve(fn());
|
|
108
|
+
}, delayMs);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
renderPeaks(channelData, width, height) {
|
|
112
|
+
var _a;
|
|
113
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
114
|
+
const { devicePixelRatio } = window;
|
|
115
|
+
const { ctx } = this;
|
|
116
|
+
const barWidth = this.options.barWidth != null ? this.options.barWidth * devicePixelRatio : 1;
|
|
117
|
+
const barGap = this.options.barGap != null ? this.options.barGap * devicePixelRatio : this.options.barWidth ? barWidth / 2 : 0;
|
|
118
|
+
const barRadius = (_a = this.options.barRadius) !== null && _a !== void 0 ? _a : 0;
|
|
119
|
+
const leftChannel = channelData[0];
|
|
120
|
+
const len = leftChannel.length;
|
|
121
|
+
const barCount = Math.floor(width / (barWidth + barGap));
|
|
122
|
+
const barIndexScale = barCount / len;
|
|
123
|
+
const halfHeight = height / 2;
|
|
124
|
+
const isMono = channelData.length === 1;
|
|
125
|
+
const rightChannel = isMono ? leftChannel : channelData[1];
|
|
126
|
+
const useNegative = isMono && rightChannel.some((v) => v < 0);
|
|
127
|
+
const draw = (start, end) => {
|
|
128
|
+
let prevX = 0;
|
|
129
|
+
let prevLeft = 0;
|
|
130
|
+
let prevRight = 0;
|
|
131
|
+
ctx.beginPath();
|
|
132
|
+
for (let i = start; i < end; i++) {
|
|
133
|
+
const barIndex = Math.round(i * barIndexScale);
|
|
134
|
+
if (barIndex > prevX) {
|
|
135
|
+
const leftBarHeight = Math.round(prevLeft * halfHeight);
|
|
136
|
+
const rightBarHeight = Math.round(prevRight * halfHeight);
|
|
137
|
+
ctx.roundRect(prevX * (barWidth + barGap), halfHeight - leftBarHeight, barWidth, leftBarHeight + rightBarHeight || 1, barRadius);
|
|
138
|
+
prevX = barIndex;
|
|
139
|
+
prevLeft = 0;
|
|
140
|
+
prevRight = 0;
|
|
141
|
+
}
|
|
142
|
+
const leftValue = useNegative ? leftChannel[i] : Math.abs(leftChannel[i]);
|
|
143
|
+
const rightValue = useNegative ? rightChannel[i] : Math.abs(rightChannel[i]);
|
|
144
|
+
if (leftValue > prevLeft) {
|
|
145
|
+
prevLeft = leftValue;
|
|
146
|
+
}
|
|
147
|
+
// If stereo, both channels are drawn as max values
|
|
148
|
+
// If mono with negative values, the bottom channel will be the min negative values
|
|
149
|
+
if (useNegative ? rightValue < -prevRight : rightValue > prevRight) {
|
|
150
|
+
prevRight = rightValue < 0 ? -rightValue : rightValue;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
ctx.fillStyle = this.options.waveColor;
|
|
154
|
+
ctx.fill();
|
|
155
|
+
ctx.closePath();
|
|
156
|
+
};
|
|
157
|
+
// Clear the canvas
|
|
158
|
+
ctx.clearRect(0, 0, width, height);
|
|
159
|
+
// Draw the currently visible part of the waveform
|
|
160
|
+
const { scrollLeft, scrollWidth, clientWidth } = this.scrollContainer;
|
|
161
|
+
const scale = len / scrollWidth;
|
|
162
|
+
const start = Math.floor(scrollLeft * scale);
|
|
163
|
+
const end = Math.ceil((scrollLeft + clientWidth) * scale);
|
|
164
|
+
draw(start, end);
|
|
165
|
+
// Draw the progress mask
|
|
166
|
+
this.createProgressMask();
|
|
167
|
+
// Draw the rest of the waveform with a timeout for better performance
|
|
168
|
+
if (start > 0) {
|
|
169
|
+
yield this.delay(() => {
|
|
170
|
+
draw(0, start);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
if (end < len) {
|
|
174
|
+
yield this.delay(() => {
|
|
175
|
+
draw(end, len);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
// Redraw the progress mask
|
|
179
|
+
this.delay(() => {
|
|
180
|
+
this.createProgressMask();
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
createProgressMask() {
|
|
185
|
+
const progressCtx = this.progressCanvas.getContext('2d', { desynchronized: true });
|
|
186
|
+
// Set the canvas to the same size as the main canvas
|
|
187
|
+
this.progressCanvas.width = this.mainCanvas.width;
|
|
188
|
+
this.progressCanvas.height = this.mainCanvas.height;
|
|
189
|
+
this.progressCanvas.style.width = this.mainCanvas.style.width;
|
|
190
|
+
this.progressCanvas.style.height = this.mainCanvas.style.height;
|
|
191
|
+
// Copy the waveform image to the progress canvas
|
|
192
|
+
// The main canvas itself is used as the source image
|
|
193
|
+
progressCtx.drawImage(this.mainCanvas, 0, 0);
|
|
194
|
+
// Set the composition method to draw only where the waveform is drawn
|
|
195
|
+
progressCtx.globalCompositeOperation = 'source-in';
|
|
196
|
+
progressCtx.fillStyle = this.options.progressColor;
|
|
197
|
+
// This rectangle acts as a mask thanks to the composition method
|
|
198
|
+
progressCtx.fillRect(0, 0, this.progressCanvas.width, this.progressCanvas.height);
|
|
199
|
+
}
|
|
200
|
+
render(channelData, duration) {
|
|
201
|
+
// Determine the width of the canvas
|
|
202
|
+
const { devicePixelRatio } = window;
|
|
203
|
+
const parentWidth = this.scrollContainer.clientWidth * devicePixelRatio;
|
|
204
|
+
const scrollWidth = duration * this.options.minPxPerSec;
|
|
205
|
+
const scrollParent = scrollWidth > parentWidth;
|
|
206
|
+
const width = scrollParent ? scrollWidth : parentWidth;
|
|
207
|
+
const { height } = this.options;
|
|
208
|
+
this.mainCanvas.width = width;
|
|
209
|
+
this.mainCanvas.height = height;
|
|
210
|
+
this.mainCanvas.style.width = scrollParent ? Math.round(scrollWidth / devicePixelRatio) + 'px' : '100%';
|
|
211
|
+
this.renderPeaks(channelData, width, height);
|
|
212
|
+
}
|
|
213
|
+
zoom(channelData, duration, minPxPerSec) {
|
|
214
|
+
// Remember the current cursor position
|
|
215
|
+
const oldCursorPosition = this.cursor.getBoundingClientRect().left;
|
|
216
|
+
this.options.minPxPerSec = minPxPerSec;
|
|
217
|
+
this.render(channelData, duration);
|
|
218
|
+
// Adjust the scroll position so that the cursor stays in the same place
|
|
219
|
+
const newCursortPosition = this.cursor.getBoundingClientRect().left;
|
|
220
|
+
this.scrollContainer.scrollLeft += newCursortPosition - oldCursorPosition;
|
|
221
|
+
}
|
|
222
|
+
renderProgress(progress, autoCenter = false) {
|
|
223
|
+
this.progressCanvas.style.clipPath = `inset(0 ${100 - progress * 100}% 0 0)`;
|
|
224
|
+
this.cursor.style.left = `${progress * 100}%`;
|
|
225
|
+
if (autoCenter) {
|
|
226
|
+
const center = this.container.clientWidth / 2;
|
|
227
|
+
const fullWidth = this.mainCanvas.clientWidth;
|
|
228
|
+
if (fullWidth * progress >= center) {
|
|
229
|
+
this.scrollContainer.scrollLeft = fullWidth * progress - center;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
export default Renderer;
|
package/dist/timer.d.ts
CHANGED
package/dist/timer.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import EventEmitter from './event-emitter.js';
|
|
2
|
+
class Timer extends EventEmitter {
|
|
3
|
+
constructor() {
|
|
4
|
+
super(...arguments);
|
|
5
|
+
this.unsubscribe = () => undefined;
|
|
6
|
+
}
|
|
7
|
+
start() {
|
|
8
|
+
this.unsubscribe = this.on('tick', () => {
|
|
9
|
+
requestAnimationFrame(() => {
|
|
10
|
+
this.emit('tick');
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
this.emit('tick');
|
|
14
|
+
}
|
|
15
|
+
stop() {
|
|
16
|
+
this.unsubscribe();
|
|
17
|
+
}
|
|
18
|
+
destroy() {
|
|
19
|
+
this.unsubscribe();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export default Timer;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.Regions=t():e.Regions=t()}(WaveSurfer,(()=>(()=>{"use strict";var e={d:(t,i)=>{for(var n in i)e.o(i,n)&&!e.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:i[n]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)},t={};e.d(t,{default:()=>
|
|
1
|
+
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.Regions=t():e.Regions=t()}(WaveSurfer,(()=>(()=>{"use strict";var e={d:(t,i)=>{for(var n in i)e.o(i,n)&&!e.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:i[n]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)},t={};e.d(t,{default:()=>r});const i=class{constructor(){this.eventTarget=new EventTarget}emit(e,t){const i=new CustomEvent(String(e),{detail:t});this.eventTarget.dispatchEvent(i)}on(e,t,i){const n=e=>{e instanceof CustomEvent&&t(e.detail)},s=String(e);return this.eventTarget.addEventListener(s,n,{once:i}),()=>this.eventTarget.removeEventListener(s,n)}once(e,t){return this.on(e,t,!0)}},n=class extends i{constructor(e){super(),this.subscriptions=[],this.wavesurfer=e.wavesurfer,this.container=e.container}destroy(){this.subscriptions.forEach((e=>e()))}},s=(e,t)=>{for(const i in t)e.style[i]=t[i]||""},o=(e,t)=>{const i=document.createElement(e);return s(i,t),i},r=class extends n{constructor(e){super(e),this.dragStart=NaN,this.regions=[],this.createdRegion=null,this.modifiedRegion=null,this.isResizingLeft=!1,this.isMoving=!1,this.handleMouseDown=e=>{this.dragStart=e.clientX-this.container.getBoundingClientRect().left},this.handleMouseMove=e=>{const t=e.clientX-this.container.getBoundingClientRect().left;if(this.modifiedRegion&&this.isMoving)return this.moveRegion(this.modifiedRegion,t-this.dragStart),void(this.dragStart=t);if(this.modifiedRegion)this.updateRegion(this.modifiedRegion,this.isResizingLeft?t:void 0,this.isResizingLeft?void 0:t);else if(!isNaN(this.dragStart)){const t=e.clientX-this.wrapper.getBoundingClientRect().left;t-this.dragStart>=10&&(this.createdRegion?this.updateRegion(this.createdRegion,this.dragStart,t):(this.container.style.pointerEvents="none",this.createdRegion=this.createRegion(this.dragStart,t)))}},this.handleMouseUp=()=>{this.createdRegion&&(this.addRegion(this.createdRegion),this.createdRegion=null),this.modifiedRegion=null,this.isMoving=!1,this.dragStart=NaN,this.container.style.pointerEvents=""},this.wrapper=this.initWrapper();const t=this.wavesurfer.on("decode",(()=>{this.container.appendChild(this.wrapper),t()}));this.subscriptions.push(t),this.container.addEventListener("mousedown",this.handleMouseDown),document.addEventListener("mousemove",this.handleMouseMove),document.addEventListener("mouseup",this.handleMouseUp)}destroy(){this.container.removeEventListener("mousedown",this.handleMouseDown),document.removeEventListener("mousemove",this.handleMouseMove),document.removeEventListener("mouseup",this.handleMouseUp,!0),this.wrapper.remove(),super.destroy()}initWrapper(){return o("div",{position:"absolute",top:"0",left:"0",width:"100%",height:"100%",zIndex:"3",pointerEvents:"none"})}createRegionElement(e,t,i=""){const n=o("div",{position:"absolute",left:`${e}px`,width:t-e+"px",height:"100%",backgroundColor:"rgba(0, 0, 0, 0.1)",transition:"background-color 0.2s ease",cursor:"move",pointerEvents:"all"});n.title=i;const r=o("div",{position:"absolute",left:"0",width:"6px",height:"100%",borderLeft:"2px solid rgba(0, 0, 0, 0.5)",borderRadius:"2px 0 0 2px",cursor:"ew-resize",pointerEvents:"all"});n.appendChild(r);const d=r.cloneNode();return s(d,{left:"",right:"0",borderLeft:"",borderRight:"2px solid rgba(0, 0, 0, 0.5)",borderRadius:"0 2px 2px 0"}),n.appendChild(d),r.addEventListener("mousedown",(e=>{e.stopPropagation(),this.modifiedRegion=this.regions.find((e=>e.element===n))||null,this.isResizingLeft=!0,this.isMoving=!1})),d.addEventListener("mousedown",(e=>{e.stopPropagation(),this.modifiedRegion=this.regions.find((e=>e.element===n))||null,this.isResizingLeft=!1,this.isMoving=!1})),n.addEventListener("mousedown",(()=>{this.modifiedRegion=this.regions.find((e=>e.element===n))||null,this.isMoving=!0})),n.addEventListener("click",(()=>{const e=this.regions.find((e=>e.element===n));e&&this.emit("region-clicked",{region:e})})),this.wrapper.appendChild(n),n}createRegion(e,t,i=""){const n=this.wavesurfer.getDuration(),s=this.wrapper.clientWidth;return{element:this.createRegionElement(e,t,i),start:e,end:t,startTime:e/s*n,endTime:t/s*n,title:i}}addRegion(e){this.regions.push(e),this.emit("region-created",{region:e})}updateRegion(e,t,i){null!=t&&(e.start=null!=t?t:e.start,e.element.style.left=`${e.start}px`,e.startTime=t/this.wrapper.clientWidth*this.wavesurfer.getDuration()),null!=i&&(e.end=i,e.element.style.width=e.end-e.start+"px",e.endTime=i/this.wrapper.clientWidth*this.wavesurfer.getDuration()),this.emit("region-updated",{region:e})}moveRegion(e,t){this.updateRegion(e,e.start+t,e.end+t)}add(e,t,i=""){const n=this.wavesurfer.getDuration(),s=this.wrapper.clientWidth,o=e/n*s,r=t/n*s,d=this.createRegion(o,r,i);return this.addRegion(d),d}setRegionColor(e,t){e.element.style.backgroundColor=t}};return t.default})()));
|
package/dist/wavesurfer.min.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.WaveSurfer=e():t.WaveSurfer=e()}(this,(()=>(()=>{"use strict";var t={d:(e,i)=>{for(var n in i)t.o(i,n)&&!t.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:i[n]})},o:(t,e)=>Object.prototype.hasOwnProperty.call(t,e)},e={};t.d(e,{default:()=>l});const i=class{constructor(){this.eventTarget=new EventTarget}emit(t,e){const i=new CustomEvent(String(t),{detail:e});this.eventTarget.dispatchEvent(i)}on(t,e){const i=t=>{t instanceof CustomEvent&&e(t.detail)},n=String(t);return this.eventTarget.addEventListener(n,i),()=>this.eventTarget.removeEventListener(n,i)}once(t,e){const i=this.on(t,((...t)=>{i(),e(...t)}));return i}},n=class extends i{constructor(t){super(),this.options=t;let e=null;if("string"==typeof this.options.container?e=document.querySelector(this.options.container):this.options.container instanceof HTMLElement&&(e=this.options.container),!e)throw new Error("Container not found");const i=document.createElement("div"),n=i.attachShadow({mode:"open"});n.innerHTML=`\n <style>\n :host .scroll {\n overflow-x: auto;\n overflow-y: visible;\n user-select: none;\n width: 100%;\n height: ${this.options.height}px;\n position: relative;\n }\n :host .wrapper {\n position: relative;\n width: fit-content;\n min-width: 100%;\n height: 100%;\n z-index: 2;\n }\n :host canvas {\n display: block;\n height: 100%;\n min-width: 100%;\n image-rendering: pixelated;\n }\n :host .progress {\n position: absolute;\n z-index: 2;\n top: 0;\n left: 0;\n height: 100%;\n pointer-events: none;\n clip-path: inset(100%);\n }\n :host .cursor {\n position: absolute;\n z-index: 3;\n top: 0;\n left: 0;\n height: 100%;\n border-right: 1px solid ${this.options.progressColor};\n }\n </style>\n\n <div class="scroll">\n <div class="wrapper">\n <canvas></canvas>\n <canvas class="progress"></canvas>\n <div class="cursor"></div>\n </div>\n </div>\n `,this.container=i,this.shadowRoot=n,this.mainCanvas=n.querySelector("canvas"),this.ctx=this.mainCanvas.getContext("2d",{desynchronized:!0}),this.progressCanvas=n.querySelector(".progress"),this.cursor=n.querySelector(".cursor"),e.appendChild(i),this.mainCanvas.addEventListener("click",(t=>{const e=this.mainCanvas.getBoundingClientRect(),i=(t.clientX-e.left)/e.width;this.emit("click",{relativeX:i})}))}destroy(){this.container.remove()}renderLinePeaks(t,e,i){const{ctx:n}=this;n.clearRect(0,0,e,i),n.beginPath(),n.moveTo(0,i/2);const s=t[0],r=1===t.length,o=r?s:t[1];let a=-1,h=0;for(let t=0,r=s.length;t<r;t++){const r=Math.round(t/s.length*e),o=Math.round((1-s[t])*i/2);(r!==a||o>h)&&(n.lineTo(r,o),a=r,h=o)}a=-1,h=0;for(let t=o.length-1;t>=0;t--){const s=Math.round(t/o.length*e),d=Math.round((1+o[t])*i/2);(s!==a||(r?d<-h:d>h))&&(n.lineTo(s,d),a=s,h=d)}n.strokeStyle=n.fillStyle=this.options.waveColor,n.stroke(),n.fill()}renderBarPeaks(t,e,i){var n,s;const{devicePixelRatio:r}=window,{ctx:o}=this;o.clearRect(0,0,e,i);const a=this.options.barWidth||1,h=a*r,d=(null!==(n=this.options.barGap)&&void 0!==n?n:a/2)*r,l=null!==(s=this.options.barRadius)&&void 0!==s?s:0,c=t[0],u=1===t.length,p=u?c:t[1],v=Math.floor(e/(h+d)),m=new Float32Array(v),g=new Float32Array(v),y=v/c.length,f=c.length;for(let t=0;t<f;t++){const e=Math.round(t*y);m[e]=Math.max(m[e],c[t]),g[e]=(u?Math.min:Math.max)(g[e],p[t])}o.beginPath();for(let t=0;t<v;t++){const e=Math.max(1,Math.round(m[t]*i/2)),n=Math.max(1,Math.round(Math.abs(g[t]*i)/2));o.roundRect(t*(h+d),i/2-e,h,e+n,l)}o.fillStyle=this.options.waveColor,o.fill()}render(t,e,i=this.options.minPxPerSec){const{devicePixelRatio:n}=window,s=Math.max(this.container.clientWidth*n,e*i),{height:r}=this.options;this.mainCanvas.width=s,this.mainCanvas.height=r,this.mainCanvas.style.width=Math.round(s/n)+"px",(this.options.barWidth?this.renderBarPeaks:this.renderLinePeaks).call(this,t,s,r),this.createProgressMask()}createProgressMask(){const t=this.progressCanvas.getContext("2d",{desynchronized:!0});if(!t)throw new Error("Failed to get canvas context");this.progressCanvas.width=this.mainCanvas.width,this.progressCanvas.height=this.mainCanvas.height,this.progressCanvas.style.width=this.mainCanvas.style.width,this.progressCanvas.style.height=this.mainCanvas.style.height,t.drawImage(this.mainCanvas,0,0),t.globalCompositeOperation="source-in",t.fillStyle=this.options.progressColor,t.fillRect(0,0,this.progressCanvas.width,this.progressCanvas.height)}renderProgress(t,e=!1){if(this.progressCanvas.style.clipPath=`inset(0 ${100-100*t}% 0 0)`,this.cursor.style.left=100*t+"%",e){const e=this.container.clientWidth/2,i=this.mainCanvas.clientWidth;i*t>=e&&(this.container.scrollLeft=i*t-e)}}getContainer(){return this.shadowRoot.querySelector(".scroll")}},s=class{constructor({media:t}){this.isExternalMedia=!1,t?(this.media=t,this.isExternalMedia=!0):this.media=document.createElement("audio")}on(t,e){return this.media.addEventListener(t,e),()=>this.media.removeEventListener(t,e)}destroy(){this.isExternalMedia||this.media.remove()}loadUrl(t){this.media.src=t}getCurrentTime(){return this.media.currentTime}play(){this.media.play()}pause(){this.media.pause()}isPlaying(){return this.media.currentTime>0&&!this.media.paused&&!this.media.ended}seekTo(t){this.media.currentTime=t}},r=class extends s{constructor(){super(...arguments),this.audioCtx=null,this.sourceNode=null}destroy(){var t,e;null===(t=this.sourceNode)||void 0===t||t.disconnect(),this.sourceNode=null,null===(e=this.audioCtx)||void 0===e||e.close(),this.audioCtx=null,super.destroy()}loadUrl(t){super.loadUrl(t),this.audioCtx||(this.audioCtx=new AudioContext({latencyHint:"playback"})),this.sourceNode&&this.sourceNode.disconnect(),this.sourceNode=this.audioCtx.createMediaElementSource(this.media),this.sourceNode.connect(this.audioCtx.destination)}},o=class extends i{constructor(){super(),this.unsubscribe=()=>{},this.unsubscribe=this.on("tick",(()=>{requestAnimationFrame((()=>{this.emit("tick")}))})),this.emit("tick")}destroy(){this.unsubscribe()}};var a;!function(t){t.WebAudio="WebAudio",t.MediaElement="MediaElement"}(a||(a={}));const h={height:128,waveColor:"#999",progressColor:"#555",minPxPerSec:0,backend:"MediaElement"};class d extends i{static create(t){return new d(t)}constructor(t){var e;super(),this.plugins=[],this.subscriptions=[],this.channelData=null,this.duration=0,this.options=Object.assign({},h,t),this.fetcher=new class{load(t){return e=this,i=void 0,s=function*(){return fetch(t).then((t=>t.arrayBuffer()))},new((n=void 0)||(n=Promise))((function(t,r){function o(t){try{h(s.next(t))}catch(t){r(t)}}function a(t){try{h(s.throw(t))}catch(t){r(t)}}function h(e){var i;e.done?t(e.value):(i=e.value,i instanceof n?i:new n((function(t){t(i)}))).then(o,a)}h((s=s.apply(e,i||[])).next())}));var e,i,n,s}},this.decoder=new class{constructor(){this.audioCtx=null,this.audioCtx=new AudioContext({latencyHint:"playback",sampleRate:3e3})}decode(t){return e=this,i=void 0,s=function*(){if(!this.audioCtx)throw new Error("AudioContext is not initialized");const e=yield this.audioCtx.decodeAudioData(t),i=[e.getChannelData(0)];return e.numberOfChannels>1&&i.push(e.getChannelData(1)),{duration:e.duration,channelData:i}},new((n=void 0)||(n=Promise))((function(t,r){function o(t){try{h(s.next(t))}catch(t){r(t)}}function a(t){try{h(s.throw(t))}catch(t){r(t)}}function h(e){var i;e.done?t(e.value):(i=e.value,i instanceof n?i:new n((function(t){t(i)}))).then(o,a)}h((s=s.apply(e,i||[])).next())}));var e,i,n,s}destroy(){var t;null===(t=this.audioCtx)||void 0===t||t.close(),this.audioCtx=null}},this.timer=new o,this.player=new(this.options.backend===a.WebAudio?r:s)({media:this.options.media}),this.renderer=new n({container:this.options.container,height:this.options.height,waveColor:this.options.waveColor,progressColor:this.options.progressColor,minPxPerSec:this.options.minPxPerSec,barWidth:this.options.barWidth,barGap:this.options.barGap,barRadius:this.options.barRadius}),this.initPlayerEvents(),this.initRendererEvents(),this.initTimerEvents();const i=this.options.url||(null===(e=this.options.media)||void 0===e?void 0:e.src);i&&this.load(i,this.options.channelData,this.options.duration)}initPlayerEvents(){this.subscriptions.push(this.player.on("timeupdate",(()=>{const t=this.getCurrentTime();this.renderer.renderProgress(t/this.duration,this.isPlaying()),this.emit("audioprocess",{currentTime:t})})),this.player.on("play",(()=>{this.emit("play")})),this.player.on("pause",(()=>{this.emit("pause")})),this.player.on("canplay",(()=>{this.emit("canplay")})),this.player.on("seeking",(()=>{this.emit("seek",{time:this.getCurrentTime()})})))}initRendererEvents(){this.subscriptions.push(this.renderer.on("click",(({relativeX:t})=>{const e=this.getDuration()*t;this.seekTo(e)})))}initTimerEvents(){this.subscriptions.push(this.timer.on("tick",(()=>{if(this.isPlaying()){const t=this.getCurrentTime();this.renderer.renderProgress(t/this.duration,!0),this.emit("audioprocess",{currentTime:t})}})))}destroy(){this.subscriptions.forEach((t=>t())),this.plugins.forEach((t=>t.destroy())),this.timer.destroy(),this.player.destroy(),this.decoder.destroy(),this.renderer.destroy()}load(t,e,i){return n=this,s=void 0,o=function*(){if(this.player.loadUrl(t),null==e||null==i){const n=yield this.fetcher.load(t),s=yield this.decoder.decode(n);e=s.channelData,i=s.duration}this.channelData=e,this.duration=i,this.renderer.render(this.channelData,this.duration),this.emit("ready",{duration:this.duration})},new((r=void 0)||(r=Promise))((function(t,e){function i(t){try{h(o.next(t))}catch(t){e(t)}}function a(t){try{h(o.throw(t))}catch(t){e(t)}}function h(e){var n;e.done?t(e.value):(n=e.value,n instanceof r?n:new r((function(t){t(n)}))).then(i,a)}h((o=o.apply(n,s||[])).next())}));var n,s,r,o}zoom(t){if(null==this.channelData||null==this.duration)throw new Error("No audio loaded");this.renderer.render(this.channelData,this.duration,t),this.renderer.renderProgress(this.getCurrentTime()/this.duration,!0)}play(){this.player.play()}pause(){this.player.pause()}seekTo(t){this.player.seekTo(t)}isPlaying(){return this.player.isPlaying()}getDuration(){return this.duration}getCurrentTime(){return this.player.getCurrentTime()}registerPlugin(t){const e=new t({wavesurfer:this,renderer:this.renderer});return this.plugins.push(e),e}}const l=d;return e.default})()));
|
|
1
|
+
!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.WaveSurfer=e():t.WaveSurfer=e()}(this,(()=>(()=>{"use strict";var t={d:(e,i)=>{for(var n in i)t.o(i,n)&&!t.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:i[n]})},o:(t,e)=>Object.prototype.hasOwnProperty.call(t,e)},e={};t.d(e,{default:()=>a});const i=class{constructor(){this.eventTarget=new EventTarget}emit(t,e){const i=new CustomEvent(String(t),{detail:e});this.eventTarget.dispatchEvent(i)}on(t,e,i){const n=t=>{t instanceof CustomEvent&&e(t.detail)},s=String(t);return this.eventTarget.addEventListener(s,n,{once:i}),()=>this.eventTarget.removeEventListener(s,n)}once(t,e){return this.on(t,e,!0)}};const n=class extends i{constructor(t){super(),this.timeout=null,this.options=t;let e=null;if("string"==typeof this.options.container?e=document.querySelector(this.options.container):this.options.container instanceof HTMLElement&&(e=this.options.container),!e)throw new Error("Container not found");const i=document.createElement("div"),n=i.attachShadow({mode:"open"});n.innerHTML=`\n <style>\n :host {\n user-select: none;\n }\n :host .scroll {\n overflow-x: auto;\n overflow-y: visible;\n width: 100%;\n height: ${this.options.height}px;\n position: relative;\n }\n :host .wrapper {\n position: relative;\n width: fit-content;\n min-width: 100%;\n height: 100%;\n z-index: 2;\n }\n :host canvas {\n display: block;\n height: 100%;\n min-width: 100%;\n image-rendering: pixelated;\n }\n :host .progress {\n position: absolute;\n z-index: 2;\n top: 0;\n left: 0;\n height: 100%;\n pointer-events: none;\n clip-path: inset(100%);\n }\n :host .cursor {\n position: absolute;\n z-index: 3;\n top: 0;\n left: 0;\n height: 100%;\n border-right: 1px solid ${this.options.progressColor};\n margin-left: -1px;\n }\n </style>\n\n <div class="scroll">\n <div class="wrapper">\n <canvas></canvas>\n <canvas class="progress"></canvas>\n <div class="cursor"></div>\n </div>\n </div>\n `,this.container=i,this.scrollContainer=n.querySelector(".scroll"),this.mainCanvas=n.querySelector("canvas"),this.ctx=this.mainCanvas.getContext("2d",{desynchronized:!0}),this.progressCanvas=n.querySelector(".progress"),this.cursor=n.querySelector(".cursor"),e.appendChild(i),this.mainCanvas.addEventListener("click",(t=>{const e=this.mainCanvas.getBoundingClientRect(),i=(t.clientX-e.left)/e.width;this.emit("click",{relativeX:i})}))}getContainer(){return this.scrollContainer.querySelector(".wrapper")}destroy(){this.container.remove()}delay(t,e=100){return this.timeout&&clearTimeout(this.timeout),new Promise((i=>{this.timeout=setTimeout((()=>{i(t())}),e)}))}renderPeaks(t,e,i){var n,s,r,o,a;return s=this,r=void 0,a=function*(){const{devicePixelRatio:s}=window,{ctx:r}=this,o=null!=this.options.barWidth?this.options.barWidth*s:1,a=null!=this.options.barGap?this.options.barGap*s:this.options.barWidth?o/2:0,h=null!==(n=this.options.barRadius)&&void 0!==n?n:0,l=t[0],c=l.length,d=Math.floor(e/(o+a))/c,u=i/2,p=1===t.length,m=p?l:t[1],v=p&&m.some((t=>t<0)),y=(t,e)=>{let i=0,n=0,s=0;r.beginPath();for(let c=t;c<e;c++){const t=Math.round(c*d);if(t>i){const e=Math.round(n*u),l=Math.round(s*u);r.roundRect(i*(o+a),u-e,o,e+l||1,h),i=t,n=0,s=0}const e=v?l[c]:Math.abs(l[c]),p=v?m[c]:Math.abs(m[c]);e>n&&(n=e),(v?p<-s:p>s)&&(s=p<0?-p:p)}r.fillStyle=this.options.waveColor,r.fill(),r.closePath()};r.clearRect(0,0,e,i);const{scrollLeft:g,scrollWidth:f,clientWidth:C}=this.scrollContainer,w=c/f,b=Math.floor(g*w),x=Math.ceil((g+C)*w);y(b,x),this.createProgressMask(),b>0&&(yield this.delay((()=>{y(0,b)}))),x<c&&(yield this.delay((()=>{y(x,c)}))),this.delay((()=>{this.createProgressMask()}))},new((o=void 0)||(o=Promise))((function(t,e){function i(t){try{h(a.next(t))}catch(t){e(t)}}function n(t){try{h(a.throw(t))}catch(t){e(t)}}function h(e){var s;e.done?t(e.value):(s=e.value,s instanceof o?s:new o((function(t){t(s)}))).then(i,n)}h((a=a.apply(s,r||[])).next())}))}createProgressMask(){const t=this.progressCanvas.getContext("2d",{desynchronized:!0});this.progressCanvas.width=this.mainCanvas.width,this.progressCanvas.height=this.mainCanvas.height,this.progressCanvas.style.width=this.mainCanvas.style.width,this.progressCanvas.style.height=this.mainCanvas.style.height,t.drawImage(this.mainCanvas,0,0),t.globalCompositeOperation="source-in",t.fillStyle=this.options.progressColor,t.fillRect(0,0,this.progressCanvas.width,this.progressCanvas.height)}render(t,e){const{devicePixelRatio:i}=window,n=this.scrollContainer.clientWidth*i,s=e*this.options.minPxPerSec,r=s>n,o=r?s:n,{height:a}=this.options;this.mainCanvas.width=o,this.mainCanvas.height=a,this.mainCanvas.style.width=r?Math.round(s/i)+"px":"100%",this.renderPeaks(t,o,a)}zoom(t,e,i){const n=this.cursor.getBoundingClientRect().left;this.options.minPxPerSec=i,this.render(t,e);const s=this.cursor.getBoundingClientRect().left;this.scrollContainer.scrollLeft+=s-n}renderProgress(t,e=!1){if(this.progressCanvas.style.clipPath=`inset(0 ${100-100*t}% 0 0)`,this.cursor.style.left=100*t+"%",e){const e=this.container.clientWidth/2,i=this.mainCanvas.clientWidth;i*t>=e&&(this.scrollContainer.scrollLeft=i*t-e)}}},s=class extends i{constructor(){super(...arguments),this.unsubscribe=()=>{}}start(){this.unsubscribe=this.on("tick",(()=>{requestAnimationFrame((()=>{this.emit("tick")}))})),this.emit("tick")}stop(){this.unsubscribe()}destroy(){this.unsubscribe()}};const r={height:128,waveColor:"#999",progressColor:"#555",minPxPerSec:0};class o extends i{static create(t){return new o(t)}constructor(t){var e;super(),this.plugins=[],this.subscriptions=[],this.channelData=null,this.duration=0,this.options=Object.assign({},r,t),this.fetcher=new class{load(t){return e=this,i=void 0,s=function*(){return fetch(t).then((t=>t.arrayBuffer()))},new((n=void 0)||(n=Promise))((function(t,r){function o(t){try{h(s.next(t))}catch(t){r(t)}}function a(t){try{h(s.throw(t))}catch(t){r(t)}}function h(e){var i;e.done?t(e.value):(i=e.value,i instanceof n?i:new n((function(t){t(i)}))).then(o,a)}h((s=s.apply(e,i||[])).next())}));var e,i,n,s}},this.decoder=new class{initAudioContext(t){this.audioCtx=new AudioContext({latencyHint:"playback",sampleRate:t})}constructor(){this.audioCtx=null;try{this.initAudioContext(3e3)}catch(t){this.initAudioContext(8e3)}}decode(t){return e=this,i=void 0,s=function*(){if(!this.audioCtx)throw new Error("AudioContext is not initialized");const e=yield this.audioCtx.decodeAudioData(t),i=[e.getChannelData(0)];return e.numberOfChannels>1&&i.push(e.getChannelData(1)),{duration:e.duration,channelData:i}},new((n=void 0)||(n=Promise))((function(t,r){function o(t){try{h(s.next(t))}catch(t){r(t)}}function a(t){try{h(s.throw(t))}catch(t){r(t)}}function h(e){var i;e.done?t(e.value):(i=e.value,i instanceof n?i:new n((function(t){t(i)}))).then(o,a)}h((s=s.apply(e,i||[])).next())}));var e,i,n,s}destroy(){var t;null===(t=this.audioCtx)||void 0===t||t.close(),this.audioCtx=null}},this.timer=new s,this.player=new class{constructor({media:t,autoplay:e}){this.isExternalMedia=!1,this.hasPlayedOnce=!1,t?(this.media=t,this.isExternalMedia=!0):this.media=document.createElement("audio");const i=this.on("play",(()=>{this.hasPlayedOnce=!0,i()}));e&&(this.media.autoplay=!0)}on(t,e){var i;return null===(i=this.media)||void 0===i||i.addEventListener(t,e),()=>{var i;return null===(i=this.media)||void 0===i?void 0:i.removeEventListener(t,e)}}destroy(){var t,e;null===(t=this.media)||void 0===t||t.pause(),this.isExternalMedia||null===(e=this.media)||void 0===e||e.remove()}loadUrl(t){this.media&&(this.media.src=t)}getCurrentTime(){return this.media.currentTime}play(){this.media.play()}pause(){this.media.pause()}isPlaying(){return this.media.currentTime>0&&!this.media.paused&&!this.media.ended}seekTo(t){if(this.media){const{hasPlayedOnce:e}=this;e||this.media.play(),this.media.currentTime=t,e||this.media.pause()}}getVolume(){return this.media.volume}setVolume(t){this.media.volume=t}getMuted(){return this.media.muted}setMuted(t){this.media.muted=t}getPlaybackRate(){return this.media.playbackRate}setPlaybackRate(t,e){null!=e&&(this.media.preservesPitch=e),this.media.playbackRate=t}}({media:this.options.media,autoplay:this.options.autoplay}),this.renderer=new n({container:this.options.container,height:this.options.height,waveColor:this.options.waveColor,progressColor:this.options.progressColor,minPxPerSec:this.options.minPxPerSec,barWidth:this.options.barWidth,barGap:this.options.barGap,barRadius:this.options.barRadius}),this.initPlayerEvents(),this.initRendererEvents(),this.initTimerEvents();const i=this.options.url||(null===(e=this.options.media)||void 0===e?void 0:e.src);i&&this.load(i,this.options.channelData,this.options.duration)}initPlayerEvents(){this.subscriptions.push(this.player.on("timeupdate",(()=>{const t=this.getCurrentTime();this.renderer.renderProgress(t/this.duration,this.isPlaying()),this.emit("audioprocess",{currentTime:t})})),this.player.on("play",(()=>{this.emit("play"),this.timer.start()})),this.player.on("pause",(()=>{this.emit("pause"),this.timer.stop()})),this.player.on("canplay",(()=>{this.emit("canplay",{duration:this.getDuration()})})),this.player.on("seeking",(()=>{this.emit("seek",{time:this.getCurrentTime()})})))}initRendererEvents(){this.subscriptions.push(this.renderer.on("click",(({relativeX:t})=>{const e=this.getDuration()*t;this.seekTo(e)})))}initTimerEvents(){this.subscriptions.push(this.timer.on("tick",(()=>{const t=this.getCurrentTime();this.renderer.renderProgress(t/this.duration,!0),this.emit("audioprocess",{currentTime:t})})))}destroy(){this.subscriptions.forEach((t=>t())),this.plugins.forEach((t=>t.destroy())),this.timer.destroy(),this.player.destroy(),this.decoder.destroy(),this.renderer.destroy()}load(t,e,i){return n=this,s=void 0,o=function*(){if(this.player.loadUrl(t),null==e||null==i){const n=yield this.fetcher.load(t),s=yield this.decoder.decode(n);e=s.channelData,i=s.duration}this.channelData=e,this.duration=i,this.renderer.render(this.channelData,this.duration),this.emit("decode",{duration:this.duration})},new((r=void 0)||(r=Promise))((function(t,e){function i(t){try{h(o.next(t))}catch(t){e(t)}}function a(t){try{h(o.throw(t))}catch(t){e(t)}}function h(e){var n;e.done?t(e.value):(n=e.value,n instanceof r?n:new r((function(t){t(n)}))).then(i,a)}h((o=o.apply(n,s||[])).next())}));var n,s,r,o}zoom(t){if(null==this.channelData||null==this.duration)throw new Error("No audio loaded");this.renderer.zoom(this.channelData,this.duration,t)}play(){this.player.play()}pause(){this.player.pause()}seekTo(t){this.player.seekTo(t)}isPlaying(){return this.player.isPlaying()}getDuration(){return this.duration}getCurrentTime(){return this.player.getCurrentTime()}getVolume(){return this.player.getVolume()}setVolume(t){this.player.setVolume(t)}getMuted(){return this.player.getMuted()}setMuted(t){this.player.setMuted(t)}getPlaybackRate(){return this.player.getPlaybackRate()}setPlaybackRate(t,e){this.player.setPlaybackRate(t,e)}registerPlugin(t){const e=new t({wavesurfer:this,container:this.renderer.getContainer()});return this.plugins.push(e),e}getAudioData(){return this.channelData}}const a=o;return e.default})()));
|
package/package.json
CHANGED
|
@@ -1,25 +1,31 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wavesurfer.js",
|
|
3
|
-
"version": "7.0.0-alpha.
|
|
3
|
+
"version": "7.0.0-alpha.4",
|
|
4
4
|
"description": "wavesurfer.js is an audio library that draws waveforms and plays audio in the browser.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
|
-
"files": [
|
|
7
|
+
"files": [
|
|
8
|
+
"dist/"
|
|
9
|
+
],
|
|
8
10
|
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
9
12
|
"build:dev": "tsc -w",
|
|
10
|
-
"build:umd": "webpack && webpack --config webpack.config.plugins.js",
|
|
13
|
+
"build:umd": "tsc && webpack && webpack --config webpack.config.plugins.js",
|
|
11
14
|
"prepublishOnly": "npm run build:umd",
|
|
12
15
|
"test": "echo \"Error: no test specified\" && exit 1",
|
|
13
16
|
"lint": "eslint --ext .ts src --fix",
|
|
14
17
|
"prettier": "prettier -w src",
|
|
15
18
|
"docs": "npx typedoc src/index.ts",
|
|
16
|
-
"serve": "
|
|
19
|
+
"serve": "browser-sync start --server --port 9090 --startPath / --watch '*'",
|
|
20
|
+
"start": "npm run build:dev & npm run serve"
|
|
17
21
|
},
|
|
18
22
|
"author": "katspaugh <katspaugh@gmail.com>",
|
|
19
23
|
"license": "ISC",
|
|
20
24
|
"devDependencies": {
|
|
25
|
+
"@types/react": "^18.0.28",
|
|
21
26
|
"@typescript-eslint/eslint-plugin": "^5.54.0",
|
|
22
27
|
"@typescript-eslint/parser": "^5.54.0",
|
|
28
|
+
"browser-sync": "^2.29.0",
|
|
23
29
|
"eslint": "^8.35.0",
|
|
24
30
|
"eslint-config-prettier": "^8.7.0",
|
|
25
31
|
"eslint-plugin-prettier": "^4.2.1",
|