前端 音频录制剖析

前端 音频录制剖析

作者:@ 很菜的小白在分享
时间:2021年12月29日

音视频三部曲

前端 音频录制剖析
前端 视频录制剖析
前端 桌面共享剖析

前言

今天与大家分享一下 音频录制 的实现过程,该功能的实现类似 视频录制 一文中视频录制的实现过程,所有本文的讲解会绕过一些细节部分。

内容有些长,本页面右侧有目录结构可以进行跳转
音频输入

介绍

前端实现音频录制是通过 getUserMediaAPI实现的,这与视频录制是同一个API,所以所遵循的 规则 是相同的。

       1. 目录

           1.1 授权麦克风

           1.2 处理设备返回的流

           1.3 音频可视化

                   1.3.1 原理

           1.4 完结

授权麦克风

这里就不过多的介绍了请看上文 视频录制 中授权摄像头的介绍,这里唯一的区别就是授权的设备不同,直接上代码。

// JavaScript
// 获取麦克风权限
getUserMediaPermission() {
  if (window.navigator.mediaDevices.getUserMedia) {
    let constraints = {audio: true}
    window.navigator.mediaDevices.getUserMedia(constraints).then(this.onSuccess, this.onError);
  }
},

处理设备返回的流

// JavaScript
// 授权麦克风成功的回调
onSuccess(stream) {
  let duration = 0
  let startTime = 0
  
  // 生成音频可视化
  this.visualize(stream);
  // 创建记录器
  this.mediaRecorder = new MediaRecorder(stream)
  // 开始记录
  this.mediaRecorder.start(10)
  // 音频数据发生变化时收集音频片段,用于合成音频文件
  this.mediaRecorder.ondataavailable = (event) => {
    this.chunks.push(event.data)
  }
  // 监听音频开始录制
  this.mediaRecorder.onstart = () => {
    startTime = new Date().getTime()
  }
  // 音频录制结束回调
  this.mediaRecorder.onstop = (event) => {
    duration = new Date().getTime() - startTime
    console.log(this.chunks);
    let blob = new Blob(this.chunks, {
      type: 'audio/webm'
    })
  }
},
onError() {

},

音频可视化

音频录制的过程通常需要一个可视化图形展示,主要是给用户反馈证明当前正在录制中,可视化图形的方案例如:

  1. UI通过制作一个GIF图实现可视化图形
  2. 通过 div + css 实现

虽然以上两种方式都可以实现可视化效果,但太假了,要是能实现类似 聊天APP 中的语音输入可视化图形就完美了。
没错,前端也可以实现通过输入音频的实现动态可视化图形,下面会重点介绍。

原理

  1. 创建音频上下文对象处理音频流数据
  2. 与麦克风返回的流数据进行关联
  3. 获取音频时间和频率数据
  4. 根据音频数据生成可视化图形

涉及到的API介绍

内容有点长,不建议跳过,最好了解一下

跳过
完整代码
效果

AudioContext

AudioContext API表示由链接在一起的音频模块构建的音频处理图,每个模块由一个AudioNode表示。音频上下文控制它包含的节点的创建和音频处理或解码的执行。在做任何其他操作之前,您需要创建一个AudioContext对象,因为所有事情都是在上下文中发生的。建议创建一个AudioContext对象并复用它,而不是每次初始化一个新的AudioContext对象,并且可以对多个不同的音频源和管道同时使用一个AudioContext对象。

属性

名称说明
baseLatency返回AudioContext将音频从AudioDestinationNode传递到音频子系统的处理延迟的秒数。
options返回对当前音频上下文的预估输出延迟。

属性

名称说明
createMediaElementSource创建一个MediaElementAudioSourceNode接口来关联HTMLMediaElement. 这可以用来播放和处理来自或 元素的音频。
createMediaStreamSource创建一个MediaStreamAudioSourceNode接口来关联可能来自本地计算机麦克风或其他来源的音频流MediaStream。
createMediaStreamDestination创建一个MediaStreamAudioDestinationNode (en-US)接口来关联可能储存在本地或已发送至其他计算机的MediaStream音频。
resume恢复之前被暂停的音频上下文中的时间进程。
suspend暂停音频上下文中的时间进程,暂停音频硬件访问并减少进程中的CPU/电池使用。

参考

createMediaStreamSource

AudioContext接口的createMediaStreamSource()方法用于创建一个新的MediaStreamAudioSourceNode 对象, 需要传入一个媒体流对象(MediaStream对象)(可以从 navigator.getUserMedia 获得MediaStream对象实例), 然后来自MediaStream的音频就可以被播放和操作。

参数

名称说明
stream一个MediaStream 对象,把他传入一个音频处理器进行操作

createAnalyser

AudioContext的createAnalyser()方法能创建一个AnalyserNode,可以用来获取音频时间和频率数据,以及实现数据可视化。

AnalyserNode API 表示了一个可以提供实时频域和时域分析信息的节点。它是一个不对音频流作任何改动的 AudioNode,同时允许你获取和处理它生成的数据,从而创建音频可视化。
重点来了,我们的音频可视化就是通过这些 Node 实现的。
属性

名称说明
fftSize一个无符号长整形(unsigned long)的值,代表了用于计算频域信号时使用的 FFT (快速傅里叶变换) 的窗口大小
frequencyBinCount一个无符号长整形(unsigned long)的值, 值为fftSize的一半。这通常等于将要用于可视化的数据值的数量

方法

名称说明
getFloatFrequencyData将当前频域数据拷贝进Float32Array (en-US)数组。
getByteFrequencyData将当前频域数据拷贝进Uint8Array (en-US)数组(无符号字节数组)。
getFloatTimeDomainData将当前波形,或者时域数据拷贝进Float32Array (en-US)数组。
getByteTimeDomainData将当前波形,或者时域数据拷贝进 Uint8Array (en-US)数组(无符号字节数组)。

创建音频上下文

AudioContext

this.audioCtx = new AudioContext();

获取音频关联数据

const source = this.audioCtx.createMediaStreamSource(stream);

获取音频时间和频率数据

// 获取音频时间和频率数据
const analyser = this.audioCtx.createAnalyser();
// 定义长度
analyser.fftSize = 256;
const bufferLength = analyser.frequencyBinCount;
// 生成 fftSize 长度一半的 Uint8Array 数组
const dataArray = new Uint8Array(bufferLength);
// 合并流数据
source.connect(analyser);

绘制可视化图形

const draw = () => {
  requestAnimationFrame(draw);
  // 将当前波形的数据复制给 dataArray
  analyser.getByteTimeDomainData(dataArray);

  let x = 0;
  this.waveCanvasContext.fillStyle = 'rgb(0, 0, 0)';
  this.waveCanvasContext.fillRect(0, 0, WIDTH, HEIGHT);

  let barWidth = (WIDTH / bufferLength) * 2.5;
  let barHeight;
  
  for (let i = 0; i < bufferLength; i++) {
    barHeight = dataArray[i] + 50;

    this.waveCanvasContext.fillStyle = 'rgb(' + (barHeight+100) + ',50,50)';

    this.waveCanvasContext.fillRect(x, HEIGHT-barHeight/2, barWidth, barHeight/2);

    x += barWidth + 5;
  }
}
draw()

完整代码

visualize(stream) {
  if (!this.audioCtx) {
    this.audioCtx = new AudioContext();
  }

  const source = this.audioCtx.createMediaStreamSource(stream);

  const analyser = this.audioCtx.createAnalyser();

  console.log('analyser', analyser);
  console.log('source', source);
  analyser.fftSize = 256;
  const bufferLength = analyser.frequencyBinCount;

  const dataArray = new Uint8Array(bufferLength);

  source.connect(analyser);
  const WIDTH = this.waveCanvas.width
  const HEIGHT = this.waveCanvas.height;
  this.waveCanvasContext.clearRect(0, 0, WIDTH, HEIGHT);

  const draw = () => {

    requestAnimationFrame(draw);

    analyser.getByteTimeDomainData(dataArray);

    let x = 0;
    this.waveCanvasContext.fillStyle = 'rgb(0, 0, 0)';
    this.waveCanvasContext.fillRect(0, 0, WIDTH, HEIGHT);

    let barWidth = (WIDTH / bufferLength) * 2.5;
    let barHeight;


    for (let i = 0; i < bufferLength; i++) {
      barHeight = dataArray[i] + 50;

      this.waveCanvasContext.fillStyle = 'rgb(' + (barHeight+100) + ',50,50)';

      this.waveCanvasContext.fillRect(x, HEIGHT-barHeight/2, barWidth, barHeight/2);

      x += barWidth + 5;
    }
  }
  draw()
}

效果

效果图

完结

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值