web audio API实现可视化音乐盒

  最近给个人博客做了个音乐盒功能,能实现暂停,切换歌曲,拖拽时间,音频可视化的功能,效果:在个人博客界面右上角可以看到旋转的音乐图标,hover后可展示音乐盒。因为项目中用了vuecli框架,音乐盒以vue组件的形式编写,图标用到了阿里图标库,如果你想直接使用该组件需要替换下图标,css变量。
web audio API实现可视化音乐盒_第1张图片

1 旋转音标

  通过animation重复动画实现,不是很难,值得注意的是rotate对inline标签不起作用,需要转化成block或者inline-block,直接上代码:

.musicBox .icon-music{
/* rotate对inline元素不起作用,转化成block */
	display: block;
	font-size: 1.8em;
	color: var(--green1);
	cursor: pointer;
	animation: musicRotate 5s linear infinite;
}
@keyframes musicRotate{
	from{
		transform: rotate(0);
	}
	to{
		transform: rotate(360deg);
	}
}

2 拖拽条设置

  一个input做拖动条,一个div标签做填充。

<div class="progress">div>
<input
	type="range" 
	class="range" 
	:value='audio.value' 
	min="0" max="1" step="0.001" 
	@mousedown="setTime=true" 
	@touchstart="setTime=true"
	@input="dragTime"
	@change="changeTime"
>

js部分包含了audio API部分,可以先跳过看css,看完音频可视化再来看这部分js,拖拽过程实时更新进度条跟时间,但是不更新播放的音乐,当松手(input的值改变)时计算出时间然后跳转到指定时间音乐,audioCtx跟source属于Audio API。

dragTime(e){
	const value = e.target.value
	// 更新当前时间
	const minutes = parseInt(value * source.buffer.duration / 60, 10)
	const seconds = parseInt(value * source.buffer.duration % 60)
	this.audio.currentMinutes = minutes
	this.audio.currentSeconds = seconds
	if(minutes < 10)
		this.audio.currentMinutes = '0' + minutes
	if(seconds < 10)
		this.audio.currentSeconds = '0' + seconds
	this.audio.value = value
	document.querySelector('.progress').style.width = this.audio.value*95 + '%'
},
changeTime(e){
	const value = e.target.value
	offect = value * this.buffer.duration
	source.stop()
	audioCtx = new AudioContext()//重置audioCtx,会清空currentTime
	this.setSource()
	// 音频立即从offect秒开始播放
	source.start(0,offect)
	this.playing = true
	this.setTime = false
},

修改默认range的样式,并让div标签与input标签重叠,将div标签覆盖在input上方,改变div的宽度实现背景已播放部分的背景填充。

/* 去掉默认样式 */
.range{
	-webkit-appearance: none;
	width: 100%;
	border: none;
	position: absolute;
	left: 0;
}
.range:focus{
	box-shadow: none;
}
/* 进度条样式 */
.range::-webkit-slider-runnable-track{
	height: 5px;
	border-radius: 20px;
	background-color: var(--tint-gray);
	z-index: 0;
}  
/* 滑块样式 */
.range::-webkit-slider-thumb{
	-webkit-appearance: none;
	height: 13px;
	width: 13px;
	margin-top: -4px;
	background-color: var(--green2);
	border-radius: 50%;
	cursor: pointer;
}
/* 填充背景 */
.progress{
	background-color: var(--green1);
	height: 5px;
	width: 0;
	border-radius: 20px;
	position: absolute;
	overflow: hidden;
	z-index: 1;
	cursor: pointer;
}
/* 解决覆盖在input上方的部分无法被点击问题 */
.progress:hover{
	z-index: 0;
}

3 audio API使用

参考文档,主要用到了:

  • AudioContext
  • Analyser - 音频解析器
  • BufferSource - 音频播放器
var source,audioCtx,analyser,offect=0
loadMusic(){//部分内容
	// 初始化audio API
	let AudioContext = window.AudioContext || window.webkitAudioContext;
	audioCtx = new AudioContext() //实例化AudioContext对象
	var dataArray //存放解析后的数据
	
	// 请求音频文件,以二进制格式返回
	this.$axios.get(this.audio.url,{responseType:'arraybuffer'})
	.then(res => {
		// 解码二进制文件
		audioCtx.decodeAudioData(res.data,(buffer) => {
			this.buffer = buffer
			this.setSource()
			// 创建接受数组
			dataArray = new Uint8Array(analyser.frequencyBinCount)
			// 计算音频时长
			this.audio.minutes = parseInt(buffer.duration / 60, 10)
			this.audio.seconds = parseInt(buffer.duration % 60)
			// 开始播放
			if(!firstLoad){//第一次播放需要点击触发,在mounted部分定义
				this.playing = true
				source.start()
			}
			draw()
		})
	})
}setSource(){
	// 创建音频解析器
	analyser = audioCtx.createAnalyser()
	// 计算频域信号时使用的 FFT,值为需要可视化的数量的两倍
	//例如项目中创建了32根线,这里就用64,需要是以2为底的数。
	analyser.fftSize = 64 
	// 创建播放节点
	source = audioCtx.createBufferSource()
	// 填充音频buffer数据
	source.buffer = this.buffer
	// 连接节点
	source.connect(analyser)
	analyser.connect(audioCtx.destination)
	// 播放结束事件
	source.onended = () => {
		if(audioCtx.currentTime > 0)
			this.switchMusic(1)//切换下一曲
	}
},

注释都挺完整的了,应该都能理解。可能播放事件的条件比较疑惑,这里需要考虑到changeTime里的事件

  • audioCtx.currentTIme理解为source.start()后播放的时间,单纯的改变source是不能够修改audioCtx.currentTIme的值得,所以利用重置audioCtx清零currentTime
  • source.stop()会触发播放结束事件
  • 通过拖拽触发的播放结束事件将不被触发,只有真正的播放结束才会触发
  • 使用BufferSource来播放音乐是无法直接改变currentTime的(报错只可读,暂未找到其他解决方法)
  • 通过调整开始播放的时间来实现切换播放时间的效果
  • 设置offect来填充跳过的音频,当前播放时间=audioCtx.currentTIme + offect
// 获取到音频已播放的时长
audioCtx.currentTime

changeTime(){
	……
	// 停止播放
	source.stop()
	audioCtx = new AudioContext()//重置audioCtx,会清空currentTime
	this.setSource()
	// 音频立即从offect秒开始播放,offect的值参考完整的changeTime函数
	source.start(0,offect)
	……
}

4 音频可视化

也可以用canvas画图的方式实现

<div class="animate">
	<div
		class="item"
		v-for="item in 32"
		:key="item"
	>
	div>
div>
.musicBox .box .animate{
	width: 100%;
	height: 80px;
	display: flex;
	justify-content: space-between;
	align-items: flex-end;
	overflow: hidden;
}
.musicBox .box .animate .item{
	width: 2px;
	height: 30px;
	background-color: var(--green1);
	border-radius: 4px;
}
loadMusic(){
……
const doms = document.querySelectorAll('.animate .item')
const draw = () => {
	requestAnimationFrame(draw)
	// 判断是否是正在播放状态
	if(!this.playing) return
	analyser.getByteFrequencyData(dataArray)//傅里叶计算
	
	doms.forEach((item,i) => {//修改线长
		item.style.height = `${dataArray[i]/8 + 30}px`
	})
	
	// 判断是否处于拖拽状态
	if(this.setTime) return
	// 获取当前播放的时间
	const currentTime = audioCtx.currentTime + offect
	const minutes = parseInt(currentTime / 60, 10)
	const seconds = parseInt(currentTime % 60)
	this.audio.currentMinutes = minutes
	this.audio.currentSeconds = seconds
	if(minutes < 10)
		this.audio.currentMinutes = '0' + minutes
	if(seconds < 10)
		this.audio.currentSeconds = '0' + seconds
	this.audio.value = currentTime / source.buffer.duration
	document.querySelector('.progress').style.width = this.audio.value*94 + '%'
}
draw()
……
}

js部分也比较简单包含两部分:

  1. 可视化部分用到了getByteFrequencyData方法来获取到傅里叶计算后的结果,根据结果计算线的高度。
  2. 更新当前播放时间,如果处于拖拽状态的话直接跳过。正常播放状态实时更新时间,前面提到了当前播放时间=audioCtx.currentTime + offect,这里的offect很关键,因为audioCtx.currentTime仅代表已播放的时间,前面已经被重置过了,所以不准确需要用offect来补充。

5 完整代码

  可以在我的博客项目地址中找到,github: src -> components -> musicBox.vue
  项目中音频是写死的,可以自行通过请求来获取,还有字幕,播放列表,音效等功能待完善。







你可能感兴趣的:(前端Demo,音乐可视化,audio)