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
Sign In screen
Get Started
Start Session
Practice Room
Leaderboard
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
Account Schema
Session and User Schema
Verification and Authentication Schema
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.