Skip to content

MusicXML: <metronome> tempo ratio is inverted for non-quarter beat units (eighth = 4× too fast, half = 4× too slow) #2798

Description

@magmasystems

Is there an existing issue for this?

  • I have searched the existing issues

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].value60 (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.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

Fields

Priority

High

Effort

Low

Area

File Formats

Platform

All / Multiple

Work State

Accepted

Projects

Status
No status

Relationships

None yet

Development

No branches or pull requests

Issue actions