// A chord, in music, is any harmonic set of three or more notes that is heard as if sounding simultaneously.
//
// https://en.wikipedia.org/wiki/Chord_(music)
//
// # Credit
//
// Charney Kaye
// <hi@charneykaye.com>
// https://charneykaye.com
//
// XJ Music
// https://xj.io
package chord
import (
"strings"
"github.com/go-music-theory/music-theory/note"
)
// Chord in a particular key
type Chord struct {
Root note.Class
AdjSymbol note.AdjSymbol
Tones map[Interval]note.Class
Bass note.Class // Bass note for slash chords (e.g., C/E has E as bass)
}
// Of a particular key, e.g. Of("C minor 7")
func Of(name string) Chord {
c := Chord{}
c.parse(name)
return c
}
// Notes to obtain the notes from the Chord
func (this *Chord) Notes() (notes []*note.Note) {
// If there's a bass note (slash chord), add it first
if this.Bass != note.Nil {
notes = append(notes, note.OfClass(this.Bass))
// Check if bass note exists in chord tones to avoid duplication
bassInTones := false
for _, class := range this.Tones {
if class == this.Bass {
bassInTones = true
break
}
}
// Add chord tones, skipping the bass note if it was already added
forAllIn(this.Tones, func(class note.Class) {
if bassInTones && class == this.Bass {
return // Skip - bass note already added
}
notes = append(notes, note.OfClass(class))
})
} else {
// No bass note - add all chord tones normally
forAllIn(this.Tones, func(class note.Class) {
notes = append(notes, note.OfClass(class))
})
}
return
}
// Transpose a chord +/- semitones
func (this Chord) Transpose(semitones int) Chord {
transposedChord := Chord{
AdjSymbol: this.AdjSymbol,
Tones: make(map[Interval]note.Class),
}
transposedChord.Root, _ = this.Root.Step(semitones)
// Transpose bass note if it exists
if this.Bass != note.Nil {
transposedChord.Bass, _ = this.Bass.Step(semitones)
}
for interval, class := range this.Tones {
transposedChord.Tones[interval], _ = class.Step(semitones)
}
return transposedChord
}
//
// Private
//
func (this *Chord) parse(name string) {
this.Tones = make(map[Interval]note.Class)
this.Bass = note.Nil // Initialize bass note as Nil
// determine whether the name is "sharps" or "flats"
this.AdjSymbol = note.AdjSymbolOf(name)
// Check for slash chord notation (e.g., "C/E" or "Cmaj7/B")
slashIndex := strings.Index(name, "/")
if slashIndex != -1 {
// Parse bass note from slash notation
bassString := name[slashIndex+1:]
this.Bass, _ = note.RootAndRemaining(bassString)
// Parse the chord part before the slash
name = name[:slashIndex]
}
// parse the root, and keep the remaining string
this.Root, name = note.RootAndRemaining(name)
// parse the chord Form
this.parseForms(name)
}
// Chords have different Forms, such as Triad, Seventh, Extended, Added/Omitted, Specific or General.
package chord
import (
//"log"
"regexp"
"github.com/go-music-theory/music-theory/note"
)
// Form is identified by positive/negative regular expressions, and then adds/removes pitch classes by interval from the root of the chord.
type Form struct {
Name string
pos *regexp.Regexp
add FormAdd
omit FormOmit
}
// FormAdd maps an interval-from-chord-root to a +/1 semitone adjustment
type FormAdd map[Interval]int
// Special marker values for FormAdd
const (
HarmonicSeventhCents = 969 // Harmonic 7th interval: 969 cents (~9.69 semitones, 31 cents flat of minor 7th)
)
// FormOmit maps an interval-from-chord-root to omit
type FormOmit []Interval
// MatchString processes the positive/negative regular expressions to determine if this form matches a string.
func (this *Form) MatchString(s string) bool {
return this.matchPosNegString(s)
}
//
// Private
//
// Regular expression to use mid-word, gluing together form expression parts
var nExp = "[. ]*"
// Regular expressions for different utilities
var (
majorExp = "(M|maj|major|Δ)"
minorExp = "([^a-z]|^)(m|min|minor|−)"
flatExp = "(f|flat|b|♭)"
sharpExp = "(#|s|sharp)"
halfExp = "half"
omitExp = "(omit|\\-)"
dominantExp = "(^|dom|dominant)"
nondominantExp = "(non|nondom|nondominant)"
diminishedExp = "(dim|dimin|diminished|o|°)"
augmentedExp = "(aug|augment|augmented)"
suspendedExp = "(sus|susp|suspend|suspended)"
harmonicExp = "(harm|harmonic)"
halfDiminishedExp = "(ø|Ø)"
alteredExp = "alt"
)
// Regular expressions for interval modifiers (compiled once at package level)
var (
sharpIntervalExp = regexp.MustCompile(`#\s*(\d+)`)
flatIntervalExp = regexp.MustCompile(`(♭|b)\s*(\d+)`)
flatFifthPattern = regexp.MustCompile(`(f|flat|b|♭)[. ]*5`)
sharpNinthPattern = regexp.MustCompile(`(#|s|sharp)[. ]*9`)
flatSeventhPattern = regexp.MustCompile(`(b|♭)\s*7`)
)
// Common FormAdd for altered dominant chords
var alteredDominantFormAdd = FormAdd{
I3: 4, // major 3rd
I5: 6, // flat 5th (diminished 5th)
I6: 8, // sharp 5th (augmented 5th / flat 13th)
I7: 10, // dominant 7th
I9: 13, // flat 9th
I10: 15, // sharp 9th
}
// forms is an ordered set of rules to match, and corresponding chord intervals to setup.
var forms = []Form{
// Root
Form{
Name: "Basic",
add: FormAdd{
I1: 0, // root
I3: 4, // major 3rd
I5: 7, // perfect 5th
},
},
Form{
Name: "Nondominant",
pos: exp(nondominantExp),
omit: FormOmit{
I1, // no root
},
},
// Triads
Form{
Name: "Major Triad",
pos: exp("^" + majorExp + "([^a-z]|$)"),
add: FormAdd{
I3: 4, // major 3rd
I5: 7, // perfect 5th
},
},
Form{
Name: "Minor Triad",
pos: exp("^" + minorExp + "([^a-z]|$)"),
add: FormAdd{
I3: 3, // minor 3rd
I5: 7, // perfect 5th
},
},
Form{
Name: "Augmented Triad",
pos: exp("^(" + augmentedExp + "|\\+)"),
add: FormAdd{
I3: 4, // major 3rd
I5: 8, // augmented 5th
},
},
Form{
Name: "Diminished Triad",
pos: exp("^" + diminishedExp),
add: FormAdd{
I3: 3, // diminished (minor) 3rd
I5: 6, // diminished 5th
},
},
Form{
Name: "Suspended Triad",
pos: exp("^" + suspendedExp),
add: FormAdd{
I4: 5, // 4th
I5: 7, // perfect 5th
},
omit: FormOmit{
I3,
},
},
// Fifth
Form{
Name: "Power Chord",
pos: exp("^5$"),
add: FormAdd{
I5: 7, // perfect 5th
},
omit: FormOmit{
I3, // no third
},
},
Form{
Name: "Omit Fifth",
pos: exp(omitExp + nExp + "5"),
omit: FormOmit{
I5, // no fifth
},
},
Form{
Name: "Flat Fifth",
pos: exp(flatExp + nExp + "5"),
add: FormAdd{
I5: 6, // flat 5th
},
},
// Sixth
Form{
Name: "Add Sixth",
pos: exp("6"),
add: FormAdd{
I6: 9, // 6th
},
},
Form{
Name: "Augmented Sixth",
pos: exp(augmentedExp + nExp + "6"),
add: FormAdd{
I6: 10, // augmented 6th
},
},
Form{
Name: "Omit Sixth",
pos: exp(omitExp + nExp + "6"),
omit: FormOmit{I6},
},
// Seventh
Form{
Name: "Add Seventh",
pos: exp("7"),
add: FormAdd{
I7: 10, // dominant 7th
},
},
Form{
Name: "Dominant Seventh",
pos: exp(dominantExp + nExp + "7"),
add: FormAdd{
I7: 10, // dominant 7th
},
},
Form{
Name: "Altered Dominant Seventh",
pos: exp(alteredExp + nExp + "7|7" + nExp + alteredExp),
add: alteredDominantFormAdd,
},
Form{
Name: "Altered Dominant",
pos: exp("^" + alteredExp + "([^a-z]|$)"),
add: alteredDominantFormAdd,
},
Form{
Name: "Major Seventh",
pos: exp(majorExp + nExp + "7"),
add: FormAdd{
I7: 11, // major 7th
},
},
Form{
Name: "Minor Seventh",
pos: exp(minorExp + nExp + "7"),
add: FormAdd{
I7: 10, // minor 7th
},
omit: FormOmit{},
},
Form{
Name: "Diminished Seventh",
pos: exp(diminishedExp + nExp + "7"),
add: FormAdd{
I7: 9, // diminished 7th
},
},
Form{
Name: "Half Diminished Seventh",
pos: exp("(" + halfExp + nExp + diminishedExp + "|" + halfDiminishedExp + ")" + nExp + "7"),
add: FormAdd{
I3: 3, // minor 3rd
I5: 6, // diminished 5th
I7: 10, // minor 7th
},
},
Form{
Name: "Diminished Major Seventh",
pos: exp(diminishedExp + nExp + majorExp + nExp + "7"),
add: FormAdd{
// TODO
},
},
Form{
Name: "Augmented Major Seventh",
pos: exp(augmentedExp + nExp + majorExp + nExp + "7"),
add: FormAdd{
// TODO
},
},
Form{
Name: "Augmented Minor Seventh",
pos: exp(augmentedExp + nExp + minorExp + nExp + "7"),
add: FormAdd{
// TODO
},
omit: FormOmit{},
},
Form{
Name: "Harmonic Seventh",
pos: exp(harmonicExp + nExp + "7"),
add: FormAdd{
I3: 4, // major 3rd
I5: 7, // perfect 5th
I7: HarmonicSeventhCents, // harmonic 7th (special: 969 cents, not semitones)
},
},
Form{
Name: "Omit Seventh",
pos: exp(omitExp + nExp + "7"),
omit: FormOmit{
I7, // no 7th
},
},
// Ninth
Form{
Name: "Add Ninth",
pos: exp("9"),
add: FormAdd{
I9: 14, // 9th
},
},
Form{
Name: "Dominant Ninth",
pos: exp(dominantExp + nExp + "9"),
add: FormAdd{
I7: 10, // minor 7th
I9: 14, // dominant 9th
},
},
Form{
Name: "Major Ninth",
pos: exp(majorExp + nExp + "9"),
add: FormAdd{
I7: 11, // major 7th
I9: 14, // dominant 9th
},
},
Form{
Name: "Minor Ninth",
pos: exp(minorExp + nExp + "9"),
add: FormAdd{
I7: 10, // minor 7th
I9: 14, // dominant 9th
},
},
Form{
Name: "Sharp Ninth",
pos: exp(sharpExp + nExp + "9"),
add: FormAdd{
I9: 15, // sharp 9th
},
},
Form{
Name: "Omit Ninth",
pos: exp(omitExp + nExp + "9"),
omit: FormOmit{
I9, // no 9th
},
},
// Eleventh
Form{
Name: "Add Eleventh",
pos: exp("11"),
add: FormAdd{
I11: 17, // 11th
},
},
Form{
Name: "Dominant Eleventh",
pos: exp(dominantExp + nExp + "11"),
add: FormAdd{
I7: 10, // minor 7th
I9: 14, // dominant 9th
I11: 17, // dominant 11th
},
omit: FormOmit{
I3, // no 3rd
},
},
Form{
Name: "Major Eleventh",
pos: exp(majorExp + nExp + "11"),
add: FormAdd{
I7: 11, // major 7th
I9: 14, // dominant 9th
I11: 17, // dominant 11th
},
},
Form{
Name: "Minor Eleventh",
pos: exp(minorExp + nExp + "11"),
add: FormAdd{
I3: 3, // minor 3rd
I7: 10, // minor 7th
I9: 14, // dominant 9th
I11: 17, // dominant 11th
},
},
Form{
Name: "Omit Eleventh",
pos: exp(omitExp + nExp + "11"),
omit: FormOmit{
I11,
},
},
// Thirteenth
Form{
Name: "Add Thirteenth",
pos: exp("13"),
add: FormAdd{
I13: 21, // dominant 13th
},
},
Form{
Name: "Dominant Thirteenth",
pos: exp(dominantExp + nExp + "13"),
add: FormAdd{
I7: 10, // minor 7th
I9: 14, // dominant 9th
I11: 17, // dominant 11th
I13: 21, // dominant 13th
},
omit: FormOmit{
I3, // no 3rd
},
},
Form{
Name: "Major Thirteenth",
pos: exp(majorExp + nExp + "13"),
add: FormAdd{
I3: 4, // major 3rd
I7: 11, // major 7th
I9: 14, // dominant 9th
I11: 17, // dominant 11th
I13: 21, // dominant 13th
},
},
Form{
Name: "Minor Thirteenth",
pos: exp(minorExp + nExp + "13"),
add: FormAdd{
I3: 3, // minor 3rd
I7: 10, // minor 7th
I9: 14, // dominant 9th
I11: 17, // dominant 11th
I13: 21, // dominant 13th
},
},
// Lydian
Form{
Name: "Lydian",
pos: exp("lyd"),
add: FormAdd{
I3: 4, // major 3rd
I4: 6, // augmented 4th (#11)
I5: 7, // perfect 5th
I7: 11, // major 7th
},
},
Form{
Name: "Omit Lydian",
pos: exp(omitExp + nExp + "lyd"),
omit: FormOmit{
I4, // no 4th
},
},
// Specific
Form{
Name: "AlphaSpecific",
pos: exp("alpha"),
add: FormAdd{
I3: 4, // major 3rd
I4: 6, // augmented 4th
I5: 7, // perfect 5th
I7: 10, // minor 7th
},
},
Form{
Name: "BridgeSpecific",
pos: exp("bridge"),
add: FormAdd{
I2: 2, // major 2nd
I3: 4, // major 3rd
I5: 7, // perfect 5th
},
},
Form{
Name: "ComplexeSonoreSpecific",
pos: exp("(complexe|sonore)"),
add: FormAdd{
I2: 2, // major 2nd
I4: 6, // augmented 4th
I6: 9, // major 6th
I7: 11, // major 7th
},
},
Form{
Name: "DreamSpecific",
pos: exp("dream"),
add: FormAdd{
I5: 7, // perfect 5th
I6: 8, // minor 6th (augmented 5th)
I7: 10, // minor 7th
},
},
Form{
Name: "ElektraSpecific",
pos: exp("elektra"),
add: FormAdd{
I3: 4, // major 3rd
I5: 7, // perfect 5th
I6: 9, // major 6th
I7: 11, // major 7th
I9: 13, // flat 9th
},
},
Form{
Name: "FarbenSpecific",
pos: exp("farben"),
add: FormAdd{
I3: 4, // major 3rd
I5: 7, // perfect 5th
I6: 9, // major 6th
I7: 11, // major 7th
},
},
Form{
Name: "GrandmotherSpecific",
pos: exp("grandmother"),
add: FormAdd{
I2: 2, // major 2nd
I4: 5, // perfect 4th
I5: 7, // perfect 5th
I9: 14, // major 9th
},
},
Form{
Name: "MagicSpecific",
pos: exp("magic"),
add: FormAdd{
I2: 2, // major 2nd
I4: 6, // augmented 4th
I5: 7, // perfect 5th
I7: 11, // major 7th
},
},
Form{
Name: "MµSpecific",
pos: exp("µ"),
add: FormAdd{
I3: 4, // major 3rd
I4: 6, // augmented 4th
I5: 7, // perfect 5th
},
},
Form{
Name: "MysticSpecific",
pos: exp("mystic"),
add: FormAdd{
I2: 2, // major 2nd
I4: 6, // augmented 4th
I6: 9, // major 6th
I7: 11, // major 7th
},
},
Form{
Name: "NorthernLightsSpecific",
pos: exp("northern" + nExp + "light"),
add: FormAdd{
I2: 1, // minor 2nd
I3: 4, // major 3rd
I5: 6, // diminished 5th
I6: 8, // augmented 5th
},
},
Form{
Name: "PetrushkaSpecific",
pos: exp("petrush"),
add: FormAdd{
I3: 4, // major 3rd
I4: 6, // augmented 4th
I5: 7, // perfect 5th
},
},
Form{
Name: "PsalmsSpecific",
pos: exp("psalm"),
add: FormAdd{
I2: 2, // major 2nd
I3: 3, // minor 3rd
I4: 5, // perfect 4th
},
},
Form{
Name: "SoWhatSpecific",
pos: exp("so" + nExp + "what"),
add: FormAdd{
I4: 5, // perfect 4th
I5: 7, // perfect 5th
I7: 10, // minor 7th
},
},
Form{
Name: "TristanSpecific",
pos: exp("tristan"),
add: FormAdd{
I4: 6, // augmented 4th
I6: 8, // augmented 5th
I7: 10, // minor 7th
},
},
Form{
Name: "VienneseTrichordSpecific",
pos: exp("viennese" + nExp + "trichord"),
add: FormAdd{
I2: 1, // minor 2nd
I3: 3, // minor 3rd
},
},
// General
Form{
Name: "MixedIntervalGeneral",
pos: exp("mixed" + nExp + "interval"),
add: FormAdd{
I2: 2, // major 2nd
I3: 4, // major 3rd
I4: 5, // perfect 4th
I5: 7, // perfect 5th
},
},
Form{
Name: "SecundalGeneral",
pos: exp("secundal"),
add: FormAdd{
I2: 2, // major 2nd
I4: 4, // major 3rd (secundal stacking)
},
},
Form{
Name: "TertianGeneral",
pos: exp("tertian"),
add: FormAdd{
I3: 4, // major 3rd
I5: 7, // perfect 5th
},
},
Form{
Name: "QuartalGeneral",
pos: exp("quartal"),
add: FormAdd{
I4: 5, // perfect 4th
I5: 10, // minor 7th (quartal stacking)
},
},
Form{
Name: "SyntheticChordGeneral",
pos: exp("synthetic"),
add: FormAdd{
I2: 2, // major 2nd
I3: 4, // major 3rd
I5: 7, // perfect 5th
I6: 9, // major 6th
},
},
}
func exp(s string) *regexp.Regexp {
r, _ := regexp.Compile(s)
return r
}
func (this *Form) matchPosNegString(s string) bool {
if this.pos == nil {
return true
} else if this.pos.MatchString(s) {
//if this.neg != nil {
// if this.neg.MatchString(s) {
// return false
// }
//}
return true
} else {
return false
}
}
// Build the chord by processing all Forms against the given name.
func (this *Chord) parseForms(name string) {
var toDelete []Interval
for _, f := range forms {
if f.MatchString(name) {
toDelete = append(toDelete, this.applyForm(f)...)
}
}
for _, t := range toDelete {
delete(this.Tones, t)
}
// After processing all forms, apply sharp/flat interval modifiers
this.applyIntervalModifiers(name)
return
}
func (this *Chord) applyForm(f Form) (toDelete []Interval) {
for i, c := range f.add {
// Special handling for harmonic seventh (HarmonicSeventhCents marker)
if c == HarmonicSeventhCents {
// Harmonic seventh is approximately 969 cents (~9.69 semitones)
// Use custom pitch class for this microtonal interval
this.Tones[i] = calculateHarmonicSeventh(this.Root)
} else {
this.Tones[i], _ = this.Root.Step(c)
}
}
for _, t := range f.omit {
toDelete = append(toDelete, t)
}
return
}
// applyIntervalModifiers parses and applies sharp/flat modifiers to intervals
// Handles patterns like #4, ♭6, b9, #11, etc.
// Only applies modifiers that aren't already handled by existing forms
func (this *Chord) applyIntervalModifiers(name string) {
// These intervals are already handled by existing forms when they have flat/sharp modifiers
// Flat Fifth handles: ♭5, b5, flat 5, f5
// Sharp Ninth handles: #9, sharp 9, s9
// Add Seventh handles: 7 (and this includes when preceded by b, as in b7)
// So we should NOT process these if they match the form patterns
// Check if patterns that existing forms handle are present
skipFlatFifth := flatFifthPattern.MatchString(name)
skipSharpNinth := sharpNinthPattern.MatchString(name)
skipFlatSeventh := flatSeventhPattern.MatchString(name)
// Find all sharp intervals
sharpMatches := sharpIntervalExp.FindAllStringSubmatch(name, -1)
for _, match := range sharpMatches {
if len(match) >= 2 {
intervalNum := parseIntervalNumber(match[1])
if intervalNum > 0 {
if intervalNum == I9 && skipSharpNinth {
continue // Skip #9 as it's handled by Sharp Ninth form
}
this.modifyInterval(intervalNum, 1) // +1 semitone
}
}
}
// Find all flat intervals
flatMatches := flatIntervalExp.FindAllStringSubmatch(name, -1)
for _, match := range flatMatches {
if len(match) >= 3 {
intervalNum := parseIntervalNumber(match[2])
if intervalNum > 0 {
if intervalNum == I5 && skipFlatFifth {
continue // Skip ♭5/b5 as it's handled by Flat Fifth form
}
if intervalNum == I7 && skipFlatSeventh {
continue // Skip b7/♭7 as it's standard notation for dominant 7th
}
this.modifyInterval(intervalNum, -1) // -1 semitone
}
}
}
}
// parseIntervalNumber converts a string to an Interval
func parseIntervalNumber(s string) Interval {
switch s {
case "1":
return I1
case "2":
return I2
case "3":
return I3
case "4":
return I4
case "5":
return I5
case "6":
return I6
case "7":
return I7
case "8":
return I8
case "9":
return I9
case "10":
return I10
case "11":
return I11
case "12":
return I12
case "13":
return I13
case "14":
return I14
case "15":
return I15
case "16":
return I16
default:
return 0
}
}
// modifyInterval adjusts an interval by the specified number of semitones
// If the interval doesn't exist yet, it calculates it from the default value
func (this *Chord) modifyInterval(interval Interval, semitoneDelta int) {
if existingTone, exists := this.Tones[interval]; exists {
// Modify existing interval
this.Tones[interval], _ = existingTone.Step(semitoneDelta)
} else {
// Add new interval with modification from default
defaultSemitones := getDefaultSemitones(interval)
this.Tones[interval], _ = this.Root.Step(defaultSemitones + semitoneDelta)
}
}
// getDefaultSemitones returns the default semitone distance for each interval
// These are the "natural" intervals from the root
func getDefaultSemitones(interval Interval) int {
switch interval {
case I1:
return 0 // root/unison
case I2:
return 2 // major 2nd
case I3:
return 4 // major 3rd
case I4:
return 5 // perfect 4th
case I5:
return 7 // perfect 5th
case I6:
return 9 // major 6th
case I7:
return 11 // major 7th
case I8:
return 12 // octave
case I9:
return 14 // major 9th
case I10:
return 16 // major 10th
case I11:
return 17 // perfect 11th
case I12:
return 19 // perfect 12th
case I13:
return 21 // major 13th
case I14:
return 23 // major 14th
case I15: // Note: I16 has the same value as I15
return 24 // double octave
default:
return 0
}
}
// calculateHarmonicSeventh returns the harmonic seventh pitch class for any root
// The harmonic seventh is approximately 969 cents above the root (31 cents flat of minor 7th)
func calculateHarmonicSeventh(root note.Class) note.Class {
// Map each root to its corresponding harmonic seventh pitch class
switch root {
case note.C:
return note.Ch7
case note.Cs:
return note.Csh7
case note.D:
return note.Dh7
case note.Ds:
return note.Dsh7
case note.E:
return note.Eh7
case note.F:
return note.Fh7
case note.Fs:
return note.Fsh7
case note.G:
return note.Gh7
case note.Gs:
return note.Gsh7
case note.A:
return note.Ah7
case note.As:
return note.Ash7
case note.B:
return note.Bh7
default:
// Fallback to closest chromatic approximation (10 semitones)
result, _ := root.Step(10)
return result
}
}
// A Chord Interval is how the members of the chord are counted, from 1 (the "root") to e.g. 3 (the "third") or 5 (the "fifth")
package chord
import (
"github.com/go-music-theory/music-theory/note"
)
// Interval within a chord, counted from 1 (the "root" to e.g. 3 (the "third") or 5 (the "fifth") up to 16.
type Interval int
const (
I1 Interval = 1
I2 Interval = 2
I3 Interval = 3
I4 Interval = 4
I5 Interval = 5
I6 Interval = 6
I7 Interval = 7
I8 Interval = 8
I9 Interval = 9
I10 Interval = 10
I11 Interval = 11
I12 Interval = 12
I13 Interval = 13
I14 Interval = 14
I15 Interval = 15
I16 Interval = 15
)
//
// Private
//
// forAllIn the intervals 1-16 of a chord, run the given function.
func forAllIn(setIntervals map[Interval]note.Class, callback classIteratorFunc) {
for _, i := range intervalOrder {
if class, isInSet := setIntervals[i]; isInSet {
callback(class)
}
}
}
// classIteratorFunc is run on a note class. See `ForAllIn`
type classIteratorFunc func(class note.Class)
// Order of all the intervals, e.g. for stepping from the root of a chord outward to its other tones.
var intervalOrder = []Interval{
I1,
I2,
I3,
I4,
I5,
I6,
I7,
I8,
I9,
I10,
I11,
I12,
I13,
I14,
I15,
I16,
}
// It's possible to export a list of all known chord parsing rules.
package chord
import (
"gopkg.in/yaml.v2"
)
type List []string
// ToYAML any List to an array of strings
func (l List) ToYAML() string {
out, _ := yaml.Marshal(l)
return string(out[:])
}
var ChordFormList List
//
// Private
//
func init() {
for _, f := range forms {
ChordFormList = append(ChordFormList, f.Name)
}
}
// Chords are expressed in readable strings, e.g. CMb5b7 or Cm679-5
package chord
import (
"github.com/go-music-theory/music-theory/note"
"gopkg.in/yaml.v2"
)
func (c Chord) ToYAML() string {
spec := specFrom(c)
out, _ := yaml.Marshal(spec)
return string(out[:])
}
//
// Private
//
func specFrom(c Chord) specChord {
s := specChord{}
s.Root = c.Root.String(c.AdjSymbol)
s.Tones = make(map[int]string)
for i, t := range c.Tones {
s.Tones[int(i)] = t.String(c.AdjSymbol)
}
// Include bass note if present (slash chord)
if c.Bass != note.Nil {
s.Bass = c.Bass.String(c.AdjSymbol)
}
return s
}
type specChord struct {
Root string
Bass string `yaml:"bass,omitempty"`
Tones map[int]string
}
// Key Difference to another Key can be calculated in +/- semitones
package key
// Diff to another Key calculated in +/- semitones
func (this Key) Diff(targetKey Key) int {
return this.Root.Diff(targetKey.Root)
}
// The key of a piece is a group of pitches, or scale upon which a music composition is created in classical, Western art, and Western pop music.
//
// https://en.wikipedia.org/wiki/Key_(music)
//
// # Credit
//
// Charney Kaye
// <hi@charneykaye.com>
// https://charneykaye.com
//
// XJ Music
// https://xj.io
package key
import (
"github.com/go-music-theory/music-theory/note"
)
// Of a particular key, e.g. Of("C minor 7")
func Of(name string) Key {
k := Key{}
k.parse(name)
return k
}
// Key is a model of a musical key signature
type Key struct {
Root note.Class
AdjSymbol note.AdjSymbol
Mode Mode
}
//
// Private
//
func (this *Key) parse(name string) {
// determine whether the name is "sharps" or "flats"
this.AdjSymbol = note.AdjSymbolOf(name)
// parse the root, and keep the remaining string
this.Root, name = note.RootAndRemaining(name)
// parse the key mode
this.parseMode(name)
}
// Package key provides key-finding algorithms including the Krumhansl-Schmuckler algorithm
package key
import (
"math"
"github.com/go-music-theory/music-theory/note"
)
// majorProfile contains the Krumhansl-Kessler key profile weights for major keys.
// These empirically derived weights represent the stability of each pitch class in a major key context.
// Index represents semitones from the tonic: [C, C#, D, D#, E, F, F#, G, G#, A, A#, B]
var majorProfile = []float64{6.35, 2.23, 3.48, 2.33, 4.38, 4.09, 2.52, 5.19, 2.39, 3.66, 2.29, 2.88}
// minorProfile contains the Krumhansl-Kessler key profile weights for minor keys.
// These empirically derived weights represent the stability of each pitch class in a minor key context.
// Index represents semitones from the tonic: [C, C#, D, D#, E, F, F#, G, G#, A, A#, B]
var minorProfile = []float64{6.33, 2.68, 3.52, 5.38, 2.60, 3.53, 2.54, 4.75, 3.98, 2.69, 3.34, 3.17}
// FindKey implements the Krumhansl-Schmuckler key-finding algorithm.
// It takes a slice of note.Class values and returns the most likely key.
// The algorithm:
// 1. Calculates the pitch class distribution from the input notes
// 2. Correlates this distribution with all 24 major and minor key profiles
// 3. Returns the key with the highest correlation coefficient
func FindKey(notes []note.Class) Key {
if len(notes) == 0 {
return Key{}
}
// Calculate pitch class distribution
distribution := calculatePitchClassDistribution(notes)
// Find the best matching key
var bestKey Key
var bestCorrelation = -2.0 // Correlation coefficient ranges from -1 to 1
// Check all 12 major keys
allNotes := []note.Class{note.C, note.Cs, note.D, note.Ds, note.E, note.F, note.Fs, note.G, note.Gs, note.A, note.As, note.B}
for _, root := range allNotes {
rotatedDistribution := rotateDistribution(distribution, classToSemitone(root))
correlation := correlate(rotatedDistribution, majorProfile)
if correlation > bestCorrelation {
bestCorrelation = correlation
bestKey = Key{
Root: root,
Mode: Major,
AdjSymbol: sharpOrFlat(root),
}
}
}
// Check all 12 minor keys
for _, root := range allNotes {
rotatedDistribution := rotateDistribution(distribution, classToSemitone(root))
correlation := correlate(rotatedDistribution, minorProfile)
if correlation > bestCorrelation {
bestCorrelation = correlation
bestKey = Key{
Root: root,
Mode: Minor,
AdjSymbol: sharpOrFlat(root),
}
}
}
return bestKey
}
// calculatePitchClassDistribution creates a histogram of pitch classes.
// Returns a slice of 12 floats representing the count of each pitch class (C through B).
func calculatePitchClassDistribution(notes []note.Class) []float64 {
distribution := make([]float64, 12)
for _, n := range notes {
semitone := classToSemitone(n)
if semitone >= 0 && semitone < 12 {
distribution[semitone]++
}
}
return distribution
}
// rotateDistribution rotates a pitch class distribution by the specified number of semitones.
// This allows us to transpose the distribution so a different pitch class becomes the tonic (index 0).
func rotateDistribution(distribution []float64, semitones int) []float64 {
rotated := make([]float64, 12)
for i := 0; i < 12; i++ {
rotated[i] = distribution[(i+semitones)%12]
}
return rotated
}
// correlate calculates the Pearson correlation coefficient between two distributions.
// This measures how well the observed pitch class distribution matches a key profile.
func correlate(x, y []float64) float64 {
if len(x) != len(y) || len(x) == 0 {
return -2.0 // Invalid correlation
}
// Calculate means
var sumX, sumY float64
for i := 0; i < len(x); i++ {
sumX += x[i]
sumY += y[i]
}
meanX := sumX / float64(len(x))
meanY := sumY / float64(len(y))
// Calculate correlation coefficient
var numerator, denomX, denomY float64
for i := 0; i < len(x); i++ {
diffX := x[i] - meanX
diffY := y[i] - meanY
numerator += diffX * diffY
denomX += diffX * diffX
denomY += diffY * diffY
}
if denomX == 0 || denomY == 0 {
return 0.0
}
return numerator / math.Sqrt(denomX*denomY)
}
// classToSemitone converts a note.Class to its semitone value (0-11).
// This is a helper function for the key-finding algorithm.
func classToSemitone(c note.Class) int {
switch c {
case note.C:
return 0
case note.Cs:
return 1
case note.D:
return 2
case note.Ds:
return 3
case note.E:
return 4
case note.F:
return 5
case note.Fs:
return 6
case note.G:
return 7
case note.Gs:
return 8
case note.A:
return 9
case note.As:
return 10
case note.B:
return 11
default:
return 0
}
}
// sharpOrFlat determines the appropriate accidental symbol for a given root note.
// Returns Sharp for notes typically written with sharps, Flat for notes typically written with flats.
func sharpOrFlat(root note.Class) note.AdjSymbol {
switch root {
case note.Cs, note.Ds, note.Fs, note.Gs, note.As:
// These pitch classes are black keys and can be represented as either sharps or flats
// We use flat notation for the common flat keys (Db, Eb, Gb, Ab, Bb)
return note.Flat
}
return note.Sharp
}
// Key has a Mode, e.g. Major or Minor
package key
import (
"regexp"
)
// Mode is the mode of a key, e.g. Major or Minor
type Mode int
const (
Nil Mode = iota
Major
Minor
)
// String of the Mode, e.g. "Major" or "Minor"
func (of Mode) String() string {
switch of {
case Nil:
return "Nil"
case Major:
return "Major"
case Minor:
return "Minor"
}
return ""
}
//
// Private
//
var (
rgxMajor, _ = regexp.Compile("^(M|maj|major)")
rgxMinor, _ = regexp.Compile("^(m\\b|min|minor|Minor)")
)
func (k *Key) parseMode(name string) {
// parse the chord Mode
k.Mode = modeOf(name)
}
func modeOf(name string) Mode {
switch {
case rgxMinor.MatchString(name):
return Minor
case rgxMajor.MatchString(name):
return Major
default:
return Major
}
}
// The relative minor of a major key has the same key signature and starts down a minor third (or equivalently up a major sixth); for example, the relative minor of G major is E minor. Similarly the relative major of a minor key starts up a minor third (or down a major sixth); for example, the relative major of F minor is A♭ major.
package key
import (
"github.com/go-music-theory/music-theory/note"
)
func (k Key) RelativeMinor() (rk Key) {
rk = k
if rk.Mode == Major {
rk.Mode = Minor
rk.AdjSymbol = note.Flat
rk.Root, _ = rk.Root.Step(-3)
}
return
}
func (k Key) RelativeMajor() (rk Key) {
rk = k
if rk.Mode == Minor {
rk.Mode = Major
rk.AdjSymbol = note.Sharp
rk.Root, _ = rk.Root.Step(3)
}
return
}
// Keys are expressed in readable strings, e.g. C major or Ab minor
package key
import (
"gopkg.in/yaml.v2"
)
func (k Key) ToYAML() string {
spec := specFrom(k)
out, _ := yaml.Marshal(spec)
return string(out[:])
}
//
// Private
//
func specFrom(k Key) specKey {
s := specKey{}
s.Root = k.Root.String(k.AdjSymbol)
s.Mode = k.Mode.String()
if k.Mode == Major {
rel := k.RelativeMinor()
s.Relative.Root = rel.Root.String(k.AdjSymbol)
s.Relative.Mode = rel.Mode.String()
} else if k.Mode == Minor {
rel := k.RelativeMajor()
s.Relative.Root = rel.Root.String(k.AdjSymbol)
s.Relative.Mode = rel.Mode.String()
}
return s
}
type specKey struct {
Root string
Mode string
Relative specRelativeKey
}
type specRelativeKey struct {
Root string
Mode string
}
// Notes, Keys, Chords and Scales
//
// There's an example command-line utility `music-theory.go` to demo the libraries, with a `bin/` wrapper.
//
// Build and install `music-theory` to your machine
//
// make install
//
// Determine a Chord
//
// $ music-theory chord "Cm nondominant -5 679"
//
// root: C
// tones:
// 3: D#
// 6: A
// 7: A#
// 9: D
//
// List known chord-building rules
//
// $ music-theory chords
//
// - Basic
// - Nondominant
// - Major Triad
// - Minor Triad
// - Augmented Triad
// - Diminished Triad
// - Suspended Triad
// - Omit Fifth
// - Flat Fifth
// - Add Sixth
// - Augmented Sixth
// - Omit Sixth
// - Add Seventh
// - Dominant Seventh
// - Major Seventh
// - Minor Seventh
// - Diminished Seventh
// - Half Diminished Seventh
// - Diminished Major Seventh
// - Augmented Major Seventh
// - Augmented Minor Seventh
// - Harmonic Seventh
// - Omit Seventh
// - Add Ninth
// - Dominant Ninth
// - Major Ninth
// - Minor Ninth
// - Sharp Ninth
// - Omit Ninth
// - Add Eleventh
// - Dominant Eleventh
// - Major Eleventh
// - Minor Eleventh
// - Omit Eleventh
// - Add Thirteenth
// - Dominant Thirteenth
// - Major Thirteenth
// - Minor Thirteenth
//
// Determine a Scale
//
// $ music-theory scale "C aug"
//
// root: C
// tones:
// 1: C
// 2: D#
// 3: E
// 4: G
// 5: G#
// 6: B
//
// List known scale-building rules
//
// $ music-theory scales
//
// - Default (Major)
// - Minor
// - Major
// - Natural Minor
// - Diminished
// - Augmented
// - Melodic Minor Ascend
// - Melodic Minor Descend
// - Harmonic Minor
// - Ionian
// - Dorian
// - Phrygian
// - Lydian
// - Mixolydian
// - Aeolian
// - Locrian
//
// Determine a key
//
// $ music-theory key Db
//
// root: Db
// mode: Major
// relative:
// root: Bb
// mode: Minor
//
// # Credit
//
// Charney Kaye
// <hi@charneykaye.com>
// https://charneykaye.com
//
// XJ Music
// https://xj.io
package main
import (
"fmt"
"os"
"gopkg.in/urfave/cli.v1"
"github.com/go-music-theory/music-theory/chord"
"github.com/go-music-theory/music-theory/key"
"github.com/go-music-theory/music-theory/note"
"github.com/go-music-theory/music-theory/scale"
)
func main() {
app := cli.NewApp()
app.EnableBashCompletion = true
app.Name = "music-theory"
app.Usage = "Notes, Keys, Chords and Scales"
app.Version = "0.0.4"
app.Authors = []cli.Author{cli.Author{Name: "Charney Kaye", Email: "hi@charneykaye.com"}}
app.Flags = []cli.Flag{
cli.Float64Flag{
Name: "tuning",
Value: 440.0,
Usage: "tuning frequency for A4 in Hz (e.g., 440 for standard, 432 for Verdi)",
},
}
app.Commands = commands
app.Run(os.Args)
}
var commands = []cli.Command{
{ // Build a Chord
Name: "chord",
Aliases: []string{"c"},
Usage: "build a Chord",
Description: "Chord is a named harmonic set of three or more pitch classes specified by a name, e.g. C or Cm6 or D♭m679-5",
Action: func(c *cli.Context) {
name := c.Args().First()
if len(name) > 0 {
fmt.Printf("%s", chord.Of(name).ToYAML())
} else {
// no arguments
cli.ShowCommandHelp(c, "chord")
}
},
},
{ // List all Chords
Name: "chords",
Usage: "list all known Chords",
Description: "The Chord DNA is this software is a sequential chain of rules to be executed by matching text in the chord name to its musical implications from the root of the chord.",
Action: func(c *cli.Context) {
fmt.Printf("%s", chord.ChordFormList.ToYAML())
},
},
{ // Build a Scale
Name: "scale",
Aliases: []string{"c"},
Usage: "build a Scale",
Description: "Scale is any set of musical notes ordered by fundamental frequency or pitch specified by a name, e.g. C or Cm6 or D♭m679-5",
Action: func(c *cli.Context) {
name := c.Args().First()
if len(name) > 0 {
fmt.Printf("%s", scale.Of(name).ToYAML())
} else {
// no arguments
cli.ShowCommandHelp(c, "scale")
}
},
},
{ // List all Scales
Name: "scales",
Usage: "list all known Scales",
Description: "The Scale DNA is this software is a sequential chain of rules to be executed by matching text in the scale name to its musical implications from the root of the scale.",
Action: func(c *cli.Context) {
fmt.Printf("%s", scale.ScaleModeList.ToYAML())
},
},
{ // Find a Key
Name: "key",
Aliases: []string{"k"},
Usage: "find a Key",
Description: "The key of a piece is a group of pitches, or scale upon which a music composition is created in classical, Western art, and Western pop music.",
Action: func(c *cli.Context) {
name := c.Args().First()
if len(name) > 0 {
fmt.Printf("%s", key.Of(name).ToYAML())
} else {
// no arguments
cli.ShowCommandHelp(c, "key")
}
},
},
{ // Calculate Pitch
Name: "pitch",
Aliases: []string{"p"},
Usage: "calculate pitch frequency in Hz for a note",
Description: "Calculate the pitch frequency in Hz for a note with optional tuning. Supports formats like 'A4' or 'A 4'.",
Action: func(c *cli.Context) {
args := c.Args()
if !args.Present() {
// no arguments
cli.ShowCommandHelp(c, "pitch")
return
}
// Get tuning from global flag
tuning := note.Tuning(c.GlobalFloat64("tuning"))
// Parse note - support both "A4" and "A 4" formats
var noteName string
if len(args) == 1 {
// Single argument like "A4"
noteName = args.First()
} else {
// Two or more arguments like "A 4"
noteName = args.Get(0) + args.Get(1)
}
// Create note and calculate pitch
n := note.Named(noteName)
frequency := n.Pitch(tuning)
// Format output similar to the example in the issue
fmt.Printf("%.2fHz\n", frequency)
},
},
}
// Note has an adjustment symbol (Sharp or Flat) to render the "accidental notes for a given name (e.g. of a chord, scale or key)
package note
import (
"regexp"
)
// AdjSymbolOf the adjustment symbol (Sharp or Flat) for a given name (e.g. of a chord, scale or key)
func AdjSymbolOf(name string) AdjSymbol {
numSharps := len(rgxSharpIn.FindAllString(name, -1))
numFlats := len(rgxFlatIn.FindAllString(name, -1))
numSharpish := len(rgxSharpishIn.FindAllString(name, -1))
numFlattish := len(rgxFlattishIn.FindAllString(name, -1))
// sharp/flat has precedent over sharpish/flattish; overall default is sharp
if numSharps > 0 && numSharps > numFlats {
return Sharp
} else if numFlats > 0 {
return Flat
} else if numSharpish > 0 && numSharpish > numFlattish {
return Sharp
} else if numFlattish > 0 {
return Flat
} else {
return Sharp
}
}
// AdjSymbolBegin the adjustment symbol (Sharp or Flat) that begins a given name (e.g. the Root of a chord, scale or key)
func AdjSymbolBegin(name string) AdjSymbol {
if rgxSharpBegin.MatchString(name) {
return Sharp
} else if rgxFlatBegin.MatchString(name) {
return Flat
} else {
return No
}
}
// Expression of the "accidental notes" as either Sharps or Flats
type AdjSymbol int
const (
No AdjSymbol = iota
Sharp
Flat
)
//
// Private
//
var (
rgxSharpIn, _ = regexp.Compile("[♯#]|major")
rgxFlatIn, _ = regexp.Compile("^F|[♭b]")
rgxSharpBegin, _ = regexp.Compile("^[♯#]")
rgxFlatBegin, _ = regexp.Compile("^[♭b]")
rgxSharpishIn, _ = regexp.Compile("(M|maj|major|aug)")
rgxFlattishIn, _ = regexp.Compile("([^a-z]|^)(m|min|minor|dim)")
)
// In music, a pitch class is a set of all pitches that are a whole number of octaves apart, e.g., the pitch class C consists of the Cs in all octaves.
package note
// Class of pitch for a note (across all octaves)
type Class int
const (
Nil Class = iota
C
Cs
D
Ds
E
F
Fs
G
Gs
A
As
B
// Microtonal/custom pitches (not part of 12-tone chromatic scale)
// Harmonic seventh pitches - approximately 969 cents above their respective roots (31 cents below the minor 7th)
// Notation: The '♮7' suffix is a custom notation indicating "natural harmonic 7th" to distinguish
// from tempered intervals. Not to be confused with the natural sign (♮) used to cancel accidentals.
Ch7 // C harmonic 7th - between A and A# (A super-sharp)
Csh7 // C# harmonic 7th - between A# and B
Dh7 // D harmonic 7th - between B and C
Dsh7 // D# harmonic 7th - between C and C#
Eh7 // E harmonic 7th - between C# and D
Fh7 // F harmonic 7th - between D and D#
Fsh7 // F# harmonic 7th - between D# and E
Gh7 // G harmonic 7th - between E and F
Gsh7 // G# harmonic 7th - between F and F#
Ah7 // A harmonic 7th - between F# and G
Ash7 // A# harmonic 7th - between G and G#
Bh7 // B harmonic 7th - between G# and A
)
// NameOf a note will return its Class and Octave
func NameOf(text string) (Class, Octave) {
return baseNameOf(text).Step(baseStepOf(text))
}
// Step from a class to another class, +/- semitones, +/- octave
func (from Class) Step(inc int) (Class, Octave) {
return stepFrom(from, inc)
}
// String of the note, expressed with Sharps or Flats
func (from Class) String(with AdjSymbol) string {
return stringOf(from, with)
}
//
// Private
//
func stringOf(from Class, with AdjSymbol) string {
switch from {
case C:
return "C"
case D:
return "D"
case E:
return "E"
case F:
return "F"
case G:
return "G"
case A:
return "A"
case B:
return "B"
}
if with == Sharp {
return stringSharpOf(from)
} else if with == Flat {
return stringFlatOf(from)
}
return "-"
}
func stringSharpOf(from Class) string {
switch from {
case Cs:
return "C#"
case Ds:
return "D#"
case Fs:
return "F#"
case Gs:
return "G#"
case As:
return "A#"
// Harmonic seventh pitches
case Ch7:
return "A♮7"
case Csh7:
return "A#♮7"
case Dh7:
return "B♮7"
case Dsh7:
return "C♮7"
case Eh7:
return "C#♮7"
case Fh7:
return "D♮7"
case Fsh7:
return "D#♮7"
case Gh7:
return "E♮7"
case Gsh7:
return "F♮7"
case Ah7:
return "F#♮7"
case Ash7:
return "G♮7"
case Bh7:
return "G#♮7"
}
return "-"
}
func stringFlatOf(from Class) string {
switch from {
case Cs:
return "Db"
case Ds:
return "Eb"
case Fs:
return "Gb"
case Gs:
return "Ab"
case As:
return "Bb"
// Harmonic seventh pitches (flat notation)
case Ch7:
return "B♭♭7"
case Csh7:
return "B♭7"
case Dh7:
return "C♭7"
case Dsh7:
return "D♭♭7"
case Eh7:
return "D♭7"
case Fh7:
return "E♭♭7"
case Fsh7:
return "E♭7"
case Gh7:
return "F♭7"
case Gsh7:
return "G♭♭7"
case Ah7:
return "G♭7"
case Ash7:
return "A♭♭7"
case Bh7:
return "A♭7"
}
return "-"
}
func baseNameOf(text string) Class {
if len(text) > 0 {
switch text[0:1] {
case "C":
return C
case "D":
return D
case "E":
return E
case "F":
return F
case "G":
return G
case "A":
return A
case "B":
return B
default:
return Nil
}
} else {
return Nil
}
}
func baseStepOf(text string) int {
if len(text) < 2 {
return 0
}
switch AdjSymbolBegin(text[1:]) {
case Sharp:
return 1
case Flat:
return -1
default:
return 0
}
}
func stepFrom(name Class, inc int) (Class, Octave) {
if inc > 0 {
return stepFromUp(name, inc)
} else if inc < 0 {
return stepFromDown(name, -inc)
}
return name, 0
}
func stepFromUp(name Class, inc int) (Class, Octave) {
octave := Octave(0)
for i := 0; i < inc; i++ {
shift := stepUp[name]
name = shift.Name
octave += shift.Octave
}
return name, octave
}
func stepFromDown(name Class, inc int) (Class, Octave) {
octave := Octave(0)
for i := 0; i < inc; i++ {
shift := stepDown[name]
name = shift.Name
octave += shift.Octave
}
return name, octave
}
type step struct {
Name Class
Octave Octave
}
var (
stepUp = map[Class]step{
Nil: step{Nil, 0},
C: step{Cs, 0},
Cs: step{D, 0},
D: step{Ds, 0},
Ds: step{E, 0},
E: step{F, 0},
F: step{Fs, 0},
Fs: step{G, 0},
G: step{Gs, 0},
Gs: step{A, 0},
A: step{As, 0},
As: step{B, 0},
B: step{C, 1},
// Harmonic seventh pitches step up to their closest chromatic neighbor
Ch7: step{As, 0},
Csh7: step{B, 0},
Dh7: step{C, 0},
Dsh7: step{Cs, 0},
Eh7: step{D, 0},
Fh7: step{Ds, 0},
Fsh7: step{E, 0},
Gh7: step{F, 0},
Gsh7: step{Fs, 0},
Ah7: step{G, 0},
Ash7: step{Gs, 0},
Bh7: step{A, 0},
}
stepDown = map[Class]step{
Nil: step{Nil, 0},
C: step{B, -1},
Cs: step{C, 0},
D: step{Cs, 0},
Ds: step{D, 0},
E: step{Ds, 0},
F: step{E, 0},
Fs: step{F, 0},
G: step{Fs, 0},
Gs: step{G, 0},
A: step{Gs, 0},
As: step{A, 0},
B: step{As, 0},
// Harmonic seventh pitches step down to their closest chromatic neighbor
Ch7: step{A, 0},
Csh7: step{As, 0},
Dh7: step{B, 0},
Dsh7: step{C, 0},
Eh7: step{Cs, 0},
Fh7: step{D, 0},
Fsh7: step{Ds, 0},
Gh7: step{E, 0},
Gsh7: step{F, 0},
Ah7: step{Fs, 0},
Ash7: step{G, 0},
Bh7: step{Gs, 0},
}
)
// A Note is used to represent the relative duration and pitch of a sound.
package note
import (
"math"
)
// Diff to another pitch Class calculated in +/- semitones
func (this Class) Diff(targetPitch Class) int {
diffUp := classDiff(this, targetPitch, 1)
diffDown := classDiff(this, targetPitch, -1)
if math.Abs(float64(diffUp)) < math.Abs(float64(diffDown)) {
return diffUp
} else {
return diffDown
}
}
//
// Private
//
func classDiff(from Class, to Class, inc int) int {
if from == Nil {
panic("Cannot step semitones from Nil pitch class")
}
diff := 0
for {
if from == to {
return diff
}
diff += inc
from, _ = from.Step(inc)
}
}
// A Note is used to represent the relative duration and pitch of a sound.
//
// https://en.wikipedia.org/wiki/Musical_note
//
// # Credit
//
// Charney Kaye
// <hi@charneykaye.com>
// https://charneykaye.com
//
// XJ Music
// https://xj.io
package note
// Note models a musical note
type Note struct {
Class Class // Class of pitch
Octave Octave // Octave #
Performer string // Can be used to sort out whose Notes are whose
Position float64 // Can be used to represent time within the composition
Duration float64 // Can be used to represent time of note duration
Code string // Can be used to store any custom values
}
// Named note returns a Note model
func Named(text string) (n *Note) {
n = &Note{}
// First the name, including octave shift.
n.Class, n.Octave = NameOf(text)
// Last, add the originally named octave.
n.Octave += OctaveOf(text)
return
}
// OfClass pitch returns a Note model
func OfClass(class Class) (n *Note) {
n = &Note{}
n.Class = class
return
}
// ClassNamed returns a pitch Class
func ClassNamed(text string) Class {
n := Named(text)
return n.Class
}
// A perfect octave is the interval between one musical pitch and another with half or double its frequency.
package note
import (
"regexp"
"strconv"
)
// Octave models a musical octave
type Octave int
// Octave of text returns a new Octave
func OctaveOf(text string) Octave {
o, _ := strconv.Atoi(rgxOctave.FindString(text))
return Octave(o)
}
//
// Private
//
var rgxOctave *regexp.Regexp
func init() {
rgxOctave, _ = regexp.Compile("(-*[0-9]+)$")
}
// Package note provides musical note models and utilities
package note
import (
"math"
)
// Tuning represents the reference frequency for A4 (default is 440Hz)
type Tuning float64
const (
// TuningStandard represents the standard concert pitch (A4 = 440Hz)
TuningStandard Tuning = 440.0
// TuningVerdi represents Verdi's tuning (A4 = 432Hz)
TuningVerdi Tuning = 432.0
)
// Pitch returns the frequency in Hz for this note based on the given tuning.
// Uses the standard pitch formula: f(n) = tuning * 2^((n-69)/12)
// where n is the MIDI note number and 69 is the MIDI number for A4.
func (n *Note) Pitch(tuning Tuning) float64 {
if tuning == 0 {
tuning = TuningStandard
}
midiNote := n.MIDI()
return float64(tuning) * math.Pow(2.0, float64(midiNote-69)/12.0)
}
// MIDI returns the MIDI note number for this note.
// MIDI note numbers: C-1 = 0, A4 = 69, C4 (middle C) = 60
func (n *Note) MIDI() int {
// Get semitone offset from C
semitoneOffset := classToSemitone(n.Class)
// Calculate MIDI note: (octave + 1) * 12 + semitoneOffset
// We add 1 to octave because MIDI octave -1 starts at 0
return (int(n.Octave)+1)*12 + semitoneOffset
}
// classToSemitone returns the semitone offset from C for a given pitch class
func classToSemitone(c Class) int {
switch c {
case C:
return 0
case Cs:
return 1
case D:
return 2
case Ds:
return 3
case E:
return 4
case F:
return 5
case Fs:
return 6
case G:
return 7
case Gs:
return 8
case A:
return 9
case As:
return 10
case B:
return 11
default:
return 0
}
}
// Note can be the Root of a Chord, Key or Scale.
package note
import (
"regexp"
"strings"
)
//
// Private
//
var (
rgxSingle, _ = regexp.Compile("^[ABCDEFG]")
rgxDouble, _ = regexp.Compile("^[ABCDEFG][♯#♭b]")
)
// Parse all forms using Regexp's against a string
func RootAndRemaining(name string) (Class, string) {
if r := rgxDouble.FindString(name); len(r) > 0 {
return ClassNamed(r), strings.TrimSpace(name[len(r):])
}
if r := rgxSingle.FindString(name); len(r) > 0 {
return ClassNamed(r), strings.TrimSpace(name[len(r):])
}
return Nil, name
}
// A Scale Interval is how the members of the scale are counted, from 1 (the "root") to e.g. 3 (the "third") or 5 (the "fifth")
package scale
import (
"github.com/go-music-theory/music-theory/note"
)
// Interval within a scale, counted from 1 (the "root" to e.g. 3 (the "third") or 5 (the "fifth") up to 16.
type Interval int
const (
I1 Interval = 1
I2 Interval = 2
I3 Interval = 3
I4 Interval = 4
I5 Interval = 5
I6 Interval = 6
I7 Interval = 7
I8 Interval = 8
I9 Interval = 9
I10 Interval = 10
I11 Interval = 11
I12 Interval = 12
I13 Interval = 13
I14 Interval = 14
I15 Interval = 15
I16 Interval = 16
)
//
// Private
//
// forAllIn the intervals 1-16 of a scale, run the given function.
func forAllIn(setIntervals map[Interval]note.Class, callback classIteratorFunc) {
for _, i := range intervalOrder {
if class, isInSet := setIntervals[i]; isInSet {
callback(class)
}
}
}
// classIteratorFunc is run on a note class. See `ForAllIn`
type classIteratorFunc func(class note.Class)
// Order of all the intervals, e.g. for stepping from the root of a scale outward to its other tones.
var intervalOrder = []Interval{
I1,
I2,
I3,
I4,
I5,
I6,
I7,
I8,
I9,
I10,
I11,
I12,
I13,
I14,
I15,
I16,
}
// It's possible to export a list of all known scale parsing rules.
package scale
import (
"gopkg.in/yaml.v2"
)
type List []string
// ToYAML any List to an array of strings
func (l List) ToYAML() string {
out, _ := yaml.Marshal(l)
return string(out[:])
}
var ScaleModeList List
//
// Private
//
func init() {
for _, f := range modes {
ScaleModeList = append(ScaleModeList, f.Name)
}
}
// Scales have different Modes, such as Triad, Seventh, Extended, Added/Omitted, Specific or General.
package scale
import (
"regexp"
)
// Mode is identified by positive/negative regular expressions, and then adds/removes pitch classes by interval from the root of the scale.
type Mode struct {
Name string
pos *regexp.Regexp
set ModeIntervals
omit ModeOmit
}
// ModeAdd maps an interval-from-scale-root to a +/1 semitone adjustment
type ModeIntervals []int
// ModeOmit maps an interval-from-scale-root to omit
type ModeOmit []Interval
// MatchString processes the positive/negative regular expressions to determine if this mode matches a string.
func (this *Mode) MatchString(s string) bool {
return this.matchPosNegString(s)
}
//
// Private
//
// Regular expression to use mid-word, gluing together mode expression parts
var nExp = "[. ]*"
// Regular expressions for different utilities
var (
majorExp = "(M|maj|major)"
minorStrictExp = "([^a-z ]|^)(m|min|minor)"
minorExp = "(m|min|minor)"
//flatExp = "(f|flat|b|♭)"
//sharpExp = "(#|s|sharp)"
//halfExp = "half"
//omitExp = "(omit|\\-)"
naturalExp = "(nat|natural)"
melodicExp = "(mel|melodic)"
ascendExp = "(asc|ascend)"
descendExp = "(desc|descend)"
diminishedExp = "(dim|dimin|diminished)"
augmentedExp = "(aug|augment|augmented)"
harmonicExp = "(harm|harmonic)"
//dominantExp = "(^|dom|dominant)"
//nondominantExp = "(non|nondom|nondominant)"
//suspendedExp = "(sus|susp|suspend|suspended)"
locrianExp = "(loc|locrian)"
ionianExp = "(ion|ionian)"
dorianExp = "(dor|dorian)"
phrygianExp = "(phr|phrygian)"
lydianExp = "(lyd|lydian)"
mixolydianExp = "(mix|mixolydian)"
aeolianExp = "(aeo|aeolian)"
ionianIntervals = ModeIntervals{2, 2, 1, 2, 2, 2}
dorianIntervals = ModeIntervals{2, 1, 2, 2, 2, 1}
phrygianIntervals = ModeIntervals{1, 2, 2, 2, 1, 2}
lydianIntervals = ModeIntervals{2, 2, 2, 1, 2, 2}
mixolydianIntervals = ModeIntervals{2, 2, 1, 2, 2, 1}
aeolianIntervals = ModeIntervals{2, 1, 2, 2, 1, 2}
locrianIntervals = ModeIntervals{1, 2, 2, 1, 2, 2}
)
// modes is an ordered set of rules to match, and corresponding scale intervals to setup.
var modes = []Mode{
// Basic
Mode{
Name: "Default (Major)",
set: ionianIntervals,
},
Mode{
Name: "Minor",
pos: exp(minorStrictExp),
set: aeolianIntervals,
},
Mode{
Name: "Major",
pos: exp(majorExp),
set: ionianIntervals,
},
Mode{
Name: "Natural Minor",
pos: exp(naturalExp + nExp + minorExp),
set: aeolianIntervals,
},
Mode{
Name: "Diminished",
pos: exp(diminishedExp),
set: ModeIntervals{2, 1, 2, 1, 2, 1, 2},
},
Mode{
Name: "Augmented",
pos: exp(augmentedExp),
set: ModeIntervals{3, 1, 3, 1, 3},
omit: ModeOmit{I7},
},
Mode{
Name: "Melodic Minor Ascend",
pos: exp(melodicExp + nExp + minorExp + nExp + ascendExp),
set: ModeIntervals{2, 1, 2, 2, 2, 2},
},
Mode{
Name: "Melodic Minor Descend",
pos: exp(melodicExp + nExp + minorExp + nExp + descendExp),
set: ModeIntervals{2, 1, 2, 2, 1, 2},
},
Mode{
Name: "Harmonic Minor",
pos: exp(harmonicExp + nExp + minorExp),
set: ModeIntervals{2, 1, 2, 2, 1, 3},
},
Mode{
Name: "Ionian",
pos: exp(ionianExp),
set: ionianIntervals,
},
Mode{
Name: "Dorian",
pos: exp(dorianExp),
set: dorianIntervals,
},
Mode{
Name: "Phrygian",
pos: exp(phrygianExp),
set: phrygianIntervals,
},
Mode{
Name: "Lydian",
pos: exp(lydianExp),
set: lydianIntervals,
},
Mode{
Name: "Mixolydian",
pos: exp(mixolydianExp),
set: mixolydianIntervals,
},
Mode{
Name: "Aeolian",
pos: exp(aeolianExp),
set: aeolianIntervals,
},
Mode{
Name: "Locrian",
pos: exp(locrianExp),
set: locrianIntervals,
},
}
func exp(s string) *regexp.Regexp {
r, _ := regexp.Compile(s)
return r
}
func (this *Mode) matchPosNegString(s string) bool {
if this.pos == nil {
//fmt.Printf("[%s] matched %s by default", s, this.Name)
return true
} else if this.pos.MatchString(s) {
//log.Printf("[%s] matched pos %s\n", s, this.Name)
//if this.neg != nil {
// if this.neg.MatchString(s) {
// return false
// }
//}
return true
} else {
return false
}
}
// Build the scale by processing all Modes against the given name.
func (this *Scale) parseModes(name string) {
var toDelete []Interval
for _, f := range modes {
if f.MatchString(name) {
toDelete = append(toDelete, this.applyMode(f)...)
}
}
for _, t := range toDelete {
delete(this.Tones, t)
}
return
}
func (this *Scale) applyMode(f Mode) (toDelete []Interval) {
ct := I1
this.Tones[ct] = this.Root
for _, c := range f.set {
ct++
this.Tones[ct], _ = this.Tones[ct-1].Step(c)
}
for _, t := range f.omit {
toDelete = append(toDelete, t)
}
return
}
// In music theory, a scale is any set of musical notes ordered by fundamental frequency or pitch.
//
// https://en.wikipedia.org/wiki/Scale_(music)
//
// A scale ordered by increasing pitch is an ascending scale, and a scale ordered by decreasing pitch is a descending scale. Some scales contain different pitches when ascending than when descending. For example, the Melodic minor scale.
//
// # Credit
//
// Charney Kaye
// <hi@charneykaye.com>
// https://charneykaye.com
//
// XJ Music
// https://xj.io
package scale
import (
"github.com/go-music-theory/music-theory/note"
)
// Scale in a particular key
type Scale struct {
Root note.Class
AdjSymbol note.AdjSymbol
Tones map[Interval]note.Class
}
// Of a particular key, e.g. Of("C minor 7")
func Of(name string) Scale {
c := Scale{}
c.parse(name)
return c
}
// Notes to obtain the notes from the Scale
func (this *Scale) Notes() (notes []*note.Note) {
forAllIn(this.Tones, func(class note.Class) {
notes = append(notes, note.OfClass(class))
})
return
}
//
// Private
//
func (this *Scale) parse(name string) {
this.Tones = make(map[Interval]note.Class)
// determine whether the name is "sharps" or "flats"
this.AdjSymbol = note.AdjSymbolOf(name)
// parse the root, and keep the remaining string
this.Root, name = note.RootAndRemaining(name)
// parse the scale Mode
this.parseModes(name)
}
// Scales are expressed in readable strings, e.g. CMb5b7 or Cm679-5
package scale
import (
"gopkg.in/yaml.v2"
)
func (c Scale) ToYAML() string {
spec := specFrom(c)
out, _ := yaml.Marshal(spec)
return string(out[:])
}
//
// Private
//
func specFrom(c Scale) specScale {
s := specScale{}
s.Root = c.Root.String(c.AdjSymbol)
s.Tones = make(map[int]string)
for i, t := range c.Tones {
s.Tones[int(i)] = t.String(c.AdjSymbol)
}
return s
}
type specScale struct {
Root string
Tones map[int]string
}