OLD | NEW |
(Empty) | |
| 1 // Copyright 2015 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 /** |
| 6 * @fileoverview This is the low-level class that generates ChromeVox's |
| 7 * earcons. It's designed to be self-contained and not depend on the |
| 8 * rest of the code. |
| 9 */ |
| 10 |
| 11 goog.provide('EarconEngine'); |
| 12 |
| 13 /** |
| 14 * EarconEngine generates ChromeVox's earcons using the web audio API. |
| 15 * @constructor |
| 16 */ |
| 17 EarconEngine = function() { |
| 18 // Public control parameters. All of these are meant to be adjustable. |
| 19 |
| 20 /** @type {number} The master volume, as an amplification factor. */ |
| 21 this.masterVolume = 0.2; |
| 22 |
| 23 /** @type {number} The base relative pitch adjustment, in half-steps. */ |
| 24 this.masterPitch = -4; |
| 25 |
| 26 /** @type {number} The click volume, as an amplification factor. */ |
| 27 this.clickVolume = 0.4; |
| 28 |
| 29 /** |
| 30 * @type {number} The volume of the static sound, as an |
| 31 * amplification factor. |
| 32 */ |
| 33 this.staticVolume = 0.2; |
| 34 |
| 35 /** @type {number} The base delay for repeated sounds, in seconds. */ |
| 36 this.baseDelay = 0.045; |
| 37 |
| 38 /** @type {number} The master stereo panning, from -1 to 1. */ |
| 39 this.masterPan = 0; |
| 40 |
| 41 /** @type {number} The master reverb level as an amplification factor. */ |
| 42 this.masterReverb = 0.4; |
| 43 |
| 44 /** |
| 45 * @type {string} The choice of the reverb impulse response to use. |
| 46 * Must be one of the strings from EarconEngine.REVERBS. |
| 47 */ |
| 48 this.reverbSound = 'small_room_2'; |
| 49 |
| 50 /** @type {number} The base pitch for the 'wrap' sound in half-steps. */ |
| 51 this.wrapPitch = 0; |
| 52 |
| 53 /** @type {number} The base pitch for the 'alert' sound in half-steps. */ |
| 54 this.alertPitch = 0; |
| 55 |
| 56 /** @type {string} The choice of base sound for most controls. */ |
| 57 this.controlSound = 'control'; |
| 58 |
| 59 /** |
| 60 * @type {number} The delay between sounds in the on/off sweep effect, |
| 61 * in seconds. |
| 62 */ |
| 63 this.sweepDelay = 0.045; |
| 64 |
| 65 /** |
| 66 * @type {number} The delay between echos in the on/off sweep, in seconds. |
| 67 */ |
| 68 this.sweepEchoDelay = 0.15; |
| 69 |
| 70 /** @type {number} The number of echos in the on/off sweep. */ |
| 71 this.sweepEchoCount = 3; |
| 72 |
| 73 /** @type {number} The pitch offset of the on/off sweep, in half-steps. */ |
| 74 this.sweepPitch = -7; |
| 75 |
| 76 /** |
| 77 * @type {number} The final gain of the progress sound, as an |
| 78 * amplification factor. |
| 79 */ |
| 80 this.progressFinalGain = 0.05; |
| 81 |
| 82 /** @type {number} The multiplicative decay rate of the progress ticks. */ |
| 83 this.progressGain_Decay = 0.7; |
| 84 |
| 85 // Private variables. |
| 86 |
| 87 /** @type {AudioContext} @private The audio context. */ |
| 88 this.context_ = new AudioContext(); |
| 89 |
| 90 /** @type {?ConvolverNode} @private The reverb node, lazily initialized. */ |
| 91 this.reverbConvolver_ = null; |
| 92 |
| 93 /** |
| 94 * @type {Object<string, AudioBuffer>} A map between the name of an |
| 95 * audio data file and its loaded AudioBuffer. |
| 96 * @private |
| 97 */ |
| 98 this.buffers_ = {}; |
| 99 |
| 100 /** |
| 101 * The source audio nodes for queued tick / tocks for progress. |
| 102 * Kept around so they can be canceled. |
| 103 * |
| 104 * @type {Array<AudioNode>} |
| 105 * @private |
| 106 */ |
| 107 this.progressSources_ = []; |
| 108 |
| 109 /** @type {number} The current gain for progress sounds. @private */ |
| 110 this.progressGain_ = 1.0; |
| 111 |
| 112 /** @type {?number} The current time for progress sounds. @private */ |
| 113 this.progressTime_ = this.context_.currentTime; |
| 114 |
| 115 /** |
| 116 * @type {?number} The window.setInterval ID for progress sounds. |
| 117 * @private |
| 118 */ |
| 119 this.progressIntervalID_ = null; |
| 120 |
| 121 // Initialization: load the base sound data files asynchronously. |
| 122 var allSoundFilesToLoad = EarconEngine.SOUNDS.concat(EarconEngine.REVERBS); |
| 123 allSoundFilesToLoad.forEach((function(sound) { |
| 124 var url = EarconEngine.BASE_URL + sound + '.wav'; |
| 125 this.loadSound(sound, url); |
| 126 }).bind(this)); |
| 127 }; |
| 128 |
| 129 /** |
| 130 * @type {Array<string>} The list of sound data files to load. |
| 131 * @const |
| 132 */ |
| 133 EarconEngine.SOUNDS = [ |
| 134 'control', |
| 135 'selection', |
| 136 'selection_reverse', |
| 137 'skim', |
| 138 'static']; |
| 139 |
| 140 /** |
| 141 * @type {Array<string>} The list of reverb data files to load. |
| 142 * @const |
| 143 */ |
| 144 EarconEngine.REVERBS = [ |
| 145 'small_room_2']; |
| 146 |
| 147 /** |
| 148 * @type {number} The scale factor for one half-step. |
| 149 * @const |
| 150 */ |
| 151 EarconEngine.HALF_STEP = Math.pow(2.0, 1.0 / 12.0); |
| 152 |
| 153 /** |
| 154 * @type {string} The base url for earcon sound resources. |
| 155 * @const |
| 156 */ |
| 157 EarconEngine.BASE_URL = chrome.extension.getURL('cvox2/background/earcons/'); |
| 158 |
| 159 /** |
| 160 * Fetches a sound asynchronously and loads its data into an AudioBuffer. |
| 161 * |
| 162 * @param {string} name The name of the sound to load. |
| 163 * @param {string} url The url where the sound should be fetched from. |
| 164 */ |
| 165 EarconEngine.prototype.loadSound = function(name, url) { |
| 166 var request = new XMLHttpRequest(); |
| 167 request.open('GET', url, true); |
| 168 request.responseType = 'arraybuffer'; |
| 169 |
| 170 // Decode asynchronously. |
| 171 request.onload = (function() { |
| 172 this.context_.decodeAudioData( |
| 173 /** @type {ArrayBuffer} */ (request.response), |
| 174 (function(buffer) { |
| 175 this.buffers_[name] = buffer; |
| 176 }).bind(this)); |
| 177 }).bind(this); |
| 178 request.send(); |
| 179 }; |
| 180 |
| 181 /** |
| 182 * Return an AudioNode containing the final processing that all |
| 183 * sounds go through: master volume / gain, panning, and reverb. |
| 184 * The chain is hooked up to the destination automatically, so you |
| 185 * just need to connect your source to the return value from this |
| 186 * method. |
| 187 * |
| 188 * @param {{gain: (number | undefined), |
| 189 * pan: (number | undefined), |
| 190 * reverb: (number | undefined)}} properties |
| 191 * An object where you can override the default |
| 192 * gain, pan, and reverb, otherwise these are taken from |
| 193 * masterVolume, masterPan, and masterReverb. |
| 194 * @return {AudioNode} The filters to be applied to all sounds, connected |
| 195 * to the destination node. |
| 196 */ |
| 197 EarconEngine.prototype.createCommonFilters = function(properties) { |
| 198 var gain = this.masterVolume; |
| 199 if (properties.gain) { |
| 200 gain *= properties.gain; |
| 201 } |
| 202 var gainNode = this.context_.createGain(); |
| 203 gainNode.gain.value = gain; |
| 204 var first = gainNode; |
| 205 var last = gainNode; |
| 206 |
| 207 var pan = this.masterPan; |
| 208 if (properties.pan !== undefined) { |
| 209 pan = properties.pan; |
| 210 } |
| 211 if (pan != 0) { |
| 212 var panNode = this.context_.createPanner(); |
| 213 panNode.setPosition(pan, 0, -1); |
| 214 panNode.setOrientation(0, 0, 1); |
| 215 last.connect(panNode); |
| 216 last = panNode; |
| 217 } |
| 218 |
| 219 var reverb = this.masterReverb; |
| 220 if (properties.reverb !== undefined) { |
| 221 reverb = properties.reverb; |
| 222 } |
| 223 if (reverb) { |
| 224 if (!this.reverbConvolver_) { |
| 225 this.reverbConvolver_ = this.context_.createConvolver(); |
| 226 this.reverbConvolver_.buffer = this.buffers_[this.reverbSound]; |
| 227 this.reverbConvolver_.connect(this.context_.destination); |
| 228 } |
| 229 |
| 230 // Dry |
| 231 last.connect(this.context_.destination); |
| 232 |
| 233 // Wet |
| 234 var reverbGainNode = this.context_.createGain(); |
| 235 reverbGainNode.gain.value = reverb; |
| 236 last.connect(reverbGainNode); |
| 237 reverbGainNode.connect(this.reverbConvolver_); |
| 238 } else { |
| 239 last.connect(this.context_.destination); |
| 240 } |
| 241 |
| 242 return first; |
| 243 }; |
| 244 |
| 245 /** |
| 246 * High-level interface to play a sound from a buffer source by name, |
| 247 * with some simple adjustments like pitch change (in half-steps), |
| 248 * a start time (relative to the current time, in seconds), |
| 249 * gain, panning, and reverb. |
| 250 * |
| 251 * The only required parameter is the name of the sound. The time, pitch, |
| 252 * gain, panning, and reverb are all optional and are passed in an |
| 253 * object of optional properties. |
| 254 * |
| 255 * @param {string} sound The name of the sound to play. It must already |
| 256 * be loaded in a buffer. |
| 257 * @param {{pitch: (number | undefined), |
| 258 * time: (number | undefined), |
| 259 * gain: (number | undefined), |
| 260 * pan: (number | undefined), |
| 261 * reverb: (number | undefined)}=} opt_properties |
| 262 * An object where you can override the default pitch, gain, pan, |
| 263 * and reverb. |
| 264 * @return {AudioBufferSourceNode} The source node, so you can stop it |
| 265 * or set event handlers on it. |
| 266 */ |
| 267 EarconEngine.prototype.play = function(sound, opt_properties) { |
| 268 var source = this.context_.createBufferSource(); |
| 269 source.buffer = this.buffers_[sound]; |
| 270 |
| 271 if (!opt_properties) { |
| 272 // This typecast looks silly, but the Closure compiler doesn't support |
| 273 // optional fields in record types very well so this is the shortest hack. |
| 274 opt_properties = /** @type {undefined} */({}); |
| 275 } |
| 276 |
| 277 var pitch = this.masterPitch; |
| 278 if (opt_properties.pitch) { |
| 279 pitch += opt_properties.pitch; |
| 280 } |
| 281 if (pitch != 0) { |
| 282 source.playbackRate.value = Math.pow(EarconEngine.HALF_STEP, pitch); |
| 283 } |
| 284 |
| 285 var destination = this.createCommonFilters(opt_properties); |
| 286 source.connect(destination); |
| 287 |
| 288 if (opt_properties.time) { |
| 289 source.start(this.context_.currentTime + opt_properties.time); |
| 290 } else { |
| 291 source.start(this.context_.currentTime); |
| 292 } |
| 293 |
| 294 return source; |
| 295 }; |
| 296 |
| 297 /** |
| 298 * Play the static sound. |
| 299 */ |
| 300 EarconEngine.prototype.onStatic = function() { |
| 301 this.play('static', {gain: this.staticVolume}); |
| 302 }; |
| 303 |
| 304 /** |
| 305 * Play the link sound. |
| 306 */ |
| 307 EarconEngine.prototype.onLink = function() { |
| 308 this.play('static', {gain: this.clickVolume}); |
| 309 this.play(this.controlSound, {pitch: 12}); |
| 310 }; |
| 311 |
| 312 /** |
| 313 * Play the button sound. |
| 314 */ |
| 315 EarconEngine.prototype.onButton = function() { |
| 316 this.play('static', {gain: this.clickVolume}); |
| 317 this.play(this.controlSound); |
| 318 }; |
| 319 |
| 320 /** |
| 321 * Play the text field sound. |
| 322 */ |
| 323 EarconEngine.prototype.onTextField = function() { |
| 324 this.play('static', {gain: this.clickVolume}); |
| 325 this.play('static', {time: this.baseDelay * 1.5, |
| 326 gain: this.clickVolume * 0.5}); |
| 327 this.play(this.controlSound, {pitch: 4}); |
| 328 this.play(this.controlSound, |
| 329 {pitch: 4, |
| 330 time: this.baseDelay * 1.5, |
| 331 gain: 0.5}); |
| 332 }; |
| 333 |
| 334 /** |
| 335 * Play the pop up button sound. |
| 336 */ |
| 337 EarconEngine.prototype.onPopUpButton = function() { |
| 338 this.play('static', {gain: this.clickVolume}); |
| 339 |
| 340 this.play(this.controlSound); |
| 341 this.play(this.controlSound, |
| 342 {time: this.baseDelay * 3, |
| 343 gain: 0.2, |
| 344 pitch: 12}); |
| 345 this.play(this.controlSound, |
| 346 {time: this.baseDelay * 4.5, |
| 347 gain: 0.2, |
| 348 pitch: 12}); |
| 349 }; |
| 350 |
| 351 /** |
| 352 * Play the check on sound. |
| 353 */ |
| 354 EarconEngine.prototype.onCheckOn = function() { |
| 355 this.play('static', {gain: this.clickVolume}); |
| 356 this.play(this.controlSound, {pitch: -5}); |
| 357 this.play(this.controlSound, {pitch: 7, time: this.baseDelay * 2}); |
| 358 }; |
| 359 |
| 360 /** |
| 361 * Play the check off sound. |
| 362 */ |
| 363 EarconEngine.prototype.onCheckOff = function() { |
| 364 this.play('static', {gain: this.clickVolume}); |
| 365 this.play(this.controlSound, {pitch: 7}); |
| 366 this.play(this.controlSound, {pitch: -5, time: this.baseDelay * 2}); |
| 367 }; |
| 368 |
| 369 /** |
| 370 * Play the select control sound. |
| 371 */ |
| 372 EarconEngine.prototype.onSelect = function() { |
| 373 this.play('static', {gain: this.clickVolume}); |
| 374 this.play(this.controlSound); |
| 375 this.play(this.controlSound, {time: this.baseDelay}); |
| 376 this.play(this.controlSound, {time: this.baseDelay * 2}); |
| 377 }; |
| 378 |
| 379 /** |
| 380 * Play the slider sound. |
| 381 */ |
| 382 EarconEngine.prototype.onSlider = function() { |
| 383 this.play('static', {gain: this.clickVolume}); |
| 384 this.play(this.controlSound); |
| 385 this.play(this.controlSound, |
| 386 {time: this.baseDelay, |
| 387 gain: 0.5, |
| 388 pitch: 2}); |
| 389 this.play(this.controlSound, |
| 390 {time: this.baseDelay * 2, |
| 391 gain: 0.25, |
| 392 pitch: 4}); |
| 393 this.play(this.controlSound, |
| 394 {time: this.baseDelay * 3, |
| 395 gain: 0.125, |
| 396 pitch: 6}); |
| 397 this.play(this.controlSound, |
| 398 {time: this.baseDelay * 4, |
| 399 gain: 0.0625, |
| 400 pitch: 8}); |
| 401 }; |
| 402 |
| 403 /** |
| 404 * Play the skim sound. |
| 405 */ |
| 406 EarconEngine.prototype.onSkim = function() { |
| 407 this.play('skim'); |
| 408 }; |
| 409 |
| 410 /** |
| 411 * Play the selection sound. |
| 412 */ |
| 413 EarconEngine.prototype.onSelection = function() { |
| 414 this.play('selection'); |
| 415 }; |
| 416 |
| 417 /** |
| 418 * Play the selection reverse sound. |
| 419 */ |
| 420 EarconEngine.prototype.onSelectionReverse = function() { |
| 421 this.play('selection_reverse'); |
| 422 }; |
| 423 |
| 424 /** |
| 425 * Generate a synthesized musical note based on a sum of sinusoidals shaped |
| 426 * by an envelope, controlled by a number of properties. |
| 427 * |
| 428 * The sound has a frequency of |freq|, or if |endFreq| is specified, does |
| 429 * an exponential ramp from |freq| to |endFreq|. |
| 430 * |
| 431 * If |overtones| is greater than 1, the sound will be mixed with additional |
| 432 * sinusoidals at multiples of |freq|, each one scaled by |overtoneFactor|. |
| 433 * This creates a rounder tone than a pure sine wave. |
| 434 * |
| 435 * The envelope is shaped by the duration |dur|, the attack time |attack|, |
| 436 * and the decay time |decay|, in seconds. |
| 437 * |
| 438 * As with other functions, |pan| and |reverb| can be used to override |
| 439 * masterPan and masterReverb. |
| 440 * |
| 441 * @param {{gain: number, |
| 442 * freq: number, |
| 443 * endFreq: (number | undefined), |
| 444 * time: (number | undefined), |
| 445 * overtones: (number | undefined), |
| 446 * overtoneFactor: (number | undefined), |
| 447 * dur: (number | undefined), |
| 448 * attack: (number | undefined), |
| 449 * decay: (number | undefined), |
| 450 * pan: (number | undefined), |
| 451 * reverb: (number | undefined)}} properties |
| 452 * An object containing the properties that can be used to |
| 453 * control the sound, as described above. |
| 454 */ |
| 455 EarconEngine.prototype.generateSinusoidal = function(properties) { |
| 456 var envelopeNode = this.context_.createGain(); |
| 457 envelopeNode.connect(this.context_.destination); |
| 458 |
| 459 var time = properties.time; |
| 460 if (time === undefined) { |
| 461 time = 0; |
| 462 } |
| 463 |
| 464 // Generate an oscillator for the frequency corresponding to the specified |
| 465 // frequency, and then additional overtones at multiples of that frequency |
| 466 // scaled by the overtoneFactor. Cue the oscillator to start and stop |
| 467 // based on the start time and specified duration. |
| 468 // |
| 469 // If an end frequency is specified, do an exponential ramp to that end |
| 470 // frequency. |
| 471 var gain = properties.gain; |
| 472 for (var i = 0; i < properties.overtones; i++) { |
| 473 var osc = this.context_.createOscillator(); |
| 474 osc.frequency.value = properties.freq * (i + 1); |
| 475 |
| 476 if (properties.endFreq) { |
| 477 osc.frequency.setValueAtTime( |
| 478 properties.freq * (i + 1), |
| 479 this.context_.currentTime + time); |
| 480 osc.frequency.exponentialRampToValueAtTime( |
| 481 properties.endFreq * (i + 1), |
| 482 this.context_.currentTime + properties.dur); |
| 483 } |
| 484 |
| 485 osc.start(this.context_.currentTime + time); |
| 486 osc.stop(this.context_.currentTime + time + properties.dur); |
| 487 |
| 488 var gainNode = this.context_.createGain(); |
| 489 gainNode.gain.value = gain; |
| 490 osc.connect(gainNode); |
| 491 gainNode.connect(envelopeNode); |
| 492 |
| 493 gain *= properties.overtoneFactor; |
| 494 } |
| 495 |
| 496 // Shape the overall sound by an envelope based on the attack and |
| 497 // decay times. |
| 498 envelopeNode.gain.setValueAtTime(0, this.context_.currentTime + time); |
| 499 envelopeNode.gain.linearRampToValueAtTime( |
| 500 1, this.context_.currentTime + time + properties.attack); |
| 501 envelopeNode.gain.setValueAtTime( |
| 502 1, this.context_.currentTime + time + |
| 503 properties.dur - properties.decay); |
| 504 envelopeNode.gain.linearRampToValueAtTime( |
| 505 0, this.context_.currentTime + time + properties.dur); |
| 506 |
| 507 // Route everything through the common filters like reverb at the end. |
| 508 var destination = this.createCommonFilters({}); |
| 509 envelopeNode.connect(destination); |
| 510 }; |
| 511 |
| 512 /** |
| 513 * Play a sweep over a bunch of notes in a scale, with an echo, |
| 514 * for the ChromeVox on or off sounds. |
| 515 * |
| 516 * @param {boolean} reverse Whether to play in the reverse direction. |
| 517 */ |
| 518 EarconEngine.prototype.onChromeVoxSweep = function(reverse) { |
| 519 var pitches = [-7, -5, 0, 5, 7, 12, 17, 19, 24]; |
| 520 |
| 521 if (reverse) { |
| 522 pitches.reverse(); |
| 523 } |
| 524 |
| 525 var attack = 0.015; |
| 526 var dur = pitches.length * this.sweepDelay; |
| 527 |
| 528 var destination = this.createCommonFilters({reverb: 2.0}); |
| 529 for (var k = 0; k < this.sweepEchoCount; k++) { |
| 530 var envelopeNode = this.context_.createGain(); |
| 531 var startTime = this.context_.currentTime + this.sweepEchoDelay * k; |
| 532 var sweepGain = Math.pow(0.3, k); |
| 533 var overtones = 2; |
| 534 var overtoneGain = sweepGain; |
| 535 for (var i = 0; i < overtones; i++) { |
| 536 var osc = this.context_.createOscillator(); |
| 537 osc.start(startTime); |
| 538 osc.stop(startTime + dur); |
| 539 |
| 540 var gainNode = this.context_.createGain(); |
| 541 osc.connect(gainNode); |
| 542 gainNode.connect(envelopeNode); |
| 543 |
| 544 for (var j = 0; j < pitches.length; j++) { |
| 545 var freqDecay; |
| 546 if (reverse) { |
| 547 freqDecay = Math.pow(0.75, pitches.length - j); |
| 548 } else { |
| 549 freqDecay = Math.pow(0.75, j); |
| 550 } |
| 551 var gain = overtoneGain * freqDecay; |
| 552 var freq = (i + 1) * 220 * |
| 553 Math.pow(EarconEngine.HALF_STEP, pitches[j] + this.sweepPitch); |
| 554 if (j == 0) { |
| 555 osc.frequency.setValueAtTime(freq, startTime); |
| 556 gainNode.gain.setValueAtTime(gain, startTime); |
| 557 } else { |
| 558 osc.frequency.exponentialRampToValueAtTime( |
| 559 freq, startTime + j * this.sweepDelay); |
| 560 gainNode.gain.linearRampToValueAtTime( |
| 561 gain, startTime + j * this.sweepDelay); |
| 562 } |
| 563 osc.frequency.setValueAtTime( |
| 564 freq, startTime + j * this.sweepDelay + this.sweepDelay - attack); |
| 565 } |
| 566 |
| 567 overtoneGain *= 0.1 + 0.2 * k; |
| 568 } |
| 569 |
| 570 envelopeNode.gain.setValueAtTime(0, startTime); |
| 571 envelopeNode.gain.linearRampToValueAtTime(1, startTime + this.sweepDelay); |
| 572 envelopeNode.gain.setValueAtTime(1, startTime + dur - attack * 2); |
| 573 envelopeNode.gain.linearRampToValueAtTime(0, startTime + dur); |
| 574 envelopeNode.connect(destination); |
| 575 } |
| 576 }; |
| 577 |
| 578 /** |
| 579 * Play the "ChromeVox On" sound. |
| 580 */ |
| 581 EarconEngine.prototype.onChromeVoxOn = function() { |
| 582 this.onChromeVoxSweep(false); |
| 583 }; |
| 584 |
| 585 /** |
| 586 * Play the "ChromeVox Off" sound. |
| 587 */ |
| 588 EarconEngine.prototype.onChromeVoxOff = function() { |
| 589 this.onChromeVoxSweep(true); |
| 590 }; |
| 591 |
| 592 /** |
| 593 * Play an alert sound. |
| 594 */ |
| 595 EarconEngine.prototype.onAlert = function() { |
| 596 var freq1 = 220 * Math.pow(EarconEngine.HALF_STEP, this.alertPitch - 2); |
| 597 var freq2 = 220 * Math.pow(EarconEngine.HALF_STEP, this.alertPitch - 3); |
| 598 this.generateSinusoidal({attack: 0.02, |
| 599 decay: 0.07, |
| 600 dur: 0.15, |
| 601 gain: 0.3, |
| 602 freq: freq1, |
| 603 overtones: 3, |
| 604 overtoneFactor: 0.1}); |
| 605 this.generateSinusoidal({attack: 0.02, |
| 606 decay: 0.07, |
| 607 dur: 0.15, |
| 608 gain: 0.3, |
| 609 freq: freq2, |
| 610 overtones: 3, |
| 611 overtoneFactor: 0.1}); |
| 612 }; |
| 613 |
| 614 /** |
| 615 * Play a wrap sound. |
| 616 */ |
| 617 EarconEngine.prototype.onWrap = function() { |
| 618 this.play('static', {gain: this.clickVolume * 0.3}); |
| 619 var freq1 = 220 * Math.pow(EarconEngine.HALF_STEP, this.wrapPitch - 8); |
| 620 var freq2 = 220 * Math.pow(EarconEngine.HALF_STEP, this.wrapPitch + 8); |
| 621 this.generateSinusoidal({attack: 0.01, |
| 622 decay: 0.1, |
| 623 dur: 0.15, |
| 624 gain: 0.3, |
| 625 freq: freq1, |
| 626 endFreq: freq2, |
| 627 overtones: 1, |
| 628 overtoneFactor: 0.1}); |
| 629 }; |
| 630 |
| 631 /** |
| 632 * Queue up a few tick/tock sounds for a progress bar. This is called |
| 633 * repeatedly by setInterval to keep the sounds going continuously. |
| 634 * @private |
| 635 */ |
| 636 EarconEngine.prototype.generateProgressTickTocks_ = function() { |
| 637 while (this.progressTime_ < this.context_.currentTime + 3.0) { |
| 638 var t = this.progressTime_ - this.context_.currentTime; |
| 639 this.progressSources_.push( |
| 640 [this.progressTime_, |
| 641 this.play('static', |
| 642 {gain: 0.5 * this.progressGain_, |
| 643 time: t})]); |
| 644 this.progressSources_.push( |
| 645 [this.progressTime_, |
| 646 this.play(this.controlSound, |
| 647 {pitch: 20, |
| 648 time: t, |
| 649 gain: this.progressGain_})]); |
| 650 |
| 651 if (this.progressGain_ > this.progressFinalGain) { |
| 652 this.progressGain_ *= this.progressGain_Decay; |
| 653 } |
| 654 t += 0.5; |
| 655 |
| 656 this.progressSources_.push( |
| 657 [this.progressTime_, |
| 658 this.play('static', |
| 659 {gain: 0.5 * this.progressGain_, |
| 660 time: t})]); |
| 661 this.progressSources_.push( |
| 662 [this.progressTime_, |
| 663 this.play(this.controlSound, |
| 664 {pitch: 8, |
| 665 time: t, |
| 666 gain: this.progressGain_})]); |
| 667 |
| 668 if (this.progressGain_ > this.progressFinalGain) { |
| 669 this.progressGain_ *= this.progressGain_Decay; |
| 670 } |
| 671 |
| 672 this.progressTime_ += 1.0; |
| 673 } |
| 674 |
| 675 var removeCount = 0; |
| 676 while (removeCount < this.progressSources_.length && |
| 677 this.progressSources_[removeCount][0] < this.context_.currentTime - 0.2) { |
| 678 removeCount++; |
| 679 } |
| 680 this.progressSources_.splice(0, removeCount); |
| 681 }; |
| 682 |
| 683 /** |
| 684 * Start playing tick / tock progress sounds continuously until |
| 685 * explicitly canceled. |
| 686 */ |
| 687 EarconEngine.prototype.startProgress = function() { |
| 688 this.progressSources_ = []; |
| 689 this.progressGain_ = 0.5; |
| 690 this.progressTime_ = this.context_.currentTime; |
| 691 this.generateProgressTickTocks_(); |
| 692 this.progressIntervalID_ = window.setInterval( |
| 693 this.generateProgressTickTocks_.bind(this), 1000); |
| 694 }; |
| 695 |
| 696 /** |
| 697 * Stop playing any tick / tock progress sounds. |
| 698 */ |
| 699 EarconEngine.prototype.cancelProgress = function() { |
| 700 if (!this.progressIntervalID_) { |
| 701 return; |
| 702 } |
| 703 |
| 704 for (var i = 0; i < this.progressSources_.length; i++) { |
| 705 this.progressSources_[i][1].stop(); |
| 706 } |
| 707 this.progressSources_ = []; |
| 708 |
| 709 window.clearInterval(this.progressIntervalID_); |
| 710 this.progressIntervalID_ = null; |
| 711 }; |
OLD | NEW |