Barmark: Chord Charts Bands and Machines Can Both Read

Every working band has a chord chart somewhere: in a group text, a shared doc, a photo of a notebook page. It looks like | D | A | Bm | G |. A drummer reads it at a glance. Software does not, because the convention was never written down, and the places where it is vague are exactly the places a parser has to decide something. We hit those places building Stem Shovel, whose chart reader places a band's sections on their recording. Today we are releasing what we learned as Barmark: Markdown-inspired plain-text notation for chord charts, the bar grid musicians already write, specified precisely enough for machines. There is a grammar draft, a parser package and a renderer package, and the code is MIT.
The Chart Everyone Writes and Nobody Specified
Three traps convinced us the notation needed a spec rather than a tooltip. All three came from real charts going through Stem Shovel's parser.
The first is the empty cell. In the chart dialect the app shipped, an empty cell held the previous harmony, which is the natural way to write a chord that rings for four bars: | D | | | |. The same rule means | G | | | G | is four bars of G, silently, when the author may have meant "something goes here, I'll fill it in later." The hold is the tool and the trap at once.
The second is the scope of x N. Does | D | A | x2 play the line twice, or the last bar twice? Our docs plan for the app records that this exact ambiguity made a real chart unreadable, so the app's rule became "always the line." That is a reasonable rule, and it is also invisible: nothing on the page tells a reader which way the parser went.
The third is the jazz slash. A jazz player reads | Am7 / / G7 | as three beats of Am7 and one of G7, with the slashes as beat holds. The app's parser treated a lone / as "repeat the previous bar," so the split came out wrong with no warning. The bench found it, not a user, which is the only reason it was not embarrassing.
Each of these is a case where a reasonable musician would read a construct one way and a reasonable program another. A help page explaining the parser's choices would not reach the people who never read help pages, which is everyone.
Where the Rules Came From
Barmark was extracted from working software. Stem Shovel's chart reader (chordChartBars, with helpers that read stated key, meter and tempo, name sections, and turn chord symbols into pitch classes) is what feeds the alignment we wrote about in The Last Job: render the chart as sustained chord pads at the declared tempo, warp the render against the recording, and read every bar's timestamp off the warping path.
That pipeline is only as good as the parse, so stem-bench validated the parse against real recordings by Badverbs, with the band's own declared section starts as ground truth. Study 14 ported the production parsers verbatim and found they reproduce the bench's two reference transcriptions bar for bar and section for section from the band's own markdown conventions, 76 of 76 bars on one song and 86 of 86 on the other, with six of eight mid-song boundaries placed within 0.11 seconds of the declared starts. Study 15 ran again on the band's revised chart, whose ten sections matched the declared starts exactly, the first fully machine-true chart in the catalog. Those numbers are the receipts for the rules: every rule in the founding draft, apart from the ones marked as proposed, was checked against the shipped parser, the one the bench had checked against the recordings, before it was written into a grammar.
Then, instead of freezing the app's dialect, we made the grammar draft a clean-sheet redesign. It keeps what musicians already write on paper (simile %, beat slashes /, repeat barlines, x2, N.C.) and drops what the parser did only for historical reasons. Compatibility with the app is explicitly not a goal; the draft carries a migration table instead.
What It Looks Like
A Barmark file is a Markdown file, with the extension .chart.md, so every Markdown toolchain renders it acceptably on day one. This is the example from the repository's README:
# Example Song
key: D, time: 4/4, tempo: 145
## Verse
|: D | A | Bm | G :|
| Em | / | A | A7 |
## Chorus
| G / D / | A | Bm / / A | G |
## Verse
## Chorus x2
A line's first characters decide what it is. | starts a bar line, # the title, ## a section, and key:, time:, tempo: or feel: a directive. Anything else is text: lyrics, cues, notes to the bass player. Text is kept so the file round-trips, but it is never scanned for chords, which means a typo in a chord line is always an error and can never quietly become lyrics.
Inside a bar line, each cell is one bar, and the tokens in a cell divide it equally: | G / D / | is two beats of G and two of D. |: :| plays the enclosed bars twice. A section with no bar lines replays the most recent section with that name, so a verse is written once and played by name after that, and ## Chorus x2 plays the chorus twice. The full example in the repository is the chart for "Peaceful Dreams," a song our principal Kevin Peckham wrote for Badverbs. It runs to intro, verses, choruses, a bridge, an interlude and a counted outro in about forty lines.
Fixing the Traps in the Notation
The draft's third design principle is "fix traps in the notation, don't just document them," and each of the three traps above got that treatment.
An empty cell is now an error, with a message that says what to write instead. Holds are spelled /, and % alone in a cell repeats the previous bar. That split also settles the jazz slash: / means "whatever was sounding continues," so | Am7 / / G7 | is three beats of Am7 and one of G7, exactly as a jazz player reads it. The two symbols differ precisely where it matters: | C G | / | holds G into the second bar, while | C G | % | plays C then G again.
x N repeats whatever its line denotes: a bar line, a section heading, or a closed repeat bracket. An x2 inside a cell is an error that suggests moving it after the final pipe. Here is the parser on the three inputs that started this:
| G | | | G |
error: an empty cell; use / to hold or % to repeat
| D5 x2 | A |
error: a repeat is written x N after the final |
| Am7 / / G7 |
one bar: Am7, hold, hold, G7
Two more decisions fall out of the same principle. N.C. means no harmony, which is right for a drum break or a riff that moves through many notes, and a full stop by the whole band is --. And ? is a legal slot, so a chart can be drafted with a gap, but it always warns, so no gap gets published unnoticed. The grammar's fourth principle is "never fail silently," and every message carries a line number and the offending token.
Two Packages
barmark 0.2.0 is the reference parser. parseChart reads a whole chart into the document as written and the bars in play order, each carrying its section, key, meter, tempo and feel, with replays, repeats, simile and one-bar meters expanded. It never throws: a line with an error is left out of the play order and reported, and the rest of the chart still reads. printChart rewrites a chart in canonical form, changing spelling and layout only, so | G / D / | prints as | G D | while lines with errors and lyrics print exactly as written. Printing preserves meaning and is stable when applied twice.
The package also speaks Harte syntax, the chord-label vocabulary proposed at ISMIR 2005 and used by the music information retrieval community since. toHarte writes the label for any chord or note set, and harteToBarmark converts a label back. We checked the conversion against the McGill Billboard dataset, a CC0 fixture of 112,307 chord labels: every label converts to Barmark naming exactly the same spelled notes, and 97.92 percent come back as named chords rather than note sets. A report-only script runs the same check over a local copy of the Isophonics Beatles annotations (100 percent same notes, 99.22 percent named); that data is not redistributable, so it is not committed. The package is ESM only, Node 20 or later, with TypeScript declarations and no runtime dependencies.
@barmark/render 0.1.0 draws the chart. renderChart returns a <barmark-chart> element holding a declarative shadow root, so a server-rendered or static page shows an evenly spaced bar grid with no browser JavaScript at all, and the shadow root keeps the page's resets and fonts from rearranging the bars. For pages that want it live, importing @barmark/render/element registers the same element as a web component that renders from a src URL or a child <script type="text/barmark"> and renders again when either changes. Chords appear in canonical form with ♯ and ♭ glyphs and the extension as a superscript, set in Edwin, MuseScore's text font for music, which ships unmodified under its Open Font License. Both the light and dark palettes meet WCAG AA contrast, everything is sized in em so text zoom scales the chart, and a section too wide for its container wraps its rows rather than scrolling sideways. Every bar has a spoken name built from the parsed structure ("bar 9: A seven sus four, hold"), so a screen reader hears music rather than punctuation. Pages restyle it through CSS custom properties and ::part() names, which are the public surface.
Standing Next to Prior Art
Barmark names something practice already did, and it is worth being precise about what already exists. ChordPro is the established standard for lyrics-with-chords files, lyric-anchored where Barmark is bar-anchored; its grid environment is the closest prior art to the bar grid. ChartDown is an active plain-text notation for the Nashville Number System with play-order expansion; it is complementary rather than rival, since Nashville is key-relative numbers and Barmark is absolute chord symbols, and a converter between the two is a natural future package. iReal Pro is the de facto digital bar-grid format, bound to its app. Harte syntax is the academic standard for chord labels, the right vocabulary for Barmark's machine output rather than its authoring surface. And the engraved conventions, simile marks, repeat barlines, D.S. and segno, are the ancestors of all of it.
What Is Draft and What Is Shipped
The grammar is at v0.1 draft, for review, and its version is separate from the packages' versions. The founding brief marked three rules as proposed; they are now in the draft as the parentheses rule (G5(7) is G, D and F, the way Logic reads it), replays by section name, and open repeats (x ... for a fade or a vamp until cue). Endings (first and second voltas) are reserved for v0.2 and are an error today, so adding them later changes the meaning of no existing chart. The draft lists its open questions: pushes, articulation, voicings on named chords, omissions, custom swing ratios. The packages are at 0.x, so a minor release may change the grammar or the API, and each CHANGELOG says which grammar it implements. Stem Shovel's shipped parser still reads the older dialect; section 10 of the draft is the migration table from one to the other.
The code is MIT. The specification will be published under CC-BY 4.0, following ChordPro's posture. The reference parser began as an extraction of Stem Shovel's reader, and the copyright on both sides is ours, which made relicensing a one-line decision.
Two Lessons That Travel
The repository keeps a findings file, and two entries apply to any project with a parser in it.
The printer's round-trip test compared bars before and after printing, and both sides went through the same slot-reduction function. When that function was wrong (it merged a hold after a triplet group, turning quarter-note triplets on beats one and two into half-note triplets across the bar), the test called the two charts equal. The fix was an independent oracle: a timeline of when each attack lands, as exact fractions of the bar, computed without the code under test. When a test asserts "same meaning," compute the meaning some other way.
The second is about what fuzzing finds. Two thousand random strings drawn from the grammar's alphabet never assembled a valid cell with a hold after a group, so the bug above survived them. Two thousand random charts built from grammar pieces (slots, groups, %, one-bar meters, directives, headings, repeats) found it in 45 milliseconds. We keep both: the character fuzz proves the parser never throws, and the grammar fuzz exercises meaning.
Barmark is small on purpose. It describes harmony in time, and nothing about who plays what; orchestration belongs in text lines. If you write charts, the grammar draft is a short read, and we would like to hear where it misreads what your band writes. If you build music software, the parser is an npm install away, and the bench that validated it is open. The package page and technology page on this site will keep the links current as the packages move.