ゲーム開発
Unity
UnrealEngine
C++
ゲーム数学
ゲームAI
サウンド
アニメーション
GBDK
制作日記
3DCG
Houdini
Blender
USD
グラフィックス
テクノロジ
ツール開発
フロントエンド関連
サーバサイド関連
ソフトウェア設計
ハードウェア関連
おすすめ技術書
音楽
DTM
楽器・機材
ピアノ
その他
都会のエレキベア
ラーメン日記
四コマ漫画
おすすめアイテム
おもしろコラム
  • ゲーム開発
    • Unity
    • UnrealEngine
    • C++
    • ゲーム数学
    • ゲームAI
    • サウンド
    • アニメーション
    • GBDK
    • 制作日記
  • 3DCG
    • Houdini
    • Blender
    • USD
    • グラフィックス
  • テクノロジ
    • ツール開発
    • フロントエンド関連
    • サーバサイド関連
    • ソフトウェア設計
    • ハードウェア関連
    • おすすめ技術書
  • 音楽
    • DTM
    • 楽器・機材
    • ピアノ
  • その他
    • 都会のエレキベア
    • ラーメン日記
    • 四コマ漫画
    • おすすめアイテム
    • おもしろコラム
  1. ホーム
  2. 20240327_01_unity_multi_audio

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

サウンドUnityWwiseCRIADXインタラクティブミュージック
2024-03-27

マイケル
マイケル
みなさんこんにちは! マイケルです!
エレキベア
エレキベア
こんにちクマ〜〜
マイケル
マイケル
最近はサウンドミドルウェア周りを学習していまして、 CRI ADXとWwiseについて大方使える状態になりました!
エレキベア
エレキベア
おぉ〜〜どちらもよく聞く定番のミドルウェアクマね
マイケル
マイケル
その中で学習した内容の総まとめとして、オーディオ処理のサンプルを各ライブラリで切り替えできるUnityプロジェクトを作成しました。 今回はこちらを実装するにあたっての設計についてご紹介します!
20240327_01_unity-multi_audio_09
▲オーディオライブラリを切り替え可能なプロジェクトを作成した

エレキベア
エレキベア
ほぅ・・・一度使い始めると中々変更が難しそうな印象があるクマから 切り替えれるのは大きいクマね
マイケル
マイケル
実際にやってみると苦戦した点がいくつかあったので、その辺りを含めて記載していこうかと思います!

作成したサンプル

マイケル
マイケル
実際に作成したサンプルプロジェクトは下記の3つで、GitHubリポジトリに格納しています。

GitHub - unity-multi-audio

20240327_01_unity-multi_audio_03
▲サンプル1:基本機能

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

20240327_01_unity-multi_audio_05
▲サンプル3:インタラクティブミュージック

マイケル
マイケル
実装したライブラリは UnityAudio、CRI ADX、Wwiseの3つ で、各サンプルの実装状況は下記のようになっています。 周波数データの取得がWwiseではデフォルトで対応していなかったため、オーディオスペクトラムの実装は省いています。
各機能の実装状況
機能
UnityAudio
CRI ADX
Wwise
基本再生
オーディオスペクトラム
×
インタラクティブミュージック
×
エレキベア
エレキベア
Wwiseで周波数データの取得ができないのは以外クマね
マイケル
マイケル
調べたらカスタムプラグインを作って回避する方法は出てきたけど Unity標準でも対応してるし、導入してほしいね・・・

サウンドミドルウェアの依存

マイケル
マイケル
これらのライブラリを切り替えれるよう汎用化するにあたり、下記のような問題がありました。 通常の方法で使用すると サウンドミドルウェアのプロジェクトへの依存が強い 状態になってしまいます。

グローバルオブジェクトの存在

マイケル
マイケル
1つ目はグローバルオブジェクトの存在です。 CRI、Wwiseではそれぞれ初期化するために独自のグローバルオブジェクトをシーンに生成しておく必要があります。
20240327_01_unity-multi_audio_10
▲CRIADXのグローバルオブジェクト

エレキベア
エレキベア
ミドルウェアの管理オブジェクトが必要になるのクマね

独自コンポーネントの存在

マイケル
マイケル
そしてオーディオの再生元とするためのSourceコンポーネント、Listenerコンポーネントなど独自のコンポーネントをオブジェクトにアタッチしておかなければなりません。
20240327_01_unity-multi_audio_11
▲ライブラリ独自のコンポーネントをアタッチする必要がある

エレキベア
エレキベア
これはUnityAudioでも同様クマね

実装方法の違い

マイケル
マイケル
最後にこれは仕方がないですが、ライブラリごとに実装方法が異なります。 この辺りを上手く汎用的に書かなければいけません。
        /// <summary>
        /// サウンドイベント再生
        /// </summary>
        /// <param name="eventName">サウンドイベント名</param>
        /// <param name="option">再生オプション</param>
        public void PlaySoundEvent(string eventName, IGameAudioService.SoundPlayOption option = null)
        {
            _criAdxApiService.Play(GameCriAdxAudioSettings.CriAdx.GetCueSheetName(eventName), eventName,
                new ICriAdxApiService.SoundPlayOption()
            {
                FadeTimeMs = option?.FadeTimeMs ?? 0,
            });

            // コールバックイベントの追加
            if (option?.BeatSyncCallback != null)
            {
                _criAdxApiService.SetBeatSyncCallback(
                    GameCriAdxAudioSettings.CriAdx.GetCueSheetName(eventName), option.BeatSyncLabel, option.BeatSyncCallback);
            }
            if (option?.CustomEventCallback != null)
            {
                _criAdxApiService.SetSequenceCallback(option.CustomEventName, option.CustomEventCallback);
            }
        }
▲CRIの例、再生処理の後に別途コールバックを追加している
        /// <summary>
        /// サウンドイベント再生
        /// </summary>
        /// <param name="eventName">サウンドイベント名</param>
        /// <param name="option">再生オプション</param>
        public void PlaySoundEvent(string eventName, IGameAudioService.SoundPlayOption option = null)
        {
            _wwiseApiService.PlayEvent(GameWwiseAudioSettings.Wwise.GetSoundBankName(eventName), eventName,
                new IWwiseApiService.SoundPlayOption()
                {
                    IsStopOther = true,
                    FadeTimeMs = option?.FadeTimeMs ?? 0,
                    BeatSyncCallback = option?.BeatSyncCallback,
                    CustomEventCallback = option?.CustomEventCallback,
                });
        }
▲Wwiseの例、再生時にコールバックを渡している
エレキベア
エレキベア
中々一筋縄ではいかなそうクマね・・・

最終的な設計

設計方針

マイケル
マイケル
これらの問題を解決するため、下記のような方針で実装を行いました。
  • オーディオ設定は各ライブラリごとに定義するが、汎用で使える変数も用意する
  • グローバルオブジェクトの生成、コンポーネントのアタッチはスクリプト内で行う
  • API実行処理、ゲーム固有のオーディオ処理は別クラスに分ける
  • ゲーム側からはインターフェイスを参照する
マイケル
マイケル
オーディオ関連の処理をAPI実行クラスとゲーム固有の処理実行クラスに分割し、 ゲーム側からはインターフェイスを参照することで使用ライブラリを意識しない構成です。
20240327_01_unity-multi_audio_01
▲パッケージ間の依存関係

20240327_01_unity-multi_audio_02
▲最終的なクラス設計

20240327_01_unity-multi_audio_07
▲右:オーディオライブラリのAPI実行クラス

20240327_01_unity-multi_audio_06
▲左:ゲーム固有のオーディオ処理クラス

エレキベア
エレキベア
サービスロケータを使ってクラスを切り替えるクマね
【Unity】ServiceLocatorを使ってサービスクラスをまとめる
2022-02-27

オーディオの固有設定と汎用化

マイケル
マイケル
各ライブラリの構成は下記のようになっていて、 名称は異なりますが大方似たような構成になっています。
各ライブラリの構成
 
UnityAudio
CRI ADX
Wwise
再生の分類
-
※AudioMixer単位での制御は可能
CueSheet
Soundbank
再生の対象
Audio
Cue
Event
ゲームとやり取りするパラメータ
-
AISAC
GameSync
(Switch, State, GameParameter)
Listenerコンポーネント
AudioListener
CriAtomListener
AkAudioListener
Sourceコンポーネント
AudioSource
CriAtomSource
AkBank
マイケル
マイケル
そのため各ライブラリ固有の設定を定義するのに加えて、 下記のような共通の変数を定義して実装しました。
汎用化するための定義
 
汎用の定義
再生の分類
SoundSheet
再生の対象
SoundEvent
ゲームとやり取りするパラメータ
GameParameter
namespace GameSample.Audio
{
    /// <summary>
    /// ゲーム内サウンド設定 (CRI固有)
    /// </summary>
    public class GameCriAdxAudioSettings : IGameAudioSettings
    {
        /// <summary>
        /// SoundEvent名
        /// </summary>
        public string SoundEventName_BgmSpaceWould => CriAdx.CueName.BgmSpaceWould;
        public string SoundEventName_BgmShotThunder => CriAdx.CueName.BgmShotThunder;
        public string SoundEventName_BgmAtomChain => CriAdx.CueName.BgmAtomChain;
        public string SoundEventName_SeAttack => CriAdx.CueName.SeAttack;
        public string SoundEventName_SeMove => CriAdx.CueName.SeMove;

・・・略・・・

        /// <summary>
        /// CRI固有設定
        /// </summary>
        public static class CriAdx
        {

・・・略・・・

            /// <summary>
            /// Cue名
            /// </summary>
            public static class CueName
            {
                public const string BgmSpaceWould = "BGM_SpaceWould";
                public const string BgmShotThunder = "BGM_ShotThunder";
                public const string BgmMogTheme = "BGM_MogTheme";
                public const string BgmAtomChain = "BGM_AtomChain";
                public const string SeMove = "SE_Move";
                public const string SeAttack = "SE_Attack";
                public const string SeRandom = "SE_Random";
            }

・・・略・・・

▲ライブラリ固有の設定と共通設定を定義する
エレキベア
エレキベア
ゲーム側からは使用ライブラリに関わらず共通の変数を使用するクマね

グローバルオブジェクトの生成とコンポーネントのアタッチ

マイケル
マイケル
そしてグローバルオブジェクトや独自コンポーネントに関しては、各ライブラリのクラス内で生成するようにしました。
    /// <summary>
    /// CriAtom API操作クラス
    /// </summary>
    public class CriAdxApiService : ICriAdxApiService, IDisposable
    {
#if AUDIO_LIB_CRI

