LoginSignup
55
49

More than 1 year has passed since last update.

React(Next.js)+WebAudioで簡単なオーディオプレイヤー+スペクトラムアナライザをつくる

Last updated at Posted at 2021-10-28

趣味で音楽をつくっているのですが、
それ用の特設サイトをNext.jsでつくったのにあわせてオーディオプレイヤー(スペクトラムアナライザ付き)をReact+WebAudioでつくってみました。

実際の結果
https://romot.studio

その記録です。

事前

  • 単純に再生だけなら<audio>だけあればWebAudioいらない
  • そうでなくてもReact用音声プレイヤーライブラリたくさんあるのでその方がいい
  • 今回使わなかったが、スペクトラムアナライザだけでなく
    • プレイリスト
    • 曲やその再生時間にあわせた背景変更
    • 曲の再生秒数にあわせた歌詞など表示

などもつけたかったので必要だったという前提
↓こんな感じで一応実装はしていた

test.gif

※ イラストはゆーはくさま

要素を置く

プリロードなどが面倒なので、audioタグを利用しています。

/pages/index.js

import { useState, useEffect, useRef } from 'react';

export default function Home() {
  const audioRef = useRef(null); // ref経由でaudio要素利用
  const timePositionRef = useRef(null); // ref経由でタイムスライダー要素利用
  const spectrumRef = useRef(null); // ref経由でcanvas要素利用(スペクトラムアナライザ用)
  return (
    <>
      {/* 開始/停止ボタン */}
      <button
        type="button"
      >
       停止
            </button>
           {/* タイムスライダー */}
      <input 
        type="range"
        ref={timePositionRef}
      >
      {/* audio */}
      <audio 
       src="/demo.mp3" 
       ref={audioRef} 
      />
      <canvas className="spectrums" ref={spectrumRef} />
    </>
  );
}

初期化

初期化し、WebAudio経由でも操作できるようにします。

