Skip to content

Commit

Permalink
tutorial-percussion WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
infojunkie committed Dec 4, 2024
1 parent e3c2038 commit 5728177
Show file tree
Hide file tree
Showing 3 changed files with 908 additions and 39 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ Screenshot of a score converted with a specialized stylesheet file.

![Screenshot of a score converted with a specialized stylesheet file.](https://github.com/infojunkie/musicxml-mscx/blob/main/9-20-special.png?raw=true)

Screenshot of a score with accidentals, including microtonal accidentals.
Screenshot of a Lilypond test snippet with microtonal accidentals.

![Screenshot of a score with accidentals, including microtonal accidentals.](https://github.com/infojunkie/musicxml-mscx/blob/main/01f-Pitches-ParenthesizedMicrotoneAccidentals.png?raw=true)
![Screenshot of a Lilypond test snippet with microtonal accidentals.](https://github.com/infojunkie/musicxml-mscx/blob/main/01f-Pitches-ParenthesizedMicrotoneAccidentals.png?raw=true)

# Theory of operation
Why write a new converter between MusicXML and MuseScore, when MuseScore itself already does 2-way conversion?
Expand Down
258 changes: 221 additions & 37 deletions src/mscx.xsl
Original file line number Diff line number Diff line change
Expand Up @@ -136,24 +136,30 @@
-->
<xsl:template match="score-part">
<Part>
<xsl:variable name="part" select="current()"/>
<xsl:variable name="part" select="//part[@id = current()/@id]"/>
<xsl:variable name="clef" select="$part//attributes/clef[1]"/>
<xsl:variable name="staffType" select="if ($clef/sign = 'percussion') then 'percussion' else 'pitched'"/>
<xsl:variable name="staffIds" select="accumulator-after('staffIds')(@id)"/>
<xsl:for-each select="$staffIds">
<xsl:variable name="staff" select="position()"/>
<Staff>
<xsl:attribute name="id"><xsl:value-of select="."/></xsl:attribute>
<StaffType group="pitched">
<name>stdNormal</name>
<xsl:if test="$root//part[@id = $part/@id]//attributes/clef/@print-object = 'no'">
<StaffType>
<xsl:attribute name="group" select="$staffType"/>
<name><xsl:value-of select="if ($staffType = 'percussion') then 'perc5Line' else 'stdNormal'"/></name>
<xsl:if test="$part//attributes/staff-details/staff-lines > 0">
<lines><xsl:value-of select="$part//attributes/staff-details/staff-lines"/></lines>
</xsl:if>
<xsl:if test="$part//attributes/clef/@print-object = 'no'">
<clef>0</clef>
</xsl:if>
<xsl:if test="$root//part[@id = $part/@id]//attributes/time/@print-object = 'no'">
<xsl:if test="$part//attributes/time/@print-object = 'no'">
<timesig>0</timesig>
</xsl:if>
<xsl:if test="$root//part[@id = $part/@id]//attributes/staff-details/staff-lines = 0">
<xsl:if test="$part//attributes/staff-details/staff-lines = 0">
<invisible>1</invisible>
</xsl:if>
<xsl:if test="$root//part[@id = $part/@id]//attributes/key/@print-object = 'no'">
<xsl:if test="$part//attributes/key/@print-object = 'no' or $staffType = 'percussion'">
<keysig>0</keysig>
</xsl:if>
</StaffType>
Expand All @@ -163,28 +169,67 @@
</bracket>
<barLineSpan><xsl:value-of select="count($staffIds)"/></barLineSpan>
</xsl:if>
<xsl:if test="$root//part[@id = $part/@id]//attributes/staff-details/staff-lines = 0">
<xsl:if test="$part//attributes/staff-details/staff-lines = 0">
<invisible>1</invisible>
</xsl:if>
</Staff>
</xsl:for-each>
<trackName><xsl:value-of select="part-name"/></trackName>
<xsl:apply-templates select="$instruments//Instrument[trackName=current()/part-name]"/>
<Instrument>
<xsl:attribute name="id" select="mscx:toHyphenated(part-name)"/>
<trackName><xsl:value-of select="part-name"/></trackName>
<longName><xsl:value-of select="if (part-name/@print-object = 'no') then '' else part-name"/></longName>
<xsl:choose>
<xsl:when test="score-instrument/instrument-sound">
<instrumentId><xsl:value-of select="score-instrument[1]/instrument-sound"/></instrumentId>
</xsl:when>
<xsl:when test="$instruments//Instrument[@id = mscx:toHyphenated(part-name)]">
<instrumentId><xsl:value-of select="$instruments//Instrument[@id = mscx:toHyphenated(part-name)]/musicXMLid"/></instrumentId>
</xsl:when>
</xsl:choose>
<xsl:if test="midi-instrument/midi-unpitched">
<useDrumset>1</useDrumset>
<xsl:apply-templates select="score-instrument" mode="drum">
<xsl:with-param name="clef" select="$clef"/>
<xsl:with-param name="lines" select="($part//attributes/staff-details/staff-lines, 5)[1]"/>
</xsl:apply-templates>
<clef>PERC</clef>
</xsl:if>
<xsl:copy-of select="$instruments/museScore/Articulation"/>
<!-- TODO Channel -->
<Channel>
<controller ctrl="0" value="1"/>
<controller ctrl="32" value="0"/>
<program value="0"/>
<controller ctrl="7" value="101"/>
<controller ctrl="10" value="63"/>
<synti>Fluid</synti>
</Channel>
</Instrument>
</Part>
</xsl:template>

<!--
Template: Part > Instrument.
Template: Part > Drum.
-->
<xsl:template match="Instrument">
<Instrument>
<xsl:copy-of select="@id|longName|shortName|trackName|Channel|Articulation"/>
<instrumentId><xsl:value-of select="musicXMLid"/></instrumentId>
<minPitchP><xsl:value-of select="tokenize(pPitchRange, '-')[1]"/></minPitchP>
<maxPitchP><xsl:value-of select="tokenize(pPitchRange, '-')[2]"/></maxPitchP>
<minPitchA><xsl:value-of select="tokenize(aPitchRange, '-')[1]"/></minPitchA>
<maxPitchA><xsl:value-of select="tokenize(aPitchRange, '-')[2]"/></maxPitchA>
</Instrument>
<xsl:template match="score-instrument" mode="drum">
<xsl:param name="clef"/>
<xsl:param name="lines"/>
<xsl:variable name="midi-instrument" select="../midi-instrument[@id = current()/@id]"/>
<xsl:variable name="note" select="($root//note[instrument/@id = current()/@id])[1]"/>
<xsl:message>
NOTE <xsl:value-of select="count($note)"/>
</xsl:message>
<Drum>
<xsl:choose>
<xsl:when test="$note/notehead"><xsl:apply-templates select="$note/notehead"/></xsl:when>
<xsl:otherwise><head>normal</head></xsl:otherwise>
</xsl:choose>
<line><xsl:value-of select="mscx:noteToLine($note/unpitched, $clef, $lines)"/></line>
<voice>0</voice>
<name><xsl:value-of select="instrument-name"/></name>
<stem><xsl:value-of select="if ($note/stem = 'up') then 1 else 2"/></stem>
</Drum>
</xsl:template>

<!--
Expand Down Expand Up @@ -584,6 +629,7 @@
<xsl:choose>
<xsl:when test="rest">
<Rest>
<xsl:apply-templates select="rest/display-step" mode="rest"/>
<xsl:apply-templates select="current()" mode="inner"/>
</Rest>
</xsl:when>
Expand Down Expand Up @@ -633,6 +679,7 @@
<Note>
<xsl:apply-templates select="notations/tied"/>
<xsl:apply-templates select="pitch"/>
<xsl:apply-templates select="instrument"/>
<xsl:apply-templates select="accidental"/>
<xsl:apply-templates select="notehead"/>
<xsl:if test="cue">
Expand All @@ -649,11 +696,31 @@
]" mode="inner"/>
</xsl:template>

<!--
Template: Rest > Offset.
-->
<xsl:template match="display-step" mode="rest">
<offset x="0">
<xsl:attribute name="y" select="mscx:noteToYOffset(.., accumulator-after('clef'))"/>
</offset>
</xsl:template>

<!--
Template: Note > Notehead.
-->
<xsl:template match="notehead">
<head><xsl:value-of select="text()"/></head>
<head>
<xsl:choose>
<xsl:when test="text() = 'x'">cross</xsl:when>
<xsl:otherwise><xsl:value-of select="text()"/></xsl:otherwise>
</xsl:choose>
</head>
<headType>
<xsl:choose>
<xsl:when test="@filled = 'no'">half</xsl:when>
<xsl:otherwise>auto</xsl:otherwise>
</xsl:choose>
</headType>
</xsl:template>

<!--
Expand Down Expand Up @@ -889,6 +956,13 @@
</xsl:if>
</xsl:template>

<xsl:template match="instrument">
<xsl:variable name="instrument" select="//midi-instrument[@id = current()/@id]"/>
<pitch><xsl:value-of select="number($instrument/midi-unpitched) - 1"/></pitch>
<tpc>-9</tpc>
<tpc2><xsl:value-of select="mscx:midiToTpc(number($instrument/midi-unpitched) - 1)"/></tpc2>
</xsl:template>

<!--
Template: Note > Accidental.
@see https://github.com/musescore/MuseScore/blob/v4.4.2/src/importexport/musicxml/internal/musicxml/musicxmlsupport.cpp#mxmlString2accSymId
Expand Down Expand Up @@ -1079,12 +1153,20 @@
</xsl:choose>
</xsl:template>

<!--
Function: Convert English Title to hyphenated-title.
-->
<xsl:function name="mscx:toHyphenated" as="xs:string">
<xsl:param name="text" as="xs:string"/>
<xsl:sequence select="replace(replace(lower-case($text), '\P{L}+', '-'), '^-|-$', '')"/>
</xsl:function>

<!--
Function: Convert hyphenated-title to camelCase.
-->
<xsl:function name="mscx:toCamelCase" as="xs:string">
<xsl:param name="text" as="xs:string"/>
<xsl:variable name="caps" select="string-join(for $t in tokenize($text,'-') return concat(upper-case(substring($t, 1, 1)), substring($t, 2)),'')"/>
<xsl:variable name="caps" select="string-join(for $t in tokenize($text, '-') return concat(upper-case(substring($t, 1, 1)), substring($t, 2)), '')"/>
<xsl:sequence select="concat(lower-case(substring($caps, 1, 1)), substring($caps, 2))"/>
</xsl:function>

Expand All @@ -1105,42 +1187,55 @@
-->
<xsl:function name="mscx:noteToTpc" as="xs:double">
<xsl:param name="note"/>
<xsl:variable name="step" select="($note/root-step, $note/bass-step, $note/step)[1]"/>
<xsl:variable name="step" select="($note/root-step, $note/bass-step, $note/step, $note/display-step)[1]"/>
<xsl:variable name="alter" select="number(($note/root-alter, $note/bass-alter, $note/alter, 0)[1])"/>
<xsl:variable name="useAlter" select="$alter - round($alter) = 0 and abs($alter) &lt;= 2"/>
<xsl:variable name="tpc" as="xs:integer">
<xsl:choose>
<xsl:when test="$step='C'">14</xsl:when>
<xsl:when test="$step='D'">16</xsl:when>
<xsl:when test="$step='E'">18</xsl:when>
<xsl:when test="$step='F'">13</xsl:when>
<xsl:when test="$step='G'">15</xsl:when>
<xsl:when test="$step='A'">17</xsl:when>
<xsl:when test="$step='B'">19</xsl:when>
<xsl:when test="$step = 'C'">14</xsl:when>
<xsl:when test="$step = 'D'">16</xsl:when>
<xsl:when test="$step = 'E'">18</xsl:when>
<xsl:when test="$step = 'F'">13</xsl:when>
<xsl:when test="$step = 'G'">15</xsl:when>
<xsl:when test="$step = 'A'">17</xsl:when>
<xsl:when test="$step = 'B'">19</xsl:when>
</xsl:choose>
</xsl:variable>
<xsl:sequence select="$tpc + (7 * (if ($useAlter) then $alter else 0))"/>
</xsl:function>

<!--
Function: Convert MIDI note to tpc.
-->
<xsl:function name="mscx:midiToTpc" as="xs:double">
<xsl:param name="midi"/>
<xsl:variable name="step" select="$midi mod 12"/>
<xsl:variable name="alter" select="(0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0)[$step + 1]"/>
<xsl:variable name="tpc" as="xs:integer" select="(14, 14, 16, 16, 18, 13, 13, 15, 15, 17, 17, 19)[$step + 1]"/>
<xsl:sequence select="$tpc + (7 * $alter)"/>
</xsl:function>

<!--
Function: Convert note to pitch.
-->
<xsl:function name="mscx:noteToPitch" as="xs:double">
<xsl:param name="note"/>
<xsl:variable name="step" select="($note/step, $note/display-step)[1]"/>
<xsl:variable name="octave" select="($note/octave, $note/display-octave)[1]"/>
<xsl:variable name="alter" select="number(($note/alter, 0)[1])"/>
<xsl:variable name="useAlter" select="$alter - round($alter) = 0 and abs($alter) &lt;= 2"/>
<xsl:variable name="pitch" as="xs:integer">
<xsl:choose>
<xsl:when test="$note/step='C'">0</xsl:when>
<xsl:when test="$note/step='D'">2</xsl:when>
<xsl:when test="$note/step='E'">4</xsl:when>
<xsl:when test="$note/step='F'">5</xsl:when>
<xsl:when test="$note/step='G'">7</xsl:when>
<xsl:when test="$note/step='A'">9</xsl:when>
<xsl:when test="$note/step='B'">11</xsl:when>
<xsl:when test="$step = 'C'">0</xsl:when>
<xsl:when test="$step = 'D'">2</xsl:when>
<xsl:when test="$step = 'E'">4</xsl:when>
<xsl:when test="$step = 'F'">5</xsl:when>
<xsl:when test="$step = 'G'">7</xsl:when>
<xsl:when test="$step = 'A'">9</xsl:when>
<xsl:when test="$step = 'B'">11</xsl:when>
</xsl:choose>
</xsl:variable>
<xsl:sequence select="$pitch + (12 * ($note/octave + 1)) + (if ($useAlter) then $alter else 0)"/>
<xsl:sequence select="$pitch + (12 * ($octave + 1)) + (if ($useAlter) then $alter else 0)"/>
</xsl:function>

<!--
Expand All @@ -1153,4 +1248,93 @@
<xsl:sequence select="100 * (if ($useAlter) then $alter else 0)"/>
</xsl:function>

<!--
Function: Convert note to ledger line.
In MuseScore, lines are counted from the top. Spaces are also counted:
.
.
.
-1
__________________ 0
1
__________________ 2
3
__________________ 4
5
__________________ 6
7
__________________ 8
9
.
.
.
In MusicXML, lines are counted from the bottom and do not include spaces:
https://www.w3.org/2021/06/musicxml40/musicxml-reference/elements/line/
__________________ 5
__________________ 4
__________________ 3
__________________ 2
__________________ 1
ALGORITHM:
- We choose F5 to be canonical line 0. This corresponds to a G clef. For other keys we compute the offset for the
note that sits at line 0. For example, in F clef, it's A3 which has offset -12 from F5 (in MuseScore line space).
- Then, we handle the case of staves with less than 5 lines. MusicXML removes lines from top to bottom,
FIXME: Handle @line and @clef-octave-change.
-->
<xsl:function name="mscx:noteToLine" as="xs:double">
<xsl:param name="note"/>
<xsl:param name="clef"/>
<xsl:param name="lines"/>
<xsl:variable name="step" select="($note/step, $note/display-step)[1]"/>
<xsl:variable name="octave" select="($note/octave, $note/display-octave)[1]"/>
<xsl:variable name="offset" as="xs:integer">
<xsl:choose>
<xsl:when test="$clef/sign = ('none', 'percussion', 'G')">0</xsl:when>
<xsl:when test="$clef/sign = 'F'">-12</xsl:when>
<xsl:otherwise><xsl:message>[mscx:noteToLine] Unhandled clef '<xsl:value-of select="$clef/sign"/>'</xsl:message></xsl:otherwise>
</xsl:choose>
</xsl:variable>
<xsl:variable name="line" as="xs:integer">
<xsl:choose>
<xsl:when test="$step = 'C'">3</xsl:when>
<xsl:when test="$step = 'D'">2</xsl:when>
<xsl:when test="$step = 'E'">1</xsl:when>
<xsl:when test="$step = 'F'">0</xsl:when>
<xsl:when test="$step = 'G'">-1</xsl:when>
<xsl:when test="$step = 'A'">-2</xsl:when>
<xsl:when test="$step = 'B'">-3</xsl:when>
<xsl:otherwise><xsl:message>[mscx:noteToLine] Unhandled step '<xsl:value-of select="$step"/>'</xsl:message></xsl:otherwise>
</xsl:choose>
</xsl:variable>
<xsl:sequence select="$line + $offset + 7 * (5 - number($octave)) - 2 * (5 - number($lines))"/>
</xsl:function>

<!--
Function: Convert note to y-offset.
MuseScore <offset> @y attribute starts at the bottom ledger line and increases downward, 1 per line (not counting spaces).
This is contrast with the <lines> element above with starts at the top ledger line and increases downward, counting the spaces.
Since we already have an algorithm to convert notes to <lines>, we can reuse it to compute the y-offset here.
FIXME: Assumes 5 lines.
-->
<xsl:function name="mscx:noteToYOffset" as="xs:double">
<xsl:param name="note"/>
<xsl:param name="clef"/>
<xsl:sequence select="(mscx:noteToLine($note, $clef, 5) - 8) div 2"/>
</xsl:function>

</xsl:stylesheet>
Loading

0 comments on commit 5728177

Please sign in to comment.