UniTaskを用いたマイクの音量取得とwavのレコーディング

# 目的
UniTaskを使ってマイクの音量の取得と一定の音量以上で wavのRecordingをしたかった。

## コード

```MicChecker.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Linq;
using System.Threading;
using Cysharp.Threading.Tasks;

public class MicChecker
{
    float m_gain = 1f; // 音量に掛ける倍率
    
    float m_threshold;
    
    public float m_volumeRate; // 音量(0-1)

    AudioSource MICAudioSource;

    private int MOVING_AVE_SAMPLE;

    public bool isUserSpeaking;

    private float spkeakingTime;

    private int REC_SEC;
    

    public MicChecker(AudioSource audioSource, CancellationToken cancellationToken, int recSec, float gain = 1f, float threshold = 0.05f)
    {
        MICAudioSource = audioSource;
        isUserSpeaking = false;
        m_gain = gain;
        m_threshold = threshold;
        REC_SEC = recSec;
        MicSetup();
        UpdateLoop(cancellationToken);

    }
    // Use this for initialization

    void MicSetup()
    {
        if ((MICAudioSource != null) && (Microphone.devices.Length > 0)) // オーディオソースとマイクがある
        {
            foreach (var device in Microphone.devices)
            {
                Debug.Log(device);
            }

            string devName = Microphone.devices[0]; // 複数見つかってもとりあえず0番目のマイクを使用
            int minFreq, maxFreq;
            Microphone.GetDeviceCaps(devName, out minFreq, out maxFreq); // 最大最小サンプリング数を得る
            // MOVING_AVE_SAMPLE = minFreq; なぜか minFreqが44100だったので変更
            MOVING_AVE_SAMPLE = 512;
            MICAudioSource.clip = Microphone.Start(devName, true, REC_SEC, minFreq); // 音の大きさを取るだけなので最小サンプリングで十分
            MICAudioSource.loop = true;
            MICAudioSource.Play(); //マイクをオーディオソースとして実行(Play)開始
        }
    }

    // Update is called once per frame
    async UniTaskVoid UpdateLoop(CancellationToken cancellationToken)
    {
        while (true)
        {
            GetVolume();
            spkeakingTime += Time.deltaTime;
            if (isUserSpeaking && spkeakingTime < 2.0f)
            {
                await UniTask.Yield(PlayerLoopTiming.Update, cancellationToken: cancellationToken);
                continue;
            }
            
            if (isUserSpeaking)
            {
                
                isUserSpeaking = false;
                Debug.Log("End Speaking!");
                await UniTask.Yield(PlayerLoopTiming.Update, cancellationToken: cancellationToken);
                continue;
            }
            
            if (m_volumeRate > m_threshold && !isUserSpeaking)
            {
                //初の発音かチェック
                //初の発音から0.8s以内かをチェック
                //
                spkeakingTime = 0.0f;
                isUserSpeaking = true;
                Debug.Log("Start Speaking!");
                await UniTask.Yield(PlayerLoopTiming.Update, cancellationToken: cancellationToken);
                continue;
            }
            
            await UniTask.Yield(PlayerLoopTiming.Update, cancellationToken: cancellationToken);
        }
    }

    void GetVolume()
    {
        float[] waveData = new float[MOVING_AVE_SAMPLE];
        MICAudioSource.GetOutputData(waveData, 0);

        //バッファ内の平均振幅を取得（絶対値を平均する）
        m_volumeRate = waveData.Average(Mathf.Abs) * m_gain;
    }
}


```


