Skip to main content
Lightning Jar - Web Studio Lightning Jar Wordmark

Barmark, One Week In

Illustrated fox in business suit playing a grand piano against a cream background

A week ago we released Barmark: a markdown-inspired plain-text chord chart notation with a grammar draft, a parser and a renderer. The parser was at 0.1.0. Today it is at 0.19.0, three more packages have shipped beside the first two, and the grammar can say about a dozen things it could not say on launch day. None of that came from a roadmap. The notation grew because real tools needed it, and each of the three tools asked for something the chart could not yet say. This is the account of that week, with the engineering lessons that would apply to any small format with software on both ends of it.

Stem Shovel Asked for a Number

The first demand arrived on November 2, and it reversed a decision made earlier that day.

Stem Shovel is our music collaboration app, and it has a chord player with its own swing setting. Barmark's feel: directive takes straight, swing, shuffle or swing 16, and the grammar fixed swing at the triplet feel, with the second eighth of each beat landing two thirds of the way through. Issue 66 asked whether feel: could carry an amount, and the first answer was no: a swing amount is a performance setting a player or a DAW holds, and a chart should name the kind of groove and leave the amount to whoever plays it. Barmark 0.8.0 recorded that, closed the "custom swing ratios" open question as not planned, and shipped.

Then Stem Shovel built its "Download as Barmark chord chart" export, and the decision met the app. Stem Shovel's swing is a continuous amount from 0 to 1, shared by the metronome, the chord player, the Studio MIDI tracks and the drum generator. The export had one bit to put it in, so it wrote feel: swing when the amount was 0.5 or more and nothing otherwise. The drum generator's presets use swing amounts of 0.1, 0.15, 0.2 and 0.25, which is light swing, the most common kind in pop and hip-hop. Every one of them exported as straight. Importing a chart that said feel: swing set the player to its default amount, so a chart could not survive a round trip through the app that wrote it. The grammar was already privileging one amount, two thirds, and forbidding every other.

Kevin reopened the issue and decided the shape the same day: an optional percentage from 50 to 75 inclusive, the percent sign required, and shuffle takes an amount on the same scale. The percentage is where the swung note lands within its pair, which is the drum-machine convention players already know: 50 percent is even, 66.7 percent is the triplet feel, 75 percent is the dotted feel.

key: D, time: 4/4, tempo: 96, feel: swing 58%
| Dmaj7 | Bm7 | Em7 | A7 |

Barmark 0.9.0 shipped it later that day, and the renderer's header line started reading "D major · swing 58%" with the same release. One detail carried the compatibility: swing 66.7% reads and prints as the bare word swing, so every chart written before the change reads and prints exactly as it did.

The lesson is not that the first decision was careless. The reasoning was sound in the abstract, and the grammar's decisions log still records it, now beside the reversal and the evidence for it. A closed decision about a format stays closed only until a real tool contradicts it, and when one does, the right move is to reopen the issue with the evidence attached rather than defend the earlier reasoning. The whole cycle took one day, because the evidence was specific: four presets, all rounding to straight.

Notation Programs Asked for a Score

The second tool was @barmark/musicxml, which exports a chart as MusicXML 4.0 so that it opens in MuseScore, Dorico, Sibelius or Finale with the same bars, chords, repeats and sections, and plays in the chart's play order. Its first release was November 4. Getting there exposed how much a chart leaves unsaid.

A notation program wants to know who is playing. A chart that reads | G | C | G | D | could be anyone's part, so on November 3 the grammar gained part: lines and, the same day, several parts in one chart, written as systems: a row per part, sharing bars the way a score's system does.

part: Gtr = Electric Guitar 1
part: Pno = Piano, instrument: piano

## Verse
Gtr | G   | C | G  | D |
Pno | G/B | C | Em | D |

A program also wants to know what instrument each part is, because that decides the clef, the staves and the written transposition. The instrument: on the part line came on November 4, with a list of sixteen instruments carrying their clef, transposition, staff count and General MIDI program. A trumpet in B flat is written a major second up with a <transpose> taking it back. A keyboard gets two staves, and the grammar gained part lines so a pianist can write each hand:

Pno.RH | G{B3 D4 G4} | C{C4 E4 G4} |
Pno.LH | {G2}        | {C3}        |

Pedal marks (+G presses, *G lifts) and dotted figures (C._D, a dotted eighth and a sixteenth) came in the same release, because a keyboard part without them exports as something no pianist wrote. Credit lines, composer:, lyricist:, arranger: and copyright:, arrived because a MusicXML file has <creator> and <rights> elements to fill. And the engraved vocabulary a score reader expects, first and second endings, Segno and Coda, D.S. and D.C., rehearsal marks, words on a bar, accents, hits and pushes, all landed on November 3 under one issue, because exporting a chart that could not say "second time to Coda" would have been exporting half a song.

## Verse
|: C | F | G | C |
|1 G | G7 :|
|2 G | C |

Two days later the package learned to read scores back. importMusicXml takes a MuseScore, Dorico, Sibelius or Finale file, or this package's own export, and writes a canonical chart from it, listing what could not come over as warnings rather than dropping it silently. It reads compressed .mxl archives with no dependency, treating the archive as untrusted input. The 0.6.0 release on November 7 added lyrics and melodies in both directions, so a lead sheet makes the round trip as written.

The README has a section on Dorico, checked in Dorico Elements 6.2.31, because a conversion is only done when the program on the other end agrees. Dorico puts imported credits on the flow rather than the project, imports every chord symbol as a global one, and ignores the repeat count in the file, so the export also prints a repeat played more than twice as words. None of that is in the MusicXML specification. It is in the behavior of a program people use, which is the only place a conversion can be verified.

Recordings Asked for a Timeline

