js解析歌词方法实践

导语:之前写过一个音乐播放小项目,感觉里面的歌词解析挺好玩的,现在就这个实现的方法做一个总结。

# 目录

  • 准备工作
  • 解析方法
  • 实战案例

# 准备工作

# 下载歌曲文件

首先准备三个文件,分别是歌曲xxx.mp3和歌词xxx.lrc以及封面图xxx.jpg,可以到相关的音乐软件下载一下的。

准备文件

# 简单页面布局

  • 页面目录
+ css
  + iconfont.css
  + index.css
+ js
  + axios.min.js
  + index.js
+ fonts
  + iconfont.eot
  + iconfont.js
  + iconfont.json
  + iconfont.svg
  + iconfont.ttf
  + iconfont.woff
  + iconfont.woff2
+ data
  + lrh-mq.mp3
  + lrh-mq.lrc
  + lrh-mq.jpg
+ index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  • 页面结构代码
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>js解析歌词案例</title>
    <link rel="stylesheet" type="text/css" href="css/iconfont.css">
    <link rel="stylesheet" type="text/css" href="css/index.css">
</head>
<body>
    <div class="music">
        <div class="loading-box active">
            <span></span>
        </div>
        <div class="music-title">小奇音乐盒</div>
        <div class="music-lyric">
            <ul class="lyric-ls scroll-box"></ul>
        </div>
        <div class="music-progress">
            <small id="startTime">00:00</small>
            <span id="music-progress-bar">
                <i></i>
            </span>
            <small id="endTime">00:00</small>
        </div>
        <div class="music-audio">
            <div class="music-audio-set">
                <i class="music-play qm qm-play"></i>
            </div>
            <audio id="music-player" src="data/lrh-mq.mp3"></audio>
        </div>
    </div>
    <script src="js/axios.min.js"></script>
    <script src="js/index.js"></script>
</body>
</html>
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
  • 页面样式
/* iconfont.css */
@font-face {
  font-family: "qm";
  /* Project id 2846512 */
  src: url('../fonts/iconfont.eot?t=1633074105096');
  /* IE9 */
  src: url('../fonts/iconfont.eot?t=1633074105096#iefix') format('embedded-opentype'),
    /* IE6-IE8 */
    url('../fonts/iconfont.woff2?t=1633074105096') format('woff2'),
    url('../fonts/iconfont.woff?t=1633074105096') format('woff'),
    url('../fonts/iconfont.ttf?t=1633074105096') format('truetype'),
    url('../fonts/iconfont.svg?t=1633074105096#qm') format('svg');
}

.qm {
  font-family: "qm" !important;
  font-size: 16px;
  font-style: normal;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

.qm-close:before {
  content: "\e66b";
}

.qm-list:before {
  content: "\e61e";
}

.qm-right:before {
  content: "\e68a";
}

.qm-left:before {
  content: "\ea87";
}

.qm-pause:before {
  content: "\e69e";
}

.qm-play:before {
  content: "\e701";
}

.qm-volume:before {
  content: "\e67a";
}

.qm-muted:before {
  content: "\e7cd";
}

.qm-sort:before {
  content: "\e610";
}

.qm-random:before {
  content: "\e60a";
}

.qm-lyric:before {
  content: "\e727";
}
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
58
59
60
61
62
63
64
65
/* index.css */
/*
 * @Author: fegq
 * @Date: 2021-10-01 20:53:48
 * @LastEditors: fegq
 * @LastEditTime: 2021-10-01 22:39:50
 * @Description: This is a file comment!
 * @Version: 0.0.1
 */

body,
ul,
li {
    margin: 0;
    padding: 0;
    border: none;
}

.scroll-box::-webkit-scrollbar {
    width: 10px;
    height: 8px;
}

.scroll-box::-webkit-scrollbar-thumb {
    border-radius: 20px;
    -webkit-box-shadow: inset 0 0 5px #d32d2d;
    box-shadow: inset 0 0 5px #d32d2d;
    background: #d32d2d;
}

.scroll-box::-webkit-scrollbar-track {
    -webkit-box-shadow: inset 0 0 5px white;
    box-shadow: inset 0 0 5px white;
    border-radius: 10px;
    background: white;
}

.music {
    margin: 20px auto;
    width: 100%;
    max-width: 400px;
    height: auto;
    border-radius: 8px;
    border: 1px solid #ccc;
    box-shadow: 0 0 15px #ccc;
    font-family: 'opensansBold';
    overflow: hidden;
}

.music-title {
    font-size: 20px;
    text-align: center;
    font-weight: bold;
    color: #fff;
    line-height: 3;
    background: #d32d2d;
}

.music-lyric {
    box-sizing: border-box;
    padding: 15px 20px;
    height: 390px;
    overflow: hidden;
}

.lyric-ls {
    height: 100%;
    font-size: 16px;
    line-height: 2;
    list-style: none;
    text-align: center;
    overflow-x: hidden;
    overflow-y: auto;
    transition: all .5s;
}

.lyric-ls li.active {
    font-weight: bold;
    color: #d32d2d;
    font-size: 18px;
}

.music-progress {
    display: flex;
    justify-content: space-between;
    align-items: center;
    box-sizing: border-box;
    padding: 5px 20px;
    height: 30px;
    font-size: 16px;
}

.music-progress span {
    position: relative;
    display: inline-block;
    margin: 0 10px;
    width: 270px;
    height: 5px;
    background: #ccc;
    border-radius: 8px;
    overflow: hidden;
}

.music-progress span i {
    position: absolute;
    left: 0;
    top: 0;
    display: inline-block;
    width: auto;
    height: 100%;
    background: #d32d2d;
}

.music-audio {
    display: flex;
    justify-content: space-between;
    align-items: center;
    box-sizing: border-box;
    padding: 0 20px;
    height: 70px;
    text-align: center;
    background: #f9f9f9;
}

.music-audio audio {
    opacity: 0;
}

.music-audio i::before {
    font-size: 24px;
    cursor: pointer;
}

.music-audio-set {
    flex: 1;
    display: flex;
    justify-content: center;
    align-items: center;
}

.music-audio-set i {
    margin: 0 15px;
}

.music-audio-set .music-play::before,
.music-audio-set .music-pause::before {
    font-size: 32px;
}

.music-play,
.music-muted {
    display: block;
}

.music-pause,
.music-mute {
    display: none;
}

.music-play.active,
.music-muted.active {
    display: none;
}

.music-audio-bar {
    position: absolute;
    top: -115px;
    left: 3px;
    display: none;
    padding: 5px;
    width: 10px;
    height: 100px;
    border-radius: 14px;
    background: #f8f8f8;
    border: 1px solid #ccc;
    overflow: hidden;
}

.music-audio-bar.active {
    display: block;
}

.music-audio-bar span {
    position: absolute;
    top: 5px;
    left: 50%;
    margin-left: -5px;
    display: block;
    width: 10px;
    height: 100px;
    border-radius: 14px;
    background: #fff;
    transform: rotate(180deg);
    overflow: hidden;
}

.music-audio-bar span em {
    position: absolute;
    top: 0;
    left: 0;
    display: block;
    width: 10px;
    height: 100px;
    border-radius: 14px;
    background: #d32d2d;
}

.no-lyric {
    overflow-y: visible;
}

.no-lyric .music-lyric {
    display: none;
}

@media all and (max-width: 768px) {
    .music {
        margin: 0;
        display: flex;
        flex-direction: column;
        box-sizing: border-box;
        width: 100%;
        height: 100vh;
        border-radius: 0;
    }

    .music-lyric {
        flex: 1;
        padding: 15px 5px;
        height: calc(100vh - 160px);
    }

    .lyric-ls {
        margin: 0 auto;
        width: 95%;
    }
}
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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
  • 效果预览

完成以后就下面这个样子。

歌曲界面

# 解析方法

下面开始正式的解析方法介绍。

# 获取歌词

通过axios这个包来请求歌词文件,然后获取歌词内容。

// 获取歌词
async function getLyric () {  
    let data = await axios.get('data/lrh-mq.lrc');
    let lrcs = parseLyric(data.data);
    localStorage.setItem('lyrcis', JSON.stringify(lrcs));
    showLyric(lrcs);
}
1
2
3
4
5
6
7

# 解析歌词

获取到歌词字符串以后,就开始解析成对象的格式。

function parseLyric(data) {
    let lrc = {
        ti: '', // 歌名
        ar: '', // 歌手
        al: '', // 专辑
        by: '', // 歌词作者
        offset: 0, // 时间毫秒,调整歌词位置
        ms: [] // 歌词{t:时间,c:歌词}
    }
    if (data.length === 0) return;
    let lrcs = data.split('\n');
    for (let key in lrcs) {
        lrcs[key] = lrcs[key].replace(/(^\s*)|(\s*$)/g, '');
        let t = lrcs[key].substring(lrcs[key].indexOf("[") + 1, lrcs[key].indexOf("]"));
        let s = t.split(":");
        if (isNaN(parseInt(s[0]))) { // 非数值
            for (const lKey in lrc) {
                if (lKey !== 'ms' &&
                    lKey == s[0].toLowerCase()) {
                    lrc[lKey] = s[1];
                }
            }
        } else {
            let arr = lrcs[key].match(/\[(\d+:.+?)\]/g); // 时间
            let start = 0;
            for (const k in arr) {
                start += arr[k].length; // 歌词位置
            }
            let content = lrcs[key].substring(start); // 歌词内容
            for (const k in arr) {
                let t = arr[k].substring(1, arr[k].length - 1);
                let s = t.split(":");
                lrc.ms.push({
                    t: (parseFloat(s[0]) * 60 + parseFloat(s[1])).toFixed(3),
                    c: content,
                })
            }
        }
    }
    lrc.ms.sort(function (a, b) {
        return a.t - b.t;
    })
    return lrc;
}
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

# 显示歌词

// 歌曲元素
let lrcList = document.querySelector('.lyric-ls'),
player = document.querySelector('#music-player'),
playBtn = document.querySelector('.qm-play'),
pauseBtn = document.querySelector('.qm-pause'),
startTime = document.querySelector('#startTime'),
endTime = document.querySelector('#endTime'),
progressBar = document.querySelector('#music-progress-bar i'),
playStatus = false;

// 显示歌词
function showLyric (lrc) {
    let lrcStr = '';
    for (const item of lrc.ms) {
        lrcStr += `<li>${item.c}</li>`;
    }
    lrcList.innerHTML = lrcStr;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

再次查看页面。

歌词页面

# 播放暂停

// 播放暂停事件
playBtn.addEventListener('click', playSong, false);
pauseBtn.addEventListener('click', playSong, false);
1
2
3
  • 播放暂停
// 播放歌曲
function playSong () {
    if (!playStatus) {
        player.play();
        playBtn.classList.remove('active');
        pauseBtn.classList.add('active');
    } else {
        player.pause();
        playBtn.classList.add('active');
        pauseBtn.classList.remove('active');
    }
    playStatus = !playStatus;
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 歌词高亮

  • 监听播放时间
player.addEventListener('timeupdate', getTime, false);
1
  • 获取时间
// 获取时间
function getTime (e) {  
    let currentTime = player.currentTime || e.timeStamp / 1000;
    startTime.innerText = calcTimeStr(currentTime).m;
    let precent = (currentTime / player.duration) * 100;
    progressBar.style.width = precent+'%';
    if (endTime && endTime.innerText === '00:00') {
        endTime.innerText = calcTimeStr(player.duration).m;
    }
    showLyricPos(currentTime);
}
1
2
3
4
5
6
7
8
9
10
11
  • 计算时间字符串
// 计算时间字符串
function calcTimeStr(second) {
    let s = 0;
    let m = 0;
    let h = 0;
    let sNum = second % 60;
    let res = '';
    if (second < 3600) {
        s = sNum;
        m = parseInt(second / 60);
        h = 0;
    } else {
        let mNum = second % 3600;
        if (mNum % 60 < 60) {
            s = mNum % 60;
            m = parseInt(mNum / 60)
        }
        m = parseInt(mNum / 60);
        h = parseInt(second / 3600);
    }
    s = s > 10 ? parseInt(s) : '0' + parseInt(s);
    m = m > 10 ? m : '0' + m;
    h = h > 10 ? h : '0' + h;
    res = h + ':' + m + ':' + s;
    return {
        h: res,
        m: m + ':' + s,
    };
}
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
  • 歌词高亮
// 歌词高亮
function showLyricPos (time) {
    let lyrcis = localStorage.getItem('lyrcis'), 
    lrcItem = document.querySelectorAll('.lyric-ls li');
    lyrcis = JSON.parse(lyrcis).ms;
    let index = 0;
    for (let i = 0; i < lyrcis.length; i++) {
        let item = lyrcis[i];
        let diff = time - item.t;
        if (diff >= 0 && diff < 1) {
            index = i;
            for (const item of lrcItem) {
                item.className = '';
            }
            lrcItem[i].className = 'active';
            let lyrTop = lrcItem[i].offsetTop;
            let midTop = lrcList.offsetHeight / 2;
            let allowLineNum = midTop / 36;
            let diffTop = lyrTop - midTop;
            if (i >= allowLineNum && diffTop > 0) {
                lrcList.scrollTop = diffTop;
            } else {
                lrcList.scrollTop = 0;
            }
        }
    }
}
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

下面就是最终的效果。

歌词高亮

在线预览 (opens new window)

# 写在最后

以上就是一个简单的歌词获取解析的小案例。

分享至:

  • qq
  • qq空间
  • 微博
  • 豆瓣
  • 贴吧