🤖

React Three Fiberを使ってオーディオビジュアライザーを作りたかった

2023/12/11に公開

React Three Fiberを使ってオーディオビジュアライザーを作りたかった

みなさんThreejsはご存じですか? ご存知ですね。
フロントエンドで3D表現をするためのライブラリです。
そして、それが命令的に記述するライブラリであることもご存じですか? ご存知ですね。
Trhee.jsというのは通常、命令的に記述します。良し悪しはともかく、Reactとは思想が異なりますね。
Reactは命令的に記述しますか? 宣言的に記述しますね。そういうことです。
ReactでThree.jsを宣言的に書くためのライブラリ、React Three Fiberならそれができます。

https://github.com/pmndrs/react-three-fiber

この記事ではReact+Three.jsでオーディオビジュアライザーを作ります。

https://dcyoung.github.io/r3f-audio-visualizer/

うまくいけばこういうのが作れます。

環境を作る

今回作りたいものはシンプルなアプリケーションなので、Viteでプロジェクトを始めます。

$ npm create vite@latest

Three.js本体とReact Three Fiber、ついでにreact-threeエコシステムに属し、便利なラッパー関数群をまとめたライブラリであるdreiをインストール。
この時点だとdreiは使うかわからないんですがあると色々便利なので入れておきます。粒子表現とかを簡易化してくれたりする。結局使わなかったらuninstallします。

$ npm install three @react-three/fiber @react-three/drei

最後に儀式。

$ npm run dev

を実行して画面が表示されることを確認する。

何にせよあとは適当に不要なファイルを消したりなんだりしてコードを書くだけです。

ものを作る

当たり前なんですけどオーディオビジュアライザーを作るためにはオーディオがなければならないんですね。まあそこはうまく用意してもらうとして、まずオーディオを再生する簡単なWebアプリケーションを作りましょう。
Web Audio APIを使います。
ちなみにWeb Audio APIの動作は高速フーリエ変換(FFT)というアルゴリズムに基づいていて、これはこれでけっこう面白いんですけど死ぬほど難しいので今回は省きます。
今回は「Web Audio APIは音声データを配列に変換する」という程度の大雑把な理解で大丈夫です。

オーディオを再生する

チャッとやるとこうなります。

async function createAudioData(): Promise<AudioBuffer> {
  // Contextを作成
  const context = new window.AudioContext();

  // 音源を読み込む
  const response = await fetch("/test.wav");

  // バイナリを作成
  const arrayBuffer = await response.arrayBuffer();

  // デコードしてWebAudio APIで扱える音声データに変換
  const audioData = context.decodeAudioData(arrayBuffer);

  // 音声データを返す
  return audioData;
}

export async function createAudio(): Promise<{
  audioSource: AudioBufferSourceNode;
  audioAnalyser: AnalyserNode;
}> {
  // 音声データを作成
  const audioData = await createAudioData();

  // Contextを作成
  const context = new window.AudioContext();

  // 音声ソースを作成
  const audioSource = context.createBufferSource();

  // Analyserを作成
  const audioAnalyser = context.createAnalyser();

  // fftサイズを指定。高いほうがより細かいデータとなる
  audioAnalyser.fftSize = 2048;

  // 音声データを音声ソースに設定
  audioSource.buffer = audioData;

  // ループするように設定
  audioSource.loop = true;

  // 音声データをAnalyserに接続
  audioSource.connect(audioAnalyser);

  // Analyserを音声出力に接続
  audioAnalyser.connect(context.destination);

  return {
    audioSource,
    audioAnalyser,
  };
}

createAudio関数から返ってくるaudioSourceが整形された音声ソースなので、これに対してstartメソッドを実行することでオーディオが再生されます。
あれこれ言うのもなんなので詳しいところはMDNを見てください。Mozillaを信じろ。そしてみんなFirefoxを入れよう。
https://developer.mozilla.org/ja/docs/Web/API/Web_Audio_API
https://developer.mozilla.org/ja/docs/Web/API/AudioScheduledSourceNode/start

初期表示画面を追加する

Uncaught (in promise) DOMException: play() failed because the user didn't interact with the document first.

これで終わりかと思いきや、startメソッドをただ実行しても音声は再生されません。コンソールには変なエラーが出ています。何これ?
理由はブラウザポリシーです。Google Chromeなどの一般的なWebブラウザでは音声の自動再生はユーザ体験の観点から制限されていて、音声を再生するにはユーザからの能動的な操作を必要とします。アニメの公式サイトとかたまに音が出るタイプのがありますけど、ああいうのって必ずワンクッション置くじゃないですか、あれです。
詳しくはお使いのブラウザの自動再生に関するポリシーを調べるなり、MDNを見るなりするとよいです。

https://developer.mozilla.org/ja/docs/Web/Media/Autoplay_guide

まあonClickイベントでやってあければ解決するので、初期表示画面を追加してユーザからの能動的なクリックによって再生を開始するようにしましょう。

export const InitView: React.FC<{ onClick: () => void }> = (props) => {
  return (
    <div
      style={{
        display: "flex",
        justifyContent: "center",
        alignItems: "center",
        flexWrap: "wrap",
        width: "100vw",
        height: "100vh",
        backgroundColor: "black",
      }}
      onClick={props.onClick}
    >
      <div>
        <img src="aoide.svg" alt="" />
        <br />
        <p>Please Click.</p>
        <p>音声が再生されます。音量に注意してください。</p>
      </div>
    </div>
  );
};

initViewコンポーネントです。

オーディオビジュアライザーを作る

音声処理はlib/audio.tsファイルにまとめたので、次はAudioVisualizerコンポーネントを作ります。

音声情報を取得する

当たり前ですが、オーディオをビジュアライズするためにJavaScriptを使うわけですから、JavaScriptから参照、操作が容易なオーディオである必要があります。
ここでの目的は前段で作ったaudioSourceを配列に変換することです。
まずはstartAudio関数を作ります。非同期通信処理が入っているのでuseEffectを使って初回に実行します。

/**
 * 音声データを読み込んで再生する処理
 */
async function startAudio() {
  const { audioSource: source, audioAnalyser: analyser } =
    await createAudio();
  audioSource.current = source;
  audioAnalyser.current = analyser;

  // 再生
  audioSource.current.start();
}

~~~

useEffect(() => {
  if (!audioSource.current) startAudio();
});

これの役割は、createAudio関数を実行してaudioSourceとaudioAnalyserを作って取得することと、オーディオを再生することです。初期表示画面のクリックによってトリガーされるようにしておきます。
次にupdateAudioData関数。音声データを配列に変換します。

/**
 * 読み込んだ音声データを加工する処理
 */
function updateAudioData() {
  if (!audioAnalyser.current) return;

  // 音声データから周波数の配列を取得する
  const audioData = new Uint8Array(audioAnalyser.current.frequencyBinCount);
  audioAnalyser.current.getByteFrequencyData(audioData);

  // 加工
  const formattedData = audioData.reduce((x, y) => Math.max(x, y)) / 255;

  setAudioData(formattedData + 0.5);
  console.log(audioData);
}

~~~

useFrame(() => {
  updateAudioData();
});

useFrameはReact Three Fiberによって追加されるHookで、1フレームごとに実行されます。
こいつでupdateAudioData関数を連打することで、1フレームごとにAnalyzerNodeから周波数データを配列にコピーできるという寸法です。基本的には1秒あたり60フレームなので、これで再生中の音声データを適宜参照できるようになります。なりました。
ちなみにこの配列の長さはaudio.tsファイルにあるfftSizeの半分の値になりますので、デフォルト値であるfftSize = 2048であればarray.length = 1024です。
より細かく言うならfftSizeは2^5から2^15 までの2の累乗であって云々なんですがまあこの辺も書いてたら終わらないので省略。

Three.jsに反映する

そろそろ疲れてきたので、とりあえずこれまでできた動きを見てテンションを上げます。
updateAudioData関数でいい感じの数字になるように加工します。
これをThree.jsで作った青いBoxのScaleに入れると、useFrameによって毎フレームごとにaudioData Stateが更新され、音に合わせてガタガタ振動する謎の立方体が出来ます。
これをそれっぽくしたら完成です。先は長い。

それっぽくする

updateAudioData関数をあれこれすると大体いい感じになることはわかったので、これをあれこれして見せたいビジュアルを作っていきます。
Three.jsのBoxをPartialにしたり、色相をFrameごとに変えたりして粒子がワチャワチャするやつを作りたい。

「作りたいな~」とは思ったんですが、

残念ながら時間切れです。
Three.jsとDreiを使った粒子表現を組み込めれば相当それっぽい見た目になることが期待されています。
まあ基本的な部分はできたので暇なときにやりましょう。

できたもの

https://github.com/bugyepy/aoide

たぶんこの下に自動で挿入はされると思います、2回言っても損はしないので2回言います。
現在アルサーガパートナーズ株式会社は採用強化中です。エンジニアが足りません。経験者の方はぜひどうぞ。
ちなみにお仕事も募集しています。詳しくは公式サイトをチェックだ。
では下にあるだろう求人ボタンのクリックとSNSでのシェアをお願いします。

さようなら。

Arsaga Developers Blog

Discussion