Closed craigsapp closed 6 years ago
I implemented the MIDI program change in Verovio with offset.
Wikipedia tells about GM
The 0 to 127 numbering is usually only used internally by the synthesizer; the vast majority of MIDI devices, digital audio workstations and professional MIDI sequencers display these Program Numbers as shown in the table (1-128).
As I see MEI coding more on the visual side, I would go with 1-128. Although it is not always a good idea to follow MusicXML, this is how it is used there. https://usermanuals.musicxml.com/MusicXML/Content/CT-MusicXML-midi-instrument.htm
And whatever it may be, it should be harmonized with data.MIDICHANNEL
.
One idea related to data.MIDICHANNEL
data types would be to allow both systems (enumerating from 1 or enumerating from 0): A plain integer would be zero offset, so MIDI channels would be in the range from 0 to 15. To use the one-indexed version, the channel would be a string rather than a number, and the values would be something like: ch1
, ch2
, ch3
, ..., ch16
, prefixing the data with ch
to indicate it is a 1-offset channel number. These would be mapped to the integers 0–15 when converting to MIDI files. This is probably the sanest thing to do.
This is how I often do it in my C++ MIDI code, where I use the integers to match how the MIDI protocol implement the channel enumerations from 0, and then I use an enumeration such as CH_1 = 0
, and use CH_1
in the code rather than 0
. Then for clarity if I am working from a user's viewpoint I use CH_1 for the first MIDI channel, and 0 when from a programming viewpoint.
As I see MEI coding more on the visual side, I would go with 1-128. Although it is not always a good idea to follow MusicXML, this is how it is used there. https://usermanuals.musicxml.com/MusicXML/Content/CT-MusicXML-midi-instrument.htm
MusicXML is indexing them from 1. But there are parameters like pan and elevation which need to be mapped even further to translate into MIDI data, which are strictly integers in the range from 0 to 127.
Compare how pan is currently expressed in MEI:
http://music-encoding.org/guidelines/v3/elements/instrdef.html
@midi.pan(optional)Sets the instrument's position in a stereo field. Values of 0 and 1 both pan left, 127 pans right, and 64 pans to the center. Value conforms to data.MIDIVALUE. att.midiinstrument
While in MusicXML the value for pan is in the range from -180 to 180 deg.
When converting to a MIDI file, the range -180 to 180 would be mapped to the range 0 to 127. A hybrid system would be to use integers in the range from 0 to 127, and then allow a data type such as "deg", so 0deg
would map to the MIDI data value 64
, for example. Then you can use raw MIDI data values or degrees.
An esthetic problem is that percussion timbers are implemented as MIDI data values representing keys. These are typically indicated by 0-indexed values (such that middle C is the integer 60) in the user domain. So if you are indexing your timbres from 1 and percussion MIDI data from 0, there is some incongruity.
The true values for MIDI protocol data is integers in the range from 0 to 127, so plain integers should represent these values directly. For convenience mappings, there should be a datatype associated with the alternate interpretation, such as ch1
meaning the first channel, which maps to the integer equivalent 0
, or 50%
representing a MIDI volume of 64
. In other words, there are two units for describing MIDI channels, just as there are multiple units for describing distances. So allowing both by adding a unit type with the value is a good way to allow flexibility and avoid potential confusion (and the unitless version is the raw MIDI data type which is 0-indexed).
The system proposed above for MIDI channels could also be applied to General MIDI instrument numbers: integer values could be index from 0 as in the MIDI protocol, and string variants could be indexed by 1. For example the integer 0
or the string i1
(or pc1
for "patch change 1" which might be more readable) could represent the first instrument in the enumeration (Acoustic_Grand_Piano). This would allow for MIDI instruments to have the MIDI data datatype as well as a string for which could be easily converted to the integer form by removing the string prefix and subtracting one.
I like @craigsapp's idea of accommodating both 0- and 1-based values. I'd keep integer or "ch"+integer values for MIDI channels, but use integers or "in"+integer (for "instrument") values for instrument numbers. "pc" (for "patch change") implies a change from something to something else; "in" is more declarative.
Using in
as a prefix for General MIDI instrument numbers that are indexed starting at 1 sounds good to me.
"Patch change" is the traditional name for the MIDI message that changes the instrument, but of course the input parameter is the instrument number. "Patch change" is a bit technically obscure now and comes from electroacoustic era and early computers where there was a patch bay that was "programmed" by making connections with wires:
@craigsapp, Can you submit a feature request please?
Is this pressing enough to add to v. 4.0.0 or can it wait 'til 5.0.0?
It should be definitely in 4.0.0, as the current state isn’t ok!
I'll implement the following ODD asap --
<macroSpec ident="data.MIDICHANNEL.0BASED" mode="add">
<desc>0-based MIDI channel numbers; i.e., 0-15.</desc>
<content>
<rng:data type="nonNegativeInteger">
<rng:param name="maxInclusive">15</rng:param>
</rng:data>
</content>
</macroSpec>
<macroSpec ident="data.MIDICHANNEL.1BASED" mode="add">
<desc>1-based MIDI channel numbers; i.e., 1-16. Distinguished from 0-based values by the
addition of leading "ch".</desc>
<content>
<rng:data type="token">
<rng:param name="pattern">ch([1-9]|1[0-6])</rng:param>
</rng:data>
</content>
</macroSpec>
<macroSpec ident="data.MIDICHANNEL" module="MEI" type="dt" mode="replace">
<desc>MIDI channel number.</desc>
<content>
<alternate>
<macroRef key="data.MIDICHANNEL.0BASED"/>
<macroRef key="data.MIDICHANNEL.1BASED"/>
</alternate>
</content>
</macroSpec>
<macroSpec ident="data.MIDIINSTR.0BASED" mode="add">
<desc>0-based MIDI instrumental number, i.e., 0-127.</desc>
<content>
<rng:data type="nonNegativeInteger">
<rng:param name="maxInclusive">127</rng:param>
</rng:data>
</content>
</macroSpec>
<macroSpec ident="data.MIDIINSTR.1BASED" mode="add">
<desc>1-based MIDI instrumental number, i.e., 1-128. Distinguished from 0-based values by the
addition of leading "in".</desc>
<content>
<rng:data type="token">
<rng:param name="pattern">in(12[0-8]|1[01][0-9]|[1-9][0-9]?)</rng:param>
</rng:data>
</content>
</macroSpec>
<macroSpec ident="data.MIDIINSTR">
<desc>MIDI instrument number.</desc>
<content>
<alternate>
<macroRef key="data.MIDIINSTR.0BASED"/>
<macroRef key="data.MIDIINSTR.1BASED"/>
</alternate>
</content>
</macroSpec>
<classSpec ident="att.midiInstrument" module="MEI.midi" type="atts" mode="replace">
<desc>Attributes that record MIDI instrument information.</desc>
<constraintSpec ident="One_of_instrname_or_instrnum" scheme="isoschematron">
<constraint>
<sch:rule context="mei:*[@midi.instrname]">
<sch:assert test="not(@midi.instrnum)">Only one of @midi.instrname and @midi.instrnum
allowed.</sch:assert>
</sch:rule>
</constraint>
</constraintSpec>
<constraintSpec ident="One_of_patchname_or_patchnum" scheme="isoschematron">
<constraint>
<sch:rule context="mei:*[@midi.patchname]">
<sch:assert test="not(@midi.patchnum)">Only one of @midi.patchname and @midi.patchnum
allowed.</sch:assert>
</sch:rule>
</constraint>
</constraintSpec>
<attList>
<attDef ident="midi.instrnum" usage="opt">
<desc>Captures the General MIDI instrument number.</desc>
<datatype>
<rng:ref name="data.MIDIINSTR"/>
</datatype>
</attDef>
<attDef ident="midi.instrname" usage="opt">
<desc>Provides a General MIDI label for the MIDI instrument.</desc>
<datatype>
<rng:ref name="data.MIDINAMES"/>
</datatype>
</attDef>
<attDef ident="midi.pan" usage="opt">
<desc>Sets the instrument's position in a stereo field. Values of 0 and 1 both pan left, 127
pans right, and 64 pans to the center.</desc>
<datatype>
<rng:ref name="data.MIDIVALUE"/>
</datatype>
</attDef>
<attDef ident="midi.patchname" usage="opt">
<desc>Records a non-General MIDI patch/instrument name.</desc>
<datatype>
<rng:data type="NMTOKEN"/>
</datatype>
</attDef>
<attDef ident="midi.patchnum" usage="opt">
<desc>Records a non-General MIDI patch/instrument number.</desc>
<datatype>
<rng:ref name="data.MIDIVALUE"/>
</datatype>
</attDef>
<attDef ident="midi.volume" usage="opt">
<desc>Sets the instrument's volume.</desc>
<datatype>
<rng:ref name="data.MIDIVALUE"/>
</datatype>
</attDef>
</attList>
</classSpec>
Question -- Should other places where 0-127 is currently expected,
also allow 0- and 1-based values?
If so, then we may want to use a single, generic prefix for distinguishing the different kinds of values. The only thing that comes to mind immediately is "o" (oh) for "one".
midi.port
midi.pan
midi.patchnum
midi.volume
For midi.pan
0-127 is the raw MIDI implemetation. It might be nice to have two alternate units: percentage from 0%
to 100%
, and degrees from -90deg
to +90deg
(however for degrees, how to deal with MusicXML range from -180degrees to +180degrees should be considered):
https://usermanuals.musicxml.com/MusicXML/Content/EL-MusicXML-pan.htm
MIDI panning is strictly stereo panning between left/right speakers. Allowing for 360 degree panning might be useful for surround-sound applications, but that is not MIDI.
For midi.volume
, also a percentage unit might be nice in addition to raw MIDI: 0%
to 100%
.
midi.port
is rare and would be used to play a MIDI file on several synthesizers at the same time (allowing for more than the 16 instrument limit of standard MIDI protocol). I think I have seen it as "Port A" and "Port B" on MIDI patch bays (so not necessarily presented to the user as a number).
What is midi.patchnum
? That seems to be what I was thinking midi.instrnum
is. There is also to consider how percussion timbers are encoded in MEI. These are encoded in a different way from regular instruments in MIDI. Regular instruments are selected using the patch-change MIDI message, but percussion instruments are selected in General MIDI by playing a particular key on the tenth MIDI channel.
Instead of "o" (oh) we could use "-" (minus, as in minus 1) for 1-based values. Not a bad mnemonic, I think.
Putting the "-" after the value, i.e. @midi.pan="100-"
would fit well with any of those places where percentage values could also be used, i.e. @midi.pan="100%"
. Bare integers, i.e. @midi.pan="100"
, would still indicate 0-based values.
@craigsapp, I'm inclined to allow 0- and 1-based values everywhere. That way, I can modify the data.MIDIVALUE declaration and be done for those attributes that use only a number.
For those items in your list that need another kind of value, percent or degree, the data.MIDIVALUE can be alternated with other kinds of values.
@midi.patchnum
was added to handle non-General MIDI instrument layouts where, for instance, instrument 1 might not be an acoustic piano. Same impetus behind `@midi.patchname'.
Currently, there's no distinction between percussion and non-percussion instrument names/numbers. It's up to the encoder to use the correct values. I'm not confident that much more can be done within the schema.
I don't think handling 1-based values with a minus or an "o" would be very obvious. The whole problem arises from differentiating between programmable (internal) MIDI values and the user-side "visual" values. And the suggestions by @craigsapp are pointing in a more convenient direction. I like the proposed ODD very much, but it shoud be, indeed, expanded consistently over other attributes. But IMHO that is nothing a generic prefix could handle. Percentage would be the way to go midi.volume
and perhaps midi.pan
.
@rettinghaus, my intention is to allow both 0-based and 1-based values everywhere a "raw" MIDI value is expected and to allow "alternative" values where convenient, 0-100% for pan and volume, -90 to +90 deg for pan, etc. However, rather than prefix each 1-based value with a different mnemonic, such as 'ch' for values for MIDI channel or 'in' for MIDI instrument numbers, I'd prefer to use a single mnemonic for all 1-based values, such as 'o'. This will reduce the number and complexity of the data type declarations in the schema. Following this plan, a MIDI channel value can be expressed as 0-127 or 1-128o, while pan can be written as 0-127, 1-128o, -90 to +90deg, or 0-100%. Since "value + units" is used in many other places in MEI, for example, "100%" or "6pt", it makes sense to do the same here, i.e., "128o" instead of "o128".
As @craigsapp points out, the MusicXML value range for pan isn't supported by the MIDI spec. One option is to allow the MusicXML range and force (most?) applications to map values to the proper MIDI range. A variation of this is to use MEI's data.DEGREES which permits values from -360 to +360. Of course, this range would also need to be mapped onto -90/+90 for strict MIDI compliance. Another option is to stick with the MIDI spec and map MusicXML values (or other values like clock positions) to the proper range during conversion.
Yet another possibility is to use only MIDI-approved values in @midi.pan
, but add another attribute (such as @surround
, @field
, etc.) to hold values intended to represent a 180- or 360-degree sound field. While we're at it, we could add an attribute that's equivalent to MusicXML's concept of elevation (with the same 180- or 360-degree range, but on an "above/below" axis).
Regarding my earlier comments, I'm including percentage values in the phrase "MIDI-approved values", since they're another expression of the 0-127 range.
On the other hand, spatial location seems like a different thing to me. How about "azimuth" and "elevation" as names for the new attributes that describe this concept? Just to be clear -- @azimuth
is different from @midi.pan
because it describes not just left-to-right position, but rather a circle around the listener's head. Therefore, @azimuth
should use the range from -360 to +360, where -360, +360, and 0 all describe a position directly in front of the listener, and -180 and +180 both describe a place directly behind the listener. In the case of @elevation
, -360, +360, and 0 describe a position above the listener, while -180 and +180 both describe a place below the listener.
Here's another stab at it --
<macroSpec ident="data.MIDICHANNEL" module="MEI" type="dt" mode="replace">
<desc>MIDI channel number.</desc>
<content>
<rng:data type="token">
<rng:param name="pattern">(0|([1-9]|1[0-5])o?|16)</rng:param>
</rng:data>
</content>
</macroSpec>
<macroSpec ident="data.MIDIVALUE" module="MEI" type="dt" mode="replace">
<desc>MIDI values in the 0-127 or 1-128 range.</desc>
<content>
<rng:data type="token">
<rng:param name="pattern">0|([1-9]|[1-9][0-9]|1([0-1][0-1]|2[0-7]))o?|128</rng:param>
</rng:data>
</content>
</macroSpec>
<macroSpec ident="data.PERCENT.LIMITED" module="MEI" type="dt" mode="add">
<desc>Positive decimal number between 0 and 100, plus '%'.</desc>
<content>
<rng:data type="token">
<rng:param name="pattern">(([0-9]|[1-9][0-9])(\.[0-9]+)?|100)%</rng:param>
</rng:data>
</content>
</macroSpec>
<macroSpec ident="data.MIDIVALUE_PERCENT" module="MEI" type="dt" mode="add">
<desc>MIDI values allowed by data.MIDIVALUE plus percentage values.</desc>
<content>
<alternate>
<macroRef key="data.MIDIVALUE"/>
<macroRef key="data.PERCENT.LIMITED"/>
</alternate>
</content>
</macroSpec>
<macroSpec ident="data.NCNAME" module="MEI" type="dt" mode="add">
<desc>"Convenience" datatype that permits combining enumerated values with user-supplied
values.</desc>
<content>
<rng:data type="NCName"/>
</content>
</macroSpec>
<macroSpec ident="data.MIDIVALUE_NAME" module="MEI" type="dt" mode="add">
<desc>MIDI values allowed by data.MIDIVALUE plus NCName values.</desc>
<content>
<alternate>
<macroRef key="data.MIDIVALUE"/>
<macroRef key="data.NCNAME"/>
</alternate>
</content>
</macroSpec>
<classSpec ident="att.instrDef.ges" module="MEI.gestural" type="atts" mode="replace">
<desc>Gestural domain attributes.</desc>
<classes>
<memberOf key="att.channelized"/>
<memberOf key="att.midiInstrument"/>
<memberOf key="att.soundfield"/>
</classes>
</classSpec>
<classSpec ident="att.soundfield" module="MEI.gestural" type="atts" mode="add">
<desc>Attributes that locate a sound source within 3-D space.</desc>
<attList>
<attDef ident="azimuth" usage="opt">
<desc>The lateral or left-to-right plane.</desc>
<datatype>
<rng:ref name="data.DEGREES"/>
</datatype>
</attDef>
<attDef ident="elevation" usage="opt">
<desc>The above-to-below axis.</desc>
<datatype>
<rng:ref name="data.DEGREES"/>
</datatype>
</attDef>
</attList>
</classSpec>
<classSpec ident="att.midiInstrument" module="MEI.midi" type="atts" mode="replace">
<desc>Attributes that record MIDI instrument information.</desc>
<constraintSpec ident="One_of_instrname_or_instrnum" scheme="isoschematron">
<constraint>
<sch:rule context="mei:*[@midi.instrname]">
<sch:assert test="not(@midi.instrnum)">Only one of @midi.instrname and @midi.instrnum
allowed.</sch:assert>
</sch:rule>
</constraint>
</constraintSpec>
<constraintSpec ident="One_of_patchname_or_patchnum" scheme="isoschematron">
<constraint>
<sch:rule context="mei:*[@midi.patchname]">
<sch:assert test="not(@midi.patchnum)">Only one of @midi.patchname and @midi.patchnum
allowed.</sch:assert>
</sch:rule>
</constraint>
</constraintSpec>
<attList>
<attDef ident="midi.instrnum" usage="opt">
<desc>Captures the General MIDI instrument number. Use an integer for a 0-based value. An
integer preceded by "in" indicates a 1-based value.</desc>
<datatype>
<rng:ref name="data.MIDIVALUE"/>
</datatype>
</attDef>
<attDef ident="midi.instrname" usage="opt">
<desc>Provides a General MIDI label for the MIDI instrument.</desc>
<datatype>
<rng:ref name="data.MIDINAMES"/>
</datatype>
</attDef>
<attDef ident="midi.pan" usage="opt">
<desc>Sets the instrument's position in a stereo field. Values of 0 and 1 both pan left, 127
pans right, and 64 pans to the center.</desc>
<datatype>
<rng:ref name="data.MIDIVALUE_PERCENT"/>
</datatype>
</attDef>
<attDef ident="midi.patchname" usage="opt">
<desc>Records a non-General MIDI patch/instrument name.</desc>
<datatype>
<rng:data type="NMTOKEN"/>
</datatype>
</attDef>
<attDef ident="midi.patchnum" usage="opt">
<desc>Records a non-General MIDI patch/instrument number.</desc>
<datatype>
<rng:ref name="data.MIDIVALUE"/>
</datatype>
</attDef>
<attDef ident="midi.volume" usage="opt">
<desc>Sets the instrument's volume.</desc>
<datatype>
<rng:ref name="data.MIDIVALUE_PERCENT"/>
</datatype>
</attDef>
</attList>
</classSpec>
<classSpec ident="att.channelized" module="MEI.midi" type="atts" mode="replace">
<desc>Attributes that record MIDI channel information.</desc>
<attList>
<attDef ident="midi.channel" usage="opt">
<desc>Records a MIDI channel value.</desc>
<datatype>
<rng:ref name="data.MIDICHANNEL"/>
</datatype>
</attDef>
<attDef ident="midi.duty" usage="opt">
<desc>Specifies the 'on' part of the duty cycle as a percentage of a note's duration.</desc>
<datatype>
<rng:ref name="data.PERCENT.LIMITED"/>
</datatype>
</attDef>
<attDef ident="midi.port" usage="opt">
<desc>Sets the MIDI port value.</desc>
<datatype>
<rng:ref name="data.MIDIVALUE_NAME"/>
</datatype>
</attDef>
<attDef ident="midi.track" usage="opt">
<desc>Sets the MIDI track.</desc>
<datatype>
<rng:data type="positiveInteger"/>
</datatype>
</attDef>
</attList>
</classSpec>
I believe this captures all the desired features --
@midi.channel
is limited to 0-15 or 1-16@midi.pan
allows numeric or percentage values@azimuth
and @elevation
record "surround sound" values; MusicXML pan and elevation can be mapped to these attributesIn addition, a bug that allowed @midi.duty
to be greater than 100% is also fixed.
Have I spotted an error in your regex? Shouldn't it be
0|([1-9]|1[0-5])o?|16o
and 0|([1-9]|[1-9][0-9]|1([0-1][0-1]|2[0-7]))o?|128o
?
Perhaps for clarity, but not absolutely necessary. "16" and "128" are only possible in a 1-based system, just as "0" is only possible in a 0-based one. It's only the values that are possible in both systems, 1-127, which must be differentiated with "o".
@craigsapp, @lpugin, Unless I hear objections, I'll make the changes to the schema described above at the end of the week.
I like clarity. :-)
Take data.PERCENT
for example: 0%
is always 0
, but you have to add the %
.
True, but this case is somewhat different.
A percent sign is not significant for values that are always expressed in hundredths of something. Its real value is in differentiating percent values from others when something may be expressed in multiple ways, for instance, in the case of font size. In addition to percentage values, data.FONTSIZE
allows other numeric values -- it's the trailing "%," "pt", or "vu" that makes it possible to tell a value in one system from one in another system.
Extrapolating to data.MIDIVALUE, it's the trailing 'o' that makes it possible to differentiate a 0-based value from a 1-based one, "127" and "127o", for instance. "0 (1-based)" and "128" (zero-based) are impossible; therefore, there's no need to tell them apart from their "opposing" cousins. A value of "0" is always in the 0-based system; likewise, "128" is always 1-based. Without any confusion factor, there's no need for "128o", just as there's no need for "0" in a 1-based system.
Insisting on "128o" creates a different inconsistency problem; that is, for absolute clarity we must introduce a way to say that a value of "0" is not 1-based. Alternatively, we must introduce another required trailing symbol for zero-based counting, something like "0z". But, requiring that every MIDI value state its counting system seems like overkill to me, especially if the encoding is limited to 0-based values.
@craigsapp and @lpugin, thoughts?
A somewhat confusing thing is that o
looks like a zero, but that is not very much of a problem (definitely do not allow a capital O
...). Also o
can also mean "ordinal" which is what is happening here, which is good: 0
is the first channel, 1
is the second channel and so on. So 0
= 1o
, 1
= 2o
, etc.
A reverse point of view could be to use m
for the 0-indexed numbers and leave the 1-indexed numbers without units. The m
would mean "MIDI implementation numbers that are indexed from 0".
Perhaps for clarity, but not absolutely necessary. "16" and "128" are only possible in a 1-based system, just as "0" is only possible in a 0-based one. It's only the values that are possible in both systems, 1-127, which must be differentiated with "o".
I would say that the the o
should be appended in all cases where the number is 1-indexed. Allowing for the default unit to change is dangerous and probably adds unnecessary complexity.
16
would be an invalid token for the MIDI channel, since it should be 16o
which is equivalent to the integer 15
. Likewise, the gunshot timbre should be 128o
or 127
, and not allowed to be 128
. There is no ambiguity for the human, but for the computer it would require knowledge of the (full) MEI specification to understand the difference between 127
(0-index) and 128
(1-indexed).
I can get behind the argument for "o" as short hand for "ordinal". So, "16o" and "128o" it is. I'll make the change to the regex.
Sounds good.
On the page:
http://music-encoding.org/guidelines/v3/attribute-classes/att.midiinstrument.html
The definition of a
midi.instrnum
attribute is to be of typedata.MIDIVALUE
:And on the page
http://music-encoding.org/guidelines/v3/data-types/data.midivalue.html
The definition of a MIDI value is:
Is this correct? In other words, did I win the argument about the starting number for instruments? As these were originally offset from 1 rather than 0 in MEI.
I will have to check verovio whether or not this new definition is followed. @rettinghaus just gave me an example which was indexed from 1 rather than 0 (
Drawbar_Organ
is 16 or 17 depending on the starting instrument number, with 16 being the value to use according to the above definition).https://github.com/humdrum-tools/verovio-humdrum-viewer/issues/80