-
Notifications
You must be signed in to change notification settings - Fork 187
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Transposition in Verovio #1189
Comments
This would be useful to have in verovio, and it is not too complicated to implement. Both For transposing notes, see: http://www.ccarh.org/publications/reprints/base40 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-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 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:
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:
The base-40 interval classes are:
Here is an algorithm for converting from pitch names into base-40: Here is an algorithm for converting from base-40 integers into pitch names: |
Musical example, transposing up a major second (from D minor to E minor): 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> |
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,
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). |
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 Here is an example where I add E minor as the key, and then transpose to D minor: Test data:
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. |
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
Possible implementation:
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. |
Transposing by semitones is ambiguous as you note, so it is better to transpose by a specific chromatic interval, with possible examples being:
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 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:
Then verovio would identify C from the first key, and then calculate the base-40 transposition interval as 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. |
That looks good.
I think a good way of doing multiple octaves would be to double the tonic letter:
|
Below is a draft of a transposition system for verovio. There are two classes: (1) 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 At the bottom of the source code are example uses of the two classes ( Note that there is a string-based system for representing intervals that the
These names could be used in conjunction with the To implement in verovio: (1) create a conversion between (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 Transposition by key (such as 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 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
*/ |
Stolen from 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). |
That looks ok based on the definition of 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 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 |
The octave is more complicated. I think the best thing to do is to always use Example: For both notes, the 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 |
Borrowing the accidental alteration from @lpugin can comment on whether or not both Here are some examples where 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 |
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: 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). |
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 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 Here is a sample score where there are two instruments, one transposing (both instruments playing in unison, but I did not add key information). <?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:
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 This interval system is used for transposition in the Humdrum Toolkit I will think about adding a function to the Also for transposing key signatures, a function for converting between base-40 and circle-of-fifth systems should be added to the |
This issue discussion is getting pretty long. I started a PR at #1219. Feel free to commit onto it, or cherry-pick any commits into your own branch. So far I've
|
Here are additional methods for the 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:
|
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 The reason for this is that when there is no key signature, the logical accidentals will be migrating between Example: Here is a case where there is no key signature in the starting or ending music: Notice that the third note in the transposed music has an When there is a key signature in the input/output to transposition which matches the transposition interval, there will be no migration between 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 The |
Here is a refined algorithm for transposition by interval or key tonic. The idea is that there will be an option to verovio called For (1) chromatic intervals, the format is an optional sign, followed by a chromatic quality followed by a diatonic number of steps. Examples: As a regular expression:
pieces of the regular expression:
The direction of the interval, with
Then there is the chromatic quality of the interval.
This is the diatonic interval which is any (reasonable) positive integer. A unison is The For (2) tonic pitch names given as arguments to the
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 Then comes a case-insensitive
Followed by an optional
Examples:
Note that in the original system I proposed, the octave was indicated by doubling The algorithm for transposing by tonic:
At this point the key transposition process becomes equivalent to the interval transposition process.
|
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). |
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 (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 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 (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 #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. |
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 |
Here is an example of transposition by key tonic related to an early test example on this issue thread: Transposing from the original D minor (which is specified in the data) into E minor: verovio --transpose e test.mei Results in the notation: 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> |
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.
The text was updated successfully, but these errors were encountered: