Skip to content

1.9.0 hardening: fix release-review findings across rendering, importers, player and packaging - #2990

Merged
Danielku15 merged 102 commits into
developfrom
release/1.9.0-prep
Oct 29, 2026
Merged

Danielku15 merged 102 commits into
developfrom
release/1.9.0-prep

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

Most items in this PR do not come from issues. They are findings of the 1.9.0 release review, done by the maintainer before cutting the release branch (scope frozen at 57 items).

Proposed changes

This PR prepares develop for the 1.9.0 release branch. It contains the fixes from the 1.9.0 release review, a regression review of those fixes, and a pass that slims the code added for 1.9, on top of the attribution and player work that started the branch.

Every fix is made in the layer that causes the problem (model, importer, layout, renderer, player, packaging), and every fix comes with a test. Visual references change only where a fix intends it, and the maintainer reviewed every changed reference.

Rendering and layout

  • Attribution: "♪ alphaTab.net" mark at the end of the first system. It is kept clear of effect bands, neighbouring text and the multi-bar rest count.
  • Effect placement: bend and whammy slurs and tab bends enter the skyline as their drawn shape, so "grad.", let-ring and other labels no longer sit on them. Dynamics below the staff clear the accents. The bracket spike is reserved only once.
  • Spacing:
    • Squeezed bars take space from the generous springs first and keep the minimum widths of their content.
    • Full-bar notes are centred between the header and the barline; pick-up bars and empty voices no longer break centring.
    • Grace-only bars place their grace notes side by side.
    • Dense dynamics get horizontal room.
    • Ledger lines of neighbouring notes stay apart.
  • Overlay texts: beat text, lyrics, free-time text and dynamics are spaced only against their own staff row; texts on other tracks no longer widen the bar.
  • Bar numbers: shown on multi-bar rests, not clipped at partial boundaries in the horizontal layout, and placed on the first visible track with hideEmptyStaves.
  • Other fixes:
    • String-number circles are stacked by pitch.
    • Adjacent separate trills are drawn as separate trills.
    • A two-voice unison whose note head is displaced by a second no longer crashes.
    • Rest-only beams in numbered notation no longer crash.
    • Top-aligned canvas text sits at the same height in Firefox as in Chromium.

Importers and model

  • JSON: unset note values survive a JSON round trip, and 1.8-style -1 values load as unset. RenderingResourcesJson keeps the deprecated font keys.
  • Guitar Pro 3–5:
    • The MIDI port and channel are range-checked again.
    • Fret −1 becomes a dead note; notes without a fret byte are dropped.
  • Drum tablature: shown according to the file's flag (GP5 staff flags, GP6–8 PartConfiguration), hidden when the file has none.
  • GP6 / GPIF: ties survive on staves that get a synthesized fingering. Track pan and volume come from the RSE channel strip.
  • MusicXML:
    • measure-numbering holds until it changes.
    • A string without a fret on a tuned staff gets its fret from the pitch.
    • Notes without a tab position get no tab glyphs.
  • Model: Note.isTrillStart marks where a separate trill starts. A custom bar number of 0 is respected.

Player and platform

  • Cursor handlers: the default cursor handlers are public API (ToNextBeatAnimatingCursorHandler, NonAnimatingCursorHandler), and a newly set custom handler is attached.
  • Scroll mode: changes after load (updateSettings) take effect.
  • Backing track: when the browser refuses playback, the player pauses and no longer reports Playing.
  • Worker renderer: bounds resolve against the score that was actually rendered, so a render started while load() is still running no longer throws.
  • Audio export: export uses the presets the exported song needs.

Transpiler and native ports

  • Deprecated property accessors now emit [Obsolete] in C# and @Deprecated in Kotlin, together with their XML docs / KDoc.

Packaging and tooling

  • Vite plugin:
    • Peer range is ^7 || ^8 again, and Dependabot no longer rewrites peer ranges.
    • The worker and worklet bundles are minified on Vite 8.
    • The plugin declarations import the Plugin type.
  • Language server:
    • Loadable with require (default export leaves plus ./package.json).
    • The bin starts with a hashbang.
    • It and Monaco declare the vscode-languageserver packages they import.
  • Deprecated subpaths: @coderline/alphatab/vite and /webpack forward to the plugin packages, declared as optional peers.
  • Other:
    • AlphaTexAtTokenNode is exported.
    • CI uploads the web test results when the web tests fail.

