Source: audio/audio.js

goog.provide('lime.audio.Audio');

goog.require('goog.events');
goog.require('goog.events.EventTarget');
goog.require('lime.userAgent');

/**
 * Audio stream object
 * @constructor
 * @param {string} filePath Path to audio file.
 */
lime.audio.Audio = function(filePath) {
    goog.events.EventTarget.call(this);

    if (filePath && goog.isFunction(filePath.data)) {
        filePath = filePath.data();
    }

    /**
     * @type bBoolean}
     * @private
     */
    this.loaded_ = false;

    /**
     * @type {boolean}
     * @private
     */
    this.playing_ = false;

    if (goog.userAgent.GECKO && (/\.mp3$/).test(filePath)) {
        filePath = filePath.replace(/\.mp3$/, '.ogg');
    }

    if (lime.audio.AudioContext) {
        this.volume_ = 1;
        this.prepareContext_();
        this.loadBuffer(filePath, goog.bind(this.bufferLoadedHandler_, this));
    } else {
        /**
         * Internal audio element
         * @type {audio}
         */
        this.baseElement = document.createElement('audio');
        this.baseElement['preload'] = true;
        this.baseElement['loop'] = false;
        this.baseElement.src = filePath;
        this.baseElement.load();
        this.baseElement.addEventListener('ended', goog.bind(this.onEnded_, this));
        this.loadInterval = setInterval(goog.bind(this.loadHandler_, this), 10);

        this.loaded_ = false;
    }
};
goog.inherits(lime.audio.Audio, goog.events.EventTarget);

lime.audio.AudioContext = goog.global['AudioContext'] || goog.global['webkitAudioContext'];
lime.audio._buffers = {};

lime.audio.supportsMultiChannel = lime.audio.AudioContext || !(lime.userAgent.IOS || lime.userAgent.WINPHONE);

lime.audio.Audio.prototype.prepareContext_ = function() {
    if (lime.audio.context) return;
    var context = lime.audio.context = new lime.audio.AudioContext();
    var gain = lime.audio.masterGain = context['createGainNode']();
    gain['connect'](context['destination']);
};

lime.audio.Audio.prototype.loadBuffer = function(path, cb) {
    var buffers = lime.audio._buffers;
    if (buffers[path] && buffers[path].buffer) {
        cb(buffers[path].buffer, path);
    } else if (buffers[path]) {
        buffers[path].push(cb);
    } else {
        buffers[path] = [cb];
        var req = new XMLHttpRequest();
        req.open('GET', path, true);
        req.responseType = 'arraybuffer';
        req.onload = function() {
            lime.audio.context['decodeAudioData'](req.response, function(buffer) {
                if (!buffer) {
                    return console.error('Error decoding file:', path);
                }
                var cbArray = buffers[path];
                buffers[path] = {
                    buffer: buffer
                };
                for (var i = 0; i < cbArray.length; i++) {
                    cbArray[i](buffer, path);
                }
            }, function(e) {
                console.error('Error decoding file', e);
            });
        };
        req.onerror = function() {
            console.error('XHR error loading file:', path);
        };
        req.send();
    }
};

lime.audio.Audio.prototype.bufferLoadedHandler_ = function(buffer, path) {
    this.buffer = buffer;
    this.loaded_ = true;
    var ev = new goog.events.Event('loaded');
    ev.event = null;
    this.dispatchEvent(ev);
    if (this.autoplay_) {
        this.play.apply(this, this.autoplay_);
    }
};

lime.audio.Audio.prototype.onEnded_ = function(e) {
    this.playing_ = false;
    var ev = new goog.events.Event('ended');
    ev.event = e;
    this.dispatchEvent(ev);
    this.playPosition_ = 0;
    var delay = lime.audio.AudioContext ? this.playTime_ + this.buffer.duration - this.playPositionCache - 0.05 : 0;
    if (this.next_) {
        for (var i = 0; i < this.next_.length; i++) {
            this.next_[i][0].play(this.next_[i][1], delay);
        }
    } else if (ev.returnValue_ !== false && this.loop_) {
        this.play(this.loop_, delay);
    }
}

/**
 * Handle loading the audio file. Event handlers seem to fail
 * on lot of browsers.
 * @private
 */
