@@ -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)]
|
||||
|
||||
|
||||
@@ -121,7 +121,7 @@ def accompaniment_notes(measure: dict, chord_symbol: str, sequence: int, *, incl
|
||||
return apply_source_chord_markers(notes, measure, include_optional=include_optional), sequence
|
||||
|
||||
|
||||
def slash_notes(measure: dict, chord_symbol: str, sequence: int) -> tuple[list[dict], int]:
|
||||
def slash_notes(measure: dict, chord_symbol: str, sequence: int, *, include_optional: bool = True) -> tuple[list[dict], int]:
|
||||
capacity = measure_capacity_ticks(str(measure.get("time_signature", "4/4")))
|
||||
notes: list[dict] = []
|
||||
cursor = 0
|
||||
@@ -130,7 +130,7 @@ def slash_notes(measure: dict, chord_symbol: str, sequence: int) -> tuple[list[d
|
||||
duration = min(QUARTER_TICKS, capacity - cursor)
|
||||
notes.append(slash_note(f"arr-slash-{sequence}", duration))
|
||||
cursor += duration
|
||||
return notes, sequence
|
||||
return apply_source_chord_markers(notes, measure, include_optional=include_optional), sequence
|
||||
|
||||
|
||||
def rest_notes(measure: dict, chord_symbol: str, sequence: int, *, include_optional: bool = True) -> tuple[list[dict], int]:
|
||||
@@ -211,7 +211,7 @@ def voice_for_section(section: ArrangementSection, measure: dict, chord_symbol:
|
||||
elif section.role == "accompaniment":
|
||||
notes, sequence = accompaniment_notes(measure, chord_symbol, sequence, include_optional=include_optional)
|
||||
elif section.role == "slash":
|
||||
notes, sequence = slash_notes(measure, chord_symbol, sequence)
|
||||
notes, sequence = slash_notes(measure, chord_symbol, sequence, include_optional=include_optional)
|
||||
else:
|
||||
notes, sequence = rest_notes(measure, chord_symbol, sequence, include_optional=include_optional)
|
||||
return {"id": "voice-arranged-guitar", "tuplets": [], "notes": notes}, sequence
|
||||
|
||||
Reference in New Issue
Block a user