wavesurfer.js
Version:
Navigable audio waveform player
298 lines (297 loc) • 11.7 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import Decoder from './decoder.js';
import Fetcher from './fetcher.js';
import Player from './player.js';
import Renderer from './renderer.js';
import Timer from './timer.js';
const defaultOptions = {
waveColor: '#999',
progressColor: '#555',
cursorWidth: 1,
minPxPerSec: 0,
fillParent: true,
interact: true,
dragToSeek: false,
autoScroll: true,
autoCenter: true,
sampleRate: 8000,
};
class WaveSurfer extends Player {
/** Create a new WaveSurfer instance */
static create(options) {
return new WaveSurfer(options);
}
/** Create a new WaveSurfer instance */
constructor(options) {
var _a, _b;
super({
media: options.media,
mediaControls: options.mediaControls,
autoplay: options.autoplay,
playbackRate: options.audioRate,
});
this.plugins = [];
this.decodedData = null;
this.subscriptions = [];
this.options = Object.assign({}, defaultOptions, options);
this.timer = new Timer();
const audioElement = !options.media ? this.getMediaElement() : undefined;
this.renderer = new Renderer(this.options, audioElement);
this.initPlayerEvents();
this.initRendererEvents();
this.initTimerEvents();
this.initPlugins();
// Load audio if URL is passed or an external media with an src
const url = this.options.url || ((_a = this.options.media) === null || _a === void 0 ? void 0 : _a.currentSrc) || ((_b = this.options.media) === null || _b === void 0 ? void 0 : _b.src);
if (url) {
this.load(url, this.options.peaks, this.options.duration);
}
}
initTimerEvents() {
// The timer fires every 16ms for a smooth progress animation
this.subscriptions.push(this.timer.on('tick', () => {
const currentTime = this.getCurrentTime();
this.renderer.renderProgress(currentTime / this.getDuration(), true);
this.emit('timeupdate', currentTime);
this.emit('audioprocess', currentTime);
}));
}
initPlayerEvents() {
this.subscriptions.push(this.onMediaEvent('timeupdate', () => {
const currentTime = this.getCurrentTime();
this.renderer.renderProgress(currentTime / this.getDuration(), this.isPlaying());
this.emit('timeupdate', currentTime);
}), this.onMediaEvent('play', () => {
this.emit('play');
this.timer.start();
}), this.onMediaEvent('pause', () => {
this.emit('pause');
this.timer.stop();
}), this.onMediaEvent('emptied', () => {
this.timer.stop();
}), this.onMediaEvent('ended', () => {
this.emit('finish');
}), this.onMediaEvent('seeking', () => {
this.emit('seeking', this.getCurrentTime());
}));
}
initRendererEvents() {
this.subscriptions.push(
// Seek on click
this.renderer.on('click', (relativeX, relativeY) => {
if (this.options.interact) {
this.seekTo(relativeX);
this.emit('interaction', relativeX * this.getDuration());
this.emit('click', relativeX, relativeY);
}
}),
// Double click
this.renderer.on('dblclick', (relativeX, relativeY) => {
this.emit('dblclick', relativeX, relativeY);
}),
// Scroll
this.renderer.on('scroll', (startX, endX) => {
const duration = this.getDuration();
this.emit('scroll', startX * duration, endX * duration);
}),
// Redraw
this.renderer.on('render', () => {
this.emit('redraw');
}));
// Drag
{
let debounce;
this.subscriptions.push(this.renderer.on('drag', (relativeX) => {
if (!this.options.interact)
return;
// Update the visual position
this.renderer.renderProgress(relativeX);
// Set the audio position with a debounce
clearTimeout(debounce);
debounce = setTimeout(() => {
this.seekTo(relativeX);
}, this.isPlaying() ? 0 : 200);
this.emit('interaction', relativeX * this.getDuration());
this.emit('drag', relativeX);
}));
}
}
initPlugins() {
var _a;
if (!((_a = this.options.plugins) === null || _a === void 0 ? void 0 : _a.length))
return;
this.options.plugins.forEach((plugin) => {
this.registerPlugin(plugin);
});
}
/** Set new wavesurfer options and re-render it */
setOptions(options) {
this.options = Object.assign({}, this.options, options);
this.renderer.setOptions(this.options);
if (options.audioRate) {
this.setPlaybackRate(options.audioRate);
}
if (options.mediaControls != null) {
this.getMediaElement().controls = options.mediaControls;
}
}
/** Register a wavesurfer.js plugin */
registerPlugin(plugin) {
plugin.init(this);
this.plugins.push(plugin);
// Unregister plugin on destroy
this.subscriptions.push(plugin.once('destroy', () => {
this.plugins = this.plugins.filter((p) => p !== plugin);
}));
return plugin;
}
/** For plugins only: get the waveform wrapper div */
getWrapper() {
return this.renderer.getWrapper();
}
/** Get the current scroll position in pixels */
getScroll() {
return this.renderer.getScroll();
}
/** Get all registered plugins */
getActivePlugins() {
return this.plugins;
}
loadAudio(url, blob, channelData, duration) {
return __awaiter(this, void 0, void 0, function* () {
this.emit('load', url);
if (this.isPlaying())
this.pause();
this.decodedData = null;
// Fetch the entire audio as a blob if pre-decoded data is not provided
if (!blob && !channelData) {
const onProgress = (percentage) => this.emit('loading', percentage);
blob = yield Fetcher.fetchBlob(url, onProgress, this.options.fetchParams);
}
// Set the mediaelement source
this.setSrc(url, blob);
// Decode the audio data or use user-provided peaks
if (channelData) {
// Wait for the audio duration
// It should be a promise to allow event listeners to subscribe to the ready and decode events
duration =
(yield Promise.resolve(duration || this.getDuration())) ||
(yield new Promise((resolve) => {
this.onceMediaEvent('loadedmetadata', () => resolve(this.getDuration()));
})) ||
(yield Promise.resolve(0));
this.decodedData = Decoder.createBuffer(channelData, duration);
}
else if (blob) {
const arrayBuffer = yield blob.arrayBuffer();
this.decodedData = yield Decoder.decode(arrayBuffer, this.options.sampleRate);
}
this.emit('decode', this.getDuration());
// Render the waveform
if (this.decodedData) {
this.renderer.render(this.decodedData);
}
this.emit('ready', this.getDuration());
});
}
/** Load an audio file by URL, with optional pre-decoded audio data */
load(url, channelData, duration) {
return __awaiter(this, void 0, void 0, function* () {
yield this.loadAudio(url, undefined, channelData, duration);
});
}
/** Load an audio blob */
loadBlob(blob, channelData, duration) {
return __awaiter(this, void 0, void 0, function* () {
yield this.loadAudio('blob', blob, channelData, duration);
});
}
/** Zoom the waveform by a given pixels-per-second factor */
zoom(minPxPerSec) {
if (!this.decodedData) {
throw new Error('No audio loaded');
}
this.renderer.zoom(minPxPerSec);
this.emit('zoom', minPxPerSec);
}
/** Get the decoded audio data */
getDecodedData() {
return this.decodedData;
}
/** Get decoded peaks */
exportPeaks({ channels = 1, maxLength = 8000, precision = 10000 } = {}) {
if (!this.decodedData) {
throw new Error('The audio has not been decoded yet');
}
const channelsLen = Math.min(channels, this.decodedData.numberOfChannels);
const peaks = [];
for (let i = 0; i < channelsLen; i++) {
const data = this.decodedData.getChannelData(i);
const length = Math.min(data.length, maxLength);
const scale = data.length / length;
const sampledData = [];
for (let j = 0; j < length; j++) {
const n = Math.round(j * scale);
const val = data[n];
sampledData.push(Math.round(val * precision) / precision);
}
peaks.push(sampledData);
}
return peaks;
}
/** Get the duration of the audio in seconds */
getDuration() {
let duration = super.getDuration() || 0;
// Fall back to the decoded data duration if the media duration is incorrect
if ((duration === 0 || duration === Infinity) && this.decodedData) {
duration = this.decodedData.duration;
}
return duration;
}
/** Toggle if the waveform should react to clicks */
toggleInteraction(isInteractive) {
this.options.interact = isInteractive;
}
/** Seek to a percentage of audio as [0..1] (0 = beginning, 1 = end) */
seekTo(progress) {
const time = this.getDuration() * progress;
this.setTime(time);
}
/** Play or pause the audio */
playPause() {
return __awaiter(this, void 0, void 0, function* () {
return this.isPlaying() ? this.pause() : this.play();
});
}
/** Stop the audio and go to the beginning */
stop() {
this.pause();
this.setTime(0);
}
/** Skip N or -N seconds from the current position */
skip(seconds) {
this.setTime(this.getCurrentTime() + seconds);
}
/** Empty the waveform by loading a tiny silent audio */
empty() {
this.load('', [[0]], 0.001);
}
/** Unmount wavesurfer */
destroy() {
this.emit('destroy');
this.plugins.forEach((plugin) => plugin.destroy());
this.subscriptions.forEach((unsubscribe) => unsubscribe());
this.timer.destroy();
this.renderer.destroy();
super.destroy();
}
}
export default WaveSurfer;