はじめに
ブラウザ経由で音声を取得して、いろいろやりたくて調べた。最終的に、getUserMedia
してScriptProcessorNode
で音声データをバッファにためて、wavヘッダをつけてBlob
で保存した。
最初は、MediaRecorder
とか、AnalyserNode
とかで、録音したり解析したりしようとしたけど、なんだか思ったのと挙動が違った。
MediaRecorder
で保存しようとすると、webmとかいうのんで保存された。wavじゃできなかった。ブラウザ依存なんかもしれん。ChromeとFirefoxで試したけど、wavは無理やった。
AnalyserNode
は、データをFFTしたりできるけど、どうもすべてのバッファに対して処理するんじゃなくて、一定タイミングとか画面の更新タイミングで、そのときたまってるデータを解析してるっぽい。たぶん。
すべてのデータに対して処理するには、ScriptProcessorNode
とかAudioWorklet
っての使うらしい。最近は、ScriptProcessorNode
じゃなくてAudioWorklet
を使った方が良さそう。後継っぽい。AudioWorklet
でもよかったんだけど、やりたいこと調べてたら、ScriptProcessorNode
使って、似たようなことしてる人見つけたので、そっちを使うことにした。
調べてると、下記サイトが出てきた。これを真似したらできるはずなんやけど、Javascriptのこと、あまりわかってないので、試行錯誤した。中身ほぼコピペで恐縮やけど、僕みたいな初心者でも真似できるように、やったことを残しておく。
getUserMediaで録音したデータをWAVファイルとして保存する - Qiita
コード
こんな感じ。ひとつのコードに書いてるからわかりにくいけど、やってることはシンプルなはず。exportWAVでwavに変換してて、データを処理するだけなら、bufferData
とかaudioData
とかを使ったらいい。
<!DOCTYPE html>
<html lang="ja">
<head>
<title>Save audio data</title>
</head>
<body>
<a id="download">Download</a>
<button id="stop">Stop</button>
<script>
// for html
const downloadLink = document.getElementById('download');
const stopButton = document.getElementById('stop');
// for audio
let audio_sample_rate = null;
let scriptProcessor = null;
let audioContext = null;
// audio data
let audioData = [];
let bufferSize = 1024;
let saveAudio = function () {
downloadLink.href = exportWAV(audioData);
downloadLink.download = 'test.wav';
downloadLink.click();
audioContext.close().then(function () {
stopButton.setAttribute('disabled', 'disabled');
});
}
// export WAV from audio float data
let exportWAV = function (audioData) {
let encodeWAV = function (samples, sampleRate) {
let buffer = new ArrayBuffer(44 + samples.length * 2);
let view = new DataView(buffer);
let writeString = function (view, offset, string) {
for (let i = 0; i < string.length; i++) {
view.setUint8(offset + i, string.charCodeAt(i));
}
};
let floatTo16BitPCM = function (output, offset, input) {
for (let i = 0; i < input.length; i++ , offset += 2) {
let s = Math.max(-1, Math.min(1, input[i]));
output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
}
};
writeString(view, 0, 'RIFF'); // RIFFヘッダ
view.setUint32(4, 32 + samples.length * 2, true); // これ以降のファイルサイズ
writeString(view, 8, 'WAVE'); // WAVEヘッダ
writeString(view, 12, 'fmt '); // fmtチャンク
view.setUint32(16, 16, true); // fmtチャンクのバイト数
view.setUint16(20, 1, true); // フォーマットID
view.setUint16(22, 1, true); // チャンネル数
view.setUint32(24, sampleRate, true); // サンプリングレート
view.setUint32(28, sampleRate * 2, true); // データ速度
view.setUint16(32, 2, true); // ブロックサイズ
view.setUint16(34, 16, true); // サンプルあたりのビット数
writeString(view, 36, 'data'); // dataチャンク
view.setUint32(40, samples.length * 2, true); // 波形データのバイト数
floatTo16BitPCM(view, 44, samples); // 波形データ
return view;
};
let mergeBuffers = function (audioData) {
let sampleLength = 0;
for (let i = 0; i < audioData.length; i++) {
sampleLength += audioData[i].length;
}
let samples = new Float32Array(sampleLength);
let sampleIdx = 0;
for (let i = 0; i < audioData.length; i++) {
for (let j = 0; j < audioData[i].length; j++) {
samples[sampleIdx] = audioData[i][j];
sampleIdx++;
}
}
return samples;
};
let dataview = encodeWAV(mergeBuffers(audioData), audio_sample_rate);
let audioBlob = new Blob([dataview], { type: 'audio/wav' });
console.log(dataview);
let myURL = window.URL || window.webkitURL;
let url = myURL.createObjectURL(audioBlob);
return url;
};
// stop button
stopButton.addEventListener('click', function () {
saveAudio();
console.log('saved wav');
});
// save audio data
var onAudioProcess = function (e) {
var input = e.inputBuffer.getChannelData(0);
var bufferData = new Float32Array(bufferSize);
for (var i = 0; i < bufferSize; i++) {
bufferData[i] = input[i];
}
audioData.push(bufferData);
};
// getusermedia
let handleSuccess = function (stream) {
audioContext = new AudioContext();
audio_sample_rate = audioContext.sampleRate;
console.log(audio_sample_rate);
scriptProcessor = audioContext.createScriptProcessor(bufferSize, 1, 1);
var mediastreamsource = audioContext.createMediaStreamSource(stream);
mediastreamsource.connect(scriptProcessor);
scriptProcessor.onaudioprocess = onAudioProcess;
scriptProcessor.connect(audioContext.destination);
console.log('record start?');
// when time passed without pushing the stop button
setTimeout(function () {
console.log("10 sec");
if (stopButton.disabled == false) {
saveAudio();
console.log("saved audio");
}
}, 10000);
};
// getUserMedia
navigator.mediaDevices.getUserMedia({ audio: true, video: false })
.then(handleSuccess);
</script>
</body>
</html>
動作
Chromeで実行。
はじめに、マイクの許可を聞かれる。許可したら録音開始される。
インターフェースはStopボタンがあるだけ。Stop押したら、録音止まる。
録音が終わったら、自動でデータ保存のポップアップがでる。そのあとは、Downloadがリンクになって、保存のポップアップでるようになる。Stopボタンはdisabledされる。あんまり、長いデータを貯めるのも微妙かなと思って、10秒間押されなかったら、勝手に保存するようにしてみた。
おわりに
やりたいことから、紆余曲折して、最終的に、ScriptProcessorNode
に落ち着くまでが、それなりに時間かかった。でも、JavaScriptのクラスっぽい動きとか、変数のスコープとか、コールバックとかをそこそこ勉強できたので、よかった。変数を関数間で受け渡しするときに、めんどくさくてグローバル変数的なことしちゃったけど、もっとうまいことやりたいなと思う。エラー処理も、もっとちゃんとやりたい。
参照
ここを大いに参照。
getUserMediaで録音したデータをWAVファイルとして保存する - Qiita
AudioContext.createScriptProcessor() - Web API | MDN
JavaScriptのWeb Audio APIで録音してみる - saitodev.co
Blobを使って(jQueryを使わずに)JavaScriptでデータをファイルに保存 - Qiita
参照しなかったところ
調べてたけど、使わなかった。
ユーザーから音声データを取得する | Web | Google Developers
MediaRecorder.ondataavailable - Web API | MDN
[WebAudio API] AudioWorklet の使い方 | g200kg Music & Software
Web Audio API – AudioWorklet で遊ぶ – – rilakkuma3xjapan's blog
AnalyserNode.getByteTimeDomainData() - Web APIs | MDN