I used Vue3 to write a NetEase cloud music APP for mobile phone. When writing the playback component, I found that the open source community did not have a lyric parsing plug-in that I was satisfied with. That is to say, if you want to parse the lyrics, you can only write them manually. However, since his algorithm does not support parsing NetEase Cloud lyrics, I plan to implement a wave myself manually
1. Introduction
You should read this article with a body of knowledge
Basic TypeScript syntax 2. Basic JavaScript syntaxCopy the code
The installation
npm i lyric-resolver
Copy the code
Making the address
Github.com/SnowingFox/…
example
Github.com/SnowingFox/…
Application scenarios
This plugin is used when you want to create a lyric scrolling effect similar to that of the NetEase Cloud music client
Song Lyrics data analysis
This is a basic lyric data
[00:00. 000] lyrics: noda kojiro the composing 00:01onsaturday (UK time). 000, noda kojiro the [00:19. 600] [00:20. 000] や っ と eye を 覚 ま し た か い そ れ な の に な ぜ eye も close わ せ や し な い ん だ い? [00:30. 090] "遅 い よ" と nu る jun こ れ で も や れ る だ け fly ば し て き た ん だ よ [00:38. 720] [00:39. 670] heart が を chase い body more し て き た ん だ よ [00:44. 340] [00:45. 340] jun の hair や pupil だ け で chest pain が い よ [00:50. 560] with じ を い absorption こ ん で from し た く な い よ [00:55. 200] remote か yesterday か ら know る そ の sound に [01:00. 260] raw ま れ て は じ め て what を said え ば い い? [01:07. 240] before [01:07. 640] before jun の past か ら servant は jun を agent し は じ め た よ [01:12. 280] そ の ぶ き っ ち ょ な smile い party を め が け て や っ て き た ん だ よ [01:17. 260] [01:17. 630] jun が completely all な く な っ て チ リ ヂ リ に な っ た っ て [01:22. 430] も う fan わ な い ま た 1 か ら agent し は じ め る さ [01:27. 240] む し ろ 0 か ら ま た universe を は じ め て み よ う か [01:32. 670] [01:43. 440] ど っ か ら words す か な jun が sleeps っ て い た between の ス ト ー リ ー What [01:53. 480] million What light points の monogatari を language り に き た ん だ よ け ど い ざ そ の pose こ の eye に reflected す と [02:07. 720] [02:08. 720] jun も know ら ぬ jun と ジ ャ レ て play れ た い よ [02:13. 530] jun の え elimination ぬ pain み ま で love し て み た い よ [02:18. 480] what a galaxy points か の fruit て に out every え た [02:23. 530] そ の hand を 壊 さ ず に ど う grip っ た な ら い い? [02:30. 500] before [02:31. 000] before jun の past か ら servant は jun を agent し は じ め た よ [02:35. 680] そ の 騒 が し い sound と 涙 を め が け や っ て き た ん だ よ [02:40. 550] [02:40. 990] そ ん な revolution eve の servant ら を who が check め る と い う ん だ ろ う [02:45. 720] も う fan わ な い jun の ハ ー ト を に flag set て る よ は servant [02:50. 740] king か ら truths め を duo い get っ た の [02:55. 880] [03:53. 030] before previous か ら servant は jun を agent し は じ め た よ [03:57. 290] そ の ぶ き っ ち ょ な smile い party を め が け て や っ て き た ん だ よ [04:01. 990] [HKT. 620] jun が completely all な く な っ て チ リ ヂ リ に な っ た っ て [04:07. 240] も う fan わ な い ま た 1 か ら agent し は じ め る さ [04:12. 370] what light-years で も こ の song を mouth ず さ み な が ら [04:18. 090]Copy the code
It can be found that the time of the lyrics will give minutes, seconds and milliseconds, such as [03:58.970] is 3min:58s:970ms. We can calculate the time of the lyrics according to this time, so as to correctly output the line of the current lyrics to the user
Basic usage
import Lyric, { HandlerParams } from 'lyric-resolver'
import { getLyric } from '.. /api/lyric.js'
export async function useLyric() :any {
const lrc = await getLyric()
const currentLyric = new Lyric(lrc, handleLyric)
/* * @params curLineNum [number] Current line of lyric * @params txt [string] Current line's txt * * @return void * */
function handleLyric(payload: HandlerParams) :void {
const { curLineNum, txt } = payload
// You can also get curLineNum by doing the following
const curLine: number = currentLyric.curLine
}
function play() :void {
currentLyric.play()
}
function stop() :void {
currentLyric.stop()
}
function togglePlay() :void {
currentLyric.togglePlay()
}
function seek(time: number) :void {
currentLyric.seek(time)
}
}
Copy the code
You can guess from the literal meaning of the code what’s being done, and the main thing I want to say is, okay
const { curLineNum, txt } = payload
Copy the code
CurLineNum is to get the current line of lyrics, TXT is to get the current lyrics text
implementation
Time to calculate
Remember the lyrics of time counting? To get the lyrics time, we need to get the data in [03:58.970] first and convert it into a number type data
Regular match
const lyricTimeReg: RegExp = / \ [(\ d {2}) : (\ d {2}) (\ d {2, 3})] / g
Copy the code
[01:07.640] mY become commanded Commanded Usage of his 91% 91% 91% 91% 91% in the Scot
const time = lyricTimeReg.exec([01:07.640] mY son becomes the verbal usage of his meal reasonably meted out ')
Copy the code
After output, we get the following data
[
"[01:07. 640]"."01"./ / minute
"7"./ / SEC.
"640" / / ms
]
Copy the code
All we need is a helper function to convert this data to a number in milliseconds
function transformRegTime(times: RegExpExecArray) :number {
const result: number[] = []
times.forEach((time, index) = > {
if (index >= 1 && index <= 3) {
result.push(parseInt(time))
}
})
return (result[0] * 60 + result[1]) * 1000 + result[2]}Copy the code
Finally, test the output
transformRegTime(time!) / / 67640
Copy the code
We get 67640, the position of 67s, about time! Not empty assertion is exec returns a RegExpExecArray | null type
OK, time calculation problem solved, now to write the plug-in theme section
Subject to realize
interface
// The time and text of each line of lyrics
interface Lines {
lineTime: number
txt: string
}
// Handle function parameter definitions
interface HandlerParams {
curLineNum: number
txt: string
}
// Play status
const enum PLAYING_STATE {
stop = 0,
playing = 1,}Copy the code
Initialize the
export default class Lyric {
lines: Lines[]
lrc: string
state: any
curLine: number
startTime: number
stopTime: number
offset: number
timer: any
handler: any
constructor(lrc: string, handler: (params: HandlerParams) => void) {
this.lrc = lrc
this.lines = []
this.state = PLAYING_STATE.stop
this.curLine = 0
this.timer = null
this.startTime = 0
this.stopTime = 0
this.offset = 0
this.handler = handler
this._init()
}
private _init(): void {
this._initLines()
}
private _initLines(): void {
this.lrc.split('\n').forEach((lrc) = > {
let time = lyricTimeReg.exec(lrc)
// Use a non-null assertion when passing time
if(! time) {return
}
let txt: string = lrc.replace(lineTimeReg, ' ')
// Filter blank text
if (txt === ' ') {
return
}
this.lines.push({
lineTime: transformRegTime(time),
txt,
})
})
// In ascending order, make sure the lyrics are in the current position determined by its time
this.lines.sort((a, b) = > {
return a.lineTime - b.lineTime
})
}
}
Copy the code
For now, we just need to focus
lines: Lines[]
Copy the code
For this row, when we do the following
const lyric = new Lyric(txt)
console.log(lyric.lines)
Copy the code
The final print will look something like thisOK, here we end the initial analysis of the lyrics, the next to achieve the lyrics play, pause function
Play/Pause
export default class Lyric {
lines: Lines[]
lrc: string
state: any
curLine: number
startTime: number
stopTime: number
offset: number
timer: any
handler: any
constructor(lrc: string, handler: (params: HandlerParams) => void) {
this.lrc = lrc
this.lines = []
this.state = PLAYING_STATE.stop
this.curLine = 0
this.timer = null
this.startTime = 0
this.stopTime = 0
this.offset = 0
this.handler = handler
this._init()
}
private _playReset(): void {
// Calculate how much time is left until the next line
let { delay, targetIndex } = this._calculateDelay()
this.curLine = targetIndex
clearTimeout(this.timer)
this.timer = setTimeout(() = > {
// The implementation of hanlder, that is, the user's own definition of the lyrics to obtain information function
this._callHandler(this.curLine++)
if (this.curLine < this.lines.length && this.state === PLAYING_STATE.playing) {
this._playReset()
}
}, delay)
}
play(): void {
this.state = PLAYING_STATE.playing
this.startTime = Date.now()
if (this.curLine < this.lines.length) {
clearTimeout(this.timer)
this._playReset()
}
}
stop(): void {
this.state = PLAYING_STATE.stop
this.stopTime = Date.now()
this.offset = this.offset + this.stopTime - this.startTime
clearTimeout(this.timer)
}
togglePlay(): void {
if (this.state === PLAYING_STATE.playing) {
this.stop()
} else {
this.play()
}
}
private _calculateDelay(): any {
let delay: number = this._findLine(this.curLine).lineTime - this.offset
let targetIndex: number = this.curLine
let isFind: boolean = false
if (delay < 0) {
this.lines.forEach((line, index) = > {
delay = this._findLine(index).lineTime - this.offset
if (delay >= 0 && !isFind) {
targetIndex = index
isFind = true
return}})}else {
this.lines.forEach((line, index) = > {
if (
this.offset >= this._findLine(index - 1).lineTime &&
this.offset < line.lineTime
) {
targetIndex = index
delay = this._findLine(targetIndex).lineTime - this.offset
}
})
}
return {
delay,
targetIndex,
}
}
/** function handleLyric(payload: HandlerParams): void { const { curLineNum, txt } = payload const curLine: number = currentLyric.curLine } */
private _callHandler(index: number) :void {
if (index < 0) {
return
}
let curLine = index
if (this._findCur()? .txt ===' ') {}try {
this.handler({
curLineNum: curLine,
txt: this._findCur()? .txt.trim() ||' '})},catch (e) {
return}}// Find information about the current lyrics
private _findCur(): Lines {
return this.lines[this.curLine]
}
// Find the lyric information for the specified number of Lines, type Lines
private _findLine(i: number): Lines {
const lines = this.lines
if (i < 0) {
return lines[0]}if (i >= lines.length) {
return lines[lines.length - 1]}return lines[i]
}
}
Copy the code
For lyrics playing, it is actually a recursive loop, and the delay is obtained by calculating how long it will take to play the next line of lyrics. However, there is a pain point, which is also a problem I encountered when making this plug-in. How to calculate the next delay?
I want to explain a couple of numbers
startTime
When the lyrics playstopTime
The time when the song stopsoffset
The progress time of the current lyrics
Here’s how I did it
play() {
this.startTime = Date.now()
}
stop() {
this.stopTime = Date.now()
this.offset = this.offset + (this.stopTime - this.startTime)
}
Copy the code
Can correctly calculate the progress, so pause playback time – start playing time = the time to play, pay attention to what I said is the play, not play song, is this time you play, pause, a total of how much time after and before the offset and the playing time, is the progress of the current song playing
seek
So one wonders, what if I fall back? So this is row NTH, what if I want to go back to row NTH minus M? Or what if I fast-forward to row N plus M? At this time you can use seek to achieve
seek(offset: number) :void {
this.offset = offset
this.play()
}
Copy the code
The final code
interface Lines {
lineTime: number
txt: string
}
// Handle function parameter definitions
interface HandlerParams {
curLineNum: number
txt: string
}
// Play status
const enum PLAYING_STATE {
stop = 0,
playing = 1,}export default class Lyric {
lines: Lines[]
lrc: string
state: any
curLine: number
startTime: number
stopTime: number
offset: number
timer: any
handler: any
constructor(lrc: string, handler: (params: HandlerParams) => void) {
this.lrc = lrc
this.lines = []
this.state = PLAYING_STATE.stop
this.curLine = 0
this.timer = null
this.startTime = 0
this.stopTime = 0
this.offset = 0
this.handler = handler
this._init()
}
private _init(): void {
this._initLines()
}
private _initLines(): void {
this.lrc.split('\n').forEach((lrc) = > {
let time = lyricTimeReg.exec(lrc)
// Use a non-null assertion when passing time
if(! time) {return
}
let txt: string = lrc.replace(lineTimeReg, ' ')
// Filter blank text
if (txt === ' ') {
return
}
this.lines.push({
lineTime: transformRegTime(time),
txt,
})
})
// In ascending order, make sure the lyrics are in the current position determined by its time
this.lines.sort((a, b) = > {
return a.lineTime - b.lineTime
})
}
private _playReset(): void {
// Calculate how much time is left until the next line
let { delay, targetIndex } = this._calculateDelay()
this.curLine = targetIndex
clearTimeout(this.timer)
this.timer = setTimeout(() = > {
// The implementation of hanlder, that is, the user's own definition of the lyrics to obtain information function
this._callHandler(this.curLine++)
if (this.curLine < this.lines.length && this.state === PLAYING_STATE.playing) {
this._playReset()
}
}, delay)
}
play(): void {
this.state = PLAYING_STATE.playing
this.startTime = Date.now()
if (this.curLine < this.lines.length) {
clearTimeout(this.timer)
this._playReset()
}
}
stop(): void {
this.state = PLAYING_STATE.stop
this.stopTime = Date.now()
this.offset = this.offset + this.stopTime - this.startTime
clearTimeout(this.timer)
}
togglePlay(): void {
if (this.state === PLAYING_STATE.playing) {
this.stop()
} else {
this.play()
}
}
private _calculateDelay(): any {
let delay: number = this._findLine(this.curLine).lineTime - this.offset
let targetIndex: number = this.curLine
let isFind: boolean = false
if (delay < 0) {
this.lines.forEach((line, index) = > {
delay = this._findLine(index).lineTime - this.offset
if (delay >= 0 && !isFind) {
targetIndex = index
isFind = true
return}})}else {
this.lines.forEach((line, index) = > {
if (
this.offset >= this._findLine(index - 1).lineTime &&
this.offset < line.lineTime
) {
targetIndex = index
delay = this._findLine(targetIndex).lineTime - this.offset
}
})
}
return {
delay,
targetIndex,
}
}
/** function handleLyric(payload: HandlerParams): void { const { curLineNum, txt } = payload const curLine: number = currentLyric.curLine } */
private _callHandler(index: number) :void {
if (index < 0) {
return
}
let curLine = index
if (this._findCur()? .txt ===' ') {
return
}
try {
this.handler({
curLineNum: curLine,
txt: this._findCur()? .txt.trim() ||' '})},catch (e) {
return}}// Find information about the current lyrics
private _findCur(): Lines {
return this.lines[this.curLine]
}
// Find the lyric information for the specified number of Lines, type Lines
private _findLine(i: number): Lines {
const lines = this.lines
if (i < 0) {
return lines[0]}if (i >= lines.length) {
return lines[lines.length - 1]}return lines[i]
}
}
Copy the code
End and spend
If you have any questions about the content of this article, please leave them in the comments section and I will try to answer them