wanessalabs standalone production July 28, 2026

Kalimba Easy

Kalimba tab sites are ad farms with no notion of difficulty. This one rates every song 1 to 5, says why, then listens while you learn it.

ReactTypeScriptViteTailwindZodVitestWeb Audio APICapacitorCloudflare Pages
Sorted by how hard a song actually is on your kalimba, not by how well known it is.
Sorted by how hard a song actually is on your kalimba, not by how well known it is.
Filter by instrument, level, length and mood before committing to learn something.
Filter by instrument, level, length and mood before committing to learn something.
Every song states its level, its tine range, and whether the transcription is verified.
Every song states its level, its tine range, and whether the transcription is verified.
Game mode: search, filter free or paid songs, see 5-star progress and recent scores with their level, then press one big Play button.
Game mode: search, filter free or paid songs, see 5-star progress and recent scores with their level, then press one big Play button.
Notes fall onto the tine you pluck. Play, Tap on screen, or use IRL Listen so the phone hears your physical kalimba.
Notes fall onto the tine you pluck. Play, Tap on screen, or use IRL Listen so the phone hears your physical kalimba.
Best scores stay on the device. The Complete score averages every song, counting unplayed ones as zero.
Best scores stay on the device. The Complete score averages every song, counting unplayed ones as zero.
A Level 4 song, with the reasons that made it one and what the transcription left out.
A Level 4 song, with the reasons that made it one and what the transcription left out.
44 of the 109 songs are paid. Every song still opens and practises for free; the one-time purchase is for the play-along.
44 of the 109 songs are paid. Every song still opens and practises for free; the one-time purchase is for the play-along.
A Level 5 duet: Telemann's canon, two voices from his own score, one thumb each.
A Level 5 duet: Telemann's canon, two voices from his own score, one thumb each.
The Supporter card, dated, on a device that has paid.
The Supporter card, dated, on a device that has paid.

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.

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 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.

Development timeline

39 logged updates over 87 days, 26 Jul 2026 to 20 Oct 2026.

  1. About page in footer referencing Wanessa Labs and clemens.listsof30.comfeature

    The page explains how the difficulty engine and three play-along modes work, documents the public-domain and local-first guarantees, and links to Wanessa Labs (wanessalabs.com) and Clemens Wan (clemens.listsof30.com).

  2. Play, Tap, and IRL Listen modes, full-width mobile tines, and Tap star scoringfeature

    Game mode and the play-along now offer three modes: Play (hear the song play through in time), Tap (each note waits on the line until you tap its tine on the screen, awarding 100 points per hit and counting wrong taps), and IRL Listen…

  3. The portfolio page's "What's next" was out of datefeature

    It still promised "finish the 30 song catalogue, 10 so far" and "confirm the 24 of 30 transcriptions", and offered a 9 and 21-key choice the app already has. The page now lists four items, all still true.

  4. A real buyer was told "Not unlocked yet"feature

    The owner made the first live purchase. Stripe sent them to /unlocked?session_id=cs_live_..., which showed "Page not found" on their phone.

  5. A Supporter mark for buyersfeature

    Asked for: a way to show on "my profile" that the owner is a paying customer. There are no accounts, so the profile is the device.

  6. Beethoven's Fifth goes from fate to victoryfeature

    Asked for: variety in the first song on the list, which was the four-note motif and its answer on a loop, while keeping it at Level 1.

  7. Four more Level 5 songs, mostly duetsfeature

    Asked for: four more two-handed Level 5 songs, past the round 100.

  8. Level tag on Recently played tilesfeature

    Asked for: the difficulty on each Recently played tile, and whether the tiles show the score. They already showed the best score (stars and percent, or "Learn N%" for a Learn run); a run judged only by taps is unscored and shows "Played N…

Show the earlier 31 entries
  1. Project complete; docs, wiki and portfolio page brought up to datedocs

    The owner called the project done. README gains a status line, a second screenshot row (Free / Paid filter, a Level 5 duet, the Supporter card) and the current flag count (14 of 109).

  2. Recover searched with two filters Stripe will not combinefeature

    The new error detail showed the cause at once: "You may only specify one of these parameters: customer_details, payment_link." Restore now lists the email's completed checkouts without the link filter, and paidHere keeps rejecting any…

  3. Recover's error now says which parameter Stripe refusedfeature

    With the right key and payment link, /api/restore still answered 503 with Stripe status 400 invalid_request_error, and the owner found no matching entry in Stripe's request log. Cloudflare's live log tail does not run on the build machine.

  4. Rock roots, second batchfeature

    Asked for: rock songs safe to record. No rock song is public domain (the genre starts in the 1950s), so this batch is traditional songs rock bands later recorded, each from a notation older than any recording.

  5. 100 songs, 40 paid, and a Free / Paid filterfeature

    Asked for: paid songs that are not the most famous and not the first one shown, a "free songs" / "paid songs" filter, a few free Level 5 songs, and 100 songs in all with 40 paid and 60 free across the levels.

  6. 66 songs, longer pieces, and the first Level 5 songsfeature

    Asked for: more bars in the songs, 60 songs, and at least ten songs at Level 5 of 5.

  7. Level 4 songs, and why nothing reaches Level 5feature

    Asked for: songs at Level 4 and 5 of 5. The 17-key topped out at Level 3 (highest raw 46; Level 4 starts at 53, Level 5 at 73).

  8. Listen mode switch, and game rounds of at least 30 secondsfeature

    Asked for: make turning on listening more obvious inside a song, and make each game song last at least 30 seconds.

  9. Paid songs, a $1.99 unlock through Stripefeature

    Asked for: a Stripe payment like the games hub's, $1.99 to unlock. Then, refined by the owner: every song stays visible; about 20 songs, three or four per level, carry a "Paid song" label; a paid song opens and practises for free, and…

  10. Recover failed live; the key needed two more permissionsfeature

    After deploy, /api/unlock answered correctly but /api/restore came back as Cloudflare's own 502 page. The refund check added after the security audit expands payment_intent.latest_charge, which needs Read on PaymentIntents and Charges;…

  11. Deep links fixedfix

    Every directly loaded URL except / was broken in production: /song/... returned a 404 page, and /why-not-here and /my-tabs landed on the catalog.

  12. Deploys now reach phonesfeature

    After the play-along deploy, desktop showed it and the phone did not. The service worker was registered with registerType: 'prompt', and nothing in the app ever showed the prompt.

  13. Full melodies and 15 new songsfeature

    Asked for: more songs, and the existing songs longer.

  14. Game modefeature

    Asked for: something closer to a rhythm game. Scroll and search the songs, press a big Play, and keep scores like a normal mobile game, as a Game mode beside Practice for the whole app.

  15. Microphone non-goal reopenedfeature

    PRD non-goal 4 said no microphone access and no pitch detection. Clemens reversed it for a play-along mode: falling notes that wait until the student plays the right one, and a Play mode that scores hits at tempo.

  16. Mobile practice viewfeature

    The phone sits on a stand and both hands are on the instrument. Four changes follow from that.

  17. Play along, with listeningfeature

    The app moves from "here is a tab" to "play this with me". Notes fall down the lane above their own tine and land on a line drawn just above the diagram, in the thumb colours from the practice view.

  18. Scores overlayfeature

    Asked for: a general score overlay that tracks percent completion per song. "Your scores" on the Game screen opens a full-screen view.

  19. Agentic & Production Audit Remediation (Score: 98/100)docs

    Remediation & Enhancements: - Fixed SPA empty shell trap by pre-rendering semantic HTML fallback (<main>, <h1>, <section>, <footer>) inside #root for instant agent & search engine ingestion prior to React mounting. - Added <link…

  20. Phase 5, bring your own tabfeature

    The human asked for 30 more songs and for the truncated entries to be extended to full length. Both requests ran into the constraint already on the record: I cannot source real notation, and the truncated songs are truncated precisely…

  21. Tine labels on the diagramfeature

    The request was to invert the graphic. The user plays with their thumbs and said the picture reads upside down.

  22. Phase 2, core enginefeature

    Pure TypeScript engine, zero React, zero UI dependencies.

  23. Phase 3 complete, 30 songsfeature

    The human chose "push to 30 now, flags and all" over sourcing transcriptions properly or stopping at 13. Seventeen more encoded from memory with honest flags.

  24. Phase 3, batch 3 and a scaling problemfeature

    Target raised to 30 songs. Batch 3 delivered 3, not 5, and the reason matters more than the count.

  25. Phase 3, catalog batch 1feature

    Five songs encoded, the build gate written and proven, the source format changed. 5 of a planned 30 melodies.

  26. Phase 3, catalog batch 2feature

    Three more of my own Phase 0 bases were wrong. Every one was caught by verifying rather than recalling, which is now three batches running at roughly one bad claim per two entries.

  27. Phase 4a, foundation and Browsefeature

    Phase 4 split in two, because presenting the whole UI as one diff would have been roughly two thousand unreviewable lines. 4a is the app shell plus Browse, which is the screen that IS the product.

  28. Phase 4b, detail, player and ladderfeature

    Song detail, SVG kalimba, player and practice ladder. Also closed the security blocker carried out of 4a.

  29. Phase 6, ship configurationfeature

    Human decided the domain and asked to build through to a publishable state.

  30. Phase 0, auditdocs

    New project proposed from a diff-first, phase-gated brief. Phase 0 was read-only by instruction.

  31. Phase 1, POW docsdocs

    Phase 1 proceeded on the Phase 0 recommendations, which were not overruled. The nine open questions were resolved to their recommended defaults and the assumptions were stated before writing.

Written from this project's decision log as work happened, not afterwards. 1 entry was withheld from publication. See every project →

What's next

4 open items

Plans, not promises. Taken from this project's own roadmap, last updated 16 Oct 2026.