web-tuner/density.go

250 lines
5.4 KiB
Go

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
}
}