from __future__ import annotations import dataclasses import fractions import functools import generator.fretboard import generator.solo.types 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)) @functools.lru_cache(maxsize=4096) def place(notes: tuple[generator.solo.types.Note, ...], tempo: fractions.Fraction, previous: generator.solo.types.Note | None = None) -> tuple[int, tuple[generator.solo.types.Note, ...]] | None: states = [(0, ())] for note in notes: following = [] for position in positions(note.pitch.number()): current = dataclasses.replace(note, position=position) candidates = [] for cost, path 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 candidates.append((cost + (0 if note.continuation else travel(prior, current, tempo)), (*path, current))) if candidates: following.append(min(candidates, key=lambda item: (item[0], tuple(n.position for n in item[1])))) if not following: return None states = following return min(states, key=lambda item: (item[0], tuple(n.position for n in item[1]))) @functools.lru_cache(maxsize=128) def positions(number: int): return generator.fretboard.positions_for_pitch(fractions.Fraction(number))