396 lines
25 KiB
Python
396 lines
25 KiB
Python
"""Thematic transformations used by the existing complete-draft composer.
|
|
|
|
Source features initialize a heard theme. Subsequent complete routes develop
|
|
that realization, conditioned on the actual incoming musical state.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import dataclasses
|
|
from fractions import Fraction as F
|
|
|
|
import generator.model
|
|
import generator.harmony
|
|
from generator.solo import types, policy, sequences, routes, references, similarity
|
|
|
|
|
|
def source_notes(analysis):
|
|
return tuple(dataclasses.replace(n,start=max(F(),n.start-analysis.body_offset),end=n.end-analysis.body_offset,
|
|
event_id=n.sources[0],position=None) for n in analysis.melody if n.end>analysis.body_offset)
|
|
|
|
|
|
def windows(analysis, measures, end, harmony):
|
|
"""Existing phrase representation; source extraction seams do not close it."""
|
|
bounds=[F()]
|
|
for measure in measures:bounds.append(bounds[-1]+generator.model.fraction(measure['duration']))
|
|
melody=source_notes(analysis); result=[]; index=0
|
|
while bounds[index]<end:
|
|
stop=min(index+4,len(measures))
|
|
# Permit an extension through a cadence rather than closing on its dominant.
|
|
last=harmony.at(min(end,bounds[stop])-F(1,100))
|
|
if last and last.chord!='N.C.' and generator.harmony.degrees(generator.harmony.parse_symbol(last.chord)).get(7)==-1 and generator.harmony.degrees(generator.harmony.parse_symbol(last.chord)).get(3)==0:
|
|
for size in (6,8):
|
|
candidate=min(index+size,len(measures)); span=harmony.at(min(end,bounds[candidate])-F(1,100))
|
|
if span and span.chord!='N.C.' and generator.harmony.degrees(generator.harmony.parse_symbol(span.chord)).get(3)==-1:
|
|
stop=candidate;break
|
|
start,finish=bounds[index],min(end,bounds[stop])
|
|
notes=tuple(dataclasses.replace(n,start=max(start,n.start),end=min(finish,n.end),continuation=n.start<start)
|
|
for n in melody if n.start<finish and n.end>start)
|
|
result.append(types.SourcePhrase(f'theme_window_{start}_{finish}',start,finish,notes,(),''))
|
|
index=stop
|
|
# Explicit missing harmony interrupts sounding material, not thematic memory.
|
|
divided=[]
|
|
for window in result:
|
|
cuts=sorted({window.start,window.end,*(max(window.start,s.start) for s in harmony.spans if not s.scale and s.start<window.end and s.end>window.start),*(min(window.end,s.end) for s in harmony.spans if not s.scale and s.start<window.end and s.end>window.start)})
|
|
for start,finish in zip(cuts,cuts[1:]):
|
|
notes=tuple(dataclasses.replace(n,start=max(start,n.start),end=min(finish,n.end),continuation=n.start<start) for n in window.notes if n.start<finish and n.end>start)
|
|
divided.append(dataclasses.replace(window,id=f'theme_window_{start}_{finish}',start=start,end=finish,notes=notes))
|
|
return tuple(divided)
|
|
|
|
|
|
def silence(source,incoming,part,harmony,tempo,seed):
|
|
from generator.solo import realization
|
|
motif=types.Motif('intentional_space',(),source.end-source.start,(),'explicit missing harmony')
|
|
plan=types.PhrasePlan(part+'_'+source.id,source.start,source.end,motif,'space','thematic_recomposition',None,(41,76),F(),F(),
|
|
(types.ActivitySpan(source.start,source.end,'space',F(),F()),),passage_id=source.id,passage_start=source.start,passage_end=source.end)
|
|
draft=types.MelodicDraft(plan.id,source.start,source.end,(),(),(),(),(),incoming.home,'space',source_phrase=source,source_reference=incoming.source_reference)
|
|
phrase=realization.checked_phrase(plan,draft,(),incoming.treatments,incoming,harmony,tempo)
|
|
if phrase is None:return ()
|
|
state=dataclasses.replace(phrase.state,home=incoming.home,source_reference=incoming.source_reference,callbacks=incoming.callbacks)
|
|
original=dataclasses.replace(draft,notes=source.notes)
|
|
return (dataclasses.replace(phrase,state=state,initial=original,refined=draft,score=0.),)
|
|
|
|
|
|
def short_fragment(source,incoming,part,harmony,tempo,seed,final):
|
|
"""An explicitly reduced pickup around missing harmony; not a new theme."""
|
|
from generator.solo import leads,realization
|
|
reference=incoming.home.notes if incoming.home else source.notes
|
|
if not reference:return ()
|
|
template=dataclasses.replace(reference[0],start=source.start,end=source.end,position=None,continuation=False,event_id=source.id+':reduction')
|
|
result=[]
|
|
for pitch in pitches(harmony,template,incoming.last.pitch.number() if incoming.last else template.pitch.number(),members=True):
|
|
note=dataclasses.replace(template,pitch=pitch)
|
|
motif=types.Motif('reduction',reference,source.end-source.start,(),'explicit short fragment; no claim of full motif fidelity')
|
|
anchor=types.Anchor(note.event_id+':arrival',note.start,note.start,pitch,harmony.members(note.start,note.end),(41,76),note.end-note.start,'terminal' if final else 'intermediate',harmony.at(note.start).chord,hold_until=note.end)
|
|
transform=types.Transformation(note.event_id,reference[0].event_id,incoming.home.id if incoming.home else '', 'fragment',note.start,note.end,(note.event_id,),((F(),note.end-note.start),),preserves=())
|
|
draft=types.MelodicDraft(part+'_'+note.event_id,source.start,source.end,(note,),(transform,),(note.event_id,),(),(anchor,),incoming.home,'fragment',source_phrase=source,source_reference=incoming.source_reference)
|
|
plan=types.PhrasePlan(draft.id,source.start,source.end,motif,'fragment','thematic_recomposition',None,(41,76),F(5,4),F(),ending='sustain' if final else 'connected',closure='closed' if final else 'open',passage_id=source.id,passage_start=source.start,passage_end=source.end)
|
|
phrase=realization.checked_phrase(plan,draft,(note,),incoming.treatments,incoming,harmony,tempo)
|
|
if phrase:
|
|
state=dataclasses.replace(phrase.state,home=incoming.home,source_reference=incoming.source_reference,callbacks=incoming.callbacks)
|
|
result.append(dataclasses.replace(phrase,state=state,initial=draft,refined=draft))
|
|
return tuple(result)
|
|
|
|
|
|
def seeds(analysis, bar, seed):
|
|
melody=tuple(dataclasses.replace(n,start=n.start-analysis.body_offset,end=n.end-analysis.body_offset,
|
|
event_id=n.sources[0]) for n in analysis.melody)
|
|
groups=[]
|
|
for i in range(len(melody)):
|
|
for count in ((len(melody),) if len(melody)<3 else (3,4,5)):
|
|
group=melody[i:i+count]
|
|
if len(group)!=count or group[-1].end-group[0].start>max(bar,F(3)) or group[0].start>=8*bar:
|
|
continue
|
|
local=tuple(dataclasses.replace(n,start=n.start-group[0].start,end=n.end-group[0].start) for n in group)
|
|
parent=next((s.id for s in analysis.source_phrases if s.start<=max(0,group[0].start)<s.end),'pickup')
|
|
for treatment in ('rhythmic_seed','contour_seed','opening_fragment','characteristic_interval'):
|
|
identity=policy.stable_rank(seed,(group,treatment))[:12]
|
|
groups.append(types.SourceReference(identity,parent,group[0].start,group[-1].end,local,treatment))
|
|
def rank(ref):
|
|
rhythm={(n.start,n.end-n.start) for n in ref.notes}
|
|
intervals={b.pitch.number()-a.pitch.number() for a,b in zip(ref.notes,ref.notes[1:])}
|
|
return (-len(intervals),-len(rhythm),ref.start,ref.id)
|
|
return sequences.diverse(groups,rank,lambda r:r.treatment,policy.DEFAULT.theme_seed_limit)
|
|
|
|
|
|
def feature(reference, actual, *, treatment=None):
|
|
treatment=treatment or reference.treatment
|
|
required={'rhythmic_seed':('rhythm',),'contour_seed':('contour',),
|
|
'opening_fragment':('rhythm','pitches'),'characteristic_interval':('exact_intervals',),
|
|
'repeat_with_harmonic_adaptation':('rhythm','contour'),'answer':('contour',),
|
|
'extend':('rhythm','contour'),'fragment':('rhythm',),'callback':('rhythm','pitches')}[treatment]
|
|
original=reference.notes
|
|
origin=original[0].start
|
|
octave=12*round((actual[0].pitch.number()-original[0].pitch.number())/12)
|
|
return types.IdentifyingFeature(reference.id+':'+treatment,tuple(n.event_id for n in actual),
|
|
tuple((n.start-origin,n.end-n.start) for n in original),
|
|
tuple(b.pitch.number()-a.pitch.number() for a,b in zip(original,original[1:])),reference.id,required,
|
|
tuple(n.pitch.number()+octave for n in original))
|
|
|
|
|
|
def rhythm_windows(library,meter,length,context):
|
|
from generator.solo import drafts
|
|
key=('theme-rhythm',id(library),meter,length)
|
|
if key not in context.pools:
|
|
patterns={}
|
|
for identifier,pattern,total in drafts.catalog(library,meter):
|
|
if total<length:continue
|
|
for offset in sorted({F(),*(a for a,_ in pattern)}):
|
|
if offset+length>total or any(a<offset<b or a<offset+length<b for a,b in pattern):continue
|
|
selected=tuple((a-offset,b-offset) for a,b in pattern if offset<=a and b<=offset+length)
|
|
if selected and len(selected)<=16:patterns.setdefault(selected,identifier)
|
|
context.pools[key]=sequences.diverse(tuple(patterns.items()),
|
|
lambda x:(not .7*float(length)<=len(x[0])<=1.8*float(length),policy.stable_rank('rhythm-window',x[0])),
|
|
lambda x:(len(x[0]),x[0][0][0],any(a[1]!=b[0] for a,b in zip(x[0],x[0][1:]))),8)
|
|
return context.pools[key]
|
|
|
|
|
|
def patterns(ref, start, finish, rhythms, meter, context, *, home=None, operation='introduce', terminal=False):
|
|
length=finish-start; values={}
|
|
if home and operation=='repeat_with_harmonic_adaptation':
|
|
pattern=tuple((n.start,n.end) for n in home.notes if n.end<=length)
|
|
if pattern:values[pattern]='heard-theme'
|
|
prefix=tuple((n.start,n.end) for n in ref.notes)
|
|
if ref.treatment in ('rhythmic_seed','opening_fragment','callback') or operation in ('extend','fragment','repeat_with_harmonic_adaptation'):
|
|
duration=prefix[-1][1]
|
|
if duration==length:values[prefix]=ref.id
|
|
if duration<length:
|
|
for tail,label in rhythm_windows(rhythms,meter,length-duration,context):
|
|
values[(*prefix,*((a+duration,b+duration) for a,b in tail))]=ref.id+' + '+label
|
|
else:
|
|
values.update(rhythm_windows(rhythms,meter,length,context))
|
|
if home and operation=='answer':
|
|
for p,label in rhythm_windows(rhythms,meter,length,context):values.setdefault(p,label)
|
|
if terminal:
|
|
for original,label in tuple(values.items()):
|
|
for hold in (F(1,2),F(1),F(3,2),F(2)):
|
|
for release in (F(),F(1,2)):
|
|
arrival=length-hold-release
|
|
head=tuple((a,min(b,arrival)) for a,b in original if a<arrival)
|
|
if len(head)>=len(ref.notes):values[(*head,(arrival,length-release))]=label+f'; arrival {hold}, release {release}'
|
|
return sequences.diverse(tuple(values.items()),lambda x:(max(0,len(x[0])-1.5*float(length),float(length)-len(x[0])),policy.stable_rank(ref.id,x[0])),
|
|
lambda x:(len(x[0]),x[0][-1][1]-x[0][-1][0],x[0][0][0]),4)
|
|
|
|
|
|
def pitches(harmony,note,base,*,members=False,limit=6):
|
|
from generator.solo.leads import available
|
|
domain=available(harmony,note.start,note.end,members=members or note.end-note.start>=2)
|
|
return sequences.diverse(domain,lambda p:(abs(p.number()-base),p.number()),
|
|
lambda p:((p.number()-base>0)-(p.number()-base<0),p.number()//12),limit)
|
|
|
|
|
|
def relative_steps(notes,harmony):
|
|
result=[]
|
|
for a,b in zip(notes,notes[1:]):
|
|
scale=harmony.at(b.start).scale
|
|
try:step=types.scale_index(scale,b.pitch.number())-types.scale_index(scale,a.pitch.number())
|
|
except ValueError:
|
|
lo,hi=sorted((a.pitch.number(),b.pitch.number()))
|
|
intervening=range(lo+1,hi+1) if b.pitch.number()>a.pitch.number() else range(lo,hi)
|
|
count=sum(p%12 in scale.pitches for p in intervening)
|
|
step=count if b.pitch.number()>a.pitch.number() else -count
|
|
result.append(step)
|
|
return tuple(result)
|
|
|
|
|
|
def instantiate(templates,first,steps,harmony):
|
|
result=[dataclasses.replace(templates[0],pitch=first)]
|
|
for note,step in zip(templates[1:],steps):
|
|
span=harmony.at(note.start)
|
|
if not span or not span.scale:return ()
|
|
options=routes.advance(span.scale,result[-1].pitch.number(),step)
|
|
if not options:return ()
|
|
p=options[0]
|
|
if not 41<=p.number()<=76 or p.number()%12 not in types.permitted(harmony.choices,note.start,note.end):return ()
|
|
result.append(dataclasses.replace(note,pitch=p))
|
|
return tuple(result) if len(result)==len(templates) else ()
|
|
|
|
|
|
def prefix_paths(templates,ref,first,harmony,library):
|
|
count=len(ref.notes); original=ref.notes
|
|
if count==1:return ((dataclasses.replace(templates[0],pitch=first),),)
|
|
if ref.treatment in ('opening_fragment','characteristic_interval','callback'):
|
|
delta=first.number()-original[0].pitch.number()
|
|
if ref.treatment in ('opening_fragment','callback') and delta%12:return ()
|
|
result=[]
|
|
from generator.solo.leads import available
|
|
for template,n in zip(templates,original):
|
|
target=n.pitch.number()+delta
|
|
p=next((p for p in available(harmony,template.start,template.end,members=template.end-template.start>=2) if p.number()==target),None)
|
|
if p is None:return ()
|
|
result.append(dataclasses.replace(template,pitch=p))
|
|
return (tuple(result),)
|
|
paths=[]
|
|
end_template=templates[count-1]
|
|
source_motion=original[-1].pitch.number()-original[0].pitch.number()
|
|
for target in pitches(harmony,end_template,first.number()+source_motion,limit=4):
|
|
template=tuple(dataclasses.replace(n,pitch=first if i==0 else target) for i,n in enumerate(templates[:count]))
|
|
paths.extend(r.notes for r in routes.candidates(template,{template[-1].event_id},harmony,library,(41,76),'seed '+ref.id))
|
|
valid=[p for p in paths if not references.feature_error(feature(ref,p),p)]
|
|
return sequences.diverse(valid,lambda ns:(sum(abs(b.pitch.number()-a.pitch.number()) for a,b in zip(ns,ns[1:])),tuple(n.pitch for n in ns)),
|
|
lambda ns:sequences.family(tuple(b.pitch.number()-a.pitch.number() for a,b in zip(ns,ns[1:]))),4)
|
|
|
|
|
|
@dataclasses.dataclass(frozen=True)
|
|
class Unit:
|
|
notes: tuple[types.Note,...]
|
|
feature: types.IdentifyingFeature
|
|
reference: types.SourceReference
|
|
operation: str
|
|
anchor: types.Anchor
|
|
score: float
|
|
source: str
|
|
routes: tuple[types.RouteCandidate,...] = ()
|
|
|
|
|
|
def unit_candidates(ref,start,finish,incoming,harmony,library,rhythms,meter,context,*,home=None,operation='introduce',terminal=False,batch=0):
|
|
# Position and the recent realization participate even when two paths share a last pitch.
|
|
if not hasattr(harmony,'fingerprint'):harmony.fingerprint=policy.stable_rank('harmony',harmony.choices)
|
|
key=('theme-unit',ref,start,finish,incoming.last,incoming.direction,incoming.treatments,home,operation,terminal,batch,harmony.fingerprint,id(library),id(rhythms),policy.DEFAULT)
|
|
if key in context.results:
|
|
context.diagnostics['cache_hits']+=1;return context.results[key]
|
|
result=[]; count=len(ref.notes)
|
|
for pattern,label in patterns(ref,start,finish,rhythms,meter,context,home=home,operation=operation,terminal=terminal):
|
|
if len(pattern)<count:continue
|
|
templates=tuple(types.Note(start+a,start+b,ref.notes[0].pitch,tuple(s for n in ref.notes for s in n.sources),
|
|
'theme',event_id=f'{ref.id}:{start}:{i}') for i,(a,b) in enumerate(pattern))
|
|
base=incoming.last.pitch.number() if incoming.last else ref.notes[0].pitch.number()
|
|
firsts=pitches(harmony,templates[0],base,limit=6 if batch else 3)
|
|
for first in firsts:
|
|
proposed=[]
|
|
if home and operation=='repeat_with_harmonic_adaptation' and len(templates)==len(home.notes):
|
|
heard=instantiate(templates,first,home.degree_steps,harmony)
|
|
if heard:proposed.append((heard,()))
|
|
for prefix in prefix_paths(templates,ref,first,harmony,library):
|
|
if len(prefix)==len(templates):
|
|
proposed.append((prefix,()));continue
|
|
base=prefix[-1].pitch.number()
|
|
for target in pitches(harmony,templates[-1],base,members=True,limit=4 if batch else 2):
|
|
tail=(prefix[-1],*templates[count:])
|
|
tail=(*tail[:-1],dataclasses.replace(tail[-1],pitch=target))
|
|
for route in routes.candidates(tail,{tail[-1].event_id},harmony,library,(41,76),operation+' '+ref.id):
|
|
proposed.append(((*prefix[:-1],*route.notes),(route,)))
|
|
for actual,connections in proposed:
|
|
core=actual[:count]; contract=feature(ref,core,treatment=operation if operation!='introduce' else None)
|
|
if references.feature_error(contract,actual):continue
|
|
last=actual[-1]
|
|
if last.pitch.number()%12 not in harmony.members(last.start,last.end):continue
|
|
checked=harmony.evaluate(actual,until=finish,complete=True)
|
|
if not checked.valid:continue
|
|
density=len(actual)*4/float(finish-start)
|
|
entry=max(0,abs(first.number()-incoming.last.pitch.number())-5)/12 if incoming.last else 0.
|
|
leaps=sum(max(0,abs(b.pitch.number()-a.pitch.number())-5)/12 for a,b in zip(actual,actual[1:]))/max(1,len(actual)-1)
|
|
# A repeat preserves the full heard idea. Answers and extensions
|
|
# preserve their declared fragment, not every event of the old ending.
|
|
development=references.relationship(actual,home) if home and operation=='repeat_with_harmonic_adaptation' else 0.
|
|
score=2*checked.exposure+entry+leaps+development+.35*max(0,4-density,density-6)
|
|
anchor=types.Anchor(last.event_id+':arrival',last.start,last.start,last.pitch,harmony.members(last.start,last.end),(41,76),
|
|
last.end-last.start,'terminal' if terminal else 'intermediate',harmony.at(last.start).chord,hold_until=last.end)
|
|
result.append(Unit(actual,contract,ref,operation,anchor,score,label,connections))
|
|
unique={tuple((n.start,n.end,n.pitch) for n in u.notes):u for u in result}
|
|
selected=sequences.diverse(tuple(unique.values()),lambda u:(u.score,policy.stable_rank(ref.id,u.notes)),
|
|
lambda u:(u.notes[0].pitch.number()//12,u.notes[-1].pitch.number()%12,tuple(n.end-n.start for n in u.notes[:count])),8)
|
|
context.diagnostics['generated_candidates']+=len(selected)
|
|
context.results[key]=selected
|
|
return selected
|
|
|
|
|
|
def home_reference(home,operation):
|
|
notes=home.notes[:min(4,len(home.notes))]
|
|
# A short characteristic beginning leaves room for a newly composed answer.
|
|
while len(notes)>3 and notes[-1].end>home.duration/2:notes=notes[:-1]
|
|
return types.SourceReference(home.id,home.parent,home.source_start,home.source_start+home.duration,notes,
|
|
'rhythmic_seed' if operation in ('repeat_with_harmonic_adaptation','extend','fragment') else 'contour_seed')
|
|
|
|
|
|
def make_home(unit,harmony,start=None,end=None):
|
|
from generator.solo.realization import make_identity
|
|
start=unit.notes[0].start if start is None else start
|
|
identity=policy.stable_rank('solo-theme',(unit.reference.id,unit.notes))[:16]
|
|
home=make_identity('solo_theme_'+identity,unit.reference.id,unit.notes,start,unit.notes[-1].end if end is None else end,'introduce',harmony)
|
|
return dataclasses.replace(home,degree_steps=relative_steps(unit.notes,harmony))
|
|
|
|
|
|
def phrase_candidates(source,analysis,incoming,part,harmony,library,rhythms,meter,tempo,context,seed,*,stage='elaborated',batch=0,final=False):
|
|
from generator.solo import leads,realization
|
|
if source.end-source.start<2:
|
|
return short_fragment(source,incoming,part,harmony,tempo,seed,final)
|
|
bar=F(meter.split('/')[0])*4/int(meter.split('/')[1])
|
|
unit_length=2*bar
|
|
head=source_notes(analysis)
|
|
seed_key=('source-seeds',analysis,bar,seed)
|
|
if seed_key not in context.pools:context.pools[seed_key]=seeds(analysis,bar,seed)
|
|
roots=context.pools[seed_key]
|
|
states=[(0.,(),incoming.home,incoming)]
|
|
cursor=source.start
|
|
while cursor<source.end:
|
|
finish=min(cursor+unit_length,source.end)
|
|
next_states=[]
|
|
for cost,units,home,state in states:
|
|
introducing=home is None
|
|
if introducing:
|
|
requests=[(r,'introduce') for r in roots]
|
|
else:
|
|
operations=('repeat_with_harmonic_adaptation',) if cursor==source.start and source.start<=4*bar else ('repeat_with_harmonic_adaptation','answer','extend')
|
|
requests=[(home_reference(home,op),op) for op in operations]
|
|
used=incoming.callbacks+sum(u.operation=='callback' for u in units)
|
|
if cursor>=8*bar and cursor==source.start and used<2:
|
|
requests.extend((dataclasses.replace(r,treatment='callback'),'callback') for r in roots[:2] if r.end-r.start<=bar)
|
|
for ref,operation in requests:
|
|
if ref.notes[-1].end>finish-cursor:
|
|
reduced=tuple(n for n in ref.notes if n.end<=finish-cursor)
|
|
if len(reduced)<2:continue
|
|
ref=dataclasses.replace(ref,notes=reduced,treatment='contour_seed')
|
|
operation='answer'
|
|
options=unit_candidates(ref,cursor,finish,state,harmony,library,rhythms,meter,context,home=home,operation=operation,
|
|
terminal=final and finish==source.end,batch=batch)
|
|
for unit in options:
|
|
if introducing and similarity.near_copy(unit.notes,head,cursor,finish)['near_copy']:
|
|
context.diagnostics['near_copy_rejections']=context.diagnostics.get('near_copy_rejections',0)+1;continue
|
|
actual_home=home or make_home(unit,harmony,cursor,finish)
|
|
following=dataclasses.replace(state,home=actual_home,last=unit.notes[-1],notes=unit.notes,source_reference=state.source_reference or ref)
|
|
next_states.append((cost+unit.score,(*units,unit),actual_home,following))
|
|
states=sequences.diverse(next_states,lambda v:(v[0],policy.stable_rank(seed,tuple(u.notes for u in v[1]))),
|
|
lambda v:(v[2].id,v[1][-1].notes[-1].pitch.number()),2 if incoming.home and finish<source.end else 8)
|
|
if not states:return ()
|
|
cursor=finish
|
|
result=[]
|
|
for cost,units,home,state in states:
|
|
plain=tuple(n for u in units for n in u.notes)
|
|
if similarity.near_copy(plain,head,source.start,source.end)['near_copy']:
|
|
context.diagnostics['near_copy_rejections']=context.diagnostics.get('near_copy_rejections',0)+1;continue
|
|
features=tuple(u.feature for u in units)
|
|
anchors=tuple(u.anchor for u in units)
|
|
options=[(plain,(),())]
|
|
if stage=='elaborated':
|
|
protected=(*features,types.IdentifyingFeature('destinations',tuple(n.event_id for n in plain if any(n.event_id+':arrival'==a.id for a in anchors)),(),(),'',()))
|
|
for actual,edit,route in leads.elaborate(plain,protected,harmony,library,rhythms,meter,source,sustain_entry=True):
|
|
options.append((actual,(edit,),(route,)))
|
|
for notes,edits,selected_routes in options:
|
|
identifier=part+'_'+source.id+'_'+policy.stable_rank(seed,leads.signature(notes))[:12]
|
|
transforms=tuple(types.Transformation(identifier+f':{i}',u.reference.id,home.id,u.operation,u.notes[0].start,u.notes[-1].end,
|
|
tuple(n.event_id for n in notes if u.notes[0].start<=n.start<u.notes[-1].end),tuple((n.start-u.notes[0].start,n.end-u.notes[0].start) for n in notes if u.notes[0].start<=n.start<u.notes[-1].end),preserves=(),source=u.reference.id+'; rhythm '+u.source) for i,u in enumerate(units))
|
|
source_ref=state.source_reference
|
|
draft=types.MelodicDraft(identifier,source.start,source.end,notes,transforms,
|
|
tuple(a.id.removesuffix(':arrival') for a in anchors),(),anchors,home,'introduce' if not incoming.home else 'develop',
|
|
features=features,reference=source_ref.id,reference_motif=home,source_phrase=source,source_reference=source_ref,
|
|
source_links=tuple((n.event_id,n.sources) for n in notes))
|
|
# One shared musical context crosses computational/source boundaries.
|
|
activity=[];cursor=source.start
|
|
for n in notes:
|
|
if n.start>cursor:activity.append(types.ActivitySpan(cursor,n.start,'space',F(),F()))
|
|
activity.append(types.ActivitySpan(n.start,n.end,'active',F(5,4),F()))
|
|
cursor=n.end
|
|
if cursor<source.end:activity.append(types.ActivitySpan(cursor,source.end,'space',F(),F()))
|
|
motif=types.Motif(home.id,home.notes,home.duration,(), 'realized solo theme from '+source_ref.id)
|
|
plan=types.PhrasePlan(identifier,source.start,source.end,motif,draft.relationship,'thematic_recomposition',None,(41,76),F(5,4),
|
|
source.end-notes[-1].end,tuple(activity),(), 'sustain' if final else 'connected','closed' if final else 'open',seed,
|
|
source.id,source.start,source.end,(anchors,))
|
|
phrase=realization.checked_phrase(plan,draft,notes,incoming.treatments,incoming,harmony,tempo,edits)
|
|
if phrase is None:continue
|
|
density=len(notes)*4/float(source.end-source.start)
|
|
score=cost/len(units)+.35*(max(0,4-density,density-6)-max(0,4-len(plain)*4/float(source.end-source.start),len(plain)*4/float(source.end-source.start)-6))
|
|
score+=.05*phrase.state.travel/max(1,len(notes))/2000
|
|
current=dataclasses.replace(phrase.state,home=home,source_reference=source_ref,callbacks=incoming.callbacks+sum(u.operation=='callback' for u in units))
|
|
# Store the actual introduced sound, including any legal elaboration.
|
|
if not incoming.home:
|
|
introduced=tuple(n for n in notes if n.start<units[0].notes[-1].end)
|
|
heard=dataclasses.replace(home,notes=tuple(dataclasses.replace(n,start=n.start-source.start,end=n.end-source.start,position=None) for n in introduced),
|
|
degree_steps=relative_steps(introduced,harmony))
|
|
current=dataclasses.replace(current,home=heard,themes=(heard,))
|
|
source_control=leads.shift(source.notes, next((o for o in (0,1,-1,2,-2) if all(41<=n.pitch.number()+12*o<=76 for n in source.notes)),0))
|
|
original=dataclasses.replace(draft,id=identifier+':source',notes=source_control,features=(),anchors=(),structural=())
|
|
refined=dataclasses.replace(draft,id=identifier+':plain',notes=plain)
|
|
result.append(dataclasses.replace(phrase,score=score,state=current,initial=original,refined=refined,routes=(*tuple(r for u in units for r in u.routes if all(any((n.event_id,n.start,n.end,n.pitch)==(a.event_id,a.start,a.end,a.pitch) for a in notes) for n in r.notes)),*selected_routes)))
|
|
return sequences.diverse(result,lambda p:(p.score,policy.stable_rank(seed,p.draft.id)),
|
|
lambda p:(p.state.home.id,p.state.last.pitch.number(),sum(e.kind=='complete_route' for e in p.edits)),8)
|