公開日:2019/11/30
title.png

8th Season


#8-6 音声合成、の、マネ事

 今回は、DA変換器とSDカードI/Fを使って、音声合成に挑戦です🔥🔥🔥
 最近の人工知能(A.I.)が喋る、流ちょうな音声合成とは異なり、「いかにもコンピュータに喋らせました」的な、黎明期の音声合成ですので、あんまりハードルを上げない様、お願い申し上げます。

音声合成の実現方法

 「音声合成」と聞いて、真っ先に思いつく方法で、実現します。

 日本語には、表意文字である「漢字」の他に、表音文字である「ひらがな」が存在します。音声合成して、音として発声させるのですから、表音文字に対して処理するのが順当です。そこで、「ひらがな」に着目すると、日本語のひらがなは「五十音」と言って、綺麗な直交テーブルになっています。
 つまり、「あかさたな……(子音)」と、「あいうえお(母音)」で直交しているので、発声テーブルを作り易いはずです。
 子音(あかさたな……)×母音(あいうえお)の組み合わせで、一文字づつ発声した音声データをWAVファイルとして蓄積しておき、各々、読み上げたい文字に対応するWAVファイルを再生していくことで、任意の言葉を喋らせることができるはずです。
 例えば、「あいうえお」を発声する時には、「あ」の発声データをWAVファイルから再生 → 続いて「い」のデータを再生 → ……と、言う手順です。
 WAVファイルの再生は、「#7-9 WAVファイルを再生する」で実験しましたので、その応用で行けます。

 ただし、各文字の発声データは、それぞれ1つしか持たない形式となるので、言葉の抑揚やイントネイション、アクセントなどを表現することができません。そこら辺が、「いかにも」な感じの、「黎明期の音声合成」たる所以です。

50音ではない(足りない)

 早速、発声用のWAVファイルを生成して行きたいと思います。
 前節で、「五十音」とは書きましたが、実は、50個の音だけでは、全く足りません。

五十音

 まずは、五十音が必要です。ローマ字を併記すると、「あaかkさsたtなnはhまmやyらrわw」×「あaいiうuえeおo」のマトリクスで50音となります。

濁点、半濁点

 ここにまず、濁点と半濁点の付いた子音を追加する必要があります。
 ローマ字を併記すると、「がgざzだdばbぱp」×「あaいiうuえeおo」の25音となります。

長音、促音

 さらに、伸ばす音「ー」である長音と、詰まる音「っ」である促音が、全ての文字に対して必要となります。

カタカナ語への対応

 さらにさらに、カタカナ語に対応するために、「母音(あいうえお)」とではなく、「ゃぃゅぇょ」の小さい文字とのマトリクスが必要となります。ただし、この場合、組み合わせる相手は子音ではなくて、各行「い段」の文字とのマトリクスとなります。
 どういうことか?と言うと、

  子音と母音とのマトリクスでは:「かきくけこ」……これは既に上で議論済み
  か行い段「き」と「ゃぃゅぇょ」とのマトリクスでは:「きゃkyaきぃkyiきゅkyuきぇkyeきょkyo」

 「キャンバス」に使われる「きゃ」や、放射能の単位「キュリー」に使われる「きゅ」等が含まれます。「カタカナ語」とは言ってみましたが、「きょう(今日)」の「きょ」なども含まれてますね。

 例示した「か行い段」の「き」だけでなく、全ての「い段子音」に適用する必要があります。
 ただし、わ行に関しては、い段の「い」ではなくて、「う」とのマトリクスで、「うゃwyaうぃwyiうゅwyuうぇwyeうょwyo」のデータを発生させることにします。ほとんど使い道がなさそうですが、「ウェイト」の「うぇwye」、「ウィーク」の「うぃwyi」などが含まれています。
 「や行い段」の「いゃyyaいぃyyiいゅyyuいぇyyeいょyyo」には、考えうる範囲で使えそうな文字は1つもないのですが、マトリクスの直交性を維持して処理を簡略化するため、一応、発声データには含めておきます。

 まとめて、ローマ字を併記すると、「きkしsちtにnひhみmいy(や行い段)りrうw(わ行う段)ぎgじzぢdびbぴp」×「ゃyaぃyiゅyuぇyeょyo」の70音となります。

