Web API 內建的 MediaRecorder 並無法將麥克風的聲音儲存成 WAV 格式檔案,而是 Google 自己的 WebM 格式,必須使用 Web Audio API 的 ScriptProcessor 內自己將 WebM 轉成 PCM,才能夠產生 PCM 格式檔案下載。
Version
Vue 3.3
Architecture
MediaStream: 從麥克風取得 streamAudioContext: 產生 16 bit stream- 由
AudioContext產生SrcNode、ScriptNode與DestNode,需使用connect()將各 node 連結在一起 ScriptNode:將WebM轉PCM
ScriptProcessor
<template>
<button @click="onStart">Start</button>
<button @click="onStop">Stop</button>
</template>
<script setup>
const SAMPLE_RATE = 16000
const FILE_NAME = 'sample.pcm'
let isRecording = false
let chunks = []
let encodePCM = (output, input) => {
let offset = 0
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)
}
return output
}
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
let arrayBuffer = new ArrayBuffer(chunks.length * 2)
let dataView = new DataView(arrayBuffer)
let pcms = encodePCM(dataView, chunks)
let blob = new Blob([pcms])
let link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = FILE_NAME
link.click()
chunks.length = 0
URL.revokeObjectURL(link.href)
}
</script>
Line 7
const SAMPLE_RATE = 16000
const FILE_NAME = 'sample.pcm'
設定可改變的變數:
SAMPLE_RATE:取樣頻率FILE_NAME:下載檔名
Line 10
let isRecording = false
let chunks = []
isRecording:是否正在錄音chunks:儲存音訊的chunksarray
Line 23
let onStart = async () => {
isRecording = true
}
- 當按下 Start 時,將
isRecording設定為true表示開始錄音
Line 27
let mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true })
- 使用 Web API 內建的
navigator.mediaDevices.getUserMedia()取得MediaStream
Line 28
let audioContext = new AudioContext({ sampleRate: SAMPLE_RATE })
- 使用
AudioContext產生 16 bit 的 stream
Line 30
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 34
srcNode.connect(scriptNode)
scriptNode.connect(destNode)
- 將
srcNode、scriptNode與destNode連在一起
Line 37
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 50
let onStop = () => {
isRecording = false
let arrayBuffer = new ArrayBuffer(chunks.length * 2)
let dataView = new DataView(arrayBuffer)
let pcms = encodePCM(dataView, chunks)
let blob = new Blob([pcms])
let link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = FILE_NAME
link.click()
chunks.length = 0
URL.revokeObjectURL(link.href)
}
- 設定
isRecording為false表示停止錄音 - 開始將儲存音訊的
chunks寫入PCM檔案
Line 13
let encodePCM = (output, input) => {
let offset = 0
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)
}
return output
}
- 將
WebM轉成PCM
Conclusion
- 使用 Audio API 的
ScriptProcessor就可將WebM轉成PCM,且不需使用MediaRecorder