音声を口パクアニメーションに変換して音 + 口パクアニメをリアルタイム再生するプログラムを、C#/WPF/AquesTalk + 「ゆっくり」 の組み合わせで作った例を紹介します。
2015/5/14追記: 世間的にはこういうのを「リップシンク(Lip Synch)」って言うようなのでタグ追加しました。
前回の記事では「"AquesTalk.dll"で声を出させる」という(凡庸な)ネタを紹介しましたが、そちらと同様、今回の記事は「ゆっくりWindowWalker(YWW)」の情報公開の一部です。
今回は音声を出すだけでなく、音声を画像上の動きに関連づけてみようと思います。こういうアプリケーションはYWWやYWWの源流的存在であるキャラ素材スクリプト以外にCrazyTalkとかが存在しますが、今回はそういう外部依存のライブラリに頼らない方法をやってみます。
1. やり方の指針
まず基礎的なこととして、標準的に使われるwavファイルデータではファイル内に音声波形がほぼそのまま収納されています。最初の数十バイトにあるヘッダ情報を除くと、wavファイルの中身は実質「500, 0, -1000,...」というような整数値がギッチリ詰めて書かれてるだけのファイルです。そこで、それらの波形値(今回ならshortつまり16bit整数)の絶対値などを取って、波形の大きさをもとにキャラの口の開き方を決めれば良さそうであることが分かります。C#のLINQ風に擬似コードを書くならこんな感じでやりましょうと。
public static int[] GetKuchiPakus(short[] volume) { return volume.Select(v => Math.Abs(v)) .Select(v => v < 5000 ? 0 : v < 10000 ? 1 : v < 15000 ? 2 : v < 20000 ? 3 : v < 25000 ? 4 : 5 ).ToArray(); }
コレで音量が"0"から"5"の整数に変換されるので、値に応じて画像を差し替えれば口パクになります。実際には音の聞こえ方と映像の対応を見て、口パクのしきい値(上の5000とか10000とか)を調整しながら自然な演出を目指すことになります。
ただ、実際には音声データを一つ一つ見ていくのではなく、一定の時間区間(例 :0.02秒分など)の波形値をかき集めて使います(例: サンプリングレート44.1kHzで0.02秒分のデータを使うなら882個の整数を用いることになる)。これは音のサンプリングレートをアニメーションのフレームレート(60fpsとか)へ落としこむために必須の処置です。また絶対値関数を使うより単に2乗を計算する方が計算が軽いので、実際のコードでは
- wavの波形データを時間ごとに区切る(0.00秒から0.02秒の波形値、0.02秒から0.04秒の波形値、…)。それぞれの塊はshort[]になる。
- それぞれの塊について、波形値を2乗した値の平均を求める
- 求めた値をしきい値で適当に区切って、画像の番号に対応する整数値(今回は"0,1,2,3,4,5"のどれか)に変換する
こういう手順で進めます。もちろん実装法の一例に過ぎませんが。
2. 実装
今回は60fpsの口パクに必要な最低限の実装を作った…んですが、画像表示やデータ取得コードといった本質的でない部分のコードが意外と長くなってしまったので、作ったプロジェクトを丸ごと公開します。こちら(Google Drive)からzipをDLできます(※第三者の製作物が含まれています。"readme.txt"を読んでから使って下さい)。動かした絵はこんな感じ。
…静止画ですがきちんと口パクしてるんですよ?
コードの構成は以下のようになっています。
- MainWindow.xaml : キャラ表示用の場所を用意
- MainWindow.xaml.cs : 喋り + 口パクの最終的な結合
- AquesTalk : "AquesTalk.dll"で喋らせるための最低限のコード(前回の記事参照)
- WaveInfo : 今回のプログラムの心臓部。wavファイルから口パク用の値を求める
特にC#コードについて、関数呼び出し形式のLinqを使ってる部分は読みづらいかもしれませんが、頑張って解読してください…。
コードに関して注意を二つだけ。第一に、AquesTalkから出てくるwavのファイルヘッダの仕様は完全に下調べしてからコーディングしています。ヘッダの具体的な内容は本記事の下に補足で書いてあるので参考にどうぞ。
第二に、細かい注意ですがソフトの汎用性を高めるにはCPUのエンディアンに注意しましょう。実際にやるのは「BitConverter.IsLittleEndianプロパティを見て必要そうならバイト配列を反転させる」ことだけですが、間違うと数値の読みが狂う可能性があります。
/// <summary> byte列データをshortに変換する。CPUのエンディアンが考慮される </summary> private static short GetShortFrom(byte b1, byte b2) { var bytes = new byte[] { b1, b2 }; if (!BitConverter.IsLittleEndian) { Array.Reverse(bytes); } return BitConverter.ToInt16(bytes, 0); }
3. まとめ
結局のところドカッと完成品のzip置いてドヤァしただけの記事になっちゃいましたが、口パクを作るための理屈は意外と単純です。zipの中を見てもらえれば分かりますが、実装も(扱うwavが限定的なら)そこまで複雑じゃなく、本プログラムの為に手打ちしたコードは200行前後です。
もちろん今回やったアプローチはC#以外でも作れますから、たとえばIronPythonで実装すればYWWより前から有名なデスクトップマスコットである「アプリコタン」のプラグインにして、アプリコタンのキャラを口パクさせることも可能だと思います。もしかしたら既に誰かが作ってるかもしれませんが。
…ていうか自分で作ろうかな。
補足. AquesTalkのファイルヘッダ構成
一般的なwavファイルヘッダの構成についてはこちらのサイトが大変参考になります。特にAquesTalkが出力するwavデータの場合、起点を第0バイトと数えて
0,1,2,3 : 'RIFF'という固定文字列 4,5,6,7 : ここから先のファイル内バイト数( = ファイルサイズ(byte) - 8) 8,9,10,11 : 'WAVE'という固定文字列 12,13,14,15 : 'fmt 'という固定文字列
16,17,18,19 : 'fmt 'チャンク、つまりヘッダーらしいヘッダーの記述バイト数 -> AquesTalkの場合16で固定
20,21 : フォーマットID(short) -> AqueTalkの場合1(リニアPCM)で固定
22,23 : チャネル数(short) -> AquesTalkの場合1で固定、つまりモノラル音
24,25,26,27 : サンプリングレートHz -> AquesTalkの場合8000で固定っぽい
28,29,30,31 : 1秒当たり読み込みバイト数(int) -> AquesTalkの場合16000で固定っぽい
32,33 : ある時刻での波形を表すために用いるバイト数(short) -> AquesTalkの場合2で固定
34,35 : ひとつの波形値を表すために用いるビット数(short) -> AquesTalkの場合16( = 16bit = 2byte)
36,37,38,39 : 'data'という決まった文字列
40,41,42,43 : データサイズ(int) -> AquesTalkの場合(ファイルサイズ(byte) - 44)
ここまでがヘッダで、第44バイト以降はファイル末尾まですべてshort整数で記述された波形値となります。