+16
-55
@@ -38,7 +38,6 @@ class Importer:
|
||||
self.staff_details = {'lines': 5, 'tunings': []}
|
||||
self.absolute = Fraction()
|
||||
self.tremolo_starts = {}
|
||||
self.has_repeats = False
|
||||
def fail(self, text: str):
|
||||
raise MusicXMLError(f'{self.location}: {text}')
|
||||
def children(self, node, allowed: set[str]) -> None:
|
||||
@@ -178,8 +177,6 @@ class Importer:
|
||||
self.harmony_sequence += 1
|
||||
result = {'id': f'harmony-{self.harmony_sequence}', 'onset': rational(self.offset(node, cursor)), 'kind': kind.text, 'degrees': []}
|
||||
self.attrs(kind, {'text', 'use-symbols', 'stack-degrees', 'parentheses-degrees', 'bracket-degrees'})
|
||||
if 'text' in kind.attrib:
|
||||
result['kind_text'] = kind.get('text')
|
||||
for tag in ('root', 'bass'):
|
||||
item = node.find(tag)
|
||||
if item is None:
|
||||
@@ -204,17 +201,9 @@ class Importer:
|
||||
elif operation == 'subtract' and degree_alter:
|
||||
self.fail('cannot subtract a degree different from the one in the chord kind')
|
||||
result['degrees'].append({'value': value, 'alter': rational(degree_alter), 'type': operation})
|
||||
if kind.get('text') and result['kind'] != 'none':
|
||||
harmony.normalize_text(result, kind.get('text'))
|
||||
chord_degrees = harmony.degrees(result)
|
||||
if 'kind_text' in result and result['kind'] != 'none':
|
||||
label = 'C' + result['kind_text']
|
||||
try:
|
||||
displayed = harmony.degrees(harmony.parse_symbol(label))
|
||||
except ValueError:
|
||||
self.fail(f'unmapped harmony display text {label!r}; use a standard chord kind/degree spelling')
|
||||
if displayed != chord_degrees:
|
||||
if displayed != harmony.KINDS[result['kind']]:
|
||||
self.fail(f'contradictory harmony text {label!r} and structured chord degrees')
|
||||
result.pop('kind_text')
|
||||
if node.find('inversion') is not None:
|
||||
inversion = self.number(node.findtext('inversion'), integer=True)
|
||||
result['inversion'] = inversion
|
||||
@@ -235,6 +224,8 @@ class Importer:
|
||||
self.children(node, {'direction-type', 'offset', 'staff', 'voice', 'sound'})
|
||||
onset = self.offset(node, cursor)
|
||||
for group in node.findall('direction-type'):
|
||||
if group.find('segno') is not None or group.find('coda') is not None:
|
||||
self.fail('navigation jumps are unsupported; write the music out in reading order')
|
||||
self.children(group, {'words', 'octave-shift', 'rehearsal', 'metronome'})
|
||||
for item in group:
|
||||
if item.tag == 'words':
|
||||
@@ -275,6 +266,8 @@ class Importer:
|
||||
if sound is not None:
|
||||
self.sound(sound, onset, measure)
|
||||
def sound(self, node, onset: Fraction, measure: dict) -> None:
|
||||
if {'dacapo', 'dalsegno', 'segno', 'coda', 'tocoda', 'fine', 'forward-repeat', 'time-only'} & node.attrib.keys():
|
||||
self.fail('navigation jumps are unsupported; write the music out in reading order')
|
||||
self.attrs(node, {'tempo'})
|
||||
self.children(node, set())
|
||||
if node.get('tempo') and not any(fraction(t['onset']) == onset for t in measure.get('tempos', [])):
|
||||
@@ -375,8 +368,8 @@ class Importer:
|
||||
ties = {t.get('type') for t in node.findall('tie')}
|
||||
for tie in node.findall('tie'):
|
||||
self.attrs(tie, {'type', 'time-only'})
|
||||
if tie.get('time-only') and not self.has_repeats:
|
||||
self.fail('time-only tie requires a repeated section')
|
||||
if 'time-only' in tie.attrib:
|
||||
self.fail('pass-specific ties are unsupported; write the music out in reading order')
|
||||
notations = node.find('notations')
|
||||
visual_ties = set()
|
||||
if notations is not None:
|
||||
@@ -387,22 +380,12 @@ class Importer:
|
||||
ties |= visual_ties
|
||||
if ties - {'start', 'stop'}:
|
||||
self.fail('unsupported tie notation')
|
||||
if self.has_repeats and ties:
|
||||
from generator.repeats import pass_numbers
|
||||
p['sound_ties'] = []
|
||||
p['printed_ties'] = sorted(visual_ties)
|
||||
for tie in node.findall('tie'):
|
||||
self.attrs(tie, {'type', 'time-only'})
|
||||
marker = {'type': tie.get('type')}
|
||||
if tie.get('time-only'):
|
||||
marker['passes'] = pass_numbers(tie.get('time-only'))
|
||||
p['sound_ties'].append(marker)
|
||||
if not self.has_repeats and 'stop' in ties:
|
||||
if 'stop' in ties:
|
||||
source = self.open_ties.pop(value, None)
|
||||
if source is None or source['_absolute'] + fraction(source['duration']) != p['_absolute']:
|
||||
self.fail('tie stop has no contiguous matching start')
|
||||
source['tie_to'], p['tie_from'] = p['id'], source['id']
|
||||
if not self.has_repeats and 'start' in ties:
|
||||
if 'start' in ties:
|
||||
if value in self.open_ties:
|
||||
self.fail('ambiguous duplicate tie at the same pitch')
|
||||
self.open_ties[value] = p
|
||||
@@ -512,7 +495,6 @@ class Importer:
|
||||
root = ElementTree.parse(self.path, forbid_entities=True, forbid_external=True).getroot()
|
||||
if root.tag != 'score-partwise' or root.get('version') not in ('3.1', '4.0'):
|
||||
self.fail('expected uncompressed score-partwise MusicXML 3.1 or 4.0')
|
||||
self.has_repeats = bool(root.findall('.//barline/repeat') or root.findall('.//barline/ending'))
|
||||
self.children(root, IGNORED_TOP | {'part'})
|
||||
parts = root.findall('part')
|
||||
definitions = root.findall('part-list/score-part')
|
||||
@@ -564,8 +546,6 @@ class Importer:
|
||||
if item.tag == 'attributes':
|
||||
state = self.attributes(item, cursor)
|
||||
if cursor == 0:
|
||||
if self.has_repeats:
|
||||
measure.setdefault('initial_attributes', {}).update({k: v for k, v in state.items() if k != 'onset'})
|
||||
for k in ('key', 'meter', 'clef', 'transpose', 'staff_details'):
|
||||
measure[k] = copy.deepcopy(getattr(self, k))
|
||||
elif len(state) > 1:
|
||||
@@ -599,11 +579,13 @@ class Importer:
|
||||
extent = max(extent, cursor)
|
||||
previous = None
|
||||
elif item.tag == 'barline':
|
||||
if item.find('repeat') is not None or item.find('ending') is not None:
|
||||
self.fail('repeats and numbered endings are unsupported; write the music out in reading order')
|
||||
self.attrs(item, {'location'})
|
||||
self.children(item, {'bar-style', 'repeat', 'ending'})
|
||||
self.children(item, {'bar-style'})
|
||||
side = item.get('location', 'right')
|
||||
if side not in ('left', 'right'):
|
||||
self.fail('barlines and repeats must be at measure boundaries')
|
||||
self.fail('barlines must be at measure boundaries')
|
||||
if side in measure.get('barlines', {}):
|
||||
self.fail(f'duplicate {side} barline')
|
||||
if side == 'left' and cursor != 0 or side == 'right' and cursor != extent:
|
||||
@@ -614,26 +596,6 @@ class Importer:
|
||||
measure.setdefault('barlines', {})[side] = style
|
||||
if side == 'right':
|
||||
measure['_right_barline_onset'] = cursor
|
||||
for repeat in item.findall('repeat'):
|
||||
self.attrs(repeat, {'direction', 'times', 'winged'})
|
||||
self.children(repeat, set())
|
||||
if side in measure.get('repeats', {}):
|
||||
self.fail('duplicate repeat marker')
|
||||
marker = {'direction': repeat.get('direction')}
|
||||
if repeat.get('times'):
|
||||
if marker['direction'] != 'backward':
|
||||
self.fail('repeat times requires a backward repeat')
|
||||
marker['times'] = self.number(repeat.get('times'), integer=True, positive=True)
|
||||
if repeat.get('winged', 'none') != 'none':
|
||||
self.fail('winged repeat engraving is not supported')
|
||||
measure.setdefault('repeats', {})[side] = marker
|
||||
for ending in item.findall('ending'):
|
||||
from generator.repeats import pass_numbers
|
||||
self.attrs(ending, {'number', 'type', 'print-object', 'end-length', 'text-x', 'text-y'})
|
||||
self.children(ending, set())
|
||||
if side in measure.get('endings', {}):
|
||||
self.fail('duplicate ending marker')
|
||||
measure.setdefault('endings', {})[side] = {'type': ending.get('type'), 'numbers': pass_numbers(ending.get('number', '')), 'text': ending.text or '', 'visible': ending.get('print-object', 'yes') != 'no'}
|
||||
if not voices or extent <= 0:
|
||||
self.fail('empty or zero-duration measure')
|
||||
cursor = extent
|
||||
@@ -653,6 +615,8 @@ class Importer:
|
||||
if len(matches) != 1 or onset in used:
|
||||
self.fail('optional-chord marker must identify exactly one harmony and occur only once')
|
||||
matches[0]['chord_optional'] = True
|
||||
matches[0]['parenthesized'] = True
|
||||
matches[0]['symbol'] = harmony.symbol(matches[0])
|
||||
used.add(onset)
|
||||
measures.append(measure)
|
||||
self.absolute += cursor
|
||||
@@ -667,9 +631,6 @@ class Importer:
|
||||
metadata.update({'key_signature': measures[0]['key_signature'], 'time_signature': measures[0]['time_signature'], 'key': copy.deepcopy(measures[0]['key'])})
|
||||
result = {'schema_version': SCHEMA_VERSION, 'metadata': metadata, 'staves': [{'id': 'staff-source', 'name': definitions[0].findtext('part-name', 'Source'), 'role': 'vocal', 'clef': copy.deepcopy(measures[0]['clef']), 'tuplets': self.tuplets, 'octave_shifts': self.octaves, 'measures': measures}]}
|
||||
result['staves'][0]['instruments'] = instruments
|
||||
if self.has_repeats:
|
||||
from generator.repeats import expand_document
|
||||
validate_document(expand_document(result))
|
||||
validate_document(result)
|
||||
return result
|
||||
|
||||
|
||||
Reference in New Issue
Block a user