Major refactor of internal note storage to better support transposition.
Refactor to store notes as key-agnostic scale degrees + alterations (flat, sharp, etc.). We now think of a pitch in different ways: - *Note*: the 7 notes diatonic to C major. Notes capture the principle in western harmony of the common scales having seven distinct diatonic "notes" and allows us to do arithmetic with them (do up three scale degrees). - *Pitch*: 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. - *SpelledPitch*: a unique spelling of one of the chromatic pitches (*Note* + alteration) allowing the stylistic choice to use different *Notes* to describe a single *Pitch*. - *ScaleDegree*: a variant of *SpelledPitch* that uses the scale degree instead of the *Pitch* to store a pitch in a key-agnostic manner. To illustrate, the difference, consider the flat-six in the key of Eb. - *Note*: The 6th scale degree in Eb is C (1-E 2-F 3-G 4-A 5-B 6-C). - *Pitch*: In the chromatic scale, ignoring the key, this is the *Pitch* called B. - *SpelledPitch*: In the context of the key of Eb, because this is the *Note* C, we should spell this as Cb, not B. So the spelled pitch is *Note*(C), *Alteration*(flat). - *ScaleDegree*: This captures the key-agnostic representation that we used in the begining: *Number*(6) and *Alteration*(flat) With these four ways of representing a note, we can transpose any pitches that follow western 12-tone harmony arbitrarily between keys preserving the author's choice of chord function (remembering that this is the b6 and not the #5, in our example). Building on this new notational data model, the AST now uses the *ScaleDegree* relative to the provided key as the internal representation of a pitch. Formatting of a *ScaleDegree* always requires the key in which it is being rendered. Transposition is now only a matter or updating the current key.
This commit is contained in:
@@ -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:
|
||||
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"):
|
||||
|
||||
Reference in New Issue
Block a user