Fixed acoustic
ci / checks (push) Successful in 48m31s

This commit is contained in:
khodowany
2026-09-30 12:14:49 -05:00
parent 7683793052
commit 10d295abfe
5 changed files with 173 additions and 26 deletions
+26 -3
View File
@@ -17,6 +17,8 @@ MIN_FRET = 1
MAX_FRET = 20
PREFERRED_MAX_FRET = 12
MAX_FRET_SPAN = 4
# Scale-length-normalized reach, in millionths, using frets 7-10 as the reference.
PREFERRED_ACOUSTIC_REACH = round(1_000_000 * (2 ** (-7 / 12) - 2 ** (-10 / 12)))
WRITTEN_GUITAR_OCTAVE_OFFSET = 12
ACOUSTIC_STRINGS = ((6, 4, 3), (6, 4, 3, 2), (5, 4, 3), (5, 3, 2), (5, 4, 3, 2))
ELECTRIC_STRINGS = ((3, 2, 1), (4, 3, 2, 1))
@@ -46,7 +48,9 @@ def voicing_cost(positions: tuple[TabPosition, ...], profile: str = '', allowed:
span = compactness(positions)
effort = 2 * max(0, max(frets, default=0) - PREFERRED_MAX_FRET)
if profile in ('acoustic', 'electric'):
effort += max(frets, default=0) + 3 * span + 2 * max(0, len(frets) - 1)
effort += max(frets, default=0) + 2 * max(0, len(frets) - 1)
if profile == 'electric':
effort += 3 * span
else:
effort += span
return VoicingCost(max(0, 4 - len(positions)) if profile == 'electric' else 0, int(bool(above)), above, effort, len(set(allowed) - classes))
@@ -55,6 +59,12 @@ def within_fret_span(positions) -> bool:
frets = [position.fret for position in positions]
return not frets or max(frets) - min(frets) <= MAX_FRET_SPAN
def physical_reach(positions: tuple[TabPosition, ...]) -> int:
if not positions:
return 0
frets = [position.fret for position in positions]
return round(1_000_000 * (2 ** (-min(frets) / 12) - 2 ** (-max(frets) / 12)))
def positions_for_pitch(midi: fractions.Fraction) -> tuple[TabPosition, ...]:
if midi.denominator != 1:
return ()
@@ -103,7 +113,7 @@ def chord_requirements(definition: dict, profile: str = '') -> ChordRequirements
if bass_pc is not None:
allowed.add(bass_pc)
required.add(bass_pc)
basses = {bass_pc} if bass_pc is not None else foundation if profile == 'acoustic' else set()
basses = {bass_pc} if bass_pc is not None else allowed if profile == 'acoustic' else set()
return ChordRequirements(tuple(sorted(required)), tuple(sorted(allowed)), tuple(sorted(foundation)), tuple(sorted(basses)))
@functools.lru_cache(maxsize=256)
@@ -146,6 +156,16 @@ def chord_positions(requirements: ChordRequirements, prune: bool = True, profile
best[center] = (rank, positions)
return tuple(sorted(item[1] for item in best.values()))
@functools.lru_cache(maxsize=256)
def acoustic_positions(requirements: ChordRequirements, complete_triad: bool = False) -> tuple[tuple[TabPosition, ...], ...]:
reaches = {shape: physical_reach(shape) for shape in chord_positions(requirements, profile='acoustic')}
limit = max(PREFERRED_ACOUSTIC_REACH, min(reaches.values(), default=PREFERRED_ACOUSTIC_REACH))
shapes = tuple(shape for shape, reach in reaches.items() if reach <= limit)
if complete_triad:
complete = tuple(shape for shape in shapes if set(requirements.allowed) <= {p.midi % 12 for p in shape})
shapes = complete or shapes
return shapes
def chord_pitches(definition: dict, positions: tuple[TabPosition, ...]) -> list[dict]:
spellings = {}
for degree, alter in generator.harmony.degrees(definition).items():
@@ -255,7 +275,10 @@ class EventPlacement:
def acoustic_candidates(span: generator.acoustic.HarmonySpan):
requirements = chord_requirements(span.harmony, 'acoustic')
for shape in chord_positions(requirements, profile='acoustic'):
shapes = acoustic_positions(requirements, set(generator.harmony.degrees(span.harmony)) == {1, 3, 5})
if shapes and physical_reach(shapes[0]) > PREFERRED_ACOUSTIC_REACH:
logging.getLogger(__name__).warning('%s: %s requires a wider acoustic reach; using the least-reaching valid grips', span.location, generator.harmony.symbol(span.harmony))
for shape in shapes:
active = tuple((str(p.string_number), p) for p in shape)
choices = tuple(EventPlacement(note, (), tuple(p for p in shape if (p.string_number >= 5) == (note['_guitar_lane'] == 'bass'))) for note in span.notes)
yield AttackCandidate(active, choices, shape, voicing_cost(shape, 'acoustic', requirements.allowed))