```WavRecording.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;

public class WavRecording
{
    private AudioSource audio;

    private int head;

    public bool isRecording;

    private string recFileName;

    private int HEADER_SIZE = 44;

    private int REC_SEC;
    
    private float RESCALE_FACTOR = 32767;

    private string wavName;
    
    public WavRecording(string _wavName, AudioSource _audio, int _recSec, CancellationToken cancellationToken)
    {
        REC_SEC = _recSec;
        audio = _audio;
        wavName = _wavName;
        Listen(cancellationToken);
    }

    public void StartRecord()
    {
        if (isRecording)
        {
            return;
        }
     
        Debug.Log("Start Recording!");
        isRecording = true;
 
        recFileName = Path.Combine(Application.dataPath, "Resources", wavName + ".wav");
        using (var fileStream = new FileStream(recFileName, FileMode.Create))
        {
            byte[] headerSpace = new byte[HEADER_SIZE];
            fileStream.Write(headerSpace, 0, headerSpace.Length);
        }
    }

    public void EndRecord()
    {
        if (!isRecording)
        {
            return;
        }
        isRecording = false;
        Debug.Log("End Recording!");
        using (var fileStream = new FileStream(recFileName, FileMode.Open))
        {
            WavHeaderWrite(fileStream, audio.clip.channels, audio.clip.frequency);
        }
    }
    
    public async UniTask Listen(CancellationToken cancellationToken)
    {
        while (true)
        {
            int position = Microphone.GetPosition(null);
 
            //MICが取得できていなかったときは1秒待つ。
            if (position < 0 || position == head)
            {
                await UniTask.Delay(1000, cancellationToken: cancellationToken);
                continue;;
            }
 
            float[] tmp = new float[audio.clip.samples];
            audio.clip.GetData(tmp, 0);
            
            if (isRecording)
            {
                List<float> audioData = new List<float>();
 
                if (head < position)
                {
                    for (int i = head; i < position; i++)
                    {
                        audioData.Add(tmp[i]);
                    }
                }
                else
                {
                    for (int i = head; i < tmp.Length; i++)
                    {
                        audioData.Add(tmp[i]);
                    }
                    for (int i = 0; i < position; i++)
                    {
                        audioData.Add(tmp[i]);
                    }
                }
                using (var fileStream = new FileStream(recFileName, FileMode.Append))
                {
                    WavBufferWrite(fileStream, audioData);
                }
            }
            head = position;

            await UniTask.Delay(1000, cancellationToken: cancellationToken);
        }
    }
     private void WavBufferWrite(FileStream fileStream, List<float> dataList)
    {
        foreach (float data in dataList)
        {
            Byte[] buffer = BitConverter.GetBytes((short)(data * RESCALE_FACTOR));
            fileStream.Write(buffer, 0, 2);
        }
        fileStream.Flush();
    }
 
    private void WavHeaderWrite(FileStream fileStream, int channels, int samplingFrequency)
    {
        //サンプリング数を計算
        var samples = ((int)fileStream.Length - HEADER_SIZE) / 2;
 
        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 _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();
    }
    
}


```

コツとしては UpdateLoop内で while文を使っているときに、return ではなく 
```snipet.cs
await UniTask.Yield(PlayerLoopTiming.Update, cancellationToken: cancellationToken);
continue;
```

を使うことでUniTaskで同様のことを実現しています。

## 使い方

```AudioManager.cs
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using UnityEngine;
using Cysharp.Threading.Tasks;

public class AudioManager : MonoBehaviour
{
    private MicChecker micChecker;

    private WavRecording wavRecording;

    [SerializeField] private AudioSource _audioSource;

    private int REC_SEC = 2;
    
    // Start is called before the first frame update
    async void Start()
    {
        micChecker = new MicChecker(_audioSource, recSec: REC_SEC, threshold: 0.1f, cancellationToken: this.GetCancellationTokenOnDestroy());
        wavRecording = new WavRecording("test", _audioSource, REC_SEC, cancellationToken: this.GetCancellationTokenOnDestroy());
        await UpdateLoop(cancellationToken: this.GetCancellationTokenOnDestroy());
    }
    

    async UniTask UpdateLoop(CancellationToken cancellationToken)
    {
        while (true)
        {
            if (micChecker.isUserSpeaking && !wavRecording.isRecording)
            {
                wavRecording.StartRecord();
                await UniTask.Delay(200, cancellationToken:cancellationToken);
                continue;
            }

            if (!micChecker.isUserSpeaking && wavRecording.isRecording)
            {
                wavRecording.EndRecord();
                await UniTask.Delay(200, cancellationToken:cancellationToken);
                continue;
            }
            await UniTask.Yield(PlayerLoopTiming.PostLateUpdate, cancellationToken: cancellationToken);
        }
    }
}


```

初期化して CancellationTokenを渡すことで実行しています。

"test"がwavファイルの名前です。デフォルトでは Assets/Resourcesフォルダーの直下に保存されます。

thresholdの値を変更することで録音を開始する音量の閾値を変えられます。

以上。


### 参考

https://qiita.com/toRisouP/items/4445b6b9bf00e49eb147#%E6%9D%A1%E4%BB%B6%E3%82%92%E4%B8%8E%E3%81%88%E3%81%A6await%E3%81%99%E3%82%8B

https://hackmd.io/@-xLrSnFfROOeIeRnENCWcQ/HkVAMY5Sd#CancellationToken-%E3%81%AE%E6%B8%A1%E3%81%97%E6%96%B9

https://www.arp-corp.co.jp/blog/contents/2019/08/001.html

https://aizu-vr.hatenablog.com/entry/2020/03/16/UniTask%E3%81%A8UniRx%E3%81%A7%E3%80%81%E5%BE%85%E3%81%A1%E3%81%AE%E5%87%A6%E7%90%86%E3%82%92%E5%AE%9F%E8%A3%85%E3%81%99%E3%82%8B%EF%BC%88%E5%89%8D%E7%B7%A8%3AUniTask%EF%BC%89