html5音频录制解决方案

12,767次阅读
没有评论

共计 4804 个字符,预计需要花费 13 分钟才能阅读完成。

前言

前段时间,项目中用到 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

// 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 = 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 

二、支持波形播放插件

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

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

小结

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

    正文完
     0
    Yojack
    版权声明:本篇文章由 Yojack 于1970-01-01发表,共计4804字。
    转载说明:
    1 本网站名称:优杰开发笔记
    2 本站永久网址:https://yojack.cn
    3 本网站的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,请联系站长进行删除处理。
    4 本站一切资源不代表本站立场,并不代表本站赞同其观点和对其真实性负责。
    5 本站所有内容均可转载及分享, 但请注明出处
    6 我们始终尊重原创作者的版权,所有文章在发布时,均尽可能注明出处与作者。
    7 站长邮箱:laylwenl@gmail.com
    评论(没有评论)