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

Figured bass import and export for MusicXML and MEI #1613

Open
wants to merge 75 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
48d216d
added figuredBass tag to mei import
Feb 26, 2023
b3249a9
fixed import and moved figuredBassIndication to harmony
Feb 26, 2023
59d54c0
Merge pull request #2 from mxordn/mei-import-fb-support
mxordn Feb 26, 2023
afcde73
added meterSig to mei import
Feb 26, 2023
7ca3834
first working musicxml output for FiguredBassIndication Objects
Feb 28, 2023
79ab796
improved figuredbass mei and musicxml import/export deals now with ex…
Feb 28, 2023
8fe69ed
Merge branch 'cuthbertLab:master' into mei-import-fb-support
mxordn Feb 28, 2023
fe3aaf5
stable import and export of figured base. Some unicode characters mig…
Mar 4, 2023
7708338
cleanup mei import and musicxml export
Mar 4, 2023
0793349
Merge pull request #3 from mxordn/mei-import-fb-support
mxordn Mar 4, 2023
82995d7
xml import and export fixed problems with more than two figures per note
Mar 7, 2023
60653c1
Merge pull request #4 from cuthbertLab/master
mxordn Mar 21, 2023
0cabb7d
fixed wrong offsets for multiple figures
Mar 21, 2023
fd1626d
fixed export of multiple figures, where an already exsiting duration …
Mar 22, 2023
874b959
cleaned upcomments and stuff
Mar 22, 2023
7c62238
Merge pull request #5 from mxordn/musicxml-fb-import
mxordn Mar 22, 2023
a85aeeb
added support for prefix tags and solved problems with empty number tags
Mar 22, 2023
ffc6190
figured bass is now checked
Mar 22, 2023
9acd4a6
updated mei/base.py
Mar 23, 2023
1c11c71
mei/base.py recent update to upstream
Mar 23, 2023
13d0d59
full merge of mei/base.py
Mar 23, 2023
f065d4c
edit in mei/test_base.py to pass new fb element in part list
Mar 23, 2023
389bb82
suggestions from review added, smaller formatting improvements
Mar 27, 2023
2fb6285
adapted tests in mei/test_base-py and doctest for figuredBass/notatio…
Mar 27, 2023
4e5fdec
simplification of a if else statement from review
Mar 27, 2023
ad8b09d
finally cleaned up for tests and flake8 and mypy
Mar 28, 2023
695a1e9
Update music21/musicxml/xmlToM21.py
mxordn Mar 30, 2023
3e5074f
refactoring and adding testfles. put figures in measure-
Apr 8, 2023
27a1c44
added figuredBass tag to mei import
Feb 26, 2023
621aef3
fixed import and moved figuredBassIndication to harmony
Feb 26, 2023
7e416cf
added meterSig to mei import
Feb 26, 2023
8085310
first working musicxml output for FiguredBassIndication Objects
Feb 28, 2023
f10cbb9
improved figuredbass mei and musicxml import/export deals now with ex…
Feb 28, 2023
52363bc
stable import and export of figured base. Some unicode characters mig…
Mar 4, 2023
413c17b
cleanup mei import and musicxml export
Mar 4, 2023
8244b3e
xml import and export fixed problems with more than two figures per note
Mar 7, 2023
f9179f7
fixed wrong offsets for multiple figures
Mar 21, 2023
3649265
fixed export of multiple figures, where an already exsiting duration …
Mar 22, 2023
e497210
cleaned upcomments and stuff
Mar 22, 2023
c6ac193
added support for prefix tags and solved problems with empty number tags
Mar 22, 2023
d8cef56
figured bass is now checked
Mar 22, 2023
4f098b1
updated mei/base.py
Mar 23, 2023
9717173
mei/base.py recent update to upstream
Mar 23, 2023
0f4aba5
full merge of mei/base.py
Mar 23, 2023
c8318c5
edit in mei/test_base.py to pass new fb element in part list
Mar 23, 2023
7076597
suggestions from review added, smaller formatting improvements
Mar 27, 2023
d6c49db
adapted tests in mei/test_base-py and doctest for figuredBass/notatio…
Mar 27, 2023
9688055
simplification of a if else statement from review
Mar 27, 2023
97882bc
finally cleaned up for tests and flake8 and mypy
Mar 28, 2023
dcd1477
Update music21/musicxml/xmlToM21.py
mxordn Mar 30, 2023
1164fd9
rebase 8.3
Jun 13, 2023
59974de
added function documentation strings with examples
Jun 13, 2023
a1efced
added more documentation in the mei parser
Jun 13, 2023
000b4ea
Merge branch 'master' into test-figured-bass
mxordn Jun 13, 2023
91c712d
Merge pull request #7 from mxordn/test-figured-bass
mxordn Jun 13, 2023
95c57fd
mei commit
Jun 13, 2023
08edf89
Merge branch 'test-figured-bass' of https://github.com/mxordn/music21…
Jun 13, 2023
21469bc
Merge pull request #8 from mxordn/test-figured-bass
mxordn Jun 13, 2023
86c95be
resolve two merge conflicts
Jun 13, 2023
dd0de78
Merge branch 'master' of https://github.com/mxordn/music21
Jun 13, 2023
7b2d99a
version with Figures at top level
Jun 13, 2023
e008660
Merge pull request #9 from mxordn/test-figured-bass
mxordn Jun 13, 2023
704434f
added some additional checks and warnings for prefixes and suffixes
Jun 15, 2023
1f2d4ce
added prefix and suffix warnings
Jun 16, 2023
a3bd064
Merge branch 'cuthbertLab:master' into master
mxordn Jun 17, 2023
cf1f237
Merge branch 'cuthbertLab:master' into test-figured-bass
mxordn Jun 17, 2023
74f4d96
added examples for figured bass
Jun 17, 2023
0d91795
mei import of figured bass goes to the measure; added exmaples for me…
Jun 17, 2023
3e45f77
commented out old mei import into score
Jun 17, 2023
b5d7201
Merge branch 'cuthbertLab:master' into test-figured-bass
mxordn Jun 17, 2023
f62fc34
added modified tests
Jun 17, 2023
774c126
Merge branch 'cuthbertLab:master' into master
mxordn Jun 17, 2023
03ca64e
Merge pull request #10 from mxordn/test-figured-bass
mxordn Jun 17, 2023
36970de
cleanup comments
Jun 17, 2023
a4b523e
second cleanup comments and tests
Jun 17, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 120 additions & 17 deletions music21/figuredBass/notation.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,33 @@
(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 +106,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 All @@ -104,11 +132,11 @@ class Notation(prebase.ProtoM21Object):
<music21.figuredBass.notation.Modifier + sharp>,
<music21.figuredBass.notation.Modifier None None>)
>>> n1.figures[0]
<music21.figuredBass.notation.Figure 6 <Modifier None None>>
<music21.figuredBass.notation.Figure 6 Mods: <Modifier None None> hasExt: False>
>>> n1.figures[1]
<music21.figuredBass.notation.Figure 4 <Modifier + sharp>>
<music21.figuredBass.notation.Figure 4 Mods: <Modifier + sharp> hasExt: False>
>>> n1.figures[2]
<music21.figuredBass.notation.Figure 2 <Modifier None None>>
<music21.figuredBass.notation.Figure 2 Mods: <Modifier None None> hasExt: False>


