WaveSurfer - Tampermonkey

WaveSurfer

此腳本不應該直接安裝,它是一個供其他腳本使用的函式庫。欲使用本函式庫,請在腳本 metadata 寫上: // @require https://update.cn-greasyfork.org/scripts/32322/212062/WaveSurfer%20-%20Tampermonkey.js

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         WaveSurfer - Tampermonkey
// @namespace    https://wavesurfer-js.org/
// @version      1.0
// @description  WaveSurfer
// @author       katspaugh
// @grant        GM_xmlhttpRequest
// ==/UserScript==


(function (root, factory) {
    if (typeof define === 'function' && define.amd) {
        // AMD. Register as an anonymous module unless amdModuleId is set
        define('wavesurfer', [], function () {
            return (root['WaveSurfer'] = factory());
        });
    } else if (typeof exports === 'object') {
        // Node. Does not work with strict CommonJS, but
        // only CommonJS-like environments that support module.exports,
        // like Node.
        module.exports = factory();
    } else {
        root['WaveSurfer'] = factory();
    }
}(this, function () {

    'use strict';

    var WaveSurfer = {
        defaultParams: {
            height        : 128,
            waveColor     : '#999',
            progressColor : '#555',
            cursorColor   : '#333',
            cursorWidth   : 1,
            skipLength    : 2,
            minPxPerSec   : 20,
            pixelRatio    : window.devicePixelRatio || screen.deviceXDPI / screen.logicalXDPI,
            fillParent    : true,
            scrollParent  : false,
            hideScrollbar : false,
            normalize     : false,
            audioContext  : null,
            container     : null,
            dragSelection : true,
            loopSelection : true,
            audioRate     : 1,
            interact      : true,
            splitChannels : false,
            mediaContainer: null,
            mediaControls : false,
            renderer      : 'Canvas',
            backend       : 'WebAudio',
            mediaType     : 'audio',
            autoCenter    : true
        },

        init: function (params) {
            // Extract relevant parameters (or defaults)
            this.params = WaveSurfer.util.extend({}, this.defaultParams, params);

            this.container = 'string' == typeof params.container ?
                document.querySelector(this.params.container) :
            this.params.container;

            if (!this.container) {
                throw new Error('Container element not found');
            }

            if (this.params.mediaContainer == null) {
                this.mediaContainer = this.container;
            } else if (typeof this.params.mediaContainer == 'string') {
                this.mediaContainer = document.querySelector(this.params.mediaContainer);
            } else {
                this.mediaContainer = this.params.mediaContainer;
            }

            if (!this.mediaContainer) {
                throw new Error('Media Container element not found');
            }

            // Used to save the current volume when muting so we can
            // restore once unmuted
            this.savedVolume = 0;

            // The current muted state
            this.isMuted = false;

            // Will hold a list of event descriptors that need to be
            // cancelled on subsequent loads of audio
            this.tmpEvents = [];

            // Holds any running audio downloads
            this.currentAjax = null;

            this.createDrawer();
            this.createBackend();
        },

        createDrawer: function () {
            var my = this;

            this.drawer = Object.create(WaveSurfer.Drawer[this.params.renderer]);
            this.drawer.init(this.container, this.params);

            this.drawer.on('redraw', function () {
                my.drawBuffer();
                my.drawer.progress(my.backend.getPlayedPercents());
            });

            // Click-to-seek
            this.drawer.on('click', function (e, progress) {
                setTimeout(function () {
                    my.seekTo(progress);
                }, 0);
            });

            // Relay the scroll event from the drawer
            this.drawer.on('scroll', function (e) {
                my.fireEvent('scroll', e);
            });
        },

        createBackend: function () {
            var my = this;

            if (this.backend) {
                this.backend.destroy();
            }

            // Back compat
            if (this.params.backend == 'AudioElement') {
                this.params.backend = 'MediaElement';
            }

            if (this.params.backend == 'WebAudio' && !WaveSurfer.WebAudio.supportsWebAudio()) {
                this.params.backend = 'MediaElement';
            }

            this.backend = Object.create(WaveSurfer[this.params.backend]);
            this.backend.init(this.params);

            this.backend.on('finish', function () { my.fireEvent('finish'); });
            this.backend.on('play', function () { my.fireEvent('play'); });
            this.backend.on('pause', function () { my.fireEvent('pause'); });

            this.backend.on('audioprocess', function (time) {
                my.drawer.progress(my.backend.getPlayedPercents());
                my.fireEvent('audioprocess', time);
            });
        },

        getDuration: function () {
            return this.backend.getDuration();
        },

        getCurrentTime: function () {
            return this.backend.getCurrentTime();
        },

        play: function (start, end) {
            this.backend.play(start, end);
        },

        pause: function () {
            this.backend.pause();
        },

        playPause: function () {
            this.backend.isPaused() ? this.play() : this.pause();
        },

        isPlaying: function () {
            return !this.backend.isPaused();
        },

        skipBackward: function (seconds) {
            this.skip(-seconds || -this.params.skipLength);
        },

        skipForward: function (seconds) {
            this.skip(seconds || this.params.skipLength);
        },

        skip: function (offset) {
            var position = this.getCurrentTime() || 0;
            var duration = this.getDuration() || 1;
            position = Math.max(0, Math.min(duration, position + (offset || 0)));
            this.seekAndCenter(position / duration);
        },

        seekAndCenter: function (progress) {
            this.seekTo(progress);
            this.drawer.recenter(progress);
        },

        seekTo: function (progress) {
            var paused = this.backend.isPaused();
            // avoid small scrolls while paused seeking
            var oldScrollParent = this.params.scrollParent;
            if (paused) {
                this.params.scrollParent = false;
            }
            this.backend.seekTo(progress * this.getDuration());
            this.drawer.progress(this.backend.getPlayedPercents());

            if (!paused) {
                this.backend.pause();
                this.backend.play();
            }
            this.params.scrollParent = oldScrollParent;
            this.fireEvent('seek', progress);
        },

        stop: function () {
            this.pause();
            this.seekTo(0);
            this.drawer.progress(0);
        },

        /**
     * Set the playback volume.
     *
     * @param {Number} newVolume A value between 0 and 1, 0 being no
     * volume and 1 being full volume.
     */
        setVolume: function (newVolume) {
            this.backend.setVolume(newVolume);
        },

        /**
     * Set the playback rate.
     *
     * @param {Number} rate A positive number. E.g. 0.5 means half the
     * normal speed, 2 means double speed and so on.
     */
        setPlaybackRate: function (rate) {
            this.backend.setPlaybackRate(rate);
        },

        /**
     * Toggle the volume on and off. It not currenly muted it will
     * save the current volume value and turn the volume off.
     * If currently muted then it will restore the volume to the saved
     * value, and then rest the saved value.
     */
        toggleMute: function () {
            if (this.isMuted) {
                // If currently muted then restore to the saved volume
                // and update the mute properties
                this.backend.setVolume(this.savedVolume);
                this.isMuted = false;
            } else {
                // If currently not muted then save current volume,
                // turn off the volume and update the mute properties
                this.savedVolume = this.backend.getVolume();
                this.backend.setVolume(0);
                this.isMuted = true;
            }
        },

        toggleScroll: function () {
            this.params.scrollParent = !this.params.scrollParent;
            this.drawBuffer();
        },

        toggleInteraction: function () {
            this.params.interact = !this.params.interact;
        },

        drawBuffer: function () {
            var nominalWidth = Math.round(
                this.getDuration() * this.params.minPxPerSec * this.params.pixelRatio
            );
            var parentWidth = this.drawer.getWidth();
            var width = nominalWidth;

            // Fill container
            if (this.params.fillParent && (!this.params.scrollParent || nominalWidth < parentWidth)) {
                width = parentWidth;
            }

            var peaks = this.backend.getPeaks(width);
            this.drawer.drawPeaks(peaks, width);
            this.fireEvent('redraw', peaks, width);
        },

        zoom: function (pxPerSec) {
            this.params.minPxPerSec = pxPerSec;

            this.params.scrollParent = true;

            this.drawBuffer();

            this.seekAndCenter(
                this.getCurrentTime() / this.getDuration()
            );
            this.fireEvent('zoom', pxPerSec);
        },

        /**
     * Internal method.
     */
        loadArrayBuffer: function (arraybuffer) {
            this.decodeArrayBuffer(arraybuffer, function (data) {
                this.loadDecodedBuffer(data);
            }.bind(this));
        },

        /**
     * Directly load an externally decoded AudioBuffer.
     */
        loadDecodedBuffer: function (buffer) {
            this.backend.load(buffer);
            this.drawBuffer();
            this.fireEvent('ready');
        },

        /**
     * Loads audio data from a Blob or File object.
     *
     * @param {Blob|File} blob Audio data.
     */
        loadBlob: function (blob) {
            var my = this;
            // Create file reader
            var reader = new FileReader();
            reader.addEventListener('progress', function (e) {
                my.onProgress(e);
            });
            reader.addEventListener('load', function (e) {
                my.loadArrayBuffer(e.target.result);
            });
            reader.addEventListener('error', function () {
                my.fireEvent('error', 'Error reading file');
            });
            reader.readAsArrayBuffer(blob);
            this.empty();
        },

        /**
     * Loads audio and rerenders the waveform.
     */
        load: function (url, peaks) {
            switch (this.params.backend) {
                case 'WebAudio': return this.loadBuffer(url);
                case 'MediaElement': return this.loadMediaElement(url, peaks);
            }
        },

        /**
     * Loads audio using Web Audio buffer backend.
     */
        loadBuffer: function (url) {
            this.empty();
            // load via XHR and render all at once
            return this.getArrayBuffer(url, this.loadArrayBuffer.bind(this));
        },

        /**
     *  Either create a media element, or load
     *  an existing media element.
     *  @param  {String|HTMLElement} urlOrElt Either a path to a media file,
     *                                          or an existing HTML5 Audio/Video
     *                                          Element
     *  @param  {Array}            [peaks]     Array of peaks. Required to bypass
     *                                          web audio dependency
     */
        loadMediaElement: function (urlOrElt, peaks) {
            this.empty();
            var url, elt;
            if (typeof urlOrElt === 'string') {
                url = urlOrElt;
                this.backend.load(url, this.mediaContainer, peaks);
            } else {
                elt = urlOrElt;
                this.backend.loadElt(elt, peaks);

                // if peaks are not provided,
                // url = element.src so we can get peaks with web audio
                if (!peaks) {
                    url = elt.src;
                }
            }

            this.tmpEvents.push(
                this.backend.once('canplay', (function () {
                    this.drawBuffer();
                    this.fireEvent('ready');
                }).bind(this)),

                this.backend.once('error', (function (err) {
                    this.fireEvent('error', err);
                }).bind(this))
            );

            // If no pre-decoded peaks provided, attempt to download the
            // audio file and decode it with Web Audio.
            if (url && !peaks && this.backend.supportsWebAudio()) {
                this.getArrayBuffer(url, (function (arraybuffer) {
                    this.decodeArrayBuffer(arraybuffer, (function (buffer) {
                        this.backend.buffer = buffer;
                        this.drawBuffer();
                    }).bind(this));
                }).bind(this));
            }
        },

        decodeArrayBuffer: function (arraybuffer, callback) {
            this.backend.decodeArrayBuffer(
                arraybuffer,
                this.fireEvent.bind(this, 'decoded'),
                this.fireEvent.bind(this, 'error', 'Error decoding audiobuffer')
            );
            this.tmpEvents.push(
                this.once('decoded', callback)
            );
        },

        getArrayBuffer: function (url, callback) {
            var my = this;

            var ajax = WaveSurfer.util.ajax({
                url: url,
                responseType: 'arraybuffer'
            });

            this.currentAjax = ajax;

            this.tmpEvents.push(
                ajax.on('progress', function (e) {
                    my.onProgress(e);
                }),
                ajax.on('success', function (data, e) {
                    callback(data);
                    my.currentAjax = null;
                }),
                ajax.on('error', function (e) {
                    my.fireEvent('error', 'XHR error: ' + e.target.statusText);
                    my.currentAjax = null;
                })
            );

            return ajax;
        },

        onProgress: function (e) {
            if (e.lengthComputable) {
                var percentComplete = e.loaded / e.total;
            } else {
                // Approximate progress with an asymptotic
                // function, and assume downloads in the 1-3 MB range.
                percentComplete = e.loaded / (e.loaded + 1000000);
            }
            this.fireEvent('loading', Math.round(percentComplete * 100), e.target);
        },

        /**
     * Exports PCM data into a JSON array and opens in a new window.
     */
        exportPCM: function (length, accuracy, noWindow) {
            length = length || 1024;
            accuracy = accuracy || 10000;
            noWindow = noWindow || false;
            var peaks = this.backend.getPeaks(length, accuracy);
            var arr = [].map.call(peaks, function (val) {
                return Math.round(val * accuracy) / accuracy;
            });
            var json = JSON.stringify(arr);
            if (!noWindow) {
                window.open('data:application/json;charset=utf-8,' +
                            encodeURIComponent(json));
            }
            return json;
        },

        cancelAjax: function () {
            if (this.currentAjax) {
                this.currentAjax.xhr.abort();
                this.currentAjax = null;
            }
        },

        clearTmpEvents: function () {
            this.tmpEvents.forEach(function (e) { e.un(); });
        },

        /**
     * Display empty waveform.
     */
        empty: function () {
            if (!this.backend.isPaused()) {
                this.stop();
                this.backend.disconnectSource();
            }
            this.cancelAjax();
            this.clearTmpEvents();
            this.drawer.progress(0);
            this.drawer.setWidth(0);
            this.drawer.drawPeaks({ length: this.drawer.getWidth() }, 0);
        },

        /**
     * Remove events, elements and disconnect WebAudio nodes.
     */
        destroy: function () {
            this.fireEvent('destroy');
            this.cancelAjax();
            this.clearTmpEvents();
            this.unAll();
            this.backend.destroy();
            this.drawer.destroy();
        }
    };

    WaveSurfer.create = function (params) {
        var wavesurfer = Object.create(WaveSurfer);
        wavesurfer.init(params);
        return wavesurfer;
    };

    WaveSurfer.util = {
        extend: function (dest) {
            var sources = Array.prototype.slice.call(arguments, 1);
            sources.forEach(function (source) {
                Object.keys(source).forEach(function (key) {
                    dest[key] = source[key];
                });
            });
            return dest;
        },

        min: function(values) {
            var min = +Infinity;
            for (var i in values) {
                if (values[i] < min) {
                    min = values[i];
                }
            }

            return min;
        },

        max: function(values) {
            var max = -Infinity;
            for (var i in values) {
                if (values[i] > max) {
                    max = values[i];
                }
            }

            return max;
        },

        getId: function () {
            return 'wavesurfer_' + Math.random().toString(32).substring(2);
        },

        ajax: function (options) {
            var ajax = Object.create(WaveSurfer.Observer);
            var xhr = GM_xmlhttpRequest({
                method:  options.method || 'GET',
                url: options.url,
                responseType: options.responseType || 'json',
                onprogress: function (e) {
                    ajax.fireEvent('progress', e);
                    if (e.lengthComputable && e.loaded == e.total) {
                        fired100 = true;
                    }
                },
                onload: function (e) {
                    if (!fired100) {
                        ajax.fireEvent('progress', e);
                    }
                    ajax.fireEvent('load', e);

                    if (200 == e.status || 206 == e.status) {
                        ajax.fireEvent('success', e.response, e);
                    } else {
                        ajax.fireEvent('error', e);
                    }
                },
                onerror: function (e) {
                    ajax.fireEvent('error', e);
                }
            });
            var fired100 = false;
            ajax.xhr = xhr;
            return ajax;
        }
    };

    /* Observer */
    WaveSurfer.Observer = {
        /**
     * Attach a handler function for an event.
     */
        on: function (event, fn) {
            if (!this.handlers) { this.handlers = {}; }

            var handlers = this.handlers[event];
            if (!handlers) {
                handlers = this.handlers[event] = [];
            }
            handlers.push(fn);

            // Return an event descriptor
            return {
                name: event,
                callback: fn,
                un: this.un.bind(this, event, fn)
            };
        },

        /**
     * Remove an event handler.
     */
        un: function (event, fn) {
            if (!this.handlers) { return; }

            var handlers = this.handlers[event];
            if (handlers) {
                if (fn) {
                    for (var i = handlers.length - 1; i >= 0; i--) {
                        if (handlers[i] == fn) {
                            handlers.splice(i, 1);
                        }
                    }
                } else {
                    handlers.length = 0;
                }
            }
        },

        /**
     * Remove all event handlers.
     */
        unAll: function () {
            this.handlers = null;
        },

        /**
     * Attach a handler to an event. The handler is executed at most once per
     * event type.
     */
        once: function (event, handler) {
            var my = this;
            var fn = function () {
                handler.apply(this, arguments);
                setTimeout(function () {
                    my.un(event, fn);
                }, 0);
            };
            return this.on(event, fn);
        },

        fireEvent: function (event) {
            if (!this.handlers) { return; }
            var handlers = this.handlers[event];
            var args = Array.prototype.slice.call(arguments, 1);
            handlers && handlers.forEach(function (fn) {
                fn.apply(null, args);
            });
        }
    };

    /* Make the main WaveSurfer object an observer */
    WaveSurfer.util.extend(WaveSurfer, WaveSurfer.Observer);

    'use strict';

    WaveSurfer.WebAudio = {
        scriptBufferSize: 256,
        PLAYING_STATE: 0,
        PAUSED_STATE: 1,
        FINISHED_STATE: 2,

        supportsWebAudio: function () {
            return !!(window.AudioContext || window.webkitAudioContext);
        },

        getAudioContext: function () {
            if (!WaveSurfer.WebAudio.audioContext) {
                WaveSurfer.WebAudio.audioContext = new (
                    window.AudioContext || window.webkitAudioContext
                );
            }
            return WaveSurfer.WebAudio.audioContext;
        },

        getOfflineAudioContext: function (sampleRate) {
            if (!WaveSurfer.WebAudio.offlineAudioContext) {
                WaveSurfer.WebAudio.offlineAudioContext = new (
                    window.OfflineAudioContext || window.webkitOfflineAudioContext
                )(1, 2, sampleRate);
            }
            return WaveSurfer.WebAudio.offlineAudioContext;
        },

        init: function (params) {
            this.params = params;
            this.ac = params.audioContext || this.getAudioContext();

            this.lastPlay = this.ac.currentTime;
            this.startPosition = 0;
            this.scheduledPause = null;

            this.states = [
                Object.create(WaveSurfer.WebAudio.state.playing),
                Object.create(WaveSurfer.WebAudio.state.paused),
                Object.create(WaveSurfer.WebAudio.state.finished)
            ];

            this.createVolumeNode();
            this.createScriptNode();
            this.createAnalyserNode();

            this.setState(this.PAUSED_STATE);
            this.setPlaybackRate(this.params.audioRate);
        },

        disconnectFilters: function () {
            if (this.filters) {
                this.filters.forEach(function (filter) {
                    filter && filter.disconnect();
                });
                this.filters = null;
                // Reconnect direct path
                this.analyser.connect(this.gainNode);
            }
        },

        setState: function (state) {
            if (this.state !== this.states[state]) {
                this.state = this.states[state];
                this.state.init.call(this);
            }
        },

        // Unpacked filters
        setFilter: function () {
            this.setFilters([].slice.call(arguments));
        },

        /**
     * @param {Array} filters Packed ilters array
     */
        setFilters: function (filters) {
            // Remove existing filters
            this.disconnectFilters();

            // Insert filters if filter array not empty
            if (filters && filters.length) {
                this.filters = filters;

                // Disconnect direct path before inserting filters
                this.analyser.disconnect();

                // Connect each filter in turn
                filters.reduce(function (prev, curr) {
                    prev.connect(curr);
                    return curr;
                }, this.analyser).connect(this.gainNode);
            }

        },

        createScriptNode: function () {
            if (this.ac.createScriptProcessor) {
                this.scriptNode = this.ac.createScriptProcessor(this.scriptBufferSize);
            } else {
                this.scriptNode = this.ac.createJavaScriptNode(this.scriptBufferSize);
            }

            this.scriptNode.connect(this.ac.destination);
        },

        addOnAudioProcess: function () {
            var my = this;

            this.scriptNode.onaudioprocess = function () {
                var time = my.getCurrentTime();

                if (time >= my.getDuration()) {
                    my.setState(my.FINISHED_STATE);
                    my.fireEvent('pause');
                } else if (time >= my.scheduledPause) {
                    my.setState(my.PAUSED_STATE);
                    my.fireEvent('pause');
                } else if (my.state === my.states[my.PLAYING_STATE]) {
                    my.fireEvent('audioprocess', time);
                }
            };
        },

        removeOnAudioProcess: function () {
            this.scriptNode.onaudioprocess = null;
        },

        createAnalyserNode: function () {
            this.analyser = this.ac.createAnalyser();
            this.analyser.connect(this.gainNode);
        },

        /**
     * Create the gain node needed to control the playback volume.
     */
        createVolumeNode: function () {
            // Create gain node using the AudioContext
            if (this.ac.createGain) {
                this.gainNode = this.ac.createGain();
            } else {
                this.gainNode = this.ac.createGainNode();
            }
            // Add the gain node to the graph
            this.gainNode.connect(this.ac.destination);
        },

        /**
     * Set the gain to a new value.
     *
     * @param {Number} newGain The new gain, a floating point value
     * between 0 and 1. 0 being no gain and 1 being maximum gain.
     */
        setVolume: function (newGain) {
            this.gainNode.gain.value = newGain;
        },

        /**
     * Get the current gain.
     *
     * @returns {Number} The current gain, a floating point value
     * between 0 and 1. 0 being no gain and 1 being maximum gain.
     */
        getVolume: function () {
            return this.gainNode.gain.value;
        },

        decodeArrayBuffer: function (arraybuffer, callback, errback) {
            if (!this.offlineAc) {
                this.offlineAc = this.getOfflineAudioContext(this.ac ? this.ac.sampleRate : 44100);
            }
            this.offlineAc.decodeAudioData(arraybuffer, (function (data) {
                callback(data);
            }).bind(this), errback);
        },

        /**
     * Compute the max and min value of the waveform when broken into
     * <length> subranges.
     * @param {Number} How many subranges to break the waveform into.
     * @returns {Array} Array of 2*<length> peaks or array of arrays
     * of peaks consisting of (max, min) values for each subrange.
     */
        getPeaks: function (length) {
            var sampleSize = this.buffer.length / length;
            var sampleStep = ~~(sampleSize / 10) || 1;
            var channels = this.buffer.numberOfChannels;
            var splitPeaks = [];
            var mergedPeaks = [];

            for (var c = 0; c < channels; c++) {
                var peaks = splitPeaks[c] = [];
                var chan = this.buffer.getChannelData(c);

                for (var i = 0; i < length; i++) {
                    var start = ~~(i * sampleSize);
                    var end = ~~(start + sampleSize);
                    var min = 0;
                    var max = 0;

                    for (var j = start; j < end; j += sampleStep) {
                        var value = chan[j];

                        if (value > max) {
                            max = value;
                        }

                        if (value < min) {
                            min = value;
                        }
                    }

                    peaks[2 * i] = max;
                    peaks[2 * i + 1] = min;

                    if (c == 0 || max > mergedPeaks[2 * i]) {
                        mergedPeaks[2 * i] = max;
                    }

                    if (c == 0 || min < mergedPeaks[2 * i + 1]) {
                        mergedPeaks[2 * i + 1] = min;
                    }
                }
            }

            return this.params.splitChannels ? splitPeaks : mergedPeaks;
        },

        getPlayedPercents: function () {
            return this.state.getPlayedPercents.call(this);
        },

        disconnectSource: function () {
            if (this.source) {
                this.source.disconnect();
            }
        },

        destroy: function () {
            if (!this.isPaused()) {
                this.pause();
            }
            this.unAll();
            this.buffer = null;
            this.disconnectFilters();
            this.disconnectSource();
            this.gainNode.disconnect();
            this.scriptNode.disconnect();
            this.analyser.disconnect();
        },

        load: function (buffer) {
            this.startPosition = 0;
            this.lastPlay = this.ac.currentTime;
            this.buffer = buffer;
            this.createSource();
        },

        createSource: function () {
            this.disconnectSource();
            this.source = this.ac.createBufferSource();

            //adjust for old browsers.
            this.source.start = this.source.start || this.source.noteGrainOn;
            this.source.stop = this.source.stop || this.source.noteOff;

            this.source.playbackRate.value = this.playbackRate;
            this.source.buffer = this.buffer;
            this.source.connect(this.analyser);
        },

        isPaused: function () {
            return this.state !== this.states[this.PLAYING_STATE];
        },

        getDuration: function () {
            if (!this.buffer) {
                return 0;
            }
            return this.buffer.duration;
        },

        seekTo: function (start, end) {
            this.scheduledPause = null;

            if (start == null) {
                start = this.getCurrentTime();
                if (start >= this.getDuration()) {
                    start = 0;
                }
            }
            if (end == null) {
                end = this.getDuration();
            }

            this.startPosition = start;
            this.lastPlay = this.ac.currentTime;

            if (this.state === this.states[this.FINISHED_STATE]) {
                this.setState(this.PAUSED_STATE);
            }

            return { start: start, end: end };
        },

        getPlayedTime: function () {
            return (this.ac.currentTime - this.lastPlay) * this.playbackRate;
        },

        /**
     * Plays the loaded audio region.
     *
     * @param {Number} start Start offset in seconds,
     * relative to the beginning of a clip.
     * @param {Number} end When to stop
     * relative to the beginning of a clip.
     */
        play: function (start, end) {
            // need to re-create source on each playback
            this.createSource();

            var adjustedTime = this.seekTo(start, end);

            start = adjustedTime.start;
            end = adjustedTime.end;

            this.scheduledPause = end;

            this.source.start(0, start, end - start);

            this.setState(this.PLAYING_STATE);

            this.fireEvent('play');
        },

        /**
     * Pauses the loaded audio.
     */
        pause: function () {
            this.scheduledPause = null;

            this.startPosition += this.getPlayedTime();
            this.source && this.source.stop(0);

            this.setState(this.PAUSED_STATE);

            this.fireEvent('pause');
        },

        /**
    *   Returns the current time in seconds relative to the audioclip's duration.
    */
        getCurrentTime: function () {
            return this.state.getCurrentTime.call(this);
        },

        /**
     * Set the audio source playback rate.
     */
        setPlaybackRate: function (value) {
            value = value || 1;
            if (this.isPaused()) {
                this.playbackRate = value;
            } else {
                this.pause();
                this.playbackRate = value;
                this.play();
            }
        }
    };

    WaveSurfer.WebAudio.state = {};

    WaveSurfer.WebAudio.state.playing = {
        init: function () {
            this.addOnAudioProcess();
        },
        getPlayedPercents: function () {
            var duration = this.getDuration();
            return (this.getCurrentTime() / duration) || 0;
        },
        getCurrentTime: function () {
            return this.startPosition + this.getPlayedTime();
        }
    };

    WaveSurfer.WebAudio.state.paused = {
        init: function () {
            this.removeOnAudioProcess();
        },
        getPlayedPercents: function () {
            var duration = this.getDuration();
            return (this.getCurrentTime() / duration) || 0;
        },
        getCurrentTime: function () {
            return this.startPosition;
        }
    };

    WaveSurfer.WebAudio.state.finished = {
        init: function () {
            this.removeOnAudioProcess();
            this.fireEvent('finish');
        },
        getPlayedPercents: function () {
            return 1;
        },
        getCurrentTime: function () {
            return this.getDuration();
        }
    };

    WaveSurfer.util.extend(WaveSurfer.WebAudio, WaveSurfer.Observer);

    'use strict';

    WaveSurfer.MediaElement = Object.create(WaveSurfer.WebAudio);

    WaveSurfer.util.extend(WaveSurfer.MediaElement, {
        init: function (params) {
            this.params = params;

            // Dummy media to catch errors
            this.media = {
                currentTime: 0,
                duration: 0,
                paused: true,
                playbackRate: 1,
                play: function () {},
                pause: function () {}
            };

            this.mediaType = params.mediaType.toLowerCase();
            this.elementPosition = params.elementPosition;
            this.setPlaybackRate(this.params.audioRate);
            this.createTimer();
        },


        /**
     * Create a timer to provide a more precise `audioprocess' event.
     */
        createTimer: function () {
            var my = this;
            var playing = false;

            var onAudioProcess = function () {
                if (my.isPaused()) { return; }

                my.fireEvent('audioprocess', my.getCurrentTime());

                // Call again in the next frame
                var requestAnimationFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame;
                requestAnimationFrame(onAudioProcess);
            };

            this.on('play', onAudioProcess);
        },

        /**
     *  Create media element with url as its source,
     *  and append to container element.
     *  @param  {String}        url         path to media file
     *  @param  {HTMLElement}   container   HTML element
     *  @param  {Array}         peaks       array of peak data
     */
        load: function (url, container, peaks) {
            var my = this;

            var media = document.createElement(this.mediaType);
            media.controls = this.params.mediaControls;
            media.autoplay = this.params.autoplay || false;
            media.preload = 'auto';
            media.src = url;
            media.style.width = '100%';

            var prevMedia = container.querySelector(this.mediaType);
            if (prevMedia) {
                container.removeChild(prevMedia);
            }
            container.appendChild(media);

            this._load(media, peaks);
        },

        /**
     *  Load existing media element.
     *  @param  {MediaElement}  elt     HTML5 Audio or Video element
     *  @param  {Array}         peaks   array of peak data
     */
        loadElt: function (elt, peaks) {
            var my = this;

            var media = elt;
            media.controls = this.params.mediaControls;
            media.autoplay = this.params.autoplay || false;

            this._load(media, peaks);
        },

        /**
     *  Private method called by both load (from url)
     *  and loadElt (existing media element).
     *  @param  {MediaElement}  media     HTML5 Audio or Video element
     *  @param  {Array}         peaks   array of peak data
     *  @private
     */
        _load: function (media, peaks) {
            var my = this;

            media.addEventListener('error', function () {
                my.fireEvent('error', 'Error loading media element');
            });

            media.addEventListener('canplay', function () {
                my.fireEvent('canplay');
            });

            media.addEventListener('ended', function () {
                my.fireEvent('finish');
            });

            this.media = media;
            this.peaks = peaks;
            this.onPlayEnd = null;
            this.buffer = null;
            this.setPlaybackRate(this.playbackRate);
        },

        isPaused: function () {
            return !this.media || this.media.paused;
        },

        getDuration: function () {
            var duration = this.media.duration;
            if (duration >= Infinity) { // streaming audio
                duration = this.media.seekable.end(0);
            }
            return duration;
        },

        getCurrentTime: function () {
            return this.media && this.media.currentTime;
        },

        getPlayedPercents: function () {
            return (this.getCurrentTime() / this.getDuration()) || 0;
        },

        /**
     * Set the audio source playback rate.
     */
        setPlaybackRate: function (value) {
            this.playbackRate = value || 1;
            this.media.playbackRate = this.playbackRate;
        },

        seekTo: function (start) {
            if (start != null) {
                this.media.currentTime = start;
            }
            this.clearPlayEnd();
        },

        /**
     * Plays the loaded audio region.
     *
     * @param {Number} start Start offset in seconds,
     * relative to the beginning of a clip.
     * @param {Number} end End offset in seconds,
     * relative to the beginning of a clip.
     */
        play: function (start, end) {
            this.seekTo(start);
            this.media.play();
            end && this.setPlayEnd(end);
            this.fireEvent('play');
        },

        /**
     * Pauses the loaded audio.
     */
        pause: function () {
            this.media && this.media.pause();
            this.clearPlayEnd();
            this.fireEvent('pause');
        },

        setPlayEnd: function (end) {
            var my = this;
            this.onPlayEnd = function (time) {
                if (time >= end) {
                    my.pause();
                    my.seekTo(end);
                }
            };
            this.on('audioprocess', this.onPlayEnd);
        },

        clearPlayEnd: function () {
            if (this.onPlayEnd) {
                this.un('audioprocess', this.onPlayEnd);
                this.onPlayEnd = null;
            }
        },

        getPeaks: function (length) {
            if (this.buffer) {
                return WaveSurfer.WebAudio.getPeaks.call(this, length);
            }
            return this.peaks || [];
        },

        getVolume: function () {
            return this.media.volume;
        },

        setVolume: function (val) {
            this.media.volume = val;
        },

        destroy: function () {
            this.pause();
            this.unAll();
            this.media && this.media.parentNode && this.media.parentNode.removeChild(this.media);
            this.media = null;
        }
    });

    //For backwards compatibility
    WaveSurfer.AudioElement = WaveSurfer.MediaElement;

    'use strict';

    WaveSurfer.Drawer = {
        init: function (container, params) {
            this.container = container;
            this.params = params;

            this.width = 0;
            this.height = params.height * this.params.pixelRatio;

            this.lastPos = 0;

            this.initDrawer(params);
            this.createWrapper();
            this.createElements();
        },

        createWrapper: function () {
            this.wrapper = this.container.appendChild(
                document.createElement('wave')
            );

            this.style(this.wrapper, {
                display: 'block',
                position: 'relative',
                userSelect: 'none',
                webkitUserSelect: 'none',
                height: this.params.height + 'px'
            });

            if (this.params.fillParent || this.params.scrollParent) {
                this.style(this.wrapper, {
                    width: '100%',
                    overflowX: this.params.hideScrollbar ? 'hidden' : 'auto',
                    overflowY: 'hidden'
                });
            }

            this.setupWrapperEvents();
        },

        handleEvent: function (e) {
            e.preventDefault();

            var bbox = this.wrapper.getBoundingClientRect();

            var nominalWidth = this.width;
            var parentWidth = this.getWidth();

            var progress;

            if (!this.params.fillParent && nominalWidth < parentWidth) {
                progress = ((e.clientX - bbox.left) * this.params.pixelRatio / nominalWidth) || 0;

                if (progress > 1) {
                    progress = 1;
                }
            } else {
                progress = ((e.clientX - bbox.left + this.wrapper.scrollLeft) / this.wrapper.scrollWidth) || 0;
            }

            return progress;
        },

        setupWrapperEvents: function () {
            var my = this;

            this.wrapper.addEventListener('click', function (e) {
                var scrollbarHeight = my.wrapper.offsetHeight - my.wrapper.clientHeight;
                if (scrollbarHeight != 0) {
                    // scrollbar is visible.  Check if click was on it
                    var bbox = my.wrapper.getBoundingClientRect();
                    if (e.clientY >= bbox.bottom - scrollbarHeight) {
                        // ignore mousedown as it was on the scrollbar
                        return;
                    }
                }

                if (my.params.interact) {
                    my.fireEvent('click', e, my.handleEvent(e));
                }
            });

            this.wrapper.addEventListener('scroll', function (e) {
                my.fireEvent('scroll', e);
            });
        },

        drawPeaks: function (peaks, length) {
            this.resetScroll();
            this.setWidth(length);

            this.params.barWidth ?
                this.drawBars(peaks) :
            this.drawWave(peaks);
        },

        style: function (el, styles) {
            Object.keys(styles).forEach(function (prop) {
                if (el.style[prop] !== styles[prop]) {
                    el.style[prop] = styles[prop];
                }
            });
            return el;
        },

        resetScroll: function () {
            if (this.wrapper !== null) {
                this.wrapper.scrollLeft = 0;
            }
        },

        recenter: function (percent) {
            var position = this.wrapper.scrollWidth * percent;
            this.recenterOnPosition(position, true);
        },

        recenterOnPosition: function (position, immediate) {
            var scrollLeft = this.wrapper.scrollLeft;
            var half = ~~(this.wrapper.clientWidth / 2);
            var target = position - half;
            var offset = target - scrollLeft;
            var maxScroll = this.wrapper.scrollWidth - this.wrapper.clientWidth;

            if (maxScroll == 0) {
                // no need to continue if scrollbar is not there
                return;
            }

            // if the cursor is currently visible...
            if (!immediate && -half <= offset && offset < half) {
                // we'll limit the "re-center" rate.
                var rate = 5;
                offset = Math.max(-rate, Math.min(rate, offset));
                target = scrollLeft + offset;
            }

            // limit target to valid range (0 to maxScroll)
            target = Math.max(0, Math.min(maxScroll, target));
            // no use attempting to scroll if we're not moving
            if (target != scrollLeft) {
                this.wrapper.scrollLeft = target;
            }

        },

        getWidth: function () {
            return Math.round(this.container.clientWidth * this.params.pixelRatio);
        },

        setWidth: function (width) {
            if (width == this.width) { return; }

            this.width = width;

            if (this.params.fillParent || this.params.scrollParent) {
                this.style(this.wrapper, {
                    width: ''
                });
            } else {
                this.style(this.wrapper, {
                    width: ~~(this.width / this.params.pixelRatio) + 'px'
                });
            }

            this.updateSize();
        },

        setHeight: function (height) {
            if (height == this.height) { return; }
            this.height = height;
            this.style(this.wrapper, {
                height: ~~(this.height / this.params.pixelRatio) + 'px'
            });
            this.updateSize();
        },

        progress: function (progress) {
            var minPxDelta = 1 / this.params.pixelRatio;
            var pos = Math.round(progress * this.width) * minPxDelta;

            if (pos < this.lastPos || pos - this.lastPos >= minPxDelta) {
                this.lastPos = pos;

                if (this.params.scrollParent && this.params.autoCenter) {
                    var newPos = ~~(this.wrapper.scrollWidth * progress);
                    this.recenterOnPosition(newPos);
                }

                this.updateProgress(progress);
            }
        },

        destroy: function () {
            this.unAll();
            if (this.wrapper) {
                this.container.removeChild(this.wrapper);
                this.wrapper = null;
            }
        },

        /* Renderer-specific methods */
        initDrawer: function () {},

        createElements: function () {},

        updateSize: function () {},

        drawWave: function (peaks, max) {},

        clearWave: function () {},

        updateProgress: function (position) {}
    };

    WaveSurfer.util.extend(WaveSurfer.Drawer, WaveSurfer.Observer);

    'use strict';

    WaveSurfer.Drawer.Canvas = Object.create(WaveSurfer.Drawer);

    WaveSurfer.util.extend(WaveSurfer.Drawer.Canvas, {
        createElements: function () {
            var waveCanvas = this.wrapper.appendChild(
                this.style(document.createElement('canvas'), {
                    position: 'absolute',
                    zIndex: 1,
                    left: 0,
                    top: 0,
                    bottom: 0
                })
            );
            this.waveCc = waveCanvas.getContext('2d');

            this.progressWave = this.wrapper.appendChild(
                this.style(document.createElement('wave'), {
                    position: 'absolute',
                    zIndex: 2,
                    left: 0,
                    top: 0,
                    bottom: 0,
                    overflow: 'hidden',
                    width: '0',
                    display: 'none',
                    boxSizing: 'border-box',
                    borderRightStyle: 'solid',
                    borderRightWidth: this.params.cursorWidth + 'px',
                    borderRightColor: this.params.cursorColor
                })
            );

            if (this.params.waveColor != this.params.progressColor) {
                var progressCanvas = this.progressWave.appendChild(
                    document.createElement('canvas')
                );
                this.progressCc = progressCanvas.getContext('2d');
            }
        },

        updateSize: function () {
            var width = Math.round(this.width / this.params.pixelRatio);

            this.waveCc.canvas.width = this.width;
            this.waveCc.canvas.height = this.height;
            this.style(this.waveCc.canvas, { width: width + 'px'});

            this.style(this.progressWave, { display: 'block'});

            if (this.progressCc) {
                this.progressCc.canvas.width = this.width;
                this.progressCc.canvas.height = this.height;
                this.style(this.progressCc.canvas, { width: width + 'px'});
            }

            this.clearWave();
        },

        clearWave: function () {
            this.waveCc.clearRect(0, 0, this.width, this.height);
            if (this.progressCc) {
                this.progressCc.clearRect(0, 0, this.width, this.height);
            }
        },

        drawBars: function (peaks, channelIndex) {
            // Split channels
            if (peaks[0] instanceof Array) {
                var channels = peaks;
                if (this.params.splitChannels) {
                    this.setHeight(channels.length * this.params.height * this.params.pixelRatio);
                    channels.forEach(this.drawBars, this);
                    return;
                } else {
                    peaks = channels[0];
                }
            }

            // Bar wave draws the bottom only as a reflection of the top,
            // so we don't need negative values
            var hasMinVals = [].some.call(peaks, function (val) { return val < 0; });
            if (hasMinVals) {
                peaks = [].filter.call(peaks, function (_, index) { return index % 2 == 0; });
            }

            // A half-pixel offset makes lines crisp
            var $ = 0.5 / this.params.pixelRatio;
            var width = this.width;
            var height = this.params.height * this.params.pixelRatio;
            var offsetY = height * channelIndex || 0;
            var halfH = height / 2;
            var length = peaks.length;
            var bar = this.params.barWidth * this.params.pixelRatio;
            var gap = Math.max(this.params.pixelRatio, ~~(bar / 2));
            var step = bar + gap;

            var absmax = 1;
            if (this.params.normalize) {
                absmax = Math.max.apply(Math, peaks);
            }

            var scale = length / width;

            this.waveCc.fillStyle = this.params.waveColor;
            if (this.progressCc) {
                this.progressCc.fillStyle = this.params.progressColor;
            }

            [ this.waveCc, this.progressCc ].forEach(function (cc) {
                if (!cc) { return; }

                for (var i = 0; i < width; i += step) {
                    var h = Math.round(peaks[Math.floor(i * scale)] / absmax * halfH);
                    cc.fillRect(i + $, halfH - h + offsetY, bar + $, h * 2);
                }
            }, this);
        },

        drawWave: function (peaks, channelIndex) {
            // Split channels
            if (peaks[0] instanceof Array) {
                var channels = peaks;
                if (this.params.splitChannels) {
                    this.setHeight(channels.length * this.params.height * this.params.pixelRatio);
                    channels.forEach(this.drawWave, this);
                    return;
                } else {
                    peaks = channels[0];
                }
            }

            // Support arrays without negative peaks
            var hasMinValues = [].some.call(peaks, function (val) { return val < 0; });
            if (!hasMinValues) {
                var reflectedPeaks = [];
                for (var i = 0, len = peaks.length; i < len; i++) {
                    reflectedPeaks[2 * i] = peaks[i];
                    reflectedPeaks[2 * i + 1] = -peaks[i];
                }
                peaks = reflectedPeaks;
            }

            // A half-pixel offset makes lines crisp
            var $ = 0.5 / this.params.pixelRatio;
            var height = this.params.height * this.params.pixelRatio;
            var offsetY = height * channelIndex || 0;
            var halfH = height / 2;
            var length = ~~(peaks.length / 2);

            var scale = 1;
            if (this.params.fillParent && this.width != length) {
                scale = this.width / length;
            }

            var absmax = 1;
            if (this.params.normalize) {
                var max = Math.max.apply(Math, peaks);
                var min = Math.min.apply(Math, peaks);
                absmax = -min > max ? -min : max;
            }

            this.waveCc.fillStyle = this.params.waveColor;
            if (this.progressCc) {
                this.progressCc.fillStyle = this.params.progressColor;
            }

            [ this.waveCc, this.progressCc ].forEach(function (cc) {
                if (!cc) { return; }

                cc.beginPath();
                cc.moveTo($, halfH + offsetY);

                for (var i = 0; i < length; i++) {
                    var h = Math.round(peaks[2 * i] / absmax * halfH);
                    cc.lineTo(i * scale + $, halfH - h + offsetY);
                }

                // Draw the bottom edge going backwards, to make a single
                // closed hull to fill.
                for (var i = length - 1; i >= 0; i--) {
                    var h = Math.round(peaks[2 * i + 1] / absmax * halfH);
                    cc.lineTo(i * scale + $, halfH - h + offsetY);
                }

                cc.closePath();
                cc.fill();

                // Always draw a median line
                cc.fillRect(0, halfH + offsetY - $, this.width, $);
            }, this);
        },

        updateProgress: function (progress) {
            var pos = Math.round(
                this.width * progress
            ) / this.params.pixelRatio;
            this.style(this.progressWave, { width: pos + 'px' });
        }
    });

    'use strict';

    WaveSurfer.Drawer.MultiCanvas = Object.create(WaveSurfer.Drawer);

    WaveSurfer.util.extend(WaveSurfer.Drawer.MultiCanvas, {

        initDrawer: function (params) {
            this.maxCanvasWidth = params.maxCanvasWidth != null ? params.maxCanvasWidth : 4000;
            this.maxCanvasElementWidth = Math.round(this.maxCanvasWidth / this.params.pixelRatio);

            if (this.maxCanvasWidth <= 1) {
                throw 'maxCanvasWidth must be greater than 1.';
            } else if (this.maxCanvasWidth % 2 == 1) {
                throw 'maxCanvasWidth must be an even number.';
            }

            this.hasProgressCanvas = this.params.waveColor != this.params.progressColor;
            this.halfPixel = 0.5 / this.params.pixelRatio;
            this.canvases = [];
        },

        createElements: function () {
            this.progressWave = this.wrapper.appendChild(
                this.style(document.createElement('wave'), {
                    position: 'absolute',
                    zIndex: 2,
                    left: 0,
                    top: 0,
                    bottom: 0,
                    overflow: 'hidden',
                    width: '0',
                    display: 'none',
                    boxSizing: 'border-box',
                    borderRightStyle: 'solid',
                    borderRightWidth: this.params.cursorWidth + 'px',
                    borderRightColor: this.params.cursorColor
                })
            );

            this.addCanvas();
        },

        updateSize: function () {
            var totalWidth = Math.round(this.width / this.params.pixelRatio),
                requiredCanvases = Math.ceil(totalWidth / this.maxCanvasElementWidth);

            while (this.canvases.length < requiredCanvases) {
                this.addCanvas();
            }

            while (this.canvases.length > requiredCanvases) {
                this.removeCanvas();
            }

            for (var i in this.canvases) {
                // Add some overlap to prevent vertical white stripes, keep the width even for simplicity.
                var canvasWidth = this.maxCanvasWidth + 2 * Math.ceil(this.params.pixelRatio / 2);

                if (i == this.canvases.length - 1) {
                    canvasWidth = this.width - (this.maxCanvasWidth * (this.canvases.length - 1));
                }

                this.updateDimensions(this.canvases[i], canvasWidth, this.height);
                this.clearWaveForEntry(this.canvases[i]);
            }
        },

        addCanvas: function () {
            var entry = {};
            var leftOffset = this.maxCanvasElementWidth * this.canvases.length;

            entry.wave = this.wrapper.appendChild(
                this.style(document.createElement('canvas'), {
                    position: 'absolute',
                    zIndex: 1,
                    left: leftOffset + 'px',
                    top: 0,
                    bottom: 0
                })
            );
            entry.waveCtx = entry.wave.getContext('2d');

            if (this.hasProgressCanvas) {
                entry.progress = this.progressWave.appendChild(
                    this.style(document.createElement('canvas'), {
                        position: 'absolute',
                        left: leftOffset + 'px',
                        top: 0,
                        bottom: 0
                    })
                );
                entry.progressCtx = entry.progress.getContext('2d');
            }

            this.canvases.push(entry);
        },

        removeCanvas: function () {
            var lastEntry = this.canvases.pop();
            lastEntry.wave.parentElement.removeChild(lastEntry.wave);
            if (this.hasProgressCanvas) {
                lastEntry.progress.parentElement.removeChild(lastEntry.progress);
            }
        },

        updateDimensions: function (entry, width, height) {
            var elementWidth = Math.round(width / this.params.pixelRatio);

            entry.waveCtx.canvas.width = width;
            entry.waveCtx.canvas.height = height;
            this.style(entry.waveCtx.canvas, { width: elementWidth + 'px'});

            this.style(this.progressWave, { display: 'block'});

            if (this.hasProgressCanvas) {
                entry.progressCtx.canvas.width = width;
                entry.progressCtx.canvas.height = height;
                this.style(entry.progressCtx.canvas, { width: elementWidth + 'px'});
            }
        },

        clearWave: function () {
            for (var i in this.canvases) {
                this.clearWaveForEntry(this.canvases[i]);
            }
        },

        clearWaveForEntry: function (entry) {
            entry.waveCtx.clearRect(0, 0, entry.waveCtx.canvas.width, entry.waveCtx.canvas.height);
            if (this.hasProgressCanvas) {
                entry.progressCtx.clearRect(0, 0, entry.progressCtx.canvas.width, entry.progressCtx.canvas.height);
            }
        },

        drawBars: function (peaks, channelIndex) {
            // Split channels
            if (peaks[0] instanceof Array) {
                var channels = peaks;
                if (this.params.splitChannels) {
                    this.setHeight(channels.length * this.params.height * this.params.pixelRatio);
                    channels.forEach(this.drawBars, this);
                    return;
                } else {
                    peaks = channels[0];
                }
            }

            // Bar wave draws the bottom only as a reflection of the top,
            // so we don't need negative values
            var hasMinVals = [].some.call(peaks, function (val) { return val < 0; });
            if (hasMinVals) {
                peaks = [].filter.call(peaks, function (_, index) { return index % 2 == 0; });
            }

            // A half-pixel offset makes lines crisp
            var width = this.width;
            var height = this.params.height * this.params.pixelRatio;
            var offsetY = height * channelIndex || 0;
            var halfH = height / 2;
            var length = peaks.length;
            var bar = this.params.barWidth * this.params.pixelRatio;
            var gap = Math.max(this.params.pixelRatio, ~~(bar / 2));
            var step = bar + gap;

            var absmax = 1;
            if (this.params.normalize) {
                absmax = WaveSurfer.util.max(peaks);
            }

            var scale = length / width;

            this.canvases[0].waveCtx.fillStyle = this.params.waveColor;
            if (this.canvases[0].progressCtx) {
                this.canvases[0].progressCtx.fillStyle = this.params.progressColor;
            }

            for (var i = 0; i < width; i += step) {
                var h = Math.round(peaks[Math.floor(i * scale)] / absmax * halfH);
                this.fillRect(i + this.halfPixel, halfH - h + offsetY, bar + this.halfPixel, h * 2);
            }
        },

        drawWave: function (peaks, channelIndex) {
            // Split channels
            if (peaks[0] instanceof Array) {
                var channels = peaks;
                if (this.params.splitChannels) {
                    this.setHeight(channels.length * this.params.height * this.params.pixelRatio);
                    channels.forEach(this.drawWave, this);
                    return;
                } else {
                    peaks = channels[0];
                }
            }

            // Support arrays without negative peaks
            var hasMinValues = [].some.call(peaks, function (val) { return val < 0; });
            if (!hasMinValues) {
                var reflectedPeaks = [];
                for (var i = 0, len = peaks.length; i < len; i++) {
                    reflectedPeaks[2 * i] = peaks[i];
                    reflectedPeaks[2 * i + 1] = -peaks[i];
                }
                peaks = reflectedPeaks;
            }

            // A half-pixel offset makes lines crisp
            var height = this.params.height * this.params.pixelRatio;
            var offsetY = height * channelIndex || 0;
            var halfH = height / 2;
            var length = ~~(peaks.length / this.canvases.length / 2);

            var absmax = 1;
            if (this.params.normalize) {
                var max = WaveSurfer.util.max(peaks);
                var min = WaveSurfer.util.min(peaks);
                absmax = -min > max ? -min : max;
            }

            this.drawLine(length, peaks, absmax, halfH, offsetY);

            // Always draw a median line
            this.fillRect(0, halfH + offsetY - this.halfPixel, this.width, this.halfPixel);
        },

        drawLine: function (length, peaks, absmax, halfH, offsetY) {
            for (var index in this.canvases) {
                var entry = this.canvases[index];

                this.setFillStyles(entry);

                this.drawLineToContext(entry.waveCtx, length, index, peaks, absmax, halfH, offsetY);
                this.drawLineToContext(entry.progressCtx, length, index, peaks, absmax, halfH, offsetY);
            }
        },

        drawLineToContext: function (ctx, length, index, peaks, absmax, halfH, offsetY) {
            if (!ctx) { return; }

            var scale = 1;
            if (this.params.fillParent && this.width != length) {
                scale = ctx.canvas.width / length;
            }

            var first = index * length,
                last = first + length + 1;

            ctx.beginPath();
            ctx.moveTo(this.halfPixel, halfH + offsetY);

            for (var i = first; i < last; i++) {
                var h = Math.round(peaks[2 * i] / absmax * halfH);
                ctx.lineTo((i - first) * scale + this.halfPixel, halfH - h + offsetY);
            }

            // Draw the bottom edge going backwards, to make a single
            // closed hull to fill.
            for (var i = last - 1; i >= first; i--) {
                var h = Math.round(peaks[2 * i + 1] / absmax * halfH);
                ctx.lineTo((i - first) * scale + this.halfPixel, halfH - h + offsetY);
            }

            ctx.closePath();
            ctx.fill();
        },

        fillRect: function (x, y, width, height) {
            for (var i in this.canvases) {
                var entry = this.canvases[i],
                    leftOffset = i * this.maxCanvasWidth;

                var intersection = {
                    x1: Math.max(x, i * this.maxCanvasWidth),
                    y1: y,
                    x2: Math.min(x + width, i * this.maxCanvasWidth + entry.waveCtx.canvas.width),
                    y2: y + height
                };

                if (intersection.x1 < intersection.x2) {
                    this.setFillStyles(entry);

                    this.fillRectToContext(entry.waveCtx,
                                           intersection.x1 - leftOffset,
                                           intersection.y1,
                                           intersection.x2 - intersection.x1,
                                           intersection.y2 - intersection.y1);

                    this.fillRectToContext(entry.progressCtx,
                                           intersection.x1 - leftOffset,
                                           intersection.y1,
                                           intersection.x2 - intersection.x1,
                                           intersection.y2 - intersection.y1);
                }
            }
        },

        fillRectToContext: function (ctx, x, y, width, height) {
            if (!ctx) { return; }
            ctx.fillRect(x, y, width, height);
        },

        setFillStyles: function (entry) {
            entry.waveCtx.fillStyle = this.params.waveColor;
            if (this.hasProgressCanvas) {
                entry.progressCtx.fillStyle = this.params.progressColor;
            }
        },

        updateProgress: function (progress) {
            var pos = Math.round(
                this.width * progress
            ) / this.params.pixelRatio;
            this.style(this.progressWave, { width: pos + 'px' });
        }
    });

    'use strict';

    /* Init from HTML */
    (function () {
        var init = function () {
            var containers = document.querySelectorAll('wavesurfer');

            Array.prototype.forEach.call(containers, function (el) {
                var params = WaveSurfer.util.extend({
                    container: el,
                    backend: 'MediaElement',
                    mediaControls: true
                }, el.dataset);

                el.style.display = 'block';

                var wavesurfer = WaveSurfer.create(params);

                if (el.dataset.peaks) {
                    var peaks = JSON.parse(el.dataset.peaks);
                }

                wavesurfer.load(el.dataset.url, peaks);
            });
        };

        if (document.readyState === 'complete') {
            init();
        } else {
            window.addEventListener('load', init);
        }
    }());

    return WaveSurfer;

}));