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
+12 -20
View File
@@ -75,36 +75,27 @@ class VoicingCost(typing.NamedTuple):
def moved(self, distance):
return self._replace(effort=self.effort + distance)
def acoustic_preferences(grip, center):
"""Soft register/hand-position costs, in the same units as finger travel."""
policy = generator.vocabulary.role('acoustic_accompaniment').get('voicing', {})
register_weight = fractions.Fraction(str(policy.get('register_weight', 1)))
position_weight = fractions.Fraction(str(policy.get('high_position_weight', 1)))
threshold = fractions.Fraction(str(policy.get('comfortable_max_fret', 9)))
if register_weight < 0 or position_weight < 0 or threshold < 1:
raise ValueError('acoustic voicing requires nonnegative weights and a positive fret threshold')
register = abs(fractions.Fraction(sum(p.midi for p in grip), len(grip)) - center)
position = max(fractions.Fraction(), fractions.Fraction(sum(p.fret for p in grip), len(grip)) - threshold)
return register_weight * register, position_weight * position
def voicing_preferences(part, grip, center):
return generator.guitar.voicing_preferences(grip, generator.guitar.voicing_preference(part + '_accompaniment', center))
def local_cost(part, key, grip, center):
required = requirements(json.loads(key), part)
register = abs(fractions.Fraction(sum(p.midi for p in grip), len(grip)) - center)
preference = sum(voicing_preferences(part, grip, center))
if part == 'electric':
value = generator.guitar.voicing_cost(grip, 'electric', required.allowed)
return VoicingCost(value.extended_grips, 3 * value.string_shortfall + value.family_penalty, value.effort, value.missing_tones, register)
return VoicingCost(value.extended_grips, 3 * value.string_shortfall + value.family_penalty, value.effort + preference, value.missing_tones, register)
# Familiar degree orders on either bass string, then balanced whole-form
# movement, sounding register and hand position. Neither is an absolute ban.
family = generator.chord_families.acoustic(json.loads(key), grip)
effort = sum(acoustic_preferences(grip, center))
return VoicingCost(generator.fretboard.span_penalty(grip), int(not family['preferred']), effort, len(set(required.allowed) - {p.midi % 12 for p in grip}), register)
return VoicingCost(generator.fretboard.span_penalty(grip), int(not family['preferred']), preference, len(set(required.allowed) - {p.midi % 12 for p in grip}), register)
def select_grips(layers, costs, part, seed, *, left=(), right=(), close=False):
"""Exact small-state dynamic programming; never prune by hand center.
Electric effort includes its melody grip cost and hand-center travel.
Acoustic effort balances register/position with travel of all four voices,
including a bass-string change. Compact span and family remain higher tiers.
Both roles balance register/position with travel. Electric includes its
base grip effort; acoustic follows all four voices and bass-string changes.
Compact span and family remain higher tiers.
Fixing the first grip permits an exact repeat-join cost without a beam.
"""
if not layers:
@@ -319,10 +310,11 @@ class Canonical:
'bass_degree': next((d for d, a in degrees.items() if bass and (root + generator.harmony.degree_semitones(d, a)) % 12 == bass.midi % 12), None),
'bass_string': bass.string_number if bass else None,
'movement': movement(self.part, previous, grip), 'voicing_candidates': len(span.options)})
if grip and self.part == 'acoustic':
register, position = acoustic_preferences(grip, self.policy.get('center', 55))
if grip:
register, position = voicing_preferences(self.part, grip, self.policy.get('center', 55))
travel = movement(self.part, previous, grip)
rows[-1]['voicing_cost'] = {'movement': travel, 'register': str(register), 'position': str(position), 'combined': str(travel + register + position)}
effort = generator.guitar.voicing_cost(grip, 'electric').effort if self.part == 'electric' else 0
rows[-1]['voicing_cost'] = {'movement': travel, 'grip_effort': str(effort), 'register': str(register), 'position': str(position), 'combined': str(travel + effort + register + position)}
for event in span.events:
lanes = ['bass', 'upper'] if event.lane == 'combined' else [event.lane]
for lane in lanes: