UnityのAudioSourceの鳴り方やDSP Buffer Sizeの仕様を確認する

最近趣味でFC版スターソルジャーのUnityによる移植をしていて(きっかけはブログの別記事参照)、サウンド以外は一通り移植し終えて遊べる状態になったのですが、サウンド移植にあたって次のフレームで急遽鳴らし方を変えなければいけないなどのシビアなタイミングを要求されたりするので、まずUnityのAudio機能がどういう仕様になってるのかを検証してみた、というのが今回の記事です。

 

あくまでこれらは自分のPC環境による、WindowsのUnityエディタやWindowsビルドでの結果です。別環境では結果が変わる事柄もあります。

Unity 2021.3.13f1 でテストしています。

 

 

 

AudioSourceのVolumeやPitchの変更はどのくらい細かい頻度で可能なのか

まずは、高速にVolumeをランダムに変えまくってみて、再生された波形はどのくらいの頻度で音量が変わっているのかを調べます。

テストのために、Lchが「徐々に音程と音量が下がる三角波」、Rchが「ずっと1.0(最大)」という波形のAudioClipを用意して鳴らしています、

音量だけ見れば十分なのでRchの方の波形だけを見てみます、再生された実際の音の波形はこちら。

音量を変更してからは少しの間その音量が変化していません。VSyncはOFFにしてるのでVSyncでもない。

もう少し拡大してみます。

音量の上がり始めから下がり始めまでを長さを測ったところ、1024サンプルありました。Audio設定のSystem Sample Rateは48,000Hzなので時間にして0.021333...秒(約21.3ms)の長さです。1024サンプルに1度の頻度で変更できるということですね。

ん?1024サンプルってどこかで…と思い、Project SettingのAudio設定のDSP Buffer SizeをBest PerformanceからGood Latencyに変えてみると、512サンプルに変化しました。

qiita.com

こちらのページに、GetDSPBufferSizeのbufferLengthが、Best Latencyは256、Good Latencyは512、Best Performanceは1024だったという情報があります。numBuffersは4なので長さbufferLengthのバッファを4つ用意して次々更新している模様。

つまりDSP Buffer Size毎に音量が変更されているわけですね。

ちなみに、音量が一瞬で上下するのではなく、リニアに変化しています。

これはプチノイズ対策だと思われます。一瞬で音量が変化すると「プチッ」というノイズが鳴るので、緩やかに変化させてノイズを抑制しています。

変化にかかる長さは64サンプルでした、こっちはDSP Buffer Sizeを変更しても変化せず、それどころかSystem Sample Rateを変更しても変化しませんでした。48,000Hzだと64サンプルは時間にして1.333...ms(1/750秒)の長さですが、22,050Hzに変更しても64サンプルのままで、約2.9ms。

 

Volumeの次はPitchとpadStereo。前述のGetDSPBufferSizeについてのUnityのマニュアルに「ボリューム / ピッチ / パンのような変更の更新コマンド」とセットで書かれていたので多分Volumeの更新と同じDSP Buffer Size毎と予想しつつ。

そして試した結果、やはり同じでした、というか頻度だけでなく変化するタイミングも3つで一致していました。DSP Buffer1つの中では変化せず、次のDSP Bufferの最初で一斉に更新されるということなんでしょうね。

なおpadStereoでもプチノイズ対策は同様で、パンがリニアに変化してましたが(長さも同じく64サンプル)、Pitchの方は一瞬で目標のピッチまで変化していました。こっちは別に一瞬で変化してもプチノイズが鳴ったりはしませんしね。

 

AudioSource.Playはどのくらい細かい頻度で反映可能なのか

AudioSourceのVolumeやPitchはDSPバッファ単位の時間でしか変化しませんでした。だったらPlayが反映されるタイミングも同じなのでは?1つのDSPバッファ単位の間に何回もPlayしても1つにまとめられてしまうのでは?

ということで確認してみます。方法は簡単で、60fpsで1フレームに1回Playして1秒間に何度鳴るかを確認。

結果、やはり0.021333...秒毎に1回、DSPバッファ1つ分経過する毎に鳴っています。しかも、一緒に別の音をPitchをランダムに鳴らしてタイミングを見たところ、Playで鳴り始めるのとPitch変更が反映されるタイミングは一致していました。

回数にして1秒間に46.875回(0.021333...の逆数が46.875)。1秒間の内60回Playして46回鳴って14回は無効になっています。

それにしても0.0213秒って結構大きいな…1秒間に46.875回となると60fps(0.01666...秒)よりも粗い。44,100Hzだったら1秒間に約43.0664回と更に遅い。曲に合わせて音鳴らす場合はPlayDelayedでちゃんとタイミング指定しないとリズムがよれよれになってしまう。でもそうすると遅延は増す。ファミコンでも秒間約60回更新でリズムがよれないBPMの種類が限定されてたというのに、46.875回となると…。

 

AudioSource.Playのアタックが弱まる

実は最初のテストで、AudioSourceをPlayで再生してみて気になる所が…。

 

アタックが弱まっている!右と同様一瞬で立ち上がらないといけないのに。

この画像は前述の実験でRchで「ずっと1.0(最大)」という波形をVolume0.5で再生した時の波形ですが、Lchで鳴らしてる三角波の方でも同様に段々音量が増す形にになっていました。

長さが64サンプルなので、前述のプチノイズ対策が、普通にPlayしてるだけなのに勝手に発動してしまってるようです。実際のゲーム等に使う波形は普通0から始まる前提なので最初にプチノイズが鳴ることは無く、ここでプチノイズ対策されても困ります。

むしろプチノイズ対策が必要なのって開始時よりも急な停止時では?というか停止時にプチノイズ対策する仕様が開始時にも影響しちゃってるのかもしれない、と思いPlayの後に途中でStopやPauseで止めてみました。

すると、

盛大に「プチッ!」と鳴りました。こっちは対策入らんのかーい。

あっそうだPauseした後のUnPauseで再開した時こそ波形の途中から始まるからプチノイズ対策が必要だ、とUnPauseしてみました。

「プチッ!」

えぇ…。

まぁ実際のゲームではこんな1.0固定の波形を使う意味は無く、普通に波打ってる波形を再生するのでこんなにプチプチ鳴りませんが、それでも高周波が少ない音や声ではプチノイズが目立つ危険性はあります。

1回目だけ音量が0から始まるのか?と同じAudioSourceを2回鳴らしてみましたが(Play→最後まで再生→Play)、2回とも音の最初は0からリニアに変化しました。じゃあ鳴ってる途中にもう1回鳴らすと?(Play→再生途中にPlay)

プチノイズ対策が必要な部分は対策されずノイズが鳴り、プチノイズ対策がいらない部分で対策が発動している。

PlayではなくPlayOneShotも試しましたが、結果は同じく0からリニアに変化しました。PlayDelayedで遅らせて再生…も、恐らく…

あっ普通に鳴る!
PlayScheduledで遅らせて再生した場合もPlayDelayed同様プチノイズ対策は入りませんでした。
つまりアタックを弱めずにAudioSourceを再生したい時はPlayの代わりにPlayDelayedを使える?PlayDelayedの引数に0を渡して再生。

あっダメでした。

遅らせない場合は結局同じか…。PlayScheduledの方も、遅らせない場合はプチノイズ対策が入りました。

遅らせると対策が影響しないということは…PlayDelayedやPlayScheduledで遅らせて鳴らした場合、実際に音が鳴るのは遅れるけど0からリニアに変化し始めるタイミングは遅れてないということか?と試しにPlayDelayedで32サンプル分遅らせてみる(32/48000秒遅らせる)。

やはりそうです。アタックを弱めたくない場合はPlayDelayedなどで64サンプル分遅らせて再生すると大丈夫そうです、もちろん64サンプル分発音が遅延しますが。

遅らせる時間が固定ではなく「リズムに沿ってこのタイミングに鳴らすためにPlayScheduledで後で鳴らす時間を指定する」という使い方なら、鳴らすタイミングが大抵64サンプル以上後でしょうから影響は受けないですね。

 

これらを試したのはWindowsのUnityのエディター上でですが、Windows用exeファイルをビルドして実行しても同じでした。あとAudioClipはPCMとVorbisで試しましたがどちらも結果は変わらず。

AudioSourceでPlay On Awakeを指定して最初から鳴ってる場合はプチノイズ対策は入りませんでした。Loopで波形の最後から最初にループする時も普通にループします。

Chrome(バージョンは107.0.5304.107)上のWebGLで試すと、結果がちょっと違いました。リニアな変化ではなく

アンダーシュートしつつカーブを描く形になっています。原因はChromeなのかAAC圧縮なのかUnityのWebGLビルドなのか。

あとWebGLビルドではPlayDelayedとPlayScheduledで遅れて再生させてもPlayと同じくこの波形になりました。


というわけで、余計なプチノイズ対策が発生していますね。これではアタックの強い音の鳴り方が変わってしまいそうです。まぁ長さが64サンプルなら、48,000Hz再生時の750Hzの1波長分と考えると影響のある波形もそんなに多くないかもしれませんが。

逆に、停止する方では対策されてなくてプチノイズが入り放題です。急に止めるのではなく、AudioSourceのVolumeを0にして音量をリニアに0にしてから止める必要があるケースが出てくるかもしれません。

 

 

AudioSettings.dspTimeの分解能

AudioSettings.dspTimeについて、Unityのマニュアルには

オーディオシステムの現在時刻を返します。

これは秒単位で指定された値であり、オーディオシステムが処理する実際のサンプル数にもとづくため、Time.time プロパティーから取得される時間よりも正確です。

とあります。どのくらいの細かさで変化してるのか、48,000Hzだったら変化の最小単位は1/48,000秒なのか、検証。

その前に。Update関数内でTime.timeは変化しませんが、AudioSettings.dspTimeもそうなのかを確認してみたいと思います。

Update関数内で、forループで10万回AudioSettings.dspTimeが変化しないか監視(Stopwatchで測ったら5.3ms)してみたところ、ループ内で変化することがありました。細かく増えるのではなく、どれも値が0.021333...増えてます、それ以外は止まってます。これがAudioSettings.dspTimeの分解能ということ?

そしてまた0.0213秒が出てきた。この数字は、

DSP Buffer Size ➗ System Sample Rate

ですね。

1024 ÷ 48000 = 0.021333...

確認のためにAudio設定のDSP Buffer SizeをBest PerformanceからGood LatencyにしてDSPバッファサイズを半減してみると、AudioSettings.dspTimeが増える量が0.010666...になりました、こっちも半減。

そこからSystem Sample Rateを48,000Hzから半分の24,000Hzに変更してみると、増加量が0.021333...に戻りました。ということでやはりDSP Buffer Size / System Sample Rate。

つまりDSPバッファ1つ分の実時間での長さが、AudioSettings.dspTimeの増加量の最小単位ということになります。1つのDSPバッファを更新し終わった時に変化するんだろうか。

 

AudioSettings.dspTimeの値を直接基準にして何か動かす、ということはしない方がいいでしょうね、動きがガタガタします。Update関数の更新間隔と合いませんしデフォルト設定では秒間46.875回更新(44,100Hzだと秒間約43.0664回更新)と60fpsよりも遅いですし。

試しに60fpsでAudioSettings.dspTimeがどう変化するかをグラフにしてみます、横軸がTime.time、縦軸がAudioSettings.dspTime。

これだけ何度もガタガタします。このグラフでは1秒間に13回ほど1フレーム後になっても停止しています。停止する間隔も3フレームだったり5フレームだったりとブレがあります。

 

次に、AudioSettings.dspTimeがUpdate関数内でも更新されてるなら、その更新される間隔もきっちり0.0213秒毎なのでしょうか。正確な間隔で更新されてるならAudioSettings.dspTimeを確認しながらシビアなタイミング調整して音を鳴らしたりできそうですが。

VSyncをOffにして500fps近く出る状態にして、Updateが呼ばれるたびにUpdate関数内でTime.timeとAudioSettings.dspTimeの値を記録して、グラフにしてみます。

一見問題なさそうですが、よく見るとちょくちょく横に伸びてる部分があります。Updateが呼び出される間隔が一定じゃないせいでしょうか。

そこで、先程は500fps近く出てる状態で1秒間に500回近くUpdateを呼びUpdate1回につき1回Time.timeとdspTimeを記録していたのを、1回のUpdate内で100万回Stopwatch.ElapsedTicksとdspTimeを記録するループを回す(その間Update関数内からreturnしない)というやり方に変更してみました、100万回のループ終了までの時間は0.8秒ほどになりました。

精度は良くなりましたが変わりませんね。少し横に伸びてる所に印をつけました。7、8回に1回のペースで少し横に伸びてます。

間隔を確認すると、横が短い方は0.02秒前後、横が長い方は0.03秒前後になっていました、0.02秒と0.021333...秒の差が溜まってくると0.03秒になって帳尻を合わせる感じです。0.021333 - 0.02 で誤差0.001333 、1 / 0.001333...は7.5なので、7、8回に1回という頻度はここからですかね。

これでは誤差の上下が大きすぎて、dspTimeが変わったのに合わせて何かシビアなタイミングの処理をする、みたいなことはできませんね。PlayScheduledで、すぐ来るタイミング(0.021333秒以内とか)を指定する場合は7、8回に1回の確率で間に合わず遅れ方が変わるかもしれない?

dspTimeの変化のタイミングが正確だったら、変化するどれくらい直前のギリギリのタイミングまでPlayが間に合って反映されるかも確認したかったのですが。

 

 

AudioSource.PlayScheduledの時間の精度

dspTimeの特性がわかったところで、続いてdspTimeを使うPlayScheduledについて調べてみます。

まずPlayScheduledの再生タイミングはどのくらい細かく設定できるのか、サンプル単位までいけるのか確認。2つのAudioSourceを若干ずらして鳴らしてみます。

double time = AudioSettings.dspTime;
audioSource[0].PlayScheduled( time + 0.125 );
audioSource[1].PlayScheduled( time + 0.125 + n / 48000.0 );

こんな感じで。nにずらす量を入れて、1/48000単位でタイミングをずらしつつ0.125秒後に鳴らす、というのをTime.timeが0.25増える毎に何度も繰り返し実行します。これで何サンプルずれるか。

結果はこちら。

ずらす秒数 ずれたサンプル数の遷移
0 / 48,000 000000000000000000000000000000000000000000000000000000000000
1 / 48,000 111111111112112111111111111111111111111111121121111111111111
2 / 48,000 222222222211122222222222111222222222222222222222233332222222
3 / 48,000 434333333333333333333333333333333333333333333333333333333333
4 / 48,000 444444444444444444444444444444444444444444444444444444444444

0と4は指定通りでしたが、1と3は少し+1が混じり、2は±1の誤差です。4が大丈夫なら8もいけるかも?と試してみましたが少し+1が混じりました。

PlayScheduledの引数はdoubleですが、それでもどこかしらで誤差が出るのかもしれません。n/48000は浮動小数点でぴったり表現できる値ではないでしょうし。

まぁたまに±1サンプルずれる程度で、だいたい1/48000秒単位の指定は可能そうです。もしずれても±1サンプルなら実用上問題になることはそうそう無いかと…波形と使い方によってはプチノイズの原因になるかもしれませんが。

 

次に、PlayScheduledの時間指定の仕様を確認。

時間指定の引数にdspTimeより小さい値(もう過ぎた時間)や直接dspTimeを渡す(現在の時間または若干前の時間を指定する)とどうなる?

Playで鳴らすのと同様の挙動になりました、別のAudioSourceを同時にPlayすると鳴るタイミングも1サンプルもずれずに同時です。プチノイズ対策も入ります。

PlayとPlayScheduled( dspTime+0.5 )を一緒に実行して(つまりPlayScheduledは現在の時間より0.5遅れで鳴らす)それを録音してその時間差をサンプル単位で確認、理想は0.5秒差だけどどれくらいずれる?

24,000サンプル差、きっかり0.5秒差でした。40回ほど繰り返してみたところ、7回23.999サンプル差になることがありました、PlayとPlayScheduledで後者の方が1サンプルだけ早まってる?dspTime1つ分ずれるならdspTimeの変化するタイミングのせいでしょうけど、そうではなく1サンプルずれるということはどこかの誤差でしょうか。

PlayScheduled( dspTime+0.125 )とPlayScheduled( dspTime+0.625 )にしてみたら、全部24,000サンプル差になりました。

qiita.com

こちらの記事に、Start関数でPlayとPlayScheduledを使ってときどきズレが発生する情報があります。1度のUpdate関数内(同一フレーム内)での実行では1サンプルしかずれなかったので、Start関数でも試してみます。

20回試して3回ずれました、その内2回は1サンプルでしたが、1回は1024サンプルずれました。1024サンプルといえば、dspTime1つ分ですね。PlayをPlayScheduled( dspTime )、遅延0にしてみましたが結果は同じでした。遅延0のPlayDelayedやPlayScheduledがPlayと同じになるのはすでに試してますしね。

Start内から即再生(イントロ部分)と遅延再生(ループ部分)するとdspTime1つ分ずれることがあるのは確認できた、ならばイントロ部分とループ部分両方予めdspTime1つ分の時間遅らせてPlayScheduledで再生すればStart内からの再生でも問題は出なくなる?試しにずらす時間を0~dspTime1つ分(1024/48,000秒)の間でテスト。

PlayScheduled( dspTime + n / 48000.0 + 0.5 ) と
PlayScheduled( dspTime + n / 48000.0 ) でちゃんと0.5秒差からずれないか(テスト時のAudio設定のSystem Sample Rateが48,000Hzなので48,000で割ります)。

試行回数は各50回。

ずらす秒数 ずれた回数
0/48,000 1
64/48,000 1
128/48,000 1
256/48,000 3
512/48,000 1
768/48,000 1
1024/48,000 0

0秒に近いほど確率少なくなるかもと思ったけどそんなことはなかった。dspTimeの更新がdspバッファサイズ単位で増えてるためかずれるかずれないかの2択ですね。そもそもの確率が低すぎるせいか256/48,000の時3回に偶然?増えている。

256/48,000を追試で50回やってみたところ2回ずれましたが、ズレ方が、1回は1024サンプル、もう1回は768サンプルずれてました。768は1024-256ですね。これなら1024/48,000遅らせればずれは無くなる。

1024/48,000も追試で50回追加しましたがずれませんでした。ということで遅らせる量はDSP Buffer Size / System Sample Rateで良いのではないでしょうか(1024/48,000の場合は0.0213秒)。合計100回しか試してないので本当に0%かという不安は残りますが、心配なら更に2倍(2048)くらいにしておけばまぁ大丈夫でしょう。

そもそもStart内ではなくUpdate内でやればずれませんが。初回のUpdateでPlayとPlayScheduledを実行して試しましたがずれませんでした。ずれたとしても1サンプルだけでしょうね。

 

 

DSP Buffer Sizeを変えるとどのくらい遅延が増減するのか

小さくすると遅延が減る、どうやら更新間隔も短くなる、でも音飛びの危険が増す、Audio設定のDSP Buffer Size。変更が遅延にどう影響するか調べてみます。

ただしこれは環境(使用プラットフォームやゲームの作り)によって大きく変わるので、結局環境次第ではありますが、とりあえず「自分の環境の場合」「Windowsビルドの場合」の話ということで。

 

測定方法

どういうテスト方法かについて書きますが、測定方法に試行錯誤があって文章が長くなったので、読まずに測定結果まで飛ばして大丈夫です、測定結果まで飛びたい場合はこのリンクをクリック。

 

テスト方法としては、球体のゲームオブジェクトを表示するのと同時に音を鳴らすのを1秒に2回行い、それを録画してAviUtlで映像と音にどれくらいズレがあるかを見て確認してみます。

まずUnity Recorderで遅延差が測れるか試してみました。再現性は完璧なんですが(何度やっても同じ結果になる)、

DSP Buffer Size 映像に対する音声のタイミング
Best Performance 0.05~1.10フレームほど早い
Best Latency 0.05~0.20フレームほど早い

Best Latencyの方が逆に音の遅延が大きい、バラツキは小さい、という結果になりました。どうも録画段階で映像タイミングを基準に音声タイミングに補正がかかっているようです。これは遅延測定に使えない。

 

次に、「テスト表示をOBSで録画」しつつその様子を「HDMI出力してHDMIキャプチャ」しつつ「HDMIキャプチャのパススルーからテレビに映してスマホカメラで撮る」という3種類の方法で同時進行して撮ってみて、どの方法が使えるか試してみます。バラツキの小さいBest Latency設定。

撮る方法 映像に対する音声のタイミング
OBSでウィンドウを録画 -0.7 ~ -0.4フレーム
HDMIキャプチャで録画 +0.9 ~ +1.2フレーム
テレビをスマホで録画 +1.3 ~ +1.5フレーム

数字上はこんな感じですが、どれが実際の遅延の大きさなのかよくわかりません。特にテレビは測定方法がかなりアバウトです。この数字を実際の「テレビ表示時の遅延量」として信用はしないでください。

OBSだとUnityを使ったテスト表示の60fps表示とOBSの60fps録画がときどき完全に同期しないことがあり同じフレームが2度繰り返される場合がありました。

