見出し画像

サウンドをクロスフェードする際の音量トラップについて

この記事は「GRIMOIRE アドベントカレンダー2021 ODD」の5つ目の記事です

今回の記事はグリモアのRe:プロデューサーチーム所属のジュニアが担当します。

我らグリモアは優れた『悪の組織』となるため、日々高みを目指す【挑戦する文化】があります。
その中には「恐ろしく〇〇な点、俺でなきゃ見逃しちゃうね」と言いたくなるような細かな異変や壁に気がつき、乗り越えていくプロ意識があります。
今回はそんなプロ意識の元、サウンドをクロスフェードする際に発生する音量トラップについてご紹介します!

自己紹介

2021年3月に入社したゲーム業界歴5年目の駆け出しエンジニアです!
自分は新卒時代からUnityを使ったクライアントエンジニアで、現在もグリモアでクライアント業務に携わっております。

ジュニアと名乗っていますが、苗字が千原というわけではないです。
勝手にそう名乗っているので悪しからず。

サウンドのクロスフェードについて

AWSの話、キーボードの話ときて、今回はサウンドのクロスフェードについて書こうと思います。脈略無いけど気にしない方向。

サウンドのクロスフェードとは

「再生中BGMのフェードアウトと、新たに再生するBGMのフェードインを同時に行い、自然にBGMを切り替える演出」です。
(↓サンプル、下記リンクはmp3を開きます)

https://grmr.co.jp/event/adventcalendar/2021/maoudamashii_crossfade_test.mp3

BGM:MaouDamashii
https://maou.audio/

Unityで実装した場合の具体的な例を見ていきましょう。

[SerializeField] AudioSource audioSource1;
[SerializeField] AudioSource audioSource2;

public void SwapBGM()
{
	var fadeOutSource = GetPlayingAudioSource();	// 再生中のAudioSourceを取得する
	var fadeInSource = GetStandbyAudioSource();	// 再生していないAudioSourceを取得する
	CrossFadeBGM(fadeInSource, fadeOutSource, 1.0f).Forget();
}

async UniTask CrossFadeBGM(AudioSource fadeInSource, AudioSource fadeOutSource, float duration)
{
	fadeInSource.volume = 0;
	fadeInSource.Play();
	float initVol = fadeOutSource.volume;
	for (float time = 0; time < duration; time += Time.deltaTime)
	{
		fadeInSource.volume = Mathf.Lerp(0, 1, time / duration);
		fadeOutSource.volume = Mathf.Lerp(initVol, 0, time / duration);

		await UniTask.WaitForEndOfFrame();
                /* ~中断チェックは省略~ */
	}

	fadeInSource.volume = targetVolume;
	fadeOutSource.volume = 0;
}

上記例は各BGMの音量を経過時間に応じて直線的に変化させた実装です。
簡素に書くならこんな実装になるでしょう、CrossFadeBGM()の引数で音量を指定するのも良さそうです。
実際、Web記事や入門書本等で紹介されるクロスフェードはこのような実装が多い印象です。

一方で、下記の演出を見てみましょう。(下記リンクは動画を開きます)

https://grmr.co.jp/event/adventcalendar/2021/track_add_action.mp4

上の動画は、再生されているBGMに対して楽器が後から追加されていくような演出で、[Swap]ボタンを押すと、楽器が入ったり出たりします。
実際にやっていることは二つの曲をクロスフェードで切り替えているだけになります。

しかし!このような演出をする際、上記のような実装をしてはいけません!!!

この記事では主にその理由と解決策を書いていこうと思います。

トラック追加演出に↑の実装を使ってはいけない理由

その理由は
BGMがαからβにクロスフェードする最中、音量が小さくなっているから

最終的には元の音量になりますが、切り替わっている最中は音量がだんだん下がって〜だんだん上がって〜(元にもどって)みたいな感じで変わってしまいます。

もしBGM αとβが脈略のないBGM同士であれば、さほど気にはならないでしょう。
しかし、時間を合わせて切り替えたり、今回のような楽器が増える演出では、対象の楽器以外に変化を感じさせたくありません。
ゲームプレイ中の感の良い人に「トラックごと変えてるな」とか思われては、曲を作ってくれた人に申し訳なさで切なくなります(1敗)。

では「何故、音量が小さくなるのか」ですが、その理由は音量の単位にあります。
Unityの音量はClipデータの再生位置音量によって決まり、この音量は主に音圧レベル:デシベル(dB)で取り扱われます。
AudioSource.volumeはこの音量割合を操作しており、0なら無音、1ならClipデータの音量そのまま再生するので、上記実装はこれを利用しています。

