Source code for music21.musicxml.test_m21ToXml

from __future__ import annotations

import copy
import difflib
import fractions
import io
import re
import unittest
from xml.etree.ElementTree import (
    ElementTree, fromstring as et_fromstring
)

from music21 import articulations
from music21 import chord
from music21 import common
from music21 import converter
from music21 import corpus
from music21 import defaults
from music21 import duration
from music21 import dynamics
from music21 import expressions
from music21 import harmony
from music21 import instrument
from music21 import layout
from music21 import meter
from music21 import note
from music21 import repeat
from music21 import spanner
from music21 import stream
from music21 import style
from music21 import tempo

from music21.musicxml import helpers
from music21.musicxml import testPrimitive
from music21.musicxml.m21ToXml import (
    GeneralObjectExporter, ScoreExporter,
    MusicXMLWarning, MusicXMLExportException
)
from music21.musicxml.xmlToM21 import MeasureParser

[docs] def stripInnerSpaces(txt: str): ''' Collapse all whitespace (say, in some XML) to a single space, for ease of comparison. ''' return re.sub(r'\s+', ' ', txt)
class Test(unittest.TestCase): def getXml(self, obj): gex = GeneralObjectExporter() bytesOut = gex.parse(obj) bytesOutUnicode = bytesOut.decode('utf-8') return bytesOutUnicode def getET(self, obj, makeNotation=True): ''' Return a <score-partwise> ElementTree. ''' if makeNotation: gex = GeneralObjectExporter() obj = gex.fromGeneralObject(obj) SX = ScoreExporter(obj) SX.makeNotation = makeNotation mxScore = SX.parse() helpers.indent(mxScore) return mxScore def testExceptionMessage(self): s = stream.Score() p = stream.Part() p.partName = 'Offstage Trumpet' p.insert(note.Note(quarterLength=(4 / 2048))) s.insert(p) msg = 'In part (Offstage Trumpet), measure (1): ' msg += 'Cannot convert "2048th" duration to MusicXML (too short).' with self.assertRaises(MusicXMLExportException) as error: s.write() self.assertEqual(str(error.exception), msg) def testSpannersWrite(self): p = converter.parse("tinynotation: 4/4 c4 d e f g a b c' b a g2") listNotes = list(p.recurse().notes) c = listNotes[0] d = listNotes[1] sl1 = spanner.Slur([c, d]) p.insert(0.0, sl1) f = listNotes[3] g = listNotes[4] a = listNotes[5] sl2 = spanner.Slur([f, g, a]) p.insert(0.0, sl2) c2 = listNotes[6] g2 = listNotes[-1] sl3 = spanner.Slur([c2, g2]) p.insert(0.0, sl3) self.assertEqual(self.getXml(p).count('<slur '), 6) def testSpannerAnchors(self): score = stream.Score() part = stream.Part() score.insert(0, part) measure = stream.Measure() part.insert(0, measure) voice = stream.Voice() measure.insert(0, voice) n = note.Note('C', quarterLength=4) sa1 = spanner.SpannerAnchor() sa2 = spanner.SpannerAnchor() voice.insert(0, n) voice.insert(2, sa1) voice.insert(4, sa2) cresc = dynamics.Crescendo(n, sa1) # cresc from n to sa1 dim = dynamics.Diminuendo(sa1, sa2) # dim from sa1 to sa2 score.append((cresc, dim)) xmlOut = self.getXml(score) self.assertIn( stripInnerSpaces( '''<measure implicit="no" number="0"> <attributes> <divisions>10080</divisions> </attributes> <note> <pitch> <step>C</step> <octave>4</octave> </pitch> <duration>40320</duration> <type>whole</type> </note> <backup> <duration>40320</duration> </backup> <direction placement="below"> <direction-type> <wedge number="1" spread="0" type="crescendo" /> </direction-type> </direction> <forward> <duration>20160</duration> </forward> <direction placement="below"> <direction-type> <wedge number="2" spread="15" type="diminuendo" /> </direction-type> </direction> <direction placement="below"> <direction-type> <wedge number="1" spread="15" type="stop" /> </direction-type> </direction> <forward> <duration>20160</duration> </forward> <direction placement="below"> <direction-type> <wedge number="2" spread="0" type="stop" /> </direction-type> </direction> </measure>'''), stripInnerSpaces(xmlOut) ) def testSpannersWritePartStaffs(self): ''' Test that spanners are gathered on the PartStaffs that need them. Multi-staff instruments are separated on import into distinct PartStaff objects, where usually all the spanners will remain on the first object. ''' xmlDir = common.getSourceFilePath() / 'musicxml' / 'lilypondTestSuite' s = converter.parse(xmlDir / '43e-Multistaff-ClefDynamics.xml') # StaffGroup spanner stored on the score self.assertEqual(len(s.spanners), 1) self.assertIsInstance(s.spanners[0], layout.StaffGroup) # Crescendo in LH actually stored in first PartStaff object self.assertEqual(len(s.parts[0].spanners), 1) self.assertEqual(len(s.parts[1].spanners), 0) self.assertIsInstance(s.parts[0].spanners[0], dynamics.Crescendo) # Will it be found by coreGatherMissingSpanners without being inserted? s.makeNotation(inPlace=True) self.assertEqual(len(s.parts[1].spanners), 0) # and written after the backup tag, i.e. on the LH? xmlOut = self.getXml(s) xmlAfterFirstBackup = xmlOut.split('</backup>\n')[1] self.assertIn( stripInnerSpaces( ''' <direction placement="below"> <direction-type> <wedge number="1" spread="0" type="crescendo" /> </direction-type> <staff>2</staff> </direction>'''), stripInnerSpaces(xmlAfterFirstBackup) ) def testLowVoiceNumbers(self): n = note.Note() v1 = stream.Voice([n]) m = stream.Measure([v1]) # Unnecessary voice is removed by makeNotation xmlOut = self.getXml(m) self.assertNotIn('<voice>1</voice>', xmlOut) n2 = note.Note() v2 = stream.Voice([n2]) m.insert(0, v2) xmlOut = self.getXml(m) self.assertIn('<voice>1</voice>', xmlOut) self.assertIn('<voice>2</voice>', xmlOut) v1.id = 234 xmlOut = self.getXml(m) self.assertIn('<voice>234</voice>', xmlOut) self.assertIn('<voice>235</voice>', xmlOut) v2.id = 'hello' xmlOut = self.getXml(m) self.assertIn('<voice>hello</voice>', xmlOut) def testVoiceNumberOffsetsThreeStaffsInGroup(self): n1_1 = note.Note() v1_1 = stream.Voice([n1_1]) m1_1 = stream.Measure([v1_1]) n1_2 = note.Note() v1_2 = stream.Voice([n1_2]) m1_2 = stream.Measure([v1_2]) ps1 = stream.PartStaff([m1_1, m1_2]) n2_1 = note.Note() v2_1 = stream.Voice([n2_1]) m2_1 = stream.Measure([v2_1]) n2_2 = note.Note() v2_2 = stream.Voice([n2_2]) m2_2 = stream.Measure([v2_2]) ps2 = stream.PartStaff([m2_1, m2_2]) n3_1 = note.Note() v3_1 = stream.Voice([n3_1]) m3_1 = stream.Measure([v3_1]) n3_2 = note.Note() v3_2 = stream.Voice([n3_2]) m3_2 = stream.Measure([v3_2]) ps3 = stream.PartStaff([m3_1, m3_2]) s = stream.Score([ps1, ps2, ps3]) staffGroup = layout.StaffGroup([ps1, ps2, ps3]) s.insert(0, staffGroup) tree = self.getET(s, makeNotation=False) # helpers.dump(tree) mxNotes = tree.findall('part/measure/note') for mxNote in mxNotes: voice = mxNote.find('voice') staff = mxNote.find('staff') # Because there is one voice per staff/measure, voicenum == staffnum self.assertEqual(voice.text, staff.text) def testCompositeLyrics(self): xmlDir = common.getSourceFilePath() / 'musicxml' / 'lilypondTestSuite' fp = xmlDir / '61l-Lyrics-Elisions-Syllables.xml' s = converter.parse(fp) n1 = s[note.NotRest].first() xmlOut = self.getXml(n1) self.assertIn('<lyric name="1" number="1">', xmlOut) self.assertIn('<syllabic>begin</syllabic>', xmlOut) self.assertIn('<text>a</text>', xmlOut) tree = self.getET(s) mxLyrics = tree.findall('part/measure/note/lyric') ly0 = mxLyrics[0] self.assertEqual(ly0.get('number'), '1') self.assertEqual(len(ly0), 2) self.assertEqual(ly0[0].tag, 'syllabic') self.assertEqual(ly0[1].tag, 'text') # contents already checked above ly1 = mxLyrics[1] self.assertEqual(len(ly1), 5) tags = [child.tag for child in ly1] self.assertEqual(tags, ['syllabic', 'text', 'elision', 'syllabic', 'text']) self.assertEqual(ly1.find('elision').text, ' ') self.assertEqual(ly1.findall('syllabic')[0].text, 'middle') self.assertEqual(ly1.findall('text')[0].text, 'b') self.assertEqual(ly1.findall('syllabic')[1].text, 'middle') self.assertEqual(ly1.findall('text')[1].text, 'c') ly2 = mxLyrics[2] self.assertEqual(len(ly2), 5) tags = [child.tag for child in ly2] self.assertEqual(tags, ['syllabic', 'text', 'elision', 'syllabic', 'text']) self.assertIsNone(ly2.find('elision').text) self.assertEqual(ly2.findall('syllabic')[0].text, 'middle') self.assertEqual(ly2.findall('text')[0].text, 'd') self.assertEqual(ly2.findall('syllabic')[1].text, 'end') self.assertEqual(ly2.findall('text')[1].text, 'e') def testExportChordSymbolWithInversions(self): ''' This test checks for the issue in 1756 where a chord symbol imported with both inversion and bass would not have them both set. There were multiple bugs fixed, so important to check. ''' explicitFm6 = harmony.ChordSymbol(root='F', bass='A-', inversion=1, kind='minor') et = self.getET(explicitFm6) harmonyEls = et.findall('part/measure/harmony') self.assertEqual(len(harmonyEls), 1) harmonyEl = harmonyEls[0] # helpers.dump(harmonyEl) root = harmonyEl.find('root') rootStep = root.find('root-step') self.assertEqual(rootStep.text, 'F') self.assertEqual(harmonyEl.find('kind').text, 'minor') self.assertEqual(harmonyEl.find('inversion').text, '1') self.assertEqual(harmonyEl.find('bass/bass-step').text, 'A') self.assertEqual(harmonyEl.find('bass/bass-alter').text, '-1') # test round trip mp = MeasureParser() cs = mp.xmlToChordSymbol(harmonyEl) self.assertEqual(explicitFm6.bass(find=False), cs.bass(find=False)) self.assertEqual(explicitFm6.root(find=False), cs.root(find=False)) self.assertEqual(explicitFm6.inversion(), cs.inversion()) def testExportChordSymbolPlacement(self): cs = harmony.ChordSymbol('C') cs.placement = 'below' et = self.getET(cs) harmonyEl = et.find('part/measure/harmony') self.assertEqual(harmonyEl.get('placement'), 'below') cs2 = harmony.ChordSymbol('G') et2 = self.getET(cs2) harmonyEl2 = et2.find('part/measure/harmony') self.assertIsNone(harmonyEl2.get('placement')) def testExportNC(self): s = stream.Score() p = stream.Part() m = stream.Measure() m.append(harmony.ChordSymbol('C')) m.repeatAppend(note.Note('C'), 4) p.append(m) m = stream.Measure() m.append(harmony.NoChord()) m.repeatAppend(note.Note('C'), 2) m.append(harmony.ChordSymbol('C')) m.repeatAppend(note.Note('C'), 2) p.append(m) s.append(p) self.assertEqual(3, self.getXml(s).count('<harmony')) self.assertEqual(1, self.getXml(s).count('<kind text="N.C.">none</kind>')) self.assertEqual(1, self.getXml(s).count('<root-step text="">')) s = stream.Score() p = stream.Part() m = stream.Measure() m.append(harmony.NoChord()) m.repeatAppend(note.Note('C'), 2) m.append(harmony.ChordSymbol('C')) m.repeatAppend(note.Note('C'), 2) p.append(m) m = stream.Measure() m.append(harmony.NoChord('No Chord')) m.repeatAppend(note.Note('C'), 2) m.append(harmony.ChordSymbol('C')) m.repeatAppend(note.Note('C'), 2) p.append(m) s.append(p) self.assertEqual(1, self.getXml(s).count('<kind text="N.C.">none</kind>')) self.assertEqual(1, self.getXml(s).count('<kind text="No Chord">none</kind>')) def testSetPartsAndRefStreamMeasure(self): p = converter.parse('tinynotation: 4/4 c1 d1') sc = stream.Score([p]) sx = ScoreExporter(sc) # substreams are measures sx.setPartsAndRefStream() measuresAtOffsetZero = [m for m in p if m.offset == 0] self.assertSequenceEqual(measuresAtOffsetZero, p.elements[:1]) def testFromScoreNoParts(self): ''' Badly nested streams should warn but output no gaps. ''' s = stream.Score() s.append(meter.TimeSignature('1/4')) s.append(note.Note()) s.append(note.Note()) gex = GeneralObjectExporter(s) with self.assertWarns(MusicXMLWarning) as cm: tree = et_fromstring(gex.parse().decode('utf-8')) self.assertIn(repr(s).split(' 0x')[0], str(cm.warning)) self.assertIn(' is not well-formed; see isWellFormedNotation()', str(cm.warning)) # The original score with its original address should not # be found in the message because makeNotation=True makes a copy self.assertNotIn(repr(s), str(cm.warning)) # Assert no gaps in stream self.assertSequenceEqual(tree.findall('.//forward'), []) def testFromScoreNoMeasures(self): s = stream.Score() s.append(note.Note()) scExporter = ScoreExporter(s) tree = scExporter.parse() # Measures should have been made self.assertIsNotNone(tree.find('.//measure')) def testFromSoundingPitch(self): ''' A score with mixed sounding and written parts. ''' m = stream.Measure([instrument.Clarinet(), note.Note('C')]) p1 = stream.Part(m) p1.atSoundingPitch = True p2 = stream.Part(stream.Measure([instrument.Bassoon(), note.Note()])) s = stream.Score([p1, p2]) self.assertEqual(s.atSoundingPitch, 'unknown') gex = GeneralObjectExporter(s) root = et_fromstring(gex.parse().decode('utf-8')) self.assertEqual(len(root.findall('.//transpose')), 1) self.assertEqual(root.find('.//step').text, 'D') s.atSoundingPitch = True gex = GeneralObjectExporter(s) root = et_fromstring(gex.parse().decode('utf-8')) self.assertEqual(len(root.findall('.//transpose')), 1) self.assertEqual(root.find('.//step').text, 'D') def testMultipleInstruments(self): ''' This is a score for two woodwind players both doubling on flute and oboe. They both switch to flute and then back to oboe. There are six m21 instruments to represent this, but the <score-instrument> tags need just four, since no musicXML <part> needs two oboes in it, etc., unless there is a patch change/MIDI instrument change. ''' p1 = stream.Part([ stream.Measure([instrument.Oboe(), note.Note(type='whole')]), stream.Measure([instrument.Flute(), note.Note(type='whole')]), stream.Measure([instrument.Oboe(), note.Note(type='whole')]), ]) p2 = stream.Part([ stream.Measure([instrument.Oboe(), note.Note(type='whole')]), stream.Measure([instrument.Flute(), note.Note(type='whole')]), stream.Measure([instrument.Oboe(), note.Note(type='whole')]), ]) s = stream.Score([p1, p2]) scEx = ScoreExporter(s) tree = scEx.parse() self.assertEqual(len(tree.findall('.//score-instrument')), 4) self.assertEqual(len(tree.findall('.//measure/note/instrument')), 6) self.assertEqual(tree.find('.//score-instrument').get('id'), tree.find('.//measure/note/instrument').get('id')) self.assertNotEqual(tree.find('.//score-instrument').get('id'), tree.findall('.//measure/note/instrument')[-1].get('id')) def testMultipleInstrumentsPiano(self): ps1 = stream.PartStaff([ stream.Measure([instrument.ElectricPiano(), note.Note(type='whole')]), stream.Measure([instrument.ElectricOrgan(), note.Note(type='whole')]), stream.Measure([instrument.Piano(), note.Note(type='whole')]), ]) ps2 = stream.PartStaff([ stream.Measure([instrument.Vocalist(), note.Note(type='whole')]), stream.Measure([note.Note(type='whole')]), stream.Measure([note.Note(type='whole')]), ]) sg = layout.StaffGroup([ps1, ps2]) s = stream.Score([ps1, ps2, sg]) scEx = ScoreExporter(s) tree = scEx.parse() self.assertEqual( # allow for non-deterministic ordering: caused by instrument.deduplicate() (?) {el.text for el in tree.findall('.//instrument-name')}, {'Electric Piano', 'Voice', 'Electric Organ', 'Piano'} ) self.assertEqual(len(tree.findall('.//measure/note/instrument')), 6) def testExportIdOfMeasure(self): ''' Check that id of measure are exported as attributes of the <measure> eelement in MusicXML ''' measure = stream.Measure(id='test', number=1) measure.append(note.Note(type='whole')) s = stream.Score(measure) scEx = ScoreExporter(s) tree = scEx.parse() for measure_xml in tree.findall('.//measure'): self.assertTrue('id' in measure_xml.attrib) self.assertEqual(measure_xml.get('id'), 'test') def testMidiInstrumentNoName(self): i = instrument.Instrument() i.midiProgram = 42 s = converter.parse('tinyNotation: c1') s.measure(1).insert(i) sc = stream.Score(s) scExporter = ScoreExporter(sc) tree = scExporter.parse() mxScoreInstrument = tree.findall('.//score-instrument')[0] mxMidiInstrument = tree.findall('.//midi-instrument')[0] self.assertEqual(mxScoreInstrument.get('id'), mxMidiInstrument.get('id')) def testOrnamentAccidentals(self): s = converter.parse(testPrimitive.notations32a) x = self.getET(s) accidentalMarks = x.findall('.//ornaments/accidental-mark') self.assertEqual( [accMark.text for accMark in accidentalMarks], ['natural', 'sharp', 'three-quarters-flat'] ) def testMultiDigitEndingsWrite(self): # Relevant barlines: # Measure 2, left barline: <ending number="1,2" type="start"/> # Measure 2, right barline: <ending number="1,2" type="stop"/> # Measure 3, left barline: <ending number="3" type="start"/> # Measure 3, right barline: <ending number="3" type="stop"/> s = converter.parse(testPrimitive.multiDigitEnding) x = self.getET(s) endings = x.findall('.//ending') self.assertEqual([e.get('number') for e in endings], ['1,2', '1,2', '3', '3']) # Check templates also template = s.template() template.makeNotation(inPlace=True) # not essential, but since getET() skips this x = self.getET(template) endings = x.findall('.//ending') self.assertEqual([e.get('number') for e in endings], ['1,2', '1,2', '3', '3']) # m21 represents lack of bracket numbers as 0; musicxml uses '' s.parts[0].getElementsByClass(spanner.RepeatBracket).first().number = 0 x = self.getET(s) endings = x.findall('.//ending') self.assertEqual([e.get('number') for e in endings], ['', '', '3', '3']) def testMultiMeasureEndingsWrite(self): # Relevant barlines: # Measure 1, left barline: no ending # Measure 1, right barline: no ending # Measure 2, left barline: <ending number="1" type="start"/> # Measure 2, right barline: no ending # Measure 3, left barline: no ending # Measure 3, right barline: no ending # Measure 4, left barline: no ending # Measure 4, right barline: <ending number="1" type="stop"/> # Measure 5, left barline: <ending number="2" type="start"/> # Measure 5, right barline: no ending # Measure 6, left barline: no ending # Measure 6, right barline: no ending # Measure 7, left barline: no ending # Measure 7, right barline: <ending number="2" type="stop"/> # Measure 8, left barline: no ending # Measure 8, right barline: no ending s = converter.parse(testPrimitive.multiMeasureEnding) x = self.getET(s) endings = x.findall('.//ending') self.assertEqual([e.get('number') for e in endings], ['1', '1', '2', '2']) expectedEndings = { # key = measure number, value = list(leftEndingType, rightEndingType) '1': [None, None], # measure before the endings '2': ['start', None], # first measure of ending 1 '3': [None, None], # second measure of ending 1 '4': [None, 'stop'], # last measure of ending 1 '5': ['start', None], # first measure of ending 2 '6': [None, None], # second measure of ending 2 '7': [None, 'stop'], # last measure of ending 2 '8': [None, None] # measure after the endings } measures = x.findall('.//measure') self.assertEqual(len(measures), 8) for measure in measures: measNumber = measure.get('number') with self.subTest(measureNumber=measNumber): expectLeftBarline = bool(expectedEndings[measNumber][0] is not None) expectRightBarline = bool(expectedEndings[measNumber][1] is not None) gotLeftBarline = False gotRightBarline = False barlines = measure.findall('.//barline') for i, barline in enumerate(barlines): if barline.get('location') == 'left': gotLeftBarline = True leftEndingType = None leftEnding = barline.find('ending') if leftEnding is not None: leftEndingType = leftEnding.get('type') self.assertEqual(leftEndingType, expectedEndings[measNumber][0]) elif barline.get('location') == 'right': gotRightBarline = True rightEndingType = None rightEnding = barline.find('ending') if rightEnding is not None: rightEndingType = rightEnding.get('type') self.assertEqual(rightEndingType, expectedEndings[measNumber][1]) if expectLeftBarline: self.assertTrue(gotLeftBarline) if expectRightBarline: self.assertTrue(gotRightBarline) def testTextExpressionOffset(self): ''' Transfer element offset after calling getTextExpression(). ''' # https://github.com/cuthbertLab/music21/issues/624 s = converter.parse('tinynotation: 4/4 c1') c = repeat.Coda() c.useSymbol = False f = repeat.Fine() mm = tempo.MetronomeMark(text='Langsam') mm.placement = 'above' s.measure(1).storeAtEnd([c, f, mm]) tree = self.getET(s) for direction in tree.findall('.//direction'): self.assertIsNone(direction.find('offset')) # Also check position mxDirection = tree.find('part/measure/direction') self.assertEqual(mxDirection.get('placement'), 'above') def testFullMeasureRest(self): s = converter.parse('tinynotation: 9/8 r1') r = s[note.Rest].first() r.quarterLength = 4.5 self.assertEqual(r.fullMeasure, 'auto') tree = self.getET(s) # Previously, this 4.5QL rest with a duration.type 'complex' # was split on export into 4.0QL and 0.5QL self.assertEqual(len(tree.findall('.//rest')), 1) rest = tree.find('.//rest') self.assertEqual(rest.get('measure'), 'yes') self.assertIsNone(tree.find('.//note/type')) def testArticulationSpecialCases(self): n = note.Note() a = articulations.StringIndication() n.articulations.append(a) h = articulations.HammerOn() # Appending a spanner such as HammerOn to the articulations # array is contrary to the documentation and contrary to the # behavior of the musicxml importer, but we're testing it here # anyway to just ensure that *IF* a user decides to do this themselves, # we don't create a superfluous <other-technical> tag on export. n.articulations.append(h) # Legal values for StringIndication begin at 1 self.assertEqual(a.number, 0) # Use GEX to go through wellformed object conversion gex = GeneralObjectExporter(n) tree = et_fromstring(gex.parse().decode('utf-8')) self.assertIsNone(tree.find('.//string')) self.assertIsNone(tree.find('.//other-technical')) def testMeasurePadding(self): s = stream.Score([converter.parse('tinyNotation: 4/4 c4')]) s[stream.Measure].first().paddingLeft = 2.0 s[stream.Measure].first().paddingRight = 1.0 # workaround until getET() helper starts calling fromGeneralObject s = GeneralObjectExporter().fromGeneralObject(s) tree = self.getET(s) self.assertEqual(len(tree.findall('.//rest')), 0) s[stream.Measure].first().paddingLeft = 1.0 # workaround until getET() helper starts calling fromGeneralObject s = GeneralObjectExporter().fromGeneralObject(s) tree = self.getET(s) self.assertEqual(len(tree.findall('.//rest')), 1) def test_instrumentDoesNotCreateForward(self): ''' Instrument tags were causing forward motion in some cases. From Chapter 14, Key Signatures This is a transposed score. Instruments were being extended in duration in the toSoundingPitch and not having their durations restored afterwards leading to Instrument objects being split if the duration was complex ''' alto = corpus.parse('bach/bwv57.8').parts['#Alto'] alto.measure(7).timeSignature = meter.TimeSignature('6/8') newAlto = alto.flatten().getElementsNotOfClass(meter.TimeSignature).stream() newAlto.insert(0, meter.TimeSignature('2/4')) newAlto.makeMeasures(inPlace=True) newAltoFixed = newAlto.makeNotation() tree = self.getET(newAltoFixed) self.assertTrue(tree.findall('.//note')) self.assertFalse(tree.findall('.//forward')) def test_writeFromSpannerAnchorsGetsMeasureEndOffsetRight(self): ''' Write to MusicXML from a Measure containing SpannerAnchors was not positioning the current time offset correctly before starting the next written measure. Now the next measure is positioned at the correct offset. ''' m1 = stream.Measure() m1.append(note.Note()) m1.append(note.Note()) m1.append(note.Note()) m1.append(note.Note()) cresc = dynamics.Crescendo() startAnchor = spanner.SpannerAnchor() endAnchor = spanner.SpannerAnchor() m1.insert(0.5, startAnchor) m1.insert(1.5, endAnchor) cresc.addSpannedElements(startAnchor, endAnchor) m1.append(cresc) p = stream.Part() p.append(m1) s = stream.Score() s.append(p) # write to MusicXML tree = self.getET(s) # walk all the durations (notes, forwards, backups) and make sure they add up # to where the end of the measure should be (4.0ql) measEl = None divisionsEl = None for el in tree.iter(): if el.tag == 'measure': measEl = el for el in measEl.iter(): if el.tag == 'divisions': divisionsEl = el break break self.assertIsNotNone(measEl) self.assertIsNotNone(divisionsEl) divisionsInt = int(divisionsEl.text) currOffsetQL = 0. for el in measEl.findall('*'): dur = el.find('duration') if dur is not None: durInt = int(dur.text) durQL = common.opFrac(fractions.Fraction(durInt, divisionsInt)) if el.tag == 'backup': currOffsetQL = common.opFrac(currOffsetQL - durQL) else: currOffsetQL = common.opFrac(currOffsetQL + durQL) self.assertEqual(currOffsetQL, 4.0) def testOutOfBoundsExpressionDoesNotCreateForward(self): ''' A metronome mark at an offset exceeding the bar duration was causing <forward> tags, i.e. hidden rests. Prefer <offset> instead. ''' m = stream.Measure() m.append(meter.TimeSignature('1/4')) m.append(note.Rest()) m.insert(2, tempo.MetronomeMark('slow', 40)) p = stream.Part([m]) s = stream.Score([p]) tree = self.getET(s, makeNotation=False) self.assertFalse(tree.findall('.//forward')) self.assertEqual( int(tree.findall('.//direction/offset')[0].text), defaults.divisionsPerQuarter) def testPedals(self): expectedResults1 = ( { 'type': 'start', 'line': 'yes', 'number': '1', }, { 'type': 'change', 'line': 'yes', }, { 'type': 'discontinue', 'line': 'yes', }, { 'type': 'resume', 'line': 'yes', }, { 'type': 'change', 'line': 'yes', }, { 'type': 'stop', 'line': 'yes', 'number': '1', }, ) s = converter.parse(testPrimitive.pedalLines) x = self.getET(s) mxPart = x.find('part') for i, mxPedal in enumerate(mxPart.findall('.//pedal')): with self.subTest(pedal_index=i): for k in expectedResults1[i]: self.assertEqual(mxPedal.get(k, ''), expectedResults1[i][k]) startIdx = len(expectedResults1) expectedResults2 = ( { 'type': 'start', 'sign': 'yes', 'number': '1', }, { 'type': 'resume', 'line': 'yes', }, { 'type': 'change', 'line': 'yes', }, { 'type': 'discontinue', 'line': 'yes', }, { 'type': 'resume', 'line': 'yes', }, { 'type': 'change', 'line': 'yes', }, { 'type': 'stop', 'line': 'yes', 'number': '1', }, ) s = converter.parse(testPrimitive.pedalSymLines) x = self.getET(s) mxPart = x.find('part') for i, mxPedal in enumerate(mxPart.findall('.//pedal')): with self.subTest(pedal_index=startIdx + i): for k in expectedResults2[i]: self.assertEqual(mxPedal.get(k, ''), expectedResults2[i][k]) def testArpeggios(self): expectedResults = ( 'arpeggiate', 'arpeggiate', 'arpeggiate', 'arpeggiate up', 'arpeggiate up', 'arpeggiate up', 'arpeggiate', 'arpeggiate', 'arpeggiate', 'arpeggiate down', 'arpeggiate down', 'arpeggiate down', 'arpeggiate', 'arpeggiate', 'arpeggiate', 'non-arpeggiate bottom', '', 'non-arpeggiate top', 'arpeggiate', 'arpeggiate', 'arpeggiate', ) s = converter.parse(testPrimitive.arpeggio32d) x = self.getET(s) # helpers.dump(x) mxPart = x.find('part') mxMeasure = mxPart.find('measure') for i, mxNote in enumerate(mxMeasure.findall('note')): with self.subTest(note_index=i): nonArp = None arp = None notations = mxNote.find('notations') if notations is not None: nonArp = notations.find('non-arpeggiate') arp = notations.find('arpeggiate') if expectedResults[i].startswith('non-arpeggiate'): self.assertIsNotNone(nonArp) nonArpType = nonArp.get('type') for whichEnd in ('top', 'bottom'): if expectedResults[i].endswith(whichEnd): self.assertEqual(nonArpType, whichEnd) continue if expectedResults[i].startswith('arpeggiate'): self.assertIsNotNone(arp) arpDirection = arp.get('direction') if expectedResults[i] == 'arpeggiate': self.assertIsNone(arpDirection) continue for direction in ('up', 'down'): if expectedResults[i].endswith(direction): self.assertEqual(arpDirection, direction) continue self.assertIsNone(arp) self.assertIsNone(nonArp) def testArpeggioMarkSpanners(self): expectedNumber = ( # three-note chord with single-chord arpeggio None, None, None, # three-note chord in a multi-chord (cross-voice) arpeggio (number == 1) '1', '1', '1', # backup and do next voice # three-note chord with single-chord arpeggio None, None, None, # three-note chord in that same multi-chord (cross-voice) arpeggio (number == 1) '1', '1', '1', ) s = converter.parse(testPrimitive.multiStaffArpeggios) x = self.getET(s) mxPart = x.find('part') mxMeasure = mxPart.find('measure') for note_index, mxNote in enumerate(mxMeasure.findall('note')): with self.subTest(note_index=note_index): arp = None arpNum = None notations = mxNote.find('notations') if notations is not None: arp = notations.find('arpeggiate') if arp is not None: arpNum = arp.get('number') self.assertEqual(arpNum, expectedNumber[note_index]) def testArpeggioMarkSpannersNonArpeggiate(self): c1 = chord.Chord(['C3', 'E3', 'G3']) n2 = note.Note('D4') am = expressions.ArpeggioMarkSpanner([c1, n2], arpeggioType='non-arpeggio') m1 = stream.Measure() m1.append(c1) p1 = stream.PartStaff([m1]) m2 = stream.Measure() m2.append(n2) p2 = stream.PartStaff([m2]) sl = layout.StaffGroup([p1, p2]) s = stream.Score([sl, am, p1, p2]) x = self.getET(s) mxPart = x.find('part') mxMeasure = mxPart.find('measure') for note_index, mxNote in enumerate(mxMeasure.findall('note')): with self.subTest(note_index=note_index): arp = None arpNum = -1 notations = mxNote.find('notations') if notations is not None: arp = notations.find('non-arpeggiate') if note_index in (1, 2) and arp is not None: self.fail(f'{note_index=} should not have non-arpeggiate') if note_index in (0, 3) and arp is None: self.fail(f'{note_index=} should have non-arpeggiate') elif note_index in (0, 3): self.fail(f'{note_index=} should have notations') if arp is not None: arpNum = arp.get('number') self.assertEqual(arpNum, '1') def testExportChordSymbolsWithRealizedDurations(self): def realizeDurationsAndAssertTags(mm: stream.Measure, forwardTag=False, offsetTag=False): mm = copy.deepcopy(mm) harmony.realizeChordSymbolDurations(mm) p = stream.Part([mm]) s = stream.Score([p]) tree = self.getET(s, makeNotation=False) self.assertIs(bool(tree.findall('.//forward')), forwardTag) self.assertIs(bool(tree.findall('.//offset')), offsetTag) # Two consecutive chord symbols, no rests cs1 = harmony.ChordSymbol('C7') cs2 = harmony.ChordSymbol('F7') m = stream.Measure() m.insert(0, cs1) m.insert(2, cs2) realizeDurationsAndAssertTags(m, forwardTag=True, offsetTag=False) # Two consecutive chord symbols, rest coinciding with first one r1 = note.Rest(type='half') m.insert(0, r1) realizeDurationsAndAssertTags(m, forwardTag=False, offsetTag=False) # One chord symbol midmeasure, no rests m.remove(cs1) m.remove(r1) realizeDurationsAndAssertTags(m, forwardTag=True, offsetTag=False) # One chord symbol midmeasure coinciding with whole note n1 = note.Note(type='whole') m.insert(0, n1) # Need an offset tag to show the -2.0 offset to get from end to midmeasure realizeDurationsAndAssertTags(m, forwardTag=False, offsetTag=True) # One chord symbol at beginning of measure coinciding with whole note m.remove(cs2) m.insert(0, cs1) realizeDurationsAndAssertTags(m, forwardTag=False, offsetTag=False) # One chord symbol at beginning of measure with writeAsChord=True # followed by a half note cs1.writeAsChord = True n1.offset = 2 n1.quarterLength = 2 realizeDurationsAndAssertTags(m, forwardTag=False, offsetTag=False) def test_inexpressible_hidden_rests_become_forward_tags(self): ''' Express hidden rests with inexpressible durations as <forward> tags. ''' m = stream.Measure() # 7 eighths in the space of 4 eighths, imported as 137/480 # (137/480) * 7 = 1.9979, not 2.0 # music21 filled gap with an inexpressible 0.0021 rest and couldn't export septuplet = note.Note(type='eighth') tuplet_obj = duration.Tuplet(7, 4, 'eighth') septuplet.duration.appendTuplet(tuplet_obj) septuplet.duration.linked = False septuplet.quarterLength = fractions.Fraction(137, 480) m.repeatAppend(septuplet, 7) # leave 0.0021 gap and do the same thing from 2.0 -> 3.9979 m.repeatInsert(septuplet, [2.0]) m.repeatAppend(septuplet, 6) m.insert(0, meter.TimeSignature('4/4')) m.makeRests(inPlace=True, fillGaps=True, hideRests=True, timeRangeFromBarDuration=True) self.assertLess(m[note.Rest].first().quarterLength, 0.0025) gex = GeneralObjectExporter() tree = self.getET(gex.fromGeneralObject(m)) # Only one <forward> tag to get from 1.9979 -> 2.0 # No <forward> tag is necessary to finish the incomplete measure (3.9979 -> 4.0) self.assertEqual(len(tree.findall('.//forward')), 1) def test_roman_musicxml_two_kinds(self): from music21.roman import RomanNumeral # normal roman numerals take up no time in xml output. rn1 = RomanNumeral('I', 'C') rn1.duration.type = 'half' rn2 = RomanNumeral('V', 'C') rn2.duration.type = 'half' m = stream.Measure() m.insert(0.0, rn1) m.insert(2.0, rn2) # with writeAsChord=True, they get their own Chord objects and durations. self.assertTrue(rn1.writeAsChord) xmlOut = GeneralObjectExporter().parse(m).decode('utf-8') self.assertNotIn('<forward>', xmlOut) self.assertIn('<chord', xmlOut) rn1.writeAsChord = False rn2.writeAsChord = False xmlOut = GeneralObjectExporter().parse(m).decode('utf-8') self.assertIn('<numeral', xmlOut) self.assertIn('<forward>', xmlOut) self.assertNotIn('<chord', xmlOut) self.assertNotIn('<offset', xmlOut) self.assertNotIn('<rest', xmlOut) rn1.duration.quarterLength = 0.0 rn2.duration.quarterLength = 0.0 xmlOut = GeneralObjectExporter().parse(m).decode('utf-8') self.assertIn('<rest', xmlOut) self.assertNotIn('<forward>', xmlOut) def test_stem_style_without_direction(self): one_note_tune = converter.parse('tinyNotation: 2/4 c2') half_note = one_note_tune.recurse().notes.first() half_note.style.stemStyle = style.Style() half_note.style.stemStyle.color = 'red' xmlOut = GeneralObjectExporter().parse(one_note_tune).decode('utf-8') self.assertIn('<stem color="#FF0000">up</stem>', xmlOut) class TestExternal(unittest.TestCase): show = True def testSimple(self): # b = converter.parse(corpus.corpora.CoreCorpus().getWorkList('cpebach')[0], # format='musicxml', forceSource=True) b = corpus.parse('cpebach') # b.show('text') # n = b[note.NotRest].first() # print(n.expressions) # return SX = ScoreExporter(b) mxScore = SX.parse() helpers.indent(mxScore) sio = io.BytesIO() sio.write(SX.xmlHeader()) et = ElementTree(mxScore) et.write(sio, encoding='utf-8', xml_declaration=False) v = sio.getvalue() sio.close() v = v.decode('utf-8') # v = v.replace(' />', '/>') # normalize # b2 = converter.parse(v) fp = b.write('musicxml') if self.show: print(fp) with io.open(fp, encoding='utf-8') as f: v2 = f.read() differ = list(difflib.ndiff(v.splitlines(), v2.splitlines())) for i, line in enumerate(differ): if line.startswith('-') or line.startswith('?') or line.startswith('+'): if 'id=' in line: continue if self.show: print(line) # for j in range(i - 1,i + 1): # print(differ[j]) # print('------------------') import os os.remove(fp) if __name__ == '__main__': import music21 music21.mainTest(Test) # , runTest='testExceptionMessage')