はじめに
ADXアンバサダーのこはとです!
ゲーム配信が盛んな昨今では、マイクを使って「叫ぶこと」や「音を真似る」などのインタラクションを用いたゲームも増えてきました。
Unityの既存システムでもマイク入力を取得することはできますが、ADXを用いることでも、より簡単にマイクの入力を取得できます。
今回は、ADXの機能でマイクの入力を取得し、その値をゲームの動きに反映させるような機能の実装例を紹介します!
この記事は、CRIWAREが提供するサウンドミドルウェア「CRI ADX」の機能についての紹介をします。
本記事では、CRI ADXを触ったことがある方を対象としており、細かい操作の解説は割愛しております。
CRI ADXについてはこちら
忙しい人向け
using UnityEngine;
using CriWare;
public class MicCapture : MonoBehaviour
{
//マイク用変数。マイクの機能はここに格納されます。
CriAtomExMic mic;
//マイクの入力内容が入るFloatの配列。
//入力内容は各配列内容毎に-1.0~1.0まで。
public float[] micdata = new float[512];
void Start()
{
//マイクモジュール初期化
CriAtomExMic.InitializeModule();
//デフォルトマイクを取得する※マイクが接続されている必要があります!
CriAtomExMic.DeviceInfo? defaultDevice = CriAtomExMic.GetDefaultDevice();
if (!defaultDevice.HasValue)
{
Debug.Log("デバイスが見つかりませんでした。");
}
//デフォルトデバイスが取れていたら
if (defaultDevice.HasValue)
{
//マイク入力を開始
StartMic();
}
}
/// <summary>
/// マイク取得を作成し、入力を開始します。
/// </summary>
public void StartMic()
{
//マイクの設定用Configを作成。
CriAtomExMic.Config config = CriAtomExMic.Config.Default;
//1フレームに取得するサンプリング数
config.frameSize = (uint)micdata.Length;
//マイク生成
mic = CriAtomExMic.Create(config);
if(mic != null)
{
mic.Start();
}
}
/// <summary>
/// マイクの処理を停止し、削除します。
/// 実行しても、Start処理で行ったDefaultDeviceの取得はそのまま残っているため、
/// 再開するためにはStartMic()を再度実行して下さい
/// </summary>
public void StopAndDestroyMic()
{
//マイクを削除
if(mic != null)
{
//マイクを停止
mic.Stop();
//マイク自体の処理を削除
mic.Dispose();
//参照エラー回避のため、nullで上書き
mic = null;
}
}
void Update()
{
if(mic != null)
{
uint numSamples = mic.ReadData(micdata);
}
}
//オブジェクトが消える際、またはゲーム終了時
private void OnDestroy()
{
//マイクをオフにする
StopAndDestroyMic();
}
}
最小の構成は以上の内容です。
StartでMicを取得後、StartMic()を呼び初期化。Updateで随時ReadData(micdata)を呼ぶことで、サンプルデータ(pcmデータ)を取得することができます。
micdataの詳細な利用方法については以下で解説します。
UnityProjectを準備する
ADXの音声再生機能では普通、AtomCraftを用いてCueを作成しますが、マイクの入力を取得するだけならAtomCraftを操作する必要はありません。早速Projectを作成し、プロジェクトにADXforUnityのPluginをインポートしましょう。
Pluginをインポートしたら、SceneにObjectを配置していきます。
入力がされていることを確認するためのText、音声に応じて動かしたいGameObjectを配置しましょう。
サンプルではGameObjectの回転角がマイクの入力に応じて変化します。
マイク入力取得処理を実装する
確認用のTextが作成できたら、早速実装内容を記述していきます。
今回は
- Start()でデフォルトのマイクを取得
- 随時Textに「Peak値(最大音量)」「RMS値(平均音量)」を表示させる
- 取得できたRMS値にオブジェクトの角度を追従させる
という機能を実装してみます。
まずフローチャートで全体の流れを見てみましょう。
基本的に必須なコードは
- CriAtomExMic.InitializeModule() でマイク初期化
- CriAtomExMic.Create() でマイク作成
- CriAtomExMic.ReadData(float[]) で入力データを取得
といった手順のコードとなります。
必須コード自体は少ないのですが「マイクが接続されていない場合の例外対策」や「マイク設定は正しい内容か」「終了時はマイクを停止させる」など、デバイスを安全に使用する上で必要な操作がある点に注意して下さい。
フローチャートが確認できたら、早速コードを実装していきましょう!
Project内の任意のフォルダにScriptを作成し、Script名を「MicCapture」とします。
using UnityEngine;
using CriWare;
using TMPro;
public class MicCapture : MonoBehaviour
{
//マイク用変数。マイクの機能はここに格納されます。
CriAtomExMic mic;
//マイクの入力内容が入るFloatの配列。
//入力内容は各配列内容毎に-1.0~1.0まで。
float[] micdata = new float[512];
//最大値
public float peakLevel { get; private set; }
//波形平均値
public float RMSLevel { get; private set; }
//最大音量を表示するText
[SerializeField]
TextMeshProUGUI peakText;
//音声の平均音量を表示するためのText
[SerializeField]
TextMeshProUGUI RMSText;
//実装例:角度を変更するオブジェクト
[SerializeField]
GameObject testObject;
//実装例:オブジェクトを動かす最小音量。これより小さい値は0とみなす。
[SerializeField]
float threshold = 0.02f;
void Start()
{
//マイクモジュール初期化
CriAtomExMic.InitializeModule();
//デフォルトマイクを取得する※マイクが接続されている必要があります!
//「?」は変数名ではなく、Null許容型の演算子
CriAtomExMic.DeviceInfo? defaultDevice = CriAtomExMic.GetDefaultDevice();
if (!defaultDevice.HasValue)
{
Debug.Log("デバイスが見つかりませんでした。");
}
//デバッグログ:取得されているマイクデバイスの内容をすべて表示
foreach(var device in CriAtomExMic.GetDevices())
{
//もし対象デバイスがDefaultデバイスと同じであれば…?
if(device.deviceName == defaultDevice.Value.deviceName)
{
//…Debug.Logに(has set as default)を追加
Debug.Log($"{device.deviceName} (has set as default)");
}
else
{
//…それ以外は普通にデバイス名を出す
Debug.Log($"{device.deviceName}");
}
}
//デフォルトデバイスが取れていたら
if (defaultDevice.HasValue)
{
//マイク入力を開始
StartMic();
}
}
/// <summary>
/// マイク取得を作成し、入力を開始します。
/// </summary>
public void StartMic()
{
//マイクの設定用Configを作成。
CriAtomExMic.Config config = CriAtomExMic.Config.Default;
//1フレームに取得するサンプリング数
config.frameSize = (uint)micdata.Length;
//マイク生成
mic = CriAtomExMic.Create(config);
if(mic != null)
{
mic.Start();
}
}
/// <summary>
/// マイクの処理を停止し、削除します。
/// 実行しても、Start処理で行ったDefaultDeviceの取得はそのまま残っているため、
/// 再開するためにはStartMic()を再度実行して下さい
/// </summary>
public void StopAndDestroyMic()
{
//マイクを削除
if(mic != null)
{
//マイクを停止
mic.Stop();
//マイク自体の処理を削除
mic.Dispose();
//参照エラー回避のため、nullで上書き
mic = null;
}
}
// Update is called once per frame
void Update()
{
//マイクが設定されているときだけ入力
if(mic != null)
{
//マイクの入力取得
//マイクの内容は、厳密には必ず毎フレーム取得されるわけではなく、指定のサンプリング数が取得できたタイミングでマイクの入力を返します。
//そのため、擬似的に毎フレーム入力されているような挙動を作るためには「マイクの入力があったときのみ」入力を扱うようにします
//マイクの入力があったかどうかは、mic.ReadData(micData)を入力した際の値で判別できます
//マイク入力あり→1以上の値を返す(通常、ReadData()に入力した配列のLengthの数が入力されます)
//マイク入力なし→0が入力を返す
uint numSamples = mic.ReadData(micdata);
//取得できたサンプル数が0以上の場合(=マイク入力があった場合)
if(numSamples > 0)
{
//最大音量
float maxPeak = 0f;
//平均音量
float sample_total = 0f;
for(uint i = 0; i < numSamples; i++)
{
//最大音量
if(maxPeak < Mathf.Abs(micdata[i]))
{
maxPeak = Mathf.Abs(micdata[i]);
}
//波形全体の平均音量=>まずすべての二乗数を取る
sample_total += micdata[i] * micdata[i];
}
//最大音量値
peakLevel = maxPeak;
//波形全体の平均音量=>for文内で入力したすべての値を取得サンプル数で割り、平方根を取得する
RMSLevel = Mathf.Sqrt(sample_total / numSamples);
//Textの内容を更新
//Tips:ToStringに(N+桁数)を与えると、小数点の表示桁数を指定できます。
peakText.text = "PEAK : " + peakLevel.ToString("N2");
RMSText.text = "RMS : " + RMSLevel.ToString("N2");
}
Debug.Log("Get Sample Num = " + numSamples + " : Frame Count (" + 1f/Time.deltaTime + "fps) : "+ Time.frameCount);
}
//==============================================活用例==============================================
//マイクの入力音量(RMS)に合わせて、オブジェクトの回転角を変える。
float deg;
//非常に小さい音量が入力された際にガタガタするのを防ぐため、しきい値で正規化
if (Mathf.Abs(RMSLevel) < threshold)
{
deg = 0;
}
else
{
deg = RMSLevel;
}
//オブジェクトの角度を変更
testObject.transform.localEulerAngles = new Vector3(0, 0, deg * -120f);
}
//オブジェクトが消える際、またはゲーム終了時
private void OnDestroy()
{
//マイクをオフにする
StopAndDestroyMic();
}
}
それぞれの機能について解説していきます。
CriAtomExMic
マイクモジュールの入力取得や、設定を管理できるクラスです。
マイクデバイスの初期化、取得、変更ができる他、実際に入力されたデータもここから読み取ることができます。
また、ADXのマイク機能には、CriAtomExMic.GetDefaultDevice()というメソッドが実装されており、このメソッドを使うことで、自動でPCに設定されている規定のマイクデバイスを取得することができます。
OnDestroy()にも記載した通り、マイクデバイスの使用を終了する際は必ず「CriAtomExMic.Stop()」「CriAtomExMic.Dispose()」を呼び出してマイク入力の取得を終了させて下さい。
CriAtomExMic.Config
マイクを作成する際に使用できる、マイク設定用の構造体です。
内容としては主に、マイクのサンプリングレートやバッファサイズ(後述)などの入力パラメーターを指定するために使用されます。
CriAtomExMic.Create()でマイクを作成する際に引数へ指定することができますが、無くても作成できます。
ただし、これを引数に設定しない場合は、マイクのサンプリングレートやバッファサイズがデフォルトの値で設定されるため、最適化などのためにこれらの数値を厳密に設定したい場合は、Configを作成したうえで設定するようにして下さい。
CriAtomExMic.ReadData(float[])
マイク取得で最も重要なメソッドです。
このメソッドにfloatの配列を渡すことで、その配列内容にマイクから取得されたサンプリングデータ(pcmデータ)が渡されます。
また、同時にこのメソッドは取得できたサンプリング数(int)を返し、これは引数に設定したfloat[]配列のサイズと一致します。
引数を変更することで、ステレオでの取得、マルチチャンネルでの取得にも対応します。
今回はモノラルでの取得を想定しています。
実行!
以上の内容をScriptに記載できたら、任意のGameObjectに追加し、Sceneに配置したTextMeshとGameObjectを参照させて、プレビューを実行してみましょう。
※プレビュー前に、PCにマイクが接続されていることを確認してから再生しましょう!
声の大きさに併せてオブジェクトが動けばOKです!
実装例では、オブジェクトの角度が声に合わせて変わります。
マイク入力によって変化する値は最終的にRMS値(0f~1f)として変換されるため、この値に様々なパラメーターを紐づけて動かしてみましょう!
また、動かない場合はPCのマイク音量の設定なども確認してみて下さい。
おまけ~マイク取得の理論的なことについて~
動作はできたけど、実際にどういった処理が行われているのか?について解説していきます。
ここはADXの機能に限らず、一般のマイク機能についても同じことが言えます。
マイク取得時に何が起こっているか
マイクの入力は、入力された波形がデジタル変換されることで、最終的にfloat値の配列に変換されます。
これはマイクに限らず、コンピューターに生音源を流し込む場合は、基本このような変換が行われます。
そして、マイクの入力は、厳密には毎フレーム取得されているわけではありません。
マイクに入力された内容を一定値バッファし、一定量溜まったらそれをFloat[]の配列として入力する、という処理で動作しています。
なぜ1サンプル毎に取得せず、複数のサンプルが溜まってから入力する、という手法をとるかというと不必要に処理負荷がかかるのを避けるためです。
例えば、60fpsのゲームのサンプリングレートが44,100Hz(44,100回/秒)で、バッファサイズを1(1回サンプリングされるごとに変換)で計算を行う場合、1フレーム間に計算される回数は
(44,100/1)/60fps=735
となり、1フレームに735回の計算が実行されることになります。
これはCPUに対し非常に負荷がかかる他、正常に音声が取得できないなどの不具合の原因にもなります。
そのため、上図のように「概ね1フレームずつの間隔で音を切り出してデータ化する」という形にすることで、実質的に毎フレーム実行されているような挙動を実装しています。
今回のケースでは、バッファサイズを512にすることで、60fpsで概ね1フレーム(0.7フレーム)に1回データを取得するような実装になっています。
これは、制限をかけてないUnityEditor上など、フレームレートが60fps以上の環境で確認すると、バッファが貯まる前のフレームではサンプルデータが取得できていない様子が見て取れます。
そのため、複数のフレームレート制限があるゲームで同様な取得できるようにするためには、バッファサイズをフレームレートに併せて変更してみても良いかも知れません。
int targetFrameRate;
int targetSamplingRate;
int bufferSize;
void OptimizeMicBufferSize()
{
//ピッタリ1フレームに1回だと収まらない可能性があるため、0.8fに1回くらいの数にする
bufferSize = Mathf.RoundToInt((targetSamplingRate / targetFrameRate)*0.8f);
//↓16の倍数で揃えたい場合
//bufferSize = Mathf.RoundToInt(((targetSamplingRate / targetFrameRate)*0.8f)/16f) * 16;
}
終わりに
以上、ADXでマイクを使う方法でした!
基本的には取得されたサンプルデータからRMS値(波形平均値)を計算するだけで、概ね「音声に合わせた動作」が実装できます。
マイクのバッファサイズ周りについては、基本的にフレームレートを固定して、バッファサイズも合わせた内容にするのがよりベターかと思います。
マイクの入力をゲームのInputに活かすことで、ちょっと変わったゲームに挑戦してみてはいかがでしょうか?
また、CRIADXでは、公式のDiscordコミュニティも随時公開されています。
「こんなことはできる?」
「不具合が解消しない!」
「他の人がどんな使い方をしているのか知りたい!」
など、気になることや知りたいことを、開発スタッフに直接質問することもできます。
イベント情報等もこちらで告知しておりますので、ご興味のある方は、ぜひご参加ください!