Compare commits
1 Commits
Author | SHA1 | Date | |
---|---|---|---|
25d7ddcd1b |
@ -1,6 +1,6 @@
|
||||
# Package
|
||||
|
||||
version = "0.5.6"
|
||||
version = "0.6.0"
|
||||
author = "Jonathan Bernard"
|
||||
description = "Chord chart formatter compatible with Planning Center Online"
|
||||
license = "MIT"
|
||||
|
@ -1,10 +1,11 @@
|
||||
import std/[nre, strtabs, strutils]
|
||||
import ./notation
|
||||
import zero_functional
|
||||
|
||||
type
|
||||
ChordChartMetadata* = object
|
||||
title*: string
|
||||
key*: ChordChartChord
|
||||
key*: Key
|
||||
optionalProps: StringTableRef
|
||||
|
||||
ChordChart* = ref object
|
||||
@ -23,13 +24,11 @@ type
|
||||
ccnkRedefineKey,
|
||||
ccnkNone
|
||||
|
||||
ChordChartPitch* = enum Af, A, Bf, B, C, Df, D, Ef, E, F, Gf, G
|
||||
|
||||
ChordChartChord* = object
|
||||
original*: Option[string]
|
||||
rootPitch*: ChordChartPitch
|
||||
root*: ScaleDegree
|
||||
flavor*: Option[string]
|
||||
bassPitch*: Option[ChordChartPitch]
|
||||
bass*: Option[ScaleDegree]
|
||||
|
||||
ChordChartNode* = ref object
|
||||
case kind: ChordChartNodeKind
|
||||
@ -52,12 +51,12 @@ type
|
||||
of ccnkTransposeKey:
|
||||
transposeSteps*: int
|
||||
of ccnkRedefineKey:
|
||||
newKey*: ChordChartPitch
|
||||
newKey*: Key
|
||||
of ccnkNone: discard
|
||||
|
||||
ParserContext = ref object
|
||||
lines: seq[string]
|
||||
curKeyCenter: ChordChartPitch
|
||||
curKey: Key
|
||||
curLineNum: int
|
||||
curSection: ChordChartNode
|
||||
unparsedLineParts: seq[string]
|
||||
@ -77,97 +76,6 @@ iterator pairs*(ccmd: ChordChartMetadata): tuple[key, value: string] =
|
||||
|
||||
func kind*(ccn: ChordChartNode): ChordChartNodeKind = ccn.kind
|
||||
|
||||
func `$`*(pitch: ChordChartPitch): string =
|
||||
["A♭", "A", "B♭", "B", "C", "D♭", "D", "E♭", "E", "F", "G♭", "G"][ord(pitch)]
|
||||
|
||||
func renderPitchInKey*(
|
||||
pitch: ChordChartPitch,
|
||||
key: ChordChartPitch,
|
||||
useSharps: Option[bool] = none[bool]()): string =
|
||||
|
||||
var scaleNames: array[(ord(high(ChordChartPitch)) + 1), string]
|
||||
if useSharps.isNone:
|
||||
# If we aren't told to use sharps or flats, we render the diatonic pitches
|
||||
# according to standard theory (C# in the key of D, Db in the key of Ab)
|
||||
# but we render non-diatonic notes with flats (prefer the b6 and b7 over
|
||||
# the #5 and #6).
|
||||
#
|
||||
# TODO: In the future, we should also remember the scale degree of the
|
||||
# chord when parsing. So, for example, in the key of D we would parse Bb as
|
||||
# the b6 and A# as the #5. The pitch would be the same, but the scale
|
||||
# degree would differ. This would allow us to preserve intentional choices
|
||||
# in the chart when transposing.
|
||||
scaleNames = case key
|
||||
of C:
|
||||
["A♭", "A", "B♭", "B", "C", "D♭", "D", "E♭", "E", "F", "G♭", "G"]
|
||||
of G:
|
||||
["A♭", "A", "B♭", "B", "C", "D♭", "D", "E♭", "E", "F", "F#", "G"]
|
||||
of D:
|
||||
["A♭", "A", "B♭", "B", "C", "C#", "D", "E♭", "E", "F", "F#", "G"]
|
||||
of A:
|
||||
["G#", "A", "B♭", "B", "C", "C#", "D", "E♭", "E", "F", "F#", "G"]
|
||||
of E:
|
||||
["G#", "A", "B♭", "B", "C", "C#", "D", "D#", "E", "F", "F#", "G"]
|
||||
of B:
|
||||
["G#", "A", "A#", "B", "C", "C#", "D", "D#", "E", "F", "F#", "G"]
|
||||
of Gf:
|
||||
["A♭", "B𝄫", "B♭", "C♭", "D𝄫", "D♭", "E𝄫", "E♭", "F♭", "F", "G♭", "A𝄫"]
|
||||
of Df:
|
||||
["A♭", "B𝄫", "B♭", "C♭", "C", "D♭", "E𝄫", "E♭", "F♭", "F", "G♭", "A𝄫"]
|
||||
of Af:
|
||||
["A♭", "B𝄫", "B♭", "C♭", "C", "D♭", "E𝄫", "E♭", "F♭", "F", "G♭", "G"]
|
||||
of Ef:
|
||||
["A♭", "B𝄫", "B♭", "C♭", "C", "D♭", "D", "E♭", "F♭", "F", "G♭", "G"]
|
||||
of Bf:
|
||||
["A♭", "A", "B♭", "C♭", "C", "D♭", "D", "E♭", "F♭", "F", "G♭", "G"]
|
||||
of F:
|
||||
["A♭", "A", "B♭", "C♭", "C", "D♭", "D", "E♭", "E", "F", "G♭", "G"]
|
||||
|
||||
|
||||
elif useSharps.isSome and useSharps.get:
|
||||
scaleNames = case key
|
||||
of A, B, C, D, E, G:
|
||||
["G#", "A", "A#", "B", "C", "C#", "D", "D#", "E", "F", "F#", "G"]
|
||||
of Af:
|
||||
["G#", "A", "A#", "B", "B#", "C#", "D", "D#", "E", "E#", "F#", "F𝄪"]
|
||||
of Bf:
|
||||
["G#", "G𝄪", "A#", "B", "B#", "C#", "C𝄪", "D#", "E", "E#", "F#", "F𝄪"]
|
||||
of Df:
|
||||
["G#", "A", "A#", "B", "B#", "C#", "D", "D#", "E", "E#", "F#", "G"]
|
||||
of Ef:
|
||||
["G#", "A", "A#", "B", "B#", "C#", "C𝄪", "D#", "E", "E#", "F#", "F𝄪"]
|
||||
of Gf:
|
||||
["G#", "A", "A#", "B", "C", "C#", "D", "D#", "E", "E#", "F#", "G"]
|
||||
of F:
|
||||
["G#", "G𝄪", "A#", "B", "B#", "C#", "C𝄪", "D#", "D𝄪", "E#", "F#", "F𝄪"]
|
||||
|
||||
else: # !useSharps (useSharps.isSome and not useSharps.get)
|
||||
scaleNames = case key
|
||||
of C, Af, Bf, Df, Ef, F:
|
||||
["A♭", "A", "B♭", "B", "C", "D♭", "D", "E♭", "E", "F", "G♭", "G"]
|
||||
of A:
|
||||
["A♭", "B𝄫", "B♭", "C♭", "C", "D♭", "E𝄫", "E♭", "F♭", "F", "G♭", "G"]
|
||||
of B:
|
||||
["A♭", "A", "B♭", "C♭", "C", "D♭", "D", "E♭", "F♭", "F", "G♭", "G"]
|
||||
of D:
|
||||
["A♭", "B𝄫", "B♭", "C♭", "C", "D♭", "E𝄫", "E♭", "F♭", "F", "G♭", "A𝄫"]
|
||||
of E:
|
||||
["A♭", "B𝄫", "B♭", "C♭", "C", "D♭", "D", "E♭", "F♭", "F", "G♭", "G"]
|
||||
of G:
|
||||
["A♭", "B𝄫", "B♭", "C♭", "D𝄫", "D♭", "E𝄫", "E♭", "F♭", "F", "G♭", "A𝄫"]
|
||||
of Gf:
|
||||
["A♭", "A", "B♭", "C♭", "C", "D♭", "D", "E♭", "E", "F", "G♭", "G"]
|
||||
|
||||
|
||||
return scaleNames[ord(pitch)]
|
||||
|
||||
func `+`*(pitch: ChordChartPitch, steps: int): ChordChartPitch =
|
||||
cast[ChordChartPitch]((ord(pitch) + steps) mod 12)
|
||||
|
||||
func `-`*(a, b: ChordChartPitch): int =
|
||||
result = ord(a) - ord(b)
|
||||
if result < 0: result += 12
|
||||
|
||||
func dump*(m: ChordChartMetadata, indent = ""): string =
|
||||
return indent & "Metadata\p" & join(m.pairs --> map(indent & " " & it.key & ": " & it.value), "\p")
|
||||
|
||||
@ -203,14 +111,14 @@ func dump*(ccn: ChordChartNode, indent = ""): string =
|
||||
let chord = ccn.chord.get
|
||||
result &= "["
|
||||
if chord.original.isSome and chord.flavor.isNone and
|
||||
chord.bassPitch.isNone:
|
||||
chord.bass.isNone:
|
||||
result &= chord.original.get
|
||||
else:
|
||||
result &= $chord.rootPitch
|
||||
result &= $chord.root
|
||||
if chord.flavor.isSome:
|
||||
result &= "_" & chord.flavor.get
|
||||
if chord.bassPitch.isSome:
|
||||
result &= "/" & $chord.bassPitch.get
|
||||
if chord.bass.isSome:
|
||||
result &= "/" & $chord.bass.get
|
||||
result &= "]"
|
||||
|
||||
if ccn.word.isSome: result &= ccn.word.get
|
||||
@ -256,36 +164,7 @@ template addToCurSection(n: ChordChartNode): untyped =
|
||||
if ctx.curSection.kind == ccnkSection: ctx.curSection.sectionContents.add(n)
|
||||
else: result.add(n)
|
||||
|
||||
proc parsePitch*(ctx: ParserContext, keyValue: string): ChordChartPitch =
|
||||
let normK = keyValue.strip.toLower
|
||||
case normK
|
||||
of "gs", "gis", "g#", "ab", "a♭", "af", "aes": return ChordChartPitch.Af
|
||||
of "g𝄪", "a", "a♮", "b𝄫": return ChordChartPitch.A
|
||||
of "as", "ais", "a#", "bf", "bb", "b♭", "bes", "c𝄫": return ChordChartPitch.Bf
|
||||
of "a𝄪", "b", "c♭", "cb", "ces", "cf": return ChordChartPitch.B
|
||||
of "bs", "bis", "b#", "c", "d𝄫": return ChordChartPitch.C
|
||||
of "b𝄪", "cs", "cis", "c#", "d♭", "df", "db", "des": return ChordChartPitch.Df
|
||||
of "c𝄪", "d", "e𝄫": return ChordChartPitch.D
|
||||
of "ds", "dis", "d#", "ef", "e♭", "eb", "ees", "f𝄫": return ChordChartPitch.Ef
|
||||
of "d𝄪", "e", "f♭", "fes", "ff": return ChordChartPitch.E
|
||||
of "es", "eis", "e#", "f", "g𝄫": return ChordChartPitch.F
|
||||
of "e𝄪", "fs", "fis", "f#", "g♭", "gf", "gb", "ges": return ChordChartPitch.Gf
|
||||
of "f𝄪", "g", "a𝄫": return ChordChartPitch.G
|
||||
of "#7", "1", "𝄫2": return ctx.curKeyCenter
|
||||
of "𝄪7", "#1", "b2", "♭2": return ctx.curKeyCenter + 1
|
||||
of "𝄪1", "2", "𝄫3": return ctx.curKeyCenter + 2
|
||||
of "#2", "b3", "♭3", "𝄫4": return ctx.curKeyCenter + 3
|
||||
of "𝄪2", "3", "b4", "♭4": return ctx.curKeyCenter + 4
|
||||
of "#3", "4", "𝄫5": return ctx.curKeyCenter + 5
|
||||
of "𝄪3", "#4", "b5", "♭5": return ctx.curKeyCenter + 6
|
||||
of "𝄪4", "5", "𝄫6": return ctx.curKeyCenter + 7
|
||||
of "#5", "b6", "♭6": return ctx.curKeyCenter + 8
|
||||
of "𝄪5", "6", "𝄫7": return ctx.curKeyCenter + 9
|
||||
of "#6", "b7", "♭7", "𝄫1": return ctx.curKeyCenter + 10
|
||||
of "7", "b1", "♭1": return ctx.curKeyCenter + 11
|
||||
else: raise ctx.makeError(keyValue.strip & " is not a recognized key.")
|
||||
|
||||
# see regexr.com/70nv1
|
||||
# see https://regexr.com/8f4ru
|
||||
let CHORD_REGEX =
|
||||
"([b#♭♮𝄫𝄪]?[A-G1-7][b#♭♮𝄫𝄪]?)" & # chord root
|
||||
"((min|maj|aug|dim|sus|6\\/9|[mM1-9#b♭♮𝄫𝄪Δ+oøoø°𝆩][0-9]?|\\([1-9#b♭]+\\))*)" & # chord flavor/type
|
||||
@ -293,7 +172,8 @@ let CHORD_REGEX =
|
||||
let CHORD_PAT = re(CHORD_REGEX)
|
||||
|
||||
proc parseChord*(
|
||||
ctx: ParserContext, chordValue: string
|
||||
ctx: ParserContext,
|
||||
chordValue: string
|
||||
): Option[ChordChartChord] =
|
||||
|
||||
let m = chordValue.match(CHORD_PAT)
|
||||
@ -304,16 +184,16 @@ proc parseChord*(
|
||||
some(m.get.captures[1])
|
||||
else: none[string]()
|
||||
|
||||
let bassPitch =
|
||||
let bass =
|
||||
if m.get.captures.contains(4) and m.get.captures[4].len > 0:
|
||||
some(ctx.parsePitch(m.get.captures[4]))
|
||||
else: none[ChordChartPitch]()
|
||||
some(toScaleDegree(ctx.curKey, parseSpelledPitch(m.get.captures[4])))
|
||||
else: none[ScaleDegree]()
|
||||
|
||||
return some(ChordChartChord(
|
||||
original: some(chordValue),
|
||||
rootPitch: ctx.parsePitch(m.get.captures[0]),
|
||||
root: toScaleDegree(ctx.curKey, parseSpelledPitch(m.get.captures[0])),
|
||||
flavor: flavor,
|
||||
bassPitch: bassPitch))
|
||||
bass: bass))
|
||||
|
||||
let METADATA_LINE_PAT = re"^([^:]+):(.*)$"
|
||||
let METADATA_END_PAT = re"^-+$"
|
||||
@ -321,7 +201,7 @@ proc parseMetadata(ctx: var ParserContext): ChordChartMetadata =
|
||||
|
||||
var title = "MISSING"
|
||||
var optProps = newStringTable()
|
||||
var songKey: Option[ChordChartChord]
|
||||
var songKey: Option[Key]
|
||||
|
||||
while ctx.curLineNum < ctx.lines.len:
|
||||
let line = ctx.nextLine
|
||||
@ -337,7 +217,12 @@ proc parseMetadata(ctx: var ParserContext): ChordChartMetadata =
|
||||
let value = m.get.captures[1].strip
|
||||
if key == "title": title = value
|
||||
elif key == "key":
|
||||
songKey = ctx.parseChord(value)
|
||||
let parts = value.split(" ", 1)
|
||||
songKey = some(Key(
|
||||
tonic: parseSpelledPitch(parts[0]),
|
||||
mode:
|
||||
if parts.len > 1: parseMode(parts[1])
|
||||
else: Ionian))
|
||||
if songKey.isNone:
|
||||
raise ctx.makeError("unrecognized key: " & value)
|
||||
else: optProps[key] = value
|
||||
@ -499,14 +384,9 @@ proc parseBody(ctx: var ParserContext): seq[ChordChartNode] =
|
||||
|
||||
m = line.match(REDEFINE_KEY_PAT)
|
||||
if m.isSome:
|
||||
let newKeyInt = (
|
||||
ord(ctx.curKeyCenter) +
|
||||
parseInt(m.get.captures[0])
|
||||
) mod 12
|
||||
|
||||
addToCurSection(ChordChartNode(
|
||||
kind: ccnkRedefineKey,
|
||||
newKey: cast[ChordChartPitch](newKeyInt)))
|
||||
newKey: ctx.curKey.transpose(parseInt(m.get.captures[0]))))
|
||||
continue
|
||||
|
||||
m = line.match(COL_BREAK_PAT)
|
||||
@ -548,7 +428,7 @@ proc parseChordChart*(s: string): ChordChart =
|
||||
unparsedLineParts: @[])
|
||||
|
||||
let metadata = parseMetadata(parserCtx)
|
||||
parserCtx.curKeyCenter = metadata.key.rootPitch
|
||||
parserCtx.curKey = metadata.key
|
||||
|
||||
result = ChordChart(
|
||||
rawContent: s,
|
||||
|
@ -1,4 +1,4 @@
|
||||
const PCO_CHORDS_VERSION* = "0.5.6"
|
||||
const PCO_CHORDS_VERSION* = "0.6.0"
|
||||
|
||||
const USAGE* = """Usage:
|
||||
pco_chords [options]
|
||||
|
@ -1,13 +1,13 @@
|
||||
import std/[logging, options, os, strutils]
|
||||
import zero_functional
|
||||
|
||||
import ./ast
|
||||
import ./[ast, notation]
|
||||
|
||||
type
|
||||
FormatContext = ref object
|
||||
chart: ChordChart
|
||||
currentKey: ChordChartPitch
|
||||
sourceKey: ChordChartPitch
|
||||
currentKey: Key
|
||||
sourceKey: Key
|
||||
currentSection: ChordChartNode
|
||||
numberChart: bool
|
||||
transposeSteps: int
|
||||
@ -80,7 +80,7 @@ h3 .section-text {
|
||||
height: 1.2em;
|
||||
}
|
||||
|
||||
.chord .flavor {
|
||||
.number-chart .chord .flavor {
|
||||
font-variant-position: super;
|
||||
}
|
||||
|
||||
@ -175,7 +175,7 @@ h3 .section-text {
|
||||
height: 1.2em;
|
||||
}
|
||||
|
||||
.chord .flavor {
|
||||
.number-chart .chord .flavor {
|
||||
font-variant-position: super;
|
||||
}
|
||||
|
||||
@ -197,62 +197,23 @@ h3 .section-text {
|
||||
</style>
|
||||
"""
|
||||
|
||||
func scaleDegree(root: ChordChartPitch, p: ChordChartPitch): string =
|
||||
let distance = p - root
|
||||
case distance:
|
||||
of 0: "1"
|
||||
of 1: "♭2"
|
||||
of 2: "2"
|
||||
of 3: "♭3"
|
||||
of 4: "3"
|
||||
of 5: "4"
|
||||
of 6: "♭5"
|
||||
of 7: "5"
|
||||
of 8: "♭6"
|
||||
of 9: "6"
|
||||
of 10: "♭7"
|
||||
of 11: "7"
|
||||
else: raise newException(Exception, "Impossible")
|
||||
|
||||
func format(ctx: FormatContext, chord: ChordChartChord, useNumber = false): string =
|
||||
##if not useNumber and chord.original.isSome: return chord.original.get
|
||||
|
||||
if useNumber:
|
||||
result = "<span class=root>" &
|
||||
ctx.currentKey.scaleDegree(chord.rootPitch) & "</span>"
|
||||
result = "<span class=root>"
|
||||
if useNumber: result &= $chord.root
|
||||
else: result &= $ctx.currentKey.spellPitch(chord.root)
|
||||
result &= "</span>"
|
||||
|
||||
if chord.flavor.isSome:
|
||||
result &= "<span class=flavor>" & chord.flavor.get & "</span>"
|
||||
|
||||
if chord.bassPitch.isSome:
|
||||
result &= "<span class=bass>/" &
|
||||
ctx.currentKey.scaleDegree(chord.bassPitch.get) & "</span>"
|
||||
if chord.bass.isSome:
|
||||
result &= "<span class=bass>/"
|
||||
if useNumber: result &= $chord.bass.get
|
||||
else: result &= $ctx.currentKey.spellPitch(chord.bass.get)
|
||||
result &= "</span>"
|
||||
|
||||
else:
|
||||
result = "<span class=root>" &
|
||||
renderPitchInKey(chord.rootPitch, ctx.currentKey) & "</span>"
|
||||
|
||||
if chord.flavor.isSome:
|
||||
result &= "<span class=flavor>" & chord.flavor.get & "</span>"
|
||||
|
||||
if chord.bassPitch.isSome:
|
||||
result &= "<span class=bass>/" &
|
||||
renderPitchInKey(chord.bassPitch.get, ctx.currentKey) & "</span>"
|
||||
|
||||
proc transpose(ctx: FormatContext, chord: ChordChartChord): ChordChartChord =
|
||||
result = chord
|
||||
|
||||
let distance = ctx.currentKey - ctx.sourceKey
|
||||
if distance != 0:
|
||||
result = ChordChartChord(
|
||||
original: none[string](),
|
||||
rootPitch: chord.rootPitch + distance,
|
||||
flavor:
|
||||
if chord.flavor.isSome: some(chord.flavor.get)
|
||||
else: none[string](),
|
||||
bassPitch:
|
||||
if chord.bassPitch.isSome: some(chord.bassPitch.get + distance)
|
||||
else: none[ChordChartPitch]())
|
||||
|
||||
func makeSongOrder(songOrder, indent: string): string =
|
||||
result = indent & "<section class=song-order>\p" &
|
||||
@ -298,7 +259,7 @@ proc toHtml(ctx: var FormatContext, node: ChordChartNode, indent: string): strin
|
||||
|
||||
if node.chord.isSome:
|
||||
result &= "<span class=chord>" &
|
||||
ctx.format(ctx.transpose(node.chord.get), ctx.numberChart) & "</span>"
|
||||
ctx.format(node.chord.get, ctx.numberChart) & "</span>"
|
||||
|
||||
result &= "<span class=lyric>"
|
||||
if node.word.isSome: result &= node.word.get
|
||||
@ -317,13 +278,13 @@ proc toHtml(ctx: var FormatContext, node: ChordChartNode, indent: string): strin
|
||||
result &= "</div><div class=page-contents>"
|
||||
|
||||
of ccnkTransposeKey:
|
||||
ctx.currentKey = ctx.currentKey + node.transposeSteps
|
||||
ctx.currentKey = ctx.currentKey.transpose(node.transposeSteps)
|
||||
result &= indent & "<h4 class=note>Key Change: " & $ctx.currentKey & "</h4>"
|
||||
|
||||
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 & "<h4 class=note>Key Change: " & $ctx.currentKey & "</h4>"
|
||||
@ -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 &= " </head>\p"
|
||||
|
||||
var bodyClasses = newSeq[string]()
|
||||
if numberChart: bodyClasses.add("number-chart")
|
||||
if cc.metadata.contains("columns") and cc.metadata["columns"] == "1":
|
||||
result &= " <body class='one-column'>"
|
||||
else:
|
||||
result &= " <body class='two-column'>"
|
||||
bodyClasses.add("one-column")
|
||||
else: bodyClasses.add("two-column")
|
||||
result &= " <body class='" & bodyClasses.join(" ") & "'>"
|
||||
|
||||
var indent = " "
|
||||
|
||||
# Title
|
||||
result &= indent & "<h1>" & cc.metadata.title & "</h1>\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"):
|
||||
|
BIN
src/pco_chordspkg/notation
Executable file
BIN
src/pco_chordspkg/notation
Executable file
Binary file not shown.
245
src/pco_chordspkg/notation.nim
Normal file
245
src/pco_chordspkg/notation.nim
Normal file
@ -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"))
|
Reference in New Issue
Block a user