Unity標準オーディオ+UniRxでBGMの黒フェード、白フェードをいい感じに実装する


## はじめに

[川口@CRI ADX2 Portal](https://twitter.com/CRI_kawaguchi)様が

![スクリーンショット 2018-02-22 15.19.19.png](https://qiita-image-store.s3.amazonaws.com/0/18557/9b84755d-b886-6131-1e4e-796bb6232bfd.png "スクリーンショット 2018-02-22 15.19.19.png")

という[ツイート](https://twitter.com/CRI_kawaguchi/status/964680245293953024)をしておられたので、それをなるべくUnity標準の機能で実装して見たいと思います。
結構めんどくさいので上司が許してくれるならミドルウェア使いましょう（ステマ）
この実装は前回[UniRx+Zenjectでクロスフェード](https://qiita.com/dTAT/items/d2252b16059b9d300b81)の続きにあたりますので、細かいところはそちらもご覧ください。

また本内容のデモを[Github](https://github.com/dTAT/AudioCrossFadeWithUniRx)にて公開しておりますので、「動いてるの見ないとわけわからん！」方はご参照ください。

UniRxって書いたけどそんなには使いません。ごめんなさい。

## 仕様を考える

ざっくりどうやるかというと「ホワイトアウトするときはBGMのAudioMixerをハイパス聞いたやつに変更してからやる」というだけです。どうせならホワイトアウトだけでなくブラックアウトも演出変えて見ましょう。ブラックアウトの時はローパスつけてやりましょう。ホワイトアウトはフワーッと音が消えていくイメージ、ブラックアウトはライブハウスから外に出る時に段々音がこもるイメージですね。

- AudioMixerのBGM Groupにハイパス、ローパスフィルタつける
- ハイパスの効いたSnapshotを作る
- ローパスの効いたSnapshotを作る
- ホワイトアウトの際はハイパス効いたSnapshotに変える
- ブラックアウトの際はローパス効いたSnapshotに変える
- フェードアウト終わったら元のSnapshotに戻す

というのが仕様となります。

## Snapshotを作る

そういえば前回説明を省いてしまったのでちょっと丁寧に書いておきます。

### AudioMixerとはGrouupとは

Unityメニュー>Window>AudioMixerにあります。
開くとこんな感じのWindowが開きます。これがMixerと言って音量調整などをやりやすくしてくれる機能です。

![スクリーンショット 2018-02-22 15.43.32.png](https://qiita-image-store.s3.amazonaws.com/0/18557/24540d89-e19a-7071-00ef-7cfc71e888e4.png "スクリーンショット 2018-02-22 15.43.32.png")

左側のGroupsという欄の横にある+ボタンを押すと新たにGroupを作ることができます。デフォルトではMasterしかないのでMusicというGroupを作ります。

再生するAudioSourceのOutputとしてこのGroupを設定してあげるとそのGroupの音量などの設定を継承した音がなります。

![スクリーンショット 2018-02-22 15.44.32.png](https://qiita-image-store.s3.amazonaws.com/0/18557/f7afaf4b-097b-bf1e-d0f4-bdd4b58a258a.png "スクリーンショット 2018-02-22 15.44.32.png")

こういう風にしてあげるとこのAudioSourceはMusic Groupの情報を使って発音されます。ちょっとBGMは控えめにしたいなーって時はGroupの音量を小さくしてあげれば、Music GroupをOututに設定されている音全てを同様に小さくすることが可能です。(個別に設定する必要なんて無いんですよ！！)


### Snapshotとは

SnapshotというのはMixer Windowの左側にあるSnapshotのことで、音量状態をキャプチャしておく機能です。
例えばある場面ではBGMを控えめにして音声を聞こえやすくして、別の場面ではBGMを前面に出したいとします。その場合、BGM Groupの音量を小さくしたSnapshotとBGM Groupの音量を大きくしたSnapshotを用意しておいて、スクリプトから場面ごとに切り替えてあげればいいわけです。
実践的なグループ設計としてはBGMの他にSYSTEM SE、BATTLE SE、VOICEなどのグループを分けて使ったりします。

### ホワイトアウト、ブラックアウトのSnapshotを作る

特に難しいことはないです。作ったMusic Groupを右クリック>Add Effect>Lowpass Simpleを選ぶとローパスが、Hipass Simpleを選ぶとハイパスフィルタがつけられます

![スクリーンショット 2018-02-22 15.43.47.png](https://qiita-image-store.s3.amazonaws.com/0/18557/97b0f0f7-2ee7-802a-d1bb-56b4d0a2e6df.png "スクリーンショット 2018-02-22 15.43.47.png")

これでMusic Groupはハイパス、ローパス両方がかけられるGroupとなりました。あとはSnapshotでこのハイパス、ローパスの値をSnapshotに設定しましょう。

#### ホワイトアウト

まずSnapshotの横の+ボタンを押します。新しいSnapshotができるので一つ目はWhiteoutと名前をつけます。Snapshot一覧でWhiteoutを選択した状態でMusic Groupを選択するとインスペクタでパラメータをいじることができます

![スクリーンショット 2018-02-22 16.37.34.png](https://qiita-image-store.s3.amazonaws.com/0/18557/c608765f-4400-db62-cdc4-e7d6b8f77bb0.png "スクリーンショット 2018-02-22 16.37.34.png")


とりあえずこんな感じにして見ました。1.6K~22Kだけが通るようになっております。結構わかりやすくシャリシャリした味わいの音にします。この辺を記号的にわかりやすくするか、まろやかにするかはコンポーザーさんの趣味でどうぞ。

####  ブラックアウト

同じように新しいSnapshotを作り、Blacoutと名付けます。
こちらは以下のようなパラメータにして見ましょうか。

![スクリーンショット 2018-02-22 16.37.27.png](https://qiita-image-store.s3.amazonaws.com/0/18557/148873b7-e7c4-34f9-233d-b7ebb54e4ac2.png "スクリーンショット 2018-02-22 16.37.27.png")


10~400Hzしか通らない。これはエンジニアが勝手にやるとコンポーザーさんがブチギレるやつですね。今回はわかりやすくするためなのでやっちゃいましょう。

#### デフォルト

通常状態も作っておきましょう。DefaultのSnapshotはこんな感じでしょうか

![スクリーンショット 2018-02-22 16.37.41.png](https://qiita-image-store.s3.amazonaws.com/0/18557/9f57a50c-dedf-7cb5-822c-ff7d90f706e1.png "スクリーンショット 2018-02-22 16.37.41.png")


10~22kだけ通る。これは事前にコンポーザーと握っておきましょう。現状のUnityにおけるSnapshotではEffectを無効にすることができません。パラメータを振り切ることで擬似的に無効にします。
「演出的にこういうのやりたい、やるとこういう制約がつくので音作りの時には注意してください」ってのを調整するのも大切な仕事です。

ちなみにお気付きの方いらっしゃるかと思いますが、各スナップショットで微妙にVolume値を変えてます。これは調整に使った曲を各スナップショットで聴き比べた際に同じくらいのうるささになるように調整するためです。僕のセンス的にはこれくらい偏ったのが好きです。

## 切り替える仕組みを作る

### パラメータの持ち方

今回はシンプルにシーンの情報の中に入れることにします。
フェードイン、フェードアウトに合わせてフェードアウト時に使用するSnapshotをシーン情報に登録しておきます。

```SceneComponent.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Audio;
using Zenject;

public class SceneComponent : MonoBehaviour {
	/// <summary>
	/// 再生するシーン名
	/// </summary>
	public string SceneName;
	/// <summary>
	/// フェードインする際の色
	/// </summary>
	public Color FadeinColor;
	/// <summary>
	/// フェードインする時間
	/// </summary>
	public float FadeinDuration;
	/// <summary>
	/// フェードアウトする際の色
	/// </summary>
	public Color FadeoutColor;
	/// <summary>
	/// フェードアウトする時間
	/// </summary>
	public float FadeoutDuration;
	/// <summary>
	/// フェードアウトする際のSnapshot
	/// </summary>
	public AudioMixerSnapshot FadeoutSnapshot;

	[Inject] MusicController musicController;
	public string MusicName;
	/// <summary>
	/// シーン開始時の処理
	/// </summary>
	public void EntertScene () {
		Debug.Log ("START SCENE:" + SceneName);
		if (!string.IsNullOrEmpty (MusicName)) {
			musicController.Play (MusicName);
		}
	}
	/// <summary>
	/// シーン終了時の処理
	/// </summary>
	public void ExitScne () {
		Debug.Log ("EXIT SCENE:" + SceneName);
		musicController.StopAll ();
		musicController.SetMixerSnapshot (FadeoutSnapshot, FadeoutDuration);
	}
	/// <summary>
	/// シーン終了完了時の処理
	/// </summary>
	public void OnCompleteExit () {
		musicController.ResetMixerSnapshot (FadeoutSnapshot, 0.04f);
	}
}
```

な感じのスクリプトをはりつけてインスペクタからパラメータをいじります。

![スクリーンショット 2018-02-22 16.53.19.png](https://qiita-image-store.s3.amazonaws.com/0/18557/e55076c6-7fa3-9873-a12c-acccc9a3938b.png "スクリーンショット 2018-02-22 16.53.19.png")

スクリプトからSnapshotをとる場合は[文字列で取得](https://docs.unity3d.com/ja/2017.3/ScriptReference/Audio.AudioMixer.FindSnapshot.html)しますが、めんどくさいのでインスペクタで指しておきます。

### Snapshotの切り替え方


Snapshot同士の切り替えはAudioMixer.TransitionToSnapshots()で行います。複数のSnapshot同士のミックスも行えるのですが、今回はDefault <-> Blackout/Whiteoutさえできればいいので簡単な実装にしてます

```MusicController.cs
	/// <summary>
	/// スナップショットを切り替える
	/// </summary>
	/// <param name="src">切り替え元スナップショット</param>
	/// <param name="dst">切り替え先スナップショット</param>
	/// <param name="duration">切り替え時間</param>
	void TeransitionSnapshot (AudioMixerSnapshot src, AudioMixerSnapshot dst, float duration) {
		AudioMixerSnapshot[] ss = { src, dst };
		float[] w = { 0.0f, 1.0f };
		defaultMixer.TransitionToSnapshots (ss, w, duration);
	}
```

あとは上記の関数を利用してDefault <-> Blackout/Whiteoutを切り替えるAPIを作っておきます。

```MusicController.cs
	/// <summary>
	/// デフォルトのミキサーとそのスナップショット
	/// Set関数でスナップショットを変化させることを許容し、
	/// Reset関数で戻すことを要求する
	/// </summary>
	[SerializeField]
	AudioMixer defaultMixer;
	[SerializeField]
	AudioMixerSnapshot defaultSnapshot;
	/// <summary>
	/// スナップショットをデフォルトに戻す
	/// </summary>
	/// <param name="srcSnSnapshot">現スナップショット</param>
	/// <param name="duration">切り替え時間</param>
	public void ResetMixerSnapshot (AudioMixerSnapshot srcSnSnapshot, float duration) {
		TeransitionSnapshot (srcSnSnapshot, defaultSnapshot, duration);
	}
	/// <summary>
	/// スナップショットをデフォルトから切り替える
	/// </summary>
	/// <param name="dstMixerSnapshot">切り替え先スナップショット</param>
	/// <param name="duration">切り替え時間</param>
	public void SetMixerSnapshot (AudioMixerSnapshot dstMixerSnapshot, float duration) {
		TeransitionSnapshot (defaultSnapshot, dstMixerSnapshot, duration);
	}
```

あとはシーンのフェードアウトが開始された瞬間にSetMixerSnapshot()してフェードアウトが完了したタイミングでResetMixerSnapshot()する流れができれば完成です。

### シーンの切り替えの流れ

####見た目のフェード

正直見た目のフェード処理は今回どうでもいいので省きたいところですが一応掲載します

```TransitionScreen.cs
using System.Collections;
using System.Collections.Generic;
using UniRx;
using UniRx.Triggers;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;

/// <summary>
/// 遷移時のスクリーン
/// </summary>
public class TransitionScreen : MonoBehaviour {
	Image screenImage;
	Color currentColor = Color.black;
	public UnityEvent OnTransitionComplete = new UnityEvent ();
	[SerializeField]
	AnimationCurve openCurve;
	[SerializeField]
	AnimationCurve closeCuve;

	private void Awake () {
		screenImage = GetComponent<Image> ();
		screenImage.color = currentColor;
	}
	/// <summary>
	/// スクリーンを開ける＝フェードイン
	/// </summary>
	/// <param name="color">フェードイン色</param>
	/// <param name="duration">フェードイン時間</param>
	public void Open (Color color, float duration) {
		screenImage.raycastTarget = true;
		color.a = 0.0f;
		Observable
			.FromCoroutine<Color> (
				o => TransitionColor (o, currentColor, color, openCurve, duration))
			.Subscribe (c => screenImage.color = c, () => {
				screenImage.raycastTarget = false;
				OnTransitionComplete.Invoke ();
				currentColor = color;
			});
	}
	/// <summary>
	/// スクリーンを閉める=フェードアウト
	/// </summary>
	/// <param name="color">フェードアウト色</param>
	/// <param name="duration">フェードアウト時間</param>
	public void Close (Color color, float duration) {
		screenImage.raycastTarget = true;
		Observable
			.FromCoroutine<Color> (
				o => TransitionColor (o, currentColor, color, openCurve, duration))
			.Subscribe (c => screenImage.color = c, () => {
				OnTransitionComplete.Invoke ();
				currentColor = color;
			});
	}
	/// <summary>
	/// 色遷移Coroutine
	/// </summary>
	/// <param name="observer">色フェードオブザーバ</param>
	/// <param name="begin">開始色</param>
	/// <param name="end">終了色</param>
	/// <param name="curve">遷移カーブ</param>
	/// <param name="duration">遷移時間</param>
	/// <returns></returns>
	IEnumerator TransitionColor (IObserver<Color> observer, Color begin, Color end, AnimationCurve curve, float duration) {
		var timer = duration;
		while (timer > 0) {
			timer -= Time.deltaTime;
			var r = curve.Evaluate (timer / duration);
			var c = Color.Lerp (end, begin, r);
			observer.OnNext (c);
			yield return null;
		}
		observer.OnCompleted ();
	}
}
```

スクリーンを開ける/閉めるが完了したらOnTransitionCompleteのEventをInvoke()してくれる感じです。

あ！ここでUniRx使ってますよ！UniRx万歳！

#### シーンの遷移...

```IllustController.cs
	IEnumerator SwitchSene (SceneComponent sceneExit, SceneComponent sceneEnter) {
		if (sceneExit != null) {
			bool closed = false;
			UnityEngine.Events.UnityAction wait = () => {
				closed = true;
			};
			transitionScreen.OnTransitionComplete.AddListener (wait);
			//シーンの終了処理を行う
			sceneExit.ExitScne ();
			//画面をフェードアウトさせる
			transitionScreen.Close (sceneExit.FadeoutColor, sceneExit.FadeoutDuration);
			//フェード完了までまつ
			while (!closed) yield return null;
			transitionScreen.OnTransitionComplete.RemoveListener (wait);
			//フェード完了後処理をする
			sceneExit.OnCompleteExit ();
			sceneExit.gameObject.SetActive (false);
		}
		if (sceneEnter != null) {
			bool closed = true;
			UnityEngine.Events.UnityAction wait = () => {
				closed = false;
			};
			sceneEnter.gameObject.SetActive (true);
			transitionScreen.OnTransitionComplete.AddListener (wait);
			//シーンの開始処理をする
			sceneEnter.EntertScene ();
			//画面をフェードインさせる
			transitionScreen.Open (sceneEnter.FadeinColor, sceneEnter.FadeinDuration);
			//フェードインをまつ
			while (closed) yield return null;
			transitionScreen.OnTransitionComplete.RemoveListener (wait);
		}
	}
```

これこそUniRxを使うと綺麗にかけそうな部分なのですが自分の理解が足りないのでスパゲティーCoroutineです。画面のフェードと各シーンの開始/終了処理を叩いてくれます。これにより画面のフェードイン/アウトと同時にシーンのBGM再生/停止が行われます。

動かしてあげるとBGMの停止&フェードアウトと設定されたSnapshotへの切り替えが同時に行われるかと思います。

## まとめ

基本的な処理部分はSnapshotを作る、Snapshotを切り替える、だけですので実はそんなにコード書くところありません。今回掲載したコードもほとんどがそれ以外の制御部分です。なので実際にミドルウェアのイベントをキックするのと大した差はありません。

が、「Snapshotの切り替えをもうちょっといい感じにしたい〜」とか言われた時のことや、Snapshot設定の制約などを考えるとミドルウェアの採用もアリなんじゃないでしょうか。

ちなみに[AudioMixerのネイティブプラグイン](https://bitbucket.org/Unity-Technologies/nativeaudioplugins)はめちゃくちゃすごいです。

## 参考
- [AudioMixer -Unityマニュアル](https://docs.unity3d.com/ja/2017.3/Manual/AudioMixer.html)
- [AudioMixer　用途と API の概要-Unityマニュアル](https://docs.unity3d.com/ja/2017.3/Manual/AudioMixerUsage.html)