Inspiration

Ear training is the most useful skill a musician can have and the one almost nobody practises. Not because it is hard to start — because every tool that teaches it opens on a grid reading m2 M2 m3 M3 P4 TT P5. A self-taught player who already feels behind takes one look at that and leaves.

That is a design failure, not a knowledge failure. The people who need this already know they should train. What stops them is friction and a quiet embarrassment about their own ear, and no amount of explaining fixes either.

So the brief was narrow: a player must be able to answer the first question before understanding anything, and no word on screen may be something they have to look up before they can play. The interval names are still there — a serious student would find their absence insulting — but each one carries a mark you recognise, so the word stops being the only handle.

What it does

You hear a reference note, a short gap, then a second note. You tap the distance you heard. The whole question takes about six seconds, and the first ones ask nothing harder than "higher or lower?".

Then three things make it a game rather than an exercise:

The reveal. While the notes sound, the screen shows nothing that encodes pitch — a player who mutes the phone must be unable to answer. The moment you answer, the two notes are drawn in their true relationship and resolve into a short musical phrase built on that interval, so you hear it doing something musical instead of sitting in a vacuum. On a miss, the answer you chose plays back to back with the right one: the confusion is heard as a contrast, not explained in words.

Rivals. Miss the same interval three times in your last thirty questions and it becomes a rival — it gets a mark, a face, and it starts turning up more often. Answer it right three times in a row and the run turns into a duel: five questions, that interval against its closest confusion. Win and it is beaten permanently. Lose and nothing is taken away — an opponent, never an accusation.

One number. An Ear Rating that starts near 800 and is always led by a band name and a plain sentence about what you can currently do: "you're telling big leaps from small ones". The integer is second, and smaller.

Free gets the whole game: unlimited runs, the daily challenge, every rival, every duel. Pro is the coaching on top — The Session, a daily set built from the pairs you confuse, a weekly target, and the evidence it is working.

How I built it

Flutter, portrait-only, offline-first, no accounts, no backend.

The app came out of a Claude Design handoff — a high-fidelity design document with thirteen screens, which became a ThemeExtension and thirteen matching Dart screens. Some specifics worth naming:

  • The pitch constraint is enforced structurally, not by review. The during-note visual is driven by an EnergyEnvelope built from the note onsets the app itself schedules; the painter that draws it takes no pitch parameter at all, so there is nothing to leak. A test renders the visual for two different intervals and compares raw pixels. If that test fails it is a P0 bug.
  • One painter is the whole identity system. IntervalPainter draws two dots on a twelve-step track with a bend parameter: at 0 it is the flat glyph on an answer button, at 1 it closes into the rival's ring. Animating bend is the transition between the two. Each interval therefore has a mark you recognise instead of a label you parse — which is the whole point, since reading "perfect fifth" against "tritone" is a reading task, and reading is not the skill being trained. The launcher and notification icons are the same geometry, redrawn by a script rather than by the painter: they have to exist as files before the app runs, and at 40px and 24dp they need weights the painter has no reason to carry.
  • The audio set is generated and committed. A standalone Python script writes the note and phrase WAVs, so the app ships with its sound rather than synthesising at runtime.
  • Material is deliberately off. No ink splashes, no stock transitions. Taps are 160 ms AnimatedContainers, navigation is a cross-fade. An app about listening should not feel like a form.
  • RevenueCat powers Pro, and the paywall reads prices, trial length and the annual saving from the offering rather than hardcoding them — an introductory price only counts as a trial when it is actually free. The rewarded video is the one ad at launch: the extra life after your third miss, once per run, taken only when the player asks for it.

Every change shipped through a gated pipeline: plan, plan review, a worktree and a PR, then cross-review by other agents, then a human merge. A local pre-push gate runs format, analyze, its own tests and the unit suite in about twelve seconds, and there is no environment variable that turns it off.

Challenges I ran into

Not leaking the answer. The first version of the design revealed the interval while the notes were still sounding — a beautiful visualisation that quietly destroyed the exercise. Fixing it properly meant removing pitch from the painter's inputs, because a rule that lives in a code review is a rule that comes back.

Making a low number not feel like a verdict. An Ear Rating arriving straight after a placement test reads as a judgement to exactly the person this app is for. So placement shows no number at all; the rating appears at the end of the first real run, led by the band name and a plain description.

Tone on the rival system. "Killed three times by the tritone" delights a musician and insults a lapsed learner. Rivals speak plainly, the framing is always an opponent you are hunting, and putting one on a share card is opt-in.

The subscription had to be worth buying on day one. An earlier draft sold "the pairs you confuse and how that changed" on the paywall while deferring the screen that shows it. That is an empty promise to anyone who subscribes on day one, so the confused-pairs proof ships inside The Session in v1.

Accomplishments that I'm proud of

  • The reveal. It is the moment the app earns its keep, and it holds up.
  • A constraint enforced by a pixel-diff test instead of a comment.
  • Thirteen screens, the full core loop, rivals, duels, the daily, the share card export, the paywall and the rewarded video — with the end-to-end flows passing on a real device.
  • No dark patterns anywhere in the money: no fake countdowns, nothing pre-ticked, free and paid stated plainly, and unlimited free play forever.

What I learned

That the hard part of an ear-training app is not the audio. It is deciding what the screen is allowed to know.

Also: a design document detailed enough to be implemented directly is worth more than a prototype. Two review rounds against the brief killed four real product faults before a line of Dart existed, and each of those would have been a rewrite later.

What's next for Two Notes

  • The between-run ad, capped and held back deliberately, so a free player is worth something without sitting between the player and the "Again" button.
  • Bespoke phrases for the remaining intervals — six ship at launch, the rest fall back to a plain resolve, and adding them changes no timing or layout.
  • The standalone progress screen and the ledger reveal, both named as cuts in the brief rather than forgotten.
  • A real daily endpoint, so the day's ten come from a service instead of being derived from the day code.

Built With

  • dart
  • flutter
  • google-mobile-ads
  • maestro
  • purchases-flutter
  • python
  • revenuecat
  • shared-preferences
  • sqflite
Share this project:

Updates

Submission history