UnityのOnAudioFilterRead関数内でWAV再生する
はじめに
この記事はUnityを使ってオーディオを再生する関連の記事です。
具体的には、Unityの組み込み関数のひとつであるOnAudioFilterRead関数にて、WAVファイルを再生するという割とニッチな内容になっております。
本来だとAudioClipを使えばどうにかなるのですが、どうにもならない場合は往々にして存在するので、そこらへん書いていこうかと。
20:38
驚く間違いしていたので修正しました。
本来だとどういう処理するのか
AudioSourceにAudioClipをセットすれば音が鳴ります。
またループや音量・ピッチ設定等もできますし、
あとUpdate関数内で制御するのであれば、たいがいのケースは以下の方法で大丈夫ではあります。
なぜ本記事の内容が必要か
再生スケジュールとその他処理の関係で必要になってきます。
リアルタイムでピッチを可変させたりするような処理にはUpdate関数で制御する方法だと都合が悪くなります。(先行して予約する方法もリアルタイム制御というのは不可能になる)
具体的には事前の発生タイミングでどのくらいピッチを変更するか、等は事前に決めるのが辛いですし、Update関数がそもそもスペックのいいPCで1/200秒くらいの単位、オーディオ制御のためには他のPCで動かすこと考慮すると粒度が大きいです。
あと、例えばフィルターワークみたいなことをしようとすると再生タイミングで波形を書き込むほうが都合が良かったりします。
方法
以下、箇条書きします。
- 事前にデータを取得する
- 再生スケジュールを自分で管理する
- OnAudioFilterRead関数内で読み込んだデータを書き込む
具体的な方法を記載
1.事前にデータを取得する。
AudioClipにはGetDataという関数があって、これでfloat値で波形データを取得することができます。
まずは設定を確認してからですかね。
この関数を利用して、float配列にデータを格納しておきます。
さて、ここで上記のアプローチを採用する理由はご存知でしょうか?
理由は明確で、Unityの仕様として、メインスレッド以外でUnityEngineのAPIを呼び出すことはできないからです。
AudioClipもUnityEngineのAPIのひとつとなるため、OnAudioFilterReadのように別スレッドで起動するような関数では実行できません。
ということで別途UnityEngineのAPIを使わないようにするための対策として、予めfloat配列にデータを突っ込んでおくわけですね。
この方法では、「AudioClip」「波形データ」と2種類のデータをメモリ上に展開するわけで言ってしまうとデータが重複するのですが、これはもうどうしようもないですし、不要なのがわかっていたら解放できるようにしておいたほうがいいでしょう。
自分はオーディオの再生レートやピッチを変えたりするのを考慮してとっておきますが。
2.再生スケジュールを自分で管理する
割と簡単です。
まず、AudioSettings.outputSampleRateで現状の出力に対するサンプル数を取得できます。
上記の値は、outputSampleRate=1秒あたりに処理されるfloat値の量です。
すなわち、これで1秒周期のデータ処理状況を確認することができました。
これが取得できたらこっちのもんです。
例えばこの単位を以下のようにすれば、簡単にスケジューリングが可能です。
データを取得したときサンプル数が48000だった場合、以下のように計算が可能です。
- 1秒あたり48000個データが存在する(再生される)
- 0.5秒→1秒分の1/2→サンプル24000個分
- 2秒→1秒分の2倍→サンプル96000個分
ここで、特定のサンプル数分カウントが終了したらアプリ終了するなりカウンタリセットするなりすればいいと思います。
ただ、注意なのが、スタートに戻る時。
この方法でスケジュールを管理する場合、安易にゼロクリアするとスケジュールがぐちゃぐちゃになってしまうので、減算でスケジュールを調整することをおすすめします。
(自分が書いたソースコードだとカウンタを正確に加算していくのであまりズレないんですけどね)
3.OnAudioFilterRead関数内で読み込んだデータを書き込む
下記サンプルコードを適当なGameObjectにアタッチすれば動きます。(Mixerなしでも)
ただ、正確にはAudioSourceが付いたオブジェクトにアタッチして、ミキサーに流す等が安全かなって思ってます。
サンプルソースコード
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System; // OnAudioFilterRead関数内でMathクラスを使うため追加
public class sampleTrigger : MonoBehaviour {
public AudioClip SEClip;//AudioClip
int count=0;//スケジュールを管理する変数
bool playing= false;//準備ができたフラグ
int length;//多ch分含めたサンプルの長さ
int sampling_rate;//サンプリングレートを保管するための変数
//floatの配列。ここにサンプルのデータを確保する。
float[] temp_sample;
void Start() {
//サンプリングレートを取得
sampling_rate = AudioSettings.outputSampleRate;
//波形データを保存する変数の領域を、動的にメモリ上に確保
temp_sample = new float[SEClip.samples* SEClip.channels];
//ちょくちょくアクセスすることになる「サンプルのバイト数」を確保
length = SEClip.samples* SEClip.channels;
//UnityEngineの関数。データをfloat配列に書き込む
SEClip.GetData(temp_sample, 0);
//準備ができたのでフラグをONにする
playing = true;
}
void OnAudioFilterRead (float[] data, int channels) {
//データが読み込み終わらないうちにアクセスしようとすると
//メモリ違反するので準備が終わってからにする。
if( !playing)
return;
//OnAudioFilterRead関数に書き込むオーディオ出力をサンプル単位で
//加工する、もしくは書き込む。(今回は書き込み
for (int n = 0; n < data.Length; n = n + channels) {
float smp= 0.0f;
//スケジュール調整の関係上ゼロクリアではなく減算で調整する
if( count>=sampling_rate){
count-=sampling_rate;
}
//片側チャンネルの音を両側に鳴らす
//ここ本当はしっかりチャンネル数を指定するけど、
//チャンネル数を2と仮定して書き込む
//(モノラルWAVは対応してないよ)
if( count <(int)(length/2) ){
smp = temp_sample[(int)(count*2)];
}
//関数が取得してくるデータの変数内に値を入力する
//今回はdbを考慮せずそのまま値を入力。
data[n] = smp;
//また、2chを想定してデータの書き込みを行っている。
//勿論多chある場合はその分の計算を行うのがいいですね
data[n+1] = smp;
//スケジュールを1サンプル分進める
count++;
}
}
void Update() {
//用がないので書かない
}
}
おまけ:オーディオの音切れ対策
当該方法は、OnAudioFilterRead関数内でスケジュール管理を実装してます。
よって、何らかの理由でOnAudioFilterRead関数が正常にキューを出せない場合、
スケジュールが狂う可能性があります。
具体的には音にグリッチが発生するとか、発音スケジュールが遅くなるとか。
この場合、以下項目を弄ると問題なくなるかと思います。
(Editor上)
Edit→ProjectSettings→Audio→DSP Buffer Size
この設定を弄ることで、オーディオグリッチが減ることを確認しました。
Discussion