+69
-40
@@ -5,77 +5,106 @@ import fractions
|
||||
import functools
|
||||
|
||||
import generator.fretboard
|
||||
import generator.hand_position
|
||||
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)
|
||||
return generator.hand_position.regions((position,))
|
||||
|
||||
@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)
|
||||
def seconds(duration, tempo):
|
||||
return max(fractions.Fraction(1, 20), duration * 60 / tempo)
|
||||
|
||||
@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)]
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class Path:
|
||||
travel: int = 0
|
||||
peak: int = 0
|
||||
effort: fractions.Fraction = fractions.Fraction()
|
||||
notes: tuple = ()
|
||||
hands: tuple = ()
|
||||
ties: tuple = ()
|
||||
@property
|
||||
def rank(self):
|
||||
return self.travel, self.peak, self.effort, self.ties
|
||||
|
||||
@functools.lru_cache(maxsize=512)
|
||||
def paths(notes, tempo, previous=None, profile=generator.solo.profiles.UNRESTRICTED, fixed=False):
|
||||
"""Complete TAB paths: hand travel first, then maximum hand position.
|
||||
|
||||
Preserve different peaks at a common endpoint. No barline or tied fragment
|
||||
resets the hand or shortens the elapsed time from the previous attack.
|
||||
"""
|
||||
states = [(Path(peak=h), h) for h in hand_positions(previous.position)] if previous and previous.position else [(Path(), 0)]
|
||||
last_attack = previous.start if previous else None
|
||||
for note in notes:
|
||||
following = {}
|
||||
for position in positions(note.pitch.number(), profile):
|
||||
options = (note.position,) if fixed and note.position else positions(note.pitch.number(), profile)
|
||||
for position in options:
|
||||
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):
|
||||
for path, old_hand in states:
|
||||
prior = path.notes[-1] if path.notes else previous
|
||||
if note.continuation and (prior is None or prior.pitch != note.pitch or prior.position != position or prior.end != note.start or hand != old_hand):
|
||||
continue
|
||||
movement = 0
|
||||
motion = generator.hand_position.Motion()
|
||||
effort = fractions.Fraction()
|
||||
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
|
||||
motion = generator.hand_position.motion((prior.position,), (position,), old_hand, hand)
|
||||
effort = 100 * (motion.shift + motion.ergonomics) / seconds(note.start - last_attack, tempo)
|
||||
candidate = Path(path.travel + motion.shift, max(path.peak, hand), path.effort + effort, (*path.notes, current), (*path.hands, hand), (*path.ties, (position_rank(position), hand)))
|
||||
key = (position, hand, candidate.peak)
|
||||
if key not in following or candidate.rank < following[key].rank:
|
||||
following[key] = candidate
|
||||
if not following:
|
||||
return ()
|
||||
states = list(following.values())
|
||||
# Endpoints survive so the following phrase can choose a better transition.
|
||||
# A strictly worse hand travel at the identical endpoint/hand cannot recover.
|
||||
best = {}
|
||||
for (position, hand, _), path in following.items():
|
||||
best[position, hand] = min(best.get((position, hand), path.travel), path.travel)
|
||||
states = [(path, hand) for (position, hand, _), path in following.items() if path.travel == best[position, hand]]
|
||||
if not note.continuation:
|
||||
last_attack = note.start
|
||||
return tuple(sorted((path for path, _ in states), key=lambda path: path.rank))
|
||||
|
||||
@functools.lru_cache(maxsize=512)
|
||||
def alternatives(notes, tempo, previous=None, limit=2, profile=generator.solo.profiles.UNRESTRICTED):
|
||||
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
|
||||
for path in paths(notes, tempo, previous, profile):
|
||||
endpoint = (path.notes[-1].position, path.hands[-1]) if path.notes else None
|
||||
if endpoint in seen:
|
||||
continue
|
||||
seen.add(endpoint)
|
||||
selected.append((cost, path))
|
||||
selected.append((path.travel, path.notes))
|
||||
if len(selected) == limit:
|
||||
break
|
||||
return tuple(selected)
|
||||
|
||||
@functools.lru_cache(maxsize=4096)
|
||||
@functools.lru_cache(maxsize=512)
|
||||
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
|
||||
|
||||
def describe(notes, tempo):
|
||||
candidates = paths(notes, tempo, fixed=True)
|
||||
if not candidates:
|
||||
raise ValueError('cannot describe an invalid guitar path')
|
||||
path = candidates[0]
|
||||
result, prior, old_hand, last_attack = [], None, 0, None
|
||||
for note, hand in zip(path.notes, path.hands):
|
||||
motion = generator.hand_position.motion((prior.position,), (note.position,), old_hand, hand) if prior and not note.continuation else generator.hand_position.Motion()
|
||||
effort = 100 * (motion.shift + motion.ergonomics) / seconds(note.start - last_attack, tempo) if prior and not note.continuation else 0
|
||||
result.append({'start': str(note.start), 'end': str(note.end), 'hand_position': hand, 'string': note.position.string_number,
|
||||
'fret': note.position.fret, 'hand_shift': motion.shift, 'finger_adjustment': motion.fingers, 'string_changes': motion.strings, 'secondary_effort': str(effort)})
|
||||
if not note.continuation:
|
||||
last_attack = note.start
|
||||
prior, old_hand = note, hand
|
||||
return {'version': generator.hand_position.VERSION, **generator.hand_position.summary(r['hand_shift'] for r in result), 'secondary_effort': str(path.effort), 'peak_position': path.peak, 'positions': result}
|
||||
|
||||
@functools.lru_cache(maxsize=2048)
|
||||
def positions(number: int, profile=generator.solo.profiles.UNRESTRICTED):
|
||||
profile = profile or generator.solo.profiles.UNRESTRICTED
|
||||
|
||||
Reference in New Issue
Block a user