僕のホラは、次の3月18日にリアルになるか?

Will my big talk be real next March 18 ?


2022/12/25
藤田昭人


5日間連続投稿の最中に kun432 氏に「こっちにも投稿しませんか?」と 誘われて…

qiita.com

実は昨日は一日 Qiita の markdown と格闘してました。 が、やっぱりエディタは慣れてないとねぇ…
どうやら Advent Calendar の執筆は 外部のブログでも良いようなので、 結局、慣れてる「はてなブログ」 で執筆することにしました。

これは「5日間連続投稿」の総集編*1とご理解ください。 Advent Calendar との連携の都合上、 先に公開してないといけないようなので いつもとちょっと勝手が違いますが…


僕の「5日間連続投稿」について

僕は毎年このシーズンになると 京都ノートルダム女子大学の授業「インターネット社会論」の ゲストスピーカーに呼ばれてAIの講義をしてきました。 で、今年の授業に向けて作成したデモプログラムの作成過程を 5つのトピックに分けてブログに書きました。

今、注目のオープンソース音声合成ソフト SHAREVOX の core ライブラリだけを使って、 京都検定 の問題を読み上げる CLI ベースの text-to-speach コマンドを 作成した報告です。

ちゃんと動くところまでは持っていったのですが、 読み上げてくれる「つくよみちゃん」 の声質が気に入らず、 授業での紹介は控えた…という顛末でした。 なので、今のところ未完です😀


SHAREVOX について

SHAREVOX は話題のオープンソース音声合成ソフト VOICEVOX から派生した COEIROINK に続く2番目の fork です。

たぶん、次の YouTube 動画を見てもらうのが一番早そう…*2

www.youtube.com

見た目は macOSWindows で動く ダブルクリック・アプリケーションですが、 中身はウェブ・アプリの構成になっています。

github.com

この構成は派生した COEIROINK と SHAREVOX の2つでも共通で、 僕は SHAREVOX の core を使って読み上げアプリケーションを作りました。

ちなみに…

見た目のポップさとは裏腹に、 VOICEVOXシリーズは 本格的な機械学習の技術を使って 実現されています。

shirowanisan.com

これは COEIROINK の開発者の シロワニさんのブログです。 記事数が少ないので全部目を通すのは 比較的ラクなんですが、 音声合成に関する arXiv の論文 とかを紹介したり… とか機械学習の先端研究の成果を取り入れて かなり高度なことをしていることがわかります*3

前述の、僕が記事にした core ライブラリは、 名工大OpenJTalkONNX runtime による音声合成の複合体です。 GPU を搭載していない僕の旧式の Mac Book でも 実用的なスピードで動くのには驚いてます*4


スマートスピーカーとの関わりは?

しかし kun432 氏は 音声合成ネタオンリーの僕のブログ記事をみて、 何故スマートスピーカーアドベントカレンダーに 誘ってくれたのでしょうかねぇ???

思い当たるのは…

僕を毎年授業に呼んでくれる 吉田智子先生が書いた 12月15日の僕の講義のレポート です。絵が小さいので再掲すると…

次世代AIスピーカー

これは僕が今年の講義で話した 独自研究による「近未来予想」… 平たく言えば僕のホラです😁

ひょっとして、彼は ここまでチェックしてたのかも?

もちろん、これは ただの思いつき…って訳ではなくて、 元祖チャットボットの ELIZE に備わっていた 傾聴対話機能 (厳密には ELIZA が実行するスクリプト DOCTOR の機能) をスマートスピーカーに当てはめた アイデアです*5。第1次AIブームの頃、 チャットボットは「会話を楽しむプログラム」 として(ゲーム的な)人気を博したのですが…

今世紀になったあたりから 「チャットするためのボット」 つまり(自然言語で)何かを命令できるボット との認識が広まった感が僕にはあります。 その延長上の 「音声チャットボット専用デバイス」 としてスマートスピーカーは 位置付けられたのではないでしょうか? でも、ホームユースにおいて 命令したいことってそれほど多くない。 確かに「電気を灯けて」とか 「3分間測って」といったシンプルな命令は 非常に需要は高いと思います。 が、それ以上に高度な命令って なかなか思いつきません。 最近のスマートスピーカーの先細り感の 本質的な原因はこういうところあると 僕は考えています。 やはりチャットボットの原点である 「会話を楽しむデバイス」 に立ち戻った方が良い というのが僕の意見です。

そこには「一人暮らしの高齢者」である 僕の個人的な事情も含まれています。 事実、定年退職を境に誰かと会話する機会が 急激に減りました。そういう生活を半年も 続けていると、なんだか滑舌が悪くなったり、 他の誰かの話し声が聞き取りにくくなったり… もちろん目や声に異常がある訳ではなく、 日常的に声を出して会話する機会が減ると 会話する能力が衰えていくようです。 コロナ禍の影響で同じような体験をした方も いらっしゃるんではないかと思うのですが、 僕のような高齢者の場合は特にこの能力低下が 著しいようです*6

授業では、 このような「人間に寄り添うAI」をメインテーマに、 さらに加齢による記憶力や認知能力の低下を補う 「知的義肢としてのAI」のコンセプトについて 学生諸君に話をしました。


対話してくれるのは誰か?

授業では「次世代AIスピーカー」のコンセプトや 開発目標のみを簡単に説明するに留めましたが…

この場では実現方法に関する これまでの検討について簡単に述べます。

「人間の比較的短い音声メッセージに スピーカーが音声で応答する」 従来のスマートスピーカーとは逆に、 次世代AIスピーカーの場合は 「スピーカーの比較的短い音声メッセージに 人間が音声で応答する」 ので「人間が発話するロングメッセージ」に対応する 音声認識技術に開発の重点を置かなければならないと 考えていたのです。実は夏頃までは…

が、今年の9月に OpenAI *7 が Whisper を公開しました。

openai.com

あくまでも僕個人の見立てなのですが…
ソースコードも公開されている Whisper は音声認識技術の ゲームチェンジャー*8だと僕は考えてまして、 この技術の応用事例の発表は 既に始まってますが、 今後もしばらくは続くだろうと 想像しています*9。やはり、音声認識技術に関しては Whisper の動向を しばらくウォッチしておいた方が 良さげかと考えてます*10

一方、音声合成技術の側、 つまりスピーカー側の発話に関する実装には、 僕が 「対話してくれるのは誰か?」 と呼んでいる問題があると考えてします。 1年ぶりに復活した記事 でも書いたように、僕はほぼ1年前に 「童話の読み聞かせ」 をする音声チャットボットを実装しました。 実装している最中は 「利用者は自分の発言が 正しく伝わっていることに 関心を持つだろう」と、 つまり音声認識に関心が集まるだろうと 考えていたのですが、 完成後に実際に利用した人の感想を聞くと 「iPhoneMacで声が違う」 といった音声合成へ関心を寄せる声が多かった。 僕にとってこれは意外な反応でした。

その後、理由を考えてみたのですが…

人間は聞こえてきた音が言葉のように聞こえると 「それが擬似的であろうが人工的であろうが そこに人格があると仮定し その存在が親和的であるか?敵対的であるか? あるいは信用できるか?否か?を 直感的に判断しようとするのではないか?」 と推測しました。また、この傾向は 高齢者の方が強いとも考えました*11

つまり、デバイスの使用感を支配するのは 音声合成技術ではないかと僕は考えています。

そこで、SHAREVOX core を使った音声合成機能だけの 「次世代AIスピーカー」 のデモシステムを作ることにしました。 その際、よく考えなければならないことが、 既に述べた「対話してくれるのは誰か?」 という問題です。

この問題を考える上でも SHAREVOX (および VOICEVOX から派生したオープンソース) は非常に良い示唆を与えてくれます。 VOICEVOX シリーズの音声合成ソフトウェアはいずれも、 複数のキャラクターをバンドルしています*12。僕は世代的に、 ゲームは全くやらないし、 漫画は読んでもアニメは ほとんど見ない人間なので、 大きく誤解してると思いますが、 VOICEVOX シリーズは ニコニコ動画のコミュニティから 出て来たソフトウェアで、 それ故に複数のキャラクターが バンドルされる事は 極めて自然なことなのだと想像しています。

一方、 数ある音声合成ソフトの中での VOICEVOX シリーズの特徴を考えると、 オープンソースであることと 徹底したキャラクター指向が挙げられるように思います。 特に音声チャットボットを構築する上では、 このキャラクター指向は対話をする人間の 関心と共感を獲得する上で 大きな助けになるのではないか と僕は考えています*13

SHAREVOX では4人6声のキャラクタが標準でバンドルされますが、 僕が特に注目しているのは「つくよみちゃん」です。 というのも、「つくよみちゃん」の声の主である夢前黎さんが 次の「会話テキストデータセット」も作成・配布されているからです。

tyc.rei-yumesaki.net

これは、いわゆる(言語)コーパスでして、 この「会話テキストデータセット」を利用すれば、 単純なルールベースのチャットボットであれば 比較的簡単に構築できそうです。


クリスマスでは終わらない

このように開発者には好条件が揃っているので、 僕は「次世代AIスピーカー」のデモシステムを作ることにしました。

最後にタイトルの「3月18日」について少しだけ…

実は、勧めてくれる方々がいたので、 次のコンテンテストに応募しました。

social-innovation.kyoto.jp

これは事業アイデアのコンテストなので プロトタイプやデモシステムは必須ではありませんし、 コンテスト当時は4分間のプレゼンテーションの時間しかありません。

ですが…

応募した本人でも事業アイデアは夢物語に聞こえそうな内容に思えるので 「何かエビデンスが必要だろうなぁ…」と考えてました。

還暦を迎えて、このところ自分の馬力の衰えを感じることが多かったのですが…

ここへ来て、冒頭の「5日間連続投稿」をなんとかやり切ったので、 久しぶりに3ヶ月一本勝負にチャレンジすることにしました。



若宮正子さんもCMで言ってるじゃないですか…
とにかくバッターボックスに立ってバットを振ってみろって😀

www.youtube.com

以上

*1:NHKの大河ドラマみたいな…

*2:僕は今年の夏、椎間板ヘルニアを発症しまして、 7月、8月、9月の3ヶ月を棒に振りました。

一番、ひどい時は10分間も椅子に座ってられなくて…
寝たきりでスマホを覗くしか やれることがなかったので、 たまたま見つけた動画です。

文字どおり「怪我の功名」ですね😀

*3:たぶん COEIROINK を開発しておられる期間だけ 書いておられたのかな? と想像してます。

*4:ちなみにマスターの VOICEVOX core は今、 Rust 対応を含む全面的な改修を 行なっている最中です。

github.com

Stable なバージョンをお望みの方は、 ひとつ前の 0.13.X か SHAREVOX core を使った方が良さげです。

github.com

*5:ELIZEと傾聴については 大昔にブログに書いたので参考まで…

akito-fujita.hatenablog.com

akito-fujita.hatenablog.com

実は1966年からあるアイデアなのです。

*6:対策はとにかく声を出して会話をすること。 今は毎朝同じ喫茶店に行って、 いつもの店員さんと努力して 世間話をするようにしています。 問題は多少改善したように感じてますが😀

まぁ、街中で暮らしている僕の場合は こういった対策が取れるのですが、 田舎暮らしのうちの両親の場合は 会話の少ない生活を何十年と続けて来たので、 認知症の症状が顕在化しています。

