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
+76
View File
@@ -0,0 +1,76 @@
"""Instantiate complete authored routes; a failed path is never repaired note by note."""
from __future__ import annotations
import dataclasses
import fractions
import generator.fretboard
import generator.solo.policy
import generator.solo.sequences
import generator.solo.types as types
def advance(scale, previous, step):
try:
degree = types.scale_index(scale, previous)
return tuple([types.scale_pitch(scale, degree + step)])
except ValueError:
# At a changed collection count actual intervening scale tones from the
# incoming sound. No old degree number is transferred to the new scale.
if not step:
return ()
nearby = sorted((types.scale_pitch(scale, i) for i in range(-28, 35)), key=lambda p: p.number())
outgoing = [p for p in nearby if (p.number() - previous) * step > 0]
if step < 0:
outgoing.reverse()
return tuple(outgoing[abs(step) - 1:abs(step)])
def candidates(notes, fixed, harmony, library, register, relationship='connect theme'):
"""Every returned candidate includes its entry, complete rhythm and destination.
Callers validate the whole surrounding sounding context and guitar path.
This function applies exact scale paths, fixed events and range constraints.
"""
if not 3 <= len(notes) <= 16:
return ()
first, destination = notes[0], notes[-1]
span = harmony.at(first.start)
if not span or not span.scale:
return ()
try:
displacement = types.scale_index(span.scale, destination.pitch.number()) - types.scale_index(span.scale, first.pitch.number())
displacements = tuple([displacement])
except ValueError:
# Enumerate possible degree routes when the destination changes collection;
# actual endpoint equality below decides compatibility, not a pitch estimate.
displacements = tuple(range(-7, 8))
direction = (destination.pitch.number() > first.pitch.number()) - (destination.pitch.number() < first.pitch.number())
paths = dict.fromkeys(path for d in displacements for path in library.retrieve(len(notes), d, direction))
result = []
low, high = generator.fretboard.single_register((register[0] - 7, register[1] + 5))
allowed = {n.start: types.permitted(harmony.choices, n.start, n.end) for n in notes}
for path in paths:
realized = [first]
for template, step in zip(notes[1:], path.steps):
current = harmony.at(template.start)
if not current or not current.scale:
break
options = advance(current.scale, realized[-1].pitch.number(), step)
if not options:
break
pitch = options[0]
if not low <= pitch.number() <= high or pitch.number() % 12 not in allowed[template.start]:
break
if (template.event_id in fixed or template is destination) and pitch.number() != template.pitch.number():
break
if not generator.fretboard.single_positions_for_pitch(fractions.Fraction(pitch.number())):
break
realized.append(dataclasses.replace(template, pitch=template.pitch if pitch.number() == template.pitch.number() else pitch, position=None))
if len(realized) != len(notes):
continue
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)
identity = generator.solo.policy.stable_rank('complete-route', (tuple(realized), path.sources))[:16]
result.append(types.RouteCandidate(identity, tuple(realized), destination, route_spans, relationship, path.sources))
return 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:]))),
generator.solo.policy.DEFAULT.join_alternatives)