Windowsの機能で喋らせる(Speech APIのなんとやら)

Windowsで喋らせる実装、ゆっくりことAquesTalkが有名です。

今回はAquesTalkを使わず、Microsoftの提供する機能を使いWindowsを喋らせる話を書きます。

Microsoftの提供する諸々について

ググっても情報が分散していたりしてよくわからない。MicrosoftのWebサイト見に行ってもそう。

docs.microsoft.com

f:id:W53SA:20200724124547p:plain

「Speech Platform SDK」とか「Speech SDK」を使えば喋ってくれそう。なんか下の方に「Speech API」って見えるけど、これと前者の関係はなに?「Speech API」と「Speech SDK」の違いは??

ja.wikipedia.org

Wikipedia見てもいまいち。

ぐぐると「XPではデフォルトで音声合成ができない」とか出てくるけど平成も終わったのにXPってなんですねん令和の話がしたいんじゃ!

なんか調べるとCOMなAPIで叩けば喋ってくれそう。多くの人がWindows10セットアップ時に無効化することで有名なCortanaもAPI叩けば喋ってくれるらしい(後述)。

世界をまとめるのは諦めてとにかく喋らせたい!!!!という方向で以下にざっとまとめていきます。Windows10だと、特段なんかインストールしなくても喋ってくれそうね。

COMなAPIだと色んな言語で叩けそうですが、WinAPI(C++)でやっていきます。dotnetの場合については最後にちょろっとだけ。COMの叩き方*1は誰が書いても同じようなものになるので特に言及しません。

喋らせ方

喋らせるには、COMでSpeech API(以下SAPI)を叩けばいいことがわかってきた。Speech APIにはいくつかバージョンがあるけど、喋らせるAPI(TTS:Text-To-Speech)についてはSAPI5になってからそんなに変わっていない様子。様子って書いてるのは、さっき書いたようにバージョンの扱いがよくわからないゆえ。

COMオブジェクトISpVoiceを作って、ISpVoice::Speakにテキストを渡すと喋ってくれます。

テキストのマークアップで喋る際の諸々を調整できるらしい。cf.Text-to-Speech (TTS) Overview >> Modify Voice Attributes

音量と速度について

ISpVoiceのSetVolumeSetRateを使って、音量と速度を変えることができます。

音量は0から100で、初期値は100になってるようです。速度は -10から +10 で、初期値は0になっているようです。どちらも、範囲外の値を渡すとE_INVALIDARGを返してきます。

音量100にしても正直あんまり大きくなくて、音楽流したりしてると負けそう。もっと大きくするにはどうすればいいんだ・・・。

声を選ぶ

どの声でしゃべるかはISpVoice::SetVoiceに、喋らせたい声のISpObjectTokenを渡します。

当然ISpObjectTokenもCOMオブジェクトなので、なんとかして取ってくる必要があります。

SAPIはSDK提供のヘルパ関数が多くて、認識できる声一覧オブジェクトIEnumSpObjectTokens を取ってくるSpEnumTokensのような関数から、コンボボックスに一覧として追加するSpInitTokenComboBoxのような関数まで用意されています。なお、コンボボックスに登録された声オブジェクトはISpObjectTokenで、ダイアログを閉じる際にはよろしく解放する必要があり、SpDestroyTokenComboBoxという解放関数もあります。

レジストリなどに選ばれた声を記録しておくには、選んだ声に対してISpObjectToken::GetIdを呼んで返ってきた文字列を書くことになります。逆(レジストリなどに書かれたIDから声オブジェクトをつくる)は、ヘルパ関数SpGetTokenFromIdを呼びます。

この「ID」は、声の実体であるTTSサービスを指定するレジストリエントリのパス*2です。

声の一覧を取ってくる

SpEnumTokensやSpInitTokenComboBoxなどのヘルパ関数を呼ぶことで一覧を取ってこれます。これらの関数、どちらも最初に「カテゴリID」というものを指定する必要があります。だいたいは「SPCAT_VOICES」というマクロを指定するようで、これはsapi.hとかsapi5.hで定義されていて値は「HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech\Voices」です。

なぜか、Microsoft Speech PlatformとかCortanaの場合はこのレジストリパスだけが違う様子。

  • Microsoft Speech Platformの音声は「HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech Server\v10.0\Voices」
  • Microsoft Speech Platform 11の音声は「HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech Server\v11.0\Voices」
  • Cortanaの音声は「HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech_OneCore\Voices」

ヘルパ関数のカテゴリIDにこのレジストリパスを書くと正常に認識してくれたので、ユーザ選択画面を出すなど一覧を表示する場合は、SPCAT_VOICESだけでなく、これらのレジストリパスも読んであげると良いのではないでしょうか。

インターネットを探索すると、「レジストリSpeech Server\v11.0/Voicesの中身をSpeech\VoicesへコピーすることでSAPI対応させる」という気合のある記事とか出てきます。

どうしてこうなった、、、

非同期に喋らせた場合の終了検出

ISpVoice::Spaeakの引数に渡せるSPEAKFLAGSに、「SPF_ASYNC」というのがあって、これを指定して呼び出すと発話終了を待たずに処理が返ってきます。

発話終了を検出しないとまずい場合、いくつかの手段で検出できます。

なんか例外が飛んでる

デバッグ実行すると、発話開始時(RPC_S_SERVER_UNAVAILABLE)と終了時(RPC_S_CALL_CANCELLED)に例外が飛んでるのが見えてる。

0x00007FF863189129 (KernelBase.dll) で例外がスローされました (a.exe 内): 0x000006BA(RPC_S_SERVER_UNAVAILABLE): RPC サーバーを利用できません。
0x00007FF863189129 (KernelBase.dll) で例外がスローされました (a.exe 内): 0x0000071A(RPC_S_CALL_CANCELLED): The remote procedure call was canceled, or if a call time-out was specified, the call timed out

何が起きてるのか。。。。。

SAPIに対応したサードパーティ

Microsoft提供のTTSサービス使う前提で書いてきましたが、CeVIO Creative StudioはSAPI対応してるらしいです(未検証)。
guide2.project-cevio.com

マネージドに喋らせる

名前空間System.Speech.SynthesisSpeechSynthesizerクラスが.NET Framework 3.0から存在していて、こいつをよしなにすると喋ってくれるようです。

が、このクラスは内部でメモリリークをするらしい(????)。

Stackoverflowに書かれてるURIhttp://connect.microsoft.com/VisualStudio/feedback/details/664196/system-speech-has-a-memory-leak」を叩くと、「Microsoft Connect Has Been Retired」とかいうページに飛ばされて、「We have reviewed and migrated bugs to Developer Community. 」とか言うので探してみたけど出てこない(マイクロソフトのこういうとこ嫌い)。

どうなってるのかしらねこれ、という気持ちになるけど二番目の記事は最後に「Microsoft Speech Platform 11 のSDK入れて名前空間Microsoft.Speech.SynthesisSpeechSynthesizerを使えばリークしないぞ」って投稿がある(未検証)。

*1:CoCreateInstanceでほげ、というあたり

*2:たとえば、「HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech_OneCore\Voices\Tokens\MSTTS_V110_jaJP_SayakaM」

*3:ISpNotifySource::SetNotifyWindowMessageを使った場合は、同じメッセージID/WPARAM/LPARAMでウィンドウメッセージが投げ込まれる。