// MIDI.js - module to handle the %%MIDI parameters
//
// Copyright (C) 2019-2025 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 "%%MIDI" appears in a ABC source.
//
// Parameters (see abcMIDI - https://abcmidi.sourceforge.io/ - for details)
// %%MIDI channel n
// %%MIDI program [channel] n
// %%MIDI control k v
// %%MIDI drummap ABC_note MIDI_pitch
// %%MIDI temperamentequal nedo
// %%MIDI gchordbars n
// %%MIDI chordname <chord_type> <list of MIDI pitches>
// %%MIDI chordprog <#MIDI program> [octave=<n>]
// %%MIDI chordvol <volume>
// %%MIDI gchord <string>
// %%MIDI gchordon
// %%MIDI gchordoff
// Using %%MIDI drummap creates a voicemap named "MIDIdrum".
// This name must be used if some print map is required:
// %%MIDI drummap g 42
// %%map MIDIdrum g heads=x
// A same effect may be done by
// %%percmap g 42 x
// but this is not abcMIDI compatible!
if (typeof abc2svg == "undefined")
var abc2svg = {}
abc2svg.MIDI = {
// parse %%MIDI commands
do_midi: function(parm) {
// build the equal temperament as a b40 float array
function tb40(qs) {
// b40 C D E F G A B
// [ 2, 8, 14, 19, 25, 31, 37 ]
var i,
// C G D A E B ^F ^C ^G ^D ^A ^E ^B^^F^^C^^G^^D^^A^^E^^B
n1 = [2,25, 8,31,14,37,20, 3,26, 9,32,15,38,21, 4,27,10,33,16,39],
// C F _B _E _A _D _G _C _F__B__E__A__D__G__C__F
n2 = [0,19,36,13,30, 7,24, 1,18,35,12,29, 6,23, 0,17],
da = 21 - 3 * qs, // 21 = 12 (octave) + 9 (A)
b = new Float32Array(40)
for (i = 0; i < n1.length; i++)
b[n1[i]] = (qs * i + da) % 12
for (i = 1; i <= n2.length; i++)
b[n2[i]] = 12 - (qs * i - da) % 12
return b
} // tb40()
// do_midi()
var n, v, s, maps,
o, q, n, qs,
a = parm.split(/\s+/),
abc = this,
cfmt = abc.cfmt(),
curvoice = abc.get_curvoice(),
parse = abc.get_parse()
if (curvoice) {
if (curvoice.ignore)
return
if (curvoice.chn == undefined)
curvoice.chn = curvoice.v < 9 ?
curvoice.v :
curvoice.v + 1
}
switch (a[1]) {
// case "bassprog": // %%MIDI bassprog <#MIDI program> [octave=<n>]
// break
// case "bassvol": // %%MIDI bassvol <volume>
// break
// case "beatstring": // %%MIDI beatstring <string of fmp>
// break
case "chordname": // %%MIDI chordname <list of MIDI pitches>
// example: %%MIDI chordname m 0 3 7
if (!cfmt.chord)
cfmt.chord = {}
if (!cfmt.chord.names)
cfmt.chord.names = {}
cfmt.chord.names[a[2]] = a.slice(3)
break
case "chordprog": // %%MIDI chordprog <#MIDI program> [octave=<n>]
if (!cfmt.chord)
cfmt.chord = {}
cfmt.chord.prog = a[2]
if (a[3] && a[3].slice(0, 7) == "octave=")
cfmt.chord.trans = Number(a[3].slice(7))
break
case "chordvol": // %%MIDI chordvol <volume>
v = Number(a[2])
if (isNaN(v) || v < 0 || v > 127) {
abc.syntax(1, abc.errs.bad_val, "%%MIDI chordvol")
break
}
if (!cfmt.chord)
cfmt.chord = {}
cfmt.chord.vol = v
break
// case "drone": // %%MIDI drone <#prog> <pit_1> <pit_2> <vol_1> <vol_2>
// // default: 70 45 33 80 80
// break
// case "droneon": // %%MIDI droneon
// break
// case "droneoff": // %%MIDI droneoff
// break
case "gchord": // %%MIDI gchord <list of letters and repeat numbers>
// // z rest
// // c chord
// // f fundamental
// // b fundamental + chord
// // G/H/I/J/K individual notes starting
// // from the lowest note of the chord
// // g/h/i/j/k an octave above these
// // defaults:
// // M:2/4 or 4/4 fzczfzcz
// // M:3/4 fzczcz
// // M:6/8 fzcfzc
// // M:9/8 fzcfzcfzc
// fall thru
case "gchordbars": // %%MIDI gchordbars n
case "gchordon": // %%MIDI gchordon
case "gchordoff": // %%MIDI gchordoff
if (!cfmt.chord)
cfmt.chord = {}
if (parse.state >= 2
&& curvoice) {
s = abc.new_block("midigch")
s.play = s.invis = 1 //true
if (a[1][6] == 'o')
s.on = a[1][7] == 'n'
else if (a[1][6] == 'b')
s.gchnb = +a[2]
else
s.rhy = a[2] // chord rhythm
} else if (a[1][6] == 'o') {
cfmt.chord.gchon = a[1][7] == 'n'
} else if (a[1][6] == 'b') {
cfmt.chord.gchnb = +a[2]
} else {
cfmt.chord.rhy = a[2]
}
break
case "channel":
v = parseInt(a[2])
if (isNaN(v) || v <= 0 || v > 16) {
abc.syntax(1, abc.errs.bad_val, "%%MIDI channel")
break
}
v-- // channel range 1..16 => 0..15
if (parse.state >= 2) {
s = abc.new_block("midiprog")
s.play = s.invis = 1 //true
s.chn = v
} else {
abc.set_v_param("channel", v)
}
break
case "drummap":
//fixme: should have a 'MIDIdrum' per voice?
v = Number(a[3])
if (isNaN(v)) {
abc.syntax(1, abc.errs.bad_val, "%%MIDI drummap")
break
}
n = ["C","^C","D","_E","E","F","^F","G","^G","A","_B","B"][v % 12]
while (v < 60) {
n += ','
v += 12
}
while (v > 72) {
n += "'"
v -= 12
}
this.do_pscom("map MIDIdrum " + a[2] + " play=" + n)
abc.set_v_param("mididrum", "MIDIdrum")
break
case "program":
a.shift()
v = []
if (a[2]) {
v[0] = +a[2] // program
v[1] = +a[1] // channel
} else {
v[0] = +a[1]
v[1] = 0
}
if (isNaN(v[0]) || v[0] < 0 || v[0] > 127
|| (v[1]
&& (isNaN(v[1]) || v[1] <= 0 || v[1] > 16))) {
abc.syntax(1, abc.errs.bad_val, "%%MIDI program")
break
}
if (parse.state >= 2) {
s = abc.new_block("midiprog");
s.play = s.invis = 1 //true
s.instr = v[0]
s.chn = v[1] > 0
? (v[1] - 1)
: curvoice.v < 9 ? curvoice.v : curvoice.v + 1
} else {
abc.set_v_param("instr", a.slice(1).join(' '))
}
break
case "control":
n = parseInt(a[2])
if (isNaN(n) || n < 0 || n > 127) {
abc.syntax(1, "Bad controller number in %%MIDI")
break
}
v = parseInt(a[3])
if (isNaN(v) || v < 0 || v > 127) {
abc.syntax(1, "Bad controller value in %%MIDI")
break
}
if (parse.state >= 2) {
s = abc.new_block("midictl");
s.play = s.invis = 1 //true
s.ctrl = n;
s.val = v
} else {
abc.set_v_param("midictl", a[2] + ' ' + a[3])
}
break
case "temperamentequal":
n = parseInt(a[2])
if (isNaN(n) || n < 5 || n > 255) {
abc.syntax(1, abc.errs.bad_val, "%%MIDI " + a[1])
return
}
// define the Turkish accidentals (53-TET)
s = abc.get_glyphs()
if (n == 53
&& !s.acc12_53) { // do not redefine the glyphs
// #1
s.acc12_53 = '<text id="acc12_53" x="-1"></text>'
// #2
s.acc24_53 = '<text id="acc24_53" x="-1">\
<tspan x="0" y="-9" style="font-size:9px">2</tspan></text>'
// #3
s.acc36_53 = '<text id="acc36_53" x="-1">\
<tspan x="0" y="-9" style="font-size:9px">3</tspan></text>'
// #4
s.acc48_53 = '<text id="acc48_53" x="-1"></text>'
// #5
s.acc60_53 = '<g id="acc60_53">\n\
<text style="font-size:1.1em" x="-1"></text>\n\
<path class="stroke" stroke-width="1.6" d="M-2 1l7 -2.2"/>\n\
</g>'
// #8
s.acc96_53 = '<g id="acc96_53">\n\
<text style="font-size:1.1em" x="-2"></text>\n\
<path class="stroke" stroke-width="1.6" d="M-2.8 1.4l8.5 -2.8"/>\n\
</g>'
// #9
s.acc108_53 = '<text id="acc108_53" x="-3"></text>'
// b9
s["acc-108_53"] = '<text id="acc-108_53" x="-3"></text>'
// b8
s["acc-96_53"] = '<g id="acc-96_53">\n\
<text x="-1"></text>\n\
<path class="stroke" stroke-width="1.3" d="M-3 -7l5 -2m0 3l-5 2"/>\n\
</g>'
// b5
s["acc-60_53"] = '<text id="acc-60_53" x="-1"></text>'
// b4
s["acc-48_53"] = '<g id="acc-48_53">\n\
<text x="-1"></text>\n\
<path class="stroke" stroke-width="1.3" d="M-3 -5.5l5 -2"/>\n\
</g>'
// b3
s["acc-36_53"] = '<g id="acc-36_53">\n\
<text x="-1">\
<tspan x="0" y="-12" style="font-size:9px">3</tspan></text>\n\
<path class="stroke" stroke-width="1.3" d="M-3 -5.5l5 -2"/>\n\
</g>'
// b2
s["acc-24_53"] = '<text id="acc-24_53" x="-2">\
<tspan x="0" y="-12" style="font-size:9px">2</tspan></text>'
// b1
s["acc-12_53"] = '<text id="acc-12_53" x="-2"></text>'
}
// define the detune values
q = 7.019550008653874, // Math.log(3/2)/Math.log(2) * 12
// = just intonation fifth
o = 12 // octave
cfmt.nedo = n // octave divider
qs = ((n * q / o + .5) | 0) * o / n // new fifth
// warn on bad fifth values
if (qs < 6.85 || qs > 7.2)
abc.syntax(0, abc.errs.bad_val, "%%MIDI " + a[1])
cfmt.temper = tb40(qs) // pitches / A in 100th of cents
break
}
}, // do_midi()
// set the MIDI parameters in the current voice
set_vp: function(of, a) {
var i, item, s,
abc = this,
curvoice = abc.get_curvoice()
// set the voice parameters before inserting any block
of(a.slice(0)) // (copy because the parameters are removed)
for (i = 0; i < a.length; i++) {
switch (a[i]) {
case "channel=": // %%MIDI channel
s = abc.new_block("midiprog")
s.play = s.invis = 1 //true
s.chn = +a[++i]
break
case "instr=": // %%MIDI program
s = abc.new_block("midiprog")
s.play = s.invis = 1 //true
s.instr = a[++i].split(' ')
if (s.instr[1])
s.chn = s.instr.shift() - 1
else
s.chn = curvoice.v < 9 ?
curvoice.v :
curvoice.v + 1
s.instr = +s.instr[0]
break
case "midictl=": // %%MIDI control
if (!curvoice.midictl)
curvoice.midictl = []
item = a[++i].split(' ');
curvoice.midictl[item[0]] = Number(item[1])
break
case "mididrum=": // %%MIDI drummap note midipitch
if (!curvoice.map)
curvoice.map = {}
curvoice.map = a[++i]
break
}
}
}, // set_vp()
do_pscom: function(of, text) {
if (text.slice(0, 5) == "MIDI ")
abc2svg.MIDI.do_midi.call(this, text)
else
of(text)
},
set_hooks: function(abc) {
abc.do_pscom = abc2svg.MIDI.do_pscom.bind(abc, abc.do_pscom);
abc.set_vp = abc2svg.MIDI.set_vp.bind(abc, abc.set_vp)
}
} // MIDI
if (!abc2svg.mhooks)
abc2svg.mhooks = {}
abc2svg.mhooks.MIDI = abc2svg.MIDI.set_hooks