Skip to content
learn

Journal

  • 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: TabStave and TabNote
  • 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
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.

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.

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.

  • #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, ch and pint no longer consume the whitespace after them. The same bug made key=G followed by a newline read the n of the next line’s notes as a natural sign. The parser and the generator now follow VexTab where the tutorial and vextab.jison are 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: stave and voice lines, ties, slurs, tuplets, the durations :1 :2 :4 :64, and X as 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. MemoizedVexTab parses a block and draws a tab stave with VexFlow 4.2.5; a block it cannot draw whole stays as text, and data-renderer on the block says which the reader sees. The constant notation.renderer is gone: the server draws nothing, so the traces now report notation.vextab.block_count and valid_count.
  • What the course had left to verify. No GA workflow runs ga-client’s Playwright specs: ci.yml only lints and builds the client. And vextab-rendering.spec.ts could not pass as written, because its beforeEach clicks 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.renderer field; 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-copilot crashing on its first render; that was fixed by #737.
  • The eb tuning in a browser: the value is in VexFlow’s table and the headless drawing shows C♯3, but the browser comparison didn’t include l03-tuning.
  • Firefox and Safari: the browser comparison ran in headless Chrome on Windows only.
  • Whether VexFlow 4, as GA uses it, has the setFont argument order that made VexTab’s italic a style there: not checked, since the course pins VexFlow 5.
  • 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? One parseInt already exists, a line above the one that forgets it.