Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Duplicate tempo change export in MIDI output #3776

Closed
craigsapp opened this issue Sep 14, 2024 · 5 comments · Fixed by #3789
Closed

Duplicate tempo change export in MIDI output #3776

craigsapp opened this issue Sep 14, 2024 · 5 comments · Fixed by #3789
Labels
bug For issues describing crashes or unexpected behaviour low priority midi

Comments

@craigsapp
Copy link
Contributor

craigsapp commented Sep 14, 2024

Related to studying the example MusicXML in issue #3775, I notice a problem with generating tempo changes in the MIDI export from verovio.

Here is a minimal example that behaves properly:

Screenshot 2024-09-14 at 8 54 41 AM
Click to view original MusicXML data for above example
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 4.0 Partwise//EN" "http://www.musicxml.org/dtds/partwise.dtd">
<score-partwise version="4.0">
 <part-list>
  <score-part id="P1" />
  </part-list>

 <part id="P1">
  <measure number="1">
   <attributes>
    <divisions>1</divisions>
    <key>
     <fifths>0</fifths>
     </key>
    <time>
     <beats>4</beats>
     <beat-type>4</beat-type>
     </time>
    <staves>1</staves>
    <clef number="1">
     <sign>G</sign>
     <line>2</line>
     </clef>
    </attributes>
   <direction placement="above">
    <direction-type>
     <words font-weight="bold" font-size="12">Grave</words>
     </direction-type>
    <staff>1</staff>
    <sound tempo="35"/>
    </direction>

   <note>
    <pitch>
     <step>E</step>
     <octave>4</octave>
     </pitch>
    <duration>4</duration>
    <voice>1</voice>
    <type>whole</type>
    <staff>1</staff>
    </note>
   </measure>

  </part>
 </score-partwise>

Tempo in the MusicXML file:

<direction placement="above">
   <direction-type>
      <words font-weight="bold" font-size="12">Grave</words>
  </direction-type>
</direction>
Click to view MEI conversion for original MusicXML.
<?xml version="1.0" encoding="UTF-8"?>
<?xml-model href="https://music-encoding.org/schema/5.0/mei-all.rng" type="application/xml" schematypens="http://relaxng.org/ns/structure/1.0"?>
<?xml-model href="https://music-encoding.org/schema/5.0/mei-all.rng" type="application/xml" schematypens="http://purl.oclc.org/dsdl/schematron"?>
<mei xmlns="http://www.music-encoding.org/ns/mei" meiversion="5.0">
 <meiHead xml:id="m14wnguh">
  <fileDesc xml:id="f1d45wkl">
   <titleStmt xml:id="t4y2knx">
    <title />
    <respStmt />
   </titleStmt>
   <pubStmt xml:id="p1rjaoka" />
  </fileDesc>
  <encodingDesc xml:id="e18c52q2">
   <appInfo xml:id="arg2kqw">
    <application xml:id="a1x0j92c" isodate="2024-09-14T08:49:20" version="4.3.0-dev-7cf6fff">
     <name xml:id="n15sve10">Verovio</name>
     <p xml:id="p185xpxi">Transcoded from MusicXML</p>
    </application>
   </appInfo>
  </encodingDesc>
 </meiHead>
 <music>
  <body>
   <mdiv xml:id="m1v95fep">
    <score xml:id="spo5sy8">
     <scoreDef xml:id="s187x7ks">
      <staffGrp xml:id="s1320hq6">
       <staffDef xml:id="P1" n="1" lines="5" ppq="1">
        <clef xml:id="c196dx1r" shape="G" line="2" />
        <keySig xml:id="k1qhemas" sig="0" />
        <meterSig xml:id="m1512ndw" count="4" unit="4" />
       </staffDef>
      </staffGrp>
     </scoreDef>
     <section xml:id="st20c6k">
      <measure xml:id="m1xxvpxi" n="1">
       <staff xml:id="s1eixgng" n="1">
        <layer xml:id="l1ufrjl9" n="1">
         <note xml:id="n1uf1n6s" dur.ppq="4" dur="1" oct="4" pname="e" />
        </layer>
       </staff>
       <tempo xml:id="t1n2y34t" place="above" staff="1" tstamp="1.000000" xml:lang="it" midi.bpm="35.000000">
        <rend xml:id="r10vrbjb" fontweight="bold">Grave</rend>
       </tempo>
      </measure>
     </section>
    </score>
   </mdiv>
  </body>
 </music>
</mei>

Tempo in MEI:

<tempo xml:id="t1n2y34t" place="above" staff="1" tstamp="1.000000" xml:lang="it" midi.bpm="35.000000">
   <rend xml:id="r10vrbjb" fontweight="bold">Grave</rend>
</tempo>

The output MIDI from verovio does not has a slight problem:

"MThd"			; MIDI header chunk marker
4'6			; bytes to follow in header chunk
2'1			; file format: Type-1 (multitrack)
2'2			; number of tracks
2'120			; ticks per quarter note

;;; TRACK 0 ----------------------------------
"MTrk"			; MIDI track chunk marker
4'18			; bytes to follow in track chunk
v0	ff 51 v3 t120	; tempo
v0	ff 51 v3 t35	; tempo
v0	ff 2f v0	; end-of-track

