※本ブログは Building Apps for Windows “Use inking and speech to support natural input (10 by 10)” の抄訳です。
Windows 10 では、アプリでのナチュラル インプットがかつてないほどサポートしやすくなりました。今日は、インクと音声認識を使って、アプリのより自然な操作を実現する方法について説明します。
DirectInk によるデジタルインク
さまざまなコンピューター入力デバイスが登場し、進化していますが、情報を残したり、自分を表現したりする手段としては、ペンと紙の利用頻度の方が高いのが実情です。これは、幼少期から手書きするように教わってきたことが一因ですが、2013 年に『Psychological Science』に発表された研究 (英語) によると、手書きによって、思考や記憶、学習能力が向上することが証明されています。
Windows 10 では、DirectInk プラットフォームを使うことで、アプリにデジタル インク機能を実装しやすくしました。DirectInk を使ってインクを収集、表示、管理することで、Microsoft Edge、OneNote、手書きパネルで使われているものと同じ高度なインク機能を使用できます。以下では、DirectInk をアプリにどのように実装するか、簡単な例をいくつか紹介します。
インクの収集
Windows 8.1 アプリでは、キャンバスを作成して入力イベントを聞き取り、ストロークをレンダーする必要がありましたが、Windows 10 では組み込みの InkCanvas (英語) コントロールを使ってインク機能をすぐ実現できるようになりました。
<Grid> <InkCanvas x:Name="myInkCanvas"/> </Grid>
このコントロールを使うだけで、すぐユーザーにインク機能を提供できます。また、InkCanvas の InkPresenter (英語) プロパティにアクセスすることで、簡単に機能を拡張することもできます。InkPresenter を使って、ペン入力やタッチ入力、マウス入力の収集を構成でき、InkCanvas で収集されたインクの描画属性を操作することもできます。次の例をご覧ください。
InkPresenter myPresenter = myInkCanvas.InkPresenter; myPresenter.InputDeviceTypes = Windows.UI.Core.CoreInputDeviceTypes.Pen | Windows.UI.Core.CoreInputDeviceTypes.Mouse; InkDrawingAttributes myAttributes = myPresenter.CopyDefaultDrawingAttributes(); myAttributes.Color = Windows.UI.Colors.Crimson; myAttributes.PenTip = PenTipShape.Rectangle; myAttributes.PenTipTransform = System.Numerics.Matrix3x2.CreateRotation((float) Math.PI/4); myAttributes.Size = new Size(2,6); myPresenter.UpdateDefaultDrawingAttributes(myAttributes);
ここでは、ペンとマウスを使ったインクの収集を許可し、インク入力もペン入力も同時に利用できるようにしています。また、InkPresenter の PenTip 描画属性と PenTipTransform 描画属性を設定し、草書ブラシを作成しています。
インクの編集、保存、読み込み
ユーザーがインク入力を行うキャンバスが用意できたら、さらに高度なインクの制御機能を提供しましょう。鉛筆と紙を使う場合と同様に、書いたものを消すことも一般的なシナリオの 1 つです。これは InkPresenter の InputProcessingConfiguration.Mode プロパティを使用することで簡単に実現できます。
private void Eraser_Click(object sender, RoutedEventArgs e) { myInkCanvas.InkPresenter.InputProcessingConfiguration.Mode = InkInputProcessingMode.Erasing; }
ここでは、ペンとマウスを使ったインクの収集を許可し、インク入力もペン入力も同時に利用できるようにしています。また、InkPresenter の PenTip 描画属性と PenTipTransform 描画属性を設定し、草書ブラシを作成しています。
この例では、ボタンを押すとアプリの消しゴム モードが有効になり、消しゴムを使ってインクを消すことができます。各ストロークを消して、すばやく消去できるので、非常に便利です。また、既定で、消しゴムボタンを押すことで消しゴム モードがトリガーされる点も、そのためのコードを追加で記述しなくて済むため便利です。
DirectInk は Ink Serialized Format (ISF) を使用して、インク キャプチャの保存と読み込みをサポートします。InkStrokeContainer の SaveAsync メソッドと LoadAsync メソッドを使うことで、InkStrokeContainer にストローク データをキャプチャし、埋め込みの ISF データとして GIF ファイル形式で保存できます。次に例を示します。
var savePicker = new FileSavePicker(); savePicker.SuggestedStartLocation = Windows.Storage.Pickers.PickerLocationId.PicturesLibrary; savePicker.FileTypeChoices.Add("Gif with embedded ISF", new System.Collections.Generic.List<string> { ".gif" }); StorageFile file = await savePicker.PickSaveFileAsync(); if (null != file) { try { using (IRandomAccessStream stream = await file.OpenAsync(FileAccessMode.ReadWrite)) { await myInkCanvas.InkPresenter.StrokeContainer.SaveAsync(stream); } } catch (Exception ex) { GenerateErrorMessage(); } }
ご覧のとおり、InkStrokeContainer で直接 SaveAsync メソッドを呼び出し、既によくご存じだと思われる API を使ってファイルに書き込んでいます。同様に、LoadAsync メソッドを使って ISF ファイルまたは ISF データを埋め込んだ GIF ファイルから一連のストロークを InkStrokeContainer に読み込むことができます。ストロークが読み込まれると、InkPresenter によって自動的にレンダリングされます。
高度なインク機能
現在、DirectInk プラットフォームでは、組み込みのインク選択をサポートしていませんが、InkPresenter を使って UnprocessedInput イベントを処理することで、この機能を簡単に開発できます。UnprocessedInput イベントは、InkPresenter の処理構成モードを None に設定している場合に、InkPresenter が入力を受け取ると発生します。この場合、入力はキャプチャされますが、ストロークは画面にレンダリングされません。また、RightDragAction プロパティを使って、マウスの右クリックやペン ボタンで同じ動作を構成することもできます。次の例をご覧ください。
// ... myInkCanvas.InkPresenter.UnprocessedInput.PointerPressed += StartLasso; myInkCanvas.InkPresenter.UnprocessedInput.PointerMoved += ContinueLasso; myInkCanvas.InkPresenter.UnprocessedInput.PointerReleased += CompleteLasso; // ... private void StartLasso(InkUnprocessedInput sender,Windows.UI.Core.PointerEventArgs args) { selectionLasso = new Polyline() { Stroke = new SolidColorBrush(Windows.UI.Colors.Black), StrokeThickness = 2, StrokeDashArray = new DoubleCollection() { 7, 3}, }; selectionLasso.Points.Add(args.CurrentPoint.RawPosition); AddSelectionLassoToVisualTree(); } private void ContinueLasso(InkUnprocessedInput sender, Windows.UI.Core.PointerEventArgs args) { selectionLasso.Points.Add(args.CurrentPoint.RawPosition); } private void CompleteLasso(InkUnprocessedInput sender, Windows.UI.Core.PointerEventArgs args) { selectionLasso.Points.Add(args.CurrentPoint.RawPosition); bounds = myInkCanvas.InkPresenter.StrokeContainer.SelectWithPolyLine(selectionLasso.Points); DrawBoundingRect(bounds); }
上の例では、なげなわ選択を手書きして選択領域を指定し、InkStrokeContainer.MoveSelected() メソッドを使って選択領域内のストロークを移動しています。InkStroke.PointTransform プロパティを使って、ストロークを変換することもできます。移動後のストロークや変換後のストロークの新しいレンダリングは、InkPresenter によって自動的に処理されます。
これは、組み込みの構成を使用して入力とインクをレンダリングする方法の、ほんの一部にすぎません。Windows.UI.Input.Inking API をより詳しく知るには、GitHub のインクサンプル (英語) をご覧ください。
インクのトピックのまとめに入る前に、インクを使って他にどんなことができるかを考えてみましょう。Windows で Fresh Paint アプリ (英語) を使ったことがあるなら、画面上でストロークを “乾燥” させるタイミングを手動で決定できる機能をご存じだと思います。この機能のおかげで、ストロークや色をどのように組み合わせるかを細かく制御できます。InkPresenter はカスタム乾燥機能をサポートするため、独自の DirectX サーフェイスで自在にレンダリングや管理が可能で、アプリで強力なシナリオを実現できます。これはより複雑な InkPresenter の機能の 1 つですが、InkPresenter を使って “ウェット” なストロークのレンダリングや色のブレンドを処理する方法を把握しておくことには意味があります。カスタム乾燥機能の詳細と例については、GitHub の複雑なインク サンプル (英語) を参照してください。
音声コマンドによるアプリの操作とアプリとの会話
2 週間前に、このシリーズでWindows 10 の Cortana を利用して Windows 10 のコア エクスペリエンスにアプリを拡張する方法 (英語) について説明しました。Windows 10 では主要な入力メカニズムの 1 つとして音声認識を採用しているため、アプリで自然言語、音声コマンド、またはディクテーションをサポートすることで、Cortana の機能を利用して効率よい音声認識機能をアプリで提供できます。同様に、音声合成 (TTS) を使用して、アプリからユーザーに “返答” し、会話を開始できます。
2 週間前の記事で Cortana を使ってアプリとの自然な会話をサポートし、自然にユーザーに応答するデモをお見せしたのが、”自然言語” です。”音声コマンド” は、アプリでの音声認識というと最も一般的に思い浮かぶ機能でしょう。特定のコマンドをアプリに話すことで、複数のクリック操作やキーボードコマンドを実行しなければならなかったコマンドをすぐに実行できる機能です。”ディクテーション” は、電子メール アプリやメッセージング アプリなど、ユーザー入力を一語一句キャプチャする必要があるアプリに適しています。”音声合成” は、テキストから音声を生成することで、会話による応対を実現できます。ここからは、これらの 3 種類の音声による操作の機能をアプリに組み込む方法を説明します。
音声コマンド
アプリで音声認識機能を実現するには、まず SpeechRecognizer (英語) クラスを使います。ただし、このクラスを使う前に、ユーザーのマイクを使って音声をキャプチャする許可をユーザーに求める必要があります。それには、AudioCapturePermissions.RequestMicrophonePermission() を呼び出し、返されたブール値の結果を処理します。許可を得たら、SpeechRecognizer を設定して、ユーザー入力の認識を開始できます。
// SpeechRecognizer のインスタンスを作成します。 speechRecognizer = new SpeechRecognizer(recognizerLanguage); // SpeachRecognizer に Web 検索トピックの制約を追加します。 var webSearchGrammar = new SpeechRecognitionTopicConstraint(SpeechRecognitionScenario.WebSearch, "webSearch"); speechRecognizer.Constraints.Add(webSearchGrammar); // RecognizeWithUIAsync を使うとプロンプトをカスタマイズできます。 speechRecognizer.UIOptions.AudiblePrompt = "Say what you want to search for..."; speechRecognizer.UIOptions.ExampleText = speechResourceMap.GetValue("WebSearchUIOptionsExampleText", speechContext).ValueAsString; // 制約をコンパイルします。 SpeechRecognitionCompilationResult compilationResult = await speechRecognizer.CompileConstraintsAsync(); // 音声認識を開始します。 IAsyncOperation<SpeechRecognitionResult> recognitionOperation = speechRecognizer.RecognizeWithUIAsync(); SpeechRecognitionResult speechRecognitionResult = await recognitionOperation;
上の例では、特定の文法を定義せずにアプリで音声認識をサポートできる WebSearch SpeechRecognitionScenario を使っています。これは Microsoft のリモート Web サービスを使うため、ユーザーの OS 設定で音声入力とディクテーションが有効になっている場合にのみ機能します ([設定]、[プライバシー]、[音声認識、インク操作、入力] を選択し、[自分を知ってもらう] から設定します)。そのため、この設定を確認して、有効になっていない場合にメッセージを表示するコードを追加することをお勧めします。
private static uint HResultPrivacyStatementDeclined = 0x80045509; catch (Exception exception) { // 音声認識のプライバシー ポリシー エラーを処理します。 if ((uint)exception.HResult == HResultPrivacyStatementDeclined) { resultTextBlock.Visibility = Visibility.Visible; resultTextBlock.Text = "To use this feature, go to Settings -> Privacy -> Speech, inking and typing, and ensure you have viewed the privacy policy, and 'Getting to know you' is enabled."; // [プライバシー] の [音声認識、インク操作、入力] 設定ページを開きます。 await Windows.System.Launcher.LaunchUriAsync(new Uri("ms-settings:privacy-accounts")); } else { var messageDialog = new Windows.UI.Popups.MessageDialog(exception.Message, "Exception"); await messageDialog.ShowAsync(); } }
WebSearch SpeechRecognitionScenario を使う代わりに、SpeechRecognitionListConstraint (英語) クラスを使用してシンプルな文法をコードに作成するか、SpeechRecognitionGrammarFileConstraint (英語) を使って Speech Recognition Grammar Specification (SRGS) XML ファイルから読み取ることで、独自の文法を提供できます。音声認識のその他の音声コマンド用メソッドをサポートする方法については、MSDN のドキュメントをご覧ください。
ディクテーション
電子メールやテキスト メッセージを作成する場合など、シナリオによっては、長文になる可能性があるテキストをディクテーションする手段をユーザーに提供しなければなりません。SpeechRecognizer は連続的なディクテーションをサポートします。この場合、ディクテーション中に UI を更新できます。通常、連続的なディクテーションにカスタム文法は必要ありません。開発者が文法を指定しない場合、システムは事前定義済みのディクテーション文法を既定で使用します。
アプリは、バックグラウンド スレッドから長時間にわたりディクテーションをキャプチャする可能性があるため、ユーザーのディクテーション中にユーザーインターフェイスを更新できるよう、ディスパッチャを確実に用意する必要があります。ディスパッチャの使い方をよくご存じない場合は、次の簡単な例をご覧ください。
private CoreDispatcher dispatcher; dispatcher = CoreWindow.GetForCurrentThread().Dispatcher; await dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => { dictationTextBox.Text = dictatedTextBuilder.ToString(); });
以下は、コードでの連続的なディクテーションの使用例です。
private StringBuilder dictatedTextBuilder; // ディクテーション トピックの制約を適用して、ディクテーションされたフリーフォームの発話を最適化します。 var dictationConstraint = new SpeechRecognitionTopicConstraint(SpeechRecognitionScenario.Dictation, "dictation"); speechRecognizer.Constraints.Add(dictationConstraint); SpeechRecognitionCompilationResult result = await speechRecognizer.CompileConstraintsAsync(); speechRecognizer.ContinuousRecognitionSession.Completed -= ContinuousRecognitionSession_Completed; speechRecognizer.ContinuousRecognitionSession.ResultGenerated -= ContinuousRecognitionSession_ResultGenerated; speechRecognizer.HypothesisGenerated -= SpeechRecognizer_HypothesisGenerated; await speechRecognizer.ContinuousRecognitionSession.StartAsync();
まず、認識したディクテーションを保持する StringBuilder を作成します。次に、SpeechRecognizer を Dictation SpeechRecognitionScenario で初期化し、イベント発生を聞き取り始めます。最後に、ContinuousRecognitionSession を開始します。
ディクテーションが認識されると ResultGenerated イベントが発生します。つまり、この部分に dictatedTextBuilder を追加します。認識したテキストの Confidence 値を確認して、少なくとも Medium レベルの SpeechRecognitionConfidence でない語は除外することをお勧めします。
アプリが聞き取る 2 つ目のイベントは HypothesisGenerated です。これは、単語が正しく認識されていることが信頼できると、認識エンジンが判断した場合に発生します。たとえば、”weight” (重さ) と “wait” (待つ) という単語は、周辺の単語からコンテキストを収集できない限り、区別ができません。その場合、ResultsGenerated は発生しません。したがって、認識の進行状態についてユーザー インターフェイスを更新する場所としては、HypothesisGenerated イベントの方が適しています。MSDN のドキュメントでは、この 2 つを組み合わせて、応答のよいエクスペリエンスを維持できる優れたパターンを紹介しています。
音声合成 (TTS)
音声によるアプリとのやりとりをより自然なものにするには、音声合成を使ってユーザーに応答できます。これは、SpeechSynthesizer (英語) を使用し、プレーン テキストか、Speech Synthesis Markup Language (SSML) のどちらかを処理することで実現します。プレーン テキストと SSML の違いは、テキストの合成方法をどの程度制御できるかです。どちらの場合も、音声の断片を合成したものをユーザーに再生する手段が必要です。その最も簡単な方法は MediaElement を使用することですが、これはデバイスで先に再生中の音声 (Groove Music のストリームなど) に干渉する可能性があることに注意してください。
テキストから音声を合成するには、次の例を使用します。
private SpeechSynthesizer synthesizer; // テキストからストリームを作成します。このストリームは media 要素を使って再生します。 SpeechSynthesisStream synthesisStream = await synthesizer.SynthesizeTextToStreamAsync(text); // ソースを設定し、合成された音声ストリームの再生を開始します。 media.AutoPlay = true; media.SetSource(synthesisStream, synthesisStream.ContentType); media.Play();
SpeechSynthesizer の Voice プロパティを設定して、使用する音声を選択することもできます。
SSML を使ったテキスト合成も上の例と非常に似ています。違いは、プレーン テキストではなく SSML を SynthesizeSsmlToStreamAsync メソッドを使って渡すことです。この XML マークアップを使うことで、音声の合成方法を変更できます。
<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xml:lang="en-US"> Hello <prosody contour="(0%,+80Hz) (10%,+80%) (40%,+80Hz)">World</prosody> <break time="500ms" /> Goodbye <prosody rate="slow" contour="(0%,+20Hz) (10%,+30%) (40%,+10Hz)">World</prosody> </speak>
上の例では、テキストの特定のセクションのピッチとテンポを変えています。音声認識と音声合成の詳細については、GitHub の音声認識と音声合成のサンプル (英語) を参照してください。
まとめ
これで Windows 10 by 10 開発シリーズの 5 週目は終わりです。次週のトピックは、マルチデバイスのサポートです。アプリを実行しているデバイスの機能を利用できるようにアプリを調整する方法について説明します。
それまでは、DVLUP の「インクと音声認識: さらにパーソナルなコンピューティング」(英語) で、今週紹介したインクと音声認識について完全に理解しているかクイズに挑戦して XP とポイントを獲得してください。また、どのようにこれらの機能をアプリで使ってユーザーに喜ばれるエクスペリエンスを提供するつもりか、#Win10x10 を付けて Twitter (@WindowsDev) 宛てに英語でアイデアをお聞かせください。