web-tuner/fingerings.go

533 lines
10 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 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)
}