from __future__ import annotations import dataclasses import fractions import functools import generator.fretboard import generator.solo.types import generator.solo.profiles import generator.solo.policy def travel(previous: generator.solo.types.Note | None, current: generator.solo.types.Note, tempo: fractions.Fraction) -> int: if previous is None: return 0 return transition(previous.position, current.position, current.start - previous.start, tempo) @functools.lru_cache(maxsize=16384) def transition(previous, current, duration, tempo): distance = abs(previous.fret - current.fret) + abs(previous.string_number - current.string_number) seconds = float(duration * 60 / tempo) # Movement is a preference, not an unsupported claim about a player's limits. return round(100 * distance / max(seconds, 0.05)) def hand_positions(position): return tuple(h for h in range(1, 10) if h <= position.fret <= h + 3) @functools.lru_cache(maxsize=512) def position_rank(position): return generator.solo.policy.stable_rank('guitar-position', position) def stable_path(path): return tuple(position_rank(n.position) for n in path) @functools.lru_cache(maxsize=4096) def alternatives(notes, tempo, previous=None, limit=2, profile=generator.solo.profiles.UNRESTRICTED): """Phrase paths with a four-fret hand region and distinct endpoint choices.""" states = [(0, (), h) for h in hand_positions(previous.position)] if previous and previous.position else [(0, (), None)] for note in notes: following = {} for position in positions(note.pitch.number(), profile): current = dataclasses.replace(note, position=position) for hand in hand_positions(position): best = None for cost, path, old_hand in states: prior = path[-1] if path else previous if note.continuation and (prior is None or prior.pitch != note.pitch or prior.position != position or prior.end != note.start): continue movement = 0 if prior and not note.continuation: seconds = max(.05, float((note.start - prior.start) * 60 / tempo)) distance = abs(hand - old_hand) if old_hand is not None else 0 distance += .25 * abs(position.fret - prior.position.fret) + .5 * abs(position.string_number - prior.position.string_number) movement = round(100 * distance / seconds) candidate = (cost + movement, (*path, current), hand) if best is None or candidate[0] < best[0] or candidate[0] == best[0] and stable_path(candidate[1]) < stable_path(best[1]): best = candidate if best is not None: following[position, hand] = best if not following: return () states = list(following.values()) # Endpoints survive so the following phrase can choose a better transition. selected, seen = [], set() for cost, path, hand in sorted(states, key=lambda v: (v[0], stable_path(v[1]))): endpoint = path[-1].position if path else None if endpoint in seen: continue seen.add(endpoint) selected.append((cost, path)) if len(selected) == limit: break return tuple(selected) @functools.lru_cache(maxsize=4096) def place(notes, tempo, previous=None, profile=generator.solo.profiles.UNRESTRICTED): result = alternatives(notes, tempo, previous, 1, profile) return result[0] if result else None @functools.lru_cache(maxsize=2048) def positions(number: int, profile=generator.solo.profiles.UNRESTRICTED): profile = profile or generator.solo.profiles.UNRESTRICTED return tuple(p for p in generator.fretboard.single_positions_for_pitch(fractions.Fraction(number)) if p.string_number in profile.strings)