Comping Gen

This commit is contained in:
khodowany
2026-10-06 22:13:50 -05:00
parent 2f21ec1f16
commit 125aaa1637
13 changed files with 213 additions and 44 deletions
+30 -9
View File
@@ -23,7 +23,7 @@ class VoicingCost(typing.NamedTuple):
covering_tones: int = 0
string_shortfall: int = 0
family_penalty: int = 0
effort: int = 0
effort: fractions.Fraction = fractions.Fraction()
missing_tones: int = 0
def plus(self, other: VoicingCost) -> VoicingCost:
return VoicingCost(*(a + b for a, b in zip(self, other)))
@@ -31,18 +31,38 @@ class VoicingCost(typing.NamedTuple):
def moved(self, distance: int) -> VoicingCost:
return self._replace(effort=self.effort + distance)
def voicing_cost(positions: tuple[generator.fretboard.TabPosition, ...], profile: str = '', allowed: tuple[int, ...] = (), melody: tuple[int, ...] = ()) -> VoicingCost:
families = tuple(generator.vocabulary.accompaniment('electric')['voicing_families']['rank'].items()) if profile == 'electric' else ()
return _voicing_cost(positions, profile, allowed, melody, families)
def voicing_preference(role, center=None):
"""Immutable role settings for both selection and scored-retrieval caches."""
settings = generator.vocabulary.role(role)
policy = settings.get('voicing', {})
center = settings.get('center', 53 if role.startswith('acoustic') else 60) if center is None else center
result = tuple(fractions.Fraction(str(v)) for v in (center, policy.get('register_weight', 1), policy.get('high_position_weight', 1), policy.get('comfortable_max_fret', 9)))
if result[1] < 0 or result[2] < 0 or result[3] < 1:
raise ValueError('voicing preference requires nonnegative weights and a positive fret threshold')
return result
@functools.lru_cache(maxsize=8192)
def _voicing_cost(positions, profile, allowed, melody, families):
def voicing_preferences(positions, preference):
if not positions or not preference:
return fractions.Fraction(), fractions.Fraction()
center, register_weight, position_weight, threshold = preference
register = abs(fractions.Fraction(sum(p.midi for p in positions), len(positions)) - center)
position = max(fractions.Fraction(), fractions.Fraction(sum(p.fret for p in positions), len(positions)) - threshold)
return register_weight * register, position_weight * position
def voicing_cost(positions: tuple[generator.fretboard.TabPosition, ...], profile: str = '', allowed: tuple[int, ...] = (), melody: tuple[int, ...] = ()) -> VoicingCost:
families = tuple(generator.vocabulary.accompaniment('electric')['voicing_families']['rank'].items()) if profile == 'electric' else ()
preference = voicing_preference('electric_melody') if profile == 'electric' and melody else ()
return _voicing_cost(positions, profile, allowed, melody, families, preference)
@functools.lru_cache(maxsize=8192)
def _voicing_cost(positions, profile, allowed, melody, families, preference):
frets = {p.fret for p in positions}
classes = {p.midi % 12 for p in positions}
above = sum(p.midi > max(melody) for p in positions) if melody else 0
span = compactness(positions)
if profile == 'electric':
effort = 2 * max(0, len(frets) - 1) + 3 * span
effort = 2 * max(0, len(frets) - 1) + 3 * span + sum(voicing_preferences(positions, preference))
else:
effort = 2 * max(0, max(frets, default=0) - PREFERRED_MAX_FRET) + span
family_penalty = dict(families).get(generator.chord_families.electric(positions), 0) if positions and families else 0
@@ -51,10 +71,11 @@ def _voicing_cost(positions, profile, allowed, melody, families):
def chord_positions(requirements: generator.fretboard.ChordRequirements, prune: bool = True, profile: str = '', melody: tuple[int, ...] = ()) -> tuple[tuple[generator.fretboard.TabPosition, ...], ...]:
families = tuple(generator.vocabulary.accompaniment('electric')['voicing_families']['rank'].items()) if profile == 'electric' else ()
return _chord_positions(requirements, prune, profile, melody, families)
preference = voicing_preference('electric_melody') if profile == 'electric' and melody else ()
return _chord_positions(requirements, prune, profile, melody, families, preference)
@functools.lru_cache(maxsize=256)
def _chord_positions(requirements, prune, profile, melody, families):
def _chord_positions(requirements, prune, profile, melody, families, preference):
result = []
options = [tuple(generator.fretboard.TabPosition(string, fret, open_midi + fret) for fret in range(generator.fretboard.MIN_FRET, generator.fretboard.MAX_FRET + 1) if (open_midi + fret) % 12 in requirements.allowed or open_midi + fret in melody) for string, open_midi in generator.fretboard.STANDARD_TUNING]
@@ -90,7 +111,7 @@ def _chord_positions(requirements, prune, profile, melody, families):
best = {}
for positions in result:
center = position_center(positions)
rank = (voicing_cost(positions, profile, requirements.allowed, melody), positions)
rank = (_voicing_cost(positions, profile, requirements.allowed, melody, families, preference), positions)
if center not in best or rank < best[center][0]:
best[center] = (rank, positions)
return tuple(sorted(item[1] for item in best.values()))