93 lines
6.0 KiB
Python
93 lines
6.0 KiB
Python
from __future__ import annotations
|
|
|
|
import pathlib
|
|
|
|
import generator.harmony
|
|
import generator.musicxml
|
|
|
|
def same(left, right, location: str) -> None:
|
|
if left != right:
|
|
raise ValueError(f'{location}: engraver changed musical content ({left!r} -> {right!r})')
|
|
|
|
def event_positions(staff: dict) -> dict:
|
|
return {event['id']: (mi, vi, ni) for mi, measure in enumerate(staff['measures']) for vi, voice in enumerate(measure['voices']) for ni, event in enumerate(voice['notes'])}
|
|
|
|
def tuplet_members(staff: dict, identifier: str) -> list:
|
|
positions = event_positions(staff)
|
|
return [positions[event['id']] for measure in staff['measures'] for voice in measure['voices'] for event in voice['notes'] if identifier in event.get('tuplet_ids', [])]
|
|
|
|
def audit_roundtrip(source: pathlib.Path, output: pathlib.Path) -> None:
|
|
before = generator.musicxml.load_musicxml(source)['staves'][0]
|
|
after = generator.musicxml.load_musicxml(output)['staves'][0]
|
|
before_positions, after_positions = event_positions(before), event_positions(after)
|
|
before_instruments = {i['id']: i for i in before.get('instruments', [])}
|
|
after_instruments = {i['id']: i for i in after.get('instruments', [])}
|
|
same(len(before['measures']), len(after['measures']), 'measure count')
|
|
for original, rendered in zip(before['measures'], after['measures']):
|
|
location = original['id']
|
|
for field in ('duration', 'key', 'meter', 'clef', 'transpose', 'attributes'):
|
|
same(original[field], rendered[field], f'{location} {field}')
|
|
same({'capo': 0, **original['staff_details']}, {'capo': 0, **rendered['staff_details']}, f'{location} staff details')
|
|
for side, style in original.get('barlines', {}).items():
|
|
same(style, rendered.get('barlines', {}).get(side, 'regular'), f'{location} {side} barline')
|
|
same(original.get('tempos', []), rendered.get('tempos', []), f'{location} tempos')
|
|
same(original.get('section_label', ''), rendered.get('section_label', ''), f'{location} section label')
|
|
same(len(original['harmonies']), len(rendered['harmonies']), f'{location} harmony count')
|
|
for a, b in zip(original['harmonies'], rendered['harmonies']):
|
|
for field in ('root', 'bass', 'onset'):
|
|
same(a.get(field), b.get(field), f'{location} harmony {field}')
|
|
same(generator.harmony.degrees(a), generator.harmony.degrees(b), f'{location} harmony degrees')
|
|
same(len(original['voices']), len(rendered['voices']), f'{location} voice count')
|
|
for first, second in zip(original['voices'], rendered['voices']):
|
|
same(len(first['notes']), len(second['notes']), f'{location} voice {first["id"]} event count')
|
|
for a, b in zip(first['notes'], second['notes']):
|
|
event_location = f'{location}, voice {first["id"]}, {a["id"]}'
|
|
for field in ('kind', 'onset', 'duration', 'written', 'hidden'):
|
|
same(a[field], b[field], f'{event_location} {field}')
|
|
for field in ('grace', 'cue', 'measure_rest', 'rest_position', 'lyrics'):
|
|
same(a.get(field), b.get(field), f'{event_location} {field}')
|
|
for field in ('stem', 'beams', 'notehead'):
|
|
if field in a:
|
|
same(a[field], b.get(field), f'{event_location} explicit {field}')
|
|
same(len(a['pitches']), len(b['pitches']), f'{event_location} chord tone count')
|
|
for p, q in zip(a['pitches'], b['pitches']):
|
|
if p.get('instrument') and 'unpitched' in p:
|
|
first_instrument = before_instruments[p['instrument']]
|
|
second_instrument = after_instruments.get(q.get('instrument'), {})
|
|
same(first_instrument.get('midi', {}).get('midi-unpitched'), second_instrument.get('midi', {}).get('midi-unpitched'), f'{event_location} percussion instrument')
|
|
for field in ('letter', 'alter', 'octave', 'duration', 'written', 'unpitched', 'tablature', 'notehead', 'cue'):
|
|
same(p.get(field), q.get(field), f'{event_location} pitch {field}')
|
|
for field in ('stem', 'print'):
|
|
if field in p:
|
|
same(p[field], q.get(field), f'{event_location} pitch {field}')
|
|
for field in ('tie_from', 'tie_to'):
|
|
same(bool(p.get(field)), bool(q.get(field)), f'{event_location} {field}')
|
|
if display := p.get('accidental_display'):
|
|
other = q.get('accidental_display', {})
|
|
same(display.get('name'), other.get('name'), f'{event_location} accidental')
|
|
for field in ('parentheses', 'bracket'):
|
|
same(display.get(field, 'no'), other.get(field, 'no'), f'{event_location} accidental {field}')
|
|
for field in ('cautionary', 'editorial'):
|
|
if field in display:
|
|
same(display[field], other.get(field, 'no'), f'{event_location} accidental {field}')
|
|
if tremolo := a.get('tremolo'):
|
|
other = b.get('tremolo', {})
|
|
for field in ('type', 'marks'):
|
|
same(tremolo[field], other.get(field), f'{event_location} tremolo {field}')
|
|
if 'partner' in tremolo:
|
|
same(before_positions.get(tremolo['partner']), after_positions.get(other.get('partner')), f'{event_location} tremolo partner')
|
|
same(len(before.get('tuplets', [])), len(after.get('tuplets', [])), 'tuplet count')
|
|
for a, b in zip(before.get('tuplets', []), after.get('tuplets', [])):
|
|
for field in ('note_count', 'space_count', 'depth', 'show_number', 'show_type'):
|
|
same(a.get(field), b.get(field), f'{a["id"]} {field}')
|
|
if a['bracket'] != 'auto':
|
|
same(a['bracket'], b['bracket'], f'{a["id"]} bracket')
|
|
same(tuplet_members(before, a['id']), tuplet_members(after, b['id']), f'{a["id"]} members')
|
|
for endpoint in ('start', 'end'):
|
|
same(before_positions[a[endpoint]], after_positions[b[endpoint]], f'{a["id"]} {endpoint}')
|
|
for field in ('tuplet_actual', 'tuplet_normal'):
|
|
if a.get(field, {}).get('type'):
|
|
same(a[field], b.get(field), f'{a["id"]} {field}')
|
|
same([{k: v for k, v in s.items() if k != 'id'} for s in before.get('octave_shifts', [])],
|
|
[{k: v for k, v in s.items() if k != 'id'} for s in after.get('octave_shifts', [])], 'octave spans')
|