Span
ci / checks (push) Successful in 48m23s

This commit is contained in:
khodowany
2026-10-01 00:15:35 -05:00
parent 7b60182a3d
commit ce695a626a
10 changed files with 128 additions and 59 deletions
+2 -4
View File
@@ -9,8 +9,6 @@ import generator.acoustic
import generator.fretboard
import generator.harmony
MAX_ANCHOR_FRET = 12
class Voicing(typing.NamedTuple):
bass: generator.fretboard.TabPosition
string4: generator.fretboard.TabPosition
@@ -18,7 +16,7 @@ class Voicing(typing.NamedTuple):
string2: generator.fretboard.TabPosition
def playable(voicing: Voicing) -> bool:
return voicing.bass.string_number in (5, 6) and tuple(p.string_number for p in voicing[1:]) == (4, 3, 2) and all(generator.fretboard.MIN_FRET <= p.fret <= generator.fretboard.MAX_FRET for p in voicing) and min(p.fret for p in voicing) <= MAX_ANCHOR_FRET and generator.fretboard.within_fret_span(voicing) and voicing.bass.midi == min(p.midi for p in voicing)
return voicing.bass.string_number in (5, 6) and tuple(p.string_number for p in voicing[1:]) == (4, 3, 2) and all(generator.fretboard.MIN_FRET <= p.fret <= generator.fretboard.MAX_FRET for p in voicing) and generator.fretboard.has_fret_anchor(voicing) and generator.fretboard.within_fret_span(voicing) and voicing.bass.midi == min(p.midi for p in voicing)
@functools.lru_cache(maxsize=256)
def candidates(requirements: generator.fretboard.ChordRequirements, complete_triad: bool = False) -> tuple[Voicing, ...]:
@@ -65,7 +63,7 @@ def select(spans: list[generator.acoustic.HarmonySpan]) -> list[Voicing]:
requirements = generator.fretboard.chord_requirements(span.harmony)
options = candidates(requirements, generator.harmony.is_triad(span.harmony))
if not options:
raise ValueError(f'no acoustic grip on strings 2-4 plus bass 5 or 6 within frets {generator.fretboard.MIN_FRET}-{generator.fretboard.MAX_FRET}, three-fret span, and at least one fret <= {MAX_ANCHOR_FRET}')
raise ValueError(f'no acoustic grip on strings 2-4 plus bass 5 or 6 within frets {generator.fretboard.MIN_FRET}-{generator.fretboard.MAX_FRET}, at most {generator.fretboard.MAX_FRET_SPAN} consecutive fret positions, and at least one fret <= {generator.fretboard.MAX_ANCHOR_FRET}')
except ValueError as error:
raise ValueError(f'{span.location}, {generator.harmony.symbol(span.harmony)}: {error}') from error
states = advance(options, requirements, states)
+6 -1
View File
@@ -12,6 +12,7 @@ STANDARD_TUNING = ((6, 40), (5, 45), (4, 50), (3, 55), (2, 59), (1, 64))
MIN_FRET = 1
MAX_FRET = 20
MAX_FRET_SPAN = 3
MAX_ANCHOR_FRET = 12
WRITTEN_GUITAR_OCTAVE_OFFSET = 12
class TabPosition(typing.NamedTuple):
@@ -20,8 +21,12 @@ class TabPosition(typing.NamedTuple):
midi: int
def within_fret_span(positions) -> bool:
"""Count occupied fret positions inclusively: frets 1-3 fit, 1-4 do not."""
frets = [position.fret for position in positions]
return not frets or max(frets) - min(frets) <= MAX_FRET_SPAN
return not frets or max(frets) - min(frets) + 1 <= MAX_FRET_SPAN
def has_fret_anchor(positions) -> bool:
return any(MIN_FRET <= position.fret <= MAX_ANCHOR_FRET for position in positions)
def positions_for_pitch(midi: fractions.Fraction) -> tuple[TabPosition, ...]:
if midi.denominator != 1:
+4 -6
View File
@@ -32,12 +32,10 @@ def voicing_cost(positions: tuple[generator.fretboard.TabPosition, ...], profile
classes = {p.midi % 12 for p in positions}
above = sum(p.midi > max(melody) for p in positions) if melody else 0
span = compactness(positions)
effort = 2 * max(0, max(frets, default=0) - PREFERRED_MAX_FRET)
if profile == 'electric':
effort += max(frets, default=0) + 2 * max(0, len(frets) - 1)
effort += 3 * span
effort = 2 * max(0, len(frets) - 1) + 3 * span
else:
effort += span
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))
@functools.lru_cache(maxsize=256)
@@ -61,7 +59,7 @@ def chord_positions(requirements: generator.fretboard.ChordRequirements, prune:
by_string = {string: options[i] for i, (string, _) in enumerate(generator.fretboard.STANDARD_TUNING)}
for strings in ELECTRIC_STRINGS:
for selected in itertools.product(*(by_string[s] for s in strings)):
if not generator.fretboard.within_fret_span(selected) or not requirements.covers({p.midi % 12 for p in selected}) or not set(melody) <= {p.midi for p in selected}:
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
if requirements.basses and (selected[0].midi % 12 not in requirements.basses or selected[0].midi != min(p.midi for p in selected)):
continue
@@ -307,7 +305,7 @@ def advance_positions(notes: list[dict], onset: fractions.Fraction, tones: dict[
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)
raise ValueError(f'event {notes[0]["id"]}, onset {onset}: no guitar placement within frets {generator.fretboard.MIN_FRET}-{generator.fretboard.MAX_FRET} and three-fret span; held/tied strings must remain fixed')
raise ValueError(f'event {notes[0]["id"]}, onset {onset}: no guitar placement within frets {generator.fretboard.MIN_FRET}-{generator.fretboard.MAX_FRET} and at most {generator.fretboard.MAX_FRET_SPAN} consecutive fret positions; electric chords need a fret <= {generator.fretboard.MAX_ANCHOR_FRET}; held/tied strings must remain fixed')
return result
def place_guitar(document: dict) -> None: