Revamp
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
"""Optional MusicXML views of finished production roles; never composes."""
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
from pathlib import Path
|
||||
from fractions import Fraction as F
|
||||
|
||||
from generator import model, musicxml_writer, score
|
||||
|
||||
|
||||
def region(document, part):
|
||||
measures = document['staves'][0]['measures']
|
||||
first = next(i for i, m in enumerate(measures) if m.get('section_label') == part.capitalize() + ' Solo')
|
||||
end = next((i for i in range(first + 1, len(measures)) if measures[i].get('section_label')), len(measures))
|
||||
return first, end - first
|
||||
|
||||
|
||||
def excerpt(document, first, count):
|
||||
result = copy.deepcopy(document)
|
||||
staff = result['staves'][0]
|
||||
original = staff['measures']
|
||||
staff['measures'] = original[first:first + count]
|
||||
if not staff['measures']:
|
||||
raise ValueError('Empty solo export region')
|
||||
notes = [n for m in staff['measures'] for n in score.measure_notes(m)]
|
||||
identities = {p['id'] for n in notes for p in n['pitches']}
|
||||
events = {n['id'] for n in notes}
|
||||
for note in notes:
|
||||
for pitch in note['pitches']:
|
||||
for key in ('tie_from', 'tie_to'):
|
||||
if pitch.get(key) not in identities:
|
||||
pitch.pop(key, None)
|
||||
staff['tuplets'] = [t for t in staff.get('tuplets', []) if t['start'] in events and t['end'] in events]
|
||||
tuplets = {t['id'] for t in staff['tuplets']}
|
||||
for note in notes:
|
||||
if 'tuplet_ids' in note:
|
||||
note['tuplet_ids'] = [i for i in note['tuplet_ids'] if i in tuplets]
|
||||
offset = sum((model.fraction(m['duration']) for m in original[:first]), F())
|
||||
end = offset + sum((model.fraction(m['duration']) for m in staff['measures']), F())
|
||||
for key in ('scale_spans', 'octave_shifts'):
|
||||
staff[key] = [{**s, 'start': model.rational(max(offset, model.fraction(s['start'])) - offset),
|
||||
'end': model.rational(min(end, model.fraction(s['end'])) - offset)} for s in staff.get(key, [])
|
||||
if model.fraction(s['start']) < end and model.fraction(s['end']) > offset]
|
||||
model.validate_document(result)
|
||||
return result
|
||||
|
||||
|
||||
def write_role(result, path):
|
||||
role = result['role']
|
||||
if role not in ('acoustic_solo', 'electric_solo'):
|
||||
raise ValueError('Solo XML export requires an acoustic_solo or electric_solo role')
|
||||
part = role.split('_')[0]
|
||||
document = result['document']
|
||||
first, count = region(document, part)
|
||||
selected = excerpt(document, first, count)
|
||||
numbers = [m['number'] for m in selected['staves'][0]['measures']]
|
||||
for i, measure in enumerate(selected['staves'][0]['measures'], 1):
|
||||
measure['number'] = str(i)
|
||||
path = Path(path)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
musicxml_writer.write_musicxml(path, selected, tablature=True)
|
||||
return {'role': role, 'file': path.name, 'form_start_measure': 1,
|
||||
'measure_count': count, 'arrangement_measures': numbers,
|
||||
'arrangement_start_beat': str(sum((model.fraction(m['duration']) for m in document['staves'][0]['measures'][:first]), F())),
|
||||
'pitch_convention': 'guitar-written; explicit octave transposition -1',
|
||||
'trace': result['trace'], 'phrase_report': result.get('details', {})}
|
||||
|
||||
|
||||
def export_tree(assets, output=None):
|
||||
assets = Path(assets)
|
||||
output = Path(output) if output else assets / 'solo_xml'
|
||||
output.mkdir(parents=True, exist_ok=True)
|
||||
entries = {}
|
||||
for path in sorted((assets / 'analysis').glob('*.roles')):
|
||||
roles = json.loads(path.read_text())
|
||||
for i, part in enumerate(('acoustic', 'electric'), 1):
|
||||
name = path.stem.replace('_', '-') + f'-solo{i}.xml'
|
||||
entries[path.stem + '/' + part] = write_role(roles[part + '_solo'], output / name)
|
||||
manifest = {'version': 1, 'composition_inputs': False, 'solos': entries}
|
||||
# Runtime score discovery treats *.json under assets as score documents.
|
||||
# Diagnostic manifests deliberately use a non-score extension, like .roles.
|
||||
(output / 'manifest.solo').write_text(json.dumps(manifest, sort_keys=True, indent=2) + '\n')
|
||||
return manifest
|
||||
@@ -14,9 +14,45 @@ def fragmentation(notes):
|
||||
return isolated + .1*gaps
|
||||
|
||||
|
||||
def shared_arrival(draft,harmony,treatments=()):
|
||||
"""A provisional endpoint may also begin a continuation, without reattack.
|
||||
|
||||
Retain the continuation event's identity. A rhythm/exact-quote contract or
|
||||
a committed destination can forbid sharing; full context is revalidated.
|
||||
"""
|
||||
notes=draft.notes
|
||||
joins={t.start for t in draft.transformations[1:] if t.operation=='continue'}
|
||||
fixed=set(draft.protected)|set(draft.structural)
|
||||
fixed.update(n.event_id for n in notes if n.role=='quote')
|
||||
fixed.update(e for f in draft.features if 'rhythm' in f.preserves for e in f.events)
|
||||
edits=[]
|
||||
for join in sorted(joins):
|
||||
for i,(a,b) in enumerate(zip(notes,notes[1:])):
|
||||
if b.start!=join or a.end!=b.start or a.pitch!=b.pitch:continue
|
||||
if a.event_id in fixed or b.event_id in fixed:continue
|
||||
actual=(*notes[:i],dataclasses.replace(b,start=a.start),*notes[i+2:])
|
||||
if not drafts.contract_valid(draft,actual)[0]:continue
|
||||
if not harmony.evaluate(actual,treatments,until=draft.end,complete=True).valid:continue
|
||||
if not harmonic.anchors_valid(draft.anchors,actual,draft.end,complete=True,closure='closed')[0]:continue
|
||||
edits.append(types.DraftEdit('shared_arrival',a.start,b.end,(a.event_id,b.event_id),(b.event_id,),
|
||||
'reopen provisional endpoint as the same sounding continuation; preserve feature and arrival contracts'))
|
||||
notes=actual
|
||||
break
|
||||
if not edits:return draft,()
|
||||
by_id={n.event_id:n for n in notes}
|
||||
transforms=[]
|
||||
for t in draft.transformations:
|
||||
group=tuple(by_id[e] for e in t.events if e in by_id)
|
||||
if group:
|
||||
transforms.append(dataclasses.replace(t,start=group[0].start,end=group[-1].end,
|
||||
events=tuple(n.event_id for n in group),rhythm=tuple((n.start-group[0].start,n.end-group[0].start) for n in group)))
|
||||
return dataclasses.replace(draft,notes=notes,transformations=tuple(transforms)),tuple(edits)
|
||||
|
||||
|
||||
def revise(draft, harmony, treatments=()):
|
||||
notes=draft.notes; edits=[]
|
||||
if len(notes)<3: return draft, ()
|
||||
draft,shared=shared_arrival(draft,harmony,treatments)
|
||||
notes=draft.notes; edits=list(shared)
|
||||
if len(notes)<3: return draft, tuple(edits)
|
||||
protected=set(draft.protected)|set(draft.structural)|{n.event_id for n in notes if n.role=='quote'}
|
||||
# An identifying rhythm also protects its internal rests.
|
||||
protected.update(e for feature in draft.features if 'rhythm' in feature.preserves for e in feature.events)
|
||||
|
||||
@@ -50,7 +50,7 @@ class Policy:
|
||||
enclosure_routes: int = 4
|
||||
enclosure_frontier: int = 8
|
||||
enclosure_max_per_32_bars: int = 4
|
||||
composition_version: str = 'independent-roles-1'
|
||||
composition_version: str = 'purposeful-phrases-2'
|
||||
|
||||
# Preserve the former default root seeds while decoupling them from policy edits.
|
||||
SEED_NAMESPACE = 'a85abb240d7cfa9d054375b654bcb5c23c746201fd4aed7acf81cb3f4ee720e1'
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
+75
-36
@@ -99,10 +99,34 @@ def feature(reference, actual, *, treatment=None):
|
||||
tuple(n.pitch.number()+octave for n in original))
|
||||
|
||||
|
||||
def rhythm_windows(library,meter,length,context):
|
||||
def rhythmic_purpose(operation, terminal=False):
|
||||
if terminal:return 'arrival'
|
||||
if operation in ('introduce','callback','return','repeat_with_harmonic_adaptation'):return 'entrance'
|
||||
if operation in ('answer','fragment'):return 'answer'
|
||||
return 'connect' if operation=='continue' else 'continue'
|
||||
|
||||
|
||||
def rhythm_cost(pattern,length,purpose):
|
||||
"""Evaluate a rhythmic job, not resemblance to any target score or density."""
|
||||
if not pattern:return float('inf')
|
||||
gaps=[b[0]-a[1] for a,b in zip(pattern,pattern[1:])]
|
||||
isolated=sum(b-a<=F(1,2) and prev[1]<a and b<nxt[0]
|
||||
for prev,(a,b),nxt in zip(pattern,pattern[1:],pattern[2:]))
|
||||
values={b-a for a,b in pattern}
|
||||
uniform=.15 if len(pattern)>=4 and len(values)==1 else 0.
|
||||
if purpose in ('continue','connect'):
|
||||
# Silence may articulate an idea; disconnected tiny islands do not carry
|
||||
# unfinished motion. No minimum occupancy or chorus attack quota.
|
||||
return isolated+.25*float(sum(gaps,F())/length)+uniform
|
||||
if purpose=='arrival':
|
||||
return isolated+float(max(F(),F(1)- (pattern[-1][1]-pattern[-1][0])))/2+uniform
|
||||
return isolated+uniform
|
||||
|
||||
|
||||
def rhythm_windows(library,meter,length,context,purpose='continue'):
|
||||
from generator.solo import drafts
|
||||
tempo=getattr(context,'tempo',120)
|
||||
key=('theme-rhythm',id(library),meter,length,tempo)
|
||||
key=('theme-rhythm',id(library),meter,length,tempo,purpose)
|
||||
if key not in context.pools:
|
||||
patterns={}
|
||||
bar=F(meter.split('/')[0])*4/int(meter.split('/')[1])
|
||||
@@ -112,8 +136,8 @@ def rhythm_windows(library,meter,length,context):
|
||||
# than accidentally excluding eighths/dotted values by snippet length.
|
||||
# Pitch paths and feature contracts span the assembled rhythm's seams.
|
||||
split=bar
|
||||
for left,left_id in rhythm_windows(library,meter,split,context):
|
||||
for right,right_id in rhythm_windows(library,meter,length-split,context):
|
||||
for left,left_id in rhythm_windows(library,meter,split,context,'continue' if purpose=='arrival' else purpose):
|
||||
for right,right_id in rhythm_windows(library,meter,length-split,context,purpose):
|
||||
combined=(*left,*((a+split,b+split) for a,b in right))
|
||||
if len(combined)<=16:patterns.setdefault(combined,left_id+' + '+right_id)
|
||||
for identifier,pattern,total in drafts.catalog(library,meter):
|
||||
@@ -123,22 +147,29 @@ def rhythm_windows(library,meter,length,context):
|
||||
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)
|
||||
# Authored cells can start late and continue over computational barlines.
|
||||
for identifier,pattern,total in generator.vocabulary.rhythm_cells(tempo):
|
||||
for cell in generator.vocabulary.rhythm_cells(tempo,purpose):
|
||||
identifier,pattern,total=cell.id,cell.pattern,cell.duration
|
||||
if total>length:continue
|
||||
for offset in (F(),F(1,2),max(F(),length-total)):
|
||||
if offset+total>length:continue
|
||||
head=tuple((a+offset,b+offset) for a,b in pattern)
|
||||
tails=tuple((a,b) for a,b in next(iter(patterns),()) if a>=offset+total)
|
||||
combined=(*head,*tails)
|
||||
if 2<=len(combined)<=16:patterns.setdefault(combined,'authored:'+identifier)
|
||||
# Keep representative compatible continuations; never let dictionary
|
||||
# insertion order secretly choose the only tail for an authored cell.
|
||||
tails=sequences.diverse(tuple(patterns),lambda p:(rhythm_cost(p,length,purpose),policy.stable_rank('rhythm-tail',p)),
|
||||
lambda p:(len(p),p[-1][1]-p[-1][0]),2) or ((),)
|
||||
for tail in tails:
|
||||
if any(a<offset+total<b for a,b in tail):continue
|
||||
combined=(*head,*((a,b) for a,b in tail if a>=offset+total))
|
||||
if 2<=len(combined)<=16:patterns.setdefault(combined,'authored:'+identifier)
|
||||
context.pools[key]=sequences.diverse(tuple(patterns.items()),
|
||||
lambda x:(len(x[0])>16,policy.stable_rank('rhythm-window',x[0])),
|
||||
lambda x:(rhythm_cost(x[0],length,purpose)+(0 if x[1].startswith('authored:') else .05),policy.stable_rank('rhythm-window',x[0])),
|
||||
lambda x:(x[1].startswith('authored:'),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, ending=None):
|
||||
length=finish-start; values={}
|
||||
purpose=rhythmic_purpose(operation,terminal)
|
||||
if home and operation in ('repeat_with_harmonic_adaptation','return'):
|
||||
pattern=tuple((n.start,n.end) for n in home.notes if n.end<=length)
|
||||
if pattern:values[pattern]='heard-theme'
|
||||
@@ -148,12 +179,12 @@ def patterns(ref, start, finish, rhythms, meter, context, *, home=None, operatio
|
||||
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):
|
||||
for tail,label in rhythm_windows(rhythms,meter,length-duration,context,purpose):
|
||||
values[(*prefix,*((a+duration,b+duration) for a,b in tail))]=ref.id+' + '+label
|
||||
else:
|
||||
values.update(rhythm_windows(rhythms,meter,length,context))
|
||||
values.update(rhythm_windows(rhythms,meter,length,context,purpose))
|
||||
if home and operation=='answer':
|
||||
for p,label in rhythm_windows(rhythms,meter,length,context):values.setdefault(p,label)
|
||||
for p,label in rhythm_windows(rhythms,meter,length,context,purpose):values.setdefault(p,label)
|
||||
if not fixed_rhythm:
|
||||
# Develop the heard timing, preserving total duration. Eighth/quarter/dotted
|
||||
# alternatives are actual contracts, not failed copies of a rhythmic seed.
|
||||
@@ -174,7 +205,7 @@ def patterns(ref, start, finish, rhythms, meter, context, *, home=None, operatio
|
||||
if len(head)>=len(ref.notes):
|
||||
arrivals[(*head,(arrival,ending.hold_until-start))]=label+'; planned sustained close'
|
||||
values=arrivals
|
||||
return sequences.diverse(tuple(values.items()),lambda x:(len(x[0])>16,policy.stable_rank(ref.id,x[0])),
|
||||
return sequences.diverse(tuple(values.items()),lambda x:(rhythm_cost(x[0],length,purpose),policy.stable_rank(ref.id,x[0])),
|
||||
lambda x:(len(x[0]),tuple(b-a for a,b in x[0][:3]),x[0][0][0]),policy.DEFAULT.theme_rhythm_alternatives)
|
||||
|
||||
|
||||
@@ -251,10 +282,10 @@ def _prefix_paths(templates,ref,first,harmony,library):
|
||||
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,harmony.profile.register,'seed '+ref.id))
|
||||
paths.extend(r.notes for r in routes.candidates(template,{template[-1].event_id},harmony,library,harmony.profile.register,'seed '+ref.id,purpose='entrance'))
|
||||
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)
|
||||
return sequences.diverse(valid,lambda ns:(routes.path_cost(ns),policy.stable_rank(ref.id,ns)),
|
||||
lambda ns:routes.path_family(ns,harmony),4)
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
@@ -267,6 +298,8 @@ class Unit:
|
||||
score: float
|
||||
source: str
|
||||
routes: tuple[types.RouteCandidate,...] = ()
|
||||
start: F = F()
|
||||
end: F = F()
|
||||
|
||||
|
||||
def unit_candidates(ref,start,finish,incoming,harmony,library,rhythms,meter,context,*,home=None,operation='introduce',terminal=False,batch=0,ending=None,goal=None):
|
||||
@@ -317,7 +350,7 @@ def unit_candidates(ref,start,finish,incoming,harmony,library,rhythms,meter,cont
|
||||
continue
|
||||
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,harmony.profile.register,operation+' '+ref.id)[:3+batch]:
|
||||
for route in routes.candidates(tail,{tail[-1].event_id},harmony,library,harmony.profile.register,operation+' '+ref.id,purpose=rhythmic_purpose(operation,terminal))[:3+batch]:
|
||||
proposed.append(((*prefix[:-1],*route.notes),(route,)))
|
||||
seen=set()
|
||||
for actual,connections in proposed:
|
||||
@@ -337,7 +370,8 @@ def unit_candidates(ref,start,finish,incoming,harmony,library,rhythms,meter,cont
|
||||
score=harmony.profile.cost(profiles.weighted_center(actual))+2*checked.exposure+entry+leaps+phrasing.destination_cost(last.pitch,last.start,last.end,harmony,incoming,goal,terminal)
|
||||
anchor=ending or types.Anchor(last.event_id+':arrival',last.start,last.start,last.pitch,harmony.members(last.start,last.end),harmony.profile.register,
|
||||
last.end-last.start,'intermediate',harmony.at(last.start).chord,hold_until=last.end)
|
||||
result.append(Unit(actual,contract,ref,operation,anchor,score,label,connections))
|
||||
score+=.25*rhythm_cost(tuple((n.start-start,n.end-start) for n in actual),finish-start,rhythmic_purpose(operation,terminal))
|
||||
result.append(Unit(actual,contract,ref,operation,anchor,score,label,connections,start,finish))
|
||||
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(),tuple(n.end-n.start for n in u.notes[:count])),policy.DEFAULT.theme_unit_alternatives)
|
||||
@@ -366,12 +400,25 @@ def make_home(unit,harmony,start=None,end=None):
|
||||
return dataclasses.replace(home,degree_steps=relative_steps(unit.notes,harmony),signature=home.notes[:len(unit.reference.notes)])
|
||||
|
||||
|
||||
def unit_end(source,cursor,home,ending,bar):
|
||||
"""Bound search by time without cutting the heard signature at a barline."""
|
||||
length=2*bar
|
||||
if home and home.signature:
|
||||
length=home.signature[-1].end
|
||||
while 0<length<bar:
|
||||
length+=home.signature[-1].end
|
||||
length=min(2*bar,max(bar,length))
|
||||
finish=min(source.end,cursor+length)
|
||||
if source.end-finish<bar+2 or ending.earliest<finish:
|
||||
finish=source.end
|
||||
return finish
|
||||
|
||||
|
||||
def phrase_candidates(source,analysis,incoming,part,harmony,library,rhythms,meter,tempo,context,seed,*,stage='elaborated',batch=0,final=False,solo_end=None):
|
||||
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
|
||||
solo_end=solo_end or max(c.span.end for c in harmony.choices)
|
||||
head=source_notes(analysis)
|
||||
seed_key=('source-seeds',analysis,bar,seed)
|
||||
@@ -379,17 +426,14 @@ def phrase_candidates(source,analysis,incoming,part,harmony,library,rhythms,mete
|
||||
roots=context.pools[seed_key]
|
||||
ending_options=phrasing.endings(source,incoming,part,harmony,bar,limit=8*(batch+1),final=final)
|
||||
states=[(0.,(),incoming.home,incoming,ending) for ending in ending_options]
|
||||
cursor=source.start
|
||||
while cursor<source.end and states:
|
||||
finish=min(cursor+unit_length,source.end)
|
||||
if cursor==source.start and source.end-source.start>=3*bar:
|
||||
# Selected home rhythms finish as objects, not at the nearest barline.
|
||||
signature=incoming.home.signature if incoming.home else ()
|
||||
if signature and signature[-1].end>unit_length:
|
||||
finish=min(source.end,cursor+signature[-1].end)
|
||||
if source.end-finish<6 or any(ending.earliest<finish for *_,ending in states):finish=source.end
|
||||
while states and any(not units or units[-1].end<source.end for _,units,_,_,_ in states):
|
||||
next_states=[]
|
||||
for cost,units,home,state,ending in states:
|
||||
cursor=units[-1].end if units else source.start
|
||||
if cursor>=source.end:
|
||||
next_states.append((cost,units,home,state,ending))
|
||||
continue
|
||||
finish=unit_end(source,cursor,home,ending,bar)
|
||||
introducing=home is None
|
||||
if introducing:
|
||||
requests=[(r,'introduce') for r in roots]
|
||||
@@ -437,19 +481,14 @@ def phrase_candidates(source,analysis,incoming,part,harmony,library,rhythms,mete
|
||||
following=dataclasses.replace(state,home=actual_home,last=unit.notes[-1],notes=unit.notes,source_reference=state.source_reference or ref,
|
||||
history=history,returns=(*state.returns,*((cursor,) if operation=='return' else ())),
|
||||
register_peak=max(state.register_peak,*(n.pitch.number() for n in unit.notes)))
|
||||
next_states.append((cost+unit.score+.8*repetition+appearance,(*units,unit),actual_home,following,ending))
|
||||
next_states.append((cost+float(finish-cursor)*(unit.score+.8*repetition+appearance),(*units,unit),actual_home,following,ending))
|
||||
def rank(value):
|
||||
cost,units,home,state,ending=value
|
||||
attacks=sum(len(u.notes) for u in units)
|
||||
remaining=max(F(),ending.earliest-finish)
|
||||
possible=attacks+2*float(remaining)+(0 if finish==source.end else 1)
|
||||
expected=float(source.end-source.start)
|
||||
activity_bound=0.0 # Activity is a gesture/difficulty property, not a chorus quota.
|
||||
return cost/max(1,len(units))+activity_bound,policy.stable_rank(seed,tuple(u.notes for u in units))
|
||||
elapsed=units[-1].end-source.start
|
||||
return cost/max(.001,float(elapsed)),policy.stable_rank(seed,tuple(u.notes for u in units))
|
||||
states=sequences.diverse(next_states,rank,
|
||||
lambda v:(v[2].id,v[4].pitch.number(),v[4].duration,len(v[1][-1].notes),v[1][-1].operation),policy.DEFAULT.theme_continuations)
|
||||
if not states:return ()
|
||||
cursor=finish
|
||||
result=[]
|
||||
for cost,units,home,state,ending in states:
|
||||
plain=tuple(n for u in units for n in u.notes)
|
||||
|
||||
Reference in New Issue
Block a user