Index: gs3-extensions/mars-src/trunk/src/wavesurfer-renderer/drawer.spectrummulticanvas.js
===================================================================
--- gs3-extensions/mars-src/trunk/src/wavesurfer-renderer/drawer.spectrummulticanvas.js	(revision 34668)
+++ gs3-extensions/mars-src/trunk/src/wavesurfer-renderer/drawer.spectrummulticanvas.js	(revision 34668)
@@ -0,0 +1,979 @@
+import Drawer from './drawer';
+import * as util from './util';
+import CanvasEntry from './drawer.canvasentry';
+
+/**
+ * Calculate FFT - Based on https://github.com/corbanbrook/dsp.js
+ */
+/* eslint-disable complexity, no-redeclare, no-var, one-var */
+const FFT = function(bufferSize, sampleRate, windowFunc, alpha) {
+    this.bufferSize = bufferSize;
+    this.sampleRate = sampleRate;
+    this.bandwidth = (2 / bufferSize) * (sampleRate / 2);
+
+    this.sinTable = new Float32Array(bufferSize);
+    this.cosTable = new Float32Array(bufferSize);
+    this.windowValues = new Float32Array(bufferSize);
+    this.reverseTable = new Uint32Array(bufferSize);
+
+    this.peakBand = 0;
+    this.peak = 0;
+
+    var i;
+    switch (windowFunc) {
+        case 'bartlett':
+            for (i = 0; i < bufferSize; i++) {
+                this.windowValues[i] =
+                    (2 / (bufferSize - 1)) *
+                    ((bufferSize - 1) / 2 - Math.abs(i - (bufferSize - 1) / 2));
+            }
+            break;
+        case 'bartlettHann':
+            for (i = 0; i < bufferSize; i++) {
+                this.windowValues[i] =
+                    0.62 -
+                    0.48 * Math.abs(i / (bufferSize - 1) - 0.5) -
+                    0.38 * Math.cos((Math.PI * 2 * i) / (bufferSize - 1));
+            }
+            break;
+        case 'blackman':
+            alpha = alpha || 0.16;
+            for (i = 0; i < bufferSize; i++) {
+                this.windowValues[i] =
+                    (1 - alpha) / 2 -
+                    0.5 * Math.cos((Math.PI * 2 * i) / (bufferSize - 1)) +
+                    (alpha / 2) *
+                        Math.cos((4 * Math.PI * i) / (bufferSize - 1));
+            }
+            break;
+        case 'cosine':
+            for (i = 0; i < bufferSize; i++) {
+                this.windowValues[i] = Math.cos(
+                    (Math.PI * i) / (bufferSize - 1) - Math.PI / 2
+                );
+            }
+            break;
+        case 'gauss':
+            alpha = alpha || 0.25;
+            for (i = 0; i < bufferSize; i++) {
+                this.windowValues[i] = Math.pow(
+                    Math.E,
+                    -0.5 *
+                        Math.pow(
+                            (i - (bufferSize - 1) / 2) /
+                                ((alpha * (bufferSize - 1)) / 2),
+                            2
+                        )
+                );
+            }
+            break;
+        case 'hamming':
+            for (i = 0; i < bufferSize; i++) {
+                this.windowValues[i] =
+                    0.54 -
+                    0.46 * Math.cos((Math.PI * 2 * i) / (bufferSize - 1));
+            }
+            break;
+        case 'hann':
+        case undefined:
+            for (i = 0; i < bufferSize; i++) {
+                this.windowValues[i] =
+                    0.5 * (1 - Math.cos((Math.PI * 2 * i) / (bufferSize - 1)));
+            }
+            break;
+        case 'lanczoz':
+            for (i = 0; i < bufferSize; i++) {
+                this.windowValues[i] =
+                    Math.sin(Math.PI * ((2 * i) / (bufferSize - 1) - 1)) /
+                    (Math.PI * ((2 * i) / (bufferSize - 1) - 1));
+            }
+            break;
+        case 'rectangular':
+            for (i = 0; i < bufferSize; i++) {
+                this.windowValues[i] = 1;
+            }
+            break;
+        case 'triangular':
+            for (i = 0; i < bufferSize; i++) {
+                this.windowValues[i] =
+                    (2 / bufferSize) *
+                    (bufferSize / 2 - Math.abs(i - (bufferSize - 1) / 2));
+            }
+            break;
+        default:
+            throw Error("No such window function '" + windowFunc + "'");
+    }
+
+    var limit = 1;
+    var bit = bufferSize >> 1;
+    var i;
+
+    while (limit < bufferSize) {
+        for (i = 0; i < limit; i++) {
+            this.reverseTable[i + limit] = this.reverseTable[i] + bit;
+        }
+
+        limit = limit << 1;
+        bit = bit >> 1;
+    }
+
+    for (i = 0; i < bufferSize; i++) {
+        this.sinTable[i] = Math.sin(-Math.PI / i);
+        this.cosTable[i] = Math.cos(-Math.PI / i);
+    }
+
+    this.calculateSpectrum = function(buffer) {
+        // Locally scope variables for speed up
+        var bufferSize = this.bufferSize,
+            cosTable = this.cosTable,
+            sinTable = this.sinTable,
+            reverseTable = this.reverseTable,
+            real = new Float32Array(bufferSize),
+            imag = new Float32Array(bufferSize),
+            bSi = 2 / this.bufferSize,
+            sqrt = Math.sqrt,
+            rval,
+            ival,
+            mag,
+            spectrum = new Float32Array(bufferSize / 2);
+
+        var k = Math.floor(Math.log(bufferSize) / Math.LN2);
+
+        if (Math.pow(2, k) !== bufferSize) {
+            throw 'Invalid buffer size, must be a power of 2.';
+        }
+        if (bufferSize !== buffer.length) {
+            throw 'Supplied buffer is not the same size as defined FFT. FFT Size: ' +
+                bufferSize +
+                ' Buffer Size: ' +
+                buffer.length;
+        }
+
+        var halfSize = 1,
+            phaseShiftStepReal,
+            phaseShiftStepImag,
+            currentPhaseShiftReal,
+            currentPhaseShiftImag,
+            off,
+            tr,
+            ti,
+            tmpReal;
+
+        for (var i = 0; i < bufferSize; i++) {
+            real[i] =
+                buffer[reverseTable[i]] * this.windowValues[reverseTable[i]];
+            imag[i] = 0;
+        }
+
+        while (halfSize < bufferSize) {
+            phaseShiftStepReal = cosTable[halfSize];
+            phaseShiftStepImag = sinTable[halfSize];
+
+            currentPhaseShiftReal = 1;
+            currentPhaseShiftImag = 0;
+
+            for (var fftStep = 0; fftStep < halfSize; fftStep++) {
+                var i = fftStep;
+
+                while (i < bufferSize) {
+                    off = i + halfSize;
+                    tr =
+                        currentPhaseShiftReal * real[off] -
+                        currentPhaseShiftImag * imag[off];
+                    ti =
+                        currentPhaseShiftReal * imag[off] +
+                        currentPhaseShiftImag * real[off];
+
+                    real[off] = real[i] - tr;
+                    imag[off] = imag[i] - ti;
+                    real[i] += tr;
+                    imag[i] += ti;
+
+                    i += halfSize << 1;
+                }
+
+                tmpReal = currentPhaseShiftReal;
+                currentPhaseShiftReal =
+                    tmpReal * phaseShiftStepReal -
+                    currentPhaseShiftImag * phaseShiftStepImag;
+                currentPhaseShiftImag =
+                    tmpReal * phaseShiftStepImag +
+                    currentPhaseShiftImag * phaseShiftStepReal;
+            }
+
+            halfSize = halfSize << 1;
+        }
+
+        for (var i = 0, N = bufferSize / 2; i < N; i++) {
+            rval = real[i];
+            ival = imag[i];
+            mag = bSi * sqrt(rval * rval + ival * ival);
+
+            if (mag > this.peak) {
+                this.peakBand = i;
+                this.peak = mag;
+            }
+            spectrum[i] = mag;
+        }
+        return spectrum;
+    };
+};
+/* eslint-enable complexity, no-redeclare, no-var, one-var */
+
+
+/**
+ * Experimental SpectrumMultiCanvas renderer for wavesurfer.
+ *
+ * A `SpectrumMultiCanvas` consists of one or more `CanvasEntry` instances, depending
+ * on the zoom level.
+ */
+export default class SpectrumMultiCanvas extends Drawer {
+    /**
+     * @param {HTMLElement} container The container node of the wavesurfer instance
+     * @param {WavesurferParams} params The wavesurfer initialisation options
+     */
+    constructor(container, params) {
+        super(container, params);
+
+        /**
+         * @type {number}
+         */
+        this.maxCanvasWidth = params.maxCanvasWidth;
+
+        /**
+         * @type {number}
+         */
+        this.maxCanvasElementWidth = Math.round(
+            params.maxCanvasWidth / params.pixelRatio
+        );
+
+        /**
+         * Whether or not the progress wave is rendered. If the `waveColor`
+         * and `progressColor` are the same color it is not.
+         *
+         * @type {boolean}
+         */
+        this.hasProgressCanvas = params.waveColor != params.progressColor;
+
+        /**
+         * @type {number}
+         */
+        this.halfPixel = 0.5 / params.pixelRatio;
+
+        /**
+         * List of `CanvasEntry` instances.
+         *
+         * @type {Array}
+         */
+        this.canvases = [];
+
+        /**
+         * @type {HTMLElement}
+         */
+        this.progressWave = null;
+
+        /**
+         * Class used to generate entries.
+         *
+         * @type {function}
+         */
+        this.EntryClass = CanvasEntry;
+
+        /**
+         * Canvas 2d context attributes.
+         *
+         * @type {object}
+         */
+        this.canvasContextAttributes = params.drawingContextAttributes;
+
+        /**
+         * Overlap added between entries to prevent vertical white stripes
+         * between `canvas` elements.
+         *
+         * @type {number}
+         */
+        this.overlap = 2 * Math.ceil(params.pixelRatio / 2);
+
+        /**
+         * The radius of the wave bars. Makes bars rounded
+         *
+         * @type {number}
+         */
+        this.barRadius = params.barRadius || 0;
+
+
+            if (params.colorMap) {
+                if (params.colorMap.length < 256) {
+                    throw new Error('Colormap must contain 256 elements');
+                }
+                for (let i = 0; i < params.colorMap.length; i++) {
+                    const cmEntry = params.colorMap[i];
+                    if ((cmEntry.length !== 4) && (cmEntry.length !== 3)) {
+                        throw new Error(
+                            'ColorMap entries must contain 3 (rgb) or 4 (rgba) values'
+                        );
+                    }
+                }
+                this.colorMap = params.colorMap;
+            } else {
+                this.colorMap = [];
+                for (let i = 0; i < 256; i++) {
+                    const val = (255 - i) / 256;
+                    this.colorMap.push([val, val, val, 1]);
+                }
+            }
+	
+    }
+
+    /**
+     * Initialize the drawer
+     */
+    init() {
+        this.createWrapper();
+        this.createElements();
+    }
+
+    /**
+     * Create the canvas elements and style them
+     *
+     */
+    createElements() {
+        this.progressWave = this.wrapper.appendChild(
+            this.style(document.createElement('wave'), {
+                position: 'absolute',
+                zIndex: 3,
+                left: 0,
+                top: 0,
+                bottom: 0,
+                overflow: 'hidden',
+                width: '0',
+                display: 'none',
+                boxSizing: 'border-box',
+                borderRightStyle: 'solid',
+                pointerEvents: 'none'
+            })
+        );
+
+        this.addCanvas();
+        this.updateCursor();
+    }
+
+    /**
+     * Update cursor style
+     */
+    updateCursor() {
+        this.style(this.progressWave, {
+            borderRightWidth: this.params.cursorWidth + 'px',
+            borderRightColor: this.params.cursorColor
+        });
+    }
+
+    /**
+     * Adjust to the updated size by adding or removing canvases
+     */
+    updateSize() {
+        const totalWidth = Math.round(this.width / this.params.pixelRatio);
+        const requiredCanvases = Math.ceil(
+            totalWidth / (this.maxCanvasElementWidth + this.overlap)
+        );
+
+        // add required canvases
+        while (this.canvases.length < requiredCanvases) {
+            this.addCanvas();
+        }
+
+        // remove older existing canvases, if any
+        while (this.canvases.length > requiredCanvases) {
+            this.removeCanvas();
+        }
+
+        let canvasWidth = this.maxCanvasWidth + this.overlap;
+        const lastCanvas = this.canvases.length - 1;
+        this.canvases.forEach((entry, i) => {
+            if (i == lastCanvas) {
+                canvasWidth = this.width - this.maxCanvasWidth * lastCanvas;
+            }
+            this.updateDimensions(entry, canvasWidth, this.height);
+
+            entry.clearWave();
+        });
+    }
+
+    /**
+     * Add a canvas to the canvas list
+     *
+     */
+    addCanvas() {
+        const entry = new this.EntryClass();
+        entry.canvasContextAttributes = this.canvasContextAttributes;
+        entry.hasProgressCanvas = this.hasProgressCanvas;
+        entry.halfPixel = this.halfPixel;
+        const leftOffset = this.maxCanvasElementWidth * this.canvases.length;
+
+        // wave
+        entry.initWave(
+            this.wrapper.appendChild(
+                this.style(document.createElement('canvas'), {
+                    position: 'absolute',
+                    zIndex: 2,
+                    left: leftOffset + 'px',
+                    top: 0,
+                    bottom: 0,
+                    height: '100%',
+                    pointerEvents: 'none'
+                })
+            )
+        );
+
+        // progress
+        if (this.hasProgressCanvas) {
+            entry.initProgress(
+                this.progressWave.appendChild(
+                    this.style(document.createElement('canvas'), {
+                        position: 'absolute',
+                        left: leftOffset + 'px',
+                        top: 0,
+                        bottom: 0,
+                        height: '100%'
+                    })
+                )
+            );
+        }
+
+        this.canvases.push(entry);
+    }
+
+    /**
+     * Pop single canvas from the list
+     *
+     */
+    removeCanvas() {
+        let lastEntry = this.canvases[this.canvases.length - 1];
+
+        // wave
+        lastEntry.wave.parentElement.removeChild(lastEntry.wave);
+
+        // progress
+        if (this.hasProgressCanvas) {
+            lastEntry.progress.parentElement.removeChild(lastEntry.progress);
+        }
+
+        // cleanup
+        if (lastEntry) {
+            lastEntry.destroy();
+            lastEntry = null;
+        }
+
+        this.canvases.pop();
+    }
+
+    /**
+     * Update the dimensions of a canvas element
+     *
+     * @param {CanvasEntry} entry Target entry
+     * @param {number} width The new width of the element
+     * @param {number} height The new height of the element
+     */
+    updateDimensions(entry, width, height) {
+        const elementWidth = Math.round(width / this.params.pixelRatio);
+        const totalWidth = Math.round(this.width / this.params.pixelRatio);
+
+        // update canvas dimensions
+        entry.updateDimensions(elementWidth, totalWidth, width, height);
+
+        // style element
+        this.style(this.progressWave, { display: 'block' });
+    }
+
+    /**
+     * Clear the whole multi-canvas
+     */
+    clearWave() {
+        util.frame(() => {
+            this.canvases.forEach(entry => entry.clearWave());
+        })();
+    }
+
+
+    drawPeaks(peaks, length, start, end) {
+        if (!this.setWidth(length)) {
+            this.clearWave();
+        }
+	
+	this.getFrequencies(this.drawSpectrogram);
+    }
+
+        resample(oldMatrix) {
+        const columnsNumber = this.width;
+        const newMatrix = [];
+
+        const oldPiece = 1 / oldMatrix.length;
+        const newPiece = 1 / columnsNumber;
+        let i;
+
+        for (i = 0; i < columnsNumber; i++) {
+            const column = new Array(oldMatrix[0].length);
+            let j;
+
+            for (j = 0; j < oldMatrix.length; j++) {
+                const oldStart = j * oldPiece;
+                const oldEnd = oldStart + oldPiece;
+                const newStart = i * newPiece;
+                const newEnd = newStart + newPiece;
+
+                const overlap =
+                    oldEnd <= newStart || newEnd <= oldStart
+                        ? 0
+                        : Math.min(
+                            Math.max(oldEnd, newStart),
+                            Math.max(newEnd, oldStart)
+                        ) -
+                        Math.max(
+                            Math.min(oldEnd, newStart),
+                            Math.min(newEnd, oldStart)
+                        );
+                let k;
+                /* eslint-disable max-depth */
+                if (overlap > 0) {
+                    for (k = 0; k < oldMatrix[0].length; k++) {
+                        if (column[k] == null) {
+                            column[k] = 0;
+                        }
+                        column[k] += (overlap / newPiece) * oldMatrix[j][k];
+                    }
+                }
+                /* eslint-enable max-depth */
+            }
+
+            const intColumn = new Uint8Array(oldMatrix[0].length);
+            let m;
+
+            for (m = 0; m < oldMatrix[0].length; m++) {
+                intColumn[m] = column[m];
+            }
+
+            newMatrix.push(intColumn);
+        }
+
+        return newMatrix;
+    }
+
+    drawSpectrogram(frequenciesData, my) {
+	console.log("*** spectrummultican as::drawSpectrum() spec ....spec.length = " + frequenciesData.length)
+	console.log(frequenciesData);
+        //const spectrCc = my.spectrCc;
+        //const length = my.wavesurfer.backend.getDuration();
+        //const height = my.height;
+        //const pixels = my.resample(frequenciesData);
+        //const heightFactor = my.buffer ? 2 / my.buffer.numberOfChannels : 1;
+
+	const canvas = my.canvases[0].wave;
+	const spectrCc = canvas.getContext('2d');
+        const length = my.wavesurfer.backend.getDuration();
+        const height = canvas.height;
+        const pixels = my.resample(frequenciesData);
+        const heightFactor = my.buffer ? 2 / my.buffer.numberOfChannels : 1;
+
+
+	let i;
+        let j;
+
+        for (i = 0; i < pixels.length; i++) {
+            for (j = 0; j < pixels[i].length; j++) {
+                const colorMap = my.colorMap[pixels[i][j]]; // ****
+		if (colorMap.length == 4) {
+		    //my.spectrCc.fillStyle =
+		    spectrCc.fillStyle =
+			'rgba(' +
+			colorMap[0] * 256 +
+			', ' +
+			colorMap[1] * 256 +
+			', ' +
+			colorMap[2] * 256 +
+			',' +
+			colorMap[3] +
+			')';
+		}
+		else {
+		    //my.spectrCc.fillStyle =
+		    spectrCc.fillStyle =
+			'rgb(' +
+			colorMap[0] * 256 +
+			', ' +
+			colorMap[1] * 256 +
+			', ' +
+			colorMap[2] * 256 +
+			')';
+		}
+
+                //my.spectrCc.fillRect(
+                spectrCc.fillRect(
+                    i,
+                    height - j * heightFactor,
+                    1,
+                    heightFactor
+                );
+            }
+        }
+    }
+
+    getFrequencies(callback) {
+        const fftSamples = this.fftSamples || 512;
+        const buffer = (this.buffer = this.wavesurfer.backend.buffer);
+	if (buffer == null) return; // **** GS3 edit
+        const channelOne = buffer.getChannelData(0);
+        const bufferLength = buffer.length;
+        const sampleRate = buffer.sampleRate;
+        const frequencies = [];
+
+        if (!buffer) {
+            this.fireEvent('error', 'Web Audio buffer is not available');
+            return;
+        }
+
+        let noverlap = this.noverlap;
+        if (!noverlap) {
+            const uniqueSamplesPerPx = buffer.length / this.canvases[0].wave.width; // **** GS3 edit
+            noverlap = Math.max(0, Math.round(fftSamples - uniqueSamplesPerPx));
+        }
+
+        const fft = new FFT(
+            fftSamples,
+            sampleRate,
+            this.windowFunc,
+            this.alpha
+        );
+        const maxSlicesCount = Math.floor(
+            bufferLength / (fftSamples - noverlap)
+        );
+        let currentOffset = 0;
+
+        while (currentOffset + fftSamples < channelOne.length) {
+            const segment = channelOne.slice(
+                currentOffset,
+                currentOffset + fftSamples
+            );
+            const spectrum = fft.calculateSpectrum(segment);
+            const array = new Uint8Array(fftSamples / 2);
+            let j;
+            for (j = 0; j < fftSamples / 2; j++) {
+                array[j] = Math.max(-255, Math.log10(spectrum[j]) * 45);
+            }
+            frequencies.push(array);
+            currentOffset += fftSamples - noverlap;
+        }
+        callback(frequencies, this);
+    }
+
+    
+    /**
+     * Draw a waveform with bars
+     *
+     * @param {number[]|Number.<Array[]>} peaks Can also be an array of arrays
+     * for split channel rendering
+     * @param {number} channelIndex The index of the current channel. Normally
+     * should be 0. Must be an integer.
+     * @param {number} start The x-offset of the beginning of the area that
+     * should be rendered
+     * @param {number} end The x-offset of the end of the area that should be
+     * rendered
+     * @returns {void}
+     */
+    drawBars(peaks, channelIndex, start, end) {
+        return this.prepareDraw(
+            peaks,
+            channelIndex,
+            start,
+            end,
+            ({ absmax, hasMinVals, height, offsetY, halfH, peaks }) => {
+                // if drawBars was called within ws.empty we don't pass a start and
+                // don't want anything to happen
+                if (start === undefined) {
+                    return;
+                }
+                // Skip every other value if there are negatives.
+                const peakIndexScale = hasMinVals ? 2 : 1;
+                const length = peaks.length / peakIndexScale;
+                const bar = this.params.barWidth * this.params.pixelRatio;
+                const gap =
+                    this.params.barGap === null
+                        ? Math.max(this.params.pixelRatio, ~~(bar / 2))
+                        : Math.max(
+                            this.params.pixelRatio,
+                            this.params.barGap * this.params.pixelRatio
+                        );
+                const step = bar + gap;
+
+                const scale = length / this.width;
+                const first = start;
+                const last = end;
+                let i = first;
+
+                for (i; i < last; i += step) {
+                    const peak =
+                        peaks[Math.floor(i * scale * peakIndexScale)] || 0;
+                    let h = Math.round((peak / absmax) * halfH);
+
+                    /* in case of silences, allow the user to specify that we
+                     * always draw *something* (normally a 1px high bar) */
+                    if (h == 0 && this.params.barMinHeight)
+                        h = this.params.barMinHeight;
+
+                    this.fillRect(
+                        i + this.halfPixel,
+                        halfH - h + offsetY,
+                        bar + this.halfPixel,
+                        h * 2,
+                        this.barRadius
+                    );
+                }
+            }
+        );
+    }
+
+    /**
+     * Draw a waveform
+     *
+     * @param {number[]|Number.<Array[]>} peaks Can also be an array of arrays
+     * for split channel rendering
+     * @param {number} channelIndex The index of the current channel. Normally
+     * should be 0
+     * @param {number?} start The x-offset of the beginning of the area that
+     * should be rendered (If this isn't set only a flat line is rendered)
+     * @param {number?} end The x-offset of the end of the area that should be
+     * rendered
+     * @returns {void}
+     */
+    drawWave(peaks, channelIndex, start, end) {
+        return this.prepareDraw(
+            peaks,
+            channelIndex,
+            start,
+            end,
+            ({ absmax, hasMinVals, height, offsetY, halfH, peaks, channelIndex }) => {
+                if (!hasMinVals) {
+                    const reflectedPeaks = [];
+                    const len = peaks.length;
+                    let i = 0;
+                    for (i; i < len; i++) {
+                        reflectedPeaks[2 * i] = peaks[i];
+                        reflectedPeaks[2 * i + 1] = -peaks[i];
+                    }
+                    peaks = reflectedPeaks;
+                }
+
+                // if drawWave was called within ws.empty we don't pass a start and
+                // end and simply want a flat line
+                if (start !== undefined) {
+                    this.drawLine(peaks, absmax, halfH, offsetY, start, end, channelIndex);
+                }
+
+                // always draw a median line
+                this.fillRect(
+                    0,
+                    halfH + offsetY - this.halfPixel,
+                    this.width,
+                    this.halfPixel,
+                    this.barRadius
+                );
+            }
+        );
+    }
+
+    /**
+     * Tell the canvas entries to render their portion of the waveform
+     *
+     * @param {number[]} peaks Peaks data
+     * @param {number} absmax Maximum peak value (absolute)
+     * @param {number} halfH Half the height of the waveform
+     * @param {number} offsetY Offset to the top
+     * @param {number} start The x-offset of the beginning of the area that
+     * should be rendered
+     * @param {number} end The x-offset of the end of the area that
+     * should be rendered
+     * @param {channelIndex} channelIndex The channel index of the line drawn
+     */
+    drawLine(peaks, absmax, halfH, offsetY, start, end, channelIndex) {
+        const { waveColor, progressColor } = this.params.splitChannelsOptions.channelColors[channelIndex] || {};
+        this.canvases.forEach((entry, i) => {
+            this.setFillStyles(entry, waveColor, progressColor);
+            entry.drawLines(peaks, absmax, halfH, offsetY, start, end);
+        });
+    }
+
+    /**
+     * Draw a rectangle on the multi-canvas
+     *
+     * @param {number} x X-position of the rectangle
+     * @param {number} y Y-position of the rectangle
+     * @param {number} width Width of the rectangle
+     * @param {number} height Height of the rectangle
+     * @param {number} radius Radius of the rectangle
+     */
+    fillRect(x, y, width, height, radius) {
+        const startCanvas = Math.floor(x / this.maxCanvasWidth);
+        const endCanvas = Math.min(
+            Math.ceil((x + width) / this.maxCanvasWidth) + 1,
+            this.canvases.length
+        );
+        let i = startCanvas;
+        for (i; i < endCanvas; i++) {
+            const entry = this.canvases[i];
+            const leftOffset = i * this.maxCanvasWidth;
+
+            const intersection = {
+                x1: Math.max(x, i * this.maxCanvasWidth),
+                y1: y,
+                x2: Math.min(
+                    x + width,
+                    i * this.maxCanvasWidth + entry.wave.width
+                ),
+                y2: y + height
+            };
+
+            if (intersection.x1 < intersection.x2) {
+                this.setFillStyles(entry);
+
+                entry.fillRects(
+                    intersection.x1 - leftOffset,
+                    intersection.y1,
+                    intersection.x2 - intersection.x1,
+                    intersection.y2 - intersection.y1,
+                    radius
+                );
+            }
+        }
+    }
+
+    /**
+     * Returns whether to hide the channel from being drawn based on params.
+     *
+     * @param {number} channelIndex The index of the current channel.
+     * @returns {bool} True to hide the channel, false to draw.
+     */
+    hideChannel(channelIndex) {
+        return this.params.splitChannels && this.params.splitChannelsOptions.filterChannels.includes(channelIndex);
+    }
+
+    /**
+     * Performs preparation tasks and calculations which are shared by `drawBars`
+     * and `drawWave`
+     *
+     * @param {number[]|Number.<Array[]>} peaks Can also be an array of arrays for
+     * split channel rendering
+     * @param {number} channelIndex The index of the current channel. Normally
+     * should be 0
+     * @param {number?} start The x-offset of the beginning of the area that
+     * should be rendered. If this isn't set only a flat line is rendered
+     * @param {number?} end The x-offset of the end of the area that should be
+     * rendered
+     * @param {function} fn The render function to call, e.g. `drawWave`
+     * @param {number} drawIndex The index of the current channel after filtering.
+     * @returns {void}
+     */
+    prepareDraw(peaks, channelIndex, start, end, fn, drawIndex) {
+        return util.frame(() => {
+            // Split channels and call this function with the channelIndex set
+            if (peaks[0] instanceof Array) {
+                const channels = peaks;
+
+                if (this.params.splitChannels) {
+                    const filteredChannels =  channels.filter((c, i) => !this.hideChannel(i));
+                    if (!this.params.splitChannelsOptions.overlay) {
+                        this.setHeight(
+                            Math.max(filteredChannels.length, 1) *
+                                this.params.height *
+                                this.params.pixelRatio
+                        );
+                    }
+
+                    return channels.forEach((channelPeaks, i) =>
+                        this.prepareDraw(channelPeaks, i, start, end, fn, filteredChannels.indexOf(channelPeaks))
+                    );
+                }
+                peaks = channels[0];
+            }
+
+            // Return and do not draw channel peaks if hidden.
+            if (this.hideChannel(channelIndex)) {
+                return;
+            }
+
+            // calculate maximum modulation value, either from the barHeight
+            // parameter or if normalize=true from the largest value in the peak
+            // set
+            let absmax = 1 / this.params.barHeight;
+            if (this.params.normalize) {
+                const max = util.max(peaks);
+                const min = util.min(peaks);
+                absmax = -min > max ? -min : max;
+            }
+
+            // Bar wave draws the bottom only as a reflection of the top,
+            // so we don't need negative values
+            const hasMinVals = [].some.call(peaks, val => val < 0);
+            const height = this.params.height * this.params.pixelRatio;
+            const offsetY = height * drawIndex || 0;
+            const halfH = height / 2;
+
+            return fn({
+                absmax: absmax,
+                hasMinVals: hasMinVals,
+                height: height,
+                offsetY: offsetY,
+                halfH: halfH,
+                peaks: peaks,
+                channelIndex: channelIndex,
+            });
+        })();
+    }
+
+    /**
+     * Set the fill styles for a certain entry (wave and progress)
+     *
+     * @param {CanvasEntry} entry Target entry
+     * @param {string} waveColor Wave color to draw this entry
+     * @param {string} progressColor Progress color to draw this entry
+     */
+    setFillStyles(entry, waveColor = this.params.waveColor, progressColor = this.params.progressColor) {
+        entry.setFillStyles(waveColor, progressColor);
+    }
+
+    /**
+     * Return image data of the multi-canvas
+     *
+     * When using a `type` of `'blob'`, this will return a `Promise`.
+     *
+     * @param {string} format='image/png' An optional value of a format type.
+     * @param {number} quality=0.92 An optional value between 0 and 1.
+     * @param {string} type='dataURL' Either 'dataURL' or 'blob'.
+     * @return {string|string[]|Promise} When using the default `'dataURL'`
+     * `type` this returns a single data URL or an array of data URLs,
+     * one for each canvas. When using the `'blob'` `type` this returns a
+     * `Promise` that resolves with an array of `Blob` instances, one for each
+     * canvas.
+     */
+    getImage(format, quality, type) {
+        if (type === 'blob') {
+            return Promise.all(
+                this.canvases.map(entry => {
+                    return entry.getImage(format, quality, type);
+                })
+            );
+        } else if (type === 'dataURL') {
+            let images = this.canvases.map(entry =>
+                entry.getImage(format, quality, type)
+            );
+            return images.length > 1 ? images : images[0];
+        }
+    }
+
+    /**
+     * Render the new progress
+     *
+     * @param {number} position X-offset of progress position in pixels
+     */
+    updateProgress(position) {
+        this.style(this.progressWave, { width: position + 'px' });
+    }
+}
Index: gs3-extensions/mars-src/trunk/src/wavesurfer-renderer/wavesurfer.js
===================================================================
--- gs3-extensions/mars-src/trunk/src/wavesurfer-renderer/wavesurfer.js	(revision 34668)
+++ gs3-extensions/mars-src/trunk/src/wavesurfer-renderer/wavesurfer.js	(revision 34668)
@@ -0,0 +1,1697 @@
+import * as util from './util';
+import MultiCanvas from './drawer.multicanvas';
+import SpectrumMultiCanvas from './drawer.spectrummulticanvas';
+import WebAudio from './webaudio';
+import MediaElement from './mediaelement';
+import PeakCache from './peakcache';
+import MediaElementWebAudio from './mediaelement-webaudio';
+
+/*
+ * This work is licensed under a BSD-3-Clause License.
+ */
+
+/** @external {HTMLElement} https://developer.mozilla.org/en/docs/Web/API/HTMLElement */
+/** @external {OfflineAudioContext} https://developer.mozilla.org/en-US/docs/Web/API/OfflineAudioContext */
+/** @external {File} https://developer.mozilla.org/en-US/docs/Web/API/File */
+/** @external {Blob} https://developer.mozilla.org/en-US/docs/Web/API/Blob */
+/** @external {CanvasRenderingContext2D} https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D */
+/** @external {MediaStreamConstraints} https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamConstraints */
+/** @external {AudioNode} https://developer.mozilla.org/de/docs/Web/API/AudioNode */
+
+/**
+ * @typedef {Object} WavesurferParams
+ * @property {AudioContext} audioContext=null Use your own previously
+ * initialized AudioContext or leave blank.
+ * @property {number} audioRate=1 Speed at which to play audio. Lower number is
+ * slower.
+ * @property {ScriptProcessorNode} audioScriptProcessor=null Use your own previously
+ * initialized ScriptProcessorNode or leave blank.
+ * @property {boolean} autoCenter=true If a scrollbar is present, center the
+ * waveform on current progress
+ * @property {number} autoCenterRate=5 If autoCenter is active, rate at which the
+ * waveform is centered
+ * @property {boolean} autoCenterImmediately=false If autoCenter is active, immediately
+ * center waveform on current progress
+ * @property {string} backend='WebAudio' `'WebAudio'|'MediaElement'|'MediaElementWebAudio'` In most cases
+ * you don't have to set this manually. MediaElement is a fallback for unsupported browsers.
+ * MediaElementWebAudio allows to use WebAudio API also with big audio files, loading audio like with
+ * MediaElement backend (HTML5 audio tag). You have to use the same methods of MediaElement backend for loading and
+ * playback, giving also peaks, so the audio data are not decoded. In this way you can use WebAudio features, like filters,
+ * also with audio with big duration. For example:
+ * ` wavesurfer.load(url | HTMLMediaElement, peaks, preload, duration);
+ *   wavesurfer.play();
+ *   wavesurfer.setFilter(customFilter);
+ * `
+ * @property {string} backgroundColor=null Change background color of the
+ * waveform container.
+ * @property {number} barHeight=1 The height of the wave bars.
+ * @property {number} barRadius=0 The radius of the wave bars. Makes bars rounded
+ * @property {number} barGap=null The optional spacing between bars of the wave,
+ * if not provided will be calculated in legacy format.
+ * @property {number} barWidth=null Draw the waveform using bars.
+ * @property {number} barMinHeight=null If specified, draw at least a bar of this height,
+ * eliminating waveform gaps
+ * @property {boolean} closeAudioContext=false Close and nullify all audio
+ * contexts when the destroy method is called.
+ * @property {!string|HTMLElement} container CSS selector or HTML element where
+ * the waveform should be drawn. This is the only required parameter.
+ * @property {string} cursorColor='#333' The fill color of the cursor indicating
+ * the playhead position.
+ * @property {number} cursorWidth=1 Measured in pixels.
+ * @property {object} drawingContextAttributes={desynchronized: false} Drawing context
+ * attributes.
+ * @property {number} duration=null Optional audio length so pre-rendered peaks
+ * can be display immediately for example.
+ * @property {boolean} fillParent=true Whether to fill the entire container or
+ * draw only according to `minPxPerSec`.
+ * @property {boolean} forceDecode=false Force decoding of audio using web audio
+ * when zooming to get a more detailed waveform.
+ * @property {number} height=128 The height of the waveform. Measured in
+ * pixels.
+ * @property {boolean} hideScrollbar=false Whether to hide the horizontal
+ * scrollbar when one would normally be shown.
+ * @property {boolean} interact=true Whether the mouse interaction will be
+ * enabled at initialization. You can switch this parameter at any time later
+ * on.
+ * @property {boolean} loopSelection=true (Use with regions plugin) Enable
+ * looping of selected regions
+ * @property {number} maxCanvasWidth=4000 Maximum width of a single canvas in
+ * pixels, excluding a small overlap (2 * `pixelRatio`, rounded up to the next
+ * even integer). If the waveform is longer than this value, additional canvases
+ * will be used to render the waveform, which is useful for very large waveforms
+ * that may be too wide for browsers to draw on a single canvas.
+ * @property {boolean} mediaControls=false (Use with backend `MediaElement` or `MediaElementWebAudio`)
+ * this enables the native controls for the media element
+ * @property {string} mediaType='audio' (Use with backend `MediaElement` or `MediaElementWebAudio`)
+ * `'audio'|'video'` ('video' only for `MediaElement`)
+ * @property {number} minPxPerSec=20 Minimum number of pixels per second of
+ * audio.
+ * @property {boolean} normalize=false If true, normalize by the maximum peak
+ * instead of 1.0.
+ * @property {boolean} partialRender=false Use the PeakCache to improve
+ * rendering speed of large waveforms
+ * @property {number} pixelRatio=window.devicePixelRatio The pixel ratio used to
+ * calculate display
+ * @property {PluginDefinition[]} plugins=[] An array of plugin definitions to
+ * register during instantiation, they will be directly initialised unless they
+ * are added with the `deferInit` property set to true.
+ * @property {string} progressColor='#555' The fill color of the part of the
+ * waveform behind the cursor. When `progressColor` and `waveColor` are the same
+ * the progress wave is not rendered at all.
+ * @property {boolean} removeMediaElementOnDestroy=true Set to false to keep the
+ * media element in the DOM when the player is destroyed. This is useful when
+ * reusing an existing media element via the `loadMediaElement` method.
+ * @property {Object} renderer=MultiCanvas Can be used to inject a custom
+ * renderer.
+ * @property {boolean|number} responsive=false If set to `true` resize the
+ * waveform, when the window is resized. This is debounced with a `100ms`
+ * timeout by default. If this parameter is a number it represents that timeout.
+ * @property {boolean} rtl=false If set to `true`, renders waveform from
+ * right-to-left.
+ * @property {boolean} scrollParent=false Whether to scroll the container with a
+ * lengthy waveform. Otherwise the waveform is shrunk to the container width
+ * (see fillParent).
+ * @property {number} skipLength=2 Number of seconds to skip with the
+ * skipForward() and skipBackward() methods.
+ * @property {boolean} splitChannels=false Render with separate waveforms for
+ * the channels of the audio
+ * @property {string} waveColor='#999' The fill color of the waveform after the
+ * cursor.
+ * @property {object} xhr={} XHR options. For example:
+ * `let xhr = {
+ *     cache: 'default',
+ *     mode: 'cors',
+ *     method: 'GET',
+ *     credentials: 'same-origin',
+ *     redirect: 'follow',
+ *     referrer: 'client',
+ *     headers: [
+ *         {
+ *             key: 'Authorization',
+ *             value: 'my-token'
+ *         }
+ *     ]
+ * };`
+ */
+
+/**
+ * @typedef {Object} PluginDefinition
+ * @desc The Object used to describe a plugin
+ * @example wavesurfer.addPlugin(pluginDefinition);
+ * @property {string} name The name of the plugin, the plugin instance will be
+ * added as a property to the wavesurfer instance under this name
+ * @property {?Object} staticProps The properties that should be added to the
+ * wavesurfer instance as static properties
+ * @property {?boolean} deferInit Don't initialise plugin
+ * automatically
+ * @property {Object} params={} The plugin parameters, they are the first parameter
+ * passed to the plugin class constructor function
+ * @property {PluginClass} instance The plugin instance factory, is called with
+ * the dependency specified in extends. Returns the plugin class.
+ */
+
+/**
+ * @interface PluginClass
+ *
+ * @desc This is the interface which is implemented by all plugin classes. Note
+ * that this only turns into an observer after being passed through
+ * `wavesurfer.addPlugin`.
+ *
+ * @extends {Observer}
+ */
+class PluginClass {
+    /**
+     * Plugin definition factory
+     *
+     * This function must be used to create a plugin definition which can be
+     * used by wavesurfer to correctly instantiate the plugin.
+     *
+     * It returns a `PluginDefinition` object representing the plugin.
+     *
+     * @param {Object} params={} The plugin params (specific to the plugin)
+     */
+    create(params) {}
+    /**
+     * Construct the plugin
+     *
+     * @param {Object} params={} The plugin params (specific to the plugin)
+     * @param {Object} ws The wavesurfer instance
+     */
+    constructor(params, ws) {}
+    /**
+     * Initialise the plugin
+     *
+     * Start doing something. This is called by
+     * `wavesurfer.initPlugin(pluginName)`
+     */
+    init() {}
+    /**
+     * Destroy the plugin instance
+     *
+     * Stop doing something. This is called by
+     * `wavesurfer.destroyPlugin(pluginName)`
+     */
+    destroy() {}
+}
+
+/**
+ * WaveSurfer core library class
+ *
+ * @extends {Observer}
+ * @example
+ * const params = {
+ *   container: '#waveform',
+ *   waveColor: 'violet',
+ *   progressColor: 'purple'
+ * };
+ *
+ * // initialise like this
+ * const wavesurfer = WaveSurfer.create(params);
+ *
+ * // or like this ...
+ * const wavesurfer = new WaveSurfer(params);
+ * wavesurfer.init();
+ *
+ * // load audio file
+ * wavesurfer.load('example/media/demo.wav');
+ */
+export default class WaveSurfer extends util.Observer {
+    /** @private */
+    defaultParams = {
+        audioContext: null,
+        audioScriptProcessor: null,
+        audioRate: 1,
+        autoCenter: true,
+        autoCenterRate: 5,
+        autoCenterImmediately: false,
+        backend: 'WebAudio',
+        backgroundColor: null,
+        barHeight: 1,
+        barRadius: 0,
+        barGap: null,
+        barMinHeight: null,
+        container: null,
+        cursorColor: '#333',
+        cursorWidth: 1,
+        dragSelection: true,
+        drawingContextAttributes: {
+            // Boolean that hints the user agent to reduce the latency
+            // by desynchronizing the canvas paint cycle from the event
+            // loop
+            desynchronized: false
+        },
+        duration: null,
+        fillParent: true,
+        forceDecode: false,
+        height: 128,
+        hideScrollbar: false,
+        interact: true,
+        loopSelection: true,
+        maxCanvasWidth: 4000,
+        mediaContainer: null,
+        mediaControls: false,
+        mediaType: 'audio',
+        minPxPerSec: 20,
+        normalize: false,
+        partialRender: false,
+        pixelRatio:
+            window.devicePixelRatio || screen.deviceXDPI / screen.logicalXDPI,
+        plugins: [],
+        progressColor: '#555',
+        removeMediaElementOnDestroy: true,
+        renderer: SpectrumMultiCanvas,
+        responsive: false,
+        rtl: false,
+        scrollParent: false,
+        skipLength: 2,
+        splitChannels: false,
+        splitChannelsOptions: {
+            overlay: false,
+            channelColors: {},
+            filterChannels: [],
+        },
+        waveColor: '#999',
+        xhr: {}
+    };
+
+    /** @private */
+    backends = {
+        MediaElement,
+        WebAudio,
+        MediaElementWebAudio
+    };
+
+    /**
+     * Instantiate this class, call its `init` function and returns it
+     *
+     * @param {WavesurferParams} params The wavesurfer parameters
+     * @return {Object} WaveSurfer instance
+     * @example const wavesurfer = WaveSurfer.create(params);
+     */
+    static create(params) {
+        const wavesurfer = new WaveSurfer(params);
+        return wavesurfer.init();
+    }
+
+    /**
+     * The library version number is available as a static property of the
+     * WaveSurfer class
+     *
+     * @type {String}
+     * @example
+     * console.log('Using wavesurfer.js ' + WaveSurfer.VERSION);
+     */
+    static VERSION = __VERSION__;
+
+    /**
+     * Functions in the `util` property are available as a prototype property to
+     * all instances
+     *
+     * @type {Object}
+     * @example
+     * const wavesurfer = WaveSurfer.create(params);
+     * wavesurfer.util.style(myElement, { background: 'blue' });
+     */
+    util = util;
+
+    /**
+     * Functions in the `util` property are available as a static property of the
+     * WaveSurfer class
+     *
+     * @type {Object}
+     * @example
+     * WaveSurfer.util.style(myElement, { background: 'blue' });
+     */
+    static util = util;
+
+    /**
+     * Initialise wavesurfer instance
+     *
+     * @param {WavesurferParams} params Instantiation options for wavesurfer
+     * @example
+     * const wavesurfer = new WaveSurfer(params);
+     * @returns {this} Wavesurfer instance
+     */
+    constructor(params) {
+        super();
+        /**
+         * Extract relevant parameters (or defaults)
+         * @private
+         */
+        this.params = Object.assign({}, this.defaultParams, params);
+
+        /** @private */
+        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) {
+            /** @private */
+            this.mediaContainer = this.container;
+        } else if (typeof this.params.mediaContainer == 'string') {
+            /** @private */
+            this.mediaContainer = document.querySelector(
+                this.params.mediaContainer
+            );
+        } else {
+            /** @private */
+            this.mediaContainer = this.params.mediaContainer;
+        }
+
+        if (!this.mediaContainer) {
+            throw new Error('Media Container element not found');
+        }
+
+        if (this.params.maxCanvasWidth <= 1) {
+            throw new Error('maxCanvasWidth must be greater than 1');
+        } else if (this.params.maxCanvasWidth % 2 == 1) {
+            throw new Error('maxCanvasWidth must be an even number');
+        }
+
+        if (this.params.rtl === true) {
+            util.style(this.container, { transform: 'rotateY(180deg)' });
+        }
+
+        if (this.params.backgroundColor) {
+            this.setBackgroundColor(this.params.backgroundColor);
+        }
+
+        /**
+         * @private Used to save the current volume when muting so we can
+         * restore once unmuted
+         * @type {number}
+         */
+        this.savedVolume = 0;
+
+        /**
+         * @private The current muted state
+         * @type {boolean}
+         */
+        this.isMuted = false;
+
+        /**
+         * @private Will hold a list of event descriptors that need to be
+         * canceled on subsequent loads of audio
+         * @type {Object[]}
+         */
+        this.tmpEvents = [];
+
+        /**
+         * @private Holds any running audio downloads
+         * @type {Observer}
+         */
+        this.currentRequest = null;
+        /** @private */
+        this.arraybuffer = null;
+        /** @private */
+        this.drawer = null;
+        /** @private */
+        this.backend = null;
+        /** @private */
+        this.peakCache = null;
+
+        // cache constructor objects
+        if (typeof this.params.renderer !== 'function') {
+            throw new Error('Renderer parameter is invalid');
+        }
+        /**
+         * @private The uninitialised Drawer class
+         */
+        this.Drawer = this.params.renderer;
+	
+        /**
+         * @private The uninitialised Backend class
+         */
+        // Back compat
+        if (this.params.backend == 'AudioElement') {
+            this.params.backend = 'MediaElement';
+        }
+
+        if (
+            (this.params.backend == 'WebAudio' ||
+                this.params.backend === 'MediaElementWebAudio') &&
+            !WebAudio.prototype.supportsWebAudio.call(null)
+        ) {
+            this.params.backend = 'MediaElement';
+        }
+        this.Backend = this.backends[this.params.backend];
+
+        /**
+         * @private map of plugin names that are currently initialised
+         */
+        this.initialisedPluginList = {};
+        /** @private */
+        this.isDestroyed = false;
+
+        /**
+         * Get the current ready status.
+         *
+         * @example const isReady = wavesurfer.isReady;
+         * @return {boolean}
+         */
+        this.isReady = false;
+
+        // responsive debounced event listener. If this.params.responsive is not
+        // set, this is never called. Use 100ms or this.params.responsive as
+        // timeout for the debounce function.
+        let prevWidth = 0;
+        this._onResize = util.debounce(
+            () => {
+                if (
+                    prevWidth != this.drawer.wrapper.clientWidth &&
+                    !this.params.scrollParent
+                ) {
+                    prevWidth = this.drawer.wrapper.clientWidth;
+                    this.drawer.fireEvent('redraw');
+                }
+            },
+            typeof this.params.responsive === 'number'
+                ? this.params.responsive
+                : 100
+        );
+
+        return this;
+    }
+
+    /**
+     * Initialise the wave
+     *
+     * @example
+     * var wavesurfer = new WaveSurfer(params);
+     * wavesurfer.init();
+     * @return {this} The wavesurfer instance
+     */
+    init() {
+        this.registerPlugins(this.params.plugins);
+        this.createDrawer();
+        this.createBackend();
+        this.createPeakCache();
+        return this;
+    }
+
+    /**
+     * Add and initialise array of plugins (if `plugin.deferInit` is falsey),
+     * this function is called in the init function of wavesurfer
+     *
+     * @param {PluginDefinition[]} plugins An array of plugin definitions
+     * @emits {WaveSurfer#plugins-registered} Called with the array of plugin definitions
+     * @return {this} The wavesurfer instance
+     */
+    registerPlugins(plugins) {
+        // first instantiate all the plugins
+        plugins.forEach(plugin => this.addPlugin(plugin));
+
+        // now run the init functions
+        plugins.forEach(plugin => {
+            // call init function of the plugin if deferInit is falsey
+            // in that case you would manually use initPlugins()
+            if (!plugin.deferInit) {
+                this.initPlugin(plugin.name);
+            }
+        });
+        this.fireEvent('plugins-registered', plugins);
+        return this;
+    }
+
+    /**
+     * Get a map of plugin names that are currently initialised
+     *
+     * @example wavesurfer.getPlugins();
+     * @return {Object} Object with plugin names
+     */
+    getActivePlugins() {
+        return this.initialisedPluginList;
+    }
+
+    /**
+     * Add a plugin object to wavesurfer
+     *
+     * @param {PluginDefinition} plugin A plugin definition
+     * @emits {WaveSurfer#plugin-added} Called with the name of the plugin that was added
+     * @example wavesurfer.addPlugin(WaveSurfer.minimap());
+     * @return {this} The wavesurfer instance
+     */
+    addPlugin(plugin) {
+        if (!plugin.name) {
+            throw new Error('Plugin does not have a name!');
+        }
+        if (!plugin.instance) {
+            throw new Error(
+                `Plugin ${plugin.name} does not have an instance property!`
+            );
+        }
+
+        // staticProps properties are applied to wavesurfer instance
+        if (plugin.staticProps) {
+            Object.keys(plugin.staticProps).forEach(pluginStaticProp => {
+                /**
+                 * Properties defined in a plugin definition's `staticProps` property are added as
+                 * staticProps properties of the WaveSurfer instance
+                 */
+                this[pluginStaticProp] = plugin.staticProps[pluginStaticProp];
+            });
+        }
+
+        const Instance = plugin.instance;
+
+        // turn the plugin instance into an observer
+        const observerPrototypeKeys = Object.getOwnPropertyNames(
+            util.Observer.prototype
+        );
+        observerPrototypeKeys.forEach(key => {
+            Instance.prototype[key] = util.Observer.prototype[key];
+        });
+
+        /**
+         * Instantiated plugin classes are added as a property of the wavesurfer
+         * instance
+         * @type {Object}
+         */
+        this[plugin.name] = new Instance(plugin.params || {}, this);
+        this.fireEvent('plugin-added', plugin.name);
+        return this;
+    }
+
+    /**
+     * Initialise a plugin
+     *
+     * @param {string} name A plugin name
+     * @emits WaveSurfer#plugin-initialised
+     * @example wavesurfer.initPlugin('minimap');
+     * @return {this} The wavesurfer instance
+     */
+    initPlugin(name) {
+        if (!this[name]) {
+            throw new Error(`Plugin ${name} has not been added yet!`);
+        }
+        if (this.initialisedPluginList[name]) {
+            // destroy any already initialised plugins
+            this.destroyPlugin(name);
+        }
+        this[name].init();
+        this.initialisedPluginList[name] = true;
+        this.fireEvent('plugin-initialised', name);
+        return this;
+    }
+
+    /**
+     * Destroy a plugin
+     *
+     * @param {string} name A plugin name
+     * @emits WaveSurfer#plugin-destroyed
+     * @example wavesurfer.destroyPlugin('minimap');
+     * @returns {this} The wavesurfer instance
+     */
+    destroyPlugin(name) {
+        if (!this[name]) {
+            throw new Error(
+                `Plugin ${name} has not been added yet and cannot be destroyed!`
+            );
+        }
+        if (!this.initialisedPluginList[name]) {
+            throw new Error(
+                `Plugin ${name} is not active and cannot be destroyed!`
+            );
+        }
+        if (typeof this[name].destroy !== 'function') {
+            throw new Error(`Plugin ${name} does not have a destroy function!`);
+        }
+
+        this[name].destroy();
+        delete this.initialisedPluginList[name];
+        this.fireEvent('plugin-destroyed', name);
+        return this;
+    }
+
+    /**
+     * Destroy all initialised plugins. Convenience function to use when
+     * wavesurfer is removed
+     *
+     * @private
+     */
+    destroyAllPlugins() {
+        Object.keys(this.initialisedPluginList).forEach(name =>
+            this.destroyPlugin(name)
+        );
+    }
+
+    /**
+     * Create the drawer and draw the waveform
+     *
+     * @private
+     * @emits WaveSurfer#drawer-created
+     */
+    createDrawer() {
+        this.drawer = new this.Drawer(this.container, this.params);
+	this.drawer.wavesurfer = this; // **** // GS3 edit
+
+        this.drawer.init();
+        this.fireEvent('drawer-created', this.drawer);
+
+        if (this.params.responsive !== false) {
+            window.addEventListener('resize', this._onResize, true);
+            window.addEventListener('orientationchange', this._onResize, true);
+        }
+
+        this.drawer.on('redraw', () => {
+            this.drawBuffer();
+            this.drawer.progress(this.backend.getPlayedPercents());
+        });
+
+        // Click-to-seek
+        this.drawer.on('click', (e, progress) => {
+            setTimeout(() => this.seekTo(progress), 0);
+        });
+
+        // Relay the scroll event from the drawer
+        this.drawer.on('scroll', e => {
+            if (this.params.partialRender) {
+                this.drawBuffer();
+            }
+            this.fireEvent('scroll', e);
+        });
+    }
+
+    /**
+     * Create the backend
+     *
+     * @private
+     * @emits WaveSurfer#backend-created
+     */
+    createBackend() {
+        if (this.backend) {
+            this.backend.destroy();
+        }
+
+        this.backend = new this.Backend(this.params);
+        this.backend.init();
+        this.fireEvent('backend-created', this.backend);
+
+        this.backend.on('finish', () => {
+            this.drawer.progress(this.backend.getPlayedPercents());
+            this.fireEvent('finish');
+        });
+        this.backend.on('play', () => this.fireEvent('play'));
+        this.backend.on('pause', () => this.fireEvent('pause'));
+
+        this.backend.on('audioprocess', time => {
+            this.drawer.progress(this.backend.getPlayedPercents());
+            this.fireEvent('audioprocess', time);
+        });
+
+        // only needed for MediaElement and MediaElementWebAudio backend
+        if (
+            this.params.backend === 'MediaElement' ||
+            this.params.backend === 'MediaElementWebAudio'
+        ) {
+            this.backend.on('seek', () => {
+                this.drawer.progress(this.backend.getPlayedPercents());
+            });
+
+            this.backend.on('volume', () => {
+                let newVolume = this.getVolume();
+                this.fireEvent('volume', newVolume);
+
+                if (this.backend.isMuted !== this.isMuted) {
+                    this.isMuted = this.backend.isMuted;
+                    this.fireEvent('mute', this.isMuted);
+                }
+            });
+        }
+    }
+
+    /**
+     * Create the peak cache
+     *
+     * @private
+     */
+    createPeakCache() {
+        if (this.params.partialRender) {
+            this.peakCache = new PeakCache();
+        }
+    }
+
+    /**
+     * Get the duration of the audio clip
+     *
+     * @example const duration = wavesurfer.getDuration();
+     * @return {number} Duration in seconds
+     */
+    getDuration() {
+        return this.backend.getDuration();
+    }
+
+    /**
+     * Get the current playback position
+     *
+     * @example const currentTime = wavesurfer.getCurrentTime();
+     * @return {number} Playback position in seconds
+     */
+    getCurrentTime() {
+        return this.backend.getCurrentTime();
+    }
+
+    /**
+     * Set the current play time in seconds.
+     *
+     * @param {number} seconds A positive number in seconds. E.g. 10 means 10
+     * seconds, 60 means 1 minute
+     */
+    setCurrentTime(seconds) {
+        if (seconds >= this.getDuration()) {
+            this.seekTo(1);
+        } else {
+            this.seekTo(seconds / this.getDuration());
+        }
+    }
+
+    /**
+     * Starts playback from the current position. Optional start and end
+     * measured in seconds can be used to set the range of audio to play.
+     *
+     * @param {?number} start Position to start at
+     * @param {?number} end Position to end at
+     * @emits WaveSurfer#interaction
+     * @return {Promise} Result of the backend play method
+     * @example
+     * // play from second 1 to 5
+     * wavesurfer.play(1, 5);
+     */
+    play(start, end) {
+        this.fireEvent('interaction', () => this.play(start, end));
+        return this.backend.play(start, end);
+    }
+
+    /**
+     * Set a point in seconds for playback to stop at.
+     *
+     * @param {number} position Position (in seconds) to stop at
+     * @version 3.3.0
+     */
+    setPlayEnd(position) {
+        this.backend.setPlayEnd(position);
+    }
+
+    /**
+     * Stops and pauses playback
+     *
+     * @example wavesurfer.pause();
+     * @return {Promise} Result of the backend pause method
+     */
+    pause() {
+        if (!this.backend.isPaused()) {
+            return this.backend.pause();
+        }
+    }
+
+    /**
+     * Toggle playback
+     *
+     * @example wavesurfer.playPause();
+     * @return {Promise} Result of the backend play or pause method
+     */
+    playPause() {
+        return this.backend.isPaused() ? this.play() : this.pause();
+    }
+
+    /**
+     * Get the current playback state
+     *
+     * @example const isPlaying = wavesurfer.isPlaying();
+     * @return {boolean} False if paused, true if playing
+     */
+    isPlaying() {
+        return !this.backend.isPaused();
+    }
+
+    /**
+     * Skip backward
+     *
+     * @param {?number} seconds Amount to skip back, if not specified `skipLength`
+     * is used
+     * @example wavesurfer.skipBackward();
+     */
+    skipBackward(seconds) {
+        this.skip(-seconds || -this.params.skipLength);
+    }
+
+    /**
+     * Skip forward
+     *
+     * @param {?number} seconds Amount to skip back, if not specified `skipLength`
+     * is used
+     * @example wavesurfer.skipForward();
+     */
+    skipForward(seconds) {
+        this.skip(seconds || this.params.skipLength);
+    }
+
+    /**
+     * Skip a number of seconds from the current position (use a negative value
+     * to go backwards).
+     *
+     * @param {number} offset Amount to skip back or forwards
+     * @example
+     * // go back 2 seconds
+     * wavesurfer.skip(-2);
+     */
+    skip(offset) {
+        const duration = this.getDuration() || 1;
+        let position = this.getCurrentTime() || 0;
+        position = Math.max(0, Math.min(duration, position + (offset || 0)));
+        this.seekAndCenter(position / duration);
+    }
+
+    /**
+     * Seeks to a position and centers the view
+     *
+     * @param {number} progress Between 0 (=beginning) and 1 (=end)
+     * @example
+     * // seek and go to the middle of the audio
+     * wavesurfer.seekTo(0.5);
+     */
+    seekAndCenter(progress) {
+        this.seekTo(progress);
+        this.drawer.recenter(progress);
+    }
+
+    /**
+     * Seeks to a position
+     *
+     * @param {number} progress Between 0 (=beginning) and 1 (=end)
+     * @emits WaveSurfer#interaction
+     * @emits WaveSurfer#seek
+     * @example
+     * // seek to the middle of the audio
+     * wavesurfer.seekTo(0.5);
+     */
+    seekTo(progress) {
+        // return an error if progress is not a number between 0 and 1
+        if (
+            typeof progress !== 'number' ||
+            !isFinite(progress) ||
+            progress < 0 ||
+            progress > 1
+        ) {
+            throw new Error(
+                'Error calling wavesurfer.seekTo, parameter must be a number between 0 and 1!'
+            );
+        }
+        this.fireEvent('interaction', () => this.seekTo(progress));
+
+        const paused = this.backend.isPaused();
+        // avoid draw wrong position while playing backward seeking
+        if (!paused) {
+            this.backend.pause();
+        }
+        // avoid small scrolls while paused seeking
+        const oldScrollParent = this.params.scrollParent;
+        this.params.scrollParent = false;
+        this.backend.seekTo(progress * this.getDuration());
+        this.drawer.progress(progress);
+
+        if (!paused) {
+            this.backend.play();
+        }
+        this.params.scrollParent = oldScrollParent;
+        this.fireEvent('seek', progress);
+    }
+
+    /**
+     * Stops and goes to the beginning.
+     *
+     * @example wavesurfer.stop();
+     */
+    stop() {
+        this.pause();
+        this.seekTo(0);
+        this.drawer.progress(0);
+    }
+
+    /**
+     * Sets the ID of the audio device to use for output and returns a Promise.
+     *
+     * @param {string} deviceId String value representing underlying output
+     * device
+     * @returns {Promise} `Promise` that resolves to `undefined` when there are
+     * no errors detected.
+     */
+    setSinkId(deviceId) {
+        return this.backend.setSinkId(deviceId);
+    }
+
+    /**
+     * Set the playback volume.
+     *
+     * @param {number} newVolume A value between 0 and 1, 0 being no
+     * volume and 1 being full volume.
+     * @emits WaveSurfer#volume
+     */
+    setVolume(newVolume) {
+        this.backend.setVolume(newVolume);
+        this.fireEvent('volume', newVolume);
+    }
+
+    /**
+     * Get the playback volume.
+     *
+     * @return {number} A value between 0 and 1, 0 being no
+     * volume and 1 being full volume.
+     */
+    getVolume() {
+        return this.backend.getVolume();
+    }
+
+    /**
+     * 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.
+     * @example wavesurfer.setPlaybackRate(2);
+     */
+    setPlaybackRate(rate) {
+        this.backend.setPlaybackRate(rate);
+    }
+
+    /**
+     * Get the playback rate.
+     *
+     * @return {number} The current playback rate.
+     */
+    getPlaybackRate() {
+        return this.backend.getPlaybackRate();
+    }
+
+    /**
+     * Toggle the volume on and off. If not currently 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.
+     *
+     * @example wavesurfer.toggleMute();
+     */
+    toggleMute() {
+        this.setMute(!this.isMuted);
+    }
+
+    /**
+     * Enable or disable muted audio
+     *
+     * @param {boolean} mute Specify `true` to mute audio.
+     * @emits WaveSurfer#volume
+     * @emits WaveSurfer#mute
+     * @example
+     * // unmute
+     * wavesurfer.setMute(false);
+     * console.log(wavesurfer.getMute()) // logs false
+     */
+    setMute(mute) {
+        // ignore all muting requests if the audio is already in that state
+        if (mute === this.isMuted) {
+            this.fireEvent('mute', this.isMuted);
+            return;
+        }
+
+        if (this.backend.setMute) {
+            // Backends such as the MediaElement backend have their own handling
+            // of mute, let them handle it.
+            this.backend.setMute(mute);
+            this.isMuted = mute;
+        } else {
+            if (mute) {
+                // 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;
+                this.fireEvent('volume', 0);
+            } else {
+                // If currently muted then restore to the saved volume
+                // and update the mute properties
+                this.backend.setVolume(this.savedVolume);
+                this.isMuted = false;
+                this.fireEvent('volume', this.savedVolume);
+            }
+        }
+        this.fireEvent('mute', this.isMuted);
+    }
+
+    /**
+     * Get the current mute status.
+     *
+     * @example const isMuted = wavesurfer.getMute();
+     * @return {boolean} Current mute status
+     */
+    getMute() {
+        return this.isMuted;
+    }
+
+    /**
+     * Get the list of current set filters as an array.
+     *
+     * Filters must be set with setFilters method first
+     *
+     * @return {array} List of enabled filters
+     */
+    getFilters() {
+        return this.backend.filters || [];
+    }
+
+    /**
+     * Toggles `scrollParent` and redraws
+     *
+     * @example wavesurfer.toggleScroll();
+     */
+    toggleScroll() {
+        this.params.scrollParent = !this.params.scrollParent;
+        this.drawBuffer();
+    }
+
+    /**
+     * Toggle mouse interaction
+     *
+     * @example wavesurfer.toggleInteraction();
+     */
+    toggleInteraction() {
+        this.params.interact = !this.params.interact;
+    }
+
+    /**
+     * Get the fill color of the waveform after the cursor.
+     *
+     * @return {string} A CSS color string.
+     */
+    getWaveColor() {
+        return this.params.waveColor;
+    }
+
+    /**
+     * Set the fill color of the waveform after the cursor.
+     *
+     * @param {string} color A CSS color string.
+     * @example wavesurfer.setWaveColor('#ddd');
+     */
+    setWaveColor(color) {
+        this.params.waveColor = color;
+        this.drawBuffer();
+    }
+
+    /**
+     * Get the fill color of the waveform behind the cursor.
+     *
+     * @return {string} A CSS color string.
+     */
+    getProgressColor() {
+        return this.params.progressColor;
+    }
+
+    /**
+     * Set the fill color of the waveform behind the cursor.
+     *
+     * @param {string} color A CSS color string.
+     * @example wavesurfer.setProgressColor('#400');
+     */
+    setProgressColor(color) {
+        this.params.progressColor = color;
+        this.drawBuffer();
+    }
+
+    /**
+     * Get the background color of the waveform container.
+     *
+     * @return {string} A CSS color string.
+     */
+    getBackgroundColor() {
+        return this.params.backgroundColor;
+    }
+
+    /**
+     * Set the background color of the waveform container.
+     *
+     * @param {string} color A CSS color string.
+     * @example wavesurfer.setBackgroundColor('#FF00FF');
+     */
+    setBackgroundColor(color) {
+        this.params.backgroundColor = color;
+        util.style(this.container, { background: this.params.backgroundColor });
+    }
+
+    /**
+     * Get the fill color of the cursor indicating the playhead
+     * position.
+     *
+     * @return {string} A CSS color string.
+     */
+    getCursorColor() {
+        return this.params.cursorColor;
+    }
+
+    /**
+     * Set the fill color of the cursor indicating the playhead
+     * position.
+     *
+     * @param {string} color A CSS color string.
+     * @example wavesurfer.setCursorColor('#222');
+     */
+    setCursorColor(color) {
+        this.params.cursorColor = color;
+        this.drawer.updateCursor();
+    }
+
+    /**
+     * Get the height of the waveform.
+     *
+     * @return {number} Height measured in pixels.
+     */
+    getHeight() {
+        return this.params.height;
+    }
+
+    /**
+     * Set the height of the waveform.
+     *
+     * @param {number} height Height measured in pixels.
+     * @example wavesurfer.setHeight(200);
+     */
+    setHeight(height) {
+        this.params.height = height;
+        this.drawer.setHeight(height * this.params.pixelRatio);
+        this.drawBuffer();
+    }
+
+    /**
+     * Hide channels from being drawn on the waveform if splitting channels.
+     *
+     * For example, if we want to draw only the peaks for the right stereo channel:
+     *
+     * const wavesurfer = new WaveSurfer.create({...splitChannels: true});
+     * wavesurfer.load('stereo_audio.mp3');
+     *
+     * wavesurfer.setFilteredChannel([0]); <-- hide left channel peaks.
+     *
+     * @param {array} channelIndices Channels to be filtered out from drawing.
+     * @version 4.0.0
+     */
+    setFilteredChannels(channelIndices) {
+        this.params.splitChannelsOptions.filterChannels = channelIndices;
+        this.drawBuffer();
+    }
+
+    /**
+     * Get the correct peaks for current wave view-port and render wave
+     *
+     * @private
+     * @emits WaveSurfer#redraw
+     */
+    drawBuffer() {
+        const nominalWidth = Math.round(
+            this.getDuration() *
+                this.params.minPxPerSec *
+                this.params.pixelRatio
+        );
+        const parentWidth = this.drawer.getWidth();
+        let width = nominalWidth;
+        // always start at 0 after zooming for scrolling : issue redraw left part
+        let start = 0;
+        let end = Math.max(start + parentWidth, width);
+        // Fill container
+        if (
+            this.params.fillParent &&
+            (!this.params.scrollParent || nominalWidth < parentWidth)
+        ) {
+            width = parentWidth;
+            start = 0;
+            end = width;
+        }
+
+        let peaks;
+        if (this.params.partialRender) {
+            const newRanges = this.peakCache.addRangeToPeakCache(
+                width,
+                start,
+                end
+            );
+            let i;
+            for (i = 0; i < newRanges.length; i++) {
+                peaks = this.backend.getPeaks(
+                    width,
+                    newRanges[i][0],
+                    newRanges[i][1]
+                );
+                this.drawer.drawPeaks(
+                    peaks,
+                    width,
+                    newRanges[i][0],
+                    newRanges[i][1]
+                );
+            }
+        } else {
+            peaks = this.backend.getPeaks(width, start, end);
+            this.drawer.drawPeaks(peaks, width, start, end);
+        }
+        this.fireEvent('redraw', peaks, width);
+    }
+
+    /**
+     * Horizontally zooms the waveform in and out. It also changes the parameter
+     * `minPxPerSec` and enables the `scrollParent` option. Calling the function
+     * with a falsey parameter will reset the zoom state.
+     *
+     * @param {?number} pxPerSec Number of horizontal pixels per second of
+     * audio, if none is set the waveform returns to unzoomed state
+     * @emits WaveSurfer#zoom
+     * @example wavesurfer.zoom(20);
+     */
+    zoom(pxPerSec) {
+        if (!pxPerSec) {
+            this.params.minPxPerSec = this.defaultParams.minPxPerSec;
+            this.params.scrollParent = false;
+        } else {
+            this.params.minPxPerSec = pxPerSec;
+            this.params.scrollParent = true;
+        }
+
+        this.drawBuffer();
+        this.drawer.progress(this.backend.getPlayedPercents());
+
+        this.drawer.recenter(this.getCurrentTime() / this.getDuration());
+        this.fireEvent('zoom', pxPerSec);
+    }
+
+    /**
+     * Decode buffer and load
+     *
+     * @private
+     * @param {ArrayBuffer} arraybuffer Buffer to process
+     */
+    loadArrayBuffer(arraybuffer) {
+        this.decodeArrayBuffer(arraybuffer, data => {
+            if (!this.isDestroyed) {
+                this.loadDecodedBuffer(data);
+            }
+        });
+    }
+
+    /**
+     * Directly load an externally decoded AudioBuffer
+     *
+     * @private
+     * @param {AudioBuffer} buffer Buffer to process
+     * @emits WaveSurfer#ready
+     */
+    loadDecodedBuffer(buffer) {
+        this.backend.load(buffer);
+        this.drawBuffer();
+        this.isReady = true;
+        this.fireEvent('ready');
+    }
+
+    /**
+     * Loads audio data from a Blob or File object
+     *
+     * @param {Blob|File} blob Audio data
+     * @example
+     */
+    loadBlob(blob) {
+        // Create file reader
+        const reader = new FileReader();
+        reader.addEventListener('progress', e => this.onProgress(e));
+        reader.addEventListener('load', e =>
+            this.loadArrayBuffer(e.target.result)
+        );
+        reader.addEventListener('error', () =>
+            this.fireEvent('error', 'Error reading file')
+        );
+        reader.readAsArrayBuffer(blob);
+        this.empty();
+    }
+
+    /**
+     * Loads audio and re-renders the waveform.
+     *
+     * @param {string|HTMLMediaElement} url The url of the audio file or the
+     * audio element with the audio
+     * @param {number[]|Number.<Array[]>} peaks Wavesurfer does not have to decode
+     * the audio to render the waveform if this is specified
+     * @param {?string} preload (Use with backend `MediaElement` and `MediaElementWebAudio`)
+     * `'none'|'metadata'|'auto'` Preload attribute for the media element
+     * @param {?number} duration The duration of the audio. This is used to
+     * render the peaks data in the correct size for the audio duration (as
+     * befits the current `minPxPerSec` and zoom value) without having to decode
+     * the audio.
+     * @returns {void}
+     * @throws Will throw an error if the `url` argument is empty.
+     * @example
+     * // uses fetch or media element to load file (depending on backend)
+     * wavesurfer.load('http://example.com/demo.wav');
+     *
+     * // setting preload attribute with media element backend and supplying
+     * // peaks
+     * wavesurfer.load(
+     *   'http://example.com/demo.wav',
+     *   [0.0218, 0.0183, 0.0165, 0.0198, 0.2137, 0.2888],
+     *   true
+     * );
+     */
+    load(url, peaks, preload, duration) {
+        if (!url) {
+            throw new Error('url parameter cannot be empty');
+        }
+        this.empty();
+        if (preload) {
+            // check whether the preload attribute will be usable and if not log
+            // a warning listing the reasons why not and nullify the variable
+            const preloadIgnoreReasons = {
+                "Preload is not 'auto', 'none' or 'metadata'":
+                    ['auto', 'metadata', 'none'].indexOf(preload) === -1,
+                'Peaks are not provided': !peaks,
+                "Backend is not of type 'MediaElement' or 'MediaElementWebAudio'":
+                    ['MediaElement', 'MediaElementWebAudio'].indexOf(
+                        this.params.backend
+                    ) === -1,
+                'Url is not of type string': typeof url !== 'string'
+            };
+            const activeReasons = Object.keys(preloadIgnoreReasons).filter(
+                reason => preloadIgnoreReasons[reason]
+            );
+            if (activeReasons.length) {
+                // eslint-disable-next-line no-console
+                console.warn(
+                    'Preload parameter of wavesurfer.load will be ignored because:\n\t- ' +
+                        activeReasons.join('\n\t- ')
+                );
+                // stop invalid values from being used
+                preload = null;
+            }
+        }
+
+        switch (this.params.backend) {
+            case 'WebAudio':
+                return this.loadBuffer(url, peaks, duration);
+            case 'MediaElement':
+            case 'MediaElementWebAudio':
+                return this.loadMediaElement(url, peaks, preload, duration);
+        }
+    }
+
+    /**
+     * Loads audio using Web Audio buffer backend.
+     *
+     * @private
+     * @param {string} url URL of audio file
+     * @param {number[]|Number.<Array[]>} peaks Peaks data
+     * @param {?number} duration Optional duration of audio file
+     * @returns {void}
+     */
+    loadBuffer(url, peaks, duration) {
+        const load = action => {
+            if (action) {
+                this.tmpEvents.push(this.once('ready', action));
+            }
+            return this.getArrayBuffer(url, data => this.loadArrayBuffer(data));
+        };
+
+        if (peaks) {
+            this.backend.setPeaks(peaks, duration);
+            this.drawBuffer();
+            this.tmpEvents.push(this.once('interaction', load));
+        } else {
+            return load();
+        }
+    }
+
+    /**
+     * Either create a media element, or load an existing media element.
+     *
+     * @private
+     * @param {string|HTMLMediaElement} urlOrElt Either a path to a media file, or an
+     * existing HTML5 Audio/Video Element
+     * @param {number[]|Number.<Array[]>} peaks Array of peaks. Required to bypass web audio
+     * dependency
+     * @param {?boolean} preload Set to true if the preload attribute of the
+     * audio element should be enabled
+     * @param {?number} duration Optional duration of audio file
+     */
+    loadMediaElement(urlOrElt, peaks, preload, duration) {
+        let url = urlOrElt;
+
+        if (typeof urlOrElt === 'string') {
+            this.backend.load(url, this.mediaContainer, peaks, preload);
+        } else {
+            const elt = urlOrElt;
+            this.backend.loadElt(elt, peaks);
+
+            // If peaks are not provided,
+            // url = element.src so we can get peaks with web audio
+            url = elt.src;
+        }
+
+        this.tmpEvents.push(
+            this.backend.once('canplay', () => {
+                // ignore when backend was already destroyed
+                if (!this.backend.destroyed) {
+                    this.drawBuffer();
+                    this.isReady = true;
+                    this.fireEvent('ready');
+                }
+            }),
+            this.backend.once('error', err => this.fireEvent('error', err))
+        );
+
+        // If no pre-decoded peaks provided or pre-decoded peaks are
+        // provided with forceDecode flag, attempt to download the
+        // audio file and decode it with Web Audio.
+        if (peaks) {
+            this.backend.setPeaks(peaks, duration);
+        }
+
+        if (
+            (!peaks || this.params.forceDecode) &&
+            this.backend.supportsWebAudio()
+        ) {
+            this.getArrayBuffer(url, arraybuffer => {
+                this.decodeArrayBuffer(arraybuffer, buffer => {
+                    this.backend.buffer = buffer;
+                    this.backend.setPeaks(null);
+                    this.drawBuffer();
+                    this.fireEvent('waveform-ready');
+                });
+            });
+        }
+    }
+
+    /**
+     * Decode an array buffer and pass data to a callback
+     *
+     * @private
+     * @param {Object} arraybuffer The array buffer to decode
+     * @param {function} callback The function to call on complete
+     */
+    decodeArrayBuffer(arraybuffer, callback) {
+        this.arraybuffer = arraybuffer;
+        this.backend.decodeArrayBuffer(
+            arraybuffer,
+            data => {
+                // Only use the decoded data if we haven't been destroyed or
+                // another decode started in the meantime
+                if (!this.isDestroyed && this.arraybuffer == arraybuffer) {
+                    callback(data);
+                    this.arraybuffer = null;
+                }
+            },
+            () => this.fireEvent('error', 'Error decoding audiobuffer')
+        );
+    }
+
+    /**
+     * Load an array buffer using fetch and pass the result to a callback
+     *
+     * @param {string} url The URL of the file object
+     * @param {function} callback The function to call on complete
+     * @returns {util.fetchFile} fetch call
+     * @private
+     */
+    getArrayBuffer(url, callback) {
+        let options = Object.assign(
+            {
+                url: url,
+                responseType: 'arraybuffer'
+            },
+            this.params.xhr
+        );
+        const request = util.fetchFile(options);
+
+        this.currentRequest = request;
+
+        this.tmpEvents.push(
+            request.on('progress', e => {
+                this.onProgress(e);
+            }),
+            request.on('success', data => {
+                callback(data);
+                this.currentRequest = null;
+            }),
+            request.on('error', e => {
+                this.fireEvent('error', e);
+                this.currentRequest = null;
+            })
+        );
+
+        return request;
+    }
+
+    /**
+     * Called while the audio file is loading
+     *
+     * @private
+     * @param {Event} e Progress event
+     * @emits WaveSurfer#loading
+     */
+    onProgress(e) {
+        let percentComplete;
+        if (e.lengthComputable) {
+            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.
+     *
+     * @param {number} length=1024 The scale in which to export the peaks
+     * @param {number} accuracy=10000
+     * @param {?boolean} noWindow Set to true to disable opening a new
+     * window with the JSON
+     * @param {number} start Start index
+     * @param {number} end End index
+     * @return {Promise} Promise that resolves with array of peaks
+     */
+    exportPCM(length, accuracy, noWindow, start, end) {
+        length = length || 1024;
+        start = start || 0;
+        accuracy = accuracy || 10000;
+        noWindow = noWindow || false;
+        const peaks = this.backend.getPeaks(length, start, end);
+        const arr = [].map.call(
+            peaks,
+            val => Math.round(val * accuracy) / accuracy
+        );
+        return new Promise((resolve, reject) => {
+            const json = JSON.stringify(arr);
+
+            if (!noWindow) {
+                window.open(
+                    'data:application/json;charset=utf-8,' +
+                        encodeURIComponent(json)
+                );
+            }
+            resolve(json);
+        });
+    }
+
+    /**
+     * Save waveform image as data URI.
+     *
+     * The default format is `'image/png'`. Other supported types are
+     * `'image/jpeg'` and `'image/webp'`.
+     *
+     * @param {string} format='image/png' A string indicating the image format.
+     * The default format type is `'image/png'`.
+     * @param {number} quality=1 A number between 0 and 1 indicating the image
+     * quality to use for image formats that use lossy compression such as
+     * `'image/jpeg'`` and `'image/webp'`.
+     * @param {string} type Image data type to return. Either 'dataURL' (default)
+     * or 'blob'.
+     * @return {string|string[]|Promise} When using `'dataURL'` type this returns
+     * a single data URL or an array of data URLs, one for each canvas. When using
+     * `'blob'` type this returns a `Promise` resolving with an array of `Blob`
+     * instances, one for each canvas.
+     */
+    exportImage(format, quality, type) {
+        if (!format) {
+            format = 'image/png';
+        }
+        if (!quality) {
+            quality = 1;
+        }
+        if (!type) {
+            type = 'dataURL';
+        }
+
+        return this.drawer.getImage(format, quality, type);
+    }
+
+    /**
+     * Cancel any fetch request currently in progress
+     */
+    cancelAjax() {
+        if (this.currentRequest && this.currentRequest.controller) {
+            this.currentRequest.controller.abort();
+            this.currentRequest = null;
+        }
+    }
+
+    /**
+     * @private
+     */
+    clearTmpEvents() {
+        this.tmpEvents.forEach(e => e.un());
+    }
+
+    /**
+     * Display empty waveform.
+     */
+    empty() {
+        if (!this.backend.isPaused()) {
+            this.stop();
+            this.backend.disconnectSource();
+        }
+        this.isReady = false;
+        this.cancelAjax();
+        this.clearTmpEvents();
+
+        // empty drawer
+        this.drawer.progress(0);
+        this.drawer.setWidth(0);
+        this.drawer.drawPeaks({ length: this.drawer.getWidth() }, 0);
+    }
+
+    /**
+     * Remove events, elements and disconnect WebAudio nodes.
+     *
+     * @emits WaveSurfer#destroy
+     */
+    destroy() {
+        this.destroyAllPlugins();
+        this.fireEvent('destroy');
+        this.cancelAjax();
+        this.clearTmpEvents();
+        this.unAll();
+        if (this.params.responsive !== false) {
+            window.removeEventListener('resize', this._onResize, true);
+            window.removeEventListener(
+                'orientationchange',
+                this._onResize,
+                true
+            );
+        }
+        if (this.backend) {
+            this.backend.destroy();
+        }
+        if (this.drawer) {
+            this.drawer.destroy();
+        }
+        this.isDestroyed = true;
+        this.isReady = false;
+        this.arraybuffer = null;
+    }
+}
Index: gs3-extensions/mars-src/trunk/src/wavesurfer-renderer/wavesurfer.js.orig
===================================================================
--- gs3-extensions/mars-src/trunk/src/wavesurfer-renderer/wavesurfer.js.orig	(revision 34668)
+++ gs3-extensions/mars-src/trunk/src/wavesurfer-renderer/wavesurfer.js.orig	(revision 34668)
@@ -0,0 +1,1693 @@
+import * as util from './util';
+import MultiCanvas from './drawer.multicanvas';
+import WebAudio from './webaudio';
+import MediaElement from './mediaelement';
+import PeakCache from './peakcache';
+import MediaElementWebAudio from './mediaelement-webaudio';
+
+/*
+ * This work is licensed under a BSD-3-Clause License.
+ */
+
+/** @external {HTMLElement} https://developer.mozilla.org/en/docs/Web/API/HTMLElement */
+/** @external {OfflineAudioContext} https://developer.mozilla.org/en-US/docs/Web/API/OfflineAudioContext */
+/** @external {File} https://developer.mozilla.org/en-US/docs/Web/API/File */
+/** @external {Blob} https://developer.mozilla.org/en-US/docs/Web/API/Blob */
+/** @external {CanvasRenderingContext2D} https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D */
+/** @external {MediaStreamConstraints} https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamConstraints */
+/** @external {AudioNode} https://developer.mozilla.org/de/docs/Web/API/AudioNode */
+
+/**
+ * @typedef {Object} WavesurferParams
+ * @property {AudioContext} audioContext=null Use your own previously
+ * initialized AudioContext or leave blank.
+ * @property {number} audioRate=1 Speed at which to play audio. Lower number is
+ * slower.
+ * @property {ScriptProcessorNode} audioScriptProcessor=null Use your own previously
+ * initialized ScriptProcessorNode or leave blank.
+ * @property {boolean} autoCenter=true If a scrollbar is present, center the
+ * waveform on current progress
+ * @property {number} autoCenterRate=5 If autoCenter is active, rate at which the
+ * waveform is centered
+ * @property {boolean} autoCenterImmediately=false If autoCenter is active, immediately
+ * center waveform on current progress
+ * @property {string} backend='WebAudio' `'WebAudio'|'MediaElement'|'MediaElementWebAudio'` In most cases
+ * you don't have to set this manually. MediaElement is a fallback for unsupported browsers.
+ * MediaElementWebAudio allows to use WebAudio API also with big audio files, loading audio like with
+ * MediaElement backend (HTML5 audio tag). You have to use the same methods of MediaElement backend for loading and
+ * playback, giving also peaks, so the audio data are not decoded. In this way you can use WebAudio features, like filters,
+ * also with audio with big duration. For example:
+ * ` wavesurfer.load(url | HTMLMediaElement, peaks, preload, duration);
+ *   wavesurfer.play();
+ *   wavesurfer.setFilter(customFilter);
+ * `
+ * @property {string} backgroundColor=null Change background color of the
+ * waveform container.
+ * @property {number} barHeight=1 The height of the wave bars.
+ * @property {number} barRadius=0 The radius of the wave bars. Makes bars rounded
+ * @property {number} barGap=null The optional spacing between bars of the wave,
+ * if not provided will be calculated in legacy format.
+ * @property {number} barWidth=null Draw the waveform using bars.
+ * @property {number} barMinHeight=null If specified, draw at least a bar of this height,
+ * eliminating waveform gaps
+ * @property {boolean} closeAudioContext=false Close and nullify all audio
+ * contexts when the destroy method is called.
+ * @property {!string|HTMLElement} container CSS selector or HTML element where
+ * the waveform should be drawn. This is the only required parameter.
+ * @property {string} cursorColor='#333' The fill color of the cursor indicating
+ * the playhead position.
+ * @property {number} cursorWidth=1 Measured in pixels.
+ * @property {object} drawingContextAttributes={desynchronized: false} Drawing context
+ * attributes.
+ * @property {number} duration=null Optional audio length so pre-rendered peaks
+ * can be display immediately for example.
+ * @property {boolean} fillParent=true Whether to fill the entire container or
+ * draw only according to `minPxPerSec`.
+ * @property {boolean} forceDecode=false Force decoding of audio using web audio
+ * when zooming to get a more detailed waveform.
+ * @property {number} height=128 The height of the waveform. Measured in
+ * pixels.
+ * @property {boolean} hideScrollbar=false Whether to hide the horizontal
+ * scrollbar when one would normally be shown.
+ * @property {boolean} interact=true Whether the mouse interaction will be
+ * enabled at initialization. You can switch this parameter at any time later
+ * on.
+ * @property {boolean} loopSelection=true (Use with regions plugin) Enable
+ * looping of selected regions
+ * @property {number} maxCanvasWidth=4000 Maximum width of a single canvas in
+ * pixels, excluding a small overlap (2 * `pixelRatio`, rounded up to the next
+ * even integer). If the waveform is longer than this value, additional canvases
+ * will be used to render the waveform, which is useful for very large waveforms
+ * that may be too wide for browsers to draw on a single canvas.
+ * @property {boolean} mediaControls=false (Use with backend `MediaElement` or `MediaElementWebAudio`)
+ * this enables the native controls for the media element
+ * @property {string} mediaType='audio' (Use with backend `MediaElement` or `MediaElementWebAudio`)
+ * `'audio'|'video'` ('video' only for `MediaElement`)
+ * @property {number} minPxPerSec=20 Minimum number of pixels per second of
+ * audio.
+ * @property {boolean} normalize=false If true, normalize by the maximum peak
+ * instead of 1.0.
+ * @property {boolean} partialRender=false Use the PeakCache to improve
+ * rendering speed of large waveforms
+ * @property {number} pixelRatio=window.devicePixelRatio The pixel ratio used to
+ * calculate display
+ * @property {PluginDefinition[]} plugins=[] An array of plugin definitions to
+ * register during instantiation, they will be directly initialised unless they
+ * are added with the `deferInit` property set to true.
+ * @property {string} progressColor='#555' The fill color of the part of the
+ * waveform behind the cursor. When `progressColor` and `waveColor` are the same
+ * the progress wave is not rendered at all.
+ * @property {boolean} removeMediaElementOnDestroy=true Set to false to keep the
+ * media element in the DOM when the player is destroyed. This is useful when
+ * reusing an existing media element via the `loadMediaElement` method.
+ * @property {Object} renderer=MultiCanvas Can be used to inject a custom
+ * renderer.
+ * @property {boolean|number} responsive=false If set to `true` resize the
+ * waveform, when the window is resized. This is debounced with a `100ms`
+ * timeout by default. If this parameter is a number it represents that timeout.
+ * @property {boolean} rtl=false If set to `true`, renders waveform from
+ * right-to-left.
+ * @property {boolean} scrollParent=false Whether to scroll the container with a
+ * lengthy waveform. Otherwise the waveform is shrunk to the container width
+ * (see fillParent).
+ * @property {number} skipLength=2 Number of seconds to skip with the
+ * skipForward() and skipBackward() methods.
+ * @property {boolean} splitChannels=false Render with separate waveforms for
+ * the channels of the audio
+ * @property {string} waveColor='#999' The fill color of the waveform after the
+ * cursor.
+ * @property {object} xhr={} XHR options. For example:
+ * `let xhr = {
+ *     cache: 'default',
+ *     mode: 'cors',
+ *     method: 'GET',
+ *     credentials: 'same-origin',
+ *     redirect: 'follow',
+ *     referrer: 'client',
+ *     headers: [
+ *         {
+ *             key: 'Authorization',
+ *             value: 'my-token'
+ *         }
+ *     ]
+ * };`
+ */
+
+/**
+ * @typedef {Object} PluginDefinition
+ * @desc The Object used to describe a plugin
+ * @example wavesurfer.addPlugin(pluginDefinition);
+ * @property {string} name The name of the plugin, the plugin instance will be
+ * added as a property to the wavesurfer instance under this name
+ * @property {?Object} staticProps The properties that should be added to the
+ * wavesurfer instance as static properties
+ * @property {?boolean} deferInit Don't initialise plugin
+ * automatically
+ * @property {Object} params={} The plugin parameters, they are the first parameter
+ * passed to the plugin class constructor function
+ * @property {PluginClass} instance The plugin instance factory, is called with
+ * the dependency specified in extends. Returns the plugin class.
+ */
+
+/**
+ * @interface PluginClass
+ *
+ * @desc This is the interface which is implemented by all plugin classes. Note
+ * that this only turns into an observer after being passed through
+ * `wavesurfer.addPlugin`.
+ *
+ * @extends {Observer}
+ */
+class PluginClass {
+    /**
+     * Plugin definition factory
+     *
+     * This function must be used to create a plugin definition which can be
+     * used by wavesurfer to correctly instantiate the plugin.
+     *
+     * It returns a `PluginDefinition` object representing the plugin.
+     *
+     * @param {Object} params={} The plugin params (specific to the plugin)
+     */
+    create(params) {}
+    /**
+     * Construct the plugin
+     *
+     * @param {Object} params={} The plugin params (specific to the plugin)
+     * @param {Object} ws The wavesurfer instance
+     */
+    constructor(params, ws) {}
+    /**
+     * Initialise the plugin
+     *
+     * Start doing something. This is called by
+     * `wavesurfer.initPlugin(pluginName)`
+     */
+    init() {}
+    /**
+     * Destroy the plugin instance
+     *
+     * Stop doing something. This is called by
+     * `wavesurfer.destroyPlugin(pluginName)`
+     */
+    destroy() {}
+}
+
+/**
+ * WaveSurfer core library class
+ *
+ * @extends {Observer}
+ * @example
+ * const params = {
+ *   container: '#waveform',
+ *   waveColor: 'violet',
+ *   progressColor: 'purple'
+ * };
+ *
+ * // initialise like this
+ * const wavesurfer = WaveSurfer.create(params);
+ *
+ * // or like this ...
+ * const wavesurfer = new WaveSurfer(params);
+ * wavesurfer.init();
+ *
+ * // load audio file
+ * wavesurfer.load('example/media/demo.wav');
+ */
+export default class WaveSurfer extends util.Observer {
+    /** @private */
+    defaultParams = {
+        audioContext: null,
+        audioScriptProcessor: null,
+        audioRate: 1,
+        autoCenter: true,
+        autoCenterRate: 5,
+        autoCenterImmediately: false,
+        backend: 'WebAudio',
+        backgroundColor: null,
+        barHeight: 1,
+        barRadius: 0,
+        barGap: null,
+        barMinHeight: null,
+        container: null,
+        cursorColor: '#333',
+        cursorWidth: 1,
+        dragSelection: true,
+        drawingContextAttributes: {
+            // Boolean that hints the user agent to reduce the latency
+            // by desynchronizing the canvas paint cycle from the event
+            // loop
+            desynchronized: false
+        },
+        duration: null,
+        fillParent: true,
+        forceDecode: false,
+        height: 128,
+        hideScrollbar: false,
+        interact: true,
+        loopSelection: true,
+        maxCanvasWidth: 4000,
+        mediaContainer: null,
+        mediaControls: false,
+        mediaType: 'audio',
+        minPxPerSec: 20,
+        normalize: false,
+        partialRender: false,
+        pixelRatio:
+            window.devicePixelRatio || screen.deviceXDPI / screen.logicalXDPI,
+        plugins: [],
+        progressColor: '#555',
+        removeMediaElementOnDestroy: true,
+        renderer: MultiCanvas,
+        responsive: false,
+        rtl: false,
+        scrollParent: false,
+        skipLength: 2,
+        splitChannels: false,
+        splitChannelsOptions: {
+            overlay: false,
+            channelColors: {},
+            filterChannels: [],
+        },
+        waveColor: '#999',
+        xhr: {}
+    };
+
+    /** @private */
+    backends = {
+        MediaElement,
+        WebAudio,
+        MediaElementWebAudio
+    };
+
+    /**
+     * Instantiate this class, call its `init` function and returns it
+     *
+     * @param {WavesurferParams} params The wavesurfer parameters
+     * @return {Object} WaveSurfer instance
+     * @example const wavesurfer = WaveSurfer.create(params);
+     */
+    static create(params) {
+        const wavesurfer = new WaveSurfer(params);
+        return wavesurfer.init();
+    }
+
+    /**
+     * The library version number is available as a static property of the
+     * WaveSurfer class
+     *
+     * @type {String}
+     * @example
+     * console.log('Using wavesurfer.js ' + WaveSurfer.VERSION);
+     */
+    static VERSION = __VERSION__;
+
+    /**
+     * Functions in the `util` property are available as a prototype property to
+     * all instances
+     *
+     * @type {Object}
+     * @example
+     * const wavesurfer = WaveSurfer.create(params);
+     * wavesurfer.util.style(myElement, { background: 'blue' });
+     */
+    util = util;
+
+    /**
+     * Functions in the `util` property are available as a static property of the
+     * WaveSurfer class
+     *
+     * @type {Object}
+     * @example
+     * WaveSurfer.util.style(myElement, { background: 'blue' });
+     */
+    static util = util;
+
+    /**
+     * Initialise wavesurfer instance
+     *
+     * @param {WavesurferParams} params Instantiation options for wavesurfer
+     * @example
+     * const wavesurfer = new WaveSurfer(params);
+     * @returns {this} Wavesurfer instance
+     */
+    constructor(params) {
+        super();
+        /**
+         * Extract relevant parameters (or defaults)
+         * @private
+         */
+        this.params = Object.assign({}, this.defaultParams, params);
+
+        /** @private */
+        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) {
+            /** @private */
+            this.mediaContainer = this.container;
+        } else if (typeof this.params.mediaContainer == 'string') {
+            /** @private */
+            this.mediaContainer = document.querySelector(
+                this.params.mediaContainer
+            );
+        } else {
+            /** @private */
+            this.mediaContainer = this.params.mediaContainer;
+        }
+
+        if (!this.mediaContainer) {
+            throw new Error('Media Container element not found');
+        }
+
+        if (this.params.maxCanvasWidth <= 1) {
+            throw new Error('maxCanvasWidth must be greater than 1');
+        } else if (this.params.maxCanvasWidth % 2 == 1) {
+            throw new Error('maxCanvasWidth must be an even number');
+        }
+
+        if (this.params.rtl === true) {
+            util.style(this.container, { transform: 'rotateY(180deg)' });
+        }
+
+        if (this.params.backgroundColor) {
+            this.setBackgroundColor(this.params.backgroundColor);
+        }
+
+        /**
+         * @private Used to save the current volume when muting so we can
+         * restore once unmuted
+         * @type {number}
+         */
+        this.savedVolume = 0;
+
+        /**
+         * @private The current muted state
+         * @type {boolean}
+         */
+        this.isMuted = false;
+
+        /**
+         * @private Will hold a list of event descriptors that need to be
+         * canceled on subsequent loads of audio
+         * @type {Object[]}
+         */
+        this.tmpEvents = [];
+
+        /**
+         * @private Holds any running audio downloads
+         * @type {Observer}
+         */
+        this.currentRequest = null;
+        /** @private */
+        this.arraybuffer = null;
+        /** @private */
+        this.drawer = null;
+        /** @private */
+        this.backend = null;
+        /** @private */
+        this.peakCache = null;
+
+        // cache constructor objects
+        if (typeof this.params.renderer !== 'function') {
+            throw new Error('Renderer parameter is invalid');
+        }
+        /**
+         * @private The uninitialised Drawer class
+         */
+        this.Drawer = this.params.renderer;
+        /**
+         * @private The uninitialised Backend class
+         */
+        // Back compat
+        if (this.params.backend == 'AudioElement') {
+            this.params.backend = 'MediaElement';
+        }
+
+        if (
+            (this.params.backend == 'WebAudio' ||
+                this.params.backend === 'MediaElementWebAudio') &&
+            !WebAudio.prototype.supportsWebAudio.call(null)
+        ) {
+            this.params.backend = 'MediaElement';
+        }
+        this.Backend = this.backends[this.params.backend];
+
+        /**
+         * @private map of plugin names that are currently initialised
+         */
+        this.initialisedPluginList = {};
+        /** @private */
+        this.isDestroyed = false;
+
+        /**
+         * Get the current ready status.
+         *
+         * @example const isReady = wavesurfer.isReady;
+         * @return {boolean}
+         */
+        this.isReady = false;
+
+        // responsive debounced event listener. If this.params.responsive is not
+        // set, this is never called. Use 100ms or this.params.responsive as
+        // timeout for the debounce function.
+        let prevWidth = 0;
+        this._onResize = util.debounce(
+            () => {
+                if (
+                    prevWidth != this.drawer.wrapper.clientWidth &&
+                    !this.params.scrollParent
+                ) {
+                    prevWidth = this.drawer.wrapper.clientWidth;
+                    this.drawer.fireEvent('redraw');
+                }
+            },
+            typeof this.params.responsive === 'number'
+                ? this.params.responsive
+                : 100
+        );
+
+        return this;
+    }
+
+    /**
+     * Initialise the wave
+     *
+     * @example
+     * var wavesurfer = new WaveSurfer(params);
+     * wavesurfer.init();
+     * @return {this} The wavesurfer instance
+     */
+    init() {
+        this.registerPlugins(this.params.plugins);
+        this.createDrawer();
+        this.createBackend();
+        this.createPeakCache();
+        return this;
+    }
+
+    /**
+     * Add and initialise array of plugins (if `plugin.deferInit` is falsey),
+     * this function is called in the init function of wavesurfer
+     *
+     * @param {PluginDefinition[]} plugins An array of plugin definitions
+     * @emits {WaveSurfer#plugins-registered} Called with the array of plugin definitions
+     * @return {this} The wavesurfer instance
+     */
+    registerPlugins(plugins) {
+        // first instantiate all the plugins
+        plugins.forEach(plugin => this.addPlugin(plugin));
+
+        // now run the init functions
+        plugins.forEach(plugin => {
+            // call init function of the plugin if deferInit is falsey
+            // in that case you would manually use initPlugins()
+            if (!plugin.deferInit) {
+                this.initPlugin(plugin.name);
+            }
+        });
+        this.fireEvent('plugins-registered', plugins);
+        return this;
+    }
+
+    /**
+     * Get a map of plugin names that are currently initialised
+     *
+     * @example wavesurfer.getPlugins();
+     * @return {Object} Object with plugin names
+     */
+    getActivePlugins() {
+        return this.initialisedPluginList;
+    }
+
+    /**
+     * Add a plugin object to wavesurfer
+     *
+     * @param {PluginDefinition} plugin A plugin definition
+     * @emits {WaveSurfer#plugin-added} Called with the name of the plugin that was added
+     * @example wavesurfer.addPlugin(WaveSurfer.minimap());
+     * @return {this} The wavesurfer instance
+     */
+    addPlugin(plugin) {
+        if (!plugin.name) {
+            throw new Error('Plugin does not have a name!');
+        }
+        if (!plugin.instance) {
+            throw new Error(
+                `Plugin ${plugin.name} does not have an instance property!`
+            );
+        }
+
+        // staticProps properties are applied to wavesurfer instance
+        if (plugin.staticProps) {
+            Object.keys(plugin.staticProps).forEach(pluginStaticProp => {
+                /**
+                 * Properties defined in a plugin definition's `staticProps` property are added as
+                 * staticProps properties of the WaveSurfer instance
+                 */
+                this[pluginStaticProp] = plugin.staticProps[pluginStaticProp];
+            });
+        }
+
+        const Instance = plugin.instance;
+
+        // turn the plugin instance into an observer
+        const observerPrototypeKeys = Object.getOwnPropertyNames(
+            util.Observer.prototype
+        );
+        observerPrototypeKeys.forEach(key => {
+            Instance.prototype[key] = util.Observer.prototype[key];
+        });
+
+        /**
+         * Instantiated plugin classes are added as a property of the wavesurfer
+         * instance
+         * @type {Object}
+         */
+        this[plugin.name] = new Instance(plugin.params || {}, this);
+        this.fireEvent('plugin-added', plugin.name);
+        return this;
+    }
+
+    /**
+     * Initialise a plugin
+     *
+     * @param {string} name A plugin name
+     * @emits WaveSurfer#plugin-initialised
+     * @example wavesurfer.initPlugin('minimap');
+     * @return {this} The wavesurfer instance
+     */
+    initPlugin(name) {
+        if (!this[name]) {
+            throw new Error(`Plugin ${name} has not been added yet!`);
+        }
+        if (this.initialisedPluginList[name]) {
+            // destroy any already initialised plugins
+            this.destroyPlugin(name);
+        }
+        this[name].init();
+        this.initialisedPluginList[name] = true;
+        this.fireEvent('plugin-initialised', name);
+        return this;
+    }
+
+    /**
+     * Destroy a plugin
+     *
+     * @param {string} name A plugin name
+     * @emits WaveSurfer#plugin-destroyed
+     * @example wavesurfer.destroyPlugin('minimap');
+     * @returns {this} The wavesurfer instance
+     */
+    destroyPlugin(name) {
+        if (!this[name]) {
+            throw new Error(
+                `Plugin ${name} has not been added yet and cannot be destroyed!`
+            );
+        }
+        if (!this.initialisedPluginList[name]) {
+            throw new Error(
+                `Plugin ${name} is not active and cannot be destroyed!`
+            );
+        }
+        if (typeof this[name].destroy !== 'function') {
+            throw new Error(`Plugin ${name} does not have a destroy function!`);
+        }
+
+        this[name].destroy();
+        delete this.initialisedPluginList[name];
+        this.fireEvent('plugin-destroyed', name);
+        return this;
+    }
+
+    /**
+     * Destroy all initialised plugins. Convenience function to use when
+     * wavesurfer is removed
+     *
+     * @private
+     */
+    destroyAllPlugins() {
+        Object.keys(this.initialisedPluginList).forEach(name =>
+            this.destroyPlugin(name)
+        );
+    }
+
+    /**
+     * Create the drawer and draw the waveform
+     *
+     * @private
+     * @emits WaveSurfer#drawer-created
+     */
+    createDrawer() {
+        this.drawer = new this.Drawer(this.container, this.params);
+        this.drawer.init();
+        this.fireEvent('drawer-created', this.drawer);
+
+        if (this.params.responsive !== false) {
+            window.addEventListener('resize', this._onResize, true);
+            window.addEventListener('orientationchange', this._onResize, true);
+        }
+
+        this.drawer.on('redraw', () => {
+            this.drawBuffer();
+            this.drawer.progress(this.backend.getPlayedPercents());
+        });
+
+        // Click-to-seek
+        this.drawer.on('click', (e, progress) => {
+            setTimeout(() => this.seekTo(progress), 0);
+        });
+
+        // Relay the scroll event from the drawer
+        this.drawer.on('scroll', e => {
+            if (this.params.partialRender) {
+                this.drawBuffer();
+            }
+            this.fireEvent('scroll', e);
+        });
+    }
+
+    /**
+     * Create the backend
+     *
+     * @private
+     * @emits WaveSurfer#backend-created
+     */
+    createBackend() {
+        if (this.backend) {
+            this.backend.destroy();
+        }
+
+        this.backend = new this.Backend(this.params);
+        this.backend.init();
+        this.fireEvent('backend-created', this.backend);
+
+        this.backend.on('finish', () => {
+            this.drawer.progress(this.backend.getPlayedPercents());
+            this.fireEvent('finish');
+        });
+        this.backend.on('play', () => this.fireEvent('play'));
+        this.backend.on('pause', () => this.fireEvent('pause'));
+
+        this.backend.on('audioprocess', time => {
+            this.drawer.progress(this.backend.getPlayedPercents());
+            this.fireEvent('audioprocess', time);
+        });
+
+        // only needed for MediaElement and MediaElementWebAudio backend
+        if (
+            this.params.backend === 'MediaElement' ||
+            this.params.backend === 'MediaElementWebAudio'
+        ) {
+            this.backend.on('seek', () => {
+                this.drawer.progress(this.backend.getPlayedPercents());
+            });
+
+            this.backend.on('volume', () => {
+                let newVolume = this.getVolume();
+                this.fireEvent('volume', newVolume);
+
+                if (this.backend.isMuted !== this.isMuted) {
+                    this.isMuted = this.backend.isMuted;
+                    this.fireEvent('mute', this.isMuted);
+                }
+            });
+        }
+    }
+
+    /**
+     * Create the peak cache
+     *
+     * @private
+     */
+    createPeakCache() {
+        if (this.params.partialRender) {
+            this.peakCache = new PeakCache();
+        }
+    }
+
+    /**
+     * Get the duration of the audio clip
+     *
+     * @example const duration = wavesurfer.getDuration();
+     * @return {number} Duration in seconds
+     */
+    getDuration() {
+        return this.backend.getDuration();
+    }
+
+    /**
+     * Get the current playback position
+     *
+     * @example const currentTime = wavesurfer.getCurrentTime();
+     * @return {number} Playback position in seconds
+     */
+    getCurrentTime() {
+        return this.backend.getCurrentTime();
+    }
+
+    /**
+     * Set the current play time in seconds.
+     *
+     * @param {number} seconds A positive number in seconds. E.g. 10 means 10
+     * seconds, 60 means 1 minute
+     */
+    setCurrentTime(seconds) {
+        if (seconds >= this.getDuration()) {
+            this.seekTo(1);
+        } else {
+            this.seekTo(seconds / this.getDuration());
+        }
+    }
+
+    /**
+     * Starts playback from the current position. Optional start and end
+     * measured in seconds can be used to set the range of audio to play.
+     *
+     * @param {?number} start Position to start at
+     * @param {?number} end Position to end at
+     * @emits WaveSurfer#interaction
+     * @return {Promise} Result of the backend play method
+     * @example
+     * // play from second 1 to 5
+     * wavesurfer.play(1, 5);
+     */
+    play(start, end) {
+        this.fireEvent('interaction', () => this.play(start, end));
+        return this.backend.play(start, end);
+    }
+
+    /**
+     * Set a point in seconds for playback to stop at.
+     *
+     * @param {number} position Position (in seconds) to stop at
+     * @version 3.3.0
+     */
+    setPlayEnd(position) {
+        this.backend.setPlayEnd(position);
+    }
+
+    /**
+     * Stops and pauses playback
+     *
+     * @example wavesurfer.pause();
+     * @return {Promise} Result of the backend pause method
+     */
+    pause() {
+        if (!this.backend.isPaused()) {
+            return this.backend.pause();
+        }
+    }
+
+    /**
+     * Toggle playback
+     *
+     * @example wavesurfer.playPause();
+     * @return {Promise} Result of the backend play or pause method
+     */
+    playPause() {
+        return this.backend.isPaused() ? this.play() : this.pause();
+    }
+
+    /**
+     * Get the current playback state
+     *
+     * @example const isPlaying = wavesurfer.isPlaying();
+     * @return {boolean} False if paused, true if playing
+     */
+    isPlaying() {
+        return !this.backend.isPaused();
+    }
+
+    /**
+     * Skip backward
+     *
+     * @param {?number} seconds Amount to skip back, if not specified `skipLength`
+     * is used
+     * @example wavesurfer.skipBackward();
+     */
+    skipBackward(seconds) {
+        this.skip(-seconds || -this.params.skipLength);
+    }
+
+    /**
+     * Skip forward
+     *
+     * @param {?number} seconds Amount to skip back, if not specified `skipLength`
+     * is used
+     * @example wavesurfer.skipForward();
+     */
+    skipForward(seconds) {
+        this.skip(seconds || this.params.skipLength);
+    }
+
+    /**
+     * Skip a number of seconds from the current position (use a negative value
+     * to go backwards).
+     *
+     * @param {number} offset Amount to skip back or forwards
+     * @example
+     * // go back 2 seconds
+     * wavesurfer.skip(-2);
+     */
+    skip(offset) {
+        const duration = this.getDuration() || 1;
+        let position = this.getCurrentTime() || 0;
+        position = Math.max(0, Math.min(duration, position + (offset || 0)));
+        this.seekAndCenter(position / duration);
+    }
+
+    /**
+     * Seeks to a position and centers the view
+     *
+     * @param {number} progress Between 0 (=beginning) and 1 (=end)
+     * @example
+     * // seek and go to the middle of the audio
+     * wavesurfer.seekTo(0.5);
+     */
+    seekAndCenter(progress) {
+        this.seekTo(progress);
+        this.drawer.recenter(progress);
+    }
+
+    /**
+     * Seeks to a position
+     *
+     * @param {number} progress Between 0 (=beginning) and 1 (=end)
+     * @emits WaveSurfer#interaction
+     * @emits WaveSurfer#seek
+     * @example
+     * // seek to the middle of the audio
+     * wavesurfer.seekTo(0.5);
+     */
+    seekTo(progress) {
+        // return an error if progress is not a number between 0 and 1
+        if (
+            typeof progress !== 'number' ||
+            !isFinite(progress) ||
+            progress < 0 ||
+            progress > 1
+        ) {
+            throw new Error(
+                'Error calling wavesurfer.seekTo, parameter must be a number between 0 and 1!'
+            );
+        }
+        this.fireEvent('interaction', () => this.seekTo(progress));
+
+        const paused = this.backend.isPaused();
+        // avoid draw wrong position while playing backward seeking
+        if (!paused) {
+            this.backend.pause();
+        }
+        // avoid small scrolls while paused seeking
+        const oldScrollParent = this.params.scrollParent;
+        this.params.scrollParent = false;
+        this.backend.seekTo(progress * this.getDuration());
+        this.drawer.progress(progress);
+
+        if (!paused) {
+            this.backend.play();
+        }
+        this.params.scrollParent = oldScrollParent;
+        this.fireEvent('seek', progress);
+    }
+
+    /**
+     * Stops and goes to the beginning.
+     *
+     * @example wavesurfer.stop();
+     */
+    stop() {
+        this.pause();
+        this.seekTo(0);
+        this.drawer.progress(0);
+    }
+
+    /**
+     * Sets the ID of the audio device to use for output and returns a Promise.
+     *
+     * @param {string} deviceId String value representing underlying output
+     * device
+     * @returns {Promise} `Promise` that resolves to `undefined` when there are
+     * no errors detected.
+     */
+    setSinkId(deviceId) {
+        return this.backend.setSinkId(deviceId);
+    }
+
+    /**
+     * Set the playback volume.
+     *
+     * @param {number} newVolume A value between 0 and 1, 0 being no
+     * volume and 1 being full volume.
+     * @emits WaveSurfer#volume
+     */
+    setVolume(newVolume) {
+        this.backend.setVolume(newVolume);
+        this.fireEvent('volume', newVolume);
+    }
+
+    /**
+     * Get the playback volume.
+     *
+     * @return {number} A value between 0 and 1, 0 being no
+     * volume and 1 being full volume.
+     */
+    getVolume() {
+        return this.backend.getVolume();
+    }
+
+    /**
+     * 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.
+     * @example wavesurfer.setPlaybackRate(2);
+     */
+    setPlaybackRate(rate) {
+        this.backend.setPlaybackRate(rate);
+    }
+
+    /**
+     * Get the playback rate.
+     *
+     * @return {number} The current playback rate.
+     */
+    getPlaybackRate() {
+        return this.backend.getPlaybackRate();
+    }
+
+    /**
+     * Toggle the volume on and off. If not currently 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.
+     *
+     * @example wavesurfer.toggleMute();
+     */
+    toggleMute() {
+        this.setMute(!this.isMuted);
+    }
+
+    /**
+     * Enable or disable muted audio
+     *
+     * @param {boolean} mute Specify `true` to mute audio.
+     * @emits WaveSurfer#volume
+     * @emits WaveSurfer#mute
+     * @example
+     * // unmute
+     * wavesurfer.setMute(false);
+     * console.log(wavesurfer.getMute()) // logs false
+     */
+    setMute(mute) {
+        // ignore all muting requests if the audio is already in that state
+        if (mute === this.isMuted) {
+            this.fireEvent('mute', this.isMuted);
+            return;
+        }
+
+        if (this.backend.setMute) {
+            // Backends such as the MediaElement backend have their own handling
+            // of mute, let them handle it.
+            this.backend.setMute(mute);
+            this.isMuted = mute;
+        } else {
+            if (mute) {
+                // 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;
+                this.fireEvent('volume', 0);
+            } else {
+                // If currently muted then restore to the saved volume
+                // and update the mute properties
+                this.backend.setVolume(this.savedVolume);
+                this.isMuted = false;
+                this.fireEvent('volume', this.savedVolume);
+            }
+        }
+        this.fireEvent('mute', this.isMuted);
+    }
+
+    /**
+     * Get the current mute status.
+     *
+     * @example const isMuted = wavesurfer.getMute();
+     * @return {boolean} Current mute status
+     */
+    getMute() {
+        return this.isMuted;
+    }
+
+    /**
+     * Get the list of current set filters as an array.
+     *
+     * Filters must be set with setFilters method first
+     *
+     * @return {array} List of enabled filters
+     */
+    getFilters() {
+        return this.backend.filters || [];
+    }
+
+    /**
+     * Toggles `scrollParent` and redraws
+     *
+     * @example wavesurfer.toggleScroll();
+     */
+    toggleScroll() {
+        this.params.scrollParent = !this.params.scrollParent;
+        this.drawBuffer();
+    }
+
+    /**
+     * Toggle mouse interaction
+     *
+     * @example wavesurfer.toggleInteraction();
+     */
+    toggleInteraction() {
+        this.params.interact = !this.params.interact;
+    }
+
+    /**
+     * Get the fill color of the waveform after the cursor.
+     *
+     * @return {string} A CSS color string.
+     */
+    getWaveColor() {
+        return this.params.waveColor;
+    }
+
+    /**
+     * Set the fill color of the waveform after the cursor.
+     *
+     * @param {string} color A CSS color string.
+     * @example wavesurfer.setWaveColor('#ddd');
+     */
+    setWaveColor(color) {
+        this.params.waveColor = color;
+        this.drawBuffer();
+    }
+
+    /**
+     * Get the fill color of the waveform behind the cursor.
+     *
+     * @return {string} A CSS color string.
+     */
+    getProgressColor() {
+        return this.params.progressColor;
+    }
+
+    /**
+     * Set the fill color of the waveform behind the cursor.
+     *
+     * @param {string} color A CSS color string.
+     * @example wavesurfer.setProgressColor('#400');
+     */
+    setProgressColor(color) {
+        this.params.progressColor = color;
+        this.drawBuffer();
+    }
+
+    /**
+     * Get the background color of the waveform container.
+     *
+     * @return {string} A CSS color string.
+     */
+    getBackgroundColor() {
+        return this.params.backgroundColor;
+    }
+
+    /**
+     * Set the background color of the waveform container.
+     *
+     * @param {string} color A CSS color string.
+     * @example wavesurfer.setBackgroundColor('#FF00FF');
+     */
+    setBackgroundColor(color) {
+        this.params.backgroundColor = color;
+        util.style(this.container, { background: this.params.backgroundColor });
+    }
+
+    /**
+     * Get the fill color of the cursor indicating the playhead
+     * position.
+     *
+     * @return {string} A CSS color string.
+     */
+    getCursorColor() {
+        return this.params.cursorColor;
+    }
+
+    /**
+     * Set the fill color of the cursor indicating the playhead
+     * position.
+     *
+     * @param {string} color A CSS color string.
+     * @example wavesurfer.setCursorColor('#222');
+     */
+    setCursorColor(color) {
+        this.params.cursorColor = color;
+        this.drawer.updateCursor();
+    }
+
+    /**
+     * Get the height of the waveform.
+     *
+     * @return {number} Height measured in pixels.
+     */
+    getHeight() {
+        return this.params.height;
+    }
+
+    /**
+     * Set the height of the waveform.
+     *
+     * @param {number} height Height measured in pixels.
+     * @example wavesurfer.setHeight(200);
+     */
+    setHeight(height) {
+        this.params.height = height;
+        this.drawer.setHeight(height * this.params.pixelRatio);
+        this.drawBuffer();
+    }
+
+    /**
+     * Hide channels from being drawn on the waveform if splitting channels.
+     *
+     * For example, if we want to draw only the peaks for the right stereo channel:
+     *
+     * const wavesurfer = new WaveSurfer.create({...splitChannels: true});
+     * wavesurfer.load('stereo_audio.mp3');
+     *
+     * wavesurfer.setFilteredChannel([0]); <-- hide left channel peaks.
+     *
+     * @param {array} channelIndices Channels to be filtered out from drawing.
+     * @version 4.0.0
+     */
+    setFilteredChannels(channelIndices) {
+        this.params.splitChannelsOptions.filterChannels = channelIndices;
+        this.drawBuffer();
+    }
+
+    /**
+     * Get the correct peaks for current wave view-port and render wave
+     *
+     * @private
+     * @emits WaveSurfer#redraw
+     */
+    drawBuffer() {
+        const nominalWidth = Math.round(
+            this.getDuration() *
+                this.params.minPxPerSec *
+                this.params.pixelRatio
+        );
+        const parentWidth = this.drawer.getWidth();
+        let width = nominalWidth;
+        // always start at 0 after zooming for scrolling : issue redraw left part
+        let start = 0;
+        let end = Math.max(start + parentWidth, width);
+        // Fill container
+        if (
+            this.params.fillParent &&
+            (!this.params.scrollParent || nominalWidth < parentWidth)
+        ) {
+            width = parentWidth;
+            start = 0;
+            end = width;
+        }
+
+        let peaks;
+        if (this.params.partialRender) {
+            const newRanges = this.peakCache.addRangeToPeakCache(
+                width,
+                start,
+                end
+            );
+            let i;
+            for (i = 0; i < newRanges.length; i++) {
+                peaks = this.backend.getPeaks(
+                    width,
+                    newRanges[i][0],
+                    newRanges[i][1]
+                );
+                this.drawer.drawPeaks(
+                    peaks,
+                    width,
+                    newRanges[i][0],
+                    newRanges[i][1]
+                );
+            }
+        } else {
+            peaks = this.backend.getPeaks(width, start, end);
+            this.drawer.drawPeaks(peaks, width, start, end);
+        }
+        this.fireEvent('redraw', peaks, width);
+    }
+
+    /**
+     * Horizontally zooms the waveform in and out. It also changes the parameter
+     * `minPxPerSec` and enables the `scrollParent` option. Calling the function
+     * with a falsey parameter will reset the zoom state.
+     *
+     * @param {?number} pxPerSec Number of horizontal pixels per second of
+     * audio, if none is set the waveform returns to unzoomed state
+     * @emits WaveSurfer#zoom
+     * @example wavesurfer.zoom(20);
+     */
+    zoom(pxPerSec) {
+        if (!pxPerSec) {
+            this.params.minPxPerSec = this.defaultParams.minPxPerSec;
+            this.params.scrollParent = false;
+        } else {
+            this.params.minPxPerSec = pxPerSec;
+            this.params.scrollParent = true;
+        }
+
+        this.drawBuffer();
+        this.drawer.progress(this.backend.getPlayedPercents());
+
+        this.drawer.recenter(this.getCurrentTime() / this.getDuration());
+        this.fireEvent('zoom', pxPerSec);
+    }
+
+    /**
+     * Decode buffer and load
+     *
+     * @private
+     * @param {ArrayBuffer} arraybuffer Buffer to process
+     */
+    loadArrayBuffer(arraybuffer) {
+        this.decodeArrayBuffer(arraybuffer, data => {
+            if (!this.isDestroyed) {
+                this.loadDecodedBuffer(data);
+            }
+        });
+    }
+
+    /**
+     * Directly load an externally decoded AudioBuffer
+     *
+     * @private
+     * @param {AudioBuffer} buffer Buffer to process
+     * @emits WaveSurfer#ready
+     */
+    loadDecodedBuffer(buffer) {
+        this.backend.load(buffer);
+        this.drawBuffer();
+        this.isReady = true;
+        this.fireEvent('ready');
+    }
+
+    /**
+     * Loads audio data from a Blob or File object
+     *
+     * @param {Blob|File} blob Audio data
+     * @example
+     */
+    loadBlob(blob) {
+        // Create file reader
+        const reader = new FileReader();
+        reader.addEventListener('progress', e => this.onProgress(e));
+        reader.addEventListener('load', e =>
+            this.loadArrayBuffer(e.target.result)
+        );
+        reader.addEventListener('error', () =>
+            this.fireEvent('error', 'Error reading file')
+        );
+        reader.readAsArrayBuffer(blob);
+        this.empty();
+    }
+
+    /**
+     * Loads audio and re-renders the waveform.
+     *
+     * @param {string|HTMLMediaElement} url The url of the audio file or the
+     * audio element with the audio
+     * @param {number[]|Number.<Array[]>} peaks Wavesurfer does not have to decode
+     * the audio to render the waveform if this is specified
+     * @param {?string} preload (Use with backend `MediaElement` and `MediaElementWebAudio`)
+     * `'none'|'metadata'|'auto'` Preload attribute for the media element
+     * @param {?number} duration The duration of the audio. This is used to
+     * render the peaks data in the correct size for the audio duration (as
+     * befits the current `minPxPerSec` and zoom value) without having to decode
+     * the audio.
+     * @returns {void}
+     * @throws Will throw an error if the `url` argument is empty.
+     * @example
+     * // uses fetch or media element to load file (depending on backend)
+     * wavesurfer.load('http://example.com/demo.wav');
+     *
+     * // setting preload attribute with media element backend and supplying
+     * // peaks
+     * wavesurfer.load(
+     *   'http://example.com/demo.wav',
+     *   [0.0218, 0.0183, 0.0165, 0.0198, 0.2137, 0.2888],
+     *   true
+     * );
+     */
+    load(url, peaks, preload, duration) {
+        if (!url) {
+            throw new Error('url parameter cannot be empty');
+        }
+        this.empty();
+        if (preload) {
+            // check whether the preload attribute will be usable and if not log
+            // a warning listing the reasons why not and nullify the variable
+            const preloadIgnoreReasons = {
+                "Preload is not 'auto', 'none' or 'metadata'":
+                    ['auto', 'metadata', 'none'].indexOf(preload) === -1,
+                'Peaks are not provided': !peaks,
+                "Backend is not of type 'MediaElement' or 'MediaElementWebAudio'":
+                    ['MediaElement', 'MediaElementWebAudio'].indexOf(
+                        this.params.backend
+                    ) === -1,
+                'Url is not of type string': typeof url !== 'string'
+            };
+            const activeReasons = Object.keys(preloadIgnoreReasons).filter(
+                reason => preloadIgnoreReasons[reason]
+            );
+            if (activeReasons.length) {
+                // eslint-disable-next-line no-console
+                console.warn(
+                    'Preload parameter of wavesurfer.load will be ignored because:\n\t- ' +
+                        activeReasons.join('\n\t- ')
+                );
+                // stop invalid values from being used
+                preload = null;
+            }
+        }
+
+        switch (this.params.backend) {
+            case 'WebAudio':
+                return this.loadBuffer(url, peaks, duration);
+            case 'MediaElement':
+            case 'MediaElementWebAudio':
+                return this.loadMediaElement(url, peaks, preload, duration);
+        }
+    }
+
+    /**
+     * Loads audio using Web Audio buffer backend.
+     *
+     * @private
+     * @param {string} url URL of audio file
+     * @param {number[]|Number.<Array[]>} peaks Peaks data
+     * @param {?number} duration Optional duration of audio file
+     * @returns {void}
+     */
+    loadBuffer(url, peaks, duration) {
+        const load = action => {
+            if (action) {
+                this.tmpEvents.push(this.once('ready', action));
+            }
+            return this.getArrayBuffer(url, data => this.loadArrayBuffer(data));
+        };
+
+        if (peaks) {
+            this.backend.setPeaks(peaks, duration);
+            this.drawBuffer();
+            this.tmpEvents.push(this.once('interaction', load));
+        } else {
+            return load();
+        }
+    }
+
+    /**
+     * Either create a media element, or load an existing media element.
+     *
+     * @private
+     * @param {string|HTMLMediaElement} urlOrElt Either a path to a media file, or an
+     * existing HTML5 Audio/Video Element
+     * @param {number[]|Number.<Array[]>} peaks Array of peaks. Required to bypass web audio
+     * dependency
+     * @param {?boolean} preload Set to true if the preload attribute of the
+     * audio element should be enabled
+     * @param {?number} duration Optional duration of audio file
+     */
+    loadMediaElement(urlOrElt, peaks, preload, duration) {
+        let url = urlOrElt;
+
+        if (typeof urlOrElt === 'string') {
+            this.backend.load(url, this.mediaContainer, peaks, preload);
+        } else {
+            const elt = urlOrElt;
+            this.backend.loadElt(elt, peaks);
+
+            // If peaks are not provided,
+            // url = element.src so we can get peaks with web audio
+            url = elt.src;
+        }
+
+        this.tmpEvents.push(
+            this.backend.once('canplay', () => {
+                // ignore when backend was already destroyed
+                if (!this.backend.destroyed) {
+                    this.drawBuffer();
+                    this.isReady = true;
+                    this.fireEvent('ready');
+                }
+            }),
+            this.backend.once('error', err => this.fireEvent('error', err))
+        );
+
+        // If no pre-decoded peaks provided or pre-decoded peaks are
+        // provided with forceDecode flag, attempt to download the
+        // audio file and decode it with Web Audio.
+        if (peaks) {
+            this.backend.setPeaks(peaks, duration);
+        }
+
+        if (
+            (!peaks || this.params.forceDecode) &&
+            this.backend.supportsWebAudio()
+        ) {
+            this.getArrayBuffer(url, arraybuffer => {
+                this.decodeArrayBuffer(arraybuffer, buffer => {
+                    this.backend.buffer = buffer;
+                    this.backend.setPeaks(null);
+                    this.drawBuffer();
+                    this.fireEvent('waveform-ready');
+                });
+            });
+        }
+    }
+
+    /**
+     * Decode an array buffer and pass data to a callback
+     *
+     * @private
+     * @param {Object} arraybuffer The array buffer to decode
+     * @param {function} callback The function to call on complete
+     */
+    decodeArrayBuffer(arraybuffer, callback) {
+        this.arraybuffer = arraybuffer;
+        this.backend.decodeArrayBuffer(
+            arraybuffer,
+            data => {
+                // Only use the decoded data if we haven't been destroyed or
+                // another decode started in the meantime
+                if (!this.isDestroyed && this.arraybuffer == arraybuffer) {
+                    callback(data);
+                    this.arraybuffer = null;
+                }
+            },
+            () => this.fireEvent('error', 'Error decoding audiobuffer')
+        );
+    }
+
+    /**
+     * Load an array buffer using fetch and pass the result to a callback
+     *
+     * @param {string} url The URL of the file object
+     * @param {function} callback The function to call on complete
+     * @returns {util.fetchFile} fetch call
+     * @private
+     */
+    getArrayBuffer(url, callback) {
+        let options = Object.assign(
+            {
+                url: url,
+                responseType: 'arraybuffer'
+            },
+            this.params.xhr
+        );
+        const request = util.fetchFile(options);
+
+        this.currentRequest = request;
+
+        this.tmpEvents.push(
+            request.on('progress', e => {
+                this.onProgress(e);
+            }),
+            request.on('success', data => {
+                callback(data);
+                this.currentRequest = null;
+            }),
+            request.on('error', e => {
+                this.fireEvent('error', e);
+                this.currentRequest = null;
+            })
+        );
+
+        return request;
+    }
+
+    /**
+     * Called while the audio file is loading
+     *
+     * @private
+     * @param {Event} e Progress event
+     * @emits WaveSurfer#loading
+     */
+    onProgress(e) {
+        let percentComplete;
+        if (e.lengthComputable) {
+            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.
+     *
+     * @param {number} length=1024 The scale in which to export the peaks
+     * @param {number} accuracy=10000
+     * @param {?boolean} noWindow Set to true to disable opening a new
+     * window with the JSON
+     * @param {number} start Start index
+     * @param {number} end End index
+     * @return {Promise} Promise that resolves with array of peaks
+     */
+    exportPCM(length, accuracy, noWindow, start, end) {
+        length = length || 1024;
+        start = start || 0;
+        accuracy = accuracy || 10000;
+        noWindow = noWindow || false;
+        const peaks = this.backend.getPeaks(length, start, end);
+        const arr = [].map.call(
+            peaks,
+            val => Math.round(val * accuracy) / accuracy
+        );
+        return new Promise((resolve, reject) => {
+            const json = JSON.stringify(arr);
+
+            if (!noWindow) {
+                window.open(
+                    'data:application/json;charset=utf-8,' +
+                        encodeURIComponent(json)
+                );
+            }
+            resolve(json);
+        });
+    }
+
+    /**
+     * Save waveform image as data URI.
+     *
+     * The default format is `'image/png'`. Other supported types are
+     * `'image/jpeg'` and `'image/webp'`.
+     *
+     * @param {string} format='image/png' A string indicating the image format.
+     * The default format type is `'image/png'`.
+     * @param {number} quality=1 A number between 0 and 1 indicating the image
+     * quality to use for image formats that use lossy compression such as
+     * `'image/jpeg'`` and `'image/webp'`.
+     * @param {string} type Image data type to return. Either 'dataURL' (default)
+     * or 'blob'.
+     * @return {string|string[]|Promise} When using `'dataURL'` type this returns
+     * a single data URL or an array of data URLs, one for each canvas. When using
+     * `'blob'` type this returns a `Promise` resolving with an array of `Blob`
+     * instances, one for each canvas.
+     */
+    exportImage(format, quality, type) {
+        if (!format) {
+            format = 'image/png';
+        }
+        if (!quality) {
+            quality = 1;
+        }
+        if (!type) {
+            type = 'dataURL';
+        }
+
+        return this.drawer.getImage(format, quality, type);
+    }
+
+    /**
+     * Cancel any fetch request currently in progress
+     */
+    cancelAjax() {
+        if (this.currentRequest && this.currentRequest.controller) {
+            this.currentRequest.controller.abort();
+            this.currentRequest = null;
+        }
+    }
+
+    /**
+     * @private
+     */
+    clearTmpEvents() {
+        this.tmpEvents.forEach(e => e.un());
+    }
+
+    /**
+     * Display empty waveform.
+     */
+    empty() {
+        if (!this.backend.isPaused()) {
+            this.stop();
+            this.backend.disconnectSource();
+        }
+        this.isReady = false;
+        this.cancelAjax();
+        this.clearTmpEvents();
+
+        // empty drawer
+        this.drawer.progress(0);
+        this.drawer.setWidth(0);
+        this.drawer.drawPeaks({ length: this.drawer.getWidth() }, 0);
+    }
+
+    /**
+     * Remove events, elements and disconnect WebAudio nodes.
+     *
+     * @emits WaveSurfer#destroy
+     */
+    destroy() {
+        this.destroyAllPlugins();
+        this.fireEvent('destroy');
+        this.cancelAjax();
+        this.clearTmpEvents();
+        this.unAll();
+        if (this.params.responsive !== false) {
+            window.removeEventListener('resize', this._onResize, true);
+            window.removeEventListener(
+                'orientationchange',
+                this._onResize,
+                true
+            );
+        }
+        if (this.backend) {
+            this.backend.destroy();
+        }
+        if (this.drawer) {
+            this.drawer.destroy();
+        }
+        this.isDestroyed = true;
+        this.isReady = false;
+        this.arraybuffer = null;
+    }
+}
