-
Notifications
You must be signed in to change notification settings - Fork 406
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
Import and export of figured bass elements from MEI and musicxml file formats #1539
Changes from 26 commits
267bb07
9f3f066
c472631
48d216d
b3249a9
59d54c0
afcde73
7ca3834
79ab796
8fe69ed
fe3aaf5
7708338
0793349
82995d7
60653c1
0cabb7d
dc812a4
fd1626d
874b959
7c62238
a85aeeb
582a367
90f38cf
ea8d3e8
ffc6190
9acd4a6
1c11c71
5114ede
13d0d59
f065d4c
2e92bfb
98533df
57b5f01
52a3280
f64ea43
6f75a9b
835910c
1c88d57
517f046
fe5186a
d4ebe55
a7135d3
1ea8840
c634196
f558b8e
0def6e3
b83121b
88333d5
256d013
389bb82
2fb6285
4e5fdec
a16c2d5
e76f405
ad8b09d
f519b9d
333721f
75420e1
81b2bd5
e45753d
37763c4
e9274e9
4ad73d0
187a2dc
f6242a0
9011556
f00e741
f738377
1d87d2d
9536d10
f05ec0a
59e02d5
3283c3f
4cae3c2
695a1e9
53263a5
e90ae4f
053d933
3414cc2
7851f48
0150cc4
7997ce6
44450c4
65692c8
b9a1494
cf27d2b
b0ab490
08fd681
edfba3b
bcc3808
a9de789
8ed3beb
3308ee4
19c91f4
b23eba0
c7965bf
367e9ab
77af85e
40f89f7
4d321cf
a401969
d82183f
6c3da7c
9678517
2526b68
3c4b95b
1573e37
b2af34b
5c232c9
0206700
18b750f
df97c67
696545a
9c67043
0d3386b
6973483
6d83988
83696c5
42534a3
dd4f03f
8d5cce8
8dbf9ed
067dc62
8a8c167
e4a294c
9f1cf26
d6303db
851c776
8e29ff4
e36a014
0f9d6cc
820efd9
0911ff1
892bd30
54b2753
c20a4cd
2cc61a4
3f1af1f
9bab45f
05548b0
6201cf0
acf926c
1cccdf5
868136a
3b17a68
e8db15d
c1d166f
2f6833a
013bdc3
41bf5dd
46ee5dc
18fdfd6
e25334b
ffacccf
c56dd47
8ebea2e
bddd31a
bcb87e8
a6b6ad4
fd07402
2dd2fa6
2ca1f13
878e0de
e40fa08
bb5a579
7558873
80c32c9
6ff290d
32352df
4f8d003
8749987
64836b9
c7769a5
4e6f057
a6955a1
2cad009
4b5155c
69bcccb
5562943
048a384
55207a2
3b6a927
58f5495
ac23422
3ea66e4
cfdc95b
fcc1bd5
61dcdbc
149516f
425ec69
405b1fc
c0f8c3d
d9676fb
c8a1869
06fd20c
b04bfe4
aed4ace
35d2ce8
b3c3513
30afe10
9b13e16
e0c761b
0fdf0b4
27a1c44
621aef3
7e416cf
8085310
f10cbb9
52363bc
413c17b
8244b3e
f9179f7
3649265
e497210
c6ac193
d8cef56
4f098b1
9717173
0f4aba5
c8318c5
7076597
d6c49db
9688055
97882bc
dcd1477
1164fd9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -2492,6 +2493,33 @@ def transpose(self: NCT, _value, *, inPlace=False) -> NCT | None: | |
|
||
# ------------------------------------------------------------------------------ | ||
|
||
class FiguredBassIndication(Harmony): | ||
isFigure = True | ||
tstamp = 1 | ||
Comment on lines
+2504
to
+2505
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If these are class attributes, won't each instance contaminate the the others when attempting to set them? Also, can you explain what tstamp is needed for? (Can we just use |
||
def __init__(self, figs: str | list | 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) | ||
|
||
@property | ||
def fig_notation(self) -> notation.Notation: | ||
return self._fig_notation | ||
|
||
@fig_notation.setter | ||
def fig_notation(self, figs): | ||
self._fig_notation = notation.Notation(figs) | ||
|
||
def __repr__(self): | ||
return f'<{self.__class__.__name__} figures: {self.fig_notation.notationColumn}>' | ||
|
||
def realizeChordSymbolDurations(piece): | ||
''' | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -199,6 +199,7 @@ | |
from music21 import stream | ||
from music21 import spanner | ||
from music21 import tie | ||
from music21 import harmony | ||
|
||
|
||
if t.TYPE_CHECKING: | ||
|
@@ -466,6 +467,17 @@ def allPartsPresent(scoreElem) -> tuple[str, ...]: | |
partNs.append(staffDef.get('n')) | ||
if not partNs: | ||
raise MeiValidityError(_SEEMINGLY_NO_PARTS) | ||
|
||
# Get information of possible <harm> tags in the score. If there are tags prepare a list to | ||
# store them and process them later. | ||
# TODO: Maybe to be put in a separate function e.g. like allPartsPresent | ||
figuredBassQuery = f'.//{MEI_NS}fb' | ||
if scoreElem.findall(figuredBassQuery): | ||
environLocal.printDebug('harm tag found!') | ||
partNs.append('fb') | ||
# here other <harm> elements (e.g. chordsymbols) can be added. | ||
# … 'if …' | ||
|
||
return tuple(partNs) | ||
|
||
|
||
|
@@ -2493,6 +2505,48 @@ def chordFromElement(elem, slurBundle=None): | |
|
||
return theChord | ||
|
||
def harmFromElement(elem, slurBundle=None) -> tuple: | ||
# other tags than <fb> to be added… | ||
tagToFunction = {f'{MEI_NS}fb': figuredbassFromElement} | ||
|
||
fb_harmony_tag: tuple = () | ||
|
||
# Collect all elements in a measure and go throug extenders | ||
# tstamp has to be used as a duration marker between two elements | ||
|
||
for subElement in _processEmbeddedElements(elem.findall('*'), | ||
tagToFunction, | ||
elem.tag, slurBundle): | ||
subElement.tstamp = float(elem.get('tstamp')) | ||
subElement.offset = subElement.tstamp - 1 | ||
fb_harmony_tag = (subElement.tstamp - 1, subElement) | ||
|
||
return fb_harmony_tag | ||
|
||
def figuredbassFromElement(elem, slurBundle=None) -> harmony.FiguredBassIndication: | ||
if elem.get(_XMLID): | ||
fb_id = elem.get(_XMLID) | ||
fb_notation: str = '' | ||
fb_notation_list: list[str] = [] | ||
dauer: float = 0 | ||
# loop through all child elements and collect <f> tags | ||
for subElement in elem.findall('*'): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You don't need to loop through all of the elements -- you can just do an xquery to find just the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you mean something like |
||
if subElement.tag == f'{MEI_NS}f': | ||
if subElement.text is not None: | ||
fb_notation_list.append(subElement.text) | ||
else: | ||
if 'extender' in subElement.attrib.keys(): | ||
fb_notation_list.append('_') | ||
if 'dur.metrical' in subElement.attrib.keys(): | ||
dauer = float(subElement.attrib['dur.metrical']) | ||
|
||
# Generate a FiguredBassIndication object and set the collected information | ||
fb_notation = ','.join(fb_notation_list) | ||
theFbNotation = harmony.FiguredBassIndication(fb_notation) | ||
theFbNotation.id = fb_id | ||
theFbNotation.duration = duration.Duration(quarterLength=dauer) | ||
|
||
return theFbNotation | ||
|
||
def clefFromElement(elem, slurBundle=None): # pylint: disable=unused-argument | ||
''' | ||
|
@@ -3131,10 +3185,12 @@ def measureFromElement(elem, backupNum, expectedNs, slurBundle=None, activeMeter | |
''' | ||
staves = {} | ||
stavesWaiting = {} # for staff-specific objects processed before the corresponding staff | ||
harmElements: dict = {'fb': []} | ||
|
||
# mapping from tag name to our converter function | ||
staffTag = f'{MEI_NS}staff' | ||
staffDefTag = f'{MEI_NS}staffDef' | ||
harmTag = f'{MEI_NS}harm' | ||
|
||
# track the bar's duration | ||
maxBarDuration = None | ||
|
@@ -3154,6 +3210,10 @@ def measureFromElement(elem, backupNum, expectedNs, slurBundle=None, activeMeter | |
environLocal.warn(_UNIMPLEMENTED_IMPORT.format('<staffDef>', '@n')) | ||
else: | ||
stavesWaiting[whichN] = staffDefFromElement(eachElem, slurBundle) | ||
elif harmTag == eachElem.tag: | ||
# get all information stored in <harm> tags | ||
harmElements['fb'].append(harmFromElement(eachElem)) | ||
|
||
elif eachElem.tag not in _IGNORE_UNPROCESSED: | ||
environLocal.printDebug(_UNPROCESSED_SUBELEMENT.format(eachElem.tag, elem.tag)) | ||
|
||
|
@@ -3166,6 +3226,10 @@ def measureFromElement(elem, backupNum, expectedNs, slurBundle=None, activeMeter | |
# *start* of the <measure> in which it appears. | ||
staves[whichN].insert(0, eachObj) | ||
|
||
# Add <harm> objects to the staves dict | ||
staves['fb'] = harmElements | ||
# other childs of <harm> tags can be added here… | ||
|
||
# create rest-filled measures for expected parts that had no <staff> tag in this <measure> | ||
for eachN in expectedNs: | ||
if eachN not in staves: | ||
|
@@ -3485,6 +3549,9 @@ def scoreFromElement(elem, slurBundle): | |
|
||
# Get a tuple of all the @n attributes for the <staff> tags in this score. Each <staff> tag | ||
# corresponds to what will be a music21 Part. | ||
|
||
# UPDATE: If <harm> tags are found, they will also be collected as a separate 'part' | ||
# to process them later. | ||
allPartNs = allPartsPresent(elem) | ||
|
||
# This is the actual processing. | ||
|
@@ -3494,6 +3561,14 @@ def scoreFromElement(elem, slurBundle): | |
# We must iterate here over "allPartNs," which preserves the part-order found in the MEI | ||
# document. Iterating the keys in "parsed" would not preserve the order. | ||
environLocal.printDebug('*** making the Score') | ||
|
||
# Extract collected <harm> information stored in the dict unter the 'fb' key | ||
harms: list[dict] | None = None | ||
if 'fb' in parsed.keys(): | ||
harms = parsed['fb'][0] | ||
del parsed['fb'] | ||
allPartNs = allPartNs[0:-1] | ||
|
||
theScore = [stream.Part() for _ in range(len(allPartNs))] | ||
for i, eachN in enumerate(allPartNs): | ||
# set "atSoundingPitch" so transposition works | ||
|
@@ -3502,6 +3577,13 @@ def scoreFromElement(elem, slurBundle): | |
theScore[i].append(eachObj) | ||
theScore = stream.Score(theScore) | ||
|
||
# loop through measures to insert harm elements from harms list at the right offsets | ||
if harms: | ||
for index, measureOffset in enumerate(theScore.measureOffsetMap().keys()): | ||
hms = harms[index]['fb'] | ||
for h in hms: | ||
theScore.insert(measureOffset + h[0], h[1]) | ||
|
||
# put slurs in the Score | ||
theScore.append(list(slurBundle)) | ||
# TODO: when all the Slur objects are at the end, they'll only be outputted properly if the | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm wondering what you think we should do about the existing
figure
property, which as of now will give: