From 48d216d84cb4d0ce046dbcf2cd21f0dcb2384ff5 Mon Sep 17 00:00:00 2001 From: Moritz Heffter Date: Sun, 26 Feb 2023 15:57:30 +0100 Subject: [PATCH 01/58] added figuredBass tag to mei import --- music21/figuredBass/notation.py | 28 ++++ music21/mei/base.py | 238 +++++++++++++++++++++----------- 2 files changed, 182 insertions(+), 84 deletions(-) diff --git a/music21/figuredBass/notation.py b/music21/figuredBass/notation.py index 774e1236e1..f827450749 100644 --- a/music21/figuredBass/notation.py +++ b/music21/figuredBass/notation.py @@ -15,6 +15,7 @@ from music21 import exceptions21 from music21 import pitch +from music21 import harmony from music21 import prebase shorthandNotation = {(None,): (5, 3), @@ -635,6 +636,33 @@ def convertToPitch(pitchString): raise TypeError('Cannot convert ' + pitchString + ' to a music21 Pitch.') +class FiguredBassIndication(harmony.Harmony): + isFigure = True + tstamp = 1 + def __init__(self, figs=None, **keywords): + super().__init__(**keywords) + if figs: + if isinstance(figs, list): + fig_string = str(figs[0]) + for sf in figs: + fig_string += f',{sf}' + figs = fig_string + #pass + else: + figs = '' + self.__fig_notation = Notation(figs) + + @property + def fig_notation(self) -> Notation: + return self.__fig_notation + + @fig_notation.setter + def fig_notation(self, figs): + self.__fig_notation = Notation(figs) + + def __repr__(self): + return f'<{self.__class__.__name__} figures: {self.fig_notation.notationColumn}>' + _DOC_ORDER = [Notation, Figure, Modifier] diff --git a/music21/mei/base.py b/music21/mei/base.py index 70eeb75c87..da504e8d99 100644 --- a/music21/mei/base.py +++ b/music21/mei/base.py @@ -1,18 +1,18 @@ # -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Name: mei/base.py -# Purpose: Public interfaces for the MEI module +# Purpose: Public methods for the MEI module # # Authors: Christopher Antila # -# Copyright: Copyright © 2014 Michael Scott Asato Cuthbert +# Copyright: Copyright © 2014 Michael Scott Cuthbert and the music21 Project # License: BSD, see license.txt # ----------------------------------------------------------------------------- ''' -These are the public interfaces for the MEI module by Christopher Antila +These are the public methods for the MEI module by Christopher Antila To convert a string with MEI markup into music21 objects, -use :meth:`~music21.mei.MeiToM21Converter.convertFromString`. +use :meth:`MeiToM21Converter.convertFromString`. In the future, most of the functions in this module should be moved to a separate, import-only module, so that functions for writing music21-to-MEI will fit nicely. @@ -54,6 +54,7 @@ ... ... ... """ +>>> from music21 import * >>> conv = mei.MeiToM21Converter(meiString) >>> result = conv.run() >>> result @@ -170,16 +171,16 @@ * : a page break * : a line break * : a system break -''' -from __future__ import annotations +''' +# pylint: disable=misplaced-comparison-constant +from typing import Optional, Union, List, Tuple +from xml.etree import ElementTree as ETree +from xml.etree.ElementTree import Element from collections import defaultdict -from copy import deepcopy -import typing as t +from fractions import Fraction # for typing from uuid import uuid4 -from xml.etree.ElementTree import Element, ParseError, fromstring, ElementTree - # music21 from music21 import articulations @@ -199,12 +200,10 @@ from music21 import stream from music21 import spanner from music21 import tie +from music21 import figuredBass - -if t.TYPE_CHECKING: - from fractions import Fraction - -environLocal = environment.Environment('mei.base') +_MOD = 'mei.base' +environLocal = environment.Environment(_MOD) # Module-Level Constants @@ -227,30 +226,22 @@ # Exceptions # ----------------------------------------------------------------------------- class MeiValidityError(exceptions21.Music21Exception): - ''' - When there is an otherwise-unspecified validity error that prevents parsing. - ''' + 'When there is an otherwise-unspecified validity error that prevents parsing.' pass class MeiValueError(exceptions21.Music21Exception): - ''' - When an attribute has an invalid value. - ''' + 'When an attribute has an invalid value.' pass class MeiAttributeError(exceptions21.Music21Exception): - ''' - When an element has an invalid attribute. - ''' + 'When an element has an invalid attribute.' pass class MeiElementError(exceptions21.Music21Exception): - ''' - When an element itself is invalid. - ''' + 'When an element itself is invalid.' pass @@ -299,14 +290,14 @@ def __init__(self, theDocument=None): self.documentRoot = Element(f'{MEI_NS}mei') else: try: - self.documentRoot = fromstring(theDocument) - except ParseError as parseErr: + self.documentRoot = ETree.fromstring(theDocument) + except ETree.ParseError as parseErr: environLocal.printDebug( '\n\nERROR: Parsing the MEI document with ElementTree failed.') environLocal.printDebug(f'We got the following error:\n{parseErr}') raise MeiValidityError(_INVALID_XML_DOC) - if isinstance(self.documentRoot, ElementTree): + if isinstance(self.documentRoot, ETree.ElementTree): # pylint warns that :class:`Element` doesn't have a getroot() method, which is # true enough, but... self.documentRoot = self.documentRoot.getroot() # pylint: disable=maybe-no-member @@ -353,8 +344,8 @@ def run(self) -> stream.Stream: # ----------------------------------------------------------------------------- def safePitch( name: str, - accidental: str | None = None, - octave: str | int = '' + accidental: Optional[str] = None, + octave: Union[str, int] = '' ) -> pitch.Pitch: ''' Safely build a :class:`~music21.pitch.Pitch` from a string. @@ -364,9 +355,7 @@ def safePitch( function instead returns a default :class:`~music21.pitch.Pitch` instance. name: Desired name of the :class:`~music21.pitch.Pitch`. - accidental: (Optional) Symbol for the accidental. - octave: (Optional) Octave number. Returns A :class:`~music21.pitch.Pitch` with the appropriate properties. @@ -376,25 +365,19 @@ def safePitch( >>> safePitch('D', '#', '6') - >>> safePitch('D', '#') - ''' if not name: return pitch.Pitch() - if octave and accidental is not None: - return pitch.Pitch(name, octave=int(octave), accidental=accidental) - if octave: - return pitch.Pitch(name, octave=int(octave)) - if accidental is not None: - return pitch.Pitch(name, accidental=accidental) + elif accidental is None: + return pitch.Pitch(name + octave) else: - return pitch.Pitch(name) + return pitch.Pitch(name, accidental=accidental, octave=int(octave)) def makeDuration( - base: float | int | Fraction = 0.0, + base: Union[float, int, Fraction] = 0.0, dots: int = 0 -) -> duration.Duration: +) -> 'music21.duration.Duration': ''' Given a ``base`` duration and a number of ``dots``, create a :class:`~music21.duration.Duration` instance with the @@ -404,6 +387,7 @@ def makeDuration( **Examples** + >>> from music21 import * >>> from fractions import Fraction >>> mei.base.makeDuration(base=2.0, dots=0).quarterLength # half note, no dots 2.0 @@ -423,7 +407,7 @@ def makeDuration( return returnDuration -def allPartsPresent(scoreElem) -> tuple[str, ...]: +def allPartsPresent(scoreElem) -> Tuple[str, ...]: # noinspection PyShadowingNames ''' Find the @n values for all elements in a element. This assumes that every @@ -449,6 +433,7 @@ def allPartsPresent(scoreElem) -> tuple[str, ...]: ... ... """ >>> import xml.etree.ElementTree as ETree + >>> from music21 import * >>> meiDoc = ETree.fromstring(meiDoc) >>> mei.base.allPartsPresent(meiDoc) ('1', '2') @@ -459,6 +444,7 @@ def allPartsPresent(scoreElem) -> tuple[str, ...]: ''' # xpathQuery = f'.//{MEI_NS}music//{MEI_NS}score//{MEI_NS}staffDef' xpathQuery = f'.//{MEI_NS}staffDef' + partNs = [] # hold the @n attribute for all the parts for staffDef in scoreElem.findall(xpathQuery): @@ -466,6 +452,16 @@ def allPartsPresent(scoreElem) -> tuple[str, ...]: partNs.append(staffDef.get('n')) if not partNs: raise MeiValidityError(_SEEMINGLY_NO_PARTS) + + # Get information of possible tags in the score. If there are tags prepare a list to + # store them and process them later. TODO: Maybe to be put in a separate function e.g. like allPartsPresent + figuredBassQuery = f'.//{MEI_NS}fb' + if scoreElem.findall(figuredBassQuery): + environLocal.printDebug('harm tag found!') + partNs.append('fb') + # here other elements (e.g. chordsymbols) can be added. + # … 'if …' + return tuple(partNs) @@ -569,6 +565,7 @@ def _accidentalFromAttr(attr): ''' Use :func:`_attrTranslator` to convert the value of an "accid" attribute to its music21 string. + >>> from music21 import * >>> mei.base._accidentalFromAttr('s') '#' ''' @@ -580,6 +577,7 @@ def _accidGesFromAttr(attr): Use :func:`_attrTranslator` to convert the value of an @accid.ges attribute to its music21 string. + >>> from music21 import * >>> mei.base._accidGesFromAttr('s') '#' ''' @@ -590,6 +588,7 @@ def _qlDurationFromAttr(attr): ''' Use :func:`_attrTranslator` to convert an MEI "dur" attribute to a music21 quarterLength. + >>> from music21 import * >>> mei.base._qlDurationFromAttr('4') 1.0 @@ -628,8 +627,7 @@ def _makeArticList(attr): return articList -def _getOctaveShift(dis: t.Literal['8', '15', '22'] | None, - disPlace: str) -> int: +def _getOctaveShift(dis, disPlace): ''' Use :func:`_getOctaveShift` to calculate the :attr:`octaveShift` attribute for a :class:`~music21.clef.Clef` subclass. Any of the arguments may be ``None``. @@ -722,6 +720,7 @@ def _ppSlurs(theConverter): ... ... ... """ + >>> from music21 import * >>> theConverter = mei.base.MeiToM21Converter(meiDoc) >>> >>> mei.base._ppSlurs(theConverter) @@ -959,7 +958,7 @@ def _ppConclude(theConverter): # Helper Functions # ----------------------------------------------------------------------------- def _processEmbeddedElements( - elements: list[Element], + elements: List[Element], mapping, callerTag=None, slurBundle=None @@ -993,6 +992,7 @@ def _processEmbeddedElements( Because there is no ``'rest'`` key in the ``mapping``, that :class:`Element` is ignored. >>> from xml.etree.ElementTree import Element + >>> from music21 import * >>> elements = [Element('note'), Element('rest'), Element('note')] >>> mapping = {'note': lambda x, y: note.Note('D2')} >>> mei.base._processEmbeddedElements(elements, mapping, 'doctest') @@ -1038,7 +1038,7 @@ def _timeSigFromAttrs(elem): return meter.TimeSignature(f"{elem.get('meter.count')!s}/{elem.get('meter.unit')!s}") -def _keySigFromAttrs(elem: Element) -> key.Key | key.KeySignature: +def _keySigFromAttrs(elem: Element) -> Union[key.Key, key.KeySignature]: ''' From any tag with (at minimum) either @key.pname or @key.sig attributes, make a :class:`KeySignature` or :class:`Key`, as possible. @@ -1052,8 +1052,6 @@ def _keySigFromAttrs(elem: Element) -> key.Key | key.KeySignature: # noinspection PyTypeChecker mode = elem.get('key.mode', '') step = elem.get('key.pname') - if step is None: # pragma: no cover - raise MeiValidityError('Key missing step') accidental = _accidentalFromAttr(elem.get('key.accid')) if accidental is None: tonic = step @@ -1186,9 +1184,7 @@ def addSlurs(elem, obj, slurBundle): addedSlur = False def wrapGetByIdLocal(theId): - ''' - Avoid crashing when getByIdLocl() doesn't find the slur - ''' + "Avoid crashing when getByIdLocl() doesn't find the slur" try: slurBundle.getByIdLocal(theId)[0].addSpannedElements(obj) return True @@ -1299,17 +1295,17 @@ def metaSetTitle(work, meta): :return: The ``meta`` argument, having relevant metadata added. ''' # title, subtitle, and movement name - subtitle = None for title in work.findall(f'./{MEI_NS}titleStmt/{MEI_NS}title'): - if title.get('type', '') == 'subtitle': # or 'subordinate', right? - subtitle = title.text + if title.get('type', '') == 'subtitle': + meta.subtitle = title.text elif meta.title is None: meta.title = title.text - if subtitle: + if hasattr(meta, 'subtitle'): # Since m21.Metadata doesn't actually have a "subtitle" attribute, we'll put the subtitle # in the title - meta.title = f'{meta.title} ({subtitle})' + meta.title = f'{meta.title} ({meta.subtitle})' + del meta.subtitle tempo = work.find(f'./{MEI_NS}tempo') if tempo is not None: @@ -1340,7 +1336,7 @@ def metaSetComposer(work, meta): if len(composers) == 1: meta.composer = composers[0] elif len(composers) > 1: - meta.composers = composers + meta.composer = composers return meta @@ -1364,12 +1360,12 @@ def metaSetDate(work, meta): except ValueError: environLocal.warn(_MISSED_DATE.format(dateStr)) else: - meta.dateCreated = theDate + meta.date = theDate else: dateStart = date.get('notbefore') if date.get('notbefore') else date.get('startdate') dateEnd = date.get('notafter') if date.get('notafter') else date.get('enddate') if dateStart and dateEnd: - meta.dateCreated = metadata.DateBetween((dateStart, dateEnd)) + meta.date = metadata.DateBetween((dateStart, dateEnd)) return meta @@ -1555,6 +1551,7 @@ def scoreDefFromElement(elem, slurBundle=None): # pylint: disable=unused-argume ... ... ... """ + >>> from music21 import * >>> from xml.etree import ElementTree as ET >>> scoreDef = ET.fromstring(meiDoc) >>> result = mei.base.scoreDefFromElement(scoreDef) @@ -1721,6 +1718,7 @@ def staffDefFromElement(elem, slurBundle=None): # pylint: disable=unused-argume >>> meiDoc = """ ... ... """ + >>> from music21 import * >>> from xml.etree import ElementTree as ET >>> staffDef = ET.fromstring(meiDoc) >>> result = mei.base.staffDefFromElement(staffDef) @@ -1741,6 +1739,7 @@ def staffDefFromElement(elem, slurBundle=None): # pylint: disable=unused-argume ... ... ... """ + >>> from music21 import * >>> from xml.etree import ElementTree as ET >>> staffDef = ET.fromstring(meiDoc) >>> result = mei.base.staffDefFromElement(staffDef) @@ -1893,14 +1892,15 @@ def articFromElement(elem, slurBundle=None): # pylint: disable=unused-argument :attr:`~music21.note.GeneralNote.articulations` attribute. >>> from xml.etree import ElementTree as ET - >>> meiSnippet = '' + >>> from music21 import * + >>> meiSnippet = """""" >>> meiSnippet = ET.fromstring(meiSnippet) >>> mei.base.articFromElement(meiSnippet) [] A single element may indicate many :class:`Articulation` objects. - >>> meiSnippet = '' + >>> meiSnippet = """""" >>> meiSnippet = ET.fromstring(meiSnippet) >>> mei.base.articFromElement(meiSnippet) [, ] @@ -1952,11 +1952,12 @@ def accidFromElement(elem, slurBundle=None): # pylint: disable=unused-argument a string. Accidentals up to triple-sharp and triple-flat are supported. >>> from xml.etree import ElementTree as ET - >>> meiSnippet = '' + >>> from music21 import * + >>> meiSnippet = """""" >>> meiSnippet = ET.fromstring(meiSnippet) >>> mei.base.accidFromElement(meiSnippet) '#' - >>> meiSnippet = '' + >>> meiSnippet = """""" >>> meiSnippet = ET.fromstring(meiSnippet) >>> mei.base.accidFromElement(meiSnippet) '---' @@ -2339,16 +2340,13 @@ def spaceFromElement(elem, slurBundle=None): # pylint: disable=unused-argument A placeholder used to fill an incomplete measure, layer, etc. most often so that the combined duration of the events equals the number of beats in the measure. - Returns a Rest element with hideObjectOnPrint = True - In MEI 2013: pg.440 (455 in PDF) (MEI.shared module) ''' # NOTE: keep this in sync with restFromElement() theDuration = _qlDurationFromAttr(elem.get('dur')) theDuration = makeDuration(theDuration, int(elem.get('dots', 0))) - theSpace = note.Rest(duration=theDuration) - theSpace.style.hideObjectOnPrint = True + theSpace = note.SpacerRest(duration=theDuration) if elem.get(_XMLID) is not None: theSpace.id = elem.get(_XMLID) @@ -2493,6 +2491,50 @@ def chordFromElement(elem, slurBundle=None): return theChord +def harmFromElement(elem, slurBundle=None): + # other tags than to be added… + tagToFunction = {f'{MEI_NS}fb': figuredbassFromElement} + + fb_harmony_tag: tuple = () + + # Collect all elements in a measure and go throug extenders + # tstamp has to be used as a duration marker between two elements + + for subElement in _processEmbeddedElements(elem.findall('*'), + tagToFunction, + elem.tag, slurBundle): + subElement.tstamp = float(elem.get('tstamp')) + subElement.offset = subElement.tstamp - 1 + fb_harmony_tag = (subElement.tstamp - 1, subElement) + + return fb_harmony_tag + +def figuredbassFromElement(elem, slurBundle=None): + if elem.get(_XMLID): + id = elem.get(_XMLID) + fb_notation = '' + dauer: float = 0 + + # loop through all child elements and collect tags + for subElement in elem.findall('*'): + if subElement.tag == f'{MEI_NS}f': + if subElement.text != None: + if fb_notation != '': + fb_notation += f',{subElement.text}' + else: + fb_notation = subElement.text + else: + if 'extender' in subElement.attrib.keys(): + print('Extender found!') + if 'dur.metrical' in subElement.attrib.keys(): + dauer = float(subElement.attrib['dur.metrical']) + + # Generate a FiguredBassIndication object and set the collected information + theFbNotation = figuredBass.notation.FiguredBassIndication(fb_notation) + theFbNotation.id = id + theFbNotation.duration = duration.Duration(quarterLength=dauer) + + return theFbNotation def clefFromElement(elem, slurBundle=None): # pylint: disable=unused-argument ''' @@ -2604,6 +2646,7 @@ def beamFromElement(elem, slurBundle=None): a list of three objects, none of which is a :class:`Beam` or similar. >>> from xml.etree import ElementTree as ET + >>> from music21 import * >>> meiSnippet = """ ... ... @@ -3045,7 +3088,7 @@ def _makeBarlines(elem, staves): bars = bars[1] for eachMeasure in staves.values(): if isinstance(eachMeasure, stream.Measure): - eachMeasure.leftBarline = deepcopy(bars) + eachMeasure.leftBarline = bars if elem.get('right') is not None: bars = _barlineFromAttr(elem.get('right')) @@ -3055,7 +3098,7 @@ def _makeBarlines(elem, staves): bars = bars[0] for eachMeasure in staves.values(): if isinstance(eachMeasure, stream.Measure): - eachMeasure.rightBarline = deepcopy(bars) + eachMeasure.rightBarline = bars return staves @@ -3070,7 +3113,7 @@ def measureFromElement(elem, backupNum, expectedNs, slurBundle=None, activeMeter :param elem: The ```` element to process. :type elem: :class:`~xml.etree.ElementTree.Element` :param int backupNum: A fallback value for the resulting - :class:`~music21.stream.Measure` objects' number attribute. + :class:`~music21.measure.Measure` objects' number attribute. :param expectedNs: A list of the expected @n attributes for the tags in this . If an expected isn't in the , it will be created with a full-measure rest. :type expectedNs: iterable of str @@ -3131,10 +3174,12 @@ def measureFromElement(elem, backupNum, expectedNs, slurBundle=None, activeMeter ''' staves = {} stavesWaiting = {} # for staff-specific objects processed before the corresponding staff + harmElements: dict = {'fb': []} # mapping from tag name to our converter function staffTag = f'{MEI_NS}staff' staffDefTag = f'{MEI_NS}staffDef' + harmTag = f'{MEI_NS}harm' # track the bar's duration maxBarDuration = None @@ -3154,6 +3199,10 @@ def measureFromElement(elem, backupNum, expectedNs, slurBundle=None, activeMeter environLocal.warn(_UNIMPLEMENTED_IMPORT.format('', '@n')) else: stavesWaiting[whichN] = staffDefFromElement(eachElem, slurBundle) + elif harmTag == eachElem.tag: + # get all information stored in tags + harmElements['fb'].append(harmFromElement(eachElem)) + elif eachElem.tag not in _IGNORE_UNPROCESSED: environLocal.printDebug(_UNPROCESSED_SUBELEMENT.format(eachElem.tag, elem.tag)) @@ -3165,6 +3214,10 @@ def measureFromElement(elem, backupNum, expectedNs, slurBundle=None, activeMeter # We must insert() these objects because a signals its changes for the # *start* of the in which it appears. staves[whichN].insert(0, eachObj) + + # Add objects to the staves dict + staves['fb'] = harmElements + # other childs of tags can be added here… # create rest-filled measures for expected parts that had no tag in this for eachN in expectedNs: @@ -3279,6 +3332,7 @@ def sectionScoreCore(elem, allPartNs, slurBundle, *, # only process elements if this is a
if measureTag == eachElem.tag and sectionTag == elem.tag: backupMeasureNum += 1 + # process all the stuff in the measureResult = measureFromElement(eachElem, backupMeasureNum, allPartNs, slurBundle=slurBundle, @@ -3291,7 +3345,7 @@ def sectionScoreCore(elem, allPartNs, slurBundle, *, inNextThing[eachN] = [] # if we got a left-side barline from the previous measure, use it if nextMeasureLeft is not None: - measureResult[eachN].leftBarline = deepcopy(nextMeasureLeft) + measureResult[eachN].leftBarline = nextMeasureLeft # add this Measure to the Part parsed[eachN].append(measureResult[eachN]) # if we got a barline for the next @@ -3305,13 +3359,8 @@ def sectionScoreCore(elem, allPartNs, slurBundle, *, for allPartObject in localResult['all-part objects']: if isinstance(allPartObject, meter.TimeSignature): activeMeter = allPartObject - for i, eachN in enumerate(allPartNs): - if i == 0: - to_insert = allPartObject - else: - # a single Music21Object should not exist in multiple parts - to_insert = deepcopy(allPartObject) - inNextThing[eachN].append(to_insert) + for eachN in allPartNs: + inNextThing[eachN].append(allPartObject) for eachN in allPartNs: if eachN in localResult: for eachObj in localResult[eachN].values(): @@ -3345,6 +3394,7 @@ def sectionScoreCore(elem, allPartNs, slurBundle, *, # put those into the first Measure object we encounter in this Part # TODO: this is where the Instruments get added # TODO: I think "eachList" really means "each list that will become a Part" + if inNextThing[eachN]: # we have to put Instrument objects just before the Measure to which they apply theInstr = None @@ -3377,11 +3427,13 @@ def sectionScoreCore(elem, allPartNs, slurBundle, *, # must "flatten" everything so it doesn't cause a disaster when we try to make # a Part out of it. for eachObj in eachList: - parsed[eachN].append(eachObj) + if eachN in parsed.keys(): + parsed[eachN].append(eachObj) elif scoreTag == elem.tag: # If this is a , we can just append the result of each
to the # list that will become the Part. - parsed[eachN].append(eachList) + if eachN in parsed.keys(): + parsed[eachN].append(eachList) elif eachElem.tag not in _IGNORE_UNPROCESSED: environLocal.printDebug(_UNPROCESSED_SUBELEMENT.format(eachElem.tag, elem.tag)) @@ -3485,8 +3537,11 @@ def scoreFromElement(elem, slurBundle): # Get a tuple of all the @n attributes for the tags in this score. Each tag # corresponds to what will be a music21 Part. + + # UPDATE: If tags are found, they will also be collected as a separate 'part' to process them later. allPartNs = allPartsPresent(elem) + # This is the actual processing. parsed = sectionScoreCore(elem, allPartNs, slurBundle=slurBundle)[0] @@ -3494,13 +3549,28 @@ def scoreFromElement(elem, slurBundle): # We must iterate here over "allPartNs," which preserves the part-order found in the MEI # document. Iterating the keys in "parsed" would not preserve the order. environLocal.printDebug('*** making the Score') + + # Extract collected information stored in the dict unter th 'fb' key + if 'fb' in parsed.keys(): + harms = parsed['fb'][0] + del parsed['fb'] + allPartNs = allPartNs[0:-1] + theScore = [stream.Part() for _ in range(len(allPartNs))] + for i, eachN in enumerate(allPartNs): # set "atSoundingPitch" so transposition works theScore[i].atSoundingPitch = False for eachObj in parsed[eachN]: theScore[i].append(eachObj) + theScore = stream.Score(theScore) + + # loop through measures to insert harm elements from harms list at the right offsets + for index, measureOffset in enumerate(theScore.measureOffsetMap().keys()): + hms = harms[index]['fb'] + for h in hms: + theScore.insert(measureOffset + h[0], h[1]) # put slurs in the Score theScore.append(list(slurBundle)) From b3249a9c6d7a30bc3aa508c95d03608f16f05b3f Mon Sep 17 00:00:00 2001 From: Moritz Heffter Date: Sun, 26 Feb 2023 16:11:43 +0100 Subject: [PATCH 02/58] fixed import and moved figuredBassIndication to harmony --- music21/figuredBass/notation.py | 27 --------------------------- music21/harmony.py | 27 +++++++++++++++++++++++++++ music21/mei/base.py | 4 ++-- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/music21/figuredBass/notation.py b/music21/figuredBass/notation.py index f827450749..e3c76f8689 100644 --- a/music21/figuredBass/notation.py +++ b/music21/figuredBass/notation.py @@ -636,33 +636,6 @@ def convertToPitch(pitchString): raise TypeError('Cannot convert ' + pitchString + ' to a music21 Pitch.') -class FiguredBassIndication(harmony.Harmony): - isFigure = True - tstamp = 1 - def __init__(self, figs=None, **keywords): - super().__init__(**keywords) - if figs: - if isinstance(figs, list): - fig_string = str(figs[0]) - for sf in figs: - fig_string += f',{sf}' - figs = fig_string - #pass - else: - figs = '' - self.__fig_notation = Notation(figs) - - @property - def fig_notation(self) -> Notation: - return self.__fig_notation - - @fig_notation.setter - def fig_notation(self, figs): - self.__fig_notation = Notation(figs) - - def __repr__(self): - return f'<{self.__class__.__name__} figures: {self.fig_notation.notationColumn}>' - _DOC_ORDER = [Notation, Figure, Modifier] diff --git a/music21/harmony.py b/music21/harmony.py index bc26d75925..2f3891fa77 100644 --- a/music21/harmony.py +++ b/music21/harmony.py @@ -31,6 +31,7 @@ from music21 import environment from music21 import exceptions21 from music21.figuredBass import realizerScale +from music21.figuredBass import notation from music21 import interval from music21 import key from music21 import pitch @@ -2492,6 +2493,32 @@ def transpose(self: NCT, _value, *, inPlace=False) -> NCT | None: # ------------------------------------------------------------------------------ +class FiguredBassIndication(Harmony): + isFigure = True + tstamp = 1 + def __init__(self, figs=None, **keywords): + super().__init__(**keywords) + if figs: + if isinstance(figs, list): + fig_string = str(figs[0]) + for sf in figs: + fig_string += f',{sf}' + figs = fig_string + #pass + else: + figs = '' + self.__fig_notation = notation.Notation(figs) + + @property + def fig_notation(self) -> notation.Notation: + return self.__fig_notation + + @fig_notation.setter + def fig_notation(self, figs): + self.__fig_notation = notation.Notation(figs) + + def __repr__(self): + return f'<{self.__class__.__name__} figures: {self.fig_notation.notationColumn}>' def realizeChordSymbolDurations(piece): ''' diff --git a/music21/mei/base.py b/music21/mei/base.py index da504e8d99..76f41cfdd4 100644 --- a/music21/mei/base.py +++ b/music21/mei/base.py @@ -200,7 +200,7 @@ from music21 import stream from music21 import spanner from music21 import tie -from music21 import figuredBass +from music21 import harmony _MOD = 'mei.base' environLocal = environment.Environment(_MOD) @@ -2530,7 +2530,7 @@ def figuredbassFromElement(elem, slurBundle=None): dauer = float(subElement.attrib['dur.metrical']) # Generate a FiguredBassIndication object and set the collected information - theFbNotation = figuredBass.notation.FiguredBassIndication(fb_notation) + theFbNotation = harmony.FiguredBassIndication(fb_notation) theFbNotation.id = id theFbNotation.duration = duration.Duration(quarterLength=dauer) From afcde732f1bc94fd67b1250328db0d826679b5b4 Mon Sep 17 00:00:00 2001 From: Moritz Heffter Date: Sun, 26 Feb 2023 18:43:49 +0100 Subject: [PATCH 03/58] added meterSig to mei import --- music21/mei/base.py | 22 ++++++++++++++++++++-- music21/musicxml/m21ToXml.py | 4 ++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/music21/mei/base.py b/music21/mei/base.py index 76f41cfdd4..cbc0f7c040 100644 --- a/music21/mei/base.py +++ b/music21/mei/base.py @@ -1797,7 +1797,8 @@ def staffDefFromElement(elem, slurBundle=None): # pylint: disable=unused-argume - MEI.shared: clefGrp keySig label layerDef ''' # mapping from tag name to our converter function - tagToFunction = {f'{MEI_NS}clef': clefFromElement} + tagToFunction = {f'{MEI_NS}clef': clefFromElement, + f'{MEI_NS}meterSig': meterSigFromElement} # first make the Instrument post = elem.find(f'{MEI_NS}instrDef') @@ -1824,7 +1825,10 @@ def staffDefFromElement(elem, slurBundle=None): # pylint: disable=unused-argume # --> time signature if elem.get('meter.count') is not None: post['meter'] = _timeSigFromAttrs(elem) - + # --> or + if elem.find(f'{MEI_NS}meterSig') is not None: + post['meter'] = meterSigFromElement(elem.find(f'{MEI_NS}meterSig')) + # --> key signature if elem.get('key.pname') is not None or elem.get('key.sig') is not None: post['key'] = _keySigFromAttrs(elem) @@ -1848,6 +1852,20 @@ def staffDefFromElement(elem, slurBundle=None): # pylint: disable=unused-argume return post +def meterSigFromElement(elem, slurBundle=None) -> meter.TimeSignature: + ''' Container for information on meter and TimeSignature. + + In MEI 4: (MEI.cmn module) + + :returns: A meter.TimeSignature that is created from the @count und @unit attributes. + + If a xml:id is set it is provided. + ''' + + ts = meter.TimeSignature(f"{elem.get('count')!s}/{elem.get('unit')!s}") + if elem.get('xml:id') is not None: + ts.id = elem.get('xml:id') + return ts def dotFromElement(elem, slurBundle=None): # pylint: disable=unused-argument ''' diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index c2302a15b0..2673eec6a1 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -362,6 +362,7 @@ class GeneralObjectExporter: ('DiatonicScale', 'fromDiatonicScale'), ('Scale', 'fromScale'), ('Music21Object', 'fromMusic21Object'), + ('FiguredBassIndication', 'fromFiguredBassIndication'), ]) def __init__(self, obj: prebase.ProtoM21Object | None = None): @@ -515,6 +516,9 @@ def fromGeneralObject(self, obj: prebase.ProtoM21Object): ) return outObj + def fromFiguredBassIndication(self): + print('was here!') + def fromScore(self, sc): ''' Copies the input score and runs :meth:`~music21.stream.Score.makeNotation` on the copy. From 7ca3834572847f1907c9de9cc3dc9a0a392d779b Mon Sep 17 00:00:00 2001 From: Moritz Heffter Date: Tue, 28 Feb 2023 17:51:15 +0100 Subject: [PATCH 04/58] first working musicxml output for FiguredBassIndication Objects --- music21/figuredBass/notation.py | 29 +++++++++++ music21/musicxml/m21ToXml.py | 91 +++++++++++++++++++++++++++++++-- 2 files changed, 116 insertions(+), 4 deletions(-) diff --git a/music21/figuredBass/notation.py b/music21/figuredBass/notation.py index e3c76f8689..8b05d08773 100644 --- a/music21/figuredBass/notation.py +++ b/music21/figuredBass/notation.py @@ -31,6 +31,25 @@ (2,): (6, 4, 2), } +prefixes = ["+", "#", "++", "##"] +suffixes = ["\\"] + +modifiersDictXmlToM21 = { + "sharp": "#", + "flat": "b", + "double-sharp": "##", + "flat-flat": "bb", + "backslash": "\\" + } + +modifiersDictM21ToXml = { + "#": "sharp", + "b": "flat", + "##": "double-sharp", + "bb": "flat-flat", + "\\": "backslash", + "+": "sharp" +} class Notation(prebase.ProtoM21Object): ''' @@ -196,6 +215,7 @@ def __init__(self, notationColumn=None): # Convert to convenient notation self.modifiers = None self.figures = None + self.figuresFromNotationColumn = None self._getModifiers() self._getFigures() @@ -372,6 +392,15 @@ def _getFigures(self): self.figures = figures + figuresFromNotaCol = [] + + for i in range(len(self.origNumbers)): + number = self.origNumbers[i] + modifierString = self.origModStrings[i] + figure = Figure(number, modifierString) + figuresFromNotaCol.append(figure) + + self.figuresFromNotationColumn = figuresFromNotaCol class NotationException(exceptions21.Music21Exception): pass diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index 2673eec6a1..0c5e03a768 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -60,6 +60,7 @@ from music21 import tempo from music21 import tie +from music21.figuredBass.notation import Figure, prefixes, suffixes, modifiersDictM21ToXml from music21.musicxml import helpers from music21.musicxml.partStaffExporter import PartStaffExporterMixin from music21.musicxml import xmlObjects @@ -362,7 +363,6 @@ class GeneralObjectExporter: ('DiatonicScale', 'fromDiatonicScale'), ('Scale', 'fromScale'), ('Music21Object', 'fromMusic21Object'), - ('FiguredBassIndication', 'fromFiguredBassIndication'), ]) def __init__(self, obj: prebase.ProtoM21Object | None = None): @@ -493,6 +493,7 @@ def fromGeneralObject(self, obj: prebase.ProtoM21Object): >>> '' in outStr True ''' + classes = obj.classes outObj = None @@ -516,9 +517,6 @@ def fromGeneralObject(self, obj: prebase.ProtoM21Object): ) return outObj - def fromFiguredBassIndication(self): - print('was here!') - def fromScore(self, sc): ''' Copies the input score and runs :meth:`~music21.stream.Score.makeNotation` on the copy. @@ -1484,6 +1482,9 @@ def __init__(self, score: stream.Score | None = None, makeNotation: bool = True) self.firstScoreLayout = None self.textBoxes = None self.highestTime = 0.0 + self.fb_part = -1 + self.fbis_dict = {} + self.currentDivisions = defaults.divisionsPerQuarter self.refStreamOrTimeRange = [0.0, self.highestTime] @@ -1594,6 +1595,7 @@ def scorePreliminaries(self): ''' self.setScoreLayouts() self.setMeterStream() + self.getFiguredBassIndications() self.setPartsAndRefStream() # get all text boxes self.textBoxes = self.stream['TextBox'] @@ -1695,6 +1697,19 @@ def setScoreLayouts(self): self.scoreLayouts = scoreLayouts self.firstScoreLayout = scoreLayout + def getFiguredBassIndications(self): + ''' + Collect all harmony.FiguredBassIndications found in the score and store them + in a dict. The dict is later passed to the PartExporter specified + (standard value -1 for the lowest part/staff). With in the MeasureExporter the objeccts are + inserted locally in the measure and finally parsed to the converter. + ''' + for fbi in self.stream.getElementsByClass(harmony.FiguredBassIndication): + if fbi.offset not in self.fbis_dict.keys(): + self.fbis_dict[fbi.offset] = [fbi] + else: + self.fbis_dict[fbi.offset].append([fbi]) + def _populatePartExporterList(self): count = 0 sp = list(self.parts) @@ -1707,6 +1722,11 @@ def _populatePartExporterList(self): pp = PartExporter(innerStream, parent=self) pp.spannerBundle = self.spannerBundle + + # add figuredBass information to the part where it should be attached later + if innerStream == sp[self.fb_part]: + pp.fbis = self.fbis_dict + self.partExporterList.append(pp) def parsePartlikeScore(self): @@ -2673,6 +2693,7 @@ def __init__(self, partObj = stream.Part() self.stream: stream.Part | stream.Score = partObj self.parent = parent # ScoreExporter + self.fbis = None self.xmlRoot = Element('part') if parent is None: @@ -3131,6 +3152,7 @@ class MeasureExporter(XMLExporterBase): ('ChordWithFretBoard', 'chordWithFretBoardToXml'), # these three ('ChordSymbol', 'chordSymbolToXml'), # must come before ('RomanNumeral', 'romanNumeralToXml'), # ChordBase + ('FiguredBassIndication', 'figuredBassToXml'), ('ChordBase', 'chordToXml'), ('Unpitched', 'unpitchedToXml'), ('Rest', 'restToXml'), @@ -3203,9 +3225,15 @@ def parse(self): self.setMxPrint() self.setMxAttributesObjectForStartOfMeasure() self.setLeftBarline() + # Look for FiguredBassIndications and add them to the local copy of the measure + # and after the other elements + if self.parent.fbis: + self.insertFiguredBassIndications() + # BIG ONE self.mainElementsParse() # continue + self.setRightBarline() return self.xmlRoot @@ -3492,6 +3520,22 @@ def parseOneElement( for sp in postList: root.append(sp) + def insertFiguredBassIndications(self) -> None: + ''' + Adds figured bass elements from the score to the measure. In a MusicXML file tags + usually stand before the corresponding note object. The order is then provided by parseFlatElements() + ''' + # get the measure range to map the corresponding figuredBass Items + measureRange = (self.stream.offset, self.stream.offset + self.stream.highestTime) + + # look if there are figures in the current measure and insert them + # to add them later + for o, f in self.parent.fbis.items(): + if o >= measureRange[0] and o < measureRange[1]: + for fbi in f: + self.stream.insert(o - self.stream.offset, fbi) + #print('FBI inserted:', fbi) + def _hasRelatedSpanners(self, obj) -> bool: ''' returns True if and only if: @@ -4543,6 +4587,45 @@ def chordToXml(self, c: chord.ChordBase) -> list[Element]: mxNoteList.append(self.noteToXml(n, noteIndexInChord=i, chordParent=c)) return mxNoteList + def figuredBassToXml(self, f: harmony.FiguredBassIndication): + if isinstance(f, harmony.FiguredBassIndication): + mxFB = self._figuresToXml(f) + + self.xmlRoot.append(mxFB) + #_synchronizeIds(mxFB, f) + return mxFB + + def _figuresToXml(self, f: harmony.FiguredBassIndication, figureCnt=1, noteIndexInChord=0, chordParent=None): + #do Figure elements + #self.addDividerComment('BEGIN: figured-bass') + + mxFB = Element('figured-bass') + for fig in f.fig_notation.figuresFromNotationColumn: + mxFigure = SubElement(mxFB, 'figure') + + #get only the fbnumber without prefixes or suffixes + mxFNumber = SubElement(mxFigure, 'figure-number') + if fig.number: + mxFNumber.text = str(fig.number) + else: + mxFNumber.text = '' + + #modifiers are eother handled as prefixes or suffixes here + fbModifier = fig.modifierString + if fbModifier: + mxModifier = SubElement(mxFigure, 'prefix') + mxModifier.text = modifiersDictM21ToXml[fbModifier] + + mxFbDuration = SubElement(mxFB, 'duration') + duration = round(f.quarterLength * self.currentDivisions) + if duration > 0: + mxFbDuration.text = str(duration) + else: + mxFbDuration.text = str(0) + + return mxFB + #self.addDividerComment('END: figured-bass') + def durationXml(self, dur: duration.Duration): # noinspection PyShadowingNames ''' From 79ab796c6bea864cfb72e13d74fda32007cb4858 Mon Sep 17 00:00:00 2001 From: Moritz Heffter Date: Wed, 1 Mar 2023 00:38:08 +0100 Subject: [PATCH 05/58] improved figuredbass mei and musicxml import/export deals now with extenders --- music21/figuredBass/notation.py | 20 +++++++++++++++----- music21/mei/base.py | 7 +++++-- music21/musicxml/m21ToXml.py | 18 +++++++++++++----- 3 files changed, 33 insertions(+), 12 deletions(-) diff --git a/music21/figuredBass/notation.py b/music21/figuredBass/notation.py index 8b05d08773..6e49e98cc5 100644 --- a/music21/figuredBass/notation.py +++ b/music21/figuredBass/notation.py @@ -99,6 +99,7 @@ class Notation(prebase.ProtoM21Object): * '13' -> '13,11,9,7,5,3' + * '_' -> treated as an extender Figures are saved in order from left to right as found in the notationColumn. @@ -209,6 +210,7 @@ def __init__(self, notationColumn=None): self.origModStrings = None self.numbers = None self.modifierStrings = None + self.hasExtenders = False self._parseNotationColumn() self._translateToLonghand() @@ -248,8 +250,8 @@ def _parseNotationColumn(self): ''' delimiter = '[,]' figures = re.split(delimiter, self.notationColumn) - patternA1 = '([0-9]*)' - patternA2 = '([^0-9]*)' + patternA1 = '([0-9_]*)' + patternA2 = '([^0-9_]*)' numbers = [] modifierStrings = [] figureStrings = [] @@ -269,7 +271,11 @@ def _parseNotationColumn(self): number = None modifierString = None if m1: - number = int(m1[0].strip()) + if '_' in m1: + self.hasExtenders = True + number = '_' + else: + number = int(m1[0].strip()) if m2: modifierString = m2[0].strip() @@ -442,14 +448,18 @@ class Figure(prebase.ProtoM21Object): ''', } - def __init__(self, number=1, modifierString=None): + def __init__(self, number=1, modifierString=None, isExtender=None): self.number = number self.modifierString = modifierString self.modifier = Modifier(modifierString) + if self.number == '_': + self.isExtender = True + else: + self.isExtender = False def _reprInternal(self): mod = repr(self.modifier).replace('music21.figuredBass.notation.', '') - return f'{self.number} {mod}' + return f'{self.number} {mod} {self.isExtender}' # ------------------------------------------------------------------------------ diff --git a/music21/mei/base.py b/music21/mei/base.py index cbc0f7c040..28b2991e96 100644 --- a/music21/mei/base.py +++ b/music21/mei/base.py @@ -2532,7 +2532,7 @@ def figuredbassFromElement(elem, slurBundle=None): id = elem.get(_XMLID) fb_notation = '' dauer: float = 0 - + isExtender = False # loop through all child elements and collect tags for subElement in elem.findall('*'): if subElement.tag == f'{MEI_NS}f': @@ -2543,7 +2543,10 @@ def figuredbassFromElement(elem, slurBundle=None): fb_notation = subElement.text else: if 'extender' in subElement.attrib.keys(): - print('Extender found!') + if fb_notation != '': + fb_notation += f',_' + else: + fb_notation = '_' if 'dur.metrical' in subElement.attrib.keys(): dauer = float(subElement.attrib['dur.metrical']) diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index 0c5e03a768..7a055d1ded 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -4606,7 +4606,12 @@ def _figuresToXml(self, f: harmony.FiguredBassIndication, figureCnt=1, noteIndex #get only the fbnumber without prefixes or suffixes mxFNumber = SubElement(mxFigure, 'figure-number') if fig.number: - mxFNumber.text = str(fig.number) + if fig.number == '_': + mxFNumber.text = '' + mxExtender = SubElement(mxFigure, 'extend') + mxExtender.set('type', 'continue') + else: + mxFNumber.text = str(fig.number) else: mxFNumber.text = '' @@ -4616,12 +4621,15 @@ def _figuresToXml(self, f: harmony.FiguredBassIndication, figureCnt=1, noteIndex mxModifier = SubElement(mxFigure, 'prefix') mxModifier.text = modifiersDictM21ToXml[fbModifier] - mxFbDuration = SubElement(mxFB, 'duration') + # look for information on duration. If duration is 0 + # jump over the tag duration = round(f.quarterLength * self.currentDivisions) if duration > 0: - mxFbDuration.text = str(duration) - else: - mxFbDuration.text = str(0) + mxFbDuration = SubElement(mxFB, 'duration') + if duration > 0: + mxFbDuration.text = str(duration) + else: + mxFbDuration.text = str(0) return mxFB #self.addDividerComment('END: figured-bass') From fe3aaf56c98fb9dee30e2ca6930d0f749feb3cb4 Mon Sep 17 00:00:00 2001 From: Moritz Heffter Date: Sat, 4 Mar 2023 12:45:29 +0100 Subject: [PATCH 06/58] stable import and export of figured base. Some unicode characters might have to be added in the future --- music21/figuredBass/notation.py | 18 ++++++++-- music21/harmony.py | 6 ++-- music21/mei/base.py | 5 ++- music21/musicxml/m21ToXml.py | 63 ++++++++++++++++++++++++++------- music21/pitch.py | 1 + 5 files changed, 73 insertions(+), 20 deletions(-) diff --git a/music21/figuredBass/notation.py b/music21/figuredBass/notation.py index 6e49e98cc5..70ba4d3abe 100644 --- a/music21/figuredBass/notation.py +++ b/music21/figuredBass/notation.py @@ -15,7 +15,7 @@ from music21 import exceptions21 from music21 import pitch -from music21 import harmony + from music21 import prebase shorthandNotation = {(None,): (5, 3), @@ -48,7 +48,11 @@ "##": "double-sharp", "bb": "flat-flat", "\\": "backslash", - "+": "sharp" + "+": "sharp", + '\u266f': 'sharp', + '\u266e': 'natural', + '\u266d': 'flat', + '\u20e5': 'sharp' } class Notation(prebase.ProtoM21Object): @@ -369,6 +373,7 @@ def _getModifiers(self): modifiers = [] for i in range(len(self.numbers)): + print(i, self.modifierStrings) modifierString = self.modifierStrings[i] modifier = Modifier(modifierString) modifiers.append(modifier) @@ -459,7 +464,7 @@ def __init__(self, number=1, modifierString=None, isExtender=None): def _reprInternal(self): mod = repr(self.modifier).replace('music21.figuredBass.notation.', '') - return f'{self.number} {mod} {self.isExtender}' + return f'{self.number} Mods: {mod} hasExt: {self.isExtender}' # ------------------------------------------------------------------------------ @@ -473,6 +478,10 @@ def _reprInternal(self): '++': '##', '+++': '###', '++++': '####', + '\u266f': '#', + '\u266e': 'n', + '\u266d': 'b', + '\u20e5': '#' } @@ -532,6 +541,8 @@ class Modifier(prebase.ProtoM21Object): } def __init__(self, modifierString=None): + if modifierString is not None: + print('Input: ', modifierString, isinstance(modifierString, str), modifierString == '\u20e5') self.modifierString = modifierString self.accidental = self._toAccidental() @@ -566,6 +577,7 @@ def _toAccidental(self): return None a = pitch.Accidental() + print('Modifer String', self.modifierString) try: a.set(self.modifierString) except pitch.AccidentalException: diff --git a/music21/harmony.py b/music21/harmony.py index 2f3891fa77..e36fba17f4 100644 --- a/music21/harmony.py +++ b/music21/harmony.py @@ -2507,15 +2507,15 @@ def __init__(self, figs=None, **keywords): #pass else: figs = '' - self.__fig_notation = notation.Notation(figs) + self._fig_notation = notation.Notation(figs) @property def fig_notation(self) -> notation.Notation: - return self.__fig_notation + return self._fig_notation @fig_notation.setter def fig_notation(self, figs): - self.__fig_notation = notation.Notation(figs) + self._fig_notation = notation.Notation(figs) def __repr__(self): return f'<{self.__class__.__name__} figures: {self.fig_notation.notationColumn}>' diff --git a/music21/mei/base.py b/music21/mei/base.py index 28b2991e96..c6e65a5134 100644 --- a/music21/mei/base.py +++ b/music21/mei/base.py @@ -2204,10 +2204,14 @@ def noteFromElement(elem, slurBundle=None): f'{MEI_NS}artic': articFromElement, f'{MEI_NS}accid': accidFromElement, f'{MEI_NS}syl': sylFromElement} + + #print('note', elem, elem.attrib) # start with a Note with Pitch theNote = _accidentalFromAttr(elem.get('accid')) theNote = safePitch(elem.get('pname', ''), theNote, elem.get('oct', '')) + #print('thenote', theNote, isinstance(theNote, pitch.Pitch)) + theNote = note.Note(theNote) # set the Note's duration @@ -2532,7 +2536,6 @@ def figuredbassFromElement(elem, slurBundle=None): id = elem.get(_XMLID) fb_notation = '' dauer: float = 0 - isExtender = False # loop through all child elements and collect tags for subElement in elem.findall('*'): if subElement.tag == f'{MEI_NS}f': diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index 211a3a3fe3..622b10fcca 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -3364,7 +3364,20 @@ def parseFlatElements( # That way chord symbols and other 0-width objects appear before notes as much as # possible. objIterator: OffsetIterator[base.Music21Object] = OffsetIterator(m) - for objGroup in objIterator: + + # Prepare the iteration by offsets. If there are FiguredBassIndication objects + # first group them in one list together with their note, instead of handling them + # seperately. O + groupedObjList = [] + for els in objIterator: + if len(groupedObjList) > 0: + if len(els) == 1 and isinstance(els[0], harmony.FiguredBassIndication): + print('**multiple fb found**') + groupedObjList[-1].append(els[0]) + continue + groupedObjList.append(els) + + for objGroup in groupedObjList: groupOffset = m.elementOffset(objGroup[0]) offsetToMoveForward = groupOffset - self.offsetInMeasure if offsetToMoveForward > 0 and any( @@ -3390,6 +3403,7 @@ def parseFlatElements( if hasSpannerAnchors: self.parseOneElement(obj, AppendSpanners.NONE) else: + # ENTRY FOR FB self.parseOneElement(obj, AppendSpanners.NORMAL) for n in notesForLater: @@ -3522,8 +3536,11 @@ def parseOneElement( def insertFiguredBassIndications(self) -> None: ''' - Adds figured bass elements from the score to the measure. In a MusicXML file tags - usually stand before the corresponding note object. The order is then provided by parseFlatElements() + Adds relevant figured bass elements collected from the stream.Score to the + current stream.Measure object parsed afterwards. + + In a MusicXML file tags usually stand before the corresponding note object. + This order will be observed by parseFlatElements() function. Same for multiple figures at one note. ''' # get the measure range to map the corresponding figuredBass Items measureRange = (self.stream.offset, self.stream.offset + self.stream.highestTime) @@ -3534,7 +3551,7 @@ def insertFiguredBassIndications(self) -> None: if o >= measureRange[0] and o < measureRange[1]: for fbi in f: self.stream.insert(o - self.stream.offset, fbi) - #print('FBI inserted:', fbi) + #print('FBI inserted:', fbi, fbi.offset) def _hasRelatedSpanners(self, obj) -> bool: ''' @@ -4588,14 +4605,25 @@ def chordToXml(self, c: chord.ChordBase) -> list[Element]: return mxNoteList def figuredBassToXml(self, f: harmony.FiguredBassIndication): + # For FiguredBassElements we need to check whether there are + # multiple figures for one note. + # Therefore we compare offset of the figure and the current offsetInMeasure variable + print('*f:', f.offset, self.offsetInMeasure) + if f.offset > self.offsetInMeasure: + print('more than one figure found', (f.offset - self.offsetInMeasure) * self.currentDivisions) + multipleFigures = True + else: + multipleFigures = False + if isinstance(f, harmony.FiguredBassIndication): - mxFB = self._figuresToXml(f) + mxFB = self._figuresToXml(f, multipleFigures=multipleFigures) self.xmlRoot.append(mxFB) + self._fbiBefore = (f.offset, mxFB) #_synchronizeIds(mxFB, f) return mxFB - def _figuresToXml(self, f: harmony.FiguredBassIndication, figureCnt=1, noteIndexInChord=0, chordParent=None): + def _figuresToXml(self, f: harmony.FiguredBassIndication, multipleFigures=False, figureCnt=1, noteIndexInChord=0, chordParent=None): #do Figure elements #self.addDividerComment('BEGIN: figured-bass') @@ -4623,14 +4651,23 @@ def _figuresToXml(self, f: harmony.FiguredBassIndication, figureCnt=1, noteIndex # look for information on duration. If duration is 0 # jump over the tag - duration = round(f.quarterLength * self.currentDivisions) - if duration > 0: - mxFbDuration = SubElement(mxFB, 'duration') - if duration > 0: - mxFbDuration.text = str(duration) + # If we have multiple figures we have to set a tag + # and update the tag one before. + if multipleFigures: + dura = round((f.offset - self.offsetInMeasure) * self.currentDivisions) + # Update figures-bass tag before + fbDuration_before = self._fbiBefore[1] + if fbDuration_before.find('duration'): + fbDuration_before.find('duration').text = str(dura) else: - mxFbDuration.text = str(0) - + SubElement(fbDuration_before, 'duration').text = str(dura) + else: + dura = round(f.quarterLength * self.currentDivisions) + # add to the figure itself if dura > 0 + if dura > 0: + mxFbDuration = SubElement(mxFB, 'duration') + mxFbDuration.text = str(dura) + return mxFB #self.addDividerComment('END: figured-bass') diff --git a/music21/pitch.py b/music21/pitch.py index cb6a07ce24..ac9baa6695 100644 --- a/music21/pitch.py +++ b/music21/pitch.py @@ -1853,6 +1853,7 @@ def __init__(self, else: # is a number # is a midiNumber or a ps -- a float midiNumber # get step and accidental w/o octave + print(name) self.step, self._accidental = _convertPsToStep(name)[0:2] self.spellingIsInferred = True if name >= 12: # is not a pitchClass From 7708338314f9cd6b90dc8c98d78415c0beb012f4 Mon Sep 17 00:00:00 2001 From: Moritz Heffter Date: Sat, 4 Mar 2023 19:50:22 +0100 Subject: [PATCH 07/58] cleanup mei import and musicxml export --- music21/figuredBass/notation.py | 4 ---- music21/mei/base.py | 4 ---- music21/musicxml/m21ToXml.py | 10 +++++----- music21/pitch.py | 1 - 4 files changed, 5 insertions(+), 14 deletions(-) diff --git a/music21/figuredBass/notation.py b/music21/figuredBass/notation.py index 70ba4d3abe..7da5461ec6 100644 --- a/music21/figuredBass/notation.py +++ b/music21/figuredBass/notation.py @@ -373,7 +373,6 @@ def _getModifiers(self): modifiers = [] for i in range(len(self.numbers)): - print(i, self.modifierStrings) modifierString = self.modifierStrings[i] modifier = Modifier(modifierString) modifiers.append(modifier) @@ -541,8 +540,6 @@ class Modifier(prebase.ProtoM21Object): } def __init__(self, modifierString=None): - if modifierString is not None: - print('Input: ', modifierString, isinstance(modifierString, str), modifierString == '\u20e5') self.modifierString = modifierString self.accidental = self._toAccidental() @@ -577,7 +574,6 @@ def _toAccidental(self): return None a = pitch.Accidental() - print('Modifer String', self.modifierString) try: a.set(self.modifierString) except pitch.AccidentalException: diff --git a/music21/mei/base.py b/music21/mei/base.py index c6e65a5134..2436721928 100644 --- a/music21/mei/base.py +++ b/music21/mei/base.py @@ -2205,13 +2205,9 @@ def noteFromElement(elem, slurBundle=None): f'{MEI_NS}accid': accidFromElement, f'{MEI_NS}syl': sylFromElement} - #print('note', elem, elem.attrib) - # start with a Note with Pitch theNote = _accidentalFromAttr(elem.get('accid')) theNote = safePitch(elem.get('pname', ''), theNote, elem.get('oct', '')) - #print('thenote', theNote, isinstance(theNote, pitch.Pitch)) - theNote = note.Note(theNote) # set the Note's duration diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index 622b10fcca..5cb60b6ecd 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -60,7 +60,7 @@ from music21 import tempo from music21 import tie -from music21.figuredBass.notation import Figure, prefixes, suffixes, modifiersDictM21ToXml +from music21.figuredBass.notation import modifiersDictM21ToXml from music21.musicxml import helpers from music21.musicxml.partStaffExporter import PartStaffExporterMixin from music21.musicxml import xmlObjects @@ -493,7 +493,6 @@ def fromGeneralObject(self, obj: prebase.ProtoM21Object): >>> '' in outStr True ''' - classes = obj.classes outObj = None @@ -3372,7 +3371,7 @@ def parseFlatElements( for els in objIterator: if len(groupedObjList) > 0: if len(els) == 1 and isinstance(els[0], harmony.FiguredBassIndication): - print('**multiple fb found**') + #print('**multiple fb found**') groupedObjList[-1].append(els[0]) continue groupedObjList.append(els) @@ -4608,9 +4607,10 @@ def figuredBassToXml(self, f: harmony.FiguredBassIndication): # For FiguredBassElements we need to check whether there are # multiple figures for one note. # Therefore we compare offset of the figure and the current offsetInMeasure variable - print('*f:', f.offset, self.offsetInMeasure) + # print('*f:', f.offset, self.offsetInMeasure) if f.offset > self.offsetInMeasure: - print('more than one figure found', (f.offset - self.offsetInMeasure) * self.currentDivisions) + # Handle multiple figures or not + # print('more than one figure found', (f.offset - self.offsetInMeasure) * self.currentDivisions) multipleFigures = True else: multipleFigures = False diff --git a/music21/pitch.py b/music21/pitch.py index ac9baa6695..cb6a07ce24 100644 --- a/music21/pitch.py +++ b/music21/pitch.py @@ -1853,7 +1853,6 @@ def __init__(self, else: # is a number # is a midiNumber or a ps -- a float midiNumber # get step and accidental w/o octave - print(name) self.step, self._accidental = _convertPsToStep(name)[0:2] self.spellingIsInferred = True if name >= 12: # is not a pitchClass From 82995d732c23b78ed377d1abff52bee59ca4d42b Mon Sep 17 00:00:00 2001 From: Moritz Heffter Date: Tue, 7 Mar 2023 01:55:50 +0100 Subject: [PATCH 08/58] xml import and export fixed problems with more than two figures per note --- music21/figuredBass/notation.py | 1 - music21/musicxml/m21ToXml.py | 36 +++++++++++++++---- music21/musicxml/xmlToM21.py | 62 +++++++++++++++++++++++++++++++-- 3 files changed, 89 insertions(+), 10 deletions(-) diff --git a/music21/figuredBass/notation.py b/music21/figuredBass/notation.py index 7da5461ec6..cf07333e80 100644 --- a/music21/figuredBass/notation.py +++ b/music21/figuredBass/notation.py @@ -15,7 +15,6 @@ from music21 import exceptions21 from music21 import pitch - from music21 import prebase shorthandNotation = {(None,): (5, 3), diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index 5cb60b6ecd..9673ff1815 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -3196,6 +3196,8 @@ def __init__(self, self.mxTranspose = None self.measureOffsetStart = 0.0 self.offsetInMeasure = 0.0 + self.offsetFiguresInMeasure: float = 0.0 + self.tempFigureDuration: float = 0.0 self.currentVoiceId: int | str | None = None self.nextFreeVoiceNumber: int = 1 self.nextArpeggioNumber: int = 1 @@ -4628,6 +4630,7 @@ def _figuresToXml(self, f: harmony.FiguredBassIndication, multipleFigures=False, #self.addDividerComment('BEGIN: figured-bass') mxFB = Element('figured-bass') + dura = 0 for fig in f.fig_notation.figuresFromNotationColumn: mxFigure = SubElement(mxFB, 'figure') @@ -4654,19 +4657,38 @@ def _figuresToXml(self, f: harmony.FiguredBassIndication, multipleFigures=False, # If we have multiple figures we have to set a tag # and update the tag one before. if multipleFigures: - dura = round((f.offset - self.offsetInMeasure) * self.currentDivisions) + dura = round((f.offset - self.offsetInMeasure - self.offsetFiguresInMeasure) * self.currentDivisions) + self.offsetFiguresInMeasure = f.offset - self.offsetInMeasure + #print('Dura', fig, dura, self.offsetFiguresInMeasure) # Update figures-bass tag before fbDuration_before = self._fbiBefore[1] - if fbDuration_before.find('duration'): - fbDuration_before.find('duration').text = str(dura) + + # Check whether the figured-bass tag before already has a tag. + # If not create one and set its value. Otherwise update the value + if fbDuration_before.find('duration') == None: + #print('Create') + newDura = SubElement(fbDuration_before, 'duration') + newDura.text = str(dura) + #newDura.attrib['created'] = f'with {dura}' else: - SubElement(fbDuration_before, 'duration').text = str(dura) + #print('UPDATE') + + #duraBefore = fbDuration_before.find('duration').text + for d in fbDuration_before.findall('duration'): + d.text = str(dura) + #d.attrib['update'] = f'from {duraBefore}' + self.tempFigureDuration = dura else: - dura = round(f.quarterLength * self.currentDivisions) + self.offsetFiguresInMeasure = 0.0 + # dura is likely 0. + # dura = round(f.quarterLength * self.currentDivisions) + # print('Calc: ', dura) # add to the figure itself if dura > 0 - if dura > 0: + if self.tempFigureDuration > 0: + #print(f.quarterLength * self.currentDivisions) mxFbDuration = SubElement(mxFB, 'duration') - mxFbDuration.text = str(dura) + mxFbDuration.text = str(round(self.tempFigureDuration)) + self.tempFigureDuration = 0.0 return mxFB #self.addDividerComment('END: figured-bass') diff --git a/music21/musicxml/xmlToM21.py b/music21/musicxml/xmlToM21.py index 21bf435317..78d8912ed4 100644 --- a/music21/musicxml/xmlToM21.py +++ b/music21/musicxml/xmlToM21.py @@ -841,6 +841,7 @@ def __init__(self): self.m21PartObjectsById = {} self.partGroupList = [] self.parts = [] + self.fbis: list[harmony.FiguredBassIndication] | None = None self.musicXmlVersion = defaults.musicxmlVersion @@ -924,6 +925,10 @@ def xmlRootToScore(self, mxScore, inputM21=None): if part is not None: # for instance, in partStreams s.coreInsert(0.0, part) self.m21PartObjectsById[partId] = part + for fbi in self.fbis: + print('Figure:', fbi[0], fbi[1], 'Stimmt\'s?') + # fbi.offset = + s.insert(fbi[0], fbi[1]) self.partGroups() @@ -2352,6 +2357,13 @@ def applyMultiMeasureRest(self, r: note.Rest): self.stream.insert(0, self.activeMultiMeasureRestSpanner) self.activeMultiMeasureRestSpanner = None + def appendFbis(self, fbi, measureOffset): + absOffset = self.lastMeasureOffset + measureOffset + if self.parent.fbis: + self.parent.fbis.append((absOffset, fbi)) + else: + self.parent.fbis = [(absOffset, fbi)] + #print(self.parent.fbis) # ----------------------------------------------------------------------------- class MeasureParser(XMLParserBase): @@ -2388,7 +2400,7 @@ class MeasureParser(XMLParserBase): 'direction': 'xmlDirection', 'attributes': 'parseAttributesTag', 'harmony': 'xmlHarmony', - 'figured-bass': None, + 'figured-bass': 'xmlToFiguredBass', 'sound': None, 'barline': 'xmlBarline', 'grouping': None, @@ -2451,7 +2463,10 @@ def __init__(self, # what is the offset in the measure of the current note position? self.offsetMeasureNote: OffsetQL = 0.0 - + + # Offset Calc if more than one figure is set under a single note + self.lastFigureDuration = 0 + # keep track of the last rest that was added with a forward tag. # there are many pieces that end with incomplete measures that # older versions of Finale put a forward tag at the end, but this @@ -5148,6 +5163,49 @@ def xmlToChordSymbol( return cs + def xmlToFiguredBass(self, mxFiguredBass): + #print('Hello from xmlToFiguredBass', mxFiguredBass.findall('*')) + fb_strings: list[str] = [] + sep = ',' + d: duration.Duration | None = None + offsetFbi = self.offsetMeasureNote + + for figure in mxFiguredBass.findall('*'): + for el in figure.findall('*'): + #print(' ', el) + if el.tag == 'figure-number': + fb_strings.append(el.text) + if el.tag == 'extend': + if 'type' in el.attrib.keys(): + if el.attrib['type'] == 'continue': + fb_strings.append('_') + + # If a is given, this usually means that there are multiple figures for a single note. + # We have to look for offsets here. + if figure.tag == 'duration': + #print('** Dauer:', figure.text, self.lastFigureDuration) + d = self.xmlToDuration(mxFiguredBass) + if self.lastFigureDuration > 0: + offsetFbi = self.offsetMeasureNote + self.lastFigureDuration + self.lastFigureDuration += d.quarterLength + else: + offsetFbi = self.offsetMeasureNote + self.lastFigureDuration = d.quarterLength + + # If is not in the tag list set self.lastFigureDuration to 0. + # This missing duration in MuseScore3 XML usually means that the following figure is again at a note's offset. + if mxFiguredBass.find('duration'): + self.lastFigureDuration = 0 + + fb_string = sep.join(fb_strings) + #print(fb_string, 'Offset', offsetFbi, self.lastFigureDuration) + fbi = harmony.FiguredBassIndication(fb_string) + if d: + fbi.quarterLength = d.quarterLength + # function in parent add add found objects. + # + self.parent.appendFbis(fbi, offsetFbi) + def xmlDirection(self, mxDirection): ''' convert a tag to one or more expressions, metronome marks, etc. From 0cabb7dfa367488df8f69e8f1ea6efe0ae12b13b Mon Sep 17 00:00:00 2001 From: Moritz Heffter Date: Wed, 22 Mar 2023 00:39:47 +0100 Subject: [PATCH 09/58] fixed wrong offsets for multiple figures --- music21/musicxml/xmlToM21.py | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/music21/musicxml/xmlToM21.py b/music21/musicxml/xmlToM21.py index 78d8912ed4..27553d6960 100644 --- a/music21/musicxml/xmlToM21.py +++ b/music21/musicxml/xmlToM21.py @@ -2864,6 +2864,15 @@ def xmlToNote(self, mxNote: ET.Element) -> None: # only increment Chords after completion self.offsetMeasureNote += offsetIncrement self.endedWithForwardTag = None + + # reset offset for figures. This is needed to put in + # multiple FiguredBassIndications at one note at the right offset. + # Musicxml puts tags immediately before a tag, + # which means that we have to reset a given offset duration of some + # tags after inserting the coressponding note and + # before going to a new note. + self.lastFigureDuration = 0 + def xmlToChord(self, mxNoteList: list[ET.Element]) -> chord.ChordBase: # noinspection PyShadowingNames @@ -5169,6 +5178,7 @@ def xmlToFiguredBass(self, mxFiguredBass): sep = ',' d: duration.Duration | None = None offsetFbi = self.offsetMeasureNote + #print('===') for figure in mxFiguredBass.findall('*'): for el in figure.findall('*'): @@ -5183,19 +5193,27 @@ def xmlToFiguredBass(self, mxFiguredBass): # If a is given, this usually means that there are multiple figures for a single note. # We have to look for offsets here. if figure.tag == 'duration': - #print('** Dauer:', figure.text, self.lastFigureDuration) + #print(fb_strings) + #print('** Dauer:', self.lastFigureDuration) d = self.xmlToDuration(mxFiguredBass) + #print('** D:', d) if self.lastFigureDuration > 0: offsetFbi = self.offsetMeasureNote + self.lastFigureDuration self.lastFigureDuration += d.quarterLength + # print('***lfd', self.lastFigureDuration, 'offsetFbi', offsetFbi) else: offsetFbi = self.offsetMeasureNote self.lastFigureDuration = d.quarterLength - + # print('***lfd Ohne', self.lastFigureDuration, d.quarterLength, offsetFbi) + #else: + # self.lastFigureDuration = 0 # If is not in the tag list set self.lastFigureDuration to 0. # This missing duration in MuseScore3 XML usually means that the following figure is again at a note's offset. - if mxFiguredBass.find('duration'): - self.lastFigureDuration = 0 + #if mxFiguredBass.find('duration'): + # print('reset') + # self.lastFigureDuration = 0 + #else: + # print('H', mxFiguredBass.find('duration')) fb_string = sep.join(fb_strings) #print(fb_string, 'Offset', offsetFbi, self.lastFigureDuration) From fd1626d9e8a2189e7e53a2fed03a5b8908d80b7f Mon Sep 17 00:00:00 2001 From: Moritz Heffter Date: Wed, 22 Mar 2023 12:14:16 +0100 Subject: [PATCH 10/58] fixed export of multiple figures, where an already exsiting duration was erroneously set to 0 --- music21/musicxml/m21ToXml.py | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index 9673ff1815..79b20b0375 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -3371,13 +3371,14 @@ def parseFlatElements( # seperately. O groupedObjList = [] for els in objIterator: + print(els) if len(groupedObjList) > 0: if len(els) == 1 and isinstance(els[0], harmony.FiguredBassIndication): #print('**multiple fb found**') groupedObjList[-1].append(els[0]) continue groupedObjList.append(els) - + #print('üüü', groupedObjList) for objGroup in groupedObjList: groupOffset = m.elementOffset(objGroup[0]) offsetToMoveForward = groupOffset - self.offsetInMeasure @@ -3404,10 +3405,12 @@ def parseFlatElements( if hasSpannerAnchors: self.parseOneElement(obj, AppendSpanners.NONE) else: - # ENTRY FOR FB + # ENTRY FOR Figured Bass Indications + print('object: ', obj) self.parseOneElement(obj, AppendSpanners.NORMAL) for n in notesForLater: + print('notes:', n) if n.isRest and n.style.hideObjectOnPrint and n.duration.type == 'inexpressible': # Prefer a gap in stream, to be filled with a tag by # fill_gap_with_forward_tag() rather than raising exceptions @@ -3550,8 +3553,13 @@ def insertFiguredBassIndications(self) -> None: # to add them later for o, f in self.parent.fbis.items(): if o >= measureRange[0] and o < measureRange[1]: + print('figures: ', f, o) for fbi in f: + #if isinstance(fbi, harmony.FiguredBassIndication): self.stream.insert(o - self.stream.offset, fbi) + #elif isinstance(fbi, list): + # for i in fbi: + # self.stream.insert(o - self.stream.offset, i) #print('FBI inserted:', fbi, fbi.offset) def _hasRelatedSpanners(self, obj) -> bool: @@ -4611,11 +4619,13 @@ def figuredBassToXml(self, f: harmony.FiguredBassIndication): # Therefore we compare offset of the figure and the current offsetInMeasure variable # print('*f:', f.offset, self.offsetInMeasure) if f.offset > self.offsetInMeasure: - # Handle multiple figures or not - # print('more than one figure found', (f.offset - self.offsetInMeasure) * self.currentDivisions) + # Handle multiple figures or not# + print('** more than one figure found', (f.offset - self.offsetInMeasure) * self.currentDivisions) multipleFigures = True else: multipleFigures = False + print('** stay:', (f.offset - self.offsetInMeasure) * self.currentDivisions) + if isinstance(f, harmony.FiguredBassIndication): mxFB = self._figuresToXml(f, multipleFigures=multipleFigures) @@ -4666,17 +4676,18 @@ def _figuresToXml(self, f: harmony.FiguredBassIndication, multipleFigures=False, # Check whether the figured-bass tag before already has a tag. # If not create one and set its value. Otherwise update the value if fbDuration_before.find('duration') == None: - #print('Create') newDura = SubElement(fbDuration_before, 'duration') newDura.text = str(dura) #newDura.attrib['created'] = f'with {dura}' else: - #print('UPDATE') - #duraBefore = fbDuration_before.find('duration').text - for d in fbDuration_before.findall('duration'): - d.text = str(dura) - #d.attrib['update'] = f'from {duraBefore}' + + # If dura is set to 0 skip. This happens e.g. at the beginning of a piece. + # Otherwise an already set duration tag is resetted to 0 + if dura > 0: + for d in fbDuration_before.findall('duration'): + d.text = str(dura) + #d.attrib['update'] = f'from {duraBefore}' self.tempFigureDuration = dura else: self.offsetFiguresInMeasure = 0.0 From 874b95909cedc5eea516b28e1419d2cddf73c739 Mon Sep 17 00:00:00 2001 From: Moritz Heffter Date: Wed, 22 Mar 2023 15:43:28 +0100 Subject: [PATCH 11/58] cleaned upcomments and stuff --- music21/musicxml/m21ToXml.py | 44 +++++++++++------------------------- music21/musicxml/xmlToM21.py | 21 +++-------------- 2 files changed, 16 insertions(+), 49 deletions(-) diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index 79b20b0375..d3edc5c2e6 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -3371,7 +3371,7 @@ def parseFlatElements( # seperately. O groupedObjList = [] for els in objIterator: - print(els) + #print(els) if len(groupedObjList) > 0: if len(els) == 1 and isinstance(els[0], harmony.FiguredBassIndication): #print('**multiple fb found**') @@ -3406,11 +3406,9 @@ def parseFlatElements( self.parseOneElement(obj, AppendSpanners.NONE) else: # ENTRY FOR Figured Bass Indications - print('object: ', obj) self.parseOneElement(obj, AppendSpanners.NORMAL) for n in notesForLater: - print('notes:', n) if n.isRest and n.style.hideObjectOnPrint and n.duration.type == 'inexpressible': # Prefer a gap in stream, to be filled with a tag by # fill_gap_with_forward_tag() rather than raising exceptions @@ -3549,19 +3547,13 @@ def insertFiguredBassIndications(self) -> None: # get the measure range to map the corresponding figuredBass Items measureRange = (self.stream.offset, self.stream.offset + self.stream.highestTime) - # look if there are figures in the current measure and insert them - # to add them later + # Look if there are figures in the current measure and insert them + # to add them later. for o, f in self.parent.fbis.items(): if o >= measureRange[0] and o < measureRange[1]: - print('figures: ', f, o) for fbi in f: - #if isinstance(fbi, harmony.FiguredBassIndication): self.stream.insert(o - self.stream.offset, fbi) - #elif isinstance(fbi, list): - # for i in fbi: - # self.stream.insert(o - self.stream.offset, i) - #print('FBI inserted:', fbi, fbi.offset) - + def _hasRelatedSpanners(self, obj) -> bool: ''' returns True if and only if: @@ -4617,19 +4609,15 @@ def figuredBassToXml(self, f: harmony.FiguredBassIndication): # For FiguredBassElements we need to check whether there are # multiple figures for one note. # Therefore we compare offset of the figure and the current offsetInMeasure variable - # print('*f:', f.offset, self.offsetInMeasure) if f.offset > self.offsetInMeasure: - # Handle multiple figures or not# - print('** more than one figure found', (f.offset - self.offsetInMeasure) * self.currentDivisions) multipleFigures = True else: multipleFigures = False - print('** stay:', (f.offset - self.offsetInMeasure) * self.currentDivisions) - - if isinstance(f, harmony.FiguredBassIndication): - mxFB = self._figuresToXml(f, multipleFigures=multipleFigures) + # Hand the FiguredBassIndication over to the helper function. + mxFB = self._figuresToXml(f, multipleFigures=multipleFigures) + # Append it to xmlRoot self.xmlRoot.append(mxFB) self._fbiBefore = (f.offset, mxFB) #_synchronizeIds(mxFB, f) @@ -4656,7 +4644,7 @@ def _figuresToXml(self, f: harmony.FiguredBassIndication, multipleFigures=False, else: mxFNumber.text = '' - #modifiers are eother handled as prefixes or suffixes here + #modifiers are either handled as prefixes or suffixes here fbModifier = fig.modifierString if fbModifier: mxModifier = SubElement(mxFigure, 'prefix') @@ -4669,7 +4657,7 @@ def _figuresToXml(self, f: harmony.FiguredBassIndication, multipleFigures=False, if multipleFigures: dura = round((f.offset - self.offsetInMeasure - self.offsetFiguresInMeasure) * self.currentDivisions) self.offsetFiguresInMeasure = f.offset - self.offsetInMeasure - #print('Dura', fig, dura, self.offsetFiguresInMeasure) + # Update figures-bass tag before fbDuration_before = self._fbiBefore[1] @@ -4678,23 +4666,17 @@ def _figuresToXml(self, f: harmony.FiguredBassIndication, multipleFigures=False, if fbDuration_before.find('duration') == None: newDura = SubElement(fbDuration_before, 'duration') newDura.text = str(dura) - #newDura.attrib['created'] = f'with {dura}' else: - #duraBefore = fbDuration_before.find('duration').text - - # If dura is set to 0 skip. This happens e.g. at the beginning of a piece. - # Otherwise an already set duration tag is resetted to 0 + # If dura is set to 0 skip the update process. + # This happens e.g. at the beginning of a piece. + # Otherwise an already set duration tag is resetted to 0 which is not wanted. if dura > 0: for d in fbDuration_before.findall('duration'): d.text = str(dura) - #d.attrib['update'] = f'from {duraBefore}' self.tempFigureDuration = dura else: self.offsetFiguresInMeasure = 0.0 - # dura is likely 0. - # dura = round(f.quarterLength * self.currentDivisions) - # print('Calc: ', dura) - # add to the figure itself if dura > 0 + if self.tempFigureDuration > 0: #print(f.quarterLength * self.currentDivisions) mxFbDuration = SubElement(mxFB, 'duration') diff --git a/music21/musicxml/xmlToM21.py b/music21/musicxml/xmlToM21.py index 27553d6960..b684eb4a63 100644 --- a/music21/musicxml/xmlToM21.py +++ b/music21/musicxml/xmlToM21.py @@ -926,8 +926,7 @@ def xmlRootToScore(self, mxScore, inputM21=None): s.coreInsert(0.0, part) self.m21PartObjectsById[partId] = part for fbi in self.fbis: - print('Figure:', fbi[0], fbi[1], 'Stimmt\'s?') - # fbi.offset = + s.insert(fbi[0], fbi[1]) self.partGroups() @@ -5178,7 +5177,6 @@ def xmlToFiguredBass(self, mxFiguredBass): sep = ',' d: duration.Duration | None = None offsetFbi = self.offsetMeasureNote - #print('===') for figure in mxFiguredBass.findall('*'): for el in figure.findall('*'): @@ -5193,31 +5191,18 @@ def xmlToFiguredBass(self, mxFiguredBass): # If a is given, this usually means that there are multiple figures for a single note. # We have to look for offsets here. if figure.tag == 'duration': - #print(fb_strings) - #print('** Dauer:', self.lastFigureDuration) d = self.xmlToDuration(mxFiguredBass) - #print('** D:', d) if self.lastFigureDuration > 0: offsetFbi = self.offsetMeasureNote + self.lastFigureDuration self.lastFigureDuration += d.quarterLength - # print('***lfd', self.lastFigureDuration, 'offsetFbi', offsetFbi) else: offsetFbi = self.offsetMeasureNote self.lastFigureDuration = d.quarterLength - # print('***lfd Ohne', self.lastFigureDuration, d.quarterLength, offsetFbi) - #else: - # self.lastFigureDuration = 0 - # If is not in the tag list set self.lastFigureDuration to 0. - # This missing duration in MuseScore3 XML usually means that the following figure is again at a note's offset. - #if mxFiguredBass.find('duration'): - # print('reset') - # self.lastFigureDuration = 0 - #else: - # print('H', mxFiguredBass.find('duration')) + fb_string = sep.join(fb_strings) - #print(fb_string, 'Offset', offsetFbi, self.lastFigureDuration) fbi = harmony.FiguredBassIndication(fb_string) + # If a duration is provided, set length of the FigureBassIndication if d: fbi.quarterLength = d.quarterLength # function in parent add add found objects. From a85aeeba44237775ef99e4e1a4710088d7cb5edb Mon Sep 17 00:00:00 2001 From: Moritz Heffter Date: Wed, 22 Mar 2023 18:54:39 +0100 Subject: [PATCH 12/58] added support for prefix tags and solved problems with empty number tags --- music21/figuredBass/notation.py | 1 + music21/musicxml/xmlToM21.py | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/music21/figuredBass/notation.py b/music21/figuredBass/notation.py index cf07333e80..3bb5806606 100644 --- a/music21/figuredBass/notation.py +++ b/music21/figuredBass/notation.py @@ -36,6 +36,7 @@ modifiersDictXmlToM21 = { "sharp": "#", "flat": "b", + "natural": "\u266e", "double-sharp": "##", "flat-flat": "bb", "backslash": "\\" diff --git a/music21/musicxml/xmlToM21.py b/music21/musicxml/xmlToM21.py index b684eb4a63..c5754518ca 100644 --- a/music21/musicxml/xmlToM21.py +++ b/music21/musicxml/xmlToM21.py @@ -54,6 +54,7 @@ from music21 import tempo from music21 import text # for text boxes from music21 import tie +from music21.figuredBass.notation import modifiersDictXmlToM21 from music21.musicxml import xmlObjects from music21.musicxml.xmlObjects import MusicXMLImportException, MusicXMLWarning @@ -5181,8 +5182,20 @@ def xmlToFiguredBass(self, mxFiguredBass): for figure in mxFiguredBass.findall('*'): for el in figure.findall('*'): #print(' ', el) + fb_number: str = '' + fb_prefix: str = '' if el.tag == 'figure-number': - fb_strings.append(el.text) + if el.text: + fb_number = el.text + if figure.findall('prefix'): + for prefix in figure.findall('prefix'): + + if prefix.text: + fb_prefix = modifiersDictXmlToM21[prefix.text] + # put prefix and number together + if fb_prefix + fb_number != '': + fb_strings.append(fb_prefix + fb_number) + if el.tag == 'extend': if 'type' in el.attrib.keys(): if el.attrib['type'] == 'continue': @@ -5199,7 +5212,7 @@ def xmlToFiguredBass(self, mxFiguredBass): offsetFbi = self.offsetMeasureNote self.lastFigureDuration = d.quarterLength - + #print('ü', fb_strings) fb_string = sep.join(fb_strings) fbi = harmony.FiguredBassIndication(fb_string) # If a duration is provided, set length of the FigureBassIndication From ffc61900f43c8d006c1bff22a00b026dd322783c Mon Sep 17 00:00:00 2001 From: Moritz Heffter Date: Thu, 23 Mar 2023 00:06:54 +0100 Subject: [PATCH 13/58] figured bass is now checked --- music21/musicxml/xmlToM21.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/music21/musicxml/xmlToM21.py b/music21/musicxml/xmlToM21.py index c5754518ca..d24c47b1d8 100644 --- a/music21/musicxml/xmlToM21.py +++ b/music21/musicxml/xmlToM21.py @@ -926,9 +926,10 @@ def xmlRootToScore(self, mxScore, inputM21=None): if part is not None: # for instance, in partStreams s.coreInsert(0.0, part) self.m21PartObjectsById[partId] = part - for fbi in self.fbis: - - s.insert(fbi[0], fbi[1]) + + if self.fbis: + for fbi in self.fbis: + s.insert(fbi[0], fbi[1]) self.partGroups() @@ -5180,6 +5181,7 @@ def xmlToFiguredBass(self, mxFiguredBass): offsetFbi = self.offsetMeasureNote for figure in mxFiguredBass.findall('*'): + # TODO: suffixes are ignored at the moment for el in figure.findall('*'): #print(' ', el) fb_number: str = '' From 9acd4a6af7a2d9792cc5be272e85fef607c71715 Mon Sep 17 00:00:00 2001 From: Moritz Heffter Date: Fri, 24 Mar 2023 00:14:22 +0100 Subject: [PATCH 14/58] updated mei/base.py --- music21/mei/base.py | 177 ++++++++++++++++++----------------- music21/mei/test_base.py | 6 +- music21/musicxml/m21ToXml.py | 9 +- 3 files changed, 101 insertions(+), 91 deletions(-) diff --git a/music21/mei/base.py b/music21/mei/base.py index 2436721928..7964ad8a8e 100644 --- a/music21/mei/base.py +++ b/music21/mei/base.py @@ -1,18 +1,18 @@ # -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Name: mei/base.py -# Purpose: Public methods for the MEI module +# Purpose: Public interfaces for the MEI module # # Authors: Christopher Antila # -# Copyright: Copyright © 2014 Michael Scott Cuthbert and the music21 Project +# Copyright: Copyright © 2014 Michael Scott Asato Cuthbert # License: BSD, see license.txt # ----------------------------------------------------------------------------- ''' -These are the public methods for the MEI module by Christopher Antila +These are the public interfaces for the MEI module by Christopher Antila To convert a string with MEI markup into music21 objects, -use :meth:`MeiToM21Converter.convertFromString`. +use :meth:`~music21.mei.MeiToM21Converter.convertFromString`. In the future, most of the functions in this module should be moved to a separate, import-only module, so that functions for writing music21-to-MEI will fit nicely. @@ -54,7 +54,6 @@ ... ... ... """ ->>> from music21 import * >>> conv = mei.MeiToM21Converter(meiString) >>> result = conv.run() >>> result @@ -171,16 +170,16 @@ * : a page break * : a line break * : a system break - ''' -# pylint: disable=misplaced-comparison-constant -from typing import Optional, Union, List, Tuple -from xml.etree import ElementTree as ETree -from xml.etree.ElementTree import Element + +from __future__ import annotations from collections import defaultdict -from fractions import Fraction # for typing +from copy import deepcopy +import typing as t from uuid import uuid4 +from xml.etree.ElementTree import Element, ParseError, fromstring, ElementTree + # music21 from music21 import articulations @@ -202,8 +201,11 @@ from music21 import tie from music21 import harmony -_MOD = 'mei.base' -environLocal = environment.Environment(_MOD) + +if t.TYPE_CHECKING: + from fractions import Fraction + +environLocal = environment.Environment('mei.base') # Module-Level Constants @@ -226,22 +228,30 @@ # Exceptions # ----------------------------------------------------------------------------- class MeiValidityError(exceptions21.Music21Exception): - 'When there is an otherwise-unspecified validity error that prevents parsing.' + ''' + When there is an otherwise-unspecified validity error that prevents parsing. + ''' pass class MeiValueError(exceptions21.Music21Exception): - 'When an attribute has an invalid value.' + ''' + When an attribute has an invalid value. + ''' pass class MeiAttributeError(exceptions21.Music21Exception): - 'When an element has an invalid attribute.' + ''' + When an element has an invalid attribute. + ''' pass class MeiElementError(exceptions21.Music21Exception): - 'When an element itself is invalid.' + ''' + When an element itself is invalid. + ''' pass @@ -290,14 +300,14 @@ def __init__(self, theDocument=None): self.documentRoot = Element(f'{MEI_NS}mei') else: try: - self.documentRoot = ETree.fromstring(theDocument) - except ETree.ParseError as parseErr: + self.documentRoot = fromstring(theDocument) + except ParseError as parseErr: environLocal.printDebug( '\n\nERROR: Parsing the MEI document with ElementTree failed.') environLocal.printDebug(f'We got the following error:\n{parseErr}') raise MeiValidityError(_INVALID_XML_DOC) - if isinstance(self.documentRoot, ETree.ElementTree): + if isinstance(self.documentRoot, ElementTree): # pylint warns that :class:`Element` doesn't have a getroot() method, which is # true enough, but... self.documentRoot = self.documentRoot.getroot() # pylint: disable=maybe-no-member @@ -344,8 +354,8 @@ def run(self) -> stream.Stream: # ----------------------------------------------------------------------------- def safePitch( name: str, - accidental: Optional[str] = None, - octave: Union[str, int] = '' + accidental: str | None = None, + octave: str | int = '' ) -> pitch.Pitch: ''' Safely build a :class:`~music21.pitch.Pitch` from a string. @@ -355,7 +365,9 @@ def safePitch( function instead returns a default :class:`~music21.pitch.Pitch` instance. name: Desired name of the :class:`~music21.pitch.Pitch`. + accidental: (Optional) Symbol for the accidental. + octave: (Optional) Octave number. Returns A :class:`~music21.pitch.Pitch` with the appropriate properties. @@ -365,19 +377,25 @@ def safePitch( >>> safePitch('D', '#', '6') + >>> safePitch('D', '#') + ''' if not name: return pitch.Pitch() - elif accidental is None: - return pitch.Pitch(name + octave) + if octave and accidental is not None: + return pitch.Pitch(name, octave=int(octave), accidental=accidental) + if octave: + return pitch.Pitch(name, octave=int(octave)) + if accidental is not None: + return pitch.Pitch(name, accidental=accidental) else: - return pitch.Pitch(name, accidental=accidental, octave=int(octave)) + return pitch.Pitch(name) def makeDuration( - base: Union[float, int, Fraction] = 0.0, + base: float | int | Fraction = 0.0, dots: int = 0 -) -> 'music21.duration.Duration': +) -> duration.Duration: ''' Given a ``base`` duration and a number of ``dots``, create a :class:`~music21.duration.Duration` instance with the @@ -387,7 +405,6 @@ def makeDuration( **Examples** - >>> from music21 import * >>> from fractions import Fraction >>> mei.base.makeDuration(base=2.0, dots=0).quarterLength # half note, no dots 2.0 @@ -407,7 +424,7 @@ def makeDuration( return returnDuration -def allPartsPresent(scoreElem) -> Tuple[str, ...]: +def allPartsPresent(scoreElem) -> tuple[str, ...]: # noinspection PyShadowingNames ''' Find the @n values for all elements in a element. This assumes that every @@ -433,7 +450,6 @@ def allPartsPresent(scoreElem) -> Tuple[str, ...]: ...
...
""" >>> import xml.etree.ElementTree as ETree - >>> from music21 import * >>> meiDoc = ETree.fromstring(meiDoc) >>> mei.base.allPartsPresent(meiDoc) ('1', '2') @@ -444,7 +460,6 @@ def allPartsPresent(scoreElem) -> Tuple[str, ...]: ''' # xpathQuery = f'.//{MEI_NS}music//{MEI_NS}score//{MEI_NS}staffDef' xpathQuery = f'.//{MEI_NS}staffDef' - partNs = [] # hold the @n attribute for all the parts for staffDef in scoreElem.findall(xpathQuery): @@ -565,7 +580,6 @@ def _accidentalFromAttr(attr): ''' Use :func:`_attrTranslator` to convert the value of an "accid" attribute to its music21 string. - >>> from music21 import * >>> mei.base._accidentalFromAttr('s') '#' ''' @@ -577,7 +591,6 @@ def _accidGesFromAttr(attr): Use :func:`_attrTranslator` to convert the value of an @accid.ges attribute to its music21 string. - >>> from music21 import * >>> mei.base._accidGesFromAttr('s') '#' ''' @@ -588,7 +601,6 @@ def _qlDurationFromAttr(attr): ''' Use :func:`_attrTranslator` to convert an MEI "dur" attribute to a music21 quarterLength. - >>> from music21 import * >>> mei.base._qlDurationFromAttr('4') 1.0 @@ -627,7 +639,8 @@ def _makeArticList(attr): return articList -def _getOctaveShift(dis, disPlace): +def _getOctaveShift(dis: t.Literal['8', '15', '22'] | None, + disPlace: str) -> int: ''' Use :func:`_getOctaveShift` to calculate the :attr:`octaveShift` attribute for a :class:`~music21.clef.Clef` subclass. Any of the arguments may be ``None``. @@ -720,7 +733,6 @@ def _ppSlurs(theConverter): ...
... ... """ - >>> from music21 import * >>> theConverter = mei.base.MeiToM21Converter(meiDoc) >>> >>> mei.base._ppSlurs(theConverter) @@ -958,7 +970,7 @@ def _ppConclude(theConverter): # Helper Functions # ----------------------------------------------------------------------------- def _processEmbeddedElements( - elements: List[Element], + elements: list[Element], mapping, callerTag=None, slurBundle=None @@ -992,7 +1004,6 @@ def _processEmbeddedElements( Because there is no ``'rest'`` key in the ``mapping``, that :class:`Element` is ignored. >>> from xml.etree.ElementTree import Element - >>> from music21 import * >>> elements = [Element('note'), Element('rest'), Element('note')] >>> mapping = {'note': lambda x, y: note.Note('D2')} >>> mei.base._processEmbeddedElements(elements, mapping, 'doctest') @@ -1038,7 +1049,7 @@ def _timeSigFromAttrs(elem): return meter.TimeSignature(f"{elem.get('meter.count')!s}/{elem.get('meter.unit')!s}") -def _keySigFromAttrs(elem: Element) -> Union[key.Key, key.KeySignature]: +def _keySigFromAttrs(elem: Element) -> key.Key | key.KeySignature: ''' From any tag with (at minimum) either @key.pname or @key.sig attributes, make a :class:`KeySignature` or :class:`Key`, as possible. @@ -1052,6 +1063,8 @@ def _keySigFromAttrs(elem: Element) -> Union[key.Key, key.KeySignature]: # noinspection PyTypeChecker mode = elem.get('key.mode', '') step = elem.get('key.pname') + if step is None: # pragma: no cover + raise MeiValidityError('Key missing step') accidental = _accidentalFromAttr(elem.get('key.accid')) if accidental is None: tonic = step @@ -1295,17 +1308,17 @@ def metaSetTitle(work, meta): :return: The ``meta`` argument, having relevant metadata added. ''' # title, subtitle, and movement name + subtitle = None for title in work.findall(f'./{MEI_NS}titleStmt/{MEI_NS}title'): - if title.get('type', '') == 'subtitle': - meta.subtitle = title.text + if title.get('type', '') == 'subtitle': # or 'subordinate', right? + subtitle = title.text elif meta.title is None: meta.title = title.text - if hasattr(meta, 'subtitle'): + if subtitle: # Since m21.Metadata doesn't actually have a "subtitle" attribute, we'll put the subtitle # in the title - meta.title = f'{meta.title} ({meta.subtitle})' - del meta.subtitle + meta.title = f'{meta.title} ({subtitle})' tempo = work.find(f'./{MEI_NS}tempo') if tempo is not None: @@ -1336,7 +1349,7 @@ def metaSetComposer(work, meta): if len(composers) == 1: meta.composer = composers[0] elif len(composers) > 1: - meta.composer = composers + meta.composers = composers return meta @@ -1360,12 +1373,12 @@ def metaSetDate(work, meta): except ValueError: environLocal.warn(_MISSED_DATE.format(dateStr)) else: - meta.date = theDate + meta.dateCreated = theDate else: dateStart = date.get('notbefore') if date.get('notbefore') else date.get('startdate') dateEnd = date.get('notafter') if date.get('notafter') else date.get('enddate') if dateStart and dateEnd: - meta.date = metadata.DateBetween((dateStart, dateEnd)) + meta.dateCreated = metadata.DateBetween((dateStart, dateEnd)) return meta @@ -1718,7 +1731,6 @@ def staffDefFromElement(elem, slurBundle=None): # pylint: disable=unused-argume >>> meiDoc = """ ... ... """ - >>> from music21 import * >>> from xml.etree import ElementTree as ET >>> staffDef = ET.fromstring(meiDoc) >>> result = mei.base.staffDefFromElement(staffDef) @@ -1739,7 +1751,6 @@ def staffDefFromElement(elem, slurBundle=None): # pylint: disable=unused-argume ... ... ... """ - >>> from music21 import * >>> from xml.etree import ElementTree as ET >>> staffDef = ET.fromstring(meiDoc) >>> result = mei.base.staffDefFromElement(staffDef) @@ -1825,10 +1836,7 @@ def staffDefFromElement(elem, slurBundle=None): # pylint: disable=unused-argume # --> time signature if elem.get('meter.count') is not None: post['meter'] = _timeSigFromAttrs(elem) - # --> or - if elem.find(f'{MEI_NS}meterSig') is not None: - post['meter'] = meterSigFromElement(elem.find(f'{MEI_NS}meterSig')) - + # --> key signature if elem.get('key.pname') is not None or elem.get('key.sig') is not None: post['key'] = _keySigFromAttrs(elem) @@ -1910,15 +1918,14 @@ def articFromElement(elem, slurBundle=None): # pylint: disable=unused-argument :attr:`~music21.note.GeneralNote.articulations` attribute. >>> from xml.etree import ElementTree as ET - >>> from music21 import * - >>> meiSnippet = """""" + >>> meiSnippet = '' >>> meiSnippet = ET.fromstring(meiSnippet) >>> mei.base.articFromElement(meiSnippet) [] A single element may indicate many :class:`Articulation` objects. - >>> meiSnippet = """""" + >>> meiSnippet = '' >>> meiSnippet = ET.fromstring(meiSnippet) >>> mei.base.articFromElement(meiSnippet) [, ] @@ -1970,12 +1977,11 @@ def accidFromElement(elem, slurBundle=None): # pylint: disable=unused-argument a string. Accidentals up to triple-sharp and triple-flat are supported. >>> from xml.etree import ElementTree as ET - >>> from music21 import * - >>> meiSnippet = """""" + >>> meiSnippet = '' >>> meiSnippet = ET.fromstring(meiSnippet) >>> mei.base.accidFromElement(meiSnippet) '#' - >>> meiSnippet = """""" + >>> meiSnippet = '' >>> meiSnippet = ET.fromstring(meiSnippet) >>> mei.base.accidFromElement(meiSnippet) '---' @@ -2358,13 +2364,16 @@ def spaceFromElement(elem, slurBundle=None): # pylint: disable=unused-argument A placeholder used to fill an incomplete measure, layer, etc. most often so that the combined duration of the events equals the number of beats in the measure. + Returns a Rest element with hideObjectOnPrint = True + In MEI 2013: pg.440 (455 in PDF) (MEI.shared module) ''' # NOTE: keep this in sync with restFromElement() theDuration = _qlDurationFromAttr(elem.get('dur')) theDuration = makeDuration(theDuration, int(elem.get('dots', 0))) - theSpace = note.SpacerRest(duration=theDuration) + theSpace = note.Rest(duration=theDuration) + theSpace.style.hideObjectOnPrint = True if elem.get(_XMLID) is not None: theSpace.id = elem.get(_XMLID) @@ -2529,13 +2538,13 @@ def harmFromElement(elem, slurBundle=None): def figuredbassFromElement(elem, slurBundle=None): if elem.get(_XMLID): - id = elem.get(_XMLID) + fb_id = elem.get(_XMLID) fb_notation = '' dauer: float = 0 # loop through all child elements and collect tags for subElement in elem.findall('*'): if subElement.tag == f'{MEI_NS}f': - if subElement.text != None: + if subElement.text is not None: if fb_notation != '': fb_notation += f',{subElement.text}' else: @@ -2543,7 +2552,7 @@ def figuredbassFromElement(elem, slurBundle=None): else: if 'extender' in subElement.attrib.keys(): if fb_notation != '': - fb_notation += f',_' + fb_notation += ',_' else: fb_notation = '_' if 'dur.metrical' in subElement.attrib.keys(): @@ -2551,7 +2560,7 @@ def figuredbassFromElement(elem, slurBundle=None): # Generate a FiguredBassIndication object and set the collected information theFbNotation = harmony.FiguredBassIndication(fb_notation) - theFbNotation.id = id + theFbNotation.id = fb_id theFbNotation.duration = duration.Duration(quarterLength=dauer) return theFbNotation @@ -2666,7 +2675,6 @@ def beamFromElement(elem, slurBundle=None): a list of three objects, none of which is a :class:`Beam` or similar. >>> from xml.etree import ElementTree as ET - >>> from music21 import * >>> meiSnippet = """ ... ... @@ -3108,7 +3116,7 @@ def _makeBarlines(elem, staves): bars = bars[1] for eachMeasure in staves.values(): if isinstance(eachMeasure, stream.Measure): - eachMeasure.leftBarline = bars + eachMeasure.leftBarline = deepcopy(bars) if elem.get('right') is not None: bars = _barlineFromAttr(elem.get('right')) @@ -3118,7 +3126,7 @@ def _makeBarlines(elem, staves): bars = bars[0] for eachMeasure in staves.values(): if isinstance(eachMeasure, stream.Measure): - eachMeasure.rightBarline = bars + eachMeasure.rightBarline = deepcopy(bars) return staves @@ -3133,7 +3141,7 @@ def measureFromElement(elem, backupNum, expectedNs, slurBundle=None, activeMeter :param elem: The ```` element to process. :type elem: :class:`~xml.etree.ElementTree.Element` :param int backupNum: A fallback value for the resulting - :class:`~music21.measure.Measure` objects' number attribute. + :class:`~music21.stream.Measure` objects' number attribute. :param expectedNs: A list of the expected @n attributes for the tags in this . If an expected isn't in the , it will be created with a full-measure rest. :type expectedNs: iterable of str @@ -3352,7 +3360,6 @@ def sectionScoreCore(elem, allPartNs, slurBundle, *, # only process elements if this is a
if measureTag == eachElem.tag and sectionTag == elem.tag: backupMeasureNum += 1 - # process all the stuff in the measureResult = measureFromElement(eachElem, backupMeasureNum, allPartNs, slurBundle=slurBundle, @@ -3365,7 +3372,7 @@ def sectionScoreCore(elem, allPartNs, slurBundle, *, inNextThing[eachN] = [] # if we got a left-side barline from the previous measure, use it if nextMeasureLeft is not None: - measureResult[eachN].leftBarline = nextMeasureLeft + measureResult[eachN].leftBarline = deepcopy(nextMeasureLeft) # add this Measure to the Part parsed[eachN].append(measureResult[eachN]) # if we got a barline for the next @@ -3379,8 +3386,13 @@ def sectionScoreCore(elem, allPartNs, slurBundle, *, for allPartObject in localResult['all-part objects']: if isinstance(allPartObject, meter.TimeSignature): activeMeter = allPartObject - for eachN in allPartNs: - inNextThing[eachN].append(allPartObject) + for i, eachN in enumerate(allPartNs): + if i == 0: + to_insert = allPartObject + else: + # a single Music21Object should not exist in multiple parts + to_insert = deepcopy(allPartObject) + inNextThing[eachN].append(to_insert) for eachN in allPartNs: if eachN in localResult: for eachObj in localResult[eachN].values(): @@ -3414,7 +3426,6 @@ def sectionScoreCore(elem, allPartNs, slurBundle, *, # put those into the first Measure object we encounter in this Part # TODO: this is where the Instruments get added # TODO: I think "eachList" really means "each list that will become a Part" - if inNextThing[eachN]: # we have to put Instrument objects just before the Measure to which they apply theInstr = None @@ -3447,13 +3458,11 @@ def sectionScoreCore(elem, allPartNs, slurBundle, *, # must "flatten" everything so it doesn't cause a disaster when we try to make # a Part out of it. for eachObj in eachList: - if eachN in parsed.keys(): - parsed[eachN].append(eachObj) + parsed[eachN].append(eachObj) elif scoreTag == elem.tag: # If this is a , we can just append the result of each
to the # list that will become the Part. - if eachN in parsed.keys(): - parsed[eachN].append(eachList) + parsed[eachN].append(eachList) elif eachElem.tag not in _IGNORE_UNPROCESSED: environLocal.printDebug(_UNPROCESSED_SUBELEMENT.format(eachElem.tag, elem.tag)) @@ -3561,7 +3570,6 @@ def scoreFromElement(elem, slurBundle): # UPDATE: If tags are found, they will also be collected as a separate 'part' to process them later. allPartNs = allPartsPresent(elem) - # This is the actual processing. parsed = sectionScoreCore(elem, allPartNs, slurBundle=slurBundle)[0] @@ -3570,7 +3578,8 @@ def scoreFromElement(elem, slurBundle): # document. Iterating the keys in "parsed" would not preserve the order. environLocal.printDebug('*** making the Score') - # Extract collected information stored in the dict unter th 'fb' key + # Extract collected information stored in the dict unter the 'fb' key + harms: list[dict] | None = None if 'fb' in parsed.keys(): harms = parsed['fb'][0] del parsed['fb'] @@ -3583,14 +3592,14 @@ def scoreFromElement(elem, slurBundle): theScore[i].atSoundingPitch = False for eachObj in parsed[eachN]: theScore[i].append(eachObj) - theScore = stream.Score(theScore) # loop through measures to insert harm elements from harms list at the right offsets - for index, measureOffset in enumerate(theScore.measureOffsetMap().keys()): - hms = harms[index]['fb'] - for h in hms: - theScore.insert(measureOffset + h[0], h[1]) + if harms: + for index, measureOffset in enumerate(theScore.measureOffsetMap().keys()): + hms = harms[index]['fb'] + for h in hms: + theScore.insert(measureOffset + h[0], h[1]) # put slurs in the Score theScore.append(list(slurBundle)) diff --git a/music21/mei/test_base.py b/music21/mei/test_base.py index 012b439b17..b61065c96e 100644 --- a/music21/mei/test_base.py +++ b/music21/mei/test_base.py @@ -199,7 +199,7 @@ def testAllPartsPresent1(self): staffDefs[0].get = mock.MagicMock(return_value='1') elem = mock.MagicMock(spec_set=ETree.Element) elem.findall = mock.MagicMock(return_value=staffDefs) - expected = ['1'] + expected = ['1', 'fb'] actual = base.allPartsPresent(elem) self.assertSequenceEqual(expected, actual) @@ -212,7 +212,7 @@ def testAllPartsPresent2(self): staffDefs[i].get = mock.MagicMock(return_value=str(i + 1)) elem = mock.MagicMock(spec_set=ETree.Element) elem.findall = mock.MagicMock(return_value=staffDefs) - expected = list('1234') + expected = list('1234fb') actual = base.allPartsPresent(elem) self.assertSequenceEqual(expected, actual) @@ -225,7 +225,7 @@ def testAllPartsPresent3(self): staffDefs[i].get = mock.MagicMock(return_value=str((i % 4) + 1)) elem = mock.MagicMock(spec_set=ETree.Element) elem.findall = mock.MagicMock(return_value=staffDefs) - expected = list('1234') + expected = list('1234fb') actual = base.allPartsPresent(elem) self.assertSequenceEqual(expected, actual) diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index d3edc5c2e6..12041d116d 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -3541,8 +3541,9 @@ def insertFiguredBassIndications(self) -> None: Adds relevant figured bass elements collected from the stream.Score to the current stream.Measure object parsed afterwards. - In a MusicXML file tags usually stand before the corresponding note object. - This order will be observed by parseFlatElements() function. Same for multiple figures at one note. + In a MusicXML file tags usually stand before the corresponding + note object. This order will be observed by parseFlatElements() function. + Same for multiple figures at one note. ''' # get the measure range to map the corresponding figuredBass Items measureRange = (self.stream.offset, self.stream.offset + self.stream.highestTime) @@ -4623,7 +4624,7 @@ def figuredBassToXml(self, f: harmony.FiguredBassIndication): #_synchronizeIds(mxFB, f) return mxFB - def _figuresToXml(self, f: harmony.FiguredBassIndication, multipleFigures=False, figureCnt=1, noteIndexInChord=0, chordParent=None): + def _figuresToXml(self, f: harmony.FiguredBassIndication, multipleFigures=False): #do Figure elements #self.addDividerComment('BEGIN: figured-bass') @@ -4663,7 +4664,7 @@ def _figuresToXml(self, f: harmony.FiguredBassIndication, multipleFigures=False, # Check whether the figured-bass tag before already has a tag. # If not create one and set its value. Otherwise update the value - if fbDuration_before.find('duration') == None: + if fbDuration_before.find('duration') is None: newDura = SubElement(fbDuration_before, 'duration') newDura.text = str(dura) else: From 1c11c71c6d86010c8322d889ae9e436e58f0d3a0 Mon Sep 17 00:00:00 2001 From: Moritz Heffter Date: Fri, 24 Mar 2023 00:19:00 +0100 Subject: [PATCH 15/58] mei/base.py recent update to upstream --- music21/mei/base.py | 24 +++++------------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/music21/mei/base.py b/music21/mei/base.py index 7964ad8a8e..2d1d6d4eee 100644 --- a/music21/mei/base.py +++ b/music21/mei/base.py @@ -1197,7 +1197,9 @@ def addSlurs(elem, obj, slurBundle): addedSlur = False def wrapGetByIdLocal(theId): - "Avoid crashing when getByIdLocl() doesn't find the slur" + ''' + Avoid crashing when getByIdLocl() doesn't find the slur + ''' try: slurBundle.getByIdLocal(theId)[0].addSpannedElements(obj) return True @@ -1564,7 +1566,6 @@ def scoreDefFromElement(elem, slurBundle=None): # pylint: disable=unused-argume ... ... ... """ - >>> from music21 import * >>> from xml.etree import ElementTree as ET >>> scoreDef = ET.fromstring(meiDoc) >>> result = mei.base.scoreDefFromElement(scoreDef) @@ -1808,8 +1809,7 @@ def staffDefFromElement(elem, slurBundle=None): # pylint: disable=unused-argume - MEI.shared: clefGrp keySig label layerDef ''' # mapping from tag name to our converter function - tagToFunction = {f'{MEI_NS}clef': clefFromElement, - f'{MEI_NS}meterSig': meterSigFromElement} + tagToFunction = {f'{MEI_NS}clef': clefFromElement} # first make the Instrument post = elem.find(f'{MEI_NS}instrDef') @@ -1860,20 +1860,6 @@ def staffDefFromElement(elem, slurBundle=None): # pylint: disable=unused-argume return post -def meterSigFromElement(elem, slurBundle=None) -> meter.TimeSignature: - ''' Container for information on meter and TimeSignature. - - In MEI 4: (MEI.cmn module) - - :returns: A meter.TimeSignature that is created from the @count und @unit attributes. - - If a xml:id is set it is provided. - ''' - - ts = meter.TimeSignature(f"{elem.get('count')!s}/{elem.get('unit')!s}") - if elem.get('xml:id') is not None: - ts.id = elem.get('xml:id') - return ts def dotFromElement(elem, slurBundle=None): # pylint: disable=unused-argument ''' @@ -2210,7 +2196,7 @@ def noteFromElement(elem, slurBundle=None): f'{MEI_NS}artic': articFromElement, f'{MEI_NS}accid': accidFromElement, f'{MEI_NS}syl': sylFromElement} - + # start with a Note with Pitch theNote = _accidentalFromAttr(elem.get('accid')) theNote = safePitch(elem.get('pname', ''), theNote, elem.get('oct', '')) From 13d0d59558ece7aff8f9582ebc62dd14d9c85356 Mon Sep 17 00:00:00 2001 From: Moritz Heffter Date: Fri, 24 Mar 2023 00:21:17 +0100 Subject: [PATCH 16/58] full merge of mei/base.py --- music21/mei/base.py | 1 - 1 file changed, 1 deletion(-) diff --git a/music21/mei/base.py b/music21/mei/base.py index 2d1d6d4eee..ff447059e7 100644 --- a/music21/mei/base.py +++ b/music21/mei/base.py @@ -3572,7 +3572,6 @@ def scoreFromElement(elem, slurBundle): allPartNs = allPartNs[0:-1] theScore = [stream.Part() for _ in range(len(allPartNs))] - for i, eachN in enumerate(allPartNs): # set "atSoundingPitch" so transposition works theScore[i].atSoundingPitch = False From f065d4c092effe1796efee3800c89e49b3f731b9 Mon Sep 17 00:00:00 2001 From: Moritz Heffter Date: Fri, 24 Mar 2023 00:41:44 +0100 Subject: [PATCH 17/58] edit in mei/test_base.py to pass new fb element in part list --- music21/mei/test_base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/music21/mei/test_base.py b/music21/mei/test_base.py index b61065c96e..1a23c82980 100644 --- a/music21/mei/test_base.py +++ b/music21/mei/test_base.py @@ -212,7 +212,7 @@ def testAllPartsPresent2(self): staffDefs[i].get = mock.MagicMock(return_value=str(i + 1)) elem = mock.MagicMock(spec_set=ETree.Element) elem.findall = mock.MagicMock(return_value=staffDefs) - expected = list('1234fb') + expected = ['1', '2', '3', '4', 'fb'] actual = base.allPartsPresent(elem) self.assertSequenceEqual(expected, actual) @@ -225,7 +225,7 @@ def testAllPartsPresent3(self): staffDefs[i].get = mock.MagicMock(return_value=str((i % 4) + 1)) elem = mock.MagicMock(spec_set=ETree.Element) elem.findall = mock.MagicMock(return_value=staffDefs) - expected = list('1234fb') + expected = ['1', '2', '3', '4', 'fb'] actual = base.allPartsPresent(elem) self.assertSequenceEqual(expected, actual) From 389bb823ad4357de5fabea86b453ee8a66f2e14d Mon Sep 17 00:00:00 2001 From: Moritz Heffter Date: Mon, 27 Mar 2023 22:31:08 +0200 Subject: [PATCH 18/58] suggestions from review added, smaller formatting improvements --- music21/figuredBass/notation.py | 11 ++++------- music21/harmony.py | 10 +++------- music21/mei/base.py | 12 +++++++----- music21/mei/test_base.py | 2 +- music21/musicxml/m21ToXml.py | 23 ++++++++++++----------- music21/musicxml/xmlToM21.py | 32 +++++++++++++++++++------------- 6 files changed, 46 insertions(+), 44 deletions(-) diff --git a/music21/figuredBass/notation.py b/music21/figuredBass/notation.py index 3bb5806606..db797962d1 100644 --- a/music21/figuredBass/notation.py +++ b/music21/figuredBass/notation.py @@ -404,8 +404,7 @@ def _getFigures(self): figuresFromNotaCol = [] - for i in range(len(self.origNumbers)): - number = self.origNumbers[i] + for i, number in enumerate(self.origNumbers): modifierString = self.origModStrings[i] figure = Figure(number, modifierString) figuresFromNotaCol.append(figure) @@ -452,14 +451,12 @@ class Figure(prebase.ProtoM21Object): ''', } - def __init__(self, number=1, modifierString=None, isExtender=None): + def __init__(self, number=1, modifierString=None): self.number = number self.modifierString = modifierString self.modifier = Modifier(modifierString) - if self.number == '_': - self.isExtender = True - else: - self.isExtender = False + # look for extenders underscore + self.isExtender: bool = (self.number == '_') def _reprInternal(self): mod = repr(self.modifier).replace('music21.figuredBass.notation.', '') diff --git a/music21/harmony.py b/music21/harmony.py index e36fba17f4..6855144a76 100644 --- a/music21/harmony.py +++ b/music21/harmony.py @@ -2496,15 +2496,11 @@ def transpose(self: NCT, _value, *, inPlace=False) -> NCT | None: class FiguredBassIndication(Harmony): isFigure = True tstamp = 1 - def __init__(self, figs=None, **keywords): + def __init__(self, figs: list | None=None, **keywords): super().__init__(**keywords) if figs: if isinstance(figs, list): - fig_string = str(figs[0]) - for sf in figs: - fig_string += f',{sf}' - figs = fig_string - #pass + figs = ','.join(figs) else: figs = '' self._fig_notation = notation.Notation(figs) @@ -2512,7 +2508,7 @@ def __init__(self, figs=None, **keywords): @property def fig_notation(self) -> notation.Notation: return self._fig_notation - + @fig_notation.setter def fig_notation(self, figs): self._fig_notation = notation.Notation(figs) diff --git a/music21/mei/base.py b/music21/mei/base.py index ff447059e7..797bad676e 100644 --- a/music21/mei/base.py +++ b/music21/mei/base.py @@ -469,7 +469,8 @@ def allPartsPresent(scoreElem) -> tuple[str, ...]: raise MeiValidityError(_SEEMINGLY_NO_PARTS) # Get information of possible tags in the score. If there are tags prepare a list to - # store them and process them later. TODO: Maybe to be put in a separate function e.g. like allPartsPresent + # store them and process them later. + # TODO: Maybe to be put in a separate function e.g. like allPartsPresent figuredBassQuery = f'.//{MEI_NS}fb' if scoreElem.findall(figuredBassQuery): environLocal.printDebug('harm tag found!') @@ -2504,7 +2505,7 @@ def chordFromElement(elem, slurBundle=None): return theChord -def harmFromElement(elem, slurBundle=None): +def harmFromElement(elem, slurBundle=None) -> tuple: # other tags than to be added… tagToFunction = {f'{MEI_NS}fb': figuredbassFromElement} @@ -2522,10 +2523,10 @@ def harmFromElement(elem, slurBundle=None): return fb_harmony_tag -def figuredbassFromElement(elem, slurBundle=None): +def figuredbassFromElement(elem, slurBundle=None) -> harmony.FiguredBassIndication: if elem.get(_XMLID): fb_id = elem.get(_XMLID) - fb_notation = '' + fb_notation: str = '' dauer: float = 0 # loop through all child elements and collect tags for subElement in elem.findall('*'): @@ -3553,7 +3554,8 @@ def scoreFromElement(elem, slurBundle): # Get a tuple of all the @n attributes for the tags in this score. Each tag # corresponds to what will be a music21 Part. - # UPDATE: If tags are found, they will also be collected as a separate 'part' to process them later. + # UPDATE: If tags are found, they will also be collected as a separate 'part' + # to process them later. allPartNs = allPartsPresent(elem) # This is the actual processing. diff --git a/music21/mei/test_base.py b/music21/mei/test_base.py index 1a23c82980..c13981341d 100644 --- a/music21/mei/test_base.py +++ b/music21/mei/test_base.py @@ -3929,7 +3929,7 @@ def testMeasureUnit1(self, mockVoice, mockMeasure, elem.append(eachStaff) # @n="4" is in "expectedNs" but we're leaving it out as part of the test backupNum = 900 # should be ignored by measureFromElement() - expectedNs = ['1', '2', '3', '4'] + expectedNs = ['1', '2', '3', '4', 'fb'] slurBundle = mock.MagicMock(name='slurBundle') activeMeter = mock.MagicMock(name='activeMeter') activeMeter.barDuration = duration.Duration(4.0) diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index 12041d116d..44f88dea90 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -1482,7 +1482,7 @@ def __init__(self, score: stream.Score | None = None, makeNotation: bool = True) self.textBoxes = None self.highestTime = 0.0 self.fb_part = -1 - self.fbis_dict = {} + self.fbis_dict: dict = {} self.currentDivisions = defaults.divisionsPerQuarter self.refStreamOrTimeRange = [0.0, self.highestTime] @@ -3211,6 +3211,7 @@ def __init__(self, self.spannerBundle = parent.spannerBundle self.objectSpannerBundle = self.spannerBundle # will change for each element. + self._fbiBefore: tuple[float, Element] = () def parse(self): ''' @@ -3369,7 +3370,7 @@ def parseFlatElements( # Prepare the iteration by offsets. If there are FiguredBassIndication objects # first group them in one list together with their note, instead of handling them # seperately. O - groupedObjList = [] + groupedObjList: list = [] for els in objIterator: #print(els) if len(groupedObjList) > 0: @@ -3540,7 +3541,7 @@ def insertFiguredBassIndications(self) -> None: ''' Adds relevant figured bass elements collected from the stream.Score to the current stream.Measure object parsed afterwards. - + In a MusicXML file tags usually stand before the corresponding note object. This order will be observed by parseFlatElements() function. Same for multiple figures at one note. @@ -4625,15 +4626,15 @@ def figuredBassToXml(self, f: harmony.FiguredBassIndication): return mxFB def _figuresToXml(self, f: harmony.FiguredBassIndication, multipleFigures=False): - #do Figure elements - #self.addDividerComment('BEGIN: figured-bass') + # do Figure elements + # self.addDividerComment('BEGIN: figured-bass') mxFB = Element('figured-bass') dura = 0 for fig in f.fig_notation.figuresFromNotationColumn: mxFigure = SubElement(mxFB, 'figure') - #get only the fbnumber without prefixes or suffixes + # get only the fbnumber without prefixes or suffixes mxFNumber = SubElement(mxFigure, 'figure-number') if fig.number: if fig.number == '_': @@ -4644,8 +4645,8 @@ def _figuresToXml(self, f: harmony.FiguredBassIndication, multipleFigures=False) mxFNumber.text = str(fig.number) else: mxFNumber.text = '' - - #modifiers are either handled as prefixes or suffixes here + + # modifiers are either handled as prefixes or suffixes here fbModifier = fig.modifierString if fbModifier: mxModifier = SubElement(mxFigure, 'prefix') @@ -4656,7 +4657,8 @@ def _figuresToXml(self, f: harmony.FiguredBassIndication, multipleFigures=False) # If we have multiple figures we have to set a tag # and update the tag one before. if multipleFigures: - dura = round((f.offset - self.offsetInMeasure - self.offsetFiguresInMeasure) * self.currentDivisions) + dura = round((f.offset - self.offsetInMeasure - self.offsetFiguresInMeasure) + * self.currentDivisions) self.offsetFiguresInMeasure = f.offset - self.offsetInMeasure # Update figures-bass tag before @@ -4679,13 +4681,12 @@ def _figuresToXml(self, f: harmony.FiguredBassIndication, multipleFigures=False) self.offsetFiguresInMeasure = 0.0 if self.tempFigureDuration > 0: - #print(f.quarterLength * self.currentDivisions) + # print(f.quarterLength * self.currentDivisions) mxFbDuration = SubElement(mxFB, 'duration') mxFbDuration.text = str(round(self.tempFigureDuration)) self.tempFigureDuration = 0.0 return mxFB - #self.addDividerComment('END: figured-bass') def durationXml(self, dur: duration.Duration): # noinspection PyShadowingNames diff --git a/music21/musicxml/xmlToM21.py b/music21/musicxml/xmlToM21.py index d24c47b1d8..43ab8ec24f 100644 --- a/music21/musicxml/xmlToM21.py +++ b/music21/musicxml/xmlToM21.py @@ -926,7 +926,7 @@ def xmlRootToScore(self, mxScore, inputM21=None): if part is not None: # for instance, in partStreams s.coreInsert(0.0, part) self.m21PartObjectsById[partId] = part - + if self.fbis: for fbi in self.fbis: s.insert(fbi[0], fbi[1]) @@ -5183,28 +5183,28 @@ def xmlToFiguredBass(self, mxFiguredBass): for figure in mxFiguredBass.findall('*'): # TODO: suffixes are ignored at the moment for el in figure.findall('*'): - #print(' ', el) fb_number: str = '' fb_prefix: str = '' if el.tag == 'figure-number': if el.text: fb_number = el.text - if figure.findall('prefix'): - for prefix in figure.findall('prefix'): - - if prefix.text: - fb_prefix = modifiersDictXmlToM21[prefix.text] - # put prefix and number together - if fb_prefix + fb_number != '': - fb_strings.append(fb_prefix + fb_number) + + # Get prefix and/or suffix. + # The function returns an empty string if nothing is found. + fb_prefix = self._getFigurePrefixOrSuffix(figure, 'prefix') + fb_suffix = self._getFigurePrefixOrSuffix(figure, 'suffix') + + # put prefix/suffix and number together + if fb_prefix + fb_number + fb_suffix != '': + fb_strings.append(fb_prefix + fb_number + fb_suffix) if el.tag == 'extend': if 'type' in el.attrib.keys(): if el.attrib['type'] == 'continue': fb_strings.append('_') - # If a is given, this usually means that there are multiple figures for a single note. - # We have to look for offsets here. + # If a is given, this usually means that there are multiple figures + # for a single note. We have to look for offsets here. if figure.tag == 'duration': d = self.xmlToDuration(mxFiguredBass) if self.lastFigureDuration > 0: @@ -5214,7 +5214,6 @@ def xmlToFiguredBass(self, mxFiguredBass): offsetFbi = self.offsetMeasureNote self.lastFigureDuration = d.quarterLength - #print('ü', fb_strings) fb_string = sep.join(fb_strings) fbi = harmony.FiguredBassIndication(fb_string) # If a duration is provided, set length of the FigureBassIndication @@ -5224,6 +5223,13 @@ def xmlToFiguredBass(self, mxFiguredBass): # self.parent.appendFbis(fbi, offsetFbi) + def _getFigurePrefixOrSuffix(self, figure, presuf: str='prefix') -> str: + if figure.findall(presuf): + for fix in figure.findall(presuf): + if fix.text: + return modifiersDictXmlToM21[fix.text] + return '' + def xmlDirection(self, mxDirection): ''' convert a tag to one or more expressions, metronome marks, etc. From 2fb6285834f4f6fe9283164d66be71e1d53bc38a Mon Sep 17 00:00:00 2001 From: Moritz Heffter Date: Tue, 28 Mar 2023 00:16:59 +0200 Subject: [PATCH 19/58] adapted tests in mei/test_base-py and doctest for figuredBass/notation.py --- music21/figuredBass/notation.py | 20 ++++++------- music21/mei/test_base.py | 51 +++++++++++++++++++-------------- 2 files changed, 39 insertions(+), 32 deletions(-) diff --git a/music21/figuredBass/notation.py b/music21/figuredBass/notation.py index db797962d1..7967a958df 100644 --- a/music21/figuredBass/notation.py +++ b/music21/figuredBass/notation.py @@ -129,11 +129,11 @@ class Notation(prebase.ProtoM21Object): , ) >>> n1.figures[0] - > + hasExt: False> >>> n1.figures[1] - > + hasExt: False> >>> n1.figures[2] - > + hasExt: False> Here, a stand-alone '#' is being passed to Notation. @@ -146,9 +146,9 @@ class Notation(prebase.ProtoM21Object): (, ) >>> n2.figures[0] - > + hasExt: False> >>> n2.figures[1] - > + hasExt: False> Now, a stand-alone b is being passed to Notation as part of a larger notationColumn. @@ -161,9 +161,9 @@ class Notation(prebase.ProtoM21Object): (, ) >>> n3.figures[0] - > + hasExt: False> >>> n3.figures[1] - > + hasExt: False> ''' _DOC_ORDER = ['notationColumn', 'figureStrings', 'numbers', 'modifiers', 'figures', 'origNumbers', 'origModStrings', 'modifierStrings'] @@ -388,9 +388,9 @@ def _getFigures(self): >>> from music21.figuredBass import notation as n >>> notation2 = n.Notation('-6,-') #__init__ method calls _getFigures() >>> notation2.figures[0] - > + hasExt: False> >>> notation2.figures[1] - > + hasExt: False> ''' figures = [] @@ -426,7 +426,7 @@ class Figure(prebase.ProtoM21Object): >>> from music21.figuredBass import notation >>> f1 = notation.Figure(4, '+') >>> f1 - > + hasExt: False> >>> f1.number 4 diff --git a/music21/mei/test_base.py b/music21/mei/test_base.py index c13981341d..a8904b62d0 100644 --- a/music21/mei/test_base.py +++ b/music21/mei/test_base.py @@ -3936,10 +3936,12 @@ def testMeasureUnit1(self, mockVoice, mockMeasure, # this must match Measure.duration.quarterLength # prepare the mock Measure objects returned by mockMeasure mockMeasRets = [mock.MagicMock(name=f'Measure {i + 1}') for i in range(4)] - expected = mockMeasRets # finish preparing "expected" below... for meas in mockMeasRets: meas.duration = mock.MagicMock(spec_set=duration.Duration) meas.duration.quarterLength = 4.0 # must match activeMeter.barDuration.quarterLength + # append figured bass stuff + mockMeasRets.append({'fb': []}) + expected = mockMeasRets # finish preparing "expected" below... mockMeasure.side_effect = lambda *x, **y: mockMeasRets.pop(0) # prepare mock of _makeBarlines() which returns "staves" mockMakeBarlines.side_effect = lambda elem, staves: staves @@ -3961,7 +3963,7 @@ def testMeasureUnit1(self, mockVoice, mockMeasure, for eachStaff in innerStaffs: mockStaffFE.assert_any_call(eachStaff, slurBundle=slurBundle) # ensure Measure.__init__() was called properly - self.assertEqual(len(expected), mockMeasure.call_count) + self.assertEqual(len(expected) -1, mockMeasure.call_count) for i in range(len(innerStaffs)): mockMeasure.assert_any_call(i, number=int(elem.get('n'))) mockMeasure.assert_any_call([mockVoice.return_value], number=int(elem.get('n'))) @@ -3997,14 +3999,14 @@ def testMeasureIntegration1(self): elem.append(eachStaff) # @n="4" is in "expectedNs" but we're leaving it out as part of the test backupNum = 900 # should be ignored by measureFromElement() - expectedNs = ['1', '2', '3', '4'] + expectedNs = ['1', '2', '3', '4', 'fb'] slurBundle = spanner.SpannerBundle() activeMeter = meter.TimeSignature('8/8') # bet you thought this would be 4/4, eh? actual = base.measureFromElement(elem, backupNum, expectedNs, slurBundle, activeMeter) # ensure the right number and @n of parts - self.assertEqual(4, len(actual.keys())) + self.assertEqual(5, len(actual.keys())) for eachN in expectedNs: self.assertTrue(eachN in actual) # ensure the measure number is set properly, @@ -4053,7 +4055,7 @@ def testMeasureUnit2(self, mockVoice, mockMeasure, elem.append(eachStaff) # @n="4" is in "expectedNs" but we're leaving it out as part of the test backupNum = 900 # should be used by measureFromElement() - expectedNs = ['1', '2', '3', '4'] + expectedNs = ['1', '2', '3', '4', 'fb'] slurBundle = mock.MagicMock(name='slurBundle') activeMeter = mock.MagicMock(name='activeMeter') # this must be longer than Measure.duration.quarterLength @@ -4065,6 +4067,7 @@ def testMeasureUnit2(self, mockVoice, mockMeasure, for meas in mockMeasRets: meas.duration = mock.MagicMock(spec_set=duration.Duration) meas.duration.quarterLength = base._DUR_ATTR_DICT[None] # must be _DUR_ATTR_DICT[None] + mockMeasRets.append({'fb': []}) mockMeasure.side_effect = lambda *x, **y: mockMeasRets.pop(0) # prepare mock of _makeBarlines() which returns "staves" mockMakeBarlines.side_effect = lambda elem, staves: staves @@ -4086,7 +4089,7 @@ def testMeasureUnit2(self, mockVoice, mockMeasure, for eachStaff in innerStaffs: mockStaffFE.assert_any_call(eachStaff, slurBundle=slurBundle) # ensure Measure.__init__() was called properly - self.assertEqual(len(expected), mockMeasure.call_count) + self.assertEqual(len(expected) - 1, mockMeasure.call_count) for i in range(len(innerStaffs)): mockMeasure.assert_any_call(i, number=backupNum) mockMeasure.assert_any_call([mockVoice.return_value], number=backupNum) @@ -4120,14 +4123,14 @@ def testMeasureIntegration2(self): elem.append(eachStaff) # @n="4" is in "expectedNs" but we're leaving it out as part of the test backupNum = 900 # should be used by measureFromElement() - expectedNs = ['1', '2', '3', '4'] + expectedNs = ['1', '2', '3', '4', 'fb'] slurBundle = spanner.SpannerBundle() activeMeter = meter.TimeSignature('8/8') # bet you thought this would be 4/4, eh? actual = base.measureFromElement(elem, backupNum, expectedNs, slurBundle, activeMeter) # ensure the right number and @n of parts (we expect one additional key, for the "rptboth") - self.assertEqual(5, len(actual.keys())) + self.assertEqual(6, len(actual.keys())) for eachN in expectedNs: self.assertTrue(eachN in actual) self.assertTrue('next @left' in actual) @@ -4137,15 +4140,18 @@ def testMeasureIntegration2(self): # (Note we can test all four parts together this time--- # the fourth should be indistinguishable) for eachN in expectedNs: - self.assertEqual(backupNum, actual[eachN].number) - self.assertEqual(2, len(actual[eachN])) # first the Note, then the Barline - self.assertIsInstance(actual[eachN][0], stream.Voice) - self.assertEqual(1, len(actual[eachN][0])) - self.assertIsInstance(actual[eachN][0][0], note.Rest) - self.assertEqual(activeMeter.barDuration.quarterLength, + if isinstance(actual[eachN], stream.Measure): + self.assertEqual(backupNum, actual[eachN].number) + self.assertEqual(2, len(actual[eachN])) # first the Note, then the Barline + self.assertIsInstance(actual[eachN][0], stream.Voice) + self.assertEqual(1, len(actual[eachN][0])) + self.assertIsInstance(actual[eachN][0][0], note.Rest) + self.assertEqual(activeMeter.barDuration.quarterLength, actual['4'][0][0].duration.quarterLength) - self.assertIsInstance(actual[eachN].rightBarline, bar.Repeat) - self.assertEqual('final', actual[eachN].rightBarline.type) + self.assertIsInstance(actual[eachN].rightBarline, bar.Repeat) + self.assertEqual('final', actual[eachN].rightBarline.type) + else: + self.assertEqual([], actual[eachN]['fb']) @mock.patch('music21.mei.base.staffFromElement') @mock.patch('music21.mei.base._correctMRestDurs') @@ -4170,7 +4176,7 @@ def testMeasureUnit3a(self, mockStaffDefFE, mockMeasure, staffElem = ETree.Element(staffTag, attrib={'n': '1'}) elem.append(staffElem) backupNum = 900 # should be ignored by measureFromElement() - expectedNs = ['1'] + expectedNs = ['1', 'fb'] slurBundle = mock.MagicMock(name='slurBundle') activeMeter = mock.MagicMock(name='activeMeter') # this must match Measure.duration.quarterLength @@ -4186,10 +4192,10 @@ def testMeasureUnit3a(self, mockStaffDefFE, mockMeasure, # prepare mock of staffDefFromElement() mockStaffDefFE.return_value = {'clef': mock.MagicMock(name='SomeClef')} # prepare the expected return value - expected = {'1': mockMeasure.return_value} + expected = {'1': mockMeasure.return_value, 'fb': {'fb': []}} actual = base.measureFromElement(elem, backupNum, expectedNs, slurBundle, activeMeter) - + actual['fb'] = {'fb': []} self.assertDictEqual(expected, actual) # ensure staffFromElement() was called properly mockStaffFE.assert_called_once_with(staffElem, slurBundle=slurBundle) @@ -4224,7 +4230,7 @@ def testMeasureUnit3b(self, mockEnviron, mockMeasure, staffElem = ETree.Element(staffTag, attrib={'n': '1'}) elem.append(staffElem) backupNum = 900 # should be ignored by measureFromElement() - expectedNs = ['1'] + expectedNs = ['1', 'fb'] slurBundle = mock.MagicMock(name='slurBundle') activeMeter = mock.MagicMock(name='activeMeter') # this must match Measure.duration.quarterLength @@ -4238,9 +4244,10 @@ def testMeasureUnit3b(self, mockEnviron, mockMeasure, # prepare mock of staffFromElement(), which just needs to return several unique things mockStaffFE.return_value = 'staffFromElement() return value' # prepare the expected return value - expected = {'1': mockMeasure.return_value} + expected = {'1': mockMeasure.return_value, 'fb': {'fb': []}} actual = base.measureFromElement(elem, backupNum, expectedNs, slurBundle, activeMeter) + actual['fb'] = {'fb': []} self.assertDictEqual(expected, actual) # ensure staffFromElement() was called properly @@ -4283,7 +4290,7 @@ def testMeasureIntegration3(self): actual = base.measureFromElement(elem, backupNum, expectedNs, slurBundle, activeMeter) # ensure the right number and @n of parts - self.assertEqual(['1'], list(actual.keys())) + self.assertEqual(['1', 'fb'], list(actual.keys())) # ensure the Measure has its expected Voice, BassClef, and Instrument self.assertEqual(backupNum, actual['1'].number) self.assertEqual(2, len(actual['1'])) From 4e5fdec0f04ca39cd0b50e2cf19f46d711baebbe Mon Sep 17 00:00:00 2001 From: Moritz Heffter Date: Tue, 28 Mar 2023 00:37:38 +0200 Subject: [PATCH 20/58] simplification of a if else statement from review --- music21/mei/base.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/music21/mei/base.py b/music21/mei/base.py index 797bad676e..d0e009edc7 100644 --- a/music21/mei/base.py +++ b/music21/mei/base.py @@ -2527,25 +2527,21 @@ def figuredbassFromElement(elem, slurBundle=None) -> harmony.FiguredBassIndicati if elem.get(_XMLID): fb_id = elem.get(_XMLID) fb_notation: str = '' + fb_notation_list: list[str] = [] dauer: float = 0 # loop through all child elements and collect tags for subElement in elem.findall('*'): if subElement.tag == f'{MEI_NS}f': if subElement.text is not None: - if fb_notation != '': - fb_notation += f',{subElement.text}' - else: - fb_notation = subElement.text + fb_notation_list.append(subElement.text) else: if 'extender' in subElement.attrib.keys(): - if fb_notation != '': - fb_notation += ',_' - else: - fb_notation = '_' + fb_notation_list.append('_') if 'dur.metrical' in subElement.attrib.keys(): dauer = float(subElement.attrib['dur.metrical']) # Generate a FiguredBassIndication object and set the collected information + fb_notation = ",".join(fb_notation_list) theFbNotation = harmony.FiguredBassIndication(fb_notation) theFbNotation.id = fb_id theFbNotation.duration = duration.Duration(quarterLength=dauer) From ad8b09dc9cccae646b9617fd5c9a556292663e18 Mon Sep 17 00:00:00 2001 From: Moritz Heffter Date: Tue, 28 Mar 2023 20:15:52 +0200 Subject: [PATCH 21/58] finally cleaned up for tests and flake8 and mypy --- music21/figuredBass/notation.py | 67 ++++++++++++++++----------------- music21/harmony.py | 13 +++++-- music21/mei/base.py | 18 ++++----- music21/mei/test_base.py | 2 +- music21/musicxml/m21ToXml.py | 47 ++++++++++++----------- music21/musicxml/xmlToM21.py | 20 +++++----- 6 files changed, 85 insertions(+), 82 deletions(-) diff --git a/music21/figuredBass/notation.py b/music21/figuredBass/notation.py index 7967a958df..c260c8efa5 100644 --- a/music21/figuredBass/notation.py +++ b/music21/figuredBass/notation.py @@ -30,30 +30,27 @@ (2,): (6, 4, 2), } -prefixes = ["+", "#", "++", "##"] -suffixes = ["\\"] - -modifiersDictXmlToM21 = { - "sharp": "#", - "flat": "b", - "natural": "\u266e", - "double-sharp": "##", - "flat-flat": "bb", - "backslash": "\\" - } - -modifiersDictM21ToXml = { - "#": "sharp", - "b": "flat", - "##": "double-sharp", - "bb": "flat-flat", - "\\": "backslash", - "+": "sharp", - '\u266f': 'sharp', - '\u266e': 'natural', - '\u266d': 'flat', - '\u20e5': 'sharp' -} +prefixes = ['+', '#', '++', '##'] +suffixes = ['\\'] + +modifiersDictXmlToM21 = {'sharp': '#', + 'flat': 'b', + 'natural': '\u266e', + 'double-sharp': '##', + 'flat-flat': 'bb', + 'backslash': '\\', + 'slash': '/'} + +modifiersDictM21ToXml = {'#': 'sharp', + 'b': 'flat', + '##': 'double-sharp', + 'bb': 'flat-flat', + '\\': 'backslash', + '+': 'sharp', + '\u266f': 'sharp', + '\u266e': 'natural', + '\u266d': 'flat', + '\u20e5': 'sharp'} class Notation(prebase.ProtoM21Object): ''' @@ -129,11 +126,11 @@ class Notation(prebase.ProtoM21Object): , ) >>> n1.figures[0] - hasExt: False> + hasExt: False> >>> n1.figures[1] - hasExt: False> + hasExt: False> >>> n1.figures[2] - hasExt: False> + hasExt: False> Here, a stand-alone '#' is being passed to Notation. @@ -146,9 +143,9 @@ class Notation(prebase.ProtoM21Object): (, ) >>> n2.figures[0] - hasExt: False> + hasExt: False> >>> n2.figures[1] - hasExt: False> + hasExt: False> Now, a stand-alone b is being passed to Notation as part of a larger notationColumn. @@ -161,9 +158,9 @@ class Notation(prebase.ProtoM21Object): (, ) >>> n3.figures[0] - hasExt: False> + hasExt: False> >>> n3.figures[1] - hasExt: False> + hasExt: False> ''' _DOC_ORDER = ['notationColumn', 'figureStrings', 'numbers', 'modifiers', 'figures', 'origNumbers', 'origModStrings', 'modifierStrings'] @@ -388,9 +385,9 @@ def _getFigures(self): >>> from music21.figuredBass import notation as n >>> notation2 = n.Notation('-6,-') #__init__ method calls _getFigures() >>> notation2.figures[0] - hasExt: False> + hasExt: False> >>> notation2.figures[1] - hasExt: False> + hasExt: False> ''' figures = [] @@ -426,7 +423,7 @@ class Figure(prebase.ProtoM21Object): >>> from music21.figuredBass import notation >>> f1 = notation.Figure(4, '+') >>> f1 - hasExt: False> + hasExt: False> >>> f1.number 4 @@ -455,7 +452,7 @@ def __init__(self, number=1, modifierString=None): self.number = number self.modifierString = modifierString self.modifier = Modifier(modifierString) - # look for extenders underscore + # look for extenders underscore self.isExtender: bool = (self.number == '_') def _reprInternal(self): diff --git a/music21/harmony.py b/music21/harmony.py index 6855144a76..e3f2c16636 100644 --- a/music21/harmony.py +++ b/music21/harmony.py @@ -2496,14 +2496,19 @@ def transpose(self: NCT, _value, *, inPlace=False) -> NCT | None: class FiguredBassIndication(Harmony): isFigure = True tstamp = 1 - def __init__(self, figs: list | None=None, **keywords): + def __init__(self, figs: str | list | None = None, **keywords): super().__init__(**keywords) if figs: if isinstance(figs, list): - figs = ','.join(figs) + _figs: str = ','.join(figs) + elif isinstance(figs, str): + if ',' in figs: + _figs = figs + else: + _figs = ','.join(figs) else: - figs = '' - self._fig_notation = notation.Notation(figs) + _figs = '' + self._fig_notation = notation.Notation(_figs) @property def fig_notation(self) -> notation.Notation: diff --git a/music21/mei/base.py b/music21/mei/base.py index d0e009edc7..de09c8e336 100644 --- a/music21/mei/base.py +++ b/music21/mei/base.py @@ -468,8 +468,8 @@ def allPartsPresent(scoreElem) -> tuple[str, ...]: if not partNs: raise MeiValidityError(_SEEMINGLY_NO_PARTS) - # Get information of possible tags in the score. If there are tags prepare a list to - # store them and process them later. + # Get information of possible tags in the score. If there are tags prepare a list to + # store them and process them later. # TODO: Maybe to be put in a separate function e.g. like allPartsPresent figuredBassQuery = f'.//{MEI_NS}fb' if scoreElem.findall(figuredBassQuery): @@ -2510,7 +2510,7 @@ def harmFromElement(elem, slurBundle=None) -> tuple: tagToFunction = {f'{MEI_NS}fb': figuredbassFromElement} fb_harmony_tag: tuple = () - + # Collect all elements in a measure and go throug extenders # tstamp has to be used as a duration marker between two elements @@ -2541,11 +2541,11 @@ def figuredbassFromElement(elem, slurBundle=None) -> harmony.FiguredBassIndicati dauer = float(subElement.attrib['dur.metrical']) # Generate a FiguredBassIndication object and set the collected information - fb_notation = ",".join(fb_notation_list) + fb_notation = ','.join(fb_notation_list) theFbNotation = harmony.FiguredBassIndication(fb_notation) theFbNotation.id = fb_id theFbNotation.duration = duration.Duration(quarterLength=dauer) - + return theFbNotation def clefFromElement(elem, slurBundle=None): # pylint: disable=unused-argument @@ -3225,7 +3225,7 @@ def measureFromElement(elem, backupNum, expectedNs, slurBundle=None, activeMeter # We must insert() these objects because a signals its changes for the # *start* of the in which it appears. staves[whichN].insert(0, eachObj) - + # Add objects to the staves dict staves['fb'] = harmElements # other childs of tags can be added here… @@ -3550,7 +3550,7 @@ def scoreFromElement(elem, slurBundle): # Get a tuple of all the @n attributes for the tags in this score. Each tag # corresponds to what will be a music21 Part. - # UPDATE: If tags are found, they will also be collected as a separate 'part' + # UPDATE: If tags are found, they will also be collected as a separate 'part' # to process them later. allPartNs = allPartsPresent(elem) @@ -3568,7 +3568,7 @@ def scoreFromElement(elem, slurBundle): harms = parsed['fb'][0] del parsed['fb'] allPartNs = allPartNs[0:-1] - + theScore = [stream.Part() for _ in range(len(allPartNs))] for i, eachN in enumerate(allPartNs): # set "atSoundingPitch" so transposition works @@ -3576,7 +3576,7 @@ def scoreFromElement(elem, slurBundle): for eachObj in parsed[eachN]: theScore[i].append(eachObj) theScore = stream.Score(theScore) - + # loop through measures to insert harm elements from harms list at the right offsets if harms: for index, measureOffset in enumerate(theScore.measureOffsetMap().keys()): diff --git a/music21/mei/test_base.py b/music21/mei/test_base.py index a8904b62d0..493fb8bc6f 100644 --- a/music21/mei/test_base.py +++ b/music21/mei/test_base.py @@ -3963,7 +3963,7 @@ def testMeasureUnit1(self, mockVoice, mockMeasure, for eachStaff in innerStaffs: mockStaffFE.assert_any_call(eachStaff, slurBundle=slurBundle) # ensure Measure.__init__() was called properly - self.assertEqual(len(expected) -1, mockMeasure.call_count) + self.assertEqual(len(expected) - 1, mockMeasure.call_count) for i in range(len(innerStaffs)): mockMeasure.assert_any_call(i, number=int(elem.get('n'))) mockMeasure.assert_any_call([mockVoice.return_value], number=int(elem.get('n'))) diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index 44f88dea90..b98dc43d99 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -3211,7 +3211,7 @@ def __init__(self, self.spannerBundle = parent.spannerBundle self.objectSpannerBundle = self.spannerBundle # will change for each element. - self._fbiBefore: tuple[float, Element] = () + self._fbiBefore: tuple = () def parse(self): ''' @@ -3227,7 +3227,7 @@ def parse(self): self.setMxPrint() self.setMxAttributesObjectForStartOfMeasure() self.setLeftBarline() - # Look for FiguredBassIndications and add them to the local copy of the measure + # Look for FiguredBassIndications and add them to the local copy of the measure # and after the other elements if self.parent.fbis: self.insertFiguredBassIndications() @@ -3372,14 +3372,13 @@ def parseFlatElements( # seperately. O groupedObjList: list = [] for els in objIterator: - #print(els) if len(groupedObjList) > 0: if len(els) == 1 and isinstance(els[0], harmony.FiguredBassIndication): - #print('**multiple fb found**') + # print('**multiple fb found**') groupedObjList[-1].append(els[0]) continue groupedObjList.append(els) - #print('üüü', groupedObjList) + for objGroup in groupedObjList: groupOffset = m.elementOffset(objGroup[0]) offsetToMoveForward = groupOffset - self.offsetInMeasure @@ -3543,19 +3542,23 @@ def insertFiguredBassIndications(self) -> None: current stream.Measure object parsed afterwards. In a MusicXML file tags usually stand before the corresponding - note object. This order will be observed by parseFlatElements() function. + note object. This order will be observed by parseFlatElements() function. Same for multiple figures at one note. ''' # get the measure range to map the corresponding figuredBass Items - measureRange = (self.stream.offset, self.stream.offset + self.stream.highestTime) - - # Look if there are figures in the current measure and insert them - # to add them later. - for o, f in self.parent.fbis.items(): - if o >= measureRange[0] and o < measureRange[1]: - for fbi in f: - self.stream.insert(o - self.stream.offset, fbi) - + if self.stream: + measureRange = (self.stream.offset, self.stream.offset + self.stream.highestTime) + + # Look if there are figures in the current measure and insert them + # to add them later. + if self.parent and self.parent.fbis: + for o, f in self.parent.fbis.items(): + if o >= measureRange[0] and o < measureRange[1]: + for fbi in f: + self.stream.insert(o - self.stream.offset, fbi) + else: + raise MusicXMLExportException('No stream found') + def _hasRelatedSpanners(self, obj) -> bool: ''' returns True if and only if: @@ -4618,13 +4621,13 @@ def figuredBassToXml(self, f: harmony.FiguredBassIndication): # Hand the FiguredBassIndication over to the helper function. mxFB = self._figuresToXml(f, multipleFigures=multipleFigures) - + # Append it to xmlRoot self.xmlRoot.append(mxFB) self._fbiBefore = (f.offset, mxFB) - #_synchronizeIds(mxFB, f) + return mxFB - + def _figuresToXml(self, f: harmony.FiguredBassIndication, multipleFigures=False): # do Figure elements # self.addDividerComment('BEGIN: figured-bass') @@ -4652,7 +4655,7 @@ def _figuresToXml(self, f: harmony.FiguredBassIndication, multipleFigures=False) mxModifier = SubElement(mxFigure, 'prefix') mxModifier.text = modifiersDictM21ToXml[fbModifier] - # look for information on duration. If duration is 0 + # look for information on duration. If duration is 0 # jump over the tag # If we have multiple figures we have to set a tag # and update the tag one before. @@ -4670,7 +4673,7 @@ def _figuresToXml(self, f: harmony.FiguredBassIndication, multipleFigures=False) newDura = SubElement(fbDuration_before, 'duration') newDura.text = str(dura) else: - # If dura is set to 0 skip the update process. + # If dura is set to 0 skip the update process. # This happens e.g. at the beginning of a piece. # Otherwise an already set duration tag is resetted to 0 which is not wanted. if dura > 0: @@ -4679,13 +4682,13 @@ def _figuresToXml(self, f: harmony.FiguredBassIndication, multipleFigures=False) self.tempFigureDuration = dura else: self.offsetFiguresInMeasure = 0.0 - + if self.tempFigureDuration > 0: # print(f.quarterLength * self.currentDivisions) mxFbDuration = SubElement(mxFB, 'duration') mxFbDuration.text = str(round(self.tempFigureDuration)) self.tempFigureDuration = 0.0 - + return mxFB def durationXml(self, dur: duration.Duration): diff --git a/music21/musicxml/xmlToM21.py b/music21/musicxml/xmlToM21.py index 43ab8ec24f..3a509e6015 100644 --- a/music21/musicxml/xmlToM21.py +++ b/music21/musicxml/xmlToM21.py @@ -2364,7 +2364,6 @@ def appendFbis(self, fbi, measureOffset): self.parent.fbis.append((absOffset, fbi)) else: self.parent.fbis = [(absOffset, fbi)] - #print(self.parent.fbis) # ----------------------------------------------------------------------------- class MeasureParser(XMLParserBase): @@ -2464,10 +2463,10 @@ def __init__(self, # what is the offset in the measure of the current note position? self.offsetMeasureNote: OffsetQL = 0.0 - + # Offset Calc if more than one figure is set under a single note self.lastFigureDuration = 0 - + # keep track of the last rest that was added with a forward tag. # there are many pieces that end with incomplete measures that # older versions of Finale put a forward tag at the end, but this @@ -2865,12 +2864,12 @@ def xmlToNote(self, mxNote: ET.Element) -> None: # only increment Chords after completion self.offsetMeasureNote += offsetIncrement self.endedWithForwardTag = None - + # reset offset for figures. This is needed to put in # multiple FiguredBassIndications at one note at the right offset. - # Musicxml puts tags immediately before a tag, + # Musicxml puts tags immediately before a tag, # which means that we have to reset a given offset duration of some - # tags after inserting the coressponding note and + # tags after inserting the coressponding note and # before going to a new note. self.lastFigureDuration = 0 @@ -5174,7 +5173,7 @@ def xmlToChordSymbol( return cs def xmlToFiguredBass(self, mxFiguredBass): - #print('Hello from xmlToFiguredBass', mxFiguredBass.findall('*')) + # print('Hello from xmlToFiguredBass', mxFiguredBass.findall('*')) fb_strings: list[str] = [] sep = ',' d: duration.Duration | None = None @@ -5188,7 +5187,6 @@ def xmlToFiguredBass(self, mxFiguredBass): if el.tag == 'figure-number': if el.text: fb_number = el.text - # Get prefix and/or suffix. # The function returns an empty string if nothing is found. fb_prefix = self._getFigurePrefixOrSuffix(figure, 'prefix') @@ -5219,14 +5217,14 @@ def xmlToFiguredBass(self, mxFiguredBass): # If a duration is provided, set length of the FigureBassIndication if d: fbi.quarterLength = d.quarterLength - # function in parent add add found objects. - # + # call function in parent to add found objects. self.parent.appendFbis(fbi, offsetFbi) - def _getFigurePrefixOrSuffix(self, figure, presuf: str='prefix') -> str: + def _getFigurePrefixOrSuffix(self, figure, presuf: str = 'prefix') -> str: if figure.findall(presuf): for fix in figure.findall(presuf): if fix.text: + print(fix.text) return modifiersDictXmlToM21[fix.text] return '' From 695a1e971a44bd4e8688984cf895c2ce7ff9d30d Mon Sep 17 00:00:00 2001 From: mxordn Date: Fri, 31 Mar 2023 00:53:06 +0200 Subject: [PATCH 22/58] Update music21/musicxml/xmlToM21.py Co-authored-by: Jacob Walls --- music21/musicxml/xmlToM21.py | 1 - 1 file changed, 1 deletion(-) diff --git a/music21/musicxml/xmlToM21.py b/music21/musicxml/xmlToM21.py index 3a509e6015..51335307ec 100644 --- a/music21/musicxml/xmlToM21.py +++ b/music21/musicxml/xmlToM21.py @@ -5173,7 +5173,6 @@ def xmlToChordSymbol( return cs def xmlToFiguredBass(self, mxFiguredBass): - # print('Hello from xmlToFiguredBass', mxFiguredBass.findall('*')) fb_strings: list[str] = [] sep = ',' d: duration.Duration | None = None From 3e5074f3523f0a11daac99ca4e80cc78f452930f Mon Sep 17 00:00:00 2001 From: Moritz Heffter Date: Sat, 8 Apr 2023 15:01:27 +0200 Subject: [PATCH 23/58] refactoring and adding testfles. put figures in measure- --- music21/figuredBass/notation.py | 78 +++++++- music21/harmony.py | 8 +- music21/mei/base.py | 16 +- .../lilypondTestSuite/74b-FiguredBass.xml | 179 ++++++++++++++++++ music21/musicxml/xmlToM21.py | 99 ++++++---- 5 files changed, 324 insertions(+), 56 deletions(-) create mode 100644 music21/musicxml/lilypondTestSuite/74b-FiguredBass.xml diff --git a/music21/figuredBass/notation.py b/music21/figuredBass/notation.py index c260c8efa5..0ea12b7b0e 100644 --- a/music21/figuredBass/notation.py +++ b/music21/figuredBass/notation.py @@ -46,11 +46,16 @@ '##': 'double-sharp', 'bb': 'flat-flat', '\\': 'backslash', + '/': 'slash', '+': 'sharp', '\u266f': 'sharp', '\u266e': 'natural', '\u266d': 'flat', - '\u20e5': 'sharp'} + '\u20e5': 'sharp', + '\u0338': 'slash', + '\U0001D12A': 'double-sharp', + '\U0001D12B': 'flat-flat', + } class Notation(prebase.ProtoM21Object): ''' @@ -201,17 +206,18 @@ class Notation(prebase.ProtoM21Object): ''', } - def __init__(self, notationColumn=None): + def __init__(self, notationColumn=None, extenders=None): # Parse notation string if notationColumn is None: notationColumn = '' self.notationColumn = notationColumn + self.extenders = extenders self.figureStrings = None self.origNumbers = None self.origModStrings = None self.numbers = None self.modifierStrings = None - self.hasExtenders = False + self.hasExtenders: bool = False self._parseNotationColumn() self._translateToLonghand() @@ -256,6 +262,7 @@ def _parseNotationColumn(self): numbers = [] modifierStrings = [] figureStrings = [] + extenders = [] for figure in figures: figure = figure.strip() @@ -271,20 +278,40 @@ def _parseNotationColumn(self): number = None modifierString = None + extender = False if m1: + # if no number is there and only an extender is found. if '_' in m1: self.hasExtenders = True number = '_' + extender = True else: - number = int(m1[0].strip()) + # is an extender part of the number string? + if '_' in m1[0]: + self.hasExtenders = True + extender = True + number = int(m1[0].strip('_')) + else: + number = int(m1[0].strip()) if m2: modifierString = m2[0].strip() numbers.append(number) modifierStrings.append(modifierString) + extenders.append(extender) numbers = tuple(numbers) modifierStrings = tuple(modifierStrings) + + # extenders come from the optional argument when instantionting the object. + # If nothing is provided, no extenders will be set. + # Otherwise we have to look if amount of extenders and figure numbers match + # + if not self.extenders: + self.extenders = [False for i in range(len(modifierStrings))] + else: + extenders = tuple(self.extenders) + print('angekommen', numbers, modifierStrings, extenders) self.origNumbers = numbers # Keep original numbers self.numbers = numbers # Will be converted to longhand @@ -394,7 +421,12 @@ def _getFigures(self): for i in range(len(self.numbers)): number = self.numbers[i] modifierString = self.modifierStrings[i] - figure = Figure(number, modifierString) + if self.extenders: + if i < len(self.extenders): + extender = self.extenders[i] + else: + extender = False + figure = Figure(number, modifierString, extender) figures.append(figure) self.figures = figures @@ -431,6 +463,20 @@ class Figure(prebase.ProtoM21Object): '+' >>> f1.modifier + >>> f1.hasExtender + False + >>> f1.isExtender + False + >>> f2 = notation.Figure(6, '\', extender=True) + >>> f2.hasExtender + True + >>> f2.isExtender + False + >>> f3 = notation.Figure(extender=True) + >>> f3.isExtender + True + >>> f3.hasExtender + True ''' _DOC_ATTR: dict[str, str] = { 'number': ''' @@ -446,18 +492,28 @@ class Figure(prebase.ProtoM21Object): associated with an expanded :attr:`~music21.figuredBass.notation.Notation.notationColumn`. ''', + 'hasExtender': ''' + A bool value that indicates whether an extender is part of the figure. + It is set by a keyword argument. + ''', + 'isExtender': ''' + A bool value that returns true if an extender is part of the figure but no + number is given. Pure extender if you will. + It is set by evaluating the number and extender arguments. + ''' } - def __init__(self, number=1, modifierString=None): + def __init__(self, number=1, modifierString=None, extender=False): self.number = number self.modifierString = modifierString self.modifier = Modifier(modifierString) # look for extenders underscore - self.isExtender: bool = (self.number == '_') + self.hasExtender: bool = extender + self.isExtender: bool = (self.number == 1 and self.hasExtender) def _reprInternal(self): mod = repr(self.modifier).replace('music21.figuredBass.notation.', '') - return f'{self.number} Mods: {mod} hasExt: {self.isExtender}' + return f'{self.number} Mods: {mod} hasExt: {self.hasExtender}' # ------------------------------------------------------------------------------ @@ -474,7 +530,10 @@ def _reprInternal(self): '\u266f': '#', '\u266e': 'n', '\u266d': 'b', - '\u20e5': '#' + '\u20e5': '#', + '\u0338': '#', + '\U0001d12a': '##', + '\U0001d12b': '--' } @@ -535,6 +594,7 @@ class Modifier(prebase.ProtoM21Object): def __init__(self, modifierString=None): self.modifierString = modifierString + self.originalString = modifierString self.accidental = self._toAccidental() def _reprInternal(self): diff --git a/music21/harmony.py b/music21/harmony.py index e3f2c16636..73c4fcf19a 100644 --- a/music21/harmony.py +++ b/music21/harmony.py @@ -2496,7 +2496,7 @@ def transpose(self: NCT, _value, *, inPlace=False) -> NCT | None: class FiguredBassIndication(Harmony): isFigure = True tstamp = 1 - def __init__(self, figs: str | list | None = None, **keywords): + def __init__(self, figs: str | list | None = None, extenders: list[bool] | None = None , **keywords): super().__init__(**keywords) if figs: if isinstance(figs, list): @@ -2508,15 +2508,15 @@ def __init__(self, figs: str | list | None = None, **keywords): _figs = ','.join(figs) else: _figs = '' - self._fig_notation = notation.Notation(_figs) + self._fig_notation = notation.Notation(_figs, extenders) @property def fig_notation(self) -> notation.Notation: return self._fig_notation @fig_notation.setter - def fig_notation(self, figs): - self._fig_notation = notation.Notation(figs) + def fig_notation(self, figs, extenders=None): + self._fig_notation = notation.Notation(figs, extenders) def __repr__(self): return f'<{self.__class__.__name__} figures: {self.fig_notation.notationColumn}>' diff --git a/music21/mei/base.py b/music21/mei/base.py index de09c8e336..f267724508 100644 --- a/music21/mei/base.py +++ b/music21/mei/base.py @@ -2528,21 +2528,29 @@ def figuredbassFromElement(elem, slurBundle=None) -> harmony.FiguredBassIndicati fb_id = elem.get(_XMLID) fb_notation: str = '' fb_notation_list: list[str] = [] + fb_extenders: list[bool] = [] dauer: float = 0 # loop through all child elements and collect tags for subElement in elem.findall('*'): if subElement.tag == f'{MEI_NS}f': if subElement.text is not None: - fb_notation_list.append(subElement.text) + # remove Parentheses around figure numbers + fb_fig = subElement.text.strip('()') + fb_notation_list.append(fb_fig) + if 'extender' in subElement.attrib.keys(): + #if subElement.attrib['extender'] == 'true': + #fb_notation_list.append('_') + print('extender: ', (subElement.attrib['extender'] == 'true')) + fb_extenders.append((subElement.attrib['extender'] == 'true')) else: - if 'extender' in subElement.attrib.keys(): - fb_notation_list.append('_') + fb_extenders.append(False) + if 'dur.metrical' in subElement.attrib.keys(): dauer = float(subElement.attrib['dur.metrical']) # Generate a FiguredBassIndication object and set the collected information fb_notation = ','.join(fb_notation_list) - theFbNotation = harmony.FiguredBassIndication(fb_notation) + theFbNotation = harmony.FiguredBassIndication(fb_notation, extenders=fb_extenders) theFbNotation.id = fb_id theFbNotation.duration = duration.Duration(quarterLength=dauer) diff --git a/music21/musicxml/lilypondTestSuite/74b-FiguredBass.xml b/music21/musicxml/lilypondTestSuite/74b-FiguredBass.xml new file mode 100644 index 0000000000..5072ab7698 --- /dev/null +++ b/music21/musicxml/lilypondTestSuite/74b-FiguredBass.xml @@ -0,0 +1,179 @@ + + + + + + Some figured bass containing + alterated figures, bracketed figures and slashed figures. The last + note contains an empty <figured-bass> element, which is + invalid MusicXML, to check how well applications cope with malformed + files. + + Note that this file does not contain any extenders! + + + + + MusicXML Part + + + + + + + 8 + + 0 + major + + + + G + 2 + + + +
3
+ 4 +
+ + G4 + 4 + 1 + eighth + + + G4 + 4 + 1 + eighth + + +
sharp1
+
flat3
+
natural5
+ 6 +
+ + G4 + 6 + 1 + eighth + + + +
6
+
+ + G4 + 2 + 1 + 16th + + +
5slash
+
flat127slash
+ 8 +
+ + G4 + 8 + 1 + eighth + + + + + + + G4 + 8 + 1 + quarter + + + light-light + +
+ + +
5
+ 2 +
+ +
6
+ 2 +
+ + G4 + 4 + 1 + eighth + + + G4 + 4 + 1 + eighth + + +
double-sharp3
+
flat-flat3
+
natural5
+ 4 +
+ +
6backslash
+
+
+ 2 +
+ +
7
+
+ 2 +
+ + G4 + 8 + 1 + quarter + + +
6
+
+ + G4 + 2 + 1 + 16th + + +
5slash
+
flat12slash
+ 6 +
+ + G4 + 6 + 1 + eighth + + + + G4 + 8 + 1 + quarter + + + light-heavy + +
+
+ +
diff --git a/music21/musicxml/xmlToM21.py b/music21/musicxml/xmlToM21.py index 3a509e6015..57689fb48f 100644 --- a/music21/musicxml/xmlToM21.py +++ b/music21/musicxml/xmlToM21.py @@ -842,7 +842,7 @@ def __init__(self): self.m21PartObjectsById = {} self.partGroupList = [] self.parts = [] - self.fbis: list[harmony.FiguredBassIndication] | None = None + # self.fbis: list[harmony.FiguredBassIndication] | None = None self.musicXmlVersion = defaults.musicxmlVersion @@ -927,9 +927,9 @@ def xmlRootToScore(self, mxScore, inputM21=None): s.coreInsert(0.0, part) self.m21PartObjectsById[partId] = part - if self.fbis: - for fbi in self.fbis: - s.insert(fbi[0], fbi[1]) + #if self.fbis: + # for fbi in self.fbis: + # s.insert(fbi[0], fbi[1]) self.partGroups() @@ -2358,12 +2358,12 @@ def applyMultiMeasureRest(self, r: note.Rest): self.stream.insert(0, self.activeMultiMeasureRestSpanner) self.activeMultiMeasureRestSpanner = None - def appendFbis(self, fbi, measureOffset): - absOffset = self.lastMeasureOffset + measureOffset - if self.parent.fbis: - self.parent.fbis.append((absOffset, fbi)) - else: - self.parent.fbis = [(absOffset, fbi)] + #def appendFbis(self, fbi, measureOffset): + # absOffset = self.lastMeasureOffset + measureOffset + # if self.parent.fbis: + # self.parent.fbis.append((absOffset, fbi)) + # else: + # self.parent.fbis = [(absOffset, fbi)] # ----------------------------------------------------------------------------- class MeasureParser(XMLParserBase): @@ -5172,38 +5172,56 @@ def xmlToChordSymbol( return cs - def xmlToFiguredBass(self, mxFiguredBass): + def xmlToFiguredBass(self, mxFiguredBass) -> harmony.FiguredBassIndication: # print('Hello from xmlToFiguredBass', mxFiguredBass.findall('*')) fb_strings: list[str] = [] + fb_extenders: list[bool] = [] sep = ',' d: duration.Duration | None = None offsetFbi = self.offsetMeasureNote - for figure in mxFiguredBass.findall('*'): - # TODO: suffixes are ignored at the moment - for el in figure.findall('*'): - fb_number: str = '' - fb_prefix: str = '' - if el.tag == 'figure-number': - if el.text: - fb_number = el.text - # Get prefix and/or suffix. - # The function returns an empty string if nothing is found. - fb_prefix = self._getFigurePrefixOrSuffix(figure, 'prefix') - fb_suffix = self._getFigurePrefixOrSuffix(figure, 'suffix') - - # put prefix/suffix and number together - if fb_prefix + fb_number + fb_suffix != '': - fb_strings.append(fb_prefix + fb_number + fb_suffix) - - if el.tag == 'extend': - if 'type' in el.attrib.keys(): - if el.attrib['type'] == 'continue': - fb_strings.append('_') - - # If a is given, this usually means that there are multiple figures - # for a single note. We have to look for offsets here. - if figure.tag == 'duration': + # parentheses is not used at the moment + hasParentheses: bool = False + + if 'parentheses' in mxFiguredBass.attrib.keys(): + if mxFiguredBass.attrib['parentheses'] == 'yes': + hasParentheses = True + warnings.warn('Parentheses are ignored and removed at the moment.', + MusicXMLWarning) + + for subElement in mxFiguredBass.findall('*'): + fb_number: str = '' + fb_prefix: str = '' + fb_suffix: str = '' + fb_extender: str = '' + if subElement.tag == 'figure': + for el in subElement.findall('*'): + if el.tag == 'figure-number': + if el.text: + fb_number = el.text + # Get prefix and/or suffix. + # The function returns an empty string if nothing is found. + fb_prefix = self._getFigurePrefixOrSuffix(subElement, 'prefix') + fb_suffix = self._getFigurePrefixOrSuffix(subElement, 'suffix') + + # collect information on extenders + if el.tag == 'extend': + if 'type' in el.attrib.keys(): + print((el.attrib['type'] in ['stop', 'continue', 'start'])) + fb_extenders.append((el.attrib['type'] in ['stop', 'continue', 'start'])) + if not subElement.findall('extend'): + fb_extenders.append(False) + + # put prefix/suffix, extender and number together + if fb_prefix + fb_number + fb_extender + fb_suffix != '': + fb_strings.append(fb_prefix + fb_number + fb_suffix) + else: + # Warning because an empty figured-bass tag is not valid musixml. + warnings.warn('There was an empty tag.', MusicXMLWarning) + + # If a is given, this usually means that there are multiple figures + # for a single note. We have to look for offsets here. + if subElement.tag == 'duration': d = self.xmlToDuration(mxFiguredBass) if self.lastFigureDuration > 0: offsetFbi = self.offsetMeasureNote + self.lastFigureDuration @@ -5213,18 +5231,21 @@ def xmlToFiguredBass(self, mxFiguredBass): self.lastFigureDuration = d.quarterLength fb_string = sep.join(fb_strings) - fbi = harmony.FiguredBassIndication(fb_string) + print('Ergebnis:', fb_string, fb_extenders) + fbi = harmony.FiguredBassIndication(fb_string, extenders=fb_extenders) # If a duration is provided, set length of the FigureBassIndication if d: fbi.quarterLength = d.quarterLength # call function in parent to add found objects. - self.parent.appendFbis(fbi, offsetFbi) + #self.parent.appendFbis(fbi, offsetFbi) + self.stream.insert(offsetFbi, fbi) + return fbi def _getFigurePrefixOrSuffix(self, figure, presuf: str = 'prefix') -> str: + # helper function for prefixes and suffixes of figure numbers. if figure.findall(presuf): for fix in figure.findall(presuf): if fix.text: - print(fix.text) return modifiersDictXmlToM21[fix.text] return '' From 27a1c446cf0cfc6fdc652e68fbc94aca706c5d3a Mon Sep 17 00:00:00 2001 From: Moritz Heffter Date: Sun, 26 Feb 2023 15:57:30 +0100 Subject: [PATCH 24/58] added figuredBass tag to mei import --- music21/figuredBass/notation.py | 28 ++++ music21/mei/base.py | 238 +++++++++++++++++++++----------- 2 files changed, 182 insertions(+), 84 deletions(-) diff --git a/music21/figuredBass/notation.py b/music21/figuredBass/notation.py index 774e1236e1..f827450749 100644 --- a/music21/figuredBass/notation.py +++ b/music21/figuredBass/notation.py @@ -15,6 +15,7 @@ from music21 import exceptions21 from music21 import pitch +from music21 import harmony from music21 import prebase shorthandNotation = {(None,): (5, 3), @@ -635,6 +636,33 @@ def convertToPitch(pitchString): raise TypeError('Cannot convert ' + pitchString + ' to a music21 Pitch.') +class FiguredBassIndication(harmony.Harmony): + isFigure = True + tstamp = 1 + def __init__(self, figs=None, **keywords): + super().__init__(**keywords) + if figs: + if isinstance(figs, list): + fig_string = str(figs[0]) + for sf in figs: + fig_string += f',{sf}' + figs = fig_string + #pass + else: + figs = '' + self.__fig_notation = Notation(figs) + + @property + def fig_notation(self) -> Notation: + return self.__fig_notation + + @fig_notation.setter + def fig_notation(self, figs): + self.__fig_notation = Notation(figs) + + def __repr__(self): + return f'<{self.__class__.__name__} figures: {self.fig_notation.notationColumn}>' + _DOC_ORDER = [Notation, Figure, Modifier] diff --git a/music21/mei/base.py b/music21/mei/base.py index 70eeb75c87..da504e8d99 100644 --- a/music21/mei/base.py +++ b/music21/mei/base.py @@ -1,18 +1,18 @@ # -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Name: mei/base.py -# Purpose: Public interfaces for the MEI module +# Purpose: Public methods for the MEI module # # Authors: Christopher Antila # -# Copyright: Copyright © 2014 Michael Scott Asato Cuthbert +# Copyright: Copyright © 2014 Michael Scott Cuthbert and the music21 Project # License: BSD, see license.txt # ----------------------------------------------------------------------------- ''' -These are the public interfaces for the MEI module by Christopher Antila +These are the public methods for the MEI module by Christopher Antila To convert a string with MEI markup into music21 objects, -use :meth:`~music21.mei.MeiToM21Converter.convertFromString`. +use :meth:`MeiToM21Converter.convertFromString`. In the future, most of the functions in this module should be moved to a separate, import-only module, so that functions for writing music21-to-MEI will fit nicely. @@ -54,6 +54,7 @@ ... ... ... """ +>>> from music21 import * >>> conv = mei.MeiToM21Converter(meiString) >>> result = conv.run() >>> result @@ -170,16 +171,16 @@ * : a page break * : a line break * : a system break -''' -from __future__ import annotations +''' +# pylint: disable=misplaced-comparison-constant +from typing import Optional, Union, List, Tuple +from xml.etree import ElementTree as ETree +from xml.etree.ElementTree import Element from collections import defaultdict -from copy import deepcopy -import typing as t +from fractions import Fraction # for typing from uuid import uuid4 -from xml.etree.ElementTree import Element, ParseError, fromstring, ElementTree - # music21 from music21 import articulations @@ -199,12 +200,10 @@ from music21 import stream from music21 import spanner from music21 import tie +from music21 import figuredBass - -if t.TYPE_CHECKING: - from fractions import Fraction - -environLocal = environment.Environment('mei.base') +_MOD = 'mei.base' +environLocal = environment.Environment(_MOD) # Module-Level Constants @@ -227,30 +226,22 @@ # Exceptions # ----------------------------------------------------------------------------- class MeiValidityError(exceptions21.Music21Exception): - ''' - When there is an otherwise-unspecified validity error that prevents parsing. - ''' + 'When there is an otherwise-unspecified validity error that prevents parsing.' pass class MeiValueError(exceptions21.Music21Exception): - ''' - When an attribute has an invalid value. - ''' + 'When an attribute has an invalid value.' pass class MeiAttributeError(exceptions21.Music21Exception): - ''' - When an element has an invalid attribute. - ''' + 'When an element has an invalid attribute.' pass class MeiElementError(exceptions21.Music21Exception): - ''' - When an element itself is invalid. - ''' + 'When an element itself is invalid.' pass @@ -299,14 +290,14 @@ def __init__(self, theDocument=None): self.documentRoot = Element(f'{MEI_NS}mei') else: try: - self.documentRoot = fromstring(theDocument) - except ParseError as parseErr: + self.documentRoot = ETree.fromstring(theDocument) + except ETree.ParseError as parseErr: environLocal.printDebug( '\n\nERROR: Parsing the MEI document with ElementTree failed.') environLocal.printDebug(f'We got the following error:\n{parseErr}') raise MeiValidityError(_INVALID_XML_DOC) - if isinstance(self.documentRoot, ElementTree): + if isinstance(self.documentRoot, ETree.ElementTree): # pylint warns that :class:`Element` doesn't have a getroot() method, which is # true enough, but... self.documentRoot = self.documentRoot.getroot() # pylint: disable=maybe-no-member @@ -353,8 +344,8 @@ def run(self) -> stream.Stream: # ----------------------------------------------------------------------------- def safePitch( name: str, - accidental: str | None = None, - octave: str | int = '' + accidental: Optional[str] = None, + octave: Union[str, int] = '' ) -> pitch.Pitch: ''' Safely build a :class:`~music21.pitch.Pitch` from a string. @@ -364,9 +355,7 @@ def safePitch( function instead returns a default :class:`~music21.pitch.Pitch` instance. name: Desired name of the :class:`~music21.pitch.Pitch`. - accidental: (Optional) Symbol for the accidental. - octave: (Optional) Octave number. Returns A :class:`~music21.pitch.Pitch` with the appropriate properties. @@ -376,25 +365,19 @@ def safePitch( >>> safePitch('D', '#', '6') - >>> safePitch('D', '#') - ''' if not name: return pitch.Pitch() - if octave and accidental is not None: - return pitch.Pitch(name, octave=int(octave), accidental=accidental) - if octave: - return pitch.Pitch(name, octave=int(octave)) - if accidental is not None: - return pitch.Pitch(name, accidental=accidental) + elif accidental is None: + return pitch.Pitch(name + octave) else: - return pitch.Pitch(name) + return pitch.Pitch(name, accidental=accidental, octave=int(octave)) def makeDuration( - base: float | int | Fraction = 0.0, + base: Union[float, int, Fraction] = 0.0, dots: int = 0 -) -> duration.Duration: +) -> 'music21.duration.Duration': ''' Given a ``base`` duration and a number of ``dots``, create a :class:`~music21.duration.Duration` instance with the @@ -404,6 +387,7 @@ def makeDuration( **Examples** + >>> from music21 import * >>> from fractions import Fraction >>> mei.base.makeDuration(base=2.0, dots=0).quarterLength # half note, no dots 2.0 @@ -423,7 +407,7 @@ def makeDuration( return returnDuration -def allPartsPresent(scoreElem) -> tuple[str, ...]: +def allPartsPresent(scoreElem) -> Tuple[str, ...]: # noinspection PyShadowingNames ''' Find the @n values for all elements in a element. This assumes that every @@ -449,6 +433,7 @@ def allPartsPresent(scoreElem) -> tuple[str, ...]: ...
...
""" >>> import xml.etree.ElementTree as ETree + >>> from music21 import * >>> meiDoc = ETree.fromstring(meiDoc) >>> mei.base.allPartsPresent(meiDoc) ('1', '2') @@ -459,6 +444,7 @@ def allPartsPresent(scoreElem) -> tuple[str, ...]: ''' # xpathQuery = f'.//{MEI_NS}music//{MEI_NS}score//{MEI_NS}staffDef' xpathQuery = f'.//{MEI_NS}staffDef' + partNs = [] # hold the @n attribute for all the parts for staffDef in scoreElem.findall(xpathQuery): @@ -466,6 +452,16 @@ def allPartsPresent(scoreElem) -> tuple[str, ...]: partNs.append(staffDef.get('n')) if not partNs: raise MeiValidityError(_SEEMINGLY_NO_PARTS) + + # Get information of possible tags in the score. If there are tags prepare a list to + # store them and process them later. TODO: Maybe to be put in a separate function e.g. like allPartsPresent + figuredBassQuery = f'.//{MEI_NS}fb' + if scoreElem.findall(figuredBassQuery): + environLocal.printDebug('harm tag found!') + partNs.append('fb') + # here other elements (e.g. chordsymbols) can be added. + # … 'if …' + return tuple(partNs) @@ -569,6 +565,7 @@ def _accidentalFromAttr(attr): ''' Use :func:`_attrTranslator` to convert the value of an "accid" attribute to its music21 string. + >>> from music21 import * >>> mei.base._accidentalFromAttr('s') '#' ''' @@ -580,6 +577,7 @@ def _accidGesFromAttr(attr): Use :func:`_attrTranslator` to convert the value of an @accid.ges attribute to its music21 string. + >>> from music21 import * >>> mei.base._accidGesFromAttr('s') '#' ''' @@ -590,6 +588,7 @@ def _qlDurationFromAttr(attr): ''' Use :func:`_attrTranslator` to convert an MEI "dur" attribute to a music21 quarterLength. + >>> from music21 import * >>> mei.base._qlDurationFromAttr('4') 1.0 @@ -628,8 +627,7 @@ def _makeArticList(attr): return articList -def _getOctaveShift(dis: t.Literal['8', '15', '22'] | None, - disPlace: str) -> int: +def _getOctaveShift(dis, disPlace): ''' Use :func:`_getOctaveShift` to calculate the :attr:`octaveShift` attribute for a :class:`~music21.clef.Clef` subclass. Any of the arguments may be ``None``. @@ -722,6 +720,7 @@ def _ppSlurs(theConverter): ...
... ... """ + >>> from music21 import * >>> theConverter = mei.base.MeiToM21Converter(meiDoc) >>> >>> mei.base._ppSlurs(theConverter) @@ -959,7 +958,7 @@ def _ppConclude(theConverter): # Helper Functions # ----------------------------------------------------------------------------- def _processEmbeddedElements( - elements: list[Element], + elements: List[Element], mapping, callerTag=None, slurBundle=None @@ -993,6 +992,7 @@ def _processEmbeddedElements( Because there is no ``'rest'`` key in the ``mapping``, that :class:`Element` is ignored. >>> from xml.etree.ElementTree import Element + >>> from music21 import * >>> elements = [Element('note'), Element('rest'), Element('note')] >>> mapping = {'note': lambda x, y: note.Note('D2')} >>> mei.base._processEmbeddedElements(elements, mapping, 'doctest') @@ -1038,7 +1038,7 @@ def _timeSigFromAttrs(elem): return meter.TimeSignature(f"{elem.get('meter.count')!s}/{elem.get('meter.unit')!s}") -def _keySigFromAttrs(elem: Element) -> key.Key | key.KeySignature: +def _keySigFromAttrs(elem: Element) -> Union[key.Key, key.KeySignature]: ''' From any tag with (at minimum) either @key.pname or @key.sig attributes, make a :class:`KeySignature` or :class:`Key`, as possible. @@ -1052,8 +1052,6 @@ def _keySigFromAttrs(elem: Element) -> key.Key | key.KeySignature: # noinspection PyTypeChecker mode = elem.get('key.mode', '') step = elem.get('key.pname') - if step is None: # pragma: no cover - raise MeiValidityError('Key missing step') accidental = _accidentalFromAttr(elem.get('key.accid')) if accidental is None: tonic = step @@ -1186,9 +1184,7 @@ def addSlurs(elem, obj, slurBundle): addedSlur = False def wrapGetByIdLocal(theId): - ''' - Avoid crashing when getByIdLocl() doesn't find the slur - ''' + "Avoid crashing when getByIdLocl() doesn't find the slur" try: slurBundle.getByIdLocal(theId)[0].addSpannedElements(obj) return True @@ -1299,17 +1295,17 @@ def metaSetTitle(work, meta): :return: The ``meta`` argument, having relevant metadata added. ''' # title, subtitle, and movement name - subtitle = None for title in work.findall(f'./{MEI_NS}titleStmt/{MEI_NS}title'): - if title.get('type', '') == 'subtitle': # or 'subordinate', right? - subtitle = title.text + if title.get('type', '') == 'subtitle': + meta.subtitle = title.text elif meta.title is None: meta.title = title.text - if subtitle: + if hasattr(meta, 'subtitle'): # Since m21.Metadata doesn't actually have a "subtitle" attribute, we'll put the subtitle # in the title - meta.title = f'{meta.title} ({subtitle})' + meta.title = f'{meta.title} ({meta.subtitle})' + del meta.subtitle tempo = work.find(f'./{MEI_NS}tempo') if tempo is not None: @@ -1340,7 +1336,7 @@ def metaSetComposer(work, meta): if len(composers) == 1: meta.composer = composers[0] elif len(composers) > 1: - meta.composers = composers + meta.composer = composers return meta @@ -1364,12 +1360,12 @@ def metaSetDate(work, meta): except ValueError: environLocal.warn(_MISSED_DATE.format(dateStr)) else: - meta.dateCreated = theDate + meta.date = theDate else: dateStart = date.get('notbefore') if date.get('notbefore') else date.get('startdate') dateEnd = date.get('notafter') if date.get('notafter') else date.get('enddate') if dateStart and dateEnd: - meta.dateCreated = metadata.DateBetween((dateStart, dateEnd)) + meta.date = metadata.DateBetween((dateStart, dateEnd)) return meta @@ -1555,6 +1551,7 @@ def scoreDefFromElement(elem, slurBundle=None): # pylint: disable=unused-argume ... ... ... """ + >>> from music21 import * >>> from xml.etree import ElementTree as ET >>> scoreDef = ET.fromstring(meiDoc) >>> result = mei.base.scoreDefFromElement(scoreDef) @@ -1721,6 +1718,7 @@ def staffDefFromElement(elem, slurBundle=None): # pylint: disable=unused-argume >>> meiDoc = """ ... ... """ + >>> from music21 import * >>> from xml.etree import ElementTree as ET >>> staffDef = ET.fromstring(meiDoc) >>> result = mei.base.staffDefFromElement(staffDef) @@ -1741,6 +1739,7 @@ def staffDefFromElement(elem, slurBundle=None): # pylint: disable=unused-argume ... ... ... """ + >>> from music21 import * >>> from xml.etree import ElementTree as ET >>> staffDef = ET.fromstring(meiDoc) >>> result = mei.base.staffDefFromElement(staffDef) @@ -1893,14 +1892,15 @@ def articFromElement(elem, slurBundle=None): # pylint: disable=unused-argument :attr:`~music21.note.GeneralNote.articulations` attribute. >>> from xml.etree import ElementTree as ET - >>> meiSnippet = '' + >>> from music21 import * + >>> meiSnippet = """""" >>> meiSnippet = ET.fromstring(meiSnippet) >>> mei.base.articFromElement(meiSnippet) [] A single element may indicate many :class:`Articulation` objects. - >>> meiSnippet = '' + >>> meiSnippet = """""" >>> meiSnippet = ET.fromstring(meiSnippet) >>> mei.base.articFromElement(meiSnippet) [, ] @@ -1952,11 +1952,12 @@ def accidFromElement(elem, slurBundle=None): # pylint: disable=unused-argument a string. Accidentals up to triple-sharp and triple-flat are supported. >>> from xml.etree import ElementTree as ET - >>> meiSnippet = '' + >>> from music21 import * + >>> meiSnippet = """""" >>> meiSnippet = ET.fromstring(meiSnippet) >>> mei.base.accidFromElement(meiSnippet) '#' - >>> meiSnippet = '' + >>> meiSnippet = """""" >>> meiSnippet = ET.fromstring(meiSnippet) >>> mei.base.accidFromElement(meiSnippet) '---' @@ -2339,16 +2340,13 @@ def spaceFromElement(elem, slurBundle=None): # pylint: disable=unused-argument A placeholder used to fill an incomplete measure, layer, etc. most often so that the combined duration of the events equals the number of beats in the measure. - Returns a Rest element with hideObjectOnPrint = True - In MEI 2013: pg.440 (455 in PDF) (MEI.shared module) ''' # NOTE: keep this in sync with restFromElement() theDuration = _qlDurationFromAttr(elem.get('dur')) theDuration = makeDuration(theDuration, int(elem.get('dots', 0))) - theSpace = note.Rest(duration=theDuration) - theSpace.style.hideObjectOnPrint = True + theSpace = note.SpacerRest(duration=theDuration) if elem.get(_XMLID) is not None: theSpace.id = elem.get(_XMLID) @@ -2493,6 +2491,50 @@ def chordFromElement(elem, slurBundle=None): return theChord +def harmFromElement(elem, slurBundle=None): + # other tags than to be added… + tagToFunction = {f'{MEI_NS}fb': figuredbassFromElement} + + fb_harmony_tag: tuple = () + + # Collect all elements in a measure and go throug extenders + # tstamp has to be used as a duration marker between two elements + + for subElement in _processEmbeddedElements(elem.findall('*'), + tagToFunction, + elem.tag, slurBundle): + subElement.tstamp = float(elem.get('tstamp')) + subElement.offset = subElement.tstamp - 1 + fb_harmony_tag = (subElement.tstamp - 1, subElement) + + return fb_harmony_tag + +def figuredbassFromElement(elem, slurBundle=None): + if elem.get(_XMLID): + id = elem.get(_XMLID) + fb_notation = '' + dauer: float = 0 + + # loop through all child elements and collect tags + for subElement in elem.findall('*'): + if subElement.tag == f'{MEI_NS}f': + if subElement.text != None: + if fb_notation != '': + fb_notation += f',{subElement.text}' + else: + fb_notation = subElement.text + else: + if 'extender' in subElement.attrib.keys(): + print('Extender found!') + if 'dur.metrical' in subElement.attrib.keys(): + dauer = float(subElement.attrib['dur.metrical']) + + # Generate a FiguredBassIndication object and set the collected information + theFbNotation = figuredBass.notation.FiguredBassIndication(fb_notation) + theFbNotation.id = id + theFbNotation.duration = duration.Duration(quarterLength=dauer) + + return theFbNotation def clefFromElement(elem, slurBundle=None): # pylint: disable=unused-argument ''' @@ -2604,6 +2646,7 @@ def beamFromElement(elem, slurBundle=None): a list of three objects, none of which is a :class:`Beam` or similar. >>> from xml.etree import ElementTree as ET + >>> from music21 import * >>> meiSnippet = """ ... ... @@ -3045,7 +3088,7 @@ def _makeBarlines(elem, staves): bars = bars[1] for eachMeasure in staves.values(): if isinstance(eachMeasure, stream.Measure): - eachMeasure.leftBarline = deepcopy(bars) + eachMeasure.leftBarline = bars if elem.get('right') is not None: bars = _barlineFromAttr(elem.get('right')) @@ -3055,7 +3098,7 @@ def _makeBarlines(elem, staves): bars = bars[0] for eachMeasure in staves.values(): if isinstance(eachMeasure, stream.Measure): - eachMeasure.rightBarline = deepcopy(bars) + eachMeasure.rightBarline = bars return staves @@ -3070,7 +3113,7 @@ def measureFromElement(elem, backupNum, expectedNs, slurBundle=None, activeMeter :param elem: The ```` element to process. :type elem: :class:`~xml.etree.ElementTree.Element` :param int backupNum: A fallback value for the resulting - :class:`~music21.stream.Measure` objects' number attribute. + :class:`~music21.measure.Measure` objects' number attribute. :param expectedNs: A list of the expected @n attributes for the tags in this . If an expected isn't in the , it will be created with a full-measure rest. :type expectedNs: iterable of str @@ -3131,10 +3174,12 @@ def measureFromElement(elem, backupNum, expectedNs, slurBundle=None, activeMeter ''' staves = {} stavesWaiting = {} # for staff-specific objects processed before the corresponding staff + harmElements: dict = {'fb': []} # mapping from tag name to our converter function staffTag = f'{MEI_NS}staff' staffDefTag = f'{MEI_NS}staffDef' + harmTag = f'{MEI_NS}harm' # track the bar's duration maxBarDuration = None @@ -3154,6 +3199,10 @@ def measureFromElement(elem, backupNum, expectedNs, slurBundle=None, activeMeter environLocal.warn(_UNIMPLEMENTED_IMPORT.format('', '@n')) else: stavesWaiting[whichN] = staffDefFromElement(eachElem, slurBundle) + elif harmTag == eachElem.tag: + # get all information stored in tags + harmElements['fb'].append(harmFromElement(eachElem)) + elif eachElem.tag not in _IGNORE_UNPROCESSED: environLocal.printDebug(_UNPROCESSED_SUBELEMENT.format(eachElem.tag, elem.tag)) @@ -3165,6 +3214,10 @@ def measureFromElement(elem, backupNum, expectedNs, slurBundle=None, activeMeter # We must insert() these objects because a signals its changes for the # *start* of the in which it appears. staves[whichN].insert(0, eachObj) + + # Add objects to the staves dict + staves['fb'] = harmElements + # other childs of tags can be added here… # create rest-filled measures for expected parts that had no tag in this for eachN in expectedNs: @@ -3279,6 +3332,7 @@ def sectionScoreCore(elem, allPartNs, slurBundle, *, # only process elements if this is a
if measureTag == eachElem.tag and sectionTag == elem.tag: backupMeasureNum += 1 + # process all the stuff in the measureResult = measureFromElement(eachElem, backupMeasureNum, allPartNs, slurBundle=slurBundle, @@ -3291,7 +3345,7 @@ def sectionScoreCore(elem, allPartNs, slurBundle, *, inNextThing[eachN] = [] # if we got a left-side barline from the previous measure, use it if nextMeasureLeft is not None: - measureResult[eachN].leftBarline = deepcopy(nextMeasureLeft) + measureResult[eachN].leftBarline = nextMeasureLeft # add this Measure to the Part parsed[eachN].append(measureResult[eachN]) # if we got a barline for the next @@ -3305,13 +3359,8 @@ def sectionScoreCore(elem, allPartNs, slurBundle, *, for allPartObject in localResult['all-part objects']: if isinstance(allPartObject, meter.TimeSignature): activeMeter = allPartObject - for i, eachN in enumerate(allPartNs): - if i == 0: - to_insert = allPartObject - else: - # a single Music21Object should not exist in multiple parts - to_insert = deepcopy(allPartObject) - inNextThing[eachN].append(to_insert) + for eachN in allPartNs: + inNextThing[eachN].append(allPartObject) for eachN in allPartNs: if eachN in localResult: for eachObj in localResult[eachN].values(): @@ -3345,6 +3394,7 @@ def sectionScoreCore(elem, allPartNs, slurBundle, *, # put those into the first Measure object we encounter in this Part # TODO: this is where the Instruments get added # TODO: I think "eachList" really means "each list that will become a Part" + if inNextThing[eachN]: # we have to put Instrument objects just before the Measure to which they apply theInstr = None @@ -3377,11 +3427,13 @@ def sectionScoreCore(elem, allPartNs, slurBundle, *, # must "flatten" everything so it doesn't cause a disaster when we try to make # a Part out of it. for eachObj in eachList: - parsed[eachN].append(eachObj) + if eachN in parsed.keys(): + parsed[eachN].append(eachObj) elif scoreTag == elem.tag: # If this is a , we can just append the result of each
to the # list that will become the Part. - parsed[eachN].append(eachList) + if eachN in parsed.keys(): + parsed[eachN].append(eachList) elif eachElem.tag not in _IGNORE_UNPROCESSED: environLocal.printDebug(_UNPROCESSED_SUBELEMENT.format(eachElem.tag, elem.tag)) @@ -3485,8 +3537,11 @@ def scoreFromElement(elem, slurBundle): # Get a tuple of all the @n attributes for the tags in this score. Each tag # corresponds to what will be a music21 Part. + + # UPDATE: If tags are found, they will also be collected as a separate 'part' to process them later. allPartNs = allPartsPresent(elem) + # This is the actual processing. parsed = sectionScoreCore(elem, allPartNs, slurBundle=slurBundle)[0] @@ -3494,13 +3549,28 @@ def scoreFromElement(elem, slurBundle): # We must iterate here over "allPartNs," which preserves the part-order found in the MEI # document. Iterating the keys in "parsed" would not preserve the order. environLocal.printDebug('*** making the Score') + + # Extract collected information stored in the dict unter th 'fb' key + if 'fb' in parsed.keys(): + harms = parsed['fb'][0] + del parsed['fb'] + allPartNs = allPartNs[0:-1] + theScore = [stream.Part() for _ in range(len(allPartNs))] + for i, eachN in enumerate(allPartNs): # set "atSoundingPitch" so transposition works theScore[i].atSoundingPitch = False for eachObj in parsed[eachN]: theScore[i].append(eachObj) + theScore = stream.Score(theScore) + + # loop through measures to insert harm elements from harms list at the right offsets + for index, measureOffset in enumerate(theScore.measureOffsetMap().keys()): + hms = harms[index]['fb'] + for h in hms: + theScore.insert(measureOffset + h[0], h[1]) # put slurs in the Score theScore.append(list(slurBundle)) From 621aef39cd121a6f29dced9daa832ba3aa8b2bb1 Mon Sep 17 00:00:00 2001 From: Moritz Heffter Date: Sun, 26 Feb 2023 16:11:43 +0100 Subject: [PATCH 25/58] fixed import and moved figuredBassIndication to harmony --- music21/figuredBass/notation.py | 27 --------------------------- music21/harmony.py | 27 +++++++++++++++++++++++++++ music21/mei/base.py | 4 ++-- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/music21/figuredBass/notation.py b/music21/figuredBass/notation.py index f827450749..e3c76f8689 100644 --- a/music21/figuredBass/notation.py +++ b/music21/figuredBass/notation.py @@ -636,33 +636,6 @@ def convertToPitch(pitchString): raise TypeError('Cannot convert ' + pitchString + ' to a music21 Pitch.') -class FiguredBassIndication(harmony.Harmony): - isFigure = True - tstamp = 1 - def __init__(self, figs=None, **keywords): - super().__init__(**keywords) - if figs: - if isinstance(figs, list): - fig_string = str(figs[0]) - for sf in figs: - fig_string += f',{sf}' - figs = fig_string - #pass - else: - figs = '' - self.__fig_notation = Notation(figs) - - @property - def fig_notation(self) -> Notation: - return self.__fig_notation - - @fig_notation.setter - def fig_notation(self, figs): - self.__fig_notation = Notation(figs) - - def __repr__(self): - return f'<{self.__class__.__name__} figures: {self.fig_notation.notationColumn}>' - _DOC_ORDER = [Notation, Figure, Modifier] diff --git a/music21/harmony.py b/music21/harmony.py index 73787a2355..58b85e99d9 100644 --- a/music21/harmony.py +++ b/music21/harmony.py @@ -31,6 +31,7 @@ from music21 import environment from music21 import exceptions21 from music21.figuredBass import realizerScale +from music21.figuredBass import notation from music21 import interval from music21 import key from music21 import pitch @@ -2499,6 +2500,32 @@ def transpose(self: NCT, _value, *, inPlace=False) -> NCT | None: # ------------------------------------------------------------------------------ +class FiguredBassIndication(Harmony): + isFigure = True + tstamp = 1 + def __init__(self, figs=None, **keywords): + super().__init__(**keywords) + if figs: + if isinstance(figs, list): + fig_string = str(figs[0]) + for sf in figs: + fig_string += f',{sf}' + figs = fig_string + #pass + else: + figs = '' + self.__fig_notation = notation.Notation(figs) + + @property + def fig_notation(self) -> notation.Notation: + return self.__fig_notation + + @fig_notation.setter + def fig_notation(self, figs): + self.__fig_notation = notation.Notation(figs) + + def __repr__(self): + return f'<{self.__class__.__name__} figures: {self.fig_notation.notationColumn}>' def realizeChordSymbolDurations(piece): ''' diff --git a/music21/mei/base.py b/music21/mei/base.py index da504e8d99..76f41cfdd4 100644 --- a/music21/mei/base.py +++ b/music21/mei/base.py @@ -200,7 +200,7 @@ from music21 import stream from music21 import spanner from music21 import tie -from music21 import figuredBass +from music21 import harmony _MOD = 'mei.base' environLocal = environment.Environment(_MOD) @@ -2530,7 +2530,7 @@ def figuredbassFromElement(elem, slurBundle=None): dauer = float(subElement.attrib['dur.metrical']) # Generate a FiguredBassIndication object and set the collected information - theFbNotation = figuredBass.notation.FiguredBassIndication(fb_notation) + theFbNotation = harmony.FiguredBassIndication(fb_notation) theFbNotation.id = id theFbNotation.duration = duration.Duration(quarterLength=dauer) From 7e416cf7abcb1ed6fed92851c49e684267dbd3e9 Mon Sep 17 00:00:00 2001 From: Moritz Heffter Date: Sun, 26 Feb 2023 18:43:49 +0100 Subject: [PATCH 26/58] added meterSig to mei import --- music21/mei/base.py | 22 ++++++++++++++++++++-- music21/musicxml/m21ToXml.py | 4 ++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/music21/mei/base.py b/music21/mei/base.py index 76f41cfdd4..cbc0f7c040 100644 --- a/music21/mei/base.py +++ b/music21/mei/base.py @@ -1797,7 +1797,8 @@ def staffDefFromElement(elem, slurBundle=None): # pylint: disable=unused-argume - MEI.shared: clefGrp keySig label layerDef ''' # mapping from tag name to our converter function - tagToFunction = {f'{MEI_NS}clef': clefFromElement} + tagToFunction = {f'{MEI_NS}clef': clefFromElement, + f'{MEI_NS}meterSig': meterSigFromElement} # first make the Instrument post = elem.find(f'{MEI_NS}instrDef') @@ -1824,7 +1825,10 @@ def staffDefFromElement(elem, slurBundle=None): # pylint: disable=unused-argume # --> time signature if elem.get('meter.count') is not None: post['meter'] = _timeSigFromAttrs(elem) - + # --> or + if elem.find(f'{MEI_NS}meterSig') is not None: + post['meter'] = meterSigFromElement(elem.find(f'{MEI_NS}meterSig')) + # --> key signature if elem.get('key.pname') is not None or elem.get('key.sig') is not None: post['key'] = _keySigFromAttrs(elem) @@ -1848,6 +1852,20 @@ def staffDefFromElement(elem, slurBundle=None): # pylint: disable=unused-argume return post +def meterSigFromElement(elem, slurBundle=None) -> meter.TimeSignature: + ''' Container for information on meter and TimeSignature. + + In MEI 4: (MEI.cmn module) + + :returns: A meter.TimeSignature that is created from the @count und @unit attributes. + + If a xml:id is set it is provided. + ''' + + ts = meter.TimeSignature(f"{elem.get('count')!s}/{elem.get('unit')!s}") + if elem.get('xml:id') is not None: + ts.id = elem.get('xml:id') + return ts def dotFromElement(elem, slurBundle=None): # pylint: disable=unused-argument ''' diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index afea40fbd8..f570b46a12 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -365,6 +365,7 @@ class GeneralObjectExporter: ('DiatonicScale', 'fromDiatonicScale'), ('Scale', 'fromScale'), ('Music21Object', 'fromMusic21Object'), + ('FiguredBassIndication', 'fromFiguredBassIndication'), ]) def __init__(self, obj: prebase.ProtoM21Object | None = None): @@ -521,6 +522,9 @@ def fromGeneralObject(self, obj: prebase.ProtoM21Object): ) return outObj + def fromFiguredBassIndication(self): + print('was here!') + def fromScore(self, sc): ''' Runs :meth:`~music21.stream.Score.makeNotation` on the copy. From 808531015ae31b9ebb82b27fc941acaee3f699a8 Mon Sep 17 00:00:00 2001 From: Moritz Heffter Date: Tue, 28 Feb 2023 17:51:15 +0100 Subject: [PATCH 27/58] first working musicxml output for FiguredBassIndication Objects --- music21/figuredBass/notation.py | 29 ++++++++++ music21/musicxml/m21ToXml.py | 95 +++++++++++++++++++++++++++++++-- 2 files changed, 120 insertions(+), 4 deletions(-) diff --git a/music21/figuredBass/notation.py b/music21/figuredBass/notation.py index e3c76f8689..8b05d08773 100644 --- a/music21/figuredBass/notation.py +++ b/music21/figuredBass/notation.py @@ -31,6 +31,25 @@ (2,): (6, 4, 2), } +prefixes = ["+", "#", "++", "##"] +suffixes = ["\\"] + +modifiersDictXmlToM21 = { + "sharp": "#", + "flat": "b", + "double-sharp": "##", + "flat-flat": "bb", + "backslash": "\\" + } + +modifiersDictM21ToXml = { + "#": "sharp", + "b": "flat", + "##": "double-sharp", + "bb": "flat-flat", + "\\": "backslash", + "+": "sharp" +} class Notation(prebase.ProtoM21Object): ''' @@ -196,6 +215,7 @@ def __init__(self, notationColumn=None): # Convert to convenient notation self.modifiers = None self.figures = None + self.figuresFromNotationColumn = None self._getModifiers() self._getFigures() @@ -372,6 +392,15 @@ def _getFigures(self): self.figures = figures + figuresFromNotaCol = [] + + for i in range(len(self.origNumbers)): + number = self.origNumbers[i] + modifierString = self.origModStrings[i] + figure = Figure(number, modifierString) + figuresFromNotaCol.append(figure) + + self.figuresFromNotationColumn = figuresFromNotaCol class NotationException(exceptions21.Music21Exception): pass diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index f570b46a12..6d073b9c3f 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -62,6 +62,7 @@ from music21 import text from music21 import tie +from music21.figuredBass.notation import Figure, prefixes, suffixes, modifiersDictM21ToXml from music21.musicxml import helpers from music21.musicxml.partStaffExporter import PartStaffExporterMixin from music21.musicxml import xmlObjects @@ -365,7 +366,6 @@ class GeneralObjectExporter: ('DiatonicScale', 'fromDiatonicScale'), ('Scale', 'fromScale'), ('Music21Object', 'fromMusic21Object'), - ('FiguredBassIndication', 'fromFiguredBassIndication'), ]) def __init__(self, obj: prebase.ProtoM21Object | None = None): @@ -498,6 +498,7 @@ def fromGeneralObject(self, obj: prebase.ProtoM21Object): >>> len(v[note.Rest]) # original stream unchanged 0 ''' + classes = obj.classes outObj = None @@ -522,9 +523,6 @@ def fromGeneralObject(self, obj: prebase.ProtoM21Object): ) return outObj - def fromFiguredBassIndication(self): - print('was here!') - def fromScore(self, sc): ''' Runs :meth:`~music21.stream.Score.makeNotation` on the copy. @@ -1495,6 +1493,9 @@ def __init__(self, score: stream.Score | None = None, makeNotation: bool = True) self.firstScoreLayout: layout.ScoreLayout | None = None self.textBoxes: list[text.TextBox] = [] self.highestTime = 0.0 + self.fb_part = -1 + self.fbis_dict = {} + self.currentDivisions = defaults.divisionsPerQuarter self.refStreamOrTimeRange = [0.0, self.highestTime] @@ -1605,6 +1606,7 @@ def scorePreliminaries(self) -> None: ''' self.setScoreLayouts() self.setMeterStream() + self.getFiguredBassIndications() self.setPartsAndRefStream() # get all text boxes self.textBoxes = list(self.stream[text.TextBox]) @@ -1705,7 +1707,24 @@ def setScoreLayouts(self) -> None: self.firstScoreLayout = scoreLayouts.first() self.scoreLayouts = list(scoreLayouts) +<<<<<<< HEAD def _populatePartExporterList(self) -> None: +======= + def getFiguredBassIndications(self): + ''' + Collect all harmony.FiguredBassIndications found in the score and store them + in a dict. The dict is later passed to the PartExporter specified + (standard value -1 for the lowest part/staff). With in the MeasureExporter the objeccts are + inserted locally in the measure and finally parsed to the converter. + ''' + for fbi in self.stream.getElementsByClass(harmony.FiguredBassIndication): + if fbi.offset not in self.fbis_dict.keys(): + self.fbis_dict[fbi.offset] = [fbi] + else: + self.fbis_dict[fbi.offset].append([fbi]) + + def _populatePartExporterList(self): +>>>>>>> 7ca383457 (first working musicxml output for FiguredBassIndication Objects) count = 0 sp = list(self.parts) for innerStream in sp: @@ -1717,6 +1736,11 @@ def _populatePartExporterList(self) -> None: pp = PartExporter(innerStream, parent=self) pp.spannerBundle = self.spannerBundle + + # add figuredBass information to the part where it should be attached later + if innerStream == sp[self.fb_part]: + pp.fbis = self.fbis_dict + self.partExporterList.append(pp) def parsePartlikeScore(self) -> None: @@ -2690,6 +2714,7 @@ def __init__(self, partObj = stream.Part() self.stream: stream.Part | stream.Score = partObj self.parent = parent # ScoreExporter + self.fbis = None self.xmlRoot = Element('part') if parent is None: @@ -3148,6 +3173,7 @@ class MeasureExporter(XMLExporterBase): ('ChordWithFretBoard', 'chordWithFretBoardToXml'), # these three ('ChordSymbol', 'chordSymbolToXml'), # must come before ('RomanNumeral', 'romanNumeralToXml'), # ChordBase + ('FiguredBassIndication', 'figuredBassToXml'), ('ChordBase', 'chordToXml'), ('Unpitched', 'unpitchedToXml'), ('Rest', 'restToXml'), @@ -3220,9 +3246,15 @@ def parse(self): self.setMxPrint() self.setMxAttributesObjectForStartOfMeasure() self.setLeftBarline() + # Look for FiguredBassIndications and add them to the local copy of the measure + # and after the other elements + if self.parent.fbis: + self.insertFiguredBassIndications() + # BIG ONE self.mainElementsParse() # continue + self.setRightBarline() return self.xmlRoot @@ -3514,6 +3546,22 @@ def parseOneElement( for sp in postList: root.append(sp) + def insertFiguredBassIndications(self) -> None: + ''' + Adds figured bass elements from the score to the measure. In a MusicXML file tags + usually stand before the corresponding note object. The order is then provided by parseFlatElements() + ''' + # get the measure range to map the corresponding figuredBass Items + measureRange = (self.stream.offset, self.stream.offset + self.stream.highestTime) + + # look if there are figures in the current measure and insert them + # to add them later + for o, f in self.parent.fbis.items(): + if o >= measureRange[0] and o < measureRange[1]: + for fbi in f: + self.stream.insert(o - self.stream.offset, fbi) + #print('FBI inserted:', fbi) + def _hasRelatedSpanners(self, obj) -> bool: ''' returns True if and only if: @@ -4570,6 +4618,45 @@ def chordToXml(self, c: chord.ChordBase) -> list[Element]: mxNoteList.append(self.noteToXml(n, noteIndexInChord=i, chordParent=c)) return mxNoteList + def figuredBassToXml(self, f: harmony.FiguredBassIndication): + if isinstance(f, harmony.FiguredBassIndication): + mxFB = self._figuresToXml(f) + + self.xmlRoot.append(mxFB) + #_synchronizeIds(mxFB, f) + return mxFB + + def _figuresToXml(self, f: harmony.FiguredBassIndication, figureCnt=1, noteIndexInChord=0, chordParent=None): + #do Figure elements + #self.addDividerComment('BEGIN: figured-bass') + + mxFB = Element('figured-bass') + for fig in f.fig_notation.figuresFromNotationColumn: + mxFigure = SubElement(mxFB, 'figure') + + #get only the fbnumber without prefixes or suffixes + mxFNumber = SubElement(mxFigure, 'figure-number') + if fig.number: + mxFNumber.text = str(fig.number) + else: + mxFNumber.text = '' + + #modifiers are eother handled as prefixes or suffixes here + fbModifier = fig.modifierString + if fbModifier: + mxModifier = SubElement(mxFigure, 'prefix') + mxModifier.text = modifiersDictM21ToXml[fbModifier] + + mxFbDuration = SubElement(mxFB, 'duration') + duration = round(f.quarterLength * self.currentDivisions) + if duration > 0: + mxFbDuration.text = str(duration) + else: + mxFbDuration.text = str(0) + + return mxFB + #self.addDividerComment('END: figured-bass') + def durationXml(self, dur: duration.Duration): # noinspection PyShadowingNames ''' From f10cbb9b0b9a11fa1a48cd09f6136014d80ee260 Mon Sep 17 00:00:00 2001 From: Moritz Heffter Date: Wed, 1 Mar 2023 00:38:08 +0100 Subject: [PATCH 28/58] improved figuredbass mei and musicxml import/export deals now with extenders --- music21/figuredBass/notation.py | 20 +++++++++++++++----- music21/mei/base.py | 7 +++++-- music21/musicxml/m21ToXml.py | 18 +++++++++++++----- 3 files changed, 33 insertions(+), 12 deletions(-) diff --git a/music21/figuredBass/notation.py b/music21/figuredBass/notation.py index 8b05d08773..6e49e98cc5 100644 --- a/music21/figuredBass/notation.py +++ b/music21/figuredBass/notation.py @@ -99,6 +99,7 @@ class Notation(prebase.ProtoM21Object): * '13' -> '13,11,9,7,5,3' + * '_' -> treated as an extender Figures are saved in order from left to right as found in the notationColumn. @@ -209,6 +210,7 @@ def __init__(self, notationColumn=None): self.origModStrings = None self.numbers = None self.modifierStrings = None + self.hasExtenders = False self._parseNotationColumn() self._translateToLonghand() @@ -248,8 +250,8 @@ def _parseNotationColumn(self): ''' delimiter = '[,]' figures = re.split(delimiter, self.notationColumn) - patternA1 = '([0-9]*)' - patternA2 = '([^0-9]*)' + patternA1 = '([0-9_]*)' + patternA2 = '([^0-9_]*)' numbers = [] modifierStrings = [] figureStrings = [] @@ -269,7 +271,11 @@ def _parseNotationColumn(self): number = None modifierString = None if m1: - number = int(m1[0].strip()) + if '_' in m1: + self.hasExtenders = True + number = '_' + else: + number = int(m1[0].strip()) if m2: modifierString = m2[0].strip() @@ -442,14 +448,18 @@ class Figure(prebase.ProtoM21Object): ''', } - def __init__(self, number=1, modifierString=None): + def __init__(self, number=1, modifierString=None, isExtender=None): self.number = number self.modifierString = modifierString self.modifier = Modifier(modifierString) + if self.number == '_': + self.isExtender = True + else: + self.isExtender = False def _reprInternal(self): mod = repr(self.modifier).replace('music21.figuredBass.notation.', '') - return f'{self.number} {mod}' + return f'{self.number} {mod} {self.isExtender}' # ------------------------------------------------------------------------------ diff --git a/music21/mei/base.py b/music21/mei/base.py index cbc0f7c040..28b2991e96 100644 --- a/music21/mei/base.py +++ b/music21/mei/base.py @@ -2532,7 +2532,7 @@ def figuredbassFromElement(elem, slurBundle=None): id = elem.get(_XMLID) fb_notation = '' dauer: float = 0 - + isExtender = False # loop through all child elements and collect tags for subElement in elem.findall('*'): if subElement.tag == f'{MEI_NS}f': @@ -2543,7 +2543,10 @@ def figuredbassFromElement(elem, slurBundle=None): fb_notation = subElement.text else: if 'extender' in subElement.attrib.keys(): - print('Extender found!') + if fb_notation != '': + fb_notation += f',_' + else: + fb_notation = '_' if 'dur.metrical' in subElement.attrib.keys(): dauer = float(subElement.attrib['dur.metrical']) diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index 6d073b9c3f..5ab45938e7 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -4637,7 +4637,12 @@ def _figuresToXml(self, f: harmony.FiguredBassIndication, figureCnt=1, noteIndex #get only the fbnumber without prefixes or suffixes mxFNumber = SubElement(mxFigure, 'figure-number') if fig.number: - mxFNumber.text = str(fig.number) + if fig.number == '_': + mxFNumber.text = '' + mxExtender = SubElement(mxFigure, 'extend') + mxExtender.set('type', 'continue') + else: + mxFNumber.text = str(fig.number) else: mxFNumber.text = '' @@ -4647,12 +4652,15 @@ def _figuresToXml(self, f: harmony.FiguredBassIndication, figureCnt=1, noteIndex mxModifier = SubElement(mxFigure, 'prefix') mxModifier.text = modifiersDictM21ToXml[fbModifier] - mxFbDuration = SubElement(mxFB, 'duration') + # look for information on duration. If duration is 0 + # jump over the tag duration = round(f.quarterLength * self.currentDivisions) if duration > 0: - mxFbDuration.text = str(duration) - else: - mxFbDuration.text = str(0) + mxFbDuration = SubElement(mxFB, 'duration') + if duration > 0: + mxFbDuration.text = str(duration) + else: + mxFbDuration.text = str(0) return mxFB #self.addDividerComment('END: figured-bass') From 52363bc5cbe7d30b49ecd0abdd77c23f1f77e7b6 Mon Sep 17 00:00:00 2001 From: Moritz Heffter Date: Sat, 4 Mar 2023 12:45:29 +0100 Subject: [PATCH 29/58] stable import and export of figured base. Some unicode characters might have to be added in the future --- music21/figuredBass/notation.py | 18 ++++++++-- music21/harmony.py | 6 ++-- music21/mei/base.py | 5 ++- music21/musicxml/m21ToXml.py | 63 ++++++++++++++++++++++++++------- music21/pitch.py | 1 + 5 files changed, 73 insertions(+), 20 deletions(-) diff --git a/music21/figuredBass/notation.py b/music21/figuredBass/notation.py index 6e49e98cc5..70ba4d3abe 100644 --- a/music21/figuredBass/notation.py +++ b/music21/figuredBass/notation.py @@ -15,7 +15,7 @@ from music21 import exceptions21 from music21 import pitch -from music21 import harmony + from music21 import prebase shorthandNotation = {(None,): (5, 3), @@ -48,7 +48,11 @@ "##": "double-sharp", "bb": "flat-flat", "\\": "backslash", - "+": "sharp" + "+": "sharp", + '\u266f': 'sharp', + '\u266e': 'natural', + '\u266d': 'flat', + '\u20e5': 'sharp' } class Notation(prebase.ProtoM21Object): @@ -369,6 +373,7 @@ def _getModifiers(self): modifiers = [] for i in range(len(self.numbers)): + print(i, self.modifierStrings) modifierString = self.modifierStrings[i] modifier = Modifier(modifierString) modifiers.append(modifier) @@ -459,7 +464,7 @@ def __init__(self, number=1, modifierString=None, isExtender=None): def _reprInternal(self): mod = repr(self.modifier).replace('music21.figuredBass.notation.', '') - return f'{self.number} {mod} {self.isExtender}' + return f'{self.number} Mods: {mod} hasExt: {self.isExtender}' # ------------------------------------------------------------------------------ @@ -473,6 +478,10 @@ def _reprInternal(self): '++': '##', '+++': '###', '++++': '####', + '\u266f': '#', + '\u266e': 'n', + '\u266d': 'b', + '\u20e5': '#' } @@ -532,6 +541,8 @@ class Modifier(prebase.ProtoM21Object): } def __init__(self, modifierString=None): + if modifierString is not None: + print('Input: ', modifierString, isinstance(modifierString, str), modifierString == '\u20e5') self.modifierString = modifierString self.accidental = self._toAccidental() @@ -566,6 +577,7 @@ def _toAccidental(self): return None a = pitch.Accidental() + print('Modifer String', self.modifierString) try: a.set(self.modifierString) except pitch.AccidentalException: diff --git a/music21/harmony.py b/music21/harmony.py index 58b85e99d9..27722663f7 100644 --- a/music21/harmony.py +++ b/music21/harmony.py @@ -2514,15 +2514,15 @@ def __init__(self, figs=None, **keywords): #pass else: figs = '' - self.__fig_notation = notation.Notation(figs) + self._fig_notation = notation.Notation(figs) @property def fig_notation(self) -> notation.Notation: - return self.__fig_notation + return self._fig_notation @fig_notation.setter def fig_notation(self, figs): - self.__fig_notation = notation.Notation(figs) + self._fig_notation = notation.Notation(figs) def __repr__(self): return f'<{self.__class__.__name__} figures: {self.fig_notation.notationColumn}>' diff --git a/music21/mei/base.py b/music21/mei/base.py index 28b2991e96..c6e65a5134 100644 --- a/music21/mei/base.py +++ b/music21/mei/base.py @@ -2204,10 +2204,14 @@ def noteFromElement(elem, slurBundle=None): f'{MEI_NS}artic': articFromElement, f'{MEI_NS}accid': accidFromElement, f'{MEI_NS}syl': sylFromElement} + + #print('note', elem, elem.attrib) # start with a Note with Pitch theNote = _accidentalFromAttr(elem.get('accid')) theNote = safePitch(elem.get('pname', ''), theNote, elem.get('oct', '')) + #print('thenote', theNote, isinstance(theNote, pitch.Pitch)) + theNote = note.Note(theNote) # set the Note's duration @@ -2532,7 +2536,6 @@ def figuredbassFromElement(elem, slurBundle=None): id = elem.get(_XMLID) fb_notation = '' dauer: float = 0 - isExtender = False # loop through all child elements and collect tags for subElement in elem.findall('*'): if subElement.tag == f'{MEI_NS}f': diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index 5ab45938e7..9f8d97a76e 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -3385,7 +3385,20 @@ def parseFlatElements( # That way chord symbols and other 0-width objects appear before notes as much as # possible. objIterator: OffsetIterator[base.Music21Object] = OffsetIterator(m) - for objGroup in objIterator: + + # Prepare the iteration by offsets. If there are FiguredBassIndication objects + # first group them in one list together with their note, instead of handling them + # seperately. O + groupedObjList = [] + for els in objIterator: + if len(groupedObjList) > 0: + if len(els) == 1 and isinstance(els[0], harmony.FiguredBassIndication): + print('**multiple fb found**') + groupedObjList[-1].append(els[0]) + continue + groupedObjList.append(els) + + for objGroup in groupedObjList: groupOffset = m.elementOffset(objGroup[0]) offsetToMoveForward = groupOffset - self.offsetInMeasure # PyCharm typechecker does not properly read __next__ from OffsetIterator @@ -3414,6 +3427,7 @@ def parseFlatElements( if hasSpannerAnchors: self.parseOneElement(obj, AppendSpanners.NONE) else: + # ENTRY FOR FB self.parseOneElement(obj, AppendSpanners.NORMAL) for n in notesForLater: @@ -3548,8 +3562,11 @@ def parseOneElement( def insertFiguredBassIndications(self) -> None: ''' - Adds figured bass elements from the score to the measure. In a MusicXML file tags - usually stand before the corresponding note object. The order is then provided by parseFlatElements() + Adds relevant figured bass elements collected from the stream.Score to the + current stream.Measure object parsed afterwards. + + In a MusicXML file tags usually stand before the corresponding note object. + This order will be observed by parseFlatElements() function. Same for multiple figures at one note. ''' # get the measure range to map the corresponding figuredBass Items measureRange = (self.stream.offset, self.stream.offset + self.stream.highestTime) @@ -3560,7 +3577,7 @@ def insertFiguredBassIndications(self) -> None: if o >= measureRange[0] and o < measureRange[1]: for fbi in f: self.stream.insert(o - self.stream.offset, fbi) - #print('FBI inserted:', fbi) + #print('FBI inserted:', fbi, fbi.offset) def _hasRelatedSpanners(self, obj) -> bool: ''' @@ -4619,14 +4636,25 @@ def chordToXml(self, c: chord.ChordBase) -> list[Element]: return mxNoteList def figuredBassToXml(self, f: harmony.FiguredBassIndication): + # For FiguredBassElements we need to check whether there are + # multiple figures for one note. + # Therefore we compare offset of the figure and the current offsetInMeasure variable + print('*f:', f.offset, self.offsetInMeasure) + if f.offset > self.offsetInMeasure: + print('more than one figure found', (f.offset - self.offsetInMeasure) * self.currentDivisions) + multipleFigures = True + else: + multipleFigures = False + if isinstance(f, harmony.FiguredBassIndication): - mxFB = self._figuresToXml(f) + mxFB = self._figuresToXml(f, multipleFigures=multipleFigures) self.xmlRoot.append(mxFB) + self._fbiBefore = (f.offset, mxFB) #_synchronizeIds(mxFB, f) return mxFB - def _figuresToXml(self, f: harmony.FiguredBassIndication, figureCnt=1, noteIndexInChord=0, chordParent=None): + def _figuresToXml(self, f: harmony.FiguredBassIndication, multipleFigures=False, figureCnt=1, noteIndexInChord=0, chordParent=None): #do Figure elements #self.addDividerComment('BEGIN: figured-bass') @@ -4654,14 +4682,23 @@ def _figuresToXml(self, f: harmony.FiguredBassIndication, figureCnt=1, noteIndex # look for information on duration. If duration is 0 # jump over the tag - duration = round(f.quarterLength * self.currentDivisions) - if duration > 0: - mxFbDuration = SubElement(mxFB, 'duration') - if duration > 0: - mxFbDuration.text = str(duration) + # If we have multiple figures we have to set a tag + # and update the tag one before. + if multipleFigures: + dura = round((f.offset - self.offsetInMeasure) * self.currentDivisions) + # Update figures-bass tag before + fbDuration_before = self._fbiBefore[1] + if fbDuration_before.find('duration'): + fbDuration_before.find('duration').text = str(dura) else: - mxFbDuration.text = str(0) - + SubElement(fbDuration_before, 'duration').text = str(dura) + else: + dura = round(f.quarterLength * self.currentDivisions) + # add to the figure itself if dura > 0 + if dura > 0: + mxFbDuration = SubElement(mxFB, 'duration') + mxFbDuration.text = str(dura) + return mxFB #self.addDividerComment('END: figured-bass') diff --git a/music21/pitch.py b/music21/pitch.py index 4ea598b2cf..952fa6a651 100644 --- a/music21/pitch.py +++ b/music21/pitch.py @@ -1954,6 +1954,7 @@ def __init__(self, else: # is a number # is a midiNumber or a ps -- a float midiNumber # get step and accidental w/o octave + print(name) self.step, self._accidental = _convertPsToStep(name)[0:2] self.spellingIsInferred = True if name >= 12: # is not a pitchClass From 413c17b8196c85ec5c34d0b5184f97098881b623 Mon Sep 17 00:00:00 2001 From: Moritz Heffter Date: Sat, 4 Mar 2023 19:50:22 +0100 Subject: [PATCH 30/58] cleanup mei import and musicxml export --- music21/figuredBass/notation.py | 4 ---- music21/mei/base.py | 4 ---- music21/musicxml/m21ToXml.py | 10 +++++----- music21/pitch.py | 1 - 4 files changed, 5 insertions(+), 14 deletions(-) diff --git a/music21/figuredBass/notation.py b/music21/figuredBass/notation.py index 70ba4d3abe..7da5461ec6 100644 --- a/music21/figuredBass/notation.py +++ b/music21/figuredBass/notation.py @@ -373,7 +373,6 @@ def _getModifiers(self): modifiers = [] for i in range(len(self.numbers)): - print(i, self.modifierStrings) modifierString = self.modifierStrings[i] modifier = Modifier(modifierString) modifiers.append(modifier) @@ -541,8 +540,6 @@ class Modifier(prebase.ProtoM21Object): } def __init__(self, modifierString=None): - if modifierString is not None: - print('Input: ', modifierString, isinstance(modifierString, str), modifierString == '\u20e5') self.modifierString = modifierString self.accidental = self._toAccidental() @@ -577,7 +574,6 @@ def _toAccidental(self): return None a = pitch.Accidental() - print('Modifer String', self.modifierString) try: a.set(self.modifierString) except pitch.AccidentalException: diff --git a/music21/mei/base.py b/music21/mei/base.py index c6e65a5134..2436721928 100644 --- a/music21/mei/base.py +++ b/music21/mei/base.py @@ -2205,13 +2205,9 @@ def noteFromElement(elem, slurBundle=None): f'{MEI_NS}accid': accidFromElement, f'{MEI_NS}syl': sylFromElement} - #print('note', elem, elem.attrib) - # start with a Note with Pitch theNote = _accidentalFromAttr(elem.get('accid')) theNote = safePitch(elem.get('pname', ''), theNote, elem.get('oct', '')) - #print('thenote', theNote, isinstance(theNote, pitch.Pitch)) - theNote = note.Note(theNote) # set the Note's duration diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index 9f8d97a76e..7ef41431c8 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -62,7 +62,7 @@ from music21 import text from music21 import tie -from music21.figuredBass.notation import Figure, prefixes, suffixes, modifiersDictM21ToXml +from music21.figuredBass.notation import modifiersDictM21ToXml from music21.musicxml import helpers from music21.musicxml.partStaffExporter import PartStaffExporterMixin from music21.musicxml import xmlObjects @@ -498,7 +498,6 @@ def fromGeneralObject(self, obj: prebase.ProtoM21Object): >>> len(v[note.Rest]) # original stream unchanged 0 ''' - classes = obj.classes outObj = None @@ -3393,7 +3392,7 @@ def parseFlatElements( for els in objIterator: if len(groupedObjList) > 0: if len(els) == 1 and isinstance(els[0], harmony.FiguredBassIndication): - print('**multiple fb found**') + #print('**multiple fb found**') groupedObjList[-1].append(els[0]) continue groupedObjList.append(els) @@ -4639,9 +4638,10 @@ def figuredBassToXml(self, f: harmony.FiguredBassIndication): # For FiguredBassElements we need to check whether there are # multiple figures for one note. # Therefore we compare offset of the figure and the current offsetInMeasure variable - print('*f:', f.offset, self.offsetInMeasure) + # print('*f:', f.offset, self.offsetInMeasure) if f.offset > self.offsetInMeasure: - print('more than one figure found', (f.offset - self.offsetInMeasure) * self.currentDivisions) + # Handle multiple figures or not + # print('more than one figure found', (f.offset - self.offsetInMeasure) * self.currentDivisions) multipleFigures = True else: multipleFigures = False diff --git a/music21/pitch.py b/music21/pitch.py index 952fa6a651..4ea598b2cf 100644 --- a/music21/pitch.py +++ b/music21/pitch.py @@ -1954,7 +1954,6 @@ def __init__(self, else: # is a number # is a midiNumber or a ps -- a float midiNumber # get step and accidental w/o octave - print(name) self.step, self._accidental = _convertPsToStep(name)[0:2] self.spellingIsInferred = True if name >= 12: # is not a pitchClass From 8244b3ef68ff9821bcce2e000447011fb74cce3c Mon Sep 17 00:00:00 2001 From: Moritz Heffter Date: Tue, 7 Mar 2023 01:55:50 +0100 Subject: [PATCH 31/58] xml import and export fixed problems with more than two figures per note --- music21/figuredBass/notation.py | 1 - music21/musicxml/m21ToXml.py | 36 ++++++++++++++---- music21/musicxml/xmlToM21.py | 65 ++++++++++++++++++++++++++++++++- 3 files changed, 93 insertions(+), 9 deletions(-) diff --git a/music21/figuredBass/notation.py b/music21/figuredBass/notation.py index 7da5461ec6..cf07333e80 100644 --- a/music21/figuredBass/notation.py +++ b/music21/figuredBass/notation.py @@ -15,7 +15,6 @@ from music21 import exceptions21 from music21 import pitch - from music21 import prebase shorthandNotation = {(None,): (5, 3), diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index 7ef41431c8..df0046c883 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -3217,6 +3217,8 @@ def __init__(self, self.mxTranspose = None self.measureOffsetStart = 0.0 self.offsetInMeasure = 0.0 + self.offsetFiguresInMeasure: float = 0.0 + self.tempFigureDuration: float = 0.0 self.currentVoiceId: int | str | None = None self.nextFreeVoiceNumber: int = 1 self.nextArpeggioNumber: int = 1 @@ -4659,6 +4661,7 @@ def _figuresToXml(self, f: harmony.FiguredBassIndication, multipleFigures=False, #self.addDividerComment('BEGIN: figured-bass') mxFB = Element('figured-bass') + dura = 0 for fig in f.fig_notation.figuresFromNotationColumn: mxFigure = SubElement(mxFB, 'figure') @@ -4685,19 +4688,38 @@ def _figuresToXml(self, f: harmony.FiguredBassIndication, multipleFigures=False, # If we have multiple figures we have to set a tag # and update the tag one before. if multipleFigures: - dura = round((f.offset - self.offsetInMeasure) * self.currentDivisions) + dura = round((f.offset - self.offsetInMeasure - self.offsetFiguresInMeasure) * self.currentDivisions) + self.offsetFiguresInMeasure = f.offset - self.offsetInMeasure + #print('Dura', fig, dura, self.offsetFiguresInMeasure) # Update figures-bass tag before fbDuration_before = self._fbiBefore[1] - if fbDuration_before.find('duration'): - fbDuration_before.find('duration').text = str(dura) + + # Check whether the figured-bass tag before already has a tag. + # If not create one and set its value. Otherwise update the value + if fbDuration_before.find('duration') == None: + #print('Create') + newDura = SubElement(fbDuration_before, 'duration') + newDura.text = str(dura) + #newDura.attrib['created'] = f'with {dura}' else: - SubElement(fbDuration_before, 'duration').text = str(dura) + #print('UPDATE') + + #duraBefore = fbDuration_before.find('duration').text + for d in fbDuration_before.findall('duration'): + d.text = str(dura) + #d.attrib['update'] = f'from {duraBefore}' + self.tempFigureDuration = dura else: - dura = round(f.quarterLength * self.currentDivisions) + self.offsetFiguresInMeasure = 0.0 + # dura is likely 0. + # dura = round(f.quarterLength * self.currentDivisions) + # print('Calc: ', dura) # add to the figure itself if dura > 0 - if dura > 0: + if self.tempFigureDuration > 0: + #print(f.quarterLength * self.currentDivisions) mxFbDuration = SubElement(mxFB, 'duration') - mxFbDuration.text = str(dura) + mxFbDuration.text = str(round(self.tempFigureDuration)) + self.tempFigureDuration = 0.0 return mxFB #self.addDividerComment('END: figured-bass') diff --git a/music21/musicxml/xmlToM21.py b/music21/musicxml/xmlToM21.py index 62ac872906..86639419d2 100644 --- a/music21/musicxml/xmlToM21.py +++ b/music21/musicxml/xmlToM21.py @@ -841,6 +841,7 @@ def __init__(self): self.m21PartObjectsById = {} self.partGroupList = [] self.parts = [] + self.fbis: list[harmony.FiguredBassIndication] | None = None self.musicXmlVersion = defaults.musicxmlVersion @@ -924,6 +925,10 @@ def xmlRootToScore(self, mxScore, inputM21=None): if part is not None: # for instance, in partStreams s.coreInsert(0.0, part) self.m21PartObjectsById[partId] = part + for fbi in self.fbis: + print('Figure:', fbi[0], fbi[1], 'Stimmt\'s?') + # fbi.offset = + s.insert(fbi[0], fbi[1]) self.partGroups() @@ -2358,6 +2363,13 @@ def applyMultiMeasureRest(self, r: note.Rest): self.stream.insert(0, self.activeMultiMeasureRestSpanner) self.activeMultiMeasureRestSpanner = None + def appendFbis(self, fbi, measureOffset): + absOffset = self.lastMeasureOffset + measureOffset + if self.parent.fbis: + self.parent.fbis.append((absOffset, fbi)) + else: + self.parent.fbis = [(absOffset, fbi)] + #print(self.parent.fbis) # ----------------------------------------------------------------------------- class MeasureParser(XMLParserBase): @@ -2396,8 +2408,13 @@ class MeasureParser(XMLParserBase): 'direction': 'xmlDirection', 'attributes': 'parseAttributesTag', 'harmony': 'xmlHarmony', +<<<<<<< HEAD 'figured-bass': None, 'sound': 'xmlSound', +======= + 'figured-bass': 'xmlToFiguredBass', + 'sound': None, +>>>>>>> 82995d732 (xml import and export fixed problems with more than two figures per note) 'barline': 'xmlBarline', 'grouping': None, 'link': None, @@ -2463,7 +2480,10 @@ def __init__(self, # what is the offset in the measure of the current note position? self.offsetMeasureNote: OffsetQL = 0.0 - + + # Offset Calc if more than one figure is set under a single note + self.lastFigureDuration = 0 + # keep track of the last rest that was added with a forward tag. # there are many pieces that end with incomplete measures that # older versions of Finale put a forward tag at the end, but this @@ -5239,6 +5259,49 @@ def xmlToChordSymbol( return cs + def xmlToFiguredBass(self, mxFiguredBass): + #print('Hello from xmlToFiguredBass', mxFiguredBass.findall('*')) + fb_strings: list[str] = [] + sep = ',' + d: duration.Duration | None = None + offsetFbi = self.offsetMeasureNote + + for figure in mxFiguredBass.findall('*'): + for el in figure.findall('*'): + #print(' ', el) + if el.tag == 'figure-number': + fb_strings.append(el.text) + if el.tag == 'extend': + if 'type' in el.attrib.keys(): + if el.attrib['type'] == 'continue': + fb_strings.append('_') + + # If a is given, this usually means that there are multiple figures for a single note. + # We have to look for offsets here. + if figure.tag == 'duration': + #print('** Dauer:', figure.text, self.lastFigureDuration) + d = self.xmlToDuration(mxFiguredBass) + if self.lastFigureDuration > 0: + offsetFbi = self.offsetMeasureNote + self.lastFigureDuration + self.lastFigureDuration += d.quarterLength + else: + offsetFbi = self.offsetMeasureNote + self.lastFigureDuration = d.quarterLength + + # If is not in the tag list set self.lastFigureDuration to 0. + # This missing duration in MuseScore3 XML usually means that the following figure is again at a note's offset. + if mxFiguredBass.find('duration'): + self.lastFigureDuration = 0 + + fb_string = sep.join(fb_strings) + #print(fb_string, 'Offset', offsetFbi, self.lastFigureDuration) + fbi = harmony.FiguredBassIndication(fb_string) + if d: + fbi.quarterLength = d.quarterLength + # function in parent add add found objects. + # + self.parent.appendFbis(fbi, offsetFbi) + def xmlDirection(self, mxDirection): ''' convert a tag to one or more expressions, metronome marks, etc. From f9179f74848e10c1304cb70754423f26de9d5814 Mon Sep 17 00:00:00 2001 From: Moritz Heffter Date: Wed, 22 Mar 2023 00:39:47 +0100 Subject: [PATCH 32/58] fixed wrong offsets for multiple figures --- music21/musicxml/xmlToM21.py | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/music21/musicxml/xmlToM21.py b/music21/musicxml/xmlToM21.py index 86639419d2..bbb8fca834 100644 --- a/music21/musicxml/xmlToM21.py +++ b/music21/musicxml/xmlToM21.py @@ -2881,6 +2881,15 @@ def xmlToNote(self, mxNote: ET.Element) -> None: # only increment Chords after completion self.offsetMeasureNote += offsetIncrement self.endedWithForwardTag = None + + # reset offset for figures. This is needed to put in + # multiple FiguredBassIndications at one note at the right offset. + # Musicxml puts tags immediately before a tag, + # which means that we have to reset a given offset duration of some + # tags after inserting the coressponding note and + # before going to a new note. + self.lastFigureDuration = 0 + def xmlToChord(self, mxNoteList: list[ET.Element]) -> chord.ChordBase: # noinspection PyShadowingNames @@ -5265,6 +5274,7 @@ def xmlToFiguredBass(self, mxFiguredBass): sep = ',' d: duration.Duration | None = None offsetFbi = self.offsetMeasureNote + #print('===') for figure in mxFiguredBass.findall('*'): for el in figure.findall('*'): @@ -5279,19 +5289,27 @@ def xmlToFiguredBass(self, mxFiguredBass): # If a is given, this usually means that there are multiple figures for a single note. # We have to look for offsets here. if figure.tag == 'duration': - #print('** Dauer:', figure.text, self.lastFigureDuration) + #print(fb_strings) + #print('** Dauer:', self.lastFigureDuration) d = self.xmlToDuration(mxFiguredBass) + #print('** D:', d) if self.lastFigureDuration > 0: offsetFbi = self.offsetMeasureNote + self.lastFigureDuration self.lastFigureDuration += d.quarterLength + # print('***lfd', self.lastFigureDuration, 'offsetFbi', offsetFbi) else: offsetFbi = self.offsetMeasureNote self.lastFigureDuration = d.quarterLength - + # print('***lfd Ohne', self.lastFigureDuration, d.quarterLength, offsetFbi) + #else: + # self.lastFigureDuration = 0 # If is not in the tag list set self.lastFigureDuration to 0. # This missing duration in MuseScore3 XML usually means that the following figure is again at a note's offset. - if mxFiguredBass.find('duration'): - self.lastFigureDuration = 0 + #if mxFiguredBass.find('duration'): + # print('reset') + # self.lastFigureDuration = 0 + #else: + # print('H', mxFiguredBass.find('duration')) fb_string = sep.join(fb_strings) #print(fb_string, 'Offset', offsetFbi, self.lastFigureDuration) From 36492656be2f0b9f2b1594a601009147018290b5 Mon Sep 17 00:00:00 2001 From: Moritz Heffter Date: Wed, 22 Mar 2023 12:14:16 +0100 Subject: [PATCH 33/58] fixed export of multiple figures, where an already exsiting duration was erroneously set to 0 --- music21/musicxml/m21ToXml.py | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index df0046c883..dae88c2db0 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -3392,13 +3392,14 @@ def parseFlatElements( # seperately. O groupedObjList = [] for els in objIterator: + print(els) if len(groupedObjList) > 0: if len(els) == 1 and isinstance(els[0], harmony.FiguredBassIndication): #print('**multiple fb found**') groupedObjList[-1].append(els[0]) continue groupedObjList.append(els) - + #print('üüü', groupedObjList) for objGroup in groupedObjList: groupOffset = m.elementOffset(objGroup[0]) offsetToMoveForward = groupOffset - self.offsetInMeasure @@ -3428,10 +3429,12 @@ def parseFlatElements( if hasSpannerAnchors: self.parseOneElement(obj, AppendSpanners.NONE) else: - # ENTRY FOR FB + # ENTRY FOR Figured Bass Indications + print('object: ', obj) self.parseOneElement(obj, AppendSpanners.NORMAL) for n in notesForLater: + print('notes:', n) if n.isRest and n.style.hideObjectOnPrint and n.duration.type == 'inexpressible': # Prefer a gap in stream, to be filled with a tag by # fill_gap_with_forward_tag() rather than raising exceptions @@ -3576,8 +3579,13 @@ def insertFiguredBassIndications(self) -> None: # to add them later for o, f in self.parent.fbis.items(): if o >= measureRange[0] and o < measureRange[1]: + print('figures: ', f, o) for fbi in f: + #if isinstance(fbi, harmony.FiguredBassIndication): self.stream.insert(o - self.stream.offset, fbi) + #elif isinstance(fbi, list): + # for i in fbi: + # self.stream.insert(o - self.stream.offset, i) #print('FBI inserted:', fbi, fbi.offset) def _hasRelatedSpanners(self, obj) -> bool: @@ -4642,11 +4650,13 @@ def figuredBassToXml(self, f: harmony.FiguredBassIndication): # Therefore we compare offset of the figure and the current offsetInMeasure variable # print('*f:', f.offset, self.offsetInMeasure) if f.offset > self.offsetInMeasure: - # Handle multiple figures or not - # print('more than one figure found', (f.offset - self.offsetInMeasure) * self.currentDivisions) + # Handle multiple figures or not# + print('** more than one figure found', (f.offset - self.offsetInMeasure) * self.currentDivisions) multipleFigures = True else: multipleFigures = False + print('** stay:', (f.offset - self.offsetInMeasure) * self.currentDivisions) + if isinstance(f, harmony.FiguredBassIndication): mxFB = self._figuresToXml(f, multipleFigures=multipleFigures) @@ -4697,17 +4707,18 @@ def _figuresToXml(self, f: harmony.FiguredBassIndication, multipleFigures=False, # Check whether the figured-bass tag before already has a tag. # If not create one and set its value. Otherwise update the value if fbDuration_before.find('duration') == None: - #print('Create') newDura = SubElement(fbDuration_before, 'duration') newDura.text = str(dura) #newDura.attrib['created'] = f'with {dura}' else: - #print('UPDATE') - #duraBefore = fbDuration_before.find('duration').text - for d in fbDuration_before.findall('duration'): - d.text = str(dura) - #d.attrib['update'] = f'from {duraBefore}' + + # If dura is set to 0 skip. This happens e.g. at the beginning of a piece. + # Otherwise an already set duration tag is resetted to 0 + if dura > 0: + for d in fbDuration_before.findall('duration'): + d.text = str(dura) + #d.attrib['update'] = f'from {duraBefore}' self.tempFigureDuration = dura else: self.offsetFiguresInMeasure = 0.0 From e497210db2fa43f3810d882cb5a5e93bf7f070fe Mon Sep 17 00:00:00 2001 From: Moritz Heffter Date: Wed, 22 Mar 2023 15:43:28 +0100 Subject: [PATCH 34/58] cleaned upcomments and stuff --- music21/musicxml/m21ToXml.py | 44 +++++++++++------------------------- music21/musicxml/xmlToM21.py | 21 +++-------------- 2 files changed, 16 insertions(+), 49 deletions(-) diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index dae88c2db0..8f04910bb2 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -3392,7 +3392,7 @@ def parseFlatElements( # seperately. O groupedObjList = [] for els in objIterator: - print(els) + #print(els) if len(groupedObjList) > 0: if len(els) == 1 and isinstance(els[0], harmony.FiguredBassIndication): #print('**multiple fb found**') @@ -3430,11 +3430,9 @@ def parseFlatElements( self.parseOneElement(obj, AppendSpanners.NONE) else: # ENTRY FOR Figured Bass Indications - print('object: ', obj) self.parseOneElement(obj, AppendSpanners.NORMAL) for n in notesForLater: - print('notes:', n) if n.isRest and n.style.hideObjectOnPrint and n.duration.type == 'inexpressible': # Prefer a gap in stream, to be filled with a tag by # fill_gap_with_forward_tag() rather than raising exceptions @@ -3575,19 +3573,13 @@ def insertFiguredBassIndications(self) -> None: # get the measure range to map the corresponding figuredBass Items measureRange = (self.stream.offset, self.stream.offset + self.stream.highestTime) - # look if there are figures in the current measure and insert them - # to add them later + # Look if there are figures in the current measure and insert them + # to add them later. for o, f in self.parent.fbis.items(): if o >= measureRange[0] and o < measureRange[1]: - print('figures: ', f, o) for fbi in f: - #if isinstance(fbi, harmony.FiguredBassIndication): self.stream.insert(o - self.stream.offset, fbi) - #elif isinstance(fbi, list): - # for i in fbi: - # self.stream.insert(o - self.stream.offset, i) - #print('FBI inserted:', fbi, fbi.offset) - + def _hasRelatedSpanners(self, obj) -> bool: ''' returns True if and only if: @@ -4648,19 +4640,15 @@ def figuredBassToXml(self, f: harmony.FiguredBassIndication): # For FiguredBassElements we need to check whether there are # multiple figures for one note. # Therefore we compare offset of the figure and the current offsetInMeasure variable - # print('*f:', f.offset, self.offsetInMeasure) if f.offset > self.offsetInMeasure: - # Handle multiple figures or not# - print('** more than one figure found', (f.offset - self.offsetInMeasure) * self.currentDivisions) multipleFigures = True else: multipleFigures = False - print('** stay:', (f.offset - self.offsetInMeasure) * self.currentDivisions) - - if isinstance(f, harmony.FiguredBassIndication): - mxFB = self._figuresToXml(f, multipleFigures=multipleFigures) + # Hand the FiguredBassIndication over to the helper function. + mxFB = self._figuresToXml(f, multipleFigures=multipleFigures) + # Append it to xmlRoot self.xmlRoot.append(mxFB) self._fbiBefore = (f.offset, mxFB) #_synchronizeIds(mxFB, f) @@ -4687,7 +4675,7 @@ def _figuresToXml(self, f: harmony.FiguredBassIndication, multipleFigures=False, else: mxFNumber.text = '' - #modifiers are eother handled as prefixes or suffixes here + #modifiers are either handled as prefixes or suffixes here fbModifier = fig.modifierString if fbModifier: mxModifier = SubElement(mxFigure, 'prefix') @@ -4700,7 +4688,7 @@ def _figuresToXml(self, f: harmony.FiguredBassIndication, multipleFigures=False, if multipleFigures: dura = round((f.offset - self.offsetInMeasure - self.offsetFiguresInMeasure) * self.currentDivisions) self.offsetFiguresInMeasure = f.offset - self.offsetInMeasure - #print('Dura', fig, dura, self.offsetFiguresInMeasure) + # Update figures-bass tag before fbDuration_before = self._fbiBefore[1] @@ -4709,23 +4697,17 @@ def _figuresToXml(self, f: harmony.FiguredBassIndication, multipleFigures=False, if fbDuration_before.find('duration') == None: newDura = SubElement(fbDuration_before, 'duration') newDura.text = str(dura) - #newDura.attrib['created'] = f'with {dura}' else: - #duraBefore = fbDuration_before.find('duration').text - - # If dura is set to 0 skip. This happens e.g. at the beginning of a piece. - # Otherwise an already set duration tag is resetted to 0 + # If dura is set to 0 skip the update process. + # This happens e.g. at the beginning of a piece. + # Otherwise an already set duration tag is resetted to 0 which is not wanted. if dura > 0: for d in fbDuration_before.findall('duration'): d.text = str(dura) - #d.attrib['update'] = f'from {duraBefore}' self.tempFigureDuration = dura else: self.offsetFiguresInMeasure = 0.0 - # dura is likely 0. - # dura = round(f.quarterLength * self.currentDivisions) - # print('Calc: ', dura) - # add to the figure itself if dura > 0 + if self.tempFigureDuration > 0: #print(f.quarterLength * self.currentDivisions) mxFbDuration = SubElement(mxFB, 'duration') diff --git a/music21/musicxml/xmlToM21.py b/music21/musicxml/xmlToM21.py index bbb8fca834..4c7bcdaba3 100644 --- a/music21/musicxml/xmlToM21.py +++ b/music21/musicxml/xmlToM21.py @@ -926,8 +926,7 @@ def xmlRootToScore(self, mxScore, inputM21=None): s.coreInsert(0.0, part) self.m21PartObjectsById[partId] = part for fbi in self.fbis: - print('Figure:', fbi[0], fbi[1], 'Stimmt\'s?') - # fbi.offset = + s.insert(fbi[0], fbi[1]) self.partGroups() @@ -5274,7 +5273,6 @@ def xmlToFiguredBass(self, mxFiguredBass): sep = ',' d: duration.Duration | None = None offsetFbi = self.offsetMeasureNote - #print('===') for figure in mxFiguredBass.findall('*'): for el in figure.findall('*'): @@ -5289,31 +5287,18 @@ def xmlToFiguredBass(self, mxFiguredBass): # If a is given, this usually means that there are multiple figures for a single note. # We have to look for offsets here. if figure.tag == 'duration': - #print(fb_strings) - #print('** Dauer:', self.lastFigureDuration) d = self.xmlToDuration(mxFiguredBass) - #print('** D:', d) if self.lastFigureDuration > 0: offsetFbi = self.offsetMeasureNote + self.lastFigureDuration self.lastFigureDuration += d.quarterLength - # print('***lfd', self.lastFigureDuration, 'offsetFbi', offsetFbi) else: offsetFbi = self.offsetMeasureNote self.lastFigureDuration = d.quarterLength - # print('***lfd Ohne', self.lastFigureDuration, d.quarterLength, offsetFbi) - #else: - # self.lastFigureDuration = 0 - # If is not in the tag list set self.lastFigureDuration to 0. - # This missing duration in MuseScore3 XML usually means that the following figure is again at a note's offset. - #if mxFiguredBass.find('duration'): - # print('reset') - # self.lastFigureDuration = 0 - #else: - # print('H', mxFiguredBass.find('duration')) + fb_string = sep.join(fb_strings) - #print(fb_string, 'Offset', offsetFbi, self.lastFigureDuration) fbi = harmony.FiguredBassIndication(fb_string) + # If a duration is provided, set length of the FigureBassIndication if d: fbi.quarterLength = d.quarterLength # function in parent add add found objects. From c6ac193946f901080553e9b37b044c9ac5d5f318 Mon Sep 17 00:00:00 2001 From: Moritz Heffter Date: Wed, 22 Mar 2023 18:54:39 +0100 Subject: [PATCH 35/58] added support for prefix tags and solved problems with empty number tags --- music21/figuredBass/notation.py | 1 + music21/musicxml/xmlToM21.py | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/music21/figuredBass/notation.py b/music21/figuredBass/notation.py index cf07333e80..3bb5806606 100644 --- a/music21/figuredBass/notation.py +++ b/music21/figuredBass/notation.py @@ -36,6 +36,7 @@ modifiersDictXmlToM21 = { "sharp": "#", "flat": "b", + "natural": "\u266e", "double-sharp": "##", "flat-flat": "bb", "backslash": "\\" diff --git a/music21/musicxml/xmlToM21.py b/music21/musicxml/xmlToM21.py index 4c7bcdaba3..618d9fea77 100644 --- a/music21/musicxml/xmlToM21.py +++ b/music21/musicxml/xmlToM21.py @@ -54,6 +54,7 @@ from music21 import tempo from music21 import text # for text boxes from music21 import tie +from music21.figuredBass.notation import modifiersDictXmlToM21 from music21.musicxml import xmlObjects from music21.musicxml.xmlObjects import MusicXMLImportException, MusicXMLWarning @@ -5277,8 +5278,20 @@ def xmlToFiguredBass(self, mxFiguredBass): for figure in mxFiguredBass.findall('*'): for el in figure.findall('*'): #print(' ', el) + fb_number: str = '' + fb_prefix: str = '' if el.tag == 'figure-number': - fb_strings.append(el.text) + if el.text: + fb_number = el.text + if figure.findall('prefix'): + for prefix in figure.findall('prefix'): + + if prefix.text: + fb_prefix = modifiersDictXmlToM21[prefix.text] + # put prefix and number together + if fb_prefix + fb_number != '': + fb_strings.append(fb_prefix + fb_number) + if el.tag == 'extend': if 'type' in el.attrib.keys(): if el.attrib['type'] == 'continue': @@ -5295,7 +5308,7 @@ def xmlToFiguredBass(self, mxFiguredBass): offsetFbi = self.offsetMeasureNote self.lastFigureDuration = d.quarterLength - + #print('ü', fb_strings) fb_string = sep.join(fb_strings) fbi = harmony.FiguredBassIndication(fb_string) # If a duration is provided, set length of the FigureBassIndication From d8cef5625bac5284ad37be77720f8c0128c74dc4 Mon Sep 17 00:00:00 2001 From: Moritz Heffter Date: Thu, 23 Mar 2023 00:06:54 +0100 Subject: [PATCH 36/58] figured bass is now checked --- music21/musicxml/xmlToM21.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/music21/musicxml/xmlToM21.py b/music21/musicxml/xmlToM21.py index 618d9fea77..04389f208f 100644 --- a/music21/musicxml/xmlToM21.py +++ b/music21/musicxml/xmlToM21.py @@ -926,9 +926,10 @@ def xmlRootToScore(self, mxScore, inputM21=None): if part is not None: # for instance, in partStreams s.coreInsert(0.0, part) self.m21PartObjectsById[partId] = part - for fbi in self.fbis: - - s.insert(fbi[0], fbi[1]) + + if self.fbis: + for fbi in self.fbis: + s.insert(fbi[0], fbi[1]) self.partGroups() @@ -5276,6 +5277,7 @@ def xmlToFiguredBass(self, mxFiguredBass): offsetFbi = self.offsetMeasureNote for figure in mxFiguredBass.findall('*'): + # TODO: suffixes are ignored at the moment for el in figure.findall('*'): #print(' ', el) fb_number: str = '' From 4f098b1d027305b256604c43efdf110b9fca0acb Mon Sep 17 00:00:00 2001 From: Moritz Heffter Date: Fri, 24 Mar 2023 00:14:22 +0100 Subject: [PATCH 37/58] updated mei/base.py --- music21/mei/base.py | 177 ++++++++++++++++++----------------- music21/mei/test_base.py | 6 +- music21/musicxml/m21ToXml.py | 9 +- 3 files changed, 101 insertions(+), 91 deletions(-) diff --git a/music21/mei/base.py b/music21/mei/base.py index 2436721928..7964ad8a8e 100644 --- a/music21/mei/base.py +++ b/music21/mei/base.py @@ -1,18 +1,18 @@ # -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Name: mei/base.py -# Purpose: Public methods for the MEI module +# Purpose: Public interfaces for the MEI module # # Authors: Christopher Antila # -# Copyright: Copyright © 2014 Michael Scott Cuthbert and the music21 Project +# Copyright: Copyright © 2014 Michael Scott Asato Cuthbert # License: BSD, see license.txt # ----------------------------------------------------------------------------- ''' -These are the public methods for the MEI module by Christopher Antila +These are the public interfaces for the MEI module by Christopher Antila To convert a string with MEI markup into music21 objects, -use :meth:`MeiToM21Converter.convertFromString`. +use :meth:`~music21.mei.MeiToM21Converter.convertFromString`. In the future, most of the functions in this module should be moved to a separate, import-only module, so that functions for writing music21-to-MEI will fit nicely. @@ -54,7 +54,6 @@ ... ... ... """ ->>> from music21 import * >>> conv = mei.MeiToM21Converter(meiString) >>> result = conv.run() >>> result @@ -171,16 +170,16 @@ * : a page break * : a line break * : a system break - ''' -# pylint: disable=misplaced-comparison-constant -from typing import Optional, Union, List, Tuple -from xml.etree import ElementTree as ETree -from xml.etree.ElementTree import Element + +from __future__ import annotations from collections import defaultdict -from fractions import Fraction # for typing +from copy import deepcopy +import typing as t from uuid import uuid4 +from xml.etree.ElementTree import Element, ParseError, fromstring, ElementTree + # music21 from music21 import articulations @@ -202,8 +201,11 @@ from music21 import tie from music21 import harmony -_MOD = 'mei.base' -environLocal = environment.Environment(_MOD) + +if t.TYPE_CHECKING: + from fractions import Fraction + +environLocal = environment.Environment('mei.base') # Module-Level Constants @@ -226,22 +228,30 @@ # Exceptions # ----------------------------------------------------------------------------- class MeiValidityError(exceptions21.Music21Exception): - 'When there is an otherwise-unspecified validity error that prevents parsing.' + ''' + When there is an otherwise-unspecified validity error that prevents parsing. + ''' pass class MeiValueError(exceptions21.Music21Exception): - 'When an attribute has an invalid value.' + ''' + When an attribute has an invalid value. + ''' pass class MeiAttributeError(exceptions21.Music21Exception): - 'When an element has an invalid attribute.' + ''' + When an element has an invalid attribute. + ''' pass class MeiElementError(exceptions21.Music21Exception): - 'When an element itself is invalid.' + ''' + When an element itself is invalid. + ''' pass @@ -290,14 +300,14 @@ def __init__(self, theDocument=None): self.documentRoot = Element(f'{MEI_NS}mei') else: try: - self.documentRoot = ETree.fromstring(theDocument) - except ETree.ParseError as parseErr: + self.documentRoot = fromstring(theDocument) + except ParseError as parseErr: environLocal.printDebug( '\n\nERROR: Parsing the MEI document with ElementTree failed.') environLocal.printDebug(f'We got the following error:\n{parseErr}') raise MeiValidityError(_INVALID_XML_DOC) - if isinstance(self.documentRoot, ETree.ElementTree): + if isinstance(self.documentRoot, ElementTree): # pylint warns that :class:`Element` doesn't have a getroot() method, which is # true enough, but... self.documentRoot = self.documentRoot.getroot() # pylint: disable=maybe-no-member @@ -344,8 +354,8 @@ def run(self) -> stream.Stream: # ----------------------------------------------------------------------------- def safePitch( name: str, - accidental: Optional[str] = None, - octave: Union[str, int] = '' + accidental: str | None = None, + octave: str | int = '' ) -> pitch.Pitch: ''' Safely build a :class:`~music21.pitch.Pitch` from a string. @@ -355,7 +365,9 @@ def safePitch( function instead returns a default :class:`~music21.pitch.Pitch` instance. name: Desired name of the :class:`~music21.pitch.Pitch`. + accidental: (Optional) Symbol for the accidental. + octave: (Optional) Octave number. Returns A :class:`~music21.pitch.Pitch` with the appropriate properties. @@ -365,19 +377,25 @@ def safePitch( >>> safePitch('D', '#', '6') + >>> safePitch('D', '#') + ''' if not name: return pitch.Pitch() - elif accidental is None: - return pitch.Pitch(name + octave) + if octave and accidental is not None: + return pitch.Pitch(name, octave=int(octave), accidental=accidental) + if octave: + return pitch.Pitch(name, octave=int(octave)) + if accidental is not None: + return pitch.Pitch(name, accidental=accidental) else: - return pitch.Pitch(name, accidental=accidental, octave=int(octave)) + return pitch.Pitch(name) def makeDuration( - base: Union[float, int, Fraction] = 0.0, + base: float | int | Fraction = 0.0, dots: int = 0 -) -> 'music21.duration.Duration': +) -> duration.Duration: ''' Given a ``base`` duration and a number of ``dots``, create a :class:`~music21.duration.Duration` instance with the @@ -387,7 +405,6 @@ def makeDuration( **Examples** - >>> from music21 import * >>> from fractions import Fraction >>> mei.base.makeDuration(base=2.0, dots=0).quarterLength # half note, no dots 2.0 @@ -407,7 +424,7 @@ def makeDuration( return returnDuration -def allPartsPresent(scoreElem) -> Tuple[str, ...]: +def allPartsPresent(scoreElem) -> tuple[str, ...]: # noinspection PyShadowingNames ''' Find the @n values for all elements in a element. This assumes that every @@ -433,7 +450,6 @@ def allPartsPresent(scoreElem) -> Tuple[str, ...]: ...
...
""" >>> import xml.etree.ElementTree as ETree - >>> from music21 import * >>> meiDoc = ETree.fromstring(meiDoc) >>> mei.base.allPartsPresent(meiDoc) ('1', '2') @@ -444,7 +460,6 @@ def allPartsPresent(scoreElem) -> Tuple[str, ...]: ''' # xpathQuery = f'.//{MEI_NS}music//{MEI_NS}score//{MEI_NS}staffDef' xpathQuery = f'.//{MEI_NS}staffDef' - partNs = [] # hold the @n attribute for all the parts for staffDef in scoreElem.findall(xpathQuery): @@ -565,7 +580,6 @@ def _accidentalFromAttr(attr): ''' Use :func:`_attrTranslator` to convert the value of an "accid" attribute to its music21 string. - >>> from music21 import * >>> mei.base._accidentalFromAttr('s') '#' ''' @@ -577,7 +591,6 @@ def _accidGesFromAttr(attr): Use :func:`_attrTranslator` to convert the value of an @accid.ges attribute to its music21 string. - >>> from music21 import * >>> mei.base._accidGesFromAttr('s') '#' ''' @@ -588,7 +601,6 @@ def _qlDurationFromAttr(attr): ''' Use :func:`_attrTranslator` to convert an MEI "dur" attribute to a music21 quarterLength. - >>> from music21 import * >>> mei.base._qlDurationFromAttr('4') 1.0 @@ -627,7 +639,8 @@ def _makeArticList(attr): return articList -def _getOctaveShift(dis, disPlace): +def _getOctaveShift(dis: t.Literal['8', '15', '22'] | None, + disPlace: str) -> int: ''' Use :func:`_getOctaveShift` to calculate the :attr:`octaveShift` attribute for a :class:`~music21.clef.Clef` subclass. Any of the arguments may be ``None``. @@ -720,7 +733,6 @@ def _ppSlurs(theConverter): ...
... ... """ - >>> from music21 import * >>> theConverter = mei.base.MeiToM21Converter(meiDoc) >>> >>> mei.base._ppSlurs(theConverter) @@ -958,7 +970,7 @@ def _ppConclude(theConverter): # Helper Functions # ----------------------------------------------------------------------------- def _processEmbeddedElements( - elements: List[Element], + elements: list[Element], mapping, callerTag=None, slurBundle=None @@ -992,7 +1004,6 @@ def _processEmbeddedElements( Because there is no ``'rest'`` key in the ``mapping``, that :class:`Element` is ignored. >>> from xml.etree.ElementTree import Element - >>> from music21 import * >>> elements = [Element('note'), Element('rest'), Element('note')] >>> mapping = {'note': lambda x, y: note.Note('D2')} >>> mei.base._processEmbeddedElements(elements, mapping, 'doctest') @@ -1038,7 +1049,7 @@ def _timeSigFromAttrs(elem): return meter.TimeSignature(f"{elem.get('meter.count')!s}/{elem.get('meter.unit')!s}") -def _keySigFromAttrs(elem: Element) -> Union[key.Key, key.KeySignature]: +def _keySigFromAttrs(elem: Element) -> key.Key | key.KeySignature: ''' From any tag with (at minimum) either @key.pname or @key.sig attributes, make a :class:`KeySignature` or :class:`Key`, as possible. @@ -1052,6 +1063,8 @@ def _keySigFromAttrs(elem: Element) -> Union[key.Key, key.KeySignature]: # noinspection PyTypeChecker mode = elem.get('key.mode', '') step = elem.get('key.pname') + if step is None: # pragma: no cover + raise MeiValidityError('Key missing step') accidental = _accidentalFromAttr(elem.get('key.accid')) if accidental is None: tonic = step @@ -1295,17 +1308,17 @@ def metaSetTitle(work, meta): :return: The ``meta`` argument, having relevant metadata added. ''' # title, subtitle, and movement name + subtitle = None for title in work.findall(f'./{MEI_NS}titleStmt/{MEI_NS}title'): - if title.get('type', '') == 'subtitle': - meta.subtitle = title.text + if title.get('type', '') == 'subtitle': # or 'subordinate', right? + subtitle = title.text elif meta.title is None: meta.title = title.text - if hasattr(meta, 'subtitle'): + if subtitle: # Since m21.Metadata doesn't actually have a "subtitle" attribute, we'll put the subtitle # in the title - meta.title = f'{meta.title} ({meta.subtitle})' - del meta.subtitle + meta.title = f'{meta.title} ({subtitle})' tempo = work.find(f'./{MEI_NS}tempo') if tempo is not None: @@ -1336,7 +1349,7 @@ def metaSetComposer(work, meta): if len(composers) == 1: meta.composer = composers[0] elif len(composers) > 1: - meta.composer = composers + meta.composers = composers return meta @@ -1360,12 +1373,12 @@ def metaSetDate(work, meta): except ValueError: environLocal.warn(_MISSED_DATE.format(dateStr)) else: - meta.date = theDate + meta.dateCreated = theDate else: dateStart = date.get('notbefore') if date.get('notbefore') else date.get('startdate') dateEnd = date.get('notafter') if date.get('notafter') else date.get('enddate') if dateStart and dateEnd: - meta.date = metadata.DateBetween((dateStart, dateEnd)) + meta.dateCreated = metadata.DateBetween((dateStart, dateEnd)) return meta @@ -1718,7 +1731,6 @@ def staffDefFromElement(elem, slurBundle=None): # pylint: disable=unused-argume >>> meiDoc = """ ... ... """ - >>> from music21 import * >>> from xml.etree import ElementTree as ET >>> staffDef = ET.fromstring(meiDoc) >>> result = mei.base.staffDefFromElement(staffDef) @@ -1739,7 +1751,6 @@ def staffDefFromElement(elem, slurBundle=None): # pylint: disable=unused-argume ... ... ... """ - >>> from music21 import * >>> from xml.etree import ElementTree as ET >>> staffDef = ET.fromstring(meiDoc) >>> result = mei.base.staffDefFromElement(staffDef) @@ -1825,10 +1836,7 @@ def staffDefFromElement(elem, slurBundle=None): # pylint: disable=unused-argume # --> time signature if elem.get('meter.count') is not None: post['meter'] = _timeSigFromAttrs(elem) - # --> or - if elem.find(f'{MEI_NS}meterSig') is not None: - post['meter'] = meterSigFromElement(elem.find(f'{MEI_NS}meterSig')) - + # --> key signature if elem.get('key.pname') is not None or elem.get('key.sig') is not None: post['key'] = _keySigFromAttrs(elem) @@ -1910,15 +1918,14 @@ def articFromElement(elem, slurBundle=None): # pylint: disable=unused-argument :attr:`~music21.note.GeneralNote.articulations` attribute. >>> from xml.etree import ElementTree as ET - >>> from music21 import * - >>> meiSnippet = """""" + >>> meiSnippet = '' >>> meiSnippet = ET.fromstring(meiSnippet) >>> mei.base.articFromElement(meiSnippet) [] A single element may indicate many :class:`Articulation` objects. - >>> meiSnippet = """""" + >>> meiSnippet = '' >>> meiSnippet = ET.fromstring(meiSnippet) >>> mei.base.articFromElement(meiSnippet) [, ] @@ -1970,12 +1977,11 @@ def accidFromElement(elem, slurBundle=None): # pylint: disable=unused-argument a string. Accidentals up to triple-sharp and triple-flat are supported. >>> from xml.etree import ElementTree as ET - >>> from music21 import * - >>> meiSnippet = """""" + >>> meiSnippet = '' >>> meiSnippet = ET.fromstring(meiSnippet) >>> mei.base.accidFromElement(meiSnippet) '#' - >>> meiSnippet = """""" + >>> meiSnippet = '' >>> meiSnippet = ET.fromstring(meiSnippet) >>> mei.base.accidFromElement(meiSnippet) '---' @@ -2358,13 +2364,16 @@ def spaceFromElement(elem, slurBundle=None): # pylint: disable=unused-argument A placeholder used to fill an incomplete measure, layer, etc. most often so that the combined duration of the events equals the number of beats in the measure. + Returns a Rest element with hideObjectOnPrint = True + In MEI 2013: pg.440 (455 in PDF) (MEI.shared module) ''' # NOTE: keep this in sync with restFromElement() theDuration = _qlDurationFromAttr(elem.get('dur')) theDuration = makeDuration(theDuration, int(elem.get('dots', 0))) - theSpace = note.SpacerRest(duration=theDuration) + theSpace = note.Rest(duration=theDuration) + theSpace.style.hideObjectOnPrint = True if elem.get(_XMLID) is not None: theSpace.id = elem.get(_XMLID) @@ -2529,13 +2538,13 @@ def harmFromElement(elem, slurBundle=None): def figuredbassFromElement(elem, slurBundle=None): if elem.get(_XMLID): - id = elem.get(_XMLID) + fb_id = elem.get(_XMLID) fb_notation = '' dauer: float = 0 # loop through all child elements and collect tags for subElement in elem.findall('*'): if subElement.tag == f'{MEI_NS}f': - if subElement.text != None: + if subElement.text is not None: if fb_notation != '': fb_notation += f',{subElement.text}' else: @@ -2543,7 +2552,7 @@ def figuredbassFromElement(elem, slurBundle=None): else: if 'extender' in subElement.attrib.keys(): if fb_notation != '': - fb_notation += f',_' + fb_notation += ',_' else: fb_notation = '_' if 'dur.metrical' in subElement.attrib.keys(): @@ -2551,7 +2560,7 @@ def figuredbassFromElement(elem, slurBundle=None): # Generate a FiguredBassIndication object and set the collected information theFbNotation = harmony.FiguredBassIndication(fb_notation) - theFbNotation.id = id + theFbNotation.id = fb_id theFbNotation.duration = duration.Duration(quarterLength=dauer) return theFbNotation @@ -2666,7 +2675,6 @@ def beamFromElement(elem, slurBundle=None): a list of three objects, none of which is a :class:`Beam` or similar. >>> from xml.etree import ElementTree as ET - >>> from music21 import * >>> meiSnippet = """ ... ... @@ -3108,7 +3116,7 @@ def _makeBarlines(elem, staves): bars = bars[1] for eachMeasure in staves.values(): if isinstance(eachMeasure, stream.Measure): - eachMeasure.leftBarline = bars + eachMeasure.leftBarline = deepcopy(bars) if elem.get('right') is not None: bars = _barlineFromAttr(elem.get('right')) @@ -3118,7 +3126,7 @@ def _makeBarlines(elem, staves): bars = bars[0] for eachMeasure in staves.values(): if isinstance(eachMeasure, stream.Measure): - eachMeasure.rightBarline = bars + eachMeasure.rightBarline = deepcopy(bars) return staves @@ -3133,7 +3141,7 @@ def measureFromElement(elem, backupNum, expectedNs, slurBundle=None, activeMeter :param elem: The ```` element to process. :type elem: :class:`~xml.etree.ElementTree.Element` :param int backupNum: A fallback value for the resulting - :class:`~music21.measure.Measure` objects' number attribute. + :class:`~music21.stream.Measure` objects' number attribute. :param expectedNs: A list of the expected @n attributes for the tags in this . If an expected isn't in the , it will be created with a full-measure rest. :type expectedNs: iterable of str @@ -3352,7 +3360,6 @@ def sectionScoreCore(elem, allPartNs, slurBundle, *, # only process elements if this is a
if measureTag == eachElem.tag and sectionTag == elem.tag: backupMeasureNum += 1 - # process all the stuff in the measureResult = measureFromElement(eachElem, backupMeasureNum, allPartNs, slurBundle=slurBundle, @@ -3365,7 +3372,7 @@ def sectionScoreCore(elem, allPartNs, slurBundle, *, inNextThing[eachN] = [] # if we got a left-side barline from the previous measure, use it if nextMeasureLeft is not None: - measureResult[eachN].leftBarline = nextMeasureLeft + measureResult[eachN].leftBarline = deepcopy(nextMeasureLeft) # add this Measure to the Part parsed[eachN].append(measureResult[eachN]) # if we got a barline for the next @@ -3379,8 +3386,13 @@ def sectionScoreCore(elem, allPartNs, slurBundle, *, for allPartObject in localResult['all-part objects']: if isinstance(allPartObject, meter.TimeSignature): activeMeter = allPartObject - for eachN in allPartNs: - inNextThing[eachN].append(allPartObject) + for i, eachN in enumerate(allPartNs): + if i == 0: + to_insert = allPartObject + else: + # a single Music21Object should not exist in multiple parts + to_insert = deepcopy(allPartObject) + inNextThing[eachN].append(to_insert) for eachN in allPartNs: if eachN in localResult: for eachObj in localResult[eachN].values(): @@ -3414,7 +3426,6 @@ def sectionScoreCore(elem, allPartNs, slurBundle, *, # put those into the first Measure object we encounter in this Part # TODO: this is where the Instruments get added # TODO: I think "eachList" really means "each list that will become a Part" - if inNextThing[eachN]: # we have to put Instrument objects just before the Measure to which they apply theInstr = None @@ -3447,13 +3458,11 @@ def sectionScoreCore(elem, allPartNs, slurBundle, *, # must "flatten" everything so it doesn't cause a disaster when we try to make # a Part out of it. for eachObj in eachList: - if eachN in parsed.keys(): - parsed[eachN].append(eachObj) + parsed[eachN].append(eachObj) elif scoreTag == elem.tag: # If this is a , we can just append the result of each
to the # list that will become the Part. - if eachN in parsed.keys(): - parsed[eachN].append(eachList) + parsed[eachN].append(eachList) elif eachElem.tag not in _IGNORE_UNPROCESSED: environLocal.printDebug(_UNPROCESSED_SUBELEMENT.format(eachElem.tag, elem.tag)) @@ -3561,7 +3570,6 @@ def scoreFromElement(elem, slurBundle): # UPDATE: If tags are found, they will also be collected as a separate 'part' to process them later. allPartNs = allPartsPresent(elem) - # This is the actual processing. parsed = sectionScoreCore(elem, allPartNs, slurBundle=slurBundle)[0] @@ -3570,7 +3578,8 @@ def scoreFromElement(elem, slurBundle): # document. Iterating the keys in "parsed" would not preserve the order. environLocal.printDebug('*** making the Score') - # Extract collected information stored in the dict unter th 'fb' key + # Extract collected information stored in the dict unter the 'fb' key + harms: list[dict] | None = None if 'fb' in parsed.keys(): harms = parsed['fb'][0] del parsed['fb'] @@ -3583,14 +3592,14 @@ def scoreFromElement(elem, slurBundle): theScore[i].atSoundingPitch = False for eachObj in parsed[eachN]: theScore[i].append(eachObj) - theScore = stream.Score(theScore) # loop through measures to insert harm elements from harms list at the right offsets - for index, measureOffset in enumerate(theScore.measureOffsetMap().keys()): - hms = harms[index]['fb'] - for h in hms: - theScore.insert(measureOffset + h[0], h[1]) + if harms: + for index, measureOffset in enumerate(theScore.measureOffsetMap().keys()): + hms = harms[index]['fb'] + for h in hms: + theScore.insert(measureOffset + h[0], h[1]) # put slurs in the Score theScore.append(list(slurBundle)) diff --git a/music21/mei/test_base.py b/music21/mei/test_base.py index 012b439b17..b61065c96e 100644 --- a/music21/mei/test_base.py +++ b/music21/mei/test_base.py @@ -199,7 +199,7 @@ def testAllPartsPresent1(self): staffDefs[0].get = mock.MagicMock(return_value='1') elem = mock.MagicMock(spec_set=ETree.Element) elem.findall = mock.MagicMock(return_value=staffDefs) - expected = ['1'] + expected = ['1', 'fb'] actual = base.allPartsPresent(elem) self.assertSequenceEqual(expected, actual) @@ -212,7 +212,7 @@ def testAllPartsPresent2(self): staffDefs[i].get = mock.MagicMock(return_value=str(i + 1)) elem = mock.MagicMock(spec_set=ETree.Element) elem.findall = mock.MagicMock(return_value=staffDefs) - expected = list('1234') + expected = list('1234fb') actual = base.allPartsPresent(elem) self.assertSequenceEqual(expected, actual) @@ -225,7 +225,7 @@ def testAllPartsPresent3(self): staffDefs[i].get = mock.MagicMock(return_value=str((i % 4) + 1)) elem = mock.MagicMock(spec_set=ETree.Element) elem.findall = mock.MagicMock(return_value=staffDefs) - expected = list('1234') + expected = list('1234fb') actual = base.allPartsPresent(elem) self.assertSequenceEqual(expected, actual) diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index 8f04910bb2..4873740143 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -3567,8 +3567,9 @@ def insertFiguredBassIndications(self) -> None: Adds relevant figured bass elements collected from the stream.Score to the current stream.Measure object parsed afterwards. - In a MusicXML file tags usually stand before the corresponding note object. - This order will be observed by parseFlatElements() function. Same for multiple figures at one note. + In a MusicXML file tags usually stand before the corresponding + note object. This order will be observed by parseFlatElements() function. + Same for multiple figures at one note. ''' # get the measure range to map the corresponding figuredBass Items measureRange = (self.stream.offset, self.stream.offset + self.stream.highestTime) @@ -4654,7 +4655,7 @@ def figuredBassToXml(self, f: harmony.FiguredBassIndication): #_synchronizeIds(mxFB, f) return mxFB - def _figuresToXml(self, f: harmony.FiguredBassIndication, multipleFigures=False, figureCnt=1, noteIndexInChord=0, chordParent=None): + def _figuresToXml(self, f: harmony.FiguredBassIndication, multipleFigures=False): #do Figure elements #self.addDividerComment('BEGIN: figured-bass') @@ -4694,7 +4695,7 @@ def _figuresToXml(self, f: harmony.FiguredBassIndication, multipleFigures=False, # Check whether the figured-bass tag before already has a tag. # If not create one and set its value. Otherwise update the value - if fbDuration_before.find('duration') == None: + if fbDuration_before.find('duration') is None: newDura = SubElement(fbDuration_before, 'duration') newDura.text = str(dura) else: From 9717173ec5366bda7de5f9728e04d92aec349fca Mon Sep 17 00:00:00 2001 From: Moritz Heffter Date: Fri, 24 Mar 2023 00:19:00 +0100 Subject: [PATCH 38/58] mei/base.py recent update to upstream --- music21/mei/base.py | 24 +++++------------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/music21/mei/base.py b/music21/mei/base.py index 7964ad8a8e..2d1d6d4eee 100644 --- a/music21/mei/base.py +++ b/music21/mei/base.py @@ -1197,7 +1197,9 @@ def addSlurs(elem, obj, slurBundle): addedSlur = False def wrapGetByIdLocal(theId): - "Avoid crashing when getByIdLocl() doesn't find the slur" + ''' + Avoid crashing when getByIdLocl() doesn't find the slur + ''' try: slurBundle.getByIdLocal(theId)[0].addSpannedElements(obj) return True @@ -1564,7 +1566,6 @@ def scoreDefFromElement(elem, slurBundle=None): # pylint: disable=unused-argume ... ... ... """ - >>> from music21 import * >>> from xml.etree import ElementTree as ET >>> scoreDef = ET.fromstring(meiDoc) >>> result = mei.base.scoreDefFromElement(scoreDef) @@ -1808,8 +1809,7 @@ def staffDefFromElement(elem, slurBundle=None): # pylint: disable=unused-argume - MEI.shared: clefGrp keySig label layerDef ''' # mapping from tag name to our converter function - tagToFunction = {f'{MEI_NS}clef': clefFromElement, - f'{MEI_NS}meterSig': meterSigFromElement} + tagToFunction = {f'{MEI_NS}clef': clefFromElement} # first make the Instrument post = elem.find(f'{MEI_NS}instrDef') @@ -1860,20 +1860,6 @@ def staffDefFromElement(elem, slurBundle=None): # pylint: disable=unused-argume return post -def meterSigFromElement(elem, slurBundle=None) -> meter.TimeSignature: - ''' Container for information on meter and TimeSignature. - - In MEI 4: (MEI.cmn module) - - :returns: A meter.TimeSignature that is created from the @count und @unit attributes. - - If a xml:id is set it is provided. - ''' - - ts = meter.TimeSignature(f"{elem.get('count')!s}/{elem.get('unit')!s}") - if elem.get('xml:id') is not None: - ts.id = elem.get('xml:id') - return ts def dotFromElement(elem, slurBundle=None): # pylint: disable=unused-argument ''' @@ -2210,7 +2196,7 @@ def noteFromElement(elem, slurBundle=None): f'{MEI_NS}artic': articFromElement, f'{MEI_NS}accid': accidFromElement, f'{MEI_NS}syl': sylFromElement} - + # start with a Note with Pitch theNote = _accidentalFromAttr(elem.get('accid')) theNote = safePitch(elem.get('pname', ''), theNote, elem.get('oct', '')) From 0f4aba51d5aea0d0d130de17de3f4ea2dc612346 Mon Sep 17 00:00:00 2001 From: Moritz Heffter Date: Fri, 24 Mar 2023 00:21:17 +0100 Subject: [PATCH 39/58] full merge of mei/base.py --- music21/mei/base.py | 1 - 1 file changed, 1 deletion(-) diff --git a/music21/mei/base.py b/music21/mei/base.py index 2d1d6d4eee..ff447059e7 100644 --- a/music21/mei/base.py +++ b/music21/mei/base.py @@ -3572,7 +3572,6 @@ def scoreFromElement(elem, slurBundle): allPartNs = allPartNs[0:-1] theScore = [stream.Part() for _ in range(len(allPartNs))] - for i, eachN in enumerate(allPartNs): # set "atSoundingPitch" so transposition works theScore[i].atSoundingPitch = False From c8318c5edebffa0d11b88b4024eaf46c940d900d Mon Sep 17 00:00:00 2001 From: Moritz Heffter Date: Fri, 24 Mar 2023 00:41:44 +0100 Subject: [PATCH 40/58] edit in mei/test_base.py to pass new fb element in part list --- music21/mei/test_base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/music21/mei/test_base.py b/music21/mei/test_base.py index b61065c96e..1a23c82980 100644 --- a/music21/mei/test_base.py +++ b/music21/mei/test_base.py @@ -212,7 +212,7 @@ def testAllPartsPresent2(self): staffDefs[i].get = mock.MagicMock(return_value=str(i + 1)) elem = mock.MagicMock(spec_set=ETree.Element) elem.findall = mock.MagicMock(return_value=staffDefs) - expected = list('1234fb') + expected = ['1', '2', '3', '4', 'fb'] actual = base.allPartsPresent(elem) self.assertSequenceEqual(expected, actual) @@ -225,7 +225,7 @@ def testAllPartsPresent3(self): staffDefs[i].get = mock.MagicMock(return_value=str((i % 4) + 1)) elem = mock.MagicMock(spec_set=ETree.Element) elem.findall = mock.MagicMock(return_value=staffDefs) - expected = list('1234fb') + expected = ['1', '2', '3', '4', 'fb'] actual = base.allPartsPresent(elem) self.assertSequenceEqual(expected, actual) From 7076597abf4e44f981c1d99a66e307023ffa4347 Mon Sep 17 00:00:00 2001 From: Moritz Heffter Date: Mon, 27 Mar 2023 22:31:08 +0200 Subject: [PATCH 41/58] suggestions from review added, smaller formatting improvements --- music21/figuredBass/notation.py | 11 ++++------- music21/harmony.py | 10 +++------- music21/mei/base.py | 12 +++++++----- music21/mei/test_base.py | 2 +- music21/musicxml/m21ToXml.py | 23 ++++++++++++----------- music21/musicxml/xmlToM21.py | 32 +++++++++++++++++++------------- 6 files changed, 46 insertions(+), 44 deletions(-) diff --git a/music21/figuredBass/notation.py b/music21/figuredBass/notation.py index 3bb5806606..db797962d1 100644 --- a/music21/figuredBass/notation.py +++ b/music21/figuredBass/notation.py @@ -404,8 +404,7 @@ def _getFigures(self): figuresFromNotaCol = [] - for i in range(len(self.origNumbers)): - number = self.origNumbers[i] + for i, number in enumerate(self.origNumbers): modifierString = self.origModStrings[i] figure = Figure(number, modifierString) figuresFromNotaCol.append(figure) @@ -452,14 +451,12 @@ class Figure(prebase.ProtoM21Object): ''', } - def __init__(self, number=1, modifierString=None, isExtender=None): + def __init__(self, number=1, modifierString=None): self.number = number self.modifierString = modifierString self.modifier = Modifier(modifierString) - if self.number == '_': - self.isExtender = True - else: - self.isExtender = False + # look for extenders underscore + self.isExtender: bool = (self.number == '_') def _reprInternal(self): mod = repr(self.modifier).replace('music21.figuredBass.notation.', '') diff --git a/music21/harmony.py b/music21/harmony.py index 27722663f7..e2419fe991 100644 --- a/music21/harmony.py +++ b/music21/harmony.py @@ -2503,15 +2503,11 @@ def transpose(self: NCT, _value, *, inPlace=False) -> NCT | None: class FiguredBassIndication(Harmony): isFigure = True tstamp = 1 - def __init__(self, figs=None, **keywords): + def __init__(self, figs: list | None=None, **keywords): super().__init__(**keywords) if figs: if isinstance(figs, list): - fig_string = str(figs[0]) - for sf in figs: - fig_string += f',{sf}' - figs = fig_string - #pass + figs = ','.join(figs) else: figs = '' self._fig_notation = notation.Notation(figs) @@ -2519,7 +2515,7 @@ def __init__(self, figs=None, **keywords): @property def fig_notation(self) -> notation.Notation: return self._fig_notation - + @fig_notation.setter def fig_notation(self, figs): self._fig_notation = notation.Notation(figs) diff --git a/music21/mei/base.py b/music21/mei/base.py index ff447059e7..797bad676e 100644 --- a/music21/mei/base.py +++ b/music21/mei/base.py @@ -469,7 +469,8 @@ def allPartsPresent(scoreElem) -> tuple[str, ...]: raise MeiValidityError(_SEEMINGLY_NO_PARTS) # Get information of possible tags in the score. If there are tags prepare a list to - # store them and process them later. TODO: Maybe to be put in a separate function e.g. like allPartsPresent + # store them and process them later. + # TODO: Maybe to be put in a separate function e.g. like allPartsPresent figuredBassQuery = f'.//{MEI_NS}fb' if scoreElem.findall(figuredBassQuery): environLocal.printDebug('harm tag found!') @@ -2504,7 +2505,7 @@ def chordFromElement(elem, slurBundle=None): return theChord -def harmFromElement(elem, slurBundle=None): +def harmFromElement(elem, slurBundle=None) -> tuple: # other tags than to be added… tagToFunction = {f'{MEI_NS}fb': figuredbassFromElement} @@ -2522,10 +2523,10 @@ def harmFromElement(elem, slurBundle=None): return fb_harmony_tag -def figuredbassFromElement(elem, slurBundle=None): +def figuredbassFromElement(elem, slurBundle=None) -> harmony.FiguredBassIndication: if elem.get(_XMLID): fb_id = elem.get(_XMLID) - fb_notation = '' + fb_notation: str = '' dauer: float = 0 # loop through all child elements and collect tags for subElement in elem.findall('*'): @@ -3553,7 +3554,8 @@ def scoreFromElement(elem, slurBundle): # Get a tuple of all the @n attributes for the tags in this score. Each tag # corresponds to what will be a music21 Part. - # UPDATE: If tags are found, they will also be collected as a separate 'part' to process them later. + # UPDATE: If tags are found, they will also be collected as a separate 'part' + # to process them later. allPartNs = allPartsPresent(elem) # This is the actual processing. diff --git a/music21/mei/test_base.py b/music21/mei/test_base.py index 1a23c82980..c13981341d 100644 --- a/music21/mei/test_base.py +++ b/music21/mei/test_base.py @@ -3929,7 +3929,7 @@ def testMeasureUnit1(self, mockVoice, mockMeasure, elem.append(eachStaff) # @n="4" is in "expectedNs" but we're leaving it out as part of the test backupNum = 900 # should be ignored by measureFromElement() - expectedNs = ['1', '2', '3', '4'] + expectedNs = ['1', '2', '3', '4', 'fb'] slurBundle = mock.MagicMock(name='slurBundle') activeMeter = mock.MagicMock(name='activeMeter') activeMeter.barDuration = duration.Duration(4.0) diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index 4873740143..3dded78f08 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -1493,7 +1493,7 @@ def __init__(self, score: stream.Score | None = None, makeNotation: bool = True) self.textBoxes: list[text.TextBox] = [] self.highestTime = 0.0 self.fb_part = -1 - self.fbis_dict = {} + self.fbis_dict: dict = {} self.currentDivisions = defaults.divisionsPerQuarter self.refStreamOrTimeRange = [0.0, self.highestTime] @@ -3232,6 +3232,7 @@ def __init__(self, self.spannerBundle = parent.spannerBundle self.objectSpannerBundle = self.spannerBundle # will change for each element. + self._fbiBefore: tuple[float, Element] = () def parse(self): ''' @@ -3390,7 +3391,7 @@ def parseFlatElements( # Prepare the iteration by offsets. If there are FiguredBassIndication objects # first group them in one list together with their note, instead of handling them # seperately. O - groupedObjList = [] + groupedObjList: list = [] for els in objIterator: #print(els) if len(groupedObjList) > 0: @@ -3566,7 +3567,7 @@ def insertFiguredBassIndications(self) -> None: ''' Adds relevant figured bass elements collected from the stream.Score to the current stream.Measure object parsed afterwards. - + In a MusicXML file tags usually stand before the corresponding note object. This order will be observed by parseFlatElements() function. Same for multiple figures at one note. @@ -4656,15 +4657,15 @@ def figuredBassToXml(self, f: harmony.FiguredBassIndication): return mxFB def _figuresToXml(self, f: harmony.FiguredBassIndication, multipleFigures=False): - #do Figure elements - #self.addDividerComment('BEGIN: figured-bass') + # do Figure elements + # self.addDividerComment('BEGIN: figured-bass') mxFB = Element('figured-bass') dura = 0 for fig in f.fig_notation.figuresFromNotationColumn: mxFigure = SubElement(mxFB, 'figure') - #get only the fbnumber without prefixes or suffixes + # get only the fbnumber without prefixes or suffixes mxFNumber = SubElement(mxFigure, 'figure-number') if fig.number: if fig.number == '_': @@ -4675,8 +4676,8 @@ def _figuresToXml(self, f: harmony.FiguredBassIndication, multipleFigures=False) mxFNumber.text = str(fig.number) else: mxFNumber.text = '' - - #modifiers are either handled as prefixes or suffixes here + + # modifiers are either handled as prefixes or suffixes here fbModifier = fig.modifierString if fbModifier: mxModifier = SubElement(mxFigure, 'prefix') @@ -4687,7 +4688,8 @@ def _figuresToXml(self, f: harmony.FiguredBassIndication, multipleFigures=False) # If we have multiple figures we have to set a tag # and update the tag one before. if multipleFigures: - dura = round((f.offset - self.offsetInMeasure - self.offsetFiguresInMeasure) * self.currentDivisions) + dura = round((f.offset - self.offsetInMeasure - self.offsetFiguresInMeasure) + * self.currentDivisions) self.offsetFiguresInMeasure = f.offset - self.offsetInMeasure # Update figures-bass tag before @@ -4710,13 +4712,12 @@ def _figuresToXml(self, f: harmony.FiguredBassIndication, multipleFigures=False) self.offsetFiguresInMeasure = 0.0 if self.tempFigureDuration > 0: - #print(f.quarterLength * self.currentDivisions) + # print(f.quarterLength * self.currentDivisions) mxFbDuration = SubElement(mxFB, 'duration') mxFbDuration.text = str(round(self.tempFigureDuration)) self.tempFigureDuration = 0.0 return mxFB - #self.addDividerComment('END: figured-bass') def durationXml(self, dur: duration.Duration): # noinspection PyShadowingNames diff --git a/music21/musicxml/xmlToM21.py b/music21/musicxml/xmlToM21.py index 04389f208f..cf00ecd91a 100644 --- a/music21/musicxml/xmlToM21.py +++ b/music21/musicxml/xmlToM21.py @@ -926,7 +926,7 @@ def xmlRootToScore(self, mxScore, inputM21=None): if part is not None: # for instance, in partStreams s.coreInsert(0.0, part) self.m21PartObjectsById[partId] = part - + if self.fbis: for fbi in self.fbis: s.insert(fbi[0], fbi[1]) @@ -5279,28 +5279,28 @@ def xmlToFiguredBass(self, mxFiguredBass): for figure in mxFiguredBass.findall('*'): # TODO: suffixes are ignored at the moment for el in figure.findall('*'): - #print(' ', el) fb_number: str = '' fb_prefix: str = '' if el.tag == 'figure-number': if el.text: fb_number = el.text - if figure.findall('prefix'): - for prefix in figure.findall('prefix'): - - if prefix.text: - fb_prefix = modifiersDictXmlToM21[prefix.text] - # put prefix and number together - if fb_prefix + fb_number != '': - fb_strings.append(fb_prefix + fb_number) + + # Get prefix and/or suffix. + # The function returns an empty string if nothing is found. + fb_prefix = self._getFigurePrefixOrSuffix(figure, 'prefix') + fb_suffix = self._getFigurePrefixOrSuffix(figure, 'suffix') + + # put prefix/suffix and number together + if fb_prefix + fb_number + fb_suffix != '': + fb_strings.append(fb_prefix + fb_number + fb_suffix) if el.tag == 'extend': if 'type' in el.attrib.keys(): if el.attrib['type'] == 'continue': fb_strings.append('_') - # If a is given, this usually means that there are multiple figures for a single note. - # We have to look for offsets here. + # If a is given, this usually means that there are multiple figures + # for a single note. We have to look for offsets here. if figure.tag == 'duration': d = self.xmlToDuration(mxFiguredBass) if self.lastFigureDuration > 0: @@ -5310,7 +5310,6 @@ def xmlToFiguredBass(self, mxFiguredBass): offsetFbi = self.offsetMeasureNote self.lastFigureDuration = d.quarterLength - #print('ü', fb_strings) fb_string = sep.join(fb_strings) fbi = harmony.FiguredBassIndication(fb_string) # If a duration is provided, set length of the FigureBassIndication @@ -5320,6 +5319,13 @@ def xmlToFiguredBass(self, mxFiguredBass): # self.parent.appendFbis(fbi, offsetFbi) + def _getFigurePrefixOrSuffix(self, figure, presuf: str='prefix') -> str: + if figure.findall(presuf): + for fix in figure.findall(presuf): + if fix.text: + return modifiersDictXmlToM21[fix.text] + return '' + def xmlDirection(self, mxDirection): ''' convert a tag to one or more expressions, metronome marks, etc. From d6c49db7a1f82f6862718b19cd94cea33f176731 Mon Sep 17 00:00:00 2001 From: Moritz Heffter Date: Tue, 28 Mar 2023 00:16:59 +0200 Subject: [PATCH 42/58] adapted tests in mei/test_base-py and doctest for figuredBass/notation.py --- music21/figuredBass/notation.py | 20 ++++++------- music21/mei/test_base.py | 51 +++++++++++++++++++-------------- 2 files changed, 39 insertions(+), 32 deletions(-) diff --git a/music21/figuredBass/notation.py b/music21/figuredBass/notation.py index db797962d1..7967a958df 100644 --- a/music21/figuredBass/notation.py +++ b/music21/figuredBass/notation.py @@ -129,11 +129,11 @@ class Notation(prebase.ProtoM21Object): , ) >>> n1.figures[0] - > + hasExt: False> >>> n1.figures[1] - > + hasExt: False> >>> n1.figures[2] - > + hasExt: False> Here, a stand-alone '#' is being passed to Notation. @@ -146,9 +146,9 @@ class Notation(prebase.ProtoM21Object): (, ) >>> n2.figures[0] - > + hasExt: False> >>> n2.figures[1] - > + hasExt: False> Now, a stand-alone b is being passed to Notation as part of a larger notationColumn. @@ -161,9 +161,9 @@ class Notation(prebase.ProtoM21Object): (, ) >>> n3.figures[0] - > + hasExt: False> >>> n3.figures[1] - > + hasExt: False> ''' _DOC_ORDER = ['notationColumn', 'figureStrings', 'numbers', 'modifiers', 'figures', 'origNumbers', 'origModStrings', 'modifierStrings'] @@ -388,9 +388,9 @@ def _getFigures(self): >>> from music21.figuredBass import notation as n >>> notation2 = n.Notation('-6,-') #__init__ method calls _getFigures() >>> notation2.figures[0] - > + hasExt: False> >>> notation2.figures[1] - > + hasExt: False> ''' figures = [] @@ -426,7 +426,7 @@ class Figure(prebase.ProtoM21Object): >>> from music21.figuredBass import notation >>> f1 = notation.Figure(4, '+') >>> f1 - > + hasExt: False> >>> f1.number 4 diff --git a/music21/mei/test_base.py b/music21/mei/test_base.py index c13981341d..a8904b62d0 100644 --- a/music21/mei/test_base.py +++ b/music21/mei/test_base.py @@ -3936,10 +3936,12 @@ def testMeasureUnit1(self, mockVoice, mockMeasure, # this must match Measure.duration.quarterLength # prepare the mock Measure objects returned by mockMeasure mockMeasRets = [mock.MagicMock(name=f'Measure {i + 1}') for i in range(4)] - expected = mockMeasRets # finish preparing "expected" below... for meas in mockMeasRets: meas.duration = mock.MagicMock(spec_set=duration.Duration) meas.duration.quarterLength = 4.0 # must match activeMeter.barDuration.quarterLength + # append figured bass stuff + mockMeasRets.append({'fb': []}) + expected = mockMeasRets # finish preparing "expected" below... mockMeasure.side_effect = lambda *x, **y: mockMeasRets.pop(0) # prepare mock of _makeBarlines() which returns "staves" mockMakeBarlines.side_effect = lambda elem, staves: staves @@ -3961,7 +3963,7 @@ def testMeasureUnit1(self, mockVoice, mockMeasure, for eachStaff in innerStaffs: mockStaffFE.assert_any_call(eachStaff, slurBundle=slurBundle) # ensure Measure.__init__() was called properly - self.assertEqual(len(expected), mockMeasure.call_count) + self.assertEqual(len(expected) -1, mockMeasure.call_count) for i in range(len(innerStaffs)): mockMeasure.assert_any_call(i, number=int(elem.get('n'))) mockMeasure.assert_any_call([mockVoice.return_value], number=int(elem.get('n'))) @@ -3997,14 +3999,14 @@ def testMeasureIntegration1(self): elem.append(eachStaff) # @n="4" is in "expectedNs" but we're leaving it out as part of the test backupNum = 900 # should be ignored by measureFromElement() - expectedNs = ['1', '2', '3', '4'] + expectedNs = ['1', '2', '3', '4', 'fb'] slurBundle = spanner.SpannerBundle() activeMeter = meter.TimeSignature('8/8') # bet you thought this would be 4/4, eh? actual = base.measureFromElement(elem, backupNum, expectedNs, slurBundle, activeMeter) # ensure the right number and @n of parts - self.assertEqual(4, len(actual.keys())) + self.assertEqual(5, len(actual.keys())) for eachN in expectedNs: self.assertTrue(eachN in actual) # ensure the measure number is set properly, @@ -4053,7 +4055,7 @@ def testMeasureUnit2(self, mockVoice, mockMeasure, elem.append(eachStaff) # @n="4" is in "expectedNs" but we're leaving it out as part of the test backupNum = 900 # should be used by measureFromElement() - expectedNs = ['1', '2', '3', '4'] + expectedNs = ['1', '2', '3', '4', 'fb'] slurBundle = mock.MagicMock(name='slurBundle') activeMeter = mock.MagicMock(name='activeMeter') # this must be longer than Measure.duration.quarterLength @@ -4065,6 +4067,7 @@ def testMeasureUnit2(self, mockVoice, mockMeasure, for meas in mockMeasRets: meas.duration = mock.MagicMock(spec_set=duration.Duration) meas.duration.quarterLength = base._DUR_ATTR_DICT[None] # must be _DUR_ATTR_DICT[None] + mockMeasRets.append({'fb': []}) mockMeasure.side_effect = lambda *x, **y: mockMeasRets.pop(0) # prepare mock of _makeBarlines() which returns "staves" mockMakeBarlines.side_effect = lambda elem, staves: staves @@ -4086,7 +4089,7 @@ def testMeasureUnit2(self, mockVoice, mockMeasure, for eachStaff in innerStaffs: mockStaffFE.assert_any_call(eachStaff, slurBundle=slurBundle) # ensure Measure.__init__() was called properly - self.assertEqual(len(expected), mockMeasure.call_count) + self.assertEqual(len(expected) - 1, mockMeasure.call_count) for i in range(len(innerStaffs)): mockMeasure.assert_any_call(i, number=backupNum) mockMeasure.assert_any_call([mockVoice.return_value], number=backupNum) @@ -4120,14 +4123,14 @@ def testMeasureIntegration2(self): elem.append(eachStaff) # @n="4" is in "expectedNs" but we're leaving it out as part of the test backupNum = 900 # should be used by measureFromElement() - expectedNs = ['1', '2', '3', '4'] + expectedNs = ['1', '2', '3', '4', 'fb'] slurBundle = spanner.SpannerBundle() activeMeter = meter.TimeSignature('8/8') # bet you thought this would be 4/4, eh? actual = base.measureFromElement(elem, backupNum, expectedNs, slurBundle, activeMeter) # ensure the right number and @n of parts (we expect one additional key, for the "rptboth") - self.assertEqual(5, len(actual.keys())) + self.assertEqual(6, len(actual.keys())) for eachN in expectedNs: self.assertTrue(eachN in actual) self.assertTrue('next @left' in actual) @@ -4137,15 +4140,18 @@ def testMeasureIntegration2(self): # (Note we can test all four parts together this time--- # the fourth should be indistinguishable) for eachN in expectedNs: - self.assertEqual(backupNum, actual[eachN].number) - self.assertEqual(2, len(actual[eachN])) # first the Note, then the Barline - self.assertIsInstance(actual[eachN][0], stream.Voice) - self.assertEqual(1, len(actual[eachN][0])) - self.assertIsInstance(actual[eachN][0][0], note.Rest) - self.assertEqual(activeMeter.barDuration.quarterLength, + if isinstance(actual[eachN], stream.Measure): + self.assertEqual(backupNum, actual[eachN].number) + self.assertEqual(2, len(actual[eachN])) # first the Note, then the Barline + self.assertIsInstance(actual[eachN][0], stream.Voice) + self.assertEqual(1, len(actual[eachN][0])) + self.assertIsInstance(actual[eachN][0][0], note.Rest) + self.assertEqual(activeMeter.barDuration.quarterLength, actual['4'][0][0].duration.quarterLength) - self.assertIsInstance(actual[eachN].rightBarline, bar.Repeat) - self.assertEqual('final', actual[eachN].rightBarline.type) + self.assertIsInstance(actual[eachN].rightBarline, bar.Repeat) + self.assertEqual('final', actual[eachN].rightBarline.type) + else: + self.assertEqual([], actual[eachN]['fb']) @mock.patch('music21.mei.base.staffFromElement') @mock.patch('music21.mei.base._correctMRestDurs') @@ -4170,7 +4176,7 @@ def testMeasureUnit3a(self, mockStaffDefFE, mockMeasure, staffElem = ETree.Element(staffTag, attrib={'n': '1'}) elem.append(staffElem) backupNum = 900 # should be ignored by measureFromElement() - expectedNs = ['1'] + expectedNs = ['1', 'fb'] slurBundle = mock.MagicMock(name='slurBundle') activeMeter = mock.MagicMock(name='activeMeter') # this must match Measure.duration.quarterLength @@ -4186,10 +4192,10 @@ def testMeasureUnit3a(self, mockStaffDefFE, mockMeasure, # prepare mock of staffDefFromElement() mockStaffDefFE.return_value = {'clef': mock.MagicMock(name='SomeClef')} # prepare the expected return value - expected = {'1': mockMeasure.return_value} + expected = {'1': mockMeasure.return_value, 'fb': {'fb': []}} actual = base.measureFromElement(elem, backupNum, expectedNs, slurBundle, activeMeter) - + actual['fb'] = {'fb': []} self.assertDictEqual(expected, actual) # ensure staffFromElement() was called properly mockStaffFE.assert_called_once_with(staffElem, slurBundle=slurBundle) @@ -4224,7 +4230,7 @@ def testMeasureUnit3b(self, mockEnviron, mockMeasure, staffElem = ETree.Element(staffTag, attrib={'n': '1'}) elem.append(staffElem) backupNum = 900 # should be ignored by measureFromElement() - expectedNs = ['1'] + expectedNs = ['1', 'fb'] slurBundle = mock.MagicMock(name='slurBundle') activeMeter = mock.MagicMock(name='activeMeter') # this must match Measure.duration.quarterLength @@ -4238,9 +4244,10 @@ def testMeasureUnit3b(self, mockEnviron, mockMeasure, # prepare mock of staffFromElement(), which just needs to return several unique things mockStaffFE.return_value = 'staffFromElement() return value' # prepare the expected return value - expected = {'1': mockMeasure.return_value} + expected = {'1': mockMeasure.return_value, 'fb': {'fb': []}} actual = base.measureFromElement(elem, backupNum, expectedNs, slurBundle, activeMeter) + actual['fb'] = {'fb': []} self.assertDictEqual(expected, actual) # ensure staffFromElement() was called properly @@ -4283,7 +4290,7 @@ def testMeasureIntegration3(self): actual = base.measureFromElement(elem, backupNum, expectedNs, slurBundle, activeMeter) # ensure the right number and @n of parts - self.assertEqual(['1'], list(actual.keys())) + self.assertEqual(['1', 'fb'], list(actual.keys())) # ensure the Measure has its expected Voice, BassClef, and Instrument self.assertEqual(backupNum, actual['1'].number) self.assertEqual(2, len(actual['1'])) From 9688055529c151a9f6a9d3b9f9f9e318a2bc159f Mon Sep 17 00:00:00 2001 From: Moritz Heffter Date: Tue, 28 Mar 2023 00:37:38 +0200 Subject: [PATCH 43/58] simplification of a if else statement from review --- music21/mei/base.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/music21/mei/base.py b/music21/mei/base.py index 797bad676e..d0e009edc7 100644 --- a/music21/mei/base.py +++ b/music21/mei/base.py @@ -2527,25 +2527,21 @@ def figuredbassFromElement(elem, slurBundle=None) -> harmony.FiguredBassIndicati if elem.get(_XMLID): fb_id = elem.get(_XMLID) fb_notation: str = '' + fb_notation_list: list[str] = [] dauer: float = 0 # loop through all child elements and collect tags for subElement in elem.findall('*'): if subElement.tag == f'{MEI_NS}f': if subElement.text is not None: - if fb_notation != '': - fb_notation += f',{subElement.text}' - else: - fb_notation = subElement.text + fb_notation_list.append(subElement.text) else: if 'extender' in subElement.attrib.keys(): - if fb_notation != '': - fb_notation += ',_' - else: - fb_notation = '_' + fb_notation_list.append('_') if 'dur.metrical' in subElement.attrib.keys(): dauer = float(subElement.attrib['dur.metrical']) # Generate a FiguredBassIndication object and set the collected information + fb_notation = ",".join(fb_notation_list) theFbNotation = harmony.FiguredBassIndication(fb_notation) theFbNotation.id = fb_id theFbNotation.duration = duration.Duration(quarterLength=dauer) From 97882bc3cd5e610495264f7c7fc0c2476b3526e4 Mon Sep 17 00:00:00 2001 From: Moritz Heffter Date: Tue, 28 Mar 2023 20:15:52 +0200 Subject: [PATCH 44/58] finally cleaned up for tests and flake8 and mypy --- music21/figuredBass/notation.py | 67 ++++++++++++++++----------------- music21/harmony.py | 13 +++++-- music21/mei/base.py | 18 ++++----- music21/mei/test_base.py | 2 +- music21/musicxml/m21ToXml.py | 47 ++++++++++++----------- music21/musicxml/xmlToM21.py | 20 +++++----- 6 files changed, 85 insertions(+), 82 deletions(-) diff --git a/music21/figuredBass/notation.py b/music21/figuredBass/notation.py index 7967a958df..c260c8efa5 100644 --- a/music21/figuredBass/notation.py +++ b/music21/figuredBass/notation.py @@ -30,30 +30,27 @@ (2,): (6, 4, 2), } -prefixes = ["+", "#", "++", "##"] -suffixes = ["\\"] - -modifiersDictXmlToM21 = { - "sharp": "#", - "flat": "b", - "natural": "\u266e", - "double-sharp": "##", - "flat-flat": "bb", - "backslash": "\\" - } - -modifiersDictM21ToXml = { - "#": "sharp", - "b": "flat", - "##": "double-sharp", - "bb": "flat-flat", - "\\": "backslash", - "+": "sharp", - '\u266f': 'sharp', - '\u266e': 'natural', - '\u266d': 'flat', - '\u20e5': 'sharp' -} +prefixes = ['+', '#', '++', '##'] +suffixes = ['\\'] + +modifiersDictXmlToM21 = {'sharp': '#', + 'flat': 'b', + 'natural': '\u266e', + 'double-sharp': '##', + 'flat-flat': 'bb', + 'backslash': '\\', + 'slash': '/'} + +modifiersDictM21ToXml = {'#': 'sharp', + 'b': 'flat', + '##': 'double-sharp', + 'bb': 'flat-flat', + '\\': 'backslash', + '+': 'sharp', + '\u266f': 'sharp', + '\u266e': 'natural', + '\u266d': 'flat', + '\u20e5': 'sharp'} class Notation(prebase.ProtoM21Object): ''' @@ -129,11 +126,11 @@ class Notation(prebase.ProtoM21Object): , ) >>> n1.figures[0] - hasExt: False> + hasExt: False> >>> n1.figures[1] - hasExt: False> + hasExt: False> >>> n1.figures[2] - hasExt: False> + hasExt: False> Here, a stand-alone '#' is being passed to Notation. @@ -146,9 +143,9 @@ class Notation(prebase.ProtoM21Object): (, ) >>> n2.figures[0] - hasExt: False> + hasExt: False> >>> n2.figures[1] - hasExt: False> + hasExt: False> Now, a stand-alone b is being passed to Notation as part of a larger notationColumn. @@ -161,9 +158,9 @@ class Notation(prebase.ProtoM21Object): (, ) >>> n3.figures[0] - hasExt: False> + hasExt: False> >>> n3.figures[1] - hasExt: False> + hasExt: False> ''' _DOC_ORDER = ['notationColumn', 'figureStrings', 'numbers', 'modifiers', 'figures', 'origNumbers', 'origModStrings', 'modifierStrings'] @@ -388,9 +385,9 @@ def _getFigures(self): >>> from music21.figuredBass import notation as n >>> notation2 = n.Notation('-6,-') #__init__ method calls _getFigures() >>> notation2.figures[0] - hasExt: False> + hasExt: False> >>> notation2.figures[1] - hasExt: False> + hasExt: False> ''' figures = [] @@ -426,7 +423,7 @@ class Figure(prebase.ProtoM21Object): >>> from music21.figuredBass import notation >>> f1 = notation.Figure(4, '+') >>> f1 - hasExt: False> + hasExt: False> >>> f1.number 4 @@ -455,7 +452,7 @@ def __init__(self, number=1, modifierString=None): self.number = number self.modifierString = modifierString self.modifier = Modifier(modifierString) - # look for extenders underscore + # look for extenders underscore self.isExtender: bool = (self.number == '_') def _reprInternal(self): diff --git a/music21/harmony.py b/music21/harmony.py index e2419fe991..05b29f2b2b 100644 --- a/music21/harmony.py +++ b/music21/harmony.py @@ -2503,14 +2503,19 @@ def transpose(self: NCT, _value, *, inPlace=False) -> NCT | None: class FiguredBassIndication(Harmony): isFigure = True tstamp = 1 - def __init__(self, figs: list | None=None, **keywords): + def __init__(self, figs: str | list | None = None, **keywords): super().__init__(**keywords) if figs: if isinstance(figs, list): - figs = ','.join(figs) + _figs: str = ','.join(figs) + elif isinstance(figs, str): + if ',' in figs: + _figs = figs + else: + _figs = ','.join(figs) else: - figs = '' - self._fig_notation = notation.Notation(figs) + _figs = '' + self._fig_notation = notation.Notation(_figs) @property def fig_notation(self) -> notation.Notation: diff --git a/music21/mei/base.py b/music21/mei/base.py index d0e009edc7..de09c8e336 100644 --- a/music21/mei/base.py +++ b/music21/mei/base.py @@ -468,8 +468,8 @@ def allPartsPresent(scoreElem) -> tuple[str, ...]: if not partNs: raise MeiValidityError(_SEEMINGLY_NO_PARTS) - # Get information of possible tags in the score. If there are tags prepare a list to - # store them and process them later. + # Get information of possible tags in the score. If there are tags prepare a list to + # store them and process them later. # TODO: Maybe to be put in a separate function e.g. like allPartsPresent figuredBassQuery = f'.//{MEI_NS}fb' if scoreElem.findall(figuredBassQuery): @@ -2510,7 +2510,7 @@ def harmFromElement(elem, slurBundle=None) -> tuple: tagToFunction = {f'{MEI_NS}fb': figuredbassFromElement} fb_harmony_tag: tuple = () - + # Collect all elements in a measure and go throug extenders # tstamp has to be used as a duration marker between two elements @@ -2541,11 +2541,11 @@ def figuredbassFromElement(elem, slurBundle=None) -> harmony.FiguredBassIndicati dauer = float(subElement.attrib['dur.metrical']) # Generate a FiguredBassIndication object and set the collected information - fb_notation = ",".join(fb_notation_list) + fb_notation = ','.join(fb_notation_list) theFbNotation = harmony.FiguredBassIndication(fb_notation) theFbNotation.id = fb_id theFbNotation.duration = duration.Duration(quarterLength=dauer) - + return theFbNotation def clefFromElement(elem, slurBundle=None): # pylint: disable=unused-argument @@ -3225,7 +3225,7 @@ def measureFromElement(elem, backupNum, expectedNs, slurBundle=None, activeMeter # We must insert() these objects because a signals its changes for the # *start* of the in which it appears. staves[whichN].insert(0, eachObj) - + # Add objects to the staves dict staves['fb'] = harmElements # other childs of tags can be added here… @@ -3550,7 +3550,7 @@ def scoreFromElement(elem, slurBundle): # Get a tuple of all the @n attributes for the tags in this score. Each tag # corresponds to what will be a music21 Part. - # UPDATE: If tags are found, they will also be collected as a separate 'part' + # UPDATE: If tags are found, they will also be collected as a separate 'part' # to process them later. allPartNs = allPartsPresent(elem) @@ -3568,7 +3568,7 @@ def scoreFromElement(elem, slurBundle): harms = parsed['fb'][0] del parsed['fb'] allPartNs = allPartNs[0:-1] - + theScore = [stream.Part() for _ in range(len(allPartNs))] for i, eachN in enumerate(allPartNs): # set "atSoundingPitch" so transposition works @@ -3576,7 +3576,7 @@ def scoreFromElement(elem, slurBundle): for eachObj in parsed[eachN]: theScore[i].append(eachObj) theScore = stream.Score(theScore) - + # loop through measures to insert harm elements from harms list at the right offsets if harms: for index, measureOffset in enumerate(theScore.measureOffsetMap().keys()): diff --git a/music21/mei/test_base.py b/music21/mei/test_base.py index a8904b62d0..493fb8bc6f 100644 --- a/music21/mei/test_base.py +++ b/music21/mei/test_base.py @@ -3963,7 +3963,7 @@ def testMeasureUnit1(self, mockVoice, mockMeasure, for eachStaff in innerStaffs: mockStaffFE.assert_any_call(eachStaff, slurBundle=slurBundle) # ensure Measure.__init__() was called properly - self.assertEqual(len(expected) -1, mockMeasure.call_count) + self.assertEqual(len(expected) - 1, mockMeasure.call_count) for i in range(len(innerStaffs)): mockMeasure.assert_any_call(i, number=int(elem.get('n'))) mockMeasure.assert_any_call([mockVoice.return_value], number=int(elem.get('n'))) diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index 3dded78f08..d45d1d14e3 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -3232,7 +3232,7 @@ def __init__(self, self.spannerBundle = parent.spannerBundle self.objectSpannerBundle = self.spannerBundle # will change for each element. - self._fbiBefore: tuple[float, Element] = () + self._fbiBefore: tuple = () def parse(self): ''' @@ -3248,7 +3248,7 @@ def parse(self): self.setMxPrint() self.setMxAttributesObjectForStartOfMeasure() self.setLeftBarline() - # Look for FiguredBassIndications and add them to the local copy of the measure + # Look for FiguredBassIndications and add them to the local copy of the measure # and after the other elements if self.parent.fbis: self.insertFiguredBassIndications() @@ -3393,14 +3393,13 @@ def parseFlatElements( # seperately. O groupedObjList: list = [] for els in objIterator: - #print(els) if len(groupedObjList) > 0: if len(els) == 1 and isinstance(els[0], harmony.FiguredBassIndication): - #print('**multiple fb found**') + # print('**multiple fb found**') groupedObjList[-1].append(els[0]) continue groupedObjList.append(els) - #print('üüü', groupedObjList) + for objGroup in groupedObjList: groupOffset = m.elementOffset(objGroup[0]) offsetToMoveForward = groupOffset - self.offsetInMeasure @@ -3569,19 +3568,23 @@ def insertFiguredBassIndications(self) -> None: current stream.Measure object parsed afterwards. In a MusicXML file tags usually stand before the corresponding - note object. This order will be observed by parseFlatElements() function. + note object. This order will be observed by parseFlatElements() function. Same for multiple figures at one note. ''' # get the measure range to map the corresponding figuredBass Items - measureRange = (self.stream.offset, self.stream.offset + self.stream.highestTime) - - # Look if there are figures in the current measure and insert them - # to add them later. - for o, f in self.parent.fbis.items(): - if o >= measureRange[0] and o < measureRange[1]: - for fbi in f: - self.stream.insert(o - self.stream.offset, fbi) - + if self.stream: + measureRange = (self.stream.offset, self.stream.offset + self.stream.highestTime) + + # Look if there are figures in the current measure and insert them + # to add them later. + if self.parent and self.parent.fbis: + for o, f in self.parent.fbis.items(): + if o >= measureRange[0] and o < measureRange[1]: + for fbi in f: + self.stream.insert(o - self.stream.offset, fbi) + else: + raise MusicXMLExportException('No stream found') + def _hasRelatedSpanners(self, obj) -> bool: ''' returns True if and only if: @@ -4649,13 +4652,13 @@ def figuredBassToXml(self, f: harmony.FiguredBassIndication): # Hand the FiguredBassIndication over to the helper function. mxFB = self._figuresToXml(f, multipleFigures=multipleFigures) - + # Append it to xmlRoot self.xmlRoot.append(mxFB) self._fbiBefore = (f.offset, mxFB) - #_synchronizeIds(mxFB, f) + return mxFB - + def _figuresToXml(self, f: harmony.FiguredBassIndication, multipleFigures=False): # do Figure elements # self.addDividerComment('BEGIN: figured-bass') @@ -4683,7 +4686,7 @@ def _figuresToXml(self, f: harmony.FiguredBassIndication, multipleFigures=False) mxModifier = SubElement(mxFigure, 'prefix') mxModifier.text = modifiersDictM21ToXml[fbModifier] - # look for information on duration. If duration is 0 + # look for information on duration. If duration is 0 # jump over the tag # If we have multiple figures we have to set a tag # and update the tag one before. @@ -4701,7 +4704,7 @@ def _figuresToXml(self, f: harmony.FiguredBassIndication, multipleFigures=False) newDura = SubElement(fbDuration_before, 'duration') newDura.text = str(dura) else: - # If dura is set to 0 skip the update process. + # If dura is set to 0 skip the update process. # This happens e.g. at the beginning of a piece. # Otherwise an already set duration tag is resetted to 0 which is not wanted. if dura > 0: @@ -4710,13 +4713,13 @@ def _figuresToXml(self, f: harmony.FiguredBassIndication, multipleFigures=False) self.tempFigureDuration = dura else: self.offsetFiguresInMeasure = 0.0 - + if self.tempFigureDuration > 0: # print(f.quarterLength * self.currentDivisions) mxFbDuration = SubElement(mxFB, 'duration') mxFbDuration.text = str(round(self.tempFigureDuration)) self.tempFigureDuration = 0.0 - + return mxFB def durationXml(self, dur: duration.Duration): diff --git a/music21/musicxml/xmlToM21.py b/music21/musicxml/xmlToM21.py index cf00ecd91a..25243f645c 100644 --- a/music21/musicxml/xmlToM21.py +++ b/music21/musicxml/xmlToM21.py @@ -2370,7 +2370,6 @@ def appendFbis(self, fbi, measureOffset): self.parent.fbis.append((absOffset, fbi)) else: self.parent.fbis = [(absOffset, fbi)] - #print(self.parent.fbis) # ----------------------------------------------------------------------------- class MeasureParser(XMLParserBase): @@ -2481,10 +2480,10 @@ def __init__(self, # what is the offset in the measure of the current note position? self.offsetMeasureNote: OffsetQL = 0.0 - + # Offset Calc if more than one figure is set under a single note self.lastFigureDuration = 0 - + # keep track of the last rest that was added with a forward tag. # there are many pieces that end with incomplete measures that # older versions of Finale put a forward tag at the end, but this @@ -2882,12 +2881,12 @@ def xmlToNote(self, mxNote: ET.Element) -> None: # only increment Chords after completion self.offsetMeasureNote += offsetIncrement self.endedWithForwardTag = None - + # reset offset for figures. This is needed to put in # multiple FiguredBassIndications at one note at the right offset. - # Musicxml puts tags immediately before a tag, + # Musicxml puts tags immediately before a tag, # which means that we have to reset a given offset duration of some - # tags after inserting the coressponding note and + # tags after inserting the coressponding note and # before going to a new note. self.lastFigureDuration = 0 @@ -5270,7 +5269,7 @@ def xmlToChordSymbol( return cs def xmlToFiguredBass(self, mxFiguredBass): - #print('Hello from xmlToFiguredBass', mxFiguredBass.findall('*')) + # print('Hello from xmlToFiguredBass', mxFiguredBass.findall('*')) fb_strings: list[str] = [] sep = ',' d: duration.Duration | None = None @@ -5284,7 +5283,6 @@ def xmlToFiguredBass(self, mxFiguredBass): if el.tag == 'figure-number': if el.text: fb_number = el.text - # Get prefix and/or suffix. # The function returns an empty string if nothing is found. fb_prefix = self._getFigurePrefixOrSuffix(figure, 'prefix') @@ -5315,14 +5313,14 @@ def xmlToFiguredBass(self, mxFiguredBass): # If a duration is provided, set length of the FigureBassIndication if d: fbi.quarterLength = d.quarterLength - # function in parent add add found objects. - # + # call function in parent to add found objects. self.parent.appendFbis(fbi, offsetFbi) - def _getFigurePrefixOrSuffix(self, figure, presuf: str='prefix') -> str: + def _getFigurePrefixOrSuffix(self, figure, presuf: str = 'prefix') -> str: if figure.findall(presuf): for fix in figure.findall(presuf): if fix.text: + print(fix.text) return modifiersDictXmlToM21[fix.text] return '' From dcd147734526b273a93ec2e4ff89af47fc3c9dba Mon Sep 17 00:00:00 2001 From: mxordn Date: Fri, 31 Mar 2023 00:53:06 +0200 Subject: [PATCH 45/58] Update music21/musicxml/xmlToM21.py Co-authored-by: Jacob Walls --- music21/musicxml/xmlToM21.py | 1 - 1 file changed, 1 deletion(-) diff --git a/music21/musicxml/xmlToM21.py b/music21/musicxml/xmlToM21.py index 25243f645c..b61e515ed3 100644 --- a/music21/musicxml/xmlToM21.py +++ b/music21/musicxml/xmlToM21.py @@ -5269,7 +5269,6 @@ def xmlToChordSymbol( return cs def xmlToFiguredBass(self, mxFiguredBass): - # print('Hello from xmlToFiguredBass', mxFiguredBass.findall('*')) fb_strings: list[str] = [] sep = ',' d: duration.Duration | None = None From 59974de52c4a7b92984666eca880996accbfd5f3 Mon Sep 17 00:00:00 2001 From: Moritz Heffter Date: Tue, 13 Jun 2023 16:53:39 +0200 Subject: [PATCH 46/58] added function documentation strings with examples --- music21/figuredBass/notation.py | 2 +- music21/harmony.py | 3 +-- music21/musicxml/m21ToXml.py | 24 +++++++++++++++++++++ music21/musicxml/xmlToM21.py | 37 +++++++++++++++++++++++++++------ 4 files changed, 57 insertions(+), 9 deletions(-) diff --git a/music21/figuredBass/notation.py b/music21/figuredBass/notation.py index 0ea12b7b0e..778b0baa28 100644 --- a/music21/figuredBass/notation.py +++ b/music21/figuredBass/notation.py @@ -311,7 +311,7 @@ def _parseNotationColumn(self): self.extenders = [False for i in range(len(modifierStrings))] else: extenders = tuple(self.extenders) - print('angekommen', numbers, modifierStrings, extenders) + #print('angekommen', numbers, modifierStrings, extenders) self.origNumbers = numbers # Keep original numbers self.numbers = numbers # Will be converted to longhand diff --git a/music21/harmony.py b/music21/harmony.py index 73c4fcf19a..590b291384 100644 --- a/music21/harmony.py +++ b/music21/harmony.py @@ -2494,8 +2494,7 @@ def transpose(self: NCT, _value, *, inPlace=False) -> NCT | None: # ------------------------------------------------------------------------------ class FiguredBassIndication(Harmony): - isFigure = True - tstamp = 1 + isFigure: bool = True def __init__(self, figs: str | list | None = None, extenders: list[bool] | None = None , **keywords): super().__init__(**keywords) if figs: diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index b98dc43d99..2b8c2c638d 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -4611,6 +4611,30 @@ def chordToXml(self, c: chord.ChordBase) -> list[Element]: return mxNoteList def figuredBassToXml(self, f: harmony.FiguredBassIndication): + ''' + Converts a FigurdBassIndication object to a musicxml tag. + If there are multiple indications for one note they will be observed. + + >>> fbi = harmony.FiguredBassIndication() + >>> fbi.quarterLength = 2 + >>> fbi.fig_noation = '#,6b' + + >>> MEX = musicxml.m21ToXml.MeasureExporter() + + >>> mxFigures = MEX.figuredBassToXml(fbi) + >>> MEX.dump(mxFigures) + +
+ + sharp +
+
+ 6 + flat +
+
+ ''' + # For FiguredBassElements we need to check whether there are # multiple figures for one note. # Therefore we compare offset of the figure and the current offsetInMeasure variable diff --git a/music21/musicxml/xmlToM21.py b/music21/musicxml/xmlToM21.py index 57689fb48f..bc9f13aeec 100644 --- a/music21/musicxml/xmlToM21.py +++ b/music21/musicxml/xmlToM21.py @@ -5173,10 +5173,32 @@ def xmlToChordSymbol( return cs def xmlToFiguredBass(self, mxFiguredBass) -> harmony.FiguredBassIndication: - # print('Hello from xmlToFiguredBass', mxFiguredBass.findall('*')) + # noinspection PyShadowingNames + ''' + Converts a figured bass tag in musicxml to a harmony.FiguredBassIndication object: + + >>> from xml.etree.ElementTree import fromstring as EL + >>> MP = musicxml.xmlToM21.MeasureParser() + + >>> fbStr = """ + +
+ 5 +
+
+ 4 +
+
+ """ + >>> mxFigures = EL(fbStr) + >>> fbi = MP.xmlToFiguredBass(mxFigures) + >>> fbi + + ''' + fb_strings: list[str] = [] fb_extenders: list[bool] = [] - sep = ',' + sep: str = ',' d: duration.Duration | None = None offsetFbi = self.offsetMeasureNote @@ -5231,18 +5253,21 @@ def xmlToFiguredBass(self, mxFiguredBass) -> harmony.FiguredBassIndication: self.lastFigureDuration = d.quarterLength fb_string = sep.join(fb_strings) - print('Ergebnis:', fb_string, fb_extenders) fbi = harmony.FiguredBassIndication(fb_string, extenders=fb_extenders) + # If a duration is provided, set length of the FigureBassIndication if d: fbi.quarterLength = d.quarterLength - # call function in parent to add found objects. - #self.parent.appendFbis(fbi, offsetFbi) + self.stream.insert(offsetFbi, fbi) return fbi def _getFigurePrefixOrSuffix(self, figure, presuf: str = 'prefix') -> str: - # helper function for prefixes and suffixes of figure numbers. + ''' + A helper function for prefixes and suffixes of figure numbers. + Called two times from xmlToFiguredBass(). + ''' + if figure.findall(presuf): for fix in figure.findall(presuf): if fix.text: From a1efcedb1219da30ef6dc123edb959c00824ad48 Mon Sep 17 00:00:00 2001 From: Moritz Heffter Date: Tue, 13 Jun 2023 21:44:38 +0200 Subject: [PATCH 47/58] added more documentation in the mei parser --- music21/mei/base.py | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/music21/mei/base.py b/music21/mei/base.py index f267724508..08f5814be7 100644 --- a/music21/mei/base.py +++ b/music21/mei/base.py @@ -2506,6 +2506,22 @@ def chordFromElement(elem, slurBundle=None): return theChord def harmFromElement(elem, slurBundle=None) -> tuple: + ''' + The MEI tag is used for several types of indications. In the MEI v4 guidelines it says: + + An indication of harmony, e.g., chord names, tablature grids, harmonic analysis, figured bass. + (In MEI v4 guidelines: https://music-encoding.org/guidelines/v4/elements/harm.html) + + **Attributes/Elements Implemented:** + + - @xml:id (or id), an XML id (submitted as the Music21Object "id") + - tags that contain figured bass information + + **Attributes/Elements in Testing:** none + + **Attributes not Implemented:** a lot + ''' + # other tags than to be added… tagToFunction = {f'{MEI_NS}fb': figuredbassFromElement} @@ -2513,7 +2529,6 @@ def harmFromElement(elem, slurBundle=None) -> tuple: # Collect all elements in a measure and go throug extenders # tstamp has to be used as a duration marker between two elements - for subElement in _processEmbeddedElements(elem.findall('*'), tagToFunction, elem.tag, slurBundle): @@ -2524,6 +2539,21 @@ def harmFromElement(elem, slurBundle=None) -> tuple: return fb_harmony_tag def figuredbassFromElement(elem, slurBundle=None) -> harmony.FiguredBassIndication: + ''' + This function handles basically the MEI tag which is contained in the tag. + At the end a harmony.FiguredBassIndication object is returned. + + **Attributes/Elements Implemented:** + + - @xml:id (or id), an XML id (submitted as the Music21Object "id") + - @dur.metrical, duration of the figure + - @extender, contains information if a figure is prolonged over the duration of the figure + + **Attributes/Elements in Testing:** none + + **Attributes not Implemented:** a lot + ''' + if elem.get(_XMLID): fb_id = elem.get(_XMLID) fb_notation: str = '' From 95c57fdccdbaa274389b8109658fc8785912ab51 Mon Sep 17 00:00:00 2001 From: Moritz Heffter Date: Tue, 13 Jun 2023 22:02:15 +0200 Subject: [PATCH 48/58] mei commit --- music21/mei/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/music21/mei/base.py b/music21/mei/base.py index 08f5814be7..4586051583 100644 --- a/music21/mei/base.py +++ b/music21/mei/base.py @@ -2560,6 +2560,7 @@ def figuredbassFromElement(elem, slurBundle=None) -> harmony.FiguredBassIndicati fb_notation_list: list[str] = [] fb_extenders: list[bool] = [] dauer: float = 0 + # loop through all child elements and collect tags for subElement in elem.findall('*'): if subElement.tag == f'{MEI_NS}f': From 86c95beab605f9b5c75a2a6f58ff6a1039ec5cde Mon Sep 17 00:00:00 2001 From: Moritz Heffter Date: Tue, 13 Jun 2023 22:15:21 +0200 Subject: [PATCH 49/58] resolve two merge conflicts --- music21/musicxml/m21ToXml.py | 17 ----------------- music21/musicxml/xmlToM21.py | 10 +--------- 2 files changed, 1 insertion(+), 26 deletions(-) diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index 198e9d7814..632f2f25bd 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -1706,22 +1706,6 @@ def setScoreLayouts(self) -> None: self.firstScoreLayout = scoreLayouts.first() self.scoreLayouts = list(scoreLayouts) -<<<<<<< HEAD - def _populatePartExporterList(self) -> None: -======= - def getFiguredBassIndications(self): - ''' - Collect all harmony.FiguredBassIndications found in the score and store them - in a dict. The dict is later passed to the PartExporter specified - (standard value -1 for the lowest part/staff). With in the MeasureExporter the objeccts are - inserted locally in the measure and finally parsed to the converter. - ''' - for fbi in self.stream.getElementsByClass(harmony.FiguredBassIndication): - if fbi.offset not in self.fbis_dict.keys(): - self.fbis_dict[fbi.offset] = [fbi] - else: - self.fbis_dict[fbi.offset].append([fbi]) - def getFiguredBassIndications(self): ''' Collect all harmony.FiguredBassIndications found in the score and store them @@ -1736,7 +1720,6 @@ def getFiguredBassIndications(self): self.fbis_dict[fbi.offset].append([fbi]) def _populatePartExporterList(self): ->>>>>>> 7ca383457 (first working musicxml output for FiguredBassIndication Objects) count = 0 sp = list(self.parts) for innerStream in sp: diff --git a/music21/musicxml/xmlToM21.py b/music21/musicxml/xmlToM21.py index dbe8772dea..c50632095d 100644 --- a/music21/musicxml/xmlToM21.py +++ b/music21/musicxml/xmlToM21.py @@ -2408,16 +2408,8 @@ class MeasureParser(XMLParserBase): 'direction': 'xmlDirection', 'attributes': 'parseAttributesTag', 'harmony': 'xmlHarmony', -<<<<<<< HEAD -<<<<<<< HEAD - 'figured-bass': None, - 'sound': 'xmlSound', -======= -======= ->>>>>>> 695a1e971a44bd4e8688984cf895c2ce7ff9d30d 'figured-bass': 'xmlToFiguredBass', - 'sound': None, ->>>>>>> 82995d732 (xml import and export fixed problems with more than two figures per note) + 'sound': 'xmlSound', 'barline': 'xmlBarline', 'grouping': None, 'link': None, From 7b2d99a8914ce407a6320b0f3b6dd698482edf26 Mon Sep 17 00:00:00 2001 From: Moritz Heffter Date: Tue, 13 Jun 2023 22:27:30 +0200 Subject: [PATCH 50/58] version with Figures at top level --- music21/musicxml/m21ToXml.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index 5b61b307db..0749409cd0 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -1706,22 +1706,6 @@ def setScoreLayouts(self) -> None: self.firstScoreLayout = scoreLayouts.first() self.scoreLayouts = list(scoreLayouts) -<<<<<<< HEAD - def _populatePartExporterList(self) -> None: -======= - def getFiguredBassIndications(self): - ''' - Collect all harmony.FiguredBassIndications found in the score and store them - in a dict. The dict is later passed to the PartExporter specified - (standard value -1 for the lowest part/staff). With in the MeasureExporter the objeccts are - inserted locally in the measure and finally parsed to the converter. - ''' - for fbi in self.stream.getElementsByClass(harmony.FiguredBassIndication): - if fbi.offset not in self.fbis_dict.keys(): - self.fbis_dict[fbi.offset] = [fbi] - else: - self.fbis_dict[fbi.offset].append([fbi]) - def getFiguredBassIndications(self): ''' Collect all harmony.FiguredBassIndications found in the score and store them @@ -1736,7 +1720,6 @@ def getFiguredBassIndications(self): self.fbis_dict[fbi.offset].append([fbi]) def _populatePartExporterList(self): ->>>>>>> 7ca383457 (first working musicxml output for FiguredBassIndication Objects) count = 0 sp = list(self.parts) for innerStream in sp: From 704434f1601669e1cbad48abbcd37f2ee21d6e1f Mon Sep 17 00:00:00 2001 From: mxordn Date: Thu, 15 Jun 2023 15:40:32 +0200 Subject: [PATCH 51/58] added some additional checks and warnings for prefixes and suffixes --- music21/musicxml/xmlToM21.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/music21/musicxml/xmlToM21.py b/music21/musicxml/xmlToM21.py index c88d34727b..e74c760d65 100644 --- a/music21/musicxml/xmlToM21.py +++ b/music21/musicxml/xmlToM21.py @@ -5362,7 +5362,12 @@ def _getFigurePrefixOrSuffix(self, figure, presuf: str = 'prefix') -> str: if figure.findall(presuf): for fix in figure.findall(presuf): if fix.text: - return modifiersDictXmlToM21[fix.text] + mod: str | None = modifiersDictXmlToM21.get(fix.text) + if mod: + return mod + else: + warnings.warn(f'''{fix.text} is currently not supported. Please look into + the modifiersDictXmlToM21 for changes, that fit your needs.''') return '' def xmlDirection(self, mxDirection): From 1f2d4ce58a6f4d81fba480f4573051d95c70cce4 Mon Sep 17 00:00:00 2001 From: mxordn Date: Fri, 16 Jun 2023 12:09:55 +0200 Subject: [PATCH 52/58] added prefix and suffix warnings --- music21/musicxml/xmlToM21.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/music21/musicxml/xmlToM21.py b/music21/musicxml/xmlToM21.py index e74c760d65..6fe094d26f 100644 --- a/music21/musicxml/xmlToM21.py +++ b/music21/musicxml/xmlToM21.py @@ -5349,7 +5349,7 @@ def xmlToFiguredBass(self, mxFiguredBass) -> harmony.FiguredBassIndication: # If a duration is provided, set length of the FigureBassIndication if d: fbi.quarterLength = d.quarterLength - + print(fbi) self.stream.insert(offsetFbi, fbi) return fbi @@ -5358,7 +5358,6 @@ def _getFigurePrefixOrSuffix(self, figure, presuf: str = 'prefix') -> str: A helper function for prefixes and suffixes of figure numbers. Called two times from xmlToFiguredBass(). ''' - if figure.findall(presuf): for fix in figure.findall(presuf): if fix.text: @@ -5366,8 +5365,9 @@ def _getFigurePrefixOrSuffix(self, figure, presuf: str = 'prefix') -> str: if mod: return mod else: - warnings.warn(f'''{fix.text} is currently not supported. Please look into - the modifiersDictXmlToM21 for changes, that fit your needs.''') + warnings.warn(f'''{fix.text} is currently not supported. Please look into + the modifiersDictXmlToM21 and make your changes, that will + fit your needs.''') return '' def xmlDirection(self, mxDirection): From 74f4d969972ce2480041bc1645cd961252baa21a Mon Sep 17 00:00:00 2001 From: mxordn Date: Sat, 17 Jun 2023 15:19:35 +0200 Subject: [PATCH 53/58] added examples for figured bass --- .../piece01-bwv-1023-1-beginning.musicxml | 1580 +++++++++++++++ .../piece02-bwv-1021-1-beginning.musicxml | 1788 +++++++++++++++++ .../piece03-advanced-figures.musicxml | 425 ++++ 3 files changed, 3793 insertions(+) create mode 100644 music21/musicxml/lilypondTestSuite/piece01-bwv-1023-1-beginning.musicxml create mode 100644 music21/musicxml/lilypondTestSuite/piece02-bwv-1021-1-beginning.musicxml create mode 100644 music21/musicxml/lilypondTestSuite/piece03-advanced-figures.musicxml diff --git a/music21/musicxml/lilypondTestSuite/piece01-bwv-1023-1-beginning.musicxml b/music21/musicxml/lilypondTestSuite/piece01-bwv-1023-1-beginning.musicxml new file mode 100644 index 0000000000..bc071f9f7a --- /dev/null +++ b/music21/musicxml/lilypondTestSuite/piece01-bwv-1023-1-beginning.musicxml @@ -0,0 +1,1580 @@ + + + + + + MuseScore 3.6.2 + 2023-06-17 + + + + + + + + + + 6.99912 + 40 + + + 1697.36 + 1200.15 + + 85.7251 + 85.7251 + 85.7251 + 85.7251 + + + 85.7251 + 85.7251 + 85.7251 + 85.7251 + + + + + + + + Part 1 + + + + + + 1 + 1 + 78.7402 + 0 + + + + Part 2 + + + + + + 2 + 1 + 78.7402 + 0 + + + + + + + + + 50.00 + 0.00 + + 70.00 + + + + 24 + + 1 + + + + G + 2 + + + + + E + 4 + + 24 + 1 + quarter + up + + + + + + + 24 + 1 + quarter + + + + E + 5 + + 18 + 1 + eighth + + down + begin + + + + + + + F + 1 + 5 + + 3 + 1 + 32nd + down + continue + begin + begin + + + + G + 5 + + 3 + 1 + 32nd + down + end + end + end + + + + + + + + + F + 1 + 5 + + 8 + 1 + eighth + + 3 + 2 + + down + begin + + + + + + + + D + 1 + 5 + + 8 + 1 + eighth + sharp + + 3 + 2 + + down + continue + + + + E + 5 + + 8 + 1 + eighth + + 3 + 2 + + down + end + + + + + + + + C + 5 + + 24 + 1 + quarter + down + + + + + + + B + 4 + + 18 + 1 + eighth + + up + begin + + + + + + + + + + A + 4 + + 6 + 1 + 16th + up + end + backward hook + + + + + + G + 4 + + 18 + 1 + eighth + + up + begin + + + + E + 4 + + 3 + 1 + 32nd + up + continue + begin + begin + + + + + + + F + 1 + 4 + + 3 + 1 + 32nd + up + end + end + end + + + + G + 4 + + 18 + 1 + eighth + + up + begin + + + + + + + A + 4 + + 3 + 1 + 32nd + up + continue + begin + begin + + + + + + + B + 4 + + 3 + 1 + 32nd + up + end + end + end + + + + C + 5 + + 18 + 1 + eighth + + down + begin + + + + + + + D + 5 + + 3 + 1 + 32nd + natural + down + continue + begin + begin + + + + E + 5 + + 3 + 1 + 32nd + down + end + end + end + + + + + + D + 4 + + 18 + 1 + eighth + + up + begin + + + + F + 1 + 5 + + 6 + 1 + 16th + up + end + backward hook + + + + A + 5 + + 18 + 1 + eighth + + down + begin + + + + G + 5 + + 3 + 1 + 32nd + down + continue + begin + begin + + + + F + 1 + 5 + + 3 + 1 + 32nd + down + end + end + end + + + + + F + 1 + 5 + + 1 + 16th + up + + + + + + + E + 5 + + 18 + 1 + eighth + + down + begin + + + + + + + + + + + D + 1 + 5 + + 3 + 1 + 32nd + sharp + down + continue + begin + begin + + + + E + 5 + + 3 + 1 + 32nd + down + end + end + end + + + + + + + + + D + 1 + 5 + + 24 + 1 + quarter + sharp + down + + + + C + 1 + 5 + + 12 + 1 + eighth + sharp + down + begin + + + + B + 4 + + 12 + 1 + eighth + down + end + + + + + + + B + 5 + + 24 + + 1 + quarter + down + + + + + + + + + B + 5 + + 8 + + 1 + eighth + + 3 + 2 + + down + begin + + + + + + + + + G + 1 + 5 + + 8 + 1 + eighth + sharp + + 3 + 2 + + down + continue + + + + E + 5 + + 8 + 1 + eighth + + 3 + 2 + + down + end + + + + + + + + + D + 5 + + 6 + 1 + 16th + natural + down + begin + begin + + + + + + + B + 4 + + 6 + 1 + 16th + down + continue + end + + + + C + 5 + + 12 + 1 + eighth + natural + down + end + + + + + + + A + 5 + + 24 + + 1 + quarter + down + + + + + + + + + A + 5 + + 8 + + 1 + eighth + + 3 + 2 + + down + begin + + + + + + + + + F + 1 + 5 + + 8 + 1 + eighth + + 3 + 2 + + down + continue + + + + + + + D + 1 + 5 + + 8 + 1 + eighth + sharp + + 3 + 2 + + down + end + + + + + + + + C + 5 + + 8 + 1 + eighth + natural + + 3 + 2 + + down + begin + + + + + + + + + A + 4 + + 8 + 1 + eighth + + 3 + 2 + + down + continue + + + + B + 4 + + 8 + 1 + eighth + + 3 + 2 + + down + end + + + + + + + + G + 5 + + 24 + + 1 + quarter + natural + down + + + + + + + + + G + 5 + + 8 + + 1 + eighth + + 3 + 2 + + down + begin + + + + + + + + + F + 1 + 5 + + 8 + 1 + eighth + + 3 + 2 + + down + continue + + + + + + + C + 6 + + 8 + 1 + eighth + + 3 + 2 + + down + end + + + + + + + + B + 5 + + 8 + 1 + eighth + + 3 + 2 + + down + begin + + + + + + + + + D + 1 + 5 + + 8 + 1 + eighth + sharp + + 3 + 2 + + down + continue + + + + E + 5 + + 8 + 1 + eighth + + 3 + 2 + + down + end + + + + + + + + A + 5 + + 8 + 1 + eighth + + 3 + 2 + + down + begin + + + + + + + + G + 5 + + 8 + 1 + eighth + + 3 + 2 + + down + continue + + + + F + 1 + 5 + + 8 + 1 + eighth + + 3 + 2 + + down + end + + + + + + + + + + E + 5 + + 18 + 1 + eighth + + down + begin + + + + + + + G + 5 + + 3 + 1 + 32nd + down + continue + begin + begin + + + + F + 1 + 5 + + 3 + 1 + 32nd + down + end + end + end + + + + + + + G + 5 + + 48 + 1 + half + down + + + light-heavy + + + + + + + + 65.00 + + + + 24 + + 1 + + + + F + 4 + + + + + E + 2 + + 24 + 1 + quarter + up + + + + + + + E + 3 + + 48 + + 1 + half + down + + + + + + + +
+ 6 +
+
+ 4 +
+
+ 2 +
+
+ + + E + 3 + + 24 + + 1 + quarter + down + + + + 4 + + + + +
+ 7 +
+
+ 5 +
+ 24 +
+ +
+ 6 +
+ 24 +
+ + + D + 1 + 3 + + 48 + 1 + half + sharp + down + + + 2 + + + +
+ + +
+ 9 +
+
+ + + E + 3 + + 24 + 1 + quarter + down + + +
+ 8 +
+ 12 +
+ +
+ 7 +
+ 12 +
+ + + E + 2 + + 24 + 1 + quarter + up + + +
+ 6 +
+
+ + + E + 3 + + 24 + 1 + quarter + down + +
+ + +
+ 7 +
+ 12 +
+ +
+ 6 +
+ 12 +
+ + + D + 3 + + 24 + 1 + quarter + down + + + 2 + + + + +
+ 6 +
+
+ + + C + 3 + + 48 + 1 + half + up + +
+ + +
+ sharp +
+
+ + + B + 2 + + 24 + 1 + quarter + up + + +
+ sharp +
+
+ + + B + 3 + + 12 + 1 + eighth + down + begin + + + + + + + + + A + 3 + + 12 + 1 + eighth + down + end + + +
+ 6 +
+
+ 5 + natural +
+
+ + + G + 1 + 3 + + 24 + 1 + quarter + sharp + down + +
+ + +
+ 9 +
+
+ 4 +
+ 24 +
+ +
+ 8 +
+
+ 3 +
+ 24 +
+ + + A + 3 + + 48 + 1 + half + down + + + 4 + + + + +
+ 6 + backslash +
+
+ 5 +
+ 12 +
+ +
+ 7 +
+ 12 +
+ + + F + 1 + 3 + + 24 + 1 + quarter + down + +
+ + +
+ 7 +
+
+ 5 +
+ 24 +
+ +
+ 6 +
+ 24 +
+ + + D + 1 + 3 + + 48 + 1 + half + sharp + down + + + 1 + + + + + + E + 3 + + 24 + 1 + quarter + down + +
+ + +
+ 7 +
+
+ 5 +
+ 12 +
+ +
+ 6 +
+
+ 4 + cross +
+ 12 +
+ + + A + 2 + + 24 + 1 + quarter + up + + + 2 + + + + +
+ 6 +
+
+ + + G + 2 + + 12 + 1 + eighth + up + begin + + +
+ 7 +
+
+ 5 +
+
+ + + A + 2 + + 12 + 1 + eighth + up + end + + +
+ 8 +
+
+ sharp +
+ 12 +
+ +
+ 7 +
+ 12 +
+ + + B + 2 + + 24 + 1 + quarter + up + +
+ + + + E + 2 + + 24 + 1 + quarter + up + + + + 24 + 1 + quarter + + +
+ 8 +
+ 12 +
+ +
+ 7 +
+ 12 +
+ + + E + 3 + + 24 + 1 + quarter + down + + + light-heavy + +
+
+
diff --git a/music21/musicxml/lilypondTestSuite/piece02-bwv-1021-1-beginning.musicxml b/music21/musicxml/lilypondTestSuite/piece02-bwv-1021-1-beginning.musicxml new file mode 100644 index 0000000000..dc2ea21366 --- /dev/null +++ b/music21/musicxml/lilypondTestSuite/piece02-bwv-1021-1-beginning.musicxml @@ -0,0 +1,1788 @@ + + + + + + MuseScore 3.6.2 + 2023-06-14 + + + + + + + + + + 6.4 + 40 + + + 1856.25 + 1312.5 + + 93.75 + 93.75 + 93.75 + 93.75 + + + 93.75 + 93.75 + 93.75 + 93.75 + + + + + + + + bracket + + + Violine + Vl. + + + + + + 1 + 1 + 78.7402 + 0 + + + + (r. H.) + (r. H.) + + Klavier + + + + 3 + 1 + 78.7402 + 0 + + + + Basso continuo + B.c. + + + + + + 2 + 1 + 78.7402 + 0 + + + + + + + + + + 153.15 + 0.00 + + 70.00 + + + + 8 + + 1 + + + + G + 2 + + + + + D + 5 + + 8 + + 1 + quarter + down + + + + + + + D + 5 + + 2 + + 1 + 16th + down + begin + begin + + + + + + + D + 5 + + 2 + 1 + 16th + down + continue + continue + + + + E + 5 + + 2 + 1 + 16th + down + continue + continue + + + + F + 1 + 5 + + 1 + 1 + 32nd + down + continue + continue + begin + + + + + + + G + 5 + + 1 + 1 + 32nd + down + end + end + end + + + + + + + C + 5 + + 1 + 1 + 32nd + down + begin + begin + begin + + + + + + + + + + B + 4 + + 1 + 1 + 32nd + down + continue + continue + end + + + + + + + + + C + 5 + + 2 + 1 + 16th + down + continue + end + + + + + + + A + 5 + + 4 + + 1 + eighth + down + end + + + + + + + A + 5 + + 2 + + 1 + 16th + down + begin + begin + + + + + + + C + 5 + + 2 + 1 + 16th + down + continue + continue + + + + B + 4 + + 2 + 1 + 16th + down + continue + continue + + + + A + 4 + + 2 + 1 + 16th + down + end + end + + + + + + B + 4 + + 2 + 1 + 16th + up + begin + begin + + + + G + 4 + + 1 + 1 + 32nd + up + continue + continue + begin + + + + + + + A + 4 + + 1 + 1 + 32nd + up + continue + continue + continue + + + + B + 4 + + 1 + 1 + 32nd + up + continue + continue + continue + + + + C + 5 + + 1 + 1 + 32nd + up + continue + continue + end + + + + D + 5 + + 2 + + 1 + 16th + up + end + end + + + + + + + + D + 5 + + 2 + + 1 + 16th + down + begin + begin + + + + + + + E + 5 + + 1 + 1 + 32nd + down + continue + continue + begin + + + + + + + F + 1 + 5 + + 1 + 1 + 32nd + down + continue + continue + continue + + + + G + 5 + + 1 + 1 + 32nd + down + continue + continue + continue + + + + A + 5 + + 1 + 1 + 32nd + down + continue + continue + end + + + + B + 5 + + 2 + + 1 + 16th + down + end + end + + + + + + + + B + 5 + + 2 + + 1 + 16th + down + begin + begin + + + + + + + A + 5 + + 1 + 1 + 32nd + down + continue + continue + begin + + + + G + 5 + + 1 + 1 + 32nd + down + continue + continue + continue + + + + A + 5 + + 1 + 1 + 32nd + down + continue + continue + continue + + + + F + 1 + 5 + + 1 + 1 + 32nd + down + continue + continue + end + + + + G + 5 + + 2 + 1 + 16th + down + end + end + + + + F + 1 + 5 + + 4 + 1 + eighth + down + begin + + + + D + 5 + + 4 + + 1 + eighth + down + end + + + + + + + + + D + 5 + + 4 + + 1 + eighth + down + begin + + + + + + + B + 4 + + 4 + 1 + eighth + down + end + + + + E + 4 + + 4 + 1 + eighth + up + begin + + + + D + 5 + + 2 + 1 + 16th + up + continue + begin + + + + + + + C + 5 + + 1 + 1 + 32nd + up + continue + continue + begin + + + + B + 4 + + 1 + 1 + 32nd + up + end + end + end + + + + + + + C + 5 + + 4 + 1 + eighth + up + begin + + + + A + 4 + + 4 + 1 + eighth + up + end + + + + D + 4 + + 4 + 1 + eighth + up + begin + + + + C + 5 + + 2 + 1 + 16th + up + continue + begin + + + + + + + B + 4 + + 1 + 1 + 32nd + up + continue + continue + begin + + + + A + 4 + + 1 + 1 + 32nd + up + end + end + end + + + + + + + + + B + 4 + + 4 + 1 + eighth + down + begin + + + + G + 5 + + 4 + + 1 + eighth + down + end + + + + + + + G + 5 + + 2 + + 1 + 16th + down + begin + begin + + + + + + + F + 1 + 5 + + 1 + 1 + 32nd + down + continue + continue + begin + + + + E + 5 + + 1 + 1 + 32nd + down + continue + continue + end + + + + D + 5 + + 2 + 1 + 16th + down + continue + continue + + + + C + 5 + + 2 + 1 + 16th + down + end + end + + + + B + 4 + + 4 + + 1 + eighth + up + begin + + + + + + + B + 4 + + 1 + + 1 + 32nd + up + continue + begin + begin + + + + + + + A + 4 + + 1 + 1 + 32nd + up + continue + continue + continue + + + + G + 4 + + 1 + 1 + 32nd + up + continue + continue + continue + + + + F + 1 + 4 + + 1 + 1 + 32nd + up + end + end + end + + + + G + 4 + + 2 + 1 + 16th + up + begin + begin + + + + F + 1 + 4 + + 1 + 1 + 32nd + up + continue + continue + begin + + + + E + 4 + + 1 + 1 + 32nd + up + continue + continue + end + + + + D + 4 + + 2 + 1 + 16th + up + continue + continue + + + + C + 4 + + 2 + 1 + 16th + up + end + end + + + light-heavy + + + + + + + + 65.00 + + + + 8 + + 1 + + + + G + 2 + + + + + 32 + 1 + + + + + + 32 + 1 + + + + + + 32 + 1 + + + + + + 32 + 1 + + + light-heavy + + + + + + + + 65.00 + + + + 8 + + 1 + + + + F + 4 + + + +
+ 3 +
+
+ 8 +
+ 4 +
+ +
+ 4 +
+
+ 2 +
+ 4 +
+ + + G + 2 + + 8 + 1 + quarter + up + + +
+ 5 +
+
+ 3 +
+ 4 +
+ +
+ 6 +
+ 2 +
+ +
+ 5 +
+ 2 +
+ + + G + 3 + + 8 + + 1 + quarter + down + + + + + +
+ 6 +
+
+ 4 +
+
+ 2 +
+
+ + + G + 3 + + 4 + + 1 + eighth + down + begin + + + + + + + F + 1 + 3 + + 2 + 1 + 16th + down + continue + begin + + + + E + 3 + + 2 + 1 + 16th + down + end + end + + +
+ 6 +
+
+ 5 +
+
+ + + F + 1 + 3 + + 4 + 1 + eighth + down + begin + + +
+ +
+
+ + + D + 3 + + 4 + 1 + eighth + down + end + +
+ + +
+ 9 +
+
+ + + G + 3 + + 4 + 1 + eighth + up + begin + + +
+ 6 +
+
+ 4 +
+ 2 +
+ +
+ +
+
+ 3 +
+ 2 +
+ + + D + 3 + + 4 + 1 + eighth + up + continue + + +
+ 6 +
+
+ + + B + 2 + + 4 + 1 + eighth + up + continue + + + + G + 2 + + 4 + 1 + eighth + up + end + + +
+ 6 +
+
+ 4 +
+ 4 +
+ +
+ 5 +
+
+ 3 +
+ 2 +
+ +
+ 4 +
+
+ 2 +
+ 2 +
+ + + D + 3 + + 8 + 1 + quarter + down + + +
+ 5 +
+
+ 3 +
+
+ + + D + 2 + + 2 + 1 + 16th + up + begin + begin + + + + D + 3 + + 2 + 1 + 16th + up + continue + continue + + + + C + 3 + + 2 + 1 + 16th + up + continue + continue + + + + D + 3 + + 2 + 1 + 16th + up + end + end + +
+ + +
+ 5 +
+
+ 3 +
+
+ + + B + 2 + + 2 + 1 + 16th + down + begin + begin + + + + B + 3 + + 2 + 1 + 16th + down + continue + continue + + + + A + 3 + + 2 + 1 + 16th + down + continue + continue + + + + B + 3 + + 2 + 1 + 16th + down + end + end + + +
+ 6 +
+
+ 5 +
+
+ + + G + 1 + 3 + + 2 + 1 + 16th + sharp + down + begin + begin + + + + B + 3 + + 2 + 1 + 16th + down + continue + continue + + + + E + 3 + + 2 + 1 + 16th + down + continue + continue + + + + G + 1 + 3 + + 2 + 1 + 16th + down + end + end + + +
+ 9 +
+
+ + + A + 2 + + 2 + 1 + 16th + down + begin + begin + + + + A + 3 + + 2 + 1 + 16th + down + continue + continue + + +
+ 4 +
+
+ 2 +
+
+ + + G + 3 + + 2 + 1 + 16th + natural + down + continue + continue + + + + A + 3 + + 2 + 1 + 16th + down + end + end + + +
+ 6 +
+
+ 5 +
+
+ + + F + 1 + 3 + + 2 + 1 + 16th + down + begin + begin + + + + A + 3 + + 2 + 1 + 16th + down + continue + continue + + + + D + 3 + + 2 + 1 + 16th + down + continue + continue + + + + F + 1 + 3 + + 2 + 1 + 16th + down + end + end + +
+ + +
+ 9 +
+
+ + + G + 2 + + 2 + 1 + 16th + up + begin + begin + + + + A + 2 + + 2 + 1 + 16th + up + continue + continue + + +
+ 6 +
+
+ + + B + 2 + + 2 + 1 + 16th + up + continue + continue + + + + C + 3 + + 2 + 1 + 16th + up + end + end + + +
+ 5 +
+
+ 4 +
+
+ + + D + 3 + + 4 + 1 + eighth + up + begin + + +
+ 6 +
+
+ 5 +
+ 2 +
+ +
+ 7 +
+ 2 +
+ + + D + 2 + + 4 + 1 + eighth + up + end + + +
+ 3 +
+
+ 8 +
+
+ 5 +
+ 4 +
+ +
+ 2 +
+
+ 7 +
+
+ 4 +
+ 4 +
+ +
+ 3 +
+
+ 8 +
+
+ 5 +
+ 4 +
+ +
+ 4 +
+
+ 2 +
+ 4 +
+ + + G + 2 + + 16 + 1 + half + up + + + light-heavy + +
+
+
diff --git a/music21/musicxml/lilypondTestSuite/piece03-advanced-figures.musicxml b/music21/musicxml/lilypondTestSuite/piece03-advanced-figures.musicxml new file mode 100644 index 0000000000..d7a6543040 --- /dev/null +++ b/music21/musicxml/lilypondTestSuite/piece03-advanced-figures.musicxml @@ -0,0 +1,425 @@ + + + + + + MuseScore 3.6.2 + 2023-06-14 + + + + + + + + + + 6.4 + 40 + + + 1856.25 + 1312.5 + + 93.75 + 93.75 + 93.75 + 93.75 + + + 93.75 + 93.75 + 93.75 + 93.75 + + + + + + + + Basso continuo + B.c. + + + + + + 2 + 1 + 78.7402 + 0 + + + + + + + + + 50.00 + 0.00 + + 70.00 + + + + 4 + + 1 + + + + F + 4 + + + +
+ 3 +
+
+ 8 +
+ 2 +
+ +
+ flat + 4 +
+
+ sharp + 2 +
+ 2 +
+ + + G + 2 + + 4 + 1 + quarter + up + + +
+ flat-flat + 5 +
+
+ double-sharp + 3 +
+ 2 +
+ +
+ 6 +
+ 1 +
+ +
+ 5 +
+ 1 +
+ + + G + 3 + + 4 + + 1 + quarter + down + + + + + +
+ 6 +
+
+ 4 +
+
+ 2 +
+
+ + + G + 3 + + 2 + + 1 + eighth + down + begin + + + + + +
+ 7 + flat +
+
+ + + F + 1 + 3 + + 1 + 1 + 16th + down + continue + begin + + +
+ 6 + backslash +
+
+ + + E + 3 + + 1 + 1 + 16th + down + end + end + + +
+ 24 +
+
+ double-sharp + 5 + +
+
+ + + F + 1 + 3 + + 2 + 1 + eighth + down + begin + + +
+ +
+
+ +
+
+ + + D + 3 + + 2 + 1 + eighth + down + end + +
+ + +
+ 9 + backslash +
+
+ + + G + 3 + + 2 + 1 + eighth + up + begin + + +
+ 6 +
+
+ 4 + cross +
+ 1 +
+ +
+ +
+
+ flat +
+ 1 +
+ + + D + 3 + + 2 + 1 + eighth + up + continue + + +
+ 6 +
+
+ + + B + 2 + + 2 + 1 + eighth + up + continue + + + + G + 2 + + 2 + 1 + eighth + up + end + + +
+ 6 +
+
+ 4 +
+ 2 +
+ +
+ 5 +
+
+ 3 +
+ 1 +
+ +
+ 4 +
+
+ 2 +
+ 1 +
+ + + D + 3 + + 4 + 1 + quarter + down + + +
+ 5 +
+
+ 3 +
+
+ + + D + 2 + + 1 + 1 + 16th + up + begin + begin + + + + D + 3 + + 1 + 1 + 16th + up + continue + continue + + + + C + 3 + + 1 + 1 + 16th + up + continue + continue + + + + D + 3 + + 1 + 1 + 16th + up + end + end + + + light-heavy + +
+
+
From 0d91795ff8ecde653750c743a1c34ed9136ad1be Mon Sep 17 00:00:00 2001 From: Moritz Heffter Date: Sat, 17 Jun 2023 20:37:32 +0200 Subject: [PATCH 54/58] mei import of figured bass goes to the measure; added exmaples for mei figured bass --- music21/figuredBass/notation.py | 3 +- music21/harmony.py | 25 +- music21/mei/base.py | 29 +- .../mei/test/piece01-bwv-1023-1-beginning.mei | 454 ++++++++++++++++++ .../mei/test/piece02-bwv-1021-1-beginning.mei | 454 ++++++++++++++++++ .../piece01-bwv-1023-1-beginning.musicxml | 110 ++--- .../piece02-bwv-1021-1-beginning.musicxml | 321 ++++++------- music21/musicxml/m21ToXml.py | 2 +- music21/musicxml/test_xmlToM21.py | 13 + music21/musicxml/xmlToM21.py | 13 +- 10 files changed, 1163 insertions(+), 261 deletions(-) create mode 100644 music21/mei/test/piece01-bwv-1023-1-beginning.mei create mode 100644 music21/mei/test/piece02-bwv-1021-1-beginning.mei diff --git a/music21/figuredBass/notation.py b/music21/figuredBass/notation.py index 778b0baa28..b09c28bf86 100644 --- a/music21/figuredBass/notation.py +++ b/music21/figuredBass/notation.py @@ -39,7 +39,8 @@ 'double-sharp': '##', 'flat-flat': 'bb', 'backslash': '\\', - 'slash': '/'} + 'slash': '/', + 'cross': '+'} modifiersDictM21ToXml = {'#': 'sharp', 'b': 'flat', diff --git a/music21/harmony.py b/music21/harmony.py index 998f68818a..d42ce41594 100644 --- a/music21/harmony.py +++ b/music21/harmony.py @@ -2501,8 +2501,28 @@ def transpose(self: NCT, _value, *, inPlace=False) -> NCT | None: # ------------------------------------------------------------------------------ class FiguredBassIndication(Harmony): + ''' + The FiguredBassIndication objects store information about thorough bass figures. + + It is created as a representation for tags in MEI and tags in MusicXML. + + The FiguredBassIndication object derives from the Harmony object and can be used + in the following way: + + >>> fbi = harmony.FiguredBassIndication('#,6#') + >>> fbi + + + The single figures are stored as figuredBass.notation.Figure objects: + >>> fbi.fig_notation.figures + [ hasExt: False>, + hasExt: False>] + ''' + isFigure: bool = True - def __init__(self, figs: str | list | None = None, extenders: list[bool] | None = None , **keywords): + part: str | None = None + def __init__(self, figs: str | list | None = None, extenders: list[bool] | None = None , + part: str | None=None, **keywords): super().__init__(**keywords) if figs: if isinstance(figs, list): @@ -2515,6 +2535,7 @@ def __init__(self, figs: str | list | None = None, extenders: list[bool] | None else: _figs = '' self._fig_notation = notation.Notation(_figs, extenders) + self.part = part @property def fig_notation(self) -> notation.Notation: @@ -2525,7 +2546,7 @@ def fig_notation(self, figs, extenders=None): self._fig_notation = notation.Notation(figs, extenders) def __repr__(self): - return f'<{self.__class__.__name__} figures: {self.fig_notation.notationColumn}>' + return f'<{self.__class__.__name__} figures: {self.fig_notation.notationColumn} part: {self.part}>' def realizeChordSymbolDurations(piece): ''' diff --git a/music21/mei/base.py b/music21/mei/base.py index 4586051583..a8aa4f07e0 100644 --- a/music21/mei/base.py +++ b/music21/mei/base.py @@ -2527,13 +2527,14 @@ def harmFromElement(elem, slurBundle=None) -> tuple: fb_harmony_tag: tuple = () - # Collect all elements in a measure and go throug extenders + # Collect all elements in a measure and go through extenders # tstamp has to be used as a duration marker between two elements for subElement in _processEmbeddedElements(elem.findall('*'), tagToFunction, elem.tag, slurBundle): subElement.tstamp = float(elem.get('tstamp')) subElement.offset = subElement.tstamp - 1 + subElement.part = elem.get('staff') fb_harmony_tag = (subElement.tstamp - 1, subElement) return fb_harmony_tag @@ -2581,7 +2582,8 @@ def figuredbassFromElement(elem, slurBundle=None) -> harmony.FiguredBassIndicati # Generate a FiguredBassIndication object and set the collected information fb_notation = ','.join(fb_notation_list) - theFbNotation = harmony.FiguredBassIndication(fb_notation, extenders=fb_extenders) + theFbNotation = harmony.FiguredBassIndication(fb_notation, extenders=fb_extenders, + part=elem.get('n')) theFbNotation.id = fb_id theFbNotation.duration = duration.Duration(quarterLength=dauer) @@ -3234,9 +3236,16 @@ def measureFromElement(elem, backupNum, expectedNs, slurBundle=None, activeMeter # track the bar's duration maxBarDuration = None + print('=== EL', elem, '===') + # iterate all immediate children for eachElem in elem.iterfind('*'): - if staffTag == eachElem.tag: + # first get all information stored in tags. + # They are stored on the same level as . + if harmTag == eachElem.tag: + harmElements['fb'].append(harmFromElement(eachElem)) + #print(' harms', harmElements, eachElem.get('n')) + elif staffTag == eachElem.tag: staves[eachElem.get('n')] = stream.Measure(staffFromElement(eachElem, slurBundle=slurBundle), number=int(elem.get('n', backupNum))) @@ -3249,9 +3258,6 @@ def measureFromElement(elem, backupNum, expectedNs, slurBundle=None, activeMeter environLocal.warn(_UNIMPLEMENTED_IMPORT.format('', '@n')) else: stavesWaiting[whichN] = staffDefFromElement(eachElem, slurBundle) - elif harmTag == eachElem.tag: - # get all information stored in tags - harmElements['fb'].append(harmFromElement(eachElem)) elif eachElem.tag not in _IGNORE_UNPROCESSED: environLocal.printDebug(_UNPROCESSED_SUBELEMENT.format(eachElem.tag, elem.tag)) @@ -3266,7 +3272,15 @@ def measureFromElement(elem, backupNum, expectedNs, slurBundle=None, activeMeter staves[whichN].insert(0, eachObj) # Add objects to the staves dict - staves['fb'] = harmElements + #staves['fb'] = harmElements + + # Add objects to the corrresponding staff within the Measure + for fb in harmElements['fb']: + offset = fb[0] + fbi = fb[1] + m = staves.get(fbi.part) + m.insert(offset, fbi) + # other childs of tags can be added here… # create rest-filled measures for expected parts that had no tag in this @@ -3601,6 +3615,7 @@ def scoreFromElement(elem, slurBundle): # document. Iterating the keys in "parsed" would not preserve the order. environLocal.printDebug('*** making the Score') + # TODO: Replace this with a better solution. # Extract collected information stored in the dict unter the 'fb' key harms: list[dict] | None = None if 'fb' in parsed.keys(): diff --git a/music21/mei/test/piece01-bwv-1023-1-beginning.mei b/music21/mei/test/piece01-bwv-1023-1-beginning.mei new file mode 100644 index 0000000000..9cd40c49e2 --- /dev/null +++ b/music21/mei/test/piece01-bwv-1023-1-beginning.mei @@ -0,0 +1,454 @@ + + + + + + + + + <respStmt /> + </titleStmt> + <pubStmt><date isodate="2023-06-17" type="encoding-date">2023-06-17</date> + </pubStmt> + </fileDesc> + <encodingDesc xml:id="encodingdesc-xfqkst"> + <appInfo xml:id="appinfo-1orxspt"> + <application xml:id="application-msu6g4" isodate="2023-06-17T16:54:16" version="3.15.0-5abc7c0"> + <name xml:id="name-1huf1pd">Verovio</name> + <p xml:id="p-q8u2x7">Transcoded from MusicXML</p> + </application> + </appInfo> + </encodingDesc> + </meiHead> + <music> + <body> + <mdiv xml:id="m19j97br"> + <score xml:id="s1hlumks"> + <scoreDef xml:id="syuwo6"> + <staffGrp xml:id="soc4nsq"> + <staffDef xml:id="P1" n="1" lines="5" ppq="24"> + <label xml:id="l6o5d52">Klavier</label> + <instrDef xml:id="i13dxe08" midi.channel="0" midi.instrnum="0" midi.volume="78.00%" /> + <clef xml:id="c1la4s3k" shape="G" line="2" /> + <keySig xml:id="k14ck05q" sig="1s" /> + <meterSig xml:id="mt9k8qw" count="3" unit="4" /> + </staffDef> + <staffDef xml:id="P2" n="2" lines="5" ppq="24"> + <label xml:id="l521k4k">Klavier</label> + <instrDef xml:id="isa5xkn" midi.channel="1" midi.instrnum="0" midi.volume="78.00%" /> + <clef xml:id="c1xtvh1l" shape="F" line="4" /> + <keySig xml:id="k6p4k8o" sig="1s" /> + <meterSig xml:id="mn1h5nq" count="3" unit="4" /> + </staffDef> + </staffGrp> + </scoreDef> + <section xml:id="sxmtnri"> + <measure xml:id="m68onu7" n="1"> + <staff xml:id="sctk2va" n="1"> + <layer xml:id="l1f7y2ha" n="1"> + <note xml:id="n2e83pg" dur.ppq="24" dur="4" oct="4" pname="e" stem.dir="up" /> + <rest xml:id="r1kshh4l" dur.ppq="24" dur="4" /> + <beam xml:id="bxssvrq"> + <note xml:id="n1vvq3ph" dots="1" dur.ppq="18" dur="8" oct="5" pname="e" stem.dir="down" /> + <note xml:id="n4w592w" dur.ppq="3" dur="32" oct="5" pname="f" stem.dir="down" accid.ges="s" /> + <note xml:id="n1qbihg2" dur.ppq="3" dur="32" oct="5" pname="g" stem.dir="down" /> + </beam> + </layer> + </staff> + <staff xml:id="s1lpopmn" n="2"> + <layer xml:id="l17rzmxg" n="1"> + <note xml:id="n1q7g3ky" dur.ppq="24" dur="4" oct="2" pname="e" stem.dir="up" /> + <note xml:id="nipakd2" dur.ppq="48" dur="2" oct="3" pname="e" stem.dir="down" /> + </layer> + </staff> + <fermata xml:id="fwmqlyh" staff="1" startid="#n2e83pg" form="norm" place="above" /> + <slur xml:id="s183tqe9" startid="#n1vvq3ph" endid="#n1qbihg2" curvedir="above" /> + <fermata xml:id="f1ugzrbb" staff="2" startid="#n1q7g3ky" form="norm" place="above" /> + <tie xml:id="t1x5rufn" startid="#nipakd2" endid="#n1ao29qd" /> + </measure> + <measure xml:id="mytbz7a" n="2"> + <staff xml:id="shp4cva" n="1"> + <layer xml:id="l1e9nlaf" n="1"> + <beam xml:id="bd3nwaa"> + <tuplet xml:id="to9u0c6" num="3" numbase="2" bracket.visible="false"> + <note xml:id="nm68yov" dur.ppq="8" dur="8" oct="5" pname="f" stem.dir="down" accid.ges="s" /> + <note xml:id="nutwci1" dur.ppq="8" dur="8" oct="5" pname="d" stem.dir="down" accid="s" /> + <note xml:id="nrvwpas" dur.ppq="8" dur="8" oct="5" pname="e" stem.dir="down" /> + </tuplet> + </beam> + <note xml:id="n1d6l5xv" dur.ppq="24" dur="4" oct="5" pname="c" stem.dir="down" /> + <beam xml:id="b15bd2wb"> + <note xml:id="nxazi3f" dots="1" dur.ppq="18" dur="8" oct="4" pname="b" stem.dir="up" /> + <note xml:id="n10h9r8o" dur.ppq="6" dur="16" oct="4" pname="a" stem.dir="up" /> + </beam> + </layer> + </staff> + <staff xml:id="si25zx9" n="2"> + <layer xml:id="l1spr0ea" n="1"> + <note xml:id="n1ao29qd" dur.ppq="24" dur="4" oct="3" pname="e" stem.dir="down" /> + <note xml:id="n5j60be" dur.ppq="48" dur="2" oct="3" pname="d" stem.dir="down" accid="s" /> + </layer> + </staff> + <slur xml:id="s19ddjbr" startid="#nm68yov" endid="#nrvwpas" curvedir="above" /> + <slur xml:id="s1gmmdi3" startid="#n1d6l5xv" endid="#nxazi3f" curvedir="above" /> + <trill xml:id="t1dcb5d1" staff="1" startid="#nxazi3f" /> + <harm xml:id="h1oap480" staff="2" tstamp="1.000000"> + <fb xml:id="f4hsfxn"> + <f xml:id="f1iskldj">6</f> + <f xml:id="fekbja7">4</f> + <f xml:id="f1fq5brv">2</f> + </fb> + </harm> + <harm xml:id="hl6szkk" staff="2" tstamp="2.000000"> + <fb xml:id="f1mgchbu"> + <f xml:id="fn02fe5">7</f> + <f xml:id="f1bdxnbd">5</f> + </fb> + </harm> + <harm xml:id="h1uw45cr" staff="2" tstamp="3.000000"> + <fb xml:id="f1tjf5hl"> + <f xml:id="f1p10jpe">6</f> + </fb> + </harm> + </measure> + <measure xml:id="m18yomk9" n="3"> + <staff xml:id="s1ucchml" n="1"> + <layer xml:id="ljbz9vi" n="1"> + <beam xml:id="b18sg0tt"> + <note xml:id="n1wd6zu7" dots="1" dur.ppq="18" dur="8" oct="4" pname="g" stem.dir="up" /> + <note xml:id="n9anp71" dur.ppq="3" dur="32" oct="4" pname="e" stem.dir="up" /> + <note xml:id="nhd52aa" dur.ppq="3" dur="32" oct="4" pname="f" stem.dir="up" accid.ges="s" /> + </beam> + <beam xml:id="bkjnq9t"> + <note xml:id="n1b4jity" dots="1" dur.ppq="18" dur="8" oct="4" pname="g" stem.dir="up" /> + <note xml:id="n10lzkff" dur.ppq="3" dur="32" oct="4" pname="a" stem.dir="up" /> + <note xml:id="neaeh0p" dur.ppq="3" dur="32" oct="4" pname="b" stem.dir="up" /> + </beam> + <beam xml:id="bw7taia"> + <note xml:id="nm7kn4k" dots="1" dur.ppq="18" dur="8" oct="5" pname="c" stem.dir="down" /> + <note xml:id="n1uvq0wi" dur.ppq="3" dur="32" oct="5" pname="d" stem.dir="down" accid="n" /> + <note xml:id="n1dqt7lp" dur.ppq="3" dur="32" oct="5" pname="e" stem.dir="down" /> + </beam> + </layer> + </staff> + <staff xml:id="s16cmbqy" n="2"> + <layer xml:id="lcdt03l" n="1"> + <note xml:id="n1dhjley" dur.ppq="24" dur="4" oct="3" pname="e" stem.dir="down" /> + <note xml:id="n1u3z623" dur.ppq="24" dur="4" oct="2" pname="e" stem.dir="up" /> + <note xml:id="n50b43x" dur.ppq="24" dur="4" oct="3" pname="e" stem.dir="down" /> + </layer> + </staff> + <slur xml:id="s1baaes0" startid="#n9anp71" endid="#n1b4jity" curvedir="below" /> + <slur xml:id="s7hvrp9" startid="#n10lzkff" endid="#nm7kn4k" curvedir="above" /> + <harm xml:id="h4jflu4" staff="2" tstamp="1.000000"> + <fb xml:id="f1cgm4m6"> + <f xml:id="f12vdmkn">9</f> + </fb> + </harm> + <harm xml:id="hssx5e7" staff="2" tstamp="2.000000"> + <fb xml:id="fdfe3n6"> + <f xml:id="f1xzlum8">8</f> + </fb> + </harm> + <harm xml:id="h1ne6q0q" staff="2" tstamp="2.500000"> + <fb xml:id="f15ch7r0"> + <f xml:id="f1cww1hs">7</f> + </fb> + </harm> + <harm xml:id="hcdcxxi" staff="2" tstamp="3.000000"> + <fb xml:id="fih0gzd"> + <f xml:id="f1skvmxe">6</f> + </fb> + </harm> + </measure> + <measure xml:id="m12v79wm" n="4"> + <staff xml:id="ss3muv4" n="1"> + <layer xml:id="le5ixgq" n="1"> + <beam xml:id="b230iwy"> + <note xml:id="n1fmzv4a" dots="1" dur.ppq="18" dur="8" oct="4" pname="d" stem.dir="up" /> + <note xml:id="nu9eovj" dur.ppq="6" dur="16" oct="5" pname="f" stem.dir="up" accid.ges="s" /> + </beam> + <beam xml:id="b5nbwfg"> + <note xml:id="n1mhj9wy" dots="1" dur.ppq="18" dur="8" oct="5" pname="a" stem.dir="down" /> + <note xml:id="nq1et92" dur.ppq="3" dur="32" oct="5" pname="g" stem.dir="down" /> + <note xml:id="n1liwdxf" dur.ppq="3" dur="32" oct="5" pname="f" stem.dir="down" accid.ges="s" /> + </beam> + <note xml:id="nmq4k7g" dur.ppq="0" dur="16" oct="5" pname="f" grace="acc" stem.dir="up" accid.ges="s" /> + <beam xml:id="b10r45h3"> + <note xml:id="n1w4wmxw" dots="1" dur.ppq="18" dur="8" oct="5" pname="e" stem.dir="down" /> + <note xml:id="n1oxes9o" dur.ppq="3" dur="32" oct="5" pname="d" stem.dir="down" accid="s" /> + <note xml:id="npd0w7m" dur.ppq="3" dur="32" oct="5" pname="e" stem.dir="down" /> + </beam> + </layer> + </staff> + <staff xml:id="sagy2fm" n="2"> + <layer xml:id="lut8pz2" n="1"> + <note xml:id="n1tmcewz" dur.ppq="24" dur="4" oct="3" pname="d" stem.dir="down" /> + <note xml:id="n15f4hue" dur.ppq="48" dur="2" oct="3" pname="c" stem.dir="up" /> + </layer> + </staff> + <slur xml:id="s1tsva7y" startid="#nmq4k7g" endid="#n1w4wmxw" curvedir="below" /> + <slur xml:id="s14qkobu" startid="#n1w4wmxw" endid="#npd0w7m" curvedir="above" /> + <trill xml:id="tfhi5kq" staff="1" startid="#n1w4wmxw" /> + <harm xml:id="h1sph74m" staff="2" tstamp="1.000000"> + <fb xml:id="fmc6c7g"> + <f xml:id="f1qkw9x6">7</f> + </fb> + </harm> + <harm xml:id="h5l130c" staff="2" tstamp="1.500000"> + <fb xml:id="fzec6km"> + <f xml:id="f1jrrxzh">6</f> + </fb> + </harm> + <harm xml:id="hs413vo" staff="2" tstamp="2.000000"> + <fb xml:id="f14fjzx"> + <f xml:id="f58zsu7">6</f> + </fb> + </harm> + </measure> + <measure xml:id="m2b16w2" n="5"> + <staff xml:id="s1eo6gx7" n="1"> + <layer xml:id="ld3tbg7" n="1"> + <note xml:id="n10co5ew" dur.ppq="24" dur="4" oct="5" pname="d" stem.dir="down" accid="s" /> + <beam xml:id="ba8vkcx"> + <note xml:id="n1mzjr02" dur.ppq="12" dur="8" oct="5" pname="c" stem.dir="down" accid="s" /> + <note xml:id="n1jihpm" dur.ppq="12" dur="8" oct="4" pname="b" stem.dir="down" /> + </beam> + <note xml:id="npewiwz" dur.ppq="24" dur="4" oct="5" pname="b" stem.dir="down" /> + </layer> + </staff> + <staff xml:id="sysj1j" n="2"> + <layer xml:id="l14hf6oj" n="1"> + <note xml:id="n1uf1h57" dur.ppq="24" dur="4" oct="2" pname="b" stem.dir="up" /> + <beam xml:id="b3rl7kp"> + <note xml:id="nwshlpy" dur.ppq="12" dur="8" oct="3" pname="b" stem.dir="down"> + <artic xml:id="a54fic8" artic="stacciss" /> + </note> + <note xml:id="n15iks1l" dur.ppq="12" dur="8" oct="3" pname="a" stem.dir="down" /> + </beam> + <note xml:id="ny4fr8u" dur.ppq="24" dur="4" oct="3" pname="g" stem.dir="down" accid="s" /> + </layer> + </staff> + <slur xml:id="swzv20g" startid="#n1jihpm" endid="#n1ptr6li" curvedir="above" /> + <tie xml:id="t1j3yxng" startid="#npewiwz" endid="#n15v7qah" /> + <harm xml:id="h1gr38lw" staff="2" tstamp="1.000000"> + <fb xml:id="f9sz65l"> + <f xml:id="fjhqqel">♯</f> + </fb> + </harm> + <harm xml:id="hxni7mx" staff="2" tstamp="2.000000"> + <fb xml:id="f1h9do7n"> + <f xml:id="f1m51tuw">♯</f> + </fb> + </harm> + <harm xml:id="h57cuag" staff="2" tstamp="3.000000"> + <fb xml:id="f3zh618"> + <f xml:id="f93xdnx">6</f> + <f xml:id="f98iw8l">5♮</f> + </fb> + </harm> + </measure> + <measure xml:id="m5a1yr7" n="6"> + <staff xml:id="s1njpbne" n="1"> + <layer xml:id="l189ebpx" n="1"> + <beam xml:id="bow3qfr"> + <tuplet xml:id="thcwcgx" num="3" numbase="2" bracket.visible="false"> + <note xml:id="n15v7qah" dur.ppq="8" dur="8" oct="5" pname="b" stem.dir="down" /> + <note xml:id="nb5rbiz" dur.ppq="8" dur="8" oct="5" pname="g" stem.dir="down" accid="s" /> + <note xml:id="n1ptr6li" dur.ppq="8" dur="8" oct="5" pname="e" stem.dir="down" /> + </tuplet> + </beam> + <beam xml:id="b19af39g"> + <note xml:id="n1y41fy0" dur.ppq="6" dur="16" oct="5" pname="d" stem.dir="down" accid="n" /> + <note xml:id="n280tol" breaksec="1" dur.ppq="6" dur="16" oct="4" pname="b" stem.dir="down" /> + <note xml:id="nimpui7" dur.ppq="12" dur="8" oct="5" pname="c" stem.dir="down" accid="n" /> + </beam> + <note xml:id="nes6p8f" dur.ppq="24" dur="4" oct="5" pname="a" stem.dir="down" /> + </layer> + </staff> + <staff xml:id="s1j3aabx" n="2"> + <layer xml:id="l16nf4yd" n="1"> + <note xml:id="n1puhysz" dur.ppq="48" dur="2" oct="3" pname="a" stem.dir="down" /> + <note xml:id="nm9wuha" dur.ppq="24" dur="4" oct="3" pname="f" stem.dir="down" accid.ges="s" /> + </layer> + </staff> + <slur xml:id="skblb6f" startid="#n15v7qah" endid="#n1ptr6li" curvedir="above" /> + <slur xml:id="s13f1mfc" startid="#n1y41fy0" endid="#nimpui7" curvedir="above" /> + <tie xml:id="tfnc5ez" startid="#nes6p8f" endid="#nytlraa" /> + <harm xml:id="hn4cjij" staff="2" tstamp="1.000000"> + <fb xml:id="f89sg2o"> + <f xml:id="f80fisp">9</f> + <f xml:id="f181zh9c">4</f> + </fb> + </harm> + <harm xml:id="hnhlsu9" staff="2" tstamp="2.000000"> + <fb xml:id="fmpifnn"> + <f xml:id="fsmbhcz">8</f> + <f xml:id="f14fud37">3</f> + </fb> + </harm> + <harm xml:id="hopblct" staff="2" tstamp="3.000000"> + <fb xml:id="fwptjd3"> + <f xml:id="f1bec1la">6⃥</f> + <f xml:id="f1eihg5">5</f> + </fb> + </harm> + <harm xml:id="hjrwq1t" staff="2" tstamp="3.500000"> + <fb xml:id="f1xglkoc"> + <f xml:id="f111wgxf">7</f> + </fb> + </harm> + </measure> + <measure xml:id="m167wcku" n="7"> + <staff xml:id="s1mktj4y" n="1"> + <layer xml:id="lbn5coc" n="1"> + <beam xml:id="b1nb3ib0"> + <tuplet xml:id="t3n5otg" num="3" numbase="2" bracket.visible="false"> + <note xml:id="nytlraa" dur.ppq="8" dur="8" oct="5" pname="a" stem.dir="down" /> + <note xml:id="nuj93hk" dur.ppq="8" dur="8" oct="5" pname="f" stem.dir="down" accid.ges="s" /> + <note xml:id="n1384whq" dur.ppq="8" dur="8" oct="5" pname="d" stem.dir="down" accid="s" /> + </tuplet> + </beam> + <beam xml:id="b1a4zcc5"> + <tuplet xml:id="t8ij1f0" num="3" numbase="2" bracket.visible="false"> + <note xml:id="n1hj2x62" dur.ppq="8" dur="8" oct="5" pname="c" stem.dir="down" accid="n" /> + <note xml:id="n1k2xfk0" dur.ppq="8" dur="8" oct="4" pname="a" stem.dir="down" /> + <note xml:id="nhzp46z" dur.ppq="8" dur="8" oct="4" pname="b" stem.dir="down" /> + </tuplet> + </beam> + <note xml:id="n1typxlz" dur.ppq="24" dur="4" oct="5" pname="g" stem.dir="down" accid="n" /> + </layer> + </staff> + <staff xml:id="swsrhfm" n="2"> + <layer xml:id="l1w3xfo9" n="1"> + <note xml:id="nu9z6xj" dur.ppq="48" dur="2" oct="3" pname="d" stem.dir="down" accid="s" /> + <note xml:id="n1xo0su6" dur.ppq="24" dur="4" oct="3" pname="e" stem.dir="down" /> + </layer> + </staff> + <slur xml:id="sdtfpun" startid="#nytlraa" endid="#n1384whq" curvedir="above" /> + <slur xml:id="sycmts8" startid="#nuj93hk" endid="#n1hj2x62" curvedir="above" /> + <slur xml:id="st7i04l" startid="#n1hj2x62" endid="#nhzp46z" curvedir="above" /> + <tie xml:id="t133wlr0" startid="#n1typxlz" endid="#nihjhcp" /> + <harm xml:id="h1y5jiny" staff="2" tstamp="1.000000"> + <fb xml:id="f1f4o4hk"> + <f xml:id="f1r2pli3">7</f> + <f xml:id="fcbydoo">5</f> + </fb> + </harm> + <harm xml:id="h17dg6rh" staff="2" tstamp="2.000000"> + <fb xml:id="fkidyv0"> + <f xml:id="fv3hgic">6</f> + </fb> + </harm> + </measure> + <measure xml:id="mu3rfp6" n="8"> + <staff xml:id="s1r61ezi" n="1"> + <layer xml:id="l7631sc" n="1"> + <beam xml:id="b1vmrrh"> + <tuplet xml:id="t13k37wv" num="3" numbase="2" bracket.visible="false"> + <note xml:id="nihjhcp" dur.ppq="8" dur="8" oct="5" pname="g" stem.dir="down" /> + <note xml:id="nsgp043" dur.ppq="8" dur="8" oct="5" pname="f" stem.dir="down" accid.ges="s" /> + <note xml:id="n1qwg02l" dur.ppq="8" dur="8" oct="6" pname="c" stem.dir="down" /> + </tuplet> + </beam> + <beam xml:id="bmev5ra"> + <tuplet xml:id="tel7emp" num="3" numbase="2" bracket.visible="false"> + <note xml:id="n114narx" dur.ppq="8" dur="8" oct="5" pname="b" stem.dir="down" /> + <note xml:id="n4lhzli" dur.ppq="8" dur="8" oct="5" pname="d" stem.dir="down" accid="s" /> + <note xml:id="npdzhj9" dur.ppq="8" dur="8" oct="5" pname="e" stem.dir="down" /> + </tuplet> + </beam> + <beam xml:id="b39o0ey"> + <tuplet xml:id="t16bu0us" num="3" numbase="2" bracket.visible="false"> + <note xml:id="n1kb9xic" dur.ppq="8" dur="8" oct="5" pname="a" stem.dir="down" /> + <note xml:id="n1pl1p9" dur.ppq="8" dur="8" oct="5" pname="g" stem.dir="down" /> + <note xml:id="n1i3bvrf" dur.ppq="8" dur="8" oct="5" pname="f" stem.dir="down" accid.ges="s" /> + </tuplet> + </beam> + </layer> + </staff> + <staff xml:id="s1e8qrn2" n="2"> + <layer xml:id="l1t4gueq" n="1"> + <note xml:id="n1qtxw1s" dur.ppq="24" dur="4" oct="2" pname="a" stem.dir="up" /> + <beam xml:id="b1gcuwvr"> + <note xml:id="nf3q0cy" dur.ppq="12" dur="8" oct="2" pname="g" stem.dir="up" /> + <note xml:id="n2crfuo" dur.ppq="12" dur="8" oct="2" pname="a" stem.dir="up" /> + </beam> + <note xml:id="n1c806y9" dur.ppq="24" dur="4" oct="2" pname="b" stem.dir="up" /> + </layer> + </staff> + <slur xml:id="sriy1fh" startid="#nihjhcp" endid="#n1qwg02l" curvedir="above" /> + <slur xml:id="s1rcphq4" startid="#nsgp043" endid="#n114narx" curvedir="below" /> + <slur xml:id="s188w2s8" startid="#n114narx" endid="#npdzhj9" curvedir="above" /> + <slur xml:id="sil3tui" startid="#n1kb9xic" endid="#n1i3bvrf" curvedir="above" /> + <harm xml:id="h12vyvgm" staff="2" tstamp="1.000000"> + <fb xml:id="f7dm9xt"> + <f xml:id="f160g5nv">7</f> + <f xml:id="fp8jb7n">5</f> + </fb> + </harm> + <harm xml:id="h16vb0tb" staff="2" tstamp="1.500000"> + <fb xml:id="f2wb3lh"> + <f xml:id="f1pepvqe">6</f> + <f xml:id="f1r0mp02">4+</f> + </fb> + </harm> + <harm xml:id="hy6f871" staff="2" tstamp="2.000000"> + <fb xml:id="f1j1zxeo"> + <f xml:id="f17s3owa">6</f> + </fb> + </harm> + <harm xml:id="h1opxddc" staff="2" tstamp="2.500000"> + <fb xml:id="f10a5p0i"> + <f xml:id="f1xme5en">7</f> + <f xml:id="fbf3l62">5</f> + </fb> + </harm> + <harm xml:id="hfc8gz3" staff="2" tstamp="3.000000"> + <fb xml:id="febloey"> + <f xml:id="f11ofmph">8</f> + <f xml:id="f1gp1zia">♯</f> + </fb> + </harm> + <harm xml:id="hr7wtwd" staff="2" tstamp="3.500000"> + <fb xml:id="fzrhwp"> + <f xml:id="f13zf8ew">7</f> + </fb> + </harm> + </measure> + <measure xml:id="m15hz713" right="end" n="9"> + <staff xml:id="sj657s1" n="1"> + <layer xml:id="l6n00g9" n="1"> + <beam xml:id="bsubasg"> + <note xml:id="nl5patx" dots="1" dur.ppq="18" dur="8" oct="5" pname="e" stem.dir="down" /> + <note xml:id="nyyjgkl" dur.ppq="3" dur="32" oct="5" pname="g" stem.dir="down" /> + <note xml:id="n1e1wai7" dur.ppq="3" dur="32" oct="5" pname="f" stem.dir="down" accid.ges="s" /> + </beam> + <note xml:id="neipp5d" dur.ppq="48" dur="2" oct="5" pname="g" stem.dir="down" /> + </layer> + </staff> + <staff xml:id="st45ecm" n="2"> + <layer xml:id="lihiz5q" n="1"> + <note xml:id="n1eb6tl6" dur.ppq="24" dur="4" oct="2" pname="e" stem.dir="up" /> + <rest xml:id="rjn9v1j" dur.ppq="24" dur="4" /> + <note xml:id="n1h0q4es" dur.ppq="24" dur="4" oct="3" pname="e" stem.dir="down" /> + </layer> + </staff> + <slur xml:id="s10v4fx" startid="#nl5patx" endid="#n1e1wai7" curvedir="above" /> + <harm xml:id="h4i3eaa" staff="2" tstamp="3.000000"> + <fb xml:id="f1ob6utv"> + <f xml:id="f17t2xur">8</f> + </fb> + </harm> + <harm xml:id="hocercw" staff="2" tstamp="3.500000"> + <fb xml:id="f3gyhdu"> + <f xml:id="f1x39h28">7</f> + </fb> + </harm> + </measure> + </section> + </score> + </mdiv> + </body> + </music> +</mei> diff --git a/music21/mei/test/piece02-bwv-1021-1-beginning.mei b/music21/mei/test/piece02-bwv-1021-1-beginning.mei new file mode 100644 index 0000000000..411159126f --- /dev/null +++ b/music21/mei/test/piece02-bwv-1021-1-beginning.mei @@ -0,0 +1,454 @@ +<?xml version="1.0" encoding="UTF-8"?> +<?xml-model href="https://music-encoding.org/schema/dev/mei-all.rng" type="application/xml" schematypens="http://relaxng.org/ns/structure/1.0"?> +<?xml-model href="https://music-encoding.org/schema/dev/mei-all.rng" type="application/xml" schematypens="http://purl.oclc.org/dsdl/schematron"?> +<mei xmlns="http://www.music-encoding.org/ns/mei" meiversion="5.0.0-dev"> + <meiHead> + <fileDesc> + <titleStmt> + <title /> + <respStmt /> + </titleStmt> + <pubStmt><date isodate="2023-06-17" type="encoding-date">2023-06-17</date> + </pubStmt> + </fileDesc> + <encodingDesc xml:id="encodingdesc-3zkwqi"> + <appInfo xml:id="appinfo-gqd9cz"> + <application xml:id="application-164aews" isodate="2023-06-17T17:35:22" version="3.15.0-5abc7c0"> + <name xml:id="name-18u3t8g">Verovio</name> + <p xml:id="p-45xr8w">Transcoded from MusicXML</p> + </application> + </appInfo> + </encodingDesc> + </meiHead> + <music> + <body> + <mdiv xml:id="m1hc62vy"> + <score xml:id="si9gptu"> + <scoreDef xml:id="s2jvi6g"> + <staffGrp xml:id="sjilxxd"> + <staffGrp xml:id="shmi7mq"> + <grpSym xml:id="gg339tt" symbol="bracket" /> + <staffDef xml:id="P1" n="1" lines="5" ppq="8"> + <label xml:id="l1cqd9df">Violine</label> + <labelAbbr xml:id="lbvnrq7">Vl.</labelAbbr> + <instrDef xml:id="i1mgkrs5" midi.channel="0" midi.instrnum="0" midi.volume="78.00%" /> + <clef xml:id="cc88b2g" shape="G" line="2" /> + <keySig xml:id="k1rnvnri" sig="1s" /> + <meterSig xml:id="mjh61cn" count="4" sym="common" unit="4" /> + </staffDef> + <staffDef xml:id="P2" n="2" lines="5" ppq="8"> + <label xml:id="ljzgfz3">Basso continuo</label> + <labelAbbr xml:id="l7bcvhk">B.c.</labelAbbr> + <instrDef xml:id="igtlvwr" midi.channel="1" midi.instrnum="0" midi.volume="78.00%" /> + <clef xml:id="c1nlymj1" shape="F" line="4" /> + <keySig xml:id="k187jlqk" sig="1s" /> + <meterSig xml:id="msez4nk" count="4" sym="common" unit="4" /> + </staffDef> + </staffGrp> + </staffGrp> + </scoreDef> + <section xml:id="snv8ev9"> + <measure xml:id="mmm2cx" n="1"> + <staff xml:id="sah0bjk" n="1"> + <layer xml:id="lc8vxmy" n="1"> + <note xml:id="nfawosr" dur.ppq="8" dur="4" oct="5" pname="d" stem.dir="down" /> + <beam xml:id="be9yy34"> + <note xml:id="nqkwd2f" dur.ppq="2" dur="16" oct="5" pname="d" stem.dir="down" /> + <note xml:id="nevczwz" dur.ppq="2" dur="16" oct="5" pname="d" stem.dir="down" /> + <note xml:id="n6hg0w2" dur.ppq="2" dur="16" oct="5" pname="e" stem.dir="down" /> + <note xml:id="ni40huz" dur.ppq="1" dur="32" oct="5" pname="f" stem.dir="down" accid.ges="s" /> + <note xml:id="n1cpdyo6" dur.ppq="1" dur="32" oct="5" pname="g" stem.dir="down" /> + </beam> + <beam xml:id="b1e2r70h"> + <note xml:id="n1pidydw" dur.ppq="1" dur="32" oct="5" pname="c" stem.dir="down" /> + <note xml:id="n1werv3i" breaksec="2" dur.ppq="1" dur="32" oct="4" pname="b" stem.dir="down"> + <artic xml:id="a1lj5zum" artic="ten" /> + </note> + <note xml:id="nbsakf4" breaksec="1" dur.ppq="2" dur="16" oct="5" pname="c" stem.dir="down" /> + <note xml:id="n13mx2r6" dur.ppq="4" dur="8" oct="5" pname="a" stem.dir="down" /> + </beam> + <beam xml:id="b13jqefk"> + <note xml:id="nmlxded" dur.ppq="2" dur="16" oct="5" pname="a" stem.dir="down" /> + <note xml:id="n1r1ecw4" dur.ppq="2" dur="16" oct="5" pname="c" stem.dir="down" /> + <note xml:id="n1itvzvf" dur.ppq="2" dur="16" oct="4" pname="b" stem.dir="down" /> + <note xml:id="n1wdsgl0" dur.ppq="2" dur="16" oct="4" pname="a" stem.dir="down" /> + </beam> + </layer> + </staff> + <staff xml:id="su2708j" n="2"> + <layer xml:id="l1cnumkk" n="1"> + <note xml:id="nze63bz" dur.ppq="8" dur="4" oct="2" pname="g" stem.dir="up" /> + <note xml:id="ngvywit" dur.ppq="8" dur="4" oct="3" pname="g" stem.dir="down" /> + <beam xml:id="b1lyfmhb"> + <note xml:id="n19238zs" dur.ppq="4" dur="8" oct="3" pname="g" stem.dir="down" /> + <note xml:id="n1ilxpi2" dur.ppq="2" dur="16" oct="3" pname="f" stem.dir="down" accid.ges="s" /> + <note xml:id="nn3f0w9" dur.ppq="2" dur="16" oct="3" pname="e" stem.dir="down" /> + </beam> + <beam xml:id="bxn37t4"> + <note xml:id="ntmeedq" dur.ppq="4" dur="8" oct="3" pname="f" stem.dir="down" accid.ges="s" /> + <note xml:id="n1l5t9o2" dur.ppq="4" dur="8" oct="3" pname="d" stem.dir="down" /> + </beam> + </layer> + </staff> + <tie xml:id="t166kic1" startid="#nfawosr" endid="#nqkwd2f" /> + <slur xml:id="s15es49g" startid="#ni40huz" endid="#n1cpdyo6" curvedir="above" /> + <slur xml:id="s3k35gw" startid="#n1pidydw" endid="#nbsakf4" curvedir="above" /> + <trill xml:id="t5g65q" staff="1" startid="#n1pidydw" /> + <tie xml:id="tvg8y3o" startid="#n13mx2r6" endid="#nmlxded" /> + <harm xml:id="hvex83f" staff="2" tstamp="1.000000"> + <fb xml:id="fmx2c5y"> + <f xml:id="fpb9hr5">3</f> + <f xml:id="fc50v42">8</f> + </fb> + </harm> + <harm xml:id="hku8baf" staff="2" tstamp="1.500000"> + <fb xml:id="f1c5f3if"> + <f xml:id="fxy31ga">4</f> + <f xml:id="fwpljky">2</f> + </fb> + </harm> + <harm xml:id="h1iuo2m" staff="2" tstamp="2.000000"> + <fb xml:id="f172embs"> + <f xml:id="fhor21q">5</f> + <f xml:id="f1kiun7u">3</f> + </fb> + </harm> + <harm xml:id="h19v6q2u" staff="2" tstamp="2.500000"> + <fb xml:id="fk11tjs"> + <f xml:id="f1klghe4">6</f> + </fb> + </harm> + <harm xml:id="h13fu69a" staff="2" tstamp="2.750000"> + <fb xml:id="f7vw3ij"> + <f xml:id="fie4qs1">5</f> + </fb> + </harm> + <tie xml:id="t19t2t1y" startid="#ngvywit" endid="#n19238zs" /> + <harm xml:id="hq5jprh" staff="2" tstamp="3.000000"> + <fb xml:id="f1wughfn"> + <f xml:id="fwl0yy0">6</f> + <f xml:id="f1uusydo">4</f> + <f xml:id="fejgtuu">2</f> + </fb> + </harm> + <harm xml:id="h1msmar7" staff="2" tstamp="4.000000"> + <fb xml:id="f18tp5lo"> + <f xml:id="f126yytx">6</f> + <f xml:id="f22oqpa">5</f> + </fb> + </harm> + </measure> + <measure xml:id="m1ornr4l" n="2"> + <staff xml:id="s4g97k9" n="1"> + <layer xml:id="l3chkjb" n="1"> + <beam xml:id="bymmiwj"> + <note xml:id="n95d8om" dur.ppq="2" dur="16" oct="4" pname="b" stem.dir="up" /> + <note xml:id="n9ck596" dur.ppq="1" dur="32" oct="4" pname="g" stem.dir="up" /> + <note xml:id="nlv9nwe" dur.ppq="1" dur="32" oct="4" pname="a" stem.dir="up" /> + <note xml:id="nz90bx3" dur.ppq="1" dur="32" oct="4" pname="b" stem.dir="up" /> + <note xml:id="n14py6x5" breaksec="2" dur.ppq="1" dur="32" oct="5" pname="c" stem.dir="up" /> + <note xml:id="nh4b1ho" dur.ppq="2" dur="16" oct="5" pname="d" stem.dir="up" /> + </beam> + <beam xml:id="b1tqx3ur"> + <note xml:id="n3cr9p6" dur.ppq="2" dur="16" oct="5" pname="d" stem.dir="down" /> + <note xml:id="nzxpnwo" dur.ppq="1" dur="32" oct="5" pname="e" stem.dir="down" /> + <note xml:id="n1la9mb2" dur.ppq="1" dur="32" oct="5" pname="f" stem.dir="down" accid.ges="s" /> + <note xml:id="nsa6xod" dur.ppq="1" dur="32" oct="5" pname="g" stem.dir="down" /> + <note xml:id="ngtwje4" breaksec="2" dur.ppq="1" dur="32" oct="5" pname="a" stem.dir="down" /> + <note xml:id="nt3vaiy" dur.ppq="2" dur="16" oct="5" pname="b" stem.dir="down" /> + </beam> + <beam xml:id="bv0n461"> + <note xml:id="n1go6aeo" dur.ppq="2" dur="16" oct="5" pname="b" stem.dir="down" /> + <note xml:id="n174x7t1" dur.ppq="1" dur="32" oct="5" pname="a" stem.dir="down" /> + <note xml:id="n1i72nzq" dur.ppq="1" dur="32" oct="5" pname="g" stem.dir="down" /> + <note xml:id="n1ls9lkj" dur.ppq="1" dur="32" oct="5" pname="a" stem.dir="down" /> + <note xml:id="ni2kvhs" breaksec="2" dur.ppq="1" dur="32" oct="5" pname="f" stem.dir="down" accid.ges="s" /> + <note xml:id="ntafgt" dur.ppq="2" dur="16" oct="5" pname="g" stem.dir="down" /> + </beam> + <beam xml:id="b4993up"> + <note xml:id="n1uynlbb" dur.ppq="4" dur="8" oct="5" pname="f" stem.dir="down" accid.ges="s" /> + <note xml:id="n168yxq2" dur.ppq="4" dur="8" oct="5" pname="d" stem.dir="down" /> + </beam> + </layer> + </staff> + <staff xml:id="si3d16e" n="2"> + <layer xml:id="l1yowb2j" n="1"> + <beam xml:id="b15pzihx"> + <note xml:id="n11asejt" dur.ppq="4" dur="8" oct="3" pname="g" stem.dir="up" /> + <note xml:id="n1hhp6bg" dur.ppq="4" dur="8" oct="3" pname="d" stem.dir="up" /> + <note xml:id="n1f9cl1n" dur.ppq="4" dur="8" oct="2" pname="b" stem.dir="up" /> + <note xml:id="n1vmtgup" dur.ppq="4" dur="8" oct="2" pname="g" stem.dir="up" /> + </beam> + <note xml:id="n1pi0n1f" dur.ppq="8" dur="4" oct="3" pname="d" stem.dir="down" /> + <beam xml:id="b10wbjea"> + <note xml:id="n8uv92t" dur.ppq="2" dur="16" oct="2" pname="d" stem.dir="up" /> + <note xml:id="n1b5xkjs" dur.ppq="2" dur="16" oct="3" pname="d" stem.dir="up" /> + <note xml:id="n1btu6gb" dur.ppq="2" dur="16" oct="3" pname="c" stem.dir="up" /> + <note xml:id="niayqs6" dur.ppq="2" dur="16" oct="3" pname="d" stem.dir="up" /> + </beam> + </layer> + </staff> + <slur xml:id="s1fbhfr0" startid="#n9ck596" endid="#nh4b1ho" curvedir="below" /> + <tie xml:id="tsa546u" startid="#nh4b1ho" endid="#n3cr9p6" /> + <slur xml:id="s13sp6ol" startid="#nzxpnwo" endid="#nt3vaiy" curvedir="above" /> + <tie xml:id="ttiqq4h" startid="#nt3vaiy" endid="#n1go6aeo" /> + <tie xml:id="tfxjhg" startid="#n168yxq2" endid="#n170ckul" /> + <harm xml:id="h1dcjeo6" staff="2" tstamp="1.000000"> + <fb xml:id="f1ygyl81"> + <f xml:id="f1qqd3xs">9</f> + </fb> + </harm> + <harm xml:id="hvi7chh" staff="2" tstamp="1.500000"> + <fb xml:id="f1ctzktl"> + <f xml:id="f1wdoznc">6</f> + <f xml:id="f1f8w41o">4</f> + </fb> + </harm> + <harm xml:id="h1lbtt0" staff="2" tstamp="1.750000"> + <fb xml:id="ftwx06e"> + <f xml:id="f1cyau83">3</f> + </fb> + </harm> + <harm xml:id="h1st0iuf" staff="2" tstamp="2.000000"> + <fb xml:id="f1x68pfn"> + <f xml:id="fl318nj">6</f> + </fb> + </harm> + <harm xml:id="hjw8jfg" staff="2" tstamp="3.000000"> + <fb xml:id="f1jb82oo"> + <f xml:id="fydhx8p">6</f> + <f xml:id="f105lzzl">4</f> + </fb> + </harm> + <harm xml:id="hhxda5g" staff="2" tstamp="3.500000"> + <fb xml:id="f17mje5x"> + <f xml:id="fih891r">5</f> + <f xml:id="f1t28idb">3</f> + </fb> + </harm> + <harm xml:id="h1997ven" staff="2" tstamp="3.750000"> + <fb xml:id="fi2ul9"> + <f xml:id="f1cnme4t">4</f> + <f xml:id="f151j71s">2</f> + </fb> + </harm> + <harm xml:id="hors6iq" staff="2" tstamp="4.000000"> + <fb xml:id="f1wejset"> + <f xml:id="f1ugx9r1">5</f> + <f xml:id="fiudu3l">3</f> + </fb> + </harm> + </measure> + <measure xml:id="m1o854s5" n="3"> + <staff xml:id="slnduvw" n="1"> + <layer xml:id="lliu88v" n="1"> + <beam xml:id="by00u2q"> + <note xml:id="n170ckul" dur.ppq="4" dur="8" oct="5" pname="d" stem.dir="down" /> + <note xml:id="n1p1jihj" dur.ppq="4" dur="8" oct="4" pname="b" stem.dir="down" /> + </beam> + <beam xml:id="bgsb2xb"> + <note xml:id="n14j6aw3" dur.ppq="4" dur="8" oct="4" pname="e" stem.dir="up" /> + <note xml:id="n1nbwiic" dur.ppq="2" dur="16" oct="5" pname="d" stem.dir="up" /> + <note xml:id="nlsrm66" dur.ppq="1" dur="32" oct="5" pname="c" stem.dir="up" /> + <note xml:id="nee17ab" dur.ppq="1" dur="32" oct="4" pname="b" stem.dir="up" /> + </beam> + <beam xml:id="b1qgl9no"> + <note xml:id="nmvqu21" dur.ppq="4" dur="8" oct="5" pname="c" stem.dir="up" /> + <note xml:id="n16jjcfm" dur.ppq="4" dur="8" oct="4" pname="a" stem.dir="up" /> + </beam> + <beam xml:id="bvr34n"> + <note xml:id="n1aj355b" dur.ppq="4" dur="8" oct="4" pname="d" stem.dir="up" /> + <note xml:id="n167veco" dur.ppq="2" dur="16" oct="5" pname="c" stem.dir="up" /> + <note xml:id="n1rjxpsf" dur.ppq="1" dur="32" oct="4" pname="b" stem.dir="up" /> + <note xml:id="n1va0y18" dur.ppq="1" dur="32" oct="4" pname="a" stem.dir="up" /> + </beam> + </layer> + </staff> + <staff xml:id="shg79lr" n="2"> + <layer xml:id="l1vype4t" n="1"> + <beam xml:id="b2gww6s"> + <note xml:id="n18facy3" dur.ppq="2" dur="16" oct="2" pname="b" stem.dir="down" /> + <note xml:id="nkbm0bs" dur.ppq="2" dur="16" oct="3" pname="b" stem.dir="down" /> + <note xml:id="nm1ybyl" dur.ppq="2" dur="16" oct="3" pname="a" stem.dir="down" /> + <note xml:id="nc0nnpd" dur.ppq="2" dur="16" oct="3" pname="b" stem.dir="down" /> + </beam> + <beam xml:id="b1bjxkqg"> + <note xml:id="n1g6lbnu" dur.ppq="2" dur="16" oct="3" pname="g" stem.dir="down" accid="s" /> + <note xml:id="nurjalm" dur.ppq="2" dur="16" oct="3" pname="b" stem.dir="down" /> + <note xml:id="n1efnj48" dur.ppq="2" dur="16" oct="3" pname="e" stem.dir="down" /> + <note xml:id="nl0gy9v" dur.ppq="2" dur="16" oct="3" pname="g" stem.dir="down" accid.ges="s" /> + </beam> + <beam xml:id="b1bn5wz0"> + <note xml:id="nm79bei" dur.ppq="2" dur="16" oct="2" pname="a" stem.dir="down" /> + <note xml:id="nljsbtp" dur.ppq="2" dur="16" oct="3" pname="a" stem.dir="down" /> + <note xml:id="ntg2uib" dur.ppq="2" dur="16" oct="3" pname="g" stem.dir="down" accid="n" /> + <note xml:id="n1gj9vxn" dur.ppq="2" dur="16" oct="3" pname="a" stem.dir="down" /> + </beam> + <beam xml:id="b1azuww2"> + <note xml:id="n1ww9z2j" dur.ppq="2" dur="16" oct="3" pname="f" stem.dir="down" accid.ges="s" /> + <note xml:id="n1opewo8" dur.ppq="2" dur="16" oct="3" pname="a" stem.dir="down" /> + <note xml:id="ni989tc" dur.ppq="2" dur="16" oct="3" pname="d" stem.dir="down" /> + <note xml:id="nlm2eue" dur.ppq="2" dur="16" oct="3" pname="f" stem.dir="down" accid.ges="s" /> + </beam> + </layer> + </staff> + <slur xml:id="sq52avb" startid="#n1nbwiic" endid="#nee17ab" curvedir="below" /> + <slur xml:id="s12o69ct" startid="#n167veco" endid="#n1va0y18" curvedir="below" /> + <harm xml:id="h11wdvy" staff="2" tstamp="1.000000"> + <fb xml:id="f8k5n35"> + <f xml:id="f1rajw9">5</f> + <f xml:id="f1cccwis">3</f> + </fb> + </harm> + <harm xml:id="h1sfwvbt" staff="2" tstamp="2.000000"> + <fb xml:id="f1pjengn"> + <f xml:id="f1fnej9">6</f> + <f xml:id="f1yvv7yx">5</f> + </fb> + </harm> + <harm xml:id="hds29xy" staff="2" tstamp="3.000000"> + <fb xml:id="f1g9cpbd"> + <f xml:id="f106kcvj">9</f> + </fb> + </harm> + <harm xml:id="h16qu3lz" staff="2" tstamp="3.500000"> + <fb xml:id="f1apyw93"> + <f xml:id="f1wyfcem">4</f> + <f xml:id="fek3v94">2</f> + </fb> + </harm> + <harm xml:id="hq0sqhm" staff="2" tstamp="4.000000"> + <fb xml:id="flc2av4"> + <f xml:id="f9blb0i">6</f> + <f xml:id="f1euf81m">5</f> + </fb> + </harm> + </measure> + <measure xml:id="muirav6" n="4"> + <staff xml:id="s15baso6" n="1"> + <layer xml:id="l1lxpipu" n="1"> + <beam xml:id="b13tcrho"> + <note xml:id="nfmggf5" dur.ppq="4" dur="8" oct="4" pname="b" stem.dir="down" /> + <note xml:id="n13ayqb7" dur.ppq="4" dur="8" oct="5" pname="g" stem.dir="down" /> + </beam> + <beam xml:id="bu4jrao"> + <note xml:id="n1fdszhf" dur.ppq="2" dur="16" oct="5" pname="g" stem.dir="down" /> + <note xml:id="n1hbezg4" dur.ppq="1" dur="32" oct="5" pname="f" stem.dir="down" accid.ges="s" /> + <note xml:id="n1cd5cn7" breaksec="2" dur.ppq="1" dur="32" oct="5" pname="e" stem.dir="down" /> + <note xml:id="nu7ulr1" dur.ppq="2" dur="16" oct="5" pname="d" stem.dir="down" /> + <note xml:id="n1lwhxle" dur.ppq="2" dur="16" oct="5" pname="c" stem.dir="down" /> + </beam> + <beam xml:id="bhshgut"> + <note xml:id="n3qpcm8" dur.ppq="4" dur="8" oct="4" pname="b" stem.dir="up" /> + <note xml:id="n1dbtdth" dur.ppq="1" dur="32" oct="4" pname="b" stem.dir="up" /> + <note xml:id="n1xfu30d" dur.ppq="1" dur="32" oct="4" pname="a" stem.dir="up" /> + <note xml:id="n2fncpb" dur.ppq="1" dur="32" oct="4" pname="g" stem.dir="up" /> + <note xml:id="n1vj1wat" dur.ppq="1" dur="32" oct="4" pname="f" stem.dir="up" accid.ges="s" /> + </beam> + <beam xml:id="b1vg96br"> + <note xml:id="nat4zpo" dur.ppq="2" dur="16" oct="4" pname="g" stem.dir="up" /> + <note xml:id="nh9k90w" dur.ppq="1" dur="32" oct="4" pname="f" stem.dir="up" accid.ges="s" /> + <note xml:id="nb6qnol" breaksec="2" dur.ppq="1" dur="32" oct="4" pname="e" stem.dir="up" /> + <note xml:id="n1f5iuzi" dur.ppq="2" dur="16" oct="4" pname="d" stem.dir="up" /> + <note xml:id="n8q7ac" dur.ppq="2" dur="16" oct="4" pname="c" stem.dir="up" /> + </beam> + </layer> + </staff> + <staff xml:id="s175f070" n="2"> + <layer xml:id="lojtwvv" n="1"> + <beam xml:id="b9yon14"> + <note xml:id="n1wws3qk" dur.ppq="2" dur="16" oct="2" pname="g" stem.dir="up" /> + <note xml:id="nl0kazr" dur.ppq="2" dur="16" oct="2" pname="a" stem.dir="up" /> + <note xml:id="nebfihr" dur.ppq="2" dur="16" oct="2" pname="b" stem.dir="up" /> + <note xml:id="ndrgqet" dur.ppq="2" dur="16" oct="3" pname="c" stem.dir="up" /> + </beam> + <beam xml:id="b1g4pb46"> + <note xml:id="n1xpe40q" dur.ppq="4" dur="8" oct="3" pname="d" stem.dir="up" /> + <note xml:id="nc0tq9z" dur.ppq="4" dur="8" oct="2" pname="d" stem.dir="up" /> + </beam> + <note xml:id="n1s239gu" dur.ppq="16" dur="2" oct="2" pname="g" stem.dir="up" /> + </layer> + </staff> + <tie xml:id="t19s2nka" startid="#n13ayqb7" endid="#n1fdszhf" /> + <tie xml:id="t1bhl0yk" startid="#n3qpcm8" endid="#n1dbtdth" /> + <harm xml:id="h1u3lzfh" staff="2" tstamp="1.000000"> + <fb xml:id="fkp21sc"> + <f xml:id="f1mtld8o">9</f> + </fb> + </harm> + <harm xml:id="h1qdsb1p" staff="2" tstamp="1.500000"> + <fb xml:id="f168tvwo"> + <f xml:id="f1ujc6f9">6</f> + </fb> + </harm> + <harm xml:id="h121r6ws" staff="2" tstamp="2.000000"> + <fb xml:id="f1wox7n6"> + <f xml:id="f4ki067">5</f> + <f xml:id="f1l0qdk0">4</f> + </fb> + </harm> + <harm xml:id="h1tt47bf" staff="2" tstamp="2.500000"> + <fb xml:id="fdjhuwq"> + <f xml:id="f9lm0t8">6</f> + <f xml:id="f1luy5i5">5</f> + </fb> + </harm> + <harm xml:id="h1xkzc75" staff="2" tstamp="2.750000"> + <fb xml:id="fbxky45"> + <f xml:id="f2zuvqs">7</f> + </fb> + </harm> + <harm xml:id="h1oc28l4" staff="2" tstamp="3.000000"> + <fb xml:id="f1fy10qq"> + <f xml:id="f1s4v4zm">3</f> + <f xml:id="fexzj0i">8</f> + <f xml:id="f15nhyv6">5</f> + </fb> + </harm> + <harm xml:id="hdw4du3" staff="2" tstamp="3.500000"> + <fb xml:id="f1g6c64m"> + <f xml:id="f1nxkhsy">2</f> + <f xml:id="f1peaqqi">7</f> + <f xml:id="f16ler4o">4</f> + </fb> + </harm> + <harm xml:id="h47gl7k" staff="2" tstamp="4.000000"> + <fb xml:id="f1al9sn0"> + <f xml:id="f1q5og2j">3</f> + <f xml:id="f17w73yu">8</f> + <f xml:id="f1q9upe6">5</f> + </fb> + </harm> + <harm xml:id="h104iprz" staff="2" tstamp="4.500000"> + <fb xml:id="f12o8tyj"> + <f xml:id="f7y749v">4</f> + <f xml:id="f19o5fbm">2</f> + </fb> + </harm> + </measure> + <measure xml:id="msahvch" right="end" n="5"> + <staff xml:id="s1o8347x" n="1"> + <layer xml:id="l1gkew8l" n="1"> + <note xml:id="nvm96wz" dur.ppq="8" dur="4" oct="3" pname="b" stem.dir="up" /> + <rest xml:id="r7ezwk1" dur.ppq="8" dur="4" /> + <rest xml:id="r1i0e8i6" dur.ppq="16" dur="2" /> + </layer> + </staff> + <staff xml:id="s1o7w8fi" n="2"> + <layer xml:id="l18xlg2p" n="1"> + <mRest xml:id="m1246q2m" /> + </layer> + </staff> + <harm xml:id="h1jymuc8" staff="1" tstamp="1.000000"> + <fb xml:id="f6ygtbe"> + <f xml:id="frvm4dq">5</f> + <f xml:id="fa4fqbo">♮</f> + </fb> + </harm> + </measure> + </section> + </score> + </mdiv> + </body> + </music> +</mei> diff --git a/music21/musicxml/lilypondTestSuite/piece01-bwv-1023-1-beginning.musicxml b/music21/musicxml/lilypondTestSuite/piece01-bwv-1023-1-beginning.musicxml index bc071f9f7a..2d850026f0 100644 --- a/music21/musicxml/lilypondTestSuite/piece01-bwv-1023-1-beginning.musicxml +++ b/music21/musicxml/lilypondTestSuite/piece01-bwv-1023-1-beginning.musicxml @@ -38,7 +38,7 @@ </defaults> <part-list> <score-part id="P1"> - <part-name print-object="no">Part 1</part-name> + <part-name>Klavier</part-name> <score-instrument id="P1-I1"> <instrument-name></instrument-name> </score-instrument> @@ -51,7 +51,7 @@ </midi-instrument> </score-part> <score-part id="P2"> - <part-name print-object="no">Part 2</part-name> + <part-name>Klavier</part-name> <score-instrument id="P2-I1"> <instrument-name></instrument-name> </score-instrument> @@ -65,14 +65,14 @@ </score-part> </part-list> <part id="P1"> - <measure number="1" width="249.00"> + <measure number="1" width="241.05"> <print> <system-layout> <system-margins> - <left-margin>50.00</left-margin> + <left-margin>78.66</left-margin> <right-margin>0.00</right-margin> </system-margins> - <top-system-distance>70.00</top-system-distance> + <top-system-distance>170.00</top-system-distance> </system-layout> </print> <attributes> @@ -108,7 +108,7 @@ <voice>1</voice> <type>quarter</type> </note> - <note default-x="173.70" default-y="-5.00"> + <note default-x="168.24" default-y="-5.00"> <pitch> <step>E</step> <octave>5</octave> @@ -123,7 +123,7 @@ <slur type="start" placement="above" number="1"/> </notations> </note> - <note default-x="204.20" default-y="0.00"> + <note default-x="196.25" default-y="0.00"> <pitch> <step>F</step> <alter>1</alter> @@ -137,7 +137,7 @@ <beam number="2">begin</beam> <beam number="3">begin</beam> </note> - <note default-x="219.20" default-y="5.00"> + <note default-x="211.25" default-y="5.00"> <pitch> <step>G</step> <octave>5</octave> @@ -154,7 +154,7 @@ </notations> </note> </measure> - <measure number="2" width="189.12"> + <measure number="2" width="181.18"> <note default-x="13.00" default-y="0.00"> <pitch> <step>F</step> @@ -192,7 +192,7 @@ <stem>down</stem> <beam number="1">continue</beam> </note> - <note default-x="64.41" default-y="-5.00"> + <note default-x="62.92" default-y="-5.00"> <pitch> <step>E</step> <octave>5</octave> @@ -211,7 +211,7 @@ <slur type="stop" number="1"/> </notations> </note> - <note default-x="86.37" default-y="-15.00"> + <note default-x="83.38" default-y="-15.00"> <pitch> <step>C</step> <octave>5</octave> @@ -224,7 +224,7 @@ <slur type="start" placement="above" number="1"/> </notations> </note> - <note default-x="125.03" default-y="-20.00"> + <note default-x="119.42" default-y="-20.00"> <pitch> <step>B</step> <octave>4</octave> @@ -242,7 +242,7 @@ </ornaments> </notations> </note> - <note default-x="159.33" default-y="-25.00"> + <note default-x="151.38" default-y="-25.00"> <pitch> <step>A</step> <octave>4</octave> @@ -255,7 +255,7 @@ <beam number="2">backward hook</beam> </note> </measure> - <measure number="3" width="265.10"> + <measure number="3" width="257.15"> <note default-x="13.00" default-y="-30.00"> <pitch> <step>G</step> @@ -268,7 +268,7 @@ <stem>up</stem> <beam number="1">begin</beam> </note> - <note default-x="53.79" default-y="-40.00"> + <note default-x="52.33" default-y="-40.00"> <pitch> <step>E</step> <octave>4</octave> @@ -284,7 +284,7 @@ <slur type="start" placement="below" number="1"/> </notations> </note> - <note default-x="69.78" default-y="-35.00"> + <note default-x="67.75" default-y="-35.00"> <pitch> <step>F</step> <alter>1</alter> @@ -298,7 +298,7 @@ <beam number="2">end</beam> <beam number="3">end</beam> </note> - <note default-x="85.77" default-y="-30.00"> + <note default-x="83.17" default-y="-30.00"> <pitch> <step>G</step> <octave>4</octave> @@ -313,7 +313,7 @@ <slur type="stop" number="1"/> </notations> </note> - <note default-x="146.54" default-y="-25.00"> + <note default-x="141.76" default-y="-25.00"> <pitch> <step>A</step> <octave>4</octave> @@ -329,7 +329,7 @@ <slur type="start" placement="above" number="1"/> </notations> </note> - <note default-x="162.53" default-y="-20.00"> + <note default-x="157.18" default-y="-20.00"> <pitch> <step>B</step> <octave>4</octave> @@ -342,7 +342,7 @@ <beam number="2">end</beam> <beam number="3">end</beam> </note> - <note default-x="178.52" default-y="-15.00"> + <note default-x="172.60" default-y="-15.00"> <pitch> <step>C</step> <octave>5</octave> @@ -357,7 +357,7 @@ <slur type="stop" number="1"/> </notations> </note> - <note default-x="219.31" default-y="-10.00"> + <note default-x="211.93" default-y="-10.00"> <pitch> <step>D</step> <octave>5</octave> @@ -371,7 +371,7 @@ <beam number="2">begin</beam> <beam number="3">begin</beam> </note> - <note default-x="235.30" default-y="-5.00"> + <note default-x="227.35" default-y="-5.00"> <pitch> <step>E</step> <octave>5</octave> @@ -385,7 +385,7 @@ <beam number="3">end</beam> </note> </measure> - <measure number="4" width="275.48"> + <measure number="4" width="270.67"> <note default-x="13.00" default-y="-45.00"> <pitch> <step>D</step> @@ -398,7 +398,7 @@ <stem>up</stem> <beam number="1">begin</beam> </note> - <note default-x="73.82" default-y="0.00"> + <note default-x="71.52" default-y="0.00"> <pitch> <step>F</step> <alter>1</alter> @@ -411,7 +411,7 @@ <beam number="1">end</beam> <beam number="2">backward hook</beam> </note> - <note default-x="99.42" default-y="10.00"> + <note default-x="96.17" default-y="10.00"> <pitch> <step>A</step> <octave>5</octave> @@ -423,7 +423,7 @@ <stem>down</stem> <beam number="1">begin</beam> </note> - <note default-x="140.25" default-y="5.00"> + <note default-x="135.45" default-y="5.00"> <pitch> <step>G</step> <octave>5</octave> @@ -436,7 +436,7 @@ <beam number="2">begin</beam> <beam number="3">begin</beam> </note> - <note default-x="156.25" default-y="0.00"> + <note default-x="150.85" default-y="0.00"> <pitch> <step>F</step> <alter>1</alter> @@ -450,7 +450,7 @@ <beam number="2">end</beam> <beam number="3">end</beam> </note> - <note default-x="172.65" default-y="0.00"> + <note default-x="167.25" default-y="0.00"> <grace/> <pitch> <step>F</step> @@ -464,7 +464,7 @@ <slur type="start" placement="below" number="1"/> </notations> </note> - <note default-x="188.86" default-y="-5.00"> + <note default-x="186.19" default-y="-5.00"> <pitch> <step>E</step> <octave>5</octave> @@ -483,7 +483,7 @@ </ornaments> </notations> </note> - <note default-x="229.68" default-y="-10.00"> + <note default-x="225.47" default-y="-10.00"> <pitch> <step>D</step> <alter>1</alter> @@ -498,7 +498,7 @@ <beam number="2">begin</beam> <beam number="3">begin</beam> </note> - <note default-x="245.69" default-y="-5.00"> + <note default-x="240.88" default-y="-5.00"> <pitch> <step>E</step> <octave>5</octave> @@ -785,7 +785,7 @@ <stem>down</stem> <beam number="1">continue</beam> </note> - <note default-x="155.53" default-y="-20.00"> + <note default-x="155.52" default-y="-20.00"> <pitch> <step>B</step> <octave>4</octave> @@ -1053,7 +1053,7 @@ </measure> </part> <part id="P2"> - <measure number="1" width="249.00"> + <measure number="1" width="241.05"> <print> <staff-layout number="1"> <staff-distance>65.00</staff-distance> @@ -1086,7 +1086,7 @@ <fermata type="upright" relative-y="5.00"/> </notations> </note> - <note default-x="140.22" default-y="-120.00"> + <note default-x="137.49" default-y="-120.00"> <pitch> <step>E</step> <octave>3</octave> @@ -1101,7 +1101,7 @@ </notations> </note> </measure> - <measure number="2" width="189.12"> + <measure number="2" width="181.18"> <figured-bass> <figure> <figure-number>6</figure-number> @@ -1125,9 +1125,6 @@ <stem>down</stem> <notations> <tied type="stop"/> - <technical> - <fingering>4</fingering> - </technical> </notations> </note> <figured-bass> @@ -1145,7 +1142,7 @@ </figure> <duration>24</duration> </figured-bass> - <note default-x="86.37" default-y="-125.00"> + <note default-x="83.38" default-y="-125.00"> <pitch> <step>D</step> <alter>1</alter> @@ -1156,14 +1153,9 @@ <type>half</type> <accidental>sharp</accidental> <stem>down</stem> - <notations> - <technical> - <fingering>2</fingering> - </technical> - </notations> </note> </measure> - <measure number="3" width="265.10"> + <measure number="3" width="257.15"> <figured-bass> <figure> <figure-number>9</figure-number> @@ -1191,7 +1183,7 @@ </figure> <duration>12</duration> </figured-bass> - <note default-x="85.77" default-y="-155.00"> + <note default-x="83.17" default-y="-155.00"> <pitch> <step>E</step> <octave>2</octave> @@ -1206,7 +1198,7 @@ <figure-number>6</figure-number> </figure> </figured-bass> - <note default-x="178.52" default-y="-120.00"> + <note default-x="172.60" default-y="-120.00"> <pitch> <step>E</step> <octave>3</octave> @@ -1217,7 +1209,7 @@ <stem>down</stem> </note> </measure> - <measure number="4" width="275.48"> + <measure number="4" width="270.67"> <figured-bass> <figure> <figure-number>7</figure-number> @@ -1239,18 +1231,13 @@ <voice>1</voice> <type>quarter</type> <stem>down</stem> - <notations> - <technical> - <fingering>2</fingering> - </technical> - </notations> </note> <figured-bass> <figure> <figure-number>6</figure-number> </figure> </figured-bass> - <note default-x="99.42" default-y="-130.00"> + <note default-x="96.17" default-y="-130.00"> <pitch> <step>C</step> <octave>3</octave> @@ -1359,11 +1346,6 @@ <voice>1</voice> <type>half</type> <stem>down</stem> - <notations> - <technical> - <fingering>4</fingering> - </technical> - </notations> </note> <figured-bass> <figure> @@ -1420,11 +1402,6 @@ <type>half</type> <accidental>sharp</accidental> <stem>down</stem> - <notations> - <technical> - <fingering>1</fingering> - </technical> - </notations> </note> <note default-x="182.06" default-y="-120.00"> <pitch> @@ -1466,11 +1443,6 @@ <voice>1</voice> <type>quarter</type> <stem>up</stem> - <notations> - <technical> - <fingering>2</fingering> - </technical> - </notations> </note> <figured-bass> <figure> diff --git a/music21/musicxml/lilypondTestSuite/piece02-bwv-1021-1-beginning.musicxml b/music21/musicxml/lilypondTestSuite/piece02-bwv-1021-1-beginning.musicxml index dc2ea21366..df9c907d3c 100644 --- a/music21/musicxml/lilypondTestSuite/piece02-bwv-1021-1-beginning.musicxml +++ b/music21/musicxml/lilypondTestSuite/piece02-bwv-1021-1-beginning.musicxml @@ -4,7 +4,7 @@ <identification> <encoding> <software>MuseScore 3.6.2</software> - <encoding-date>2023-06-14</encoding-date> + <encoding-date>2023-06-17</encoding-date> <supports element="accidental" type="yes"/> <supports element="beam" type="yes"/> <supports element="print" attribute="new-page" type="no"/> @@ -55,27 +55,13 @@ </midi-instrument> </score-part> <score-part id="P2"> - <part-name>(r. H.)</part-name> - <part-abbreviation>(r. H.)</part-abbreviation> - <score-instrument id="P2-I1"> - <instrument-name>Klavier</instrument-name> - </score-instrument> - <midi-device id="P2-I1" port="1"></midi-device> - <midi-instrument id="P2-I1"> - <midi-channel>3</midi-channel> - <midi-program>1</midi-program> - <volume>78.7402</volume> - <pan>0</pan> - </midi-instrument> - </score-part> - <score-part id="P3"> <part-name>Basso continuo</part-name> <part-abbreviation>B.c.</part-abbreviation> - <score-instrument id="P3-I1"> + <score-instrument id="P2-I1"> <instrument-name></instrument-name> </score-instrument> - <midi-device id="P3-I1" port="1"></midi-device> - <midi-instrument id="P3-I1"> + <midi-device id="P2-I1" port="1"></midi-device> + <midi-instrument id="P2-I1"> <midi-channel>2</midi-channel> <midi-program>1</midi-program> <volume>78.7402</volume> @@ -85,14 +71,14 @@ <part-group type="stop" number="1"/> </part-list> <part id="P1"> - <measure number="1" width="464.98"> + <measure number="1" width="453.09"> <print> <system-layout> <system-margins> <left-margin>153.15</left-margin> - <right-margin>0.00</right-margin> + <right-margin>-0.00</right-margin> </system-margins> - <top-system-distance>70.00</top-system-distance> + <top-system-distance>170.00</top-system-distance> </system-layout> </print> <attributes> @@ -109,7 +95,7 @@ <line>2</line> </clef> </attributes> - <note default-x="104.64" default-y="-8.50"> + <note default-x="104.64" default-y="-10.00"> <pitch> <step>D</step> <octave>5</octave> @@ -123,7 +109,7 @@ <tied type="start"/> </notations> </note> - <note default-x="167.96" default-y="-8.50"> + <note default-x="165.18" default-y="-10.00"> <pitch> <step>D</step> <octave>5</octave> @@ -139,7 +125,7 @@ <tied type="stop"/> </notations> </note> - <note default-x="190.98" default-y="-8.50"> + <note default-x="187.19" default-y="-10.00"> <pitch> <step>D</step> <octave>5</octave> @@ -151,7 +137,7 @@ <beam number="1">continue</beam> <beam number="2">continue</beam> </note> - <note default-x="214.01" default-y="-4.25"> + <note default-x="209.20" default-y="-5.00"> <pitch> <step>E</step> <octave>5</octave> @@ -163,7 +149,7 @@ <beam number="1">continue</beam> <beam number="2">continue</beam> </note> - <note default-x="237.04" default-y="0.00"> + <note default-x="231.22" default-y="0.00"> <pitch> <step>F</step> <alter>1</alter> @@ -180,7 +166,7 @@ <slur type="start" placement="above" number="1"/> </notations> </note> - <note default-x="252.03" default-y="4.25"> + <note default-x="246.22" default-y="5.00"> <pitch> <step>G</step> <octave>5</octave> @@ -196,7 +182,7 @@ <slur type="stop" number="1"/> </notations> </note> - <note default-x="267.03" default-y="-12.75"> + <note default-x="261.21" default-y="-15.00"> <pitch> <step>C</step> <octave>5</octave> @@ -215,7 +201,7 @@ </ornaments> </notations> </note> - <note default-x="282.03" default-y="-17.00"> + <note default-x="276.21" default-y="-20.00"> <pitch> <step>B</step> <octave>4</octave> @@ -233,7 +219,7 @@ </articulations> </notations> </note> - <note default-x="297.02" default-y="-12.75"> + <note default-x="291.21" default-y="-15.00"> <pitch> <step>C</step> <octave>5</octave> @@ -248,7 +234,7 @@ <slur type="stop" number="1"/> </notations> </note> - <note default-x="320.05" default-y="8.50"> + <note default-x="313.22" default-y="10.00"> <pitch> <step>A</step> <octave>5</octave> @@ -263,7 +249,7 @@ <tied type="start"/> </notations> </note> - <note default-x="366.10" default-y="8.50"> + <note default-x="357.25" default-y="10.00"> <pitch> <step>A</step> <octave>5</octave> @@ -279,7 +265,7 @@ <tied type="stop"/> </notations> </note> - <note default-x="389.13" default-y="-12.75"> + <note default-x="379.26" default-y="-15.00"> <pitch> <step>C</step> <octave>5</octave> @@ -291,7 +277,7 @@ <beam number="1">continue</beam> <beam number="2">continue</beam> </note> - <note default-x="412.15" default-y="-17.00"> + <note default-x="401.28" default-y="-20.00"> <pitch> <step>B</step> <octave>4</octave> @@ -303,7 +289,7 @@ <beam number="1">continue</beam> <beam number="2">continue</beam> </note> - <note default-x="435.18" default-y="-21.25"> + <note default-x="423.29" default-y="-25.00"> <pitch> <step>A</step> <octave>4</octave> @@ -316,8 +302,8 @@ <beam number="2">end</beam> </note> </measure> - <measure number="2" width="506.87"> - <note default-x="13.00" default-y="-17.00"> + <measure number="2" width="518.76"> + <note default-x="13.00" default-y="-20.00"> <pitch> <step>B</step> <octave>4</octave> @@ -329,7 +315,7 @@ <beam number="1">begin</beam> <beam number="2">begin</beam> </note> - <note default-x="41.03" default-y="-25.50"> + <note default-x="41.43" default-y="-30.00"> <pitch> <step>G</step> <octave>4</octave> @@ -345,7 +331,7 @@ <slur type="start" placement="below" number="1"/> </notations> </note> - <note default-x="58.56" default-y="-21.25"> + <note default-x="59.20" default-y="-25.00"> <pitch> <step>A</step> <octave>4</octave> @@ -358,7 +344,7 @@ <beam number="2">continue</beam> <beam number="3">continue</beam> </note> - <note default-x="76.08" default-y="-17.00"> + <note default-x="76.97" default-y="-20.00"> <pitch> <step>B</step> <octave>4</octave> @@ -371,7 +357,7 @@ <beam number="2">continue</beam> <beam number="3">continue</beam> </note> - <note default-x="93.60" default-y="-12.75"> + <note default-x="94.74" default-y="-15.00"> <pitch> <step>C</step> <octave>5</octave> @@ -384,7 +370,7 @@ <beam number="2">continue</beam> <beam number="3">end</beam> </note> - <note default-x="111.12" default-y="-8.50"> + <note default-x="112.51" default-y="-10.00"> <pitch> <step>D</step> <octave>5</octave> @@ -401,7 +387,7 @@ <slur type="stop" number="1"/> </notations> </note> - <note default-x="139.15" default-y="-8.50"> + <note default-x="140.94" default-y="-10.00"> <pitch> <step>D</step> <octave>5</octave> @@ -417,7 +403,7 @@ <tied type="stop"/> </notations> </note> - <note default-x="167.19" default-y="-4.25"> + <note default-x="169.37" default-y="-5.00"> <pitch> <step>E</step> <octave>5</octave> @@ -433,7 +419,7 @@ <slur type="start" placement="above" number="1"/> </notations> </note> - <note default-x="184.71" default-y="0.00"> + <note default-x="187.14" default-y="0.00"> <pitch> <step>F</step> <alter>1</alter> @@ -447,7 +433,7 @@ <beam number="2">continue</beam> <beam number="3">continue</beam> </note> - <note default-x="202.23" default-y="4.25"> + <note default-x="204.91" default-y="5.00"> <pitch> <step>G</step> <octave>5</octave> @@ -460,7 +446,7 @@ <beam number="2">continue</beam> <beam number="3">continue</beam> </note> - <note default-x="219.75" default-y="8.50"> + <note default-x="223.41" default-y="10.00"> <pitch> <step>A</step> <octave>5</octave> @@ -473,7 +459,7 @@ <beam number="2">continue</beam> <beam number="3">end</beam> </note> - <note default-x="238.75" default-y="12.75"> + <note default-x="245.41" default-y="15.00"> <pitch> <step>B</step> <octave>5</octave> @@ -490,7 +476,7 @@ <slur type="stop" number="1"/> </notations> </note> - <note default-x="266.78" default-y="12.75"> + <note default-x="273.84" default-y="15.00"> <pitch> <step>B</step> <octave>5</octave> @@ -506,7 +492,7 @@ <tied type="stop"/> </notations> </note> - <note default-x="294.82" default-y="8.50"> + <note default-x="302.27" default-y="10.00"> <pitch> <step>A</step> <octave>5</octave> @@ -519,7 +505,7 @@ <beam number="2">continue</beam> <beam number="3">begin</beam> </note> - <note default-x="312.34" default-y="4.25"> + <note default-x="320.77" default-y="5.00"> <pitch> <step>G</step> <octave>5</octave> @@ -532,7 +518,7 @@ <beam number="2">continue</beam> <beam number="3">continue</beam> </note> - <note default-x="329.86" default-y="8.50"> + <note default-x="339.26" default-y="10.00"> <pitch> <step>A</step> <octave>5</octave> @@ -545,7 +531,7 @@ <beam number="2">continue</beam> <beam number="3">continue</beam> </note> - <note default-x="347.38" default-y="0.00"> + <note default-x="357.03" default-y="0.00"> <pitch> <step>F</step> <alter>1</alter> @@ -559,7 +545,7 @@ <beam number="2">continue</beam> <beam number="3">end</beam> </note> - <note default-x="364.90" default-y="4.25"> + <note default-x="374.80" default-y="5.00"> <pitch> <step>G</step> <octave>5</octave> @@ -571,7 +557,7 @@ <beam number="1">end</beam> <beam number="2">end</beam> </note> - <note default-x="392.94" default-y="0.00"> + <note default-x="403.23" default-y="0.00"> <pitch> <step>F</step> <alter>1</alter> @@ -583,7 +569,7 @@ <stem>down</stem> <beam number="1">begin</beam> </note> - <note default-x="449.00" default-y="-8.50"> + <note default-x="460.10" default-y="-10.00"> <pitch> <step>D</step> <octave>5</octave> @@ -599,8 +585,8 @@ </notations> </note> </measure> - <measure number="3" width="563.15"> - <note default-x="81.12" default-y="-8.50"> + <measure number="3" width="503.22"> + <note default-x="81.12" default-y="-10.00"> <pitch> <step>D</step> <octave>5</octave> @@ -615,7 +601,7 @@ <tied type="stop"/> </notations> </note> - <note default-x="137.96" default-y="-17.00"> + <note default-x="129.60" default-y="-20.00"> <pitch> <step>B</step> <octave>4</octave> @@ -626,7 +612,7 @@ <stem>down</stem> <beam number="1">end</beam> </note> - <note default-x="195.84" default-y="-34.00"> + <note default-x="183.29" default-y="-40.00"> <pitch> <step>E</step> <octave>4</octave> @@ -637,7 +623,7 @@ <stem>up</stem> <beam number="1">begin</beam> </note> - <note default-x="252.68" default-y="-8.50"> + <note default-x="231.77" default-y="-10.00"> <pitch> <step>D</step> <octave>5</octave> @@ -652,7 +638,7 @@ <slur type="start" placement="below" number="1"/> </notations> </note> - <note default-x="281.11" default-y="-12.75"> + <note default-x="256.01" default-y="-15.00"> <pitch> <step>C</step> <octave>5</octave> @@ -665,7 +651,7 @@ <beam number="2">continue</beam> <beam number="3">begin</beam> </note> - <note default-x="298.87" default-y="-17.00"> + <note default-x="271.16" default-y="-20.00"> <pitch> <step>B</step> <octave>4</octave> @@ -681,7 +667,7 @@ <slur type="stop" number="1"/> </notations> </note> - <note default-x="316.63" default-y="-12.75"> + <note default-x="286.31" default-y="-15.00"> <pitch> <step>C</step> <octave>5</octave> @@ -692,7 +678,7 @@ <stem>up</stem> <beam number="1">begin</beam> </note> - <note default-x="373.48" default-y="-21.25"> + <note default-x="337.08" default-y="-25.00"> <pitch> <step>A</step> <octave>4</octave> @@ -703,7 +689,7 @@ <stem>up</stem> <beam number="1">end</beam> </note> - <note default-x="430.32" default-y="-38.25"> + <note default-x="385.56" default-y="-45.00"> <pitch> <step>D</step> <octave>4</octave> @@ -714,7 +700,7 @@ <stem>up</stem> <beam number="1">begin</beam> </note> - <note default-x="487.16" default-y="-12.75"> + <note default-x="434.03" default-y="-15.00"> <pitch> <step>C</step> <octave>5</octave> @@ -729,7 +715,7 @@ <slur type="start" placement="below" number="1"/> </notations> </note> - <note default-x="515.59" default-y="-17.00"> + <note default-x="458.27" default-y="-20.00"> <pitch> <step>B</step> <octave>4</octave> @@ -742,7 +728,7 @@ <beam number="2">continue</beam> <beam number="3">begin</beam> </note> - <note default-x="533.35" default-y="-21.25"> + <note default-x="473.42" default-y="-25.00"> <pitch> <step>A</step> <octave>4</octave> @@ -759,8 +745,8 @@ </notations> </note> </measure> - <measure number="4" width="491.91"> - <note default-x="13.00" default-y="-17.00"> + <measure number="4" width="427.18"> + <note default-x="13.00" default-y="-20.00"> <pitch> <step>B</step> <octave>4</octave> @@ -771,7 +757,7 @@ <stem>down</stem> <beam number="1">begin</beam> </note> - <note default-x="70.19" default-y="4.25"> + <note default-x="63.00" default-y="5.00"> <pitch> <step>G</step> <octave>5</octave> @@ -786,7 +772,7 @@ <tied type="start"/> </notations> </note> - <note default-x="127.38" default-y="4.25"> + <note default-x="113.00" default-y="5.00"> <pitch> <step>G</step> <octave>5</octave> @@ -802,7 +788,7 @@ <tied type="stop"/> </notations> </note> - <note default-x="155.98" default-y="0.00"> + <note default-x="138.00" default-y="0.00"> <pitch> <step>F</step> <alter>1</alter> @@ -816,7 +802,7 @@ <beam number="2">continue</beam> <beam number="3">begin</beam> </note> - <note default-x="173.85" default-y="-4.25"> + <note default-x="153.63" default-y="-5.00"> <pitch> <step>E</step> <octave>5</octave> @@ -829,7 +815,7 @@ <beam number="2">continue</beam> <beam number="3">end</beam> </note> - <note default-x="191.72" default-y="-8.50"> + <note default-x="169.25" default-y="-10.00"> <pitch> <step>D</step> <octave>5</octave> @@ -841,7 +827,7 @@ <beam number="1">continue</beam> <beam number="2">continue</beam> </note> - <note default-x="220.32" default-y="-12.75"> + <note default-x="194.25" default-y="-15.00"> <pitch> <step>C</step> <octave>5</octave> @@ -853,7 +839,7 @@ <beam number="1">end</beam> <beam number="2">end</beam> </note> - <note default-x="248.92" default-y="-17.00"> + <note default-x="219.26" default-y="-20.00"> <pitch> <step>B</step> <octave>4</octave> @@ -868,7 +854,7 @@ <tied type="start"/> </notations> </note> - <note default-x="288.24" default-y="-17.00"> + <note default-x="253.63" default-y="-20.00"> <pitch> <step>B</step> <octave>4</octave> @@ -885,7 +871,7 @@ <tied type="stop"/> </notations> </note> - <note default-x="306.11" default-y="-21.25"> + <note default-x="269.26" default-y="-25.00"> <pitch> <step>A</step> <octave>4</octave> @@ -898,7 +884,7 @@ <beam number="2">continue</beam> <beam number="3">continue</beam> </note> - <note default-x="323.98" default-y="-25.50"> + <note default-x="284.88" default-y="-30.00"> <pitch> <step>G</step> <octave>4</octave> @@ -911,7 +897,7 @@ <beam number="2">continue</beam> <beam number="3">continue</beam> </note> - <note default-x="341.85" default-y="-29.75"> + <note default-x="300.51" default-y="-35.00"> <pitch> <step>F</step> <alter>1</alter> @@ -925,7 +911,7 @@ <beam number="2">end</beam> <beam number="3">end</beam> </note> - <note default-x="359.73" default-y="-25.50"> + <note default-x="316.13" default-y="-30.00"> <pitch> <step>G</step> <octave>4</octave> @@ -937,7 +923,7 @@ <beam number="1">begin</beam> <beam number="2">begin</beam> </note> - <note default-x="388.32" default-y="-29.75"> + <note default-x="341.13" default-y="-35.00"> <pitch> <step>F</step> <alter>1</alter> @@ -951,7 +937,7 @@ <beam number="2">continue</beam> <beam number="3">begin</beam> </note> - <note default-x="406.19" default-y="-34.00"> + <note default-x="356.76" default-y="-40.00"> <pitch> <step>E</step> <octave>4</octave> @@ -964,7 +950,7 @@ <beam number="2">continue</beam> <beam number="3">end</beam> </note> - <note default-x="424.07" default-y="-38.25"> + <note default-x="372.38" default-y="-45.00"> <pitch> <step>D</step> <octave>4</octave> @@ -976,7 +962,7 @@ <beam number="1">continue</beam> <beam number="2">continue</beam> </note> - <note default-x="452.66" default-y="-42.50"> + <note default-x="397.39" default-y="-50.00"> <pitch> <step>C</step> <octave>4</octave> @@ -988,65 +974,45 @@ <beam number="1">end</beam> <beam number="2">end</beam> </note> - <barline location="right"> - <bar-style>light-heavy</bar-style> - </barline> - </measure> - </part> - <part id="P2"> - <measure number="1" width="464.98"> - <print> - <staff-layout number="1"> - <staff-distance>65.00</staff-distance> - </staff-layout> - </print> - <attributes> - <divisions>8</divisions> - <key> - <fifths>1</fifths> - </key> - <time symbol="common"> - <beats>4</beats> - <beat-type>4</beat-type> - </time> - <clef> - <sign>G</sign> - <line>2</line> - </clef> - </attributes> - <note> - <rest measure="yes"/> - <duration>32</duration> - <voice>1</voice> - </note> </measure> - <measure number="2" width="506.87"> - <note> - <rest measure="yes"/> - <duration>32</duration> + <measure number="5" width="146.31"> + <figured-bass> + <figure> + <figure-number>5</figure-number> + </figure> + <figure> + <prefix>natural</prefix> + </figure> + </figured-bass> + <note default-x="16.50" default-y="-55.00"> + <pitch> + <step>B</step> + <octave>3</octave> + </pitch> + <duration>8</duration> <voice>1</voice> + <type>quarter</type> + <stem>up</stem> </note> - </measure> - <measure number="3" width="563.15"> <note> - <rest measure="yes"/> - <duration>32</duration> + <rest/> + <duration>8</duration> <voice>1</voice> + <type>quarter</type> </note> - </measure> - <measure number="4" width="491.91"> <note> - <rest measure="yes"/> - <duration>32</duration> + <rest/> + <duration>16</duration> <voice>1</voice> + <type>half</type> </note> <barline location="right"> <bar-style>light-heavy</bar-style> </barline> </measure> </part> - <part id="P3"> - <measure number="1" width="464.98"> + <part id="P2"> + <measure number="1" width="453.09"> <print> <staff-layout number="1"> <staff-distance>65.00</staff-distance> @@ -1084,7 +1050,7 @@ </figure> <duration>4</duration> </figured-bass> - <note default-x="104.64" default-y="-244.00"> + <note default-x="104.64" default-y="-145.00"> <pitch> <step>G</step> <octave>2</octave> @@ -1115,7 +1081,7 @@ </figure> <duration>2</duration> </figured-bass> - <note default-x="167.96" default-y="-209.00"> + <note default-x="165.18" default-y="-110.00"> <pitch> <step>G</step> <octave>3</octave> @@ -1140,7 +1106,7 @@ <figure-number>2</figure-number> </figure> </figured-bass> - <note default-x="267.03" default-y="-209.00"> + <note default-x="261.21" default-y="-110.00"> <pitch> <step>G</step> <octave>3</octave> @@ -1155,7 +1121,7 @@ <tied type="stop"/> </notations> </note> - <note default-x="320.05" default-y="-214.00"> + <note default-x="313.22" default-y="-115.00"> <pitch> <step>F</step> <alter>1</alter> @@ -1168,7 +1134,7 @@ <beam number="1">continue</beam> <beam number="2">begin</beam> </note> - <note default-x="343.08" default-y="-219.00"> + <note default-x="335.23" default-y="-120.00"> <pitch> <step>E</step> <octave>3</octave> @@ -1188,7 +1154,7 @@ <figure-number>5</figure-number> </figure> </figured-bass> - <note default-x="366.10" default-y="-214.00"> + <note default-x="357.25" default-y="-115.00"> <pitch> <step>F</step> <alter>1</alter> @@ -1205,7 +1171,7 @@ <extend type="stop" /> </figure> </figured-bass> - <note default-x="412.15" default-y="-224.00"> + <note default-x="401.28" default-y="-125.00"> <pitch> <step>D</step> <octave>3</octave> @@ -1217,13 +1183,13 @@ <beam number="1">end</beam> </note> </measure> - <measure number="2" width="506.87"> + <measure number="2" width="518.76"> <figured-bass> <figure> <figure-number>9</figure-number> </figure> </figured-bass> - <note default-x="13.00" default-y="-209.00"> + <note default-x="13.00" default-y="-110.00"> <pitch> <step>G</step> <octave>3</octave> @@ -1252,7 +1218,7 @@ </figure> <duration>2</duration> </figured-bass> - <note default-x="76.08" default-y="-224.00"> + <note default-x="76.97" default-y="-125.00"> <pitch> <step>D</step> <octave>3</octave> @@ -1268,7 +1234,7 @@ <figure-number>6</figure-number> </figure> </figured-bass> - <note default-x="139.15" default-y="-234.00"> + <note default-x="140.94" default-y="-135.00"> <pitch> <step>B</step> <octave>2</octave> @@ -1279,7 +1245,7 @@ <stem>up</stem> <beam number="1">continue</beam> </note> - <note default-x="202.23" default-y="-244.00"> + <note default-x="204.91" default-y="-145.00"> <pitch> <step>G</step> <octave>2</octave> @@ -1317,7 +1283,7 @@ </figure> <duration>2</duration> </figured-bass> - <note default-x="266.78" default-y="-224.00"> + <note default-x="273.84" default-y="-125.00"> <pitch> <step>D</step> <octave>3</octave> @@ -1335,7 +1301,7 @@ <figure-number>3</figure-number> </figure> </figured-bass> - <note default-x="392.94" default-y="-259.00"> + <note default-x="403.23" default-y="-160.00"> <pitch> <step>D</step> <octave>2</octave> @@ -1347,7 +1313,7 @@ <beam number="1">begin</beam> <beam number="2">begin</beam> </note> - <note default-x="420.97" default-y="-224.00"> + <note default-x="431.67" default-y="-125.00"> <pitch> <step>D</step> <octave>3</octave> @@ -1359,7 +1325,7 @@ <beam number="1">continue</beam> <beam number="2">continue</beam> </note> - <note default-x="449.00" default-y="-229.00"> + <note default-x="460.10" default-y="-130.00"> <pitch> <step>C</step> <octave>3</octave> @@ -1371,7 +1337,7 @@ <beam number="1">continue</beam> <beam number="2">continue</beam> </note> - <note default-x="477.04" default-y="-224.00"> + <note default-x="488.53" default-y="-125.00"> <pitch> <step>D</step> <octave>3</octave> @@ -1384,7 +1350,7 @@ <beam number="2">end</beam> </note> </measure> - <measure number="3" width="563.15"> + <measure number="3" width="503.22"> <figured-bass> <figure> <figure-number>5</figure-number> @@ -1393,7 +1359,7 @@ <figure-number>3</figure-number> </figure> </figured-bass> - <note default-x="81.12" default-y="-234.00"> + <note default-x="81.12" default-y="-136.30"> <pitch> <step>B</step> <octave>2</octave> @@ -1405,7 +1371,7 @@ <beam number="1">begin</beam> <beam number="2">begin</beam> </note> - <note default-x="109.54" default-y="-199.00"> + <note default-x="105.36" default-y="-101.30"> <pitch> <step>B</step> <octave>3</octave> @@ -1417,7 +1383,7 @@ <beam number="1">continue</beam> <beam number="2">continue</beam> </note> - <note default-x="137.96" default-y="-204.00"> + <note default-x="129.60" default-y="-106.30"> <pitch> <step>A</step> <octave>3</octave> @@ -1429,7 +1395,7 @@ <beam number="1">continue</beam> <beam number="2">continue</beam> </note> - <note default-x="166.38" default-y="-199.00"> + <note default-x="153.83" default-y="-101.30"> <pitch> <step>B</step> <octave>3</octave> @@ -1449,7 +1415,7 @@ <figure-number>5</figure-number> </figure> </figured-bass> - <note default-x="195.84" default-y="-209.00"> + <note default-x="183.29" default-y="-111.30"> <pitch> <step>G</step> <alter>1</alter> @@ -1463,7 +1429,7 @@ <beam number="1">begin</beam> <beam number="2">begin</beam> </note> - <note default-x="224.26" default-y="-199.00"> + <note default-x="207.53" default-y="-101.30"> <pitch> <step>B</step> <octave>3</octave> @@ -1475,7 +1441,7 @@ <beam number="1">continue</beam> <beam number="2">continue</beam> </note> - <note default-x="252.68" default-y="-219.00"> + <note default-x="231.77" default-y="-121.30"> <pitch> <step>E</step> <octave>3</octave> @@ -1487,7 +1453,7 @@ <beam number="1">continue</beam> <beam number="2">continue</beam> </note> - <note default-x="281.11" default-y="-209.00"> + <note default-x="256.01" default-y="-111.30"> <pitch> <step>G</step> <alter>1</alter> @@ -1505,7 +1471,7 @@ <figure-number>9</figure-number> </figure> </figured-bass> - <note default-x="316.63" default-y="-239.00"> + <note default-x="286.31" default-y="-141.30"> <pitch> <step>A</step> <octave>2</octave> @@ -1517,7 +1483,7 @@ <beam number="1">begin</beam> <beam number="2">begin</beam> </note> - <note default-x="345.05" default-y="-204.00"> + <note default-x="310.54" default-y="-106.30"> <pitch> <step>A</step> <octave>3</octave> @@ -1537,7 +1503,7 @@ <figure-number>2</figure-number> </figure> </figured-bass> - <note default-x="373.48" default-y="-209.00"> + <note default-x="337.08" default-y="-111.30"> <pitch> <step>G</step> <octave>3</octave> @@ -1550,7 +1516,7 @@ <beam number="1">continue</beam> <beam number="2">continue</beam> </note> - <note default-x="401.90" default-y="-204.00"> + <note default-x="361.32" default-y="-106.30"> <pitch> <step>A</step> <octave>3</octave> @@ -1570,7 +1536,7 @@ <figure-number>5</figure-number> </figure> </figured-bass> - <note default-x="430.32" default-y="-214.00"> + <note default-x="385.56" default-y="-116.30"> <pitch> <step>F</step> <alter>1</alter> @@ -1583,7 +1549,7 @@ <beam number="1">begin</beam> <beam number="2">begin</beam> </note> - <note default-x="458.74" default-y="-204.00"> + <note default-x="409.79" default-y="-106.30"> <pitch> <step>A</step> <octave>3</octave> @@ -1595,7 +1561,7 @@ <beam number="1">continue</beam> <beam number="2">continue</beam> </note> - <note default-x="487.16" default-y="-224.00"> + <note default-x="434.03" default-y="-126.30"> <pitch> <step>D</step> <octave>3</octave> @@ -1607,7 +1573,7 @@ <beam number="1">continue</beam> <beam number="2">continue</beam> </note> - <note default-x="515.59" default-y="-214.00"> + <note default-x="458.27" default-y="-116.30"> <pitch> <step>F</step> <alter>1</alter> @@ -1621,13 +1587,13 @@ <beam number="2">end</beam> </note> </measure> - <measure number="4" width="491.91"> + <measure number="4" width="427.18"> <figured-bass> <figure> <figure-number>9</figure-number> </figure> </figured-bass> - <note default-x="13.00" default-y="-244.00"> + <note default-x="13.00" default-y="-146.30"> <pitch> <step>G</step> <octave>2</octave> @@ -1639,7 +1605,7 @@ <beam number="1">begin</beam> <beam number="2">begin</beam> </note> - <note default-x="41.60" default-y="-239.00"> + <note default-x="38.00" default-y="-141.30"> <pitch> <step>A</step> <octave>2</octave> @@ -1656,7 +1622,7 @@ <figure-number>6</figure-number> </figure> </figured-bass> - <note default-x="70.19" default-y="-234.00"> + <note default-x="63.00" default-y="-136.30"> <pitch> <step>B</step> <octave>2</octave> @@ -1668,7 +1634,7 @@ <beam number="1">continue</beam> <beam number="2">continue</beam> </note> - <note default-x="98.79" default-y="-229.00"> + <note default-x="88.00" default-y="-131.30"> <pitch> <step>C</step> <octave>3</octave> @@ -1688,7 +1654,7 @@ <figure-number>4</figure-number> </figure> </figured-bass> - <note default-x="127.38" default-y="-224.00"> + <note default-x="113.00" default-y="-126.30"> <pitch> <step>D</step> <octave>3</octave> @@ -1714,7 +1680,7 @@ </figure> <duration>2</duration> </figured-bass> - <note default-x="191.72" default-y="-259.00"> + <note default-x="169.25" default-y="-161.30"> <pitch> <step>D</step> <octave>2</octave> @@ -1770,7 +1736,7 @@ </figure> <duration>4</duration> </figured-bass> - <note default-x="248.92" default-y="-244.00"> + <note default-x="219.26" default-y="-146.30"> <pitch> <step>G</step> <octave>2</octave> @@ -1780,6 +1746,13 @@ <type>half</type> <stem>up</stem> </note> + </measure> + <measure number="5" width="146.31"> + <note> + <rest measure="yes"/> + <duration>32</duration> + <voice>1</voice> + </note> <barline location="right"> <bar-style>light-heavy</bar-style> </barline> diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index 0749409cd0..6d1d9d0024 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -3425,7 +3425,7 @@ def parseFlatElements( if hasSpannerAnchors: self.parseOneElement(obj, AppendSpanners.NONE) else: - # ENTRY FOR Figured Bass Indications + # ENTRY for other elements e.g. ChordSymbols,FiguredBassIndications self.parseOneElement(obj, AppendSpanners.NORMAL) for n in notesForLater: diff --git a/music21/musicxml/test_xmlToM21.py b/music21/musicxml/test_xmlToM21.py index 4847c1d166..1cadc6db01 100644 --- a/music21/musicxml/test_xmlToM21.py +++ b/music21/musicxml/test_xmlToM21.py @@ -1504,6 +1504,19 @@ def testImportImplicitMeasureNumber(self): m = s[stream.Measure].first() self.assertIs(m.showNumber, stream.enums.ShowNumber.NEVER) + def testImportFiguredBassIndications(self): + from music21 import converter + + xml_dir = common.getSourceFilePath() / 'musicxml' / 'lilypondTestSuite' + s = converter.parse(xml_dir / '74a-FiguredBass.xml') + self.assertIs(len(s.flatten().getElementsByClass(harmony.FiguredBassIndication)), 5) + + s = converter.parse(xml_dir / '74b-FiguredBass.xml') + self.assertIs(len(s.flatten().getElementsByClass(harmony.FiguredBassIndication)), 12) + + s = converter.parse(xml_dir / 'piece01-bwv-1023-1-beginning.musicxml') + self.assertEqual(s.flatten().getElementsByClass(harmony.FiguredBassIndication)[12], '<FiguredBassIndication figures: 6,5♮>') + if __name__ == '__main__': import music21 diff --git a/music21/musicxml/xmlToM21.py b/music21/musicxml/xmlToM21.py index 6fe094d26f..4a9a599829 100644 --- a/music21/musicxml/xmlToM21.py +++ b/music21/musicxml/xmlToM21.py @@ -5306,7 +5306,7 @@ def xmlToFiguredBass(self, mxFiguredBass) -> harmony.FiguredBassIndication: fb_number: str = '' fb_prefix: str = '' fb_suffix: str = '' - fb_extender: str = '' + if subElement.tag == 'figure': for el in subElement.findall('*'): if el.tag == 'figure-number': @@ -5314,8 +5314,8 @@ def xmlToFiguredBass(self, mxFiguredBass) -> harmony.FiguredBassIndication: fb_number = el.text # Get prefix and/or suffix. # The function returns an empty string if nothing is found. - fb_prefix = self._getFigurePrefixOrSuffix(subElement, 'prefix') - fb_suffix = self._getFigurePrefixOrSuffix(subElement, 'suffix') + fb_prefix = self._getFigurePrefixOrSuffix(subElement, 'prefix') + fb_suffix = self._getFigurePrefixOrSuffix(subElement, 'suffix') # collect information on extenders if el.tag == 'extend': @@ -5325,8 +5325,8 @@ def xmlToFiguredBass(self, mxFiguredBass) -> harmony.FiguredBassIndication: if not subElement.findall('extend'): fb_extenders.append(False) - # put prefix/suffix, extender and number together - if fb_prefix + fb_number + fb_extender + fb_suffix != '': + # put prefix/suffix, number and extenders together + if fb_prefix + fb_number + fb_suffix != '' or len(fb_extenders) != 0 : fb_strings.append(fb_prefix + fb_number + fb_suffix) else: # Warning because an empty figured-bass tag is not valid musixml. @@ -5344,12 +5344,11 @@ def xmlToFiguredBass(self, mxFiguredBass) -> harmony.FiguredBassIndication: self.lastFigureDuration = d.quarterLength fb_string = sep.join(fb_strings) - fbi = harmony.FiguredBassIndication(fb_string, extenders=fb_extenders) + fbi = harmony.FiguredBassIndication(fb_string, extenders=fb_extenders, part=self.parent.partId) # If a duration is provided, set length of the FigureBassIndication if d: fbi.quarterLength = d.quarterLength - print(fbi) self.stream.insert(offsetFbi, fbi) return fbi From 3e45f77f118800a86c6f6be885785c294ba4f814 Mon Sep 17 00:00:00 2001 From: Moritz Heffter <heffter@mh-freiburg.de> Date: Sat, 17 Jun 2023 20:44:50 +0200 Subject: [PATCH 55/58] commented out old mei import into score --- music21/mei/base.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/music21/mei/base.py b/music21/mei/base.py index a8aa4f07e0..8bcfc09f3f 100644 --- a/music21/mei/base.py +++ b/music21/mei/base.py @@ -471,10 +471,9 @@ def allPartsPresent(scoreElem) -> tuple[str, ...]: # Get information of possible <harm> tags in the score. If there are tags prepare a list to # store them and process them later. # TODO: Maybe to be put in a separate function e.g. like allPartsPresent - figuredBassQuery = f'.//{MEI_NS}fb' - if scoreElem.findall(figuredBassQuery): - environLocal.printDebug('harm tag found!') - partNs.append('fb') + #figuredBassQuery = f'.//{MEI_NS}fb' + #if scoreElem.findall(figuredBassQuery): + # environLocal.printDebug('harm tag found!') # here other <harm> elements (e.g. chordsymbols) can be added. # … 'if …' @@ -3618,10 +3617,11 @@ def scoreFromElement(elem, slurBundle): # TODO: Replace this with a better solution. # Extract collected <harm> information stored in the dict unter the 'fb' key harms: list[dict] | None = None - if 'fb' in parsed.keys(): - harms = parsed['fb'][0] - del parsed['fb'] - allPartNs = allPartNs[0:-1] + print('fb' in parsed.keys()) + #if 'fb' in parsed.keys(): + # harms = parsed['fb'][0] + # del parsed['fb'] + # allPartNs = allPartNs[0:-1] theScore = [stream.Part() for _ in range(len(allPartNs))] for i, eachN in enumerate(allPartNs): @@ -3632,11 +3632,11 @@ def scoreFromElement(elem, slurBundle): theScore = stream.Score(theScore) # loop through measures to insert harm elements from harms list at the right offsets - if harms: - for index, measureOffset in enumerate(theScore.measureOffsetMap().keys()): - hms = harms[index]['fb'] - for h in hms: - theScore.insert(measureOffset + h[0], h[1]) + #if harms: + # for index, measureOffset in enumerate(theScore.measureOffsetMap().keys()): + # hms = harms[index]['fb'] + # for h in hms: + # theScore.insert(measureOffset + h[0], h[1]) # put slurs in the Score theScore.append(list(slurBundle)) From f62fc349a013e8eb67bf106467c60cda39540774 Mon Sep 17 00:00:00 2001 From: mxordn <m.heffter@me.com> Date: Sun, 18 Jun 2023 00:03:13 +0200 Subject: [PATCH 56/58] added modified tests --- music21/harmony.py | 8 +++++--- music21/mei/base.py | 18 ------------------ music21/mei/test_base.py | 30 +++++++++++++++--------------- music21/musicxml/m21ToXml.py | 2 +- music21/musicxml/test_xmlToM21.py | 3 ++- music21/musicxml/xmlToM21.py | 25 +++++++++++-------------- 6 files changed, 34 insertions(+), 52 deletions(-) diff --git a/music21/harmony.py b/music21/harmony.py index d42ce41594..a96d42417a 100644 --- a/music21/harmony.py +++ b/music21/harmony.py @@ -2509,9 +2509,9 @@ class FiguredBassIndication(Harmony): The FiguredBassIndication object derives from the Harmony object and can be used in the following way: - >>> fbi = harmony.FiguredBassIndication('#,6#') + >>> fbi = harmony.FiguredBassIndication('#,6#', part='1') >>> fbi - <FiguredBassIndication figures: #,6#> + <FiguredBassIndication figures: #,6# part: 1> The single figures are stored as figuredBass.notation.Figure objects: >>> fbi.fig_notation.figures @@ -2520,7 +2520,9 @@ class FiguredBassIndication(Harmony): ''' isFigure: bool = True - part: str | None = None + part: str | None = None + _figs: str = '' + def __init__(self, figs: str | list | None = None, extenders: list[bool] | None = None , part: str | None=None, **keywords): super().__init__(**keywords) diff --git a/music21/mei/base.py b/music21/mei/base.py index 8bcfc09f3f..acd8ba4cd6 100644 --- a/music21/mei/base.py +++ b/music21/mei/base.py @@ -3235,15 +3235,12 @@ def measureFromElement(elem, backupNum, expectedNs, slurBundle=None, activeMeter # track the bar's duration maxBarDuration = None - print('=== EL', elem, '===') - # iterate all immediate children for eachElem in elem.iterfind('*'): # first get all information stored in <harm> tags. # They are stored on the same level as <staff>. if harmTag == eachElem.tag: harmElements['fb'].append(harmFromElement(eachElem)) - #print(' harms', harmElements, eachElem.get('n')) elif staffTag == eachElem.tag: staves[eachElem.get('n')] = stream.Measure(staffFromElement(eachElem, slurBundle=slurBundle), @@ -3614,14 +3611,6 @@ def scoreFromElement(elem, slurBundle): # document. Iterating the keys in "parsed" would not preserve the order. environLocal.printDebug('*** making the Score') - # TODO: Replace this with a better solution. - # Extract collected <harm> information stored in the dict unter the 'fb' key - harms: list[dict] | None = None - print('fb' in parsed.keys()) - #if 'fb' in parsed.keys(): - # harms = parsed['fb'][0] - # del parsed['fb'] - # allPartNs = allPartNs[0:-1] theScore = [stream.Part() for _ in range(len(allPartNs))] for i, eachN in enumerate(allPartNs): @@ -3631,13 +3620,6 @@ def scoreFromElement(elem, slurBundle): theScore[i].append(eachObj) theScore = stream.Score(theScore) - # loop through measures to insert harm elements from harms list at the right offsets - #if harms: - # for index, measureOffset in enumerate(theScore.measureOffsetMap().keys()): - # hms = harms[index]['fb'] - # for h in hms: - # theScore.insert(measureOffset + h[0], h[1]) - # put slurs in the Score theScore.append(list(slurBundle)) # TODO: when all the Slur objects are at the end, they'll only be outputted properly if the diff --git a/music21/mei/test_base.py b/music21/mei/test_base.py index 493fb8bc6f..bcbb318494 100644 --- a/music21/mei/test_base.py +++ b/music21/mei/test_base.py @@ -199,7 +199,7 @@ def testAllPartsPresent1(self): staffDefs[0].get = mock.MagicMock(return_value='1') elem = mock.MagicMock(spec_set=ETree.Element) elem.findall = mock.MagicMock(return_value=staffDefs) - expected = ['1', 'fb'] + expected = ['1'] actual = base.allPartsPresent(elem) self.assertSequenceEqual(expected, actual) @@ -212,7 +212,7 @@ def testAllPartsPresent2(self): staffDefs[i].get = mock.MagicMock(return_value=str(i + 1)) elem = mock.MagicMock(spec_set=ETree.Element) elem.findall = mock.MagicMock(return_value=staffDefs) - expected = ['1', '2', '3', '4', 'fb'] + expected = ['1', '2', '3', '4'] actual = base.allPartsPresent(elem) self.assertSequenceEqual(expected, actual) @@ -225,7 +225,7 @@ def testAllPartsPresent3(self): staffDefs[i].get = mock.MagicMock(return_value=str((i % 4) + 1)) elem = mock.MagicMock(spec_set=ETree.Element) elem.findall = mock.MagicMock(return_value=staffDefs) - expected = ['1', '2', '3', '4', 'fb'] + expected = ['1', '2', '3', '4'] actual = base.allPartsPresent(elem) self.assertSequenceEqual(expected, actual) @@ -3929,7 +3929,7 @@ def testMeasureUnit1(self, mockVoice, mockMeasure, elem.append(eachStaff) # @n="4" is in "expectedNs" but we're leaving it out as part of the test backupNum = 900 # should be ignored by measureFromElement() - expectedNs = ['1', '2', '3', '4', 'fb'] + expectedNs = ['1', '2', '3', '4'] slurBundle = mock.MagicMock(name='slurBundle') activeMeter = mock.MagicMock(name='activeMeter') activeMeter.barDuration = duration.Duration(4.0) @@ -3963,7 +3963,7 @@ def testMeasureUnit1(self, mockVoice, mockMeasure, for eachStaff in innerStaffs: mockStaffFE.assert_any_call(eachStaff, slurBundle=slurBundle) # ensure Measure.__init__() was called properly - self.assertEqual(len(expected) - 1, mockMeasure.call_count) + self.assertEqual(len(expected), mockMeasure.call_count) for i in range(len(innerStaffs)): mockMeasure.assert_any_call(i, number=int(elem.get('n'))) mockMeasure.assert_any_call([mockVoice.return_value], number=int(elem.get('n'))) @@ -3999,14 +3999,14 @@ def testMeasureIntegration1(self): elem.append(eachStaff) # @n="4" is in "expectedNs" but we're leaving it out as part of the test backupNum = 900 # should be ignored by measureFromElement() - expectedNs = ['1', '2', '3', '4', 'fb'] + expectedNs = ['1', '2', '3', '4'] slurBundle = spanner.SpannerBundle() activeMeter = meter.TimeSignature('8/8') # bet you thought this would be 4/4, eh? actual = base.measureFromElement(elem, backupNum, expectedNs, slurBundle, activeMeter) # ensure the right number and @n of parts - self.assertEqual(5, len(actual.keys())) + self.assertEqual(4, len(actual.keys())) for eachN in expectedNs: self.assertTrue(eachN in actual) # ensure the measure number is set properly, @@ -4055,7 +4055,7 @@ def testMeasureUnit2(self, mockVoice, mockMeasure, elem.append(eachStaff) # @n="4" is in "expectedNs" but we're leaving it out as part of the test backupNum = 900 # should be used by measureFromElement() - expectedNs = ['1', '2', '3', '4', 'fb'] + expectedNs = ['1', '2', '3', '4'] slurBundle = mock.MagicMock(name='slurBundle') activeMeter = mock.MagicMock(name='activeMeter') # this must be longer than Measure.duration.quarterLength @@ -4089,7 +4089,7 @@ def testMeasureUnit2(self, mockVoice, mockMeasure, for eachStaff in innerStaffs: mockStaffFE.assert_any_call(eachStaff, slurBundle=slurBundle) # ensure Measure.__init__() was called properly - self.assertEqual(len(expected) - 1, mockMeasure.call_count) + self.assertEqual(len(expected), mockMeasure.call_count) for i in range(len(innerStaffs)): mockMeasure.assert_any_call(i, number=backupNum) mockMeasure.assert_any_call([mockVoice.return_value], number=backupNum) @@ -4123,14 +4123,14 @@ def testMeasureIntegration2(self): elem.append(eachStaff) # @n="4" is in "expectedNs" but we're leaving it out as part of the test backupNum = 900 # should be used by measureFromElement() - expectedNs = ['1', '2', '3', '4', 'fb'] + expectedNs = ['1', '2', '3', '4'] slurBundle = spanner.SpannerBundle() activeMeter = meter.TimeSignature('8/8') # bet you thought this would be 4/4, eh? actual = base.measureFromElement(elem, backupNum, expectedNs, slurBundle, activeMeter) # ensure the right number and @n of parts (we expect one additional key, for the "rptboth") - self.assertEqual(6, len(actual.keys())) + self.assertEqual(5, len(actual.keys())) for eachN in expectedNs: self.assertTrue(eachN in actual) self.assertTrue('next @left' in actual) @@ -4151,7 +4151,7 @@ def testMeasureIntegration2(self): self.assertIsInstance(actual[eachN].rightBarline, bar.Repeat) self.assertEqual('final', actual[eachN].rightBarline.type) else: - self.assertEqual([], actual[eachN]['fb']) + self.assertEqual([], actual[eachN]) @mock.patch('music21.mei.base.staffFromElement') @mock.patch('music21.mei.base._correctMRestDurs') @@ -4176,7 +4176,7 @@ def testMeasureUnit3a(self, mockStaffDefFE, mockMeasure, staffElem = ETree.Element(staffTag, attrib={'n': '1'}) elem.append(staffElem) backupNum = 900 # should be ignored by measureFromElement() - expectedNs = ['1', 'fb'] + expectedNs = ['1'] slurBundle = mock.MagicMock(name='slurBundle') activeMeter = mock.MagicMock(name='activeMeter') # this must match Measure.duration.quarterLength @@ -4230,7 +4230,7 @@ def testMeasureUnit3b(self, mockEnviron, mockMeasure, staffElem = ETree.Element(staffTag, attrib={'n': '1'}) elem.append(staffElem) backupNum = 900 # should be ignored by measureFromElement() - expectedNs = ['1', 'fb'] + expectedNs = ['1'] slurBundle = mock.MagicMock(name='slurBundle') activeMeter = mock.MagicMock(name='activeMeter') # this must match Measure.duration.quarterLength @@ -4290,7 +4290,7 @@ def testMeasureIntegration3(self): actual = base.measureFromElement(elem, backupNum, expectedNs, slurBundle, activeMeter) # ensure the right number and @n of parts - self.assertEqual(['1', 'fb'], list(actual.keys())) + self.assertEqual(['1'], list(actual.keys())) # ensure the Measure has its expected Voice, BassClef, and Instrument self.assertEqual(backupNum, actual['1'].number) self.assertEqual(2, len(actual['1'])) diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index 6d1d9d0024..03cbfe12ae 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -4644,7 +4644,7 @@ def figuredBassToXml(self, f: harmony.FiguredBassIndication): >>> fbi = harmony.FiguredBassIndication() >>> fbi.quarterLength = 2 - >>> fbi.fig_noation = '#,6b' + >>> fbi.fig_notation = '#,6b' >>> MEX = musicxml.m21ToXml.MeasureExporter() diff --git a/music21/musicxml/test_xmlToM21.py b/music21/musicxml/test_xmlToM21.py index 1cadc6db01..46a8456f67 100644 --- a/music21/musicxml/test_xmlToM21.py +++ b/music21/musicxml/test_xmlToM21.py @@ -1515,7 +1515,8 @@ def testImportFiguredBassIndications(self): self.assertIs(len(s.flatten().getElementsByClass(harmony.FiguredBassIndication)), 12) s = converter.parse(xml_dir / 'piece01-bwv-1023-1-beginning.musicxml') - self.assertEqual(s.flatten().getElementsByClass(harmony.FiguredBassIndication)[12], '<FiguredBassIndication figures: 6,5♮>') + self.assertEqual(s.flatten().getElementsByClass(harmony.FiguredBassIndication)[12], + '<FiguredBassIndication figures: 6,5♮ part: P2>') if __name__ == '__main__': diff --git a/music21/musicxml/xmlToM21.py b/music21/musicxml/xmlToM21.py index 4a9a599829..5426f3eade 100644 --- a/music21/musicxml/xmlToM21.py +++ b/music21/musicxml/xmlToM21.py @@ -5265,27 +5265,25 @@ def xmlToChordSymbol( def xmlToFiguredBass(self, mxFiguredBass) -> harmony.FiguredBassIndication: # noinspection PyShadowingNames - ''' + """ Converts a figured bass tag in musicxml to a harmony.FiguredBassIndication object: >>> from xml.etree.ElementTree import fromstring as EL >>> MP = musicxml.xmlToM21.MeasureParser() - >>> fbStr = """ - <figured-bass> - <figure> - <figure-number>5</figure-number> - </figure> - <figure> - <figure-number>4</figure-number> - </figure> - </figured-bass> - """ + >>> fbStr = '''<figured-bass> + ... <figure> + ... <figure-number>5</figure-number> + ... </figure> + ... <figure> + ... <figure-number>4</figure-number> + ... </figure> + ... </figured-bass>''' >>> mxFigures = EL(fbStr) >>> fbi = MP.xmlToFiguredBass(mxFigures) >>> fbi - <FiguredBassIndication figures: 5,4> - ''' + <FiguredBassIndication figures: 5,4 part: > + """ fb_strings: list[str] = [] fb_extenders: list[bool] = [] @@ -5320,7 +5318,6 @@ def xmlToFiguredBass(self, mxFiguredBass) -> harmony.FiguredBassIndication: # collect information on extenders if el.tag == 'extend': if 'type' in el.attrib.keys(): - print((el.attrib['type'] in ['stop', 'continue', 'start'])) fb_extenders.append((el.attrib['type'] in ['stop', 'continue', 'start'])) if not subElement.findall('extend'): fb_extenders.append(False) From 36970def50dd5d0a49cfee46707a887d9e15ea9a Mon Sep 17 00:00:00 2001 From: mxordn <m.heffter@me.com> Date: Sun, 18 Jun 2023 01:09:23 +0200 Subject: [PATCH 57/58] cleanup comments --- music21/mei/base.py | 29 ++++++----------------------- music21/mei/test_base.py | 33 +++++++++++++-------------------- 2 files changed, 19 insertions(+), 43 deletions(-) diff --git a/music21/mei/base.py b/music21/mei/base.py index acd8ba4cd6..bab5d74e11 100644 --- a/music21/mei/base.py +++ b/music21/mei/base.py @@ -467,16 +467,6 @@ def allPartsPresent(scoreElem) -> tuple[str, ...]: partNs.append(staffDef.get('n')) if not partNs: raise MeiValidityError(_SEEMINGLY_NO_PARTS) - - # Get information of possible <harm> tags in the score. If there are tags prepare a list to - # store them and process them later. - # TODO: Maybe to be put in a separate function e.g. like allPartsPresent - #figuredBassQuery = f'.//{MEI_NS}fb' - #if scoreElem.findall(figuredBassQuery): - # environLocal.printDebug('harm tag found!') - # here other <harm> elements (e.g. chordsymbols) can be added. - # … 'if …' - return tuple(partNs) @@ -3237,11 +3227,7 @@ def measureFromElement(elem, backupNum, expectedNs, slurBundle=None, activeMeter # iterate all immediate children for eachElem in elem.iterfind('*'): - # first get all information stored in <harm> tags. - # They are stored on the same level as <staff>. - if harmTag == eachElem.tag: - harmElements['fb'].append(harmFromElement(eachElem)) - elif staffTag == eachElem.tag: + if staffTag == eachElem.tag: staves[eachElem.get('n')] = stream.Measure(staffFromElement(eachElem, slurBundle=slurBundle), number=int(elem.get('n', backupNum))) @@ -3254,6 +3240,11 @@ def measureFromElement(elem, backupNum, expectedNs, slurBundle=None, activeMeter environLocal.warn(_UNIMPLEMENTED_IMPORT.format('<staffDef>', '@n')) else: stavesWaiting[whichN] = staffDefFromElement(eachElem, slurBundle) + + # Get all information stored in <harm> tags. + # They are stored on the same level as <staff>. + elif harmTag == eachElem.tag: + harmElements['fb'].append(harmFromElement(eachElem)) elif eachElem.tag not in _IGNORE_UNPROCESSED: environLocal.printDebug(_UNPROCESSED_SUBELEMENT.format(eachElem.tag, elem.tag)) @@ -3267,9 +3258,6 @@ def measureFromElement(elem, backupNum, expectedNs, slurBundle=None, activeMeter # *start* of the <measure> in which it appears. staves[whichN].insert(0, eachObj) - # Add <harm> objects to the staves dict - #staves['fb'] = harmElements - # Add <harm> objects to the corrresponding staff within the Measure for fb in harmElements['fb']: offset = fb[0] @@ -3598,9 +3586,6 @@ def scoreFromElement(elem, slurBundle): # Get a tuple of all the @n attributes for the <staff> tags in this score. Each <staff> tag # corresponds to what will be a music21 Part. - - # UPDATE: If <harm> tags are found, they will also be collected as a separate 'part' - # to process them later. allPartNs = allPartsPresent(elem) # This is the actual processing. @@ -3610,8 +3595,6 @@ def scoreFromElement(elem, slurBundle): # We must iterate here over "allPartNs," which preserves the part-order found in the MEI # document. Iterating the keys in "parsed" would not preserve the order. environLocal.printDebug('*** making the Score') - - theScore = [stream.Part() for _ in range(len(allPartNs))] for i, eachN in enumerate(allPartNs): # set "atSoundingPitch" so transposition works diff --git a/music21/mei/test_base.py b/music21/mei/test_base.py index bcbb318494..312656dd74 100644 --- a/music21/mei/test_base.py +++ b/music21/mei/test_base.py @@ -212,7 +212,7 @@ def testAllPartsPresent2(self): staffDefs[i].get = mock.MagicMock(return_value=str(i + 1)) elem = mock.MagicMock(spec_set=ETree.Element) elem.findall = mock.MagicMock(return_value=staffDefs) - expected = ['1', '2', '3', '4'] + expected = list('1234') actual = base.allPartsPresent(elem) self.assertSequenceEqual(expected, actual) @@ -225,7 +225,7 @@ def testAllPartsPresent3(self): staffDefs[i].get = mock.MagicMock(return_value=str((i % 4) + 1)) elem = mock.MagicMock(spec_set=ETree.Element) elem.findall = mock.MagicMock(return_value=staffDefs) - expected = ['1', '2', '3', '4'] + expected = list('1234') actual = base.allPartsPresent(elem) self.assertSequenceEqual(expected, actual) @@ -3936,12 +3936,10 @@ def testMeasureUnit1(self, mockVoice, mockMeasure, # this must match Measure.duration.quarterLength # prepare the mock Measure objects returned by mockMeasure mockMeasRets = [mock.MagicMock(name=f'Measure {i + 1}') for i in range(4)] + expected = mockMeasRets # finish preparing "expected" below... for meas in mockMeasRets: meas.duration = mock.MagicMock(spec_set=duration.Duration) meas.duration.quarterLength = 4.0 # must match activeMeter.barDuration.quarterLength - # append figured bass stuff - mockMeasRets.append({'fb': []}) - expected = mockMeasRets # finish preparing "expected" below... mockMeasure.side_effect = lambda *x, **y: mockMeasRets.pop(0) # prepare mock of _makeBarlines() which returns "staves" mockMakeBarlines.side_effect = lambda elem, staves: staves @@ -4067,7 +4065,6 @@ def testMeasureUnit2(self, mockVoice, mockMeasure, for meas in mockMeasRets: meas.duration = mock.MagicMock(spec_set=duration.Duration) meas.duration.quarterLength = base._DUR_ATTR_DICT[None] # must be _DUR_ATTR_DICT[None] - mockMeasRets.append({'fb': []}) mockMeasure.side_effect = lambda *x, **y: mockMeasRets.pop(0) # prepare mock of _makeBarlines() which returns "staves" mockMakeBarlines.side_effect = lambda elem, staves: staves @@ -4140,18 +4137,15 @@ def testMeasureIntegration2(self): # (Note we can test all four parts together this time--- # the fourth should be indistinguishable) for eachN in expectedNs: - if isinstance(actual[eachN], stream.Measure): - self.assertEqual(backupNum, actual[eachN].number) - self.assertEqual(2, len(actual[eachN])) # first the Note, then the Barline - self.assertIsInstance(actual[eachN][0], stream.Voice) - self.assertEqual(1, len(actual[eachN][0])) - self.assertIsInstance(actual[eachN][0][0], note.Rest) - self.assertEqual(activeMeter.barDuration.quarterLength, - actual['4'][0][0].duration.quarterLength) - self.assertIsInstance(actual[eachN].rightBarline, bar.Repeat) - self.assertEqual('final', actual[eachN].rightBarline.type) - else: - self.assertEqual([], actual[eachN]) + self.assertEqual(backupNum, actual[eachN].number) + self.assertEqual(2, len(actual[eachN])) # first the Note, then the Barline + self.assertIsInstance(actual[eachN][0], stream.Voice) + self.assertEqual(1, len(actual[eachN][0])) + self.assertIsInstance(actual[eachN][0][0], note.Rest) + self.assertEqual(activeMeter.barDuration.quarterLength, + actual['4'][0][0].duration.quarterLength) + self.assertIsInstance(actual[eachN].rightBarline, bar.Repeat) + self.assertEqual('final', actual[eachN].rightBarline.type) @mock.patch('music21.mei.base.staffFromElement') @mock.patch('music21.mei.base._correctMRestDurs') @@ -4192,10 +4186,9 @@ def testMeasureUnit3a(self, mockStaffDefFE, mockMeasure, # prepare mock of staffDefFromElement() mockStaffDefFE.return_value = {'clef': mock.MagicMock(name='SomeClef')} # prepare the expected return value - expected = {'1': mockMeasure.return_value, 'fb': {'fb': []}} + expected = {'1': mockMeasure.return_value} actual = base.measureFromElement(elem, backupNum, expectedNs, slurBundle, activeMeter) - actual['fb'] = {'fb': []} self.assertDictEqual(expected, actual) # ensure staffFromElement() was called properly mockStaffFE.assert_called_once_with(staffElem, slurBundle=slurBundle) From a4b523ed87c9cc21ab55c7042f352d39663851ba Mon Sep 17 00:00:00 2001 From: mxordn <m.heffter@me.com> Date: Sun, 18 Jun 2023 01:37:28 +0200 Subject: [PATCH 58/58] second cleanup comments and tests --- music21/mei/test_base.py | 6 +++--- music21/musicxml/test_xmlToM21.py | 6 +++--- music21/musicxml/xmlToM21.py | 11 ----------- 3 files changed, 6 insertions(+), 17 deletions(-) diff --git a/music21/mei/test_base.py b/music21/mei/test_base.py index 312656dd74..012b439b17 100644 --- a/music21/mei/test_base.py +++ b/music21/mei/test_base.py @@ -4143,7 +4143,7 @@ def testMeasureIntegration2(self): self.assertEqual(1, len(actual[eachN][0])) self.assertIsInstance(actual[eachN][0][0], note.Rest) self.assertEqual(activeMeter.barDuration.quarterLength, - actual['4'][0][0].duration.quarterLength) + actual['4'][0][0].duration.quarterLength) self.assertIsInstance(actual[eachN].rightBarline, bar.Repeat) self.assertEqual('final', actual[eachN].rightBarline.type) @@ -4189,6 +4189,7 @@ def testMeasureUnit3a(self, mockStaffDefFE, mockMeasure, expected = {'1': mockMeasure.return_value} actual = base.measureFromElement(elem, backupNum, expectedNs, slurBundle, activeMeter) + self.assertDictEqual(expected, actual) # ensure staffFromElement() was called properly mockStaffFE.assert_called_once_with(staffElem, slurBundle=slurBundle) @@ -4237,10 +4238,9 @@ def testMeasureUnit3b(self, mockEnviron, mockMeasure, # prepare mock of staffFromElement(), which just needs to return several unique things mockStaffFE.return_value = 'staffFromElement() return value' # prepare the expected return value - expected = {'1': mockMeasure.return_value, 'fb': {'fb': []}} + expected = {'1': mockMeasure.return_value} actual = base.measureFromElement(elem, backupNum, expectedNs, slurBundle, activeMeter) - actual['fb'] = {'fb': []} self.assertDictEqual(expected, actual) # ensure staffFromElement() was called properly diff --git a/music21/musicxml/test_xmlToM21.py b/music21/musicxml/test_xmlToM21.py index 46a8456f67..24df55e7d4 100644 --- a/music21/musicxml/test_xmlToM21.py +++ b/music21/musicxml/test_xmlToM21.py @@ -1505,7 +1505,7 @@ def testImportImplicitMeasureNumber(self): self.assertIs(m.showNumber, stream.enums.ShowNumber.NEVER) def testImportFiguredBassIndications(self): - from music21 import converter + from music21 import converter, harmony xml_dir = common.getSourceFilePath() / 'musicxml' / 'lilypondTestSuite' s = converter.parse(xml_dir / '74a-FiguredBass.xml') @@ -1515,8 +1515,8 @@ def testImportFiguredBassIndications(self): self.assertIs(len(s.flatten().getElementsByClass(harmony.FiguredBassIndication)), 12) s = converter.parse(xml_dir / 'piece01-bwv-1023-1-beginning.musicxml') - self.assertEqual(s.flatten().getElementsByClass(harmony.FiguredBassIndication)[12], - '<FiguredBassIndication figures: 6,5♮ part: P2>') + fbi = harmony.FiguredBassIndication('6,5♮', part='P2') + self.assertEqual(s.flatten().getElementsByClass(harmony.FiguredBassIndication)[12], fbi) if __name__ == '__main__': diff --git a/music21/musicxml/xmlToM21.py b/music21/musicxml/xmlToM21.py index 5426f3eade..7a8578fdb1 100644 --- a/music21/musicxml/xmlToM21.py +++ b/music21/musicxml/xmlToM21.py @@ -842,7 +842,6 @@ def __init__(self): self.m21PartObjectsById = {} self.partGroupList = [] self.parts = [] - # self.fbis: list[harmony.FiguredBassIndication] | None = None self.musicXmlVersion = defaults.musicxmlVersion @@ -927,10 +926,6 @@ def xmlRootToScore(self, mxScore, inputM21=None): s.coreInsert(0.0, part) self.m21PartObjectsById[partId] = part - #if self.fbis: - # for fbi in self.fbis: - # s.insert(fbi[0], fbi[1]) - self.partGroups() # Mark all ArpeggioMarkSpanners as complete (now that we've parsed all the Parts) @@ -2364,12 +2359,6 @@ def applyMultiMeasureRest(self, r: note.Rest): self.stream.insert(0, self.activeMultiMeasureRestSpanner) self.activeMultiMeasureRestSpanner = None - #def appendFbis(self, fbi, measureOffset): - # absOffset = self.lastMeasureOffset + measureOffset - # if self.parent.fbis: - # self.parent.fbis.append((absOffset, fbi)) - # else: - # self.parent.fbis = [(absOffset, fbi)] # ----------------------------------------------------------------------------- class MeasureParser(XMLParserBase):