Skip to content

Commit 73fbad0

Browse files
authored
Merge pull request #1818 from cuthbertLab/piano_endings
Support RepeatBrackets for pianos
2 parents 85db937 + 38915e6 commit 73fbad0

6 files changed

Lines changed: 265 additions & 21 deletions

File tree

music21/musicxml/__init__.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,14 @@
1212
from __future__ import annotations
1313

1414
__all__ = [
15-
'archiveTools', 'lilypondTestSuite', 'm21ToXml',
15+
'archiveTools',
16+
'lilypondTestSuite',
17+
'm21ToXml',
1618
'partStaffExporter',
17-
'test_m21ToXml', 'test_xmlToM21',
18-
'xmlObjects', 'xmlToM21',
19+
'test_m21ToXml',
20+
'test_xmlToM21',
21+
'xmlObjects',
22+
'xmlToM21',
1923
]
2024

2125
from music21.musicxml import archiveTools

music21/musicxml/testFiles.py

Lines changed: 130 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010

1111
import unittest
1212

13+
from music21 import common
14+
1315
_DOC_IGNORE_MODULE_OR_PACKAGE = True
1416

1517
chantQuemQueritis = '''<?xml version="1.0" standalone="no"?>
@@ -16067,6 +16069,131 @@
1606716069
'''
1606816070

1606916071

16072+
pianoRepeatEndings = r'''
16073+
<?xml version="1.0" encoding="UTF-8"?>
16074+
<!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 4.0 Partwise//EN" "http://www.musicxml.org/dtds/partwise.dtd">
16075+
<score-partwise version="4.0">
16076+
<work>
16077+
<work-title>Piano-Repeat</work-title>
16078+
</work>
16079+
<part-list>
16080+
<score-part id="P1">
16081+
</score-part>
16082+
</part-list>
16083+
<part id="P1">
16084+
<measure number="1">
16085+
<barline location="left">
16086+
<bar-style>heavy-light</bar-style>
16087+
<repeat direction="forward"/>
16088+
</barline>
16089+
<attributes>
16090+
<divisions>1</divisions>
16091+
<key>
16092+
<fifths>0</fifths>
16093+
</key>
16094+
<time symbol="common">
16095+
<beats>4</beats>
16096+
<beat-type>4</beat-type>
16097+
</time>
16098+
<staves>2</staves>
16099+
<clef number="1">
16100+
<sign>G</sign>
16101+
<line>2</line>
16102+
</clef>
16103+
<clef number="2">
16104+
<sign>G</sign>
16105+
<line>2</line>
16106+
</clef>
16107+
</attributes>
16108+
<note>
16109+
<pitch>
16110+
<step>G</step>
16111+
<octave>4</octave>
16112+
</pitch>
16113+
<duration>4</duration>
16114+
<type>whole</type>
16115+
<staff>1</staff>
16116+
</note>
16117+
<backup>
16118+
<duration>4</duration>
16119+
</backup>
16120+
<note>
16121+
<pitch>
16122+
<step>G</step>
16123+
<octave>4</octave>
16124+
</pitch>
16125+
<duration>4</duration>
16126+
<type>whole</type>
16127+
<staff>2</staff>
16128+
</note>
16129+
</measure>
16130+
<measure number="2">
16131+
<barline location="left">
16132+
<ending number="1" type="start">1</ending>
16133+
</barline>
16134+
<note>
16135+
<pitch>
16136+
<step>A</step>
16137+
<octave>4</octave>
16138+
</pitch>
16139+
<duration>4</duration>
16140+
<type>whole</type>
16141+
<staff>1</staff>
16142+
</note>
16143+
<backup>
16144+
<duration>4</duration>
16145+
</backup>
16146+
<note>
16147+
<pitch>
16148+
<step>A</step>
16149+
<octave>4</octave>
16150+
</pitch>
16151+
<duration>4</duration>
16152+
<type>whole</type>
16153+
<staff>2</staff>
16154+
</note>
16155+
<barline location="right">
16156+
<bar-style>light-heavy</bar-style>
16157+
<ending number="1" type="stop"/>
16158+
<repeat direction="backward"/>
16159+
</barline>
16160+
</measure>
16161+
<measure number="3">
16162+
<barline location="left">
16163+
<ending number="2" type="start">2</ending>
16164+
</barline>
16165+
<note>
16166+
<pitch>
16167+
<step>B</step>
16168+
<octave>4</octave>
16169+
</pitch>
16170+
<duration>4</duration>
16171+
<type>whole</type>
16172+
<staff>1</staff>
16173+
</note>
16174+
<backup>
16175+
<duration>4</duration>
16176+
</backup>
16177+
<note>
16178+
<pitch>
16179+
<step>B</step>
16180+
<octave>4</octave>
16181+
</pitch>
16182+
<duration>4</duration>
16183+
<type>whole</type>
16184+
<staff>2</staff>
16185+
</note>
16186+
<barline location="right">
16187+
<bar-style>light-heavy</bar-style>
16188+
<ending number="2" type="discontinue"/>
16189+
</barline>
16190+
</measure>
16191+
</part>
16192+
</score-partwise>
16193+
'''
16194+
16195+
16196+
1607016197
# ------------------------------------------------------------------------------
1607116198
# define all strings for access
1607216199

@@ -16086,16 +16213,14 @@
1608616213
ALL = [
1608716214
chantQuemQueritis, mozartTrioK581Excerpt, schumannOp48No1,
1608816215
binchoisMagnificat, edgefield82b, tabTest,
16216+
pianoRepeatEndings,
1608916217
]
1609016218

1609116219

16220+
@common.deprecated('v10', 'This has never been developed beyond one file')
1609216221
def get(contentRequest):
1609316222
'''
16094-
Get test material by type of content
16095-
16096-
>>> from music21.musicxml.testFiles import get
16097-
16098-
>>> a = get('lyrics')
16223+
Get test material by type of content -- Deprecated - to be removed in v10
1609916224
'''
1610016225
if contentRequest in ['lyrics']:
1610116226
return chantQuemQueritis

music21/musicxml/test_xmlToM21.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1670,6 +1670,52 @@ def testAdjustTimeAttributesFromMeasure(self):
16701670
pp.adjustTimeAttributesFromMeasure(m)
16711671
self.assertEqual(pp.lastMeasureOffset, 25.0)
16721672

1673+
def testPianoStaffWithRepeatEndings(self):
1674+
from music21 import converter
1675+
from music21.musicxml import testFiles
1676+
1677+
s = converter.parse(testFiles.pianoRepeatEndings)
1678+
self.assertEqual(len(s.parts), 2)
1679+
self.assertTrue(s[layout.StaffGroup])
1680+
sg = s[layout.StaffGroup].first()
1681+
1682+
for p_num in (0, 1):
1683+
p = s.parts[p_num]
1684+
self.assertIn(p, sg)
1685+
self.assertEqual(len(p[note.Note]), 3)
1686+
self.assertEqual(len(p[stream.Measure]), 3)
1687+
m1, m2, m3 = p[stream.Measure]
1688+
self.assertTrue(m1[bar.Repeat])
1689+
repeat_start = m1[bar.Repeat].first()
1690+
self.assertEqual(repeat_start.direction, 'start')
1691+
self.assertEqual(repeat_start.offset, 0.0)
1692+
self.assertTrue(m2[bar.Repeat])
1693+
repeat_end = m2[bar.Repeat].first()
1694+
self.assertEqual(repeat_end.direction, 'end')
1695+
self.assertEqual(repeat_end.offset, 4.0)
1696+
self.assertFalse(m3[bar.Repeat])
1697+
self.assertTrue(m3[bar.Barline].getElementsByOffset(4.0))
1698+
final_bar = m3[bar.Barline].getElementsByOffset(4.0, 4.0).first()
1699+
self.assertEqual(final_bar.type, 'final')
1700+
1701+
# formerly mistake
1702+
repeat_bracket_iterator = p[spanner.RepeatBracket]
1703+
self.assertTrue(repeat_bracket_iterator, f'Part {p_num}:{p} has no RepeatBrackets')
1704+
self.assertEqual(len(repeat_bracket_iterator), 2)
1705+
rb1, rb2 = repeat_bracket_iterator
1706+
if rb1.number == 2:
1707+
# these should iterate as 1, 2 but there's no guarantee
1708+
rb1, rb2 = rb2, rb1
1709+
self.assertEqual(list(rb1), [m2])
1710+
self.assertEqual(list(rb2), [m3])
1711+
1712+
p_notes = [n.name for n in p[note.Note]]
1713+
self.assertEqual(p_notes, ['G', 'A', 'B'])
1714+
1715+
p_expanded = repeat.Expander[stream.Part](p).process()
1716+
p_notes2 = [n.name for n in p_expanded[note.Note]]
1717+
self.assertEqual(p_notes2, ['G', 'A', 'G', 'B'])
1718+
16731719

16741720
if __name__ == '__main__':
16751721
import music21

music21/musicxml/xmlToM21.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1475,6 +1475,7 @@ def __init__(self,
14751475

14761476
self.atSoundingPitch = True
14771477

1478+
# a list of StaffReferenceType dicts -- one for each Measure parsed by MeasureParser
14781479
self.staffReferenceList: list[StaffReferenceType] = []
14791480

14801481
self.lastTimeSignature: meter.TimeSignature|None = None
@@ -1826,14 +1827,19 @@ def separateOutPartStaves(self) -> list[stream.PartStaff]:
18261827
'TempoIndication',
18271828
'TimeSignature',
18281829
]
1830+
# spanners generally appear only on the first staff.
1831+
# RepeatBracket spanners, however, need to appear on every staff.
1832+
EXEMPT_FROM_REMOVE = frozenset(
1833+
['RepeatBracket'],
1834+
)
18291835

18301836
uniqueStaffKeys: list[int] = self._getUniqueStaffKeys()
18311837
partStaves: list[stream.PartStaff] = []
18321838
appendedElementIds: set[int] = set() # id is id(el) not el.id
18331839

18341840
def copy_into_partStaff(source: stream.Stream,
18351841
target: stream.Stream,
1836-
omitTheseElementIds: set[int]):
1842+
omitTheseElementIds: set[int]) -> None:
18371843
elementIterator = source.getElementsByClass(STAFF_SPECIFIC_CLASSES)
18381844
elementIterator.restoreActiveSites = False
18391845
for sourceElem in elementIterator:
@@ -1858,7 +1864,9 @@ def copy_into_partStaff(source: stream.Stream,
18581864
removeClasses = STAFF_SPECIFIC_CLASSES[:]
18591865
if staffIndex != 0: # spanners only on the first staff.
18601866
removeClasses.append('Spanner')
1861-
newPartStaff = self.stream.template(removeClasses=removeClasses, fillWithRests=False)
1867+
newPartStaff = self.stream.template(removeClasses=removeClasses,
1868+
fillWithRests=False,
1869+
exemptFromRemove=EXEMPT_FROM_REMOVE)
18621870
partStaffId = f'{self.partId}-Staff{staffKey}'
18631871
newPartStaff.id = partStaffId
18641872
# set group for components (recurse?)
@@ -1867,7 +1875,11 @@ def copy_into_partStaff(source: stream.Stream,
18671875
partStaves.append(newPartStaff)
18681876
self.parent.m21PartObjectsById[partStaffId] = newPartStaff
18691877
elementsIdsNotToGoInThisStaff: set[int] = set()
1878+
1879+
# iterate over the StaffReferenceType dicts, one for each measure.
18701880
for staffReference in self.staffReferenceList:
1881+
# this is a list of Music21Objects that should not go into the staff
1882+
# called by staffKey.
18711883
excludeOneMeasure = self._getStaffExclude(
18721884
staffReference,
18731885
staffKey
@@ -1905,7 +1917,8 @@ def _getStaffExclude(
19051917
targetKey: int
19061918
) -> list[base.Music21Object]:
19071919
'''
1908-
Given a staff reference dictionary, remove and combine in a list all elements that
1920+
Given a staff reference dictionary (for a single measure),
1921+
remove and combine in a list all elements that
19091922
are NOT part of the given targetKey. Thus, return a list of all entries to remove.
19101923
It keeps those elements under the staff key None (common to all) and
19111924
those under given key. This then is the list of all elements that should be deleted.

0 commit comments

Comments
 (0)