點燈坊

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

從麥克風取得 PCM 格式檔案

Sam Xiao's Avatar 2024-02-09

Web API 內建的 MediaRecorder 並無法將麥克風的聲音儲存成 PCM 格式檔案,而 extendable-media-recorder 提供與 MediaRecorder 相同的 interface,但可支援 PCM 格式。

Version

Vue 3.3
extendable-media-recorder 9.1.6
extendable-media-recorder-wav-encoder 7.0.101

Install Package

$ npm install extendable-media-recorder
$ npm install extendable-media-recorder-wav-encoder
  • extendable-media-recorder:提供與原生 MediaRecorder 相同 interface 的新 MediaRecorder
  • extendable-media-recorder-wav-encoder:支援 WAV 格式

Architecture

pcm001

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

Extendable Media Recorder

main.js

import { createApp } from 'vue'
import App from './App.vue'
import { register } from 'extendable-media-recorder'
import { connect } from 'extendable-media-recorder-wav-encoder'

await register(await connect())

createApp(App).mount('#app')

Line 7

await register(await connect())
  • 與原生 MediaRecorder 不同,extendable-media-recorder 所提供的 MediaRecorder 必須先註冊才能使用
  • register() 不能寫在每個頁面的 mounted(),只要 route 改變重新進入該 page,就會造成重複註冊的錯誤,因此只能寫在 main.js 只註冊一次

App.vue

<template>
  <button @click="onStart">Start</button>
  <button @click="onStop">Stop</button>
</template>

<script setup>
import { MediaRecorder } from 'extendable-media-recorder'

const SAMPLE_RATE = 16000
const CHANNEL_COUNT = 1
const SAMPLE_INTERVAL = 1000
const FILE_NAME = 'sample.pcm'

let mediaRecorder = null
let chunks = []

let onStart = async () => {
  try {
    let mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true })

    let audioContext = new AudioContext({ sampleRate: SAMPLE_RATE })

    let srcNode = new MediaStreamAudioSourceNode(audioContext, { mediaStream })
    let destNode = new MediaStreamAudioDestinationNode(audioContext, {
      channelCount: CHANNEL_COUNT
    })

    srcNode.connect(destNode)

    mediaRecorder = new MediaRecorder(destNode.stream, {
      mimeType: 'audio/wav'
    })

    mediaRecorder.ondataavailable = e => {
      chunks.push(e.data)
    }

    mediaRecorder.onstop = () => {
      let blob = new Blob(chunks)

      let link = document.createElement('a')
      link.href = URL.createObjectURL(blob)
      link.download = FILE_NAME
      link.click()

      URL.revokeObjectURL(link.href)
    }

    mediaRecorder.start(SAMPLE_INTERVAL)
  } catch (err) {
    console.warn(err)
  }
}

let onStop = () => {
  mediaRecorder.stop()
}
</script>

Line 7

import { MediaRecorder } from 'extendable-media-recorder'
  • 使用 extendable-media-recorder 所提供的 MediaRecorder 取代 Web API 內建的 MediaRecorder

Line 9

const SAMPLE_RATE = 16000
const CHANNEL_COUNT = 1
const SAMPLE_INTERVAL = 1000
const FILE_NAME = 'sample.pcm'

設定可改用的變數:

  • SAMPLE_RATE:取樣頻率
  • CHANNEL_COUNT:Mono channel
  • PERIOD:每次取樣時間
  • FILE_NAME:下載檔名

Line 19

let mediaStream = await navigator.mediaDevices.getUserMedia({
  audio: true,
})
  • 使用 Web API 內建的 navigator.mediaDevices.getUserMedia() 取得 MediaStream

Line 21

let audioContext = new AudioContext({ sampleRate: SAMPLE_RATE })
  • 要改變取樣頻率,必須使用 AudioContext

Line 23

let srcNode = new MediaStreamAudioSourceNode(audioContext, { mediaStream })
let destNode = new MediaStreamAudioDestinationNode(audioContext)
  • AudioConext 必須靠 node 方式運行
  • AudioContextMediaStream 建立 srcNode
  • AudioContext 建立 destNode

Line 28

srcNode.connect(destNode)
  • 使用 connect() 連結 srcNodedestNode

Line 30

mediaRecorder = new MediaRecorder(destNode.stream, {
  mimeType: 'audio/wav',
})
  • 使用 extendable-media-recorder 提供的 MediaRecorder,並改由 destNode 所處理過的 MediaStream
  • 指定 mineTypeaudio/wav

Line 55

let onStop = () => {
  mediaRecorder.stop()
}
  • 啟動 MediaRecoder 開始錄音

Line 34

mediaRecorder.ondataavailable = e => {
  chunks.push(e.data)
}
  • 觸發 dataavaliable event 取得音訊
  • 依序塞入儲存音訊的 chunks array

Line 38

mediaRecorder.onstop = () => {
  let blob = new Blob(chunks)

  let link = document.createElement('a')
  link.href = URL.createObjectURL(blob)
  link.download = FILE_NAME
  link.click()

  URL.revokeObjectURL(link.href)
}
  • chunks array 轉成 Blob
  • 建立臨時 url 下載 PCM 檔案

Conclusion

  • Web API 內建的 MediaRecorder 並不支援 WAV 格式
  • extendable-media-recorder 提供與 MediaRecorder 相同的 interface,但可支援 WAV 格式
  • extendable-media-recorderMediaRecorder 必須 register() 之後才能使用,且與 Vue 的生命週期不太一樣,不能在 mounted()register(),會造成重複註冊問題,必須改寫在 main.js 只重複一次