前言
大家好!我叫。。。额。。。小米爱老鼠(没有笔名,就用户名吧),你们可以称呼我小米;前几天我写了我第一篇掘金文章,分享了我的视频聊天室项目,虽然它主要功能都已实现,但是还是有一些有趣想法没有写到里面,我觉得有趣好玩的想法我都想实现一下;这不就来了一个音量可视化的需求,我想在语音&视频通话时显示音量大小的变化。
需求描述
我希望它是一个信号图案,根据音量的强弱,改变信号强度
需求实现
使用到的Web API有AudioContext
和Canvas
AudioContext
AudioContext
接口表示由链接在一起的音频模块构建的音频处理图,每个模块由一个AudioNode
表示。音频上下文控制它包含的节点的创建和音频处理或解码的执行。在做任何其他操作之前,你需要创建一个AudioContext
对象,因为所有事情都是在上下文中发生的。建议创建一个AudioContext
对象并复用它,而不是每次初始化一个新的AudioContext
对象,并且可以对多个不同的音频源和管道同时使用一个AudioContext
对象。
AudioContext.createAnalyser()
创建一个
AnalyserNode
,可以用来获取音频时间和频率数据,以及实现数据可视化。
AnalyserNode.getByteFrequencyData()
将当前频率数据复制到传入的 Uint8Array(无符号字节数组)中。
设计
- 我希望它有一定的扩展性,使用者能够自己定制可视化的图案,所以将绘画的权限交给使用者。
typescript
// 音频来源可以是媒体流(MediaStream)或者是HTML媒体元素
type Source = MediaStream | HTMLAudioElement | HTMLVideoElement
export function audioVisualizer(draw) {
return function start(audioSource: Source, canvas: HTMLCanvasElement) {
...
draw(...)
...
return function cancel() {
...
}
}
}
- 实现一个函数,它接收一个绘画函数,返回一个开始绘画的函数,开始函数又会返回一个取消绘画的函数,接下来我们使用
AudioContext
和Canvas
来填充其余部分。
typescript
const isType = (data: any, type: string) => toString.call(data) === `[object ${type}]`
const isMediaStream = (source: Source) => isType(source, 'MediaStream')
const isHTMLMediaElement = (source: Source) => isType(source, 'HTMLAudioElement') || isType(source, 'HTMLVideoElement')
type Source = MediaStream | HTMLAudioElement | HTMLVideoElement
export function audioVisualizer(
draw: (dataArray: Uint8Array, ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement) => void
) {
return function start(audioSource: Source, canvas: HTMLCanvasElement) {
const audioCtx = new window.AudioContext()
// 创建AnalyserNode,它提供获取当前音频的频域数据的方法
const analyser = audioCtx.createAnalyser()
// 创建音频源
const source = createSource()
// 将声音数据定向到analyser节点
source.connect(analyser)
// 设置频域数据样本区域大小,其值是2的幂,它决定了analyser.frequencyBinCount大小
analyser.fftSize = 32
// fftSize的1/2,表示可视化的数据值的数量
const bufferLength = analyser.frequencyBinCount
// 将来收集频域数据的数组
const dataArray = new Uint8Array(bufferLength)
let id: number
(function drawVisual() {
// 反复收集当前音频的频域数据
id = requestAnimationFrame(drawVisual)
// 当前频率数据复制到传入的Uint8Array数组中,频率数据由0到255范围内的整数组成
analyser.getByteFrequencyData(dataArray)
const context = canvas.getContext('2d')
draw(dataArray, context, canvas)
}())
return function cancel() {
cancelAnimationFrame(id);
source.disconnect(analyser);
analyser.disconnect(audioCtx.destination);
}
function createSource() {
if (isMediaStream(audioSource)) {
return audioCtx.createMediaStreamSource(audioSource as MediaStream)
} else if (isHTMLMediaElement(audioSource)) {
// 将声音数据定向到音频设备(扬声器),如果没有则会导致媒体元素无声音
analyser.connect(audioCtx.destination);
return audioCtx.createMediaElementSource(audioSource as HTMLMediaElement)
}
}
}
}
- 代码中我们看到
analyser.fftSize
被写死32
,他决定了收集频域数据的数组的长度为16
,如果我们需要更多的数据怎么办,所以修改上述代码,使fftSize
作为function audioVisualizer
的参数。fftSize 属性的值必须是从32
到32768
范围内的2
的非零幂; 其默认值为2048
.
typescript
export function audioVisualizer(
draw: (dataArray: Uint8Array, ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement) => void,
options?: { fftSize: number }
) {
return function start(audioSource: Source, canvas: HTMLCanvasElement) {
...
const { fftSize = 2048 } = options || {}
analyser.fftSize = fftSize
...
}
}
- 现在就已经实现好了,在扩展我要绘制的信号图案之前,先分析一下这个图如何绘制
- 我需要红色部分,正方形表示
canvas
元素,需要canvas
绘画的部分就是红色部分,使用CanvasRenderingContext2D.arc(x, y, radius, startAngle, endAngle, counterclockwise)
来画这部分扇形,它接收6个参数,x
和y
是圆心坐标,radius
是半径,startAngle
和endAngle
分别代表圆弧的起始点和终点,x 轴方向开始计算,单位以弧度表示,anticlockwise
表示逆时针(true
)还是顺时针(false
)开始绘画,默认值false
。 - 确定圆心
- 由上面图能够看出,圆心坐标为 <math xmlns="http://www.w3.org/1998/Math/MathML"> ( c a n v a s . w i d t h / 2 , c a n v a s . h e i g h t ) (canvas.width / 2, canvas.height) </math>(canvas.width/2,canvas.height),
- 确定半径
- 画的扇形半径由
音量强度
和canvas.height
元素决定,我需要画三个不同大小的扇形来表示音量强度,这三条线将canvas.height
划分为四段,所以半径就以 <math xmlns="http://www.w3.org/1998/Math/MathML"> h e i g h t / 4 ∗ x height / 4 * x </math>height/4∗x表示,x
为音量强度等级。
- 画的扇形半径由
- 确定弧度
- 圆的一周为
2π
,画出一个圆的参数是CanvasRenderingContext2D.arc(x, y, radius, 0, 2 * Math.PI)
,默认顺时针绘画,如下图所示,可以看出我需要的弧度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> 2 π ∗ 5 / 8 2π * 5 / 8 </math>2π∗5/8到 <math xmlns="http://www.w3.org/1998/Math/MathML"> 2 π ∗ 7 / 8 2π * 7 / 8 </math>2π∗7/8 - 如果逆时针画的话弧度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> − 2 π ∗ 1 / 8 -2π * 1 / 8 </math>−2π∗1/8到 <math xmlns="http://www.w3.org/1998/Math/MathML"> − 2 π ∗ 3 / 8 -2π * 3 / 8 </math>−2π∗3/8
- 圆的一周为
- 全部参数已确认,现在就实现绘画函数
typescript
function draw(dataArray, ctx, canvas) => {
const width = canvas.width;
const height = canvas.height;
// 频率数据最大值, dataArray频率数据由0到255范围内的整数组成
const max = Math.max(...dataArray);
// 频率最大值占canvas.height的部分
const av_height = max * height / 255
ctx.fillRect(0, 0, width, height) // 重置画布样式
ctx.lineWidth = 1 // 线段宽度
ctx.strokeStyle = "#73c991" // 线段颜色
const base_y = height / 4 // 三条线分四等分,三条线代表三个音量等级
let lv = 1 // 音量等级
while(av_height > base_y * lv) {
ctx.beginPath();
ctx.arc(width / 2, height, base_y * (lv - 0.5), Math.PI / 4 * 5, Math.PI / 4 * 7);
ctx.stroke()
lv++
}
}
// 注册draw,得到一个音频可视化函数;
// 取频率数据最大值,不需太多的频域数据,fftSize给最小值即可
export const audioVisible = audioVisualizer(draw, { fftSize: 32 })
- 注册draw后得到一个新的函数,接收两个参数,第一个是音频源,可以是
HTML元素
和MediaStream媒体流
,第二个参数就是你要绘画的canvas
元素。
测试
- 代码
html
<template lang="">
<div class="audio-visualizer">
<canvas ref="canvas" width="100" height="100" style="border-radius: 20%"></canvas>
<!-- -->
<!-- <audio ref="audio" controls :src="audioSrc"></audio> -->
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, onBeforeUnmount } from "vue"
import { audioVisible } from "@/utils/audio/audioVisualizer";
import audioSrc from "@/assets/Stay_tonight.wav"
const canvas = ref<HTMLCanvasElement>(null)
const audio = ref<HTMLAudioElement>(null)
let close = () => {}
onMounted(() => {
// HTMLMediaElement
// audio.value.onloadedmetadata = () => {
// audio.value.play();
// close = audioVisual(audio.value, canvas.value)
// };
// MediaStream
navigator.mediaDevices.getUserMedia({audio: true}).then((stream) => {
close = audioVisible(stream, canvas.value)
})
})
onBeforeUnmount(() => {
close()
})
</script>
- 效果
扩展阅读
参考
- MDN
- AudioContext: developer.mozilla.org/zh-CN/docs/...
- CanvasRenderingContext2D: developer.mozilla.org/zh-CN/docs/...
未经作者授权 禁止转载