+108
-50
@@ -10,7 +10,7 @@ import fractions
|
||||
|
||||
import generator.model
|
||||
import generator.fretboard
|
||||
from generator.solo import types, harmonic, policy, placement, routes, sequences, drafts
|
||||
from generator.solo import types, harmonic, policy, placement, routes, sequences, drafts, references
|
||||
|
||||
F = fractions.Fraction
|
||||
|
||||
@@ -160,49 +160,54 @@ def connection_patterns(start,end,entry,destination,rhythms,meter):
|
||||
return result
|
||||
|
||||
|
||||
def elaborate(notes, features, harmony, library, rhythms, meter, source, *, sustain_entry=False):
|
||||
"""Edit an exposed held tail or a complete interior span, with both endpoints known."""
|
||||
fixed={e for f in features for e in f.events}
|
||||
def elaborate(notes, features, harmony, library, rhythms, meter, source, *, sustain_entry=False, tails=None):
|
||||
"""Replace complete internal spans while retaining identity and sounding tails."""
|
||||
legacy=tails is None
|
||||
tails=tails or ()
|
||||
fixed={e for f in features for e in f.events} if legacy else {n.event_id for n in notes if n.role=='quote'}
|
||||
windows=[]
|
||||
for i,(note,target) in enumerate(zip(notes,notes[1:])):
|
||||
if note.event_id in fixed or note.end != target.start:
|
||||
continue
|
||||
if note.event_id in fixed or note.end!=target.start:continue
|
||||
if note.end-note.start>=F(3,2):
|
||||
start=max(note.start+F(1,2),target.start-3)
|
||||
windows.append((i,start,target,False))
|
||||
# A source-held ending can arrive later via a related in-scale turn.
|
||||
if notes and notes[-1].event_id not in fixed and notes[-1].end-notes[-1].start>=F(5,2):
|
||||
windows.append((i,i+1,start,target))
|
||||
if not legacy:
|
||||
for j in range(i+2,min(i+5,len(notes))):
|
||||
target=notes[j]
|
||||
if not 1<=target.start-note.start<=3:continue
|
||||
if any(n.event_id in fixed for n in notes[i:j]):continue
|
||||
if any(t.start<target.start and t.end>note.start for t in tails):continue
|
||||
windows.append((i,j,note.start,target))
|
||||
if legacy and notes and notes[-1].event_id not in fixed and notes[-1].end-notes[-1].start>=F(5,2):
|
||||
tail=notes[-1]
|
||||
for duration in (F(1,2),F(1),F(3,2),F(2)):
|
||||
target=dataclasses.replace(tail,start=tail.end-duration,event_id=tail.event_id+':arrival')
|
||||
start=max(tail.start+F(1,2),target.start-3)
|
||||
if target.start-start>=1:
|
||||
windows.append((len(notes)-1,start,target,True))
|
||||
if target.start-start>=1:windows.append((len(notes)-1,len(notes)-1,start,target))
|
||||
# Consequential edits only; do not exhaustively branch at each passing note.
|
||||
windows=sequences.diverse(windows,lambda w:(policy.stable_rank(source.id,w),),lambda w:w[0],8)
|
||||
result=[]
|
||||
for i,start,target,terminal in windows:
|
||||
entry=notes[i];end=target.start
|
||||
for i,j,start,target in windows:
|
||||
entry=notes[i]
|
||||
prefix=dataclasses.replace(entry,end=start)
|
||||
seed=dataclasses.replace(entry,start=start,event_id=entry.event_id+':tail',role='connection')
|
||||
for pattern,rhythm_id in connection_patterns(start,end,seed,target,rhythms,meter):
|
||||
for pattern,rhythm_id in connection_patterns(start,target.start,seed,target,rhythms,meter):
|
||||
for route in routes.candidates(pattern,{target.event_id},harmony,library,(41,76),'develop '+source.id):
|
||||
if sustain_entry:
|
||||
# The route owns its complete sounding entry, including the old hold.
|
||||
if sustain_entry or start==entry.start:
|
||||
joined=dataclasses.replace(route.notes[0],start=entry.start,event_id=entry.event_id)
|
||||
route=dataclasses.replace(route,notes=(joined,*route.notes[1:]))
|
||||
actual=(*notes[:i],*route.notes,*notes[i+1 if terminal else i+2:])
|
||||
actual=(*notes[:i],*route.notes,*notes[j+1:])
|
||||
else:
|
||||
actual=(*notes[:i],prefix,*route.notes,*notes[i+1 if terminal else i+2:])
|
||||
if not harmony.evaluate(actual,complete=True).valid:
|
||||
continue
|
||||
if any(abs(b.pitch.number()-a.pitch.number())>7 for a,b in zip(route.notes,route.notes[1:])):
|
||||
continue
|
||||
actual=(*notes[:i],prefix,*route.notes,*notes[j+1:])
|
||||
if any(references.feature_error(f,actual) for f in features):continue
|
||||
if any(not any(n.pitch.number()==t.pitch and n.start<=t.start and n.end>=t.end for n in actual) for t in tails):continue
|
||||
if not harmony.evaluate(actual,complete=True).valid:continue
|
||||
if any(abs(b.pitch.number()-a.pitch.number())>7 for a,b in zip(route.notes,route.notes[1:])):continue
|
||||
route=dataclasses.replace(route,sources=(*route.sources,'rhythm:'+rhythm_id))
|
||||
edit=types.DraftEdit('complete_route',start,target.start,(entry.event_id,),tuple(n.event_id for n in route.notes),
|
||||
source.id+'; '+rhythm_id+'; '+('later terminal arrival on ' if terminal else 'lead into ')+target.pitch.label())
|
||||
edit=types.DraftEdit('complete_route',start,target.start,tuple(n.event_id for n in notes[i:j+1]),tuple(n.event_id for n in route.notes),
|
||||
source.id+'; '+rhythm_id+'; lead into '+target.pitch.label())
|
||||
result.append((actual,edit,route))
|
||||
# Whole routes compete; bounded diversity retains different locations and rhythms.
|
||||
return sequences.diverse(result,lambda v:(abs(len(v[0])*4/float(source.end-source.start)-5),
|
||||
sum(abs(b.pitch.number()-a.pitch.number()) for a,b in zip(v[2].notes,v[2].notes[1:])),v[2].id),
|
||||
return sequences.diverse(result,lambda v:(max(0,4-len(v[0])*4/float(source.end-source.start),len(v[0])*4/float(source.end-source.start)-6),v[2].id),
|
||||
lambda v:(v[1].start,v[2].destination.end-v[2].destination.start,len(v[2].notes)),8)
|
||||
|
||||
|
||||
@@ -315,13 +320,81 @@ def transition(first,second):
|
||||
return .2*leap+.15*bool(repeated_end)
|
||||
|
||||
|
||||
def solo_options(analysis,measures,part,end,choices,library,rhythms,context,stage,mode='thematic'):
|
||||
from generator.solo import realization, themes
|
||||
if mode not in ('thematic','paraphrase'):raise ValueError('Unknown solo treatment: '+mode)
|
||||
def thematic_options(analysis,measures,part,end,choices,library,rhythms,context,stage):
|
||||
from generator.solo import realization,themes,phrasing
|
||||
context.resources[id(library)]=library
|
||||
context.resources[id(rhythms)]=rhythms
|
||||
harmony=harmonic.Harmony(choices)
|
||||
sources=themes.windows(analysis,measures,end,harmony) if mode=='thematic' else clipped_sources(analysis,end)
|
||||
seed=policy.stable_rank('source_variations',(analysis.melody,choices,policy.SEED_NAMESPACE))
|
||||
states=[(0.,())]
|
||||
failures=[]
|
||||
for _ in range(2*len(measures)+1):
|
||||
if all(history and history[-1].plan.end==end for _,history in states):break
|
||||
following=[]
|
||||
for cost,history in states:
|
||||
start=history[-1].plan.end if history else F()
|
||||
if start==end:
|
||||
following.append((cost,history));continue
|
||||
incoming=history[-1].state if history else types.ContinuationState('')
|
||||
sources=phrasing.window_options(analysis,measures,start,end,harmony,incoming)
|
||||
elapsed=F();meter=measures[0]['time_signature']
|
||||
for measure in measures:
|
||||
elapsed+=generator.model.fraction(measure['duration'])
|
||||
if elapsed>start:
|
||||
meter=measure['time_signature'];break
|
||||
bar=F(meter.split('/')[0])*4/int(meter.split('/')[1])
|
||||
for source in sources:
|
||||
key=('phrasing',source,incoming,part,end,stage,seed,id(library),id(rhythms),policy.DEFAULT)
|
||||
if key in context.results:
|
||||
pool=context.results[key];context.diagnostics['cache_hits']+=1
|
||||
else:
|
||||
pool=()
|
||||
if not harmony.at(start) or not harmony.at(start).scale:
|
||||
pool=themes.silence(source,incoming,part,harmony,analysis.tempo,seed)
|
||||
else:
|
||||
for batch in range(policy.DEFAULT.replan_attempts):
|
||||
pool=themes.phrase_candidates(source,analysis,incoming,part,harmony,library,rhythms,meter,analysis.tempo,context,seed,
|
||||
stage=stage,batch=batch,final=source.end==end,solo_end=end)
|
||||
if pool:break
|
||||
context.diagnostics['theme_replans']=context.diagnostics.get('theme_replans',0)+1
|
||||
context.results[key]=pool
|
||||
context.diagnostics.setdefault('source_candidate_pools',{}).setdefault(source.id,[]).extend(
|
||||
{'id':p.draft.id,'score':p.score,'attacks':len(p.draft.notes),'theme':p.state.home.id if p.state.home else None,
|
||||
'ending':str(p.anchors[-1].duration) if p.anchors else None,'returns':len(p.state.returns)} for p in pool)
|
||||
for candidate in pool:
|
||||
span=float((source.end-source.start)/end)
|
||||
total=cost+span*(candidate.score+phrasing.endpoint_cost(source.end,start,end,incoming.closures,harmony,bar))
|
||||
total+=transition(history[-1] if history else None,candidate)
|
||||
following.append((total,(*history,candidate)))
|
||||
if not pool:failures.append(f'{source.id}: planned sustained destination, feature or incoming continuation')
|
||||
if not following:
|
||||
raise realization.SearchExhausted(f'{part}: {"; ".join(failures[-4:])} failed within the bounded search; not proof of incompatibility')
|
||||
def rank(value):
|
||||
cost,history=value;state=history[-1].state;elapsed=history[-1].plan.end
|
||||
musical=cost/max(.001,float(elapsed/end))
|
||||
if elapsed==end:
|
||||
target=policy.DEFAULT.settling_target*float(end)/(32*4)
|
||||
musical+=.1*max(0,abs(state.closures-target)-1)
|
||||
musical+=.15*abs(len(state.returns)-policy.DEFAULT.return_target*float(end)/(32*4))
|
||||
return musical,policy.stable_rank(seed,tuple(p.draft.id for p in history))
|
||||
states=sequences.diverse(following,rank,
|
||||
lambda v:(v[1][-1].state.home.id if v[1][-1].state.home else '',v[1][-1].plan.end,len(v[1][-1].state.returns)),policy.DEFAULT.chorus_frontier)
|
||||
finished=[(cost,history) for cost,history in states if history[-1].plan.end==end]
|
||||
if not finished:raise realization.SearchExhausted(f'{part}: phrase-boundary search budget exhausted; not proof of incompatibility')
|
||||
return tuple((rank((cost,phrases))[0],types.Solo(part,choices,phrases,analysis.tempo,end,
|
||||
{**context.diagnostics,'construction':'thematic_recomposition','policy':policy.describe()['fingerprint']},
|
||||
tuple(types.MelodicPassagePlan(p.plan.passage_id,p.plan.start,p.plan.end,(p.plan.id,),seed) for p in phrases))) for cost,phrases in finished)
|
||||
|
||||
|
||||
def solo_options(analysis,measures,part,end,choices,library,rhythms,context,stage,mode='thematic'):
|
||||
from generator.solo import realization
|
||||
if mode not in ('thematic','paraphrase'):raise ValueError('Unknown solo treatment: '+mode)
|
||||
if mode=='thematic':
|
||||
return thematic_options(analysis,measures,part,end,choices,library,rhythms,context,stage)
|
||||
context.resources[id(library)]=library
|
||||
context.resources[id(rhythms)]=rhythms
|
||||
harmony=harmonic.Harmony(choices)
|
||||
sources=clipped_sources(analysis,end)
|
||||
seed=policy.stable_rank('source_variations', (analysis.melody,choices,policy.SEED_NAMESPACE))
|
||||
states=[(0.,())]
|
||||
for source in sources:
|
||||
@@ -329,30 +402,15 @@ def solo_options(analysis,measures,part,end,choices,library,rhythms,context,stag
|
||||
following=[]
|
||||
for cost,history in states:
|
||||
incoming=history[-1].state if history else types.ContinuationState('')
|
||||
key=(mode,source,incoming if mode=='thematic' else None,policy.stable_rank("harmony",choices),part,analysis.tempo,seed,stage,id(library),id(rhythms),policy.DEFAULT)
|
||||
key=(mode,source,None,policy.stable_rank("harmony",choices),part,analysis.tempo,seed,stage,id(library),id(rhythms),policy.DEFAULT)
|
||||
if key in context.results:
|
||||
pool=context.results[key];context.diagnostics['cache_hits']+=1
|
||||
else:
|
||||
if mode=='thematic' and any(c.scale and c.span.start<source.end and c.span.end>source.start for c in choices):
|
||||
pool=()
|
||||
for batch in range(2):
|
||||
pool=themes.phrase_candidates(source,analysis,incoming,part,harmony,library,rhythms,meter,analysis.tempo,context,seed,stage=stage,batch=batch,final=source.end==end)
|
||||
if pool:break
|
||||
context.diagnostics['theme_replans']=context.diagnostics.get('theme_replans',0)+1
|
||||
elif mode=='thematic':
|
||||
pool=themes.silence(source,incoming,part,harmony,analysis.tempo,seed)
|
||||
else:
|
||||
pool=candidates(source,harmony,library,rhythms,meter,part,analysis.tempo,seed,stage=stage)
|
||||
pool=candidates(source,harmony,library,rhythms,meter,part,analysis.tempo,seed,stage=stage)
|
||||
context.results[key]=pool;context.diagnostics['generated_candidates']+=len(pool)
|
||||
context.diagnostics.setdefault('source_candidate_pools', {}).setdefault(source.id,[]).extend(
|
||||
{'id':p.draft.id,'score':p.score,'octave':p.draft.octave_transfer,'attacks':len(p.draft.notes),'edits':[e.kind for e in p.edits],'source_family':source.family,'theme':p.state.home.id if p.state.home else None} for p in pool)
|
||||
for candidate in pool:
|
||||
# The thematic constructor has already placed all decisions from the
|
||||
# actual incoming state; do not collapse its motif groups into one bar.
|
||||
if mode=='thematic':
|
||||
total=cost+candidate.score*float(source.end-source.start)/float(end)+transition(history[-1] if history else None,candidate)
|
||||
following.append((total,(*history,candidate)))
|
||||
continue
|
||||
last=incoming.last
|
||||
placements=placement.alternatives(tuple(n for d in candidate.decisions for n in d.notes),analysis.tempo,last,limit=2)
|
||||
for travel,notes in placements:
|
||||
@@ -364,8 +422,8 @@ def solo_options(analysis,measures,part,end,choices,library,rhythms,context,stag
|
||||
if not following:
|
||||
raise realization.SearchExhausted(f'{part} {source.id}: {mode} feature, harmonic destination or continuation failed within the bounded search; not proof of incompatibility')
|
||||
states=sequences.diverse(following,lambda v:(v[0],policy.stable_rank(seed,tuple((p.draft.id,p.state.last.position if p.state.last else None) for p in v[1]))),
|
||||
lambda v:(v[1][-1].state.home.id if mode=='thematic' and v[1][-1].state.home else tuple(p.draft.octave_transfer for p in v[1])),policy.DEFAULT.chorus_frontier)
|
||||
return tuple((cost,types.Solo(part,choices,phrases,analysis.tempo,end,{**context.diagnostics,'construction':'thematic_recomposition' if mode=='thematic' else 'source_phrase_variations','policy':policy.describe()['fingerprint']},
|
||||
lambda v:(tuple(p.draft.octave_transfer for p in v[1])),policy.DEFAULT.chorus_frontier)
|
||||
return tuple((cost,types.Solo(part,choices,phrases,analysis.tempo,end,{**context.diagnostics,'construction':'source_phrase_variations','policy':policy.describe()['fingerprint']},
|
||||
tuple(types.MelodicPassagePlan(p.plan.passage_id,p.plan.start,p.plan.end,(p.plan.id,),seed) for p in phrases))) for cost,phrases in states)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user