272 lines
10 KiB
Python
272 lines
10 KiB
Python
import os
|
|
import json
|
|
from itertools import product, combinations
|
|
from utils import load_config, export_json
|
|
|
|
def build_note_map():
|
|
base_notes = ['C', 'C#', 'D', 'D#', 'E', 'F',
|
|
'F#', 'G', 'G#', 'A', 'A#', 'B']
|
|
enharmonic_keys = ['Cb', 'B#', 'Db', 'C##', 'Eb', 'D##', 'Fb', 'E#', 'Gb', 'F##', 'Ab', 'G##', 'Bb', 'A##']
|
|
enharmonic_vals = ['B', 'C', 'C#', 'D', 'D#', 'E', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']
|
|
note_map = {}
|
|
reverse_note_map = {}
|
|
for i, note in enumerate(base_notes):
|
|
note_map[note] = i
|
|
reverse_note_map[i] = note
|
|
for enh, actual in zip(enharmonic_keys, enharmonic_vals):
|
|
note_map[enh] = note_map[actual]
|
|
return note_map, reverse_note_map
|
|
|
|
def load_json(name):
|
|
path = os.path.join("generated_data", f"{name}.json")
|
|
with open(path, "r") as f:
|
|
return json.load(f)
|
|
|
|
def count_effective_fingers(fingering, num_strings):
|
|
fretted = [(i, int(f)) for i, f in enumerate(fingering) if f not in ("x", "X", "0")]
|
|
if not fretted:
|
|
return 0
|
|
fingers_used = set()
|
|
frets = {}
|
|
for idx, fret in fretted:
|
|
if fret not in frets:
|
|
frets[fret] = []
|
|
frets[fret].append(idx)
|
|
|
|
for fret, strings in frets.items():
|
|
if len(strings) >= 2:
|
|
start = min(strings)
|
|
end = max(strings)
|
|
if end - start <= 4:
|
|
valid = True
|
|
for i in range(start, end + 1):
|
|
val = fingering[i]
|
|
if val not in ("x", "X"):
|
|
try:
|
|
if int(val) < fret:
|
|
valid = False
|
|
break
|
|
except ValueError:
|
|
valid = False
|
|
break
|
|
if valid:
|
|
fingers_used.add((fret, "barre"))
|
|
|
|
for fret, strings in frets.items():
|
|
if fret not in fingers_used:
|
|
fingers_used.add(fret)
|
|
|
|
return sum(2 if isinstance(f, tuple) and f[1] == "barre" else 1 for f in fingers_used)
|
|
|
|
def find_chord_fingerings(config):
|
|
chords = load_json("chord_definitions")
|
|
results = []
|
|
generated_fingerings = set()
|
|
|
|
string_tunings = config.get("tuning", ["G", "C", "E", "A"])
|
|
NUM_STRINGS = len(string_tunings)
|
|
MAX_FRET = config.get("frets", 4)
|
|
MAX_FINGERS = config.get("max_fingers", 3)
|
|
|
|
all_chords = {}
|
|
for chord_type, chord_group in chords.items():
|
|
for chord_name_in_group, intervals in chord_group.items():
|
|
full_chord_name = f"{chord_name_in_group.capitalize()} {chord_type.capitalize()[:-1]}"
|
|
all_chords[full_chord_name] = {
|
|
"intervals": intervals,
|
|
"type": chord_type,
|
|
"name": chord_name_in_group
|
|
}
|
|
|
|
note_map, reverse_note_map = build_note_map()
|
|
fret_options_no_x = [str(fret) for fret in range(MAX_FRET + 1)] + ["x"]
|
|
|
|
for chord_name, chord_data in all_chords.items():
|
|
intervals = chord_data["intervals"]
|
|
interval_set = set(intervals)
|
|
|
|
for test_fingering_tuple in product(fret_options_no_x, repeat=NUM_STRINGS):
|
|
test_fingering = list(test_fingering_tuple)
|
|
|
|
fretted_notes_semitones = []
|
|
for i, fret in enumerate(test_fingering):
|
|
if fret not in ("x", "X"):
|
|
tuning_note = string_tunings[i].strip()
|
|
note_semitone = (note_map[tuning_note] + int(fret)) % 12
|
|
fretted_notes_semitones.append(note_semitone)
|
|
|
|
def is_valid_mute_config(fingering):
|
|
for i, f in enumerate(fingering):
|
|
if f in ("x", "X") and i not in (0, len(fingering) - 1):
|
|
return False
|
|
return True
|
|
|
|
if not is_valid_mute_config(test_fingering) or count_effective_fingers(test_fingering, NUM_STRINGS) > MAX_FINGERS:
|
|
continue
|
|
|
|
unique_fretted_notes = sorted(list(set(fretted_notes_semitones)))
|
|
|
|
if len(unique_fretted_notes) < len(intervals):
|
|
continue
|
|
|
|
for potential_root_semitone in unique_fretted_notes:
|
|
intervals_in_fingering = set()
|
|
for note_semitone in fretted_notes_semitones:
|
|
interval = (note_semitone - potential_root_semitone) % 12
|
|
intervals_in_fingering.add(interval)
|
|
|
|
if intervals_in_fingering == interval_set:
|
|
fingering_tuple = tuple(test_fingering)
|
|
if fingering_tuple not in generated_fingerings:
|
|
root_note_name = reverse_note_map.get(potential_root_semitone, str(potential_root_semitone))
|
|
result_chord_name = f"{root_note_name} {chord_name}"
|
|
result = {
|
|
"chord": result_chord_name,
|
|
"fingering": test_fingering,
|
|
"intervals": list(interval_set),
|
|
"interval_set": interval_set
|
|
}
|
|
|
|
def detect_barres(fingering):
|
|
fretted = [(i, int(f)) for i, f in enumerate(fingering) if f not in ("x", "X", "0")]
|
|
if not fretted:
|
|
return []
|
|
frets = {}
|
|
for idx, fret in fretted:
|
|
frets.setdefault(fret, []).append(idx)
|
|
barres = []
|
|
for fret, strings in frets.items():
|
|
if len(strings) < 2:
|
|
continue
|
|
if all(
|
|
all(fingering[j] in ("x", "X") or int(fingering[j]) >= fret for j in range(NUM_STRINGS))
|
|
for j in strings
|
|
):
|
|
barres.append({"fret": fret, "strings": strings})
|
|
return barres
|
|
|
|
result["barres"] = detect_barres(test_fingering)
|
|
results.append(result)
|
|
generated_fingerings.add(fingering_tuple)
|
|
break
|
|
|
|
def count_fingers(fingering):
|
|
return sum(1 for f in fingering if f not in ("x", "X", "0"))
|
|
|
|
def is_same_chord(fingering, chord_name, string_tunings, note_map, intervals):
|
|
fretted = []
|
|
for i, f in enumerate(fingering):
|
|
if f not in ("x", "X"):
|
|
tuning_note = string_tunings[i].strip()
|
|
note = (note_map[tuning_note] + int(f)) % 12
|
|
fretted.append(note)
|
|
for root in set(fretted):
|
|
interval_set_fingering = set()
|
|
for note in fretted:
|
|
interval_set_fingering.add((note - root) % 12)
|
|
if interval_set_fingering == intervals:
|
|
return True
|
|
return False
|
|
|
|
def is_open_chord(fingering):
|
|
return all(f in ("0", "x", "X") for f in fingering)
|
|
|
|
# Filter valid configurations
|
|
results = [
|
|
r for r in results
|
|
if is_valid_mute_config(r["fingering"]) and count_effective_fingers(r["fingering"], NUM_STRINGS) <= MAX_FINGERS
|
|
]
|
|
|
|
# Group fingerings by chord name
|
|
grouped = {}
|
|
for r in results:
|
|
chord_key = r["chord"].replace(" (alt)", "")
|
|
grouped.setdefault(chord_key, []).append(r["fingering"])
|
|
|
|
final_results = []
|
|
|
|
for chord_name, fingerings in grouped.items():
|
|
checked = set()
|
|
primary = None
|
|
alternatives = []
|
|
has_fretted_primary = False
|
|
|
|
for fingering in fingerings:
|
|
key = tuple(fingering)
|
|
if key in checked:
|
|
continue
|
|
checked.add(key)
|
|
|
|
intervals = next((r["intervals"] for r in results if r["fingering"] == list(fingering) and r["chord"] == chord_name), [])
|
|
intervals = set(intervals)
|
|
is_exact = is_same_chord(fingering, chord_name, string_tunings, note_map, intervals)
|
|
is_open = is_open_chord(fingering)
|
|
is_fretted = not is_open
|
|
|
|
if is_exact:
|
|
if is_fretted:
|
|
if not has_fretted_primary:
|
|
primary = fingering
|
|
has_fretted_primary = True
|
|
elif not has_fretted_primary:
|
|
if primary is None:
|
|
primary = fingering
|
|
else:
|
|
alternatives.append(fingering)
|
|
else:
|
|
if primary is not None:
|
|
alternatives.append(fingering)
|
|
elif primary is None and not alternatives:
|
|
primary = fingering
|
|
else:
|
|
alternatives.append(fingering)
|
|
|
|
# Find simpler muted variations of the primary
|
|
if primary is not None and fingering == primary:
|
|
for n in range(1, len(fingering)):
|
|
for idxs in combinations(range(len(fingering)), n):
|
|
test = fingering[:]
|
|
for i in idxs:
|
|
test[i] = "x"
|
|
if is_valid_mute_config(test) and is_same_chord(test, chord_name, string_tunings, note_map, intervals):
|
|
alternatives.append(list(test))
|
|
|
|
if primary is None and alternatives:
|
|
primary = alternatives.pop(0)
|
|
|
|
if primary is not None:
|
|
final_results.append({
|
|
"chord": chord_name.replace(" Triad", "").replace(" Seventh", "").replace(" Sixth", "").replace(" Ext.", ""),
|
|
"fingering": primary,
|
|
"alternatives": [alt for alt in set(map(tuple, alternatives)) if list(alt) != primary and count_fingers(list(alt)) <= MAX_FINGERS]
|
|
})
|
|
|
|
return final_results
|
|
|
|
def generate_chord_positions():
|
|
return find_chord_fingerings(load_config())
|
|
|
|
def convert_to_tab(chord_results):
|
|
chord_tabs = []
|
|
for chord in chord_results:
|
|
fretted_notes = chord.get('fingering', [])
|
|
chord_name = chord.get('chord', 'Unknown')
|
|
tab = list(fretted_notes)
|
|
chord_tabs.append({
|
|
"chord": chord_name,
|
|
"tab": tab
|
|
})
|
|
|
|
def main():
|
|
config = load_config()
|
|
fingerings = find_chord_fingerings(config)
|
|
if not fingerings:
|
|
print("No fingerings found.")
|
|
else:
|
|
print(f"Found {len(fingerings)} fingerings.")
|
|
export_json(fingerings, "triad_chords")
|
|
print(json.dumps(fingerings, indent=2))
|
|
|
|
if __name__ == "__main__":
|
|
main()
|