クレウスさんの、
C#は倒しにくいけどJSならなんとかなる。
帰ったらnode.jsと戦う。— クレウス@VRChat (@kleus_balut) 2018年9月28日
なところから、Web Speech API がブラウザ上で使えることが分かったので試してみるの巻です。
Web Speech APIの実装 – Speech Synthesis API | CodeGrid https://app.codegrid.net/entry/2016-web-speech-api-1
ブラウザ上で Web Speech API を使って文章の読み上げができる、って話は聞いてことはあったのですが、これ OS の機能をブラウザから呼び出している訳ではなくて、ブラウザの内部機能として実装されている、という話だったのですね。なので、Google Chrome で読み上げると Google の音声で喋るし、Edge で動かすと Microsoft の音声出力になります。おそらく Safari とかも違う音声になっているはず。
Javascript は簡単
読み上げするだけなら非常に簡単で、
1 2 | var text = “hello world.”; speechSynthesis.speak(new SpeechSynthesisUtterance(text)); |
と Javascript で書くだけです。text の部分は、文章を選択した場所でもよいし、何かを喋らせたい文章を書くもよし。声質も変えられるので、自前であれこれできます。
じゃあ、.NET から使ってみよう
ブラウザ上で音声を出せることは分かったけど、では、.NET からどう扱えばいいのか?ってことを考えると悩みどころなのですが、まあ、定番の WebBrowser か WebView を使えばなんとかなるでしょう?と考えました。
が、試してみると、意外と難関があることが判明。
- WinForms や WPF で使われている WebBrowser は内部で IE を使っているので、Web Speech API の対象にならない。
なので、確か WPF などで Edge エンジンを使えるようになったような気がしたので、調べなおしてみると、
WPFやWindowsフォームでEdgeのWebViewを使うには?[Windows 10 1803以降]:.NET TIPS – @IT
http://www.atmarkit.co.jp/ait/articles/1807/04/news017.html
に、ありますね。
どうやら、NuGet で「Windows Community Toolkit v3.0」を落としてきて、その中にある WebView を使えばよいそうです。Microsoft.Toolkit.Win32.Controls を NuGet でダウンロードします。ちなみに、このコントロールは .NET Framework 4.6.2 以上なので、別途 .NET Framework をダウンロードする必要があります。開発者向けのをダウンロードしないといけないので、https://docs.microsoft.com/ja-jp/dotnet/framework/install/guide-for-developers から新しいものを入れてください。
ツールボックスに「WebView」コントロールが増えるので、これをウィンドウに貼り付けます。
読み上げるための TextBox と Button も貼り付けておきます。
グレーの WebView は、呼び出し用にあるだけなので見えなくても大丈夫なはず。大抵の例は、ブラウザ上で Web Speech API を動かすわけですが、ここでは WPF に貼り付けてある TextBox を読み上げるようにします。
InvokeScript で Javascript を実行する
ブラウザ内にある Javascript を実行するのに、WebView には InvokeScript というメソッドがあります。WebBrowser のときには、WebBrowser.Document.InvokeScript だったのですが、WebView では Document がなくなっていて、直接 InvokeScript メソッドが追加されていますね。Document がなくなったのでブラウザの DOM を直接見ることはできなくなったのですが、まあ、InvokeScript があればトリッキーなことをして DOM を探ることもできるので問題はありません。
以下、は動作コードですが、いつか試行錯誤の痕跡を残しておきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); this .Loaded += MainWindow_Loaded; } private void MainWindow_Loaded( object sender, RoutedEventArgs e) { // web.IsJavaScriptEnabled = true; // web.Navigate("http://moonmile.net/speech.html"); // 非推奨だがローカルファイルから読み込む web.NavigateToLocal( "speech.html" ); } private void clickGo( object sender, RoutedEventArgs e) { var text = @" <body> <script> function hello(text){ speechSynthesis.speak(new SpeechSynthesisUtterance(text)); } </script> </body> " ; // 直接文字列を入れるとダメ // web.NavigateToString(text); this .web.InvokeScript( "hello" , new string [] { text1.Text } ); } } |
NavigateToString メソッドを使って、直接 HTML を入れることはできるのですが、なぜか InvokeScript メソッドを呼び出したときにアプリケーションごと落ちてしまいます。仕方がないので、Navigate メソッドで外部サイトから読み込むか、NavigateToLocal メソッドでローカルファイル(exeと同じフォルダ)から読み込みます。NavigateToLocal は非推奨ってことになっていますが、まあ、そのまま使えます。InvokeScript メソッドの呼び出しで落ちるのは、何かの初期化が足りていないんでしょう。
InvokeScript メソッドは、HTML 内にある Javascript の関数を直接呼び出せます。パラメータも渡すことができるので、これを TextBox から取り込みます。
ローカルファイルにある speech.html の内容は、以下のように hello 関数にひとつの引数を入れたものだけです。
1 2 3 4 5 6 7 | <body> <script> function hello(text){ speechSynthesis.speak( new SpeechSynthesisUtterance(text)); } </script> </body> |
後は、ボタンを押して InvokeScript を呼び出せばふつうに Edge で喋ってくれます。
これは Unity で使えるのか?
さて、肝心のこの仕組みは Unity で使えるんでしょうか?って話なんですが、たぶん、ダメだと思うんですよね。UWP だと使えるんでしたっけ?
と思って、UWP で使ってみると…あっさりいけますね。UWP のほうはもともと内部的に Edge が使われているので大丈夫そうです。NavigateToString メソッドも普通に使えます。InvokeScript はダメで、InvokeScriptAsync だとうまく動きます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | public sealed partial class MainPage : Page { public MainPage() { this .InitializeComponent(); this .Loaded += MainPage_Loaded; } private void MainPage_Loaded( object sender, RoutedEventArgs e) { // this.web.Navigate(new Uri( "http://moonmile.net/speech.html")); var text = @" <body> <script> function hello(text){ speechSynthesis.speak(new SpeechSynthesisUtterance(text)); } </script> </body> " ; this .web.NavigateToString(text); } private async void clickGo( object sender, RoutedEventArgs e) { await web.InvokeScriptAsync( "hello" , new string [] { text1.Text }); } } |
ってことは、Unity でもいけるのかも。
~~~
追記 2018/10/01
Unity はやってないのでわからないのですが、Xamarin.Forms と Xamarin.Android の WebView で試してみました。結果は「ダメ」です。Android 上の Chrome では動くのですが、WebView の内部で使っている Chrome(Chroniumらしい)が対応していないらしく、AspeechSynthesis を認識できません。ちなみに音声を出すだけならば、TextToSpeech があるので、それを使えば ok. iOS の WebView の場合は AspeechSynthesis が使えるようです。