"""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)