UNPKG

wavesurfer.js

Version:
459 lines (458 loc) 18 kB
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 BasePlugin from './base-plugin.js'; import Decoder from './decoder.js'; import * as dom from './dom.js'; import Fetcher from './fetcher.js'; import Player from './player.js'; import Renderer from './renderer.js'; import Timer from './timer.js'; import WebAudioPlayer from './webaudio.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) { const media = options.media || (options.backend === 'WebAudio' ? new WebAudioPlayer() : undefined); super({ media, mediaControls: options.mediaControls, autoplay: options.autoplay, playbackRate: options.audioRate, }); this.plugins = []; this.decodedData = null; this.stopAtPosition = null; this.subscriptions = []; this.mediaSubscriptions = []; this.abortController = null; this.options = Object.assign({}, defaultOptions, options); this.timer = new Timer(); const audioElement = media ? undefined : this.getMediaElement(); this.renderer = new Renderer(this.options, audioElement); this.initPlayerEvents(); this.initRendererEvents(); this.initTimerEvents(); this.initPlugins(); // Read the initial URL before load has been called const initialUrl = this.options.url || this.getSrc() || ''; // Init and load async to allow external events to be registered Promise.resolve().then(() => { this.emit('init'); // Load audio if URL or an external media with an src is passed, // of render w/o audio if pre-decoded peaks and duration are provided const { peaks, duration } = this.options; if (initialUrl || (peaks && duration)) { // Swallow async errors because they cannot be caught from a constructor call. // Subscribe to the wavesurfer's error event to handle them. this.load(initialUrl, peaks, duration).catch(() => null); } }); } updateProgress(currentTime = this.getCurrentTime()) { this.renderer.renderProgress(currentTime / this.getDuration(), this.isPlaying()); return currentTime; } initTimerEvents() { // The timer fires every 16ms for a smooth progress animation this.subscriptions.push(this.timer.on('tick', () => { if (!this.isSeeking()) { const currentTime = this.updateProgress(); this.emit('timeupdate', currentTime); this.emit('audioprocess', currentTime); // Pause audio when it reaches the stopAtPosition if (this.stopAtPosition != null && this.isPlaying() && currentTime >= this.stopAtPosition) { this.pause(); } } })); } initPlayerEvents() { if (this.isPlaying()) { this.emit('play'); this.timer.start(); } this.mediaSubscriptions.push(this.onMediaEvent('timeupdate', () => { const currentTime = this.updateProgress(); this.emit('timeupdate', currentTime); }), this.onMediaEvent('play', () => { this.emit('play'); this.timer.start(); }), this.onMediaEvent('pause', () => { this.emit('pause'); this.timer.stop(); this.stopAtPosition = null; }), this.onMediaEvent('emptied', () => { this.timer.stop(); this.stopAtPosition = null; }), this.onMediaEvent('ended', () => { this.emit('timeupdate', this.getDuration()); this.emit('finish'); this.stopAtPosition = null; }), this.onMediaEvent('seeking', () => { this.emit('seeking', this.getCurrentTime()); }), this.onMediaEvent('error', () => { var _a; this.emit('error', ((_a = this.getMediaElement().error) !== null && _a !== void 0 ? _a : new Error('Media error'))); this.stopAtPosition = null; })); } 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, scrollLeft, scrollRight) => { const duration = this.getDuration(); this.emit('scroll', startX * duration, endX * duration, scrollLeft, scrollRight); }), // Redraw this.renderer.on('render', () => { this.emit('redraw'); }), // RedrawComplete this.renderer.on('rendered', () => { this.emit('redrawcomplete'); }), // DragStart this.renderer.on('dragstart', (relativeX) => { this.emit('dragstart', relativeX); }), // DragEnd this.renderer.on('dragend', (relativeX) => { this.emit('dragend', relativeX); })); // 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); let debounceTime; if (this.isPlaying()) { debounceTime = 0; } else if (this.options.dragToSeek === true) { debounceTime = 200; } else if (typeof this.options.dragToSeek === 'object' && this.options.dragToSeek !== undefined) { debounceTime = this.options.dragToSeek['debounceTime']; } debounce = setTimeout(() => { this.seekTo(relativeX); }, debounceTime); 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); }); } unsubscribePlayerEvents() { this.mediaSubscriptions.forEach((unsubscribe) => unsubscribe()); this.mediaSubscriptions = []; } /** Set new wavesurfer options and re-render it */ setOptions(options) { this.options = Object.assign({}, this.options, options); if (options.duration && !options.peaks) { this.decodedData = Decoder.createBuffer(this.exportPeaks(), options.duration); } if (options.peaks && options.duration) { // Create new decoded data buffer from peaks and duration this.decodedData = Decoder.createBuffer(options.peaks, options.duration); } 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(); } /** For plugins only: get the scroll container client width */ getWidth() { return this.renderer.getWidth(); } /** Get the current scroll position in pixels */ getScroll() { return this.renderer.getScroll(); } /** Set the current scroll position in pixels */ setScroll(pixels) { return this.renderer.setScroll(pixels); } /** Move the start of the viewing window to a specific time in the audio (in seconds) */ setScrollTime(time) { const percentage = time / this.getDuration(); this.renderer.setScrollPercentage(percentage); } /** Get all registered plugins */ getActivePlugins() { return this.plugins; } loadAudio(url, blob, channelData, duration) { return __awaiter(this, void 0, void 0, function* () { var _a; this.emit('load', url); if (!this.options.media && this.isPlaying()) this.pause(); this.decodedData = null; this.stopAtPosition = null; // Fetch the entire audio as a blob if pre-decoded data is not provided if (!blob && !channelData) { const fetchParams = this.options.fetchParams || {}; if (window.AbortController && !fetchParams.signal) { this.abortController = new AbortController(); fetchParams.signal = (_a = this.abortController) === null || _a === void 0 ? void 0 : _a.signal; } const onProgress = (percentage) => this.emit('loading', percentage); blob = yield Fetcher.fetchBlob(url, onProgress, fetchParams); const overridenMimeType = this.options.blobMimeType; if (overridenMimeType) { blob = new Blob([blob], { type: overridenMimeType }); } } // Set the mediaelement source this.setSrc(url, blob); // Wait for the audio duration const audioDuration = yield new Promise((resolve) => { const staticDuration = duration || this.getDuration(); if (staticDuration) { resolve(staticDuration); } else { this.mediaSubscriptions.push(this.onMediaEvent('loadedmetadata', () => resolve(this.getDuration()), { once: true })); } }); // Set the duration if the player is a WebAudioPlayer without a URL if (!url && !blob) { const media = this.getMediaElement(); if (media instanceof WebAudioPlayer) { media.duration = audioDuration; } } // Decode the audio data or use user-provided peaks if (channelData) { this.decodedData = Decoder.createBuffer(channelData, audioDuration || 0); } else if (blob) { const arrayBuffer = yield blob.arrayBuffer(); this.decodedData = yield Decoder.decode(arrayBuffer, this.options.sampleRate); } if (this.decodedData) { this.emit('decode', this.getDuration()); 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* () { try { return yield this.loadAudio(url, undefined, channelData, duration); } catch (err) { this.emit('error', err); throw err; } }); } /** Load an audio blob */ loadBlob(blob, channelData, duration) { return __awaiter(this, void 0, void 0, function* () { try { return yield this.loadAudio('', blob, channelData, duration); } catch (err) { this.emit('error', err); throw err; } }); } /** 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 = 2, maxLength = 8000, precision = 10000 } = {}) { if (!this.decodedData) { throw new Error('The audio has not been decoded yet'); } const maxChannels = Math.min(channels, this.decodedData.numberOfChannels); const peaks = []; for (let i = 0; i < maxChannels; i++) { const channel = this.decodedData.getChannelData(i); const data = []; const sampleSize = channel.length / maxLength; for (let i = 0; i < maxLength; i++) { const sample = channel.slice(Math.floor(i * sampleSize), Math.ceil((i + 1) * sampleSize)); let max = 0; for (let x = 0; x < sample.length; x++) { const n = sample[x]; if (Math.abs(n) > Math.abs(max)) max = n; } data.push(Math.round(max * precision) / precision); } peaks.push(data); } 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; } /** Jump to a specific time in the audio (in seconds) */ setTime(time) { this.stopAtPosition = null; super.setTime(time); this.updateProgress(time); this.emit('timeupdate', time); } /** Seek to a percentage of audio as [0..1] (0 = beginning, 1 = end) */ seekTo(progress) { const time = this.getDuration() * progress; this.setTime(time); } /** Start playing the audio */ play(start, end) { const _super = Object.create(null, { play: { get: () => super.play } }); return __awaiter(this, void 0, void 0, function* () { if (start != null) { this.setTime(start); } const playResult = yield _super.play.call(this); if (end != null) { if (this.media instanceof WebAudioPlayer) { this.media.stopAt(end); } else { this.stopAtPosition = end; } } return playResult; }); } /** 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 */ empty() { this.load('', [[0]], 0.001); } /** Set HTML media element */ setMediaElement(element) { this.unsubscribePlayerEvents(); super.setMediaElement(element); this.initPlayerEvents(); } exportImage() { return __awaiter(this, arguments, void 0, function* (format = 'image/png', quality = 1, type = 'dataURL') { return this.renderer.exportImage(format, quality, type); }); } /** Unmount wavesurfer */ destroy() { var _a; this.emit('destroy'); (_a = this.abortController) === null || _a === void 0 ? void 0 : _a.abort(); this.plugins.forEach((plugin) => plugin.destroy()); this.subscriptions.forEach((unsubscribe) => unsubscribe()); this.unsubscribePlayerEvents(); this.timer.destroy(); this.renderer.destroy(); super.destroy(); } } WaveSurfer.BasePlugin = BasePlugin; WaveSurfer.dom = dom; export default WaveSurfer;