diff --git a/music21/mei/base.py b/music21/mei/base.py index ead4acb67..80d06296d 100644 --- a/music21/mei/base.py +++ b/music21/mei/base.py @@ -188,6 +188,7 @@ from music21 import duration from music21 import environment from music21 import exceptions21 +from music21 import expressions from music21 import instrument from music21 import interval from music21 import key @@ -272,6 +273,21 @@ class MeiElementError(exceptions21.Music21Exception): _BAD_VERSE_NUMBER = 'Verse number must be an int (got "{}")' +# Mapping from an MEI @unitdur (the note value of one tremolo repetition) to the number of +# tremolo marks used by :class:`~music21.expressions.Tremolo`: an eighth-note unit is one mark, a +# sixteenth two, and so on (log2(unitdur) - 2). Durations outside this table produce no marks. +_MEI_UNITDUR_TO_TREMOLO_MARKS = { + '8': 1, + '16': 2, + '32': 3, + '64': 4, + '128': 5, + '256': 6, + '512': 7, + '1024': 8, +} + + # Module-level Functions # ----------------------------------------------------------------------------- class MeiToM21Converter: @@ -2632,7 +2648,7 @@ def beamFromElement(elem, slurBundle=None): **Attributes/Elements Implemented:** - - , , , , , , , + - , , , , , , , , **Attributes/Elements Ignored:** @@ -2660,7 +2676,7 @@ def beamFromElement(elem, slurBundle=None): **Contained Elements not Implemented:** - - MEI.cmn: bTrem beatRpt fTrem halfmRpt meterSig meterSigGrp + - MEI.cmn: beatRpt fTrem halfmRpt meterSig meterSigGrp - MEI.critapp: app - MEI.edittrans: (all) - MEI.mensural: ligature mensur proport @@ -2677,6 +2693,7 @@ def beamFromElement(elem, slurBundle=None): f'{MEI_NS}rest': restFromElement, f'{MEI_NS}tuplet': tupletFromElement, f'{MEI_NS}beam': beamFromElement, + f'{MEI_NS}bTrem': bTremFromElement, f'{MEI_NS}space': spaceFromElement, f'{MEI_NS}barLine': barLineFromElement, } @@ -2753,7 +2770,7 @@ def tupletFromElement(elem, slurBundle=None): **Attributes/Elements Implemented:** - - , , , , , , , + - , , , , , , , , - @num and @numbase **Attributes/Elements in Testing:** none @@ -2782,7 +2799,7 @@ def tupletFromElement(elem, slurBundle=None): **Contained Elements not Implemented:** - - MEI.cmn: bTrem beatRpt fTrem halfmRpt meterSig meterSigGrp + - MEI.cmn: beatRpt fTrem halfmRpt meterSig meterSigGrp - MEI.critapp: app - MEI.edittrans: (all) - MEI.mensural: ligature mensur proport @@ -2796,6 +2813,7 @@ def tupletFromElement(elem, slurBundle=None): f'{MEI_NS}rest': restFromElement, f'{MEI_NS}chord': chordFromElement, f'{MEI_NS}clef': clefFromElement, + f'{MEI_NS}bTrem': bTremFromElement, f'{MEI_NS}space': spaceFromElement, f'{MEI_NS}barLine': barLineFromElement, } @@ -2835,6 +2853,87 @@ def tupletFromElement(elem, slurBundle=None): return tuple(tupletMembers) +def bTremFromElement(elem, slurBundle=None): + ''' + A measured tremolo on a single note or chord (also called a bowed tremolo), in which + the enclosed note or chord is rapidly repeated in performance. + + In MEI 2013 (MEI.cmn module). + + :param elem: The ```` element to process. + :type elem: :class:`~xml.etree.ElementTree.Element` + :returns: An iterable of the objects contained within the ```` container. The wrapped + ```` or ```` is imported with its notated duration, so that the onsets of + following events are not shifted. + :rtype: list of :class:`~music21.base.Music21Object` + + **Example** + + A quarter-note chord notated as a measured tremolo of eighth notes (``@unitdur="8"``, one + tremolo mark). The function returns the chord itself, carrying its notated duration and a + :class:`~music21.expressions.Tremolo` expression. + + >>> from xml.etree import ElementTree as ET + >>> meiSnippet = """ + ... + ... + ... + ... + ... """ + >>> meiSnippet = ET.fromstring(meiSnippet) + >>> result = mei.base.bTremFromElement(meiSnippet) + >>> len(result) + 1 + >>> result[0] + + >>> result[0].quarterLength + 1.0 + >>> result[0].expressions + [] + >>> result[0].expressions[0].numberOfMarks + 1 + + **Attributes/Elements Implemented:** + + - , + - @unitdur (mapped to :attr:`~music21.expressions.Tremolo.numberOfMarks`) + + **Attributes/Elements in Testing:** none + + **Attributes not Implemented:** + + - att.common (@label, @n, @xml:base) (att.id (@xml:id)) + - att.typography (@fontfam, @fontname, @fontsize, @fontstyle, @fontweight) + - att.bTrem.log (att.tremmeasured (@num, @numbase)) + - att.bTrem.vis (all) + - att.bTrem.ges (att.duration.performed (@dur.ges)) + - att.bTrem.anl (all) + + **Contained Elements not Implemented:** none + ''' + # mapping from tag name to our converter function + tagToFunction = { + f'{MEI_NS}chord': chordFromElement, + f'{MEI_NS}note': noteFromElement, + } + + # Import the wrapped / as an ordinary note/chord so its notated duration survives; + # otherwise the tremolo (and every onset after it in the layer) would be silently dropped. + tremoloStuff = _processEmbeddedElements(elem.findall('*'), tagToFunction, elem.tag, slurBundle) + + # Attach a measured Tremolo, deriving the number of marks from @unitdur when it is a usable + # duration; timing correctness is preserved even when @unitdur is absent or unrecognized. + numberOfMarks = _MEI_UNITDUR_TO_TREMOLO_MARKS.get(elem.get('unitdur', '')) + if numberOfMarks is not None: + for eachObj in tremoloStuff: + if isinstance(eachObj, note.NotRest): + tremolo = expressions.Tremolo() + tremolo.numberOfMarks = numberOfMarks + eachObj.expressions.append(tremolo) + + return tremoloStuff + + def layerFromElement(elem, overrideN=None, slurBundle=None): ''' An independent stream of events on a staff. @@ -2860,7 +2959,7 @@ def layerFromElement(elem, overrideN=None, slurBundle=None): **Attributes/Elements Implemented:** - - , , , , , , , , , and + - , , , , , , , , , , and contained within - @n, from att.common @@ -2881,7 +2980,7 @@ def layerFromElement(elem, overrideN=None, slurBundle=None): **Contained Elements not Implemented:** - - MEI.cmn: arpeg bTrem beamSpan beatRpt bend breath fTrem fermata gliss hairpin halfmRpt + - MEI.cmn: arpeg beamSpan beatRpt bend breath fTrem fermata gliss hairpin halfmRpt harpPedal mRpt mRpt2 meterSig meterSigGrp multiRest multiRpt octave pedal reh slur tie tuplet tupletSpan - MEI.cmnOrnaments: mordent trill turn @@ -2906,6 +3005,7 @@ def layerFromElement(elem, overrideN=None, slurBundle=None): f'{MEI_NS}mRest': mRestFromElement, f'{MEI_NS}beam': beamFromElement, f'{MEI_NS}tuplet': tupletFromElement, + f'{MEI_NS}bTrem': bTremFromElement, f'{MEI_NS}space': spaceFromElement, f'{MEI_NS}mSpace': mSpaceFromElement, f'{MEI_NS}barLine': barLineFromElement, diff --git a/music21/mei/test_base.py b/music21/mei/test_base.py index 68c973f31..cf3e8c225 100644 --- a/music21/mei/test_base.py +++ b/music21/mei/test_base.py @@ -36,6 +36,7 @@ from music21 import bar from music21 import clef from music21 import duration +from music21 import expressions from music21 import instrument from music21 import interval from music21 import key @@ -2223,6 +2224,64 @@ def testIntegration1cLayerFromElement(self): except base.MeiAttributeError as maError: self.assertEqual(base._MISSING_VOICE_ID, maError.args[0]) + # ----------------------------------------------------------------------------- + # Tests for bTremFromElement() + + def testIntegration1BTremFromElement(self): + ''' + bTremFromElement(): the wrapped is imported with its notated duration, and a + Tremolo expression is attached with the number of marks derived from @unitdur. + ''' + inputXML = ''' + + + + + ''' + elem = ETree.fromstring(inputXML) + + actual = base.bTremFromElement(elem) + + self.assertIsInstance(actual, list) + self.assertEqual(1, len(actual)) + self.assertEqual(1.0, actual[0].quarterLength) + self.assertEqual(['E4', 'E5'], [p.nameWithOctave for p in actual[0].pitches]) + self.assertEqual(1, len(actual[0].expressions)) + self.assertIsInstance(actual[0].expressions[0], expressions.Tremolo) + self.assertEqual(1, actual[0].expressions[0].numberOfMarks) + + def testIntegration2BTremFromElement(self): + ''' + bTremFromElement() within a : a must not be silently dropped. The tremolo + note/chord contributes its notated duration so the onsets of following events do not shift + (regression test for https://github.com/cuthbertLab/music21/issues/1994). + ''' + inputXML = ''' + + + + + + + + + ''' + elem = ETree.fromstring(inputXML) + + actual = base.layerFromElement(elem) + + self.assertEqual(3, len(actual)) + # onsets are not shifted: the tremolo chord keeps its place on beat 2 + self.assertEqual([0.0, 1.0, 2.0], [obj.offset for obj in actual]) + self.assertEqual([1.0, 1.0, 2.0], [obj.quarterLength for obj in actual]) + self.assertEqual('C4', actual[0].nameWithOctave) + # the beat-2 element is the E4/E5 chord, imported with a Tremolo expression + self.assertEqual(['E4', 'E5'], [p.nameWithOctave for p in actual[1].pitches]) + self.assertEqual(1, len(actual[1].expressions)) + self.assertIsInstance(actual[1].expressions[0], expressions.Tremolo) + self.assertEqual(1, actual[1].expressions[0].numberOfMarks) + self.assertEqual('G4', actual[2].nameWithOctave) + # ----------------------------------------------------------------------------- # Tests for staffFromElement()