+77
-58
@@ -8,71 +8,90 @@ from __future__ import annotations
|
||||
import dataclasses
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
from fractions import Fraction as F
|
||||
from generator import musicxml,parts,training_io,harmonic_time
|
||||
from generator.solo import analysis,types,placement,profiles,harmonic
|
||||
import pathlib
|
||||
import fractions
|
||||
import generator.musicxml
|
||||
import generator.parts
|
||||
import generator.training_io
|
||||
import generator.harmonic_time
|
||||
import generator.solo.analysis
|
||||
import generator.solo.types
|
||||
import generator.solo.placement
|
||||
import generator.solo.profiles
|
||||
import generator.solo.harmonic
|
||||
import generator.production
|
||||
|
||||
|
||||
def sounding(evidence,offset=F()):
|
||||
result=[]
|
||||
for i,event in enumerate(evidence['sounding_events']):
|
||||
p=event['pitch'];number=F(event['sounding_midi'])
|
||||
if number.denominator!=1:raise ValueError('Microtonal guitar solo is unsupported')
|
||||
pitch=types.Pitch(p['step'].lower(),F(p['alter']),p['octave']+(int(number)-int(F(event['encoded_midi'])))//12)
|
||||
result.append(types.Note(F(event['start'])-offset,F(event['end'])-offset,pitch,role='authored',event_id=f'authored_{i}'))
|
||||
def sounding(evidence, offset=fractions.Fraction()):
|
||||
result = []
|
||||
for i, event in enumerate(evidence['sounding_events']):
|
||||
p = event['pitch']
|
||||
number = fractions.Fraction(event['sounding_midi'])
|
||||
if number.denominator != 1:
|
||||
raise ValueError('Microtonal guitar solo is unsupported')
|
||||
pitch = generator.solo.types.Pitch(p['step'].lower(), fractions.Fraction(p['alter']), p['octave'] + (int(number) - int(fractions.Fraction(event['encoded_midi']))) // 12)
|
||||
result.append(generator.solo.types.Note(fractions.Fraction(event['start']) - offset, fractions.Fraction(event['end']) - offset, pitch, role='authored', event_id=f'authored_{i}'))
|
||||
return tuple(result)
|
||||
|
||||
|
||||
def validate_evidence(document,evidence):
|
||||
def validate_evidence(document, evidence):
|
||||
"""Training uses the same source-form harmonic context as solo playback."""
|
||||
h=harmonic.Harmony(harmonic_time.solo_source(document['staves'][0]))
|
||||
checked=h.evaluate(sounding(evidence),complete=True)
|
||||
if not checked.valid:raise ValueError('Unsupported harmonic relationship in authored solo: '+checked.error)
|
||||
h = generator.solo.harmonic.Harmony(generator.harmonic_time.solo_source(document['staves'][0]))
|
||||
checked = h.evaluate(sounding(evidence), complete=True)
|
||||
if not checked.valid:
|
||||
raise ValueError('Unsupported harmonic relationship in authored solo: ' + checked.error)
|
||||
return checked
|
||||
|
||||
|
||||
def lookup(source,role,data_dir):
|
||||
from generator.production import SourceAnalysis
|
||||
root=Path(data_dir);manifest=root/'training/manifest.json'
|
||||
if not manifest.exists():return None
|
||||
value=json.loads(manifest.read_text())
|
||||
if value.get('version')!=1:raise ValueError('Unsupported training manifest')
|
||||
matches=[]
|
||||
def lookup(source, role, data_dir):
|
||||
root = pathlib.Path(data_dir)
|
||||
manifest = root / 'training/manifest.json'
|
||||
if not manifest.exists():
|
||||
return None
|
||||
value = json.loads(manifest.read_text())
|
||||
if value.get('version') != 1:
|
||||
raise ValueError('Unsupported training manifest')
|
||||
matches = []
|
||||
for entry in value['examples']:
|
||||
if entry['slot']!=(1 if role=='acoustic' else 2):continue
|
||||
path=(manifest.parent/entry['source']).resolve()
|
||||
if not path.is_relative_to((root/'songs').resolve()):raise ValueError('Registered sources must be in data/songs')
|
||||
doc=parts.prepare_document(musicxml.load_musicxml(path))
|
||||
if SourceAnalysis.read(doc).musical_key==source.musical_key:
|
||||
target=(manifest.parent/entry['solo']).resolve()
|
||||
if not target.is_relative_to(manifest.parent.resolve()):raise ValueError('Registered solo must be inside data/training')
|
||||
evidence=training_io.read(target,role,pitch_convention=entry['pitch_convention'])
|
||||
training_io.validate_harmony(doc,evidence,entry['id'])
|
||||
matches.append({**entry,'path':target,'sha256':hashlib.sha256(target.read_bytes()).hexdigest()})
|
||||
if len(matches)>1:raise ValueError('Multiple authored solos registered for the same source music and role')
|
||||
if entry['slot'] != (1 if role == 'acoustic' else 2):
|
||||
continue
|
||||
path = (manifest.parent / entry['source']).resolve()
|
||||
if not path.is_relative_to((root / 'songs').resolve()):
|
||||
raise ValueError('Registered sources must be in data/songs')
|
||||
doc = generator.parts.prepare_document(generator.musicxml.load_musicxml(path))
|
||||
if generator.production.SourceAnalysis.read(doc).musical_key == source.musical_key:
|
||||
target = (manifest.parent / entry['solo']).resolve()
|
||||
if not target.is_relative_to(manifest.parent.resolve()):
|
||||
raise ValueError('Registered solo must be inside data/training')
|
||||
evidence = generator.training_io.read(target, role, pitch_convention=entry['pitch_convention'])
|
||||
generator.training_io.validate_harmony(doc, evidence, entry['id'])
|
||||
matches.append({**entry, 'path': target, 'sha256': hashlib.sha256(target.read_bytes()).hexdigest()})
|
||||
if len(matches) > 1:
|
||||
raise ValueError('Multiple authored solos registered for the same source music and role')
|
||||
return matches[0] if matches else None
|
||||
|
||||
|
||||
def realize(entry,source_analysis,measures,part,start,end,initial=None):
|
||||
evidence=training_io.read(entry['path'],part,pitch_convention=entry['pitch_convention'])
|
||||
if evidence['discrepancies']:raise ValueError(entry['id']+': invalid authored timing/ties: '+str(evidence['discrepancies']))
|
||||
expected=[(a,b-a) for a,b in zip(source_analysis.boundaries,source_analysis.boundaries[1:])]
|
||||
if [(F(m['start']),F(m['duration'])) for m in evidence['measures']]!=expected:raise ValueError(entry['id']+': source/solo forms differ; register and retrain the correct pair')
|
||||
notes=sounding(evidence,source_analysis.body_offset)
|
||||
def realize(entry, source_analysis, measures, part, start, end, initial=None):
|
||||
evidence = generator.training_io.read(entry['path'], part, pitch_convention=entry['pitch_convention'])
|
||||
if evidence['discrepancies']:
|
||||
raise ValueError(entry['id'] + ': invalid authored timing/ties: ' + str(evidence['discrepancies']))
|
||||
expected = [(a, b - a) for a, b in zip(source_analysis.boundaries, source_analysis.boundaries[1:])]
|
||||
if [(fractions.Fraction(m['start']), fractions.Fraction(m['duration'])) for m in evidence['measures']] != expected:
|
||||
raise ValueError(entry['id'] + ': source/solo forms differ; register and retrain the correct pair')
|
||||
notes = sounding(evidence, source_analysis.body_offset)
|
||||
for n in notes:
|
||||
if n.start<start or n.end>end:raise ValueError(f'{entry["id"]}: authored event {n.start}–{n.end} conflicts with arrangement window {start}–{end}; no clipping applied')
|
||||
profile=profiles.for_part(part,source_analysis)
|
||||
illegal=next((n for n in notes if not placement.positions(n.pitch.number(),profile)),None)
|
||||
if illegal:raise ValueError(f'{entry["id"]}: {illegal.pitch.label()} at {illegal.start} is outside strings 1–6/frets 1–12; authored solo left unchanged')
|
||||
placed=placement.place(tuple(notes),source_analysis.tempo,profile=profile)
|
||||
if placed is None:raise ValueError(entry['id']+': no legal complete guitar placement')
|
||||
spans=tuple(dataclasses.replace(s,start=s.start+start,end=s.end+start) for s in harmonic_time.read({'measures':measures},initial=initial))
|
||||
checked=harmonic.Harmony(spans).evaluate(placed[1],complete=True)
|
||||
if not checked.valid:raise ValueError(entry['id']+': '+checked.error)
|
||||
phrase=types.Phrase('authored',start,end,placed[1],'authored','authored','handoff',theme=entry['id'],expressions=checked.contracts)
|
||||
return types.Solo(part,spans,(phrase,),source_analysis.tempo,end,
|
||||
{'construction':'authored_repertoire','authored_id':entry['id'],'authored_sha256':entry['sha256'],
|
||||
'pitch_convention':entry['pitch_convention'],'exact_sounding_events':True,
|
||||
'note':'Authored music is preserved and passes automatic contextual harmonic validation.',
|
||||
'annotations':evidence['annotations']},profile,start)
|
||||
if n.start < start or n.end > end:
|
||||
raise ValueError(f'{entry["id"]}: authored event {n.start}–{n.end} conflicts with arrangement window {start}–{end}; no clipping applied')
|
||||
profile = generator.solo.profiles.for_part(part, source_analysis)
|
||||
illegal = next((n for n in notes if not generator.solo.placement.positions(n.pitch.number(), profile)), None)
|
||||
if illegal:
|
||||
raise ValueError(f'{entry["id"]}: {illegal.pitch.label()} at {illegal.start} is outside strings 1–6/frets 1–12; authored solo left unchanged')
|
||||
placed = generator.solo.placement.place(tuple(notes), source_analysis.tempo, profile=profile)
|
||||
if placed is None:
|
||||
raise ValueError(entry['id'] + ': no legal complete guitar placement')
|
||||
spans = tuple(dataclasses.replace(s, start=s.start + start, end=s.end + start) for s in generator.harmonic_time.read({'measures': measures}, initial=initial))
|
||||
checked = generator.solo.harmonic.Harmony(spans).evaluate(placed[1], complete=True)
|
||||
if not checked.valid:
|
||||
raise ValueError(entry['id'] + ': ' + checked.error)
|
||||
phrase = generator.solo.types.Phrase('authored', start, end, placed[1], 'authored', 'authored', 'handoff', theme=entry['id'], expressions=checked.contracts)
|
||||
return generator.solo.types.Solo(part, spans, (phrase,), source_analysis.tempo, end,
|
||||
{'construction': 'authored_repertoire', 'authored_id': entry['id'], 'authored_sha256': entry['sha256'],
|
||||
'pitch_convention': entry['pitch_convention'], 'exact_sounding_events': True,
|
||||
'note': 'Authored music is preserved and passes automatic contextual harmonic validation.',
|
||||
'annotations': evidence['annotations']}, profile, start)
|
||||
|
||||
Reference in New Issue
Block a user