本記事はVoicy Advent Calendar 2022の11日目の記事です。
はじめに
こんにちは、株式会社VoicyでWebフロントエンジニアをしているきーくん(@komura_c)です。今回は、業務に関わる内容ではないのですが、以前に趣味で開発したオーディオ波形動画を生成するWebぺージの内容を整理してまとめました。ぜひ読んでみてください!
この記事の内容
主な機能として、画像の読み込み、オーディオファイルの読み込み、波形の描画、キャプチャ、動画として出力をWebフロントエンドで行うWebぺージを作成しました。初めはHTML&CSSと素のJavaScriptで作っていたのですが、Next.jsに移行しました。そのため、TypeScriptとReactを使用したコードが出てきます。 music-waves-visualizer.vercel.app
次の順番で説明していきたいと思います。
- はじめに
- この記事の内容
- オーディオ処理(Web Audio API)
- 波形描画処理(Canvas API)
- 動画キャプチャ処理(MediaStream Recording API)
- webmをmp4に変換する処理(ffmpeg.wasm)
- おわりに
オーディオ処理(Web Audio API)
まず、オーディオ処理ではWeb Audio APIを使用しました。 Web Audio APIとは、AudioContextを用いて、音声をルーティングできるように設計されたWeb APIです。 AudioNodeというものを接続することで、複数の音源を1つのコンテキスト内で扱うことができます。
AudioContextとは、AudioNodeという音声処理モジュールで表現されたインターフェースです。AudioNodeの作成と音声処理、デコードの実行の全てを制御する役割を持っています。実際にはこのAudioNode同士をルーティング、繋げていくことで、様々な音声処理を行うことができます。
この辺りはMDNを参考にしました。より詳しい内容については次のリンクを参照してください。 developer.mozilla.org
オーディオルーティング構成
今回使っているAudioNode同士のルーティング構成の図です。
オーディオ処理の流れ
処理の流れは次のようになっています。
1.初期ロード時に、AnalyserNode、MediaStreamAudioDestinationNodeを作成
// Audio State const audioCtxRef = useRef<AudioContext>(null); const streamDestinationRef = useRef<MediaStreamAudioDestinationNode>(null); const analyserRef = useRef<AnalyserNode>(null); useEffect(() => { // AudioContext audioCtxRef.current = new AudioContext(); // AnalyserNode const analyserNode = audioCtxRef.current.createAnalyser(); analyserNode.fftSize = 2048; analyserRef.current = analyserNode; // MediaStreamAudioDestinationNode(動画出力用) const steamDest = audioCtxRef.current.createMediaStreamDestination(); streamDestinationRef.current = steamDest; }, []);
2.ファイル選択時に、ファイルを読み込み、AudioBufferを作成
// AudioLoadEvent const audioLoad = async (event: { target: HTMLInputElement }) => { const audioFile = event.target.files[0]; try { const arraybuffer = await audioFile.arrayBuffer(); decodedAudioBufferRef.current = await audioCtxRef.current.decodeAudioData( arraybuffer ); setPlaySoundDisabled(false); setRecordMovieDisabled(false); openSnackBar("音楽を読み込みました"); } catch (error) { openSnackBar(error); } };
3.再生時に、AudioBufferSourceNodeを作成、AudioNodeを接続、再生
const setAudioBufferSourceNode = () => { // AudioBufferSourceNode作成 const audioBufferSourceNode = audioCtxRef.current.createBufferSource(); audioBufferSourceNode.buffer = decodedAudioBufferRef.current; audioBufferSourceNode.loop = false; // Node接続 audioBufferSourceNode.connect(analyserRef.current); audioBufferSourceNode.connect(audioCtxRef.current.destination); audioBufferSourceNode.connect(streamDestinationRef.current); audioBufferSrcRef.current = audioBufferSourceNode; }; ... // PlaySoundEvent const onPlaySound = () => { ... audioBufferSrcRef.current.start(0); ... };
波形描画処理(Canvas API)
次に、波形を描画する処理では、Canvas APIを使用しました。
Canvas APIとは、JavaScriptと、HTMLの<canvas>
タグの要素によってグラフィックを描画できるものです。Web上でのアニメーションや、ゲームのグラフィック、データの可視化、写真加工、リアルタイム動画処理などに応用されています。
Canvas自体は描画するだけなため、これをアニメーションにするには、変更がある度にCanvasを更新する処理をする必要があります。
そこでWindow.requestAnimationFrame
というメソッドをJavaScriptから呼びます。
Window.requestAnimationFrame
とは、指定した関数を引数として渡し、再描画毎にその関数を呼び出し続け、アニメーションを更新するメソッドです。コールバックの回数は、通常毎秒60回(または、ディスプレイのリフレッシュレート=描画フレーム数)とされています。
この辺りもMDNを参考にしました。より詳しい内容については次のリンクを参照してください。 developer.mozilla.org
波形描画処理の流れ
処理の流れは次のようになっています。
1.canvas要素を生成し、window.requestAnimationFrame
に更新関数を渡す
2.更新関数にAnalyzerNodeを引数として渡しているため、音声再生のタイミングで描画される
// Canvas const canvasRef = useRef<HTMLCanvasElement>(null); const reqIdRef = useRef<number>(null); // Canvas用ImageContext const [imageCtx, setImageCtx] = useState<HTMLImageElement>(null); // Canvas Animation useEffect(() => { if (!canvasRef.current) { return; } reqIdRef.current = requestAnimationFrame(function () { return drawBars(canvasRef.current, imageCtx, mode, analyserRef.current); }); return () => cancelAnimationFrame(reqIdRef.current); }, [imageCtx, mode]);
動画キャプチャ処理(MediaStream Recording API)
次に、動画に出力するためのキャプチャ処理では、MediaStream Recording APIを使用しました。
MediaStream Recording APIとは、MediaStreamオブジェクトまたは HTMLMediaElement オブジェクトによって生成されたデータを分析、処理、または保存のためにキャプチャすることができるものです。<audio>
, <video>
タグである、HTMLMediaElementもメディアソースとして扱うことができます。
この辺りもMDNを参考にしました。より詳しい内容については次のリンクを参照してください。 developer.mozilla.org
動画キャプチャ処理の流れ
処理の流れは次のようになっています。
1.MediaStreamAudioDestinationNodeのデータをMediaStreamに変換したものとCanvas要素をMediaStreamに変換したものを繋げて、新しいMediaStreamを作成
2.作成したMediaStreamをMediaRecorderに変換
3.音声再生のタイミングで録音開始をする(MediaRecorder.start())
4.音声停止のタイミングで録音停止をする(MediaRecorder.stop())
// RecordMovieEvent const onRecordMovie = () => { const audioStream = streamDestinationRef.current.stream; const canvasStream = canvasRef.current.captureStream(); const outputStream = new MediaStream(); [audioStream, canvasStream].forEach((stream) => { stream.getTracks().forEach(function (track: MediaStreamTrack) { outputStream.addTrack(track); }); }); //ストリームからMediaRecorderを生成 const recorder = new MediaRecorder(outputStream, { mimeType: "video/webm;codecs=h264", }); const recordedBlobs: Blob[] = []; recorder.addEventListener("dataavailable", (e) => { recordedBlobs.push(e.data); }); ...
webmをmp4に変換する処理(ffmpeg.wasm)
動画キャプチャ処理で使った、MediaRecorderは開発した時点では、mp4形式で保存ができなかったので、webm形式で保存しています。 これをなんとかmp4形式に対応するために、ffmpeg.wasmを使用しています。 この処理に関しては、過去に当ブログ記事でまとめているため、そちらをご覧ください。
おわりに
オーディオ波形動画を生成するWebぺージを開発した内容をまとめてみました。 かなり説明を省いた所はありましたが、参考になれば幸いです。
また、作成したページの全コードはGithubに上げてあるので興味があればご覧ください。 github.com
Webとオーディオ領域というのは奥が深く、以前当ブログでも紹介したWeb Speech APIなど様々なWeb APIが提供されています。 今後さらに進歩していくのが楽しみですし、Web版のVoicyの開発にも何らかの形で活かしていきたいと考えています。
明日は、@yuji0316さんです。
採用情報
Voicyのエンジニアチームに少しでも興味をお持ちになった方は、ぜひこちらもご覧ください。 www.wantedly.com