Additional documentation about the new notation data model.
This commit is contained in:
@ -1,19 +1,101 @@
|
||||
import std/[strutils, unicode]
|
||||
|
||||
## This notational model is based on the 12-tone modal, harmonic system
|
||||
## standard in Western music. A key concept from this is the key-agnostic
|
||||
## naming system common to Jazz and Gospel music, an expansion of the
|
||||
## "Nashville Number" system that refers to pitches by their scale degree and
|
||||
## "alteration" (for lack of a better word). For example, "flat III,"
|
||||
## "natural/perfect V."
|
||||
##
|
||||
## In this model scale degree names are always counted from the tonic,
|
||||
## regardless of mode. Alterations are based on how the note differs from the
|
||||
## note at the same scale degree in the Ionian scale that shares the same
|
||||
## tonic. We also make a distiction between the key-agnostic scale degree and
|
||||
## alteration, and the key-specific spelling of the same. For example, consider
|
||||
## the following scales with named notes and key-agnostic scale degrees:
|
||||
##
|
||||
## Key of C major
|
||||
##
|
||||
## C D E F G A B
|
||||
## 1 2 3 4 5 6 7
|
||||
##
|
||||
## Key of C minor (Aeolian)
|
||||
##
|
||||
## C D E♭ F G A♭ B♭
|
||||
## 1 2 ♭3 4 5 ♭6 ♭7
|
||||
##
|
||||
## Key of A minor
|
||||
##
|
||||
## A B C D E F G
|
||||
## 1 2 ♭3 4 5 ♭6 ♭7
|
||||
##
|
||||
## Key of A major
|
||||
##
|
||||
## A B C# D E F# G#
|
||||
## 1 2 3 4 5 6 7
|
||||
##
|
||||
## Key of G♭ Locrian
|
||||
##
|
||||
## G♭ A𝄫 B𝄫 C♭ D𝄫 E𝄫 F♭
|
||||
## 1 ♭2 ♭3 4 ♭5 ♭6 ♭7
|
||||
##
|
||||
## Key of G Ionian (major)
|
||||
##
|
||||
## G A B C D E F#
|
||||
## 1 2 3 4 5 6 7
|
||||
##
|
||||
## In these examples, C major and A minor are enharomonic (sharing the same
|
||||
## pitches), spell all of their pitches the same, but have different flavors of
|
||||
## scale degrees. G Ionian and Gb Locrian are also enharmonic, but have very
|
||||
## different spellings of the pitches and flavors of scale degrees. Most people
|
||||
## would use F# Locrian rather than Gb Locrian, because F# Locrian has the same
|
||||
## pitch spellings as the familiari relative major of G Ionian. The key point
|
||||
## for understanding this data model is that it allows the author to use either
|
||||
## and will correctly transpose, using the correctly named pitches based on the
|
||||
## mode and spelling choice for the key.
|
||||
##
|
||||
## This preservation of choices extends to non-diatonic notes as well. For
|
||||
## example, in the key of C the notes Ab and G# are enharmonically equivalent,
|
||||
## but functionally different. Ab is the flat VI, whereas G# is the
|
||||
## augmented/sharp V. There are situations where an author may prefer either of
|
||||
## these. For example, as a submediant in a walk-up progression, ## bVI-bVII-bI,
|
||||
## writing it as Cb, the bVI emphasises the whole-tone pattern of the walk up,
|
||||
## the relationship to VII. In a different progression, a IV-vdim-vi walk up,
|
||||
## the same pitch might be thought of as a sharp V approaching the VI,
|
||||
## emphasizing its role as a continuation of the V tension before the
|
||||
## resolution to the v. Again, this data model allows us to preserve the
|
||||
## notation of both Cbmaj7-Db6-Eb2 and Bb-Bdim-Cm7 in the key of Eb, notating
|
||||
## them in the key of D as Bbmaj7-C6-D2 and A-A#dim-Bbm7 respectively.
|
||||
|
||||
|
||||
type
|
||||
Note* {.pure.} = enum A, B, C, D, E, F, G
|
||||
## Notes capture the principle in western harmony of the common scales
|
||||
## having seven distinct diatonic "notes" and allows us to do arithmetic
|
||||
## with them (go up three scale degrees).
|
||||
|
||||
Pitch* = enum Af, A, Bf, B, C, Df, D, Ef, E, F, Gf, G
|
||||
## The 12 chromatic pitches, regardless key. Pitch allows us to assign
|
||||
## unique names and ordinal values to each note of the chromatic scale,
|
||||
## allowing us to do arithmetic with them.
|
||||
|
||||
NoteAlteration* = enum
|
||||
naDoubleFlat = -2, naFlat, naNatural, naSharp, naDoubleSharp
|
||||
## The supported alterations to a Note or scale number.
|
||||
|
||||
ScaleDegree* = object
|
||||
number: int
|
||||
alteration: NoteAlteration
|
||||
## A key-agnostic representation of a pitch, capturing the scale degree
|
||||
## relative to the tonic, and the alteration of that. So, for example:
|
||||
## b3 -> b (alteration) 3 (number)
|
||||
|
||||
SpelledPitch* = object
|
||||
note: Note
|
||||
alteration: NoteAlteration
|
||||
## A unique spelling of one of the chromatic pitches allowing the stylistic
|
||||
## choice to use different *Notes* to describe a single *Pitch*. For
|
||||
## example, Cb, A𝄪, and B♮ are three distinct ways to spell Pitch.B
|
||||
|
||||
Mode* = enum
|
||||
Ionian = 0,
|
||||
@ -32,21 +114,32 @@ const MajorIntervals = [0, 2, 4, 5, 7, 9, 11]
|
||||
|
||||
|
||||
func `+`*[T: Pitch or Note](val: T, steps: int): T =
|
||||
## Move up or down the diatonic scale or the chromatic scale by the given
|
||||
## number of steps.
|
||||
cast[T]((ord(val) + steps) mod (ord(high(T)) + 1))
|
||||
|
||||
|
||||
func `-`*[T: Pitch or Note](val: T, steps: int): T =
|
||||
## Move down the diatonic or chromatic scale by the given number of steps.
|
||||
var newOrd = (ord(val) - steps) mod (ord(high(T)) + 1)
|
||||
if newOrd < 0: newOrd += 12
|
||||
return cast[T](newOrd)
|
||||
|
||||
|
||||
func `-`*(a, b: Note): int =
|
||||
## Find the distance between two notes of the diatonic scale. This always
|
||||
## returns a positive distance, as if `a` was higher in pitch than `b`. For
|
||||
## example, C - D returns +6 (the distance from D3 to C4) rather than -1
|
||||
## (the distance from D4 to C4).
|
||||
result = ord(a) - ord(b)
|
||||
if result < 0: result += 7
|
||||
|
||||
|
||||
func `-`*(a, b: Pitch): int =
|
||||
## Find the distance between two notes of the chromatic scale. This always
|
||||
## returns a positive distance, as if `a` was higher in pitch than `b`. For
|
||||
## example, C - D returns +10 (the distance from D3 to C4) rather than -2
|
||||
## (the distance from D4 to C4).
|
||||
result = ord(a) - ord(b)
|
||||
if result < 0: result += 12
|
||||
|
||||
@ -86,6 +179,9 @@ func `$`*(k: Key): string =
|
||||
|
||||
|
||||
func chromaticDistanceFromTonic*(degree: ScaleDegree): int =
|
||||
## Return the number of semitones from the tonic to the given scale degree.
|
||||
## This ignores modes and key signatures and speaks in terms of intervals
|
||||
## instead.
|
||||
if degree.number < 1 or degree.number > 7:
|
||||
raise newException(ValueError, "Invalid scale degree: " & $degree.number)
|
||||
|
||||
@ -93,14 +189,17 @@ func chromaticDistanceFromTonic*(degree: ScaleDegree): int =
|
||||
|
||||
|
||||
func toPitch*(n: Note): Pitch =
|
||||
## Return the chromatic pitch for the given natural note.
|
||||
cast[Pitch]((4 + MajorIntervals[(ord(n) + 5) mod 7]) mod 12)
|
||||
|
||||
|
||||
func toPitch*(sp: SpelledPitch): Pitch =
|
||||
## Return the chromatic pitch for a given spelled note.
|
||||
cast[Pitch]((ord(sp.note.toPitch) + ord(sp.alteration) + 12) mod 12)
|
||||
|
||||
|
||||
func toNote*(p: Pitch): Note =
|
||||
## Return the natural note for a given chromatic pitch.
|
||||
case p
|
||||
of Af, A: Note.A
|
||||
of Bf, B: Note.B
|
||||
@ -111,6 +210,18 @@ func toNote*(p: Pitch): Note =
|
||||
of Gf, G: Note.D
|
||||
|
||||
|
||||
func toSpelledPitch*(p: Pitch): SpelledPitch =
|
||||
## Get a SpelledPitch version of a chromatic Pitch. Note that this always
|
||||
## uses the simplest natural or flat spelling for the Pitch (Pitch.Bf is
|
||||
## always spelled as B♭, never C𝄫)
|
||||
SpelledPitch(
|
||||
note: p.toNote,
|
||||
alteration:
|
||||
case p
|
||||
of Af, Bf, Df, Ef, Gf: naFlat
|
||||
else: naNatural)
|
||||
|
||||
|
||||
func parseMode*(str: string): Mode =
|
||||
case str.toLower.strip
|
||||
of "ionian", "major", "": Ionian
|
||||
@ -159,20 +270,10 @@ func ionianPitch*(key: Key, degreeNumber: int): Pitch =
|
||||
cast[Pitch]((ord(key.tonic.toPitch) + MajorIntervals[degreeNumber - 1]) mod 12)
|
||||
|
||||
|
||||
#[ TODO
|
||||
func spellPitch*(key: Key, p: Pitch): SpelledPitch =
|
||||
|
||||
]#
|
||||
|
||||
func toSpelledPitch*(p: Pitch): SpelledPitch =
|
||||
SpelledPitch(
|
||||
note: p.toNote,
|
||||
alteration:
|
||||
case p
|
||||
of Af, Bf, Df, Ef, Gf: naFlat
|
||||
else: naNatural)
|
||||
|
||||
func spellPitch*(key: Key, sd: ScaleDegree): SpelledPitch =
|
||||
## Given a key and scale degree, spell it correctly in that key. For example,
|
||||
## the ♭7 in C major is spelled B♭, the ♭7 in key of Db Locrian is spelled
|
||||
## B𝄫, and the ♭7 in F# major is E.
|
||||
result.note = key.tonic.note + (sd.number - 1)
|
||||
let resultingPitch = ord(ionianPitch(key, sd.number)) + ord(sd.alteration)
|
||||
result.alteration = cast[NoteAlteration](
|
||||
@ -180,6 +281,9 @@ func spellPitch*(key: Key, sd: ScaleDegree): SpelledPitch =
|
||||
|
||||
|
||||
func toScaleDegree*(key: Key, sp: SpelledPitch): ScaleDegree =
|
||||
## Determine the ScaleDegree of a pitch according to how it is spelled and
|
||||
## the key it is in. For example, Pitch.B will be the ♮2 in the key of A
|
||||
## major, the ♭2 in the key of A# major, or the #1 in the key of B♭ major.
|
||||
result.number = sp.note - key.tonic.note + 1
|
||||
result.alteration = cast[NoteAlteration](
|
||||
ord(sp.toPitch) - ord(ionianPitch(key, result.number)))
|
||||
|
Reference in New Issue
Block a user