Skip to content

feat(render): show transposed guitar tabs on playable strings - #3007

Merged
Danielku15 merged 12 commits into
developfrom
feature/tab-transpose
Nov 1, 2026
Merged

Danielku15 merged 12 commits into
developfrom
feature/tab-transpose

Conversation

@Danielku15

Copy link
Copy Markdown
Member

Note

AI-authored disclosure (alphatab-ai-authored-v1)

Portions of this content were authored by an AI agent. The agent has read
AGENTS.md and the human submitter accepts responsibility for
compliance with the rules in that document.

Issues

Fixes #1587

Proposed changes

Problem

With notation.transpositionPitches (or alphaTex \transpose), the tab subtracts the transposition from the fret and never changes the string. Transposing down turns open strings into negative frets (-4); transposing up pushes notes beyond the neck.

Behavior

When a staff with tablature is transposed, every note is placed on a playable position of the staff's tuning:

  • A note keeps its string while the shifted fret is playable there (0 to 24 minus the capo, or higher if the original note already uses a higher fret). Tabs that were valid before render unchanged.
  • Notes continuing a tie, slide or hammer-on/pull-off follow the string of their origin, including across barlines.
  • Other notes take the free string whose fret is closest to the frets of the beat or of the previous beat. Notes of one chord never share a string.
  • Bends, slides, vibrato, tapped notes and hammer-on destinations never land on an open string; pull-offs to an open string are allowed. Trills need a playable trill fret.
  • Natural harmonics move to the node that sounds the transposed pitch.
  • A note that no string can play keeps its string and its out-of-range fret. It is painted in the new display.resources.tabUnplayableFretColor (default red). If another note of the beat took that string, the note moves to the free string where its fret is closest to the neck instead of overlapping it.
  • String-number circles in the standard notation show the displayed string. Left-hand finger numbers on the score staff are hidden for notes that moved or became open strings.

The model is not changed. The positions exist only on the rendered tab, so changing transpositionPitches at runtime takes effect on the next render.

Design

TabFingering decides the positions of a beat while TabBeatGlyph creates its note glyphs. It reads only the beat, the glyphs of earlier beats and the tie/slide/hammer-on links of the notes. The result lives on NoteNumberGlyph (displayString, displayFret, isMoved, isUnplayable); ties, slurs, bends, brushes, flags, the skyline and the staff-line gaps read it from there. Nothing is stored on the model or the layout, and there is no extra model traversal during rendering. A partial render restarts at the bar of the earliest origin of a note in the changed bar, so a placement whose origin is in an earlier system is decided again.

Untransposed staves and percussion staves take the existing path unchanged.

Platform

Fixes for gaps found while implementing this:

  • C#/Kotlin runtime: Int8Array; fill/subarray/set for Int16Array and Uint16Array; Kotlin DoubleList.splice now matches JS (it copied the wrong range for removals before the end of the list) and has the inserting overload; Kotlin BooleanList.fill and List.fill return the list.
  • Transpiler: compound bitwise assignments (a |= b) cast the right operand to int like plain bitwise operators; a non-null assertion on pop()/shift() of a number array no longer emits .Value in C#.

Measurements

  • Over all Guitar Pro files in test-data (342 files, transposed by −4, +3 and +7): moved notes keep the hand movement per beat of a segment-wide beam-search prototype (1.55 vs 1.56 frets), with 3.7% more unplayable notes and slightly wider chords (2.62 vs 2.44 frets).
  • Allocation per render of test-data/conversion/full-song.gp, compared with develop: no measurable difference untransposed; about +0.3 MB at −4.

Known limitations and follow-ups

  • Chord diagrams, chord names and the barre text are not transposed (as before). A follow-up issue will cover them.
  • Placement is greedy per beat without lookahead. Voices are placed independently, so notes of different voices sounding at the same time can share a string.
  • A natural harmonic without a node at the transposed pitch is marked unplayable.
  • Left-hand finger numbers in the effect-band fingering modes are not hidden for moved notes.
  • Possible follow-up: show an unplayable note on its own string next to the note occupying it, instead of moving it to another string.
  • The banjo drone string and partial capos are not modelled; all strings count from the same nut and capo.

Checklist

  • I consent that this change becomes part of alphaTab under its current or any future open source license
  • This PR is linked to an accepted issue (see above)
  • Changes are implemented
  • New tests were added
  • I have read AGENTS.md if an AI helped draft any part of this PR

Tests:

  • 19 visual tests in GuitarTabs.test.ts (transpose-*), each rendering the original and the transposed track. They cover open strings, chords, ties across barlines, hammer-ons/slides, pull-offs to open strings, bends, natural harmonics, dead notes, trills, ukulele, bass, capo, grace notes, repeated bars and multiple voices.
  • TabFingeringPartialUpdate.test.ts: a partial render matches a fresh render when a hammer-on crosses a system boundary.
  • TypeScript (2400), C# (2261) and Kotlin (2261) suites pass.

