Bakulog

獏の夢日記的な何か。

【C#】生バイト配列として受け取った音声をリアルタイム再生する【NAudio】

音声ストリーミング関連の小ネタです。特殊な状況でないと使わなさそうですが折角なので。

 

もくじ

  1. はじめに
  2. NAudio: .NETの音声処理ライブラリ
  3. バッファ再生用クラスBufferedWaveProvider
  4. サンプルコード
  5. まとめ

   

はじめに

本記事では「断片的なバイト配列として音声データを受け取りその場で再生する」という事をC#のプログラムで実践します。

状況想定としては「LAN内の別のマシンが集音マイクを装備しており、拾った音をそのままの(いわゆるwavデータ的な)形でネットワーク越しにポンポン投げてくる」というような、非常にリアルタイム性の高い状況を想定しています。こういう状況で飛んで来た音声データをC#プログラムで再生しましょう、と。

   

NAudio: .NETの音声処理ライブラリ

今回のような処理にうってつけのライブラリとして.NETにはNAudioなるライブラリがあります。NAudioにはリアルタイム処理、波形解析、コーデック変換など様々な機能が実装されており、対応する音声フォーマットも結構多いようです。日本語記事としてはうめつる開発室さんがいくつか記事を書かれており、ヒジョーに参考になります。

というわけで、今回はこちらのNAudioを使ってリアルタイム再生を行います。

   

バッファ再生用クラスBufferedWaveProvider

上にも書いた通りNAudioは便利で多機能なライブラリなのですが、多機能性の代償として機能を探すのがめんどくさく、ドキュメントやサンプルをしっかり読まないと所望の機能がどこにあるか分かりません。

リアルタイム再生に関してはこちらのMark Heathさんが書かれた英語記事にやり方が載っており、特に大切なこととしてBufferedWaveProviderなるクラスを使うと生のバイト配列書き込みによって再生が出来ると書かれています。紹介元の記事では更に詳しい説明があります1が、本記事では細かいことは気にせず「とりあえず動け!」程度の感覚でやってみます。

   

サンプルコード

Visual Studioでコンソールアプリケーションを作り、NuGetパッケージマネージャでNAudioをインストールしたうえで次のようなコードを書きます。namespaceclassはインデント的に邪魔なので省略しています。

using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;

using NAudio.Wave;
using NAudio.CoreAudioApi;


//...

static void Main(string[] args)
{
    //一般的な44.1kHz, 16bit, ステレオサウンドの音源を想定
    var bufferedWaveProvider = new BufferedWaveProvider(new WaveFormat(44100, 16, 2));

    //ボリューム調整をするために上のBufferedWaveProviderをデコレータっぽく包む
    var wavProvider = new VolumeWaveProvider16(bufferedWaveProvider);
    wavProvider.Volume = 0.1f;

    //再生デバイスと出力先を設定
    var mmDevice = new MMDeviceEnumerator()
        .GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia);

    //外部からの音声入力を受け付け開始
    Task t = StartDummySoundSource(bufferedWaveProvider);

    using (IWavePlayer wavPlayer = new WasapiOut(mmDevice, AudioClientShareMode.Shared, false, 200))
    {
        //出力に入力を接続して再生開始
        wavPlayer.Init(wavProvider);
        wavPlayer.Play();

        Console.WriteLine("Press ENTER to exit...");
        Console.ReadLine();

        wavPlayer.Stop();
    }
}

//外部入力のダミーとしてデスクトップにある"sample.wav"あるいは"sample.mp3"を用いて音声を入力する
static async Task StartDummySoundSource(BufferedWaveProvider provider)
{
    //外部入力のダミーとして適当な音声データを用意して使う
    string wavFilePath = Path.Combine(
        Environment.GetFolderPath(Environment.SpecialFolder.Desktop),
        "sample.wav"
        );
    //mp3を使うならこう。
    string mp3FilePath = Path.Combine(
        Environment.GetFolderPath(Environment.SpecialFolder.Desktop),
        "sample.mp3"
        );

    if (!(File.Exists(wavFilePath) || File.Exists(mp3FilePath)))
    {
        Console.WriteLine("Target sound files were not found. Wav file or MP3 file is needed for this program.");
        Console.WriteLine($"expected wav file: {wavFilePath}");
        Console.WriteLine($"expected mp3 file: {wavFilePath}");
        Console.WriteLine("(note: ONE file is enough, two files is not needed)");
        return;
    }

    //mp3しかない場合、先にwavへ変換を行う
    if (!File.Exists(wavFilePath))
    {
        using (var mp3reader = new Mp3FileReader(mp3FilePath))
        using (var pcmStream = WaveFormatConversionStream.CreatePcmStream(mp3reader))
        {
            WaveFileWriter.CreateWaveFile(wavFilePath, pcmStream);
        }
    }

    byte[] data = File.ReadAllBytes(wavFilePath);

    //若干効率が悪いがヘッダのバイト数を確実に割り出して削る
    using (var r = new WaveFileReader(wavFilePath))
    {
        int headerLength = (int)(data.Length - r.Length);
        data = data.Skip(headerLength).ToArray();
    }

    int bufsize = 16000;
    for (int i = 0; i + bufsize < data.Length; i += bufsize)
    {
        provider.AddSamples(data, i, bufsize);
        await Task.Delay(100);
    }
}

 

本体である「バッファ入力を受け取って再生する」という機構はMain関数側だけで完結しており、下のStartDummySoundSource関数はテスト用のダミー入力を生成するためのメソッドです。

ダミー入力の生成のために、デスクトップにwav音声ファイル"sample.wav"あるいはmp3ファイル"sample.mp3"のいずれかを配置してください。また、音声フォーマットは44.1kHz, 量子化16bit, ステレオチャネルであることを想定しています2。手元に使えそうな音声ファイルが無い場合、フリーの音源サイト(例えば甘茶の音楽工房3など)で主にmp3形式のファイルが見つかるので入手し、"sample.mp3"にリネームしてデスクトップに配置してください。

 

うまく行くと音源が若干ブツ切りで再生されます。ぶつ切りになるのはダミー入力側の入力サンプルレートをわざと正常値である44100より小さく設定しており、データが届くより先に再生が終わってしまうためです。ただしコレは欠点を見せようというのではなくて、BufferedWaveProviderの次のような長所に触れようと思っての実装です。

  • 上の例ではぶつ切り再生になるが、逆にデータ入力が間に合えば普通に再生できる
  • いったんデータが途切れても再びデータが入れば普通に続きが再生される

要するに、リアルタイム処理でもそう簡単に失敗しないように作られています。

あとは上のStartDummySoundSourceに代わり、ホンモノの音源データを監視する関数さえ実装すれば実際のデータでも同様に出来ます。もちろんその際はMain関数内で定義しているデータのサンプリングレートなどが正しいか十分注意してください。リアルタイムな集音アプリケーションでは44100Hzではなく8000Hzなど、低めのサンプリングレートで集音するケースも多いです。

   

まとめ

今回は以上です。完全に目的特化な話題ですが、特定の状況化では覚えておいて損しないネタだと思うので、有効に活用していただければと思います。

 


  1. 元記事では「バッファにデータが溜まり過ぎたらどうすんの」といった問題への対処法が載っているので、音声データの受信環境が厳しい場合は元記事も読んでおいた方がいいです。 
  2. 量子化bit数以外はズレてても多分動きますが、その場合おそらく音声ファイルの再生速度がおかしいことになります。 
  3. 本記事とは一切関係ありませんが、フリー音源サイトの中でも甘茶の音楽工房の曲は音質がやたら良いので私は非常に気に入っています。