文章

前端语音数据处理

1. 初始化录音设备

async function initVoiceRecorder() {
  if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
    const constrains = {
      audio: {
        sampleRate: 16000,
        sampleSize: 16,
        channelCount: 1
      }
    }
    const mediaStream = await navigator.mediaDevices.getUserMedia(constrains)
    mediaRecorder = new MediaRecorder(mediaStream, {mimeType: 'audio/webm;codec=pcm'})
    mediaRecorder.audioChannels = 1; // 单声道
    mediaRecorderReady.value = true
  } else {
    console.error('getUserMedia is not supported')
  }
}

2. 重采样

async function webmToPcm(webmData) {
  const bitDepth = 16
  let audioContext = new AudioContext({sampleRate: 16000})
  let audioBuffer = await audioContext.decodeAudioData(webmData)
  let channelData = audioBuffer.getChannelData(0)
  let byteLength = bitDepth / 8 * channelData.length
  let pcmData = new ArrayBuffer(byteLength)
  let pcmDataView = new DataView(pcmData)
  for (let i = 0; i < channelData.length; i++) {
    let sample = Math.max(-1, Math.min(1, channelData[i]))
    let intSample = sample * ((1 << (bitDepth - 1)) - 1)
    for (let j = 0; j < bitDepth / 8; j++) {
      pcmDataView.setUint8(i * bitDepth / 8 + j, (intSample >> (8 * j)) & 0xff)
    }
  }
  return pcmData
}

3. 数据分块

function splitAudio(audioBlob, chunkSize) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = async () => {
      let webmData = reader.result
      let pcmData = await webmToPcm(webmData)
      let audioFrameChunks = []
      for (let i = 0; i < pcmData.byteLength; i += chunkSize) {
        const chunk = pcmData.slice(i, Math.min(i + chunkSize, pcmData.byteLength))
        audioFrameChunks.push(chunk)
      }
      resolve(audioFrameChunks)
    }
    reader.onerror = reject
    reader.readAsArrayBuffer(audioBlob)
  })
}

4. 声音录制

function startRecording() {
  let recordedChunks = []
  mediaRecorder.ondataavailable = event => {
    recordedChunks.push(event.data)
  }
  mediaRecorder.onstop = async () => {
    //连接服务器
    await connectToServer()
    //构造音频文件
    const audioBlob = new Blob(recordedChunks, {type: 'audio/webm;codec=pcm'})
    //注入播放器
    voiceUrl.value = URL.createObjectURL(audioBlob)
    //拆分数据块
    const frameChunks = await splitAudio(audioBlob, 1280)
    //发送数据
    for (let i = 0; i < frameChunks.length; i++) {
      const params = {
        data: {
          status: i === 0 ? 0 : 1,
          format: "audio/L16;rate=16000",
          encoding: "raw",
          audio: window.btoa(String.fromCharCode(...new Uint8Array(frameChunks[i]))),
        }
      }
      websocket.send(JSON.stringify(params))
      await delay(40)
    }
    const params = {
      data: {
        status: 2
      }
    }
    websocket.send(JSON.stringify(params))
  }
  mediaRecorder.start();
}

License:  CC BY 4.0