
マイケル
みなさんこんにちは!
マイケルです!

エレキベア
こんにちクマ〜〜〜

【Unity】サウンドミドルウェアに依存しない設計を考える【CRI ADX・Wwise】
2024-03-27

マイケル
以前の記事にて、UnityAudio、CRIADX、Wwiseで切り替え可能なサンプルプロジェクトを作成しました。
今回はその中でも「CRIADXを使用した基本動作の実装方法」について紹介していきます!
具体的には下記のような再生サンプルと、周波数データを用いたオーディオスペクトラムの実装になります。

▲サンプル1:基本機能

▲サンプル2:オーディオスペクトラム

エレキベア
前UnityAudioで実装した内容と比較すると違いが分かりやすそうクマね

【Unity】第一回 UnityAudioを使いこなす 〜サウンド再生処理編〜
2024-01-22

【Unity】第二回 UnityAudioを使いこなす 〜AudioMixer活用編〜
2024-01-22

【Unity】第三回 UnityAudioを使いこなす 〜オーディオスペクトラム描画編〜
2024-01-22

マイケル
作成したサンプルのリポジトリと、CRI関連処理は下記になります!
こちらも合わせてご参照ください!
GitHub - unity-multi-audio
▲サンプルプロジェクト
フォルダ | 内容 |
---|---|
CriAtomCraftプロジェクト | |
APIを実行する処理 | |
サンプルゲーム固有のオーディオ関連処理 | |
サンプルゲームに関する処理 | |
CriAtomCraftプロジェクトから出力したサウンドデータ |

エレキベア
中々ボリュームのあるサンプルクマ
参考書籍

マイケル
今回実装するにあたり、下記書籍を参考にさせていただきました!


マイケル
UnityAudioとCRIADXを使用した実装について紹介されています。
ゲームサウンドにフォーカスした数少ない書籍なので、ぜひご一読ください!

エレキベア
Unityでのサウンド実装について幅広く知れる一冊クマね
CRI ADX、Unityの基本設定

マイケル
まずは基本となるCRIAtomCraftとUnityでの使用方法について簡単な流れを記載します。
詳細については公式サイトのチュートリアルやマニュアルをご参照ください!
CRIAtomCraft側

マイケル
まずはCriAtomCraft側で音源を取り込み、CueSheet、Cueというものを作成します。
Cueが実際にゲーム側から指定して再生する単位で、CueSheetはCueをまとまりごとに管理するものになります。
CRI ADX2(AtomCraft) 入門編01 音データの登録、ツール上での再生

▲Cue、CueSheetの定義

マイケル
そして作成したCue等の情報をCRI独自の形式(ACF、ACB、AWB)で書き出し、それをUnity側で読み込んで使用する流れになります。
公式サイトからの引用になりますが、下記のようなイメージです。

▲流れのイメージ
形式 | 概要 |
---|---|
ACFファイル | カテゴリやBus設定など全般的な設定 |
ACBファイル | CueSheetのバイナリ情報 |
AWBファイル | ストリーム再生に設定した波形データ |

エレキベア
AtomCraftが音源とゲームエンジンの間に挟まるイメージクマね

マイケル
書き出しは「ビルド > Atomキューシートバイナリのビルド」から行います。
無料のLE版では選択できないですが、有料版ではiOS、Switchといったプラットフォームを指定して出力できます。

▲バイナリの書き出し

▲ターゲットを指定してビルドする

エレキベア
作成したデータをUnity側で使う準備ができたクマね
Unity側

マイケル
次は作成したデータをUnity側で読み込んで使用する方法についてです。
チュートリアルでいうとUnity側の入門編が該当します。


マイケル
まずはCRIの処理を行うために必要な
・CRIAtom
・LibraryInitializer
・ErrorHandler
のコンポーネントを用意し、CueSheet等の設定を行います。

▲グローバルオブジェクトの作成

マイケル
そして基本的な再生位置となるCRIAtomListenerコンポーネント、
再生対象となるCRIAtomSourceコンポーネントをそれぞれオブジェクトにアタッチし、Cue等の設定を行います。

▲Listenerコンポーネントのアタッチ(基本はカメラ)

▲Sourceコンポーネントのアタッチ(再生するオブジェクト)

