音を可視化してみた
音声ファイルの読み込み(再)
最初に戻って音声ファイルを再生してみます。 今度はaudio要素を使わずに、Web Audio APIの機能を使って再生してみます。 audio要素に比べると手順は増えますが、できるようになることも増えます。
今回の参考資料はこちら。詳しい説明や情報はこちらのサイトをご覧ください。
Web Audio API | 5. オーディオファイルを鳴らす
https://webmusicdevelopers.appspot.com/codelabs/webaudio/index.html?ja-jp#5
AudioContextをつかってみる
https://qiita.com/keiskimu/items/fe8a3924ff7203a6dd73
<input type="file" accept="audio/*" id="recorder">
<button id="play">Play</button>
<script>
var recorder = document.getElementById('recorder');
var play = document.getElementById('play');
// AudioContextを作成
window.AudioContext = window.AudioContext || window.webkitAudioContext;
var audioCtx = new AudioContext();
// 波形データを格納するバッファ
var audioBuffer = null;
// --------------------
// ファイルを指定してバイナリデータを取得
// --------------------
recorder.addEventListener('change', function(e) {
// 指定されたローカルファイルの File オブジェクトを取得
var file = e.target.files[0];
var fr = new FileReader();
// --------------------
// 読み込んだバイナリデータを音声データに変換
// --------------------
fr.onload = function() {
// arrayBufferに音声ファイルのバイナリデータを入れる
var arrayBuffer = fr.result;
// オーディオファイルデータをPCMオーディオデータに変換
audioCtx.decodeAudioData(arrayBuffer, function(abuffer) {
// デコードしたデータをバッファに格納
audioBuffer = abuffer;
});
};
// --------------------
// 指定されたファイルを読み込む
// --------------------
// 読み込みが完了すると次のことが起こる。
// ・loadend イベントが発生。
// ・result プロパティにファイルのデータを表す ArrayBuffer が格納される。
fr.readAsArrayBuffer(file);
});
// --------------------
// サウンドを再生
// --------------------
var PlaySound = function(abuffer) {
// AudioBufferSourceNode を作成
// このノードは、AudioBufferオブジェクトに書き込まれた音声データを再生できる。
source = audioCtx.createBufferSource();
// ファイルから読み込んだ AudioBuffer オブジェクトを渡す。
source.buffer = abuffer;
// source -> 音声出力先
source.connect(audioCtx.destination);
// 再生開始
source.start(0);
}
window.onload = function() {
// --------------------
// サウンドを再生
// --------------------
play.addEventListener('click', function() {
PlaySound(audioBuffer);
});
};
</script>
サンプル
PlaySoundの関数内を見てもらうと分かる通り、これまでとほとんど同じ手順で再生することができます。つまり音を大きくしたりなどの加工も同じようにできるということです。Web Audio API なんだか簡単な気がしてきた!
createBufferSource()
で作成されたAudioBufferSourceNode
インスタンス(音声の入れ物)が今回の音源になります。
createBufferSource()
で作成されたAudioBufferSourceNode
は最初は空っぽなのでそのままでは音はなりません。
buffer
プロパティに音声データを突っ込んでやると音源として機能し始めます。
ではその音声データは何かと言うと、ファイルから読み込んだオーディオファイルデータ(まだArrayBuffer
)をdecodeAudioData()
でAudioBuffer
にデコードたものです。
ファイルから読み込んだArrayBuffer
は単なる固定長のバイナリデータなのでそのままでは再生できないわけです。振幅値がそのまま記録されているwavなら特にデコードなんて必要なさそうな気がしますが、mp3とかoggとかなら圧縮されてますからね。
さらにオーディオファイルデータはFile API(→外部リンク)を使って読み込みます。
readAsArrayBuffer()
で読み込みが成功するとfr.onload
が実行され、AudioBuffer
への変換が行われます。
ファイルの選択は、<input type="file">
で行っています。
大まかにまとめると、読み込んだバイナリデータをdecodeAudioData()
で音声にデコードして、createBufferSource
で準備した入れ物にデコードした音声を突っ込む。
という流れみたいですね。あれ以外と準備が面倒かも…。
音声ファイルを可視化
資料を書き写して、更にいろいろ付け足してみました。技術的に詳しい話は、参考資料元を参照ください。
今回の資料はこちらになります。
7. 音声ファイルを可視化する
参考資料:https://webmusicdevelopers.appspot.com/codelabs/webaudio/index.html?ja-jp#7
Frequency/TimeDomainをFrequencyにすると再生中の音声をFFT(フーリエ変換)したものが表示され、TimeDomainにすると波形表示になります。 Frequencyに設定するとその下の3項目の設定が画像表示に反映されます。
SmoothingTimeConstant は平均化定数(0.0~1.0)です。0の場合は時間平均しません。1に近いほどスペクトルはゆっくりと動きます。内部的にはオーバーラップさせてるんでしょうか…調べたけどよくわかりませんでした。
MinDecibels、MaxDecibels は画像の縦軸の最小値、最大値です。
いずれも再生中に数値を変更すると反映されます。
<table>
<tr><td>Frequency/TimeDomain : </td><td><select id="mode" ><option>Frequency</option><option>TimeDomain</option></select></td></tr>
<tr><td>SmoothingTimeConstant : </td><td><input type="text" id="smoothing" value="0.9"/></td></tr>
<tr><td>MinDecibels : </td><td><input type="text" id="min"/></td></tr>
<tr><td>MaxDecibels : </td><td><input type="text" id="max"/></td></tr>
</table>
<br/><br/>
<canvas id="graph" width="512" height="256"></canvas>
<script>
var recorder = document.getElementById('recorder');
var playsound = document.getElementById('playsound');
var inputBoxMin = document.getElementById("min");
var inputBoxMax = document.getElementById("max");
var cvgraph = document.getElementById("graph");
var ctx = cvgraph.getContext("2d");
// 変数初期化
var mode = 0;
// 0 = Frequency 横軸周波数
// 1 = TimeDomain 横軸時間
var timerId;
var source
// AudioContextを作成
window.AudioContext = window.AudioContext || window.webkitAudioContext;
var audioCtx = new AudioContext();
// 波形データを格納するバッファ
var audioBuffer = null;
// 音声の時間と周波数を解析するAnalyserNodeを生成
var analyser = audioCtx.createAnalyser();
// ここからは描画処理の部分
// --------------------
// 描画処理
// --------------------
function DrawGraph() {
// 背景初期化
ctx.fillStyle = "rgba(34, 34, 34, 1.0)";
ctx.fillRect(0, 0, cvgraph.width, cvgraph.height);
// 描画開始
// Uint8Array … 0~255
if(mode==0) {
// --------------------
// 周波数スペクトル
// --------------------
// ナイキスト周波数で折り返し済のデータなのでサイズは fftSize/2 になります。
var sz = analyser.frequencyBinCount;
var dataArray = new Uint8Array(sz);
// 周波数データを取得して描画
analyser.getByteFrequencyData(dataArray);
ctx.fillStyle = "rgba(204, 204, 204, 0.8)";
for(var i = 0; i < sz; ++i) {
ctx.fillRect(i*2, cvgraph.height - dataArray[i], 1, dataArray[i]);
}
} else {
// --------------------
// 時間軸波形
// --------------------
// 取得値は、0~255、無音は128
// 描画領域を1024pxにすると全波形が入ります。
var sz = analyser.fftSize;
var dataArray = new Uint8Array(sz);
// 波形データを取得して描画
analyser.getByteTimeDomainData(dataArray);
ctx.strokeStyle="rgba(255, 255, 255, 1)";
ctx.beginPath();
var h = cvgraph.height;
for(var i = 0; i < sz; ++i) {
// 無音が描画領域の中心に来るように描画
ctx.lineTo(i, h/2 - (dataArray[i]-128));
// 縦軸を描画範囲に合わせて調整する場合
// ctx.lineTo(i, h/2 - ((dataArray[i]-128) * h/256));
}
ctx.stroke();
}
// 再描画
requestAnimationFrame(DrawGraph);
}
// --------------------
// 分析・描画の設定
// --------------------
function Setup(){
// 表示モード
mode = document.getElementById("mode").selectedIndex;
// getByteFrequencyDataメソッドで取得可能なデシベルの下限
if(inputBoxMin.value!=""){
analyser.minDecibels = parseFloat(Number(inputBoxMin.value));
}
// getByteFrequencyDataメソッドで取得可能なデシベルの上限
if(inputBoxMax.value!=""){
analyser.maxDecibels = parseFloat(Number(inputBoxMax.value));
}
// 平均化定数(0.0~1.0)
// 0の場合は平均化しません。1にするとスペクトルはゆっくりと動きます。
analyser.smoothingTimeConstant = parseFloat(document.getElementById("smoothing").value);
}
// ここからは音の再生処理部分
// --------------------
// ファイルを指定してバイナリデータを取得
// --------------------
recorder.addEventListener('change', function(e) {
// 指定されたローカルファイルの File オブジェクトを取得
var file = e.target.files[0];
var fr = new FileReader();
// --------------------
// 読み込んだバイナリデータを音声データに変換
// --------------------
fr.onload = function() {
// 読み込んだデータを再生できる形式に変換
var arrayBuffer = fr.result;
audioCtx.decodeAudioData(arrayBuffer, function(abuffer) {
audioBuffer = abuffer;
});
// 分析に使用する設定
analyser.fftSize = 1024; // 高速フーリエ変換のデータサイズ
// 分析条件として設定されている情報を取得
// 取得可能なデシベルの下限
inputBoxMin.value = analyser.minDecibels;
// 取得可能なデシベルの上限
inputBoxMax.value = analyser.maxDecibels;
};
// --------------------
// 指定されたファイルを読み込む
// --------------------
fr.readAsArrayBuffer(file);
});
// --------------------
// サウンドを再生
// --------------------
var PlaySound = function(abuffer) {
// AudioBufferSourceNode を作成
source = audioCtx.createBufferSource();
source.buffer = abuffer;
// source -> analyser
// └-> destination
source.connect(analyser);
source.connect(audioCtx.destination);
// 再生開始
source.start(0);
}
window.onload = function() {
// 再生/停止
document.getElementById("playsound").addEventListener("click", function(event){
var label;
if(event.target.innerHTML=="Stop") {
// サウンドを停止
source.stop(0);
// アニメーション停止
cancelAnimationFrame(timerId);
label="Start";
} else {
// サウンドを再生
PlaySound(audioBuffer);
label="Stop";
}
event.target.innerHTML=label;
});
// Setup再実行イベント
document.getElementById("mode").addEventListener("change", Setup);
document.getElementById("smoothing").addEventListener("change", Setup);
document.getElementById("min").addEventListener("change", Setup);
document.getElementById("max").addEventListener("change", Setup);
};
// アニメーションの開始
timerId = requestAnimationFrame(DrawGraph);
// 設定を反映
Setup();
</script>
サンプル
今回の接続イメージ図はこんな感じ。
source -> analyser └-> destination
analyserの先でFFTしたり波形表示したりしています。
ここでのanalyserはcreateAnalyser
で作成したAnalyserNode
です。これを使うとFFTや波形データが用意に取得できるようになります。使い方はDrawGraph()
内に書かれてある通りです。
DrawGraph()
の実行は、requestAnimationFrame()
メソッドから行っています。
requestAnimationFrame()
メソッドはアニメーションの更新直前に引数で指定した関数を実行します。
つまりこのプログラムでは、アニメーション更新のタイミングでFFTや波形データ取得などの計算を行っています。
アニメーションの更新タイミングは、たいてい毎秒 60 回らしいのですが、使用状況により異なるそうです。
サンプルではサンプリング点数fftSize
はデフォルト値の2048ポイントの固定です。
サンプリング周波数sampleRate
が仮に44.1kHzの音声だった場合、1秒間に約21回(≒44.1k÷2048)FFT計算ができます。(オーバーラップなしの場合なのでありならもっと数が増えます。このサンプルの場合、ありなのかなしなのか確認できてません!)
アニメーションの更新タイミング毎秒 60 回なら3コマぐらい同じデータが続くということになりますね。
また何らかの原因でブラウザへの負荷が高まり、アニメーションの更新タイミングが毎秒 21 回を下回ると、画面に表示されないFFT計算結果が出てきます。この計算だと一瞬なのでわからない気もしますが…。
とまあ、画面表示とFFT結果の関係はこういう意味を持っています。
オーディオビジュアライザーを作るのであればこのサンプルのような実装で何も問題ないと思います。
FFT結果の重複や欠落は少し気持ち悪いので、これについての対策は後述します。
マイク音声を可視化
同じ要領でマイクの音を可視化してみます。
Web Audio API | 8. マイク入力を可視化する https://webmusicdevelopers.appspot.com/codelabs/webaudio/index.html?ja-jp#8
// --------------------
// マイクを開始
// --------------------
var astream, micsrc;
function Mic(){
// ユーザーにマイクの許可を求めます。
navigator.mediaDevices.getUserMedia({ audio: true, video: false })
.then(function(stream){
astream = stream; // マイクを停止する際に使用
micsrc = audioCtx.createMediaStreamSource(stream);
micsrc.connect(audioCtx.destination);
micsrc.connect(analyser);
// micsrc -> analyser
// └-> destination
})
.catch(function(e) { console.error(e)});
}
// --------------------
// マイクのON/OFF
// --------------------
document.querySelector("button#startmic").addEventListener("click", function(event){
var label;
if(event.target.innerHTML=="Start Mic") {
// マイク ON
Mic();
label="Stop Mic";
} else {
// マイク OFF
(astream.getAudioTracks())[0].stop();
label="Start Mic";
}
event.target.innerHTML=label;
});
// アニメーションの開始
timerId = requestAnimationFrame(DrawGraph);
// 設定を反映
Setup();
音声ファイルの可視化のコードに、マイクをAnalyserNode
につなぐ処理を書き加えただけです。あとはこれに付随する処理を書くだけで完成です。技術的な細かいことが知りたい場合は、参考資料の方を御覧ください。
…といってもその付随する処理がめんどくさいんですが。サンプルあさって一番扱いやすい実装方法を探す必要がありそうですね。
音を分析
ここまでは音の分析は、画像の描画サイクルに合わせて行ってきました。 オーディオビジュアライザーのようにリアルタイムに可視化・反映させるのには適した方法です。しかし厳密な解析という視点から見るとたった1/60秒サイクル程度でしか分析を行わず、しかもCPU負荷次第によっては分析周期が早くなったり遅くなったり変化してしまうというとても不正確で当てにならない結果です。 そこでここでは正確に余すことなくデータを使用して、取りこぼしのない正確な分析結果を出す方法での実装をやってみます。
ここでの参考資料は…多分この辺だったと思います。
getUserMediaで音声を拾いリアルタイムで波形を出力する
https://qiita.com/mhagita/items/6c7d73932d9a207eb94d
(1-1) <input type="file" accept="audio/*" capture="microphone" id="recorder"><br>
(1-2) ファイルを再生/ストップ : <button id="playsound">Play</button><br>
(2-1) マイク On / Off : <button id="startmic">Start Mic</button><br>
<canvas id="canvas_id" width="512" height="256"></canvas><br>
色を変更:
<button id="jet">jet</button>
<button id="hsv">hsv</button>
<button id="hot">hot</button>
<button id="original">元の画像</button><br>
<button id="plotcmap">カラーマップ再描画</button><br>
<script src="colormap.js"></script>
<script>
var recorder = document.getElementById('recorder');
var playsound = document.getElementById('playsound');
var inputBoxMin = document.getElementById("min");
var inputBoxMax = document.getElementById("max");
// カラーマップ
var cve = document.getElementById("canvas_id");
var ctx = cve.getContext('2d');
// 変数初期化
var timerId;
var source;
// AudioContextを作成
window.AudioContext = window.AudioContext || window.webkitAudioContext;
var audioCtx = new AudioContext();
// 波形データを格納するバッファ
var audioBuffer = null;
// 音声の時間と周波数を解析するAnalyserNodeを生成
var analyser = audioCtx.createAnalyser();
// 平均化定数(0.0~1.0)
// 0の場合は時間平均化しません。1にするとスペクトルはゆっくりと動きます。
analyser.smoothingTimeConstant = 0.0;
// カラーマップのデータ
var mapValue = [];
var mapValueIdx = 0;
// カラーパレット作成
var clrPlt = new Object();
clrPlt.jet = colorMapArray('jet');
clrPlt.hsv = colorMapArray('hsv');
clrPlt.hot = colorMapArray('hot');
// --------------------
// カラーマップ
// --------------------
// 背景初期化
ctx.fillStyle = "rgba(34, 34, 34, 1.0)";
ctx.fillRect(0, 0, cve.width, cve.height);
// カラーマップ描画
var src;
var dst;
function RedrawColorMap(){
// canvasの大きさを適当に変更
cve.width = window.innerWidth; // ウィンドウサイズ
cve.height = window.innerHeight/2;
// mapValue をcanvasに描画
var w = cve.width;
var h = cve.height;
var res = DrawColorMap(ctx, mapValue, 0, 0, w, h);
// Canvasのデータ列を取得
src = ctx.getImageData(0, 0, w, h); // 元の画像
dst = ctx.createImageData(src.width, src.height); // 作業領域
}
document.getElementById('plotcmap').addEventListener('click', RedrawColorMap);
// カラーマップを押されたボタンの色に変更
document.querySelector('#jet').addEventListener('click', function(){
ctx.putImageData( GradationMap(src, dst, clrPlt.jet), 0, 0);
});
document.querySelector('#hsv').addEventListener('click', function(){
ctx.putImageData( GradationMap(src, dst, clrPlt.hsv), 0, 0);
});
document.querySelector('#hot').addEventListener('click', function(){
ctx.putImageData( GradationMap(src, dst, clrPlt.hot), 0, 0);
});
document.querySelector('#original').addEventListener('click', function(){
ctx.putImageData(src, 0, 0);
});
// --------------------
// FFTしてデータを蓄積
// --------------------
// FFTした結果を変数 mapValue に蓄積して、最後に図に描画します。
var processor = audioCtx.createScriptProcessor(1024,1,1)
processor.onaudioprocess = function(e){
// この関数内で e.outputBuffer.getChannelData(0); とすると波形データが取得できる。
// ただし今回はFFTするだけなので引数は使用しない。
var dataArray = new Uint8Array(analyser.frequencyBinCount);
// 周波数データを取得して描画
analyser.getByteFrequencyData(dataArray);
// 配列をコピー
mapValue[mapValueIdx] = dataArray.slice();
mapValueIdx = mapValueIdx + 1;
}
// --------------------
// ファイルを指定してバイナリデータを取得
// --------------------
recorder.addEventListener('change', function(e) {
// 指定されたローカルファイルの File オブジェクトを取得
var file = e.target.files[0];
var fr = new FileReader();
// --------------------
// 読み込んだバイナリデータを音声データに変換
// --------------------
fr.onload = function() {
// 読み込んだデータを再生できる形式に変換
var arrayBuffer = fr.result;
audioCtx.decodeAudioData(arrayBuffer, function(abuffer) {
audioBuffer = abuffer;
});
// 分析に使用する設定
analyser.fftSize = 1024; // 高速フーリエ変換のデータサイズ
};
// --------------------
// 指定されたファイルを読み込む
// --------------------
fr.readAsArrayBuffer(file);
});
// --------------------
// サウンドを再生
// --------------------
var PlaySound = function(abuffer) {
// AudioBufferSourceNode を作成
source = audioCtx.createBufferSource();
source.buffer = abuffer;
// source -> analyser -> processor -> destination(分析)
// └-> destination(音を再生)
source.connect(analyser);
analyser.connect(processor);
processor.connect(audioCtx.destination);
source.connect(audioCtx.destination);
// 再生開始
source.start(0);
}
window.onload = function() {
// 再生/停止
document.getElementById("playsound").addEventListener("click", function(event){
var label;
if(event.target.innerHTML=="Stop") {
// サウンドを停止
processor.disconnect();
source.stop(0);
// 再描画
RedrawColorMap();
label="Start";
} else {
// サウンドを再生
PlaySound(audioBuffer);
label="Stop";
mapValue = [];
mapValueIdx = 0;
}
event.target.innerHTML=label;
});
};
// --------------------
// マイクを開始
// --------------------
var astream, micsrc;
function Mic(){
// ユーザーにマイクの許可を求めます。
navigator.mediaDevices.getUserMedia({ audio: true, video: false })
.then(function(stream){
astream = stream; // マイクを停止する際に使用
micsrc = audioCtx.createMediaStreamSource(stream);
micsrc.connect(audioCtx.destination);
micsrc.connect(analyser);
analyser.connect(processor);
processor.connect(audioCtx.destination);
// source -> analyser -> processor -> destination(分析)
// └-> destination(音を再生)
})
.catch(function(e) { console.error(e)});
}
// --------------------
// マイクのON/OFF
// --------------------
document.querySelector("button#startmic").addEventListener("click", function(event){
var label;
if(event.target.innerHTML=="Start Mic") {
// マイク ON
Mic();
label="Stop Mic";
} else {
// マイク OFF
(astream.getAudioTracks())[0].stop();
processor.disconnect();
label="Start Mic";
// 再描画
RedrawColorMap();
}
event.target.innerHTML=label;
});
</script>
サンプル
途中でcolormap.jsというjsファイルを読み込んだりいろいろとしていますが、canvasを使ってカラーマップを書くための処理です。今回はあまり気にしないで、実行してみてください。
音声ファイルまたはマイクの音を使用できます。
音声ファイルならStartを押してStopで止める。
マイクならStart Micお押して録音開始し、Stop Micで録音を終わる。
再生または録音を終えると、FFT分析結果が下の四角の中にカラーマップで表示されます。
横軸が時間で、縦軸が周波数。色で大きさを示しています。
画像の再描画のタイミングでのみ分析を行うようにしていると、CPU負荷増やユーザー操作により時折FFTができない事があるようで、歯抜けになったカラーマップになることがあります。 今回の実装では全てのデータを確実に使用する方法なので、歯抜けになることはありません。代わりに処理がすべて終わるまで再生時間より長くかかることがあります。
さて今回のノードの構成はこの様になっています。
source -> analyser -> processor -> destination(分析) └-> destination(音を再生)
今回はこれまでよりも、もう少し詳細分析がしてみたいのでcreateScriptProcessor()
メソッドを使ってScriptProcessorNode
オブジェクトを作成しています。
ScriptProcessorNode
のonaudioprocess
プロパティに関数を指定すると、指定したバッファが音声データで埋まるたびにonaudioprocess
イベントが発生して、指定された関数が実行されます。今回のサンプルではこれまで同様、getByteFrequencyData
でFFT結果を取得しています。
onaudioprocess
イベントはstop
を実行しても発生し続けます。ここではstop
を実行するときに、disconnect
でノードを切断することでイベントが発生しないようにしています。これってFFT分析装置につながったケーブルを引っこ抜くイメージでいいんでしょうか。いいのかこれで…。
onaudioprocess
メソッドの動作についてはこちらのサイトがわかりやすいと思います。
WEB SOUNDER Web Audio API 解説
https://weblike-curtaincall.ssl-lolipop.jp/portfolio-web-sounder/webaudioapi-basic/custom
このサンプルでは、FFT結果は一旦全て変数mapValue
に格納し、音声の収録または再生が終わったらカラーマップにプロットしています。canvasに自力で描画しているのでちょっと重いです。カラーマップの描画については、Web Audio APIと関係ないのでまた別の機会に。
今回は使用しませんでしたが、createScriptProcessor
を使えば.outputBuffer.getChannelData()
でバッファ内の波形データが取得できるのでFFT処理などを自前で書くこともできるようです。波形データそのものを取得できるので、用意された関数で実現できないような複雑なことなど何でもできるということになります。…それなりに知識が必要なわけですが。(´・ω・`)
Web Audio APIは一旦ここで一区切り
これでひと通りWeb Audio APIの基礎的なところに触れることができたような気がします。 全体を通してみるといろいろ書くことがあって複雑な仕組みのようにも感じるのですが、よくよく見てみると入力デバイスが異なってもあとの処理は同じに書けたり、AV機器をつなぐようにノードをつないでいくだけで様々な処理が可能だったりとシンプルな作りになっています。意外と簡単かもという気がしてきました。 あとは、音に反応してかっこいい絵を作ったり、操作に反応して音を出したりしてみたいのでもう少し調べる必要ありそうです。…しかしWeb Audio APIの習得が目的じゃないのでもっと楽をしたいです。やっぱいいライブラリ探すか!