range进度条播放器+LRC歌词同步教程(三)

位置: 首页 > 前端三套件
[发布: 2024.2.3  作者: 马黑  阅读: 126]

上一讲,《range进度条播放器+LRC歌词同步教程(二)》,我们探讨了关键帧动画的反复运行,以双动画来回替换的形式驱动歌词模拟lrc同步,但还没有加入歌词,本节就解决歌词问题、以及歌词如何真正模拟lrc同步。

我创建的花潮格式lrc歌词数组是一个二维数组,结构如下:

geci = [
	[2,"歌词1",3],
	[6,"歌词2",4.2],
	[12.41,"歌词N",3.1]
];

各行歌词自身就是一个数组,它有三个数组元素:第一个是数据(number)格式,记录当句歌词起唱时间(秒);第二个是字符串(string)格式,用引号包裹起来,是要显示的歌词;第三个是数据(number)格式,记录的是当句歌词用时(秒)。这些,它们是子维数组元素,是子数组,它们整体放在一个中括号 [] 内,外围中括号是父维数组。geci,是歌词数组变量名,我们要读取第一行歌词信息,就通过 geci[0] 来操作,geci[0] 将会得到 [2,"歌词1",3],,是一个数组。数组通过下标读取数字子项目的内容,下标从 0 起算,0 小标对应第一个数组元素,1对应第二个,依此类推。看下面的读取举例:

let ar1 = geci[0]; /* ar1 会得到一个数组格式的数据 → [2,"歌词1",3], */
let gc1 = ar1[1]; /* gc1 会得到一个字符串 → 歌词1 */

/* 可以一次性读取第N句歌词(N是一个具体数字) */
let gc2 = geci[N][1]; /* gc2 的值 → 歌词1 */

对二维数组的读取,第一个下标 [N] 读到第N个子数组,第二个下标 [1] 的子数组的下标,指向子数组的第 2 个元素。

弄懂了读取歌词机制,下来就是如何让歌词显示出来并模拟lrc同步。我们应该还有印象,audio音频控件的timeupdate事件会返回播放器的 currentTime 信息,currentTime 是音频播放器当前的播放位置(以秒计),我们就在这里做文章:在 audio 的 timeupdate 事件中,将 currentTime 和 歌词数组记录的每一句歌词的起唱时间进行比对,符合我们设定的条件就把歌词显示出来。条件怎么设定?比如现在timeupdate事件返回的currentTime为2.5秒,geci数组里头,第一句的起唱时间是2秒、说明现在正在唱“歌词1”,即将唱“歌词2”,歌词2起唱时间是6秒,待timeupdate事件返回大于等于6秒的恰那,我们就处理相关事宜让lrc元素显示歌词2、执行关键帧动画。请看代码样板来加以理解:

/* audio timeupdate 监听事件 */
aud.addListener('timeupdate', () => {
	/* ... 这里是进度驱动等代码 */
	for (let j = 0; j < geci.length; j ++) {
		if (aud.currentTime >= geci[j][0]) {
			/* ... 这里的代码将实现歌词显示机制 */
		}
	}
});

for循环放在timeupdate监听事件里,这意味着audio每返回一次 currentTime,我们都要循环一次歌词数组,然后,如上已述,我们在这里要比对 currentTime 和 geci[j][0],如果 currentTime 大于等于数组下标为 j 的子数组的起唱时间记录,就处理歌词显示及执行动画等工作。处理机制我们写成一个 showLrc(time) 函数,time 是个传参,将有 timeupdate 监听等事件传给:

/* 声明一个全局变量 lrcKey : 用来记录处理的歌词序号 */
var lrcKey = 0;

/* 显示lrc歌词函数 */
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'); /* ③ */
	papa.style.setProperty('--state', 'running'); /* ④ */
	aniIdx = aniIdx === 0 ? 1 : 0; /* ⑤ */
	lrcKey ++; /* ⑥ */
};

