diff --git a/music21/articulations.py b/music21/articulations.py index d9d148579..e65200f41 100644 --- a/music21/articulations.py +++ b/music21/articulations.py @@ -83,11 +83,9 @@ from music21 import common from music21.common.classTools import tempAttribute from music21 import environment -from music21 import style from music21 import spanner +from music21 import style -if t.TYPE_CHECKING: - from music21 import interval environLocal = environment.Environment('articulations') @@ -582,10 +580,47 @@ class PullOff(spanner.Spanner, TechnicalIndication): pass class FretBend(FretIndication): - bendAlter: interval.IntervalBase|None = None - preBend: t.Any = None - release: t.Any = None - withBar: t.Any = None + ''' + Bend indication for fretted instruments + + Bend in musicxml + + Number is an identifier for the articulation. Defaults to 0. + + BendAlter is the interval of the bend in number of semitones, + bend-alter in musicxml. Defaults to None. + + PreBend indicates if the string is bent before + the onset of the note. Defaults to False. + + Release is the quarterLength value from the start + of the note for releasing the bend, if Any. Defaults to None. + + WithBar indicates if the bend is done using a whammy bar movement. Defaults to False. + + >>> fb = articulations.FretBend(bendAlter=interval.ChromaticInterval(-2), release=0.5) + >>> fb + + >>> fb.preBend + False + >>> fb.withBar + False + >>> fb.bendAlter + + >>> fb.release + 0.5 + ''' + bendAlter: interval.IntervalBase | None + preBend: bool + release: float | None + withBar: bool + + def __init__(self, number=0, bendAlter=None, preBend=False, release=None, withBar=False, **keywords): + super().__init__(**keywords) + self.bendAlter = bendAlter + self.preBend = preBend + self.release = release + self.withBar = withBar class FretTap(FretIndication): pass diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index aad81005e..a399281d6 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -65,7 +65,7 @@ from music21.musicxml import helpers from music21.musicxml.partStaffExporter import PartStaffExporterMixin from music21.musicxml import xmlObjects -from music21.musicxml.xmlObjects import MusicXMLExportException +from music21.musicxml.xmlObjects import MusicXMLExportException, booleanToYesNo from music21.musicxml.xmlObjects import MusicXMLWarning environLocal = environment.Environment('musicxml.m21ToXml') @@ -5411,11 +5411,18 @@ def articulationToXmlTechnical(self, articulationMark: articulations.Articulatio >>> mxOther = MEX.articulationToXmlTechnical(g) >>> MEX.dump(mxOther) unda maris + + Same with technical marks not yet supported. + TODO: support HammerOn, PullOff, Hole, Arrow. + + >>> h = articulations.HammerOn() + >>> mxOther = MEX.articulationToXmlTechnical(h) + >>> MEX.dump(mxOther) + ''' # these technical have extra information # TODO: hammer-on # TODO: pull-off - # TODO: bend # TODO: hole # TODO: arrow musicXMLTechnicalName = None @@ -5427,7 +5434,7 @@ def articulationToXmlTechnical(self, articulationMark: articulations.Articulatio musicXMLTechnicalName = 'other-technical' # TODO: support additional technical marks listed above - if musicXMLTechnicalName in ('bend', 'hole', 'arrow'): + if musicXMLTechnicalName in ('hole', 'arrow'): musicXMLTechnicalName = 'other-technical' mxTechnicalMark = Element(musicXMLTechnicalName) @@ -5461,7 +5468,8 @@ def articulationToXmlTechnical(self, articulationMark: articulations.Articulatio if t.TYPE_CHECKING: assert isinstance(articulationMark, articulations.FretIndication) mxTechnicalMark.text = str(articulationMark.number) - + if musicXMLTechnicalName == 'bend': + self.setBend(mxTechnicalMark, articulationMark) # harmonic needs to check for whether it is artificial or natural, and # whether it is base-pitch, sounding-pitch, or touching-pitch if musicXMLTechnicalName == 'harmonic': @@ -5477,6 +5485,46 @@ def articulationToXmlTechnical(self, articulationMark: articulations.Articulatio # mxArticulations.append(mxArticulationMark) return mxTechnicalMark + @staticmethod + def setBend(mxh: Element, bend: articulations.FretBend) -> None: + ''' + Sets the bend-alter SubElement and the pre-bend, + release and with-bar SubElements when present. + + Called from articulationToXmlTechnical + + >>> MEXClass = musicxml.m21ToXml.MeasureExporter + + >>> a = articulations.FretBend() + + >>> from xml.etree.ElementTree import Element + >>> mxh = Element('bend') + + >>> MEXClass.setBend(mxh, a) + >>> MEXClass.dump(mxh) + + + + ''' + bendAlterSubElement = SubElement(mxh, 'bend-alter') + alter = bend.bendAlter + if alter is not None: + # musicxml expects a number of semitones but not sure how to get it + # from a GeneralInterval + pass + if bend.preBend: + SubElement(mxh, 'pre-bend') + if bend.release is not None: + # Specifies where the release starts in terms of + # divisions relative to the current note. + releaseSubElement = SubElement(mxh, 'release') + quarterLengthValue = bend.release + divisionsValue = defaults.divisionsPerQuarter * quarterLengthValue + releaseSubElement.set('offset', str(divisionsValue)) + if bend.withBar is not None: + withBarSubElement = SubElement(mxh, 'with-bar') + withBarSubElement.text = str(bend.withBar) + @staticmethod def setHarmonic(mxh: Element, harm: articulations.StringHarmonic) -> None: # noinspection PyShadowingNames diff --git a/music21/musicxml/xmlObjects.py b/music21/musicxml/xmlObjects.py index fb241ea97..5aea4f8fc 100644 --- a/music21/musicxml/xmlObjects.py +++ b/music21/musicxml/xmlObjects.py @@ -67,12 +67,12 @@ ('stopped', articulations.Stopped), ('snap-pizzicato', articulations.SnapPizzicato), ('string', articulations.StringIndication), + ('bend', articulations.FretBend), # hammer-on and pull-off not implemented because handled # in method objectAttachedSpannersToTechnicals of m21ToXml.py # ('hammer-on', articulations.HammerOn), # ('pull-off', articulations.PullOff), - # bend not implemented because it needs many subcomponents - # ('bend', articulations.FretBend), + ('bend', articulations.FretBend), ('tap', articulations.FretTap), ('fret', articulations.FretIndication), ('heel', articulations.OrganHeel), diff --git a/music21/musicxml/xmlToM21.py b/music21/musicxml/xmlToM21.py index 711f541da..89860838a 100644 --- a/music21/musicxml/xmlToM21.py +++ b/music21/musicxml/xmlToM21.py @@ -3861,17 +3861,32 @@ def xmlTechnicalToArticulation(self, mxObj): if tag in ('heel', 'toe'): if mxObj.get('substitution') is not None: tech.substitution = xmlObjects.yesNoToBoolean(mxObj.get('substitution')) + if tag == 'bend': + self.setBend(mxObj, tech) # TODO: attr: accelerate, beats, first-beat, last-beat, shape (4.0) # TODO: sub-elements: bend-alter, pre-bend, with-bar, release # TODO: musicxml 4: release sub-element as offset attribute - - self.setPlacement(mxObj, tech) return tech else: environLocal.printDebug(f'Cannot translate {tag} in {mxObj}.') return None + @staticmethod + def setBend(mxh, bend): + alter = mxh.find('bend-alter') + if alter is not None: + if alter.text is not None: + bend.bendAlter = interval.Interval(int(alter.text)) + if mxh.find('pre-bend') is not None: + bend.preBend = True + if mxh.find('release') is not None: + try: + divisions = float(mxh.find('release').get('offset')) + bend.release = divisions / defaults.divisionsPerQuarter + except (ValueError, TypeError) as unused_err: + bend.release = 0.0 + @staticmethod def setHarmonic(mxh, harm): '''