Skip to content
← All writing

Sheet Guide: learning to read sheet music by playing it

  • Python
  • Flask
  • Web Audio
  • DSP
  • SQLAlchemy

Source →

Where this came from

I wanted to learn to play songs on the flute. Every song I found was written as sheet music. I can’t read sheet music.

That’s the entire problem, and it’s a more annoying one than it sounds. The gap isn’t skill — I could produce the notes on the instrument fine. The gap is a lookup table living in someone else’s head: a dot sitting on the second line from the bottom means G, and until you’ve internalised a few dozen of those mappings, every piece of sheet music in the world is a locked door.

The standard advice is to fix that first. Learn the notation, work through the theory, then play the music you wanted to play. Which means months of studying a symbol system before you’re allowed to touch the thing that made you interested in the first place. I understand why it’s taught that way. I just wasn’t going to do it.

So I inverted it. Instead of learning notation and then playing, play — and let the instrument teach you the notation.

What that means in practice

Sheet Guide shows you real sheet music with the note name printed under each notehead. You play what you think it says. It listens, and it will not move on until you get it right.

That loop does something a textbook can’t. You see the dot, you see the letter, you find the fingering, you hear the note, and the app confirms it — four representations of the same thing arriving together, over and over. The association forms because you keep using it, not because you memorised a chart. After enough repetitions the letter underneath stops being the thing you read, and then you don’t need it.

It’s the same reason nobody learns a language from a dictionary.

Why it ignores rhythm

Refusing to score timing is the decision that makes the rest work.

Reading pitch and playing in time are two separate skills, and conventional practice software grades them simultaneously. For someone who can already read music, that’s fine — it’s the realistic test. For someone who can’t, it’s fatal: you play a wrong note late, get one red mark, and learn nothing about which failure was which.

Stripping out rhythm entirely leaves exactly one question — is this the right note, yes or no? — and one question is something a beginner can actually act on. Timing is a real skill and I’ll need it eventually. It just isn’t the door I was locked out of.

What it does

  • Renders standard sheet music for a chosen instrument and song
  • Shows the note name (A–G) under each notehead
  • Listens through the microphone in real time
  • Turns the note green and advances when you play it correctly
  • Stays put until you do — there is no skipping
  • Beginner mode enlarges the note names and adds a keyboard/fretboard hint

How it works

The split is deliberate: the browser does the listening, the server does the bookkeeping.

In the browser, pitchy runs autocorrelation over the mic stream to get a fundamental frequency, which maps to a note. VexFlow renders the staff. A small state machine holds the current position and only advances when the detected note matches the expected one.

On the server, Flask 3 exposes songs, instruments, and practice sessions as blueprints, with the real logic in a services layer:

ServiceResponsibility
MusicParserTurns MusicXML and ABC files into a note sequence via music21
PracticeEngineOwns session state and what counts as “next”
NoteVerifierDecides whether a detected pitch matches the expected note

SQLAlchemy 2 models back it: instrument, song, song_note, practice_session, session_attempt. Storing individual attempts — not just completed sessions — is what makes “which notes does this person keep missing” answerable later.

Backend      Python 3.11+, Flask 3.x, SQLAlchemy 2.x, marshmallow
Database     SQLite in development, PostgreSQL in production
Parsing      music21
Frontend     Vanilla HTML/CSS/JS — no framework
Notation     VexFlow 4.x
Pitch        pitchy, browser-side autocorrelation

The decision I’d defend

No audio leaves the machine. Pitch detection could have run server-side — it would have been easier to iterate on, and I’d have had real recordings to debug against. Doing it in the browser meant giving that up.

It was still the right call, for two reasons. The obvious one is privacy: a practice tool that streams your microphone to a server is asking for trust it hasn’t earned, and “we don’t record” is a much weaker promise than “there is nothing to record.” The less obvious one is latency. Note verification is a feedback loop with a human in it — the gap between playing a note and seeing it turn green has to feel immediate, and a network round trip per detection would have put a hard floor under that.

Choosing vanilla JS on the frontend follows from the same instinct. The interactive surface is one staff and one state machine; a framework would have added a build step and a dependency tree to manage roughly six DOM updates.

What it deliberately doesn’t do

The non-goals are written down, because on a project like this the failure mode isn’t building the wrong thing — it’s building six more things: no rhythm evaluation, no accounts, no audio storage, no progress gamification.

Every one of those is a feature someone would genuinely want, and every one turns a tool that solves my problem into a music-education platform that solves nobody’s yet. The scope stayed at the size of the thing that was actually blocking me.