序
决定开始写博客了,从简单的东西写起吧。
今天依然在琢磨各平台音乐的实现细节,发现它们在播放和暂停歌曲的时候,都会有一个短暂的渐入渐出效果,下面就来动手实现一下。
设计
目前想到的较好的办法是在接到用户的播放请求时,通过在短时间内连续减少音量直至为0时再调用audio元素的pause接口,而接到用户的暂停请求时则首先调用audio元素的play接口,并在短时间内增加音量至用户设置的音量
想到需要连续进行某个步骤,脑子里就浮现出两种方法,setTimeOut或setInterVal和requestAnimationFrame,因为没有太耗时的持续异步操作,所以不需要用setTimeOut来避免setInterVal的累积效应(如果真的有这些操作,应该避免在主线程执行才对)。
我们希望音频的渐入渐出能够足够的顺滑,不要产生由于音量变化间隔太大而产生声音的颗粒感,尽管使用setInterVal时回调执行的间隔是不固定的,只当执行栈为空时才轮到异步队列的任务执行,但是目前并没有什么任务会阻碍回调函数执行,而requestAnimationFrame让回调函数随着屏幕的刷新而调用,使得音量变化的频率变得固定了,看起来好像能更顺滑一点(用它来作非动画渲染任务真的合适吗?)。纸上得来终觉浅,决定两者都实现然后对比一下效果
实现
先来看看有淡入淡出和没有淡入淡出的差别,点这里看示例
下面给出在requestAnimationFrame下的实现,首先在Vue构造选项内声明需要用到的datadata () {
return {
...
volume: 1, // 用户设置的歌曲音量
fadeVolume: 0, // 歌曲当前的音量,与volume不同的是,该变量是记录歌曲在淡入淡出时的实时音量
volumeFadeInFlag: null, // 保存歌曲淡入的计时器
volumeFadeOutFlag: null, // 保存歌曲淡出的计时器
...
}
}
之后通过循环不断增加或减少fadeVolume的值,直至它为volume或0,比如淡出的代码为songVolumeFadeOut (audio) {
this.volumeFadeInFlag && cancelAnimationFrame(this.volumeFadeInFlag)
const _this = this;
(function fadeOut () {
if (_this.fadeVolume - 0.02 <= 0) {
_this.fadeVolume = 0
audio.volume = _this.fadeVolume
cancelAnimationFrame(_this.volumeFadeOutFlag)
_this.volumeFadeOutFlag = null
audio.pause()
} else {
_this.fadeVolume = _this.fadeVolume - 0.02
_this.fadeVolume = Number(_this.fadeVolume.toFixed(2))
audio.volume = _this.fadeVolume
_this.volumeFadeOutFlag = requestAnimationFrame(fadeOut)
}
})()
}
注意在执行淡出循环前需要先清除淡入循环,如果不这样做的话如果用户猛点暂停和播放按钮,就会同时存在多个淡入淡出,并且它们都无法达到终止条件,从而陷入死循环中,因为刷新频率是一秒大约60次,设置增量为0.02,就可以在大约一秒内结束淡入和淡出。
写好了淡出淡入代码后,就要寻找执行时机了,我把淡入放在audio元素play事件的回调函数中,因此淡入代码不需要手动调用audio.play(),把淡出放在表示音频播放状态的playing变量的监听中,当收到音频暂停的请求时执行淡出。onPlay() {
...
// 在窗口最小化或被隐藏时,requestAnimationFrame将会推迟到窗口恢复后执行,这不是我们想要的
if (document.visibilityState == 'visible') {
this.songVolumeFadeIn(audio)
} else {
// 因此在窗口不可见时我们使用setInterval来代替requestAnimationFrame
this.songVolumeFadeInByInterval(audio)
}
...
}
...
watch: {
playing() {
...
if (document.visibilityState == 'visible') {
this.songVolumeFadeOut(audio)
} else {
this.songVolumeFadeOutByInterval(audio)
}
...
}
}
前面说到requestAnimationFrame是专为动画设计的API为了提高性能和电池寿命,因此在大多数浏览器里,当requestAnimationFrame() 运行在后台标签页或者隐藏的 里时,requestAnimationFrame() 会被暂停调用以提升性能和电池寿命。
如果在普通网页中使用requestAnimationFrame来实现音频淡入淡出效果,当然没问题(假设用户没有极快的手速点击播放按钮后瞬间切标签或最小化浏览器),但是在实际的桌面播放器中,不止一个主界面可以控制音频的播放和暂停,mini窗口以及托盘菜单等都能控制,此时如果主界面被最小化,则不会进行requestAnimationFrame循环,因此在这里使用了document.visibilityState来判断主界面是否可见,据此来选择到底是使用requestAnimationFrame还是setInterval。
点这里查看简单版源代码