基于canvas和three.js实现音频可视化二

前言
在数字化时代,我们正在见证音乐和视觉艺术的深度融合。流行音乐图像化,即将音乐视觉化,成为了一种独特的艺术表达方式。它跨越了时间和空间,用视觉元素诠释音乐的情感和氛围,为听众带来全新的感官体验。
今天分享的是一个基于WebAudio、canvas、three.js实现的简易音乐播放器。由于篇幅过长,所以分为三篇文章进行讲解,该篇文章主要叙述通过canvas实现2d音频可视化。音乐播放功能实现请查看 基于canvas和three.js实现音频可视化一

先来看下一最终效果:
在这里插入图片描述

实现简介

本篇文章主要讲述使用canvas实现音频可视化。

功能实现分为以下几个步骤:

  1. 添加canvas标签,设置宽高和css样式
  2. 使用AudioContext获取音频每一帧对应数据
  3. 使用canvas实现音频可视化

1、添加canvas标签,设置宽高和css样式

html代码

  <van-swipe-item>
    <div class="detailContent">
      <canvas class="my_canvas" ref="canvasDom" width="320" height="320"></canvas>
      <img src="@/assets/cd.png" alt="" class="img_cd" />
      <img :src="picUrl" alt="" class="img_ar" :class="{ img_ar_active: !isbtnShow, img_ar_pauesd: isbtnShow }" />
    </div>
  </van-swipe-item>
  

css部分

.my_canvas {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    z-index: -1;
  }

2、使用AudioContext获取音频每一帧对应数据

相关文档请参考Web Audio API
在data中添加变量musicOriginData保存音频源实例,通过实例获取每一帧数据
封装获取音频数据类

 class MusicSingleComp {
  constructor(data) {
    this.name = "MusicSingleComp";
    this._fftSize = 512;
    this._myAudioDom = data.audioDom
    // console.log('audioDom===>', this._myAudioDom)
    this._analyser = null;
    this._dataArray = [];
    this.isReady()
  }

  isReady() {
      const ctx = new window.AudioContext();
      // 创建AnalyserNode对象
      this._analyser = ctx.createAnalyser();
      // 设置 fftSize 属性
      // AnalyserNode 接口的 fftSize 属性的值是一个无符号长整型的值, 表示(信号)样本的窗口大小。当执行快速傅里叶变换(Fast Fourier Transfor (FFT))时,
      // 这些(信号)样本被用来获取频域数据。
      // fftSize 属性的值必须是从32到32768范围内的2的非零幂; 其默认值为2048。
      this._analyser.fftSize = this._fftSize;
      // 创建一个新的MediaElementAudioSourceNode对象
      const source = ctx.createMediaElementSource(this._myAudioDom);
      // audioSrc 和 analyser 进行链接
      source.connect(this._analyser);
      this._analyser.connect(ctx.destination);
      // Uint8Array 数组类型表示一个8位无符号整型数组,创建时内容被初始化为0。创建完后,
      // 可以以对象的方式或使用数组下标索引的方式引用数组中的元素。
      // AnalyserNode接口的 getByteFrequencyData() 方法将当前频率数据复制到传入的Uint8Array(无符号字节数组)中。
      // 如果数组的长度小于 AnalyserNode.frequencyBinCount, 那么Analyser多出的元素会被删除. 如果是大于, 那么数组多余的元素会被忽略.
      const bufferLength = this._analyser.frequencyBinCount;
      this._dataArray = new Uint8Array(bufferLength);
  }

  get byteFrequencyDate() {
    this._analyser.getByteFrequencyData(this._dataArray);
    // console.log("_dataArray===>");
    return this._dataArray;
  }
}

methods方法更新

initMusicOrigin() {
  	this.musicOriginData = new MusicSingleComp({
      audioDom: this.$refs.audio
    })
  },

3、使用canvas实现音频可视化

新建Entity类,用来存储对象

// 单体
class Entity {
  constructor() {
    this._compMap = new Map();
  }

  addComp(comp) {
    this._compMap.set(comp.name, comp);
  }

  getComp(compName) {
    return this._compMap.get(compName);
  }
}

新建canvas画图类,具体实现请看注释

class MusicEffectSingleComp {
  constructor(data) {
    this.name = "MusicEffectSingleComp";
    this._effectColor = data.effectColor || "#FFFFFF";
    // 使用三个色值画图
    this.colorArr = ['#4caf50', '#ffeb3b', '#ff5722']
    this._canvasDom = data.canvasDom;
    if (!this._canvasDom) {
      return
    }
    this._ctx = this._canvasDom?.getContext("2d");
    this._byteFrequencyData; 
    // 获取120条数据
    this._randomData = Uint8Array.from(new Uint8Array(120), (v, k) => k);
    // 打乱数据顺序
    this._randomData.sort(() => Math.random() - 0.5);
    this.byteFrequencyDate = new Uint8Array(120).fill(0);
    this.index = 0
  }

