wavesurfer.js 7.0.0-alpha.3 → 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 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
- * Allow swapping the decoding, rendering and playback mechanisms
22
+ * Flexible (e.g. allow custom media elements and user-defined Web Audio graphs)
21
23
  * Extensible with plugins
22
24
 
23
- ![diagram](https://user-images.githubusercontent.com/381895/222349436-38b550e5-24dc-4143-9cdb-efbe00540213.png)
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
- python3 -m http.server --cgi 8080
36
+ yarn start
43
37
  ```
44
38
 
45
- Open http://localhost:8080/tutorial in your browser.
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
- See https://github.com/wavesurfer-js/wavesurfer.js/discussions/2684
43
+ Your feedback is very welcome here: https://github.com/wavesurfer-js/wavesurfer.js/discussions/2684
@@ -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 renderer: WaveSurfer['renderer'];
5
+ protected container: HTMLElement;
6
6
  protected subscriptions: (() => void)[];
7
7
  constructor(params: WaveSurferPluginParams);
8
8
  destroy(): void;
@@ -4,7 +4,7 @@ export class BasePlugin extends EventEmitter {
4
4
  super();
5
5
  this.subscriptions = [];
6
6
  this.wavesurfer = params.wavesurfer;
7
- this.renderer = params.renderer;
7
+ this.container = params.container;
8
8
  }
9
9
  destroy() {
10
10
  this.subscriptions.forEach((unsubscribe) => unsubscribe());
package/dist/decoder.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  declare class Decoder {
2
2
  audioCtx: AudioContext | null;
3
+ private initAudioContext;
3
4
  constructor();
4
5
  decode(audioData: ArrayBuffer): Promise<{
5
6
  duration: number;
package/dist/decoder.js CHANGED
@@ -7,16 +7,25 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
7
7
  step((generator = generator.apply(thisArg, _arguments || [])).next());
8
8
  });
9
9
  };
10
- // Web Audio decodeAudioData with a minimum allowed sample rate
11
- const SAMPLE_RATE = 3000;
12
10
  class Decoder {
13
- constructor() {
14
- this.audioCtx = null;
11
+ initAudioContext(sampleRate) {
15
12
  this.audioCtx = new AudioContext({
16
13
  latencyHint: 'playback',
17
- sampleRate: SAMPLE_RATE,
14
+ sampleRate,
18
15
  });
19
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
+ }
20
29
  decode(audioData) {
21
30
  return __awaiter(this, void 0, void 0, function* () {
22
31
  if (!this.audioCtx) {
@@ -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
  }
@@ -7,23 +7,19 @@ class EventEmitter {
7
7
  this.eventTarget.dispatchEvent(e);
8
8
  }
9
9
  /** Subscribe to an event and return a function to unsubscribe */
10
- on(eventType, callback) {
10
+ on(eventType, callback, once) {
11
11
  const handler = (e) => {
12
12
  if (e instanceof CustomEvent) {
13
13
  callback(e.detail);
14
14
  }
15
15
  };
16
16
  const eventName = String(eventType);
17
- this.eventTarget.addEventListener(eventName, handler);
17
+ this.eventTarget.addEventListener(eventName, handler, { once });
18
18
  return () => this.eventTarget.removeEventListener(eventName, handler);
19
19
  }
20
20
  /** Subscribe to an event once and return a function to unsubscribe */
21
21
  once(eventType, callback) {
22
- const unsubscribe = this.on(eventType, (...args) => {
23
- unsubscribe();
24
- callback(...args);
25
- });
26
- return unsubscribe;
22
+ return this.on(eventType, callback, true);
27
23
  }
28
24
  }
29
25
  export default EventEmitter;
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
- ready: {
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
- renderer: WaveSurfer['renderer'];
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 CHANGED
@@ -11,20 +11,13 @@ import Fetcher from './fetcher.js';
11
11
  import Decoder from './decoder.js';
12
12
  import Renderer from './renderer.js';
13
13
  import Player from './player.js';
14
- import WebAudioPlayer from './player-webaudio.js';
15
14
  import EventEmitter from './event-emitter.js';
16
15
  import Timer from './timer.js';
17
- export var PlayerType;
18
- (function (PlayerType) {
19
- PlayerType["WebAudio"] = "WebAudio";
20
- PlayerType["MediaElement"] = "MediaElement";
21
- })(PlayerType || (PlayerType = {}));
22
16
  const defaultOptions = {
23
17
  height: 128,
24
18
  waveColor: '#999',
25
19
  progressColor: '#555',
26
20
  minPxPerSec: 0,
27
- backend: 'MediaElement',
28
21
  };
29
22
  export class WaveSurfer extends EventEmitter {
30
23
  /** Create a new WaveSurfer instance */
@@ -43,8 +36,9 @@ export class WaveSurfer extends EventEmitter {
43
36
  this.fetcher = new Fetcher();
44
37
  this.decoder = new Decoder();
45
38
  this.timer = new Timer();
46
- this.player = new (this.options.backend === PlayerType.WebAudio ? WebAudioPlayer : Player)({
39
+ this.player = new Player({
47
40
  media: this.options.media,
41
+ autoplay: this.options.autoplay,
48
42
  });
49
43
  this.renderer = new Renderer({
50
44
  container: this.options.container,
@@ -71,10 +65,12 @@ export class WaveSurfer extends EventEmitter {
71
65
  this.emit('audioprocess', { currentTime });
72
66
  }), this.player.on('play', () => {
73
67
  this.emit('play');
68
+ this.timer.start();
74
69
  }), this.player.on('pause', () => {
75
70
  this.emit('pause');
71
+ this.timer.stop();
76
72
  }), this.player.on('canplay', () => {
77
- this.emit('canplay');
73
+ this.emit('canplay', { duration: this.getDuration() });
78
74
  }), this.player.on('seeking', () => {
79
75
  this.emit('seek', { time: this.getCurrentTime() });
80
76
  }));
@@ -89,11 +85,9 @@ export class WaveSurfer extends EventEmitter {
89
85
  initTimerEvents() {
90
86
  // The timer fires every 16ms for a smooth progress animation
91
87
  this.subscriptions.push(this.timer.on('tick', () => {
92
- if (this.isPlaying()) {
93
- const currentTime = this.getCurrentTime();
94
- this.renderer.renderProgress(currentTime / this.duration, true);
95
- this.emit('audioprocess', { currentTime });
96
- }
88
+ const currentTime = this.getCurrentTime();
89
+ this.renderer.renderProgress(currentTime / this.duration, true);
90
+ this.emit('audioprocess', { currentTime });
97
91
  }));
98
92
  }
99
93
  /** Unmount wavesurfer */
@@ -105,7 +99,7 @@ export class WaveSurfer extends EventEmitter {
105
99
  this.decoder.destroy();
106
100
  this.renderer.destroy();
107
101
  }
108
- /** Load an audio file by URL */
102
+ /** Load an audio file by URL, with optional pre-decoded audio data */
109
103
  load(url, channelData, duration) {
110
104
  return __awaiter(this, void 0, void 0, function* () {
111
105
  this.player.loadUrl(url);
@@ -119,7 +113,7 @@ export class WaveSurfer extends EventEmitter {
119
113
  this.channelData = channelData;
120
114
  this.duration = duration;
121
115
  this.renderer.render(this.channelData, this.duration);
122
- this.emit('ready', { duration: this.duration });
116
+ this.emit('decode', { duration: this.duration });
123
117
  });
124
118
  }
125
119
  /** Zoom in or out */
@@ -153,14 +147,42 @@ export class WaveSurfer extends EventEmitter {
153
147
  getCurrentTime() {
154
148
  return this.player.getCurrentTime();
155
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
+ }
156
174
  /** Register and initialize a plugin */
157
175
  registerPlugin(CustomPlugin) {
158
176
  const plugin = new CustomPlugin({
159
177
  wavesurfer: this,
160
- renderer: this.renderer,
178
+ container: this.renderer.getContainer(),
161
179
  });
162
180
  this.plugins.push(plugin);
163
181
  return plugin;
164
182
  }
183
+ /** Get the decoded audio data */
184
+ getAudioData() {
185
+ return this.channelData;
186
+ }
165
187
  }
166
188
  export default WaveSurfer;
@@ -23,8 +23,10 @@ class WebAudioPlayer extends Player {
23
23
  if (this.sourceNode) {
24
24
  this.sourceNode.disconnect();
25
25
  }
26
- this.sourceNode = this.audioCtx.createMediaElementSource(this.media);
27
- this.sourceNode.connect(this.audioCtx.destination);
26
+ if (this.media) {
27
+ this.sourceNode = this.audioCtx.createMediaElementSource(this.media);
28
+ this.sourceNode.connect(this.audioCtx.destination);
29
+ }
28
30
  }
29
31
  }
30
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
- constructor({ media }: {
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 CHANGED
@@ -1,6 +1,7 @@
1
1
  class Player {
2
- constructor({ media }) {
2
+ constructor({ media, autoplay }) {
3
3
  this.isExternalMedia = false;
4
+ this.hasPlayedOnce = false;
4
5
  if (media) {
5
6
  this.media = media;
6
7
  this.isExternalMedia = true;
@@ -8,18 +9,32 @@ class Player {
8
9
  else {
9
10
  this.media = document.createElement('audio');
10
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
+ }
11
21
  }
12
22
  on(event, callback) {
13
- this.media.addEventListener(event, callback);
14
- return () => this.media.removeEventListener(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); };
15
26
  }
16
27
  destroy() {
28
+ var _a, _b;
29
+ (_a = this.media) === null || _a === void 0 ? void 0 : _a.pause();
17
30
  if (!this.isExternalMedia) {
18
- this.media.remove();
31
+ (_b = this.media) === null || _b === void 0 ? void 0 : _b.remove();
19
32
  }
20
33
  }
21
34
  loadUrl(src) {
22
- this.media.src = src;
35
+ if (this.media) {
36
+ this.media.src = src;
37
+ }
23
38
  }
24
39
  getCurrentTime() {
25
40
  return this.media.currentTime;
@@ -34,7 +49,39 @@ class Player {
34
49
  return this.media.currentTime > 0 && !this.media.paused && !this.media.ended;
35
50
  }
36
51
  seekTo(time) {
37
- this.media.currentTime = 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;
38
85
  }
39
86
  }
40
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 container;
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 initContainer;
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
- addRegionAtTime(startTime: number, endTime: number, title?: string): Region;
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
  }
@@ -1,5 +1,15 @@
1
1
  import BasePlugin from '../base-plugin.js';
2
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
+ };
3
13
  class RegionsPlugin extends BasePlugin {
4
14
  /** Create an instance of RegionsPlugin */
5
15
  constructor(params) {
@@ -25,10 +35,10 @@ class RegionsPlugin extends BasePlugin {
25
35
  return;
26
36
  }
27
37
  if (!isNaN(this.dragStart)) {
28
- const dragEnd = e.clientX - this.container.getBoundingClientRect().left;
38
+ const dragEnd = e.clientX - this.wrapper.getBoundingClientRect().left;
29
39
  if (dragEnd - this.dragStart >= MIN_WIDTH) {
30
40
  if (!this.createdRegion) {
31
- this.renderer.getContainer().style.pointerEvents = 'none';
41
+ this.container.style.pointerEvents = 'none';
32
42
  this.createdRegion = this.createRegion(this.dragStart, dragEnd);
33
43
  }
34
44
  else {
@@ -45,97 +55,97 @@ class RegionsPlugin extends BasePlugin {
45
55
  this.modifiedRegion = null;
46
56
  this.isMoving = false;
47
57
  this.dragStart = NaN;
48
- this.renderer.getContainer().style.pointerEvents = '';
58
+ this.container.style.pointerEvents = '';
49
59
  };
50
- this.container = this.initContainer();
51
- const unsubscribeReady = this.wavesurfer.on('ready', () => {
52
- const wrapper = this.renderer.getContainer();
53
- wrapper.appendChild(this.container);
54
- unsubscribeReady();
60
+ this.wrapper = this.initWrapper();
61
+ const unsubscribe = this.wavesurfer.on('decode', () => {
62
+ this.container.appendChild(this.wrapper);
63
+ unsubscribe();
55
64
  });
56
- this.subscriptions.push(unsubscribeReady);
57
- const wsContainer = this.renderer.getContainer();
58
- wsContainer.addEventListener('mousedown', this.handleMouseDown);
65
+ this.subscriptions.push(unsubscribe);
66
+ this.container.addEventListener('mousedown', this.handleMouseDown);
59
67
  document.addEventListener('mousemove', this.handleMouseMove);
60
68
  document.addEventListener('mouseup', this.handleMouseUp);
61
69
  }
62
70
  /** Unmounts the regions */
63
71
  destroy() {
64
- var _a;
65
- this.renderer.getContainer().removeEventListener('mousedown', this.handleMouseDown);
72
+ this.container.removeEventListener('mousedown', this.handleMouseDown);
66
73
  document.removeEventListener('mousemove', this.handleMouseMove);
67
74
  document.removeEventListener('mouseup', this.handleMouseUp, true);
68
- (_a = this.container) === null || _a === void 0 ? void 0 : _a.remove();
75
+ this.wrapper.remove();
69
76
  super.destroy();
70
77
  }
71
- initContainer() {
72
- const div = document.createElement('div');
73
- div.style.position = 'absolute';
74
- div.style.top = '0';
75
- div.style.left = '0';
76
- div.style.width = '100%';
77
- div.style.height = '100%';
78
- div.style.zIndex = '3';
79
- div.style.pointerEvents = 'none';
80
- return div;
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
+ });
81
88
  }
82
89
  createRegionElement(start, end, title = '') {
83
- const el = document.createElement('div');
84
- el.style.position = 'absolute';
85
- el.style.left = `${start}px`;
86
- el.style.width = `${end - start}px`;
87
- el.style.height = '100%';
88
- el.style.backgroundColor = 'rgba(0, 0, 0, 0.1)';
89
- el.style.transition = 'background-color 0.2s ease';
90
- el.style.cursor = 'move';
91
- el.style.pointerEvents = 'all';
92
- el.title = title;
93
- const leftHandle = document.createElement('div');
94
- leftHandle.style.position = 'absolute';
95
- leftHandle.style.left = '0';
96
- leftHandle.style.width = '6px';
97
- leftHandle.style.height = '100%';
98
- leftHandle.style.borderLeft = '2px solid rgba(0, 0, 0, 0.5)';
99
- leftHandle.style.cursor = 'ew-resize';
100
- leftHandle.style.pointerEvents = 'all';
101
- el.appendChild(leftHandle);
102
- const rightHandle = document.createElement('div');
103
- rightHandle.style.position = 'absolute';
104
- rightHandle.style.right = '0';
105
- rightHandle.style.width = '6px';
106
- rightHandle.style.height = '100%';
107
- rightHandle.style.borderRight = '2px solid rgba(0, 0, 0, 0.5)';
108
- rightHandle.style.cursor = 'ew-resize';
109
- rightHandle.style.pointerEvents = 'all';
110
- el.appendChild(rightHandle);
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);
111
121
  leftHandle.addEventListener('mousedown', (e) => {
112
122
  e.stopPropagation();
113
- this.modifiedRegion = this.regions.find((r) => r.element === el) || null;
123
+ this.modifiedRegion = this.regions.find((r) => r.element === div) || null;
114
124
  this.isResizingLeft = true;
115
125
  this.isMoving = false;
116
126
  });
117
127
  rightHandle.addEventListener('mousedown', (e) => {
118
128
  e.stopPropagation();
119
- this.modifiedRegion = this.regions.find((r) => r.element === el) || null;
129
+ this.modifiedRegion = this.regions.find((r) => r.element === div) || null;
120
130
  this.isResizingLeft = false;
121
131
  this.isMoving = false;
122
132
  });
123
- el.addEventListener('mousedown', () => {
124
- this.modifiedRegion = this.regions.find((r) => r.element === el) || null;
133
+ div.addEventListener('mousedown', () => {
134
+ this.modifiedRegion = this.regions.find((r) => r.element === div) || null;
125
135
  this.isMoving = true;
126
136
  });
127
- el.addEventListener('click', () => {
128
- const region = this.regions.find((r) => r.element === el);
137
+ div.addEventListener('click', () => {
138
+ const region = this.regions.find((r) => r.element === div);
129
139
  if (region) {
130
140
  this.emit('region-clicked', { region });
131
141
  }
132
142
  });
133
- this.container.appendChild(el);
134
- return el;
143
+ this.wrapper.appendChild(div);
144
+ return div;
135
145
  }
136
146
  createRegion(start, end, title = '') {
137
147
  const duration = this.wavesurfer.getDuration();
138
- const width = this.container.clientWidth;
148
+ const width = this.wrapper.clientWidth;
139
149
  return {
140
150
  element: this.createRegionElement(start, end, title),
141
151
  start,
@@ -153,12 +163,12 @@ class RegionsPlugin extends BasePlugin {
153
163
  if (start != null) {
154
164
  region.start = start !== null && start !== void 0 ? start : region.start;
155
165
  region.element.style.left = `${region.start}px`;
156
- region.startTime = (start / this.container.clientWidth) * this.wavesurfer.getDuration();
166
+ region.startTime = (start / this.wrapper.clientWidth) * this.wavesurfer.getDuration();
157
167
  }
158
168
  if (end != null) {
159
169
  region.end = end;
160
170
  region.element.style.width = `${region.end - region.start}px`;
161
- region.endTime = (end / this.container.clientWidth) * this.wavesurfer.getDuration();
171
+ region.endTime = (end / this.wrapper.clientWidth) * this.wavesurfer.getDuration();
162
172
  }
163
173
  this.emit('region-updated', { region });
164
174
  }
@@ -166,9 +176,9 @@ class RegionsPlugin extends BasePlugin {
166
176
  this.updateRegion(region, region.start + delta, region.end + delta);
167
177
  }
168
178
  /** Create a region at a given start and end time, with an optional title */
169
- addRegionAtTime(startTime, endTime, title = '') {
179
+ add(startTime, endTime, title = '') {
170
180
  const duration = this.wavesurfer.getDuration();
171
- const width = this.container.clientWidth;
181
+ const width = this.wrapper.clientWidth;
172
182
  const start = (startTime / duration) * width;
173
183
  const end = (endTime / duration) * width;
174
184
  const region = this.createRegion(start, end, title);
@@ -0,0 +1,5 @@
1
+ import WaveSurfer, { type WaveSurferOptions } from '../index.js';
2
+ export declare const useWavesurfer: (containerRef: {
3
+ current: HTMLElement;
4
+ }, options: WaveSurferOptions) => WaveSurfer | null;
5
+ export default useWavesurfer;
@@ -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;
@@ -22,14 +22,15 @@ declare class Renderer extends EventEmitter<RendererEvents> {
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 renderLinePeaks;
28
- private renderBarPeaks;
29
+ private delay;
30
+ private renderPeaks;
29
31
  private createProgressMask;
30
32
  render(channelData: Float32Array[], duration: number): void;
31
33
  zoom(channelData: Float32Array[], duration: number, minPxPerSec: number): void;
32
34
  renderProgress(progress: number, autoCenter?: boolean): void;
33
- getContainer(): HTMLElement;
34
35
  }
35
36
  export default Renderer;
package/dist/renderer.js CHANGED
@@ -1,7 +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
+ };
1
10
  import EventEmitter from './event-emitter.js';
2
11
  class Renderer extends EventEmitter {
3
12
  constructor(options) {
4
13
  super();
14
+ this.timeout = null;
5
15
  this.options = options;
6
16
  let container = null;
7
17
  if (typeof this.options.container === 'string') {
@@ -17,10 +27,12 @@ class Renderer extends EventEmitter {
17
27
  const shadow = div.attachShadow({ mode: 'open' });
18
28
  shadow.innerHTML = `
19
29
  <style>
30
+ :host {
31
+ user-select: none;
32
+ }
20
33
  :host .scroll {
21
34
  overflow-x: auto;
22
35
  overflow-y: visible;
23
- user-select: none;
24
36
  width: 100%;
25
37
  height: ${this.options.height}px;
26
38
  position: relative;
@@ -54,6 +66,7 @@ class Renderer extends EventEmitter {
54
66
  left: 0;
55
67
  height: 100%;
56
68
  border-right: 1px solid ${this.options.progressColor};
69
+ margin-left: -1px;
57
70
  }
58
71
  </style>
59
72
 
@@ -79,101 +92,123 @@ class Renderer extends EventEmitter {
79
92
  this.emit('click', { relativeX });
80
93
  });
81
94
  }
95
+ getContainer() {
96
+ return this.scrollContainer.querySelector('.wrapper');
97
+ }
82
98
  destroy() {
83
99
  this.container.remove();
84
100
  }
85
- renderLinePeaks(channelData, width, height) {
86
- const { ctx } = this;
87
- ctx.clearRect(0, 0, width, height);
88
- ctx.beginPath();
89
- ctx.moveTo(0, height / 2);
90
- // Channel 0 is left, 1 is right
91
- const leftChannel = channelData[0];
92
- const isMono = channelData.length === 1;
93
- const rightChannel = isMono ? leftChannel : channelData[1];
94
- // Draw left channel in the top half of the canvas
95
- let prevX = -1;
96
- let prevY = 0;
97
- for (let i = 0, len = leftChannel.length; i < len; i++) {
98
- const x = Math.round((i / leftChannel.length) * width);
99
- const y = Math.round(((1 - leftChannel[i]) * height) / 2);
100
- if (x !== prevX || y > prevY) {
101
- ctx.lineTo(x, y);
102
- prevX = x;
103
- prevY = y;
104
- }
105
- }
106
- // Draw right channel in the bottom half of the canvas
107
- prevX = -1;
108
- prevY = 0;
109
- for (let i = rightChannel.length - 1; i >= 0; i--) {
110
- const x = Math.round((i / rightChannel.length) * width);
111
- const y = Math.round(((1 + rightChannel[i]) * height) / 2);
112
- if (x !== prevX || (isMono ? y < -prevY : y > prevY)) {
113
- ctx.lineTo(x, y);
114
- prevX = x;
115
- prevY = y;
116
- }
101
+ delay(fn, delayMs = 100) {
102
+ if (this.timeout) {
103
+ clearTimeout(this.timeout);
117
104
  }
118
- ctx.strokeStyle = ctx.fillStyle = this.options.waveColor;
119
- ctx.stroke();
120
- ctx.fill();
105
+ return new Promise((resolve) => {
106
+ this.timeout = setTimeout(() => {
107
+ resolve(fn());
108
+ }, delayMs);
109
+ });
121
110
  }
122
- renderBarPeaks(channelData, width, height) {
123
- var _a, _b;
124
- const { devicePixelRatio } = window;
125
- const { ctx } = this;
126
- ctx.clearRect(0, 0, width, height);
127
- const barWidthOption = this.options.barWidth || 1;
128
- const barWidth = barWidthOption * devicePixelRatio;
129
- const barGap = ((_a = this.options.barGap) !== null && _a !== void 0 ? _a : barWidthOption / 2) * devicePixelRatio;
130
- const barRadius = (_b = this.options.barRadius) !== null && _b !== void 0 ? _b : 0;
131
- const leftChannel = channelData[0];
132
- const isMono = channelData.length === 1;
133
- const rightChannel = isMono ? leftChannel : channelData[1];
134
- const barCount = Math.floor(width / (barWidth + barGap));
135
- const leftChannelBars = new Float32Array(barCount);
136
- const rightChannelBars = new Float32Array(barCount);
137
- const barIndexScale = barCount / leftChannel.length;
138
- const leftChannelLength = leftChannel.length;
139
- for (let i = 0; i < leftChannelLength; i++) {
140
- const barIndex = Math.round(i * barIndexScale);
141
- leftChannelBars[barIndex] = Math.max(leftChannelBars[barIndex], leftChannel[i]);
142
- rightChannelBars[barIndex] = (isMono ? Math.min : Math.max)(rightChannelBars[barIndex], rightChannel[i]);
143
- }
144
- ctx.beginPath();
145
- for (let i = 0; i < barCount; i++) {
146
- const leftBarHeight = Math.max(1, Math.round((leftChannelBars[i] * height) / 2));
147
- const rightBarHeight = Math.max(1, Math.round(Math.abs(rightChannelBars[i] * height) / 2));
148
- ctx.roundRect(i * (barWidth + barGap), height / 2 - leftBarHeight, barWidth, leftBarHeight + rightBarHeight, barRadius);
149
- }
150
- ctx.fillStyle = this.options.waveColor;
151
- ctx.fill();
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
+ });
152
183
  }
153
184
  createProgressMask() {
154
185
  const progressCtx = this.progressCanvas.getContext('2d', { desynchronized: true });
155
- if (!progressCtx) {
156
- throw new Error('Failed to get canvas context');
157
- }
186
+ // Set the canvas to the same size as the main canvas
158
187
  this.progressCanvas.width = this.mainCanvas.width;
159
188
  this.progressCanvas.height = this.mainCanvas.height;
160
189
  this.progressCanvas.style.width = this.mainCanvas.style.width;
161
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
162
193
  progressCtx.drawImage(this.mainCanvas, 0, 0);
194
+ // Set the composition method to draw only where the waveform is drawn
163
195
  progressCtx.globalCompositeOperation = 'source-in';
164
196
  progressCtx.fillStyle = this.options.progressColor;
197
+ // This rectangle acts as a mask thanks to the composition method
165
198
  progressCtx.fillRect(0, 0, this.progressCanvas.width, this.progressCanvas.height);
166
199
  }
167
200
  render(channelData, duration) {
201
+ // Determine the width of the canvas
168
202
  const { devicePixelRatio } = window;
169
- const width = Math.max(this.container.clientWidth * devicePixelRatio, duration * this.options.minPxPerSec);
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;
170
207
  const { height } = this.options;
171
208
  this.mainCanvas.width = width;
172
209
  this.mainCanvas.height = height;
173
- this.mainCanvas.style.width = Math.round(width / devicePixelRatio) + 'px';
174
- const renderingFn = this.options.barWidth ? this.renderBarPeaks : this.renderLinePeaks;
175
- renderingFn.call(this, channelData, width, height);
176
- this.createProgressMask();
210
+ this.mainCanvas.style.width = scrollParent ? Math.round(scrollWidth / devicePixelRatio) + 'px' : '100%';
211
+ this.renderPeaks(channelData, width, height);
177
212
  }
178
213
  zoom(channelData, duration, minPxPerSec) {
179
214
  // Remember the current cursor position
@@ -195,8 +230,5 @@ class Renderer extends EventEmitter {
195
230
  }
196
231
  }
197
232
  }
198
- getContainer() {
199
- return this.scrollContainer;
200
- }
201
233
  }
202
234
  export default Renderer;
package/dist/timer.d.ts CHANGED
@@ -4,7 +4,8 @@ type TimerEvents = {
4
4
  };
5
5
  declare class Timer extends EventEmitter<TimerEvents> {
6
6
  private unsubscribe;
7
- constructor();
7
+ start(): void;
8
+ stop(): void;
8
9
  destroy(): void;
9
10
  }
10
11
  export default Timer;
package/dist/timer.js CHANGED
@@ -1,8 +1,10 @@
1
1
  import EventEmitter from './event-emitter.js';
2
2
  class Timer extends EventEmitter {
3
3
  constructor() {
4
- super();
4
+ super(...arguments);
5
5
  this.unsubscribe = () => undefined;
6
+ }
7
+ start() {
6
8
  this.unsubscribe = this.on('tick', () => {
7
9
  requestAnimationFrame(() => {
8
10
  this.emit('tick');
@@ -10,6 +12,9 @@ class Timer extends EventEmitter {
10
12
  });
11
13
  this.emit('tick');
12
14
  }
15
+ stop() {
16
+ this.unsubscribe();
17
+ }
13
18
  destroy() {
14
19
  this.unsubscribe();
15
20
  }
@@ -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:()=>s});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){const i=e=>{e instanceof CustomEvent&&t(e.detail)},n=String(e);return this.eventTarget.addEventListener(n,i),()=>this.eventTarget.removeEventListener(n,i)}once(e,t){const i=this.on(e,((...e)=>{i(),t(...e)}));return i}},n=class extends i{constructor(e){super(),this.subscriptions=[],this.wavesurfer=e.wavesurfer,this.renderer=e.renderer}destroy(){this.subscriptions.forEach((e=>e()))}},s=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.container.getBoundingClientRect().left;t-this.dragStart>=10&&(this.createdRegion?this.updateRegion(this.createdRegion,this.dragStart,t):(this.renderer.getContainer().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.renderer.getContainer().style.pointerEvents=""},this.container=this.initContainer();const t=this.wavesurfer.on("ready",(()=>{this.renderer.getContainer().appendChild(this.container),t()}));this.subscriptions.push(t),this.renderer.getContainer().addEventListener("mousedown",this.handleMouseDown),document.addEventListener("mousemove",this.handleMouseMove),document.addEventListener("mouseup",this.handleMouseUp)}destroy(){var e;this.renderer.getContainer().removeEventListener("mousedown",this.handleMouseDown),document.removeEventListener("mousemove",this.handleMouseMove),document.removeEventListener("mouseup",this.handleMouseUp,!0),null===(e=this.container)||void 0===e||e.remove(),super.destroy()}initContainer(){const e=document.createElement("div");return e.style.position="absolute",e.style.top="0",e.style.left="0",e.style.width="100%",e.style.height="100%",e.style.zIndex="3",e.style.pointerEvents="none",e}createRegionElement(e,t,i=""){const n=document.createElement("div");n.style.position="absolute",n.style.left=`${e}px`,n.style.width=t-e+"px",n.style.height="100%",n.style.backgroundColor="rgba(0, 0, 0, 0.1)",n.style.transition="background-color 0.2s ease",n.style.cursor="move",n.style.pointerEvents="all",n.title=i;const s=document.createElement("div");s.style.position="absolute",s.style.left="0",s.style.width="6px",s.style.height="100%",s.style.borderLeft="2px solid rgba(0, 0, 0, 0.5)",s.style.cursor="ew-resize",s.style.pointerEvents="all",n.appendChild(s);const o=document.createElement("div");return o.style.position="absolute",o.style.right="0",o.style.width="6px",o.style.height="100%",o.style.borderRight="2px solid rgba(0, 0, 0, 0.5)",o.style.cursor="ew-resize",o.style.pointerEvents="all",n.appendChild(o),s.addEventListener("mousedown",(e=>{e.stopPropagation(),this.modifiedRegion=this.regions.find((e=>e.element===n))||null,this.isResizingLeft=!0,this.isMoving=!1})),o.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.container.appendChild(n),n}createRegion(e,t,i=""){const n=this.wavesurfer.getDuration(),s=this.container.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.container.clientWidth*this.wavesurfer.getDuration()),null!=i&&(e.end=i,e.element.style.width=e.end-e.start+"px",e.endTime=i/this.container.clientWidth*this.wavesurfer.getDuration()),this.emit("region-updated",{region:e})}moveRegion(e,t){this.updateRegion(e,e.start+t,e.end+t)}addRegionAtTime(e,t,i=""){const n=this.wavesurfer.getDuration(),s=this.container.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})()));
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})()));
@@ -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:()=>c});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.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})}))}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],o=1===t.length,r=o?s:t[1];let a=-1,h=0;for(let t=0,o=s.length;t<o;t++){const o=Math.round(t/s.length*e),r=Math.round((1-s[t])*i/2);(o!==a||r>h)&&(n.lineTo(o,r),a=o,h=r)}a=-1,h=0;for(let t=r.length-1;t>=0;t--){const s=Math.round(t/r.length*e),l=Math.round((1+r[t])*i/2);(s!==a||(o?l<-h:l>h))&&(n.lineTo(s,l),a=s,h=l)}n.strokeStyle=n.fillStyle=this.options.waveColor,n.stroke(),n.fill()}renderBarPeaks(t,e,i){var n,s;const{devicePixelRatio:o}=window,{ctx:r}=this;r.clearRect(0,0,e,i);const a=this.options.barWidth||1,h=a*o,l=(null!==(n=this.options.barGap)&&void 0!==n?n:a/2)*o,c=null!==(s=this.options.barRadius)&&void 0!==s?s:0,d=t[0],u=1===t.length,p=u?d:t[1],v=Math.floor(e/(h+l)),m=new Float32Array(v),g=new Float32Array(v),y=v/d.length,f=d.length;for(let t=0;t<f;t++){const e=Math.round(t*y);m[e]=Math.max(m[e],d[t]),g[e]=(u?Math.min:Math.max)(g[e],p[t])}r.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));r.roundRect(t*(h+l),i/2-e,h,e+n,c)}r.fillStyle=this.options.waveColor,r.fill()}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)}render(t,e){const{devicePixelRatio:i}=window,n=Math.max(this.container.clientWidth*i,e*this.options.minPxPerSec),{height:s}=this.options;this.mainCanvas.width=n,this.mainCanvas.height=s,this.mainCanvas.style.width=Math.round(n/i)+"px",(this.options.barWidth?this.renderBarPeaks:this.renderLinePeaks).call(this,t,n,s),this.createProgressMask()}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)}}getContainer(){return this.scrollContainer}},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}},o=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)}},r=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 l extends i{static create(t){return new l(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,o){function r(t){try{h(s.next(t))}catch(t){o(t)}}function a(t){try{h(s.throw(t))}catch(t){o(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(r,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,o){function r(t){try{h(s.next(t))}catch(t){o(t)}}function a(t){try{h(s.throw(t))}catch(t){o(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(r,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 r,this.player=new(this.options.backend===a.WebAudio?o: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,r=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((o=void 0)||(o=Promise))((function(t,e){function i(t){try{h(r.next(t))}catch(t){e(t)}}function a(t){try{h(r.throw(t))}catch(t){e(t)}}function h(e){var n;e.done?t(e.value):(n=e.value,n instanceof o?n:new o((function(t){t(n)}))).then(i,a)}h((r=r.apply(n,s||[])).next())}));var n,s,o,r}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()}registerPlugin(t){const e=new t({wavesurfer:this,renderer:this.renderer});return this.plugins.push(e),e}}const c=l;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",
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": ["dist/"],
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": "python3 -m http.server --cgi 8080"
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",