v0.4.0Native ESM · no runtime dependencies · ukulele & guitar

Turn a score into music your hands can play

on a high-G ukulele on a six-string guitar without dropping a note at the tempo you choose for the player you are

scorify finds a string, a fret and a finger for every note of a score, and checks every result again before returning it. It keeps sounding pitch, note spelling and exact musical time. When a tune can't be played under the limits you set, it tells you whether it proved that or ran out of search budget. Then it can strum and play the result with a plucked-string synthesizer.

Built by Aron Homberg (kyr0) (opens in a new tab)

Exact musical time

Onsets and durations are rational quarter notes, so q(1, 3) is an exact triplet. Rests, ties, holds, tempo, meter and key maps all keep their source identities.

Fingerings, not just frets

Four fretting fingers, open strings, barres, held notes and hand shifts. A pitch that fits on a string is not yet a playable fingering, so scorify checks both.

Editable difficulty levels

Seven levels, from baby to maestro, set limits on fret span, shift speed, attacks per second and barres. You can override each limit.

Honest results

infeasible means the search proved there is no solution within the model. search-exhausted means it ran out of budget first. optimality says whether the result is proven best or only the best one found.

Plucked-string sound

Optional waveguide synthesis with seeded humanization and strums in physical string order, including high-G. Plays through an AudioWorklet or renders offline to WAV.

Plain modules

TypeScript source, browser-native ESM and explicit subpath exports. The core has no DOM. Searches can run in a module worker, and starting a new search cancels the previous one.

Keep every note.

Find every finger.

Hear every string.

Live playground

🎸 Seven difficulties, eight instruments

Pick a song, a scale or one of the Billboard charts' most common chord progressions, then an instrument and a difficulty. The sample accompaniments have seven versions, from baby to maestro; imported scores use their available editions and show vocal pitches and lyrics when present. Eight public-domain songs, from Amazing Grace to Happy Birthday, come with every verse of their words under the tab, strummed up to semi-pro and picked above. scorify fingers the accompaniment for the chosen instrument (concert ukulele with high or low G, baritone ukulele, guitar in standard or drop D, 7- and 8-string guitar, mandolin) and checks that it plays.

Concert ukulele · high G

Somewhere Over the Rainbow

The ukulele progression everyone knows, strummed with the island pattern on a high-G concert uke.

Preparing…D · D U · U D U
♪ 0 cents – Hz Press Listen

Tune first

Concert ukulele · high G

scorify listens through your microphone, finds the pitch in an AudioWorklet with YIN and picks the nearest string of the instrument below. Play one string at a time and let it ring; the needle turns green within 5 cents. Pick a string to tune only that one.

Loading the arrangement engine…

Custom mode

Set what the playground otherwise chooses, then re-compute. Empty fields keep the chosen difficulty's defaults, shown in grey. Custom settings stay in force for every song, instrument and difficulty until Reset. Results are searched in your browser and cached there; Export saves one as a file, Save keeps it in this browser.

