Sepearate Generation
This commit is contained in:
@@ -40,6 +40,9 @@ def annotate(document, solo, offset):
|
||||
|
||||
def apply(source, parts, *, rhythms, scales, sequences, solos=None, revise=True, contexts=None, references=(), stage='elaborated', enclosures_enabled=True):
|
||||
"""Compose once per song, replacing only already-placed solo section windows."""
|
||||
if references:
|
||||
raise ValueError('Audition references are not production composition inputs')
|
||||
from generator.arrange import generate
|
||||
analysis = generator.solo.analysis.analyze(source)
|
||||
sections = regions(source)
|
||||
source_measures = generator.score.source_staff(source)['measures']
|
||||
@@ -47,8 +50,11 @@ def apply(source, parts, *, rhythms, scales, sequences, solos=None, revise=True,
|
||||
requests = {}
|
||||
for section_index, part in ((3, 'acoustic'), (4, 'electric')):
|
||||
first, count, offset, length = sections[section_index]
|
||||
initial = generator.accompaniment.carried_chords(parts[part]['staves'][0]['measures'])[first]
|
||||
template = copy.deepcopy(parts[part]['staves'][0]['measures'][first:first + count])
|
||||
form = generator.arrange.form.full_arrangement_sections(source_measures, part)
|
||||
section = form[section_index]
|
||||
effective = generate.section_source(source_measures, section, form[section_index+1])
|
||||
initial = generator.accompaniment.carried_chords(effective)[section.source_measure_index]
|
||||
template = copy.deepcopy(effective[section.source_measure_index:section.source_measure_index+count])
|
||||
cutoff = length
|
||||
if part == 'electric' and generator.score.body_start_measure_index(source_measures):
|
||||
pickup = source_measures[0]
|
||||
|
||||
@@ -83,7 +83,7 @@ def capture(root, stage, libraries=None, contexts=None, *, enclosures_enabled=Tr
|
||||
if destination.exists():
|
||||
raise ValueError(f'{destination}: snapshot already exists; choose a new output root')
|
||||
rhythms, scales, sequences = libraries or resources()
|
||||
reference_library = generator.solo.references.load_library(pathlib.Path('data/lead_phrases'))
|
||||
reference_library = generator.solo.references.load_library(pathlib.Path('audition/controls'))
|
||||
result = {'enclosures_enabled':enclosures_enabled,'stage': stage, 'implementation': fingerprint(), 'reference_versions': [r.version for r in reference_library], 'songs': {}}
|
||||
try:
|
||||
result['policy'] = importlib.import_module('generator.solo.policy').describe()
|
||||
@@ -107,7 +107,7 @@ def capture(root, stage, libraries=None, contexts=None, *, enclosures_enabled=Tr
|
||||
if shared:
|
||||
for context in shared.values():
|
||||
context.diagnostics = generator.solo.realization.SearchContext().diagnostics
|
||||
generator.solo.arrangement.apply(document, parts, rhythms=rhythms, scales=scales, sequences=sequences, solos=solos, revise=stage != 'phrase', contexts=shared, references=reference_library,enclosures_enabled=enclosures_enabled)
|
||||
generator.solo.arrangement.apply(document, parts, rhythms=rhythms, scales=scales, sequences=sequences, solos=solos, revise=stage != 'phrase', contexts=shared,enclosures_enabled=enclosures_enabled)
|
||||
song = {'source_sha256': hashlib.sha256(path.read_bytes()).hexdigest(), 'parts': parts, 'solos': {}, 'elapsed_seconds': time.monotonic() - started}
|
||||
regions = generator.solo.arrangement.regions(document)
|
||||
for solo, index in zip(solos, (3, 4)):
|
||||
|
||||
+11
-33
@@ -20,6 +20,8 @@ import generator.solo.realization
|
||||
import generator.solo.types
|
||||
|
||||
def compose(analysis, measures, part, end, *, scales, sequences, rhythms, initial=None, revise=True, context=None, references=(), stage='elaborated', mode='thematic', enclosures_enabled=True):
|
||||
if references or context is not None and getattr(context,'references',()):
|
||||
raise ValueError('Audition references are not production composition inputs')
|
||||
started = time.monotonic()
|
||||
timeline = generator.scales.timeline.read({'measures': measures}, initial=initial)
|
||||
region = generator.scales.timeline.Solo(fractions.Fraction(), end, part.capitalize() + ' Solo')
|
||||
@@ -32,43 +34,18 @@ def compose(analysis, measures, part, end, *, scales, sequences, rhythms, initia
|
||||
options = leads.solo_options(analysis, measures, part, end, choices, sequences, rhythms, context, stage, mode)
|
||||
result = options[0][1]
|
||||
result.diagnostics['strict_composition_seconds'] = time.monotonic()-started
|
||||
result = generator.solo.enclosures.refine(result,sequences,enclosures_enabled and stage=='elaborated' and mode=='thematic')
|
||||
from generator.solo import expressions
|
||||
result = expressions.refine(result,enclosures_enabled and stage=='elaborated' and mode=='thematic')
|
||||
validate(result)
|
||||
return result
|
||||
|
||||
|
||||
def compose_pair(analysis, requests, *, scales, sequences, rhythms, contexts=None, stage='elaborated', mode='thematic', enclosures_enabled=True):
|
||||
"""Select two recognizable interpretations together, before arranging backing."""
|
||||
from generator.solo import leads
|
||||
started = time.monotonic()
|
||||
pools = {}
|
||||
for part, (measures,end,initial) in requests.items():
|
||||
timeline = generator.scales.timeline.read({'measures': measures},initial=initial)
|
||||
choices = generator.scales.selection.select(timeline,generator.scales.timeline.Solo(0,end,part),scales)
|
||||
for c in choices:
|
||||
if c.span.chord and c.scale is None:
|
||||
raise ValueError(f'{part} solo, source measure {c.span.source_measure}: No exact match within the permitted catalog')
|
||||
context = (contexts or {}).get(part) or generator.solo.realization.SearchContext()
|
||||
pools[part] = leads.solo_options(analysis,measures,part,end,choices,sequences,rhythms,context,stage,mode)
|
||||
def theme_key(solo):
|
||||
home=next((p.state.home for p in solo.phrases if p.state and p.state.home),None)
|
||||
if not home or not home.notes:return ()
|
||||
return tuple((n.start-home.notes[0].start,n.end-n.start,n.pitch.number()-home.notes[0].pitch.number()) for n in home.notes)
|
||||
pairs = [(a_cost+e_cost+2*leads.similarity(a,e),a,e) for a_cost,a in pools['acoustic'] for e_cost,e in pools['electric']
|
||||
if mode!='thematic' or not theme_key(a) or theme_key(a)!=theme_key(e)]
|
||||
if not pairs:
|
||||
raise generator.solo.realization.SearchExhausted('Distinct acoustic/electric themes not found within the bounded pair search')
|
||||
_,acoustic,electric = min(pairs,key=lambda v:(v[0],generator.solo.policy.stable_rank('solo-pair',(v[1].notes(),v[2].notes()))))
|
||||
overlap = leads.similarity(acoustic,electric)
|
||||
strict_seconds=time.monotonic()-started
|
||||
selected=[]
|
||||
for solo in (acoustic,electric):
|
||||
solo.diagnostics.update(pair_similarity=overlap,pairs_compared=len(pairs),solo_alternatives=len(pools[solo.part]))
|
||||
solo.diagnostics['strict_pair_seconds']=strict_seconds
|
||||
solo=generator.solo.enclosures.refine(solo,sequences,enclosures_enabled and stage=='elaborated' and mode=='thematic')
|
||||
validate(solo)
|
||||
selected.append(solo)
|
||||
return tuple(selected)
|
||||
"""Compatibility batch helper: each winner is selected independently."""
|
||||
return tuple(compose(analysis, measures, part, end, scales=scales, sequences=sequences,
|
||||
rhythms=rhythms, initial=initial, context=(contexts or {}).get(part), stage=stage,
|
||||
mode=mode, enclosures_enabled=enclosures_enabled)
|
||||
for part, (measures, end, initial) in requests.items())
|
||||
|
||||
|
||||
def phrase_notes(phrase):
|
||||
@@ -144,7 +121,8 @@ def validate(solo):
|
||||
harmony = generator.solo.harmonic.Harmony(solo.choices)
|
||||
contracts = tuple(dict.fromkeys(t for phrase in solo.phrases if phrase.state for t in phrase.state.treatments))
|
||||
enclosures = tuple(e for p in solo.phrases if p.draft for e in p.draft.enclosures)
|
||||
checked = harmony.evaluate(solo.notes(), contracts, until=solo.end, complete=True,enclosures=enclosures)
|
||||
expressions = tuple(e for p in solo.phrases if p.draft for e in p.draft.expressions)
|
||||
checked = harmony.evaluate(solo.notes(), contracts, until=solo.end, complete=True,enclosures=enclosures,expressions=expressions)
|
||||
if not checked.valid:
|
||||
raise ValueError(f'{solo.part} solo: {checked.error}')
|
||||
for phrase in solo.phrases:
|
||||
|
||||
@@ -7,6 +7,7 @@ from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import fractions
|
||||
import functools
|
||||
|
||||
import generator.model
|
||||
import generator.solo.harmonic
|
||||
@@ -14,7 +15,7 @@ import generator.solo.policy
|
||||
import generator.solo.scoring
|
||||
import generator.solo.sequences
|
||||
import generator.solo.types
|
||||
import generator.solo.references
|
||||
import generator.solo.features
|
||||
|
||||
F = fractions.Fraction
|
||||
|
||||
@@ -47,9 +48,13 @@ class RhythmDraft:
|
||||
octave: int = 0
|
||||
|
||||
def catalog(library, meter):
|
||||
return _catalog(library.for_meter(meter))
|
||||
|
||||
@functools.lru_cache(maxsize=32)
|
||||
def _catalog(snippets):
|
||||
"""Read complete snippets; ties across authored bars stay one attack."""
|
||||
result = {}
|
||||
for snippet in library.for_meter(meter):
|
||||
for snippet in snippets:
|
||||
offset, spans = F(), []
|
||||
for bar in snippet.bars:
|
||||
for event in bar.events:
|
||||
@@ -370,7 +375,7 @@ def contract_valid(draft, notes):
|
||||
if 'opening_contour' in transform.preserves and directions != transform.directions[:len(directions)]:
|
||||
return False, f'{transform.id}: identifying contour changed'
|
||||
for feature in draft.features:
|
||||
if generator.solo.references.feature_error(feature, local_sounding(notes)):
|
||||
if generator.solo.features.feature_error(feature, local_sounding(notes)):
|
||||
return False, f'{feature.id}: selected identifying relationship changed'
|
||||
for first, repeated in draft.correspondence:
|
||||
if first not in by_id or repeated not in by_id:
|
||||
@@ -389,7 +394,7 @@ def relationship_features(draft, notes):
|
||||
direction.extend(a != b for a, b in zip(actual, transform.directions))
|
||||
reference = draft.reference_motif if transform.operation in ('statement', 'contrast') else draft.home
|
||||
if reference and not draft.lead_identity and not draft.source_phrase and transform.operation not in ('close', 'fragment', 'broaden_ending'):
|
||||
development.append(generator.solo.references.relationship(group, reference))
|
||||
development.append(generator.solo.features.relationship(group, reference))
|
||||
joins = []
|
||||
for time in draft.joins:
|
||||
left = next((n for n in reversed(notes) if n.start < time), None)
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
"""Typed, target-driven harmonic expression over a strictly mapped theme."""
|
||||
from __future__ import annotations
|
||||
import dataclasses
|
||||
from fractions import Fraction as F
|
||||
from generator import harmony as chord_model, model, pitch as spelling, vocabulary
|
||||
from generator.solo import types, drafts, enclosures, placement, policy
|
||||
|
||||
|
||||
def quality(chord):
|
||||
degrees=chord_model.degrees(chord)
|
||||
if degrees.get(3)==0 and degrees.get(7)==-1:return 'dominant'
|
||||
if degrees.get(3)==-1:return 'half_diminished' if degrees.get(5)==-1 else 'minor'
|
||||
if degrees.get(3)==0 and degrees.get(5)==0:return 'major'
|
||||
return 'other'
|
||||
|
||||
|
||||
def degree_pitch(chord,degree,alter,near):
|
||||
root={**chord['root'],'octave':4}
|
||||
p=spelling.shift_pitch(root,degree-1,chord_model.degree_semitones(degree,alter))
|
||||
value=types.Pitch(p['letter'],model.alteration(p['alter']),p['octave'])
|
||||
return dataclasses.replace(value,octave=value.octave+round((near-value.number())/12))
|
||||
|
||||
|
||||
def chromatic(target,delta):
|
||||
# Chromatic neighbors are spelled relative to their exact destination.
|
||||
if delta in (-2,-1,1,2):
|
||||
step=1 if delta>0 else -1
|
||||
root={'letter':target.letter,'alter':model.rational(target.alter),'octave':target.octave}
|
||||
p=spelling.shift_pitch(root,step,delta)
|
||||
return types.Pitch(p['letter'],model.alteration(p['alter']),p['octave'])
|
||||
raise ValueError('unsupported approach interval')
|
||||
|
||||
|
||||
def eligible_color(rule,span,harmony):
|
||||
chord=chord_model.parse_symbol(span.chord)
|
||||
degrees=chord_model.degrees(chord)
|
||||
if quality(chord) not in rule['qualities']:return False
|
||||
degree=rule['degree']; simple=(degree-1)%7+1
|
||||
# Explicit alterations and omissions take precedence over optional colors.
|
||||
if any((d['value']-1)%7+1==simple and (d['type']=='subtract' or model.alteration(d['alter'])!=rule['alter']) for d in chord.get('degrees',[])):return False
|
||||
if degree in degrees and degrees[degree]!=rule['alter']:return False
|
||||
following=next((s for s in harmony.spans if s.start>=span.end and s.chord!=span.chord and s.scale),None)
|
||||
root=int(model.NATURAL[chord['root']['letter']]+model.alteration(chord['root']['alter']))%12
|
||||
next_chord=chord_model.parse_symbol(following.chord) if following else None
|
||||
next_root=int(model.NATURAL[next_chord['root']['letter']]+model.alteration(next_chord['root']['alter']))%12 if next_chord else None
|
||||
function=rule.get('function')
|
||||
if function=='resolving_dominant' and (next_root is None or (root-next_root)%12!=7):return False
|
||||
if function=='tonic_or_predominant':
|
||||
tonic=model.parse_pitch_name(span.scale.root)
|
||||
tonic_pc=int(model.NATURAL[tonic['letter']]+model.alteration(tonic['alter']))%12
|
||||
if root!=tonic_pc and not (next_chord and quality(next_chord)=='dominant' and (next_root-root)%12==5):return False
|
||||
return True
|
||||
|
||||
|
||||
def validate(notes,contracts,harmony):
|
||||
"""Return event permissions only after checking complete actual gestures."""
|
||||
actual={n.event_id:n for n in notes};approved={}
|
||||
def same(expected):
|
||||
n=actual.get(expected.event_id)
|
||||
return n and (n.start,n.end,n.pitch)==(expected.start,expected.end,expected.pitch)
|
||||
for c in contracts:
|
||||
if not c.events or not all(same(n) for n in (*c.events,c.target)):
|
||||
return {},c.id+': missing or changed expression event/target'
|
||||
if c.preparation and not same(c.preparation):return {},c.id+': changed preparation'
|
||||
if any(n.event_id in approved for n in c.events):return {},c.id+': overlapping expression permissions'
|
||||
target=c.target; span=harmony.at(c.boundary if c.kind=='anticipation' else target.start)
|
||||
if not span or not span.scale or span.chord!=c.chord:return {},c.id+': wrong target harmony'
|
||||
if c.kind in ('approach','enclosure','connection'):
|
||||
rule=next((r for r in vocabulary.data('lead_ins')['patterns'] if r['id']==c.rule and r['kind']==c.kind),None)
|
||||
if not rule or len(c.events)!=len(rule['semitones']):return {},c.id+': unknown approach formula'
|
||||
if target.pitch.number()%12 not in harmony.members(target.start,target.end) or target.pitch.number()%12 not in types.permitted(harmony.choices,target.start,target.end):return {},c.id+': invalid destination hold'
|
||||
gap=F(rule['gap']);cursor=target.start-gap-sum((F(d) for d in rule['durations']),F())
|
||||
if target.start-cursor>F(vocabulary.data('lead_ins')['max_approach']):return {},c.id+': approach too long'
|
||||
for n,step,duration in zip(c.events,rule['semitones'],rule['durations']):
|
||||
if n.start!=cursor or n.end!=cursor+F(duration) or n.pitch.number()!=target.pitch.number()+step:return {},c.id+': wrong register, interval, or timing'
|
||||
cursor=n.end
|
||||
if any(n.start<c.target.start and n.end>c.events[0].start and n.event_id not in {e.event_id for e in c.events} for n in notes):return {},c.id+': interrupted approach'
|
||||
if c.kind=='connection' and (not c.preparation or c.preparation.end!=c.events[0].start or abs(c.preparation.pitch.number()-c.events[0].pitch.number())>2):return {},c.id+': chromatic connection lacks its incoming motion'
|
||||
elif c.kind=='color':
|
||||
rule=next((r for r in vocabulary.data('harmony')['colors'] if r['id']==c.rule),None)
|
||||
if not rule:return {},c.id+': unknown color policy'
|
||||
for n in c.events:
|
||||
if n.end-n.start>F(rule['max_beats']):return {},c.id+': excessive color duration'
|
||||
for region,a,b in harmony.segments(n):
|
||||
if not region.scale or not eligible_color(rule,region,harmony):return {},c.id+': incompatible color harmony/function'
|
||||
p=degree_pitch(chord_model.parse_symbol(region.chord),rule['degree'],rule['alter'],n.pitch.number())
|
||||
if p!=n.pitch:return {},c.id+': color degree/spelling mismatch'
|
||||
if not rule['outside'] and n.pitch.number()%12 not in region.scale.pitches:return {},c.id+': color requires mapped collection'
|
||||
last=c.events[-1]
|
||||
if rule.get('resolve'):
|
||||
if target is last or not 0<=target.start-last.end<=F(vocabulary.data('harmony')['resolution_gap']) or not 0<abs(target.pitch.number()-last.pitch.number())<=2 or target.pitch.number()%12 not in harmony.members(target.start,target.end):return {},c.id+': unresolved color'
|
||||
elif c.kind=='anticipation':
|
||||
if c.rule!='early_chord_tone' or len(c.events)!=1 or c.target!=c.events[0] or span.start!=c.boundary:
|
||||
return {},c.id+': unknown anticipation or non-boundary target'
|
||||
n,=c.events
|
||||
if c.boundary is None or not n.start<c.boundary<n.end or c.boundary-n.start>F(vocabulary.data('harmony')['anticipation_max']):return {},c.id+': invalid anticipation interval'
|
||||
if n.pitch.number()%12 not in harmony.members(c.boundary,n.end) or n.pitch.number()%12 not in types.permitted(harmony.choices,c.boundary,n.end):return {},c.id+': invalid future destination'
|
||||
elif c.kind=='blues':
|
||||
if len(c.events)!=1:return {},c.id+': blues treatment requires one inflected event'
|
||||
rule=next((r for r in vocabulary.data('lead_ins')['blues'] if r['id']==c.rule),None)
|
||||
n,=c.events
|
||||
chord=chord_model.parse_symbol(span.chord)
|
||||
if not rule or n.end-n.start>F(1,2) or n.end!=target.start or target.pitch.number()%12 not in span.roles.basic:return {},c.id+': unresolved blues inflection'
|
||||
expected=degree_pitch(chord,rule['degree'],rule['alter'],n.pitch.number())
|
||||
resolved=degree_pitch(chord,rule['target_degree'],0,target.pitch.number())
|
||||
if n.pitch!=expected or target.pitch!=resolved or target.pitch.number()-n.pitch.number()!=1:return {},c.id+': unsupported blues destination'
|
||||
if not c.preparation or c.preparation.end!=n.start or abs(c.preparation.pitch.number()-n.pitch.number())>2:return {},c.id+': unprepared blues gesture'
|
||||
else:return {},c.id+': unknown expression kind'
|
||||
for n in c.events:
|
||||
covered=n.start
|
||||
for region,a,b in harmony.segments(n):
|
||||
if a!=covered or not region.scale:return {},c.id+': missing harmony'
|
||||
covered=b
|
||||
if covered!=n.end:return {},c.id+': incomplete harmonic coverage'
|
||||
approved[n.event_id]=c
|
||||
return approved,''
|
||||
|
||||
|
||||
def proposals(phrase,harmony):
|
||||
draft=phrase.draft
|
||||
if not draft or any(n.continuation for n in draft.notes):return
|
||||
notes=types.sounding_notes(draft.notes)
|
||||
for target,role in enclosures.declared_targets(phrase,harmony):
|
||||
for rule in vocabulary.data('lead_ins')['patterns']:
|
||||
start=target.start-F(rule['gap'])-sum((F(d) for d in rule['durations']),F())
|
||||
if start<phrase.plan.start or not enclosures.editable(draft,start,target.start):continue
|
||||
cursor=start;events=[]
|
||||
for i,(step,duration) in enumerate(zip(rule['semitones'],rule['durations'])):
|
||||
events.append(types.Note(cursor,cursor+F(duration),chromatic(target.pitch,step),target.sources,rule['kind'],event_id=target.event_id+':'+rule['id']+':'+str(i)))
|
||||
cursor+=F(duration)
|
||||
actual=enclosures.replace_window(notes,start,target.start,tuple(events))
|
||||
if actual is None:continue
|
||||
prior=next((n for n in reversed(actual) if n.end<=start),None)
|
||||
contract=types.ExpressionContract(target.event_id+':'+rule['id'],rule['kind'],rule['id'],tuple(events),target,harmony.at(target.start).chord,preparation=prior)
|
||||
yield actual,contract
|
||||
for i,n in enumerate(notes[:-1]):
|
||||
target=notes[i+1]
|
||||
if not enclosures.editable(draft,n.start,n.end):continue
|
||||
span=harmony.at(n.start)
|
||||
if not span or not span.scale:continue
|
||||
for rule in vocabulary.data('harmony')['colors']:
|
||||
if not eligible_color(rule,span,harmony) or n.end-n.start>F(rule['max_beats']):continue
|
||||
p=degree_pitch(chord_model.parse_symbol(span.chord),rule['degree'],rule['alter'],n.pitch.number())
|
||||
if abs(p.number()-n.pitch.number())>4 or p==n.pitch:continue
|
||||
color=dataclasses.replace(n,pitch=p,role='color')
|
||||
resolution=target if rule.get('resolve') else color
|
||||
contract=types.ExpressionContract(n.event_id+':'+rule['id'],'color',rule['id'],(color,),resolution,harmony.at(resolution.start).chord)
|
||||
yield (*notes[:i],color,*notes[i+1:]),contract
|
||||
for rule in vocabulary.data('lead_ins')['blues']:
|
||||
if i==0 or n.end-n.start>F(1,2):continue
|
||||
p=degree_pitch(chord_model.parse_symbol(span.chord),rule['degree'],rule['alter'],n.pitch.number())
|
||||
changed=dataclasses.replace(n,pitch=p,role='blues')
|
||||
yield (*notes[:i],changed,*notes[i+1:]),types.ExpressionContract(n.event_id+':'+rule['id'],'blues',rule['id'],(changed,),target,harmony.at(target.start).chord,preparation=notes[i-1])
|
||||
for i,target in enumerate(notes):
|
||||
if i==0 or target.start not in {s.start for s in harmony.spans}:continue
|
||||
start=target.start-F(1,2)
|
||||
if not enclosures.editable(draft,start,target.start):continue
|
||||
n=dataclasses.replace(target,start=start,role='anticipation')
|
||||
actual=enclosures.replace_window(notes,start,target.start,())
|
||||
if actual:
|
||||
actual=tuple(n if a.event_id==target.event_id else a for a in actual)
|
||||
yield actual,types.ExpressionContract(target.event_id+':anticipate','anticipation','early_chord_tone',(n,),n,harmony.at(target.start).chord,boundary=target.start)
|
||||
|
||||
|
||||
def refine(solo,enabled=True):
|
||||
from generator.solo import harmonic
|
||||
harmony=harmonic.Harmony(solo.choices,solo.profile);result=[];history=[];attempts=0
|
||||
seed=vocabulary.role(solo.part+'_solo').get('choice_seed',solo.part)
|
||||
for phrase in solo.phrases:
|
||||
draft=dataclasses.replace(phrase.draft,notes=tuple(n for d in phrase.decisions for n in d.notes))
|
||||
phrase=dataclasses.replace(phrase,draft=draft,before_enclosure=draft)
|
||||
options=[(0.,phrase,None)]
|
||||
if enabled:
|
||||
original=draft.notes
|
||||
for actual,c in proposals(phrase,harmony):
|
||||
attempts+=1
|
||||
if not drafts.contract_valid(draft,actual)[0]:continue
|
||||
if not harmony.evaluate(actual,until=phrase.plan.end,complete=True,expressions=(c,)).valid:continue
|
||||
if not harmonic.anchors_valid(phrase.anchors,actual,phrase.plan.end,complete=True,closure=phrase.plan.closure)[0]:continue
|
||||
placed=placement.place(actual,solo.tempo,profile=solo.profile)
|
||||
if not placed:continue
|
||||
# Context, repetition, and motion determine utility; no chromatic count target.
|
||||
prior=next((n for n in reversed(actual) if n.end<=c.events[0].start),None)
|
||||
join=abs(prior.pitch.number()-c.events[0].pitch.number()) if prior else 0
|
||||
duplicated=bool(prior and prior.pitch==c.events[0].pitch)
|
||||
destination=c.target.pitch.number()
|
||||
old_approach=next((n for n in reversed(original) if n.end<=c.target.start),None)
|
||||
prepares=bool(old_approach and abs(old_approach.pitch.number()-destination)>2 and join<=3)
|
||||
recent=sum(kind==c.kind for kind in history[-2:])
|
||||
cost=.08*max(0,join-3)+.18*duplicated+.12*recent+.3*(enclosures.movement(actual)-enclosures.movement(original))-.12*prepares
|
||||
if c.kind=='anticipation':cost-=.05
|
||||
decisions=tuple(dataclasses.replace(d,notes=tuple(n for n in actual if d.start<=n.start<d.end)) for d in phrase.decisions)
|
||||
if tuple(n for d in decisions for n in d.notes)!=actual:continue
|
||||
# Rebase editable development spans onto the completed gesture. The
|
||||
# feature contract is unchanged, but stale event IDs/rhythm would make
|
||||
# reports falsely describe notes that the expression replaced.
|
||||
transforms=tuple(dataclasses.replace(t,
|
||||
events=tuple(n.event_id for n in actual if t.start<=n.start<t.end),
|
||||
rhythm=tuple((n.start-t.start,n.end-t.start) for n in actual if t.start<=n.start<t.end))
|
||||
for t in draft.transformations)
|
||||
changed=dataclasses.replace(draft,notes=actual,expressions=(c,),transformations=transforms)
|
||||
routes=tuple(r for r in phrase.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 actual) for n in r.notes))
|
||||
options.append((cost,dataclasses.replace(phrase,draft=changed,decisions=decisions,routes=routes),c))
|
||||
_,chosen,c=min(options,key=lambda v:(round(v[0],8),policy.stable_rank(seed,(phrase.plan.id,v[2]))))
|
||||
if c:history.append(c.kind)
|
||||
result.append(chosen)
|
||||
raw=tuple(n for p in result for d in p.decisions for n in d.notes)
|
||||
placed=placement.place(raw,solo.tempo,profile=solo.profile)
|
||||
if raw and placed is None:raise ValueError('Expression realization has no complete guitar path')
|
||||
by_id={n.event_id:n for n in placed[1]} if placed else {}
|
||||
final=[]
|
||||
for p in result:
|
||||
decisions=tuple(dataclasses.replace(d,notes=tuple(by_id[n.event_id] for n in d.notes)) for d in p.decisions)
|
||||
notes=tuple(n for d in decisions for n in d.notes)
|
||||
state=dataclasses.replace(p.state,notes=notes,last=notes[-1] if notes else p.state.last) if p.state else None
|
||||
# A legal timing revision preserves the complete route with its new times.
|
||||
routes=[]
|
||||
for route in p.routes:
|
||||
if all(n.event_id in by_id and by_id[n.event_id].pitch==n.pitch for n in route.notes):
|
||||
route_notes=tuple(by_id[n.event_id] for n in route.notes)
|
||||
routes.append(dataclasses.replace(route,notes=route_notes,destination=route_notes[-1]))
|
||||
final.append(dataclasses.replace(p,decisions=decisions,draft=dataclasses.replace(p.draft,notes=notes),state=state,routes=tuple(routes)))
|
||||
return dataclasses.replace(solo,phrases=tuple(final),diagnostics={**solo.diagnostics,'expression_candidates':attempts,'expressions_selected':history,'enclosures_enabled':enabled})
|
||||
@@ -0,0 +1,56 @@
|
||||
"""Pure musical contracts; no reference files or filesystem access."""
|
||||
from __future__ import annotations
|
||||
import dataclasses
|
||||
import fractions
|
||||
import generator.solo.types as types
|
||||
F = fractions.Fraction
|
||||
|
||||
def identifying_features(notes, source):
|
||||
"""Select a characteristic leap followed by return, wherever it occurs."""
|
||||
notes = tuple(notes)
|
||||
candidates = []
|
||||
for i in range(len(notes) - 2):
|
||||
a, b, c = notes[i:i + 3]
|
||||
leap, reply = b.pitch.number() - a.pitch.number(), c.pitch.number() - b.pitch.number()
|
||||
if 3 <= abs(leap) <= 9 and leap * reply < 0 and c.start - a.start <= 4:
|
||||
group = notes[i:min(len(notes), i + 4)]
|
||||
rhythm = tuple((n.start - a.start, n.end - n.start) for n in group)
|
||||
intervals = tuple(y.pitch.number() - x.pitch.number() for x, y in zip(group, group[1:]))
|
||||
candidates.append(types.IdentifyingFeature(a.event_id + '_identity', tuple(n.event_id for n in group), rhythm, intervals, source))
|
||||
return tuple(candidates[:1])
|
||||
|
||||
def feature_error(feature, notes):
|
||||
by_id = {n.event_id: n for n in notes}
|
||||
actual = [by_id.get(identity) for identity in feature.events]
|
||||
if not actual or not all(actual):
|
||||
return 1.0
|
||||
first = actual[0].start
|
||||
rhythm = tuple((n.start - first, n.end - n.start) for n in actual)
|
||||
movement = tuple(b.pitch.number() - a.pitch.number() for a, b in zip(actual, actual[1:]))
|
||||
errors = []
|
||||
if 'rhythm' in feature.preserves:
|
||||
errors.append(1.0 if len(rhythm) != len(feature.rhythm) else sum(a != b for a,b in zip(rhythm,feature.rhythm))/max(1,len(rhythm)))
|
||||
if 'intervals' in feature.preserves or 'contour' in feature.preserves:
|
||||
if len(movement) != len(feature.intervals) or not movement and feature.intervals:
|
||||
errors.append(1.0)
|
||||
else:
|
||||
errors.append(sum(((a>0)-(a<0)) != ((b>0)-(b<0)) or 'intervals' in feature.preserves and abs(a-b)>2 for a,b in zip(movement,feature.intervals))/max(1,len(movement)))
|
||||
if 'exact_intervals' in feature.preserves:
|
||||
errors.append(float(movement != feature.intervals))
|
||||
if 'pitches' in feature.preserves:
|
||||
errors.append(float(tuple(n.pitch.number() for n in actual) != feature.pitches))
|
||||
return max(errors,default=0.0)
|
||||
|
||||
def relationship(notes, reference):
|
||||
"""One joint relationship cost: timed movement, not absolute-pitch copying."""
|
||||
notes = tuple(notes)
|
||||
if len(notes) < 2 or len(reference.notes) < 2:
|
||||
return 0.0 if len(notes) == len(reference.notes) == 1 else 1.0
|
||||
actual = tuple(b.pitch.number() - a.pitch.number() for a, b in zip(notes, notes[1:]))
|
||||
expected = reference.contour()
|
||||
count = min(len(actual), len(expected))
|
||||
contour = sum(0.75 * (((a > 0) - (a < 0)) != ((b > 0) - (b < 0))) + 0.25 * min(1, max(0, abs(a - b) - 2) / 7) for a, b in zip(actual, expected)) / max(1, count)
|
||||
start, ref_start = notes[0].start, reference.notes[0].start
|
||||
rhythm = sum(min(1, float(abs((n.start - start) - (r.start - ref_start))) / 2) for n, r in zip(notes, reference.notes)) / min(len(notes), len(reference.notes))
|
||||
evidence = max(0, min(3, len(reference.notes)) - len(notes)) / 3
|
||||
return max(evidence, (contour + rhythm) / 2)
|
||||
@@ -148,8 +148,12 @@ class Harmony:
|
||||
intervals.append((upper.start,target.start))
|
||||
return accepted, ''
|
||||
|
||||
def evaluate(self, notes, treatments=(), *, until=None, complete=False, enclosures=()):
|
||||
def evaluate(self, notes, treatments=(), *, until=None, complete=False, enclosures=(), expressions=()):
|
||||
notes = generator.solo.types.sounding_notes(notes)
|
||||
from generator.solo.expressions import validate
|
||||
permissions, expression_error = validate(notes, expressions, self)
|
||||
if expression_error:
|
||||
return Result(False, 0, error=expression_error)
|
||||
until = until if until is not None else max((n.end for n in notes), default=fractions.Fraction())
|
||||
ornaments, error = self.enclosure_events(notes, enclosures, until) if enclosures else ({}, '')
|
||||
if error:
|
||||
@@ -190,13 +194,14 @@ class Harmony:
|
||||
for span, start, end in self.segments(note):
|
||||
contract = next((t for t in valid_contracts if t.pitch == number and t.start <= start and t.end >= end), None)
|
||||
enclosure = ornaments.get(note.event_id)
|
||||
exception = enclosure is not None and note.event_id == enclosure.lower.event_id
|
||||
expression = permissions.get(note.event_id)
|
||||
exception = enclosure is not None and note.event_id == enclosure.lower.event_id or expression is not None and (expression.kind != 'anticipation' or end <= expression.boundary)
|
||||
if start > covered or not span.scale or number % 12 not in span.scale.pitches and not exception:
|
||||
return Result(False, 0, error=f'{note.pitch.label()} at {start}-{end}: outside mapped collection or missing harmony')
|
||||
covered = end
|
||||
function = self.function(number, span)
|
||||
brief_enclosure = enclosure and (exception or enclosure.approach_start is None or enclosure.approach_start == note.start)
|
||||
if function == 'basic' or note.role == 'quote' or brief_enclosure:
|
||||
if function == 'basic' or note.role == 'quote' or brief_enclosure or expression:
|
||||
continue
|
||||
if duration >= generator.solo.policy.DEFAULT.long_note and contract is None:
|
||||
return Result(False, 0, error=f'{note.pitch.label()} at {start}-{end} over {span.chord}: sustained {function} without a planned treatment')
|
||||
@@ -206,8 +211,12 @@ class Harmony:
|
||||
if covered < note.end:
|
||||
return Result(False, 0, error=f'{note.pitch.label()} at {covered}: missing harmonic coverage')
|
||||
return Result(True, exposed / max(sounding, 0.001), tuple(pending))
|
||||
def describe(self, notes, treatments=(), enclosures=()):
|
||||
def describe(self, notes, treatments=(), enclosures=(), expressions=()):
|
||||
notes = generator.solo.types.sounding_notes(notes)
|
||||
from generator.solo.expressions import validate
|
||||
permissions, error = validate(notes, expressions, self)
|
||||
if error:
|
||||
raise ValueError(error)
|
||||
ornaments, error = self.enclosure_events(notes,enclosures) if enclosures else ({}, '')
|
||||
if error:
|
||||
raise ValueError(error)
|
||||
@@ -215,7 +224,8 @@ class Harmony:
|
||||
for index, note in enumerate(notes):
|
||||
for span, start, end in self.segments(note):
|
||||
contracts = [t for t in treatments if t.pitch == note.pitch.number() and t.start <= start and t.end >= end]
|
||||
result.append({'start': str(start), 'end': str(end), 'attack': str(note.start), 'sounding_duration': str(note.end - note.start), 'pitch': note.pitch.label(), 'chord': span.chord, 'function': self.function(note.pitch.number(), span), 'motion': self.connected_motion(notes, index), 'enclosure': ornaments[note.event_id].id if note.event_id in ornaments else '', 'scale_exception': note.event_id in ornaments and self.function(note.pitch.number(), span) == 'outside_collection', 'protected': note.role == 'quote', 'treatments': [t.id for t in contracts]})
|
||||
expression = permissions.get(note.event_id)
|
||||
result.append({'start': str(start), 'end': str(end), 'attack': str(note.start), 'sounding_duration': str(note.end - note.start), 'pitch': note.pitch.label(), 'chord': span.chord, 'function': self.function(note.pitch.number(), span), 'motion': self.connected_motion(notes, index), 'enclosure': ornaments[note.event_id].id if note.event_id in ornaments else '', 'expression': expression.kind if expression else '', 'expression_rule': expression.rule if expression else '', 'scale_exception': bool(note.event_id in ornaments or expression) and self.function(note.pitch.number(), span) == 'outside_collection', 'protected': note.role == 'quote', 'treatments': [t.id for t in contracts]})
|
||||
return result
|
||||
|
||||
def anchor_note(anchor, notes, *, until=None):
|
||||
|
||||
@@ -45,7 +45,7 @@ def document_for(template, notes, tempo, identifier):
|
||||
def controls(song, libraries):
|
||||
"""Literal reference injection is restricted to this diagnostic, never production."""
|
||||
_, scales, sequences = libraries
|
||||
reference, = references.load_library(pathlib.Path('data/lead_phrases'))
|
||||
reference, = references.load_library(pathlib.Path('audition/controls'))
|
||||
solo = song['solos']['acoustic']
|
||||
measures = song['parts']['acoustic']['staves'][0]['measures'][solo['first']:solo['first'] + solo['count']]
|
||||
choices = generator.scales.selection.select(generator.scales.timeline.read({'measures': measures}), generator.scales.timeline.Solo(F(), F(solo['length']), 'control'), scales)
|
||||
|
||||
+12
-9
@@ -7,10 +7,12 @@ from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import fractions
|
||||
import functools
|
||||
import generator.vocabulary
|
||||
|
||||
import generator.model
|
||||
import generator.fretboard
|
||||
from generator.solo import types, harmonic, policy, placement, routes, sequences, drafts, references, profiles
|
||||
from generator.solo import types, harmonic, policy, placement, routes, sequences, drafts, features as references, profiles
|
||||
|
||||
F = fractions.Fraction
|
||||
|
||||
@@ -133,10 +135,10 @@ def rhythm_catalog(library, meter):
|
||||
return drafts.catalog(library,meter)
|
||||
|
||||
|
||||
def connection_patterns(start,end,entry,destination,rhythms,meter):
|
||||
length=end-start
|
||||
@functools.lru_cache(maxsize=256)
|
||||
def connection_windows(catalog,length):
|
||||
spans={( (F(),length), ):'held'}
|
||||
for identifier,pattern,total in rhythm_catalog(rhythms,meter):
|
||||
for identifier,pattern,total in catalog:
|
||||
if total < length:
|
||||
continue
|
||||
# Retrieve every exact-duration window, including offbeat and cross-bar windows.
|
||||
@@ -151,6 +153,10 @@ def connection_patterns(start,end,entry,destination,rhythms,meter):
|
||||
ranked=sequences.diverse(tuple(spans.items()),
|
||||
lambda item:(abs(len(item[0])-float(length)*1.5),item[1]),
|
||||
lambda item:(len(item[0]),any(a[1]!=b[0] for a,b in zip(item[0],item[0][1:]))),8)
|
||||
return ranked
|
||||
|
||||
def connection_patterns(start,end,entry,destination,rhythms,meter):
|
||||
ranked=connection_windows(tuple(rhythm_catalog(rhythms,meter)),end-start)
|
||||
result=[]
|
||||
for pattern,identifier in ranked:
|
||||
events=tuple(dataclasses.replace(entry,start=start+a,end=start+b,role='connection',
|
||||
@@ -323,9 +329,10 @@ def transition(first,second):
|
||||
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.tempo=analysis.tempo
|
||||
context.resources[id(rhythms)]=rhythms
|
||||
harmony=harmonic.Harmony(choices, profiles.for_part(part,analysis))
|
||||
seed=policy.stable_rank('source_variations',(analysis.melody,choices,policy.SEED_NAMESPACE))
|
||||
seed=policy.stable_rank('source_variations',(part,analysis.melody,choices,generator.vocabulary.role(part+'_solo'),policy.DEFAULT.composition_version))
|
||||
states=[(0.,())]
|
||||
failures=[]
|
||||
for _ in range(2*len(measures)+1):
|
||||
@@ -372,10 +379,6 @@ def thematic_options(analysis,measures,part,end,choices,library,rhythms,context,
|
||||
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)
|
||||
|
||||
+20
-12
@@ -60,20 +60,28 @@ def measures(sources: list[dict], notes: tuple[generator.solo.types.Note, ...],
|
||||
return result
|
||||
|
||||
def tuplets(measures):
|
||||
spans = []
|
||||
for measure in measures:
|
||||
for ratio, values in itertools.groupby(generator.score.measure_notes(measure), key=lambda n: generator.model.fraction(n['written'].get('ratio', generator.model.rational(1)))):
|
||||
notes = list(values)
|
||||
if ratio == 1:
|
||||
continue
|
||||
for first in range(0, len(notes), ratio.denominator):
|
||||
members = notes[first:first + ratio.denominator]
|
||||
identifier = members[0]['id'] + '_tuplet'
|
||||
spans.append({'id': identifier, 'voice': 'voice_arranged', 'start': members[0]['id'], 'end': members[-1]['id'], 'note_count': ratio.denominator, 'space_count': ratio.numerator, 'depth': 0, 'bracket': 'auto', 'show_number': 'actual'})
|
||||
for note in members:
|
||||
note['tuplet_ids'] = [identifier]
|
||||
# Group by sounding duration, including notation fragments across barlines.
|
||||
spans=[]
|
||||
events=[n for m in measures for n in generator.score.measure_notes(m)]
|
||||
for ratio,values in itertools.groupby(events,key=lambda n:generator.model.fraction(n['written'].get('ratio',generator.model.rational(1)))):
|
||||
if ratio==1:continue
|
||||
members=[];elapsed=fractions.Fraction();length=None
|
||||
for note in values:
|
||||
if not members:
|
||||
length=generator.model.TYPE_LENGTHS[note['written']['type']]*ratio.numerator
|
||||
members.append(note);elapsed+=generator.model.fraction(note['duration'])
|
||||
if elapsed>=length:
|
||||
identifier=members[0]['id']+'_tuplet'
|
||||
spans.append({'id':identifier,'voice':'voice_arranged','start':members[0]['id'],'end':members[-1]['id'],'note_count':ratio.denominator,'space_count':ratio.numerator,'depth':0,'bracket':'auto','show_number':'actual'})
|
||||
for n in members:n['tuplet_ids']=[identifier]
|
||||
members=[];elapsed=fractions.Fraction()
|
||||
if members:
|
||||
identifier=members[0]['id']+'_tuplet'
|
||||
spans.append({'id':identifier,'voice':'voice_arranged','start':members[0]['id'],'end':members[-1]['id'],'note_count':ratio.denominator,'space_count':ratio.numerator,'depth':0,'bracket':'auto','show_number':'actual'})
|
||||
for n in members:n['tuplet_ids']=[identifier]
|
||||
return spans
|
||||
|
||||
|
||||
def replace_region(document: dict, first: int, replacement: list[dict]) -> None:
|
||||
staff = document['staves'][0]
|
||||
replacement = copy.deepcopy(replacement)
|
||||
|
||||
+18
-17
@@ -7,14 +7,15 @@ from fractions import Fraction as F
|
||||
|
||||
import generator.model
|
||||
import generator.harmony
|
||||
import generator.vocabulary
|
||||
from generator.solo import types, policy, sequences, profiles
|
||||
|
||||
|
||||
def heard(notes, operation, anchor=None):
|
||||
def heard(notes, operation, anchor=None, bar=F(4)):
|
||||
notes = types.sounding_notes(notes)
|
||||
first = notes[0].start
|
||||
events = tuple((n.start-first, n.end-n.start, n.pitch.number()) for n in notes)
|
||||
ending = (anchor.pitch.number()%12, anchor.earliest%4, anchor.duration) if anchor and anchor.role == 'terminal' else ()
|
||||
ending = (anchor.pitch.number()%12, anchor.earliest%bar, anchor.duration) if anchor and anchor.role == 'terminal' else ()
|
||||
return types.HeardFigure(first, notes[-1].end, events, operation, ending)
|
||||
|
||||
|
||||
@@ -56,7 +57,10 @@ def register_goal(incoming, part, fallback, profile=None):
|
||||
if profile and profile.reference is not None:
|
||||
recent = [p for h in incoming.history[-2:] for _,_,p in h.events]
|
||||
center = round(sum(recent)/len(recent)) if recent else profile.reference
|
||||
return max(profile.reference-4,min(profile.reference+4,center))
|
||||
trajectory=generator.vocabulary.role(part+'_solo').get('register_motion',[0,4,-3,0])
|
||||
phase=min(len(trajectory)-1,incoming.closures%len(trajectory))
|
||||
goal=profile.reference+trajectory[phase]
|
||||
return max(profile.register[0]+3,min(profile.register[1]-3,round((center+goal)/2)))
|
||||
recent = [p for h in incoming.history[-2:] for _,_,p in h.events]
|
||||
if not recent:
|
||||
return fallback
|
||||
@@ -72,9 +76,7 @@ def register_goal(incoming, part, fallback, profile=None):
|
||||
def endpoint_cost(finish, start, end, count, harmony, bar):
|
||||
length = float((finish-start)/bar)
|
||||
remaining = float((end-finish)/bar)
|
||||
projected = count+1+remaining/(32/policy.DEFAULT.settling_target)
|
||||
low, high = policy.DEFAULT.settling_range
|
||||
pacing = .2*max(0,low-projected,projected-high)+.025*abs(projected-policy.DEFAULT.settling_target)
|
||||
pacing = 0.
|
||||
span = harmony.at(finish-F(1,100))
|
||||
# A dominant may close an open question, but a nearby resolution is preferred.
|
||||
degrees=generator.harmony.degrees(generator.harmony.parse_symbol(span.chord)) if span and span.scale else {}
|
||||
@@ -94,7 +96,8 @@ def window_options(analysis, measures, start, end, harmony, incoming, *, limit=2
|
||||
ends=(min(end,current.end if current else bounds[index+1]),)
|
||||
else:
|
||||
barrier=min((s.start for s in harmony.spans if not s.scale and s.start>start),default=end)
|
||||
ends=tuple(sorted({min(end,barrier,bounds[min(index+n,len(measures))]) for n in (4,5,6,8)}))
|
||||
authored=[n.end-analysis.body_offset for n in analysis.melody if n.end-n.start>=2]
|
||||
ends=tuple(sorted({min(end,barrier,bounds[min(index+n,len(measures))]) for n in (3,4,5,6,8)} | {t for t in authored if start+3*bar<=t<=min(end,barrier,start+8*bar)}))
|
||||
if barrier>=end:
|
||||
# The returning pickup may shorten the last measure. Do not strand a
|
||||
# one-beat "new phrase" after an otherwise completed sustained close.
|
||||
@@ -116,9 +119,9 @@ def endings(source, incoming, part, harmony, bar, *, limit=8, final=False):
|
||||
goal=register_goal(incoming,part,base,harmony.profile)
|
||||
values=[]
|
||||
duration=source.end-source.start
|
||||
holds=(F(2),F(5,2),F(3),F(7,2),F(4)) if duration>=4 else (min(F(2),duration/2),)
|
||||
holds=tuple(F(v) for v in generator.vocabulary.data('grammar')['ending_holds']) if duration>=4 else (min(F(2),duration/2),)
|
||||
for hold in holds:
|
||||
for release in (F(),F(1,2),F(1),F(2)):
|
||||
for release in map(F,generator.vocabulary.data('grammar')['ending_releases']):
|
||||
arrival=source.end-release-hold
|
||||
if arrival-source.start<F(3,2):continue
|
||||
for pitch in available(harmony,arrival,arrival+hold,members=True):
|
||||
@@ -126,8 +129,8 @@ def endings(source, incoming, part, harmony, bar, *, limit=8, final=False):
|
||||
anchor=types.Anchor(f'{source.id}:close:{arrival}:{pitch.number()}',arrival,arrival,pitch,
|
||||
harmony.members(arrival,arrival+hold),harmony.profile.register,hold,'terminal',harmony.at(arrival).chord,
|
||||
hold_until=arrival+hold,effect=ending_effect(arrival,harmony,final))
|
||||
cadence=sum(h.ending==(pitch.number()%12,arrival%4,hold) for h in incoming.history)
|
||||
rank=harmony.profile.cost(pitch.number())+destination_cost(pitch,arrival,arrival+hold,harmony,incoming,goal,True)+.16*min(2,cadence)+.035*abs(float(hold)-3)+.015*float(release)
|
||||
cadence=sum(h.ending==(pitch.number()%12,arrival%bar,hold) for h in incoming.history)
|
||||
rank=harmony.profile.cost(pitch.number())+destination_cost(pitch,arrival,arrival+hold,harmony,incoming,goal,True)+.16*min(2,cadence)+.035*abs(float(hold)-3)
|
||||
values.append((rank,anchor))
|
||||
selected=destination_options(values,lambda x:(x[0],policy.stable_rank(source.id,x[1])),
|
||||
lambda x:x[1].pitch,lambda x:x[1].earliest,harmony,limit)
|
||||
@@ -135,12 +138,10 @@ def endings(source, incoming, part, harmony, bar, *, limit=8, final=False):
|
||||
|
||||
|
||||
def appearance_cost(operation, incoming, start, end):
|
||||
expected=policy.DEFAULT.return_target*float(start/max(end,F(1)))
|
||||
actual=len(incoming.returns)
|
||||
gap=start-(incoming.returns[-1] if incoming.returns else F())
|
||||
if operation=='return':
|
||||
gap=start-(incoming.returns[-1] if incoming.returns else F())
|
||||
return .65*max(0,actual+1-expected)+.5*max(0,1-float(gap)/24)
|
||||
return .45*max(0,expected-actual)
|
||||
return .5*max(0,1-float(gap)/16)
|
||||
return .1*min(1,max(0,float(gap)/32-1))
|
||||
|
||||
|
||||
def activity(source, ending):
|
||||
@@ -162,7 +163,7 @@ def evaluate(notes, incoming, harmony, start, end, bar):
|
||||
entry=max(0,abs(notes[0].pitch.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(notes,notes[1:]))/max(1,len(notes)-1)
|
||||
history_cost=repetition(heard(notes,'develop'),incoming.history,bar)
|
||||
return harmony.profile.cost(profiles.weighted_center(notes))+2*checked.exposure+entry+leaps+.35*max(0,4-density,density-6)+.8*history_cost
|
||||
return harmony.profile.cost(profiles.weighted_center(notes))+2*checked.exposure+entry+leaps+.8*history_cost
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=512)
|
||||
|
||||
@@ -45,14 +45,12 @@ class Policy:
|
||||
theme_continuations: int = 8
|
||||
theme_route_jobs: int = 16
|
||||
settling_range: tuple[int, int] = (5, 7)
|
||||
settling_target: int = 6
|
||||
return_target: int = 2
|
||||
history_bars: int = 16
|
||||
enclosure_targets: int = 4
|
||||
enclosure_routes: int = 4
|
||||
enclosure_frontier: int = 8
|
||||
enclosure_max_per_32_bars: int = 4
|
||||
composition_version: str = 'six-parts-register-signatures-1'
|
||||
composition_version: str = 'independent-roles-1'
|
||||
|
||||
# Preserve the former default root seeds while decoupling them from policy edits.
|
||||
SEED_NAMESPACE = 'a85abb240d7cfa9d054375b654bcb5c23c746201fd4aed7acf81cb3f4ee720e1'
|
||||
|
||||
@@ -5,6 +5,7 @@ import dataclasses
|
||||
from fractions import Fraction as F
|
||||
|
||||
import generator.fretboard
|
||||
import generator.vocabulary
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
@@ -23,7 +24,7 @@ class Instrument:
|
||||
return any(p.string_number in self.strings for p in generator.fretboard.single_positions_for_pitch(F(number)))
|
||||
|
||||
def cost(self, number):
|
||||
return min(1., max(0, abs(number - self.reference) - 4) / 12) if self.reference is not None else 0.
|
||||
return .5 * min(1., max(0, abs(number - self.reference) - 7) / 12) if self.reference is not None else 0.
|
||||
|
||||
|
||||
UNRESTRICTED = Instrument()
|
||||
@@ -43,13 +44,13 @@ def weighted_center(notes):
|
||||
def for_part(part, analysis=None):
|
||||
if part not in ('acoustic', 'electric'):
|
||||
raise ValueError('unknown solo instrument: ' + part)
|
||||
strings = (2, 3, 4, 5, 6) if part == 'acoustic' else (1, 2, 3, 4)
|
||||
strings = (1, 2, 3, 4, 5, 6)
|
||||
if analysis is None or not analysis.melody:
|
||||
return Instrument(part, strings)
|
||||
# Analysis contains the prepared concert source. Electric melody is written
|
||||
# two octaves up and sounds one down (parts.build_measure), hence +12.
|
||||
# Use the source voice, never the highest added harmonization voice.
|
||||
shift = 12 if part == 'electric' else 0
|
||||
shift = generator.vocabulary.role(part + '_solo').get('register_offset', 12 if part == 'electric' else 0)
|
||||
notes = tuple(n for n in analysis.melody if n.end > analysis.body_offset)
|
||||
return Instrument(part, strings, weighted_center(notes) + shift,
|
||||
(min(n.pitch.number() for n in notes) + shift, max(n.pitch.number() for n in notes) + shift))
|
||||
|
||||
@@ -6,7 +6,7 @@ import functools
|
||||
import time
|
||||
|
||||
import generator.solo.drafts
|
||||
import generator.solo.references
|
||||
import generator.solo.features
|
||||
import generator.solo.routes
|
||||
|
||||
import generator.fretboard
|
||||
@@ -700,7 +700,7 @@ def refine(phrase, pool, incoming, harmony, sequences, tempo, context, search=Tr
|
||||
notes = emitted_notes(phrase)
|
||||
draft = rebase_draft(phrase.draft, notes, harmony)
|
||||
# Only now commit a selected, inspectable relationship for elaboration.
|
||||
features = generator.solo.references.identifying_features(notes, draft.reference)
|
||||
features = generator.solo.features.identifying_features(notes, draft.reference)
|
||||
draft = dataclasses.replace(draft, features=features)
|
||||
# Rebuild lineage after group removal or replacement, including when no run
|
||||
# is selected afterward. The introduced home is the actual refined tune.
|
||||
|
||||
@@ -76,52 +76,4 @@ def read(path):
|
||||
def load_library(root: pathlib.Path):
|
||||
return tuple(read(path) for path in sorted(root.glob('*.json')))
|
||||
|
||||
def identifying_features(notes, source):
|
||||
"""Select a characteristic leap followed by return, wherever it occurs."""
|
||||
notes = tuple(notes)
|
||||
candidates = []
|
||||
for i in range(len(notes) - 2):
|
||||
a, b, c = notes[i:i + 3]
|
||||
leap, reply = b.pitch.number() - a.pitch.number(), c.pitch.number() - b.pitch.number()
|
||||
if 3 <= abs(leap) <= 9 and leap * reply < 0 and c.start - a.start <= 4:
|
||||
group = notes[i:min(len(notes), i + 4)]
|
||||
rhythm = tuple((n.start - a.start, n.end - n.start) for n in group)
|
||||
intervals = tuple(y.pitch.number() - x.pitch.number() for x, y in zip(group, group[1:]))
|
||||
candidates.append(types.IdentifyingFeature(a.event_id + '_identity', tuple(n.event_id for n in group), rhythm, intervals, source))
|
||||
return tuple(candidates[:1])
|
||||
|
||||
def feature_error(feature, notes):
|
||||
by_id = {n.event_id: n for n in notes}
|
||||
actual = [by_id.get(identity) for identity in feature.events]
|
||||
if not actual or not all(actual):
|
||||
return 1.0
|
||||
first = actual[0].start
|
||||
rhythm = tuple((n.start - first, n.end - n.start) for n in actual)
|
||||
movement = tuple(b.pitch.number() - a.pitch.number() for a, b in zip(actual, actual[1:]))
|
||||
errors = []
|
||||
if 'rhythm' in feature.preserves:
|
||||
errors.append(1.0 if len(rhythm) != len(feature.rhythm) else sum(a != b for a,b in zip(rhythm,feature.rhythm))/max(1,len(rhythm)))
|
||||
if 'intervals' in feature.preserves or 'contour' in feature.preserves:
|
||||
if len(movement) != len(feature.intervals) or not movement and feature.intervals:
|
||||
errors.append(1.0)
|
||||
else:
|
||||
errors.append(sum(((a>0)-(a<0)) != ((b>0)-(b<0)) or 'intervals' in feature.preserves and abs(a-b)>2 for a,b in zip(movement,feature.intervals))/max(1,len(movement)))
|
||||
if 'exact_intervals' in feature.preserves:
|
||||
errors.append(float(movement != feature.intervals))
|
||||
if 'pitches' in feature.preserves:
|
||||
errors.append(float(tuple(n.pitch.number() for n in actual) != feature.pitches))
|
||||
return max(errors,default=0.0)
|
||||
|
||||
def relationship(notes, reference):
|
||||
"""One joint relationship cost: timed movement, not absolute-pitch copying."""
|
||||
notes = tuple(notes)
|
||||
if len(notes) < 2 or len(reference.notes) < 2:
|
||||
return 0.0 if len(notes) == len(reference.notes) == 1 else 1.0
|
||||
actual = tuple(b.pitch.number() - a.pitch.number() for a, b in zip(notes, notes[1:]))
|
||||
expected = reference.contour()
|
||||
count = min(len(actual), len(expected))
|
||||
contour = sum(0.75 * (((a > 0) - (a < 0)) != ((b > 0) - (b < 0))) + 0.25 * min(1, max(0, abs(a - b) - 2) / 7) for a, b in zip(actual, expected)) / max(1, count)
|
||||
start, ref_start = notes[0].start, reference.notes[0].start
|
||||
rhythm = sum(min(1, float(abs((n.start - start) - (r.start - ref_start))) / 2) for n, r in zip(notes, reference.notes)) / min(len(notes), len(reference.notes))
|
||||
evidence = max(0, min(3, len(reference.notes)) - len(notes)) / 3
|
||||
return max(evidence, (contour + rhythm) / 2)
|
||||
from generator.solo.features import identifying_features, feature_error, relationship
|
||||
|
||||
@@ -13,7 +13,7 @@ import generator.solo.types
|
||||
import generator.solo.harmonic
|
||||
import generator.solo.drafts
|
||||
import generator.solo.similarity
|
||||
import generator.solo.references
|
||||
import generator.solo.features
|
||||
import generator.solo.placement
|
||||
|
||||
def describe(solo: generator.solo.types.Solo, scales, measure_offset=0, time_offset=0) -> str:
|
||||
@@ -32,7 +32,7 @@ def describe(solo: generator.solo.types.Solo, scales, measure_offset=0, time_off
|
||||
lines.extend(('', f'{plan.id}: {plan.start}-{plan.end}; {plan.relationship}; motif {plan.motif.id}', f' Origin: {plan.motif.origin}; source events: ' + ', '.join(s for n in plan.motif.notes for s in n.sources)))
|
||||
lines.append(f' Contract: {plan.treatment}; closure: {plan.closure}; ending: {plan.ending}; score: {phrase.score:.4f}; identity: {plan.seed}')
|
||||
if plan.treatment in ('source_variation', 'thematic_recomposition'):
|
||||
lines.append(' Activity preference: 4–6 attacks per four beats; source phrasing takes precedence. Occupancy is diagnostic, not a target.')
|
||||
lines.append(' Activity follows the selected gesture and tempo-aware vocabulary; occupancy is diagnostic, not a target.')
|
||||
if phrase.draft:
|
||||
draft = phrase.draft
|
||||
lines.append(f' Complete draft {draft.id}: {draft.relationship}; home {draft.home.id if draft.home else "none"}; {len(draft.notes)} attacks; rests {draft.rests()}')
|
||||
@@ -76,8 +76,10 @@ def describe(solo: generator.solo.types.Solo, scales, measure_offset=0, time_off
|
||||
degrees = [str(d) for d, a in generator.harmony.degrees(choice.span.chord).items() if (root + generator.harmony.degree_semitones(d, a)) % 12 == note.pitch.number() % 12]
|
||||
function = 'chord degree ' + '/'.join(degrees) if degrees else 'in-collection connection'
|
||||
if note.pitch.number()%12 not in generator.solo.types.permitted(solo.choices,note.start,note.end):
|
||||
function='validated enclosure exception'
|
||||
function='validated expression exception'
|
||||
lines.append(f' {note.start}-{note.end}: {note.pitch.label()}; {note.role}; {generator.harmony.symbol(choice.span.chord)}, {function}; {choice.scale.label()}; string {note.position.string_number}, fret {note.position.fret}')
|
||||
for expression in detail['expressions']:
|
||||
lines.append('Expression: ' + expression['kind'] + ' / ' + expression['rule'] + ' -> ' + str(expression['target']['pitch']))
|
||||
lines.append('\nSounding harmonic spans (ties merged before classification):')
|
||||
for event in detail['harmonic_roles']:
|
||||
if float(fractions.Fraction(event['sounding_duration'])) >= 1 or event['protected'] or event['enclosure']:
|
||||
@@ -88,7 +90,8 @@ def structure(solo):
|
||||
harmony = generator.solo.harmonic.Harmony(solo.choices)
|
||||
contracts = tuple(dict.fromkeys(t for p in solo.phrases if p.state for t in p.state.treatments))
|
||||
enclosures = tuple(e for p in solo.phrases if p.draft for e in p.draft.enclosures)
|
||||
checked = harmony.evaluate(solo.notes(), contracts, until=solo.end, complete=True,enclosures=enclosures)
|
||||
expressions = tuple(e for p in solo.phrases if p.draft for e in p.draft.expressions)
|
||||
checked = harmony.evaluate(solo.notes(), contracts, until=solo.end, complete=True,enclosures=enclosures,expressions=expressions)
|
||||
|
||||
def rational(value):
|
||||
if isinstance(value, dict):
|
||||
@@ -122,10 +125,10 @@ def structure(solo):
|
||||
entry['head_comparison_plain']=generator.solo.similarity.compare(phrase.refined.notes,local_source)
|
||||
entry['head_comparison_elaborated']=generator.solo.similarity.compare(tuple(n for d in phrase.decisions for n in d.notes),local_source)
|
||||
if phrase.state and phrase.state.home:
|
||||
entry['theme_relationship']=generator.solo.references.relationship(phrase.refined.notes,phrase.state.home)
|
||||
entry['theme_relationship']=generator.solo.features.relationship(phrase.refined.notes,phrase.state.home)
|
||||
entry['score'] = phrase.score
|
||||
phrases.append(entry)
|
||||
return {'instrument_profile': rational(dataclasses.asdict(solo.profile)) if solo.profile else None, 'phrasing': phrasing_report(solo), 'source_comparison': {stage:generator.solo.similarity.compare(tuple(n for p in solo.phrases for n in (p.refined.notes if stage=='plain' and p.refined else tuple(n for d in p.decisions for n in d.notes))),tuple(n for p in solo.phrases if p.initial for n in p.initial.notes)) for stage in ('plain','elaborated')}, 'fretboard': fretboard_report(solo), 'passages': [{'id': p.id, 'start': str(p.start), 'end': str(p.end), 'phrases': list(p.phrase_ids), 'seed': p.seed} for p in solo.passages], 'phrases': phrases, 'enclosures': rational([dataclasses.asdict(e) for e in enclosures]), 'harmonic_roles': harmony.describe(solo.notes(), contracts,enclosures), 'harmonic_exposure': checked.exposure, 'harmonic_valid': checked.valid, 'harmonic_error': checked.error, 'unresolved_treatments': rational([dataclasses.asdict(t) for t in checked.pending]), 'treatments': rational([dataclasses.asdict(t) for t in contracts])}
|
||||
return {'instrument_profile': rational(dataclasses.asdict(solo.profile)) if solo.profile else None, 'phrasing': phrasing_report(solo), 'source_comparison': {stage:generator.solo.similarity.compare(tuple(n for p in solo.phrases for n in (p.refined.notes if stage=='plain' and p.refined else tuple(n for d in p.decisions for n in d.notes))),tuple(n for p in solo.phrases if p.initial for n in p.initial.notes)) for stage in ('plain','elaborated')}, 'fretboard': fretboard_report(solo), 'passages': [{'id': p.id, 'start': str(p.start), 'end': str(p.end), 'phrases': list(p.phrase_ids), 'seed': p.seed} for p in solo.passages], 'phrases': phrases, 'expressions': rational([dataclasses.asdict(e) for e in expressions]), 'enclosures': rational([dataclasses.asdict(e) for e in enclosures]), 'harmonic_roles': harmony.describe(solo.notes(), contracts,enclosures,expressions), 'harmonic_exposure': checked.exposure, 'harmonic_valid': checked.valid, 'harmonic_error': checked.error, 'unresolved_treatments': rational([dataclasses.asdict(t) for t in checked.pending]), 'treatments': rational([dataclasses.asdict(t) for t in contracts])}
|
||||
|
||||
|
||||
def phrasing_report(solo):
|
||||
|
||||
+33
-12
@@ -10,7 +10,8 @@ from fractions import Fraction as F
|
||||
|
||||
import generator.model
|
||||
import generator.harmony
|
||||
from generator.solo import types, policy, sequences, routes, references, similarity, phrasing, profiles
|
||||
import generator.vocabulary
|
||||
from generator.solo import types, policy, sequences, routes, features as references, similarity, phrasing, profiles
|
||||
|
||||
|
||||
def source_notes(analysis):
|
||||
@@ -60,20 +61,23 @@ def seeds(analysis, bar, seed):
|
||||
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)):
|
||||
for count in ((len(melody),) if len(melody)<3 else (3,4,5,6,7,8)):
|
||||
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:
|
||||
if len(group)!=count or group[-1].end-group[0].start>max(2*bar,F(3)):
|
||||
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'):
|
||||
for treatment in generator.vocabulary.role().get('preferred_features',('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)
|
||||
preferred=generator.vocabulary.role().get('preferred_features',[])
|
||||
preference=preferred.index(ref.treatment) if ref.treatment in preferred else 0
|
||||
turn=any(a*b<0 for a,b in zip(ref.notes and tuple(b.pitch.number()-a.pitch.number() for a,b in zip(ref.notes,ref.notes[1:])),tuple(b.pitch.number()-a.pitch.number() for a,b in zip(ref.notes,ref.notes[1:]))[1:]))
|
||||
return (-min(4,len(intervals))-.3*turn+.15*preference,-min(3,len(rhythm)),ref.id)
|
||||
return sequences.diverse(groups,rank,lambda r:(r.treatment,int(max(0,r.start)//(4*bar))),policy.DEFAULT.theme_seed_limit)
|
||||
|
||||
|
||||
def feature(reference, actual, *, treatment=None):
|
||||
@@ -85,6 +89,8 @@ def feature(reference, actual, *, treatment=None):
|
||||
'return':('rhythm','intervals'), 'develop_rhythm':('rhythm',),
|
||||
'develop_contour':('intervals',), 'continue':('contour',)}[treatment]
|
||||
original=reference.notes
|
||||
authored=next((r for r in generator.vocabulary.data('grammar')['transformations'] if r['id']==treatment),None)
|
||||
if authored:required=tuple(authored['preserve'])
|
||||
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),
|
||||
@@ -95,7 +101,8 @@ def feature(reference, actual, *, treatment=None):
|
||||
|
||||
def rhythm_windows(library,meter,length,context):
|
||||
from generator.solo import drafts
|
||||
key=('theme-rhythm',id(library),meter,length)
|
||||
tempo=getattr(context,'tempo',120)
|
||||
key=('theme-rhythm',id(library),meter,length,tempo)
|
||||
if key not in context.pools:
|
||||
patterns={}
|
||||
bar=F(meter.split('/')[0])*4/int(meter.split('/')[1])
|
||||
@@ -115,9 +122,18 @@ def rhythm_windows(library,meter,length,context):
|
||||
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)
|
||||
# Authored cells can start late and continue over computational barlines.
|
||||
for identifier,pattern,total in generator.vocabulary.rhythm_cells(tempo):
|
||||
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)
|
||||
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)
|
||||
lambda x:(len(x[0])>16,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]
|
||||
|
||||
|
||||
@@ -366,6 +382,11 @@ def phrase_candidates(source,analysis,incoming,part,harmony,library,rhythms,mete
|
||||
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
|
||||
next_states=[]
|
||||
for cost,units,home,state,ending in states:
|
||||
@@ -409,7 +430,7 @@ def phrase_candidates(source,analysis,incoming,part,harmony,library,rhythms,mete
|
||||
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)
|
||||
figure=phrasing.heard(unit.notes,operation,unit.anchor)
|
||||
figure=phrasing.heard(unit.notes,operation,unit.anchor,bar)
|
||||
repetition=phrasing.repetition(figure,state.history,bar)
|
||||
appearance=phrasing.appearance_cost(operation,state,cursor,solo_end) if home else 0.
|
||||
history=phrasing.remember(state.history,figure,bar)
|
||||
@@ -423,7 +444,7 @@ def phrase_candidates(source,analysis,incoming,part,harmony,library,rhythms,mete
|
||||
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=.35*max(0,expected-possible,attacks-1.5*expected)/max(1,float(source.end-source.start)/4)
|
||||
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))
|
||||
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)
|
||||
@@ -474,7 +495,7 @@ def phrase_candidates(source,analysis,incoming,part,harmony,library,rhythms,mete
|
||||
history=incoming.history
|
||||
for transform in transforms:
|
||||
group=tuple(n for n in notes if n.event_id in transform.events)
|
||||
if group:history=phrasing.remember(history,phrasing.heard(group,transform.operation,ending if group[-1].end==ending.hold_until else None),bar)
|
||||
if group:history=phrasing.remember(history,phrasing.heard(group,transform.operation,ending if group[-1].end==ending.hold_until else None,bar),bar)
|
||||
current=dataclasses.replace(phrase.state,next_destination=phrasing.next_destination(ending,harmony),home=home,source_reference=source_ref,callbacks=incoming.callbacks+sum(u.operation=='callback' for u in units),
|
||||
history=history,returns=state.returns,closures=incoming.closures+1,register_peak=max(state.register_peak,*(n.pitch.number() for n in notes)))
|
||||
if not incoming.home:
|
||||
|
||||
@@ -97,6 +97,17 @@ class HarmonicTreatment:
|
||||
event: str = ''
|
||||
resolution_event: str = ''
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class ExpressionContract:
|
||||
id: str
|
||||
kind: str
|
||||
rule: str
|
||||
events: tuple[Note, ...]
|
||||
target: Note
|
||||
chord: str
|
||||
boundary: fractions.Fraction | None = None
|
||||
preparation: Note | None = None
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class LeadIdentity:
|
||||
id: str
|
||||
@@ -273,6 +284,7 @@ class MelodicDraft:
|
||||
source_reference: SourceReference | None = None
|
||||
sustained_tails: tuple[SustainedTail, ...] = ()
|
||||
enclosures: tuple[EnclosureContract, ...] = ()
|
||||
expressions: tuple[ExpressionContract, ...] = ()
|
||||
def rests(self):
|
||||
cursor, result = self.start, []
|
||||
for note in self.notes:
|
||||
|
||||
Reference in New Issue
Block a user