エレキベア
基本的にコンポーネントを設定して扱うクマね

マイケル
あとは下記のようにコンポーネントを取得してPlay関数を呼べば再生されます。
また、3D位置による再生を行わない場合、CriAtomExPlayerを使用してコンポーネントをアタッチせずに再生することもできます。
private CriAtomSource _atomSource;
private void Start()
{
_atomSource = (CriAtomSource)GetComponent("CriAtomSource");
}
public void PlaySound()
{
_atomSource.Play();
}
▲CriAtomSourceによる再生
private CriAtomExPlayer _atomExPlayer;
private void Start()
{
_atomExPlayer = new CriAtomExPlayer();
}
public void PlaySound()
{
var atomExAcb = CriWare.CriAtom.GetAcb("[CueSheetName]");
_atomExPlayer.SetCue(atomExAcb, "[CueName]");
_atomExPlayer.Start();
}
▲CriAtomExPlayerによる再生

マイケル
今回作成したサンプルではグローバルオブジェクトの生成や初期設定等も含めて全てスクリプトで行っています。
再生処理に関しても基本的にCriAtomExPlayerを使用しますが、3D位置による再生のみCriAtomSourceをアタッチして再生するよう実装しました。

エレキベア
サウンド担当でも触れるようにコンポーネントで操作できるようになっていても、なるべくスクリプトで制御するようにした方がシーンやオブジェクトが汚れずにすみそうクマね
基本機能のサンプル実装

マイケル
では早速サンプルの実装内容について紹介します。
まずは下記の基本機能をまとめたサンプルについてです。

エレキベア
フェード再生からDSPエフェクト、音量調整まで
よくある機能がまとまっているクマね
CueSheetの読込

マイケル
まずは使用するCueSheetを読み込む必要があります。
今回は下記のようなCueSheet情報クラスを定義しました。
/// <summary>
/// CueSheet情報
/// </summary>
public class CueSheetInfo
{
/// <summary>
/// CueSheet名
/// </summary>
public string Name;
/// <summary>
/// 初期化時にロードするか?
/// </summary>
public bool IsLoadOnInitialize;
/// <summary>
/// AWBファイルが存在するか?
/// </summary>
public bool IsExistAwbFile;
/// <summary>
/// ループ再生するか?
/// </summary>
public bool IsPlayLoop = false;
}
▲CueSheet情報の定義

マイケル
そして渡されてきた情報を元にCueSheetを読み込みます。
あわせて今回はCueSheetに対応するCriAtomExPlayerを作成するように実装してみました。
/// <summary>
/// 生成したCueAtomExPlayerのキャッシュ
/// key: CueSheet名, value: CueAtomExPlayer
/// </summary>
private readonly Dictionary<string, CriAtomExPlayer> _criAtomExPlayerCache = new Dictionary<string, CriAtomExPlayer>();
・・・略・・・
/// <summary>
/// CueSheetの登録
/// </summary>
/// <param name="cueSheetInfo">CueSheet情報</param>
public void RegisterCueSheet(ICriAdxApiService.CueSheetInfo cueSheetInfo)
{
// CriAtomExPlayerの生成
CreateCriAtomExPlayer(cueSheetInfo);
// CueSheet情報を追加
var cueSheetName = cueSheetInfo.Name;
CriWare.CriAtom.AddCueSheet(
cueSheetName,
Path.Combine(_initializeSetting.AssetFilePath, $"{cueSheetName}.acb"),
cueSheetInfo.IsExistAwbFile ? Path.Combine(_initializeSetting.AssetFilePath, $"{cueSheetName}.awb") : null);
}
・・・略・・・
/// <summary>
/// CriAtomExPlayerの追加
/// </summary>
/// <param name="cueSheetInfo"></param>
/// <returns></returns>
private void CreateCriAtomExPlayer(ICriAdxApiService.CueSheetInfo cueSheetInfo)
{
if (cueSheetInfo == null || string.IsNullOrEmpty(cueSheetInfo.Name))
{
Debug.LogError($"invalid CreateCriAtomExPlayer parameters.");
return;
}
// 既に生成済か?
var cueSheetName = cueSheetInfo.Name;
if (_criAtomExPlayerCache != null
&&_criAtomExPlayerCache.ContainsKey(cueSheetName))
{
Debug.LogWarning($"already create CriAtomExPlayer => {cueSheetName}");
return;
}
// CriAtomExPlayerを生成してキャッシュ
var criAtomExPlayer = new CriAtomExPlayer();
criAtomExPlayer.Loop(cueSheetInfo.IsPlayLoop);
_criAtomExPlayerCache.TryAdd(cueSheetName, criAtomExPlayer);
}
▲Cuesheetの登録とPlayerの作成

マイケル
ちなみにCueSheetはその時使用する分だけ、動的にロード/アンロードすることもできます。
下記は読み込んだCueSheetを削除する処理です。
/// <summary>
/// CueSheetの削除
/// </summary>
/// <param name="cueSheetName">CueSheet名</param>
public void RemoveCueSheet(string cueSheetName)
{
if (!_criAtomExPlayerCache.ContainsKey(cueSheetName))
{
return;
}
// CriAtomExPlayerの削除
var criAtomPlayer = _criAtomExPlayerCache[cueSheetName];
criAtomPlayer.Dispose();
_criAtomExPlayerCache.Remove(cueSheetName);
// 設定したCueSheet情報も削除
CriWare.CriAtom.RemoveCueSheet(cueSheetName);
}
▲CueSheetの削除

エレキベア
これでCueを再生する準備ができたクマね
BGM再生

マイケル
そしてCueの再生処理は下記になります。
ボリュームやピッチ等も変更できるため、オプション指定で渡せるようにしてあります。
/// <summary>
/// 指定したCueを再生する
/// </summary>
/// <param name="cueSheetName">CueSheet名</param>
/// <param name="cueName">Cue名</param>
/// <param name="option">再生オプション</param>
public void Play(string cueSheetName, string cueName, ICriAdxApiService.SoundPlayOption option = null)
{
var criAtomExPlayer = GetCriAtomExPlayer(cueSheetName);
if (criAtomExPlayer == null)
{
Debug.LogError($"not register CueSheet => {cueSheetName}");
return;
}
// フェード設定を切り替える
var fadeTime = option?.FadeTimeMs ?? 0;
ChangeCriAtomExPlayerFadeSettings(criAtomExPlayer, fadeTime);
if (fadeTime <= 0)
{
Stop(cueSheetName);
}
// オプションに応じて再生
var atomExAcb = CriWare.CriAtom.GetAcb(cueSheetName);
criAtomExPlayer.SetVolume(option?.Volume ?? 1f);
criAtomExPlayer.SetPitch(option?.Pitch ?? 0f);
criAtomExPlayer.SetCue(atomExAcb, cueName);
var criAtomPlayback = criAtomExPlayer.Start();
_criAtomPlaybackCache[cueName] = criAtomPlayback;
}
・・・略・・・
/// <summary>
/// 指定したCueSheetの再生を停止する
/// </summary>
/// <param name="cueSheetName">CueSheet名</param>
/// <param name="option">停止オプション</param>
public void Stop(string cueSheetName, ICriAdxApiService.SoundStopOption option = null)
{
var criAtomExPlayer = GetCriAtomExPlayer(cueSheetName);
if (criAtomExPlayer == null)
{
return;
}
// フェード設定を切り替える
ChangeCriAtomExPlayerFadeSettings(criAtomExPlayer, option?.FadeTimeMs ?? 0);
// CueSheetの再生を停止する
criAtomExPlayer.Stop();
}
▲Cueの再生/停止

マイケル
フェード再生についてはAttachFader処理を呼び出すことで、Playerごとに切り換えることができます。
今回は下記のように実装しました。
/// <summary>
/// CriAtomExPlayerのフェード設定 キャッシュ情報
/// </summary>
private readonly Dictionary<CriAtomExPlayer, int> _currentFadeTimeMsCache = new Dictionary<CriAtomExPlayer, int>();
/// <summary>
/// CriAtomExPlayerのフェード設定を切り替える
/// </summary>
/// <param name="criAtomExPlayer">CriAtomExPlayer</param>
/// <param name="fadeTimeMs">フェードさせる時間</param>
private void ChangeCriAtomExPlayerFadeSettings(CriAtomExPlayer criAtomExPlayer, int fadeTimeMs = 0)
{
if (criAtomExPlayer == null)
{
return;
}
// 設定の変更がない場合は何も行わない
var isFirstSettings = !_currentFadeTimeMsCache.ContainsKey(criAtomExPlayer);
var currentFadeTime = isFirstSettings ? 0 : _currentFadeTimeMsCache[criAtomExPlayer];
if (fadeTimeMs == currentFadeTime)
{
return;
}
// フェード設定を変更
if (isFirstSettings)
{
criAtomExPlayer.AttachFader();
}
criAtomExPlayer.SetFadeInTime(fadeTimeMs);
criAtomExPlayer.SetFadeOutTime(fadeTimeMs);
_currentFadeTimeMsCache[criAtomExPlayer] = fadeTimeMs;
}
▲フェード再生設定