Docs and code size

  • Doc comments: staffPaddingLeft and systemsLayoutMode document their actual defaults, with a test that checks every documented settings default. The new 1.9.0 API carries @since 1.9.0.
  • Code size: the bend, whammy, tie and attribution code shares one measure path, one skyline insert loop and generated glyph bounds. The tests added for 1.9 share their helpers instead of copying them. This leaves about 380 fewer lines, with no change in output.

Behaviour changes integrators may notice

  • Guitar Pro 3–5 files assign MIDI channels and ports per track from the file, and ports are zero-based.
  • AutomationType.Bank is 5 (it had the same value as SyncPoint in 1.8).
  • SVG output is not byte-identical between renders, because of the attribution path.

Testing

  • Suites: TS 96 files / 2082 tests, C# 2062/2062, Kotlin 2062/2062.
  • Corpus harness (563 files): compared against the state before the hardening; every difference in output, model, MIDI and rendered bounds is explained by one of the fixes. Compared before and after the code-size pass: identical model, MIDI and bounds, and no pixel differences.

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

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
  • This change will require update of the documentation/website (release notes and generated reference: companion changes in the alphaTabWebsite repository)

…channel strip

Read the mixer values directly from the channel strip parameters and
cover the import with generated scores instead of binary fixtures,
including fractional, unreadable and missing values.
…file

Exercise the full volume and pan ranges, values between the steps of
the internal range, parts with multiple instruments and parts without
mixer values.
Inspect the exported score node through the XML DOM and share a new
helper to extract the score.gpif from exported files.
Setting customCursorHandler while cursors exist called onDetach instead of
onAttach on the new handler, so it never received its attach callback.
HTMLMediaElement.play() rejects when the request is interrupted, e.g. by
pause() or a new src right after play(). The rejection was unhandled and
surfaced as an uncaught page error. Interruptions (AbortError) are now
ignored, other failures are logged as warning.
ToNextBeatAnimatingCursorHandler and NonAnimatingCursorHandler are now
public so custom ICursorHandler implementations can delegate to the
built-in cursor placement.
Rest-only beaming helpers (painted in numbered notation, e.g. whole rests)
have no notes, so the beam envelope dereferenced a missing lowest/highest
note. Anchor the envelope on the rest instead.
Draws the attribution above the top staff at the end of the first system,
as path outlines (Noto Sans Bold text, Bravura eighth note) merged into the
staff lines of the hosting bar. Shown when the song has more than 10 bars or
more than 2 tracks are rendered. The outlines are generated by the new
attribution-outlines script (opentype.js dev dependency).
Notes in README, AGENTS.md, the license header, the package description and
the settings API docs that the attribution must stay visible, and credits
Bravura for the glyph outline.
The full render result and the resized image on size mismatches were never
disposed, leaking ~3 GB of native alphaSkia memory over a Kotlin test run.
Runs the Kotlin compiler inside the Gradle daemon instead of a second 6 GB
JVM, sizes the daemon heap to 2 GB (measured live set ~0.8 GB), lets G1
return idle heap and stops idle daemons after 15 minutes. A clean build with
tests peaks at ~3 GB instead of ~8 GB.
…ild tooling

Dev server and vitest configs imported tooling/src/vite.ts only for
buildTsconfigAliases, which pulled in api-extractor, terser and the rollup
plugins (~135 MB per process).
Pre-bundling both at server start retained ~500 MB of native memory. Monaco
is now loaded on demand, lucide icons are imported per icon. The browser
only opens when the dev server runs in an interactive terminal.
The attribution rested on the staff skyline before effect bands were
placed, so bands under it (jump texts, chord names, bar numbers) were
pushed up. It is now placed after the bands against the skyline that
includes them, between EffectSystemPlacement.place() and apply(), so
notation keeps its position and the staff only grows when the mark
reaches above the space already reserved.
The "rendered by alphaTab" line below the score is gone, and scores
with more than 10 bars or more than 2 rendered tracks show the
"♪ alphaTab.net" mark at the end of the first system. Adds the missing
reference for musicxml3/track-volume-balance.
…st project

