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

      【ゲーム数学】第九回 p5.jsで学ぶゲーム数学「フーリエ解析」

      ゲーム数学サウンドJavaScriptフロントエンド関連p5.jsフーリエ解析
      2024-05-12

      マイケル
      マイケル
      みなさんこんにちは! マイケルです!
      エレキベア
      エレキベア
      こんにちクマ〜〜〜
      マイケル
      マイケル
      今回は久しぶりのp5.jsで学ぶゲーム数学シリーズ! 取り上げるのは「フーリエ解析」です!
      エレキベア
      エレキベア
      フーリエ解析・・・ 難しそうクマ〜〜
      マイケル
      マイケル
      フーリエ解析は 音の波形を周波数分解 するのによく使われる手法です! 実際に音を鳴らしながら感覚を掴んでいきますが、原理はシンプルで面白いのでお楽しみに!
      エレキベア
      エレキベア
      サウンド分野で使える手法なのクマね
      マイケル
      マイケル
      なお、今回実際にプログラミングで使用されるFFT(高速フーリエ変換)については取り上げません! 原理が分かりやすいよう、数式ベースで実装しますのでご了承ください。。
      エレキベア
      エレキベア
      FFTもまた別の記事で取り上げてくれクマ

      参考書籍

      マイケル
      マイケル
      フーリエ解析の実装を行うにあたり、 下記書籍のフーリエ解析の章を参考にさせていただきました!

      プログラミングのための数学

      エレキベア
      エレキベア
      プログラミングで使う数学全般が載っているクマね
      マイケル
      マイケル
      そしてフーリエ解析自体については、下記のマンガでわかるシリーズから入りました! 何も知らない状態から学習する場合、ハードルが低いのでおすすめです!

      ぷち マンガでわかるフーリエ解析

      エレキベア
      エレキベア
      流体力学あたりも面白かったクマね
      マイケル
      マイケル
      ちなみに今回様々な波形が登場しますが、こちらは下記のWebツールを使用しながら確認しました。 他にも類似のツールはあると思うので、ぜひ自身で関数を入力して確認しながら見てみると楽しめると思います!

      desmos.com

      エレキベア
      エレキベア
      数式だけみても中々イメージしにくいクマからね

      フーリエ解析とは(理論)

      マイケル
      マイケル
      まずフーリエ解析とは何かというと、一般的な関数を三角関数の合計で表現・近似するための手法です。 よく使用される例としては、オーディオ波形から各周波数成分を求める 例があります。
      20240512_01_math_fourier_09
      ▲オーディオ波形から周波数を求める

      エレキベア
      エレキベア
      周波数はイコライザで見れる成分になるクマね どうやって求めるかなんて、想像もつかないクマ
      マイケル
      マイケル
      この波形だけみると、そんなことほんとにできるの?って思うよね・・・ フーリエ解析の説明に入る前に、まずは音の波形の基本構造を見てみましょう!

      sin波とcos波

      マイケル
      マイケル
      三角関数のsin、cosは覚えていますでしょうか? なんとこのsin関数で作られた下記の式が音の最も基本的な波形として定義されています。

      f(x)=sin2πft f=[Hz]

      マイケル
      マイケル
      純音シヌソイド関数とも呼ばれていて、fは周波数(1秒に振動する回数)です。 fの値を大きくするとその分振動数が増え、音も高くなります。
      20240512_01_math_fourier_12
      ▲sin2πtの関数

      20240512_01_math_fourier_13
      ▲cos2πtの関数

      エレキベア
      エレキベア
      sin、cosは波のように繰り返す性質があったクマね これが音の波形の基本なのクマか・・・
      マイケル
      マイケル
      そして音の大きさは振幅(振動の大きさ)音の高さは周波数で決まっていて、振動が大きくなるほど音も大きく振動する回数が多くなるほど音も高くなります。 例えばラ(A)の高さだと441Hzと決められています。

      f(x)=2sin2π441t ▲振幅が2倍で周波数が441の例

      エレキベア
      エレキベア
      振動数と大きさで音の高さ・大きさが分かるクマね
      マイケル
      マイケル
      ちなみにsin波以外でよく使われる波形についても下記に記載してあります。 興味がある方はよければご参照ください!
      【ソフトシンセ】シンセサイザ を使って効果音を一から作成する
      2020-05-31
      エレキベア
      エレキベア
      シンセで音作りする時にも使うクマね

      フーリエ級数

      マイケル
      マイケル
      音の高さと大きさは周波数・振幅で決まりますが、 実際の音の波形は複雑で、これが音色に値します。
      20240512_01_math_fourier_14
      エレキベア
      エレキベア
      うーむ・・・ ワケが分からんクマ・・・
      マイケル
      マイケル
      このような、どんな複雑な波形でも全ての波形はcos、sinの組み合わせで表現できる という定理が フーリエ級数 になります!

      f(x)=a02+a1cosx+a2cos2x...+ancosnx +b1sinx+b2sin2x...+bnsinnx

      f(x)=a02+n=1(ancosnx+bnsinnx)

      ▲フーリエ級数の式(※a0/2:n=0のcosの係数は2a0のため)

      エレキベア
      エレキベア
      cosとsinの組み合わせで・・・表現できる・・・?
      マイケル
      マイケル
      混乱しているようだね・・・ 例をあげると、先ほど示した波形も下記のように単純なcos、sinの波形に分解できる、ということになります。
      20240512_01_math_fourier_10
      ▲複雑な波形もcos、sinの組み合わせの式で表現することができる

      エレキベア
      エレキベア
      ほうぅクマ・・・ 信じがたいクマが中々面白い定理クマね

      フーリエ解析

      マイケル
      マイケル
      ここでもう一度フーリエ級数の式を見てみます。 最初の話を思い出すと、この式でいうan、bnが振幅、nが周波数ということになります。

      f(x)=a02+n=1(ancosnx+bnsinnx)

      エレキベア
      エレキベア
      ということは・・・
      マイケル
      マイケル
      この係数を求めることができれば、周波数成分を抽出することができるということです!
      エレキベア
      エレキベア
      きたクマ〜〜〜 イコライザへの道が一歩開けたクマ
      マイケル
      マイケル
      問題はこの係数をどうやって求めるか?ですが、これには関数の直行性を利用します。 関数の直行とは違いに交わらない関数同士を指し、2つの関数を掛け合わせて定積分した結果が0になるといった特徴があります。
      エレキベア
      エレキベア
      それと今回の式とどう関係があるクマ??
      マイケル
      マイケル
      実はcosnx、sinnxといった周期の異なる関数は全て直行している状態になります。 つまり、それぞれの関数で積分することでその関数のみが残り係数部分を求めることができるというわけです!
      20240512_01_math_fourier_11
      ▲それぞれの関数で積分することで周波数成分を抽出することができる

      エレキベア
      エレキベア
      そういうことクマか・・・!!
      マイケル
      マイケル
      これを式で表すと下記のようになります。

      an=1π02πf(x)cosnxdx

      bn=1π02πf(x)sinnxdx

      a0=12π02πf(x)dx

      ▲各成分の計算
      エレキベア
      エレキベア
      関数に求めたい周期の関数を掛け合わせたものを0-2πの間で積分するクマね
      マイケル
      マイケル
      この「1/π」がどこからきているのかというと、最終的に残った「sin^2」「cos^2」を積分した結果(面積)がπになるため、最後に割ることで係数部分を求めているというわけです。

      02πsin2nx=02π12(1cos2nx)dx=π

      02πcos2nx=π

      エレキベア
      エレキベア
      これで係数はばっちり求められそうクマね
      マイケル
      マイケル
      最後に、求めたいのは周波数成分になるため、sin、cosでそれぞれ同じ周波数の成分をベクトルの長さとして考えて計算することで求めることができます。 以上でフーリエ解析の説明は完了です!!

      rn=an2+bn2

      ▲最終的な周波数成分の計算
      エレキベア
      エレキベア
      フーリエ解析すごいクマ〜〜〜

      波形によるサウンド再生

      マイケル
      マイケル
      フーリエ解析の概要が分かったところで、ここからは実際にコードを書きながら波形を再生していきましょう! まずは波形・・・といっていいかは分かりませんが、ランダムに生成したノイズ波形の再生を行ってみます。 時間を引数にとって波形を生成する関数は下記のようになります。
      /**
       * 波形: ノイズ
       * @param {number} t 時間 
       */
      function waveNoiseFunc(t) {
        return Math.random() * 2 - 1;
      }
      
      ▲ノイズ波形の関数
      エレキベア
      エレキベア
      ほんとにぐちゃぐちゃした波形ができそうマね
      マイケル
      マイケル
      この関数を使用してWebで音を鳴らす方法として、AudioBufferを使用しました。 関数を受け取って再生する処理は下記のようになります。 サンプルレート分配列を用意してそちらに関数から得られたデータを格納しています。

      参考:
      BaseAudioContext.createBuffer() - Web API | MDN

      /**
       * サンプルレート
       */
      const SampleRate = 44100;
      
      /**
       * 再生時間(秒)
       */
      const PlaySec = 1;
      
      /**
       * 再生中の波形データ
       */
      let bufferSource = null;
      
      /**
       * サウンド再生処理
       * @param {*} func 波形関数
       */
      function playSound(func) {
          // stop playing sound.
          if (bufferSource) {
              bufferSource.stop();
          }
      
          // create context.
          let audioCtx = new AudioContext();
          audioCtx.sampleRate = SampleRate;
      
          // create mono channel buffer.
          let buffer = audioCtx.createBuffer(1, PlaySec * SampleRate, SampleRate);
          let channelData = buffer.getChannelData(0);
          for (var i = 0; i < buffer.length; i++) {
              let playSec = i / SampleRate;
              channelData[i] = func(playSec);
          }
      
          // set play wave data.
          let playWaveData = channelData;
      
          // play sound.
          bufferSource = audioCtx.createBufferSource();
          bufferSource.buffer = buffer;
          bufferSource.connect(audioCtx.destination);
          bufferSource.start();
      
          return playWaveData;
      }
      
      ▲波形関数からのサウンド再生
      エレキベア
      エレキベア
      Webでも波形データから再生することができるのクマね
      マイケル
      マイケル
      これらを使用して音声を再生したサンプルが下記になります! ボタン押下することで音声が再生され、波形も表示されるようにしています。
      ▲ノイズ波形によるサウンド再生 20240512_01_math_fourier_01
      ▲ノイズ波形の表示

      エレキベア
      エレキベア
      おお〜〜〜 このよく聞く音はノイズ波形だったのクマね
      マイケル
      マイケル
      不規則な波形にするだけでこんな音が作れるのは面白いよね! 次は最初に解説したsin、cos波形を再生してみます。 こちらの関数と再生サンプルは下記になります。
      /**
       * 波形: sin(2πft)
       * @param {number} t 時間
       * @param {number} freq 周波数 
       */
      function waveSin2npiFunc(t, freq) {
        return Math.sin(2 * Math.PI * freq * t);
      }
      
      /**
       * 波形: cos(2πft)
       * @param {number} t 時間
       * @param {number} freq 周波数
       */
      function waveCos2npiFunc(t, freq) {
        return Math.cos(2 * Math.PI * freq * t);
      }
      
      ▲sin、cos波形の作成
      ▲sin、cos波形によるサウンド再生 20240512_01_math_fourier_02
      ▲sin波形の表示

      20240512_01_math_fourier_03
      ▲cos波形の表示

      エレキベア
      エレキベア
      基本的な波形という情報を聞いていたこともあってか、
      シンプルな音に聞こえるクマ
      マイケル
      マイケル
      cos波形はsin波形がずれただけだから、音色としてもほぼ変わってないね

      フーリエ級数による波形作成

      マイケル
      マイケル
      それでは次はフーリエ級数を使用して作成した波形で音を鳴らしてみます。 復習になりますが、フーリエ級数は下記のような式になっていました。

      f(x)=a02+n=1(ancosnx+bnsinnx)

      マイケル
      マイケル
      これを関数に落とし込むと下記のようになります。  サウンド再生で確認しやすいよう、ベースとなる周波数も指定できるようにしてあります。
      /**
       * 波形: フーリエ級数
       * @param {number} t 時間
       * @param {number} a0 定数関数
       * @param {number[]} aArray cos関数の定数項
       * @param {number[]} bArray sin関数の定数項
       * @param {number[]} baseFreq 計算のベースとする周波数
       */
      function waveFourierSeriesFunc(t, a0, aArray, bArray, baseFreq = 441) {
        // cos値の計算
        let cosValues = [];
        for (let n = 0; n < aArray.length; n++) {
          const an = aArray[n];
          const freq = (n + 1) * baseFreq;
          cosValues.push(an * waveCos2npiFunc(t, freq));
        }
        // sin値の計算
        let sinValues = [];
        for (let n = 0; n < bArray.length; n++) {
          const bn = bArray[n];
          const freq = (n + 1) * baseFreq;
          sinValues.push(bn * waveSin2npiFunc(t, freq));
        }
        // フーリエ級数の形で計算して返却
        const cosSum = cosValues.length == 0 ? 0 : cosValues.reduce((sum, x) => sum + x, 0);
        const sinSum = sinValues.length == 0 ? 0 : sinValues.reduce((sum, x) => sum + x, 0);
        return a0 / 2 + cosSum + sinSum;
      }
      
      ▲フーリエ級数による波形関数
      エレキベア
      エレキベア
      anとbnの配列を渡すことで作成しているクマね
      マイケル
      マイケル
      この関数を使用して何種類か波形を作成して再生するサンプルは下記になります。
      ↑フーリエ関数で作成した波形によるサウンド再生
      /**
       * フーリエ波形1再生ボタン押下
       */
      function pushFoorier1PlayButton() {
        playWaveData = playSound((t) => waveFourierSeriesFunc(t, 0, [1, 2, 3], [1]));
      }
      
      /**
       * フーリエ波形1再生ボタン押下
       */
      function pushFoorier2PlayButton() {
        playWaveData = playSound((t) => waveFourierSeriesFunc(t, 0, [], [2, 4]));
      }
      
      /**
       * 短形波再生ボタン押下
       */
      function pushRectPlayButton() {
        let bArray = [];
        const bCount = 30; // この数を上げるほど短形波に近づく
        for (let n = 1; n <= bCount; n++) {
          let b = n % 2 != 0 ? (4 / (n * Math.PI)) : 0;
          bArray.push(b);
        }
        playWaveData = playSound((t) => waveFourierSeriesFunc(t, 0, [], bArray));
      }
      
      
      ▲フーリエ級数による波形の作成例
      20240512_01_math_fourier_04
      20240512_01_math_fourier_05
      20240512_01_math_fourier_06
      エレキベア
      エレキベア
      いろんな形状の波形が作成できるのも、それで音色が変わるのも面白いクマね〜〜
      マイケル
      マイケル
      最後のは短形波といって、こんな波形もsin、cosの組み合わせで表現できてしまいます。 bCount変数を増やすことでより短形波に近づくので、値を変えて試してみてください!

      フーリエ解析

      マイケル
      マイケル
      最後に波形関数から作成したデータから、フーリエ解析で元の係数を求めてみます。 係数an、bnを求める式は下記になっていました。

      an=1π02πf(x)cosnxdx

      bn=1π02πf(x)sinnxdx

      エレキベア
      エレキベア
      関数同士を掛け合わせたものを積分して求めるのだったクマね
      マイケル
      マイケル
      これを計算しやすいよう、0-1の範囲で書きなおすと下記のようになります。 今回はこちらの式を使用して実装しました。

      <f(x),g(x)>=201f(x)g(x)dx

      /**
       * フーリエ解析
       * @param {(t: number) => number} f 波形関数
       * @param {number} N 解析する定数項の数
       * @param {number[]} baseFreq 計算のベースとする周波数
       * @param {number} dotN 内積を行う回数(数値を上げると精度が向上する)
       * @returns a0, aArray, bArray
       */
      function fourierCoefficients(f, N, baseFreq = 441, dotN = 100) {
        // 関数同士の内積
        // 2 * ∮[0→1]f(t)g(t)
        function dot_function(f, g, N) {
          const dt = 1 / N;
          let array = [];
          for (let t = 0; t < 1; t += dt) {
            array.push(f(t) * g(t) * dt);
          }
          return 2 * array.reduce((sum, x) => sum + x, 0);
        }
      
        // sin、cos関数との内積結果から係数を求める
        let a0 = dot_function(f, (t) => 1 / 2, dotN);
        let anArray = [];
        let bnArray = [];
        for (let n = 0; n < N; n++) {
          const freq = (n + 1) * baseFreq;
          anArray.push(dot_function(f, (t) => waveCos2npiFunc(t, freq), dotN));
          bnArray.push(dot_function(f, (t) => waveSin2npiFunc(t, freq), dotN));
        }
        return {a0, anArray, bnArray};
      }
      
      ▲フーリエ解析関数
      エレキベア
      エレキベア
      係数を求めて返却する関数クマね
      マイケル
      マイケル
      この関数を使用して、サウンド再生後に係数を求めて表示する処理とサンプルは下記になります。
      /**
       * サウンド再生とフーリエ解析を行う
       * @param {(t: number) => number} waveFunc 波形関数
       */
      function playSoundAndCheckFourier(waveFunc) {
        // サウンド再生
        playWaveData = playSound(waveFunc);
        
        // フーリエ級数を求める
        const checkFourierCount = 5;
        let result = fourierCoefficients(waveFunc, checkFourierCount);
        a0Value = result.a0;
        anValueArray = result.anArray;
        bnValueArray = result.bnArray;
      }
      
      ▲フーリエ解析の実行
      ▲波形データからフーリエ解析を行うサンプル 20240512_01_math_fourier_07
      ▲フーリエ解析の例

      エレキベア
      エレキベア
      ちゃんと求められてるクマ〜〜〜
      マイケル
      マイケル
      これで当初の目的だった、「波形データから周波数成分を求める」という目的が達成できました!

      おわりに

      マイケル
      マイケル
      というわけで、今回はフーリエ解析についてでした! どうだったかな?
      エレキベア
      エレキベア
      波形から周波数成分を求めるなんて最初は予想も出来なかったクマが、 原理が知れて楽しかったクマ〜〜〜
      マイケル
      マイケル
      実際にサウンドと波形を確認しながら見るのは中々面白かったね! これを実際にゲーム等で使用する場合には、処理を高速化したFFT(高速フーリエ変換)という手法があります。 FFTを使用してUnityでオーディオスペクトラムを実装した例もありますので、こちらもよければご参照ください!
      【Unity】第三回 UnityAudioを使いこなす 〜オーディオスペクトラム描画編〜
      2024-01-22
      エレキベア
      エレキベア
      Unity含めてFFTライブラリはよく用意されているクマね
      マイケル
      マイケル
      それでは今日はこの辺で! アデューー!!
      エレキベア
      エレキベア
      クマ〜〜〜〜

      【ゲーム数学】第九回 p5.jsで学ぶゲーム数学「フーリエ解析」〜完〜


      ゲーム数学サウンドJavaScriptフロントエンド関連p5.jsフーリエ解析
      2024-05-12

      関連記事
      【Unity】第二回 Wwiseを使用したサウンド制御 〜インタラクティブミュージック編〜
      2024-03-30
      【Unity】第一回 Wwiseを使用したサウンド制御 〜基本動作編〜
      2024-03-30
      【Unity】第二回 CRI ADXを使用したサウンド制御 〜インタラクティブミュージック編〜
      2024-03-28
      【Unity】第一回 CRI ADXを使用したサウンド制御 〜基本動作、周波数解析編〜
      2024-03-28
      【Unity】サウンドミドルウェアに依存しない設計を考える【CRI ADX・Wwise】
      2024-03-27
      【JUCE】DTMプラグインを作ってみる 〜ディストーション編〜【VST/AU】
      2024-03-22
      【Unity】第三回 UnityAudioを使いこなす 〜オーディオスペクトラム描画編〜
      2024-01-22
      【Unity】第二回 UnityAudioを使いこなす 〜AudioMixer活用編〜
      2024-01-22