合計で何音になった?

 最後に、忘れずに「んnn」もカウントすると:(15(akstnhmyrwgzdbp)+14(kstnhmyrwgzdbp&y))×5(aiueo)×3(そのまま・長音・促音)+1(nn)=436音となります。

 これでもまだ対応できない文字にお気づきでしょうか?「ティーカップ」の「てぃ」がありません。
 「て」は、た行ですが、い段ではなくて、え段なので、「ち(た行い段)」×「ゃぃゅぇょ」には含まれませんでした。一方、「ちぃtyi」と言う音は、長音を使い、「ちーti-」としても代用可能です。そこで、ここだけ例外として、「ちゃtyaてぃtyiちゅtyuちぇtyeちょtyo」の発声データを生成することとします。
 同じ理由で、「ぢぃdyi」も、「でぃdyi」と発声させることで、「アルディーノ」等に対応可能とします。

 これで大抵の発声には対応できるはずです。

五十音発声データの生成

 五十音と言いながら、50音では全く足りず、436音もの発声データを、WAVファイル化する必要があるとわかり、途方に暮れてしまいました。

 436音、自分でマイクに向かって発声して、WAVファイルに録音しますか??

 止めましょう。以前より何度か使っていますが、Windows 10には、音声合成API(SAPI;Speech API)がインストールされていて、拙作のsPeachを使うと、テキストファイルを読み上げさせることができます。読ませるだけではなくて、WAVファイルに出力することも可能なので、この機能をうまく使えないか?と考えました。
 でも、sPeachだと、1文字づつWAVファイルに出力するのも、全てのWAVファイルをミキシングするのも、手作業となってしまい、その手間を考えると、まだ現実的ではありません。

 そこで、拙作Olive+のスクリプトを使うことにしました。
 Olive+のスクリプト言語でも、SAPIを使ってテキストの読み上げを実行したり、読み上げデータをWAVファイルに保存することができます。さらに、sPeachの機能である、WAVファイルのミキシングにも対応しているので、発声データ(WAVファイル)の生成を自動化できます。

 スクリプトの手順としては、「あいうえお かきくけこ ……」と言ったマトリクスに従って、1文字づつ発声させたWAVファイルを436個生成します。以下の様な感じです。

  aviwav speak, "あ", $(tmpfn)#(cnt)".wav";
  cnt=cnt + 1;
  aviwav speak, "い", $(tmpfn)#(cnt)".wav";
  cnt=cnt + 1;

    : 436文字分繰り返し

※実際のスクリプトでは、各文字の生成にもマクロを使っているので、もう少し複雑な記述になっています。完成版スクリプト(speak_jp.olv)でご確認ください。

 最後に、436個のWAVファイルをミキシング機能で1本のWAVファイルに統合して完成です。

  aviwav mixing, "speak_jp.wav", "mixing.csv";

※同

 436本のWAVファイルのままでは、再生する度にファイルのopen/closeが必要となり、Arduinoの様に非力なMCUで実行する場合の足かせになりますので、最後にミキシングして、1本の発声データファイル「speak_jp.wav」に統合しておくことが、肝要です。

1音の発声データ

 Olive+のスクリプトをWindows 10 Home (x64)で実行させて、この時の1音分のWAVファイルをsPeachで開いて確認したところ、以下の様になっていました。

ar86_1on.png

 436音分の発声データを1つのWAVファイルに詰め込んでおき、後で任意の文字だけを抜き出してWAV再生したいので、上のデータを根拠に、「1音のデータを500ms間隔で整然と並べる」ことに決めました。従って、例えば、表の中で145番の文字(speak_jp.olvでは、145:「あー」)を発声させたい場合には、145×500ms=72.5s〜73.0sの間を再生すれば良いことになります。

