Greasy Fork is available in English.

WaveSurfer - Tampermonkey

WaveSurfer

Tätä skriptiä ei tulisi asentaa suoraan. Se on kirjasto muita skriptejä varten sisällytettäväksi metadirektiivillä // @require https://update.greasyfork.org/scripts/32322/212062/WaveSurfer%20-%20Tampermonkey.js.

// ==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;

}));