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 The class to Manage both offline / online speech recognition. 7 */ 8 9<include src="plugin_manager.js"/> 10<include src="audio_manager.js"/> 11<include src="speech_recognition_manager.js"/> 12 13cr.define('speech', function() { 14 'use strict'; 15 16 /** 17 * The state of speech recognition. 18 * 19 * @enum {string} 20 */ 21 var SpeechState = { 22 READY: 'READY', 23 HOTWORD_RECOGNIZING: 'HOTWORD_RECOGNIZING', 24 RECOGNIZING: 'RECOGNIZING', 25 IN_SPEECH: 'IN_SPEECH', 26 STOPPING: 'STOPPING', 27 NETWORK_ERROR: 'NETWORK_ERROR' 28 }; 29 30 /** 31 * The time to show the network error message in seconds. 32 * 33 * @const {number} 34 */ 35 var SPEECH_ERROR_TIMEOUT = 3; 36 37 /** 38 * Checks the prefix for the hotword module based on the language. This is 39 * fragile if the file structure has changed. 40 */ 41 function getHotwordPrefix() { 42 var prefix = navigator.language.toLowerCase(); 43 if (prefix == 'en-gb') 44 return prefix; 45 var hyphen = prefix.indexOf('-'); 46 if (hyphen >= 0) 47 prefix = prefix.substr(0, hyphen); 48 if (prefix == 'en') 49 prefix = ''; 50 return prefix; 51 } 52 53 /** 54 * @constructor 55 */ 56 function SpeechManager() { 57 this.audioManager_ = new speech.AudioManager(); 58 this.audioManager_.addEventListener('audio', this.onAudioLevel_.bind(this)); 59 this.speechRecognitionManager_ = new speech.SpeechRecognitionManager(this); 60 this.errorTimeoutId_ = null; 61 } 62 63 /** 64 * Updates the state. 65 * 66 * @param {SpeechState} newState The new state. 67 * @private 68 */ 69 SpeechManager.prototype.setState_ = function(newState) { 70 if (this.state == newState) 71 return; 72 73 this.state = newState; 74 chrome.send('setSpeechRecognitionState', [this.state]); 75 }; 76 77 /** 78 * Called with the mean audio level when audio data arrives. 79 * 80 * @param {cr.event.Event} event The event object for the audio data. 81 * @private 82 */ 83 SpeechManager.prototype.onAudioLevel_ = function(event) { 84 var data = event.data; 85 var level = 0; 86 for (var i = 0; i < data.length; ++i) 87 level += Math.abs(data[i]); 88 level /= data.length; 89 chrome.send('speechSoundLevel', [level]); 90 }; 91 92 /** 93 * Called when the hotword recognizer is ready. 94 * 95 * @param {PluginManager} pluginManager The hotword plugin manager which gets 96 * ready. 97 * @private 98 */ 99 SpeechManager.prototype.onHotwordRecognizerReady_ = function(pluginManager) { 100 this.pluginManager_ = pluginManager; 101 this.audioManager_.addEventListener( 102 'audio', pluginManager.sendAudioData.bind(pluginManager)); 103 this.pluginManager_.startRecognizer(); 104 this.audioManager_.start(); 105 this.setState_(SpeechState.HOTWORD_RECOGNIZING); 106 }; 107 108 /** 109 * Called when an error happens for loading the hotword recognizer. 110 * 111 * @private 112 */ 113 SpeechManager.prototype.onHotwordRecognizerLoadError_ = function() { 114 this.setHotwordEnabled(false); 115 this.setState_(SpeechState.READY); 116 }; 117 118 /** 119 * Called when the hotword is recognized. 120 * 121 * @private 122 */ 123 SpeechManager.prototype.onHotwordRecognized_ = function() { 124 if (this.state != SpeechState.HOTWORD_RECOGNIZING) 125 return; 126 this.pluginManager_.stopRecognizer(); 127 this.speechRecognitionManager_.start(); 128 }; 129 130 /** 131 * Called when the speech recognition has happened. 132 * 133 * @param {string} result The speech recognition result. 134 * @param {boolean} isFinal Whether the result is final or not. 135 */ 136 SpeechManager.prototype.onSpeechRecognized = function(result, isFinal) { 137 chrome.send('speechResult', [result, isFinal]); 138 if (isFinal) 139 this.speechRecognitionManager_.stop(); 140 }; 141 142 /** 143 * Called when the speech recognition has started. 144 */ 145 SpeechManager.prototype.onSpeechRecognitionStarted = function() { 146 this.setState_(SpeechState.RECOGNIZING); 147 }; 148 149 /** 150 * Called when the speech recognition has ended. 151 */ 152 SpeechManager.prototype.onSpeechRecognitionEnded = function() { 153 // Do not handle the speech recognition ends if it ends due to an error 154 // because an error message should be shown for a while. 155 // See onSpeechRecognitionError. 156 if (this.state == SpeechState.NETWORK_ERROR) 157 return; 158 159 // Restarts the hotword recognition. 160 if (this.state != SpeechState.STOPPING && this.pluginManager_) { 161 this.pluginManager_.startRecognizer(); 162 this.audioManager_.start(); 163 this.setState_(SpeechState.HOTWORD_RECOGNIZING); 164 } else { 165 this.audioManager_.stop(); 166 this.setState_(SpeechState.READY); 167 } 168 }; 169 170 /** 171 * Called when a speech has started. 172 */ 173 SpeechManager.prototype.onSpeechStarted = function() { 174 if (this.state == SpeechState.RECOGNIZING) 175 this.setState_(SpeechState.IN_SPEECH); 176 }; 177 178 /** 179 * Called when a speech has ended. 180 */ 181 SpeechManager.prototype.onSpeechEnded = function() { 182 if (this.state == SpeechState.IN_SPEECH) 183 this.setState_(SpeechState.RECOGNIZING); 184 }; 185 186 /** 187 * Called when the speech manager should recover from the error state. 188 * 189 * @private 190 */ 191 SpeechManager.prototype.onSpeechRecognitionErrorTimeout_ = function() { 192 this.errorTimeoutId_ = null; 193 this.setState_(SpeechState.READY); 194 this.onSpeechRecognitionEnded(); 195 }; 196 197 /** 198 * Called when an error happened during the speech recognition. 199 * 200 * @param {SpeechRecognitionError} e The error object. 201 */ 202 SpeechManager.prototype.onSpeechRecognitionError = function(e) { 203 if (e.error == 'network') { 204 this.setState_(SpeechState.NETWORK_ERROR); 205 this.errorTimeoutId_ = window.setTimeout( 206 this.onSpeechRecognitionErrorTimeout_.bind(this), 207 SPEECH_ERROR_TIMEOUT * 1000); 208 } else { 209 if (this.state != SpeechState.STOPPING) 210 this.setState_(SpeechState.READY); 211 } 212 }; 213 214 /** 215 * Changes the availability of the hotword plugin. 216 * 217 * @param {boolean} enabled Whether enabled or not. 218 */ 219 SpeechManager.prototype.setHotwordEnabled = function(enabled) { 220 var recognizer = $('recognizer'); 221 if (enabled) { 222 if (recognizer) 223 return; 224 if (!this.naclArch) 225 return; 226 227 var prefix = getHotwordPrefix(); 228 var pluginManager = new speech.PluginManager( 229 prefix, 230 this.onHotwordRecognizerReady_.bind(this), 231 this.onHotwordRecognized_.bind(this), 232 this.onHotwordRecognizerLoadError_.bind(this)); 233 var modelUrl = 'chrome://app-list/_platform_specific/' + this.naclArch + 234 '_' + prefix + '/hotword.data'; 235 pluginManager.scheduleInitialize(this.audioManager_.sampleRate, modelUrl); 236 } else { 237 if (!recognizer) 238 return; 239 document.body.removeChild(recognizer); 240 this.pluginManager_ = null; 241 if (this.state == SpeechState.HOTWORD_RECOGNIZING) { 242 this.audioManager_.stop(); 243 this.setState_(SpeechState.READY); 244 } 245 } 246 }; 247 248 /** 249 * Sets the NaCl architecture for the hotword module. 250 * 251 * @param {string} arch The architecture. 252 */ 253 SpeechManager.prototype.setNaclArch = function(arch) { 254 this.naclArch = arch; 255 }; 256 257 /** 258 * Called when the app-list bubble is shown. 259 * 260 * @param {boolean} hotwordEnabled Whether the hotword is enabled or not. 261 */ 262 SpeechManager.prototype.onShown = function(hotwordEnabled) { 263 this.setHotwordEnabled(hotwordEnabled); 264 265 // No one sets the state if the content is initialized on shown but hotword 266 // is not enabled. Sets the state in such case. 267 if (!this.state && !hotwordEnabled) 268 this.setState_(SpeechState.READY); 269 }; 270 271 /** 272 * Called when the app-list bubble is hidden. 273 */ 274 SpeechManager.prototype.onHidden = function() { 275 this.setHotwordEnabled(false); 276 277 // SpeechRecognition is asynchronous. 278 this.audioManager_.stop(); 279 if (this.state == SpeechState.RECOGNIZING || 280 this.state == SpeechState.IN_SPEECH) { 281 this.setState_(SpeechState.STOPPING); 282 this.speechRecognitionManager_.stop(); 283 } else { 284 this.setState_(SpeechState.READY); 285 } 286 }; 287 288 /** 289 * Toggles the current state of speech recognition. 290 */ 291 SpeechManager.prototype.toggleSpeechRecognition = function() { 292 if (this.state == SpeechState.NETWORK_ERROR) { 293 if (this.errorTimeoutId_) 294 window.clearTimeout(this.errorTimeoutId_); 295 this.onSpeechRecognitionErrorTimeout_(); 296 } else if (this.state == SpeechState.RECOGNIZING || 297 this.state == SpeechState.IN_SPEECH) { 298 this.audioManager_.stop(); 299 this.speechRecognitionManager_.stop(); 300 } else { 301 if (this.pluginManager_) 302 this.pluginManager_.stopRecognizer(); 303 if (this.audioManager_.state == speech.AudioState.STOPPED) 304 this.audioManager_.start(); 305 this.speechRecognitionManager_.start(); 306 } 307 }; 308 309 return { 310 SpeechManager: SpeechManager 311 }; 312}); 313