+68
-74
@@ -1,14 +1,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
from fractions import Fraction
|
||||
from pathlib import Path
|
||||
import fractions
|
||||
import pathlib
|
||||
|
||||
from defusedxml import ElementTree
|
||||
import defusedxml.ElementTree
|
||||
|
||||
from generator import harmony
|
||||
from generator.model import (ACCIDENTALS, TYPE_LENGTHS, alteration, fraction, key_from_fifths, key_label, meter_label, meter_length, pitch_number, rational, validate_document, written_for, written_length, )
|
||||
from generator.styles import canonical_style
|
||||
import generator.harmony
|
||||
import generator.model
|
||||
|
||||
class MusicXMLError(ValueError):
|
||||
pass
|
||||
@@ -18,11 +17,11 @@ LAYOUT = {'default-x', 'default-y', 'relative-x', 'relative-y', 'color', 'font-f
|
||||
IGNORED_TOP = {'defaults', 'credit', 'work', 'movement-number', 'movement-title', 'identification', 'part-list'}
|
||||
|
||||
class Importer:
|
||||
def __init__(self, path: Path) -> None:
|
||||
def __init__(self, path: pathlib.Path) -> None:
|
||||
self.path = path
|
||||
self.location = str(path)
|
||||
self.divisions = Fraction(1)
|
||||
self.key = key_from_fifths(0)
|
||||
self.divisions = fractions.Fraction(1)
|
||||
self.key = generator.model.key_from_fifths(0)
|
||||
self.meter = {'groups': [{'beats': [4], 'unit': 4}], 'symbol': 'normal'}
|
||||
self.clef = {'sign': 'G', 'line': 2, 'octave_change': 0}
|
||||
self.transpose = {'diatonic': 0, 'chromatic': 0, 'octave_change': 0}
|
||||
@@ -36,7 +35,7 @@ class Importer:
|
||||
self.octaves: list[dict] = []
|
||||
self.voice_id = '1'
|
||||
self.staff_details = {'lines': 5, 'tunings': []}
|
||||
self.absolute = Fraction()
|
||||
self.absolute = fractions.Fraction()
|
||||
self.tremolo_starts = {}
|
||||
def fail(self, text: str):
|
||||
raise MusicXMLError(f'{self.location}: {text}')
|
||||
@@ -48,16 +47,16 @@ class Importer:
|
||||
for name in node.attrib:
|
||||
if name not in LAYOUT and name not in allowed:
|
||||
self.fail(f'unsupported {node.tag} attribute {name!r}')
|
||||
def number(self, value: str | None, *, integer: bool = False, positive: bool = False) -> Fraction | int:
|
||||
def number(self, value: str | None, *, integer: bool = False, positive: bool = False) -> fractions.Fraction | int:
|
||||
try:
|
||||
result = Fraction(value or '')
|
||||
result = fractions.Fraction(value or '')
|
||||
if integer and result.denominator != 1 or positive and result <= 0:
|
||||
raise ValueError()
|
||||
rational(result)
|
||||
generator.model.rational(result)
|
||||
return int(result) if integer else result
|
||||
except (ValueError, ZeroDivisionError, OverflowError):
|
||||
self.fail(f'invalid {"integer" if integer else "number"}: {value!r}')
|
||||
def offset(self, node, cursor: Fraction) -> Fraction:
|
||||
def offset(self, node, cursor: fractions.Fraction) -> fractions.Fraction:
|
||||
return cursor + self.number(node.findtext('offset', '0')) / self.divisions
|
||||
def identity(self, node) -> None:
|
||||
voice = node.findtext('voice', '1' if node.tag in ('note', 'forward') else self.voice_id)
|
||||
@@ -69,9 +68,9 @@ class Importer:
|
||||
if not voice.strip():
|
||||
self.fail('empty voice identifier')
|
||||
self.voice_id = voice
|
||||
def attributes(self, node, onset: Fraction) -> dict:
|
||||
def attributes(self, node, onset: fractions.Fraction) -> dict:
|
||||
self.children(node, {'divisions', 'key', 'time', 'clef', 'staves', 'transpose', 'instruments', 'staff-details'})
|
||||
result = {'onset': rational(onset)}
|
||||
result = {'onset': generator.model.rational(onset)}
|
||||
for item in node:
|
||||
if item.get('number', '1') != '1':
|
||||
self.fail('attributes for multiple staves are not supported')
|
||||
@@ -84,9 +83,9 @@ class Importer:
|
||||
self.attrs(item, {'number', 'print-object'})
|
||||
self.children(item, {'cancel', 'fifths', 'mode', 'key-step', 'key-alter', 'key-accidental', 'key-octave'})
|
||||
if item.find('fifths') is not None:
|
||||
self.key = key_from_fifths(self.number(item.findtext('fifths'), integer=True), item.findtext('mode', 'major'))
|
||||
self.key = generator.model.key_from_fifths(self.number(item.findtext('fifths'), integer=True), item.findtext('mode', 'major'))
|
||||
else:
|
||||
self.key = {'fifths': 0, 'mode': 'none', 'tonic': None, 'alterations': [rational(0) for _ in range(7)], 'custom': True, 'steps': []}
|
||||
self.key = {'fifths': 0, 'mode': 'none', 'tonic': None, 'alterations': [generator.model.rational(0) for _ in range(7)], 'custom': True, 'steps': []}
|
||||
pending = None
|
||||
for field in item:
|
||||
if field.tag == 'key-step':
|
||||
@@ -96,7 +95,7 @@ class Importer:
|
||||
elif field.tag == 'key-alter':
|
||||
if pending is None:
|
||||
self.fail('key alteration without a step')
|
||||
amount = rational(self.number(field.text))
|
||||
amount = generator.model.rational(self.number(field.text))
|
||||
self.key['alterations']['cdefgab'.index(pending)] = amount
|
||||
self.key['steps'].append({'letter': pending, 'alter': amount})
|
||||
pending = None
|
||||
@@ -155,7 +154,7 @@ class Importer:
|
||||
self.staff_details = {'lines': self.number(item.findtext('staff-lines', '5'), integer=True, positive=True), 'tunings': [], 'capo': self.number(item.findtext('capo', '0'), integer=True)}
|
||||
for tuning in item.findall('staff-tuning'):
|
||||
self.children(tuning, {'tuning-step', 'tuning-alter', 'tuning-octave'})
|
||||
self.staff_details['tunings'].append({'line': self.number(tuning.get('line'), integer=True, positive=True), 'letter': tuning.findtext('tuning-step', '').lower(), 'alter': rational(self.number(tuning.findtext('tuning-alter', '0'))), 'octave': self.number(tuning.findtext('tuning-octave'), integer=True)})
|
||||
self.staff_details['tunings'].append({'line': self.number(tuning.get('line'), integer=True, positive=True), 'letter': tuning.findtext('tuning-step', '').lower(), 'alter': generator.model.rational(self.number(tuning.findtext('tuning-alter', '0'))), 'octave': self.number(tuning.findtext('tuning-octave'), integer=True)})
|
||||
result['staff_details'] = copy.deepcopy(self.staff_details)
|
||||
elif item.tag == 'transpose':
|
||||
self.children(item, {'diatonic', 'chromatic', 'octave-change'})
|
||||
@@ -169,7 +168,7 @@ class Importer:
|
||||
self.transpose['diatonic'] = (1 if semitones >= 0 else -1) * (7 * (magnitude // 12) + (0, 0, 1, 2, 2, 3, 3, 4, 5, 5, 6, 6)[magnitude % 12])
|
||||
result['transpose'] = copy.deepcopy(self.transpose)
|
||||
return result
|
||||
def read_harmony(self, node, cursor: Fraction) -> dict:
|
||||
def read_harmony(self, node, cursor: fractions.Fraction) -> dict:
|
||||
self.attrs(node, {'print-object', 'print-frame', 'type'})
|
||||
self.children(node, {'root', 'kind', 'degree', 'bass', 'inversion', 'offset', 'staff'})
|
||||
if len(node.findall('kind')) != 1 or len(node.findall('root')) > 1:
|
||||
@@ -177,7 +176,7 @@ class Importer:
|
||||
self.identity(node)
|
||||
kind = node.find('kind')
|
||||
self.harmony_sequence += 1
|
||||
result = {'id': f'harmony-{self.harmony_sequence}', 'onset': rational(self.offset(node, cursor)), 'kind': kind.text, 'degrees': []}
|
||||
result = {'id': f'harmony-{self.harmony_sequence}', 'onset': generator.model.rational(self.offset(node, cursor)), 'kind': kind.text, 'degrees': []}
|
||||
self.attrs(kind, {'text', 'use-symbols', 'stack-degrees', 'parentheses-degrees', 'bracket-degrees'})
|
||||
for tag in ('root', 'bass'):
|
||||
item = node.find(tag)
|
||||
@@ -189,7 +188,7 @@ class Importer:
|
||||
step = item.findtext(tag + '-step', '').lower()
|
||||
if len(step) != 1 or step not in 'cdefgab':
|
||||
self.fail('invalid harmony root/bass')
|
||||
result[tag] = {'letter': step, 'alter': rational(self.number(item.findtext(tag + '-alter', '0')))}
|
||||
result[tag] = {'letter': step, 'alter': generator.model.rational(self.number(item.findtext(tag + '-alter', '0')))}
|
||||
for degree in node.findall('degree'):
|
||||
self.children(degree, {'degree-value', 'degree-alter', 'degree-type'})
|
||||
value = self.number(degree.findtext('degree-value'), integer=True, positive=True)
|
||||
@@ -197,15 +196,15 @@ class Importer:
|
||||
operation = degree.findtext('degree-type')
|
||||
# MusicXML alterations are relative to the kind; additions use dominant intervals.
|
||||
if operation == 'alter':
|
||||
degree_alter += harmony.KINDS.get(result['kind'], {}).get(value, 0)
|
||||
degree_alter += generator.harmony.KINDS.get(result['kind'], {}).get(value, 0)
|
||||
elif operation == 'add' and (value - 1) % 7 == 6:
|
||||
degree_alter -= 1
|
||||
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})
|
||||
result['degrees'].append({'value': value, 'alter': generator.model.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)
|
||||
generator.harmony.normalize_text(result, kind.get('text'))
|
||||
chord_degrees = generator.harmony.degrees(result)
|
||||
if node.find('inversion') is not None:
|
||||
inversion = self.number(node.findtext('inversion'), integer=True)
|
||||
result['inversion'] = inversion
|
||||
@@ -216,12 +215,11 @@ class Importer:
|
||||
root = result['root']
|
||||
letter_index = 'cdefgab'.index(root['letter']) + degree - 1
|
||||
bass_letter = 'cdefgab'[letter_index % 7]
|
||||
from generator.model import NATURAL
|
||||
target = NATURAL[root['letter']] + alteration(root['alter']) + harmony.degree_semitones(degree, chord_degrees[degree])
|
||||
result['bass'] = {'letter': bass_letter, 'alter': rational(target - (12 * (letter_index // 7) + NATURAL[bass_letter]))}
|
||||
result['symbol'] = harmony.symbol(result)
|
||||
target = generator.model.NATURAL[root['letter']] + generator.model.alteration(root['alter']) + generator.harmony.degree_semitones(degree, chord_degrees[degree])
|
||||
result['bass'] = {'letter': bass_letter, 'alter': generator.model.rational(target - (12 * (letter_index // 7) + generator.model.NATURAL[bass_letter]))}
|
||||
result['symbol'] = generator.harmony.symbol(result)
|
||||
return result
|
||||
def direction(self, node, cursor: Fraction, measure: dict, optional: list[Fraction]) -> None:
|
||||
def direction(self, node, cursor: fractions.Fraction, measure: dict, optional: list[fractions.Fraction]) -> None:
|
||||
self.attrs(node, {'print-object', 'directive', 'system'})
|
||||
self.children(node, {'direction-type', 'offset', 'staff', 'voice', 'sound'})
|
||||
onset = self.offset(node, cursor)
|
||||
@@ -245,7 +243,7 @@ class Importer:
|
||||
size = self.number(item.get('size', '8'), integer=True)
|
||||
if size not in (8, 15, 22):
|
||||
self.fail('octave shifts support 8, 15, or 22')
|
||||
position = rational(self.absolute + onset)
|
||||
position = generator.model.rational(self.absolute + onset)
|
||||
if action in ('up', 'down'):
|
||||
if number in self.open_octaves:
|
||||
self.fail('duplicate octave-shift start')
|
||||
@@ -261,20 +259,20 @@ class Importer:
|
||||
elif item.tag == 'metronome':
|
||||
self.children(item, {'beat-unit', 'beat-unit-dot', 'per-minute'})
|
||||
written = {'type': item.findtext('beat-unit'), 'dots': len(item.findall('beat-unit-dot'))}
|
||||
written_length(written)
|
||||
generator.model.written_length(written)
|
||||
bpm = self.number(item.findtext('per-minute'), positive=True)
|
||||
measure.setdefault('tempos', []).append({'onset': rational(onset), 'written': written, 'bpm': rational(bpm)})
|
||||
measure.setdefault('tempos', []).append({'onset': generator.model.rational(onset), 'written': written, 'bpm': generator.model.rational(bpm)})
|
||||
sound = node.find('sound')
|
||||
if sound is not None:
|
||||
self.sound(sound, onset, measure)
|
||||
def sound(self, node, onset: Fraction, measure: dict) -> None:
|
||||
def sound(self, node, onset: fractions.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', [])):
|
||||
measure.setdefault('tempos', []).append({'onset': rational(onset), 'written': {'type': 'quarter', 'dots': 0}, 'bpm': rational(self.number(node.get('tempo'), positive=True))})
|
||||
def read_note(self, node, cursor: Fraction, previous: dict | None) -> tuple[dict, Fraction, bool]:
|
||||
if node.get('tempo') and not any(generator.model.fraction(t['onset']) == onset for t in measure.get('tempos', [])):
|
||||
measure.setdefault('tempos', []).append({'onset': generator.model.rational(onset), 'written': {'type': 'quarter', 'dots': 0}, 'bpm': generator.model.rational(self.number(node.get('tempo'), positive=True))})
|
||||
def read_note(self, node, cursor: fractions.Fraction, previous: dict | None) -> tuple[dict, fractions.Fraction, bool]:
|
||||
self.attrs(node, {'print-object', 'print-dot', 'print-spacing', 'default-x', 'dynamics', 'end-dynamics'})
|
||||
if node.get('dynamics') or node.get('end-dynamics'):
|
||||
self.fail('note dynamics are not supported')
|
||||
@@ -288,34 +286,34 @@ class Importer:
|
||||
voice = self.voice_id
|
||||
is_chord = node.find('chord') is not None
|
||||
grace = node.find('grace')
|
||||
duration = Fraction() if grace is not None else self.number(node.findtext('duration'), positive=True) / self.divisions
|
||||
onset = fraction(previous['onset']) if is_chord and previous else cursor
|
||||
if is_chord and (previous is None or previous['voice'] != voice or previous['kind'] != 'note' or duration > fraction(previous['duration'])):
|
||||
duration = fractions.Fraction() if grace is not None else self.number(node.findtext('duration'), positive=True) / self.divisions
|
||||
onset = generator.model.fraction(previous['onset']) if is_chord and previous else cursor
|
||||
if is_chord and (previous is None or previous['voice'] != voice or previous['kind'] != 'note' or duration > generator.model.fraction(previous['duration'])):
|
||||
self.fail('invalid chord tone or chord tone longer than its anchor')
|
||||
if is_chord and (grace is not None) != ('grace' in previous):
|
||||
self.fail('chord tones must share grace/regular timing')
|
||||
written = {'type': node.findtext('type'), 'dots': len(node.findall('dot'))}
|
||||
if written['type'] is None:
|
||||
written = {'type': 'whole', 'dots': 0} if node.find('rest') is not None and node.find('rest').get('measure') == 'yes' else written_for(duration)
|
||||
written_length(written)
|
||||
written = {'type': 'whole', 'dots': 0} if node.find('rest') is not None and node.find('rest').get('measure') == 'yes' else generator.model.written_for(duration)
|
||||
generator.model.written_length(written)
|
||||
modulation = node.find('time-modification')
|
||||
if modulation is not None:
|
||||
self.children(modulation, {'actual-notes', 'normal-notes', 'normal-type', 'normal-dot'})
|
||||
actual = self.number(modulation.findtext('actual-notes'), integer=True, positive=True)
|
||||
normal = self.number(modulation.findtext('normal-notes'), integer=True, positive=True)
|
||||
written['ratio'] = rational(Fraction(normal, actual))
|
||||
written['ratio'] = generator.model.rational(fractions.Fraction(normal, actual))
|
||||
if modulation.find('normal-type') is not None:
|
||||
written['normal'] = {'type': modulation.findtext('normal-type'), 'dots': len(modulation.findall('normal-dot'))}
|
||||
written_length(written['normal'])
|
||||
generator.model.written_length(written['normal'])
|
||||
elif modulation.findall('normal-dot'):
|
||||
self.fail('<normal-dot> requires <normal-type>')
|
||||
measure_rest = node.find('rest') is not None and node.find('rest').get('measure') == 'yes'
|
||||
if grace is None and not measure_rest:
|
||||
expected = written_length(written) * fraction(written.get('ratio', rational(1)))
|
||||
expected = generator.model.written_length(written) * generator.model.fraction(written.get('ratio', generator.model.rational(1)))
|
||||
if duration != expected:
|
||||
self.fail(f'<note> duration {duration} disagrees with written duration/time-modification {expected}')
|
||||
self.sequence += 1
|
||||
note = {'id': f'note-{self.sequence}', 'voice': voice, 'kind': 'rest' if node.find('rest') is not None else 'note', 'onset': rational(onset), 'duration': rational(duration), 'written': written, 'hidden': node.get('print-object') == 'no', 'pitches': []}
|
||||
note = {'id': f'note-{self.sequence}', 'voice': voice, 'kind': 'rest' if node.find('rest') is not None else 'note', 'onset': generator.model.rational(onset), 'duration': generator.model.rational(duration), 'written': written, 'hidden': node.get('print-object') == 'no', 'pitches': []}
|
||||
if is_chord:
|
||||
note = previous
|
||||
if grace is not None and not is_chord:
|
||||
@@ -324,7 +322,7 @@ class Importer:
|
||||
for name in ('steal-time-previous', 'steal-time-following', 'make-time'):
|
||||
if name in grace.attrib:
|
||||
value = self.number(grace.get(name), positive=True)
|
||||
note['grace'][name] = rational(value / self.divisions if name == 'make-time' else value)
|
||||
note['grace'][name] = generator.model.rational(value / self.divisions if name == 'make-time' else value)
|
||||
if node.find('cue') is not None and not is_chord:
|
||||
note['cue'] = True
|
||||
if node.find('rest') is not None:
|
||||
@@ -335,12 +333,12 @@ class Importer:
|
||||
note['rest_position'] = {'letter': node.findtext('rest/display-step').lower(), 'octave': self.number(node.findtext('rest/display-octave'), integer=True)}
|
||||
else:
|
||||
pitch_node = node.find('pitch')
|
||||
p = {'id': f'{note["id"]}-p{len(note["pitches"]) + 1}', 'duration': rational(duration), 'written': written}
|
||||
p = {'id': f'{note["id"]}-p{len(note["pitches"]) + 1}', 'duration': generator.model.rational(duration), 'written': written}
|
||||
if pitch_node is not None:
|
||||
self.children(pitch_node, {'step', 'alter', 'octave'})
|
||||
p.update({'letter': pitch_node.findtext('step', '').lower(), 'alter': rational(self.number(pitch_node.findtext('alter', '0'))), 'octave': self.number(pitch_node.findtext('octave'), integer=True)})
|
||||
pitch_number(p)
|
||||
value = (voice, p['letter'], alteration(p['alter']), p['octave'])
|
||||
p.update({'letter': pitch_node.findtext('step', '').lower(), 'alter': generator.model.rational(self.number(pitch_node.findtext('alter', '0'))), 'octave': self.number(pitch_node.findtext('octave'), integer=True)})
|
||||
generator.model.pitch_number(p)
|
||||
value = (voice, p['letter'], generator.model.alteration(p['alter']), p['octave'])
|
||||
elif node.find('unpitched') is not None:
|
||||
self.children(node.find('unpitched'), {'display-step', 'display-octave'})
|
||||
p['unpitched'] = {'letter': node.findtext('unpitched/display-step', 'B').lower(), 'octave': self.number(node.findtext('unpitched/display-octave', '4'), integer=True)}
|
||||
@@ -362,7 +360,7 @@ class Importer:
|
||||
accidental = node.find('accidental')
|
||||
if accidental is not None:
|
||||
self.attrs(accidental, {'cautionary', 'editorial', 'parentheses', 'bracket', 'size', 'smufl'})
|
||||
if 'unpitched' in p or accidental.text in ACCIDENTALS and ACCIDENTALS[accidental.text] != alteration(p['alter']):
|
||||
if 'unpitched' in p or accidental.text in generator.model.ACCIDENTALS and generator.model.ACCIDENTALS[accidental.text] != generator.model.alteration(p['alter']):
|
||||
self.fail('contradictory accidental')
|
||||
p['accidental_display'] = {'name': accidental.text, **{k: v for k, v in accidental.attrib.items() if k not in LAYOUT}}
|
||||
if node.find('notehead') is not None:
|
||||
@@ -384,7 +382,7 @@ class Importer:
|
||||
self.fail('unsupported tie notation')
|
||||
if 'stop' in ties:
|
||||
source = self.open_ties.pop(value, None)
|
||||
if source is None or source['_absolute'] + fraction(source['duration']) != p['_absolute']:
|
||||
if source is None or source['_absolute'] + generator.model.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:
|
||||
@@ -491,7 +489,7 @@ class Importer:
|
||||
self.active_tuplets.pop(number)['end'] = note['id']
|
||||
return note, cursor if is_chord else cursor + duration, is_chord
|
||||
def read(self) -> dict:
|
||||
root = ElementTree.parse(self.path, forbid_entities=True, forbid_external=True).getroot()
|
||||
root = defusedxml.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.children(root, IGNORED_TOP | {'part'})
|
||||
@@ -519,21 +517,18 @@ class Importer:
|
||||
instruments.append(instrument)
|
||||
if any(item.get('id') not in {i['id'] for i in instruments} for item in definitions[0].findall('midi-instrument')):
|
||||
self.fail('MIDI definition refers to an unknown score instrument')
|
||||
fields = {}
|
||||
for item in root.findall('identification/miscellaneous/miscellaneous-field'):
|
||||
name = item.get('name', '')
|
||||
if name.startswith('vamp:'):
|
||||
if name != 'vamp:style' or name in fields:
|
||||
self.fail(f'unknown or duplicate Vamp property {name!r}')
|
||||
fields[name] = item.text or ''
|
||||
metadata = {'title': root.findtext('work/work-title') or root.findtext('movement-title') or self.path.stem, 'composer': ' / '.join(c.text for c in root.findall('identification/creator') if c.get('type') == 'composer' and c.text), 'style': canonical_style(fields.get('vamp:style'))}
|
||||
self.fail(f'unsupported Vamp property {name!r}')
|
||||
metadata = {'title': root.findtext('work/work-title') or root.findtext('movement-title') or self.path.stem, 'composer': ' / '.join(c.text for c in root.findall('identification/creator') if c.get('type') == 'composer' and c.text)}
|
||||
measures = []
|
||||
self.children(parts[0], {'measure'})
|
||||
for index, bar in enumerate(parts[0]):
|
||||
self.location = f'{self.path}: part {parts[0].get("id")}, measure {bar.get("number", str(index + 1))}'
|
||||
self.children(bar, {'attributes', 'note', 'direction', 'sound', 'harmony', 'forward', 'backup', 'barline', 'print'})
|
||||
self.attrs(bar, {'number', 'implicit', 'non-controlling', 'width', 'text'})
|
||||
cursor, extent = Fraction(), Fraction()
|
||||
cursor, extent = fractions.Fraction(), fractions.Fraction()
|
||||
voices = {}
|
||||
previous = None
|
||||
optional = []
|
||||
@@ -567,10 +562,9 @@ class Importer:
|
||||
self.children(item, {'duration', 'voice', 'staff'})
|
||||
self.identity(item)
|
||||
duration = self.number(item.findtext('duration'), positive=True) / self.divisions
|
||||
from generator.model import make_event
|
||||
if item.find('voice') is not None:
|
||||
self.sequence += 1
|
||||
voices.setdefault(self.voice_id, []).append(make_event(f'spacer-{self.sequence}', cursor, duration, 'spacer') | {'voice': self.voice_id})
|
||||
voices.setdefault(self.voice_id, []).append(generator.model.make_event(f'spacer-{self.sequence}', cursor, duration, 'spacer') | {'voice': self.voice_id})
|
||||
cursor += duration
|
||||
extent = max(extent, cursor)
|
||||
previous = None
|
||||
@@ -597,22 +591,22 @@ class Importer:
|
||||
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'])
|
||||
expected = generator.model.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:
|
||||
self.fail(f'measure has {cursor} quarter notes, expected {expected}; mark a partial measure implicit')
|
||||
measure['duration'] = rational(cursor)
|
||||
measure['key_signature'], measure['time_signature'] = key_label(measure['key']), meter_label(measure['meter'])
|
||||
measure['voices'] = [{'id': voice, 'notes': sorted(events, key=lambda n: fraction(n['onset']))} for voice, events in voices.items()]
|
||||
measure['harmonies'].sort(key=lambda h: fraction(h['onset']))
|
||||
measure['duration'] = generator.model.rational(cursor)
|
||||
measure['key_signature'], measure['time_signature'] = generator.model.key_label(measure['key']), generator.model.meter_label(measure['meter'])
|
||||
measure['voices'] = [{'id': voice, 'notes': sorted(events, key=lambda n: generator.model.fraction(n['onset']))} for voice, events in voices.items()]
|
||||
measure['harmonies'].sort(key=lambda h: generator.model.fraction(h['onset']))
|
||||
used = set()
|
||||
for onset in optional:
|
||||
matches = [h for h in measure['harmonies'] if fraction(h['onset']) == onset]
|
||||
matches = [h for h in measure['harmonies'] if generator.model.fraction(h['onset']) == onset]
|
||||
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])
|
||||
matches[0]['symbol'] = generator.harmony.symbol(matches[0])
|
||||
used.add(onset)
|
||||
measures.append(measure)
|
||||
self.absolute += cursor
|
||||
@@ -627,11 +621,11 @@ class Importer:
|
||||
metadata.update({'key_signature': measures[0]['key_signature'], 'time_signature': measures[0]['time_signature'], 'key': copy.deepcopy(measures[0]['key'])})
|
||||
result = {'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
|
||||
validate_document(result)
|
||||
generator.model.validate_document(result)
|
||||
return result
|
||||
|
||||
def load_musicxml(path: Path) -> dict:
|
||||
importer = Importer(Path(path))
|
||||
def load_musicxml(path: pathlib.Path) -> dict:
|
||||
importer = Importer(pathlib.Path(path))
|
||||
try:
|
||||
return importer.read()
|
||||
except MusicXMLError:
|
||||
|
||||
Reference in New Issue
Block a user