package main import ( "fmt" "sort" "sync" ) type DensityChord struct { Chord string `json:"chord"` Root string `json:"root"` Quality string `json:"quality"` Frets []int `json:"frets"` Fingers int `json:"fingers"` Notes []string `json:"notes"` } type DensityCandidate struct { Tuning []string `json:"tuning"` TuningMIDI []int `json:"tuning_midi"` Root string `json:"root"` Chord string `json:"chord"` Chords []DensityChord `json:"chords"` ValidChords int `json:"valid_chords"` MajMinCount int `json:"maj_min_count"` HighCompat bool `json:"high_compat"` AvgFingers float64 `json:"avg_fingers"` } func findDensityTunings(query ShapeQuery, maxFret, maxFingers int) ([]DensityCandidate, error) { tunings, err := findCandidateTunings(query) if err != nil { return nil, err } nStrings := len(query.Shape.Frets) results := make([]DensityCandidate, len(tunings)) var wg sync.WaitGroup for idx, ct := range tunings { wg.Add(1) go func(i int, t candidateTuning) { defer wg.Done() chords := scoreTuningDensity(t.tuningMIDI, maxFret, maxFingers) tuningNames := make([]string, nStrings) for s := 0; s < nStrings; s++ { tuningNames[s] = midiToNoteName(t.tuningMIDI[s]) } majMin := 0 totalFingers := 0 for _, c := range chords { if c.Quality == "major" || c.Quality == "minor" { majMin++ } totalFingers += c.Fingers } avg := 0.0 if len(chords) > 0 { avg = float64(totalFingers) / float64(len(chords)) } results[i] = DensityCandidate{ Tuning: tuningNames, TuningMIDI: t.tuningMIDI, Root: SemitoneToNote[t.root], Chord: SemitoneToNote[t.root] + " " + query.TargetQuality, Chords: chords, ValidChords: len(chords), MajMinCount: majMin, HighCompat: majMin > 5, AvgFingers: avg, } }(idx, ct) } wg.Wait() sort.Slice(results, func(i, j int) bool { if results[i].ValidChords != results[j].ValidChords { return results[i].ValidChords > results[j].ValidChords } return results[i].AvgFingers < results[j].AvgFingers }) return results, nil } func scoreTuningDensity(tuningMIDI []int, maxFret, maxFingers int) []DensityChord { nStrings := len(tuningMIDI) defs := GetChordDefinitions() type chordDef struct { quality string intervals []int } var allDefs []chordDef for _, cat := range defs { for name, ivs := range cat { allDefs = append(allDefs, chordDef{name, ivs}) } } type chordKey struct { root int quality string } best := make(map[chordKey]DensityChord) optCount := maxFret + 2 totalCombinations := 1 for i := 0; i < nStrings; i++ { totalCombinations *= optCount } fingering := make([]string, nStrings) for combo := 0; combo < totalCombinations; combo++ { tmp := combo for s := nStrings - 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 } fingers := countEffectiveFingers(fingering, nStrings) if fingers > maxFingers { continue } var soundedPCs []int frets := make([]int, nStrings) notes := make([]string, nStrings) for s := 0; s < nStrings; s++ { if fingering[s] == "x" { frets[s] = -1 notes[s] = "x" continue } fn := atoi(fingering[s]) frets[s] = fn midi := tuningMIDI[s] + fn pc := midi % 12 soundedPCs = append(soundedPCs, pc) notes[s] = midiToNoteName(midi) } if len(soundedPCs) < 2 { continue } uniquePCs := uniqueInts(soundedPCs) // try each unique PC as root, match against chord dictionary type candidate struct { root int quality string size int bassIdx int } var candidates []candidate for ri, root := range uniquePCs { ivSet := make(map[int]bool) for _, pc := range soundedPCs { ivSet[(pc-root+12)%12] = true } for _, cd := range allDefs { if len(ivSet) != len(cd.intervals) { continue } ok := true for _, iv := range cd.intervals { if !ivSet[iv] { ok = false break } } if ok { candidates = append(candidates, candidate{root, cd.quality, len(cd.intervals), ri}) } } } if len(candidates) == 0 { continue } // pick simplest: fewest intervals, bass preference pick := candidates[0] for _, c := range candidates[1:] { if c.size < pick.size || (c.size == pick.size && c.bassIdx < pick.bassIdx) { pick = c } } ck := chordKey{pick.root, pick.quality} existing, exists := best[ck] if !exists || fingers < existing.Fingers { fretsCopy := make([]int, nStrings) copy(fretsCopy, frets) notesCopy := make([]string, nStrings) copy(notesCopy, notes) best[ck] = DensityChord{ Chord: SemitoneToNote[pick.root] + " " + pick.quality, Root: SemitoneToNote[pick.root], Quality: pick.quality, Frets: fretsCopy, Fingers: fingers, Notes: notesCopy, } } } result := make([]DensityChord, 0, len(best)) for _, dc := range best { result = append(result, dc) } sort.Slice(result, func(i, j int) bool { ri := qualityRank(result[i].Quality) rj := qualityRank(result[j].Quality) if ri != rj { return ri < rj } return result[i].Fingers < result[j].Fingers }) return result } func qualityRank(q string) int { switch q { case "major": return 0 case "minor": return 1 default: return 2 } }