Unityでマルチチャンネル音声入力を試す

#はじめに

UnityはMicrophoneクラスを使えば簡単にマイクから音声データが取得出来ます。
ただ、Microphoneクラスは1デバイスしか識別しないため、同時に複数のマイク入力をやりたい場合は複数のオーディオIFを接続する必要があります。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">5台つながったのでマイク祭の始まり！<br>ZOOM製品が多いのはたまたまです。 <a href="https://t.co/1rezY8o9Bl">pic.twitter.com/1rezY8o9Bl</a></p>&mdash; アオダマ (@aodama) <a href="https://twitter.com/aodama/status/1403658961140674564?ref_src=twsrc%5Etfw">June 12, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

こんな状況になります…

テストの度にこれではとても面倒なので、これを何とかしようと音声のマルチチャンネル入力を試すことにしました。

なお検証環境はUnity 2020.3.18f1です。

#結果

マルチチャンネル入力対応のオーディオIFとASIOドライバを使うことで、1台のオーディオIFで複数マイク入力出来るようになりました。
マイクが3本しかなかったので地味ですが、各マイクに音が入るとミキサーが反応しています。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">ASIOドライバ経由だとLiveTrak L-8の入力がマルチチャンネルで行えるので、これで複数オーディオIF繋げなくても複数マイク入力のテストがUnityで出来る！ <a href="https://t.co/5Gt9FklPx4">pic.twitter.com/5Gt9FklPx4</a></p>&mdash; アオダマ (@aodama) <a href="https://twitter.com/aodama/status/1428592706872172546?ref_src=twsrc%5Etfw">August 20, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

#ASIOドライバならマルチチャンネル入力できるの？

オーディオIFによっては12in/4outのような複数入力対応している製品がありますが、UnityのMicrophoneクラスからはあくまで1デバイス（1入力）としか認識されません。
しかしASIOドライバを経由すれば複数入力出来るようなので、UnityからASIOドライバ経由でオーディオIFにアクセスできればマルチチャンネル入力が使えそうです。

今回検証に使うオーディオIFは12in/4out対応のZOOM LiveTrak L-8です。
https://zoomcorp.com/ja/jp/digital-mixer-multi-track-recorders/multi-track-recorders/LIVETRAK-L-8/

まずDTMソフトのFL StudioからL-8の入力を確認すると、ASIOドライバ使用時はスペック通り12in出来ることがわかりました。
ちなみにキャノンケーブルで接続するマイクはIn1～6になります。
![FL StudioからASIO経由でL8の入力を確認.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227733/3442a214-af0f-5cfb-a3e6-ea21d9b857cb.png)

#ASIOSDKを使って動作確認

ここからASIO SDKをダウンロードします。
https://www.steinberg.net/developers/

とりあえず動かすところまで持っていくのはここを参考にするといいでしょう。
https://zenn.dev/kodai100/articles/ca449cf826e5ce

上記サイトとASIO SDK付属のhostsample.cppを参考にしてASIOドライバが使えるようになったら、データ入出力時に呼ばれる

```
ASIOTime* bufferSwitchTimeInfo(ASIOTime* timeInfo, long index, ASIOBool processNow)
```
で音声データが取得出来ているか確認できます。
確認のやり方は色々ありますが、私は3秒録音（メモリに保存）し、録音終了後に保存した音声を再生して確認しました。
なお真面目に対応するとASIOSampleTypeに合わせてデータを整形しないとですが、今回はL-8限定でASIOSTInt32LSBだけ処理しました。あくまで確認なので。

問題はこれをUnityからどうやって使うかになります。実装の候補としては

* ASIOSDKをDLL化してC#側でDllImportする
* ASIOSDKをC++/CLIを使ってアセンブリとして使う
* C#でASIOSDK相当を再実装（やることは主にレジストリの読み込みとCOMを扱うだけなので）

あたりですが、ぶっちゃけどれも面倒だ…

#救世主NAudio

ASIOドライバをUnity（マネージド環境）で動かせるライブラリがないかなーと探していたらありました！

NAudio
https://github.com/naudio/NAudio

それもASIOで録音出来る！
https://github.com/naudio/NAudio/blob/master/Docs/AsioRecording.md

はい、もうさっきのASIO SDKは不要です。もう一気にUnityでやってしまいましょう。

ただ、過去トラブルが出ているようですが、今回試した限りでは特にトラブルはありませんでした。
http://furipro.blog.fc2.com/blog-entry-8.html

#NAudioのインストール

NAudioのver.1.8.4はビルド済みのzipがあるので、それをダウンロードします。
https://github.com/naudio/NAudio/releases/tag/v1.8.4

新規でUnityプロジェクトを作り、NAudio-Release.zip内のNAudio.dllをUnityのAssets配下のどこかにコピーします。
慣例に従うならAssets\Plugins\x86_64内です。
![NAudioインストール.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227733/c0ad01a6-06b5-b3f2-ce97-ec9f155772b8.png)
これでNAudioを使う準備は出来ました。

#ASIOドライバの初期化

UnityでC#スクリプトを作成し、最低限これだけ書けば動きます。

```cs
private AsioOut _asioOut;

private void Start()
{
    _asioOut = new AsioOut("ZOOM L-8 ASIO Driver"); // 使用するオーディオIF
    _asioOut.InitRecordAndPlayback(null, 12, 48000); // 12ch録音する
    _asioOut.InputChannelOffset = 0; // 録音する先頭ch
    _asioOut.AudioAvailable += OnAsioOutAudioAvailable; // データ取得時のイベント登録
    _asioOut.Play(); // 録音開始
}

void OnAsioOutAudioAvailable(object sender, AsioAudioAvailableEventArgs e)
{
    e.GetAsInterleavedSamples(_inputSamples);	// 事前に確保しておいたfloat[]。サイズはASIOバッファサイズ * ch数

    // 音声データをUnityのメインスレッドに渡す（ConcurrentQueueを使うと簡単）
}
```

録音が始まり音声データが入ってくると登録したイベントが呼ばれます。
InputChannelOffsetはどのチャンネルから読み込むか指定できます。L-8の入力は

![L-8の入力レイアウト.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227733/0507dcb6-e70e-f35d-d7a8-23c2dbc73831.png)

なので、マイクの音声データだけ取るなら

```cs
    _asioOut.InitRecordAndPlayback(null, 6, 48000);
    _asioOut.InputChannelOffset = 2;
```

と設定出来ます。

#注意すること

音声データ取得時のイベントはUnityのメインスレッドとは別スレッドで呼ばれるイベントなので、Unityの機能を使う場合はUnityのメインスレッドに音声データを渡す必要があります。

また音声データは

・インターリーブになっている
・0～1に正規化されている（ASIOSampleTypeに合わせた変換はやらなくていい！ありがたい！）

の点に注意です。インターリーブはこんな処理で直列な音声データに戻せます。

```cs
var samplesPerBuffer = 512;
var ch = _inputSamples.Length / samplesPerBuffer;
var buf = new float[samplesPerBuffer];
for (var i = 0; i < ch; ++i)
{
    for (var j = 0; j < samplesPerBuffer; ++j)
    {
        buf[j] = data[i + ch * j];
    }
    
    // チャンネル毎の処理を行う
}
```

後は好きに加工して処理するなりAudioSourceに書き込むなり自由です。

#最後に
これでオーディオIFとマイクの山で苦労している方のお役に立てば幸いです。