123 lines
6.3 KiB
Python
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)
|