+25
-19
@@ -7,10 +7,10 @@ import itertools
|
||||
import logging
|
||||
import typing
|
||||
|
||||
import generator.chord_families
|
||||
import generator.fretboard
|
||||
import generator.model
|
||||
import generator.pitch
|
||||
import generator.chord_families
|
||||
import generator.vocabulary
|
||||
|
||||
PREFERRED_MAX_FRET = 12
|
||||
@@ -31,9 +31,10 @@ class VoicingCost(typing.NamedTuple):
|
||||
def moved(self, distance: int) -> VoicingCost:
|
||||
return self._replace(effort=self.effort + distance)
|
||||
|
||||
def voicing_preference(role, center=None):
|
||||
def voicing_preference(role, center=None, *, composition=None):
|
||||
"""Immutable role settings for both selection and scored-retrieval caches."""
|
||||
settings = generator.vocabulary.role(role)
|
||||
composition = composition or generator.vocabulary.current()
|
||||
settings = composition.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)))
|
||||
@@ -50,9 +51,10 @@ def voicing_preferences(positions, preference):
|
||||
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 ()
|
||||
def voicing_cost(positions: tuple[generator.fretboard.TabPosition, ...], profile: str = '', allowed: tuple[int, ...] = (), melody: tuple[int, ...] = (), *, composition=None) -> VoicingCost:
|
||||
composition = composition or generator.vocabulary.current()
|
||||
families = tuple(composition.accompaniment('electric')['voicing_families']['rank'].items()) if profile == 'electric' else ()
|
||||
preference = voicing_preference('electric_melody', composition=composition) if profile == 'electric' and melody else ()
|
||||
return _voicing_cost(positions, profile, allowed, melody, families, preference)
|
||||
|
||||
@functools.lru_cache(maxsize=8192)
|
||||
@@ -69,9 +71,10 @@ def _voicing_cost(positions, profile, allowed, melody, families, preference):
|
||||
melody_string = min((p.string_number - 1 for p in positions if melody and p.midi == max(melody)), default=0)
|
||||
return VoicingCost(extended_grips=generator.fretboard.span_penalty(positions), melody_string=melody_string, string_shortfall=max(0, 4 - len(positions)) if profile == 'electric' else 0, covered_melody=int(bool(above)), covering_tones=above, family_penalty=family_penalty, effort=effort, missing_tones=len(set(allowed) - classes))
|
||||
|
||||
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 ()
|
||||
preference = voicing_preference('electric_melody') if profile == 'electric' and melody else ()
|
||||
def chord_positions(requirements: generator.fretboard.ChordRequirements, prune: bool = True, profile: str = '', melody: tuple[int, ...] = (), *, composition=None) -> tuple[tuple[generator.fretboard.TabPosition, ...], ...]:
|
||||
composition = composition or generator.vocabulary.current()
|
||||
families = tuple(composition.accompaniment('electric')['voicing_families']['rank'].items()) if profile == 'electric' else ()
|
||||
preference = voicing_preference('electric_melody', composition=composition) if profile == 'electric' and melody else ()
|
||||
return _chord_positions(requirements, prune, profile, melody, families, preference)
|
||||
|
||||
@functools.lru_cache(maxsize=256)
|
||||
@@ -211,7 +214,8 @@ def continued_sources(notes):
|
||||
def has_attack(notes: list[dict]) -> bool:
|
||||
return any('_harmony' in n and '_continue_from' not in n or any('tie_from' not in p for p in n['pitches']) for n in notes)
|
||||
|
||||
def attack_candidates(notes: list[dict], tones: dict[str, GuitarTone], active: dict, held: dict, preserve_strings: bool, fallback: bool = False):
|
||||
def attack_candidates(notes: list[dict], tones: dict[str, GuitarTone], active: dict, held: dict, preserve_strings: bool, fallback: bool = False, *, composition=None):
|
||||
composition = composition or generator.vocabulary.current()
|
||||
pending = []
|
||||
chords = [n for n in notes if '_harmony' in n]
|
||||
chord = chords[0] if chords else None
|
||||
@@ -229,7 +233,7 @@ def attack_candidates(notes: list[dict], tones: dict[str, GuitarTone], active: d
|
||||
requirements = generator.fretboard.chord_requirements(chord['_harmony'], respect_bass=profile != 'electric') if chord else generator.fretboard.ChordRequirements()
|
||||
melody = tuple(sorted({p.midi for _, options in pending for p in options})) if profile == 'electric' else ()
|
||||
fresh = has_attack(notes)
|
||||
candidates = chord_positions(requirements, prune=not (preserve_strings or held or pending or profile), profile=profile, melody=melody) if chord else [()]
|
||||
candidates = chord_positions(requirements, prune=not (preserve_strings or held or pending or profile), profile=profile, melody=melody, composition=composition) if chord else [()]
|
||||
for exact in extend_positions(held, sorted(pending, key=lambda p: len(p[1]))):
|
||||
exact_positions = dict(exact)
|
||||
for positions in candidates:
|
||||
@@ -253,7 +257,7 @@ def attack_candidates(notes: list[dict], tones: dict[str, GuitarTone], active: d
|
||||
choices.append(EventPlacement(note, tuple((p, exact_positions[p['id']]) for p in note['pitches']), added))
|
||||
if not compatible or not playable_choices(choices):
|
||||
continue
|
||||
cost = voicing_cost(positions, profile, requirements.allowed, melody) if fresh else VoicingCost()
|
||||
cost = voicing_cost(positions, profile, requirements.allowed, melody, composition=composition) if fresh else VoicingCost()
|
||||
yield AttackCandidate(tuple(sorted(assignment.items())), tuple(choices), positions, cost)
|
||||
else:
|
||||
shape = tuple(p for _, p in exact) + positions
|
||||
@@ -263,7 +267,7 @@ def attack_candidates(notes: list[dict], tones: dict[str, GuitarTone], active: d
|
||||
choices = tuple(EventPlacement(n, tuple((p, exact_positions[p['id']]) for p in n['pitches']), positions if n is chord else ()) for n in notes)
|
||||
if not playable_choices(choices):
|
||||
continue
|
||||
cost = voicing_cost(shape, profile, requirements.allowed, melody) if fresh else VoicingCost()
|
||||
cost = voicing_cost(shape, profile, requirements.allowed, melody, composition=composition) if fresh else VoicingCost()
|
||||
yield AttackCandidate(assignment, choices, shape, cost)
|
||||
|
||||
def guitar_events(document: dict) -> tuple[list, dict[str, GuitarTone]]:
|
||||
@@ -325,7 +329,8 @@ def advance_attacks(candidates, states: dict) -> dict:
|
||||
result[candidate.active] = Placement(cost, candidate.active, previous, candidate.choices)
|
||||
return result
|
||||
|
||||
def advance_positions(notes: list[dict], onset: fractions.Fraction, tones: dict[str, GuitarTone], states: dict, preserve_strings: bool, fallback: bool = False) -> dict:
|
||||
def advance_positions(notes: list[dict], onset: fractions.Fraction, tones: dict[str, GuitarTone], states: dict, preserve_strings: bool, fallback: bool = False, *, composition=None) -> dict:
|
||||
composition = composition or generator.vocabulary.current()
|
||||
continued = continued_sources(notes)
|
||||
fresh = has_attack(notes)
|
||||
held_ids = {i for previous in states.values() for i, _ in previous.active if retain_tone(tones[i], onset)}
|
||||
@@ -337,29 +342,30 @@ def advance_positions(notes: list[dict], onset: fractions.Fraction, tones: dict[
|
||||
result[candidate.active] = Placement(cost, candidate.active, previous, candidate.choices)
|
||||
|
||||
if not held_ids and not continued:
|
||||
result = advance_attacks(attack_candidates(notes, tones, {}, {}, preserve_strings, fallback), states)
|
||||
result = advance_attacks(attack_candidates(notes, tones, {}, {}, preserve_strings, fallback, composition=composition), states)
|
||||
else:
|
||||
for previous in states.values():
|
||||
active = dict(previous.active)
|
||||
held = {i: p for i, p in previous.active if i not in continued and i in held_ids}
|
||||
for candidate in attack_candidates(notes, tones, active, held, preserve_strings, fallback):
|
||||
for candidate in attack_candidates(notes, tones, active, held, preserve_strings, fallback, composition=composition):
|
||||
distance = movement(tuple(p for _, p in previous.active), candidate.shape) if fresh else 0
|
||||
keep(candidate, previous, previous.cost.moved(distance))
|
||||
if not result:
|
||||
if not fallback and any(n.get('_harmonize') for n in notes):
|
||||
return advance_positions(notes, onset, tones, states, preserve_strings, True)
|
||||
return advance_positions(notes, onset, tones, states, preserve_strings, True, composition=composition)
|
||||
raise ValueError(f'event {notes[0]["id"]}, onset {onset}: no guitar placement; single notes need frets {generator.fretboard.MIN_FRET}-{generator.fretboard.MAX_SINGLE_FRET}; chords need frets {generator.fretboard.MIN_FRET}-{generator.fretboard.MAX_FRET}, an anchor <= {generator.fretboard.MAX_ANCHOR_FRET}, and at most {generator.fretboard.MAX_FRET_SPAN} consecutive fret positions; held/tied strings must remain fixed')
|
||||
return result
|
||||
|
||||
def place_guitar(document: dict) -> None:
|
||||
def place_guitar(document: dict, *, composition=None) -> None:
|
||||
"""Choose electric voicings or exact/held melody placements."""
|
||||
composition = composition or generator.vocabulary.current()
|
||||
events, tones = guitar_events(document)
|
||||
states = {(): Placement(VoicingCost(), (), None, ())}
|
||||
groups = [(key[0], [item[3] for item in values]) for key, values in itertools.groupby(events, key=lambda e: e[:3])]
|
||||
for index, (onset, notes) in enumerate(groups):
|
||||
next_onset = groups[index + 1][0] if index + 1 < len(groups) else None
|
||||
preserve_strings = next_onset is not None and any('_harmony' in n and onset + generator.model.fraction(n['duration']) > next_onset for n in notes)
|
||||
states = advance_positions(notes, onset, tones, states, preserve_strings)
|
||||
states = advance_positions(notes, onset, tones, states, preserve_strings, composition=composition)
|
||||
selected = min(states.values(), key=lambda s: (s.cost, s.active))
|
||||
skipped = []
|
||||
while selected.previous is not None:
|
||||
|
||||
Reference in New Issue
Block a user