Fret Limit

This commit is contained in:
khodowany
2026-10-01 12:06:02 -05:00
parent 5414caf30a
commit a7b64c5361
16 changed files with 252 additions and 28 deletions
+43
View File
@@ -11,6 +11,7 @@ import generator.pitch
STANDARD_TUNING = ((6, 40), (5, 45), (4, 50), (3, 55), (2, 59), (1, 64))
MIN_FRET = 1
MAX_FRET = 20
MAX_SINGLE_FRET = 12
MAX_FRET_SPAN = 3
MAX_ANCHOR_FRET = 12
WRITTEN_GUITAR_OCTAVE_OFFSET = 12
@@ -33,6 +34,48 @@ def positions_for_pitch(midi: fractions.Fraction) -> tuple[TabPosition, ...]:
return ()
return tuple(TabPosition(string, int(midi - open_midi), int(midi)) for string, open_midi in STANDARD_TUNING if MIN_FRET <= midi - open_midi <= MAX_FRET)
def single_positions_for_pitch(midi: fractions.Fraction) -> tuple[TabPosition, ...]:
return tuple(p for p in positions_for_pitch(midi) if p.fret <= MAX_SINGLE_FRET)
def single_register(register: tuple[int, int]) -> tuple[int, int]:
return max(register[0], min(p + MIN_FRET for _, p in STANDARD_TUNING)), min(register[1], max(p + MAX_SINGLE_FRET for _, p in STANDARD_TUNING))
def playable_attack(tones) -> bool:
"""Check a played note/chord, including tails after shorter tones release.
Each item is a (position, sounding duration) pair. Independent parts and
unsounded members of an underlying grip cannot supply the chord's anchor.
"""
tones = tuple(tones)
for time in {0, *(duration for _, duration in tones if duration > 0)}:
positions = tuple(p for p, duration in tones if duration > time or time == duration == 0)
if not positions:
continue
if any(p not in positions_for_pitch(fractions.Fraction(p.midi)) for p in positions) or len({p.string_number for p in positions}) != len(positions) or not within_fret_span(positions):
return False
if len(positions) == 1:
if positions[0].fret > MAX_SINGLE_FRET:
return False
elif not has_fret_anchor(positions):
return False
return True
def validate_document(document: dict) -> None:
"""Validate generated guitar policy separately from the score wire format."""
for staff in document['staves']:
if not staff.get('tablature'):
continue
for measure in staff['measures']:
for voice in measure['voices']:
for note in voice['notes']:
tones = []
for pitch in note['pitches']:
tab = pitch['tablature']
position = TabPosition(tab['string'], tab['fret'], dict(STANDARD_TUNING)[tab['string']] + tab['fret'])
tones.append((position, generator.model.fraction(pitch['duration'])))
if not playable_attack(tones):
raise ValueError(f'{measure["id"]}, event {note["id"]}: single notes require frets {MIN_FRET}-{MAX_SINGLE_FRET}; each sounding chord requires an anchor in {MIN_FRET}-{MAX_ANCHOR_FRET} and at most {MAX_FRET_SPAN} consecutive fret positions, including held/tied tails')
def validate_tuning(state: dict, location: str) -> None:
details = state['staff_details']
if details.get('capo', 0):