※なお、この検証結果は、執筆時点のWindows 10 Home+SAPIによる結果です。アップグレイドなどによって、SAPIの仕様が変わったり、別バージョンのWindows(8等)で実行すると、異なる結果となる場合があります。適宜修正してお使いください。

発声データを生成する

 完成したスクリプトを以下にリンクしておきます。

  完成スクリプト:speak_jp.olv

 このスクリプトをOlive+で読み込んで実行してください。436音分のWAVファイル生成と、ミキシング(統合)を実施するので、そこそこ時間は掛かります。ただし、436音、自分でマイクに向かって録音することを思えば、スクリプトの実行終了を待つだけなので、苦にはならないはずです。
 終了すると、スクリプトと同じディレクトリに「speak_jp.wav」と言うファイルができたはずです。このWAVファイルは、16ビットデータ、ステレオ(2チャンネル)になっています。Arduinoで再生するには、8ビットデータ、モノラル(1チャンネル)にしないと、処理能力が足りなくて、雑音にしか聞こえなくなってしまうので、sPeachなどを使って所望のフォーマットに変換してください。sPeachを使った変換方法はこちらで解説してあります。

 変換したら、Arduinoで使用するSDカードにコピーしておいてください。ファイル名は、デフォルトでは「speak_jp.wav」を認識しますので、特に問題なければこの名前で、SDカードのルートディレクトリにコピーしてください。

※完成したWAVファイルは、結構な大きさになるので、提供いたしません。この先の実験を進めるのであれば、各自、スクリプト(speak_jp.olv)を実行して「speak_jp.wav」を生成してください。スクリプト(.olv)の実行にはOlive+が必要です。ダウンロードコーナやベクターのHPからダウンロードしてインストールしてお使いください。

変則ローマ字

 Arduino IDEだと、日本語はunicode(UTF-8)になるのですが、個人的に好みではありません(断然、S-JIS派)。そこで、Arduinoに読み上げさせる文字列は、7ビットASCIIコードで表現する「ローマ字」とします。
 ここまでに示してきた、訓令式ローマ字を基本として、ヘボン式ローマ字には対応しません。

  例:「ちゃ」は、「cha」ではなく「tya」
  例:「じ」は、「ji」ではなく「zi」
  例:「しゃ」は、「sha」ではなく「sya」

 さらに、長音(定まったルールがない?)と、促音(次の子音を重ねる)の処理は、解析が複雑化するので避け、長音にはハイフン(−)、促音にはシングルクオテーションマーク(’)を使うことにします(ここから変則)。
 「ん」も、本来のルールでは、「n」の次が母音なら「な行」、母音以外なら「ん」となるのですが、これも処理が複雑化するので、常に「nn」と表記することに決めます。

  例:「真っ白」 → 「ma'siro」
  例:「トータル」 → 「to-taru」
  例:「満天の星」 → 「manntennnohosi」
  例:「きゃりーぱみゅぱみゅ」 → 「kyari-pamyupamyu」
  例:「11月」 → 「zyuuitigatu」または「zyu-itigatu」
  例:「仕事のABC」 → 「sigotonoeibi-si-」

 Arduinoで読み上げ可能な文字列は、この変則ローマ字のみです。アルファベットや数字などは一切発声できないので、上例の様に、変則ローマ字に変換して発声させてください。
 処理速度の限られたArduinoに於いて、解析しながら発声するので、処理の複雑化は極力避けたいと考えました。

開発環境(ターゲットボード)

 うまくすると、いつものArduino UNOでもなんとか処理できるかもしれませんが、まずは、Cortex-M3 @72MHzと、処理速度に余裕のあるSTM32ボード(下図)をターゲットにしたいと思います。

ar41_stm32.jpg

 「#7-10 WAVファイルを再生する(回収編)」で実験して解った様に、処理速度が出力レートを決め、出力レートが音質に直結するので、まずは、STM32でトライします。余裕があれば、Arduino UNOでも試してみたいと思います。

 STM32は3.3V I/OのMCUですので、SDカードI/Fも、12ビットDA変換器(MCP4725)も全て3.3Vで動作させれば、レベルシフタなしで回路を構築することができます(下図が必要な部材)。

