ゲーム開発
Unity
UnrealEngine
C++
Blender
ゲーム数学
ゲームAI
グラフィックス
サウンド
アニメーション
GBDK
制作日記
IT関連
ツール開発
フロントエンド関連
サーバサイド関連
WordPress関連
ソフトウェア設計
おすすめ技術書
音楽
DTM
楽器・機材
ピアノ
ラーメン日記
四コマ漫画
その他
おすすめアイテム
おもしろコラム
  • ゲーム開発
    • Unity
    • UnrealEngine
    • C++
    • Blender
    • ゲーム数学
    • ゲームAI
    • グラフィックス
    • サウンド
    • アニメーション
    • GBDK
    • 制作日記
  • IT関連
    • ツール開発
    • フロントエンド関連
    • サーバサイド関連
    • WordPress関連
    • ソフトウェア設計
    • おすすめ技術書
  • 音楽
    • 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

      関連記事
      【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