點燈坊

失くすものさえない今が強くなるチャンスよ

從麥克風取得 PCM 格式檔案

Sam Xiao's Avatar 2024-02-09

Web API 內建的 MediaRecorder 並無法將麥克風的聲音儲存成 WAV 格式檔案,而是 Google 自己的 WebM 格式,必須使用 Web Audio API 的 ScriptProcessor 內自己將 WebM 轉成 PCM,才能夠產生 PCM 格式檔案下載。

Version

Vue 3.3

Architecture

pcm001

  • MediaStream: 從麥克風取得 stream
  • AudioContext: 產生 16 bit stream
  • AudioContext 產生 SrcNodeScriptNodeDestNode,需使用 connect() 將各 node 連結在一起
  • ScriptNode:將 WebMPCM

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:儲存音訊的 chunks array

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 建立 srcNodescriptNodedestNode
  • srcNode:起始節點
  • scriptNode:將 WebMPCM
  • destNode:結束節點

Line 34

srcNode.connect(scriptNode)
scriptNode.connect(destNode)
  • srcNodescriptNodedestNode 連在一起

Line 37

scriptNode.onaudioprocess = e => {
  let inputData = e.inputBuffer.getChannelData(0)
  if (isRecording === true) {
    for (let x of inputData) {
      chunks.push(x)
    }
  }
}
  • 麥克風的聲音會不斷地從 audioprocess event 傳入
  • getChannelData() 傳入 0,取得 Mono Channel 數據
  • isRecording 判斷是否在錄音
  • 將 inputData 依序塞入儲存音訊的 chunks array

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)
}
  • 設定 isRecordingfalse 表示 停止錄音
  • 開始將儲存音訊的 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