"
if node.word.isSome: result &= node.word.get
@@ -317,13 +278,13 @@ proc toHtml(ctx: var FormatContext, node: ChordChartNode, indent: string): strin
result &= ""
of ccnkTransposeKey:
- ctx.currentKey = ctx.currentKey + node.transposeSteps
+ ctx.currentKey = ctx.currentKey.transpose(node.transposeSteps)
result &= indent & "
Key Change: " & $ctx.currentKey & "
"
of ccnkRedefineKey:
let oldKey = ctx.currentKey
- ctx.sourceKey = node.newKey + ctx.transposeSteps
- ctx.currentKey = ctx.sourceKey
+ ctx.sourceKey = node.newKey
+ ctx.currentKey = ctx.sourceKey.transpose(ctx.transposeSteps)
if oldKey != ctx.currentKey:
let headingVal = indent & "Key Change: " & $ctx.currentKey & "
"
@@ -342,8 +303,8 @@ proc toHtml*(
var ctx = FormatContext(
chart: cc,
- currentKey: cc.metadata.key.rootPitch + transpose,
- sourceKey: cc.metadata.key.rootPitch,
+ currentKey: cc.metadata.key.transpose(transpose),
+ sourceKey: cc.metadata.key,
currentSection: EMPTY_CHORD_CHART_NODE,
numberChart: numberChart,
transposeSteps: transpose)
@@ -368,17 +329,19 @@ proc toHtml*(
result &= " \p"
+ var bodyClasses = newSeq[string]()
+ if numberChart: bodyClasses.add("number-chart")
if cc.metadata.contains("columns") and cc.metadata["columns"] == "1":
- result &= " "
- else:
- result &= " "
+ bodyClasses.add("one-column")
+ else: bodyClasses.add("two-column")
+ result &= " "
var indent = " "
# Title
result &= indent & "" & cc.metadata.title & "
\p"
- var metadataPieces = @["Key: " & ctx.format(ctx.transpose(cc.metadata.key))]
+ var metadataPieces = @["Key: " & $ctx.currentKey]
if cc.metadata.contains("time signature"):
metadataPieces.add(cc.metadata["time signature"])
if cc.metadata.contains("bpm"):
diff --git a/src/pco_chordspkg/notation b/src/pco_chordspkg/notation
new file mode 100755
index 0000000..4236165
Binary files /dev/null and b/src/pco_chordspkg/notation differ
diff --git a/src/pco_chordspkg/notation.nim b/src/pco_chordspkg/notation.nim
new file mode 100644
index 0000000..597d933
--- /dev/null
+++ b/src/pco_chordspkg/notation.nim
@@ -0,0 +1,245 @@
+import std/[strutils, unicode]
+
+type
+ Note* {.pure.} = enum A, B, C, D, E, F, G
+ Pitch* = enum Af, A, Bf, B, C, Df, D, Ef, E, F, Gf, G
+
+ NoteAlteration* = enum
+ naDoubleFlat = -2, naFlat, naNatural, naSharp, naDoubleSharp
+
+ ScaleDegree* = object
+ number: int
+ alteration: NoteAlteration
+
+ SpelledPitch* = object
+ note: Note
+ alteration: NoteAlteration
+
+ Mode* = enum
+ Ionian = 0,
+ Dorian,
+ Phrygian,
+ Lydian,
+ Mixolydian,
+ Aeolian,
+ Locrian
+
+ Key* = object
+ tonic*: SpelledPitch
+ mode*: Mode
+
+const MajorIntervals = [0, 2, 4, 5, 7, 9, 11]
+
+
+func `+`*[T: Pitch or Note](val: T, steps: int): T =
+ cast[T]((ord(val) + steps) mod (ord(high(T)) + 1))
+
+
+func `-`*[T: Pitch or Note](val: T, steps: int): T =
+ var newOrd = (ord(val) - steps) mod (ord(high(T)) + 1)
+ if newOrd < 0: newOrd += 12
+ return cast[T](newOrd)
+
+
+func `-`*(a, b: Note): int =
+ result = ord(a) - ord(b)
+ if result < 0: result += 7
+
+
+func `-`*(a, b: Pitch): int =
+ result = ord(a) - ord(b)
+ if result < 0: result += 12
+
+
+func `$`*(pitch: Pitch): string =
+ ["A♭", "A", "B♭", "B", "C", "D♭", "D", "E♭", "E", "F", "G♭", "G"][ord(pitch)]
+
+
+func `$`*(sp: SpelledPitch): string =
+ case sp.alteration
+ of naDoubleFlat: return $sp.note & "𝄫"
+ of naFlat: return $sp.note & "♭"
+ of naNatural: return $sp.note
+ of naSharp: return $sp.note & "#"
+ of naDoubleSharp: return $sp.note & "𝄪"
+
+
+func `$`*(sd: ScaleDegree): string =
+ case sd.alteration
+ of naDoubleFlat: return "𝄫" & $sd.number
+ of naFlat: return "♭" & $sd.number
+ of naNatural: return $sd.number
+ of naSharp: return "#" & $sd.number
+ of naDoubleSharp: return "𝄪" & $sd.number
+
+
+func `$`*(k: Key): string =
+ result = $k.tonic
+ case k.mode
+ of Ionian: discard
+ of Dorian: result &= " Dorian"
+ of Phrygian: result &= " Phrygian"
+ of Lydian: result &= " Lydian"
+ of Mixolydian: result &= " Mixolydian"
+ of Aeolian: result &= " Minor"
+ of Locrian: result &= " Locrian"
+
+
+func chromaticDistanceFromTonic*(degree: ScaleDegree): int =
+ if degree.number < 1 or degree.number > 7:
+ raise newException(ValueError, "Invalid scale degree: " & $degree.number)
+
+ result = MajorIntervals[(degree.number - 1)] + ord(degree.alteration)
+
+
+func toPitch*(n: Note): Pitch =
+ cast[Pitch]((4 + MajorIntervals[(ord(n) + 5) mod 7]) mod 12)
+
+
+func toPitch*(sp: SpelledPitch): Pitch =
+ cast[Pitch]((ord(sp.note.toPitch) + ord(sp.alteration) + 12) mod 12)
+
+
+func toNote*(p: Pitch): Note =
+ case p
+ of Af, A: Note.A
+ of Bf, B: Note.B
+ of C: Note.C
+ of Df, D: Note.D
+ of Ef, E: Note.E
+ of F: Note.F
+ of Gf, G: Note.D
+
+
+func parseMode*(str: string): Mode =
+ case str.toLower.strip
+ of "ionian", "major", "": Ionian
+ of "dorian": Dorian
+ of "phrygian": Phrygian
+ of "lydian": Lydian
+ of "mixolydian": Mixolydian
+ of "aeolian", "minor": Aeolian
+ of "locrian": Locrian
+ else: raise newException(ValueError, "Unrecognized mode/scale: " & str)
+
+
+func parseSpelledPitch*(str: string): SpelledPitch =
+ try: result.note = parseEnum[Note]($str[0])
+ except:
+ raise newException(ValueError, str[0] & " is not a recognized pitch name.")
+
+ result.alteration =
+ case str[1..^1]
+ of "𝄫", "bb", "♭♭": naDoubleFlat
+ of "b", "♭", "f", "es": naFlat
+ of "", "♮": naNatural
+ of "s", "is", "#": naSharp
+ of "𝄪", "##": naDoubleSharp
+ else: raise newException(ValueError,
+ str[1..^1] & " is not a recognized accidental.")
+
+
+func parseScaleDegree*(str: string): ScaleDegree =
+ try: result.number = parseInt($str[0])
+ except:
+ raise newException(ValueError, str[0] & " is not a valid scale degree.")
+
+ result.alteration =
+ case str[0..^2]
+ of "𝄫", "bb", "♭♭": naDoubleFlat
+ of "b", "♭", "f", "es": naFlat
+ of "", "♮": naNatural
+ of "s", "is", "#": naSharp
+ of "𝄪", "##": naDoubleSharp
+ else: raise newException(ValueError,
+ str[0..^2] & " is not a recognized accidental.")
+
+
+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 =
+ result.note = key.tonic.note + (sd.number - 1)
+ let resultingPitch = ord(ionianPitch(key, sd.number)) + ord(sd.alteration)
+ result.alteration = cast[NoteAlteration](
+ resultingPitch - ord(result.note.toPitch))
+
+
+func toScaleDegree*(key: Key, sp: SpelledPitch): ScaleDegree =
+ result.number = sp.note - key.tonic.note + 1
+ result.alteration = cast[NoteAlteration](
+ ord(sp.toPitch) - ord(ionianPitch(key, result.number)))
+
+
+func transpose*(k: Key, steps: int): Key =
+ Key(
+ tonic: toSpelledPitch(k.tonic.toPitch + steps),
+ mode: k.mode)
+
+
+when isMainModule:
+ assert A + 1 == Bf
+ assert A + 12 == A
+ assert A - 1 == Af
+ assert A + 14 == B
+
+ assert C - A == 3
+ assert A - G == 2
+ assert G - A == 10
+
+ assert Note.D - Note.B == 2
+ assert Note.A - Note.C == 5
+ assert Note.C - Note.A == 2
+
+ assert chromaticDistanceFromTonic(ScaleDegree(number: 1, alteration: naNatural)) == 0
+ assert chromaticDistanceFromTonic(ScaleDegree(number: 5, alteration: naNatural)) == 7
+ assert chromaticDistanceFromTonic(ScaleDegree(number: 5, alteration: naDoubleFlat)) == 5
+ assert chromaticDistanceFromTonic(ScaleDegree(number: 7, alteration: naFlat)) == 10
+ assert chromaticDistanceFromTonic(ScaleDegree(number: 3, alteration: naDoubleSharp)) == 6
+
+ assert $SpelledPitch(note: Note.B, alteration: naFlat) == "B♭"
+
+ assert A == parseSpelledPitch("A").toPitch
+ assert B == parseSpelledPitch("B").toPitch
+ assert C == parseSpelledPitch("C").toPitch
+ assert D == parseSpelledPitch("D").toPitch
+ assert E == parseSpelledPitch("E").toPitch
+ assert F == parseSpelledPitch("F").toPitch
+ assert G == parseSpelledPitch("G").toPitch
+
+ assert Af == parseSpelledPitch("Ab").toPitch
+ assert G == parseSpelledPitch("Abb").toPitch
+ assert B == parseSpelledPitch("Cb").toPitch
+ assert Df == parseSpelledPitch("B##").toPitch
+
+ assert "3" == $toScaleDegree(
+ Key(tonic: parseSpelledPitch("B"), mode: Ionian),
+ parseSpelledPitch("D#"))
+
+ assert "1" == $toScaleDegree(
+ Key(tonic: parseSpelledPitch("Gb"), mode: Ionian),
+ parseSpelledPitch("Gb"))
+
+ assert parseSpelledPitch("Cb").toPitch == parseSpelledPitch("B").toPitch
+
+ assert "♭6" == $toScaleDegree(
+ Key(tonic: parseSpelledPitch("Eb"), mode: Ionian),
+ parseSpelledPitch("Cb"))
+
+ assert "#5" == $toScaleDegree(
+ Key(tonic: parseSpelledPitch("Eb"), mode: Ionian),
+ parseSpelledPitch("B"))