前端解析歌词文件并展示

前端解析歌词文件并展示

前言

在前端开发工作中,我们经常会碰到音乐播放器,除了能播放歌曲外,还能实时地展示对应的歌词。那么,这是如何实现的呢?

本文介绍了 npm包lyric-parser,探索其是如何实现歌词文件的解析和实时展示的。

歌词文件的格式

歌词文件是.lrc结尾的文件,文本中含有两类标签:

  1. 一是标识标签,格式为[标识名:值],大小写等价。包括:

    • [ti:曲名]
    • [ar:艺人名]
    • [al:专辑名]
    • [by:编者]
    • [offset:时间补偿值] 单位毫秒,正值表示歌词整体提前,负值相反。
  2. 二是时间标签,其标准格式是[分钟:秒.毫秒] 歌词

举个例子,下面是周董的《兰亭序》的歌词:

"[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: {
          formatterrequire('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({
    txtthis.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

(0)
李, 若俞的头像李, 若俞

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!