凹みTips

C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

uMicrophoneWebGL の複数チャンネル対応

はじめに

今年はじめに Unity の WebGL ビルドでマイク入力を使えるようにするライブラリを作りました。

tips.hecomi.com

ただ、次の記事にも記載したのですが、どうやら録音時に音声が遅くなってしまう(ピッチが低くなってしまう)という問題があるようです。

tips.hecomi.com

本記事では、こちらの調査と修正を行った結果をまとめたいと思います。

リリース

github.com

問題と対応

ピッチが低くなるのはおそらくマルチチャンネル入力のバッファをモノラルの AudioClip のバッファに入れているからかな、と当たりをつけました。ライブラリ内には 2 箇所、モノラルを前提とした処理を行ってしまっている場所があります。

AudioClip の生成

まずは録音終了時です。AudioClip.Create() での AudioClip 生成時には、引数にチャンネル数を指定する場所があります。現状はここが固定値で 1 チャンネル前提になっていました...。

...
public class MicrophoneRecorder : MonoBehaviour
{
    ...
    public void OnEnd()
    {
        if (!audioSource) return;
        
        var freq = microphoneWebGL.selectedDevice.sampleRate;
        _clip = AudioClip.Create(
            "uMicrophoneWebGL-Recorded",
            _bufferSize + freq,
            1,
            freq,
            false);
        var data = new float[_bufferSize];
        System.Array.Copy(_buffer, data, _bufferSize);
        _clip.SetData(data, 0);
    }
    ...
}

ここで次のように選択したデバイスからチャンネルを取ってくるように修正します。このチャンネルカウントを取ってくる機能はなかったので次の節で追加します。

    ...
    public void OnEnd()
    {
        if (!audioSource) return;
        
        var device = microphoneWebGL.selectedDevice;
        var freq = device.sampleRate;
        var ch = device.channelCount;
        _clip = AudioClip.Create(
            "uMicrophoneWebGL-Recorded",
            _bufferSize + freq,
            ch,
            freq,
            false);
        var data = new float[_bufferSize];
        System.Array.Copy(_buffer, data, _bufferSize);
        _clip.SetData(data, 0);
    }
    ...

チャンネルカウントの取得

WebGL

さて、このチャンネルカウント、JavaScript 側では次のように取れるようです。

...
updateDeviceList: async function() {
    ...
    for (const device of this.devices) {
        ...
        const source = audioContext.createMediaStreamSource(stream);
        const audioTrack = stream.getAudioTracks()[0];
        const settings = audioTrack.getSettings();
        device.sampleRate = settings.sampleRate;
        device.channelCount = settings.channelCount;
        ...
    }
    ...
},
...

developer.mozilla.org

Unity 側

しかし、C# 版では何とチャンネル数を取得する API がないようです。

調べてみましたが、残念ながら Unity の Microphpone クラスでは、チャンネル数を取得する API がないようです。次のように Microphone.Start() を通じて得られる AudioClip 経由で取得できないかな、と思いやってみましたが駄目でした。

public static int GetChannelCount(int index)
{
    var name = GetDeviceId(index);
    if (string.IsNullOrEmpty(name)) return 1;
    
    var freq = GetSampleRate(index);
    var audioClip = Microphone.Start(name, false, 10, freq);
    
    // 実際にマイクがスタートするまで待たないとならない...
    int retryCount = 0;
    while (Microphone.GetPosition(name) <= 0)
    {
        if (++retryCount >= 1000)
        {
            Debug.LogError("Failed to get microphone.");
            return 1;
        }
        System.Threading.Thread.Sleep(1);
    } 
    Microphone.End(name);
    
    return audioClip.channels; // 常に 1 が返ってきてしまう模様
}

次に、固定 1 チャンネルで割り切ることにするかと思い、OnAudioFilterRead(int[] input, int channels) 経由で取得しているバッファを間引いて使おうと次のようなコードを書いてみました。

void OnAudioFilterRead(float[] input, int channels)
{
    lock (_lock)
    {
        int n = input.Length / channels;
        if (_bufferSize + n >= _buffer.Length) return;

        if (channels > 1)
        {
            for (int i = 0; i < n; ++i)
            {
                _buffer[_bufferSize + i] = input[i * channels];
            }
        }
        else
        {
            System.Array.Copy(input, 0, _buffer, _bufferSize, n);
        }

        _bufferSize += n;
    }

    if (!playMicrophoneSound)
    {
        System.Array.Clear(input, 0, input.Length);
    }
}

しかしながら、これだと毎回 channels = 2 となってしまい、逆に Mac の内蔵マイクのようなモノのデバイスでピッチが高くなってしまいました...。しょうがないので残念ながらデバッグ用に Preference から「手動で」数値を入力してもらう形式にしました。

この数字を使って上記の OnAudioFilterRead() でスキップするチャンネル数を指定する、という方式となっています。

その他バグ修正

ついでにバグ修正しました。これまで、WebGL 側で選択したマイクで適切に録音できていませんでした…(デフォルトのマイクが常に指定されてしまっていました)。

おわりに

クラスプラットフォームを考慮しつつ、かつゲームエンジンとしては、マイクの入力チャンネル数はまぁ確かに重要ではないので API がないのは残念ですが理解しました。ワークアラウンドは用意できたのでひとまず良しとします。