HDMIキャプチャは、キャプチャハードはMonsterX U3.0R、録画ソフトはアマレコTVで、マルチディスプレイの1つとしてビデオカードからHDMI出力してキャプチャしてます。
なお後で気付きますが、自分のキャプチャ環境だと録画開始する度に遅延量が変化するため(対策は後述)、上の表の数値も「他の方式に比べてこれだけ相対的な遅延時間の差が」という比較には使えません。

その映像をキャプチャからHDMIパススルーで液晶テレビにも映して、それをスマホのカメラで録画してもいますが、液晶なので映像が一瞬で切り替わらないしスマホ録画だと映像と音声のタイミングに若干のずれがあるので(一応、机をトントンしてその音でどれほど映像と音がずれてるか確認して補正してますが)、テレビの測定値に関しては正確さに欠け全く当てにならないです。

 

ということでフレームレートが安定して録画できるHDMIキャプチャで測定することにしましたが、テスト表示exe実行 → キャプチャで録画 → exe終了して録画をAviUtlで確認、という手順をAudio設定を変えるごとに辿ったら、同じ設定の時もなんだか結果が微妙に変わります。
どうもHDMIキャプチャだと録画開始の度に映像と音声のズレ加減が変化するようです。録画開始から録画終了までは変化しないのですが。キャプチャの設定が甘いのか?アマレコTVの、タイムスタンプを使ってタイミングを合わせるというオプションも試したもののときどき録画に失敗するので中止。


何度も録画開始するのがダメなら録画を終了させない方法をと思い、テスト表示にボタンUIを置きそれを押して設定を切り替える方式にして、録画開始したら録画もテスト表示exeも終了させずに設定を変更して測定しましたが、それでも設定変更(AudioSettings.Reset)した後元の設定に変更すると、同じ設定だろうと結果が微妙に変わります。遅延量の上下幅の大きさや上下の仕方は同じなのに結果が丸々0.4フレーム差ある、とか。設定変更しない間は同じパターンで遅延が上下したりと結果が安定してるんですけどね…AudioSettings.Resetした途端出るパターンは同じなのに全体に+0.4されたり。

 

設定変更でランダムに上下するなら、いっそのこと1秒に2回のペースで設定変更というかAudioSettings.Resetを実行&音を鳴らすのを繰り返しそれを測定して、ランダムに上下した結果の中央値を遅延の量と判断することにしました。録画も途中で止めずにボタンUIを押して設定を切り替えます。

 

測定結果

では測定結果を。
数値は映像表示と音が鳴る時間差です、映像表示の反映タイミングが基準です。音を鳴らす処理を実行してから実際に鳴るまでの遅延時間ではありません。
あくまで「設定を変更したらどれだけ増減するか」という相対的な意味で見てください。HDMIキャプチャによる計測ですがテレビで計測したら遅延時間が変わりました。なので実際のゲームでもこの遅延時間差になるというわけではありません。環境によって変わります、

 

設定項目は、DSP Buffer SizeはBest PerformanceとBest Latency、System Sample Rateに48,000Hzと22,050Hzをそれぞれ変えてみて測定します。ついでにDefault Speaker ModeをStereoからMonoにしてみたのも付け加えてます。フレームレートは60fps、VSyncはOnです。

DSP Buffer Size System Sample Rate Default Speaker Mode 映像に対する
音声のタイミング
Best Performance 48,000Hz Stereo +3.43フレーム
Best Latency 48,000Hz Stereo +1.35フレーム
Best Performance 22,050Hz Stereo +11.93フレーム
Best Latency 22,050Hz Stereo +3.18フレーム
Best Performance 48,000Hz Mono +4.35フレーム

1つの設定あたり21回試してその中央値を表記してます。

とりあえず、22,050HzのBest Performanceの遅延デカいな!48,000Hzと比べて8.5フレーム増加て。
48,000HzのBest PerformanceとBest Latencyの差は2.08フレームでした。バラツキの上下幅がBest PerformanceよりBest Latencyの方が小さい印象です。グラフにするとこの通り。


48,000HzのBest PerformanceでStereoからMonoにすると、+3.43フレームから+4.35フレームに遅延が増えました。22,050Hzもそうですが計算量は減るけど遅延が増えてる、何故。バッファサイズが半減した結果OS側のサウンドバッファを1周使い切るまでの時間が増えるとかですかね?

 

 

DSP Buffer Sizeの変更と音飛びの関連性

最後にこのお題、DSPバッファを小さくするとどれくらい音飛びしやすくなるのかなんですが、WindowsPCの処理パワーだとどうもよくわかりませんでした…。255音同時に鳴らそうが音飛びしない。