*7:11月に公開された ChatGPT が今話題になっていますよね?

openai.com

*8:純粋に技術的に ゲームチェンジャーであることは もちろんですが、 この技術がオープンソースであることも 音声に関わる既存技術の商業価値に 大きなインパクトを 与えるのではないかと思っています。

*9:例えば、こんなレクチュアなどは 既にたくさん出回っていますね。

gigazine.net

*10:Whisper が公開された今年の9月から半年後、 つまり来年の4月頃までは動向を静観するべき、 と僕自身は考えています。

*11:このように考えると、 過去のうまくいかなかった知見の 辻褄が合うようにも思えるのです

*12:VOICEVOX のキャラクタは以下。

voicevox.hiroshiba.jp

COEIROINK のキャラクタは以下。

coeiroink.com

SHAREVOX のキャラクタは以下。

キャラクター一覧 | SHAREVOX

いずれも音声合成ソフトの開発者ではない方々が 制作したキャラクタですので、 使用条件等には注意が必要なようです。

*13:スマートスピーカーのスキルを書いていた時の 実証実験での僕の数々の苦い経験から言わせてもらうと、 この種のITデバイスに対する 高齢者の心理的ハードルは 皆さんが考えている以上に高いのです。

つくよみちゃん Recitation(未完)

Tsukuyomi-chan Recitation (unfinished)


2022/12/23
藤田昭人


5日間連続投稿の5日目、最終回です。

スマホ全盛で、 帯域を湯水の如く使える今日では 全く需要がないであろう、 CLIベースの音声プログラムの解説記事に お付き合いいただき 大変ありがとうございます。

しかし書き始めたときには全く意識していなかったのですが…

このように「毎日、何か書かなければならない!!」 プレッシャーがかかると、 1980年代のハッキング・レポート風の 記事になっちゃうんですね、 僕は*1。 やっぱりこれが楽しくて プログラムを書いてたんだよなぁ。 すっかり忘れてたけど…

さてさて…

最終日なのでオチをつけなくちゃ‼︎

既に12月に入ってましたが 何とか t2s が動いたので、 当初から考えてた recitation なる プログラムの作成に取りかかりました。 1年ぶりに復活した記事 で口走った 「つくよみちゃん」 に「京都検定」の問題を読み上げてもらう… というヤツです。

でも、これは時間切れで 授業には間にあいませんでした。


京都検定の過去問

ちなみに…

京都検定京都商工会議所が 主催する検定試験ですが

www.kyotokentei.ne.jp

過去問題は試験後に京都新聞に掲載されます。

www.kyoto-np.co.jp

先日実施された第21回の3級の試験問題はこちら

www.kyoto-np.co.jp

1年ぶりに復活した記事 でも語りましたが、 昨年の「童話読み聞かせ」の実習が 学生たちにことの他ウケが良かったので、 「次なるネタ」と僕は密かに考えていたのです*2

僕のような京都在住者にとって、 「京都検定」の問題は 日常生活に密着したトピックが出題されます。 例えば、うちの近所の石薬師御門は、 僕にとって真夏の暑い日の夕暮れに 夕涼みがてらアイスを食べる場所なんですけどね。

それに音声合成のサンプルテキストとしても、 京都特有の(風変わりな読みの)地名や人名が たくさん登場するので、なかなか興味深い*3。なので「つくよみちゃん」に 「京都検定」の問題を読み上げてもらおうと考えてました。


SHAREVOX とCOEIROINK に「京都検定」の問題を読んでもらう

で、講義では VOICEVOX と SHAREVOX、 それから COEIROINK を紹介するつもりだったので、 資料を書く都合から SHAREVOX と COEIROINK に ひとつ前の第20回(令和4年夏)の 第1問の問題文と4択の回答を 読み上げてもらいました。

まずは SHAREVOX から…

次に COEIROINK にも…

いかがでしょうか? Twitter の動画ではわかりにくいかもしれませんが、 生で聞くと COEIROINK の声質のグレードが明らかに上でした。 特に4つの回答例の頭の ア、イ、ウ、エ の発音では SHAREVOX の発声は少し不快な感じがしました。

なるほど 「つくよみちゃん」の声の主である 夢前黎 さんが時折 Twitter で呟いているのは こういうことだったのか…


つくよみちゃん Recitation は…未完

実は、この問題に気がついたのは 授業の前々日ぐらいだったのです。 で、COEIROINKの「つくよみちゃん」ボイスを SHAREVOX に移植する方法でジタバタして半日。 結局、打つ手を思いつかずにタイムアップとなりました。

このジタバタのために大きくタイムロスすることになり、 Recitation のCコードを実装する時間もなくなり、 授業では SHAREVOX と COEIROINK のデモだけにしました。

くそぉ❗️❗️❗️❗️❗️❗️

…ということで、 つくよみちゃん Recitation は今のところ未完です。 問題は声質のグレードの問題だけではなく、 例えば回答例(ア)の「青龍」は「アオリュウ」 って読んじゃうんだよね。 これは SHAREVOX と COEIROINK 共通の問題で、 読みの修正自体は SHAREVOX あるいは COEIROINK でやれば良いのですが、その修正結果を 各々のプロジェクトファイルから 取り出す機能も必要かと…

それに学生諸君に実習してもらうには ウェブアプリ化も必要だし…

まだまだ先は長いなぁ… というのが今の僕の感想です。



でもね…

この作業に没頭していたときの僕は 明らかに35〜6年 若返っていたんだと思います。

そうそう、 僕はこういうことが 楽しいって思えるから プログラマーになったんだよ。 本当に忘れてたけど…

5日間付き合っていただいた 読者の皆さんには 申し訳ないオチですいません。

でも、僕はまだ諦めてませんからね。

それではお約束の…

#つくよみちゃんを利用してフォロワー増やしたい

以上

*1:確か 日本国内ではインターネットが全く普及しておらず、 WWW(World Wide Web)もちろん存在しなかった1986年頃、 それでも電子メールは使えていたし、 今日のSNSとよく似た使用感のある NetNews も存在してました。 そこでは…

ソースコードは全部送ってください。
でもデータ量を抑えるため diff などを活用して。

もちろん説明は必要です。 送った意味がなくなりますから…
でも最小限のコメントやヒントに留めてください。
くれぐれも、くだらないことを書いて データ量を増やさないよう 十分注意してください。

…といった大いに矛盾するチェックが行われていました。

日本国内では JUNET という公衆回線網 (電話線ネットワークですね) による情報インフラが存在したのです。 全く帯域制御のないネットワークですので、 こうやってデータ量を抑制しないと モデムが何時間もピーヒューとなり続けて、 ネットワーク管理者が 「来月の電話代はどれくらい請求されるんだろう?」 と青い顔をしていたのです。30年前ぐらいの話です。

そもそも…

民営化直後のNTTが 「社内の部門に電話代を請求するルールもシステムもない」 を逆手に取って…

NTTの某研究所が日本全国の主要大学に電話をかけまくることでバックボーンを維持する

…という公然の秘密の元に 成り立っていた ゲリラ的アプローチによる 無料ネットワークでしたので、 破綻の兆しは日常のそこらじゅうに 転がっていたのですが…

でも、そんな事情は全く知らない 僕のような鼻垂れ小僧プログラマーは ムキになってレポート風のヨタ話を 毎日投稿していました😀

たぶん当時 「こんな馬鹿げたことは そう長くは続けていけない」 と考えていたのは 村井さんとその周辺だけではないのかな?

*2:もっとも、実際の授業で「京都検定」の話題は出たのですが、 学生諸君の関心は今ひとつという感じでしたが…

*3:先ほどの「いしやくしごもん」を カナ漢字変換すると「石薬師五問」になりませんか?

SHAREVOX core の API を探る(4)jasson

Explore the SHAREVOX core API (4) jasson


2022/12/22
藤田昭人


5日間連続投稿の4日目です。(あと2日)

この記事を今日初めてみた人のために…

この記事は5回シリーズで、 12/15 に京都ノートルダム女子大学で 僕が行った講義のために用意した ちょっとしたツールの作成過程について紹介しています。 なお、毎年僕を講義に呼んでくれる吉田智子先生が 先日の僕が担当した講義の様子をブログ( https://ndsi.kyo2.jp/d2022-12-18.html )で紹介してくれます。ご参考まで…

では、昨日に続き、対話コマンド t2s の話を続けます。


対話形式で喋ってくれるようにはなったのだが…

昨日の記事 でもチラッと触れましたが、 Model ディレクトリを変更して出てきた女性の声は おそらく「小春音アミ」の声なんですよね。

そもそも SHAREVOXの公式リリースには 次の4キャラクター6声が収録されています。

SHAREVOXとキャラクター

たしか「つきよみちゃん」は speaker_id = 4 のはず…

で、 月曜日の記事 に書いたAPI一覧の中から 「登録キャラクター&ボイスの情報を取得できるAPI」 を探したら metas が見つかりました。 でもこれ、戻り値の文字列は JSON なんですよね…


C言語で JSON を扱うには

普通 JSON と言えば 僕も迷わず JavaScript を使うんですが、 今回はC言語縛りなので…

C/C++ 用の JSON ライブラリは多数出回ってますが、 今回は jansson を使うことにしました。

jansson.readthedocs.io

…というのもマニュアルがしっかりしてそうだったから (英語だけど…)。 で、マニュアルを読み始めたのだけど、 jansson の Github Rep をチラ見したらこんなのが見つかった。

github.com

これは jansson のリリースに付属するサンプルなんですが、 対話形式の汎用 JSON パーサーです。 今回は metas の情報一覧を表示するだけなので 「これは丁度いい」 とばかりに、 コードをガシガシ書き換えて実装したのが print_metas.cpp です(後述します)。


収録されているキャラクターを一覧する実装

例によって修正したソースコードは diff の出力を掲載しています。

CMakeLists.txt
$ diff -u CMakeLists.txt.orig CMakeLists.txt
--- CMakeLists.txt.orig 2022-12-22 18:06:36.000000000 +0900
+++ CMakeLists.txt  2022-12-13 10:23:24.000000000 +0900
@@ -13,6 +13,7 @@
 set(OPENAL_DIR "/usr/local/opt/openal-soft")
 message(STATUS "OPENAL_DIR: ${OPENAL_DIR}")

-link_directories("${OPENAL_DIR}/lib")
-add_executable(t2s t2s.cpp play.cpp)
-target_link_libraries(t2s ${CORE_LIB} readline openal)
+include_directories(/usr/local/include)
+link_directories("${OPENAL_DIR}/lib" /usr/local/lib)
+add_executable(t2s t2s.cpp play.cpp print_metas.cpp)
+target_link_libraries(t2s ${CORE_LIB} readline openal jansson)
$ 
t2s.cpp
$ diff -u t2s.cpp.orig t2s.cpp
--- t2s.cpp.orig    2022-12-22 18:06:56.000000000 +0900
+++ t2s.cpp 2022-12-13 14:19:22.000000000 +0900
@@ -1,9 +1,10 @@
 #include <stdio.h>
+#include <ctype.h>
 #include <string.h>
 #include <stdlib.h>

 extern void play(uint8_t *output, int size);
-
+extern void print_table(const char *text);

 #include "../core/src/core.h"

@@ -16,19 +17,17 @@
   if (!initialize(model, false)) {
     printf("coreの初期化に失敗しました\n");
     exit(1);
-  } else {
-    printf("coreの初期化に成功しました\n");
   }

   //printf("metas: %s\n", metas());
-
+  //print_table(load_json(metas()));
+  //print_table(metas());
+
   printf("dic: %s\n", dic);
   result = sharevox_load_openjtalk_dict(dic);
   if (result != SHAREVOX_RESULT_SUCCEED) {
     printf("\n%s\n", sharevox_error_result_to_message(result));
     exit(1);
-  } else {
-    printf("終了\n");
   }
 }