Here, a stand-alone '#' is being passed to Notation.
Expand All @@ -121,9 +149,9 @@ class Notation(prebase.ProtoM21Object):
(<music21.figuredBass.notation.Modifier None None>,
<music21.figuredBass.notation.Modifier # sharp>)
>>> n2.figures[0]
<music21.figuredBass.notation.Figure 5 <Modifier None None>>
<music21.figuredBass.notation.Figure 5 Mods: <Modifier None None> hasExt: False>
>>> n2.figures[1]
<music21.figuredBass.notation.Figure 3 <Modifier # sharp>>
<music21.figuredBass.notation.Figure 3 Mods: <Modifier # sharp> hasExt: False>


Now, a stand-alone b is being passed to Notation as part of a larger notationColumn.
Expand All @@ -136,9 +164,9 @@ class Notation(prebase.ProtoM21Object):
(<music21.figuredBass.notation.Modifier b flat>,
<music21.figuredBass.notation.Modifier b flat>)
>>> n3.figures[0]
<music21.figuredBass.notation.Figure 6 <Modifier b flat>>
<music21.figuredBass.notation.Figure 6 Mods: <Modifier b flat> hasExt: False>
>>> n3.figures[1]
<music21.figuredBass.notation.Figure 3 <Modifier b flat>>
<music21.figuredBass.notation.Figure 3 Mods: <Modifier b flat> hasExt: False>
'''
_DOC_ORDER = ['notationColumn', 'figureStrings', 'numbers', 'modifiers',
'figures', 'origNumbers', 'origModStrings', 'modifierStrings']
Expand Down Expand Up @@ -179,22 +207,25 @@ class Notation(prebase.ProtoM21Object):
''',
}

def __init__(self, notationColumn=None):
def __init__(self, notationColumn=None, extenders=None):
# Parse notation string
if notationColumn is None:
notationColumn = ''
self.notationColumn = notationColumn
self.extenders = extenders
self.figureStrings = None
self.origNumbers = None
self.origModStrings = None
self.numbers = None
self.modifierStrings = None
self.hasExtenders: bool = False
self._parseNotationColumn()
self._translateToLonghand()

# Convert to convenient notation
self.modifiers = None
self.figures = None
self.figuresFromNotationColumn = None
self._getModifiers()
self._getFigures()