The OS-conditional package references are not evaluated by Dependabot on
Linux, so they stayed on 3.4.135 while AlphaSkia moved to 3.5.147.
colors-disabled and single-staff-brackets-hide still carried the removed footer, so color-performance and single-staff-brackets failed on all platforms.
Dependabot's dev-dependency group updates narrowed the peer range to ^8.3.0, so installs with Vite 7 or Vite 8.0-8.2 failed with ERESOLVE although the plugin works with both. The range is back to ^7 || ^8, and dependabot now keeps ranges that already allow the new version (increase-if-necessary) instead of raising them.
The exports map had only import leaves, so require(), require.resolve() and the package.json lookup failed with ERR_PACKAGE_PATH_NOT_EXPORTED. The entries now use default leaves, which Node's require(esm) can load, and ./package.json is exported.
The node is public and AlphaTexNoteNode.noteStringSeparator can hold it, but it was missing from the alphaTex barrel and therefore from alphaTab.d.ts.
The public plugin modules took Plugin from an internal re-export in the bridge. The per-file d.ts filter removed that re-export and the imports of it, so the published declarations referenced an unbound Plugin, which resolves to the DOM type and fails with TS2315 when skipLibCheck is off. The modules now import Plugin from vite, and the d.ts filter fails the build when a kept declaration still references a type whose import it removed.
The language server imports vscode-languageserver-protocol and -types, and the Monaco worker, which bundles the server source, imports vscode-languageserver, -protocol and -textdocument. These resolved only through hoisting, so strict layouts like pnpm or Yarn PnP could fail. Both packages now declare them, and the shared build config fails when a chunk statically imports a package its package.json does not declare.
dist/index.node.mjs began with the licence banner, so running alphatab-language-server through npx or node_modules/.bin on Linux and macOS made the shell interpret it and fail with a syntax error. The build now prepends #!/usr/bin/env node to the bin entry, after the banner is added.
Rolldown minifies as an output option, which the worker bundling did not pass, so on Vite 8 the alphaTab worker and worklet were emitted unminified (about twice the size). The option is now derived from the worker environment's build.minify the same way Vite does for its own workers.
The AlphaTexImporterNew/Old snapshots have no test files any more, and effects-and-annotations/rhythm.png, rhythm-with-beams.png and 'brush - Copy.png' are referenced by no test (the rhythm tests render into guitar-tabs/).
The test id was on the build-monaco step, so the upload of new and diff reference images only ran when the Monaco build failed, never for failing web tests.
The comments in SystemSpacing.test.ts described a default spacingRatio of 1.5 and the proportions derived from it, while the default is √2 and the references show the √2 progression.
…he plugin packages

Since 1.8.0 @coderline/alphatab/vite and /webpack could not load: prepack copied the plugin builds to dist/vite and dist/webpack, but the package files did not include them, and the CommonJS shims required file names that do not exist. The shims now re-export @coderline/alphatab-vite and @coderline/alphatab-webpack, which @coderline/alphatab declares as optional peers, and the copies are gone. Without the plugin package installed, the import fails with a missing-package error that names it.
Note and Beat number fields that default to NaN (fret, string, octave,
tone, percussionArticulation, restDisplayTone/Octave) were written as
NaN, which JSON turns into null, and read back as null. After
scoreToJson -> jsonToScore every note was classified as percussion, MIDI
keys became 0/NaN and some scores threw in Score.finish.

The serializer generator now omits NaN-defaulted numbers when unset and
reads null/undefined back as NaN. Notes written by 1.8 and earlier carry
all five pitch keys with -1 for unset values; Note.afterFromJson maps
those to NaN (fret only when the string is unset, since stringed notes
can have fret -1).

The JSON comparison helper skips numbers without counterpart, matching
how it already treated NaN values.
Bend and whammy slurs entered the skyline as one box from their start
to their end at the height of their peak, so effects and bar numbers
beside the low part of a slur were pushed up as far as the peak. Tab
bends did not enter the skyline at all, so a let-ring was drawn under
the bend line and its "full" label.

Ties can now describe their drawn extent as a shape: lines and curves
split into narrow steps, labels, arrows and helper notes as their own
boxes. Standard-notation bends and whammys and tab bends record the
shape from their paint code; the bounding box still sizes the overflow.
…ed by a second

