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 countEffectiveFingers(alt, len(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 { fretStrings := make(map[int][]int) for i, f := range fingering { if f == "x" || f == "0" { continue } fv := atoi(f) fretStrings[fv] = append(fretStrings[fv], i) } count := 0 for fret, strings := range fretStrings { if len(strings) == 1 { count++ continue } groups := 1 for i := 1; i < len(strings); i++ { consecutive := true for s := strings[i-1] + 1; s < strings[i]; s++ { if fingering[s] == "x" || atoi(fingering[s]) < fret { consecutive = false break } } if !consecutive { groups++ } } count += groups } 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 countEffectiveFingers(test, len(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 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) }