// Copyright 2013 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 The class to Manage both offline / online speech recognition. */ cr.define('speech', function() { 'use strict'; /** * The state of speech recognition. * * @enum {string} */ var SpeechState = { READY: 'READY', HOTWORD_RECOGNIZING: 'HOTWORD_RECOGNIZING', RECOGNIZING: 'RECOGNIZING', IN_SPEECH: 'IN_SPEECH', STOPPING: 'STOPPING', NETWORK_ERROR: 'NETWORK_ERROR' }; /** * The time to show the network error message in seconds. * * @const {number} */ var SPEECH_ERROR_TIMEOUT = 3; /** * Checks the prefix for the hotword module based on the language. This is * fragile if the file structure has changed. */ function getHotwordPrefix() { var prefix = navigator.language.toLowerCase(); if (prefix == 'en-gb') return prefix; var hyphen = prefix.indexOf('-'); if (hyphen >= 0) prefix = prefix.substr(0, hyphen); if (prefix == 'en') prefix = ''; return prefix; } /** * @constructor */ function SpeechManager() { this.audioManager_ = new speech.AudioManager(); this.audioManager_.addEventListener('audio', this.onAudioLevel_.bind(this)); this.speechRecognitionManager_ = new speech.SpeechRecognitionManager(this); this.errorTimeoutId_ = null; } /** * Updates the state. * * @param {SpeechState} newState The new state. * @private */ SpeechManager.prototype.setState_ = function(newState) { if (this.state == newState) return; this.state = newState; chrome.send('setSpeechRecognitionState', [this.state]); }; /** * Called with the mean audio level when audio data arrives. * * @param {cr.event.Event} event The event object for the audio data. * @private */ SpeechManager.prototype.onAudioLevel_ = function(event) { var data = event.data; var level = 0; for (var i = 0; i < data.length; ++i) level += Math.abs(data[i]); level /= data.length; chrome.send('speechSoundLevel', [level]); }; /** * Called when the hotword recognizer is ready. * * @param {PluginManager} pluginManager The hotword plugin manager which gets * ready. * @private */ SpeechManager.prototype.onHotwordRecognizerReady_ = function(pluginManager) { this.pluginManager_ = pluginManager; this.audioManager_.addEventListener( 'audio', pluginManager.sendAudioData.bind(pluginManager)); this.pluginManager_.startRecognizer(); this.audioManager_.start(); this.setState_(SpeechState.HOTWORD_RECOGNIZING); }; /** * Called when an error happens for loading the hotword recognizer. * * @private */ SpeechManager.prototype.onHotwordRecognizerLoadError_ = function() { this.setHotwordEnabled(false); this.setState_(SpeechState.READY); }; /** * Called when the hotword is recognized. * * @private */ SpeechManager.prototype.onHotwordRecognized_ = function() { if (this.state != SpeechState.HOTWORD_RECOGNIZING) return; this.pluginManager_.stopRecognizer(); this.speechRecognitionManager_.start(); }; /** * Called when the speech recognition has happened. * * @param {string} result The speech recognition result. * @param {boolean} isFinal Whether the result is final or not. */ SpeechManager.prototype.onSpeechRecognized = function(result, isFinal) { chrome.send('speechResult', [result, isFinal]); if (isFinal) this.speechRecognitionManager_.stop(); }; /** * Called when the speech recognition has started. */ SpeechManager.prototype.onSpeechRecognitionStarted = function() { this.setState_(SpeechState.RECOGNIZING); }; /** * Called when the speech recognition has ended. */ SpeechManager.prototype.onSpeechRecognitionEnded = function() { // Do not handle the speech recognition ends if it ends due to an error // because an error message should be shown for a while. // See onSpeechRecognitionError. if (this.state == SpeechState.NETWORK_ERROR) return; // Restarts the hotword recognition. if (this.state != SpeechState.STOPPING && this.pluginManager_) { this.pluginManager_.startRecognizer(); this.audioManager_.start(); this.setState_(SpeechState.HOTWORD_RECOGNIZING); } else { this.audioManager_.stop(); this.setState_(SpeechState.READY); } }; /** * Called when a speech has started. */ SpeechManager.prototype.onSpeechStarted = function() { if (this.state == SpeechState.RECOGNIZING) this.setState_(SpeechState.IN_SPEECH); }; /** * Called when a speech has ended. */ SpeechManager.prototype.onSpeechEnded = function() { if (this.state == SpeechState.IN_SPEECH) this.setState_(SpeechState.RECOGNIZING); }; /** * Called when the speech manager should recover from the error state. * * @private */ SpeechManager.prototype.onSpeechRecognitionErrorTimeout_ = function() { this.errorTimeoutId_ = null; this.setState_(SpeechState.READY); this.onSpeechRecognitionEnded(); }; /** * Called when an error happened during the speech recognition. * * @param {SpeechRecognitionError} e The error object. */ SpeechManager.prototype.onSpeechRecognitionError = function(e) { if (e.error == 'network') { this.setState_(SpeechState.NETWORK_ERROR); this.errorTimeoutId_ = window.setTimeout( this.onSpeechRecognitionErrorTimeout_.bind(this), SPEECH_ERROR_TIMEOUT * 1000); } else { if (this.state != SpeechState.STOPPING) this.setState_(SpeechState.READY); } }; /** * Changes the availability of the hotword plugin. * * @param {boolean} enabled Whether enabled or not. */ SpeechManager.prototype.setHotwordEnabled = function(enabled) { var recognizer = $('recognizer'); if (enabled) { if (recognizer) return; if (!this.naclArch) return; var prefix = getHotwordPrefix(); var pluginManager = new speech.PluginManager( prefix, this.onHotwordRecognizerReady_.bind(this), this.onHotwordRecognized_.bind(this), this.onHotwordRecognizerLoadError_.bind(this)); var modelUrl = 'chrome://app-list/_platform_specific/' + this.naclArch + '_' + prefix + '/hotword.data'; pluginManager.scheduleInitialize(this.audioManager_.sampleRate, modelUrl); } else { if (!recognizer) return; document.body.removeChild(recognizer); this.pluginManager_ = null; if (this.state == SpeechState.HOTWORD_RECOGNIZING) { this.audioManager_.stop(); this.setState_(SpeechState.READY); } } }; /** * Sets the NaCl architecture for the hotword module. * * @param {string} arch The architecture. */ SpeechManager.prototype.setNaclArch = function(arch) { this.naclArch = arch; }; /** * Called when the app-list bubble is shown. * * @param {boolean} hotwordEnabled Whether the hotword is enabled or not. */ SpeechManager.prototype.onShown = function(hotwordEnabled) { this.setHotwordEnabled(hotwordEnabled); // No one sets the state if the content is initialized on shown but hotword // is not enabled. Sets the state in such case. if (!this.state && !hotwordEnabled) this.setState_(SpeechState.READY); }; /** * Called when the app-list bubble is hidden. */ SpeechManager.prototype.onHidden = function() { this.setHotwordEnabled(false); // SpeechRecognition is asynchronous. this.audioManager_.stop(); if (this.state == SpeechState.RECOGNIZING || this.state == SpeechState.IN_SPEECH) { this.setState_(SpeechState.STOPPING); this.speechRecognitionManager_.stop(); } else { this.setState_(SpeechState.READY); } }; /** * Toggles the current state of speech recognition. */ SpeechManager.prototype.toggleSpeechRecognition = function() { if (this.state == SpeechState.NETWORK_ERROR) { if (this.errorTimeoutId_) window.clearTimeout(this.errorTimeoutId_); this.onSpeechRecognitionErrorTimeout_(); } else if (this.state == SpeechState.RECOGNIZING || this.state == SpeechState.IN_SPEECH) { this.audioManager_.stop(); this.speechRecognitionManager_.stop(); } else { if (this.pluginManager_) this.pluginManager_.stopRecognizer(); if (this.audioManager_.state == speech.AudioState.STOPPED) this.audioManager_.start(); this.speechRecognitionManager_.start(); } }; return { SpeechManager: SpeechManager }; });