A More Remarkable Auto Karaoke System with Genius

A More Remarkable Auto Karaoke System with Genius

Introduction

Welcome all! Previously on this blog, we used React to create an auto karaoke app. From identifying the currently playing song to moving the lyrics in real-time, it almost had it all.

Then, we used the Musixmatch API. It only gave us 30% of the entire lyrics. But today, we will integrate the Genius API to get access to cien por ciento of the lyrics!

(Along with 1 other improvement)

Pre-requisites

I am writing this assuming you have read the previous article. Today's code will depend on it.

Also, I have already introduced scraping lyrics with the Genius API before, so make sure to read that as well.

But if you don't want to start from scratch, you can always find the branch for V1.

Setup

Now that we have the V1 codebase, we are ready to install some NPM packages.

npm i cheerio cookie simple-oauth2
npm i -D @types/cookie @types/simple-oauth2

Note simple-oauth2. The only downside of using the Genius API is that we will also have to implement OAuth. This will now require extra user interaction.

Changing Song Identification Props

In our quest to make your karaoke sessions even better, we decided to fine-tune the song identification process. Here's what we did:

Musixmatch required a song's ISRC to provide the song lyrics. Yet Genius only needs a search query (title + artists).

We can go into arc-identify.ts (the serverless function) and update it like so:

Creating an OAuth Client

Our OAuth Client uses the authorisation code flow, requiring user interaction in exchange for an access token.

The code will be taken from the Genius lyrics scraping article, with the following addition:

export const redirectUri = "http://localhost:3000/api/auth-callback";

This little constant saves us from having to repeat ourselves later.

We must rename the file to _authClient.ts so that Vercel won't treat it as a serverless function.

Setting Up the Authorisation Endpoint

As shown before, the OAuth client generates a URL. There, the user can access to authorise our application with their Genius account. Place the following code into api/auth.ts:

import type { VercelResponse, VercelRequest } from "@vercel/node";
import { client, redirectUri } from "./_authClient";

const authorizationUrl = client.authorizeURL({
  redirect_uri: redirectUri,
});

export default async (req: VercelRequest, res: VercelResponse) => {
  res.redirect(authorizationUrl);
};

Handling the Authorisation Callback

Here, we take the auth code from the URL to generate an access token. The token is then stored in cookies. Let's implement this into our callback function in api/auth-callback.ts:

import type { VercelResponse, VercelRequest } from "@vercel/node";
import { client, redirectUri } from "./_authClient";
const cookie = require("cookie");

export default async (req: VercelRequest, res: VercelResponse) => {
  const { code } = req.query;

  const options = {
    code: code as string,
    redirect_uri: redirectUri,
  };

  const token = await client.getToken(options);
  const accessToken = token.token.access_token as string;

  res.setHeader(
    "Set-Cookie",
    cookie.serialize("accessToken", accessToken, { path: "/" })
  );
  res.redirect("/");
};

Note that we use the path: "/" option when setting the cookie, to access it throughout the application.

Genius Lyrics Scraper (As Seen Previously)

Place the genius.ts file into the project. But make sure to understand the code first.

Creating an Endpoint for Genius Lyrics

Continuing from our previous work, we've created a dedicated endpoint for Genius Lyrics, at api/lyrics.ts. It takes in a search query and uses the access token cookie to return the song's lyrics.

Note that this new file (api/lyrics.ts) would replace the api/find-lyrics.ts file, which used the Musixmatch API.

import type { VercelResponse, VercelRequest } from "@vercel/node";
import { search, extractLyrics } from "./_genius";

export default async (req: VercelRequest, res: VercelResponse) => {
  const { q } = req.query;
  const accessToken = req.cookies.accessToken;

  const song = await search(accessToken, q as string);
  const lyrics = await extractLyrics(song.url);

  res.status(200).json(lyrics);
};

Frontend Authentication

Remember that our app won't work without the user authorising with their Genius account.

We can use the same cookie library to access the accessToken on the front end. Then, we can render the microphone button only if an accessToken exists. If not, we can display a login button.

Update src/App.tsx like so:

Creating a Login Button

Our new login button will be a simple and effective addition for authorisation. Clicking it will take you to the authorisation route (/api/auth).

Add the following code to src/AuthButton.tsx:

export default function AuthButton() {
  return (
    <a
      type="button"
      className="bg-blue-400 rounded-3xl border-sky-50 border-4 p-6 text-2xl uppercase font-mono hover:bg-blue-300"
      href="/api/auth"
    >
      Authorise
    </a>
  );
}

Enhancing the Microphone Input Button

Now, we must reflect all the backend changes we made in the front end. Mainly, src/MicrophoneInput.tsx:


Fine-tuning Word Cycle Rate

In pursuit of improvement, we can improve the word cycling feature. This adjustment ensures a smoother karaoke experience.

So, instead of a "word rate" that defines when to change focus to the next word, we can use a "syllable rate".

Thus, the highlighted word will only change once delay * syllableCount amount of time passes. So, delay is the amount of time a single syllable will take. Then, syllableCount is the number of syllables in the current word.

Let's now update src/lyrics/LyricsBody.tsx to include this new logic.

First, this React component needs access to a few more atoms:

export default function LyricsBody() {
    // ...

    const currentWord = useAtomValue(currentWordAtom);
     const lyricWords = useAtomValue(lyricWordsAtom);

    // ...
}

As seen before, currentWord is the index of the current word in the lyrics. But, newly, lyricWords is the entire lyrics as an array of individual words. This is important to access the current word string.

Now, we can update the lifecycle callback (the useEffect call) with the following code:


Now, we can go back to the store file (in src/store.ts) to add the lyricWordsAtom:

// Get every word in the lyrics
export const lyricWordsAtom = atom((get) =>
  [...get(lyricsAtom).matchAll(/[\w']+/g)].map((match) => match[0])
);

And we can now convert our default "word rate" into a default "syllable rate":

export const songRateAtom = atom(160 * 0.6);

Counting Syllables

Now let’s create a function that will count the number of syllables in a given word.

Generally in the English language, the number of syllables is equal to the number of vowel sounds. A vowel sound never has more than 2 vowels within it. An “e” at the end of the word would not be counted as an extra syllable (think of “scathe” or "dive").

Add the following code to src/lyrics/countSyllables.ts:

const VOWELS = ["a", "e", "i", "o", "u"];

export default function countSyllables(word: string) {
  let vowelSounds = 0;
  let vowelFound = false;

  word.split("").forEach((letter, index) => {
    if (VOWELS.includes(letter) && !vowelFound) {
      if (!(index === word.length - 1 && letter === "e")) {
        vowelSounds++;
      }
      vowelFound = true;
      return;
    }
    vowelFound = false;
  });

  return vowelSounds;
}

Conclusion

And there you have it. Today, we learned how to combine the logic from multiple code repositories to create one awesome application.

First, we evaluated two different APIs to see which was the best fit. Then, we adapted an existing codebase to suit the requirements of the app’s infrastructure.

To continue exploring this app, perhaps you could create a solution to refine the word cycling even further.

As before, the code for this article is available on GitHub.

If you liked this post, follow and share more.

References

What Are Syllables, and How Do You Count Them? | Grammarly

https://strainindex.wordpress.com/2010/03/13/syllable-word-and-sentence-length/

Identification API - ACRCloud

Scrape Music Lyrics from the Genius API with NodeJS (hashnode.dev)

Serverless Functions Overview | Vercel Docs

Genius API

Did you find this article valuable?

Support Wool Doughnut by becoming a sponsor. Any amount is appreciated!