BlogProjectsVideosMusic
|
Muscii
duolingo for the language of music
2024/08/28
link

I joined a band.

As the resident pianist, keyboardist, and synthesist of this new band, I very quickly realised that my sight-reading abilities were not up to scratch. I would not be able to pick up new songs quickly enough to keep up if I continued having to spend so long reading the sheet music.

During lockdown I started learning French on popular language app Duolingo. I was charmed by the streak feature and found myself returning every single day for a short 2-minute lesson. It worked well enough that I kept it up for around 2 years lol.

Je joue un peu du piano ici ou là. J'aime aussi créer des applications mobiles.

Le choix est simple.

I decided to build a duolingo clone in flutter in an attempt to make me practice sight-reading. The frontend, the backend, everything.


The Frontend.

I have been doing a lot with Flutter at work, so I thought it was time to exercise my learnings and drop the shackles of having to maintain an old rusty codebase. The UI in Flutter is just a breeze if I'm honest. As a self-proclaimed flex-boxer, once you learn that everything goes in a Column or a Row or a Stack it never really gets more complicated

Don't ask about accessibility, though

One thing I learned extremely quickly is that there is enough much screen real-estate for an 88-key piano mockup. I opted for a single octave with 12 keys; 5 black and 7 white.

The next goal was to render a music staff with a note or collection of notes on it so the user can match it on the provided keys. After a little bit of research - and theft - I found that other sites like MuseScore actually use Scaled Vector Graphics (SVGs). Now as a long time user of the website, this made a lot of sense. Their "Export to PDF" function was and continues to be really slow and bad. Another thing I stumbled across was LilyPond, which is a CLI tool for generating music notation. Fortunately, they also export to SVG.

SVGs are just XML objects with extra steps...
Flutter has an SVG renderer that can take a string parameter... Perhaps I can generate every single music staff ever using just SVG shapes and changing the values...

*evil laugh*

Here is a function that calculates how many extra lines a note above the stave needs. It's quite cursed, as is all of the SVG rendering logic that I wrote...

1String buildStaff({num offset = 6.5}) { 2 3 const topLine = 4; 4 const bottomLine = 8; 5 String extraLines = ""; 6 7 List<int> guidelines = []; 8 if (offset < topLine - 0.5) { 9 guidelines = List.generate(topLine - offset.ceil(), (i) => topLine - i - 1); 10 } else if (offset > bottomLine + 0.5) { 11 guidelines = List.generate(offset.floor() - bottomLine, (i) => i + bottomLine + 1); 12 } 13 14 for (final i in guidelines) { 15 extraLines += buildGuideline(i); 16 } 17 18 return """ 19 <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.2" width="20.00px" height="20.00px" viewBox="0 0 12 12"> 20 ... 21 $extraLines 22 </svg> 23 """; 24}

but hey it works, and I won 't need to refactor it for a while.

Now I need some data to pull into the frontend so that I can actually play the game.

Cue...


The Backend.

For some godforsaken reason I convinced myself that I had to start with rolling my own authentication solution.

This is a bad idea.

But I learned a lot about best - and worst - practices of implementing the OAuth model; complete with access tokens, refresh tokens, subscription keys, permission levels, etc... I was using Elysia which is a HTTP server express-like package for Bun, and they had clearly thought of all this before. Using Bun's built-in library for SQLite and wrapping that in Drizzle's type-safety, I got a really good handle on the different integration layers that I had seen in other applications before, and more importantly, why it was useful to separate these layers.

I had an authController, an authModel, an authService, and an authPlugin; all of which were interacting with one another and different parts of the application. Don't get me wrong this was nearly a project-ending challenge. But I made it though the other side with a pretty good understanding of how to implement this again.

Now, I'd also like to say that I brought my Zig learnings from last project with me into this one. Namely, the test-driven development, and using assertions to fail early when the function is called.

I only used the latter... Writing tests was not on the list for this project. Due to tests not being fun and my tendency to abandon projects once they stop being fun. So assertions and runtime errors it is. Next time I'd have liked to write the backend in Dart so that I can import the same packages into both frontend and backend. Or I could use ReactNative for the frontend, but I have zero experience with that and also zero incentive to get better at it.


General Takeaways

This has definitely invigorated me to create more mobile apps. Not so much native apps though, Swift and Kotlin seem to have too many idiosyncracies for only compiling to only half the mobile devices. However, if I want to make money I'd better pick something to specialize in eventually.

More projects