🎤
AudioWorkletで録音したデータをwavで書き出す
完成イメージ
- デバイスのマイクから録音
- 停止したらwav形式のファイルを作成
- ダウンロードリンクを表示
成果物
- ソースコード : https://github.com/kanaru-ssk/audioworklet-wav
- デモ : https://kanaru-ssk.github.io/audioworklet-wav/
実装
index.html
button
タグと、ダウンロードリンクを追加するためのul
タグ
<!DOCTYPE html>
<html>
<body>
<button id="record">Record</button>
<ul id="fileList"></ul>
<script src="script.js" type="text/javascript"></script>
</body>
</html>
processor.js
processorは別ファイルに書き出す必要があるのでprocessor.js
を作成。
class WorkletProcessor extends AudioWorkletProcessor {
static get parameterDescriptors() {
return [];
}
constructor() {
super();
this.audioBuffer = [];
}
convertToFloat32ToInt16(inputs) {
const inputChannelData = inputs[0][0];
const data = Int16Array.from(inputChannelData, (n) => {
const res = n < 0 ? n * 32768 : n * 32767; // convert in range [-32768, 32767]
return Math.max(-32768, Math.min(32767, res)); // clamp
});
this.audioBuffer = Int16Array.from([...this.audioBuffer, ...data]);
if (this.audioBuffer.length >= 3200) {
this.port.postMessage({
eventType: "data",
audioBuffer: this.audioBuffer,
});
this.audioBuffer = [];
}
}
process(inputs) {
if (inputs[0].length === 0) {
console.error("From Convert Bits Worklet, input is null");
return false;
}
this.convertToFloat32ToInt16(inputs);
return true;
}
}
registerProcessor("worklet-processor", WorkletProcessor);
script.js
録音・wavの書き出しをするコード
const record = document.getElementById("record");
const chunks = [];
let localStream;
let context;
let source;
let worklet;
record.onclick = () => {
if (record.innerText === "Record") {
// マイクの許可を取得してから録音スタート
navigator.mediaDevices
.getUserMedia({ audio: true, video: false })
.then(startRecording);
} else {
// 録音停止
stopRecording(localStream);
}
};
// 録音スタート
async function startRecording(stream) {
localStream = stream;
context = new AudioContext();
source = context.createMediaStreamSource(stream);
await context.audioWorklet.addModule("processor.js");
worklet = new AudioWorkletNode(context, "worklet-processor");
worklet.port.onmessage = (e) => {
if (e.data.eventType === "data") {
chunks.push(e.data.audioBuffer);
}
};
source.connect(worklet);
worklet.connect(context.destination);
record.innerText = "Stop";
record.style.background = "red";
}
// 録音停止
function stopRecording(stream) {
stream.getTracks().forEach((track) => track.stop());
source.disconnect();
worklet.disconnect();
const wavRawData = [getWAVHeader(), ...chunks];
const blob = new Blob(wavRawData, { type: "audio/wav" });
const url = URL.createObjectURL(blob);
chunks.splice(0);
const fileName = `${new Date().toLocaleTimeString()}.wav`;
const link = document.createElement("a");
link.setAttribute("href", url);
link.setAttribute("download", fileName);
link.innerText = fileName;
const li = document.createElement("li");
li.appendChild(link);
const fileList = document.getElementById("fileList");
fileList.appendChild(li);
record.innerText = "Record";
record.style.background = "";
}
// wavファイルのヘッダーデータ作成
function getWAVHeader() {
const BYTES_PER_SAMPLE = Int16Array.BYTES_PER_ELEMENT;
const channel = 1;
const sampleRate = context.sampleRate;
const dataLength = chunks.reduce((acc, cur) => acc + cur.byteLength, 0);
const header = new ArrayBuffer(44);
const view = new DataView(header);
writeString(view, 0, "RIFF"); // RIFF identifier 'RIFF'
view.setUint32(4, 36 + dataLength, true); // file length minus RIFF identifier length and file description length
writeString(view, 8, "WAVE"); // RIFF type 'WAVE'
writeString(view, 12, "fmt "); // format chunk identifier 'fmt '
view.setUint32(16, 16, true); // format chunk length
view.setUint16(20, 1, true); // sample format (raw)
view.setUint16(22, channel, true); // channel count
view.setUint32(24, sampleRate, true); // sample rate
view.setUint32(28, sampleRate * BYTES_PER_SAMPLE * channel, true); // byte rate (sample rate * block align)
view.setUint16(32, BYTES_PER_SAMPLE * channel, true); // block align (channel count * bytes per sample)
view.setUint16(34, 8 * BYTES_PER_SAMPLE, true); // bits per sample
writeString(view, 36, "data"); // data chunk identifier 'data'
view.setUint32(40, dataLength, true); // data chunk length
return header;
}
function writeString(dataView, offset, string) {
for (let i = 0; i < string.length; i++) {
dataView.setUint8(offset + i, string.charCodeAt(i));
}
}
参考URL
以下のページを参考にさせて頂きました。
Discussion