DEV Community

Cover image for Javascript Lyric Synchronizer
mcanam
mcanam

Posted on

Javascript Lyric Synchronizer

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

lrc file format example:

[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
Enter fullscreen mode Exit fullscreen mode

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 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 👩‍💻

// 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;
}
Enter fullscreen mode Exit fullscreen mode

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:

[
    {
        text: "Faith you're driving me away",
        time: "21.16"
    },
    ...
]
Enter fullscreen mode Exit fullscreen mode

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 😻

// 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);
}
Enter fullscreen mode Exit fullscreen mode

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

<!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>
Enter fullscreen mode Exit fullscreen mode

style.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;
}
Enter fullscreen mode Exit fullscreen mode

script.js

!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
}
Enter fullscreen mode Exit fullscreen mode

result:

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. 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 ☺️

Top comments (3)

Collapse
 
adamelitzur profile image
Adam Elitzur

Amazing post and library! I tried using it, but the import is not working yet.

I have this, because just saying the const liricle line didn't work:
import Liricle from "liricle";
const liricle = new Liricle();

I'm getting this error: TypeError: liricle_WEBPACK_IMPORTED_MODULE_5_.default is not a constructor

Any recommendations for how to make this work? I'm using next.js, which might be the difference. Thanks so much!

Collapse
 
mcanam profile image
mcanam • Edited

ohh i apologize for responding so late, i didn't see the notification :( can i know what version of next.js you are using? and you can open the issue here

Collapse
 
adamelitzur profile image
Adam Elitzur

Thanks! I'm using next 12. I ended up trying to make it myself, but I have no idea how to tackle the auto-scroll feature. How did you do that? I love how on the demo, it scrolls to the middle of the page, so the line is always in the middle. Thanks!