函数的第 ① 行,令 lrc 的文本(lrc.textContent)和标签携带的 data-lrc 属性值都等于 geci 数组第 lrcKey 子项的第二子项 geci[lrcKey][1] 即歌词字符串,对歌词还做了替换处理,<br> 替换为换行符,这是兼顾双语歌词用的,花潮格式的歌词数组约定,双语歌词用<br>标签隔开双语歌词,而这里使用文本性质赋值给lrc标签,所以有必要转换一下(歌词中没有<br>标签不影响赋值语句的执行)。

函数的第 ②、③、④ 行,用 setProperty 改变几个针对lrc元素和papa元素的CSS变量;① --ani 动画名称,它依据 aniIdx 变量值从数组 ['lrcGo0','lrcGo1'] 获得应执行的动画名;② --duration 歌词动画运行时长,它依据函数的唯一传参(传递过来的参数)time 赋值,time 值则是在 audio 控件的 timeupdate 监听事件中调用本函数时传来的,CSS事件变量要加上单位 s 即秒;③ 前两个针对lrc元素,最后一个针对papa元素,--state 关键帧动画运行状态,这里赋的值是 running —— 要模拟lrc歌词,动画自然要运行。

第 ⑤ 行,处理 aniIdx 变量,该变量是变换动画名称的依据,两次间的运行不同相同,但它的值非0即1,所以用一句三元运算简洁地解决问题:当前 aniIdx 若等于 0,则它现在等于 1,否则它现在等于 0,如此就能保证两句之间运行的动画不会重复,CSS动画的反复调用就没有障碍,lrc歌词模拟才可能实现。

最后,第 ⑥ 行,lrcKey 自增,数值变量名后面来个 ++ 等同于 数值 = 数值 + 1,用 ++ 显得更简洁。这一句至关重要,是歌词序列往后推进所在,也是 timeupdate 监听事件正常工作的依托:for循环语句里,先比对时间,时间相符后再比对当前的 lrcKey 值是否也相符,如果也相符就执行本函数,本函数完成相应工作后,令 lrcKey 加 1 ,timeupdate 监听事件在下一句歌词开唱前for循环中就不会重复比对 j 和 lrcKey 的值(重复比对就不能推进歌词序列,就像原地踏步走一样)。

根据 lrcKey 变量和以上 showLrc(time) 函数,timeupdate监听事件此时需要加入另一个条件判断语句,currentTime 和 歌词起唱时间匹配后,将循环步进 j 和 lrcKey 变量进行比对,下面是前述 timeupdate 代码样板的改进:

/* audio timeupdate 监听事件 */
aud.addListener('timeupdate', () => {
	/* ... 这里是进度驱动等代码 */
	for (let j = 0; j < geci.length; j ++) {
		if (aud.currentTime >= geci[j][0]) {  /* 第一个 if */
			if (j === lrcKey) showLrc(geci[j][0]); /* 第二个 if */
		}
	}
});

第一个 if 语句是第一个前提,如果播放器的currentTime和歌词的起唱时间信息比对成功,则运行嵌套在里面的第二个 if 语句,第二个 if 语句拿 j 和 lrcKey 比对,若等于就运行 showLrc(time) 函数,一旦运行 lrcKey 就变为了 lrcKey + 1,这样,当句正在播放时的余下 for 循环 j 和 lrcKey 匹配不上,就不会再操作歌词显示机制,直到第一个前提再次成立、要播放唱下一句歌词,如此反复。

lrck加1的机制解决了新的问题,同时也创造一个新的问题:一路加下去的话 lrcKey 值会大于歌词数组总数,所以我们要处理它。实际上,lrcKey 是否大于歌词总数我们都要处理的,考虑一下:我们的播放器是可以手动调整进度的,当用户调整了进度,歌词序号总得重新计算。进度调整会触发 audio 的 onseeked 事件,包括循环播放机制的重新播放也会触发此事件,所以本节,只需在 seeked 监听事件中令 lrcKey 等于 0 即可。