@@ -38,13 +37,13 @@
 {
   SharevoxResultCode result;

-  printf("音声生成中...");
+  //printf("音声生成中...");
   result = sharevox_tts(text, speaker, output_size, output);
   if (result != SHAREVOX_RESULT_SUCCEED) {
      printf("\n%s\n", sharevox_error_result_to_message(result));
      return(-1);
   }
-   printf("終了: %d bytes\n", *output_size);
+   //printf("終了: %d bytes\n", *output_size);
   return(0);
 }

@@ -115,7 +114,7 @@
   return(-1);
 }

-#define    MODEL       "../../model"
+#define    MODEL       "../model"
 #define    OPENJTALK_DIC   "../open_jtalk_dic_utf_8-1.11"
 #define    OUTPUT_WAV_NAME "audio.wav"

@@ -161,17 +160,25 @@
                    &output_binary_size,
                    &output_wav) < 0) exit(1);
       if (output_wav != NULL) play(output_wav, output_binary_size);
-      //printf("# %d bytes\n", output_binary_size);
     } else if (n == 1) {
-      if (strcmp(line, "save") == 0) {
+      if (isnumber(line[0])) {
+        speaker_id = line[0] - '0';
+        printf("speaker id: %lld\n", speaker_id);
+      } else if (strcmp(line, "i") == 0 ||
+                 strcmp(line, "id") == 0) {
+        printf("speaker id: %lld\n", speaker_id);
+      } else if (strcmp(line, "l") == 0 ||
+                 strcmp(line, "list") == 0) {
+        print_table(metas());
+      } else if (strcmp(line, "save") == 0) {
         if (save(OUTPUT_WAV_NAME, output_wav, output_binary_size) < 0) exit(1);
         sharevox_wav_free(output_wav);
          output_wav = NULL;
       } else if (strcmp(line, "show") == 0) {
         if (output_wav != NULL) show(output_wav, output_binary_size);
       } else if (strcmp(line, "p") == 0 ||
-        strcmp(line, "play") == 0) {
-   if (output_wav != NULL) play(output_wav, output_binary_size);
+                 strcmp(line, "play") == 0) {
+        if (output_wav != NULL) play(output_wav, output_binary_size);
       }
     } else {
       printf("[%d] %s\n", n, line);
$ 
play.cpp
$ diff -u play.cpp.orig play.cpp
--- play.cpp.orig   2022-12-22 18:07:17.000000000 +0900
+++ play.cpp    2022-12-11 23:49:05.000000000 +0900
@@ -62,7 +62,7 @@
 {
   wav_header *hp = (wav_header *) output;

-  printf("### play\n");
+  //printf("### play\n");
   //printf("RIFF: 0x%x\n", hp->RIFF);
    if (hp->RIFF != WAVH_RIFF) {
        printf("NOT match riff\n");
@@ -89,8 +89,8 @@
    short wavchannels   = hp->nChannels;
    // nSamplesPerSec(サンプリング周波数)と
    // nAvgBytesPerSec(1秒あたりのバイト数)の違いを説明
-   printf("hp->nSamplesPerSec: %d \n", hp->nSamplesPerSec);
-   printf("hp->nAvgBytesPerSec: %d \n", hp->nAvgBytesPerSec);
+   //printf("hp->nSamplesPerSec: %d \n", hp->nSamplesPerSec);
+   //printf("hp->nAvgBytesPerSec: %d \n", hp->nAvgBytesPerSec);
    int   samplesPerSec = hp->nSamplesPerSec;
   int   byteParSec    = hp->nAvgBytesPerSec;
    //printf("byteParSec: %d \n", byteParSec);
@@ -99,7 +99,7 @@
   short bitsParSample = hp->wBitsPerSample;
    //printf("bitsParSample: %d \n", bitsParSample);

-  printf("ov_datasize: %d\n", hp->ov_datasize);
+  //printf("ov_datasize: %d\n", hp->ov_datasize);
   //printf("ov_data: 0x%x\n", hp->ov_data);
   if (hp->ov_data != WAVH_OV_DATA) {
        printf("NOT match ov data\n");
@@ -111,14 +111,14 @@
   int wavSize         = hp->ov_datasize;
   int wavSamplingrate = samplesPerSec;

-  printf("wavChannels: %d \n", wavChannels);
-   printf("wavBit: %d \n", wavBit);
-   printf("wavSize: %d \n", wavSize);
-   printf("wavSamplingrate: %d \n", wavSamplingrate);
+  //printf("wavChannels: %d \n", wavChannels);
+   //printf("wavBit: %d \n", wavBit);
+   //printf("wavSize: %d \n", wavSize);
+   //printf("wavSamplingrate: %d \n", wavSamplingrate);

   //int time_playback = (float)wavSize / (float)(4*wavSamplingrate);
    int playback_ms = ((float)wavSize / (float)byteParSec) * 1000.0F;
-   printf("playback_ms: %d msec \n", playback_ms);
+   //printf("playback_ms: %d msec \n", playback_ms);
    unsigned char *data = hp->data;

    ALuint source;
$ 

要はデバッグ用のプリントを止めただけです。 あとはキャラクターの一覧表示と選択のために 内部コマンドを追加しました (内部コマンドについては後述)。


こちらが、本稿のホットポイントの print_metas.cpp です。 既に述べたように jansson のリリースに付属する simple_parse.c を 手直しして作成しました。

/*
 * print_metas
 */

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>

#include <jansson.h>


json_t *load_json(const char *text) {
    json_t *root;
    json_error_t error;

    root = json_loads(text, 0, &error);

    if (root) {
        return root;
    } else {
        fprintf(stderr, "json error on line %d: %s\n", error.line, error.text);
        return (json_t *)0;
    }
}

void
print_style(json_t *style, const char *primary)
{
  size_t size = json_object_size(style);
  const char *key;
  int id = -1;
  json_t *value;
  char secondary[8192];

  json_object_foreach(style, key, value) {
    if (strcmp(key, "name") == 0) {
      strcpy(secondary, json_string_value(value));
    } else if (strcmp(key, "id") == 0) {
      id = json_integer_value(value);
    }
  }
  if (id > -1) printf("%d: %s  %s\n", id, primary, secondary);
}

void
print_styles(json_t *styles, const char *primary)
{
  if (json_typeof(styles) != JSON_ARRAY) return;
  size_t i;
  size_t size = json_array_size(styles);
  for (i = 0; i < size; i++) {
    json_t *child = json_array_get(styles, i);
    print_style(child, primary);
  }
}

void
print_line(json_t *object)
{
  size_t size = json_object_size(object);
  const char *key;
  json_t *value;
  char primary[8192];
  
  json_object_foreach(object, key, value) {
    if (strcmp(key, "name") == 0) {
      strcpy(primary, json_string_value(value));
    } else if (strcmp(key, "styles") == 0) {
      print_styles(value, primary);
    }
  }
}

void
print_table(const char *text)
{
  size_t i, size;
  json_t *root = load_json(text);
  if (root == NULL) return;
  if (json_typeof(root) != JSON_ARRAY) return;
  size = json_array_size(root);
  for (i = 0; i < size; i++) {
    json_t *child = json_array_get(root, i);
    print_line(child);
  }
}

#if 0
#include <fcntl.h>

#define M1     1048576
#define DEF_PATH    "../sharevox_core-0.1.2/t2s/model/official/metas.json"

int
main()
{
  char buff[M1];

  int fd = open(DEF_PATH, O_RDONLY);
  if (fd < 0) exit(1);
  int n = read(fd, buff, M1);
  if (n < 0) exit(1);

  json_t *root = load_json(buff);
  if (root) {
    //print_json(root);
    print_table(root);
    json_decref(root);
  }

  close(fd);
  exit(0);
}
#endif

ちなみに末尾の '#if 0' の中身は print_metas.cpp を単体で動かすときの main ルーチンです。 SHAREVOX の Model ディレクトリにある metas.json (APIの metas を呼び出した時に 返ってくる JSON 文字列はこのファイルの内容では?) の内容を表示してくれます。


ビルド

昨日までと同様に cmake でビルドします。

$ cd sharevox_core-0.1.2/t2s/
$ rm -rf build
$ mkdir build
$ cd build/
$ cmake ..
-- The C compiler identification is AppleClang 13.0.0.13000029
-- The CXX compiler identification is AppleClang 13.0.0.13000029
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/cc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- CMAKE_CXX_FLAGS: -Wno-deprecated-declarations -Wno-writable-strings
-- PARENT: /Users/fujita/xtr/BookBot/BookBot3/06_T2S_JSON
-- TOP_DIR: /Users/fujita/xtr/BookBot/BookBot3/06_T2S_JSON/sharevox_core-0.1.2
-- CORE_LIB: /Users/fujita/xtr/BookBot/BookBot3/06_T2S_JSON/sharevox_core-0.1.2/core/lib/libcore.dylib
-- OPENAL_DIR: /usr/local/opt/openal-soft
-- Configuring done
-- Generating done
-- Build files have been written to: /Users/fujita/xtr/BookBot/BookBot3/06_T2S_JSON/sharevox_core-0.1.2/t2s/build
maverick:build fujita$ cmake --build .
[ 25%] Building CXX object CMakeFiles/t2s.dir/t2s.cpp.o
[ 50%] Building CXX object CMakeFiles/t2s.dir/play.cpp.o
[ 75%] Building CXX object CMakeFiles/t2s.dir/print_metas.cpp.o
[100%] Linking CXX executable t2s
[100%] Built target t2s
$ 


実行

いよいよ完成が近づいたので内部コマンドをちょっと整理しました。

さらに内部コマンドは次のとおりです。

  • 数字1文字:スピーカーIDを設定します。
  • id(i):現在設定されているスピーカーIDを表示します。
  • list(l):現在の model に収録されているキャラクタとスピーカーIDを表示します。
  • save:直前に発話したメッセージを audio.wav に格納します。
  • show:直前に発話したメッセージの WAV ヘッダー情報を表示します。
  • play(p):直前に発話したメッセージを再度発話します。
  • quit、exit(q):t2s を終了します。

まぁ「キャラクタとスピーカーIDの表示」「スピーカーIDの設定」 「発話メッセージ」「終了」ぐらいしか使わないと思いますが…

$ ./t2s
model: ../model
dic: ../open_jtalk_dic_utf_8-1.11
> l
0: 小春音アミ  ノーマル
1: 小春音アミ  喜び
2: 小春音アミ  怒り
3: 小春音アミ  悲しみ
4: つくよみちゃん  ノーマル
5: 白痴ー  ノーマル
6: 開発者  ノーマル
> 4
speaker id: 4
> こんにちは
> つくよみちゃんです
> q
exit
$



ようやく「つくよみちゃん」が喋ってくれました。

#つくよみちゃんを利用してフォロワー増やしたい

でも…

授業ではデモしなかったんです。 いや、できなかったってできなかったって言ったほうが良いかも。

その理由は…明日、説明します。

以上

SHAREVOX core の API を探る(3)OpenAL

Explore the SHAREVOX core API (3) OpenAL


2022/12/21
藤田昭人


昨日の「マクラでモタつくとその日のうちに書き上げるのは難しい」を教訓に…

毎年、僕を講義に呼んでくれる吉田さんが 先日の僕が担当した講義の様子をブログ( https://ndsi.kyo2.jp/d2022-12-18.html )で紹介してくれました。 でも、この「音声チャットボット」について 僕が語り始めるとまたまた収まらなくなるので…

昨日の t2s の続きを進めます。


sharevox_tts が生成するデータ

昨日の記事 でも書きましたが、 せっかく対話インターフェースをつけたのに 読み上げてくれないと興醒めなので、 WAVデータを再生する機能が欲しいところ。

そこで、 まずはWAVデータを生成する sharevox_tts の出力を見てみました。 次のソースコード(抜粋)の箇所です。

  result = sharevox_tts(text.c_str(), speaker_id, &output_binary_size, &output_wav);
  if (result != SHAREVOX_RESULT_SUCCEED) {
    std::cout << sharevox_error_result_to_message(result) << std::endl;
    return 1;
  }

  std::cout << "音声ファイル保存中..." << std::endl;

  std::ofstream wav_file(OUTPUT_WAV_NAME, std::ios::binary);
  wav_file.write(reinterpret_cast<const char*>(output_wav), output_binary_size);
  sharevox_wav_free(output_wav);

コードからもわかるように、 どうやら (ヘッダーを含む) WAVデータが丸々メモリー上に 格納されているようです。 で、音声データを再生する 類似の実装を幾つかチェックしてみたのですが、 データを一旦ファイルに格納し 音声データを再生するコマンド (macOSの場合はafplayとか) を起動する実装が一般的でした。 なんかイマイチなやり方ですねぇ…


WAVファイルの再生方法

そこでオンメモリのWAVデータを (ファイルに格納する事なく) 再生できるオープンソースを 探してみました。 いずれもWAVファイルを再生する ツールだったのですが、 オンメモリ・データの再生に 改造しやすそうな次のサンプルを見つけました。

github.com

このプログラムには元ネタがあるようです*1

WAVファイルのヘッダーの情報などが きっちり定義されていて、 最初の入力ファイルの読み込み部分さえ 外せば上手くいきそう…と思ったのですが、 作業してみたらちょっと上手くないところがありました。 これは後述の「play.cpp」のところで説明します。


OpenAL

上記のサンプルは OpenAL なるライブラリの利用を仮定しています。
例によって、詳細は Wikipedia から…

ja.wikipedia.org

どうやら、ゲームプログラミングでは著名なライブラリのようです。 オリジナルを開発した Loki Software は既に廃業しているようで、今は次の団体が引き継いでるとか…

www.openal.org

でも、オープンソース版は 次のサイトからダウンロードできるようです*2

openal-soft.org

MacBook を使っている僕の場合、 例によってホームブリューで インストールするのが簡単ですね。

formulae.brew.sh


t2s を喋らせる実装

今回は 昨日の記事 に対するアップデートです。

CMakeLists.txt

CMakeLists.txt の修正箇所は次のとおりです。

$ diff -u CMakeLists.txt.orig CMakeLists.txt
--- CMakeLists.txt.orig 2022-12-21 10:42:15.000000000 +0900
+++ CMakeLists.txt  2022-12-10 23:26:14.000000000 +0900
@@ -1,6 +1,8 @@
 cmake_minimum_required(VERSION 3.16)
 project(T2S)
 set(CMAKE_CXX_STANDARD 11)
+set(CMAKE_CXX_FLAGS "-Wno-deprecated-declarations -Wno-writable-strings")
+message(STATUS "CMAKE_CXX_FLAGS: ${CMAKE_CXX_FLAGS}")

 find_path(PARENT NAMES "sharevox_core-0.1.2" PATHS "../..")
 message(STATUS "PARENT: ${PARENT}")
@@ -8,6 +10,9 @@
 message(STATUS "TOP_DIR: ${TOP_DIR}")
 find_library(CORE_LIB NAMES core PATHS "${TOP_DIR}/core" PATH_SUFFIXES lib)
 message(STATUS "CORE_LIB: ${CORE_LIB}")
+set(OPENAL_DIR "/usr/local/opt/openal-soft")
+message(STATUS "OPENAL_DIR: ${OPENAL_DIR}")

-add_executable(t2s t2s.cpp)
-target_link_libraries(t2s ${CORE_LIB} readline)
+link_directories("${OPENAL_DIR}/lib")
+add_executable(t2s t2s.cpp play.cpp)
+target_link_libraries(t2s ${CORE_LIB} readline openal)
$ 

G++ のコンパイル・オプションを追加し、 OpenAL のライブラリもリンク対象に含めました。

t2s.cpp

t2s.cpp は内部コマンドを幾つか追加しました。

$ diff -u t2s.cpp.orig t2s.cpp
--- t2s.cpp.orig    2022-12-21 10:42:34.000000000 +0900
+++ t2s.cpp 2022-12-11 23:36:11.000000000 +0900
@@ -2,26 +2,28 @@
 #include <string.h>
 #include <stdlib.h>

-#include "../core/src/core.h"
+extern void play(uint8_t *output, int size);

-#define    MODEL       "../../model"
-#define    OPENJTALK_DIC   "../open_jtalk_dic_utf_8-1.11"
-#define    OUTPUT_WAV_NAME "audio.wav"
+
+#include "../core/src/core.h"

 void
-init()
+init(const char *model, const char *dic)
 {
   SharevoxResultCode result;

-  if (!initialize(MODEL, false)) {
+  printf("model: %s\n", model);
+  if (!initialize(model, false)) {
     printf("coreの初期化に失敗しました\n");
     exit(1);
   } else {
     printf("coreの初期化に成功しました\n");
   }

-  printf("openjtalk辞書の読み込み中...");
-  result = sharevox_load_openjtalk_dict(OPENJTALK_DIC);
+  //printf("metas: %s\n", metas());
+
+  printf("dic: %s\n", dic);
+  result = sharevox_load_openjtalk_dict(dic);
   if (result != SHAREVOX_RESULT_SUCCEED) {
     printf("\n%s\n", sharevox_error_result_to_message(result));
     exit(1);
@@ -75,6 +77,27 @@
 }


+// wave format
+
+void
+show(uint8_t *wav, int wsiz)
+{
+  printf("%d bytes\n\n", wsiz);
+  printf("RIFF: %c%c%c%c\n", wav[0], wav[1], wav[2], wav[3]);
+  printf("size: %d\n",            *((int32_t *) &wav[4]));
+  printf("WAVE: %c%c%c%c\n", wav[8], wav[9], wav[10], wav[11]);
+  printf(" fmt: %c%c%c%c\n", wav[12], wav[13], wav[14], wav[15]);
+  printf("wFormatLength: %d\n",   *((int32_t *) &wav[16]));
+  printf("wFormatTag: %d\n",      *((int16_t *) &wav[20]));
+  printf("nChannels: %d\n",       *((int16_t *) &wav[22]));
+  printf("nSamplesPerSec: %d\n",  *((int32_t *) &wav[24]));
+  printf("nAvgBytesPerSec: %d\n", *((int32_t *) &wav[28]));
+  printf("nBlockAlign: %d\n",     *((int16_t *) &wav[32]));
+  printf("wBitsPerSample: %d\n",  *((int16_t *) &wav[34]));
+  printf("ov_data: %c%c%c%c\n", wav[36], wav[37], wav[38], wav[39]);
+  printf("ov_datasize: %d\n",     *((int32_t *) &wav[40]));
+}
+
 // readline

 #include <readline/readline.h>
@@ -92,8 +115,12 @@
   return(-1);
 }

+#define    MODEL       "../../model"
+#define    OPENJTALK_DIC   "../open_jtalk_dic_utf_8-1.11"
+#define    OUTPUT_WAV_NAME "audio.wav"
+
 int
-main()
+main(int argc, char *argv[])
 {
   char *line = NULL;
   int n;
@@ -101,9 +128,16 @@
   SharevoxResultCode result;
   int64_t speaker_id = 0;
   int output_binary_size = 0;
-  uint8_t *output_wav = nullptr;
+  uint8_t *output_wav = NULL;

-  init();
+  char *model = MODEL;
+  char *dic = OPENJTALK_DIC;
+
+  if (argc > 1) {
+    model = argv[1];
+  }
+
+  init(model, dic);

   while (1) {
     line = readline("> ");
@@ -120,17 +154,29 @@
             break;
       }
     }
+
     if (n == 3) {
-      printf("[ja] %s\n", line);
       if (generate(line,
                    speaker_id,
                    &output_binary_size,
                    &output_wav) < 0) exit(1);
-      if (save(OUTPUT_WAV_NAME, output_wav, output_binary_size) < 0) exit(1);
-      sharevox_wav_free(output_wav);
+      if (output_wav != NULL) play(output_wav, output_binary_size);
+      //printf("# %d bytes\n", output_binary_size);
+    } else if (n == 1) {
+      if (strcmp(line, "save") == 0) {
+        if (save(OUTPUT_WAV_NAME, output_wav, output_binary_size) < 0) exit(1);
+        sharevox_wav_free(output_wav);
+         output_wav = NULL;
+      } else if (strcmp(line, "show") == 0) {
+        if (output_wav != NULL) show(output_wav, output_binary_size);
+      } else if (strcmp(line, "p") == 0 ||
+        strcmp(line, "play") == 0) {
+   if (output_wav != NULL) play(output_wav, output_binary_size);
+      }
     } else {
       printf("[%d] %s\n", n, line);
     }
+
     add_history(line);
     free(line);
   }
$ 

追加した内部コマンドについては後述します。

play.cpp

さて、本稿のホットポイントの play.cpp です。 関数 play はOpenAL を使って WAVデータを再生する関数です。 関数の前半では WAVヘッダーから情報を取り出し、 後半では OpenAL で再生しています。

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>

#include <OpenAL/al.h>
#include <OpenAL/alc.h>

#define WAVH_HEADER_SIZE 44
#define WAVH_CHANNELS_MONO 1
#define WAVH_CHANNELS_STEREO 2
#define WAVH_SAMPLINGRATE_CD 44100
#define  WAVH_BITSPERSAMPLE8 8
#define  WAVH_BITSPERSAMPLE16 16

ALenum getFormat(int wavChannels,  int wavBit)
{
    ALenum format;
    if(wavChannels == WAVH_CHANNELS_MONO){
        if(wavBit == 8) {
            format = AL_FORMAT_MONO8;
        }else if(wavBit == 16) {
            format = AL_FORMAT_MONO16;
        }
    }else if(wavChannels == WAVH_CHANNELS_STEREO){
        if(wavBit== 8){
            format = AL_FORMAT_STEREO8;
        }else if(wavBit == 16) {
            format = AL_FORMAT_STEREO16;
        }
    }
    return format;
}

typedef struct tag_wav_header {   
    int RIFF;                // 'R','I','F','F'                
    int size;                   // size of wave file from here on
    int WAVE;             // 'W','A','V','E'
    int fmt;            //'f','m','t',' '
    int   wFormatLength;          // The length of the TAG format    
    short    wFormatTag;             // should be 1 for PCM type ov_data  
    short    nChannels;              // should be 1 for MONO type ov_data
    int   nSamplesPerSec;         // should be 11025, 22050, 44100  
    int   nAvgBytesPerSec;        // Average Data Rate              
    short    nBlockAlign;            // 1 for 8 bit ov_data, 2 for 16 bit
    short    wBitsPerSample;         // 8 for 8 bit ov_data, 16 for 16 bit
    int ov_data;                // 'd','a','t','a'                        
    int   ov_datasize;               // size of ov_data from here on              

  unsigned char data[0];
} wav_header ;

const int   WAVH_RIFF = 0x46464952; // "RIFF"
const int   WAVH_WAVE =  0x45564157; // "WAVE"
const int   WAVH_FMT = 0x20746D66; // "'fmt"
const int   WAVH_OV_DATA = 0x61746164;    // "ov_data"
const int   WAVH_WFORMATLENGTH = 16;
const short WAVH_WFORMATTAG_PCM = 1;

void
play(uint8_t *output, int size)
{
  wav_header *hp = (wav_header *) output;

  printf("### play\n");
  //printf("RIFF: 0x%x\n", hp->RIFF);
    if (hp->RIFF != WAVH_RIFF) {
        printf("NOT match riff\n");
    return;
  }
    //printf("size: %d\n", hp->size);
  //printf("WAVE: 0x%x\n", hp->WAVE);
    if (hp->WAVE != WAVH_WAVE) {
        printf("NOT match wave\n");
        return;
    }
  //printf(" fmt: 0x%x\n", hp->fmt);
    if (hp->fmt != WAVH_FMT) {
        printf("NOT match fmt\n");
        return;
    }
    //printf("wFormatLength: %d\n", hp->wFormatLength);
    //printf("wFormatTag: %d\n", hp->wFormatTag);
  if (hp->wFormatTag != WAVH_WFORMATTAG_PCM) {
    printf("wFormatTag should be 1\n");
    return;
  }  

    short wavchannels   = hp->nChannels;
    // nSamplesPerSec(サンプリング周波数)と
    // nAvgBytesPerSec(1秒あたりのバイト数)の違いを説明
    printf("hp->nSamplesPerSec: %d \n", hp->nSamplesPerSec);
    printf("hp->nAvgBytesPerSec: %d \n", hp->nAvgBytesPerSec);
    int   samplesPerSec = hp->nSamplesPerSec;
  int   byteParSec    = hp->nAvgBytesPerSec;
    //printf("byteParSec: %d \n", byteParSec);
  short blockAlign    = hp->nBlockAlign;
    //printf("blockAlign: %d \n", blockAlign);
  short bitsParSample = hp->wBitsPerSample;
    //printf("bitsParSample: %d \n", bitsParSample);
    
  printf("ov_datasize: %d\n", hp->ov_datasize);
  //printf("ov_data: 0x%x\n", hp->ov_data);
  if (hp->ov_data != WAVH_OV_DATA) {
        printf("NOT match ov data\n");
      return;
  }

  int wavChannels     = wavchannels;
  int wavBit          = bitsParSample;
  int wavSize         = hp->ov_datasize;
  int wavSamplingrate = samplesPerSec;

  printf("wavChannels: %d \n", wavChannels);
    printf("wavBit: %d \n", wavBit);
    printf("wavSize: %d \n", wavSize);
    printf("wavSamplingrate: %d \n", wavSamplingrate);

  //int time_playback = (float)wavSize / (float)(4*wavSamplingrate);
    int playback_ms = ((float)wavSize / (float)byteParSec) * 1000.0F;
    printf("playback_ms: %d msec \n", playback_ms);
    unsigned char *data = hp->data;

    ALuint source;
    ALuint buffer;
  ALCdevice *device = alcOpenDevice(NULL);
    if (!device) { printf("alcOpenDevice Faild\n"); return; }
    ALCcontext *context = alcCreateContext(device, NULL);
    if (!context) { printf("alcCreateContext Faild\n"); return; }

    alcMakeContextCurrent(context);
    alGenSources (1, &source);
    alGenBuffers(1, &buffer);

    ALenum format = getFormat(wavChannels,  wavBit);
    alBufferData(buffer, format, data, wavSize, wavSamplingrate);
    alSourcei(source, AL_BUFFER, buffer);
    alSourcePlay(source);

    //printf("alSourcePlay \n");
    int time_count; 
    for (time_count = playback_ms; time_count > 0; time_count--) {
        usleep(1000);
    }

    alDeleteBuffers(1, &buffer);
    alDeleteSources(1, &source);
}

OpenALOpenGL と よく似たインターフェースなんだそうですが、 要は関数 alSourcei でPCMデータ(WAVデータ)を ハードウェアにセットし、 関数 alSourcePlay で音声再生をキックする… デバイスドライバーみたいなコードを書くようです。 音声再生自体はハードウェアで実行されるのですが、 ソフトウェアから見れば バックグラウンドで 実行されているように見えます。 この時、ソフトウェアは データの再生時間を計算して ウェイトループに入ります。 再生時間が過ぎると 関数 alDeleteBuffers をコールして ハードウェアの音声再生を停止します。

で、前述のオリジナル・サンプルの 問題点ですが、ひとつは nSamplesPerSec(サンプリング周波数)とnAvgBytesPerSec(1秒あたりのバイト数) の意味を取り違えてるように思える事、 もうひとつは再生時間の単位を1秒にしている事です。 そのためオリジナル・サンプルのコードのままだと WAVデータを最後まで再生せずに 処理を終了してしまいます。 (つまり尻切れトンボ状態になります) そこで再生待ちのループをミリ秒単位に変更しました。 音声の場合、ミリ秒ぐらいまで 人間は聞き分けてしまいますからね。


ビルドと実行

ビルドの手順は 昨日の記事 と変わりません。

$ cd sharevox_core-0.1.2/t2s/
$ ls
CMakeLists.txt          play.cpp
model               t2s.cpp
open_jtalk_dic_utf_8-1.11
$ rm -rf build
$ mkdir build
$ cd build
$ cmake ..
-- The C compiler identification is AppleClang 13.0.0.13000029
-- The CXX compiler identification is AppleClang 13.0.0.13000029
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/cc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- CMAKE_CXX_FLAGS: -Wno-deprecated-declarations -Wno-writable-strings
-- PARENT: /Users/fujita/xtr/BookBot/BookBot3/05_T2S_OpenAL
-- TOP_DIR: /Users/fujita/xtr/BookBot/BookBot3/05_T2S_OpenAL/sharevox_core-0.1.2
-- CORE_LIB: /Users/fujita/xtr/BookBot/BookBot3/05_T2S_OpenAL/sharevox_core-0.1.2/core/lib/libcore.dylib
-- OPENAL_DIR: /usr/local/opt/openal-soft
-- Configuring done
-- Generating done
-- Build files have been written to: /Users/fujita/xtr/BookBot/BookBot3/05_T2S_OpenAL/sharevox_core-0.1.2/t2s/build
maverick:build fujita$ cmake --build .
[ 33%] Building CXX object CMakeFiles/t2s.dir/t2s.cpp.o
[ 66%] Building CXX object CMakeFiles/t2s.dir/play.cpp.o
[100%] Linking CXX executable t2s
[100%] Built target t2s
$ 

但し、t2s ディレクトリには SHAREVOX の公式リリースに 添付されている model ディレクトリも配置しました*3。この公式リリースの model ディレクトリには 4キャラクターの声が格納されています。

次に実行方法なんですが…

昨日の記事 でも説明したように、 起動後は初期化処理のため20秒弱待たされます。 また内部コマンドは1バイト文字(=英字)、 3バイト文字(≒漢字)は発話テキストと 認識されることは変わりません。

さらにこのバージョンでは次の model を選択できるようにしました。 コマンド起動時に第1引数として model ディレクトリのパスを受け付けます。 デフォルト(引数なし)では "../../model" が指定されます。 上記の build ディレクトリからだと "sharevox_core-0.1.2/model" が指定され SHAREVOX core に付属する 男性の声のみが収録されています。 引数に "../model" を指定すると "sharevox_core-0.1.2/t2s/model" 前述のSHAREVOX公式リリースの 4声が発声できるようになります。

音が聞こえないので あまり意味はないですが コマンドのログは以下のとおりです。

$ ./t2s
model: ../../model
coreの初期化に成功しました
dic: ../open_jtalk_dic_utf_8-1.11
終了
> こんにちは
音声生成中...終了: 69504 bytes
### play
hp->nSamplesPerSec: 48000
hp->nAvgBytesPerSec: 96000
ov_datasize: 69460
wavChannels: 1
wavBit: 16
wavSize: 69460
wavSamplingrate: 48000
playback_ms: 723 msec
> q
exit
$ ./t2s ../model
model: ../model
coreの初期化に成功しました
dic: ../open_jtalk_dic_utf_8-1.11
終了
> こんにちは
音声生成中...終了: 69848 bytes
### play
hp->nSamplesPerSec: 48000
hp->nAvgBytesPerSec: 96000
ov_datasize: 69804
wavChannels: 1
wavBit: 16
wavSize: 69804
wavSamplingrate: 48000
playback_ms: 727 msec
> q
exit
$ 



…ということで

ようやく t2s を喋らせることができました。 明日は t2s の最後のお色直しとして、 model ディレクトリに収録されている キャラクターの一覧を表示する機能などを 追加します。 あと、t2s が動いている動画も 貼り付けるよう頑張ってみるつもりです。

それではお約束の…

#つくよみちゃんを利用してフォロワー増やしたい

以上

*1:付属のメモランダムから拾い出した URL は次のページでした。

tips.hecomi.com

こちらはオリジナルの OpenJTalk+HTS を対象にしているようです。

*2:どういう事情かよく知りません。

*3:以前、紹介したと思いますが、 次の Github の SHAREVOX 0.1.0 のリポジトリ

github.com

…に格納されている sharevox_model-0.1.0.zip というZIPファイルです。

このファイルを解凍すると model ディレクトリが生成されますので sharevox_core-0.1.2/t2s/ に格納してください。

SHAREVOX core の API を探る(2)ReadLine

Explore the SHAREVOX core API (2) ReadLine


2022/12/20
藤田昭人


5日間連続投稿の2回目、 今回はテクニカル(?)なトピックから始めます。

実はこの春先まで僕は、 音声合成を含む機械学習アルゴリズムの実行には GPU を使った実行環境が必須だと思い込んでいました。 なので昨年、シンデレラの童話読み聞かせを作ったときも Chrome で標準的に使える Web Speech API を迷わず使っていました。 噂によればこの API は 裏で Google音声認識音声合成が 動いているのだとか… 「まぁ、そう言うもんなんだろう」 という認識だったのです。 っていうかぁ… 無料の割には音声認識の精度が高かったので 結構満足していた訳で、 難点はブラウザ内蔵のAPIの面倒くささ*1と、 あと iPhone では音声認識が動かない*2ぐらいかな? と思っていたのです。

ところが、 夏の椎間板ヘルニアで 寝たきりになっている時期、 本当にやれることが無くて 暇つぶしに スマホ相手にググりまくっている時期に たまたま見つけたのが VOICEVOX だった訳です*3

…って語ってると 明日のマクラがなくなるので、 本日はこの辺で止めて 本題に入ります。


【注】昨日の diff に誤りがありました

昨日の記事 の simple_tts.cpp の修正箇所に漏れがありました。 正しくはこちら。

$ diff -u ../example/cpp/unix/simple_tts.cpp simple_tts.cpp
--- ../example/cpp/unix/simple_tts.cpp  2022-10-28 17:21:34.000000000 +0900
+++ simple_tts.cpp  2022-12-20 13:35:13.000000000 +0900
@@ -2,7 +2,7 @@
 #include <iostream>
 #include <string>

-#include "../../../core/src/core.h"
+#include "../core/src/core.h"

 #define OUTPUT_WAV_NAME "audio.wav"

@@ -17,7 +17,7 @@

   std::cout << "coreの初期化中..." << std::endl;

-  if (!initialize("../../../model", false)) {
+  if (!initialize("../model", false)) {
     std::cout << "coreの初期化に失敗しました" << std::endl;
     return 1;
   }
# 

修正前のコードだと model ディレクトリのパスが違うので初期化でコケます。
それから OpenJTalk の辞書も simple_tts ディレクトリに コピーしておく必要があります。

関連ファイルが正しく配置できると…

$ ls
CMakeLists.txt          open_jtalk_dic_utf_8-1.11
build               simple_tts.cpp
maverick:simple_tts fujita$ ./build/simple_tts 'こんにちは'
coreの初期化中...
openjtalk辞書の読み込み中...
音声生成中...
音声ファイル保存中...
音声ファイル保存完了 (audio.wav)
maverick:simple_tts fujita$ ls
CMakeLists.txt          open_jtalk_dic_utf_8-1.11
audio.wav           simple_tts.cpp
build
$ 

見てのとおり audio.wav が生成されます。


Simple_TTS は遅い

さて、 この simple_tts は WAV ファイルを生成するだけ (再生はしてくれない) のも困りものですが、 とにかく実行が遅い。

実行時間を測ってみると…

$ time -p ./build/simple_tts 'こんにちは'
coreの初期化中...
openjtalk辞書の読み込み中...
音声生成中...
音声ファイル保存中...
音声ファイル保存完了 (audio.wav)
real 18.64
user 18.95
sys 0.40
$ 

なんと 18 秒もかかっています。 「ひょっとして初期化がノロいのでは?」 と考えたので次のようなプログラムを 書いてみました。

$ cat c.cpp
#include "../core/src/core.h"

int
main()
{
  initialize("../model", false);
  finalize();
}
$ g++ -o ccc c.cpp -Wl,-rpath,../core/lib ../core/lib/libcore.dylib
ld: warning: dylib (../core/lib/libcore.dylib) was built for newer macOS version (11.7) than being linked (11.0)
$ time -p ./ccc
real 18.88
user 17.97
sys 0.38
$ 

このプログラムは初期化処理(initialize)と 終了処理(finalize)だけを実行する2行のプログラムです。 ld コマンドが出力する waring は忘れてもらうとして…

どうやら simple_tts がノロいのは model ファイルを取り込む 初期化処理に非常に時間がかかっているようです。 言い換えれば WAVファイルを生成する sharevox_tts の実行はそれほど時間を要しない。

であれば…

起動時に初期化処理を行い、 その後は対話形式で text-to-speach を実行する 対話型のプログラムを作れば良いことになります。


コマンド t2s

…という事で、 simple_tts にちょこっとコードを追加して 対話型のコマンドにすることにしました。 名付けて t2s 。もちろん爺さん趣味です😁

GNU ReadLine

対話型インターフェースといえば GNU ReadLine です。詳細は Wikipedia を見てもらうとして…

ja.wikipedia.org

要はシェルのライン・エディティング機能を 提供してくれるライブラリーでして、 今回のように UTF-8 のコード入力が必須の場合 これをかました方が安全なんです。 ヒストリー機能も使えるしね。

Mac の場合、 インストールはホームブリューを使うのが 一番お手軽です。次のページに簡単な解説が 書いてあるので見てもうとして…

qiita.com

このページに掲載されている サンプルコードと simple_tts.cpp を ドッキングすることにしました。 (詳細は後述の「ソースコード」の項を参照)


CMakeLists.txt

コマンド t2s のディレクトリは 昨日の記事 の simple_tts ディレクトリの隣におくことにしました。 なので CMakeLists.txt は昨日作成したものをちょいと修正するだけです。

$ diff -u CMakeLists.txt.orig CMakeLists.txt
--- CMakeLists.txt.orig 2022-12-19 10:33:15.000000000 +0900
+++ CMakeLists.txt  2022-12-20 15:01:02.000000000 +0900
@@ -1,5 +1,5 @@
 cmake_minimum_required(VERSION 3.16)
-project(SimpleTTS)
+project(T2S)
 set(CMAKE_CXX_STANDARD 11)

 find_path(PARENT NAMES "sharevox_core-0.1.2" PATHS "../..")
@@ -9,5 +9,5 @@
 find_library(CORE_LIB NAMES core PATHS "${TOP_DIR}/core" PATH_SUFFIXES lib)
 message(STATUS "CORE_LIB: ${CORE_LIB}")

-add_executable(simple_tts simple_tts.cpp)
-target_link_libraries(simple_tts ${CORE_LIB})
+add_executable(t2s t2s.cpp)
+target_link_libraries(t2s ${CORE_LIB} readline)
$ 

名前を simple_tts から t2s に変更し、 target_link_libraries に readline を追加しました。


ソースコード

ソースコード t2s.cpp はこちら。

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#include "../core/src/core.h"

#define MODEL       "../../model"
#define OPENJTALK_DIC   "../open_jtalk_dic_utf_8-1.11"
#define OUTPUT_WAV_NAME "audio.wav"

void
init()
{
  SharevoxResultCode result;

  if (!initialize(MODEL, false)) {
    printf("coreの初期化に失敗しました\n");
    exit(1);
  } else {
    printf("coreの初期化に成功しました\n");
  }

  printf("openjtalk辞書の読み込み中...");
  result = sharevox_load_openjtalk_dict(OPENJTALK_DIC);
  if (result != SHAREVOX_RESULT_SUCCEED) {
    printf("\n%s\n", sharevox_error_result_to_message(result));
    exit(1);
  } else {
    printf("終了\n");
  }
}

int
generate(const char *text, int64_t speaker, 
         int *output_size, uint8_t **output)
{
  SharevoxResultCode result;

  printf("音声生成中...");
  result = sharevox_tts(text, speaker, output_size, output);
  if (result != SHAREVOX_RESULT_SUCCEED) {
      printf("\n%s\n", sharevox_error_result_to_message(result));
      return(-1);
  }
    printf("終了: %d bytes\n", *output_size);
  return(0);
}

int
save(const char *name, uint8_t *output, int size)
{
  FILE* fp;

  printf("音声ファイル保存中...");
  fp = fopen(name, "wb");
  if (fp == NULL){
      printf("失敗\n");
      return(-1);
  }

  if (fwrite(output, size, 1, fp) < 1 ){
      printf("ファイルへの書き込みに失敗しました。\n");
      return(-1);
  }

  fclose(fp);
  printf("音声ファイル保存完了 (%s)\n", name);
  return(0);
}

void
term()
{
  finalize();
}


// readline

#include <readline/readline.h>
#include <readline/history.h>

int
numMB(char *str)
{
  if ((*str & 0x80) == 0x00) return(1);
  if ((*str & 0xE0) == 0xC0) return(2);
  if ((*str & 0xF0) == 0xE0) return(3);
  if ((*str & 0xF8) == 0xF0) return(4);
  if ((*str & 0xFC) == 0xF8) return(5);
  if ((*str & 0xFE) == 0xFC) return(6);
  return(-1);
}

int 
main()
{
  char *line = NULL;
  int n;

  SharevoxResultCode result;
  int64_t speaker_id = 0;
  int output_binary_size = 0;
  uint8_t *output_wav = nullptr;
  
  init();

  while (1) {
    line = readline("> ");
    if (line == NULL || strlen(line) == 0) {
      free(line);
      continue;
    }
    n = numMB(line);
    if (n == 1) {
      if (strcmp(line, "q") == 0 ||
          strcmp(line, "quit") == 0 ||
          strcmp(line, "exit") == 0) {
            free(line);
            break;
      }
    }
    if (n == 3) {
      printf("[ja] %s\n", line);
      if (generate(line, 
                   speaker_id,
                   &output_binary_size,
                   &output_wav) < 0) exit(1);
      if (save(OUTPUT_WAV_NAME, output_wav, output_binary_size) < 0) exit(1);
      sharevox_wav_free(output_wav);
    } else {
      printf("[%d] %s\n", n, line);
    }
    add_history(line);
    free(line);
  }

  printf("exit\n");
  term();
  exit(0);
}

前述のとおり、 サンプルコードと simple_tts.cpp を シンプルにドッキングした内容です。 ミソは int numMB(char *str) なる関数。 この関数はマルチバイト文字のバイト数を判定します。 返り値が1なら1バイト文字(=英字)、 返り値が3なら3バイト文字(≒漢字)で、 t2s の対話インターフェースでは 3バイト文字から始まる文字列はWAVファイルを生成するテキスト、 1バイト文字から始まる文字列は t2s の内部コマンドと判定しています。 もっとも、このコードでは内部コマンドは exit/quit/q の1つだけです。


ビルドと実行

トップディレクトリの直下に t2s ディレクトリを作成し、 上記の open_jtalk_dic_utf_8-1.11 と CMakeLists.txt と t2s.cpp を作成したら、 昨日の記事 とほぼ同様の手順でビルドができます。

$ rm -r build
$ mkdir build
$ cd build/
$ cmake ..
-- The C compiler identification is AppleClang 13.0.0.13000029
-- The CXX compiler identification is AppleClang 13.0.0.13000029
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/cc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- PARENT: /Users/fujita/xtr/BookBot/BookBot3/04_T2S_ReadLine
-- TOP_DIR: /Users/fujita/xtr/BookBot/BookBot3/04_T2S_ReadLine/sharevox_core-0.1.2
-- CORE_LIB: /Users/fujita/xtr/BookBot/BookBot3/04_T2S_ReadLine/sharevox_core-0.1.2/core/lib/libcore.dylib
-- Configuring done
-- Generating done
-- Build files have been written to: /Users/fujita/xtr/BookBot/BookBot3/04_T2S_ReadLine/sharevox_core-0.1.2/t2s/build
$ cmake --build .
[ 50%] Building CXX object CMakeFiles/t2s.dir/t2s.cpp.o
[100%] Linking CXX executable t2s
[100%] Built target t2s
$ 
$ ./t2s
coreの初期化に成功しました
openjtalk辞書の読み込み中...終了
> こんにちは
[ja] こんにちは
音声生成中...終了: 69504 bytes
音声ファイル保存中...音声ファイル保存完了 (audio.wav)
> q
exit
$ 



予想したとおり、一度 initialize を実行して model をロードすると、 sharevox_tts の実行は短時間です。 が、やはり WAV ファイルの再生機能がないと t2s コマンドは役に立ちそうにありません。

明日は WAV ファイルの再生機能を紹介します。

それではお約束の…

#つくよみちゃんを利用してフォロワー増やしたい

以上

*1:クラッキング防止のためなんだそうですが…

*2:Apple vs Google の構図なんでしょうかね?

*3:正直に答えましょう。 僕が見たのはこちら…

www.youtube.com

そもそもニコ動に縁のなかった僕には この手の動画は当然ノーマークで… だって、こんなところから テクニカルな情報を拾えるなんて思ってなかったもん。

でも…

結局これが 「唯一ヘルニアになって良かったこと」 になりました😀

SHAREVOX core の API を探る(1)CMake

Explore the SHAREVOX core API (1) CMake


2022/12/19
藤田昭人


ここ数年は、 この時期になると ゲスト・スピーカーの準備に 追われるのが常で、 特に去年は例の音声チャットボットの プロトタイプ作成に追い立てられる 日々を過ごしてました。 今年は夏の3ヶ月完全休業を 取り返していたこともあって 「去年と同じメニューで授業」 と秋口あたりから相談してました。なので 「今年は平穏な年末を過ごせそうだ」 と鷹を括っていたのですが…

やはり今年も悪夢からは逃れられなかった😀

実は、昨年の実習教材が突如動かなくなったのでした*1。 その対応に追われて授業準備の予定は総崩れ。 おかげでブログの続きを公開するのが だいぶん遅れてしまいました。

さて…

今回から数回、 SHAREVOX core を使ったプログラミングを紹介していきます。 実は授業で 「ちょっとしたデモぐらい見せたい」 とコードと ブログの下書きを 書き溜めていたのです。 が、結構な分量になってしまったので トピックごと小出しして行くことにしました。


SHAREVOX core の API

まずはSHAREVOX core の API をざっくり概観していきます。 SHAREVOX core のREADME.mdによれば、 「APIは core/src/core.h を見ろ」と書いてますが、中身は doxygenフォーマットの インターフェース情報でした。 関数名だけ抜き書きすると…

関数名 説明
1 initialize 初期化する
2 load_model モデルをロードする
3 is_model_loaded モデルがロード済みかどうか
4 finalize 終了処理を行う
5 metas メタ情報を取得する
6 supported_devices 対応デバイス情報を取得する
7 variance_forward 音素ごとの音高と長さを求める
8 decode_forward 波形を求める
9 last_error_message 最後に発生したエラーのメッセージを取得する
10 sharevox_load_openjtalk_dict open jtalkの辞書を読み込む
11 sharevox_tts text to spearchを実行する
12 sharevox_tts_from_kana text to spearchをAquesTalkライクな記法で実行する
13 sharevox_wav_free voicevox_ttsで生成した音声データを開放する
14 sharevox_error_result_to_message エラーで返ってきた結果コードをメッセージに変換する

…と、音声合成に関わりがありそうな 14エントリーが定義されていました。

付属するサンプルソースexample/cpp/unix/simple_tts.cpp を眺めると、このうち initialize, sharevox_load_openjtalk_dict, sharevox_tts, sharevox_wav_free, finalize を呼び出しているようです。 このプログラムは比較的フラットで SHAREVOX core の API を探るための もっとも単純なサンプルとして 活用できそうです。


SimpleTTS のディレクトリを作り直す

早速 simple_tts.cpp を手直しして ライブラリーの挙動を確認することにしたのですが…

このプログラムは見ての通り ディレクトリ・ネストの深いところにあり( 前回 の手順を実行した方はご存知のとおり) ビルドするためには関連ライブラリーを ローカルディレクトリに コピーしてくる必要があります。


CMakeLists.txt を書いてみる

そこでトップディレクトリ直下に 新たなディレクトリを作り、 関連ライブラリーを移動することなく ビルドできるように CMakeLists.txt を作成します。

初心者向けのサンプルを探して いろいろチェックしてみたのですが、 Wikipedia 英語版のCMakeのページにある Example に掲載されている次のコードが 一番シンプルでわかりやすい例のようです。

// hello.cpp
#include <iostream>

int main()
{
    std::cout << "Hello, world!\n";
}

という C++ のコードをビルドするための CMakeLists.txt は次のようになります。

cmake_minimum_required(VERSION 3.5)
project(HelloWorld CXX)
add_executable(hello hello.cpp)

この記法、年寄りの僕には かつて書いていた BSD UnixMakefile を彷彿させます。 等価な Makefile を書くと、 確かこんな感じ…

PROG= hello
SRCS=  hello.c

ということで CMake によるビルド手順の記述方法を 感覚的に理解したところで simple_tts.cpp のために僕が用意したのは以下の CMakeLists.txt です。

cmake_minimum_required(VERSION 3.16)
project(SimpleTTS)
set(CMAKE_CXX_STANDARD 11)

find_path(PARENT NAMES "sharevox_core-0.1.2" PATHS "../..")
message(STATUS "PARENT: ${PARENT}")
set(TOP_DIR "${PARENT}/sharevox_core-0.1.2")
message(STATUS "TOP_DIR: ${TOP_DIR}")
find_library(CORE_LIB NAMES core PATHS "${TOP_DIR}/core" PATH_SUFFIXES lib)
message(STATUS "CORE_LIB: ${CORE_LIB}")

add_executable(simple_tts simple_tts.cpp)
target_link_libraries(simple_tts ${CORE_LIB})

要は SHAREVOX core のライブラリの格納位置は トップディレクトリから相対パスが決まっているとの 仮定の元に find_path なるコマンドで トップディレクトリを探しに行っています。あと message(STATUS, "XXX") なる記法は、かつての echo "XXX" と等価と考えれば良いみたい。


新しいディレクトリで simple_tts をビルドしてみる

CMakeLists.txt が用意できたら simple_tts をビルドしてみましょう。

【注】ターミナルソフトで UNIX コマンドを使ってください。

  • まずは 前回 紹介した手順で SHAREVOX core のビルドツリーを用意し、 ビルドしてください。
$ ls sharevox_core-0.1.2/core/lib
core.h              libonnxruntime.dylib
libcore.dylib           libopenjtalk.a
libonnxruntime.1.10.0.dylib
$ 

上記のように core/lib にライブラリのバイナリが確認できればOKです。

$ cd sharevox_core-0.1.2
$ mkdir simple_tts
$ cd simple_tts
$ vi CMakeLists.txt
<中略>
$ cp ../example/cpp/unix/simple_tts.cpp .
$ vi simple_tts.cpp
<中略>
$ diff -u ../example/cpp/unix/simple_tts.cpp simple_tts.cpp
--- ../example/cpp/unix/simple_tts.cpp  2022-10-28 17:21:34.000000000 +0900
+++ simple_tts.cpp  2022-12-20 13:35:13.000000000 +0900
@@ -2,7 +2,7 @@
 #include <iostream>
 #include <string>

-#include "../../../core/src/core.h"
+#include "../core/src/core.h"

 #define OUTPUT_WAV_NAME "audio.wav"

@@ -17,7 +17,7 @@

   std::cout << "coreの初期化中..." << std::endl;

-  if (!initialize("../../../model", false)) {
+  if (!initialize("../model", false)) {
     std::cout << "coreの初期化に失敗しました" << std::endl;
     return 1;
   }
$ 

トップディレクトリで simple_tts を作成し、 前述の CMakeLists.txt と simple_tts.cpp を置きます。 simple_tts.cpp はオリジナルをコピーしてきますが、 上記の通り、インクルードするヘッダーファイルのパスを変更してください。

$ mkdir build
$ cd build
$ cmake ..
-- The C compiler identification is AppleClang 13.0.0.13000029
-- The CXX compiler identification is AppleClang 13.0.0.13000029
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/cc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- PARENT: /Users/fujita/xtr/BookBot/BookBot3/03_T2S_CMake
-- TOP_DIR: /Users/fujita/xtr/BookBot/BookBot3/03_T2S_CMake/sharevox_core-0.1.2
-- CORE_LIB: /Users/fujita/xtr/BookBot/BookBot3/03_T2S_CMake/sharevox_core-0.1.2/core/lib/libcore.dylib
-- Configuring done
-- Generating done
-- Build files have been written to: /Users/fujita/xtr/BookBot/BookBot3/03_T2S_CMake/sharevox_core-0.1.2/simple_tts/build
$ cmake --build .
[ 50%] Building CXX object CMakeFiles/simple_tts.dir/simple_tts.cpp.o
[100%] Linking CXX executable simple_tts
[100%] Built target simple_tts
$ ls
CMakeCache.txt      Makefile        simple_tts
CMakeFiles      cmake_install.cmake
$ 

これも 前回 と同じ手順になります。


ずいぶん丁寧な CMakeLists.txt の解説になっちゃいましたが…

元々 cmake コマンドは X Window の imake や GNU の autoconf など UNIX の make コマンドの Makefile を自動生成するコマンドだったはずで、 今では大幅に機能強化されていて「覚えるのが大変」なコマンドですが、 オリジナルの記法に立ち戻れば案外スラッと書けるなぁ…感慨しているところです😀

ちなみに、件の授業は 12/15 に終わってまして、 相方の吉田さんに「今回のブログは連続5日間連続投稿」と 啖呵を切ってしまったので、一発目はこのへんで…
明日もがんばります。

それではお約束の…

#つくよみちゃんを利用してフォロワー増やしたい

以上

*1:蓋を開ければ大したことではなかったのですが、 やはり最初にみつけた時は衝撃的だったのは確か😀

SHAREVOX core をビルドしてみる

Try to build SHAREVOX core


2022/10/30
藤田昭人


前回YouTube の話から始めましたが…

今回はテレビの話。 完全に昭和世代の僕は 今もってテレビ依存の生活をしています。 最近、朝のニュースを付けてると 「1ドル=150円」だとか、 「マクドナルドが立て続けに値上げした」 とか、果ては 「ロシアに爆撃されたウクライナの街角」 の映像まで連日見せられています。 ティーンエイジャーの諸君にも 「円安」や「戦争」の話はピンと来なくても 「マクドナルドの値上げ」は あまり経験のない 実感的な話ではないでしょうか?

確かに、僕のような老人にも、 昨年と一昨年のパンデミックは未知の体験で、 正直かなりビビっていたのですが…

3年目のコロナ禍にもどこか慣れてしまった 今年の印象はだいぶん違ったものになりました。 例えば「円安」。 僕らは子供の頃 「1ドル=360円」 をリアルタイムで経験しているので、 若い方々ほどは衝撃的には受け止めません*1。 このところ毎日ウクライナから届く 「爆撃された街角の映像」 にも既視感があります*2。 こういった子供の頃の生活に根ざした体験は いつまでも記憶に残るものなんですね。

今年は…

なにやら50年前に舞い戻ったような 不安げな社会の空気を感じ取りつつ…
このブログでは子供の頃感じた 楽しげな感覚に突き動かされている気が しています😁


またまたファイルをダウンロードする。

さて…

本稿でのお題は SHAREVOX core を ソースコードからビルドします。 今回も僕が MacBook Pro (Retina, 15-inch, Mid 2014)/macOS Big Sur verion 11.7 で実行した手順を紹介します。 ちなみにビルドには cmake 3.20.2 を使いました*3

まずはライブラリのソースとバイナリのファイルをダウンロードするところから。 任意の作業ディレクトリに移動してください。

今回も SHAREVOX Core 0.1.2 を使用しました。次のファイルをダウンロードします。

今回はビルドするので、 ダウンロードするのはソースだけ。

次に ONNX Runtime v1.10.0 のライブラリをダウンロードします。

これも前回とおなじ。

一方、Open JTalk のソースコードは VOICEVOX のリポジトリからダウンロードします*4

このリポジトリはリリース・タグが設定されていないので、 上記の URL から「Code」「 ZIP ファイルをダウンロード」を指定してください。

さらに Open JTalk の辞書セット(UTF-8)をダウンロードします。

これは simple_tts を動かす際に必要になります。

いずれも前回同様、 作業ディレクトリの下の archive ディレクトリにダウンロードして、 その場でアーカイブファイルを 展開することにします。


ビルドのためのソースツリーを構築する

README.md の コアライブラリのビルド で紹介されている手順通りに エラー無しでビルドするには、 ソースツリーの構築に注意する必要があります。

【注】ターミナルソフトで UNIX コマンドを使ってください。

  • SHAREVOX Core 0.1.2のソースツリーを archive ディレクトリの隣に作る
$ cd <作業ディレクトリ>
$ cp -r archive/sharevox_core-0.1.2 .
$ 
  • sharevox_core-0.1.2/onnxruntimeをコピー
$ cp -r archive/onnxruntime-osx-universal2-1.10.0 sharevox_core-0.1.2/onnxruntime
$ 
  • open_jtalkを移動
$ cp -r archive/open_jtalk-1.11 sharevox_core-0.1.2/open_jtalk
$ 
  • core/src/engine/openjtalk.cpp を次のように修正
    text2mecabの引数に "8192" を追加します。
$ cd sharevox_core-0.1.2/core/src/engine/
$ cp openjtalk.cpp openjtalk.cpp.orig
$ diff -u openjtalk.cpp.orig openjtalk.cpp
--- openjtalk.cpp.orig  2022-10-28 17:26:57.000000000 +0900
+++ openjtalk.cpp       2022-10-28 17:27:13.000000000 +0900
@@ -15,7 +15,7 @@
 namespace voicevox::core::engine {
 std::vector<std::string> OpenJTalk::extract_fullcontext(std::string text) {
   char buff[8192];
-  text2mecab(buff, text.c_str());
+  text2mecab(buff, 8192, text.c_str());
   Mecab_analysis(&mecab, buff);
   mecab2njd(&njd, Mecab_get_feature(&mecab), Mecab_get_size(&mecab));
   njd_set_pronunciation(&njd);
$ 

これで、README.md の説明どおりに ビルドできます。


SHAREVOX core をビルドする

作業ディレクトリに戻って…

SHAREVOX core の README.md の コアライブラリのビルド の項目の手順にしたがってビルドします。

$ cd sharevox_core-0.1.2
$ mkdir build
$ cd build
$ cmake ..

<中略>

Required HTSEngine not found
-- Configuring done
-- Generating done

$ cmake --build . --config Release
[  2%] Building C object open_jtalk/src/CMakeFiles/openjtalk.dir/jpcommon/jpcommon.c.o

<中略>

[ 97%] Building CXX object core/CMakeFiles/core.dir/src/engine/synthesis_engine.cpp.o
[100%] Linking CXX shared library libcore.dylib
[100%] Built target core
$ 

ご存知のように、 この "cmake .." がビルドするための Makefile を用意するプロセスで、 続く "cmake --build . --config Release" が (実際にソースをコンパイルする) ビルドのプロセスです。

最後に "cmake --install ." で 所定のディレクトリにインストールすると…

$ cmake --install .

<中略>

$ cd .. 
$ ls sharevox_core-0.1.2/core/lib/
core.h              libonnxruntime.dylib
libcore.dylib           libopenjtalk.a
libonnxruntime.1.10.0.dylib
$

それまで存在しなかった lib ディレクトリが作成されて、 ビルドされたバイナリ・ライブラリがコピーされます。


SHAREVOX core をテストしてみる

さらに README.md の コアライブラリのビルド の「C++のテスト実行」の手順を実行してみましょう。

$ cmake -S . -B test_build -DBUILD_TEST=YES

<中略>

[100%] Linking CXX executable unit_test
dyld: Library not loaded: @rpath/libonnxruntime.1.10.0.dylib
  Referenced from: 02_sharevox_core/sharevox_core-0.1.2/test_build/tests/unit_test
  Reason: no suitable image found.  Did find:
    02_sharevox_core/sharevox_core-0.1.2/onnxruntime/lib/libonnxruntime.1.10.0.dylib: code signature in (02_sharevox_core/sharevox_core-0.1.2/onnxruntime/lib/libonnxruntime.1.10.0.dylib) not valid for use in process using Library Validation: library load disallowed by system policy
CMake Error at 02_sharevox_core/sharevox_core-0.1.2/test_build/_deps/catch2-src/extras/CatchAddTests.cmake:45 (message):
  Error running test executable
  '02_sharevox_core/sharevox_core-0.1.2/test_build/tests/unit_test':


    Result: Subprocess aborted
    Output: 



make[2]: *** [tests/unit_test] Error 1
make[2]: *** Deleting file `tests/unit_test'
make[1]: *** [tests/CMakeFiles/unit_test.dir/all] Error 2
make: *** [all] Error 2
$ 

テスト用のプログラムをリンクするところでコケます。 これは 前回 の脚注で説明したライブラリの証明書の問題です。 前回同様、 アップルメニューから「システム環境設定」を選択して 「セキュリティとプライバシー」 をクリックし、 ダイアログの「一般」パネルで「このまま許可」選んでください。 次のとおり cmake --build test_build が再実行できます。

$ cmake --build test_build
Consolidate compiler generated dependencies of target openjtalk
[ 31%] Built target openjtalk
Consolidate compiler generated dependencies of target core
[ 38%] Built target core
Consolidate compiler generated dependencies of target Catch2
[ 96%] Built target Catch2
Consolidate compiler generated dependencies of target Catch2WithMain
[ 98%] Built target Catch2WithMain
Consolidate compiler generated dependencies of target unit_test
[ 99%] Linking CXX executable unit_test
[100%] Built target unit_test
$ ctest --test-dir test_build --verbose
Internal ctest changing into directory: 02_sharevox_core/sharevox_core-0.1.2/test_build
UpdateCTestConfiguration  from :02_sharevox_core/sharevox_core-0.1.2/test_build/DartConfiguration.tcl
UpdateCTestConfiguration  from :02_sharevox_core/sharevox_core-0.1.2/test_build/DartConfiguration.tcl
Test project 02_sharevox_core/sharevox_core-0.1.2/test_build
Constructing a list of tests
Done constructing a list of tests
Updating test list for fixtures
Added 0 tests to meet fixture requirements
Checking test dependency graph...
Checking test dependency graph end
test 1
    Start 1: extract_one_character

1: Test command: 02_sharevox_core/sharevox_core-0.1.2/test_build/tests/unit_test "extract_one_character"
1: Test timeout computed to be: 10000000
1: Filters: extract_one_character
1: Randomness seeded to: 3858558281
1: ===============================================================================
1: All tests passed (6 assertions in 1 test case)
1: 
1/1 Test #1: extract_one_character ............   Passed    0.01 sec

100% tests passed, 0 tests failed out of 1

Total Test time (real) =   0.02 sec
$ 

以上で「C++のテスト実行」は終了です。

この状態で 前回 の「SHAREVOX core のサンプルプログラムをビルドする」の手順を 実行すれば、再び男性の声を聞くことができます。


今回は SHAREVOX core のビルドの手順を紹介しました。 「前回、バイナリライブラリで動かしたのに、何で?」 と思ってらっしゃる方も多いかと…

そもそも僕は そういう無駄な努力が 大好きなヤツですが…*5 *6

ソースからのビルドは コンパイル対象となるソースコードを スキャンしていくプロセスですので、 注意深く観察していれば 対象となるソフトウェアについて より多くの情報が引き出せます。

例えば、今回の SHAREVOX core の場合、 前回 紹介した simple_tts をビルドするためには onnxruntime のライブラリと open_jtalk の辞書ファイルが必要だったので この二つが関連することは分かっていたのですが、 ソースのビルド・プロセスから さらに次のことがわかります。

  • SHAREVOX core は比較的小さなライブラリである
  • 下位で open_jtalk と onnxruntime を呼び出している?
  • 各々はトップディレクトリ以下の次のサブディレクトリに格納されている
  • onnxruntime は公開されているコードを無修正で利用している

…とまぁ、 現時点で分かっているのはこれくらい。

とはいえ、simple_tts でちゃんと WAV ファイルが生成できることが確認できたので、 次は model ディレクトリを差し替えると つくよみちゃんの声に切り替わる理由を 調べてみたいと思っています。

それではお約束の…

#つくよみちゃんを利用してフォロワー増やしたい

以上

*1:「なんで、子供に為替がわかるんだ!!」 とのツッコミがありそうですが…
僕が子供の頃は アメリカやヨーロッパから輸入されるお菓子は とんでもない高級品で、 危篤な人がその年のお歳暮に送ってくれないと、 僕たちの口には入らない代物だったのです😀

*2:僕らはベトナム戦争をリアルタイムで体験した世代、 つまり日常的な報道番組で 戦場の様子を(テレビ越しに) 目撃した世代ですから。

*3:README.md によれば CMake 3.16以上とのことです。

*4:どうやら VOICEVOX/SHAREVOX で使用する Open JTalk のソースコードは 独自に修正されているようです。

*5:実際、中学生の僕といえば…

ゴミ捨て場で拾った 壊れたトランジスタラジオを 再び鳴らそうとハンダゴテを振り回したり…

組立説明書が無くなったプラモデルを引き取って 箱に印刷されている完成写真と睨めっこして、 ちゃんと完成すべく、 ああでもない、 こうでもないと…

無駄なことに労力を注ぎ込む パッとしない変わった趣味の男子でした😁

*6:これが、先日の 「今、16歳の自分に出会ったら、 今の自分は何を語りかけるのか?」 というお題に僕は引っかかり続けている理由でもあります😀

前々回 にもチラッと触れたように、今の僕は 「これから就職を控えた学生諸君に何かをいう」 立場でもあるのですが、 自分の経験から言えることは 「16歳で 自分の人生の目標が定まっている人なんて ほとんどいない」 ということだけなんです。

実際「16歳の僕」がやっていた 周りの人たちには全く理解されない 「無駄な努力」と「自己満足」が (というか「それができる能力」が) その後の僕の人生を切り開いたことは 否定しようがない事実。 だって、僕が16歳の頃は プログラマなんて職業はみんな 知らなかったからね (もちろん親だって)。 プログラマが社会的に広く認知される 職業になったのはそこから10年あまり 経過した後でした。

それに、 当時は周囲から「無駄な努力」と 言われ続けていたことを 「16歳の僕」は何か後ろめたく 感じていたのも本当の話。 なので、今の僕は…

「迷うな。ブレるな。 自分を信じろ。やがて、その力が お前の人生を切り開く時が必ずやってくる!!」

…と彼に語ってやりたいと 思うのが正直なところです。

が、それは単に 僕が運が良かっただけ かもしれない。 これが今の僕の 酷く悩ましい問題 でもあります。