Is there an existing issue for this?
Current Behavior
When importing MusicXML, _parseMetronome converts a <metronome> marking into alphaTab's internal quarter-note BPM with an inverted ratio. The result is correct only when <beat-unit> is quarter; every other beat unit produces a tempo that is wrong by a factor of (unit/4)².
In src/importer/MusicXmlImporter.ts (alphaTab.core.mjs:23665 in the 1.8.4 dist):
tempoAutomation.value = perMinute * (unit / 4);
Duration values are note denominators (Whole=1, Half=2, Quarter=4, Eighth=8, …). One beat-unit is 4 / unit quarter notes — an eighth is 4/8 = 0.5 quarter notes — so converting "N beat-units per minute" into quarter-notes-per-minute needs 4 / unit. The expression has it the other way round.
<metronome> |
alphaTab |
Correct |
Error |
quarter = 120 |
120 |
120 |
✓ correct |
eighth = 120 |
240 |
60 |
4× too fast |
half = 60 |
30 |
120 |
4× too slow |
Because quarter is by far the most common beat unit, the bug is invisible for most scores. It shows up on compound-metre music (6/8, 9/8, 12/8), where an eighth = marking is normal, and on alla breve.
A second, related problem: conflicting duplicate automations
parseDirection handles <sound tempo="…"> before the direction-type loop that calls _parseMetronome, and each pushes its own Automation. _hasSameTempo only suppresses a duplicate when ratioPosition and value both match — so when a file contains both a correct <sound tempo> and a <metronome> marking (which is what most notation software exports), the mismatched values mean both automations survive at the same ratio position:
tempoAutomations = [60, 240]
Score.tempo returns masterBars[0].tempoAutomations[0].value → 60 (correct), while playback resolves to the later automation → 240. So the reported tempo and the actual playback tempo disagree, which makes the symptom especially confusing: the UI shows the right number while the audio runs 4× fast.
Expected Behavior
eighth = 120 in 6/8 means 120 eighth notes per minute, i.e. 60 quarter notes per minute. alphaTab reports 240 (case B) or plays at 240 (case C).
Steps To Reproduce
Self-contained Node script, no browser needed (run from a project with @coderline/alphatab installed):
import * as alphaTab from '@coderline/alphatab';
const xml = (beatUnit, perMinute, soundTempo) => `<?xml version="1.0" encoding="UTF-8"?>
<score-partwise version="4.0">
<part-list><score-part id="P1"><part-name>Guitar</part-name></score-part></part-list>
<part id="P1">
<measure number="1">
<attributes><divisions>2</divisions><key><fifths>0</fifths></key>
<time><beats>6</beats><beat-type>8</beat-type></time>
<clef><sign>G</sign><line>2</line></clef></attributes>
<direction placement="above">
<direction-type>
<metronome><beat-unit>${beatUnit}</beat-unit><per-minute>${perMinute}</per-minute></metronome>
</direction-type>${soundTempo ? `\n <sound tempo="${soundTempo}"/>` : ''}
</direction>
<note><pitch><step>C</step><octave>4</octave></pitch><duration>1</duration><type>eighth</type></note>
<note><pitch><step>D</step><octave>4</octave></pitch><duration>1</duration><type>eighth</type></note>
<note><pitch><step>E</step><octave>4</octave></pitch><duration>1</duration><type>eighth</type></note>
<note><pitch><step>F</step><octave>4</octave></pitch><duration>1</duration><type>eighth</type></note>
<note><pitch><step>G</step><octave>4</octave></pitch><duration>1</duration><type>eighth</type></note>
<note><pitch><step>A</step><octave>4</octave></pitch><duration>1</duration><type>eighth</type></note>
</measure>
</part>
</score-partwise>`;
const load = (x) => alphaTab.importer.ScoreLoader.loadScoreFromBytes(
new Uint8Array(Buffer.from(x, 'utf-8')), new alphaTab.Settings());
const show = (label, beatUnit, perMinute, soundTempo, expected) => {
const score = load(xml(beatUnit, perMinute, soundTempo));
const autos = score.masterBars[0].tempoAutomations.map(a => a.value);
console.log(`${label}\n tempoAutomations = [${autos.join(', ')}]`);
console.log(` score.tempo = ${score.tempo} (expected ${expected}) -> ${score.tempo === expected ? 'OK' : 'MISMATCH'}\n`);
};
show('A. quarter = 120', 'quarter', 120, null, 120);
show('B. eighth = 120, no <sound>', 'eighth', 120, null, 60);
show('C. eighth = 120 with <sound tempo="60">','eighth', 120, 60, 60);
show('D. half = 60', 'half', 60, null, 120);
Link to jsFiddle, CodePen, Project
No response
Version and Environment
- alphaTab **1.8.4**
- Node 22.22.3 (importer only — no browser involved)
- Also observed in-browser via the Web API, where the audible result is a 6/8 piece playing roughly 4× too fast
Platform
Web
Anything else?
Suggested fix
- tempoAutomation.value = perMinute * (unit / 4);
+ tempoAutomation.value = perMinute * (4 / unit);
That alone fixes A–D. Worth also considering whether _hasSameTempo should dedupe on ratioPosition regardless of value, so a <sound tempo> and a <metronome> at the same position cannot leave two conflicting automations behind (once the ratio is fixed they will agree in the common case, but a file with genuinely inconsistent values would still produce two).
<metronome> also supports a dotted beat unit via <beat-unit-dot/> (e.g. dotted-quarter = 40 in 6/8). I did not test that path; if beat-unit-dot is not currently applied, the same class of error would affect it.
Is there an existing issue for this?
Current Behavior
When importing MusicXML,
_parseMetronomeconverts a<metronome>marking into alphaTab's internal quarter-note BPM with an inverted ratio. The result is correct only when<beat-unit>isquarter; every other beat unit produces a tempo that is wrong by a factor of(unit/4)².In
src/importer/MusicXmlImporter.ts(alphaTab.core.mjs:23665in the 1.8.4 dist):Durationvalues are note denominators (Whole=1,Half=2,Quarter=4,Eighth=8, …). One beat-unit is4 / unitquarter notes — an eighth is4/8 = 0.5quarter notes — so converting "N beat-units per minute" into quarter-notes-per-minute needs4 / unit. The expression has it the other way round.<metronome>quarter= 120eighth= 120half= 60Because
quarteris by far the most common beat unit, the bug is invisible for most scores. It shows up on compound-metre music (6/8, 9/8, 12/8), where aneighth =marking is normal, and on alla breve.A second, related problem: conflicting duplicate automations
parseDirectionhandles<sound tempo="…">before thedirection-typeloop that calls_parseMetronome, and each pushes its ownAutomation._hasSameTempoonly suppresses a duplicate whenratioPositionandvalueboth match — so when a file contains both a correct<sound tempo>and a<metronome>marking (which is what most notation software exports), the mismatched values mean both automations survive at the same ratio position:Score.temporeturnsmasterBars[0].tempoAutomations[0].value→ 60 (correct), while playback resolves to the later automation → 240. So the reported tempo and the actual playback tempo disagree, which makes the symptom especially confusing: the UI shows the right number while the audio runs 4× fast.Expected Behavior
eighth = 120in 6/8 means 120 eighth notes per minute, i.e. 60 quarter notes per minute. alphaTab reports 240 (case B) or plays at 240 (case C).Steps To Reproduce
Self-contained Node script, no browser needed (run from a project with
@coderline/alphatabinstalled):Link to jsFiddle, CodePen, Project
No response
Version and Environment
Platform
Web
Anything else?
Suggested fix
That alone fixes A–D. Worth also considering whether
_hasSameTemposhould dedupe onratioPositionregardless of value, so a<sound tempo>and a<metronome>at the same position cannot leave two conflicting automations behind (once the ratio is fixed they will agree in the common case, but a file with genuinely inconsistent values would still produce two).<metronome>also supports a dotted beat unit via<beat-unit-dot/>(e.g.dotted-quarter = 40in 6/8). I did not test that path; ifbeat-unit-dotis not currently applied, the same class of error would affect it.