Skip to content

Commit ed245aa

Browse files
authored
Merge pull request #1580 from adhooge/pr-bend
FretBend supports alter (interval), prebend, and release
2 parents 9e84df1 + 7e8a800 commit ed245aa

4 files changed

Lines changed: 191 additions & 15 deletions

File tree

music21/articulations.py

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -76,18 +76,17 @@
7676
'''
7777
from __future__ import annotations
7878

79-
import typing as t
8079
import unittest
8180

8281
from music21 import base
8382
from music21 import common
8483
from music21.common.classTools import tempAttribute
84+
from music21.common.types import OffsetQL
8585
from music21 import environment
86-
from music21 import style
86+
from music21 import interval
8787
from music21 import spanner
88+
from music21 import style
8889

89-
if t.TYPE_CHECKING:
90-
from music21 import interval
9190

9291

9392
environLocal = environment.Environment('articulations')
@@ -582,10 +581,57 @@ class PullOff(spanner.Spanner, TechnicalIndication):
582581
pass
583582

584583
class FretBend(FretIndication):
585-
bendAlter: interval.IntervalBase|None = None
586-
preBend: t.Any = None
587-
release: t.Any = None
588-
withBar: t.Any = None
584+
'''
585+
Bend indication for fretted instruments
586+
587+
Bend in musicxml
588+
589+
`number` is an identifier for the articulation. Defaults to 0.
590+
591+
`bendAlter` is the interval defined by the bend,
592+
bend-alter in musicxml. Defaults to `None`.
593+
594+
`preBend` indicates if the string is bent before
595+
the onset of the note. Defaults to `False`.
596+
597+
`release` is the quarterLength value from the start
598+
of the note for releasing the bend, if any. Defaults to `None`.
599+
600+
`withBar` indicates what whammy bar movement is used, if any.
601+
MusicXML supports 'scoop' or 'dip'. Defaults to `None`.
602+
603+
>>> fb = articulations.FretBend(1, bendAlter=interval.ChromaticInterval(-2), release=0.5)
604+
>>> fb
605+
<music21.articulations.FretBend 1>
606+
>>> fb.preBend
607+
False
608+
>>> fb.withBar is None
609+
True
610+
>>> fb.bendAlter
611+
<music21.interval.ChromaticInterval -2>
612+
>>> fb.release
613+
0.5
614+
'''
615+
bendAlter: interval.Interval | interval.ChromaticInterval | None
616+
preBend: bool
617+
release: OffsetQL | None
618+
withBar: str | None
619+
620+
def __init__(
621+
self,
622+
number: int = 0,
623+
*,
624+
bendAlter: interval.Interval | interval.ChromaticInterval | None = None,
625+
preBend: bool = False,
626+
release: OffsetQL | None = None,
627+
withBar: str | None = None,
628+
**keywords
629+
):
630+
super().__init__(number=number, **keywords)
631+
self.bendAlter = bendAlter
632+
self.preBend = preBend
633+
self.release = release
634+
self.withBar = withBar
589635

590636
class FretTap(FretIndication):
591637
pass

music21/musicxml/m21ToXml.py

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5473,11 +5473,18 @@ def articulationToXmlTechnical(self, articulationMark: articulations.Articulatio
54735473
>>> mxOther = MEX.articulationToXmlTechnical(g)
54745474
>>> MEX.dump(mxOther)
54755475
<other-technical>unda maris</other-technical>
5476+
5477+
Same with technical marks not yet supported.
5478+
TODO: support HammerOn, PullOff, Hole, Arrow.
5479+
5480+
>>> h = articulations.HammerOn()
5481+
>>> mxOther = MEX.articulationToXmlTechnical(h)
5482+
>>> MEX.dump(mxOther)
5483+
<other-technical />
54765484
'''
54775485
# these technical have extra information
54785486
# TODO: hammer-on
54795487
# TODO: pull-off
5480-
# TODO: bend
54815488
# TODO: hole
54825489
# TODO: arrow
54835490
musicXMLTechnicalName = None
@@ -5489,7 +5496,7 @@ def articulationToXmlTechnical(self, articulationMark: articulations.Articulatio
54895496
musicXMLTechnicalName = 'other-technical'
54905497

54915498
# TODO: support additional technical marks listed above
5492-
if musicXMLTechnicalName in ('bend', 'hole', 'arrow'):
5499+
if musicXMLTechnicalName in ('hole', 'arrow'):
54935500
musicXMLTechnicalName = 'other-technical'
54945501

54955502
mxTechnicalMark = Element(musicXMLTechnicalName)
@@ -5523,7 +5530,10 @@ def articulationToXmlTechnical(self, articulationMark: articulations.Articulatio
55235530
if t.TYPE_CHECKING:
55245531
assert isinstance(articulationMark, articulations.FretIndication)
55255532
mxTechnicalMark.text = str(articulationMark.number)
5526-
5533+
if musicXMLTechnicalName == 'bend':
5534+
if t.TYPE_CHECKING:
5535+
assert isinstance(articulationMark, articulations.FretBend)
5536+
self.setBend(mxTechnicalMark, articulationMark)
55275537
# harmonic needs to check for whether it is artificial or natural, and
55285538
# whether it is base-pitch, sounding-pitch, or touching-pitch
55295539
if musicXMLTechnicalName == 'harmonic':
@@ -5539,6 +5549,67 @@ def articulationToXmlTechnical(self, articulationMark: articulations.Articulatio
55395549
# mxArticulations.append(mxArticulationMark)
55405550
return mxTechnicalMark
55415551

5552+
@staticmethod
5553+
def setBend(mxh: Element, bend: articulations.FretBend) -> None:
5554+
'''
5555+
Sets the bend-alter SubElement and the pre-bend,
5556+
release and with-bar SubElements when present.
5557+
5558+
Called from articulationToXmlTechnical
5559+
5560+
>>> from xml.etree.ElementTree import Element
5561+
>>> from fractions import Fraction
5562+
5563+
>>> MEXclass = musicxml.m21ToXml.MeasureExporter
5564+
5565+
>>> a = articulations.FretBend(bendAlter=interval.Interval(2))
5566+
>>> mxh = Element('bend')
5567+
>>> MEXclass.setBend(mxh, a)
5568+
>>> MEXclass.dump(mxh)
5569+
<bend>
5570+
<bend-alter>2</bend-alter>
5571+
</bend>
5572+
>>> mxh = Element('bend')
5573+
>>> a = articulations.FretBend(bendAlter=interval.Interval(2))
5574+
>>> a.preBend = True
5575+
>>> MEXclass.setBend(mxh, a)
5576+
>>> MEXclass.dump(mxh)
5577+
<bend>
5578+
<bend-alter>2</bend-alter>
5579+
<pre-bend />
5580+
</bend>
5581+
>>> mxh = Element('bend')
5582+
>>> a = articulations.FretBend(bendAlter=interval.Interval(-2), release=Fraction(1, 10080))
5583+
>>> MEXclass.setBend(mxh, a)
5584+
>>> MEXclass.dump(mxh)
5585+
<bend>
5586+
<bend-alter>-2</bend-alter>
5587+
<release offset="1" />
5588+
</bend>
5589+
>>> mxh = Element('bend')
5590+
>>> a = articulations.FretBend(bendAlter=interval.Interval(-2), withBar='scoop')
5591+
>>> MEXclass.setBend(mxh, a)
5592+
>>> MEXclass.dump(mxh)
5593+
<bend>
5594+
<bend-alter>-2</bend-alter>
5595+
<with-bar>scoop</with-bar>
5596+
</bend>
5597+
'''
5598+
bendAlterSubElement = SubElement(mxh, 'bend-alter')
5599+
alter = bend.bendAlter
5600+
if alter is not None:
5601+
bendAlterSubElement.text = str(alter.semitones)
5602+
if bend.preBend:
5603+
SubElement(mxh, 'pre-bend')
5604+
if bend.release is not None:
5605+
releaseSubElement = SubElement(mxh, 'release')
5606+
quarterLengthValue = bend.release
5607+
divisionsValue = int(defaults.divisionsPerQuarter * quarterLengthValue)
5608+
releaseSubElement.set('offset', str(divisionsValue))
5609+
if bend.withBar is not None:
5610+
withBarSubElement = SubElement(mxh, 'with-bar')
5611+
withBarSubElement.text = str(bend.withBar)
5612+
55425613
@staticmethod
55435614
def setHarmonic(mxh: Element, harm: articulations.StringHarmonic) -> None:
55445615
# noinspection PyShadowingNames

music21/musicxml/xmlObjects.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,7 @@
7171
# in method objectAttachedSpannersToTechnicals of m21ToXml.py
7272
# ('hammer-on', articulations.HammerOn),
7373
# ('pull-off', articulations.PullOff),
74-
# bend not implemented because it needs many subcomponents
75-
# ('bend', articulations.FretBend),
74+
('bend', articulations.FretBend),
7675
('tap', articulations.FretTap),
7776
('fret', articulations.FretIndication),
7877
('heel', articulations.OrganHeel),

music21/musicxml/xmlToM21.py

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3866,17 +3866,77 @@ def xmlTechnicalToArticulation(self, mxObj):
38663866
if tag in ('heel', 'toe'):
38673867
if mxObj.get('substitution') is not None:
38683868
tech.substitution = xmlObjects.yesNoToBoolean(mxObj.get('substitution'))
3869+
if tag == 'bend':
3870+
self.setBend(mxObj, tech)
38693871
# TODO: <bend> attr: accelerate, beats, first-beat, last-beat, shape (4.0)
38703872
# TODO: <bent> sub-elements: bend-alter, pre-bend, with-bar, release
38713873
# TODO: musicxml 4: release sub-element as offset attribute
3872-
3873-
38743874
self.setPlacement(mxObj, tech)
38753875
return tech
38763876
else:
38773877
environLocal.printDebug(f'Cannot translate {tag} in {mxObj}.')
38783878
return None
38793879

3880+
def setBend(self, mxh, bend):
3881+
'''
3882+
Gets the bend amplitude from the bend-alter tag,
3883+
then optional pre-bend and with-bar tags are processed,
3884+
as well as release which is converted from divisions to music21 time.
3885+
3886+
Called from xmlTechnicalToArticulation
3887+
3888+
>>> from xml.etree.ElementTree import fromstring as EL
3889+
>>> MP = musicxml.xmlToM21.MeasureParser()
3890+
3891+
>>> mxTech = EL('<bend><bend-alter>2</bend-alter></bend>')
3892+
>>> a = MP.xmlTechnicalToArticulation(mxTech)
3893+
>>> a
3894+
<music21.articulations.FretBend 0>
3895+
>>> a.bendAlter.semitones
3896+
2
3897+
>>> a.release
3898+
3899+
>>> a.withBar
3900+
3901+
>>> a.preBend
3902+
False
3903+
3904+
>>> mxTech = EL('<bend><bend-alter>-2</bend-alter><pre-bend/></bend>')
3905+
>>> a = MP.xmlTechnicalToArticulation(mxTech)
3906+
>>> a.bendAlter.semitones
3907+
-2
3908+
>>> a.preBend
3909+
True
3910+
3911+
>>> mxTech = EL('<bend><bend-alter>-2</bend-alter><release offset="1"/></bend>')
3912+
>>> a = MP.xmlTechnicalToArticulation(mxTech)
3913+
>>> a.bendAlter.semitones
3914+
-2
3915+
>>> a.release
3916+
Fraction(1, 10080)
3917+
3918+
>>> mxTech = EL('<bend><bend-alter>-1</bend-alter><with-bar>dip</with-bar></bend>')
3919+
>>> a = MP.xmlTechnicalToArticulation(mxTech)
3920+
>>> a.bendAlter.semitones
3921+
-1
3922+
>>> a.withBar
3923+
'dip'
3924+
'''
3925+
alter = mxh.find('bend-alter')
3926+
if alter is not None:
3927+
if alter.text is not None:
3928+
bend.bendAlter = interval.Interval(float(alter.text))
3929+
if mxh.find('pre-bend') is not None:
3930+
bend.preBend = True
3931+
if mxh.find('with-bar') is not None:
3932+
bend.withBar = mxh.find('with-bar').text
3933+
if mxh.find('release') is not None:
3934+
try:
3935+
divisions = float(mxh.find('release').get('offset'))
3936+
bend.release = opFrac(divisions / self.divisions)
3937+
except (ValueError, TypeError) as unused_err:
3938+
bend.release = 0.0
3939+
38803940
@staticmethod
38813941
def setHarmonic(mxh, harm):
38823942
'''

0 commit comments

Comments
 (0)