LoginSignup
0
3

More than 1 year has passed since last update.

【React】Audio Visualizer の実装

Last updated at Posted at 2021-11-11

概要

Web Audio APIを使用して、Audio Visualizerを実装します。

https://nemutas.github.io/app-audio-visualizer/
output(video-cutter-js.com) (4).gif

ドキュメント

視覚化の原理(FFT)

Web Audio APIでは、音源の波形を高速フーリエ変換(FFT)して視覚化します。

FFT(Fast Fourier Transformation)高速フーリエ変換は、音響・振動測定分野において重要な解析手法です。FFTを使うことにより、ある信号をいくつかの周波数成分に分解し、それらの大きさをスペクトルとして表すことできます。用途として、機器や機械の異常の検出、品質管理、振動観測などがあります。ここでは、FFTの基本的な考え方と選択されたパラメータが測定結果にどのように反映されているかを説明します。

FFTとは、DFT(Discrete Fourier Transformation)離散フーリエ変換を求めるための最適化されたアルゴリズムと言うことができます。解析する信号波形を一定の時間で切り取り、この波形を周波数成分に分割して表します。これら離散的な周波数成分は、振幅と位相の異なる単純な正弦波です。一例を下の図に示します。測定された時間波形には三つの単純な周波数が含まれています。

FFT-Time-Frequency-View-540.png

高速フーリエ変換 - 基礎編

実装

以下のコードを参考に実装しました。

App.tsx
import React, { useEffect, useRef, VFC } from 'react';
import { css } from '@emotion/css';

export const App: VFC = () => {
    const audioRef = useRef<HTMLAudioElement>(null)
    const canvasRef = useRef<HTMLCanvasElement>(null)
    const analyserRef = useRef<AnalyserNode>()
    const animeIdRef = useRef<number>()
    const volumeRef = useRef(0.05)

    /**
     * 曲を再生させたとき
     */
    const playHandler = () => {
        if (!analyserRef.current) {
            const audioContext = new AudioContext()
            const src = audioContext.createMediaElementSource(audioRef.current!)
            const analyser = audioContext.createAnalyser()
            src.connect(analyser)
            analyser.connect(audioContext.destination)
            analyser.fftSize = 256
            analyserRef.current = analyser
        }

        audioRef.current!.volume = volumeRef.current

        const bufferLength = analyserRef.current!.frequencyBinCount
        const dataArray = new Uint8Array(bufferLength)

        canvasRef.current!.width = window.innerWidth
        canvasRef.current!.height = window.innerHeight
        const ctx = canvasRef.current!.getContext('2d')!

        renderFrame(ctx, dataArray)
    }

    /**
     * フレーム毎にcanvasに描画する
     * @param ctx
     * @param dataArray
     */
    function renderFrame(ctx: CanvasRenderingContext2D, dataArray: Uint8Array) {
        const WIDTH = ctx.canvas.width
        const HEIGHT = ctx.canvas.height
        const dataLength = dataArray.length
        const barWidth = WIDTH / dataLength
        let x = 0

        analyserRef.current!.getByteFrequencyData(dataArray)

        ctx.fillStyle = '#1e1e1e'
        ctx.fillRect(0, 0, WIDTH, HEIGHT)

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

            const r = barHeight + 25 * (i / dataLength)
            const g = 250 * (i / dataLength)
            const b = 50

            ctx.fillStyle = 'rgb(' + r + ',' + g + ',' + b + ')'
            ctx.fillRect(x, HEIGHT / 2, barWidth, -barHeight)
            ctx.fillRect(x, HEIGHT / 2, barWidth, barHeight)

            x += barWidth + 1
        }

        animeIdRef.current = requestAnimationFrame(() => renderFrame(ctx, dataArray))
    }

    useEffect(() => {
        return () => {
            if (animeIdRef.current) {
                cancelAnimationFrame(animeIdRef.current)
            }
        }
    }, [])

    return (
        <div className={styles.container}>
            <canvas ref={canvasRef} className={styles.canvas} />
            <audio
                ref={audioRef}
                className={styles.player}
                controls
                loop
                src="./assets/たぬきちの冒険.mp3"
                onPlay={playHandler}
                onVolumeChange={() => (volumeRef.current = audioRef.current!.volume)}
            />
        </div>
    )
}

// ==============================================
// styles

const styles = {
    container: css`
        position: relative;
        width: 100vw;
        height: 100vh;
        display: flex;
        justify-content: center;
        align-items: center;
    `,
    canvas: css`
        width: 100%;
        height: 100%;
    `,
    player: css`
        position: absolute;
        bottom: 20px;
    `
}
  • analyserは、音源を再生したときに生成する必要があります。
    useEffectで作成しようとすると、音源データが正常に読み込めないので注意が必要です。
const playHandler = () => {
    if (!analyserRef.current) {
        const audioContext = new AudioContext()
        const src = audioContext.createMediaElementSource(audioRef.current!)
        const analyser = audioContext.createAnalyser()
        src.connect(analyser)
        analyser.connect(audioContext.destination)
        analyser.fftSize = 256
        analyserRef.current = analyser
    }
    // ・・・
}

  • bufferLength(周波数の分割数)は、analyser.fftSizeで指定した大きさの1/2になります。
    実装例の場合、bufferLengthは128になります。つまり、canvasに描画される縦棒の数は128個になります。
analyser.fftSize = 256
const bufferLength = analyserRef.current!.frequencyBinCount
  • 音源の指定は、ビルドしたときのindex.htmlから見たファイルパスにする必要があります。
<audio
    ref={audioRef}
    className={styles.player}
    controls
    loop
    src="./assets/たぬきちの冒険.mp3"
    onPlay={playHandler}
    onVolumeChange={() => (volumeRef.current = audioRef.current!.volume)}
/>

リポジトリ

音源

0
3
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
0
3