概要
Web Audio APIを使用して、Audio Visualizerを実装します。
https://nemutas.github.io/app-audio-visualizer/
ドキュメント
視覚化の原理(FFT)
Web Audio APIでは、音源の波形を高速フーリエ変換(FFT)して視覚化します。
FFT(Fast Fourier Transformation)高速フーリエ変換は、音響・振動測定分野において重要な解析手法です。FFTを使うことにより、ある信号をいくつかの周波数成分に分解し、それらの大きさをスペクトルとして表すことできます。用途として、機器や機械の異常の検出、品質管理、振動観測などがあります。ここでは、FFTの基本的な考え方と選択されたパラメータが測定結果にどのように反映されているかを説明します。
FFTとは、DFT(Discrete Fourier Transformation)離散フーリエ変換を求めるための最適化されたアルゴリズムと言うことができます。解析する信号波形を一定の時間で切り取り、この波形を周波数成分に分割して表します。これら離散的な周波数成分は、振幅と位相の異なる単純な正弦波です。一例を下の図に示します。測定された時間波形には三つの単純な周波数が含まれています。
実装
以下のコードを参考に実装しました。
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)}
/>
リポジトリ
音源