This commit is contained in:
khodowany
2026-10-01 16:41:18 -05:00
parent 28fc67fba1
commit f5861fcdf6
22 changed files with 1473 additions and 122 deletions
+66 -22
View File
@@ -14,6 +14,7 @@ import generator.solo.policy
import generator.solo.scoring
import generator.solo.sequences
import generator.solo.types
import generator.solo.references
F = fractions.Fraction
@@ -77,7 +78,7 @@ def templates(plan, reference, library, meter):
result.append((reference.id, source, length))
choices = catalog(library, meter)
ranked = generator.solo.sequences.diverse(choices,
lambda item: (abs(len(item[1]) / float(item[2]) - float(plan.activity)), abs(sum(float(b - a) for a, b in item[1]) / float(item[2]) - 0.75), item[0]),
lambda item: (abs(len(item[1]) / float(item[2]) - float(plan.activity)) > 0.75, generator.solo.policy.stable_rank(plan.seed, item)),
lambda item: (len(item[1]), bool(item[1][0][0]), item[2]), 4)
result.extend(ranked)
return tuple(result)
@@ -127,17 +128,51 @@ def ending_options(anchors, plan, choices):
terminal = next((a for a in anchors if a.role == 'terminal'), None)
if terminal is None or terminal.hold_until is None:
return tuple([anchors])
finish = terminal.hold_until
last = next((c for c in choices if c.span.start < finish <= c.span.end), None)
passage_end = plan.passage_end or plan.end
active = [c for c in choices if c.scale and c.span.start < passage_end and c.span.end > plan.start]
if not active:
return tuple([anchors])
# Missing harmony is mandatory space. Retiming an ordinary ending must not
# move its destination into a trailing N.C. span.
harmonic_end = min(passage_end, active[-1].span.end)
result = []
for duration in (F(1, 2), F(1), F(2), F(3)):
start = finish - duration
if last is None or start < max(last.span.start, plan.passage_start or plan.start) or any(a.role != 'terminal' and a.latest + a.duration > start for a in anchors):
continue
changed = dataclasses.replace(terminal, earliest=start, latest=start, duration=duration)
result.append(tuple(changed if a.id == terminal.id else a for a in anchors))
for release in (F(), F(1, 2), F(1)):
finish = harmonic_end - release
last = next((c for c in choices if c.span.start < finish <= c.span.end), None)
for duration in (F(1, 2), F(1), F(2), F(3)):
start = finish - duration
if last is None or start < max(last.span.start, plan.passage_start or plan.start) or any(a.role != 'terminal' and a.latest + a.duration > start for a in anchors):
continue
changed = dataclasses.replace(terminal, earliest=start, latest=start, duration=duration, hold_until=finish)
result.append(tuple(changed if a.id == terminal.id else a for a in anchors))
return tuple(result) or tuple([anchors])
def ending_plan(plan, anchors):
if plan.target or plan.treatment in ('quote', 'exact_quote', 'rhythmic_reference'):
return plan
terminal = next((a for a in anchors if a.role == 'terminal'), None)
if terminal is None or terminal.hold_until is None:
return plan
finish = plan.passage_end or plan.end
release = finish - terminal.hold_until
# The candidate chooses where arrival begins. Earlier portions remain active.
spans = []
for span in plan.passages:
low, high = span.start, min(span.end, terminal.earliest)
if low < high:
spans.append(dataclasses.replace(span, end=high, intent='active', occupancy=F(str(generator.solo.policy.DEFAULT.active_occupancy))))
if not spans and plan.start < terminal.earliest:
spans.append(generator.solo.types.ActivitySpan(plan.passage_start if plan.passage_start is not None else plan.start, terminal.earliest, 'active', plan.activity, F(str(generator.solo.policy.DEFAULT.active_occupancy))))
arrival_length = terminal.hold_until - terminal.earliest
desired = max(F(), plan.activity * (finish - (plan.passage_start if plan.passage_start is not None else plan.start)) - 1)
expected = sum(s.activity * (s.end - s.start) for s in spans)
spans = [dataclasses.replace(s, activity=min(F(2), s.activity * desired / expected) if expected else plan.activity) for s in spans]
spans.append(generator.solo.types.ActivitySpan(terminal.earliest, terminal.hold_until, 'sustained_arrival', 1 / arrival_length, F(str(generator.solo.policy.DEFAULT.arrival_occupancy))))
if release:
spans.append(generator.solo.types.ActivitySpan(terminal.hold_until, finish, 'space', F(), F()))
complete = plan.end == finish
return dataclasses.replace(plan, release=release if complete else F(), passages=tuple(spans), ending=('breath' if release else 'sustain') if complete else 'connected')
def align_anchors(slots, anchors, plan):
"""Choose arrival timing with the rhythm, before assigning any free pitches."""
slots = list(slots)
@@ -172,10 +207,12 @@ def construct(plan, reference, relationship, template, anchors, variant, choices
finish = plan.end - plan.release
slots, groups, cursor = (), [], plan.start
protected = plan.treatment in ('quote', 'exact_quote', 'rhythmic_reference')
schedule = (('repeat', 'extend', 'answer'), ('extend', 'displace', 'recall'))[variant % 2]
while cursor < finish:
index = len(groups)
operation = relationship if index == 0 else schedule[(index - 1) % len(schedule)]
# Establish the idea before departing. Choose from the remaining musical
# span instead of cycling a fixed list of transformations indefinitely.
remaining = finish - cursor
operation = relationship if index == 0 else 'repeat' if index == 1 and variant == 0 else 'answer' if remaining <= period else 'extend' if variant == 1 and index == 1 else 'recall'
activity = next((p for p in plan.passages if p.start <= cursor < p.end), None)
rate = F(len(pattern), 1) / period
unit = F(2) if activity and activity.activity < F(3, 4) else F(1, 2) if activity and activity.activity > rate + F(1, 4) and index else F(1)
@@ -250,7 +287,7 @@ def construct(plan, reference, relationship, template, anchors, variant, choices
identity = generator.solo.policy.stable_rank(plan.seed, (identifier, variant, anchors, relationship, octave))[:12]
return RhythmDraft(identity, tuple(filtered), tuple(groups), anchors, reference, relationship, octave)
def rhythms(plan, incoming, measures, choices, library, batch=0):
def rhythms(plan, incoming, measures, choices, library, batch=0, reference_library=()):
continuing = bool(plan.passage_id and incoming.passage_id == plan.passage_id)
anchors = (incoming.selected_anchors,) if continuing and incoming.selected_anchors else plan.anchor_options or tuple([()])
references = [(plan.motif, 'statement')]
@@ -262,6 +299,8 @@ def rhythms(plan, incoming, measures, choices, library, batch=0):
references.append((plan.motif, 'contrast'))
if incoming.themes and incoming.themes[0].id != incoming.home.id:
references.append((incoming.themes[0], 'recall'))
if plan.treatment not in ('quote', 'exact_quote', 'rhythmic_reference') and not continuing:
references.extend((motif, 'contrast' if incoming.home else 'statement') for example in reference_library for motif in example.windows())
elapsed = F()
meter = measures[0]['time_signature']
for measure in measures:
@@ -273,28 +312,29 @@ def rhythms(plan, incoming, measures, choices, library, batch=0):
result = []
for reference, relationship in references:
available_templates = templates(plan, reference, library, meter)
if isinstance(reference, generator.solo.types.RealizedMotif):
available_templates = available_templates[:1]
if isinstance(reference, generator.solo.types.RealizedMotif) or reference.origin == 'accepted reference profile':
available_templates = tuple([(reference.id, tuple((n.start, n.end) for n in reference.notes), reference.duration)])
for template in available_templates:
for backbone in anchors:
endings = (backbone,) if continuing else ending_options(backbone, plan, choices)
for ending in endings:
for variant in range(2):
for octave in ((0, 1, -1, 2, -2) if plan.treatment in ('quote', 'exact_quote') else tuple([0])):
rhythm = construct(plan, reference, relationship, template, ending, variant, choices, octave)
rhythm = construct(ending_plan(plan, ending), reference, relationship, template, ending, variant, choices, octave)
if rhythm:
result.append(rhythm)
evaluation = generator.solo.scoring.Evaluation(dataclasses.replace(plan, passages=tuple(p for p in plan.passages if p.end > plan.start)), choices)
def rank(rhythm):
dummy = tuple(generator.solo.types.Note(s.start, s.end, s.forced or rhythm.reference.notes[0].pitch) for s in rhythm.slots)
features = evaluation.features(dummy)
return features['activity'] * 4 + features['occupancy'] * 4, generator.solo.policy.stable_rank(plan.seed, rhythm.id)
expected = float(plan.activity * (plan.end - plan.start))
# Broad rhythm feasibility only. Pitch/rhythm relationships are evaluated
# after realization; one attack cannot eliminate an otherwise useful tune.
broad = abs(len(rhythm.slots) - expected) > max(4, expected / 2)
return broad, generator.solo.policy.stable_rank(plan.seed, rhythm.id)
def family(rhythm):
pitches = tuple(a.pitch.number() for a in rhythm.anchors)
shape = generator.solo.sequences.family(tuple(b - a for a, b in zip(pitches, pitches[1:])))
return rhythm.relationship, shape, rhythm.groups[0].source, rhythm.anchors[-1].duration if rhythm.anchors else None
return rhythm.relationship, rhythm.groups[0].source, shape
limit = generator.solo.policy.DEFAULT.draft_frontier
return generator.solo.sequences.diverse(result, rank, family, limit * (batch + 1))[limit * batch:]
@@ -326,6 +366,9 @@ def contract_valid(draft, notes):
directions = tuple((b.pitch.number() > a.pitch.number()) - (b.pitch.number() < a.pitch.number()) for a, b in zip(core, core[1:]))
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)):
return False, f'{feature.id}: selected identifying relationship changed'
return True, ''
def relationship_features(draft, notes):
@@ -338,8 +381,9 @@ def relationship_features(draft, notes):
group = [by_id[i] for i in transform.events if i in by_id]
actual = tuple((b.pitch.number() > a.pitch.number()) - (b.pitch.number() < a.pitch.number()) for a, b in zip(group, group[1:]))
direction.extend(a != b for a, b in zip(actual, transform.directions))
if draft.home and transform.operation not in ('statement', 'contrast', 'close', 'fragment', 'broaden_ending'):
development.append(generator.solo.scoring.motif_fidelity(group[:3], draft.home, 'opening_reference'))
reference = draft.reference_motif if transform.operation in ('statement', 'contrast') else draft.home
if reference and transform.operation not in ('close', 'fragment', 'broaden_ending'):
development.append(generator.solo.references.relationship(group, reference))
joins = []
for time in draft.joins:
left = next((n for n in reversed(notes) if n.start < time), None)