wavesurfer.js 7.0.0-alpha.2 → 7.0.0-alpha.3

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
@@ -42,7 +42,7 @@ Run an HTTP server to view the examples:
42
42
  python3 -m http.server --cgi 8080
43
43
  ```
44
44
 
45
- Open http://localhost:8080/examples/basic/ in your browser.
45
+ Open http://localhost:8080/tutorial in your browser.
46
46
  There's no hot reload yet, so you'll need to reload the page manually on every change.
47
47
 
48
48
  ## Feedback
@@ -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.renderer = params.renderer;
8
+ }
9
+ destroy() {
10
+ this.subscriptions.forEach((unsubscribe) => unsubscribe());
11
+ }
12
+ }
13
+ export default BasePlugin;
@@ -0,0 +1,42 @@
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
+ // Web Audio decodeAudioData with a minimum allowed sample rate
11
+ const SAMPLE_RATE = 3000;
12
+ class Decoder {
13
+ constructor() {
14
+ this.audioCtx = null;
15
+ this.audioCtx = new AudioContext({
16
+ latencyHint: 'playback',
17
+ sampleRate: SAMPLE_RATE,
18
+ });
19
+ }
20
+ decode(audioData) {
21
+ return __awaiter(this, void 0, void 0, function* () {
22
+ if (!this.audioCtx) {
23
+ throw new Error('AudioContext is not initialized');
24
+ }
25
+ const buffer = yield this.audioCtx.decodeAudioData(audioData);
26
+ const channelData = [buffer.getChannelData(0)];
27
+ if (buffer.numberOfChannels > 1) {
28
+ channelData.push(buffer.getChannelData(1));
29
+ }
30
+ return {
31
+ duration: buffer.duration,
32
+ channelData,
33
+ };
34
+ });
35
+ }
36
+ destroy() {
37
+ var _a;
38
+ (_a = this.audioCtx) === null || _a === void 0 ? void 0 : _a.close();
39
+ this.audioCtx = null;
40
+ }
41
+ }
42
+ export default Decoder;
@@ -0,0 +1,29 @@
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) {
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);
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
+ const unsubscribe = this.on(eventType, (...args) => {
23
+ unsubscribe();
24
+ callback(...args);
25
+ });
26
+ return unsubscribe;
27
+ }
28
+ }
29
+ export default EventEmitter;
@@ -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.js ADDED
@@ -0,0 +1,166 @@
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 WebAudioPlayer from './player-webaudio.js';
15
+ import EventEmitter from './event-emitter.js';
16
+ import Timer from './timer.js';
17
+ export var PlayerType;
18
+ (function (PlayerType) {
19
+ PlayerType["WebAudio"] = "WebAudio";
20
+ PlayerType["MediaElement"] = "MediaElement";
21
+ })(PlayerType || (PlayerType = {}));
22
+ const defaultOptions = {
23
+ height: 128,
24
+ waveColor: '#999',
25
+ progressColor: '#555',
26
+ minPxPerSec: 0,
27
+ backend: 'MediaElement',
28
+ };
29
+ export class WaveSurfer extends EventEmitter {
30
+ /** Create a new WaveSurfer instance */
31
+ static create(options) {
32
+ return new WaveSurfer(options);
33
+ }
34
+ /** Create a new WaveSurfer instance */
35
+ constructor(options) {
36
+ var _a;
37
+ super();
38
+ this.plugins = [];
39
+ this.subscriptions = [];
40
+ this.channelData = null;
41
+ this.duration = 0;
42
+ this.options = Object.assign({}, defaultOptions, options);
43
+ this.fetcher = new Fetcher();
44
+ this.decoder = new Decoder();
45
+ this.timer = new Timer();
46
+ this.player = new (this.options.backend === PlayerType.WebAudio ? WebAudioPlayer : Player)({
47
+ media: this.options.media,
48
+ });
49
+ this.renderer = new Renderer({
50
+ container: this.options.container,
51
+ height: this.options.height,
52
+ waveColor: this.options.waveColor,
53
+ progressColor: this.options.progressColor,
54
+ minPxPerSec: this.options.minPxPerSec,
55
+ barWidth: this.options.barWidth,
56
+ barGap: this.options.barGap,
57
+ barRadius: this.options.barRadius,
58
+ });
59
+ this.initPlayerEvents();
60
+ this.initRendererEvents();
61
+ this.initTimerEvents();
62
+ const url = this.options.url || ((_a = this.options.media) === null || _a === void 0 ? void 0 : _a.src);
63
+ if (url) {
64
+ this.load(url, this.options.channelData, this.options.duration);
65
+ }
66
+ }
67
+ initPlayerEvents() {
68
+ this.subscriptions.push(this.player.on('timeupdate', () => {
69
+ const currentTime = this.getCurrentTime();
70
+ this.renderer.renderProgress(currentTime / this.duration, this.isPlaying());
71
+ this.emit('audioprocess', { currentTime });
72
+ }), this.player.on('play', () => {
73
+ this.emit('play');
74
+ }), this.player.on('pause', () => {
75
+ this.emit('pause');
76
+ }), this.player.on('canplay', () => {
77
+ this.emit('canplay');
78
+ }), this.player.on('seeking', () => {
79
+ this.emit('seek', { time: this.getCurrentTime() });
80
+ }));
81
+ }
82
+ initRendererEvents() {
83
+ // Seek on click
84
+ this.subscriptions.push(this.renderer.on('click', ({ relativeX }) => {
85
+ const time = this.getDuration() * relativeX;
86
+ this.seekTo(time);
87
+ }));
88
+ }
89
+ initTimerEvents() {
90
+ // The timer fires every 16ms for a smooth progress animation
91
+ 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
+ }
97
+ }));
98
+ }
99
+ /** Unmount wavesurfer */
100
+ destroy() {
101
+ this.subscriptions.forEach((unsubscribe) => unsubscribe());
102
+ this.plugins.forEach((plugin) => plugin.destroy());
103
+ this.timer.destroy();
104
+ this.player.destroy();
105
+ this.decoder.destroy();
106
+ this.renderer.destroy();
107
+ }
108
+ /** Load an audio file by URL */
109
+ load(url, channelData, duration) {
110
+ return __awaiter(this, void 0, void 0, function* () {
111
+ this.player.loadUrl(url);
112
+ // Fetch and decode the audio of no pre-computed audio data is provided
113
+ if (channelData == null || duration == null) {
114
+ const audio = yield this.fetcher.load(url);
115
+ const data = yield this.decoder.decode(audio);
116
+ channelData = data.channelData;
117
+ duration = data.duration;
118
+ }
119
+ this.channelData = channelData;
120
+ this.duration = duration;
121
+ this.renderer.render(this.channelData, this.duration);
122
+ this.emit('ready', { duration: this.duration });
123
+ });
124
+ }
125
+ /** Zoom in or out */
126
+ zoom(minPxPerSec) {
127
+ if (this.channelData == null || this.duration == null) {
128
+ throw new Error('No audio loaded');
129
+ }
130
+ this.renderer.zoom(this.channelData, this.duration, minPxPerSec);
131
+ }
132
+ /** Start playing the audio */
133
+ play() {
134
+ this.player.play();
135
+ }
136
+ /** Pause the audio */
137
+ pause() {
138
+ this.player.pause();
139
+ }
140
+ /** Skip to a time position in seconds */
141
+ seekTo(time) {
142
+ this.player.seekTo(time);
143
+ }
144
+ /** Check if the audio is playing */
145
+ isPlaying() {
146
+ return this.player.isPlaying();
147
+ }
148
+ /** Get the duration of the audio in seconds */
149
+ getDuration() {
150
+ return this.duration;
151
+ }
152
+ /** Get the current audio position in seconds */
153
+ getCurrentTime() {
154
+ return this.player.getCurrentTime();
155
+ }
156
+ /** Register and initialize a plugin */
157
+ registerPlugin(CustomPlugin) {
158
+ const plugin = new CustomPlugin({
159
+ wavesurfer: this,
160
+ renderer: this.renderer,
161
+ });
162
+ this.plugins.push(plugin);
163
+ return plugin;
164
+ }
165
+ }
166
+ export default WaveSurfer;
@@ -0,0 +1,30 @@
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
+ this.sourceNode = this.audioCtx.createMediaElementSource(this.media);
27
+ this.sourceNode.connect(this.audioCtx.destination);
28
+ }
29
+ }
30
+ export default WebAudioPlayer;
package/dist/player.js ADDED
@@ -0,0 +1,40 @@
1
+ class Player {
2
+ constructor({ media }) {
3
+ this.isExternalMedia = false;
4
+ if (media) {
5
+ this.media = media;
6
+ this.isExternalMedia = true;
7
+ }
8
+ else {
9
+ this.media = document.createElement('audio');
10
+ }
11
+ }
12
+ on(event, callback) {
13
+ this.media.addEventListener(event, callback);
14
+ return () => this.media.removeEventListener(event, callback);
15
+ }
16
+ destroy() {
17
+ if (!this.isExternalMedia) {
18
+ this.media.remove();
19
+ }
20
+ }
21
+ loadUrl(src) {
22
+ this.media.src = src;
23
+ }
24
+ getCurrentTime() {
25
+ return this.media.currentTime;
26
+ }
27
+ play() {
28
+ this.media.play();
29
+ }
30
+ pause() {
31
+ this.media.pause();
32
+ }
33
+ isPlaying() {
34
+ return this.media.currentTime > 0 && !this.media.paused && !this.media.ended;
35
+ }
36
+ seekTo(time) {
37
+ this.media.currentTime = time;
38
+ }
39
+ }
40
+ export default Player;
@@ -0,0 +1,183 @@
1
+ import BasePlugin from '../base-plugin.js';
2
+ const MIN_WIDTH = 10;
3
+ class RegionsPlugin extends BasePlugin {
4
+ /** Create an instance of RegionsPlugin */
5
+ constructor(params) {
6
+ super(params);
7
+ this.dragStart = NaN;
8
+ this.regions = [];
9
+ this.createdRegion = null;
10
+ this.modifiedRegion = null;
11
+ this.isResizingLeft = false;
12
+ this.isMoving = false;
13
+ this.handleMouseDown = (e) => {
14
+ this.dragStart = e.clientX - this.container.getBoundingClientRect().left;
15
+ };
16
+ this.handleMouseMove = (e) => {
17
+ const dragEnd = e.clientX - this.container.getBoundingClientRect().left;
18
+ if (this.modifiedRegion && this.isMoving) {
19
+ this.moveRegion(this.modifiedRegion, dragEnd - this.dragStart);
20
+ this.dragStart = dragEnd;
21
+ return;
22
+ }
23
+ if (this.modifiedRegion) {
24
+ this.updateRegion(this.modifiedRegion, this.isResizingLeft ? dragEnd : undefined, this.isResizingLeft ? undefined : dragEnd);
25
+ return;
26
+ }
27
+ if (!isNaN(this.dragStart)) {
28
+ const dragEnd = e.clientX - this.container.getBoundingClientRect().left;
29
+ if (dragEnd - this.dragStart >= MIN_WIDTH) {
30
+ if (!this.createdRegion) {
31
+ this.renderer.getContainer().style.pointerEvents = 'none';
32
+ this.createdRegion = this.createRegion(this.dragStart, dragEnd);
33
+ }
34
+ else {
35
+ this.updateRegion(this.createdRegion, this.dragStart, dragEnd);
36
+ }
37
+ }
38
+ }
39
+ };
40
+ this.handleMouseUp = () => {
41
+ if (this.createdRegion) {
42
+ this.addRegion(this.createdRegion);
43
+ this.createdRegion = null;
44
+ }
45
+ this.modifiedRegion = null;
46
+ this.isMoving = false;
47
+ this.dragStart = NaN;
48
+ this.renderer.getContainer().style.pointerEvents = '';
49
+ };
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();
55
+ });
56
+ this.subscriptions.push(unsubscribeReady);
57
+ const wsContainer = this.renderer.getContainer();
58
+ wsContainer.addEventListener('mousedown', this.handleMouseDown);
59
+ document.addEventListener('mousemove', this.handleMouseMove);
60
+ document.addEventListener('mouseup', this.handleMouseUp);
61
+ }
62
+ /** Unmounts the regions */
63
+ destroy() {
64
+ var _a;
65
+ this.renderer.getContainer().removeEventListener('mousedown', this.handleMouseDown);
66
+ document.removeEventListener('mousemove', this.handleMouseMove);
67
+ document.removeEventListener('mouseup', this.handleMouseUp, true);
68
+ (_a = this.container) === null || _a === void 0 ? void 0 : _a.remove();
69
+ super.destroy();
70
+ }
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;
81
+ }
82
+ 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);
111
+ leftHandle.addEventListener('mousedown', (e) => {
112
+ e.stopPropagation();
113
+ this.modifiedRegion = this.regions.find((r) => r.element === el) || null;
114
+ this.isResizingLeft = true;
115
+ this.isMoving = false;
116
+ });
117
+ rightHandle.addEventListener('mousedown', (e) => {
118
+ e.stopPropagation();
119
+ this.modifiedRegion = this.regions.find((r) => r.element === el) || null;
120
+ this.isResizingLeft = false;
121
+ this.isMoving = false;
122
+ });
123
+ el.addEventListener('mousedown', () => {
124
+ this.modifiedRegion = this.regions.find((r) => r.element === el) || null;
125
+ this.isMoving = true;
126
+ });
127
+ el.addEventListener('click', () => {
128
+ const region = this.regions.find((r) => r.element === el);
129
+ if (region) {
130
+ this.emit('region-clicked', { region });
131
+ }
132
+ });
133
+ this.container.appendChild(el);
134
+ return el;
135
+ }
136
+ createRegion(start, end, title = '') {
137
+ const duration = this.wavesurfer.getDuration();
138
+ const width = this.container.clientWidth;
139
+ return {
140
+ element: this.createRegionElement(start, end, title),
141
+ start,
142
+ end,
143
+ startTime: (start / width) * duration,
144
+ endTime: (end / width) * duration,
145
+ title,
146
+ };
147
+ }
148
+ addRegion(region) {
149
+ this.regions.push(region);
150
+ this.emit('region-created', { region });
151
+ }
152
+ updateRegion(region, start, end) {
153
+ if (start != null) {
154
+ region.start = start !== null && start !== void 0 ? start : region.start;
155
+ region.element.style.left = `${region.start}px`;
156
+ region.startTime = (start / this.container.clientWidth) * this.wavesurfer.getDuration();
157
+ }
158
+ if (end != null) {
159
+ region.end = end;
160
+ region.element.style.width = `${region.end - region.start}px`;
161
+ region.endTime = (end / this.container.clientWidth) * this.wavesurfer.getDuration();
162
+ }
163
+ this.emit('region-updated', { region });
164
+ }
165
+ moveRegion(region, delta) {
166
+ this.updateRegion(region, region.start + delta, region.end + delta);
167
+ }
168
+ /** Create a region at a given start and end time, with an optional title */
169
+ addRegionAtTime(startTime, endTime, title = '') {
170
+ const duration = this.wavesurfer.getDuration();
171
+ const width = this.container.clientWidth;
172
+ const start = (startTime / duration) * width;
173
+ const end = (endTime / duration) * width;
174
+ const region = this.createRegion(start, end, title);
175
+ this.addRegion(region);
176
+ return region;
177
+ }
178
+ /** Set the background color of a region */
179
+ setRegionColor(region, color) {
180
+ region.element.style.backgroundColor = color;
181
+ }
182
+ }
183
+ export default RegionsPlugin;
@@ -17,7 +17,7 @@ type RendererEvents = {
17
17
  declare class Renderer extends EventEmitter<RendererEvents> {
18
18
  private options;
19
19
  private container;
20
- private shadowRoot;
20
+ private scrollContainer;
21
21
  private mainCanvas;
22
22
  private progressCanvas;
23
23
  private cursor;
@@ -26,8 +26,9 @@ declare class Renderer extends EventEmitter<RendererEvents> {
26
26
  destroy(): void;
27
27
  private renderLinePeaks;
28
28
  private renderBarPeaks;
29
- render(channelData: Float32Array[], duration: number, minPxPerSec?: number): void;
30
29
  private createProgressMask;
30
+ render(channelData: Float32Array[], duration: number): void;
31
+ zoom(channelData: Float32Array[], duration: number, minPxPerSec: number): void;
31
32
  renderProgress(progress: number, autoCenter?: boolean): void;
32
33
  getContainer(): HTMLElement;
33
34
  }
@@ -0,0 +1,202 @@
1
+ import EventEmitter from './event-emitter.js';
2
+ class Renderer extends EventEmitter {
3
+ constructor(options) {
4
+ super();
5
+ this.options = options;
6
+ let container = null;
7
+ if (typeof this.options.container === 'string') {
8
+ container = document.querySelector(this.options.container);
9
+ }
10
+ else if (this.options.container instanceof HTMLElement) {
11
+ container = this.options.container;
12
+ }
13
+ if (!container) {
14
+ throw new Error('Container not found');
15
+ }
16
+ const div = document.createElement('div');
17
+ const shadow = div.attachShadow({ mode: 'open' });
18
+ shadow.innerHTML = `
19
+ <style>
20
+ :host .scroll {
21
+ overflow-x: auto;
22
+ overflow-y: visible;
23
+ user-select: none;
24
+ width: 100%;
25
+ height: ${this.options.height}px;
26
+ position: relative;
27
+ }
28
+ :host .wrapper {
29
+ position: relative;
30
+ width: fit-content;
31
+ min-width: 100%;
32
+ height: 100%;
33
+ z-index: 2;
34
+ }
35
+ :host canvas {
36
+ display: block;
37
+ height: 100%;
38
+ min-width: 100%;
39
+ image-rendering: pixelated;
40
+ }
41
+ :host .progress {
42
+ position: absolute;
43
+ z-index: 2;
44
+ top: 0;
45
+ left: 0;
46
+ height: 100%;
47
+ pointer-events: none;
48
+ clip-path: inset(100%);
49
+ }
50
+ :host .cursor {
51
+ position: absolute;
52
+ z-index: 3;
53
+ top: 0;
54
+ left: 0;
55
+ height: 100%;
56
+ border-right: 1px solid ${this.options.progressColor};
57
+ }
58
+ </style>
59
+
60
+ <div class="scroll">
61
+ <div class="wrapper">
62
+ <canvas></canvas>
63
+ <canvas class="progress"></canvas>
64
+ <div class="cursor"></div>
65
+ </div>
66
+ </div>
67
+ `;
68
+ this.container = div;
69
+ this.scrollContainer = shadow.querySelector('.scroll');
70
+ this.mainCanvas = shadow.querySelector('canvas');
71
+ this.ctx = this.mainCanvas.getContext('2d', { desynchronized: true });
72
+ this.progressCanvas = shadow.querySelector('.progress');
73
+ this.cursor = shadow.querySelector('.cursor');
74
+ container.appendChild(div);
75
+ this.mainCanvas.addEventListener('click', (e) => {
76
+ const rect = this.mainCanvas.getBoundingClientRect();
77
+ const x = e.clientX - rect.left;
78
+ const relativeX = x / rect.width;
79
+ this.emit('click', { relativeX });
80
+ });
81
+ }
82
+ destroy() {
83
+ this.container.remove();
84
+ }
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
+ }
117
+ }
118
+ ctx.strokeStyle = ctx.fillStyle = this.options.waveColor;
119
+ ctx.stroke();
120
+ ctx.fill();
121
+ }
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();
152
+ }
153
+ createProgressMask() {
154
+ const progressCtx = this.progressCanvas.getContext('2d', { desynchronized: true });
155
+ if (!progressCtx) {
156
+ throw new Error('Failed to get canvas context');
157
+ }
158
+ this.progressCanvas.width = this.mainCanvas.width;
159
+ this.progressCanvas.height = this.mainCanvas.height;
160
+ this.progressCanvas.style.width = this.mainCanvas.style.width;
161
+ this.progressCanvas.style.height = this.mainCanvas.style.height;
162
+ progressCtx.drawImage(this.mainCanvas, 0, 0);
163
+ progressCtx.globalCompositeOperation = 'source-in';
164
+ progressCtx.fillStyle = this.options.progressColor;
165
+ progressCtx.fillRect(0, 0, this.progressCanvas.width, this.progressCanvas.height);
166
+ }
167
+ render(channelData, duration) {
168
+ const { devicePixelRatio } = window;
169
+ const width = Math.max(this.container.clientWidth * devicePixelRatio, duration * this.options.minPxPerSec);
170
+ const { height } = this.options;
171
+ this.mainCanvas.width = width;
172
+ 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();
177
+ }
178
+ zoom(channelData, duration, minPxPerSec) {
179
+ // Remember the current cursor position
180
+ const oldCursorPosition = this.cursor.getBoundingClientRect().left;
181
+ this.options.minPxPerSec = minPxPerSec;
182
+ this.render(channelData, duration);
183
+ // Adjust the scroll position so that the cursor stays in the same place
184
+ const newCursortPosition = this.cursor.getBoundingClientRect().left;
185
+ this.scrollContainer.scrollLeft += newCursortPosition - oldCursorPosition;
186
+ }
187
+ renderProgress(progress, autoCenter = false) {
188
+ this.progressCanvas.style.clipPath = `inset(0 ${100 - progress * 100}% 0 0)`;
189
+ this.cursor.style.left = `${progress * 100}%`;
190
+ if (autoCenter) {
191
+ const center = this.container.clientWidth / 2;
192
+ const fullWidth = this.mainCanvas.clientWidth;
193
+ if (fullWidth * progress >= center) {
194
+ this.scrollContainer.scrollLeft = fullWidth * progress - center;
195
+ }
196
+ }
197
+ }
198
+ getContainer() {
199
+ return this.scrollContainer;
200
+ }
201
+ }
202
+ export default Renderer;
package/dist/timer.js ADDED
@@ -0,0 +1,17 @@
1
+ import EventEmitter from './event-emitter.js';
2
+ class Timer extends EventEmitter {
3
+ constructor() {
4
+ super();
5
+ this.unsubscribe = () => undefined;
6
+ this.unsubscribe = this.on('tick', () => {
7
+ requestAnimationFrame(() => {
8
+ this.emit('tick');
9
+ });
10
+ });
11
+ this.emit('tick');
12
+ }
13
+ destroy() {
14
+ this.unsubscribe();
15
+ }
16
+ }
17
+ export default Timer;
@@ -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:()=>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})()));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wavesurfer.js",
3
- "version": "7.0.0-alpha.2",
3
+ "version": "7.0.0-alpha.3",
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",