  set byteFrequencyDate(value) {
    this._byteFrequencyData = value;
    const bData = [];
   	// 设置画图使用用的数据
    this._randomData.forEach((value) => {
      bData.push(this._byteFrequencyData[value]);
    });
	// 360度平分成120份对应的度数
    const angle = (Math.PI * 2) / bData.length;
    // 设置画布大小
    this._ctx.clearRect(0, 0, this._canvasDom.width, this._canvasDom.height);
    this._ctx.save();
    // 矩形移动
    this._ctx.translate(this._canvasDom.width / 2, this._canvasDom.height / 2);
    // 遍历数组画图
    bData.forEach((value, index) => {
      this._ctx.save();
      this._ctx.rotate(angle * index);
      this._ctx.beginPath();
      // 每40条数据使用一种色值
      this._ctx.fillStyle = this.colorArr[Math.floor(index/40)];
      // value值最大为256,这里的h值范围为0-60
      const h = (value / 256) * 60;
      // 画矩形
      this._ctx.roundRect(-4, 110, 4, h < 4 ? 4 : h, 4);
      // 若上行的 roundRect 存在兼容性问题可以更换为下面注释的代码
      // this._ctx.fillRect(-4, 140,  4, (h < 4) ? 4 : h);
      this._ctx.fill();
      this._ctx.restore();
    });
    this._ctx.restore();
  }
}

新建获取音频源数据类MusicPlayer,通过requestAnimationFrame定时获取音频数据

class MusicPlayer {
  constructor(
    data = {
      // audioDom: null,
      audioContextOrigin: null,
      canvasDom: null,
      musicSrc: "./test.mp3",
      effectColor: "#FFFFFF",
    }
  ) {
    this._requestID = null;
    // 音频源dom对象
    this.audioContextOrigin = data.audioContextOrigin
    if (!this.audioContextOrigin) {
      throw new Error('缺少音乐数据源')
    }
    // 特效单体
    this._effectEntity = new Entity();
    this._effectEntity.addComp(
      new MusicEffectSingleComp({
        canvasDom: data.canvasDom,
        effectColor: data.effectColor,
      })
    );
    this.play()
  }
  play() {
    this._requestID = requestAnimationFrame(
      this._renderFrame.bind(this)
    );
  }
  paused() {
    cancelAnimationFrame(this._requestID);
  }
  _renderFrame() {
   // 定时获取音频数据
    this._requestID = requestAnimationFrame(this._renderFrame.bind(this));
    const data = this.audioContextOrigin.byteFrequencyDate
    // 截取数组前120条
    this._effectEntity.getComp("MusicEffectSingleComp").byteFrequencyDate = data.slice(0, 120);
  }
}

在data中添加变量isbtnShow标识当前状态是暂停或者播放中, play方法中更新播放状态,watch监听变量变化来控制动画开启和暂停
添加watch

watch: {
  isbtnShow: {
     handler(val) {
       if (!val) {
         this.$nextTick(() => {
           this.initMusicEffect();
         });
       }
        else {
         if (this.musicEffect) {
           this.musicEffect.paused();
         }
       }
     },
     immediate: true
   }
  },

methods方法更新

// 播放或暂停
async play() {
   // 在切换播放、暂停时先清除定时器
   clearInterval(this.playTimer)
   clearInterval(this.pauseTimer)
   clearInterval(this.countTimer)
   // 判断音乐是否播放
   // 暂停动作触发之后歌曲声音实际上并没有立即暂停,而是渐渐减小,
   // 这里设置一个标识isToPause用来表示当前是暂停状态
  if (this.$refs.audio.paused || this.isToPause) {
    this.isToPause = false
    this.isbtnShow = false
    this.handlerPlay()
    setTimeout(() => {
      this.setCountDown()
    }, 1000)
  } else {
    this.handlerPause()
  }
 },
  // canvas实现2d音频可视化
  initMusicEffect() {
    if (!this.musicOriginData) {
      this.initMusicOrigin()
    }
    if (this.musicEffect) {
      this.musicEffect.play()
    } else {
      this.musicEffect = new MusicPlayer({
        audioContextOrigin: this.musicOriginData,
        canvasDom: this.$refs.canvasDom
      })
    }
  },

最终实现效果请在文章开头查看
音乐播放实现请看上篇文章 基于canvas和three.js实现音频可视化一

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值