        /// <summary>
        /// グローバルオブジェクトの生成
        /// シーンロード前に実行する
        /// </summary>
        [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
        private static void CreateAudioGlobalObject()
        {
            const string audioGlobalPrefabName = "CriAdxGlobal";
            var audioGlobalPrefab = Resources.Load(audioGlobalPrefabName);
            var audioGlobalObject = UnityEngine.Object.Instantiate(audioGlobalPrefab);
            UnityEngine.Object.DontDestroyOnLoad(audioGlobalObject);
        }

#endif

・・・略・・・
▲グローバルオブジェクトの生成(CRI)
        /// <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;
        }
▲コンポーネントのアタッチ(CRI)
エレキベア
エレキベア
これでオブジェクトやシーンの状態は汚さずにすむクマね

ゲームからはインターフェイスを参照する

マイケル
マイケル
最後に、ゲーム側からはServiceLocatorを通じてインターフェイスからサービスクラスを取得するようにします。 ライブラリ切り替え時にはDefine定義も差し替えることで処理を分けています。
    /// <summary>
    /// SoundTestシーン管理クラス
    /// </summary>
    public class SoundTestSceneManager : MonoBehaviour
    {

・・・略・・・

        /// <summary>
        /// Audioサービス
        /// </summary>
        private IGameAudioService GameAudioService => ServiceLocator.Resolve<IGameAudioService>();
        private IGameAudioSettings GameAudioSettings => ServiceLocator.Resolve<IGameAudioSettings>();

・・・略・・・

        private void PlayBgm01()
        {
            GameAudioService.PlaySoundEvent(GameAudioSettings.SoundEventName_BgmSpaceWould);
        }

・・・略・・・

▲ゲームからはインターフェイスを参照する
20240327_01_unity-multi_audio_08
▲メニューからライブラリを切り替える

        /// <summary>
        /// シーンのロード後の初期化処理
        /// </summary>
        [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
        private static void InitializeAfterSceneLoad()
        {
            // サービス登録
#if AUDIO_LIB_UNITY_AUDIO
            ServiceLocator.Register<IGameAudioSettings>(new GameUnityAudioSettings());
            ServiceLocator.Register<IGameAudioService>(new GameUnityAudioService());
#elif AUDIO_LIB_CRI
            ServiceLocator.Register<IGameAudioSettings>(new GameCriAdxAudioSettings());
            ServiceLocator.Register<IGameAudioService>(new GameCriAdxAudioService());
#elif AUDIO_LIB_WWISE
            ServiceLocator.Register<IGameAudioSettings>(new GameWwiseAudioSettings());
            ServiceLocator.Register<IGameAudioService>(new GameWwiseAudioService());
#endif
        }
▲サービスロケータへの登録
エレキベア
エレキベア
これでだいぶ綺麗にまとまったクマね

おわりに

マイケル
マイケル
というわけで今回はサウンドミドルウェアの依存を減らしつつオーディオ処理を実装する方法についてでした! どうだったかな?
エレキベア
エレキベア
中々面倒だったクマがなんとか汎用的にできたクマね
マイケル
マイケル
ライブラリを切り換えることはそうそうないと思うけど、 普段から依存を減らすよう実装には気をつけたいね・・・
マイケル
マイケル
次回以降、CRIとWwiseの実装内容についても軽く執筆する予定です! お楽しみに!!
エレキベア
エレキベア
クマ〜〜〜〜〜

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


サウンドUnityWwiseCRIADXインタラクティブミュージック
2024-03-27
記事をSNSで共有する
X
Facebook
LINE
はてなブックマーク
Pocket
LinkedIn
Reddit

著者の各種アカウント
フォローいただけると大変励みになります!
X
GitHub

関連記事
【Unity】Timeline × Excelでスライドショーを効率よく制作する
2024-10-31
【Unity】Boidsアルゴリズムを用いて魚の群集シミュレーションを実装する
2024-05-28
【ゲーム数学】第九回 p5.jsで学ぶゲーム数学「フーリエ解析」
2024-05-12
【Unity】GoでのランキングAPI実装とVPSへのデプロイ方法についてまとめる【Go言語】
2024-04-14
【Unity】第二回 Wwiseを使用したサウンド制御 〜インタラクティブミュージック編〜
2024-03-30
【Unity】第一回 Wwiseを使用したサウンド制御 〜基本動作編〜
2024-03-30
【Unity】第二回 CRI ADXを使用したサウンド制御 〜インタラクティブミュージック編〜
2024-03-28
【Unity】第一回 CRI ADXを使用したサウンド制御 〜基本動作、周波数解析編〜
2024-03-28