【Unity】長時間のマイク録音を実現する方法

11/19/2018

マイクを使って録音してみた。

マイクで採取した音声をWav形式にして保存してみました。
取り組みにあたり、いろいろ調べました。主に2つぐらい方法があります。
①マイクのAudioClipをAudioととつなげて保存したClipを保存する方法。
 MicのAudioClipが最大60分までとなっているため、この方法を考えた? 手軽ですが、メモリで扱っているかぎり長時間の録音は困難です。
②マイクのAudioClipを取得しながらファイルに保存します。
 MicのAudioClipをWav形式に保存します。メモリに保持しないですぐにファイルに保存します。

今回は、2番目の方法を採用します。
理由は
1:動作安定している
2:時間管理が簡単
3:ファイルに保存するのでメモリ管理が簡単
といった感じです。動作が安定するのが一番ですね。残存容量サイズ次第ですが、2時間以上の録音が可能です。
時間管理は、サンプリング周波数と鳴っているデータ位置を取ると1/1000秒で時間計算できます。
ファイルで扱う事でメモリ管理が楽になると思います。BufferやPool数が減らせます。なにより安定します。

以下の機能を実現したいと思います。
・タイトル通りですが、長時間の録音に耐える。(2時間ぐらいは楽勝にする) 
・バックグランド動作(数分~30分)

マイクの準備

まずは、マイクの設定です。

foreach (string device in Microphone.devices)
            {
                //Debug.Log("マイク Name: " + device);
                status.aplStatus.micName = device;
            }

マイクの名前を取得します。そして普通に録音を開始します。

//Buffer
            microphoneBuffer = new float[maxRecordingTime * samplingFrequency];
            //録音開始
            micClip = Microphone.Start(deviceName: micDeviceName, loop: true,
                                       lengthSec: maxRecordingTime, frequency: samplingFrequency);

deviceName:Microphone.devicesで取得した名前をセットします。
loop:Trueでループ録音します。falseはLengthsecになると録音が終了します。
lengthSec:メモリで保持する最大録音時間。秒で指定します。最大60分だったと思います。あまり大きいとGetDataのBufferサイズが大きくなり動作が重たくなります。
iPhone7では、1200(20分)ぐらいが快適ラインだと思います。(frequency 44100のとき)
frequency:サンプリング周波数。1秒あたりのデータ数。44100がCDの音質。落すことでデータ数が減ります。

Wavファイルに変換する

head = 0;
            do
            {
                //ディバイス待ち(レンテシー null)
                position = Microphone.GetPosition(null);

                if (position < 0 || head == position)
                {
                    yield return null;
                }
                else
                {
                    Bufferwite(fileStream,head, position, micClip);
                    head = position;
                }
                yield return null;

            } while (isRecording);

isRecording 発火用のFLGです。録音中はこのループを回ります。
fileStreamは、あとでまとめて説明。
micClipがマイクで取得中のデータ。Microphone.GetPositionで現在の位置を取得して、Positionに。head 前回保存したデータの位置。少ないデータをこまめに保存することにより、フレームレート落ちを防ぎます。
後記していますが、スリープ後の復帰時に大量データを保存する場合、このままでは、データが落ちたり、データが多すぎでフレームが落ちます。そのため、復帰処理をする際にmicClipをスリープ時間に応じて対処(読みだしてしまうORクリア)してから、ループに戻す方が良いとおもいます。

private void Bufferwite(FileStream _fileStream ,int _head,int _position,AudioClip _clip)
        {
            //Bufferに音声データを取り込み
            _clip.GetData(microphoneBuffer, 0);
            Debug.Log("recClipGetData " + microphoneBuffer.Length + " HEAD " + _head + " Position " + _position);
            //音声データをFileに追加
            if (_head < _position)
            {
                for (int i = _head; i < _position; i++)
                {
                    //
                    Byte[] _buffer = BitConverter.GetBytes((short)(microphoneBuffer[i] * rescaleFactor));
                    _fileStream.Write(_buffer, 0, 2);
                }
            }
            else
            {
                for (int i = _head; i < microphoneBuffer.Length; i++)
                {
                    //
                    Byte[] _buffer = BitConverter.GetBytes((short)(microphoneBuffer[i] * rescaleFactor));
                    _fileStream.Write(_buffer, 0, 2);
                }
                for (int i = 0; i < _position; i++)
                {
                    //
                    Byte[] _buffer = BitConverter.GetBytes((short)(microphoneBuffer[i] * rescaleFactor));
                    _fileStream.Write(_buffer, 0, 2);
                }
            }
        }

_clip.GetData(microphoneBuffer, 0);でmicrophoneBufferに音が詰まってくる。microphoneBufferは、ループ利用されているので前回保存したデータ位置と今回のデータ位置までを対象とします。

             Byte[] _buffer = BitConverter.GetBytes((short)(microphoneBuffer[i] * rescaleFactor));
                    	_fileStream.Write(_buffer, 0, 2);

microphoneBuffer[i]は、Flortなので整数に変換しByte化しファイルに保存します。
これを録音を終了するまで行います。

Wavファイルにする

//WavFile準備
            if (!filePath.ToLower().EndsWith(".wav"))
            {
                filePath += ".wav";
            }
            Debug.Log("FilePath" + filePath);

            //ディレクトリ作成
            Directory.CreateDirectory(Path.GetDirectoryName(filePath));
            //Fileストーム
            fileStream = new FileStream(filePath, FileMode.Create);

            //Head領域の事前に確保
            byte headerByte = new byte();
            for (int i = 0; i < HEADER_SIZE; i++) //preparing the header 44バイト
            {
                fileStream.WriteByte(headerByte);
            }

Wavファイルの準備です。一番最初に行います。
事前にHead領域の事前に確保しています。理由は、ファイル長とデータ長を格納しなければならないためです。
ヘッダー詳細は、下記のサイトを参考にしました。

ほぼ、サンプルと同じですが、Flush() Close()を入れてます。入れないと動かなかったですよ。ファイル長とサンプリング数を計算をして保存しています。

private void WavHeaderWrite(FileStream _fileStream,int channels,int samplingFrequency)
        {
            //サンプリング数を計算
            var samples = ((int)_fileStream.Length – HEADER_SIZE)/2;

            //おまじない
            _fileStream.Flush();
            _fileStream.Seek(0, SeekOrigin.Begin);

            Byte[] riff = System.Text.Encoding.UTF8.GetBytes("RIFF");
            _fileStream.Write(riff, 0, 4);
            Byte[] chunkSize = BitConverter.GetBytes(_fileStream.Length – 8);
            _fileStream.Write(chunkSize, 0, 4);
            Byte[] wave = System.Text.Encoding.UTF8.GetBytes("WAVE");
            _fileStream.Write(wave, 0, 4);
            Byte[] fmt = System.Text.Encoding.UTF8.GetBytes("fmt ");
            _fileStream.Write(fmt, 0, 4);
            Byte[] subChunk1 = BitConverter.GetBytes(16);
            _fileStream.Write(subChunk1, 0, 4);
            //UInt16 _two = 2;
            UInt16 _one = 1;
            Byte[] audioFormat = BitConverter.GetBytes(_one);
            _fileStream.Write(audioFormat, 0, 2);
            Byte[] numChannels = BitConverter.GetBytes(channels);
            _fileStream.Write(numChannels, 0, 2);
            Byte[] sampleRate = BitConverter.GetBytes(samplingFrequency);
            _fileStream.Write(sampleRate, 0, 4);
            Byte[] byteRate = BitConverter.GetBytes(samplingFrequency * channels * 2); // sampleRate * bytesPerSample*number of channels, here 44100*2*2
            _fileStream.Write(byteRate, 0, 4);
            UInt16 blockAlign = (ushort)(channels * 2);
            _fileStream.Write(BitConverter.GetBytes(blockAlign), 0, 2);
            UInt16 bps = 16;
            Byte[] bitsPerSample = BitConverter.GetBytes(bps);
            _fileStream.Write(bitsPerSample, 0, 2);
            Byte[] datastring = System.Text.Encoding.UTF8.GetBytes("data");
            _fileStream.Write(datastring, 0, 4);
            Byte[] subChunk2 = BitConverter.GetBytes(samples * channels * 2);
            _fileStream.Write(subChunk2, 0, 4);
            //必ずクローズ
            _fileStream.Flush();
            _fileStream.Close();
        }
    }

iOSでの対応について

#if UNITY_IOS
            //フレームレートを落とす。電池節約のため。
            Application.targetFrameRate = 240;
	        //寝ません
            Screen.sleepTimeout = SleepTimeout.NeverSleep;

            //マイクプライバシー設定
            Screen.sleepTimeout = SleepTimeout.NeverSleep;
            if (Application.HasUserAuthorization(UserAuthorization.Microphone) == false)
            {
                yield return Application.RequestUserAuthorization(UserAuthorization.Microphone);
                if (Application.HasUserAuthorization(UserAuthorization.Microphone) == false)
                {
                    //終了
                    SetCurrentState(APLState.END);
                    ExitCoroutin("StartAction");
                }
                else
                {
                    yield return new WaitForSeconds(1.0f);
                }
            }
            //ファイルパスの設定
            status.aplStatus.wavFilePath = Application.persistentDataPath + "/wavdata";
            Directory.CreateDirectory(status.aplStatus.wavFilePath);
            UnityEngine.iOS.Device.SetNoBackupFlag(status.aplStatus.wavFilePath);
#endif

ここは、メイン処理の準備段階で実行しています。最後のソースにはない部分です。メイン処理は、UI制御とステータス制御で山盛りなので説明を割愛します。
たぶん長時間放置するようなアプリだと思います。電池節約のため、フレームレートを落としましょう。
指定しない場合600です。FPS60
プライバシー設定を促して、ユーザに設定してもらいます。実機場合のみ、利用許可を求めるダイアログが出ます。
また、保存先は、persistentDataPathです。審査のために、バックアップ対象から外します。
自動スリープしないように設定しています。

スリープ中の録音(ios)

録音中にスリープしてしまった場合、難しいことせずとも多少なら処理を継続することができます。
ただし制限があります。上記にも書きましたが、マイクの最大録音時間までになります。それ以内に復帰できなかった場合は、マイクのClipがループしてしまい、失われる時間あるけど処理は継続できます。言い換えると、継続できるけどループで上書きされた部分だけが消えます。また、ループ上書きが発生すると時間関係がおかしくなります。スリープから復帰した際に、スリープしていた時間を計算してループ上書きが発生していた場合は録音を中断する必要があります。まぁ、制限だらけですww。
本格化な対処方法は、プラグインを作成してMicClipにたまったデータを処理すればいいと思うのですが・・・ためしてません。
中途半端な対応なので厳密に録音するような場合はちゃんと実装しましょう。

今回は、暫定的ですけどスリープの設定を説明します。

まず、[PlayerSettings][OtherSetings][Behavior in Background]をCustomにし、Audio、AirPlay,PiPをチェックする。

あとは、Xcode側になります。

info.plistに上記のように設定を入れます。丸じるしあたりに+が出ますので、押下して「Privacy – Microphone Usage Description」を選択します。

次にtabを切り替え下記のようにします。

Capabilities の Background Modesにて Audio,AirPlay、PinP を設定しONにします。
上記設定にて、バックグランドで動作するようになります。

さいごに

残念ながら、全ソース大量なので載せられませんが、肝の部分を記載します。
疑問・質問があればメールやTwitterにて連絡お願いします。訂正および追記いたします。

using System;
using System.IO;
using UnityEngine;
using System.Collections.Generic;
using System.Collections;

namespace Section31Develop
{
    public class WavFileManager : SingletonMonoBehaviour<WavFileManager>
    {

        private AudioClip micClip;
        private float[] microphoneBuffer;
        private FileStream fileStream;
        private int head;
        private int position;
        public bool isRecording;
        //public int  timeLine;
        public StatusManager status;

        const int HEADER_SIZE = 44;
        const float rescaleFactor = 32767; //to convert float to Int16

        override protected void Awake()
        {
            //必ずbase.Awake()必要
            base.Awake();
            //初期化
            status = StatusManager.Instance;
        }
        //

        public IEnumerator WavRecording(string micDeviceName, int maxRecordingTime, int samplingFrequency, string filePath)
        {
            Debug.Log("WavFileManager Recording start");
            //Recording開始
            isRecording = true;
            //WavFile準備
            if (!filePath.ToLower().EndsWith(".wav"))
            {
                filePath += ".wav";
            }
            Debug.Log("FilePath" + filePath);

            //ディレクトリ作成
            Directory.CreateDirectory(Path.GetDirectoryName(filePath));
            //Fileストーム
            fileStream = new FileStream(filePath, FileMode.Create);

            //Head領域の事前に確保
            byte headerByte = new byte();
            for (int i = 0; i < HEADER_SIZE; i++) //preparing the header 44バイト
            {
                fileStream.WriteByte(headerByte);
            }

            //Buffer
            microphoneBuffer = new float[maxRecordingTime * samplingFrequency];
            //録音開始
            micClip = Microphone.Start(deviceName: micDeviceName, loop: false,
                                       lengthSec: maxRecordingTime, frequency: samplingFrequency);
            //初期位置
            status.clipStatus.timeLine = 0;
            head = 0;
            do
            {
                //ディバイス待ち(レンテシー null)
                position = Microphone.GetPosition(null);

                if (position < 0 || head == position)
                {
                    yield return null;
                }
                else
                {
                    WavBufferwite(fileStream,head, position, micClip);
                    head = position;
                }
                yield return null;

            } while (isRecording);

            //マイク録音停止
            position = Microphone.GetPosition(null);
            Microphone.End(micDeviceName);
            //Bufferをファイル書き込みしファイナライズ
            WavBufferwite(fileStream,head, position, micClip);
            WavHeaderWrite(fileStream, micClip.channels, samplingFrequency);//サイズが決まってから
            Debug.Log("WavFileManager Recording end");
        }

        private void WavBufferwite(FileStream _fileStream, int _head, int _position, AudioClip _clip)
        {
            //Bufferに音声データを取り込み
            _clip.GetData(microphoneBuffer, 0);
            Debug.Log("recClipGetData " + microphoneBuffer.Length + " HEAD " + _head + " Position " + _position);
            //音声データをFileに追加
            if (_head < _position)
            {
                for (int i = _head; i < _position; i++)
                {
                    //
                    Byte[] _buffer = BitConverter.GetBytes((short)(microphoneBuffer[i] * rescaleFactor));
                    _fileStream.Write(_buffer, 0, 2);
                    status.clipStatus.timeLine++;
                }
            }
            else
            {
                for (int i = _head; i < microphoneBuffer.Length; i++)
                {
                    //
                    Byte[] _buffer = BitConverter.GetBytes((short)(microphoneBuffer[i] * rescaleFactor));
                    _fileStream.Write(_buffer, 0, 2);
                    status.clipStatus.timeLine++;
                }
                for (int i = 0; i < _position; i++)
                {
                    //
                    Byte[] _buffer = BitConverter.GetBytes((short)(microphoneBuffer[i] * rescaleFactor));
                    _fileStream.Write(_buffer, 0, 2);
                    status.clipStatus.timeLine++;
                }
            }

        }
        public void RecordingStop()
        {
            if (isRecording == true)
            {
                isRecording = false;
            }
        }

        //
        private void WavHeaderWrite(FileStream _fileStream,int channels,int samplingFrequency)
        {
            //サンプリング数を計算
            var samples = ((int)_fileStream.Length – HEADER_SIZE)/2;

            //おまじない
            _fileStream.Flush();
            _fileStream.Seek(0, SeekOrigin.Begin);

            Byte[] riff = System.Text.Encoding.UTF8.GetBytes("RIFF");
            _fileStream.Write(riff, 0, 4);

            Byte[] chunkSize = BitConverter.GetBytes(_fileStream.Length – 8);
            _fileStream.Write(chunkSize, 0, 4);

            Byte[] wave = System.Text.Encoding.UTF8.GetBytes("WAVE");
            _fileStream.Write(wave, 0, 4);

            Byte[] fmt = System.Text.Encoding.UTF8.GetBytes("fmt ");
            _fileStream.Write(fmt, 0, 4);

            Byte[] subChunk1 = BitConverter.GetBytes(16);
            _fileStream.Write(subChunk1, 0, 4);

            //UInt16 _two = 2;
            UInt16 _one = 1;

            Byte[] audioFormat = BitConverter.GetBytes(_one);
            _fileStream.Write(audioFormat, 0, 2);

            Byte[] numChannels = BitConverter.GetBytes(channels);
            _fileStream.Write(numChannels, 0, 2);

            Byte[] sampleRate = BitConverter.GetBytes(samplingFrequency);
            _fileStream.Write(sampleRate, 0, 4);

            Byte[] byteRate = BitConverter.GetBytes(samplingFrequency * channels * 2); // sampleRate * bytesPerSample*number of channels, here 44100*2*2
            _fileStream.Write(byteRate, 0, 4);

            UInt16 blockAlign = (ushort)(channels * 2);
            _fileStream.Write(BitConverter.GetBytes(blockAlign), 0, 2);

            UInt16 bps = 16;
            Byte[] bitsPerSample = BitConverter.GetBytes(bps);
            _fileStream.Write(bitsPerSample, 0, 2);

            Byte[] datastring = System.Text.Encoding.UTF8.GetBytes("data");
            _fileStream.Write(datastring, 0, 4);

            Byte[] subChunk2 = BitConverter.GetBytes(samples * channels * 2);
            _fileStream.Write(subChunk2, 0, 4);

            //必ずクローズ
            _fileStream.Flush();
            _fileStream.Close();
        }
    }
}

スポンサーリンク