Skip to content

fix: align beaming, metronome, count-in and automations with pick-up bars; fix beat timers and multi-track MIDI events - #2939

Merged
Danielku15 merged 6 commits into
developfrom
fix/2871-pickup-bar-timing
Oct 8, 2026
Merged

Danielku15 merged 6 commits into
developfrom
fix/2871-pickup-bar-timing

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 #2871

Proposed changes

#2871: in a pick-up bar (anacrusis) the beaming is wrong, and the metronome and count-in are out of sync. alphaTab stores a pick-up's beats from tick 0, and the bar plays only for its content length. Musically, though, a pick-up is the end of a full bar. No layer knew about that shift:

  • rendering and triplet feel read positions inside the bar as if the bar started on the downbeat;
  • the synthesizer's metronome was one global grid derived from MIDI time-signature events, which have no notion of bars or pick-ups.

Analysing the issue turned up more defects in the same layers, so this PR fixes all of them.

Design

  • Pick-up offset: MasterBar caches the pick-up's position within the full bar of its time signature: the full bar duration minus the actual duration. It is computed once in finish() and is internal (not part of the public API or JSON). Beaming, triplet feel and the metronome add it to positions inside the bar. One offset per master bar is enough, because Guitar Pro also counts every voice from the bar start.
  • Automation positions: Automation.ratioPosition is relative to the full time signature duration, also in pick-ups. Guitar Pro 8 writes 0.5 for a tempo change on the 3rd eighth of a 3-eighth pick-up in 2/4, and the GP, MusicXML and Capella importers already follow this. All conversions now go through one internal ModelUtils helper.
  • Metronome: MidiFileGenerator emits the clicks bar by bar through the new IMidiFileHandler.addMetronome, so repeats, jumps, time-signature changes and pick-ups are resolved where the bars are known.
    • AlphaSynthMidiFileHandler adds them as AlphaTabMetronomeEvents, but not in SMF1 mode, so exported MIDI files are unchanged.
    • The sequencer keeps the old time-signature grid for MIDI files without clicks.
  • Count-in: it counts one full bar and then continues until the beat where playback starts. Guitar Pro 8 and MuseScore 4 behave the same way, for pick-ups as well as for starts in the middle of a bar.

Behaviour changes

Reference score: \ts 2 4 \ac C4.8 D4.8 E4.8 | F4.4 G4.4 | … at 120 bpm.

Case Before After
Beaming of the pick-up C and D joined, E with its own flag C with its own flag, D and E joined (as in GP8 and MuseScore)
Metronome clicks (tick:beat) 0:0 960:1 1920:0 2880:1 …, so every click after the pick-up falls on an off-beat 480:1 1440:0 2400:1 3360:0 …
Count-in before the pick-up 2 clicks, the pick-up starts at 1000 ms (felt as a downbeat) 3 clicks, the pick-up starts at 1250 ms
Count-in when starting on beat 3 of 4/4 4 clicks, the start note is felt as beat 1 6 clicks, the start note is on beat 3
Swing (triplet feel) in the pick-up C swung as the long note, D short, E straight C straight, D and E swung as a pair
Tempo change on E plays at tick 720 plays at tick 960
alphaTex sustain pedal on E ratio 0.667 ratio 0.5 (GP convention)
Metronome beat numbers, one-quarter pick-up in 4/4 pick-up 0, bar 1 starts at 1 pick-up 3, bar 1 starts at 0
Metronome beat numbers after 7/8 → 4/4 4/4 bar counts 3,0,1,2 0,1,2,3
Beat timers, two tracks at 120 bpm track A 0/4000/8000, track B 2000/6000/10000 ms 0/2000/4000 ms for both tracks
Beat timer after a mid-bar tempo change to 60 bar 3 shows 8000 ms 5000 ms
MidiFile.events with MidiFileFormat.MultiTrack stack overflow merged events, same-tick order preserved on all platforms
Regular bars without a pick-up or meter change — unchanged (identical click ticks; the audio export reference is untouched)

Notes:

  • The metronome beat numbers (metronomeNumerator) are only visible to midiEventsPlayed consumers. The built-in synthesizer plays every click the same.
  • Beat timers: each staff started where the previous staff ended, and the bar-end calculation compared a bar-relative tick with an absolute one. Both staves now start from the bar start time that the play-through already tracks correctly.
  • MidiFile.events: the getter called itself instead of filling its local list. The tracks are now merged instead of sorted, because the C# port's sort is not stable.

Not changed (deliberately)

  • Incomplete bars without the pick-up flag keep playing for the full duration of their time signature.
  • In exported MIDI files, a pick-up keeps the time signature of the score.
  • The tempo marker stays drawn at a linear position across the bar width, the same as in GP8.

API changes

  • IMidiFileHandler.addMetronome(tick, counter, durationInTicks) is a new required member. Custom handler implementations need to add it; they can ignore the call. The same happened with addTickShift in fix(player): grace beats on song start #2415.
  • Doc updates on AlphaTabMetronomeEvent and Automation.ratioPosition.