そして、このデシベルの数値と実際の音量が比例しない点が音量を小さくしてしまう原因になります!
例えば20dBと20dBの音を足しても、40dBとはなりません。
実際にどういう値になるのかは、公害防止管理者受験対策(kougai.net)の解説が分かりやすいと思います!

騒音の単位 dB(デシベル)の足し算の簡単な計算方法!!

すなわち、volume値が0.5のAudioSourceを二つ鳴らしても、volume値が1.0のAudioSourceと同じ音量にならないのです!

(あっちなみに0.5の音量 + 0.5の音量が1.0の音量に聞こえる単位もあります、詳しくは「ラウドネスレベル」で調べてみてください)

じゃあどうすりゃいいの!

デシベルの話をしておきながら何ですが、アプローチを音のエネルギーに変えて解決することが出来ます。

音のエネルギーは絶対値の2乗で定義する事ができ、冒頭の実装例によるクロスフェード時のエネルギーは以下のような式になります。

$${A(t), B(t) = クロスフェード中の時間による係数、要はtとt-1です}$$
$${S1(t), S2(t)=BGMα,βの音量}$$

$$
|A(t)S1(t)|^2 + |B(t)S2(t)|^2
$$

S1(t), S2(t)を1とし、この計算結果をグラフにすると、見事中央が凹んだグラフになり、曲が切り替わる最中は音のエネルギーも小さくなっていることがわかります。

この音のエネルギーを一定にできれば音量は一定になりそうです。
一定になる2乗の関係といえば三角関数の使い所でしょう。

$$
|sin^2|+|cos^2|
$$

上のA(t), B(t) をそれぞれsin^2、cos^2に当て嵌めれば、グラフはこうなり、1を保ちます。

具体的な実装例

やり方がわかったので、具体的にコードへ写すとこんな感じです。

async UniTask CrossFadeBGM(AudioSource fadeInSource, AudioSource fadeOutSource, float duration, float targetVolume)
{
	fadeInSource.volume = 0;
	fadeInSource.Play();
	float firstVol = fadeOutSource.volume;
	for (float time = 0; time < duration; time += Time.deltaTime)
	{
		float value = Mathf.Cos(time / duration * 2f);
		fadeInSource.volume  = (1 - value) / 2 * targetVolume;
		fadeOutSource.volume = (1 + value) / 2 * firstVol;
		await UniTask.WaitForEndOfFrame();
	}
    
	fadeInSource.volume = targetVolume;
	fadeOutSource.volume = 0;
}

そして、こちら↓が上記実装を入れてみた比較動画です。(下記リンクは動画を開きます)

https://grmr.co.jp/event/adventcalendar/2021/crossfade_fix.mp4

上記動画は、同じBGM(というかビープ音)へ5秒かけてクロスフェードする様子です。

右上のボタンは音量が変わってしまうクロスフェード(本記事冒頭処理)、
右下は上記の音量調整版クロスフェード処理で遷移しています。

右上のボタンを押した際、音量が最初より小さくなってしまう様子と
右下のボタンを押した際、音量の変化がない様子がわかると思います。

まとめ

  • クロスフェードの演出は、場面によって処理を気をつけなければいけない

  • volume=0.5のAudioSourceを二つ鳴らしても、volume=1.0のAudioSourceと同じ音量にはならない

  • フェードアウトをcos^2(t)に、フェードインをsin^2(t)で表現すると、一定の音量を保ったままクロスフェードできる

最後に

いかがでしたでしょうか!
こういった粗は意外と目につく、、耳につくものです!
改めて、サウンドに意識を向ける機会を設けてみてはいかがでしょうか!

ここまでお付き合いいただき、本当にありがとうございます!
ということで、グリモアは一緒に【中二病を救う】側になってくれる仲間を大大大募集中です!
少しでも当社に興味を持って頂けましたら、是非とも下記の採用サイトを御覧ください!

次回はmasamasaさんによる「ゲーム開発ツールの選定」です!お楽しみに!(仮なので変わるかも?)

※各社の会社名、製品名、サービス名は各社の商標または登録商標です。

読んでくださりありがとうございま――…… え?さぽーと…?いやいやいや!そんな恐れ多いですよ!でも、サポートいただけると、ゲーム開発が少しだけ楽になるかも…… あ!ごめんなさい、独り言ですっ!えへへへ……