最近给个人博客做了个音乐盒功能,能实现暂停,切换歌曲,拖拽时间,音频可视化的功能,效果:在个人博客界面右上角可以看到旋转的音乐图标,hover后可展示音乐盒。因为项目中用了vuecli框架,音乐盒以vue组件的形式编写,图标用到了阿里图标库,如果你想直接使用该组件需要替换下图标,css变量。
通过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);
}
}
一个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;
}
参考文档,主要用到了:
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)
……
}
也可以用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部分也比较简单包含两部分:
可以在我的博客项目地址中找到,github: src -> components -> musicBox.vue
项目中音频是写死的,可以自行通过请求来获取,还有字幕,播放列表,音效等功能待完善。
{{audio.name}}
{{audio.currentMinutes}}:{{audio.currentSeconds}}/{{audio.minutes}}:{{audio.seconds}}