Guitar Transcription Workstation

Claude Code plus Python for auditing and repairing Guitar Pro tabs, with Guitar Pro for human verification.

Jacks-iMac · x86_64 · macOS 13.7.8 · built and measured 2026-08-28

Read this first

Four premises in the proposed setup do not hold on this machine, and the fixes cost less work than the original plan. This page carries the corrections, the live inventory, the repo that got built, and the one thing still blocked.

Contents

  1. The three phases
  2. Four corrections
  3. Verified inventory
  4. What got built
  5. Mundo Cani, live result
  6. The 87/88 octave probe
  7. Beat maps, derived
  8. The one blocker

Phase plan

Phase 1. Verify the machine before installing anything

Read the actual interpreter, architecture, package versions, and app list. Most of the proposed stack was present before this session, and one proposed step would have reintroduced a known crash. done

Phase 2. Build the repo around the gates that exist

Twenty five validators live in ~/.claude/skills/impossible-guitar-parts/, tuned against recorded regression cases. The new repo calls them and never forks them. Ships doctor.py, validate_song.py, env.sh, and a pytest suite. done

Phase 3. Prove it on the named song and publish the state

validate_song.py "Mundo Cani" runs end to end against the real corpus and surfaces a finding the plan did not anticipate. This page is the showcase. done

Four corrections to the proposal

1. This Mac is Intel, not Apple Silicon

uname -m returns x86_64 on macOS 13.7.8. The Apple Silicon rationale for pinning Basic Pitch to Python 3.10 does not apply here.

2. A new 3.10 Basic Pitch environment would rebuild a working one and reintroduce a crash

~/venvs/audio_midi_311 holds basic-pitch 0.4.0 on Python 3.11.15 with onnxruntime 1.23.2. The constraint on this machine is the model format. The Python version is not what breaks. Basic Pitch defaults to:

/Users/drwu/venvs/audio_midi_311/lib/python3.11/site-packages/
    basic_pitch/saved_models/icassp_2022/nmp.mlpackage

That CoreML package SIGFPEs on this Mac. nmp.onnx ships in the same directory, so no download is needed. env.sh pins BASIC_PITCH_MODEL_TYPE=onnx, and a pytest case fails if that pin is ever dropped.

3. The validator stack was installed before this session, minus one package

Python 3.13.6 at /usr/local/bin/python3 carried pyguitarpro, lxml, numpy, scipy, librosa, soundfile, audioread, mido, pretty_midi, pandas, matplotlib, pydub, and music21. Only pytest was missing, now installed at 9.1.1. Building a fresh 3.12 venv would have duplicated thirteen working packages. Demucs 4.0.1 is installed in ~/venvs/demucs on Python 3.12.13 with torch 2.2.2.

4. validate_song.py mostly exists

The gates for pitch preservation, impossible spans, string collisions, ownership, octave doubling, tuning, and candidate versus source statistics are written and tuned. Writing a second set from scratch would have thrown away the recorded regression cases. The new script is an orchestrator over them, roughly 130 lines.

Verified inventory

33
doctor checks
1
hard failure left
25
gates written before this
46
guitar stems on disk
ComponentProposedFound on the machineAction
Homebrewinstall6.0.15present
gitinstall2.39.2 (Apple)present
ffmpeginstall8.1.1present
uvinstall0.11.29present
Claude Codebrew cask~/.local/bin/claudepresent
Python 3.12 validator envnew venv3.13.6 with 13 of 14 pkgsused in place
pytestin the venvabsentinstalled 9.1.1
Python 3.10 Basic Pitch envnew venv3.11.15, basic-pitch 0.4.0reused, ONNX pinned
Demucsnew venv4.0.1, torch 2.2.2present
Sonic Visualiserinstallabsentinstalled 5.2.1
Guitar Pro 8installabsentneeds your password
MuseScore 4auxiliaryabsentleft out, optional

What got built

Repo at ~/Music/guitar-transcription, git initialised, one commit.

A gate that cannot run reports NOT RUN

The audio gates need --stem --offset --bpm. Without a validated beat map they are skipped and printed as NOT RUN. A skipped gate is never folded into a pass.

. ~/Music/guitar-transcription/env.sh
python3 doctor.py
python3 validate_song.py "Mundo Cani"
python3 -m pytest tests/ -q

Mundo Cani, live result

The command from the proposal, run for real against 05_TWO_STAFF_SPLIT/Mare-Vitalis-01-The Immortal Soul of Mundo Cani__beamfix__split.gp.

