477 lines
9.9 KiB
Go
477 lines
9.9 KiB
Go
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)
|
|
}
|