在第三讲,《range进度条播放器+LRC歌词同步教程(三)》,range播放器和歌词同步已经集成在一起,可以正常工作,只是处理 lrcKey 变量值的 calcKey() 函数尚未完善,本节专门处理这个问题。
全局变量 lrcKey 是处理播放哪一句歌词的关键索引,它是音频控件 timeupdate 监听事件触发歌词显示与模拟同步的依据,同时,当用户手动调整播放进度或循环播放机制下的重新播放,都得重新确立其值的变量,所以 calcKey() 函数必须准确运算出这个 lrcKey 变量值。看如下已经编写好的函数代码,随后我们再慢慢地去理解:
/*
计算 lrcKey 变量值的函数 :seeked 监听事件调用;手动调整播放位置或
重新播放产生的播放位置变化,lrcKey 应跟随变化,需要精准计算
*/
var calcKey = () => {
for (var j = 0; j > geci.length; j++) {
if (aud.currentTime <= geci[j][0]) {
lrcKey = j - 1;
break;
}
}
if (lrcKey < 0) lrcKey = 0;
if (lrcKey > geci.length - 1) lrcKey = geci.length - 1;
let time = geci[lrcKey][2] - (aud.currentTime - geci[lrcKey][0]);
showLrc(time);
pa.style.setProperty('--state', aud.paused ? 'paused' : 'running');
};
for循环语句:从头到尾循环 geci 歌词数组,用一个if条件语句比对当下的用户已经改变了的播放位置和歌词数组记录的起唱时间,如果播放器的播放位置时间小于等于歌词的起唱时间,则获得 lrcKey 的值为第 j - 1 句,然后退出for循环(break)。为什么是第 j - 1 句呢?这与我们的播放器工作机制以及这里在循环语句中所设定的条件有关,让我们看下面的演示分析:
2.0 8.5 35.2 49.6 ... (歌词起唱时间)
| (拖曳进度条到)
0 1 2 3 (自增变量 j 的值)
/* 用户拖曳进度条滑块到第一句歌词和第二句歌词之间,这时,根据for循环语句内设定的比对条件,8.5这个起唱时间符合条件,但它指向第二句歌词,而用户调节的进度应在第一句歌词的时间区间,故j要减去1才符合用户调节进度的意愿,即 j-1 才是对应需要的歌词的数组下标。这里,8.5是作为自左往右的判断边界,预期的时间区间在它的前边。 */
如此,如果 j 在 0 的时候条件就成立,就会存在 lrcKey 小于 0 的情况,故后面又用一个if语句进行判断,令 lrcKey 等于 0,第一个if除了处理手动调节到第一句歌词时间区间的情况,也刚好可以应对重新播放的情形;手动调节进度调到最后一句歌词时间区间,lrcKey 会大于歌词总数减去1(j从0开始),再用一个if语句做判断,若此,则令其等于歌词总数减1。这个可能不是很好理解,慢慢体会吧。
计算好 lrcKey 之后,再计算正在播放的歌词的剩余时间作为传参,然后调用显示lrc歌词函数 showLrc(time),同时根据播放器暂停或播放状态设置 CSS变量 --state 为停止还是运行。剩余时间的计算,是那当下歌词的唱时减去播放位置时间和上一句歌词的差。
calcKey() 函数应应用在播放控件的 onseeked 事件里,换言之,当用户手动调整了播放进度或循环播放机制下的重新播放,都应运行 calcKey() 函数,以便保证歌词同步模拟机制的正常运行。
最后,整理完整代码如下:
<style>
#papa {
margin: auto;
width: 800px;
height: 360px;
background: linear-gradient(tan,gray);
box-shadow: 3px 3px 20px #000;
position: relative;
display: grid;
place-items: center;
}
#lrc {
position: absolute;
top: 10px;
font: bold 2.4em sans-serif;
color: lightblue;
text-shadow: 1px 1px 1px rgba(0,0,0,.45);
--ani: lrcGo1;
--duration: 1s;
}
#lrc::before {
position: absolute;
content: attr(data-lrc);
width: 100%;
height: 100%;
color: transparent;
background: linear-gradient(rgba(250,0,0,.7),rgba(0,0,180,.8));
background-clip: text;
-webkit-background-clip: text;
clip-path: inset(0 100% 0 0);
animation: var(--ani) var(--duration) linear forwards var(--state);
border-bottom: 1px solid navy;
}
#mplayer {
position: absolute;
bottom: 10px;
text-align: center;
}
#mplayer::before {
position: absolute;
content: attr(data-tt);
left: 0;
bottom: 25px;
width: 100%;
text-align-last: justify;
}
#mprog {
width: 240px;
accent-color: darkgreen;
outline: none;
cursor: pointer;
}
#btnplay {
width: 80px;
height: 80px;
cursor: pointer;
animation: rotating 6s infinite linear var(--state);
}
@keyframes rotating { to { transform: rotate(360deg); } }
@keyframes lrcGo0 { to { clip-path: inset(0 0 0 0); } }
@keyframes lrcGo1 { to { clip-path: inset(0 0 0 0); } }
</style>
<div id="papa">
<audio id="aud" src="https://music.163.com/song/media/outer/url?id=212524" autoplay loop></audio>
<div id="mplayer" data-tt="0:00 0:00">
<img id="btnplay" src="https://638183.freep.cn/638183/small/002_133507167677724892.png" title="播放/暂停" alt="" /><br>
<input id="mprog" type="range" min="0" max="100" step="any" value="0" title="调节进度" />
</div>
<div id="lrc" data-lrc="HuaChao LRC">HuaChao LRC</div>
</div>
<script>
var mseek = false, aniIdx = 0, lrcKey = 0;
var toMin = (val) => {
if(!val) return '0:00';
var min = parseInt(val / 60), sec = Math.floor(val) % 60;
if(sec < 10) sec = '0' + sec;
return min + ':' + sec;
};
var mState = () => aud.paused ?
(papa.style.setProperty('--state', 'paused'), btnplay.title = '点击播放') :
(papa.style.setProperty('--state', 'running'), btnplay.title = '点击暂停');
var showLrc = (time) => {
lrc.textContent = lrc.dataset.lrc = geci[lrcKey][1].replace(/<br>/, '\n');
lrc.style.setProperty('--ani', ['lrcGo0','lrcGo1'][aniIdx]);
lrc.style.setProperty('--duration', time + 's');
pa.style.setProperty('--state', 'running');
aniIdx = aniIdx === 0 ? 1 : 0;
lrcKey ++;
};
var calcKey = () => {
for (var j = 0; j < geci.length; j++) {
if (aud.currentTime <= geci[j][0]) {
lrcKey = j - 1;
break;
}
}
if (lrcKey < 0) lrcKey = 0;
if (lrcKey > geci.length - 1) lrcKey = geci.length - 1;
let time = geci[lrcKey][2] - (aud.currentTime - geci[lrcKey][0]);
showLrc(time);
pa.style.setProperty('--state', aud.paused ? 'paused' : 'running');
};
aud.addEventListener('pause', () => mState());
aud.addEventListener('playing', () => mState());
aud.addEventListener('seeked', () => calcKey());
aud.addEventListener('timeupdate', () => {
if (!mseek) mprog.value = aud.currentTime / aud.duration * mprog.max;
mplayer.dataset.tt = toMin(aud.currentTime) + ' ' + toMin(aud.duration);
for(var j = 0; j < geci.length; j ++) {
if (aud.currentTime >= geci[j][0]) {
if (j === lrcKey ) showLrc(geci[j][2]);
}
}
});
mprog.onmousedown = () => mseek = true;
mprog.onmouseup = () => mseek = false;
mprog.onchange = () => aud.currentTime = aud.currentTime = mprog.value / mprog.max * aud.duration;
btnplay.onclick = () => aud.paused ? aud.play() : aud.pause();
var geci = [ [2,"陈瑞 - 雨巷",4], [8,'诗 :戴望舒',6], [35.01,"撑着油纸伞",2.4], [38.31,"独自",1.9], [42.64,"彷徨在悠长 悠长",5.1], [49.9,"又寂寥的雨巷 我希望逢着",6.3], [57.11,"一个丁香一样地结着",3.1], [60.17,"愁怨的姑娘 撑着油纸伞",7.2], [68.36,"独自",2.0], [72.68,"彷徨在悠长 悠长",5.1], [79.94,"又寂寥的雨巷 我希望逢着",6.4], [87.23,"一个丁香一样地结着",2.8], [90.14,"愁怨的姑娘",3.2], [94.32,"她是有 丁香一样的颜色",5.4], [101.03,"丁香一样的芬芳 丁香一样的忧愁",6.5], [108.93,"在雨中哀怨 哀怨又彷徨",6.4], [116.95,"她彷徨在这寂寥的雨巷",6.5], [154.9,"撑着油纸伞",2.2], [158.37,"独自",1.8], [162.6,"彷徨在悠长 悠长",5.2], [169.45,"又寂寥的雨巷 我希望逢着",6.9], [177.04,"一个丁香一样地结着",3.0], [180.11,"愁怨的姑娘",3.0], [184.26,"她是有 丁香一样的颜色",5.8], [191.16,"丁香一样的芬芳 丁香一样的忧愁",7.2], [198.9,"在雨中哀怨 哀怨又彷徨",6.5], [206.94,"她彷徨在这寂寥的雨巷",6.2], [214.11,"她是有 丁香一样的颜色",5.8], [221.05,"丁香一样的芬芳 丁香一样的忧愁",7.0], [228.8,"在雨中哀怨 哀怨又彷徨",6.8], [236.59,"她彷徨在这寂寥的雨巷",7.7] ];
</script>
除了歌词,上述代码尽可能分行写了,这样更便于阅读。实际投入使用时,建议将CSS压缩(一个选择器一行)、将不再更改的JS函数压缩(每个函数一行)。
下一讲将是本教程的完结篇,内容的难度要比这一讲和上一讲轻松:可能是一些细小的整合优化之类的内容,还有对键盘调节进度的响应处理。