1 /**
  2  * The MIT License (MIT)
  3  *
  4  * Copyright (c) 2016 DeNA Co., Ltd.
  5  *
  6  * Permission is hereby granted, free of charge, to any person obtaining a copy
  7  * of this software and associated documentation files (the "Software"), to deal
  8  * in the Software without restriction, including without limitation the rights
  9  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 10  * copies of the Software, and to permit persons to whom the Software is
 11  * furnished to do so, subject to the following conditions:
 12  *
 13  * The above copyright notice and this permission notice shall be included in
 14  * all copies or substantial portions of the Software.
 15  *
 16  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 17  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 18  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 19  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 20  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 21  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 22  * SOFTWARE.
 23  */
 24 
 25 /**
 26  * The top-level namespace for this library.
 27  * @namespace createjs
 28  */
 29 var createjs = {};
 30 
 31 /**
 32  * Represents whether this library is compiled with the Closure compiler.
 33  * @define {boolean}
 34  */
 35 createjs.COMPILED = false;
 36 
 37 /**
 38  * Represents whether to enable debugging features.
 39  * @define {boolean}
 40  */
 41 createjs.DEBUG = true;
 42 
 43 /**
 44  * A reference to the global context.
 45  * @const {Object}
 46  */
 47 createjs.global = window;
 48 
 49 /**
 50  * The constructor of the AudioContext class.
 51  * @const {function(new:AudioContext)}
 52  */
 53 createjs.AudioContext =
 54     createjs.global['webkitAudioContext'] || createjs.global['AudioContext'];
 55 
 56 /**
 57  * The commands used for communicating with a createjs.FramePlayer object, which
 58  * runs on an <iframe> element.
 59  * @enum {number}
 60  */
 61 createjs.FrameCommand = {
 62   LOAD: 0,
 63   PLAY: 1,
 64   STOP: 2,
 65   SET_VOLUME: 3,
 66   INITIALIZE: 4,
 67   TOUCH: 5,
 68   DECODE: 6,
 69   END: 7,
 70   CLONE: 8
 71 };
 72 
 73 /**
 74  * Writes a log message to a console.
 75  * @param {string} message
 76  */
 77 createjs.log = function(message) {
 78   /// <param type="string" name="message"/>
 79   if (createjs.DEBUG) {
 80     console.log(message);
 81   }
 82 };
 83 
 84 /**
 85  * Writes a log message to a console.
 86  * @param {string} message
 87  */
 88 createjs.debug = function(message) {
 89   /// <param type="string" name="message"/>
 90   if (createjs.DEBUG) {
 91     console.debug(message);
 92   }
 93 };
 94 
 95 /**
 96  * The AudioContext instance used by this player.
 97  * @type {AudioContext}
 98  * @private
 99  */
