Repository navigation
1.9.0 hardening: fix release-review findings across rendering, importers, player and packaging - #2990
Merged
Merged
Conversation
…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.
This was referenced Oct 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
hideEmptyStaves.Importers and model
-1values load as unset.RenderingResourcesJsonkeeps the deprecated font keys.measure-numberingholds until it changes.Note.isTrillStartmarks where a separate trill starts. A custom bar number of 0 is respected.Player and platform
ToNextBeatAnimatingCursorHandler,NonAnimatingCursorHandler), and a newly set custom handler is attached.updateSettings) take effect.load()is still running no longer throws.Transpiler and native ports
[Obsolete]in C# and@Deprecatedin Kotlin, together with their XML docs / KDoc.Packaging and tooling
^7 || ^8again, and Dependabot no longer rewrites peer ranges.Plugintype.require(default export leaves plus./package.json).vscode-languageserverpackages they import.@coderline/alphatab/viteand/webpackforward to the plugin packages, declared as optional peers.AlphaTexAtTokenNodeis exported.Docs and code size
staffPaddingLeftandsystemsLayoutModedocument their actual defaults, with a test that checks every documented settings default. The new 1.9.0 API carries@since 1.9.0.Behaviour changes integrators may notice
AutomationType.Bankis 5 (it had the same value asSyncPointin 1.8).Testing
Checklist
AI authorship disclosure
(
alphatab-ai-authored-v1) is present at the top of this body, and I havepersonally reviewed every change and can explain each one
Further details