# script to convert SymbTr mu2 files to ABC # using QuickJS (https://bellard.org/quickjs/) # # Copyright (C) 2025 Jean-François Moine # License GPL3+ # # Turkish Makams database: https://github.com/MTG/SymbTr/ # # Usage: # mu22abc SymbTr.mu2 > abc_file.abc exec qjs --std -e ' "use strict" function printErr(p) { std.err.printf("%s\n", p) } function main(args) { var f, b, l, v, n, d, tp, i, ly, bmp, nt, md, mn, lyskip, snt, snv, cp, // current part numbers snt_a = [], // indexes of small / normal notes in "lo" margin = "\n%%leftmargin .5cm\n\ %%rightmargin .5cm", part = "", // P: in tune header pn = "?ABCDEFG", // P: names lo = "", // ABC music line ly1 = "", // 1st line of lyrics ly2 = "", w1 = "", // two verses in W: w2 = "", ks = [], // key signature am = {}, // accidentals of the measure ct = 0, // current time in the measure bm = [], // beams stt = "" // subtitle // change the scale of the notes // - this is done using a voice overlay | normal notes & small notes | // - when no bar in the line, assume the previous line ends with a bar // @snt_a = array of indexes in the output string // of the small note sequences (0: start, 1: stop and 2: 1st lyric word) // in a measure function do_snt() { if (!snt_a[0][1]) { // if no end of small notes snt_a[0][1] = lo.length snt = 0 } // get the start of measure var sn1, ss1, sn2, ss2, sn3, ss3, loo = lo, // original output string e = snt_a.pop(), // next sequence i = loo.lastIndexOf("|", e[0]), // measure start ls = "" // voice overlay // loop on the sequences if (!++i) lo = "" // bar in the previous line else lo = loo.slice(0, i) do { sn1 = loo.slice(i, e[0]) ss1 = sn1.replace(/[a-gA-Gz]/g,"x") .replace(/S|"[^"]+"|![^!]+!|{[^}]+}|[_=^][1-9]?/g, "") .replace(/ ?x\/\/ ?x\/\/([^/])/g,"x/$1") .replace(/ ?x\/ ?x\/([^/])/g,"x$1") ss2 = loo.slice(e[0], e[1]) // if (e[2]) // ss2 = "\"@0,-40" + e[2] + "\"y0" + ss2 //// ss2 = "\"@0,-34" + e[2] + "\"" + ss2 //// ss2 = "\"_" + e[2] + "\"" + ss2 sn2 = ss2.replace(/[a-gA-Gz]/g,"x") .replace(/S|"[^"]+"|![^!]+!|{[^}]+}|[_=^][1-9]?/g, "") .replace(/ ?x\/\/ ?x\/\/([^/])/g,"x/$1") .replace(/ ?x\/ ?x\/([^/])/g,"x$1") lo += sn1 + sn2 if (e[2]) ss2 = "\"@0,-40" + e[2] + "\"y0" + ss2 ls += ss1 + ss2 i = e[1] e = snt_a.pop() // next sequence } while (e) sn1 = loo.slice(i) ss1 = sn1.replace(/[a-gA-Gz]/g,"x") .replace(/S|"[^"]+"|![^!]+!|{[^}]+}|[_=^][1-9]?/g, "") .replace(/ ?x\/\/ ?x\/\/([^/])/g,"x/$1") .replace(/ ?x\/ ?x\/([^/])/g,"x$1") lo += sn1 + "&\n" if (!snv) { // if new voice overlay snv = 1 lo += "%%voicescale .8\n" } lo += ls + ss1 //fixme:old sequence // var p = lo.slice(snt.length), // little notes in the main voice // sb = p.replace(/[a-gA-G]/g,"x") // .replace(/[_=^][1-9]?/g, "") // .replace(/\|$/, "") // // lo = snt + "(&" // if (snl) // lo += "\"@0,-34" + snl + "\"y0" // lo += sb + "&" // snt = snl = 0 // if (!snv) { // if new voice overlay // snv = 1 //true //// lo += "[K:scale=.8]" // lo += "[I:voicescale .8]" // } // if (p.slice(-1) == "|") // lo += p.slice(0, -1) + "&)|" // else // lo += p + "&)" } // do_snt() // build the parts (P: in the tune header) function p_build() { var cp1, cp2, cp3, i, l, n, f, t = "", // time a = [] // array of flags // variables: // part = P: in tune header // cp = current part number (1,2...: A,B...) // cp1 = part number starting by [ // cp2 = part number starting by { // cp3 = part number starting by < (not used) // first pass, build an array of the flags about the sequences // the flags are in in l[7] and l[8] - and "1" in l[4] // <{[ common part ]}> // [...] and {..} parts (ending with fine or dalcoda) // $ go back to [ // + go back to { // 1+ go back to the beginning (?) // X go back to < // 1X go back to the beginning (?) for (n = 0; n < b.length - 1; n++) { l = b[n].split("\t") if (f && l[9] != t) { a.push(f) f = "" } switch (l[7]) { case "<": case "{": case "[": if (f == undefined) { if (l[9]) a.push("") f = "" } if (f) { l[5] = f // no new P: b[n] = l.join("\t") } f += l[7] break default: switch (l[8]) { default: continue case "]": case "}": case ">": case "$": case "+": case "X": if (f) { l[5] = f // no new P: b[n] = l.join("\t") } f += l[8] break } break } t = l[9] } if (f) a.push(f) else if (a.length && t != l[9]) a.push("") // second pass, build the P: of the tune header part = "" cp1 = cp2 = cp3 = cp = 0 while (1) { f = a.shift() if (!f) { if (f == undefined) break if (!part) part = "\nP:A." cp++ continue } if (f[0] == "]" || f[0] == "}" || f[0] == ">") { if (f[1] != "$" && f[1] != "+" && f[1] != "X") cp++ f = f.slice(1) } while (f[0] == "$" || f[0] == "+" || f[0] == "X") { part += "." + pn[f[0] == "$" ? cp1 : f[0] == "+" ? cp2 : cp3] f = f.slice(1) if (f[0] != "[" && f[0] != "{") cp++ else l[7] = f[0] } if (f[0] == "<" || f[0] == "{" || f[0] == "[") { if (!part) part = "\nP:" cp++ for (i = 0; i < f.length; i++) { switch (f[i]) { case "[": cp1 = cp; break case "{": cp2 = cp; break case "<": cp3 = cp; break } } } if (a.length) part += pn[cp] } if (part == "\nP:A.A" // change to S .. S || part == "\nP:AB.A") // change to S .. !fine! .. S part = 0 } // p_build() // reset the accidentals at start of a measure // @ks key signature (array) // @am accidentals in the measure (object, key=note pitch) function acc_reset() { var i, k, a for (k in am) delete am[k] for (i = 0; i < ks.length; i++) { k = ks[i].match(/([_^]\d)(.)/) k[2] = k[2].toUpperCase() am[k[2] + ",,"] = k[1] am[k[2] + ","] = k[1] am[k[2]] = k[1] k[2] = k[2].toLowerCase() am[k[2]] = k[1] am[k[2] + "\u0027"] = k[1] am[k[2] + "\u0027\u0027"] = k[1] } } // acc_reset() // check if any lyric in the tune function check_ly(b) { var i, v for (i = 0; i < b.length; i++) { if (b[i][0] != "9" || b[i][2] == "\t") // if no note continue v = b[i].split("\t") if (v[7] && v[8][0] != "^") return 1 //true } } // check_ly() function get_note(v) { var n, a = "" v = v.match(/([^\d]+)(\d)([b#]?)(\d?)/) // [ note, octave, accidental, comma ] switch (v[1].toLowerCase()) { case "do": n = "C"; break case "re": n = "D"; break case "mi": n = "E"; break case "fa": n = "F"; break case "sol": n = "G"; break case "la": n = "A"; break case "si": n = "B"; break default: n = v[1]; break } if (v[2] > "4") { n = n.toLowerCase() if (v[2] > "5") n += "\u0027\u0027\u0027".slice(0, Number(v[2]) - 5) } else { n = n.toUpperCase() if (v[2] < "3") ",,,".slice(0, 3 - Number(v[2])) } switch (v[3]) { case "#": case "b": a = (v[3] == "#" ? "^" : "_") + v[4] break } return a + n } // get_note() // get lyric words function get_lyrics(wl1, wl2) { if (l[0] == "1") // if a small note return if (wl2 && wl2[0] == "^") // if an annotation return if (wl1 == "." // if start instrument (SAZ, ARANA..) && lyskip && --lyskip > 0) ly1 += "*" else ly1 += wl1 ? ly_clean(wl1) : "*" if (wl1 && wl1 != ".") { if (wl1.slice(0, 8) != "ARANAĞME" && wl1.slice(0, 3) != "SAZ") { if (w1 && w1.slice(-2) != " " && wl1[0].toUpperCase() == wl1[0]) w1 += " " if (wl1 != "|") w1 += wl1 } else { lyskip = wl1[0] == "A" ? 6 : 2 } } ly2 += wl2 ? ly_clean(wl2) : "*" if (wl2) { if (w2 && w2.slice(-2) != " " && wl2[0].toUpperCase() == wl2[0]) w2 += " " if (wl2 != "|") w2 += wl2 } } // get_lyrics() // clean a lyric word function ly_clean(p) { return p.replace(/ +$/, "") .replace("_", "\\_") .replace("|", "\\|") .replace(/ /g, "~") + " " } // ly_clean() // start a new line function nl() { if (lo) { print(lo) lo = "" } if (ly) { // if some lyrics if (ly1.match(/[^*]/)) print("w:" + ly1.replace(/\*+$/, "")) else print("w:") if (ly2.match(/[^*]/)) print("w:" + ly2.replace(/\*+$/, "")) else print("w:") ly1 = ly2 = "" } } // nl() // output a part P:n function outp(n) { var p = lo.slice(-2) if (p[1] == "|") { // double bar switch (p[0]) { default: lo += "|" // fall thru case ":": case "|": break } } lo += lo ? "[P:" + pn[n] + "]" : "P:" + pn[n] + "\n" } // outp() // main code try { f = std.open(args[0], "r") } catch(e) { print("Cannot read " + args[0]) std.exit(1) } b = f.readAsString() // some mu2 files have lines that start with a space // and also have a space before or after the TABs if (b.indexOf("\n 9") > 0) { // shit! b = b.replace(/\t /g, "\t") .replace(/ \t/g, "\t") .replace(/^ /, "") } b = b.split("\n") f.close() print("X:1") b.shift() // skip the mu2 header // get the header hl: while (1) { l = b.shift() if (!l) break v = l.split("\t") switch (+v[0]) { default: if (+v[0] <= 14) { b.unshift(l) // first note break hl } break case 50: // makam change n = v[8] // key signature if (n) { n = n.split("/") ks.length = 0 for (i = 0; i < n.length; i++) ks.push(get_note(n[i])) acc_reset() } stt = "\nT:" + v[7] // subtitle 1st part break case 51: // usul (rhythm) change case 56: // measure change if (!+v[2]) { // 0/0 or 0/1 mn = 1000000 md = 1 lo += "\nM:none\nL:1/8" } else { mn = +v[2] md = +v[3] lo += "\nM:" + v[2] + "/" + v[3] + "\nL:1/8" } if (v[7]) lo += "\nR:Usul:" + v[7] break case 52: // tempo change lo += "\nQ:" + v[2] + "/" + v[3] + "=" + v[4] break // case 53: // phrase boundary // break // case 54: // modulation start // break // case 55: // modulation end // break // case 56: // measure change - see above // mn = +v[2] // md = +v[3] // lo += "\nM:" + v[2] + "/" + v[3] // break case 57: // form stt += " " + v[7] // subtitle 2nd part break case 58: // composer lo += "\nC:Beste:" + v[7] break case 59: // lyricist if (v[7]) lo += "\nC:Güfte:" + v[7] break case 60: // starting lyrics stt = "T:" + v[7] + stt // (main title) break // case 61: // transposition // break // case 62: // folk music if "E" // break case 63: // notation (THM/TSM) nt = v[7] == "THM" if (!nt) { // if classical (TSM) for (i = 0; i < ks.length; i++) { v = ks[i].match(/[_^]\d/) if (v && v[1] == "2") ks[i] = ks[i].slice(0, -1) + "1" } } break // case 64: // ?? // break // case 65: // composition date // break // case 66: // duration of the composition // break case 71: // paper size lo += "\n%%pagewidth " + (v[2] * 10) + "\n%%pageheight " + (v[3] * 10) break case 72: // margins margin = "\n%%leftmargin " + (v[5] * 10) + "\n%%rightmargin " + (v[6] * 10) break // case 73: // Vert./Horiz. layout // break // case 89: // gracenote to note // break // case 108: // from postmultiply to note // break } } // build the parts (P: in the tune header) p_build() // the music starts here lo += "\nZ:generated by mu22abc from " + args[0].match(/[^/\\]*\.mu2/) print(stt + lo + part + margin + "\n%%titleformat TT,C>C\n\ %%MIDI temperamentequal 53\n\ %%soloffs part=8 tempo=50\n\ %%graceslurs 0\n\ %%maxshrink .9\n\ K:C " + (n ? ks.join("") : "")) if (part) print("P:A") cp = 1 // get the music ly = check_ly(b) lo = "" while (1) { l = b.shift() if (!l) break l = l.split("\t") if (snt && l[0] != "1") { // if end of small notes snt_a[0][1] = lo.length snt = 0 } // get the repeat indications switch (l[7]) { case "<": // sequence <...> - see "$" case "{": // sequence {...} - see "+" case "[": // sequence [...] - see "X" if (l[7] == "<" // < may be start of <{[ && b[0].split("\t")[7] == "{" && b[1].split("\t")[7] == "[") { b.shift() b.shift() // keep [ } if (!part) { lo += "S" // segno start common part break } if (l[9] // if time == 0 && !l[5]) // and not [$+X] outp(++cp) continue case "(": // left repeat if (lo.slice(-1) == "|") { if (lo.slice(-2) == ":|") lo = lo.slice(0, -1) lo += ":" } else { lo += "|:" } continue case "(1": case "(2": n = 0 if (lo.slice(-1) == "]") { // if M: + R: i = lo.length do { i = lo.lastIndexOf("[", --i) } while (lo[i - 1] == "]") n = lo.slice(i) lo = lo.slice(0, i) } if (ct != mn / md // if in the middle of a measure && lo.slice(-1) != "|") lo += "[" if (!lo) // (if start of line) lo = "[" lo += l[7][1] if (n) lo += n continue } switch (l[8]) { case "]": // end of [...] case "}": // end of {...} case ">": // end of <...> if (l[8] == "]" // ] may be end of ]}> && b[0].split("\t")[8] == "}" && b[1].split("\t")[8] == ">") { b.shift() b.shift() } if (!part) { if (l[8] == "]" && b[0].split("\t")[8] != "$") { if (snt_a.length) do_snt() lo += "!fine!.|" } continue } // fall thru case "X": // back to [...] case "+": // back to {...} case "$": // back to <...> or end bar if (part) { if (b.length > 1 && !l[5]) // and not [$+X] outp(++cp) continue } if (lo.slice(-1) == "|") { if (lo.slice(-2) == ":|") lo = lo.slice(0, -2) + "S:|" else lo = lo.slice(0, -1) + "S|" if (b.length == 1) // end tune lo += "]" } else { lo += l[8] != "S" ? "S" : "]" } continue case ":": // right repeat if (lo.slice(-1) == "|") lo = lo.slice(0, -1) lo += ":|" continue // no action on ")", "1)", "2)" } // start a new ABC line if (l[0] == "21") { // new music line n = 0 i = lo.length if (lo.slice(-1) == "]") { // if [P: ... ] do { i = lo.lastIndexOf("[", --i) } while (lo[i - 1] == "]") n = lo.slice(i) lo = lo.slice(0, i) } nl() if (n) lo += n continue } if ((lo.slice(-1) == "|" && lo.slice(-2) != ".|") || lo.slice(-1) == ":") { // cut line on a measure bar if (lo.length > 72 && (!b.length || b[0][2] != "\t")) // and no more bar nl() } if (+l[0] > 50) { switch (l[0]) { case "51": // meter change case "56": // meter change if (!+l[2]) { // 0/0 or 0/1 mn = 1000000 md = 1 lo += "[M:none]\"^recitativo\"y0" } else { mn = +l[2] md = +l[3] lo += "[M:" + l[2] + "/" + l[3] + "]" } ct = 0 if (l[7]) // new rhythm lo += "[R:Usul:" + l[7] + "]" break case "52": // tempo change lo += "[Q:" + l[2] + "/" + l[3] + "=" + l[4] + "]" break } continue } if (l[0] == "14") { bm.length = 0 v = l[7] // beams for (n = 0; n < v.length - 2; n++) bm[n] = +v[n] / md + (n ? bm[n - 1] : 0) bmp = bm.slice() // (duplicate) continue } if (!l[2] // not a note nor a rest && l[0] != "8") // nor a grace note continue // get the note/rest if (!l[1]) { //fixme: also l[0] == "0" ? n = "z" } else { n = get_note(l[1]) v = n.match(/([_^]\d)?(.+)/) if (v[1]) { // if note with accidental if (am[v[2]] == v[1]) n = v[2] else am[v[2]] = v[1] } else if (am[v[2]]) { // if accidental in the key sig n = "=" + n am[v[2]] = "" } if (ly // if some lyrics && l[0] != "8") // (not on a grace note) get_lyrics(l[7], l[8]) } // start of tuplet (triplet) if (l[3] == "6" || l[3] == "12" || l[3] == "24") { if (!tp) { tp = b[1].split("\t")[3] == l[3] ? 3 : 2 lo += tp == 3 ? "(3" : "(3::2" } } if (l[0] == "8") { // grace note lo += "{/" + n + "}" } else { if (l[8][0] == "^") // upper annotation lo += "\"" + l[7] + "\"" switch (l[0]) { case "1": // little note if (!snt) { // if first small note snt_a.unshift([lo.length]) snt = 1 if (!snt_a[0][2] && l[7]) snt_a[0][2] = l[7] // 1st lyric word } break // case "2": // rest on the 1st occurence // break // case "3": // rest on the 2nd occurence // break // case "4": // glissendo (to next note) // break // case "5": // attenuating tremolo // break // case "6": // amplifying tremolo // break case "7": // tremolo lo += "!//!" break // case "9": // note/rest // break // case "10": // note followed by a gracenote // break // case "11": // silent gracenote // break case "12": // trill lo += "!trill!" break // case "13": // natural trill // // (higher note with natural acc) // break // case "14": // clustering, change of style // break // (beaming) // case "15": // increasing - fading tremolo // break // case "16": // fading - increasing tremolo // break // case "17": // normal postmultiply // break // case "18": // finish the staff at this level // break case "19": // new page lo += "\n%%newpage" break // case "20": // forbid new page + CR // break // case "21": // carriage return // break // case "22": // silent natural gracenote // break case "23": // mordent lo += "P" // (Pc is played d1/4 c3/4?) break case "24": // lower mordent lo += "M" // (Pc is played b1/4 c3/4?) break // case "25": // natural mordent // break // case "26": // natural lower mordent // break // case "27": // portamento // break // (this note ~~~ the next note) // case "28": // grupetto // break // case "29": // lower grupetto // break // case "30": // natural grupetto // break // case "31": // natural lower grupetto // break // case "32": // lower trill // break // case "33": // triplet with gracenote // break // case "34": // triplet with soft gracenote // break // case "35": // repeat the previous measure // break case "36": // D.C. lo += "!D.C.!" break // case "37": // (1) // break // case "38": // (2) // break // case "39": // (3) // break // case "40": // (4) // break // case "41": // (5) // break // case "42": // (6) // break // case "43": // reverse mordent // break // case "44": // reverse down mordent // break // case "45": // reverse single purple // break // case "46": // reverse single down purple // break // case "47": // vibrato // break // case "48": // free text below staff // break // case "49": // free text on staff // break } v = l[2] // length numerator switch (v) { case "5": v = "4" // fall thru case "9": case "10": if (v == "9" || v == "10") v = "8" d = "" switch (l[3]) { // length denominator default: case "1": d = (8 * v).toString(); break case "2": d = (4 * v).toString(); break case "4": d = (2 * v).toString(); break case "8": d = v; break case "16": d = v + "/"; break case "32": d = v + "//"; break } lo += n + d + "-" v = l[2] == "10" ? "2" : "1" if (ly) { ly1 += "*" ly2 += "*" } break } d = "" switch (l[3]) { // length denominator default: case "1": d = (8 * v).toString(); break case "2": d = (4 * v).toString(); break case "6": tp-- // fall thru case "4": d = (2 * v).toString(); break case "12": tp-- // fall thru case "8": if (v != "1") d = v; break case "24": tp-- // fall thru case "16": if (v != "1") d = v; d += "/"; break case "32": if (v != "1") d = v; d += "//"; break case "64": if (v != "1") d = v; d += "///"; break } lo += n + d } // handle the end of measure if (l[2]) { ct += +l[2] / +l[3] if (!(+l[3] % 12) // if in triplet && ((ct * 32) | 0) != ((ct * 32 + .01) | 0)) ct = Math.round(ct * 32) / 32 } if (l[0] == "10") // if note followed by a gracenote continue if (ct >= mn / md) { // new measure if(ct != mn / md) printErr("Measure problem - mu2_time:"+l[9]+" ct:"+ct+" m:"+(mn/md) +"\n\tline: "+l) if (snt_a.length) do_snt() // generate the small notes lo += "|" ct = 0 acc_reset() if (bmp) bm = bmp.slice() } // handle the beam breaks if (bm[0] && ct >= bm[0]) { bm.shift() if (!Number(d.slice(-1)) // if previous note < quarter && lo.slice(-1) != "|") lo += " " // beam break } else if (ct * 4 == ((ct * 4) | 0) //fixme KO if 6/8/ or 9/8 && !Number(d.slice(-1)) && lo.slice(-1) != "|") { lo += " " } } if (lo) print(lo) if (ly) { if (ly1.match(/[^*]/)) print("w:" + ly1.replace(/\*+$/, "")) if (ly2.match(/[^*]/)) print("w:" + ly2.replace(/\*+$/, "")) if (w1) print("W:" + w1.replace(/ +/g, "\nW:")) if (w2) print("W:" + w2.replace(/ +/g, "\nW:")) } } main(scriptArgs) ' "$@" History: 25-04-01 - change the overlay syntax - replace [K:scale=.8] by %%voicescale .8 - bad handling of only one small note - bug: put the measure bar after the end of the voice overlay (small notes) 25-03-27 - bug: bad lines in the second words after tune - bug: lyric shift when non standard durations (9/8 or 10/16) - handle the small notes - convert P:AB.A into $ .. !fine! .. $ - convert P:A.A into $ .. $ - handle the other measure change inside tune 25-03-21 - shift the tempos somewhat more to the right - handle tempo changes inside tune - handle durations as 9/8th or 10/16th of whole note - handle triplets with 2 notes - handle successive triplets - handle triplets of quarter notes - shift the parts somewhat more to the left - do not display | in lyrics after tune - put double bars before P: - better handling of parts - handle M:/R: before 2nd repeat - handle a last sequence (outro?) in a repeated sequence - accept M:0/0 as M:none 25-03-10 - print the parts somewhat more to the left - add the global P: - upper annotations in the source are followed by one or two "^"s - better repeat indications (?) - avoid to put [P:xx] at end of line - handle first repeat in mid-measure - handle measure change at start of tune - handle grace notes before measure bars - accept the character "|" in the lyrics - accept note names with upper or lower case characters - bad handling of M: when found inside a music line 25-03-03 - fix: no generation when no "14" line at start of tune reported by Hudson Lacerda 25-03-01 - remove some "." after "SAZ" and "ARANAĞME" - remove "ARANAĞME" from the lyrics after tune - accept 1/64th notes 25-01-24 - loss of measure bars since previous version - do not put a space after a measure bar - better handling of the beaming lines (code 14) - set the parts when needed - Add Usul, Beste and Güfte 25-01-23 - ignore the mu2 time (column 9) for setting the measure bars - handle the beam breaks when no line code 14 25-01-22 - handle irregular mu2 files with spaces around \t and bad times (column 9)