250 lines
5.4 KiB
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
|
|
}
|
|
}
|