rism-digital / verovio

🎵 Music notation engraving library for MEI with MusicXML and Humdrum support and various toolkits (JavaScript, Python)
https://www.verovio.org
GNU Lesser General Public License v3.0
660 stars 181 forks source link

Transposition in Verovio #1189

Closed Noroxs closed 4 years ago

Noroxs commented 4 years ago

I would like to open a feature request: Music transposition with Verovio.

I think an option that allows Verovio to automatically transpose a piece up or down would be a huge improvement for this project. Currently the music file has to be manipulated to achieve this transposition. This is complicated and can not be done in an easy and fast way.

Maybe it's easier for Verovio to do this transposition when the file is loaded into memory and before the rendering process is started.

craigsapp commented 4 years ago

This would be useful to have in verovio, and it is not too complicated to implement. Both <note> and <keySig> need to be processed.

For transposing notes, see: http://www.ccarh.org/publications/reprints/base40 Although that article is a bit theoretical, so here is a more applied summary:


The system described in the article converts pitches into integers, then adds an integer transposition, and then converts from the resulting integer back into a diatonic pitch and chromatic alteration. It is called "base-40" since the octave interval is 40 in the system.

The Base-40 system is interval-invariant, so transposition (to any key) preserves the spelling of the notes and intervals between the notes. Base-40 will handle spellings up to double sharp/flats. If you need more, then there are higher-order bases according to this formula:

    base = 7 * (2n+1) + 5

Base-40 is the case where n=2, allowing up to double sharp/flats. Base-54 is the case for +/- 3 sharps/flats, and so on (which I can explain more if you want a more generalized system, since verovio allows for triple sharps/flats).

Three pieces of information can be used to construct the numbers for pitches in Base-40:

(1) A major second is the number 6 (2) A minor second is the number 5 (3) Then choose a starting number for some pitch. I assign 162 to middle C, but the paper assigns 123 to middle C. I use 162 so that dividing a pitch number by 40 will give the octave, with middle C being in the 4th octave (in other words the octave boundary is between B-double-sharp and C-double-flat).

All other intervals can be calculated from (1) and (2): a perfect fifth is equal to three major seconds and one minor second, so the interval number is:

     P5 = 3 * 6 + 1 * 5 = 23.

If 162 represents middle C, then the G above it is 162 + 23 = 185. To transpose from one key to another, simply add an integer to the notes to transpose them to the new key. If transposing up a Major 2nd, then add 6 to all pitches. To transpose down a minor third subtract 11, etc. The enharmonic relations will always be preserved, unless you have an overflow/underflow beyond double sharp/flats (in which case switch to a higher base).

Here are the base-40 pitch class integers:

0 = Cbb (when using 162 for middle C)
1 = Cb
2 = C
3 = C#
4 = C##
5 = unused
6 = Dbb
7 = Db
8 = D
9 = D#
10 = D##
11 = unused
12 = Ebb
13 = Eb
14 = E
15 = E#
16 = E##
17 = Fbb (note no unassigned position between diatonic notes that are a minor second apart)
18 = Fb
19 = F
20 = F#
21 = F##
22 = unused
23 = Gbb
24 = Gb
25 = G
26 = G#
27 = G##
28 = unused
29 = Abb
30 = Ab
31 = A
32 = A#
33 = A##
34 = unused
35 = Bbb
36 = Bb
37 = B
38 = B#
39 = B##

The base-40 interval classes are:

0 = unison
1 = augmented unison
2 = doubly augmented unison
3 = unused
4 = diminished second
5 =  minor second
6 = major second 
7 = augmented second
8 = doubly augmented second
9 = unused
10 = diminished third
11 = minor third
12 = major third
13 = augmented third
14 = doubly augmented third
15 = double diminished fourth
16 = diminished fourth
17 = perfect fourth
18 = augmented fourth
19 = doubly augmented fourth
20 = unused
21 = doubly diminished fifth
22 = diminished fifth
23 = perfect fifth
24 = augmented fifth
25 = doubly augmented fifth
26 = unused
27 = diminished sixth
28 = minor sixth
29 = major sixth
30 = augmented sixth
31 = doubly augmented sixth
32 = unused
33 = diminished seventh
34 = minor seventh
35 = major seventh
36 = augmented seventh
37 = doubly augmented seventh
38 = doubly diminished octave
39 = diminished octave
40 = perfect octave

Here is an algorithm for converting from pitch names into base-40: https://github.com/craigsapp/humlib/blob/master/src/Convert-pitch.cpp#L201-L242

Here is an algorithm for converting from base-40 integers into pitch names: https://github.com/craigsapp/humlib/blob/master/src/Convert-pitch.cpp#L289-L341

craigsapp commented 4 years ago

Musical example, transposing up a major second (from D minor to E minor):

Screen Shot 2019-11-18 at 5 33 53 AM

For example D4 is 168, up a major second is +6, and 174 is E4.

MEI data:

<?xml version="1.0" encoding="UTF-8"?>
<?xml-model href="http://music-encoding.org/schema/4.0.0/mei-all.rng" type="application/xml" schematypens="http://relaxng.org/ns/structure/1.0"?>
<?xml-model href="http://music-encoding.org/schema/4.0.0/mei-all.rng" type="application/xml" schematypens="http://purl.oclc.org/dsdl/schematron"?>
<mei xmlns="http://www.music-encoding.org/ns/mei" meiversion="4.0.0">
 <meiHead>
  <fileDesc>
   <titleStmt>
    <title />
   </titleStmt>
   <pubStmt />
  </fileDesc>
  <encodingDesc>
   <appInfo>
    <application isodate="2019-11-17T19:04:53" version="2.3.0-dev-5f4c553">
     <name>Verovio</name>
     <p>Transcoded from Humdrum</p>
    </application>
   </appInfo>
  </encodingDesc>
  <workList>
   <work>
    <title />
   </work>
  </workList>
 </meiHead>
 <music>
  <body>
   <mdiv xml:id="mdiv-0000002006807184">
    <score xml:id="score-0000000806701909">
     <scoreDef xml:id="scoredef-0000000272711289">
      <staffGrp xml:id="staffgrp-0000001146738750" symbol="brace" bar.thru="true">
       <labelAbbr xml:id="labelAbbr-0000001807258089" />
       <staffDef xml:id="staffdef-0000001400224964" n="1" lines="5">
        <clef xml:id="clef-0000000290985166" shape="G" line="2" />
        <keySig xml:id="keysig-L4F2" sig="1f" />
        <meterSig xml:id="metersig-L3F2" count="4" unit="4" />
       </staffDef>
       <staffDef xml:id="staffdef-0000001741149957" n="2" lines="5">
        <clef xml:id="clef-0000001871644615" shape="G" line="2" />
        <keySig xml:id="keysig-L4F1" sig="1s" />
        <meterSig xml:id="metersig-L3F1" count="4" unit="4" />
       </staffDef>
      </staffGrp>
     </scoreDef>
     <section xml:id="section-L2F1">
      <measure xml:id="measure-L1">
       <staff xml:id="staff-0000001889136892" n="1">
        <layer xml:id="layer-L2F2N1" n="1">
         <beam xml:id="beam-L6F2-L9F2">
          <note xml:id="note-L6F2" dur="16" oct="4" pname="d" accid.ges="n">
           <verse xml:id="verse-L6F3" n="1">
            <syl xml:id="syl-L6F3">168</syl>
           </verse>
           <verse xml:id="verse-L6F4" n="2">
            <syl xml:id="syl-L6F4">+6</syl>
           </verse>
           <verse xml:id="verse-L6F5" n="3">
            <syl xml:id="syl-L6F5">174</syl>
           </verse>
          </note>
          <note xml:id="note-L7F2" dur="16" oct="4" pname="e" accid.ges="n">
           <verse xml:id="verse-L7F3" n="1">
            <syl xml:id="syl-L7F3">174</syl>
           </verse>
           <verse xml:id="verse-L7F4" n="2">
            <syl xml:id="syl-L7F4">+6</syl>
           </verse>
           <verse xml:id="verse-L7F5" n="3">
            <syl xml:id="syl-L7F5">180</syl>
           </verse>
          </note>
          <note xml:id="note-L8F2" dur="16" oct="4" pname="f" accid.ges="n">
           <verse xml:id="verse-L8F3" n="1">
            <syl xml:id="syl-L8F3">179</syl>
           </verse>
           <verse xml:id="verse-L8F4" n="2">
            <syl xml:id="syl-L8F4">+6</syl>
           </verse>
           <verse xml:id="verse-L8F5" n="3">
            <syl xml:id="syl-L8F5">185</syl>
           </verse>
          </note>
          <note xml:id="note-L9F2" dur="16" oct="4" pname="g" accid.ges="n">
           <verse xml:id="verse-L9F3" n="1">
            <syl xml:id="syl-L9F3">185</syl>
           </verse>
           <verse xml:id="verse-L9F4" n="2">
            <syl xml:id="syl-L9F4">+6</syl>
           </verse>
           <verse xml:id="verse-L9F5" n="3">
            <syl xml:id="syl-L9F5">191</syl>
           </verse>
          </note>
         </beam>
         <beam xml:id="beam-L10F2-L11F2">
          <note xml:id="note-L10F2" dur="8" oct="4" pname="a" accid.ges="n">
           <verse xml:id="verse-L10F3" n="1">
            <syl xml:id="syl-L10F3">191</syl>
           </verse>
           <verse xml:id="verse-L10F4" n="2">
            <syl xml:id="syl-L10F4">+6</syl>
           </verse>
           <verse xml:id="verse-L10F5" n="3">
            <syl xml:id="syl-L10F5">197</syl>
           </verse>
          </note>
          <note xml:id="note-L11F2" dur="8" oct="5" pname="d" accid.ges="n">
           <verse xml:id="verse-L11F3" n="1">
            <syl xml:id="syl-L11F3">208</syl>
           </verse>
           <verse xml:id="verse-L11F4" n="2">
            <syl xml:id="syl-L11F4">+6</syl>
           </verse>
           <verse xml:id="verse-L11F5" n="3">
            <syl xml:id="syl-L11F5">214</syl>
           </verse>
          </note>
         </beam>
         <beam xml:id="beam-L12F2-L13F2">
          <note xml:id="note-L12F2" dur="8" oct="5" pname="c" accid="s">
           <verse xml:id="verse-L12F3" n="1">
            <syl xml:id="syl-L12F3">203</syl>
           </verse>
           <verse xml:id="verse-L12F4" n="2">
            <syl xml:id="syl-L12F4">+6</syl>
           </verse>
           <verse xml:id="verse-L12F5" n="3">
            <syl xml:id="syl-L12F5">209</syl>
           </verse>
          </note>
          <note xml:id="note-L13F2" dur="8" oct="4" pname="a" accid.ges="n">
           <verse xml:id="verse-L13F3" n="1">
            <syl xml:id="syl-L13F3">191</syl>
           </verse>
           <verse xml:id="verse-L13F4" n="2">
            <syl xml:id="syl-L13F4">+6</syl>
           </verse>
           <verse xml:id="verse-L13F5" n="3">
            <syl xml:id="syl-L13F5">197</syl>
           </verse>
          </note>
         </beam>
         <beam xml:id="beam-L14F2-L15F2">
          <note xml:id="note-L14F2" dur="8" oct="4" pname="e" accid.ges="n">
           <verse xml:id="verse-L14F3" n="1">
            <syl xml:id="syl-L14F3">174</syl>
           </verse>
           <verse xml:id="verse-L14F4" n="2">
            <syl xml:id="syl-L14F4">+6</syl>
           </verse>
           <verse xml:id="verse-L14F5" n="3">
            <syl xml:id="syl-L14F5">180</syl>
           </verse>
          </note>
          <note xml:id="note-L15F2" dur="8" oct="4" pname="g" accid.ges="n">
           <verse xml:id="verse-L15F3" n="1">
            <syl xml:id="syl-L15F3">185</syl>
           </verse>
           <verse xml:id="verse-L15F4" n="2">
            <syl xml:id="syl-L15F4">+6</syl>
           </verse>
           <verse xml:id="verse-L15F5" n="3">
            <syl xml:id="syl-L15F5">191</syl>
           </verse>
          </note>
         </beam>
        </layer>
       </staff>
       <staff xml:id="staff-0000002108361190" n="2">
        <layer xml:id="layer-L2F1N1" n="1">
         <beam xml:id="beam-L6F1-L9F1">
          <note xml:id="note-L6F1" dur="16" oct="4" pname="e" accid.ges="n" />
          <note xml:id="note-L7F1" dur="16" oct="4" pname="f" accid.ges="s" />
          <note xml:id="note-L8F1" dur="16" oct="4" pname="g" accid.ges="n" />
          <note xml:id="note-L9F1" dur="16" oct="4" pname="a" accid.ges="n" />
         </beam>
         <beam xml:id="beam-L10F1-L11F1">
          <note xml:id="note-L10F1" dur="8" oct="4" pname="b" accid.ges="n" />
          <note xml:id="note-L11F1" dur="8" oct="5" pname="e" accid.ges="n" />
         </beam>
         <beam xml:id="beam-L12F1-L13F1">
          <note xml:id="note-L12F1" dur="8" oct="5" pname="d" accid="s" />
          <note xml:id="note-L13F1" dur="8" oct="4" pname="b" accid.ges="n" />
         </beam>
         <beam xml:id="beam-L14F1-L15F1">
          <note xml:id="note-L14F1" dur="8" oct="4" pname="f" accid.ges="s" />
          <note xml:id="note-L15F1" dur="8" oct="4" pname="a" accid.ges="n" />
         </beam>
        </layer>
       </staff>
      </measure>
      <measure xml:id="measure-L16" n="2">
       <staff xml:id="staff-L16F2N1" n="1">
        <layer xml:id="layer-L16F2N1" n="1">
         <beam xml:id="beam-L17F2-L18F2">
          <note xml:id="note-L17F2" dur="8" oct="4" pname="f" accid="s">
           <verse xml:id="verse-L17F3" n="1">
            <syl xml:id="syl-L17F3">180</syl>
           </verse>
           <verse xml:id="verse-L17F4" n="2">
            <syl xml:id="syl-L17F4">+6</syl>
           </verse>
           <verse xml:id="verse-L17F5" n="3">
            <syl xml:id="syl-L17F5">186</syl>
           </verse>
          </note>
          <note xml:id="note-L18F2" dur="8" oct="4" pname="d" accid.ges="n">
           <verse xml:id="verse-L18F3" n="1">
            <syl xml:id="syl-L18F3">168</syl>
           </verse>
           <verse xml:id="verse-L18F4" n="2">
            <syl xml:id="syl-L18F4">+6</syl>
           </verse>
           <verse xml:id="verse-L18F5" n="3">
            <syl xml:id="syl-L18F5">174</syl>
           </verse>
          </note>
         </beam>
         <note xml:id="note-L19F2" dur="4" oct="5" pname="c">
          <accid xml:id="accid-L19F2" accid="n" func="caution" />
          <verse xml:id="verse-L19F3" n="1">
           <syl xml:id="syl-L19F3">202</syl>
          </verse>
          <verse xml:id="verse-L19F4" n="2">
           <syl xml:id="syl-L19F4">+6</syl>
          </verse>
          <verse xml:id="verse-L19F5" n="3">
           <syl xml:id="syl-L19F5">208</syl>
          </verse>
         </note>
         <beam xml:id="beam-L20F2-L22F2">
          <note xml:id="note-L20F2" dur="8" oct="5" pname="c" accid.ges="n">
           <verse xml:id="verse-L20F3" n="1">
            <syl xml:id="syl-L20F3">202</syl>
           </verse>
           <verse xml:id="verse-L20F4" n="2">
            <syl xml:id="syl-L20F4">+6</syl>
           </verse>
           <verse xml:id="verse-L20F5" n="3">
            <syl xml:id="syl-L20F5">208</syl>
           </verse>
          </note>
          <note xml:id="note-L21F2" dur="16" oct="4" pname="b" accid="n">
           <verse xml:id="verse-L21F3" n="1">
            <syl xml:id="syl-L21F3">197</syl>
           </verse>
           <verse xml:id="verse-L21F4" n="2">
            <syl xml:id="syl-L21F4">+6</syl>
           </verse>
           <verse xml:id="verse-L21F5" n="3">
            <syl xml:id="syl-L21F5">203</syl>
           </verse>
          </note>
          <note xml:id="note-L22F2" dur="16" oct="4" pname="a" accid.ges="n">
           <verse xml:id="verse-L22F3" n="1">
            <syl xml:id="syl-L22F3">191</syl>
           </verse>
           <verse xml:id="verse-L22F4" n="2">
            <syl xml:id="syl-L22F4">+6</syl>
           </verse>
           <verse xml:id="verse-L22F5" n="3">
            <syl xml:id="syl-L22F5">197</syl>
           </verse>
          </note>
         </beam>
         <beam xml:id="beam-L23F2-L24F2">
          <note xml:id="note-L23F2" dur="8" oct="4" pname="b" accid.ges="n">
           <verse xml:id="verse-L23F3" n="1">
            <syl xml:id="syl-L23F3">197</syl>
           </verse>
           <verse xml:id="verse-L23F4" n="2">
            <syl xml:id="syl-L23F4">+6</syl>
           </verse>
           <verse xml:id="verse-L23F5" n="3">
            <syl xml:id="syl-L23F5">203</syl>
           </verse>
          </note>
          <note xml:id="note-L24F2" dur="8" oct="4" pname="g" accid.ges="n">
           <verse xml:id="verse-L24F3" n="1">
            <syl xml:id="syl-L24F3">185</syl>
           </verse>
           <verse xml:id="verse-L24F4" n="2">
            <syl xml:id="syl-L24F4">+6</syl>
           </verse>
           <verse xml:id="verse-L24F5" n="3">
            <syl xml:id="syl-L24F5">191</syl>
           </verse>
          </note>
         </beam>
        </layer>
       </staff>
       <staff xml:id="staff-L16F1N1" n="2">
        <layer xml:id="layer-L16F1N1" n="1">
         <beam xml:id="beam-L17F1-L18F1">
          <note xml:id="note-L17F1" dur="8" oct="4" pname="g" accid="s" />
          <note xml:id="note-L18F1" dur="8" oct="4" pname="e" accid.ges="n" />
         </beam>
         <note xml:id="note-L19F1" dur="4" oct="5" pname="d" accid="n" />
         <beam xml:id="beam-L20F1-L22F1">
          <note xml:id="note-L20F1" dur="8" oct="5" pname="d" accid.ges="n" />
          <note xml:id="note-L21F1" dur="16" oct="5" pname="c" accid="s" />
          <note xml:id="note-L22F1" dur="16" oct="4" pname="b" accid.ges="n" />
         </beam>
         <beam xml:id="beam-L23F1-L24F1">
          <note xml:id="note-L23F1" dur="8" oct="5" pname="c" accid.ges="s" />
          <note xml:id="note-L24F1" dur="8" oct="4" pname="a" accid.ges="n" />
         </beam>
        </layer>
       </staff>
       <tie xml:id="tie-L19F2-L20F2" startid="#note-L19F2" endid="#note-L20F2" />
       <tie xml:id="tie-L19F1-L20F1" startid="#note-L19F1" endid="#note-L20F1" />
      </measure>
     </section>
    </score>
   </mdiv>
  </body>
 </music>
</mei>
earboxer commented 4 years ago

Assuming for the interface we would want to use semitones as units, we need some kind of algorithm to decide if we want it to be more sharpy, or more flatty (which direction in the circle of fifths we are going: clockwise movements are sharpy).

As an example, if we want to go up two semitones, we have 3 choices from the base-40 interval classes:

If we wanted to transpose the key of B major up two semitones,

+0 B +2 B## +6 C# +10 Db
37 B 39 B## 43 C# 47 Db
43 C# 45 ??? 49 D# 53 Eb
49 D# 51 ??? 55 E# 59 F
54 E 56 E## 60 F# 64 Gb
60 F# 62 ??? 66 G# 70 Ab
66 G# 68 ??? 72 A# 76 Bb
72 A# 74 ??? 78 B# 82 C

We can't use the doubly-augmented unison because it results in undefined notes (triple sharps, which aren't allowed in the base-40 system).

In this example, either the major second or the diminished third are acceptable, since C# and Db are (arguably) both well-loved key-signatures. However, (going up 2 from C#), we might want a way to prevent something from being changed into the key of D#, since that has 5 sharps and 2 double-sharps (not well-loved).

(For my purposes, it's not necessary that this handles cases of files with key-changes, but a general solution would be the best (don't decide based solely on key signatures))(It may be desired that we transpose one section in a sharpy way and another in a flatty way, but this may be needless complication).

craigsapp commented 4 years ago

Assuming for the interface we would want to use semitones as units, we need some kind of algorithm to decide if we want it to be more sharpy, or more flatty (which direction in the circle of fifths we are going: clockwise movements are sharpy).

You have to explain this some more. What is the purpose of transposition and also the interface you have in mind (which you already explained a little)? Your application seems to be more for listening rather than printing. The main applications for printing that I can imagine for MEI would be working with transposing parts, and transposing for singers to get to their preferred vocal range. In that case, the chromatic interval would be given: either a major second or a diminished third (and the doubly-augmented unison not considered a practical interval).

In other words, more "flatty" or more "sharpy" is not a useful interface for music notation, and is more suitable for MIDI (base-12), which is for sound applications. MEI can handle up to triple sharp/flats, so base-54 would cover the general case in MEI. I have never needed more than base-40 for doing practical music notation applications, but since MEI is up to triple-sharp/flat capable, it would be reasonable to use at least base-54. And ideally you would use base-68 which can handle up to 4 sharp/flats. In this way you can transpose, and then check if there are invalid quadruple-sharp/flats that would cause problems in the resulting notation. But it would be even better for the most general case to expand the number of sharps/flats. For example if three sharp/flats are allows, then allowing for 7 sharp/flats for the base would be the most general (double to handle the most extreme transposition allows plus one for error checking). Base-600 allows for 42 sharp/flats. That is a nice round number for the octave and would allow for quite extreme transpositions well beyond most theoretical transpositions.

"Semitones" are a base-12 interval, and as you note, there are multiple ways of noting such a transposition in notation. What are the cases where the user does not know the chromatic interval of the transposition? Mostly I think a user would want to transpose from B-major to D-flat major (5 sharps to 5 flats). B-major to C-sharp major would also be somewhat reasonable (5 sharps to 7 sharps), but B major to B-double-sharp major would not be reasonable for standard musical notation (5 sharps to 29 sharps in the key signature). If you know what key you are starting in and the number of semitones, then you can calculate the most reasonable chromatic interval to use. For example B## has 29 sharps, C# has 7 and Db has 5 flats. Therefore the best key for two semitone transposition is Db, since this minimizes the number of accidentals in the key signature.

For music from the late Romantic period, such as by Brahms, the keys can be wrapped around to make them fit into the +/- 7 accidentals in the key signature. For example music might modulate from C-sharp major to G-sharp major, but since F## is not allowed in the key signature, A-flat major is used instead of G-sharp major. Allowing for similar automatic wrapping and/or unwrapping when transposing such music might be an interesting feature to add to a transposition interface.


You can look at the Verovio Humdrum Viewer interface for transposition, which is at the bottom of the Edit menu. There are three submenus for transposition: (1) transposing by key, (2) transposing up by chromatic interval, and (3) transposing down by chromatic interval:

Screen Shot 2019-12-02 at 6 13 47 PM Screen Shot 2019-12-02 at 6 13 54 PM Screen Shot 2019-12-02 at 6 13 56 PM

Here is an example where I add E minor as the key, and then transpose to D minor:

Screen Shot 2019-12-02 at 6 16 22 PM

Test data:

**kern
*M4/4
*k[f#]
*e:
=
16eL
16f#y
16g
16aJ
8bL
8eeJ
8dd#L
8bJ
8f#yL
8aJ
=3
8g#L
8eJ
[4dd
8ddL]
16cc#
16bJ
8cc#yL
8aJ
=
*-

I leave out-of-bounds accidental errors as a responsibility of the user to check for themselves. For most musical uses of transposition there will not be a problem, but I could increase the base number to avoid transposition errors.

earboxer commented 4 years ago

By interface, I mean how we will interact with it. (Though I do have a specific UI in mind, and my application is for printing)

e.g. by command line; I would prefer to use verovio --transpose-semitones -5 than verovio --transpose-base40 -23 --transpose-autowrap (and leave the base40/whatever information under-the-hood)

If you know what key you are starting in ... then you can calculate the most reasonable chromatic interval to use

Possible implementation:

  1. --transpose-semitones determines how many semitones to transpose by (perhaps bounded [-120,+120] (maximum 10 octaves up or down)?)
  2. verovio looks at the first ** key signature in the piece, chooses the base-54* interval that will minimize the number of accidentals in the key signature.
  3. Verovio applies that base-54 interval change to all of the notes
  4. If any of the notes lands on an undefined value, print out an error message, output the diminished second (or augmented seventh) of the undefined value, and highlight the note in red.

With this system, I assume microtones could be represented (3.5 is C 3 quarter tones sharp), but since MEI can't represent C sharp and 3 quarters (4.5), it would be more likely to become an error message. (on an implementation-level, microtones would suggest we use floating point numbers in our base-54 system... I don't have a problem with this)

craigsapp commented 4 years ago

Transposing by semitones is ambiguous as you note, so it is better to transpose by a specific chromatic interval, with possible examples being:

verovio --transpose M2                # up a major second
verovio --transpose -m3               # down a minor third
verovio --transpose +P5               # up a perfect fifth
verovio --transpose -P15              # down two octaves

The transpose argument would consist of a diatonic interval (1 = unison, 2 = 2nd, 3 = third, 4= fourth, etc.), which is prefixed by a chromatic quality for the diatonic interval: M = major, m = minor, P/p = perfect, A/a = augmented, d/D = diminished, aa/AA = doubly augmented, dd/DD = doubly diminished. And prefixed before the quality would be - for down and optionally + for up.

The base-40 (or base-52, etc.) numbers should not be accessible from the verovio options, but rather be used internally in the process of transposing. The above system is more easy for musicians to understand.

I also like to specify a target key to transpose to. This would apply to the first key signature at the start of the music. They way it works is that the base-40 interval between the tonic of the music and the tonic of the target key is calculated by subtracting the two tonics (and choosing the closest interval between the two keys). For example, if the music is in C major and you want to transpose to G major, the option could be:

verovio --transpose g

Then verovio would identify C from the first key, and then calculate the base-40 transposition interval as 23 since that is the interval between C and G (perfect fifth). Although the inversion would be a closer transposition, so a perfect fourth is 40 - 23 = 17.

If you want microtonal transpositions, then I will have to review that: I implemented a base-40 like system for microtonal transposition with someone a while ago. I think it was as you suggest, with .5 being a quarter-tone, and 0.25 being an eighth-tone. This system was necessary for generating transposing parts which contain microtones.

earboxer commented 4 years ago

I like the proposal for verovio --transpose g: It's the easiest option that allows both Gb and F# to be accessible.

I suggest we also allow direction to be specified:

(do we need a way to specify multiple octaves?)

The interface I'm trying to replace allows for [-12,+12] semitones, so I think verovio --transpose +c should go up an octave if your current key is C.

(I think showing the key "transpose up to G" would be a better UI)

Screen Shot 2019-12-03 at 11 42 42
craigsapp commented 4 years ago

I suggest we also allow direction to be specified:

  • verovio --transpose +g (transposes up to G) (From C: up a perfect fifth: +23 in base-40)
  • verovio --transpose -g (transposes down to G) (From C: down a perfect fourth: -17 in base-40)
  • verovio --transpose g (transposes to G) (From C: down a perfect fourth: -17 in base-40)

That looks good.

(do we need a way to specify multiple octaves?)

I think a good way of doing multiple octaves would be to double the tonic letter:

craigsapp commented 4 years ago

Below is a draft of a transposition system for verovio. There are two classes: (1) TPitch which is an interface to pitch information used by (2) the second class Transpose, which is a class that is used to transpose pitches in the TPitch representation (diatonic pitch index, chromatic alteration, octave).

The Transpose class is a generalized implementation of the base-40 system that allows for an arbitrary maximum sharp/flat count (where base-40 can handle up to double sharps/flats). By default the Transpose class works in the base-40 system, but by calling Transpose::setMaxAccid(), higher-order bases can be used, such as base-52 when calling Transpose::setMaxAccid(3). I created two convenience functions for low and high bases: Transpose::setBase40() is equivalent to Transpose::setMaxAccid(2) and Transpose::setBase600() is equivalent to Transpose::setMaxAccid(42).

At the bottom of the source code are example uses of the two classes (TPitch and Transpose) in the main() function.

Note that there is a string-based system for representing intervals that the Transpose class understands. This allows for automatically calculating the integer interval class for any base. Example interval names:

name meaning
P1 perfect unison
M2 major second up
+M2 major second up
-M2 major second down
m2 minor second up
d2 diminished second up
dd2 doubly diminished second up
A2 augmented second up
AA2 doubly augmented second up
M3 major third up
P4 perfect fourth up
d4 diminished fourth up
A4 augmented fourth up
P8 perfect octave up
P15 two perfect octaves up
m10 perfect octave plus minor third up

These names could be used in conjunction with the --transpose option discussed above.

To implement in verovio:

(1) create a conversion between <note> and TPitch, either building functionality into the TPitch class, or writing methods for the Note class that extract the needed information ( note@pname as an integer: C=0, D=1, E=2, F=3, G=4, A=5, B=6), note@accid as an integer: 0=natural, -1=flat, +1=sharp, -2=double flat, +2=double sharp, -3 = triple flat, +3 = triple sharp), and the value ofnote@oct as an integer. note@accid will be the most complicated, because this information can be stored in alternate locations within the note element: note@accid.ges,note/accid@accid, note/accid@accid.ges, etc. This is of general interest for other applications to know this information, so adding functionality to get the sounding/analytic accidental would be useful. Also it would be necessary to keep track of where the accidental information is recorded so that it can be updated after transposition.

(2) Key designations and key signatures need to be updated at the same time as transposition of notes.

When transposing, walk through the tree structure to find all note elements. The order of the transposing process is not important, so there is no memory needed (each layer in a measure can be processed in the order of the tree-walking, for example). Likewise for keys and key-signatures. For <keySig> (and related staffDef attributes for key signature), the transposition should be converted into a circle-of-fifths difference (+1 for up a perfect fifth, +2 for up two perfect fifths or a major second, etc). This calculation could be added to the Transpose class as a helper function.

Transposition by key (such as verovio --transpose +g discussed above) requires that the starting tonic-key of the music be encoded. This is not typically done at the moment in MEI data, so implementation of a such a feature is problematic due to consistency. I implemented the functions:

int getIntervalClass(const TPitch &p1, const TPitch &p2);
std::string getIntervalName(const TPitch &p1, const TPitch &p2);

with this feature in mind, so at some point transposition to a new tonic could be implemented for cases where the key of the music is encoded in locations such as staffDef@key.mode,staffDef@key.pname and staffDef@key.accid. I will note here that staffDef@key.sig should be staffDef@keysig since the signature is not directly related to the key (music in C major can be notated with a key signature of one sharp just as well as no sharps).

Source code:

//
// Programmer:    Craig Stuart Sapp <craig@ccrma.stanford.edu>
// Creation Date: Tue Dec  3 11:42:25 PST 2019
// Last Modified: Wed Dec  4 09:19:46 PST 2019
//
// References:
//            http://www.ccarh.org/publications/reprints/base40
//            https://github.com/craigsapp/humlib/blob/master/src/tool-transpose.cpp
//
// Description:   Draft implementation of a transposition system for verovio.
//                There is a main() function at the bottom of the file for demo/testing.
//                There are two classes in this file:
//                   TPitch: pitch representation as three integers:
//                            pname: diatonic pitch class integer from C=0 to B=6.
//                            accid: chromatic alterations in semitones (0=natural, -1=flat).
//                            oct: octave number (4 = middle-C octave).
//                   Transpose: transposition system which uses TPitch as a user interface.
//                   (Add MEI to TPitch conversions in TPitch class, or use external
//                    code to interface to verovio attributes for <note>).
//                   The default maximum accidental handling is +/- two sharps/flats (base-40).
//                   Use the Transpose::setMaxAccid() to set the maximum allowed accidental
//                   count.  Transpose::setBase40() is equivalent to Transpose::setMaxAccid(2),
//                   and Transpose::setBase600() is equivalent to Transpose::setMaxAccid(42).
//
// Todo: Probably useful to add an autowrap feature to force unrepresentable pitches to be
//     moved to enharmonic equivalent pitches (better than leaving a pitch undefined).
//     For example, F#### in a system that cannot represent more than two or three
//     sharps would be converted to G##, probably with a warning message.  From F####
//     to G## is up a diminished second ("d2").
//

#define INVALID_INTERVAL_CLASS -123456789

// Diatonic pitch class integers:
// These could be converted into an enum provided
// that the same values are assigned to each class.
#define dpc_C 0 /* Integer for Diatonic pitch class for C */
#define dpc_D 1
#define dpc_E 2
#define dpc_F 3
#define dpc_G 4
#define dpc_A 5
#define dpc_B 6

#include <iostream>
#include <string>
#include <vector>

////////////////////////////////////////////////////////////////////////////
//
// The TPitch class is an interface for storing information about notes which
// will be used in the Transpose class.  The diatonic pitch class, chromatic alteration
// of the diatonic pitch and the octave are store in the class.  Names given to the
// parameters are analogous to MEI note attributes.  Note that note@accid can be also
// note/accid in MEI data, and other complications that need to be resolved into
// storing the correct pitch information in TPitch.
//

class TPitch {
public:
    int pname; // diatonic pitch class name: C = 0, D = 1, ... B = 6.
    int accid; // chromatic alteration: 0 = natural, 1 = sharp, -2 = flat, +2 = double sharp
    int oct; // octave number: 4 = middle-C octave

    TPitch(){};
    TPitch(int aPname, int anAccid, int anOct);
    TPitch(const TPitch &pitch);
    TPitch &operator=(const TPitch &pitch);
    void setPitch(int aPname, int anAccid, int anOct);
    bool isValid(int maxAccid);
};

std::ostream &operator<<(std::ostream &out, const TPitch &pitch);

//////////////////////////////
//
// TPitch::Tpitch -- TPitch constructor.
//

TPitch::TPitch(int aPname, int anAccid, int anOct)
{
    setPitch(aPname, anAccid, anOct);
}

TPitch::TPitch(const TPitch &pitch)
{
    pname = pitch.pname;
    accid = pitch.accid;
    oct = pitch.oct;
}

//////////////////////////////
//
// operator= TPitch -- copy operator for pitches.
//

TPitch &TPitch::operator=(const TPitch &pitch)
{
    if (this != &pitch) {
        pname = pitch.pname;
        accid = pitch.accid;
        oct = pitch.oct;
    }
    return *this;
}

//////////////////////////////
//
// TPitch::isValid -- returns true if the absolute value of the accidental
//     is less than or equal to the max value.

bool TPitch::isValid(int maxAccid)
{
    return abs(accid) <= abs(maxAccid);
}

//////////////////////////////
//
// TPitch::setPitch -- Set the attributes for a pitch all at once.
//

void TPitch::setPitch(int aPname, int anAccid, int anOct)
{
    pname = aPname;
    accid = anAccid;
    oct = anOct;
}

//////////////////////////////
//
// operator<< TPitch -- Print pitch data as string for debugging.
//

std::ostream &operator<<(std::ostream &out, const TPitch &pitch)
{
    switch (pitch.pname) {
        case dpc_C: out << "C"; break;
        case dpc_D: out << "D"; break;
        case dpc_E: out << "E"; break;
        case dpc_F: out << "F"; break;
        case dpc_G: out << "G"; break;
        case dpc_A: out << "A"; break;
        case dpc_B: out << "B"; break;
        default: out << "X";
    }
    if (pitch.accid > 0) {
        for (int i = 0; i < pitch.accid; i++) {
            out << "#";
        }
    }
    else if (pitch.accid < 0) {
        for (int i = 0; i < abs(pitch.accid); i++) {
            out << "b";
        }
    }
    out << pitch.oct;
    return out;
}

////////////////////////////////////////////////////////////////////////////
//
// The Transpose class is an interface for transposing notes represented in the
// TPitch class format.
//

class Transpose {
public:
    Transpose();
    ~Transpose();

    void setBase40();
    void setBase600();
    int getBase();
    int getMaxAccid();
    void setMaxAccid(int maxAccid);
    int getIntervalClass(const std::string &intervalName);
    int pitchToInteger(const TPitch &pitch);
    TPitch integerToPitch(int ipitch);
    void setTransposition(int transVal);
    void setTransposition(const std::string &transString);
    void transpose(TPitch &pitch);
    void transpose(TPitch &pitch, int transVal);
    void transpose(TPitch &pitch, const std::string &transString);
    int getIntervalClass(const TPitch &p1, const TPitch &p2);
    std::string getIntervalName(const TPitch &p1, const TPitch &p2);
    std::string getIntervalName(int interval);

    // Convenience functions for calculating common interval classes.
    // augmented classes can be calculated by adding 1 to
    // perfect/major classes, and diminished classes can be
    // calcualted by subtracting 1 from perfect/minor classes.
    int perfectUnisonClass();
    int minorSecondClass();
    int majorSecondClass();
    int minorThirdClass();
    int majorThirdClass();
    int perfectFourthClass();
    int perfectFifthClass();
    int minorSixthClass();
    int majorSixthClass();
    int minorSeventhClass();
    int majorSeventhClass();
    int perfectOctaveClass();

protected:
    int m_base; // integer representation for perfect octave
    int m_maxAccid; // maximum allowable sharp/flats for transposing
    int m_transpose; // integer interval class for transposing
    std::vector<int> m_diatonicMapping; // pitch integers for each natural diatonic pitch class

private:
    void calculateDiatonicMapping();
};

///////////////////////////////////////////////////////////////////////////

//////////////////////////////
//
// Transpose::Transpose -- Transpose constructor.
//

Transpose::Transpose()
{
    // Initialize with base-40 system by default:
    setMaxAccid(2);
}

//////////////////////////////
//
// Transpose::~Transpose -- Transpose deconstructor.
//

Transpose::~Transpose()
{
    // do nothing;
}

//////////////////////////////
//
// Transpose::setTransposition -- Set the transposition value which is an
//   interval class in the current base system.  When Transpose::setMaxAccid()
//   or Transpose.setBase*() are called, the transposition value will be set
//   to 0 (a perfect unison).
//

void Transpose::setTransposition(int transVal)
{
    m_transpose = transVal;
}

// Use a string to set the interval class in the current base system.  For example,
//  "+M2" means up a major second, which is the integer 6 in base-40.

void Transpose::setTransposition(const std::string &transString)
{
    m_transpose = getIntervalClass(transString);
}

//////////////////////////////
//
// Transpose::transpose -- Do a transposition at the stored transposition interval, or
//   with a temporary provided integer interval class, or a temporary interval name.
//

void Transpose::transpose(TPitch &pitch)
{
    int ipitch = pitchToInteger(pitch);
    ipitch += m_transpose;
    pitch = integerToPitch(ipitch);
}

// Use a temporary transposition value in the following
// two functions. To save for later use of Transpose::transpose
// without specifying the transposition interval, store
// transposition value with Transpose::setTransposition() first.

void Transpose::transpose(TPitch &pitch, int transVal)
{
    int ipitch = pitchToInteger(pitch);
    ipitch += transVal;
    pitch = integerToPitch(ipitch);
}

void Transpose::transpose(TPitch &pitch, const std::string &transString)
{
    int transVal = getIntervalClass(transString);
    int ipitch = pitchToInteger(pitch);
    ipitch += transVal;
    pitch = integerToPitch(ipitch);
}

//////////////////////////////
//
// Transpose::getBase -- Return the integer interval class representing an octave.
//

int Transpose::getBase()
{
    return m_base;
}

//////////////////////////////
//
// Transpose::getMaxAccid -- Return the maximum possible absolute accidental value
//     that can be represented by the current transposition base.
//

int Transpose::getMaxAccid()
{
    return m_maxAccid;
}

//////////////////////////////
//
// Transpose::setMaxAccid -- Calculate variables related to a specific base system.
//

void Transpose::setMaxAccid(int maxAccid)
{
    m_maxAccid = abs(maxAccid);
    m_base = 7 * (2 * m_maxAccid + 1) + 5;
    calculateDiatonicMapping();
    m_transpose = 0;
}

//////////////////////////////
//
// Transpose::calculateDiatonicMaping -- Calculate the integer values for the
//    natural diatonic pitch classes: C, D, E, F, G, A, and B in the current
//    base system.
//

void Transpose::calculateDiatonicMapping()
{
    int M2 = majorSecondClass();
    int m2 = M2 - 1;
    m_diatonicMapping.resize(7);
    m_diatonicMapping[dpc_C] = m_maxAccid + 1;
    m_diatonicMapping[dpc_D] = m_diatonicMapping[dpc_C] + M2;
    m_diatonicMapping[dpc_E] = m_diatonicMapping[dpc_D] + M2;
    m_diatonicMapping[dpc_F] = m_diatonicMapping[dpc_E] + m2;
    m_diatonicMapping[dpc_G] = m_diatonicMapping[dpc_F] + M2;
    m_diatonicMapping[dpc_A] = m_diatonicMapping[dpc_G] + M2;
    m_diatonicMapping[dpc_B] = m_diatonicMapping[dpc_A] + M2;
}

//////////////////////////////
//
// Transpose::getIntervalClass -- Convert a diatonic interval with chromatic
//     quality and direction into an integer interval class.   Input string
//     is in the format: direction + quality + diatonic interval.
//     Such as +M2 for up a major second, -P5 is down a perfect fifth.
//     Regular expression that the string should conform to:
//            (-|\+?)([Pp]|M|m|[aA]+|[dD]+)(\d+)
//

