+41
-152
@@ -4,6 +4,8 @@ from itertools import product
|
||||
from typing import NamedTuple
|
||||
|
||||
from generator.transposition import mod12
|
||||
from generator import harmony
|
||||
from generator.model import NATURAL
|
||||
|
||||
STANDARD_TUNING = ((6, 40), (5, 45), (4, 50), (3, 55), (2, 59), (1, 64))
|
||||
WRITTEN_GUITAR_OCTAVE_OFFSET = 12
|
||||
@@ -15,134 +17,8 @@ class TabPosition(NamedTuple):
|
||||
midi: int
|
||||
|
||||
|
||||
class ChordSymbol(NamedTuple):
|
||||
root: str
|
||||
quality: str
|
||||
bass: str
|
||||
|
||||
|
||||
ROOTS = {
|
||||
"C": 0,
|
||||
"C#": 1,
|
||||
"Db": 1,
|
||||
"D": 2,
|
||||
"D#": 3,
|
||||
"Eb": 3,
|
||||
"E": 4,
|
||||
"F": 5,
|
||||
"F#": 6,
|
||||
"Gb": 6,
|
||||
"G": 7,
|
||||
"G#": 8,
|
||||
"Ab": 8,
|
||||
"A": 9,
|
||||
"A#": 10,
|
||||
"Bb": 10,
|
||||
"B": 11,
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
if len(symbol) > 1 and symbol[1] in {"#", "b"}:
|
||||
length = 2
|
||||
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().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 unique_intervals((0, 4, 8, 10) + colors) if ("7" in value or colors) else (0, 4, 8)
|
||||
if "sus" in value:
|
||||
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 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_chord_symbol(symbol)
|
||||
if parsed is None:
|
||||
return ()
|
||||
root_pc = pitch_class_for_root(parsed.root)
|
||||
if root_pc is None:
|
||||
return ()
|
||||
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 chord_pitch_classes(symbol: str | dict) -> tuple[int, ...]:
|
||||
return harmony.pitch_classes(harmony.parse_symbol(symbol) if isinstance(symbol, str) else symbol)
|
||||
|
||||
|
||||
def positions_for_string(string_number: int, open_midi: int, pitch_classes: tuple[int, ...]) -> list[TabPosition]:
|
||||
@@ -168,8 +44,23 @@ def candidate_score(candidate: tuple[TabPosition, ...], pitch_classes: tuple[int
|
||||
return missing * 1000 + span * 80 + abs(low - 5) * 12 + open_penalty * 8 + sum(position.fret for position in candidate)
|
||||
|
||||
|
||||
def find_playable_voicing(symbol: str) -> list[TabPosition]:
|
||||
pitch_classes = chord_pitch_classes(symbol)
|
||||
def find_playable_voicing(symbol: str | dict) -> list[TabPosition]:
|
||||
definition = harmony.parse_symbol(symbol) if isinstance(symbol, str) else symbol
|
||||
semantic_degrees = harmony.degrees(definition)
|
||||
voicing = dict(semantic_degrees)
|
||||
if 7 in voicing and voicing.get(5) == 0:
|
||||
del voicing[5]
|
||||
if len(voicing) > 4:
|
||||
for degree in (11, 9, 5):
|
||||
if len(voicing) > 4 and voicing.get(degree) == 0:
|
||||
del voicing[degree]
|
||||
root = definition.get('root', {'letter':'c','alter':0})
|
||||
root_pc = NATURAL[root['letter']] + root['alter']
|
||||
pitch_classes = tuple(dict.fromkeys((root_pc + harmony.degree_semitones(d,a)) % 12 for d,a in voicing.items()))
|
||||
bass = definition.get('bass')
|
||||
bass_pc = (NATURAL[bass['letter']] + bass['alter']) % 12 if bass else None
|
||||
if bass_pc is not None and bass_pc not in pitch_classes:
|
||||
pitch_classes += (bass_pc,)
|
||||
if not pitch_classes:
|
||||
return []
|
||||
|
||||
@@ -182,11 +73,13 @@ def find_playable_voicing(symbol: str) -> list[TabPosition]:
|
||||
if any(not item for item in options):
|
||||
continue
|
||||
for candidate in product(*options):
|
||||
if bass_pc is not None and min(candidate, key=lambda p:p.midi).midi % 12 != bass_pc:
|
||||
continue
|
||||
fretted = [position.fret for position in candidate if position.fret > 0]
|
||||
if fretted and max(fretted) - min(fretted) > 4:
|
||||
continue
|
||||
covered = {mod12(position.midi) for position in candidate}
|
||||
if len(covered & set(pitch_classes)) < min(3, len(set(pitch_classes))):
|
||||
if not set(pitch_classes).issubset(covered):
|
||||
continue
|
||||
score = candidate_score(candidate, pitch_classes)
|
||||
if score < best_score:
|
||||
@@ -195,24 +88,20 @@ def find_playable_voicing(symbol: str) -> list[TabPosition]:
|
||||
return sorted(best or [], key=lambda item: item.midi)
|
||||
|
||||
|
||||
def pitch_from_midi(midi: int) -> dict[str, int | str]:
|
||||
names = (
|
||||
("c", "n"),
|
||||
("c", "#"),
|
||||
("d", "n"),
|
||||
("e", "b"),
|
||||
("e", "n"),
|
||||
("f", "n"),
|
||||
("f", "#"),
|
||||
("g", "n"),
|
||||
("a", "b"),
|
||||
("a", "n"),
|
||||
("b", "b"),
|
||||
("b", "n"),
|
||||
)
|
||||
letter, accidental = names[mod12(midi)]
|
||||
return {"letter": letter, "accidental": accidental, "octave": midi // 12 - 1}
|
||||
|
||||
|
||||
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)]
|
||||
def pitches_for_voicing(symbol: str | dict) -> list[dict[str, int | str]]:
|
||||
definition = harmony.parse_symbol(symbol) if isinstance(symbol, str) else symbol
|
||||
from generator.transposition import shift_pitch
|
||||
root = definition.get('root', {'letter':'c', 'alter':0})
|
||||
spellings = {}
|
||||
for degree, alter in harmony.degrees(definition).items():
|
||||
p = shift_pitch(root, degree-1, harmony.degree_semitones(degree, alter))
|
||||
spellings[(NATURAL[p['letter']]+p['alter']) % 12] = p
|
||||
if bass := definition.get('bass'):
|
||||
spellings[(NATURAL[bass['letter']]+bass['alter']) % 12] = bass
|
||||
result = []
|
||||
for position in find_playable_voicing(definition):
|
||||
midi = position.midi + WRITTEN_GUITAR_OCTAVE_OFFSET
|
||||
p = spellings[midi % 12].copy()
|
||||
p['octave'] = (midi - NATURAL[p['letter']] - p['alter']) // 12 - 1
|
||||
result.append(p)
|
||||
return result
|
||||
|
||||
Reference in New Issue
Block a user