;;; TRACK 1 ----------------------------------
"MTrk"			; MIDI track chunk marker
4'27			; bytes to follow in track chunk
v0	ff 59 v2 '0 '0	; key signature
v0	ff 58 v4 '4 '2 '24 '8	; time signature
v0	90 '64 '90	; note-on E4
v480	90 '64 '0	; note-off E4
v0	ff 2f v0	; end-of-track

There are two tempo markings at tick-time 0: (1) q=120 (which is undesired) and (2) q=35 (which is desired). Where the q=120 is coming from is uncertain, but probably related to there being two tracks in the MIDI file, and a tempo change is added for each track, and it is inserting q=120 when there is no tempo change for the track (q=120 is the default tempo for Standard MIDI Files, so the music should play at tempo q=120 even if there were no tempo change in the file). In this case the q=120 tempo marking is overriden by q=35, which occurs at the same time, but follows the q=120. The placement of the tempo marking within the measure seems to be causing the duplate tempo change (I will give a third example below where this q=120 tempo change is not added when the tempo is added in the score/scoreDef.


Example 2: when I change <staves>1</staves> in the attributes element of the MusicXML file to <staves>10</staves>, ten q=35 tempo changes are added at tick-time 0, plus one spurious q=120 tempo change, and there is still only one tempo element at the end of the measure element (as expected):

Screenshot 2024-09-14 at 9 23 22 AM
MusicXML data with 10 staves
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 4.0 Partwise//EN" "http://www.musicxml.org/dtds/partwise.dtd">
<score-partwise version="4.0">
 <part-list>
  <score-part id="P1" />
  </part-list>

 <part id="P1">
  <measure number="1">
   <attributes>
    <divisions>1</divisions>
    <key>
     <fifths>0</fifths>
     </key>
    <time>
     <beats>4</beats>
     <beat-type>4</beat-type>
     </time>
    <staves>10</staves>
    <clef number="1">
     <sign>G</sign>
     <line>2</line>
     </clef>
    </attributes>
   <direction placement="above">
    <direction-type>
     <words font-weight="bold" font-size="12">Grave</words>
     </direction-type>
    <staff>1</staff>
    <sound tempo="35"/>
    </direction>

   <note>
    <pitch>
     <step>E</step>
     <octave>4</octave>
     </pitch>
    <duration>4</duration>
    <voice>1</voice>
    <type>whole</type>
    <staff>1</staff>
    </note>
   </measure>

  </part>
 </score-partwise>

Converting to MEI does not cause any problems in terms of duplicated tempo changes

Click to view MEI data of 10-staff MusicXML data.
<?xml version="1.0" encoding="UTF-8"?>
<?xml-model href="https://music-encoding.org/schema/5.0/mei-all.rng" type="application/xml" schematypens="http://relaxng.org/ns/structure/1.0"?>
<?xml-model href="https://music-encoding.org/schema/5.0/mei-all.rng" type="application/xml" schematypens="http://purl.oclc.org/dsdl/schematron"?>
<mei xmlns="http://www.music-encoding.org/ns/mei" meiversion="5.0">
 <meiHead xml:id="m14xna6y">
  <fileDesc xml:id="fsx0srh">
   <titleStmt xml:id="t1goud5g">
    <title />
    <respStmt />
   </titleStmt>
   <pubStmt xml:id="p1i4mjcd" />
  </fileDesc>
  <encodingDesc xml:id="ed41gqn">
   <appInfo xml:id="a19oajyq">
    <application xml:id="a5unz87" isodate="2024-09-14T08:50:19" version="4.3.0-dev-7cf6fff">
     <name xml:id="n1s73k1x">Verovio</name>
     <p xml:id="p27c9lp">Transcoded from MusicXML</p>
    </application>
   </appInfo>
  </encodingDesc>
 </meiHead>
 <music>
  <body>
   <mdiv xml:id="mwo2k4n">
    <score xml:id="s1u9phh6">
     <scoreDef xml:id="s1jlphng">
      <staffGrp xml:id="s1g3np5v">
       <staffGrp xml:id="P1" bar.thru="true">
        <grpSym xml:id="g14hj57n" symbol="brace" />
        <staffDef xml:id="sgdy4hf" n="1" lines="5" ppq="1">
         <clef xml:id="c1o94jat" shape="G" line="2" />
         <keySig xml:id="kqq74q9" sig="0" />
         <meterSig xml:id="m19p67pi" count="4" unit="4" />
        </staffDef>
        <staffDef xml:id="s1r5gozb" n="2" lines="5" ppq="1">
         <keySig xml:id="k3h9wl6" sig="0" />
         <meterSig xml:id="m1pl1uea" count="4" unit="4" />
        </staffDef>
        <staffDef xml:id="s187lbbq" n="3" lines="5" ppq="1">
         <keySig xml:id="k1dl3bre" sig="0" />
         <meterSig xml:id="mf2qkya" count="4" unit="4" />
        </staffDef>
        <staffDef xml:id="s12cxybf" n="4" lines="5" ppq="1">
         <keySig xml:id="khn9lbn" sig="0" />
         <meterSig xml:id="m1wo832w" count="4" unit="4" />
        </staffDef>
        <staffDef xml:id="sie8bqa" n="5" lines="5" ppq="1">
         <keySig xml:id="kjuzqst" sig="0" />
         <meterSig xml:id="m1k2h8ua" count="4" unit="4" />
        </staffDef>
        <staffDef xml:id="ss7u8ck" n="6" lines="5" ppq="1">
         <keySig xml:id="kckksgr" sig="0" />
         <meterSig xml:id="mmnn245" count="4" unit="4" />
        </staffDef>
        <staffDef xml:id="s1ols9fd" n="7" lines="5" ppq="1">
         <keySig xml:id="kxvh852" sig="0" />
         <meterSig xml:id="m112w31c" count="4" unit="4" />
        </staffDef>
        <staffDef xml:id="s1lj08n5" n="8" lines="5" ppq="1">
         <keySig xml:id="kkd1qz1" sig="0" />
         <meterSig xml:id="m1ipi6hk" count="4" unit="4" />
        </staffDef>
        <staffDef xml:id="se9fj2d" n="9" lines="5" ppq="1">
         <keySig xml:id="k1j0p3xi" sig="0" />
         <meterSig xml:id="mna3wai" count="4" unit="4" />
        </staffDef>
        <staffDef xml:id="s1j2hc82" n="10" lines="5" ppq="1">
         <keySig xml:id="ksk8s0l" sig="0" />
         <meterSig xml:id="mvffc2z" count="4" unit="4" />
        </staffDef>
       </staffGrp>
      </staffGrp>
     </scoreDef>
     <section xml:id="s2v34rf">
      <measure xml:id="mcr2cx5" n="1">
       <staff xml:id="sb0xbp9" n="1">
        <layer xml:id="l1g2ftpr" n="1">
         <note xml:id="n136ak32" dur.ppq="4" dur="1" oct="4" pname="e" />
        </layer>
       </staff>
       <staff xml:id="s1anmds3" n="2">
        <layer xml:id="l1d25f47" n="1">
         <mSpace xml:id="m1wqv62" />
        </layer>
       </staff>
       <staff xml:id="sq1x2fb" n="3">
        <layer xml:id="lcpdm9x" n="1">
         <mSpace xml:id="m1pdocdz" />
        </layer>
       </staff>
       <staff xml:id="s1afpb97" n="4">
        <layer xml:id="lcbfak2" n="1">
         <mSpace xml:id="m1tz87sz" />
        </layer>
       </staff>
       <staff xml:id="s1y5c1iu" n="5">
        <layer xml:id="l1ddwt5k" n="1">
         <mSpace xml:id="m124yvoe" />
        </layer>
       </staff>
       <staff xml:id="sb25363" n="6">
        <layer xml:id="l65kxli" n="1">
         <mSpace xml:id="mfkgv5q" />
        </layer>
       </staff>
       <staff xml:id="szy0vyz" n="7">
        <layer xml:id="l1ampylj" n="1">
         <mSpace xml:id="m1g3ni4x" />
        </layer>
       </staff>
       <staff xml:id="s106cfjv" n="8">
        <layer xml:id="l4y43vv" n="1">
         <mSpace xml:id="m1lgmtvs" />
        </layer>
       </staff>
       <staff xml:id="sld1cut" n="9">
        <layer xml:id="lf5206y" n="1">
         <mSpace xml:id="m1w10pht" />
        </layer>
       </staff>
       <staff xml:id="sv1qw60" n="10">
        <layer xml:id="l1p6aqhr" n="1">
         <mSpace xml:id="m8wnyfb" />
        </layer>
       </staff>
       <tempo xml:id="tjxp434" place="above" staff="1" tstamp="1.000000" xml:lang="it" midi.bpm="35.000000">
        <rend xml:id="r1kxs2bg" fontweight="bold">Grave</rend>
       </tempo>
      </measure>
     </section>
    </score>
   </mdiv>
  </body>
 </music>
</mei>

However, when converting this MEI data to MIDI with verovio, 10 identical q=35 tempo changes are found in the MIDI data, plus the spurious q=120 tempo change:

"MThd"			; MIDI header chunk marker
4'6			; bytes to follow in header chunk
2'1			; file format: Type-1 (multitrack)
2'11			; number of tracks
2'120			; ticks per quarter note

;;; TRACK 0 ----------------------------------
"MTrk"			; MIDI track chunk marker
4'81			; bytes to follow in track chunk
v0	ff 51 v3 t35	; tempo
v0	ff 51 v3 t35	; tempo
v0	ff 51 v3 t35	; tempo
v0	ff 51 v3 t35	; tempo
v0	ff 51 v3 t35	; tempo
v0	ff 51 v3 t35	; tempo
v0	ff 51 v3 t35	; tempo
v0	ff 51 v3 t35	; tempo
v0	ff 51 v3 t35	; tempo
v0	ff 51 v3 t35	; tempo
v0	ff 51 v3 t120	; tempo
v0	ff 2f v0	; end-of-track

;;; TRACK 1 ----------------------------------
"MTrk"			; MIDI track chunk marker
4'27			; bytes to follow in track chunk
v0	ff 59 v2 '0 '0	; key signature
v0	ff 58 v4 '4 '2 '24 '8	; time signature
v0	90 '64 '90	; note-on E4
v480	90 '64 '0	; note-off E4
v0	ff 2f v0	; end-of-track

;;; TRACK 2 ----------------------------------
"MTrk"			; MIDI track chunk marker
4'18			; bytes to follow in track chunk
v0	ff 59 v2 '0 '0	; key signature
v0	ff 58 v4 '4 '2 '24 '8	; time signature
v0	ff 2f v0	; end-of-track

;;; TRACK 3 ----------------------------------
"MTrk"			; MIDI track chunk marker
4'18			; bytes to follow in track chunk
v0	ff 59 v2 '0 '0	; key signature
v0	ff 58 v4 '4 '2 '24 '8	; time signature
v0	ff 2f v0	; end-of-track

;;; TRACK 4 ----------------------------------
"MTrk"			; MIDI track chunk marker
4'18			; bytes to follow in track chunk
v0	ff 59 v2 '0 '0	; key signature
v0	ff 58 v4 '4 '2 '24 '8	; time signature
v0	ff 2f v0	; end-of-track

;;; TRACK 5 ----------------------------------
"MTrk"			; MIDI track chunk marker
4'18			; bytes to follow in track chunk
v0	ff 59 v2 '0 '0	; key signature
v0	ff 58 v4 '4 '2 '24 '8	; time signature
v0	ff 2f v0	; end-of-track

;;; TRACK 6 ----------------------------------
"MTrk"			; MIDI track chunk marker
4'18			; bytes to follow in track chunk
v0	ff 59 v2 '0 '0	; key signature
v0	ff 58 v4 '4 '2 '24 '8	; time signature
v0	ff 2f v0	; end-of-track

;;; TRACK 7 ----------------------------------
"MTrk"			; MIDI track chunk marker
4'18			; bytes to follow in track chunk
v0	ff 59 v2 '0 '0	; key signature
v0	ff 58 v4 '4 '2 '24 '8	; time signature
v0	ff 2f v0	; end-of-track

;;; TRACK 8 ----------------------------------
"MTrk"			; MIDI track chunk marker
4'18			; bytes to follow in track chunk
v0	ff 59 v2 '0 '0	; key signature
v0	ff 58 v4 '4 '2 '24 '8	; time signature
v0	ff 2f v0	; end-of-track

;;; TRACK 9 ----------------------------------
"MTrk"			; MIDI track chunk marker
4'18			; bytes to follow in track chunk
v0	ff 59 v2 '0 '0	; key signature
v0	ff 58 v4 '4 '2 '24 '8	; time signature
v0	ff 2f v0	; end-of-track

;;; TRACK 10 ----------------------------------
"MTrk"			; MIDI track chunk marker
4'18			; bytes to follow in track chunk
v0	ff 59 v2 '0 '0	; key signature
v0	ff 58 v4 '4 '2 '24 '8	; time signature
v0	ff 2f v0	; end-of-track

Here are the tempo changes in track 0:

;;; TRACK 0 ----------------------------------
"MTrk"			; MIDI track chunk marker
4'81			; bytes to follow in track chunk
v0	ff 51 v3 t35	; tempo
v0	ff 51 v3 t35	; tempo
v0	ff 51 v3 t35	; tempo
v0	ff 51 v3 t35	; tempo
v0	ff 51 v3 t35	; tempo
v0	ff 51 v3 t35	; tempo
v0	ff 51 v3 t35	; tempo
v0	ff 51 v3 t35	; tempo
v0	ff 51 v3 t35	; tempo
v0	ff 51 v3 t35	; tempo
v0	ff 51 v3 t120	; tempo
v0	ff 2f v0	; end-of-track

The problem is that each staff is generating a tempo change, but there should only be one tempo change. Probably the q=120 is being added during this process: the index of the track is being used to insert a tempo by linking it to a staff (one staff for each track), but verovio is trying to do the same with track 0 (doubly) incorrectly, and it makes up q=120 for track 0 since there is no staff labeled @n=0.

For such a <tempo> element added to the <measure>, only one tempo change should be inserted into the MIDI file in track 0. The tempo change applies to all track so only one is needed, and it is messy to include redundant tempo changes.

In this particular case the q=120 tempo change is added after all of the q=35 tempo changes. This causes an error, since the music will be played in q=120, since it is cancelling out all of the q=35 tempo message that occur before it.


Example 3: If you move the tempo change to the score/scoreDef, there is no problem: only one q=40 tempo change is added to the MIDI file (although the duplicate q=35 are added by the Grave tempo element in at the bottom of the measure element).

Click to view MEI from example 2 is adjusted to add `score/[email protected]="40" tempo
<?xml version="1.0" encoding="UTF-8"?>
<?xml-model href="https://music-encoding.org/schema/5.0/mei-all.rng" type="application/xml" schematypens="http://relaxng.org/ns/structure/1.0"?>
<?xml-model href="https://music-encoding.org/schema/5.0/mei-all.rng" type="application/xml" schematypens="http://purl.oclc.org/dsdl/schematron"?>
<mei xmlns="http://www.music-encoding.org/ns/mei" meiversion="5.0">
 <meiHead xml:id="m14xna6y">
  <fileDesc xml:id="fsx0srh">
   <titleStmt xml:id="t1goud5g">
    <title />
    <respStmt />
   </titleStmt>
   <pubStmt xml:id="p1i4mjcd" />
  </fileDesc>
  <encodingDesc xml:id="ed41gqn">
   <appInfo xml:id="a19oajyq">
    <application xml:id="a5unz87" isodate="2024-09-14T08:50:19" version="4.3.0-dev-7cf6fff">
     <name xml:id="n1s73k1x">Verovio</name>
     <p xml:id="p27c9lp">Transcoded from MusicXML</p>
    </application>
   </appInfo>
  </encodingDesc>
 </meiHead>
 <music>
  <body>
   <mdiv xml:id="mwo2k4n">
    <score xml:id="s1u9phh6">
     <scoreDef midi.bpm="40.000000" xml:id="s1jlphng">
      <staffGrp xml:id="s1g3np5v">
       <staffGrp xml:id="P1" bar.thru="true">
        <grpSym xml:id="g14hj57n" symbol="brace" />
        <staffDef xml:id="sgdy4hf" n="1" lines="5" ppq="1">
         <clef xml:id="c1o94jat" shape="G" line="2" />
         <keySig xml:id="kqq74q9" sig="0" />
         <meterSig xml:id="m19p67pi" count="4" unit="4" />
        </staffDef>
        <staffDef xml:id="s1r5gozb" n="2" lines="5" ppq="1">
         <keySig xml:id="k3h9wl6" sig="0" />
         <meterSig xml:id="m1pl1uea" count="4" unit="4" />
        </staffDef>
        <staffDef xml:id="s187lbbq" n="3" lines="5" ppq="1">
         <keySig xml:id="k1dl3bre" sig="0" />
         <meterSig xml:id="mf2qkya" count="4" unit="4" />
        </staffDef>
        <staffDef xml:id="s12cxybf" n="4" lines="5" ppq="1">
         <keySig xml:id="khn9lbn" sig="0" />
         <meterSig xml:id="m1wo832w" count="4" unit="4" />
        </staffDef>
        <staffDef xml:id="sie8bqa" n="5" lines="5" ppq="1">
         <keySig xml:id="kjuzqst" sig="0" />
         <meterSig xml:id="m1k2h8ua" count="4" unit="4" />
        </staffDef>
        <staffDef xml:id="ss7u8ck" n="6" lines="5" ppq="1">
         <keySig xml:id="kckksgr" sig="0" />
         <meterSig xml:id="mmnn245" count="4" unit="4" />
        </staffDef>
        <staffDef xml:id="s1ols9fd" n="7" lines="5" ppq="1">
         <keySig xml:id="kxvh852" sig="0" />
         <meterSig xml:id="m112w31c" count="4" unit="4" />
        </staffDef>
        <staffDef xml:id="s1lj08n5" n="8" lines="5" ppq="1">
         <keySig xml:id="kkd1qz1" sig="0" />
         <meterSig xml:id="m1ipi6hk" count="4" unit="4" />
        </staffDef>
        <staffDef xml:id="se9fj2d" n="9" lines="5" ppq="1">
         <keySig xml:id="k1j0p3xi" sig="0" />
         <meterSig xml:id="mna3wai" count="4" unit="4" />
        </staffDef>
        <staffDef xml:id="s1j2hc82" n="10" lines="5" ppq="1">
         <keySig xml:id="ksk8s0l" sig="0" />
         <meterSig xml:id="mvffc2z" count="4" unit="4" />
        </staffDef>
       </staffGrp>
      </staffGrp>
     </scoreDef>
     <section xml:id="s2v34rf">
      <measure xml:id="mcr2cx5" n="1">
       <staff xml:id="sb0xbp9" n="1">
        <layer xml:id="l1g2ftpr" n="1">
         <note xml:id="n136ak32" dur.ppq="4" dur="1" oct="4" pname="e" />
        </layer>
       </staff>
       <staff xml:id="s1anmds3" n="2">
        <layer xml:id="l1d25f47" n="1">
         <mSpace xml:id="m1wqv62" />
        </layer>
       </staff>
       <staff xml:id="sq1x2fb" n="3">
        <layer xml:id="lcpdm9x" n="1">
         <mSpace xml:id="m1pdocdz" />
        </layer>
       </staff>
       <staff xml:id="s1afpb97" n="4">
        <layer xml:id="lcbfak2" n="1">
         <mSpace xml:id="m1tz87sz" />
        </layer>
       </staff>
       <staff xml:id="s1y5c1iu" n="5">
        <layer xml:id="l1ddwt5k" n="1">
         <mSpace xml:id="m124yvoe" />
        </layer>
       </staff>
       <staff xml:id="sb25363" n="6">
        <layer xml:id="l65kxli" n="1">
         <mSpace xml:id="mfkgv5q" />
        </layer>
       </staff>
       <staff xml:id="szy0vyz" n="7">
        <layer xml:id="l1ampylj" n="1">
         <mSpace xml:id="m1g3ni4x" />
        </layer>
       </staff>
       <staff xml:id="s106cfjv" n="8">
        <layer xml:id="l4y43vv" n="1">
         <mSpace xml:id="m1lgmtvs" />
        </layer>
       </staff>
       <staff xml:id="sld1cut" n="9">
        <layer xml:id="lf5206y" n="1">
         <mSpace xml:id="m1w10pht" />
        </layer>
       </staff>
       <staff xml:id="sv1qw60" n="10">
        <layer xml:id="l1p6aqhr" n="1">
         <mSpace xml:id="m8wnyfb" />
        </layer>
       </staff>
       <tempo xml:id="tjxp434" place="above" staff="1" tstamp="1.000000" xml:lang="it" midi.bpm="35.000000">
        <rend xml:id="r1kxs2bg" fontweight="bold">Grave</rend>
       </tempo>
      </measure>
     </section>
    </score>
   </mdiv>
  </body>
 </music>
</mei>

Here is the resulting MIDI output from verovio for the above MEI data:

"MThd"			; MIDI header chunk marker
4'6			; bytes to follow in header chunk
2'1			; file format: Type-1 (multitrack)
2'11			; number of tracks
2'120			; ticks per quarter note

;;; TRACK 0 ----------------------------------
"MTrk"			; MIDI track chunk marker
4'81			; bytes to follow in track chunk
v0	ff 51 v3 t35	; tempo
v0	ff 51 v3 t35	; tempo
v0	ff 51 v3 t35	; tempo
v0	ff 51 v3 t35	; tempo
v0	ff 51 v3 t35	; tempo
v0	ff 51 v3 t35	; tempo
v0	ff 51 v3 t35	; tempo
v0	ff 51 v3 t35	; tempo
v0	ff 51 v3 t35	; tempo
v0	ff 51 v3 t35	; tempo
v0	ff 51 v3 t40	; tempo
v0	ff 2f v0	; end-of-track

;;; TRACK 1 ----------------------------------
"MTrk"			; MIDI track chunk marker
4'27			; bytes to follow in track chunk
v0	ff 59 v2 '0 '0	; key signature
v0	ff 58 v4 '4 '2 '24 '8	; time signature
v0	90 '64 '90	; note-on E4
v480	90 '64 '0	; note-off E4
v0	ff 2f v0	; end-of-track

;;; TRACK 2 ----------------------------------
"MTrk"			; MIDI track chunk marker
4'18			; bytes to follow in track chunk
v0	ff 59 v2 '0 '0	; key signature
v0	ff 58 v4 '4 '2 '24 '8	; time signature
v0	ff 2f v0	; end-of-track

;;; TRACK 3 ----------------------------------
"MTrk"			; MIDI track chunk marker
4'18			; bytes to follow in track chunk
v0	ff 59 v2 '0 '0	; key signature
v0	ff 58 v4 '4 '2 '24 '8	; time signature
v0	ff 2f v0	; end-of-track

;;; TRACK 4 ----------------------------------
"MTrk"			; MIDI track chunk marker
4'18			; bytes to follow in track chunk
v0	ff 59 v2 '0 '0	; key signature
v0	ff 58 v4 '4 '2 '24 '8	; time signature
v0	ff 2f v0	; end-of-track

;;; TRACK 5 ----------------------------------
"MTrk"			; MIDI track chunk marker
4'18			; bytes to follow in track chunk
v0	ff 59 v2 '0 '0	; key signature
v0	ff 58 v4 '4 '2 '24 '8	; time signature
v0	ff 2f v0	; end-of-track

;;; TRACK 6 ----------------------------------
"MTrk"			; MIDI track chunk marker
4'18			; bytes to follow in track chunk
v0	ff 59 v2 '0 '0	; key signature
v0	ff 58 v4 '4 '2 '24 '8	; time signature
v0	ff 2f v0	; end-of-track

;;; TRACK 7 ----------------------------------
"MTrk"			; MIDI track chunk marker
4'18			; bytes to follow in track chunk
v0	ff 59 v2 '0 '0	; key signature
v0	ff 58 v4 '4 '2 '24 '8	; time signature
v0	ff 2f v0	; end-of-track

;;; TRACK 8 ----------------------------------
"MTrk"			; MIDI track chunk marker
4'18			; bytes to follow in track chunk
v0	ff 59 v2 '0 '0	; key signature
v0	ff 58 v4 '4 '2 '24 '8	; time signature
v0	ff 2f v0	; end-of-track

;;; TRACK 9 ----------------------------------
"MTrk"			; MIDI track chunk marker
4'18			; bytes to follow in track chunk
v0	ff 59 v2 '0 '0	; key signature
v0	ff 58 v4 '4 '2 '24 '8	; time signature
v0	ff 2f v0	; end-of-track

;;; TRACK 10 ----------------------------------
"MTrk"			; MIDI track chunk marker
4'18			; bytes to follow in track chunk
v0	ff 59 v2 '0 '0	; key signature
v0	ff 58 v4 '4 '2 '24 '8	; time signature
v0	ff 2f v0	; end-of-track

Track 0 contents:

;;; TRACK 0 ----------------------------------
"MTrk"			; MIDI track chunk marker
4'81			; bytes to follow in track chunk
v0	ff 51 v3 t35	; tempo
v0	ff 51 v3 t35	; tempo
v0	ff 51 v3 t35	; tempo
v0	ff 51 v3 t35	; tempo
v0	ff 51 v3 t35	; tempo
v0	ff 51 v3 t35	; tempo
v0	ff 51 v3 t35	; tempo
v0	ff 51 v3 t35	; tempo
v0	ff 51 v3 t35	; tempo
v0	ff 51 v3 t35	; tempo
v0	ff 51 v3 t40	; tempo
v0	ff 2f v0	; end-of-track

The q=40 tempo change comes from score/[email protected]. This demonstrates where the spurious q=120 tempo change comes from. I would not recommend adding q=120 if there is no score/[email protected] attribute: the Standard MIDI File standard states that if no tempo change is given, q=120 should be used.

Ideally the q=35 should also not be inserted in such a case. When the tempo is text, then it must be added to the measure. But if someone sets score/[email protected], then that should take priority over a text-based tempo (not for numeric bpm that is added to the first measure, however).

But in any case, the duplicate q=35, with one added to each staff, should not be done since it is not possible in MIDI files to have different tempos occurring in parallel. In such case the tempo changes will be interpreted serially, with only the last one at a given tick time having any affect.

@craigsapp craigsapp added bug For issues describing crashes or unexpected behaviour midi low priority labels Sep 14, 2024
@rettinghaus
Copy link
Contributor

Without investigating any further I think it comes from the default tempo of 120 which is always set and then gets overwritten by the first tempo change. We always write this because it's set and needed for the time map calculation.

@craigsapp
Copy link
Contributor Author

craigsapp commented Sep 15, 2024

Without investigating any further I think it comes from the default tempo of 120 which is always set and then gets overwritten by the first tempo change. We always write this because it's set and needed for the time map calculation.

Yes, it would not be objectionable to add q=120 to the exported MIID file even if there were no starting tempo in either score/[email protected] or measure[1]/tempo. Ideally the MIDI output would match the explicit/implicit tempo indications in the source MEI file: if there is no score/[email protected] (and no tempo indication at the start of the first measure), then the MIDI file would not have a tempo change message at the start of the file (in which case it implicitly defaults to q=120, unless MEI were to have a different implicit starting tempo).

When there is a tempo in score/[email protected] or measure[1]/tempo@tstamp="1", then then an explicit tempo change should be added to the start of the MIDI file, even if it is q=120. Only one of these two should be added, and probably it would need to be measure[1].tempo instead of score/[email protected] (provided that the measure/tempo is @tstamp="1").

For the timemap output, it would be good to always include an explicit tempo for the first entry, since this is not a standard representation such as a Standard MIDI File (which says that implicitly the starting tempo is 120). I think this is already the case (I thought I generated an example where there was no tempo entry in the timemap, but I cannot reproduce the case).


For completeness, here is the URL for the official MIDI file specs that state the implicit tempo of q=120 unless otherwise indicated:

https://midi.org/standard-midi-files

The "STANDARD MIDI FILES SPECIFICATION
RP-001_v1-0_Standard_MIDI_Files_Specification_96-1-4.pdf" entry:

On PDF page 16 (print page 14):

Screenshot 2024-09-15 at 11 40 24 AM

There are currently two bugs:

(1) There should only be one tempo for the system in the exported MIDI file. Currently there is one tempo for each staff, when a measure has a <tempo> element. This is not a serious bug (just an aesthetic ugliness of adding redundant tempo changes that will ultimately be ignored, so this case should never cause a real problem if all of the redundant tempo changes are the same, but see bottom caveat in this message, and more importantly point number (2) below).

(2) Somewhat related and mentioned in the first section of this message, measure[1]/tempo@tstamp="1" tempo change should be added to the output MIDI and score/[email protected] should be suppressed when both are present. This problem is actually causing a transient secondary bug in the MIID export from verovio.

Consider one of the example expression tracks given in the initial measure:

;;; TRACK 0 ----------------------------------
"MTrk"			; MIDI track chunk marker
4'81			; bytes to follow in track chunk
v0	ff 51 v3 t35	; tempo
v0	ff 51 v3 t35	; tempo
v0	ff 51 v3 t35	; tempo
v0	ff 51 v3 t35	; tempo
v0	ff 51 v3 t35	; tempo
v0	ff 51 v3 t35	; tempo
v0	ff 51 v3 t35	; tempo
v0	ff 51 v3 t35	; tempo
v0	ff 51 v3 t35	; tempo
v0	ff 51 v3 t35	; tempo
v0	ff 51 v3 t120	; tempo
v0	ff 2f v0	; end-of-track

score/[email protected] is (implicitly) q=120, and measure[1]/tempo@tstamp="1" is q=35. The problem is that the q=120 is the last tempo at tick-time 0. This means that the MIDI file will be in q=120 tempo, but it is expected to be in q=35 tempo. This is cause by MidiFile.sort() where it is using qsort algorithm, and it treats all of the tempo changes at a given tick-time as equal (allowing them to change order within a given tick-time).

Instead of altering the timemap generation or export to a MIDI file, I propose post-processing the tempo markings in the MIDI data before it is written to a file (or data stream in javascript):

After the MidiFile has been filled with events, and before it is sorted by absolute tick time, add a sequence number to the track 0 events:

Code snippet 1:

MidiFile midifile;
for (int i=0; i<midifile[0].getEventCount(); ++i) {
    midifile[0][i].seq = i;
}

The seq parameter for MidiEvent is designed to solve this problem (which I have thought about before :-). It is only necessary to mark the sequence for the expression track. Adding to other tracks will not hurt and may possibly help (I sort note-offs before note-ons) so the sorting process should not create zero-duration notes when a note-off for a previous note comes after the note-on at the same tick-time, so the seq system should not be needed for these possible sorting problems).

When you want to preserve the original order that events were added randomly in an unsorted fashion to a MIDI file track, the the seq parameter is useful for doing this. Likely the score/[email protected] tempo will be added first to the MIDI file, and then tempo changes from measure[1]/tempo@tstamp="1". The seq variable will then ensure that the score/[email protected] tempo change will be first tempo change at tick-time 0 in the sorted MIDI file

After the MidiFile events have been sorted and before converting to delta ticks (optionally) and then writing the MIDI file, you would then search through track 0 for cases where there are more than one tempo change at a given tick-time, and then delete any before the last tempo change at the given tick time:

Code snippet 2:

MidiFile midifile;
int lastTempoTick = -1;
for (int i=midifile[0].getEventCount() - 1; i>=0; --i) {
    if (midifile[0][i].isTempo()) {
        if (midifile[0][i].tick == lastTempoTick) {
            midifile[0][i].clear();
        }
        lastTempoTick = midifile[0][i].tick;
    }
}

Optionally the following function could be run after the for-loop: midifile[0].removeEmpties() but that is not really necessary (the writing process will ignore empty MidiMessages when writing the MIDI file).


Summary:

The current Line 1740 of src/toolkit.cpp should be changed from:

    outputfile.sortTracks();

to:

    // Preserve insertion sequence of tempo-change messages:
    for (int i=0; i<outputfile[0].getEventCount(); ++i) {
        outputfile[0][i].seq = i;
    }

    outputfile.sortTracks();

    // Remove unnecesary tempo-change messages:
    int lastTempoTick = -1;
    for (int i=outputfile[0].getEventCount() - 1; i>=0; --i) {
        if (outputfile[0][i].isTempo()) {
            if (outputfile[0][i].tick == lastTempoTick) {
                outputfile[0][i].clear();
            }
            lastTempoTick = outputfile[0][i].tick;
        }
    }

I can add this fix if you like (I would be lazy and do it in the develop-humdrum branch).

You can implement the perservation of implicit/explicit tempo changes at the start of the MIDI File for q=120 if you want to do it (I would not object to the implicit tempo-change of q=120 always being added, so this fix is optional), and the above fix should solve the problem of having both score/scoreDef@midi-bpm (implicit or explicit) and measure[1].tempo@tstamp="1" being both present in the MEI data. I am making the assumption that score/scoreDef@midi-bpm (either implicit or explicit) is always added to the MidiFile before measure[1]/tempo@tstamp="1".


It is also possible for different staves to have different tempos. In reasonable music, this can only happen if the durations of the notes have a mutual scaling factor that resolve to the same tempo in quarter notes. Suppose one staff has music has quarter notes at q=160 and another staff has half notes at h=80. In this case the half-note tempo easily resolves to q=160.

A more exotic case is where quarter notes in one staff align with half notes in another staff. This would probably be down using <note dur="2" dur.ges="4"> (or vice versa for the other staff). The tempo of the non @dur.ges staff should be inserted into the timemap/MIDI file only.

These sorts of more exotic cases can be dealt with later when someone has a real musical example that does this (and would need to be resolved before inserting tempo changes into the MIDI file).

@craigsapp
Copy link
Contributor Author

Core of issue not fixed (PR was for preserving implicit/explicit default tempo).

I can add the proposed fix which is to filter duplicate tempo-changes after they are inserted into the MidiFile object and before it is written (as opposed to preventing duplicates tempo-changes from being inserted into the MidiFile object). I will do that now. Also I will add warnings for conflicting tempos that occur at the same time.

@craigsapp craigsapp reopened this Sep 18, 2024
@rettinghaus
Copy link
Contributor

@craigsapp isn't that one a ticket for https://github.com/craigsapp/midifile/?

@craigsapp
Copy link
Contributor Author

That is possible, but if so, it would be as function called something like MidiFile::removeDuplicateTempos() (as opposed to doing it automatically when writing the file). But this would be complicated to use since the input data should be prepared in a certain way (input should be unsorted and in absolute tick format, and I would have to convert to and from delta ticks in the function maybe), then the function would overwrite any seq data that might already be present (so I would have to first check if there is existing seq data and restore it later or possibly use the seq information in the input data), and the output would become sorted (which should be OK, but maybe there may be cases where the output should remain unsorted).

Also I am thinking of issuing a LogWarning when ignored tempos occur, so it would be cleaner if done inside of verovio to use its logging system as opposed to the external library cerr capture for the logging system.

I suggested initially that the change needs to be done in toolkit.cpp, but better seems to be in doc.cpp::ExportMIDI() (I am looking at that now).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug For issues describing crashes or unexpected behaviour low priority midi
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants