はじめに
ブラウザで、マイク入力の音声の解析とか、音の再生とか、エフェクタ的なものを作ってみたいなと思っていろいろ調べて試してみた。getUserMedia使ったらマイク使えて、audioContextとか使ったら解析できることがわかった。JavaScriptなんとなく知ってるくらいやったけど、コールバックとかPromiseとか少しわかった。勉強になった。
調べ物
まずは似たようなことしてる人探した。
マイクで音声キャプチャしてFFTして表示してる人見つけた。getUserMediaを使うと良いらしい。コード見ても、あんまりよくわからんかったので、もうちょい調べることにした。
getUserMediaで音声を拾いリアルタイムで波形を出力する - Qiita
getUserMediaを調べたら、この人の記事がシンプルでわかりやすかった。
navigator.mediaDevices.getUserMedia()ってのが新しいらしい。Promiseってコールバックじゃないやりかたしてる。なんか、こっちのほうが今風っぽい。
ブラウザからメディアデバイスを操る - getUserMedia()の基本 | CodeGrid
Promiseはここらへん呼んだ。
Promiseを使う - JavaScript | MDN
JavaScript入門者必見!Promiseの基礎の基礎を解説!(then, all, get) | 侍エンジニア塾ブログ | プログラミング入門者向け学習情報サイト
もうちょい調べると、navigator.mediaDevices.getUserMedia()使って波形表示してる人も見つけた。FFTはしてないけれど。このままだと動かなかったから、少し修正が必要だった。
試したこと
- マイクで音をキャプチャ
- 波形を表示
- FFTしてスペクトルを表示
まずは、こんな感じで試してみた。
コード
こんな感じで書いてみた。
主に、下記URLのコードを参考にさせてもらった。
ブラウザで音声入力の可視化と録音 - EagleLand
FFTするときはFFTポイント数の半分だけ表示した。ナイキスト周波数的に。標本化定理で出てくるやつ。
窓関数かけてたりすんのかな?下記サイトを見るとどうもデシベルで出てくるっぽい。
AnalyserNode.maxDecibels - Web APIs | MDN
querySelectorはここを確認した。
Document.querySelector() - Web API | MDN
サンプルレートはconsole.log(audioContext.sampleRate);
で確認。48kHzやった。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>microphone</title>
</head>
<body>
<canvas id="canvas1" width="400" height="300"></canvas>
<canvas id="canvas2" width="400" height="300"></canvas>
<script src="mic_test2.js"></script>
</body>
</html>
const canvas1 = document.querySelector('#canvas1');
const drawContext1 = canvas1.getContext('2d');
const canvas2 = document.querySelector('#canvas2');
const drawContext2 = canvas2.getContext('2d');
navigator.mediaDevices.getUserMedia({
audio: true,
video: false
}).then(stream => {
const audioContext = new AudioContext();
const sourceNode = audioContext.createMediaStreamSource(stream);
const analyserNode = audioContext.createAnalyser();
console.log(audioContext.sampleRate);
analyserNode.fftSize = 1024*2;
sourceNode.connect(analyserNode);
function draw1() {
const barWidth = canvas1.width / analyserNode.fftSize;
const time_array = new Uint8Array(analyserNode.fftSize);
analyserNode.getByteTimeDomainData(time_array);
drawContext1.fillStyle = 'rgba(0, 0, 0, 1)';
drawContext1.fillRect(0, 0, canvas1.width, canvas1.height);
for (let i = 0; i < analyserNode.fftSize; ++i) {
const value = time_array[i];
const percent = value / 255;
const height = canvas1.height * percent;
const offset = canvas1.height - height;
drawContext1.fillStyle = 'lime';
drawContext1.fillRect(i * barWidth, offset, 4*barWidth, 4);
}
requestAnimationFrame(draw1);
}
draw1();
function draw2() {
const barWidth = canvas2.width / (analyserNode.fftSize/2);
const freq_array = new Uint8Array(analyserNode.fftSize);
analyserNode.getByteFrequencyData(freq_array);
drawContext2.fillStyle = 'rgba(0, 0, 0, 1)';
drawContext2.fillRect(0, 0, canvas2.width, canvas2.height);
for (let i = 0; i < analyserNode.fftSize; ++i) {
const value = freq_array[i];
const percent = value / 255;
const height = canvas2.height * percent;
const offset = canvas2.height - height;
drawContext2.fillStyle = 'lime';
drawContext2.fillRect(i * barWidth, offset, 4*barWidth, 4);
}
requestAnimationFrame(draw2);
}
draw2();
});
結果
こんな感じで表示される。描画をLineにせずにRectangleにしてるから、ばたつくと見にくい気もする。なおそうとそこそこめんどくさそう。なんとなく、すべてのデータを描画してくれてそうな気はするけれど、よくわからん。
おわりに
いまいちJavaScriptのPromiseがわかってなかったので、調べながらやって時間かかった。最終的にシンプルな感じでできることがわかったので良かった。次は、エフェクターっぽいの作ってみたい。