Unity上で外部デバイスとの同期処理を行うための考察(デバイスセットアップ編)

はじめに

エンジニアの松原です。ゲームや業務用アプリケーション開発以外にもライブ配信システムにUnityを利用するケースがあります。以前実験調査の際に、デバイス間での時間同期を取る方法が課題に挙がりました。
デバイス間の同期ズレをできる限り無くしたい、または同期ズレが起きた場合でも後で修正する仕組み(またはマスタリング)として時間の尺度としてタイムコード(SMPTE timecode)を利用するアイデアがあります。 また、映像機器ではGenlockという、同期処理信号の入出力により、フレーム単位での同期処理を行う仕組みがあります。 Unityで同期処理に関する情報について数回の技術関連の記事としてまとめていきたいと思います。
今回はタイムコードの簡単な説明と、タイムコードを生成するための環境、また、Unity上で音声データとして取り込むところまで(タイムコードへの変換については次回)扱いたいと思います。

非互換デバイス間での同期処理の概要

デバイス間での同期処理では、お互いのシステム環境で似たようなハードウェアを持っているか、または互換性を担保するためのソフトウェアがあることが前提になっている場合があります。
その環境であれば、同じプロトコルやデータのフォーマットを利用でき、送受信データに同期処理に関する内容を含めやすいので、ソフトウェア側の工夫によって同期処理が解決するケースが多いと思います。

今回の記事ではデバイス間で扱っているデータが異なっており、ハードソフト両面で互換性が無い場合に同期処理をどうするか、という課題解決の一つにタイムコードを利用したく、タイムコードをUnity上で扱う際の考察を数回の記事に分割して紹介していく予定です。

タイムコードを使うメリットについて

タイムコードそのものについて説明すると記事がすごく長くなるため、詳しい話はWikipediaのSMPTEタイムコードの項目をお読みください。

ja.wikipedia.org

タイムコードを扱うメリットですが、音声(マイク入力)を扱う場合に大きな恩恵を受けることができます。タイムコードは音声信号(LTC信号)として入出力できるので、音声出力のチャンネルに混ぜることにより、どの時間軸での音声データであるかという紐づけが簡単にできます。ビデオカメラがタイムコード同期に対応していれば、映像の同期処理も簡単に行えるようになります。
さらにPC側でタイムコードが読み取れれば、処理単位にタイムコードとの紐づけが行えるようになります。今回目指しているのは、Unityの動作環境(PC)での時間をタイムコードをベースに合わせる方法について考えていきます。

また、UnrealEngine(5.0)ではタイムコードを扱う内容を取り上げた説明ページも用意されており、映像・音声とゲーム画面との同期を意識していそうです。(以下のページは2022年4月時点での仮ページのようなので、将来消えている可能性があります)

docs.unrealengine.com

音声映像の機器では、タイムコード以外にも、Genlockという専用の同期信号を受信する方法もあります。(Genlockは今回扱わないので説明は省略します)

タイムコード生成器とテストのデバイス構成について

タイムコード生成器はタイムコードのLTC信号を発生するデバイスで、業務用のものからホビーユースに近いものまで販売されています。
今回はスマホからお手軽にタイムコードの信号を生成してくれる、「TimeCode Generator」を利用しました。(※有料アプリ)

timecodesync.com

上記のアプリを使ってタイムコードのテストを行う場合、3.5mmオーディオジャックを使ってスマホのイヤフォン出力をPCの音声入力につなげるだけのシンプルな構成になります。

Unity上でのマイク音声読み取りについて

マイク入力の音声をUnity上でデータ配列として取得するようにします。

Unityでは Microphone クラスから AudioClip が取得でき、この AudioClip を設定した AudioSource をGameObjectコンポーネントに加えることでマイクの音声を再生することができます。

docs.unity3d.com

また、AudioSource をコンポーネントとしてGameObjectに登録しており、なおかつ AudioSource が再生状態の場合、OnAudioFilterRead() の関数の引数から float[] 型の配列として音声信号のデータを取得できます。
以下にサンプルコードを記載します。

#nullable enable

using System;
using UnityEngine;
using System.Linq;

public class MicrophoneGrabber : MonoBehaviour
{
    [SerializeField]
    private string micFilterName = string.Empty;
    
    [SerializeField]
    private int bufferSeconds = 1;

    private void Start()
    {
        var deviceName = GetDeviceName(micFilterName);
        if (deviceName == null)
        {
            Debug.LogWarning($"Audio Device (keywords: {micFilterName}) was not found.");
            return;
        }
        var clip = Microphone.Start(deviceName, true, bufferSeconds, 44100);
        var source = gameObject.AddComponent<AudioSource>();
        source.clip = clip;
        source.loop = true;
        while (Microphone.GetPosition(deviceName) < 0) { }
        source.Play();
    }

    private void OnAudioFilterRead(float[] data, int channels) {
        // ここにTimecode取得の処理を記述する
        
        // そのままだとTimecodeの音声が鳴り響くので、ゲインを0にする(以下のコードでは音を小さくしている)
        for (int i = 0; i < data.Length; i++)
        {
            data[i] *= 0.005f;
        }
    }

    private static string? GetDeviceName(string filterName)
    {
        return Microphone.devices.FirstOrDefault(device => device.Contains(filterName));
    }
}

このスクリプトを適当なGameObjectに張り付け、再生することで、スマホから出力したタイムコードの信号を音声データとしてUnity側で受け取れるようになります。 また、デバイス名を指定することで、デフォルトに指定していないデバイスも扱えます。上記のサンプルコードでは、デバイス名の一部からデバイス名を取得する実装になっています。

OnAudioFilterRead()以外にも、AudioClipから直接音声信号のデータを取り出すこともできます。 詳しいやり方については、「OnAudioFilterRead」や「AudioClip.GetData」などでGoogle検索すれば出てくると思います。

Unity上でのタイムコード読み取りについて

タイムコード読み取り方法については次回紹介したいと思います。

まとめ

この記事ではUnityでタイムコードを扱う経緯について、またその利便性について簡単に紹介しました。 次回は実際にタイムコードを読み取るところまでをカバーしていこうと考えています。 また、音声取り込み時の遅延に関してですが、ASIOドライバを利用することでUnityで再生する遅延時間を減らせることができるはずなので、こちらもいつか取り組んでみたいと思います。