侧边栏壁纸
  • 累计撰写 53 篇文章
  • 累计创建 12 个标签
  • 累计收到 8 条评论

目 录CONTENT

文章目录

音频可视化

Kirito
2024-04-16 / 0 评论 / 0 点赞 / 58 阅读 / 8890 字 / 正在检测是否收录...

<audio>作为HTML5中非常经典的组件之一,我们也看到了很多音频可视化相关的作品,不论是Wallpaper Engine,还是滑雪动画化,都是针对于音频流分析的应用领域。

示例

Web Audio API

AudioContext()

俗称音频上下文,以下用 audioAtx表述,最最最主要的Web Audio API,几乎包含所有音频相关的功能API。可选参数 latencyHint,值为 balancedinteactive(默认),playback,这里我们使用默认值即可,即不传参。

AudioContext().createMediaElementSource()

创建音频源节点,传递一个 audio对象,我这里没有使用html的 <audio>组件,通过初始化时直接 new Audio()对象实现。用于连接API和对应的Audio对象。

AudioContext().createAnalyser()

创建音频分析器,用于分析音频信息,获取具体音频数据,在这里我们用分析器返回的数据来实现可视化绘制。

JS代码逐个分析

初始化分析器

let audioRef = ref<HTMLMediaElement>(new Audio())
let isInit = false
let audioAnalyser: AnalyserNode
const audioArray = ref<Uint8Array>(new Uint8Array(512).fill(0))

const init = (audio: HTMLMediaElement) => {
  audio.onplay = () => {
    if (isInit) return
    const audCtx = new AudioContext()
    // 音频源节点
    const source = audCtx.createMediaElementSource(audio)
    // 分析器
    audioAnalyser = audCtx.createAnalyser()
    audioAnalyser.fftSize = 512
    audioArray.value = new Uint8Array(audioAnalyser.frequencyBinCount)
    source.connect(audioAnalyser)
    audioAnalyser.connect(audCtx.destination)

    isInit = true
    // 绘制函数
    draw()
  }
  audio.onended = () => {
    state.value = false
  }
}

Audio()对象设置对应状态的回调函数,在开始播放回调函数 onplay()函数中,初始化Web Audio API相关的对象,以及调用动画帧绘制函数。

Canvas绘制函数

const draw = () => {
  requestAnimationFrame(draw)
  const { width, height } = canvasRef.value || { width: 0, height: 0 }
  const ctx = canvasRef.value?.getContext('2d')
  ctx?.clearRect(0, 0, width, height)
  if (ctx) ctx.fillStyle = '#5aa4ae'

  audioAnalyser.getByteFrequencyData(audioArray.value)
  const len = audioArray.value.length / 5
  const nodeWidth = width / len / 2
  for (let i = 0; i < len; i++) {
    const audioNode = audioArray.value[i]
    const nodeHeight = (audioNode / 255) * height
    // 对称从中向左向右绘制
    const x1 = i * nodeWidth + width / 2
    const x2 = width / 2 - (i + 1) * nodeWidth
    const y = height - nodeHeight
    ctx?.fillRect(x1, y, nodeWidth - 2, nodeHeight)
    ctx?.fillRect(x2, y, nodeWidth - 2, nodeHeight)
  }
}

在这个动画帧函数中,利用分析器所返回的音频数据的数组,进行简单的计算后,绘制在Canvas组件内,实现可视化效果。

播放/暂停

const play = () => {
  if (fileList.value.length === 0) {
    // 使用远程OSS音源
    audioRef.value.crossOrigin = 'anonymous' // 跨域Cors
    audioRef.value.src = 'OSS_PATH'
    audioRef.value.play()
    state.value = !state.value
  } else {
    // 使用本地音源
    const file = fileList.value[0]
    const reader = new FileReader()
    reader.onload = () => {
      audioRef.value.src = reader.result as string
      audioRef.value.play()
    }
    reader.readAsDataURL(file)
    state.value = !state.value
  }
}

const pause = () => {
  if (audioRef.value) {
    audioRef.value.pause()
    state.value = !state.value
  }
}

当远程音源涉及到跨域访问时,需设置 Audio()对象的crossOrigin属性为 'anonymous',否则浏览器将直接拦截该请求。

当使用本地音源文件时,利用 FileReader()读取器获取数据地址,再行播放。

Audio()对象还有许多的函数、生命周期、回调等等,此处不详细赘述。

全代码