int Transpose::getIntervalClass(const std::string &intervalName)
{
    std::string direction;
    std::string quality;
    std::string number;
    int state = 0;

    for (int i = 0; i < (int)intervalName.size(); i++) {
        switch (state) {
            case 0: // direction or quality expected
                switch (intervalName[i]) {
                    case '-': // interval is down
                        direction = "-";
                        state++;
                        break;
                    case '+': // interval is up
                        direction += "";
                        state++;
                        break;
                    default: // interval is up by default
                        direction += "";
                        state++;
                        i--;
                        break;
                }
                break;

            case 1: // quality expected
                if (std::isdigit(intervalName[i])) {
                    state++;
                    i--;
                }
                else {
                    switch (intervalName[i]) {
                        case 'M': // major
                            quality = "M";
                            break;
                        case 'm': // minor
                            quality = "m";
                            break;
                        case 'P': // perfect
                        case 'p': quality = "P"; break;
                        case 'D': // diminished
                        case 'd': quality += "d"; break;
                        case 'A': // augmented
                        case 'a': quality += "A"; break;
                    }
                }
                break;

            case 2: // digit expected
                if (std::isdigit(intervalName[i])) {
                    number += intervalName[i];
                }
                break;
        }
    }

    if (quality.empty()) {
        std::cerr << "Interval requires a chromatic quality: " << intervalName << std::endl;
        return INVALID_INTERVAL_CLASS;
    }

    if (number.empty()) {
        std::cerr << "Interval requires a diatonic interval number: " << intervalName << std::endl;
        return INVALID_INTERVAL_CLASS;
    }

    int dnum = stoi(number);
    if (dnum == 0) {
        std::cerr << "Integer interval number cannot be zero: " << intervalName << std::endl;
        return INVALID_INTERVAL_CLASS;
    }
    dnum--;
    int octave = dnum / 7;
    dnum = dnum - octave * 7;

    int base = 0;
    int adjust = 0;

    switch (dnum) {
        case 0: // unison
            base = perfectUnisonClass();
            if (quality[0] == 'A') {
                adjust = (int)quality.size();
            }
            else if (quality[0] == 'd') {
                adjust = -(int)quality.size();
            }
            else if (quality != "P") {
                std::cerr << "Error in interval quality: " << intervalName << std::endl;
                return INVALID_INTERVAL_CLASS;
            }
            break;
        case 1: // second
            if (quality == "M") {
                base = majorSecondClass();
            }
            else if (quality == "m") {
                base = minorSecondClass();
            }
            else if (quality[0] == 'A') {
                base = majorSecondClass();
                adjust = (int)quality.size();
            }
            else if (quality[0] == 'd') {
                base = minorSecondClass();
                adjust = -(int)quality.size();
            }
            else {
                std::cerr << "Error in interval quality: " << intervalName << std::endl;
                return INVALID_INTERVAL_CLASS;
            }
            break;
        case 2: // third
            if (quality == "M") {
                base = majorThirdClass();
            }
            else if (quality == "m") {
                base = minorThirdClass();
            }
            else if (quality[0] == 'A') {
                base = majorThirdClass();
                adjust = (int)quality.size();
            }
            else if (quality[0] == 'd') {
                base = minorThirdClass();
                adjust = -(int)quality.size();
            }
            else {
                std::cerr << "Error in interval quality: " << intervalName << std::endl;
                return INVALID_INTERVAL_CLASS;
            }
            break;
        case 3: // fourth
            base = perfectFourthClass();
            if (quality[0] == 'A') {
                adjust = (int)quality.size();
            }
            else if (quality[0] == 'd') {
                adjust = -(int)quality.size();
            }
            else if (quality != "P") {
                std::cerr << "Error in interval quality: " << intervalName << std::endl;
                return INVALID_INTERVAL_CLASS;
            }
            break;
        case 4: // fifth
            base = perfectFifthClass();
            if (quality[0] == 'A') {
                adjust = (int)quality.size();
            }
            else if (quality[0] == 'd') {
                adjust = -(int)quality.size();
            }
            else if (quality != "P") {
                std::cerr << "Error in interval quality: " << intervalName << std::endl;
                return INVALID_INTERVAL_CLASS;
            }
            break;
        case 5: // sixth
            if (quality == "M") {
                base = majorSixthClass();
            }
            else if (quality == "m") {
                base = minorSixthClass();
            }
            else if (quality[0] == 'A') {
                base = majorSixthClass();
                adjust = (int)quality.size();
            }
            else if (quality[0] == 'd') {
                base = minorSixthClass();
                adjust = -(int)quality.size();
            }
            else {
                std::cerr << "Error in interval quality: " << intervalName << std::endl;
                return INVALID_INTERVAL_CLASS;
            }
            break;
        case 6: // seventh
            if (quality == "M") {
                base = majorSeventhClass();
            }
            else if (quality == "m") {
                base = minorSeventhClass();
            }
            else if (quality[0] == 'A') {
                base = majorSeventhClass();
                adjust = (int)quality.size();
            }
            else if (quality[0] == 'd') {
                base = minorSeventhClass();
                adjust = -(int)quality.size();
            }
            else {
                std::cerr << "Error in interval quality: " << intervalName << std::endl;
                return INVALID_INTERVAL_CLASS;
            }
            break;
    }

    if (direction == "-") {
        return -((octave * m_base) + base + adjust);
    }
    else {
        return (octave * m_base) + base + adjust;
    }
}

//////////////////////////////
//
// Transpose::perfectUnisonClass -- Return the integer interval class
//     representing a perfect unison.
//

int Transpose::perfectUnisonClass()
{
    return 0;
}

//////////////////////////////
//
// Transpose::minorSecondClass -- Return the integer interval class
//     representing a minor second.
//

int Transpose::minorSecondClass()
{
    return m_maxAccid * 2 + 1;
}

//////////////////////////////
//
// Transpose::majorSecondClass -- Return the integer interval class
//    representing a major second.
//

int Transpose::majorSecondClass()
{
    return minorSecondClass() + 1;
}

//////////////////////////////
//
// Transpose::minorThirdClass -- Return the integer interval class
//    representing a minor third.
//

int Transpose::minorThirdClass()
{
    return majorThirdClass() - 1;
}

//////////////////////////////
//
// Transpose::majorThirdClass -- Return the integer interval class
//    representing a major third.
//

int Transpose::majorThirdClass()
{
    return 2 * majorSecondClass();
}

//////////////////////////////
//
// Transpose::perfectFourthClass -- Return the integer interval class
//    representing a perfect fourth.
//

int Transpose::perfectFourthClass()
{
    return perfectOctaveClass() - perfectFifthClass();
}

//////////////////////////////
//
// Transpose::perfectFifthClass -- Return the integer interval class
//    representing a perfect fifth.
//

int Transpose::perfectFifthClass()
{
    return 3 * majorSecondClass() + minorSecondClass();
}

//////////////////////////////
//
// Transpose::minorSixthClass -- Return the integer interval class
//    representing a minor sixth.
//

int Transpose::minorSixthClass()
{
    return perfectOctaveClass() - majorThirdClass();
}

//////////////////////////////
//
// Transpose::majorSixthClass -- Return the integer interval class
//    representing a major sixth.
//

int Transpose::majorSixthClass()
{
    return perfectOctaveClass() - minorThirdClass();
}

//////////////////////////////
//
// Transpose::minorSeventhClass -- Return the integer interval class
//    representing a minor sixth.
//

int Transpose::minorSeventhClass()
{
    return perfectOctaveClass() - majorSecondClass();
}

//////////////////////////////
//
// Transpose::majorSeventhClass -- Return the integer interval class
//    representing a major sixth.
//

int Transpose::majorSeventhClass()
{
    return perfectOctaveClass() - minorSecondClass();
}

//////////////////////////////
//
// Transpose::octaveClass -- Return the integer interval class
//    representing a major second.
//

int Transpose::perfectOctaveClass()
{
    return m_base;
}

//////////////////////////////
//
// Transpose::pitchToInteger -- Convert a pitch (octave/diatonic pitch class/chromatic
//     alteration) into an integer value according to the current base.
//

int Transpose::pitchToInteger(const TPitch &pitch)
{
    return pitch.oct * m_base + m_diatonicMapping[pitch.pname] + pitch.accid;
}

//////////////////////////////
//
// Transpose::integerToPitch -- Convert an integer within the current base
//    into a pitch (octave/diatonic pitch class/chromatic alteration).  Pitches
//    with negative octaves will have to be tested.
//

TPitch Transpose::integerToPitch(int ipitch)
{
    TPitch pitch;
    pitch.oct = ipitch / m_base;
    int chroma = ipitch - pitch.oct * m_base;
    int mindiff = -1000;
    int mini = -1;

    int targetdiff = m_maxAccid;

    if (chroma > m_base / 2) {
        // search from B downwards
        mindiff = chroma - m_diatonicMapping.back();
        mini = (int)m_diatonicMapping.size() - 1;
        for (int i = m_diatonicMapping.size() - 2; i >= 0; i--) {
            int diff = chroma - m_diatonicMapping[i];
            if (abs(diff) < abs(mindiff)) {
                mindiff = diff;
                mini = i;
            }
            if (abs(mindiff) <= m_maxAccid) {
                break;
            }
        }
    }
    else {
        // search from C upwards
        mindiff = chroma - m_diatonicMapping[0];
        mini = 0;
        for (int i = 1; i < (int)m_diatonicMapping.size(); i++) {
            int diff = chroma - m_diatonicMapping[i];
            if (abs(diff) < abs(mindiff)) {
                mindiff = diff;
                mini = i;
            }
            if (abs(mindiff) <= m_maxAccid) {
                break;
            }
        }
    }
    pitch.pname = mini;
    pitch.accid = mindiff;
    return pitch;
}

//////////////////////////////
//
// Transpose::setBase40 -- Allow up to double sharp/flats.
//

void Transpose::setBase40()
{
    setMaxAccid(2);
}

//////////////////////////////
//
// Transpose::setBase600 -- Allow up to 42 sharp/flats.
//

void Transpose::setBase600()
{
    setMaxAccid(42);
}

//////////////////////////////
//
// Transpose::getIntervalClass -- Return the interval between two pitches.
//    If the second pitch is higher than the first, then the interval will be
//    positive; otherwise, the interval will be negative.
//

int Transpose::getIntervalClass(const TPitch &p1, const TPitch &p2)
{
    return pitchToInteger(p2) - pitchToInteger(p1);
}

// similar function, but the integer interval class is converted into a string
// that is not dependent on a base.

std::string Transpose::getIntervalName(const TPitch &p1, const TPitch &p2)
{
    int iclass = getIntervalClass(p1, p2);
    return getIntervalName(iclass);
}

std::string Transpose::getIntervalName(int interval)
{
    std::string direction;
    if (interval < 0) {
        direction = "-";
        interval = -interval;
    }

    int octave = interval / m_base;
    int chroma = interval - octave * m_base;

    int mindiff = chroma;
    int mini = 0;
    for (int i = 1; i < (int)m_diatonicMapping.size(); i++) {
        int diff = chroma - (m_diatonicMapping[i] - m_diatonicMapping[0]);
        if (abs(diff) < abs(mindiff)) {
            mindiff = diff;
            mini = i;
        }
        if (abs(mindiff) <= m_maxAccid) {
            break;
        }
    }

    int number = -123456789;
    int diminished = 0;
    int augmented = 0;
    std::string quality;

    switch (mini) {
        case 0: // unison
            number = 1;
            if (mindiff == 0) {
                quality = "P";
            }
            else if (mindiff < 0) {
                diminished = -mindiff;
            }
            else if (mindiff > 0) {
                augmented = mindiff;
            }
            break;
        case 1: // second
            number = 2;
            if (mindiff == 0) {
                quality = "M";
            }
            else if (mindiff == -1) {
                quality = "m";
            }
            else if (mindiff < 0) {
                diminished = -mindiff - 1;
            }
            else if (mindiff > 0) {
                augmented = mindiff;
            }
            break;
        case 2: // third
            number = 3;
            if (mindiff == 0) {
                quality = "M";
            }
            else if (mindiff == -1) {
                quality = "m";
            }
            else if (mindiff < 0) {
                diminished = -mindiff - 1;
            }
            else if (mindiff > 0) {
                augmented = mindiff;
            }
            break;
        case 3: // fourth
            number = 4;
            if (mindiff == 0) {
                quality = "P";
            }
            else if (mindiff < 0) {
                diminished = -mindiff;
            }
            else if (mindiff > 0) {
                augmented = mindiff;
            }
            break;
        case 4: // fifth
            number = 5;
            if (mindiff == 0) {
                quality = "P";
            }
            else if (mindiff < 0) {
                diminished = -mindiff;
            }
            else if (mindiff > 0) {
                augmented = mindiff;
            }
            break;
        case 5: // sixth
            number = 6;
            if (mindiff == 0) {
                quality = "M";
            }
            else if (mindiff == -1) {
                quality = "m";
            }
            else if (mindiff < 0) {
                diminished = -mindiff - 1;
            }
            else if (mindiff > 0) {
                augmented = mindiff;
            }
            break;
        case 6: // seventh
            number = 7;
            if (mindiff == 0) {
                quality = "M";
            }
            else if (mindiff == -1) {
                quality = "m";
            }
            else if (mindiff < 0) {
                diminished = -mindiff - 1;
            }
            else if (mindiff > 0) {
                augmented = mindiff;
            }
            break;
    }

    if (quality.empty()) {
        if (augmented) {
            for (int i = 0; i < augmented; i++) {
                quality += "A";
            }
        }
        else if (diminished) {
            for (int i = 0; i < diminished; i++) {
                quality += "d";
            }
        }
        else {
            quality = "?";
        }
    }

    number += octave * 7;

    std::string output = direction;
    output += quality;
    output += std::to_string(number);

    return output;
}

/////////////////////////////////////////////////////

int main(void)
{
    TPitch pitch(dpc_C, 0, 4); // middle C

    Transpose transpose;

    // transpose.setBase40() is the default system.
    transpose.setTransposition(transpose.perfectFifthClass());
    std::cout << "Starting pitch:\t\t\t\t" << pitch << std::endl;
    transpose.transpose(pitch);
    std::cout << "Transposed up a perfect fifth:\t\t" << pitch << std::endl;

    // testing use of a different base for transposition:
    transpose.setBase600(); // allows up to 42 sharps or flats
    // Note that transpose value is cleared when setAccid() or setBase*() is called.
    transpose.setTransposition(-transpose.perfectFifthClass());
    transpose.transpose(pitch);
    std::cout << "Transposed back down a perfect fifth:\t" << pitch << std::endl;

    // testing use of interval string
    transpose.setTransposition("-m3");
    transpose.transpose(pitch);
    std::cout << "Transposed down a minor third:\t\t" << pitch << std::endl;

    // testing validation system for under/overflows:
    std::cout << std::endl;
    pitch.setPitch(dpc_C, 2, 4); // C##4
    std::cout << "Initial pitch:\t\t" << pitch << std::endl;
    transpose.transpose(pitch, "A4"); // now F###4
    bool valid = pitch.isValid(2);
    std::cout << "Up an aug. 4th:\t\t" << pitch;
    if (!valid) {
        std::cout << "\t(not valid in base-40 system)";
    }
    std::cout << std::endl;

    // calculate interval between two pitches:
    std::cout << std::endl;
    std::cout << "TESTING INTERVAL NAMES IN BASE-40:" << std::endl;
    transpose.setBase40();
    TPitch p1(dpc_C, 0, 4);
    TPitch p2(dpc_F, 2, 4);
    std::cout << "\tInterval between " << p1 << " and " << p2;
    std::cout << " is " << transpose.getIntervalName(p1, p2) << std::endl;
    TPitch p3(dpc_G, -2, 3);
    std::cout << "\tInterval between " << p1 << " and " << p3;
    std::cout << " is " << transpose.getIntervalName(p1, p3) << std::endl;

    std::cout << "TESTING INTERVAL NAMES IN BASE-600:" << std::endl;
    transpose.setBase600();
    std::cout << "\tInterval between " << p1 << " and " << p2;
    std::cout << " is " << transpose.getIntervalName(p1, p2) << std::endl;
    std::cout << "\tInterval between " << p1 << " and " << p3;
    std::cout << " is " << transpose.getIntervalName(p1, p3) << std::endl;

    return 0;
}

