OLD | NEW |
---|---|
(Empty) | |
1 // Copyright 2013 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 | |
7 * This is a component extension that implements a speech engine | |
8 * powered by Google's speech synthesis API. | |
Evan Stade
2013/10/22 22:32:13
I don't see where TTS is defined in this file. Exp
dmazzoni
2013/10/22 22:45:31
Clarified the acronym here, thanks.
| |
9 * | |
10 * This is an "event page", so it's not loaded when the API isn't being used, | |
11 * and doesn't waste resources. When a web page or web app makes a speech | |
12 * request and the parameters match one of the voices in this extension's | |
13 * manifest, it makes a request to Google's API using Chrome's private key | |
14 * and plays the resulting speech using HTML5 audio. | |
15 */ | |
16 | |
17 /** | |
18 * The main class for this extension. Adds listeners to | |
19 * chrome.ttsEngine.onSpeak and chrome.ttsEngine.onStop and implements | |
20 * them using Google's speech synthesis API. | |
21 * @constructor | |
22 */ | |
23 function TtsExtension() { | |
Evan Stade
2013/10/22 22:32:13
nit: {} on one line
dmazzoni
2013/10/22 22:45:31
Done.
| |
24 } | |
25 | |
26 TtsExtension.prototype = { | |
27 /** | |
28 * The url prefix of the speech server, including static query | |
29 * parameters that don't change. | |
30 * @type {string} | |
31 * @const | |
32 * @private | |
33 */ | |
34 SPEECH_SERVER_URL_: | |
35 'https://www.google.com/speech-api/v2/synthesize?enc=mpeg&client=chromium' , | |
Evan Stade
2013/10/22 22:32:13
80
dmazzoni
2013/10/22 22:45:31
Done.
| |
36 | |
37 /** | |
38 * A mapping from language and gender to voice name, hardcoded for now | |
39 * until the speech synthesis server capabilities response provides this. | |
40 * The key of this map is of the form '<lang>-<gender>'. | |
41 * @type {Object.<string, string>} | |
42 * @private | |
43 */ | |
44 LANG_AND_GENDER_TO_VOICE_NAME_: { | |
45 'en-gb-male': 'rjs', | |
46 'en-gb-female': 'fis', | |
47 }, | |
48 | |
49 /** | |
50 * The arguments passed to the onSpeak event handler for the utterance | |
51 * that's currently being spoken. Should be null when no object is | |
52 * pending. | |
53 * | |
54 * @type {?{utterance: string, options: Object, callback: Function}} | |
55 * @private | |
56 */ | |
57 currentUtterance_: null, | |
58 | |
59 /** | |
60 * The HTML5 audio element we use for playing the sound served by the | |
61 * speech server. | |
62 * @type {HTMLAudioElement} | |
63 * @private | |
64 */ | |
65 audioElement_: null, | |
66 | |
67 /** | |
68 * A mapping from voice name to language and gender, derived from the | |
69 * manifest file. This is used in case the speech synthesis request | |
70 * specifies a voice name but doesn't specify a language code or gender. | |
71 * @type {Object.<string, {lang: string, gender: string}>} | |
72 * @private | |
73 */ | |
74 voiceNameToLangAndGender_: {}, | |
75 | |
76 /** | |
77 * This is the main function called to initialize this extension. | |
78 * Initializes data structures and adds event listeners. | |
79 */ | |
80 init: function() { | |
81 // Get voices from manifest. | |
82 var voices = chrome.app.getDetails().tts_engine.voices; | |
83 for (var i = 0; i < voices.length; i++) { | |
84 this.voiceNameToLangAndGender_[voices[i].voice_name] = { | |
85 lang: voices[i].lang, | |
86 gender: voices[i].gender | |
87 }; | |
88 } | |
89 | |
90 // Initialize the audio element and event listeners on it. | |
91 this.audioElement_ = document.createElement('audio'); | |
92 document.body.appendChild(this.audioElement_); | |
93 this.audioElement_.addEventListener( | |
94 'ended', this.onStop_.bind(this), false); | |
95 this.audioElement_.addEventListener( | |
96 'canplaythrough', this.onStart_.bind(this), false); | |
97 | |
98 // Install event listeners for the ttsEngine API. | |
99 chrome.ttsEngine.onSpeak.addListener(this.onSpeak_.bind(this)); | |
100 chrome.ttsEngine.onStop.addListener(this.onStop_.bind(this)); | |
101 chrome.ttsEngine.onPause.addListener(this.onPause_.bind(this)); | |
102 chrome.ttsEngine.onResume.addListener(this.onResume_.bind(this)); | |
103 }, | |
104 | |
105 /** | |
106 * Handler for the chrome.ttsEngine.onSpeak interface. | |
107 * Gets Chrome's Google API key and then uses it to generate a request | |
108 * url for the requested speech utterance. Sets that url as the source | |
109 * of the HTML5 audio element. | |
110 * @param {string} utterance The text to be spoken. | |
111 * @param {Object} options Options to control the speech, as defined | |
112 * in the Chrome ttsEngine extension API. | |
113 * @private | |
114 */ | |
115 onSpeak_: function(utterance, options, callback) { | |
116 // Truncate the utterance if it's too long. Both Chrome's tts | |
117 // extension api and the web speech api specify 32k as the | |
118 // maximum limit for an utterance. | |
119 if (utterance.length > 32768) | |
120 utterance = utterance.substr(0, 32768); | |
121 | |
122 try { | |
123 // Firsttop any pending audio. | |
Evan Stade
2013/10/22 22:32:13
First stop?
dmazzoni
2013/10/22 22:45:31
Done.
| |
124 this.onStop_(); | |
125 | |
126 this.currentUtterance_ = { | |
127 utterance: utterance, | |
128 options: options, | |
129 callback: callback | |
130 }; | |
131 | |
132 var lang = options.lang; | |
133 var gender = options.gender; | |
134 if (options.voiceName) { | |
135 lang = this.voiceNameToLangAndGender_[options.voiceName].lang; | |
136 gender = this.voiceNameToLangAndGender_[options.voiceName].gender; | |
137 } | |
138 | |
139 // Look up the specific voice name for this language and gender. | |
140 // If it's not in the map, it doesn't matter - the language will | |
141 // be used directly. This is only used for languages where more | |
142 // than one gender is actually available. | |
143 var key = lang.toLowerCase() + '-' + gender; | |
144 var voiceName = this.LANG_AND_GENDER_TO_VOICE_NAME_[key]; | |
145 | |
146 var url = this.SPEECH_SERVER_URL_; | |
147 chrome.systemPrivate.getApiKey((function(key) { | |
148 url += '&key=' + key; | |
149 url += '&text=' + escape(utterance); | |
150 url += '&lang=' + lang.toLowerCase(); | |
151 if (voiceName) | |
Evan Stade
2013/10/22 22:32:13
nit: more vertical whitespace somewhere in this vi
dmazzoni
2013/10/22 22:45:31
Done.
| |
152 url += '&name=' + voiceName; | |
153 if (options.rate) { | |
154 // Input rate is between 0.1 and 10.0 with a default of 1.0. | |
155 // Output speed is between 0.0 and 1.0 with a default of 0.5. | |
156 url += '&speed=' + (options.rate / 2.0); | |
157 } | |
158 if (options.pitch) { | |
159 // Input pitch is between 0.0 and 2.0 with a default of 1.0. | |
160 // Output pitch is between 0.0 and 1.0 with a default of 0.5. | |
161 url += '&pitch=' + (options.pitch / 2.0); | |
162 } | |
163 | |
164 // This begins loading the audio but does not play it. | |
165 // When enough of the audio has loaded to begin playback, | |
166 // the 'canplaythrough' handler will call this.onStart_, | |
167 // which sends a start event to the ttsEngine callback and | |
168 // then begins playing audio. | |
169 this.audioElement_.src = url; | |
170 }).bind(this)); | |
171 } catch (err) { | |
172 console.error(String(err)); | |
173 callback({ | |
174 'type': 'error', | |
175 'errorMessage': String(err) | |
176 }); | |
177 this.currentUtterance_ = null; | |
178 } | |
179 }, | |
180 | |
181 /** | |
182 * Handler for the chrome.ttsEngine.onStop interface. | |
183 * Called either when the ttsEngine API requests us to stop, or when | |
184 * we reach the end of the audio stream. Pause the audio element to | |
185 * silence it, and send a callback to the ttsEngine API to let it know | |
186 * that we've completed. Note that the ttsEngine API manages callback | |
187 * messages and will automatically replace the 'end' event with a | |
188 * more specific callback like 'interrupted' when sending it to the | |
189 * TTS client. | |
190 * @private | |
191 */ | |
192 onStop_: function() { | |
193 if (this.currentUtterance_) { | |
194 this.audioElement_.pause(); | |
195 this.currentUtterance_.callback({ | |
196 'type': 'end', | |
197 'charIndex': this.currentUtterance_.utterance.length | |
198 }); | |
199 } | |
200 this.currentUtterance_ = null; | |
201 }, | |
202 | |
203 /** | |
204 * Handler for the canplaythrough event on the audio element. | |
205 * Called when the audio element has buffered enough audio to begin | |
206 * playback. Send the 'start' event to the ttsEngine callback and | |
207 * then begin playing the audio element. | |
208 * @private | |
209 */ | |
210 onStart_: function() { | |
211 if (this.currentUtterance_) { | |
212 if (this.currentUtterance_.options.volume !== undefined) { | |
213 // Both APIs use the same range for volume, between 0.0 and 1.0. | |
214 this.audioElement_.volume = this.currentUtterance_.options.volume; | |
215 } | |
216 this.audioElement_.play(); | |
217 this.currentUtterance_.callback({ | |
218 'type': 'start', | |
219 'charIndex': 0 | |
220 }); | |
221 } | |
222 }, | |
223 | |
224 /** | |
225 * Handler for the chrome.ttsEngine.onPause interface. | |
226 * Pauses audio if we're in the middle of an utterance. | |
227 * @private | |
228 */ | |
229 onPause_: function() { | |
230 if (this.currentUtterance_) { | |
231 this.audioElement_.pause(); | |
232 } | |
233 }, | |
234 | |
235 /** | |
236 * Handler for the chrome.ttsEngine.onPause interface. | |
237 * Resumes audio if we're in the middle of an utterance. | |
238 * @private | |
239 */ | |
240 onResume_: function() { | |
241 if (this.currentUtterance_) { | |
242 this.audioElement_.play(); | |
243 } | |
244 } | |
245 | |
246 }; | |
247 | |
248 (new TtsExtension()).init(); | |
OLD | NEW |