Search for kalimba tabs and you get an ad-choked SEO farm with an unsorted pile of songs and no notion of which are hard. A nursery rhyme and a two-octave chromatic pop arrangement sit in the same list, formatted identically. A beginner who bought a 17-key kalimba three weeks ago wants to know one thing: what can I actually play tonight.
The Rating Is the Product
The catalog is the demo. The thing being built is a difficulty engine that can say:
Level 1 of 5. No leap larger than a third, fits inside 5 tines, unhurried at tempo, 30 notes, one repeated phrase.
That sentence is generated, not written. Nine factors are measured, each emits its weighted contribution, and the explainer renders the two or three that actually drove the score. The engine is a pure function with no clock, no randomness and no I/O, so the level computed at build time and the level computed in the browser cannot disagree.
The Instrument Has Opinions
A C-major kalimba puts its longest, lowest tine in the centre and alternates ascending pitches outward, left then right. Two consequences fall out of that, and they reshaped the whole model.
Each side is a chain of thirds. A scale step is one tine away on the opposite side, so stepwise melodies alternate thumbs and feel easy. A third is one tine away on the same side. Which means a triad is three consecutive tines under one thumb and rolls easily, the opposite of piano intuition. The original difficulty rubric had a rule penalising chords; measuring the physics showed it was wrong, and the rule became “penalise a gap in one thumb’s run” instead.
The layout fans outward, so reach depends on register. C4 to G4 hugs the centre across 4 tines. The same five-note shape an octave up straddles both outer edges across 10. That correction split one factor into two.
There are also melodies the instrument simply cannot play. Seven notes, no sharps or flats, so a piece only works if some transposition puts every note on a real tine. Für Elise alternates E and D sharp and there is no D sharp. The app says so on a dedicated page rather than quietly altering the tune and calling it the piece, which is what the tab farms do.
Copyright Is the Governing Constraint
A kalimba tab of a copyrighted song is a derivative of the composition. So every melody shipped is public domain, and every one carries a provenance block: composition title, composer, year, the legal basis, and a source URL you can check.
This is enforced by a build script that breaks the build, with no override flag. Flip verified to false on any song and the build stops with no catalog written. The Song schema is Zod strict with no lyrics field, so lyric text cannot ship by accident even for public-domain songs, because many carry copyrighted lyric translations.
Doing the research properly corrected five claims that had felt obviously true:
- The Mary Had a Little Lamb tune is not Lowell Mason’s 1831 setting. It is the chorus of E.P. Christy’s “Goodnight, Ladies”, attached to those words in 1868.
- Row, Row, Row Your Boat first appeared in 1852 with a completely different melody. The familiar tune is credited to Eliphalet Oram Lyte in 1881.
- London Bridge appears in a 1744 songbook, but the tune everyone sings first shows up in 1879.
- We Wish You a Merry Christmas was excluded outright. Arthur Warrell was awarded copyright on his arrangement in 1939, and the version everyone knows was unknown outside the West Country before he published it.
The pattern is worth naming: the famous publication and the familiar tune are routinely different events, decades apart. A date that looks like provenance often is not.
Asked for rock songs, the catalog applied the same rule. No rock song is in the public domain yet, so the catalog carries traditional songs that rock bands later recorded, each from a notation older than any recording: Drunken Sailor from a 1921 shanty book, The Maid Freed from the Gallows from a tune collected in 1902. House of the Rising Sun was dropped, because the familiar tune is not written down anywhere older than a collection still under copyright.
Note Names, Not Tine Numbers
Song files store C4 C4 G4 G4 A4 A4 G4, not 8 8 10 10 5 5 10. The build derives tine indices at compile time. Two reasons, and the second matters more.
A tine index is only meaningful against one layout, so if the physical tine order turns out to be wrong, note names re-derive and indices would all be garbage. And a human can check C4 C4 G4 G4 by humming it. Nobody can check a row of tine numbers, and a review gate nobody can perform is decoration.
Honest About What Is Not Verified
The catalog launched with thirty melodies, six of them transcription-confident. It now holds 109, each transcribed from a cited source, almost always a published score. Fourteen still carry a flag for a specific doubt about the notes, such as a tune whose only fetchable source was a forum post, and that flag sits on the browse card in plain sight, not behind a tap. The physical tine layout itself has not yet been confirmed against a real instrument, and that is recorded in the code, the roadmap and the app.
An app whose entire pitch is that it tells you the truth about difficulty does not get to be coy about its own uncertainty.
Bring Your Own Tab
The catalog can only ever contain what is legally free, which is the wrong shape for the songs people actually want. So the app accepts pasted tabs. Any kalimba number tab gets the same rating, the same generated explanation, the same best-fit transposition and the same player, and it never leaves the device.
The parser is permissive on purpose. It reads what it can and reports what it could not, token by token, rather than rejecting a whole paste over one stray character. Somebody copying from a video description should not be punished for a typo.
From Choosing a Song to Playing It
A rating tells you what to learn. It does not help you learn it. So the app grew a play-along: notes fall down a lane above their own tine and land on a line just above the kalimba drawing, coloured by which thumb plays them.
Three play styles sit at the top of every song card and inside the play-along header: Play sounds the full song in time, Tap holds each note on the line until you tap its tine on the screen, and IRL Listen waits for you to pluck the note on a physical kalimba. On mobile, the falling-note highway runs three beats tall (45 SVG units above a 62-unit diagram) so preserveAspectRatio="xMidYMid meet" keeps the SVG width-bound rather than height-bound and stretches the seventeen tines across the full phone width, with full-column gap-spanning hit targets so narrow tines are easy to tap cleanly. The screen stays awake, a Bluetooth page-turner pedal can advance a waiting note, and the view also works in landscape.
IRL Listen hears the instrument, on the device. The first attempt used a standard pitch tracker and failed on the thing a kalimba does most: tines keep ringing, so a new pluck lands on top of two or three older notes and the tracker hears the blend. A D over a ringing E read as D sharp. The fix uses what the app already knows. A kalimba can only make the notes on its tines, so for each tine the listener measures how much energy appeared at that pitch at the moment of the pluck. A note still ringing from before shows no increase and drops out. Audio never leaves the phone; nothing is recorded or sent.
Game mode turns the catalog into a song picker with search, the Play / Tap / IRL Listen selector, 5-star progress on every song card, total stars earned across the catalog, and one big Play button. In both Tap and IRL Listen, each hit awards 100 points and updates a live points, percentage and star badge in the header as you play, while wrong taps or wrong plucks count against the final percentage and 1-to-5 star rating. Only the “Played it” skip button or pedal advances unscored. A song shorter than thirty seconds plays through again, the way you would sing another verse, rather than having melody invented for it.
Until October 2026 nothing in the catalog rated above Level 3, so seven fast fiddle tunes and classical openings were added at Level 4. Level 5 needed a rule change. The engine gives weight to playing several tines at once, and a single melody line never does, so no melody can reach it. Rather than retune the engine to flatter the catalog, songs were allowed a second voice on one condition: it has to be the composer’s own, from the same score. Invented harmony stays out.
Few pieces qualify. Almost every keyboard piece in a sweep of about five hundred changed key within a few bars, and a C kalimba has no sharps or flats. The pieces that stay in key spread wider than seventeen tines, and moving the left hand up to fit makes the hands cross. Duets did better: two flutes, two bassoons or two violins already share one register, and baroque duets stay in key for long stretches. Seven songs now reach Level 5: three keyboard pieces and four duets.
Paying for It Without an Account
44 of the 109 songs are paid: about four in ten at every level, never the best-known tunes, and never the first song on the list, so a new player meets free songs first. A paid song still opens and practises for free. The one-time $1.99 purchase covers the play-along.
There are no accounts, so the device is the profile. Payment is a Stripe Payment Link. Stripe sends the buyer back with a checkout id, and a small Cloudflare Pages Function asks Stripe whether that checkout was paid, for this product, and not refunded. Nothing is stored server side. A second Function restores the purchase on another device from the email on the receipt. A device that has paid shows a Supporter mark and a dated thank-you.
The first real purchase found three faults that a green test suite had not. The stored payment-link setting held the wrong id, so a paid checkout did not match. A key in the wrong mode made the page tell a paying buyer they had not paid; it now reports a failed check and says the payment is safe. And the email restore searched by email and payment link together, a pair Stripe refuses. The unit tests had pinned that exact query against a stubbed Stripe, so they passed while the feature could never work. What caught it was a live request with a dummy email, the only kind of test that can see a rule Stripe alone enforces.
The Build
Vite, React, TypeScript and Tailwind on Cloudflare Pages, with Capacitor for a native shell. Zod validates the catalog at build time and is then deliberately kept out of the browser bundle, because the client validates nothing at runtime. That alone saved 67 kB.
React Router was removed. Every published version carries a high-severity advisory with no clean gap between them, and the standing rule is that security failures block deploys and get fixed at the source rather than worked around. The app used four of its APIs, so it got about ninety lines of routing with a pure, tested path matcher instead. npm audit now reports zero vulnerabilities.
Audio is synthesised rather than sampled: five partials with per-partial decay through the Web Audio API. No sample licence to clear, no asset weight, and the code says plainly that it is a plausible pluck rather than a measured model of any real instrument.
The browser makes zero third-party requests until you choose to pay. No ads, no accounts, no analytics, no fonts CDN. The only server code is the two payment Functions, which talk to Stripe from Cloudflare, so the content security policy still has an empty allowlist. Fonts are self-hosted as woff2. The full catalog is bundled, so it works offline after first load, and service worker registration is skipped on native where the assets are already local.
305 tests, every difficulty factor isolated by its own synthetic fixture, and a suite that pins the rendered explanation sentences character for character. That last one exists because two copy defects survived a fully green test run and were only caught by reading real output.









