何度か話題にしている、Pepperが聞いている音声を拾うシリーズの続きです。
2016/3/15追記: 本記事のプログラムはNuGetパッケージとして公開してるBaku.LibqiDotNetのver1.0.1では動作しますが、GitHubの最新コードやNuGetパッケージの最新版(2.0)では動作しません。ご注意下さい。
もくじ
PepperはC#から操作できるんです
そもそもC#でPepperが操作できるライブラリがあるのかという話ですが、私が作ったラッパーがあるので、結論から言うとC#でのPepper操縦は可能です。使い勝手という点では若干アレな部分もありますが、他言語のライブラリ(Java
とかPython
とか)と根本的には同程度の機能を提供しています。
ラッパーのソースはGitHubにあり、導入方法もGitHubの方に掲載しているので、導入はそちらを参考に行ってください。本記事では環境整備が整っている前提で音声を拾うプログラムの作成方法を紹介します。
Pythonで作ったコードを真似しよう
音声を拾う方法はコンセプトが最重要であり、実装言語の差はそこまで大きな差になるわけではありません。Python
の例は「Python/qi Frameworkを使ってPepperのマイクからストリーミングする」で紹介しています。Python
では次のような手業手順で音声を拾ってリアルタイム再生するところまで漕ぎつけていました。
- 自作クラスを定義し、クラスの中に
processRemote
というコールバック関数を定義する。この関数では、音声データのバイナリを受け取ってPC上で再生できる処理を書く(Pythonの場合PyAudioを使う)。 qi.Session
のregisterService
という関数を使い、1で定義したクラスのインスタンスをPepper上に登録するALAudioDevice
で初期設定を行ったうえでsubscribe
関数を呼び出すことで、2で登録したインスタンスが実際に音声データを連続的に受け取れるようになる
このうち2と3の処理はC#
でもPython
とほぼ同様に出来ますが、1については少し雰囲気が違うことをやります。ただし根本的に違うことをするのではありません。Python
にくらべてC#
のラッパーライブラリは作りが薄いので、よりqi Frameworkの生の処理に近いことをやるというだけです。具体的にはPython
のクラス定義に代わる次のようなコードを記述し、クラスっぽいものを表現します。
var objBuilder = QiObjectBuilder.Create(); objBuilder.AdvertiseMethod( "processRemote::v(iimm)", (sig, arg) => { //ここで処理 byte[] raw = arg[3].GetRaw(); //wavProviderというのが「データを書き込むと再生してくれる」やつ wavProvider.AddSamples(raw, 0, raw.Length); return QiValue.Void; }); var service = objBuilder.BuildObject();
QiObjectBuilder
クラスはqi Frameworkへ登録できるインスタンスを生成するためのクラスで、AdvertiseMethod
関数を用いることで「今から作るインスタンスにはこんなメンバ関数が用意されています」というのを手続き的に登録していくことができます1。このような手順を踏むと、qi Framework
から見るとPython
で作ったのと区別がつかないレベルで等価な、「"processRemote"という関数が定義されたインスタンス」が作成できます。
C#の場合リアルタイムに音声再生ってどうやるの?
サービスの生成問題は解決しましたがC#
のコードを書く前に把握すべき問題はもう一点あります。Python
ではPyAudioというライブラリでバイナリを用いた音声再生を行っていましたが、当然これはPython
用のライブラリですからC#
では使えません。似たような機能のライブラリを探す必要があります。
そこでちょっと調べてみるとNAudioというメジャーな音声処理ライブラリが見つかるので、今回はこれを使います。特にNAudioを用いてリアルタイムに断片のバイナリデータを再生する方法はC#で、音声を生バイト配列として受け取って再生するで紹介しています2。
実際のプログラム
以上の要素知識を踏まえてコードを書くと次のようになります。コンソールアプリケーションを想定した例になっています。
[expand title="C#でPepperが聞いてる音を取得するコード(クリックで展開)"]
using System; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using NAudio.CoreAudioApi; using NAudio.Wave; using Baku.LibqiDotNet; namespace PepperSoundGetter { class Program { static void Main(string[] args) { AddEnvironmentPaths(Path.Combine(Environment.CurrentDirectory, "dlls")); //Pepperのアドレス string address = "tcp://192.168.xxx.xxx:9559"; DownloadAndPlaySound(address); } static void DownloadAndPlaySound(string address, string serviceName = "CSharpSoundDownloader") { #region 1/3: バッファを入れると再生するwavProviderを作る var mmDevice = new MMDeviceEnumerator().GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia); var wavProvider = new BufferedWaveProvider(new WaveFormat(16000, 16, 1)); var wavPlayer = new WasapiOut(mmDevice, AudioClientShareMode.Shared, false, 200); //NOTE: うるさいと思ったらボリュームを絞る //wavPlayer.Volume = 1.0f; wavPlayer.Init(new VolumeWaveProvider16(wavProvider)); wavPlayer.Play(); #endregion #region 2/3: Pepper/Naoに接続し、音声ダウンロード用のサービスを登録 var objBuilder = QiObjectBuilder.Create(); //コールバックであるprocessRemote関数を登録することでALAudioDevice側の仕様に対応 objBuilder.AdvertiseMethod( "processRemote::v(iimm)", (sig, arg) => { //ここで処理 //Console.WriteLine("Received Buffer!"); //Console.WriteLine(arg.Dump()); //データの内容については上記のダンプを行うことで確認可能 byte[] raw = arg[3].GetRaw(); wavProvider.AddSamples(raw, 0, raw.Length); return QiValue.Void; }); //上記のコールバック取得用サービスを登録 var serverSession = QiSession.Create(address); serverSession.Listen("tcp://0.0.0.0:0").Wait(); ulong registeredId = serverSession.RegisterService(serviceName, objBuilder.BuildObject()).GetUInt64(0UL); #endregion #region 3/3: クライアントとして音声ダウンロードを開始 var audioDevice = QiSession.Create(address).GetService("ALAudioDevice"); //マジックナンバーあるけど詳細は右記参照 http://www.baku-dreameater.net/archives/2411 audioDevice.Call("setClientPreferences", new QiString(serviceName), new QiInt32(16000), new QiInt32(3), new QiInt32(0) ); //開始。 audioDevice.Call("subscribe", new QiString(serviceName)); try { Console.WriteLine("Press ENTER to exit"); Console.ReadLine(); } finally { audioDevice.Call("unsubscribe", new QiString(serviceName)); serverSession.UnregisterService((uint)registeredId); wavPlayer.Stop(); wavPlayer.Dispose(); } #endregion } static void AddEnvironmentPaths(params string[] paths) { var path = new[] { Environment.GetEnvironmentVariable("PATH") ?? "" }; string newPath = string.Join(Path.PathSeparator.ToString(), path.Concat(paths)); Environment.SetEnvironmentVariable("PATH", newPath); } } }
[/expand]
コードを動かす際の注意は以下の通りです。
- コードでPepperのIPアドレスを指定するよう書き換える
- NuGetパッケージマネージャで"NAudio"をインストールする
- "Baku.LibqiDotNet.dll"を参照に加える
- "Baku.LibqiDotNet"のGithubにあるように、実行ディレクトリに"dlls"というフォルダを作ってアンマネージライブラリを配置する
うまく行くとPepperが聞いている音がそのままPCで再生されます。
まとめ
Pepperからの音声取得はそこそこ難しい処理なのですが、C#
のラッパー越しでも普通に出来るというのが紹介出来ました。これに限らずC#
のラッパーライブラリで面白い事が出来たら随時紹介していきたいと思います。
-
本文で触れていませんが、
AdvertiseMethod
では第一引数で関数名のあとに"::v(iimm)"
という良く分からない文字列がくっついてます。特に"::"
のあとに続く文字"v(iimm)"
は登録したい関数のシグネチャを表しており、今回であれば「(int
、int
、object
、object
)が引数で戻り値がvoid
」くらいの意味になってます。シグネチャの文字単位での意味は公式のドキュメントにも少しだけ載ってますが、C#
ラッパーを使うにはコレだけだと難しい部分もあるので、これについては別のドキュメンテーションか記事にまとめる予定です。 ↩ - 白々しい書き方になってますが、実際はC#で、音声を生バイト配列として受け取って再生するは本記事で使う前提知識を紹介しようと思って書いた記事です。 ↩