Tests

  • MidiFileGenerator.test.ts:
    • metronome: regular bars, time-signature change, pick-up, whole-beat pick-up, pick-up with repeat
    • triplet feel in a pick-up
    • tempo automation in a pick-up
    • beat timers with multiple tracks and after a mid-bar tempo change
    • multi-track MidiFile.events
  • SyncPoint.test.ts: count-in at a bar start, before a pick-up and mid-bar
  • AlphaTexImporter.test.ts: sustain-pedal position in a pick-up
  • Visual test special-notes/beaming-anacrusis

TypeScript, C# and Kotlin builds and test suites pass.

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

The beats of a pick-up bar are stored from tick 0, but musically they form
the end of a full bar. Beaming and triplet feel interpreted the bar
relative ticks as if the bar started on the downbeat, e.g. a 3/8 pick-up in
2/4 was beamed (C D) E instead of C (D E) and swung the offbeat eighth.

MasterBar now caches the offset of the pick-up within the nominal meter
(computed once in finish) which both features add to the bar relative
position.

Refs #2871
…nature

Automation ratio positions are written relative to the full duration of
the time signature (Guitar Pro stores 0.5 for an automation on the 3rd
eighth note of a 3/8 pick-up in 2/4, the Gp/MusicXML/Capella importers
follow this). The MIDI generation converted them using the shorter actual
pick-up duration, playing tempo changes and sync points inside pick-ups too
early (e.g. tick 720 instead of 960). The alphaTex importer wrote sustain
pedal positions with the actual duration.

All conversions now use one internal helper in ModelUtils, the convention
is documented on Automation.ratioPosition.

Refs #2871
…rough

Beat timers were calculated from a running time which every staff advanced
by a whole bar. With multiple tracks or staves the timers accumulated (two
tracks showed 0/4000/8000ms and 2000/6000/10000ms instead of 0/2000/4000ms).
The bar end recalculation additionally compared a bar relative tempo
automation tick with an absolute tick, breaking the timers of all bars
following a mid-bar tempo change.

All staves now start at the bar start time which the play-through already
tracks correctly respecting tempo changes.

Refs #2871
The metronome was generated by the sequencer on one continuous grid derived
from the time signature events. A midi time signature cannot express the
position of a pick-up within the meter, after a 3/8 pick-up in 2/4 every
click fell on an offbeat for the whole song. The click counters were
computed from the absolute tick and were wrong after time signature
changes. The count-in always counted a single bar, letting pick-ups or
mid-bar starts enter as if they were a downbeat.

The MidiFileGenerator now emits the clicks bar by bar (respecting repeats
and pick-ups) via the new IMidiFileHandler.addMetronome. They are not
written to standard midi files. The sequencer uses these clicks and keeps
the time signature based grid for midi files without clicks. The count-in
counts one full bar and continues until the beat on which the playback
starts (e.g. 3 clicks before a 3/8 pick-up in 2/4, matching Guitar Pro and
MuseScore).

Refs #2871
MidiFile.events called itself recursively for files in the multi-track
format, overflowing the stack (e.g. when loading such a file into the
player) and would have returned an empty list.

The already sorted tracks are now merged, which keeps the order of events
on the same tick on all platforms (the sort used before is not stable in
the C# port).

Refs #2871
@Danielku15
Danielku15 merged commit c9015cc into develop Oct 8, 2026
6 checks passed
@Danielku15
Danielku15 deleted the fix/2871-pickup-bar-timing branch October 8, 2026 11:09
leocaseiro added a commit to leocaseiro/alphaTab that referenced this pull request Oct 13, 2026
…erLine#2928 and CoderLine#2939 (F-47)

The next alpha upgrade's rebase brings two merged upstream changes that Task 26 Step 4 did
not name. CoderLine#2928 adds a test worker that Task 5's 'error' overload breaks
at typecheck (one line). CoderLine#2939 merges without a conflict, but its
start-beat distance is in the song's milliseconds while a count-in that follows the media
plays at the media's tempo, so an off-beat start leads into the song off its beat when the
recording's tempo differs from the score's.

Step 4 now names both: widen TestSynthWorker's addEventListener, scale the distance by the
song's tempo over the count-in's (two lines) with a FollowMedia test that expects 600 ms,
not 650, and update spec §2's count-in row and S6 at rebase time. The base stays 25ef76d;
the newest alpha on npm (1.9.0-alpha.1891) is still older than it.

Decided in lap 3's triage (option 2 of 3). Spiked on develop plus Task 2: the new test fails
at 650 ms and passes at 600 ms; upstream's count-in tests 16/16, Task 2's 7/7, tsc clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

[Bug] Incorrect beaming and metronome desync on native pickup measures (anacrusis)

1 participant