前言
在数字化时代,我们正在见证音乐和视觉艺术的深度融合。流行音乐图像化,即将音乐视觉化,成为了一种独特的艺术表达方式。它跨越了时间和空间,用视觉元素诠释音乐的情感和氛围,为听众带来全新的感官体验。
今天分享的是一个基于WebAudio、canvas、three.js实现的简易音乐播放器。由于篇幅过长,所以分为三篇文章进行讲解,该篇文章主要叙述通过canvas实现2d音频可视化。音乐播放功能实现请查看 基于canvas和three.js实现音频可视化一
先来看下一最终效果:
实现简介
本篇文章主要讲述使用canvas实现音频可视化。
功能实现分为以下几个步骤:
- 添加canvas标签,设置宽高和css样式
- 使用AudioContext获取音频每一帧对应数据
- 使用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实现音频可视化一