エレキベア
フェーダーが用意されてるのはありがたいクマね
SEの3D再生

マイケル
そして3D位置再生を行う場合にはCriAtomSourceを使用する必要がありますが、
こちらはGameObjectを受け取り、スクリプト内でアタッチするよう実装しています。
/// <summary>
/// 指定したCueを再生する(3D位置再生)
/// </summary>
/// <param name="gameObject">対象GameObject</param>
/// <param name="cueSheetName">CueSheet名</param>
/// <param name="cueName">Cue名</param>
/// <param name="option">再生オプション</param>
public void Play3d(GameObject gameObject, string cueSheetName, string cueName, ICriAdxApiService.SoundPlayOption option = null)
{
// CriAtomSourceの生成
var criAtomSource = gameObject.GetComponent<CriAtomSource>();
if (criAtomSource == null)
{
criAtomSource = gameObject.AddComponent<CriAtomSource>();
criAtomSource.cueSheet = cueSheetName;
criAtomSource.use3dPositioning = true;
}
// フェード設定を切り替える
var criAtomExPlayer = criAtomSource.player;
ChangeCriAtomExPlayerFadeSettings(criAtomExPlayer, option?.FadeTimeMs ?? 0);
// オプションに応じて再生
criAtomSource.cueName = cueName;
criAtomSource.volume = option?.Volume ?? 1f;
criAtomSource.pitch = option?.Pitch ?? 0f;
var criAtomPlayback = criAtomSource.Play();
_criAtomPlaybackCache[cueName] = criAtomPlayback;
}
▲SEの3D位置再生

エレキベア
use3dPositioningフラグも設定する必要があるのクマね
音量調整

マイケル
ミキサーによる音量調整については、今回はカテゴリを定義して振り分けることで調整しました。
下記のようにBGM、SEのカテゴリを作成しています。
参考:CRI ADX2(AtomCraft) 初級編09 カテゴリ設定

▲BGM、SEのカテゴリ振り分け

マイケル
これらのボリュームを取得/設定する関数は下記になります。
マスターボリュームに関してはBusから取得できるので、Busとカテゴリの2つを用意しました。
/// <summary>
/// 指定バスのボリューム取得
/// </summary>
/// <param name="busName">バス名</param>
/// <returns></returns>
public float GetBusVolume(string busName)
{
CriAtomExAsr.GetBusVolume(busName, out var volume);
return volume;
}
/// <summary>
/// 指定バスのボリューム設定
/// </summary>
/// <param name="busName">バス名</param>
/// <param name="volume">ボリューム</param>
public void SetBusVolume(string busName, float volume) => CriAtomExAsr.SetBusVolume(busName, volume);
/// <summary>
/// 指定カテゴリのボリューム取得
/// </summary>
/// <param name="categoryName">カテゴリ名</param>
/// <returns></returns>
public float GetCategoryVolume(string categoryName) => CriWare.CriAtom.GetCategoryVolume(categoryName);
/// <summary>
/// 指定カテゴリのボリューム設定
/// </summary>
/// <param name="categoryName">カテゴリ名</param>
/// <param name="volume">ボリューム</param>
public void SetCategoryVolume(string categoryName, float volume) => CriWare.CriAtom.SetCategoryVolume(categoryName, volume);
▲バス、カテゴリボリュームの取得/設定
/// <summary>
/// Masterボリューム
/// </summary>
public float MasterVolume
{
get => _criAdxApiService.GetBusVolume(GameCriAdxAudioSettings.CriAdx.BusName.Master);
set => _criAdxApiService.SetBusVolume(GameCriAdxAudioSettings.CriAdx.BusName.Master, value);
}
/// <summary>
/// BGMボリューム
/// </summary>
public float BgmVolume
{
get => _criAdxApiService.GetCategoryVolume(GameCriAdxAudioSettings.CriAdx.CategoryName.Bgm);
set => _criAdxApiService.SetCategoryVolume(GameCriAdxAudioSettings.CriAdx.CategoryName.Bgm, value);
}
/// <summary>
/// SEボリューム
/// </summary>
public float SeVolume
{
get => _criAdxApiService.GetCategoryVolume(GameCriAdxAudioSettings.CriAdx.CategoryName.Se);
set => _criAdxApiService.SetCategoryVolume(GameCriAdxAudioSettings.CriAdx.CategoryName.Se, value);
}
▲ボリュームの紐付け