lime.audio.Audio.prototype.loadHandler_ = function() {
    if (this.baseElement['readyState'] > 2) {
        this.bufferLoadedHandler_();
        clearTimeout(this.loadInterval);
    }
    if (this.baseElement['error']) clearTimeout(this.loadInterval);

    if (lime.userAgent.IOS && this.baseElement['readyState'] == 0) {
        //ios hack do not work any more after 4.2.1 updates
        // no good solutions that i know
        this.bufferLoadedHandler_();
        clearTimeout(this.loadInterval);
        // this means that ios audio anly works if called from user action
    }
};

/**
 * Returns true if audio file has been loaded
 * @return {boolean} Audio has been loaded.
 */
lime.audio.Audio.prototype.isLoaded = function() {
    return this.loaded_;
};

/**
 * Returns true if audio file is playing
 * @return {boolean} Audio is playing.
 */
lime.audio.Audio.prototype.isPlaying = function() {
    return this.playing_;
};

/**
 * Start playing the audio
 * @param {number=} opt_loop Loop the sound.
 */
lime.audio.Audio.prototype.play = function(opt_loop) {
    if (!this.isLoaded()) {
        this.autoplay_ = goog.array.toArray(arguments);
    }
    if (this.isLoaded() && !this.isPlaying() && !lime.audio.getMute()) {
        if (lime.audio.AudioContext) {
            if (this.source && this.source['playbackState'] == this.source['FINISHED_STATE']) {
                this.playPosition_ = 0;
            }
            this.source = lime.audio.context['createBufferSource']();
            this.source.buffer = this.buffer;
            this.gain = lime.audio.context['createGainNode']();
            this.gain['connect'](lime.audio.masterGain);
            this.gain['gain']['value'] = this.volume_;
            this.source['connect'](this.gain);

            this.playTime_ = lime.audio.context['currentTime'];
            var delay = arguments[1] || 0

            if (this.playPosition_ > 0) {
                this.source['noteGrainOn'](delay, this.playPosition_, this.buffer.duration - this.playPosition_);
            } else {
                this.source['noteOn'](delay);
            }
            this.playPositionCache = this.playPosition_;
            this.endTimeout_ = setTimeout(goog.bind(this.onEnded_, this), (this.buffer.duration - (this.playPosition_ || 0)) * 1000 - 150);
        } else {
            this.baseElement.play();
        }
        this.playing_ = true;
        this.loop_ = !! opt_loop;
        if (lime.audio._playQueue.indexOf(this) == -1) {
            lime.audio._playQueue.push(this);
        }
    }
};

/**
 * Stop playing the audio
 */
lime.audio.Audio.prototype.stop = function() {
    if (!this.isLoaded()) {
        this.autoplay_ = null;
    }
    if (this.isPlaying()) {
        if (lime.audio.AudioContext) {
            clearTimeout(this.endTimeout_);
            this.playPosition_ = lime.audio.context.currentTime - this.playTime_ + (this.playPosition_ || 0);
            if (this.playPosition_ > this.buffer.duration) {
                this.playPosition_ = 0;
            }
            this.source['noteOff'](0);
            this.gain['disconnect'](lime.audio.masterGain);
            this.source = null;
        } else {
            this.baseElement.pause();
        }
        this.playing_ = false;
    }
};

lime.audio._isMute = false;
lime.audio._playQueue = [];

lime.audio.getMute = function() {
    return lime.audio._isMute;
};

lime.audio.setMute = function(bool) {
    if (bool && !lime.audio._isMute) {
        for (var i = 0; i < lime.audio._playQueue.length; i++) {
            lime.audio._playQueue[i].stop();
        }
        lime.audio._playQueue = [];
    }
    lime.audio._isMute = bool;
};

lime.audio.Audio.prototype.setVolume = function(value) {
    if (lime.audio.AudioContext) {
        this.volume_ = value;
        if (this.gain) this.gain['gain']['value'] = value;
    } else {
        this.baseElement.volume = value;
    }
};
lime.audio.Audio.prototype.getVolume = function() {
    if (lime.audio.AudioContext) {
        return this.volume_;
    } else {
        return this.baseElement.volume;
    }
};