VoiceChat3D開発記です. C# + Managed DirectXにて開発を行っています. 三次元音響を採用することで VR空間における没入感を高めることを目的としています. 01.構想,UDP送信 02.UDP受信 03.受信再生,三次元音響 04.ノイズ抑制(未遂) 05.ノイズ抑制(挫折) 06.VR空間への組込試験,コード整理,多人数化 07.エフェクタによる音圧向上 08.ADPCMによる音声圧縮 09.シーケンスIDと再生位置制御 10.再生位置最適化
大学の研究で開発しているアプリケーションに VoiceChat機能をつけることになりました.言語はC#です. 気軽に難しい事を言われた気もしますが, 生命は自分が生まれてきた責任を果たさなければならないのでやるっきゃ騎士です. 構想としては,単純に 送信側: 音声をマイクからキャプチャ ( DirectSoundを使用 ) キャプチャ中,バッファデータに送信者IDとシーケンスIDを付加 UDPで相手に送信 受信側: UDPでバラバラのデータを受信 送信者IDとシーケンスIDで分類してバッファを再構成 再生 ( DirectSoundを使用 ) という風に考えています. LAN内で適用する予定なので音声圧縮は必要ない・・・と思います. (音声の圧縮を考慮すると手間が格段に増えるので それを避けたいというのが本音ですが) 念のため,キャプチャの音質は最低設定( 8000Hz, 8-bit Mono )にしています. とりあえずDirectX SDKのSoundCaptureサンプルを改造して キャプチャ中のデータをUDPで指定のクライアントに飛ばすところまでは達成しました. BinaryWriterがWAVEデータにバッファ内容を書き込みに行くタイミングで送信しています. 次は受信側でバッファを再構成する部分を作ろうと思います. 飛んできたデータを同じくBinaryWriterで書き込んでいけばうまくいくはずです. これが完成すると,送信側が喋った内容が 受信側でwavフォーマットで保存されることになります. 本当はwavフォーマットに直す必要は無く, 音声データはバッファからそのまま再生できるはずです. バッファからのストリーミング再生も勉強しなければなりません.
前回: キャプチャ中のバッファをUDPで指定のクライアントに飛ばした 今回: 飛んできたバッファをバイナリに書き込んでwavデータを再現した 内容: 送信と逆の操作をやればよいので,特に苦労することもなく サンプルからのコピペを繰り返すことでプログラムを書くことができました. 自分で書いたのはスレッドの操作ぐらいでしょうか. wavデータに関する仕様をよく理解せずにコピペしていたので 受信側のwavデータ構築で少し躓いてしまいました. なぜ再生できないのだろうと思って 送信側と受信側のwavデータをバイナリエディタで開いてみると ファイルヘッダから既に違っていましたフハハ!Execute me. バッファを圧縮しなくても再生できたので, どの程度まで音質を上げてもいいのかしら?という欲が出てきました. 教官殿に相談してみると,LANカードのスループットの1%程度を参考にせよ!との事です. 研究室内ではほとんどの端末が100MbpsのLANカードですので, それの1%で1Mbpsまでオッケーということになります. 現在は8800Hz 8bit monoで録音していますので,70400bpsで送受信を行うことになります.余裕のヨッちゃんイカですね. 現状の仕様では利用者数に比例して送受信するデータ量が倍増するので ここで調子コイてサンプリング周波数や解像度を大幅に増やしてしまうと LANカードが爆発してPCが勢いよく前方に射出される恐れがあります. 利用者数に応じて音質を変化させるような仕様にすればいいかもしれませんね. 次回は受信バッファのリアルタイム再生をやってみようと思います. 未知長のデータを受信するので,リングバッファっぽい処理が必要になりそうですね. 再生タイミングはどうするのかなぁ・・・ 追記---- データの流量Qは毎秒のデータ量をD,利用者をn人とした時Q = D * nで求まります. LANが全二重モードなら入出力の乗算を考慮する必要はありません. Q<10^3なので,D=70400の時,n<14.20となり 最低音質ならば14人使用できる計算になります. 音質を最高の48000Hz 16bit monoにした場合はD=768000となり, その時はn<1.3でジグザグザグジグ一人きりですね. 解像度を8bitに落とせばギリギリ二人です. 高品質な音声でツーショットを演出! 音声圧縮を導入すればもっと利用可能者数が増えるかしら・・・
前回: 飛んできたバッファをバイナリに書き込んでwavデータを再現した 今回: 飛んできたバッファをリアルタイム再生した 3Dバッファによって仮想的に音源の位置を操作した 内容: 受信した音声のリアルタイム再生を達成したので VoiceChatの基本的な部分が完成しました. イェーッ イェーッ(民家に飛び掛りながら) 作成開始から一週間程度でここに至る事ができたのは C#の使いやすさや.NETクラスライブラリ及びDirectX APIの多様さ, そして偉大なる教官様のお陰です. ホントは家のPCが壊れて暇だったのが一番大きいんだけどね! バッファの受信はリングバッファっぽい処理を行いました. この場合バッファ長が重要になるので 送信側のWaveFormatを考慮した再生バッファを作成する必要があります. DirectSoundの仕様を知るために MSDNを舐めるように・・・そう,恥ずかしいほどペロペロしながら見ていると 再生バッファ(SecondaryBuffer型)の書き込みメソッドに byte型の配列を引数とするものがあったので コイツを用いて受信&書き込み&受信バイト長からのリングバッファ制御を行いました. 再生タイミングをどうするか悩みましたが, 受信中に適当に再生してみたらそれっぽかったのでこれでいいや! とか書くと教官が見えない鎖で僕の首を狩るので次回以降になんとかします. 現状では遅延が0.5秒ほど発生しているのですが, 再生タイミングの改善でもう少し短くなるような気がします. 設定課題: 適切な再生設定 異なる環境での実験 使用できる音質設定の検証 エフェクトの追加(ノイズ除去,音量調整)
前回: VoiceChatの基礎的な部分が完成した 今回: ノイズ抑制を試みたが未遂 内容: モゲーラ!!(発狂) ノイズ抑制を行うための手続きがとっても面倒なんですよ奥さん!(興奮)(人妻) 今までは VoiceCaptor(キャプチャと送信) と VoicePlayer(受信と再生) という別々のクラスを作って動かしていました. しかし聴感エコー制御を行うためにはその時の入力も知っていないといけないわけですから これらのクラスを統合しなきゃいけないっぽいんですよ奥さん!(米屋)(興奮) DirectSoundのFullDuplexクラスが,コンストラクタの引数に CaptureBufferDescription キャプチャバッファの詳細を記述するクラス BufferDescription たぶんセカンダリバッファの詳細 Control 協調するコントロール CooperativeLevel 協調レベル ref CaptureBuffer キャプチャバッファの参照 ref SecondaryBuffer セカンダリバッファの参照 を取っているので,コイツラをキチッと設定してやらなければならないようです. (他にもオーバーロードされたコンストラクタがありますが,おそらく使用するのはコヤツです) しかしこれ,CaptureBufferをコンストラクタから作成するに当たって, プロパティであるCaptureBufferDescriptionのCaptureEffectDiscriptionを設定していると REGDB_E_CLASSNOTREGというエラーが帰ってきてしまいます. 「レジストリに登録されてないヨ」とか,そんな感じの意味らしいですが・・・? 直接設定せずにFullDuplexで関連付けろってことなのかな. MSDNにも「FullDuplexと関連付けないとエコー制御とノイズ抑制はできないよ」と書いてありました. というわけで, 録音と再生で分けていたクラスを統合する必要がありそうです. ・・・それらのクラスを制御するクラス内でFullDuplexを定義すりゃいいのかな? 今日はもう疲れ果てたので,明日以降やってみます. っていうかサンプルが(ドキュメントも)少ないんだよぉManaged DirectXはよぉ! 全言語でgoogle検索してもあんまり引っかからないんだよぉ!! 案外,CodeProjectとかに完璧なキャプチャとか掲載されちゃったりなんかしちゃったりして(早口) ---- 再生位置設定は 受信したバッファを書き込んだ直後に, 書き込み開始点を再生地点とする事で遅延が解消されましたので クリア!クリア!(フッハハー!) こんな場当たり的解決法をやっているときっと後で泣きます. 「きっと」っていうか,バッファが順番に届かなかったとき多分バグります. ---- 音質の設定ですが, サンプリング周波数を上げるとノイズもビッグになりますので 先にノイズ抑制を実装したいと考えています. 解像度は・・・二倍にしてもあまり変化がわかりません. 16bitの方が音がイイような気もするのですが・・・ きっと,スパシーボ効果というヤツです.(ありがとう!) ---- 多人数でVoiceChatを利用する時のことですが 一旦サーバーに音声を送信し,そこから分配するようにすれば マルチキャスト風味となってLANの出力側スループットが下がるような気がします. でも遅延が出るかもしれないなぁ. ドウシ (両手を胸の前で交差させる) ヨッカ (交差を解いて肘を引き,手は肩の高さに) ナー (両手の手のひらを前に突き出して気孔波) 設定課題: エフェクトの追加(ノイズ除去,音量調整) 送信者ID,シーケンスIDの追加 多人数同時通信
っつーかFullDuplexのコードがありましたよ! CodeProjectさまさまだ! そしてこの記事のAuthorたるIanier Munozさんは DirectShow.Netの記事も書いていらっしゃるではないですか! こちらも重宝しております. サンプルをちょっと試してみた感じ,確かにノイズが乗っていません.すげぇすげぇ! 早速このサンプルを解体してみたいと思います. これ2003年度版かぁ.私って遅れてるゥ. ----- ダメだこりゃ!(10分) 私の研究に対する姿勢は 「面倒なところはDirectXに丸投げ!」という感じなのですが 上記のサンプルは何もかもセルフで実装している感じで コードを解読するだけでひとつ上の男になれそうです. というか,C#なのにバリバリポインタ使ってます.(しかもunsafeではない) 神すぎるぜェェ! このコードをいじって使えるようにすることもできそうですが・・・ うーん・・・ とりあえずDirectXでキャプチャする方針で行こうと思います(MS信者). だって,せっかく作ったクラスとか勿体無いからね!(貧乏根性) じゃあノイズとエコーはどうするか・・・ ノイズ対策 → 受信側でエフェクトかけてごまかす エコー対策 → おまえらヘッドフォンかぶれ! カンペキダ!(ジーコ) 頑張ります. 設定課題: エフェクトの追加(ノイズ除去,音量調整) 送信者ID,シーケンスIDの追加 多人数同時通信
前回: 楽にノイズ除去したいなぁ,車輪を再発明したくないなぁと思って web上をアッパー気味にウロつき,諦めた 今回: 三次元仮想空間にVoiceChat機能を組み込んだ 内容: DATTE 報告会が近いJAN!(ファイター < 乙女チック) Get you 修羅場モードJAN!(ストレス < ロマンス) ということで,以前に作っていた三次元仮想空間に 出来上がったボイスチャットを組み込んでみました. 俗な言い方をすれば 超しょっぱいネットゲーにボイチャ機能つけたお!(^ ^ )/ 各ユーザのIPや位置座標などの情報は TCP通信でサーバを経由して他のユーザに送っています. そこで取得したIP情報をUDP送信に用いています. ボイスデータはサーバを経由していないということですね. また,多人数化のためにVoicePlayerクラスに大きく手を加えました. 参加しているユーザ分の SecondaryBuffer, Buffer3D, Buffer3DSettings を それぞれ用意して,個別に設定を行ないます. そしてUDPで飛んできたバッファの先頭に付いているユーザIDを用いて 対応するSecondaryBufferに書き込みを行ないます. モチロンTCPで送信されてきたユーザの位置情報を Buffer3DSettingsのPositionプロパティに反映させることも忘れないさ! そうそう, このPosition設定の話なんですが, Listener3DSettingsのPositionに自分の位置を設定し, そしてBuffer3DSettingsのPositionに音源の位置を設定すると その位置にあわせて3Dサウンドがミキシングされるはずなんですが 何故か自分の位置が常に原点(0,0,0)にあるようにミキシングされます. デバッグでパラメータを見ると値の代入はできているのですが・・・? 仕方が無いので,音源位置設定の際は 自己位置との相対を設定するようにしています. まぁ内部でも同じようなことをやっているはずさ! 今後は・・・なにしろ音量が小さいのをどうにかしたいと考えています. イコライザをいじってなんとかしてみよう・・・ エフェクタを色々試していると,声が甲高くなったりして面白いので こういう機能も無駄に搭載していきたいですネ. (そして報告会で「それ意味なくね?」とか教授に言われたいネ) 設定課題: 音量調整 エフェクトの追加 シーケンスIDの追加(今のところ無くても動作していますが)
前回: 三次元仮想空間にVoiceChat機能を組み込んだ 今回: DirectSoundのエフェクトを用いて音量を調整した 内容: 「音が小さい」という大きな欠点を克服したいと思い 音量を上げる方法について探りました. ・OSの音量出力を上げる ・スピーカの出力を上げる ・入力側のマイク感度を上げる ・音声再生時にデータを加工する 上三つを全部やってもまだ音が小さいので 最後の項目を何とかして何とかします.(曖昧) DirectSoundには 音声にエコーやコーラスなどのエフェクトをかける機能があります. それらをうまく用いて,その・・・アレする.(口下手) 使用したのはパラメトリックイコライザの機能です. これで全帯域の出力を増幅してみると, 使用前よりも音が大きくなりました. より大きくするには,自分でバッファをいじるしかないと思うのですが, 単純に振幅を増幅するだけではダメっぽいので(音が割れました) 音圧を上げる行為にはいろいろな工夫が必要なようです. ・・・めんどいので,終了! いずれまた会うこともあるだろう・・・(ライバル風に) 今後の課題: 再生地点の調整 (バッファ受信時に一つ前のバッファから再生して音の途切れを防止する) 圧縮の導入(ADPCMまたはDPCM) シーケンスIDの追加 今回,wave(というよりはriff)ファイルに関することを少し勉強したので, 圧縮については以前よりも見通しが立つようになりました. 年内には無理ですが,今年度中には導入していきたいと思います.
前回: DirectSoundのエフェクトを用いて音量を調整した 今回: ADPCM変換を導入して音質を向上させた 内容: 色々と調べた結果,なんとか音声圧縮導入に至りました.以下に経緯を示します. SecondaryBufferのコンストラクタで wavファイルのパスを指定するオーバーロードを使うと BufferDescriptionの記述を省略してバッファを構築できます. そこでADPCM音源サウンドを読み込むと, プロパティのwaveformatTagの値は"Pcm"になります. 読み込み時に既に変換を行っているのでしょうか? また,キャプチャバッファのBufferDescriptionを設定する際, riffで既定のADPCMフォーマットIDをwaveformatTagに代入しても 録音はリニアPCMで行われる模様です. なんだよそれ!MSDNでは「圧縮キャプチャをサポート」とか書いてるくせに! 私の使い方が悪いのでしょうか. 仕方が無いので,自力でADPCM変換処理を実装することにしました. 面倒そうだなぁ,と思っていたのですが 一日勉強すればサインカーブの符号化と復号化のサンプルを作れました. 実装は思ったより簡単です.理論を考えた人は偉大だなぁ・・・ あとはバイトオーダに気をつけながらVoiceChatに組み込んで実装完了です. 今回実装した処理で16bitの音声データが4bitに圧縮できました. 現在までは8800Hz 16bitで録音していましたが, これによって44000Hz 16bitで録音しても問題ないでしょう. 現在は1/8秒毎にデータをUDP送信しているので, 通信量を比較すると以下のような感じになるのでしょうか. 8800Hz 16bit (PCM) : (1100*16 + 32)*8 = 141056 bps 44000Hz 16bit (ADPCM) : (5500* 4 + 32)*8 = 176256 bps 通信量を1.25倍にするだけで音質は5倍良くなるということですね. もちろん,等価交換で送受信端末での計算量が増えています. 計算機性能のおかげで特に問題はありませんが, 巨大な変換テーブルを用意することで計算量を減らすこともできるようです. ・・・まぁ,問題になってきたら導入すっか!(先送りキング) 44000Hzで録音すると,音声はかなり明瞭に聞こえます. 導入してよかった. 今後の課題: 再生位置の補正 シーケンスIDの追加 この2つは時間さえかければなんとかなりそうです. もう少しで一区切りだ!がんばるぞ!
前回: ADPCM変換を導入して音質を向上させた 今回: シーケンスIDの追加と再生位置補正を行った 内容: ということで,残りの仕事をやっつけました. シーケンスIDとは,受信側でUDPデータ到着の順序が 適切でなかった場合に,それを判定するものです. 今回実装したのは, 順序が適切でないと判断した場合に 受信データを廃棄するという単純な作りです. 受信データの数は無限なので どこまでもIDの値を増加させていくわけには行きません. そこで最大値を設けて,IDの値が一巡する場合の処理を加えています. 現在はLAN内でのみ実験を行っているので, この処理がどのような効果をもたらすのか確認できないのが 少し気になります. 再生位置補正については, データを受信した時,音声バッファの再生位置を 「一つ前の受信データの書込み開始位置」に設定するようにしました. ----- 新たな再生位置 │ 受信データ ↓ ↓↓↓↓↓ □□□□□■■■■■□□□□□ ----- これによって,データ到着に遅延が出た場合において 音声の途切れによる不自然さを緩和できる様です. しかしこれは・・・ 以下の図をご覧ください. 横長くて申し訳ありません.並んでいる図について, ▲が再生地点,□がバッファを表しています. □について,色がついたものにはデータが格納されているとお考えください. 一つの図に二本のバッファが並んでいます. それぞれ, 左側が「一つ前の受信データの書込み開始位置」 右側が「最新の受信データの書込み開始位置」 に再生地点を設定した場合です. バッファ上部の文字は, ひとつ前の図との間の時間に再生される音声を表しています. 送信側は「いろはにほへとち」と発声しています. 1,2,3秒時点では順調にデータを受信できていたのですが, 4秒後にデータが届かなかったという事態を想定しています. この場合, 右側では4.5秒経過時点で「適切でないバッファ」を再生しています. そして5秒後にデータが到着したとすると, 左側と右側の両方において再生位置の巻き戻りが発生します. 左側の再生音声:「 いろはにほへほへ」 右側の再生音声:「いろはにほへ☆☆とち」 ノイズが入るか,音が飛ぶか・・・ほへほへ! 右側の再生位置制御に関して, 再生済みバッファを0クリアしまくればノイズを無音にできますが どちらにせよ,音飛びは回避できません. どうしようかな. まぁ悪いのは通信遅延って事で!(ヤッホウ!) ところで,このような再生位置制御を行うと 再生位置変更時点で微小なノイズが発生します. 恐らく,上記のような遅延による不具合が小規模に発生しているのでしょう. 実はこの現象,ADPCM導入によって音質を向上させるまでは気づきませんでした. 「できる男は目標に到達しても振り返らない! その先にまた新たな目標が見えてしまうからだ!! できる男にふり返ってるヒマなど、ないっ!!! 」 島本和彦『逆境ナイン』より 今後は,再生位置制御を必要最小限に抑えなければなりません. 今回導入したシーケンスIDを使えばなんとかなりそうです. 今後の課題: 再生位置制御の最適化
前回: シーケンスIDの追加と再生位置補正を行った 今回: 再生位置補正の最適化によりノイズを除去した 内容: ポコッと出やがった問題をやっつけました. シーケンスIDが連番でないときだけ再生位置制御を行うようにしました. これによって細かな再生位置制御で生じる小さいノイズを除去することができました. 今後は遅延が生じるような環境でテストを行う必要があります. グローバルIP保有端末同士の通信か, またはSoftEtherを用いれば実験ができそうです. ・・・でもまぁ,別にやんなくてもいいかなぁ・・・(目線をそらしながら) そろそろ音声通信以外の仕事をやらなければならない気がするので とりあえずこれでひと段落,ということにしておきます. 振り返ると, アルゴリズムの考案よりも 用意された機能の用法や組み合わせに苦労していた気がします. C#とDirectXの機能の充実に感謝するばかりですが,自分を省みれば どう見てもプログラマというよりエディタです. 本当にありがとうございました. 通信周りをいじっていると,どんどんSIPとRTPのような感じになっていきました. 通信データを自分で好きなようにいじれるというのは強みではありますが, 便利な技術を食わず嫌いするのも良くないと思うので 機会があれば挑戦してみたいですね. 今後の課題: 遅延が発生しうる環境でのテスト
[EOF]