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 (0)

πŸ€” Did you know?

Β 
🌚 You can turn on dark mode in Settings