给我的视频聊天项目加一个音频可视化的需求

前言

  大家好!我叫。。。额。。。小米爱老鼠(没有笔名,就用户名吧),你们可以称呼我小米;前几天我发布了我第一篇blog,分享了我的视频聊天室项目,虽然它主要功能都已实现,但是还是有一些有趣想法没有写到里面,我觉得有趣好玩的想法我都想实现一下;这不就来了一个音量可视化的需求,我想在语音&视频通话时显示音量大小的变化。

需求描述

  我希望它是一个信号图案,根据音量的强弱,改变信号强度

需求实现

  使用到的Web API有AudioContextCanvas

AudioContext

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

AudioContext.createAnalyser()

创建一个AnalyserNode,可以用来获取音频时间和频率数据,以及实现数据可视化。

AnalyserNode.getByteFrequencyData()

将当前频率数据复制到传入的 Uint8Array(无符号字节数组)中。

设计

  • 我希望它有一定的扩展性,使用者能够自己定制可视化的图案,所以将绘画的权限交给使用者。
// 音频来源可以是媒体流(MediaStream)或者是HTML媒体元素
type Source = MediaStream | HTMLAudioElement | HTMLVideoElement

export function audioVisualizer(draw) {
  return function start(audioSource: Source, canvas: HTMLCanvasElement) {
    ...
    draw(...)
    ...
    return function cancel() {
      ...
    }
  }
}
  • 实现一个函数,它接收一个绘画函数,返回一个开始绘画的函数,开始函数又会返回一个取消绘画的函数,接下来我们使用AudioContextCanvas来填充其余部分。
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);
      if (isHTMLMediaElement(audioSource)) {
        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 属性的值必须是从3232768范围内的2的非零幂; 其默认值为2048.
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
    ...
  }
}
  • 现在就已经实现好了,在扩展我要绘制的信号图案之前,先分析一下这个图如何绘制

image.png

  • 我需要红色部分,正方形表示canvas元素,需要canvas绘画的部分就是红色部分,使用CanvasRenderingContext2D.arc(x, y, radius, startAngle, endAngle, counterclockwise)来画这部分扇形,它接收6个参数,xy是圆心坐标,radius是半径,startAngleendAngle分别代表圆弧的起始点和终点,x 轴方向开始计算,单位以弧度表示,anticlockwise表示逆时针(true)还是顺时针(false)开始绘画,默认值false
  • 确定圆心
    • 由上面图能够看出,圆心坐标为 ( 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) (canvas.width/2,canvas.height),
  • 确定半径
    • 画的扇形半径由音量强度canvas.height元素决定,我需要画三个不同大小的扇形来表示音量强度,这三条线将canvas.height划分为四段,所以半径就以 h e i g h t / 4 ∗ x height / 4 * x height/4x表示,x为音量强度等级。
  • 确定弧度
    • 圆的一周为,画出一个圆的参数是CanvasRenderingContext2D.arc(x, y, radius, 0, 2 * Math.PI),默认顺时针绘画,如下图所示,可以看出我需要的弧度为 2 π ∗ 5 / 8 2π * 5 / 8 2π5/8 2 π ∗ 7 / 8 2π * 7 / 8 2π7/8
    • 如果逆时针画的话弧度为 − 2 π ∗ 1 / 8 -2π * 1 / 8 2π1/8 − 2 π ∗ 3 / 8 -2π * 3 / 8 2π3/8

image.png

  • 全部参数已确认,现在就实现绘画函数
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元素。

测试

  • 代码
<template lang="">
  <div class="audio-visualizer">
    <canvas ref="canvas" width="100" height="100" style="border-radius: 20%"></canvas>
    <!-- &nbsp; -->
    <!-- <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 = audioVisible(audio.value, canvas.value)
  // };

  // MediaStream 
  navigator.mediaDevices.getUserMedia({audio: true}).then((stream) => {
    close = audioVisible(stream, canvas.value)
  })
})

onBeforeUnmount(() => {
  close()
})
</script>
  • 效果

20231019_143312.gif

  • 后来我又调整了一下,我觉着这样更像一个喇叭;
  • ctx.fillStyle = 'rgb(255, 255, 255)'canvas画布背景色为白色;
  • const base_x = height / 5,三条线改为了4条线;
  • ctx.arc(0, height / 2, base_x * (lv - 0.5), Math.PI / 16 * 29, Math.PI / 16 * 3),圆心和弧度发生了变化,不过与上面分析同理。

20231020_121114.gif

扩展阅读

参考

未经作者授权 禁止转载

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小米爱老鼠

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值