UNPKG

8.65 kBJavaScriptView Raw
1"use strict";
2
3window.AudioContext = window.AudioContext || window.webkitAudioContext;
4
5/**
6 * Loads sound files and lets you know when they're all available. An instance of SoundLoader is available as {@link Splat.Game#sounds}.
7 * This implementation uses the Web Audio API, and if that is not available it automatically falls back to the HTML5 <audio> tag.
8 * @constructor
9 */
10function SoundLoader(onLoad) {
11 /**
12 * The key-value object that stores named sounds.
13 * @member {object}
14 * @private
15 */
16 this.sounds = {};
17 /**
18 * The total number of sounds to be loaded.
19 * @member {number}
20 * @private
21 */
22 this.totalSounds = 0;
23 /**
24 * The number of sounds that have loaded completely.
25 * @member {number}
26 * @private
27 */
28 this.loadedSounds = 0;
29 /**
30 * A flag signifying if sounds have been muted through {@link SoundLoader#mute}.
31 * @member {boolean}
32 * @private
33 */
34 this.muted = false;
35 /**
36 * A key-value object that stores named looping sounds.
37 * @member {object}
38 * @private
39 */
40 this.looping = {};
41
42 /**
43 * The Web Audio API AudioContext
44 * @member {external:AudioContext}
45 * @private
46 */
47 this.context = new window.AudioContext();
48
49 this.gainNode = this.context.createGain();
50 this.gainNode.connect(this.context.destination);
51 this.volume = this.gainNode.gain.value;
52 this.onLoad = onLoad;
53}
54/**
55 * Load an audio file.
56 * @param {string} name The name you want to use when you {@link SoundLoader#play} the sound.
57 * @param {string} path The path of the sound file.
58 */
59SoundLoader.prototype.load = function(name, path) {
60 var self = this;
61
62 if (this.totalSounds === 0) {
63 // safari on iOS mutes sounds until they're played in response to user input
64 // play a dummy sound on first touch
65 var firstTouchHandler = function() {
66 window.removeEventListener("click", firstTouchHandler);
67 window.removeEventListener("keydown", firstTouchHandler);
68 window.removeEventListener("touchstart", firstTouchHandler);
69
70 var source = self.context.createOscillator();
71 source.connect(self.gainNode);
72 source.start(0);
73 source.stop(0);
74
75 if (self.firstPlay) {
76 self.play(self.firstPlay, self.firstPlayLoop);
77 } else {
78 self.firstPlay = "workaround";
79 }
80 };
81 window.addEventListener("click", firstTouchHandler);
82 window.addEventListener("keydown", firstTouchHandler);
83 window.addEventListener("touchstart", firstTouchHandler);
84 }
85
86 this.totalSounds++;
87
88 var request = new XMLHttpRequest();
89 request.open("GET", path, true);
90 request.responseType = "arraybuffer";
91 request.addEventListener("readystatechange", function() {
92 if (request.readyState !== 4) {
93 return;
94 }
95 if (request.status !== 200 && request.status !== 0) {
96 console.error("Error loading sound " + path);
97 return;
98 }
99 self.context.decodeAudioData(request.response, function(buffer) {
100 self.sounds[name] = buffer;
101 self.loadedSounds++;
102 if (self.allLoaded() && self.onLoad) {
103 self.onLoad();
104 }
105 }, function(err) {
106 console.error("Error decoding audio data for " + path + ": " + err);
107 });
108 });
109 request.addEventListener("error", function() {
110 console.error("Error loading sound " + path);
111 });
112 try {
113 request.send();
114 } catch (e) {
115 console.error("Error loading sound", path, e);
116 }
117};
118SoundLoader.prototype.loadFromManifest = function(manifest) {
119 var keys = Object.keys(manifest);
120 var self = this;
121 keys.forEach(function(key) {
122 self.load(key, manifest[key]);
123 });
124};
125/**
126 * Test if all sounds have loaded.
127 * @returns {boolean}
128 */
129SoundLoader.prototype.allLoaded = function() {
130 return this.totalSounds === this.loadedSounds;
131};
132/**
133 * Play a sound.
134 * @param {string} name The name given to the sound during {@link SoundLoader#load}
135 * @param {Object} [loop=undefined] A hash containing loopStart and loopEnd options. To stop a looped sound use {@link SoundLoader#stop}.
136 */
137SoundLoader.prototype.play = function(name, loop) {
138 if (loop && this.looping[name]) {
139 return;
140 }
141 if (!this.firstPlay) {
142 // let the iOS user input workaround handle it
143 this.firstPlay = name;
144 this.firstPlayLoop = loop;
145 return;
146 }
147 var snd = this.sounds[name];
148 if (snd === undefined) {
149 console.error("Unknown sound: " + name);
150 }
151 var source = this.context.createBufferSource();
152 source.buffer = snd;
153 source.connect(this.gainNode);
154 if (loop) {
155 source.loop = true;
156 source.loopStart = loop.loopStart || 0;
157 source.loopEnd = loop.loopEnd || 0;
158 this.looping[name] = source;
159 }
160 source.start(0);
161};
162/**
163 * 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.
164 * @param {string} name The name given to the sound during {@link SoundLoader#load}
165 */
166SoundLoader.prototype.stop = function(name) {
167 if (!this.looping[name]) {
168 return;
169 }
170 this.looping[name].stop(0);
171 delete this.looping[name];
172};
173/**
174 * Silence all sounds. Sounds keep playing, but at zero volume. Call {@link SoundLoader#unmute} to restore the previous volume level.
175 */
176SoundLoader.prototype.mute = function() {
177 this.gainNode.gain.value = 0;
178 this.muted = true;
179};
180/**
181 * Restore volume to whatever value it was before {@link SoundLoader#mute} was called.
182 */
183SoundLoader.prototype.unmute = function() {
184 this.gainNode.gain.value = this.volume;
185 this.muted = false;
186};
187/**
188 * Set the volume of all sounds.
189 * @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.
190 */
191SoundLoader.prototype.setVolume = function(gain) {
192 this.volume = gain;
193 this.gainNode.gain = gain;
194 this.muted = false;
195};
196/**
197 * Test if the volume is currently muted.
198 * @return {boolean} True if the volume is currently muted.
199 */
200SoundLoader.prototype.isMuted = function() {
201 return this.muted;
202};
203
204function AudioTagSoundLoader(onLoad) {
205 this.sounds = {};
206 this.totalSounds = 0;
207 this.loadedSounds = 0;
208 this.muted = false;
209 this.looping = {};
210 this.volume = new Audio().volume;
211 this.onLoad = onLoad;
212}
213AudioTagSoundLoader.prototype.load = function(name, path) {
214 this.totalSounds++;
215
216 var audio = new Audio();
217 var self = this;
218 audio.addEventListener("error", function() {
219 console.error("Error loading sound " + path);
220 });
221 audio.addEventListener("canplaythrough", function() {
222 self.sounds[name] = audio;
223 self.loadedSounds++;
224 if (self.allLoaded() && self.onLoad) {
225 self.onLoad();
226 }
227 });
228 audio.volume = this.volume;
229 audio.src = path;
230 audio.load();
231};
232AudioTagSoundLoader.prototype.loadFromManifest = function(manifest) {
233 var keys = Object.keys(manifest);
234 var self = this;
235 keys.forEach(function(key) {
236 self.load(key, manifest[key]);
237 });
238};
239AudioTagSoundLoader.prototype.allLoaded = function() {
240 return this.totalSounds === this.loadedSounds;
241};
242AudioTagSoundLoader.prototype.play = function(name, loop) {
243 if (loop && this.looping[name]) {
244 return;
245 }
246 var snd = this.sounds[name];
247 if (snd === undefined) {
248 console.error("Unknown sound: " + name);
249 }
250 if (loop) {
251 snd.loop = true;
252 this.looping[name] = snd;
253 }
254 snd.play();
255};
256AudioTagSoundLoader.prototype.stop = function(name) {
257 var snd = this.looping[name];
258 if (!snd) {
259 return;
260 }
261 snd.loop = false;
262 snd.pause();
263 snd.currentTime = 0;
264 delete this.looping[name];
265};
266function setAudioTagVolume(sounds, gain) {
267 for (var name in sounds) {
268 if (sounds.hasOwnProperty(name)) {
269 sounds[name].volume = gain;
270 }
271 }
272}
273AudioTagSoundLoader.prototype.mute = function() {
274 setAudioTagVolume(this.sounds, 0);
275 this.muted = true;
276};
277AudioTagSoundLoader.prototype.unmute = function() {
278 setAudioTagVolume(this.sounds, this.volume);
279 this.muted = false;
280};
281AudioTagSoundLoader.prototype.setVolume = function(gain) {
282 this.volume = gain;
283 setAudioTagVolume(this.sounds, gain);
284 this.muted = false;
285};
286AudioTagSoundLoader.prototype.isMuted = function() {
287 return this.muted;
288};
289
290
291function FakeSoundLoader(onLoad) {
292 this.onLoad = onLoad;
293}
294FakeSoundLoader.prototype.load = function() {
295 if (this.onLoad) {
296 this.onLoad();
297 }
298};
299FakeSoundLoader.prototype.loadFromManifest = function() {};
300FakeSoundLoader.prototype.allLoaded = function() { return true; };
301FakeSoundLoader.prototype.play = function() {};
302FakeSoundLoader.prototype.stop = function() {};
303FakeSoundLoader.prototype.mute = function() {};
304FakeSoundLoader.prototype.unmute = function() {};
305FakeSoundLoader.prototype.setVolume = function() {};
306FakeSoundLoader.prototype.isMuted = function() {
307 return true;
308};
309
310if (window.AudioContext) {
311 module.exports = SoundLoader;
312} else if (window.Audio) {
313 module.exports = AudioTagSoundLoader;
314} else {
315 console.log("This browser doesn't support the Web Audio API or the HTML5 audio tag.");
316 module.exports = FakeSoundLoader;
317}