とりあえず、System Sample Rateが48kHzや44.1Khzの時にDSP Buffer SizeをBest Latencyにすると十数回に1回音飛びしっぱなしになることは確認しました。UnityエディタをPlayして音飛びしてないのを確認してStopしてからまたPlay、と繰り返すと十数回に1回音飛びしっぱなしになる。

 

以下、いろいろ試して結局よくわからなかったという話が続きます。

まずCPU負荷が高くなる設定として、Audio Clipを圧縮形式をVorbis、Load TypeをCompressed in Memory(圧縮状態のままメモリに置いて、再生時にデコード)にして、Audio設定のMax Real Voicesを最大の255に設定、Audio Sourceを255個再生してみましたが、DSP loadは4.0%前後、Audio Mixer Threadの1フレーム(60fps)あたりの合計は0.9ms程度で全然重くない。

ProfilerのAudioをよく見てみるとChannnelsのVirtualの項目がNoになってるのが最大32、それ以外はYes(Virtual状態で、実際には発音されてない)になっていました。Max Real Voicesの設定255が効いてない。どうもCompressed in Memory且つVorbisADPCMにするとMax Real Voicesを設定してても実際に同時に鳴る数が最大32になってしまうようですね。PCMにしたりLoad TypeをDecompress On Load(ロード時にデコード、メモリには非圧縮で置く)にするとVirtualの項目が全てNoになりました。

255音同時に鳴らしててもDSP loadは3.3%前後、Audio Mixer Threadは0.7ms程度でむしろ軽くなってます。DSP Buffer SizeをBest LatencyにしてもDSP loadは4.5%前後で音飛びしそうな感じは一切無い。使用してるPCのCPUはRyzen 7 3700Xですが、もっと遅いCPUじゃないと大した負荷にならない?

そこで別の古いPC、Core i5-3570(Ivy BridgeGPU内蔵)で試してみました。Best PerformanceだとDSP loadは6.3%、Audio Mixer Threadは0.7ms程度、Best LatencyだとDSP loadは10.3%前後、Audio Mixer Threadは1.9ms程度になりました。

 

そして、ここでBest Latencyでときどき音飛びすることに気付きました。ただ、CPU負荷関係なく数十回に1回音飛びします、Max Real Voicesを1にして1音しか鳴らしてなくても音飛びしてます。これはRyzenの方のPCでも起こってます。開始1秒程度でブチブチ音飛びして、それがそのまま続く感じです。48,000Hzや44,100Hzではこの現象が起こりますが、36,000Hzや22,050Hzでは試した範囲内では起こりませんでした。

なおGood LatencyやBest Performanceでは音飛びしませんでした。なんだかときどき起こる理由がわからないとBest Latencyは地雷的で怖くて使えませんね…。

 

CPU負荷を増やすべく、大量のキューブを物理演算で跳ねさせて数fpsくらいしか出ないほどに負荷をかけてみましたが、音飛びに関しては変わりませんでした。もうちょっと違うCPUへの負荷のかけ方を考えないといけないのかもしれない。

 

 

最後に

いろいろ確認してみて気になった所は、

・Audio処理はAudioSettings.dspTime単位で動いている

DSP Buffer Size設定Best PerformanceでのAudioSource.Playは結構なタイミング誤差がある(48kHzで誤差の上下幅21.333…ms)、1フレーム違いのPlayが同時に鳴ったりする

・ボリュームの変更は一瞬ではなく64サンプルかけて行われる(Sample Rateに関わらず64サンプル)

・AudioSettings.dspTimeの値を直接基準にしてオブジェクト等を動かしてはいけない(VSyncと合わないのでガクガクする)

・44.1~48kHzでのDSP Buffer Size設定Best Latencyは音飛びしっぱなしになることがある

あたりでしょうか。

 

一番強く思ったのは、もし音ゲーやリズムゲーを作るなら別のサウンド関連のミドルウェアやアセットを使った方が、こんな細かいことを把握しなくてもよさそう、ということです。タイミングにシビアな用途に使う場合は、Unity標準Audio機能は気をつけなきゃいけないことが色々埋まっている。

 

あ、繰り返しますがあくまでこれらは自分のPC環境によるWindowsビルドの結果です。

というか結果がどうもあやふやだなぁという事も多く、検証の仕方にあまり自信が持てません。