下面,是时候将 range 播放器代码的时机了,以下代码,修改过的或新的代码会用红色标记,和歌词相关的地方会有简要的注释说明:

<!--***** CSS代码 *****-->
<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; font: bold 2.4em sans-serif; color: lightblue; text-shadow: 1px 1px 1px rgba(0,0,0,.45); }
	#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: 20px; 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>

<!--***** HTML代码 *****-->
<div id="papa">
	<audio id="aud" src="https://music.163.com/song/media/outer/url?id=212524" autoplay loop></audio>
	<div id="lrc" data-lrc="HuaChao LRC">HuaChao LRC</div>
	<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>

<!--***** JavaScript代码 *****-->
<script>

/* 增加两个全局变量 : aniIdx - 动画名称索引;lrcKey : 当前唱到的lrc歌词序号 */
var mseek = false, aniIdx = 0, lrcKey = 0;

/* 按钮、歌词等状态函数 :父元素 papa 掌控 --state 变量,控制歌词同步与按钮 */
var mState = () => aud.paused ?
	( papa.style.setProperty('--state', 'paused'), btnplay.title = '点击播放' ) :
	( papa.style.setProperty('--state', 'running'), btnplay.title = '点击暂停' );

/* 秒数变分秒函数 */
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; };

/* 显示歌词函数 :time 参数为动画运行时长 */
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');
	papa.style.setProperty('--state', 'running');
	aniIdx = aniIdx === 0 ? 1 : 0;
	lrcKey ++;
};

/* 计算歌词索引函数 :用于 onseeked 监听事件,本节处理lrcKey归零问题,其余内容待补充 */
var calcKey = () => lrcKey = 0;

/* audio timeupdate 监听事件 */
aud.addEventListener('timeupdate', () => {
	if (!mseek) mprog.value = aud.currentTime / aud.duration * mprog.max;
	mplayer.dataset.tt = toMin(aud.currentTime) + ' ' + toMin(aud.duration);
	/* 循环歌词 */
	for (let j = 0; j < geci.length; j ++) {
		if (aud.currentTime >= geci[j][0]) { /* 比对当前播放位置时间和歌词起唱时间 : 若成立 */
			if (j === lrcKey) showLrc(geci[j][2]); /* 再比对上述条件成立时的 j 和 lrcKey,成立则运行函数 show(time)驱动新歌词运行 */
		}
	}
});

aud.addEventListener('pause', () => mState()); /* 监听onpause事件 */
aud.addEventListener('playing', () => mState()); /* 监听playing事件 */
aud.addEventListener('seeked', () => calcKey()); /* 监听 onseeked 时间,重新播放时运行 calcKey() 函数重新计算 lrcKey(这里归零) */

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>

以上代码还不是range+lrc播放器的终结版,但已经可以正常运行,可以到 pencil code 运行,或存为本地html文档后运行以查看效果。

本节理解起来有一定的难度,用心领会,做到全理解,将有助于提升解决细节问题的能力乃至编程思想,做不到全理解也没关系,学会将已经业已编写好的函数用到自己的程序中也很不错,能根据自己的需要修改一下函数则更好。下一讲,我们将完善 calcKey 函数,以便手动调整音频播放进度时lrc歌词同步能更好地工作。

前一篇: range进度条播放器+LRC歌词同步教程(二)
下一篇: range进度条播放器+LRC歌词同步教程(四)

发表评论:

  
 

评论列表 [2条]

#2 | 悄然 于 2024-2-3 12:00 发布: 仔细看了一遍~~看到黑将军指挥着千军万马,运筹帷幄果断英明。小白幼儿园刚毕业,JS论文看不懂。。溜了溜了。。

#1 | 悄然 于 2024-2-3 10:01 发布: (三)是豪华篇,有点晕乎,需得焚香、正冠、端坐,待心静、意诚之后方可开始细读。。^_^

Copyright © 2023 All Right Reserved 马黑PHP文章管理整站系统v1.8
联系我们: gxblk@163.com