90 lines
4.4 KiB
Python
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)
|