/* Example output from test program:

   Starting pitch:                       C4
   Transposed up a perfect fifth:        G4
   Transposed back down a perfect fifth: C4
   Transposed down a minor third:        A3

   Initial pitch:   C##4
   Up an aug. 4th:  F###4  (not valid in base-40 system)

   TESTING INTERVAL NAMES IN BASE-40:
      Interval between C4 and F##4 is AA4
      Interval between C4 and Gbb3 is -AA4
   TESTING INTERVAL NAMES IN BASE-600:
      Interval between C4 and F##4 is AA4
      Interval between C4 and Gbb3 is -AA4
 */
earboxer commented 4 years ago

note@pname as an integer: C=0, D=1, E=2, F=3, G=4, A=5, B=6),

note->GetPname() - PITCHNAME_c (or is this cheating?)

note@accid as an integer: 0=natural, -1=flat, +1=sharp, -2=double flat, +2=double sharp, -3 = triple flat, +3 = triple sharp), ... note@accid will be the most complicated, because this information can be stored in alternate locations within the note element: note@accid.ges,note/accid@accid, note/accid@accid.ges

Note::GenerateMIDI has a fairly comprehensive section on accidentals that we could copy/refactor into its own method.

and the value ofnote@oct as an integer.

Stolen from Note::GenerateMIDI:

    int oct = note->GetOct();
    if (note->HasOctGes()) oct = note->GetOctGes();

An idea for getting the key of a song when the signature doesn't specify is that we could calculate it based on the mode and number of sharps and flats (and assume the mode is major if that's not specified).

craigsapp commented 4 years ago

note@pname as an integer: C=0, D=1, E=2, F=3, G=4, A=5, B=6), note->GetPname() - PITCHNAME_c (or is this cheating?)

That looks ok based on the definition of data_PITCHNAME in include/vrv/attdef.h:

enum data_PITCHNAME {
    PITCHNAME_NONE = 0,
    PITCHNAME_c,
    PITCHNAME_d,
    PITCHNAME_e,
    PITCHNAME_f,
    PITCHNAME_g,
    PITCHNAME_a,
    PITCHNAME_b,
};

(Depending on if all compilers allow subtracting enums from each other).

I notice that src/note.cpp::GenerateMIDI() avoids this method and uses a switch statement instead:

data_PITCHNAME pname = note->GetPname();
    switch (pname) {
        case PITCHNAME_c: midiBase = 0; break;
        case PITCHNAME_d: midiBase = 2; break;
        case PITCHNAME_e: midiBase = 4; break;
        case PITCHNAME_f: midiBase = 5; break;
        case PITCHNAME_g: midiBase = 7; break;
        case PITCHNAME_a: midiBase = 9; break;
        case PITCHNAME_b: midiBase = 11; break;
        case PITCHNAME_NONE: break;
    }

The benefit of this system is that if the order of the pitch classes ever change, then the code will remain correct. Also, there may be problems when encountering PITCHNAME_NONE somehow. And there would be a problem if additional pitch names are ever added for some reason. When the pitch name is not in the set C, D, E, F, G, A, B, then the particular note should not be transposed.

craigsapp commented 4 years ago

and the value of note@oct as an integer.

Stolen from Note::GenerateMIDI:

int oct = note->GetOct();
if (note->HasOctGes()) oct = note->GetOctGes();

The octave is more complicated. note@oct.ges will be different than note@oct when a note is under a ottava mark (transposing the note up or down an octave or two to get to the sounding pitch). In such cases note@oct will be the written pitch and note@oct.ges will be the sounding pitch.

I think the best thing to do is to always use note@oct as the input to the transposition system. When receiving back the transposed oct value, the differences between the original and new oct should be calculated, and this difference applied to note@oct.ges (when present). So note@oct.ges will not be input into the transposition system, but it may be adjusted by note@oct changes.

Example:

Screen Shot 2019-12-05 at 2 16 15 PM

For both notes, the oct is 5. When the note is transposed down a perfect fifth to F4, the octave will change from 5 to 4. This is a difference of -1 octaves. This difference is then applied to the oct.ges of the first note, which moves it from oct.ges="6" to oct.ges="5".

MEI data:

<?xml version="1.0" encoding="UTF-8"?>
<?xml-model href="https://music-encoding.org/schema/4.0.0/mei-all.rng" type="application/xml" schematypens="http://relaxng.org/ns/structure/1.0"?>
<?xml-model href="https://music-encoding.org/schema/4.0.0/mei-all.rng" type="application/xml" schematypens="http://purl.oclc.org/dsdl/schematron"?>
<mei xmlns="http://www.music-encoding.org/ns/mei" meiversion="4.0.0">
    <meiHead>
        <fileDesc>
            <titleStmt>
                <title />
            </titleStmt>
            <pubStmt />
        </fileDesc>
        <encodingDesc>
            <appInfo>
                <application isodate="2019-12-05T14:16:19" version="2.4.0-dev-f9adb55">
                    <name>Verovio</name>
                    <p>Transcoded from Humdrum</p>
                </application>
            </appInfo>
        </encodingDesc>
        <workList>
            <work>
                <title />
            </work>
        </workList>
    </meiHead>
    <music>
        <body>
            <mdiv xml:id="mdiv-0000001386913243">
                <score xml:id="score-0000000117324956">
                    <scoreDef xml:id="scoredef-0000002121267550" midi.bpm="400">
                        <staffGrp xml:id="staffgrp-0000000115196003">
                            <staffDef xml:id="staffdef-0000000766256050" n="1" lines="5">
                                <clef xml:id="clef-0000000287964263" shape="G" line="2" />
                                <meterSig xml:id="metersig-L2F1" count="4" unit="4" />
                            </staffDef>
                        </staffGrp>
                    </scoreDef>
                    <section xml:id="section-L1F1">
                        <measure xml:id="measure-L1" right="end" n="0">
                            <staff xml:id="staff-0000000987882087" n="1">
                                <layer xml:id="layer-L1F1N1" n="1">
                                    <note xml:id="note-L5F1" dur="2" oct.ges="6" oct="5" pname="c" accid.ges="n" />
                                    <note xml:id="note-L7F1" dur="2" oct="5" pname="c" accid.ges="n" />
                                </layer>
                            </staff>
                            <octave xml:id="octave-0000001000615025" staff="1" startid="#note-L5F1" endid="#note-L5F1" dis="8" dis.place="above" />
                        </measure>
                    </section>
                </score>
            </mdiv>
        </body>
    </music>
</mei>

Note that I made the output note from the Transpose::transpose() function reuse the input notes storage. This might not be the best option since the old and new octave information needs to be compared. Either this can be done by storing the old octave separately, or by redefinine void Transpose::transpose(TPitch& pitch) to TPitch Transpose::transpose(const TPitch& pitch).

craigsapp commented 4 years ago

Borrowing the accidental alteration from Note::GenerateMIDI is good, and I think it would be great to refactor to generate a separate function called something like Note::GetSoundingAccidental() or Note::GetChromaticAlteration() which would return an integer (or double when dealing with microtones), where 0 = natural, +1 = sharp, -1 = flat, etc. This function can then be used both by the Note::GenerateMIDI and the transposition system.

@lpugin can comment on whether or not both note@accid and note/accid are processed by the Note::GenerateMIDI function. They both may, because I think that note@accid is converted into note/accid internally by verovio when it loads an MEI file.

Here are some examples where note/accid is used instead of note@accid:

Screen Shot 2019-12-05 at 2 39 46 PM

MEI data:

<?xml version="1.0" encoding="UTF-8"?>
<?xml-model href="https://music-encoding.org/schema/4.0.0/mei-all.rng" type="application/xml" schematypens="http://relaxng.org/ns/structure/1.0"?>
<?xml-model href="https://music-encoding.org/schema/4.0.0/mei-all.rng" type="application/xml" schematypens="http://purl.oclc.org/dsdl/schematron"?>
<mei xmlns="http://www.music-encoding.org/ns/mei" meiversion="4.0.0">
 <meiHead>
  <fileDesc>
   <titleStmt>
    <title />
   </titleStmt>
   <pubStmt />
  </fileDesc>
  <encodingDesc>
   <appInfo>
    <application isodate="2019-12-05T14:39:19" version="2.4.0-dev-f9adb55">
     <name>Verovio</name>
     <p>Transcoded from Humdrum</p>
    </application>
   </appInfo>
  </encodingDesc>
  <workList>
   <work>
    <title />
   </work>
  </workList>
 </meiHead>
 <music>
  <body>
   <mdiv xml:id="mdiv-0000001999373906">
    <score xml:id="score-0000000067361312">
     <scoreDef xml:id="scoredef-0000002038512030" midi.bpm="400">
      <staffGrp xml:id="staffgrp-0000000816141100">
       <staffDef xml:id="staffdef-0000000040963550" n="1" lines="5">
        <clef xml:id="clef-0000001589089957" shape="G" line="2" />
        <keySig xml:id="keysig-L2F1" sig="1s" />
       </staffDef>
      </staffGrp>
     </scoreDef>
     <section xml:id="section-L1F1">
      <measure xml:id="measure-L1" right="end" n="0">
       <staff xml:id="staff-0000000395498281" n="1">
        <layer xml:id="layer-L1F1N1" n="1">
         <note xml:id="note-L4F1" dur="1" oct="4" pname="f" accid.ges="s" />
         <note xml:id="note-L5F1" dur="1" oct="4" pname="f">
          <accid xml:id="accid-L5F1" accid="s" func="caution" />
         </note>
         <note xml:id="note-L6F1" dur="1" oct="4" pname="c">
          <accid xml:id="accid-L6F1" accid="s" func="edit" />
         </note>
         <note xml:id="note-L7F1" dur="1" oct="4" pname="e" accid="f" />
         <note xml:id="note-L8F1" dur="1" oct="4" pname="f">
          <accid xml:id="accid-L8F1" accid="n" enclose="paren" />
         </note>
        </layer>
       </staff>
      </measure>
     </section>
    </score>
   </mdiv>
  </body>
 </music>
</mei>

Note that you will have to replace the accidental in the location that it was extracted from once the transposition processing is finished. In other words, replace whichever of note@accid, note@accid.ges, note/accid@accid or note/accid@accid.ges that is used as input to transposition. If more than one of these parameters exist at the same time, then things may get more complicated. I would start with choosing accid.ges over accid when present. But this will not be totally correct since accid will likely change if accid.ges changes. However in such cases, the visual accidental is probably in a non-standard syntax, and this could be dealt with in the future when transposition is needed for such cases.

craigsapp commented 4 years ago

An idea for getting the key of a song when the signature doesn't specify is that we could calculate it based on the mode and number of sharps and flats (and assume the mode is major if that's not specified).

Since you will be generating your own data (I presume), you can control the data to ensure that the key is encoded in the MEI data. MusicXML usually includes this information as a major/minor mode. The MusicXML data will not always be correct in the general case since mostly people typesetting do not care if a visual sharp in the key signature means G major or E minor, since they are only interested in the visual aspect of the notation. In other words, it is probabe that E minor music is incorrectly labeled as G major music, for example. If the MusicXML-to-MEI converter does not add the key information, it would be good to do so.

Otherwise, statistical analysis of the pitch content can be used with reasonable success. Here is documentation for example tools that I do such analysis: http://extras.humdrum.org/man/keycor http://extras.humdrum.org/man/mkeyscape

The accuracy will be roughly about 90% for identifying the correct key (the accuracy mostly depending on the complexity of the music). A good algorithm may be to count the pitches in the first four measures, then do the correlation analysis described on the keycor webpage. Then assume that the key is congruent with the key signature and choose which of the two modes for the key is more likely. This would not work for modal music (the best match of all correlation values should be used rather than the expected major/minor tonics in that case).

A simpler method might be to count all of the notes in the first four or so measures, then compare the sum of the C/E/G counts to A/C/E counts for C major/A major cases. This might not work as well as the correlation method, but it should still work much of the time.

Of course this is somewhat complicated and the outcome of the key analysis is not guaranteed to be correct. Another possibility would be to have verovio print a warning message when there is no key information when transposition by key is requested (and then verovio would not try to do any transposition).

craigsapp commented 4 years ago

Transposing parts will cause some minor complications. When transposing by interval, no knowledge of whether or not there are transposing parts will be necessary, and all <keySig> and <staffDef> key sigature information should be able to be processed independently .

When transposing by key, presumably the starting key and the target key is in sounding (untransposed) format. The algorithm of transposing by key is:

(1) Identify the tonic note of the original key. Either search for a part that is not transposed, or find a part with transposition and make note of the transposition needed to convert the written tonic to the sounding tonic.

(2) calculate the interval from the sounding tonic to the target tonic provided by verovio --transpose. Then use this interval to transpose all parts in all transpositions in the same way as a transposition by interval.

Here is a sample score where there are two instruments, one transposing (both instruments playing in unison, but I did not add key information).

Screen Shot 2019-12-05 at 3 29 41 PM
<?xml version="1.0" encoding="UTF-8"?>
<?xml-model href="https://music-encoding.org/schema/4.0.0/mei-all.rng" type="application/xml" schematypens="http://relaxng.org/ns/structure/1.0"?>
<?xml-model href="https://music-encoding.org/schema/4.0.0/mei-all.rng" type="application/xml" schematypens="http://purl.oclc.org/dsdl/schematron"?>
<mei xmlns="http://www.music-encoding.org/ns/mei" meiversion="4.0.0">
 <meiHead>
  <fileDesc>
   <titleStmt>
    <title />
   </titleStmt>
   <pubStmt />
  </fileDesc>
  <encodingDesc>
   <appInfo>
    <application isodate="2019-12-05T15:28:49" version="2.4.0-dev-f9adb55">
     <name>Verovio</name>
     <p>Transcoded from Humdrum</p>
    </application>
   </appInfo>
  </encodingDesc>
  <workList>
   <work>
    <title />
   </work>
  </workList>
 </meiHead>
 <music>
  <body>
   <mdiv xml:id="mdiv-0000001638815905">
    <score xml:id="score-0000001029031421">
     <scoreDef xml:id="scoredef-0000000620477984" midi.bpm="120">
      <staffGrp xml:id="staffgrp-0000002097541236" symbol="brace" bar.thru="true">
       <labelAbbr xml:id="labelAbbr-0000001519758273" />
       <staffDef xml:id="staffdef-0000001650389027" n="1" lines="5">
        <label xml:id="label-L4F2">flute</label>
        <clef xml:id="clef-L5F2" shape="G" line="2" />
        <keySig xml:id="keysig-L7F2" sig="2f" />
        <meterSig xml:id="metersig-L8F2" count="3" unit="4" />
       </staffDef>
       <staffDef xml:id="staffdef-0000000815706890" n="2" lines="5" trans.diat="-1.000000" trans.semi="-2.000000">
        <label xml:id="label-L4F1">clarinet in Bb</label>
        <clef xml:id="clef-L5F1" shape="G" line="2" />
        <keySig xml:id="keysig-L7F1" sig="0" />
        <meterSig xml:id="metersig-L8F1" count="3" unit="4" />
       </staffDef>
      </staffGrp>
     </scoreDef>
     <section xml:id="section-L1F1">
      <measure xml:id="measure-L1" n="0">
       <staff xml:id="staff-0000000945950146" n="1">
        <layer xml:id="layer-L1F2N1" n="1">
         <note xml:id="note-L10F2" dur="4" oct="5" pname="e" accid.ges="f" />
        </layer>
       </staff>
       <staff xml:id="staff-0000002059300853" n="2">
        <layer xml:id="layer-L1F1N1" n="1">
         <note xml:id="note-L10F1" dur="4" oct="5" pname="f" accid.ges="n" />
        </layer>
       </staff>
      </measure>
      <measure xml:id="measure-L11" n="1">
       <staff xml:id="staff-L11F2N1" n="1">
        <layer xml:id="layer-L11F2N1" n="1">
         <note xml:id="note-L12F2" dur="4" oct="5" pname="d" accid.ges="n" />
         <note xml:id="note-L13F2" dur="4" oct="5" pname="c" accid.ges="n" />
         <note xml:id="note-L14F2" dur="4" oct="4" pname="b" accid.ges="f" />
        </layer>
       </staff>
       <staff xml:id="staff-L11F1N1" n="2">
        <layer xml:id="layer-L11F1N1" n="1">
         <note xml:id="note-L12F1" dur="4" oct="5" pname="e" accid.ges="n" />
         <note xml:id="note-L13F1" dur="4" oct="5" pname="d" accid.ges="n" />
         <note xml:id="note-L14F1" dur="4" oct="5" pname="c" accid.ges="n" />
        </layer>
       </staff>
      </measure>
      <measure xml:id="measure-L15" n="2">
       <staff xml:id="staff-L15F2N1" n="1">
        <layer xml:id="layer-L15F2N1" n="1">
         <note xml:id="note-L16F2" dur="4" oct="4" pname="a" accid.ges="n" />
         <note xml:id="note-L17F2" dur="4" oct="4" pname="b" accid.ges="f" />
         <note xml:id="note-L18F2" dur="4" oct="4" pname="a" accid.ges="n" />
        </layer>
       </staff>
       <staff xml:id="staff-L15F1N1" n="2">
        <layer xml:id="layer-L15F1N1" n="1">
         <note xml:id="note-L16F1" dur="4" oct="4" pname="b" accid.ges="n" />
         <note xml:id="note-L17F1" dur="4" oct="5" pname="c" accid.ges="n" />
         <note xml:id="note-L18F1" dur="4" oct="4" pname="b" accid.ges="n" />
        </layer>
       </staff>
      </measure>
      <measure xml:id="measure-L19" right="end" n="3">
       <staff xml:id="staff-L19F2N1" n="1">
        <layer xml:id="layer-L19F2N1" n="1">
         <note xml:id="note-L20F2" dots="1" dur="2" oct="4" pname="g" accid.ges="n" />
        </layer>
       </staff>
       <staff xml:id="staff-L19F1N1" n="2">
        <layer xml:id="layer-L19F1N1" n="1">
         <note xml:id="note-L20F1" dots="1" dur="2" oct="4" pname="a" accid.ges="n" />
        </layer>
       </staff>
      </measure>
     </section>
    </score>
   </mdiv>
  </body>
 </music>
</mei>

The transposition interval in the clarinets staffDef is:

trans.diat="-1.000000" trans.semi="-2.000000"

This is a two-dimensional description of the interval which means that in order to get to the sounding pitch you have to subtract one from the @pname and then spell the resulting pitch based on going down two semitones from the original pitch. Example: start on C4 (middle C). Subtracting one diatonic step takes you to B3. Subtracting two semitones from C4 takes you to the sounding note that is spelled as B-flat3. So the resulting transposition for @pname="C"/@accid.ges="n"/@oct="4" is @pname="B"/@accid.ges="f"/@oct="3".

This interval system is used for transposition in the Humdrum Toolkit trans program: http://www.humdrum.org/man/trans

I will think about adding a function to the Transpose class that converts between the one-dimensional base-40 system and the two-dimensional diatonic/chromatic system.

Also for transposing key signatures, a function for converting between base-40 and circle-of-fifth systems should be added to the Transpose class.

earboxer commented 4 years ago

This issue discussion is getting pretty long. I started a PR at https://github.com/rism-ch/verovio/pull/1219. Feel free to commit onto it, or cherry-pick any commits into your own branch.

So far I've

craigsapp commented 4 years ago

Here are additional methods for the Transpose class to convert in and out of circle-of-fifths for key signature transpositions, and diatonic/chromatic system for woriking with instrument transpositions.

I will add these to the PR.


//////////////////////////////
//
// Transpose::intervalToCircleOfFifths -- Returns the circle-of-fiths count
//    that is represented by the given interval class or interval string.
//    Examples:  "P5"  => +1      "-P5" => -1
//               "P4"  => -1      "-P4" => +1
//               "M2"  => +2      "m7"  => -2
//               "M6"  => +3      "m3"  => -3
//               "M3"  => +4      "m6"  => -4
//               "M7"  => +5      "m2"  => -5
//               "A4"  => +6      "d5"  => -6
//               "A1"  => +7      "d1"  => -7
//
// If a key-signature plus the transposition interval in circle-of-fifths format
// is greater than +/-7, Then the -/+ 7 should be added to the key signature to
// avoid double sharp/flats in the key signature (and the transposition interval
// should be adjusted accordingly).
//

int Transpose::intervalToCircleOfFifths(const std::string &transstring)
{
    int intervalClass = getIntervalClass(transstring);
    return intervalToCircleOfFifths(intervalClass);
}

int Transpose::intervalToCircleOfFifths(int transval)
{
    if (transval < 0) {
        transval = (m_base * 100 - transval) % m_base;
    }
    int p5 = perfectFifthClass();
    int p4 = perfectFourthClass();
    if (transval == 0) {
        return 0;
    }
    for (int i = 1; i < m_base / 2; i++) {
        if ((p5 * i) % m_base == transval) {
            return i;
        }
        if ((p4 * i) % m_base == transval) {
            return -i;
        }
    }
    return INVALID_INTERVAL_CLASS;
}

//////////////////////////////
//
// Transpose::circleOfFifthsToIntervalClass -- Inputs a circle-of-fifths value and
//   returns the interval class as an integer in the current base.
//

int Transpose::circleOfFifthsToIntervalClass(int fifths)
{
    if (fifths == 0) {
        return 0;
    }
    else if (fifths > 0) {
        return (perfectFifthClass() * fifths) % m_base;
    }
    else {
        return (perfectFourthClass() * (-fifths)) % m_base;
    }
}

//////////////////////////////
//
// Transpose::circleOfFifthsToIntervalName -- Convert a circle-of-fifths position
//    into an interval string.
//

std::string Transpose::circleOfFifthsToIntervalName(int fifths)
{
    int intervalClass = circleOfFifthsToIntervalClass(fifths);
    return getIntervalName(intervalClass);
}

//////////////////////////////
//
// Transpose::diatonicChromaticToIntervalClass -- Convert a diatonic/chromatic interval
//    into a base-n interval class integer.
//      +1D +1C = m2
//      +1D +2C = M2
//      +1D +3C = A2
//      +2D +4C = M3
//      +2D +3C = m3
//      +2D +2C = m3
//      +2D +1C = d3
//      +3D +5C = P4
//      +3D +6C = A4
//      +3D +4C = d4
//
//

std::string Transpose::diatonicChromaticToIntervalName(int diatonic, int chromatic)
{
    if (diatonic == 0) {
        std::string output;
        if (chromatic == 0) {
            output += "P";
        }
        else if (chromatic > 0) {
            for (int i = 0; i < chromatic; i++) {
                output += "A";
            }
        }
        else {
            for (int i = 0; i < -chromatic; i++) {
                output += "d";
            }
        }
        output += "1";
        return output;
    }

    int octave = 0;
    std::string direction;
    if (diatonic < 0) {
        direction = "-";
        octave = -diatonic / 7;
        diatonic = (-diatonic - octave * 7);
        chromatic = -chromatic;
    }
    else {
        octave = diatonic / 7;
        diatonic = diatonic - octave * 7;
    }

    int augmented = 0;
    int diminished = 0;
    std::string quality;

    switch (abs(diatonic)) {
        case 0: // unsion
            if (chromatic == 0) {
                quality = "P";
            }
            else if (chromatic > 0) {
                augmented = chromatic;
            }
            else {
                diminished = chromatic;
            }
            break;
        case 1: // second
            if (chromatic == 2) {
                quality = "M";
            }
            else if (chromatic == 1) {
                quality = "m";
            }
            else if (chromatic > 2) {
                augmented = chromatic - 2;
            }
            else {
                diminished = chromatic - 1;
            }
            break;
        case 2: // third
            if (chromatic == 4) {
                quality = "M";
            }
            else if (chromatic == 3) {
                quality = "m";
            }
            else if (chromatic > 4) {
                augmented = chromatic - 4;
            }
            else {
                diminished = chromatic - 3;
            }
            break;
        case 3: // fourth
            if (chromatic == 5) {
                quality = "P";
            }
            else if (chromatic > 5) {
                augmented = chromatic - 5;
            }
            else {
                diminished = chromatic - 5;
            }
            break;
        case 4: // fifth
            if (chromatic == 7) {
                quality = "P";
            }
            else if (chromatic > 7) {
                augmented = chromatic - 7;
            }
            else {
                diminished = chromatic - 7;
            }
            break;
        case 5: // sixth
            if (chromatic == 9) {
                quality = "M";
            }
            else if (chromatic == 8) {
                quality = "m";
            }
            else if (chromatic > 9) {
                augmented = chromatic - 9;
            }
            else {
                diminished = chromatic - 8;
            }
            break;
        case 6: // seventh
            if (chromatic == 11) {
                quality = "M";
            }
            else if (chromatic == 10) {
                quality = "m";
            }
            else if (chromatic > 11) {
                augmented = chromatic - 11;
            }
            else {
                diminished = chromatic - 10;
            }
            break;
    }

    augmented = abs(augmented);
    diminished = abs(diminished);

    if (quality.empty()) {
        if (augmented) {
            for (int i = 0; i < augmented; i++) {
                quality += "A";
            }
        }
        else if (diminished) {
            for (int i = 0; i < diminished; i++) {
                quality += "d";
            }
        }
    }

    return direction + quality + std::to_string(octave * 7 + diatonic + 1);
}

//////////////////////////////
//
// Transpose::diatonicChromaticToIntervalClass --
//

int Transpose::diatonicChromaticToIntervalClass(int diatonic, int chromatic)
{
    std::string intervalName = diatonicChromaticToIntervalName(diatonic, chromatic);
    return getIntervalClass(intervalName);
}

//////////////////////////////
//
// Transpose::intervalToDiatonicChromatic --
//

void Transpose::intervalToDiatonicChromatic(int &diatonic, int &chromatic, int intervalClass)
{
    std::string intervalName = getIntervalName(intervalClass);
    intervalToDiatonicChromatic(diatonic, chromatic, intervalName);
}

