Revamp
This commit is contained in:
@@ -9,6 +9,22 @@ import generator.solo.placement
|
||||
import generator.solo.policy
|
||||
import generator.solo.sequences
|
||||
import generator.solo.types as types
|
||||
import generator.vocabulary
|
||||
|
||||
|
||||
def path_cost(notes):
|
||||
"""Penalize unexplained extremes, not every third or arpeggio interval."""
|
||||
intervals=[b.pitch.number()-a.pitch.number() for a,b in zip(notes,notes[1:])]
|
||||
excess=sum(max(0,abs(v)-7) for v in intervals)/max(1,len(intervals))
|
||||
oscillation=sum(a!=0 and a==-b and a==c and b==d for a,b,c,d in zip(intervals,intervals[1:],intervals[2:],intervals[3:]))
|
||||
return excess+.1*oscillation
|
||||
|
||||
|
||||
def path_family(notes,harmony):
|
||||
intervals=tuple(b.pitch.number()-a.pitch.number() for a,b in zip(notes,notes[1:]))
|
||||
chordal=all(n.pitch.number()%12 in harmony.members(n.start,n.end) for n in notes)
|
||||
return ('arpeggio' if chordal and any(abs(v)>=3 for v in intervals) else 'mapped',
|
||||
generator.solo.sequences.family(intervals))
|
||||
|
||||
def advance(scale, previous, step):
|
||||
try:
|
||||
@@ -25,7 +41,7 @@ def advance(scale, previous, step):
|
||||
outgoing.reverse()
|
||||
return tuple(outgoing[abs(step) - 1:abs(step)])
|
||||
|
||||
def candidates(notes, fixed, harmony, library, register, relationship='connect theme'):
|
||||
def candidates(notes, fixed, harmony, library, register, relationship='connect theme', *, purpose='connect'):
|
||||
# Cache concrete routes only with their endpoints, timing, scale context and
|
||||
# vocabulary. The per-Harmony cache cannot leak across collection changes.
|
||||
if not hasattr(harmony,'route_cache'):
|
||||
@@ -33,7 +49,7 @@ def candidates(notes, fixed, harmony, library, register, relationship='connect t
|
||||
harmony.route_libraries={}
|
||||
harmony.concrete_routes={}
|
||||
harmony.route_libraries[id(library)]=library
|
||||
concrete_key=(notes,frozenset(fixed),id(library),register,harmony.profile)
|
||||
concrete_key=(notes,frozenset(fixed),id(library),register,harmony.profile,purpose)
|
||||
if concrete_key in harmony.concrete_routes:
|
||||
return tuple(dataclasses.replace(r,relationship=relationship) for r in harmony.concrete_routes[concrete_key])
|
||||
# Event/source IDs and non-fixed placeholder pitches do not affect route
|
||||
@@ -42,9 +58,9 @@ def candidates(notes, fixed, harmony, library, register, relationship='connect t
|
||||
fixed_indices=frozenset(i for i,n in enumerate(notes) if n.event_id in fixed)
|
||||
canonical=tuple(dataclasses.replace(n,sources=(),event_id=str(i),role='connection',position=None,
|
||||
pitch=n.pitch if i==0 or i==len(notes)-1 or i in fixed_indices else types.Pitch('c',fractions.Fraction(),0)) for i,n in enumerate(notes))
|
||||
key=(canonical,fixed_indices,id(library),register,harmony.profile)
|
||||
key=(canonical,fixed_indices,id(library),register,harmony.profile,purpose)
|
||||
if key not in harmony.route_cache:
|
||||
harmony.route_cache[key]=_candidates(canonical,{str(i) for i in fixed_indices},harmony,library,register)
|
||||
harmony.route_cache[key]=_candidates(canonical,{str(i) for i in fixed_indices},harmony,library,register,purpose)
|
||||
result=[]
|
||||
for route in harmony.route_cache[key]:
|
||||
actual=tuple(n if i==0 else dataclasses.replace(n,pitch=n.pitch if n.pitch.number()==r.pitch.number() else r.pitch,position=None)
|
||||
@@ -52,15 +68,15 @@ def candidates(notes, fixed, harmony, library, register, relationship='connect t
|
||||
identity=generator.solo.policy.stable_rank('complete-route',(actual,route.sources))[:16]
|
||||
result.append(dataclasses.replace(route,id=identity,notes=actual,destination=notes[-1],relationship=relationship))
|
||||
selected=generator.solo.sequences.diverse(result,
|
||||
lambda r:(sum(abs(b.pitch.number()-a.pitch.number()) for a,b in zip(r.notes,r.notes[1:])),r.id),
|
||||
lambda r:generator.solo.sequences.family(tuple(b.pitch.number()-a.pitch.number() for a,b in zip(r.notes,r.notes[1:]))),
|
||||
lambda r:(path_cost(r.notes),r.id),
|
||||
lambda r:path_family(r.notes,harmony),
|
||||
generator.solo.policy.DEFAULT.join_alternatives)
|
||||
if len(harmony.concrete_routes)>=8192:harmony.concrete_routes.pop(next(iter(harmony.concrete_routes)))
|
||||
if len(harmony.route_cache)>=8192:harmony.route_cache.pop(next(iter(harmony.route_cache)))
|
||||
harmony.concrete_routes[concrete_key]=selected
|
||||
return selected
|
||||
|
||||
def _candidates(notes, fixed, harmony, library, register):
|
||||
def _candidates(notes, fixed, harmony, library, register, purpose='connect'):
|
||||
"""Every returned candidate includes its entry, complete rhythm and destination.
|
||||
|
||||
Callers validate the whole surrounding sounding context and guitar path.
|
||||
@@ -83,7 +99,24 @@ def _candidates(notes, fixed, harmony, library, register):
|
||||
route_spans=tuple((max(first.start,c.span.start),min(destination.end,c.span.end),c.scale.label())
|
||||
for c in harmony.choices if c.scale and c.span.start<destination.end and c.span.end>first.start)
|
||||
result=[]
|
||||
for pitches,sources in harmony.pitch_routes[key]:
|
||||
paths=list(harmony.pitch_routes[key])
|
||||
# These are traversal operators, not stored phrases. An arpeggio prefix
|
||||
# adapts to the chord at each note and a complete scale route then connects
|
||||
# it to this caller's actual destination. No endpoint is attacked twice.
|
||||
for prefix,rule in chord_prefixes(notes,fixed,harmony,register,purpose):
|
||||
i=len(prefix)-1
|
||||
if i==len(notes)-1:
|
||||
if prefix[-1].pitch.number()==destination.pitch.number():
|
||||
paths.append((tuple(n.pitch for n in prefix),('chord-path:'+rule,)))
|
||||
continue
|
||||
suffix_key=(prefix[-1].pitch,contexts[i:],id(library),register,harmony.profile)
|
||||
if suffix_key not in harmony.pitch_routes:
|
||||
while len(harmony.pitch_routes)>=8192:
|
||||
harmony.pitch_routes.pop(next(iter(harmony.pitch_routes)))
|
||||
harmony.pitch_routes[suffix_key]=pitch_paths(prefix[-1].pitch,contexts[i:],library,register,harmony.profile)
|
||||
for tail,sources in harmony.pitch_routes[suffix_key][:2]:
|
||||
paths.append((tuple(n.pitch for n in prefix[:-1])+tail,('chord-path:'+rule,*sources)))
|
||||
for pitches,sources in paths:
|
||||
realized=(first,*(dataclasses.replace(n,pitch=n.pitch if n.pitch.number()==p.number() else p,position=None)
|
||||
for n,p in zip(notes[1:],pitches[1:])))
|
||||
identity=generator.solo.policy.stable_rank('complete-route',(realized,sources))[:16]
|
||||
@@ -91,6 +124,37 @@ def _candidates(notes, fixed, harmony, library, register):
|
||||
return tuple(result)
|
||||
|
||||
|
||||
def chord_prefixes(notes,fixed,harmony,register,purpose):
|
||||
from generator.solo import phrasing
|
||||
if len(notes)<2:return ()
|
||||
values=[]
|
||||
first=notes[0];destination=notes[-1]
|
||||
for rule in generator.vocabulary.data('melodic_vocabulary')['chord_paths']:
|
||||
if purpose not in rule['purpose']:continue
|
||||
if rule['require_chord_entry'] and first.pitch.number()%12 not in harmony.members(first.start,first.end):continue
|
||||
displacement=destination.pitch.number()-first.pitch.number()
|
||||
direction=1 if rule['direction']=='up' else -1 if rule['direction']=='down' else (1 if displacement>=0 else -1)
|
||||
for count in rule['prefix_attacks']:
|
||||
if count>len(notes):continue
|
||||
prefix=[first]
|
||||
for i,n in enumerate(notes[1:count]):
|
||||
span=harmony.at(n.start)
|
||||
if not span or not span.scale:break
|
||||
eligible=harmony.members(n.start,n.end)&types.permitted(harmony.choices,n.start,n.end)
|
||||
if rule['members']=='defining':eligible=eligible&phrasing.defining_members(span.chord)
|
||||
step=rule['member_steps'][i%len(rule['member_steps'])]
|
||||
old=prefix[-1].pitch.number()
|
||||
pitches=sorted((types.scale_pitch(span.scale,j) for j in range(-21,29)),key=lambda p:p.number(),reverse=direction*step<0)
|
||||
options=[p for p in pitches if p.number()%12 in eligible and 0<(p.number()-old)*direction*step
|
||||
and abs(p.number()-old)<=rule['max_interval'] and register[0]<=p.number()<=register[1] and harmony.profile.permits(p.number())]
|
||||
if len(options)<abs(step):break
|
||||
selected=options[abs(step)-1]
|
||||
if (n.event_id in fixed or i+1==len(notes)-1) and selected.number()!=n.pitch.number():break
|
||||
prefix.append(dataclasses.replace(n,pitch=selected))
|
||||
if len(prefix)==count:values.append((tuple(prefix),rule['id']))
|
||||
return tuple(values)
|
||||
|
||||
|
||||
def pitch_paths(first, contexts, library, register, profile=None):
|
||||
"""Time-independent pitch work; context includes every overlapped collection.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user