UNPKG

7.88 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() {
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}
53/**
54 * Load an audio file.
55 * @param {string} name The name you want to use when you {@link SoundLoader#play} the sound.
56 * @param {string} path The path of the sound file.
57 */
58SoundLoader.prototype.load = function(name, path) {
59 var self = this;
60
61 if (this.totalSounds === 0) {
62 // safari on iOS mutes sounds until they're played in response to user input
63 // play a dummy sound on first touch
64 var firstTouchHandler = function() {
65 window.removeEventListener("click", firstTouchHandler);
66 window.removeEventListener("keydown", firstTouchHandler);
67 window.removeEventListener("touchstart", firstTouchHandler);
68
69 var source = self.context.createOscillator();
70 source.connect(self.gainNode);
71 source.start(0);
72 source.stop(0);
73
74 if (self.firstPlay) {
75 self.play(self.firstPlay, self.firstPlayLoop);
76 } else {
77 self.firstPlay = "workaround";
78 }
79 };
80 window.addEventListener("click", firstTouchHandler);
81 window.addEventListener("keydown", firstTouchHandler);
82 window.addEventListener("touchstart", firstTouchHandler);
83 }
84
85 this.totalSounds++;
86
87 var request = new XMLHttpRequest();
88 request.open("GET", path, true);
89 request.responseType = "arraybuffer";
90 request.addEventListener("readystatechange", function() {
91 if (request.readyState !== 4) {
92 return;
93 }
94 if (request.status !== 200 && request.status !== 0) {
95 console.error("Error loading sound " + path);
96 return;
97 }
98 self.context.decodeAudioData(request.response, function(buffer) {
99 self.sounds[name] = buffer;
100 self.loadedSounds++;
101 }, function(err) {
102 console.error("Error decoding audio data for " + path + ": " + err);
103 });
104 });
105 request.addEventListener("error", function() {
106 console.error("Error loading sound " + path);
107 });
108 try {
109 request.send();
110 } catch (e) {
111 console.error("Error loading sound", path, e);
112 }
113};
114/**
115 * Test if all sounds have loaded.
116 * @returns {boolean}
117 */
118SoundLoader.prototype.allLoaded = function() {
119 return this.totalSounds === this.loadedSounds;
120};
121/**
122 * Play a sound.
123 * @param {string} name The name given to the sound during {@link SoundLoader#load}
124 * @param {boolean} [loop=false] A flag denoting whether the sound should be looped. To stop a looped sound use {@link SoundLoader#stop}.
125 */
126SoundLoader.prototype.play = function(name, loop) {
127 if (loop && this.looping[name]) {
128 return;
129 }
130 if (!this.firstPlay) {
131 // let the iOS user input workaround handle it
132 this.firstPlay = name;
133 this.firstPlayLoop = loop;
134 return;
135 }
136 var snd = this.sounds[name];
137 if (snd === undefined) {
138 console.error("Unknown sound: " + name);
139 }
140 var source = this.context.createBufferSource();
141 source.buffer = snd;
142 source.connect(this.gainNode);
143 if (loop) {
144 source.loop = true;
145 this.looping[name] = source;
146 }
147 source.start(0);
148};
149/**
150 * 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.
151 * @param {string} name The name given to the sound during {@link SoundLoader#load}
152 */
153SoundLoader.prototype.stop = function(name) {
154 if (!this.looping[name]) {
155 return;
156 }
157 this.looping[name].stop();
158 delete this.looping[name];
159};
160/**
161 * Silence all sounds. Sounds keep playing, but at zero volume. Call {@link SoundLoader#unmute} to restore the previous volume level.
162 */
163SoundLoader.prototype.mute = function() {
164 this.gainNode.gain.value = 0;
165 this.muted = true;
166};
167/**
168 * Restore volume to whatever value it was before {@link SoundLoader#mute} was called.
169 */
170SoundLoader.prototype.unmute = function() {
171 this.gainNode.gain.value = this.volume;
172 this.muted = false;
173};
174/**
175 * Set the volume of all sounds.
176 * @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.
177 */
178SoundLoader.prototype.setVolume = function(gain) {
179 this.volume = gain;
180 this.gainNode.gain = gain;
181 this.muted = false;
182};
183/**
184 * Test if the volume is currently muted.
185 * @return {boolean} True if the volume is currently muted.
186 */
187SoundLoader.prototype.isMuted = function() {
188 return this.muted;
189};
190
191function AudioTagSoundLoader() {
192 this.sounds = {};
193 this.totalSounds = 0;
194 this.loadedSounds = 0;
195 this.muted = false;
196 this.looping = {};
197 this.volume = new Audio().volume;
198}
199AudioTagSoundLoader.prototype.load = function(name, path) {
200 this.totalSounds++;
201
202 var audio = new Audio();
203 var self = this;
204 audio.addEventListener("error", function() {
205 console.error("Error loading sound " + path);
206 });
207 audio.addEventListener("canplaythrough", function() {
208 self.sounds[name] = audio;
209 self.loadedSounds++;
210 });
211 audio.volume = this.volume;
212 audio.src = path;
213 audio.load();
214};
215AudioTagSoundLoader.prototype.allLoaded = function() {
216 return this.totalSounds === this.loadedSounds;
217};
218AudioTagSoundLoader.prototype.play = function(name, loop) {
219 if (loop && this.looping[name]) {
220 return;
221 }
222 var snd = this.sounds[name];
223 if (snd === undefined) {
224 console.error("Unknown sound: " + name);
225 }
226 if (loop) {
227 snd.loop = true;
228 this.looping[name] = snd;
229 }
230 snd.play();
231};
232AudioTagSoundLoader.prototype.stop = function(name) {
233 var snd = this.looping[name];
234 if (!snd) {
235 return;
236 }
237 snd.loop = false;
238 snd.pause();
239 snd.currentTime = 0;
240 delete this.looping[name];
241};
242function setAudioTagVolume(sounds, gain) {
243 for (var name in sounds) {
244 if (sounds.hasOwnProperty(name)) {
245 sounds[name].volume = gain;
246 }
247 }
248}
249AudioTagSoundLoader.prototype.mute = function() {
250 setAudioTagVolume(this.sounds, 0);
251 this.muted = true;
252};
253AudioTagSoundLoader.prototype.unmute = function() {
254 setAudioTagVolume(this.sounds, this.volume);
255 this.muted = false;
256};
257AudioTagSoundLoader.prototype.setVolume = function(gain) {
258 this.volume = gain;
259 setAudioTagVolume(this.sounds, gain);
260 this.muted = false;
261};
262AudioTagSoundLoader.prototype.isMuted = function() {
263 return this.muted;
264};
265
266
267function FakeSoundLoader() {}
268FakeSoundLoader.prototype.load = function() {};
269FakeSoundLoader.prototype.allLoaded = function() { return true; };
270FakeSoundLoader.prototype.play = function() {};
271FakeSoundLoader.prototype.stop = function() {};
272FakeSoundLoader.prototype.mute = function() {};
273FakeSoundLoader.prototype.unmute = function() {};
274FakeSoundLoader.prototype.setVolume = function() {};
275FakeSoundLoader.prototype.isMuted = function() {
276 return true;
277};
278
279if (window.AudioContext) {
280 module.exports = SoundLoader;
281} else if (window.Audio) {
282 module.exports = AudioTagSoundLoader;
283} else {
284 console.log("This browser doesn't support the Web Audio API or the HTML5 audio tag.");
285 module.exports = FakeSoundLoader;
286}