AI authorship disclosure

  • No AI agent authored any part of this PR (description, code, tests, or commit messages)
  • An AI agent contributed to this PR. The AI-authored disclosure block
    (alphatab-ai-authored-v1) is present at the top of this body, and I have
    personally reviewed every change and can explain each one

Further details

  • This is a breaking change
  • Docs / release notes updated in website/

…C# and Kotlin

Adds an Int8Array runtime type to the C# and Kotlin ports and gives
Int16Array/Uint16Array the same fill/subarray/set surface as Int32Array.
The transpiler no longer rejects Int8Array and strips its type arguments
like the other typed arrays.
…eric code

- Compound bitwise assignments (a |= b) now cast the right operand to int
  like plain bitwise operators do; before, a double right operand did not
  compile in C# or Kotlin.
- A non-null assertion on pop()/shift() of a number/boolean array no longer
  emits .Value in C#; the runtime helpers return the plain element type.
- Kotlin DoubleList.splice(start, deleteCount) copied the wrong range and
  corrupted or threw for removals before the end; it now matches JS and
  clamps like JS. Adds the inserting splice(start, deleteCount, ...items).
- Kotlin BooleanList.fill and List.fill return the list like JS fill.
… staves

TabFingeringSolver decides on which string and fret tab notes are shown
when the staff is transposed, purely from the model. Notes keep their
string whenever the transposed fret is playable; otherwise the bars
connected by ties, slides and hammer-ons/pull-offs are re-fingered with a
beam search preferring fewer unplayable notes, fewer moved notes and less
hand movement and stretch. Natural harmonics are mapped to the node of
the transposed pitch, or shown as artificial harmonic when none exists.

Not yet wired into rendering.
When a staff with tablature is transposed (notation.transpositionPitches,
\transpose), the tab no longer just subtracts the transposition from the
fret. TabBarRenderer resolves the fingering of the segment of linked bars
once per layout via TabFingeringSolver and all tab glyphs read the
displayed string and fret from it: fret numbers, note lines, staff line
gaps, ties, slurs, bends, brushes, flags and the harmonic labels. The
model stays untouched.

Notes which fit on no string keep their string and the out-of-range fret
and are painted in the new RenderingResources.tabUnplayableFretColor.
String-number circles show the displayed string and left-hand finger
numbers of re-fingered notes are hidden. A partial render restarts at the
first bar of the affected segment.
…he search buffers

The fast path of TabFingeringSolver reads the model directly instead of
building the note tables first, so segments keeping their fingering
allocate nothing. The beam search keeps only the states of the previous
slice plus back pointers and new-group deltas in typed arrays, merges the
voices instead of sorting beats and avoids splice and iterator
allocations in its loops.

Natural harmonics needing the artificial-harmonic fallback on their own
string no longer pass the fast path, which displayed them at the wrong
pitch.
Adds a 'Transposed tablature' section to the guitar tablature page, the
new tabUnplayableFretColor to the palette page, the 1.10 release notes
and the behavior to the transpositionPitches JSDoc.
…etup for unchanged segments

Costs are compared by candidate slot and passed to and from the slice
scoring through an array, so no doubles cross function boundaries. The
fast path runs before the solver state is created, so segments keeping
their fingering allocate nothing. Back pointers are stored as bytes.
… out the tab

Replaces the segment-wide beam search (TabFingeringSolver,
TabFingeringSegment) by TabFingering, which decides the string of each
note while TabBeatGlyph creates its note glyphs and keeps the result on
the NoteNumberGlyph. It only reads the beat, the glyphs of earlier beats
and the tie/slide/hammer-on links of the notes: continued notes follow
their origin, notes playable on their string keep it, the others take
the free string closest to the frets of the beat or the previous beat.

Natural harmonics without a node at the transposed pitch are marked
unplayable instead of being shown as artificial harmonics.
…n by other notes

A note which no string can play keeps its own string only if no other note
of the beat took it; otherwise it moves to the free string where its fret
is closest to the neck instead of overlapping the other note.
…and transposed track

Each case renders the original track and the track transposed through
notation.transpositionPitches one above the other. Replaces the
render-based unit test harness, which the visual cases now cover.
@Danielku15
Danielku15 merged commit 7d3efe7 into develop Nov 1, 2026
12 checks passed
@Danielku15
Danielku15 deleted the feature/tab-transpose branch November 1, 2026 16:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Improvement: Transposing tabs with string adjustments.

1 participant