Files
product-vamp/generator/solo/routes.py
T
khodowany a0bd19b3ba Solo
2026-10-02 00:35:31 -05:00

90 lines
4.4 KiB
Python

"""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.placement
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'):
# 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'):
harmony.route_cache={}
harmony.route_libraries={}
harmony.route_libraries[id(library)]=library
key=(notes,frozenset(fixed),id(library),register)
if key not in harmony.route_cache:
harmony.route_cache[key]=_candidates(notes,fixed,harmony,library,register)
return tuple(dataclasses.replace(r,relationship=relationship) for r in harmony.route_cache[key])
def _candidates(notes, fixed, harmony, library, register):
"""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 2 <= 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.solo.placement.positions(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, 'connect theme', 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)