package main import ( "fmt" "strings" "sync" ) type ChordResult struct { Chord string `json:"chord"` Root string `json:"root"` Quality string `json:"quality"` Category string `json:"category"` Fingering []string `json:"fingering"` Alternatives [][]string `json:"alternatives"` } type chordSpec struct { fullName string quality string category string intervals []int } func findChordFingerings(cfg Config) []ChordResult { tuning := cfg.Tuning numStrings := len(tuning) maxFret := cfg.Frets maxFingers := cfg.MaxFingers defs := GetChordDefinitions() var specs []chordSpec for category, group := range defs { for name, intervals := range group { label := titleCase(category) if len(label) > 1 { label = label[:len(label)-1] } full := fmt.Sprintf("%s %s", titleCase(name), label) specs = append(specs, chordSpec{ fullName: full, quality: name, category: category, intervals: intervals, }) } } optCount := maxFret + 2 // 0..maxFret + "x" totalCombinations := 1 for i := 0; i < numStrings; i++ { totalCombinations *= optCount } type intermediateResult struct { chord string root string quality string category string fingering []string intervalSet map[int]bool } var mu sync.Mutex var allResults []intermediateResult generatedFingerings := make(map[string]bool) var wg sync.WaitGroup for _, spec := range specs { wg.Add(1) go func(sp chordSpec) { defer wg.Done() intervalSet := make(map[int]bool, len(sp.intervals)) for _, iv := range sp.intervals { intervalSet[iv] = true } var localResults []intermediateResult fingering := make([]string, numStrings) for combo := 0; combo < totalCombinations; combo++ { tmp := combo for s := numStrings - 1; s >= 0; s-- { val := tmp % optCount tmp /= optCount if val == maxFret+1 { fingering[s] = "x" } else { fingering[s] = fmt.Sprintf("%d", val) } } if !isValidMuteConfig(fingering) { continue } if countEffectiveFingers(fingering, numStrings) > maxFingers { continue } frettedSemitones := make([]int, 0, numStrings) for i, f := range fingering { if f == "x" { continue } fretNum := atoi(f) sem := (NoteToSemitone[strings.TrimSpace(tuning[i])] + fretNum) % 12 frettedSemitones = append(frettedSemitones, sem) } uniqueNotes := uniqueInts(frettedSemitones) if len(uniqueNotes) < len(sp.intervals) { continue } for _, root := range uniqueNotes { match := true intervalsFound := make(map[int]bool, len(frettedSemitones)) for _, sem := range frettedSemitones { intervalsFound[(sem-root+12)%12] = true } if len(intervalsFound) != len(intervalSet) { match = false } else { for iv := range intervalSet { if !intervalsFound[iv] { match = false break } } } if !match { continue } key := fingeringKey(fingering) mu.Lock() if generatedFingerings[key] { mu.Unlock() continue } generatedFingerings[key] = true mu.Unlock() rootName := SemitoneToNote[root] fCopy := make([]string, numStrings) copy(fCopy, fingering) localResults = append(localResults, intermediateResult{ chord: fmt.Sprintf("%s %s", rootName, sp.fullName), root: rootName, quality: sp.quality, category: sp.category, fingering: fCopy, intervalSet: intervalSet, }) break } } mu.Lock() allResults = append(allResults, localResults...) mu.Unlock() }(spec) } wg.Wait() // Group by chord name grouped := make(map[string][]intermediateResult) for _, r := range allResults { key := r.chord grouped[key] = append(grouped[key], r) } var finalResults []ChordResult for chordName, fingerings := range grouped { first := fingerings[0] checked := make(map[string]bool) var primary []string var alternatives [][]string hasFrettedPrimary := false for _, r := range fingerings { key := fingeringKey(r.fingering) if checked[key] { continue } checked[key] = true isOpen := isOpenChord(r.fingering) isFretted := !isOpen isExact := isSameChord(r.fingering, tuning, r.intervalSet) if isExact { if isFretted { if !hasFrettedPrimary { primary = r.fingering hasFrettedPrimary = true } else { alternatives = append(alternatives, r.fingering) } } else if !hasFrettedPrimary { if primary == nil { primary = r.fingering } else { alternatives = append(alternatives, r.fingering) } } else { alternatives = append(alternatives, r.fingering) } } else { if primary != nil { alternatives = append(alternatives, r.fingering) } else if primary == nil && len(alternatives) == 0 { primary = r.fingering } else { alternatives = append(alternatives, r.fingering) } } // Generate muted variations of primary if primary != nil && fingeringKey(r.fingering) == fingeringKey(primary) { muteAlts := generateMutedVariations(primary, tuning, r.intervalSet, maxFingers) alternatives = append(alternatives, muteAlts...) } } if primary == nil && len(alternatives) > 0 { primary = alternatives[0] alternatives = alternatives[1:] } if primary == nil { continue } cleanName := chordName for _, suffix := range []string{" Triad", " Sixth", " Seventh", " Ninth", " Eleventh", " Thirteenth"} { cleanName = strings.ReplaceAll(cleanName, suffix, "") } // Deduplicate alternatives seen := make(map[string]bool) seen[fingeringKey(primary)] = true var dedupAlts [][]string for _, alt := range alternatives { k := fingeringKey(alt) if seen[k] { continue } if countFingers(alt) > maxFingers { continue } seen[k] = true dedupAlts = append(dedupAlts, alt) } finalResults = append(finalResults, ChordResult{ Chord: cleanName, Root: first.root, Quality: first.quality, Category: first.category, Fingering: primary, Alternatives: dedupAlts, }) } return finalResults } func isValidMuteConfig(fingering []string) bool { for i, f := range fingering { if f == "x" && i != 0 && i != len(fingering)-1 { return false } } return true } func countEffectiveFingers(fingering []string, numStrings int) int { type fretInfo struct { fret int strings []int } frets := make(map[int][]int) for i, f := range fingering { if f == "x" || f == "0" { continue } fv := atoi(f) frets[fv] = append(frets[fv], i) } if len(frets) == 0 { return 0 } type fingerKey struct { fret int barre bool } used := make(map[fingerKey]bool) for fret, strings := range frets { if len(strings) >= 2 { start := strings[0] end := strings[len(strings)-1] for _, s := range strings { if s < start { start = s } if s > end { end = s } } if end-start <= 4 { valid := true for i := start; i <= end; i++ { if fingering[i] == "x" { continue } fv := atoi(fingering[i]) if fv < fret { valid = false break } } if valid { used[fingerKey{fret, true}] = true } } } } for fret := range frets { found := false for k := range used { if k.fret == fret { found = true break } } if !found { used[fingerKey{fret, false}] = true } } count := 0 for k := range used { if k.barre { count += 2 } else { count++ } } return count } func detectBarres(fingering []string, numStrings int) []map[string]interface{} { frets := make(map[int][]int) for i, f := range fingering { if f == "x" || f == "0" { continue } frets[atoi(f)] = append(frets[atoi(f)], i) } var barres []map[string]interface{} for fret, strings := range frets { if len(strings) < 2 { continue } valid := true for i := 0; i < numStrings; i++ { if fingering[i] == "x" { continue } fv := atoi(fingering[i]) if fv < fret { valid = false break } } if valid { barres = append(barres, map[string]interface{}{ "fret": fret, "strings": strings, }) } } return barres } func isSameChord(fingering []string, tuning []string, intervals map[int]bool) bool { var fretted []int for i, f := range fingering { if f == "x" { continue } note := (NoteToSemitone[strings.TrimSpace(tuning[i])] + atoi(f)) % 12 fretted = append(fretted, note) } roots := uniqueInts(fretted) for _, root := range roots { iSet := make(map[int]bool) for _, note := range fretted { iSet[(note-root+12)%12] = true } if len(iSet) == len(intervals) { match := true for iv := range intervals { if !iSet[iv] { match = false break } } if match { return true } } } return false } func generateMutedVariations(primary []string, tuning []string, intervals map[int]bool, maxFingers int) [][]string { n := len(primary) var results [][]string for numMute := 1; numMute < n; numMute++ { combinations(n, numMute, func(idxs []int) { test := make([]string, n) copy(test, primary) for _, i := range idxs { test[i] = "x" } if !isValidMuteConfig(test) { return } if countFingers(test) > maxFingers { return } if isSameChord(test, tuning, intervals) { cp := make([]string, n) copy(cp, test) results = append(results, cp) } }) } return results } func combinations(n, k int, fn func([]int)) { idxs := make([]int, k) for i := range idxs { idxs[i] = i } for { cp := make([]int, k) copy(cp, idxs) fn(cp) i := k - 1 for i >= 0 && idxs[i] == i+n-k { i-- } if i < 0 { break } idxs[i]++ for j := i + 1; j < k; j++ { idxs[j] = idxs[j-1] + 1 } } } func isOpenChord(fingering []string) bool { for _, f := range fingering { if f != "0" && f != "x" { return false } } return true } func countFingers(fingering []string) int { count := 0 for _, f := range fingering { if f != "x" && f != "0" { count++ } } return count } func fingeringKey(f []string) string { return strings.Join(f, ",") } func uniqueInts(s []int) []int { seen := make(map[int]bool, len(s)) var result []int for _, v := range s { if !seen[v] { seen[v] = true result = append(result, v) } } return result } func atoi(s string) int { n := 0 for _, c := range s { n = n*10 + int(c-'0') } return n } func titleCase(s string) string { if len(s) == 0 { return s } b := []byte(s) if b[0] >= 'a' && b[0] <= 'z' { b[0] -= 32 } return string(b) }