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