Chord Families

This commit is contained in:
khodowany
2026-10-06 02:57:37 -05:00
parent df83cae38c
commit e71385dad1
16 changed files with 590 additions and 126 deletions
+21 -5
View File
@@ -10,14 +10,18 @@ import typing
import generator.fretboard
import generator.model
import generator.pitch
import generator.chord_families
import generator.vocabulary
PREFERRED_MAX_FRET = 12
ELECTRIC_STRINGS = ((3, 2, 1), (4, 3, 2, 1))
class VoicingCost(typing.NamedTuple):
string_shortfall: int = 0
melody_string: int = 0
covered_melody: int = 0
covering_tones: int = 0
string_shortfall: int = 0
family_penalty: int = 0
effort: int = 0
missing_tones: int = 0
def plus(self, other: VoicingCost) -> VoicingCost:
@@ -26,8 +30,12 @@ class VoicingCost(typing.NamedTuple):
def moved(self, distance: int) -> VoicingCost:
return self._replace(effort=self.effort + distance)
@functools.lru_cache(maxsize=8192)
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)
@functools.lru_cache(maxsize=8192)
def _voicing_cost(positions, profile, allowed, melody, families):
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
@@ -36,10 +44,16 @@ def voicing_cost(positions: tuple[generator.fretboard.TabPosition, ...], profile
effort = 2 * max(0, len(frets) - 1) + 3 * span
else:
effort = 2 * max(0, max(frets, default=0) - PREFERRED_MAX_FRET) + span
return VoicingCost(max(0, 4 - len(positions)) if profile == 'electric' else 0, int(bool(above)), above, effort, len(set(allowed) - classes))
family_penalty = dict(families).get(generator.chord_families.electric(positions), 0) if positions and families else 0
melody_string = min((p.string_number - 1 for p in positions if melody and p.midi == max(melody)), default=0)
return VoicingCost(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 ()
return _chord_positions(requirements, prune, profile, melody, families)
@functools.lru_cache(maxsize=256)
def chord_positions(requirements: generator.fretboard.ChordRequirements, prune: bool = True, profile: str = '', melody: tuple[int, ...] = ()) -> tuple[tuple[generator.fretboard.TabPosition, ...], ...]:
def _chord_positions(requirements, prune, profile, melody, families):
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]
@@ -57,7 +71,9 @@ def chord_positions(requirements: generator.fretboard.ChordRequirements, prune:
if profile:
by_string = {string: options[i] for i, (string, _) in enumerate(generator.fretboard.STANDARD_TUNING)}
for strings in ELECTRIC_STRINGS:
# A lower source melody can be the top of a three-note adaptation on
# strings 2-4. It need not be buried beneath an added first-string tone.
for strings in (*ELECTRIC_STRINGS, (4, 3, 2)) if melody else ELECTRIC_STRINGS:
for selected in itertools.product(*(by_string[s] for s in strings)):
if not generator.fretboard.within_fret_span(selected) or not generator.fretboard.has_fret_anchor(selected) or not requirements.covers({p.midi % 12 for p in selected}) or not set(melody) <= {p.midi for p in selected}:
continue