@@ -8,6 +8,7 @@ import generator.harmony
|
||||
import generator.model
|
||||
import generator.scales.selection
|
||||
import generator.scales.timeline
|
||||
import generator.solo.harmonic
|
||||
import generator.solo.analysis
|
||||
import generator.solo.planning
|
||||
import generator.solo.policy
|
||||
@@ -31,17 +32,37 @@ def compose(analysis, measures, part, end, *, scales, sequences, rhythms, initia
|
||||
context = generator.solo.realization.SearchContext()
|
||||
|
||||
def realize(plan, previous):
|
||||
return generator.solo.realization.alternatives(plan, measures, choices, sequences, rhythms, analysis.tempo, previous, context)
|
||||
original = plan
|
||||
for attempt in range(generator.solo.policy.DEFAULT.replan_attempts + 1):
|
||||
try:
|
||||
options = generator.solo.realization.alternatives(plan, measures, choices, sequences, rhythms, analysis.tempo, previous, context)
|
||||
if attempt:
|
||||
context.results[(original, previous)] = options
|
||||
return options
|
||||
except generator.solo.realization.SearchExhausted as error:
|
||||
if attempt == generator.solo.policy.DEFAULT.replan_attempts or plan.treatment in ('quote', 'exact_quote'):
|
||||
raise
|
||||
context.diagnostics.setdefault('replanning', []).append({'phrase': plan.id, 'attempt': attempt + 1, 'reason': str(error), 'adjustment': 'contour reference' if attempt == 0 else 'opening reference'})
|
||||
# Replan only ordinary development contracts; harmony, anchor pitches,
|
||||
# required holds, timing, activity and protected quotations stay fixed.
|
||||
treatment = 'contour_reference' if attempt == 0 else 'opening_reference'
|
||||
gestures = tuple(dataclasses.replace(g, treatment=treatment) if g.relationship not in ('statement', 'close') else g for g in plan.gestures)
|
||||
plan = dataclasses.replace(plan, gestures=gestures)
|
||||
|
||||
phrases = choose_phrases(plans, realize)
|
||||
context.diagnostics['policy'] = generator.solo.policy.describe()['fingerprint']
|
||||
context.diagnostics['musical_identity'] = generator.solo.analysis.identity(analysis, measures, part)
|
||||
result = generator.solo.types.Solo(part, choices, tuple(phrases), analysis.tempo, end, context.diagnostics)
|
||||
passage_ids = tuple(dict.fromkeys(p.passage_id for p in plans if p.passage_id))
|
||||
passages = tuple(generator.solo.types.MelodicPassagePlan(identifier, min(p.start for p in plans if p.passage_id == identifier), max(p.end for p in plans if p.passage_id == identifier), tuple(p.id for p in plans if p.passage_id == identifier), next(p.seed for p in plans if p.passage_id == identifier)) for identifier in passage_ids)
|
||||
result = generator.solo.types.Solo(part, choices, tuple(phrases), analysis.tempo, end, context.diagnostics, passages)
|
||||
validate(result)
|
||||
return result
|
||||
|
||||
def phrase_notes(phrase):
|
||||
return generator.solo.types.sounding_notes(n for d in phrase.decisions for n in d.notes)
|
||||
notes = tuple(n for d in phrase.decisions for n in d.notes)
|
||||
if notes and notes[0].continuation:
|
||||
notes = (dataclasses.replace(notes[0], continuation=False), *notes[1:])
|
||||
return generator.solo.types.sounding_notes(notes)
|
||||
|
||||
def transition_cost(first, second):
|
||||
before, after = phrase_notes(first), phrase_notes(second)
|
||||
@@ -70,20 +91,23 @@ def choose_phrases(plans, realize):
|
||||
ranked.append((option.score + incoming, option))
|
||||
continue
|
||||
try:
|
||||
following = realize(plans[index + 1], last)
|
||||
following = realize(plans[index + 1], option.state or last)
|
||||
except generator.solo.realization.SearchExhausted as error:
|
||||
failure = error
|
||||
continue
|
||||
length = float(option.plan.end - option.plan.start)
|
||||
next_length = float(plans[index + 1].end - plans[index + 1].start)
|
||||
cost = min((option.score * length + future.score * next_length) / (length + next_length) + transition_cost(option, future) for future in following)
|
||||
shared_passage = bool(plan.passage_id and plan.passage_id == plans[index + 1].passage_id)
|
||||
cost = min((future.score if shared_passage else (option.score * length + future.score * next_length) / (length + next_length)) + transition_cost(option, future) for future in following)
|
||||
ranked.append((cost + incoming, option))
|
||||
if not ranked:
|
||||
raise failure or generator.solo.realization.SearchExhausted(f'{plan.id}: no feasible phrase transition')
|
||||
_, chosen = min(ranked, key=lambda item: (item[0], tuple((n.pitch, n.start, n.end, n.position) for n in phrase_notes(item[1]))))
|
||||
result.append(chosen)
|
||||
notes = phrase_notes(chosen)
|
||||
if notes:
|
||||
if chosen.state:
|
||||
previous = chosen.state
|
||||
elif notes:
|
||||
previous = notes[-1]
|
||||
return tuple(result)
|
||||
|
||||
@@ -101,3 +125,14 @@ def validate(solo):
|
||||
notes = phrase_notes(phrase)
|
||||
if not generator.solo.realization.target_ok(notes, phrase.plan.target):
|
||||
raise ValueError(f'{phrase.plan.id}: planned harmonic arrival was not realized')
|
||||
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))
|
||||
checked = harmony.evaluate(solo.notes(), contracts, until=solo.end, complete=True)
|
||||
if not checked.valid:
|
||||
raise ValueError(f'{solo.part} solo: {checked.error}')
|
||||
for phrase in solo.phrases:
|
||||
if phrase.state:
|
||||
complete = phrase.plan.end == (phrase.plan.passage_end or phrase.plan.end)
|
||||
valid, error = generator.solo.harmonic.anchors_valid(phrase.anchors, phrase.state.notes, phrase.plan.end, complete=complete, closure=phrase.plan.closure)
|
||||
if not valid:
|
||||
raise ValueError(error)
|
||||
|
||||
Reference in New Issue
Block a user