// gamelan.js - module to output Gamelan (indonesian) music sheets
//
// Copyright (C) 2020-2024 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 "%%gamelan" appears in a ABC source.
//
// Parameters (none)
// %%gamelan 1
// scale:
// - sléndro - 5 equal tones
// 5-TET C D E G A
// detune 0 +40 +80 . +20 +60 .
// - pélog - 7 unequal tones
// D _E+ F- ^G+ A _B c+ d
// 1 2 3 4 5 6 7 1
// (first note = ding)
// Bali
// selisir ^C D E ^G A 12356
// tembung E F A B c 45612
// sunaren F G B c d (?) 56723
// Java
// bem D_E ^G A _B 12456
// barang _E F A _B c 23567
// numbers
// ?? E F G B c
// ?? C ^C _E G _A (or ^C D E ^G A)
// dot = continuation (not rest)
if (typeof abc2svg == "undefined")
var abc2svg = {}
abc2svg.gamelan = {
cde2fcg: new Int8Array([0, 2, 4, -1, 1, 3, 5]),
cgd2cde: new Int8Array([0, -4, -1, -5, -2, -6, -3,
0, -4, -1, -5, -2, -6, -3, 0]),
acc2: new Int8Array([-2, -1, 3, 1, 2]),
// change %%staves and %%score
do_pscom: function(of, p) {
switch (p.match(/\w+/)[0]) {
case 'staves':
case 'score':
p = p.replace(/\(|\)/g, '')
break
}
of(p)
},
// adjust some symbols before the generation
output_music: function(of) {
var v,
C = abc2svg.C,
abc = this,
cur_sy = abc.get_cur_sy(),
voice_tb = abc.get_voice_tb()
if (!abc.cfmt().gamelan) {
of()
return
}
// expand dots and long notes/rests
function slice(s) {
var m, n, s2, s3, d, d_orig
if (s.dur <= C.BLEN * 3 / 8) {
if ((s.dur_orig / 9 | 0) * 9 != s.dur_orig)
return
d = s.dur / 3
d_orig = s.dur_orig / 3
s.dur -= d
s.dur_orig -= d_orig
n = 1
} else {
if (s.dur >= C.BLEN)
n = 3
else if (s.dur == C.BLEN / 2)
n = 1
else
n = 2
d = d_orig = C.BLEN / 4
s.dur = s.dur_orig = C.BLEN / 4
}
for (m = 0; m <= s.nhd; m++)
s.notes[m].dur = s.dur
s.beam_on = true
while (--n >= 0) {
s2 = {
type: C.REST,
v: s.v,
p_v: s.p_v,
st: s.st,
fmt: s.fmt,
dur: d,
dur_orig: d_orig,
stem: 1,
multi: 0,
nhd: 0,
notes: [{
pit: s.notes[0].pit,
jn: 8
}],
xmx: 0,
beam_on: true,
noplay: true,
time: s.time + s.dur,
prev: s,
next: s.next
}
s.next = s2
if (s2.next)
s2.next.prev = s2
if (!s.ts_next) {
s.ts_next = s2
if (s.soln)
s.soln = false
s2.ts_prev = s
s2.seqst = true
} else {
for (s3 = s.ts_next; s3; s3 = s3.ts_next) {
if (s3.time < s2.time)
continue
if (s3.time > s2.time) {
s2.seqst = true
s3 = s3.ts_prev
}
s2.ts_next = s3.ts_next
s2.ts_prev = s3
if (s2.ts_next)
s2.ts_next.ts_prev = s2
s3.ts_next = s2
break
}
}
s = s2
}
} // slice()
// replace the tied notes by a '.'
function do_tie(s) {
var end_time = s.time + s.dur
while (1) {
s = s.ts_next
if (!s || s.time > end_time)
return // ?!
if (s.type == C.NOTE
&& s.time == end_time)
break
}
s.notes[0].jn = 8
s.notes[0].jo = 2
} // do_tie()
function set_sym(p_v) {
var s, s2, note, pit, nn, p, a, m, i,
sf = p_v.key.k_sf,
delta = abc2svg.gamelan.cgd2cde[sf + 7] - 2
delete p_v.key.k_a_acc // no accidental
// no (visible) clef
p_v.clef.invis = true
// scan the voice
for (s = p_v.sym; s; s = s.next) {
s.st = p_v.st
switch (s.type) {
case C.CLEF:
s.invis = true
// continue
default:
continue
case C.KEY:
delta = abc2svg.gamelan.cgd2cde[s.k_sf + 7] - 2
continue
case C.REST:
if (s.notes[0].jn)
continue
s.notes[0].jn = 0
s.notes[0].pit = 21
slice(s)
continue
case C.NOTE: // change the notes
break
}
s.stem = 1
s.stemless = true
// set the slurs offset and direction
if (s.sls) {
for (i = 0; i < s.sls.length; i++)
s.sls[i].ty = C.SL_BELOW
}
for (m = 0; m <= s.nhd; m++) {
note = s.notes[m]
// note head
p = note.pit
pit = p + delta
if (note.jn == undefined) { // if not tied
note.jn = ((pit + 77) % 7) + 1 // note number
note.jo = (pit / 7) | 0 // octave number
}
// set a fixed offset to the note
// for the slurs and decorations
note.pit = 21 // "A"
// accidentals
a = note.acc
if (a) {
nn = abc2svg.gamelan.cde2fcg[(p + 5 + 16 * 7) % 7] - sf
if (a != 3)
nn += a * 7
nn = ((((nn + 1 + 21) / 7) | 0) + 2 - 3 + 32 * 5) % 5
note.acc = abc2svg.gamelan.acc2[nn]
}
if (note.tie_ty) {
do_tie(s)
delete note.tie_ty
}
}
// change the dots and the long notes
slice(s)
// replace the staccato dot
if (s.a_dd) {
for (i = 0; i < s.a_dd.length; i++) {
if (s.a_dd[i].glyph == "stc") {
abc.deco_put("gstc", s)
s.a_dd[i] = s.a_dd.pop()
}
}
}
}
} // set_sym()
// -- output_music --
for (v = 0; v < voice_tb.length; v++)
set_sym(voice_tb[v])
of()
}, // output_music()
draw_symbols: function(of, p_voice) {
var i, m, nl, note, s, s2, x, y,
C = abc2svg.C,
abc = this,
dot = "\ue1e7",
staff_tb = abc.get_staff_tb(),
out_svg = abc.out_svg,
out_sxsy = abc.out_sxsy,
xypath = abc.xypath
if (!abc.cfmt().gamelan) {
of(p_voice)
return
}
// draw the duration lines above the notes
function draw_dur(s1, y, s2, n, nl) {
var s, s3
xypath(s1.x - 3, y + 24)
out_svg('h' + (s2.x - s1.x + 8).toFixed(1) + '"/>\n') // "
y -= 2.5
while (++n <= nl) {
s = s1
while (1) {
if (s.nflags && s.nflags >= n) {
s3 = s
while (s != s2) {
if (s.next.beam_br1
|| (s.next.beam_br2 && n > 2)
|| (s.next.nflags
&& s.next.nflags < n))
break
s = s.next
}
draw_dur(s3, y, s, n, nl)
}
if (s == s2)
break
s = s.next
}
}
} // draw_dur()
function out_mus(x, y, p) {
out_svg('<text x="')
out_sxsy(x, '" y="', y)
out_svg('">' + p + '</text>\n')
} // out_txt()
function out_txt(x, y, p) {
out_svg('<text class="bn" x="')
out_sxsy(x, '" y="', y)
out_svg('">' + p + '</text>\n')
} // out_txt()
function draw_hd(s, x, y) {
var m, note, ym
for (m = 0; m <= s.nhd; m++) {
note = s.notes[m]
out_txt(x - 3.5, y + 8, "01234567."[note.jn])
if (note.acc) {
out_svg('<path class="stroke" stroke-width="1.1" d="M')
if (note.acc > 0) {
out_sxsy(x - 6, ' ', y + 10)
out_svg("l12 -6")
} else {
out_sxsy(x - 6, ' ', y + 16)
out_svg("l12 6")
}
out_svg('"/>\n')
}
if (note.jo > 2) {
out_mus(x - 1, y + 23, dot)
if (note.jo > 3) {
y += 3
out_mus(x - 1, y + 23.4, dot)
}
} else if (note.jo < 2) {
ym = y + 4
out_mus(x - 1, ym, dot)
}
y += 20
}
} // draw_hd()
// -- draw_symbols --
for (s = p_voice.sym; s; s = s.next) {
if (s.invis)
continue
switch (s.type) {
case C.NOTE:
case C.REST:
x = s.x
y = staff_tb[s.st].y
draw_hd(s, x, y)
break
}
}
// draw the (pseudo) beams
for (s = p_voice.sym; s; s = s.next) {
if (s.invis)
continue
switch (s.type) {
case C.NOTE:
case C.REST:
nl = s.nflags
if (nl <= 0)
continue
y = staff_tb[s.st].y
s2 = s
while (s.next && s.next.nflags > 0) {
s = s.next
if (s.nflags > nl)
nl = s.nflags
if (s.beam_end)
break
}
draw_dur(s2, y + 7, s, 1, nl)
break
}
}
}, // draw_symbols()
// set some parameters
set_fmt: function(of, cmd, param) {
if (cmd == "gamelan") {
var cfmt = this.cfmt()
if (!this.get_bool(param))
return
cfmt.gamelan = true
cfmt.staffsep = 20
cfmt.sysstaffsep = 14
this.set_v_param("stafflines", "...")
cfmt.tuplets = [0, 1, 0, 1] // [auto, slur, number, above]
return
}
of(cmd, param)
}, // set_fmt()
// adjust some values
set_pitch: function(of, last_s) {
of(last_s)
if (!last_s
|| !this.cfmt().gamelan)
return // first time
var C = abc2svg.C
for (var s = this.get_tsfirst(); s; s = s.ts_next) {
switch (s.type) {
// adjust the vertical spacing above the note heads
case C.NOTE:
s.ymx = 20 * s.nhd + (s.nflags > 0 ? 30 : 24)
if (s.notes[s.nhd].jo > 2) {
s.ymx += 3
if (s.notes[s.nhd].jo > 3)
s.ymx += 2
}
s.ys = s.ymx // (for tuplets)
break
}
}
}, // set_pitch()
// set the width of some symbols
set_width: function(of, s) {
of(s)
if (!this.cfmt().gamelan)
return
var w, m, note,
C = abc2svg.C
switch (s.type) {
case C.CLEF:
case C.KEY:
case C.METER:
s.wl = s.wr = .1 // (must not be null)
break
}
}, // set_width()
set_hooks: function(abc) {
abc.do_pscom = abc2svg.gamelan.do_pscom.bind(abc, abc.do_pscom)
abc.draw_symbols = abc2svg.gamelan.draw_symbols.bind(abc, abc.draw_symbols)
abc.output_music = abc2svg.gamelan.output_music.bind(abc, abc.output_music)
abc.set_format = abc2svg.gamelan.set_fmt.bind(abc, abc.set_format)
abc.set_pitch = abc2svg.gamelan.set_pitch.bind(abc, abc.set_pitch)
abc.set_width = abc2svg.gamelan.set_width.bind(abc, abc.set_width)
// big staccato dot
abc.get_glyphs().gstc = '<circle id="gstc" cx="0" cy="-3" r="2"/>'
abc.get_decos().gstc = "0 gstc 5 1 1"
abc.add_style("\n.bn {font-family:sans-serif; font-size:16px}")
} // set_hooks()
} // gamelan
if (!abc2svg.mhooks)
abc2svg.mhooks = {}
abc2svg.mhooks.gamelan = abc2svg.gamelan.set_hooks