sound.js 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837
  1. "use strict";
  2. /// vwf/model/sound.js is a sound driver that wraps the capabilities of the
  3. /// HTML5 web audio API.
  4. ///
  5. /// @module vwf/model/sound
  6. /// @requires vwf/model
  7. // References:
  8. // http://www.html5rocks.com/en/tutorials/webaudio/intro/
  9. // https://dvcs.w3.org/hg/audio/raw-file/tip/webaudio/specification.html
  10. // http://www.html5rocks.com/en/tutorials/webaudio/fieldrunners/
  11. define( [ "module", "vwf/model" ], function( module, model ) {
  12. // TODO: should these be stored in this.state so that the view can access them?
  13. var context;
  14. var soundData = {};
  15. var soundGroups = {};
  16. var masterVolume = 1.0;
  17. var logger;
  18. var soundDriver;
  19. var startTime = Date.now();
  20. var driver = model.load( module, {
  21. initialize: function() {
  22. // In case somebody tries to reference it before we get a chance to create it.
  23. // (it's created in the view)
  24. this.state.soundManager = {};
  25. soundDriver = this;
  26. logger = this.logger;
  27. try {
  28. // I quote: "For WebKit- and Blink-based browsers, you
  29. // currently need to use the webkit prefix, i.e.
  30. // webkitAudioContext."
  31. // http://www.html5rocks.com/en/tutorials/webaudio/intro/
  32. if ( !window.AudioContext ) {
  33. window.AudioContext = window.webkitAudioContext;
  34. }
  35. context = new AudioContext();
  36. }
  37. catch( e ) {
  38. // alert( 'Web Audio API is not supported in this browser' );
  39. logger.warnx( "initialize", "Web Audio API is not supported in this browser." );
  40. }
  41. },
  42. callingMethod: function( nodeID, methodName, params ) {
  43. if ( nodeID !== this.state.soundManager.nodeID ) {
  44. return undefined;
  45. }
  46. if ( !context ) {
  47. return undefined;
  48. }
  49. // variables that we'll need in the switch statement below. These all have function
  50. // scope, might as well declare them here.
  51. var soundDefinition, successCallback, failureCallback, exitCallback;
  52. var soundName, soundNames, soundDatum, soundDefinition, soundInstance;
  53. var instanceIDs, instanceID, i, volume, fadeTime, fadeMethod, instanceHandle;
  54. var soundGroup, groupName;
  55. switch( methodName ) {
  56. // arguments: soundDefinition, successCallback, failureCallback
  57. case "loadSound":
  58. soundDefinition = params[ 0 ];
  59. successCallback = params[ 1 ];
  60. failureCallback = params[ 2 ];
  61. if ( soundDefinition === undefined ) {
  62. logger.errorx( "loadSound", "The 'loadSound' method requires " +
  63. "a definition for the sound." );
  64. return undefined;
  65. }
  66. soundName = soundDefinition.soundName;
  67. if ( soundName === undefined ) {
  68. logger.errorx( "loadSound", "The sound definition must contain soundName." );
  69. return undefined;
  70. }
  71. if ( soundData[ soundName ] !== undefined ) {
  72. logger.errorx( "loadSound", "Duplicate sound named '" + soundName + "'." );
  73. return undefined;
  74. }
  75. soundData[ soundName ] = new SoundDatum( soundDefinition,
  76. successCallback,
  77. failureCallback );
  78. return;
  79. // arguments: soundName
  80. // returns: true if sound is done loading and is playable
  81. case "isReady":
  82. soundDatum = getSoundDatum( params[ 0 ] );
  83. return soundDatum !== undefined ? !!soundDatum.buffer : false;
  84. // arguments: soundName, exitCallback (which is called when the sound stops)
  85. // returns: an instance handle, which is an object:
  86. // { soundName: value, instanceID: value }
  87. // instanceID is -1 on failure
  88. case "playSound":
  89. soundName = params[ 0 ];
  90. soundDatum = getSoundDatum( soundName );
  91. exitCallback = params[ 1 ];
  92. return soundDatum ? soundDatum.playSound( exitCallback )
  93. : { soundName: soundName, instanceID: -1 };
  94. case "playSequence":
  95. soundNames = params;
  96. soundDatum = getSoundDatum( soundNames[ 0 ] );
  97. var playNext = function ( soundNames, current ){
  98. if (current !== soundNames.length){
  99. soundDatum = getSoundDatum( soundNames[ current ] );
  100. soundDatum.playSound( playNext ( soundNames, current + 1 ) );
  101. }
  102. }
  103. return soundDatum ? soundDatum.playSound( playNext ( soundNames, 0 ) )
  104. : { soundName: soundName, instanceID: - 1 };
  105. // arguments: soundName
  106. // returns: true if sound is currently playing
  107. case "isSoundPlaying":
  108. soundDatum = getSoundDatum( params[ 0 ] );
  109. return soundDatum ? soundDatum.isPlaying() : false;
  110. // arguments: instanceHandle
  111. // returns: true if sound is currently playing
  112. case "isInstancePlaying":
  113. return getSoundInstance( params[ 0 ] ) !== undefined;
  114. // // arguments: instanceHandle, volume, fadeTime, fadeMethod
  115. case "setVolume":
  116. instanceHandle = params [ 0 ];
  117. soundDatum = getSoundDatum( instanceHandle.soundName );
  118. if ( soundDatum ){
  119. soundDatum.setVolume ( params [ 0 ], params [ 1 ], params [ 2 ], params [ 3 ] );
  120. }
  121. // // arguments: volume (0.0-1.0)
  122. case "setMasterVolume":
  123. masterVolume = params [ 0 ];
  124. for ( soundName in soundData ){
  125. soundDatum = soundData[ soundName ];
  126. if ( soundDatum ) {
  127. soundDatum.resetOnMasterVolumeChange();
  128. }
  129. }
  130. // // arguments: instanceHandle
  131. case "hasSubtitle":
  132. instanceHandle = params [ 0 ];
  133. soundDatum = getSoundDatum( instanceHandle.soundName );
  134. return soundDatum ? !!soundDatum.subtitle : undefined;
  135. // // arguments: instanceHandle
  136. case "getSubtitle":
  137. instanceHandle = params [ 0 ];
  138. soundDatum = getSoundDatum( instanceHandle.soundName );
  139. return soundDatum ? soundDatum.subtitle : undefined;
  140. // arguments: instanceHandle
  141. // returns: the duration of the sound
  142. case "getDuration":
  143. instanceHandle = params[ 0 ];
  144. soundDatum = getSoundDatum( instanceHandle.soundName );
  145. return soundDatum && soundDatum.buffer ? soundDatum.buffer.duration : undefined;
  146. // arguments: instanceHandle
  147. case "stopSoundInstance":
  148. instanceHandle = params[ 0 ];
  149. //If a user chooses to pass just a soundName, stop all instances with that name.
  150. if ( !instanceHandle.soundName ){
  151. soundName = params[ 0 ];
  152. soundDatum = getSoundDatum( soundName );
  153. soundDatum && soundDatum.stopDatumSoundInstances();
  154. } else {
  155. //Otherwise stop the specific instance.
  156. soundDatum = getSoundDatum( instanceHandle.soundName );
  157. soundDatum && soundDatum.stopInstance( instanceHandle );
  158. }
  159. return;
  160. // arguments: groupName
  161. case "stopSoundGroup":
  162. groupName = params[ 0 ];
  163. soundGroup = soundGroups[ groupName ];
  164. soundGroup && soundGroup.clearQueue();
  165. soundGroup && soundGroup.stopPlayingSound();
  166. return;
  167. // arguments: none
  168. case "stopAllSoundInstances":
  169. for ( groupName in soundGroups ) {
  170. soundGroup = soundGroups[ groupName ];
  171. soundGroup && soundGroup.clearQueue();
  172. }
  173. for ( soundName in soundData ) {
  174. soundDatum = soundData[ soundName ];
  175. soundDatum && soundDatum.stopDatumSoundInstances();
  176. }
  177. return undefined;
  178. // arguments: soundName
  179. case "getSoundDefinition":
  180. soundDatum = getSoundDatum( params[ 0 ] );
  181. return soundDatum ? soundDatum.soundDefinition : undefined;
  182. }
  183. return undefined;
  184. }
  185. } );
  186. function SoundDatum( soundDefinition, successCallback, failureCallback ) {
  187. this.initialize( soundDefinition, successCallback, failureCallback );
  188. return this;
  189. }
  190. SoundDatum.prototype = {
  191. constructor: SoundDatum,
  192. // the name
  193. name: "",
  194. // the actual sound
  195. buffer: null,
  196. // a hashtable of sound instances
  197. playingInstances: null,
  198. // control parameters
  199. initialVolume: 1.0,
  200. isLooping: false,
  201. allowMultiplay: false,
  202. soundDefinition: null,
  203. playOnLoad: false,
  204. subtitle: undefined,
  205. soundGroup: undefined,
  206. groupReplacementMethod: undefined,
  207. queueDelayTime: undefined, // in seconds
  208. // a counter for creating instance IDs
  209. instanceIDCounter: 0,
  210. initialize: function( soundDefinition, successCallback, failureCallback ) {
  211. this.name = soundDefinition.soundName;
  212. this.playingInstances = {};
  213. this.soundDefinition = soundDefinition;
  214. if ( this.soundDefinition.isLooping !== undefined ) {
  215. this.isLooping = soundDefinition.isLooping;
  216. }
  217. if ( this.soundDefinition.allowMultiplay !== undefined ) {
  218. this.allowMultiplay = soundDefinition.allowMultiplay;
  219. }
  220. if (this.soundDefinition.initialVolume !== undefined ) {
  221. this.initialVolume = soundDefinition.initialVolume;
  222. }
  223. if ( this.soundDefinition.playOnLoad !== undefined ) {
  224. this.playOnLoad = soundDefinition.playOnLoad;
  225. }
  226. this.subtitle = this.soundDefinition.subtitle;
  227. var soundGroupName = this.soundDefinition.soundGroup;
  228. if ( soundGroupName ) {
  229. if ( !soundGroups[ soundGroupName ] ) {
  230. soundGroups[ soundGroupName ] =
  231. new SoundGroup( soundGroupName );
  232. }
  233. this.soundGroup = soundGroups[ soundGroupName ];
  234. }
  235. this.groupReplacementMethod = this.soundDefinition.groupReplacementMethod;
  236. if ( this.groupReplacementMethod && !this.soundGroup ) {
  237. logger.warnx( "SoundDatum.initialize",
  238. "You defined a replacement method but not a sound " +
  239. "group. Replacement is only done when you replace " +
  240. "another sound in the same group!" );
  241. }
  242. if ( this.soundDefinition.queueDelayTime !== undefined ) {
  243. this.queueDelayTime = this.soundDefinition.queueDelayTime;
  244. if ( this.groupReplacementMethod !== "queue" ) {
  245. logger.warnx( "SoundDatum.initialize",
  246. "You defined a queue delay time, but " +
  247. "the replacement method is not 'queue'.");
  248. }
  249. } else {
  250. this.queueDelayTime =
  251. this.groupReplacementMethod === "queue" ? 0.8 : 0;
  252. }
  253. // Create & send the request to load the sound asynchronously
  254. var request = new XMLHttpRequest();
  255. request.open( 'GET', soundDefinition.soundURL, true );
  256. request.responseType = 'arraybuffer';
  257. var thisSoundDatum = this;
  258. request.onload = function() {
  259. context.decodeAudioData(
  260. request.response,
  261. function( buffer ) {
  262. thisSoundDatum.buffer = buffer;
  263. if ( thisSoundDatum.playOnLoad === true ) {
  264. thisSoundDatum.playSound( null, true );
  265. }
  266. successCallback && successCallback();
  267. },
  268. function() {
  269. logger.warnx( "SoundDatum.initialize", "Failed to load sound: '" +
  270. thisSoundDatum.name + "'." );
  271. delete soundData[ thisSoundDatum.name ];
  272. failureCallback && failureCallback();
  273. }
  274. );
  275. }
  276. request.send();
  277. },
  278. playSound: function( exitCallback ) {
  279. if ( !this.buffer ) {
  280. logger.errorx( "SoundDatum.playSound", "Sound '" + this.name + "' hasn't finished " +
  281. "loading, or loaded improperly." );
  282. return { soundName: this.name, instanceID: -1 };
  283. }
  284. if ( !this.allowMultiplay && this.isPlaying() ) {
  285. return { soundName: this.name,
  286. instanceID: this.playingInstances[ 0 ] };
  287. }
  288. var id = this.instanceIDCounter;
  289. ++this.instanceIDCounter;
  290. this.playingInstances[ id ] = new PlayingInstance( this, id, exitCallback );
  291. return { soundName: this.name, instanceID: id };
  292. },
  293. stopInstance: function( instanceHandle ) {
  294. var soundInstance = this.playingInstances[ instanceHandle.instanceID ];
  295. soundInstance && soundInstance.stop();
  296. },
  297. stopDatumSoundInstances: function () {
  298. for ( var instanceID in this.playingInstances ) {
  299. var soundInstance = this.playingInstances[ instanceID ];
  300. soundInstance && soundInstance.stop();
  301. }
  302. // I have no freaking idea why uncommenting this breaks absolutely
  303. // everything, but it does!
  304. // this.playingInstances = {};
  305. },
  306. resetOnMasterVolumeChange: function () {
  307. for ( var instanceID in this.playingInstances ) {
  308. var soundInstance = this.playingInstances[ instanceID ];
  309. if ( soundInstance ) {
  310. soundInstance.resetVolume();
  311. }
  312. }
  313. },
  314. setVolume: function ( instanceHandle, volume, fadeTime, fadeMethod ) {
  315. // arguments: instanceHandle, volume, fadeTime, fadeMethod
  316. var soundInstance = getSoundInstance( instanceHandle );
  317. soundInstance && soundInstance.setVolume( volume, fadeTime, fadeMethod );
  318. },
  319. isPlaying: function() {
  320. var instanceIDs = Object.keys( this.playingInstances );
  321. return instanceIDs.length > 0;
  322. },
  323. }
  324. function PlayingInstance( soundDatum, id, exitCallback, successCallback ) {
  325. this.initialize( soundDatum, id, exitCallback, successCallback );
  326. return this;
  327. }
  328. PlayingInstance.prototype = {
  329. constructor: PlayingInstance,
  330. // our id, for future reference
  331. id: undefined,
  332. // a reference back to the soundDatum
  333. soundDatum: null,
  334. // the control nodes for the flowgraph
  335. sourceNode: undefined,
  336. gainNode: undefined,
  337. // we need to know the volume for this node *before* the master volume
  338. // adjustment is applied. This is that value.
  339. localVolume$: undefined,
  340. // stopped, delayed, playing, stopping, delayCancelled
  341. playStatus: undefined,
  342. initialize: function( soundDatum, id, exitCallback, successCallback ) {
  343. // NOTE: from http://www.html5rocks.com/en/tutorials/webaudio/intro/:
  344. //
  345. // An important point to note is that on iOS, Apple currently mutes all sound
  346. // output until the first time a sound is played during a user interaction
  347. // event - for example, calling playSound() inside a touch event handler.
  348. // You may struggle with Web Audio on iOS "not working" unless you circumvent
  349. // this - in order to avoid problems like this, just play a sound (it can even
  350. // be muted by connecting to a Gain Node with zero gain) inside an early UI
  351. // event - e.g. "touch here to play".
  352. this.id = id;
  353. this.soundDatum = soundDatum;
  354. this.playStatus = "stopped";
  355. this.sourceNode = context.createBufferSource();
  356. this.sourceNode.buffer = this.soundDatum.buffer;
  357. this.sourceNode.loop = this.soundDatum.isLooping;
  358. this.localVolume$ = this.soundDatum.initialVolume;
  359. this.gainNode = context.createGain();
  360. this.sourceNode.connect( this.gainNode );
  361. this.gainNode.connect( context.destination );
  362. this.resetVolume();
  363. var group = soundDatum.soundGroup;
  364. // Browsers will handle onended differently depending on audio
  365. // filetype - needs support.
  366. var thisInstance = this;
  367. this.sourceNode.onended = function() {
  368. thisInstance.playStatus = "stopped";
  369. fireSoundEvent( "soundFinished", thisInstance );
  370. // logger.logx( "PlayingInstance.onended",
  371. // "Sound ended: '" + thisInstance.soundDatum.name +
  372. // "', Timestamp: " + timestamp() );
  373. if ( group ) {
  374. group.soundFinished( thisInstance );
  375. var nextInstance = group.unQueueSound();
  376. if ( nextInstance ) {
  377. var delaySeconds = nextInstance.soundDatum.queueDelayTime;
  378. // logger.logx( "PlayingInstance.onended",
  379. // "Popped from the queue: '" +
  380. // nextInstance.soundDatum.name +
  381. // ", Timeout: " + delaySeconds +
  382. // ", Timestamp: " + timestamp() );
  383. if ( delaySeconds > 0 ) {
  384. nextInstance.startDelayed( delaySeconds );
  385. } else {
  386. nextInstance.start();
  387. }
  388. }
  389. }
  390. delete soundDatum.playingInstances[ id ];
  391. exitCallback && exitCallback();
  392. }
  393. if ( group ) {
  394. switch ( soundDatum.groupReplacementMethod ) {
  395. case "queue":
  396. // We're only going to play the sound if there isn't
  397. // already a sound from this group playing.
  398. // Otherwise, add it to a queue to play later.
  399. if ( group.getPlayingSound() ) {
  400. group.queueSound( this );
  401. } else {
  402. this.start();
  403. }
  404. break;
  405. case "replace":
  406. group.clearQueue();
  407. group.stopPlayingSound();
  408. this.start();
  409. break;
  410. default:
  411. logger.errorx( "PlayingInstance.initialize",
  412. "This sound ('" +
  413. thisInstance.soundDatum.name +
  414. "') is in a group, but doesn't " +
  415. "have a valid replacement method!" );
  416. group.clearQueue();
  417. group.stopPlayingSound();
  418. this.start();
  419. }
  420. } else {
  421. this.start();
  422. }
  423. },
  424. getVolume: function() {
  425. return this.localVolume$ * ( masterVolume !== undefined ? masterVolume : 1.0 );
  426. },
  427. setVolume: function( volume, fadeTime, fadeMethod ) {
  428. if ( !volume ) {
  429. logger.errorx( "PlayingInstance.setVolume", "The 'setVolume' " +
  430. "method requires a volume." );
  431. return;
  432. }
  433. this.localVolume$ = volume;
  434. if ( !fadeTime || ( fadeTime <= 0 ) ) {
  435. fadeMethod = "immediate";
  436. }
  437. var now = context.currentTime;
  438. this.gainNode.gain.cancelScheduledValues( now );
  439. switch( fadeMethod ) {
  440. case "linear":
  441. var endTime = now + fadeTime;
  442. this.gainNode.gain.linearRampToValueAtTime( this.getVolume(),
  443. endTime );
  444. break;
  445. case "exponential":
  446. case undefined:
  447. this.gainNode.gain.setTargetValueAtTime( this.getVolume(),
  448. now, fadeTime );
  449. break;
  450. case "immediate":
  451. this.gainNode.gain.value = this.getVolume();
  452. break;
  453. default:
  454. logger.errorx( "PlayingInstance.setVolume", "Unknown fade method: '" +
  455. fadeMethod + "'. Using an exponential " +
  456. "fade." );
  457. this.gainNode.gain.setTargetValueAtTime( this.getVolume(),
  458. now, fadeTime );
  459. }
  460. },
  461. resetVolume: function() {
  462. this.setVolume(this.localVolume$);
  463. },
  464. start: function() {
  465. switch ( this.playStatus ) {
  466. case "playing":
  467. logger.warnx( "PlayingInstance.start",
  468. "Duplicate call to start. Sound: '" +
  469. this.soundDatum.name + "'." );
  470. break;
  471. case "stopping":
  472. logger.warnx( "PlayingInstance.start", "Start is being " +
  473. "called, but we're not done stopping yet. " +
  474. "Is that bad?" );
  475. // deliberately drop through - we can restart it.
  476. case "delayed":
  477. case "stopped":
  478. var group = this.soundDatum.soundGroup;
  479. var playingSound = group ? group.getPlayingSound()
  480. : undefined;
  481. if ( !group ||
  482. !playingSound ||
  483. ( ( playingSound === this ) &&
  484. ( this.playStatus !== "stopping" ) ) ) {
  485. this.playStatus = "playing";
  486. group && group.setPlayingSound( this );
  487. this.sourceNode.start( 0 );
  488. // logger.logx( "startSoundInstance",
  489. // "Sound started: '" + this.soundDatum.name +
  490. // "', Timestamp: " + timestamp() );
  491. fireSoundEvent( "soundStarted", this );
  492. } else {
  493. if ( ( playingSound !== this ) &&
  494. ( playingSound.playStatus != "stopping" ) ) {
  495. logger.errorx( "PlayingInstance.start",
  496. "We are trying to start a sound " +
  497. "('" + this.soundDatum.name +
  498. "') that is in a sound group, " +
  499. "but the currently playing sound " +
  500. "in that group ('" +
  501. playingSound.soundDatum.name +
  502. "') isn't in the process of " +
  503. "stopping. This is probably bad." );
  504. }
  505. // Because the sound API is asynchronous, this happens
  506. // fairly often. The trick is to just stuff this
  507. // sound onto the front of the queue, and let it run
  508. // whenever whatever is playing right now finishes.
  509. group.jumpQueue( this );
  510. }
  511. break;
  512. case "delayCancelled":
  513. // don't start - we've been trumped.
  514. // NOTE: it's theoretically possible to re-queue the sound
  515. // in between when the delay is cancelled and when it
  516. // finishes the delay and calls start. In this case the
  517. // sound might be delayed more than desired, but should
  518. // still eventually play (I think). If you're restarting
  519. // sounds alot, consider looking into this.
  520. this.playStatus = "stopped";
  521. break;
  522. default:
  523. logger.errorx( "PlayingInstance.start", "Invalid " +
  524. "playStatus: '" + this.playStatus + "'!" );
  525. }
  526. },
  527. startDelayed: function( delaySeconds ) {
  528. var group = this.soundDatum.soundGroup;
  529. if ( group ) {
  530. if ( group.getPlayingSound() ) {
  531. logger.errorx( "PlayingInstance.startDelayed",
  532. "How is there already a sound playing " +
  533. "when startDelayed() is called?" );
  534. return;
  535. }
  536. group.setPlayingSound( this );
  537. }
  538. this.playStatus = "delayed";
  539. setTimeout( this.start.bind(this), delaySeconds * 1000 );
  540. },
  541. stop: function() {
  542. switch ( this.playStatus ) {
  543. case "playing":
  544. this.playStatus = "stopping";
  545. this.sourceNode.stop();
  546. break;
  547. case "delayed":
  548. this.playStatus = "delayCancelled";
  549. var group = this.soundDatum.soundGroup;
  550. if ( group ) {
  551. group.soundFinished( this );
  552. }
  553. break;
  554. case "delayCancelled":
  555. case "stopping":
  556. case "stopped":
  557. logger.warnx( "PlayingInstance.stop", "Duplicate call " +
  558. "to stop (or it was never started). " +
  559. "Sound: '" + this.soundDatum.name + "'." );
  560. break;
  561. default:
  562. logger.errorx( "PlayingInstance.stop", "Invalid " +
  563. "playStatus: '" + this.playStatus + "'!" );
  564. }
  565. },
  566. }
  567. function SoundGroup( groupName ) {
  568. this.initialize( groupName );
  569. return this;
  570. }
  571. SoundGroup.prototype = {
  572. constructor: SoundGroup,
  573. // Trying out a new convention - make the values in the prototype be
  574. // something obvious, so I can tell if they don't get reset.
  575. name$: "PROTOTYPE", // the name of the group, for debugging
  576. queue$: "PROTOTYPE", // for storing queued sounds while they wait
  577. playingSound$: "PROTOTYPE", // the sound that is currently playing
  578. initialize: function( groupName ) {
  579. this.name$ = groupName;
  580. this.queue$ = [];
  581. this.playingSound$ = undefined;
  582. },
  583. getPlayingSound: function() {
  584. return this.playingSound$;
  585. },
  586. setPlayingSound: function( playingInstance ) {
  587. if ( this.playingSound$ ) {
  588. if ( this.playingSound$ !== playingInstance ) {
  589. logger.errorx( "SoundGroup.setPlayingSound",
  590. "Trying to set playingSound to '" +
  591. playingInstance.soundDatum.name +
  592. "', but it is already set to '" +
  593. this.playingSound$.soundDatum.name + "'!");
  594. } else if ( !this.playingSound$.playStatus !== "delayed" ) {
  595. logger.errorx("SoundGroup.setPlayingSound",
  596. "How are we re-setting the playing sound " +
  597. "when we're not in a delay? Sound: '" +
  598. this.playingSound$.soundDatum.name + "'." );
  599. }
  600. } else {
  601. this.playingSound$ = playingInstance;
  602. }
  603. },
  604. soundFinished: function( playingInstance ) {
  605. if ( playingInstance !== this.playingSound$ ) {
  606. logger.errorx( "SoundGroup.soundFinished", "'" +
  607. playingInstance.soundDatum.name + "' just " +
  608. "repored that it is finished, but we thought " +
  609. "that '" + this.playingSound$.soundDatum.name +
  610. "' was playing!");
  611. return;
  612. }
  613. this.playingSound$ = undefined;
  614. },
  615. stopPlayingSound: function() {
  616. this.playingSound$ && this.playingSound$.stop();
  617. },
  618. // get in the back of the queue of sounds to play
  619. queueSound: function( playingInstance ) {
  620. this.queue$.unshift( playingInstance );
  621. },
  622. // jump to the front of the queue of sounds to play
  623. jumpQueue: function( playingInstance ) {
  624. this.queue$.push( playingInstance );
  625. },
  626. unQueueSound: function() {
  627. return this.queue$.pop();
  628. },
  629. clearQueue: function() {
  630. this.queue$.length = 0;
  631. },
  632. hasQueuedSounds: function() {
  633. return queue$.length > 0;
  634. },
  635. }
  636. function getSoundDatum( soundName ) {
  637. if ( soundName === undefined ) {
  638. logger.errorx( "getSoundDatum", "The 'getSoundDatum' method " +
  639. "requires the sound name." );
  640. return undefined;
  641. }
  642. var soundDatum = soundData[ soundName ];
  643. if ( soundDatum === undefined ) {
  644. logger.errorx( "getSoundDatum", "Sound '" + soundName +
  645. "' not found." );
  646. return undefined;
  647. }
  648. return soundDatum;
  649. }
  650. function getSoundInstance( instanceHandle ) {
  651. if ( instanceHandle === undefined ) {
  652. logger.errorx( "getSoundInstance", "The 'GetSoundInstance' " +
  653. "method requires the instance ID." );
  654. return undefined;
  655. }
  656. if ( ( instanceHandle.soundName === undefined ) ||
  657. ( instanceHandle.instanceID === undefined ) ) {
  658. logger.errorx( "getSoundInstance", "The instance handle must " +
  659. "contain soundName and instanceID values");
  660. return undefined;
  661. }
  662. var soundDatum = getSoundDatum( instanceHandle.soundName );
  663. if ( soundDatum.isLayered === true ) {
  664. return soundDatum;
  665. } else {
  666. return soundDatum ? soundDatum.playingInstances[ instanceHandle.instanceID ]
  667. : undefined;
  668. }
  669. }
  670. function fireSoundEvent( eventString, instance ) {
  671. vwf_view.kernel.fireEvent( soundDriver.state.soundManager.nodeID,
  672. eventString,
  673. [ { soundName: instance.soundDatum.name,
  674. instanceID: instance.id } ] );
  675. }
  676. function timestamp() {
  677. var delta = Date.now() - startTime;
  678. var minutes = Math.floor( delta / 60000 );
  679. var seconds = ( delta % 60000 ) / 1000;
  680. return "" + minutes + ":" + seconds;
  681. }
  682. return driver;
  683. } );