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

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

      UnityサウンドUnityAudioオーディオスペクトラム
      2024-01-22

      マイケル
      マイケル
      みなさんこんにちは! マイケルです!
      エレキベア
      エレキベア
      こんにちクマ〜〜〜
      マイケル
      マイケル
      これまで何記事かに分けてUnityAudioを触ってきましたが、 第三回、ラストということで最後にオーディオスペクトラムを実装してみようと思います!
      【Unity】第一回 UnityAudioを使いこなす 〜サウンド再生処理編〜
      2024-01-22
      【Unity】第二回 UnityAudioを使いこなす 〜AudioMixer活用編〜
      2024-01-22
      【Unity】第三回 UnityAudioを使いこなす 〜オーディオスペクトラム描画編〜
      2024-01-22
      エレキベア
      エレキベア
      オーディオスペクトラムというと、音の周波数がウネウネしているあれクマね
      マイケル
      マイケル
      その通り! 今回は下記二種類のオーディオスペクトラムを実装してみました!
      20240122_01_unity_audio_spectrum_01
      ▲LineRendererによる描画

      20240122_01_unity_audio_spectrum_02
      ▲3DCubeによる描画

      エレキベア
      エレキベア
      3Dオブジェクトで描画できるようになると、ゲーム内で使用したりと幅が広がりそうクマね
      マイケル
      マイケル
      UnityAudioに元々用意されている関数で、比較的簡単に実装できるので興味がある方は真似して実装しみてください! ちなみに今回実装するサンプルは下記にGitHubリポジトリとしてあげています!

      GitHub - unity-audio-sample

      エレキベア
      エレキベア
      これまで作ったサンプルも入っているから見てくれクマ〜〜

      参考書籍

      マイケル
      マイケル
      今回、サウンド周りの学習をするにあたり、下記書籍を参考にさせていただきました!

      Unityサウンド エキスパート養成講座

      マイケル
      マイケル
      Unityでのサウンド処理をまとめた数少ない書籍です! 発売日からそれなりに経過していますが、UnityAudioだけでなくCRIと連携した使い方まで解説されているため、興味を持った方はぜひ一読してみてください!
      エレキベア
      エレキベア
      サウンド専門となると書籍は中々ないクマから持っておく価値はありそうクマね

      周波数情報の取得

      マイケル
      マイケル
      まず、オーディオスペクトラムとして表示するにはサウンドの周波数データが必要になります。 それをどうやって取得するかというと、UnityAudioに元々用意されている AudioSource.GetSpectrumData という関数から取得することができます。

      Unityスクリプトリファレンス - AudioSource.GetSpectrumData

      マイケル
      マイケル
      これを読み込んだAudioSourceから実行するだけで取得することができます。 本来はフーリエ変換という計算を行わないといけないのですが、それを用意してくれているので簡単ですね!
              /// <summary>
              /// 再生中BGMのスペクトラム周波数を取得
              /// </summary>
              public float[] GetBgmSpectrumData(int sampleCount)
              {
                  var spectrumData = new float[sampleCount];
                  var bgmAudioResource = _bgmAudioSourceList.FirstOrDefault(audioSource => audioSource.isPlaying);
                  if (bgmAudioResource == null)
                  {
                      return spectrumData;
                  }
                  bgmAudioResource.GetSpectrumData(spectrumData, 0, FFTWindow.BlackmanHarris);
                  return spectrumData;
              }
      
      ▲今回は上記のような形で取得処理を用意した
      エレキベア
      エレキベア
      これで準備は完了クマ あとはこのデータを使ってオーディオスペクトラムを描画するクマ

      オーディオスペクトラムの描画

      オーディオスペクトラム基底処理の実装

      マイケル
      マイケル
      オーディオスペクトラムを描画するにあたり、基底クラスとしてSpectrumVisualizerというクラスを用意しました。 こちらのパラメータは下記のようになっています。
              /// <summary>
              /// 周波数の分割数
              /// ※2の冪乗かつ64-8192の範囲で設定
              /// </summary>
              protected enum FrequencyResolutionType
              {
                  _64 = 64,
                  _128 = 128,
                  _256 = 256,
                  _512 = 512,
                  _1024 = 1024,
                  _2048 = 2048,
                  _4096 = 4096,
                  _8192 = 8192,
              }
      
              [Tooltip("スペクトラム周波数のサンプル数")]
              [SerializeField]
              protected FrequencyResolutionType _frequencyResolution = FrequencyResolutionType._2048;
              protected int FrequencyResolution => (int)_frequencyResolution;
      
              [Tooltip("取得する最小周波数")]
              [Range(0, 5000)]
              [SerializeField] protected int _filterMinFrequency = 2000;
      
              [Tooltip("取得する最大周波数")]
              [Range(20000, 44100)]
              [SerializeField] protected int _filterMaxFrequency = 20000;
      
              [Tooltip("Rendererの表示サンプル数")]
              [Range(0, 512)]
              [SerializeField] protected int _rendererSampleCount = 128;
      
              [Tooltip("Rendererの周波数表示倍率")]
              [Range(0, 1000)]
              [SerializeField] protected int _rendererFrequencyGain = 100;
      
              [Tooltip("Rendererの最大高さ")]
              [Range(0, 10)]
              [SerializeField] protected float _rendererMaxHeight = 3f;
      
              [Tooltip("Rendererの表示範囲")]
              [Range(0, 30)]
              [SerializeField] protected float _rendererRange = 10f;
      
              /// <summary>
              /// Renderのルート位置
              /// </summary>
              protected Vector3 RendererRootPosition => transform.localPosition;
      
      ▲調整可能なパラメータ
      マイケル
      マイケル
      GetSpectrumDataを実行する際に渡すサンプル数は、2の冪乗かつ64-8192の範囲でないといけないため、その中から選択できるようにしています。 その他、柔軟性を高めるために実際に描画する際の分割数も別途指定できるようにしています。
      エレキベア
      エレキベア
      確かに3Dオブジェクトでの描画も考えると、描画する側の数は制限できるようにしといた方がよさそうクマ
      マイケル
      マイケル
      初期化処理と更新処理については下記のようになっています。 表示する周波数域を絞るためのフィルタ処理(GetFilteredSpectrumDataArray)と、 取得した周波数配列から描画用の配列データに変換する処理(GetAverageSpectrumDataValue) も用意しました。 最終的に加工したデータを、描画するためのUpdateRenderer関数に渡すようにしています。
              /// <summary>
              /// 初期化処理
              /// </summary>
              /// <param name="getSpectrumDataFunc">周波数データの取得処理</param>
              public void Initialize(Func<int, float[]> getSpectrumDataFunc)
              {
                  _getSpectrumDataFunc = getSpectrumDataFunc;
                  _isInitialized = true;
                  InitializeRenderer();
              }
      
              /// <summary>
              /// Renderer初期化処理
              /// </summary>
              protected virtual void InitializeRenderer()
              {
              }
      
              /// <summary>
              /// 更新処理
              /// </summary>
              private void Update()
              {
                  if (!_isInitialized)
                  {
                      return;
                  }
      
                  // 周波数データを取得して指定範囲にフィルタ
                  var spectrumDataArray = _getSpectrumDataFunc.Invoke(FrequencyResolution);
                  spectrumDataArray = GetFilteredSpectrumDataArray(spectrumDataArray, _filterMinFrequency, _filterMaxFrequency);
      
                  // 描画用配列の数に合わせてデータを整形する
                  var dataLength = spectrumDataArray.Length;
                  var renderLength = _rendererSampleCount;
                  var rendererSpectrumDataArray = new float[renderLength];
                  var prevDataIndex = 0;
                  for (var i = 0; i < renderLength; i++)
                  {
                      var dataIndex = Mathf.Min(Mathf.CeilToInt((i + 1) * ((float)dataLength / renderLength)), dataLength - 1);
                      rendererSpectrumDataArray[i] = GetAverageSpectrumDataValue(spectrumDataArray, prevDataIndex, dataIndex);
                      prevDataIndex = dataIndex;
                  }
                  UpdateRenderer(rendererSpectrumDataArray);
              }
      
              /// <summary>
              /// Renderer更新処理
              /// </summary>
              /// <param name="dataArray"></param>
              protected virtual void UpdateRenderer(float[] dataArray)
              {
              }
      
              /// <summary>
              /// 周波数データ配列を指定範囲に切り取る
              /// </summary>
              /// <param name="dataArray"></param>
              /// <param name="minFrequency"></param>
              /// <param name="maxFrequency"></param>
              /// <returns></returns>
              private float[] GetFilteredSpectrumDataArray(float[] dataArray, int minFrequency, int maxFrequency)
              {
                  // 1indexあたりの値
                  var indexValue = (float) AudioSettings.outputSampleRate / FrequencyResolution;
      
                  // 開始、終了周波数に該当するindexを求めてフィルタ
                  var startIndex = Mathf.CeilToInt(minFrequency / indexValue);
                  var endIndex = Mathf.FloorToInt(maxFrequency / indexValue);
                  return new List<float>(dataArray).GetRange(startIndex, endIndex - startIndex).ToArray();
              }
      
              /// <summary>
              /// 周波数データ配列の指定範囲の平均値を返却する
              /// </summary>
              /// <param name="dataArray"></param>
              /// <param name="startIndex"></param>
              /// <param name="endIndex"></param>
              /// <returns></returns>
              protected float GetAverageSpectrumDataValue(float[] dataArray, int startIndex, int endIndex)
              {
                  var value = 0f;
                  var count = 0;
                  for (var i = startIndex; i <= endIndex; i++)
                  {
                      value += dataArray[i];
                      count++;
                  }
                  return value / count;
              }
      
      ▲周波数データ配列を指定範囲に切り取り、表示用に配列数を調整
      エレキベア
      エレキベア
      InitializeRenderer、UpdateRendererを描画する関数内で記述するクマね データはこれで用意できたクマね

      LineRendererによる描画

      マイケル
      マイケル
      それでは用意したクラスを使用して、LineRendererとして描画する実装を考えてみます。
      20240122_01_unity_audio_spectrum_01
      ▲LineRendererによる描画

      マイケル
      マイケル
      今回は下記のように実装してみました。 LineRenderer内のpositionCountを表示する分割数に設定し、周波数データから位置を計算して設定しています。
      using UnityEngine;
      
      namespace Spectrum
      {
          /// <summary>
          /// LineRendererによるスペクトラム周波数の描画
          /// </summary>
          public class LineSpectrumVisualizer : SpectrumVisualizer
          {
              [Tooltip("LineRendererの初期幅")]
              [Range(0f, 1f)]
              [SerializeField] float _initLineRendererWidth = 0.05f;
      
              private LineRenderer _lineRenderer;
              private Vector3[] _lineRendererPositionArray;
      
              protected override void InitializeRenderer()
              {
                  base.InitializeRenderer();
      
                  _lineRenderer = gameObject.AddComponent<LineRenderer>();
                  _lineRenderer.startWidth = _initLineRendererWidth;
                  _lineRenderer.endWidth = _initLineRendererWidth;
      
                  CreateLineRenderer(_rendererSampleCount, _rendererRange, RendererRootPosition);
              }
      
              protected override void UpdateRenderer(float[] dataArray)
              {
                  base.UpdateRenderer(dataArray);
      
                  // 情報の更新があったら作り直す
                  if (UpdateLineRendererInfo())
                  {
                      CreateLineRenderer(_rendererSampleCount, _rendererRange, RendererRootPosition);
                  }
      
                  // Rendererの更新
                  UpdateLineRenderer(dataArray);
              }
      
              private int _lineRendererSampleCount;
              private float _lineRendererRange;
              private Vector3 _lineRendererRootPosition;
      
              /// <summary>
              /// LineRendererに必要な情報の更新
              /// </summary>
              /// <returns></returns>
              private bool UpdateLineRendererInfo()
              {
                  if (_rendererSampleCount != _lineRendererSampleCount
                      || !Mathf.Approximately(_rendererRange, _lineRendererRange)
                      || RendererRootPosition != _lineRendererRootPosition)
                  {
                      _lineRendererSampleCount = _rendererSampleCount;
                      _lineRendererRange = _rendererRange;
                      _lineRendererRootPosition = RendererRootPosition;
                      return true;
                  }
                  return false;
              }
      
              /// <summary>
              /// LineRendererの作成
              /// </summary>
              /// <param name="sampleCount"></param>
              /// <param name="range"></param>
              /// <param name="rootPosition"></param>
              private void CreateLineRenderer(int sampleCount, float range, Vector3 rootPosition)
              {
                  if (_lineRenderer == null)
                  {
                      _lineRenderer = gameObject.GetComponent<LineRenderer>();
                  }
      
                  // 描画する点の数を設定
                  _lineRenderer.positionCount = sampleCount;
      
                  // 点の位置を計算する
                  _lineRendererPositionArray = new Vector3[sampleCount];
                  for (var i = 0; i < sampleCount; i++)
                  {
                      _lineRendererPositionArray[i] = GetLineRendererPosition(i, sampleCount, range, rootPosition, 0f);
                  }
                  _lineRenderer.SetPositions(_lineRendererPositionArray);
      
                  _lineRendererSampleCount = sampleCount;
                  _lineRendererRange = range;
                  _lineRendererRootPosition = rootPosition;
              }
      
              /// <summary>
              /// LineRendererの更新処理
              /// </summary>
              /// <param name="dataArray"></param>
              private void UpdateLineRenderer(float[] dataArray)
              {
                  // Renderer用にデータを整形して表示
                  for (var i = 0; i < _lineRendererPositionArray.Length; i++)
                  {
                      _lineRendererPositionArray[i] = GetLineRendererPosition(i, _rendererSampleCount, _rendererRange, gameObject.transform.localPosition, dataArray[i]);
                  }
                  _lineRenderer.SetPositions(_lineRendererPositionArray);
              }
      
              /// <summary>
              /// LineRendererの位置情報を取得
              /// </summary>
              /// <param name="index"></param>
              /// <param name="sampleCount"></param>
              /// <param name="range"></param>
              /// <param name="rootPosition"></param>
              /// <param name="dataValue"></param>
              /// <returns></returns>
              private Vector3 GetLineRendererPosition(int index, int sampleCount, float range, Vector3 rootPosition, float dataValue)
              {
                  var x = range * (index / ((float) sampleCount / 2) - 1);
                  var y = Mathf.Min(_rendererMaxHeight, (dataValue * _rendererFrequencyGain));
                  return rootPosition + new Vector3(x, y, 0f);
              }
          }
      }
      
      
      
      ▲LineRendererによる描画
      エレキベア
      エレキベア
      LineRendererの各点の位置の高さを周波数データによって変えてるクマね データは渡されてくるからシンプルクマね

      3DCubeによる描画

      マイケル
      マイケル
      次は3DCubeによる描画についてです。 これが出来るようになれば、3D空間上でオブジェクトをサウンドに合わせて変形させるといったことも出来そうです。
      20240122_01_unity_audio_spectrum_02
      ▲3DCubeによる描画

      マイケル
      マイケル
      実装に関しては下記の通りで、データによって3DCubeのscaleを変化させているだけです。 位置や大きさの更新と調整パラメータ以外はLineRendererと大きな違いはありません。
      using UnityEngine;
      
      namespace Spectrum
      {
          /// <summary>
          /// 3DCubeによるスペクトラム周波数の描画
          /// </summary>
          public class CubeSpectrumVisualizer : SpectrumVisualizer
          {
              [Tooltip("CubeのX方向の大きさ")]
              [Range(0f, 1f)]
              [SerializeField] private float _cubeScaleX = 0.1f;
      
              [Tooltip("CubeのZ方向の大きさ")]
              [Range(0f, 1f)]
              [SerializeField] private float _cubeScaleZ = 0.1f;
      
              private GameObject[] _cubeObjectArray;
      
              protected override void InitializeRenderer()
              {
                  base.InitializeRenderer();
      
                  CreateCubeObjects(_rendererSampleCount, _rendererRange, RendererRootPosition);
              }
      
              protected override void UpdateRenderer(float[] dataArray)
              {
                  base.UpdateRenderer(dataArray);
      
                  if (UpdateCubeRendererInfo())
                  {
                      CreateCubeObjects(_rendererSampleCount, _rendererRange, RendererRootPosition);
                  }
                  UpdateCubeObjects(dataArray);
              }
      
              private void CreateCubeObjects(int sampleCount, float range, Vector3 rootPosition)
              {
                  // リアルタイム検証用でオブジェクト再生成しているが、
                  // 実際のゲームでは数は固定にした方がよいでしょう...
                  if (_cubeObjectArray != null && _cubeObjectArray.Length > 0)
                  {
                      foreach (var cubeObject in _cubeObjectArray)
                      {
                          Destroy(cubeObject);
                      }
                  }
      
                  _cubeObjectArray = new GameObject[sampleCount];
                  for (var i = 0; i < sampleCount; i++)
                  {
                      var cube = GameObject.CreatePrimitive(PrimitiveType.Cube);
                      cube.transform.parent = gameObject.transform;
                      _cubeObjectArray[i] = cube;
                  }
      
                  for (var i = 0; i < _cubeObjectArray.Length; i++)
                  {
                      _cubeObjectArray[i].transform.localPosition = GetCubePosition(i, sampleCount, range, rootPosition);
                      _cubeObjectArray[i].transform.localScale = GetCubeScale(0f);
                  }
              }
      
              private int _cubeRendererSampleCount;
              private float _cubeRendererRange;
              private Vector3 _cubeRendererRootPosition;
      
              private bool UpdateCubeRendererInfo()
              {
                  if (_rendererSampleCount != _cubeRendererSampleCount
                      || !Mathf.Approximately(_rendererRange, _cubeRendererRange)
                      || RendererRootPosition != _cubeRendererRootPosition)
                  {
                      _cubeRendererSampleCount = _rendererSampleCount;
                      _cubeRendererRange = _rendererRange;
                      _cubeRendererRootPosition = RendererRootPosition;
                      return true;
                  }
                  return false;
              }
      
              private void UpdateCubeObjects(float[] dataArray)
              {
                  for (var i = 0; i < _rendererSampleCount; i++)
                  {
                      _cubeObjectArray[i].transform.localScale = GetCubeScale(dataArray[i]);
                  }
              }
      
              private Vector3 GetCubePosition(int index, int sampleCount, float range, Vector3 rootPosition)
              {
                  var x = range * (index / ((float) sampleCount / 2) - 1);
                  return rootPosition + new Vector3(x, 0f, 0f);
              }
      
              private Vector3 GetCubeScale(float dataValue)
              {
                  var y = Mathf.Min(_rendererMaxHeight, (dataValue * _rendererFrequencyGain));
                  return new Vector3(_cubeScaleX, y, _cubeScaleZ);
              }
          }
      }
      
      
      
      ▲3DCubeによる描画
      エレキベア
      エレキベア
      これで一通り実装が完了したクマね オブジェクトの位置を3D空間上にばらけさせると、見た目的に更に面白いものが作れそうクマ

      おわりに

      マイケル
      マイケル
      というわkでオーディオスペクトラムの描画についてでした! どうだったかな??
      エレキベア
      エレキベア
      周波数データの取得関数が用意されているクマから、案外簡単に実装できたクマね
      マイケル
      マイケル
      今回は用意された関数を使ったけど、 フーリエ変換も面白いトピックだと思うから、どこかでまた取り上げてみたいね
      マイケル
      マイケル
      というわけでUnityAudioについて何回かに分けて書いてきました。 今後はCRIやWwiseといったサウンドミドルウェアも触りながら学習してみようかと思っています。 お楽しみに〜〜!!
      エレキベア
      エレキベア
      クマ〜〜〜

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

      【Unity】第一回 UnityAudioを使いこなす 〜サウンド再生処理編〜
      2024-01-22
      【Unity】第二回 UnityAudioを使いこなす 〜AudioMixer活用編〜
      2024-01-22
      【Unity】第三回 UnityAudioを使いこなす 〜オーディオスペクトラム描画編〜
      2024-01-22

      UnityサウンドUnityAudioオーディオスペクトラム
      2024-01-22

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