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

  • startTimeWhen the lyrics play
  • stopTimeThe time when the song stops
  • offsetThe 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