Acoustic Comping
This commit is contained in:
+23
-4
@@ -67,7 +67,7 @@ def movement(part, previous, current):
|
||||
class VoicingCost(typing.NamedTuple):
|
||||
extended_grips: int = 0
|
||||
family: int = 0
|
||||
effort: int = 0
|
||||
effort: fractions.Fraction = fractions.Fraction()
|
||||
missing_tones: int = 0
|
||||
register: fractions.Fraction = fractions.Fraction()
|
||||
def plus(self, other):
|
||||
@@ -75,21 +75,36 @@ 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 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)
|
||||
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)
|
||||
# Familiar degree orders on either bass string, then whole-form movement.
|
||||
# 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)
|
||||
return VoicingCost(generator.fretboard.span_penalty(grip), int(not family['preferred']), 0, len(set(required.allowed) - {p.midi % 12 for p in grip}), register)
|
||||
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)
|
||||
|
||||
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 follows all four voices, including a bass-string change.
|
||||
Acoustic effort balances register/position with travel of all four voices,
|
||||
including a bass-string change. Compact span and family remain higher tiers.
|
||||
Fixing the first grip permits an exact repeat-join cost without a beam.
|
||||
"""
|
||||
if not layers:
|
||||
@@ -304,6 +319,10 @@ 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))
|
||||
travel = movement(self.part, previous, grip)
|
||||
rows[-1]['voicing_cost'] = {'movement': travel, 'register': str(register), 'position': str(position), 'combined': str(travel + register + position)}
|
||||
for event in span.events:
|
||||
lanes = ['bass', 'upper'] if event.lane == 'combined' else [event.lane]
|
||||
for lane in lanes:
|
||||
|
||||
@@ -213,7 +213,14 @@ def _generate_role(source, role, data_dir, settings=None, cache_dir=None):
|
||||
trace = {'source': source.musical_key, 'role': role, 'production_resources': inputs['catalogs'],
|
||||
'vocabulary': generator.vocabulary.fingerprint(inputs['vocabulary']), 'settings': settings, 'algorithm': algorithm, 'code': inputs['code'], 'cache_key': key}
|
||||
choice_source = source.accompaniment_key() if role.endswith('_accompaniment') else source.musical_key
|
||||
seed = generator.vocabulary.fingerprint({'source': choice_source, 'role': role, 'settings': settings, 'data': inputs['vocabulary'], 'version': algorithm})
|
||||
choice_data, choice_settings = inputs['vocabulary'], settings
|
||||
if role == 'acoustic_accompaniment':
|
||||
# Voicing weights invalidate the concrete cache, but must not reshuffle
|
||||
# rhythm patterns. Removing this new field preserves the pre-policy seed.
|
||||
choice_data = copy.deepcopy(choice_data)
|
||||
choice_data['roles']['role'].pop('voicing', None)
|
||||
choice_settings = {k: v for k, v in settings.items() if k != 'voicing'}
|
||||
seed = generator.vocabulary.fingerprint({'source': choice_source, 'role': role, 'settings': choice_settings, 'data': choice_data, 'version': algorithm})
|
||||
details = {}
|
||||
solo_report = ''
|
||||
composition_timings = {}
|
||||
|
||||
Reference in New Issue
Block a user