こんにちは。きんくまです。
最近 Web Audioについて調べていました。
今回は、Web AudioでSoundCloudにあるみたいな波形を描画したいです。
どんなやつかといいますと、こんなやつです。
簡単にやりたい
一番簡単なのは、wavesurfer.jsというライブラリを使うことです。
今回の記事はこのwavesurferのソースを参考にさせていただき、自前で作ります。
>> katspaugh/wavesurfer.js: Navigable waveform built on Web Audio and Canvas
手順
1. ajaxで音ファイルを読み込み
2. decodeAudioDataでAudioBufferを取得
3. Float32Arrayの配列から特定の区間ごとにサンプルの最大値(Peak)を求める
4. Canvasで描画
Web Audioの入門はMDNを読むとわかりやすかったです。
今度入門記事(といってもほとんどMDNのまま)を書きたいと思います。
>> Web Audio API – Web APIs | MDN
1. ajaxで音ファイルを読み込み
まずはajaxで音ファイルを読み込みます。
今回 PeakAnalyzer というクラスを作成しました。このクラスは上の手順でいうところの 1 – 3 を行うものです。
export default class PeakAnalyzer { audioCtx = new AudioContext(); promise = null; constructor(){ } /** * 音ファイルからPeakを取得します * @param url 分析する音ファイル * @param peakLength 欲しいpeakの配列の長さ * @return {*} */ analyze(url, peakLength){ this.promise = new Promise((resolve, reject)=>{ const req = new XMLHttpRequest(); req.open('GET', url, true); req.responseType = 'arraybuffer'; req.onload = ()=>{ if(req.status === 200){ this.onLoadSound(req.response, peakLength, resolve, reject); }else{ reject(req.statusText); } }; req.send(); }); return this.promise; }
音ファイルを読み込むと、ArrayBufferが返ってきます。req.responseの中身です。
このままだと単なるデータの配列なので、これをWebAudioで使える形にします。 decodeAudioData で AudioBuffer に変換します。
2. decodeAudioDataでAudioBufferを取得
onLoadSound(audioData, peakLength, resolve, reject){ this.audioCtx.decodeAudioData(audioData).then((buffer)=>{ const ch1 = buffer.getChannelData(0); const peaks1 = this.getPeaks(ch1, peakLength); const ch2 = buffer.getChannelData(1); const peaks2 = this.getPeaks(ch2, peakLength); resolve([peaks1, peaks2]); }).catch((error)=>{ reject(error); }); }
このあたりは、以下のMDNに書いてあります。
>> BaseAudioContext.decodeAudioData()
それで、今回はステレオの音ファイルなので、AudioBufferからチャンネルごとにpeakをもとめることにしました。
getChannelDataのあたりです。
3. Float32Arrayの配列から特定の区間ごとにサンプルの最大値(Peak)を求める
getChannelDataから出てくるのは Float32Array の配列です。
-1 〜 1 までの値が入っています。
ここから、区間ごとに最大値を求めて、それを新規配列に詰めます。
getPeaks(array, peakLength){ let step; if(!peakLength){ peakLength = 9000; } step = Math.floor(array.length / peakLength); if(step < 1){ step = 1; } let peaks = []; for(let i = 0, len = array.length; i < len; i += step){ const peak = this.getPeak(array, i, i + step); peaks.push(peak); } return peaks; } getPeak(array, startIndex, endIndex){ const sliced = array.slice(startIndex, endIndex); let peak = -100; for(let i = 0, len = sliced.length; i < len; i++){ const sample = sliced[i]; if(sample > peak){ peak = sample; } } return peak; }
なんで区間ごとに区切るのかといいますと、Float32Arrayから音サンプルを間引いているといいますかそんな感じです。
Canvasで波形を描画するには、サンプル数がものすごく多すぎます。なので、Canvasでいい感じに描画できるように音を間引いています。
これでpeakの配列が求まりました。
4. Canvasで描画
そうしたらCanvasで描画してみましょう!
今回Vue.jsの中で描画していますが、Vueの中じゃない普通のCanvasでも、もちろん全く問題ないです。
import PeakAnalyzer from "./models/PeakAnalyzer"; export default{ data(){ return{ analyzer:null, canvas:null } }, components:{ }, mounted(){ this.canvas = document.querySelector('.web-audio-sample .waveform'); this.analyzer = new PeakAnalyzer(); let fileUrl; fileUrl = "sounds/sound_filename.mp3"; this.analyzer.analyze(fileUrl, this.canvas.width * 10) .then((peaksArr)=>{ this.drawWaveform(peaksArr[0], peaksArr[1]); }).catch((err)=>{ console.error(err); }); }, methods:{ drawWaveform(ch1, ch2){ const canvas = this.canvas; const ctx = canvas.getContext('2d'); const barMargin = 0; const barWidth = canvas.width / ch1.length - barMargin; const canvasH = canvas.height; const halfCanvasH = canvasH / 2; ctx.fillStyle = '#1355a5'; let sample; let barHeight; for(let i = 0, len = ch1.length; i < len; i++){ //ch1 sample = ch1[i]; barHeight = sample * halfCanvasH; ctx.fillRect(i * (barWidth + barMargin), halfCanvasH - barHeight, barWidth, barHeight); //ch2 sample = ch2[i]; barHeight = sample * halfCanvasH; ctx.fillRect(i * (barWidth + barMargin), halfCanvasH, barWidth, barHeight); } } } }
実行結果
無事に波形が描画されました!! waveform.jsとほとんど同じ結果になりました。
あとは、描画したい波形をイメージしてパラメータを調整してあげれば、SoundCloudみたいなこんな感じのやつもできます。
おまけ PeakAnalyzerクラス
本文ではぶつぶつと途切れていましたが、PeakAnalyzerクラスの全体です。
ちなみに、最初decodeするのに OfflineAudioContext を使っていたのですが、音を特に加工しない分には普通の AudioContext で問題ないみたいです。
export default class PeakAnalyzer { audioCtx = new AudioContext(); promise = null; constructor(){ } /** * 音ファイルからPeakを取得します * @param url 分析する音ファイル * @param peakLength 欲しいpeakの配列の長さ * @return {*} */ analyze(url, peakLength){ this.promise = new Promise((resolve, reject)=>{ const req = new XMLHttpRequest(); req.open('GET', url, true); req.responseType = 'arraybuffer'; req.onload = ()=>{ if(req.status === 200){ this.onLoadSound(req.response, peakLength, resolve, reject); }else{ reject(req.statusText); } }; req.send(); }); return this.promise; } onLoadSound(audioData, peakLength, resolve, reject){ this.audioCtx.decodeAudioData(audioData).then((buffer)=>{ const ch1 = buffer.getChannelData(0); const peaks1 = this.getPeaks(ch1, peakLength); const ch2 = buffer.getChannelData(1); const peaks2 = this.getPeaks(ch2, peakLength); resolve([peaks1, peaks2]); }).catch((error)=>{ reject(error); }); } getPeaks(array, peakLength){ let step; if(!peakLength){ peakLength = 9000; } step = Math.floor(array.length / peakLength); if(step < 1){ step = 1; } let peaks = []; for(let i = 0, len = array.length; i < len; i += step){ const peak = this.getPeak(array, i, i + step); peaks.push(peak); } return peaks; } getPeak(array, startIndex, endIndex){ const sliced = array.slice(startIndex, endIndex); let peak = -100; for(let i = 0, len = sliced.length; i < len; i++){ const sample = sliced[i]; if(sample > peak){ peak = sample; } } return peak; } }
■ 自作iPhoneアプリ 好評発売中!
・フォルメモ - シンプルなフォルダつきメモ帳
・ジッピー電卓 - 消費税や割引もサクサク計算!
■ LINEスタンプ作りました!
毎日使える。とぼけたウサギ