Bakulog

獏の夢日記的な何か。

C#プログラムでPepperが聞いてる音をストリーミング

何度か話題にしている、Pepperが聞いている音声を拾うシリーズの続きです。

 

2016/3/15追記: 本記事のプログラムはNuGetパッケージとして公開してるBaku.LibqiDotNetのver1.0.1では動作しますが、GitHubの最新コードやNuGetパッケージの最新版(2.0)では動作しません。ご注意下さい。

 

もくじ

  1. PepperはC#から操作できるんです
  2. Pythonで作ったコードを真似しよう
  3. C#の場合リアルタイムに音声再生ってどうやるの?
  4. 実際のプログラム
  5. まとめ

   

PepperはC#から操作できるんです

そもそもC#でPepperが操作できるライブラリがあるのかという話ですが、私が作ったラッパーがあるので、結論から言うとC#でのPepper操縦は可能です。使い勝手という点では若干アレな部分もありますが、他言語のライブラリ(JavaとかPythonとか)と根本的には同程度の機能を提供しています。

ラッパーのソースはGitHubにあり、導入方法もGitHubの方に掲載しているので、導入はそちらを参考に行ってください。本記事では環境整備が整っている前提で音声を拾うプログラムの作成方法を紹介します。

   

Pythonで作ったコードを真似しよう

音声を拾う方法はコンセプトが最重要であり、実装言語の差はそこまで大きな差になるわけではありません。Pythonの例は「Python/qi Frameworkを使ってPepperのマイクからストリーミングする」で紹介しています。Pythonでは次のような手業手順で音声を拾ってリアルタイム再生するところまで漕ぎつけていました。

  1. 自作クラスを定義し、クラスの中にprocessRemoteというコールバック関数を定義する。この関数では、音声データのバイナリを受け取ってPC上で再生できる処理を書く(Pythonの場合PyAudioを使う)。
  2. qi.SessionregisterServiceという関数を使い、1で定義したクラスのインスタンスをPepper上に登録する
  3. ALAudioDeviceで初期設定を行ったうえでsubscribe関数を呼び出すことで、2で登録したインスタンスが実際に音声データを連続的に受け取れるようになる

 

このうち23の処理は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#のラッパーライブラリで面白い事が出来たら随時紹介していきたいと思います。

   


  1. 本文で触れていませんが、AdvertiseMethodでは第一引数で関数名のあとに"::v(iimm)"という良く分からない文字列がくっついてます。特に"::"のあとに続く文字"v(iimm)"は登録したい関数のシグネチャを表しており、今回であれば「(intintobjectobject)が引数で戻り値がvoid」くらいの意味になってます。シグネチャの文字単位での意味は公式のドキュメントにも少しだけ載ってますが、C#ラッパーを使うにはコレだけだと難しい部分もあるので、これについては別のドキュメンテーションか記事にまとめる予定です。 
  2. 白々しい書き方になってますが、実際はC#で、音声を生バイト配列として受け取って再生するは本記事で使う前提知識を紹介しようと思って書いた記事です。