Javascript Lyric Synchronizer
Tags: javascript, webdev, music, tutorial
Published: 2022-06-20 14:44:23

Hai I'am Anam 👋 and this is my first article ☺️

In this article we will experiment to making a simple lyrics synchronizer with javascript from scratch. 

Let's get started 🚀

## LRC File

Before we start, we need to know about (*.lrc) file.

> LRC (short for LyRiCs) is a computer file format that synchronizes song lyrics with an audio file, such as MP3, Vorbis or MIDI. 
 [wikipedia](https://en.m.wikipedia.org/wiki/LRC_(file_format))

lrc file format example:

``` plaintext
[ar:Lyrics artist]
[al:Album where the song is from]
[ti:Lyrics (song) title]
[00:12.00]Line 1 lyrics
[00:17.20]Line 2 lyrics
[00:21.10]Line 3 lyrics
```
> the lrc file has several [variants](), the example above is a simple variant.

the lrc file format is quite simple: 

1. lines 1 - 3 are [id tags](https://en.m.wikipedia.org/wiki/ID3) or metadata to be displayed before lyrics 

2. lines 4 - 6 are lyrics with timestamps enclosed in square brackets. the time format used is mm:ss.xx:

  - mm: minutes
  - ss: seconds
  - xx: hundredths of a second

next we will create a simple lrc parser with javascript.

## LRC Parser

Based on the previous example, the lrc file has two parts:

1. id tags 
2. lyrics

but for now we ignore the id tags and focus on the lyrics only. 

Time to code 👩‍💻

``` javascript
// lrc (String) - lrc file text
function parseLyric(lrc) {
    // will match "[00:00.00] ooooh yeah!"
    // note: i use named capturing group
    const regex = /^\[(?<time>\d{2}:\d{2}(.\d{2})?)\](?<text>.*)/;

    // split lrc string to individual lines
    const lines = lrc.split("\n");
    
    const output = [];
    
    lines.forEach(line => {
        const match = line.match(regex);
        
        // if doesn't match, return.
        if (match == null) return;
        
        const { time, text } = match.groups;
        
        output.push({
            time: parseTime(time),
            text: text.trim()
        });
    });
    
    // parse formated time
    // "03:24.73" => 204.73 (total time in seconds)
    function parseTime(time) {
        const minsec = time.split(":");
        
        const min = parseInt(minsec[0]) * 60;
        const sec = parseFloat(minsec[1]);
        
        return min + sec;
    }
    
    return output;
}
```

the function above will separate the time and text using a regular expression and will return the output as an array of objects. 

the output will look like this:

``` javascript
[
    {
        text: "Faith you're driving me away",
        time: "21.16"
    },
    ...
]
```

## Sync Lyric

The next task is to synchronize the lyrics.

The method we will use is to find the timestamp of the lyrics closest to the given time. If you have another method please let me know in the comments 😻

``` javascript
// lyrics (Array) - output from parseLyric function
// time (Number) - current time from audio player
function syncLyric(lyrics, time) {
    const scores = [];
    
    lyrics.forEach(lyric => {
        // get the gap or distance or we call it score
        const score = time - lyric.time;

        // only accept score with positive values
        if (score >= 0) scores.push(score);
    });
    
    if (scores.length == 0) return null;
    
    // get the smallest value from scores
    const closest = Math.min(...scores);

    // return the index of closest lyric
    return scores.indexOf(closest);
}
```

## Simple music player

Now we have two essential functions, parser and synchronizer.

It's time to build a simple music player with realtime lyric sync 🤸


index.html

``` html
<!DOCTYPE html>
<html lang="en">

<head>
    <title>Simple music player with lyric</title>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=Edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="style.css">
</head>

<body>
    <audio class="player" controls></audio>
    <div class="lyric"></div>
    <script src="script.js"></script>
</body>

</html>
```

style.css

``` css
* {
    box-sizing: border-box;
}

html {
    font-family: sans-serif;
    font-size: 16px;
    color: hsl(200, 20%, 25%);
}

body {
    width: 100vw;
    height: 100vh;
    margin: 0;
    padding: 40px;
    display: flex;
    flex-direction: column;
    align-items: center;
}

.lyric {
    font-size: 2rem;
    font-weight: bolder;
    line-height: 1.5;
    text-align: center;
    text-transform: uppercase;
    max-width: 300px;
    margin-top: 40px;
}

.player {
    width: 100%;
    max-width: 300px;
}
```

script.js

``` javascript
!async function main() {
    "use strict";

    const dom = {
        lyric: document.querySelector(".lyric"),
        player: document.querySelector(".player")
    };

    // load lrc file
    const res = await fetch("./lyric.lrc");
    const lrc = await res.text();

    const lyrics = parseLyric(lrc);

    dom.player.src = "./audio.mp3";
    
    dom.player.ontimeupdate = () => {
        const time = dom.player.currentTime;
        const index = syncLyric(lyrics, time);
        
        if (index == null) return;
        
        dom.lyric.innerHTML = lyrics[index].text;
    };

}();

function parseLyric(lrc) {
    // same as previous code
}

function syncLyric(lyrics, time) {
    // same as previous code
}
```

result:

{% codepen https://codepen.io/mcanam/pen/vYdMRLW %}

## Conclusion

In this experiment we learn what an lrc file is, how to parse it and sync it with songs using javascript. 

Next, you can make your own version of the music player which is even cooler 🤩.

Oh one more thing, I have made a simple javascript library based on this experiment called ***liricle***, you can check it on [github](https://github.com/mcanam/liricle). feel free to star or fork 👉👈 just kidding 😅

Thank you very much for reading. Don't hesitate to leave comments, criticisms or suggestions, I will really appreciate it ☺️