Cleanup
ci / checks (push) Successful in 38m56s

This commit is contained in:
khodowany
2026-09-08 08:53:41 -05:00
parent 62abb2d3f6
commit d7919f148d
21 changed files with 1322 additions and 479 deletions
+82 -15
View File
@@ -15,6 +15,12 @@ class TabPosition(NamedTuple):
midi: int
class ChordSymbol(NamedTuple):
root: str
quality: str
bass: str
ROOTS = {
"C": 0,
"C#": 1,
@@ -37,6 +43,7 @@ ROOTS = {
def parse_root(symbol: str) -> tuple[str, str] | None:
symbol = symbol.strip()
if not symbol or symbol[0].upper() not in "ABCDEFG":
return None
length = 1
@@ -45,36 +52,97 @@ def parse_root(symbol: str) -> tuple[str, str] | None:
return symbol[:length], symbol[length:]
def parse_chord_symbol(symbol: str) -> ChordSymbol | None:
clean = symbol.strip().replace(" ", "")
base, _, slash = clean.partition("/")
parsed = parse_root(base)
if parsed is None:
return None
root, quality = parsed
bass = ""
if slash:
bass_parsed = parse_root(slash)
if bass_parsed is not None:
bass = bass_parsed[0]
return ChordSymbol(root, quality, bass)
def unique_intervals(values: tuple[int, ...]) -> tuple[int, ...]:
result: list[int] = []
seen: set[int] = set()
for value in values:
interval = mod12(value)
if interval in seen:
continue
seen.add(interval)
result.append(interval)
return tuple(result)
def color_intervals(value: str) -> tuple[int, ...]:
colors: list[int] = []
if "alt" in value:
colors.append(1)
if "b9" in value:
colors.append(1)
if "#9" in value:
colors.append(3)
if "9" in value and "b9" not in value and "#9" not in value:
colors.append(2)
if "#11" in value:
colors.append(6)
elif "11" in value:
colors.append(5)
if "b13" in value:
colors.append(8)
elif "13" in value:
colors.append(9)
return tuple(colors)
def chord_intervals(quality: str) -> tuple[int, ...]:
value = quality.lower()
value = quality.lower().replace(" ", "")
if "7b5" in value or "ø" in value or "half" in value:
return (0, 3, 6, 10)
if "dim" in value or "o" in value:
return (0, 3, 6, 9)
colors = color_intervals(value)
if "aug" in value or "+" in value:
return (0, 4, 8, 10)
return unique_intervals((0, 4, 8, 10) + colors) if ("7" in value or colors) else (0, 4, 8)
if "sus" in value:
return (0, 5, 7, 10)
if value.startswith("-") or (value.startswith("m") and not value.startswith("maj")):
return (0, 3, 7, 10 if "7" in value else 10)
if "maj7" in value or "ma7" in value or "Δ" in value:
return (0, 4, 7, 11)
return unique_intervals((0, 5, 10) + colors) if ("7" in value or colors) else (0, 5, 7)
minor = value.startswith("-") or (value.startswith("m") and not value.startswith(("maj", "ma")))
if minor:
if "6" in value and "7" not in value:
return unique_intervals((0, 3, 7, 9) + colors)
if "7" in value or colors:
return unique_intervals((0, 3, 10) + colors)
return (0, 3, 7)
if "maj" in value or value.startswith("ma") or "Δ" in value:
return unique_intervals((0, 4, 11) + colors)
if "6" in value and "7" not in value:
return (0, 4, 7, 9)
if "7" in value:
return (0, 4, 7, 10)
return unique_intervals((0, 4, 7, 9) + colors)
if "7" in value or colors:
return unique_intervals((0, 4, 10) + colors)
return (0, 4, 7)
def pitch_class_for_root(root: str) -> int | None:
return ROOTS.get(root[0].upper() + root[1:], ROOTS.get(root.upper()))
def chord_pitch_classes(symbol: str) -> tuple[int, ...]:
parsed = parse_root(symbol)
parsed = parse_chord_symbol(symbol)
if parsed is None:
return ()
root, quality = parsed
root_pc = ROOTS.get(root[0].upper() + root[1:], ROOTS.get(root.upper()))
root_pc = pitch_class_for_root(parsed.root)
if root_pc is None:
return ()
return tuple(mod12(root_pc + interval) for interval in chord_intervals(quality))
pitch_classes = [mod12(root_pc + interval) for interval in chord_intervals(parsed.quality)]
bass_pc = pitch_class_for_root(parsed.bass) if parsed.bass else None
if bass_pc is not None and bass_pc not in pitch_classes:
pitch_classes.append(bass_pc)
return tuple(pitch_classes)
def positions_for_string(string_number: int, open_midi: int, pitch_classes: tuple[int, ...]) -> list[TabPosition]:
@@ -148,4 +216,3 @@ def pitch_from_midi(midi: int) -> dict[str, int | str]:
def pitches_for_voicing(symbol: str) -> list[dict[str, int | str]]:
return [pitch_from_midi(position.midi + WRITTEN_GUITAR_OCTAVE_OFFSET) for position in find_playable_voicing(symbol)]