Files
product-vamp/generator/solo/placement.py
T
khodowany 75655786e9
ci / checks (push) Successful in 1h9m55s
Soloing
2026-10-01 08:35:12 -05:00

45 lines
1.9 KiB
Python

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