Files
product-vamp/generator/solo/routes.py
T
khodowany 5fff5a165e
ci / checks (push) Failing after 23m43s
Solo
2026-10-02 06:33:01 -05:00

123 lines
6.3 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.concrete_routes={}
harmony.route_libraries[id(library)]=library
concrete_key=(notes,frozenset(fixed),id(library),register)
if concrete_key in harmony.concrete_routes:
return tuple(dataclasses.replace(r,relationship=relationship) for r in harmony.concrete_routes[concrete_key])
# Event/source IDs and non-fixed placeholder pitches do not affect route
# feasibility. Cache the pitch work across different thematic histories,
# then restore this caller's provenance before ranking complete routes.
fixed_indices=frozenset(i for i,n in enumerate(notes) if n.event_id in fixed)
canonical=tuple(dataclasses.replace(n,sources=(),event_id=str(i),role='connection',position=None,
pitch=n.pitch if i==0 or i==len(notes)-1 or i in fixed_indices else types.Pitch('c',fractions.Fraction(),0)) for i,n in enumerate(notes))
key=(canonical,fixed_indices,id(library),register)
if key not in harmony.route_cache:
harmony.route_cache[key]=_candidates(canonical,{str(i) for i in fixed_indices},harmony,library,register)
result=[]
for route in harmony.route_cache[key]:
actual=tuple(n if i==0 else dataclasses.replace(n,pitch=n.pitch if n.pitch.number()==r.pitch.number() else r.pitch,position=None)
for i,(n,r) in enumerate(zip(notes,route.notes)))
identity=generator.solo.policy.stable_rank('complete-route',(actual,route.sources))[:16]
result.append(dataclasses.replace(route,id=identity,notes=actual,destination=notes[-1],relationship=relationship))
selected=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)
if len(harmony.concrete_routes)>=8192:harmony.concrete_routes.pop(next(iter(harmony.concrete_routes)))
if len(harmony.route_cache)>=8192:harmony.route_cache.pop(next(iter(harmony.route_cache)))
harmony.concrete_routes[concrete_key]=selected
return selected
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 ()
contexts=tuple((harmony.at(n.start).scale if harmony.at(n.start) else None,
types.permitted(harmony.choices,n.start,n.end),
n.pitch.number() if n.event_id in fixed or i==len(notes)-1 else None) for i,n in enumerate(notes))
if not hasattr(harmony,'pitch_routes'):harmony.pitch_routes={}
key=(first.pitch,contexts,id(library),register)
if key not in harmony.pitch_routes:
harmony.pitch_routes[key]=pitch_paths(first.pitch,contexts,library,register)
if len(harmony.pitch_routes)>8192:harmony.pitch_routes.pop(next(iter(harmony.pitch_routes)))
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)
result=[]
for pitches,sources in harmony.pitch_routes[key]:
realized=(first,*(dataclasses.replace(n,pitch=n.pitch if n.pitch.number()==p.number() else p,position=None)
for n,p in zip(notes[1:],pitches[1:])))
identity=generator.solo.policy.stable_rank('complete-route',(realized,sources))[:16]
result.append(types.RouteCandidate(identity,realized,destination,route_spans,'connect theme',sources))
return tuple(result)
def pitch_paths(first, contexts, library, register):
"""Time-independent pitch work; context includes every overlapped collection.
Sounding durations and chord roles are still validated on each concrete
realization. Equal collection contexts can reuse paths across rhythmic edits.
"""
destination=contexts[-1][2]
try:
displacement=types.scale_index(contexts[0][0],destination)-types.scale_index(contexts[0][0],first.number())
displacements=(displacement,)
except ValueError:
displacements=tuple(range(-7,8))
direction=(destination>first.number())-(destination<first.number())
paths=dict.fromkeys(path for d in displacements for path in library.retrieve(len(contexts),d,direction))
low,high=generator.fretboard.single_register((register[0]-7,register[1]+5))
result=[]
for path in paths:
realized=[first]
for (scale,allowed,fixed),step in zip(contexts[1:],path.steps):
if not scale:break
options=advance(scale,realized[-1].number(),step)
if not options:break
pitch=options[0];number=pitch.number()
if not low<=number<=high or number%12 not in allowed:break
if fixed is not None and number!=fixed:break
if not generator.solo.placement.positions(number):break
realized.append(pitch)
if len(realized)==len(contexts):result.append((tuple(realized),path.sources))
return tuple(result)