FileDoes
env.shPins the three interpreters and BASIC_PITCH_MODEL_TYPE=onnx. Source it before anything.
doctor.py33 live checks across CLI tools, packages, both transcription environments, the GUI apps, the six gates, and the corpus. Exits non zero on a hard failure.
validate_song.pyTakes a path or part of a song title, resolves the candidate and its guitar stem out of the corpus, runs the gates, prints one verdict, writes JSON to reports/.
octave_probe.pyTests the 87/88 harmonic hypothesis. Known-answer self test, count matched control, wrong time control, two anchor map health gate. Refuses to publish a verdict on a suspect map.
beatmap_fit.pyDerives offset and tempo from the stem by onset correlation. Thresholds calibrated against positive and negative controls. Refuses a map that cannot beat both.
tests/test_workstation.pyThe rules as tests. Package imports, the ONNX pin, gate presence, the human tab regression anchor, and a check that no pitch exceeds a 22 fret neck.
GateVerdictWhat it found
playabilityreview585 notes, 539 attacks, hand skip 42 percent, SHIFT x4, severe JUMP x7, isolated octave x6, ownership suspect x9, tuning evidence flagged. BPM read as 172.
octave copy and note losspass0.0 percent copy rate on both guitar staves. Neither staff is the other at fret plus twelve.
audio accuracynot runNeeds a validated beat map. The stem was resolved automatically and is ready.
duration and tienot runSame requirement.

The finding the run surfaced

The existing two staff split of this song put 585 notes on Guitar and 1 note on Guitar 2. The split ran and produced a file, and the file does almost nothing. The 42 percent hand skip and the nine ownership flags remain on the first staff. Exit zero and a written file prove only that the tool ran. Neither measures whether the output is good.

Another session is editing that same tree right now. The delivery README changed on disk mid read. Nothing here writes into ~/Projects/_outputs/APPLESEED-DELIVERY-2026-08-28/, and coordinating before any repair pass is the safe move.

On two guitar ownership for Mare Vitalis

The proposal routes through a two guitar ownership step. On this album that step was measured on 2026-08-11 and it does not separate. Both players fall within about 1 dB of each other in the stereo image where roughly 12 dB would be needed to assign a note. Ownership scored at chance across all 46 songs. The workable route is the one the corpus README describes: split an unplayable beat into two hands by register and reach, change no pitches, and treat the audio as evidence for pitch rather than for authorship.

The 87/88 octave probe

Task 2 in the corpus README: 19 bad beats hold a pitch that does not exist on a 22 fret guitar, only MIDI 87 and 88, across five songs. The suspicion is that the transcriber heard the second harmonic of a note an octave below and wrote the harmonic as the note. octave_probe.py tests that. It measures band energy at the written pitch against the band an octave below, in the guitar stem, at each note's own time.

The measure was validated before it was pointed at any music

Two synthetic signals with a known answer, run on every invocation. The probe refuses to read audio if either fails.

SELF TEST  (synthetic signals, known answer)
  [ok] written_pitch_is_real        ratio 6051095.288
  [ok] octave_below_with_harmonic   ratio       0.400
  => the measure separates the two cases

Map free finding: all 19 are one written position class

This needs no audio and no time map, so it holds regardless of everything below.

SongCountWritten position
30 Degrees 3 Am1string 5, fret 23, pitch 87
Barrier Islands (Do We Remain)4string 5, fret 24, pitch 88
Sentence7string 5, fret 24, pitch 88
Convict3string 5, fret 23 x2 and fret 24 x1
Confession4string 5, fret 24, pitch 88
Total1988 on str5/fret24 x16, 87 on str5/fret23 x3

Every one is on string 5, the high E, at fret 23 or 24. Not one appears on any other string. Since 87 and 88 have no legal position on a 22 fret neck, the writer had nowhere to put them and fell back to arithmetic fret on the top string. That makes this one systematic fallback firing 19 times rather than 19 independent mishearings, and it points the investigation at the pitch detector rather than the fingering.

No octave verdict yet, because the beat map fails its own control

On Sentence, with the score tempo of 200 bpm at offset 0:

map anchor 1  notated onsets to nearest real attack:  median 88 ms
              SAME map pointed 11 s off the mark:    median 83 ms
              -> MAP DOES NOT BEAT ITS CONTROL
map anchor 2  notated end 153.5 s vs audio 177.5 s (13.5% drift)
MAP           SUSPECT

Pointing the map eleven seconds away from the music aligns slightly better than pointing it at the music. A map that cannot beat its own wrong time control does not locate notes, so the per note ratios below carry no information. The wrong time ratio control agrees: median 1.31 against 1.122 for the flagged notes.

