Android Speech Recognizerを使いこなす

# Android Speech Recognizerの基本
Androidで音声認識といえばこれがスタンダードと言っても過言ではないでしょう。
それほど簡単に高性能な音声認識が利用可能なのです。
簡単に使いやすい分、パッケージ化されているので、かゆいところに手が届かないところもあります。
### 基本

```java
Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
intent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, context.getPackageName());
SpeechRecognizer recognizer = SpeechRecognizer.createSpeechRecognizer(context);
recognizer.setRecognitionListener(new RecognitionListener() {
	//...略
	@Override
	public void onResults(Bundle results) {
		List<String> recData = results.getStringArrayList(android.speech.SpeechRecognizer.RESULTS_RECOGNITION);
	}
});
recognizer.startListening(intent);

```
これはgoogle製のダイアログを表示させないパターンです。
ダイアログ表示させるときはstartActivityForResult()使います。

Extraをputしているところで、
RecognizerIntent.EXTRA_LANGUAGE_MODELは認識結果をWEB検索に使う場合はACTION_WEB_SEARCHを使うといいらしいです。いずれを選択しても認識精度に少し影響がある程度なので間違って使ってても気づかないこともあるかもしれないです。

# オフラインで音声認識
Android Speech Recognizerは通常オンラインでgoogleのサーバーと通信して音声認識を行います。しかし実はオフラインでの音声認識も同様に対応されています。
なのでデフォルトの設定ではオンラインで認識するか、オフラインで認識するかが自動的に切り替えられて認識されます。
しかしインターネットつながってるけど、速度がめっちゃ遅いみたいなパターンも有ると思います。その場合、認識精度が非常に悪くなってしまいますので、強制的にオフラインで認識させることも出来ます。

```java
intent.putExtra(RecognizerIntent.EXTRA_PREFER_OFFLINE, true)
```
これはAdded in API level 23です。

オフラインで音声認識するためには当然デバイスにモデルデータそのものが必要になります。
Androidの設定から→言語と入力→Google音声入力→オフラインの音声認識にいくとインストール済みの言語のモデルの確認や、新しく別の言語のモデルをインストールすることが出来ます。
このモデルなしでかつこのフラグをtrueで認識させた場合は、かならず onError()でERROR_SERVERが返ってきて認識に失敗します。　


# 言語の指定
もちろん多言語の音声認識に対応しています。
グローバルなサービスを提供する場合、多様することでしょう。

```java
String languageString = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
	? context.getResources().getConfiguration().locale.toLanguageTag()
	: String.format(Locale.ENGLISH, "%s-%s", context.getResources().getConfiguration().locale.getLanguage().toLowerCase(), context.getResources().getConfiguration().locale.getCountry().toUpperCase());

// 英語ならen-USというStringが入る
intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, language);
```
languageString is defined by BCP 47.

# 音声のサイレント区間の延長
音声認識に慣れていないユーザーはUX的に音声認識が開始されたことを知覚するのが遅くなる傾向にありそうですが、最近のAndroidSpeechRecognizerは時に1秒少々で認識終了（認識失敗）してしまうことがあります。もうちょっと認識開始からユーザーの声が入るまでの時間を延長したいと思うようになることでしょう。
一応RecognizerIntentには下記プロパティが設定できます。

```java
EXTRA_SPEECH_INPUT_COMPLETE_SILENCE_LENGTH_MILLIS
EXTRA_SPEECH_INPUT_MINIMUM_LENGTH_MILLIS
EXTRA_SPEECH_INPUT_POSSIBLY_COMPLETE_SILENCE_LENGTH_MILLIS
```
これらをintent putExtraでlongのvalueを設定すると良し！とおもいきや、全然いうことを聞きません。
昔はこれで動いてたみたいですが、今となってはこの問題を解決する方法は見当たりません。

# RECOGNIZER_BUSY問題
Google Speech Recognizerを多様していると、たまにonError（）でERROR_RECOGNIZER_BUSYを受け取ることがあります。基本的にクライアント側の問題なのですが、原因はいろいろあるようです。
主にSpeechRecognizerのインスタンスを複数使っていたらこの問題が生じるので、シングルトンにする、認識が終了したらspeechRecognizer.destroy()を呼ぶようにするとだいたい解決します。
それ以外にも、音声認識を立て続けに使うようなUXフローの場合に、比較的ロースペックな端末で起こりえるようです。認識終了と認識開始の間は短くなり過ぎないように注意が必要です。

# NO_MATCHとSPEECH_TIMEOUT
エラーの数が一番多いのがこの２つです。
違いは、ユーザーが何か声だしたけど、何言ってるのかさっぱりわからんときはNO_MATCH, ユーザーが何も言わなくてサイレントな状態でタイムアウトしたのがSPEECH_TIMEOUTです。
しかし雑音環境下ではサイレントな状態でもNO_MATCHがかえってくることがかなりあります。
あまりこのエラーの数によってユーザーの行動を決めつけるのはよくないかもしれません。

