Unityでのマイク録音３種盛り：レイテンシ比較

# Unityでマイク録音する方法
Unityでマイク録音する方法には、自分が把握しているだけで以下の3種類ある。

- Microphone.StartとOnAudioFilterReadの組み合わせ
- Microphone.StartとAudioClip.GetDataの組み合わせ
- Native PluginによりOS機能を利用する

これらの方法でどれだけレイテンシに差があるかをおおまかに計測してみた。
以下でそれぞれの方法を解説する。

注：下記コードは擬似コードを含むので、そのままではコンパイルできない。「録音」メソッドは各自で置き換えてほしい。各サンプルコードは自由に使用してもらってかまわない。

## Microphone.StartとOnAudioFilterReadの組み合わせ
Microphone.Startで作成したAudioClipをAudioSourceで再生し、その再生音をOnAudioFilterReadで受け取って録音する。

最も回りくどい（コード自体はシンプル）が、OVRLipSyncで採用されており、いろんなサンプルで出回っている方法である。

```OnAudioFilterReadSample.cs
using UnityEngine;

public class OnAudioFilterReadSample : MonoBehaviour {
    void Start() {
        var source = gameObject.AddComponent<AudioSource>();
        var clip = Microphone.Start(null, true, 1, 44100);
        source.clip = clip;
        source.loop = true;
        while (Microphone.GetPosition(null) < 0) { }

        source.Play();
    }

    void OnAudioFilterRead(float[] data, int channels) {
        // 擬似コード
        録音(data);

        // 出力をミュートする
        for (int i = 0; i < data.Length; i++) {
            data[i] = 0;
        }
    }
}
```

## Microphone.StartとAudioClip.GetDataの組み合わせ
Microphone.Startで作成したAudioClipから音声データを取得する。

今回の記事で一押しの方法。

```MicrophoneAudioClipSample.cs
using System;
using UnityEngine;

public class MicrophoneAudioClipSample : MonoBehaviour {
    AudioClip clip;
    int head = 0;
    const int samplingFrequency = 44100;
    const int lengthSeconds = 1;
    float[] processBuffer = new float[256];
    float[] microphoneBuffer = new float[lengthSeconds * samplingFrequency];

    void Start() {
        clip = Microphone.Start(null, true, 1, samplingFrequency);
        while (Microphone.GetPosition(null) < 0) { }
    }

    void Update() {
        var position = Microphone.GetPosition(null);
        if (position < 0 || head == position) {
            return;
        }

        clip.GetData(microphoneBuffer, 0);
        while (GetDataLength(microphoneBuffer.Length, head, position) > processBuffer.Length) {
            var remain = microphoneBuffer.Length - head;
            if (remain < processBuffer.Length) {
                Array.Copy(microphoneBuffer, head, processBuffer, 0, remain);
                Array.Copy(microphoneBuffer, 0, processBuffer, remain, processBuffer.Length - remain);
            } else {
                Array.Copy(microphoneBuffer, head, processBuffer, 0, processBuffer.Length);
            }

            // 擬似コード
            録音(processBuffer);

            head += processBuffer.Length;
            if (head > microphoneBuffer.Length) {
                head -= microphoneBuffer.Length;
            }
        }
    }

    static int GetDataLength(int bufferLength, int head, int tail) {
        if (head < tail) {
            return tail - head;
        } else {
            return bufferLength - head + tail;
        }
    }
}
```

## Native PluginによりOS機能を利用する

Native Pluginを自力で書いてOSの機能を利用するプラットフォーム依存な方法。
今回はWindows向けにWASAPI録音プラグインを作成して検証した。

# レイテンシ比較
## 結果
最もレイテンシが少ないNative Plugin (WASAPI) による手法を基準に、相対的なレイテンシを計測した。
結果は以下のとおり。
※Unity2019.1.2f1では状況が変わっているようです。OnAudioFilterReadでもレイテンシが改善しているもようです。

| 手法 | 相対レイテンシ |
|:-----------------|------------------|
|WASAPI|0ms(基準)|
|AudioClip.GetData|約40ms|
|OnAudioFilterRead|約60ms～200ms|

## 計測方法
44100Hzの256サンプル間のRMS値がある閾値を超える時刻を計測した。
Unity AudioのバッファはBest Latencyに設定した。
Unityのバージョンは 2018.1.0b11 を用いた。
オーディオインターフェースは AG03 MIKU を使用した。
エディタ上で測定し、何度かPlayしなおしてだいたいの値の範囲を記録した。

# 考察
リップシンクやボイスチャット、音声解析、可視化に用いる程度であれば、AudioClip.GetDataを使用する方法がよい。実装の手間が少なく、（今回はWindows以外で検証していないが）マルチプラットフォームに対応している。
音声にUnity付属のエフェクトをかけたものを録音したい場合はAudioClip.GetDataの手法ではできない。ただし、OnAudioFilterReadの手法はループバックするとノイズが乗ったりと、安定版でも動作が不安定なようだ。今回も、何度かPlayを繰り返すうちにレイテンシが大きく変動している。
※Unity2019.1.2f1ではOnAudioFilterReadの方法でもノイズが乗らないように改善されているようです。また、レイテンシのばらつきも改善されているようです。
最速を目指したい場合はOS依存の処理を自力で書くほうがよい。ただし、ループバック再生はWASAPI排他モードでも数msの遅延があり、かなり音量を小さくしないとしゃべりにくくなってしまう。ループバックでモニタリングしたい場合は、ハードウェアの機能を使うほうがよい。

質問などあれば Twitter [@tyounanmoti](https://twitter.com/tyounanmoti) で受け付けているので、お気軽に！