Chromium Code Reviews| Index: chrome/browser/resources/chromeos/chromevox/cvox2/background/earcon_engine.js |
| diff --git a/chrome/browser/resources/chromeos/chromevox/cvox2/background/earcon_engine.js b/chrome/browser/resources/chromeos/chromevox/cvox2/background/earcon_engine.js |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..185aa8c931acffb639631f79c5ba9891c8657cb5 |
| --- /dev/null |
| +++ b/chrome/browser/resources/chromeos/chromevox/cvox2/background/earcon_engine.js |
| @@ -0,0 +1,687 @@ |
| +// Copyright 2015 The Chromium Authors. All rights reserved. |
| +// Use of this source code is governed by a BSD-style license that can be |
| +// found in the LICENSE file. |
| + |
| +/** |
| + * @fileoverview This is the low-level class that generates ChromeVox's |
| + * earcons. It's designed to be self-contained and not depend on the |
| + * rest of the code. |
| + */ |
| + |
| +goog.provide('EarconEngine'); |
| + |
| +/** |
| + * EarconEngine generates ChromeVox's earcons using the web audio API. |
| + * @constructor |
| + */ |
| +EarconEngine = function() { |
| + // Public control parameters. All of these are meant to be adjustable. |
| + |
| + /** @type {number} The master volume, as an amplification factor. */ |
| + this.masterVolume = 0.2; |
| + |
| + /** @type {number} The base relative pitch adjustment, in half-steps. */ |
| + this.masterPitch = -4; |
| + |
| + /** @type {number} The click volume, as an amplification factor. */ |
| + this.clickVolume = 0.4; |
| + |
| + /** @type {number} The volume of the static sound, as an ampl factor. */ |
|
Peter Lundblad
2015/08/11 11:15:28
nit: be consistent ampl vs. amplification
dmazzoni
2015/09/14 18:06:37
Done.
|
| + this.staticVolume = 0.2; |
| + |
| + /** @type {number} The base delay for repeated sounds, in ms. */ |
| + this.baseDelay = 45; |
|
Peter Lundblad
2015/08/11 11:15:29
Since many other times are in whole seconds, shoul
dmazzoni
2015/09/14 18:06:37
Done.
|
| + |
| + /** @type {number} The master stereo panning, from -1 to 1. */ |
| + this.masterPan = 0; |
| + |
| + /** @type {number} The master reverb level as an ampl factor. */ |
| + this.masterReverb = 0.4; |
| + |
| + /** @type {string} The choice of the reverb impulse response to use. */ |
|
Peter Lundblad
2015/08/11 11:15:28
Say that this must we one of the items of EarconEn
dmazzoni
2015/09/14 18:06:37
Done.
|
| + this.reverbSound = 'small_room_2'; |
| + |
| + /** @type {number} The base pitch for the 'wrap' sound in half-steps. */ |
| + this.wrapPitch = 0; |
| + |
| + /** @type {number} The base pitch for the 'alert' sound in half-steps. */ |
| + this.alertPitch = 0; |
| + |
| + /** @type {string} The choice of base sound for most controls. */ |
| + this.controlSound = 'control'; |
| + |
| + /** @type {number} The delay between sounds in the on/off sweep effect. */ |
|
Peter Lundblad
2015/08/11 11:15:29
.. in ms ?
dmazzoni
2015/09/14 18:06:37
Done.
|
| + this.sweepDelay = 0.045; |
| + |
| + /** @type {number} The delay between echos in the on/off sweep. */ |
|
Peter Lundblad
2015/08/11 11:15:29
In ms ?
dmazzoni
2015/09/14 18:06:37
Done.
|
| + this.sweepEchoDelay = 0.15; |
| + |
| + /** @type {number} The number of echos in the on/off sweep. */ |
| + this.sweepEchoCount = 3; |
| + |
| + /** @type {number} The pitch offset of the on/off sweep, in half-steps. */ |
| + this.sweepPitch = -7; |
| + |
| + /** @type {number} The final gain of the progress sound, as an ampl factor. */ |
| + this.progressFinalGain = 0.05; |
| + |
| + /** @type {number} The multiplicative decay rate of the progress ticks. */ |
| + this.progressGain_Decay = 0.7; |
| + |
| + // Private variables. |
| + |
| + /** @type {AudioContext} @private */ |
| + this.context_ = new AudioContext(); |
| + |
| + /** |
| + * @type {Object<string, AudioBuffer>} A map between the name of an |
| + * audio data file and its loaded AudioBuffer. |
| + * @private |
| + */ |
| + this.buffers_ = {}; |
| + |
| + /** |
| + * The source audio nodes for queued tick / tocks for progress. |
| + * Kept around so they can be canceled. |
| + * |
| + * @type {?Array<AudioNode>} |
| + * @private |
| + */ |
| + this.progressSources_ = null; |
|
Peter Lundblad
2015/08/11 11:15:28
Do you need the distinction between null and empty
dmazzoni
2015/09/14 18:06:37
Done.
|
| + |
| + /** @type {number} The current gain for progress sounds. @private */ |
| + this.progressGain_ = 1.0; |
| + |
| + /** @type {?number} The current time for progress sounds. @private */ |
| + this.progressT_ = null; |
|
Peter Lundblad
2015/08/11 11:15:28
Is it very beneficial to use T instead of Time her
dmazzoni
2015/09/14 18:06:37
Done.
|
| + |
| + /** @type {?number} The window.setInterval ID for progress sounds. |
|
Peter Lundblad
2015/08/11 11:15:29
nit: start and end comment tokens on their own lin
dmazzoni
2015/09/14 18:06:37
Done.
|
| + * @private */ |
| + this.progressIntervalID_ = null; |
| + |
| + // Initialization: load the base sound data files asynchronously. |
| + EarconEngine.SOUNDS.forEach((function(sound) { |
|
Peter Lundblad
2015/08/11 11:15:28
Could save some repetitive code by concatenating t
dmazzoni
2015/09/14 18:06:37
Done.
|
| + var url = EarconEngine.BASE_URL + sound + '.wav'; |
| + this.loadSound(sound, url); |
| + }).bind(this)); |
| + EarconEngine.REVERBS.forEach((function(reverb) { |
| + var url = EarconEngine.BASE_URL + reverb + '.wav'; |
| + this.loadSound(reverb, url); |
| + }).bind(this)); |
| +}; |
| + |
| +/** |
| + * @type {Array<string>} The list of sound data files to load. |
|
Peter Lundblad
2015/08/11 11:15:29
@const
dmazzoni
2015/09/14 18:06:37
Done.
|
| + */ |
| +EarconEngine.SOUNDS = [ |
| + 'control', |
| + 'selection', |
| + 'selection_reverse', |
| + 'skim', |
| + 'static']; |
| + |
| +/** |
| + * @type {Array<string>} The list of reverb data files to load. |
|
Peter Lundblad
2015/08/11 11:15:28
@const
dmazzoni
2015/09/14 18:06:37
Done.
|
| + */ |
| +EarconEngine.REVERBS = [ |
| + 'small_room_2']; |
| + |
| +/** |
| + * @type {number} The scale factor for one half-step. |
|
Peter Lundblad
2015/08/11 11:15:28
@const
dmazzoni
2015/09/14 18:06:37
Done.
|
| + */ |
| +EarconEngine.HALF_STEP = Math.pow(2.0, 1.0 / 12.0); |
| + |
| +/** |
| + * @type {string} The base url for earcon sound resources. |
|
Peter Lundblad
2015/08/11 11:15:28
@cosnt
dmazzoni
2015/09/14 18:06:37
Done.
|
| + */ |
| +EarconEngine.BASE_URL = chrome.extension.getURL('cvox2/background/earcons/'); |
| + |
| +/** |
| + * Fetches a sound asynchronously and loads its data into an AudioBuffer. |
| + * |
| + * @param {string} sound The name of the sound to load. |
|
Peter Lundblad
2015/08/11 11:15:29
nit: call name?
dmazzoni
2015/09/14 18:06:37
Done.
|
| + * @param {string} url The url where the sound should be fetched from. |
| + */ |
| +EarconEngine.prototype.loadSound = function(sound, url) { |
| + var request = new XMLHttpRequest(); |
| + request.open('GET', url, true); |
| + request.responseType = 'arraybuffer'; |
| + |
| + // Decode asynchronously. |
| + request.onload = (function() { |
| + this.context_.decodeAudioData( |
| + /** @type {ArrayBuffer} */ (request.response), |
| + (function(buffer) { |
| + this.buffers_[sound] = buffer; |
| + }).bind(this)); |
| + }).bind(this); |
| + request.send(); |
| +}; |
| + |
| +/** |
| + * Return an AudioNode containing the final processing that all |
| + * sounds go through: master volume / gain, panning, and reverb. |
| + * The chain is hooked up to the destination automatically, so you |
| + * just need to connect your source to the return value from this |
| + * method. |
| + * |
| + * @param {{gain: (number | undefined), |
| + * pan: (number | undefined), |
| + * reverb: (number | undefined)}} properties |
| + * An object where you can override the default |
| + * gain, pan, and reverb, otherwise these are taken from |
| + * masterVolume, masterPan, and masterReverb. |
| + * @return {AudioNode} The filters to be applied to all sounds, connected |
| + * to the destination node. |
| + */ |
| +EarconEngine.prototype.createCommonFilters = function(properties) { |
| + var gain = this.masterVolume; |
| + if (properties.gain) { |
| + gain *= properties.gain; |
| + } |
| + var gainNode = this.context_.createGain(); |
| + gainNode.gain.value = gain; |
| + var first = gainNode; |
| + var last = gainNode; |
| + |
| + var pan = this.masterPan; |
| + if (properties.pan) { |
|
Peter Lundblad
2015/08/11 11:15:29
Compare to undefined here. Else, a caller can't se
dmazzoni
2015/09/14 18:06:37
Good catch.
|
| + pan = properties.pan; |
| + } |
| + if (pan != 0) { |
| + var panNode = this.context_.createPanner(); |
| + panNode.setPosition(pan, 0, -1); |
| + panNode.setOrientation(0, 0, 1); |
| + last.connect(panNode); |
| + last = panNode; |
| + } |
| + |
| + var reverb = this.masterReverb; |
| + if (properties.reverb) |
| + reverb = properties.reverb; |
|
Peter Lundblad
2015/08/11 11:15:28
Indentation and please be consistent with one-line
dmazzoni
2015/09/14 18:06:37
Done.
|
| + if (reverb) { |
| + var filter = this.context_.createConvolver(); |
| + filter.buffer = this.buffers_[this.reverbSound]; |
| + |
| + // Dry |
| + last.connect(this.context_.destination); |
| + |
| + // Wet |
| + var reverbGainNode = this.context_.createGain(); |
| + reverbGainNode.gain.value = reverb; |
| + last.connect(reverbGainNode); |
| + reverbGainNode.connect(filter); |
| + filter.connect(this.context_.destination); |
| + } else { |
| + last.connect(this.context_.destination); |
| + } |
| + |
| + return first; |
| +}; |
| + |
| +/** |
| + * High-level interface to play a sound from a buffer source by name, |
| + * with some simple adjustments like pitch change (in half-steps), |
| + * a start time (relative to the current time, in seconds), |
| + * gain, panning, and reverb. |
| + * |
| + * The only required parameter is the name of the sound. The time, pitch, |
| + * gain, panning, and reverb are all optional and are passed in an |
| + * object of optional properties. |
| + * |
| + * @param {string} sound The name of the sound to play. It must already |
| + * be loaded in a buffer. |
| + * @param {{pitch: (number | undefined), |
| + * time: (number | undefined), |
| + * gain: (number | undefined), |
| + * pan: (number | undefined), |
| + * reverb: (number | undefined)}=} opt_properties |
| + * An object where you can override |
| + * the default pitch, gain, pan, and reverb. |
|
Peter Lundblad
2015/08/11 11:15:29
nit: odd line break above. (short line)
dmazzoni
2015/09/14 18:06:37
Done.
|
| + * @return {AudioBufferSourceNode} The source node, so you can stop it |
| + * or set event handlers on it. |
| + */ |
| +EarconEngine.prototype.play = function(sound, opt_properties) { |
| + var source = this.context_.createBufferSource(); |
| + source.buffer = this.buffers_[sound]; |
| + |
| + if (!opt_properties) { |
| + // This typecast looks silly, but the Closure compiler doesn't support |
| + // optional fields in record types very well so this is the shortest hack. |
| + opt_properties = /** @type {undefined} */({}); |
| + } |
| + |
| + var pitch = this.masterPitch; |
| + if (opt_properties.pitch) { |
| + pitch += opt_properties.pitch; |
| + } |
| + if (pitch != 0) { |
| + source.playbackRate.value = Math.pow(EarconEngine.HALF_STEP, pitch); |
| + } |
| + |
| + var destination = this.createCommonFilters(opt_properties); |
| + source.connect(destination); |
| + |
| + if (opt_properties.time) { |
| + source.start(this.context_.currentTime + opt_properties.time); |
| + } else { |
| + source.start(this.context_.currentTime); |
| + } |
| + |
| + return source; |
| +}; |
| + |
| +/** |
| + * Play the static sound. |
| + */ |
| +EarconEngine.prototype.onStatic = function() { |
| + this.play('static', {gain: this.staticVolume}); |
| +}; |
| + |
| +/** |
| + * Play the link sound. |
| + */ |
| +EarconEngine.prototype.onLink = function() { |
| + this.play('static', {gain: this.clickVolume}); |
| + this.play(this.controlSound, {pitch: 12}); |
| +}; |
| + |
| +/** |
| + * Play the button sound. |
| + */ |
| +EarconEngine.prototype.onButton = function() { |
| + this.play('static', {gain: this.clickVolume}); |
| + this.play(this.controlSound); |
| +}; |
| + |
| +/** |
| + * Play the text field sound. |
| + */ |
| +EarconEngine.prototype.onTextField = function() { |
| + this.play('static', {gain: this.clickVolume}); |
| + this.play('static', {time: this.baseDelay * 0.0015, |
| + gain: this.clickVolume * 0.5}); |
| + this.play(this.controlSound, {pitch: 4}); |
| + this.play(this.controlSound, |
| + {pitch: 4, |
| + time: this.baseDelay * 0.0015, |
| + gain: 0.5}); |
| +}; |
| + |
| +/** |
| + * Play the pop up button sound. |
| + */ |
| +EarconEngine.prototype.onPopUpButton = function() { |
| + this.play('static', {gain: this.clickVolume}); |
| + |
| + this.play(this.controlSound); |
| + this.play(this.controlSound, |
| + {time: this.baseDelay * 0.003, |
| + gain: 0.2, |
| + pitch: 12}); |
| + this.play(this.controlSound, |
| + {time: this.baseDelay * 0.0045, |
| + gain: 0.2, |
| + pitch: 12}); |
| +}; |
| + |
| +/** |
| + * Play the check on sound. |
| + */ |
| +EarconEngine.prototype.onCheckOn = function() { |
| + this.play('static', {gain: this.clickVolume}); |
| + this.play(this.controlSound, {pitch: -5}); |
| + this.play(this.controlSound, {pitch: 7, time: this.baseDelay * 0.002}); |
| +}; |
| + |
| +/** |
| + * Play the check off sound. |
| + */ |
| +EarconEngine.prototype.onCheckOff = function() { |
| + this.play('static', {gain: this.clickVolume}); |
| + this.play(this.controlSound, {pitch: 7}); |
| + this.play(this.controlSound, {pitch: -5, time: this.baseDelay * 0.002}); |
| +}; |
| + |
| +/** |
| + * Play the select control sound. |
| + */ |
| +EarconEngine.prototype.onSelect = function() { |
| + this.play('static', {gain: this.clickVolume}); |
| + this.play(this.controlSound); |
| + this.play(this.controlSound, {time: this.baseDelay * 0.001}); |
| + this.play(this.controlSound, {time: this.baseDelay * 0.002}); |
| +}; |
| + |
| +/** |
| + * Play the slider sound. |
| + */ |
| +EarconEngine.prototype.onSlider = function() { |
| + this.play('static', {gain: this.clickVolume}); |
| + this.play(this.controlSound); |
| + this.play(this.controlSound, |
| + {time: this.baseDelay * 0.001, |
| + gain: 0.5, |
| + pitch: 2}); |
| + this.play(this.controlSound, |
| + {time: this.baseDelay * 0.002, |
| + gain: 0.25, |
| + pitch: 4}); |
| + this.play(this.controlSound, |
| + {time: this.baseDelay * 0.003, |
| + gain: 0.125, |
| + pitch: 6}); |
| + this.play(this.controlSound, |
| + {time: this.baseDelay * 0.004, |
| + gain: 0.0625, |
| + pitch: 8}); |
| +}; |
| + |
| +/** |
| + * Play the skim sound. |
| + */ |
| +EarconEngine.prototype.onSkim = function() { |
| + this.play('skim'); |
| +}; |
| + |
| +/** |
| + * Play the selection sound. |
| + */ |
| +EarconEngine.prototype.onSelection = function() { |
| + this.play('selection'); |
| +}; |
| + |
| +/** |
| + * Play the selection reverse sound. |
| + */ |
| +EarconEngine.prototype.onSelectionReverse = function() { |
| + this.play('selection_reverse'); |
| +}; |
| + |
| +/** |
| + * Generate a synthesized musical note based on a sum of sinusoidals shaped |
| + * by an envelope, controlled by a number of properties. |
| + * |
| + * The sound has a frequency of |freq|, or if |endFreq| is specified, does |
| + * an exponential ramp from |freq| to |endFreq|. |
| + * |
| + * If |overtones| is greater than 1, the sound will be mixed with additional |
| + * sinusoidals at multiples of |freq|, each one scaled by |overtoneFactor|. |
| + * This creates a rounder tone than a pure sine wave. |
| + * |
| + * The envelope is shaped by the duration |dur|, the attack time |attack|, |
| + * and the decay time |decay|, in seconds. |
| + * |
| + * As with other functions, |pan| and |reverb| can be used to override |
| + * masterPan and masterReverb. |
| + * |
| + * @param {{gain: number, |
| + * freq: number, |
| + * endFreq: (number | undefined), |
| + * time: (number | undefined), |
| + * overtones: (number | undefined), |
| + * overtoneFactor: (number | undefined), |
| + * dur: (number | undefined), |
| + * attack: (number | undefined), |
| + * decay: (number | undefined), |
| + * pan: (number | undefined), |
| + * reverb: (number | undefined)}} properties |
| + * An object containing the properties that can be used to |
| + * control the sound, as described above. |
| + */ |
| +EarconEngine.prototype.generateSinusoidal = function(properties) { |
| + var envelopeNode = this.context_.createGain(); |
| + envelopeNode.connect(this.context_.destination); |
| + |
| + if (!properties.time) { |
|
Peter Lundblad
2015/08/11 11:15:28
Can you avoid modifying the passed in properties?
dmazzoni
2015/09/14 18:06:37
Done.
|
| + properties.time = 0; |
| + } |
| + |
| + // Generate an oscillator for the frequency corresponding to the specified |
| + // frequency, and then additional overtones at multiples of that frequency |
| + // scaled by the overtoneFactor. Cue the oscillator to start and stop |
| + // based on the start time and specified duration. |
| + // |
| + // If an end frequency is specified, do an exponential ramp to that end |
| + // frequency. |
| + var gain = properties.gain; |
| + for (var i = 0; i < properties.overtones; i++) { |
| + var osc = this.context_.createOscillator(); |
| + osc.frequency.value = properties.freq * (i + 1); |
| + |
| + if (properties.endFreq) { |
| + osc.frequency.setValueAtTime( |
| + properties.freq * (i + 1), |
| + this.context_.currentTime + properties.time); |
| + osc.frequency.exponentialRampToValueAtTime( |
| + properties.endFreq * (i + 1), |
| + this.context_.currentTime + properties.dur); |
| + } |
| + |
| + osc.start(this.context_.currentTime + properties.time); |
| + osc.stop(this.context_.currentTime + properties.time + properties.dur); |
| + |
| + var gainNode = this.context_.createGain(); |
| + gainNode.gain.value = gain; |
| + osc.connect(gainNode); |
| + gainNode.connect(envelopeNode); |
| + |
| + gain *= properties.overtoneFactor; |
| + } |
| + |
| + // Shape the overall sound by an envelope based on the attack and |
| + // decay times. |
| + envelopeNode.gain.setValueAtTime( |
| + 0, this.context_.currentTime + properties.time); |
| + envelopeNode.gain.linearRampToValueAtTime( |
| + 1, this.context_.currentTime + properties.time + properties.attack); |
| + envelopeNode.gain.setValueAtTime( |
| + 1, this.context_.currentTime + properties.time + |
| + properties.dur - properties.decay); |
| + envelopeNode.gain.linearRampToValueAtTime( |
| + 0, this.context_.currentTime + properties.time + properties.dur); |
| + |
| + // Route everything through the common filters like reverb at the end. |
| + var destination = this.createCommonFilters({}); |
| + envelopeNode.connect(destination); |
| +}; |
| + |
| +/** |
| + * Play a sweep over a bunch of notes in a scale, with an echo, |
| + * for the ChromeVox on or off sounds. |
| + * |
| + * @param {boolean} reverse Whether to play in the reverse direction. |
| + */ |
| +EarconEngine.prototype.onChromeVoxSweep = function(reverse) { |
| + var pitches = [-7, -5, 0, 5, 7, 12, 17, 19, 24]; |
| + |
| + if (reverse) { |
| + pitches.reverse(); |
| + } |
| + |
| + var attack = 0.015; |
| + var dur = pitches.length * this.sweepDelay; |
| + |
| + var destination = this.createCommonFilters({reverb: 2.0}); |
| + for (var k = 0; k < this.sweepEchoCount; k++) { |
| + var envelopeNode = this.context_.createGain(); |
| + var startTime = this.context_.currentTime + this.sweepEchoDelay * k; |
| + var sweepGain = Math.pow(0.3, k); |
| + var overtones = 2; |
| + var overtoneGain = sweepGain; |
| + for (var i = 0; i < overtones; i++) { |
| + var osc = this.context_.createOscillator(); |
| + osc.start(startTime); |
| + osc.stop(startTime + dur); |
| + |
| + var gainNode = this.context_.createGain(); |
| + osc.connect(gainNode); |
| + gainNode.connect(envelopeNode); |
| + |
| + for (var j = 0; j < pitches.length; j++) { |
| + var freqDecay; |
| + if (reverse) |
| + freqDecay = Math.pow(0.75, pitches.length - j); |
|
Peter Lundblad
2015/08/11 11:15:29
Indentation.
dmazzoni
2015/09/14 18:06:37
Done.
|
| + else |
| + freqDecay = Math.pow(0.75, j); |
| + var gain = overtoneGain * freqDecay; |
| + var freq = (i + 1) * 220 * |
| + Math.pow(EarconEngine.HALF_STEP, pitches[j] + this.sweepPitch); |
| + if (j == 0) { |
| + osc.frequency.setValueAtTime(freq, startTime + j * this.sweepDelay); |
| + gainNode.gain.setValueAtTime(gain, startTime + j * this.sweepDelay); |
|
Peter Lundblad
2015/08/11 11:15:28
Seems like the second arguments on these two lines
dmazzoni
2015/09/14 18:06:37
Done.
|
| + } else { |
| + osc.frequency.exponentialRampToValueAtTime( |
| + freq, startTime + j * this.sweepDelay); |
| + gainNode.gain.linearRampToValueAtTime( |
| + gain, startTime + j * this.sweepDelay); |
| + } |
| + osc.frequency.setValueAtTime( |
| + freq, startTime + j * this.sweepDelay + this.sweepDelay - attack); |
| + } |
| + |
| + overtoneGain *= 0.1 + 0.2 * k; |
| + } |
| + |
| + envelopeNode.gain.setValueAtTime(0, startTime); |
| + envelopeNode.gain.linearRampToValueAtTime(1, startTime + this.sweepDelay); |
| + envelopeNode.gain.setValueAtTime(1, startTime + dur - attack * 2); |
| + envelopeNode.gain.linearRampToValueAtTime(0, startTime + dur); |
| + envelopeNode.connect(destination); |
| + } |
| +}; |
| + |
| +/** |
| + * Play the "ChromeVox On" sound. |
| + */ |
| +EarconEngine.prototype.onChromeVoxOn = function() { |
| + this.onChromeVoxSweep(false); |
| +}; |
| + |
| +/** |
| + * Play the "ChromeVox Off" sound. |
| + */ |
| +EarconEngine.prototype.onChromeVoxOff = function() { |
| + this.onChromeVoxSweep(true); |
| +}; |
| + |
| +/** |
| + * Play an alert sound. |
| + */ |
| +EarconEngine.prototype.onAlert = function() { |
| + var freq1 = 220 * Math.pow(EarconEngine.HALF_STEP, this.alertPitch - 2); |
| + var freq2 = 220 * Math.pow(EarconEngine.HALF_STEP, this.alertPitch - 3); |
| + this.generateSinusoidal({attack: 0.02, |
| + decay: 0.07, |
| + dur: 0.15, |
| + gain: 0.3, |
| + freq: freq1, |
| + overtones: 3, |
| + overtoneFactor: 0.1}); |
| + this.generateSinusoidal({attack: 0.02, |
| + decay: 0.07, |
| + dur: 0.15, |
| + gain: 0.3, |
| + freq: freq2, |
| + overtones: 3, |
| + overtoneFactor: 0.1}); |
| +}; |
| + |
| +/** |
| + * Play a wrap sound. |
| + */ |
| +EarconEngine.prototype.onWrap = function() { |
| + this.play('static', {gain: this.clickVolume * 0.3}); |
| + var freq1 = 220 * Math.pow(EarconEngine.HALF_STEP, this.wrapPitch - 8); |
| + var freq2 = 220 * Math.pow(EarconEngine.HALF_STEP, this.wrapPitch + 8); |
| + this.generateSinusoidal({attack: 0.01, |
| + decay: 0.1, |
| + dur: 0.15, |
| + gain: 0.3, |
| + freq: freq1, |
| + endFreq: freq2, |
| + overtones: 1, |
| + overtoneFactor: 0.1}); |
| +}; |
| + |
| +/** |
| + * Queue up a few tick/tock sounds for a progress bar. This is called |
| + * repeatedly by setInterval to keep the sounds going continuously. |
| + * @private |
| + */ |
| +EarconEngine.prototype.generateProgressTickTocks_ = function() { |
| + while (this.progressT_ < this.context_.currentTime + 3.0) { |
| + var t = this.progressT_ - this.context_.currentTime; |
| + this.progressSources_.push( |
| + [this.progressT_, |
| + this.play('static', |
| + {gain: 0.5 * this.progressGain_, |
| + time: t})]); |
| + this.progressSources_.push( |
| + [this.progressT_, |
| + this.play(this.controlSound, |
| + {pitch: 20, |
| + time: t, |
| + gain: this.progressGain_})]); |
| + |
| + if (this.progressGain_ > this.progressFinalGain) { |
| + this.progressGain_ *= this.progressGain_Decay; |
| + } |
| + t += 0.5; |
| + |
| + this.progressSources_.push( |
| + [this.progressT_, |
| + this.play('static', |
| + {gain: 0.5 * this.progressGain_, |
| + time: t})]); |
| + this.progressSources_.push( |
| + [this.progressT_, |
| + this.play(this.controlSound, |
| + {pitch: 8, |
| + time: t, |
| + gain: this.progressGain_})]); |
| + |
| + if (this.progressGain_ > this.progressFinalGain) { |
| + this.progressGain_ *= this.progressGain_Decay; |
| + } |
| + |
| + this.progressT_ += 1.0; |
| + } |
| + |
| + var removeCount = 0; |
| + while (removeCount < this.progressSources_.length && |
| + this.progressSources_[removeCount][0] < this.context_.currentTime - 0.2) { |
| + removeCount++; |
| + } |
| + this.progressSources_.splice(0, removeCount); |
| +}; |
| + |
| +/** |
| + * Start playing tick / tock progress sounds continuously until |
| + * explicitly canceled. |
| + */ |
| +EarconEngine.prototype.startProgress = function() { |
| + this.progressSources_ = []; |
| + this.progressGain_ = 0.5; |
| + this.progressT_ = this.context_.currentTime; |
| + this.generateProgressTickTocks_(); |
| + this.progressIntervalID_ = window.setInterval( |
| + this.generateProgressTickTocks_.bind(this), 1000); |
| +}; |
| + |
| +/** |
| + * Stop playing any tick / tock progress sounds. |
| + */ |
| +EarconEngine.prototype.cancelProgress = function() { |
| + if (!this.progressIntervalID_) { |
| + return; |
| + } |
| + |
| + for (var i = 0; i < this.progressSources_.length; i++) { |
| + this.progressSources_[i][1].stop(); |
| + } |
| + this.progressSources_ = []; |
| + |
| + window.clearInterval(this.progressIntervalID_); |
| + this.progressIntervalID_ = null; |
| +}; |