feat(components): 添加音频波形组件

- 实现了一个基于 Canvas 的音频波形组件
- 组件根据 PCM 数据动态绘制波形
- 采用平滑动画效果,实现流畅的波形显示
- 优化了波形计算算法,提高性能和准确性
This commit is contained in:
Lexcubia 2025-04-27 11:51:11 +08:00
parent f7dd3b43fe
commit 6a20e26e5d
1 changed files with 128 additions and 0 deletions

View File

@ -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>