音频可视化

150 阅读3分钟

实现当时电脑播放器中随着音乐的激烈程度产生高低不同的动态柱子

主要应用的技术是 requestAnimationFrame() 动画帧 、 canvas 、 十六进制颜色随机(中间注释的那部分) 、 偏白色的简约风格的按钮,带动画。 目前只实现了一首歌。歌曲切换根据期待值后面进行添加

1699863783886.jpg

有兴趣的小伙伴可以优化后面的歌曲名字,切换歌曲,快进等功能。

废话不多说,直接贴代码
<template>
	<div class="p30 angbg">
		<div>音频可视化</div>
		<div class="width-full lookbrod mb30">
			<canvas id="myCanvas" style="border:1px solid #000;width:100%;height:100%"></canvas>
		</div>
		<div class="text-center">
			
		</div>
		 <!-- // 时间与进度条 -->
		<div class="flex">
			<div>{{timefitter(begintime)}}</div>
			<el-progress class="flex-1 pl20 pr20 progress"  :show-text='false' :percentage="percentage" color="#67c23a" />
			<div>{{timefitter(endtime)}}</div>
		</div>
		<!-- // 左右开始暂停 -->
		<div class="flex flex-around p20">
			<div class="text-center">
				<div class="iconfont icon-skip-back-fill btnbg " :class="isclick? 'actbtn' : ''" @click="clickit"></div>
			</div>
			<div class="text-center" v-if="isplay">
				<div class="iconfont icon-zanting1 btnbg " :class="isclick? 'actbtn' : ''" @click="pause"></div>
			</div>
			<div class="text-center" v-else>
				<div class="iconfont icon-icon_play btnbg " :class="isclick? 'actbtn' : ''" @click="play"></div>
			</div>
			<div class="text-center">
				<div class="iconfont icon-skip-forward-fill btnbg " :class="isclick? 'actbtn' : ''" @click="clickit"></div>
			</div>
		</div>
		<!-- // 隐藏的音频原生 -->
		<div class="audiof">
			<audio id='myaudio'  @canplay="canplay" @play='onplay' controls :src="videosrc"></audio>
		</div>
	</div>
</template>

<script setup>
	import { ref,nextTick,computed ,onMounted} from 'vue';
	import videosrc from '@/assets/img/test.mp3'
	let myaudio = null
	nextTick(()=>{
		myaudio = document.getElementById('myaudio')
		// 监听播放 中
		myaudio.addEventListener('timeupdate', (event) => {
			// 当前播放时间赋值
			begintime.value = event.target.currentTime
		});
	})
	
	

	onMounted(() => {
		initcanvas()
		draw()
	})
	let initcanvas = function(){
		let canvas = document.getElementById("myCanvas");
		
		let ctx = canvas.getContext("2d"); 
		
		ctx.fillRect(0, 0, 300, 150);
		ctx.fillStyle = '#000'
	}
	
	// 准备好播放了!
	let canplay = ()=>{
		 endtime.value =  myaudio.duration.toFixed()
	}

	// 是否正在播放
	let isplay = ref(false)
	// 开始时间
	let begintime = ref(0)
	// 总时间
	let endtime = ref(0)
	// 计算当前进度
	let percentage = computed(() => {
		return begintime.value * 100 / endtime.value 
	}) 
	// 格化显示的时间
	let timefitter = (time)=>{
		let values,left ,right;
		left = parseInt(time/60)
		right =  Math.ceil(time % 60)
		function addzro(num){
			if(num < 10) {
				num = '0' + num
			}
			return num
		}
		left = addzro(left)
		right = addzro(right)
		values = left + ':' + right
		return values
	}
	
	// 封装一个按钮的点击效果,通过调用 Promise 来实现延时后执行的动画
	let isclick = ref(false)
	let clickprmisese = ()=>{
		isclick.value = true
		return new Promise((res,rej)=>{
			setTimeout(function(){
				isclick.value = false
				res('完成了')
			}, 300);
		})
	}
	
	let clickit = ()=>{
		clickprmisese().then(res=>{
			console.log(res,'---')
		})
	}
	
	// 点击播放
	let play = ()=>{
		clickprmisese().then(res=>{
			isplay.value = true
			myaudio.play()
		})
	}
	// 开始播放后调用的函数
	let isInit = false
	let dataArry ,analyser ,source ,audCtx
	let onplay = ()=> {
		if(isInit){
			return
		}
		audCtx = new AudioContext() // 创建音频上下文
		analyser = audCtx.createAnalyser();
		source = audCtx.createMediaElementSource(myaudio)   //创建音频源节点
		analyser.fftSize = 512;
		dataArry =  new Uint8Array(analyser.frequencyBinCount)
		source.connect(analyser);
		analyser.connect(audCtx.destination)
		
		isInit = true
	}
	let draw = () => {
	      let cvs = document.getElementById("myCanvas");
	      let ctx = cvs.getContext("2d");
	      requestAnimationFrame(draw);
	      //清空画布
	      const { width, height } = cvs;
	      ctx.clearRect(0, 0, width, height);
		   // ctx.fillRect(0, 0, width, width);
		  
		  ctx.fillStyle = '#000'
	      //判断分析器是否初始化
	      if (!isInit) {
	        return;
	      }
	      //分析器节点分析出数据到数组中
	      analyser.getByteFrequencyData(dataArry); //让分析器节点分析当前音频源数据,把分析结果添加到数组
		  
		  // console.log(dataArry)
		  
	      const len = dataArry.length / 2.5;
	      const barWidth = width / len /2 ;
		  // function color16(){//十六进制颜色随机
		  //   const r = Math.floor(Math.random()*256);
		  //   const g = Math.floor(Math.random()*256);
		  //   const b = Math.floor(Math.random()*256);
		  //   const color = `#${r.toString(16)}${g.toString(16)}${b.toString(16)}`;
		  //   return color;
		  // }
	      ctx.fillStyle =  "#78c5f7";
	      for (let i = 0; i < len; i++) {
	        const data = dataArry[i]; //<256
	        const barHeight = (data / 255) * height;
	        const x1 = i * barWidth + width / 2;
	        const x2 = width / 2 - (i + 1) * barWidth;
	        const y = height - barHeight;
	        ctx.fillRect(x1, y, barWidth, barHeight);
	        ctx.fillRect(x2, y, barWidth, barHeight);
	      }
	    }

	// 点击暂停
	let pause = ()=>{
		clickprmisese().then(res=>{
			isplay.value = false
			myaudio.pause()
		})
	}
	
</script>



<style lang="scss" scoped>
	.btnbg{
		-webkit-appearance: none;
		box-shadow: -10px -10px 15px rgba(255,255, 255, .5),10px 10px 15px rgba(70,70, 70, .12);
		width:50px;
		line-height: 50px;
		border-radius: 50%;
	}
	
	.actbtn:hover{
		box-shadow: -10px -10px 15px rgba(255,255, 255, .5),
		10px 10px 15px rgba(70,70, 70, .12),
		inset -15px -15px 15px rgba(255,255, 255, .5),
		inset 15px 15px 15px rgba(70,70, 70, .12);
		color: rgba(85, 170, 0, 0.5);
	}
	.audiof{
		display: none;
		
	}
	.lookbrod{
		height: 50%;
		background-color: #000;
	}
	.angbg{
		background-color: #ececec;
		height: 100%;
	}

</style>

<style lang="scss">
	.progress{
		.el-progress-bar{
			.el-progress-bar__outer{
				background-color: #e5e5e5;
			}
		}
	}
</style>