void Transpose::intervalToDiatonicChromatic(int &diatonic, int &chromatic, const std::string &intervalName)
{
    int direction = 1;
    std::string quality;
    std::string number;
    int state = 0;

    for (int i = 0; i < (int)intervalName.size(); i++) {
        switch (state) {
            case 0: // direction or quality expected
                switch (intervalName[i]) {
                    case '-': // interval is down
                        direction = -1;
                        state++;
                        break;
                    case '+': // interval is up
                        direction = 1;
                        state++;
                        break;
                    default: // interval is up by default
                        direction = 1;
                        state++;
                        i--;
                        break;
                }
                break;

            case 1: // quality expected
                if (std::isdigit(intervalName[i])) {
                    state++;
                    i--;
                }
                else {
                    switch (intervalName[i]) {
                        case 'M': // major
                            quality = "M";
                            break;
                        case 'm': // minor
                            quality = "m";
                            break;
                        case 'P': // perfect
                        case 'p': quality = "P"; break;
                        case 'D': // diminished
                        case 'd': quality += "d"; break;
                        case 'A': // augmented
                        case 'a': quality += "A"; break;
                    }
                }
                break;

            case 2: // digit expected
                if (std::isdigit(intervalName[i])) {
                    number += intervalName[i];
                }
                break;
        }
    }

    if (quality.empty()) {
        std::cerr << "Interval requires a chromatic quality: " << intervalName << std::endl;
        chromatic = INVALID_INTERVAL_CLASS;
        diatonic = INVALID_INTERVAL_CLASS;
        return;
    }

    if (number.empty()) {
        std::cerr << "Interval requires a diatonic interval number: " << intervalName << std::endl;
        chromatic = INVALID_INTERVAL_CLASS;
        diatonic = INVALID_INTERVAL_CLASS;
        return;
    }

    int dnum = stoi(number);
    if (dnum == 0) {
        std::cerr << "Integer interval number cannot be zero: " << intervalName << std::endl;
        chromatic = INVALID_INTERVAL_CLASS;
        diatonic = INVALID_INTERVAL_CLASS;
        return;
    }
    dnum--;
    int octave = dnum / 7;
    dnum = dnum - octave * 7;

    diatonic = direction * (octave * 7 + dnum);
    chromatic = 0;

    switch (dnum) {
        case 0: // unison
            if (quality[0] == 'A') {
                chromatic = (int)quality.size();
            }
            else if (quality[0] == 'd') {
                chromatic = -(int)quality.size();
            }
            else if (quality == "P") {
                chromatic = 0;
            }
            else {
                std::cerr << "Error in interval quality: " << intervalName << std::endl;
                chromatic = INVALID_INTERVAL_CLASS;
                diatonic = INVALID_INTERVAL_CLASS;
                return;
            }
            break;
        case 1: // second
            if (quality == "M") {
                chromatic = 2;
            }
            else if (quality == "m") {
                chromatic = 1;
            }
            else if (quality[0] == 'A') {
                chromatic = 2 + (int)quality.size();
            }
            else if (quality[0] == 'd') {
                chromatic = 1 - (int)quality.size();
            }
            else {
                std::cerr << "Error in interval quality: " << intervalName << std::endl;
                chromatic = INVALID_INTERVAL_CLASS;
                diatonic = INVALID_INTERVAL_CLASS;
                return;
            }
            break;
        case 2: // third
            if (quality == "M") {
                chromatic = 4;
            }
            else if (quality == "m") {
                chromatic = 3;
            }
            else if (quality[0] == 'A') {
                chromatic = 4 + (int)quality.size();
            }
            else if (quality[0] == 'd') {
                chromatic = 3 - (int)quality.size();
            }
            else {
                std::cerr << "Error in interval quality: " << intervalName << std::endl;
                chromatic = INVALID_INTERVAL_CLASS;
                diatonic = INVALID_INTERVAL_CLASS;
                return;
            }
            break;
        case 3: // fourth
            if (quality[0] == 'A') {
                chromatic = 5 + (int)quality.size();
            }
            else if (quality[0] == 'd') {
                chromatic = 5 - (int)quality.size();
            }
            else if (quality == "P") {
                chromatic = 5;
            }
            else {
                std::cerr << "Error in interval quality: " << intervalName << std::endl;
                chromatic = INVALID_INTERVAL_CLASS;
                diatonic = INVALID_INTERVAL_CLASS;
                return;
            }
            break;
        case 4: // fifth
            if (quality[0] == 'A') {
                chromatic = 7 + (int)quality.size();
            }
            else if (quality[0] == 'd') {
                chromatic = 7 - (int)quality.size();
            }
            else if (quality == "P") {
                chromatic = 7;
            }
            else {
                std::cerr << "Error in interval quality: " << intervalName << std::endl;
                chromatic = INVALID_INTERVAL_CLASS;
                diatonic = INVALID_INTERVAL_CLASS;
                return;
            }
            break;
        case 5: // sixth
            if (quality == "M") {
                chromatic = 9;
            }
            else if (quality == "m") {
                chromatic = 8;
            }
            else if (quality[0] == 'A') {
                chromatic = 9 + (int)quality.size();
            }
            else if (quality[0] == 'd') {
                chromatic = 8 - (int)quality.size();
            }
            else {
                std::cerr << "Error in interval quality: " << intervalName << std::endl;
                chromatic = INVALID_INTERVAL_CLASS;
                diatonic = INVALID_INTERVAL_CLASS;
                return;
            }
            break;
        case 6: // seventh
            if (quality == "M") {
                chromatic = 11;
            }
            else if (quality == "m") {
                chromatic = 10;
            }
            else if (quality[0] == 'A') {
                chromatic = 11 + (int)quality.size();
            }
            else if (quality[0] == 'd') {
                chromatic = 10 - (int)quality.size();
            }
            else {
                std::cerr << "Error in interval quality: " << intervalName << std::endl;
                chromatic = INVALID_INTERVAL_CLASS;
                diatonic = INVALID_INTERVAL_CLASS;
                return;
            }
            break;
    }
    chromatic *= direction;
}

New test program:


int main(void)
{
    TPitch pitch(dpc_C, 0, 4); // middle C

    Transpose transpose;

    // transpose.setBase40() is the default system.
    transpose.setTransposition(transpose.perfectFifthClass());
    std::cout << "Starting pitch:\t\t\t\t" << pitch << std::endl;
    transpose.transpose(pitch);
    std::cout << "Transposed up a perfect fifth:\t\t" << pitch << std::endl;

    // testing use of a different base for transposition:
    transpose.setBase600(); // allows up to 42 sharps or flats
    // Note that transpose value is cleared when setAccid() or setBase*() is called.
    transpose.setTransposition(-transpose.perfectFifthClass());
    transpose.transpose(pitch);
    std::cout << "Transposed back down a perfect fifth:\t" << pitch << std::endl;

    // testing use of interval string
    transpose.setTransposition("-m3");
    transpose.transpose(pitch);
    std::cout << "Transposed down a minor third:\t\t" << pitch << std::endl;

    // testing validation system for under/overflows:
    std::cout << std::endl;
    pitch.setPitch(dpc_C, 2, 4); // C##4
    std::cout << "Initial pitch:\t\t" << pitch << std::endl;
    transpose.transpose(pitch, "A4"); // now F###4
    bool valid = pitch.isValid(2);
    std::cout << "Up an aug. 4th:\t\t" << pitch;
    if (!valid) {
        std::cout << "\t(not valid in base-40 system)";
    }
    std::cout << std::endl;

    // calculate interval between two pitches:
    std::cout << std::endl;
    std::cout << "TESTING INTERVAL NAMES IN BASE-40:" << std::endl;
    transpose.setBase40();
    TPitch p1(dpc_C, 0, 4);
    TPitch p2(dpc_F, 2, 4);
    std::cout << "\tInterval between " << p1 << " and " << p2;
    std::cout << " is " << transpose.getIntervalName(p1, p2) << std::endl;
    TPitch p3(dpc_G, -2, 3);
    std::cout << "\tInterval between " << p1 << " and " << p3;
    std::cout << " is " << transpose.getIntervalName(p1, p3) << std::endl;

    std::cout << "TESTING INTERVAL NAMES IN BASE-600:" << std::endl;
    transpose.setBase600();
    std::cout << "\tInterval between " << p1 << " and " << p2;
    std::cout << " is " << transpose.getIntervalName(p1, p2) << std::endl;
    std::cout << "\tInterval between " << p1 << " and " << p3;
    std::cout << " is " << transpose.getIntervalName(p1, p3) << std::endl;
    std::cout << std::endl;

    std::cout << "TESTING INTERVAL NAME TO CIRCLE-OF-FIFTHS:" << std::endl;
    std::cout << "\tM6 should be 3:  " << transpose.intervalToCircleOfFifths("M6") << std::endl;
    std::cout << "\tm6 should be -4: " << transpose.intervalToCircleOfFifths("m6") << std::endl;

    std::cout << "TESTING CIRCLE-OF-FIFTHS TO INTERVAL NAME:" << std::endl;
    std::cout << "\t3 should be M6:  " << transpose.circleOfFifthsToIntervalName(3) << std::endl;
    std::cout << "\t-4 should be m6: " << transpose.circleOfFifthsToIntervalName(-4) << std::endl;
    std::cout << std::endl;

    std::cout << "TESTING INTERVAL NAME TO DIATONIC/CHROMATIC:" << std::endl;
    std::cout << "\tD-1,C-2 should be -M2:  " << transpose.diatonicChromaticToIntervalName(-1, -2) << std::endl;
    std::cout << "\tD3,C6 should be A4:     " << transpose.diatonicChromaticToIntervalName(3, 6) << std::endl;

    int chromatic;
    int diatonic;

    std::cout << "TESTING DIATONIC/CHROMATIC TO INTERVAL NAME:" << std::endl;
    std::cout << "\t-M2 should be D-1,C-2:  ";
    transpose.intervalToDiatonicChromatic(diatonic, chromatic, "-M2");
    std::cout << "D" << diatonic << ",C" << chromatic << std::endl;

    std::cout << "\tA4 should be D3,C6:     ";
    transpose.intervalToDiatonicChromatic(diatonic, chromatic, "A4");
    std::cout << "D" << diatonic << ",C" << chromatic << std::endl;

    return 0;
}

New output from basic test program:

Starting pitch:             C4
Transposed up a perfect fifth:      G4
Transposed back down a perfect fifth:   C4
Transposed down a minor third:      A3

Initial pitch:      C##4
Up an aug. 4th:     F###4   (not valid in base-40 system)

TESTING INTERVAL NAMES IN BASE-40:
    Interval between C4 and F##4 is AA4
    Interval between C4 and Gbb3 is -AA4
TESTING INTERVAL NAMES IN BASE-600:
    Interval between C4 and F##4 is AA4
    Interval between C4 and Gbb3 is -AA4

TESTING INTERVAL NAME TO CIRCLE-OF-FIFTHS:
    M6 should be 3:  3
    m6 should be -4: -4
TESTING CIRCLE-OF-FIFTHS TO INTERVAL NAME:
    3 should be M6:  M6
    -4 should be m6: m6

TESTING INTERVAL NAME TO DIATONIC/CHROMATIC:
    D-1,C-2 should be -M2:  -M2
    D3,C6 should be A4:     A4
TESTING DIATONIC/CHROMATIC TO INTERVAL NAME:
    -M2 should be D-1,C-2:  D-1,C-2
    A4 should be D3,C6:     D3,C6
craigsapp commented 4 years ago

Another implementation note for transposition of MEI data:

It will be important for a key signature to be present in the data, and that this key signature be transposed along with the music. If there is no key signature, then a key signature with no sharps/flats should be assumed in the original data, and this dummy key signature should be transposed as usual for a real <keySig>.

The reason for this is that when there is no key signature, the logical accidentals will be migrating between @accid and @accid.ges; however, there is no mechanism currently in verovio to recalculate visual accidentals vs. gestural accidentals.

Example:

Here is a case where there is no key signature in the starting or ending music:

Screen Shot 2019-12-06 at 9 19 55 PM

Notice that the third note in the transposed music has an @accid="s" where there was originally no @accid, since the original accidental was @accid.ges="n". The logical accidental migrated from @accid.ges to @accid.

When there is a key signature in the input/output to transposition which matches the transposition interval, there will be no migration between @accid and @accid.ges:

Screen Shot 2019-12-06 at 9 20 12 PM

The starting key signature is 0 sharp/flats and transposing up a major second generates a key signature with two sharps. This allows for the sharp on the third note in the transposition to remain in the @accid.ges parameter for the note (from the original @accid.ges="n", or an implied version of a natural.

The Transpose::intervalToCircleOfFifths() function can be used to calculate the correct new key signature for a given transposition interval.

craigsapp commented 4 years ago

Here is a refined algorithm for transposition by interval or key tonic.

The idea is that there will be an option to verovio called --transpose that can be given two types of data: (1) a chromatic interval, or (2) a tonic pitch in the new key with optional direction and octave of transposition added.

For (1) chromatic intervals, the format is an optional sign, followed by a chromatic quality followed by a diatonic number of steps. Examples: +M2 = up major second, -d5 = down diminished fifth

As a regular expression:

([-]|[+]?)([PpMm]|[Aa]+|[dD]+)([^0]\d*)

pieces of the regular expression:

([-]|[+]?)

The direction of the interval, with - indicating down and no sign or a + means up. A special cases is P1 which is a perfect unison (so +P1 == -P1 since there is no movement up or down.

([PpMm]|[Aa]+|[dD]+)

Then there is the chromatic quality of the interval. P means perfect, M means major, m means minor, d means diminished, A means augmented, dd means doubly diminished (and so on), AA means doubly augmented (and so on). For [PdA] the case of the letter does not matter so [pDa] should be interpreted as equivalent. M and m are case sensitive (major and minor).

([^0]\d*)

This is the diatonic interval which is any (reasonable) positive integer. A unison is 1, a second is 2, etc. Compound intervals an octave and above can also be represented, such as 8 for an octave, a 9 for a ninth (octave plus a second), 10 for a tenth (octave plus a third), 15 = two octaves, 16 = two octaves plus a second.

The Transposer class understands this format of chromatic interval, so verovio does not need to otherwise parse this string and it can be passed to the Transposer class. The Transposer class will print an error message if the string is not formatted correctly, and it will return an error interval which is a very large interval going down. Probably refinement of handling errors will be necessary, either by checking the string before sending it to the Transposer class or adding a function that pre-verifies the input string. One possibility is to add a boolean output to Transposer::setTransposition(string) and/or the Transposer::Transpose functions which can be checked for error. The Transposer::IsValid function can also be used inTransposer::Transpose to verify no overflow/underflow in the pitch spellings (provided that Transposer::setBase600 is used rather than the default Transpose::setBase40. Each transpose pitch could be checked with IsValid(3) to verify that the accidental does not exceed +/- three sharps/flats. If so, then false could be returned by Transpose, indicating there is at least one error in note@accid. Alternatively there could be enharmonic transposition (such as F#### being transposed to G##). Doing an enharmonic correction would prevent bad note@accid data, but it is a lossy process: reversing the transposition will not result in the same note@pname values before transposition was done.


For (2) tonic pitch names given as arguments to the --transpose option, the regular expression is made up of an optional direction, a pname and an accid:

([+]*|[-]*)([A-Ga-g])([Ss]*|[Ff]*)

Optional direction:

([+-]?)

If no direction is given, then the smallest interval will be chosen. For example if starting from C major and transposing to G major, the calculated interval will be down a perfect fourth, since the G below C is closer than the G above C.

When the direction is +, the next higher pitch that matches the new tonic will define the interval. For C major to G major, this is a perfect fifth up. When the direction is -, the next lower pitch that matches the new tonic will define the interval. For C major to G major, this is a perfect fourth down.

The + or - direction can be doubled/tripled/etc. to indicate additional octave transpositions. For example --g from C major means to transpose down an octave and a fourth: A forth to the next lower G, and then an octave to the next lower G. Likewise, +++g from C major means to transpose up two octaves and a fifth: A fifth to the next higher G and ++ means two octaves above that G.

Then comes a case-insensitive @pname for the tonic of the new key:

([A-Ga-g])

Followed by an optional @accid for the new key tonic, which is also case-insensitive:

([Ss]*|[Ff]*)

Examples:

tonic parameter meaning
g transpose current tonic to closest G tonic note (up or down a fourth from current tonic)
+g transpose to the next higher G tonic
-g transpose down to next lower G tonic
++g transpose to second next higher G tonic
--g transpose to second next lower G tonic
ff transpose to nearest F-flat
-cs transpose to next lower C-sharp
++BF transpose up to second next higher B-flat

Note that in the original system I proposed, the octave was indicated by doubling @pname. But the problem is that this conflicts with using f for flat, so I move the octave transpositions to doubling of the direction sign. Using # may also be problematic for the original sharp sign on the command line, since this character can be interpreted as being the start of a comment if it is not backslashed or placed in quotations.


The algorithm for transposing by tonic:


At this point the key transposition process becomes equivalent to the interval transposition process.

earboxer commented 4 years ago

After #1242 is merged, I think this will work completely as specified above.

The only issue left is just an error message being outputted:

When you transpose by tonic, it will still output a message to stderr about the failure to parse the interval string.

(After which, this should be good to be merged into develop and this issue closed).

craigsapp commented 4 years ago

This issue is sufficiently implemented to warrant closing.

Four main outstanding things to deal with as needed:

(1) When there is no key signature at the start of the music, the current code is assigning no-sharp/flat key signature which is correct. But this key signature also needs to be inserted into the data before transposition is done, so that the dummy key signature can be transposed along with the rest of the music. This is necessary to avoid having to redistribute accidentals between note@accid and note@accid.ges which is complicated, and usually not wanted when doing transpositions (usually the key signature should change when transposing anyway). This is added as a separate issue to deal with as needed: https://github.com/rism-ch/verovio/issues/1240 .

(2) When there is no key designation (such as C major) in the data and transposition is supposed to be done by key rather than key signature, the program is making an assumption that the music is in a major mode, and that the tonic matches the number of sharps/flats in the music. This will cause the wrong transposition in many cases, so at a minimum, an error message should be printed that there could be a problem in the transposition due to there being no key designation given in the data. Eventually more sophisticated estimating of the key could be used to minimize the error rate in such cases. Another enhancement is to check for a keySig@mode and use that when keySig@pname is missing. Extra functions have been added to the Transposer class to allow for this eventually:

    TransPitch CircleOfFifthsToMajorTonic(int fifths);
    TransPitch CircleOfFifthsToMinorTonic(int fifths);
    TransPitch CircleOfFifthsToDorianTonic(int fifths);
    TransPitch CircleOfFifthsToPhrygianTonic(int fifths);
    TransPitch CircleOfFifthsToLydianTonic(int fifths);
    TransPitch CircleOfFifthsToMixolydianTonic(int fifths);
    TransPitch CircleOfFifthsToLocrianTonic(int fifths);

(3) Dealing with scores with transposing parts and using the key transposition system. This will require more refinement as described in the refined transposition algorithm description further up in the issue thread. Also maybe refinements for transposing all keySig in the data (the updates that @lpugin proposed in one of the pull requests are not clear to me that the current system will catch all cases where keySig elements will be found and transposed). See more info in comment https://github.com/rism-ch/verovio/issues/1189#issuecomment-562369413 .

(4) Microtonal transposition. Currently music in microtonal notation will not be transposed correctly. This can be enhanced as needed. The microtonal transposition could be handled in two possible ways: treat quarter tones as intermediate values in the chromatic alterations, such as 0.5 for a half-sharp (quarter tone), and 1.5 for a sharp plus a half sharp, -0.5 for a half-flat, -1.5 for a flat plus half flat. The second way is to redefine integers to represent half-sharps: 0 = natural, 1 = half sharp (instead of sharp), 2 = sharp, 3 = one and a half sharps., etc. The second way may be less sensitive to errors, since 1.499999 would need to be corrected to 1.5 when using floating-point accidental. There may also be the need for 1/8th tone in certain cases, so allowing for 0.25 units for 1/4 of a sharp might be useful to implement directly rather than reimplementing after the quarter-tone system.

Another possible refinement is to allow extended circle-of-fifth key signatures (issue https://github.com/rism-ch/verovio/issues/1230). The data will currently be correct when transposing up to three sharps/flats accidentals on notes, and key signatures will be correct up to relatively infinite numbers of sharps flats, but key signatures can only be correctly rendered up to +/- 7 sharps/flats.

craigsapp commented 4 years ago

Also there is the issue of enharmonic wrapping which is discussed a bit above and which would be slightly interesting to implement. How to to interface to this sort of case would have to be thought about, and in any case it can be implemented when really needed (not likely for the current uses of verovio, but may be useful at some point).

An example would be: Suppose music containing the keys C and G major are transposed to C-sharp and G-sharp major. G-sharp major is a theoretical key with a theoretical key signature of 8 sharps (the f position would be displayed as a double sharp). G-sharp major key signatures are never notated, and instead the music would be transposed enharmonically to A-flat major key signature (4 flats instead of 8 sharps).

The current behavior of --transpose is to not do any enharmonic equivalent transpositions for theoretical key signatures. However, the theoretical key signatures are not currently rendered by verovio.

craigsapp commented 4 years ago

Here is an example of transposition by key tonic related to an early test example on this issue thread:

Screen Shot 2019-12-20 at 00 43 29

Transposing from the original D minor (which is specified in the data) into E minor:

verovio --transpose e test.mei

Results in the notation:

Screen Shot 2019-12-20 at 00 43 22

MEI input data:

<?xml version="1.0" encoding="UTF-8"?>
<?xml-model href="https://music-encoding.org/schema/4.0.0/mei-all.rng" type="application/xml" schematypens="http://relaxng.org/ns/structure/1.0"?>
<?xml-model href="https://music-encoding.org/schema/4.0.0/mei-all.rng" type="application/xml" schematypens="http://purl.oclc.org/dsdl/schematron"?>
<mei xmlns="http://www.music-encoding.org/ns/mei" meiversion="4.0.0">
 <meiHead>
  <fileDesc>
   <titleStmt>
    <title />
   </titleStmt>
   <pubStmt />
  </fileDesc>
  <encodingDesc>
   <appInfo>
    <application isodate="2019-12-20T00:42:44" version="2.4.0-dev-2bbba3e-dirty">
     <name>Verovio</name>
     <p>Transcoded from Humdrum</p>
    </application>
   </appInfo>
  </encodingDesc>
  <workList>
   <work>
    <title />
   </work>
  </workList>
 </meiHead>
 <music>
  <body>
   <mdiv xml:id="mdiv-0000001424087937">
    <score xml:id="score-0000000940711344">
     <scoreDef xml:id="scoredef-0000000256908801">
      <staffGrp xml:id="staffgrp-0000000789733026">
       <staffDef xml:id="staffdef-0000001011347073" n="1" lines="5">
        <clef xml:id="clef-L2F1" shape="G" line="2" />
        <keySig xml:id="keysig-L3F1" pname="d" mode="minor" sig="1f" />
        <meterSig xml:id="metersig-L5F1" count="4" unit="4" />
       </staffDef>
      </staffGrp>
     </scoreDef>
     <section xml:id="section-L1F1">
      <measure xml:id="measure-L1" n="0">
       <staff xml:id="staff-0000000197682131" n="1">
        <layer xml:id="layer-L1F1N1" n="1">
         <beam xml:id="beam-L7F1-L10F1">
          <note xml:id="note-L7F1" dur="16" oct="4" pname="d" accid.ges="n" />
          <note xml:id="note-L8F1" dur="16" oct="4" pname="e" accid.ges="n" />
          <note xml:id="note-L9F1" dur="16" oct="4" pname="f" accid.ges="n" />
          <note xml:id="note-L10F1" dur="16" oct="4" pname="g" accid.ges="n" />
         </beam>
         <beam xml:id="beam-L11F1-L12F1">
          <note xml:id="note-L11F1" dur="8" oct="4" pname="a" accid.ges="n" />
          <note xml:id="note-L12F1" dur="8" oct="5" pname="d" accid.ges="n" />
         </beam>
         <beam xml:id="beam-L13F1-L14F1">
          <note xml:id="note-L13F1" dur="8" oct="5" pname="c" accid="s" />
          <note xml:id="note-L14F1" dur="8" oct="4" pname="a" accid.ges="n" />
         </beam>
         <beam xml:id="beam-L15F1-L16F1">
          <note xml:id="note-L15F1" dur="8" oct="4" pname="e" accid.ges="n" />
          <note xml:id="note-L16F1" dur="8" oct="4" pname="g" accid.ges="n" />
         </beam>
        </layer>
       </staff>
      </measure>
      <measure xml:id="measure-L17" n="2">
       <staff xml:id="staff-L17F1N1" n="1">
        <layer xml:id="layer-L17F1N1" n="1">
         <beam xml:id="beam-L18F1-L19F1">
          <note xml:id="note-L18F1" dur="8" oct="4" pname="f" accid="s" />
          <note xml:id="note-L19F1" dur="8" oct="4" pname="d" accid.ges="n" />
         </beam>
         <note xml:id="note-L20F1" dur="4" oct="5" pname="c">
          <accid xml:id="accid-L20F1" accid="n" func="caution" />
         </note>
         <beam xml:id="beam-L21F1-L23F1">
          <note xml:id="note-L21F1" dur="8" oct="5" pname="c" accid.ges="n" />
          <note xml:id="note-L22F1" dur="16" oct="4" pname="b" accid="n" />
          <note xml:id="note-L23F1" dur="16" oct="4" pname="a" accid.ges="n" />
         </beam>
         <beam xml:id="beam-L24F1-L25F1">
          <note xml:id="note-L24F1" dur="8" oct="4" pname="b" accid.ges="n" />
          <note xml:id="note-L25F1" dur="8" oct="4" pname="g" accid.ges="n" />
         </beam>
        </layer>
       </staff>
       <tie xml:id="tie-L20F1-L21F1" startid="#note-L20F1" endid="#note-L21F1" />
      </measure>
     </section>
    </score>
   </mdiv>
  </body>
 </music>
</mei>

MEI output data, transposed to E minor:

<?xml version="1.0" encoding="UTF-8"?>
<?xml-model href="https://music-encoding.org/schema/4.0.0/mei-all.rng" type="application/xml" schematypens="http://relaxng.org/ns/structure/1.0"?>
<?xml-model href="https://music-encoding.org/schema/4.0.0/mei-all.rng" type="application/xml" schematypens="http://purl.oclc.org/dsdl/schematron"?>
<mei xmlns="http://www.music-encoding.org/ns/mei" meiversion="4.0.0">
 <meiHead>
  <fileDesc>
   <titleStmt>
    <title />
   </titleStmt>
   <pubStmt />
  </fileDesc>
  <encodingDesc>
   <appInfo>
    <application isodate="2019-12-20T00:42:44" version="2.4.0-dev-2bbba3e-dirty">
     <name>Verovio</name>
     <p>Transcoded from Humdrum</p>
    </application>
   </appInfo>
  </encodingDesc>
  <workList>
   <work>
    <title />
   </work>
  </workList>
 </meiHead>
 <music>
  <body>
   <mdiv xml:id="mdiv-0000001424087937">
    <score xml:id="score-0000000940711344">
     <scoreDef xml:id="scoredef-0000000256908801">
      <staffGrp xml:id="staffgrp-0000000789733026">
       <staffDef xml:id="staffdef-0000001011347073" n="1" lines="5">
        <clef xml:id="clef-L2F1" shape="G" line="2" />
        <keySig xml:id="keysig-L3F1" accid="n" pname="e" mode="minor" sig="1s" />
        <meterSig xml:id="metersig-L5F1" count="4" unit="4" />
       </staffDef>
      </staffGrp>
     </scoreDef>
     <section xml:id="section-L1F1">
      <measure xml:id="measure-L1" n="0">
       <staff xml:id="staff-0000000197682131" n="1">
        <layer xml:id="layer-L1F1N1" n="1">
         <beam xml:id="beam-L7F1-L10F1">
          <note xml:id="note-L7F1" dur="16" oct="4" pname="e" accid.ges="n" />
          <note xml:id="note-L8F1" dur="16" oct="4" pname="f" accid.ges="s" />
          <note xml:id="note-L9F1" dur="16" oct="4" pname="g" accid.ges="n" />
          <note xml:id="note-L10F1" dur="16" oct="4" pname="a" accid.ges="n" />
         </beam>
         <beam xml:id="beam-L11F1-L12F1">
          <note xml:id="note-L11F1" dur="8" oct="4" pname="b" accid.ges="n" />
          <note xml:id="note-L12F1" dur="8" oct="5" pname="e" accid.ges="n" />
         </beam>
         <beam xml:id="beam-L13F1-L14F1">
          <note xml:id="note-L13F1" dur="8" oct="5" pname="d" accid="s" />
          <note xml:id="note-L14F1" dur="8" oct="4" pname="b" accid.ges="n" />
         </beam>
         <beam xml:id="beam-L15F1-L16F1">
          <note xml:id="note-L15F1" dur="8" oct="4" pname="f" accid.ges="s" />
          <note xml:id="note-L16F1" dur="8" oct="4" pname="a" accid.ges="n" />
         </beam>
        </layer>
       </staff>
      </measure>
      <measure xml:id="measure-L17" n="2">
       <staff xml:id="staff-L17F1N1" n="1">
        <layer xml:id="layer-L17F1N1" n="1">
         <beam xml:id="beam-L18F1-L19F1">
          <note xml:id="note-L18F1" dur="8" oct="4" pname="g" accid="s" />
          <note xml:id="note-L19F1" dur="8" oct="4" pname="e" accid.ges="n" />
         </beam>
         <note xml:id="note-L20F1" dur="4" oct="5" pname="d">
          <accid xml:id="accid-L20F1" accid="n" func="caution" />
         </note>
         <beam xml:id="beam-L21F1-L23F1">
          <note xml:id="note-L21F1" dur="8" oct="5" pname="d" accid.ges="n" />
          <note xml:id="note-L22F1" dur="16" oct="5" pname="c" accid="s" />
          <note xml:id="note-L23F1" dur="16" oct="4" pname="b" accid.ges="n" />
         </beam>
         <beam xml:id="beam-L24F1-L25F1">
          <note xml:id="note-L24F1" dur="8" oct="5" pname="c" accid.ges="s" />
          <note xml:id="note-L25F1" dur="8" oct="4" pname="a" accid.ges="n" />
         </beam>
        </layer>
       </staff>
       <tie xml:id="tie-L20F1-L21F1" startid="#note-L20F1" endid="#note-L21F1" />
      </measure>
     </section>
    </score>
   </mdiv>
  </body>
 </music>
</mei>