// Detect It Easy: DiE-JS framework file
// Don't change anything unless you're sure about what you're doing

// Author: Kae <TG@kaens>

/* A collection of binary (bytecodes, disassemblers) parsers intended for sanity checks.
Each parser must have these parameters (len optional):
    {UInt} [p=0] - pointer (file offset) from where to begin
    {Int} [len=BCParseUntilReasonable] - either the block length or one of the constants above. Not all parsers will support all constants (EoF is implied anyway, and there may not be end markers in a format) but the ToReasonable must be present.
It must return a list [n, e, 0, ...] where:
    - n: BCInvalidFormat, or the first value is the number of commands parsed,or the number of notes read. If n is BCInvalidFormat, the other return list values may be absent;
    - e: -1 or the position after the end marker;
    - 0: [reserved]
    - any custom useful data, such as tags, may be added as well (values [3...]).
*/
/* beautify ignore:start */

//Put these in the length parameter when you're unsure how long the tested block is
BCParseToReasonable = 0; // the default idea of the parser
BCParseToEoF = -1; // physical file end
BCParseToEndMarker = -2; // logical block end (like an end-block bytecode, RET in disassembly...)
BCInvalidFormat = -1;

const debug = 0;


// -= PC98xx MUAP parsing =-

    function MUAP98CmdStr(ch, o, recurse) {
    if (typeof recurse != 'number') recurse = 0; if (recurse > 2) return '…';
    const
        C = [/*0~2: FM:*/'FM1: ', 'FM2: ', 'FM3: ', /*3~5:*/'SSGA: ', 'SSGB: ', 'SSGC: ',
        /*6~8,11~17: FM:*/'FM4: ', 'FM5: ', 'FM6: ', /*9,10:*/'RHY: ', 'PCM: ',
            'FM7: ',/*12~14: either YM3438 or YM2203*/'FM8: ', 'FM9: ', 'FM10: ', 'FM11: ', 'FM12: '],
        notes = ['C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B', 'C'],
        c = X.U8(o), cht = ch == 9 ? 'rhy' : ch == 10 ? 'pcm' : 3 <= ch && ch <= 5 ? 'ssg' : 'fm',
        ifop = ['=', '>', '<', '!=' /*the rest always denote FP*/];
    var t;
    if (cht == 'fm' && c < 0x40)
        return C[ch] + 'note "' + Hex(c) + '" ' + notes[c % 12] + (1 + Util.divu64(c, 12)) + ' ~' + X.U8(o + 1);
    else if (cht == 'ssg' && c < 0x10)
        return C[ch] + 'key-on "' + Hex(c) + '" ' + ' ~' + X.U8(o + 1);
    else switch (c) {
        case 0xFF: return C[ch] + 'rest'; case 0xFE: return C[ch] + 'reset & play'; case 0xFD: return C[ch] + 'reset & stop';
        case 0xFC: return '-= ' + C[ch] + 'End. =-';
        case 0xFB: return C[ch] + "wait on '";
        case 0xFA: return C[ch] + (ch == 9 || ch == 10 ? 'x9 nop' : '3ch 4harm play ' + outArray(X.readBytes(o + 1, 8), 16));
        case 0xF9: return C[ch] + (ch == 9 || ch == 10 ? 'rhy cmd end' : 'same freq play');
        case 0xF8: return C[ch] + 'add freq ' + outArray(X.readBytes(o + 1, 3), 16);
        case 0xF7: return C[ch] + 'loop @' + Hex(o - X.I16(o + 1)) + ' x' + Hex(X.U8(o + 3));
        case 0xF6: return C[ch] + (cht == 'ssg' ? 'noise freq ' : 'pan ') + Hex(X.U8(o + 1));
        case 0xF5: return C[ch] + 'Timer-A tempo ' + Hex(X.U16(o + 1));
        case 0xF4: return C[ch] + 'set length ' + Hex(X.U8(o + 1)) + ', ratio ' + Hex(X.U8(o + 2));
        case 0xF3: return 'wait all channels';
        case 0xF2: return C[ch] + (ch == 9 || ch == 10 ? 'DSP mode, level, delay' + outArray(X.readBytes(o + 1, 3), 16) :
            cht == 'ssg' ? 'set start decay data' + outArray(X.readBytes(o + 1, 3), 16) : 'nop');
        case 0xF1: return C[ch] + 'R' + Hex(X.U8(o + 1)) + ' = ' + Hex(X.U8(o + 2));
        case 0xF0: return C[ch] + (ch == 9 || ch == 10 ? 'Rhythm Key On ' : 'set system detune ') + Hex(X.U8(o + 1));
        case 0xEF: return C[ch] + (ch == 9 || ch == 10 ? 'Rhythm Dump ' : 'hard LFO speed ') + Hex(X.U8(o + 1));
        case 0xEE: return C[ch] + (ch == 9 || ch == 10 ? 'Rhythm pan/vol ' : 'hard LFO AMD,PMD,AMon') + outArray(X.readBytes(o + 1, 2), 16);
        case 0xED: return C[ch] + (ch == 9 || ch == 10 ? 'x2 nop' : '3ch 4harm mode ' + outArray(X.readBytes(o + 1, 2), 16));
        case 0xEC: return C[ch] + 'key display mask on/off & colour ' + Hex(X.U8(o + 1));
        case 0xEB: return C[ch] + (ch == 9 || ch == 10 ? 'PCM Tone ' : cht == 'ssg' ? 'mixer mode ' : 'tone ') + Hex(X.U8(o));
        case 0xEA: return C[ch] + '@jump ' + Hex(o + X.I16(o + 1));
        case 0xE9: return C[ch] + '@call ' + Hex(o + X.I16(o + 1)) + ' ("' + MUAP98CmdStr(ch, o + X.I16(o + 1), recurse + 1) + '"...)';
        case 0xE8: return C[ch] + '@ret';
        case 0xE7: return C[ch] + 'Source Line symbolic info ' + Hex(X.U16(o + 1));
        case 0xE6: return C[ch] + (ch == 9 || ch == 10 || cht == 'ssg' ? 'x27 nop' : 'USR Tone');
        case 0xE5: return C[ch] + 'Play Stack init';
        case 0xE4: return C[ch] + '@if x' + ((X.U8(o + 1) & 0xF) - 6) + ' ' + ifop[X.U8(o + 1) >> 4] + ' ' + X.U8(o + 2)
            + ' jump ' + Hex(t = o + 2 + X.I16(o + 3)) + ' ("' + MUAP98CmdStr(ch, t, recurse + 1) + '"...)';
        case 0xE3: return C[ch] + '@if x' + ((X.U8(o + 1) & 0xF) - 6) + ' ' + ifop[X.U8(o + 1) >> 4] + ' ' + X.U8(o + 2)
            + ' call ' + Hex(t = o + 2 + X.I16(o + 3)) + ' ("' + MUAP98CmdStr(ch, t, recurse + 1) + '")...';
        case 0xE2: return C[ch] + 'change vol data ' + Hex(X.U8(o + 1));
        case 0xE1: return C[ch] + 'tie';
        case 0xE0: return C[ch] + 'loopcnt clear';
        case 0xDF: return C[ch] + 'slur';
        case 0xDE: return C[ch] + 'set ratio ' + Hex(X.U8(o + 1));
        case 0xDD: return C[ch] + 'cmt len ' + Hex(X.U8(o + 1));
        case 0xDC: return C[ch] + 'init Skip_data ' + outArray(X.readBytes(o + 1, 3), 16);
        case 0xDB: return C[ch] + 'cmt: ' + Hex(X.U8(o + 1)) + ' ' + Hex(X.U8(o + 2)) + ': "' + X.SC(o + 4, X.U8(o + 3), 'SJIS') + '"';
        case 0xDA: return C[ch] + 'set X: ' + outArray(X.readBytes(o + 1, 3), 16);
        case 0xD9: return C[ch] + 'set LFO pars. ' + outArray(X.readBytes(o + 1, 6), 16);
        case 0xD8: return C[ch] + 'LFO start(p,a)/stop ' + Hex(X.U8(o + 1)); //LFO start(pmd,amd)/stop
        case 0xD7: return C[ch] + 'vol += ' + Hex(X.U8(o + 1));
        case 0xD6: return C[ch] + 'vol -= ' + Hex(X.U8(o + 1));
        case 0xD5: return C[ch] + (ch == 9 || ch == 10 ? 'PCM play ' : cht == 'ssg' ? 'Start vol/Attack rate ' : 'x3 nop ') + X.U16(o);
        case 0xD4: return C[ch] + (ch == 9 || ch == 10 ? 'PCM addr ' + X.U32(o + 1) : 'x5 nop');
        case 0xD3: var t = (X.U8(o + 1) & 0xF) - 6; return C[ch] + '@if ' + (t >= 0 ? 'x' + t : 'lpcnt') + ' ' + ifop[X.U8(o + 1) >> 4] + ' ' + X.U8(o + 2)
            + ' exit ' + Hex(o + 5 + X.U16(o + 3));
        case 0xD2: return C[ch] + 'Play Stack +1';
        case 0xD1: return C[ch] + 'fade out';
        case 0xD0: return C[ch] + 'ssg||pcm ' + Hex(X.U8(o + 1));
        case 0xCF: return C[ch] + 'channel change ' + Hex(X.U8(o + 1));
        case 0xCE: return C[ch] + (ch == 9 || ch == 10 ? 'set last tone,vol,pan' : cht == 'ssg' ? 'set last tone,vol' : '?? FM: CEh ??');
        default: return C[ch] + 'unk. cmd ' + Hex(c);
    }
}

/** Packen/ぱっくん Software MUAP98/みゅあっぷ Object bytecode detector. If < 0, consider invalid.
 * The custom data returned is the comment field or "".
 */
function parseMUAP98(p, len, ch) {
    len = len || BCParseToReasonable; p = p || 0;
    const p0 = p,
        max = (len == BCParseToReasonable) ? Math.min(0x10000, X.Sz(), p + 0x400) :
            (len == BCParseToEoF || len == BCParseToEndMarker) ? Math.min(0x10000, X.Sz()) : p + len;
    var cht = (ch == 9) ? 'rhy' : ch == 10 ? 'pcm' : 3 <= ch && ch <= 5 ? 'ssg' : 'fm',
        c, notes = 0, stop = false, cmtlen = -1, cmt = "", /*stack depths:*/ lpd = 1, calld = ifd = mp = ic = 0;
    var visited = [];
    for (var i = p0; i < max; i++) visited[i] = false;
    function re(p, t) { if (debug > 1) _l2r('muap98', p, 'ch' + ch + ': ' + t); return [BCInvalidFormat, p, 0]; }
    while (p0 <= p && p < max && !stop) {
        if (ifd < 0) sus++;
        visited[p] = true; if (p > mp) mp = p;
        //_log(Hex(p,4)+': '+MUAP98CmdStr(ch,p));
        c = X.U8(p);
        if (c < 0x40) {
            if (c > 15 && cht == 'ssg') return re(p, '!badSSGnote');
            if (!X.U8(p + 1)) ic++; //actually happens...
            notes++; p += 2;
        }
        else if (c < 0xCE) return re(p, '!badcmd' + Hex(c));
        else switch (c) {
            case 0xFF: p++; break; //keyoff and rest for note length
            case 0xFE: lpd = 0; p++; break; //reset & restart
            case 0xFD: p++; stop = true; break; //reset & stop playback
            case 0xFC: stop = true; p++; if (mp < p) mp = p; break; //channel end playback
            case 0xFB: p++; break; /*wait on '*/
            case 0xF9: if (ch != 9) notes++; p++; break; //FM/SSG: same frequency play; RHY/PCM: rhythm cmd end
            case 0xF8: p += 4; break; //add frequency
            case 0xF7: t = p - X.I16(p + 1); if (!isWithin(t, 0x18, max) || Math.abs(t - p) < 2) return re(p, 'loop@' + Hex(t));
                if (!isWithin(t, p0, max)) sus++; p += 4; break; //loop N times
            case 0xF6: p += 2; break; //pan
            case 0xF5: if (!isWithin(X.U16(p + 1), 0x10, 0xFFF/*dox: C18 max*/)) return re(p, '!badtempo'); //Timer-A tempo
                p += 3; break;
            case 0xF4: p += 3; break; //length/ratio change
            case 0xF3: p++; break; //wait all channels: 小節位置を調整する。演奏しているチャネル全てにこのデータが来るまで待機する。
            case 0xF2: p += 4; break; //RHY/PCM: DSP mode, level, delay; FM/SSG: a 4-byte nop
            case 0xF1: p += 3; break; //set reg data
            case 0xF0: //set system detune; RHY/PCM: Rhythm Key on
                if (ch == 9 || ch == 10) { notes++; if (!X.U8(p + 1)) re(p, 'RHY F0: 0') }
                p += 2; break;
            case 0xDF/*slur*/: case 0xD2/*Play Stack +1*/: case 0xD1/*fade out*/: p++; break;
            case 0xFA: p += 9; break; // 3ch 4harm play; RHY/PCM: a 9-byte nop
            case 0xEF: p += 2; break; //hard LFO speed; RHY/PCM: Rhythm Dump
            case 0xEE: p += 3; break; //hard LFO AMD, PMD, AMon; RHY/PCM: set Rhythm pan/vol
            case 0xED: p += 2; break; //3ch 4harm mode; RHY/PCM: a 2-byte nop
            case 0xEC: p += 2; break; //key display mask on/off & colour
            case 0xEB: p += 2; break; //tone number change
            case 0xEA: //@jump, @call
                t = p + X.I16(p + 1); if (!isWithin(t, 0x18, max) || Math.abs(t - p) < 2) return re(p, '!' + MUAP98CmdStr(ch, p, 1));
                if (t < p0) ic++; if (visited[t]) stop = true; p = t; break;
            case 0xE9:
                t = p + X.I16(p + 1); if (!isWithin(t, 0x18, max) || Math.abs(t - p) < 2) return re(p, '!' + MUAP98CmdStr(ch, p, 1));
                if (t < p0) ic++; calld++; p += 3; break;
            case 0xE8: calld--; if (calld < 0) return re(p, '!RetW/oSub'); p++; break; //@ret
            case 0xE7: p += 3; break; //Source Line symbolic information
            case 0xE6:
            /* if(cht == 'fm') { TODO } */ p += 27; break; //FM: set USR Tone parameter; SSG, RHY, PCM: a 27-byte nop
            case 0xE5: p++; break; //Play Stack init
            case 0xE4: case 0xE3: //if @jump, if @call
                t = p + 2 + X.I16(p + 3); if (!isWithin(t, 0x18, max) || Math.abs(t - p) < 2 || (X.U8(p + 1) >> 4) > 3) return re(p, '!' + MUAP98CmdStr(ch, p, 1));
                if (t < p0) ic++;
                if (c == 0xE3) calld++; if (c == 0xE4) { if (visited[t]) stop = true; p = t } else p += 5; break;
            case 0xE2: p += 2; break; //change vol data
            case 0xE1: p++; break; //tie
            case 0xE0: lpd = 0; p++; break; //loop ctr clear
            case 0xDE: p += 2; break; //change ratio only
            case 0xDD: cmtlen = X.U8(p + 1); p += 2; break; //doesn't seem to be of any use...
            case 0xDC: p += 4; break; //init Skip_data
            case 0xDB: cmt = cmt.appendS(X.SC(p + 4, X.U8(p + 3), 'SJIS'), ' / '); p += X.U8(p + 3) + 4; break;
            case 0xDA: p += 4; break; //set X Value
            case 0xD9: p += 7; break; //set LFO parameters
            case 0xD8: p += 2; break; //LFO start(pmd,amd)/stop
            case 0xD7: case 0xD6: p += 2; break; //increase/decrease vol
            case 0xD5: p += 3; if (ch == 9 || ch == 10) notes++; break; //SSG: Start vol/Attack rate; RHY/PCM: PCM play; FM: a 3-byte nop
            case 0xD4: p += 5; break; //RHY/PCM: set PCM address; FM/SSG: a 5-byte nop
            case 0xD3: p += 5; break; //@if... exit current loop
            case 0xD0: re(p, 'ssg/pcm:' + Hex(X.U8(p + 1))); p += 2; break; //SSG||PCM mode
            case 0xCF:
                ch = X.U8(p + 1); cht = (ch == 9) ? 'rhy' : ch == 10 ? 'pcm' : 3 <= ch && ch <= 5 ? 'ssg' : 'fm';
                p += 2; break; //send channel change
            case 0xCE: p++; break; //SSG:set last mixer mode, vol, env; RHY/PCM:set last tone/vol/pan (from before sfx)
        }
    }
    return [notes, p, 0, cmt, mp, ic];
}


// -= MDX/MXDRV command explainer, useful for loggers =-

function MDXCmdStr(ch, o) {
    const C = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W'];
    const notes = ['D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B', 'B#', 'C', 'C#', 'D']; var c = X.U8(o);
    if (c < 0x80) return C[ch] + ': rest ' + (c + 1);
    else if (c <= 0xDF) {
        c -= 0x80;
        if (ch > 8) return C[ch] + ': smp#' + c;
        else return C[ch] + ': ' + notes[c % 12] + (Util.divu64(c, 12)) + ' ~' + (X.U8(o + 1) + 1);
    }
    else switch (c) {
        case 0xFF: return C[ch] + ': bpm ' + X.U8(o + 1); case 0xFE: return C[ch] + ': R ' + YM2151RegStr(X.U8(o + 1), X.U8(o + 2));
        case 0xFD: return C[ch] + ': voicedata ' + X.U8(o + 1); case 0xFC: return C[ch] + ': pan ' + X.U8(o + 1);
        case 0xFB: if (X.U8(o + 1) & 0x80) return C[ch] + ': @vol ' + (X.U8(o + 1) & 0x7F); else return C[ch] + ': vol ' + X.U8(o + 1);
        case 0xFA: return C[ch] + ': vol-'; case 0xF9: return C[ch] + ': vol+';
        case 0xF8: return C[ch] + ': staccato ' + X.U8(o + 1); case 0xF7: return C[ch] + ': legato';
        case 0xF6: return C[ch] + ': rep.' + X.U8(o + 1) + ' [' + (X.U8(o + 2) ? '/' + X.U8(o + 2) : '') + '...';
        case 0xF5: return C[ch] + ': ...]rep.,ret→' + Hex(o + X.I16(o + 1, _BE));
        case 0xF4: return C[ch] + ': .../rep.esc→' + Hex(o + X.I16(o + 1, _BE));
        case 0xF3: return C[ch] + ': detune ' + X.I16(o + 1, _BE) / 0x40;
        case 0xF2: return C[ch] + ': portamento ' + X.I16(o + 1, _BE) / 0x4000 + ' ↓';
        case 0xF1: if (X.U8(o + 1)) return C[ch] + ': loop from ' + Hex(o + 3 + X.I16(o + 1, _BE)) + '.'; else return C[ch] + ' ends.';
        case 0xF0: return C[ch] + ': delay key-on ' + X.U8(o + 1); case 0xEF: return C[ch] + ': sync send on ch' + X.U8(o + 1);
        case 0xEE: return C[ch] + ': sync wait on ch' + X.U8(o + 1); case 0xED: return C[ch] + ': noise/smp freq ' + X.U8(o + 1);
        case 0xEC: if (X.U8(o + 1) == 0x80) return C[ch] + ': pitch LFO off';
        else if (X.U8(o + 1) == 0x81) return C[ch] + ': pitch LFO on';
        else return C[ch] + ': LFO pitch wf ' + X.U8(o + 1) + ' freq ' + X.U16(o + 2, _BE) + ' amp ' + X.U16(o + 4, _BE);
        case 0xEB: if (X.U8(o + 1) == 0x80) return C[ch] + ': vol LFO off';
        else if (X.U8(o + 1) == 0x81) return C[ch] + ': vol LFO on';
        else return C[ch] + ': LFO vol wf ' + X.U8(o + 1) + ' freq ' + X.U16(o + 2, _BE) + ' amp ' + X.U16(o + 4, _BE);
        case 0xEA: if (X.U8(o + 1) == 0x80) return C[ch] + ': OPM LFO off';
        else if (X.U8(o + 1) == 0x81) return C[ch] + ': OPM LFO on';
        else return C[ch] + ': LFO OPM syn/wf ' + X.U8(o + 1) + ' lfrq ' + X.U8(o + 2) + ' PMD ' + X.U8(o + 3) + ' AMD ' + X.U8(o + 4) + ' P/AMS ' + X.U8(o + 5);
        case 0xE9: return C[ch] + ': LFO key-on dly ' + X.U8(o + 1); case 0xE8: return C[ch] + ': PCM8 on';
        case 0xE7: return C[ch] + ': Fadeout' + (X.U8(o + 1) == 1 ? '' : Hex(X.U8(o + 1))) + ' spd ' + X.U8(o + 2);
        default: return C[ch] + ': unknown command ' + Hex(X.U8(o));
    }
}


// -= Yamaha YM2151 FM Operator Type-M (OPM) related functionality =-

function isYM2151Reg(a) {
//The OPM doesn't use these registers so we break off if we hit one:
    return [0, 2, 3, 4, 5, 6, 7, 9, 0xA, 0xB, 0xC, 0xD, 0xE, 0x10,
        0x13, 0x15, 0x16, 0x17, 0x1A, 0x1C, 0x1D, 0x1E, 0x1F].indexOf(a) < 0;
}

function YM2151RegStr(a, b) {
    //from https://retrocdn.net/images/9/9c/YM2151_Application_Manual.pdf
    // & https://cx5m.file-hunter.com/fmunit.htm - this one contains errors!
    if (!isYM2151Reg(a)) return '!bad#' + Hex(a);
    if (a == 1) if ((b & 2) == b) return 'LFOR'; else return 'TEST' + Bin(b);
    if (a == 8) return 'keyon ch' + (b & 7) + ' slot' + Bin((b >> 3) & 0xF);
    if (a == 0xF) return 'noise' + ['off', 'on'][b >> 7] + ' freq' + Hex(b >> 0x1F);
    if (a == 0x11) return 'CLKA MSB freq' + Hex(b);
    if (a == 0x12) return 'CLKA LSB freq' + Hex(b & 3);
    if (a == 0x13) return 'CLKB freq' + Hex(b);
    if (a == 0x14) return 'Clk CSM' + (b >> 7) + ' FResetBA' + Bin((b >> 4) & 3, 2)
        + ' IRQEnBA' + Bin((b >> 2) & 3, 2) + ' LoadBA' + Bin(b & 3, 2);
    if (a == 0x18) return 'LowOscFreq ' + Hex(b);
    if (a == 0x19) return ['Amp', 'Phase'][b >> 7] + 'Mod depth' + Hex(b & 0x7F);
    if (a == 0x1B) return 'LFOWave ctl' + (b >> 6) + ' ' + ['saw', 'sqr', 'tri', 'noise'][b & 3];
    if (a <= 0x27) return 'Ch ' + (a & 0x7) + ' ctl ' + (b & 0x80 ? 'R' : '') + (b & 0x40 ? 'L' : '') + ' FB' + ((b >> 3) & 7) + ' con' + (b & 7);
    if (a <= 0x2F) {
        o = ((b >> 4) & 7);
        return 'KC/prep note-on ch' + (a & 0x7) + ' '
            + (o ? ['C#', 'D', 'D#', '', 'E', 'F', 'F#', '', 'G', 'G#', 'A', '', 'A#', 'B', 'C', ''][b & 0xF] + o : '--');
    }
    if (a <= 0x37) return 'KF/prep p.bend ch' + (a & 0x7) + ' kf' + (b >> 2);
    if (a <= 0x3F) return 'ModSensy. ch' + (a & 0x7) + ' phase' + ((b >> 4) & 7) + ' amp' + (b & 3);
    if (a <= 0x47) return 'OP1 ch' + (a & 0x7) + ' dt1:' + ((b >> 4) & 7) + ' mul' + (b & 0xF);
    if (a <= 0x4F) return 'OP3 ch' + (a & 0x7) + ' dt1:' + ((b >> 4) & 7) + ' mul' + (b & 0xF);
    if (a <= 0x57) return 'OP2 ch' + (a & 0x7) + ' dt1:' + ((b >> 4) & 7) + ' mul' + (b & 0xF);
    if (a <= 0x5F) return 'OP4 ch' + (a & 0x7) + ' dt1:' + ((b >> 4) & 7) + ' mul' + (b & 0xF);
    if (a <= 0x67) return 'OP1 ch' + (a & 0x7) + ' TL' + (b & 0x7F);
    if (a <= 0x6F) return 'OP3 ch' + (a & 0x7) + ' TL' + (b & 0x7F);
    if (a <= 0x77) return 'OP2 ch' + (a & 0x7) + ' TL' + (b & 0x7F);
    if (a <= 0x7F) return 'OP4 ch' + (a & 0x7) + ' TL' + (b & 0x7F);
    if (a <= 0x87) return 'OP1 ch' + (a & 0x7) + ' KeyScl' + (b >> 6) + ' atk' + (b & 0x1F);
    if (a <= 0x8F) return 'OP3 ch' + (a & 0x7) + ' KeyScl' + (b >> 6) + ' atk' + (b & 0x1F);
    if (a <= 0x97) return 'OP2 ch' + (a & 0x7) + ' KeyScl' + (b >> 6) + ' atk' + (b & 0x1F);
    if (a <= 0x9F) return 'OP4 ch' + (a & 0x7) + ' KeyScl' + (b >> 6) + ' atk' + (b & 0x1F);
    if (a <= 0xA7) return 'OP1 ch' + (a & 0x7) + ' AMS' + ['off', 'on'][b >> 7] + ' dcy1R:' + (b & 0x1F);
    if (a <= 0xAF) return 'OP3 ch' + (a & 0x7) + ' AMS' + ['off', 'on'][b >> 7] + ' dcy1R:' + (b & 0x1F);
    if (a <= 0xB7) return 'OP2 ch' + (a & 0x7) + ' AMS' + ['off', 'on'][b >> 7] + ' dcy1R:' + (b & 0x1F);
    if (a <= 0xBF) return 'OP4 ch' + (a & 0x7) + ' AMS' + ['off', 'on'][b >> 7] + ' dcy1R:' + (b & 0x1F);
    if (a <= 0xC7) return 'OP1 ch' + (a & 0x7) + ' dt2:' + (b >> 6) + ' dcy2R:' + (b & 0x1F);
    if (a <= 0xCF) return 'OP3 ch' + (a & 0x7) + ' dt2:' + (b >> 6) + ' dcy2R:' + (b & 0x1F);
    if (a <= 0xD7) return 'OP2 ch' + (a & 0x7) + ' dt2:' + (b >> 6) + ' dcy2R:' + (b & 0x1F);
    if (a <= 0xDF) return 'OP4 ch' + (a & 0x7) + ' dt2:' + (b >> 6) + ' dcy2R:' + (b & 0x1F);
    if (a <= 0xE7) return 'OP1 ch' + (a & 0x7) + ' dcy2L:' + (b >> 4) + ' rel:' + (b & 0xF);
    if (a <= 0xEF) return 'OP3 ch' + (a & 0x7) + ' dcy2L:' + (b >> 4) + ' rel:' + (b & 0xF);
    if (a <= 0xF7) return 'OP2 ch' + (a & 0x7) + ' dcy2L:' + (b >> 4) + ' rel:' + (b & 0xF);
    return 'OP4 ch' + (a & 0x7) + ' dcy2L:' + (b >> 4) + ' rel:' + (b & 0xF);
}