エレキベア
CueSheetの他にカテゴリ分け出来るのは便利クマね
ミキサー的な使い方もできるクマ
DSPエフェクトの適用

マイケル
エフェクトの適用については、
・バスを新たに定義してエフェクトを追加する
・各バスのパラメータ値をスナップショットとして保存する
といった手順で用意しました。
ざっくりにはなりますが下記のようなイメージになります。
参考:CRI ADX2(AtomCraft) 中級編17 DSPバス(ミキサー)とエフェクトの追加

▲リバーブのスナップショット設定

▲ディストーションのスナップショット設定

マイケル
実装は簡単でApplyDspBusSnapshotに対してスナップショット名と遷移させる時間を渡すことで切り換えることができます。
/// <summary>
/// DSPバスのスナップショットを切り替える
/// </summary>
/// <param name="snapshotName">スナップショット名</param>
/// <param name="fadeTimeMs">フェードさせる時間</param>
public void ChangeBusSnapshot(string snapshotName, int fadeTimeMs)
{
CriAtomEx.ApplyDspBusSnapshot(snapshotName, fadeTimeMs);
}
▲snapshotの切り替え処理
/// <summary>
/// BGMエフェクト変更
/// </summary>
/// <param name="effectType">エフェクトタイプ</param>
public void ChangeBgmEffect(IGameAudioService.EffectType effectType)
{
switch (effectType)
{
case IGameAudioService.EffectType.Normal:
_criAdxApiService.ChangeBusSnapshot(GameCriAdxAudioSettings.CriAdx.BusSnapshotName.Normal, 1000);
break;
case IGameAudioService.EffectType.Reverb:
_criAdxApiService.ChangeBusSnapshot(GameCriAdxAudioSettings.CriAdx.BusSnapshotName.BgmReverb, 1000);
break;
case IGameAudioService.EffectType.Distortion:
_criAdxApiService.ChangeBusSnapshot(GameCriAdxAudioSettings.CriAdx.BusSnapshotName.BgmDistortion, 1000);
break;
}
}
▲エフェクト変更処理

エレキベア
UnityAudioに用意されてるスナップショット機能と似たイメージクマね
オーディオスペクトラムの実装

マイケル
基本的な再生処理は以上になりますが、最後に周波数データを取得してオーディオスペクトラムを描画する方法についても紹介します。

エレキベア
これはUnityAudioでも同じことが出来たクマね

