How I built an audio player with real-time lyrics using Vanilla Javascript?

How I built an audio player with real-time lyrics using Vanilla Javascript?

Β·

6 min read

In this article, I'll walk you through how I built an audio player with real-time lyrics. After publishing the result on Linkedin , I had messages from people asking me to explain the process. So, I decided to write an article about it.

The biggest challenge is of course to synchronize the audio with the good line. I made some research and discovered that there is a specific file format for lyrics files which is LRC ( just an abbreviation of the word lyrics πŸ˜‰ ).
An LRC file is basically a text file that contains a song’s lyrics and the exact time for each line.

[00:02.80] I found a love for me
[00:10.40] Darling just dive right in and follow my lead
[00:17.90] Well I found a girl beautiful and sweet

That's great, but the file structure was not appropriate for my use case. I turned the LRC file of the song I was working with into a JSON (Javascript Object Notation) file with a bunch of structural transformations. The transformations helped me adapt the file structure to one that was more code friendly.

I did it manually which of course was very hard. But no doubt someone could actually write a script that would make the transformations and generate a JSON file.

So, this is the structure I ended up with after maybe hours of transformations.

[
  {
    "line": ". . .",
    "start": "0",
    "end": "30"
  },
  {
    "line": "Yeah, yeah my mind",
    "start": "31",
    "end": "33"
  },
  {
    "line": "Yeah, yeah my mind",
    "start": "34",
    "end": "39"
  },
  ...
]

After having the structure I needed, the next step was to get the lyrics and add them to the DOM (Document Object Model). For that, I made an HTTP (Hypertext Transfer Protocol) request to get the JSON file and mounted every line using p tags. I also added two data attributes (data-start and data-end) to each tag. They will help us know when to show and hide each line based on the current playback time.

// Used to fetch the lyrics file
async function getLyrics() {
  let response = await fetch("assets/data.json");
  return response.json();
}
const lyrics = await getLyrics();
// Adding lyrics to DOM
lyrics.forEach((line) => {
  let p = document.createElement("p");
  p.textContent = line.line;
  p.dataset.start = line.start;
  p.dataset.end = line.end;
  p.addEventListener("click", function () {
    document.getElementById("player").currentTime = +this.dataset.start;
    document.getElementById("player").play();
  });
  document.querySelector("#song-lyrics div").appendChild(p);
});

We also attach a click event handler to each line (p). The handler will change the playback time of the audio player when the user clicks on that line.

The next step was to attach the ontimechange event to the audio player. The event handler will get the current line based on the playback time. The section that's holding the lyrics is then translated on the cross axis (Y). The translation helps us create a smooth transition between lines. I then added the active class to the current line to unblur the text.

document.getElementById("player").ontimeupdate = function () {
  // Getting the current line based on time
  let line = lyrics.filter((line) => {
    return isBetween(line.start, line.end, this.currentTime);
  });
  document.querySelectorAll("p.active").forEach(function (p) {
    p.classList.remove("active");
  });
  // Translating wrapper on Y axis
  let text = document.querySelectorAll("p");
  let currentText = {};
  for (let i = 0; i < text.length; i++) {
    let current = text[i];
    if (
      isBetween(current.dataset.start, current.dataset.end, this.currentTime)
    ) {
      currentText = current;
      break;
    }
  }
  //Translation is based on the position of current line and the top of the wrapper 
  let translate = -(currentText.offsetTop - top);
  document.getElementById(
    "lyrics-wrapper"
  ).style.transform = `translateY(${translate}px)`;
  // Setting active line
  document
    .querySelector(`p[data-start="${line[0].start}"]`)
    .classList.add("active");
};

Remember the data attributes we added to the p tags? I use them in the code above to get the current line by checking if the playback time is between the data-start value and the data-end value. I use a custom isBetween function for that purpose.

And that's it πŸŽ‰ we now have a functional audio player with real-time lyrics. I made it using the lyrics file of my favorite song, but feel free to update it if you want.

You can find the Github repository here. And if you have any questions, you can reach me on Twitter or Linkedin and I'll be glad to answer them.

Article cover by C D-X