The probe prints MAP SUSPECT and withholds the verdict. That is the gate doing its job. The existing 04_TABLES/octave_test.json shows offenders at median 2.17 against a decoy control at 1.77, which is very likely the same problem: with a broken time map, offenders and decoys measure the same thing.

What unblocks it

A validated beat map for these five songs, real offset and real tempo anchored at two musical events, rather than the score tempo at offset zero. octave_probe.py takes --offset and --bpm, so the re run costs minutes once those exist.

Beat maps, derived from the audio

beatmap_fit.py derives offset and tempo per song by cross correlating the notated onset train against the stem's onset strength envelope. It then refuses the fit unless the fitted map covers more real attacks than two controls: the same map shifted eleven seconds, and random times of the same count.

The thresholds are calibrated, not invented

The first version invented a rule and a positive control caught it. Feeding the fitter a synthetic recording built from a known map, it recovered that map to 0.1 bpm and 3 ms, and the invented rule rejected it anyway. A second attempt used the lag z score to SELECT the tempo, which preferred a compressed train and chose 246 bpm over the true 172. Selection now uses the correlation peak, which recovers the truth, and the z score is computed afterwards purely as the acceptance statistic.

Three trials, each with a positive control (the tab that made the audio) and a negative control (a different rhythm of the same density against the same audio):

lag z    positive 6.29 - 7.87    negative 2.63 - 2.77
lift     positive 1.163 - 1.366   negative 0.819 - 0.961

A fourth positive scored z 4.38, so the bar sits at z 4.0 and lift 1.08: under every positive observed, over every negative observed.

SongScore bpmFitted bpmOffsetzLift vs randomLift vs shiftedNotated / attacksVerdict
Sentence200191.251.161 s4.391.3211.267898 / 467accepted
30 Degrees 3 Am161161.2013.421 s5.231.0100.9981374 / 517refused
Barrier Islands (Do We Remain)148147.603.611 s2.950.9490.9701573 / 347refused
Convict184178.0530.174 s5.851.4430.944742 / 253refused
Confession174158.0511.424 s3.870.9481.0782702 / 891refused

Why four refuse, and it is a finding rather than a failure

Look at the last column. These tabs carry 1.9 to 4.5 times as many note onsets as the guitar stem has detectable attacks. Barrier Islands notates 1573 onsets against 347 attacks. When a notation is that much denser than the recording, almost any alignment puts a fifth of its onsets near something, so the map cannot be told apart from a random one. Only Sentence, at 1.9x, is sparse enough to align.

That density is consistent with the two guitar merge already documented: two players written onto one staff produces roughly twice the onsets a single stem shows.

The octave verdict, Sentence only

With the accepted map, all seven flagged notes re measured. The probe now takes its map verdict from beatmap_fit.py rather than carrying a second threshold of its own.

BarTimePositionE writtenE octave belowRatioReading
5158.34 sstr5 fret2413.24924.1260.549octave below, weak
7383.51 sstr5 fret24135.757105.7781.283ambiguous
90104.06 sstr5 fret248.98120.8260.431octave below
92106.10 sstr5 fret24127.707123.1351.037ambiguous
108125.08 sstr5 fret2420.7417.3162.835written pitch
110127.67 sstr5 fret2452.55787.7660.599octave below, weak
111128.14 sstr5 fret2451.28291.0590.563octave below, weak

The controls, which is what makes this readable

flagged notes             median ratio  0.599
count-matched other notes median ratio  7.083
same notes, wrong time    median ratio  1.066

Ordinary notes in this file put roughly seven times more energy at the written pitch than an octave below. The flagged notes invert that, at 0.599. Pointed at the wrong time the same seven notes read 1.066, so the effect is time locked rather than a property of the spectrum at large. On the earlier broken map the flagged median was 1.122 against a wrong time control of 1.31, which is why the probe withheld the verdict then.

Reading it honestly

Four of the seven point to a note an octave below, two are ambiguous, one matches the written pitch. Since MIDI 88 cannot be played anywhere on a 22 fret guitar, the one reading at 2.835 cannot be a guitar fundamental at that pitch either; the likelier explanations are another instrument bleeding into the stem, or a partial of a different note. The majority evidence supports the harmonic hypothesis on this song, and one song is not five.

The one blocker

Guitar Pro 8 needs an admin password

The Homebrew cask ships a pkg installer, so it prompts for a password that I will not enter on your behalf. Sonic Visualiser installed cleanly because its cask is a plain app bundle. Run this yourself and doctor.py goes to zero hard failures:

brew install --cask guitar-pro

Optional, if the auxiliary conversion path is wanted: brew install --cask musescore. Keeping it away from the authoritative editing path stays correct, since conversion can drop Guitar Pro specific detail.