+26
-3
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user