API Documentation for: 2.1.0
Show:

File:Audio.js

/**
*  @module cloudkid
*/
(function(global, doc, undefined){
	
	"use strict";
	
	// Imports
	var OS = cloudkid.OS,
		SwishSprite = cloudkid.SwishSprite,
		MediaLoader = cloudkid.MediaLoader;
	
	/**
	* Audio class is designed to play audio sprites in a cross-platform compatible manner using HTML5 and the SwishSprite library.
	* @class Audio
	*/
	var Audio = function(dataURLorObject, onReady)
	{
		this._onUpdate = this._onUpdate.bind(this);
		this._onComplete = this._onComplete.bind(this);
		this.initialize(dataURLorObject, onReady);
	},
	
	// Reference to the prototype 
	p = Audio.prototype,
	
	/** 
	* Metadata regarding Primary Audio Sprite (URLs, Audio Timings)
	* @property {Dictionary} _data
	* @private
	*/
	_data = null,
	
	/** 
	* If the Audio instance has been destroyed 
	* @property {Bool} _data 
	* @default false
	* @private
	*/
	_destroyed = false,
	
	/** 
	* Contains start + stop times for current sound 
	* @property {object} _currentData
	* @private
	*/
	_currentData = null,
	
	/**
	* The current alias of the sound playing
	* @property {String} _currentAlias
	* @private
	*/
	_currentAlias = null,
	
	/** 
	* Function to call when sound reaches end 
	* @property {Function} _onFinish
	* @private
	*/
	_onFinish = null,
	
	/** 
	* Update the function 
	* @property {Function} _onUpdate
	* @private
	*/
	_onUpdate = null,
	
	/** 
	* Set true only if paused by pause() used to determine validity of resume() 
	* @property {Bool} _paused
	* @default false
	* @private
	*/
	_paused = false,
	
	/** 
	* The current progress amount from 0 to 1 
	* @property {Number} _progress
	* @private
	*/
	_progress = 0,
	
	/** 
	* If the sounds are muted 
	* @property {Bool} _muted
	* @private
	*/
	_muted = false,
	
	/** 
	* The length of silence to play in seconds
	* @property {Number} _duration
	* @private
	*/
	_duration = 0,
	
	/** 
	* The postion we're currently on if muted 
	* @property {Number} _silencePosition
	* @private
	*/
	_silencePosition = 0,
	
	/** 
	* The silence update alias 
	* @property {String} _updateAlias
	* @private
	* @default AudioMute
	*/
	_updateAlias = 'AudioMute',
	
	/** 
	* The alias for the audiosprite update 
	* @property {String} _updateSpriteAlias
	* @private
	* @default SwishSprite
	*/
	_updateSpriteAlias = 'SwishSprite',
	
	/** 
	* Instance of the SwishSprite class 
	* @property {cloudkid.SwishSprite} _audioSprite
	* @private
	*/
	_audioSprite = null,
	
	/** 
	* Singleton instance of sound player 
	* @private
	* @property {Audio} _instance
	*/
	_instance = null,

	/** 
	* The currently playing (and thus valid) AudioInst object.
	* @property {AudioInst} _currentInst
	* @private
	*/
	_currentInst = null;

	/** 
	* If the sound file is loaded
	* @property {Bool} soundLoaded
	* @public
	*/
	p.soundLoaded = false;
	
	/** 
	* The global version of the library 
	* @static
	* @public
	* @property {String} VERSION
	*/
	Audio.VERSION = "${version}";
	
	/**
	* Static constructor initializing Audio (and soundManager)
	* @public
	* @static
	* @method init
	* @param {String|Object} dataURLorObject The optional sprite data url or sprite json object
	* @param {function} onReady function to call when Audio finished initializing
	*/
	Audio.init = function(dataURLorObject, onReady)
	{		
		if (!_instance)
		{
			new Audio(dataURLorObject, onReady);
		}
		return _instance;
	};
	
	/**
	*  Static function for getting the singleton instance
	*  @static
	*  @readOnly
	*  @public
	*  @property {Audio} instance
	*/
	Object.defineProperty(Audio, "instance", {
		get:function(){ return _instance; }
	});
	
	/**
	* Audio controller constructor
	* @constructor
	* @method initialize
	* @param {String|Object} dataURLorObject The optional sprite data url or sprite json object
	* @param {Function} onReady The callback function to call when finished initializing
	*/
	p.initialize = function(dataURLorObject, onReady)
	{
		if (_instance)
		{
			if (DEBUG)
			{
				Debug.warn("Audio is already initialized, use Audio.instance");
			}
			return;
		}
		
		_destroyed = false;
		_instance = this;
		
		// If the data is already an object, use that
		if (typeof dataURLorObject === "object")
		{
			if (DEBUG)
			{
				Debug.log("Load the JSON object directly");
			}
			validateData(dataURLorObject, onReady);	
		}
		else if (typeof dataURLorObject === "string")
		{
			if (DEBUG)
			{
				Debug.log("Load from the URL " + dataURLorObject);
			}
			
			// Load the JSON spritemap data
			MediaLoader.instance.load(
				dataURLorObject,
				function(result)
				{
					if (!result || !result.content)
					{
						if (DEBUG) Debug.error("Unable to load the audio sprite data from url '" + dataUrl + "'");
						onReady(false);
						return;
					}
					validateData(result.content, onReady);
				}
			);
		}
		else
		{
			if (DEBUG) Debug.error("Audio constructor data is not a URL or json object");
			onReady(false);
		}
	};
	
	/**
	* Validate that the sprite data is alright
	* @private
	* @method validateData
	* @param {object} data The audiosprite data
	* @param {Function} callback Method to call when we're completed
	*/
	var validateData = function(data, callback)
	{
		_data = data;
		
		var success = true;
		
		if (_data && _data.resources === undefined)
		{
			if (DEBUG) Debug.error("Sprite JSON must contain resources array");
			success = false;
		}
		
		if (_data && _data.spritemap === undefined)
		{
			if (DEBUG) Debug.error("Sprite JSON must contain spritemap dictionary");
			success = false;
		}
		callback(success);
	};
	
	/**
	* Check to make sure the audio is ready
	* @method isReady
	* @private
	* @param {String*} alias Optional alias to check for valid sprite sound
	* @return {Bool} If we can proceed with task
	*/
	var isReady = function(alias)
	{
		if (!_audioSprite) return false;
		
		if (alias !== undefined)
		{			
			if (!_data || !_data.spritemap)
			{
				if (DEBUG)
				{
					Debug.warn("Data must be setup and contain spritemap");
				}
				return false;
			}
			if (_data.spritemap[alias] === undefined)
			{
				if (DEBUG)
				{
					Debug.warn("Alias " + alias + " is not a valid sprite name");
				}
				return false;
			}
		}
		return true;
	};
	
	/**
	* Get the instance of the SwishSprite
	* @method getAudioSprite
	* @public
	* @return {cloudkid.SwishSprite}
	*/
	p.getAudioSprite = function()
	{
		return _audioSprite;
	};
	
	/**
	* Preload audio data for primary sprite, MUST be called by a click/touch event!!! 
	* @public 
	* @method load
	* @param {function} callback The callback function to call on load complete
	*/
	p.load = function(callback)
	{
		if (!_data) 
		{
			if (DEBUG) Debug.error("Must load sprite data first.");
			return;
		}
		var cacheManager = MediaLoader.instance.cacheManager,
			i, len = _data.resources.length, resource;
			
		// If there's a base path, prepend the url
		// also will take care of any versioning
		for(i = 0; i < len; i++)
		{
			resource = _data.resources[i];
			
			// Add the versioning/cache busting control to the resource URLs
			_data.resources[i] = cacheManager.prepare((resource.url !== undefined) ? resource.url : resource, true);
		}
		
		// Create the new audio sprite
		if(!_audioSprite)
		{
			_audioSprite = new SwishSprite(_data);
			_audioSprite.manualUpdate = true;
		}

		// Add listener for the Loaded event
		var self = this;
		_audioSprite.off(SwishSprite.LOADED);
		_audioSprite.on(SwishSprite.LOADED, function(){
			_audioSprite.off(SwishSprite.LOADED)
				.on(SwishSprite.PROGRESS, self._onUpdate)
				.on(SwishSprite.COMPLETE, self._onComplete);
			self.soundLoaded = true;
			callback();
		});
		
		// Add the manual update from the OS
		OS.instance.addUpdateCallback(
			_updateSpriteAlias, 
			_audioSprite.update
		);
		
		// User load
		_audioSprite.load();
	};
	
	/**
	* Goto the beginning of a sound
	* @public 
	* @method prepare
	* @param {String} alias The sound alias
	*/
	p.prepare = function(alias)
	{
		if (!isReady(alias)) return;
		_audioSprite.prepare(alias);
	};
	
	/** 
	* Returns true if a sound is currently being played
	* @public 
	* @method isPlaying
	* @return {Bool} If the audio is current playing
	*/
	p.isPlaying = function()
	{
		return !_paused;
	};
	
	/** 
	*  Used if we need to pause the current sound and resume later 
	*  @method pause
	*  @public
	*/
	p.pause = function()
	{
		if(!_paused && _audioSprite && _currentData)
		{
			if (_muted)
			{
				this._stopSilence();
			}
			else
			{
				_audioSprite.pause();
			}
			_paused = true;
		}
	};
	
	/** 
	*  Used to resume sound paused with pause(); 
	*  @method resume
	*  @public
	*/
	p.resume = function()
	{
		// make sure resume can only be activated once
		if(_paused && _audioSprite && _currentData)
		{
			// If we're mute we'll resume the silence
			if (_muted)
			{
				this._startSilence();
			}
			// Else resume the sound
			else
			{
				_audioSprite.resume();
			}
			_paused = false;
		}
	};
	
	/**
	*  Play sound from sprite by Alias
	*  @method play
	*  @public
	*  @param {String} alias Name of sound to play
	*  @param {Function} onFinish Function called when the sound is done
	*  @param {Function} onStart Function to be called when playback starts.
	*		This is called immediately, and is here to provide compatibility in usage with cloudkid.Sound.
	*  @param {Function} onUpdate Function to return the current progress amount 0 to 1
	*/
	p.play = function(alias, onFinish, onStart, onUpdate)
	{
		if (!isReady(alias)) return null;
		
		if(!_paused) this.stop();
		
		_currentAlias = alias;
		_currentData = _data.spritemap[alias];
		_onFinish = onFinish || null;
		_onUpdate = onUpdate || null;
		
		_paused = false;
		
		_progress = 0;
		_silencePosition = 0;
		
		// If we're muted we need to do a special timer 
		// to play silence for iOS because mute/volume on the
		// <audio> element is read-only
		if (_muted)
		{
			this._playSilence();
		}
		else
		{
			this._playAudio();
		}
		_currentInst = new AudioInst();
		_currentInst._end = _currentData.end * 1000;
		_currentInst._start = _currentData.start * 1000;
		_currentInst.length = _currentInst._end - _currentInst._start;
		
		if(typeof onStart == "function")
			setTimeout(onStart, 0);//call onStart ASAP after function returns the AudioInst
		
		return _currentInst;
	};
	
	/**
	* Start playing the silence when muted
	* @private
	* @method _playSilence
	*/
	p._playSilence = function()
	{
		// Get the duration of the sprite in milliseconds
		_duration = this.getLength(_currentAlias);
		
		// Get the current time in milliseconds
		_silencePosition = _audioSprite.getPosition();
		
		if (_onUpdate) _onUpdate(_progress);
		
		this._startSilence();
	};
	
	/**
	* Start the silence timer
	* @private
	* @method _startSilence
	*/
	p._startSilence = function()
	{
		OS.instance.addUpdateCallback(
			_updateAlias, 
			this._updateSilence.bind(this)
		);
	};
	
	/**
	* Stop the silence update
	* @private
	* @method _stopSilence
	*/
	p._stopSilence = function()
	{
		OS.instance.removeUpdateCallback(_updateAlias);
	};
	
	/**
	* Progress update for the silence playing
	* @private
	* @method _updateSilence
	* @param {Number} elapsed The number of ms elapsed since last update
	*/
	p._updateSilence = function(elapsed)
	{
		_silencePosition += (elapsed / 1000);
		_progress = _silencePosition / _duration;
		
		if (_silencePosition < _duration)
		{			
			if (_onUpdate) _onUpdate(Math.min(1, Math.max(0, _progress)));
		}
		// We're done
		else
		{
			this._onComplete();
		}
	};
	
	/**
	* Internal method to play the audio
	* @private
	* @method _playAudio
	*/
	p._playAudio = function()
	{	
		if (_onUpdate) _onUpdate(_progress);
		
		var position;
		
		// When unmuting from silence
		if (_silencePosition > 0)
		{
			position = _audioSprite.getSound(_currentAlias).start + _silencePosition;
		}
		_audioSprite.play(_currentAlias, position);
	};
	
	/**
	* Callback for the progress change update on the audio sprite
	* @private 
	* @method _onUpdate
	* @param {Number} p The progress from 0 to 1 of how much of the sprite we've completed
	*/
	p._onUpdate = function(p)
	{
		_progress = p;
		
		if (_onUpdate) _onUpdate(_progress);
	};
	
	/**
	* When either the sound or mute has finished
	* @private
	* @method _onComplete
	*/
	p._onComplete = function()
	{		
		if (!_currentData) return;
		
		if (_currentData.loop)
		{
			_progress = 0;
			_silencePosition = 0;
			
			if (_onFinish) _onFinish();
		}
		else
		{
			// Do a regular stop and do the callback
			this.stop(true);
		}
	};
	
	/** 
	*  Used if we need to stop playing a sound and we don't 
	*  need to resume from the current position 
	*  @public
	*  @method stop
	*  @param {Bool} doCallback If the callback should be called after stop
	*/
	p.stop = function(doCallback)
	{
		_progress = 0;
		_silencePosition = 0;
		_onUpdate = null;
		_currentAlias = null;
		_currentData = null;
		_paused = true;
		_duration = 0;
		if(_currentInst)
		{
			_currentInst.isValid = false;
			_currentInst = null;
		}
		
		// cancel the update if it's running
		this._stopSilence();
		
		var callback = _onFinish;
		_onFinish = null;
		
		if(_audioSprite)
			_audioSprite.stop();
		
		if(doCallback === undefined)
		{
			doCallback = false;
		}
		
		if (doCallback && callback !== null)
		{
			callback();
		}
	};
	
	/** 
	* Returns length in seconds of named sprite sound 
	* @method getLength
	* @public
	* @param {String} alias The sound alias
	* @return {Number} The number of a seconds duration of a sprite
	*/
	p.getLength = function(alias)
	{	
		if (_data && _data.spritemap[alias] !== undefined)
			return _data.spritemap[alias].end - _data.spritemap[alias].start;
		return 0;
	};
	
	/**
	* Set if the audio is muted
	* @public
	* @method mute
	*/
	p.mute = function()
	{
		if (!_muted)
		{
			_muted = true;
			if (_audioSprite && _currentData)
			{
				_audioSprite.pause();
				if (!_paused) this._playSilence();
			}
		}
	};
	
	/**
	* Set if the audio should turn off mute mode
	* @public
	* @method unmute
	*/
	p.unmute = function()
	{
		if (_muted)
		{
			_muted = false;
			if (_audioSprite && _currentData)
			{
				this._stopSilence();
				if (!_paused) this._playAudio();
			}
		}	
	};
	
	/**
	* Get the mute status of the audio
	* @method getMuted
	* @public
	* @return {Bool} If the audio is muted
	*/
	p.getMuted = function()
	{
		return _muted;
	};
	
	/** 
	* Returns value of loop property for named sound 
	* @public
	* @method isLooping
	* @param {Bool} alias If the alias is set to loop
	*/
	p.isLooping = function(alias)
	{
		if (!isReady(alias)) return;
		
		return _data.spritemap[alias].loop;
	};

	/** 
	* Returns if a sound alias is in the spritemap.
	* @public
	* @method hasAlias
	* @param {String} alias The sound alias to check for.
	* @return {Bool} true if the alias is in the spritemap, false otherwise.
	*/
	p.hasAlias = function(alias)
	{
		return _data ? !!_data.spritemap[alias] : false;
	};

	/** 
	* Returns array of sound aliases in spritemap
	* @public
	* @method getAliases
	* @param {Bool} includeSilence If array should include silence alias
	* @return {Array} sound aliases
	*/
	p.getAliases = function(includeSilence)
	{
		var key;
		var map = [];
		if(includeSilence)
		{
			for(key in _data.spritemap)
			{
				map.push(key);
			}
		}
		else
		{
			for(key in _data.spritemap)
			{
				if(key != "silence")
					map.push(key);
			}
		}
		return map;
	};
	
	/**
	* Don't use after this, destroys singleton and releases all references
	* @public
	* @method destroy
	*/
	p.destroy = function()
	{
		if(_destroyed) return;
		
		this.stop();
		
		if (_audioSprite)
		{
			// Remove the manual update
			OS.instance.removeUpdateCallback(_updateSpriteAlias);
			_audioSprite.destroy();
		}
		
		_instance = 
		_audioSprite =
		_data =
		_currentData =
		_currentAlias = 
		_onUpdate =
		_onFinish = null;
		
		_destroyed = true;
	};

	/**
	*  A playing instance of a sound. This class is primarily for compatability/standardization with CloudKidSound,
	*  and to make syncing animation with audio easier. These can only be created through cloudkid.Audio.instance.play().
	*  @class AudioInst
	*/
	var AudioInst = function()
	{
		/**
		*	If this AudioInst is still valid (still the actively playing audio bit).
		*	If this is false, then Audio is no longer playing this sound and this object should be discarded.
		*	@property {bool} isValid
		*	@public
		*/
		this.isValid = true;
		/**
		*	The start time of the sound in milliseconds.
		*	@property {Number} _start
		*	@private
		*/
		this._start = 0;
		/**
		*	The end time of the sound in milliseconds.
		*	@property {Number} _end
		*	@private
		*/
		this._end = 0;
		/**
		*	The length of the sound in milliseconds.
		*	@property {Number} length
		*	@public
		*/
		this.length = 0;
	};

	/**
	*	The position of the sound playhead in milliseconds, or 0 if the AudioInst is no longer valid.
	*	@property {Number} position
	*	@public
	*/
	Object.defineProperty(AudioInst.prototype, "position", {
		get: function() {
			return (this.isValid && _audioSprite) ? (_muted ? _silencePosition * 1000 : _audioSprite.getPosition() * 1000) : 0;
		}
	});

	/**
	*	Stops Audio, if this AudioInst is still valid.
	*	@method stop
	*	@public
	*/
	AudioInst.prototype.stop = function()
	{
		if(this.isValid)
		{
			_instance.stop();
		}
	};

	/**
	*	Pauses Audio, if this AudioInst is still valid.
	*	@method pause
	*	@public
	*/
	AudioInst.prototype.pause = function()
	{
		if(this.isValid)
		{
			_instance.pause();
		}
	};

	/**
	*	Resumes playing Audio, if this AudioInst is still valid.
	*	@method unpause
	*	@public
	*/
	AudioInst.prototype.unpause = function()
	{
		if(this.isValid)
		{
			_instance.resume();
		}
	};
	
	// Assign to the cloudkid namespace
	namespace('cloudkid').Audio = Audio;
	
}(window, document));