// abc2svg - nns.js - module to output chords in the NNS
// (Nashville Notation System)
//
// Copyright (C) 2021-2023 Jean-Francois Moine
//
// This file is part of abc2svg.
//
// abc2svg is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// abc2svg is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with abc2svg. If not, see <http://www.gnu.org/licenses/>.
//
// This module is loaded when "%%nns" appears in a ABC source.
//
// Parameters
// %%nns [n] [include=<list>] [nomusic] [repbrk] [roman= 1 | 2]
// <n> = number of chords per line (1: auto)
// > 0: above the tune, < 0: under the tune
// <list> = comma separated list of (continuous) measure numbers
// 'nomusic' displays only the chord
// 'repbrk' starts a new grid line on start/stop repeat
// 'roman' display the chord in the Roman Numeral Notation
// =1: uppercase letters with 'm' for minor chords (default)
// =2: lowercase letters for minor chords
// %%nnsfont font_name size (default: 'monospace 16')
if (typeof abc2svg == "undefined")
var abc2svg = {}
abc2svg.nns = {
note_nm: "CDEFGAB",
// Nashville
nns_nm: ["1", "♯1", "2", "♭3", "3", "4", "♯4",
"5", "♯5", "6", "♭7", "7"],
// Roman
rnn_nm: ["I", "♯I", "II", "♭III", "III", "IV", "♯IV",
"V", "♯V", "VI", "♭VII", "VII"],
rnn_nm_m: ["III", "♯III", "IV", "♭V", "V", "VI", "♯VI",
"VII", "♯VII", "I", "♭II", "II", "III"],
// Roman 2
rnn2_nm: ["I", "♯I", "ii", "♭III", "iii", "IV", "♯IV",
"V", "♯V", "vi", "♭VII", "vii"],
rnn2_nm_m: ["III", "♯III", "iv", "♭V", "v", "VI", "♯VI",
"VII", "♯VII", "i", "♭II", "ii", "III"],
// inversions: 1st: (upper)6, 2nd: (upper)6 (lower) 4
// generate the grid
block_gen: function(of, s) {
if (s.subtype != "nns") {
of(s)
return
}
var abc = this,
img,
cfmt = abc.cfmt(),
nns = cfmt.nns
// generate the chord list
function build_nns(s, font) {
var i, k, l, nr, bar, w, hr, x, y, row,
chords = s.chords,
bars = s.bars,
parts = s.parts || [],
cells = [],
nc = nns.n
// set the chord(s) in each measure
function set_chords() {
var i, ch,
pch = '-'
for (i = 0; i < chords.length; i++) {
ch = chords[i]
if (!ch[0])
ch[0] = pch
switch (ch.length) {
case 0:
continue
case 1:
pch = ch[0]
continue
case 2:
pch = ch[1]
continue
case 3:
pch = ch[2]
continue
}
pch = ch[3]
}
} // set_chords()
// get the original length of chord symbol
function get_l(p) {
var i,
l = 0
for (i = 0; i < p.length; i++) {
if (p[i] == '<') {
while (p[i] != '>')
i++
continue
}
l++
}
return l
} // get_l()
function build_cell(c) {
var i, j, k,
u = '',
t = ''
row += ' '
if (c.length == 1) {
row += c[0] // one chord in the measure
return
}
i = 0
while (i < 4) {
t += c[i]
k = get_l(c[i])
j = k
while (--j >= 0)
u += ' '
if (i < 3 && !c[i + 1]) {
while (i < 3 && !c[i + 1])
i++
i++
if (i < 4 && c[i])
t += ' '
} else {
j = k
t += '<tspan dx="-'
+ (j * .6).toFixed(1)
+ 'em" dy=".2em">'
while (--j >= 0)
t += '_'
t += '</tspan>'
i++
if (i < 4 && c[i])
t += '<tspan dy="-.2em"> </tspan>'
else
t += '<tspan dx="-.6em" dy="-.2em"> </tspan>'
}
}
row += t + '<tspan dx="-'
+ (.6 * u.length).toFixed(1)
+ 'em" text-decoration="underline">'
+ u
+ '</tspan>'
} // build_cell()
// ------- build_nns() -------
// set some chords in each measure
set_chords()
// build the content of the measures
if (!nns.ls) {
cells = chords
} else { // with list of mesure numbers
bar = bars
bars = [ ]
for (i = 0; i < nns.ls.length; i++) {
l = nns.ls[i]
if (l.indexOf('-') < 0)
l = [l, l]
else
l = l.split('-')
for (k = l[0] - 1; k < l[1]; k++) {
if (!chords[k]) // error
break
cells.push(chords[k])
bars.push(bar[k])
}
}
bars.push(bar[k]) // ending bar
}
// get the number of columns
if (nc < 0)
nc = -nc
if (nc < 3) { // auto
nc = cells.length % 6 == 0 ? 6 : 8
if (nc == 8 && cells.length < 12)
nc = 4
}
if (nc > cells.length)
nc = cells.length
// generate the rows
abc.set_font('nns')
x = img.lm + 30
y = -1 + font.size * .6
nr = 0
hr = font.size * 2
for (i = 0; i < cells.length; i++) {
if (i == 0
|| (nns.repbrk
&& (bars[i].slice(-1) == ':' || bars[i][0] == ':'))
|| parts[i]
|| k >= nc) {
if (row) {
abc.out_svg('<text class="'
+ abc.font_class(font)
+ '" x="')
abc.out_sxsy(x, '" y="', y)
abc.out_svg('">' + row + '</text>\n')
}
row = ''
y -= hr // new row
k = 0
nr++
if (parts[i]) {
w = font.size * parts[i].length * .6 + 10
if (w < 50)
w = 50
abc.out_svg('<text class="'
+ abc.font_class(font)
+ ' box'
+ '" x="')
abc.out_sxsy(x - w, '" y="', y)
abc.out_svg('">' + parts[i] + '</text>\n')
}
}
k++
if (bars[i].slice(-1) == ':')
row += ' |:'
build_cell(cells[i])
if (bars[i + 1][0] == ':')
row += ' :|'
}
if (row) {
abc.out_svg('<text class="'
+ abc.font_class(font)
+ '" x="')
abc.out_sxsy(x, '" y="', y)
abc.out_svg('">' + row + '</text>\n')
}
abc.vskip(hr * nr + 6)
} // build_nns()
// ----- block_gen() -----
var p_voice, n, font, f2
abc.set_page()
img = abc.get_img()
// set the text style
if (!cfmt.nnsfont)
abc.param_set_font("nnsfont", "monospace 16")
font = abc.get_font('nns')
// create the chord list
abc.blk_flush()
build_nns(s, font)
abc.blk_flush()
}, // block_gen()
// hook before the generation
set_stems: function(of) {
var C, tsfirst, voice_tb, fmt, p_v, s, s2,
abc = this,
nns = abc.cfmt().nns
function get_beat(s) {
var beat = C.BLEN / 4
if (!s.a_meter[0] || s.a_meter[0].top[0] == 'C'
|| !s.a_meter[0].bot)
return beat
beat = C.BLEN / s.a_meter[0].bot[0] |0
if (s.a_meter[0].bot[0] == 8
&& s.a_meter[0].top[0] % 3 == 0)
beat = C.BLEN / 8 * 3
return beat
} // get_beat()
// transpose the chord back to "C"
function set_nm(p, tr, mode) {
var i, o, o2, a, n,
csa = []
i = p.indexOf('/') // get the bass if any
if (i > 0) {
while (1) {
if (p[i -1] != '<')
break
i = p.indexOf('/', i + 1)
if (i < 0)
break
}
}
if (i < 0) {
csa.push(p)
} else {
csa.push(p.slice(0, i))
csa.push(p.slice(i + 1))
}
for (i = 0; i < csa.length; i++) { // main and optional bass
p = csa[i]
o = p.search(/[A-G]/) // get the base chord
if (o < 0)
continue // not a chord!
a = 0
o2 = o + 1
if (p[o2] == '#' || p[o2] == '♯') {
a++
o2++
} else if (p[o2] == 'b' || p[o2] == '♭') {
a--
o2++
}
n = ([0, 2, 4, 5, 7, 9, 11]
[abc2svg.nns.note_nm.indexOf(p[o])]
+ a
+ tr) % 12
if (!nns.roman) {
n = abc2svg.nns.nns_nm[n] // major and minor
} else if (nns.roman == 1) {
if (!mode)
n = abc2svg.nns.rnn_nm[n]
else
n = abc2svg.nns.rnn_nm_m[n]
} else {
if (!mode)
n = abc2svg.nns.rnn2_nm[n]
else
n = abc2svg.nns.rnn2_nm_m[n]
if (p[o2] == 'm')
o2++
}
csa[i] = p.slice(0, o)
+ n
+ p.slice(o2)
}
return csa.join('/')
} // set_nm()
// build the arrays of chords and bars
function build_chords(sb) { // block 'nns'
var s, i, w, bt, rep,
bars = [],
chords = [],
parts = [],
chord = [],
beat = get_beat(voice_tb[0].meter),
wm = voice_tb[0].meter.wmeasure,
cur_beat = 0,
beat_i = 0,
tr = (tsfirst.p_v.key.k_sf + 12) * 5, // transposition to "C"
mode = tsfirst.p_v.key.k_mode
// scan all the symbols
bars.push('|')
for (s = tsfirst; s; s = s.ts_next) {
while (s.time > cur_beat) {
if (beat_i < 3) // only 2, 3 or 4 beats / measure...
beat_i++
cur_beat += beat
}
if (s.part)
parts[chords.length] = s.part.text
switch (s.type) {
case C.KEY:
tr = (s.k_sf + 12) * 5
mode = s.k_mode
break
case C.NOTE:
case C.REST:
if (!s.a_gch)
break
// search a chord symbol
for (i = 0; i < s.a_gch.length; i++) {
if (s.a_gch[i].type == 'g') {
if (!chord[beat_i]) {
chord[beat_i] =
set_nm(s.a_gch[i].text,
tr, mode)
}
break
}
}
break
case C.BAR:
bt = s.bar_type
if (s.time < wm) { // if anacrusis
if (chord.length) {
chords.push(chord)
bars.push(bt)
} else {
bars[0] = bt
}
} else {
if (!s.bar_num) // if not normal measure bar
break
chords.push(chord)
bars.push(bt)
}
chord = []
cur_beat = s.time // synchronize in case of error
beat_i = 0
if (bt.indexOf(':') >= 0)
rep = true // some repeat
while (s.ts_next && s.ts_next.type == C.BAR)
s = s.ts_next
break
case C.METER:
beat = get_beat(s)
wm = s.wmeasure
break
}
}
if (chord.length) {
bars.push('')
chords.push(chord)
}
if (!chords.length)
return // no chord in this tune
sb.chords = chords
sb.bars = bars
if (parts.length)
sb.parts = parts
} // build_chords
// -------- set_stems --------
// create a specific block
if (nns) {
C = abc2svg.C
tsfirst = this.get_tsfirst()
fmt = tsfirst.fmt
voice_tb = this.get_voice_tb()
p_v = voice_tb[this.get_top_v()]
s = {
type: C.BLOCK,
subtype: 'nns',
dur: 0,
time: 0,
p_v: p_v,
v: p_v.v,
st: p_v.st
}
build_chords(s) // build the array of the chords
// and insert it in the tune
if (!s.chords) { // if no chord
;
} else if (nns.nomusic) { // if just the chords
this.set_tsfirst(s)
} else if (nns.n < 0) { // below
for (s2 = tsfirst; s2.ts_next; s2 = s2.ts_next)
;
s.time = s2.time
s.prev = p_v.last_sym.prev // before the last symbol
s.prev.next = s
s.next = p_v.last_sym
p_v.last_sym.prev = s
s.ts_prev = s2.ts_prev
s.ts_prev.ts_next = s
s.ts_next = s2
s2.ts_prev = s
if (s2.seqst) {
s.seqst = true
s2.seqst = false
}
} else { // above
s.next = p_v.sym
s.ts_next = tsfirst
tsfirst.ts_prev = s
this.set_tsfirst(s)
p_v.sym.prev = s
p_v.sym = s
}
s.fmt = s.prev ? s.prev.fmt : fmt
}
of()
}, // set_stems()
set_fmt: function(of, cmd, parm) {
if (cmd == "nns") {
if (!parm)
parm = "1"
parm = parm.split(/\s+/)
var nns = {
n: Number(parm.shift())
}
if (isNaN(nns.n)) {
if (parm.length) {
this.syntax(1, this.errs.bad_val, "%%nns")
return
}
nns.n = 1
}
while (parm.length) {
var item = parm.shift()
if (item == "nomusic")
nns.nomusic = true
else if (item == "roman")
nns.roman = 1
else if (item == "repbrk")
nns.repbrk = true
else if (item.slice(0, 8) == "include=")
nns.ls = item.slice(8).split(',')
else if (item.slice(0, -1) == "roman=")
nns.roman = item.slice(-1) == "1" ? 1 : 2
}
this.cfmt().nns = nns
return
}
of(cmd, parm)
},
set_hooks: function(abc) {
abc.block_gen = abc2svg.nns.block_gen.bind(abc, abc.block_gen)
abc.set_stems = abc2svg.nns.set_stems.bind(abc, abc.set_stems)
abc.set_format = abc2svg.nns.set_fmt.bind(abc, abc.set_format)
}
} // nns
if (!abc2svg.mhooks)
abc2svg.mhooks = {}
abc2svg.mhooks.nns = abc2svg.nns.set_hooks