Journal
Sections
Reading settings
No voice for this language in this browser.
More voices
Code and English terms
Off: the page’s voice reads everything, with no pause between two voices.
No voice for this language in this browser.
More voices
More voices: Microsoft Edge has natural online voices, and Windows (Settings › Time & language › Speech), macOS and Android install others.
Progress
Section titled “Progress”- Mission and outline
- Course code: headless rendering,
check.sh, CI on Linux, Windows and macOS - Lesson 1: first stave, first tab
- Lesson 2: guitar techniques
- Lesson 3: keys, times, clefs and tunings
- Lesson 4: case study, GA’s chatbot writes VexTab
- Lesson 5: VexFlow’s own objects, a strict voice
- Lesson 6:
TabStaveandTabNote - Lesson 7: rendering in React, testing with Playwright
- Lesson 8: moving GA from VexFlow 4 to 5
What the course found in the software it teaches, and in Guitar Alchemist’s code that uses it. VexTab links point at 3a5e00d, the commit npm’s 4.0.5 was built from; VexFlow links at 0ca6f88, the 5.0.0 bundled in it; GA links at 17ccee6. Every measure is an output of check.sh, compared in CI. Nothing has been reported upstream yet; a search of the VexTab and VexFlow issues found no existing report of these.
| Expected | What happens | Where | Measure | Status |
|---|---|---|---|---|
strings=4 sizes the drawing for four tab lines |
The option stays a string; VexFlow computes the stave height as ("4" + 4) * 13 |
StaveBuilder.ts:141, VexTabParser.ts:86-89, VexFlow stave.ts:142 |
Drawing 712 px tall for strings=4, 842 for 5, 1102 for 7, against 270 for six strings; same in Chrome |
Reproduced under Node and in Chrome (lesson 3) |
$G$ after a chord draws the text G |
Any text matching a note name replaces the fret number of the chord’s lowest string, and no annotation is drawn | ArticulationBuilder.ts:362, :461 |
$G$ $C$ $D$ on three chords: 0 annotations, 3 fret numbers replaced; $.big.G$ and $D $ are drawn as text |
Reproduced. Documented as a feature for single notes; a trap for chord names (lesson 2) |
$.italic.let ring$ is italic |
VexTab passes the style as VexFlow 5’s third argument, the weight | ArticulationBuilder.ts:298-302, VexFlow element.ts:392 |
SVG says font-weight="italic", no font-style |
Reproduced |
The vexflow.com logo is centred and italic |
Measured before its font is set, so it is placed with the width of the wrong font, and set upright for the same reason as above | ArtistRenderer.ts:262-266 |
x = 187.78 on a 600 px drawing with notation (Chrome: 188.52) | Reproduced under Node and in Chrome (H2) |
X/6 is a muted note on string 6 |
The lexer reads X as a note name, so 6 becomes an octave: the notation draws an x notehead where B6 would be, at y = 0, half outside the drawing |
vextab.jison:84, NoteBuilder.ts:264-266 |
Same position in Chrome, within 0.67 px | Reproduced under Node and in Chrome (H3). The tab is right |
VexFlow’s eb tuning is standard tuning a half step down |
String 6 is Db/3, a whole step down |
VexFlow tuning.ts:16 |
Open string 6 drawn as C♯3 (written), where E♭3 was meant; the line is unchanged on VexFlow’s main |
Reproduced (lesson 3) |
V draws a harsh vibrato |
VexTab calls setHarsh only if it exists, and VexFlow 5’s Vibrato has none |
ArticulationBuilder.ts:635-640, VexFlow vibrato.ts |
7v and 8V draw the same three glyphs U+EAB0 |
Reproduced |
| Five quarter notes in a 4/4 bar are refused | They are drawn: VexTab’s voices are in soft mode | ArtistRenderer.ts:67 |
l01-overfull renders |
Reproduced. By design, as far as the code says (lesson 1) |
GA’s chatbot vextab blocks are VexTab |
They use a GA token format, string/fret, with no tabstave or notes |
GA PlayableNotationFormatter.cs:15-25, :59-71 |
12 of 12 blocks (8 beginner chords, the prompt’s example, 3 e2e tests) rejected by VexTab 4.0.5 and by GA’s F# parser | Fixed upstream by #736, merged 2026-09-25: the blocks are real VexTab, and the formatter’s 10 blocks render with VexTab 4.0.5 2026-09-25 |
GA’s chat client draws vextab blocks, as its e2e test asserts |
MemoizedVexTab prints them in a <pre> |
GA MemoizedVexTab.tsx:11-28, vextab-rendering.spec.ts:94-99 |
Read, not run; the unit test is it.skip |
Fixed by #736: the client parses the block and draws a tab stave with VexFlow 4.2.5. The e2e spec runs in no GA workflow, and could not pass as written 2026-09-25 |
GA’s trace field notation.renderer says what drew the notation |
It is the constant "vexflow" |
GA OrchestratedChatApplicationService.cs:336 |
45 golden traces: 44 with added_count 0, one with 8 |
Removed by #736: the traces now count the blocks and the ones GA’s parser reads; the 45 golden traces still pin the old field until they are re-recorded 2026-09-25 |
| GA’s F# parser reads what its generator writes, and its grammar’s examples | str, ch and pint eat the whitespace after them, including the space that starts the next tabstave option |
GA VexTabParser.fs:22-29, :330 |
Round trip fails at column 51 or 52 for 6 of 6 parsable texts; 0 of 4 EBNF examples parse | Fixed by #736: separators are explicit, and a round-trip test and the grammar’s own examples pass 2026-09-25 |
| GA’s F# parser accepts VexTab | It differs on techniques (after the string), flats (b), mid-line durations, fret runs, taps, annotations and minor keys |
GA VexTabParser.fs:85, :239-244, :413-420 |
Of 32 texts, GA’s parser accepts 6 and VexTab 17; they agree on “ok” for 4 | Mostly fixed by #736: the seven differences listed are gone; stave and voice lines, ties, slurs, tuplets and some durations are still missing 2026-09-25 |
Experiments
Section titled “Experiments”| Question | Hypothesis, written before measuring | Result | Verdict |
|---|---|---|---|
| Does the headless rendering (jsdom, text measured with opentype.js) place glyphs where Chrome does? | H1: within 1.0 px in x and y for every text element of l01-notation, l02-chords and l03-keys, if Chrome loads Bravura and Academico (hypotheses.md, committed in 284eb7e before any browser run) |
157 text elements over five drawings: max |dx| 0.74 px, max |dy| 0.67 px, sizes equal (browser-2026-09-24.txt). The first two runs were wrong, see the entry |
Confirmed |
| Is the off-centre logo an artefact of the headless setup? | H2: Chrome draws it at x within 1 px of 187.78. Not blind | Chrome: 188.52 | Confirmed |
Does Chrome draw X/6 in octave 6 too? |
H3: yes, at the same y. Not blind | Same position, within the 0.67 px above | Confirmed |
Is the 712 px drawing of strings=4 an artefact of jsdom? |
H4: Chrome’s SVG is 712 px high. Not blind | 600 × 712 | Confirmed |
2026-09-24 — Which VexTab, which VexFlow
Section titled “2026-09-24 — Which VexTab, which VexFlow”npm’s vextab 4.0.5 was published on 2026-01-18; its sources are identical to commit 3a5e00d of 0xfe/vextab (checked file by file with SHA-1). Its dist/main.prod.js bundles VexFlow 5.0.0, Vex.Flow.BUILD.ID 0ca6f889…, and re-creates VexFlow 4’s Vex.Flow namespace over it. Installing vexflow next to it changes nothing for VexTab; the course pins vexflow 5.0.0 anyway, for lesson 5. GA, on the other hand, uses VexFlow ^4.2.5, and vendors a vexflow.js 4.2.5 for its chatbot API.
2026-09-24 — Rendering without a browser
Section titled “2026-09-24 — Rendering without a browser”The bundle wants window, self, document and getComputedStyle; jsdom gives them. What it doesn’t give is text measurement: VexFlow measures every glyph with a canvas. First attempt, node-canvas 3.2.3 with registerFont on the Bravura and Academico OTF files: on Windows it doesn’t load these CFF-flavoured fonts and silently measures with a sans-serif fallback. Replaced by a small context that measures with opentype.js from the same OTF files (lib/env.cjs), passed to VexFlow with Element.setTextMeasurementCanvas. The output is the same on the three OSes in CI, so the SVGs can be compared byte for byte after renumbering ids and rounding to two decimals.
2026-09-24 — Headless against Chrome
Section titled “2026-09-24 — Headless against Chrome”Hypotheses written and committed first (284eb7e). Then the same five files through the browser bundle in headless Chrome 153, reading every <text> back (browser/).
The first run gave a gap of 204 px on the logo and garbage glyphs: the test page had no <meta charset="utf-8">, Chrome decoded the bundle as Windows-1252, and every SMuFL code point in it became mojibake. A measurement error, not a result. The second run was off by one element on l02-vibrato-mute: compare.mjs skipped <text> elements without a y attribute, and VexFlow writes none when y is 0, which it is for the muted note’s glyph. Fixed by reading each attribute separately, with a missing one as 0. The third run is the one in the table: 0.74 px at most, on the logo.
2026-09-24 — GA’s VexTab, run
Section titled “2026-09-24 — GA’s VexTab, run”Blobless sparse clone of GA at 17ccee6, nine files. The C# formatter compiles alone; the three F# files compile unchanged with FParsec 1.1.1 on .NET 10, as GA references it. Results in lesson 4. What was expected going in: that the chatbot blocks might use an older VexTab dialect. What came out: they use no VexTab dialect, the client doesn’t draw them, and GA’s own parser rejects its own generator’s output. VexTab, meanwhile, reads GA’s generator output without trouble. Nothing sent to GA: the findings are in the report to the user, who decides.
2026-09-25 — Upstream fixes
Section titled “2026-09-25 — Upstream fixes”- #736, merged 2026-09-25, fixes the five GA rows. Each change has a test that fails on the old code: 51 of the 57 round-trip tests, 22 of the 35 formatter tests, the trace test, and 4 of the client’s drawing tests.
- The parser.
str,chandpintno longer consume the whitespace after them. The same bug madekey=Gfollowed by a newline read thenof the next line’snotesas a natural sign. The parser and the generator now follow VexTab where the tutorial andvextab.jisonare clear: techniques between frets (5h7/3),@for flats in notes,key=Am, durations mid-line, fret runs, taps, annotations as note items, VexTab’s order of duration flags. Run through this course’s harness on 25 texts, GA and VexTab 4.0.5 now agree on every one; for the 20 both accept, the text GA writes back draws an SVG byte-identical to the original’s. Not done, and listed in the PR:staveandvoicelines, ties, slurs, tuplets, the durations:1 :2 :4 :64, andXas a note name. - The chatbot. Its blocks are VexTab now: open C is
tabstave/notes :w (3/5.2/4.0/3.1/2.0/1), and the prompt asks for that form.MemoizedVexTabparses a block and draws a tab stave with VexFlow 4.2.5; a block it cannot draw whole stays as text, anddata-rendereron the block says which the reader sees. The constantnotation.rendereris gone: the server draws nothing, so the traces now reportnotation.vextab.block_countandvalid_count. - What the course had left to verify. No GA workflow runs ga-client’s Playwright specs:
ci.ymlonly lints and builds the client. Andvextab-rendering.spec.tscould not pass as written, because itsbeforeEachclicks an “AI Chat Assistant” tab that no longer exists. Read in GA’s workflows and spec, not run. - The 45 golden traces pin the old
notation.rendererfield; they come from live runs and need re-recording on a machine with the model. While reading the chat client, the same session found/ai-copilotcrashing on its first render; that was fixed by #737.
To verify
Section titled “To verify”- The
ebtuning in a browser: the value is in VexFlow’s table and the headless drawing shows C♯3, but the browser comparison didn’t includel03-tuning. - Firefox and Safari: the browser comparison ran in headless Chrome on Windows only.
- Whether VexFlow 4, as GA uses it, has the
setFontargument order that made VexTab’sitalica style there: not checked, since the course pins VexFlow 5.
Open questions
Section titled “Open questions”- Is the fret override by note-name annotations meant to apply to chords, or only to single notes? The code applies it to the lowest string of any note group.
- Should VexTab validate
strings=to a number and pass the number? OneparseIntalready exists, a line above the one that forgets it.