【Unity】NatCorderで動画をオフラインレコーディングする

UnityのアセットであるNatCorderで動画をオフラインレコーディングする方法についてまとめました。

Unity2019.4.0

NatCorder?

NatCorderはUnityでmp4などの動画ファイルを作成するための有料アセットです。

assetstore.unity.com

基本的な使い方については以下の記事にまとめていますので必要に応じて参照してください。

light11.hatenadiary.com

映像だけオフラインレコーディングする

映像だけオフラインレコーディングする方法については上記の記事にまとめています。
コードだけ抜粋すると以下のようになります。

using System.Linq;
using NatSuite.Recorders;
using NatSuite.Recorders.Clocks;
using UnityEngine;
using System.Threading.Tasks;

public class Example : MonoBehaviour
{
    public async void Start()
    {
        const int width = 128;
        const int height = 128;
        const int frameRate = 30;
        
        var clock = new FixedIntervalClock(frameRate);
        var recorder = new MP4Recorder(width, height, frameRate);
        
        var colors = Enumerable.Range(0, width * height)
            .Select(_ => (Color32) Color.magenta)
            .ToArray();
        var frames = Enumerable.Range(0, 100)
            .Select(x =>
            {
                var texture = new Texture2D(width, height, TextureFormat.RGBA32, false, false);
                texture.SetPixels32(colors);
                return texture.GetPixels32();
            })
            .ToArray();

        var path = await Task.Run(() =>
        {
            foreach (var frame in frames)
            {
                recorder.CommitFrame(frame, clock.timestamp);
            }
            return recorder.FinishWriting();
        });
        Debug.Log(path);
    }
}

音声だけオフラインレコーディングする

次に音声だけオフラインレコーディングする方法を紹介します。
以下に例としてWAVRecorderで音声をレコーディングするソースコードを掲載します。

using System.Linq;
using NatSuite.Recorders;
using UnityEngine;

public class Example : MonoBehaviour
{
    [SerializeField] private AudioClip _clip;

    private WAVRecorder _recorder;
    
    private void Start()
    {
        // AudioClipの周波数とチャンネル数でWAVRecorderを初期化する
        _recorder = new WAVRecorder(_clip.frequency, _clip.channels);
        
        // AudioClipからデータを取得
        var samples = new float[_clip.samples * _clip.channels];
        _clip.GetData(samples, 0);
        
        // レコーダーに全てのデータをコミットする
        var samplesList = samples.ToList();
        while (true)
        {
            // 適当な数ごとにコミットしていく
            var count = Mathf.Min(samplesList.Count, 10000);
            var d = samplesList.GetRange(0, count).ToArray();
            samplesList.RemoveRange(0, count);
            
            _recorder.CommitSamples(d);
            
            if (samplesList.Count == 0)
            {
                break;
            }
        }
    }

    private void OnDestroy()
    {
        Debug.Log(_recorder.FinishWriting().Result);
    }
}

WAVRecorderを使う場合には、WAVRecorderのコンストラクタで周波数とチャンネルを登録するため、
あとはAudioClipのデータを適当にコミットしていくだけでOKです。
おそらく内部の仕様的に一回でコミットしてしまうとうまくいきませんでしたが、2回くらいに分割するのが一番効率良いかもしれません。

動画と音声をオフラインレコーディングする

さて最後に動画と音声を同時にオフラインレコーディングする方法についてまとめます。
まず準備としてAudioClipのデータをフレーム単位で取得できるクラスを作り、その後実際にこれを使ってレコーディングします。

AudioClipのデータをフレーム単位で取得できるクラスを作る

それではまずAudioClipのデータをフレーム単位で取得できるクラスを作っていきます。

using System;
using UnityEngine;

public class AudioClipData
{
    private readonly AudioClip _clip;
    
    private float[] _samples;
    private float[] Samples => _samples ?? (_samples = FetchSamples(_clip));

    public AudioClipData(AudioClip clip)
    {
        _clip = clip;
    }

    /// <summary>
    /// データを取得する
    /// </summary>
    /// <param name="startFrame">取得開始するフレーム</param>
    /// <param name="frameCount">フレーム数</param>
    /// <param name="fps">FPS</param>
    /// <returns></returns>
    public float[] Get(int startFrame, int frameCount, int fps)
    {
        var samplesPerFrame = _clip.frequency / fps * _clip.channels;
        return Get(samplesPerFrame * startFrame, samplesPerFrame * frameCount);
    }
        
    private float[] Get(int startIndex, int length)
    {
        var result = new float[length];
        if (Samples.Length - startIndex < length)
        {
            length = Samples.Length - startIndex;
        }

        if (length >= 0)
        {
            Array.Copy(Samples, startIndex, result, 0, length);
        }

        return result;
    }

    private static float[] FetchSamples(AudioClip clip)
    {
        var samples = new float[clip.samples * clip.channels];
        clip.GetData(samples, 0);
        return samples;
    }
}

このコードを読むにはオーディオデータの構造についての前提知識が必要です。
これについては以下でまとめていますので必要に応じて参照してください。

light11.hatenadiary.com

具体的な内容としては、AudioClipの周波数とチャンネル数、またFPSから1フレーム当たりのサンプル数を求め、
AudioClip.GetData()で取得したデータをフレーム指定で取得できるようにしています。

オフラインレコーディングする

次にこのクラスを使って実際にオフラインレコーディングを行います。

using System.Linq;
using NatSuite.Recorders;
using NatSuite.Recorders.Clocks;
using UnityEngine;

public class Example : MonoBehaviour
{
    [SerializeField] private AudioClip _clip;
    private IMediaRecorder _recorder;
    
    public void Start()
    {
        const int width = 128;
        const int height = 128;
        const int frameRate = 30;
        var sampleRate = AudioSettings.outputSampleRate;
        var channelCount = (int)AudioSettings.speakerMode;
        
        // FixedIntervalClockをFrameRateを指定して生成
        var clock = new FixedIntervalClock(frameRate);

        // Recorderを生成
        // ここのsampleRateとchannelCountはAudioSettingsのものを使用する
        _recorder = new MP4Recorder(width, height, frameRate, sampleRate, channelCount);
        
        // フレームごとのテクスチャの配列を用意しておく
        var colors = Enumerable.Range(0, width * height)
            .Select(_ => (Color32) Color.magenta)
            .ToArray();
        
        var frames = Enumerable.Range(0, 100)
            .Select(x =>
            {
                var texture = new Texture2D(width, height, TextureFormat.RGBA32, false, false);
                texture.SetPixels32(colors);
                // この時点でGetPixels32()を呼んでおく
                return texture.GetPixels32();
            })
            .ToArray();

        var audioClipData = new AudioClipData(_clip);

        var currentFrame = 0;
        foreach (var frame in frames)
        {
            var timestamp = clock.timestamp;
            _recorder.CommitFrame(frame, timestamp);
            var data = audioClipData.Get(currentFrame, 1, frameRate);
            _recorder.CommitSamples(data, timestamp);
            currentFrame++;
        }
    }

    private void OnDestroy()
    {
        Debug.Log(_recorder.FinishWriting().Result);
    }
}

詳細な説明はコメントに記述しているので割愛します。
WAVRecorderと違いMP4Recorderの引数のサンプルレートはAudioSettingsのものを使わないとうまくいかなかった(Unityがクラッシュした)のでその点ご注意ください。

関連

light11.hatenadiary.com

light11.hatenadiary.com