/** OPM/YM2151 register log detector. If <= 0, consider invalid.
 * no custom data or end marker to be expected.
 */
function parseYM2151RegLog(p, len) {
    //ref https://retrocdn.net/images/9/9c/YM2151_Application_Manual.pdf
    len = len || BCParseToReasonable; p = p || 0;
    var max = (len == BCParseToEoF) ? X.Sz() : Math.min(X.Sz(), p + 0x2000),
        notes = 0, ic = 0, confirmed = false,
        v = [0, 0, 0, 0, 0], chinits = [],
        r, x;
    for(var i=0; i < 8; chinits[i++]=0);
    function re(p, t) { if(debug>1)_l2r('opm', p, t);  return [BCInvalidFormat,p,0]; }
    function iC() { if(debug>0)_l2r('opm', p-2, Hex(r)+' - '+Hex(x)+': invalid value');  ic++; }
    while(!X.U8(p) && p < 0x800) p++; //skip zeroes unless too many
    if(!X.U8(p)) return [BCInvalidFormat,p,0]; //sanity, heuristics, just reasonable they'd cut off the meaningless zeroes
    while (p < max && ic < 10) {
        r = X.U8(p++); if (!r) continue; x = X.U8(p++);
        if (!isYM2151Reg(r)) iC();
        if(debug>1)_logIt(YM2151RegStr(r,x));
/* The working YM2151 regs are:
    01: TEST    |    08: x111 1222: (SM)KON, CH №    |    0F: 1xx2 2222: NE, NFRQ
    10: CLKA1    |    11: xxxx xx11: CLKA2    |    12: CLKAB
    14: 1x22 3344: CSM, FLAG RESET B&A, IRQ-EN B&A, LOAD B&A
    18: LFRQ    |    19: PMD/AMD    |    1B: 11xx xx22: CT, W    |    20~27: 1122 23333: RL, FB, CONECT
    28~2F: x111 2222: KC { OCT, NOTE }    |    30~37: 1111 11xx: KF    |    38~3F: x111 xx22: PMS, AMS
    40~5F: x111 2222: DT1, MUL    |    60~7F: x111 1111: TL    |    80~9F: 11x2 2222: KS, AR
    A0~BF: 1xx2 2222: AMS-EN, D1R    |    C0~DF: 11x2 2222:  DT2, D2R    |    E0~FF: 1111 2222: D1L, RR
*/
        if (r <= 0x27) {
            if (r == 1) { if (x & 0xFD) break; }
            else if (r == 8) {
                if (x & 0x78) { notes += bitCount((x >> 3) & 0xF); v[0]++; }
                else if (x & 0x80) iC();
                // TODO check if the notes played are initialised properly. It won't sound right if not so expect goodness!
                // if(chinits[r & 7] > 5) chok[r & 7]++ // adapt to bitCount!
            }
            else if (r == 0xF) { if (x & 0x60) iC(); }
            else if (r == 0x11) { if (x > 3) iC(); }
            else if (r == 0x14) { if (x & 0x40) iC(); }
            else if (r == 0x1B) { if (x & 0x3C) iC(); }
        }
        else if (r <= 0x2F) { if ((x & 0x80) || [3,7,0xB,0xF].indexOf(x&0xF) >= 0) iC(); }
        else if (r <= 0x37) {} //{ if (x & 3) iC(); } // should be a filter but some tunes want it that way
        else if (r <= 0x3F) { if (x & 0x8C) iC(); }
        else if (r <= 0x7F) { if (x & 0x80) iC(); v[1]++; chinits[r & 7]++; } // TL set
        else if (r <= 0x9F) { if (x & 0x20) iC(); v[2]++; chinits[r & 7]++; } // KS/AR set
        else if (r <= 0xBF) { if (x & 0x60) iC(); v[3]++; chinits[r & 7]++; } // AMS/D1R set
        else if (r <= 0xDF) { if (x & 0x20) iC(); v[4]++; chinits[r & 7]++; } // DT2/D2R set
        else { v[4]++; chinits[r & 7]++; } // D1L/RR set

        if(!confirmed) 
            if(p > 0x1000)
                if (ic >= 40 || notes < 20 || v[0] < 24 || v[1] < 24 || v[2] < 24 || v[3] < 24 || v[4] < 24)
                    return [BCInvalidFormat]; // false positives can be pretty long!
                else confirmed = true
    }
    var chok = 0; for (var i=0; i < 8; i++) if (chinits[i] > 5) chok++;
if(debug>0)_logIt(outArray([notes, v, chok, p], 16))
    if (confirmed && chok > 0) return [notes, p, chok];
    return [BCInvalidFormat, p, chok];
}



