+58
-4
@@ -38,6 +38,7 @@ 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:
|
||||
@@ -372,7 +373,12 @@ class Importer:
|
||||
if node.find('notehead') is not None:
|
||||
p['notehead'] = {'name': node.findtext('notehead'), **dict(node.find('notehead').attrib)}
|
||||
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')
|
||||
notations = node.find('notations')
|
||||
visual_ties = set()
|
||||
if notations is not None:
|
||||
visual_ties = {t.get('type') for t in notations.findall('tied')}
|
||||
if 'continue' in visual_ties:
|
||||
@@ -381,12 +387,22 @@ class Importer:
|
||||
ties |= visual_ties
|
||||
if ties - {'start', 'stop'}:
|
||||
self.fail('unsupported tie notation')
|
||||
if 'stop' in ties:
|
||||
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:
|
||||
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 'start' in ties:
|
||||
if not self.has_repeats and 'start' in ties:
|
||||
if value in self.open_ties:
|
||||
self.fail('ambiguous duplicate tie at the same pitch')
|
||||
self.open_ties[value] = p
|
||||
@@ -496,6 +512,7 @@ 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')
|
||||
@@ -547,6 +564,8 @@ 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:
|
||||
@@ -580,14 +599,46 @@ class Importer:
|
||||
extent = max(extent, cursor)
|
||||
previous = None
|
||||
elif item.tag == 'barline':
|
||||
self.children(item, {'bar-style'})
|
||||
self.attrs(item, {'location'})
|
||||
self.children(item, {'bar-style', 'repeat', 'ending'})
|
||||
side = item.get('location', 'right')
|
||||
if side not in ('left', 'right'):
|
||||
self.fail('barlines and repeats 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:
|
||||
self.fail('barline location disagrees with its musical position')
|
||||
style = item.findtext('bar-style', 'regular')
|
||||
if style not in ('regular', 'light-light', 'light-heavy', 'heavy-light', 'heavy-heavy', 'heavy', 'dashed', 'dotted', 'none', 'tick', 'short'):
|
||||
self.fail('unsupported barline')
|
||||
measure.setdefault('barlines', {})[item.get('location', 'right')] = style
|
||||
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
|
||||
if measure.pop('_right_barline_onset', cursor) != cursor:
|
||||
self.fail('right barline must occur at the end of its measure')
|
||||
expected = meter_length(measure['meter'])
|
||||
meter_changed = any('meter' in a for a in measure['attributes'])
|
||||
if expected and cursor != expected and not measure['implicit'] and not meter_changed:
|
||||
@@ -616,6 +667,9 @@ 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