ar65_sdsock.jpgar79_dac12.jpg

※手作りのSDカードI/Fについては、「#6-5 microSDカードI/Fを自作する」をご覧ください。市販のSDカードI/Fでも、構成可能です。12ビットDA変換器(MCP4725)の使い方に関しては、逆引き索引から記事を漁ってみてください。

 「#7-9 WAVファイルを再生する」にて、ノイズ対策のために追加したアクティブLPFも効果が無かったので、取り外して、回路図は以下の様にシンプルなものになりました。

ar86_schem.png

※この回路をSTM32で構築すると、「#7-10 WAVファイルを再生する(回収編)」で作成したプログラム(play_wav.ino)を、STM32ターゲットでコンパイル&書き込みして、SDカード上のWAVファイルを再生することもできます。

音声合成プログラムの設計

 処理の流れは、変則ローマ字を解析 → WAVデータ読み出し → DA変換器へ出力、して喋らせます。

変則ローマ字の解析

 変則ローマ字で記述された文字列データを、先頭文字から順に解析して行きます。以下、解析手順です。

 ここまでくれば、発声データ(WAVファイル)内の再生位置は、nchr×<1音分の時間spk_usec_vlen=500ms>+<1音の開始時間オフセットspk_usec_vofs=100ms>で、求められます。

 変則ローマ字解析位置にて、句読点(「。」および「、」)に対する、無音時間を確保するために、空白( )、カンマ(,)、ピリオド(.)の3文字を認識します。挿入する無音時間の長さは、空白( ):カンマ(,):ピリオド(.)=1:2:3とし、一番短い空白( )の時間を、プログラム先頭付近の定数spk_msec_rest(デフォルト=100ms)で指定可能です。
 無音時間に関する文字を複数重ねた場合には、重ねた分だけ時間待ちが発生します。例えば、「,.,.」で、約1sの無音時間となります。

 また、認識できないローマ字を見つけた場合には、無視して読み飛ばします。エラーなどの表示は無いので、もし、発声されなかったり、意図しない文字を発声するようなら、まずは、変則ローマ字に誤りが無いかどうか、確認してみてください。

※デバッグ用に、#define DEBUG_ROMANを用意しておきましたので、必要に応じてご活用ください。

WAVデータ読み出し

 前節のnchrを使って、WAVファイル内の再生位置(時間)がわかっているので、「#7-10 WAVファイルを再生する(回収編)」で示したプログラム(play_wav.ino)で作った、fget_wave12()関数がそのまま流用可能です。

DA変換器へ出力

 こちらも、12ビットDA変換器(MCP4725)へ出力するだけなら、dac12_write()関数を流用して終了です。

 しかし、ここでは、もう少し考えてみる必要があります。
 ただでさえ、抑揚のない、不自然な単音の羅列となる、今回の音声合成において、全ての音を同じ時間間隔で喋らせるのは、更に不自然さを助長する結果となります。

 先程示した図の再掲となりますが、以下図は500ms間隔で発声データに統合した発声データのサンプルです。他の文字のデータも色々と眺めてみると、読み上げた文字によって発声時間が短かったり、長かったり、まちまちです。

ar86_1on.png

 これを、常に500ms毎に再生するのではなく、無音部分を検出して、そこで再生を打ち切り、次の文字に移った方が、文字と文字の間が空かずに、少しはマシになるのではないか?と、期待できます。