<script setup lang="ts" name="audio-draw">
import { ref, h, onMounted } from 'vue'
import { UploadOutlined, CaretRightOutlined, PauseOutlined } from '@ant-design/icons-vue'
import type { UploadProps } from 'ant-design-vue'

let audioRef = ref<HTMLMediaElement>(new Audio())
let isInit = false
let audioAnalyser: AnalyserNode
const audioArray = ref<Uint8Array>(new Uint8Array(512).fill(0))
const canvasRef = ref<HTMLCanvasElement>()
const fileList = ref<File[]>([])
const state = ref<boolean>(false)

const beforeUpload: UploadProps['beforeUpload'] = (file: File) => {
  fileList.value = [file]
  return false
}

// 播放
const play = () => {
  if (fileList.value.length === 0) {
    // 使用远程OSS音源
    audioRef.value.crossOrigin = 'anonymous' // 跨域Cors
    audioRef.value.src = 'OSS_PATH'
    audioRef.value.play()
    state.value = !state.value
  } else {
    // 使用本地音源
    const file = fileList.value[0]
    const reader = new FileReader()
    reader.onload = () => {
      audioRef.value.src = reader.result as string
      audioRef.value.play()
    }
    reader.readAsDataURL(file)
    state.value = !state.value
  }
}

// 暂停
const pause = () => {
  if (audioRef.value) {
    audioRef.value.pause()
    state.value = !state.value
  }
}

// 绘制Canvas
const draw = () => {
  requestAnimationFrame(draw)
  const { width, height } = canvasRef.value || { width: 0, height: 0 }
  const ctx = canvasRef.value?.getContext('2d')
  ctx?.clearRect(0, 0, width, height)
  if (ctx) ctx.fillStyle = '#5aa4ae'

  audioAnalyser.getByteFrequencyData(audioArray.value)
  const len = audioArray.value.length / 5
  const nodeWidth = width / len / 2
  for (let i = 0; i < len; i++) {
    const audioNode = audioArray.value[i]
    const nodeHeight = (audioNode / 255) * height
    // 对称从中向左向右绘制
    const x1 = i * nodeWidth + width / 2
    const x2 = width / 2 - (i + 1) * nodeWidth
    const y = height - nodeHeight
    ctx?.fillRect(x1, y, nodeWidth - 2, nodeHeight)
    ctx?.fillRect(x2, y, nodeWidth - 2, nodeHeight)
  }
}

// 初始化Audio分析器
const init = (audio: HTMLMediaElement) => {
  audio.onplay = () => {
    if (isInit) return
    const audCtx = new AudioContext()
    // 音频源节点
    const source = audCtx.createMediaElementSource(audio)
    // 分析器
    audioAnalyser = audCtx.createAnalyser()
    audioAnalyser.fftSize = 512
    audioArray.value = new Uint8Array(audioAnalyser.frequencyBinCount)
    source.connect(audioAnalyser)
    audioAnalyser.connect(audCtx.destination)

    isInit = true
    draw()
  }
  audio.onended = () => {
    state.value = false
  }
}

onMounted(() => {
  init(audioRef.value)
})
</script>
<template>
  <div class="audioDraw flex-box">
    <div class="box">
      <canvas ref="canvasRef" id="canvas"></canvas>
      <div class="operation">
        <a-upload
          name="file"
          :file-list="fileList"
          accept=".mp3"
          :before-upload="beforeUpload"
          :showUploadList="false"
        >
          <a-button>
            <upload-outlined></upload-outlined>
            选择文件
          </a-button>
        </a-upload>
        <span style="margin-left: 0.5rem">{{
          fileList.length === 0 ? '默认音频(自己唱的,没版权风险)' : fileList[0].name
        }}</span>
        <a-button
          class="play"
          shape="circle"
          v-show="!state"
          :icon="h(CaretRightOutlined)"
          @click="play"
        ></a-button
        ><a-button
          class="play"
          shape="circle"
          v-show="state"
          :icon="h(PauseOutlined)"
          @click="pause"
        ></a-button>
      </div>
    </div>
  </div>
</template>
<style lang="less" scoped>
.audioDraw {
  .box {
    width: 36rem;
    height: 20rem;
    canvas {
      width: 100%;
      height: 18rem;
      border: 0.1rem solid rgba(89, 118, 186, 0.5);
      border-radius: 1rem;
    }
    .operation {
      display: flex;
      justify-content: center;
      align-items: center;
      .play {
        margin-left: auto;
      }
    }
  }
}
</style>

0

评论区