Support for number charts, transposition when generating charts.
This commit is contained in:
parent
9ca5a1b99c
commit
5b1238f038
@ -1,6 +1,6 @@
|
||||
# Package
|
||||
|
||||
version = "0.4.1"
|
||||
version = "0.5.0"
|
||||
author = "Jonathan Bernard"
|
||||
description = "Chord chart formatter compatible with Planning Center Online"
|
||||
license = "MIT"
|
||||
|
@ -34,11 +34,16 @@ when isMainModule:
|
||||
$ast & "\p" &
|
||||
"-".repeat(16) & "\p"
|
||||
|
||||
let outputHtml = ast.toHtml()
|
||||
let transpose =
|
||||
if args["--transpose"]: parseInt($args["--transpose"])
|
||||
else: 0
|
||||
|
||||
let outputHtml = ast.toHtml(transpose, args["--number-chart"])
|
||||
|
||||
if args["--output"]: writeFile($args["--output"], outputHtml)
|
||||
else: stdout.write(outputHtml)
|
||||
|
||||
except:
|
||||
except CatchableError:
|
||||
fatal getCurrentExceptionMsg()
|
||||
debug getCurrentException().getStackTrace()
|
||||
quit(QuitFailure)
|
||||
|
@ -4,7 +4,7 @@ import zero_functional
|
||||
type
|
||||
ChordChartMetadata* = object
|
||||
title*: string
|
||||
key*: ChordChartPitch
|
||||
key*: ChordChartChord
|
||||
optionalProps: StringTableRef
|
||||
|
||||
ChordChart* = ref object
|
||||
@ -26,7 +26,7 @@ type
|
||||
ChordChartPitch* = enum Af, A, Bf, B, C, Df, D, Ef, E, F, Fs, G
|
||||
|
||||
ChordChartChord* = object
|
||||
original*: string
|
||||
original*: Option[string]
|
||||
rootPitch*: ChordChartPitch
|
||||
flavor*: Option[string]
|
||||
bassPitch*: Option[ChordChartPitch]
|
||||
@ -133,13 +133,15 @@ func dump*(ccn: ChordChartNode, indent = ""): string =
|
||||
if ccn.chord.isSome:
|
||||
let chord = ccn.chord.get
|
||||
result &= "["
|
||||
if chord.flavor.isNone and chord.bassPitch.isNone:
|
||||
result &= chord.original
|
||||
if chord.original.isSome and chord.flavor.isNone and
|
||||
chord.bassPitch.isNone:
|
||||
result &= chord.original.get
|
||||
else:
|
||||
result &= $chord.rootPitch
|
||||
if chord.flavor.isSome:
|
||||
result &= $chord.rootPitch & "_" & chord.flavor.get
|
||||
result &= "_" & chord.flavor.get
|
||||
if chord.bassPitch.isSome:
|
||||
result &= $chord.rootPitch & "/" & $chord.bassPitch.get
|
||||
result &= "/" & $chord.bassPitch.get
|
||||
result &= "]"
|
||||
|
||||
if ccn.word.isSome: result &= ccn.word.get
|
||||
@ -185,8 +187,9 @@ template addToCurSection(n: ChordChartNode): untyped =
|
||||
if ctx.curSection.kind == ccnkSection: ctx.curSection.sectionContents.add(n)
|
||||
else: result.add(n)
|
||||
|
||||
func parsePitch*(ctx: ParserContext, keyValue: string): ChordChartPitch =
|
||||
case keyValue.strip.toLower
|
||||
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
|
||||
@ -199,6 +202,13 @@ func parsePitch*(ctx: ParserContext, keyValue: string): ChordChartPitch =
|
||||
of "es", "eis", "e#", "f", "g𝄫": return ChordChartPitch.F
|
||||
of "e𝄪", "fs", "fis", "f#", "gf", "gb", "ges": return ChordChartPitch.Fs
|
||||
of "f𝄪", "g", "a𝄫": return ChordChartPitch.G
|
||||
of "1": return ctx.curKeyCenter
|
||||
of "2": return ctx.curKeyCenter + 2
|
||||
of "3": return ctx.curKeyCenter + 4
|
||||
of "4": return ctx.curKeyCenter + 5
|
||||
of "5": return ctx.curKeyCenter + 7
|
||||
of "6": return ctx.curKeyCenter + 9
|
||||
of "7": return ctx.curKeyCenter + 11
|
||||
else: raise ctx.makeError(keyValue.strip & " is not a recognized key.")
|
||||
|
||||
# see regexr.com/70nv1
|
||||
@ -226,7 +236,7 @@ proc parseChord*(
|
||||
else: none[ChordChartPitch]()
|
||||
|
||||
return some(ChordChartChord(
|
||||
original: chordValue,
|
||||
original: some(chordValue),
|
||||
rootPitch: ctx.parsePitch(m.get.captures[0]),
|
||||
flavor: flavor,
|
||||
bassPitch: bassPitch))
|
||||
@ -236,8 +246,8 @@ let METADATA_END_PAT = re"^-+$"
|
||||
proc parseMetadata(ctx: var ParserContext): ChordChartMetadata =
|
||||
|
||||
var title = "MISSING"
|
||||
var songKey = "MISSING"
|
||||
var optProps = newStringTable()
|
||||
var songKey: Option[ChordChartChord]
|
||||
|
||||
while ctx.curLineNum < ctx.lines.len:
|
||||
let line = ctx.nextLine
|
||||
@ -252,18 +262,21 @@ proc parseMetadata(ctx: var ParserContext): ChordChartMetadata =
|
||||
let key = m.get.captures[0].strip.tolower
|
||||
let value = m.get.captures[1].strip
|
||||
if key == "title": title = value
|
||||
elif key == "key": songKey = value
|
||||
elif key == "key":
|
||||
songKey = ctx.parseChord(value)
|
||||
if songKey.isNone:
|
||||
raise ctx.makeError("unrecognized key: " & value)
|
||||
else: optProps[key] = value
|
||||
|
||||
if title == "MISSING":
|
||||
raise ctx.makeError("metadata is missing the 'title' property")
|
||||
|
||||
if songKey == "MISSING":
|
||||
if songKey.isNone:
|
||||
raise ctx.makeError("metadata is missing the 'key' property")
|
||||
|
||||
result = ChordChartMetadata(
|
||||
title: title,
|
||||
key: ctx.parsePitch(songKey),
|
||||
key: songKey.get,
|
||||
optionalProps: optProps)
|
||||
|
||||
const KNOWN_SECTION_NAMES = [
|
||||
@ -447,7 +460,7 @@ proc parseChordChart*(s: string): ChordChart =
|
||||
unparsedLineParts: @[])
|
||||
|
||||
let metadata = parseMetadata(parserCtx)
|
||||
parserCtx.curKeyCenter = metadata.key
|
||||
parserCtx.curKeyCenter = metadata.key.rootPitch
|
||||
|
||||
result = ChordChart(
|
||||
rawContent: s,
|
||||
|
@ -1,4 +1,4 @@
|
||||
const PCO_CHORDS_VERSION* = "0.4.1"
|
||||
const PCO_CHORDS_VERSION* = "0.5.0"
|
||||
|
||||
const USAGE* = """Usage:
|
||||
pco_chords [options]
|
||||
@ -6,12 +6,20 @@ const USAGE* = """Usage:
|
||||
|
||||
Options:
|
||||
|
||||
-i, --input <in-file> Read input from <in-file> (rather than from
|
||||
-i, --input <in-file> Read input from <in-file> (rather than from
|
||||
STDIN, which is the default).
|
||||
|
||||
-o, --output <out-file> Write output to <out-file> (rather than from
|
||||
-o, --output <out-file> Write output to <out-file> (rather than from
|
||||
STDOUT, which is the default).
|
||||
|
||||
-t, --transpose <distance> Transpose the chart by the given number of
|
||||
semitones. Distance can be expressed as a
|
||||
positive or negative number of semitones,
|
||||
for example: 4, +3 or -5.
|
||||
|
||||
-n, --number-chart Write out a chart using the Nashville number
|
||||
system rather than explicit pitches.
|
||||
|
||||
--help Print this usage information
|
||||
--debug Enable debug logging.
|
||||
--echo-args Echo the input parameters.
|
||||
|
@ -9,6 +9,8 @@ type
|
||||
currentKey: ChordChartPitch
|
||||
sourceKey: ChordChartPitch
|
||||
currentSection: ChordChartNode
|
||||
numberChart: bool
|
||||
transposeSteps: int
|
||||
|
||||
const DEFAULT_STYLESHEET* = """
|
||||
<style>
|
||||
@ -69,6 +71,10 @@ h3 .section-text {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
.chord .flavor {
|
||||
font-variant-position: super;
|
||||
}
|
||||
|
||||
.song-order h3 {
|
||||
font-style: italic;
|
||||
font-weight: normal;
|
||||
@ -89,17 +95,58 @@ h3 .section-text {
|
||||
</style>
|
||||
"""
|
||||
|
||||
proc transpose(ctx: FormatContext, chord: ChordChartChord): string =
|
||||
let distance = ctx.currentKey - ctx.sourceKey
|
||||
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")
|
||||
|
||||
if distance != 0:
|
||||
result = $(chord.rootPitch + distance)
|
||||
func format(ctx: FormatContext, chord: ChordChartChord, useNumber = false): string =
|
||||
if not useNumber and chord.original.isSome: return chord.original.get
|
||||
|
||||
elif useNumber:
|
||||
result = "<span class=root>" &
|
||||
ctx.currentKey.scaleDegree(chord.rootPitch) & "</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>"
|
||||
|
||||
else:
|
||||
result = $chord.rootPitch
|
||||
if chord.flavor.isSome: result &= chord.flavor.get
|
||||
if chord.bassPitch.isSome: result &= "/" & $(chord.bassPitch.get + distance)
|
||||
if chord.bassPitch.isSome: result &= "/" & $chord.bassPitch.get
|
||||
#debug "transposed " & $ctx.sourceKey & " -> " & $ctx.currentKey &
|
||||
# " (distance " & $distance & ")\p\tchord: " & $chord & " to " & result
|
||||
|
||||
else: result = chord.original
|
||||
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" &
|
||||
@ -143,7 +190,8 @@ proc toHtml(ctx: var FormatContext, node: ChordChartNode, indent: string): strin
|
||||
result &= "'>"
|
||||
|
||||
if node.chord.isSome:
|
||||
result &= "<span class=chord>" & ctx.transpose(node.chord.get) & "</span>"
|
||||
result &= "<span class=chord>" &
|
||||
ctx.format(ctx.transpose(node.chord.get), ctx.numberChart) & "</span>"
|
||||
|
||||
result &= "<span class=lyric>"
|
||||
if node.word.isSome: result &= node.word.get
|
||||
@ -169,8 +217,8 @@ proc toHtml(ctx: var FormatContext, node: ChordChartNode, indent: string): strin
|
||||
result &= "</section><section>" & headingVal & "</section><section>"
|
||||
|
||||
of ccnkRedefineKey:
|
||||
let oldKey = ctx.sourceKey
|
||||
ctx.sourceKey = node.newKey
|
||||
let oldKey = ctx.currentKey
|
||||
ctx.sourceKey = node.newKey + ctx.transposeSteps
|
||||
ctx.currentKey = ctx.sourceKey
|
||||
|
||||
if oldKey != ctx.currentKey:
|
||||
@ -183,14 +231,21 @@ proc toHtml(ctx: var FormatContext, node: ChordChartNode, indent: string): strin
|
||||
|
||||
proc toHtml*(
|
||||
cc: ChordChart,
|
||||
transpose = 0,
|
||||
numberChart = false,
|
||||
stylesheets = @[DEFAULT_STYLESHEET],
|
||||
scripts: seq[string] = @[]): string =
|
||||
|
||||
var ctx = FormatContext(
|
||||
chart: cc,
|
||||
currentKey: cc.metadata.key,
|
||||
sourceKey: cc.metadata.key,
|
||||
currentSection: EMPTY_CHORD_CHART_NODE)
|
||||
currentKey: cc.metadata.key.rootPitch + transpose,
|
||||
sourceKey: cc.metadata.key.rootPitch,
|
||||
currentSection: EMPTY_CHORD_CHART_NODE,
|
||||
numberChart: numberChart,
|
||||
transposeSteps: transpose)
|
||||
|
||||
debug "Formatting:\p\tsource key: " & $ctx.sourceKey & "\p\ttransposing: " &
|
||||
$transpose & "\p\ttarget key: " & $ctx.currentKey
|
||||
|
||||
result = """<!doctype html>
|
||||
<html>
|
||||
@ -210,11 +265,11 @@ proc toHtml*(
|
||||
result &= " </head>\p <body>"
|
||||
|
||||
var indent = " "
|
||||
#
|
||||
|
||||
# Title
|
||||
result &= indent & "<h1>" & cc.metadata.title & "</h1>\p"
|
||||
|
||||
var metadataPieces = @["Key: " & $cc.metadata.key]
|
||||
var metadataPieces = @["Key: " & ctx.format(ctx.transpose(cc.metadata.key))]
|
||||
if cc.metadata.contains("time signature"):
|
||||
metadataPieces.add(cc.metadata["time signature"])
|
||||
if cc.metadata.contains("bpm"):
|
||||
|
Loading…
x
Reference in New Issue
Block a user