歌词同步

前面的话

最近发现我们班同学做了一个很酷的Demo,这个Demo实现了一个很不错的歌词同步,着实令我兴奋,迫不及待的想看看其中是怎么实现的,因为其中的效果超过了某些音乐平台的歌词同步了,于是我找他要来了源码,今天我就打算为大家分享这一个Demo,后面我会把源码附在结尾。

歌词同步的需求

要实现歌词同步的话,我们大概可以把他分成几个模块:

  1. 歌词与时间加载
  2. 歌词显示
  3. 歌曲播放时歌词的滚动
  4. 鼠标拖拽时歌词的滚动
  5. 自由滚动歌词

以上4点为比较核心的功能了,我们就围绕以上4点来进行讲解其中的实现:

歌词加载

歌词要加载就得有地方加载,所以我们先得定义好布局:

<head>
		<meta charset="UTF-8">
		<title>歌词同步</title>
		<style type="text/css">
			/*同步到的歌词的动画效果,灰变成白*/
			/* Chrome, Safari, Opera */
			@-webkit-keyframes currLrc {
				0% {font-size: 16px;}
				100% {color: #fff;font-size: 16px;}
			}
			/* Standard syntax */
			@keyframes currLrc {
				0% {font-size: 16px;}
				100% {color: #fff;font-size: 16px;}
			}
			/*当前歌词的上一句的动画效果,白变成灰*/
			/* Chrome, Safari, Opera */
			@-webkit-keyframes lastLrc {			
				0% {color: #fff;font-size: 12px;}
				100% {color: #989898;font-size: 12px;}
			}
			/* Standard syntax */
			@keyframes lastLrc {
				0% {color: #fff;font-size: 12px;}
				100% {color: #989898;font-size: 12px;}
			}

			/*歌词最大框样式,用于定位整个歌词范围*/
			#lrcModel {
				position: relative;
				width: 325px;
				height: 400px;
				margin: 0;
				padding: 0;
				background-color: rgb(33, 36, 41);
				box-shadow: -3px 8px 135px -40px inset black;
			}
			
			#lrcModel * {
				margin: 0;
				padding: 0;
			}
			
			/*歌词的默认样式*/
			#lrcModel li {
				overflow: hidden;
				padding-top: 5px;
				padding-bottom: 5px;
				text-align: center;
				color: #989898;
				font-size: 12px;
				list-style: none;
			}
			
			/*歌词的父容器,与lrcModel大小一致*/
			#lrcModel #lrcUlParent {
				display: inline-block;
				height: 100%;
				width: 100%;
				overflow: hidden;
			}
			
			/*不要修改这里大小,或者边距*/
			#lrcModel #lrcList {
				width: 100%;
			}
			
			/*隐藏的操作面板的样式,不要修改这里大小,或者边距*/
			#lrcModel #scrollDiv {
				vertical-align: top;
				display: inline-block;
				width: 100%;
				/*background-color: red;*/
				height: 100%;
				overflow-y: scroll;
				position: absolute;
				left: 0px;
				top: 0px;
				opacity: 0.001;		/*透明度不能为0,否则鼠标滚轮滚动没效果*/
			}
			/*鼠标经过歌词时鼠标指针变成指向形状*/
			#lrcModel #scrollDiv:hover {
				cursor: pointer;
			}

		</style>
	</head>

	<body>
		<!--音频控件-->
		<audio controls loop="loop" id="audio" style="width: 400px;">
			<source src="songs/痒.mp3" type="audio/mp3"></source>
		</audio>

		<!-- 歌词框,这里用于显示歌词的布局-->
		<div id="lrcModel">
			<div id="lrcUlParent">
				<ul id="lrcList">
					<!--歌词放在这里-->
				</ul>
			</div>
			<!--滚动或者拖拽的操作层,铺在ul之上-->
			<div id="scrollDiv">
				<!--用于滚动的盒子-->
				<div id="openTheBox">
					<!--
					歌词填充完之后把这里的高度设置成和歌词ul相同的高度,
					用来撑开用来滚动的盒子
					-->
				</div>
			</div>
		</div>
	</body>

有了以上布局我们就可以看到如下效果:
在这里插入图片描述
可以看到中间的黑色方框就是用来存放歌词的,接下来我们来看看如何把歌词加载到手的:
我们的歌词通常都是lrc格式的,我列出其中一部分:

				[ti:我承认我自卑]
				[ar:杨小壮]
				[al:我承认我自卑]
				[by:jiting_karakal]
				[offset:0]
				[00:00.00]我承认我自卑 (DJ) - 杨小壮
				[00:00.31]词:杨小壮
				[00:00.62]曲:杨小壮
				[00:00.93]编曲:张川
				[00:01.25]混音:豆豆龙
				[00:01.56]制作人:杨栋梁

这里我们可以看到每一句歌词都可以对应上一个时间点,而且可以发现时间都是由方括号括起来的,这样就为我们转换为json格式提供了便利,转换成json格式后的部分如下:

				"[ti:我承认我自卑]": "",
				"[ar:杨小壮]": "",
				"[al:我承认我自卑]": "",
				"[by:jiting_karakal]": "",
				"[offset:0]": "",
				"[00:00.00]": "我承认我自卑 (DJ版) - 杨小壮",
				"[00:00.31]": "词:杨小壮",
				"[00:00.62]": "曲:杨小壮",
				"[00:00.93]": "编曲:张川",
				"[00:01.25]": "混音:豆豆龙",
				"[00:01.56]": "制作人:杨栋梁",

最后在数组中的样子是这样的:

var lrcJson = {
				"[ti:我承认我自卑]": "",
				"[ar:杨小壮]": "",
				"[al:我承认我自卑]": "",
				"[by:jiting_karakal]": "",
				"[offset:0]": "",
				"[00:00.00]": "我承认我自卑 (DJ版) - 杨小壮",
				"[00:00.31]": "词:杨小壮",
				"[00:00.62]": "曲:杨小壮",
				"[00:00.93]": "编曲:张川",
				"[00:01.25]": "混音:豆豆龙",
				"[00:01.56]": "制作人:杨栋梁",
				"[00:01.87]": "DJ:DJ何鹏",
				"[00:02.19]": "工作室:创意音工坊",
				"[00:02.51]": "我承认我自卑我真的很怕黑",
				"[00:05.69]": "",
				"[00:06.32]": "每当黑夜来临时候我总是很狼狈",
				"[00:09.27]": "",
				"[00:10.71]": "我彻夜在买醉但我不曾后悔",
				"[00:13.37]": "",
				"[00:14.32]": "只想让自己清楚为什么我会掉眼泪",
				"[00:19.06]": "",
				"[00:20.68]": "一个人的房间",
				"[00:22.05]": "",
				"[00:22.74]": "漆黑的夜晚",
				"[00:24.46]": "",
				"[00:25.35]": "看着你的故事和你的朋友圈",
				"[00:29.32]": "",
				"[00:30.07]": "谁会喜欢孤单",
				"[00:31.74]": "",
				"[00:32.34]": "是不想人看穿",
				"[00:34.10]": "",
				"[00:34.78]": "最后一点点温暖也被你打散",
				"[00:38.85]": "",
				"[00:39.55]": "孤独的人晚上",
				"[00:41.29]": "",
				"[00:41.90]": "最害怕有灯光",
				"[00:43.68]": "",
				"[00:44.31]": "你关了那灯光心里有一点忧伤",
				"[00:48.55]": "",
				"[00:49.14]": "你流泪的眼眶",
				"[00:50.89]": "",
				"[00:51.51]": "他走后的模样",
				"[00:53.29]": "",
				"[00:53.96]": "深深刺痛的心告诉自己要坚强",
				"[00:58.02]": "",
				"[01:01.27]": "我承认我自卑我真的很怕黑",
				"[01:05.27]": "",
				"[01:05.84]": "黑夜来临时候我总是很狼狈",
				"[01:10.06]": "",
				"[01:10.69]": "我彻夜在买醉但我不曾后悔",
				"[01:14.93]": "",
				"[01:15.49]": "只想让自己清楚为什么掉眼泪",
				"[01:19.65]": "",
				"[01:20.26]": "这孤单的滋味我慢慢的体会",
				"[01:24.44]": "",
				"[01:25.15]": "时间会让我遇见我心中的玫瑰",
				"[01:29.25]": "",
				"[01:29.90]": "要等到时间对等大雁向南飞",
				"[01:34.14]": "",
				"[01:34.67]": "我暖着我的玫瑰不让她枯萎",
				"[01:38.58]": "",
				"[01:58.94]": "孤独的人晚上",
				"[02:00.43]": "",
				"[02:01.06]": "最害怕有灯光",
				"[02:02.88]": "",
				"[02:03.47]": "你关了那灯光心里却有一点忧伤",
				"[02:08.28]": "你流泪的眼眶",
				"[02:10.08]": "",
				"[02:10.68]": "他走后的模样",
				"[02:12.45]": "",
				"[02:13.06]": "深深刺痛的心告诉自己要坚强",
				"[02:17.26]": "",
				"[02:17.92]": "我承认我自卑我真的很怕黑",
				"[02:22.08]": "",
				"[02:22.68]": "黑夜来临的时候我总是很狼狈",
				"[02:26.83]": "",
				"[02:27.49]": "我彻夜在买醉但我不曾后悔",
				"[02:31.66]": "",
				"[02:32.27]": "只想让自己清楚为什么掉眼泪",
				"[02:36.43]": "",
				"[02:37.05]": "这孤单的滋味我慢慢的体会",
				"[02:41.28]": "",
				"[02:41.86]": "时间会让我遇见我心中的玫瑰",
				"[02:46.03]": "",
				"[02:46.65]": "要等到时间对等大雁向南飞",
				"[02:50.86]": "",
				"[02:51.48]": "我暖着我的玫瑰不让她枯萎",
				"[02:55.65]": "",
				"[02:56.41]": "要等到时间对等大雁向南飞",
				"[03:00.53]": "",
				"[03:01.07]": "我暖着我的玫瑰不让她枯萎"
			};

我们可以这样取出歌词:lrcJson["[00:00.00]"]得到的结果为:我承认我自卑 (DJ版) - 杨小壮
只要我们把歌词数据存到了数组中,后面将歌词呈现出来就有规律可循了。
歌词加载完成了,现在还剩下时间了,时间我们用数组存起来,如何存合理呢,我们来看看下面的处理过程:数组的键分为时分秒,由于从audio控件获取的当前时间为纯秒数,所以我们要将时分秒转格式转换为单纯的浮点数:
我们可以将分提取出来乘以60得到秒,再加上后面的秒就可以了:

parseFloat(分钟) * 60 + parseFloat()

歌词显示

在网页中我们可以将每一句歌词显示在每一个li中,处理起来也会方便很多,我们可以把这个操作与前面的歌词和时间初始化写成方法:

function resetLyrics(lrcJSON) {
	// 先清空所有歌词列
	$ul.html(""); 
	var i = 0;
	//遍历lrcJSON数组的每一个key(时间):value(歌词)
	$.each(lrcJSON, function(key, value) { 
		// 将没有歌词的键值对过滤掉,只有带歌词和时间的键值对才有用
		if(value.trim().length > 0) { 
			//截取分*60+截取秒转换成秒格式,可以通过在后面加或减来设置同步延迟
			lrcTime[i++] = parseFloat(key.substr(1, 3)) * 60 + parseFloat(key.substring(4, 10)) - 0.1;
			//往ul里填充歌词
			$ul.append($("<li>" + value + "</li>")); 
		}
	});
	//由于当前一句歌词要靠下一句歌词的播放时间计算,所以要让最后一句歌词同步的话必须要在最后一句歌词时间数组后面加上一个时间
	lrcTime[lrcTime.length] = lrcTime[lrcTime.length - 1] + 3;
	//获取所有li
	$li = $("#lrcModel li"); 
	// 获取单个歌词节点的高度,获取的值会比真实值大0.2,所以-0.2
	liHeight = $li.outerHeight() - 0.2; 
	// 歌词加载完毕之后撑开右边用于滚动的框
	$("#lrcModel #openTheBox").height($ul.height()); 
	// 计算能滚动的最大位置  +10是歌词最下方留的空隙
	maxTop = ($ul.height() - $ulParent.height())+10; 
	currentLine = 0;//将当前播放的歌词行数设为0
	// 刷新歌词时刷新 ul的位置
	$ul.css({"transform": "translateY(0px)","transition-duration": "500ms"});
}

这样通过这个方法可以把歌词显示在歌词框中了:
在这里插入图片描述
看歌词显示出来了。

歌曲播放时歌词的滚动

歌词显示出来了,但是歌曲播放时歌词并不会随之滚动和改变相应样式大小,想要让歌词完美的滚动起来,要从多个角度和因素去思考:
1.所有歌词行加起来的高度与歌词框高度的关系
2. 如何判断播放器时间对应上每句歌词的时间使其同步
3. 歌词的滚动细节计算

计算歌词滚动范围

这要谈到第一点了,要计算所有歌词行加起来的高度与歌词框高度的关系,正常情况下歌词的总高度肯定要比歌词框,所以我们将歌词总长度减去歌词框高度,就可以得到歌词允许滚动的范围:即对应了最后一句歌词刚好在歌词框的底部,比如歌词总高度为1200px,歌词框高度400px,1200 - 400 = 800px
那么这个800px就是歌词第一句歌词所在的高度,但是这个高度为 -800px,也可以看出来-800px的高度刚刚好对应了最后一句歌词刚好在歌词框的底部。
在得到了这个之后,我们就可以很直观的容易的控制歌词向上滚动,我们只要拿着这个800px减去一行歌词或者多行歌词的高度,就可以控制控制歌词的上下位置了。

如何同步

歌词不会自己跑,还是需要通过歌曲播放的进度的改变而改变,这就要想方设法让歌词的滚动与播放器进度同步,播放器提供了ontimeupdate事件,这个事件会在播放时的每一刻触发,我们可以利用这个事件触发让每一句歌词完美无缝的对应上每一句音频。
如何判断呢,我们之前有把每一句歌词对应的时间点存储在数组中,那么我们可以获取播放器每一时刻的秒数,来对比数组中的时间,检查是否可以同步到当前歌词。

歌词的滚动细节计算

先来看看歌词滚动的核心代码:

audioNode.ontimeupdate = function() {
	// 该循环的主要作用是控制当前歌词行号和歌词的播放时间
	for(var j = currentLine; j < lrcTime.length; j++) {
		// 若当前歌词时间小于下句歌词时间,就说明还没播放到下一句歌词
		if(audioNode.currentTime < lrcTime[j + 1]) {
			// 改变播放到的行数
			currentLine = j; 
			// 判断是否为同一行,如果不是那就允许同步
			if(LastLine !== currentLine) { 
				//获得当前歌词以及当前歌词之前的所有歌词的高度之和
				var currHeight = currentLine * liHeight; 
				//为了解决歌词样式变化时间>单句歌词持续时间而做的判断
				var time = (lrcTime[currentLine + 1] - lrcTime[currentLine]) > (changeTime / 1000) ? changeTime : (lrcTime[j + 1] - lrcTime[j]) * 1000; 	  	
				// 将当前播放同步的歌词高亮
				$($li[currentLine]).css("animation", "currLrc " + ((time) / 1000) + "s linear 0s 1 alternate forwards");
				// 将上一句歌词还原
				$($li[currentLine - 1]).css("animation", "lastLrc " + ((time) / 1000) + "s linear 0s 1 alternate forwards"); 
				// 当前播放歌词的总高度减去歌词框高度的一半
				top = (currHeight- scrollDivHeight * LrcPosition);
				//小于0说明播放过的歌词还未过半,不滚动
				if(top < 0) {	
					//让歌词呆在原地先不动			
					top = 0;
					//如果大于了最大允许滚动范围,则加以限制
				} else if(top > maxTop) {
					//防止播放完毕时继续滚动
					top = maxTop;
				}
				//这个是为了在用户滚动之后的三秒内,歌词自己不动
				if(mousewheelFlag) {
					//改变滚动条高度为当前播放位置
					$scrollDiv[0].scrollTop = top;
					// 改变 ul 的上下位置,滚动的核心代码
					$ul.css({
						"transform": "translateY(-" + (top) + "px)",
						"transition-duration": "" + (time) + "ms"
					});
				}
				//记录当前播放行数
				LastLine = currentLine;
			}
			break;
		}
	}
}

上面方法中的循环尤为重要,但并不是想象中的那样循环,他的作用主要有两个,一个是控制当前播放行数,一个是控制当前播放时间,每次执行这个循环的循环次数不超过两次。
这个循环的主要功劳代码如下:

var j = currentLine; j < lrcTime.length; j++ //第3行
currentLine = j; //第7行
LastLine = currentLine; //第40行
break; //第42行

鼠标拖拽时歌词的滚动

有时自动播放太慢,用户想要拖动进度条到精彩的地方听时就会发生问题,歌词不会实时同步进度条,那么我们可以用类似于上面自动同步的逻辑来编写下面的拖拽时歌词的滚动:

audioNode.onseeked = function() {
	//重置所有歌词li的样式
	$li.each(function() {
		$(this)[0].removeAttribute("style")
	}); 
	var Top2;
	// 从0 开始遍历时间数组
	for(var k = 0; k < lrcTime.length; k++) {
		// 如果当前的播放时间小于上一句并且大于下一句句,那么K就是当前行
		if(audioNode.currentTime > lrcTime[k - 1] && audioNode.currentTime < lrcTime[k + 1]) {
			currentLine = k;
			Top2 = currentLine * liHeight - scrollDivHeight + (scrollDivHeight / 2); // 计算这个值是为了歌曲播放进度被改变的时候立即调整 歌词ul 的上下位置
			if(Top2 > maxTop) { // 不能超过能修改的最大位置
				Top2 = maxTop;
			}
			$($li[currentLine - 1]).css("animation", "currLrc 0s linear 0s 1 alternate forwards"); // 当前句的样式
			$ul.css({
				"transform": "translateY(-" + Top2 + "px)",
				"transition-duration": "0ms"
			});
			LastCurrentLine = currentLine - 1;
			return;
		}
	}
	// 如果改变播放进度的时候 所有行都没有匹配到,那么就是走到最后一行了
	$($li[lrcTime.length - 2]).css("animation", "currLrc 0s ease-in-out 0s 1 alternate forwards"); // 最后一句的样式 -2是因为歌词时间数组的最后一个值是额外添加的,没有对应的歌词节点
	$ul.css({
		"transform": "translateY(-" + maxTop + "px)",
		"transition-duration": "0ms"
	}); // 修改 歌词ul的位置
	$scrollDiv[0].scrollTop = maxTop; // 修改滚动条的位置
	LastCurrentLine = currentLine - 1;
}

不同的是循环和最后一句歌词的处理,都大同小异。

自由滚动歌词

其实有了以上的功能基本上一个简单的歌词同步就可以做好了,但是有一些场景还是需要处理的,比如用户想要在听歌的时候拖动歌词看看前面的歌词怎么办,这时我们要想办法把当前的自动同步停下来,让歌词随着用户的拖动而滚动,上面的同步例子中就有这么一行代码:

//用户滚动之后的三秒内,歌词自己不动
if(mousewheelFlag)

现在我们要做的就是在用户用鼠标滚轮滚动歌词时把mousewheelFlag设为false就可以了,现在我们先为鼠标滚轮写好事件监听:

function addMouseWheel(obj, fn, preventDefault) {
	//检查浏览器
	if(window.navigator.userAgent.toLowerCase().indexOf("firefox") != -1) {
		//火狐
		obj.addEventListener("DOMMouseScroll", fnWheel, false);
	} else {
		//其他
		obj.onmousewheel = fnWheel;
	}
	
	//处理方法
	function fnWheel(ev) {
		var oEvent = ev || event;//为了兼容
		var bDown = true; //下
		if(oEvent.wheelDelta) { //ie chrome为了兼容
			bDown = oEvent.wheelDelta > 0 ? false : true;//大于0向上否则向下
		} else { //ff
			bDown = oEvent.detail > 0 ? true : false;//大于0向下否则向上
		}
		(typeof fn == "function") && fn(bDown);//fn如果是function那就调用fn
		if(preventDefault) {//如果不为假
			oEvent.preventDefault && oEvent.preventDefault();//去除默认操作
			return false;
		}
	}
}

以上代码核心还是要调用fn方法达到监听效果:

/**
 *  添加鼠标滚轮 滚动事件监听
 */
addMouseWheel($scrollDiv[0], function() {
	//滚动就把mousewheelFlag设为false
	mousewheelFlag = false;
	//清除计时器
	clearTimeout(mousewheelTimeoutFlag);
	//添加计时器用于3秒后把mousewheelFlag设为true
	mousewheelTimeoutFlag = setTimeout(function() {
		mousewheelFlag = true;
	}, 3000);
});

然后就剩下要滚动时的效果了:

/**
 *  被滚动的时候修改 ul 的上下位置
 */
$scrollDiv.scroll(function() {
	//只有在触发滚轮滚动时才会执行
	if(!mousewheelFlag) {
		//滚动的距离按照滚动的scrollTop来控制
		$ul.css({
			"transform": "translateY(-" + (this.scrollTop) + "px)",
			"transition-duration": "0ms"
		});
	}
});

到此一个基本的歌词同步就完成了,看代码的时候一定要记得多调试,观察每一个变量的值,把握好每一个变量。
效果图:
在这里插入图片描述

源码

<!DOCTYPE html>
<html lang="en">

	<head>
		<meta charset="UTF-8">
		<title>歌词同步</title>
		<style type="text/css">
			/* Chrome, Safari, Opera */
			
			@-webkit-keyframes currLrc {
				0% {
					font-size: 16px;
				}
				100% {
					color: #fff;
					font-size: 16px;
				}
			}
			/* Standard syntax */
			
			@keyframes currLrc {
				0% {
					font-size: 16px;
				}
				100% {
					color: #fff;
					font-size: 16px;
				}
			}
			/* Chrome, Safari, Opera */
			
			@-webkit-keyframes lastLrc {
				0% {
					color: #fff;
					font-size: 12px;
				}
				100% {
					color: #989898;
					font-size: 12px;
				}
			}
			/* Standard syntax */
			
			@keyframes lastLrc {
				0% {
					color: #fff;
					font-size: 12px;
				}
				100% {
					color: #989898;
					font-size: 12px;
				}
			}
		/*#scrollDiv::-webkit-scrollbar {*/
			/*滚动条整体样式*/
			/*width: 6px;*/
			/*display: none;*/
			/*高宽分别对应横竖滚动条的尺寸*/
			/*height: 6px;*/
			/*border-radius: 5px;*/
		/*}*/
			/*滚动条里面小方块*/
		/*#scrollDiv::-webkit-scrollbar-thumb {
            border-radius: 5px;
            -webkit-box-shadow: inset 0 0 5px rgba(255, 255, 255, 0.2);
            background-color: black;
        }*/
			/*滚动条里面轨道*/
		/*#scrollDiv::-webkit-scrollbar-track {
            -webkit-box-shadow: inset 0 0 5px rgba(0, 0, 0, 1);
            border-radius: 3px;
            background-color: transparent;
        }*/
			/*歌词的样式*/
			
			#lrcModel li {
				overflow: hidden;
				padding-top: 5px;
				padding-bottom: 5px;
				text-align: center;
				color: #989898;
				font-size: 12px;
				list-style: none;
			}
			
			#lrcModel {
				position: relative;
				width: 325px;
				height: 400px;
				/*background-color: cornsilk;*/
				margin: 0;
				background-color: rgb(33, 36, 41);
				box-shadow: -3px 8px 135px -40px inset black;
			}
			
			#lrcModel * {
				margin: 0;
				padding: 0;
			}
			
			#lrcUlParent {
				display: inline-block;
				height: 100%;
				width: 100%;
				overflow: hidden;
			}
			
			#lrcList {
				width: 100%;
			}
			
			#scrollDiv {
				vertical-align: top;
				display: inline-block;
				background-color: brown;
				width: 100%;
				height: 100%;
				overflow-y: scroll;
				position: absolute;
				left: 0px;
				top: 0px;
				opacity: 0;
			}
			
			#openTheBox {
				width: 100%;
				background-color: coral;
			}
		</style>
	</head>

	<body>
		<audio controls id="audio" style="width: 400px;">
			<source src="songs/我承认我自卑 (DJ版).mp3" type="audio/mp3"></source>
		</audio>
		
		<!--歌词框-->

		<div id="lrcModel">
			<div id="lrcUlParent">
				<ul id="lrcList">
				</ul>
			</div>
			<div id="scrollDiv">
				<!--用于滚动的盒子-->
				<div id="openTheBox">
					<!--歌词填充完之后把这里的高度设置成和歌词ul 相同的高度,用来撑开用来滚动的盒子-->
				</div>
			</div>
		</div>

	</body>

	<script src="js/jquery-1.12.4.js"></script>
	<script type="text/javascript">
		$(function() {
			var audioNode = document.getElementById("audio");

			// getLrcJson("lrcs/我承认我自卑 (DJ版).lrc");

			/**
			 *   歌词对应的时间数组,这是核心变量  
			 *   歌词就是通过  lrcTime[currentLine]   和   $li.get(currentLine)来关联的
			 */
			var lrcTime = [];
			var changeTime = 1100; // 歌词样式变化的动画时间,在这里设置就好,单位是毫秒
			var $ul = $("#lrcList"); //获取ul
			var $li; //保存所有歌词列
			var currentLine; //当前播放到哪一句了,
			var LastCurrentLine; //记录上一句,
			var mousewheelFlag = true; // 用户滚动歌词此变量会变成false , 三秒后恢复
			var $scrollDiv = $("#scrollDiv"); // 和歌词 ul 的父节点高度相等的节点,歌词加载完之后,这个节点里面的一个节点会被设置成和歌词ul高度相等,用于撑开此节点,产生滚动条
			var scrollDivHeight = $scrollDiv.height();
			var liHeight; //单句歌词的高度
			var top; // 每次滚动要设置的高度
			var $ulParent = $("#lrcUlParent"); // 歌词ul 的父节点
			var maxTop; // 能设置的最大滚动条的高度以及歌词ul的最大下移动的值

			var lrcJson = {
				"[ti:我承认我自卑]": "",
				"[ar:杨小壮]": "",
				"[al:我承认我自卑]": "",
				"[by:jiting_karakal]": "",
				"[offset:0]": "",
				"[00:00.00]": "我承认我自卑 (DJ版) - 杨小壮",
				"[00:00.31]": "词:杨小壮",
				"[00:00.62]": "曲:杨小壮",
				"[00:00.93]": "编曲:张川",
				"[00:01.25]": "混音:豆豆龙",
				"[00:01.56]": "制作人:杨栋梁",
				"[00:01.87]": "DJ:DJ何鹏",
				"[00:02.19]": "工作室:创意音工坊",
				"[00:02.51]": "我承认我自卑我真的很怕黑",
				"[00:05.69]": "",
				"[00:06.32]": "每当黑夜来临时候我总是很狼狈",
				"[00:09.27]": "",
				"[00:10.71]": "我彻夜在买醉但我不曾后悔",
				"[00:13.37]": "",
				"[00:14.32]": "只想让自己清楚为什么我会掉眼泪",
				"[00:19.06]": "",
				"[00:20.68]": "一个人的房间",
				"[00:22.05]": "",
				"[00:22.74]": "漆黑的夜晚",
				"[00:24.46]": "",
				"[00:25.35]": "看着你的故事和你的朋友圈",
				"[00:29.32]": "",
				"[00:30.07]": "谁会喜欢孤单",
				"[00:31.74]": "",
				"[00:32.34]": "是不想人看穿",
				"[00:34.10]": "",
				"[00:34.78]": "最后一点点温暖也被你打散",
				"[00:38.85]": "",
				"[00:39.55]": "孤独的人晚上",
				"[00:41.29]": "",
				"[00:41.90]": "最害怕有灯光",
				"[00:43.68]": "",
				"[00:44.31]": "你关了那灯光心里有一点忧伤",
				"[00:48.55]": "",
				"[00:49.14]": "你流泪的眼眶",
				"[00:50.89]": "",
				"[00:51.51]": "他走后的模样",
				"[00:53.29]": "",
				"[00:53.96]": "深深刺痛的心告诉自己要坚强",
				"[00:58.02]": "",
				"[01:01.27]": "我承认我自卑我真的很怕黑",
				"[01:05.27]": "",
				"[01:05.84]": "黑夜来临时候我总是很狼狈",
				"[01:10.06]": "",
				"[01:10.69]": "我彻夜在买醉但我不曾后悔",
				"[01:14.93]": "",
				"[01:15.49]": "只想让自己清楚为什么掉眼泪",
				"[01:19.65]": "",
				"[01:20.26]": "这孤单的滋味我慢慢的体会",
				"[01:24.44]": "",
				"[01:25.15]": "时间会让我遇见我心中的玫瑰",
				"[01:29.25]": "",
				"[01:29.90]": "要等到时间对等大雁向南飞",
				"[01:34.14]": "",
				"[01:34.67]": "我暖着我的玫瑰不让她枯萎",
				"[01:38.58]": "",
				"[01:58.94]": "孤独的人晚上",
				"[02:00.43]": "",
				"[02:01.06]": "最害怕有灯光",
				"[02:02.88]": "",
				"[02:03.47]": "你关了那灯光心里却有一点忧伤",
				"[02:08.28]": "你流泪的眼眶",
				"[02:10.08]": "",
				"[02:10.68]": "他走后的模样",
				"[02:12.45]": "",
				"[02:13.06]": "深深刺痛的心告诉自己要坚强",
				"[02:17.26]": "",
				"[02:17.92]": "我承认我自卑我真的很怕黑",
				"[02:22.08]": "",
				"[02:22.68]": "黑夜来临的时候我总是很狼狈",
				"[02:26.83]": "",
				"[02:27.49]": "我彻夜在买醉但我不曾后悔",
				"[02:31.66]": "",
				"[02:32.27]": "只想让自己清楚为什么掉眼泪",
				"[02:36.43]": "",
				"[02:37.05]": "这孤单的滋味我慢慢的体会",
				"[02:41.28]": "",
				"[02:41.86]": "时间会让我遇见我心中的玫瑰",
				"[02:46.03]": "",
				"[02:46.65]": "要等到时间对等大雁向南飞",
				"[02:50.86]": "",
				"[02:51.48]": "我暖着我的玫瑰不让她枯萎",
				"[02:55.65]": "",
				"[02:56.41]": "要等到时间对等大雁向南飞",
				"[03:00.53]": "",
				"[03:01.07]": "我暖着我的玫瑰不让她枯萎"
			};

			resetLyrics(lrcJson);

			/**
			 * 替换歌词
			 * @param {Object} lrcJSON 歌词json对象
			 */
			function resetLyrics(lrcJSON) {
				$ul.find("li").remove(); // 先清空所有歌词列
				var i = 0;
				$.each(lrcJSON, function(key, value) { //遍历lrc
					if(value.trim().length > 0) { // 如果歌词不为空的话。为什么要加这个判断?因为有些歌词文件的开头几句是没有歌词的,只有时间
						lrcTime[i++] = parseFloat(key.substr(1, 3)) * 60 + parseFloat(key.substring(4, 10)) - 0.1; //00:00.000转化为00.000格式 +0.1 是提前0.1秒改变样式,可以通过这个地方修改快一点或者慢一点
						$ul.append($("<li>" + value + "</li>")); //往ul里填充歌词
					}
				});
				lrcTime[lrcTime.length] = lrcTime[lrcTime.length - 1] + 3; //如不另加一个结束时间,到最后歌词滚动不到最后一句
				$li = $("#lrcModel li"); //获取所有li
				liHeight = $li.outerHeight() - 0.2; //获取单个歌词节点的高度,获取的值会比真实值大0.2,所以-0.2
				$("#openTheBox").height($ul.height()); //歌词加载完毕之后撑开右边用于滚动的框
				maxTop = $ul.height() - $ulParent.height(); //计算能滚动的最大位置  1200-400 = 800px,800代表歌词到底了,maxTop可以作为阈值
				currentLine = 0;
				LastCurrentLine = 1;
			}

			/**
			 * audio播放进度改变事件,这个事件在播放的时候会高频触发
			 * 歌曲播放时歌词同步就是靠的这个方法
			 */
			audioNode.ontimeupdate = function() {
				// 从当前行开始遍历时间数组
				for(var j = currentLine; j < lrcTime.length; j++) {
					// 如果当前的播放时间小于上一句并且大于当前句的时候就说明该搞事了,当前播放器时间还不到时间数组下一句歌词时间点与当前播放器时间超出时间数组当前歌词时间
//					console.log(audioNode.currentTime+","+lrcTime[j + 1]);
					if(audioNode.currentTime < lrcTime[j + 1] && audioNode.currentTime > lrcTime[j]) {
						console.log(j+","+lrcTime[j + 1])
						currentLine = j; // 改变播放到的行数
						if(LastCurrentLine !== currentLine) { // 由于此方法会被高频调用,此判断是为减少资源的耗费,如果当前行没有变化就不执行此作用域的代码    
							var currHeight = currentLine * liHeight; //当前行数 * 24.8(歌词li高度),记录播放过的歌词总高度
							var time = (lrcTime[currentLine + 1] - lrcTime[currentLine]) > (changeTime / 1000) ? changeTime : (lrcTime[j + 1] - lrcTime[j]) * 1000; //为了解决某句歌词节奏过快,导致一句歌词的时间短于歌词向上滑动的过度时间而导致的不同步问题	  	
							top = (currHeight - scrollDivHeight + (scrollDivHeight / 2)); // 计算本次歌词要向上滑动的距离
							$($li[currentLine]).css("animation", "currLrc " + ((time - 0.1) / 1000) + "s linear 0s 1 alternate forwards"); //当前句高亮
							$($li[currentLine - 1]).css("animation", "lastLrc " + ((time - 0.1) / 1000) + "s linear 0s 1 alternate forwards"); //上一句复原
							if(top < 0) {//小于0代表当前歌词的同步还没有达到需要滚动的条件
								top = 0;//由于歌词在向上滑动时要加上liHeight歌词的高度,所以这里要设为它的相反数,相加故为0px,即不滚动
							} else if(top > maxTop) {//歌词的滚动距离不得超过阈值
								top = maxTop;
							}
							if(mousewheelFlag) { // 用户滚动歌词时会触发滚动事件,这个事件会将mousewheelFlag设置为false,随后3后会将其设回true
								$scrollDiv[0].scrollTop = top;
								//保证歌词当前句在正中间
								$ul.css({
									"transform": "translateY(-" + (top) + "px)",
									"transition-duration": "" + time + "ms"
								});
							}
							LastCurrentLine = currentLine;
						}
						break;
					}
				}
			}

			var mousewheelTimeoutFlag; // 用户滚动标志位*/
			/**
			 *  被滚动的时候修改 ul 的上下位置
			 */
			$scrollDiv.scroll(function() {
				if(!mousewheelFlag) {
					$ul.css({
						"transform": "translateY(-" + (this.scrollTop) + "px)",
						"transition-duration": "0ms"
					});
				}
			});

			/**
			 *  添加鼠标滚轮 滚动事件监听
			 */
			addMouseWheel($scrollDiv[0], function() {
				mousewheelFlag = false;
				clearTimeout(mousewheelTimeoutFlag);
				mousewheelTimeoutFlag = setTimeout(function() {
					mousewheelFlag = true;
				}, 3000);
			});


			/**
			 *  audio播放进度被改变,比如通过代码设置其当前播放时间,或者拖拽播放进度条之后会触发的事件
			 *
			 *  用于播放进度被改变时歌词快速回滚到正确的位置
			 *
			 */
			audioNode.onseeked = function() {
				$li.each(function() {
					$(this)[0].removeAttribute("style")
				}); //重置所有歌词li的样式
				var Top2;
				// 从0 开始遍历时间数组
				for(var k = 0; k < lrcTime.length; k++) {
					// 如果当前的播放时间小于上一句并且大于下一句句,那么K就是当前行
					if(audioNode.currentTime > lrcTime[k - 1] && audioNode.currentTime < lrcTime[k + 1]) {
						currentLine = k;
						Top2 = currentLine * liHeight - scrollDivHeight + (scrollDivHeight / 2); // 计算这个值是为了歌曲播放进度被改变的时候立即调整 歌词ul 的上下位置
						if(Top2 > maxTop) { // 不能超过能修改的最大位置
							Top2 = maxTop;
						}
						$($li[currentLine - 1]).css("animation", "currLrc 0s linear 0s 1 alternate forwards"); // 当前句的样式
						$ul.css({
							"transform": "translateY(-" + Top2 + "px)",
							"transition-duration": "0ms"
						});
						LastCurrentLine = currentLine - 1;
						return;
					}
				}
				// 如果改变播放进度的时候 所有行都没有匹配到,那么就是走到最后一行了
				$($li[lrcTime.length - 2]).css("animation", "currLrc 0s ease-in-out 0s 1 alternate forwards"); // 最后一句的样式 -2是因为歌词时间数组的最后一个值是额外添加的,没有对应的歌词节点
				$ul.css({
					"transform": "translateY(-" + maxTop + "px)",
					"transition-duration": "0ms"
				}); // 修改 歌词ul的位置
				$scrollDiv[0].scrollTop = maxTop; // 修改滚动条的位置
				LastCurrentLine = currentLine - 1;
			};

			/**
			 * 添加鼠标滚轮滚动事件监听,写成方法只是为了兼容性,不是重点
			 * @param {Object} obj DOM 节点
			 * @param {Object} fn 回调函数
			 * @param {Object} preventDefault 默认参数
			 */
			function addMouseWheel(obj, fn, preventDefault) {
				//添加绑定
				if(window.navigator.userAgent.toLowerCase().indexOf("firefox") != -1) {
					obj.addEventListener("DOMMouseScroll", fnWheel, false);
				} else {
					obj.onmousewheel = fnWheel;
				}

				function fnWheel(ev) {
					var oEvent = ev || event;
					var bDown = true; //下
					if(oEvent.wheelDelta) { //ie chrome
						bDown = oEvent.wheelDelta > 0 ? false : true;
					} else { //ff
						bDown = oEvent.detail > 0 ? true : false;
					}
					(typeof fn == "function") && fn(bDown);
					if(preventDefault) {
						oEvent.preventDefault && oEvent.preventDefault();
						return false;
					}
				}
			}

		})
	</script>
</html>

最后再附上一个解析lrc文件的源码:

/**
 * 	获得歌词
 * @param path 歌词绝对路径
 * @return  拼接的JSON 格式歌词字符串
 */
public static	String  getLrc(String path){
		FileReader fs = null;
		BufferedReader br = null;
		StringBuffer sbf = new StringBuffer();
		try {
			fs = new FileReader(path);
			br = new BufferedReader(fs);
			int i = 0;
			String  sb = "";
		sbf.append("{");
		while((sb = br.readLine()) != null){
			i++;
			if (i>=5) {
				int bb = sb.lastIndexOf("]");
				sbf.append("\""+sb.substring(0,bb+1)+"\""+" : "+"\""+sb.substring(bb+1)+"\""+",");
			}
		}
		sbf.delete(sbf.length()-1,sbf.length());
		sbf.append("}");
		System.out.println(sbf);
	} catch (FileNotFoundException e) {
		e.printStackTrace();
	} catch (IOException e) {
		e.printStackTrace();
	}finally{
		if (br != null) {
			try {
				br.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		if (fs != null) {
			try {
				fs.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
	return sbf.toString();
}

最后希望大家都有开源的精神,每天都有新的收获。

  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值