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()