Web API 內建的 MediaRecorder 並無法將麥克風的聲音儲存成 WAV 格式檔案,而是 Google 自己的 WebM 格式,必須使用 Web Audio API 的 ScriptProcessor 內自己將 WebM 轉成 WAV,並加上 WAV 的 header,才能夠產生 WAV 格式檔案下載。
Version
Vue 3.3
Architecture
MediaStream: 從麥克風取得 streamAudioContext: 產生 16 bit stream- 由
AudioContext產生SrcNode、ScriptNode與DestNode,需使用connect()將各 node 連結在一起 ScriptNode:將WebM轉WAV
ScriptProcessor
<template>
<button @click="onStart">Start</button>
<button @click="onStop">Stop</button>
</template>
<script setup>
const SAMPLE_RATE = 16000
const FILE_NAME = 'sample.wav'
let isRecording = false
let chunks = []
let floatTo16BitPCM = (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)
}
}
let writeString = (view, offset, string) => {
for (let i = 0; i < string.length; i++) {
view.setUint8(offset + i, string.charCodeAt(i))
}
}
let encodeWAV = samples => {
let buffer = new ArrayBuffer(44 + samples.length * 2)
let view = new DataView(buffer)
/* RIFF identifier */
writeString(view, 0, 'RIFF')
/* RIFF chunk length */
view.setUint32(4, 36 + samples.length * 2, true)
/* RIFF type */
writeString(view, 8, 'WAVE')
/* format chunk identifier */
writeString(view, 12, 'fmt ')
/* format chunk length */
view.setUint32(16, 16, true)
/* sample format (raw) */
view.setUint16(20, 1, true)
/* channel count */
view.setUint16(22, 1, true)
/* sample rate */
view.setUint32(24, SAMPLE_RATE, true)
/* byte rate (sample rate * block align) */
view.setUint32(28, SAMPLE_RATE * 4, true)
/* block align (channel count * bytes per sample) */
view.setUint16(32, 1 * 2, true)
/* bits per sample */
view.setUint16(34, 16, true)
/* data chunk identifier */
writeString(view, 36, 'data')
/* data chunk length */
view.setUint32(40, samples.length * 2, true)
floatTo16BitPCM(view, 44, samples)
return view
}
let processPCMData = () => {
let depth16bitArrayBuffer = encodeWAV(chunks)
let blob = new Blob([depth16bitArrayBuffer])
let link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = FILE_NAME
link.click()
URL.revokeObjectURL(link.href)
}
let onStart = async () => {
isRecording = true
try {
let mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true })
let audioContext = new AudioContext({ sampleRate: SAMPLE_RATE })
let srcNode = audioContext.createMediaStreamSource(mediaStream)
let scriptNode = audioContext.createScriptProcessor(4096, 1, 1)
let destNode = audioContext.createMediaStreamDestination()
srcNode.connect(scriptNode)
scriptNode.connect(destNode)
scriptNode.onaudioprocess = e => {
let inputData = e.inputBuffer.getChannelData(0)
if (isRecording === true) {
for (let x of inputData) {
chunks.push(x)
}
}
}
} catch (err) {
console.warn(err)
}
}
let onStop = () => {
isRecording = false
processPCMData()
}
</script>
Line 7
const SAMPLE_RATE = 16000
const FILE_NAME = 'sample.wav'
設定可改變的變數:
SAMPLE_RATE:取樣頻率FILE_NAME:下載檔名
Line 10
let isRecording = false
let chunks = []
isRecording:是否正在錄音chunks:儲存音訊的chunksarray
Line 73
let onStart = async () => {
isRecording = true
}
- 當按下 Start 時,將
isRecording設定為true表示開始錄音
Line 77
let mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true })
- 使用 Web API 內建的
navigator.mediaDevices.getUserMedia()取得MediaStream
Line 78
let audioContext = new AudioContext({ sampleRate: SAMPLE_RATE })
- 使用
AudioContext產生 16 bit 的 stream
Line 80
let srcNode = audioContext.createMediaStreamSource(mediaStream)
let scriptNode = audioContext.createScriptProcessor(4096, 1, 1)
let destNode = audioContext.createMediaStreamDestination()
- 由
AudioContext建立srcNode、scriptNode與destNode srcNode:起始節點scriptNode:將WebM轉PCMdestNode:結束節點
Line 84
srcNode.connect(scriptNode)
scriptNode.connect(destNode)
- 將
srcNode、scriptNode與destNode連在一起
Line 87
scriptNode.onaudioprocess = e => {
let inputData = e.inputBuffer.getChannelData(0)
if (isRecording === true) {
for (let x of inputData) {
chunks.push(x)
}
}
}
- 麥克風的聲音會不斷地從
audioprocessevent 傳入 - 由
getChannelData()傳入0,取得 Mono Channel 數據 - 由
isRecording判斷是否在錄音 - 將 inputData 依序塞入儲存音訊的
chunksarray
Line 100
let onStop = () => {
isRecording = false
processPCMData()
}
- 設定
isRecording為false表示停止錄音 - 開始將儲存音訊的
chunks寫入WAV檔案
Line 63
let processPCMData = () => {
let depth16bitArrayBuffer = encodeWAV(chunks)
let blob = new Blob([depth16bitArrayBuffer])
let link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = FILE_NAME
link.click()
URL.revokeObjectURL(link.href)
}
- 處理成
WAV格式並下載
Line 26
let encodeWAV = samples => {
let buffer = new ArrayBuffer(44 + samples.length * 2)
let view = new DataView(buffer)
/* RIFF identifier */
writeString(view, 0, 'RIFF')
/* RIFF chunk length */
view.setUint32(4, 36 + samples.length * 2, true)
/* RIFF type */
writeString(view, 8, 'WAVE')
/* format chunk identifier */
writeString(view, 12, 'fmt ')
/* format chunk length */
view.setUint32(16, 16, true)
/* sample format (raw) */
view.setUint16(20, 1, true)
/* channel count */
view.setUint16(22, 1, true)
/* sample rate */
view.setUint32(24, sampleRate, true)
/* byte rate (sample rate * block align) */
view.setUint32(28, sampleRate * 4, true)
/* block align (channel count * bytes per sample) */
view.setUint16(32, 1 * 2, true)
/* bits per sample */
view.setUint16(34, 16, true)
/* data chunk identifier */
writeString(view, 36, 'data')
/* data chunk length */
view.setUint32(40, samples.length * 2, true)
floatTo16BitPCM(view, 44, samples)
return view
}
- 產生
WAV檔案的header
Line 20
let writeString = (view, offset, string) => {
for (let i = 0; i < string.length; i++) {
view.setUint8(offset + i, string.charCodeAt(i))
}
}
- 將字串寫以 8 bit 寫入
Line 13
let floatTo16BitPCM = (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)
}
}
- 將
WebM轉成PCM
Conclusion
- 使用 Audio API 的
ScriptProcessor就可將WebM轉成WAV,且不需使用MediaRecorder