無音部分の検出

 まず、発声データは、SAPIで喋らせてWAVファイルに落としてあるので、無音部分が「ほぼ」無音です。8ビット符号無データ換算では、128と言うデータになっています。12ビット符号無データ換算では2,048です。

 プログラム内の処理は、12ビット符号無データで統一してあるので、2,048±αの範囲を無音と識別します。以下、無音部分検出コードの抜粋です。

  // silence detection
  if (abs (wdat - 2048)>spk_sildet_lsb)
    stat=1;
  else if (stat>0 && abs (wdat - 2048)<=spk_sildet_lsb)
    stat++;
  if (stat>=spk_sildet_cnt)
    break;


 変数statが、再生中の状態を表しています。

 アルゴリズムとしては、こんな感じでしょう。あとは、実際に喋らせてみて、定数を調整します。
 注意点は、spk_sildet_cntによる、無音部分の検出手段が、「回数」であって、「時間」ではないことです。このコードは、DA変換器へアナログデータを出力するループ内の処理なので、出力レート毎に実行されます。よって、出力レートが変わってしまうと、spk_sildet_cntの最適値も変わってしまう(再調整が必要)ということです。
 MCUの処理速度もそうですが、発声データ(WAV)ファイルのフォーマットが変わっただけでも、出力レートは変化するので、再調整が必要となります(WAVファイルのフォーマットと出力レートの変化については、こちらを参照してください)。

こいつ、喋るぞ

 では、完成版プログラムをリンクしておきます。

  完成版プログラム:speak_jp.ino

 STM32で回路を構成した後、コンパイル&書き込みしておいてください。シリアルモニタから、以下のコマンドを受け付けるようになっています。

 以下、実験中の様子です。

ar86_expr.jpg

  実験サンプル:【 】内はシリアルモニタからの送信データ
  【spk,konnnitiwa,watasinonamaewa,arudyi-no desu】
  OK+ speak konnnitiwa,watasinonamaewa,arudyi-no desu
  OK: speak: 3731768[us]/38284[smpl]=97[us/smpl](10259[Hz])

 喋るのに要したWAVファイルの再生時間と、その間にDA変換器へデータを出力した回数、データ出力レート、その逆数である出力周波数が情報として表示されます。
 アンプ内蔵スピーカに出力して、スマホのボイスレコーダソフトウェアで録音したデータをお聞かせします。

   :ar86_rec_stm.wav

 こんな感じですけど、どうですか?何とか、聞き取れませんか?

  実験サンプル:【 】内はシリアルモニタからの送信データ
  【spk,nihonngo sukosi,hanasemasu】
  【spk,zyuuitigatu rokuniti,gogo sitizi sannhunn】
  【spk,tihayaburu,kamiyomokikazu,tatutagawa.】
  【spk,karakurenayini,mizukukurutowa】
  【spk,naninitukaukawa,anatano aideasidai desu】

 後は、皆様の耳でご確認ください。

UNOは喋るかな?

 予告通り、Arduino UNOでも試してみました。結論から言うと、UNOでもちゃんと聞き取ることができました。

 回路は、「#7-10 WAVファイルを再生する(回収編)」で示した回路図がそのまま使えます。プログラムも、前節でSTM32用にコンパイル&書き込みしたspeak_jp.inoが、そのまま使えるようにしてあります。

 以下、実験中の様子です。

ar86_expr2.jpg

※尚、以前の実験で効果が認められなかったアクティブLPFは削除、MCP4725への電源は5Vを供給するなど、簡略化した回路で実験しました。

  実験サンプル:【 】内はシリアルモニタからの送信データ
  【spk,konnnitiwa,watasinonamaewa,arudyi-no desu】
  OK+ speak konnnitiwa,watasinonamaewa,arudyi-no desu
  OK: speak: 3228636[us]/14064[smpl]=230[us/smpl](4356[Hz])

 ご覧の通り、出力レートがSTM32の倍以上に伸びているにもかかわらず、STM32に比べても聞き劣りしませんでした。な〜んだ、UNOでも全然行けるじゃない。信じてあげられなくてゴメン → UNOっち🤟

 以下、UNOで喋らせた時の録音データです。

   :ar86_rec_uno.wav


 こそっと、ノイズについての考察を一切挟まなかったのですが……、「#7-10 WAVファイルを再生する(回収編)」で指摘したノイズ(恐らくデジタルノイズの回り込み)については、今回の回路でも対策はしていません。あしからず。


btn_arduino.png

btn_prev.png btn_next.png

home