マイケル
CRIではCriAtomExOutputAnalyzerを使用して再生中の音源の周波数データを取得することができます。
実装は下記のようになります。
参考:CRIWARE Unity Plugin Manual スペクトル解析結果を対数周波数スケールに変換する
/// <summary>
/// スペクトラムアナライザ
/// </summary>
private CriAtomExOutputAnalyzer _spectrumAnalyzer;
/// <summary>
/// スペクトラムアナライザの設定
/// CriAtomExPlayerを再生する前に紐づけておく必要がある
/// </summary>
/// <param name="cueSheetName">CueSheet名</param>
/// <param name="sampleCount">サンプル数</param>
public void SetSpectrumAnalyzer(string cueSheetName, int sampleCount)
{
if (!_criAtomExPlayerCache.ContainsKey(cueSheetName))
{
return;
}
var criAtomExPlayer = _criAtomExPlayerCache[cueSheetName];
// 既に作成されていたらデタッチ、破棄する
_spectrumAnalyzer?.DetachExPlayer();
_spectrumAnalyzer?.Dispose();
// スペクトラムアナライザ用の設定
var config = new CriAtomExOutputAnalyzer.Config();
config.enableSpectrumAnalyzer = true;
config.numSpectrumAnalyzerBands = sampleCount;
_spectrumAnalyzer = new CriAtomExOutputAnalyzer(config);
_spectrumAnalyzer.AttachExPlayer(criAtomExPlayer);
}
/// <summary>
/// スペクトラムアナライザから周波数データを取得する
/// </summary>
/// <param name="sampleCount">サンプル数</param>
/// <param name="isConvertDecibel">デシベル値に変換するか?</param>
/// <returns></returns>
public float[] GetSpectrumData(int sampleCount, bool isConvertDecibel)
{
var result = new float[sampleCount];
if (_spectrumAnalyzer == null)
{
Debug.LogError("not called 'CreateSpectrumAnalyzer' method.");
return result;
}
// 周波数データを取得
_spectrumAnalyzer.GetSpectrumLevels(ref result);
// デシベルに変換
// https://game.criware.jp/manual/unity_plugin/latest/contents/classCriWare_1_1CriAtomExOutputAnalyzer.html#a6b99d6b5310af38efe20ff834c59c4e0
if (isConvertDecibel)
{
result = result.Select((value) =>
{
if (value <= Mathf.Epsilon)
{
return 0f;
}
return 20.0f * Mathf.Log10(value);
}).ToArray();
}
return result;
}
▲周波数データの取得
/// <summary>
/// スペクトラムアナライザの設定
/// </summary>
/// <param name="eventName">サウンドイベント名</param>
/// <param name="sampleCount">サンプル数</param>
/// <returns>作成に成功したか?</returns>
public bool SetSpectrumAnalyzer(string eventName, int sampleCount)
{
_criAdxApiService.SetSpectrumAnalyzer(GameCriAdxAudioSettings.CriAdx.GetCueSheetName(eventName), sampleCount);
return true;
}
/// <summary>
/// スペクトラムアナライザから周波数データを取得する
/// </summary>
/// <param name="sampleCount">サンプル数</param>
/// <param name="isConvertDecibel">デシベル値に変換するか?</param>
/// <returns></returns>
public float[] GetSpectrumData(int sampleCount, bool isConvertDecibel)
{
return _criAdxApiService.GetSpectrumData(sampleCount, isConvertDecibel);
}
▲アナライザの設定とデータ取得

エレキベア
実装方法もシンプルで分かりやすいクマね

マイケル
取得した周波数データを使用して具体的にどう描画するか?については下記記事にまとめてありますので、気になった方はぜひこちらもご参照ください!

【Unity】第三回 UnityAudioを使いこなす 〜オーディオスペクトラム描画編〜
2024-01-22

エレキベア
実装方法もまた違って見比べてみると面白いクマ〜〜
おわりに

マイケル
というわけで今回はCRIの基本的な使用方法についてでした!
どうだったかな?

エレキベア
ツール側でいろいろ用意されている分、UnityAudioの時と比べて実装がシンプルになった気がするクマ〜〜

マイケル
ツールでの操作は覚えないといけないけど、サウンド担当とプログラマの実装を分担できるのは大きな魅力だよね!

マイケル
次回は、サウンドミドルウェアの醍醐味である「インタラクティブミュージック」の実装について紹介していこうと思います!
お楽しみに!!

エレキベア
クマ〜〜〜〜〜
【Unity】第一回 CRI ADXを使用したサウンド制御 〜基本動作、周波数解析編〜 〜完〜

【Unity】第一回 UnityAudioを使いこなす 〜サウンド再生処理編〜
2024-01-22

【Unity】第二回 UnityAudioを使いこなす 〜AudioMixer活用編〜
2024-01-22

【Unity】第一回 Wwiseを使用したサウンド制御 〜基本動作編〜
2024-03-30

【Unity】第二回 Wwiseを使用したサウンド制御 〜インタラクティブミュージック編〜
2024-03-30

【Unity】サウンドミドルウェアに依存しない設計を考える【CRI ADX・Wwise】
2024-03-27