Expand Down Expand Up @@ -227,11 +258,12 @@ def _parseNotationColumn(self):
'''
delimiter = '[,]'
figures = re.split(delimiter, self.notationColumn)
patternA1 = '([0-9]*)'
patternA2 = '([^0-9]*)'
patternA1 = '([0-9_]*)'
patternA2 = '([^0-9_]*)'
numbers = []
modifierStrings = []
figureStrings = []
extenders = []

for figure in figures:
figure = figure.strip()
Expand All @@ -247,16 +279,40 @@ 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)
extenders.append(extender)

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

# extenders come from the optional argument when instantionting 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)
#print('angekommen', numbers, modifierStrings, extenders)

self.origNumbers = numbers # Keep original numbers
self.numbers = numbers # Will be converted to longhand
Expand Down Expand Up @@ -357,20 +413,33 @@ def _getFigures(self):
>>> from music21.figuredBass import notation as n
>>> notation2 = n.Notation('-6,-') #__init__ method calls _getFigures()
>>> notation2.figures[0]
<music21.figuredBass.notation.Figure 6 <Modifier - flat>>
<music21.figuredBass.notation.Figure 6 Mods: <Modifier - flat> hasExt: False>
>>> notation2.figures[1]
<music21.figuredBass.notation.Figure 3 <Modifier - flat>>
<music21.figuredBass.notation.Figure 3 Mods: <Modifier - flat> hasExt: False>
'''
figures = []

for i in range(len(self.numbers)):
number = self.numbers[i]
modifierString = self.modifierStrings[i]
figure = Figure(number, modifierString)
if self.extenders:
if i < len(self.extenders):
extender = self.extenders[i]
else:
extender = False
figure = Figure(number, modifierString, extender)
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 @@ -387,14 +456,28 @@ class Figure(prebase.ProtoM21Object):
>>> from music21.figuredBass import notation
>>> f1 = notation.Figure(4, '+')
>>> f1
<music21.figuredBass.notation.Figure 4 <Modifier + sharp>>
<music21.figuredBass.notation.Figure 4 Mods: <Modifier + sharp> hasExt: False>

>>> f1.number
4
>>> f1.modifierString
'+'
>>> f1.modifier
<music21.figuredBass.notation.Modifier + sharp>
>>> f1.hasExtender
False
>>> f1.isExtender
False
>>> f2 = notation.Figure(6, '\', extender=True)
>>> f2.hasExtender
True
>>> f2.isExtender
False
>>> f3 = notation.Figure(extender=True)
>>> f3.isExtender
True
>>> f3.hasExtender
True
'''
_DOC_ATTR: dict[str, str] = {
'number': '''
Expand All @@ -410,16 +493,28 @@ 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.
''',
'isExtender': '''
A bool value that returns true if an extender is part of the figure but no
number is given. Pure extender if you will.
It is set by evaluating the number and extender arguments.
'''
}

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

def _reprInternal(self):
mod = repr(self.modifier).replace('music21.figuredBass.notation.', '')
return f'{self.number} {mod}'
return f'{self.number} Mods: {mod} hasExt: {self.hasExtender}'


# ------------------------------------------------------------------------------
Expand All @@ -433,6 +528,13 @@ def _reprInternal(self):
'++': '##',
'+++': '###',
'++++': '####',
'\u266f': '#',
'\u266e': 'n',
'\u266d': 'b',
'\u20e5': '#',
'\u0338': '#',
'\U0001d12a': '##',
'\U0001d12b': '--'
}


Expand Down Expand Up @@ -493,6 +595,7 @@ class Modifier(prebase.ProtoM21Object):

def __init__(self, modifierString=None):
self.modifierString = modifierString
self.originalString = modifierString
self.accidental = self._toAccidental()

def _reprInternal(self):
Expand Down
50 changes: 50 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,55 @@ def transpose(self: NCT, _value, *, inPlace=False) -> NCT | None:

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

class FiguredBassIndication(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:

>>> fbi = harmony.FiguredBassIndication('#,6#', part='1')
>>> fbi
<FiguredBassIndication figures: #,6# part: 1>

The single figures are stored as figuredBass.notation.Figure objects:
>>> fbi.fig_notation.figures
[<music21.figuredBass.notation.Figure 3 Mods: <Modifier # sharp> hasExt: False>,
<music21.figuredBass.notation.Figure 6 Mods: <Modifier # sharp> hasExt: False>]
'''

isFigure: bool = True
part: str | None = None
_figs: str = ''

def __init__(self, figs: str | list | None = None, extenders: list[bool] | None = None ,
part: str | None=None, **keywords):
super().__init__(**keywords)
if figs:
if isinstance(figs, list):
_figs: str = ','.join(figs)
elif isinstance(figs, str):
if ',' in figs:
_figs = figs
else:
_figs = ','.join(figs)
else:
_figs = ''
self._fig_notation = notation.Notation(_figs, extenders)
self.part = part

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

@fig_notation.setter
def fig_notation(self, figs, extenders=None):
self._fig_notation = notation.Notation(figs, extenders)

def __repr__(self):
return f'<{self.__class__.__name__} figures: {self.fig_notation.notationColumn} part: {self.part}>'

def realizeChordSymbolDurations(piece):
'''
Expand Down
Loading