Skip to content

Instantly share code, notes, and snippets.

@arschles

arschles/scale.go

Created Sep 28, 2020
Embed
What would you like to do?
// Package scale provides functionality to calculate chromatic scales based on
// tonics, major/minor, sharp/flat, and intervals
//
// This requires extensive music understanding
//
// This is not an easy exercise and is not production ready.
// Hic sunt dracones 🤡
package scale
import "strings"
// this is the entire chromatic scale represented in sharps format
var everythingSharps = []string{"A", "A#", "B", "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#"}
// this is the entire chromatic scale represented in flats format
var everythingFlats = []string{"A", "Bb", "B", "C", "Db", "D", "Eb", "E", "F", "Gb", "G", "Ab"}
// Scale will give you the scale back for a given tonic and interval.
// I have no idea how to explain what this thing does in music terms, so
// don't even ask. It follows some rules. The end.
//
// By the way, if you are looking to do this for an exercism problem,
// just run. Hic sunt dracones 👺
func Scale(tonic, interval string) []string {
allSharps := map[string]struct{}{
"G": {},
"D": {},
"A": {},
"B": {},
"E": {},
"F#": {},
"f#": {},
"C": {},
"a": {},
"b": {},
"c#": {},
"g#": {},
"d#": {},
}
normalized := strings.Title(tonic)
entire := startingAt(normalized, everythingFlats)
if _, ok := allSharps[tonic]; ok {
entire = startingAt(normalized, everythingSharps)
}
if interval != "" {
return applySteps(entire, interval)
}
return entire
}
// applySteps parses the given interval and returns a slice based on the given one,
// according to it, from left to right.
//
// Once again, the rules are based on music scales.
//
// A = skip 2 elements in the slice, M = skip one, m = don't skip
func applySteps(slice []string, interval string) []string {
ret := []string{}
intervalIdx := 0
for i := 0; i < len(slice); {
elt := slice[i]
ret = append(ret, elt)
curInterval := interval[intervalIdx]
if curInterval == 'M' {
i += 2
} else if curInterval == 'A' {
i += 3
} else {
i++
}
intervalIdx++
}
return ret
}
// startingAt returns a slice based on the given one, starting at the given
// string, and looping around until it reaches the original starting point
func startingAt(start string, slice []string) []string {
startIdx := 0
for i, note := range slice {
if note == start {
startIdx = i
break
}
}
ret := make([]string, len(slice))
retIdx := 0
for curIdx := startIdx; curIdx < len(slice); curIdx++ {
ret[retIdx] = slice[curIdx]
retIdx++
}
for i := 0; i < startIdx; i++ {
ret[retIdx] = slice[i]
retIdx++
}
return ret
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment