前端解析歌词文件并展示
前言
在前端开发工作中,我们经常会碰到音乐播放器,除了能播放歌曲外,还能实时地展示对应的歌词。那么,这是如何实现的呢?
本文介绍了 npm包lyric-parser
,探索其是如何实现歌词文件的解析和实时展示的。
歌词文件的格式
歌词文件是.lrc
结尾的文件,文本中含有两类标签:
-
一是
标识标签
,格式为[标识名:值]
,大小写等价。包括: -
[ti:曲名] -
[ar:艺人名] -
[al:专辑名] -
[by:编者] -
[offset:时间补偿值] 单位毫秒,正值表示歌词整体提前,负值相反。 -
二是
时间标签
,其标准格式是[分钟:秒.毫秒] 歌词
。
举个例子,下面是周董的《兰亭序》的歌词:
"[ti:兰亭序 (慢四版)]n[ar:周杰伦]n[al:]n[by:]n[offset:0]n[00:00.00]兰亭序 (慢四版) - 周杰伦 (Jay Chou)n[00:09.46]词:方文山n[00:18.92]曲:周杰伦n[00:28.39]兰亭临帖n[00:30.07]行书如行云流水n[00:34.61]月下门推n[00:36.47]心细如你脚步碎n[00:40.81]忙不迭n[00:42.72]千年碑易拓却难拓你的美n[00:48.22]真迹绝 真心能给谁n[00:53.55]牧笛横吹n[00:55.36]黄酒小菜又几碟n[00:59.72]夕阳余晖n[01:01.78]如你的羞怯似醉n[01:06.16]摹本易写n[01:07.94]而墨香不退与你共留余味n[01:13.66]一行朱砂 到底圈了谁n[01:18.88]无关风月 我题序等你回n[01:25.13]悬笔一绝 那岸边浪千叠n[01:31.44]情字何解 怎落笔都不对n[01:37.88]而我独缺 你一生的了解n[01:45.98]无关风月 我题序等你回n[01:48.88]悬笔一绝 那岸边浪千叠n[01:52.17]情字何解 怎落笔都不对n[01:55.33]而我独缺 你一生的了解n[01:58.50]无关风月 我题序等你回n[02:01.78]悬笔一绝 那岸边浪千叠n[02:05.03]情字何解 怎落笔都不对n[02:08.08]独缺 你一生了解n[02:09.54]弹指岁月 倾城顷刻间湮灭n[02:16.10]月下门推 心细如你脚步碎n[02:22.75]忙不迭 千年碑易拓n[02:26.42]却难拓你的美n[02:29.88]真迹绝 真心能给谁n[02:35.15]牧笛横吹 黄酒小菜又几碟n[02:41.73]夕阳余晖 如你的羞怯似醉n[02:47.91]摹本易写n[02:49.94]而墨香不退与你同留余味n[02:55.38]一行朱砂 到底圈了谁n[03:00.68]无关风月 我题序等你回n[03:06.91]悬笔一绝 那岸边浪千叠n[03:13.23]情字何解 怎落笔都不对n[03:19.74]而我独缺 你一生的了解n[03:27.92]无关风月 我题序等你回n[03:30.58]悬笔一绝 那岸边浪千叠n[03:33.86]情字何解 怎落笔都不对n[03:37.03]而我独缺 你一生的了解n[03:40.19]无关风月 我题序等你回n[03:43.29]悬笔一绝 那岸边浪千叠n[03:46.52]情字何解 怎落笔都不对n[03:49.78]独缺 你一生了解n[03:51.37]弹指岁月 倾城顷刻间湮灭n[03:57.73]月下门推 心细如你脚步碎n[04:04.16]忙不迭 千年碑易拓n[04:08.05]却难拓你的美n[04:11.55]真迹绝 真心能给谁n[04:16.88]牧笛横吹 黄酒小菜又几碟n[04:23.17]夕阳余晖 如你的羞怯似醉n[04:29.64]摹本易写n[04:31.63]而墨香不退与你同留余味n[04:37.17]一行朱砂 到底圈了谁n[04:42.32]无关风月 我题序等你回n[04:48.45]悬笔一绝 那岸边浪千叠n[04:54.82]情字何解 怎落笔都不对n[05:01.13]而我独缺 你一生的了解n[05:09.50]无关风月 我题序等你回n[05:12.27]悬笔一绝 那岸边浪千叠n[05:15.53]情字何解 怎落笔都不对n[05:18.72]而我独缺 你一生的了解n[05:21.92]无关风月 我题序等你回n[05:24.93]悬笔一绝 那岸边浪千叠n[05:28.24]情字何解 怎落笔都不对n[05:31.43]独缺 你一生了解n[05:33.16]弹指岁月 倾城顷刻间湮灭n[05:39.11]青石板街 回眸一笑你婉约n[05:45.78]恨了没n[05:47.46]你摇头轻叹谁让你蹙着眉n[05:53.47]而深闺 徒留胭脂味n[05:58.55]人雁南飞 转身一瞥你噙泪n[06:04.81]掬一把月 手揽回忆怎么睡n[06:11.18]又怎么会n[06:13.10]心事密缝绣花鞋针针怨怼n[06:18.64]若花怨蝶 你会怨着谁n[06:24.12]无关风月 我题序等你回n[06:30.16]悬笔一绝 那岸边浪千叠n[06:36.58]情字何解 怎落笔都不对n[06:42.85]而我独缺 你一生的了解n[06:49.35]无关风月 我题序等你回n[06:57.14]手书无愧 无惧人间是非n[07:03.51]雨打蕉叶 又潇潇了几夜n[07:09.91]我等春雷 来提醒你爱谁"
lyric-parser
这里通过 npm包lyric-parser
,学习下如何去解析歌词文件。包的地址是:
https://github.com/ustbhuangyi/lyric-parser
git clone 下载,然后继续。
目录结构
目录中主要是 build 目录, src/index.js 文件和示例文件 demo/index.html。
build
webpack.dev.js
webpack.prod.js
demo
index.html
src
index.js
README.md
.gitignore
.babelrc
.editorconfig
.eslintignore
.eslintrc.js
package.json
源码解析
webpack.dev.js
这里是开发环境下的webpack配置文件,定义了入口文件’./src/index’、输出 output、loader(eslint, babel)。
var path = require('path')
var webpack = require('webpack')
function resolve(dir) {
return path.join(__dirname, '..', dir)
}
module.exports = {
entry: './src/index',
output: {
path: resolve('dist'),
filename: 'lyric.js',
library: 'Lyric',
libraryTarget: 'umd',
publicPath: '/assets/'
},
module: {
rules: [
{
test: /.js$/,
loader: 'eslint-loader',
enforce: "pre",
include: [resolve('src'), resolve('test')],
options: {
formatter: require('eslint-friendly-formatter')
}
},
{
test: /.js$/,
loader: 'babel-loader',
include: [resolve('src'), resolve('test')]
}
]
}
};
src/index.js
定义并导出了 Lyric 类:
const timeExp = /[(d{2,}):(d{2})(?:.(d{2,3}))?]/g
const STATE_PAUSE = 0
const STATE_PLAYING = 1
function noop() {
}
export default class Lyric {
constructor(lrc, hanlder = noop) {
this.lrc = lrc
this.tags = {}
this.lines = []
this.handler = hanlder
this.state = STATE_PAUSE
this.curLine = 0
this._init()
}
_init() {
this._initTag() // 初始化 this.tags
this._initLines() // 初始化 this.lines
}
}
首先,tagRegMap 对象也就是标识标签对象,借助正则,存放到 this.tags 上。这里的([^]]*),表示匹配除了 / 和 ] 之外的其他字符,数量零次或多次。
const tagRegMap = {
title: 'ti',
artist: 'ar',
album: 'al',
offset: 'offset',
by: 'by'
}
_initTag() {
for (let tag in tagRegMap) {
const matches = this.lrc.match(new RegExp(`[${tagRegMap[tag]}:([^]]*)]`, 'i'))
this.tags[tag] = matches && matches[1] || ''
}
}
接着,对于歌词按照 ‘n’ 分割得到数组 lines,遍历每一行歌词,如果匹配正则 timeExp,那么判断去掉时间后是否有文本(也就是去掉没有歌词的时间标签)。有歌词的话,存入数组 this.lines 中,排序。
_initLines() {
const lines = this.lrc.split('n')
const offset = parseInt(this.tags['offset']) || 0
for (let i = 0; i < lines.length; i++) {
const line = lines[i]
let result = timeExp.exec(line) // 正则
if (result) {
const txt = line.replace(timeExp, '').trim() // 去掉时间
if (txt) {
this.lines.push({
time: result[1] * 60 * 1000 + result[2] * 1000 + (result[3] || 0) * 10 + offset,
txt
})
}
}
}
this.lines.sort((a, b) => {
return a.time - b.time
})
}
下面是提供的调用方法:play, togglePlay, stop, seek。
当调用 play() 时,
-
找到startTime对应的那一行 -
记录开始时间戳 startStamp -
调用 _callHandler(), 参数是 {txt: this.lines[i].txt, lineNum: i} -
调用 _playRest(),代码中会定时地调用_callHandler函数,对外暴露了当前一行的歌词文本 txt 和对应的行数 lineNum。
_findCurNum(time) {
for (let i = 0; i < this.lines.length; i++) {
if (time <= this.lines[i].time) {
return i
}
}
return this.lines.length - 1
}
_callHandler(i) {
if (i < 0) {
return
}
this.handler({
txt: this.lines[i].txt,
lineNum: i
})
}
_playRest() {
let line = this.lines[this.curNum]
let delay = line.time - (+new Date() - this.startStamp)
this.timer = setTimeout(() => {
this._callHandler(this.curNum++) // 调用 handler(), 并切换到下一行(this.curNum++)
if (this.curNum < this.lines.length && this.state === STATE_PLAYING) {
this._playRest()
}
}, delay)
}
play(startTime = 0, skipLast) {
if (!this.lines.length) {
return
}
this.state = STATE_PLAYING
// 找到startTime对应的那一行
this.curNum = this._findCurNum(startTime)
// 开始时间戳
this.startStamp = +new Date() - startTime
// 调用 handler(), 参数是{txt: this.lines[i].txt, lineNum: i}
if (!skipLast) {
this._callHandler(this.curNum - 1)
}
// 调用 _playRest()
if (this.curNum < this.lines.length) {
clearTimeout(this.timer)
this._playRest()
}
}
togglePlay() {
var now = +new Date()
if (this.state === STATE_PLAYING) {
this.stop()
this.pauseStamp = now
} else {
this.state = STATE_PLAYING
this.play((this.pauseStamp || now) - (this.startStamp || now), true)
this.pauseStamp = 0
}
}
stop() {
this.state = STATE_PAUSE
clearTimeout(this.timer)
}
seek(offset) {
this.play(offset)
}
类似地,stop 是暂停播放,清除定时器;seek 是找到某一行歌词并播放;togglePlay 是切换播放和暂停。
测试
首先,指定 9090 端口:
"scripts": {
"dev": "webpack-dev-server --config build/webpack.dev.js --port 9090 --inline --hot --content-base ./demo",
"build": "webpack --config build/webpack.prod.js"
},
测试下代码,关键代码是:
var lyric = new window.Lyric(lrc, function (obj) {
console.log(obj)
})
执行yarn dev
,打开浏览器http://localhost:9090/
,点击 play 按钮,F12 可以看到打印出了单词和对应的行数:

文章出自:https://juejin.cn/post/7220054775298932791
作者:觅迹
原文始发于微信公众号(前端24):前端解析歌词文件并展示
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/216467.html