html5音频录制解决方案

3961次浏览

前言

前段时间,项目中用到html5音频采集,主要是和微信录音一样,流程是按住说话,右侧滑动可以音频转文字,左侧滑动撤销。关于按住说话及左右侧滑动交互,相对简单,主要是运用了onTouchStart,onTouchMove,onTouchEnd三个事件完成。我之前文章也有过模仿微信语音播放效果动画,其中左右侧滑动位置主要是根据 e.targetTouches[0].pageX,和 e.targetTouches[0].pageY来完成的。本文着重讲解一下html5音频采集解决方案。

之所以叫解决方案,因为这里有涉及了音频采集的各种问题,例如音波,格式转换,录制格式,多段音频拼接,多个语音合成等等。语音采集我主要用了。

关于音频采集

音频采集可以利用navigator.mediaDevices.getUserMedia({ audio: true }),来自己手动采集。 判断有无权限及开启权限可以如下代码

export const checkIsOpenPermission = () => {
  if (navigator.mediaDevices === undefined) {
    navigator.mediaDevices = {}
  }
  // 一些浏览器部分支持 mediaDevices。我们不能直接给对象设置 getUserMedia
  // 因为这样可能会覆盖已有的属性。这里我们只会在没有getUserMedia属性的时候添加它。
  if (navigator.mediaDevices.getUserMedia === undefined) {
    let getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia
    navigator.mediaDevices.getUserMedia = function (constraints) {
      // 首先,如果有getUserMedia的话,就获得它
      // 一些浏览器根本没实现它 - 那么就返回一个error到promise的reject来保持一个统一的接口
      if (!getUserMedia) {
        return Promise.reject(new Error('getUserMedia is not implemented in this browser'))
      }
      // 否则,为老的navigator.getUserMedia方法包裹一个Promise
      return new Promise(function (resolve, reject) {
        getUserMedia.call(navigator, constraints, resolve, reject)
      })
    }
  }
  navigator.mediaDevices
    .getUserMedia({ audio: true })
    .then(function (stream) {
      // 用户成功开启
      console.log('user allow audio')
    })
    .catch(function (error) {
      switch (error.code || error.name) {
        case 'PERMISSION_DENIED':
        case 'PermissionDeniedError':
          Toast.show({
            style: {
              zIndex: 1000000
            },
            content: '用户拒绝提供信息'
          })
          break
        case 'NOT_SUPPORTED_ERROR':
        case 'NotSupportedError':
          Toast.show({
            style: {
              zIndex: 1000000
            },
            content: '浏览器不支持硬件设备。'
          })
          break
        case 'MANDATORY_UNSATISFIED_ERROR':
        case 'MandatoryUnsatisfiedError':
          Toast.show({
            style: {
              zIndex: 1000000
            },
            content: '无法发现指定的硬件设备。'
          })
          break
        default:
          Toast.show({
            style: {
              zIndex: 1000000
            },
            content: '无法打开麦克风'
          })
          break
      }
    })
}

多个音频的拼接

// 拼接音频的方法
const concatAudio = (arrBufferList) => {
    // 获得 AudioBuffer
    const audioBufferList = arrBufferList;
    // 最大通道数
    const maxChannelNumber = Math.max(...audioBufferList.map(audioBuffer => audioBuffer.numberOfChannels));
    // 总长度
    const totalLength = audioBufferList.map((buffer) => buffer.length).reduce((lenA, lenB) => lenA + lenB, 0);

    // 创建一个新的 AudioBuffer
    const newAudioBuffer = audioContext.createBuffer(maxChannelNumber, totalLength, audioBufferList[0].sampleRate);
    // 将所有的 AudioBuffer 的数据拷贝到新的 AudioBuffer 中
    let offset = 0;

    audioBufferList.forEach((audioBuffer, index) => {
        for (let channel = 0; channel < audioBuffer.numberOfChannels; channel++) {
            newAudioBuffer.getChannelData(channel).set(audioBuffer.getChannelData(channel), offset);
        }

        offset += audioBuffer.length;
    });

    return newAudioBuffer;
}

// 获取音频AudioBuffer

// AudioContext
const audioContext = new AudioContext();
// 基于src地址获得 AudioBuffer 的方法
const getAudioBuffer = (src) => {
    return new Promise((resolve, reject) => {
        fetch(src).then(response => response.arrayBuffer()).then(arrayBuffer => {
            audioContext.decodeAudioData(arrayBuffer).then(buffer => {
                resolve(buffer);
            });
        })
    })
}

多个音频的合并