Arrangement
Instrument
Player limits
When simplifying, scorify may
Search
Playback
Saved arrangements

    Nothing saved in this browser yet.

    Your arrangement Bar 1 / 9 4/4 100%
    On the neck filled: sounding · outlined: next
    Voicings as played x = string not played

    Hand-authored versions: chord charts and rhythm patterns. House of the Rising Sun's tune and words are traditional; no other melody or lyrics are transcribed.

    Tab, neck and chord charts: defuss-shadcn Fretboard (opens in a new tab)

    How it works

    💡 From musical intent to a playable path

    You describe the music. scorify searches note positions and fingerings together, under the instrument and player limits you choose. Only a path that passes an independent replay check is returned. Musical fidelity always ranks first; comfort only decides between equally faithful arrangements.

    1. Describe

      createScore(notes, maps) takes sounding pitches, exact rational time, voices, roles, rests and ties. Each note keeps its id, so every later result can point back to the note it came from.

    2. Transform

      Transpose by a spelled interval such as 2M and scale or set the tempo. Note spelling, key and chord symbols are transposed with it. Every change is logged as an edit, with its reason and the permission that allowed it.

    3. Search

      At every attack, list the string positions and four-finger assignments. Then search for a path through them, using beam search, exact dynamic programming or an exhaustive reference. Strings stay in physical order, and the capo is part of the model.

    4. Validate

      A separate validator rebuilds the sounding notes from the output. It checks every attack, hold, finger contact, player limit and transition before arrange() returns success.

    5. Perform

      createPerformance() adds strums and seeded humanization, then checks the timing against the chosen fingering. If the timing doesn't fit, it reduces the timing variation or reports the problem. It never changes the fingering to make playback work.

    Every result says what it guarantees

    result.status separates a proof from a budget limit. A partial path is never returned as a success.

    success
    Output score, fingerings, edits, source mapping, difficulty components and search statistics.
    infeasible
    The search proved that no path exists within this finite model.
    search-exhausted
    Pruning or a resource limit stopped the search first. A solution may still exist.
    cancelled
    An AbortSignal or a newer worker request stopped the search.
    unsupported
    A note asks for a technique v0.2 does not model, such as a bend or a harmonic.
    optimality
    proven-in-model only when nothing was pruned; otherwise best-found.
    API

    📚 Plain data in, plain data out

    Scores, options and results are serializable objects. Root exports cover the score and arrangement API. Workers and audio come from explicit subpaths, so audio code stays out of the core module graph. The full contract is in the implementation specification.

    FunctionReturnsSemantics
    createScore(notes, options?)ScoreValidates and normalizes notes, rests and maps. Defaults to 120 quarter BPM, 4/4 and A=440. A key is never inferred.
    parsePitch("F#3"), q(n, d)Pitch, QA spelled sounding pitch, and an exact rational time in quarter notes.
    arrange(score, options)ArrangeResultJoint edit and fingering search, followed by an independent replay check.
    arrangeAsync(score, options, hooks?)PromiseCooperative scheduling with signal, onProgress and yieldEvery.
    createWorkerClient(worker)clientarrange() in a module worker. A newer request cancels the previous one, and stale replies are ignored.
    createArrangementSession(score)sessionRecomputes from the original score each time, with an LRU cache of results.
    transformScore(score, options){ score, edits }Spelled transposition and tempo scaling, with no instrument involved.
    analyzeHarmony(score)spansChord candidates per window, including arpeggios. Sparse evidence stays unknown.
    quantizePerformance(notes, maps)resultQuantizes timed performance notes against a known tempo and meter map.
    validateFrettedArrangement(…)reportThe public replay validator, for checking any fretted output.
    createPerformance(result, instrument, options?)PerformanceResultStrums, humanization and timing validation, run before any synthesis.
    createAudioPlayer(context), renderPerformance(plan)player, samplesAudioWorklet playback and offline rendering. The worklet and offline paths produce matching samples. encodeWav() writes the bytes.
    Install

    🧑‍💻 Add it to your app

    Install scorify from npm, or load it from a CDN in a page with no bundler. The score-import skill installs into Claude Code or Codex with one command.

    From npm

    scorify (opens in a new tab) is plain ESM with TypeScript declarations and no runtime dependencies. Bundlers and Node resolve import … from "scorify" and its subpaths, such as scorify/worker and scorify/audio.

    bun add scorify
    npm install scorify
    pnpm add scorify
    yarn add scorify

    To work on scorify itself, clone the repository (opens in a new tab) and run bun install --frozen-lockfile && bun run check (Bun 1.3.9, Node 24).


    In the browser, no bundler

    An import map loads the modules from a CDN, and each entry fetches its shared chunks from there. A Worker or an AudioWorklet must come from your page's own origin: copy the whole dist/ folder of the package next to your page for those, and pass workletUrl to the audio player. This page loads its own copy of dist/ the same way.

    index.html
    <script type="importmap">{ "imports": {  "scorify": "https://cdn.jsdelivr.net/npm/scorify@0.4.0/dist/index.mjs",  "scorify/worker": "https://cdn.jsdelivr.net/npm/scorify@0.4.0/dist/worker.mjs",  "scorify/audio": "https://cdn.jsdelivr.net/npm/scorify@0.4.0/dist/audio.mjs"} }</script><script type="module">  import { createScore, arrange, concertUkulele } from "scorify";</script>

    Use it

    Pitches are sounding pitches and time is counted in quarter notes. Check result.status before you read a fingering, and treat infeasible and search-exhausted as different answers.

    import { createScore, parsePitch, q, arrange, concertUkulele } from "scorify"; // A C major chord held for one 4/4 bar, as sounding pitches in exact quarter-note time.const notes = ["G4", "C4", "E4", "C5"].map((pitch, i) => ({  id: `c-${i}`, pitch: parsePitch(pitch), onset: q(0), duration: q(4),  partId: "uke", voiceId: "chords", role: "harmony",}));const score = createScore(notes, { tempoMap: [{ at: q(0), quarterBpm: 80 }] }); const result = arrange(score, {  instrument: concertUkulele(),   // G4 C4 E4 A4, high G  mode: "exact",                  // keep every pitch, onset and hold  player: { level: "beginner", overrides: { rightHand: "strum" } }, // one strum}); if (result.status === "success") {  console.log(result.annotations[0].shape);       // "0-0-0-3"  console.log(result.annotations[0].assignments); // string, fret and finger per note} else {  console.log(result.status, result.diagnostics);}
    import { arrange, classicalGuitar } from "scorify"; const result = arrange(score, {  instrument: classicalGuitar({ capoFret: 2 }), // sounding pitch stays, frets move  mode: "adaptive",  transform: { interval: "2M", tempoScale: 0.75 },  player: { level: "amateur", overrides: { allowBarre: false } },  adaptation: {    preserveMelody: true,           // melody notes are never dropped    allowOctaveShift: true,         // revoice notes by an octave    allowAccompanimentOmission: true, // may drop notes marked bass/harmony  },}); if (result.status === "success") {  for (const edit of result.edits) console.log(edit.kind, edit.reason, edit.permission);  console.log(result.optimality); // "proven-in-model" or "best-found"}
    import { createWorkerClient } from "scorify/worker";import { concertUkulele } from "scorify"; // Serve worker-entry.mjs from your own origin, next to its chunks.const worker = new Worker(new URL("./scorify/worker-entry.mjs", import.meta.url), { type: "module" });const client = createWorkerClient(worker); const result = await client.arrange(score, {  instrument: concertUkulele(), mode: "exact", player: { level: "pro" },}, (progress) => console.log(progress)); // A newer client.arrange() cancels this one; its stale reply is ignored.// Options must be serializable: call client.cancel() rather than passing an AbortSignal.client.dispose();worker.terminate();
    import { createPerformance, createAudioPlayer, renderPerformance, encodeWav } from "scorify/audio"; // Use a successful result and the instrument that produced it.const performed = createPerformance(result, instrument, {  seed: "take-1",                                  // repeatable excitation and timing  humanize: 0.4,  strum: { direction: "down", spreadSeconds: 0.03 }, // false to pick});if (performed.status !== "success") throw new Error(performed.diagnostics.join("; ")); const context = new AudioContext();const player = await createAudioPlayer(context);playButton.onclick = async () => {           // start audio from a user gesture  const playback = await player.play(performed.performance);  console.log(await playback.finished);}; // Or render offline: the same samples as the worklet.const wav = encodeWav(renderPerformance(performed.performance));
    // Show the solver's strings and frets in a defuss-shadcn Fretboard tab (4/4).const beats = (t) => t[0] / t[1]; // q(n, d) is [n, d] quarter notesconst items = result.annotations.flatMap((a) => {  const struck = a.assignments.filter((p) => p.noteIds.some((id) => a.attackIds.includes(id)));  if (!struck.length) return [];  const at = beats(a.at);  return [{ measure: Math.floor(at / 4) + 1, beat: (at % 4) + 1,    notes: struck.map((p) => ({ string: p.stringNumber, fret: p.relativeFret })) }];}); document.querySelector("#tab").api.setState("default", { data: {  version: 1, tuning: ["G", "C", "E", "A"],  scores: [{ id: "uke", tracks: [{ id: "tab", kind: "tab", items }] }],} });

    Import full scores

    The score-import skill finds scores on the web with CloakBrowser (opens in a new tab) and converts MusicXML, MXL, ABC and MIDI into scorify scores. It keeps exact time, spelled sounding pitches, voices, ties, repeats, chord symbols and lyrics, and records where each file came from. It honors robots.txt and stops at logins, paywalls and human checks. It runs on its own with Python 3.9 or newer and no packages: let your agent drive it (install it as shown below), or call its command line. Open the result here with Open a score file… or pass it to normalizeScore().

    score-import
    # the skill's command line, from a checkout of the repositoryscore=skills/score-import/scripts/score.py # read a page: score links and inline notationpython3 $score visit "https://example.org/scores/greensleeves"python3 $score download "…/greensleeves.mid" --from "https://example.org/scores/greensleeves"python3 $score convert greensleeves.mid --pickup 1/2python3 $score check greensleeves.scorify.json

    New scores with Claude Code or Codex

    The score-import skill works in Claude Code and Codex. Ask your agent for a song, and it searches the web, downloads a score, converts it and checks the result. You get a *.scorify.json file, or a *.scorify-set.json with several versions of one song, each with its difficulty. In Claude Code, the repository (opens in a new tab) is a plugin marketplace, and one command installs the skill; in Codex, the built-in skill installer fetches it from GitHub. It needs Python 3.9 or newer, and CloakBrowser for web pages. Open the file here with Open a score file….

    # in Claude Code: add the marketplace and install the skill/plugin install score-import --marketplace kyr0/scorify # once, for web pages: the browser it drivesbunx cloakbrowser install # then ask for a score, or call the skill/score-import:score-import Find a lead sheet of "Scarborough Fair" with melody, lyrics and chords and convert it to a scorify score
    # in Codex: install the skill into ~/.codex/skills/score-import$skill-installer install https://github.com/kyr0/scorify/tree/main/skills/score-import # once, for web pages: the browser it drivesbunx cloakbrowser install # then ask for a score, or call the skill$score-import Find a lead sheet of "Scarborough Fair" with melody, lyrics and chords and convert it to a scorify score
    # any agent that reads ~/.agents/skills, or one project's .claude/skillsgit clone --depth 1 --filter=blob:none --sparse https://github.com/kyr0/scorify.git ~/.scorifygit -C ~/.scorify sparse-checkout set skills/score-importmkdir -p ~/.agents/skills && ln -s ~/.scorify/skills/score-import ~/.agents/skills/score-import # once, for web pages: the browser it drivesbunx cloakbrowser install # later: updategit -C ~/.scorify pull

    The one-step install needs Claude Code 2.1.275 or later and clones the whole repository (116 MB on disk, mostly this site's cache). From a shell, two steps work with any version and fetch only the skill (1.7 MB): claude plugin marketplace add kyr0/scorify --sparse .claude-plugin skills, then claude plugin install score-import@scorify. Updates: claude plugin marketplace update scorify. Scores are for your private practice: the skill honors robots.txt, stops at logins, paywalls and human checks, and does not transcribe audio. Its instructions: SKILL.md (opens in a new tab).


    Vibe coding

    Paste this prompt into your coding agent. It links to the repository, so the agent works from the current README.

    Use scorify for ukulele and guitar arrangements in this app: https://github.com/kyr0/scorify
    Install it with npm install scorify (ESM, TypeScript declarations included). Read its README.md and docs/IMPLEMENTATION_SPEC.md first.
    Write sounding pitches with parsePitch() and exact quarter-note times with q(n, d). Build scores with createScore() and call arrange(score, { instrument, mode: "exact", player: { level } }).
    Branch on result.status: "infeasible" means proven impossible within the model, "search-exhausted" means not proven. Render result.annotations; never derive frets from pitches yourself.
    ⏎ send

    Write it once. Play it your way.

    Arrangement, fingering and plucked-string sound for ukulele and guitar. Exact time, honest results, plain modules.

    MIT licensed · v0.4.0 · npm install scorify