Skip to content

Commit 032c51f

Browse files
Merge branch 'cuthbertLab:master' into midilyrics
2 parents 78f8cff + 41b6b6f commit 032c51f

10 files changed

Lines changed: 272 additions & 39 deletions

File tree

documentation/source/usersGuide/usersGuide_07_chords.ipynb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -526,7 +526,7 @@
526526
"cell_type": "markdown",
527527
"metadata": {},
528528
"source": [
529-
"You can find the third and fifth of the `Chord` with .third and .fifth. Note that these properties do not have `()` after them. This was a mistake in how we created `music21` and hopefully this will all be fixed and consistent soon:"
529+
"You can find the third and fifth of the `Chord` with `.third` and `.fifth`. Note that these properties do not have `()` after them. This was a mistake in how we created `music21` and hopefully this will all be fixed and consistent soon:"
530530
]
531531
},
532532
{

music21/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
'''
5151
from __future__ import annotations
5252

53-
__version__ = '9.6.0b3'
53+
__version__ = '9.6.0b4'
5454

5555
def get_version_tuple(vv):
5656
v = vv.split('.')

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/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
<class 'music21.base.Music21Object'>
2828
2929
>>> music21.VERSION_STR
30-
'9.6.0b3'
30+
'9.6.0b4'
3131
3232
Alternatively, after doing a complete import, these classes are available
3333
under the module "base":

music21/chord/__init__.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2318,7 +2318,7 @@ def inversion(
23182318
*,
23192319
find: bool = True,
23202320
testRoot: pitch.Pitch|None = None,
2321-
transposeOnSet: bool = True
2321+
transposeOnSet: bool = True,
23222322
) -> int|None:
23232323
'''
23242324
Find the chord's inversion or (if called with a number) set the chord to
@@ -2393,7 +2393,6 @@ def inversion(
23932393
Traceback (most recent call last):
23942394
music21.chord.ChordException: Could not invert chord: inversion may not exist
23952395
2396-
23972396
If testRoot is True then that temporary root is used instead of self.root().
23982397
23992398
Get the inversion for a seventh chord showing different roots
@@ -2430,7 +2429,7 @@ def inversion(
24302429
sets the value to be returned later, which might be useful for
24312430
cases where the chords are poorly spelled, or there is an added note.
24322431
2433-
* Changed in v8: chords without pitches
2432+
* Changed in v8: deal with chords without pitches
24342433
'''
24352434
if not self.pitches:
24362435
return -1
@@ -2460,11 +2459,12 @@ def inversion(
24602459
else:
24612460
return -1
24622461

2463-
def _setInversion(self,
2464-
newInversion: int,
2465-
rootPitch: pitch.Pitch,
2466-
transposeOnSet: bool
2467-
) -> None:
2462+
def _setInversion(
2463+
self,
2464+
newInversion: int,
2465+
rootPitch: pitch.Pitch,
2466+
transposeOnSet: bool,
2467+
) -> None:
24682468
'''
24692469
Helper function for inversion(int)
24702470
'''

music21/harmony.py

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -263,38 +263,47 @@ def _reprInternal(self):
263263
summary += ' '.join([p.name for p in self.pitches])
264264
return summary
265265

266-
# PRIVATE METHODS #
266+
# PROTECTED METHODS #
267267
def _parseFigure(self):
268268
'''
269-
subclass this in extensions (SO WHY IS IT PRIVATE???)
269+
subclass this in extensions (protected in TypeScript/Java speak)
270270
'''
271271
return
272272

273273
def _updatePitches(self):
274274
'''
275-
subclass this in extensions (SO WHY IS IT PRIVATE???)
275+
subclass this in extensions (protected in TypeScript/Java speak)
276276
'''
277277
return
278278

279-
def _updateFromParameters(self, root, bass, inversion: int|None = None):
279+
def _updateFromParameters(
280+
self,
281+
root: str|pitch.Pitch|None,
282+
bass: str|pitch.Pitch|None,
283+
inversion: int|None = None
284+
) -> None:
280285
'''
281286
This method must be called twice, once before the pitches
282287
are rendered, and once after. This is because after the pitches
283-
are rendered, the root() and bass() becomes reset by the chord class,
288+
are rendered, the root() and bass() become reset by the chord class,
284289
but we want the objects to retain their initial root, bass, and inversion.
285290
'''
286291
if root and isinstance(root, str):
287292
root = common.cleanedFlatNotation(root)
288293
self.root(pitch.Pitch(root, octave=3))
289294
elif root is not None:
290295
self.root(root)
296+
297+
# set inversion first...
298+
if inversion is not None:
299+
self.inversion(inversion, transposeOnSet=True)
300+
301+
# and then bass.
291302
if bass and isinstance(bass, str):
292303
bass = common.cleanedFlatNotation(bass)
293304
self.bass(pitch.Pitch(bass, octave=3), allow_add=True)
294305
elif bass is not None:
295306
self.bass(bass, allow_add=True)
296-
if inversion is not None:
297-
self.inversion(inversion, transposeOnSet=True)
298307

299308
# PUBLIC PROPERTIES #
300309

@@ -1609,7 +1618,7 @@ class ChordSymbol(Harmony):
16091618
# INITIALIZER #
16101619

16111620
def __init__(self,
1612-
figure=None,
1621+
figure: str|None = None,
16131622
root: pitch.Pitch|str|None = None,
16141623
bass: pitch.Pitch|str|None = None,
16151624
inversion: int|None = None,
@@ -2473,7 +2482,13 @@ class NoChord(ChordSymbol):
24732482
>>> nc2.pitches
24742483
()
24752484
'''
2476-
def __init__(self, figure=None, kind='none', kindStr=None, **keywords):
2485+
def __init__(
2486+
self,
2487+
figure: str|None = None,
2488+
kind: str|None = 'none',
2489+
kindStr: str|None = None,
2490+
**keywords
2491+
):
24772492
super().__init__(figure, kind=kind, kindStr=kindStr or figure or 'N.C.', **keywords)
24782493

24792494
if self._figure is None:
@@ -2655,6 +2670,17 @@ def testChordSymbolSetsBassOctave(self):
26552670
b = d.bass()
26562671
self.assertEqual(b.nameWithOctave, 'E-3')
26572672

2673+
def testHarmonyPreservesInversionAndBass(self):
2674+
'''
2675+
Test that bass is preserved even when both bass and inversion are given
2676+
'''
2677+
explicitFm6 = ChordSymbol(root='F', bass='A-', inversion=1, kind='minor')
2678+
self.assertEqual(explicitFm6.inversion(), 1)
2679+
self.assertEqual(explicitFm6.bass(find=False).name, 'A-')
2680+
self.assertEqual(explicitFm6.root(find=False).name, 'F')
2681+
self.assertLess(explicitFm6.bass(find=False).octave,
2682+
explicitFm6.root(find=False).octave)
2683+
26582684
def testClassSortOrderHarmony(self):
26592685
'''
26602686
This tests a former bug in getContextByClass

music21/musicxml/m21ToXml.py

Lines changed: 77 additions & 4 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
@@ -5951,7 +6022,6 @@ def chordSymbolToXml(self,
59516022
self.setPrintStyle(mxHarmony, cs)
59526023

59536024
csRoot = cs.root()
5954-
csBass = cs.bass(find=False)
59556025
# TODO: do not look at ._attributes
59566026
if cs._roman is not None:
59576027
mxFunction = SubElement(mxHarmony, 'function')
@@ -5994,6 +6064,9 @@ def chordSymbolToXml(self,
59946064
mxInversion = SubElement(mxHarmony, 'inversion')
59956065
mxInversion.text = str(csInv)
59966066

6067+
# first -- go with the already defined bass, from overrides, etc.
6068+
# but if that is not defined, then find the bass itself.
6069+
csBass = cs.bass(find=False) or cs.bass(find=True)
59976070
if csBass is not None and (csRoot is None or csRoot.name != csBass.name):
59986071
# TODO.. reuse above from Root
59996072
mxBass = SubElement(mxHarmony, 'bass')

0 commit comments

Comments
 (0)