【Xamarin.Forms】音声認識の使い方

# クロスプラットフォームで開発するアプリでも音声認識を使いたい

音声認識機能はプラットフォーム固有の処理なので、もし音声認識以外の機能をほぼ持たないアプリを作る場合、Xamarin.Formsで開発するメリットは小さいです（それでもViewを共通化できるくらいのメリットはあります）。
ですが、↓みたいな事情があったらやっぱりXamarin.Formsの中で音声認識を使ってみたくなります。

* 音声認識以外の処理はほぼXamarin.Formsの共通部分に詰め込むことができる
* Xamarin.Formsで作ったアプリに音声認識機能を後付けしたい

そんな場合のための、Xamarin.Formsで音声認識機能を実装する方法を紹介します。
**「音声認識を使うからXamarin.Nativeで開発しなくちゃいけない」なんてことはありません。**

# サンプルアプリ

サンプルアプリをGitHubに公開しました。
https://github.com/microwavePC/VoiceRecognitionSample

Prism for Xamarin.Forms（Prism.Forms）のテンプレートを使用して作成しています。
Prismを使わなくても音声認識を使うことは可能ですが、適宜読み替えていただく必要があります（特にViewやViewModelの部分）。

####対応プラットフォーム
* iOS（Ver10.0以上 ※）
* Android

　※音声認識に使用しているフレームワーク（SFSpeechRecognizer）はVer10.0以上のみに対応しています。

####機能
音声を認識し、画面上に認識結果の文章を表示します。

####画面構成
| iOS | Android |
|:-:|:-:|
|![](https://github.com/microwavePC/VoiceRecognitionSample/blob/master/Screenshot/iOS/initial.png?raw=true)|![](https://github.com/microwavePC/VoiceRecognitionSample/blob/master/Screenshot/Android/initial.png?raw=true)|
認識結果の文章はボタンのすぐ上に表示されます。

# 音声認識の実装手順

以下の手順で実装していくことで、Xamarin.Forms（Prism.Forms）で音声認識を使用できるようになります。
　1. 下準備（パーミッションの追加等）
　2. 音声認識用の dependency service の作成
　3. Viewの実装
　4. ViewModelの実装


# 1. 音声認識を使うための下準備

### 1.1. iOS側の下準備

音声認識の実装に使用するフレームワーク（SFSpeechRecognizer）は、iOSのバージョン10以上のみで使用可能です。
なので、無用なトラブルを防ぐため、Info.plistでDeployment Targetを10.0以上に設定します。

<img width="299" alt="スクリーンショット 2017-03-18 21.34.02.png" src="https://qiita-image-store.s3.amazonaws.com/0/140394/a870daae-b3e6-0ad3-f042-b24faa133e6f.png">


また、マイクと音声認識を使えるようにするため、Info.plistにプライバシー設定を追加する必要があります。
追加するプライバシー設定は以下の2つです。

* Privacy - Microphone Usage Description
* Privacy - Speech Recognition Usage Description

Info.plistを開き、その中の「ソース」タブを開いたら、「新しいエントリの追加」で上記2つの設定項目を追加します。

<img width="430" alt="スクリーンショット 2017-03-16 22.21.07.png" src="https://qiita-image-store.s3.amazonaws.com/0/140394/83ba4ffe-f543-8704-9112-79a8b89879e5.png">

### 1.2. Android側の下準備

Androidの音声認識は、音声認識用の画面（アクティビティ）上で行われます。なので、そのアクティビティからの結果を dependency service で拾えるように準備しておく必要があります。
Android用プロジェクトの中にあるMainActivity.csを、以下のように修正します。

```csharp:MainActivity.cs
using System;
using Android.App;
using Android.Content;
using Android.Content.PM;
using Android.OS;
using Microsoft.Practices.Unity;
using Prism.Unity;

// ↓追加
using Android.Preferences;

namespace VoiceRecognitionSample.Droid
{
	[Activity(Label = "VoiceRecognitionSample.Droid", Icon = "@drawable/icon", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation)]
	public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsApplicationActivity
	{
		// ↓追加
		public event EventHandler<PreferenceManager.ActivityResultEventArgs> ActivityResult = delegate {};

		protected override void OnCreate(Bundle bundle)
		{
			base.OnCreate(bundle);
			global::Xamarin.Forms.Forms.Init(this, bundle);
			LoadApplication(new App(new AndroidInitializer()));
		}

		// ↓追加
		protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
		{
			base.OnActivityResult(requestCode, resultCode, data);
			var resultEventArgs = new PreferenceManager.ActivityResultEventArgs(true, requestCode, resultCode, data);
			ActivityResult(this, resultEventArgs);
		}
	}

	public class AndroidInitializer : IPlatformInitializer
	{
		public void RegisterTypes(IUnityContainer container)
		{

		}
	}
}
```



# 2. 音声認識用の dependency service を作成する

プラットフォーム固有の処理である音声認識を呼び出すために、dependency serviceを作成します。

### 2.1. 【共通プロジェクト】音声認識処理クラス用のインターフェースの作成

以下のように作ります。音声認識を使用するために最低限必要な部分を、プラットフォーム共通で利用できるようにします。

```csharp:IVoiceRecognitionService.cs
using System.ComponentModel;

namespace VoiceRecognitionSample.Models
{
	// 音声認識用のdependency service。
	// プロパティの変更をViewModelで捕まえるため、INotifyPropertyChangedを継承している。
	public interface IVoiceRecognitionService : INotifyPropertyChanged
	{
		// 音声認識が実行中かどうか（実行中の間のみtrueを返す）
		bool IsRecognizing { get; }

		// 音声認識の結果テキスト（iOSの場合、認識結果をリアルタイムで取得できる）
		string RecognizedText { get; }

		// 音声認識の開始と停止
		void StartRecognizing();
		void StopRecognizing();
	}
}
```

### 2.2. 【iOS】音声認識処理用の固有ソースの作成

2.1で作ったインターフェースを継承して作ります。

```csharp:VoiceRecognitionService（iOS側）

	// プロパティの変更をバインドで捉えられるようにするため、BindableBaseを継承する。
	public class VoiceRecognitionService : BindableBase, IVoiceRecognitionService
	{
		#region Properties

		// 音声認識の実行状況（実行中の間のみtrueを返す）
		private bool _isRecognizing;
		public bool IsRecognizing
		{
			get { return _isRecognizing; }
			set { SetProperty(ref _isRecognizing, value); }
		}

		// 音声認識の結果テキスト
		private string _recognizedText;
		public string RecognizedText
		{
			get
			{
				if (_recognizedText != null)
					return _recognizedText;
				else
					return string.Empty;
			}
			set { SetProperty(ref _recognizedText, value); }
		}

		#endregion

		#region Variables

		// 音声認識に必要な諸々のクラスのインスタンス。
		private AVAudioEngine audioEngine;
		private SFSpeechRecognizer speechRecognizer;
		private SFSpeechAudioBufferRecognitionRequest recognitionRequest;
		private SFSpeechRecognitionTask recognitionTask;

		#endregion

		#region Public Methods

		// 音声認識の開始処理
		public void StartRecognizing()
		{
			RecognizedText = string.Empty;
			IsRecognizing = true;

			// 音声認識の許可をユーザーに求める。
			SFSpeechRecognizer.RequestAuthorization((SFSpeechRecognizerAuthorizationStatus status) =>
				{
					switch (status)
					{
						case SFSpeechRecognizerAuthorizationStatus.Authorized:
							// 音声認識がユーザーに許可された場合、必要なインスタンスを生成した後に音声認識の本処理を実行する。
							// SFSpeechRecognizerのインスタンス生成時、コンストラクタの引数でlocaleを指定しなくても、
							// 端末の標準言語が日本語なら日本語は問題なく認識される。
							audioEngine = new AVAudioEngine();
							speechRecognizer = new SFSpeechRecognizer();
							recognitionRequest = new SFSpeechAudioBufferRecognitionRequest();
							startRecognitionSession();
							break;
						default:
							// 音声認識がユーザーに許可されなかった場合、処理を終了する。
							return;
					}
				}
			);
		}

		// 音声認識の停止処理
		public void StopRecognizing()
		{
			try
			{
				audioEngine?.Stop();
				recognitionTask?.Cancel();
				recognitionRequest?.EndAudio();
				IsRecognizing = false;
			}
			catch (Exception ex)
			{
				Console.WriteLine(ex.Message);
			}
		}

		#endregion

		#region Private Methods

		// 音声認識の本処理
		private void startRecognitionSession()
		{
			// 音声認識のパラメータ設定と認識開始。ここのパラメータはおまじない。
			audioEngine.InputNode.InstallTapOnBus(
				bus: 0,
				bufferSize: 1024,
				format: audioEngine.InputNode.GetBusOutputFormat(0),
				tapBlock: (buffer, when) => { recognitionRequest?.Append(buffer); }
			);
			audioEngine?.Prepare();
			NSError error = null;
			audioEngine?.StartAndReturnError(out error);
			if (error != null)
			{
				Console.WriteLine(error);
				return;
			}

			try
			{
				if (recognitionTask?.State == SFSpeechRecognitionTaskState.Running)
				{
					// 音声認識が実行中に音声認識開始処理が呼び出された場合、実行中だった音声認識を中断する。
					recognitionTask.Cancel();
				}

				recognitionTask = speechRecognizer.GetRecognitionTask(recognitionRequest,
					(SFSpeechRecognitionResult result, NSError err) =>
					{
						if (result == null)
						{
							// iOS Simulator等、端末が音声認識に対応していない場合はここに入る。
							StopRecognizing();
							return;
						}
						
						if (err != null)
						{
							Console.WriteLine(err);
							StopRecognizing();
							return;
						}
						
						if ((result.BestTranscription != null) && (result.BestTranscription.FormattedString != null))
						{
							// 音声を認識できた場合、認識結果を更新する。
							RecognizedText = result.BestTranscription.FormattedString;
						}
						
						if (result.Final)
						{
							// 音声が認識されなくなって時間が経ったら音声認識を打ち切る。
							StopRecognizing();
							return;
						}
					}
				);
			}
			catch (Exception ex)
			{
				Console.WriteLine(ex.Message);
			}
		}

		#endregion
	}

```

### 2.3. 【Android】音声認識処理用の固有ソースの作成

同じく、2.1で作ったインターフェースを継承して作ります。

```csharp:VoiceRecognitionService（Android側）

	// プロパティの変更をバインドで捉えられるようにするため、BindableBaseを継承する。
	public class VoiceRecognitionService : BindableBase, IVoiceRecognitionService
	{
		#region Properties

		// 音声認識の実行状況（実行中の間のみtrueを返す）
		private bool _isRecognizing;
		public bool IsRecognizing
		{
			get { return _isRecognizing; }
			set { SetProperty(ref _isRecognizing, value); }
		}

		// 音声認識の結果テキスト
		private string _recognizedText;
		public string RecognizedText
		{
			get
			{
				if (_recognizedText != null)
					return _recognizedText;
				else
					return string.Empty;
			}
			set { SetProperty(ref _recognizedText, value); }
		}

		#endregion

		#region Constant, MainActivity

		// 定数・MainActivity
		private readonly int REQUEST_CODE_VOICE = 10;       // 音声認識のリクエストコード
		private readonly int INTERVAL_1500_MILLISEC = 1500; // 1.5秒（ミリ秒単位）
		private MainActivity mainActivity;                  // MainActivity

		#endregion

		#region Constructor

		// コンストラクタ
		public VoiceRecognitionService()
		{
			// 音声認識のアクティビティで取得した結果をハンドルする処理をMainActivityに付ける。
			mainActivity = Forms.Context as MainActivity;
			mainActivity.ActivityResult += HandleActivityResult;
		}

		#endregion

		#region Handler

		// 音声認識のアクティビティで取得した結果をハンドルする処理の本体
		private void HandleActivityResult(object sender, PreferenceManager.ActivityResultEventArgs args)
		{
			if (args.RequestCode == REQUEST_CODE_VOICE)
			{
				IsRecognizing = false;
				if (args.ResultCode == Result.Ok)
				{
					// 認識が成功した場合、認識結果の文字列を引き出し、RecognizedTextに入れる。
					var matches = args.Data.GetStringArrayListExtra(RecognizerIntent.ExtraResults);
					if (matches.Count != 0)
					{
						RecognizedText = matches[0];
					}
				}
			}
		}

		#endregion

		#region Public Methods

		// 音声認識の開始処理
		public void StartRecognizing()
		{
			RecognizedText = string.Empty;
			IsRecognizing = true;

			try
			{
				// 音声認識のアクティビティを呼び出すためのインテントを用意する。
				var voiceIntent = new Intent(RecognizerIntent.ActionRecognizeSpeech);

				// 諸々のプロパティを設定する。
				voiceIntent.PutExtra(RecognizerIntent.ExtraLanguageModel, RecognizerIntent.LanguageModelFreeForm);
				voiceIntent.PutExtra(RecognizerIntent.ExtraPrompt, "音声認識ダイアログにこの文字列が表示される。");
				voiceIntent.PutExtra(RecognizerIntent.ExtraSpeechInputCompleteSilenceLengthMillis, INTERVAL_1500_MILLISEC);
				voiceIntent.PutExtra(RecognizerIntent.ExtraSpeechInputPossiblyCompleteSilenceLengthMillis, INTERVAL_1500_MILLISEC);
				voiceIntent.PutExtra(RecognizerIntent.ExtraSpeechInputMinimumLengthMillis, INTERVAL_1500_MILLISEC);
				voiceIntent.PutExtra(RecognizerIntent.ExtraMaxResults, 1);

				// 認識言語の指定。端末の設定言語(Java.Util.Locale.Default)で音声認識を行う。
				voiceIntent.PutExtra(RecognizerIntent.ExtraLanguage, Java.Util.Locale.Default);

				// 音声認識のアクティビティを開始する。
				mainActivity.StartActivityForResult(voiceIntent, REQUEST_CODE_VOICE);
			}
			catch (Exception ex)
			{
				Console.WriteLine(ex.Message);
			}
		}

		// 音声認識の停止処理
		public void StopRecognizing()
		{
			// Androidでは実装は不要。
		}

		#endregion
	}

```

# 3. Viewの実装

音声認識を呼び出すためのボタンと、認識結果を表示するためのラベルを設置します。

```xml:MainPage.xaml

（省略）

		<Label Text="{Binding RecognizedText}" />
		<Button Text="{Binding VoiceRecognitionButtonText}"
				Command="{Binding VoiceRecognitionCommand}" />

（省略）

```

ラベルのTextにバインドしているRecognizedText（ViewModel内の変数）に、認識結果の文章を入れます。
この後のViewModelの実装で、音声認識用の dependency service 内のRecognizedTextがこのラベルにリアルタイムに反映されるよう実装します。

音声認識呼び出し用ボタンは、音声認識が実行されていないときには「開始」ボタン、実行中は「停止」ボタンにします。
そのため、テキストを動的に変えられるよう、ViewModelの変数にバインドします。

# 4. ViewModelの実装

### 4.1. プロパティ、サービス、コマンドの宣言

ViewModel上で、必要な変数等々を以下のように宣言します。

```csharp:MainPageViewModel.cs

（省略）

		#region Constants

		// 音声認識の開始・停止ボタンのテキスト
		private const string BUTTON_TEXT_START = "開始";
		private const string BUTTON_TEXT_STOP = "停止";

		#endregion

		#region Properties, Variables

		// 音声認識の結果テキスト
		private string _recognizedText = string.Empty;
		public string RecognizedText
		{
			get { return _recognizedText; }
			protected set { SetProperty(ref _recognizedText, value); }
		}

		// 音声認識の開始・停止ボタンの表記
		private string _voiceRecognitionButtonText = BUTTON_TEXT_START;
		public string VoiceRecognitionButtonText
		{
			get { return _voiceRecognitionButtonText; }
			protected set { SetProperty(ref _voiceRecognitionButtonText, value); }
		}

		// 音声認識を実行中かどうか（trueなら実行中）
		private bool _isRecognizing;
		public bool IsRecognizing
		{
			get { return _isRecognizing;}
			protected set
			{
				// 音声認識が実行中の場合、音声認識ボタンのテキストを「停止」に変更する。
				// 音声認識が停止している場合は「開始」に変更する。
				VoiceRecognitionButtonText = value ? BUTTON_TEXT_STOP : BUTTON_TEXT_START;
				SetProperty(ref _isRecognizing, value);
			}
		}

		// 音声認識サービス
		private readonly IVoiceRecognitionService _voiceRecognitionService;

		// 音声認識サービスの処理の呼び出し用コマンド
		public ICommand VoiceRecognitionCommand { get; }

		#endregion

（省略）

```

### 4.2. 音声認識サービスを使用する処理の実装

コンストラクタで取得した音声認識サービスのインスタンスに、プロパティ変更時に実行する処理を紐付けます。
また、音声認識呼び出し用ボタンのコマンドに紐づける処理も実装します。

```csharp:MainPageViewModel.cs

（省略）

		// コンストラクタ
		public MainPageViewModel(IVoiceRecognitionService voiceRecognitionService)
		{
			_voiceRecognitionService = voiceRecognitionService;

			// 音声認識サービスのプロパティが変更されたときに実行する処理を設定する。
			_voiceRecognitionService.PropertyChanged += voiceRecognitionServicePropertyChanged;

			// 音声認識サービスの処理本体をコマンドに紐付ける。
			VoiceRecognitionCommand = new DelegateCommand(executeVoiceRecognition);
		}

（省略）

		// 音声認識サービスのプロパティ変更時にトリガーされるイベントの実処理
		private void voiceRecognitionServicePropertyChanged(object sender, PropertyChangedEventArgs args)
		{
			if (args.PropertyName == "RecognizedText")
			{
				// 音声の認識結果テキストの変更がトリガーになった場合、そのテキストをViewModelに取得する。
				RecognizedText = _voiceRecognitionService.RecognizedText;
				
				//
				// 音声の認識結果テキストを使って何か処理をしたい場合、
				// ここに処理(または処理の呼び出し)を書けばOK。
				//
			}
			if (args.PropertyName == "IsRecognizing")
			{
				// 音声認識の実行状況変更がトリガーになった場合、その実行状況をViewModelに取得する。
				IsRecognizing = _voiceRecognitionService.IsRecognizing;
			}
		}

		// 音声認識サービス呼び出し用ボタンのコマンドの実処理
		private void executeVoiceRecognition()
		{
			if (IsRecognizing)
			{
				// 音声認識を実行中の場合、「停止」ボタンとして機能させる。
				_voiceRecognitionService.StopRecognizing();
			}
			else
			{
				// 音声認識が停止中の場合、「開始」ボタンとして機能させる。
				_voiceRecognitionService.StartRecognizing();
			}
		}

（省略）

```