• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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