export default function Home() {
  ...
  const audioCtxRef = useRef(null); // ref経由でAudioContext利用
  const [source, setSource] = useState(null); // MediaElementSource
  const [analyserNode, setAnalyserNode] = useState(null); // アナライザー

  // 初期化
  useEffect(() => {
    // 初期化時にAudioContextを作成
    audioCtxRef.current = new AudioContext();
    // createMediaElementSource
    const elementSource = audioCtxRef.current.createMediaElementSource(audioRef.current);
    // スペクトラムアナライザー用node
    const analyser = audioCtxRef.current.createAnalyser();
    // nodeを接続
    elementSource.connect(analyser).connect(audioCtxRef.current.destination);
    setSource(elementSource);
    setAnalyserNode(analyser);
  },[]);

  return (...;  

プレイヤー機能をつける

再生・停止、スライダーで位置変更など

export default function Home() {
  ...
  const [playState, setPlayState] = useState('stop'); // 再生状態
  const [duration, setDuration] = useState(0); // 曲の総時間
  const [timePosition, setTimePosition] = useState(0); // タイムスライダーの位置

  // 再生/停止ボタンの操作
  const handleTogglePlay = () => {
    // 初期状態から再生開始
    if (audioCtxRef.current.state === 'suspended'){
      audioCtxRef.current.resume();
      setPlayState('play');
    }

    // 再開
    if (playState === 'stop') {
      audioRef.current.play();
      setPlayState('play');
    // 停止
    if (playState === 'play'){
      audioRef.current.pause();
      setPlayState('stop');
    }
  };

  // スライダーを操作で再生位置変更
  const handleTimeUpdate = () => {
    setTimePosition(audioRef.current.currentTime);
  };

  // スライダーを操作で再生位置変更
  const handleChangeTimePosition = (e) => {
    const position = parseInt(e.target.value);
    setTimePosition(position);
    audioRef.current.currentTime = position;
  };

  // 最後まで再生したら最初に戻り、停止
  const handleEnded = () => {
    setTimePosition(0);
    setPlayState('stop');
  };

  // 音声メタデータ取得時点で総合計時間を設定
  const handleLoadedMetadata = () => {
    const duration = audioRef.current.duration;
    setDuration(duration);
  };

  return (
    <>
      {/* 開始/停止ボタン */}
      <button
        ...
        onClick={handleTogglePlay}
      >
       {playState === 'stop' && '開始'}
       {playState === 'play' && '停止'}
            </button>
           {/* タイムスライダー */}
      <input 
        ...
        min={0}
        max={duration}
        value={timePosition}
        onInput={handleChangeTimePosition}
      >
      {/* audio */}
      <audio 
       ...
       onTimeUpdate={handleTimeUpdate}
       onLoadedMetadata={handleLoadedMetadata}
       onEnded={handleEnded}
      />
    </>
  );  

スペクトラムアナライザ

どこかで見たものをほぼそのまま使いましたが、ソースが見つかりませんでした(ごめんなさい)

上の画像だと背景画像がアナライザ的に色が変わっていますが、同じ形でCSSのfilterでざっくりやっています。

export default function Home() {
  ...
  useEffect(() => {
    if (source && analyserNode && playState === 'play') {
      const canvas = spectrumRef.current;
      canvas.width = window.innerWidth;
      canvas.height = window.innerHeight;
      const canvasCtx = canvas.getContext('2d');
      analyserNode.fftSize = 16384; // FFTサイズ
      const canvasWidth = canvas.width;
      const canvasHeight = canvas.height;
      const bufferLength = analyserNode.frequencyBinCount;
      const dataArray = new Uint8Array(bufferLength);
      const barWidth = 1; // バーの横幅

      let barHeight;
      let x = 0;

      function renderFrame() {
        requestAnimationFrame(renderFrame);

        x = 0;

        analyserNode.getByteFrequencyData(dataArray);
        canvasCtx.clearRect(0, 0, canvasWidth, canvasHeight);
        let bars = 128; // バーの数

        for (let i = 0; i < bars; i++) {
          barHeight = (dataArray[i]);

          canvasCtx.fillStyle = `rgba(255,255,255,0.8)`;
          canvasCtx.fillRect(x, (canvasHeight - barHeight), barWidth, barHeight);  
          x += barWidth + (canvasWidth / 128);
        }
      }
      renderFrame();
    }
  },[playState]);  // 再生状態からhook


  ...

  return (...

全体

import { useState, useEffect, useRef } from 'react';

export default function Home() {
  const [playState, setPlayState] = useState('stop');
  const [duration, setDuration] = useState(0);
  const [timePosition, setTimePosition] = useState(0);
  const [source, setSource] = useState(null);
  const [analyserNode, setAnalyserNode] = useState(null);
  const audioRef = useRef(null);
  const audioCtxRef = useRef(null);
  const timePositionRef = useRef(null);
  const spectrumRef = useRef(null);

  useEffect(() => {
    audioCtxRef.current = new AudioContext();
    const elementSource = audioCtxRef.current.createMediaElementSource(audioRef.current);
    const analyser = audioCtxRef.current.createAnalyser();
    elementSource.connect(analyser).connect(audioCtxRef.current.destination);
    setSource(elementSource);
    setAnalyserNode(analyser);
  },[]);

  useEffect(() => {
    if (source && analyserNode && playState === 'play') {
      const canvas = spectrumRef.current;
      canvas.width = window.innerWidth;
      canvas.height = window.innerHeight;
      const canvasCtx = canvas.getContext('2d');
      analyserNode.fftSize = 16384;
      const canvasWidth = canvas.width;
      const canvasHeight = canvas.height;
      const bufferLength = analyserNode.frequencyBinCount;
      const dataArray = new Uint8Array(bufferLength);
      const barWidth = 1;

      let barHeight;
      let x = 0;

      function renderFrame() {
        requestAnimationFrame(renderFrame);

        x = 0;

        analyserNode.getByteFrequencyData(dataArray);
        canvasCtx.clearRect(0 ,0, canvasWidth, canvasHeight);
        let bars = 128;

        for (let i = 0; i < bars; i++) {
          barHeight = (dataArray[i]);

          canvasCtx.fillStyle = `rgba(255,255,255,0.8)`;
          canvasCtx.fillRect(x, (canvasHeight - barHeight), barWidth, barHeight);  
          x += barWidth + (canvasWidth / 128);
        }
      }
      renderFrame();
    }
  },[playState]);

  const handleTogglePlay = () => {
    if (audioCtxRef.current.state === 'suspended'){
      audioCtxRef.current.resume();
      setPlayState('play');
    }

    if (playState === 'stop') {
      audioRef.current.play();
      setPlayState('play');
    } 
       if (playState === 'play') {
      audioRef.current.pause();
      setPlayState('stop');
    }
  };

  const handleTimeUpdate = () => {
    setTimePosition(audioRef.current.currentTime);
  };

  const handleEnded = () => {
    setTimePosition(0);
    setPlayState('stop');
  };

  const handleLoadedMetadata = () => {
    const duration = audioRef.current.duration;
    setDuration(duration);
  };

  const handleChangeTimePosition = (e) => {
    const position = parseInt(e.target.value);
    setTimePosition(position);
    audioRef.current.currentTime = position;
  };

  return (
    <>
      <button
        type="button"
        onClick={handleTogglePlay}
      >
       {playState === 'stop' && '開始'}
       {playState === 'play' && '停止'}
            </button>
      <input 
        type="range"
        min={0}
        max={duration}
        value={timePosition}
        onInput={handleChangeTimePosition}
      >
      <audio 
       src="/demo.mp3"
       ref={audioRef} 
       onTimeUpdate={handleTimeUpdate}
       onLoadedMetadata={handleLoadedMetadata}
       onEnded={handleEnded}
      />
      <canvas className="spectrums" ref={spectrumRef} />
    </>
  )
}

なにかの参考になれば幸いです。
間違いなどありましたらおしらせください。

55
49
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
55
49