
Fretboard to Browser: A Guitar Tab Editor
My first end-to-end vibe-coding project — a browser-based guitar tab editor that shows standard notation and tab side-by-side, with in-browser synth playback and import from ASCII tab, MIDI, and (locally) audio transcription. The backend is FastAPI with a custom SVG layout engine for the notation; the frontend is React + TypeScript with Web Audio for playback. Most of what I learned came from working out how to direct AI coding agents through a real build.
Tab Renderer — Project Writeup
A browser-based guitar tab editor that renders standard notation and tablature side-by-side, plays the score back through an in-browser synth, and ingests both MIDI and audio recordings. Built over roughly two months as a solo project.
Live demo: deployed on Vercel Stack: React + TypeScript (Vite) on the frontend, FastAPI (Python) on the backend, Tone.js / Web Audio for playback, SVG for engraving.
Why I built it
Existing guitar tab software falls into two camps: heavyweight desktop apps (Guitar Pro, MuseScore) and minimal ASCII-tab editors that don't render real notation. I wanted something in between — a fast, browser-native editor where the standard staff and the tab grid update together as you type, with a playback engine good enough to actually hear what you've written.
It also turned into a deliberate exercise in shipping a non-trivial system end-to-end: parser, layout engine, interactive editor, audio synthesis, file format, and deployment.
How it evolved
The work fell into roughly five phases over March–May 2026.
Phase 1 — Python rendering pipeline (March 16–22)
Started as a pure-Python project: ASCII tab parser, layout engine, and an SVG renderer. The renderer output static .svg files from text input — no interactivity, no web app. This phase established the data model (Tab → Measure → Note) and the engraving primitives (staff lines, clefs, fret numbers, bar lines) that the rest of the project still leans on.
A small FastAPI wrapper came next so the renderer could be driven from a browser, plus a separate MIDI parser to convert .mid files into the same Tab model using a lowest-fret heuristic for string assignment.
Phase 2 — From renderer to editor (March 22–29)
The big architectural shift: stop rendering immutable SVGs and instead build an interactive editor in React. I introduced a new score model (TrackScore with measures, voices, beats, note effects, tempo, tuning) living entirely on the frontend, and rewrote the UI around clicking and typing directly on the rendered tab.
A lot of this phase was engraving detail work that doesn't show up in a feature list but determines whether the output actually looks like sheet music:
- Conventional staff spacing relative to the tab
- The Noto Music font for noteheads, rests, and clefs
- Chord rendering (stacked noteheads on a shared stem)
- Bar lines that don't cut through fret numbers
- A selection box that highlights position without obscuring the note
Phase 3 — Notation features (March 29 – April 2)
Filling in the things a real tab needs: rests, ties, hammer-ons, pull-offs, slides, arrow-key navigation, deleting notes via selection, adding and removing bars from a dropdown, dynamic note spacing so dense passages don't collide. A short detour into a separate design branch (feature/design) was eventually merged back, with the harder bits being to restore feature parity in the new layout.
Phase 4 — Audio (April 3–7)
Three sub-problems, each interesting in its own way:
- Playback synth — Built a Tone.js-based synth that schedules notes with proper sustain (originally I was only triggering note onsets, which sounded staccato regardless of the written rhythm).
- Fallback synth — Tone.js requires a secure context, so the app falls back to a raw Web Audio implementation on non-HTTPS deployments. Same interface, different backend.
- Audio transcription — Upload a WAV/MP3/M4A and get an editable tab out. This runs as a background worker with SQLite-tracked job state. It's disabled on Vercel (returns
501) because that environment can't run the worker, ffmpeg, or persistent state — local-only.
Phase 5 — Save/load, deploy, polish (April 5 – May 20)
Native .tabscore.json project files (serialization, validation, round-trip tests), Vercel deployment with a prepare_public.py script to wire the Vite build into Vercel's expected layout, and a final synth upgrade pass — replacing the basic oscillator with a more guitar-like patch.
Decisions worth calling out
Two parallel data models, kept on purpose. The legacy ASCII-tab pipeline and the new TrackScore editor model both still exist. The temptation was to unify them, but the ASCII path is genuinely better for one-shot rendering of pasted tabs, while the score model is what the editor needs for fast in-place edits. They meet at the MIDI importer, which writes into both.
Backend does the heavy parsing; frontend owns the editor state. MIDI parsing and audio transcription stay in Python (mature libraries, easier ffmpeg integration). The interactive score lives entirely on the frontend so edits don't round-trip through the server. Save/load is a pure frontend file download — no server-side project storage to manage.
Engraving conventions over invented ones. Early versions drew the staff and tab with arbitrary spacing. Sheet music has conventions (notehead spacing, stem direction, beam grouping in 4/4) that look "right" without the reader knowing why. Adopting them was a one-day change that did more for perceived quality than weeks of feature work.
Graceful degradation for the deploy target. Web Audio fallback when Tone.js can't run, transcription disabled with a clear 501 when the host can't support it, ASCII rendering as a fast path that doesn't need the editor at all. Each one came from hitting a real limitation rather than designing it up front.
What I'd do differently
- Land the score model first. I built the ASCII renderer before deciding the editor needed its own model, then carried the legacy model along forever. Knowing what I know now, I'd have started with the interactive model and treated SVG export as the secondary path.
- Test the rendering, not just the parsing. Most tests cover parsing and score-model logic. Visual regressions in the SVG engraving I caught by eye, which doesn't scale. Snapshot testing the SVG output would have been a small upfront cost.
- Choose the synth approach earlier. I spent real time on the basic oscillator synth before realising it would never sound right for guitar; the eventual upgrade was the right move from day one.
What's in the repo
tab_renderer/parser/— ASCII and MIDI parsers (Python)tab_renderer/renderer/— layout engine and SVG renderertab_renderer/transcription/— background-worker audio transcription servicetab_renderer/web/app.py— FastAPI entrypointfrontend/src/scoreModel.ts— the editor's score modelfrontend/src/App.tsx— the editor UIfrontend/src/playback.ts— Tone.js + fallback Web Audio synthfrontend/src/projectFile.ts—.tabscore.jsonsave/loadtests/— pytest suite for parsers, web API, transcriptionfrontend/src/*.test.ts— Vitest coverage for music logic, score model, playback, import/export