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

本文最后更新于:1 个月前

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

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

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

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

关于柱形图的定义

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

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

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

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

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

获取柱形图数据

初始化音频上下文

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

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

1
let audioContext = new window.AudioContext()

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

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

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

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

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

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

1
2
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 作为参数

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

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

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

注意事项

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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元素频域数组 传入

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

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

1
2
3
4
5
6
7
8
9
10
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 绘制矩形

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

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

绘制代码如下:

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

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

完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 自带的节流功能

完整代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 站私信我及时修改。


React制作音乐播放器日记总结-2
https://blog.shiinafan.top/2022/08/08/React制作音乐播放器日记总结-2/
作者
Shiina
发布于
2022年8月8日
许可协议