feat(components): 添加音频波形组件
- 实现了一个基于 Canvas 的音频波形组件 - 组件根据 PCM 数据动态绘制波形 - 采用平滑动画效果,实现流畅的波形显示 - 优化了波形计算算法,提高性能和准确性
This commit is contained in:
parent
f7dd3b43fe
commit
6a20e26e5d
|
@ -0,0 +1,128 @@
|
|||
<template>
|
||||
<div class="audio-waveform">
|
||||
<canvas ref="canvas" width="288" height="48"></canvas>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, onMounted, onBeforeUnmount } from 'vue'
|
||||
const props = defineProps({
|
||||
pcmData: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
const canvas = ref(null)
|
||||
const BAR_COUNT = 48 // 波形条数量
|
||||
const latestPcm = ref([])
|
||||
let animationId = null
|
||||
const currentHeights = ref(Array(BAR_COUNT).fill(4)) // 当前每根条的高度(用于平滑动画)
|
||||
let targetHeights = Array(BAR_COUNT).fill(4) // 目标高度
|
||||
|
||||
const BAR_WIDTH = 3 // 波形条宽度
|
||||
const BAR_GAP = 3 // 波形条间距
|
||||
const CANVAS_WIDTH = BAR_COUNT * (BAR_WIDTH + BAR_GAP) // 画布宽度
|
||||
const CANVAS_HEIGHT = 48 // 画布高度
|
||||
const RADIUS = BAR_WIDTH / 2 // 圆角半径
|
||||
|
||||
// 计算每根波形条的目标高度(核心算法)
|
||||
const calcTargetHeights = (pcm) => {
|
||||
if (!pcm || pcm.length === 0) return Array(BAR_COUNT).fill(4)
|
||||
const step = Math.floor(pcm.length / BAR_COUNT)
|
||||
const maxBarHeight = 22 // 单边最大高度
|
||||
const THRESHOLD = 8 // 门限,低于此值视为静止
|
||||
let maxRms = 1
|
||||
const rmsArr = []
|
||||
// 计算每根条的RMS能量,并找出最大值
|
||||
for (let i = 0; i < BAR_COUNT; i++) {
|
||||
let sum = 0
|
||||
for (let j = 0; j < step; j++) {
|
||||
const idx = i * step + j
|
||||
sum += (pcm[idx] || 0) * (pcm[idx] || 0)
|
||||
}
|
||||
const rms = Math.sqrt(sum / step)
|
||||
rmsArr[i] = rms < THRESHOLD ? 0 : rms - THRESHOLD // 门限抑制底噪
|
||||
maxRms = Math.max(maxRms, rmsArr[i])
|
||||
}
|
||||
const norm = Math.max(32, maxRms) // 归一化因子,防止全静止
|
||||
const arr = []
|
||||
for (let i = 0; i < BAR_COUNT; i++) {
|
||||
// 非线性提升对比度,指数越大,只有大信号才有明显变化
|
||||
const ratio = Math.pow(rmsArr[i] / norm, 1.2)
|
||||
arr[i] = 4 + Math.min(1, ratio) * maxBarHeight // 4为最小高度
|
||||
}
|
||||
return arr
|
||||
}
|
||||
|
||||
// 绘制波形(每帧调用)
|
||||
const drawWave = () => {
|
||||
if (!canvas.value) return
|
||||
const ctx = canvas.value.getContext('2d')
|
||||
ctx.clearRect(0, 0, canvas.value.width, canvas.value.height)
|
||||
const centerY = canvas.value.height / 2
|
||||
for (let i = 0; i < BAR_COUNT; i++) {
|
||||
// 平滑过渡动画:当前高度逐步靠近目标高度
|
||||
currentHeights.value[i] = currentHeights.value[i] * 0.7 + targetHeights[i] * 0.3
|
||||
const barHeight = currentHeights.value[i]
|
||||
const x = i * (BAR_WIDTH + BAR_GAP) + BAR_GAP / 2
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.6)'
|
||||
// 画中间矩形
|
||||
ctx.fillRect(x, centerY - barHeight + RADIUS, BAR_WIDTH, barHeight * 2 - RADIUS * 2)
|
||||
// 画顶部半圆
|
||||
ctx.beginPath()
|
||||
ctx.arc(x + BAR_WIDTH / 2, centerY - barHeight + RADIUS, RADIUS, Math.PI, 0)
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
||||
// 画底部半圆
|
||||
ctx.beginPath()
|
||||
ctx.arc(x + BAR_WIDTH / 2, centerY + barHeight - RADIUS, RADIUS, 0, Math.PI)
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
||||
}
|
||||
}
|
||||
|
||||
// 动画主循环,保证高帧率刷新
|
||||
const animate = () => {
|
||||
drawWave()
|
||||
animationId = requestAnimationFrame(animate)
|
||||
}
|
||||
|
||||
// 监听PCM数据变化,更新目标高度
|
||||
watch(() => props.pcmData, (val) => {
|
||||
latestPcm.value = val
|
||||
targetHeights = calcTargetHeights(val)
|
||||
})
|
||||
|
||||
// 组件挂载时启动动画
|
||||
onMounted(() => {
|
||||
latestPcm.value = props.pcmData
|
||||
targetHeights = calcTargetHeights(props.pcmData)
|
||||
// 设置canvas宽高
|
||||
if (canvas.value) {
|
||||
canvas.value.width = CANVAS_WIDTH
|
||||
canvas.value.height = CANVAS_HEIGHT
|
||||
}
|
||||
animate()
|
||||
})
|
||||
|
||||
// 组件卸载时停止动画
|
||||
onBeforeUnmount(() => {
|
||||
if (animationId) cancelAnimationFrame(animationId)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.audio-waveform {
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.audio-waveform canvas {
|
||||
width: 288px;
|
||||
height: 48px;
|
||||
display: block;
|
||||
background: transparent;
|
||||
}
|
||||
</style>
|
Loading…
Reference in New Issue