// 合并音频的方法
const mergeAudio = (arrBufferList) => {
    // 获得 AudioBuffer
    const audioBufferList = arrBufferList;
    // 最大播放时长
    const maxDuration = Math.max(...audioBufferList.map(audioBuffer => audioBuffer.duration));
    // 最大通道数
    const maxChannelNumber = Math.max(...audioBufferList.map(audioBuffer => audioBuffer.numberOfChannels));
    // 创建一个新的 AudioBuffer
    const newAudioBuffer = audioContext.createBuffer(maxChannelNumber, audioBufferList[0].sampleRate * maxDuration, audioBufferList[0].sampleRate);
    // 将所有的 AudioBuffer 的数据合并到新的 AudioBuffer 中
    audioBufferList.forEach((audioBuffer, index) => {
        for (let channel = 0; channel < audioBuffer.numberOfChannels; channel++) {
            const outputData = newAudioBuffer.getChannelData(channel);
            const bufferData = audioBuffer.getChannelData(channel);

            for (let i = audioBuffer.getChannelData(channel).length - 1; i >= 0; i--) {
                outputData[i] += bufferData[i];
            }

            newAudioBuffer.getChannelData(channel).set(outputData);
        }
    });

    return newAudioBuffer;
}

音量的改变

// 定义一个AudioContext对象
// 因为 Web Audio API都是源自此对象
const audioContext = new AudioContext();
// 创建一个gainNode对象
// gainNode可以对音频输出进行一些控制
const gainNode = audioContext.createGain();
// 音量设置为20%
gainNode.gain.value = 0.2;
// 这个很有必要,建立联系
gainNode.connect(audioContext.destination);

// 创建AudioBufferSourceNode
let source = audioContext.createBufferSource();
// 获取音频资源
fetch('./bgmusic.mp3')
  .then(res => res.arrayBuffer())
  .then(buffer => audioContext.decodeAudioData(buffer))
  .then(audioBuffer => {
    source.buffer = audioBuffer;
    source.connect(gainNode);
  });

// 当需要播放的时候,执行
source.start(0);

第三方音频解决方案

我这边推荐的html5音频处理开源库是https://gitee.com/xiangyuecn/Recorder#%E9%99%84android-hybrid-app---webview%E4%B8%AD%E5%BD%95%E9%9F%B3%E7%A4%BA%E4%BE%8B

一、引入方式

//必须引入的核心,换成require也是一样的。注意:recorder-core会自动往window下挂载名称为Recorder对象,全局可调用window.Recorder,也许可自行调整相关源码清除全局污染

import Recorder from 'recorder-core'

//引入相应格式支持文件;如果需要多个格式支持,把这些格式的编码引擎js文件放到后面统统引入进来即可
import 'recorder-core/src/engine/mp3'
import 'recorder-core/src/engine/mp3-engine' //如果此格式有额外的编码引擎(*-engine.js)的话,必须要加上

//以上三个也可以合并使用压缩好的recorder.xxx.min.js
//比如 import Recorder from 'recorder-core/recorder.mp3.min' //已包含recorder-core和mp3格式支持

//可选的插件支持项
import 'recorder-core/src/extensions/waveview'

//ts import 提示:npm包内已自带了.d.ts声明文件(不过是any类型)

recorder-core库,可以录制各种格式视频,支持格式转换,假如语音转文字,需要pcm格式,这个开源库可以直接录制pcm格式,支持多个pcm格式的拼接。

const pcmMerge = function (fileBytesList, bitRate, sampleRate, True, False) {
  //计算所有文件总长度
  var size = 0
  for (var i = 0; i < fileBytesList.length; i++) {
    size += fileBytesList[i].byteLength
  }

  //全部直接拼接到一起
  var fileBytes = new Uint8Array(size)
  var pos = 0
  for (var i = 0; i < fileBytesList.length; i++) {
    var bytes = fileBytesList[i]
    fileBytes.set(bytes, pos)
    pos += bytes.byteLength
  }

  //计算合并后的总时长
  var duration = Math.round(((size * 8) / bitRate / sampleRate) * 1000)

  True(fileBytes, duration, { bitRate: bitRate, sampleRate: sampleRate }, size)
}

二、支持波形播放插件

例如WaveView插件,FrequencyHistogramView插件等等,音频录制波形可视化插件

三、支持音频混音、变速变调音频转换

小结

简单的音频录制需求可以自己实现,但是需要复杂的,音频解决方案,我这边推荐使用recorder-core库,这个库例子比较多,使用相对简单。推荐给大家!

相关文章: