网易云歌词数据解析及滚动动画

——> 演示地址 <——

使用网易云账户登录,点击进入歌单,双击选择音乐播放,点击下方音乐图标进入歌词页面

技术依附

网易云音乐 NodeJS 版 API - GitHub

附属文档

数据解析

获得的歌词数据示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
// 原始歌词
"lrc": {
"version": 7,
"lyric": "[00:16.90]Miku Miku, you can call me Miku\n[00:20.79]Blue hair, blue tie, hiding in your wi-fi\n[00:24.47]Open Secrets, anyone can find me\n[00:28.10]Hear your music running through my mind\n[00:31.24]I'm thinking Miku Miku oo ee oo\n[00:35.0]I'm thinking Miku Miku oo ee oo\n[00:38.63]I'm thinking Miku Miku oo ee oo\n[00:42.39]I'm thinking Miku Miku oo ee oo\n[00:46.99]I'm on top of the world because of you\n[00:50.59]All I wanted to do is follow you\n[00:54.36]I'll keep singing along to all of you\n[00:58.12]I'll keep singing along\n[01:01.25]I'm thinking Miku Miku oo ee oo\n[01:04.96]I'm thinking Miku Miku oo ee oo\n[01:08.65]I'm thinking Miku Miku oo ee oo\n[01:12.49]I'm thinking Miku Miku oo ee oo\n[01:16.66]Miku Miku, what's it like to be you?\n[01:20.64]Twenty, twenty, looking in the rear-view\n[01:24.45]Play me break me, make me feel like superman\n[01:28.71]You can do anything you want\n[01:32.5]\n[01:46.89]I'm on top of the world because of you\n[01:50.78]All I wanted to do is follow you\n[01:54.25]I'll keep singing along to all of you\n[01:58.2]I'll keep singing along\n[02:01.99]I'm on top of the world because of you\n[02:05.70]I do nothing that they could never do\n[02:09.46]I'll keep playing along to all of you\n[02:13.14]I'll keep playing along\n[02:16.20]I'm thinking Miku Miku oo ee oo\n[02:19.83]I'm thinking Miku Miku oo ee oo\n[02:23.59]I'm thinking Miku Miku oo ee oo\n[02:27.51]I'm thinking Miku Miku oo ee oo\n[02:32.3]\n[02:46.73]Where were we walking together\n[02:48.61]I will see you in the end\n[02:50.36]I'll take you to where you've never been\n[02:52.38]And bring you back again\n[02:54.33]Listen to me with your eyes\n[02:56.6]I'm watching you from the sky\n[02:57.94]If you forget I'll fade away\n[02:59.77]I'm asking you to let me stay\n[03:01.78]So bathe me in your magic light\n[03:03.45]And keep it on in darkest night\n[03:05.41]I need you here to keep me strong\n[03:07.21]To live my life and sing along\n[03:09.2]I'm waiting with you wide awake\n[03:10.90]Like your expensive poison snake\n[03:12.78]You found me here inside a dream\n[03:14.74]Walk through the fire straight to me\n"
},
// 音译歌词
"klyric": {
"version": 0,
"lyric": ""
},
// 翻译歌词
"tlyric": {
"version": 4,
"lyric": "[by:渲丶染]\n[00:16.90]Miku Miku 你可以叫我Miku\n[00:20.79]蓝色的头发 蓝色的领带 藏在你的wifi里\n[00:24.47]公开这个秘密 任何人都可以发现我\n[00:28.10]你的音乐在我的脑海里回荡\n[00:31.24]Miku我在思念你(oo-ee-oo)\n[00:35.0]Miku我在思念你(oo-ee-oo)\n[00:38.63]Miku我在思念你(oo-ee-oo)\n[00:42.39]Miku我在思念你(oo-ee-oo)\n[00:46.99]为了你 我站在这个世界顶端\n[00:50.59]我所做的一切是为了跟随你\n[00:54.36]我会一直唱给你听\n[00:58.12]我会一直歌唱\n[01:01.25]Miku我在思念你(oo-ee-oo)\n[01:04.96]Miku我在思念你(oo-ee-oo)\n[01:08.65]Miku我在思念你(oo-ee-oo)\n[01:12.49]Miku我在思念你(oo-ee-oo)\n[01:16.66]Miku你究竟像什么呢\n[01:20.64]来看看20年前的我们\n[01:24.45]陪你玩陪你疯 让你觉得就像个超人\n[01:28.71]你可以做到任何你想做的事情\n[01:46.89]为了你 我站在这个世界顶端\n[01:50.78]我所做的一切是为了跟随你\n[01:54.25]我会一直唱给你听\n[01:58.2]我会一直歌唱\n[02:01.99]为了你 我站在这个世界顶端\n[02:05.70]我不会做任何他们不让做的事\n[02:09.46]我会一直陪在你左右\n[02:13.14]我会一直跟随你\n[02:16.20]Miku我在思念你(oo-ee-oo)\n[02:19.83]Miku我在思念你(oo-ee-oo)\n[02:23.59]Miku我在思念你(oo-ee-oo)\n[02:27.51]Miku我在思念你(oo-ee-oo)\n[02:46.73]我们现在要去哪儿\n[02:48.61]我会一直看着你的\n[02:50.36]我会把你带到你从未去过的地方\n[02:52.38]再将你带回来\n[02:54.33]看着我 听我说\n[02:56.6]我会一直在天空看着你\n[02:57.94]但如果你忘记我 我会离开你\n[02:59.77]你会让我留在你身边吗\n[03:01.78]让我沐浴在你的魔法光芒下\n[03:03.45]即使黑夜也保持着\n[03:05.41]你在我身边我就会坚强起来\n[03:07.21]然后和你一起歌唱 生活下去\n[03:09.2]我等待着你接受我\n[03:10.90]就像我中了你的毒药\n[03:12.78]在我的梦中你找到了我\n[03:14.74]穿过火海来见我\n"
}
}

当然,音译不在我们考虑范围内

原始歌词

为了便于使用,我们呢可以对每一句歌词进行封装,封装这句歌词的事件,原文和翻译

1
2
3
4
5
6
7
class LyricItem {
constructor(time, text = "", fy = "") {
this.time = time;
this.text = text;
this.fy = fy;
}
}

同样我们将一首歌所有的歌词进行封装

1
2
3
4
5
6
class Lyric {
constructor() {
this.lyric = [];
this.haveFy = false;
}
}

来看原始的歌词数据

首先用\n 分割每一句歌词

再通过 ] 分割时间与歌词、: 分割分秒(将time转换为audio标签的current Time格式-浮点的秒)

1
2
3
4
5
6
7
8
9
10
11
12
13
let txt_arr = text.split("\n");
let fy_arr = fy.split("\n")

for (let i = 0; i < txt_arr.length; i++) {
let temp_row = txt_arr[i];
let temp_arr = temp_row.split("]");
let text = temp_arr.pop();
temp_arr.forEach(element => {
let time_arr = element.slice(1, element.length).split(":");
let time = parseInt(time_arr[0]) * 60 + parseFloat(time_arr[1]);
this.lyric.push(new LyricItem(time, text));
});
}

翻译歌词

与原始歌词不同的是,首行会有注解

可以直接排除第一行

这里采用的是正则表达式判断歌词格式

处理成类似的格式,加入到时间相同的原始歌词对象中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
for (let i = 0; i < fy_arr.length; i++) {
if (!/^\[[0-9]{2}:[0-9]{1,}\.[0-9]{1,}\]/.test(fy_arr[i]))
continue;
let temp_row = fy_arr[i];
let temp_arr = temp_row.split("]");
let text = temp_arr.pop();
temp_arr.forEach(element => {
let time_arr = element.slice(1, element.length).split(":");
let time = parseInt(time_arr[0]) * 60 + parseFloat(time_arr[1]);
for(let j = 0; j < this.lyric.length; j++) {
if(this.lyric[j].time == time)
this.lyric[j].fy = text
}
});
}

为了便于使用,还在歌词类中加入了获取当前歌词的方法

1
2
3
4
5
6
7
getIndex(time) {
for (let i = 0; i < this.lyric.length; i++) {
if (time < this.lyric[i].time)
return i - 1
}
return this.lyric.length - 1;
}

为此解析完是歌词数据后记得按时间排序!

滚动动画

将每句歌词全部加载入 div 标签,例如 vue : v-for

总共分成了五个 css

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
:root {
--lyricFyBoxHeight: 120px;
--lyricNofyBoxHeight: 80px;

--lyricInitFontSize: 18px;
--lyricNormalFontSize: 16px;
}

/* 共同 */
.home_footbar_detial_content > .lyric .lyricBoth {
text-align: center;
animation-duration: 0.4s;
animation-fill-mode: both;
}

/* 选中 */
.home_footbar_detial_content > .lyric .lyricInit {
animation-name: lyricInitAnm;
}

/* 普通 */
.home_footbar_detial_content > .lyric .lyricNormal {
animation-name: lyricOutitAnm;
}

/* 翻译 */
.home_footbar_detial_content > .lyric .lyricFY {
height: var(--lyricFyBoxHeight);
line-height: 35px;
}

/* 没翻译 */
.home_footbar_detial_content > .lyric .lyricNoFY {
height: var(--lyricNofyBoxHeight);
}

@keyframes lyricOutitAnm {
from {
font-size: var(--lyricInitFontSize);
color: #222;
}
to {
font-size: var(--lyricNormalFontSize);
color: #999;
}
}

@keyframes lyricInitAnm {
from {
font-size: var(--lyricNormalFontSize);
color: #999;
}
to {
font-size: var(--lyricInitFontSize);
color: #222;
}
}

通过歌曲播放的时间和有无翻译来判断当前歌词并且赋予相应类名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<div
v-for="(item, index) in lyric.lyric"
:key="index"
:class="{
lyricInit: nowIndex == index,
lyricNormal: nowIndex != index,
lyricBoth: true,
lyricFY: lyric.haveFy,
lyricNoFY: !lyric.haveFy,
}"
>
{{ item.text }}<br />
{{ item.fy }}
</div>

外套一个 div 盒子,通过 绝对-相对 组合定位方式控制歌词滚动

1
2
3
4
lyricTopPx() {
if (this.lyric.haveFy) return this.nowIndex * -120;
return this.nowIndex * -80;
},

记得加上动画缓冲!!!


THE END