// sndgen.js - sound generation
//
// Copyright (C) 2019-2020 Jean-Francois Moine
//
// This file is part of abc2svg-core.
//
// abc2svg-core 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-core 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-core. If not, see <http://www.gnu.org/licenses/>.
// This script generates the play data which are stored in the music symbols:
// - in all symbols
// s.ptim = play time
// - in BAR
// rep_p = on a right repeat bar, pointer to the left repeat symbol
// rep_s = on the first repeat variant, array of pointers to the next symbols,
// indexed by the repeat number
// - in NOTE and REST
// s.pdur = play duration
// s.instr = bank + instrument
// s.chn = MIDI channel
// - in the notes[] of NOTE
// s.notes[i].midi
function ToAudio() {
return {
// generate the play data of a tune
add: function(first, // starting symbol
voice_tb) { // voice table
var C = abc2svg.C,
p_time = 0, // last playing time
abc_time = 0, // last ABC time
play_fac = C.BLEN / 4 * 120 / 60, // play time factor - default: Q:1/4=120
i, n, dt, d, v, c,
s = first,
rst = s, // left repeat (repeat restart)
rst_fac, // play factor on repeat restart
rsk, // repeat variant array (repeat skip)
instr = [], // [voice] bank + instrument
chn = [] // [voice] MIDI channel
// adjust the MIDI pitches according to the transpositions
function midi_transp(s, voice_tb) {
var p_v, s,
temper = voice_tb[0].temper, // (set by the module temper.js)
v = voice_tb.length
// loop on the voice symbols
function vloop(s, sndtran, ctrans) {
var i, g, note,
transp = sndtran + ctrans
function set_note(note) {
var m = abc2svg.b40m(note.b40 + transp)
if (temper // if not equal temperament
&& (!note.acc
|| note.acc | 0 == note.acc)) // and not micro-tone
m += temper[m % 12]
note.midi = m
} // set_note()
while (s) {
switch (s.type) {
case C.CLEF:
ctrans = (s.clef_octave && !s.clef_oct_transp) ?
(s.clef_octave / 7 * 40) : 0
transp = ctrans + sndtran
break
case C.KEY:
if (s.k_sndtran != undefined) {
sndtran = s.k_sndtran
transp = ctrans + sndtran
}
break
case C.GRACE:
for (g = s.extra; g; g = g.next) {
for (i = 0; i <= g.nhd; i++)
set_note(g.notes[i])
}
break
case C.NOTE:
for (i = 0; i <= s.nhd; i++)
set_note(s.notes[i])
break
}
s = s.next
}
} // vloop()
// initialize the clefs and keys
while (--v >= 0) {
p_v = voice_tb[v]
if (!p_v.sym)
continue
if (p_v.key.k_bagpipe
&& !temper)
// detune in cents for just intonation in A
// C ^C D _E E F ^F G _A A _B B
// 15.3 -14.0 -2.0 -10.0 1.9 13.3 -16.0 -31.8 -12.0 0.0 11.4 3.8
// (C is ^C, F is ^F and G is =G)
// 86 84
// temp = [100-14, -14, -2, -10, 2, 100-16, -16, -32, -12, 0, 11, 4]
// but 'A' bagpipe = 480Hz => raise Math.log2(480/440)*1200 = 151
temper = new Float32Array([
2.37, 1.37, 1.49, 1.41, 1.53, 2.35, 1.35, 1.19, 1.39, 1.51, 1.62, 1.55
])
s = p_v.clef
vloop(p_v.sym,
p_v.key.k_sndtran || 0,
s.clef_octave && !s.clef_oct_transp ?
(s.clef_octave / 7 * 40) : 0)
}
} // midi_transp()
// build the information about the parts
function build_parts(first) {
var i, j, c, n, v,
s = first,
p = s.parts,
st = [],
r = ""
// build a linear string of the parts
for (i = 0; i < p.length; i++) {
c = p[i]
switch (c) {
case '.':
continue
case '(':
st.push(r.length)
continue
case ')':
j = st.pop()
if (j == undefined)
j = r.length
continue
}
if (c >= 'A' && c <= 'Z') {
j = r.length
r += c
continue
}
n = Number(c)
//fixme:one digit is enough!
// while (1) {
// c = p[i + 1]
// if (c < '0' || c > '9')
// break
// n = n * 10 + Number(c)
// i++
// }
if (isNaN(n))
break
v = r.slice(j)
if (r.length + v.length * n > 128)
continue
while (--n > 0)
r += v
}
s.parts = r
// build the part table in the first symbol
// and put the reverse pointers in the P: symbols
s.p_s = [] // pointers to the parts
while (1) {
if (!s.ts_next) {
s.part = first // end of tune = end of part
break
}
s = s.ts_next
if (s.type == C.PART) {
s.part = first // reverse pointer
for (i = 0; i < first.parts.length; i++) {
if (first.parts[i] == s.text)
first.p_s[i] = s
}
}
}
} // build_parts()
// handle a block symbol
function do_block(s) {
var v = s.v,
c = chn[v]
switch (s.subtype) {
case "midichn":
chn[v] = s.chn
break
case "midictl":
switch (s.ctrl) {
case 0: // MSB bank
instr[c] = (instr[c] & 0x3fff) |
(s.val << 14)
break
case 32: // LSB bank
instr[c] = (instr[c] & 0x1fc07f) |
(s.val << 7)
break
// case 121: // reset all controllers
// instr = []
// break
}
if ((instr[c] & ~0x7f) == 16384) { // if percussion
instr[9] = instr[c] // force the channel 10
chn[v] = c = 9
}
s.chn = c
break
case "midiprog":
instr[c] = (instr[c] & ~0x7f) | s.instr
s.chn = c
break
}
} // do_block()
// generate the grace notes
function gen_grace(s) {
var g, i, n, t, d, s2,
next = s.next
// before beat
if (s.sappo) {
d = C.BLEN / 16
} else if ((!next || next.type != C.NOTE)
&& s.prev && s.prev.type == C.NOTE) {
d = s.prev.dur / 2
// on beat
} else {
d = next.dur / 12
if (!(d & (d - 1)))
d = next.dur / 2 // no dot
else
d = next.dur / 3
next.time += d
next.dur -= d
}
//fixme: assume the grace notes in the sequence have the same duration
n = 0
for (g = s.extra; g; g = g.next)
n++
d /= n * play_fac
t = p_time
for (g = s.extra; g; g = g.next) {
g.ptim = t
g.pdur = d
g.chn = chn[s.v]
g.instr = instr[g.chn]
t += d
}
} // gen_grace()
// change the tempo
function set_tempo(s) {
var i,
d = 0,
n = s.tempo_notes.length
for (i = 0; i < n; i++)
d += s.tempo_notes[i]
return d * s.tempo / 60
} // set_tempo()
function set_variant(rsk, n, s) {
var d
n = n.match(/[1-8]-[2-9]|[1-9,.]|[^\s]+$/g)
while (1) {
d = n.shift()
if (!d)
break
if (d[1] == '-')
for (i = d[0]; i <= d[2]; i++)
rsk[i] = s
else if (d >= '1' && d <= '9')
rsk[Number(d)] = s
else if (d != ',')
rsk.push(s) // last
}
} // set_variant()
// add() main
// transpose the MIDI pitches
midi_transp(s, voice_tb)
if (s.parts)
build_parts(s)
// set the time parameters
rst = s
rst_fac = play_fac
while (s) {
if (s.noplay) { // in display macro sequence
s = s.ts_next
continue
}
dt = s.time - abc_time
if (dt > 0) {
p_time += dt / play_fac
abc_time = s.time
}
s.ptim = p_time
v = s.v
c = chn[v] // channel
if (c == undefined) {
if ((instr[v] & ~0x7f) == 16384) // if bank 128 (percussion)
c = 9 // channel '10'
else
c = v < 9 ? v : v + 1
chn[v] = c
}
if (instr[c] == undefined)
instr[c] = voice_tb[v].instr || 0
switch (s.type) {
case C.BAR:
if (s.text && rsk) { // if new variant
set_variant(rsk, s.text, s)
play_fac = rst_fac
}
// right repeat
if (s.bar_type[0] == ':') {
s.rep_p = rst // :| to |:
if (rsk)
s.rep_v = rsk // for knowing the number of variants
}
// left repeat
if (s.bar_type.slice(-1) == ':') {
rst = s
rst_fac = play_fac
rsk = null
// 1st time repeat
} else if (s.text && s.text[0] == '1'
&& !rsk) { // error if |1 already
s.rep_s = rsk = [null] // repeat skip
set_variant(rsk, s.text, s)
rst_fac = play_fac
}
while (s.ts_next && !s.ts_next.seqst) {
s = s.ts_next
s.ptim = p_time
}
break
case C.BLOCK:
do_block(s)
break
case C.GRACE:
if (s.time == 0 // if before beat at start time
&& abc_time == 0) {
dt = 0
if (s.sappo)
dt = C.BLEN / 16
else if (!s.next || s.next.type != C.NOTE)
dt = d / 2
abc_time -= dt
}
gen_grace(s)
break
case C.REST:
case C.NOTE:
d = s.dur
if (s.next && s.next.type == C.GRACE) {
dt = 0
if (s.next.sappo)
dt = C.BLEN / 16
else if (!s.next.next || s.next.next.type != C.NOTE)
dt = d / 2
s.next.time -= dt
d -= dt
}
d /= play_fac
s.pdur = d
s.chn = c
s.instr = instr[c]
break
case C.TEMPO:
if (s.tempo)
play_fac = set_tempo(s)
break
}
s = s.ts_next
} // loop
} // add()
} // return
} // ToAudio()
// play some next symbols
//
// This function is called to start playing.
// Playing is stopped on either
// - reaching the 'end' symbol (not played) or
// - reaching the end of tune or
// - seeing the 'stop' flag (user request).
//
// The po object (Play Object) contains the following items:
// - variables
// - stime: start time
// must be set by the calling function at startup time
// - stop: stop flag
// set by the user to stop playing
// - s_cur: current symbol (next to play)
// must be set to the first symbol to be played at startup time
// - s_end: stop playing on this symbol
// this symbol is not played. It may be null.
// - conf
// - speed: current speed factor
// must be set to 1 at startup time
// - new_conf: new speed factor
// set by the user
// - internal variables
// - repn: don't repeat
// - repv: variant number
// - timouts: array of the current timeouts
// this array may be used by the upper function in case of hard stop
// - p_v: voice table used for MIDI control
// - methods
// - onend: (optional)
// - onnote: (optional)
// - note_run: start playing a note
// - get_time: return the time of the underlaying sound system
if (!abc2svg)
var abc2svg = {}
abc2svg.play_next = function(po) {
// handle a tie
function do_tie(s, midi, d) {
var i, note,
C = abc2svg.C,
v = s.v,
end_time = s.time + s.dur
// search the end of the tie
while (1) {
s = s.ts_next
if (!s)
return d
switch (s.type) {
case C.BAR:
if (s.rep_p) {
if (!po.repn) {
s = s.rep_p
end_time = s.time
}
}
if (s.rep_s) {
if (!s.rep_s[po.repv + 1])
return d
s = s.rep_s[po.repv + 1]
end_time = s.time
}
while (s.ts_next && !s.ts_next.dur)
s = s.ts_next
}
if (s.time > end_time)
return d
if (s.type == C.NOTE && s.v == v)
break
}
i = s.notes.length
while (--i >= 0) {
note = s.notes[i]
if (note.midi == midi) {
note.ti2 = true // the sound is generated
d += s.pdur / po.conf.speed
return note.tie_ty ? do_tie(s, midi, d) : d
}
}
return d
} // do_tie()
// set the MIDI controls up to now
function set_ctrl(po, s2, t) {
var i,
s = {
subtype: "midictl",
p_v: s2.p_v,
chn: s2.p_v.chn
}
for (i in s2.p_v.midictl) { // MIDI controls at voice start time
s.ctrl = i
s.val = s2.p_v.midictl[i]
po.midi_ctrl(po, s, t)
}
for (s = s2.p_v.sym; s != s2; s = s.next) {
if (s.subtype == "midictl")
po.midi_ctrl(po, s, t)
}
po.p_v[s2.v] = true // synchronization done
} // set_ctrl()
// start and continue to play
function play_cont(po) {
var d, i, st, m, note, g, s2, t, maxt,
C = abc2svg.C,
s = po.s_cur
if (po.stop) {
if (po.onend)
po.onend(po.repv)
return
}
while (s.noplay) {
s = s.ts_next
if (!s || s == po.s_end) {
if (po.onend)
po.onend(po.repv)
return
}
}
t = po.stime + s.ptim / po.conf.speed // start time
// if speed change, shift the start time
if (po.conf.new_speed) {
d = po.get_time(po)
po.stime = d - (d - po.stime) *
po.conf.speed / po.conf.new_speed
po.conf.speed = po.conf.new_speed
po.conf.new_speed = 0
t = po.stime + s.ptim / po.conf.speed
}
maxt = t + po.tgen // max time = now + 'tgen' seconds
po.timouts = []
while (1) {
if (!po.p_v[s.v]) // if new voice
set_ctrl(po, s, t) // set the MIDI controls
switch (s.type) {
case C.BAR:
if (s.bar_type.slice(-1) == ':') // left repeat
po.repv = 1
if (s.rep_p) { // right repeat
po.repv++
if (!po.repn // if repeat a first time
&& (!s.rep_v // and no variant (anymore)
|| po.repv < s.rep_v.length)) {
po.stime += (s.ptim - s.rep_p.ptim) /
po.conf.speed
s = s.rep_p // left repeat
while (s.ts_next && !s.ts_next.seqst)
s = s.ts_next
t = po.stime + s.ptim / po.conf.speed
po.repn = true
break
}
po.repn = false
}
if (s.rep_s) { // first variant
s2 = s.rep_s[po.repv] // next variant
if (s2) {
po.stime += (s.ptim - s2.ptim) /
po.conf.speed
s = s2
t = po.stime + s.ptim / po.conf.speed
po.repn = false
} else { // end of tune
s = po.s_end
break
}
}
while (s.ts_next && !s.ts_next.seqst)
s = s.ts_next
if (!s.part)
break
// fall thru
case C.PART:
if (s.part // if end of part
&& po.i_p != undefined) {
s2 = s.part.p_s[++po.i_p] // next part
if (s2) {
po.stime += (s.ptim - s2.ptim) / po.conf.speed
s = s2
t = po.stime + s.ptim / po.conf.speed
} else {
s = po.s_end
}
}
break
case C.BLOCK:
if (s.subtype == "midictl")
po.midi_ctrl(po, s, t)
break
case C.GRACE:
for (g = s.extra; g; g = g.next) {
d = g.pdur / po.conf.speed
for (m = 0; m <= g.nhd; m++) {
note = g.notes[m]
po.note_run(po, g,
note.midi,
t + g.ptim - s.ptim,
//fixme: there may be a tie...
d)
}
}
break
case C.NOTE:
d = s.pdur / po.conf.speed
for (m = 0; m <= s.nhd; m++) {
note = s.notes[m]
if (note.ti2)
continue
po.note_run(po, s,
note.midi,
t,
note.tie_ty ?
do_tie(s, note.midi, d) : d)
}
// fall thru
case C.REST:
d = s.pdur / po.conf.speed
// follow the notes/rests while playing
if (po.onnote) {
i = s.istart
st = (t - po.get_time(po)) * 1000
po.timouts.push(setTimeout(po.onnote, st, i, true))
setTimeout(po.onnote, st + d * 1000, i, false)
}
break
}
while (1) {
if (s == po.s_end || !s.ts_next) {
if (po.onend)
setTimeout(po.onend,
(t - po.get_time(po) + d) * 1000,
po.repv)
po.s_cur = s
return
}
s = s.ts_next
if (!s.noplay)
break
}
t = po.stime + s.ptim / po.conf.speed // next time
if (t > maxt)
break
}
po.s_cur = s
// delay before next sound generation
po.timouts.push(setTimeout(play_cont,
(t - po.get_time(po)) * 1000
- 300, // wake before end of playing
po))
} // play_cont()
// search the index in the parts
function get_part(po) {
var s, i, s_p
for (s = po.s_cur; s; s = s.ts_prev) {
if (s.parts) {
po.i_p = 0
return
}
s_p = s.part
if (!s_p || !s_p.p_s)
continue
for (i = 0; i < s_p.p_s.length; i++) {
if (s_p.p_s[i] == s) {
po.i_p = i // index in the parts
return
}
}
}
} // get_part()
// --- play_next ---
get_part(po)
po.stime = po.get_time(po) + .3 // start time + 0.3s
- po.s_cur.ptim * po.conf.speed
po.p_v = [] // voice table for the MIDI controls
if (!po.repv)
po.repv = 1
play_cont(po) // start playing
} // play_next()
// nodejs
if (typeof module == 'object' && typeof exports == 'object')
exports.ToAudio = ToAudio