When voices of the same stem direction form a second at the step where the
other stem direction has a unison, that note head sits on the far side of its
stem. The unison check looked the step up on the non-displaced side only and
threw while laying out the chord (#2893, #2903).

Such a note head is never shared: the down-stemmed part is placed to the right,
clear of all up-stemmed note heads (Behind Bars p. 53). Unisons without a
displaced note head keep their layout.
The scroll handler update never remembered the mode and layout direction it
created the handler for. Switching to ScrollMode.Off with updateSettings() in a
vertical layout matched the initial value and was skipped, so the previous
handler kept scrolling to the cursor.
…ered

The UI side of the worker renderer rebuilt the bounds lookup against the score
the API held when the result arrived. A render still running in the worker
when a new score was loaded (load() followed by render(), or any render
during a load) reported bounds of the previous score, which were mapped onto
the new one and threw in BoundsLookup.fromJson.

The worker now echoes an id of the score it rendered, and the bounds are
resolved against the score sent with that id. Applies to the browser worker
and the C#/Android worker threads.
…playback

When the browser rejected HTMLAudioElement.play() (e.g. autoplay rules
without a user gesture), the player stayed in the playing state while the
position stood still. The audio element output now reports such a refusal
and the browser's backing track player falls back to paused. Interrupted
play requests (AbortError) keep the state as before.
A test class implementing IAlphaTabRenderingWorker is emitted with a bogus
type argument in C#, so the test runs on the web target only. The renderer
code it covers is shared by all platforms.
Without soundFonts in the export options, the exporter took over the presets
the synthesizer had decoded for its own song. Those only exist once a song is
loaded and only cover that song's instruments, so exporting another midi file
was silent, or failed with a NullReferenceException in TinySoundFont on .NET
up to 1.8.4 (#2857).

The exporter now decodes the presets its song uses from the sound fonts loaded
into the synthesizer (decoded samples are shared through the sound font).
…very browser

The html5 engine draws top-aligned text on the canvas 'hanging' baseline,
whose position browsers define differently: Chromium puts it at 0.8 of the
font ascent above the alphabetic baseline, Firefox at 0.6 em. In Firefox all
such text sat higher than laid out, and the top of the score title was cut
off at the canvas edge (#2953).

Text on the hanging baseline is now moved to the Chromium placement using the
font metrics of the canvas. Chromium output is unchanged; browsers without
these metrics keep the previous placement.
The list of loaded sound fonts shared with the audio export holds an internal
type; as a protected member of the public synthesizer class it broke the C#
build (CS0053).
Deprecated get/set accessors now produce [Obsolete] in C# and
@deprecated in Kotlin, and their JSDoc becomes XML docs / KDoc, as
fields and methods already did. A deprecation on either accessor
deprecates the shared property.
The data1/data2 overrides of the MIDI events are deprecated like the
base accessors, so C# no longer reports CS0672, and the Android view
sets the replacement player options instead of enableUserInteraction.
…temsLayoutMode

The settings reference showed 2 for staffPaddingLeft, which defaults to 6
since #2907, and 1 for systemsLayoutMode, which defaults to Automatic (0).
A test now compares every numeric, boolean and enum @DefaultValue of the
settings classes with the initializer.
The website shows the "since" badge from these tags. They were missing
on most types and members added in 1.9.0, for example TuningDisplayMode,
ElementDisplay, the staff configs and bar overrides, MasterBar.customBarNumber,
Beat.restDisplayTone, the new engraving paddings and IMidiFileHandler.addMetronome.
Members of new types inherit the badge from their type.
Beat text, lyrics, free-time text and dynamics reserve horizontal space
against their neighbours in the same band. All staves of a master bar
share one layouting info, and the reservations were keyed by notation
element only, so a text on one track was spaced against a text on
another track and widened the bar. They are now keyed by staff and side.
- the score bend and whammy glyphs share one measure and shape path in
  ScoreHelperNotesBaseGlyph; ties read their horizontal extent only
  where it is used, so the left/right overrides of the shape glyphs go
- one skyline insert loop for tie boxes and tie shapes, one helper for
  the bend slur peak
- the attribution outline generator emits the glyph bounds and formats
  its output with biome; the glyph no longer walks its outlines at
  runtime, and its unused paint override and text advance are gone
- ScrambledPath uses one segment constructor without shadow state
- BarLayoutingInfo exposes hasSprings instead of a spring constant only
  compared against -1; effect bands create voice and bar-start glyphs
  through one method
Shared helpers replace copies: the scroll-mode test moves into the mouse
tests, the attribution, effect placement and broken-render tests reuse
their render setup, one warning-collecting logger, one score.gpif
extraction and one alphaTex loader. Note JSON tests share their setup,
the GP importer tests drop a byte search and assertions on incidental
counts, and the accessor fixture pins an untruncated doc comment.
Takes the dependency updates of develop. vscode-languageserver 11 also
applies to the Monaco package, and the language server declares the
protocol version that vscode-languageserver 11 uses.
The plugin packages only have type declarations once they are built,
which the lint and native CI jobs do not do. The test now loads them at
runtime by a non-literal specifier instead of importing them statically.
@Danielku15
Danielku15 merged commit 1dd1db1 into develop Oct 29, 2026
5 checks passed
@Danielku15
Danielku15 deleted the release/1.9.0-prep branch October 29, 2026 16:31
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.

1 participant