From e0687be4c08449819627fd8059d69197e50b3810 Mon Sep 17 00:00:00 2001 From: Myke Cuthbert Date: Wed, 8 Sep 2021 14:25:34 -0400 Subject: [PATCH 1/2] Typing fixes and improvements to Only show one copy of music21 software in MusicXML output. Add more typing information in musicxml processing. --- music21/converter/__init__.py | 6 +- music21/instrument.py | 8 ++- music21/metadata/__init__.py | 4 +- music21/musicxml/m21ToXml.py | 11 +++- music21/musicxml/xmlToM21.py | 112 +++++++++++++++++----------------- music21/stream/__init__.py | 2 +- 6 files changed, 78 insertions(+), 65 deletions(-) diff --git a/music21/converter/__init__.py b/music21/converter/__init__.py index e8d6377dd9..2b21ca70ab 100644 --- a/music21/converter/__init__.py +++ b/music21/converter/__init__.py @@ -697,9 +697,9 @@ def parseURL(self, url, *, format=None, number=None, self.setSubconverterFromFormat(useFormat) self.subConverter.keywords = keywords self.subConverter.parseFile(fp, number=number) - self.stream.filePath = fp - self.stream.fileNumber = number - self.stream.fileFormat = useFormat + self.stream.filePath = fp # These are attributes defined outside of + self.stream.fileNumber = number # __init__ and will be moved to + self.stream.fileFormat = useFormat # Metadata in v8. # -----------------------------------------------------------------------# # Subconverters diff --git a/music21/instrument.py b/music21/instrument.py index df7942fa3d..5e4b2bc47a 100644 --- a/music21/instrument.py +++ b/music21/instrument.py @@ -24,6 +24,7 @@ import copy import unittest import sys +from typing import Optional from collections import OrderedDict from typing import Optional @@ -40,9 +41,10 @@ from music21 import environment _MOD = 'instrument' environLocal = environment.Environment(_MOD) +StreamType = stream.StreamType -def unbundleInstruments(streamIn, *, inPlace=False): +def unbundleInstruments(streamIn: StreamType, *, inPlace=False) -> Optional[StreamType]: # noinspection PyShadowingNames ''' takes a :class:`~music21.stream.Stream` that has :class:`~music21.note.NotRest` objects @@ -79,7 +81,7 @@ def unbundleInstruments(streamIn, *, inPlace=False): return s -def bundleInstruments(streamIn, *, inPlace=False): +def bundleInstruments(streamIn: stream.Stream, *, inPlace=False) -> Optional[stream.Stream]: # noinspection PyShadowingNames ''' >>> up1 = note.Unpitched() @@ -169,7 +171,7 @@ def __init__(self, instrumentName=None): self.highestNote = None # define interval to go from written to sounding - self.transposition = None + self.transposition: Optional[interval.Interval] = None self.inGMPercMap = False self.soundfontFn = None # if defined... diff --git a/music21/metadata/__init__.py b/music21/metadata/__init__.py index af86caecd0..67755db1e4 100644 --- a/music21/metadata/__init__.py +++ b/music21/metadata/__init__.py @@ -209,8 +209,8 @@ def __init__(self, *args, **keywords): self.software = [defaults.software] # Copyright can be None or a copyright object - # TODO: Change to property to prevent text setting - # (but need to regenerate CoreCorpus() after doing so.) + # TODO: Change to property to prevent setting as a plain string + # (but need to regenerate CoreCorpus() after doing so.) self.copyright = None # a dictionary of Text elements, where keys are work id strings diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index 60542025d6..6a49263c0e 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -2317,11 +2317,20 @@ def setEncoding(self): # TODO: encoder if self.scoreMetadata is not None: + found_m21_already = False for software in self.scoreMetadata.software: + if 'music21 v.' in software: + if found_m21_already: + # only write out one copy of the music21 software + # tag. First one should be current version. + continue + else: + found_m21_already = True mxSoftware = SubElement(mxEncoding, 'software') mxSoftware.text = software - else: + # there will not be a music21 software tag if no scoreMetadata + # if not for this. mxSoftware = SubElement(mxEncoding, 'software') mxSoftware.text = defaults.software diff --git a/music21/musicxml/xmlToM21.py b/music21/musicxml/xmlToM21.py index d84fba2aea..5ed39c2fca 100644 --- a/music21/musicxml/xmlToM21.py +++ b/music21/musicxml/xmlToM21.py @@ -108,7 +108,7 @@ def textNotNone(mxObj): return True -def textStripValid(mxObj): +def textStripValid(mxObj: ET.Element): ''' returns True if textNotNone(mxObj) and mxObj.text.strip() is not empty @@ -131,7 +131,7 @@ def textStripValid(mxObj): return True -def musicXMLTypeToType(value): +def musicXMLTypeToType(value: str) -> str: ''' Utility function to convert a MusicXML duration type to an music21 duration type. @@ -1273,7 +1273,9 @@ def xmlMetadata(self, el=None, inputM21=None): if inputM21 is None: return md - def identificationToMetadata(self, identification, inputM21=None): + def identificationToMetadata(self, + identification: ET.Element, + inputM21: Optional[metadata.Metadata] = None): ''' Convert an tag, containing tags, tags, and tag. @@ -1302,30 +1304,7 @@ def identificationToMetadata(self, identification, inputM21=None): encoding = identification.find('encoding') if encoding is not None: - # TODO: encoder (text + type = role) multiple - # TODO: encoding date multiple - # TODO: encoding-description (string) multiple - for software in encoding.findall('software'): - if textStripValid(software): - md.software.append(software.text.strip()) - - for supports in encoding.findall('supports'): - # todo: element: required - # todo: type: required -- not sure of the difference between this and value - # though type is yes-no while value is string - attr = supports.get('attribute') - value = supports.get('value') - if value is None: - value = supports.get('type') - - # found in wild: element=accidental type="no" -- No accidentals are indicated - # found in wild: transpose - # found in wild: beam - # found in wild: stem - if (attr, value) == ('new-system', 'yes'): - self.definesExplicitSystemBreaks = True - elif (attr, value) == ('new-page', 'yes'): - self.definesExplicitPageBreaks = True + self.processEncoding(encoding, md) # TODO: source # TODO: relation @@ -1348,7 +1327,35 @@ def identificationToMetadata(self, identification, inputM21=None): if inputM21 is None: return md - def creatorToContributor(self, creator, inputM21=None): + def processEncoding(self, encoding: ET.Element, md: metadata.Metadata): + # TODO: encoder (text + type = role) multiple + # TODO: encoding date multiple + # TODO: encoding-description (string) multiple + for software in encoding.findall('software'): + if textStripValid(software): + md.software.append(software.text.strip()) + + for supports in encoding.findall('supports'): + # todo: element: required + # todo: type: required -- not sure of the difference between this and value + # though type is yes-no while value is string + attr = supports.get('attribute') + value = supports.get('value') + if value is None: + value = supports.get('type') + + # found in wild: element=accidental type="no" -- No accidentals are indicated + # found in wild: transpose + # found in wild: beam + # found in wild: stem + if (attr, value) == ('new-system', 'yes'): + self.definesExplicitSystemBreaks = True + elif (attr, value) == ('new-page', 'yes'): + self.definesExplicitPageBreaks = True + + def creatorToContributor(self, + creator: ET.Element, + inputM21: Optional[metadata.primitives.Contributor] = None): # noinspection PyShadowingNames ''' Given a tag, fill the necessary parameters of a Contributor. @@ -1768,7 +1775,6 @@ def copy_into_partStaff(source, target, omitTheseElementIds): staffGroup.style.hideObjectOnPrint = True # in truth, hide the name, not the brace self.parent.stream.insert(0, staffGroup) - def _getStaffExclude( self, staffReference: StaffReferenceType, @@ -1807,7 +1813,7 @@ def _getUniqueStaffKeys(self) -> List[int]: post.sort() return post - def xmlMeasureToMeasure(self, mxMeasure): + def xmlMeasureToMeasure(self, mxMeasure: ET.Element) -> stream.Measure: # noinspection PyShadowingNames ''' Convert a measure element to a Measure, using @@ -1888,7 +1894,7 @@ def xmlMeasureToMeasure(self, mxMeasure): return m - def updateTransposition(self, newTransposition): + def updateTransposition(self, newTransposition: interval.Interval): ''' As you might expect, a measureParser that reveals a change in transposition is going to have an effect on the @@ -1936,7 +1942,7 @@ def updateTransposition(self, newTransposition): self.activeInstrument.transposition = newTransposition self.atSoundingPitch = False - def setLastMeasureInfo(self, m): + def setLastMeasureInfo(self, m: stream.Measure): # noinspection PyShadowingNames ''' Sets self.lastMeasureNumber and self.lastMeasureSuffix from the measure, @@ -2017,7 +2023,7 @@ def setLastMeasureInfo(self, m): ts = meter.TimeSignature('4/4') self.lastTimeSignature = ts - def adjustTimeAttributesFromMeasure(self, m): + def adjustTimeAttributesFromMeasure(self, m: stream.Measure): ''' Adds padAsAnacrusis to pickup measures and other measures that do not fill the whole tile, if the first measure of the piece, or @@ -2109,7 +2115,7 @@ def adjustTimeAttributesFromMeasure(self, m): self.lastMeasureOffset += mOffsetShift - def applyMultiMeasureRest(self, r): + def applyMultiMeasureRest(self, r: note.Rest): ''' If there is an active MultiMeasureRestSpanner, add the Rest, r, to it: @@ -2201,11 +2207,6 @@ class MeasureParser(XMLParserBase): 'bookmark': None, # Note: is handled separately... } - - # TODO: editorial, i.e., footnote and level - # staves: see separateOutPartStaves() - # TODO: part-symbol - # not to be done: directive DEPRECATED since MusicXML 2.0 def __init__(self, mxMeasure=None, parent=None): super().__init__() @@ -2460,7 +2461,7 @@ def parse(self): self.fullMeasureRest = True # it might already be True because a rest had a "measure='yes'" attribute - def xmlBackup(self, mxObj): + def xmlBackup(self, mxObj: ET.Element): ''' Parse a backup tag by changing :attr:`offsetMeasureNote`. @@ -2491,7 +2492,7 @@ def xmlBackup(self, mxObj): # https://github.com/cuthbertLab/music21/issues/971 self.offsetMeasureNote = max(self.offsetMeasureNote, 0.0) - def xmlForward(self, mxObj): + def xmlForward(self, mxObj: ET.Element): ''' Parse a forward tag by changing :attr:`offsetMeasureNote`. ''' @@ -2514,7 +2515,7 @@ def xmlForward(self, mxObj): # xmlToNote() sets None self.endedWithForwardTag = r - def xmlPrint(self, mxPrint): + def xmlPrint(self, mxPrint: ET.Element): ''' handles changes in pages, numbering, layout, etc. so can generate PageLayout, SystemLayout, or StaffLayout @@ -2574,7 +2575,7 @@ def hasSystemLayout(): # TODO: part-abbreviation display # TODO: print-attributes: staff-spacing, blank-page; skip deprecated staff-spacing - def xmlToNote(self, mxNote): + def xmlToNote(self, mxNote: ET.Element) -> None: ''' Handles everything for creating a Note or Rest or Chord @@ -2667,7 +2668,7 @@ def xmlToNote(self, mxNote): self.offsetMeasureNote += offsetIncrement self.endedWithForwardTag = None - def xmlToChord(self, mxNoteList): + def xmlToChord(self, mxNoteList: List[ET.Element]) -> chord.Chord: # noinspection PyShadowingNames ''' Given an a list of mxNotes, fill the necessary parameters @@ -2762,9 +2763,6 @@ def xmlToSimpleNote(self, mxNote, freeSpanners=True) -> Union[note.Note, note.Un The `spannerBundle` parameter can be a list or a Stream for storing and processing Spanner objects. - If inputM21 is not `None` then that object is used - for translating. Otherwise a new Note is created. - if freeSpanners is False then pending spanners will not be freed. >>> from xml.etree.ElementTree import fromstring as EL @@ -2804,7 +2802,7 @@ def xmlToSimpleNote(self, mxNote, freeSpanners=True) -> Union[note.Note, note.Un ''' d = self.xmlToDuration(mxNote) - n = None + n: Union[note.Note, note.Unpitched] mxUnpitched = mxNote.find('unpitched') if mxUnpitched is None: @@ -2844,7 +2842,7 @@ def xmlToSimpleNote(self, mxNote, freeSpanners=True) -> Union[note.Note, note.Un # beam and beams - def xmlToBeam(self, mxBeam, inputM21=None): + def xmlToBeam(self, mxBeam: ET.Element, inputM21=None): # noinspection PyShadowingNames ''' given an mxBeam object return a :class:`~music21.beam.Beam` object @@ -5119,21 +5117,25 @@ def parseAttributesTag(self, mxAttributes): self.activeAttributes = mxAttributes for mxSub in mxAttributes: tag = mxSub.tag - meth = None - # key, clef, time, details + # clef, key, measure-style, time, staff-details if tag in self.attributeTagsToMethods: meth = getattr(self, self.attributeTagsToMethods[tag]) - - if meth is not None: meth(mxSub) - elif tag == 'staves': - self.staves = int(mxSub.text) + # NOT to be done: directive -- deprecated since v2. elif tag == 'divisions': self.divisions = common.opFrac(float(mxSub.text)) + # TODO: for-part + # TODO: instruments -- int if more than one instrument plays most of the time + # TODO: part-symbol + elif tag == 'staves': + self.staves = int(mxSub.text) elif tag == 'transpose': self.transposition = self.xmlTransposeToInterval(mxSub) # environLocal.warn('Got a transposition of ', str(self.transposition) ) + # footnote, level + self.setEditorial(mxAttributes, self.stream) + if self.parent is not None: self.parent.lastDivisions = self.divisions self.parent.activeAttributes = self.activeAttributes diff --git a/music21/stream/__init__.py b/music21/stream/__init__.py index ae7150a1b2..48be124a98 100644 --- a/music21/stream/__init__.py +++ b/music21/stream/__init__.py @@ -14,7 +14,7 @@ from music21.exceptions21 import StreamException, ImmutableStreamException from music21.stream.base import ( Stream, Opus, Score, Part, PartStaff, Measure, Voice, - SpannerStorage, VariantStorage, System, + SpannerStorage, VariantStorage, System, StreamType ) from music21.stream import core from music21.stream import enums From 4a8f2da78fabbccc7abddd0ff26c3d7ef2f0c1f9 Mon Sep 17 00:00:00 2001 From: Myke Cuthbert Date: Wed, 8 Sep 2021 19:29:57 -0400 Subject: [PATCH 2/2] fix lint, speed up Pitch.ps = 53.0 etc. --- music21/chord/__init__.py | 2 +- music21/instrument.py | 1 - music21/musicxml/m21ToXml.py | 6 +++++- music21/musicxml/xmlToM21.py | 3 +++ music21/pitch.py | 28 ++++++++++++++++++---------- music21/vexflow/toMusic21j.py | 2 +- 6 files changed, 28 insertions(+), 14 deletions(-) diff --git a/music21/chord/__init__.py b/music21/chord/__init__.py index 00b2a96dcb..96dee31a2d 100644 --- a/music21/chord/__init__.py +++ b/music21/chord/__init__.py @@ -4228,7 +4228,7 @@ def commonName(self): Changed in v5.5: special cases for checking enharmonics in some cases Changed in v6.5: better handling of 0-, 1-, and 2-pitchClass and microtonal chords. - Changed in v7: Inversions of augmented triads are used. + Changed in v7: Inversions of augmented sixth-chords are specified. ''' if any(not p.isTwelveTone() for p in self.pitches): return 'microtonal chord' diff --git a/music21/instrument.py b/music21/instrument.py index 5e4b2bc47a..61c3c418b1 100644 --- a/music21/instrument.py +++ b/music21/instrument.py @@ -24,7 +24,6 @@ import copy import unittest import sys -from typing import Optional from collections import OrderedDict from typing import Optional diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index 6a49263c0e..48bcc35b5a 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -903,6 +903,9 @@ def setColor(self, mxObject, m21Object): ''' Sets mxObject['color'] to a normalized version of m21Object.style.color ''' + # we repeat 'color' rather than just letting setStyleAttributes + # handle it, because otherwise it will run the expensive + # hyphenToCamelCase routine on something called on each note. self.setStyleAttributes(mxObject, m21Object, 'color', 'color') if 'color' in mxObject.attrib: # set mxObject.attrib['color'] = normalizeColor(mxObject.attrib['color']) @@ -969,7 +972,6 @@ def setPosition(self, mxObject, m21Object): set positioning information for an mxObject from default-x, default-y, relative-x, relative-y from the .style attribute's absoluteX, relativeX, etc. attributes. - ''' musicXMLNames = ('default-x', 'default-y', 'relative-x', 'relative-y') m21Names = ('absoluteX', 'absoluteY', 'relativeX', 'relativeY') @@ -5785,6 +5787,8 @@ def beamToXml(self, beamObject): # not to be done: repeater (deprecated) self.setColor(mxBeam, beamObject) + # again, we pass the name 'fan' twice so we don't have to run + # hyphenToCamelCase on it. self.setStyleAttributes(mxBeam, beamObject, 'fan', 'fan') return mxBeam diff --git a/music21/musicxml/xmlToM21.py b/music21/musicxml/xmlToM21.py index 5ed39c2fca..e0b02c7135 100644 --- a/music21/musicxml/xmlToM21.py +++ b/music21/musicxml/xmlToM21.py @@ -475,6 +475,9 @@ def setColor(self, mxObject, m21Object): ''' Sets m21Object.style.color to be the same as color... ''' + # we repeat 'color' rather than just letting setStyleAttributes + # handle it, because otherwise it will run the expensive + # hyphenToCamelCase routine on something called on each note. self.setStyleAttributes(mxObject, m21Object, 'color', 'color') def setFont(self, mxObject, m21Object): diff --git a/music21/pitch.py b/music21/pitch.py index 18b5773557..c3a18afdd2 100644 --- a/music21/pitch.py +++ b/music21/pitch.py @@ -48,6 +48,16 @@ 'A': 9, 'B': 11, } +NATURAL_PCS = (0, 2, 4, 5, 7, 9, 11) +STEPREF_REVERSED = { + 0: 'C', + 2: 'D', + 4: 'E', + 5: 'F', + 7: 'G', + 9: 'A', + 11: 'B', +} STEPNAMES = {'C', 'D', 'E', 'F', 'G', 'A', 'B'} # set STEP_TO_DNN_OFFSET = {'C': 0, 'D': 1, 'E': 2, 'F': 3, 'G': 4, 'A': 5, 'B': 6} @@ -231,12 +241,14 @@ def _convertPsToStep(ps) -> Tuple[str, 'Accidental', 'Microtone', int]: >>> pitch._convertPsToStep(42.999739) ('G', , , 0) ''' - name = '' - if isinstance(ps, int): pc = ps % 12 alter = 0 micro = 0 + elif ps == int(ps): + pc = int(ps) % 12 + alter = 0 + micro = 0 else: # rounding here is essential ps = round(ps, PITCH_SPACE_SIG_DIGITS) @@ -274,7 +286,7 @@ def _convertPsToStep(ps) -> Tuple[str, 'Accidental', 'Microtone', int]: octShift = 0 # check for unnecessary enharmonics - if pc in (4, 11) and alter == 1: + if alter == 1 and pc in (4, 11): acc = Accidental(0) pcName = (pc + 1) % 12 # if a B, we are shifting out of this octave, and need to get @@ -282,10 +294,9 @@ def _convertPsToStep(ps) -> Tuple[str, 'Accidental', 'Microtone', int]: if pc == 11: octShift = 1 # its a natural; nothing to do - elif pc in STEPREF.values(): - acc = Accidental(0 + alter) + elif pc in NATURAL_PCS: # 0, 2, 4, 5, 7, 9, 11 + acc = Accidental(0 + alter) # alter is usually 0 unless half-sharp. pcName = pc - elif (pc - 1) in (0, 5, 7) and alter >= 1: # is this going to be a C##, F##, G##? acc = Accidental(alter - 1) pcName = pc + 1 @@ -306,10 +317,7 @@ def _convertPsToStep(ps) -> Tuple[str, 'Accidental', 'Microtone', int]: else: # pragma: no cover raise PitchException(f'cannot match condition for pc: {pc}') - for key, value in STEPREF.items(): - if pcName == value: - name = key - break + name = STEPREF_REVERSED.get(pcName, '') # create a micro object always if micro != 0: diff --git a/music21/vexflow/toMusic21j.py b/music21/vexflow/toMusic21j.py index dc842c4fa9..7e0bb29021 100644 --- a/music21/vexflow/toMusic21j.py +++ b/music21/vexflow/toMusic21j.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # ------------------------------------------------------------------------------ -# Name: vexflow/toM21j.py +# Name: vexflow/toMusic21j.py # Purpose: music21 classes for converting music21 objects to music21j # # Authors: Michael Scott Cuthbert