LoginSignup
5

More than 3 years have passed since last update.

posted at

updated at

Unityでマイクの音を任意の時間だけファイルに書き出しながら録音する

やりたいこと

  • Unityでマイクからの入力を録音したい。
  • 好きなタイミングで録音を開始して、好きなタイミングで録音を停止したい。
  • wavファイルにしたい。

Unity標準の録音機能だとあらかじめ録音時間を指定した固定長でしか録音できません。途中で録音を止めると残り時間に無音データを詰め込まれたものが出てきます。
普通の用途ならこのやり方で必要な長さのAudioClipを作ってUnityWav1で変換をかければ十分だと思います。

自分の場合は以下の条件が追加されたので一捻りする必要がありました。

  • 録音時間が不明、長時間も予想される
    Unity標準APIでの録音は60分が限界
  • メモリがキツキツ
    音声データをメモリに保持しておけないので随時書き出す必要がある

コードと使い方

コード

gist

MicRecorder.cs
using System;
using System.Collections;
using System.IO;
using UnityEngine;

namespace NekomimiDaimao
{
    ///  https://gist.github.com/nekomimi-daimao/a14301d7008d0a1c7e55977d6d9e2cc1
    public class MicRecorder : MonoBehaviour
    {
        private const int Frequency = 44100;
        private const int MaxLengthSec = 600;

        private const int HeaderLength = 44;
        private const int RescaleFactor = 32767;

        private FileStream _fileStream;
        private AudioClip _audioClip;
        private string _micName = null;

        private Coroutine _recordingCoroutine;

        public bool IsRecording { get; private set; } = false;


        public void StartRecord()
        {
            if (IsRecording || _recordingCoroutine != null)
            {
                return;
            }

            IsRecording = true;
            _recordingCoroutine = StartCoroutine(StartRecordCoroutine());
        }

        /// <summary>
        /// yield return StartCoroutine(MicRecorder.StopRecord());
        /// </summary>
        public IEnumerator StopRecord()
        {
            IsRecording = false;
            yield return _recordingCoroutine;
            _recordingCoroutine = null;
        }


        private IEnumerator StartRecordCoroutine(string defaultPath = null)
        {
            try
            {
                var path = defaultPath ?? $"{Application.temporaryCachePath}/record/{DateTime.Now:MMddHHmmss}.wav";
                _fileStream = new FileStream(path, FileMode.Create);
                const byte emptyByte = new byte();
                for (var count = 0; count < HeaderLength; count++)
                {
                    _fileStream.WriteByte(emptyByte);
                }

                if (Microphone.devices.Length == 0)
                {
                    yield break;
                }

                _micName = Microphone.devices[0];
                _audioClip = Microphone.Start(_micName, true, MaxLengthSec, Frequency);
                var buffer = new float[MaxLengthSec * Frequency];

                var head = 0;
                int pos;
                do
                {
                    pos = Microphone.GetPosition(_micName);
                    if (pos >= 0 && pos != head)
                    {
                        _audioClip.GetData(buffer, 0);
                        var writeBuffer = CreateWriteBuffer(pos, head, buffer);
                        ConvertAndWrite(writeBuffer);
                        head = pos;
                    }

                    yield return null;
                } while (IsRecording);


                pos = Microphone.GetPosition(_micName);
                if (pos >= 0 && pos != head)
                {
                    _audioClip.GetData(buffer, 0);
                    var writeBuffer = CreateWriteBuffer(pos, head, buffer);
                    ConvertAndWrite(writeBuffer);
                }

                Microphone.End(_micName);

                WriteWavHeader(_fileStream, _audioClip.channels, Frequency);
            }
            finally
            {
                _fileStream?.Dispose();
                _fileStream = null;
                AudioClip.Destroy(_audioClip);
                _audioClip = null;
                _micName = null;
            }
        }

