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