+160
-119
@@ -1,152 +1,193 @@
|
||||
"""Literal authored-score reader for training, registered repertoire and comparison."""
|
||||
from __future__ import annotations
|
||||
import hashlib,json
|
||||
from pathlib import Path
|
||||
from fractions import Fraction as F
|
||||
import xml.etree.ElementTree as ET
|
||||
import hashlib, json
|
||||
import pathlib
|
||||
import fractions
|
||||
import xml.etree.ElementTree
|
||||
import generator.fretboard
|
||||
|
||||
def read(path, role, form_start=None, pitch_convention='encoded'):
|
||||
if pitch_convention not in ('encoded','guitar-written'):
|
||||
raise ValueError('Unknown reference pitch convention: '+pitch_convention)
|
||||
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':pitch_convention,'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;transpose=F()
|
||||
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 pitch_convention not in ('encoded', 'guitar-written'):
|
||||
raise ValueError('Unknown reference pitch convention: ' + pitch_convention)
|
||||
raw = pathlib.Path(path).read_bytes()
|
||||
tree = xml.etree.ElementTree.fromstring(raw)
|
||||
result = {'path': str(pathlib.Path(path).resolve()), 'sha256': hashlib.sha256(raw).hexdigest(), 'validation_role': role,
|
||||
'pitch_convention': pitch_convention, 'part_names': [n.text for n in tree.findall('./part-list/score-part/part-name')],
|
||||
'measures': [], 'notes': [], 'harmonies': [], 'annotations': [], 'discrepancies': []}
|
||||
divisions = fractions.Fraction(1)
|
||||
absolute = fractions.Fraction()
|
||||
nominal = fractions.Fraction(4)
|
||||
pending = {}
|
||||
active = None
|
||||
transpose = fractions.Fraction()
|
||||
for index, m in enumerate(tree.findall('./part/measure')):
|
||||
cursor = fractions.Fraction()
|
||||
extent = fractions.Fraction()
|
||||
last_start = fractions.Fraction()
|
||||
last_duration = fractions.Fraction()
|
||||
if (value := m.findtext('attributes/divisions')) is not None:
|
||||
divisions = fractions.Fraction(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'))
|
||||
nominal = sum((fractions.Fraction(v) for v in meter.findtext('beats').split('+')), fractions.Fraction()) * 4 / fractions.Fraction(meter.findtext('beat-type'))
|
||||
for item in m:
|
||||
offset=F(item.findtext('offset','0'))/divisions
|
||||
if item.tag=='attributes' and item.find('transpose') is not None:
|
||||
transpose=F(item.findtext('transpose/chromatic','0'))+12*F(item.findtext('transpose/octave-change','0'))
|
||||
elif 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')]}
|
||||
offset = fractions.Fraction(item.findtext('offset', '0')) / divisions
|
||||
if item.tag == 'attributes' and item.find('transpose') is not None:
|
||||
transpose = fractions.Fraction(item.findtext('transpose/chromatic', '0')) + 12 * fractions.Fraction(item.findtext('transpose/octave-change', '0'))
|
||||
elif item.tag == 'backup':
|
||||
cursor -= fractions.Fraction(item.findtext('duration')) / divisions
|
||||
elif item.tag == 'forward':
|
||||
cursor += fractions.Fraction(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': xml.etree.ElementTree.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':
|
||||
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),
|
||||
'sounding_transposition':str(-12 if pitch_convention=='guitar-written' else transpose),
|
||||
'tablature':{'string':int(item.findtext('notations/technical/string')),'fret':int(item.findtext('notations/technical/fret'))} if item.find('notations/technical/string') is not None and item.find('notations/technical/fret') is not None else None,
|
||||
'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))
|
||||
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 = fractions.Fraction(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),
|
||||
'sounding_transposition': str(-12 if pitch_convention == 'guitar-written' else transpose),
|
||||
'tablature': {'string': int(item.findtext('notations/technical/string')), 'fret': int(item.findtext('notations/technical/fret'))} if item.find('notations/technical/string') is not None and item.find('notations/technical/fret') is not None else None,
|
||||
'time_modification': xml.etree.ElementTree.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'])
|
||||
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)
|
||||
for container in ('notations/technical/fingering','notations/technical/other-technical','notations/other-notation'):
|
||||
for container in ('notations/technical/fingering', 'notations/technical/other-technical', 'notations/other-notation'):
|
||||
for label in item.findall(container):
|
||||
text=''.join(label.itertext()).strip()
|
||||
if text:result['annotations'].append({'time':str(absolute+start),'measure':index+1,'text':text,
|
||||
'container':container,'interpretation':'Original note annotation; container does not establish musical permission or guitar fingering.'})
|
||||
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'})
|
||||
result['harmonies'].sort(key=lambda h:F(h['time']))
|
||||
text = ''.join(label.itertext()).strip()
|
||||
if text:
|
||||
result['annotations'].append({'time': str(absolute + start), 'measure': index + 1, 'text': text,
|
||||
'container': container, 'interpretation': 'Original note annotation; container does not establish musical permission or guitar fingering.'})
|
||||
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(fractions.Fraction(h['time']) <= absolute for h in result['harmonies'])})
|
||||
absolute += extent
|
||||
if pending:
|
||||
result['discrepancies'].append({'issue': 'unterminated ties retained'})
|
||||
result['harmonies'].sort(key=lambda h: fractions.Fraction(h['time']))
|
||||
# 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
|
||||
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['sounding_midi']=str(number+F(event['sounding_transposition']))
|
||||
event['legal_positions_at_encoded_pitch']=len(fretboard.single_positions_for_pitch(number))
|
||||
event['legal_positions_at_sounding_pitch']=len(fretboard.single_positions_for_pitch(F(event['sounding_midi'])))
|
||||
event['harmonic_spans']=[]
|
||||
start,end=F(event['start']),F(event['end'])
|
||||
changes=[F(h['time']) for h in result['harmonies'] if start<F(h['time'])<end]
|
||||
for a,b in zip([start,*changes],[*changes,end]):
|
||||
current=next((h for h in reversed(result['harmonies']) if F(h['time'])<=a),None)
|
||||
info={'start':str(a),'end':str(b),'chord':current,'chord_degrees':[]}
|
||||
if not event['pitch']:
|
||||
continue
|
||||
p = event['pitch']
|
||||
number = 12 * (p['octave'] + 1) + generator.model.NATURAL[p['step'].lower()] + fractions.Fraction(p['alter'])
|
||||
event['encoded_midi'] = str(number)
|
||||
event['sounding_midi'] = str(number + fractions.Fraction(event['sounding_transposition']))
|
||||
event['legal_positions_at_encoded_pitch'] = len(generator.fretboard.single_positions_for_pitch(number))
|
||||
event['legal_positions_at_sounding_pitch'] = len(generator.fretboard.single_positions_for_pitch(fractions.Fraction(event['sounding_midi'])))
|
||||
event['harmonic_spans'] = []
|
||||
start, end = fractions.Fraction(event['start']), fractions.Fraction(event['end'])
|
||||
changes = [fractions.Fraction(h['time']) for h in result['harmonies'] if start < fractions.Fraction(h['time']) < end]
|
||||
for a, b in zip([start, *changes], [*changes, end]):
|
||||
current = next((h for h in reversed(result['harmonies']) if fractions.Fraction(h['time']) <= a), None)
|
||||
info = {'start': str(a), 'end': str(b), 'chord': current, 'chord_degrees': []}
|
||||
if current and current['root']:
|
||||
try:
|
||||
chord=harmony.parse_symbol(training_chord(current));root=model.NATURAL[chord['root']['letter']]+model.alteration(chord['root']['alter'])
|
||||
info['chord_degrees']=[d for d,alter in harmony.degrees(chord).items() if (root+harmony.degree_semitones(d,alter)-number)%12==0]
|
||||
info['root_relative_semitones']=str((number-root)%12)
|
||||
info['spelled_simple_degree']=(model.LETTERS.index(p['step'].lower())-model.LETTERS.index(chord['root']['letter']))%7+1
|
||||
except (ValueError,KeyError,AttributeError) as error:
|
||||
info['uncertainty']=str(error)
|
||||
else:info['uncertainty']='No written/carried harmony; none invented.'
|
||||
chord = generator.harmony.parse_symbol(training_chord(current))
|
||||
root = generator.model.NATURAL[chord['root']['letter']] + generator.model.alteration(chord['root']['alter'])
|
||||
info['chord_degrees'] = [d for d, alter in generator.harmony.degrees(chord).items() if (root + generator.harmony.degree_semitones(d, alter) - number) % 12 == 0]
|
||||
info['root_relative_semitones'] = str((number - root) % 12)
|
||||
info['spelled_simple_degree'] = (generator.model.LETTERS.index(p['step'].lower()) - generator.model.LETTERS.index(chord['root']['letter'])) % 7 + 1
|
||||
except (ValueError, KeyError, AttributeError) as error:
|
||||
info['uncertainty'] = str(error)
|
||||
else:
|
||||
info['uncertainty'] = 'No written/carried harmony; none invented.'
|
||||
event['harmonic_spans'].append(info)
|
||||
sounded=[];tie_roots={}
|
||||
for i,n in enumerate(result['notes']):
|
||||
if not n['pitch']:continue
|
||||
previous=n.get('continuation_of')
|
||||
sounded = []
|
||||
tie_roots = {}
|
||||
for i, n in enumerate(result['notes']):
|
||||
if not n['pitch']:
|
||||
continue
|
||||
previous = n.get('continuation_of')
|
||||
if previous is not None and previous in tie_roots:
|
||||
root=tie_roots[previous];sounded[root]['end']=n['end'];sounded[root]['notation_events'].append(i);tie_roots[i]=root
|
||||
root = tie_roots[previous]
|
||||
sounded[root]['end'] = n['end']
|
||||
sounded[root]['notation_events'].append(i)
|
||||
tie_roots[i] = root
|
||||
else:
|
||||
tie_roots[i]=len(sounded);sounded.append({'start':n['start'],'end':n['end'],'pitch':n['pitch'],
|
||||
'encoded_midi':n['encoded_midi'],'sounding_midi':n['sounding_midi'],'measure':n['measure'],'notation_events':[i]})
|
||||
result['sounding_events']=sounded
|
||||
first=form_start or next((h['measure'] for h in result['harmonies']),None)
|
||||
offset=F(result['measures'][first-1]['start']) if first else None
|
||||
result['alignment']={'form_start_measure':first,'encoded_offset':str(offset) if offset is not None else None,'basis':'explicit override' if form_start else 'first written harmony; review required','preceding_material':'retained, without invented harmony'}
|
||||
tie_roots[i] = len(sounded)
|
||||
sounded.append({'start': n['start'], 'end': n['end'], 'pitch': n['pitch'],
|
||||
'encoded_midi': n['encoded_midi'], 'sounding_midi': n['sounding_midi'], 'measure': n['measure'], 'notation_events': [i]})
|
||||
result['sounding_events'] = sounded
|
||||
first = form_start or next((h['measure'] for h in result['harmonies']), None)
|
||||
offset = fractions.Fraction(result['measures'][first - 1]['start']) if first else None
|
||||
result['alignment'] = {'form_start_measure': first, 'encoded_offset': str(offset) if offset is not None else None, 'basis': 'explicit override' if form_start else 'first written harmony; review required', 'preceding_material': 'retained, without invented harmony'}
|
||||
for label in result['annotations']:
|
||||
t=F(label['time']); current=[h for h in result['harmonies'] if F(h['time'])<=t]
|
||||
future=next((h for h in result['harmonies'] if F(h['time'])>t),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<F(n['end']) or t<=F(n['start'])<t+1]
|
||||
label['uncertainty']='Label meaning must be assessed from these events and harmonic time; label preserved verbatim.'
|
||||
expected={'b9':F(1),'#9':F(3),'b13':F(8),'#11':F(6),'6':F(9)}.get(label['text'].strip())
|
||||
t = fractions.Fraction(label['time'])
|
||||
current = [h for h in result['harmonies'] if fractions.Fraction(h['time']) <= t]
|
||||
future = next((h for h in result['harmonies'] if fractions.Fraction(h['time']) > t), None)
|
||||
label['current_harmony'] = current[-1] if current else None
|
||||
label['following_harmony'] = future
|
||||
label['events'] = [n for n in result['notes'] if fractions.Fraction(n['start']) <= t < fractions.Fraction(n['end']) or t <= fractions.Fraction(n['start']) < t + 1]
|
||||
label['uncertainty'] = 'Label meaning must be assessed from these events and harmonic time; label preserved verbatim.'
|
||||
expected = {'b9': fractions.Fraction(1), '#9': fractions.Fraction(3), 'b13': fractions.Fraction(8), '#11': fractions.Fraction(6), '6': fractions.Fraction(9)}.get(label['text'].strip())
|
||||
if expected is not None:
|
||||
relationships=[s.get('root_relative_semitones') for n in label['events'] for s in n.get('harmonic_spans',[])]
|
||||
label['literal_interval_present']=str(expected) in relationships
|
||||
if not label['literal_interval_present']:label['uncertainty']='No matching interval in the immediate annotated context; label retained, intended scope uncertain.'
|
||||
relationships = [s.get('root_relative_semitones') for n in label['events'] for s in n.get('harmonic_spans', [])]
|
||||
label['literal_interval_present'] = str(expected) in relationships
|
||||
if not label['literal_interval_present']:
|
||||
label['uncertainty'] = 'No matching interval in the immediate annotated context; label retained, intended scope uncertain.'
|
||||
# Lead in and Bluesy stay contextual descriptions, never enclosure permits.
|
||||
return result
|
||||
|
||||
from generator import harmony,model
|
||||
import generator.harmony
|
||||
import generator.model
|
||||
|
||||
def training_chord(h):
|
||||
result={'root':{'letter':h['root'].lower(),'alter':model.rational(F(h['alter']))},'kind':h['kind'],'degrees':[]}
|
||||
result = {'root': {'letter': h['root'].lower(), 'alter': generator.model.rational(fractions.Fraction(h['alter']))}, 'kind': h['kind'], 'degrees': []}
|
||||
for d in h['degrees']:
|
||||
degree=int(d['value']);alter=F(d['alter'])
|
||||
if d['type']=='alter':alter+=harmony.KINDS.get(result['kind'],{}).get(degree,0)
|
||||
elif d['type']=='add' and (degree-1)%7==6:alter-=1
|
||||
result['degrees'].append({'value':degree,'alter':model.rational(alter),'type':d['type']})
|
||||
degree = int(d['value'])
|
||||
alter = fractions.Fraction(d['alter'])
|
||||
if d['type'] == 'alter':
|
||||
alter += generator.harmony.KINDS.get(result['kind'], {}).get(degree, 0)
|
||||
elif d['type'] == 'add' and (degree - 1) % 7 == 6:
|
||||
alter -= 1
|
||||
result['degrees'].append({'value': degree, 'alter': generator.model.rational(alter), 'type': d['type']})
|
||||
if h['bass']:
|
||||
import xml.etree.ElementTree as ET
|
||||
bass=ET.fromstring(h['bass']);result['bass']={'letter':bass.findtext('bass-step').lower(),'alter':model.rational(F(bass.findtext('bass-alter','0')))}
|
||||
display=h['display']
|
||||
bass = xml.etree.ElementTree.fromstring(h['bass'])
|
||||
result['bass'] = {'letter': bass.findtext('bass-step').lower(), 'alter': generator.model.rational(fractions.Fraction(bass.findtext('bass-alter', '0')))}
|
||||
display = h['display']
|
||||
if display:
|
||||
# Soundslice exports an optional chord's suffix with a trailing closing
|
||||
# parenthesis, but its actual root separately. Evidence stays untouched.
|
||||
if display.endswith(')') and '(' not in display:display=display[:-1]
|
||||
harmony.normalize_text(result,display)
|
||||
return harmony.symbol(result).strip('()')
|
||||
if display.endswith(')') and '(' not in display:
|
||||
display = display[:-1]
|
||||
generator.harmony.normalize_text(result, display)
|
||||
return generator.harmony.symbol(result).strip('()')
|
||||
|
||||
|
||||
def validate_harmony(document,evidence,identity):
|
||||
source_chords=[];cursor=F()
|
||||
def validate_harmony(document, evidence, identity):
|
||||
source_chords = []
|
||||
cursor = fractions.Fraction()
|
||||
for measure in document['staves'][0]['measures']:
|
||||
source_chords.extend((cursor+model.fraction(h['onset']),harmony.symbol(h).strip('()')) for h in measure['harmonies'])
|
||||
cursor+=model.fraction(measure['duration'])
|
||||
authored=[(F(h['time']),training_chord(h)) for h in evidence['harmonies']]
|
||||
if authored!=source_chords:raise ValueError(identity+': authored harmony differs from authoritative source')
|
||||
source_chords.extend((cursor + generator.model.fraction(h['onset']), generator.harmony.symbol(h).strip('()')) for h in measure['harmonies'])
|
||||
cursor += generator.model.fraction(measure['duration'])
|
||||
authored = [(fractions.Fraction(h['time']), training_chord(h)) for h in evidence['harmonies']]
|
||||
if authored != source_chords:
|
||||
raise ValueError(identity + ': authored harmony differs from authoritative source')
|
||||
|
||||
Reference in New Issue
Block a user