"""Read optional reference MusicXML literally, without repairing it or composing.""" from __future__ import annotations import argparse import hashlib import json from pathlib import Path from fractions import Fraction as F import xml.etree.ElementTree as ET def read(path, role, form_start=None): raw=Path(path).read_bytes();tree=ET.fromstring(raw) result={'path':str(Path(path).resolve()),'sha256':hashlib.sha256(raw).hexdigest(),'validation_role':role, 'pitch_convention':'encoded MusicXML; no inferred guitar octave','part_names':[n.text for n in tree.findall('./part-list/score-part/part-name')], 'measures':[],'notes':[],'harmonies':[],'annotations':[],'discrepancies':[]} divisions=F(1);absolute=F();nominal=F(4);pending={};active=None for index,m in enumerate(tree.findall('./part/measure')): cursor=F();extent=F();last_start=F();last_duration=F() if (value:=m.findtext('attributes/divisions')) is not None:divisions=F(value) meter=m.find('attributes/time') if meter is not None and meter.findtext('beats'): nominal=sum((F(v) for v in meter.findtext('beats').split('+')),F())*4/F(meter.findtext('beat-type')) for item in m: offset=F(item.findtext('offset','0'))/divisions if item.tag=='backup':cursor-=F(item.findtext('duration'))/divisions elif item.tag=='forward':cursor+=F(item.findtext('duration'))/divisions;extent=max(extent,cursor) elif item.tag=='harmony': active={'time':str(absolute+cursor+offset),'measure':index+1,'offset':str(cursor+offset), 'root':item.findtext('root/root-step'),'alter':item.findtext('root/root-alter','0'), 'kind':item.findtext('kind'),'display':item.find('kind').get('text') if item.find('kind') is not None else None, 'bass':ET.tostring(item.find('bass'),encoding='unicode') if item.find('bass') is not None else None, 'degrees':[{'value':d.findtext('degree-value'),'alter':d.findtext('degree-alter'),'type':d.findtext('degree-type')} for d in item.findall('degree')]} result['harmonies'].append(active) elif item.tag=='direction': for words in item.findall('direction-type/words'): result['annotations'].append({'time':str(absolute+cursor+offset),'measure':index+1,'text':''.join(words.itertext()),'interpretation':'descriptive; no production permission'}) elif item.tag=='note': duration=F(item.findtext('duration','0'))/divisions start=last_start if item.find('chord') is not None else cursor end=start+duration;voice=item.findtext('voice','1');p=item.find('pitch') encoded={'step':p.findtext('step'),'alter':p.findtext('alter','0'),'octave':int(p.findtext('octave'))} if p is not None else None kinds={t.get('type') for t in item.findall('tie')} | {t.get('type') for t in item.findall('notations/tied')} event={'start':str(absolute+start),'end':str(absolute+end),'measure':index+1,'offset':str(start),'pitch':encoded,'rest':item.find('rest') is not None, 'written_type':item.findtext('type'),'dots':len(item.findall('dot')),'voice':voice,'ties':sorted(kinds), 'time_modification':ET.tostring(item.find('time-modification'),encoding='unicode') if item.find('time-modification') is not None else None, 'tuplets':[dict(t.attrib) for t in item.findall('notations/tuplet')],'grace':item.find('grace') is not None,'new_attack':'stop' not in kinds and p is not None} key=(voice,json.dumps(encoded,sort_keys=True)) if 'stop' in kinds: old=pending.get(key) if old is None or result['notes'][old]['end']!=event['start']: result['discrepancies'].append({'measure':index+1,'issue':'unmatched/noncontiguous tie; retained literally'}) else:event['continuation_of']=old if 'start' not in kinds:pending.pop(key,None) if 'start' in kinds:pending[key]=len(result['notes']) result['notes'].append(event) if item.find('chord') is None and item.find('grace') is None:cursor=end last_start,last_duration=start,duration;extent=max(extent,end,cursor) if extent!=nominal and m.get('implicit')!='yes':result['discrepancies'].append({'measure':index+1,'issue':f'encoded span {extent} differs from nominal {nominal}; no padding applied'}) result['measures'].append({'number':m.get('number'),'start':str(absolute),'duration':str(extent),'implicit':m.get('implicit'),'initial_harmony_known':any(F(h['time'])<=absolute for h in result['harmonies'])}) absolute+=extent if pending:result['discrepancies'].append({'issue':'unterminated ties retained'}) # Do not run the strict score importer over these references: annotations, # unusual timing and high encoded pitches are evidence, not repair requests. from generator import harmony, model, fretboard def semantic(h): return {'root':{'letter':h['root'].lower(),'alter':model.rational(F(h['alter']))},'kind':h['kind'], 'degrees':[{'value':int(d['value']),'alter':model.rational(F(d['alter'])),'type':d['type']} for d in h['degrees']]} for event in result['notes']: if not event['pitch']:continue p=event['pitch'];number=12*(p['octave']+1)+model.NATURAL[p['step'].lower()]+F(p['alter']) event['encoded_midi']=str(number) event['legal_positions_at_encoded_pitch']=len(fretboard.single_positions_for_pitch(number)) event['harmonic_spans']=[] start,end=F(event['start']),F(event['end']) changes=[F(h['time']) for h in result['harmonies'] if startt),None) label['current_harmony']=current[-1] if current else None label['following_harmony']=future label['events']=[n for n in result['notes'] if F(n['start'])<=t