# ボイスコマンド認識としての利用
"OK google"のようなボイスコマンドをアプリに組み込めるとかっこいいですね。
しかしこれを実現するためには連続音声認識を実装しなければなりません。
[昔書いた記事](http://qiita.com/KAKKA/items/e22c544d0c8270140325)でPocketSphinxという音声認識エンジンを紹介しましたがこれで連続音声認識によるボイスコマンドの認識が可能になりますが、言語モデル、音響モデルが英語しかありません。
また当然”OK google”の連続音声認識を行っているので、マイクロフォンがこれをコンフリクトしてしまいます。うまく"OK google"をハンドリングする必要があります。
また、連続音声認識では常に集音した音を信号処理し続けているので計算量も増えてきますし、バッテリーに影響することも考えられます。
そこでこのAndroid Speech Recognizerをうまく活用してみます。

### コマンドは短すぎると認識精度が悪くなる
当然といえば当然ですが。
静かな部屋でマイクに近づいて認識させると認識精度は申し分ないですが、短すぎるコマンドはどうしても認識精度が低くなります。
人間の耳でも短すぎる言葉は時に聞き取れないのと同様に機械でも学習材料が少ないとどうにもなりません。（これはもちろん教師なしのモデルを利用する想定の話です。OK googleやPocketSphinxは学習データとしてコマンドのモデルを設定できます。）
また実環境では環境雑音が必ず入り、ハンズフリーでの音声認識となるので、認識精度が低くなります。
例えば、高速道路を走っている時に隣の人が突然「屁」といった場面を想像してください。
あなたはしっかりと「屁」と認識できるでしょうか。では「屁をこいた」といった場合はどうでしょうか。これは認識出来そうな気がしますね。
なのでユーザーが煩雑にならない程度の文脈を理解できるコマンドを設定することで認識精度が圧倒的に高くなります。例えば"send"を"send message"にするなどです。
現状のAndroid Speech Recognizerには状況に合わせた事前学習データを付加することができないのですから。

### 辞書データは甘めに作る
先ほどの例で言うと"send message"を認識させた場合に、コマンドであると判断するためには、予めコマンド辞書を作る必要があります。
この認識結果と辞書のマッチロジックは完全一致だとこれもなかなか厳しい結果となることが想像出来ます。コマンドを長くする分、惜しいけどマッチしていないパターンは増えてきます。
なので許す限りは甘めのマッチロジックを作成します。
"send message"であれば"send", "message"いずれかの単語を含んで入ればOKというように。

### UIを工夫する
RecognizerIntentを使ってstartActivityForResultすると、googleのダイアログが表示されます。これはかなり音声認識してる感が出せているので、ユーザーも直感的に音声認識をしているんだということを認識します。
一方、ダイアログを表示させない場合は、認識開始、終了の音くらいしかユーザーにとっては判断材料がありません。
なので、独自の認識中UIを作成し、ユーザーが即座に発声するようにしないといけません。UIで工夫する直前にもこれを助けることができます。例えばTTSで「メッセージを話してください」と言わせるなどです。


# 認識開始時、終了時の音をミュートするHack
世の中の音声認識を実装しているアプリケーションを見ると、必ずと言っていいほど認識開始時に「ピコン」、認識終了時に「ププン」、「ペキャン」と言った音が鳴ることがわかると思います。
これはそのアプリケーションがAndroidSpeechRecognizerを使っているということであり、またこの音を鳴らさないというダイレクトで正式な手法がないことの証明です。実際に音は消せません。
でも消せます。
無理やりデバイスの音量をミュートして、終わったら復帰させてやるのです。
startListeningの直前で

```java
private AudioManager mAudioManager;
private int mStreamVolume = 0; 
mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
mStreamVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, 0, 0);
```

ミュートし、認識おわったら、

```java
mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, mStreamVolume, 0);
```
戻してやります。

# あとなんかあったっけな？

```
　 |l､{　 　j}　/,,ｨ//｜　　　　　／￣￣￣￣￣￣￣￣￣￣￣￣￣￣￣￣￣￣￣￣
　 i|:!ヾ､_ﾉ／ u {:}//ﾍ　　　　　|　あ…ありのまま 今　起こった事を話すぜ！
　 |ﾘ u' }　 ,ﾉ　_,!V,ﾊ |　　 　 ＜　『おれは奴の前で階段を登っていたと
　 fト､_{ﾙ{,ィ'ｅﾗ　, ﾀ人.　　　　|　　思ったらいつのまにか降りていた』
　ヾ|宀| {´,)⌒`/ |<ヽﾄiゝ　　　|　催眠術だとか超スピードだとか
```

次の記事
[連続音声認識っぽくなったAndroid SpeechRecognizer速報](http://qiita.com/KAKKA/items/47927becf3ae35bbb45b)