"use strict";
window.AudioContext = window.AudioContext || window.webkitAudioContext;
/**
* Loads sound files and lets you know when they're all available. An instance of SoundLoader is available as {@link Splat.Game#sounds}.
* This implementation uses the Web Audio API, and if that is not available it automatically falls back to the HTML5 <audio> tag.
* @constructor
*/
function SoundLoader() {
/**
* The key-value object that stores named sounds.
* @member {object}
* @private
*/
this.sounds = {};
/**
* The total number of sounds to be loaded.
* @member {number}
* @private
*/
this.totalSounds = 0;
/**
* The number of sounds that have loaded completely.
* @member {number}
* @private
*/
this.loadedSounds = 0;
/**
* A flag signifying if sounds have been muted through {@link SoundLoader#mute}.
* @member {boolean}
* @private
*/
this.muted = false;
/**
* A key-value object that stores named looping sounds.
* @member {object}
* @private
*/
this.looping = {};
/**
* The Web Audio API AudioContext
* @member {external:AudioContext}
* @private
*/
this.context = new window.AudioContext();
this.gainNode = this.context.createGain();
this.gainNode.connect(this.context.destination);
this.volume = this.gainNode.gain.value;
}
/**
* Load an audio file.
* @param {string} name The name you want to use when you {@link SoundLoader#play} the sound.
* @param {string} path The path of the sound file.
*/
SoundLoader.prototype.load = function(name, path) {
var self = this;
if (this.totalSounds === 0) {
// safari on iOS mutes sounds until they're played in response to user input
// play a dummy sound on first touch
var firstTouchHandler = function() {
window.removeEventListener("click", firstTouchHandler);
window.removeEventListener("keydown", firstTouchHandler);
window.removeEventListener("touchstart", firstTouchHandler);
var source = self.context.createOscillator();
source.connect(self.gainNode);
source.start(0);
source.stop(0);
if (self.firstPlay) {
self.play(self.firstPlay, self.firstPlayLoop);
} else {
self.firstPlay = "workaround";
}
};
window.addEventListener("click", firstTouchHandler);
window.addEventListener("keydown", firstTouchHandler);
window.addEventListener("touchstart", firstTouchHandler);
}
this.totalSounds++;
var request = new XMLHttpRequest();
request.open("GET", path, true);
request.responseType = "arraybuffer";
request.addEventListener("readystatechange", function() {
if (request.readyState !== 4) {
return;
}
if (request.status !== 200 && request.status !== 0) {
console.error("Error loading sound " + path);
return;
}
self.context.decodeAudioData(request.response, function(buffer) {
self.sounds[name] = buffer;
self.loadedSounds++;
}, function(err) {
console.error("Error decoding audio data for " + path + ": " + err);
});
});
request.addEventListener("error", function() {
console.error("Error loading sound " + path);
});
try {
request.send();
} catch (e) {
console.error("Error loading sound", path, e);
}
};
/**
* Test if all sounds have loaded.
* @returns {boolean}
*/
SoundLoader.prototype.allLoaded = function() {
return this.totalSounds === this.loadedSounds;
};
/**
* Play a sound.
* @param {string} name The name given to the sound during {@link SoundLoader#load}
* @param {boolean} [loop=false] A flag denoting whether the sound should be looped. To stop a looped sound use {@link SoundLoader#stop}.
*/
SoundLoader.prototype.play = function(name, loop) {
if (loop && this.looping[name]) {
return;
}
if (!this.firstPlay) {
// let the iOS user input workaround handle it
this.firstPlay = name;
this.firstPlayLoop = loop;
return;
}
var snd = this.sounds[name];
if (snd === undefined) {
console.error("Unknown sound: " + name);
}
var source = this.context.createBufferSource();
source.buffer = snd;
source.connect(this.gainNode);
if (loop) {
source.loop = true;
this.looping[name] = source;
}
source.start(0);
};
/**
* Stop playing a sound. This currently only stops playing a sound that was looped earlier, and doesn't stop a sound mid-play. Patches welcome.
* @param {string} name The name given to the sound during {@link SoundLoader#load}
*/
SoundLoader.prototype.stop = function(name) {
if (!this.looping[name]) {
return;
}
this.looping[name].stop(0);
delete this.looping[name];
};
/**
* Silence all sounds. Sounds keep playing, but at zero volume. Call {@link SoundLoader#unmute} to restore the previous volume level.
*/
SoundLoader.prototype.mute = function() {
this.gainNode.gain.value = 0;
this.muted = true;
};
/**
* Restore volume to whatever value it was before {@link SoundLoader#mute} was called.
*/
SoundLoader.prototype.unmute = function() {
this.gainNode.gain.value = this.volume;
this.muted = false;
};
/**
* Set the volume of all sounds.
* @param {number} gain The desired volume level. A number between 0.0 and 1.0, with 0.0 being silent, and 1.0 being maximum volume.
*/
SoundLoader.prototype.setVolume = function(gain) {
this.volume = gain;
this.gainNode.gain = gain;
this.muted = false;
};
/**
* Test if the volume is currently muted.
* @return {boolean} True if the volume is currently muted.
*/
SoundLoader.prototype.isMuted = function() {
return this.muted;
};
function AudioTagSoundLoader() {
this.sounds = {};
this.totalSounds = 0;
this.loadedSounds = 0;
this.muted = false;
this.looping = {};
this.volume = new Audio().volume;
}
AudioTagSoundLoader.prototype.load = function(name, path) {
this.totalSounds++;
var audio = new Audio();
var self = this;
audio.addEventListener("error", function() {
console.error("Error loading sound " + path);
});
audio.addEventListener("canplaythrough", function() {
self.sounds[name] = audio;
self.loadedSounds++;
});
audio.volume = this.volume;
audio.src = path;
audio.load();
};
AudioTagSoundLoader.prototype.allLoaded = function() {
return this.totalSounds === this.loadedSounds;
};
AudioTagSoundLoader.prototype.play = function(name, loop) {
if (loop && this.looping[name]) {
return;
}
var snd = this.sounds[name];
if (snd === undefined) {
console.error("Unknown sound: " + name);
}
if (loop) {
snd.loop = true;
this.looping[name] = snd;
}
snd.play();
};
AudioTagSoundLoader.prototype.stop = function(name) {
var snd = this.looping[name];
if (!snd) {
return;
}
snd.loop = false;
snd.pause();
snd.currentTime = 0;
delete this.looping[name];
};
function setAudioTagVolume(sounds, gain) {
for (var name in sounds) {
if (sounds.hasOwnProperty(name)) {
sounds[name].volume = gain;
}
}
}
AudioTagSoundLoader.prototype.mute = function() {
setAudioTagVolume(this.sounds, 0);
this.muted = true;
};
AudioTagSoundLoader.prototype.unmute = function() {
setAudioTagVolume(this.sounds, this.volume);
this.muted = false;
};
AudioTagSoundLoader.prototype.setVolume = function(gain) {
this.volume = gain;
setAudioTagVolume(this.sounds, gain);
this.muted = false;
};
AudioTagSoundLoader.prototype.isMuted = function() {
return this.muted;
};
function FakeSoundLoader() {}
FakeSoundLoader.prototype.load = function() {};
FakeSoundLoader.prototype.allLoaded = function() { return true; };
FakeSoundLoader.prototype.play = function() {};
FakeSoundLoader.prototype.stop = function() {};
FakeSoundLoader.prototype.mute = function() {};
FakeSoundLoader.prototype.unmute = function() {};
FakeSoundLoader.prototype.setVolume = function() {};
FakeSoundLoader.prototype.isMuted = function() {
return true;
};
if (window.AudioContext) {
module.exports = SoundLoader;
} else if (window.Audio) {
module.exports = AudioTagSoundLoader;
} else {
console.log("This browser doesn't support the Web Audio API or the HTML5 audio tag.");
module.exports = FakeSoundLoader;
}