100 createjs.context_ = null;
101 
102 /**
103  * The 1-sample audio buffer representing an empty sound.
104  * @type {AudioBuffer}
105  * @private
106  */
107 createjs.buffer_ = null;
108 
109 /**
110  * Whether the host device is an iPhone or an iPad.
111  * @type {boolean}
112  * @private
113  */
114 createjs.iphone_ = false;
115 
116 /**
117  * The instance of the FramePlayer object.
118  * @type {createjs.FramePlayer}
119  * @private
120  */
121 createjs.player_ = null;
122 
123 /**
124  * The number of times to play an empty sound on touch.
125  * @type {number}
126  * @private
127  */
128 createjs.retry_ = 1;
129 
130 /**
131  * The user-action event that plays an empty sound. (This is a workaround for
132  * WebKit Bug 149367 <http://webkit.org/b/149367>.)
133  * @type {string}
134  * @private
135  */
136 createjs.USER_ACTION_EVENT_ =
137     MouseEvent['WEBKIT_FORCE_AT_MOUSE_DOWN'] ? 'touchend' : 'touchstart';
138 
139 /**
140  * A class that plays sound files on an <iframe> element. This class is used for
141  * decoding sound files in a background thread. Decoding sound files with the
142  * WebAudio API often consumes so much CPU power, especially on devices that use
143  * software decoders, that a game thread cannot consume enough CPU power. This
144  * class offloads such WebAudio-API calls onto an <iframe> element. (Most
145  * browsers run <iframe> elements on worker threads.)
146  * @constructor
147  */
148 createjs.FramePlayer = function() {
149   /**
150    * @const {Object.<string,createjs.FramePlayer.Sound>}
151    * @private
152    */
153   this.sounds_ = {};
154 };
155 
156 /**
157  * An inner class representing a sound file played by the createjs.FramePlayer
158  * class.
159  * @param {string} id
160  * @param {Window} parent
161  * @constructor
162  */
163 createjs.FramePlayer.Sound = function(id, parent) {
164   /**
165    * @type {string}
166    * @private
167    */
168   this.id_ = id;
169 
170   /**
171    * @const {Window}
172    * @private
173    */
174   this.parent_ = parent;
175 };
176 
177 /**
178  * The volume of this sound.
179  * @type {number}
180  * @private
181  */
182 createjs.FramePlayer.Sound.prototype.volume_ = 1;
183 
184 /**
185  * The start position in seconds.
186  * @type {number}
187  * @private
188  */
189 createjs.FramePlayer.Sound.prototype.offset_ = -1;
190 
191 /**
192  * The period of time to be played in seconds.
193  * @type {number}
194  * @private
195  */
196 createjs.FramePlayer.Sound.prototype.duration_ = 0;
197 
198 /**
199  * Whether this sound should be played when the browser finishes decoding it.
200  * (A game may send a play request for this sound while the browser is
201  * decoding it.)
202  * @type {number}
203  * @private
204  */
205 createjs.FramePlayer.Sound.prototype.auto_ = 0;
206 
207 /**
208  * Represents the number of loop counts of this sound. The
209  * createjs.FramePlayer object currently supports only -1 (infinite).
210  * @type {number}
211  * @private
212  */
213 createjs.FramePlayer.Sound.prototype.loop_ = 0;
214 
215 /**
216  * The clones that wait for this sound to be decoded. A game may send clone
217  * requests of this sound before the browser finishes decoding it. (Clones use
218  * the decoded data of this sound to avoid decoding one sound multiple times.)
219  * @type {Array.<createjs.FramePlayer.Sound>}
220  * @private
221  */
222 createjs.FramePlayer.Sound.prototype.clones_ = null;
223 
224 /**
225  * The audio data played by this player.
226  * @type {AudioBuffer}
227  * @private
228  */
229 createjs.FramePlayer.Sound.prototype.buffer_ = null;
230 
231 /**
232  * The source node that plays audio.
233  * @type {AudioBufferSourceNode}
234  * @private
235  */
236 createjs.FramePlayer.Sound.prototype.source_ = null;
237 
238 /**
239  * The gain node that changes the volume of this player.
240  * @type {GainNode}
241  * @private
242  */
243 createjs.FramePlayer.Sound.prototype.gain_ = null;
244 
245 /**
246  * Called when the AudioContext object finishes decoding audio.
247  * @param {AudioBuffer} buffer
248  * @private
249  */
250 createjs.FramePlayer.Sound.prototype.handleDecode_ = function(buffer) {
251   createjs.debug('decode=' + this.id_);
252   this.buffer_ = buffer;
253   this.parent_.postMessage({
254     'a': createjs.FrameCommand.DECODE,
255     'b': this.id_
256   }, "*");
257   if (this.auto_) {
258     this.play_(this.loop_, this.volume_);
259     this.auto_ = 0;
260   }
261   if (this.clones_) {
262     for (var i = 0; i < this.clones_.length; ++i) {
263       var clone = this.clones_[i];
264       clone.buffer_ = buffer;
265       if (clone.auto_) {
266         clone.play_(clone.loop_, clone.volume_);
267         clone.auto_ = 0;
268       }
269     }
270     this.clones_ = null;
271   }
272 };
273 
274 /**
275  * Called when the AudioBufferSourceNode object associated with this player
276  * finishes playing audio.
277  * @private
278  */
279 createjs.FramePlayer.Sound.prototype.handleEnded_ = function() {
280   createjs.debug('ended=' + this.id_);
281   this.stop_();
282   this.parent_.postMessage({
283     'a': createjs.FrameCommand.END,
284     'b': this.id_
285   }, "*");
286 };
287 
288 /**
289  * Starts playing the audio associated with this player.
290  * @param {number} loop
291  * @param {number} volume
292  * @private
293  */
294 createjs.FramePlayer.Sound.prototype.play_ = function(loop, volume) {
295   createjs.debug('play=' + this.id_ + ',' + loop + ',' + volume);
296   if (!this.buffer_) {
297     this.auto_ = 1;
298     this.loop_ = loop;
299     this.volume_ = volume;
300     return;
301   }
302   if (this.source_) {
303     this.stop_();
304   }
305   this.volume_ = volume;
306   var context = createjs.context_;
307   var gain =
308       context.createGain ? context.createGain() : context.createGainNode();
309   gain.connect(context.destination);
310   gain.gain.value = this.volume_;
311   this.gain_ = gain;
312   var source = context.createBufferSource();
313   source.connect(gain);
314   source.buffer = this.buffer_;
315   if (loop) {
316     source.loop = true;
317   } else {
318     source.onended = this.handleEnded_.bind(this);
319   }
320   if (this.offset_ >= 0) {
321     if (source.start) {
322       source.start(0, this.offset_, this.duration_);
323     } else {
324       source.noteGrainOn(0, this.offset_, this.duration_);
325     }
326   } else {
327     if (source.start) {
328       source.start(0);
329     } else {
330       source.noteOn(0);
331     }
332   }
333   this.source_ = source;
334 };
335 
336 /**
337  * Stops playing the audio associated with this player.
338  * @private
339  */
340 createjs.FramePlayer.Sound.prototype.stop_ = function() {
341   createjs.debug('stop=' + this.id_);
342   var source = this.source_;
343   if (source) {
344     var playbackState = source.playbackState || 0;
345     if (playbackState != 3) {
346       if (source.stop) {
347         source.stop(0);
348       } else {
349         source.noteOff(0);
350       }
351     }
352     source.disconnect(0);
353     source.onended = null;
354     // When an AudioBufferSouceNode object is disconnected from a destination
355     // node, Mobile Safari does not delete its AudioBuffer object. Attach a
356     // 1-sample AudioBuffer object to the AudioBufferSourceNode object to delete
357     // the AudioBuffer object on the browser. (This code throws an exception on
358     // Chrome and Firefox, i.e. this code must be executed only on Mobile
359     // Safari.)
360     if (createjs.iphone_) {
361       if (createjs.buffer_) {
362         source.buffer = createjs.buffer_;
363       }
364     }
365     this.source_ = null;
366     this.gain_.disconnect(0);
367     this.gain_ = null;
368   }
369 };
370 
371 /**
372  * Changes the volume of the audio being played by this player.
373  * @param {number} volume
374  * @private
375  */
376 createjs.FramePlayer.Sound.prototype.setVolume_ = function(volume) {
377   createjs.debug('volume=' + this.id_ + ',' + volume);
378   if (this.gain_) {
379     this.gain_.gain.value = volume;
380   }
381 };
382 
383 /**
384  * Called when the watchdog timer expires. This method sends an event to the
385  * owner window of this frame if the global AudioContext object advances its
386  * currentTime property, i.e. it actually plays a sound. Otherwise, it waits an
387  * user action to play a sound there.
388  * @private
389  */
390 createjs.FramePlayer.handleTimeout_ = function() {
391   createjs.debug('> currentTime=' + createjs.context_.currentTime);
392   if (createjs.context_.currentTime || !createjs.retry_) {
393     createjs.global.parent.postMessage({
394       'a': createjs.FrameCommand.TOUCH
395     }, "*");
396   } else {
397     --createjs.retry_;
398     var player = /** @type {EventListener} */ (createjs.player_);
399     createjs.global.addEventListener(
400         createjs.USER_ACTION_EVENT_, player, false);
401   }
402 };
403 
404 /**
405  * Plays an empty sound and see the host browser actually plays it. This method
406  * waits for 100 ms and sees the AudioContext object advances its currentTime
407  * property to verify the browser actually plays an empty sound. (If the value
408  * of this currentTime property is 0, the host browser needs a user action to
409  * play sounds.)
410  * @private
411  */
412 createjs.FramePlayer.playEmptySound_ = function() {
413   var context = createjs.context_;
414   var source = context.createBufferSource();
415   source.buffer = createjs.buffer_;
416   if (source.start) {
417     source.start(0);
418   } else {
419     source.noteOn(0);
420   }
421   setTimeout(createjs.FramePlayer.handleTimeout_, 100);
422 };
423 
424 /**
425  * Called when this player receives a DOM event.
426  * @param {Event} event
427  */
428 createjs.FramePlayer.prototype['handleEvent'] = function(event) {
429   var type = event.type;
430   if (type == 'message') {
431     var data = /** @type {Object} */ (/** @type {*} */ (event.data));
432     var command = /** @type {number} */ (data['a']);
433     var id = /** @type {string} */ (data['b']);
434     if (command == createjs.FrameCommand.LOAD) {
435       createjs.debug('load=' + id);
436       if (this.sounds_[id]) {
437         return;
438       }
439       var sound = new createjs.FramePlayer.Sound(id, event.source);
440       this.sounds_[id] = sound;
441       var handleDecode = sound.handleDecode_.bind(sound);
442       createjs.context_.decodeAudioData(
443           /** @type {ArrayBuffer} */ (data['c']),
444           handleDecode,
445           /** @type {function()} */ (handleDecode));
446       return;
447     }
448     var sound = this.sounds_[id];
449     if (!sound) {
450       return;
451     }
452     if (command == createjs.FrameCommand.PLAY) {
453       sound.play_(/** @type {number} */ (data['c']),
454                   /** @type {number} */ (data['d']));
455     } else if (command == createjs.FrameCommand.STOP) {
456       sound.stop_();
457     } else if (command == createjs.FrameCommand.SET_VOLUME) {
458       sound.setVolume_(/** @type {number} */ (data['c']));
459     } else if (command == createjs.FrameCommand.CLONE) {
460       createjs.debug('clone=' + data['c'] + ',' + data['d'] + ',' + data['e']);
461       var clone = new createjs.FramePlayer.Sound(sound.id_, sound.parent_);
462       clone.id_ = /** @type {string} */ (data['c']);
463       clone.buffer_ = sound.buffer_;
464       clone.offset_ = /** @type {number} */ (data['d']);
465       clone.duration_ = /** @type {number} */ (data['e']);
466       this.sounds_[/** @type {string} */ (data['c'])] = clone;
467       if (!sound.buffer_) {
468         if (!sound.clones_) {
469           sound.clones_ = [];
470         }
471         sound.clones_.push(clone);
472       }
473     }
474   } else {
475     createjs.debug('> type=' + type);
476     var global = createjs.global;
477     global.removeEventListener(type, this, false);
478     // This frame receives a user action. Plays an empty sound and verify the
479     // host browser actually plays it again. (Mobile Safari 9 does not play
480     // sounds on first touch when it is scrolling its view.)
481     createjs.FramePlayer.playEmptySound_();
482   }
483 };
484 
485 /**
486  * The entry-point method of this application.
487  * @const
488  */
489 createjs.global.onload = function() {
490   // Create the global objects used by this application.
491   createjs.context_ = new createjs.AudioContext();
492   createjs.player_ = new createjs.FramePlayer();
493   createjs.buffer_ = createjs.context_.createBuffer(1, 1, 22500);
494   var platform = navigator.platform;
495   createjs.iphone_ = platform == 'iPhone' || platform == 'iPad';
496 
497   // Plays an empty sound to see the host browser can play it without user
498   // actions.
499   createjs.FramePlayer.playEmptySound_();
500 
501   // Accept incoming messages from the owner window.
502   var player = /** @type {EventListener} */ (createjs.player_);
503   createjs.global.addEventListener('message', player, false);
504   var parent = createjs.global.parent;
505   parent.postMessage({
506     'a': createjs.FrameCommand.INITIALIZE
507   }, "*");
508 };
509