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