【React】Audio Visualizer の実装

<!-- ============================================= -->
# 概要
<!-- ============================================= -->
**Web Audio API**を使用して、Audio Visualizerを実装します。

https://nemutas.github.io/app-audio-visualizer/
![output(video-cutter-js.com) (4).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/f9f72e79-419d-3fd3-014f-970b54cc1f6d.gif)

<!-- ============================================= -->
# ドキュメント
<!-- ============================================= -->

https://developer.mozilla.org/ja/docs/Web/API/Web_Audio_API/Visualizations_with_Web_Audio_API

<!-- ============================================= -->
# 視覚化の原理（FFT）
<!-- ============================================= -->
Web Audio APIでは、音源の波形を高速フーリエ変換（FFT）して視覚化します。

>FFT（Fast Fourier Transformation）高速フーリエ変換は、音響・振動測定分野において重要な解析手法です。FFTを使うことにより、ある信号をいくつかの周波数成分に分解し、それらの大きさをスペクトルとして表すことできます。用途として、機器や機械の異常の検出、品質管理、振動観測などがあります。ここでは、FFTの基本的な考え方と選択されたパラメータが測定結果にどのように反映されているかを説明します。
>
FFTとは、DFT（Discrete Fourier Transformation）離散フーリエ変換を求めるための最適化されたアルゴリズムと言うことができます。解析する信号波形を一定の時間で切り取り、この波形を周波数成分に分割して表します。これら離散的な周波数成分は、振幅と位相の異なる単純な正弦波です。一例を下の図に示します。測定された時間波形には三つの単純な周波数が含まれています。
>
>![FFT-Time-Frequency-View-540.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/dd5bcc62-dfbb-9009-15ec-2db5d0c3828f.png)

[高速フーリエ変換 - 基礎編](https://www.nti-audio.com/ja/%E3%82%B5%E3%83%9D%E3%83%BC%E3%83%88/%E6%B8%AC%E5%AE%9A%E3%83%8E%E3%82%A6%E3%83%8F%E3%82%A6/%E9%AB%98%E9%80%9F%E3%83%95%E3%83%BC%E3%83%AA%E3%82%A8%E5%A4%89%E6%8F%9B)

<!-- ============================================= -->
# 実装
<!-- ============================================= -->
以下のコードを参考に実装しました。

https://codepen.io/nfj525/pen/rVBaab

```.tsx: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**は、音源を再生したときに生成する必要があります。<br>
`useEffect`で作成しようとすると、音源データが正常に読み込めないので注意が必要です。

```.tsx
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
	}
	// ・・・
}
```

https://natural-tearoom.com/video-rules/

* bufferLength（周波数の分割数）は、**analyser.fftSizeで指定した大きさの1/2**になります。<br>
実装例の場合、bufferLengthは128になります。つまり、canvasに描画される縦棒の数は128個になります。

```.tsx
analyser.fftSize = 256
const bufferLength = analyserRef.current!.frequencyBinCount
```

* 音源の指定は、ビルドしたときの**index.htmlから見たファイルパス**にする必要があります。

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

<!-- ============================================= -->
# リポジトリ
<!-- ============================================= -->

https://github.com/nemutas/app-audio-visualizer

<!-- ============================================= -->
# 音源
<!-- ============================================= -->

https://youtu.be/DQweLHdlVWo

https://dova-s.jp/bgm/play10441.html