The third tool is @barmark/sync, released at 0.1.0 on November 7. It exists because Stem Shovel's chord detector produces a bar grid and a list of chord runs, and a band wants a chart it can correct, with the chart placed on the recording so the player can follow it.

chartFromDetections is the builder. It takes a detector's bar starts in seconds, its chord runs as Harte labels or barmark symbols, and whatever else the detector knows (key, meter, sections, a pickup, mid-song changes), and returns a draft chart beside a timing map for it. Each slot takes the chord that covers most of it and ? where under half is covered, a slot that repeats the one before is /, and a repeated multi-chord bar is %. It never throws, and it never guesses at bars the grid does not have.

const { chart, timing } = chartFromDetections({
	grid: { bars: [0, 2, 4, 6, 8] },
	meter: "4/4",
	chords: { runs: [
		{ start: 0, end: 4, label: "C:maj" },
		{ start: 4, end: 6, label: "G:maj" },
		{ start: 6, end: 8, label: "A:min" },
	] },
});
// chart: "time: 4/4, tempo: 120\n| C | / | G | Am |\n"

The timing map is the part we expect to outlive this package's current shape. It is a small JSON format, barmark-timing version 1, that says when each played bar of a chart starts in a recording. It records the chart's played-bar count and a hash of its canonical print, so a map for an edited chart is detected rather than trusted. A bar may recur, because an open repeat plays once in the parse and many times in a recording, and bars may be missing, because a recording may cut a section. checkTimingMap enforces the rules and never throws, and the two lookups a player needs, barAt(map, seconds) and secondsAtBar(map, bar), come with it. An experimental confidence record rides beside the chart, scoring per slot how well the detector's runs back what was written, so a person correcting the draft knows where to look first.

Real grids corrected the builder before it shipped. Built from Stem Shovel's detector output for two Badverbs songs, "Letter Lands" and "Eat All the Clocks", the derived tempo came out at 141 and 130 where the band's charts say 132 and 120. No synthetic test had shown it. The reference grids leave many half-length bars, 24 of 76 in "Letter Lands" and 22 of 86 in "Eat All the Clocks", and a share that size, all on one side, pulls the median beat down. The builder now folds a bar under 0.6 times the median into half a bar and one over 1.6 times into two before taking the median, and gives 132 and 120 on the real grids. Synthetic tests check what you thought of. The real recordings checked what the detector does.

The Command Line Grew With Them

@barmark/cli shipped on November 2 with check, format, render and transpose, on npm and as standalone binaries. It gained part on November 3 when charts could carry several parts, export --musicxml on November 4 when the MusicXML package existed, and import on November 6 when the import did. It pins the exact versions of the libraries it runs, so a chart means the same thing in a terminal, in CI and on barmark.dev, and its README asks you to pin the CLI's version in CI too: a new release can read a chart differently, and a pinned version turns that into a pull request you review.

Lessons That Travel

Everything quadratic was a search inside a loop

Five separate slowdowns surfaced this week, all in the same shape. In the parser, a cell of many quoted words re-read the whole cell for each word, so 20,000 words in one bar took about 25 seconds. In the renderer, each bar line read every played bar of the chart to find its own, three times over. In the MusicXML import, 5,000 measures each with an unwritable time signature took 2.2 seconds and one measure of 3,000 notes took 8.9 seconds, because lookups written as find, filter and includes sat inside loops over measures. In the CLI, import took 13 seconds on a file whose one measure number was 100,000 digits long, because the CLI searched its own rendered messages for the measure number, and the file's text was in the message. In the sync builder, 300 sections named Verse x9999 (x2) took 5.7 seconds, because repairing each name parsed it with the full parser, and a hidden x9999 expands into thousands of bars.

Each fix is the same fix. Index once, with a Map or Set keyed by position, or a binary search over a sorted list, and never search a collection from inside a loop over that collection. Bound the input where a bound costs nothing: a symbol over 64 characters is ?, a measure number is cut to 64 characters, a build parses section names at most 2,000 times. Then pin the behavior with a scale test that checks ten times the input does not take a hundred times as long. Every one of these inputs was hostile rather than musical, and that is the point: a parser on a website will be handed hostile input.

Keep changes additive, so every existing chart is a fixed point

Nearly every release note this week opens with one of two sentences: "Charts that read before read the same," or "No chart changes meaning." That was a constraint on design, not a summary written afterwards. The swing amount prints 66.7% as the bare word so old charts print unchanged. Endings use |1, which the launch grammar had reserved as an error, so no valid chart gained a new meaning. New directives, credits, marks and parts all occupy syntax that was an error or plain text before.

One release broke the rule, and said so in its first line. Barmark 0.17.0 made a line of three or more hyphens a system break instead of text, and made a part's named lines join the bare line written before them. Both changes were needed by the MusicXML import, which had no silent way to keep two systems apart, and the changelog names both and tells you what to write to keep the old behavior. Being additive by default is what makes the exception readable: when every other release promises nothing changed, the one that cannot promise that stands out, and that is where a reader's attention should go.

Where It Stands

The grammar is still a v0.1 draft and the packages are still 0.x. What changed in a week is that the draft now has three tools reading and writing it under real conditions, and each of them has already sent back corrections the grammar would not have found by itself. The package page keeps the current versions, barmark.dev has the live renderer, and the repository holds the changelogs this article was written from, one per package, with the decisions log in the grammar draft. If you write charts, we want to hear where the notation still misreads what your band writes. If you build music software, the chart your tool cannot express yet is the next issue.

headshot of Kevin Peckham
Kevin Peckham
Principal, Lightning Jar

Ask Eljay

Ask about the studio's work, research, packages, or writing. A few starters:

Answers come from this site's own content and link their sources. For anything that matters, email hello@lightningjar.com.