UNPKG

xml2abc

Version:

Unofficial package for unofficial fork of Wim Vree's excellent xml2abc-js (https://wim.vree.org/js/xml2abc-js_index.html).

1,002 lines (971 loc) 81.5 kB
//~ Copyright (C) 2014-2017: Willem Vree //~ This program is free software; you can redistribute it and/or modify it under the terms of the //~ Lesser GNU General Public License as published by the Free Software Foundation; //~ This program 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 Lesser GNU General Public License for more details. <http://www.gnu.org/licenses/lgpl.html>. xml2abc_VERSION = 68; (function () { // all definitions inside an anonymous function function repstr (n, s) { return new Array (n + 1).join (s); } // repeat string s n times function reparr (n, v) { var arr = []; while (n) { arr.push (v); --n; }; return arr; } // arr = n * [v] function dict (ks, vs) { // init object with key list and value list for (var i = 0, obj = {}; i < ks.length; ++i) obj[ ks [i]] = vs [i]; return obj; } function format (str, vals) { // help for sprintf string formatting var a = str.split (/%[ds]/); // only works for simple %d and %s if (a.length > vals.length) vals.push (''); return vals.map (function (x, i) { return a[i] + x; }).join (''); } function infof (str, vals) { abcOut.info (format (str, vals)); } function endswith (str, suffix) { return str.indexOf (suffix, str.length - suffix.length) !== -1; } function keyints (obj) { return Object.keys (obj).map (function (x) { return parseInt (x); }); } var max_int = Math.pow (2, 53); // Ecma largest positive mantissa. function sortitems (d, onkey) { // {key:value} -> [[key, value]] or [value] -> [[ix, value]] var xs = [], f, k; if (Array.isArray (d)) for (k = 0; k < d.length; ++k ) { if (k in d) xs.push ([k, d[k]]); } else for (k in d) xs.push ([k, d[k]]); if (onkey) f = function (a,b) { return a[0] - b[0]; }; else f = function (a,b) { return a[1] - b[1] || b[0] - a[0]; }; // tie (1) -> reverse sort on key (0) xs.sort (f); return xs; } var note_ornamentation_map = { // for notations, modified from EasyABC 'ornaments>trill-mark': 'T', 'ornaments>mordent': 'M', 'ornaments>inverted-mordent': 'P', 'ornaments>turn': '!turn!', 'ornaments>inverted-turn': '!invertedturn!', 'ornaments>tremolo': '!///!', 'technical>up-bow': 'u', 'technical>down-bow': 'v', 'technical>harmonic': '!open!', 'technical>open-string': '!open!', 'technical>stopped': '!plus!', 'articulations>accent': '!>!', 'articulations>strong-accent':'!>!', // compromise 'articulations>staccato': '.', 'articulations>staccatissimo':'!wedge!', 'fermata': '!fermata!', 'arpeggiate': '!arpeggio!', 'articulations>tenuto': '!tenuto!', 'articulations>spiccato': '!wedge!', // not sure whether this is the right translation 'articulations>breath-mark': '!breath!', // this may need to be tested to make sure it appears on the right side of the note 'articulations>detached-legato': '!tenuto!.' } var dynamics_map = { // for direction/direction-type/dynamics/ 'p': '!p!', 'pp': '!pp!', 'ppp': '!ppp!', 'f': '!f!', 'ff': '!ff!', 'fff': '!fff!', 'mp': '!mp!', 'mf': '!mf!', 'sfz': '!sfz!' } var abcOut; //------------------- // data abstractions //------------------- function Measure (p) { this.reset (); this.ixp = p; // part number this.ixm = 0; // measure number this.mdur = 0; // measure duration (nominal metre value in divisions) this.divs = 0; // number of divisions per 1/4 } Measure.prototype.reset = function () { // reset each measure this.attr = ''; // measure signatures, tempo this.lline = ''; // left barline, but only holds ':' at start of repeat, otherwise empty this.rline = '|'; // right barline this.lnum = ''; // (left) volta number } function Note (dur, n) { this.tijd = 0; // the time in XML division units this.dur = dur; // duration of a note in XML divisions this.fact = null; // time modification for tuplet notes (num, div) this.tup = ['']; // start(s) and/or stop(s) of tuplet this.tupabc = ''; // abc tuplet string to issue before note this.beam = 0; // 1 = beamed this.grace = 0; // 1 = grace note this.before = ''; // extra abc string that goes before the note/chord this.after = ''; // the same after the note/chord this.ns = n ? [n] : []; // notes in the chord this.lyrs = {}; // {number -> syllabe} this.pos = 0; // position in Music.voices for stable sorting } function Elem (string) { this.tijd = 0 // the time in XML division units this.str = string // any abc string that is not a note this.pos = 0; // position in Music.voices for stable sorting } function Counter () {} Counter.prototype.inc = function (key, voice) { this.counters [key][voice] = (this.counters [key][voice] || 0) + 1; } Counter.prototype.clear = function (vnums) { // reset all counters var ks = Object.keys (vnums); var vs = reparr (ks.length, 0); this.counters = {'note': dict (ks,vs), 'nopr': dict (ks,vs), 'nopt': dict (ks,vs)} } Counter.prototype.getv = function (key, voice) { return this.counters[key][voice]; } Counter.prototype.prcnt = function (ip) { // print summary of all non zero counters for (var iv in this.counters ['note']) { if (this.getv ('nopr', iv) != 0) infof ('part %d, voice %d has %d skipped non printable notes', [ip, iv, this.getv ('nopr', iv)]); if (this.getv ('nopt', iv) != 0) infof ('part %d, voice %d has %d notes without pitch', [ip, iv, this.getv ('nopt', iv)]); if (this.getv ('note', iv) == 0) // no real notes counted in this voice infof ('part %d, skipped empty voice %d', [ip, iv]); } } function Music (options) { this.tijd = 0; // the current time this.maxtime = 0; // maximum time in a measure this.gMaten = []; // [voices,.. for all measures in a part], voices = {vnum: [Note | Elem]} this.gLyrics = []; // [{num: (abc_lyric_string, melis)},.. for all measures in a part] this.vnums = {}; // all used xml voice id's in a part (xml voice id's == numbers) this.cnt = new Counter (); // global counter object this.vceCnt = 1; // the global voice count over all parts this.lastnote = null; // the last real note record inserted in this.voices this.bpl = options.b; // the max number of bars per line when writing abc this.cpl = options.n; // the number of chars per line when writing abc this.repbra = 0; // true if volta is used somewhere this.nvlt = options.v; // no volta on higher voice numbers } Music.prototype.initVoices = function (newPart) { this.vtimes = {}; this.voices = {}; this.lyrics = {}; for (var v in this.vnums) { this.vtimes [v] = 0; // {voice: the end time of the last item in each voice} this.voices [v] = []; // {voice: [Note|Elem, ..]} this.lyrics [v] = []; // {voice: [{num: syl}, ..]} } if (newPart) this.cnt.clear (this.vnums); // clear counters once per part } Music.prototype.incTime = function (dt) { this.tijd += dt; if (this.tijd > this.maxtime) this.maxtime = this.tijd; } Music.prototype.appendElemCv = function (voices, elem) { for (var v in voices) this.appendElem (v, elem); // insert element in all voices } Music.prototype.insertElem = function (v, elem) { // insert at the start of voice v in the current measure var obj = new Elem (elem); obj.tijd = 0; // because voice is sorted later this.voices [v].unshift (obj); } Music.prototype.appendObj = function (v, obj, dur) { obj.tijd = this.tijd; this.voices [v].push (obj); this.incTime (dur); if (this.tijd > this.vtimes[v]) this.vtimes[v] = this.tijd; // don't update for inserted earlier items } Music.prototype.appendElemT = function (v, elem, tijd) { // insert element at specified time var obj = new Elem (elem); obj.tijd = tijd; this.voices [v].push (obj); } Music.prototype.appendElem = function (v, elem, tel) { this.appendObj (v, new Elem (elem), 0); if (tel) this.cnt.inc ('note', v); // count number of certain elements in each voice } Music.prototype.appendNote = function (v, note, noot) { note.ns.push (noot); this.appendObj (v, note, parseInt (note.dur)); if (noot != 'z' && noot != 'x') { // real notes and grace notes this.lastnote = note; // remember last note for later modifications (chord, grace) this.cnt.inc ('note', v); // count number of real notes in each voice if (!note.grace) // for every real note this.lyrics[v].push (note.lyrs); // even when it has no lyrics } } Music.prototype.getLastRec = function (voice) { if (this.gMaten.length) { var m = this.gMaten [this.gMaten.length - 1][voice]; return m [m.length - 1]; // the last record in the last measure } return null; // no previous records in the first measure } Music.prototype.getLastMelis = function (voice, num) { // get melisma of last measure if (this.gLyrics.length) { var lyrdict = this.gLyrics [this.gLyrics.length - 1][voice]; // the previous lyrics dict in this voice if (num in lyrdict) return lyrdict[num][1]; // lyrdict = num -> (lyric string, melisma) } return 0; // no previous lyrics in voice or line number } Music.prototype.addChord = function (noot) { // careful: we assume that chord notes follow immediately this.lastnote.ns.push (noot); } Music.prototype.addBar = function (lbrk, m) { // linebreak, measure data if (m.mdur && this.maxtime > m.mdur) infof ('measure %d in part %d longer than metre', [m.ixm+1, m.ixp+1]); this.tijd = this.maxtime; // the time of the bar lines inserted here for (var v in this.vnums) { if (m.lline || m.lnum) { // if left barline or left volta number var p = this.getLastRec (v); // get the previous barline record if (p) { // p == null: in measure 1 no previous measure is available var x = p.str; // p.str is the ABC barline string if (m.lline) // append begin of repeat, m.lline == ':' x = (x + m.lline).replace (/:\|:/g,'::').replace (/\|\|/g,'|'); if (this.nvlt == 3) { // add volta number only to lowest voice in part 0 if (m.ixp + parseInt (v) == Math.min.apply (null, keyints (this.vnums))) x += m.lnum; } else if (this.nvlt == 4) { // add volta to lowest voice in each part if (parseInt (v) == Math.min.apply (null, keyints (this.vnums))) x += m.lnum; } else if (m.lnum) { // new behaviour with I:repbra 0 x += m.lnum; // add volta number(s) or text to all voices this.repbra = 1; // signal occurrence of a volta } p.str = x; // modify previous right barline } else if (m.lline) { // begin of new part and left repeat bar is required this.insertElem (v, '|:'); } } if (lbrk) { var p = this.getLastRec (v); // get the previous barline record if (p) p.str += lbrk; // insert linebreak char after the barlines+volta } if (m.attr) // insert signatures at front of buffer this.insertElem (v, '' + m.attr); this.appendElem (v, ' ' + m.rline); // insert current barline record at time maxtime this.voices[v] = sortMeasure (this.voices[v], m); // make all times consistent var lyrs = this.lyrics[v]; // [{number: sylabe}, .. for all notes] var lyrdict = {}; // {number: (abc_lyric_string, melis)} for this voice var nums = lyrs.reduce (function (ns, lx) { return ns.concat (keyints (lx))}, []); var maxNums = Math.max.apply (null, nums.concat ([0])); // the highest lyrics number in this measure for (var i = maxNums; i > 0; --i) { var xs = lyrs.map (function (syldict) { return syldict [i] || ''; }); // collect the syllabi with number i var melis = this.getLastMelis (v, i); // get melisma from last measure lyrdict [i] = abcLyr (xs, melis); } this.lyrics[v] = lyrdict; // {number: (abc_lyric_string, melis)} for this measure mkBroken (this.voices[v]); } this.gMaten.push (this.voices); this.gLyrics.push (this.lyrics); this.tijd = this.maxtime = 0; this.initVoices (); } Music.prototype.outVoices = function (divs, ip) { // output all voices of part ip var lyrlines, i, n, lyrs, vvmap, unitL, lvc, iv, im, measure, xs, lyrstr, melis, mis, t; vvmap = {}; // xml voice number -> abc voice number (one part) lvc = Math.min.apply (null, keyints (this.vnums)); // lowest xml voice number of this part for (iv in this.vnums) { if (this.cnt.getv ('note', iv) == 0) // no real notes counted in this voice continue; // skip empty voices if (abcOut.denL) unitL = abcOut.denL; // take the unit length from the -d option else unitL = compUnitLength (iv, this.gMaten, divs); // compute the best unit length for this voice abcOut.cmpL.push (unitL); // remember for header output var vn = [], vl = {}; // for voice iv: collect all notes to vn and all lyric lines to vl for (im = 0; im < this.gMaten.length; ++im) { measure = this.gMaten [im][iv]; vn.push (outVoice (measure, divs, im, ip, unitL)); checkMelismas (this.gLyrics, this.gMaten, im, iv); xs = this.gLyrics [im][iv]; for (n in xs) { t = xs [n]; lyrstr = t[0]; melis = t[1]; if (n in vl) { while (vl[n].length < im) vl[n].push (''); // fill in skipped measures vl[n].push (lyrstr); } else { vl[n] = reparr (im, '').concat ([lyrstr]); // must skip im measures } } } for (n in vl) { // fill up possibly empty lyric measures at the end lyrs = vl [n]; mis = vn.length - lyrs.length; vl[n] = lyrs.concat (reparr (mis, '')); } abcOut.add ('V:' + this.vceCnt); if (this.repbra) { if (this.nvlt == 1 && this.vceCnt > 1) abcOut.add ('I:repbra 0'); // only volta on first voice if (this.nvlt == 2 && parseInt (iv) > lvc) abcOut.add ('I:repbra 0'); // only volta on first voice of each part } if (this.cpl > 0) this.bpl = 0; // option -n (max chars per line) overrules -b (max bars per line) else if (this.bpl == 0) this.cpl = 100; // the default: 100 chars per line var bn = 0; // count bars while (vn.length) { // while still measures available var ib = 1; var chunk = vn [0]; while (ib < vn.length) { if (this.cpl > 0 && chunk.length + vn [ib].length >= this.cpl) break; // line full (number of chars) if (this.bpl > 0 && ib >= this.bpl) break; // line full (number of bars) chunk += vn [ib]; ib += 1; } bn += ib; abcOut.add (chunk + ' %' + bn); // line with barnumer vn.splice (0, ib); // chop ib bars lyrlines = sortitems (vl, 1); // order the numbered lyric lines for output (alphabitical on key) for (i = 0; i < lyrlines.length; ++ i) { t = lyrlines [i]; n = t[0]; lyrs = t[1]; abcOut.add ('w: ' + lyrs.slice (0, ib).join ('|') + '|'); lyrs.splice (0, ib); } } vvmap [iv] = this.vceCnt; // xml voice number -> abc voice number this.vceCnt += 1; // count voices over all parts } this.gMaten = []; // reset the follwing instance vars for each part this.gLyrics = []; this.cnt.prcnt (ip+1); // print summary of skipped items in this part return vvmap; } function ABCoutput (fnmext, pad, X, options) { this.fnmext = fnmext; this.outlist = []; // list of ABC strings this.infolist = []; // list of info messages this.title = 'T:Title'; this.key = 'none'; this.clefs = {}; // clefs for all abc-voices this.mtr = 'none' this.tempo = 0; // 0 -> no tempo field this.pad = pad; // the output path or null this.X = X + 1; // the abc tune number this.denL = options.d; // denominator of the unit length (L:) from -d option this.volpan = options.m; // 0 -> no %%MIDI, 1 -> only program, 2 -> all %%MIDI this.cmpL = []; // computed optimal unit length for all voices this.scale = ''; // float around 1.0 this.pagewidth = ''; // in cm this.leftmargin = ''; // in cm this.rightmargin = ''; // in cm if (options.p.length == 4) { this.scale = options.p [0] != '' ? parseFloat (options.p [0]) : ''; this.pagewidth = options.p [1] != '' ? parseFloat (options.p [1]) : ''; this.leftmargin = options.p [2] != '' ? parseFloat (options.p [2]) : ''; this.rightmargin = options.p [3] != '' ? parseFloat (options.p [3]) : ''; } } ABCoutput.prototype.add = function (str) { this.outlist.push (str + '\n'); // collect all ABC output } ABCoutput.prototype.info = function (str, warn) { var indent = (typeof warn == 'undefined' || warn) ? '-- ' : ''; this.infolist.push (indent + str); } ABCoutput.prototype.mkHeader = function (stfmap, partlist, midimap) { // stfmap = [parts], part = [staves], stave = [voices] var accVce = [], accStf = [], x, staves, clfnms, part, tag, partname, partabbrv, firstVoice, t, dmap; var nm, snm, clfnms, hd, tempo, d, defL, vnum, clef, ch, prg, vol, pan, i, abcNote, midiNote, step, notehead; var staffs = stfmap.slice (); // stafmap is consumed by prgroupelem for (i = 0; i < partlist.length; ++i) { // collect partnames into accVce and staff groups into accStf x = partlist [i]; try { prgroupelem (x, ['', ''], '', stfmap, accVce, accStf); } catch (err) { infof ('lousy musicxml: error in part-list',[]); } } staves = accStf.join (' '); clfnms = {}; for (i = 0; i < staffs.length; ++ i) { part = staffs [i]; t = accVce [i]; tag = t[0]; partname = t[1]; partabbrv = t[2]; if (part.length == 0) continue; // skip empty part firstVoice = part[0][0]; // the first voice number in this part nm = partname.replace (/\n/g,'\\n').replace (/\.:/g,'.').replace (/^:|:$/g,''); snm = partabbrv.replace (/\n/g,'\\n').replace (/\.:/g,'.').replace (/^:|:$/g,''); clfnms [firstVoice] = (nm ? 'nm="' + nm + '"' : '') + (snm ? ' snm="' + snm + '"' : ''); } hd = [format ('X:%d\n%s\n', [this.X, this.title])]; if (this.scale !== '') hd.push ('%%scale ' + this.scale + '\n'); if (this.pagewidth !== '') hd.push ('%%pagewidth ' + this.pagewidth + 'cm\n'); if (this.leftmargin !== '') hd.push ('%%leftmargin ' + this.leftmargin + 'cm\n'); if (this.rightmargin !== '') hd.push ('%%rightmargin ' + this.rightmargin + 'cm\n'); if (staves && accStf.length > 1) hd.push ('%%score ' + staves + '\n'); tempo = this.tempo ? 'Q:1/4=' + this.tempo + '\n' : ''; // default no tempo field d = []; // determine the most frequently occurring unit length over all voices for (i = 0; i < this.cmpL.length; ++i) { x = this.cmpL [i]; d[x] = (d[x] || 0) + 1; } d = sortitems (d); // -> [[unitLength, numberOfTimes]], sorted on numberOfTimes (when tie select smallest unitL) defL = d [d.length-1][0]; defL = this.denL ? this.denL : defL; // override default unit length with -d option hd.push (format ('L:1/%d\n%sM:%s\n', [defL, tempo, this.mtr])); hd.push (format ('I:linebreak $\nK:%s\n', [this.key])); for (vnum in this.clefs) { t = midimap [vnum-1]; ch = t[0]; prg = t[1]; vol = t[1]; pan = t[3]; dmap = t.slice (4); clef = this.clefs [vnum]; if (dmap.length && clef.indexOf ('perc') < 0 ) clef = (clef + ' map=perc').trim (); hd.push (format ('V:%d %s %s\n', [vnum, clef, clfnms [vnum] || ''])); if (this.volpan > 1) { // option -m 2 -> output all recognized midi commands when needed and present in xml if (ch > 0 && ch != vnum) hd.push ('%%MIDI channel ' + ch + '\n'); if (prg > 0) hd.push ('%%MIDI program ' + (prg - 1) + '\n'); if (vol >= 0) hd.push ('%%MIDI control 7 ' + vol + '\n'); // volume == 0 is possible ... if (pan >= 0) hd.push ('%%MIDI control 10 ' + pan + '\n'); } else if (this.volpan > 0) { // default -> only output midi program command when present in xml if (dmap.length && ch > 0) hd.push ('%%MIDI channel ' + ch + '\n'); // also channel if percussion part if (prg > 0) hd.push ('%%MIDI program ' + (prg - 1) + '\n'); } for (i = 0; i < dmap.length; ++i) { abcNote = dmap [i].nt; step = dmap [i].step; midiNote = dmap [i].midi; notehead = dmap [i].nhd; if (!notehead) notehead = 'normal'; if (abcMid (abcNote) != midiNote || abcNote != step) { if (this.volpan > 0) hd.push ('%%MIDI drummap '+abcNote+' '+midiNote+'\n'); hd.push ('I:percmap '+abcNote+' '+step+' '+midiNote+' '+notehead+'\n'); } } if (defL != this.cmpL [vnum-1]) // only if computed unit length different from header hd.push ('L:1/' + this.cmpL [vnum-1] + '\n'); } this.outlist = hd.concat (this.outlist); } //---------------- // functions //---------------- function abcLyr (xs, melis) { // Convert list xs to abc lyrics. if (!xs.join ('')) return ['', 0]; // there is no lyrics in this measure var res = []; for (var i = 0; i < xs.length; ++i) { var x = xs[i]; // xs has for every note a lyrics syllabe or an empty string if (x == '') { // note without lyrics if (melis) x = '_'; // set melisma else x = '*'; // skip note } else if (endswith (x,'_') && !endswith (x,'\\_')) { // start of new melisma x = x.replace ('_', ''); // remove and set melis boolean melis = 1; // so next skips will become melisma } else melis = 0; // melisma stops on first syllable res.push (x); } return ([res.join (' '), melis]); } function simplify (a, b) { // divide a and b by their greatest common divisor var x = a, y = b, c; while (b) { c = a % b; a = b; b = c; } return [x / a, y / a]; } function abcdur (nx, divs, uL) { // convert an musicXML duration d to abc units with L:1/uL if (nx.dur == 0) return ''; // when called for elements without duration var num, den, numfac, denfac, dabc, t; t = simplify (uL * nx.dur, divs * 4); // L=1/8 -> uL = 8 units num = t[0]; den = t[1]; if (nx.fact) { // apply tuplet time modification numfac = nx.fact [0]; denfac = nx.fact [1]; t = simplify (num * numfac, den * denfac); num = t[0]; den = t[1]; } if (den > 64) { // limit the denominator to a maximum of 64 var x = num / den, n = Math.floor (x); if (x - n < 0.1 * x) { num = n; den = 1; } t = simplify (Math.round (64 * num / den) || 1, 64); infof ('denominator too small: %d/%d rounded to %d/%d', [num, den, t[0], t[1]]); num = t[0]; den = t[1]; } if (num == 1) { if (den == 1) dabc = ''; else if (den == 2) dabc = '/'; else dabc = '/' + den; } else if (den == 1) dabc = '' + num; else dabc = num + '/' + den; return dabc; } function abcMid (note) { // abc note -> midi pitch var r = note.match (/([_^]*)([A-Ga-g])([',]*)/); if (!r) return -1; var acc = r[1], n = r[2], oct = r[3], nUp, p; nUp = n.toUpperCase (); p = 60 + [0,2,4,5,7,9,11]['CDEFGAB'.indexOf (nUp)] + (nUp != n ? 12 : 0); if (acc) p += (acc[0] == '^' ? 1 : -1) * acc.length; if (oct) p += (oct[0] == "'" ? 12 : -12) * oct.length; return p; } function staffStep (ptc, o, clef, tstep) { var n, ndif = 0; if (clef.indexOf ('stafflines=1') >= 0) ndif += 4; // meaning of one line: E (xml) -> B (abc) if (!tstep && clef.indexOf ('bass') >= 0) ndif += 12; // transpose bass -> treble (C3 -> A4) if (ndif) { // diatonic transposition == addition modulo 7 var nm7 = 'CDEFGAB'.split (''); n = nm7.indexOf (ptc) + ndif; ptc = nm7 [n % 7]; o += Math.floor (n / 7); } if (o > 4) ptc = ptc.toLowerCase (); if (o > 5) ptc = ptc + repstr (o-5, "'"); if (o < 4) ptc = ptc + repstr (4-o, ","); return ptc; } function setKey (fifths, mode) { var accs, kmaj, kmin, key, msralts; accs = ['F','C','G','D','A','E','B']; kmaj = ['Cb','Gb','Db','Ab','Eb','Bb','F','C','G','D','A', 'E', 'B', 'F#','C#']; kmin = ['Ab','Eb','Bb','F', 'C', 'G', 'D','A','E','B','F#','C#','G#','D#','A#']; key = ''; if (mode == 'major') key = kmaj [7 + fifths]; if (mode == 'minor') key = kmin [7 + fifths] + 'min'; if (fifths >= 0) msralts = dict (accs.slice (0, fifths), reparr (fifths, 1)); else msralts = dict (accs.slice (fifths), reparr (-fifths, -1)); return [key, msralts]; } function insTup (ix, notes, fact) { // read one nested tuplet var tupcnt = 0, halted = 0, lastix, tupfact, fn, fd, fnum, fden, tupcntR, halted, tupPrefix, t; var nx = notes [ix]; var i = nx.tup.indexOf ('start'); if (i > -1) // splice (i, 1) == remove 1 element at i nx.tup.splice (i, 1); // later do recursive calls when any starts remain var tix = ix; // index of first tuplet note fn = fact[0]; fd = fact[1]; // xml time-mod of the higher level fnum = nx.fact[0]; fden = nx.fact[1]; // xml time-mod of the current level tupfact = [fnum/fn, fden/fd]; // abc time mod of this level while (ix < notes.length) { nx = notes [ix]; if ((nx instanceof Elem) || nx.grace) { ix += 1; // skip all non tuplet elements continue; } if (nx.tup.indexOf ('start') > -1) { // more nested tuplets to start t = insTup (ix, notes, tupfact); ix = t[0]; tupcntR = t[1]; // ix is on the stop note! tupcnt += tupcntR } else if (nx.fact) { tupcnt += 1; // count tuplet elements } i = nx.tup.indexOf ('stop') if (i > -1) { nx.tup.splice (i, 1); halted = 1; break; } if (!nx.fact) { // stop on first non tuplet note ix = lastix; // back to last tuplet note halted = 1; break; } lastix = ix; ix += 1; } // put abc tuplet notation before the recursive ones var tup = [tupfact[0], tupfact[1], tupcnt]; if (tup.toString () == '3,2,3') tupPrefix = '(3'; else tupPrefix = format ('(%d:%d:%d', tup); notes [tix].tupabc = tupPrefix + notes [tix].tupabc; return [ix, tupcnt] // ix is on the last tuplet note } function mkBroken (vs) { // introduce broken rhythms (vs: one voice, one measure) vs = vs.filter (function (n) { return n instanceof Note; }); var i = 0; while (i < vs.length - 1) { var n1 = vs[i], n2 = vs[i+1] // scan all adjacent pairs if (!n1.fact && !n2.fact && n1.dur > 0 && n2.beam) { // skip if note in tuplet or has no duration or outside beam if (n1.dur * 3 == n2.dur) { n2.dur = (2 * n2.dur) / 3; n1.dur = n1.dur * 2; n1.after = '<' + n1.after; i += 1; // do not chain broken rhythms } else if (n2.dur * 3 == n1.dur) { n1.dur = (2 * n1.dur) / 3; n2.dur = n2.dur * 2; n1.after = '>' + n1.after; i += 1; // do not chain broken rhythms } } i += 1; } } function outVoice (measure, divs, im, ip, unitL) { // note/elem objects of one measure in one voice var ix = 0, tupcnt, t; while (ix < measure.length) { // set all (nested) tuplet annotations var nx = measure [ix]; if ((nx instanceof Note) && nx.fact) { t = insTup (ix, measure, [1, 1]); // read one tuplet, insert annotation(s) ix = t[0]; tupcnt = t[1]; } ix += 1; } var vs = [], nospace, s; for (var i = 0; i < measure.length; ++i) { var nx = measure [i]; if (nx instanceof Note) { var durstr = abcdur (nx, divs, unitL); // xml -> abc duration string var chord = nx.ns.length > 1; var cns = nx.ns.filter (function (n) { return endswith (n, '-') }); cns = cns.map (function (n) { return n.slice (0,-1) }); // chop tie var tie = ''; if (chord && cns.length == nx.ns.length) { // all chord notes tied nx.ns = cns // chord notes without tie tie = '-' // one tie for whole chord } s = nx.tupabc + nx.before; if (chord) s += '['; s += nx.ns.join (''); if (chord) s += ']' + tie; if (endswith (s, '-')) { s = s.slice (0,-1); // split off tie tie = '-'; } s += durstr + tie; // and put it back again s += nx.after; nospace = nx.beam; } else { s = nx.str; nospace = 1; } if (nospace) vs.push (s); else vs.push (' ' + s); } vs = vs.join (''); // ad hoc: remove multiple pedal directions while (vs.indexOf ('!ped!!ped!') >= 0) vs = vs.replace (/!ped!!ped!/g,'!ped!'); while (vs.indexOf ('!ped-up!!ped-up!') >= 0) vs = vs.replace (/!ped-up!!ped-up!/g,'!ped-up!'); while (vs.indexOf ('!8va(!!8va)!') >= 0) vs = vs.replace (/!8va\(!!8va\)!/g,''); // remove empty ottava's return vs; } function sortMeasure (voice, m) { voice.map (function (e, ix) { e.pos = ix; }); // prepare for stable sorting voice.sort (function (a, b) { return a.tijd - b.tijd || a.pos - b.pos; } ) // (stable) sort objects on time var time = 0; var v = []; for (var i = 0; i < voice.length; ++i) { // establish sequentiality var nx = voice [i]; if (nx.tijd > time) v.push (new Note (nx.tijd - time, 'x')); // fill hole if (nx instanceof Elem) { if (nx.tijd < time) nx.tijd = time; // shift elems without duration to where they fit v.push (nx); time = nx.tijd; continue; } if (nx.tijd < time) { // overlapping element if (nx.ns[0] == 'z') continue; // discard overlapping rest var o = v [v.length - 1]; // last object in voice if (o.tijd <= nx.tijd) { // we can do something if (o.ns[0] == 'z') { // shorten rest o.dur = nx.tijd - o.tijd; if (o.dur == 0) v.pop (); // nothing left, remove note infof ('overlap in part %d, measure %d: rest shortened', [m.ixp+1, m.ixm+1] ); } else { // make a chord of overlap o.ns = o.ns.concat (nx.ns); infof ('overlap in part %d, measure %d: added chord', [m.ixp+1, m.ixm+1] ); nx.dur = (nx.tijd + nx.dur) - time; // the remains if (nx.dur <= 0) continue; // nothing left nx.tijd = time; // append remains } } else { // give up var s = 'overlapping notes in one voice! part %d, measure %d, note %s discarded'; infof (s, [m.ixp+1, m.ixm+1, nx instanceof Note ? nx.ns : nx.str]); continue; } } v.push (nx); time = nx.tijd + nx.dur; } // when a measure contains no elements and no forwards -> no incTime -> this.maxtime = 0 -> right barline // is inserted at time == 0 (in addbar) and is only element in the voice when sortMeasure is called if (time == 0) infof ('empty measure in part %d, measure %d, it should contain at least a rest to advance the time!', [m.ixp+1, m.ixm+1] ); return v; } function getPartlist ($ps) { // correct part-list (from buggy xml-software) function mkstop (num) { // make proper xml-element for missing part-group var elemstr = '<part-group number="%d" type="%s"></part-group>'; var newelem = format (elemstr, [num, 'stop']); // xml string of (missing) part-group newelem = $.parseXML (newelem).firstChild; // part-group element is first child of (empty) xml document return $ (newelem); // return a jquery object } var xs, e, $x, num, type, i, cs, inum; xs = []; // the corrected part-list e = []; // stack of opened part-groups for (cs = $ps.children (), i = 0; i < cs.length; i++) { $x = $(cs [i]); // insert missing stops, delete double starts if ($x[0].nodeName == 'part-group') { num = $x.attr ('number'); type = $x.attr ('type'); inum = e.indexOf (num); if (type == 'start') { if (inum > -1) { // missing stop: insert one xs.push (mkstop (num)); xs.push ($x); } else { // normal start xs.push ($x) e.push (num) } } else { if (inum > -1) { // normal stop e.splice (inum, 1); // remove stop xs.push ($x) } else {} // double stop: skip it } } else xs.push ($x); } for (i = e.length - 1; i >= 0; --i) { // fill missing stops at the end num = e[i]; xs.push (mkstop (num)); } return xs; } function parseParts (xs, d, e) { // [] {} [] -> [[elems on current level], rest of xs] var $x, num, type, s, n, elemsnext, rest1, elems, rest2, nums, sym, rest, name, t; if (xs.length == 0) return [[],[]]; $x = xs.shift (); if ($x[0].nodeName == 'part-group') { num = $x.attr ('number'); type = $x.attr ('type'); if (type == 'start') { // go one level deeper s = []; // get group data for (n in {'group-symbol':0,'group-barline':0,'group-name':0,'group-abbreviation':0}) s.push ($x.find (n).text () || ''); d [num] = s; // remember groupdata by group number e.push (num); // make stack of open group numbers t = parseParts (xs, d, e); // parse one level deeper to next stop elemsnext = t[0]; rest1 = t[1]; t = parseParts (rest1, d, e); // parse the rest on this level elems = t[0]; rest2 = t[1]; return [[elemsnext].concat (elems), rest2]; } else { // stop: close level and return group-data nums = e.pop (); // last open group number in stack order if (xs.length && xs[0].attr ('type') == 'stop') // two consequetive stops if (num != nums) { // in the wrong order (tempory solution) t = d[nums]; d[nums] = d[num]; d[num] = t; // exchange values (only works for two stops!!!) } sym = d[num]; // retrieve and return groupdata as last element of the group return [[sym], xs]; } } else { t = parseParts (xs, d, e); // parse remaining elements on current level elems = t[0]; rest = t[1]; name = ['name_tuple', $x.find ('part-name').text () || '', $x.find ('part-abbreviation').text () || '']; return [[name].concat (elems), rest]; } } function bracePart (part) { // put a brace on multistaff part and group voices var brace, ivs, i, j; if (part.length == 0) return []; // empty part in the score brace = []; for (i = 0; i < part.length; ++i) { ivs = part [i]; if (ivs.length == 1) // stave with one voice brace.push ('' + ivs[0]); else { // stave with multiple voices brace.push ('('); for (j = 0; j < ivs.length; ++j) brace.push ('' + ivs [j]); brace.push (')'); } brace.push('|'); } brace.splice (-1, 1); // no barline at the end if (part.length > 1) brace = ['{'].concat (brace).concat (['}']); return brace; } function prgroupelem (x, gnm, bar, pmap, accVce, accStf) { // collect partnames (accVce) and %%score map (accStf) var y, nms, i, n1, n2, xx; if (x[0] == 'name_tuple') { // partname-tuple = ['name_tuple', part-name, part-abbrev] y = pmap.shift (); if (gnm[0]) { // put group-name before part-name x[1] = gnm[0] + ':' + x[1]; // gnm == [group-name, group-abbrev] x[2] = gnm[1] + ':' + x[2]; } accVce.push (x); accStf.push.apply (accStf, bracePart (y)); } else if (x.length == 2) { // misuse of group just to add extra name to stave y = pmap.shift (); nms = ['name_tuple','','']; nms[1] = x[0][1] + ':' + x[1][2]; // x[0] = ['name_tuple', part-name, part-abbrev] nms[2] = x[0][2] + ':' + x[1][3]; // x[1] = [bracket symbol, continue barline, group-name, group-abbrev] accVce.push (nms) accStf.push.apply (accStf, bracePart (y)); } else { prgrouplist (x, bar, pmap, accVce, accStf); } } function prgrouplist (x, pbar, pmap, accVce, accStf) { // collect partnames, scoremap for a part-group var sym, bar, gnm, gabbr, y, z, i, t; t = x [x.length-1]; // bracket symbol, continue barline, group-name-tuple sym = t[0]; bar = t[1]; gnm = t[2]; gabbr = t[3]; bar = bar == 'yes' || pbar; // pbar -> the parent has bar accStf.push (sym == 'brace' ? '{' : '[') for (i = 0; i < x.length - 1; ++i) { prgroupelem (x[i], [gnm, gabbr], bar, pmap, accVce, accStf) if (bar) accStf.push ('|') } if (bar) accStf.splice (-1, 1); // remove last one before close accStf.push (sym == 'brace' ? '}' : ']'); } function compUnitLength (iv, maten, divs) { // compute optimal unit length var uLmin = 0, minLen = max_int, i, j; var xs = [4,8,16]; // try 1/4, 1/8 and 1/16 while (xs.length) { var uL = xs.shift (); var vLen = 0; // total length of abc duration strings in this voice for (i = 0; i < maten.length; ++i) { // all measures var m = maten [i][iv]; // voice iv for (j = 0; j < m.length; ++j) { var e = m[j]; // all notes in voice iv if ((e instanceof Elem) || e.dur == 0) continue; // no real durations vLen += abcdur (e, divs, uL).length; // add len of duration string } } if (vLen < minLen) { uLmin = uL; minLen = vLen; } // remember the smallest } return uLmin; } function doSyllable ($lyr) { var txt = ''; // collect all text and elision elements var $xs = $lyr.children (); for (var i = 0; i < $xs.length; ++i) { var e = $xs [i]; switch (e.nodeName) { case 'elision': txt += '~'; break; case 'text': // escape _, - and space txt += $(e).text ().replace (/_/g,'\\_').replace (/-/g, '\\-').replace (/ /g, '~'); break; } } if (!txt) return txt; var s = $lyr.find ('syllabic').text (); if (s == 'begin' || s == 'middle') txt += '-'; if ($lyr.find ('extend').length) txt += '_'; return txt; } function checkMelismas (lyrics, maten, im, iv) { if (im == 0) return; var maat = maten [im][iv]; // notes of the current measure var curlyr = lyrics [im][iv]; // lyrics dict of current measure var prvlyr = lyrics [im-1][iv]; // lyrics dict of previous measure var n, lyrstr, melis, ms; for (n in prvlyr) { // all lyric numbers in the previous measure var t = prvlyr [n]; lyrstr = t[0]; melis = t[1]; if (!(n in curlyr) && melis) { // melisma required, but no lyrics present -> make one! ms = getMelisma (maat); // get a melisma for the current measure if (ms) curlyr [n] = [ms, 0]; // set melisma as the n-th lyrics of the current measure } } } function getMelisma (maat) { // get melisma from notes in maat var ms = []; for (var i = 0; i < maat.length; ++i) { // every note should get an underscore var note = maat [i]; if (!(note instanceof Note)) continue; // skip Elem's if (note.grace) continue; // skip grace notes if (note.ns [0] == 'z' || note.ns [0] == 'x') break; // stop on first rest ms.push ('_'); } return ms.join (' '); } //---------------- // parser //---------------- function Parser (options) { this.slurBuf = {}; // dict of open slurs keyed by slur number this.dirStk = {}; // {direction-type + number -> (type, voice | time)} dict for proper closing this.ingrace = 0; // marks a sequence of grace notes this.msc = new Music (options); // global music data abstraction this.unfold = options.u; // turn unfolding repeats on this.ctf = options.c; // credit text filter level this.gStfMap = []; // [[abc voice numbers] for all parts] this.midiMap = []; // midi-settings for each abc voice, in order this.drumInst = {}; // inst_id -> midi pitch for channel 10 notes this.drumNotes = {}; // 'xml voice ; abc note' -> (midi note, note head) this.instMid = []; // [{inst id -> midi-settings} for all parts] this.midDflt = [-1,-1,-1,-91]; // default midi settings for channel, program, volume, panning this.msralts = {}; // xml-notenames (without octave) with accidentals from the key this.curalts = {}; // abc-notenames (with voice number) with passing accidentals this.stfMap = {}; // xml staff number -> [xml voice number] this.clefMap = {}; // xml staff number -> abc clef (for header only) this.curClef = {}; // xml staff number -> current abc clef this.clefOct = {}; // xml staff number -> current clef-octave-change this.curStf = {}; // xml voice number -> current xml staff number this.nolbrk = options.x; // generate no linebreaks ($) this.doPageFmt = options.p.length == 1; // translate xml page format this.tstep = options.t; // clef determines step on staff (percussion) this.dirtov1 = options.v1; // all directions to first voice of staff this.ped = !options.noped; // render pedal directions this.pedVce = null; // voice for pedal directions } Parser.prototype.matchSlur = function (type2, n, v2, note2, grace, stopgrace) { // match slur number n in voice v2, add abc code to before/after if (['start', 'stop'].indexOf (type2) == -1) return; // slur type continue has no abc equivalent if (!n) n = '1'; // default slur number if (n in this.slurBuf) { var t = this.slurBuf [n]; var type1 = t[0], v1 = t[1], note1 = t[2], grace1 = t[3]; if (type2 != type1) { // slur complete, now check the voice if (v2 == v1) { // begins and ends in the same voice: keep it if (type1 == 'start' && (!grace1 || !stopgrace)) { // normal slur: start before stop and no grace slur note1.before = '(' + note1.before; // keep left-right order! note2.after += ')'; } } // no else: don't bother with reversed stave spanning slurs delete this.slurBuf [n]; // slur finished, remove from stack } else { // double definition, keep the last infof ('double slur numbers %s-%s in part %d, measure %d, voice %d note %s, first discarded', [type2, n, this.msr.ixp+1, this.msr.ixm+1, v2, note2.ns]); this.slurBuf [n] = [type2, v2, note2, grace]; } } else { // unmatched slur, put in dict this.slurBuf [n] = [type2, v2, note2, grace]; } } Parser.prototype.doNotations = function (note, $nttn) { var ks = Object.keys (note_ornamentation_map).sort (); for (var i = 0; i < ks.length; ++i) { var key = ks[i]; var val = note_ornamentation_map [key]; if ($nttn.find (key).length) note.before += val; // just concat all ornaments } var $fingering = $nttn.find ('technical>fingering'); $fingering.each (function () { // handle multiple finger annotations note.before += '!' + $(this).text () + '!'; // validate text? }); var $wvln = $nttn.find ('ornaments>wavy-line'); if ($wvln.length) { switch ($wvln.attr ('type')) { case 'start': note.before = '!trill(!' + note.before; break; // keep left-right order! case 'stop': note.after += '!trill)!'; break } } } Parser.prototype.ntAbc = function (ptc, o, $note, v) { // pitch, octave -> abc notation var acc2alt = {'double-flat':-2,'flat-flat':-2,'flat':-1,'natural':0,'sharp':1,'sharp-sharp':2,'double-sharp':2}; o += this.clefOct [this.curStf [v]] || 0; // current clef-octave-change value var p = ptc; if (o > 4) p = ptc.toLowerCase (); if (o > 5) p = p + repstr (o-5, "'"); if (o < 4) p = p + repstr (4-o, ","); var acc = $note.find ('accidental').text (); // should be the notated accidental var alt = $note.find ('pitch>alter').text (); // pitch alteration (midi) if (!alt && this.msralts [ptc]) alt = 0; // no alt but key implies alt -> natural!! var p_v = p + '#' + v; // key == pitch, voice if (!alt && p_v in this.curalts) alt = 0; // no alt but previous note had one -> natural!! if (acc === '' && alt === '') { return p; // no acc, no alt } else if (acc != '') { alt = acc2alt [acc]; } else { // now see if we really must add an accidental alt = parseInt (alt); if (p_v in this.curalts) { // the note in this voice has been altered before if (alt == this.curalts [p_v]) return p; // alteration still the same } else if (alt == (this.msralts [ptc] || 0)) return p; // alteration implied by the key var xs = $note.find ('tie').add ($note.find ('notations>tied')).get (); // in xml we have separate notated ties and playback ties if (xs.some (function (x) { return x.getAttribute ('type') == 'stop'; })) return p; // don't alter tied notes infof ('accidental %d added in part %d, measure %d, voice %d note %s', [alt, this.msr.ixp+1, this.msr.ixm+1, v+1, p] ); } this.curalts [p_v] = alt; p = ['__','_','=','^','^^'][alt+2] + p; // and finally ... prepend the accidental return p; } Parser.prototype.doNote = function ($n) { var note = new Note (0, null); var v = parseInt ($n.find ('voice').text () || '1'); if (this.isSib) v += 100 * ($n.find ('staff').text () || '1') // repair bug in Sibelius var chord = $n.find ('chord').length > 0; var p = $n.find ('pitch>step').text () || $n.find ('unpitched>display-step').text (); var o = $n.find ('pitch>octave').text () || $n.find ('unpitched>display-octave').text (); var r = $n.find ('rest').length > 0; var numer = $n.find ('time-modification>actual-notes').text(); if (numer) { var denom = $n.find ('time-modification>normal-notes').text(); note.fact = [parseInt (numer), parseInt (denom)]; } note.tup = $n.find ('notations>tuplet').map (function () { return $(this).attr ('type'); }).get(); var dur = $n.find ('duration').text (); var $grc = $n.find ('grace'); note.grace = $grc.length > 0; note.before = '', note.after = '' // strings with ABC stuff that goes before or after a note/chord if (note.grace && !this.ingrace) { // open a grace sequence this.ingrace = 1; note.before = '{'; if ($grc.attr ('slash') == 'yes') note.before += '/'; // acciaccatura } var stopgrace = !note.grace && this.ingrace; if (stopgrace) { // close the grace sequence this.ingrace = 0; th