Bakulog

獏の夢日記的な何か。

【C#】PCのマイクで拾った音をPepperに送って再生させる【qi Framework】

リアルタイムなスピーカーとしてPepperを使おうという話の紹介です。

 

もくじ 1. はじめに 2. C#でマイク入力を拾うには 3. Pepperの音声出力APIの話 4. サンプルコード 5. まとめ

   

はじめに

本記事は前に公開した「C#プログラムでPepperが聞いてる音をストリーミング」の対極となる処理、つまり手元のマイクに向かって喋った音をPepperに投げつけて再生させる、言い換えるとPepperの口を乗っ取る方法の紹介になります。Pepperに音声バッファを投げる方法については直接的なAPIがあり、参考としてアルデバラン社の方が非公式で試された方法紹介もあります。この記事ではスタンダードにC++で試してますが、コアになる処理はそんなに複雑ではなくC#にも移植できそうなので、実際にやってみました。

   

C#でマイク入力を拾うには

C#で声の入力を拾う方法ですが、音声出力のプログラムでもお世話になったNAudioは当然と言わんばかりに音声入力にも対応しているため、再びNAudioを使います。入力を拾うにはWaveInEventクラスを利用すればよく、使い方もそんなに複雑ではありません。例えばPCにマイク入力を入れ、そのままエコーバック的に出力する場合は次のようなコードを書きます。

using System;
using NAudio.Wave;
using NAudio.CoreAudioApi;

namespace NAudioEchoBack
{
    class Program
    {
        static void Main(string[] args)
        {
            var mmDevice = new MMDeviceEnumerator().GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia);

            using (var waveIn = new WaveInEvent())
            using (var wavPlayer = new WasapiOut(mmDevice, AudioClientShareMode.Shared, false, 300))
            {
                //フォーマットを入力側に合わせないと速度やらなんやらおかしくなるので注意!
                var wavProvider = new BufferedWaveProvider(waveIn.WaveFormat);
                //NOTE: うるさいと思ったらボリュームを絞る
                //wavPlayer.Volume = 0.01f;
                wavPlayer.Init(new VolumeWaveProvider16(wavProvider));
                wavPlayer.Play();

                waveIn.DataAvailable += (_, e) =>
                {
                    wavProvider.AddSamples(e.Buffer, 0, e.BytesRecorded);
                };
                waveIn.StartRecording();
                Console.WriteLine("Press ENTER to quit...");
                Console.ReadLine();
                waveIn.StopRecording();
            }
            Console.WriteLine("Program ended successfully.");
        }
    }
}

 

上の例は音声の入力コードと出力コードが入り混じっていますが、マイク入力に関係ある部分だけ抜粋するとこうなります。

using (var waveIn = new WaveInEvent())
{
    waveIn.DataAvailable += (_, e) =>
    {
        //byte[]であるe.Bufferのうち先頭からe.BytesRecorded個が有効な録音データなので
        //それを使って何かする
    };
    //音声の取得開始
    waveIn.StartRecording();

    //他の処理

    //終了
    waveIn.StopRecording();
}

 

かなり単純な作りであり、上記のイベントハンドラ部分に「Pepperに音を再生させる」という処理を記述すればよさそうな事が分かります。

   

Pepperの音声出力APIの話

Pepperに音声出力用のスピーカーがついているのは今更言うまでも無い事実ですが、このスピーカーはALAudioDeviceALTextToSpeechなど一部のAPIからのみ触れるようになっています。

今回に関しては冒頭でも紹介したこちらの記事を見ると"ALAudioDevice"サービスの"sendRemoteBufferToOutput"関数を使えば普通に再生できますよ、と書かれています。同記事で紹介されているGitHubのソースコードも参考になりますし、公式ドキュメンテーションではこのへんに色々書かれています。関数のシグネチャはこんな感じ。

bool ALAudioDeviceProxy::sendRemoteBufferToOutput(const int& nbOfFrames, const AL::ALValue& buffer)

この関数ですが"sendRemoteBufferToOutput"関数のドキュメンテーションのすぐ上にあるsendLocalBufferToOutput関数のサンプル等を見ると、次のような注意点があることが分かります。

  • フレーム数にあたる数値はバイト配列の長さの1/4相当を指定すればOK
  • バイト配列にはリトルエンディアンの値で16bit値を、左右のスピーカー用に交互に入れていく
  • 出力のサンプリングレートを変えたい場合は"setParameter"関数越しで変更するらしい

これらを踏まえて出力用のコードを書くと、およそ次のような感じになります。

using Baku.LibqiDotNet;

//...

const int BufferSize = 16384;
var data = byte[BufferSize];

//dataに実際の音声を入れる処理

var audioDevice = QiSession.Create(address).GetService("ALAudioDevice");
audioDevice.Call("setProperty",
    new QiString("outputSampleRate"),
    new QiInt32(16000)
    );

audioDevice.Call("sendRemoteBufferToOutput", 
    new QiInt32(BufferSize / 4), 
    new QiByteData(data)
    );

 

上の例では出力サンプリングレートを最低値の16kHzに設定しています。当然ですが周波数は通信データ量にほぼ直結するので、必要に応じてほどほどの値に設定します。

   

サンプルコード

入力と出力をくっつけて動くようにしたのが下記のコードです。実行にあたってはNAudioが必要でありアンマネージドライブラリの配置も必要なので、実行できない場合はBaku.LibqiDotNetのGithubとか「C#プログラムでPepperが聞いてる音をストリーミング」に載っている内容を確認してください。

using System;
using System.IO;
using System.Linq;

using NAudio.Wave;
using NAudio.CoreAudioApi;
using Baku.LibqiDotNet;

//...

static void Main(string[] args)
{
    //接続先となるPepperのアドレス
    string address = "tcp://192.168.xx.xx:9559";

    AddEnvironmentPaths(Path.Combine(Environment.CurrentDirectory, "dlls"));

    var session = QiSession.Create(address);
    var audioDevice = session.GetService("ALAudioDevice");

    //出力サンプリングレートをデフォルト(48kHz)から16kHzに下げる
    audioDevice.Call("setParameter",
        new QiString("outputSampleRate"),
        new QiInt32(16000)
        );

    using (var waveIn = new WaveInEvent())
    {
        waveIn.BufferMilliseconds = 200;

        int count = 0;
        waveIn.DataAvailable += (_, e) =>
        {
            byte[] bufferToSend = new byte[e.BytesRecorded];
            Array.Copy(e.Buffer, bufferToSend, e.BytesRecorded);

            int p = audioDevice.Post("sendRemoteBufferToOutput",
                new QiInt32(bufferToSend.Length / 4),
                new QiByteData(bufferToSend)
                );
            Console.WriteLine(count);
            count++;
        };

        waveIn.WaveFormat = new WaveFormat(16000, 16, 2);
        waveIn.StartRecording();
        Console.WriteLine("Press ENTER to quit...");
        Console.ReadLine();
        waveIn.StopRecording();
    }
}

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);
}

  

うまく行った場合、実行してヘッドセット等から喋ればPepperから音が出ます。マイクが正しく選ばれずに失敗する場合はコントロールパネルで"sound"などのキーワードから検索してサウンド設定を開き、録音デバイスとして使いたいマイクを既定のデバイスに設定してください。

   

まとめ

終わってしまえばC++コードの移植以上のものではありませんでしたが、まあ「やればできるね」という話でした。