on a high-G ukuleleon a six-string guitarwithout dropping a noteat the tempo you choosefor 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.
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.
A scale in even notes, the last one held to the bar line, with the Beat on; or, for a seven-note scale, its chords strummed or as arpeggios. Key, Time and Play below set the tonic, the time signature and how it is played. scorify spells every degree, starts at the lowest octave the neck holds up to its highest fret, and fingers the scale for the difficulty as written, or says why it cannot.
Twenty-five chord progressions chosen for recognizability, harmonic variety and practice value: pop and rock loops, minor and Dorian progressions, borrowed chords and secondary dominants, jazz ii–V–I, longer sequences and the 12-bar blues, with how many of 739 Billboard Hot 100 songs from 1958 to 1991 start a phrase with each. A bar per chord unless the progression says otherwise (three chords in four bars, the blues in twelve), strummed or as an arpeggio, seven versions per time signature, or in a strumming pattern: the island strum, reggae skank, boom-chick, a waltz, a blues shuffle and more; Key, Time, Play and Pattern below set the key, the time signature, how it is played and the pattern. The small buttons by each chord name in the tab turn that chord to its next inversion, or back.
Counted by scorify's own script over the chord annotations of the McGill Billboard corpus (opens in a new tab) (Burgoyne, Wild and Fujinaga 2011; CC0): songs with a phrase that starts with the progression's chords (sevenths counted as triads), and for the 12-bar blues, songs with a 12-bar blues chorus. The corpus ends in 1991, before I–V–vi–IV and many minor pop loops became common in hits; a progression it does not contain shows no count. Other sources are linked, not counted.
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 centsPress 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.
Suggested capo
Loading the arrangement engine…
No arrangement
Custom modein force
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.
Saved arrangements
Nothing saved in this browser yet.
Your arrangementBar /
On the neck filled: sounding · outlined: next
Voicings as played x = string not played
Strings, bass edge first
G4 · C4 · E4 · A4
Difficulty
maestro
Notes vs. the hardest version
0 / 0
Search
beam
Playback timing
validated
About this arrangement
A difficulty is an editable, uncalibrated policy of scorify (player: { level }): limits on fret span, shift speed, attacks per second, notes at once and barres. A fingering that is valid in the model may still be uncomfortable for you. The versions and their chord shapes come from the demo (docs/assets/songs.mjs); scorify judges each shape, then fingers the version in exact mode, so it never drops, moves or shortens one of its notes. Only when no version plays does it simplify one in adaptive mode, and says above what it left out.
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.
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.
01
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.
02
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.
03
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.
04
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.
05
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.
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.
Function
Returns
Semantics
createScore(notes, options?)
Score
Validates 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, Q
A spelled sounding pitch, and an exact rational time in quarter notes.
arrange(score, options)
ArrangeResult
Joint edit and fingering search, followed by an independent replay check.
arrangeAsync(score, options, hooks?)
Promise
Cooperative scheduling with signal, onProgress and yieldEvery.
createWorkerClient(worker)
client
arrange() in a module worker. A newer request cancels the previous one, and stale replies are ignored.
createArrangementSession(score)
session
Recomputes 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)
spans
Chord candidates per window, including arpeggios. Sparse evidence stays unknown.
quantizePerformance(notes, maps)
result
Quantizes timed performance notes against a known tempo and meter map.
validateFrettedArrangement(…)
report
The public replay validator, for checking any fretted output.
createPerformance(result, instrument, options?)
PerformanceResult
Strums, humanization and timing validation, run before any synthesis.
AudioWorklet playback and offline rendering. The worklet and offline paths produce matching samples. encodeWav() writes the bytes.
arrange(score, options) takes an instrument. Every other key is optional.
Key
Default
Meaning
instrument
required
concertUkulele({ tuning }), classicalGuitar({ capoFret }) or createFrettedInstrument(id, name, tuning). Strings are listed from the bass edge to the treble edge.
mode
"exact"
"exact" keeps transformed pitches, onsets and holds. "adaptive" may also make the edits you permit. "unconstrained" transforms without an instrument.
A level plus complexity, flexibility and overrides, such as maxFretSpan, allowBarre or rightHand.
adaptation
ornaments only
Permissions: allowOctaveShift, allowAccompanimentOmission, allowRhythmSimplification and allowHoldShortening. Note locks and protectedNoteIds protect material.
search
beam 64
strategy: "beam" | "exact" | "exhaustive", beamWidth, maxCandidatesPerFrame and maxExpandedStates.
autoTempo
false
Lowers the fastest tempo segment to the level's suggested ceiling. It never speeds a score up.
signal, scoring
none
Cancellation, and an optional pure cost hook that can never override hard validity or fidelity.
The built-in levels are editable defaults (scorify-player-1), not calibrated ability labels. Even maestro cannot bypass a hard physical limit.
Level
Fret span
Shift frets/s
Simultaneous notes
Attacks/s
Barre
Suggested ♩
baby
2
5
3
2
no
50
beginner
3
9
4
4
no
65
amateur
4
16
6
7
yes
85
semi-pro
5
25
8
10
yes
100
pro
6
40
10
14
yes
120
expert
7
65
12
20
yes
140
maestro
9
100
16
32
yes
160
Attacks per second count every string a strum hits. That is why the strummed songs in the playground need a higher level at full tempo. Slow one down and a lower level can play it.
Smaller entry points for the same building blocks.
scorify/arrange, /fingering, /adapters, /validate
The search, the fretted adapter, the InstrumentAdapter extension point and the replay validator.
scorify/analysis, /quantize
Harmony windows, and quantization against known tempo and meter maps.
scorify/worker, /worker-entry
The worker client, and the side-effect module a dedicated worker loads.
scorify/performance, /audio-dsp
Performance planning and the DSP. Neither needs a browser API.
scorify/audio, /audio-worklet
Browser playback, offline rendering, WAV encoding, and the AudioWorklet module.
scorify/tuner, /pitch-detect, /pitch-worklet
YIN pitch detection, live microphone input through an AudioWorklet, and the strings to tune for an instrument.
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.
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 gestureconst 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));
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.