Practice Music- Ever thought practicing music can be rewarding and fun?

Practice Music- Ever thought practicing music can be rewarding and fun?

Music, once admitted to the soul, becomes a sort of spirit, and never dies!

Music soothes us!

But sometimes Musicians practicing it regularly becomes mundane to them. And there aren't much apps available to make practicing more fun and rewarding.

Inspiration

Back then, I had started learning guitar of my interest and it became boredom after sometime when I was learning it alone. I had to stop it for a long time.

Then I realized there has to be something that has to be rewarding for learning or practicing music. There are many apps out in the market which are used to teach but not making practice fun!

I then thought, why can't learning music have features like learning new language where it can be tracked, have a journal of history etc.,.

As I heard about the PlanetScale Hackathon, then it I gave a thought to develop an app which is going to be rewarding, fun and having leader board to make practicing music cheerful for achievements.

Practice Music

  • In simple terms, Practice Music is a website that makes practicing more fun for musicians.
  • Users can be able to start practice sessions, have their practice times tracked, and compete with other users to see who practiced the most, log their music history.
  • This is all intended to make it more rewarding for musicians to practice their instruments.

Features

  • It has GitHub and Google accounts integration, making it simple for users to Sign In.
  • Users can start and track the session and music.
  • Users can view leader board where they stand in terms of learning it daily based on how much time they spent.
  • It has journal features like listening and storing the music via Microphone of computer.

Demo

App Screens

Homepage

signin.PNG

Sign In screen

signin2.PNG

Get Started

signin3.PNG

Start Session

signin4.PNG

Practice Room

sigin5.PNG

Leaderboard

leaderboard.PNG

Tech Stack

  • It is built with React using the Next.js framework for the Front end and TailwindCSS for styling.
  • For database, PlanetScale MySQL database and Prisma as the ORM.

  • It makes use of PlanetScale which is a highly scalable serverless MySQL database platform which supports horizontal sharding and unlimited connections. It takes the headache from developer to make it deployment and go to market easier.

Code Snippets

PlanetScale DB

db1.PNG

Account Schema

db2.PNG

Session and User Schema

db3.PNG

Verification and Authentication Schema

db4.PNG

Prisma Schema

It is the main part which needs to be present under prisma folder of the project which can be automatically migrated to create schema in PlanetScale.

datasource db {
  provider             = "mysql"
  url                  = env("DATABASE_URL")
  //shadowDatabaseUrl    = env("SHADOW_DATABASE_URL")
  referentialIntegrity = "prisma"
}

generator client {
  provider        = "prisma-client-js"
  previewFeatures = ["referentialIntegrity"]
}

model Account {
  id                 String  @id @default(cuid())
  userId             String
  type               String
  provider           String
  providerAccountId  String
  refresh_token      String?
  access_token       String?
  expires_at         Int?
  token_type         String?
  scope              String?
  id_token           String? @db.Text
  session_state      String?
  oauth_token_secret String?
  oauth_token        String?

  user User @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@unique([provider, providerAccountId])
}

model Session {
  id           String   @id @default(cuid())
  sessionToken String   @unique
  userId       String
  expires      DateTime
  user         User     @relation(fields: [userId], references: [id], onDelete: Cascade)
}

model User {
  id            String    @id @default(cuid())
  name          String?
  email         String?   @unique
  emailVerified DateTime?
  image         String?
  accounts      Account[]
  sessions      Session[]
  timeTotal     Int       @default(0)
  parrotColor   String?
}

model VerificationToken {
  identifier String
  token      String   @unique
  expires    DateTime

  @@unique([identifier, token])
}

Refer to this documentation which makes it easier for developers.

Authentication with Google and GitHub

export default NextAuth({
  adapter: PrismaAdapter(prisma),
  providers: [
    GithubProvider({
      clientId: process.env.GITHUB_CLIENT_ID,
      clientSecret: process.env.GITHUB_CLIENT_SECRET,
    }),
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    }),
  ],

Leaderboard

export default Leaderboard;
export const getServerSideProps = async ({ req }) => {
  const data = await getSession({ req });
  let user;
  if (!data) {
    user = null;
  } else {
    user = data.user;
  }

  const TAKE = 40;
  let top = await prisma.user.findMany({
    take: TAKE,
    orderBy: {
      timeTotal: "desc",
    },
    select: {
      email: true,
      name: true,
      timeTotal: true,
      parrotColor: true,
    },
  });
  let userRank = TAKE + 1;
  top = top.map((u, i) => {
    if (user && u.email === user.email) {
      userRank = i + 1;
    }
    const { email, ...rest } = u;
    return { ...rest, rank: i + 1 };
  });
  if (userRank === TAKE + 1 && user) {
    const you = await prisma.user.findUnique({
      where: {
        email: user.email,
      },
    });
    const { email, ...rest } = you;
    top.push({ ...rest, rank: top.length + 1 });
  }
  return {
    props: {
      leaderboard: top,
      userRank: user ? userRank : null,
    },
  };
};

Tracking Practice Session

It asks permission for microphone via Web Audio API access then it listens via it and logs music and session time.

const Practice = () => {
  const { data: session } = useSession();
  const [started, setStarted] = useState(false);
  const canvasRef = useRef(null);
  const [background, setBackground] = useState(null);
  const [audio, setAudio] = useState(null);
  const [mins, setMins] = useState(0);
  const startTime = useRef(null);

  useEffect(() => {
    const beforeTabClose = () => {
      end();
    };
    window.addEventListener("beforeunload", beforeTabClose);
    return () => {
      beforeTabClose();
      window.removeEventListener("beforeunload", beforeTabClose);
    };
  }, []);
  // https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Visualizations_with_Web_Audio_API
  async function getMedia(constraints) {
    let stream;
    try {
      stream = await navigator.mediaDevices.getUserMedia(constraints);
      setAudio(stream);
    } catch (error) {
      console.error(error);
      toast.error("Please allow microphone access on this page.", {
        position: "bottom-left",
        autoClose: 5000,
        hideProgressBar: false,
        closeOnClick: false,
        pauseOnHover: true,
        draggable: true,
        progress: undefined,
      });
      return false;
    }
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
    const analyser = audioCtx.createAnalyser();
    analyser.fftSize = 256;
    const dataArray = new Uint8Array(analyser.frequencyBinCount);
    const source = audioCtx.createMediaStreamSource(stream);
    source.connect(analyser);
    const canvas = canvasRef.current;
    const canvasCtx = canvas.getContext("2d");
    const lastSampled = 0;
    const draw = () => {
      requestAnimationFrame(draw);
      canvasCtx.clearRect(0, 0, canvas.width, canvas.height);
      analyser.getByteFrequencyData(dataArray);
      const barWidth = (canvas.width / dataArray.length) * 2.5;
      let barHeight;
      let x = 0;
      let sum = 0;
      for (let i = 0; i < dataArray.length; i++) {
        sum += dataArray[i];
        barHeight = dataArray[i];
        canvasCtx.fillStyle = `hsl(${(dataArray[i] * 360) / 255}, 70%, 75%)`;
        canvasCtx.strokeStyle = "transparent";
        canvasCtx.fillRect(x, canvas.height - barHeight, barWidth, barHeight);
        x += barWidth + 1;
      }
      if (Date.now() - lastSampled > 1000) {
        lastSampled = Date.now();
        setBackground(sum / dataArray.length);
      }
    };
    draw();
    return true;

Learning and Experiences

  • Overall, it was a awe struck learning experience to build something for musicians with PlanetScale.
  • Apart from it, I learnt a lot about Prisma and PlanetScale DB.
  • Integration components with Next.js
  • Of course, debugging bugs and using Google to fix them helped me learn a lot about other things.

Source Code

Have a look at the code repo

Thank you

I would like to extend my sincere thanks and gratitude to Hashnode team for conducting this hackathon in collaboration with PlanetScale.

I learnt a lot about PlanetScale and it is my first blog on Hashnode.

Wish me luck guys! :D

About Me

I am a Senior Data Scientist at Oracle.

LinkedIn