Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

html5音频录制解决方案 #427

Open
confidence68 opened this issue Apr 30, 2024 · 0 comments
Open

html5音频录制解决方案 #427

confidence68 opened this issue Apr 30, 2024 · 0 comments

Comments

@confidence68
Copy link
Owner

前言

前段时间,项目中用到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库,这个库例子比较多,使用相对简单。推荐给大家!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant