Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/figuredbass object #1614

Merged
merged 10 commits into from
Jul 2, 2023
181 changes: 166 additions & 15 deletions music21/figuredBass/notation.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,36 @@
(2,): (6, 4, 2),
}

prefixes = ['+', '#', '++', '##']
suffixes = ['\\']

modifiersDictXmlToM21 = {
'sharp': '#',
'flat': 'b',
'natural': '\u266e',
'double-sharp': '##',
'flat-flat': 'bb',
'backslash': '\\',
'slash': '/',
'cross': '+'
}

modifiersDictM21ToXml = {
'#': 'sharp',
'b': 'flat',
'##': 'double-sharp',
'bb': 'flat-flat',
'\\': 'backslash',
'/': 'slash',
'+': 'sharp',
'\u266f': 'sharp',
'\u266e': 'natural',
'\u266d': 'flat',
'\u20e5': 'sharp',
'\u0338': 'slash',
'\U0001D12A': 'double-sharp',
'\U0001D12B': 'flat-flat',
}

class Notation(prebase.ProtoM21Object):
'''
Expand Down Expand Up @@ -79,6 +109,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.

Expand Down Expand Up @@ -139,6 +170,23 @@ class Notation(prebase.ProtoM21Object):
<music21.figuredBass.notation.Figure 6 <Modifier b flat>>
>>> n3.figures[1]
<music21.figuredBass.notation.Figure 3 <Modifier b flat>>
>>> n3.extenders
[False, False]
>>> n3.hasExtenders
False
>>> n4 = notation.Notation('b6,\U0001D12B_,#')
>>> n4.figures
[<music21.figuredBass.notation.Figure 6 <Modifier b flat>>,
<music21.figuredBass.notation.Figure _ <Modifier 𝄫 double-flat>>,
<music21.figuredBass.notation.Figure 3 <Modifier # sharp>>]
>>> n4.figuresFromNotationColumn
[<music21.figuredBass.notation.Figure 6 <Modifier b flat>>,
<music21.figuredBass.notation.Figure _ <Modifier 𝄫 double-flat>>,
<music21.figuredBass.notation.Figure None <Modifier # sharp>>]
>>> n4.extenders
[False, True, False]
>>> n4.hasExtenders
True
'''
_DOC_ORDER = ['notationColumn', 'figureStrings', 'numbers', 'modifiers',
'figures', 'origNumbers', 'origModStrings', 'modifierStrings']
Expand Down Expand Up @@ -189,12 +237,15 @@ def __init__(self, notationColumn=None):
self.origModStrings = None
self.numbers = None
self.modifierStrings = None
self.extenders: list[bool] = []
self.hasExtenders: bool = False
self._parseNotationColumn()
self._translateToLonghand()

# Convert to convenient notation
self.modifiers = None
self.figures = None
self.figuresFromNotationColumn: list[Figure] = []
self._getModifiers()
self._getFigures()

Expand Down Expand Up @@ -224,11 +275,19 @@ def _parseNotationColumn(self):
(6, None)
>>> notation2.origModStrings
('-', '-')

hasExtenders is set True if an underscore is parsed within a notation string
>>> notation2.hasExtenders
False

>>> notation3 = n.Notation('7_')
>>> notation3.hasExtenders
True
'''
delimiter = '[,]'
figures = re.split(delimiter, self.notationColumn)
patternA1 = '([0-9]*)'
patternA2 = '([^0-9]*)'
patternA1 = '([0-9_]*)'
patternA2 = '([^0-9_]*)'
numbers = []
modifierStrings = []
figureStrings = []
Expand All @@ -247,17 +306,39 @@ def _parseNotationColumn(self):

number = None
modifierString = None
extender = False
if m1:
number = int(m1[0].strip())
# if no number is there and only an extender is found.
if '_' in m1:
self.hasExtenders = True
number = '_'
extender = True
else:
# 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)
self.extenders.append(extender)

numbers = tuple(numbers)
modifierStrings = tuple(modifierStrings)

# extenders come from the optional argument when instantiating 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)

self.origNumbers = numbers # Keep original numbers
self.numbers = numbers # Will be converted to longhand
self.origModStrings = modifierStrings # Keep original modifier strings
Expand Down Expand Up @@ -366,11 +447,26 @@ def _getFigures(self):
for i in range(len(self.numbers)):
number = self.numbers[i]
modifierString = self.modifierStrings[i]
if self.extenders:
if i < len(self.extenders):
extender = self.extenders[i]
else:
extender = False
figure = Figure(number, modifierString, extender)
figure = Figure(number, modifierString)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is immediately wiped out -- mistake? Not tested enough.

figures.append(figure)

self.figures = figures

figuresFromNotaCol = []

for i, number in enumerate(self.origNumbers):
modifierString = self.origModStrings[i]
figure = Figure(number, modifierString)
figuresFromNotaCol.append(figure)

self.figuresFromNotationColumn = figuresFromNotaCol


class NotationException(exceptions21.Music21Exception):
pass
Expand All @@ -395,6 +491,20 @@ class Figure(prebase.ProtoM21Object):
'+'
>>> f1.modifier
<music21.figuredBass.notation.Modifier + sharp>
>>> f1.hasExtender
False
>>> f1.isPureExtender
False
>>> f2 = notation.Figure(6, '#', extender=True)
>>> f2.hasExtender
True
>>> f2.isPureExtender
False
>>> f3 = notation.Figure(extender=True)
>>> f3.isPureExtender
True
>>> f3.hasExtender
True
'''
_DOC_ATTR: dict[str, str] = {
'number': '''
Expand All @@ -410,30 +520,71 @@ 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.
''',
'isPureExtender': '''
A bool value that returns true if an extender is part of the figure but no number
is given. It is set on the fly by evaluating the number and extender arguments.
'''
}

def __init__(self, number=1, modifierString=None):
def __init__(self, number=1, modifierString=None, extender: bool = False):
self.number = number
self.modifierString = modifierString
self.modifier = Modifier(modifierString)
# look for extender's underscore
self.hasExtender: bool = extender

@property
def isPureExtender(self) -> bool:
'''
Read-only boolean property that returns True if an extender is part of the figure
but no number is given (a number of 1 means no-number). It is a pure extender.

>>> from music21.figuredBass import notation
>>> n = notation.Figure(1, '#', extender=True)
>>> n.isPureExtender
True
>>> n.number = 2
>>> n.isPureExtender
False
'''

return self.number == 1 and self.hasExtender


def _reprInternal(self):
if self.isPureExtender:
return '<Figure is a pure extender>'
mod = repr(self.modifier).replace('music21.figuredBass.notation.', '')
ext = 'extender: __' if self.hasExtender else ''
if self.hasExtender:
return f'{self.number} {mod} {ext}'
return f'{self.number} {mod}'


# ------------------------------------------------------------------------------
specialModifiers = {'+': '#',
'/': '-',
'\\': '#',
'b': '-',
'bb': '--',
'bbb': '---',
'bbbb': '-----',
'++': '##',
'+++': '###',
'++++': '####',
}
specialModifiers = {
'+': '#',
'/': '-',
'\\': '#',
'b': '-',
'bb': '--',
'bbb': '---',
'bbbb': '-----',
'++': '##',
'+++': '###',
'++++': '####',
'\u266f': '#',
'\u266e': 'n',
'\u266d': 'b',
'\u20e5': '#',
'\u0338': '#',
'\U0001d12a': '##',
'\U0001d12b': '--'
}


class Modifier(prebase.ProtoM21Object):
Expand Down
97 changes: 97 additions & 0 deletions music21/harmony.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -2499,6 +2500,102 @@ def transpose(self: NCT, _value, *, inPlace=False) -> NCT | None:

# ------------------------------------------------------------------------------

class FiguredBass(Harmony):
'''
The FiguredBassIndication objects store information about thorough bass figures.
It is created as a representation for <fb> tags in MEI and <figured-bass> tags in MusicXML.
The FiguredBassIndication object derives from the Harmony object and can be used
in the following way:

>>> fb = harmony.FiguredBass('#,6#')
>>> fb
<music21.harmony.FiguredBass figures: #,6#>

The single figures are stored as figuredBass.notation.Figure objects:
>>> fb.notation.figures[0]
<music21.figuredBass.notation.Figure 3 <Modifier # sharp>>
>>> fb2 = harmony.FiguredBass(figureStrings=['#_','6#'])
>>> fb2
<music21.harmony.FiguredBass figures: #_,6#>
>>> fb2.notation.hasExtenders
True
'''


def __init__(self,
figureString: str = '',
figureStrings: list[str] = [],
**keywords):
super().__init__(**keywords)

self._figs: str = ''

if figureString != '':
self.figureString = figureString
elif figureStrings != []:
self.figureString = ','.join(figureStrings)
else:
self.figureString = ''

self._figNotation: notation.Notation = notation.Notation(self._figs)

@property
def notation(self) -> notation.Notation:
return self._figNotation

@notation.setter
def notation(self, figureNotation: notation.Notation):
'''
Sets the notation property of the FiguresBass object and updates the
figureString property if needed.

>>> from music21 import harmony, figuredBass
>>> fb = harmony.FiguredBass('6,#')
>>> fb.figureString, fb.notation
('6,#', <music21.figuredBass.notation.Notation 6,#>)

>>> fb.notation = figuredBass.notation.Notation('7b,b')
>>> fb.figureString, fb.notation
('7b,b', <music21.figuredBass.notation.Notation 7b,b>)
'''

self._figNotation = figureNotation
if figureNotation.notationColumn != self._figs:
self.figureString = figureNotation.notationColumn

@property
def figureString(self) -> str:
return self._figs

@figureString.setter
def figureString(self, figureString: str):
'''
Sets the figureString property of the FiguresBass object and updates the
notation property if needed.

>>> from music21 import harmony
>>> fb = harmony.FiguredBass('6,#')
>>> fb.figureString, fb.notation
('6,#', <music21.figuredBass.notation.Notation 6,#>)

>>> fb.figureString = '5,b'
>>> fb.figureString, fb.notation
('5,b', <music21.figuredBass.notation.Notation 5,b>)
'''

if isinstance(figureString, str) and figureString != '':
if ',' in figureString:
self._figs = figureString
else:
self._figs = ','.join(figureString)

self.notation = notation.Notation(self._figs)


def _reprInternal(self):
return f'figures: {self.notation.notationColumn}'

# ------------------------------------------------------------------------------

def realizeChordSymbolDurations(piece):
'''
Expand Down