// -= Yamaha YM2612(OPN2) related functionality =-

function isYM2612Reg(a) {
//The OPN2 doesn't have these registers:
    return !( a < 0x22 || a == 0x23 || a > 0xB7/*B6*/ || isWithin(a, 0x2C,0x2F)
//        || (isWithin(a, 0x30, 0xAF) && (a & 3) == 3) // per-op registers from 3x to Ax must not have x3,x7, xB, xF. Some files do tho'.
    );
}

/** Mega Drive GYM bytecode detector. Returns BCInvalidFormat in resut[0] if invalid;
 * no custom data or end marker to be expected.
 */
function parseMDGYM(p, len) {
    //ref https://plutiedev.com/ym2612-registers
    //& https://github.com/ValleyBell/libvgm/blob/master/player/gymplayer.cpp
    len = len || BCParseToReasonable; p = p || 0;
    var tmr; if(debug>0){ tmr = new CheckpointTimer(); tmr.init(300); }
    var max = (len == BCParseToEoF) ? X.Sz() : Math.min(X.Sz(), p+0x2000),
        notes = 0, v = [0, 0, 0, 0, 0], ir  = 0, //notes, validation, invalid registers
        c, r, x, doPSGFreq2nd = false;
    function re(p, t) { if (debug>0)_l2r('gym', p, t); return [BCInvalidFormat, p, 0]; }
    while(!X.U8(p) && p < 0x800) p++; //skip zeroes unless too many
    while (p < max && ir < 10) switch (c = X.U8(p++)) {
        case 0: doPSGFreq2nd = false; break;
        case 1: case 2: r = X.U8(p++); if(c == 2 && r < 0x21) return;
            x = X.U8(p++); if (!isYM2612Reg(r)) { ir++; if(debug>0)_l2r('gym',p-2,c + ': R ' + Hex(r)) }
            if (r == 0x28 && (x >> 4)) { notes += bitCount(x >> 4); if(debug>0)_l2r('gym',p-2,'#') }
            else if (r == 0x2A) v[4]++; //PCM
            else if ((r & 0xF0) == 0x30) { v[0]++; if(debug>0)_l2r('gym',p-2,'ML/DT') } // MUL/DT set
            else if ((r & 0xF0) == 0x40 && X.U8(p) > 0) { v[1]++; if(debug>0)_l2r('gym',p-2,'TL') } // TL set
            else if ((r & 0xF0) == 0x50) { v[2]++;  if(debug>0)_l2r('gym',p-2,'AR/RS') } // AR/RS set
            else if ((r & 0xF0) == 0x60) { v[3]++;  if(debug>0)_l2r('gym',p-2,'DR/AM') }// DR/AM set
            else if(debug>0)_l2r('gym',p-2,c+': R '+Hex(r))
            // can't check other reg pushes for validity
            break;
        case 3: r = X.U8(p++); if(debug>0)_l2r('gym',p-2,'PSG');
            if(r & 0x80) doPSGFreq2nd = (!(r&0x10) && r < 0xE0);
            else if(doPSGFreq2nd && r < 0x40) doPSGFreq2nd = false;
            else return [BCInvalidFormat, p, 0];
            break;
        default: return re(p-1, '!cmd' + Hex(c));
    }
if(debug>0)tmr.next('GYM: end of tested area @'+Hex(p)+' ir='+ir)
if(debug>0)_l2r('gym',p,outArray([notes, v], 16));
    if ((!notes || v[0] < 8 || v[1] < 8 || v[2] < 8 || v[3] < 8) && v[0]+v[1]+v[2]+v[3]+v[4] < 100) return [BCInvalidFormat, p, 0];
    return [notes, p, 0];
}


// -= AdLib/Sound Blaster YM3812/OPL2 related functionality =-

//ref https://web.archive.org/web/20050205055453/http://www.gamedev.net/reference/articles/article446.asp

//The AdLib/OPL2 uses these registers:
function isYM3812Reg(a) {
    return isWithinRanges(a, [1,2,3,4,8,0xBD, [0x20,0x35]])
      || ( isWithinRanges(a, [[0x40,0x55], [0x60,0x75], [0x80,0x95], [0xE0,0xF5]] ) && [6,7,14,15].indexOf(a & 0x1F) < 0
      || isWithinRanges(a, [[0xA0,0xA8], [0xB0,0xB8], [0xC0,0xC8]]));
}

var __adlibnote = []; for(var _0=0; _0 < 9; _0++) __adlibnote.push([-1,-1,-1]); //channel: key-on, octave, F-num

function YM3812CmdStr(o, recurse) {
    const
        C = ['1.1:','2.1:','3.1:', '1.2:','2.2:','3.2:','!6:','!7:', '4.1:','5.1:','6.1:', '4.2:','5.2:','6.2:','!E:','!F:',
          '7.1:','8.1:','9.1:', '7.2:','8.2:','9.2:'],
        wf = ['sine', '/￣\\_', '/￣\\/￣\\', '/|_/|_'],
        //notes = ['C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B', 'C'], //TODO? parse the F-number
        c = X.U8(o);
    var t;
    if (c >= 0xE0) return C[c & 0x1F] + 'wf ' + wf[X.U8(o + 1)];
    else if (c >= 0xC0) return ((c & 0xF) + 1) + ':fb/conn'  + Hex(X.U8(o + 1));
    else if (c == 0xBD) return 'AMdepth/VD/Rhy ' + Hex(X.U8(o + 1));
    else if (c >= 0xB0) return ((c & 0xF) + 1) + 'oct/F_msb/key-on ' + Hex(X.U8(o + 1));
    else if (c >= 0xA0) return ((c & 0xF) + 1) + 'F_lsb ' + Hex(X.U8(o + 1));
    else if (c >= 0x80) return C[c & 0x1F] + 'S/R ' + Hex(X.U8(o + 1));
    else if (c >= 0x60) return C[c & 0x1F] + 'A/D ' + Hex(X.U8(o + 1));
    else if (c >= 0x40) return C[c & 0x1F] + 'level ' + Hex(X.U8(o + 1));
    else if (c >= 0x20) return C[c & 0x1F] + 'AM/vib/envgen/keyscale/MFmul ' + Hex(X.U8(o + 1));
    else if (c == 8) return 'CSM/keysplit ' + Hex(X.U8(o + 1));
    else if (c == 4) return 'tmrctl ' + Hex(X.U8(o + 1));
    else if (c == 3) return 'tmr2 ' + Hex(X.U8(o + 1));
    else if (c == 2) return 'tmr1 ' + Hex(X.U8(o + 1));
    else if (c == 4) return 'test/wfctl ' + Hex(X.U8(o + 1));
}

/** AdLib/OPL2 bytecode detector. If <= 0, consider invalid.
 * no custom data or end marker to be expected.
 */
function parseYM3812RegLog(p, len) {
    len = len || BCParseToReasonable; p = p || 0;
    var max = (len == BCParseToEoF) ? X.Sz() - 2 : Math.min(X.Sz() - 2, p + 0x2000),
        notes = 0, v = [0, 0, 0, 0],
        c, r, x, tmr;
    if(debug>0){ tmr = new CheckpointTimer(); tmr.init(300); }
    function re(p, t) { if(debug>1)_l2r('adlib',p,t); return [BCInvalidFormat, p, 0]; }
    while(!X.U8(p) && p < 0x800) p++; //skip zeroes unless too many
    if(!X.U8(p)) return [BCInvalidFormat,p,0]; //sanity, heuristics, just reasonable they'd cut off the meaningless zeroes
    while (p < max) {
        r = X.U8(p++); x = X.U8(p++); if (!isYM3812Reg(r)) return re(p - 2, 'R ' + Hex(r) + ' : '+Hex(x));
        if (r == 0x28 && (x >> 4)) notes += bitCount(x >> 4);
        if ((r & 0xF0) == 0x30) v[0]++; // MUL/DT set
        if ((r & 0xF0) == 0x40 && X.U8(p) > 0) v[1]++; // TL set
        if ((r & 0xF0) == 0x50) v[2]++; // AR/RS set
        if ((r & 0xF0) == 0x60) v[3]++; // DR/AM set
    }
    if(debug>0) tmr.next('OPM chiptune: end of parsed data');
    if(debug>0)_logIt(outArray([notes, v, p], 16))
    if (!notes || v[0] < 24 || v[1] < 24 || v[2] < 24 || v[3] < 24) return [BCInvalidFormat, p, 0];
    return [notes, p, 0];
}

/* beautify ignore:end */