LoginSignup
1
0

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-01-04

目的

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の値を変更することで録音を開始する音量の閾値を変えられます。

以上。

参考

1
0
0

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
  3. You can use dark theme
What you can do with signing up
1
0