React制作音乐播放器日记总结-2 - Shiina's Blog

React制作音乐播放器日记总结-2

2022 年 8 月 7 日 - 18:52:13 发布
5.8K 字 20分钟
本作品采用 CC BY-NC-SA 4.0 进行许可。
本文距离首次发布已经过去了 833 天,请注意文章的时效性!

React制作音乐播放器日记总结-2

声明:文章仅为本人总结的经验,不代表这就是正确标准答案

这篇博客主要用于记录制作音频可视化的过程与遇到的困难,本来想着别人的轮子能拿来用用,但奈何 CSDN 上的文章都没看明白,所以只好自己造一部分了

获取音频对象并将声音转换成频谱数组

关于柱形图的定义

柱形图的音频可视化可能大家都有见过,在这里放个简单的图

这种图一般用于实时展示音频频域,每一根柱子代表其频率对应的能量大小

而在 Web Audio API 中,有这样一个 API 专门用于获取音频实时数据,专门用于处理音频可视化问题:

MDN 文档:https://developer.mozilla.org/zh-CN/docs/Web/API/AnalyserNode

在本文章中我们就围绕这个 API 进行操作

获取柱形图数据

初始化音频上下文

创建一个基本的音频上下文的流程如下(图摘自 MDN 官网):

在使用 AnalyserNode API 获取数据之前,我们首先需要创建一个音频上下文对象

let audioContext = new window.AudioContext()

在该音乐播放器中只有要绘制频谱的地方用到了音频上下文,所以我直接在需要进行音频可视化的地方初始化了上下文对象,如果全局使用则更推荐在音频相关的全局 reducer 进行初始化

随后便是为上下文选择一个音频源,本人是在 全局的音频 Reducer 中 new 了一个音频对象,所以在这里直接使用 AudioContext 实例上的 createMediaElementSource() 方法来为音频上下文选择音频源

let eleSource = audioContext.createMediaElementSource(this.props.ele)

接下来是构造 AnalyserNode 实例,此时构造出来的 analyser 实例输出的频域数组长度为 1024 , 一般我们不需要那么多,所以我们还需要设计其 fftSize 属性减少数组长度

let analyser = audioContext.createAnalyser()
// 减短数组长度
analyser.fftSize = 256

最后一步就是将处理器串联起来,以音频源为起点,以系统目前的输出设备为终点。

eleSource.connect(analyser)
analyser.connect(audioContext.destination)

源码如下

至此,我们完成了音频上下文的初始化

获取数据数组

我们在此处定义一个 getDataArray 的方法,专门用于获取我们需要的数据

为获取数据,首先我们需要构造一个 Uint8Array 类型的数组,长度是 analyser 身上的 frequencyBinCount 长度为 前文设置的 fftSize 的一半

Uint8Array 数组资料:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array

随后调用 analyser 身上的 getByteFrequencyData 方法,并将刚刚初始化的 array 作为参数

接下来在控制台打印这个数组就可以看到我们想要的数据,完整代码如下:

const getDataArray = (analyser: AnalyserNode) => {
  let array = new Uint8Array(analyser.frequencyBinCount);
  analyser.getByteFrequencyData(array)
  console.log(array)
}

通过调用这个方法我们就可以看到想要的数据

注意事项

{% note warning %} 以下内容都是个人实践后所得到的经验和解决方法,仅供参考 {% endnote %}

当选择 HTMLMediaElement 作为音频上下文对象的音频源时,音频源的源文件,即 src 不能是空或者无效的文件,否则在音频上下文初始化后将无法正常播放音频,但是 Firefox 浏览器在部分情况下仍然可以正常播放

因此我在钩子 componentDidUpdate 对上下文是否初始化进行判断,当播放器有媒体资源且可以正常播放时对音频上下文进行初始化,同时开始对 canvas 进行绘图

class CanvasComponent extends React.Component {
  // 组件中设置一个私有变量判断是否初始化
  private hasInit = false

  componentDidUpdate() {
    // 未初始化且无资源
    if (!this.hasInit && this.props.ele.src.length) {
      let audioContext = new window.AudioContext();
      let eleSource = audioContext.createMediaElementSource(this.props.ele);
      let analyser = audioContext.createAnalyser();

      analyser.fftSize = this.myFftSize;

      eleSource.connect(analyser);
      analyser.connect(audioContext.destination);

      this.hasInit = true;

      // 初始化完成后调用绘制函数
      this.draw(analyser)
    }
  }
}

最后那里的 draw 函数详解:音频播放时调用绘制函数进行绘制

在 Canvas 上绘制柱形图

封装绘制函数

有了数据之后,接下来就是绘制柱形图

我们首先封装一个纯绘图函数,并将 canvas元素频域数组 传入

drawToDom = (canvas: HTMLCanvasElement, arr: Uint8Array) => {}

我们要获取 canvas 的宽高,这样可以计算其中每个条的宽度

const w = canvas.width
const h = canvas.height
// 数组长度,代表绘制柱形条的数量
const alt = arr.length

// 计算每一个柱形条宽度
let barW = w / alt
let barH = 0
// 渲染下一个柱形条的 x 轴位置
let x = 0

在渲染之前先要清空 canvas ,随后用 for 循环读出每个频域的数据,并渲染到 canvas 中,频域的数据可能过大,使得柱形条高度大于画布范围,所以有必要除一下使得柱形条高度变矮;每次 for 循环中除了加上柱形条宽度还要在加一个常熟让柱形条隔开

这里我们使用 canvas元素 上下文的 fillRect 绘制矩形

{% note %} fillRect 文档链接: https://developer.mozilla.org/zh-CN/docs/Web/API/CanvasRenderingContext2D/fillRect {% endnote %}

为了让每个柱形条居中,我们需要决定其初始位置,即 canvas 元素自身高度的一半,但此时柱形条将以 canvas 中心进行渲染,会变成这样:

因此我们还需要对起始位置的渲染进行调整,即再把 y 轴位置减去当前柱形条高度的一半,即可达到正常效果

绘制代码如下:

canvasCtx.fillStyle = '#bce5ef' // 设置渲染颜色
canvasCtx.fillRect(x, h / 2 - barH / 8, barW, (barH / 4));

这里我设置柱形条高度时除了 4,所以在设置 y 轴的位置时在除了 4 的基础上再除了 2

完整代码如下:

  drawToDom = (canvas: HTMLCanvasElement, arr: Uint8Array) => {
    let canvasCtx = canvas.getContext('2d')
    const w = canvas.width
    const h = canvas.height
    const alt = arr.length
    if (canvasCtx) {
      canvasCtx.clearRect(0, 0, w, h)
      // 计算每个条的宽度
      let barW = (w / alt) * 0.9
      let barH = 0
      let x = 0

      for (let i = 0; i < alt; i++) {
        barH = arr[i] + 20
        canvasCtx.fillStyle = '#bce5ef'
        canvasCtx.fillRect(x, h / 2, barW, (barH / 4))

        // 增加一个常数使得渲染的柱形条不会紧挨在一起
        x += barW + 3;
      }
    } else {
      throw Error('canvas 元素为空')
    }
  }

音频播放时调用绘制函数进行绘制

现在我们封装一个上下文初始化时调用的 draw 函数,我将获取音频可视化数据的 analyser 作为参数传入

在这里我将使用 requestAnimationFrame 作为动画回调函数载体进行无限递归,从而做到持续更新 canvas 画布,当然我也使用了节流,防止不必要的性能消耗,这里的节流使用了 lodash 自带的节流功能

{% note %} requestAnimationFrame 文档链接: https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestAnimationFrame {% endnote %}

完整代码如下

  draw = (analyser: AnalyserNode) => {
    const alt = analyser.frequencyBinCount
    let array = new Uint8Array(alt);

    // 绘制函数
    let drawToCanvas = throttle(() => {
      // 无限递归更新画布
      this.drawVisual = requestAnimationFrame(drawToCanvas)
      // 获取频域数据
      analyser.getByteFrequencyData(array);
      // 当 canvas 元素存在时进行绘制
      if (this.IndexCanvas.current) this.drawToDom(this.IndexCanvas.current, array)
    }, 20)

    drawToCanvas()
  }

大概总结了这么多,如果有错漏的地方可以 B 站私信我及时修改。

个人信息
avatar
Shiinafan
文章
43
标签
53
归档
4
导航