        private static float[] CreateWriteBuffer(int pos, int head, float[] buffer)
        {
            float[] writeBuffer;
            if (head < pos)
            {
                writeBuffer = new float[pos - head];
                Array.Copy(buffer, head, writeBuffer, 0, writeBuffer.Length);
            }
            else
            {
                writeBuffer = new float[(buffer.Length - head) + pos];
                Array.Copy(buffer, head, writeBuffer, 0, (buffer.Length - head));
                Array.Copy(buffer, 0, writeBuffer, (buffer.Length - head), pos);
            }

            return writeBuffer;
        }

        private void ConvertAndWrite(float[] dataSource)
        {
            Int16[] intData = new Int16[dataSource.Length];
            var bytesData = new byte[dataSource.Length * 2];
            for (int i = 0; i < dataSource.Length; i++)
            {
                intData[i] = (short) (dataSource[i] * RescaleFactor);
                var byteArr = new byte[2];
                byteArr = BitConverter.GetBytes(intData[i]);
                byteArr.CopyTo(bytesData, i * 2);
            }

            _fileStream.Write(bytesData, 0, bytesData.Length);
        }

        private void WriteWavHeader(FileStream fileStream, int channels, int samplingFrequency)
        {
            var samples = ((int) fileStream.Length - HeaderLength) / 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);
            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();
        }
    }
}

使い方

// 特に依存しているComponentはないです
[SerializeField] private MicRecorder _micRecorder;


// 録音開始。これは別にCoroutineではない
_micRecorder.StartRecord();

// 録音停止するときは処理をぜんぶ終える必要があるため、Coroutineの終了を待機してあげる
yield return StartCoroutine(_micRecorder.StopRecord());

// 録音中フラグだがMicRecorder.StopRecordを実行してから
// 完全に終了するまでの間もfalseなのであんま信用してはいけない
Debug.Log($"Are you listening? {_micRecorder.IsRecording}");

解説と注意事項

探すと理屈を解説した記事は見つかるのですが、何故かコピペしてそのまま使えるやつはなかったので、いろんなところからコピペして切り貼りしました。なんかめっちゃかけ算? してるな? ぐらいの理解度だ!
特にCreateWriteBufferでバッファを使い切って最初からになったときの処理。
(バッファのヘッドから最後まで) + (バッファの最初から現在のポジションまで)
ですがちょっとくらい取りこぼしててもバレへんやろの精神です。Enumerable.Range()とかで配列作れば確認できますがめんどくせえのでしてません。
Frequency44100でしか確認してないので他のサンプリングレートで動くかは未確認。
MaxLengthSec600なので結局10分もメモリを確保してます。状況に合わせて削ってください。理屈の上では10もあれば動くはずです。

セルフレビュー

  • using()使ってない
    微妙に使い方がまだよくわかってないやつ。
    この手のリソースを自動で閉じておいてくれる記法としてはJavatry-with-resourcesがいちばん優れてるような気がするんですが、どこも導入してくれなくてつらい。

  • 成否がわからない
    途中で録音に失敗してもなんらかのゴミファイルは作成されてしまう。なので成否のコールバックが必要だが……。
    StopRecordTaskに置き換えて成否のboolを返すとか。

  • IsRecordingを外部に公開している
    内部処理用のフラグと外部公開用のフラグは分割するべき。

  • MonoBehaviourを継承している
    コルーチンがなければ継承の必要がない。のでできればTaskにしたい。が、UnityのApiはメインスレッドからしか実行できないのでTaskにすると管理がめんどくせえ!

  • メインスレッドでファイルを書き込みしてる
    せめてバッファ変換・ファイル書き込みをワーカースレッドに逃したい。別にそんな処理重くないっぽいけど一応。

こんなコードが上がってきたらコードレビューで撥ねますが、まあ動いてるしいいかなって思いました。

おしまい。

参考

【Unity】長時間のマイク録音を実現する方法
【Unity】AudioListerを録音してwavにする
UnityのMicrophoneで正確な録音時間を取得する方法


  1. なんか適当なパスにファイル保存しちゃうのなんで? 

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
5