.NET でサウンドプログラミングをやっていきたい (libsoundio-sharp 準備編)


はじめに

ふとサウンドプログラミングをやってきいきたくなったので、とりあえず .NET でメモリーからのストリーム再生ができる環境の準備から始めましょうということで。

静的な音声ファイルを再生するのは最近だとどんな環境でも簡単にできるように思いますが、音声波形をリアルタイムに生成して再生できるようにするにはストリーム再生ができる API が必須になります。この領域は低レベル層で大抵はプラットフォーム依存なのですが、そういえば昨年の C# Advent Calender で @atsushieno さんがクロスプラットフォームなサウンドネタを記事にされていました。

この記事、実際のところ大部分は P/Invoke の自動バインディングがテーマで非常に興味深い良記事なのですが、 libsoundio-sharp 自体はほとんど触れていません
じゃあせっかくだからちょっと試してみようかなあ、というところから始めます。

ビルド手順

リポジトリ取得

上記からリポジトリをチェックアウトしてください。

後の方に出てくる拡張適用版はこちらになります。 (オリジナルの改変はしていません)

ネイティブの DLL のビルド (Windows のみ)

libsoundio-sharp はラッパーで本体はネイティブの DLL です。

Linux, macos 用のバイナリはリポジトリに含まれているようですが、 Windows 用はリポジトリにないので別途用意する必要があります。

公式サイトにある Windows 用ビルド済バイナリは使用できませんでした。

なので submodule で指定されているリポジトリのものを自前でビルドします。mingw とか追加で環境を用意するのが面倒だったので Visual Studio の CMake 機能でビルドします。

  1. Visual Studio Installer を起動し、 "個別のコンポーネント" で "CMake の Visual C++ ツール" を入れておきます。
  2. submodule を更新します。
  3. VS2017 を起動し、 "ファイル - 開く - CMake" を選択し、 "external\libsoundio\CMakeLists.txt" を開きます。
  4. "external\libsoundio\src" 直下に中身が空の "unistd.h" を作成しておいておきます。
  5. "\external\libsoundio\soundio\soundio.h" を開いて先頭の方の適当な箇所に下記を追記します。

    #ifdef _MSC_VER
    #define __attribute__(x)
    #endif
    
  6. ビルド構成を選んで "CMAKE - ビルドのみ - soundio.dll" を実行します。構成は "x64-Debug" と "x64-Release" で。

  7. エラーがなければ "%USERPROFILE%\CMakeBuilds(何かID)\build" の下に構成別にビルド結果が出力されているはずです。

なぜ x64 ビルドか

単に x86 で動かなかったからです。呼び出し規約問題だとは思うのですが、単純に全ての DllImport, delegate に cdecl を指定してもダメでした。 x64 は呼び出し規約が一つしかないのでこの手のトラブルがないのはよいです。

ネイティブ側含めてもう少し精査した方がよいかもですが、まあ x64 動いてるならいいんじゃね?的な感じでとりあえずスルー・・・

.NET 側 DLL のビルド、サンプルの実行

  1. libsoundio-sharp.sln を 開きます。
  2. (Windows) ソリューションの構成で x64 を追加します。
  3. ビルドします。
  4. (Windows) 先にビルドしたネイティブの soundio.dll を .exe (厳密には libsoundio-sharp.dll と同じ場所) にコピーします。とりあえず音が鳴る事を確認したい場合は sio-sine サンプルがよいので、 "example\sio-sine\bin\x64\Debug" 下に x64 ビルドした soundio.dll をコピーしてください。
  5. 実行します。

ちょっと使いやすくしてみる

サンプルを見ると低レベル API だけあって、使用するには非常に泥くさい実装を求められています。

とりあえずコードで音を作っていきたいだけなので、簡単に使えるようにしてみました。

上記のような拡張メソッドを実装してみました。これ使うと 2 秒間 440Hz のサイン波を鳴らすには次のようになります。

const int sampleRate = 48000;
const int pitch = 440;
Enumerable.Range(0, sampleRate * 2)
    .Select(x => (float)Math.Sin(2.0 * Math.PI * pitch * x / sampleRate))
    .PlaySound(sampleRate);

F# で書くとこんな感じ (?) になりました。

let sampleRate = 48000
let pitch = 440.0
let play stream = SoundIOOutStreamUtil.Play(stream, sampleRate)

[0..sampleRate * 2]
|> List.map(fun x -> (float32)(Math.Sin(2.0 * Math.PI * pitch * (float)x / (float)sampleRate)))
|> play

SoundIOOutStreamUtil.Play メソッド (C# の PlaySound 拡張メソッドはこのメソッドにまわすだけのもの) は Enumerator が終了するまでスレッドロックしてしまいますが、「音声波形を生成する関数のテスト用」という位置付なのでまあこれでよいかなあと。

おわりに

今回はコードで作った任意の波形を再生を行えるようにするまでを行いました。

波形生成で単純に時間引数の関数を定義する、というのはパフォーマンス的にはよくないとは思いますが、わかりやすさ優先と F# の勉強を兼ねているのでこの方向で。次回は未定 (気が向いたら) ですがサウンドネタは続けていきたいと思います。