// Detect It Easy: detection rule file
// Author: horsicq <horsicq@gmail.com>
// Authors:
// Kaens TG@kaens (for all the https://en.wikipedia.org/wiki/Music_tracker stuff)
// hypn0 <hypn0@mail.ru>

// Due to the extreme script file size, many good code practices were sacrificed to ensure it loads
/* beautify ignore:start */

meta("audio", "");
includeScript("chunkparsers");
includeScript("soundchips");
includeScript("bytecodeparsers");
const debug = 0; //verbosity up to 3, where every note/command is shown. Overrides the debug() function; use _l2r()

function isWinCert() {
	// I'll skip WIN_CERTIFICATE+bCertificate things for overlay scans to save time :)
	if(X.Sz() >= (_wcsz=X.U32(0,_LE)) && X.c('00 02 02 00 30', 4)) { // assuming the tag's just present immediately
		_wcp = 9; _wca = X.U8(_wcp++);
		if(_wca < 0x80) {} else if(_wca > 0x80) {
			_wcc = _wca ^ 0x80; for(_wci=_wca=0; _wci < _wcc; _wci++) _wca = (_wca << 8) + X.U8(_wcp++); //big-endian here
		}  else _wca = -1;
		if(_wca > 0 && _wca+_wcp <= _wcsz && X.c("06 09 2A 86 48 86 F7 0D 01 07 02",_wcp)) return true; //handoff to other decectors
	}
	return false
}

function detect() {

	if(isWinCert()) return;

	// this C-like pre-declaration is here because lol Qt6
	var p = 0, i = 0, j = 0, k = 0, l = 0, r = 0, t = 0, x = 0, sz = -1,
		title = '', a = '', c = '', msg = '', info = '', artist = '', soption = '';

	if(X.c("'[1tracker module]'0D0A")) {
		sName = "Shiru's 1tracker module (.1TM)"; bDetected = 1;
		l = X.fStr(1,0x40,"Engine="); if(l >= 0) {
			r = X.fStr(l+7,0x40,".");
			sVersion = "for "+X.SA(l+7,r-l-7)
		}
		if(X.isVerbose()) {
			l = X.fStr(r,0x80,"Title="); if(l >= 0) {
				r = X.fSig(l+6,0x40,"0D0A");
				sOption(X.SA(l+6,r-l-6)) }
			l = X.fStr(r,0x80,"Author="); if(l >= 0) {
				r = X.fSig(l+7,0x40,"0D0A");
				sOption(X.SA(l+7,r-l-7),"by: ") }
			l = X.fStr(r,0x80,"Speed="); if(l >=0 ) {
				r = X.fSig(l+7,0x40,"0D0A");
				sOption(X.SA(l+7,r-l-7),"spd:") }
		}
	}

	else if(X.c("'_A2module_'") && isWithin(nV = X.U8(0xE),1,14)) {
		bDetected = 1; bad = false;
		sName = "subz3ro's AdLib Tracker II module (.A2M)";
		sVersion = "/┤DLiB TR/┤CK3R ][ v"+nV;
		ptn = X.U8(0xF); if(!ptn || ptn>64) bad = "!badptn";
		if(bad) sVersion += "/malformed"+bad;
		else if(X.isVerbose()) sOption('ptn:'+ptn); //TODO extract more, ver.-dependent
	}

	else if(X.c("'_A2tiny_module_'") && isWithin(nV = X.U8(0x13),1,14)) {
		sName = "subz3ro's AdLib Tracker II module (.A2T)"; bDetected = 1; bad = false;
		sVersion = "/┤DLiB TR/┤CK3R ][ v"+nV+' tiny';
		ptn = X.U8(0x14); if(!ptn || ptn>64) bad = "!badptn";
		tmp = X.U8(0x15); spd = X.U8(0x16); rows = X.U16(0x18); trk = X.U8(0x1A);
		if(bad) sVersion += "/malformed"+bad;
		else if(X.isVerbose()) sOption('tempo:'+tmp+' spd:'+tmp+' trk:'+trk+' ptn:'+ptn); //TODO extract more
	}

	else if(X.c("'EXITgB'2018201820182F18201024C94AA80004660A558006A80000000200042200D28924C1D0A8000424C0D1DF2018D1D82018D1D82018201812D804800000000166F660B4610000'H|'017E01610000'Z|'027E02610000'R|'037E03610000'JL'DF7FFF'NuH'E7FFFE7E01610000AC7E02610000A67E03610000A04CDF7FFF'Nu`'0A602260000092600000'~A'FA....4BF900DFF000'p0;|'000000AA0440001064F4'NuK'F900DFF0003007C0FC",0x1A) //check D0h bytes
		) {
		// https://www.exotica.org.uk/wiki/Art_%26_Magic_(team)
		bDetected = 1; sName = "Franck Sauer and Yves Grolet's Art & Magic module (.AAM)";
		if(X.isVerbose()) {
			t = X.fSig(0x8000,Math.min(0xFFFF,X.Sz()),"'EXIT'");
			if(t >= 0) sOption(outSz(t+4),"sz:");
			else sVersion = "malformed!short"
		}
	}

	else if(X.c("'ADLIB'01")) {
		sName = "Martin Fernandez's Adlib module (.ADLIB)"; bDetected = 1;
		if(X.isVerbose()) {
			t = X.SA(6,0x100);
			g = X.SA(6+t.length+1,0x100);
			sOption(t); sOption(g,"for: ")
		}
	}

	else if(X.c("'AERO'00000001") && X.U8(0x0F) == 0xCA && X.U8(0x1F) == 0xCA && X.U8(0x2F) == 0xCA) {
		sName = "ioNeo's Aero Studio module (.AERO)"; bDetected = 1;
		if(X.isVerbose()) sOption('sz:'+outSz(X.U32(8)+12))
	}

	else if(X.fSig(20,0x100,"'<aks:song xmlns:aks=\"http://www.julien-nevo.com/ArkosTrackerSong\"") > 0) {
		sName = "Julien Névo's Arkos Tracker 2 module (.AKS)"; sVersion = 'unpacked'; bDetected = 1;
	}

	else if(X.c("'AM01'000000") && X.c("'ASD1'", 0x38) && (amp=X.fSig(0x40,0x1000000,"'AMP'.. ........'ASSH'")) > 0) {
		sName = "New Beat's Ace Tracker module (.AM)"; bDetected = 1;
		maxsz = Math.min(X.Sz(),0x1000000); title = by = bad = ''; ptn = ord = smp = sz = 0; ptns = [];
		p = 0x3C;
		while(p < amp && smp < 0x40 && (t=X.U32(p,_BE))) { p += t; smp++ }  p += 4;
		if(!X.c("'AMP'.. ........'ASSH'",p)) { p = amp; bad = bad.addIfNone('!badsmp'); smp = Math.max(1,smp) }
		ampv = X.U8(p+3)-0x30; if(ampv > 3) bad = bad.addIfNone('!unkver');
		sVersion = 'v'+ampv;
		ins = X.U32(p+4,_BE); p += 12; inss = [];
		for(i=0; i < ins; i++,p+=(ampv == 1? 0xCD: ampv == 2? 0xD1: ampv == 3? 0xD4: 0))
			if((t=X.SA(p+4,20).trim()) != '' && t != 'Empty') inss.push(t)
		if(!X.c("'ASG1'",p)) bad = bad.addIfNone('!badins');
		asg1 = X.fSig(amp,maxsz,"'ASG1'"); ord = ptn = 0;
		if(asg1 < 0) bad = bad.addIfNone('!nosong'); else {
			title = X.SC(asg1+4,20,'CP437'); by = X.SC(asg1+24,20,'CP437')
			for(i=asg1+0x2C; i < asg1+0x12C; i++) if(t=X.U8(i)) { ord = i-asg1-0x2B; if(t > ptn) ptn = t }
		}
		p = asg1+0x12C; apn1 = X.fSig(asg1,0x400,"'APN1'"); ch = ptn_ = -1;
		if(apn1 > 0) {
			if(p != apn1) bad = bad.addIfNone('!badptn'); ptn_ = X.U32(apn1+4,_BE);
			if(ptn != ptn_) bad = bad.addIfNone('!badptn'+ptn+'/'+ptn_);
			for(i=0,p=apn1+8; i < ptn_; i++) {
				t = X.SC(p+4,12,'CP437').trim(); if(t.length && t != 'Empty') ptns.push(t);
				ch_ = X.U16(p,_BE); if(ch_ > ch) ch = ch_;
				p += 16+4*ch_*X.U16(p+2,_BE);
			} sz = p
		} else bad = bad.addIfNone('!noptns');
		if(ch > 16) bad = bad.addIfNone('!badchn');
		if(bad.length) sVersion = sVersion.appendS('malformed'+bad,'/');
		if(X.isVerbose()) {
			sOptionT(title); sOptionT(by,'by: ');
			sOptionT(addEllipsis(inss.filter(funSampleName).join(' ')),'ins/msg:"','"');
			sOptionT(addEllipsis(ptns.join(' ')),'ptns:"','"');
			sOption((ch > 0? 'ch:'+ch+' ': '')+'ord:'+ord+' ptn:'+(ptn!=ptn_?ptn+'/':'')+ptn_+' ins:'+ins+' smp:'+smp
				+' xpos:'+X.I32(0x34)+(sz?' sz:'+outSz(sz):''))
		}
	}

	else if(X.c("'ASG1'") && X.c("'APN1'",0x12C)) {
		sName = "New Beat's Ace Tracker module patterns (.ASG)"; bDetected = 1; bad = '';
		title = X.SC(4,20,'CP437'); by = X.SC(24,20,'CP437'); ptn = ord = 0;
		for(p=0x2C; p < 0x12C; p++) if(t=X.U8(p)) { ord = p-0x2B; if(t > ptn) ptn = t }
		ch = -1; ptns = [];
		for(i=0,p=0x134; i < 0xFF; i++) { // unlike what .am has, .asg saves all 255 patterns
			ch_ = X.U16(p,_BE); if(ch_ > ch) ch = ch_; ptnsz = ch_*X.U16(p+2,_BE); p += 4;
			if(ptnsz) { t = X.SC(p,12,'CP437').trim(); if(t.length && t != 'Empty') ptns.push(t); p += 12+4*ptnsz }
		} sz = p;
		if(ch > 16) bad = bad.addIfNone('!badchn');
		if(bad.length) sVersion = sVersion.appendS('malformed'+bad,'/');
		if(X.isVerbose()) {
			sOptionT(title); sOptionT(by,'by: '); sOptionT(addEllipsis(ptns.join(' ')),'ptns:"','"');
			sOption((ch > 0? 'ch:'+ch+' ': '')+'ord:'+ord+' ptn:'+ptn+' sz:'+outSz(sz))
		}
	}

	else if(X.c("'AMC V1.2 REPLAY!'") && X.c("0000003C0002	",0x4E)) {
		// ref https://gitlab.com/uade-music-player/uade/-/raw/master/amigasrc/players/wanted_team/AMComposer/src/AMComposer 1.2_v4.asm
		sName = "Marc Hawlitzeck's A.M. Composer module (.AMC)"; bDetected = 1;
		sVersion = "v"+X.SA(5,3);
		if(X.isVerbose()) {
			d0 = X.U32(0x14,_BE); a3 = X.U32(0x18,_BE); d3 = c = 0; p = 0x48; special = '';
			for(smp = 0; p < X.Sz() && p < a3; p += 16) {
				d2 = X.I32(p,_BE) + 2*X.U16(p+4,_BE); smp++; if(d2 > d3) d3 = d2 }  p = d3;
			if(p < X.Sz()) {
				while(p+c < X.Sz() && isWithin(X.U8(p+c),32,126)) c++;
				if(!X.U8(p+c) && p+c+1 == X.Sz()) c++; special = X.SA(p,c)
			}
			sOption(special,'info:"','"');
			sOption('smp:'+smp+' sz:'+outSz(p+c))
		}
	}

	else if(X.c("'<o'EF'QU'EE'RoR'", 0x426) || X.c("'MaDoKaN96'", 0x426)) {
		//ref http://modland.com/pub/documents/format_documentation/AMusic%20(.amd).txt
		sName = "Elyssis AMUSiC module (.AMD)"; bDetected = 1;
		if(X.SA(0x426,1) != "<") sVersion = "xms rip-off"; else sVersion = '<o∩QUεRoR/ε£¥$$ì$ v101%';
		if(X.U8(0x42F) == 0x11) sVersion = sVersion.appendS('packed','/');
		if(X.isVerbose()) {
			sOptionT(X.SA(0,0x18));
			sOptionT(X.SA(0x18,0x18),"by: ");
			for(i=0,smps=[]; i < 26; i++) smps.push(X.SC(0x30+i*0x22,0x16,'CP437').replaceAll('\xFF',' ').trim())
			sOptionT(addEllipsis(smps.filter(funSampleName).join(" "),0x100,0xA0), 'smp/msg:"','"')
			ptn = X.U8(0x3A5)+1; notes = -1;
			if(X.U8(0x42F) == 0x11) {
				p = 0x430+ptn*18; trk = X.U16(p); p += 2;
				for(i=0; i < trk && p < X.Sz(); i++)
					for(r=0,p+=2; r < 64 && p < X.Sz(); ) if((c=X.U8(p++)) & 0x80) r += c & 0x7F; else { p += 2; r++; notes++ }
				sz = p;
			} else sz = 0x430+ptn*0x6C0;
			sOption("ord:"+X.U8(0x3A4)+" ptn:"+ptn+(sz > 0?' sz:'+outSz(sz):''));
		}
	}

	else if(X.c("'ASYLUM Music Format V1.0'00000000 00000000") && X.U8(0x22) <= 0x40
	  && X.Sz() >= 0xA66+0x800*X.U8(0x23)) {
		sName = "Electronic Arts' ASYLUM Music Format module (.AMF)"; bDetected = 1;
		sVersion = "v"+X.SA(0x15,3);
		if(X.isVerbose()) {
			spd0 = X.U8(0x20); bpm0 = X.U8(0x21);
			smp = X.U8(0x22); ptn = X.U8(0x23); ord = X.U8(0x24);
			p = 0x126;
			var allsmpsz = 0; smps = [];
			for(i=0; i < smp; i++) {
				smps.push(decAnsi(0x126+0x25*i,0x16,CP437,true,Chars0to1F).trim());
				allsmpsz += X.U32(0x126+0x25*i+0x19);
			}
			sz = 0xA66+0x800*ptn+allsmpsz;
			sOptionT(addEllipsis(smps.filter(funSampleName).join(" "),0x100,0xA0), 'smp/msg:"','"')
			sOption("spd0:"+spd0+" bpm0:"+bpm0+" ord:"+ord+" ptn:"+ptn+" smp:"+smp+" smpsz:"+Hex(allsmpsz)+" sz:"+outSz(sz))
		}
	}

	else if(X.c("'Extreme0'01") && X.U8(0xF) <= 0x20) {
		//ref http://modland.com/pub/documents/format_documentation/Extreme's%20Tracker%20v1.3%20(.ams).txt
		sName = "Extreme's Tracker Advanced Module System track (.AMS)"; bDetected = 1;
		//song tech info
		if(X.isVerbose()) {
			cmd = X.U8(9)>>5; sch = (X.U8(9)&0x1F)+1;
			smp = X.U8(10); ptn = X.U16(11); ord = X.U16(13);
			vmch = X.U8(15); xtra = X.U16(16);
			p = xtra + 18; S = []; allsmpsz = 0;
			for(i=0; i < smp; i++,p+=17) {
				var ssz = X.U32(p); allsmpsz += ssz;
				co = X.U8(p+16)&3; b16 = (X.U8(p+16)&0x80)?2:1; S.push([co,ssz,b16]);
			}
			sOptionT(X.SC(p+1,X.U8(p),'CP437')); p += 1+X.U8(p); //skip title
			smps = []; for(i=0; i < smp; i++) { //store smp names
				smps.push(X.SC(p+1,X.U8(p),'CP437')); p += 1+X.U8(p) }
			schs = []; for(i=0; i < sch; i++) { //store "channel" names
				schs.push(X.SC(p+1,X.U8(p),'CP437')); p += 1+X.U8(p) }
			for(i=0; i < ptn; i++) p += 1+X.U8(p); //skip ptn names
			msg = X.readBytes(p+2,X.U16(p),true/*\0 is a space*/); var msg_ = []; p += 2+X.U16(p);
			for(i=0; i < msg.length; i++) { //RLE
				if(msg[i] < 0x80) msg_.push(msg[i]);
				else if(msg[i] <= 0xA0) /*for(j=0; j < msg[i]-0x80; j++)*/ msg_.push(0x20); //too long
				else msg_.push(0x0A); //this RLE uses too_many_spaces as a line break; let's maybe not
			}
			msg = decEncoding(msg_,'CP437'); delete msg_;
			mptn = -1;
			for(i=0; i < ord; i++) { t = X.U16(p+2*i); if(t > mptn) mptn = t }
			mptn++; p += 2*ord;
			for(i=0; i < ptn; i++) p += 4+X.U32(p);
			allsmpcsz = 0;
			for(i=0; i < smp; i++) if(!S[i][0]) p += 8+S[i][1]*S[i][2]; //non-compressed
			else if(S[i][1]) { //compressed & smp size > 0
				p += 4; scosz = X.U32(p); cc = X.U8(p+4); p += 5+scosz; allsmpcsz += scosz
			}
			delete S;
			sOption(addEllipsis(schs.join(' '),0xA0,0x80),'chns:"','"');
			sOption(addEllipsis(msg.trim(),0x100,0x80),'msg:"','"');
			sOption('ord:'+ord+' ptn:'+ptn+(mptn!=ptn?'/'+mptn:'')+' smp:'+smp+' cmd:'+cmd
				+' strk:'+sch+(vmch?' mtrk:'+vmch:'')+' co.smpsz:'+(allsmpcsz*100/allsmpsz).toFixed(1)+'% sz:'+outSz(p))
		}
	}

	else if(X.c("'AMShdr'1A") && X.U8(7) <= 30 && X.U8(9+X.U8(7)) == 2 && X.U8(8+X.U8(7)) <= 2) {
		//ref http://modland.com/pub/documents/format_documentation/Velvet%20Studio%20v2.2,%20Advanced%20Module%20System%20(.ams).txt
		bDetected = 1; p = 8+X.U8(7); nv = X.U8(p);
		sName = "Velvet Studio Advanced Module System track (.AMS)";
		sVersion = "v"+X.U8(p+1)+'.'+nv;
		bad = '';
		ptn = X.U16(p+3); if(ptn > 1024) bad = bad.addIfNone('!badptn');
		ord = X.U16(p+5);
		if(X.isVerbose()) {
			title = X.SC(8,p-8,'CP437');
			ins = X.U8(p+2);
			p += 7; if(X.Sz() < 36+11+ins*2+ord*2+ptn*4) bad = bad.addIfNone('!short');
			if(nv == 2) {
				bpm0 = Math.max(32<<8, X.U16(p)); p += 2; bpm0 = (bpm0>>8)+'.'+(bpm0&0xFF)
				spd0 = Math.max(1,X.U8(p++)); p += 3; flg = X.U16(p); p += 2
			} else {
				bpm0 = Math.max(32,X.U8(p++)); spd0 = Math.max(1,X.U8(p++)); flg = X.U8(p++)
			}
			ch = (flg >> 6)&1?'2':'1';
			linfreqtbl = (flg & 0x40) ? " lnr.freq.tbl.":"";
			midiused = (flg & 0x80) ? " MIDI used":"";
			sOptionT(title);
			inss = []; smps = []; S = []; allsmpsz = allsmpcsz = smp = shd = 0;
			for(i = 0; i < ins; i++) {
				t_ = X.U8(p++); if(t_ > 30) bad = bad.addIfNone('!badins');
				t = X.SC(p,t_,'CP437').trim(); p += t_; if(t != "") inss.push(t);
				inssmp = X.U8(p++);
				if(inssmp) {
					if(nv == 0) p += 100; else p += 124;
					t = X.U8(p++); if(t > 63) bad = bad.addIfNone('!badenv');
					p += t*3+4; t = X.U8(p++); if(t > 63) bad = bad.addIfNone('!badpan');
					p += t*3+4; t = X.U8(p++); if(t > 63) bad = bad.addIfNone('!badvibenv');
					p += t*3; t = X.U8(p);
					shdins = t > 0; if(shdins) shd++; p += 5;
					for(s = 0; s < inssmp; s++) {
						t_ = X.U8(p++); if(t_ > 22) bad = bad.addIfNone('!badsmp');
						t = X.SC(p,t_,'CP437').trim(); p += t_; //if(t != "") smps.push(t);
						ssz = X.U32(p); p += 4; if(!shdins) smp++;
						if(ssz) {
							if(!shdins) { allsmpsz += ssz; sfl = X.U8(p+15);
								co = sfl&3; b16 = (sfl&4)?2:1; S.push([co,ssz,b16]) }
							p += 16
						}
					}
				}
			}
			t_ = X.U8(p++); sOptionT(X.SC(p,t_,'CP437'),'by:'); p += t_; 
			schs = []; for(i=0; i < 32; i++) { //store "channel" names
				schs.push(X.SC(p+1,X.U8(p),'CP437')); p += 1+X.U8(p) }
			// read the description
			msg = X.readBytes(p+11,X.U32(p)-11); msg_ = []; co = X.U8(p+10); p += X.U32(p);
			if(co) { for(i=0; i < msg.length;) { //RLE
					c = msg[i++];
					if(c == 0xFF && msg.length-i >= 2) {
						c = msg[i++]; n = msg[i++];
						if(c != 0x20) for(j=0; j < n; j++) msg_.push(c);
						else if(n <= 0x20) msg_.push(0x20); //not too many spaces in one
						else msg_.push(0x0A); //too many spaces.
					}
					else msg_.push(c);
				}
				msg = decEncoding(msg_,'CP437');
			} else msg = decEncoding(msg,'CP437');
			delete msg_;
			p += ord<<1; //skip the orderlist
			for(i=0; i < ptn; i++) p += 4+X.U32(p); //skip the patterns
			if(p > X.Sz()) bad = bad.addIfNone('!short');
			for(i=0; i < smp; i++)
				if(!S[i][0]) {
			 		p += S[i][1]*S[i][2]; //non-compressed
				} else if(S[i][1]) { //compressed & smp size > 0
					scosz = X.U32(p+4); cc = X.U8(p+9);
					p += 9+scosz; allsmpcsz += scosz
				}
			sz = p;
			sOption(addEllipsis(schs.join(' '),0xA0,0x80),'chns:"','"');
			sOption(addEllipsis(inss.filter(funSampleName).join(' '),0xA0,0x80),'insts/msg:"','"');
			//sOption(addEllipsis(smps.join(' '),0xA0,0x80),'smps/msg:"','"');
			sOption(addEllipsis(msg.trim(),0xA0),'msg:"','"');
			sOption('ch:'+ch+' ins:'+ins+(shd?'+'+shd+'sh':'')+' ord:'+ord+' ptn:'+ptn+
				' bpm0:'+bpm0+' spd0:'+spd0+linfreqtbl+midiused+
				' co.smpsz:'+(allsmpcsz*100/allsmpsz).toFixed(1)+'% sz:'+outSz(sz))
		}
		if(bad) sVersion = sVersion.appendS('malformed'+bad,'/');
	}

	else if(X.c("'AMX ")) {
		//ref http://and.intercon.ru/releases/audio/xsynth/
		sName = "Dmitry 'AND' Andreev's XSynth module (.AMX)"; bDetected = 1
	}

	else if(X.c("'AON4'") || X.c("'AON8'")) {
		// https://www.exotica.org.uk/wiki/Art_of_Noise
		sName = "Bastian 'Twice' Spiegel's Art of Noise/Chorus module (.AON)"; bDetected = 1; ch = X.U8(3)-0x30;
		if(X.isVerbose()) {
			id = X.SA(4,0x2A);
			p = 0x2E; //header skipped
			t = a = d = c = ''; ord = ptn = ins = 0;
			while (p < X.Sz()) {
				if(X.U8(p) == 0x3F) { p += (4-p) & 3; break }
				hkhd = X.readBytes(p,4); if(charStat(hkhd,1).indexOf('allasc') < 0) break;
				hkhd = decEncoding(hkhd,CP437); hksz = X.U32(p+4,_BE); p += 0x08;
				switch (hkhd) {
					case 'NAME': t = X.SC(p,hksz,'CP1252'); break;
					case 'AUTH': a = X.SC(p,hksz,'CP1252'); break;
					case 'DATE': d = X.SC(p,hksz,'CP1252'); break;
					case 'RMRK': c = X.SC(p,hksz,'CP1252'); break;
					case "PLST": ord = hksz; for(i=0; i < ord; i++) if((o=X.U8(p+i)+1) > ptn) ptn = o; break;
					case 'WLEN': for(i=0; i < (hksz>>2); i += 4) if(X.U32(p+i,_BE)) ins++; break;
				}
				p += hksz;
			}
			sOptionT(t); sOptionT(a,"by: "); sOptionT(d,"on: "); sOption(id,"in: ");
			sOption(addEllipsis(c.trim(),0xA0));
			sOption('ch:'+ch+' ord:'+ord+' ptn:'+ptn+' ins:'+ins+' sz:'+outSz(p))
		}
	}

	else if(X.c("'ADRVPACK'")) {
		sName = "Petter A. Urkedal's AProSys module (.APS)"; bDetected = 1
	}

	else if(X.c("'ARP.'")) {
		sName = "Major Tom's Player 2 module (.ARP)"; bDetected = 1
	}

	else if(X.c("'ACTIONAMICS SOUND TOOL'",0x3E)) {
		sName = "Michael Kleps's Actionamics Sound Tool module (.AST)"; bDetected = 1;
		sVersion = "v"+X.SA(0x56,3);
	}

	else if(X.c("08'AST '") && isWithin(X.U8(9), 4,8) && X.U16(0xA) <= 1001) {
		// http://justsolve.archiveteam.org/wiki/All_Sound_Tracker_module
		sName = "Patrice 'Cagliostro' Bouchand's All Sound Tracker module (.AST)";
		sVersion = 'v'+X.SA(5,4); bDetected = 1;
		if(X.isVerbose()) {
			L = X.U16(0x0A);
			info = X.SC(0xC,L+1,'CP850'); p = 0xD+L;
			s = "";
			for(i = 0; i < L; i+=0x26) {
				if((t = info.slice(i,i+0x26).trim()).length) s = s.appendS(t,'\n') //skipping the empty lines
			}
			sOption(addEllipsis(s,0x200));
			ord = X.U8(p++); sz = 0;
			for(i=ptn=0; i <= ord; i++) ptn = Math.max(ptn,X.U8(p++)+1);
			fmp = X.I32(p); awp = X.I32(p+4); ptnp = X.I32(p+8);
			awesonp = X.I32(p+0xC); wavep = X.I32(p+0x10); p += 0x14; sz = Math.max(sz,p);
			for(p=fmp,fmi=0,n=X.U8(p++); n < 0xFF && p < X.Sz(); fmi++) { p += 20; n = X.U8(p++) }
			sz = Math.max(sz,p);
			for(p=awp,awes=0,n=X.U8(p++); n < 0xFF && p < X.Sz(); awes++) { p += 51; n = X.U8(p++) }
			for(awei=0,n=X.U8(p++); n < 0xFF && p < X.Sz(); awei++) { p += 39; n = X.U8(p++) }
			for(awed=0,n=X.U8(p++); n < 0xFF && p < X.Sz(); awed++) { p += 3; n = X.U8(p++) }
			p += 2;//cht = X.U8(p++); revt = X.U8(p++);
			p += 9*(14+9) + 9*(56+9)+1; sz = Math.max(sz,p);
			for(p=ptnp,ptn_=0,n=X.U8(p++); n < 0xFF && p < X.Sz(); ptn_++) { p += 2+X.U16(p); n = X.U8(p++) }
			sz = Math.max(sz,p);
			for(p=wavep,wf=0,n=X.U8(p++); n < 0xFF && p < X.Sz(); wf++) { p += 36; n = X.U8(p++) }
			sz = Math.max(sz,p); if(sVersion === 'v0001') sz--; //v01 doesn't have the finalising FF, so EoF is how it ends
			sOption('def:'+X.U8(9)+' ord:'+ord+' ptn:'+ptn+(ptn_!=ptn?'/'+ptn_:'')   +' wavep:'+Hex(wavep)
				+(fmi?' FMins:'+fmi:'')+(awes?' AWEsmp:'+awes:'')
				+(awei?' AWEins:'+awei:'')+(wf?' wf:'+wf:'')+' sz:'+outSz(sz))
		}
	}

	else if(X.c("'AudioSculpture10'00180018")) {
		sName = "Synchron Assembly's Audio Sculpture synth file (.AS)"; bDetected = 1
	}

	else if(X.c("'##synth'0D0A")) {
		sName = "Athaudia's Athtune module (.ATHTUNE)"; bDetected = 1
	}

	else if(X.c("'FORM'.... ....'AXSFUSER'") && X.c("'SHDR'", t=X.U32(0x10,_BE)+0x14)
	  && X.c("'SONG'",X.U32(t+4,_BE)+t+8)) {
	  	//ref https://web.archive.org/web/20141129231503/http://www.resolutionaudio.nl/
		sName = "Resolution's AXS module (.AXS)"; bDetected = 1;
		if(X.isVerbose()) {
			for(ord=ptn=ins=smp=0,sz=X.U32(4,_BE)+8,p=0xC,maxsz=Math.min(sz,X.Sz()); p < maxsz;) {
				hkhd = X.readBytes(p,4); if(charStat(hkhd,1).indexOf('allasc') < 0) break;
				hkhd = decEncoding(hkhd,CP437); hksz = X.U32(p+4,_BE); p += 8;
				switch(hkhd) {
				case 'SONG': ord = hksz >> 3; break;
				case 'BLOK': ptn++; break;
				case 'INST': ins++; break;
				case 'SAMP': smp++; break
				}
				p += hksz
			}
			sOption('ord:'+ord+' ptn:'+ptn+(ins?' syn:'+ins:'')+(smp?' smp:'+smp:'')+' sz:'+outSz(sz))
		}
	}

	else if(X.c("'BBSONG'00'0001'00")) {
		//ref https://github.com/atsidaev/beepola/blob/master/STSong.pas etc.
		sName = "Shiru's Beepola module (.BBSONG)"; bDetected = 1;
		p = 0xC; var layouttag = false;
		ord = orn = ptn = svgptn = svgwarp = phains = 0; bad = title = auth = engine = '';
		while(p < X.Sz()) {
			if(p >= X.Sz()) break;
			if(X.U8(p++) != 0x3A) { p--; break }
			t = X.SA(p,254); p += t.length+1;
//_log(t+' @'+Hex(p));
			switch(t) {
			case 'INFO':
				for(; p < X.Sz();) {
					t = X.SC(p,TOEOF,'CP1251'); p += t.length + 1; if(t === ':END') break;
					xx = t.split('='); if(xx.length > 2) xx = [xx[0], xx.slice(1,xx.length).join('=')];
					switch(xx[0]) {
					case 'Title': title = xx[1]; break;
					case 'Author': auth = xx[1]; break;
					case 'Engine': engine = xx[1]; break;
					}
				}
				break;
			case 'LAYOUT':
				layouttag = true;
				while(p < X.Sz()) {
					t = X.SC(p,TOEOF,'CP1251'); p += t.length + 1; if(t === ':END') break;
					xx = t.split('='); if(xx.length > 2) xx = [xx[0], xx.slice(1,xx.length).join('=')];
					switch(xx[0]) {
					case 'LoopStart': lp = +xx[1]; break;
					case 'Length': ord = +xx[1]; ptn = 0;
						for(i=0; i < ord; i++) if(ptn <= (t=X.U8(p++))) ptn = t+1; break;
					}
				}
				break;
			case 'PATTERNDATA':
				while(p < X.Sz()) {
					t = X.SC(p,0x400,'CP1251'); p += t.length + 1; if(t === ':END') break;
					xx = t.split('='); if(xx.length > 2) xx = [xx[0], xx.slice(1,xx.length).join('=')];
					switch(xx[0]) {
					case 'PatternCount': ptns = +xx[1];
						for(i = 0; i < ptns && p < X.Sz(); i++) {
							t = X.SC(p,0x500,'CP1251'); p += t.length + 1; if(t === ':END') break;
							xx = t.split('='); if(xx.length > 2) xx = [xx[0], xx.slice(1,xx.length).join('=')];
							if(xx[0] === 'PatternName') p += 8+5*X.U32(p);
						}
						break;
					}
				}
				break;
			case 'SVGPATTERNDATA':
				while(p < X.Sz()) {
					t = X.SC(p,0x400,'CP1251'); p += t.length + 1; if(t === ':END') break;
					xx = t.split('='); if(xx.length > 2) xx = [xx[0], xx.slice(1,xx.length).join('=')];
					switch(xx[0]) {
					case 'PatternCount': svgptn = +xx[1];
						for(i = 0; i < svgptn && p < X.Sz(); i++) p += 4+16*X.U32(p);
						break;
					}
				}
				break;
			case 'SVGWARPDATA':
				while(p < X.Sz()) {
					t = X.SC(p,0x400,'CP1251'); p += t.length + 1; if(t === ':END') break;
					xx = t.split('='); if(xx.length > 2) xx = [xx[0], xx.slice(1,xx.length).join('=')];
					switch(xx[0]) {
					case 'PatternCount': svgwarp = +xx[1];
						for(i = 0; i < svgwarp && p < X.Sz(); i++) p += 4+2*X.U32(p);
						break;
					}
				}
				break;
			case 'P1INSTR':
				while(p < X.Sz()) {
					t = X.SC(p,0x400,'CP1251'); p += t.length + 1; if(t === ':END') break;
					xx = t.split('='); if(xx.length > 2) xx = [xx[0], xx.slice(1,xx.length).join('=')];
					switch(xx[0]) {
					case 'Length': phains = +xx[1]; p += phains; break;
					}
				}
				break;
			case 'SVGORNAMENTS':
				while(p < X.Sz()) {
					t = X.SC(p,0x400,'CP1251'); p += t.length + 1; if(t === ':END') break;
					xx = t.split('='); if(xx.length > 2) xx = [xx[0], xx.slice(1,xx.length).join('=')];
					switch(xx[0]) {
					case 'OrnamentCount': orn = +xx[1];
						for(i = 0; i < orn && p < X.Sz(); i++) p += 4+X.U32(p);
						break;
					}
				}
				break;
			default: bad = bad.addIfNone('!badchunk')
			}
//_log('   ...ends @'+Hex(p));
		}
		if(!layouttag) bad = bad.addIfNone('!corrupt');
		sVersion = 'engine:'+engine; if(bad != '') sVersion = sVersion.appendS('malformed'+bad,'/');
		if(X.isVerbose()) {
			sOptionT(title); sOptionT(auth,"by: ");
			sOption('ord:'+ord+(lp?' lp:'+lp:'')+' ptn:'+ptn+(svgptn?'+svg':'')
				+(svgwarp?' warp':'')+(orn?' orn':'')+(phains?' phasers':'')+' sz:'+outSz(p))
		}
	}

	else if(X.c("000003F3") && X.U8(0x14)
	  && X.c("70FF4E75'DAGLISH!'",0x20)
	  && X.U32(0x2C,_BE) && X.U32(0x30,_BE)
	  && X.U32(0x34,_BE) && X.U32(0x38,_BE)) {
		bDetected = 1;
		sName = "Ben Daglish's SID (.BDS)"; sVersion = "v1.1";
		if(X.isVerbose()) {
			title = X.SA(0x20+X.U32(0x3C,_BE),0x100);
			auth = X.SA(0x20+X.U32(0x40,_BE),0x100);
			misc = X.SA(0x20+X.U32(0x44,_BE),0x100);
			sOptionT(title);
			x = X.U32(0x38,_BE); if(x > 1) sOption(x,"×");
			sOptionT(auth,"by: ");
			sOptionT(misc);
		}
	}

	else if(/BMF1\.[012]/.test(X.SA(0,6)) && X.Sz() > 8 && X.fSig(6,42,"00") > 6) {
		//ref https://github.com/adplug/adplug/blob/master/src/bmf.cpp
		sName = "The Brain's Easy Adlib module (.BMF)"; sV = X.SA(5,1);
		sVersion = "v1."+sV; bDetected = 1;
		if(X.isVerbose() && sV > '0') {
			p = 6; bad = title = author = "";
			f = X.fSig(p,-1,"00");
			title = X.SA(p,f-p); p += title.length+1; //read title
			f = X.fSig(p,-1,"00"); if(f > 0) {
			author = X.SA(p,f-p); p += author.length+1; //read author
			spd = X.U8(p++);
			if(X.isDeepScan()) {
				fl = X.U32(p); p += 4; ins = 0;
				for(i = 0; i < 32; i++) {
					if(fl&1) { p += 24; if(p > X.Sz()) bad = bad.addIfNone('!short') } fl >>= 1; ins++
				}
				fl = X.U32(p); p += 4; const last = 1023;
				trk = 0; trkend = false;
				for(i = 0; i < 32; i++) {
					if(fl&1) { trk++; trkend = false;
						for(q = 0; p < X.Sz() && !trkend && q <= last; q < last && q++) {
							t = X.U8(p++);
							switch(t) {
							case 0xFE: trkend = true; break; //end of reader
							case 0xFC: case 0x7D: break;  //set marker, loop to marker
							default:
								evnote = t & 0x7F; if(!(t & 0x80)) break;
								t = X.U8(p);
								if(t & 0x80) {
									evdly = t & 0x3F; p++; if(!(t & 0x40)) break;
								}
								t = X.U8(p++);
								if(0x40 <= t) { evvol = t+1-0x40; }
								else if(0x20 <= t && t <= 0x3F) { evins = t+1-0x20; }
								else {
									if(sV == '2') if(1 <= t && t <= 6) p++
								}
							}
						}
					}
					fl >>= 1
				}
				if(!trkend) { bad = bad.addIfNone('!short'); p = -1 }
			}//if deepscan
			}//if author is null-terminated
			else bad = bad.addIfNone('!badtags');
			if(bad != "") sVersion = sVersion.appendS('malformed'+bad,'/');
			sOptionT(title); sOptionT(author,'by: ');
			sOption('spd:'+spd+(X.isDeepScan()?' trk:'+trk+' ins:'+ins+' sz:'+outSz(p):''));
		}
	}

	else if(X.Sz() > 0x180 && X.c("'Buzz'") && isWithin(X.U32(4),1,0xC) && X.c("7C010000",0xC) && X.fSig(8,0x40,"'MACH'") > 0) {
		sName = "Oskari Tammelin's Jeskola Buzz module (.BMX)";  bDetected = 1;
		// Do NOT waste time trying to extract anything else...
		// You'll find the format dox lie and formats differ
		// For example, the no. of machines overlaps with some beta's version string
		// For example, that beta has no float x/y coordinates
		// And there is no signature difference.
	}

	else if(X.Sz() >= 0x6E5C && X.c("0100'ADLIB'9D02A0021C000000") && X.adler32(0,0x6E5C) == 0x1DF2E9B9) {
		sName = 'Instrument bank for Adlib Visual Composer (STANDARD.BNK)'; bDetected = 1;
		if(X.isVerbose()) sOption(outSz(0x6E5C),'sz:')
	}

	else if(X.c("'BRTF'") && ['NAME','INFO'].includes(X.SA(8,4))) {
		sName = "Benjamin 'BeRo' Rousseaux's BeRoTracker module (.BRT)"; bDetected = 1;
		if(X.isVerbose()) {
			p = 8; t = c = q = ''; instmsg = []; sainmsg = [];
			ins = smp = ptn = ord = 0; //TODO figure out how to get ptn
			bpm0 = spd0 = st0 = mvol = rowsperbeat = hltu = hltd = 0; end = false;
			while (!end && p < X.Sz()) {
				hkhd = X.SA(p,4);
				hksz = X.U32(p+4,_LE);
				p += 8;
				switch (hkhd) {
					case "NAME": t = X.UCSD(p); break;
					case "MESS": c = X.SC(p+2,X.U32(p,_LE),'CP1252'); break;
					case "BPMI": rpb = X.U8(p); break;
					case "INFO":
						spd0 = X.U8(p+4); bpm0 = X.U8(p+5);
						st0 = X.U8(p+6); mvol = X.U8(p+10);
						break;
					case "PORD": ord = X.U8(p); break;
					case "PATT": ptn++; break;
					case "PAIN": hltu = X.U8(p+1); hltd = X.U8(p+2); break;
					case "SAIN":
						smp++;
						q = X.UCSD(p+2); if(q.trim().length) sainmsg.push(q);
						break;
					case "INST":
						if(X.U8(p)) ins++;
						q = X.UCSD(p+1); if(q.trim().length) instmsg.push(q);
						break;
					case "DONE": end = true; break;
				}
				p += hksz;
			}
			sOption(t); sOption(c);
			sOption(instmsg.filter(funSampleName).join("\n"),"ins/msg:\n");
			sOption(sainmsg.filter(funSampleName).join("\n"),"smp/msg:\n");
			sOption('bpm0:'+bpm0+' spd0:'+spd0+' ord:'+ord+' ins:'+ins+' smp:'+smp+' hlt:'+hltu+'/'+hltd+
				' RPB:'+rpb+' st.sep0:'+st0+' mixvol:'+mvol+' sz:'+outSz(X.U32(4)+8));
		}
	}

	else if(X.c("01000000") && isWithin(X.U8(4),1,0xF) && X.c("FFFFFFFF",0x34)
	  && X.c("1027",0x15C) && X.c("1027",0x164) && X.c("1027", 0x16C)) {
		sName = "Christer Andersson's BoyScout module (.BSF)"; bDetected = 1
	}

	else if(X.c("'FUCO'") && X.c("'DIGI'",0x4404) && X.c("'DIGP'",0x47F8)) {
		sName = "Anthony J. 'Slates' Bybell's BSI Future Composer module (.BSI)"; bDetected = 1;
		bad = '';
		for(smp=smpsz=i=0; i < 63; i++) {
			if(t=X.U32(0x440C+i*16,_BE)) smp++; smpsz += t;
			if(t > 0x20000) bad = bad.addIfNone('!badsmpsz')
		} if(bad != '') sVersion = sVersion.appendS('malformed'+bad,'/');
		if(X.isVerbose()) {
			sOption('smp:'+smp+' sz:'+outSz(0x47FC+smpsz))
		}
	}

	else if(X.c("'NuBEATHOVEN'",0x22)) {
		bDetected = 1;
		sName = "Thomas 'Dr.Nobody' Lopatic's Beathoven Synthesizer (.BSS)";
		sVersion = "v"+X.SA(0x2D,3);
		if(X.isVerbose()) {
			title = X.SA(0x6C,0x100);
			auth = X.SA(0x6C+title.length+1,0x100);
			misc = X.SA(0x6C+title.length+auth.length+2,0x100);
			sOptionT(title);
			sOptionT(auth,"by: ");
			sOptionT(misc);
		}
	}

	else if(X.c("'CBA'F9") && X.U8(0x24) == 0x1A && 0x14C+X.U16(0x25)+X.U8(0x29)*0x30 <= X.Sz()
	  && isWithin(X.U8(0x27),1,32) && X.U8(0x2B) && X.U8(0x2C) >= 32) {
		sName = "Chuck Biscuits & Zenic/Heretics' module (.CBA)"; bDetected = 1;
		if(X.isVerbose()) {
			msglen = X.U16(0x25); ch = X.U8(0x27); ptn = X.U8(0x28)+1; ord = X.U8(0x29);
			smp = X.U8(0x2A); spd0 = X.U8(0x2B); tmp0 = X.U8(0x2C);
			for(i=smpsz=0; i < smp; i++) smpsz += X.U32(0x14C+i*0x30+0x24);
			sz = 0x14C + smp*0x30 + ptn*64*5*ch + smpsz;
			msg = X.SC(sz,msglen,"CP437").trim(); sz += msglen;
			sOptionT(X.SA(4,0x20)); //some sample names have user messages but few, no good way to tell
			sOption(addEllipsis(msg,0xC0,0xA0),'msg:"','"');
			sOption('spd:'+spd0+' tempo:'+tmp0+' ord:'+ord+' ptn:'+ptn+' smp:'+smp+' sz:'+outSz(sz))
		}
	}

	else if(X.c("'<CUD-FM-File>'1ADEE0") && (!X.U8(0x13) && X.c("'CUD-FM-File - SEND A POSTCARD -'",0x601)
	  || X.U8(0x13) && X.c("'YsComp'07'CUD1997'1A04",0x20))) {
		//ref https://github.com/adplug/adplug/blob/master/src/cff.cpp
		sName = "Daniel Eshcbach/CUD's Boom Tracker 4 module (.CFF)"; sVersion = 'v'+X.U8(0x10); bDetected = 1; 
		if(X.U8(0x13)) sVersion += '/LZW-packed';
		if(X.isVerbose()) sOption(outSz(0x20+X.U16(0x11)),'sz:')
	}

	else if(X.c("'CHIPv1.0'")) {
		//ref http://alonecoder.nedopc.com/zx/CHIP14SRC.rar / chip
		sName = "Dmitry 'Alone Coder' Bystrov's Chip Tracker module (.CHI)"; bDetected = 1;
		sVersion = "v"+X.SA(5,1); bad = ''; if(X.Sz() <= 0x100) bad = bad.addIfNone('!badsz');
		ptn = smp = 0; tempo = X.U8(0x28); ord = X.U8(0x29)+1; lp = X.U8(0x30);
		for(i=0; i < ord; i++) { p = X.U8(0x100+i)+1; if(p > ptn) ptn = p; if(p > 31) bad = bad.addIfNone('!badord') }
		sz = 0x200+0x200*ptn;
		for(i=0; i < 16; i++) if(ssz = X.U16(45+i*4)) { smp++; sz += ssz; if(sz&0xFF) sz += 0x100-(sz & 0xFF) }
		if(bad.length) sVersion = sVersion.appendS('malformed'+bad,'/');
		if(X.isVerbose()) {
			sOptionT(decAnsi(8,0x20,CPSpeccy,false,Chars0to1FSpeccy));
			sOption('tempo:'+tempo+' ord:'+(lp?lp+'-':'')+ord+' ptn:'+ptn+' smp:'+smp+' sz:'+outSz(sz))
		}
	}

	else if(X.c("000003F3") && X.c("70FF4E75'S.PHIPPS'",0x20) &&
	  0 < X.I32(0x40,_BE) < X.Sz() && 0 < X.I32(0x44,_BE) < X.Sz() ) {
		sName = "Simon Phipps/Core Design module (.CORE)"; bDetected = 1;
		if(X.isVerbose()) {
			title = X.SA(0x68,0x100);
			auth = X.SA(0x68+title.length+1,0x100);
			misc = X.SA(0x68+title.length+auth.length+2,0x100);
			sOptionT(title);
			sOptionT(auth,"by: ");
			sOptionT(misc);
		}
	}

	else if(X.c("'CAT '................'FORM'")) {
		sName = "Andreas Öman's Cybertracker module (.CT)"; bDetected = 1;
	}

	else if(X.c("0004'NNTRKMZX'")) {
		sName = "CyberBrain's Cybertracker C64 module (.CT)"; bDetected = 1;
		//ref https://noname.c64.org/download.php/ctmisc/ctfileformat-1_01.html
		sVersion = "v"+X.U8(0x0B)+"."+X.U8(0x0A);
		if(X.isVerbose()) {
			for(sng=0; sng < X.U16(0x4B9,_LE); sng++)
				if(X.U8(0x4BB+sng-1) == 0) break;
			if(sng) {
				sng0ord = X.U8(0x4BB);
				loop0 = X.U8(0x6BD);
				if(sng > 1) sOption("×"+sng+" ord0:"+sng0ord+" loop0:"+loop0);
				else sOption(" ord:"+sng0ord+" loop:"+loop0)
			}
			else sOption("empty");
		}
	}

	else if(X.c("0004'NNTRKINS'")) {
		sName = "CyberBrain's Cybertracker C64 instrument (.CI)"; bDetected = 1;
		sVersion = "v"+X.U8(0x0B)+"."+X.U8(0x0A);
		if(X.isVerbose())
			sOptionT(X.SA(0x1A,0x10))
	}

	else if(X.c("000003F3") && (X.fStr(0x20,0x100,"NuDELIRIUM") > 0)) {
		sName = "DeliTracker player addon or Amiga Custom Module (.CUST)"; bDetected = 1;
		sVersion = "CustomPlay";
		if(X.isVerbose())
			sOptionT(X.SA(X.fStr(0,0x100,"NuDELIRIUM")+20,0x100))
	}

	else if(X.c("'JCH'26026601") && X.c("'Creative Voice File'1A1A000A01291101......C300",0x40)) {
		sName = "Jens Christian 'JCH/Vibrants' Huus's Edlib Tracker samples (.S01)"; bDetected = 1
	}

	else if(X.Sz() < 0xFFFF && (X.c("'JCH'26 026600") || X.c("'JCH'26 026680"))
	  && (X.c("FFFF",X.U16(0x71,_LE)-2) || X.c("FFFF",X.U16(0x73,_LE)-2))) {
		//ref http://ftp.modland.com/pub/documents/format_documentation/Edlib%20Tracker%20(.edl,%20.d00,%20.d01).txt
		msgp = 0; i = 0; //bruteforce-find the message at the end of the file (version-dependent)
		while (i < 4) { t = X.U16(0x71+i,_LE); if(t != 0xFFFF && t > msgp) msgp = t; i += 2 }
		sz = X.fSig(msgp,TOEOF,"FFFF")+2;
		bDetected = 1; bad = 0;
		sName = "Jens Christian 'JCH/Vibrants' Huus's Edlib Tracker module (.D00,.D01)";
		sVersion = "v"+X.U8(7);
		//if(X.U8(6) == 4) bad = 1;
		if(X.U8(0xA) > 1) bad = 1;
		if(bad) sVersion += "/malformed"+bad;
		if(X.isVerbose()) {
			x = X.U8(9);
			sOptionT(X.SC(0x0B,0x20,'CP850'));
			if(x > 1) sOption(x,"×");
			sOptionT(X.SC(0x2B,0x20,'CP850'),"by: ");
			sOptionT(X.SC(msgp,sz-msgp-2,'CP850'));
			sOption(outSz(sz),"sz:")
		}
	}

	else if(X.c("'DBM0'........'NAME'0000002C") && X.U8(4) < 4
	  && X.c("'INFO'0000000A",0x3C) && X.c("'SONG'",0x4E)
	  && X.U16(0x44,_BE) <= 255 && X.U16(0x46,_BE) <= 255
	  && X.U16(0x4A,_BE) <= 1024 && X.U16(0x4C,_BE) >=4
	  && X.U16(0x4C,_BE) <= 254 && !(X.U16(0x4C,_BE)%1)) {
		// ref http://www.digibooster.de/en/format.php
		// & https://www.amigafuture.de/app.php/dlext/details?df_id=5071
		sName = "APC&TCP/Andeas Magerl's DigiBooster Pro module (.DBM)"; bDetected = 1;
		sVersion = "v"+X.U8(4)+"."+X.U8(5).padStart(2,'0');
		p = 8; done = gotinfo = false; bad = ""; if(X.isVerbose()) ord = []; insts = []; titles = [];
		while(!done && p < X.Sz()) {
			hkhd = X.SA(p,4); hksz = X.U32(p+4,_BE); p += 8;
			switch(hkhd) {
			case "INFO": x = X.U16(p+4,_BE); ins = X.U16(p,_BE);
				info = "ch:"+X.U16(p+8,_BE)+" ptn:"+X.U16(p+6,_BE)
				+" ins:"+ins+" smp:"+X.U16(p+2,_BE);
				gotinfo = true;
				break;
			case "SONG": if(!gotinfo) { bad = bad.addIfNone("!badchunkorder"); break }
				if(!X.isVerbose()) break; q = 0;
				while(q+44 < hksz) { t = X.SC(p+q,44,'CP1250').trim(); titles.push(t);
					q += 44; ord.push(X.U16(p+q,_BE)); q += 2+ord[ord.length-1]*2 }
				break;
			case "INST": if(!gotinfo) { bad = bad.addIfNone("!badchunkorder"); break }
				for(i = 0; i < ins; i++) {
					t = X.SA(p+i*50,34).trim(); if(t != "") insts.push(t);
					c3freq = X.U32(p+i*50+34,_BE);
					if(c3freq < 2000 || c3freq > 192000) bad = bad.addIfNone("!badc3freq");
				} break;
			case "SMPL": if(!gotinfo) bad = bad.addIfNone("!badchunkorder"); done = true
			}
			p += hksz;
		}
		if(!done || p > X.Sz()) bad = bad.addIfNone("!short");
		if(bad != "") sVersion = sVersion.appendS("malformed"+bad,"/");
		if(X.isVerbose()) {
			title = X.SA(0x10,0x2C).trim(); sOption(title);
			sOption(titles.join("; "),(!title && title.length == 1 ? "songs:" : ""));
			if(title == "" && !titles.length) sOptionT(X.SA(0xD8,28));
			if(x > 1) sOption(x,"×");
			sOptionT(addEllipsis(insts.join(" "),0x100,0xA0),'smp/msg: "','"');
			info = "ord:"+ord.join("+")+" "+info; sOption(info+" sz:"+outSz(p))
		}
	}

	else if(X.c("'DSNGSEQU'00")) {
		sName = "David Hanney's module (.DH)"; bDetected = 1
	}

	else if(X.c("'DIGI Booster module'00") && X.Sz() >= 0x624 && X.U8(0x19)
	  && X.U8(0x19) <= 8 && X.U8(0x2F) <= 0x7F) {
		sName = "APC&TCP/Andeas Magerl's DigiBooster module (.DIGI)"; bDetected = 1;
		nV = X.U8(0x18); sVersion = "v"+(nV>>4)+"."+(nV&0xF); co = X.U8(0x1A);
		if(sVersion != X.SA(0x14,4).toLowerCase()) sVersion += '/"'+X.SA(0x14,4)+'"';
		if(co) sVersion = sVersion.appendS("co.ptn","/");
		if(X.isVerbose()) {
			sOptionT(decAnsi(0x262,0x20,CPAmiga,true)); msg = []; ch = X.U8(0x19);
			ord = X.U8(0x2F)+1; ptn = X.U8(0x2E)+1;
			smp = ssz = 0;
			for(i=0; i < 31; i++) {
				slen = X.U32(0xB0+4*i,_BE);
				if(slen) { smp++; ssz += slen }
			}
			for(i=0; i < 31; i++)
				msg.push(decAnsi(0x282+0x1E*i,0x1E,CPAmiga,true).trim());
			sz = 0x624;
			if(co) for(i=0; i < ptn && sz <= X.Sz(); i++) sz += 2+X.U16(sz,_BE);
			else sz += 0x100*ptn*ch;
			sz += ssz;
			if(sz > X.Sz() && !X.isVerbose()) sVersion = sVersion.appendS("malformed!short","/");
			sOptionT(addEllipsis(msg.filter(funSampleName).join(" "),0x100,0xA0),'by/msg: "','"');
			sOption("ord:"+ord+" ptn:"+ptn+" smp:"+smp+" sz:"+outSz(sz))
		}
	}

	else if(X.c("000003F3") && X.c("70FF'NuUNCLEART'",0x20)
	  && X.U8(0x14) && X.U32(0x2C,_BE)
	  && X.U32(0x30,_BE) && X.U32(0x38,_BE)) {
		sName = "Dave 'Uncle Tom' Lowe module (.DL)"; bDetected = 1;
		if(X.isVerbose()) {
			smpip = 0x20+X.U32(0x3C,_BE);
			smpiep = 0x20+X.U32(0x40,_BE);
			if(!smpiep) smp = 0;
			else smp = Math.floor((smpiep-smpip)/14);
			title = X.SA(0x20+X.U32(0x50,_BE),0x100);
			auth = X.SA(0x20+X.U32(0x54,_BE),0x100);
			cmt = X.SA(0x20+X.U32(0x58,_BE),0x100);
			loadsz = 0x20+X.U32(0x5C,_BE);
			sz = 0x20+X.U32(0x60,_BE);
			sza = Hex(X.Sz()-sz); //TODO value different, why? Where does it start?
			smpsz = 0x20+X.U32(0x64,_BE);
			songsz = 0x20+X.U32(0x68,_BE);
			sfx = 0x20+X.U32(0x6C,_BE);
			x = 1;
			d1 = 0x20+X.U32(0x4C,_BE);
			if(d1) {
				a1 = d1;
				while(1) {
					a1 += 16;
					d1 = X.U32(a1,_BE);
//_log("read(0x20+a1:"+Hex(a1)+") = "+Hex(d1));
					if(!d1) break;
					d1 -= X.U8(a1+3);
					if(!d1) break; else x++
				}
			}
			sOptionT(title);
			if(x > 1) sOption(x,"×");
			sOptionT(auth,"by: ");
			sOptionT(cmt);
			sOption("smp:"+smp+" sfx:"+sfx)//+" sz:"+outSz(sz)+" a:"+sza+" smpsz:"+smpsz+" songsz:"+songsz+" loadsz:"+loadsz);
		}
	}

	else if(X.c("'ALL '0000 .... 0000") && X.U8(6) <= 1 && X.U8(10) <= 1) {
		sName = "Bent 'SHOGUN' Nielsen's Delta Music module (.DM)"; sVersion = 'v1.3'; bDetected = 1
	}

	else if(X.c("4A00670003B40C000001670001EC0C000002670A0C000003671270004E7541FA0B680201003F11",4)
	  && X.c("0000'.FNL'",0xBC4)) { //the module data begins at 878h
		sName = "Bent 'SHOGUN' Nielsen's Delta Music 2 module (.DM2)"; sVersion = 'v2.0'; bDetected = 1
	}

	else if(X.c("'DMF'0E")) {
		sName = "Webfoot Digital Sound and Music Interface Advanced Music Format hack (.DMF)";
		sOption("delta samples & no text"); bDetected = 1
	}

	else if(X.c("'DDMF'") && X.U8(4) && X.U8(4) <= 10
		&& X.U16(0x3F) && X.c("'CMSG'", 0x42)) {
		//ref https://github.com/OpenMPT/openmpt/blob/master/soundlib/Load_dmf.cpp
		sName = "D-Lusion X-Tracker module (.DMF)"; bDetected = 1;
		nV = X.U8(4); sVersion = "v"+nV;
		p = 0x42; hkhd = hksz = end = lpst = ch = ord = ptn = smp = sz = ptntotallen =  allsmpsz = 0; lped = 0xFFFF;
		while(hkhd != "ENDE" && p < X.Sz()) {
			hkhd = X.SA(p,4); hksz = X.U32(p+4); p += 8;
//_log("@"+Hex(p-4)+" "+hkhd+" ["+Hex(p)+" -> "+Hex(p+hksz)+"]");
			switch(hkhd) {
			case "SEQU": ord = 0;
				if(nV == 3) p += 2; else if(nV == 4) p += 4;
				u = p;
				if(nV >= 3) { lpst = X.U16(u); u += 2 }
				if(nV >= 4) { lpst = X.U16(u); u += 2 }
				if(nV === 4 & !lped) lped = 0xFFFF;
				ord = (p+hksz-u)>>1; break;
			case "PATT":
				var ptnhdsz = nV < 3 ? 9 : 8; u = p;
				ptn = X.U16(u); trk = X.U8(u+2); u += 3;
				ch = trk; if(ch < 1) ch = 1; if(ch > 32) ch = 32;
				for(i = 0; i < ptn; i++) {
					u += ptnhdsz-4;
					var ptnlen = X.U32(u); ptntotallen += ptnlen; u += ptnlen;
				}
				break;
			case "SMPI": u = p; smp = X.U8(u++);
				for(i = 0; i < smp; i++) {
					var snamelen = nV < 2 ? 30 : X.U8(u++);
					var sname = X.SC(u,snamelen,'CP437').trim(); u += snamelen;
					var ssz = X.U32(u); allsmpsz += ssz;
//_log("slen @"+Hex(u)+" = "+Hex(slen));
					u += 4+4+4+2;
					var svol = X.U8(u++), sf = X.U8(u++);
//_log("smp["+i+"] "+sname+" length:"+Hex(slen)+" total:"+Hex(smptotallen)+(svol?"":" ignored"));
					if(nV >= 8) { var libname = X.SC(u,8,'CP437'); u += 8 }
					u += nV > 1 ? 6 : 2;
				}
				break;
			case "SMPD":
				if(!X.c("'ENDE'",p+hksz)) {
					// Simply search for it. It's unique enough.
					// The multiplier of 3 is chosen for stereo + white noise "compression"
					t = X.fSig(p,Math.min(X.Sz()-p, allsmpsz*3),"'ENDE'");
					if(t < 0) { sVersion += "/malformed!noeof"; hkhd = "ENDE"; break }
					else {
						hksz = t-p;
//_log("SMPD length should be "+Hex(hksz));
					}
				}
				break;
			case "ENDE": p -= 4+hksz; break;
			}
			sz = p;
			p += hksz;
		}
		if(hkhd != "ENDE") sVersion += "/malformed!short"; else sz = p;
		if(X.isVerbose()) {
			sOptionT(X.SC(0xD,30,'CP437'));
			sOptionT(X.SC(0x2B,20,'CP437'),"by: ");
			yy = X.U8(0x41);
			if(yy < 80) yyyy = "20"; else yyyy = "19"; yyyy += yy.padStart(2,'0');
			sOptionT(yyyy+"-"+X.U8(0x40).padStart(2,'0')+
				"-"+X.U8(0x3F).padStart(2,'0'),"on: ");
			sOptionT(X.SA(5,8),"in: ");
			cmt = ""; cnt = X.U32(0x46)-1; var linelen = 40, readpos = 0x4B;
			while(cnt) {
				var readlen = Math.min(cnt,linelen,0x200);
				var curline = X.SC(readpos,readlen,'CP437');
				if(curline != "") cmt = cmt.appendS(curline,'\n');
				cnt -= readlen; readpos += readlen;
			}
			sOption(addEllipsis(cmt,0x100,0x80));
			sOption("ch:"+ch+"+1 ord:"+ord+" ptn:"+ptn+" smp:"+smp+" sz:"+outSz(sz))
		}
	}

	else if(X.c("'.DelekDefleMask.'")) {
		//ref https://web.archive.org/web/20150226185703if_/http://www.delek.com.ar:80/soft/deflemask/DMF_SPECS.txt
		// It's a pretty damn old document though...
		sName = "Leonardo Demartino's DeFleMask module (.DMF)"; bDetected = 1;
		sVersion = "v"+X.U8(0x10);
		if(X.isVerbose()) {
			switch(X.U8(0x11)) { //TODO add more new chip versions
			case 0x01: sVersion += "#YMU759"; ch = 17; break;
			case 0x02: sVersion += "#Genesis (10ch)"; ch = 10; break;
			case 0x03: sVersion += "#SMS (4ch)"; ch = 4; break;
			case 0x04: sVersion += "#GameBoy (4ch)"; ch = 4; break;
			case 0x05: sVersion += "#PCEngine (6ch)"; ch = 6; break;
			case 0x06: sVersion += "#NES (5ch)"; ch = 5; break;
			case 0x07: case 0x47: sVersion += "#C64 (3ch)"; ch = 3; break;
			case 0x08: sVersion += "#YM2151 (13ch)"; ch = 13; break;
			default: ch = 4 //just 'cause
			}
			sOption(X.UCSD(0x12));
			l = X.U8(0x12); p = 0x12+l+1;
			sOption(X.UCSD(p),"by: ");
			l = X.U8(p); p += l+1+2;
			tbase = X.U8(p++);
			tick1 = X.U8(p++); tick2 = X.U8(p++);
			Hz = X.U8(p++)?"60(NTSC)":"50(PAL)";
			if(X.U8(p++)) Hz = X.U8(p++)+":"+X.U8(p++)+":"+X.U8(p++);
			else p+=3;
			//These have changed over the years it seems, TODO: RE it
			//p++; _ripm = X.U8(p++);
			//arptick = X.U8(p++); p += ch*_ripm;
			//ins = X.U8(p++);
			sOption("tbase:"+tbase+" tck:"+tick1+":"+tick2+" freq:"+Hz); //+" ins:"+ins+" arp.tick:");
		}
	}

	else if(X.c("' M'........'IAN'") && X.c("0000....0000....0000....0000",0x1C) && (smp=X.U32(0x44,_BE)) <= 255
		&& ( !((smpsz=X.U32(0x48,_BE)) && !smp) || (smp && smpsz) ) && isWithin(ins=X.U32(0x3C,_BE),1,255)) {
		sName = "Reinier 'Rhino' van Vliet's Digital Mugician module (.DMU,.MUG)";
		if((sv=X.SA(9,1)) == '/') sVersion = 'v1'; else if(sv == '2') sVersion = 'v2'; bDetected = 1;
		if(X.isVerbose()) {
			x = ord = 0; ptn = X.U16(0x1A,_BE); wf = X.U32(0x40,_BE);
			for (i=0x4C, p = 0x1C, a2 = 0xCC; i < 0xCC; i += 0x10, p += 4) {
				if(!X.U32(i,_BE)) break; if(X.U32(p,_BE) != 1 || X.U32(a2,_BE) || X.U32(a2+4,_BE)) x++;
				a2 += X.U32(p,_BE) << 3;
				t = X.SA(i+4,12); while(t[t.length-1] < ' ') t = t.slice(0,t.length-1); sOptionT(t)
			}
			for(i=0x1C; i < 0x3C; i += 4) ord += X.U32(i,_BE);
			sz = smp*0x20+ins*0x10+smpsz+0x1CC+ptn*0x100+wf*0x80+ord*8;
			if(x > 1) sOption(x,'×');
			sOption('ord:'+ord+' ptn:'+ptn+' ins:'+ins+' smp/syn:'+smp+'+'+wf+' sz:'+outSz(sz))
		}
	}

	else if(X.c("'DSM'10") && X.c("1A",0x24) && X.U16(0xB2)-X.U16(0xB0) == 4
	  && X.c("'DSI'10",X.U16(0xB0) << 4) && X.c("'DSI'10",X.U16(0xB0+X.U8(0x27)*2-2) << 4)) {
	  	//see also https://modland.com/pub/documents/format_documentation/Digital%20Audio%20Sound%20Interface%20Kit%20spec%20(.dsm)-v0.1.txt
		sName = "Carlos Hasan's Digital Audio Sound Interface Kit module (.DSM)"; bDetected = 1;
		if(X.isVerbose()) {
			for(i=0xAF; i >= 0x30 && X.U8(i) == 0xFF; i--) {} rord = i-0x2F;
			for(mptn = 0; i >= 0x30; i--) if((t=X.U8(i)+1) > mptn) mptn = t;
			ord = X.U8(0x26); ptn = X.U8(0x28);
			p = X.U16(0xB0)<<4; inss = []; mp = 0;
			for(i=ins=0; i < X.U8(0x27); i++,p+=0x40) {
				if(!X.c("'DSI'10",p)) bad = bad.addIfNone('!badins');
				inss.push(X.SC(p+4,0x20,'CP437').trim());
				var sp = X.U16(p+0x24)<<4; if(sp) ins++; if(sp > mp) mp = sp+X.U16(p+0x26)
			}
			p = 0xB0+X.U8(0x27)*2+ptn*2-2; p = X.U16(p) << 4; p += 2+X.U16(X.U16(p)); //past the last pattern
			sz = Math.max(mp,p);
			sOptionT(X.SC(4,0x20,'CP437'));
			sOption(addEllipsis(inss.filter(funSampleName).join(' '),0xA0),'ins/msg:"','"');
			sOption('ord:'+(rord==ord?'':rord+'/')+ord+' ptn:'+(mptn==ptn?'':mptn+'/')+ptn
				+' ins:'+ins+' sz:'+outSz(sz))
		}
	}

	else if(X.c("'RIFF'........'DSMFSONG'") && X.U16(0x36) <= 128 && X.U16(0x38) <= 128
	  && X.U16(0x3C) <= 256 && X.U16(0x3E) <= 16) {
		sName = "Carlos Hasan's Digital Sound Interface Kit module (.DSM)"; bDetected = 1;
		sVersion = "RIFF";
		if(X.isVerbose()) {
			sOptionT(X.SC(0x14,28,'CP437'));
			sz = X.U32(4)+8; p = 0xC; smp = 0;
			while(p < sz) {
				hkhd = X.SA(p,4); hksz = X.U32(p+4); p += 8;
				if(hkhd == "INST") smp++
				p += hksz;
			}
			if(p > X.Sz()) sVersion += "/malformed!badchunk";//+hkhd+" @"+Hex(p-hksz-8)+"="+Hex(hksz);
			sOption("ch:"+Math.max(1,X.U16(0x3E))+" spd0:"+X.U8(0x42)+" bpm0:"+X.U8(0x43)
				+" ord:"+X.U16(0x38)+"("+X.U16(0x34)+"-"+X.U16(0x36)
				+") ptn:"+X.U16(0x3C)+" smp:"+X.U16(0x3A)+"/"+smp
				+" sz:"+outSz(sz))
		}
	}

	else if(X.c("'DSm'1A20")) {
		sName = "The Loom Syndicate's Dynamic Studio module (.DSM)"; bDetected = 1;
		if(X.isVerbose()) {
			sOptionT(X.SA(5,20));
			sOptionT(X.SA(25,20),"by: ");
		}
	}

	else if(X.c("'DSFmt1'0D0A")) {
		sName = "Audio Simulation's DreamStation module (.DSS)"; bDetected = 1;
		sVersion = "v1.0";
		if(X.isVerbose()) {
			pt = X.fSig(0,TOEOF,"F0E40001")+4; //the 00/01 at the end is "show info at track load"
			if(pt < 4) pt = X.fSig(0,TOEOF,"F0E40000")+4;
			if(pt >= 4) {
				pa = X.fSig(pt,TOEOF,"0D0A"); t = X.SA(pt,pa-pt); pa += 2;
				pc = X.fSig(pa,TOEOF,"0D0A"); a = X.SA(pa,pc-pa); pc += 2;
				c = X.SA(pc,X.Sz()-pc);
				sOption(t); sOption(a,"by: "); sOption(c)
			}
		}
	}

	else if(X.c("'DS2F0'....'Default'")) {
		sName = "Audio Simulation's DreamStation II module (.DS2)"; bDetected = 1;
		sVersion = "v2";
		if(X.isVerbose()) {
			ts=X.U8(0x62); t=X.SA(0x63,ts);
			pa=0x62+ts+1; as=X.U8(pa); a=X.SA(pa+1,as);
			pc=pa+as+1;  cs=X.U8(pc); c=X.SA(pc+1,cs);
			sOption(t);
			sOption(a,"by: ");
			sOption(c);
		}
	}

	else if(X.c("'MMU2'00")) {
		sName = "Great Valley Products' Digital Sound Studio module (.DSS)"; bDetected = 1;
		sVersion = "v1-3.0";
		if(X.isVerbose()) {
			sOptionT(X.SA(10,32));
			sOption("ord:"+X.I16(0x59C,_BE));
		}
	}

	else if(X.c("'DTL'00") && X.c("0000001000", 0x16)) {
		sName = "Larry Tipton's Drum Traker module (.DTL)"; bDetected = 1;
		if(X.isVerbose()) {
			sOptionT(X.SA(4,20));
			//sOption('sz:'+outSz(0xD0+(1+X.U16(0x1B))*0xE0))
		}
	}

	else if(X.c("'D.T.'00")) {
		sv = ["S.Q.","VERS"].indexOf(X.SA(0x2A,4));
		if(sv >= 0) {
			sName = "Softjee's Digital Tracker module (.DTM)"; bDetected = 1;
			if(X.isVerbose()) {
				if(sv==0) sOptionT(X.SA(0x16,20))
				//let's play along with Bummtschak here
				else sOptionT(X.SA(0x16,24))
			}
		}
	}

	else if(X.c("'DeFy DTM'")) {
		sName = "DeFy AdLib Tracker module (.DTM)"; bDetected = 1;
		sVersion = "v"+X.SA(9,3);
		if(X.isVerbose()) {
			sOptionT(X.SA(0xD,20));
			sOptionT(X.SA(0x21,20),'by: ');
		}
	}

	else if(X.c("'SONG'........'NAME'")) {
		//ref http://hackipedia.org/browse.cgi/File%20formats/Music%20tracker/Digitrakker/DigiTrekker%20Module%20Format%20by%20Horst%20Beham%20Jr.%20v1.0.txt
		sName = "Horst Beham Jr.'s DigiTrekker module (.DTM)"; bDetected = 1;
		if(X.isVerbose())
			sOptionT(X.SA(0x10,20));
	}

	else if(X.c("48E7F1FE610000964CDF7F8F'NuH'E70010610000'$L'DF0800'NuH'E7F1FE610001844CDF7F8F'NuH'E70010610000'NL'DF0800'NuG'FAFFC651EB05',Q'EB05'PQ'EB05'tQ'EB059833FC000F00DFF09633FC00FF00DFF09E33FC000000DFF0A833FC000000DFF0B833FC000000DFF0C833FC000000DFF0D8'NuG'FAFF80177C00010524177C00010548177C0001056C177C00010590'NuG'FAFF'bJ+'00BA670E'S+'00B96608177C000600B9'Nu`'0003180006")) {
		sName = "David Whittaker's SFX module (.DW)"; bDetected = 1
	}

	else if(X.c("' PWD'03") && X.c("'Master'",0x0E)) {
		sName = "Daniel Werner/ExperimentalScene's DarkWave Studio module (.DWP)"; bDetected = 1
	}

	else if(X.c("'EASO'")) {
		sName = "Morten Grouleff's EarAche module (.EA,.EAS)"; bDetected = 1;
	}

	else if(X.c("'FORM'.... ....'EMODEMIC'")
	  && X.c("'PATT'",pt=0x14+X.U32(0x10,_BE)) && X.c("'8SMP'",q=pt+8+X.U32(pt+4,_BE))) {
		//ref https://github.com/libxmp/libxmp/blob/master/src/loaders/emod_load.c
		sName = "Bo Lincoln's Quadra Composer module (.EMOD)"; bDetected = 1; sVersion = 'v'+X.U16(0x14,_BE);
		smp = X.U8(0x3F); if(smp) smps = []; sz = X.U32(4,_BE)+8;
		for(i=ssz=0,p=0x40; i < smp; i++,p+=0x22) {
			if((t=X.SC(p+4,0x14,'CP1252').trim()) != '') smps.push(t);
			ssz += X.U16(p+2,_BE) << 1;
		}
		bad = ''; if(ssz != X.U32(q+4,_BE)) bad = bad.addIfNone('!badsmplen:'+X.U32(q+4,_BE)+' vs '+ssz)
		ptn = X.U8(++p); p++;  p += 0x1A*ptn;
		// for (i=0; i < ptn; i++,p+=0x1A) if((t=X.SC(p+2,0x14,'CP1252').trim()) != '') smps.push(t);
		if(X.c("'MDIN'",q1=q+8+X.U32(q+4,_BE))) q = q1;
		if(Math.abs(sz-(sz1=q+8+X.U32(q+4,_BE))) > 1) bad = bad.addIfNone('!badlen:'+sz+' vs '+sz1);
		if(sz < sz1) sz = sz1; ord = X.U8(p++); if(bad != '') sVersion = sVersion.appendS('malformed'+bad,'/');
		if(X.isVerbose()) {
			sOptionT((t=X.SA(0x16,0x14)) === '<sans titre>'? '': t);
			if(smp) sOptionT(addEllipsis(smps.join(' '),0xC0,0xA0),'smp/msg: "','"'); delete smps;
			sOption('bpm:'+X.U8(0x3E)+' ord:'+ord+' ptn:'+ptn+' smp:'+smp+' sz:'+outSz(sz))
		}
	}

	else if(X.c("'E.M.S. V6.0'..00010000") && isWithin(X.U8(0xB),0x31,0x36)) {
		sName = "Sean 'Odie' Connolly's Electronic Music System module (.EMS)"; bDetected = 1;
		sVersion = "v6."+X.SA(10,2);
		if(X.isVerbose()) {
			//ref ems_player_dis.asm
			a1 = X.U32(0xE,_BE); a2 = X.U32(0x12,_BE); a3 = X.U32(0x16,_BE); p = lb0 = 0x22;
			p += a1; lb4 = p; p += a2; lb8 = p; p += a3; insp = p; sz = p+X.U32(0x1E,_BE);
//_log(Hex(lb0)+'; [4]:'+Hex(lb4)+'; [8]:'+Hex(lb8)+'; insp:'+Hex(insp))
			// p = a2 = lb4; lbdata = a5 = 0x13740;
			// ssp = ( 0 & 0x1F)*0x84; ss1 = X.U16(p+ssp,_BE); ss2 = X.U16(p+ssp+2,_BE);
//_log('subsong @'+Hex(p+ssp)+' has '+Hex(ss1)+' and '+Hex(ss2))
			// In the player, the following bit was copying the modified offsets (of something) to the data buffer:
			// p += ssp+4; p1 = p;
			// for(d1 = 0, ptrs=[]; d1 < 0x20; d1++) { t = X.U32(p,_BE)+p1; p += 4; ptrs.push(t) }
			sOption('sz:'+outSz(sz))
		}
	}

	else if(X.c("'ENF '....'scor'")) {
		sName = "Musitek SmartScore Extended Notation Format sheet music (.ENF)"; bDetected = 1;
		p = 6; staf = brln = 0; hkhd = "dumm";
		while(p < X.Sz()) {
			hkhd = X.SA(p,4);
			if(!/[a-z\[\]\-0-9]{4}/.test(hkhd)) break;
			hksz = X.U16(p+4,_BE);
			if(hkhd === "staf") staf++;
			else if(hkhd === "brln") brln++;
			p += hksz //this format includes signatures into the chunk size
		}
		sOption("staves:"+staf+" bars:"+brln+" sz:"+outSz(p))
	}

	else if(X.Sz() > 0x24 && X.c("'ETracker (C) BY ESI.'",10) ||
	  (X.Sz() > 0x4D4 && (X.c("21B384") ||X.c("21B304")) && X.c("'ETracker (C) BY ESI.'",0x4BD))) {
		//kudos to horsicq for helping narrow down the bad samples in modland files
		sName = " Maciej J. Wołoszyk & Andrzej Siuda's E-Tracker file (.ETC,.SAA)"; bDetected = 1;
		if(X.c("21B384") || X.c("21B304")) { sVersion = "&player"; ofs = 0x4B3 } else ofs = 0;
		p = ofs; x = mp = sz = 0; ords = []; ptns = [];
		while(p < X.Sz()-0x24 && X.c("'ETracker (C) BY ESI.'",p+10)) {
			x++; //TODO not too good for a ripper but there're multitrack files like this, iono
			ordp = p+X.U16(p); if(ordp > mp) mp = ordp;
			ptnp = p+X.U16(p+2); if(ptnp > mp) mp = ptnp;
			smpp = p+X.U16(p+4); if(smpp > mp) mp = smpp;
			ornp = p+X.U16(p+6); if(ornp > mp) mp = ornp; svd = ofs+X.U16(p+6); if(svd > mp) mp = svd;
			if(sz < mp) sz = mp;
			if(x == 1 && sz > X.Sz()) return false;
//_log('Track at '+Hex(p)+': ordp:'+Hex(ordp)+' ptnp:'+Hex(ptnp)+' smpp:'+Hex(smpp)+' ornp:'+Hex(ornp)+' svd:'+Hex(svd))
			ord = lp = xpos = ptn = 0; var ordl = [];
			for(p = ordp; p < X.Sz(); p++) {
				o = X.U8(p);
				if(o == 0xFF) { p++; break }
				else if(o == 0xFE) lp = ord;
				else if(o >= 0x60) xpos = o-0x60;
				else {
					if(x == 1 && o%3) { _log('SAAFault @'+Hex(p)+': trk '+x+' pos not divisible by 3'); return false }
					o = Util.div64(o,3);
					if(x == 1 && o > 0x1F) { _log('SAAFault: trk '+x+' pos over 1Fh'); return false }
					if(o > ptn) ptn = o; ord++
				}
				if(ord > 254) { _log('SAAFault: trk '+x+' pos not divisible by 3'); return false }
			}
			if(!ord) return false; ords.push(++ord); ptns.push(++ptn);
			if(sz < p) sz = p;
			p = ptnp+12*ptn; //a pattern record contains uint16_t pattern offsets for 6 channels
			for(i=ptnp; i < p; i += 2) { //find if the a pattern isn't after its record (doesn't seem to ever happen)
				if((t=X.U16(i)) > mp-ofs) mp = ofs+t;
				if(t < 0x1E) return false;
			}
			if(x == 1 && mp > X.Sz()) return false;
			notes = -1;
			if(t > p) { // the hard way
				p = t+64;
				//parse the last pattern
				p = ptnp; notes = 0; chncnt = [64,64,64,64,64,64];
				for(l=0; l < 64; l++) { //lines
					for(c=0; c < 6; c++) { //channels
						if(chncnt[c]) { chncnt[c]--; continue }
						while(p < X.Sz()) { //ParseChannel
							cmd = X.U8(p++);
							if(cmd >= 0xD2) { chncnt[c] -= cmd-0xD2; break }
							else if(cmd >= 0x72) notes++; //set note cmd-0x72
							else if(cmd >= 0x52) {} //set sample cmd-0x52
							else if(cmd >= 0x51) break; //isn't this just if cmd == 0x51?
							//else if(cmd >= 0x50) {} //set ornament cmd-0x30
							//else if(cmd >= 0x2E) {} //set swap sample channels with "cmd > 0x2E"
							//else if(cmd >= 0x21) {} //set envelope cmd-0x21
							//else if(cmd >= 0x11) {} //set attenuation cmd-0x11
							//else if(cmd >= 0x0F) {} //set noise to "cmd != 0xF? 3: cmd-0xF" (so either 3 or 0!)
							//else {} //set tempo cmd+1
						}
					}
				}
			}
			if(sz < p) sz = p;
			if(ornp == mp)
				if(sz < mp) sz = mp //never happens so no further parsing
			if(smpp == mp)
				if(sz < mp) sz = mp; //ditto
			if(svd == mp) if(sz < mp) sz = mp //ditto
		}
		if(X.isVerbose()) {
			if(x > 1) sOption(x,'×');
			sOption('ord:'+ords.join('+')+(lp?' looped':'')+' ptn:'+ptns.join('+')+(xpos?' xpos:'+xpos:'')
				+(notes>-1?' notes:'+notes:'')+' sz:'+outSz(sz))
		}
	}

	else if((X.Sz() > 2048+6+6)
	  && X.c("00000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", 0x3E1)
	  && X.c("......00 ......00 ......00 ......00 ......00 ......00 ......00",0x4D4)
		) {
		sName = "EUPHONY module (.EUP)"; bDetected = 1;
		if(X.isVerbose()) {
			sOption(X.SC(0,0x20,'CP932'))
		}
	}

	else if(X.c("'EuGH'") && X.c("'EuSH'",t=8+X.U32(4,_BE))
	  && X.c("'MThd'",t=t+8+X.U32(t+4,_BE)) && X.c("'MTrk'",p=t+8+X.U32(t+4,_BE))) {
		sName = "Fujitsu EUPHONY II/MTR module (.EUX)"; bDetected = 1;
		//from here on it's just a MIDI file
		trk = X.U16(t+0xA,_BE); //seems it's always 100 in this format
		sz = p; txt = by = title = lyr = "";
		for(i=0; i < trk; i++) {
			var readmore = 20;
			if(!X.c("'MTrk'",p) || (i && !X.c("FF2F00",p-3))) { bad += "!badtrk"+i; break }
			len = X.U32(p+4,_BE); p += 8; sz = p;
			while(readmore && p < sz+len && p < X.Sz()) {
				readmore--; dt = readVarUInt(p);
				p += dt[0];
				switch(X.U8(p++)) {
				case 0xF0: case 0xF7: t = readVarUInt(p); p += t[0]+t[1]; break;
				case 0xFF: a = X.U8(p++);
					switch(a) {
						case 0: if(X.U8(p++) != 2) readmore = 0; else p += 4; break;
						case 3: t = readVarUInt(p); p += t[0];
							title = title.append(X.SC(p,t[1],'SJIS')); p += t[1];
							break;
						case 2: t = readVarUInt(p); p += t[0];
							by = by.append(X.SC(p,t[1],'SJIS')); p += t[1]; break;
						case 1: t = readVarUInt(p); p += t[0];
							txt = txt.append(X.SC(p,t[1],'SJIS')); p += t[1]; break;
						case 5: t = readVarUInt(p); p += t[0];
							lyr = lyr.append(X.SC(p,t[1],'SJIS'),"-"); p += t[1];
							break;
						case 4: case 6: case 7: t = readVarUInt(p); p += t[0]+t[1]; break;
						case 0x20: t = readVarUInt(p); p += t[0]+t[1]; if(t[1] != 1) {
							readmore = 0; bad += "!badprefix@"+(p-t[0]-t[1]) } break
						case 0x2F: t = readVarUInt(p); p += t[0]+t[1]; if(t[1])
							bad += "!badEoTtag@"+(p-t[0]-t[1]); readmore = 0; break;
						case 0x51: t = readVarUInt(p); p += t[0]+t[1]; if(t[1] != 3) {
							readmore = 0; bad += "!badtempo@"+(p-t[0]-t[1]) } break;
						case 0x54: t = readVarUInt(p); p += t[0]+t[1]; if(t[1] != 5) {
							readmore = 0; bad += "!badSMPTE@"+(p-t[0]-t[1]) } break;
						case 0x58: t = readVarUInt(p); p += t[0]+t[1]; if(t[1] < 2 || t[1] > 4) {
							readmore = 0; bad += "!badtime@"+(p-t[0]-t[1]) } break;
						case 0x59: t = readVarUInt(p); p += t[0]+t[1]; if(t[1] != 2) {
							readmore = 0; bad += "!badkey@"+(p-t[0]-t[1]) } break;
						default: t = readVarUInt(p); p += t[0]+t[1]
					} break;
				default:
				}
				if((txt != "" && by != "" && title != "") || p-sz > 0x200) readmore = 0;
			}
			sz += len; p = sz; if(p > X.Sz() && !X.isVerbose()) { bad += "!short"; break }
		} //for..trk
		if(bad) sVersion = sVersion.appendS('malformed'+bad,'/');
		if(X.isVerbose()) {
			sOptionT(title); sOptionT(by,"by: "); sOptionT(txt);
			sOption((trk>1?'trk:'+trk+' ':'')+'sz:'+outSz(sz))
		}
	}

	else if(X.c("'FAR'FE") && X.c("0D0A1A",0x2C) && X.Sz() > X.U16(0x2F)) {
		//ref https://web.archive.org/web/20151027135146/http://hackipedia.org/File%20formats/Music/Sample%20based/text/Format%20Specifications%20(FAR,%20FSM,%20USM,%20F2R)%20by%20Daniel%20Potter.cp437.txt.utf-8.txt
		bDetected = 1; nV = X.U8(0x31);
		sName = "Daniel Potter/Digital Infinity's Farandole Composer module (.FAR)";
		sVersion = "v"+ (nV>>4) +"."+ (nV&0x0F);
		ch = X.readBytes(0x32,16).filter(function(x){return x==1}).length;
		if(X.isVerbose()) {
			sOptionT(X.SC(4,0x28,'CP437'));
			msgsz = X.U16(0x60); msg = [];
			for(i=0,p=0x62; i < msgsz && p < X.Sz(); i+=132,p+=Math.max(0,Math.min(132,msgsz-i)))
				msg.push(X.SC(p,Math.max(0,Math.min(132,msgsz-i)),'CP437').trim());
			sOptionT(addEllipsis(msg.join(' '),0x100),'msg:"','"');
			p = 0x62+msgsz+0x100; ptn = X.U8(p++); ord = X.U8(p++); lp = X.U8(p++);
			for(i=psz=0; i < 0x100; i++,p += 2) psz += X.U16(p);
			p = X.U16(0x2F)+psz;
			smp = 0; smpm = X.readBytes(p,8); smps = []; p += 8;
			for(i=0; i < 64; i++) if(smpm[i >> 3] & (1 << (i & 7))) smp++;
			for(i=0; i < smp; i++) { smps.push(X.SC(p,0x20,'CP437')); p += 0x30+X.U32(p+0x20) }
			sOptionT(addEllipsis(smps.filter(funSampleName).join(' '),0x100),'smp/msg:"','"');
			sOption('ch:'+ch+' ord:'+(lp?lp+'~':'')+ord+' ptn:'+ptn+' smp:'+smp+' sz:'+outSz(p))
		}
	}

	else if(X.c("'FPT'FE") && X.c("0D0A1A",0x24)) {
		//ref https://web.archive.org/web/20151027135146/http://hackipedia.org/File%20formats/Music/Sample%20based/text/Format%20Specifications%20(FAR,%20FSM,%20USM,%20F2R)%20by%20Daniel%20Potter.cp437.txt.utf-8.txt
		sName = "Daniel Potter/Digital Infinity's Farandole Composer pattern (.FPT)"; bDetected = 1;
		if(X.isVerbose())
			sOptionT(X.SC(4,0x20,'CP850'));
	}

	else if(X.c("'FSM'FE") && X.c("0A0D1A",0x24)) {
		//ref https://web.archive.org/web/20151027135146/http://hackipedia.org/File%20formats/Music/Sample%20based/text/Format%20Specifications%20(FAR,%20FSM,%20USM,%20F2R)%20by%20Daniel%20Potter.cp437.txt.utf-8.txt
		sName = "Daniel Potter/Digital Infinity's Farandole Composer sample (.FSM)"; bDetected = 1;
		if(X.isVerbose()) {
			sOptionT(X.SC(4,0x20,'CP850'));
			if(X.U8(0x35)) type = "16bit"; else type = "8bit";
			if(X.U8(0x36)&4) looped = " looped"; else looped = "";
			sVersion = type+looped;
			sOption(outSz(X.U32(0x27)),'sz:');
		}
	}

	else if(X.c("'SMOD'") && X.Sz() > 0x70 && isWithin(X.U32(8,_BE),0x70,X.Sz()) && isWithin(X.U32(0x10,_BE),0x70,X.Sz())
	  && isWithin(X.U32(0x18,_BE),0x70,X.Sz()) && isWithin(X.U32(0x20,_BE),0x70,X.Sz())) {
		//ref https://github.com/tonioni/WinUAE/blob/master/prowizard/rippers/FutureComposer13.c
		// & https://github.com/neumatho/NostalgicPlayer/blob/main/Source/Agents/ModuleConverters/ModuleConverter/Formats/FutureComposer13Format.cs
		sName = "Superions' Future Composer module (.FC,.FC13,.SMOD)"; sVersion = "v1.0~3"; bDetected = 1;
		if(X.isVerbose()) {
			ordsz = X.U32(4,_BE); ordsz += ordsz%2; ord = Util.divu64(ordsz,13); if(!ord) ord = 1;
			ptnp = X.U32(8,_BE); ptnsz = X.U32(0xC,_BE); ptn = ptnsz >> 6;
			smpp = X.U32(0x20,_BE); smpsz = X.U32(0x24,_BE); smp = wf = 0;
			sszs = []; for(p=0x28,i=0; i < 10; i++,p+=6) { if(t=X.U16(p,_BE)<<1) smp++; sszs.push(t) }
			for(i=10; i < 90; i++) sszs.push(X.U8(p++)<<1);
			for(i=89; i >= 10; i--) if(sszs[i]) break; wf = i-9; delete sszs;
			sOption('ord:'+ord+' ptn:'+ptn+' smp:'+smp+' wf:'+wf+' sz:'+outSz(smpp+smpsz))
		}
	}

	else if(X.c("'FC14'") && X.Sz() > 0xC0 && isWithin(X.U32(8,_BE),0xC0,X.Sz()) && isWithin(X.U32(0x10,_BE),0xC0,X.Sz())
	  && isWithin(X.U32(0x20,_BE),0xC0,X.Sz()) && isWithin(X.U32(0x24,_BE),0xC0,X.Sz())) {
		//ref https://github.com/tonioni/WinUAE/blob/master/prowizard/rippers/FutureComposer14.c
		// & https://github.com/neumatho/NostalgicPlayer/blob/main/Source/Agents/Players/FutureComposer/FutureComposerWorker.cs
		sName = "Superions' Future Composer module (.FC,.FC14)"; sVersion = "v1.4"; bDetected = 1;
		if(X.isVerbose()) {
			ordsz = X.U32(4,_BE); ord = Util.divu64(ordsz,13); if(!ord) ord = 1;
			ptnp = X.U32(8,_BE); ptnsz = X.U32(0xC,_BE); ptn = ptnsz >> 6;
			smpp = X.U32(0x20,_BE); wfp = X.U32(0x24,_BE); smp = 10; rsmp = msmp = wf = 0;
			sszs = []; for(p=0x28,i=0; i < 10; i++,p+=6) sszs.push(X.U16(p,_BE)<<1);
			for(sz=0,i=10; i < 90; i++) { sz += t=X.U8(p++)<<1; sszs.push(t) }
			for(i=89; i >= 10; i--) if(sszs[i]) break; wf = i-9; sz += X.U32(0x24,_BE);
			for(p=smpp,i=0; i < 10; i++) {
				rsmp++; if(sszs[i]) {
					if(X.c("'SSMP'")) { mszs = [];
						p += 4; msmp++; smp--; rsmp--; for(j=0; j < 20; j++, p+=20) mszs.push(X.U16(p+4,_BE)<<1);
						for(j=0; j < 20; j++) if(mszs[j]) rsmp++; smp++
					}
				}
			} delete sszs; delete mszs;
			sOption('ord:'+ord+' ptn:'+ptn+' smp:'+rsmp+(smp!=rsmp?'('+smp+')':'')
				+(msmp?' ssmp:'+msmp:'')+' wf:'+wf+' sz:'+outSz(sz))
		}
	}

	else if(X.c("'FMK!'") && X.U8(0x3C) == 0xF4 && isWithin(X.U8(0x3D),1,2)) {
		sName = "Sami Wilenius's FM-Kingtracker module (.FMK)";
		sVersion = [,'v1.00~03','v1.06+'][X.U8(0x3D)]; bDetected = 1;
		if(X.isVerbose()) {
			text = true; for(i=0; i < 0x38; i++) if(0x20 > X.U8(i+4)) { text = false; break }
			if(text) {
				sOptionT(X.SA(0x04,0x1C));
				sOptionT(X.SA(0x20,0x1C),"by: ");
			}
			sOption('ord:'+X.U8(0x4A)+' ptn:'+X.U8(0x4C)+' ins:'+X.U8(0x4B))
		}
	}

	else if(X.c("'FMTracker'....'The FM Tracker!'")) {
		sName = "Davey W. Taylor's FM Tracker module (.FMT)"; bDetected = 1;
		sVersion = "v"+X.U8(9)+"."+X.U8(10);
		if(X.isVerbose())
			sOptionT(X.SA(0x1F,0x20));
	}

	else if(X.c("'JSR_FMT~")) {
		//ref https://github.com/XorJS/FM-Tracker & https://jsr-productions.com/_blogpost3319ddc.html
		// & https://github.com/XorJS/FM-Tracker/blob/main/FM.ASM
		sName = "Jean-Sebastien 'XorJS' Royer's FM-Tracker module (.FMT)"; bDetected = 1
	}

	else if(X.c("'FMTRK'1A")) {
		sName = "Morten Stenshorne/Sagitta Software's FM Tracker module (.FMT)"; bDetected = 1;
		if(X.isVerbose())
			sOptionT(X.SA(0x10,0x20));
	}

	else if(X.c("000003F3") && X.U8(0x14)
	  && X.c("70FF4E75'F.PLAYER'",0x20) && X.I32(0x40,_BE)) {
		sName = "Paul van der Valk's Future Player module (.FP)"; bDetected = 1;
		if(X.isVerbose()) {
			sOptionT(X.SA(X.I32(0x2C,_BE)+0x20,0x100));
			x = 1; p = 0x48;
			do { t = X.U32(p,_BE); if(t) x++; p += 8 } while(t);
			if(x > 1) sOption(x,"×");
			sOptionT(X.SA(X.I32(0x30,_BE)+0x20,0x100),"by:");
			sOptionT(outSz(X.I32(0x38,_BE)+0x20),"sz:")
		}
	}

	else if(X.c("'Fred Editor '0000") && X.U16(0xE,_BE) <= 10) {
	//ref https://github.com/neumatho/NostalgicPlayer/blob/main/Source/Agents/ModuleConverters/ModuleConverter/Formats/FredEditorFinalFormat.cs
		p = 0x10+0x401*(x=X.U16(0xE,_BE));
		spds = []; for(i=0x10; i < 0x10+x; i++) if(spds.indexOf(t=X.U8(i)) < 0 ) spds.push(t); 
		if(p < X.Sz()) for(i=0; i < 128 && p < X.Sz(); i++) { trksz = X.U32(p,_BE); p += trksz+4 }
		if(p < X.Sz()) {
			ins = X.U16(p,_BE); p += 2;
			for(i = syn = un = 0; i < ins && p < X.Sz(); i++) {
				switch(X.U8(p+0x53)) {
				case 0xFF: un++; break;
				default: syn++
				}
				p += 0x60; //name & parameters
			}
		}
		smp = X.U16(p,_BE); syn -= smp; p += 2; for(i=0; i < smp && p < X.Sz(); i++) p += 4+X.U16(p+2,_BE);
		if(X.c("12345678",p)) { bDetected = 1; sz = p+4 }
		if(bDetected) {
			sName = "Fred & Julien Clermonte's Fred Editor module (.FRED,.MOD)"; sVersion = 'project';
			if(X.isVerbose()) {
				if(x > 1) sOption(x,'×');
				sOption('tempos:'+spds.sort().join('-')+' ins:'+(ins-un)+(un?'('+ins+')':'')
					+(smp?' smp:'+smp:'')+(syn?' syn:'+syn:'')+' sz:'+outSz(sz))
			}
		}
	}

	if(!bDetected) if(X.c("'FTMN'03") && X.U8(5) < 64 && X.U16(8,_BE) >= 0x1000 && X.U16(8,_BE) < 0x5000
	  && X.U8(0xA) < 12 && X.U8(0xC) < 64 && !(X.U8(0xD) & 0xFC) && X.U8(0xE) && X.U8(0xE) <= 24
	  && X.U8(0xF) >= 4 && X.U8(0xF) <= 96 && X.U8(0xF) == Util.div64(96,X.U8(0xE))
	  && X.U8(0x50) <= 64 && !X.U8(0x51) && X.Sz() >= 0x52+X.U8(5)*32+X.U8(0x50)*4) {
	  	//ref https://github.com/OpenMPT/openmpt/blob/master/soundlib/Load_ftm.cpp
		sName = "Jörg W.Schmidt/MAXON's Face the Music module (.FTM)"; sVersion = 'v'+X.U8(4); bDetected = 1;
		if(X.isVerbose()) {
			smp = X.U8(5); msr = X.U16(6,_BE); bpm0 = (1766278.163/X.U16(8,_BE)).toFixed(0);
			fl = X.U8(0xD); fx = X.U8(0x50); gvol0 = X.U8(0xC);
			p = 0x52+smp*32; realsmp = 0;
			for(i=0; i < smp; i++) if(X.U8(0x52+i*32)) realsmp++;
			fxln = 0; ticksper = X.U8(0xE); rowsper = X.U8(0xF);
			for(i=0; i < fx; i++) { t = X.U16(p,_BE); fxln += t; p+=4+4*t }
			if(msr) for(i=0; i < 8; i++) p += 6+X.U32(p+2,_BE);
			if(fl&1) for(i=0; i < realsmp; i++) p += 4+2*(X.U16(p,_BE)+X.U16(p+2,_BE));
			sOptionT(decAnsi(0x10,0x20,CPAmiga));
			sOptionT(decAnsi(0x30,0x20,CPAmiga),'by: ');
			sOption('bpm0:'+bpm0+' ptn:'+msr+' smp:'+realsmp+'/'+smp+' fx:'+fx+'/'+fxln
				+' msr:'+ticksper+'/'+rowsper+(!(fl&1)?' ext.smps':'')+' gvol0:'+gvol0+' sz:'+outSz(p))
		}
	}

	else if(X.c("'FMS!'..000000")) {
		sName = "BleuBleu's FamiStudio module (.FMS)"; bDetected = 1;
		sVersion = "v"+X.U8(4)
	}

	else if(X.c("'FamiTracker Module'") && X.fSig(0x12,0x10,"00'PARAMS'") > 0
	  && X.fSig(0x18,0x100,"00'INFO'") > 0) {
		sName = "Jonathan Liss's FamiTracker module (.FTM)"; bDetected = 1;
		if(X.isVerbose()) {
			itag = X.fStr(0x18,0x100,"INFO");
			sOptionT(X.SA(itag+0x18,0x20));
			sOptionT(X.SA(itag+0x38,0x20),"by: ");
			sOptionT(X.SA(itag+0x58,0x20))
		}
	}

	else if(X.c("'-Furnace '")) {
		// from https://github.com/tildearrow/furnace/blob/master/papers/format.md
		switch(X.SA(9,7)) {
			case 'module-':
				cV = 'm'; bDetected = 1; sName = "tildearrow et al.'s Furnace Tracker module (.FUR)"; break;
			case 'instr.-':
				cV = 'i'; bDetected = 1; sName = "tildearrow et al.'s Furnace Tracker instrument (.FUI)"; break;
			case 'waveta-':
				cV = 'w'; bDetected = 1; sName = "tildearrow et al.'s Furnace Tracker wavetable (.FUW)"; break
			default:
				cv = "?"; bDetected = 1; sName = "unknown tildearrow et al.'s Furnace Tracker file"
		}
		nV = X.U16(0x10,_LE);
		//ref https://github.com/tildearrow/furnace/blob/master/papers/format.md
		//they update it every 0.1 femtosecond, why not help out since you're reading this?
		if(nV < 12) sVersion = "["+nV+"]"; else
		if(nV <= 14) sVersion = "v0.2.x"; else
		if(nV <= 16) sVersion = "v0.3.x"; else
		if(nV <= 27) sVersion = "v0.4.x"; else
		if(nV < 35) sVersion = "["+nV+"]"; else
		if(nV <= 54) sVersion = "v0.5.x"; else
		if(nV < 57) sVersion = "["+nV+"]"; else
		if(nV == 75) sVersion = "v.dev75/April Fools' 0.6pre0"; else
		if(nV <= 99) sVersion = "v.dev"+nV; else
		if(nV == 100) sVersion = "v0.6pre1"; else
		if(nV == 101) sVersion = "v0.6pre1 (dev101)"; else
		if(nV == 102) sVersion = "v0.6pre1 (dev102)"; else
		if(nV <= 115) sVersion = "v.dev"+nV; else
		if(nV == 116) sVersion = "v.0.6pre1.5"; else
		if(nV <= 131) sVersion = "v.dev"+nV; else
		if(nV == 132) sVersion = "v.0.6pre2"; else
		if(nV == 133) sVersion = "v.0.6pre3"; else
		if(nV <= 140) sVersion = "v.dev"+nV; else
		if(nV == 141) sVersion = "Tournament Edition"; else
		if(nV == 142) sVersion = "v.dev"+nV; else
		if(nV == 143) sVersion = "v.0.6pre4"; else
		if(nV <= 145) sVersion = "v.dev"+nV; else
		if(nV == 146) sVersion = "v.Pro (joke edition)"; else
		if(nV <= 157) sVersion = "v.dev"+nV; else
		if(nV == 158) sVersion = "v.0.6pre5"; else
		if(nV <= 160) sVersion = "v.dev"+nV; else
		if(nV == 161) sVersion = "v.0.6pre6"; else
		if(nV == 162) sVersion = "v.0.6pre7"; else
		if(nV <= 165) sVersion = "v.dev"+nV; else
		if(nV == 166) sVersion = "v.0.6pre8"; else
		if(nV <= 168) sVersion = "v.dev"+nV; else
		if(nV == 169) sVersion = "v.0.6pre9"; else
		if(nV <= 170) sVersion = "v.dev"+nV; else
		if(nV == 171) sVersion = "v.0.6pre10"; else
		if(nV == 172) sVersion = "v.0.6pre11"; else
		if(nV == 173) sVersion = "v.0.6pre12"; else
		if(nV == 174) sVersion = "v.0.6pre13"; else
		if(nV == 175) sVersion = "v.0.6pre14"; else
		if(nV <= 176) sVersion = "v.dev"+nV; else
		if(nV == 177) sVersion = "v.0.6pre15"; else
		if(nV == 178) sVersion = "v.0.6pre16"; else
		if(nV == 179) sVersion = "v.0.6pre17"; else
		if(nV == 180) sVersion = "v.0.6pre18"; else
		if(nV == 181) sVersion = "v.0.6"; else
		if(nV <= 191) sVersion = "v.dev"+nV; else
		if(nV == 192) sVersion = "v.0.6.1"; else
		if(nV <= 196) sVersion = "v.dev"+nV; else
		if(nV == 197) sVersion = "v.0.6.2"; else
		if(nV <= 200) sVersion = "v.dev"+nV; else
		if(nV == 201) sVersion = "v.0.6.3"; else
		if(nV <= 211) sVersion = "v.dev"+nV; else
		if(nV == 212) sVersion = "v.0.6.4"; else
		if(nV <= 213) sVersion = "v.dev"+nV; else
		if(nV == 214) sVersion = "v.0.6.5"; else
		if(nV <= 217) sVersion = "v.dev"+nV; else
		if(nV == 218) sVersion = "v.0.6.6"; else
		if(nV == 219) sVersion = "v.0.6.7"; else
		if(nV <= 224) sVersion = "v.dev"+nV; else
		if(nV == 225) sVersion = "v.0.6.8pre1"; else
		if(nV == 226) sVersion = "v.0.6.8pre2"; else
		if(nV == 227) sVersion = "v.0.6.8"; else
		if(nV == 228) sVersion = "v.0.6.8.1";
		else sVersion = "["+nV+"]";
		if(cV === 'i') {
			p = X.U32(0x14,_LE);
			if(p>0 && X.c("'INST'",p)) {
				ity = X.U16(0x10,_LE);
				switch(ity) {
					case 0: sVersion += "/std"; break;
					case 1: sVersion += "/FM (OPM/OPN)"; break;
					case 2: sVersion += "/Game Boy"; break;
					case 3: sVersion += "/C64"; break;
					case 4: sVersion += "/Amiga|smp"; break;
					case 5: sVersion += "/PC Engine"; break;
					case 6: sVersion += "/AY-3-8910"; break;
					case 7: sVersion += "/AY8930"; break;
					case 8: sVersion += "/TIA"; break;
					case 9: sVersion += "/SAA1099"; break;
					case 10: sVersion += "/VIC"; break;
					case 11: sVersion += "/PET"; break;
					case 12: sVersion += "/VRC6"; break;
					case 13: sVersion += "/OPLL"; break;
					case 14: sVersion += "/OPL"; break;
					case 15: sVersion += "/FDS"; break;
					case 16: sVersion += "/Vritual Boy"; break;
					case 17: sVersion += "/Namco 163"; break;
					case 18: sVersion += "/SCC"; break;
					case 19: sVersion += "/OPZ"; break;
					case 20: sVersion += "/POKEY"; break;
					case 21: sVersion += "/PC Speaker"; break;
					case 22: sVersion += "/WonderSwan"; break;
					case 23: sVersion += "/Lynx"; break;
					case 24: sVersion += "/VERA"; break;
					case 25: sVersion += "/X1-010"; break;
					case 26: sVersion += "/VRC6(saw)"; break;
					case 27: sVersion += "/ESS5506"; break;
					case 28: sVersion += "/MultiPCM"; break;
					case 29: sVersion += "/SNES"; break;
					case 30: sVersion += "/Sound Unit"; break;
					case 31: sVersion += "/Namco WSG"; break;
					default: sVersion += "/unk"
				}
				if(X.isVerbose()) {
					wvt = X.U16(p+0x18,_LE);
					smp = X.U16(p+0x1A,_LE);
					sOptionT(X.SC(p+0x0C,0x200,"UTF8"));
					sOption(" wvt:"+wvt+" smp:"+smp);
				}
			} //verbose inst
		} //inst
		if(cV === 'm' && X.isVerbose()) {
			p = X.U32(0x14,_LE);
			if(p>0 && X.c("'INFO'",p)) {
				t = X.SC(p+0x100,0x200,"UTF8");
				p1 = X.fSig(p+0x100,0x200,"00")+1;
				a = X.SC(p1,0x200,"UTF8");
				p1 = X.fSig(p1,0x200,"00")+1;
				ins = X.U16(p+0x16,_LE);
				wvt = X.U16(p+0x18,_LE);
				smp = X.U16(p+0x1A,_LE);
				ptng = X.U32(p+0x1C,_LE);
				freq = X.F32(p+0x0C,_LE);
				A4freq = X.F32(p1,_LE);
				s1spd = X.U8(p+9)+"/"+X.U8(p+0x0A);
				s1ptn = X.U16(p+0x10);
				s1ord = X.U16(p+0x12);
				sOptionT(t);
				sOptionT(a,"by: ");
				chips = []; el = false; i = 0;
				while(!el && (i<32)) {
					switch(X.U8(p+0x20+i)) {
					case 0: el = true; break;
					case 0x01: chips[i] = "YMU759 (17ch)"; break;
					case 0x02: chips[i] = "Genesis (10ch comp.)"; break;
					case 0x03: chips[i] = "SN76489 (SMS) (4ch)"; break;
					case 0x04: chips[i] = "Game Boy (4ch)"; break;
					case 0x05: chips[i] = "PC Engine (6ch)"; break;
					case 0x06: chips[i] = "NES (5ch)"; break;
					case 0x07: chips[i] = "C64 (8580) (3ch)"; break;
					case 0x08: chips[i] = "Arcade (YM2151+SegaPCM) (13ch)"; break;
					case 0x09: chips[i] = "Neo Geo CD (YM2610) (13ch)"; break;
					case 0x42: chips[i] = "Genesis extended (13ch)"; break;
					case 0x43: chips[i] = "SN76489 (SMS) + YM2413 (OPLL) (13ch comp.)"; break;
					case 0x46: chips[i] = "NES+VRC7 (11ch)"; break;
					case 0x47: chips[i] = "C64 (6581) (3ch)"; break;
					case 0x49: chips[i] = "Neo Geo CD extended (16ch)"; break;
					case 0x80: chips[i] = "AY-3-8910 (3ch)"; break;
					case 0x81: chips[i] = "Amiga (4ch)"; break;
					case 0x82: chips[i] = "YM2151 (8ch)"; break;
					case 0x83: chips[i] = "YM2612 (6ch)"; break;
					case 0x84: chips[i] = "TIA (2ch)"; break;
					case 0x85: chips[i] = "VIC-20 (4ch)"; break;
					case 0x86: chips[i] = "PET (1ch)"; break;
					case 0x87: chips[i] = "SNES (8ch)"; break;
					case 0x88: chips[i] = "VRC6 (3ch)"; break;
					case 0x89: chips[i] = "YM2413 (OPLL) (9ch)"; break;
					case 0x8A: chips[i] = "FDS (1ch)"; break;
					case 0x8B: chips[i] = "MMC5 (3ch)"; break;
					case 0x8C: chips[i] = "Namco 163 (8ch)"; break;
					case 0x8D: chips[i] = "YM2203 (6ch)"; break;
					case 0x8E: chips[i] = "YM2608 (16ch)"; break;
					case 0x8F: chips[i] = "YM3526 (OPL) (9ch)"; break;
					case 0x90: chips[i] = "YM3812 (OPL2) (9ch)"; break;
					case 0x91: chips[i] = "YMF262 (OPL3) (18ch)"; break;
					case 0x92: chips[i] = "MultiPCM (28ch)"; break;
					case 0x93: chips[i] = "Intel 8253 (beeper) (1ch)"; break;
					case 0x94: chips[i] = "POKEY (4ch)"; break;
					case 0x95: chips[i] = "RF5C68 (8ch)"; break;
					case 0x96: chips[i] = "WonderSwan (4ch)"; break;
					case 0x97: chips[i] = "Philips SAA1099 (6ch)"; break;
					case 0x98: chips[i] = "OPZ (YM2414) (8ch)"; break;
					case 0x99: chips[i] = "Pokémon Mini (1ch)"; break;
					case 0x9A: chips[i] = "AY8930 (3ch)"; break;
					case 0x9B: chips[i] = "SegaPCM (16ch)"; break;
					case 0x9C: chips[i] = "Virtual Boy (6ch)"; break;
					case 0x9D: chips[i] = "VRC7 (6ch)"; break;
					case 0x9E: chips[i] = "YM2610B (16ch)"; break;
					case 0x9F: chips[i] = "ZX Spectrum (beeper) (6ch)"; break;
					case 0xA0: chips[i] = "YM2612 extended (9ch)"; break;
					case 0xA1: chips[i] = "Konami SCC (5ch)"; break;
					case 0xA2: chips[i] = "OPL drums (YM3526) (11ch)"; break;
					case 0xA3: chips[i] = "OPL2 drums (YM3812) (11ch)"; break;
					case 0xA4: chips[i] = "OPL3 drums (YMF262) (20ch)"; break;
					case 0xA5: chips[i] = "Neo Geo (YM2610) (14ch)"; break;
					case 0xA6: chips[i] = "Neo Geo extended (YM2610) (17ch)"; break;
					case 0xA7: chips[i] = "OPLL drums (YM2413) (11ch)"; break;
					case 0xA8: chips[i] = "Atari Lynx (4ch)"; break;
					case 0xA9: chips[i] = "SegaPCM (DefleMask compat.) (5ch)"; break;
					case 0xAA: chips[i] = "MSM6295 (4ch)"; break;
					case 0xAB: chips[i] = "MSM6258 (1ch)"; break;
					case 0xAC: chips[i] = "Commander X16 (VERA) (17ch)"; break;
					case 0xAD: chips[i] = "Bubble System WSG (2ch)"; break;
					case 0xAE: chips[i] = "OPL4 (YMF278B) (42ch)"; break;
					case 0xAF: chips[i] = "OPL4 drums (YMF278B) (44ch)"; break;
					case 0xB0: chips[i] = "Seta/Allumer X1-010 (16ch)"; break;
					case 0xB1: chips[i] = "Ensoniq ES5506 (32ch)"; break;
					case 0xB2: chips[i] = "Yamaha Y8950 (10ch)"; break;
					case 0xB3: chips[i] = "Yamaha Y8950 drums (12ch)"; break;
					case 0xB4: chips[i] = "Konami SCC+ (5ch)"; break;
					case 0xB5: chips[i] = "tildearrow Sound Unit (8ch)"; break;
					case 0xB6: chips[i] = "YM2203 extended (9ch)"; break;
					case 0xB7: chips[i] = "YM2608 extended (19ch)"; break;
					case 0xB8: chips[i] = "YMZ280B (8ch)"; break;
					case 0xB9: chips[i] = "Namco WSG (3ch)"; break;
					case 0xBA: chips[i] = "Namco 15xx (8ch)"; break;
					case 0xBB: chips[i] = "Namco CUS30 (8ch)"; break;
					case 0xBC: chips[i] = "MSM5232 (8ch)"; break;
					case 0xBD: chips[i] = "YM2612 extra features extended (11ch)"; break;
					case 0xBE: chips[i] = "YM2612 extra features (7ch)"; break;
					case 0xBF: chips[i] = "T6W28 (4ch)"; break;
					case 0xC0: chips[i] = "PCM DAC (1ch)"; break;
					case 0xC1: chips[i] = "YM2612 CSM (10ch)"; break;
					case 0xC2: chips[i] = "Neo Geo CSM (YM2610) (18ch)"; break;
					case 0xC3: chips[i] = "YM2203 CSM (10ch)"; break;
					case 0xC4: chips[i] = "YM2608 CSM (20ch)"; break;
					case 0xC5: chips[i] = "YM2610B CSM (20ch)"; break;
					case 0xC6: chips[i] = "K007232 (2ch)"; break;
					case 0xC7: chips[i] = "GA20 (4ch)"; break;
					case 0xC8: chips[i] = "SM8521 (4ch)"; break;
					case 0xC9: chips[i] = "M114S (16ch)"; break;
					case 0xCA: chips[i] = "ZX Spectrum (beeper/QuadTone) (5ch)"; break;
					case 0xCB: chips[i] = "Casio PV-1000 (3ch)"; break;
					case 0xCC: chips[i] = "K053260 (4ch)"; break;
					case 0xCD: chips[i] = "TED (2ch)"; break;
					case 0xCE: chips[i] = "Namco C140 (24ch)"; break;
					case 0xCF: chips[i] = "Namco C219 (16ch)"; break;
					case 0xD0: chips[i] = "Namco C352 (32ch)"; break;
					case 0xD1: chips[i] = "ESFM (18ch)"; break;
					case 0xD2: chips[i] = "Ensoniq ESS503 (32ch)"; break;
					case 0xD4: chips[i] = "PowerNoise (4ch)"; break;
					case 0xD5: chips[i] = "Dave (4ch)"; break;
					case 0xD6: chips[i] = "NDS (16ch)"; break;
					case 0xD7: chips[i] = "GBA (direct) (2ch)"; break;
					case 0xD8: chips[i] = "GBA (MinMod) (16ch)"; break;
					case 0xD9: chips[i] = "Bifurcator (4ch)"; break;
					case 0xDA: chips[i] = "SCSP (32ch)"; break;
					case 0xDB: chips[i] = "YMF271 (OPX) (48ch)"; break;
					case 0xDC: chips[i] = "RF5C400 (32ch)"; break;
					case 0xDD: chips[i] = "YM2612 XGM (9ch)"; break;
					case 0xDE: chips[i] = "YM2610B extended (19ch)"; break;
					case 0xDF: chips[i] = "YM2612 XGM extended (13ch)"; break;
					case 0xE0: chips[i] = "QSound (19ch)"; break;
					case 0xE1: chips[i] = "PS1 (24ch)"; break;
					case 0xE2: chips[i] = "C64 (6581)+PCM (4ch)"; break;
					case 0xE3: chips[i] = "Watara Supervision (4ch)"; break;
					case 0xE5: chips[i] = "µPD1771C-017 (4ch)"; break;
					case 0xF0: chips[i] = "SID2 (3ch)"; break;
					case 0xF1: chips[i] = "5E01 (5ch)"; break;
					case 0xF5: chips[i] = "SID3 (7ch)"; break;
					case 0xFC: chips[i] = "Pong (1ch)"; break;
					case 0xFD: chips[i] = "Dummy System (8ch)"; break;
					case 0xFE: case 0xFF: chips[i] = "reserved for development"; break;
					default: chips[i] = "unk.";
					}
					if(!el) i++;
				}
				sOption("chips: "+chips.join("+")); //sVersion += #chip1#chip2?
				sOption("ticks="+freq.toFixed(2)+"Hz, A4="+A4freq+"Hz");
				sOption("ins:"+ins+" wvt:"+wvt+" smp:"+smp+" glob.ptn:"+ptng);
				sOption("1st song's spd:"+s1spd+" ptn:"+s1ptn+" ord:"+s1ord);
			} //verbose mod
		} //mod
	}
	else if(X.c("'FWMP'00")) {
		sName = "Capcom/ARC Developments' Forgotten Worlds BGM module (.FW)"; bDetected = 1;
	}
	else if(X.c("'FXSM'")) { //same as AY Amadeus!
		sName = "František Fuka's Fuxoft AY Language module (.FXM)"; bDetected = 1;
	}
	else if(X.c("'GBRF'..00") && isWithin(X.U8(4), 1, 0x30) && isWithin(X.U8(6), 0,3) && isWithin(X.U8(7), 1,3)
	  //&& [...Array(0x10).keys()].every(i => [0, 0xFF].includes(X.U8(0x10+i))) //Qt5 compatibility
	  ) {
		sName = "Gameboy Ripped Format chiptune (.GBR)"; bDetected = 1;
		t = X.SA(0x154,0x13);
		if(t != "") if(/^([a-zA-Z0-9_ -]{4,})/.test(t)) sOptions = sOptions.append(t);
	}
	else if(X.Sz() > 0x70 && X.c("'GBS'01") && X.U8(4) && X.U8(0xD) >= 0xA7) {
		bDetected = 1;
		sName = "Gameboy Sound chiptune (.GBS)";
		if(X.isVerbose())
			sOptionT(X.SA(0x10,0x20));
		tc = X.U8(4); if(tc > 1) sOption(tc,"×");
		if(X.isVerbose()) {
			sOptionT(X.SA(0x30,0x20),"by: ");
			sOptionT(X.SA(0x50,0x20));
		}
	}
	else if(X.c("'GDM'FE") && X.c("0D0A1A'GMFS'",0x44) && X.U16(0x74) <= 9
	  && isWithin(ordp=X.U32(0x76), 0x9D,X.Sz()) && isWithin(ptnp=X.U32(0x7B), 0x9D,X.Sz())
	  && isWithin(smptp=X.U32(0x80), 0x9D,X.Sz()) && isWithin(smpp=X.U32(0x84), 0x9D,X.Sz())) {
		//ref https://github.com/OpenMPT/openmpt/blob/master/soundlib/Load_gdm.cpp
		sName = "BWSB General Digital Music module (.GDM)"; bDetected = 1;
		trkr = "";
		if(X.U16(0x4D,_BE)==0) trkr = "2gdm";
		sVersion = "v"+ X.U8(0x4B) +"."+ X.U8(0x4C);
		if(trkr != "") sVersion += "/"+trkr+" v"+X.U8(0x4F)+"."+X.U8(0x50);
		if(X.isVerbose()) {
			ord = X.U8(0x7A)+1; ptn = X.U8(0x7F)+1; smp = X.U8(0x88)+1;
			for(ch=0,i=0x51; i < 0x71; ch+=X.U8(i++) != 0xFF);
			smps = [];
			for(allsmpsz=0,i=0,p=smptp; i++ < smp; p+=0x3E) {
				smps.push(X.SC(p,0x20,'CP437').trim()); allsmpsz += X.U32(p+0x2D)
			}
			switch(Math.max(ordp,ptnp,smptp,smpp)) {
			case ordp: sz = ordp+ord; break;
			case ptnp: for(p=ptnp,i=0; i++ < ptn && p < X.Sz(); p+=X.U16(p)); sz = p; break;
			case smptp: sz = smptp+0x3E*smp; break;
			case smpp: sz = smpp + allsmpsz; break; //the only case I ever saw
			}
			sOptionT(X.SC(0x04,0x20,'CP437'));
			sOptionT(X.SC(0x24,0x20,'CP437'),"by: ");
			sOption(['','ProTracker MOD','Multitracker','Scream Tracker 3','Composer 669 / UNIS 669',
				'Farandole Composer','UltraTracker','Scream Tracker 2','OctaMED',
				'Epic Megagames MASI'][X.U16(0x74)],'orig:');
			sOptionT(X.SC(X.U32(0x89),X.U32(0x8D),'CP437'));
			//Seems absolutely no-one used the message pointers, but samples do have info
			sOptionT(addEllipsis(smps.filter(funSampleName).join('\n'),0xB0),'smp/msg:"\n','"');
			sOption('ch:'+ch+' tmp0:'+X.U8(0x72)+' bpm0:'+X.U8(0x73)+' ord:'+ord+' ptn:'+ptn
				+' smp:'+smp+' sz:'+outSz(sz))
		}
	}
	else if(X.c("'GLUE'B8B3AABA")) {
		//TODO reverse & find calcsize
		sName = "Lars 'GlueMaster' Malmborg's GlueMon module (.GLUE)"; bDetected = 1;
		if(X.isVerbose()) {
			sOptionT(X.SA(8,8));
			ins = X.U8(0x7F);
			ord = X.U8(0x9E); ptn = 0;
			for(i=0; i < ord; i++) { t = X.U8(0x9F+i); if(t != 0xFF && ptn < t) ptn = t }
			ptn++;
			sOption("ord:"+ord+" ptn:"+ptn+" ins:"+ins)
		}
	}
	else if(X.c("'NuFREDGRAY'",0x22)) {
		sName = "Fred Gray's module (.GRAY)"; bDetected = 1;
		if(X.isVerbose()) {
			t = X.SA(0x50,0x100); pn = 0x50+t.length+1;
			a = X.SA(pn,0x100); pn += a.length+1;
			c = X.SA(pn,0x100);
			sOptionT(t); sOptionT(a,"by: "); sOptionT(c)
		}
	}
	else if(X.c("'GTK'") && isWithin(X.U8(3),1,4) && X.U16(0xC4,_BE) <= 255 && isWithin(X.U16(0xC6,_BE),1,256)
	  && isWithin(X.U16(0xC8,_BE),1,32) && (ord = X.U16(0xCA,_BE)) <= 256 && X.U16(0xCC,_BE) <= ord
	  || X.c("'GT2'") && (X.U8(3) > 5 && X.U32(4,_BE) == 0xE4 || X.U32(4,_BE) == 0xEC+2*X.U16(0xEA,_BE))
	  && isWithin(X.U16(0xCA,_BE),1994,9999)) {
		//GTK from http://ldesoras.free.fr/src/gt2/dev_gtk.zip / formats.txt
		//GT2 from mod_gt2.cpp
		//+ input from the author (http://ldesoras.free.fr)
		bDetected = 1;
		sigv = 0; bad = '';
		switch(X.U8(2)) {
			case 0x4B: sName = "Laurent 'Dumbo' de Soras's Graoumf Tracker module (.GTK)"; sVersion = "Amiga "; sigv = 1; break;
			case 0x32: sName = "Laurent 'Dumbo' de Soras's Graoumf Tracker 2 module (.GT2)"; sVersion = "PC " ; sigv = 2;
		}
		v = X.U8(3); sVersion += "v"+v;
		if(sigv == 1  && v < 6) switch(v) {
			case 1: sVersion += "/GT v0.7"; break;
			case 2: sVersion += "/GT v0.726"; break;
			case 3: sVersion += "/GT v0.731"; break;
			//lost in time
		}
		else {
			 if(v == 9) sVersion += "/GT r27"; //TODO maybe track down some other values
		}
		tracker = statln = '';
		if(sigv == 1) { //m68k GTK
			t = X.SC(4,0x20,'ISO8859-1').trim(); d = xc = "";
			c = X.SC(0x24,0xA0,'ISO8859-1').trim(); //the author says he never implemented this
			trk = X.U16(0xC8,_BE); ord = X.U16(0xCA,_BE); smp = 0; smprecs = X.U16(0xC4,_BE);
			rows = X.U16(0xC6,_BE); lp = X.U16(0xCC,_BE); smpinfosz = v <= 2 ? 0x30 : 0x40;
			sszofs = v == 1 ? 32 : 28; if(v >= 3) sszofs += 16; if(v >= 2) sszofs += 4;
			smp = smpsz = 0; p = 0xCE; for(i=0; i < smprecs; i++) {
				if(ssz = X.U32(p+sszofs,_BE)) { smpsz += ssz; smp++ }; p += smpinfosz;
				if(ssz%2) bad = bad.addIfNone('!oddsmpsz') }
			ptn = 0; for(i=0; i < ord; i++) { if((pt=X.U16(p,_BE)) > ptn) ptn = pt; p += 2 } ptn++;
			nn = v == 4 ? 5 : 4;
			songsz = 0x2CE + smprecs*smpinfosz + ptn*rows*trk*nn; //hdr, smpinfo, order, ptns
			sz = songsz+smpsz;
			statln = 'trk:'+trk+' ord:'+ord+' ptn:'+ptn+' smp:'+smp+(lp?' lp:'+lp:'')+' sz:'+outSz(sz)
		}
		else { //x86 GT2
			t = X.SA(8,0x20).trim();
			d = X.U16(0xCA,_BE)+"-"+X.U8(0xC9).padStart(2,'0')+"-"+X.U8(0xC8).padStart(2,'0');
			c = X.SA(0x28,0xA0).trim(); tracker = X.SA(0xCC,0x18);
			pn = songhk = spd = bpm = ord = lp = mptn = ptn = ins = smp = trk = 0; sz = -1; xc = [];
			if(v < 6) { spd = X.U16(0xE4,_BE); bpm = X.U16(0xE6,_BE) }
			while(pn < X.Sz()) {
				hkhd = X.SA(pn,4); hksz = X.U32(pn+4,_BE); pn += 8;
				switch (hkhd) {
				case 'SONG': ord = X.U16(pn,_BE); lp = X.U16(pn+2,_BE);
					mptn = 0; p = pn+4; for(i=0; i<ord; i++,p+=2) mptn = Math.max(mptn,X.U16(p,_BE));
					mptn++; songhk++; break;
				case 'PATD': case 'PAIN': case 'PAFX': case 'PAMI': ptn++;
					trk = Math.max(trk, X.U16(pn+0x16,_BE)); break;
				case 'INST': ins++; break;
				case 'SAMP': case 'SAM1': case 'SAM2': smp++; break;
				case 'XCOM': xcc = X.U16(pn,_BE);
					xc = xc.push(X.SA(pn+2,xcc).trim());
					break;
				case 'TCN2':
					bpm = X.U16(pn+2,_BE)+(X.U16(pn+4,_BE) ? '.'+X.U16(pn+4,_BE) : '');
				case 'ENDC': sz = pn+hksz-8;
					break
				}
				pn += (hksz-8 > 0)? hksz - 8 : 0;
				if(sz >= 0) break; //or one could see a mod with no ENDC. gimmekuh!.gt2 (v==1)? Any modern TCN2 thing?
				if(charStat(next = X.SA(pn,4)).indexOf('allasc') < 0 //check for all 4ccs being ok
				  || next.toUpperCase() != next) break;
			}
			if(!songhk) bad = bad.addIfNone('!noSONG');
			if(sz <= 0) { sz = pn; bad = bad.addIfNone('!noendtag') }
			if(ptn != mptn) ptn = mptn+'/'+ptn;
			statln = statln.appendS((bpm?'bpm0:'+bpm+' ':'')+(spd?'spd0:'+spd+' ':'')+'trk:'+trk
				+' ord:'+ord+(lp?' lp:'+lp:'')+' ptn:'+ptn+' ins:'+ins+' smp:'+smp+' sz:'+outSz(sz),' ')
		}
		if(X.isVerbose()) {
			sOption(t); sOption(d,'on:'); sOption(tracker,'in:'); sOption(c);
			if(xc.length) sOption(addEllipsis(xc.join('\n'),0x100,0xA0),'msg: "','"');
			sOption(statln)
		}
		if(bad != '') sVersion = sVersion.appendS('malformed'+bad,'/')
	}
	else if(X.c("'NuH.DAVIES'",0x22)) {
		sName = "Howie Davies's module (.HD)"; bDetected = 1;
		if(X.isVerbose()) {
			t = X.SA(0x64,0x100); pn = 0x64+t.length+1;
			a = X.SA(pn,0x100); pn += a.length+1;
			c = X.SA(pn,0x100);
			sOptionT(t);
			sOptionT(a,"by: ");
			sOptionT(c);
		}
	}
	else if(X.c("'HESM'00") && isWithin(X.U8(7),0x7D,0xFE) && X.c("FFF8", 8)
	  && isWithin(X.I8(0xA),-8,0x20) && isWithin(X.I8(0xB),-8,0x20) && isWithin(X.I8(0xC),-8,0x20)
	  && isWithin(X.I8(0xD),-8,0x20) && isWithin(X.I8(0xE),-8,0x20) && X.c("'DATA'", 0x10)
	  && [0,0xE0].indexOf(X.U8(0x14)) >= 0 && X.c("002000000000000000", 0x17) && X.Sz() >= 0x100) {
	 	//ref https://github.com/libgme/game-music-emu/blob/master/gme/Hes_Emu.cpp
		sName = "Hudson Entertainment System multitrack tune (.HES)"; bDetected = 1;
		sVersion = "v"+X.U8(4); startsong = X.U8(5); reqaddr = X.U16(6);
		dtsz = X.U32(0x14); dtaddr = X.U32(0x18);
		if(X.isVerbose()) {
			t = a = c = ""; p = 0x40;
			function getText() { if(X.U8(p)) {
				var len = 0x20; if(X.U8(p+0x1F) && !X.U8(p+0x2F)) len = 0x30; //some are longer than t'others
				var s = X.readBytes(p,len), z = s.indexOf(0); if(z < 0) return '';
				if(charStat(s.slice(0,z),1).indexOf('asc') < 0) return '';
				p += len; s = decEncoding(s,CP437,true,Chars0to1F); if(s == '<?>') return ''; else return s
			} else return '' }
			if(X.U8(p) >= 0x20) { sOptionT(getText()); sOptionT(getText(),"by:"); sOptionT(getText()) }
			sOption('from:'+startsong+' sz:'+outSz(dtaddr+dtsz))
		}
	}
	else if(X.c("'GTI5'")) {
		sName = "Lasse 'Faust' Öörni's GoatTracker 2 Instrument (.INS)"; bDetected = 1
	}
	else if(X.c("'ISM!V1.2'")) {
		sName = "Hans Bergstedt's Sound Invasion Music System/In Stereo! module (.IS)"; bDetected = 1;
		sVersion = "v"+X.SA(5,3);
		if(X.isVerbose())
			sOptionT(X.SA(0x24,25))
	}
	else if(X.c("'IS20DF10STBL'")) {
		sName = "Hans Bergstedt's Sound Invasion Music System/In Stereo! module (.IS20)"; bDetected = 1;
		sVersion = "v2.0";
	}
	else if(X.c("'IXS!'")) {
		sName = "Sahara Surfers' iXalance module (.IXS)"; bDetected = 1; sVersion = 'compressed';
		if(X.isVerbose()) {
			sOptionT(X.SA(0x18,0x20)); sOption(outSz(0x38+X.U32(0x10)),'sz:')
		}
		//Unpacked module detection is in the sanity checks
	}
	else if((X.c("'MUSE'DEADBEAF") || X.c("'MUSE'DEADBABE")) && X.U32(16)+24 == X.U32(8)) {
		sName = "Jazz Jackrabbit 2 container (.J2B)/Galaxy Sound System module";
		sVersion = "compressed"; bDetected = 1;
		if(X.isVerbose())
			sOption('sz:'+X.U32(8))
		//TODO: confirm if zlib extraction available; if so, derive the module info too
		//Unpacked module detection is in the sanity checks
	}
	else if(X.c("'NuJ.FLOGEL'",0x22)) {
		sName = "Janko Mrsic-Flogel's module (.JMF)"; bDetected = 1;
		if(X.isVerbose()) {
			t = X.SA(0x54,0x100); pn = 0x54+t.length+1;
			a = X.SA(pn,0x100); pn += a.length+1;
			c = X.SA(pn,0x100);
			sOptionT(t); sOptionT(a,"by: "); sOptionT(c)
		}
	}
	else if( X.c("2B7C.... ........ 2B7C.... ........ 2B7C.... ........ 2B7C.... ........ 303C00FF 32004EB9 ........ 4E75")
	  && X.I32(2,_BE) >= 0x2E) {
		sName = "Steve Turner's module (.JPO)"; bDetected = 1;
		if(X.isVerbose()) {
			//from https://gitlab.com/uade-music-player/uade/-/raw/master/amigasrc/players/wanted_team/SteveTurner/src/Steve%20Turner_v4.asm
			ofs = X.U32(2,_BE);
			p1 = p = X.U32(0x12,_BE)-ofs+0x2E;  d1 = 10;
			while(p <= Math.min(X.Sz(),0x100000)) {
				t = X.U16(p,_BE); if(t == 0xF0FF) break; p += 2;
				d0 = t; if(d0 > d1) d1 = d0;
			}
			sz = X.fSig(p1+d1,TOEOF,"FF"); if(sz > 0) sz++;
			p = X.U32(0xA,_BE)-ofs+0x2E; x = 0; // e4ec~e4fe
			while(p < X.Sz()) { d0 = X.U16(p,_BE)&0xFFF0; if(d0) break; p += 0x0C; x++ }
			if(X.c("2b7c0004449c0faa2b7c000479840fa22b7c00047ab40fa62b7c000458980fae"))
				x = 5; // paradroid90.jpo
			d0 = 0x14+X.U32(0x1A,_BE)-ofs;
			smp = (X.U32(d0,_BE) >> 2)-1;
			smpsz = X.U32(d0+4,_BE);
			if(x > 1) sOption(x,"×"); sOption("smp:"+smp+" smpsz:"+Hex(smpsz)+" sz:"+outSz(sz))
		}
	}
	else if((X.c("'KSCC'") && !X.U8(0xE) && !(X.U8(0xF) & 0xE0)) || //&meant to be 0xF0 but majutsushi*.kss have 0x10 there
	  (X.c("'KSSX'") && !X.U32(0x14))) {
		// ref https://sourceforge.net/p/nezplug/code/HEAD/tree/nezplug/trunk/src/nes/m_kss.c & trunk/in_nez.txt
		sName = "Konami's KSS chiptune (.KSS)"; bDetected = 1;
		if(X.c("'KSSX'")) { nv = 1; sVersion = "extended" } else nv = 0;
		bnk = X.U8(0xD) & 0x7F; bnk16 = X.U8(0xD) & 0x80 ? 1 : 2;
		if(bnk) sVersion = sVersion.appendS('banks'+(bnk16 == 1?'8k':'16k'), ',');
		chip = X.U8(0xF);
		if(X.isVerbose()) if(X.c("'MBM'",0x20)) sOptionT(X.SA(0x24,0x34));
		sz = 0x10+X.U8(0xE)+X.U16(6)+0x2000*bnk*bnk16;
		hnmmode = rammode = false; scc = true; ch = 1;
		if(chip & 2) {
			sVersion = sVersion+'#SEGA-MkIII(SMS)';
			if(chip & 4) { sVersion = sVersion+'/GameGear-Stereo'; ch = 2 }
			else sVersion = sVersion+'/SMS-SNG(SN76489)';
			if(chip & 1) sVersion = sVersion+'/FM-UNIT(YM2413)';
			if(chip & 0x88) rammode = true;
			scc = false
		} else {
			if(chip & 0x10) {
				if(chip & 8) { sVersion = sVersion+'#MSX-AUDIO-STEREO'; ch = 2 }
				else { sVersion = sVersion+'#MSX-AUDIO'; hnmmode = true }
			} else sVersion = sVersion+'#MSX-AUDIO';
			if(chip & 1) sVersion = sVersion+'/MSX-MUSIC';
 			if(chip & 0x80) { rammode = true; scc = false }
			else {
				rammode = (chip & 4) != 0; scc = !rammode
			}
		} 
		if(rammode) sVersion = sVersion+'/RAM'; if(scc) sVersion = sVersion+'/SCC';
		if(hnmmode) sVersion = sVersion+'/牌の魔術師DAC';
		if(nv) if(X.U8(0xE) >= 0xB) if(X.isVerbose()) {
			x1 = X.U16(0x18); x2 = X.U16(0x1A);
			if(x2-x1 > 1) sOption('subsongs '+x1+'-'+x2);
		}
		if(X.isVerbose()) sOption('ch:'+ch+(bnk?' ex.bnk:'+bnk:'')+' sz:'+outSz(sz))
	}
	else if(X.c("'cyd!song'") && X.U8(8) < 30) { //v27 was the last I saw, give it time...
		//ref https://github.com/kometbomb/klystron/tree/master/src/snd/music.c , .h : mus_load_song_RW
		sName = "Tero 'kometbomb' Lindeman's Klystrack/Klystron module (.KT)"; bDetected = 1;
		v = X.U8(8); sVersion = "v"+v;
		if(X.isVerbose()) {
			p = 9;
			if(v >= 6) ch = X.U8(p++); else if(v > 3) ch = 4; else ch = 3;
			timesig = X.U8(p++)+"/"+X.U8(p++);
			if(v >= 17) p += 2; //seqstep
			ins = X.U8(p++);  ptn = X.U16(p); p += 2;
			nseq = []; for(i=0; i < ch; i++,p+=2) nseq[i] = X.U16(p);  //seq cnt per channel
			len = X.U16(p); p += 2; /*lp = X.U16(p);*/ p += 2;
			if(v >= 12) p++; //master_volume
			spd = X.U8(p++); spd2 = X.U8(p++);
			rate = X.U8(p++); //bpm?...
			if(v > 2) { fl = X.U32(p); p += 4 } else fl = 0; if(v >= 9) p++; if(v >= 16) p++; //flags, mulpd, ptcin
			tlen = 17; if(v >= 11) tlen = X.U8(p++);
			if(v >= 5) tlen = Math.min(tlen,65);
			sOptionT(X.SC(p,tlen,'CP1250')); p += tlen;
			if(v >= 10) fx = X.U8(p++); else if(fl&1) fx = 1;
			if(fx)
				if(v >= 10) for(i=0; i < fx; i++) { //inner_load_fx
					if(v >= 22) { nsz = X.U8(p++); /*_l2r('kt',p,'fx#'+i+': '+X.SA(p,nsz));*/ if(nsz) p += Math.min(nsz,32) }
					p += 9; if(v < 27) p++; if(v < 21) p++;
					if(v >= 27) p += 6*16; else p += 4*8;//taps
					p++; if(v >= 19) p++;
				} else p += 8*8*fx;
			if(v >= 13) p += ch*2;//dfltvol,dfltpan
			insn = [];
			for(i=0; i < ins; i++) {
				p += 0x11; p += 2*X.U8(p)+1; //progsteps
				p += 7; if(v >= 20) p++;//finetune
				nsz = (v < 11)? 16: Math.min(X.U8(p++),33);
				insn.push(X.SC(p,nsz,'CP1250').trim()); p += nsz;
				if(v >= 1) p += 4;
				if(v >= 7) p += 3;
				if(v >= 10) p++; //fx_bus
				if(v >= 11) p += 3;
				if(v >= 18) p++;
				if(v >= 12) wt_entry = X.U8(p++); else wt_entry = 0;
				if(v >= 23) p += 11;
				if(v >= 25) p++;
				if(v >= 23) fm_wave = X.U8(p++); else fm_wave = 0;
			}
			ord = 0; //????
			for(i=0; i < ch; i++) { ord = Math.max(nseq[i],ord); if(v < 8) p += 6*nseq[i]; else p += 5*nseq[i] }
			notes = 0;
			for(i=0; i < ptn; i++) {
				steps = X.U16(p); p += 2;
				if(v >= 24) p++; //colour
				if(v < 8) {
					s = v < 2? 3: 6; p += steps*s;
				} else {
					slen = (steps >> 1)+(steps&1); packed = X.readBytes(p,slen); p += slen;
					for(s=c=0; s < steps; s++) {
						bits = ((s&1) || (s == steps-1))? packed[c] & 0xF : packed[c] >> 4;
						if(bits&1) { p++; notes++ } if(bits&2) p++;
						if(bits&4) { ctl = X.U8(p++); if(v >= 14) bits |= ctl & ~7 }
						if(bits&8) p += 2; if(bits&0x80) p++;
						if(s&1) c++
					}
				}
			}
			maxwt = 0;
			if(v >= 12) {
				maxwt = X.U8(p++);
				for(i=0; i < maxwt; i++) {
					sn = X.U32(p+8);
					p += 0x16;
					if(sn) if(v < 15) p += sn*2;
					else { dtsz = X.U32(p)/*in bits*/; p += 4+Util.divu64(dtsz+7,8) }
				}
			}
			wts = [];
			if(v >= 26) for(i=0; i < maxwt; i++) {
				t = X.U8(p++); wts.push(X.SC(p,t,'CP1250').trim()); p += t
			}
			sOptionT(addEllipsis(insn.filter(funSampleName).join(',')),'ins/msg:"','"');
//			sOptionT(addEllipsis(wts.filter(funSampleName).join(',')),'smp/msg:"','"');
			sOption('ch:'+ch+' tsig:'+timesig+' rate:'+rate+' ord:'+ord+' ptn:'+ptn+' ins:'+ins+' wt:'+maxwt
				+' notes:'+notes+' spd:'+spd+'-'+spd2+' fx:'+fx+' sz:'+outSz(p))
		}
	}
	else if(X.c("'cyd!inst'") ) {
		sName = "Tero 'kometbomb' Lindeman's Klystrack instrument (.KI)"; bDetected = 1
	}
	else if(X.c("'cyd!efex'") ) {
		sName = "Tero 'kometbomb' Lindeman's Klystrack effects (.KF?)"; bDetected = 1
	}
	else if( X.c("'ADL '0000....'MDhd'00000008000080..00000080'MThd'00000006000200..01E0'MTrk'0000....00FF03")
	  || ( X.U32(0,_LE)==X.Sz() && X.c("'AD'",4) ) ) {
		sName = "Lucas Arts Adlib chiptune (.LAA)"; bDetected = 1;
		if(X.SA(0,4)==="ADL ") sVersion = "new"; else sVersion = "old"
	}
	else if( X.c("'Liquid Module:'") ||
	  ( (X.isHeuristicScan() || X.c("'NO'"))
		 && extIs("liq"))
	  ||  X.c("21",0x01D6) && X.c("21",0x0AD2) && X.c("21",0x0BE6)
	  && X.c("FF",0x0ED4) && X.c("FF",0x0EE8) ) {
		sName = "Nir Oren's Liquid Tracker module (.LIQ)"; bDetected = 1;
		sVersion = X.SA(0x41,0x14).trim();
		if(X.isVerbose()) {
			if(X.SA(0,2) == "NO")
				sOptionT(X.SA(5,0x14))
			else if(X.SA(0,3) == "Liq") {
				sOptionT(X.SA(0x0E,0x32));
				sOptionT(X.SA(0x0E,0x0F),"by: ");
			}
		}
	}
	else if(X.Sz() > 0x38 && X.c("'LME'00") && X.fSig(4,0x20,"00") < 0 && !X.U32(0x24,_BE)) {
		//ref https://gitlab.com/uade-music-player/uade/-/raw/master/amigasrc/players/wanted_team/LME/src/LMEv3.asm
		sName = "Steve 'Leggless' Hasler module (.LME)"; bDetected = 1;
		if(X.isVerbose()) {
			info = decAnsi(4,0x20,CPAmiga).trim(); songsz = 0x2C+X.U32(0x34,_BE);
			d3 = ins = Util.divu64(X.U32(0x38,_BE) - (i=X.U32(0x28,_BE)), 58);
			x = (i-0x10) >> 4; i += 0x28; smp = synsmp = smpsz = 0;
			for(d7 = 3; d3; d3--, i += 0x3A)
				if(!(t=X.U32(i,_BE))) synsmp++
				else if(t > d7) { d7 = t; smpsz += X.U16(i+4,_BE) << 1; smp++ }
			steps = (X.U32(0x30,_BE) - X.U32(0x2C,_BE)) >> 2; sz = songsz+smpsz;
			if(x > 1) sOption(x,'×');  sOption(info,'info:"','"');
			sOption('steps:'+steps+' ins:'+ins+' sz:'+outSz(sz))
		}
	}
	else if(X.c("'sa-team 89a'10610A6108610678006000",0x16C)
	  && X.c("'dynamite89'",0x234) && isWithin(X.U16(0x50A,_BE), 0,15)) {
		sName = "Oscar Giesen & Marco Swagerman's Music Assembler module (.MA)"; bDetected = 1; if(X.isVerbose()) {
//ref https://web.archive.org/web/20071010162252/http://www.exotica.org.uk/tunes/formats/ma/ma_v0.zip / ma_fmt.txt
			x = ptn = 0; vp = []; ptns = []; ord = []; ch = [];
			for(p=0x4B0,i=0; i < 40; i++,p+=2) vp[i] = 0x622+X.U16(p,_BE);
			for(i=0; i < 40; i+=4) { ch_ = ord_ = 0;
				for(j=i; j < i+4 && j < 40; j++) if(X.U8(vp[j]) != 0xFE) ch_++;
				if(ch_) {
					for(j=i,p=vp[j]; j < i+4 && p < X.Sz(); p+=2)
						if((t=X.U8(p)) == 0xFE) break; else if(t == 0xFF) j++;
						else { ord_++; if(ptns.indexOf(t) < 0) ptns.push(t) }
					if(ord_) { x++; ord.push(ord_) }
					ch.push(ch_)
				}
			}
			if(x > 1) sOption(x,'×');
			insp = 0x622+X.U32(0x5B6,_BE); ins = Util.divu64(vp[0]-insp,0x18);
			sOption('ch:'+ch.join('/')+' ord:'+ord.join('+')+' ptn:'+ptns.length+' ins:'+ins+' sz:'+outSz(0x622+X.U32(0x5AE,_BE)))
		}
	}
	else if(X.c("'MADG'") ) {
		sName = "B. Birney's PlayerPro module (.MAD)"; bDetected = 1;
		if(X.isVerbose())
			sOption(X.SA(4,0x12))
	}
	else if(X.c("D040D0404EFB")) {
		sName = "Mark Cooksey's module (.MC)"; sVersion = "new"; bDetected = 1
	}
	else if(X.c("'MDC'1A 00080040")) {
		//from https://github.com/ValleyBell/MidiConverters/blob/master/mdc2mid.c
		sName = "かるちゃん/CUL.'s music creative driver module (.MDC)"; bDetected = 1;
		info = ''; maxsz = Math.max(X.Sz(),65536);
		sz = X.U32(8,_BE); p = X.U32(0x14,_BE); midires = X.U16(0x2C,_BE);
		if(p && p < maxsz && (t=X.fSig(p,maxsz,'0D0A1A')) > 0) info = X.SC(p,t-p,'Shift_JIS');
		p = trkp = X.U32(0x10,_BE); x = X.U16(p,_BE); if(x > 0x20) sVersion = '!badsongcnt';
		for(i=ch=0,p+=2; i < Math.min(x,0x20); i++,p+=8) if(ch < X.U8(p+5)) ch = X.U8(p+5)
		if(X.isVerbose()) {
			sOptionT(info); if(x > 1) sOption(x,'×'); sOption('ch:'+ch+(sz?' sz:'+outSz(sz):''));
		}
	}
	else if(X.c("'DMDL'..'IN'")) {
		sName = "Digitrakker module (.MDL)"; bDetected = 1;
		if(X.isVerbose()) {
			sOptionT(X.SA(0x0B,32));
			sOptionT(X.SA(0x2B,20),"by: ");
		}
	}
	else if(X.c("'MMDC'")) { //before the more generic compare("'MMD'")
		sName = "Tony Crowther's packed MED module (.MMDC)"; bDetected = 1;
		if(X.isVerbose()) sOption(outSz(X.U32(4,_BE)),"sz:")
	}
	else if(X.c("000001001100010014000000'GameBoy Music Module'")) {
		sName = "Paragon 5/Beyond Game Boy Tracker module (.MGB)"; bDetected = 1
		//it's proprietary and seemingly packed in some manner, not easy to extract data. TODO?
	}
	else if(X.c("'MGT'11BD'MCS'")) {
		sName = "Megatracker module (.MGT)"; bDetected = 1;
		sOptionT(X.SA(0x3A,0x20))
	}
	else if(X.c("'MThd'") && X.c("'MTrk'",8+X.U32(4,_BE)) && X.U16(8,_BE) <= 2) {
		//ref. spec. from midi.org: RP-001_v1-0_Standard_MIDI_Files_Specification_96-1-4.pdf
		sName = "Standard MIDI File (.MID)"; sVersion = "v1.0";
		nV = X.U16(8,_BE); bad = ""; bDetected = 1;
		switch(nV) {
		case 0: sVersion += " t.0:one track"; break;
		case 1: sVersion += " t.1:tracks"; break;
		case 2: sVersion += " t.2:tracks+tempo"
		}
		bDetected = 1;
		charset = 'SJIS'; //'CP1252'
		trk = X.U16(0x0A,_BE); if(trk > 1) sOption(trk,"trk:");
		if(nV == 0 && trk != 1) bad += "!badvertrk";
		sz = p = 0x0E; txt = by = title = lyr = "";
		for(i=0; i < trk; i++) {
			var readmore = 20; //mostly enough to catch the info messages; set to 0xFFFFFFFF for all of 'em
			if(!X.c("'MTrk'",p) || (i && !X.c("FF2F00",p-3))) { bad += "!badtrk"; break }
			len = X.U32(p+4,_BE); p += 8; sz = p;
			while(readmore && p < sz+len && p < X.Sz()) {
				readmore--; dt = readVarUInt(p);
//logpart1="@"+Hex(p)+" on "+dt+": ";
				p += dt[0];
				switch(X.U8(p++)) {
				case 0xF0: case 0xF7: t = readVarUInt(p); p += t[0]+t[1]; break;
				case 0xFF: a = X.U8(p++);
//_log(logpart1+"["+readVarUInt(p)+"] meta event "+Hex(a));
					switch(a) {
						case 0: if(X.U8(p++) != 2) readmore = 0; else p += 4; break;
						case 3: t = readVarUInt(p); p += t[0];
							title = title.append(X.SC(p,t[1],charset).trim()); p += t[1];
							break;
						case 2: t = readVarUInt(p); p += t[0];
							by = by.append(X.SC(p,t[1],charset).trim()); p += t[1]; break;
						case 1: t = readVarUInt(p); p += t[0];
							txt = txt.append(X.SC(p,t[1],charset).trim()); p += t[1]; break;
						case 5: t = readVarUInt(p); p += t[0];
							lyr = lyr.append(X.SC(p,t[1],charset,"-").trim()); p += t[1];
							break;
						case 4: case 6: case 7: t = readVarUInt(p); p += t[0]+t[1]; break; //ins., lyrics, markers, cues
						case 0x20: t = readVarUInt(p); p += t[0]+t[1]; if(t[1] != 1) {
							readmore = 0; bad += "!badprefix@"+(p-t[0]-t[1]) } break
						case 0x2F: t = readVarUInt(p); p += t[0]+t[1]; if(t[1])
							bad += "!badEoTtag@"+(p-t[0]-t[1]); readmore = 0; break;
						case 0x51: t = readVarUInt(p); p += t[0]+t[1]; if(t[1] != 3) {
							readmore = 0; bad += "!badtempo@"+(p-t[0]-t[1]) } break;
						case 0x54: t = readVarUInt(p); p += t[0]+t[1]; if(t[1] != 5) {
							readmore = 0; bad += "!badSMPTE@"+(p-t[0]-t[1]) } break;
						case 0x58: t = readVarUInt(p); p += t[0]+t[1]; if(t[1] < 2 || t[1] > 4) {
							readmore = 0; bad += "!badtime@"+(p-t[0]-t[1]) } break;
						case 0x59: t = readVarUInt(p); p += t[0]+t[1]; if(t[1] != 2) {
							readmore = 0; bad += "!badkey@"+(p-t[0]-t[1]) } break;
						default: t = readVarUInt(p); p += t[0]+t[1]
					} break;
				default: //p += readVarUInt(p)[0];
//_log(logpart1+"default...");
				}
				if((txt != "" && by != "" && title != "") || p-sz > 0x200) readmore = 0;
			}
			sz += len; p = sz; if(p>X.Sz()) { bad += "!short"; break }
		} //for..trk
		if(bad) sVersion = sVersion.appendS('malformed'+bad,'/');
		if(X.isVerbose()) {
			sOptionT(title); sOptionT(by,"by: "); sOptionT(txt);
			sOption(outSz(sz),"sz:")
		}
	} else if(X.c("'SMF2CLIP'")) {
		//ref. spec. from midi.org: M2-104-UM, MA06
		sName = "MIDI Clip File (.midi2)"; sVersion = "v2.0"; bDetected = 1;
	}
	else if(X.c("'MKJamz'")) {
		sName = "MK-Jamz module (.MKJ)"; bDetected = 1
	}
	else if(X.c("'MLEDMODL'000000")
	  && X.c("'VERS'",X.U32(8,_BE)+12)) {
		sName = "Musicline Editor module (.ML)"; bDetected = 1;
		p = 4; //header skipped
		t = ""; c = ""; x = 0; ch = smp = ins = ptn = 0;
		while (p < X.Sz()) {
			hkhd = X.SA(p,4); hksz = X.U32(p+4,_BE);
//_log(hkhd+': ['+Hex(p)+'..'+Hex(p+hksz)+']')
			if(charStat(hkhd).indexOf('allasc') < 0) break; //break off if it doesn't look like a 4cc
			p += 8;
			switch (hkhd) {
			case "VERS":
				v = X.U16(p,_BE); //skips MODL and VERS 4cc+4sz
				sVersion = "v"+(v>>8)+"."+((v&0xF0)>>4)+(v&0x0F);
				break;
			case "TUNE":
				x ++; t = t.appendS(X.SC(p,hksz,'CP1252').trim(),'; ');
				tmp0 = X.U16(p+0x20,_BE); spd0 = X.U8(p+0x22,_BE);
				groove = X.U8(p+0x23,_BE); ch = X.U8(p+0x27);
				//v0.79 through v1.06 = hksz broken, v1.10+ good, no samples of 1.07~1.09 :( We'll calc
				//if(v <= 0x106) {
				chsz = 0; hksz = 0x28; //we'll recalc the chunk size instead
				for(i=0; i < ch; i++) { chsz += X.U32(p+hksz,_BE); hksz += 4 }
				hksz += chsz; //fetch chdata sizes, skip data
				//}
				break;
			case "INFO":
				q = 0;
				while(q < hksz) {
					z = X.fSig(p+q,hksz-q,"00");
					if(z > -1) l = z-p-q;
					else l = hksz;
					c += X.SC(p+q,l,'CP1252')+"\n";
					q += l+1;
				} break;
			case "PART": ptn++; break
			case "INST": ins++; break
			case "SMPL":
				smp++; p += 6; break;
			}
			p += hksz;
		}
		if(X.isVerbose()) {
			if(x > 1) sOption(x,'×');
			sOptionT(t); sOptionT(addEllipsis(c,0xA0,0x80),'msg: "','"');
			sOption('ch:'+ch+' ptn:'+ptn+' ins:'+ins+' smp:'+smp+' sz:'+outSz(p));
		}
	}
	else if(X.c("'FORM'........'MMV8SDAT'................'SE'")) {
		sName = "Thomas Winischhofer's Music Maker EXT module (.MM8,.MM4)"; bDetected = 1;
		sVersion = "v8";
		if(X.isVerbose()) {
			sOptionT(X.SA(0x1A,0x14));
			sOptionT(X.SA(0x36,0x29));
			sOption(outSz(X.I32(4,_BE)+8),"sz:")
		}
	}
	//TODO insert the SDATA format of Music Maker
	else if(X.c("'SEI1XX'00")) { //TODO find more than 2 files to test on :)
		sName = "Music Maker STD instrument (.IP)"; bDetected = 1;
		sVersion = "v8 old";
	}
	else if(X.c("'MO3'")) {
		sName = "MO3 MOD module (.MO3)"; bDetected = 1
	}
	else if(X.c("'RASP'",0x438)) {
		// TODO find out what format it was...
		sName = "Generic module (.MOD)"; bDetected = 1;
		sVersion = X.SA(0x438,4);
		if(X.isVerbose()) {
			sOptionT(X.SC(0,0x14,"IBM850"));
			sOptionT(X.SC(0x14,0x16,"IBM850"),"by/inst: ")
		}
	}
	else if(X.c("08'MONOTONE'") && X.Sz() >= 0x15F) {
		//ref https://github.com/MobyGamer/MONOTONE/blob/master/MTSRC/MT_SONG.PAS
		sName = "MONOTONE module (.MON)"; bDetected = 1;
		sVersion = "v"+X.U8(0x5B);
		if(X.isVerbose()) {
			sOptionT(X.UCSD(0x09));
			sOptionT(X.UCSD(0x32));
			ptn = X.U8(0x5C); trk = X.U8(0x5D); cellsz = X.U8(0x5E);
			ord = 0; i = 0x5F; do { t = X.U8(i++); if(t != 0xFF) ord++ } while(i < 0x15F && i != 0xFF)
			sOption('trk:'+trk+' ord:'+ord+' ptn:'+ptn+' sz:'+outSz(0x15F+0x40*ptn*trk*cellsz));
		}
	}
	else if(/SONG[0-9.]{4}(COMP|NORM)/.test(X.SA(0,12))) {
		sName = "Megastation track (.MS)"; bDetected = 1;
		sVersion = "v"+X.SA(4,5).toLowerCase()
	}
	else if(/SNGM[0-9.]{4}(COMP|NORM)/.test(X.SA(0,12))) {
		sName = "Megastation MOD module (.MSM)"; bDetected = 1;
		sVersion = "v"+X.SA(4,5).toLowerCase()
	}
	else if(X.c("'MSOB'00000026") && X.c("FFFF0000",0x24) && (x=X.U16(0x28,_BE)) > 0) {
		sName = "Medley module (.MSO)"; bDetected = 1;
		if(X.isVerbose())
			if(x > 1) sOption(x,'×')
	}
	else if(X.c("'MTC1'00") && X.U8(5) < 0x10 && X.Sz() >= (sz=X.U32(4,_BE)+8)) {
		sName = "Multi-track Container module (.MTC)"; bDetected = 1;
		if(X.isVerbose()) {
			name = auth = c = ''; p = 8; x = 0; //ignoring PROP--it's never useful
			while(p < sz) {
				hkhd = X.SA(p,4); hksz = X.U32(p+4,_BE); p += 8;
				switch(hkhd) {
				case "NAME": name = name.addIfNone(decAnsi(p,hksz,CPSpeccy),'/'); break;
				case "AUTH": auth = auth.addIfNone(decAnsi(p,hksz,CPSpeccy),'/'); break;
				case "ANNO": c = c.addIfNone(decAnsi(p,hksz,CPSpeccy),'/'); break;
				case "TRCK": x++; break
				}
				p += hksz; if(p%2) p++
			}
			sOptionT(name); sOptionT(auth,'by:'); sOptionT(addEllipsis(c,0xA0));
			sOption('trk:'+x+' sz:'+outSz(sz))
		}
	}
	else if(X.c("'mpu401tr'92'kk'EE'r'")) {
		sName = "MPU-401 Trakker Adlib module (.MTK)"; bDetected = 1;
		if(X.isVerbose()) {
			t = X.UCSD(0x18); sOptionT(t);
			sOptionT(X.SA(0x18+t.length+2),"by: ")
		}
	}
	else if(X.c("'MTM'")) {
		//ref https://github.com/libxmp/libxmp/blob/master/docs/formats/Mtm-form.txt
		sName = "StarScream/Renaissance's Multitracker module (.MTM)"; bDetected = 1;
		v=X.U8(3); sVersion = "v"+(v>>4)+"."+(v & 0x0F);
		if(X.isVerbose()) {
			sOptionT(X.SA(4,20));
			trk = X.U16(24); ptn = X.U8(26);
			ord = X.U8(27)+1; csz = X.U16(28);
			nos = X.U8(30); bpt = X.U8(32); smpsz = 0;
			for(i=66+22; i < 66+22+nos*37; i += 37) smpsz += X.U32(i);
			pxc = 194+nos*37+trk*192+(ptn+1)*32*2;
			c = X.SA(pxc,csz); sz = pxc+csz+smpsz;
			if(c.length < csz) c = c.trim()+' <...>';
			sOptionT(c);
			sOption('trk:'+(trk+1)+' ord:'+(ord+1)+' ptn:'+(ptn+1)+' smp:'+nos+' sz:'+outSz(sz))
		}
	}

	else if(X.c("'MT20'") && X.Sz() >= 0x184 && X.U8(9) == 2
	  && X.U16(0x70) < 64 && X.U16(0x6A) <= 0x100) {
		//ref https://github.com/OpenMPT/openmpt/blob/master/soundlib/Load_mt2.cpp
		sName = "MadTracker 2 module (.MT2)"; nV = X.U8(8); bDetected = 1;
		sVersion = "v"+X.U8(9)+"."+nV.toString(16).padStart(2,'0');
		bad = "";
		ord = X.U16(0x6A); loop = X.U16(0x6C);
		ptn = X.U16(0x6E); trk = X.U16(0x70);
		flags = X.U32(0x76);
		ins = X.U16(0x7A); smp = X.U16(0x7C);
		hasdrums = X.U16(0x17E) != 0; //:477
		if(hasdrums) dptn = X.U16(0x180); else dptn = 0; // this one line's :789
		p = 0x184+(hasdrums?0x112:0); //180 = filehdr+hasdrums+orders
		addp = p+X.U32(p-4); msg = ""; vst2 = 0; igskip = 0;
//_log("addp @"+Hex(addp));
		smpszs = []; insszs = []; inss = []; smps = []; extsmp = [];
		while(p < addp) { //:578
			hkhd = X.SA(p,4); hksz = X.U32(p+4); p += 8;
			switch(hkhd) {
				case "MSG": showmsg = X.U8(p);
					msg = X.SC(p+1,hksz-1,'CP1252')
					  .replace("\r","\n").replace("\n\n","\n"); break;
				case "SUM": artist = X.SC(p+6,hksz-6,'CP1252');
					if(artist == "Unregistered") artist == ""; break;
				case "VST2": vst2 = X.U32(p); break;
			}
			p += hksz
		}
//_log("ptns @"+Hex(p));
		if(p > addp) bad = bad.addIfNone("!badaddsz");
		else if(p > X.Sz()) bad = bad.addIfNone("!short");
		else {
			for(i=0; i < ptn && p < X.Sz(); i++) p += 6+((X.U32(p+2)+1)&~1)
//_log("drums? @"+Hex(p));
			if(hasdrums) { //:786
				for(i=0; i < dptn && p < X.Sz(); i++) { //:811
					p += 2+X.U16(p)*32 }
			}
//_log("atmt @"+Hex(p));
			if(flags & 2) { //automation; 8 is drum autn., 0x10 is master autn.
				env = trk + (flags & 8) + ((nV >= 0x50) ? vst2 : 0) + ((flags & 0x10) ? 1 : 0);
				for(q = 0; q < ptn; q++)
					for(var e = 0; e < env && p+4 <= X.Sz(); e++) {
						if(nV >= 3) { fl = X.U32(p); p += 8 }
						else { fl = X.U16(p); p += 4 }
						while(fl) {
							if(fl&1) p += 0x104; fl >>= 1;
							if(fl<0)fl=-fl //fixes something suspiciously like a JS glitch
						}
					}
			}
		}
//_log("ins @"+Hex(p));
		if(p > X.Sz()) bad = bad.addIfNone("!short");
		else {
			for(i=0; i < 255; i++) {
				if(!msg && inss.length < 3) {
					t = X.SC(p,0x20,'CP1252'); if(t.trim() != "") inss.push(t) }
				dtlen = X.U32(p+0x20); if(dtlen == 0x20) dtlen += 0x18C;
				if(nV > 1 && dtlen) dtlen += 4;
				if(dtlen) igskip += X.U16(p+0x24)<<3; p += 0x24+dtlen;
			}
		}
//_log("smp @"+Hex(p));
		if(p > X.Sz()) bad = bad.addIfNone("!short");
		else {
			for(i=0; i < 256; i++) {
				if(!msg) {
					t = X.SA(p,0x20); if(i < smp && t.trim() != "") smps.push(t) }
				dtlen = X.U32(p+32); p += 0x24;
				if(dtlen && i < smp) {
					slen = X.U32(p); sfl = X.U8(p+10);
					if(sfl&5) smpszs.push({"ext":1,"slen":0}); else if(slen) smpszs.push({"ext":0,"slen":slen});
					p += dtlen;
				}
			}
			p += igskip;
			if(p > X.Sz()) bad = bad.addIfNone("!short");
			else for(i=0; i < smp && p < X.Sz(); i++) {
				if(smpszs[i]["ext"]) {
					slen = X.U32(p); p += 0x10;
					iextsmp.push(X.SA(p,slen));
					p += slen;
				} else p += smpszs[i]["slen"];
			}
		}
		if(bad != "") sVersion += "/malformed"+bad
		if(X.isVerbose()) {
			sOptionT(X.SA(0x2A,0x40));
			if(msg != "")
				if(showmsg) sOption(addEllipsis(msg,0x80),"msg: ");
				else sOption("("+addEllipsis(msg,0x80)+")","msg: ");
			else if(inss.length) sOption("\""+addEllipsis(inss.join(" "),200)+"\"","ins/msg:");
			else if(smps.length) sOption("\""+addEllipsis(smps.join(" "),200)+"\"","smp/msg:");
			sOption("ord:"+ord+" loop:"+loop+" ptn:"+ptn+(hasdrums?"+"+dptn:"")+" ins:"+ins+
			  " smp:"+smp+(extsmp.length > 0 ? " ext.smp: ["+extsmp.join(",")+"]" : "")+
			  " trk:"+trk+(hasdrums?"+8":"")+(vst2?("vst2:"+vst2):"")+" sz:"+outSz(p))
		}
	}

	else if(X.c("'MTRAC'") && X.U8(0x31) == 0x1A) {
		sName = "Pyro-Fyre/Arkham's Master Tracker module (.MTR)"; bDetected = 1;
		//TODO parse master tracker
		// x = X.readBytes(0,X.fSig(0,TOEOF,"1A"));
		// tt = X.SA(5,20).trim();

		// if(v!=null) sVersion = "v"+v;
		// if(X.isVerbose())
		//     sOptionT(X.SA(0x2A,0x40))
	}
	else if(X.c("3C4F3123 20391E00 1FdF1F9F 0C020C05 04040407 1AF60627")) {
		sName = "Packen/ぱっくん Software MUAP98/みゅあっぷ tone data (TONES.DTA)"; bDetected = 1;
		if(X.isVerbose()) sOption(outSz(6400),'sz:')
	}
	else if(X.c("'MVSM1'")) {
		sName = "Kaneda's MVSTracker Suite module (.MUS)"; bDetected = 1
	}
	else if(X.c("'MUS'1A") && (X.U16(4) >= X.U16(6))) {
		sName = "idSoft's DOOM music module (.MUS)"; bDetected = 1;
		if(X.isVerbose())
			sOption(outSz(X.U16(4)),'sz:')
	}
	else if(X.c("'MXM'00") && isWithin(X.U32(4),1,256) && X.U32(8) < X.U32(4)
	  && isWithin(X.U32(0xC),1,255) && X.U32(0x10)) {
		// from https://www.cubic.org/source/mxmplay-1.6.zip / xm2mxm.cpp
		sName = "Niklas 'pascal' Beisert's MXMplay module (.MXM)"; bDetected = 1;
		if(X.isVerbose()) {
			ord = X.U32(4); lp = X.U32(8); ch = X.U32(0xC); ptn = X.U32(0x10); ins = X.U32(0x14);
			spd0 = X.U8(0x18); bpm0 = X.U8(0x19);
			sOption('spd0:'+spd0+' bpm0:'+bpm0+' ch:'+ch+' ord:'+(lp?lp+'-':'')+ord+' ptn:'+ptn+' ins:'+ins)
		}
	}
	else if(X.c("'MXTX'00")) {
		sName = "David 'Talin' Joiner & Joe Pearce's MaxTrax/Music-X module (MXTX.+SMPL.)"; bDetected = 1
	}
	else if(X.c("'TWNNSNG1'00")) { // looks like the first ProTrekkr was called this
		sName = "J.A.A. 'Arguru' Ruis's NoiseTrekker module (.NTK)"; bDetected = 1;
		sVersion = "v1.6b";
		if(X.isVerbose())
			sOption(X.SA(9,20))
	}
	else if(X.c("'TWNNSNG2'00")) { // looks like the first ProTrekkr was called this
		sName = "J.A.A. 'Arguru' Ruis's NoiseTrekker module (.NTK)"; bDetected = 1;
		sVersion = "v2.0";
		if(X.isVerbose())
			sOption(X.SA(9,20))
	}
	else if((X.c("'TWNNSNG'..00") && ["6","G","I"].indexOf(X.SA(7,1) >= 0))
	  || X.c("'PROTREK'")) {
		//ref https://github.com/hitchhikr/protrekkr/blob/c321b1995d127fc232d8ea50e359d7e2603882b8/src/ptk.cpp#L2434
		sName = "J.A.A. 'Arguru' Rius et al.'s ProTrekkr module (.PTK)"; bDetected = 1;
		sVersion = "v"+X.SA(0x7,1);
		if(X.isVerbose()) // this stuff has compression in most versions, TODO?
			if(sVersion == "v6") sOption(X.SA(9,20))
	}
	else if(X.c("'NESM'1A") || X.c("'NSFE'")) {
		//ref http://www.nesdev.org/wiiki/NSF
		sName = "Nintendo Sound Format audio (."; bDetected = 1;
		sig = X.SA(0,4);
		if(sig === "NSFE") filever = "NSFe";
		else if(X.U8(5) == 2) filever = "NSF2";
		else filever = "NSF";
		sName += filever+")";
		INFOready = false; NEND = false; hkhd = ""; pn = 4;
		nsf2jump = palntsc = sz = -1; spd = ""; vrc7repl = 0;
		playtime = Number(0); g=""; t=""; a=""; c=""; taut=[]; tlbl=[]; xc="";
		if(filever === "NSFe") {
			if(X.isVerbose()) //no base header, find INFO first
			  while(pn < X.Sz()){
				hksz = X.U32(pn,_BE); hkhd = X.SA(pn+4,4);
				if(hkhd === "INFO") break;
				else pn += hksz+8;
			}
			nsf2jump = 0;
		}
		else { //NSF v1/2 have a hardplaced header, let's read that
			bDetected = 1;
			sVersion = "";
			if(X.isVerbose()) {
				palntsc = X.U8(0x7A);
				chip = X.U8(0x7B);
				tc = X.U8(6); t = X.SA(0x0E,0x20);
				a = X.SA(0x2E,0x20); c = X.SA(0x4E,0x20);
			}
			INFOready = true;
			nsf2jump = X.U24(0x7D,_LE);
			pn += nsf2jump; //NSF v1 files ALSO can have this jump... orz
		}
		// at this point we're standing at the first hunk unless it's a pure NSF, and at "INFO" for NSFe
		if(nsf2jump >= 0 && filever === "NSFe" && X.isVerbose())
			while(pn < X.Sz() && !NEND) {
				hksz = X.U32(pn,_LE); hkhd = X.SA(pn+4,4); pn+=8;
				switch (hkhd) {
					case "NEND": //end of hunks
						NEND = true; break;
					case "INFO": //really shouldn't happen with nsf1/2
						if(INFOready) break;
						palntsc = X.U8(pn+6);
						chip = X.U8(pn+7);
						tc = X.U8(pn+7);
						INFOready = true; break;
					case "RATE":
						spd = "rate :: NTSC: "+X.U16(pn,_LE)+" ticks";
						palspd = X.U16(pn+2,_LE);
						if(palspd > 0) spd += ", PAL: "+palspd+" ticks";
						dendyspd = X.U16(pn+4,_LE);
						if(dendyspd > 0) spd += ", Dendy: "+dendyspd+" ticks";
						break;
					case "VRC7":
						vrc7repl = X.U8(pn); break;
					case "time":
						for(i=0; i*4 < hksz; i++) {
							if(i*4 >= hksz) curtime=-1
							else curtime = X.I32(pn+i*4,_LE);
							if(curtime < 0) curtime = 2*60000; //default time: 2 minutes :V
							playtime += curtime/1000;
						} break;
					case "auth":
						apn = 0;
						g = X.SC(pn,0x100,"UTF8"); apn+=g.length+1;
						a = X.SC(pn+apn,0x100,"UTF8"); apn+=a.length+1;
						c = X.SC(pn+apn,0x100,"UTF8"); apn+=c.length+1;
						c += ", rip: "+X.SC(pn+apn,0x100,"UTF8");
						break;
					case "taut":
						if(X.isDeepScan()) {
							apn = 0;
							while(apn < hksz) {
								trkauth = X.SC(pn+apn,hksz-apn,"UTF8"); apn += trkauth.length+1;
								taut.push(trkauth)
							}
						} break;
					case "tlbl":
						if(X.isDeepScan()) {
							apn = 0;
							while(apn < hksz) {
								trklbl = X.SC(pn+apn,hksz-apn,"UTF8"); apn += trklbl.length+1;
								tlbl.push(trklbl)
							}
						} break;
					case "text":
						if(X.isDeepScan())
							xc = X.SC(pn,hksz,"UTF8");
				}
				pn += hksz; sz = pn;
			}
		if(X.isVerbose() && INFOready) {
			switch(palntsc) {
				case 0: sVersion += " NTSC"; break;
				case 1: sVersion += " PAL"; break;
				case 2: sVersion += " NTSC/PAL"
			}
			if(chip&0x01) sVersion += "#VRC6";
			if(chip&0x02) switch(vrc7repl) {
				case 1: sVersion += "#YM2413"; break;
				default: sVersion += "#VRC7" }
			if(chip&0x04) sVersion += "#FDS";
			if(chip&0x08) sVersion += "#MMC5";
			if(chip&0x10) sVersion += "#Namco163";
			if(chip&0x20) sVersion += "#Sunsoft5B";
			sOptionT(t);
			if(tc > 1) sOption(tc,"×");
			sOptionT(a,"by: ");
			sOptionT(c);
		}
		if(X.isVerbose()) {
			if(playtime > 1)
			  sOption(new Date(Math.round(playtime*1000)).toISOString().substr(11, 8),"Playtime: ");
			if(Math.max(tlbl.length,taut.length)>0) sOption("[Tracks]");
			for (i=0; i < Math.max(tlbl.length,taut.length); i++) {
				if(i < tlbl.length) ttlbl = tlbl[i];
				else ttlbl = "#"+(i+1);
				if(i < taut.length) ttaut = " by: "+taut[i];
				else ttaut = "";
				sOption(ttlbl+ttaut)
			}
			if(xc != "") sOption(xc,"[Commentary]: ");
			if(sz > -1) sOption(outSz(sz),"sz:")
		}
	}
	else if(X.c("'OKTASONGCMOD'00000008") && X.c("'SAMP'00000480", 0x18)
	  && X.c("'SPEE'00000002....'SLEN'00000002....'PLEN'00000002....'PATT'00000080", 0x4A0)
	  && X.c("'PBOD'", 0x546)) {
		sName = "Armin Sander's Oktalyzer module (.OK,.OKT,.OKTA)"; bDetected = 1;
		sVersion = 'v'+X.U16(0x14,_BE)+'.'+X.U16(0x16,_BE).padStart(2,'0');
		if(X.isVerbose()) {
			for(p=0x20,smp=0,smps=[]; p < 0x4A0; p += 0x20) {
				if((t=decAnsi(p,0x14,CPAmiga).trim()) != '') smps.push(t); if(X.U32(p+0x14,_BE)) smp++;
			}
			sOption(addEllipsis(smps.join(' ')),'smp/msg:"','"');
			tmp0 = X.U16(0x4A8,_BE); ptn = X.U16(0x4B2,_BE); ord = X.U16(0x4BC,_BE); rsmp = 0;
			p = 0x546; while(p < X.Sz() && rsmp < smp) {
				hkhd = X.SA(p,4); if(charStat(hkhd,1).indexOf('allasc') < 0) break;
				hksz = X.U32(p+4,_BE); if(hkhd === 'SBOD') rsmp++; p += 8+hksz
			}
			sOption('tmp0:'+tmp0+' ord:'+ord+' ptn:'+ptn+' smp:'+(rsmp!=smp?rsmp+'/':'')+smp+' sz:'+outSz(p))
		}
	}
	else if(X.c("'Onyx Music File'1A0001") && X.c("80808080",0x18E)) {
		// ref ftp://http.modland.com/pub/documents/format_documentation/Onyx%20Music%20File%20(.omf).txt
		sName = "Altar/Onyx's Onyx Music File (.OMF)"; bDetected = 1;
		if(X.isVerbose()) {
			ch = X.U8(0x192)+1;
			ptn = X.U8(0x193)+1;
			ord = X.U8(0x194)/2+1;
			title = X.SA(0x195,0x1F).trim();
			sn = []; smp = smpsz = 0;
			for(i=0; i < 31; i++) {
				t = X.SA(0x1B4+i*0x1C,21).trim(); if(t.length) sn.push(t);
				t = X.U16(0x1CC+i*0x1C); if(t) { smpsz += t; smp++ }
			}
			for(i=0,p=0x51A; i < ptn; i++) p += 3+X.U8(p+2)*0x100; sz = p+smpsz+smp*3;
			for(i=0; i < smp; i++) p += 3+X.U16(p+1); if(p != sz) sVersion = 'malformed!badsmpcnk';
			sOptionT(title); sOptionT(addEllipsis(sn.filter(funSampleName).join(' ')),'smp/msg:"','"');
			sOption('ch:'+ch+' ord:'+ord+' ptn:'+ptn+' smp:'+smp+' sz:'+outSz(sz))
		}
	}
	else if((X.c("'Org-02'") || X.c("'Org-03'")) && X.Sz() >= 114) {
		sName = "Amaya 'Pixel' Daisuke's Organya/ORG Maker module (.ORG)"; bDetected = 1;
		if(X.U8(5) === 0x32) sVersion == "v1"; else sVersion = "v2";
		if(X.isVerbose()) {
			c = ins = 0; for(i=0; i < 16; i++) {
				t = X.U16(0x16+6*i); if(t) { ins++; c += t }
			}
			sOption("ins:"+ins+" tempo:"+X.U16(6)+" rhythm:"+X.U8(8)
			  +"/"+X.U8(9)+" notes:"+c+" sz:"+outSz(114+(c<<3)))
		}
	}
	else if(X.c("'OBISYNTHPACK'") && X.Sz() > 0x510 && ( X.c("0100",0x50C) || !X.U32(0x50C,_BE) )) {
		sName = "Karsten 'Obi' Obarski's Synth Pack module (.OSP+SMP.set)"; bDetected = 1;
		if(X.isVerbose()) { //from initplayer RE
			p = a2 = 0xC; // @650
			for(x = ord = 0, d1 = 0x40; p < X.Sz() && d1--;) {
				d2 = X.U32(p,_BE); p += 4; if(!d2) break; ord++; d2 &= 0xFFFF;
				if(d2 == 0xF0) x++; // @656~664
			}
			for(d3 = d6 = 0; a2 < X.Sz() && d6 < 0x40;) { // @668~698
				d4 = X.U32(a2,_BE); a2 += 4;
				if(!d4) { if(X.U32(a2,_BE)) d6++;
				} else {
					d4 &= 0xFF00;
					if(d4 == 0xFE00 || d4 == 0xFC00) d3++;
					d6++;
				}
				if(d6 >= 0x40) break;
			}
			if(d3 > x) { x = d3; ord = d6 } // @69a~6a0
			if(X.c("2000",0xC) || X.c("FE",0xE)) x = 7; // @6a2~6b8
			d1 = 0x100;
			if(p < X.Sz()) p = 0x10C; ptn = 0; // @6bc~6c4
			for(; p < X.Sz() && d1--;) { if((t = X.U8(p)) > ptn) ptn = t; p += 4 } // @6c6~6d2
			if(p < X.Sz()) p = 0x50C; // @6d6...
			if(!X.U32(0x50C,_BE)) // to @6fe
				p += (ptn+1)*0x180; //
			else {
				for(; p < X.Sz(); p += 2) if(!X.U16(p,_BE)) break;
				for(p += X.U16(p-2,_BE); p < X.Sz(); p += 2) if(X.c("FFFFFFFF5FFF",p)) break; // @6e8~f2
				p += 6;
			}
			if(x > 1) sOption(x,'×');
			sOption('ord:'+ord+' ptn:'+ptn+' sz:'+outSz(p))
		}
	}
	else if(X.c("'OBISYNTHPACK'")) { //this must be the sample set
		for(p=0xC,i=smp=0; p < X.Sz() && i < 0x40; i++) {
			t = X.U32(p+4,_BE)-X.U32(p,_BE); p += 4; if(t) smp++;
		} p -= 4; sz = X.U32(p,_BE);
		if(sz <= X.Sz()) {
			sName = "Karsten 'Obi' Obarski's Synth Pack's sample set (SMP.SET)"; bDetected = 1;
			if(X.isVerbose()) sOption('smp:'+smp+' sz:'+outSz(sz))
		}
	}
	else if(X.c("00800404 1214191C 1FCE26D6 328E35EA 3CF23CF2") && X.c("0C000384000000000000",0x80)) {
		// I'm specifically detecting the one for Dyter-07 here
		for(p=0,i=smp=0; p < X.Sz() && i < 0x40; i++) {
			t = X.U16(p+2,_BE)-X.U16(p,_BE); p += 2; if(t) smp++;
		} p -= 2; sz = X.U16(p,_BE);
		if(sz <= X.Sz()) {
			sName = "Synth Pack's sample set (SMP.SET)"; sVersion = "headerless"; bDetected = 1;
			if(X.isVerbose()) sOption('smp:'+smp+' sz:'+outSz(sz))
		}		
	}
	else if(X.c("'PACG'........'PAIN'") && (t=X.fSig(0xC,0x100,"'SOIN'")+8) > 0x10 && X.U16(t+2)
	  && X.U8(t+4) && X.c("4005",t+5) && X.U32(4)+8 <= X.Sz()) {
	  	//ref http://hackipedia.org/browse.cgi/File%20formats/Music%20tracker/PAC%2c%20SBStudio%20II/SBStudio%20II%20by%20Henning%20Hellstrom%20v1%2e04%20%281994%29%2etxt
		sName = "Henning Hellström's SBStudio II module (.PAC)"; bDetected = 1;
		p = 8; sz = p+X.U32(4);
		title = tracker = bad = ''; end = trk = ord = ptn = spd0 = bpm0 = smp = 0;
		while (p < X.Sz()) {
			hkhd = X.SA(p,4); if(charStat(hkhd).indexOf('allasc') < 0) break;
			hksz = X.U32(p+4); p+=0x08; //chunk header = 4xfourcc + 4xsize
			switch (hkhd) {
			case "SND ": smp++; break;
			case "SOIN": spd0 = X.U8(p); bpm0 = X.U8(p+1); ptn = X.U16(p+2); trk = X.U8(p+4); break;
			case "SONA": title = X.SC(p,hksz,'CP850'); break;
			case "SOOR": ord = hksz>>1; break;
			case "PAOR": tracker = X.SC(p,hksz,'CP850'); break;
			case "END ": end = 1; break
			}
			p += hksz; if(end) break
		}
		if(bad != '') sVersion = sVersion.appendS('malformed'+bad,'/');
		if(X.isVerbose()) {
			sOptionT(title); sOption(tracker,'in: ');
			sOption('trk:'+trk+' ord:'+ord+' ptn:'+ptn+' smp:'+smp+' sz:'+outSz(X.U32(4)+8))
		}
	}
	else if(X.c("'PLM'1A..10") && isWithin(ch=X.U8(0x36),1,32)
	  && X.Sz() >= 4*((smp=X.U8(0x5C))+(ptn=X.U8(0x5D))+(ord=X.U16(0x5E,_LE)))
	  && firstNotOf(0x3C,0x20,[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]) < 0) {
		sName = "Alex 'Statix' Evans' Disorder Tracker 2 module (.PLM)"; bDetected = 1;
		p = X.U8(4); ofs = rsmp = 0; rptn = []; ptns = []; smps = []; mp = [0,'unk']; bad = '';
		for(i=0; i < ord; i++,p+=4) if(rptn.indexOf(t = X.U8(p+3)) < 0) rptn.push(t);
		for(i=0; i < ptn; i++,p+=4) {
			if((t = X.U32(p)) > mp[0]) mp = [t,'ptn'];
			t = X.SC(t+7,25,"CP437").trim(); if(t.length) ptns.push(t)
		}
		for(i=0; i < smp; i++,p+=4) {
			if((t = X.U32(p)) > mp[0]) mp = [t,'smp'];
			if(!t) continue; //fixes juice music... 10-8 eve again
			if(!X.c("'PLS'1A",t)) bad = bad.addIfNone('!badsmpsig:'+X.SA(t,4));
			if(!isWithin(X.I8(t+0x32),-1,15)) bad = bad.addIfNone('!badsmppan');
			if(X.U8(t+0x33) > 64) bad = bad.addIfNone('!badsmpvol');
			smps.push(X.SC(t+6,0x20,'CP437').trim());
			if(X.U32(t+0x43)) rsmp++;
		}
		rptn = rptn.length;
		if(p > X.Sz()) bad = bad.addIfNone('!short'); sz = Math.max(mp[0],p);
		switch(mp[1]) {
		case 'ptn': sz = mp[0]+X.U32(mp[0],_LE); break;
		case 'smp': sz = mp[0]+X.U8(mp[0]+4)+X.U32(mp[0]+0x43,_LE); break
		}
		if(bad.length) sVersion = sVersion.appendS('malformed'+bad,'/');
		if(X.isVerbose()) {
			sOptionT(X.SC(6,0x30,'CP437'));
			//sOption(addEllipsis(ptns.join(', '),0xE0),'ptn:"','"');
			sOption(addEllipsis(smps.filter(funSampleName).join(' '),0xE0),'smp/msg:"','"');
			sOption('ch:'+X.U8(0x36)+' bpm0:'+X.U8(0x3A)+' spd0:'+X.U8(0x3B)+' ord:'+ord
				+' ptn:'+(rptn != ptn? rptn+'/':'')+ptn+' smp:'+(rsmp != smp? rsmp+'/':'')+smp+' sz:'+outSz(sz));
		}
	}
	else if(X.c("'PLX'") && X.U8(3) <= 2 && X.U8(4) && X.U16(7) < X.Sz() && X.U16(9) < X.Sz() && X.U16(11) < X.Sz()) {
		//ref https://adplug.github.io/library/entry/Palladix.html
		sName = "Rainbow Arts' Palladix Sound System module (.PLX)"; bDetected = 1;
		if(X.isVerbose()) {
			for(ch=0,p=7; p < 17; p+=2) if(X.U16(p)) ch++;
			sOption('ch:'+ch)
		}
	}
	else if(X.c("FFFFE002E102")) {
		sName = "Achim Haertel's POKEYNoise chiptune (.PN)"; bDetected = 1
	}
	else if(X.c("01080B08 E1079E32 30363100 000078D8 A2FF9A20 1B082000")) {
		sName = "Aleksi Eeben's Polyanna module (.PRG)"; sVersion = "v1.0&player"; bDetected = 1;
		if(X.isVerbose()) sOption('sz:'+outSz(0x5801))
		//TODO add info
	}
	else if(X.c("'PSA'00") && 0x34 < X.U32(0x28,_BE) < X.Sz()
	  && 0x34 < X.U32(0x2C,_BE) < X.Sz()
	  && 0x34 < X.U32(0x30,_BE) < X.Sz()) {
		sName = "Professional Sound Artists module (.PSA)"; bDetected = 1;
		if(X.isVerbose()) {
			sOptionT(X.SA(0x4,0x14));
			//ref https://gitlab.com/uade-music-player/uade/-/raw/master/amigasrc/players/wanted_team/PSA/PSA_v2.asm
			d2 = X.U32(0x28,_BE);
			x = (d2-56) >> 3; if(x > 1) sOption(x,"×");
			d3 = X.U32(0x2C,_BE); d0 = X.U32(0x30,_BE);
			ins = ((d3-d2) >> 6); d3 = ins;
			synsmp = 0; smp = 0; allsmpsz = 0; d7 = 3;
			while(d3) { //hop
				ts = X.U32(d2,_BE); //ins size?
				if(ts) {
					if(ts < d7) {
						d7 = ts;
						smpsz = (X.U16(d2,_BE)+4) << 1;
						allsmpsz += smpsz;
						smp++
					}
				} else synsmp++; //synth
				d2 += 64; d3--
			}
			sz = X.U32(36,_BE); songsz = sz-allsmpsz; ptn = (songsz-d0) >> 10;
			sOption("ptn:"+ptn+" ins:"+ins+" smp:"+smp+" syn.smp:"+synsmp+" songsz:"+songsz+" sz:"+outSz(sz))
		}
	}

	else if(X.c("'PSF'")) {
		//ref https://gist.github.com/SaxxonPike/a0b47f8579aad703b842001b24d40c00
		nV = X.U8(3);
		if(nV > 0) {
			bDetected = 1;
			sName = "Neil Corlett's Portable Sound Format module (.";
			switch(nV) {
				case 0x01: sName += "PSF,.PSF1,.MINIPSF,.MINIPSF1)"; sVersion = "Playstation"; break;
				case 0x02: case 0x03: sName += "PSF2,.MINIPSF2)"; sVersion = "Playstation 2"; break;
				case 0x11: sName += "SSF,.MINISSF)"; sVersion = "Saturn"; break;
				case 0x12: sName += "DSF,.MINIDSF)"; sVersion = "Dreamcast"; break;
				case 0x21: sName += "USF,.MINIUSF)"; sVersion = 'Ultra64'; break;
				case 0x22: sName += "GSF,.MINIGSF)"; sVersion = 'Gameboy'; break;
				case 0x23: sName += "SNSF,.MINISNSF)"; sVersion = 'Super Nintendo'; break;
				case 0x24: sName += "2SF,.MINI2SF)"; sVersion = 'Nintendo DS'; break;
				case 0x25: sName += "NCSF,.MININCSF)"; sVersion = 'Nintendo DS Nitro Sound'; break;
				case 0x41: sName += "QSF,.MINIQSF)"; sVersion = 'Capcom Q-Sound'; break;
				//case ??:  sName += "DCSF,.MINIDCSF)"; sVersion = '?'; break;
				default: sName += "*SF,.MINI*SF)"; sVersion = "unk.console";
			}
			sVersion = "v"+Hex(nV)+" "+sVersion;
			if(X.isVerbose()) {
				//fs1 = Math.max(X.Sz()-0x200,0); fs2 = Math.min(X.Sz(),0x200);
				//ptags = X.fSig(fs1,fs2,"'[TAG]'")+5;
				ptags = X.U32(8,_LE)+0x15; sig2 = false;
				if(0x15 < ptags && ptags < X.Sz()) sig2 = X.SA(ptags-5,5) === "[TAG]";
				if(!sig2) { //coincides with pv==2
					ptags = X.U32(4,_LE)+0x15;
					if(0x15 < ptags && ptags < X.Sz()) sig2 = X.SA(ptags-5,5) === "[TAG]";
				}
				if(sig2) {
					tags = X.SC(ptags,X.Sz()-ptags,"UTF8").trim();
					a=""; g=""; t=""; y=""; l="";
					tagl = tags.split('\n');
					  for (i=0; i<tagl.length;i++) {
						tag = tagl[i].split("=");
						switch (tag[0]) {
							case "length": l = tag[1]; break;
							case "title": t = tag[1]; break;
							case "game": g = tag[1]; break;
							case "artist": a = tag[1]; break;
							case "copyright": if(a=="") a = tag[1]; break;
							case "year": y = tag[1]; break;
							case "dsfby","psfby","ssfby": if(a=="") a = tag[1]; break;
						}
					  }
					sOption(t);
					sOption(a,"by: ")
					sOption(y,"'");
					sOption(g,"for: ");
					sOption(l,"len ");
				}
				else sVersion += " library";
			}
			if(X.Sz() < 0x300) sVersion += " header"
		}
	}
	else if(X.c("'PSG'1A")) { sName = "Kirill Kolpakov's fMSX/x128 PSG chiptune (.PSG)"; bDetected = 1 }
	else if(X.c("'EPSG'1A")) {
		//ref https://web.archive.org/web/20070919070829/http://maxheadroom.no-ip.com/ay/formats_epsg.php
		sName = "Kirill Kolpakov's Extended PSG chiptune (.EPSG)"; bDetected = 1; hw = X.U8(5);
		switch(hw) {
		case 0: sVersion = "ZX Spectrum 128k"; break;
		case 1: "Pentagon"; break;
		}
		if(hw<2 && !X.c("00000000 000000000000",6)) sVersion += "/malformed"
	}
	else if(X.c("'PSG2'")) {
		//ref https://web.archive.org/web/20070919070829/http://maxheadroom.no-ip.com/ay/formats_epsg.php
		sName = "PSG2 chiptune (.PSG2)"; bDetected = 1;
	}
	else if((X.c("'PSM '") || X.c("'PSM'FE")) && X.fStr(8,10,'FILE') >= 0 && X.fStr(16,50,'MAINSONG') >= 0) {
		sName = "Epic Megagames MASI module (.PSM)"; bDetected = 1;
		if(X.isVerbose()) {
			pt = X.fStr(16,50,"MAINSONGTITL");
			if(pt>0) {
				ts = X.U32(pt+12);
				sOptionT(X.SA(pt+16,ts));
			}
			sOption(outSz(X.U32(4)+12),'sz:')
		}
	}
	else if(X.c("'PSY'..'SONG")) {
		sName = "J.M.A. Segura/Psycledelics' Psycle Modular Music Creation Studio module (.PSY)"; bDetected = 1;
		sV = X.SA(3,1); sVersion = "v"+sV;
		if(X.isVerbose()) {
			switch(sV) {
				case "0": case "1":
					sOptionT(X.SC(0x08,0x20,'CP1252'));
					sOptionT(X.SC(0x28,0x20,'CP1252'),"by: ");
					sOptionT(X.SC(0x48,0x80,'CP1252')); break;
				case "2":
				//ref https://sourceforge.net/p/psycle/code/HEAD/tree/trunk/psycle-core/src/psycle/core/psy2filter.cpp
					sOptionT(X.SC(0x08,0x20,'CP1252'));
					sOptionT(X.SC(0x28,0x20,'CP1252'),"by: ");
					sOptionT(X.SC(0x48,0x80,'CP1252'));
					bpm = X.I32(0xC8); ord = X.I32(0x191);
					trk = X.I32(0x195); ptn = X.I32(0x199);
					p = 0x19D;
					if(X.isDeepScan()) {
						inss = []; vsts = []; macs = []; waves = mach = 0; bad = "";
						for(i=0; i < ptn; i++) { rows = X.I32(p); p += 0x24+0xA0*rows }
						p += 4; inss = [];
						for(i=0; i < 0xFF; i++) { t = X.SC(p,0x20,'CP1252').trim();
							if(t != "" && t != "empty") inss.push(t); p += 0x20 }
						p += 0x37CC; //_log("waves @"+Hex(p))
						if(p > X.Sz()) bad = bad.addIfNone("!short");
						else for(i=0; i < 0xFF && p < X.Sz(); i++)
							for(w=0; w < 0x10 && p < X.Sz(); w++) { t = X.U32(p); p += 4;
//if(t || p+t*2 > X.Sz()) _log("wave["+i+","+w+"] @"+Hex(p)+" ["+Hex(t)+"]: "+X.SC(p,0x20,'CP1252').trim());
								if(t) { if(!w) waves++; p += 0x2D; var st = X.U8(p++)+1; p += t<<st }
							}
//_log("vsts @"+Hex(p));
						if(p > X.Sz()) bad = bad.addIfNone("!short");
						else for(i=0; i < 0x100; i++) {
							t = X.U8(p++); if(t) {
//_log("vst["+i+"] @"+Hex(p));
								t = X.SC(p,0x80,'CP1252').trim(); if(t != "") vsts.push(t);
								p += 0x80; t = X.I32(p); p += 4+t*4;
							}
						}
//_log("mach @"+Hex(p));
						m = []; for(i = 0; i < 128; i++) { t = X.U8(p++); if(t) mach++; m[i] = t }
						t = oldt = 0;
						const mtMa=0,mtRM=1,mtDi=2,mtSa=3,mtDe=4,mtF2=5,mtGa=6,mtFl=7,mtPl=8,
						  mtVI=9,mtVF=10,mtSc=11,mtAB=12,mtA1=13,mtA2=14,mtA21=15,mtDu=255;
						for(i=0; i < 128; i++) if(m[i]) {
							t = X.I32(p+8); p += 12;
							tn = X.SA(t===mtPl?p+0x100:p, 0x10);
							tpn = t === mtPl ? X.SA(p,0x100) : ""; ts = tn+tpn;
//_log("ts: "+ts+", oldt = "+oldt);
							nonascii = false; for(q=0; q < ts.length; q++)
								if(ts[q] < " " || ts[q] > "~" || /["+?*]/.test(ts[q])) {nonascii = true; break }
							if(((15 < t && t < 255) || ts.length < 2 || nonascii) && oldt === mtPl) {
								bad = bad.addIfNone("!pluginDefinedDataSize"); break
							}
							else if(15 < t && t < 255) { bad = bad.addIfNone("!badMachineType"); break }
							oldt = t;
							switch(t) {
							case mtPl: macs.push(tn);
								t = X.I32(p+0x110); p += 0x114+4*t;
//_log("   ["+macs[macs.length-1]+"] post-"+Hex(t)+" vals @"+Hex(p));
								p += 0x181; break;
							case mtVI: case mtVF: vsts.push(tn); p += 0x197; break;
							default: macs.push(X.SA(p,0x10)); p += 0x191
							}
						}
						if(p > X.Sz()) bad = bad.addIfNone("!short");
//_log("patch 0 @"+Hex(p));
						p += 5*0xFF; //patch 0
						if(p > X.Sz()) bad = bad.addIfNone("!noP0");
//_log("patch 1 @"+Hex(p));
						p += 0x40;
						if(p > X.Sz()) bad = bad.addIfNone("!noP1");
//_log("patch 2 @"+Hex(p));
						if(p < X.Sz() && X.U8(p++))
							p += 4+X.U32(p);
						if(bad != "") sVersion += "/malformed"+bad;
						if(inss.length) sOption("inss: ["+inss.join(",")+"]");
						if(vsts.length) sOption("vsts: ["+vsts.join(",")+"]");
						if(macs.length) sOption("mcn: ["+macs.join(",")+"]");
						sOption("ord:"+ord+" ptn:"+ptn+" trk:"+trk+" bpm:"+bpm+" mach:"+mach+" smp:"+waves+" sz:"+outSz(p));
					} //isdeep
					else sOption("ord:"+ord+" ptn:"+ptn+" trk:"+trk+" bpm:"+bpm);
					break;
				case "3":
				//ref https://sourceforge.net/p/psycle/code/HEAD/tree/trunk/psycle-core/src/psycle/core/psy3filter.cpp
					nV = X.I32(8);
					sVersion+="/"+nV;
					p = 0x10+X.U32(0x0C); //chunk header = 4xfourcc + 4xver + 4xsize
					hkn = X.U32(0x10,_LE);
					t=''; a=''; c=''; trk=0; bpm=0; ptnlns=0; ptn=0; mac=0; ins=0; eins=0;
					while (hkn > 0) {
						hkhd = X.SA(p,4);
						hkn--;
						cV = X.U32(p+4,_LE);
						hksz = X.U32(p+8,_LE);
						p+=0x0C;
						switch (hkhd) {
							case "INFO":
								p1 = p;
								if((cV & 0xFF00)==0) {
									t = X.SC(p1,128,'CP1252'); p1+=t.length+1;
									a = X.SC(p1,64,'CP1252'); p1+=a.length+1;
									c = X.SC(p1,0xFFFF,'CP1252'); p1+=c.length+1;
									if(cV == 0) hksz=t.length+a.length+c.length+3 //bug workaround
								}
								break;
							case "SNGI":
								if((cV & 0xFF00) == 0) {
									trk=X.I32(p,_LE);
									if(cV==0) hksz = 11*4+trk*2; //bug workaround
									bpm=X.I16(p+4,_LE) + X.I16(p+6,_LE)/100.0;
								}
								break;
							case "SEQD":
								if((cV & 0xFF00) == 0)
									seqlen=X.I32(p+4,_LE);
								break;
							case "PATD":
								if((cV & 0xFF00) == 0) {
									_idx = X.I32(p,_LE);
									ptnlns = X.I32(p+4,_LE);
									ptnn = X.SA(p+0x0C,0x20);
									p1=p+0x0C+ptnn.length+1;
									ptnsz = X.I32(p1,_LE);
									ptn++; p1+=4;
									if((cV == 0) && (p1+ptnsz == p+hksz+4)) hksz += 4; //bug workaround
								}
								break;
							case "MACD":
								mac++;
								//bugfix impossible here, let's pretend it won't happen and move on
								break;
							case "INSD":
								ins++;
								break;
							case "EINS":
								if((cV & 0xFFFF0000) == 0x10000) {
									eins = X.U32(p,_LE)
									//and again, pretending there's no bug (and no samples)
								}
								break;
						}
						p += hksz;
					}
					sOptionT(t); sOptionT(a,"by: "); sOptionT(c);
					sOption("bpm:"+bpm+" trk:"+trk+" ptnlns:"+ptnlns+
						" mac:"+mac+" ptn:"+ptn+"/idx:"+_idx+" ins:"+ins+" eins:"+eins+" sz:"+outSz(p));
					break;
				case "4":
				//ref https://sourceforge.net/p/psycle/code/HEAD/tree/trunk/psycle-core/src/psycle/core/psy4filter.cpp
					sVersion+="/future";
					//yet another zipped-xmls format
			}
		}
	}

	else if(X.c("'PSMP'") && [0,0x10].indexOf(X.U8(4)) >= 0) {
		sName = 'Sega MegaDrive Pre-SMP chiptune (.PSZ)'; bDetected = 1;
		sVersion = X.U8(4)? 'BE': 'LE';
		if(X.isVerbose()) sOption('tempo:'+X.U8(5))
	}

	else if(X.c("'PTCOLLAGE-'") || X.c("'PTTUNE--20'")) {
		//ref source code / pxtnService.cpp, pxtnDescriptor.cpp
		bDetected = 1;
		v1 = X.SA(2,1);
		if(v1 === "T") { rough = 10; //used for clocking
			sName = "Amaya 'Pixel' Daisuke's pxtone tune (.PTTUNE)" }
		else { rough = 1;
			sName = "Amaya 'Pixel' Daisuke's pxtone project (.PTCOP)" }
		dt = X.SA(10,6);
		if(dt <= "050227") { sV = "v.x1x"; nV = 1 }
		else if(dt <= "050608") { sV = "v.x2x"; nV = 2 }
		else if(dt <= "060115") { sV = "v.x3x"; nV = 3 }
		else if(dt <= "060930") { sV = "v.x4x"; nV = 4 }
		else if(dt <= "071119") { sV = "v5"; nV = 5 }
		d = dt.substr(0,2)+"-"+dt.substr(2,2)+"-"+dt.substr(4,2);
		sVersion = sV+"/20"+d;
		switch(nV) {
			case 1: case 2: p = 0x10; break;
			case 3: case 4: p = 0x14; break;
			default: p = 0x14
		}
		t = c = bad = ""; bclock = lclock = bnum = btempo = bps = ch = 0; bEnd = false;
		while(!bEnd && p < X.Sz()) {
			hkhd = X.SA(p,8); hksz = X.U32(p+8,_LE);
//_l2r('px',p,hkhd)
			switch (hkhd) {
				case "PROJECT=":
					t = X.SC(p+12,0x10,"Shift_JIS");
					btempo = X.F32(p+0x1C,_LE).toFixed(0);
					bclock = X.I16(p+0x20,_LE);
					bnum = X.I16(p+0x22,_LE);
					break;
				case "evenMAST": //x4x
					if(X.U16(p+12,_LE) != 3)
						sVersion += "/unk";
					else {
						var p_ = X.fSig(p+3,0x100,"'textNAME'")-12;
						if(p_ > -12) p = p_-hksz;
						else {
							p_ = X.fSig(p+3,0x100,"'textCOMM'")-12;
							if(p_ > -12) p = p_-hksz;
						}
					}
					break; //not gonna parse what vari-read gives me
				case "MasterV5": //x5x
					if(hksz != 0xF) bad = bad.addIfNone('!badv5fmt');
					bclock = X.I16(p+12)*rough;
					bnum = X.I8(p+14);
					btempo = X.F32(p+15).toFixed(0);
					lmeas = (X.I32(p+0x17)/(bnum*bclock)).toFixed(0);
//_l2r('px',p+15+12,'So, the last measure is '+lmeas+' at tempo '+btempo);
					break;
				case "Event V5":
					var e, evtn = X.U32(p+12,_LE);
					q = p+11;
					for(e=0; e < evtn; e++) { //hksz is broken in this chunk so it's like this, and yes it is slow
						for(i=0; i < 5; i++) { if(X.U8(++q) < 0x80) break }
						q += 2;
						for(i=0; i < 5; i++) { if(X.U8(++q) < 0x80) break }
					}
					hksz = 4+q-p-11;
					break;
				case "textNAME":
					t = X.SC(p+12,hksz,"Shift_JIS"); break;
				case "textCOMM":
					c = addEllipsis(X.SC(p+12,hksz,"Shift_JIS"),0xA0); break;
				case "assiWOIC": ch++; break;
				case "END=====": case "pxtoneND":
					bEnd = true; break
			}
			p += 12+hksz;
		}
		if(bad.length) sVersion = sVersion.appendS('malformed'+bad, '/');
		if(X.isVerbose()) {
			if(t != "no name") sOption(t);  sOptionT(c);
			if(bclock+btempo+bnum > 0)
				sOption('ch:'+ch+" bpm:"+btempo+" bclock:"+bclock+" bnum:"+bnum);
			sOption(outSz(p),"sz:")
		}
	}

	else if(X.c("'PTNOISE-'")) {
		sName = "Amaya 'Pixel' Daisuke's pxtone Noise instrument (.PTNOISE)"; bDetected = 1
	}

	else if(X.c("'PTVOICE-'")) {
		sName = "Amaya 'Pixel' Daisuke's pxtone Voice instrument (.PTVOICE)"; bDetected = 1;
		if(X.isVerbose()) sOption(outSz(X.U32(0x0C)+0x10),"sz:")
	}

	else if(X.c("'PTMF'",0x2C) && X.U8(0x1C) == 0x1A && X.U8(0x1E) <= 2 && isWithin(ch=X.U16(0x26),1,32)
	  && (ord=X.U16(0x20)) < 256 && isWithin(ins=X.U16(0x22),1,255) && isWithin(ptn=X.U16(0x24),1,128)) {
		// ref https://web.archive.org/web/20151027135201if_/http://hackipedia.org/File%20formats/Music/Sample%20based/text/Poly%20Tracker%20module%20format.cp437.txt.utf-8.txt
		sName = "Lone Ranger/AcmE's Poly Tracker module (.PTM)"; bDetected = 1;
		sV = Hex(X.U16(0x1D)); sVersion = "v"+sV.substr(0,1)+'.'+sV.substr(1,2);
		bad = '';
		for(sn=[],i=mp=rsmp=0,p=0x260; i < ins; i++,p+=0x50) {
			if(X.U8(p) & 3) { rsmp++; t = X.U32(p+0x12); if(mp < t) { mp = t; sz = t+X.U32(p+0x16) } }
			t = X.SC(p+0x30,0x1C,'CP437').trim(); if(t.length) sn.push(t);
		} sz = Math.max(sz,p);
		if(!rsmp) bad = bad.addIfNone('!badsmpavl');
		if(bad.length) sVersion = sVersion.appendS('malformed'+bad,'/');
		if(X.isVerbose()) {
			sOptionT(X.SC(0,0x1C,'CP437')); sOption(addEllipsis(sn.join(' ')),'ins/msg:"','"');
			sOption('ch:'+ch+' ord:'+ord+' ptn:'+ptn+' ins:'+ins+' smp:'+rsmp+' sz:'+outSz(sz))
		}
	}

	else if(X.c("'RAD by REALiTY!!'")) {
		//ref https://www.3eality.com/Public/Releases/RAD/radv2_0a.zip source/validate20.cpp
		// & https://www.3eality.com/Public/Releases/RAD/radv1_1a.zip player.asm
		sName = "Shayde & Rogue & Void's Reality ADlib Tracker module (.RAD)"; bDetected = 1;
		sV = (nV = X.U8(0x10)).toString(16);
		sVersion = "v"+sV[0]+"."+sV[1]; bad = "";
		if(X.isVerbose()) {
			al = X.U8(0x11);
			fbpm = nV > 0x20 && (al & 0x20); // in v2.1, if the bpm flag is set, bpm != 125 and is the next byte
			if((nV == 0x10 && (al & 0x80)) || nV >= 0x20) { //desc present
				l = 0x12; if(fbpm) l += 2;
				desc = ""; maxdesc = 250; cutoff = false;
				r = X.fSig(l,X.Sz()-32-l,"00"); p = r+1;
				if(r < 0) { maxdesc = 0; bad = bad.addIfNone("!baddesc") }
				else if(r > l+maxdesc) { r = l+maxdesc; cutoff = true }
				tmp = X.readBytes(l,r-l);
				for(i=0; desc.length < maxdesc && i < r-l; i++)
					switch(tmp[i]) {
					case 0: i = r-l; break;
					case 1: desc += "\n"; break;
					default:
						if(tmp[i] < 0x20) for(j=0; j < tmp[i] && desc.length <= maxdesc; j++) desc += " ";
						else desc += String.fromCharCode(tmp[i])
					}
				if(cutoff) { desc += "..." } sOptionT(desc);
			} else p = 0x12;
			spd0 = al&0x1F;
			if(fbpm) bpm = X.U16(0x12); else bpm = 125;
			ins = 0;
//_l2r('rad',p,'pre-ins')
			insns = [];
			while(p < X.Sz()) {
				insn = X.U8(p++); if(!insn || p > X.Sz()) break;
				ins++;
				if(nV == 0x10) p += 11;
				else if(nV == 0x21) {
					nmlen = X.U8(p++); insns.push(X.SA(p,nmlen));
					p += nmlen;
					rm = X.U8(p); riff = rm & 0x80; midi = (rm&7)==7;
					if(midi) p += 7; else p += 24;
					if(riff) p += X.U16(p)+2;
				} else bad = bad.addIfNone("!badver");
			}
//_l2r('rad',p,'pre-orderlist')
			ord = X.U8(p++); p += ord; //TODO parse jumps
			if(!ord || ord > 128) bad = bad.addIfNone("!badord");
//_l2r('rad',p,'pre-ptns')
			ptn = ptns = 0;
			if(nV > 0x20) while(p < X.Sz()) { if((t = X.U8(p++)) == 0xFF) break;
				ptns++; if(ptn <= t) ptn = t+1;  p += 2+X.U16(p);
			}
			else { //v1
				for(i=mptn=0; i < 32; ++i,p+=2) { if(t=X.U16(p)) ptn++;
					mptn = Math.max(mptn,t); if(t > X.Sz()) bad = bad.addIfNone('!short')
				}
				if(mptn) for(p=mptn,end=false; !end && p < X.Sz();)  { //parse the last ptn
					t = X.U8(p++);
//_l2r('rad1',p-1,'newptnline #'+Hex(t&0x7F)+(t&0x80?'::last':''))
					if(t & 0x80) end = true; //line
					while(p < X.Sz()) { t = X.U8(p); p += 2; if(X.U8(p++) & 0x07) p++; if(t & 0x80) break } //notes/fx in line
				}
			}
			if(!ptn) bad = bad.addIfNone("!badptn"); if(!ins) bad = bad.addIfNone("!badins");
			riff = riffs = 0;
			if(nV > 0x20) {
//_l2r('rad2',p,'pre-riffs')
				while(p < X.Sz()) {
					if((t = X.U8(p++)) == 0xFF) break;  riffs++; if(riff < t) riff = t;  p += 2+X.U16(p);
				}
				sOptionT(addEllipsis(insns.filter(funSampleName).join(' '),0x100),'ins:"','"');
				sOption('spd0:'+spd0+' bpm:'+bpm+' ord:'+ord+' ptn:'+ptns+(ptn!=ptns?'/'+ptn:'')
					+' ins:'+ins+' riffs:'+riffs+(riff!=riffs?'/'+riff:''))
			}
			else sOption('spd0:'+spd0+' bpm:'+bpm+' ord:'+ord+' ptn:'+ptn+' ins:'+ins)
			if(bad) sVersion += "/malformed"+bad
			sOption('sz:'+outSz(p))
		}//isVerbose
	}

	else if(X.c("'RAWADATA'") && X.Sz() > 10) {
		sName = "Raw OPL Capture chiptune (.RAW)"; bDetected = 1;
		if(X.isVerbose() && (X.isDeepScan() || X.Sz() < 0xFFFF)) { //may take a sec
			//taken from https://github.com/adplug/adplug/blob/master/src/raw.cpp
			p = 10; len = (X.Sz()-10) >> 1;
			tagdata = false; t = a = d = ""; next = 0;
			for(i=0; i < len && p < X.Sz(); i++) {
				x = (tagdata ? 0xFFFF : X.U16(p));
				p += 2;
				if(!tagdata && x==0xFFFF) { //trying to stop at the RAW EOF data marker
					tagcode = X.U8(p++);
					if(tagcode == 0x1A) tagdata = true; //tag marker found
					else if(!tagcode && charStat(X.readBytes(p,5),true).includes('allxsc')) { //old comment (music archive 2004)
						d = X.SA(p,1023); p += 1023; tagdata = true }
					else p--;
				}
			}
//_logIt('@'+Hex(p)+' tagdata? '+tagdata);
			if(tagdata && p+40 <= X.Sz()) { //searching for tags
				title = X.SA(p,40); p += 40;
				if(X.U8(p++) != 0x1B) { //if not, skip "author"
					p--;
					if(X.U8(p) >= 0x20) { //older version tag, eg. stunts.raw
						a = X.SA(p,60); p += 60;
						d = X.SA(p,1023); p += 1023;
						next = 2; //end_section
					} else p--;
					next = 1; //desc_section
				}
				if(!next) {
					a = X.SA(p,40); p += 40
				}
				if(next == 1) { next = 0;
					if(X.U8(p++) != 0x1C) next = 2;
					if(!next) {
						d = X.SA(p,1023); p += 1023
					}
				}
			}
			sOption(title); sOption(a,'by: '); sOption(d)
		} //isDeepVerbose
		if(X.isVerbose()) sOption(X.U16(8),'clkspd:')
	}

	else if(X.c("'RNS0'") && X.c("'>>> Chunk Start <<<'00", 9)) {
		sName = "Eduard Müller's Renoise module (.RNS)"; bDetected = 1;
		sV = X.SA(3,4); sVersion = "v"+sV;
		if(sV < "05") sVersion += "/RN<1.1.1";
		else if(sV === "05") sVersion += "/RN1.1.1";
		else if(sV < "015") sVersion += "/RN<1.2.7";
		else if(sV === "015") sVersion += "/RN1.2.7";
		else if(sV < "018") sVersion += "/RN<1.5.2";
		else if(sV === "018") sVersion += "/RN1.5.2";
		else sVersion += "/RN>1.5.2"
		if(X.isVerbose()) {
			p = 9;
			while (p < X.Sz()) {
				if(!X.c("'>>> Chunk Start <<<'00",p)) break;
				p += 20;
				if(X.c("'Header V00         '00",p)) {
					sOptionT(X.SC(p+20,20,'CP1252'));
					sOptionT(X.SC(p+40,20,'CP1252'),"by: ");
					sOptionT(X.SC(p+60,20,'CP1252'),"style: ");
					break;
				}
				else if(X.c("'Header V01         '00",p)) {  p += 20;
					sz = X.U32(p); sOptionT(X.SC(p+4,sz,'CP1252')); p+=sz+4;
					sz = X.U32(p); sOptionT(X.SC(p+4,sz,'CP1252'),"by: "); p+=sz+4;
					sz = X.U32(p); sOptionT(X.SC(p+4,sz,'CP1252'),"style: ");
					break;
				}
				else if(X.c("Header V02         '00",p)) {  p += 20;
					sz = X.U32(p); sOptionT(X.SC(p+4,sz,'CP1252')); p+=sz+4;
					sz = X.U32(p); sOptionT(X.SC(p+4,sz,'CP1252'),"by: "); p+=sz+4;
					sz = X.U32(p); sOptionT(X.SC(p+4,sz,'CP1252'),"style: "); p+=sz+4+5;
					sz = X.U32(p); sOptionT(X.SC(p+4,sz,'CP1252'));
					break;
				}
				p = X.fSig(p+1,TOEOF,"''>>>  Chunk End  <<<'00");
				if(p == -1) break;
				p += 20
			}
		}
	}
	else if(extIs("xrns") && X.c("'PK'0304")) {
		//TODO when ZIP parsing is available
		sName = "Eduard Müller's Renoise module (.XRNS)"; sType = '~audio'; sVersion = "xml"; bDetected = 1
		//versions vs doc_version:
		// 10: 1.9.1    14: 2.0.0    15: 2.1.0    21: 2.5.1    22: 2.6.1    30: 2.7.0, 2.7.1, 2.7.2
		// 37: 2.8.2    54: 3.0.1    63: 3.1.0    65: 3.3.2    66: 3.4.2
	}
	else if(extIs("xrdp") && X.fStr(0,0x100,"<FilterDevicePreset")>=0) {
		sName = "Eduard Müller's Renoise filter device preset (.XRDP)"; bDetected = 1;
		p = X.fStr(20,0x100,"doc_version=");
		if(p < 0) sVersion = "malformed";
		else {
			sVp = X.SA(p,0x10); sVersion = "v"+/doc_version=\"(\d*)\"/.exec(sVp)[1]
		}
	}
	else if(extIs("xrni") && X.c("'PK'0304")) {
		//TODO when ZIP parsing is available
		sName = "Eduard Müller's Renoise instrument (.XRNI)"; sType = '~audio'; sVersion = "xml"; bDetected = 1
	}
	else if(extIs("xrno") && (X.fStr(0,0x100,"<SampleModulationSet")>=0)) {
		sName = "Eduard Müller's Renoise sample modulation set (.XRNO)"; bDetected = 1;
		p = X.fStr(15,0x100,"doc_version=");
		if(p < 0) sVersion = "malformed";
		else {
			sVp = X.SA(p,0x10); sVersion = "v"+/doc_version=\"(\d*)\"/.exec(sVp)[1]
		}
	}
	else if(extIs("xrnt") && (X.fStr(0,0x100,"<RenoiseDeviceChain")>=0)) {
		sName = "Eduard Müller's Renoise effect chain (.XRNT)"; bDetected = 1;
		p = X.fStr(15,0x100,"doc_version=");
		if(p < 0) sVersion = "malformed";
		else {
			sVp = X.SA(p,0x10); sVersion = "v"+/doc_version=\"(\d*)\"/.exec(sVp)[1]
		}
	}
	else if(extIs("xrnt") && X.c("'PK'0304")) {
		sName = "Eduard Müller's Renoise effect chain (.XRNT)"; sVersion = "v<3.0"; bDetected = 1
	}
	else if(extIs("xrnz") && (X.fStr(0,0x100,"<InstrumentPhrase")>=0)) {
		sName = "Eduard Müller's Renoise instrument phrase (.XRNZ)"; bDetected = 1;
		p = X.fStr(15,0x100,"doc_version=");
		if(p < 0) sVersion = "malformed";
		else {
			sVp = X.SA(p,0x10); sVersion = "v"+/doc_version=\"(\d*)\"/.exec(sVp)[1]
		}
	}

	else if(X.c("'NuRIFFRAFF'",0x22)) {
		sName = "Riff Raff module (.RIFF)"; bDetected = 1;
		if(X.isVerbose()) {
			t = X.SC(0x64,0x100,'CP1252');
			a = X.SC(0x64+t.length+1,0x100,'CP1252');
			c = X.SC(0x64+t.length+a.length+2,0x100,'CP1252');
			sOptionT(t); sOptionT(a,"by: "); sOptionT(c)
		}
	}

	else if(X.c("'RON_KLAREN_SOUNDMODULE!'00", 0x28)) {
		sName = "Ron Klaren module (.RK)"; bDetected = 1
	}

	else if(X.c("'RTMM '") && X.U8(0x25) == 0x1A &&  X.c("'RTND'",0x2A+X.U16(0x28)+X.U32(0x88))) {
		sName = "Arnaud Hasenfratz's Real Tracker module (.RTM)"; bDetected = 1;
		sVersion = "v"+X.SA(0x37,7)+'/'+Hex(X.U16(0x26));
		if(X.isVerbose()) {
			rtmmxds = X.U32(0x88);
			sOptionT(X.SC(5,0x20,'CP1252'));
			sOptionT(X.SC(0x3E,0x20,'CP1252'),"by: ");
			sOptionT(X.SC(0x8C,0x20,'CP1252'),"orig.: ");
			sOption('ch:'+X.U8(0x60)+' spd0:'+X.U8(0x66)+' bpm0:'+X.U8(0x67)
				+' ord:'+X.U16(0x62)+' ptn:'+X.U16(0x64)+' ins:'+X.U8(0x61))
		}
	}

	else if(X.c("01FF..3EFF32018079FEFF2846320280B7201301FF013E1CED79053E02ED79AF320180C9") && X.c("21A9843D280E232347237E2BB67820F6232318EF5E23567AB328D22B2224803EFE320E823E08325280C35C80", 0x26)) {
		sName = "František Fuka/Fuxoft's Samadeus module (.samadeus)"; bDetected = 1;
		//TODO reverse and extract info
	}

	else if(X.c("00000000 00000200",0x0C) && X.c("00")
	  && X.U16(0x18,_LE) === X.U16(0x40,_LE)
	  && X.c("00'ST-Module.'", 0x4B7)) {
		sName = "Oliver 'BSC' Mayer's Amstrad CPC Soundtrakker module (.STK)"; bDetected = 1;
		datasz = X.U16(0x18,_LE); sz = datasz + 0x80;
		if(sz < X.Sz()) sVersion = "malformed";
		crc = 0; for(i=0; i < 67; i++) crc += X.U8(i); crc &= 0xFFFF;
		if(crc != X.U16(0x43)) sVersion += sVersion != "" ? "!badCRC" : "/malformed!badCRC";
		if(X.isVerbose())
			sOptionT(X.SA(1,8));
			sOption(outSz(sz),"sz:")
	}

	else if(X.c("00000000 00000200",0x0C) && X.c("00")
	  && X.U16(0x18,_LE) === X.U16(0x40,_LE)
	  && X.SA(1,8) === X.SA(0xB10,8)) {
		sName = "Oliver 'BSC' Mayer's Amstrad CPC Soundtrakker 128K module (.128)"; bDetected = 1;
		datasz = X.U16(0x18,_LE); sz = datasz + 0x80;
		if(sz < X.Sz()) sVersion = "malformed";
		crc = 0; for(i=0; i < 67; i++) crc += X.U8(i); crc &= 0xFFFF;
		if(crc != X.U16(0x43)) sVersion += sVersion != "" ? "!badCRC" : "/malformed!badCRC";
		if(X.isVerbose())
			sOptionT(X.SA(1,8));
			inst = []; ins = 0;
			for(i=0; i < 16; i++) if(![0,0x20].indexOf(X.U8(0xB18+8*i))) {
				ins++; inst.push(X.SA(0xB18+8*i,8).trim())
			} //TODO confirm that that's how to count the instruments
			sOption(inst);
			sOption("ins:"+ins+" sz:"+outSz(sz))
	}

	else if(X.c("00'ST-Module.'",0x437)) {
		sName = "Oliver 'BSC' Mayer's Amstrad CPC Soundtrakker module (.STK)"; bDetected = 1;
		sVersion = "headerless"
	}

	else if(X.Sz() > 0x20 && /S98[0-3]/.test(X.SA(0,4))
	  && X.U32(4) <= 0x20 && !X.U32(0xC)
	  && (!X.U32(0x10) || isWithin(X.U32(0x10),0x20,0x800000))
	  && X.U32(0x14) < 0x20000
	  && (!X.U32(0x18) || isWithin(X.U32(0x18),X.U32(0x14),0x800000)) //the highest loop point I saw is 2FB39Ah @ "Snatcher/47 staffroll2.s98"
	  && X.U32(0x1C) <= 0x40) {
		//ref https://www.purose.net/befis/download/lib/t98/ins98131s.zip / s98spec1.txt
		// & http://www.purose.net/befis/download/kmp/old/s98spec2.txt
		// & https://github.com/rururutan/s98spec3/blob/master/s98spec3-ja.txt
		sName = "Ru³'s S98 chiptune (.S98)"; bDetected = 1;
		nv = X.U8(3)-0x30; sVersion = 'v'+nv; dev = X.U32(0x1C);
		if(!dev || (nv >= 2 && !X.U32(0x20))) sVersion += '#OPNA(YM2608)@7.6MHz';
		else for(i=0,p=0x20; i < dev && X.U32(p); i++,p+=0x10) {
			clk = (X.U32(p+4)/0x100000).toFixed(1)+'MHz';
			if((dt=X.U32(p)) > 16) sVersion += '#unk@'+clk;
			else sVersion += '#'+['',
				'PSG(YM2149)','OPN(YM2203)','OPN2(YM2612)','OPNA(YM2608)','OPM(Y2151)',
				'OPLL(YM2413)','OPL(YM3526)','OPL2(YM3812)','OPL3(YMF262)',
				'unk0A','unk0B','unk0C','unk0D','unk0E','PSG(AY-3-8910)','DCSG(SN76489)'][dt] + '@'+clk
			}
		bad = ''; 
		if(X.U32(0x14) > X.Sz() || X.U32(0x18) > X.Sz()) bad += '!short';
		if(X.U32(0x10) > X.Sz()-7) bad += '!badmetadata';
		if(bad.length) sVersion += '/malformed'+bad;
		if(X.isVerbose()) {
			a = c = s = g = ti = y = '';
			switch(nv) {
				case 0: case 1: case 2:
					p = X.U32(0x10);
					if(p > 0)
						sOptionT(X.SC(p,0x40,'Shift_JIS').replace('\\','￥'));
					pk = X.U32(0x0C);
					if(pk) { sVersion += ' compressed';
						sOption("Please send this file over Telegram to @kaens, the detection author! It's unique and needs research") }
					break;
				case 3:
					ptags = X.U32(0x10);
					if(ptags > 6 && X.c("'[S98]'",ptags)) {
						ptags += 5;
						bUTF8 = X.c("EFBBBF",ptags); //if BOM's there
						if(bUTF8) tags = X.SC(ptags,0x200,'UTF8');
						else tags = X.SC(ptags,0x200,'Shift_JIS');
						tagl = tags.split('\x0A');
						for (i=0; i < tagl.length; i++) {
							tag = tagl[i].split("=");
							switch (tag[0]) {
								case 'title': case 'ｔｉｔｌｅ': ti = tag[1]; break;
								case 'game': case 'ｇａｍｅ': g = tag[1]; break;
								case 'system': case 'ｓｙｓｔｅｍ': s = tag[1]; break;
								case 'artist': case 'ａｒｔｉｓｔ': a = tag[1]; break;
								case 'year': case 'ｙｅａｒ': y = tag[1]; break;
								case 'copyright': case 'ｃｏｐｙｒｉｇｈｔ': if(a=="") a = tag[1]; break;
								case 's98by': case 'ｓ９８ｂｙ': if(a=="") a = tag[1]; break;
								case 'comment': case 'ｃｏｍｍｅｎｔ': c = tag[1]; break;
							}
						}
						sOption(ti); sOption(a,'by: '); sOption(y, y.length < 4? "y'": 'y');
						sOption(g,'for: '); sOption(s,'on: '); sOption(c);
						if(ptags > 6 && X.c("'[S98]'",ptags-5) && (t=X.fSig(ptags,0x300,"00")) > -1)
							sOption(outSz(t+1),'sz:')
						break
					} //if correct tag
					else if(ptags > 6)
						t = X.SC(ptags,0x40,'Shift_JIS').replace('\\','￥') //if it's some text—shouldn't happen in v3
			}//switch nv
		}//isVerbose
	}

	else if(X.c("'SAP'0D0A") && (p=X.fSig(3,TOEOF,"0D0AFFFF")) >= 3 && X.U16(p+4) < X.U16(p+6)) {
		//ref https://asap.sourceforge.net/sap-format.html
		sName = "Adam Bienias's Slight Atari Player chiptune (.SAP)"; bDetected = 1;
		if(X.isVerbose()) {
			bad = false; t=""; a=""; dt=""; tp=""; tm="";
			taghunk = X.SA(5,p);
			if(taghunk.length > 5) {
				tags = taghunk.split("\x0D\x0A");
				for(i=0; i < tags.length; i++) {
					tagdiv = tags[i].indexOf(" ");
					tagl = tags[i].substr(0,tagdiv);
					tagr = tags[i].substr(tagdiv+1,tags[i].length);
					switch(tagl) {
						case "NAME": if(tagr != '"<?>"') t = tagr.substr(1,tagr.length-2); break;
						case "AUTHOR": if(tagr != '"<?>"') a = tagr.substr(1,tagr.length-2); break;
						case "DATE": if(tagr != '"<?>"') dt = tagr.substr(1,tagr.length-2); break;
						case "TYPE": sVersion = "t."+tagr; break;
						case "TIME": tm = tagr; break
					}
				}
			}
			if(bad) sVersion += "/malformed";
			sOption(t); sOption(a,"by: "); sOption(dt,"'"); sOption(tm,"len: ");
			t = parseAtariBinary(p+2); sOption(t[1].length,'binblks:'); sOption(outSz(t[0]),'sz:')
		}
	}

	else if(X.c("'SC68 Music-file / (c) (BeN)jamin Gerard / SasHipA-Dev  '00'SC68'........'SCFN'")) {
		sName = "SC 68000 programmatic chiptune (.SC68)"; bDetected = 1;
		if(X.isVerbose()) {
			hdrl = X.SA(0,0x100).length+1;
			p = hdrl; //header skipped
			t=''; a=''; cp=''; st=''; df=-1; x=0; ef=false; mn = [];
			while (p < X.Sz()) {
				hkhd = X.SA(p,4);
				hksz = X.U32(p+4,_LE);
				p += 0x08; //chunk header = 4xfourcc + 4xsize
				switch (hkhd) {
					case "SC68": if(hksz+hdrl != X.Sz()) sVersion = "malformed"; hksz = 0; break;
					case "SCFN": t = X.SC(p,hksz,'CP1252'); break;
					case "SCDF": df = X.U32(p,_LE)+1; break;
					case "SCMN":
						mn.push(X.SC('CP1252',p,hksz));
						if(st=="" || df==mn.length)
							st = mn[mn.length-1];
						break;
					case "SCAN": a = X.SC(p,hksz,'CP1252'); break;
					case "SCCN": cp = X.SC(p,hksz,'CP1252'); break;
					case "SCEF": ef = true; p = X.Sz(); break
				}
				p += hksz;
			}
			sOptionT(t); if(mn.length > 1) sOption(mn.length,"×");
			sOptionT(mn.join(";"));
			sOptionT(a,"by: "); sOptionT(cp,"(c)"); sOption(outSz(p),"sz:")
			if(!ef) sVersion = "malformed!short"
		}
	}

	else if(X.c("'shro'020000")) {
		sName = "Mario Paint's Shroom module (.SHO)"; bDetected = 1;
		if(X.isVerbose()) {
			sOptionT(X.SA(7,0x20));
			sOptionT(X.SA(0x27,0x20),"by: ")
		}
	}

	else if(X.c("'PSID'") || X.c("'RSID'")) {
		v1 = X.SA(0,1); bDetected = 1;
		if(v1 == "P")  sName = "PlaySID programmatic chiptune (.SID, .PSID)";
		else sName = "RealSID programmatic chiptune (.SID, .RSID)";
		v2 = X.U16(4,_BE);
		sVersion = "v"+Hex(v2);
		bad = '';
		x = X.U16(0x0E,_BE);
		if(!isWithin(x, 1,256)) bad = bad.addIfNone('!badsubsongs');
		else if(x > 1) sOption(x,"×");
		startSong = X.U16(0x10,_BE);
		if(startSong > x) bad = bad.addIfNone('!badstartsong');
		dataOfs = X.U16(0x06,_BE);
		if((v2 == 1 && dataOfs != 0x0076) || (v2 == 2 && dataOfs != 0x007C))
			bad = bad.addIfNone('!baddatap');
		loadAddr = X.U16(0x08,_BE);
		if(v1=="R" && isWithin(loadAddr, 1,0x07E7)) bad = bad.addIfNone('!badloadp');
		initAddr = X.U16(0x0A,_BE);
		if(v1=="R" && !isWithinRanges(initAddr, [[0x7E8,0x9FFF], [0xC000,0xCFFF]]))
			bad = bad.addIfNone('!badinitp');
		flags = X.U16(0x76,_BE);
		if(v1=="R" && ((flags >> 1) & 1) && initAddr > 0)
			bad = bad.addIfNone('!badinitp2');
		sidn = 1;
		if(v2 == 0x4E) sidn += (dataOfs-0x7C) >> 1; else {
			if(v2 >= 3 && !((a2=X.U8(0x7A))&1) && !isWithinRanges(a2, [[0,0x41],[0x80,0xDF]])) sidn++;
			if(v2 >= 4 && !((a3=X.U8(0x7B))&1) && a2 != a3 && !isWithinRanges(a3, [[0,0x41],[0x80,0xDF]])) sidn++;
		}
		var sidt = [0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0]; //upto 16SID
		switch((flags >> 4) & 3) { case 1: sidt[0] = 1; break; case 2: sidt[0] = 2; break; case 3: sidt[0] = 3; break }
		if(v2 >= 3) switch((flags >> 6) & 3) {
			case 1: sidt[1] = 1; break; case 2: sidt[1] = 2; break; case 3: sidt[1] = 3; break; default: sidt[1] = sidt[0] }
		if(v2 == 0x4E) for(i=2,q=0x7C; i < sidn; i++,q+=2) switch((X.U16(q,_BE) >> 4) & 3) {
			case 1: sidt[i] = 1; break; case 2: sidt[i] = 2; break; case 3: sidt[i] = 3; break; default: sidt[i] = sidt[0]
		} else  if(v2 >= 4) switch((flags >> 8) & 3) {
			case 1: sidt[2] = 1; break; case 2: sidt[2] = 2; break; case 3: sidt[2] = 3; break; default: sidt[2] = sidt[0] }
		if(sidn > 1) sVersion += "/"+sidn+"SID";
		sidtt = [0, 0, 0, 0]; //count different chips
		for(i=0; i < sidn; i++) sidtt[sidt[i]]++;
		for(i=0; i < 4; i++) if(sidtt[i]) sVersion += ' #'+['unk','6581','8580','6581&8580'][i] + (sidtt[i]>1? '×'+sidtt[i]:'');
		switch((flags & 0x0C) >> 2) {
			case 1: sVersion += "/PAL"; break;
			case 2: sVersion += "/NTSC"; break;
			case 3: sVersion += "/PAL&NTSC"; break;
		}
		if(bad > 0) sVersion += "/malformed"+bad;
		if(X.isVerbose()) {
			t = X.SC(0x16,0x20,'CP1252'); if(t == "<?>") t = ""; sOptionT(t);
			a = X.SC(0x36,0x20,'CP1252'); if(a == "<?>") a = ""; sOptionT(a,"by: ");
			c = X.SC(0x56,0x20,'CP1252'); if(c == "<?>") c = ""; sOptionT(c);
		}
	}

	else if(X.c("00 FF00FF00 9100FF00 FF008000 92..00967F 01", 0x07)
	  && X.c("9908", 0x1542)) {
		sName = "Tony 'Nissimo' Willams' Sound Images module (.SIG)"; sVersion = 'gen.2'; bDetected = 1
	}

	else if(X.c("0100FEFF09000000'ALIM3'")) {
		sName = "Ruben Ramos 'baktery' Salvador's Skale Tracker module (.SKM)"; bDetected = 1;
		if(X.isVerbose()) sOptionT(X.SA(0x19));
	}

	else if(X.c("'<track'") && (X.fStr(6,0x100,"rowhighliohtingminor=")>0)) {
		sName = "Benjamin 'BeRo' Rousseaux's Picatune module v1 (.SMUFI) or v2 (.PT2)"; bDetected = 1;
		if(X.isVerbose()) {
			s = X.SC(0,0x100,"UTF8");
			t = /.*name="([^"]*)".*/.exec(s);
			if(t != null) sOptionT(t[1]);
			a = /.*author="([^"]*)".*/.exec(s);
			if(a != null) sOptionT(a[1],"by: ");
			spd = /.*speed="([^"]*)".*/.exec(s);
			bpm = /.*bpm="([^"]*)".*/.exec(s);
			if(spd != null) sVersion+=" spd: "+spd[1];
			if(bpm != null) sVersion+=" bpm: "+bpm[1];
		}
	}

	else if(X.c("'SNGs'") && X.Sz() > 0x10 && X.U8(0xA) <= X.U8(7) && X.U8(0xB) <= 0x1E && isWithin(X.U8(0xC),0,0x78)
	  && isWithin(X.U8(0xD),1,30) && isWithin(X.U8(0xE),1,30)) {
	  	//TODO RE reading additional data
	  	// blocks: SMP SYN BNK SNG BLK TMP
		sName = "BlueMoon's Sound Club for DOS module (.SN)"; bDetected = 1;
		if(!(ord=X.U8(7))) sVersion = 'malformed!0ord';
		if(X.isVerbose()) {
			sOptionT(X.SA(0x0F,0x1A));
			lp = X.U8(0xA); gvol = X.U8(0xB)*5; tx = X.U8(0xC); tpb = X.U8(0xD); bpms = X.U8(0xE);
			sOption('rhythm:'+tpb+'/'+bpms+' ord:'+X.U8(7)+' ins:'+X.U8(4)+' gvol:'+gvol+'%')
		}
	}

	else if(X.c("'SN2'") && isWithin(X.U32(0x13),0,0x78) && isWithin(X.U32(0x17),1,30) && isWithin(X.U32(0x1B),1,30)
	  && X.c("'NAM'",0x1F) && X.c("'SEQ'",0x26+X.U32(0x22))) {
		sName = "Bluemoon's Sound Club for Windows module (.SN2)"; bDetected = 1;
		if(X.isVerbose()) {
			sOptionT(addEllipsis(X.SA(0x26,X.U32(0x22)))); sz = X.U32(3)+7;
			lp = X.U32(0x0F); tx = X.U32(0x13); tpb = X.U32(0x17); bpms = X.U32(0x1B);
			//TODO instead of a quartic approximated curve, find out the actual calculation lol
			tempop = Math.round(200 - tx*(4.27246 - tx*(0.0603477 + tx*(-0.000453202 + 1.33871*tx/1000000))));
			p = 0x26+X.U32(0x22); ord = ptn = ins = 0;
			end = false; while(!end && p < X.Sz()) {
				hkhd = X.SA(p,3); hksz = X.U32(p+3); p += 7;
				switch(hkhd) {
				case 'PAT': ptn++; break;
				case 'SEQ': ord = Util.divu64(hksz,4); break;
				case 'INS': ins++; break;
				}
				p += hksz
			}
			sOption('rhythm:'+tpb+'/'+bpms+' tempo: '+tempop+'% ord:'+ord+(lp?' lp:'+lp:'')
				+' ptn:'+ptn+' ins:'+ins+' sz:'+outSz(sz))
		}
	}

	else if(X.c("'SNDH'",0x0C) && (X.isHeuristicScan() || X.c("6000............6000"))) {
		//ref https://sndh.atari.org/files/sndhv21.txt
		sName = "Atari ST Sound Header module (.SND,.SNDH)"; sVersion = "uncompressed"; bDetected = 1;
		if(X.isVerbose()) {
			p = 0x10; title=artist=yr=""; totaltime = i = 0; x = 1;
			while(i < 10 && p < X.Sz()) {
				t = X.SA(p,4); if(t === "TIME") {
					for(j=0; j < x; j++) totaltime += X.U16(p+4+j*2,_BE);
					tlen = 4+x*2-1; if((p+tlen+1)%2) tlen++;
				}
				else { t = X.SA(p,Math.min(X.Sz()-p),0x100); tlen = t.length }
				if(tlen >= 4) {
					hd = t.substr(0,4); tag = t.substr(4,tlen);
					switch(hd) {
						case "TITL": if(tag.substr(0,7) != "Unknown") title = tag; break;
						case "COMM": if(tag.substr(0,7) != "Unknown") artist = tag; break;
						case "YEAR": yr = tag; break;
						case "HDNS": i = 8; break;
						default: if(/##\d\d/.test(hd)) x = hd.slice(2,4);
							else if(/#!\d\d/.test(hd)) if((p+tlen+1)%1) tlen++;
							break;
					}
				}
				p += tlen+1; i++
			}
			sOptionT(title); if(x > "01") sOption(x,"×"); sOptionT(artist,"by: "); sOptionT(yr,"'");
			if(totaltime) sOption(secondsToTimeStr(totaltime,"time:"));
		}
	}

	else if(X.c("'FMC!'")) {
		sName = "Lasse 'Faust' Öörni's Faust Music Creator module (.SNG)"; bDetected = 1;
		if(X.isVerbose())
			sOptionT(X.SA(4,20));
	}

	else if(/GTS[25!]/.test(X.SA(0,4)) && X.U8(0x64) <= 32) {
		//ref https://github.com/leafo/goattracker2/blob/a78e7e5273bfbeb17c3777b1f5ceb63b8e6b4667/src/gsong.c#L177
		sName = "Lasse 'Faust' Öörni's GoatTracker module (.SNG)"; bDetected = 1;
		nV = X.U8(3)-0x30; if(nV < 0) nV = 1;
		if(nV >= 2) sVersion = 'v2.'+nV; else sVersion = 'v1.x';
		x = X.U8(0x64); p = 0x65; ords = []; ptn = mptn = 0; bad = '';
		for(ordc = 0; p < X.Sz(); ) { q = X.U8(p); if(!X.c("FF",p+q)) break;
			for(i=0, p++; i < q; i++) if((t=X.U8(p++)) <= 0xCF && t+1 > mptn) mptn = t+1;
			ordc++; ords.push(q); p++ }
		ch = 3; if(ordc == x*6) { sVersion += ' stereo'; ch = 6 }
		else if(ordc != x*3) bad = bad.addIfNone('!badordcnt');
		if(bad.length) sVersion = sVersion.appendS('malformed'+bad,'/');
		ins = nV >= 2? X.U8(p++): 31; inss = [];
		for(i=0; i < ins; i++) {
			t = X.SC(p+(nV > 1? 9: 8),0x10,'CP1250').trim(); if(t.length) inss.push(t);
			if(nV > 1) p += 0x19; else p += 0x18+2*(X.U8(p+7)>>1);
		}
		for(i=0; i < (nV == 1? 0: nV == 2? 3: 4); i++) p += 1+2*X.U8(p); //skip the tables
		ptn = X.U8(p++); for(i=0; i < ptn; i++) p += 1 + (nV == 1? 1: 4) * X.U8(p);
		if(nV == 1 && X.Sz() != p) p += 0x100; //the (optional?) filtertable at the end
		if(X.isVerbose()) {
			sOptionT(X.SC(4,0x20,'CP1250'));
			if(x > 1) sOption(x,'×');
			sOptionT(X.SC(0x24,0x20,'CP1250'),'by: ');
			sOptionT(X.SC(0x44,0x20,'CP1250'));
			sOptionT(addEllipsis(inss.join(', '),0xA0),'ins/msg:"','"');
			for(i=0, ordc = []; i < ords.length/ch; i++) {
				for(c = j = 0; j < ch; j++) c += ords[i*ch+j];
				ordc.push(c)
			}
			sOption('ch:'+ch+' ord:'+ordc.join('+')+' ptn:'+(ptn == mptn? mptn: '/'+ptn)+' ins:'+ins+' sz:'+outSz(p))
		}
	}

	else if(X.c("'ObsM'")) {
		sName = "Jonne Valtonen's SNG Player module (.SNG)"; bDetected = 1;
		if(X.U8(0x0B)) sVersion += "compressed";
		len = X.U16(4,_LE); if(len>X.Sz()) sVersion += "malformed";
		if(X.isVerbose()) {
			//ref https://github.com/adplug/adplug/blob/master/src/sng.cpp
			start = X.U16(6,_LE);
			loop = X.U16(8,_LE); delay = X.U16(0x0A,_LE);
			sOption("len:"+Hex(len)+" start:"+Hex(start)+" loop:"+Hex(loop)+" delay:"+delay)
		}
	}

	else if(X.c("'RJP'3.'SMOD'")) {
		sName = "Richard Joseph's module (.SNG)"; bDetected = 1;
		sVersion = "v"+X.SA(3,1)
	}

	else if(X.c("'SYNC'") || X.c("'SYNB'")) {
		sName = "Synder SNG-player module (.SNG)"; bDetected = 1;
		sVersion = "ver."+X.SA(3,1);
		if(X.isVerbose()) sOption(X.SA(0x10,0x200))
	}

	else if(X.c("'SYND'....'S0'")) {
		sName = "Synder Tracker module (.SNG)";  bDetected = 1; //TODO unpack & read tags
		sVersion = "ver."+X.SA(3,1)
		//if(X.isVerbose()) sOption(X.SA(0x10,0x200))
	}

	else if(X.c("'SYND'") || X.c("'SYNF'") || X.c("'SYNH'")) {
		sName = "Synder SNG-player Stereo module (.SNG)"; bDetected = 1;
		sVersion = "ver."+X.SA(3,1);
		if(X.isVerbose()) sOption(X.SA(0x10,0x200))
	}

	else if(isWithin(t = ((X.U8(0)+1) << 4) + ((X.U8(1)+1) << 7) + 0x365,0x366,X.Sz())
	  && (/df\d:/.test(X.SA(t,4)) || /[sS]amples/.test(X.SA(t,7)))) {
		//from https://gitlab.com/uade-music-player/uade/-/raw/master/amigasrc/players/wanted_team/ZoundMonitor/src/ZoundMonitor_v1.asm
		sName = "AJ [Activas]'s ZoundMonitor module (.SNG + Samples/)"; bDetected = 1;
		if(X.isVerbose()) {
			ptn = X.U8(1)+1; ord = X.U8(3)+1; spd = X.U8(4);
			for(p=5,smp=0; p < 0x32F; p+=0x36) if(X.U8(p+4)) smp++;
			sOption('spd:'+spd+' ord:'+ord+' ptn:'+ptn+' smp:'+smp
				+' len '+secondsToTimeStr(1+Util.divu64(spd*ord*32,50))+' sz:'+outSz(t+101))
		}
	}

	else if(X.c("'RJP'3. 0000 0000")) {
		sName = "Richard Joseph's module instruments (.INS)"; bDetected = 1;
		sVersion = "v"+X.SA(3,1)
	}

	else if(X.c("'SNES-SPC700 Sound File Data'")) {
		// ref http://snesmusic.org/files/spc_file_format.txt   and snippets from
		// http://www.alpha-ii.com/Source/SAmp310s.rar/snes/id666.cpp, .h
		sName = "Nintendo SNES SPC chiptune (.SPC)"; bDetected = 1;
		sVersion = "v0."+X.U8(0x24);
		if(X.isVerbose()) {
			id666inhdr = X.U8(0x23)==0x1A;
			t=""; a=""; c=""; g=""; dumper=""; emu="";
			preferBin = false;
			if(id666inhdr){
				t = X.SA(0x2E,0x20);
				g = X.SA(0x4E,0x20);
				dumper = X.SA(0x6E,0x10);
				dumpdate = X.SA(0x9E,11);
				emu = X.U8(0xD2);
				if(emu>=0x30 && emu<=0x39)
					emu -= 0x30;
				switch(emu) {
					case 1: emu = "ZSNES"; break;
					case 2: emu = "Snes9x"; break;
					case 3: emu = "ZST2SPC"; break;
					case 4: emu = "ETC"; break;
					case 5: emu = "SNEShout"; break;
					case 6: emu = "ZSNESW"; break;
					default: emu = ""
				}
				c = X.SA(0x7E,0x20);
				slen = X.SA(0xA9,3); lp=X.SA(0xAC,4);
				if((slen+lp+dumpdate).length < 5) {
					chnDis = X.U8(0xD1);
					if(chnDis == 1 && emu == "") bin = true; else bin = preferBin;
				} else if( /[0-9/]*/.test(slen+lp+dumpdate) ) { //id666 text format
					songlen = Number(slen); //in seconds
					a = X.SA(0xB1,0x20);
				} else {
					bin = true;
					//check songlen bitness
					songlen = X.U8(0xA9)<<16+X.U8(0xAA)<<8+X.U8(0xAB); //in seconds
					a = X.SA(0xB0,0x20);
				}
			}
			else
				t = X.SA(0x30,0x14);
			sOption(t); sOption(g,"for: "); sOption(a,"by: "); sOption(c);
			if(emu != "") sVersion += " "+emu;
		}
	}

	else if(X.c("'STK1.0SONG'") && [1,2,3].indexOf(X.U8(0x34)) >= 0
	  && X.U8(0x35) <= X.U8(0x3A) && X.U8(0x36) <= X.U8(0x35)
	  && X.U8(0x38) <= 0x3F && X.U8(0x39) <= 5) {
		// format details here: https://modland.com/pub/documents/format_documentation/STarKos%20(.sks).md
		sName = "Julien 'Targhan' Nevo's STarKos module (.SKS)"; bDetected = 1;
		bad = "";
		switch(X.U8(0x39)) {
		case 0: hz = 13; break;  case 1: hz = 25; break;  case 2: hz = 50; break;
		case 3: hz = 100; break;  case 4: hz = 150; break;  case 5: hz = 300; break;
		default: hz = 0; bad = bad.addIfNone("!badRepFreq")
		}
		xpos = X.I8(0x37).toString(); if(xpos[0] != "-") xpos = "+"+xpos;
		spd0 = X.U8(0x38); ord = 1+X.U16(0x3A);
		ptn = -1; rows = ptnxpos = 0; p = 0x3C;
		for(i=0; i < ord*4; i++) {
			if((i%4) == 3) rows += X.U8(p)+1;
			else { t = X.U8(p); if(ptn < t) ptn = t; if(X.I8(p+1)>>1) ptnxpos++ }
			p += 2
		} ptn++;
		insns = [];
		const ifHard = 0x80, ifPitch = 0x40, ifArp = 0x20, ifNoiseEtc = 0x10;
		for(ins = 0; p < X.Sz() && ins < 0x100; ins++) {
			insn = X.U16(p); p += 2;
			if(insn == 0xFFFF) break;
			ip = p; isz = X.U16(ip); p += 4;
			iend = 1+X.U8(p+2);
			p += 5;
			iname = X.SA(p,8).trim(); p += 8; if(iname != "") insns.push(iname);
			if(X.isDeepScan())
			for(l = 0; l < iend; l++) {
				x = X.U8(p++);
				if(!x) continue;
				if((x & ifHard)) {
					y = X.U8(p++); if(x & 8) p++; if(y & 0x40) p++; if(x & 2) p++;
					if(x & 4) p += 2; if(x & 0x10) p += 2; if(x & 0x20) p += 2
				} else {
					if(x & ifNoiseEtc) { y = X.U8(p++);
						if(y & 0x40) { p += 2;
							if(x & (ifArp | ifPitch)) { bad = bad.addIfNone("!badinsflags"); ins=l=0xFFFE; break }
						}
					}
					if(x & ifArp) p++; if(x & ifPitch) p += 2
				}
			} else p = ip+isz;
		}
		for(sptn = 0; p < X.Sz(); sptn++) {
			curptn = X.U16(p); p += 2; if(curptn == 0xFFFF) break;
			p += X.U8(p)
		}
		if(X.isDeepScan()) notecnt = 0;
		if(p < X.Sz() && ins < 0xFFFE) for(i = 0; i <= ptn; i++) {
			curptn = X.U16(p); p += 2;
			if(curptn != 0xFF && curptn != 0xFFFF && curptn > 0x200) { bad = bad.addIfNone("!badnptn"); break }
			if(curptn == 0xFFFF) break;
			pp = p; psz = X.U16(pp);
			if(X.isDeepScan()) {
				p += 2; var pvol = pins = -1, lc = 0;
				while(p < Math.min(X.Sz(), pp+psz)) {
					var ppitch = 0, lnp = p,
						pfnote = pfvol = pfpitch = false;
					x = X.U8(p++);
					if(x == 0xFF) break;
					else if(x & 0x80) lc += x & 0x7F;
					else if(x >= 0x60)
						switch(x & 0xF) {
						case 0: pfvol = true; pfpitch = false; pvol = 0xF - X.U8(p++);
							if(pvol < 0) { bad = bad.addIfNone("!badptnvol0"); i = ptn; p = pp+psz; break; }
						case 1: pfvol = false; pfpitch = true; ppitch = - X.U8(p++); break;
						case 2: pfvol = pfpitch = true;  pvol = 0xF - X.U8(p++);
							if(pvol < 0) { bad = bad.addIfNone("!badptnvol2"); i = ptn; p = pp+psz }
							ppitch = - X.U8(p++); break;
						case 3: pfnote = true; pnote = "rst"; break;
						case 4: pfnote = true; pnote = "spl"; pins = X.U8(p++); break;
						}
					else {
						pfnote = true;
						y = X.U8(p++);
						pfvol = !(y & 0x40);
						if(pfvol) pvol = 0xF - (y & 0xF);
						if(pins < 0 || !(y & 0x20)) pins = X.U8(p++);
						pfpitch = y & 0x10;
						if(pfpitch) ppitch = - X.I8(p++);
					}
					if(pfnote) notecnt++;
					lc++
				} //single pattern cycle
			} else p = pp+psz;
		} //patterns cycle
		if(p > X.Sz()) bad = bad.addIfNone("!short");
		if(bad != "") sVersion = sVersion.appendS("malformed"+bad,"/");
		if(X.isVerbose()) {
			sOptionT(X.SA(0x14,0x20));
			sOptionT(X.SA(0x0A,0x0A),"by: ");
			sOption("spd0:"+spd0+(xpos!="+0"?" xpos:"+xpos:"")+" ord:"+ord+" ptn:"+ptn+"+"+sptn+" ins:"+ins
				+(ptnxpos?" ptn.xpos:"+ptnxpos:"")+" smp.ch:"+X.U8(0x34)+" rep.freq:"+hz+"Hz"
				+" loop:"+(X.U8(0x36) ? Hex(X.U8(0x36))+"-" : "")
				+Hex(X.U8(0x35))+(X.isDeepScan()?" notes:"+notecnt:" rows:"+rows)+" sz:"+p)
		}
	}//.SKS

	else if(!X.U8(0) && X.c("'SK10'",0x80) && [1,2,3].indexOf(X.U8(0x86)) >= 0
	  && [13,25,50,100,150,300].indexOf(X.U16(0x87)) >= 0 && X.U16(0x18) == X.U16(0x40)) {
		//from https://web.archive.org/web/20240331033458_nf/https://www.grimware.org/doku.php/documentations/software/starkos/start#tweaking.the.song
		sName = "STarKos module (.BIN)"; sVersion = "compiled/ofs:80h"; bDetected = 1;
		crc = 0; for(i=0;i<67;i++) crc += X.U8(i); crc &= 0xFFFF;
		if(crc != X.U16(0x43)) sVersion += "/malformed!badCRC";
		if(X.isVerbose()) {
			sOption(X.SA(1,8).trim()+"."+X.SA(9,3).trim());
			sOption(X.U16(0x87)+"Hz");
			sOption(Hex(X.U16(0x84)),"base:");
			sOption(outSz(X.U24(0x40)+0xBC),"sz:");
		}
	}

	else if(X.c("'Nu!SOPROL!'", 0x22)) {
		sName = "Holger Gehrmann's Sound Programming Language/SOPROL module (.SPL)"; bDetected = 1;
		if(X.isVerbose()) {
			t = X.SA(0x58,0x100);
			a = X.SA(0x58+t.length+1,0x100);
			c = X.SA(0x58+t.length+a.length+2,0x100);
			sOptionT(t); sOptionT(a,"by: "); sOptionT(c)
		}
	}

	if(/SPM[\x01-\x02]/.test(X.SA(0,4))) {
		//ref http://aminet.net/mus/edit/stonefree1.lha
		sName = "Emmanuel Marty & Michael Lavaire's Stonetracker module (.SPM)";
		sVersion = "v"+X.U8(3); bDetected = 1;
		if(X.isVerbose()) {
			sOptionT(decAnsi(4,0x20,CPAmiga));
			//TODO find chunks and read the extra info
		}
	}

	if(/SPS[\x01-\x02]{2}/.test(X.SA(0,5)) && X.c("'psn'",6+0x20*(smp=X.U8(5)))) {
		sName = "Emmanuel Marty & Michael Lavaire's Stonetracker sample bank (.SPS)";
		sVersion = "v"+X.U8(3); bDetected = 1;
		if(X.isVerbose()) {
			for(i=smpsz=0,smps=[]; i < smp; i++)
				if((t=decAnsi(0x20*i+6,8,CPAmiga).trim()).length) smps.push(t);
			sOption(addEllipsis(smps.join(' ')),'smp/msg:"','"'); sOption('smp:'+smp)
		}
	}

	else if(X.c("'SPU'00") || X.c("'SPU1'") ||
		(X.fSig(X.Sz()-6,TOEOF,"1D80FF"))>-1) {
		bDetected = 1; sName = "Eternal SPU chiptune (.SPU)";
		if(X.c("'SPU'")) {
			if(!X.U8(3)) sVersion = "v0"; else sVersion = "v1"; }
		else sVersion = "headerless";
		if(X.isVerbose() && (sVersion != "headerless")) {
			game = X.SC(4,0x40,"Shift_JIS");
			title = X.SC(0x44,0x40,"Shift_JIS");
			sOptionT(title); sOptionT(game, "game: ");
			artist = X.SC(0x84,0x20,"Shift_JIS"); sOptionT(artist,"by: ");
			cmt = X.SC(0xA4,0xF00,"Shift_JIS"); sOptionT(cmt);
		}
	}

	else if(X.c("'SPEEDY-SYSTEM'")) {
		bDetected = 1; sName = "Speedy System module (.SS)"; sVersion = "v1"
	}

	else if(X.c("'ZXAYST11'")) {
		sName = "Sound Tracker module (.ST1, .ST11)"; bDetected = 1;
		sVersion = "v1.1/uncompiled";
		if(X.isVerbose()) {
			t_= X.U8(0x17); t = X.SA(0x18,t_-1).trim();
			if(t != "Some SoundTracker Song") sOption(t);
		}
	}

	else if(X.c("'STP3'")) {
		sName = "Soundtracker Pro II module (.STP)"; bDetected = 1;
	}

	else if(X.c("48E7FFFE 4DFA.... 4A2E") && isWithin(X.I8(6),-1,6) && [0x61,0x66].indexOf(X.U8(0xC)) >= 0) {
		sName = "SUNtronic module (.SUN)"; bDetected = 1
	}

	else if(X.c("'SVOX'00000000")) {
		sName = "SunVox module (.SUNVOX)"; bDetected = 1;
		if(X.isVerbose()) {
			p=8;
			t=''; bpm=0; spd=0; tme=0; ptn=0; blk=0;
			while (p < X.Sz()) {
				hkhd = X.SA(p,4); if(charStat(hkhd).indexOf('allasc') < 0) break;
				hksz = X.U32(p+4,_LE); p += 8;
				switch(hkhd) {
					case "VERS":
						nV = X.U32(p,_LE); aV=[];
						for(i=0; i < 4; i++) aV[3-i] = (nV>>(8*i))&0xFF;
						sVersion += "v"+aV.join("."); break;
					case "BVER":
						nB = X.U32(p,_LE);
						if(nB != nV) { aV = [];
							for(i=0; i < 4; i++) aV[3-i] = (nB>>(8*i))&0xFF;
							sVersion += "/v"+aV.join(".");
						}
						break;
					case "NAME": t = X.SA(p,hksz); break;
					case "BPM ": bpm = X.U32(p,_LE); break;
					case "SPED": spd = X.U32(p,_LE); break;
					case "TIME": tme = X.U32(p,_LE); break;
					case "SNAM": blk++; break;
					case "PDTA": ptn++; break;
				}
				p += hksz;
			}
			sOptionT(t);
			if(bpm > 0) sOption(bpm,"bpm:"); if(spd > 0) sOption(spd,"spd:");
			if(tme > 0) sOption(tme,"len:"); if(ptn > 0) sOption(ptn,"ptn:"); if(blk > 0) sOption(blk,"blk:");
			sOption(outSz(p),'sz:')
		}
	}

	else if(X.c("'SymM'")) {
		//ref https://github.com/OpenMPT/openmpt/blob/master/soundlib/Load_symmod.cpp
		// & https://github.com/patricklyte/SymphonieFx
		sName = "Patrick Meng's Symphonie module (.SYMMOD)"; bDetected = 1;
		sVersion = "v"+X.U32(4,_BE);
		if(X.isVerbose()) {
			p=8;
			title=''; bpm='?'; done=false; ch=0; len=0; extsmp=false; pro=false;
			while (p < X.Sz()) {
				hkhd = X.I32(p,_BE); hkhx = Hex(X.U32(p,_BE)); hksz = 4;
//_l2r('symmod',p,hkhd+' / '+hkhx)
				if(done || charStat(hkhd).indexOf('allasc') < 0) break;
				p += 0x04; //chunk header = 4xtype (the following will change by chunk type)
				switch (hkhd) {
					case -16:
						pklen=X.U32(p,_BE); hksz+=pklen; o=4; //InfoText RLE-COMPRESSED YO
						if(pklen>=10 && X.c(p+o,"'PACK'FFFF")) if(!X.isDeepScan()) { p = 0; break } else {
							o+=6; unplen = X.U32(p+o,_BE); o+=4;
							maxlen = pklen-10; if(0xFFFFFFFF/170 >= maxlen) maxlen *= 170; else maxlen = 0xFFFFFFFF;
							if(unplen > maxlen) unplen = maxlen;
							done = false; ofs = 0; left = unplen;
							while(!done && o < hksz) {
								tp = X.I8(p+o); o++;
								switch(tp) {
								case 0:
									l = X.U8(p+o); o++;
									if(left >= l) {
										t += X.SA(p+o,l);
										o += l; left -= l;
									} else done=true;
									break;
								case 1:
									l = X.U8(p+o); o++;
									dw = X.SA(p+o,4); o += 4;
									if(left >= l*4 && o < pklen) {
										left -= l*4;
										while(l--) t += dw;
									} else done = true;
									break;
								case 2:
									dw = X.SA(p+o,4); o += 4;
									if(left >= l*4 && o<pklen) {
										unp += dw+dw;
										left -= 8;
									} else done = true;
									break;
								case 3:
									l = X.U8(p+o); o++;
									if(left >= l) left-=l;
									else done = true;
									break;
								case -1: done = true; break;
								default: sVersion += "/malformed"; done = true; break;
								}//switch
							}
						} else //if no RLE sig:
						title = X.SC(p+o,hksz,'CP1252'); break;
					case -1: ch = X.I32(p,_BE); break; //NumChannels
					case -2: len = X.I32(p,_BE); if(len > 1024) len = "malformed"; break; //TrackLength
					case -3: case -4: case -5: break;
					case -7: extsmp = true; break; //ExternalSamples
					case 10: case 11: case 12: pro=true; break; //Sample Boost/Detune/Phase
					case -6: bpm = Math.round(1.24*Math.min(X.I32(p,_BE),800)); break; //Tempo
					case -12: hksz = 0; break; //EmptySample
					case -10: case -11: case -13: case -14: case -15: case -17: case -18:
					case -19: case -20: case -21: hksz+=X.I32(p,_BE); break;
					default: _l2r('symmod',p,hkhd+"/"+hkhx+" ("+Hex(hksz,8)+"): ?!?!?!?!?!");
				}
				p += hksz;
			}
			sOption(title);
			sOption('ch:'+ch+' bpm:'+bpm+' len:'+len+(done?'sz:'+outSz(p):''));
			if(extsmp) sOption("extsmp"); if(pro) sVersion+="/Pro"
			}
	}

	else if(X.c("'Synth'") && !X.c("'esi'",5)) {
		sName = "C. 'Mr Soundwave' Herbst & B. MIkic/BrainTrace Design's Synthesis module (.SYN)"; bDetected = 1;
		if(X.c("'Synth'",0x1F0E)) {
			sVersion = "v"+X.SA(0x1F13,3);
			if(X.isVerbose()) {
				sOptionT(X.SA(0x1F32,0x1B));
				sOptionT(X.SA(0x1F4E,0x100))
			}
		} else {
			sVersion = "v"+X.SA(5,3);
			if(X.isVerbose()) {
				sOptionT(X.SA(0x24,0x1B));
				sOptionT(X.SA(0x40,0x100))
			}
		}
	}

	else if(X.c("'SYNTRACKER-SONG:'00")) {
		sName = "Bastian 'flink'/'twiCe' Spiegel's SynTracker module (.SYNMOD)"; bDetected = 1;
		if(X.isVerbose()) {
			t1 = X.SC(0x14,0x20,'CP1252'); sOptionT(t1,"title/inst: ");
			t2 = X.SC(0x34,0x20,'CP1252'); sOptionT(t2);
			t3 = X.SC(0x54,0x20,'CP1252'); sOptionT(t3);
		}
	}

	else if(X.c("'T0AST'")) {
		// ref T0AST.src.zip/ SoundEnginePlayer.pas, synth.pas
		// Kudos to BeRo for the tracker sources
		sName = "Benjamin 'BeRo' Rousseaux's The 0ok Amazing Synth Tracker module (.T0AST)"; bDetected = 1;
		if(X.c("'0OK'",5)) { nV = 1; sVersion = "v1"; } else
		if(X.c("010001",5)) { nV = 2; sVersion = "v2"; } else
			{ nV = -1; sVersion = "v.unk"; }
		function info() {
			if(nV == 1) p = 0x1DA;
			else if(nV == 2) p = 0x34A;
			else return;
			drummode = X.U8(p++);
			chipmode = X.U8(p++);
			chs = X.U8(p++); ch = 0;
			if(chs > 16) { sVersion += "/malformed"; return; }
			for(i=0; i<16; i++)
				if(X.U8(p++)) { ch++; p += 3; }
			ins = 0;
			for(i=0; i<16; i++)
				if(X.U8(p++)) { ins++; p += 25; }
			ord = X.U16(p,_LE); p += 2+ord;
			ptn = 0; notes = 0;
			for(k=0; k<=255; k++)
				if(X.U8(p++)) { //gotta do what you gotta do
					ptn++;
					for(i=0; i<chs; i++) {
						j = 0;
						while(j < 0x40) {
							b = X.U8(p++);
							if(b & 0x80) { //RLE!
								c = b & 0x7F;
								if(c) {
									p++;
									while(c && j < 0x40) { notes++; j++; c--; }
								}
								else j++;
							}
							else { //just skipping the note parameters
								if(b&0x01) p++; if(b&0x02) p++; if(b&0x04) p++;
								if(b&0x08) p++; if(b&0x10) p++; if(b&0x20) p++;
								if(b&0x40) {
									c = X.U8(p++);
									while(c && j < 0x40) { notes++; j++; c--; }
								}
								else { notes++; j++; }
							}
						}
					}
				}
			loop = X.U8(p++);
			t_ = X.U32(p,_LE); p += 4;
			t = X.SC(p,t_,'CP1252'); p += t_;
			a_ = X.U32(p,_LE); p += 4;
			a = X.SC(p,a_,'CP1252'); p += a_;
			c_ = X.U32(p,_LE); p += 4;
			c = X.SC(p,c_,'CP1252'); p += c_
			sOption(t);
			sOption(addEllipsis(a,0x80),"by: ");
			sOption(addEllipsis(c,0x100),'msg:"','"');
			sOption("ch:"+ch+" ins:"+ins+" ord:"+ord+" ptn:"+ptn+" notes:"+notes+" loop:"+loop+" sz:"+outSz(p));
		}
		if(X.isVerbose() && nV != -1) info();
	}

	else if(X.c("'T0ASTINS'")) {
		sName = "Benjamin 'BeRo' Rousseaux's The 0ok Amazing Synth Tracker instrument file"; bDetected = 1;
		if(X.isVerbose()) sOption(outSz(8+25),"sz:")
	}

	else if(X.c("4EF900...... 4EF900...... 4EF900...... 4EF900...... 4EF900...... 4EF900...... 4EF900...... 4EF900")
		&& X.U32(0x2C,_BE)-X.U32(0x2,_BE) == 0xA0
	  || 
		X.c("601A") && (X.c("1010",0x1C) || X.c("1012",0x1C)) && (t=X.I32(0x22,_BE)) > 0
	  	&& t == X.I32(0x26,_BE) && t == X.I32(0x2A,_BE) && t == X.I32(0x2E,_BE)
	  ) {
		sName = "Tim Follin & Mike D.'s Follin Player II module (.TF)";
		sVersion = 'f.'+(X.U8(0) == 0x4E?'0':'1'); bDetected = 1
}

	else if(X.c("'TFMD'")) {
		sName = "Shiru's TFM Music Maker tune (.TFD)"; bDetected = 1;
		if(X.isVerbose()) {
			p = 4;
			t = X.SA(p,0x200); //I've seen CP1251 and CP936 already, nobody seems to care
			p = X.fSig(p,0x200,"00")+1;
			a = X.SA(p,0x200);
			p = X.fSig(p,0x200,"00")+1;
			c = X.SA(p,0x200);
			sOptionT(t); sOptionT(a,"by: "); sOptionT(c);
		}
	}

	else if(X.c("'TFMfmtV2'")) { // this format is very similar to VGE
		sName = "Shiru's TFM Music Maker module (.TFE)"; bDetected = 1;
		if(X.isVerbose()) {
			p = 0;
			//WELP, time to read the entire file, or at least the fun part
			x = [], tobuf = true, p = 0, b = 0x3F; // '?' by default. If this pops up, the file's garbled
			for(n=0, c = X.U8(p++); n < 0x223DD9 && p < X.Sz(); ) {
				// The logic is:
				// a byte is read and if it's not 0x80, put to buffer (and memorised)
				// if the following byte is 80, it's RLE:
				// 	 - the following byte gives 7 bits of the number of repeats of the memorised byte
				//   - if the length byte's bit 7 is set, this is the last byte, otherwise adds the next higher 7 bits
				//	 - if the length is 0, store 0x80
				//	 - else store the memorised byte repeats-1 times
				if(tobuf && n > 0x312) tobuf = false;
				if(c != 0x80) { b = c; n++; c = X.U8(p++); if(tobuf) x.push(b) }
				else {
					c = X.U8(p++); next = true; reps = shl = 0;
					while (next && shl < 57 && p < X.Sz()) { //read the vari-len int
						next = !(c & 0x80); c &= 0x7F; reps |= Util.shlu64(c,shl); shl += 7; c = X.U8(p++)
					}
					if(!reps) { if(tobuf) x.push(0x80); n++ } //special case for the RLE character
					else { reps -= 1; n += reps; shl = 0; if(tobuf) for(j=0; j < reps; j++) x.push(b) }
				}
			} p--;
			spd1 = x[8]; spd2 = x[9]; intlv = x[0xA]; ord = x[0xB]; if(!ord) ord = 256; lp = x[0xC];
			d1 = x[0x0D]+(x[0x0E] << 8); d2 = x[0x0F]+(x[0x10] << 8); saves = x[0x11]+(x[0x12] << 8);
			d1 = '20'+(d1&0x7F).padStart(2,'0')+'-'+(((d1>>7)&0xF)+1).padStart(2,'0')+'-'+(((d1>>11)&0x1F)).padStart(2,'0');
			d2 = '20'+(d2&0x7F).padStart(2,'0')+'-'+(((d2>>7)&0xF)+1).padStart(2,'0')+'-'+(((d2>>11)&0x1F)).padStart(2,'0');
			auth = decEncoding(x.slice(0x13,0x52),CP1251); //I've seen CP1251 and CP936 already, but yeah
			title = decEncoding(x.slice(0x53,0x94),CP1251);
			cmt = decEncoding(x.slice(0x93,0x214),CP1251);
			for(i=ptn=0; i < ord; i++) if(x[i+0x213]+1 > ptn) ptn = x[i+0x213]+1;
			sz = n == 0x223DD9 ? p : -1; if(sz < 0) sVersion = sVersion.appendS('malformed:'+Hex(n),'/');
			sOptionT(title); sOptionT(auth,'by: '); sOptionT(cmt);
			sOption('on: '+d1+(d1 != d2? ' to '+d2: ''));
			sOption('spd:'+spd1+'/'+spd2+' ord:'+(lp?lp+'-':'')+ord+' ptn:'+ptn+" intlv:"+intlv+' saves:'+saves+(X.isDeepScan()? ' sz:'+outSz(sz):''));
		}
	}

	else if(X.Sz() >= 0x386 && isWithin(X.U8(0x25),1,4) && (bin=parseAtariBinary())[0] > 0
	  && X.c("0E158D",6) && X.c("8D150E",0x1A) && X.U16(0x1D) == bin[1][0][1]
	  && (X.c("'TMC SONG FILE 2.0'",9) || X.c("D4CDC3A0 D3CFCEC7A0 C6C9CCC5A0 B2AEB0",9))) {
		//ref http://atariki.krap.pl/index.php/TM2
		sName = "Marcin 'Jaskier' Lewandowski's Theta Music Composer (.TM2)"; sVersion = 'v2.0'; bDetected = 1; bad = '';
//		sOption(decAnsi(6,0x17,CPATASCII,0,Chars0to1FATASCII))
		for(ins=0,p0=0x10000,p=0x86; p < 0x106; p++) {
			t = ((X.U8(p+0x280) << 8) | X.U8(p))-X.U16(2)+6; if(t < 0) break;
			if(t > 0)
				if(!isWithin(t,0x380,bin[1][0][1]+6)) bad = bad.addIfNone('!badinsp');
				else { if(p0 > t) p0 = t;  if(X.U8(t) != 0xFF) ins++ }
		}
		for(ptn=0,p=0x106; p < 0x206; p++) {
			t = ((X.U8(p+0x100) << 8) | X.U8(p))-X.U16(2)+6; if(t < 0) break;
			if(t > 0)
				if(!isWithin(t,0x380,bin[1][0][1])) bad = bad.addIfNone('!badptnp');
				else { if(p0 > t) p0 = t; if(X.U8(t) != 0xFF) ptn++ } //some patterns may be actually used despite being empty
		}
		for(ord=(p0-0x386)/0x11,p=0x386,pt=-1; p < p0; p += 0x11) {
			for(q=p+1; q < p+0x11; q += 2) if(X.U8(q) >= pt) pt = X.U8(q)+1; //actually used pattern number
			if(isWithin(X.U8(p+16),0x41,0x7F)) bad = bad.addIfNone('!badord');
			if(X.I8(p+16) <= 0) break //loop or end
		}
		if(bad.length) sVersion = sVersion.appendS('malformed'+bad,'/');
		if(X.isVerbose()) {
			for(p=0x26, t=''; p < 0x86; p += 0x20)
				t = t.appendS(decAnsi(p,0x20,CPATASCII,0,Chars0to1FATASCII).trim(),' | ');
			sOption(t);
			if(!(t=X.U8(0x1F))) t = 'mono';  else if(isWithin(t,1,0x3F)) t = 'stereo';
			else if(isWithin(t,0x40,0x7F)) t = 'RMT stereo'; else t = 'quadro';
				sOption('ch:'+t+' spd0:'+X.U8(0x24)+' ticks:'+X.U8(0x25)+' ord:'+ord
				+' ptn:'+ptn+(pt!=ptn? '/'+pt:'')+' ins:'+ins+' sz:'+outSz(bin[0]))
		}
	}

	else if(X.c("0000050F0000050F0000050F0000050F0000050F0000050F0000050F0000050F0000050F0000050F0000050F0000050F", 0x14)
	  && X.c("FFFF001000000030000000", 0x181)) {
		sName = "N.J. Luuring Jr.'s The Musical Enlightenment module (.TME)"; bDetected = 1;

	}

	else if(X.Sz() >= 0x3000 && X.c("'TRK01/TV.ES.'")) {
		sName = "Adam Davidson & Ramjet & Toxic Volume's RamTracker module (.TRK)"; bDetected = 1;
		if(X.isVerbose()) {
			sOptionT(X.SC(0x0F,0x20,'CP850'));
			sOptionT(X.SC(0x2F,0x20,'CP850'),"by: ");
			p = 0x453; ptn = -1; ord = 0;
			for(;;) { t = X.U8(p++); if(t >= 0xFE || p > X.Sz()) break;
				ord++; if(ptn < t) ptn = t }
			ptn++;
			sOption("ord:"+ord+" ptn:"+ptn)
		}
	}

	else if(X.c("'S'8F'NG.'") && /[BW][48]/.test(X.SA(5,2))
	  && (pp=X.fSig(8,0x80*(ch=X.U8(6)-0x30),"DD48")) > 0 && !(pp % 2)) {
		//ref the eagleplayer BladePacker(?!)
		sName = "Unique Development/BladePacker's module (UDS.+SMP.)"; bDetected = 1;
		if(X.isVerbose()) {
			ptn = mptn = 0; maxp = Math.min(X.Sz(), 0x10000);
			bw = X.SA(5,1) == 'B'? 1: 2; ord = (pp-8) >> (bw+1);
			for(p = 8; p < pp; p++) if((t=X.U8(p)+1) > mptn) mptn = t;
			p += 2; t = 0;
			if(bw == 1) for(; X.U16(p,_BE) != 0xD8F1 && p < maxp; ) { //for() twice for speed opt
				if((q=X.U8(p++)) > t) t = q; if((q=X.U8(p++)) > t) t = q }
			else
				for(; (q=X.U16(p,_BE)) != 0xD8F1 && p < maxp; p += 2) if(q > t) t = q;
			ptnp = p; ptn = (p-pp-2) >> (bw+5); p += 2 + ((t+1) << 2); //if p > Sz undetect
			sz = p;
			//subsongs @f732:
			p = o = x = 0; a2 = pp+2;
			_f742: for(; p < maxp; ) {
				p = 8 + o * ch;
				if(p != pp) {
					a0ch = a0 = p+ch; o++;
					_f756: for(; p < maxp; ) {
						a0 = pp+2+(X.U8(p++) << (bw+5));
						_f76a: for(d6 = 0x40; d6 && p < maxp; ) {
							if(bw == 1) d1 = X.U8(a0++); else { d1 = X.U16(a0,_BE); a0 += 2 }
							d1 <<= 2;
							if([0x2C,0x20].indexOf(X.U8(ptnp+4+d1)) >= 0) {
								x++;
								continue _f742
							}
							else {
								if(--d6) continue _f76a;
								if(p != a0ch) continue _f756; else continue _f742;
							}
						}
					}
				} else break _f742;
			}
			if(!x) x = 1; if(x > 1) sOption(x,'×');
			sOption('ch:'+ch+' ord:'+ord+' ptn:'+(mptn==ptn?'':mptn+'/')+ptn+' sz:'+outSz(sz))
		}
	}

	else if((X.Sz() > 0x133) && X.c("'MAS_UTrack_V00'") && isWithin(X.U8(0xE),0x31,0x34)
	  && X.Sz() > X.U8(0x2F)*0x20+0x50) {
		//ref https://github.com/OpenMPT/openmpt/blob/master/soundlib/Load_ult.cpp
		sName = "Marc 'MAS' Schallehn's UltraTracker module (.ULT)"; bDetected = 1;
		nV = X.U8(0xE)-0x30; sVersion = 'v'+['<1.4','1.4','1.5','1.6'][nV-1]; bad = '';
		msgn = X.U8(0x2F); p = 0x30+msgn*0x20;
		smp = X.U8(p++); smpsz = 0; smps = [];
		for(i=0; i < smp; i++) {
			t = X.SC(p,0x20,'CP437').trim(); if(t != '') smps.push(t);
			ssz = X.U32(p+0x38) - X.U32(p+0x34);
			if(ssz < 0) bad = bad.addIfNone('!badsmpsz');
			else smpsz += ((X.U8(p+0x3D)&4)? 2: 1)*ssz; //16-bit flag
			if(nV >= 4) p += 0x42; else p += 0x40;
		}
		for(i=ord=mp=0; i < 0x100; i++) if((o=X.U8(p++)) < 0xFE) { ord++; if(mp < o) mp = o }
		ch = X.U8(p++)+1; ptn = X.U8(p++)+1; mp++;
		p += ch;
		for(c=notes=0; c < ch; c++) for(t=0; t < ptn && p < X.Sz(); t++) {
			row = 0;
			while(row < 64) {
				rp = 1; b = X.U8(p++); if(b == 0xFC) { rp = X.U8(p++); b = X.U8(p++) }
				note = isWithin(b,1,96); p += 4; if(rp+row > 64) rp = 64-row;
				if(!rp) break; row += rp; if(note) notes += rp;
			}
		}
		if(p > X.Sz()) bad = bad.addIfNone('!short');
		sz = p + smpsz;
		if(bad != '') sVersion = sVersion.appendS('/malformed'+bad,'/');
		if(X.isVerbose()) {
			sOptionT(X.SC(0x0F,0x20,'CP437'));
			specialmsg =
				(msgn==23&&sz==297221&&ch==7&&ord==20&&ptn==29&&smp==17&&notes==1572); //Handleless - :) falling and flying :(.ult
			for(p=0x30,i=0,msg=[]; i < msgn; i++) {
				t = decAnsi(p,0x20,CP437,Chars0to1F); if(!specialmsg) t = t.trim();
				if(t != '') msg.push(t); p += 0x20
			}
			msg = addEllipsis(msg.join(specialmsg?'\n':' '), specialmsg?0x2000:0xC0, specialmsg?0x2000:0xA0);
			sOption(msg, 'msg:'+(specialmsg?'\n':'"'), specialmsg?'\n':'"');
			if(!msg.length) sOption(addEllipsis(smps.join(','),0x80,0x60),'smp/msg:"','"');
			sOption('ch:'+ch+' ord:'+ord+' ptn:'+mp+(ptn!=mp?'/'+ptn:'')
				+' smp:'+smp+' notes:'+notes+' sz:'+outSz(sz))
		}
	}

	else if(X.c("'VGEfmtV'") && isWithin(nV=X.U8(7)-0x30,1,3)) { // this format is similar to TFM Music Maker
		sName = "Shiru's VGM Music Maker module (.VGE)"; bDetected = 1;
		sVersion = 'v'+nV; ord = -1; //TODO determine whether to unpack from 0 or 0x10, and RE v1
		if(X.isVerbose()) {
			p = 0; ptn = 1; n = next = 0; tobuf = true; x = [];
			if(nV > 1) for(c = X.U8(p++); n < 0x6E59D1 && p < X.Sz(); ) { //see description in .TFM
				if(tobuf && n > 0x327) tobuf = false;
				if(c != 0x80) { b = c; n++; c = X.U8(p++); if(tobuf) x.push(b) }
				else {
					c = X.U8(p++);
					if(nV == 1 && !c) { x.push(0x80); n++; c = X.U8(p++); continue }
					next = true; reps = shl = 0;
					while (next && shl < 57 && p <= X.Sz()) { //read the vari-len int
						next = !(c & 0x80); c &= 0x7F; reps |= Util.shlu64(c,shl); shl += 7; c = X.U8(p++)
					}
					if(!reps) { if(tobuf) x.push(0x80); n++ } //special case for the RLE character
					else { reps--;
						n += reps; shl = 0; if(tobuf) for(i=0; i < reps; i++) x.push(b) }
				}
			} else p = 1;
			p--;
			switch(nV) {
			// case 1: // n/a for now: the compression seems to be kinda quirky, the algo fails
			// 	spd1 = x[0x10]; spd2 = x[0x11]; intlv = x[0x12];
			// 	d1 = x[0x14]+(x[0x15] << 8); d2 = x[0x16]+(x[0x17] << 8); saves = x[0x18]+(x[0x19] << 8);
			// 	ord = x[0x1B]; lp = x[0x1C];
			// 	break;
			case 2:
				spd1 = x[0x10]; spd2 = x[0x11]; intlv = x[0x12];
				d1 = x[0x14]+(x[0x15] << 8); d2 = x[0x16]+(x[0x17] << 8); saves = x[0x18]+(x[0x19] << 8);
				ord = x[0x1B]; lp = x[0x1C];
				break;
			case 3:
				spd1 = x[0x1A]; spd2 = x[0x1B]; intlv = x[0x1C];
				ord = x[0x25]; lp = x[0x26];
				d1 = x[0x1F]+(x[0x20] << 8); d2 = x[0x21]+(x[0x22] << 8); saves = x[0x23]+(x[0x24] << 8);
				break
			}
			if(nV != 1) { //TODO VGE does something different here
				if(ord > 1) for(i=0; i < 255; i++) if((t=x[0x227+i])+1 > ptn) ptn = t+1;
				d1 = '20'+(d1&0x7F).padStart(2,'0')+'-'+(((d1>>7)&0xF)+1).padStart(2,'0')+'-'+(((d1>>11)&0x1F)).padStart(2,'0');
				d2 = '20'+(d2&0x7F).padStart(2,'0')+'-'+(((d2>>7)&0xF)+1).padStart(2,'0')+'-'+(((d2>>11)&0x1F)).padStart(2,'0');
			} else { d1 = d2 = spd1 = spd2 = intlv = saves = '?'; lp = 0 }; //TODO same
			auth = decEncoding(x.slice(0x27,0x67),CP1251); //I've seen CP1251 and CP936 already, but yeah
			title = decEncoding(x.slice(0x67,0xA7),CP1251);
			cmt = decEncoding(x.slice(0xA7,0x227),CP1251);

			sz = n == 0x6E59D1? p : -1; sOptionT(title); sOptionT(auth,'by: '); sOptionT(cmt);
			sOption('on: '+d1+(d1 != d2? ' to '+d2: ''));
			sOption('spd:'+spd1+'/'+spd2+' ord:'+(lp?lp+'-':'')+ord+' ptn:'+ptn
				+' intlv:'+intlv+' saves:'+saves+' sz:'+outSz(sz));
		}
	}

	else if( X.c("'Vgm '") && X.Sz() >= (eof = X.U32(4)+4)
	  && (nV = X.U32(8)) && nV < 0x300 //give'em time tho
	  && ( !X.U32(0x14) || X.c("'Gd3 '",X.U32(0x14)+0x14) )) {
		//ref https://vgmrips.net/wiki/VGM_Specification
		// & https://vgmrips.net/wiki/GD3_Specification
		bDetected = 1; bad = '';
		nV = 1000*(nV>>12)+100*((nV>>8)&0xF)+10*((nV>>4)&0xF)+(nV&0xF); //BCD be like...
		sName = "Video Game Music chiptune stream (.VGM)";
		eoh = X.U32(0x34)+0x34; if(nV < 150 || eoh == 0x34) eoh = 0x40;
		if(nV >= 150 && eoh < 0x40) bad = '!dataofs';
		sVersion = "v"+(nV/100).toFixed(2);
		if(X.isVerbose()) {
			tags = []; //11 of them
			gd3p = X.U32(0x14,_LE)+0x14;
			if(gd3p > 0x14)
			if(X.SA(gd3p,4) === "Gd3 ") {
				sVersion += "/Gd3 v"+(X.readBytes(gd3p+4,4).join("")/100);
				taglen = X.U32(gd3p+8,_LE); gd3p += 12;
				if(gd3p+taglen > X.Sz()) bad = bad.addIfNone('!tagsz');
				i = 0;
				while(i < 11 && gd3p <= X.Sz()) {
					tpos = X.fSig(gd3p,TOEOF,"0000");
					if(tpos >= 0) {
						tags[i] = X.SU16(gd3p,taglen);
						gd3p += tags[i].length*2+2;
					}
					else {
						tags[i] = ""; gd3p += 2
					}
					i++;
				}
				if(i < 11) bad = bad.addIfNone('!tagnum'); else tagn = Math.max(0,i-1);
				sOption(slashTag(tags[0],tags[1])); sOption(slashTag(tags[6],tags[7]),"by: ");
				sOption(slashTag(tags[2],tags[3]),"for: "); sOption(slashTag(tags[4],tags[5]),"on: ");
				sOption(tags[8],"date: "); sOption(tags[9],"ripper: "); sOption(tags[10],"notes: ")
			}
			smp = X.U32(0x18); if(!smp) bad = bad.addIfNone('!badlen');
			lp = X.U32(0x1C)+0x1C; lpsmp = X.U32(0x20);
			if(lp >= eof || lpsmp > smp) {lp = 0; bad = bad.addIfNone('!badloop') }
			rate = X.U32(0x24);
			if(nV > 100) if(!rate) rate = 'n/a'; else
				if(rate == 50) rate += 'Hz(PAL)'; else if(rate == 60) rate += 'Hz(NTSC)'; else rate += 'Hz';
			chips = [[0xC,'SN76489',0], [0x10,'YM2413',0], [0x2C,'YM2612',110], [0x30,'YM2151',110],
				[0x38,'SegaPCM',151], [0x40,'RF5C68',151], [0x44,'YM2203',151], [0x48,'YM2608',151],
				[0x4C,'YM2610/B',151], [0x50,'YM3812',151], [0x54,'YM3526',151], [0x58,'Y8950',151],
				[0x5C,'YMF262',151], [0x60,'YM278B',151], [0x64,'YMF271',151], [0x68,'YMZ280B',151],
				[0x6C,'RF5C164',151], [0x70,'PWM',151], [0x74,'AY8910',151], [0x80,'GameBoyDMG',161],
				[0x84,'NES_APU',161], [0x88,'MultiPCM',161], [0x8C,'uPD7759',161], [0x90,'OKIM6258',161],
				[0x98,'OKIM6295',161], [0x9C,'K051649/K052539',161], [0xA0,'K054539',161],
				[0xA4,'HuC6280',161], [0xA8,'C140',161], [0xAC,'K053260',161], [0xB0,'Pokey',161],
				[0xB4,'QSound',161], [0xB8,'SCSP',171], [0xC0,'WonderSwan',171], [0xC4,'VSU',171],
				[0xC8,'SAA_1099',171], [0xCC,'ES5503',171], [0xD0,'ES5505/ES5506',171],
				[0xD8,'X1-010',171], [0xDC,'C352',171], [0xE0,'GA20',171], [0xE4,'Mikey/Atari_Lynx',172]
			]; chipn = 0;
			xhdr = 0; if(nV >= 170) { t = X.U32(0xBC); if(t) xhdr = t+0xBC; if(xhdr) {
				xhdsz = X.U32(xhdr); if(!xhdsz) { xhdr = 0; bad = bad.addIfNone('!badxhdr') } } }
			for(i=0; i < chips.length; i++) {
				if(chips[i][0] >= eoh || (xhdr && chips[i][0] > xhdr)) break; clk = X.U32(chips[i][0]);
				b30 = Util.shru64(clk,30); b31 = b30 >> 1; b30 &= 1; clk &= 0x3FFFFFFF;
				if(nV < chips[i][2] || !clk) continue; chipn++;
//_log('@'+Hex(chips[i][0])+' b30:'+b30+' b31:'+b31+' clk:'+clk);
				chip = chips[i][1]; //tentatively
				switch(chips[i][0]) { //all the devils in the details
				case 0xC: if(b30 && b31) chip = 'T6W28';
					if(nV <= 151 && !(X.U8(0x2B)&4)) chip += "/GGStereo";
					break;
				case 0x10: if(nV <= 101 && clk > 5000000) chip = 'YM2612'; break;
				case 0x2C:
					if(nV >= 151) if(b31) chip = 'YM3438';
					if(nV <= 101) { clk1 = X.U32(0x10)&0x3FFFFFFF; if(clk1 > 5000000) clk = clk1 }
					break;
				case 0x30:
					if(nV >= 151) if(b31) chip = 'YM2164';
					if(nV <= 101) { clk1 = X.U32(0x10)&0x3FFFFFFF; if(clk1 < 5000000) clk = clk1 }
					break;
				case 0x4C: if(b31) chip = 'YM2610B'; else chip = 'YM2610'; break;
				case 0x74:
					switch(X.U8(0x78)) {
					case 0: break;  case 1: chip = 'AY8912'; break;  case 2: chip = 'AY8913'; break;
					case 3: chip = 'AY8930'; break;  case 4: chip = 'AY8914'; break;
					case 0x10: chip = 'YM2149'; break;  case 0x11: chip = 'YM3439'; break;
					case 0x12: chip = 'YMZ284'; break;  case 0x13: chip = 'YMZ294'; break;
					default: chip += "-ish"
					}
					break;
				case 0x84: if(b31) chip += '/FDS'; break;
				case 0x90: if(X.U8(0x94)&8) chip += '12bit'; else chip += '10bit'; break;
				case 0x9C: if(b31) chip = 'K052539'; else chip = 'K051649'; break;
				case 0xA8:
					switch(X.U8(0x96)) {
					case 0: chip = 'C140+NamcoSystem2'; break; case 1: chip = 'C140+NamcoSystem21'; break;
					case 2: chip = '219_ASIC+NamcoNA-1/2'; break; default: chip += "-ish"
					}
					break;
				case 0xCC: chip += ':'+X.U8(0xD4)+'ch'; break;
				case 0xD0: if(b31) chip = 'ES5506'; else chip = 'ES5505'; chip += ':'+X.U8(0xD5)+'ch'; break;
				}
				sVersion = sVersion.appendS(chip/*+'@'+clk*/,'#');
			}
			volmod = -1; if(eoh > 0x7C) {
				volmod = X.U8(0x7C); if(volmod > 0xC0) volmod -= 0x100; if(volmod == -63) volmod = -64;
				volmod = (2^(volmod/0x20))*100; volmod = Math.round(volmod)+'%' }
			if(chipn > 2) bad = bad.addIfNone('!toomanychips');
			if(eof < X.Sz())
				if(X.SA(eof,4) == "Vgm ") sOption("multisong");
				else sOption("+extra data");
			sOption('rate: '+rate+' len: '+secondsToTimeStr(Util.divu64(smp+22000,44100))+(lp?' looped':'')
				+(volmod !== -1 && volmod !== ' 100%'?' vol: '+volmod:'')+(xhdr?' xhdr':'')
				+' sz:'+outSz(eof))
		}
		if(bad != '') sVersion = sVersion.appendS("malformed"+bad,'/')
	}

	else if(X.Sz() > 0x8C2 && X.c("0033'T1'") && [1,2].includes(X.U8(4)) && X.U8(5) <= 1 && X.U8(6) <= 10
	  && !X.U16(7) && X.U8(9) && X.U8(0xC) <= X.U8(0xB)) {
		//ref http://www.kahlin.net/daniel/victracker/vt-2.0/fileformat.txt
		sName = "Daniel Kahlin's VIC-TRACKER module (.VT)"; bDetected = 1;
		if(X.isVerbose()) {
			sOptionT(X.SA(0x19E,0x10)); //TODO? https://c64os.com/post/Vic20C64SuperChart
			x = 0; minst = 0xFFF; maxed = 0;
			for(p=0xA; p < 0x42; p+=4) {
				if(!X.U8(p+1)) continue;
				minst = Math.min(minst,X.U8(p)); maxed = Math.max(maxed,X.U8(p+1)); x++
			}
			for(ptn=0,p=0x3C2; p < 0x7C2; p++) ptn = Math.max(X.U8(p),ptn); ptn++;
			if(x > 1) sOption(x,'×');
			sOptionT(X.SA(0x1AE,0x10),"by: "); sOptionT(X.SA(0x1BE,0x10),"'");
			sOption('ptn:'+ptn+' sz:'+outSz(0x8C2+0x40*ptn))
		}
	}

	else if(X.fSig(X.Sz()-0x20,TOEOF,"'VSS0'00") > -1) {
		//TODO thaaat's not a good way to detect a thing...
		sName = "Tomas Partl's Voodoo Supreme Synthesizer audio (.VSS)"; bDetected = 1
	}

	else if(X.c("6000") && X.c("48E7FFFE610000",0x04)
		&& X.c("4CDF7FFF'Nu'",0x0C)) {
		sName = "Wally Beben's module (.WB)"; bDetected = 1
	}

	else if(X.c("'WSRF'",X.Sz()-0x20) && X.c("EA",X.Sz()-0x10)) {
		//ref http://daifukkat.su/docs/wsman/#wsr
		sName = "WonderSwan R programmatic chiptune (.WSR)"; bDetected = 1;
		p = X.Sz()-0x20;
		sVersion = "v"+X.U8(p+4);
		if(X.isVerbose()) {
			sOption(Hex(X.U8(p+0x18)),"CartID:")
			pub = X.U8(p+0x16);
			switch(pub) {
			case 0: sOption("(invalid publisher)"); break;
			case 1: sOption("Bandai"); break;
			case 2: sOption("Taito"); break;
			case 3: sOption("Tomy"); break;
			case 4: sOption("Koei"); break;
			case 5: sOption("Data East"); break;
			case 6: sOption("Asmik Ace"); break;
			case 7: sOption("Media Entertainment"); break;
			case 8: sOption("Nichibutsu"); break;
			case 0x0A: sOption("Coconuts Japan"); break;
			case 0x0B: sOption("Sammy"); break;
			case 0x0C: sOption("Sunsoft"); break;
			case 0x0D: sOption("Mebius"); break;
			case 0x0E: sOption("Banpresto"); break;
			case 0x10: sOption("Jaleco"); break;
			case 0x11: sOption("Imagineer"); break;
			case 0x12: sOption("Konami"); break;
			case 0x16: sOption("Kobunsha"); break;
			case 0x17: sOption("Bottom Up"); break;
			case 0x18: sOption("Kaga Tech"); break;
			case 0x19: sOption("Sunrise"); break;
			case 0x1A: sOption("Cyber Front"); break;
			case 0x1B: sOption("Mega House"); break;
			case 0x1D: sOption("Interbec"); break;
			case 0x1E: sOption("Nihon Application"); break;
			case 0x1F: sOption("Bandai Visual"); break;
			case 0x20: sOption("Athena"); break;
			case 0x21: sOption("KID"); break;
			case 0x22: sOption("HAL Corporation"); break;
			case 0x23: sOption("Yuki Enterprise"); break;
			case 0x24: sOption("Omega Micott"); break;
			case 0x25: sOption("Layup"); break;
			case 0x26: sOption("Kadokawa Shoten"); break;
			case 0x27: sOption("Shall Luck"); break;
			case 0x28: sOption("Squaresoft"); break;
			case 0x2B: sOption("Tom Create"); break;
			case 0x2D: sOption("Namco"); break;
			case 0x2E: sOption("Movic(?)"); break;
			case 0x2F: sOption("E3 Staff(?)"); break;
			case 0x31: sOption("Vanguard"); break;
			case 0x32: sOption("Megatron"); break;
			case 0x33: sOption("Wiz"); break;
			case 0x34: sOption("Capcom"); break;
			default: sOption("(unknown publisher)")
			}
			sOption(X.U8(p+5),"1sttrk: ");
		}
	}
	else if(X.c("3026b2758e66cf11a6d900aa0062ce6c")) { //TODO organise to a non-module file
		sName = "Windows Media (.WMV/WMA)"; bDetected = 1
	}
	else if(X.c("'XAD!'")) {
		sName = "Riven the Mage's Exotic AdLib module (.XAD)"; bDetected = 1;
		if(X.isVerbose()) {
			sOptionT(X.SA(0x04,0x24));
			sOptionT(X.SA(0x28,0x24),"by: ");
		}
	}

	else if(X.c("'Extended Module: '") && X.c("1A",0x25) && isWithin(X.U16(0x48),1,256)) {
		//ref https://github.com/OpenMPT/openmpt/blob/master/soundlib/Load_xm.cpp et al.
		V = X.U16(0x3A); charset = 'CP437';
		sName = "Fast Tracker 2 Extended module (.XM)"; bDetected = 1;
		text = tracker = mVlsw = bad = ""; insns = []; smpns = [];
		hdrp = 0x3C; hdrsz = X.U32(hdrp); restartp = X.U16(hdrp+6); flags = X.U16(hdrp+0x0E);
		linearSlides = flags & 1; xFilter = flags & 0x1000;
		ord = X.U16(hdrp+4); chn = X.U16(hdrp+8); ptn = X.U16(hdrp+0x0A); ins = X.U16(hdrp+0x0C);
		tmp0 = X.U16(hdrp+0x10); bpm0 = X.U16(hdrp+0x12);
		var ptnhdp = p = hdrp+hdrsz;
	   if(V >= 0x104) for(i=0; i < ptn; i++) p += X.U32(p)+X.U16(p+7);
		const verUnknown = 0, verOldModPlug = 1, verNewModPlug = 2, verMPfBiDi = 4, verOpenMPT = 8,
		verConfirmed = 0x10, verFT2Generic = 0x20, verOther = 0x40, verFT2Clone = 0x80,
		verPlayerPRO = 0x100, verDigiTrakker = 0x200, verUNMO3 = 0x400, verEmptyOrders = 0x800;
		var isMadTracker = isOMPTMade = isOXM = mixlevCompatFT2 = false;
		madewith = verUnknown;
		if(X.c("'FastTracker v2.00   '",0x26) && hdrsz === 276) { //version detection pt.1/9001
			if(V < 0x104) madewith = verFT2Generic | verConfirmed;
			else if((t=X.fSig(0x11,0x14,"00")) >= 0) { //song title space or zero padded?
				if(restartp)
					madewith = verFT2Clone | verNewModPlug | verEmptyOrders;
				else if(t == 0x24) // just the last character's zero
					madewith = verFT2Clone | verNewModPlug | verPlayerPRO | verEmptyOrders;
				else if(firstNotOf(t+1,0x24-t,0x20) < 0) // space-padded ascii-z?!
					madewith = verPlayerPRO | verConfirmed;
				else
					madewith = verFT2Clone | verNewModPlug | verEmptyOrders;
			}
			else if(restartp) madewith = verFT2Generic | verNewModPlug;
			else madewith = verFT2Generic | verNewModPlug | verPlayerPRO;
		}
		else if(X.c("'FastTracker v 2.00  '",0x26)) madewith = verOldModPlug;
		else {
			madewith = verUnknown | verConfirmed;
			tracker = X.SC(0x26,0x14,charset).trim();
			if(X.c("'OpenMPT '",0x26)) madewith = verOpenMPT | verConfirmed | verEmptyOrders;
			else if(X.c("'MilkyTracker '",0x26)) { if(!X.c("'       '",0x32)) mixlevCompatFT2 = true }
			else if(X.c("'Fasttracker II clone'",0x26)) madewith = verFT2Generic | verConfirmed;
			else if(X.c("'MadTracker 2.0'00",0x26)) {
				isMadTracker = true; if(X.c("00000000",0x35)) tracker = 'MadTracker 2';
				else tracker = 'MadTracker 2 (registered)'
			}
			else if(X.c("'*Converted '",0x26) && X.c("'-File*'",0x34)) {
				madewith = verDigiTrakker | verConfirmed; tracker = 'Digitrakker' }
		}
		if(xFilter && (madewith & verNewModPlug))
			madewith = verFT2Clone | verNewModPlug | verConfirmed | verEmptyOrders;
		//back on track
		smp = smpReserved = 0; insp = p; anyADPCM = anyInsSmp = false; sflags = [];
			smpsz = lastsmphdsz = lastinstp = lastsmpreserved = -1;
		if(!ord && !verEmptyOrders) ord = 1; //a fix for dark lighthouse.xm
		for(i=0; i < ins; i++) {
			if(p+4 > X.Sz()) { bad = bad.addIfNone('!short'); break }
			ihdsz = X.U32(p); if(!ihdsz) ihdsz = 263; instp = X.U8(p+0x1A);
			inst = X.SC(p+4,0x16,'CP437').trim(); if(inst.length) insns.push(inst);
			smpn = X.U16(p+0x1B); smphdsz = X.U32(p+0x1D);
			if(madewith == verOldModPlug) { madewith |= verConfirmed;
				if(ihdsz == 245) { mVlsw = "1.00.00.A5"; tracker = "ModPlug Tracker 1.0 alpha" }
				else if(ihdsz == 263) { mVlsw = "1.00.00.B3"; tracker = "ModPlug Tracker 1.0 beta" }
				else madewith = verUnknown | verConfirmed;
			} else if(!smpn) {
				if(ihdsz == 263 && !smphdsz && (madewith & verNewModPlug)) madewith |= verConfirmed;
				else if(ihdsz != 29 && (madewith & verDigiTrakker)) madewith &= ~verDigiTrakker;
				else if((madewith & (verFT2Clone|verFT2Generic)) && ihdsz != 33) madewith = verUnknown;
				if(ihdsz != 33) madewith &= ~verPlayerPRO;
				else if(smphdsz > 40 && (madewith & verPlayerPRO)) {
					if(anyInsSmp || (lastsmphdsz != -1 && smphdsz != lastsmphdsz))
						madewith = verPlayerPRO | verConfirmed;
					lastsmphdsz = smphdsz
				}
			}
			if(lastinstp == -1) lastinstp = instp;
			else if(lastinstp != instp && (madewith & verFT2Generic)) {
				madewith &= ~verFT2Generic; madewith |= verFT2Clone;
			}
			vls = X.U8(p+40+0xC3); vle = X.U8(p+40+0xC4); vef = X.U8(p+40+0xC8);
			pls = X.U8(p+40+0xC6); ple = X.U8(p+40+0xC7); pef = X.U8(p+40+0xC9);
			midichecks = X.U8(p+40+0xD0) | X.U8(p+40+0xD1) | X.U16(p+40+0xD2) | X.U8(p+40+0xD6);
			p += ihdsz; smp += smpn;
			if(p > X.Sz()) { bad = bad.addIfNone("!short"); break }
			if(V >= 0x104) sflags = [];
			var ssizes = [], slens = 0;
			if(smpn) {
				anyInsSmp = true;
				if(midichecks) madewith &= ~(verOldModPlug | verNewModPlug | verPlayerPRO);
				if(ihdsz != 0x107 || instp) madewith &= ~verPlayerPRO;
				if(!(madewith & verConfirmed) && (madewith & verPlayerPRO))
					if(!(vef & 4) && vls == 0xFF && vle == 0xFF
					  || !(pef & 4) && pls == 0xFF && ple == 0xFF) {
					  	madewith &= ~verNewModPlug; madewith |= verConfirmed
					}
				for(j=0; j < smpn; j++) {
					slen = ssizes[j] = X.U32(p); //lpst @ p+4, lpsz @ p+8, vol0 @ p+0xC, relnote @ p+0x10
					var sft = X.I8(p+0xD), sf = X.U8(p+0xE), sp = X.U8(p+0x0F), sr = X.U8(p+0x11),
					  sname = X.SC(p+0x12,22,'CP437'), snt = sname.trim(); smpReserved |= sr;
					smpns.push(snt);
					isADPCM = (sr === 0xAD) && !(sf&0x30); //flags sample16bit=10h and sampleStereo=20h
					if(sr && sr != 0xAD) madewith &= ~(verOldModPlug | verNewModPlug | verOpenMPT);
					if(lastsmpreserved == -1) lastsmpreserved = sr;
					else if(lastsmpreserved != sr) madewith &= ~verPlayerPRO;
					if(sp != 128) madewith &= ~verPlayerPRO;
					if((sft&0xF) && sft != 127) madewith &= ~verPlayerPRO;
					sflags.push([sf, isADPCM]); if(isADPCM) anyADPCM = true;
					slens += isADPCM ? 16+(slen+1 >> 1) : slen;
					p += 40; //not smphdsz; considering there are files with smphdsz = 0...
					if((madewith & (verFT2Generic | verFT2Clone)) && (madewith & (verNewModPlug | verPlayerPRO))
					  && !(madewith & verConfirmed) && (sr > 22 || sname.slice(sr).indexOf(' ') < 0)) {
						madewith &= ~verFT2Generic; madewith |= verFT2Clone | verConfirmed
					}
					if((sf&3) === 3 && (madewith & verNewModPlug)) madewith |= verMPfBiDi;
				} //read the instrument's samples info
				smpsz += slens;
				if(V >= 0x104) { if(X.c("'OggS'",p)) isOXM = true; p += slens }
			} //if smpn
		} //for i in ins
		if(!smpReserved && (madewith & verNewModPlug) && X.fSig(0x11,0x14,"00") > -1)
			madewith |= verConfirmed;
		if(V < 0x104) {
			for(i=0; i < ptn; i++)
				p += X.U32(p)+X.U16(p+(V == 0x102 ? 6 : 7));
			if(X.c("'OggS'",p)) isOXM = true;
			p += smpsz
		}
		basesz = p;
		if(tracker == "") {
			if((madewith&verDigiTrakker) && !smpReserved && (lastinstp ? lastinstp : -1) == -1)
				tracker = "Digitrakker";
			else if(madewith&verFT2Generic)
				tracker = "FastTracker 2 or compatible";
			else tracker = "Unknown"
		}
		//catch plugins, settings, messages from various XM flavours
		fx = 0; xt = "";
		while(p+6 < X.Sz()) {
			t = X.SA(p,4); it = X.U32(p);
			if(it && X.U16(p+4)
			  && (t === "228\x04" || (it & 0x80808080) || !(it & 0x60606060))) {
				//I haven't seen files with that to see how that goes so we'll just cut here!
				break
			}
			if(t === "text") {
				t = X.U32(p+4); p += 8; xt = xt.append("t");
				text = X.SC(p,t,'CP437').trim(); p += t;
				madewith &= ~verPlayerPRO; madewith |= verConfirmed
			} else if(/F[0-9X]\d\d/.test(t)) {
				t = X.U32(p+4); if(p+8+t <= X.Sz()) { p += 8+t; fx++ }
				madewith |= verConfirmed
			} else if(t === "MIDI") {
				t = X.U32(p+4); madewith &= ~verPlayerPRO; madewith |= verConfirmed;
				if(p+8+t <= X.Sz()) { p += 8+t; xt = xt.append("m") }
			} else if(t === "CHFX" || t === "CNAM" || t === "PNAM") {
				t = X.U32(p+4); if(p+8+t <= X.Sz()) p += 8+t;
				madewith &= ~verPlayerPRO; madewith |= verConfirmed
			} else if(t === "XTPM") {
				p += 4; xt = xt.append("x");
				madewith &= ~verPlayerPRO; madewith |= verConfirmed; isOMPTMade = true;
				t = X.SA(p,4);
				while(p+7 < X.Sz()) {
					if(!X.U8(p)) { p++; break }
					code = X.SA(p,4); icode = X.U32(p);
					if(code === "STPM" || code === "228\x04"
					  || (icode & 0x80808080) || !(icode & 0x60606060))
						break;
					prsz = X.U16(p+4); p += 6;
					for(i=0; i < ins; i++) p += prsz }
			} else if(t === "STPM") {
				p += 4; xt = xt.append("s"); madewith &= ~verPlayerPRO; madewith |= verConfirmed;
				while(p+6 < X.Sz()) {
					if(!X.U8(p)) { p++; break }
					if(X.c("'VWSL'",p)) { v = 0;
						function itV(v) { return v.slice(0,1)+'.'+v.slice(1,3)+'.'+v.slice(3,5)+'.'+v.slice(5,7) }
						switch(X.U16(p+4)) {
						case 1: v = X.U8(p+6); break; case 2: v = X.U16(p+6); break;
						case 3: v = X.U24(p+6); break; case 8: v = X.U64(p+6); break;
						default: v = X.U32(p+6) }
						if(v) mVlsw = itV(v.toString(16).toUpperCase().padStart(7,'0')); break;
					}
					p += 6+X.U16(p+4)
				}
			} else break;
		}
		if(madewith & verConfirmed)
			if(madewith & verMPfBiDi) {
				mVlsw = "1.11"; tracker = "ModPlug Tracker 1.0-11";
			} else if((madewith & verNewModPlug) && !(madewith & verPlayerPRO)) {
				mVlsw = "1.16"; tracker = "ModPlug Tracker 1.0-16";
			} else if((madewith & verNewModPlug) && (madewith & verPlayerPRO)) {
				mVlsw = "1.16"; tracker = "ModPlug Tracker 1.0-16 / PlayerPRO";
			} else if(!(madewith & verNewModPlug) && (madewith & verPlayerPRO))
				tracker = "PlayerPRO";
		if(X.c("'OpenMPT '",0x26)) {
			mVlsw = X.SA(0x2E,12).trim(); madewith = verOpenMPT|verConfirmed;
		}
		if(isOMPTMade && mVlsw < "1.17") mVlsw = "1.17";
		if(mVlsw >= "1.17") tracker = "OpenMPT v"+mVlsw;
		sz = p;
		charset = (mVlsw != "" || isMadTracker) ? 'CP1252' : 'CP437';
		if(X.isVerbose()) {
			sOptionT(X.SC(0x11,0x14,charset));
			sOptionT(tracker,"in:");
			if(isOXM) sOption("OggMod FastTracker 2 (.OXM)","via:");
			if(text.length) sOption(addEllipsis(text,0x100));
			if(insns.length) sOption(addEllipsis(insns.filter(funSampleName).join(' '),0x100),'ins/msg:"','"');
			if(smpns.length) sOption(addEllipsis(smpns.filter(funSampleName).join(' '),0x100),'smp/msg:"','"')
			var info = 'chn:'+chn+' ord:'+ord+' ptn:'+ptn+' ins:'+ins+' smp:'+smp;
			if(fx) info += ' fx:'+fx; if(xt.length) info += ' xt:'+xt;
			if(sz != basesz) info += " base_sz:"+basesz; info += " sz:"+outSz(sz); sOption(info)
		}
		sVersion = "v"+(V>>8)+"."+(V&0xFF)+bad+(anyADPCM?"/ADPCMpacked":"");
	}

	else if(X.c("'FORM' 0000000E 'XDIRINFO'  00000002 .... 'CAT ' ........ 'XMIDFORM' ........ 'XMID'")) {
		bDetected = 1;
		sName = "Extended MIDI chiptune (.XMI,.C55,.PCS)";
		if(X.isVerbose()) {
			if((x=X.U8(0x14)) > 1) sOption(x,'×')
			sOption(outSz(0x1E+X.U32(0x1A,_BE)),'sz:')
		}
	}

	else if(X.c("'ofTAZ!'")) {
		sName = "Davey W. Taylor's Extra Simple Music module (.XSM)"; bDetected = 1
	}
	else if(/YM\d!/.test(X.SA(0,4)) || X.c("'YM3b!'")
		  || /YMT\dLeOnArD!/.test(X.SA(0,0xC)) || X.c("'MIX1LeOnArD!'")) {
		//ref https://github.com/cpcsdk/libstsound/blob/master/Ymload.cpp
		bDetected = 1; bad = ''; frm = smp = voc = loop = ddn = 0;
		sName = "ST-Sound chiptune stream (.YM)";
		sV = X.SA(0,4).replace(/!/g,"");
		switch(sV) {
		case "YM1": sVersion = "YM1"; break;
		case "YM2": sVersion = "MADMAX specific"; break;
		case "YM3": sVersion = "YM-Atari"; break;
		case "YM3b": sVersion = "YM-Atari+loopinfo";
			loop = X.U32(X.Sz()-4,_LE); break;
		case "YM4": sVersion = "YM-Atari extended"; break;
		case "YM5": case "YM6": sVersion = "Generic YM2149 extended"; break;
		case "MIX1": sVersion = "Atari Remix digital"; break;
		case "YMT1": case "YMT2": sVersion = "YM-Tracker"; break;
		}
		if(["YM2","YM3","YM3b"/*,"YM4"*/].indexOf(sV) >= 0) //No YM4 in the wilderness, ignoring
			frm = Util.divu64(X.Sz()-4,14);
		if(["YM5","YM6","YMT1","YMT2","MIX1"].indexOf(sV) >= 0) {
			if(X.SA(4,8)!="LeOnArD!") bad = bad.addIfNone('!badsig');
			if((["YM6!","YMT1","YMT2"].indexOf(sV) >=0)
				&& (X.SA(X.Sz()-4)!="End!")) bad = bad.addIfNone('!badfilesz');
			//if(sV==="YM4") p=0x1C; else
			if(["YM5","YM6"].indexOf(sV) >= 0) {
				ddn = X.U16(0x14,_BE);
				loop = X.U32(0x1C,_BE);
				p = X.U16(0x20,_BE)+0x22;
				for(i=0; i < ddn; i++) {
					ds = X.U16(p,_BE); p += 2+ds; if(p >= X.Sz()) {bad = bad.addIfNone('!tooshort'); break}
				}
			} else
			if(sV === "MIX1") {
				p = 0x18;
				smp = X.U32(0x10,_BE);
				mixblk = X.U32(0x14,_BE);
				for(i=0;i<mixblk;i++) p+=12; //u32 smpst,smplen; u16 repeat,rplfreq skipped
			} else
			if(["YMT1","YMT2"].indexOf(sV) >= 0) {
				ddn = X.U16(0x18,_BE);
				voc = X.U16(0x0D,_BE);
				frm = X.U32(0x10,_BE);
				p = 0x1E;
			}
			//read the three tags from here
			if(p>=X.Sz()) bad = bad.addIfNone('!nodata');
			else {
				t = p; t_ = X.fSig(p,TOEOF,"00")-p;
				if(t_ >= 0) {
					p += t_+1; a = p; a_ = X.fSig(p,TOEOF,"00")-p;
					p += a_+1; c = p; c_ = X.fSig(p,TOEOF,"00")-p;
					p += c_+1;
					if(["YM5","YM6"].indexOf(sV) >= 0)	
						if(!X.c("'End!'",p+((frm = X.U32(0xC,_BE)) << 4))) {
							bad = bad.addIfNone('!badframes'); sOption("frm/frames: "+(frm<<4)+"/"+(X.Sz()-p-4)) }
				}
			}
			if(X.isVerbose()) {
				sOptionT(X.SA(t,t_));
				sOptionT(X.SA(a,a_),"by: ");
				sOptionT(X.SA(c,c_));
			}
		}
		if(X.isVerbose()) {
			if(voc) sOption(voc,"voc:");
			if(smp) sOption(outSz(p+smp),'sz:');
			if(ddn) sOption(ddn, "digidrums:")
			if(frm) sOption('len:'+frm+' sz:'+outSz(p+(frm<<4)+4));
		}
		if(bad != '') sVersion= sVersion.appendS('malformed'+bad,'/')
	}
	else if(X.c("'YMST'")) {
		sName = "Nicolas 'Mr.Styckx' Pomarede's MYST ST-YM module (.YMST,.YM)"; bDetected = 1;
		if(X.isVerbose()) {
			for(i=0,p=4; i < 48; i++) { p += 8; if(!X.U16(p-8,_BE)) break }
			t = p; t_ = X.fSig(p,TOEOF,"00")-p;
			if(t_ >= 0) {
				p += t_+1; a = p; a_ = X.fSig(p,TOEOF,"00")-p;
				p += a_+1; c = p; c_ = X.fSig(p,TOEOF,"00")-p;
				if(X.isVerbose()) {
					sOptionT(X.SA(t,t_));
					sOptionT(X.SA(a,a_),"by: ");
					sOptionT(X.SA(c,c_));
				}
			}
		}
	}

// pure signature detection end


// AND NOW,
// THE HEAVIEST STUFF GOES DOWN HERE
// THE SANITY CHECK-STYLE DETECTION
// (* It's still hella fast tho, no worries)

function isAYAmadeus() {
	//the underlying format is the same as FXM
	//ref https://vgmrips.net/wiki/AY_File_Format
	if(!X.c("'ZXAYAMAD'") || X.U8(9) > 3) return false;
	authp = 0x0C+X.I16(0x0C,_BE); if(authp < X.Sz()) auth = authp?X.SA(authp,0x100):''; else return false;
	miscp = 0x0E+X.I16(0x0E,_BE); if(miscp < X.Sz()) misc = miscp?X.SA(miscp,0x100):''; else return false;
	x = X.U8(0x10)+1; p = 0x12+X.I16(0x12,_BE); if(p > X.Sz()) return false;
	titles = []; for(i=0; i < x; i++,p+=4) if(t = p+X.I16(p,_BE))
		if(t < X.Sz()) titles.push(X.SA(t,0x100).trim()); else return false;
	return true
}
if(!bDetected && isAYAmadeus()) {
	sName = "František Fuka/Fuxoft's AY Amadeus module (.AMAD)";
	sVersion = [X.U8(8)?'Rel '+X.U8(8):'', X.U8(9)?'Plr '+X.U8(9):''].join(' ').trim();
 	if(X.isVerbose()) {
		sOptionT(titles.join(',')); if(x > 1) sOption(x,'×');
		if(authp) sOptionT(auth,'by: '); if(miscp) sOptionT(misc);
	}
}


function isAYEmul() {
	// ref https://vgmrips.net/wiki/AY_File_Format
	if(!X.c("'ZXAYEMUL'") || X.U8(9) > 3) return false;
	authp = 0x0C+X.I16(0x0C,_BE);
	if(authp < X.Sz()) auth = authp?X.SC(authp,0x100,'CP1250'):''; else return false;
	miscp = 0x0E+X.I16(0x0E,_BE);
	if(miscp < X.Sz()) misc = miscp?X.SC(miscp,0x100,'CP1250'):''; else return false;
	x = X.U8(0x10)+1; p = 0x12+X.I16(0x12,_BE); if(p > X.Sz()) return false;
	titles = []; for(i=0; i < x; i++,p+=4) if(t = p+X.I16(p,_BE))
		if(t < X.Sz()) titles.push(X.SC(t,0x100,'CP1250').trim()); else return false;
	return true
}
if(!bDetected && isAYEmul()) {
	sName = "Sergej Bulba's AY Emul chiptune (.EMUL)"; bDetected = 1;
	sVersion = [X.U8(8)?'Rel '+X.U8(8):'', X.U8(9)?'Plr '+X.U8(9):''].join(' ').trim();
 	if(X.isVerbose()) {
		sOptionT(titles.join(',')); if(x > 1) sOption(x,'×');
		if(authp) sOptionT(auth,'by: '); if(miscp) sOptionT(misc);
	}
}


function isBobsAdlib() {
	if(!X.c("'CBMF'")) return false;
	//ref https://github.com/adplug/adplug/blob/master/src/bam.cpp
	// & https://rpg.hamsterrepublic.com/ohrrpgce/BAM_Format
	p = 4; notes = ins = 0; var songend = false, m = Math.min(X.Sz(), 0x10000), v = [];
	for(i=0; i < 16; i++) v[i] = false;
	while(!songend && p < m) {
		while(!songend && (b=X.U8(p++)) < 0x80 && p < m)
			if(isWithin(b, 1,0xF) || isWithin(b, 0x40,0x4F) || isWithin(b, 0x71,0x7E)) return false; //per draft 3
			else switch(b>>4) {
			case 0: if(!X.isHeuristicScan()) songend = true; break; //this isn't guaranteed!
			case 1: if(!v[b-0x10]) return false; p++; notes++; break; //start note
			case 2: break; //stop note
			case 3: v[b-0x30] = true; p += 11; break; //define ins
			case 5: break; //set label
			case 6: p++; break; //jump
			case 7: //end of chorus
			default: break;
			}
		//if b >= 0x80 it's a pause
	}
	if(!X.isHeuristicScan()) {
		if((!songend && m < X.Sz()) || notes < 0x10) return false; //we'll ignore the "zero at the start" stupid files unless you really want them, but then the detection becomes much worse
		for(i=0; i < 16; i++) if(v[i]) ins++;  if(!ins) return false; //
	}
	return true
}
if(!bDetected && isBobsAdlib()) {
	sName = "Bob's Adlib Music module (.BAM)"; bDetected = 1
	sOption('ch:'+ins+' notes:'+notes+' sz:'+outSz(p))
}


function isCMF() {
	if(!X.c("'CTMF'")) return false;
	if(!isWithin(X.U16(4),0x100,0x101) || !isWithin(X.U16(8),0x25,X.Sz()-1)) return false;
	//ref https://github.com/adplug/adplug/blob/master/src/cmf.cpp
	// & https://moddingwiki.shikadi.net/wiki/CMF_Format
	nV = X.U16(4,_LE); ic = 0 /*invalid values for heuristics*/; bad = '';
	pins = X.U16(6,_LE); pmus = X.U16(8,_LE); //instruments, music ptr
	if(pmus <= pins) {ic++; bad = bad.addIfNone('!badptr') }
	if(nV >= 0x101) { p = 0x28; ins = X.U16(0x24) } else { p = 0x25; ins = X.U8(0x24) }
	if((t=Util.divu64(pmus-pins,0x10)) != ins) { ic++; bad = bad.addIfNone('!inconsistentinscnt'+t) }
	tp = X.U16(0x0E,_LE); if(tp && !isWithin(tp,p,pins-1)) { tp = 0; ic++; bad = bad.addIfNone('!badptr') }
	ap = X.U16(0x10,_LE); if(ap && !isWithin(ap,p,pins-1)) { ap = 0; ic++; bad = bad.addIfNone('!badptr') }
	cp = X.U16(0x12,_LE); if(cp && !isWithin(cp,p,pins-1)) { cp = 0; ic++; bad = bad.addIfNone('!badptr') }
	ch = 0; for(i=0x14; i < 0x24; i++) { if((t=X.U8(i)) == 1) ch++; else if(t > 1) ic++} if(!ch) ic++;
	sz = X.fSig(pmus,Math.min(0x10000,X.Sz()),"FF2F00");
	if(sz < 0) { sz = X.fSig(pmus,Math.min(0x10000,X.Sz()),"FF2FFE"); bad = bad.addIfNone('!badeof')}
	if(sz > 0) sz += 3;
	return ic < 5
}
if(!bDetected && isCMF()) {
	sName = "Creative Labs' Creative Music Format chiptune (.CMF)"; bDetected = 1;
	sVersion = "v"+(nV>>8)+"."+(nV&0xFF);
	if(bad != '') sVersion += "/malformed"+bad;
	if(X.isVerbose()) {
		if(tp) sOptionT(addEllipsis(X.SC(tp,0x100,'CP437')));
		if(ap) sOptionT(addEllipsis(X.SC(ap,0x100,'CP437')),"by: ");
		if(cp) sOptionT(addEllipsis(X.SC(cp,0x100,'CP437')));
		if(nV >= 0x101 && X.U8(sz) == 0xFF) sz++; //a lot of v1.1 CMFs end in FF, enough to be significant?
		sOption('ch:'+ch+' ins:'+ins+' sz:'+outSz(sz))
	}
}


function isDSym() {
	//ref https://github.com/OpenMPT/openmpt/blob/master/soundlib/Load_dsym.cpp
	// & https://sourceforge.net/p/xmp/libxmp/ci/master/tree/src/loaders/sym_load.c
	if(X.Sz() < 90 || !X.c("02011313 1412010B") || X.U8(8) > 1 || !isWithin(ch=X.U8(9), 1,8)
	  || (ord=X.U16(0xA)) > 4096 || (ptn=X.U16(0xC)) > 4096) return;
	var infolen = X.U24(0x0E), smpsz = [], smpnsz = [];
	smp = 0; p = 0x11;
	for(i=0; i < 63; i++,p++) {
		smpnsz[i] = X.U8(p); if(!(X.U8(p) & 0x80)) { smp++; smpsz[i] = X.U24(p+1); p += 3 } }
	title = X.UCSD(p); p += 1+X.U8(p);
	var ac = X.readBytes(p,8); p += 8; if(p > X.Sz()) return;
	//TODO? lzw unpacking and whatnot
	return true
}
if(!bDetected && isDSym()) {
	sName = "Oregan Developments' Digital Symphony module (.DSYM)"; sVersion = 'v'+X.U8(8); bDetected = 1;
	if(X.isVerbose()) {
		sOptionT(title); sOption('ch:'+ch+' ord:'+ord+' ptn:'+ptn+' smp:'+smp);
	}
}


function isDigitalFM() {
	//ref https://verhaagr.home.xs4all.nl/dfm.zip / dfm.pas
	if(!X.c("'DFM'1A") || X.U8(4) > 1 || [0,1,0x63].indexOf(X.U8(5)) < 0 || X.U8(6) > 0x20
	  || !isWithin(tmp0=X.U8(0x27), 5,0x1F)) return;
	inss = [];
	for(i=0,p=0x28; i < 32; i++,p+=12) if(X.U8(p) > 11) return; else inss.push(X.UCSD(p));  p += 0x160;
	for(ord=ptn=i=0; i < 128 && X.U8(p+i) != 0x80; i++,ord++) ptn = Math.max(ptn,X.U8(p+i)+1);
	for(i++; i < 128; i++) if(X.U8(p+i)) return;
	p += 0x80; optn = X.U8(p++);
	//from here the original code ignores the pattern count and just keeps reading patterns until EoF!
	for(rptn=next=0; rptn < optn && p < X.Sz(); rptn = next+1) {
		next=X.U8(p++); for(i=0; i < 64*9; i++) if(X.U8(p++) & 0x80) p++
	}
	return true
}
if(!bDetected && isDigitalFM()) {
	sName = "Rene Verhaag's Digital-FM module (.DFM)";
	sVersion = 'v'+X.U8(4)+'.'+X.U8(5).padStart(2,'0'); bDetected = 1;
	if(X.isVerbose()) {
		sOptionT(X.SC(7,32,'CP1252'));
		sOptionT(addEllipsis(inss.filter(funSampleName).join(' ')),'ins/msg:"','"');
		sOption('ch:'+X.U8(6)+' tmp0:'+tmp0+' ord:'+ord
			+' ptn:'+(ptn!=optn?ptn+'/':'')+(rptn!=optn?rptn+'/':'')+optn+' sz:'+outSz(p));
	}
}


function isDRO() {
	//ref https://github.com/ValleyBell/libvgm/blob/master/player/droplayer.cpp
	if(!X.c("'DBRAWOPL'")) return;
	if((t = X.U32(8)) & 0xFF00FF00) { sVersion = 'v0'; mV = MV = 0 }
	else if(!(t & 0x0000FFFF)) { mV = X.U16(8); MV = X.U16(0xA) }
	else { MV = X.U16(8); mV = X.U16(0xA) }
	if(MV > 3) return;
	//we only know about v2 but it may happen one day
	if(MV) sVersion = 'v'+MV+'.'+mV.padStart(2,'0'); db = '';
	switch(MV) {
	case 0:
		db = 'DOSBox 0.62'; lenMS = X.U32(8); dtsz = X.U32(0xC);
		hw = X.U8(0x10); dtofs = 0x11; //fallthrough
	case 1:
		if(MV == 1) {
			db = 'DOSBox 0.63'; lenMS = X.U32(0xC); dtsz = X.U32(0x10);
			hw = (t=X.U32(0x14)) <= 0xFF ? t : 0xFF; dtofs = 0x18;
		}
		if(hw == 1) hw = 2; else if(hw == 2) hw = 1;
		fmt = 0; co = 0; break;
	case 2:
		db = 'DOSBox 0.73'; dtsz = X.U32(0xC) << 1; lenMS = X.U32(0x10); hw = X.U8(0x14);
		fmt = X.U8(0x15); co = X.U8(0x16); ds = X.U8(0x17); dl = X.U8(0x18); regcmdcnt = X.U8(0x19);
		dtofs = 0x1A+regcmdcnt; if(regcmdcnt > 0x80) regcmdcnt = 0x80; regmap = X.readBytes(0x1A,regcmdcnt);
		//scan init block
		if(hw == 1) {
			p = dtofs; opl3on = 0; reginit = []; for(i=0; i < 0x200; i++) reginit[i] = false;
			while(p < X.Sz()) {
				t = X.U8(p); if(t == ds || t == dl) break; if((t & 0x7F) >= regcmdcnt) break;
				creg = ((t & 0x80) << 1) | regmap[t & 0x7F]; reginit[creg] = true; if(creg == 0x105) opl3en = X.U8(p+1);
				p += 2
			}
			if(reginit[0x105] && (opl3en & 1)) hw = 2;
		}
		hw = ['YM3812 (OPL2)', 'YM3812 (Dual OPL2)', 'YMF262 (OPL3)'].indexOf(hw);
		if(hw === -1) hw = 'YMF262(portshift)';
	}
	return true
}
if(!bDetected && isDRO()) {
	sName = "DOSBox Raw OPL chiptune (.DRO)";
	sVersion = sVersion.appendS(hw,'#'); bDetected = 1;
		if(X.isVerbose()) {
			sOption('len: '+secondsToTimeStr(Util.divu64(lenMS+500,1000))+' via: '+db+' packed:'+co
				+' sz:'+outSz(dtofs+dtsz))
		}

}


function isFunktracker() {
	if(X.Sz() < 0x8FF || !X.c("'Funk'")) return false;
	//ref https://sourceforge.net/p/xmp/libxmp/ci/master/tree/docs/formats/FORMAT.FunktrackerGOLD
	sz = X.U32(8); if(!isWithin(sz,0x8FF,0x100000)) return false;
	t = X.U32(4); sV = X.SA(0x0C,4); bad = ''; sus = 0;
	if(/F2\d\d/.test(sV))
		sversion = "R2 GOLD "+(1980+((t>>9)&0x7F))+"-"
		  +((t>>5)&0xF).padStart(2,'0')+"-"+(t&0x1F).padStart(2,'0')+" ";
	else sversion = "R1";
	if(/F[2vk]\d\d/.test(sV)) ch = X.SA(0x0E,2);
	else { sversion += "b"; ch = 8; sus++ }
	switch((t>>20)&0xF) {
	case 1: case 2: t = "IBM"; break; case 3: t = "Intel 386"; break;
	case 4: t = "Intel 486"; break; case 5: t = "Pentium"; break; case 6: t = "Linux"; break;
	case 7: t = "FreeBSD"; break; case 8: t = "N/A"; break; default: t = "unk.system" }
	sversion += "#"+t;
	switch((t>>16)&0xF) {
	case 0: t = "SB 2.0"; break; case 1: t = "SB Pro"; break;
	case 2: t = "GUS+ch.pan"; break; case 3: t = "SB compatible"; break;
	case 4: t = "SB 16"; break; case 5: t = "GUS"; break; case 6: t = "conversion"; break;
	case 7: t = "Pro Audio Spectrum"; break; case 8: t = "Voxware /dev/dsp 8 bit"; break;
	case 9: t = "Voxware /dev/dsp 16 bit"; break; case 15: t = "unk.soundcard"; break;
	default: t = "soundcard N/A" }
	sversion += ":"+t;
	if(!X.isVerbose() && X.Sz() < sz) bad = bad.addIfNone('!short');
	ord = 0; ptn = -1; for(i = 0; i < 256; i++) {
		t = X.U8(0x11+i); if(t == 0xFF) break; ord++;
		if(t > 0x79)
			if(X.isHeuristicScan()) { bad = bad.addIfNone("!badord") }
			else return false;
		else if(t > ptn) ptn = t;
	} ptn++;
	lp = X.U8(0x10); if(lp != 0xFF && lp > ord) bad += "!badloop";
	smp = sus = 0; smps = [];
	for(i = 0x190; i < 0x8BF; i += 0x20) {
		if(!isWithin(X.U8(i),1,0x4F)) return false; //vol		
		if(X.U32(i+0x18)) smp++; //length
		if(charStat(X.readBytes(i+1,0x13),1).indexOf('allasc') < 0) sus++; if(sus > 3) return false
		smps.push(X.SC(i+1,0x13,'CP437').trim()) //smp name; charset TBD
	}
	return true
}
if(!bDetected && isFunktracker()) {
	sName = "Jason Nunn's Funktracker module (.FNK,.Funk)"; sVersion = sversion; bDetected = 1;
	if(bad != "") sVersion += "/malformed"+bad;
	if(X.isVerbose()) {
		bpm = X.U8(4); bits = bpm&1 ? 16 : 8; bpm >>= 1; var sine = bpm >> 6;
		bpm &= (1<<6)-1; if(sine) bpm = 125-bpm; else bpm = 125+bpm;
		sOptionT(addEllipsis(smps.filter(funSampleName).join(' ')),'smp/msg:"','"');
		sOption('ch:'+ch+' bpm0:'+bpm+' ord:'+(lp!=0xFF?lp+'~':'')+ord+' ptn:'+ptn
			+' smp:'+smp+' ' +bits+'bit sz:'+outSz(sz))
	}
}


function isPreTracker() {
	  	//ref https://git.platon42.de/chrisly42/PretrackerRaspberryCasket/src/branch/main/src/raspberry_casket.asm lines 257+
	if(!X.c("'PRT'") || !isWithin(ins=X.U8(0x40), 1,0x40)) return;
	if(!isWithin(nV=X.U8(3), 10,50)) return;
	if(nV < 30) if(!(ptn=[X.U8(0x3D)])[0] || !(ord=[X.U8(0x3E)])[0] || (lp=[X.U8(0x3C)])[0] > ord[0]) return;
	x = 1; p = 0x5A; ptnp = [];
	if(nV >= 30) x = X.U8(p++); if(!x) return;
	if(nV >= 30) { ord = []; lp = []; ptn = []; }
	var steps = [];
	for(i=0; i < x; i++) {
		lp.push(X.U8(p++)); ptn.push(X.U8(p++)); steps.push(X.U8(p++));
		ord.push(X.U8(p++)); ptnp.push(X.U32(p,_BE)); p += 4;
//_l2r('prt',i,'ord:'+lp[i]+'-'+ord[i]+' ptn:'+ptn[i]+' steps:'+steps[i]+' +pp:'+Hex(ptnp[i]))
		p += ord[i]*4*2;
		//if(!ord[i] || ptnp[i] < 0x5B+8) return
		for(i=0; i < x; i++) ptnp[i] += p;
		mpp = (function(){var i=ptnp.indexOf(Math.max.apply(null,ptnp));return [ptnp[i],steps[i]];}());
//_l2r('prt',p,outArray(ptnp,16)+' s'+outArray(steps,16)+' ->'+outArray(mpp,16))
	}
	//ptn data
//_l2r('prt',p,'ptn')
//p = mpp[0]+3*mpp[1];
//_l2r('prt',p,outArray(X.readBytes(p,6),16))
// TODO: Pink's PreTracker WIP
	return X.Sz() > p
}
if(!bDetected && isPreTracker()) {
	sName = "Manfred 'Pink' Linzner's PreTracker module (.PRT)"; bDetected = 1;
	if(nV < 25) sVersion = "v<0.3"; else
	if(nV == 25) sVersion = "v0.3~0.866"; else
	if(nV == 26) sVersion = "v0.87~0.92"; else
	if(isWithin(nV, 27,29)) sVersion = "v.[0.93~1.5)"; else
	if(nV == 30) sVersion = "v1.5+";
	else sVersion = "v.TODO";
	if(X.isVerbose()) {
		sOptionT(X.SA(0x14,0x14));
		if(x > 1) sOption(x,'×');
		sOptionT(X.SA(0x28,0x14),"by: ");
		sOption('ord:'+(lp?lp+'-':'')+ord+' ptn:'+ptn+' ins:'+ins+' wf:'+(wf=X.U8(0x41)))
	}
}


function isComebackTracker() {
	//ref REing PreTracker 1.5 which has CBT beta import routines
	if(X.Sz() < 0x64 || !X.c("'CBT'") || !isWithin(nV=X.U8(3), 1,12)) return;
	// ecx = 0x10; eax = ecx+1; max = Math.min(0x10000,X.Sz());
	// while(X.U8(ecx++));
	return true
}
if(!bDetected && isComebackTracker()) {
	sName = "Manfred 'Pink' Linzner's Comeback Tracker module (.CBT)"; bDetected = 1;
	sVersion = 'v'+nV+'β'
}


function isProSoundMaker_c() {
	//ref https://ay.strangled.net/Ay_Emul30.src.7z /Help/html/ay_ru_fmt_psm.htm
	bad = ''; sus = lp = 0; m = Math.min(X.Sz(), 0x4000);
	var ordp = X.U16(0); if(!isWithin(ordp,8,0xFF)) { sus++; bad += '!badpos' }
	var smpp = X.U16(2); if(!isWithin(smpp, ordp,0x1FF)) { sus++; bad += '!badsmp' }
	var ornp = X.U16(4); if(ornp && !isWithin(ornp, smpp,0x4FF)) { sus++; bad += '!badorn' }
	var ptnp = X.U16(6); if(!isWithin(ptnp, ornp,0x5FF)) { sus++; bad += '!badptn' }
	title = '';
	if(ordp >= 0xD) {
		var idfound = X.c("'psm1'00", 8), titlep = idfound? 0xD: 8;
		if(titlep < ordp) title = decAnsi(titlep,ordp-titlep,CPSpeccy);
		if(idfound) sus -= 2
	}
	if(sus > 2) return;
	var ords = [], smps = [0], orns = [0], ptns = [];
	for(p=ordp,ord=ptn=0; p < m; ord++) {
		var o = X.U8(p++); t = X.U8(p); var ti = X.I8(p++);
		if(o == 0xFF) { if(t == 0xFF) break;  if(t & 0x80) { if((t & 0x1F) >= ord) return; else lp = t & 0x1F } break }
		if(!isWithin(ti, -36,36) || ord > 100 || o > 32) return;
		if(!ords.includes(o)) ords.push(o); if(o >= ptn) ptn = o+1;
	} if(!ord) return;
	q = X.U16(smpp); smp = (q-smpp) >> 1;
	for(p=smpp; p < q; p+=2) if(!isWithin(t=X.U16(p), smpp,ornp)) return; else if(!smps.includes(t)) smps.push(t);
	if(ornp == ptnp) orn = 0; else { q = X.U16(ornp); orn = (q-ornp) >> 1;
		for(p=ornp; p < q; p+=2) if(!isWithin(t=X.U16(p), ornp,ptnp)) return; else if(!orns.includes(t)) orns.push(t) }
	tmp = [100, 0];
	for(i=0,p=ptnp; i < ptn; i++) {
		if(!isWithin(t=X.U8(p++), 2,50)) return;
		if(tmp[0] > t) tmp[0] = t;  if(tmp[1] < 1) tmp[1] = t;
		for(j=0; j < 3; j++,p+=2)
			if(!isWithin(t=X.U16(p), ptnp,m)) return;
			else { if(!ptns.includes(t)) ptns.push(t) }
	}
	ptns.sort(function(a,b){return a-b}); if(Util.divu64(ptns[0]-ptnp,7) != ptn) return;
	for(i=1; i < ptns.length; i++) if(!isWithinRanges(t=X.U8(ptns[i]-1), [0xFF,[0xB8,0xF8]])) { sus++; if(sus > 5) return }
	// parse the highest pattern, find size
	for(p=ptns.pop(); p < m;) {
		c = X.U8(p++);
		if(c == 0xFF) break;
		if(isWithin(c, 0xB1,0xB7) || c == 0xFC) p++;
		else if(c == 0xF9) p += 4;
		else if([0xFD, 0xFE].includes(c)) return
	}
	sz = p;
	return true
}
if(!bDetected && isProSoundMaker_c()) {
	sName = "Denis 'Dexus' Dratov's Pro Sound Maker module (.PSM)"; sVersion = 'compiled'; bDetected = 1; 
	if(sus>0 || bad.length) sVersion += '/malformed'+bad+'/sus'+sus;
	if(X.isVerbose()) {
		sOptionT(title);
		sOption('tmp:'+tmp[0]+(tmp[0] == tmp[1]?'':'-'+tmp[1])+' ord:'+(lp?lp+'~':'')+ord
			+' ptn:'+ptn+' smp:'+smp+(orn?' orn:'+orn:'')+' sz:'+outSz(sz))
	}
}


function isProtrackerIFF() {
	if(!X.c("'FORM'........ 'MODLVERS'00000016") || !X.c("'INFO'00000048",0x1E)
	  || !X.c("'CMNT'000001A4",0x66) || !X.c("'PTDT'",0x20A)) return false;
	info = cmt = dt = pt = sv = ''; smp = ord = ptn = tmp0 = 0; sz = X.U32(4,_BE)+8;
	maxsz = Math.min(sz,X.Sz());
	p = 0x1E; while (!bDetected && p < maxsz) {
		hkhd = X.SA(p,4); hksz = X.U32(p+4,_BE); if(charStat(hkhd,1).indexOf('allasc') < 0) break;
		switch (hkhd) {
		case 'INFO': info = decAnsi(p+8,0x20,CPAmiga).trim();
			smp = X.U16(p+0x28,_BE); ord = X.U16(p+0x2A,_BE);
			ptn = X.U16(p+0x2C,_BE); bpm0 = X.U16(p+0x30,_BE);
			var dd = X.U16(p+0x34,_BE); if(!isWithin(dd,1,31)) return false;
			var mm = X.U16(p+0x36,_BE); if(!isWithin(mm,0,12)) return false;
			var yy = X.U16(p+0x38,_BE); if(isWithin(yy,30,88)) return false;
			dt = (1900+yy)+'-'+mm.padStart(2,'0')+'-'+dd.padStart(2,'0')+' '
				+X.U16(p+0x3A,_BE).padStart(2,'0')+':'+X.U16(p+0x3C,_BE).padStart(2,'0')
				+':'+X.U16(p+0x3E,_BE).padStart(2,'0');
			pt = secondsToTimeStr(X.U16(p+0x40,_BE)*3600+X.U16(p+0x42,_BE)*60+X.U16(p+0x44,_BE));
			break;
		case 'CMNT': auth = decAnsi(p+8,0x20,CPAmiga).trim(); if(auth === 'UNNAMED AUTHOR') auth = '';
			cmt = decAnsi(p+0x28,hksz-0x20,CPAmiga).trim(); break;
		case 'PTDT': return true
		}
		p += hksz
	}
}
if(!bDetected && isProtrackerIFF()) {
	sName = "ProTracker IFF-wrapped module (.PTM)"; bDetected = 1;
	sv = X.SA(0x18,6).trim(); if(!sv.length) sv = 'v3.6'; sVersion = sv;
	if(X.isVerbose()) {
		sOption(info); sOption(auth,'by: '); sOption(cmt);
		sOption(dt,'on '); sOption(pt,'len ');
		sOption('bpm0:'+bpm0+' ord:'+ord+' ptn:'+ptn+' smp:'+smp);
		sOption(outSz(sz),'sz:')
	}
}


function isS3M() {
	//ref https://moddingwiki.shikadi.net/wiki/S3M_Format
	//from https://github.com/OpenMPT/openmpt/blob/master/soundlib/S3MTools.h & Load_s3m.cpp
	// & http://asle.free.fr/prowiz/prowiz.src.zip / r/S3M.c
	if(!X.c("'SCRM'", 0x2C) || !X.c("10",0x1D) || [1,2].indexOf(X.U8(0x2A)) < 0 || X.Sz() < 0x60) return false;
	keepmidims = fmttrkstr = trknc = isST3 = isSchism = false;
	tracker = lswv = bad = ''; sus = 0;
	if(!X.c("1A",0x1C)) {sus++; bad = bad.addIfNone('!badsig1a') }
	z1 = X.U16(0x1E); if(z1) { sus++; bad = bad.addIfNone('!badsig0') }
	ord = X.U16(0x20); if(ord & 1) { sus++; bad = bad.addIfNone('!oddord') }
	smp = X.U16(0x22); ptn = X.U16(0x24);
	fl = X.U16(0x26); cwtv = X.U16(0x28); tv = cwtv >> 12; wtv = cwtv & 0xFFF; fmtv = X.U16(0x2A);
	gvol = X.U8(0x30); spd = X.U8(0x31); tmp = X.U8(0x32); mvol = X.U8(0x33); uc = X.U8(0x34);
	usept = (X.U8(0x35) && 0xFC) > 0; r2 = X.U16(0x36); special = X.U16(0x3E);
	ch = 4; for(i=0; i < 0x20; i++) if(X.U8(0x40+i) != 0xFF) ch = i+1;
	t = Hex(cwtv); sv = t.substr(1,1)+'.'+t.substr(2,2).padStart(2,'0');
	switch(tv) {
	case 0x208 & tv: tracker = 'Akord'; break;
	case 1: if(X.c("'SCLUB2.0'",0x36)) tracker = 'Sound Club 2';
		else if(cwtv == 0x1320 && !special && !(ord & 0xF) && !uc && !(fl & ~0x50) && usept) {
			if(mvol) { lswv = '1.16'; tracker = 'ModPlug Tracker/OpenMPT 1.17' }
			else { lswv = '1.00.00.A0'; tracker = 'ModPlug Tracker 1.0 alpha' }
			keepmidims = trknc = true
		} else if(cwtv == 0x1320 && !special && !uc && !fl && !usept) {
			if(gvol == 64 && mvol == 48) tracker = 'PlayerPRO';
			else tracker = 'Velvet Studio' //TODO attribute in a brief way
		} else if(cwtv == 0x1320 && !special && !uc && fl == 8 && !usept)
			tracker = "J.Lim's Impulse Tracker < 1.03";
		else {
			isST3 = true; if(cwtv == 0x1320) tracker = "Psi's Scream Tracker 3.20-21"; else {
				tracker = "Psi's Scream Tracker"; fmttrkstr = true }
		} break;
	case 2: fmttrkstr = cwtv != 0x2013; if(fmttrkstr) tracker = 'Imago Orpheus'; else tracker = 'PlayerPRO';
		trknc = true; break;
	case 3: if(cwtv == 0x3320) tracker = 'Impulse Tracker 1.03';
		else { if(r2 > 0x214) tracker = "Impulse Tracker 2.15";
			else if(isWithin(wtv,0x215,0x217))
					tracker = 'Impulse Tracker 2.14p'+(wtv == 0x215? '1-2': wtv == 0x216? '3': '4-5');
				else tracker = 'Impulse Tracker '+((wtv&0xF00)>>8)+'.'+(wtv&0xFF).toString(16).padStart(2,'0');
		}
		// if(cwtv >= 0x3207 && X.U32(0x38)) {} // could decode the total edit time here if you wanted
		trknc = true; break;
	case 4: if(cwtv == 0x4100) tracker = 'BeRoTracker';
		else {
			isSchism = true;
			var dt = 734016 + (wtv < 0xFFF? wtv-0x050: r2);
			var y = Util.div64(dt*10000+14780, 3652425);
			var ddd = dt - (365*y + Util.div64(y,4) - Util.div64(y,100) + Util.div64(y,400));
			if(ddd < 0) { y--; ddd = dt - (365*y + Util.div64(y,4) - Util.div64(y,100) + Util.div64(y,400)) }
			var mi = Util.div64(100*ddd+52, 3060);
			tracker = 'Schism Tracker '+(y+Util.div64(mi+2,12)).padStart(4,'0')+'-'
				+((mi+2)%12+1).padStart(2,'0')
				+'-'+(ddd - Util.div64(mi*306+5,10) + 1).padStart(2,'0')
		}
		trknc = true; break;
	case 5: if((cwtv >> 8) == 0x57) { tracker = 'NESMusa'; fmttrkstr = true
		} else if(!r2 && uc == 16 && X.U8(0x41) != 1) { tracker = 'Liquid Tracker'; fmttrkstr = true
		} else if(cwtv != 0x5447) {
			v = wtv << 16; if(v >= 0x01290000) v |= r2;
			function itV(v) { return v.slice(0,1)+'.'+v.slice(1,3)+'.'+v.slice(3,5)+'.'+v.slice(5,7) }
			lswv = itV(v.toString(16).toUpperCase().padStart(7,'0')); tracker = 'OpenMPT '+lswv
		} else tracker = "Dumbo's Graoumf Tracker";
		break;
	case 6: tracker = 'BeRoTracker'; break;
	case 7: tracker = "BeRo's CreamTracker"; break;
	default:
		if(cwtv == 0xCA00) tracker = 'Camoto';
	}
	if(sus >= 2) return false;
	if(fmttrkstr) tracker += ' '+sv;
	charset = lswv != ''? 'CP1252': 'CP437';
	p = 0x60+ord+smp*2; max = sz = 0;
	for (i=0; i < ptn; i++) {
		var p1 = X.U16(p+i*2) << 4;
		if(!p1) continue;
		if (p1 > max) { max = p1; sz = max + X.U16(p1) }
	}
	if(sz%16) sz += 16-(sz%16);
	if(usept) { var anyunpannedch = false; p += ptn*2;
		for(i=0; i < ch; i++)
			if(isST3 && isWithin(X.U8(0x40+(i&0xFF)), 16, 29) && X.U8(p+i) < 0x10)
				anyunpannedch = true;
		if(ch < 32 && lswv.indexOf('1.16') >= 0)
			if(anyunpannedch) tracker = 'ModPlug Tracker 1.16/OpenMPT 1.17';
			else tracker = 'ModPlug Tracker'
	}
	p = 0x60+ord; anysmp = anyADPCM = false; gus = 0; smps = [];
	for(i=0; i < smp; i++) {
		si = X.U16(p+i*2) << 4; if(!si) continue; else if(si > X.Sz()) { bad = bad.addIfNone('!short'); continue }
		st = X.U8(si); ssz = X.U32(si+0x10); t = X.SC(si+0x30,0x1C,charset).trim(); if(t != '') smps.push(t);
		if(st < 2) {
			if(ssz) {
				anysmp = true; sf = X.U8(0x1F);
				if(!anyADPCM && X.U8(si+0x14) == 4 && !(sf & 6)) anyADPCM = true }
			gus |= X.U16(si+0x28) }
		if(X.U8(si) === 1) {
			sofs = X.U16(si+14) << 4;
			if (sofs > max) {
				if (sf & 4) ssz *= 2; //16-bit
				max = sofs; if(sz < max+ssz) sz = max+ssz;
			}
		}
	}
	usegus = gus > 1;
	if(isST3 && anysmp && !gus && cwtv != 0x1300) {
		isST3 = false; tracker = 'Unknown';
		if(cwtv == 0x1301 && !uc) {
			if(!(fl & ~0x50) && (mvol & 0x80) && usept) tracker = "Laurent Clévy's UNMO3";
			else if(!fl && gvol == 48 && mvol == 176 && tmp == 150 && !usept)
				tracker = "Slixter's deMODifier";
			else if(!fl && gvol == 64 && (mvol & 0x7F) == 48 && spd == 6 && tmp == 125 && !usept)
				tracker = "Zab/Kosmic's To-S3M"
		}
	} else if(isST3) tracker += usegus? ' (GUS)': ' (SB)';
	if(anyADPCM) tracker += ' (ADPCM packed)';
	return true
}
if(!bDetected && isS3M()) {
	sName = "Sami 'Psi' Tammilehto's ScreamTracker 3 module (.S3M)"; bDetected = 1;
	if(sus) sVersion = sVersion.appendS('malformed'+bad+' sus'+sus,'/');
	if(X.isVerbose()) {
//if(!spd || spd == 0xFF && isST3) spd = 6; if(tmp < 33) tmp = isST3? 125: 32; //fixes for playback, include?
		sOptionT(X.SC(0,0x1C,charset)); sOption(tracker,'in:');
		sOption(addEllipsis(smps.join(' '),0xA0),'smp/msg:"','"');
		sOption('ch:'+ch+' tempo0:'+tmp+' spd0:'+spd+' ord:'+ord+' ptn:'+ptn+' smp:'+smp
			+' gvol:'+gvol+' smpvol:'+(mvol&0x7F)+(mvol&0x80?'/mono':'/stereo')+' sz:'+outSz(sz));
	}
}


function isMlatAT() {
	if(!X.c("'MAD+'")) return;
	ord = X.U8(0xB9); ptn = X.U8(0xBA); tmr = X.U8(0xBB);
	for(i=0,p=0xBC; i < ptn*32*9; i++) if(isWithin(X.U8(p++),0x60,0xFD)) return;
	for(i=0; i < ord; i++) if(!isWithin(X.U8(p++), 1,ptn)) return;
	return true
}
if(!bDetected && isMlatAT()) {
	sName = "Mlat Adlib Tracker module (.MAD)"; bDetected = 1;
	//if(bad.length) sVersion = 'malformed'+bad;
	if(X.isVerbose()) sOption('ord:'+ord+' ptn:'+ptn+' sz:'+outSz(p))
}


function isProtoTracker() {
	if(!X.c("'SONG'") ) return;
	//ref https://github.com/kometbomb/prototracker/blob/master/doc/FORMAT.TXT
	//TODO tighten somewhat
	sz = X.U32(4,_LE); p = 8;
	v = X.U8(p++); trk = v >= 1? X.U8(p++):4; fxprm = v >= 17? X.U8(p++): 1;
	fxc = v >= 17? X.U8(p++): 0;
	title = X.SC(p,0x1000,'CP1250'); p += title.length+1;
	ptnr = X.U8(p++)+1; seqr = X.U8(p++)+1;
	if(!ptnr || !seqr) return; mcr = 0;
	for(i=eof=0; p < X.Sz() && !eof; p += X.U32(p+4)) {
		switch(X.SA(p,4)) {
		case 'SEQU': if(X.U8(p+8) > seqr) return; i |= 1; break;
		case 'PATT': i |= 0x10; break;
		case 'MACR': i |= 0x100; mcr = X.U8(p+8); break;
		default: if(i < 0x11) return;  eof = 1
		}
	}
	sz = p;
	return true
}
if(!bDetected && isProtoTracker()) {
		sName = "Tero 'kometbomb' Lindeman's ProtoTracker module (.SONG)";  bDetected = 1;
		sVersion = sVersion = "v"+v;
		if(X.isVerbose()) {
			sOption(title);
			sOption('trk:'+trk+(fxc?' fx:'+fxc:'')+(mcr?' mcr:'+mcr:'')+' sz:'+outSz(sz))
		}
}


function isBeaverSweeper() {
	// ref https://www.pouet.net/prod.php?which=19993 /src/gtk_format.*, isSynth/*
	if(!X.c("'+SNT'")) return;
	// read synth:
	//   read samples
	for(i=smp=0,p=4; i < 32; i++) if(X.U8(p) > 7) return; else if(X.U8(p++) & 1) {
		var ssz = X.U24(p); p += 3+ssz*2; var lpst = X.U24(p), lped = X.U24(p+3); p += 6;
		if(lpst > ssz || lped > ssz || lped < lpst) return;
		if(ssz) smp++
	}
	//   read ins
	vs = [];
	function parseType() {
			switch(X.U8(p++)) {
			case 1/*env*/: if(X.U8(p+0xB) > 1 || !isWithin(X.F32(p+3),-0.1,1.1)) return; p += 0xC; break;
			case 2 /*LFO*/: if(X.U8(p+1) > 1 || X.U8(p+7) > 1) return; p += 0xE; break;  case 3 /*ctlr*/: p += 5; break;
			case 4 /*oscr*/: if(X.U8(p+9) > 1) return; p += 0x37; break;
			case 5 /*flt*/: if(X.U8(p+1) > 1) return; p += 0x1D; break;  case 6 /*distortion*/: p += 9; break;
			case 7 /*delay*/: p += 0x18; break;  case 8 /*chorus*/: if(X.U8(p+0x18) > 1) return; p += 0x19; break;
			default: return;
			}
		return true
	}
	function parseTypes() {
		var nr = X.U8(p++); for(var i=0; i < nr && p < X.Sz(); i++) if(!parseType()) return;
		return true;
	}
	for(i=ins=0; i < 16 && p < X.Sz(); i++) { //for each instrument:
		var v = X.U8(p++);
		p += v; //polyvoice array
		if(v) ins++;
		for(j=0; j < v && p < X.Sz(); j++) { //...for each voice: 
			p+=2; //note range
			if(!parseTypes()) return; //modulators
			p += 1+55*X.U8(p); //oscillators
			if(X.U8(p) > 8) return; //max inserts
			if(!parseTypes()) return;
			p += 0xA; //modulation
		}
		t = X.U8(p++); if(t > 1) return;  if(t) if(!parseType()) return; //distortion
		t = X.U8(p++); if(t > 1) return;  if(t) p += 0xC1; // arpeggiator
	}
	p += 0x140; //instr. mixer settings
	for(i=0; i < 3; i++) { //load effect chains
		fl = X.U8(p++);
		if(fl&1) if(!parseType()) return;  if(fl&2) if(!parseType()) return;  if(fl&4) if(!parseType()) return;
	}
	if(!X.c("'-SNT'",p)) return; p += 4;
	//read the main module
	if(!X.c("'GOAT'",p)) return;
	bad = ''; nv = X.U16(p+4); ch = X.U8(p+6); ptn = X.U8(p+7); ins = X.U8(p+8);
	bpm0 = X.U8(p+9); if(!bpm0) bpm0 = 125;  p += 12;
	var chlens = []; ord = 0;
	for(i=0; i < ch; i++) {
		var id = X.U8(p++);
		if(nv == 0x102) t = X.U8(p++); else { t = X.U16(p); p += 2 }
		ord = Math.max(t,ord);
		for(j=0; j < t; j++) {
			f = X.U16(p); p += 2; if(f > 0x100) bad = bad.addIfNone('!badchnum');
			if(f) for(k=0x100; k; k>>=1) if(f&k) p++
		}
	}
	var ptsz = 64, rows;
	for(i=0; i < ptn; i++) {
		var id = X.U8(p++);
		if(nv == 0x102) rows = X.U8(p++); else { rows = X.U16(p); p += 2 }
		if(!rows) bad = bad.addIfNone('!badptnrows');
		for(j=0; j < rows; j++) {
			f = X.U32(p); p += 4; if(f) for(k=0x10000; k; k>>=1) if(f&k) p += 4
		}
	}
	return true
}
if(!bDetected && isBeaverSweeper()) {
	sName = "Fredrik 'Gnilk' Kling & Stefan 'Steffo' Hållén & Zyrax's Beaver Sweeper module (.GTK)";
	sVersion = 'v'+nv.toString(16).padStart(4,'0');
	if(bad.length) sVersion = sVersion.appendS('malformed'+bad,'/'); bDetected = 1;
	if(X.isVerbose()) {
		sOption('ch:'+ch+' bpm0:'+bpm0+' ord:'+ord+' ptn:'+ptn+(ins?' ins:'+ins:'')+' smp:'+smp+' sz:'+outSz(p))
	}
}


function isAnders0land() {
	//ref https://gitlab.com/uade-music-player/uade/-/raw/master/amigasrc/players/wanted_team/Anders0land/SRC_Anders0land/Anders 0land_v1.asm
	if(!X.c("'mpl'")) return false;
	p = X.U32(4,_BE); if(p > X.Sz() || (p&1)) return false;
	songp = p+8;
	if(!X.c("'mdt'",p)) return false;
	p += X.I32(p+4,_BE); if(p > X.Sz() || (p&1)) return false;
	if(!X.c("'msm'",p)) return false;
	p += X.I32(p+4,_BE); if(p < X.Sz()) return false;
	sz = p;
	smpp = songp+X.I32(songp-4,_BE);
	smpsz = X.I32(smpp-4,_BE);
	smp = (X.I16(songp+20,_BE) - X.I16(songp+18,_BE)) >> 2;
	songsz = songp-8;
	x = (X.I16(songp+4,_BE) - X.I16(songp+2,_BE)) >> 2;
	p = X.I16(songp,_BE);
	ord = X.I16(songp+6,_BE)-p-X.I32(songp+p+12,_BE);
	return true
}
if(!bDetected && isAnders0land()) {
	sName = "Anders 'Zonix' 0land's Music & Player module (.HOT)"; bDetected = 1;
	sVersion = "v"+X.SA(3,1);
	if(X.isVerbose()) {
		if(x > 1) sOption(x,"×");
		sOption("ord:"+ord+" smp:"+smp+" songsz:"+Hex(songsz)+" smpsz:"+Hex(smpsz));
		sOption(outSz(sz),"sz:")
	}
}


function isUNIMOD() {
	// ref https://github.com/sezero/mikmod/blob/master/libmikmod/loaders/load_uni.c
	if(!/(UN0[4-6].*|APUN\x01[1-6])/.test(X.SA(0,6))) return;
	p = 4; voc = 0; var v, e;
	if(X.SA(3,1) != 'N') { v = X.U8(3)-0x30; if(v > 6) v = X.U16(4,_BE) } else v = 0x100;
	if(v >= 6) { e = _BE;
		if(v == 6) p++; else { v = X.U16(p,e); p+=2 }
		flg = X.U16(p,e); p+=2;
		ch = X.U8(p++);
		voc = X.U8(p++);
		ord = X.U16(p,e); ptn = X.U16(p+2,e); trk = X.U16(p+4,e); ins = X.U16(p+6,e);
		smp = X.U16(p+8,e); reppos = X.U16(p+0xA,e); p+=0xC;
		spd0 = X.U8(p++); tmp0 = X.U8(p++); vol0 = X.U8(p++);
		if(v >= 0x106) { bpmlimit = X.U16(p,e); p+=2 }
		else bpmlimit = 32;
	}
	else { e = _LE;
		ch = X.U8(p++);
		ord = X.U16(p,e); p+=2;
		if(v == 5) { lp = X.U16(p,e); p+=2 } else lp = 0;
		ptn = X.U16(p,e);
		trk = X.U16(p+2,e);
		ins = X.U16(p+4,e); p += 6;
		smp = 0; spd0 = X.U8(p++); tmp0 = X.U8(p++);
		p += 256+32; // positions+panning
		flg = X.U8(p++);
		bpmlimit = 32;
	}
	sv = X.SA(3,1) == 'N'? 'v.APlayer': 'v'+Hex(v);
	if((v < 6 && !(isWithin(ch,1,32) || isWithin(ord,1,256))) || lp > ord || !trk || !ptn
	  || !isWithin(ins,1,256) || !spd0 || !tmp0) return;
	titlesz = X.U16(p,_LE); titlep = p+2; p += 2+titlesz;
	if(v < 0x102) {
		origssz = X.U16(p,_LE); origsp = p+2;
		p = p1 = p+2+origssz; p = origsp+origssz;
	} else origssz = 0;
	cmtsz = X.U16(p,_LE); cmtp = p+2; p += 2+cmtsz;
	if(v >= 6) {
		if(v >= 0x100) p += ord*2; else p += ord;
		p += ch*3;
	}
	smps = []; inss = []; smpsz = 0;
	if(v >= 6) {
		for(i=0; i < smp; i++) { //loadsmp6
			smpsz += X.U32(p+7,e); t = X.U16(p+0x23,_LE); p += 0x25; smps.push(X.SC(p,t,'CP437').trim()); p += t
		}
		for(i=0; i < ins; i++) { //loadinstr6
			p += 13;
			pt = X.U8(p+1); p += 6 + (v>=0x100? 0x20: pt)*4;
			pt = X.U8(p+1); p += 6 + (v>=0x100? 0x20: pt)*4;
			pt = X.U8(p+1); p += 6 + (v>=0x100? 0x20: pt)*4;
			if(v >= 0x103) p += 240*smp; else p += 120*smp;
			t = X.U16(p,_LE); p += 2; inss.push(X.SC(p,t,'CP437').trim()); p += t
		}
	} else {
		for(i=0; i < ins; i++) { //loadinstr5
			smpn = X.U8(p); smp += smpn; p += 0xD1;
			t = X.U16(p,_LE); p += 2; if(t) { inss.push(X.SC(p,t,'CP437').trim()); p += t }
			for(j=0; j < smpn; j++) {
				smpsz += X.U32(p+5,e);
				t = X.U16(p+0x13,_LE); p += 0x15; if(t) { smps.push(X.SC(p,t,'CP437').trim()); p += t }
			}
		}
	}
	p += 2*ptn + 2*ch*ptn;
	for(i=0; i < trk; i++) p += X.U16(p,e)+2;
	sz = p+smpsz;
	return true
}
if(!bDetected && isUNIMOD()) {
	sName = "Otto Chrons/libmikmod UNIMOD/UNITRK module (.UNI)"; sVersion = sv; bDetected = 1;
	if(X.isVerbose()) {
		sOptionT(X.SC(titlep,titlesz,'CP437'));
		if(origssz) sOptionT(X.SA(origsp,origssz),"orig: ");
		sOptionT(X.SC(p1+2,X.U16(p1),'CP437'));
		sOption(addEllipsis(inss.filter(funSampleName).join(' ')),'ins/msg:"','"');
		// sOption(addEllipsis(smps.filter(funSampleName).join(' ')),'smp/msg:"','"');
		sOption('ch:'+ch+(voc?'/'+voc:'')+' spd0:'+spd0+' tmp0:'+tmp0+' trk:'+trk
			+' ord:'+(lp?lp+'~':'')+ord+' ptn:'+ptn+' ins:'+ins+(smp?' smp:'+smp:'')+' sz:'+outSz(sz))
	}
}


function isD00nohdr() {
	// ref https://github.com/adplug/adplug/blob/master/src/d00.h & .cpp
	if(X.Sz() < 40 || X.Sz() >= 0xFFFF) return false;
	if(X.c("0001.... 000D00") && X.c("FFFF",0xB)) nV = 0;
	else if(X.c("01..01.. 000F00") && X.c("FFFF",0xD)) nV = 1; else return false;
	seqp = X.U16(3,_LE); if(seqp < 13 || seqp > X.Sz()) return false; // ptr to sequence pointer table
	x = 1;
	seqtest = X.fSig(seqp+18+9,TOEOF,"0000000000 FFFF")-seqp; // reserved bytes are 0; endmark follows 
	while (seqtest > 0x20) { x++; seqtest -= 0x20; if(!X.c("0000000000",seqp+seqtest)) return false }
	//there may be several sequence tables but they end the same
	trkp = t = X.U16(5,_LE); if(!X.c("FFFF",trkp-2) || trkp < 13) return false; // ptr to track pointer table
	insp = X.U16(7,_LE); if(!X.c("FFFF",insp-2) || insp < 13) return false; // to instrument pointer table
	sz = X.U16(9,_LE); if(!X.c("FFFF",sz-2) || sz < 13) return false; // to message
	if(nV == 1) { sfxp = X.U16(11,_LE); if(!X.c("FFFF",sfxp+8)) return false; } // to sfx
	if(Math.abs(trkp-seqp) < 20 || Math.abs(trkp-insp) < 20 || Math.abs(seqp-insp) < 20) return false;
	oldp = trkp; p = X.U16(trkp,_LE);
	ip = 0, once = 0; // count pointers out of order, heuristic
	while(t < trkp+18 && trkp < X.Sz()) { // check 9 pointers from TPoin for validity
		if(t === trkp) p = oldp = t; // for the first pointer
		if(p) oldp = p;
		p = X.U16(t,_LE); if(!p || p < trkp || p > X.Sz()) return false;
		if(!X.c("FFFF",p-2)) { once++; if(once > 1) return false }
		if(p < oldp) { ip++; if(ip > 2) return false } // some D00s do have pointers out of order
		q = p; while(!X.c("FFFF",q) && q < X.Sz()) q += 2; //check the table data for having a FFFF somewhere near
		if(!X.c("FFFF",q)) { _log("D00nohdr: boh. t="+Hex(t)+" p="+Hex(p)+" q="+Hex(q)); return false; }
		t += 2
	}
	msg = ""; t = X.fSig(sz,0x200,"FFFF"); if(t > 0) { // the message exists and ends in FFFF
		msg = X.SC(sz,t-sz,'CP437').trim(); sz = t+2
	} else { // the message may exist but it may also be garbage, checking...
		msg = X.readBytes(sz,Math.min(sz+0x100,X.Sz())-sz);
		c = charStat(msg,1); if(c.indexOf("allxsc") >= 0) {
			msg = decEncoding(msg,CP437); sz += msg.length; msg = msg.trim()
		} else msg = ''
	}
	return true;
}
if(!bDetected && X.isDeepScan() && isD00nohdr()) {
	sName = "Jens Christian 'JCH/Vibrants' Huus's Edlib Tracker module (.D00)?"; sVersion = "old v"+X.U8(0); bDetected = 1;
	if(X.isVerbose()) {
		if(x > 1) sOption(x,'×');
		sOptionT(addEllipsis(msg,0x100,0x80),'msg:"','"');
		sOption("sz:"+outSz(sz))
	}
}


function isProtrackerMOD() {
	//from https://github.com/OpenMPT/openmpt/blob/master/soundlib/Load_mod.cpp
	if(X.Sz() < 0x43C) return false;
	var b = X.readBytes(0x438,4), s = X.SA(0x438,4), hdr = 0x43C/*1084*/, plist = 0x3B8/*952*/, msmp = 0x1F;
	var isStarTrekker = isGenericMCh = isMdKd = maybeWOW = isHMNT = isInconexia = isNoiseTracker
	  = setMODVBlankTiming = hasLongSmp = hasEmptySmpwVol = hasRepLen0 = false;
	var maybeAS = 0, ibtrsh = 40; //typical threshold for invalid bytes (in samples); _FRAGILE is 1

	//test signatures
	if(/(M\.K\.|M!K!|PATT|NSMS|LARD)/.test(s)) {
		chn = 4; tracker = "generic Protracker-compatible";
		if(s === "M.K.") { isMdKd = true; maybeWOW = true } }
	else if(/(M&K!|FEST|N\.T\.)/.test(s)) {
		chn = 4; if(s === "N.T.") tracker = "Pex 'Mahoney' Tufvesson & Anders 'Kaktus' Berkeman's NoiseTracker";
		else { tracker = "Pex 'Mahoney' Tufvesson & Anders 'Kaktus' Berkeman's His Master's NoiseTracker"; isHMNT = true }
		isNoiseTracker = true }
	else if(/O[KC]TA/.test(s)) { chn = 8; tracker = "Armin Sander's Oktalyzer" }
	else if(/CD[68]1/.test(s)) { chn = b[2]-0x30; tracker = "Christian Dahl et al.'s Octalyser STE (Atari)" }
	else if(compareArrays(b, [0x4D,0,0,0]) || compareArrays(b, [0x38,0,0,0])) {
		if(b[0] === 0x38) chn = 8; else chn = 4; ibtrsh = 1; // fragile, needs more detects!
		tracker = "Inconexia demo"; isInconexia = true }
	else if(/FA0[4-8]/.test(s)) { chn = b[3]-0x30; tracker = "Digital Tracker"; hdr = 0x440 }
	else if(/(FLT|EXO)[48]/.test(s)) {
		chn = b[3]-0x30; if(chn == 4) maybeAS++;
		isStarTrekker = setMODVBlankTiming = true; tracker = "Startrekker";
	}
	else if(/[1-9]CHN/.test(s)) { chn = b[0]-0x30; isGenericMCh = true; tracker = "generic MOD-compatible" }
	else if(/[1-9][0-9]C[HN]/.test(s)) {
		chn = b[0]*10+b[1]-0x210; isGenericMCh = true; tracker = "generic MOD-compatible" }
	else if(/TDZ[1-3]/.test(s)) { chn = b[3]-0x30; tracker = "Twaddler and Dr. Zon's TakeTracker" }
	//else if(/\.M\.K/.test(s)) { chn = 4; swapBytes = true } just one hacked module, TODO after fast patches are in
	else if(/WARD/.test(s)) { chn = 8; isGenericMCh = true; tracker = "generic MOD-compatible" }
	else return false;
	if(!chn) return false;
	restartpos = X.U8(plist-1);
	//test samples
	p = 0x14; wowsmpsz = ib = 0;
	for(; p < 0x3B6; p += 0x1E) {
		var ssz = X.U16(p+0x16,_BE)<<1; //length
		wowsmpsz += ssz;
		if(!isHMNT && !hasLongSmp && ssz >= 0x20000) hasLongSmp = true;
		var sft = X.U8(p+0x18), svol = X.U8(p+0x19), sls = X.U16(p+0x1A,_BE)<<1, sll = X.U16(p+0x1C,_BE)<<1;
		if(sft || (ssz && svol != 0x40)) maybeWOW = false; if(sft > 15) ib++; //finetune <= 15
		if(svol > 0x40) ib++; //volume <= 64
		if(!ssz && svol == 0x40) { hasEmptySmpwVol = true;
			if(maybeAS && !sls && sll <= 2) maybeAS++ }
		if(sls > ssz) ib++; //loopstart. looplen gets weird, it's ok
		if(!hasRepLen0 && ssz && !sll) hasRepLen0 = true;
		if(ib > ibtrsh) return false;
	}
	isFLT8 = isStarTrekker && (chn == 8);
	var smpsz31 = wowsmpsz;
	if(restartpos) maybeWOW = false; if(!maybeWOW) wowsmpsz = 0;
	if(maybeAS >= 2) tracker += '/Audio Sculpture'

	//test patterns
	function cntBadPtnData(ofs) {
		var c = 0;
		for(i = 0; i < chn*64; i++)
			if(X.U8(ofs+(i<<2)) & 0xE0) c++;
		return c
	}
	//get real ptn:
	ord = X.U8(plist-2); if(!ord) return false;
	ol = X.readBytes(plist,0x80); //orderlist
	if(ord > 0x80) ord = 0x80;
	else if(!ord) { ord = 0x80; while(ord > 1 && !ol[ord-1]) ord-- }
	ptn = iptn = optn = 0; i = plist;
	for(i=0; i < 0x80; i++) {
		if(isFLT8) ol[i] /= 2; var pt = ol[i];
		if(pt < 0x80 && pt >= ptn) { ptn = pt+1; if(i < ord) optn = ptn } //official patterns
		if(ptn >= iptn) iptn = ptn+1 // illegal patterns
	}
	//weirdness checks
	var alignedfsz = X.Sz() & ~1;
	if(wowsmpsz && wowsmpsz+hdr+ptn*8*256 == alignedfsz) {
		if(cntBadPtnData(hdr+ptn*4*256) < 16) chn = 8; //⚠no good for ripping -- best to err toward no WOW
	}
	else if(ptn != optn && cntBadPtnData(hdr+optn*chn*256) > 64) ptn = optn;
	if(iptn > ptn && hdr+ssz+iptn*chn*256 == alignedfsz) ptn = iptn;

	if(maybeWOW && chn === 8) { tracker = "Mod's Grave"; isGenericMCh = true }
	if(restartpos >= ord || (restartpos == 0x78 && chn === 4)) restartpos = 0; //because that's actually bpm

	var onlyAmigaNotes = true, fix7BitPanning = leftPan = extPan = maxPan = 0;
	if(!isNoiseTracker) {
		isNoiseTracker = isMdKd && !hasEmptySmpwVol && !hasLongSmp;
		p = hdr;
		for(pt = 0; pt < ptn; pt++) {
			var ptnbrk = 0;
			for(; p < pt*chn*256; p += 4) {
				var d = X.readBytes(p,4);
				var np = ((d[0]&0xF)<<8) | d[1]; // note period
				if(np && np != 0xFFF) {
					if(onlyAmigaNotes && (np < 113 || np > 856)) onlyAmigaNotes = isNoiseTracker = false;
				}
				var cmd = d[2]&0xF, prm = d[3] /*, dsmp = (d[2]>>4) | (d[0]&0x10)*/;
				if((cmd > 6 && cmd < 0xA) || (cmd == 0xE && prm > 1)
					|| (cmd == 0xF && prm > 0x1F) || (cmd == 0xD && ++ptnbrk > 1))
					isNoiseTracker = false;
				if(cmd == 8) {
					if(prm > maxPan) maxPan = prm;
					if(prm < 0x80) leftPan = true;
					else if(prm > 0x8F && prm != 0xA4) extPan = true;
				}
			}
		}
		fix7BitPanning = leftPan && !extPan && maxPan >= 0x30;
	}
	if(onlyAmigaNotes && !hasRepLen0 && ['M.K.','M!K!','PATT'].indexOf(s) >= 0) {}
	else if(!onlyAmigaNotes && restartpos == 0x7F && isMdKd && restartpos+2 >= ord)
		tracker = "Sami 'Psi/Future Crew' Tammilehto's Scream Tracker";
	//modsize
	sz = hdr+ptn*chn*256;
	if(sz > X.Sz()) return false;
	p = 0x14; smp = 0; songsz = sz; ib = 0; smps = [];
	var anyADPCM = 0;
	for(; p < 0x3B6; p += 0x1E) {
		if(!isHMNT) { //His Master's Noise have data in sample titles
			t = X.readBytes(p,22,true); t = decEncoding(t,CPAmiga).trim(); if(t.length) smps.push(t)
		}
		var ssz = X.U16(p+0x16,_BE)*2;
		if(ssz) smp++;
		//if(isMdKd && onlyAmigaNotes && !hasEmptySmpwVol) ssz = Math.max(ssz,X.U16(p+0x1C,_BE)*2);
		if(X.c("'ADPCM'",sz)) { anyADPCM++; ssz = (ssz+1>>1)+5+16; }
		sz += ssz;
	}
	if(X.c("8BBEB4BA 8BADBEBC B4BAAD",sz)) {
		var exts = '', ext = [];
		while(sz < X.Sz() && isWithin(X.U8(sz) ^ 0xDF, 0xA,0x7F)) ext.push(X.U8(sz++)^0xDF);
		exts = decEncoding(ext,CP437);
		tracker = "Twaddler and Dr. Zon's TakeTracker";
		if(/\ version\ \d+\./.test(exts)) tracker +=' v' + /\ version\ (\d[^!]+)/.exec(exts)[1];
//		if(smps.length) smps.unshift('/'); smps.unshift(exts);
	}
	else if(isMdKd && X.c("001155332211",sz)) {
		tracker = 'Tetra Music Editor:'+X.U24(sz+6,_BE).padStart(6,'0'); sz += 9
	}
	if(anyADPCM && !isInconexia) tracker += ' (ADPCM packed: '+anyADPCM+')';
	return true
}
if(!bDetected && isProtrackerMOD()) {
	sName = "Amiga Freelancers' Protracker module (.MOD)"; bDetected = 1;
	sVersion = X.SA(0x438,4);
	if(X.isVerbose()) {
		sOptionT(decAnsi(0,20,CPAmiga));
		sOption(tracker,'in: ');
		sOptionT(addEllipsis(smps.filter(funSampleName).join(" "),200),'smp/msg:\"','"');
		sOption('ch:'+chn+' ord:'+ord+' ptn:'+(optn!=ptn?optn+'/':'')+ptn+(iptn!=ptn?'('+iptn+')':'')
			+' smp:'+smp+' sz:'+outSz(sz))
	}
}


function isKRIS() {
	//from https://gitlab.com/uade-music-player/uade/-/raw/master/amigasrc/players/wanted_team/ChipTracker/ChipTracker_v3.asm
	// & https://github.com/OpenMPT/openmpt/blob/master/soundlib/Load_kris.cpp
	if(X.Sz() < 0x7C0+0x100 || !X.c("'KRIS'",0x3B8)) return false;
	ord = X.U8(0x3BC); if(ord > 0x80) return false;
	loop = X.U8(0x3BD); if(loop > 0x7F || (loop < 0x7F && loop >= ord)) return false;
	const ibtrsh = 40; //typical threshold for invalid bytes (in samples); _FRAGILE is 1
	p = 0x16; var totalsmpsz = ib = 0; smp = synwf = 0; smps = [];
	for(var i=0; i < 31; p += 0x1E, ++i)
		if(!X.U8(p)) {
			var maxwf = Math.max(X.U8(p+1),X.U8(p+5),X.U8(p+10),X.U8(p+19));
			if(maxwf && maxwf >= synwf) synwf = maxwf+1;
		} else {
			t = X.SC(p,20,"IBM850").trim(); if(t != "" && t != "\x01") smps.push(t);
			var ssz = X.U16(p+0x16,_BE)*2; //length
			totalsmpsz += ssz;
			var sft = X.U8(p+0x18);
			if(sft & 0xF0) ib++; //finetune <= 15
			var svol = X.U8(p+0x19); if(svol > 0x40) ib++; //volume <= 64
			if(X.U16(p+0x1A,_BE)*2 > ssz) ib++;
			if(ib > ibtrsh) return false;
			smp++
		}
	ptn = 0; p = 0x3BE;
	ord = X.U8(0x3BC);
	for(i=0; i < (ord << 2); i++,p+=2) if(ptn < X.U8(p)) ptn = X.U8(p);
	ptn++;
	sz = 0x7C0+(synwf<<6)+(ptn<<8)+totalsmpsz;
	return true;
}
if(!bDetected && isKRIS()) {
	sName = "Krister Wombell's ChipTracker module (.KRIS,.MOD)"; bDetected = 1;
	if(X.isVerbose()) {
		sOption(X.SA(0,16));
		sOptionT(addEllipsis(smps.join(" "),200),'smp/msg:\"','"');
		sOption('ord:'+(loop && loop < 0x7F? loop+'-':'')+ord+' ptn:'+ptn+' smp:'+smp+(synwf? ' synwf:'+synwf: '')+' sz:'+outSz(sz));
	}
}


function isMTNICE() {
	//ref https://github.com/OpenMPT/openmpt/blob/master/soundlib/Load_ice.cpp
	if(X.c("'MTN'000000",0x5B8)) tracker = "MnemoTroN's SoundTracker";
	else if(X.c("'IT10'00",0x5B8)) tracker = "Icepic's IceTracker 1.0~2'";
	else return false;
	smp = smpsz = 0; smps = [];
	for(p=20; p < 20+30*31; p+=30) {
		t = decAnsi(p,22,CPAmiga).trim(); if(t != '') smps.push(t);
		ssz = X.U16(p+22,_BE); if(ssz) smp++; smpsz += ssz
	}
	ord = X.U8(p++); ptn = X.U8(p++); if(ord > 128) return false;
	for(i=0; i < 128*4; i++) if(X.U8(p++) > ptn) return false;
	sz = 0x5BC+ptn*64*4+smpsz;
	return true
}
if(!bDetected && isMTNICE()) {
	sName = tracker+" module (.ST26,.ICE)"; sVersion = "v2.6"; bDetected = 1;
	if(X.isVerbose()) {
		sOptionT(decAnsi(0,0x14,CPAmiga));
		sOptionT(addEllipsis(smps.join(' '),0x80),'smps/msg:"','"')
		sOption('ord:'+ord+' ptn:'+ptn+' smp:'+smp+' sz:'+sz)
	}
}


function isMVXModule() {
/*header:
1 dword: 'MVM\0'
1 byte: BPM
1 byte: ticks per beat
1 dword: song length in rows
1 byte: machine count
    N byte: machine IDs (N = machine count)
    N byte: machine order (N = machine count)
1 byte: machine connection count
    N * 3 bytes: machine connections (source, target, volume)*/
	if(!X.c("'MVM'00") || !X.U8(4) || !X.U8(5) || X.U32(6) > 10000 || !X.U8(0xA)) return;
	var idsm = [], ordm = [], lstm = [];
	p = 10; mach = X.U8(p++); for(i=0; i < mach; i++) idsm.push(X.U8(p++));
	for(i=0; i < mach; i++) { t = X.U8(p++); if(ordm.indexOf(t) >= 0) return; ordm.push(t); lstm.push(i) }
//_l2r('idsm',0,outArray(idsm)); _l2r('ordm',0,outArray(ordm.sort()));
	var cnx = X.U8(p++);
	for(i=0; i < cnx; i++) {
		var src = X.U8(p++), tgt = X.U8(p++); p++;
		if(src > mach || tgt > mach || src == tgt) return;
		lstm = lstm.filter(function() { return lstm.indexOf(src) < 0 && lstm.indexOf(tgt) < 0 })
	}
	if(lstm.length) return; //all machines must have connections, otherwise it's an export failure
	return true
}
if(!bDetected && isMVXModule()) {
	sName = "Gargaj/Conspiracy's MVX Module (.MVM)"; bDetected = 1;
	if(X.isVerbose()) {
		sOption('bpm0:'+X.U8(4)+' ticks:'+X.U8(5)+' rows:'+X.U32(6)+' machines:'+mach)
	}
}


function isOctaMed() {
	//from http://fileformats.archiveteam.org/wiki/OctaMED_module_(MED)
	// & https://github.com/neumatho/NostalgicPlayer/blob/main/Source/Agents/ModuleConverters/ModuleConverter/Formats/Med4Format.cs
	// & https://github.com/neumatho/NostalgicPlayer/blob/main/Source/Agents/Players/Med/MedWorker.cs
	if(!/MED[\x02-\x04]/.test(X.SA(0,4))) return false;
	nV = X.U8(3); switch(nV) {
		//TODO find the non-\x04 files to test on
		case 2: sVersion = "v1.12"; break;
		case 3: sVersion = "v2.00"; break;
		case 4: sVersion = "v2.10+"; break;
	}
	x = 1; p = sz = ptn = ord = trk = midi = syhy = smp = realsmp = 0; smps = []; cs = bad = "";
	if(nV < 3) {
		//TODO
	} else {
		//going through the sample info
		var smpbmp0 = X.U8(4); p = 5;
		while(smpbmp0) {
			if(smpbmp0&1) { var smpbmp = X.U8(p++);
				while(smpbmp) { smp += smpbmp&1; smpbmp >>= 1 } }
			smpbmp0 >>= 1;
		}
		for(i=0; i < smp; i++) {
			fl = X.U8(p++); smpnl = X.U8(p++);
			smps.push(X.SA(p,smpnl)); p += smpnl;
			if(!(fl&1)) p+=2; if(!(fl&2)) p+=2; if(!(fl&4)) p++; if(!(fl&8)) p++;
			if(!(fl&0x30)) svol = X.U8(p++); if(svol > 0x40) return false;
			if(!(fl&0x40)) p++;
		}
		for(i = 62; i >= 0; i--) if(smps[i] && smps[i].length) {realsmp = i+1; break }
		ptn = X.U16(p,_BE); ord = X.U16(p+2,_BE); if(!ord || ord > 0x100) return false;
		p += 4; for(i=0; i < ord; i++) if(X.U8(p++) > ptn) return false;
		extsmp = !(X.U8(p+3)&8);
		tmp0 = X.U16(p,_BE)+"//"+X.U16(p+4,_BE); p += 26;
		for(i=0; i < 16; i++) if(X.U8(p++) > 0x40) return false;
		mvol = X.U8(p++); if(mvol > 0x40) return false;
		if(p > X.Sz()) return false;
		function SkipMidi() { var f = X.U32(p,_BE); p += 4;
			for (var i=0; i < 32; i++) { if(f < 0) f = -f; if((f & 0x80000000)) {midi++; p++ } f <<= 1 } }
		if(nV === 3) { SkipMidi(); SkipMidi() }
		for(i=0; i < ptn; i++) {
			var hdsz = X.U8(p++);
			var ptntrk = X.U8(p); if(trk < ptntrk) trk = ptntrk;
			var rows = X.U8(p+1), hksz = X.U16(p+2,_BE);
//_log("ptn["+i+"] @"+Hex(p)+": ("+Hex(hdsz)+") -> "+Hex(p+hdsz+hksz)+"], rows:"+rows+" trk:"+ptntrk);
			p += hdsz+hksz;
		}
		if(!extsmp) {
			var br = new BitReader(p,_BE), smp2process = 0; br.read(1);
			for(i=0; i < realsmp; i++) smp2process += br.read(1); delete br;
//_log("found "+smp2process+" samples to process");
			p += 8; for(i=0; i < smp2process; i++) {
				hksz = X.U32(p,_BE); hktp = X.U16(p+4,_BE); p += 6;
//_log("smp["+i+"] ("+Hex(p)+" -> "+Hex(p+hksz)+")")
				if(hktp == 0xFFFF || hktp == 0xFFFE) syhy++; p += hksz;
			}
		}
		if(X.c("'MEDV'",p))
			while(p+8 <= X.Sz() && /[A-Z]{4}/.test(X.SA(p,4))) {
				hkhd = X.SA(p,4); hksz = X.U32(p+4,_BE); p += 8;
				switch(hkhd) {
				case "MEDV": sVersion = "v"+X.U8(p+2)+'.'+X.U8(p+3).padStart(2,'0'); break;
				case "ANNO": cs = X.SC(p,hksz,'CP1252'); break;
				case "HLDC": break;
				default: _log("Unknown MED header: "+hkhd) //shouldn't exist but you know how they get
				}
				p += hksz
			}
	}
	sz = p;
	if(sz > X.Sz()) bad = "!short";
	return true
}
if(!bDetected && isOctaMed()) {
	sName = "OctaMED module (.MED)"; bDetected = 1;
	if(bad != "") sVersion += "/malformed"+bad;
	if(X.isVerbose()) {
		sOptionT(cs);
		while(smps[smps.length] == '') delete(smps[smps.length]);
		if(smps.length) sOption("["+smps.join(",")+"]","smps:");
		sOption("ord:"+ord+" ptn:"+ptn+" smp:"+smp+(realsmp != smp ? "("+realsmp+")" : "")
		  +(extsmp?"(ext.)":"")+(syhy?" synth+hybrid:"+syhy:"")+(midi?" midi:"+midi:"")+" trk:"+trk+" tmp0:"+tmp0
		  +" mvol:"+mvol+" sz:"+outSz(sz))
	}
}


function isOctaMedMMD() {
	//from https://web.archive.org/web/20220109073109if_/http://www.textfiles.com/programming/FORMATS/med-form.txt
	if(!X.c("'MMD'")) return false;
	nV = X.U8(3)-48; if(nV < 0 || nV > 3) return false;
	switch(nV) {
		case 0: sname = "MED module (.MED,.MMD0)"; sversion = "v2.1 Med MoDule 0"; break;
		case 1: sname = "OctaMED Professional module (.MED,.MMD1)"; sversion = "v.3.00-4.x"; break;
		case 2: sname = "OctaMED Professional module (.MED,.MMD2)"; sversion = "v5.x"; break;
		case 3: sname = "OctaMED Professional module (.MED,.MMD3)"; sversion = "v6.x?"; break;
	}
	ptnsp = X.U32(0x10,_BE); if(ptnsp < 0x34) return false;
	smpsp = X.U32(0x18,_BE); if(smpsp && smpsp < 0x34) return false;
	expp = X.U32(0x20,_BE); if(expp > X.Sz()) return false;
	sec = p = 0; sngp = X.U32(p+8,_BE);
	if(sngp < 0x34 || sngp > 0xFFFFFEFF-63*8) return false;
	if(X.Sz() < Math.max(sngp+63*8+0x100, ptnsp, smpsp?smpsp:0x34, expp+0x34)) return false;
	songname = anno = iinfo = ""; ch = 4; xsngs = X.U8(0x33); x = expp?xsngs+1:1;
	if(expp) {
		psongname = X.U32(expp+0x2C,_BE);
		if(psongname) {
			songnamelen = X.U32(expp+0x30,_BE);
			if(songnamelen)
				songname = X.SC(psongname,songnamelen,'CP1252');
		}
		pannotxt = X.U32(expp+0x0C,_BE);
		if(pannotxt > 0) {
			annolen = X.U32(expp+0x10,_BE);
			anno = X.SC(pannotxt,annolen,'CP1252');
		}
		pMMDInstrInfo = X.U32(expp+0x14,_BE);
		if(pMMDInstrInfo > 0)
			iinfo = X.SC(expp,40,'CP1252');
	} //if expp
	ptn = 0; ord = []; var ptntp = X.U32(0x10,_BE);
	for(i=0; i < x; i++) {
		p = sngp+63*8;
		ptn1 = X.U16(p,_BE); if(ptn1 > 0x7FFF) ch = 4; ptn += ptn1;
		for(j = 0; j < ptn1; j++) {
			pj = j*4+ptntp; if(pj > X.Sz()) continue;
			pj = X.U32(pj,_BE); if(pj > X.Sz()) continue;
			pj = nV < 1 ? X.U8(pj+4) : X.U16(pj+4,_BE); if(pj > ch) ch = pj
		}
		if(nV < 2) { ord[0] = X.U16(p+2,_BE); if(ord[0] > 256) return false }
		else {
			sec = X.U16(p+2,_BE); trk = X.U16(p+0x10,_BE);
			if(!trk || trk > 0x40) return false;
			sectp = X.U32(p+8,_BE); if(sectp+sec*2 > X.Sz()) continue;
			playseqtp = X.U32(p+4,_BE); nplayseq = X.U16(p+0x12,_BE);
			secs = []; for(j = 0; j < sec; j++) secs.push(X.U16(sectp+j*2,_BE));
			for(j = 0; j < secs.length; j++) if(j <= nplayseq)
				ord.push(X.U16(X.U32(playseqtp,_BE)+0x28,_BE));
		}
		expp = X.U32(sngp+0x20,_BE);
		if(expp && (X.U32(expp,_BE) < sngp || expp > X.Sz())) { x = i+1; break }
		sngp = X.U32(expp,_BE);
	}
	smp = X.U8(p+0x11B); if(smp > 63) return false;
	return true
}
if(!bDetected && isOctaMedMMD()) {
	sName = sname; sVersion = sversion; bDetected = 1;
	if(X.isVerbose()) {
		if(songname != "<unnamed>" && songname != "<ohne Namen>")
			sOption(songname);
		sOptionT(anno);
		sOptionT(iinfo,"ins0:");
		if(x > 1) sOption(x,"×");
		sOption((X.isDeepScan()?"ch:"+ch+" ":"")+"ord:"+ord.join("+")+" ptn:"+ptn+" smp:"+smp+" sz:"+outSz(X.U32(4,_BE)))
	}
}


function isAHX() {
	//ref http://lclevy.free.fr/exotica/ahx/ahxformat.txt
	// & https://github.com/pete-gordon/hivelytracker/blob/master/Replayer_Windows/hvl_replay.c
	if(X.Sz() < 30) return false;
	if(X.c("'THX'0.")) fmt = "ahx";
	else if(X.c("'HVL'0.")) fmt = "hvl";
	else return false;
	bad = 0;
	var b6 = X.U8(6);
	trk0saved = b6 >> 7;
	switch((b6 >> 4) & 7) {
	case 0: spd = "50Hz"; break;
	case 1: spd = "100Hz"; break;
	case 2: spd = "150Hz"; break;
	case 3: spd = "200Hz"; break;
	default: bad++; spd = "?Hz"
	}
	ord = X.U16(6,_BE) & 0xFFF;
	if(ord > 999) bad++; if(!ord || ord > 1024) bad++;
	if(fmt === "ahx") {
		lp = X.U16(8,_BE); if(lp >= ord) bad++;
	}
	else chn = (X.U8(8) >> 2)+4;
	if(bad > 1) return false;
	trl = X.U8(10); if(!trl || trl > 64) return false;
	trk = X.U8(11);
	ins = X.U8(12); if(ins > 63) return false;
	sub = X.U8(13);
	if(fmt === "ahx") {
		sz = 14+sub*2+ord*8+trk*trl*3; if(!trk0saved) sz += trl*3
		for(i=0; i < ins; i++) {
			var spls = X.U8(sz+21);
			sz += 22+spls*4;
		}
	}
	else {
		sz = 16+sub*2+ord*chn*2;
		if(trk0saved) i = 1; else i = 0;
		for(; i <= trk; i++)
			for(j=0; j < trl; j++) {
				if(X.U8(sz) == 0x3F) { sz++; continue }
				sz += 5;
			}
		for(i=0; i < ins; i++) {
			var spls = X.U8(sz+21);
			sz += 22+spls*5;
		}
	}
	title = "";
	for(i=0;i <= ins;i++) { var r = sz;
		while(X.U8(sz) && sz<X.Sz()) sz++;
		if(!i) title = X.SA(r,sz-r); sz++
	}
	if(sz > X.Sz()) bad++;
	return true
}
if(!bDetected && isAHX()) {
	if(fmt === "ahx") {
		sName = "Abyss' Highest eXperience module (.AHX)";
		if(!X.U8(3)) sVersion = "v1.00~1.27"; else sVersion = "v2.0+";
	}
	else
		sName = "Hively Tracker module (.HVL)";
	bDetected = 1;
	if(bad) sVersion += "/malformed"+bad;
	if(X.isVerbose()) {
		if(title != "") sOption(title); if(sub) sOption(sub,"×");
		n=0; p = 14+sub*2+ord*8; c = trk*trl; if(!trk0saved) c += trl; hp = false;
		for(i=0;i < c;i++) {
			note = X.U8(p+i*3)>>2; if(note) n++;
			if(note > 60) if(fmt == "ahx") hp = true;
		}
		if(hp) sVersion += "/hi-pitch!"
		if(fmt === "ahx")
			sOption("spd:"+spd+" ord:"+ord+" lp:"+lp+" trk:"+trk+" ins:"+ins+" notes:"+n+" sz:"+outSz(sz))
		else
			sOption("ch:"+chn+" spd:"+spd+" ord:"+ord+" trk:"+trk+" ins:"+ins+" notes:"+n+" sz:"+outSz(sz))
	}
}


function isPha() {
	// ref http://asle.free.fr/prowiz/prowiz.src.zip / r/pha.c
	if(X.Sz() < 0x3C0 || !X.c("000003C0",8)) return;
	//!(X.U8(0) < 0x30 && isWithin(X.U8(2), 0x60,0x80) && X.U8(3) <= 0x40 && X.U8(4) < 0x10 && X.c("00400600", 0x1B6)
	ins = 0; smp = []; var allsmpsz = olds = 0;
	for(p=0; p < 0x1B2; p+=14) {
		if(X.U8(p+3) > 0x40 || (ssz=X.U16(p,_BE)) < X.U16(p+4,_BE)) return; //vol > 64, smpsz < lpstart
		if(!isWithin(sofs=X.U32(p+8,_BE), 0x3C0,X.Sz()) || sofs%2) return; // smpp
		allsmpsz += ssz << 1; if(X.U16(p,_BE)) ins++; if(!smp.includes(sofs)) smp.push(sofs);
		if(p && Math.abs(sofs-olds) > 2) return;
		olds = sofs + (ssz << 1)
	}
	smp = smp.length;
	if(!isWithin(allsmpsz, 3,31*65535)) return;
	var ptnM = 0, ptnm = 0xFFFFFFFF; ptn = [];
	for(p=0x1C0; p < 0x3C0; p+=4) {
		t = X.U32(p,_BE); if(t+2 < allsmpsz+0x3C0 || t > X.Sz()) return;
		ptnM = Math.max(ptnM,t); ptnm = Math.min(ptnm,t);
		if(!ptn.includes(t)) ptn.push(t);
	}
	if(ptnm%2 || Math.abs(0x3C0+allsmpsz-ptnm) > 2) return;
	for(ord=127,p = 0x3B8; p > 0x1C0; p-=4,ord--) if(X.U32(p,_BE) != t) break; else t = X.U32(p,_BE);
	ptn = ptn.length; p = ptnM;
	for(i = 0; i < 0x100; i++) if(X.U8(p) >= 0xC0) { i += 254-X.U8(p+1); p += 2 } else p += 4;
	sz = p;
	return true
}
if(!bDetected && isPha()) {
	sName = "Azatoth/Phenomena's Pha Packer module (.PHA)"; bDetected = 1;
	if(X.isVerbose()) {
		sOption('ord:'+ord+' ptn:'+ptn+' ins:'+ins+' smp:'+smp+' sz:'+outSz(sz))
	}
}


function isSTM() {
	//ref https://github.com/OpenMPT/openmpt/blob/master/soundlib/Load_stm.cpp
	t = X.readBytes(0x14,8); for(i = 0; i < 8; i++) { if(t[i] < 0x20 || t[i] >= 0x7F) return false }
	function _isstm() {
		if([0x1A,2].indexOf(X.U8(0x1C)) < 0) return false;
		if([1,2].indexOf(X.U8(0x1D)) < 0) return false;
		if(!X.c("02",0x1E)) return false;
		if([0,10,20,21].indexOf(X.U8(0x1F)) < 0) return false;
		if(X.U8(0x21) > 0x40) return false;
		gvol = X.U8(0x22);
		if(gvol > 0x40 && gvol != 0x58) return false;
		nVm = X.U8(0x1F); ptn = X.U8(0x21);
		if(nVm) maxord = 128; else maxord = 64;
		minsz = 0x30+31*0x20+maxord+ptn*256; if(X.Sz() < minsz) return false;
		smp = 0; ord = -1; tracker = "Sami 'Psi' Tammilehto's Scream Tracker 2 module"; ext = "STM"; return true
	}
	function _isstx() {
		if(!X.c("'SCRM'",0x3C)) return false;
		ptnsz = X.U16(0x1C); if((ptnsz < 64 && ptnsz != 0x1A) || ptnsz > 0x840) return false;
		if(X.U16(0x1E) || X.U32(0x26) || X.U32(0x2C) != 1) return false;
		t = X.U8(0x2A); if(t > 0x40 && t != 0x58) return false;
		ptn = X.U16(0x30); if(ptn > 64) return false;
		smp = X.U16(0x32); if(smp > 96) return false;
		ord = X.U16(0x34); if(ord > 0x81 && ord != 0x101) return false;
		ptntp = X.U16(0x20)<<4; smptp = X.U16(0x22)<<4; chtp = X.U16(0x24)<<4;
		minsz = 0x40 + Math.max(ptntp+ptn*2, smptp+smp*2, chtp+32+ord*5); if(X.Sz() < minsz) return false;
		if(findIntersections([[ptntp,ptn*2], [smptp,smp*2],[chtp,32+ord*5]]).length) return false;
		for(p=chtp+32,i=0; i < ord; i++,p+=5) {
			t = X.U8(p); if(t > 63 && t != 99 && t != 255) return false;
		}
		tracker = "Sami 'Psi' Tammilehto's Scream Tracker Music Interface Kit module"; ext = "STX"; return true;
	}
	isstm = _isstm(); if(!isstm) { isstx = _isstx(); if(!isstx) return false }
	sz = sz1 = minsz; smpn = []; max = 0;

	if(isstm) { //STM things
		for(i = 0; i < 31; i++) {
			zero = X.U8(0x30+i*0x20+12);
			if(zero && zero != 46) return false;
			smpn.push(X.SC(0x30+i*0x20,12,'CP437').trim());
			var sofs = X.U16(0x30+i*0x20+0x0E,_LE) << 4;
			var ssz = X.U16(0x30+i*0x20+0x10,_LE);
			if(ssz && sofs > 0x30 && sofs < X.Sz()) {
				if(ssz) sz1 = sofs+ssz;
				//Actually it seems that if ssz is 1 it still must be 0, but then it'll just ignore
				//  the potentially interesting byte, so let's rip it too, just in case.
				if(sz1 > sz) sz = sz1;
				//We won't count it towards the smp counter though, that one's for the meaningful stuff.
				if(ssz > 1) smp++
			}
		}
		for(i=0; i < maxord; i++) {
			t = X.U8(0x410+i);
			if(t === 99 || t === 255) t = 255;
			else if(t > 63) return false;
			else ord++;
		} ord++;
	}
	else if(isstx) { //STX things
		p = smptp; for(i = 0; i < smp; i++) {
			si = X.U16(p+i*2) << 4;
			smpn.push(X.SC(si+0x30,28,'CP437').trim());
			if(!si) continue;
			if(X.U8(si) === 1) {
				sofs = X.U16(si+14)<<4;
				if(sofs > max) {
					ssz = X.U32(si+16); if(X.U8(si+31)&4) ssz *= 2; //16bit
					//if(ssz%16) ssz += 16-ssz%16; //the padding. Although STM2STX.EXE v1.0 doesn't pad
					max = sofs; if(sz < max+ssz) sz = max+ssz
				}
			}
		}
		fmt = 1;
		if(ptntp && ptnsz != 0x1A) {
			p = X.U16(ptntp) << 4;
			if(p > X.Sz())
				if(X.isHeuristicScan()) {
					bad = bad.addIfNone("!badptnp"); return true
				}
				else return false;
			if(X.U16(p) === ptnsz) fmt = 0;
		}
		for(i=0,p=ptntp; i < ptn; i++,p+=2) {
			if((t=(X.U16(p)<<4)) > X.Sz())
				if(X.isHeuristicScan()) {
					bad = bad.addIfNone("!badptnp"); return true
				}
				if(fmt == 0 && X.U16(t) > 0x840) return false
		} 
	}
	else return false; return true
}
if(!bDetected && isSTM()) {
	sName = tracker+" (."+ext+")"; bDetected = 1;
	if(X.isVerbose()) {
		sOptionT(X.SC(0,0x14,'CP437'));
		sOptionT(addEllipsis(smpn.filter(funSampleName).join(' ')),'smp/msg: "','"');
		if(ext === "STM") sVersion = "v"+X.U8(0x1E)+"."+nVm;
		else /*STX*/ sVersion = "v1."+fmt;
		tmp0 = X.U8(0x20); gvol = X.U8(0x22);
		sOption("ord:"+ord+" ptn:"+ptn+" smp:"+smp+" gvol:"+gvol+" sz:"+outSz(sz))
	}
}


function isIT() {
	//from https://github.com/OpenMPT/openmpt/blob/master/soundlib/Load_it.cpp et al.
	// Now, this detection is TOO MUCH
	// I'm still going to do as much as I can, but people should have a choice to live without most of it :D
	// Which is to say, do NOT use deepscan unless you need calcsize
	var t = 0;
	if(X.c("'IMPM'")) type = "it";
	else if(X.c("'tpm.'")) type = "mpt";
	else return false;
	ord = X.U16(0x20,_LE); if(!ord) t++;
	ins = X.U16(0x22,_LE); if(ins > 0x63) t++;
	smp = X.U16(0x24,_LE); if(smp > 255) t++;
	ptn = X.U16(0x26,_LE); if(!ptn && !X.isHeuristicScan()) t++;
	if(X.Sz() < 0xC0+4*(ins+smp+ptn)) return false; //itfileheader size
	cwtv /*make*/ = X.U16(0x28,_LE); cmwt /*compat*/ = X.U16(0x2A,_LE);
	flags = X.U16(0x2C,_LE); special = X.U16(0x2E,_LE);
	gvol = X.U8(0x30); if(gvol > 0x80) t++;
	mvol = X.U8(0x31); if(mvol > 0x80) t++;
	spd0 = X.U8(0x32); if(!spd0) t++;
	tmp0 = X.U8(0x33); if(tmp0 < 31) t++;
	sep = X.U8(0x34); if(sep > 0x80) t++;
	msglen = X.U16(0x36,_LE); msgofs = X.U32(0x38,_LE); if(msgofs+msglen > X.Sz()) t++;
	if(t > 2) return false; bad = ""; if(t) bad = bad.addIfNone("!badinitinfo");
	pwd = X.U8(0x35);
	nreserved = X.U32(0x3C,_LE); sreserved = X.SA(0x3C,4);
	tracker = auth = ""; mVlsw = mVcw = sV ="?"; ch = 1; cord = 0; sz = -1;
	var mptp = 0, lastsmpofs = 0; type = "it"; chnm = []; insnlst = []; smpnlst = [];
	if(X.isDeepScan()) {
		//The DeepScan part of the crazily complex Impulse Tracker/MPTM detection
		//Would be much easier if we didn't have to calculate the size! But we do, so there we go.
		const ITSampleSz = 0x50, ITHistorySz = 8, verLoadLimit = 0x1000;
		const sEmbedMsg = 1, sEditHistory = 2, sEmbedPtnHL = 4, sEmbedMIDIC = 8;
		const fEmbedMIDIC = 0x80, fEFR = 0x1000;
		var interpretModPlugMade = false; var FFchnpan = X.fSig(0x40,0x40,"FF") >= 0;
		var mptV = 0;
		function itV(v) { return v.slice(0,1)+'.'+v.slice(1,3)+'.'+v.slice(3,5)+'.'+v.slice(5,7) }

		//TODO this↓ part is not ripper-friendly but thankfully also likely unnecessary:
		if(X.c("'tpm.'")) { type = "mpt";
			p = X.Sz()-4; mptp = X.U32(p,_LE);
			if(X.c("'228'04'mptm'",mptp)) sz = X.Sz(); else mptp = 0;
		} else {
			if(0x888 < cwtv <= 0xFFF) {
				p = X.Sz()-4; mptp = X.U32(p,_LE);
				if(0x100 <= mptp < X.Sz()-4)
					if(X.c("'228'04'mptm'",mptp)) {
						type = "mpt"; sz = X.Sz(); if(cwtv >= verLoadLimit) {
							sV = "future"; charset = "UTF8"; extsmp = 0;  return true }
					} else mptp = 0;
				//if the above fails, there's a near-zero chance the ords will be misread and everything goes oof
				//that's expected behaviour, as the v1.17.02.46 format was experimental :)
			}
			//detecting made-with, part 1 of 3496987
			if(type === "it") {
				if((cwtv & 0xF000) === 0x5000) {
					mptV = (cwtv&0xFFF) << 16;
					if(X.c("'OMPT'",0x3C)) interpretModPlugMade = true;
					else if(mptV >= 0x01290000) mptV |= nreserved & 0xFFFF;
					mVlsw = itV(mptV.toString(16).toUpperCase().padStart(7,'0'));
				} else if(cwtv === 0x888 || cmwt === 0x888) {
					interpretModPlugMade = true; mVlsw = "1.17.00.00";
				} else if(cwtv === 0x214 && cmwt === 0x202 && !nreserved) {
					mVlsw = "1.09.00.00"; tracker = "ModPlug Tracker b3.2 - 1.09";
					interpretModPlugMade = true
				} else if(cwtv === 0x300 && cmwt === 0x300 && !nreserved
							&& ord === 256 && sep === 128 && !pwd) {
					mVlsw = "1.17.02.20"; interpretModPlugMade = true
				}
			}
		}
		var b32ord = -1, lastord = 0;
		// 32bit orders: a deprecated format. OpenMPT 1.17.02.46 - 1.17.02.48.
		if(type === "mpt" && 0x88A < cwtv && cwtv <= 0x88D) {
			p = 0xC0; if(X.U16(p,_LE)) return false; p += 2;
			b32ord = X.U32(p,_LE); p += 4;
			if(b32ord > 256 || X.Sz() < p+b32ord*4) return false
		} else p = 0xC0;
		if(b32ord >= 0) ord = b32ord;
		for(i=0; i < ord; i++)
			if(b32ord < 0) { o = X.U8(p++);
				if(o === 0xFE) lastord = 0xFFFE; else { cord++; if(o === 0xFF) lastord = 0xFFFF } }
			else { o = X.U32(p,_LE); p += 4; lastord = o; cord++ }

//_log("read pointers & find the smallest @"+Hex(p));
		//TODO maybe, at some point in the future, autodetect b32ord
		//by making sure these pointers actually lead to intended things, and trying the b32 way if not
		minp = 0xFFFFFFFF;
		var insp = []; for(i=0; i < ins; i++) { t = X.U32(p,_LE); p += 4;
			if(t <= p || t > X.Sz()) return false;
			q = X.readBytes(t+0x20,0x1A,true); q = decEncoding(q,CP437,false).trim(); if(q != "") insnlst.push(q);
			if(sz < t) sz = t;  if(t && t < minp) minp = t; insp.push(t) }
		var smpp = []; for(i=0; i < smp; i++) { t = X.U32(p,_LE); p += 4;
			if(t > X.Sz()) bad = bad.addIfNone("!short");
			q = X.readBytes(t+0x14,0x1A,true); q = decEncoding(q,CP437,false).trim(); if(q != "") smpnlst.push(q);
			if(sz < t) sz = t; if(t && t < minp) minp = t; smpp.push(t) }
		var ptnp = []; for(i=0; i < ptn; i++) { t = X.U32(p,_LE); p += 4;
			if((t > 0x1E && t <= p) || t > X.Sz()) return false;
			if(sz < t) sz = t; if(t && t < minp) minp = t; ptnp.push(t) }
		if(special & sEmbedMsg) { minp = Math.min(minp,msgofs);
			if(sz < msgofs+msglen) sz = msgofs+msglen }
		var possiblyUNMO3 = cmwt === 0x214 && (!cwtv || cwtv === 0x214)
		  && !X.U16(0x1E) && !pwd && !nreserved && !(flags & 0xC0);
		if(possiblyUNMO3 && !ins && smp && p+4*smpp.length+2 <= minp) {
			var oldUNMO3 = true;
			for(i=0; i < smp; i++)
				if(X.U32(p)) { oldUNMO3 = false; p -= i*4; break } else p += 4;
			if(oldUNMO3) tracker = "UNMO3 <= v2.4";
		}
		if(possiblyUNMO3 && !cwtv) tracker = "UNMO3 v0/1";
		if(special & sEditHistory) {
			//is edit history stored?
			var nflt = X.U16(p);
			if(X.Sz() > p+nflt*8 && p+nflt*8 <= minp) {
				p += 2+nflt*8;
				if(possiblyUNMO3 && !nflt)
					if(special & sEmbedPtnHL) tracker = "UNMO3 <= 2.4.0.1";
					else tracker = "UNMO3"
			}
		} else if(possiblyUNMO3 && special <= 1 && !X.U16(p)) {
			tracker = "UNMO3 <= 2.4"; p += 2
		}
		var hasMIDIC = (flags & fEmbedMIDIC) || (special & sEmbedMIDIC), u = 0;
		if(hasMIDIC) p += 0x1320; if(p > X.Sz()) return false;
//_log("chunks expected @"+Hex(p));
		var isBeroTracker = hasPluginChunks = hasMPTM = false;
		while(p+9 < X.Sz()) {
			hkhd = X.SA(p,4); hksz = X.U32(p+4,_LE);
//_log(hkhd+":"+Hex(p));
			if(hkhd === "MODU") isBeroTracker = true;
			else if(hkhd === "CNAM") { ch = hksz/20; u = p+8; hasMPTM = true;
				for(i = 0; i < ch; i++) {
					t = X.SA(u,20).trim(); u += 20;
					if(t != "") chnm.push(t);
				}
			}
			else if(hkhd === "PNAM") hasMPTM = true;
			else if(hkhd === "CHFX" || /F[X0-9]\d\d/.test(hkhd)) hasPluginChunks = true;
			if(["IMPI","IMPS","XTPM","STPM"].indexOf(hkhd) >= 0
			  || p+8+hksz > X.Sz() || p >= msgofs)
				break;
			//PNAM may have fun info but that's for when someone feels less tedium otherwise
			else p += 8+hksz
		}
		if(cwtv === 0x217 && cmwt === 0x200 && !nreserved && !isBeroTracker) {
			if(hasMPTM || (ord > 0 && lastord == 0xFFFF) || FFchnpan) {
				mVlsw = "1.16.00.00"; tracker = "ModPlug Tracker 1.09-16";
			} else {
				mVlsw = "1.17.00.00"; tracker = "OpenMPT 1.17 (compat.export)";
			}
			interpretModPlugMade = true
		}

		var lastsmpofs = (smp > 0 ? smpp[smp-1]+ITSampleSz : p); var lastsmpco = false;

// Deriving the size of just the last sample (it might already be tasking indeed)
		const/*for IT sample flags*/ s16b=2,sSt=4,sCo=8;
		const/*for cvt flags*/ cSS=1,cBE=2,cD=4,cTo16=8,cOPL=0x40,cExt=0x80,cPCM=0xFF;
		 //Possible codec values:
		 //"sPCM" "uPCM" "dPCM" "fPCM" "IT214" "IT215" "AMS" "DMF" "MDL" "8d16"
		 //"ADPCM" "MT2" "fPCM15" "fPCM23" "fPCMn" "sPCMn" "uLaw" "aLaw"
		extsmp = []; var possibleXMconv = false;
		Ss = []; //for gathering all info
		maxsmpofs = -1;
		for(i=0; i < smp; i++) {
			if(!smpp[i]) continue;
			if(smpp[i] > X.Sz()) { bad = bad.addIfNone("!short"); continue }
			if(!X.c("'IMPS'",smpp[i])) bad = bad.addIfNone("!badsmp");
//_log(X.SA(smpp[i]+4,13)+": "+decAnsi(smpp[i]+0x14,0x1A,CP437,false).trim()+" @"+Hex(smpp[i]))
			var sf = X.U8(smpp[i]+0x12); var cvt = X.U8(smpp[i]+0x2E);
			var slen = X.U32(smpp[i]+0x30); var smpofs = X.U32(smpp[i]+0x48);
			S = { "ofs":0, "slen":0,
				"bits":(sf & s16b)? 16 : 8, "chn":1,// 1 channel if mono, 2 if joint or split stereo
				"en":"LE","codec":"sPCM","bps":0,"length":0 //length is calculated from length in samples
			};
			p = smpofs; S["ofs"] = smpofs; S["slen"] = slen;
			if(cvt === cOPL) p += 12;
			else if(cvt === cExt) {
				t = readVarUInt(p); extsmp.push(X.SA(p+1,t[1]));
				p += t[0]+t[1]; }
			else if(X.c("'fLaC'",smpofs)) { S["codec"] = "FLAC" }
			else if(X.c("'OggS'",smpofs)) { S["codec"] = "Ogg" }
			else { //GetSampleFormat
				S["codec"] = (cvt & cSS) ? "sPCM" : "uPCM";
				if((sf & sSt) && cwtv >= 0x214) S["chn"] = 2;
				if(sf & sCo)
					S["codec"] = (cvt & cD) ? "IT215" : "IT214";
				else {
					if(!(sf & s16b) && cvt == cPCM) S["codec"] = "ADPCM";
					else {
						if(cvt & cBE) S["en"] = "BE";
						if(cvt & cD) S["codec"] = "dPCM";
						if(cvt & cTo16) S["codec"] = "8d16"
					}
				}
				switch(S["codec"]) {
				case "sPCM": case "uPCM": case "dPCM": case "fPCM": case "MT2": case "fPCM15":
				case "fPCM23": case "fPCMn": case "sPCMn": S["bps"] = S["bits"]; break;
				case "8d16": S["bps"] = 16; break;
				case "ADPCM": S["bps"] = 4; break; //encoded header size for this one is 16, 0 otherwise
				case "uLaw": case "aLaw": S["bps"] = 8; break;
				//case "IT214": case "IT215": case "AMS": case "DMF": case "MDL": S["bps"] = 0; //and
				default: S["bps"] = 0 //variable length-encoded
				}
				if(!S["bps"]) lastsmpco = true; //...and we'll decompress it further down
				else {
					S["length"] = slen*S["chn"]*(S["bps"]>>3);
					p += S["length"];
				}
				Ss[i] = S;
			} //GetSampleFormat
			if(lastsmpofs < p) lastsmpofs = p; if(sz < lastsmpofs) sz = lastsmpofs;
			if(S["codec"] === "uPCM" && slen) possibleXMconv = true;
//_log(X.SA(smpp[i]+0x14,0x1A)+" @"+Hex(smpofs)+": sf:"+Hex(sf)+" ["+S["codec"]+"] slen:"+slen+" ch:"+S["chn"]+" bps:"+S["bps"]+" bits:"+S["bits"]+", ends @"+Hex(smpofs+S["length"]));
		}
// Load_it.cpp:845
		if(possibleXMconv && cwtv === 0x204 && cmwt === 0x200 && !special && !nreserved
		  && (flags & (~8)) === 0x15 && gvol === 128 && mvol === 48
		  && sep === 128 && !pwd && !msglen) {
			for(i=0; i < 64; i++) if([0x20,0x40].indexOf(X.U8(0x40+i)) < 0) {
				possibleXMconv = false; break } //testing pans
			if(possibleXMconv) for(i=0; i < 64; i++) if(X.U8(0x80+i) != 0x40) {
				possibleXMconv = false; break } //testing vols
			if(possibleXMconv) for(i=20; i < 26; i++) if(X.U8(4+i)) {
				possibleXMconv = false; break } //testing songname
			if(possibleXMconv) tracker = "XM Conversion"
		}
// Load_it.cpp:885 :: Scan all of the patterns to see how many channels we need
		for(i=0; i < ptn; i++) { t = ptnp[i];
			if(!t || t > X.Sz()) continue;
			var len = X.U16(t,_LE), rows = X.U16(t+2,_LE); t += 4;
			if(!rows || rows > 0x400 || t+4 > X.Sz()) continue; p = t+4;
			var chnMask = [], row = 0;
			while(row < rows && p < X.Sz()) {
				var y = X.U8(p++); if(!y) { row++; continue }
				var c = y&0x7F; if(c) c--;
				if(y&0x80) chnMask[c] = X.U8(p++);
				if(chnMask[c] & 0x0F) if(ch <= c && c < 0x7F) ch = c+1;
				if(chnMask[c] & 1) p++;  if(chnMask[c] & 2) p++;
				if(chnMask[c] & 4) p++;  if(chnMask[c] & 8) p += 2;
			}
			if(lastsmpofs < p) lastsmpofs = p; if(sz < lastsmpofs) sz = lastsmpofs
		}
		if(!ch) ch = 1;
//find the expected length of the final compressed sample
		if(lastsmpofs) { p = lastsmpofs;
			if(lastsmpco) {
				S = Ss[Ss.length-1];
				//smp length calc
				if(S["bps"])
					if(S["bps"]%8) S["length"] = (S["codec"] == "ADPCM"?16:0)
							+Math.ceil((slen+1)/2*S["ch"]);
					else S["length"] = (S["codec"] == "ADPCM"?16:0)
							+S["slen"]*S["bps"]/8*S["chn"];
				else { //let the crazy shit begin.
					// ref https://github.com/nicolasgramlich/AndEngineMODPlayerExtension/blob/master/jni/loaders/itsex.c
					// but mainly OpenMPT's SampleIO::ReadSample :142
					if(S["bps"] == 4 && S["chn"] == 1 && S["en"] == "LE" && S["codec"] == "ADPCM") {
						S["length"] = 16+(S["slen"]+1)/2; p += S["length"]
					} else if(S["slen"] > 1 && (S["codec"] === "IT214" || S["codec"] === "IT215")) { //:165
						// Let's try a quick-n-dirty first.
						var u = p, ulen = 0, quickndirty = 0;
						while(u < X.Sz() && ulen < S["slen"] /*packers have their limits*/) {
							if(X.c("'XTPM'",u) || X.c("'STPM'",u) || X.c("'228'04'mptm'",u))
								{ quickndirty = 1; p = u; break }
							ucsz = X.U16(u,_LE); ulen += ucsz; u += 2+ucsz;
						}
						if(quickndirty) { p = u } else {
						//orrrr if no MPTX block is present, we gotta do it the hard way I guess :<
						//ITDecompression(file,sample,getencoding=it215)
						var bit_n = bit_buf = cend = readdef = 0;
						function readBits(b) {
							while(bit_n < b) { bit_buf |= Util.shlu64(X.U8(p++),bit_n); bit_n += 8 }
							var v = bit_buf & (Util.shlu64(1,b) - 1); bit_buf = Util.shru64(bit_buf,b); bit_n -= b;
							//if(p <= X.Sz()) return v; else return 0 }
							return v }

						if(S["bits"] == 16) { fetchA=4, lowerB=-8, upperB=7, defW=17 }
						else { fetchA=3, lowerB=-4, upperB=3, defW=9 }
						for(c = 0; c < S["chn"]; c++) { //ITCompression.cpp:322
							var rs = S["slen"], v = csz = clen = topbit = 0;
							cend = p;
							while(rs && p < X.Sz()) { //:325
								csz = X.U16(p,_LE); p += 2; cend = p+csz;
								if(!csz) { readdef = 10; _log("IT @"+Hex(p)+" malformed sample?"); continue }
								if(cend > X.Sz()) { c = 0xE0F; bad = bad.addIfNone("!short"); break }
								clen = Math.min(rs, 0x8000>>(S["bits"]>>4));
								var w = defW; bit_buf = bit_n = 0;
								while(clen && p < X.Sz()) { //:357
									if(w > defW) {
//_log("! IT @"+Hex(p)+" w="+Hex(w)+" > "+defW+" rs:"+rs+" clen:"+Hex(clen)+" cend:"+Hex(cend));
										bad = bad.addIfNone("!badITsmp"); rs = 0; break }
									v = readBits(w); topbit = 1<<(w-1);
									if(w < 7) {
										if(v == topbit) {
											v = readBits(fetchA)+1; if(v >= w) v++; w = v; continue
										}
									} else if(w < defW) {
										if(v >= topbit+lowerB && v <= topbit+upperB) {
											v -= topbit-1+lowerB; if(v >= w) v++; w = v;
											continue
										}
									} else if(v & topbit) { w = (v&0xFF)+1; continue }
									clen--; rs--
								}
							}
							if(p > X.Sz()) bad = bad.addIfNone("!short");
						} } //if quickndirty else
					} //if IT215||IT214
					else if(S["codec"] === "FLAC") {
						//TODO, or maybe not
					} else if(S["codec"] === "Ogg") {
						//TODO, or maybe not
					} else if(S["codec"] === "AMS" && S["chn"] == 1) { //SampleIO:170
						//TODO test it at least
						sOption("\n!!! Please send this file to the detector dev!!! Poor fella hasn't any to test on")
						var srcsz = X.U32(p+4,_LE), pc = X.U8(p+8);
						S["length"] = srcsz+9
					} else if(S["codec"] === "8d16" && S["chn"] == 1 && S["bps"] == 16) {
						sOption("\n!!! Please send this file to the detector dev!!! Poor fella hasn't any to test on")
						//TODO CopyMonoSample<SC::DecodeInt16Delta8>(sample, source, fsize)
					} else if(S["codec"] === "MDL" && S["chn"] == 1 && S["bps"] <= 16) {
						var srcsz = X.U32(p); S["length"] = 4+srcsz
					} else if(S["codec"] === "DMF" && S["chn"] == 1 && S["bps"] <= 16) {
						sOption("\n!!! Please send this file to the detector dev!!! Poor fella hasn't any to test on")
						//TODO DMFUnpack, or not to do
					} else if((S["codec"] === "uLaw" || S["codec"] === "aLaw") /*TODO && mono or stereoInterleaved */ && S["bps"] == 16) {
						S["length"] = S["slen"]*S["chn"]
					}
				} //actual length teller
				if(sz < p) sz = p
			}
		}
		var bLoadXIProps = false;
		if(X.c("'XTPM'",p)) { // 'MPTX'
			p += 4;
			while(p+6 < X.Sz()) {
				t = X.SA(p,4); it = X.U32(p);
				if(t === "STPM" || t === "228\x04" || (it&0x80808080) || !(it&0x60606060)) break;
				var prsz = X.U16(p+4,_LE); p += 6;
				for(i=0; i < ins; i++) p += prsz
			}
			bLoadXIProps = true;
			if(sz < p) sz = p;
		}
// Load_it.cpp:978
		interpretModPlugMade |= bLoadXIProps;
// Load_it.cpp:985
		if(X.c("'STPM'",p)) {
			p += 4;
			while(p+7 < X.Sz()) {
				t = X.SA(p,4); it = X.U32(p);
				prsz = X.U16(p+4,_LE);
				if(t === "228\x04" || t == "228\x02") { mptp = p; break; } //TODO check for 228\x02TC!
				if((it&0x80808080) || !(it&0x60606060) || p+6+prsz > X.Sz()) break;
				p += 6;
				switch(t) {
				case "...C": var chn = 0;
					switch(prsz) {
					case 2: chn = X.U16(p,_LE); break; case 3: chn = X.U24(p,_LE); break;
					case 4: chn = X.U32(p,_LE); break; default: chn = X.U8(p) }
					if(chn > ch) ch = chn; break;
				case ".VWC": var v = 0;
					switch(prsz) {
					case 1: v = X.U8(p); break; case 2: v = X.U16(p,_LE); break;
					case 3: v = X.U24(p,_LE); break; case 8: v = X.U64(p,_LE); break;
					default: v = X.U32(p,_LE) }
					if(v) mVcw = itV(v.toString(16).toUpperCase().padStart(7,'0')); break;
				case "VWSL": var v = 0;
					switch(prsz) {
					case 1: v = X.U8(p); break; case 2: v = X.U16(p,_LE); break;
					case 3: v = X.U24(p,_LE); break; case 8: v = X.U64(p,_LE); break;
					default: v = X.U32(p,_LE) }
					if(v) mVlsw = itV(v.toString(16).toUpperCase().padStart(7,'0')); break;
				case "AUTH":
					auth = X.SC(p,prsz,"UTF8"); break;
				}
				p += prsz;
			}
			if(sz < p) sz = p;
		}

		tunings = 0;
		if(X.c("'HSCT'",p)) { //the first-ever way of storing the tunings in an mptm
//tuningCollection.cpp
			var skip = false; var v = X.I32(p+4,_LE); p += 8;
			if(v == 1) { //can't find any files to test  this
				sOption("Please send this file to the detection author!!! This is ultra-rare, how did you make it?!");
				p += X.U32(p); p += 4+Math.min(256,l)
			} else if( v == 2 ) {
//_log("tunings text: "+X.UCSD(p));
				p += 1+X.U8(p);
			} else { skip = true; bad = bad.addIfNone("!badtuningver") }
			if(!skip) {
				tunings = X.U32(p+2); p += 6;
				t = X.fSig(p,0x1000,"'FSCT'"); if(t >= p) p = t+4;
			}
			if(sz < p) sz = p;
		}
		else if(X.c("'228'04'mptm'",p)) { //TODO there can be old-ass tunings instead here
			if(!mptp) mptp = p;
// Load_it.cpp:1299
			function readaintle(bits) { //ReadAdaptiveInt32LE et al.
				var b = 0, add = 0, r = 0, oldp = p;
				if(p+1 < X.Sz()) {  b = X.U8(p++);
					if(bits == 16) add = b & 1;  else if(bits == 32) add = b & 3;
					else if(bits == 64) add = (1<<(b&3))-1;  else return 0;
					if(bits == 16) r = b >> 1;  else r = b >> 2;
					for(var i=0; i < add; i++) {  b = 0;
						if(p+1 < X.Sz()) { b = X.U8(p++); r |= b << ((i+1)*8-2);
						} else return 0 }
				} else return 0;
				return r
			}
// serialization_utils.cpp:399
			function mptBeginRead() {
				if(!X.c("'228'",p)) return false; posstart = p; p += 3;
				var l = X.U8(p++); h = X.SA(p,l); p += l;
				flagbyte = 0; hd = X.U8(p++);
				idbytes = ((hd & 3) === 3) ? 4 : (hd & 3); twochar = hd&(1<<6);
				hsz = readaintle(32);
				if(hsz > 1) { t = X.U8(p++); if(!t) flagbyte = X.U8(p); p += hsz-1 }
				if(hd & (1<<4)) t = readaintle(64); var srlV = itV(t.toString(16).toUpperCase().padStart(7,'0'));
				if(mVlsw === "?") mVlsw = srlV+"[mptinfo]";
				if(hd & (1<<5)) p += X.U8(p++); //:477
				if(flagbyte & 1) { t = X.U8(p++);
					if(t&1) idbytes = 65535; else idbytes = t >> 1 }
				fes = 0; if(flagbyte & (1<<1)) fes = readaintle(32); //:496
				hasstartpos=hd&(1<<2); hassz=hd&(1<<3); hasid=idbytes; hasdesc=hd&(1<<7);
				hasmap = hasid || hasstartpos || hassz || hasdesc;
				if(flagbyte&(1<<2)) { t = readaintle(16); //:512
//_log("  @"+Hex(p)+" desc["+t+"]:"+(twochar ? X.SC(p,t,"UTF8") : X.SA(p,t)));
				   p += t*(twochar ? 2 : 1) }
				if(flagbyte&(1<<3)) p += 5; //:517
				entries = readaintle(64); //if(entries > 16000) break;
				if(hasmap) t = readaintle(64); if(p > X.Sz()) return false; //:533
				//endOfHdrData = rposEndOfHdrData = p-posstart;
				rposMapBegin = hasmap ? t : p-posstart;
				return true
			}

			if(mptBeginRead()) {
// serialization_utils.cpp:558
//I've left the pieces of map code intact in case there's fun in finding out what MPT-only tweaks there are
				if(hasmap || fes) {
					p = posstart+rposMapBegin;
					//var idArray = [], mapdata = [];
					for(i=0; i < entries && p < X.Sz(); i++) {
						var idsz = idbytes;
						if(idsz == 65535) idsz = readaintle(16);
						// md = {"idlen":idsz, "idpos":oldEnd, "id:":"", "rposst":0, "rposend":0, "sz":-1 }
						// var oldEnd = mapdata.length; if(idsz) md["id"] = X.SA(p,idsz);
						p += idsz;
						if(hasstartpos) /*md["rposst"] =*/ readaintle(64);
						if(!fes) { if(hassz) /*md["sz"] =*/ readaintle(64) } //else md["sz"] = fes
						// if(md["sz"] >= 0 && !hasstartpos)
						//     md["rposst"] = !i ? 0 : mapdata[i-1]["rposst"] + mapdata[i-1]["sz"];
						if(hasdesc) { t = readaintle(16); p += t*(twochar ? 2 : 1) }
						//mapdata.push(md)
					}
				} //posmapend = p
			}
			if(sz < p+4) sz = p+4; if(X.U32(p,_LE) != mptp) bad = bad.addIfNone("!badmptptr");
		} //if 228.mptm

// Load_it.cpp:1158
		if(mVlsw === "?" && cwtv === 0x888) mVlsw = "1.17.00.00";
		if(mVlsw != "?" && tracker === "") {
			tracker = "OpenMPT";
			//not sure how many people care about these but...
			isCompatX = (cwtv&0xF000) === 0x5000;
			if(mVlsw === "1.17.00.00" && sreserved != "OMPT") isCompatX = false;
			if(isCompatX) tracker += " (compat. export)";
			else if((mVlsw > "1.17.02.54" && mVlsw < "1.18.02.00" && mVlsw != "1.18.00.00")
			  || (mVlsw > "1.18.02.00" && mVlsw.slice(mVlsw.length-2,mVlsw.length) != "00"))
				tracker += " (test build)"
		}
		else {
			var STdv = 734016 + (cwtv === 0x1FFF ? nreserved : cwtv-0x1050);
			switch(cwtv >> 12) {
			case 0:
				if(isBeroTracker) tracker = "BeRoTracker";
				else if(cwtv === 0x214 && cmwt === 0x200 && flags === 9 && !special
				  && !X.U16(0x3E) && !ins && ptn+1 === ord && gvol === 128 && mvol === 100
				  && spd === 1 && sep === 128 && !pwd && !msglen && !msgofs && !nreserved)
					tracker = "OpenSPC conversion";
				else if(cwtv === 0x214 && cmwt === 0x200 && !X.U16(0x3E) && !nreserved) {
					mVlsw = "1.00.00.A5"; tracker = "ModPlug Tracker 1.00a5"; interpretModPlugMade = true
				} else if(cwtv === 0x214 && cmwt === 0x214 && sreserved === "CHBI")
					tracker = "ChibiTracker";
				else if(cwtv === 0x214 && cmwt === 0x214 && special <= 1 && !pwd && !nreserved
				  && (flags&0x10A6) === 4 && smp > 1 && X.c("'XXXXXXXX.YYY'",smpp[1]+4))
					tracker = "CheeseTracker";
				else if(!cwtv && tracker === "") tracker = "Unknown";
				else if(cmwt < 0x300 && tracker === "") {
					if(cmwt > 0x214) tracker = "Impulse Tracker 2.15";
					else if(isWithin(cwtv,0x215,0x217))
						tracker = 'Impulse Tracker 2.14p'+(cwtv == 0x215? '1-2': cwtv == 0x216? '3': '4-5');
					else tracker = 'Impulse Tracker '+((cwtv&0xF00)>>8)+'.'+(cwtv&0xFF).toString(16).padStart(2);
					//a good place to find the total edit time if you care
				}
				break;
			case 1:
				// function SchismEpoch(y,m,d) { var mm = (m+9)%12; var yy = y-mm/10;
				//     return yy*365 + yy/4 - yy/100 + yy/400 + (mm*306+5)/10 + d-1 }
				var mk = cwtv & 0xFFF;
				if(mk <= 0x50) tracker = "Schism Tracker 0."+mk.toString(16);
				else {
					var dt = 734016 + (mk < 0xFFF ? mk-0x050 : nreserved);
					var y = Util.div64(dt*10000+14780, 3652425);
					var ddd = dt - (365*y + Util.div64(y,4) - Util.div64(y,100) + Util.div64(y,400));
					if(ddd < 0) { y--; ddd = dt - (365*y + Util.div64(y,4) - Util.div64(y,100) + Util.div64(y,400)) }
					var mi = Util.div64(100*ddd+52, 3060);
					tracker = "Schism Tracker "+(y+Util.div64(mi+2,12)).padStart(4,'0')+"-"
						+((mi+2)%12+1).padStart(2,'0')
						+"-"+(ddd - Util.div64(mi*306+5,10) + 1).padStart(2,'0')
				} break;
			case 4: tracker = "pyIT "+((cwtv&0xF00)>>8)+"."+((cwtv&0xFF).toString(16)); break;
			case 6: tracker = "BeRoTracker"; break;
			case 7:
				if(cwtv === 0x7FFF && cmwt === 0x215) tracker = "munch.py";
				else tracker = "ITMCK "+((cwtv>>8)&0xF)+"."+((cwtv>>4)&0xF)+"."+(cwtv&0xF);
				break;
			case 0xD:
				if(cwtv == 0xDAEB) tracker = "spc2it";
				else if(cwtv == 0xD1CE) tracker = "itwriter";
				else tracker = "unknown";
			}
		}
	} //if isDeepScan
	if(mVlsw != "?") { charset = 'CP1252'; if(mVcw == "?") sV = mVlsw; else if(mVcw != mVlsw) sV = "cw:"+mVcw+"/lsw:"+mVlsw; else sV = mVcw }
	else { charset = 'CP437'; if(mVcw != "?") sV = mVcw }
	var badc = 0; for(i=0;i<bad.length;i++) if(bad[i] == "!") badc++;
	if(badc < 3) return true
}
if(!bDetected && isIT()) {
	if(type === "it") sName = "Impulse Tracker module (.IT)";
	else /*if(type === "mpt")*/ sName = "OpenMPT module (.MPTM)";
	//else if(type === "alike") sName = "Impulse Tracker-alike ["+X.SA(0,4)+"] module (.IT?)";
	bDetected = 1;
	if(sV != "?") sVersion = "v."+sV; if(bad != "") sVersion += "/malformed"+bad;
	if(X.isVerbose()) {
		sOptionT(decAnsi(4,26,charset));
		if(X.isDeepScan()) {
			sOption(tracker,"in: ");
			sOptionT(auth,"by: ")
			sOption(addEllipsis(extsmp.join(", "),0x40,0x30),'ext.smp: "','"');
			sOption(addEllipsis(chnm.join(", "),0x50,0x30),'ch.names: "','"')
		}
		if(special & 1)
			sOptionT(addEllipsis(X.SC(msgofs,Math.min(msglen,0x100),'CP437'),0xC0),'msg: "','"');
		sOption(addEllipsis(insnlst.filter(funSampleName).join(" "),0x80,0x30),'ins/msg: "','"');
		sOption(addEllipsis(smpnlst.filter(funSampleName).join(" "),0x80,0x30),'smp/msg: \"','"');
		if(X.isDeepScan())
			sOption('bpm0:'+tmp0+' spd0:'+spd0+' ch:'+ch+' ord:'+ord+(ord != cord?'('+cord+')':'')
				+' ptn:'+ptn+' ins:'+ins+' smp:'+smp+(tunings?"tunings:"+tunings:"")+" sz:"+outSz(sz))
		else
			sOption('bpm0:'+tmp0+' spd0:'+spd0+' ch:'+ch+' ord:'+ord+' ptn:'+ptn+' ins:'+ins+' smp:'+smp)
	}
}


function isJ2B() {
	if(X.c("'RIFF'........'AM  '")) align = 1;
	else if(X.c("'RIFF'........'AMFF'")) align = 0;
	else return false; if(!X.c("C5",0x58)) return false;
		sz = X.U32(4,_LE)+8; p = 0x0C; ok = ord = ptn = smp = ins = 0; mptn = -1;
		insts = []; t = "";
		while(p < sz && p < X.Sz()) {
			hkhd = decAnsi(p,4,'CP437',false); hksz = X.U32(p+4,_LE); p += 8;
			switch(hkhd) {
			case "INIT": case "MAIN": ok=1; break;
			case "ORDR": ord = hksz; loop = X.U8(p);
				for(i=p+1; i<p+hksz; i++) if(mptn < X.U8(i)) mptn = X.U8(i); mptn++; break;
			case "PATT": ptn++; break;
			case "RIFF": smp++; break;
			case "INST": ins++;
				t = X.readBytes(p+2,25,true); t = decEncoding(t,'CP437').trim();
				if(t != "") insts.push(t.trim());
				insn = X.U8(p+1); subsmp = X.U16(p+30,_LE);
				q = p+0xE1;
				while(q < p+hksz) {
					hhd = decAnsi(q,4,CP437); hsz = X.U32(q+4); q += 8;
					if(hhd == "SAMP") smp++;
					t_ = decAnsi(q,0x100,CP437);
					q += hsz;
if(debug>0)if(q < p+hksz && ["SAMP","INST"].indexOf(decAnsi(q,4,'CP437')) < 0) _log(" | ^WTF J2B @"+Hex(q-hsz)+"->"+Hex(q))
				}
				break
			}
			p += hksz+align*(hksz&1)
		}
	if(p > sz || !ok || !ord || !ptn || (!smp && !ins)) return false;
	return true
}
if(!bDetected && isJ2B()) {
	sName = "Jazz Jackrabbit 2/Galaxy Sound System module (.J2B)"; sVersion = "v."+(align?"__":"FF"); bDetected = 1;
	if(X.isVerbose()) {
		sOptionT(X.SA(0x14,64));
		sOptionT(addEllipsis(insts.join('\n'),0x100,0xA0),'msg:"','"');
		sOption("ord:"+ord+" loop:"+loop+" ptn:"+mptn+(mptn!=ptn ? "/"+ptn : "")
			+(ins?" ins:"+ins:"")+(smp?" smp:"+smp:"")+" sz:"+outSz(sz))
	}
}


function isRHST() {
	//from https://gitlab.com/uade-music-player/uade/-/raw/master/amigasrc/players/wanted_team/RobHubbard_ST/src/Rob%20Hubbard%20ST_v2.asm
	 if(!X.c("00407F40 00C081C0") || !X.c("41FAFFEE",0x38)) return false;
	p = 180; const max = Math.min(0x10000,X.Sz());
	do { t = X.U16(p,_BE); p += 2 } while(p < max && t != 0x4E75); //findplay 
	if(p > max) return false;
	playp = p;
	do { t = X.U16(p,_BE); p += 2 } while(p < max && t != 0xDFFC); //findend
	if(p > max) return false;
	p += 4; endp = p;
	do { t = X.U16(p,_BE); p += 2 } while(p < max && t != 0x4E75); //findinit
	if(p > max) return false;
	do { t = X.U16(p,_BE); p += 2 } while(p < max && t != 0x7E02); //findIt1
	if(p > max) return false;
	songp = p+X.U16(p+2,_BE)+2;
	d0 = X.U32(songp,_BE)>>2; x = d0/3;
	a1 = p = songp;
	for(d5=0; d0--;) { //find the max ptr
		p = songp+X.U32(a1,_BE); a1 += 4;
		while(p < max) { t = X.U32(p,_BE); p += 4; if(!t) break; if(t > d5) d5 = t }
	}
	sz = songp+d5;
	do t = X.U8(sz++); while(t != 0x87);
	return true
}
if(!bDetected && isRHST()) {
	sName = "Rob Hubbard ST module (.RHO)"; sVersion = "v1.1"; bDetected = 1;
	if(X.isVerbose()) {
		if(x > 1) sOption(x,"×");	
		sOption(outSz(sz),"sz:")
	}
}

function isASC1() { //basically ASC0 + loop pos
	//ref https://ay.strangled.net/Ay_Emul30.src.7z/Players.pas -> FoundASC1
	if(X.Sz() < 9) return false;
	ptntp = X.U16(2,_LE); ord = X.U8(8); //patterns table ptr & num of positions
	if(ptntp-ord != 9 && ptntp-ord != 72) return false;
	smptp = X.U16(4,_LE); if(smptp > X.Sz()) return false; //samples table ptr
	orntp = X.U16(6,_LE); if(orntp > X.Sz()) return false; //ornaments table ptr
	if(X.U16(smptp,_LE) != 0x40 || X.U16(orntp,_LE) != 0x40) return false;
	ptn = 0; for(i=0; i < ord; i++) { p = X.U8(9+i); if(ptn < p) ptn = p }
	ptn++; if(X.U16(ptntp,_LE) != ptn*6) return false;
	j = X.U16(orntp+0x40-2,_LE)+orntp;
	while((j < X.Sz()) && (j < 65535) && !(X.U8(j)&0x40))  j += 2;
	bad = '';
	if(j > 65534 || j >= X.Sz())
	  if(X.isHeuristicScan()) bad = '/malformed!short';
	  else return false;
	sz = j+2; delay = X.U8(0); loop = X.U8(1);
	return true
}
if(!bDetected && isASC1()) {
	sName = "A.'Andrew Strikes Code' Sendetskii/Power of Sound's ASC/Advanced Sound Master module (.ASC)"; sVersion = 'v1.x-2.x'+bad; bDetected = 1;
	//TODO find out how exactly to credit this one
	if(X.isVerbose()) {
		pt = X.fSig(8,0x80,"'ASM COMPILATION OF '")+0x13;
		if(pt > 8) {
			pa = X.fSig(pt+0x13,0x20,"' BY '");
			sOptionT(X.SA(pt,pa-pt)); sOptionT(X.SA(pa+4,0x14),'by: ')
		}
		sOption('ord:'+ord+' ptn:'+ptn+' delay:'+delay+' loop:'+loop+' sz:'+outSz(sz))
	}
}


function isASC0() {
	//ref https://ay.strangled.net/Ay_Emul30.src.7z/Players.pas -> FoundASC0
	if(X.Sz() < 8) return false;
	ptntp = X.U16(1,_LE); ord = X.U8(7); //patterns table ptr & num of positions
	if(ptntp-ord != 8 && ptntp-ord != 71) return false;
	smptp = X.U16(3,_LE); if(smptp > X.Sz()) return false; //samples table ptr
	orntp = X.U16(5,_LE); if(orntp > X.Sz()) return false; //ornaments table ptr
	if(X.U16(smptp,_LE) != 0x40 || X.U16(orntp,_LE) != 0x40) return false;
	ptn = 0; for(i=0; i < ord; i++) { p = X.U8(8+i); if(ptn < p) ptn = p; }
	ptn++; if(X.U16(ptntp,_LE) != ptn*6) return false;
	j = X.U16(orntp+0x40-2,_LE)+orntp;
	while((j < X.Sz()) && (j < 65535) && !(X.U8(j)&0x40))  j += 2;
	bad = '';
	if(j > 65534 || j >= X.Sz())
		if(X.isHeuristicScan()) bad = '/malformed!short';
		else return false;
	sz = j+2; delay = X.U8(0);
	return true
}
if(!bDetected && isASC0()) {
	bDetected = 1; sVersion = 'v0.x'+bad;
	sName = "Andrei 'Andrew Strikes Code' Sendetskii/Titus's Advanced Sound Master module (.AS0)";
	if(X.isVerbose()) {
		pt = X.fSig(7,0x80,"'ASM COMPILATION OF '")+0x13;
		if(pt > 7) {
			pa = X.fSig(pt+0x13,0x20,"' BY '");
			sOptionT(X.SA(pt,pa-pt)); sOptionT(X.SA(pa+4,0x12),'by: ')
		}
		sOption('ord:'+ord+' ptn:'+ptn+' delay:'+delay+' sz:'+outSz(sz))
	}
}


function isGlobalTracker() {
//ref https://ay.strangled.net/Ay_Emul30.src.7z/Players.pas -> FoundGTR
	if(X.Sz() < 296) return;
	p = 0x29; var adr = X.U16(5), mp = 0, old = X.U16(p-2)-adr, t;
	for(i=0; i < 14; i++) { t = X.U16(p)-adr; //sample ptrs check
		if(t-old < 6 || (t-old-2) % 4 || t > X.Sz()) return;  p += 2; old = t }
	old = X.U16(p)-adr; p += 2;
	for(i=0; i < 15; i++) { t = X.U16(p)-adr; //ornament ptrs check
		if(t-old < 3 || t > X.Sz() || t-old != 2+X.U8(old+1) /* orn. loop end */) return;  p += 2; old = t }
	sz = t+2+X.U8(t+1); //it's always the ornaments that come last, so we'll take the last one
	old = X.U16(p)-adr; p += 2;
	for(i=1; i < 32*3; i++) { t = X.U16(p)-adr; //pattern ptrs check
		if(t-old < 3 || t > X.Sz()) return;  p += 2; old = t }
	ord = X.U8(p++); lp = X.U8(p++); if(lp > ord) return;
	for(i=0; i < ord; i++) if(X.U8(p++) % 6) return; // all positions must be divisible by 6
	if(X.U16(0x65) != p) return; // start of PatChns; must match the pointer to PatChns[0]
	return true
}
if(!bDetected && isGlobalTracker()) {
	sName = "Doctor Max/Global Corp.'s' Global Tracker module (.GTR)"; bDetected = 1;
	sVersion = 'v'+(X.U8(4)>>4)+'.'+(X.U8(4)&0xF);
	if(X.isVerbose()) {
		dly = X.U8(0); ptn = X.U8(0x125); lp = X.U8(0x126);
		sOptionT(X.SA(7,0x20));
		sOption('ord:'+(lp>0?lp+'-':'')+ord+' ptn:'+ptn+' delay:'+dly+' sz:'+outSz(sz));
	}
}


function isBPSoundMon() {
	//ref https://github.com/tonioni/WinUAE/blob/master/prowizard/rippers/BP-Soundmonitor.c
	// & src/BS02-92.S for BPSM version
	if(X.Sz() < 0x200) return false;
	old = false; bad = "";
	if(!/V\.\d/.test(X.SA(0x1A,3)))
		if(X.c("'BPSM'",0x1A)) old = true; else return false;
	ord = X.U16(0x1E,_BE); if(525+ord*16 > X.Sz()) return false;
	allsmpsz = smp = 0;
	for(i=0; i < 15; i++) {
		if(X.U8(0x20+i*0x20) == 0xFF) continue;
		sn = X.readBytes(0x20+i*0x20,0x18); if(charStat(sn,CS_ALL).indexOf('xsc') < 0) return false;
		slen = X.U16(0x20+0x18+i*0x20,_BE)<<1; lp = X.U16(0x3A+i*0x20,_BE);
		lpl = X.U16(0x3A+i*0x20,_BE); svol = X.U16(0x3E+i*0x20,_BE);
		if(lpl > slen || lp-1 > slen || lpl-1 > slen) return false;
		if(svol > 0x60) return false; else if(svol > 0x40) bad = bad.addIfNone("!badvol")
		allsmpsz += slen; if(slen) smp++
	}
	sz = 0x200+allsmpsz+ord*0x10;
	if(!old) sz += X.U8(0x1D)*0x40; //what IS this value?
	ptn = 0;
	for(i=0; i < ord*16; i+=4) { p = X.U16(0x200+i,_BE); if(p>ptn) ptn = p }
	sz += ptn*0x30;
	return true
}
if(!bDetected && X.isDeepScan() && isBPSoundMon()) {
	bDetected = 1;
	sName = "Brian Postma's SoundMon module (.BP)";
	if(old) sVersion = "old"; else sVersion = "v"+X.SA(0x1C,1);
	if(X.Sz() < sz) bad = bad.addIfNone("!short");
	if(bad != "") sVersion = sVersion.appendS("malformed"+bad,'/');
	if(X.isVerbose()) {
		sOptionT(X.SA(0,0x1A));
		sOption("ord:"+ord+" ptn:"+ptn+" smp:"+smp+" sz:"+outSz(sz));
	}
}


function isFuchs() {
	//ref https://github.com/tonioni/WinUAE/blob/master/prowizard/rippers/FuchsTracker.c
	if(X.Sz() < 0xC0 || !X.c("'SONG'",0xC0) || !X.c("'INST'",0xC8+X.U32(0xC4,_BE))) return false;
	allsmpsz = X.U32(0x0A,_BE);
	if(allsmpsz <= 2 || allsmpsz >= 65535*16) return false;
	smpdescs = 0; smp = 0;
	for(i=0; i < 16; i++) {
		ssz = X.U16(0x0E+i*2,_BE); if(ssz) smp++;
		lpst = X.U16(0x4E+i*2,_BE); if(ssz < lpst) return false;
		if(X.U8(0x2E+i*2) > 0x40) return false; //vol
		smpdescs += ssz
	}
	if(smpdescs <= 2 || smpdescs > allsmpsz) return false;
	ord = X.U8(0x6F); if(!ord || ord > 0x28) return false;
	ptn = 0; //last pattern
	for(i=0; i < 0x28; i++) {
		pt = X.U8(0x71+i*2); if(pt > 40) return false; if(pt > ptn) ptn = pt }
	ptn++; k = ptn*1024; if(k+204 > X.Sz()) return false;
	sz = smpdescs+k+204;
	return true
}
if(!bDetected && isFuchs()) {
	sName = "Andreas Fuchs's FuchsTracker module (.FUCHS)";
	bDetected = 1;
	if(X.isVerbose()) sOption('ord:'+ord+' ptn:'+ptn+' smp:'+smp+' sz:'+outSz(sz))
}


function isGMC() {
	//ref https://github.com/tonioni/WinUAE/blob/master/prowizard/rippers/GMC.c
	// & https://github.com/OpenMPT/openmpt/blob/master/soundlib/Load_gmc.cpp
	if(X.Sz() < 444 || !X.c("000000",0xF0)) return;
	//smp descriptions
	allsmpsz = 0; j = 0; smps = [];
	for(k=p=0; k < 15; k++,p+=0x10) {
		o = X.U32(p,_BE); if(o > 0x7FFFFF || (o&1)) return; //ofs
		l = X.U16(p+4,_BE)<<1; //length
		if(l > 0xFFFF) return;
		if(X.U8(p+6)) return; //zero byte
		if(X.U8(p+7) > 0x40) return; //vol
		a = X.U32(p+8,_BE); if(a > 0x7FFFFF || (a&1)) return; //address
		n = X.U16(p+0xC,_BE); //looplen
		if(n > 2 && n > l) return;
		if(X.U16(p+0xE,_BE) & 1) return; //datastart
		allsmpsz += l;
		if(l) smps.push([o,l]);
	}
	if(allsmpsz <= 4) return;
	smps.sort(function(a,b){return a[0]-b[0]});
	for(k=0; k < smps.length-1; k++)  if(smps[k][0]+smps[k][1] > smps[k+1][0]) return;
	ord = X.U8(0xF3); //ptn table size
	if(!isWithin(ord, 1, 0x64)) return;
	ptn = 0; ords = [];
	for (i=0; i < 100; i++) { //ptn order table
		p = X.U16(0xF4+i*2,_BE);
		if (p & 0x3FF) return;
		if(i < ord) { p >>= 10; if(p > ptn && p != 63) ptn = p }
	} ptn++;
	if(ptn > 64) return;
	m = notes = badnotes = badled = 0; end = false; p = 0x1BC; nps = [];
	if(p+ptn*0x400 > Math.min(X.Sz(),0xFFFC)) return;
	for (i=0; i < ptn && !end; i++) { //test pattern data
		badnotes = badled = 0;
		for (j = 0; j < 256 && !end; j++,p+=4) {
			d = X.readBytes(p,4);
			np = (d[0]<<8)+d[1];
			if(np && np != 0xFFFE)
				if(isWithin(np, 113,856)) {
					notes++; if(!nps.includes(np)) nps.push(np);
					if(notes > 100 && !X.isDeepScan()) end = true;
				}
				else badnotes++;
			d[2] &= 0xF;
			if(d[2] == 3 && d[3] > 0x40) badnotes++; //vol effect
			if(d[2] == 4 && d[3] > 0x63) return;
			if(d[2] == 5 && d[3] > ord+1) return;
		}
	if(badnotes > 16) { if(debug>0)_log("GMCFault: over 16 bad notes"); return}
	}
	nps = nps.length;
	if(!notes) return;
	if(nps <= 2 && !X.isHeuristicScan()) return; // that's not music...
	sz = 0x1BC + ptn*0x400 + allsmpsz;
	return true
}
if(!bDetected && isGMC()) {
	bDetected = 1;
	sName = "Andreas Tadic's Game Music Creator module (.GMC)";
	if(X.isVerbose()) {
		sOption("ord:"+ord+" ptn:"+ptn+" smp:"+smps.length
			+((X.isDeepScan() || notes < 100) ? " notes:"+notes+"("+nps+" diff.)" : '')
			+" sz:"+outSz(sz)); if(X.Sz() < sz) sVersion = "malformed!short"
	}
}


function isMoonBlaster() {
	//ref https://www.msx.org/wiki/Moonblaster_file_format
	//TODO check for the sanity of more values passed to MSX-AUDIO, like instr/vol
	if(X.Sz() < 0x179) return false;
	ord = X.U8(0); ofs = unpsz = 0;
	if(ord > 220 && ord < 255) return false; //not sure if the manual is right about 220 being the maximum
	if(ord == 255) { ofs = 1; ord = 200 } //a subformat. Always 200 positions, 01-filled; all offsets are +1
	else ord++;
	for(i=0; i < 0x20; i++) if(X.U8(0xA3+ofs+i) > 0x16) { /*_log('MBM: instr/vol error');*/ return false }
	tmp0 = X.U8(ofs+0xCD); if(!tmp0) { /*_log('MBM: tempo = 0 error');*/ return false }
	p = ofs+0x178+ord; ptn = -1; for(i=ofs+0x178; i < p; i++) { t = X.U8(i); if(t > ptn) ptn = t }
	// Start pattern number is 1, no need to ptn++. Funnily, the manual says patterns start from 0
	if(ptn < 1) return false;
	ptnp = p+ofs; ptn1 = oldp = X.U16(ptnp);
	if(ptn1 > X.Sz()) { /*_log('MBM: ptn1 @'+Hex(ptn1)+">"+Hex(X.Sz()));*/ return false }
	for(i=0; p < X.Sz() && i < ptn; i++) {
		p = X.U16(ptnp+i*2);
		if(!i) unpsz = p; // patterns should start from the current unpsz
		if(p < ptnp+i*2 || p > X.Sz()) {
			/*_log('MBM: ptn ptr['+Hex(i)+'] error @'+Hex(ptnp+i*2)+': '+Hex(p));*/ return false }
		if(i && p-oldp > 0x100) { /*_log('MBM: ptn too large @'+Hex(ptnp+i*2));*/ return false }
		oldp = p;
	}
	if(!ofs) { p = ptnp+i*2;
		if((unpsz != p || p > X.Sz())) { /*_log('MBM: pattern offset error: p='+Hex(p));*/ return false }
	} else p = ptn1
	//decruncher: going over all the patterns at once
	while(p < X.Sz()) { c = X.U8(p++);
		if(!c) break; else if(c <= 0xF2) unpsz++; else /*decrunching 0's*/ unpsz += c-0xF2;
	}
	if(!isWithin(p, 0x178,0x3DC0)) return false;
	sz = p;
	return true
}
if(!bDetected && isMoonBlaster()) {
	//MUS is "FST-song", MBM is song, MBK is drumkit, MBV is voice-e, MBB is voice-b, MBS is samples
	sName = "Remco Schrijvers et al.'s MSX MoonBlaster for MoonSound module (.MBM)";
	bDetected = 1; if(ofs) sVersion = '200-pos.ver.';
	drumkit = X.SA(ofs+0x140,8).trim();
	if(drumkit != 'NONE') sVersion = sVersion.appendS('+'+drumkit+'.MBK','/');
	if(X.isVerbose()) {
		sOption(X.SA(ofs+0xCF,40).trim());
		sOption('tempo0:'+tmp0+' ord:'+ord+' ptn:'+ptn+' unpsz:'+unpsz+' sz:'+outSz(sz))
	}
}


function isProTracker1_sp() {
	//PT1's project file
	//TODO()
}


function isProTracker1_c() {
	//ref https://bitbucket.org/zxtune/zxtune/src/develop/src/formats/chiptune/aym/protracker1.cpp
	// & https://ay.strangled.net/Ay_Emul30.src.7z/Formats.pas -> FoundPT1
	if(X.Sz() < 0x66) return;
	tempo = X.U8(0); if(!isWithin(tempo, 2,15)) return; //delay
	ptntp = X.U16(0x43); if(ptntp >= X.Sz()) return; //patterns table pointer
	numofpos = X.U8(1);// if(len == 0) return; //numofpositions, can be corrupted?
	loop = X.U8(2);
	j = 0; j1 = 65535;
	for(i=0; i < 0x10; i++) { //samples pointers
		sp = X.U16(3+i*2); if(sp > X.Sz()) return;
		if(j < sp) j = sp;
		op = X.U16(0x23+i*2); if(op > X.Sz()) return;
		if(op && j1 > op) j1 = op
	}
	if((j1 < 0x67) || (j < 0x67) || (j > 65534) || (j > X.Sz())
	  || (j + X.U8(j) * 3 + 2 != j1)) return;
	for(i=j=0; i < 0x10; i++) { //ornaments pointers
		op = X.U16(0x23+i*2,_LE); if(op > X.Sz()) return;
		if(j < op) j = op
	}
	if(j < 0x67) return;
	len = j+0x40; if(len > 0x10000 || len > X.Sz()+1) return;
	j = 0x63;
	while(j <= ptntp && X.U8(j) < 0xFF) j++;
	if(j+1 != ptntp) return;
	ord = j-0x63; if(loop > ord) return;
	return true
}
if(!bDetected && isProTracker1_c()) {
	sName = "Golden Disk ProTracker module (.PT1)"; sVersion = "v1.x/compiled"; bDetected = 1;
	if(X.isVerbose()) {
		sOptionT(X.SA(0x45,0x1E)); //musicname
		sOption("tempo:"+tempo+" ord:"+ord+" len:"+len)
	}
}


function isProTracker2() {
	//ref https://bitbucket.org/zxtune/zxtune/src/develop/src/formats/chiptune/aym/protracker2.cpp
	// & https://ay.strangled.net/Ay_Emul30.src.7z /Players.pas & /Help/html/ay_ru_fmt_pt2.htm
	if(X.Sz() < 132) return; //the smallest pt2 in all actual collections is 521 bytes long
	tmp = X.U8(0); if(!isWithin(tmp,2,15)) return;
	const maxsz = X.Sz()+X.isHeuristicScan()*0x200;
	ord = X.U8(1); lp = X.U8(2); if(!ord || lp > ord || 0x83+ord > maxsz-2) return;
	smp0 = X.U16(3); orn0 = X.U16(0x43); if(orn0-smp0 > maxsz+2 || orn0 < smp0) return;
	ptnp = X.U16(0x63); if(!isWithin(ptnp,0x65,orn0)) return;  smp = orn = 0;
	for(i=mps=0,p=3; i < 0x20; i++,p+=2) { t = X.U16(p); if(t > mps) mps = t;
		if(t) { smp++; if(!isWithin(t,0x65,maxsz-2)) return }
	}
	for(mp=0; i < 0x30; i++,p+=2) { t = X.U16(p); if(t > mp) mp = t;
		if(t) { orn++; if(orn <= 2 && !isWithin(t,Math.max(mps,orn0),maxsz)) return }
		//just check the first two orns. Ornament records seem to be optional, and,
		//despite the pointers being present, some modules are just cut off early 
	}
	for(p=0x83,ptn=i=0; i <= 0xFF; i++,p++) {
		if(p > X.Sz()-2) return; if((o=X.U8(p)) == 0xFF) break; if(o > ptn) ptn = o
	} ptn++; p++;
	if(ord != i || ptnp != p) return;
	for(rptn=0; p < X.Sz(); rptn++,p+=2) { if(!(t=X.U16(p))) break; else if(!isWithin(t,ptnp+ptn*6,X.Sz())) return }
	if(rptn != ptn*3) return;  sz = mp+2+X.U8(mp);
	return true
}
if(!bDetected && isProTracker2()) {
	sName = "Golden Disk ProTracker module (.PT2)"; sVersion = 'v2.x'; bDetected = 1;
	if(X.isVerbose()) {
		nc = charStat(t=X.readBytes(0x65,0x1E), 1);
		if(nc.indexOf('allxsc') >= 0) sOptionT(decEncoding(t,CPSpeccy)); else sOption('<broken title>');
		sOption('tmp:'+tmp+' ord:'+(lp?lp+'-':'')+ord+' ptn:'+ptn+' smp:'+smp+' orn:'+orn+' sz:'+outSz(sz))
	}
}


function isProTracker3(ofs) { //PT3's detection but sanity checks because SOME people -_-
	//ref https://bitbucket.org/zxtune/zxtune/src/develop/src/formats/chiptune/aym/protracker3_compiled.cpp
	// & https://ay.strangled.net/Ay_Emul30.src.7z /Help/html/ay_ru_fmt_pt3.htm
	// & https://bitbucket.org/zxtune/zxtune/src/develop/src/formats/chiptune/aym/turbosound.cpp
	if(X.Sz()-ofs < 0xCA) return; p = 0; var maxsz = Math.min(65535,X.Sz()+X.isHeuristicScan()*0x200);
	var smps=[], orns=[], trks=[], ptns=[]; bad = '';
	function re(msg) { if(debug>1)_l2r('pt3',p,msg); return }
	ttn = X.U8(ofs+0x63); if(ttn > 4) if(X.isHeuristicScan()) bad = bad.addIfNone('!badtuning'); else return;
	tmp0 = X.U8(ofs+0x64); if(!tmp0) return;
	ord = X.U8(ofs+0x65); ptnp = ofs+0xCA+ord;
	if(ptnp != X.U16(ofs+0x67)+ofs || ptnp > X.Sz()-2) return re('!badptnp+'+Hex(ptnp));
	lp = X.U8(ofs+0x66); if(lp > ord-1) return re('!badlp');
	orn = smp = mp = 0; if(!ofs) ptnp0 = ptnp;
	for(p=ofs+0x69; p < ofs+0xA9; p+=2) if(X.U16(p)) smp++;  for(; p < ofs+0xC9; p+=2) if(X.U16(p)) orn++;
	for(ptn=i=0,p=ofs+0xC9; i <= 0xFF; i++) { if((o=X.U8(p++)) == 0xFF) break; if(o%3) return;
		if(ptns.indexOf(o/3) < 0) ptns.push(o/3); if(o/3 > ptn) ptn = o/3 }
	ptn++; if(!i || i != ord) return re('!realord:'+i+' != ord:'+ord);
	t = X.readBytes(ofs,10); tracker = '';
	if(charStat(t,1).indexOf('allasc') >= 0) { t = decEncoding(t,CPSpeccy);
		if(t == 'ProTracker') { tracker = 'Pro Tracker'; sv = 'v'+X.SA(ofs+0xB,4).trim(); nv = X.U8(ofs+0xD)-0x30 }
		else if(t == 'Vortex Tra') { tracker = 'Vortex Tracker ]['; sv = 'v'+X.SA(ofs+0x12,4).trim(); nv = 7 }
		else { tracker = '"'+X.SA(ofs,14)+'"'; sv = 'v3.x'; nv = 7 }
	} else { tracker = 'hacked Pro Tracker'; sv = 'v3.x'; nv = 7 }
	//track parser
	function isTrkIn(tracks,track) {
		for(var i=0; i < tracks.length; i++) if(tracks[i][0] == track[0]) return true;  return }
	for(i=notes=0; i < ptn*3 && p < maxsz; i++,p+=2) {
		// echo track addresses, pertinent to debugging:
		// 6F3, 719, 745...
		if(!isWithin(t=X.U16(p)+ofs, p,maxsz)) return re('!badptnp2:'+Hex(t));
		if(ptns.indexOf(Util.divu64(i,3)) < 0) continue;
		trk = [t,0]; if(isTrkIn(trks,trk)) continue;
		var lines = 0, eot = false;
		trkparse: while(!eot && lines < 256 && t < maxsz) {
			eol = false; z = 0; // counter of all effect parameters
			while(!eol && t < X.Sz()) {
				if((x=X.U8(t++)) == 0) eol = eot = true;
				else if(x == 1 || x == 8) z += 3;  else if(x == 2) z += 5;  else if([3,4,6,9].indexOf(x) >= 0) z++;
				else if(x == 5) z += 2;  else if(x == 7) bad = bad.addIfNone('!badcmd07h');
				else if(x == 0x10) { x = X.U8(t++); y = X.U16(0x69+x)+ofs; if(smps.indexOf(x) < 0) {
					if(y > ofs && !isWithin(y, ptnp,maxsz)) bad = bad.addIfNone('!badsmp'); smps.push(x) } }
				else if(x < 0x20) { t += 2; x = X.U8(t++); y = X.U16(0x69+x)+ofs; if(smps.indexOf(x) < 0) {
					if(y > ofs && !isWithin(y, ptnp,maxsz)) bad = bad.addIfNone('!badsmp'); smps.push(x) } }
				else if(x < 0x40) { /* could check if this isn't channel B but may be too much */ }
				else if(x < 0x50) { x &= 0xF; if(orns.indexOf(x) < 0) { orns.push(x); if(mp < (y=X.U16(ofs+0xA9+x*2)+ofs)) mp = y } }
				else if(x < 0xB0) { notes++; eol = true } else if(x == 0xB0) { /*envoff*/ }
				else if(x == 0xB1) { lines += X.U8(t++) } else if(x < 0xC0) t += 2; else if(x == 0xC0) eol = true;
				else if(x < 0xD0) { /*setvol*/ } else if(x == 0xD0) eol = true;
				else if(x < 0xF0) { x = (x&0x1F)<<1; y = X.U16(ofs+0x69+x)+ofs; if(smps.indexOf(x) < 0) {
					if(y > ofs && !isWithin(y, ptnp,maxsz)) bad = bad.addIfNone('!badsmp1'); smps.push(x) } }
				else { x &= 0xF; if(orns.indexOf(x) < 0) { orns.push(x); if(mp < (y=X.U16(ofs+0xA9+x*2))) mp = y }
					x = X.U8(t++); y = X.U16(ofs+0x69+x)+ofs; if(smps.indexOf(x) < 0) {
						if(y > ofs && !isWithin(y, ptnp,maxsz)) bad = bad.addIfNone('!badsmp2'); smps.push(x) } }
			}
			lines++; t += z; z = 0; if(lines > 256) if (!X.isHeuristicScan()) return re('!badlines'); else bad = bad.addIfNone('!badlns')
		}//trkparse
		trk[1] = t-trk[0]; if(!isTrkIn(trks,trk)) trks.push(trk); if(mp < t) mp = t
	}//all tracks parsed
//	function tupleSort(lst) { function sf(a, b) { if(a[0] != b[0]) return a[0]-b[0]; else return a[1]-b[1] } return lst.sort(sf) }
//_l2r('pt3',0,outArray(tupleSort(trks),16)+'::: '+outArray(findGaps(trks,0),16)+' :: '+outArray(findIntersections(trks,true),16))
	if(t=findIntersections(trks,true).length) if(!X.isHeuristicScan() || t > 5) return re('trackxsections');
		else bad = bad.addIfNone('!trkxsec'); //so there are some broken files, like echo.pt3 (5753 bytes)
	if(t=findGaps(trks,0).length) if(!X.isHeuristicScan() || t > 5) return re('trackgaps');
		else bad = bad.addIfNone('!trkgap'); //tracks are monolithic BUT SOME PEOPLE
	rsmp = smps.length; rorn = orns.length; rptn = ptns.length; if(!rptn || rsmp > 32) return re('!rptn='+rptn+',rsmp='+rsmp);
	if(X.fStr("compilation of",10,20) < 0 && !rsmp) return re('!nosmp');
	sz = mp+2+X.U8(mp+1); //ornaments! either at the last orn ptr, or just straight after the patterns
	return true
}
if(!bDetected && isProTracker3(0)) {
	sName = "Golden Disk ProTracker module (.PT3)"; sVersion = sv; bDetected = 1;
	tsmode = nv >= 5 && (X.U8(0x62) != 0x20 || X.c("'Vortex Tra'",sz) || X.c("'ProTracker 3'",sz));
	if(X.isVerbose()) info1 = 'tmp0:'+tmp0+' ord:'+(lp?lp+'-':'')+ord
			+' ptn:'+(rptn!=ptn?rptn+'/':'')+ptn+' smp:'+(rsmp!=smp?smp+'/':'')+rsmp
			+' orn:'+(rorn!=orn?rorn+'/':'')+orn+' notes:'+notes;
	sz1 = sz; ord1 = ord; tmp1 = tmp0;
	if(tsmode) if(!isProTracker3(sz) || ord1 != ord || tmp1 != tmp0) tsmode = false;
	if(tsmode) { //TurboSound container? check the footer, should be nearby (or isn't a footer)
		if((t=X.fSig(sz,0x200,"'02TS'")) > sz) if(X.SA(t-6,4) === X.SA(t-12,4)) if(sz1 == X.U16(t-8,_LE)) {
			sVersion += '[TurboSound:'+X.SA(t-6,4)+']'; sz = t+4;
		}
		else if(nv >= 7) sVersion += '/TurboSound'; else bad = bad.addIfNone('badTSver')
	}
	if(bad.length) sVersion = sVersion.appendS('malformed'+bad,'/');
	if(X.isVerbose()) {
		if(X.c("'by ",0x3F)) { sOptionT(decAnsi(0x1E,0x20,CPSpeccy)); sOptionT(decAnsi(0x42,0x20,CPSpeccy),'by: ') }
		sOptionT(tracker,'in: ');
		switch(ttn) {
			case 0: sOption('tuning 0: 1625000Hz PT3.3'); break;
			case 1: sOption('tuning 1: Sound Tracker'); break;
			case 2: sOption('tuning 2: ASM/PSC 1.75MHz'); break;
			case 3: sOption('tuning 3: RS 1625000Hz'); break;
			case 4: sOption("tuning 4: Ivan Roshchin's Natural Cmaj/Am"); break;
			default: sOption('tuning '+ttn+'/custom')
		}
		sOption((tsmode?'chip#1: ':'')+info1);
		if(tsmode) {
			sOption('chip#2: ptn:'+(rptn!=ptn?rptn+'/':'')+ptn+' smp:'+(rsmp!=smp?smp+'/':'')+rsmp
			+' orn:'+(rorn!=orn?rorn+'/':'')+orn+' notes:'+notes);
		}
		sOption(outSz(sz),'sz:')
	}
}


function isProSoundCreator_c(v14/*must take 0 or 1*/) {
	//from https://ay.strangled.net/Ay_Emul30.src.7z /Players.pas → FoundPSC
	switch(v14) { case 1: sv = 'v1.04+'; break; case 0: sv = 'v1.00~3'; break; default: return }
	const max = Math.min(X.Sz(),0x10000); if(max < 0x4E) return;
	var sap = X.U16(0x45), ordp = X.U16(0x47), ornp = X.U16(0x4A); //sample pointer table begins from 4Ch
	if(!isWithin(sap, 0x4E,max) || !isWithin(ordp, 0x4E,max-11)
		|| !isWithin(ornp, 0x4E,Math.min(max,0x8C)) || ornp % 2) return;
	var o0 = X.U16(ornp)+v14*ornp, s0 = X.U16(0x4C)+v14*0x4C, t = X.U16(ornp-2)+v14*0x4C;
	if(s0 > Math.min(ornp+0x40,X.Sz()-5) || o0 >= max || t >= max || o0-t < 8 || (o0-t) % 6 != 2) return;
	for(p=s0+4,smp=0; p < max && (X.U8(p) & 0x20); p += 6) smp++;
	if(p > 0xFFFE || p > X.Sz()) return;
	if(ornp-0x4C-2 > 0) { if(p+3 != X.U16(0x4E)+v14*0x4C) return } else if(p+4 != o0) return;
	p = ordp+1; if(X.U8(p) == 0xFF) return;
	for(ord=ptn=0; p+3 < max;) {
		if(!isInside(X.U16(p+1), o0,ordp) || !isInside(X.U16(p+3), o0,ordp) || !isInside(X.U16(p+5), o0,ordp)) return;
		p += 8; ord++;
		if(X.U8(p) == 0xFF) { if(X.U8(p-1) >= ord) return;  lp = X.U8(p-1); break }
		else ptn = Math.max(ptn,X.U8(p-1)+1)
	}
	sz = p+3; dly = X.U8(0x49);
	ftitle = charStat(X.readBytes(0,0x18),true).indexOf('allasc') >= 0;
	fby = charStat(X.readBytes(0x2E,0x17),true).indexOf('allasc') >= 0;
	return true
}
if(!bDetected && (isProSoundCreator_c(1) || isProSoundCreator_c(0))) {
		sName = "Viktor 'KVA' Kuźmin's Pro Sound Creator module (.PSC)"; //Виктор Кузьмин
		sVersion = sv+'/compiled'; bDetected = 1;
		if(X.isVerbose()) {
			if(ftitle) sOptionT(decAnsi(0x19,0x14,CPSpeccy,false,Chars0to1FSpeccy));
			if(fby) sOptionT(decAnsi(0x31,0x14,CPSpeccy,false,Chars0to1FSpeccy),"by: ");
			sOption('ord:'+(lp>0?'('+lp+'-)':'')+ord+' ptn:'+ptn+' smp:'+smp+' sz:'+outSz(sz))
		}
	}


function isFTC() {
	//from https://ay.strangled.net/Ay_Emul30.src.7z /Players.pas → FoundFTC
	//if(X.c("'Module: '") && X.c("';Fast Tracker v1.00'", 0x32)) // we KNOW someone broke this in their music
	if(X.Sz() < 0xD7) return; const max = Math.min(0x10000,X.Sz());
	switch(X.U8(0x32)) { case 0x3B: notet = 0; break; case 1: notet = 1; case 2: notet = 2; default: return }
	if((ptnp=X.U16(0x4B)) >= X.Sz()) return false;	if(X.U16(0x92) <= X.U16(0x52)) return false;
	for(p=0xD4,ptn=0; p < 0x1D4 && p < X.Sz() && X.U8(p) < 0x80; p += 2, ord++) ptn = Math.max(ptn,X.U8(p)+1);
	lp = X.U8(0x46); if(p >= 0x1D4 || lp > ord) return;
	var ofs = X.U16(ptnp) - (ptn*6 + ptnp+2); if(ofs < 0) return;
	if(X.U16(0x52)-ofs >= X.Sz() || ptnp >= X.U16(0x52)-ofs || X.U16(0x92)-ofs >= X.Sz()) return;
	var ornpm = 0xFFFF, ornpM = 0, msmpp = 0;
	for(p=0x52; p < 0x92; p+=2) msmpp = Math.max(msmpp,X.U16(p));
	for(; p < 0xD4; p+=2) { ornpm = Math.min(ornpm,X.U16(p)); ornpM = Math.max(ornpM,X.U16(p)) }
	if(msmpp-ofs >= Math.min(65534,X.Sz()) || ornpm-ofs >= max || ornpM-ofs >= max
	  || msmpp <= ptnp || msmpp+3+(X.U8(msmpp-ofs+2)+1)*5 != ornpm) return;
	sz = msmpp+3+(X.U8(msmpp-ofs+2)+1)*2-ofs;
	if(sz < ptnp) return false;
	return true
}
if(!bDetected && isFTC()){
	sName = "S.'Orion' Matveev & A.'Sand' Plużnikov's Fast Tracker module (.FTС)";
	//Сергей Матвеев & Александр Плужников
	bDetected = 1;
	if(X.isVerbose()) {
		sOptionT(X.SA(8,0x2A));
		sOption('ord:'+(lp>0?'('+lp+'-)':'')+ord+' ptn:'+ptn+(ofs?' ofs:'+Hex(ofs):'')+' sz:'+outSz(sz))
	}
}

function isSoundTrackerProF() {
	//ref https://bitbucket.org/zxtune/zxtune/src/develop/src/formats/chiptune/aym/soundtracker.cpp
	// & https://ay.strangled.net/STPDocs.7z -> Xplay79f.txt
	if(X.Sz() < 3009+576) return false; //header + patterns
	for(p=smp=0; smp < 15; smp++) {
		for(i=0; i < 32; i++) if(X.U8(p++) > 0x0F) return false; //levels
		for(i=0; i < 32; i++) if(X.U8(p++) & 0x20) return false; //noises
		for(i=0; i < 32; i++) { p++; if(X.U8(p++) > 0x1F) return false } //additions
		for(i=0; i < 2; i++) if(X.U8(p++) > 0x1F) return false; //loops
	}
	for(i=0; i < 0x100; i++) { if(!isWithin(X.U8(p),1,0x20)) return false; p += 2 } //positions
	// at this point, p should be 15*0x82
	if(X.U8(p++) & 0x80) return false;
	p += 32*17; //ornaments
	dly=X.U8(p++); if(!isWithin(dly,1,0xF)) return false;
	pts=X.U8(p); if(!isWithin(pts,1,0x40)) return false;
	return true;
}
if(!bDetected && isSoundTrackerProF()) {
	sName = "Stanislav 'KSA' Kuzin's Sound Tracker Pro module (.STF,.F)";
	sVersion  = "uncompiled/packed"; bDetected = 1;
	if(X.isVerbose()) {
		sOption('dly:'+dly+' pts:'+pts)
	}
}


function isSoundTrackerPro() {
	//from https://ay.strangled.net/STPDocs.7z -> stp.txt
	tmp = X.U8(0); if(!isWithin(tmp,1,50)) return;
	const max = X.Sz()-2;
	var ordtp = X.U16(1,_LE);  ord = X.U8(ordtp); lp = X.U8(ordtp+1); //ord, loop, orderlist and ptn transpose
	if(!isWithin(ordtp,10,max) || lp > ord || !ord) return;
	var ptntp = X.U16(3,_LE); if(!isWithin(ptntp,ordtp+2+ord*2,max)) return; //pattern offsets
	var orntp = X.U16(5,_LE); if(!isWithin(orntp,10,max)) return; //ornament offsets
	var smptp = X.U16(7,_LE); if(!isWithin(smptp,10,max)) return; //sample offsets
	sz = smptp+30; if(sz > X.Sz()) return;  var ptnp = [], smpp = [];
	for(i=0; i < ord; i++)
		if(X.U8(ordtp+2+i*2)%6 || !isWithin(t=X.U16(ptntp+i*2,_LE),10,max)) return false; else if(ptnp.indexOf(t) < 0) ptnp.push(t)
	for(i=0; i < 15; i++)
		if(!isWithin(t=X.U16(smptp+i*2),10,max)) return; else if(smpp.indexOf(t) < 0) smpp.push(t);
	ptn = ptnp.length; smp = smpp.length; if(!ptn) return;
	return true;
}
if(!bDetected && isSoundTrackerPro()) {
	sName = "Stanislav 'KSA' Kuzin's Sound Tracker Pro module (.STP)"; sVersion  = "compiled"; bDetected = 1;
	if(X.isVerbose()) {
		if(X.c("'KSA SOFTWARE COMPILATION OF '",10)) sOptionT(X.SC(0x26,0x19,'CP1251'));
		sOption('tmp:'+tmp+' ord:'+(lp?lp+'-':'')+ord+' ptn:'+ptn+' smp:'+smp+' sz:'+outSz(sz))
	}
}


function isSQTracker() {
	//ref https://ay.strangled.net/Ay_Emul30.src.7z/Players.pas -> FoundSQT
	if(X.Sz() < 17) return false;
	sz = X.U16(0); if(X.Sz() < sz) return false;
	smptp = X.U16(2); if(smptp < 10) return false; //samples table ptr
	orntp = X.U16(4); ptntp = X.U16(6); //ornaments & patterns table ptrs
	postp = X.U16(8); lpp = X.U16(0xA); //positions table ptr & loop ptr
	if(orntp <= smptp+1) return false; if(ptntp < orntp) return false;
	if(postp <= ptntp) return false; if(lpp < postp) return false;
	membase = smptp-10; //to eventually realign to file, not memory
	if(lpp-membase >= sz) return false;
	i = postp-membase; b = X.U8(i); if(!b) return false; //can't have 0 patterns
	ord = 0; ptn = 0;
	while(b) { //find the highest pattern #
		if(i+7 >= sz) return false;
		ord++;
		if(ptn < (t = b&0x7F)) ptn = t; i += 2; b = X.U8(i);
		if(ptn < (t = b&0x7F)) ptn = t; i += 2; b = X.U8(i);
		if(ptn < (t = b&0x7F)) ptn = t; i += 3; b = X.U8(i)
	} ptn++;
	p = X.U16(smptp-membase+2);
	if(p-ptntp != ptn*2) return false;
	p = 12; len = i+7;
	if(len != sz) return false;
	t = X.U16(12);
	for(i = 1; i <= (orntp-smptp)>>1; i++) {
		p += 2; j3 = X.U16(p);
		if(j3-t != 0x62 && (sz != 1337 || ord != 10 || ptn != 11)) return false;
		t = j3;
	}
	for(i = 1; i <= (ptntp-orntp)>>1; i++) {
		p += 2; j3 = X.U16(p);
		if(j3-t != 0x22) return false;
		t = j3;
	}

	return true;
}
if(!bDetected && isSQTracker()) {
	sName = "Jiří 'George' Koudelka's Scalex Qjeta Tracker module (.SQT)"; sVersion  = "compiled"; bDetected = 1;
	sOption('ord:'+ord+' ptn:'+ptn+' sz:'+outSz(sz))
}


function isSTC() {
	//ref https://ay.strangled.net/Ay_Emul30.src.7z/Players.pas -> FoundST
	if(X.Sz() < 6) return false;
	tempo = X.U8(0); if(tempo == 0 || tempo > 0x20) return false; //delay
	postp = X.U16(1,_LE); if(postp<0x7E || postp>X.Sz()) return false; //positions ptr
	orntp = X.U16(3,_LE); if(orntp<0x7E || orntp>X.Sz()) return false; //ornaments ptr
	ptntp = X.U16(5,_LE); if(ptntp<0x7E || ptntp>X.Sz()) return false; //patterns ptr
	ord = X.U8(postp)+1;
	j2 = ptntp - orntp; if(j2 == 0) return false; //j2<0: ST, >0: S_SONIC
	fID = false;
	if(j2 > 0) { if((j2%0x21) > 0) return false; }
	else if((j2%0x21) > 0) {
		if((j1<55) || (((j1-55)%0x21) > 0)) return false;
		fID = true; //we'll check for a preset ID at the end
	}
	j = X.U8(postp)*2+3;
	if(j2 < 0) { if(j+j2 != 0) return false; }
	else if(j+postp-orntp != 0) {
		if((ptntp < 82) || (j+postp-ptntp+55 != 0)) return false;
		fID = true;
	}
	j = orntp+0x21;
	if(j > 65535 || j > X.Sz()) return false;
	while(j) { j--; if(X.U8(j) > 0) return false; if(j == orntp) break }
	j = ptntp; j1 = 0; j2 = 0; ptn = 0;
	while((j+6 <= X.Sz()) && (j+6 < 65536) && (X.U8(j) < 0xFF)) {
		//find the highest channel data pointer
		j++; //ptn #
		j2 = X.U16(j,_LE); if(j1 < j2) j1 = j2; j += 2; //channel A data ofs
		j2 = X.U16(j,_LE); if(j1 < j2) j1 = j2; j += 2; //ditto B
		j2 = X.U16(j,_LE); if(j1 < j2) j1 = j2; j += 2; //ditto C
		ptn++
	}
	if((X.U8(j) < 0xFF) ) return false;
	if(j1 > X.Sz()) return false;
	if(X.U8(j1-1) < 0xFF) return false;
	bad = 0; //some modules are a bit more broken! Like "- unknown/ay5.stc" which lost some of its final bytes
	while(1) { //scan through the channel data at the end of file
		if(0x83 <= X.U8(j1) <= 0x8E) j1++; //I don't get what this does, no docs either
		j1++;
		if(j1 > 65535) return false;
		if(j1 > X.Sz())
			if(X.isHeuristicScan()) { bad = 1; break }
			else return false;
		if((X.U8(j1) == 0xFF) || (j1 == X.Sz())) break
	}
	if(X.U8(j1) == 0xFF) len = j1+1; else len = X.Sz();
	// len = X.U16(0x19,_LE); // because ~somebody~ used it for names
	if(fID)
	  if( (X.SA(ptntp-55,29) != 'SOUND TRACKER COMPILATION OF ') &&
		(X.SA(ptntp-55,28) != 'KSA SOFTWARE COMPILATION OF ') ) return false;

	return true;
}
if(!bDetected && X.isDeepScan() && isSTC()) {
	sName = "Jarosław 'BZYK' Burczyński's Sound Tracker module (.STC,.ZXS)"; sVersion  = "v1.x"; bDetected = 1;
	if(bad) sVersion += "/malformed"+bad;
	sz = X.U16(0x19,_LE); i_d = X.SA(7,18);
	if(['SONG BY ST COMPILE','SONG BY MB COMPILE','SONG BY ST-COMPILE',
	  'SOUND TRACKER v1.1','S.T.FULL EDITION  ','SOUND TRACKER v1.3'].indexOf(i_d) >= 0)
		msg = '';
	else
		msg = i_d;
	if(sz != X.Sz()) { //the message must've hit the size field, HAPPENS
		sz = len;
		if(X.isVerbose()) sOption("ord:"+ord+" ptn:"+ptn);
		if(32 <= (sz&0xFF) <= 0x7F) {
			msg += String.fromCharCode(sz&0xFF);
			if(32 <= (sz>>8) <= 0x7F) msg += String.fromCharCode(sz>>8)
		}
	}
	if(X.isVerbose()) { sOptionT(msg,"msg: "); sOption('ord:'+ord+' ptn:'+ptn+' sz:'+outSz(sz)) }
}


function isST3() {
	//ref https://ay.strangled.net/Ay_Emul30.src.7z/Players.pas -> FoundST3
	if(X.Sz() < 8) return;
	orntp = X.U16(5); if(orntp > X.Sz()-6) return; //ST3 ornaments/STC patterns table ptr
	ptntp = X.U16(7); //ST3 patterns table ptr
	j1 = ptntp-orntp; if(j1 <= 0) return;
	smptp = X.U16(3); //samples table ptr
	j2 = orntp-smptp; if(j2 <= 0) return;
	ordp = X.U16(1); //orderlist ptr
	j3 = smptp-ordp; if(j3 > X.Sz()) return;
	j4 = ordp-9; if(j4 <= 0) return;
	if(j4 % 130) {
		if(j4 < 55 || (j4-55) % 130) return;
		fID = true
	} else fID = false;
	smp = X.U8(smptp); if(!isWithin(smp, 1,16)) return; // smp # , j5
	p = smp*130 + 9 + (fID? 55: 0);
	if(ordp != p) return;
	ord = X.U8(p); if(!ord) return; //pos # , j6

	for(i=0,ptn=0; i < ord; i++) ptn = Math.max(ptn+1,X.U8(p+1+i*2))
	p += ord*2+1; if(smptp != p && smptp != p+2) return;
	orn = X.U8(orntp); if(!orn || orn > 16) return; // orn # , j7
	p = smptp+smp*2+1+orn*32; if(orntp != p) return;
	p += orn*2+1; if(X.U16(ptntp) != p) return;
	base = X.U16(smptp+1)-9;
	if(fID) base -= 55; if(base < 0) return;
	p = X.U16(orntp+1)-base;
	if(p != smptp+smp*2+1) return;
	for(i=p; i < p+32; i++) if(X.U8(i)) return;
	j8 = -1; p = ordp+2;
	for(i=0; i < ord; i++) {
		b = X.U8(p); if(b % 6) return;
		if(j8 < b) j8 = b; p += 2
	}
	bad = '';
	sz = ptntp+j8+6; if(sz-1 > X.Sz()) bad = bad.addIfNone('!short');
	if(base+p > 65536) return;
	return true;
}
if(!bDetected && X.isDeepScan() && isST3()) {
	sName = "S.T. Music's Recompiler v2 rebuilt STC module (.ST3)"; sVersion  = "v3.0"; bDetected = 1;
	if(bad && !X.isVerbose()) sVersion += "/malformed"+bad;
	if(X.isVerbose()) {
		if(fID) {
			sOptionT(decAnsi(9,55,CPSpeccy,false,Chars0to1FSpeccy));
			if(!X.c("'KSA SOFTWARE COMPILATION OF '",9)) sOption(X.SA(9,28),'in: ');
		}
		sOption('ord:'+ord+' ptn:'+ptn+' smp:'+smp+' delay:'+X.U8(0)+" @"+Hex(base)+" sz:"+outSz(sz));
	}
}


function isSidMon1() {
	//ref http://asle.free.fr/prowiz/prowiz.src.zip / r/sidmon1.c
	// & https://gitlab.com/uade-music-player/uade/-/raw/master/amigasrc/players/sidmon1/SIDMon1.0.s
	//I'm just used to calling the base ofs "a0" at this point
	a0 = X.fSig(0,0xFFFF,"41FA.... ....FFD4 43FA.... 228841FA ....D1E8 FFD843FA");
	if(a0 < 0 || (a0&1) || a0 > X.Sz()) return false;
	bad = "";
	p = X.U16(a0+2,_BE); msgp = a0+p+2; if(msgp < a0+16 || msgp > X.Sz()) return false;
//if(debug&&a0)_log("init @"+Hex(a0)+", p="+Hex(p));
	playp = X.fSig(a0+0x10,a0+msgp-6,"4E7548E7FFFE"); if(playp < 0 || (playp&1)) return false;
	smpd = X.U16(a0+2+p-2,_BE); ordp = X.U16(a0+2+p-6,_BE);
	ptnp = X.U16(a0+2+p-10,_BE); if(!smpd || !ordp || !ptnp) return false;
//_log("smpd:"+Hex(a0+2+p+smpd)+" ordp:"+Hex(a0+2+p+ordp)+" ptnp:"+Hex(a0+2+p+ptnp));
	if(a0+2+p+smpd > X.Sz() || a0+2+p+ordp > X.Sz()) return false;
	if(smpd <= ordp && smpd != 1) return false;
	ptn = (ordp-ptnp) >> 6; ord = 0; smps = [];
	if(smpd == 1) { //no samples, traversing orderlist
		ord++; pt = a0+2+p+ordp;
		if(pt+4 > X.Sz()) { sz = pt; return true; } // 1 position
		pt += 4; t = 1; sz = ordp-ptnp;
		while(t < sz) { t = X.U32(sz,_BE); if(sz+4 > X.Sz() || !t) return true; sz += 4 }
		sz -= 4; ord--
	}
	else { //samples present
		ord = (smpd-ordp) >> 2;
		smpd += a0+2+p; //now an absolute offset
		sdsz = X.U16(smpd+2,_BE); smp = sdsz >> 5;
		if(smpd+sdsz > X.Sz() || sdsz%32) return false;
//_log("smpinfo: ["+Hex(sdsz)+"/"+Hex(smp)+"]")
		if(!sdsz) for(;; sdsz += 0x20) { //guess manually
			if(sdsz+0x24 > X.Sz()) return false;
			stp = X.U32(smpd+sdsz+4,_BE); //smp start ofs
			k = X.U32(smpd+sdsz+8,_BE); //loop start ofs?
			endp = X.U32(smpd+sdsz+0xC,_BE); //loop end ofs or smp end ofs
			if(stp > k || k >= endp) break; smp++;
//_log("  smp found @"+Hex(sdsz))
			t = decAnsi(smpd+sdsz+0x10,0x10,CPAmiga).trim(); if(t != "") smps.push(t)
		}
		mendp = 0;
		for(i=0; i < smp; i++) {
			endp = X.U32(smpd+i*0x20+0xC,_BE); if(endp > mendp) mendp = endp;
			t = decAnsi(smpd+i*0x20+0x10,0x10,CPAmiga); if(t != "") smps.push();
		}
//_log("mendp:"+Hex(mendp)+" @"+Hex(smpd+sdsz+4+mendp));
		sz = smpd+sdsz+4+mendp;
		if(sz > X.Sz()) bad = bad.addIfNone("!short");
	}
	return true;
}
if(!bDetected && X.isDeepScan() && isSidMon1()) {
	sName = "SIDMon module (.SID1,.SMN,.SID)"; sVersion  = "v1"; bDetected = 1;
	if(bad != '') sVersion = sVersion.appendS("malformed"+bad,'/');
	if(X.isVerbose()) {
		t = decAnsi(msgp,0x100,CPAmiga).trim();
		if(t != "SID-MON BY R.v.VLIET  (c) 1988") sOptionT(t);
		sOptionT(addEllipsis(smps.join(','),200),'smps:');
		sOption("ord:"+ord+" ptn:"+ptn+" smp:"+smp+" sz:"+outSz(sz));
	}
}


function isSidMon2() {
//from https://github.com/neumatho/NostalgicPlayer/blob/main/Source/Agents/Players/SidMon20/SidMon20Worker.cs
	if(X.Sz() < 0x5A) return false;
	if(!X.c("'SIDMON II - THE MIDI VERSION'", 0x3A)) return false;
	ord = X.U8(2)+1; spd0 = X.U8(3); smp = X.U16(4,_BE);
	ofs = 0x3A+X.U32(6,_BE); songlenlen = X.U32(0xA,_BE); ofs += songlenlen; //yes
	if(ofs != 0x5A) return false;    
	p = 0x0E;
	for(i = 0; i < 10; i++) {
		t = X.U32(p,_BE); if(t > 0x0FFFFF || !t) return false;
		switch(i) {
		case 0: var ordt = [ofs,t]; break;
		case 1: if(t != ordt[1]) return false; var notexpost = [ofs,t]; break;
		case 2: if(t != notexpost[1]) return false; break;
		case 3: if(t % 32) return false; break;
		case 7: if(t != smp) return false; var smpt = [ofs,t]; break;
		case 8: trk = t >> 1; break;
		case 9: var trkt = [ofs,t]; break;
		}
		p += 4; ofs += t; if(ofs > X.Sz()) return false
	}
	ofs = trkt[0]+trkt[1]; if(ofs&1) ofs++;
	smpsz = 0; smps = [];
	for(p = smpt[0]; p < smpt[0]+smpt[1]; p += 0x40) {
		smpsz += X.U16(p+4,_BE) << 1;
		t = decAnsi(p+0x20,0x20,CPAmiga).trim(); if(t != "") smps.push(t)
	}
	sz = ofs+smpsz;  smp >>= 6;
	return true
}
if(!bDetected && isSidMon2()) {
	sName = "SIDMon II module (.SID2)"; bDetected = 1;
	if(X.isVerbose()) {
		sOptionT(X.SA(7,0x20));
		sOptionT(X.SA(0x27,0x20),"in: ");
		sOptionT(addEllipsis(smps.join(','),0x100),'smp/msg:"','"');
		sOption("trk:"+trk+" ord:"+ord+" smp:"+smp+" sz:"+outSz(sz))
	}
}


function isDigiIllu() {
	//ref https://github.com/tonioni/WinUAE/blob/master/prowizard/rippers/DigitalIllusion.c
	if(X.Sz() < 17) return false;
	smp = X.U16(0,_BE); if(smp > 31) return false;
	ordp = X.U32(2,_BE); if(ordp < smp*8+2) return false; //orderlist ptr
	ptndp = X.U32(6,_BE);  if(ptndp-ordp > 0x80 || X.U8(ptndp-1) != 0xFF) return false; //pattern data ptr
	smpdp = X.U32(10,_BE); if(smpdp > 0xFFFF) return false; //sample data ptr
	if(ptndp <= ordp+1 || smpdp <= ordp) return false;
	if(ordp > X.Sz() || ptndp > X.Sz()) return false;
	allsmpsz = 0;
	for(j=0; j < smp; j++) {
		ssz = X.U16(j*8+14,_BE)<<1; if(ssz > 0xFFFF) return false;
		lst = X.U16(j*8+18,_BE)<<1; if(lst > ssz) return false;
		lsz = X.U16(j*8+20,_BE)<<1;
		if(lsz > ssz+2 || lst+lsz > ssz+2 || (lst && lsz <= 2)) return false;
		if((lst || lsz > 2) && !ssz) return false;
		if(X.U8(16+j*8) > 0xF || X.U8(17+j*8) > 0x40) return false; //ft & vol checks
		allsmpsz += ssz
	}
	if(allsmpsz <= 2) return false;
	for(i=ordp,ptn=0; i < ptndp-1; i++) if((t=X.U8(i)) > 0x80) return false; else if(t >= ptn) ptn = t+1;
	sz = allsmpsz+smpdp;
	return true
}
if(!bDetected && isDigiIllu()) {
	sName = "Digital Illusions Creative Entertainment packed module (.DI)"; bDetected = 1;
	if(sz > X.Sz() && !X.isVerbose()) sVersion = "malformed!short";
	if(X.isVerbose()) {
		sOption('ord:'+(ptndp-ordp-1)+' ptn:'+ptn+' smp:'+smp+' sz:'+outSz(sz));
	}
}


function isImagoOrpheus() {
	//ref https://sourceforge.net/p/xmp/libxmp/ci/master/tree/src/loaders/imf_load.c
	if(!X.c("'IM10'",0x3C)) return false;
	if(X.U16(0x20) > 0x100 || X.U16(0x22) > 0x100 || X.U16(0x24) > 0xFF
	  || !X.U8(0x30) || X.U8(0x31) < 0x20 || X.U8(0x32) > 0x40 || X.I8(0x33) < 4) return false;
	ord = X.U16(0x20); ptn = X.U16(0x22); ins = X.U16(0x24); spd = X.U8(0x30); bpm = X.U8(0x31);
	ch = chon = 0; chns = [];
	var t,rs,r,psz,b,nsmp;
	for(p=0x40; p < 0x240; i++,p+=0x10) { // name[12]+chorus+rev+pan+status
		if(X.isVerbose()) { t = X.SC(p,12,'CP437').trim(); if(t.length) chns.push(t) }
		if(X.U8(p+15) <= 1) ch++; if(!X.U8(p+15)) chon++;
	}
	if(!ch) return false;
	for(notes=i=0,p=0x340; i < ptn && p < X.Sz(); i++) {
		psz = X.U16(p)-4; rs = X.U16(p+2); if(rs > 0x100) return false; p += 4;
		if(X.isDeepScan()) while(--psz >= 0 && p < X.Sz()) {
			b = X.U8(p++); if(b == 0) { r++; if(r >= rs) return false; continue }
			if(b&0x20) { if([0xA0,0xFF].indexOf(X.U8(p++)) < 0) notes++; p++; psz -= 2 }
			if(b&0x80) { p += 2; psz -= 2 }  if(b&0x40) { p += 2; psz -= 2 }
		} else p += psz;
	}
	if(p > X.Sz()) return false;
	inss = []; smps = [];
	for(smp=i=0; i < ins; i++) {
		if(X.isVerbose()) { t = X.SC(p,32,'CP437').trim(); if(t.length) inss.push(t) }
		p += 0x180; //skip name,map, reserved, all the envelopes, sample flags, fadeout
		if((nsmp=X.U16(p-6)) > 0xFF) return false;
		if(!X.c("'II10'",p-4) && X.U32(p-4)) return false;
		for(j=0; j < nsmp; j++,smp++) {
			if(X.isVerbose()) smps.push(X.SC(p,13,'CP437').trim());
			var ssz = X.U32(p+0x10), lps = X.U32(p+0x14), lpe = X.U32(p+0x18), f = X.U8(0x30);
			if(ssz > 0x100000 || lps > 0x100000 || lpe > 0x100000) return false;
			if(X.U8(p+0x24) > 0x40/*vol*/) return false;
			p += 0x40;
			if(!X.c("'IS10'",p-4) && !X.c("'IW10'",p-4)) return false;
			p += ssz
		}
	}
	return true
}
if(!bDetected && isImagoOrpheus()) {
	sName = "Imago Orpheus module (.IMF)"; bDetected = 1;
	if(X.isVerbose()) {
		sOptionT(X.SC(0,0x20,'CP437'));
		sOptionT(addEllipsis(inss.join(' ')),'insts/msg:"','"');
		sOptionT(addEllipsis(smps.filter(funSampleName).join(' ')),'smps/msg:"','"');
		sOption('spd0:'+spd+' bpm0:'+bpm+' ch:'+chon+(ch==chon?'':ch)
			+' ord:'+ord+' ptn:'+ptn+' ins:'+ins+' smp:'+smp+(notes?' notes:'+notes:'')+' sz:'+outSz(p));
	}
}



function isPMD() {
	// ref https://raw.githubusercontent.com/ValleyBell/MidiConverters/master/pmd_SeqFormat.txt
	// & https://web.archive.org/web/20220928062748/https://lithcore.cn/2318/
	// & https://github.com/mistydemeo/pmdmini/blob/master/src/pmdmini.c
	// & https://gitlab.com/bunnylin/supersakura/blob/dev/doc/mus/pmd.md
	// & https://github.com/stuerp/foo_input_pmd/blob/master/PMD/PMD.cpp
	if(X.Sz() < 37 || !isWithin(nV=X.I8(0), -1,2/*0xF*/)) return false;
	if(X.U16(1) == 0x1A) var nptr = 13; else if(X.U16(1) == 0x18) var nptr = 12; else return false;
	function re(p,t) { if(debug>1)_l2r('pmd',p,t); return false }
	function trktxt(n) {
		if(n < 0) return '??';
		else if(n < 6) return 'FM '+(n+1);
		else if(n < 9) return 'SSG '+(n-5);
		else if(n === 9) return 'OPNA ADPCM B';
		else if(n === 10) return 'OPNA Rhythm';
		else if(n === 11) return 'Rhythm Subs';
		else if(n === 12) return  'FM insts';
		else return '??'
	}
var trkpt = []; //debug
	for(i=0,oldchn=X.U16(1); i < nptr; i++) { //ptrs to the 11 tracks, rhythm funcs, (maybe optionally?) FM insts
		p = X.U16(1+i*2); if(/*p &&*/ !isWithin(p, 0x1A,X.Sz()-1)) return re(p,'!badtrkp');
trkpt.push(Hex(p+1)+': '+trktxt(i)); //debug
		if(isWithin(i, 1,11) && X.U8(p) != 0x80) return re(p,'!badtrkend'+X.U8(p)); //data blocks end in 80h
		if(isWithin(i, 1,10) && p <= oldchn) return re(p,'!badtrkp'); // not sorted right
		if(!i && X.U8(p) == 0xC6) for(j=p+1; j < p+9; j+=2)
			if((t=X.U16(j)) && !isWithin(t, p,X.Sz()-1)) return re(p,'!badC6trkp');
		oldchn = p;
	}
//_l2r('pmd','',trkpt.join(' || ')); //debug
	rhosz = (X.U16(0x17)-X.U16(0x15)-1); rhysz = (X.U16(0x19)-X.U16(0x17)-2) >> 1;
	rho = rhy = 0;
	var rhy1 = oldrhy = 0, rhy0 = 0xFFFFFF;
	if(rhosz > 0) for(q=X.U16(0x15)+1; q < X.U16(0x17)+1; q++) {
		//rhythm subroutine orderlist, most likely? the first byte seems to be the number of loops
		t = X.U8(q);
//_l2r('rho',q,'#'+Hex(t))
		if(t == 0x80) break; if(t >= rhy && t < 0x80) rhy = t+1; //just sometimes, rhy-orderlist items like F6 happen
		rho++
	}
	if(rhysz > 0 && rhosz > 0) for(q=X.U16(0x17)+1; q < rhy0 && q < X.U16(0x19)-1 && rhy1 < rhy; q+=2) {
		//rhythm subroutine pointers; the last 2 bytes are probably about the version
		t = X.U16(q)+1;
//_l2r('rhy',q,'*'+Hex(t))
		if(!isWithin(t, q,X.Sz())) break;//_l2r('rhy',q,'OOBrhyptr '+Hex(t));
		if(!rhy1) rhy0 = t;
		if(t >= 0xFE00 || X.U16(0x19)-q < 2) break;
		rhy1++; oldrhy = t;
	}
	bad = ''; ttype = ''; extra = -1; sV = '';
	if(rhysz >= 4) {
		extrap = p-3; extra_type = X.U8(extrap+2);
		if(!isWithin(extra_type, 0x40,0x4F)) bad = bad.addIfNone('!badxtype'+Hex(extra_type));
		if(extra_type < 0x42) ttype = "PCP/P86";
		else if(extra_type < 0x48) ttype = "PPS";
		else if(nV == -1) ttype = "FM Towns"; else ttype = "PPZ";
		extra = X.U16(extrap,_LE)+1;
		if(!isWithin(extra, 1,X.Sz()))
			bad = bad.addIfNone('!'+Hex(extra)+' out of '+Hex(extrap-1));
		else for(i=0; i < 4; i++) {
			p = X.U16(extra+i*2,_LE);
			if(p < 27 || p+1 > X.Sz()) bad = bad.addIfNone('!extrapOOB'+Hex(extra))
		}
	}
//else _l2r('fe',p-1,'version?'+Hex(X.U8(p-1)));
	return true
}
if(!bDetected && isPMD() ) {
	bDetected = 1;
	sName = "Masahiro 'Kajapon' Kajihara's Professional Music Driver module (.M,.M2)";
	sVersion = nV >= 0? 'v'+nV+'/'+['OPN/OPNA','OPM','OPL2',,,,,,,,,,,,,,][nV]: '';
	sVersion = sVersion.appendS(/*'t'+Hex(extra_type).substr(0,2)+":"+*/ttype/*+" tagptrs:"+Hex(extra)*/,' ');
	if(bad.length) sVersion = sVersion.appendS('malformed'+bad,'/');
	if(X.isVerbose()) {
		fnames = [];
		if(extra > 0) {
			if(extra_type >= 0x48) {
				n = X.SC(X.U16(extra,_LE)+1,0x100,"Shift_JIS"); extra+=2;
				if(n != "") fnames.push("PPZ:"+n)
			}
			if(extra_type >= 0x42) {
				n = X.SC(X.U16(extra,_LE)+1,0x100,"Shift_JIS"); extra+=2;
				if(n != "") fnames.push("PPS:"+n)
			}
			n = X.SC(X.U16(extra,_LE)+1,0x100,"Shift_JIS"); extra+=2;
			if(n != "") fnames.push("PPC/P86:"+n);
			title = X.SC(X.U16(extra,_LE)+1,0x100,"Shift_JIS");
			if(title == "\x1A") title = "";
			artist = X.SC(X.U16(extra+2,_LE)+1,0x100,"Shift_JIS");
			if(artist == "\x1A") artist = "";
			arenji = X.SC(X.U16(extra+4,_LE)+1,0x100,"Shift_JIS");
			if(arenji == "\x1A") arenji = "";
			rem = X.SC(X.U16(extra+6,_LE)+1,0x100,"Shift_JIS");
			if(rem == "\x1A") rem = "";
			sOption(title);
			sOption(artist,"by: ");
			sOption(arenji,"mixed by: ");
			sOption(rem);
			sOption(fnames.join(", "),"fn: ");
		}
		t = X.U8(X.U16(0x15)+1);
		sOption('rhyord:'+rho+(rho?(t?'×'+t:'')+' rhy:'+rhy:''))
	}
}


function isMUAP98() {
	//ref http://ohta.music.coocan.jp/packen/data/muap641s.lzh / NAX.ASM & PLAY4.ASM
	function re(p,t) { if(debug>1)_l2r('muap',p,t); return false }
	if(X.Sz() < Math.max(X.U16(0x20),0x26) ) return false;
	ch = notes = sz = sus = 0; msg = ''; var t;
	for(i=oldp=0; i < 0x11; i++) {
		t = X.U16(i*2);
		if(!isWithin(t, Math.max(0x18,oldp),X.Sz()))
			return re(i*2,'!ptr#'+i+'OOB '+outArray([t,oldp,X.Sz()],16));
		if(i && X.U8(t-1) != 0xFC) return re(t-1,'!FC@');
		if(X.U8(t) != 0xFC) {
			var r = parseMUAP98(t, X.isDeepScan()?BCParseToEoF:BCParseToReasonable, i);
			if(r[0] == BCInvalidFormat) return false;
			notes += r[0]; msg = msg.appendS(r[3],'\n'); ch++; if(sz < r[4]) sz = r[4]; sus += r[5]
		} else if(sz < t+1) sz = t+1;
		oldp = t
	}
	if(sus > ch*3) return false;
	bad = sus?'suspicious'+sus:'';
	return true
}
if(!bDetected && isMUAP98()) {
	sName = "Packen/ぱっくん Software MUAP98/みゅあっぷ Object chiptune (.O,.OX+TONES.DTA)"; bDetected = 1;
	if(bad.length) sVersion = bad;
	if(X.isVerbose()) {
		sOptionT(msg); //actually meant to display lyrics
		sOption('ch:'+ch+(X.isDeepScan()?' notes:'+notes+' sz:'+outSz(sz):''));
	}
}


function isRobHubbardRH() {
	//from https://gitlab.com/uade-music-player/uade/-/raw/master/amigasrc/players/wanted_team/RobHubbard/src/RobHubbard_v7.asm
	//the FX files not included -- TODO
	if(!X.c("6000.... 6000.... 6000.... 6000.... 6000....  41FA.... ........ 4E7541FA"))
		return false; //Check2
	a2 = 0x40; d4 = 8;
	do { //loop2
		if(X.U16(a2,_BE) == 0x2418) break;
		a2 += 2; d4--
	} while(d4);
	if(!d4) return false; //found2
	smp = d3 = X.U8(a2-1)+1;
	a2 = 0x36; d4 = 5;
	do { //loop3
		if(X.U16(a2,_BE) == 0x41FA) break;
		a2 += 2; d4--
	} while(d4);
	if(!d4) return false; //found3
	a2 += 2;
	a4 = a2;
	d4 = a2+X.U16(a2,_BE)+2; //minus a3 = moduleptr, 0 in our case
	if(X.U16(a4+2,_BE) == 0xD1FC) d4 += 0x40;
	a3 = d4-2; //again +moduleptr; then moduleptr[a6] = a3 (puts in sampleptr); a6 += 4;
	d5 = 0; a2 = a3;
	do { //loop4
		d1 = X.U32(a3,_BE);
		if(d1 > 0x10000) return false;
		d1 += 6;
		d5 += d1;
		a3 += d1;
		d3--
	} while(d3);
	if(X.U16(a3,_BE) != 0x4E71) return false; //at the EOF now
	a3 = 0; a0 = 130+a3;
	d0 = 10;
	do { //loop
		if(X.U16(a0,_BE) == 0x41EB) { a0 += 2; break }
		a0 += 2; d0--;
	} while(d0);
	if(!d0) return false; //else found
	d1 = 0;
	d2 = X.U16(a0,_BE);
	a3 += d2;
	do { //hop
		a3 += 18; d1++;
	} while(X.U16(a3,_BE));
	d2 = a2-a3;
	do { //petla
		b = X.U8(a3);
		if(b != 0x84)
			if(b != 0x85)
				{ a3++; d2--; if(d2 < 0) break; else continue }
		d0++; a3++; d2--
	} while(d2 >= 0);
	songsz = d4;
	sz = d4+d5;
	steps = d0;
	x = d1;
	return true;
}
if(!bDetected && X.isDeepScan() && isRobHubbardRH()) {
	sName = "Rob Hubbard's module (.RH)"; sVersion = "v1.4"; bDetected = 1;
	if(X.isVerbose()) {
		if(x > 1) sOption(x,"×");
		sOption("smp:"+smp+" steps:"+steps+" songsz:"+songsz+" sz:"+outSz(sz))
	}
}


function isPuma() {
	//from https://gitlab.com/uade-music-player/uade/-/raw/master/amigasrc/players/wanted_team/PumaTracker/src/PumaTracker_v2.asm
	//ref http://lclevy.free.fr/amiga/exotic/puma_str.txt
	// && https://github.com/OpenMPT/openmpt/blob/master/soundlib/Load_puma.cpp
	ord = X.U16(12,_BE)+1; if(ord > 256) return false;
	ptn = X.U16(0xE,_BE); if(!ptn || ptn > 128) return false;
	synsmp = X.U16(0x10,_BE); if(!isWithin(synsmp,1,32)) return false; //"number of sound data"
	loop = X.U16(0x12,_BE); 	bad = ''; if(loop >= ord) bad = bad.addIfNone('!badloop');
	if(charStat(X.readBytes(0,12),1).indexOf('allxsc') < 0) return false;
	for(p=0x50,i=mp=0; i < ord && p < X.Sz(); p+=14,i++) //verify orderlist integrity
		for(c=0; c < 4; c++) {
			if((pt=X.U8(p+c*3)) >= 0x80 || !isWithin(X.I8(p+c*3+2),-0x30,0x30)) return false;
			if(pt > mp) mp = pt
		}
	if(i < ord) return false; mp++;
	minsz = p+ptn*8+4+synsmp*16+4; if(minsz >= X.Sz()) return false;
	for(i=0; i < ptn; i++) { //test all patterns
		if(!X.c("'patt'",p)) return false; p += 4; row = 0;
		for(;row < 32; p += 4) {
			var d = X.readBytes(p,4); if(d[0]%2 || !isWithin(d[3],1,32-row)) return false; row += d[3]
		} }
	if(!X.c("'patt'",p)) return false; p += 4;
	smp = msmpp = mssz = evc = 0; plim = Math.min(X.Sz(),0x100000);
	for(i = 0; i < 10; i++) { //test waveform tables
		smpp = X.U32(0x14+i*4,_BE); if(!smpp) continue;
		ssz = X.U16(0x3C+i*2,_BE); if(ssz&1) ssz--; ssz <<= 1;
		if(smpp < minsz || smpp > 0x100000) return false;
		if(smpp > msmpp) { msmpp = smpp; mssz = ssz}
		smp++;
	}
	function checkScript(isVol) {
		var isFirst = true;
		while(p+4 < X.Sz()) {
			var d = X.readBytes(p,4); p += 4;
			if(isFirst && isVol && d[0] != 0xC0) return false;
			switch(d[0]) {
			case 0xA0: evc++; break;
			case 0xB0: evc++; return !(d[1] & 3);
			case 0xC0: if(!isVol) return false; evc++; break;
			case 0xD0:
				if(isVol) return false; evc++;
				if(d[1]&1)/*freezes playback*/ bad = bad.addIfNone('!oddpitch'); break;
			case 0xE0: evc++; return true;
			default:
				if(!isVol && decEncoding(d,CPAmiga) === 'inst') { p -= 4; return evc > 0 }
				return false;
			}
			isFirst = false;
		}
		return evc > 0
	}
	for(i=0; i < synsmp && p < X.Sz(); i++) {
		if(!X.c("'inst'",p)) return false; p += 4;
		if(!checkScript(true)) return false;
		if(!X.c("'insf'",p)) return false; p += 4;
		if(!checkScript(false)) return false;
	}
	if(!X.c("'inst'",p)) return false; p += 4
	if(p > X.Sz()) { sz = -1; bad = bad.addIfNone('!short'); return true }
	sz = Math.max(msmpp+mssz, p);
	return true;
}
if(!bDetected && isPuma()) {
	sName = "PumaTracker module (.PUMA)"; sVersion = "v1.1"; bDetected = 1;
	if(bad != '') sVersion = sVersion.appendS('/malformed'+bad,'/');
	if(X.isVerbose()) {
		sOption(decAnsi(0,0x0C,CPAmiga).trim());
		sOption('ord:'+ord+' ptn:'+mp+(mp!=ptn?'/'+ptn:'')+(smp?' wf.smp:'+smp:'')
			+' syn.smp:'+synsmp+(loop?' lp:'+loop:'')+' events:'+evc+' sz:'+outSz(sz))
	}
}


function isSeanConran() {
	//from https://gitlab.com/uade-music-player/uade/-/raw/master/amigasrc/players/wanted_team/SeanConran/src/Sean Conran_v3.asm
	//Check2
	if(X.c("0FFF0FE2") || X.c("10000FE2")) {
		if(!X.c("0FC40FA7 0F8B0F6E",4)) return false; //test1
		p = 0x124; //start searching for initp from here
		a0 = 8;
	}
	else
		if(X.c("0F1C0F0E 0F000EF2 0EE40ED6")) {
			p = 0x1C4;
			a0 = 0xA8; //skip to LastCheck
		}
		else return false;
	do { //InitPlayer.FindInit
		t = X.U32(p,_BE); if(!t) return false;
		p += 2; if(p > X.Sz()) return false
	} while([0x45FA,0x43F9,0x41F9].indexOf(t>>16) < 0);
	initp = p-2;
	//LastCheck
	a0 += 0x11C;
	for(d1 = 0; d1 < 0x80; d1++) { //CheckSFX
		if(X.c("7F7F7F7F",a0) || X.c("FFFF",a0)) return false;
		a0 += 2
	}
	// a0 = GetListData(0);
	x = IntAddress = 0; Twin = false;
	do { //Next
		t = X.U16(p,_BE);
		p += 2; if(p > X.Sz()) return false
	} while([0x7000,0x7200].indexOf(t) < 0);
	if(t === 0x7000) { //OneSub
		x = 1;
		// do { //FindSongs
		//     t = X.U16(p,_BE); p += 2; if(p > X.Sz()) return false
		// } while(t != 0x41FA);
		// FirstPos = p+X.I16(p,_BE);
	}
	else { //t=0x7200
		while(!x && p < X.Sz()) {
			if(X.c("00000000",p)) return false;
			if(X.c("21C80070",p)) { //OK4
				// t = p-4; IntAddress = t+X.I16(t,_BE);
				p += 2;
				if(X.c("00003B76",0x24C)) Twin = true;
			} else {
				if(X.c("43FA",p)) {
					t = p+2+X.I16(p+2,_BE); //OK0
					// songsp = t;
					x = (X.I16(t,_BE)-t) >> 3;
				}
				else if(X.c("43EA",p)) {
					t = X.I16(p+2,_BE); //OK2
					// songsp = t;
					x = (X.I16(t,_BE)-t) >> 3;
				}
				else p += 2
			}
		}
		if(!x || p >= X.Sz()) return false
	}
	// there are more checks... although they aren't really necessary here
	return true
}
if(!bDetected && isSeanConran()) {
	sName = "Sean Conran module (.SCR)"; sVersion = "v1.2"; bDetected = 1;
	if(Twin) sVersion += "/Megatwins"
	if(X.isVerbose()) {
		if(x > 1) sOption(x,"×");
	}
}


function isBenDaglish() {
	//from https://gitlab.com/uade-music-player/uade/-/raw/master/amigasrc/players/wanted_team/BennDaglish/Benn Daglishv3.asm
	if(!X.c("6000....6000.... ....6000")) return false; //Check2
	d1 = X.I16(0x02,_BE); if(d1 <= 0 || (d1&1) || d1 > X.Sz()) return false;
	a0 = a1 = a3 = d1+2;
	d1 = X.I16(0x06,_BE); if(d1 <= 0 || (d1&1) || d1 > X.Sz()) return false;
	d1 = X.I16(0x0C,_BE); if(d1 <= 0 || (d1&1) || d1 > X.Sz()) return false;
	if(!X.c("3F006100",a1)  || !X.c("3D7C",a1+6)
	  || !X.c("41FA",a1+12)) return false;

	d0 = 0x7F;
	while(d0) //.l6
		if(X.c("D040D040 D04041FA",a0)) {
			a0 += 8;
			a1 = a0+X.I16(a0,_BE); //note the value's signed
			break;
		}
		else {
			a0 += 2; d0--; if(!d0 || a0 > X.Sz()) return false
		}
	x = 0;
	BD_L7: while(1) {
		d2 = 4;
		while(d2) {
			d0 = X.U16(a1,_BE) & 0xFC00;
			a1 += 2; if(a1 > X.Sz()) return false;
			if(d0) { // .not_subsong
				x--; //last subsong
				if(x < 0) x == 0;
				break BD_L7
			}
			else d2--
		}
		x++
	}
	x++;

	//size calc
	// incorrect on some files like 3d galax.bd or super cars.bd
	// but that's what EaglePlayer has, UADE debugger-tested
	d5 = d6 = 0;
	d0 = 0x80;
	while(d0) { //.l9
		t = X.U16(a0,_BE);
		a0 += 2; if(a0 > X.Sz()) return false;
		if(t === 0x41FA) break; else d0--
	} if(!d0) return false;
	smpi1 = a0+X.U16(a0,_BE); //.ok6 .. SampleInfo1
	a0 = 12+X.I16(12,_BE);
	d0 = 0x80; //.l10
	while(d0) { d0--;
		if(X.c("D040D040 41FA",a0)) break;
		else a0 += 2;
		if(a0 > X.Sz()) return false
	}
	if(d0) { //.ok7
		a0 += 6;
		d0 = X.I16(a0,_BE);
		if(d0&1) a0 = 0;
		else {
			a0 += d0;
			if(X.I16(a0,_BE)) a0 = 0
		}
	} else a0 = 0;
	smpi2 = a0; //.ok8

	a0 = smpi1;
	while(1) { //.com1
		d0 = X.U32(a0,_BE);
		a0 += 4; if(a0 > X.Sz()) return false;
		// Interestingly, the UAE emu sets flags on "move"!
		// So ↓ is a d0 check, would've probably been a0 on a real machine
		if(!d0) break;
		if(d0 >> 16) { a0 -= 4; break }
	}
	a0 -= 8; //.ok9
	smp1 = (a0 - smpi1) >> 2;
	d0 = smpi2;
	if(d0) {
		a0 = d0;
		while(1) { //.com2
			d0 = X.U32(a0,_BE);
			a0 += 4; if(a0 > X.Sz()) return false;
			if(!d0) break;
			if(d0 >> 16){ a0 += 4; break }
		}
		a0 -= 8; //.ok10
		smp2 = (a0 - smpi2) >> 2;
	}
	else smp2 = 0;
	//.one_smp_info

	// calc size #1
	d3 = smp1;
	a2 = smpi1+X.I32(smpi1,_BE); //rel.ptr to smpinfo1 table #0?
	d1 = X.I32(a2,_BE);
	d2 = X.U16(a2+8,_BE);
	i = d4 = 0;
	do { //.l11
		i++; if(i === smp1) break;
		a2 = smpi1+X.I32(smpi1+(i<<2),_BE);
		  if(a2 < 20 || a2 > X.Sz()) return false;
		d4 = X.I32(a2,_BE);
		if(d1 > d4) continue;
		if(d1 != d4) {
			if(d4 != X.I32(a2+4,_BE))
				d6 = X.U16(a2+10,_BE);
			else d6 = 0;
			d5 = X.U16(a2+8,_BE);
		} else {
			d5 = X.U16(a2+8,_BE);
			if(d2 > d5) continue;
		}
		d1 = d4; d2 = d5
	} while(i < smp1);

	//.ok11
	d2 += d6;
	sz = smpi1+d1+(d2<<1);

	// calc size #2
	if(smpi2) {
		d3 = smp2 << 2;
		a2 = smpi2+X.I32(smpi2,_BE);
		d1 = X.I32(a2,_BE);
		d2 = X.U16(a2+8,_BE);
		i = d6 = 0;
		do {
			i += 4; if(i === d3) break;
			a2 = smpi2+X.I32(smpi2+i,_BE);
			if(a2 < 20 || a2 > X.Sz()) return false;
			d4 = X.I32(a2,_BE);
			if(d1 > d4) continue;
			if(d1 != d4) {
				if(d4 != X.I32(a2+4,_BE))
					d6 = X.U16(a2+10,_BE);
				else d6 = 0;
				d5 = X.U16(a2+8,_BE);
			} else {
				d5 = X.U16(a2+8,_BE);
				if(d2 > d5) continue;
			}
			d1 = d4; d2 = d5
		} while(i < d3);

		d2 += d6;
		a1 = smpi1 + d1 + (d2<<1);
		if(sz < a1) sz = a1;
	}
	return true
}
if(!bDetected && X.isDeepScan() && isBenDaglish()) {
	sName = "Ben Daglish's module (.BD)"; sVersion = "v1.2"; bDetected = 1;
	if(X.isVerbose()) {
		if(x > 1) sOption(x,"×");
			sOption("smp:"+smp1+"+"+smp2+" sz:"+outSz(sz));
	}
}


function isAndrewParton() {
	//from https://gitlab.com/uade-music-player/uade/-/raw/master/amigasrc/players/wanted_team/Andrew Parton/SRC_AndrewParton/Andrew Parton_v2.asm
	if(!X.c("'BANK'")) return false;
	for(i=0; i < 20; i++)
		if(X.U32(4+(i<<2),_BE) >= 0x200000) return false;
	for(i=0; i < 40; i++)
		if(X.U32(0x54+(i<<2),_BE) >= 0x10000) return false;
	p = 0x54; q = 4; smp = 0;
	smpt = [];
	s = 0x1E4;
	for(i=0; i < 20; i++) {
		t = X.U32(q,_BE); q += 4;
		if(t) {
			smpt.push(X.SA(s,16).trim());
			smp++; s += 16 + X.U32(p,_BE);
		}
		p += 4
	}
	smpsz = s;
	bad = false;
	while(s < X.Sz()) {
		t = X.U8(s); s += 1;
		if(t === 0xFF) break;
	}
	if(s > X.Sz())
		if(X.isHeuristicScan()) bad = true;
		else return false;
	if(X.U8(s) === 0x2F) { sz = s+1; songsz = sz-smpsz }
	else {sz = s; bad = true }
	return true
}
if(!bDetected && X.isDeepScan() && isAndrewParton()) {
	sName = "Andrew Parton's module (.BYE)"; sVersion = "v1.2"; bDetected = 1;
	if(bad) sVersion += "/malformed";
	if(X.isVerbose())
		sOption("smp:"+smp+" songsz:"+songsz+" smpsz:"+smpsz+" sz:"+outSz(sz))
}


function isAshleyHogg() {
	//from https://gitlab.com/uade-music-player/uade/-/raw/master/amigasrc/players/wanted_team/Ashley Hogg/SRC_AshleyHogg/Ashley Hogg_v1.asm
	p = 0;
	for(i=0; i < 4; i++) {
		if(!X.c("6000",p)) return false; p += 2;
		d2 = X.I16(p,_BE); if(d2 <= 0 || (d2&1)) return false; p += 2
	}
	if(X.c("6000",p)) { //new check
		p += 2;
		d2 = X.I16(p,_BE); if(d2 <= 0 || (d2&1)) return false; p += 2;
		if(!X.c("6000",p)) return false; p += 2;
		d2 = X.I16(p,_BE); if(d2 <= 0 || (d2&1)) return false;
		p += d2;
		if(!X.c("48E7FFFE 6100",p)) return false; p += 6;
		p += X.I16(p,_BE);
		if(!X.c("4DF9 00DFF000",p)) return false;
		sV = "new";
	} else //old check
		if(X.c("303C0000 662233C0",p))
			sV = "old";
		else return false;

	if(sV === "new") { //new format
		a2 = special = 0x1C;
		a0 = X.I16(2,_BE);
		title = X.SA(special,X.fStr(special,a0-a2,"  ")-a2).trim();
		//skipped a cycle that replaces twin spaces with enters in the info
		do {
			t = X.U16(a0,_BE); a0 += 2;
		} while(t != 0x45FA || a0 > X.Sz());
		a2 = a0; a0 += X.I16(a0,_BE);
		if(a0 > X.Sz()-2)
			if(X.isHeuristicScan()) bad = true; else return false;
		d0 = X.I16(a0,_BE);
		x = d0 >> 2;
		a0 += d0 + X.I16(a0-2,_BE);
		do {
			if(a0 > X.Sz()-2)
				if(X.isHeuristicScan()) bad = true; else return false;
			t = X.I16(a0,_BE); a0 += 2;
		} while(t != 0x03F2);
		sz = a0;
		info = 'title: "'+title+'" sz:'+outSz(sz)
	}
	else { //old format
		special = 0;
		a2 = 16;
		a0 = 2; a0 += X.U16(a0,_BE);
		do { t = X.I16(a0,_BE); a0 += 2; } while(t != 0x1970 || a0 > X.Sz());
		a1 = a0-4; a1 += X.I16(a1,_BE);
		do { t = X.I16(a0,_BE); a0 += 2; } while(t != 0x41FA || a0 > X.Sz());
		a0 += X.I16(a0,_BE);
		x = (a1-a0) >> 4;
		do { t = X.U16(a2,_BE); a2 += 2; } while(t != 0xC2FC || a0 > X.Sz());
		a2 += 4; smpip = a2+X.U16(a2,_BE);
		do { t = X.U16(a2,_BE); a2 += 2; } while(t != 0x47FA || a0 > X.Sz());
		a0 = a2; a2 += X.U16(a2,_BE);
		songsz = a2;
		do { t = X.U16(a0,_BE); a0 += 2; } while(t != 0x49FA || a0 > X.Sz());
		do { t = X.U16(a0,_BE); a0 += 2; } while(t != 0x49FA || a0 > X.Sz());
		a0 += X.U16(a0,_BE);
		smp = (a0-smpip)/0x2C;
		d1 = 0; a1 = smpip;
		a3 = 0x54F3; //value copied from a debugging session ¯\(。⊿°)/¯
		do { //NextInfo
			d2 = X.I32(a1+0x20,_BE);
			if(d2 >= 0 && d1 <= d2) { d1 = d2; a3 = a1 }
			a1 += 0x2C
		} while(a0 > a1);
		d0 = X.U16(a3+0x28,_BE);
		d1 += d0+d0;
		smpsz = d1;
		sz = songsz+smpsz;
		info = "smp:"+smp+" songsz:"+Hex(songsz)+" smpsz:"+Hex(smpsz)+" sz:"+outSz(sz)
	}
	return true
}
if(!bDetected && X.isDeepScan() && isAshleyHogg()) {
	sName = "Ashley Hogg's module (.ASH)"; sVersion = sV; bDetected = 1;
	if(bad) sVersion += "/malformed";
	if(X.isVerbose()) {
		if(x > 1) sOption(x,"×");
		sOption(info)
	 }
}


function isCinemaware() {
	//from https://gitlab.com/uade-music-player/uade/-/raw/master/amigasrc/players/wanted_team/Cinemaware/Cinemaware.asm
	if(!X.c("'IBLK'")) return false;
	d2 = X.U8(4);
	if(!d2 || d2 > 0x80) return false;
	a1 = 0x16+0x8A*d2;
	aseq = X.fSig(a1,0x104,"'ASEQ'");
	if(aseq < 0 || (aseq&1)) return false;
	p = aseq+4; sz = 0; bad = false;
	do {
		sz += 5;
		if(sz+p > X.Sz())
			if(X.isHeuristicScan()) { bad = true; break }
			else return false;
	} while(!X.c("102F00",sz+p-3));
	sz += p; ord = Math.floor((sz-p)/100);
	return true
}
if(!bDetected && X.isDeepScan() && isCinemaware()) {
	sName = "Cinemaware module (.CIN)"; bDetected = 1;
	if(bad) sVersion = "malformed";
	if(X.isVerbose()) {
		sOption("ord:"+ord+" sz:"+outSz(sz)+ " (sans ext.samples)")
	}
}


function isCustomMade() {
	//ref https://gitlab.com/uade-music-player/uade/-/raw/master/amigasrc/players/wanted_team/CustomMade/CustomMade_v1.asm
	if(X.Sz() < 3000) return false;
	if(X.c("6000.... 6000") || X.c("4EF9.... ....4EF9")
	  || X.c("4EB9.... ....4EF9")) {
		p = 8; var found = false;
		do {
			if(X.c("42280030 42280031 42280032",p)) found = true;
			p += 2
		} while(!found && p < 0x198);
	}
	if(!found) return false;
	else if(!X.isVerbose()) return true;
	//otherwise let's dig in for smp, synthsmp, and subsongs!
	p = q = d0 = d5 = 0; d7 = 0x800; org = 0; smpi = 0; songst = 0;
	bad = false;
	while(d7 && p < X.Sz()) {
		if((c=X.U32(p,_BE)) === 0xD04149FA) { /*lp @d366*/
			d0 = X.I16(p+4,_BE); q = p+d0+4;
			Table = q;
		}
		else if([0x48E7F8FC, 0x48E7FFFE].includes(c)) { /*play/ptntab @d37e; 2.0 @d38e*/
			d6 = a3;
			if(!d6 || org) d6 = 20;  else { q = p-a3; /*play-oldplay*/ d5 -= q; /*nowadr-dx*/ org = d5 }
			while(d6-- >= 0) { /* 2.lp */
				t = X.U16(p,_BE); p += 2; if(t === 0x41FA) { d0 = X.I16(p,_BE)/*2.2: @d3ae*/; songst = q = p+d0; break }
			}
		}
		else /*3 @d3bc*/ if(c === 0xE94847F0) /*ptoff*/ { d1 = X.I16(p+4,_BE) }
		else /*4 @d3cc*/ if(c === 0x00BFD500) { /*oldplay*/
			tmrval = (X.U8(p-1) << 8) + X.U8(p+7);
//_log("tmrval:"+Hex(tmrval));
			//tmrval = d0;
			if(!X.c("4E71",p+20)) {
				if(X.c("21FC",p+28)) a3 = X.U32(p+30,_BE);
				else if(X.c("C000",p+32)) a3 = X.U32(p+32,_BE);
				else a3 = X.U32(p+22,_BE);
			}
		}
		else /*6 @d410*/ if(c === 0x42A8001C) /*smpinfo*/ { d0 = X.I16(p+6,_BE); smpi = q = p+d0+6; }
		else if([0xE44843FA, 0xE448207B].includes(c)) { /*oldvoc1 7.1 @d438*/
			if(a4) {
				d0 = X.I16(p+4,_BE); // @d43c
				a4 -= X.I32(p+d0+4,_BE); //voc0-oldvoc0
				if(!org) { d5 -= a4; org = d5 }
			}
		}
		else /*8 @d44e*/ if(c === 0x48E700F0) /*voc0*/ { d0 = X.I16(p+6,_BE); /* @d456*/; a4 = p+d0+6; }

		/*lp_c @d45e*/
		p += 2; if(c === 0x1AC01940) break; /* -> @ex, because p is unused there */
		d7--;
	}
	songst += d1; // @ex @d46c
	for(x=0,p=songst+1; p < X.Sz(); x++,p+=16) /*Find*/ if(X.c("DFF0A0",p)) break;
	if(p > X.Sz()) { bad = true; return true }

	smp = synsmp = 0;
	for(p=smpi; !X.U32(p+28,_BE); p+=0x20) {
		a2 = X.U32(p,_BE)-org; a24 = X.U16(a2+4,_BE);
		if(a24 === 2 || a24 === 16) smp++;  else synsmp++;
	}
	return true
}

if(!bDetected && isCustomMade()) {
	sName = "Ivo Zoer & Ron Klaren's CustomMade module (.CM)"; bDetected = 1;
	if(X.isVerbose()) {
		if(x > 1) sOption(x,"×");
		//sOption("Origin:"+Hex(org)+" SongsTab:"+Hex(songst)+" SamplesInfo:"+Hex(smpi)+" Table:"+Hex(Table))
		sOption("smp:"+smp+" syn:"+synsmp+" timer:"+tmrval)
	}
}


function isDaveLoweNew() {
	//from https://gitlab.com/uade-music-player/uade/-/raw/master/amigasrc/players/wanted_team/DaveLoweNew/src/Dave Lowe New.s
	// ref Dave Lowe New_v2.asm
	a1 = 0; d1 = X.I16(0,_BE);
	if(d1 === 4) { if(!X.U32(0x18,_BE))  a1 += 4 }
	else if(d1 === 8)  a1 += 4;
	else return false;
	a1 += 4; a2 = a1;
	for(d2=0; d2 < 4; d2++) { //FirstCheck
		if(X.I16(a1,_BE)) return false; a1 += 2;
		d1 = X.I16(a1,_BE); a1 += 2; if(d1 <= 0 || (d1&1)) return false;
	}
	for(d0=0; d0 < 4; d0++) { //SecondCheck
		a1 = d1 = X.I32(a2,_BE); a2 += 4;
		d2 = X.I32(a1,_BE);
		if(X.I16(a1,_BE)) return false; a1 += 2;
		d1 = X.I16(a1,_BE); a1 += 2;
		if(d1 <= 0 || (d1&1)) return false;
		a1 = d2;
		t = X.I16(a1,_BE);
		if(t === 0x30) { a1 += 2; t = X.I16(a1,_BE); }
		if(t === 12) {
			a1 += 6;
			t = X.I16(a1,_BE); a1 += 2;
			if(t === 4) break;
		}
	}
	d2 = X.I32(a1,_BE); if(X.I16(d2,_BE) != 1) return false;
	a1 = a2 = d2 >> 16; if(a1) return false;
	fmt = 1;
	if(!X.U32(0x18,_BE)) {
		fmt = 0;
		x = (X.I16(2,_BE)-8) >> 5
	}
	else {
		x = 0; p = X.U16(0,_BE);
		while(p < X.Sz()) {
			t = X.I16(p,_BE); p += 2; if(t) break;
			t = X.I16(p,_BE); p += 2; if(!t || (t&1)) break;
			x++
		}
		x >>= 2
	}
	bad = ''; sz = 0; a1 = d2;
	while(a1 < X.Sz()) {
		a2 = a1; a1 -= 14;
		if(X.I16(a1,_BE) != 1) break
	}
	//smpi = a2;
	t = X.I16(a2-2,_BE);
	if(t != 8 && t != 0) a2 += 14;
	sz = 0; smp = 0;
	while(a2 < X.Sz()) { //CheckInfo
		t = X.I16(a2,_BE); if(t != 1) break; else smp++;
		a2 += 2;
		d2 = X.I32(a2,_BE); a2 += 6; t = X.I32(a2,_BE);
		if(t > d2) d2 = t;
		if(d2 > sz) sz = d2;
		a2 += 6
	}
	if(sz > X.Sz()) bad = bad.addIfNone("!short");
	if(a2 >= X.Sz()) { bad = bad.addIfNone("!badsmpinfo"); return true }
	sz += 0xFE;
	return true
}
if(!bDetected && X.isDeepScan() && isDaveLoweNew()) {
	sName = 'Dave "Uncle Art" Lowe New module (.DLN)'; bDetected = 1;
	sVersion = "f."+fmt; if(bad != "") sVersion += "/malformed"+bad;
	if(X.isVerbose()) {
		if(x > 1) sOption(x,"×");
		sOption("smp:"+smp+" sz:"+outSz(sz));
	}
}


function isDesire() {
	//from https://gitlab.com/uade-music-player/uade/-/raw/master/amigasrc/players/wanted_team/Desire/src/Desire_v1.asm
	if(X.Sz() <= 2500) return false;
	a2 = 0; a0 = 8;
	for(d1=0; d1 < 4; d1++) { if(!X.c("00010101",a0)) return false; a0 += 16 }
	a1 = 400+a0;
	do {
		if(a0 === a1) return false;
		t = X.I16(a0,_BE); a0 += 2;
	} while(t != 0x49FA);
	a0 += 2;
	if(!X.c("45F900DF F000357C 00FF009E 41FA",a0)) return false; a0 += 14;
	a0 += X.I16(a0,_BE);
	if(a0 != a2) return false;
	a1 = 0xF0;
	do { t = X.U16(a1,_BE); a1 += 2
	} while(t != 0x49FA && a1 < X.Sz()); a1 -=2; //FindPlay
	do { t = X.U16(a1,_BE); a1 += 2 //FindSongs
	} while(t != 0x47FA && a1 < X.Sz());
	a2 = a1+20; a1 += X.I16(a1,_BE); x = 0; a1++;
	do { //CheckSongs
		d1 = X.U8(a1); a1++; if(d1) x++; else break;
	} while(d1 != X.U8(a1) && a1 < X.Sz());
	do { t = X.U16(a2,_BE); a2 += 2 //Find1
	} while(t != 0xE341 && a2 < X.Sz());
	do { t = X.U16(a2,_BE); a2 += 2 //Find2
	} while(t != 0x47FA && a2 < X.Sz());
	smptsz = a1 = a2+X.I16(a2,_BE);
	do { t = X.U16(a2,_BE); a2 += 2 //Find3
	} while(t != 0x47FA && a2 < X.Sz());
	a3 = a2; a2 += X.I16(a2,_BE); smpp = a2;
	a0 = a2; d4 = a1-a2; a2 = a1+d4; smp = 0;
	do { t = X.I16(a1,_BE); a1 += 2; if(t) smp++; } while(a1 < a2); //NextS,NoSamp
	do { t = X.U16(a3,_BE); a3 += 2 //Find4
	} while(t != 0x47FA && a3 < X.Sz());
	a3 += 2;
	d6 = X.U16(a3,_BE) & 0x0E00;
	if(d6) d6 >>= 9; else d6 = 8;
	ruch = d6; //Skip8
	d1 = X.I16(a0,_BE); songsz = d2 = d1 << d6;
	t= d1;d1=d2;d2 =t; d5 = 0;
	do { //NextOff
		d0 = X.I16(a0,_BE); a0 += 2;
		if(d0 > d2) { d2 = d0; d5 = X.U16(a0+d4-2,_BE) }
	} while(a0 < smptsz);
	d2 <<= d6; d5 <<= 1; d2 += d5;
	sz = d2; smpsz = sz-songsz
	return true
}
if(!bDetected && X.isDeepScan() && isDesire()) {
	sName = "Desire player module (.DSR)"; bDetected = 1;
	sVersion = "v1.0";
	if(X.isVerbose()) {
		if(x > 1) sOption(x,"×");
		sOption("smp:"+smp+" songsz:"+Hex(songsz)+" smpsz:"+Hex(smpsz)+" sz:"+outSz(sz))
		if(sz < X.Sz()) sVersion += "/malformed!short"
	}
}


function isDavidWhittaker() {
	// from https://github.com/neumatho/NostalgicPlayer/blob/main/Source/Agents/Players/DavidWhittaker/DavidWhittakerWorker.cs
	// <space>sfx.dw not detected, -sfx.dw may be detected sometimes
	if(X.Sz() < 2048) return;
	const sl = Math.min(X.Sz(), 0x4000); //search buffer length
	//and now for the NP routine ExtractInfoFromInitFunction
	sz = x = ch = smp = sqwfsmp = i = 0;
	for(; i < sl; i += 2) if(X.U16(i,_BE) == 0x47FA && (X.U8(i+2) & 0xF0) == 0xF0) break;
	if(i >= sl-6) return;
	sofs = i+2+X.I16(i+2,_BE);
	for(; i < sl && i < X.Sz(); i += 2) if(X.U16(i,_BE) ==0x6100) break; if(i >= sl-6) return;
	sinit = i; ssmpsinit = i;
//_log("ssmpsinit:"+Hex(ssmpsinit))
	if(X.U16(i+4,_BE) == 0x6100) ssmpsinit = i+6+X.I16(i+6,_BE);
	for(i = ssmpsinit; i < sl; i += 2) if(X.U16(i,_BE) == 0x4A2B) break;
	if(i >= sl-36) return;
	if(X.U8(i+4) != 0x66) {
		for(i = sinit; i < sl; i += 2) if(X.U16(i,_BE) == 0x41EB) break;
		if(i >= sl-36) return;
		smpdp = sofs+X.I16(i+2,_BE);
		i += 4; if(X.U8(i+4) != 0x72) return;
		smp = X.U8(i+5)+1;
		for(; i < sl-4; i += 2) if(X.c("41EB....E34F",i)) break;
		if(i >= sl-4) return false;
		chvolp = sofs+X.I16(i+2,_BE);
//_log("@"+Hex(i)+" smpdatap:"+Hex(smpdp))
		//extract subsong info:
		for(i = sinit; i < sl; i += 2) if(X.c("41EB....17",i)) break;
		if(i >= sl-4) return;
		sstp = sofs+X.I16(i+2,_BE);
		is32bp = true; old = true;
	}
	else {
		old = false;
		if(!X.U8(i+5)) i += 2;
		if(X.U16(i+6,_BE) != 0x41FA) return;
		smpdp = X.I16(i+8,_BE)+i+8;
		i += 10; if(X.c("2748....D0FC",i)) {
			smpdp += X.U16(i+6,_BE);
			i += 12; if(X.U16(i,_BE) != 0xD0FC) return;
			smpdp += X.U16(i+2,_BE); i += 4
		}
//_log("@"+Hex(i)+" smpdatap:"+Hex(smpdp))
		if(!X.c("4BFA....72",i)) return false;
		smpip = X.I16(i+2,_BE)+i+2;
		smp = X.U8(i+5)+1;
		for(i = sinit; i < sl-4; i += 2) if(X.U16(i,_BE) == 0x41FA && X.U8(i+4) != 0x4B) break;
		if(i >= sl-4) return;
		if(X.U16(i+4,_BE) != 0x1230 && X.U16(i+4,_BE) != 0x3770) return;
		sstp = i+2+X.I16(i+2,_BE);
		i += 4; for(; i < sl-8; i += 2) if(X.U16(i,_BE) == 0x41FA && X.U8(i+4) != 0x23) break;
		if(i >= sl-8) return;
		if(X.U16(i+4,_BE) == 0x2070) is32bp = true;
		else if(X.U16(i+4,_BE) == 0x3070) is32bp = false;
		else return;
	}
//_log("sstp:"+Hex(sstp))
	//load sample info, get size:
	p = smpdp;
	for(i = 0; i < smp; i++) {
		ssz = X.U32(p,_BE); p += 6;
//_log("smpd["+i+"] @"+Hex(p-6)+" sz:"+outSz(ssz));
		if(p+ssz > X.Sz()) { bad = bad.addIfNone("!short"); return true }
		p += ssz; sz = p;
	}
	for(i = 0; i < sl; i += 2) if(X.U16(i,_BE) == 0x47FA) {
		if(i >= sl-10) return;
		if (X.c("4A2B....67",i+4)) {
			if([0x33FC, 0x177C, 0x08B9].includes(X.U16(i+10,_BE))) continue;
			break
		}
	}
	splay = i;
//_log("splay:"+Hex(i))
	ch = 0;
	for(i = splay; i < splay + 200; i += 2) if(X.U8(i) == 0x7E) {
		ch = X.U8(i+1);
		if(!ch) {
			for(; i < splay + 500; i += 2)
				if([0xBE7C, 0xBE3C].includes(X.U16(i,_BE))) { ch = X.U8(i+3); break }
		} else ch++;
		break
	}
	if(!ch) return;
//_log("ch:"+ch)
	//square waveforms?
	for(i = splay; i < splay+100; i += 2)
		if(X.c("207A.... 303A",i)) {
			sqwfsmp = (X.I16(i+2,_BE)+i+2 - smpip)/12;
			if(![0x31BC, 0x11BC].includes(X.U16(i+14,_BE))) return;
			if((X.U8(i+20) & 0xF0) != 0x50 || X.U8(i+21) != 0x6B) return;
			if(X.U16(i+24,_BE) != 0x0C6B) return false;
			if(![0x31BC, 0x11BC].includes(X.U16(i+38,_BE))) return;
			if(X.U16(i+48,_BE) != 0x0C6B) return;
		}
	//find diff.player parts, just the sanity checks
	if(old) {
		for(i = splay; i < sl; i += 2) if(X.U16(i,_BE) == 0x7000) break;
		if(X.U8(i+2) != 0x10) return;
		var rdtrkcmdp = i
	} else {
		for(i = splay; i < sl; i += 2) if(X.U16(i,_BE) == 0x5368) break;
		if(i >= sl-16 || X.U8(i+4) != 0x67) return;
		var rdtrkcmdp = X.U8(i+5)+i+6;
		if(X.U8(i+12) != 0x66) return;
		for(i = rdtrkcmdp; i < sl; i += 2) if(X.c("45FA.... 322D",i)) break;
		if( i >= sl-6) return;
		i = X.U16(i+2,_BE)+i+2;
		if(i >= sl-72*2) return;
		if(![0x1000, 0x2000].includes(X.U16(i,_BE))) return;
	}
	//checks for transposes:
	for(i = rdtrkcmdp; i < sl; i += 2) if(X.U16(i,_BE) == 0x6B00) break;  if(i >= sl-6) return;
	var endlyc = endlym = false; //:667
	for(i = splay; i < splay + 100; i += 2) if(X.U16(i,_BE) == 0x103A) {
		endlyc = true; if(X.U16(i+6,_BE) == 0xC0FC) endlym = true; break }
//_log("@"+Hex(i)+" dlyc?"+endlyc+" dlym?"+endlym)
	x = 0; p = sstp; minpp = 0xFFFFFFFF; //:966
	sngspd = dlyspd = 0; bad = "";
	while(p+8 < minpp) {
		if(endlyc) { sngspd = X.U8(p++); dlyspd = X.U8(p++) }
		else { sngspd = X.U16(p,_BE); p += 2; dlyspd = 0 }
		if(sngspd > 0xFF) break;
		for(i = 0; i < ch; i++) {
			if(is32bp) { t = X.U32(p,_BE); p += 4 }
			else { t = X.U16(p,_BE); p += 2 }
			if(minpp > sofs+t) minpp = sofs+t;
//_log(x+" ordp:"+Hex(t))
		}
		if(p > X.Sz()) { bad = bad.addIfNone("!short"); break }
		x++
	}
	return true
}
if(!bDetected && X.isDeepScan() && isDavidWhittaker()) {
	sName = "David Whittaker's module (.DW)";
	if(old) sVersion = "old"; else sVersion = "new"; bDetected = 1;
	if(bad != "") sVersion += "/malformed"+bad;
	if(X.isVerbose()) {
		if(x > 1) sOption(x,"×");
		sOption("ch:"+ch+" smp:"+smp+(sqwfsmp ? " sqwf.smp:"+sqwfsmp : "")
			+" spd:"+sngspd+" sz:"+outSz(sz))
	}
}


function isFashionTracker() {
	//from https://gitlab.com/uade-music-player/uade/-/raw/master/amigasrc/players/wanted_team/FashionTracker-v1.0/FashionTracker.asm
	if(!X.c("13FC0040 ........ 4E710439 0001") || !X.c("66F44E75 48E7FFFE",18)) return;
	a2 = 0; bad = "";
	while(X.U16(a2,_BE) != 0x2379 && a2 <= 1000) a2 += 2; //FindOri
	if(a2 > 1000) //corrupt module
		if(X.isHeuristicScan()) bad = "!badorigin"; else return;
	org = X.U32(a2-4,_BE);
	a2 = 0; d1 = 1;
	do { //GetValues:
		// a3 = smplen    a3+4 = smpp      a3+8 = smplp
		// a3+12 = smpn+vol      a3+16 = ptn     a3+20 = songpositions
		if(X.U16(a2,_BE) == 0x23D1) { // L???
			smplen = X.I32(a2+8,_BE)-org;
			smpp = X.I32(a2-6,_BE)-org;
			smplp = X.I32(a2+24,_BE)-org;
			smpnvol = X.I32(a2-30,_BE)-org;
			d1 -= 6;
		}
		else if(X.U32(a2,_BE) == 0xC0FC0400) { //NoL
			ptn = X.I32(a2+6,_BE)-org;
			d1 += 3;
		}
		else if(X.U32(a2,_BE) == 0x0C790400) { //NoPa
			songpos = X.I32(a2+12,_BE)-org;
			len = X.I32(a2+34,_BE);
			d1 += 2;
		}
		a2 += 2 //NextW
	} while(a2 < 1000);
	if(d1)
		if(X.isHeuristicScan()) bad = bad.addIfNone("!badval"); else return;
	a1 = smpp; songsz = X.I32(smpp,_BE)-org;
	d0 = X.I32(a1-4,_BE) << 1;
	a2 = smplen; d1 = a1-a2; a1 += d1; d1 >>= 2; smp = d1;
	d1 = X.I32(a1-4,_BE)-org;
	sz = d0+d1; smpsz = sz-songsz;
	a1 = songpos; a2 = a1+len; ptn = 0;
	do { t = X.U8(a1); if(ptn < t) ptn = t; a1++ } while(a1 < a2); //GetPat
	ptn++;
	d1 = (d0-1)*6; d0 = d1; d1 *= 0x376B;
	dur = Math.floor(d1*64/(709379-3));
	return true
}
if(!bDetected && X.isDeepScan() && isFashionTracker()) {
	sName = "Fashion Tracker module (.EX)"; bDetected = 1;
	sVersion = "v1.0"; if(bad) sVersion += "/malformed."+bad;
	if(X.isVerbose()) {
		sOption("ord:"+len+" ptn:"+ptn+" smp:"+smp+" songsz:"+songsz+" sz:"+outSz(sz))
	}
}


function isSMUS() {
	//from https://gitlab.com/uade-music-player/uade/-/raw/master/amigasrc/players/wanted_team/SonixMusicDriver/Sonix Music Driver_v1.asm
	a0 = a1 = 0; title = inst = ''; format = "Aegis Sonix Music Driver";
	if(X.c("'FORM'")) {
		if(!X.c("'SMUSSHDR'",8) || !X.U8(0x17) || !X.c("'NAME'",0x18)) return;
		d1 = X.U32(0x1C,_BE); if(d1 >> 31) return;  t_ = d1;
		d1 = (d1+1) & 0xFFFFFFFE; a1 = 0x20+d1;
		if(X.c("'SNX1'",a1)) {
			a1 += 4;
			d1 = X.U32(a1,_BE); if(d1 >> 31) return;  a1 += 4;
			d1 = (d1+1) & 0xFFFFFFFE; a1 += d1;
		} else format = "Electronic Arts' Simple Musical Score";
		insinfp = a1;
		realsmp = []; ins = 0;
		do { //MoreIns
			if(!X.c("'INS1'",a1)) return; a1 += 4;
			d1 = X.U32(a1,_BE); if(d1 >> 31) return;  a1 += 4;
			d1 = (d1+1) & 0xFFFFFFFE; if((d1 >> 24) > 0x3F) return;
			if(X.U8(a1+1)) return;
			realsmp.push(X.U8(a1));
			a1 += d1; ins++
		} while(!X.c("'TRAK'",a1));
		sz = a1; trk = 0; while(sz < X.Sz()) {
			hkhd = X.SA(sz,4); hksz = X.U32(sz+4,_BE);
			if(hkhd != "TRAK") break; trk++; sz += 8+hksz }
//_log("trk:"+trk+" sz:"+outSz(sz))
		title = X.SA(0x20,t_);
		fmt = 2; ext = "smus"
	}
	else {
		if(!(X.U16(0,_BE) & 0xF0)) {
			d3 = 20; d1 = 4;
			while(d1) {
				d2 = X.I32(a0,_BE); if(d2 <= 0 || (d2&1)) return;
				a0 += 4; d3 += d2; d1--
			}
			if(d3 >= X.Sz()) return;
			a0 += 4; d1 = 4;
			while(d1) {
				t = X.U16(a0,_BE); if(!(t & 0x8000)) return;
				if(t != 0xFFFF) if((t >> 16) > 0x84) return;
				a0 += X.I32(a1,_BE); a1 += 4; d1--
			}
			if(!X.U8(a0)) return;
			sz = X.fSig(a0,0x200,"0000");
			if(sz > 0) {
				while(a0 < sz) {
					inst = inst.appendS(X.SA(a0,t=X.fSig(a0,0x200,"00")-a0),'; '); a0 += t+1; }; sz += 2; }
			fmt = 0; ext = "snx"
		}
		else { //TinyCheck
			if(X.Sz() < 333) return;
			a1 = 0x30; if(X.I32(a1,_BE) != 0x140) return;
			a1 += 4; d1 = 3;
			while(d1) { //NextPos2
				d2 = X.I32(a1,_BE); a1 += 4;
				if(d2 <= 0 || (d2&1) || d2 > X.Sz()) return;
				if(X.I16(d2,_BE) != -1) {
					if(X.I32(d2,_BE) || X.I16(d2+4,_BE)) return;
					t = X.U8(d2+6); if(!isWithin(t,0x80,0x82)) return;
				}
				d1--
			} sz = X.fSig(d2,0x200,"FFFF"); if(sz > 0) sz += 2;
			fmt = 1; ext = "tiny"
		}
	}
	return true
}
if(!bDetected && isSMUS()) {
	sName = format+" module (."+ext+")"; sVersion = "f."+fmt; bDetected = 1;
	if(X.isVerbose()) {
		if(title.length) sOption(title);
		if(inst.length) sOption(inst,'+instr.:"','"');
		sOption('sz:'+outSz(sz))
	}
}


function isAMBK() {
	//from https://gitlab.com/uade-music-player/uade/-/raw/master/amigasrc/players/ambk/AMOS.s
	if(X.c("'AmBk'")) a0 = p = 4; else if(X.c("C0")) a0 = p = 0; else return false;
	//if(!X.c("80",p+4)) return false;
//_log('AmBk: '+Hex(X.U32(p+4,_BE)&((1<<28)-1)));
	if(!X.c("'Music   '",p+8)) return false;
	p += 0x14; p += X.I32(p,_BE);
	if(X.I16(p,_BE) != 6) return false; p += 2; r = p;
	p += X.I16(p+2,_BE)-2;
	p = X.I16(p,_BE); if(p != -2) if(p) return false;

	title = X.SA(r+0x0C,0x20);
	p = r+0x1E+X.I16(r+0x1E,_BE); ord = -1;
	do { ord++; d1 = X.I16(p,_BE); p += 2; } while(p < X.Sz() && d1 >= 0);
	if(a0) d3 = 4+(X.I32(a0+4,_BE) & 0xFFFFFF);
	else d3 = X.I32(a0,_BE) & 0xFFFFFF; sz = d3+8;
	bad = ""; if(sz > X.Sz()) bad = bad.addIfNone("!short");
	p = r+X.I16(r+6,_BE);
	ptn = 0;
	while(p < X.Sz()) { //.such
		d1 = X.I16(p,_BE); p += 2; if(d1 < 0) break;
		if(d1 > ptn) ptn = d1
	}
	ptn++;
	if(!X.I16(p,_BE)) p += 2;
	if(X.I16(p,_BE) != ptn) bad = bad.addIfNone("!badptn");
	q = a0+0x20; smp = d1 = X.I16(q,_BE); q += 0x10;
	d7 = 0;
	while(d1 && q < X.Sz()) {
		d2 = X.I16(q,_BE);
		if(!d2) d2 = X.I16(q-6,_BE);
		d7 += d2 << 1; q += 0x20; d1--
	}
	smpsz = d7;
	songsz = sz-d7;
	return true
}
if(!bDetected && X.isDeepScan() && isAMBK()) {
	sName = "François Lionet's AMOS Music Bank module (.ABK)"; bDetected = 1;
	if(bad != "") sVersion = "malformed"+bad;
	if(X.isVerbose()) {
		sOption(title);
		sOption("ord:"+ord+" ptn:"+ptn+" smp:"+smp+/*" smpsz:"+Hex(smpsz)+" songsz:"+Hex(songsz)+*/" sz:"+outSz(sz))
	}
}


function isTuneFish4() {
	// taken from tunefish src\tunefish4player\tf4player.cpp
	ins = X.I16(0,_LE); if(ins > 32) return false;
	if(!X.c("'INST'",4+ins*2)) return false; t = 8+ins*114;
	if(!X.c("'SONG'",t)) return false;
	t += 4; songsz = 0; for(i=0; i<ins; i++) songsz += X.U16(4+i*2,_LE);
	if(!X.c("'ENDS'",t+songsz*4)) return false;
	sz = t+songsz*4+4;
	return true;
}
if(!bDetected && X.isDeepScan() && isTuneFish4()) {
	sName = "TuneFish module (.TF4)"; sVersion  = "v4"; bDetected = 1;
	if(X.isVerbose())
		sOption("ins:"+ins+" tempo:"+X.U16(2,_LE)+" songsz:"+Hex(songsz)+" sz:"+outSz(sz))
}


function isJH() {
	//from https://gitlab.com/uade-music-player/uade/-/raw/master/amigasrc/players/wanted_team/Jochen_Hippel_ST/Jochen Hippel_v1.asm
	a0 = 0; tfmx = 0;
	if(X.c("'MCMD'")) { fmt = -1; tfmx = a0 }
	else if(X.c("48E7FFFE")) {
		fmt = -1; a0 += 4; d1 = 0;
		if(!X.c("61",a0)) return false;
		d1 = X.U8(a0+1); if(!d1 || (d1&1)) return false; a0 += d1+2;
		if(!X.c("2F006100",a0)) return false; a0 += 4+X.I16(a0+4,_BE);
		if(!X.c("41FA",a0)) return false;
		if(!X.c("41FA",a0+0x12)) return false;
		a0 += 0x14; a0 += X.I16(a0,_BE);
		if(!X.c("'MCMD'",a0)) return false;
		tfmx = a0
	} else { // NoMCMD
		fmt = 0;
		if(!X.c("60",a0++)) return false;
		d1 = X.U8(a0++);
		if(!d1) {
			fmt = 1; d1 = X.I16(a0,_BE); if(d1 < 0 || (d1&1)) return false;
			if(!X.c("6000",a0+2)) return false;
			a0 += d1;
			if(!X.c("48E7FFFE",a0)) return false; a0 += 4;
		} else { // No3
			if(d1&1) return false; a0 += d1;
			if(!X.c("48E7FFFE",a0)) return false; a0 += 4;
			if(!X.c("6100",a0)) return false; a0 += 2; //astaroth.hip, prince.hip fail this one and have more jumps, iono
			a0 += X.I16(a0,_BE);
			if(!X.c("2F006100",a0)) return false; a0 += 4;
			a0 += X.I16(a0,_BE);
			if(!X.c("41FA",a0)) return false; a0 += 0x14;
		} // Later:
		t = X.c("41FA",a0); a0 += 2;
		if(!t) { t = X.c("41FA",a0); a0 += 2; if(!t) return false }
		a0 += X.I16(a0,_BE);
		tfmx = a0;
		if(!X.c("'TFMX'00",a0)) return false; a0 += 4;
		if(!X.I16(a0+0x0C,_BE)) { fmt = "sfx"; sz = 0; x = 1; return true }
		d1 = (2+X.I16(a0,_BE)+X.I16(a0+2,_BE)) << 6;
		d2 = (1+X.I16(a0+4,_BE))*X.I16(a0+8,_BE);
		d3 = (1+X.I16(a0+6,_BE))*12;
		d1 += d2+d3+(1+X.I16(a0+0x0C,_BE))*6+0x20;
		a0 += d1+0x0E;
		if(X.I32(a0,_BE)) return false; a0 += 4;
		d2 = X.I16(a0,_BE); if(!d2) return false;
		d2 *= 2; if(d2 != X.I32(a0+0x1A,_BE)) return false;
	}
	sz = a0 = 0; x = 1; a1 = a0+tfmx;
	if(fmt < 0) { d0 = 0; d1 = 0x12 } else { d0 = 2; d1 = 0x20 }
	d0 += X.I16(a1+4,_BE)+X.I16(a1+6,_BE); d0 <<= 6;
	d0 += d1; if(fmt < 0) d1 = 0; else d1 = 1;  d1 += X.I16(a1+8,_BE); ptn = d1;
	d1 *= X.I16(a1+0x0C,_BE); d0 += d1;  if(fmt < 0) d1 = 0; else d1 = 1;
	d1 += X.I16(a1+0x0A,_BE); d1 *= 12; d0 += d1;  a2 = subp = a1+d0; d1 = 1;
	if(fmt < 0) { d2 = X.I16(a1+0x0E,_BE); d1 = d2<<3 }
	else { d2 = X.I16(a1+0x10,_BE); d1 = (d1+d2)*6 }
	x = d2; d0 += d1; a2 = smpinfop = a1+d0;
	if(fmt < 0) { d1 = X.I16(a1+0x10,_BE); d2 = 0x1C }
	else { d1 = X.I16(a1+0x12,_BE); d2 = 0x1E }
	smp = d1; d1 *= d2; d0 += d1;
	smpp = a2 = a1+d0; a1 = smpp; if(fmt < 0) a1 += 2;
	smpsz = X.I16(a1-8,_BE)*2+X.I32(a1-0x0C,_BE);
	songsz = a2-a0; sz = songsz+smpsz;
	return true
}
if(!bDetected && X.isDeepScan() && isJH()) {
	if(fmt < 0) ext = ".MCMD"; else ext = ".SOG";
	sName = "Jochen 'Mad Max' Hippel's module ("+ext+",.HIP)"; sVersion = "f."+fmt; bDetected = 1;
	if(X.Sz() < sz) sVersion += "!short";
	if(X.isVerbose()) {
		if(x>1) sOption(x,"×");
		sOption("ptn:"+ptn+" smp:"+smp+/*" songsz:"+Hex(songsz)+" smpsz:"+Hex(smpsz)+*/" sz:"+outSz(sz))
	}
}


function isJH7V() {
	//from https://gitlab.com/uade-music-player/uade/-/raw/master/amigasrc/players/wanted_team/Jochen_Hippel_7V/Jochen Hippel 7V_v2.asm
	p = 0;
	if(X.c("6000")) { p = 2;
		d1 = X.I16(p,_BE); if(d1 <= 0 || (d1&1)) return false; p = d1+2;
		p = 4+X.fSig(p,20,"308141FA"); if(p < 4) return false;
		d1 = X.I16(p,_BE); if(d1 <= 0 || (d1&1)) return false;
		p += d1;
	}
	if(!X.c("'TFMX'00",p)) return false;
	d0 = d1 = ( 2+X.I16(p+4,_BE) + X.I16(p+6,_BE) ) << 6;
	d0 += 0x20;
	ptn = d2 = X.I16(p+8,_BE)+1;
	d3 = (X.I16(p+0x0A,_BE)+1)*0x1C;
	d2 *= X.I16(p+0x0C,_BE);
	d0 += d2;
	d1 += d2 + d3;
	if(X.U8(p+4+d0)) txt = X.SA(p+4+d0,24);
	else txt = "";
	x = X.I16(p+0x10,_BE);
	d2 = (x+1) << 3;
	ins = X.I16(p+0x12,_BE);
	insip = p+d1+d2+0x20;
	if(X.I32(insip+0x12,_BE)) return false; //1st smp ofs
	d2 = 2*X.I16(insip+0x16,_BE);
	if(!d2) return false;
	if(d2 != X.I32(insip+0x16+0x1E-4,_BE)) return false;
	var mofs = 0; var msz = 0; //find max ofs instead of one just from last smp, just in case
	for(i=0;i < ins;i++) {
		var sofs = X.I32(insip+i*0x1E+0x12,_BE);
		var ssz = X.U16(insip+i*0x1E+0x16,_BE);
		if(sofs >= mofs) { mofs = sofs; msz = ssz } }
	smpp = insip+ins*0x1E;
	sz = smpp+mofs+msz*2;
//_log("insip:"+Hex(insip)+" smpp:"+Hex(smpp)+" sz:"+outSz(sz));
	return true;
}
if(!bDetected && X.isDeepScan() && isJH7V()) {
	sName = "Jochen 'Mad Max' Hippel's module (.HIP7,.S7G)"; sVersion = "7V"; bDetected = 1;
	if(!X.c("'TFMX'")) sVersion += "+replayer";
	if(X.Sz() < sz) sVersion += "!short";
	if(X.isVerbose()) {
		if(txt != "") sOptionT(txt);
		if(x>1) sOption(x,"×");
		sOption("ptn:"+ptn+" ins:"+ins+" sz:"+outSz(sz))
	}
}


function isJHCoSo() {
	//from uade Jochen_Hippel_COSO player, check routine @ e476t
	//initplayer disassembly: d e5c0 500
	if(!X.c("'COSO'")) return false;
	if(!X.c("'TFMX'",0x20)) return false;
	if(!X.I16(0x30,_BE) || !X.I16(0x40,_BE)) return false;
	var songsz = X.I32(0x1C,_BE); if(songsz > X.Sz()) return false;
	//@e4ac:
	a2 = X.I32(4,_BE); d6 = d7 = 0;
	d0 = X.I16(0x24,_BE); //ord?
	do { if(a1 > X.Sz()) return false;
		a1 = X.I16(a2,_BE); a2 += 2;
		if(X.U8(a1) === 0xE2)
			if(X.U8(a1+1)&0x80) d7++; else d6++;
	d0--} while(d0 >= 0);
	if(d7 >= d6) return false;
	ptn = X.I16(0x28,_BE)+1; smp = X.I16(0x32,_BE);
	a0 = X.I32(0x18,_BE)+smp*10; //_log("a0:"+Hex(a0));
	smpsz = X.I32(a0-10,_BE) + X.I16(a0-6,_BE)*2;
	sz = songsz+smpsz;
	x = X.I16(0x30,_BE);
	return true
}
if(!bDetected && X.isDeepScan() && isJHCoSo()) {
	sName = "Jochen 'Mad Max' Hippel's module (.HIPC)"; sVersion = "packed"; bDetected = 1;
	if(X.isVerbose()) {
		if(x > 1) sOption(x,"×");
		sOption("ptn:"+ptn+" smp:"+smp+" sz:"+outSz(sz));
	}
}


function isJHST() {
	//from http://wt.exotica.org.uk/files/sources/SRC_JochenHippelST.lzx
	//Test routine @e3fc, init @e57e
	a0 = a1 = a4 = d4 = 0; d1 = 0x80; d0 = X.Sz(); bad = msg = "";
	//func @e84e, returns d0 = 2 if this:
	while(d1) {
		if(a0 > X.Sz() || a1 > X.Sz()) return false;
		if(d1 != 0x80) { //detect the files that start with MMME,TFMX,COSO
			t = X.I16(a1,_BE); a1 += 2;
			if(t === 0x41FA) { d2 = X.I16(a1,_BE); if(d2 >= 0 && !(d2&1)) a0 = a1+d2 }
		}
		if(X.c("'MMME'",a0)) { d0 = 2; break; }
		else if(X.c("'TFMX'",a0)) {
			if(X.I16(a0+4,_BE) >= 0x200) d0 = 0;
			else if(!X.I16(a0+0x10,_BE)) d0 = 0;
			else { // function @e8b0
				//func.@e8b0~e8c8
				d0 = X.I16(a0+4,_BE); a1 = a0+0x20; d6 = d7 = 0;
				do { if(X.U8(a1) === 0xE2) // in func.e928
						if(X.U8(a1+1)&0x80) d6++; else d7++;
					a1 += 0x40;
				d0--} while(d0 >= 0);
				if(d6 > d7) d0 = 2; else d0 = 5; //@e8a8
			}
			break;
		}
		else if(X.c("'COSO'",a0)) {
			if(!X.I16(a0+0x30)) return false; //FX check
			if(!X.I32(a0+0x18)) return false;
			if(X.c("'TFMX'",a0+0x20)) { //@e984
				a2 = a0+X.I32(a0+4,_BE); d6 = d7 = 0;
				d0 = X.I16(a0+0x24,_BE);
				do {
					if(X.I16(a0+0x40,_BE)) { a1 = a0+X.I16(a2,_BE); a2 += 2 }
					else { a1 = a0+X.I32(a2,_BE); a2 += 4 }
					if(a1 > X.Sz()) return false;
					if(X.U8(a1) === 0xE2) {
						if(X.U8(a1+1)&0x80) d7++; else d6++;
					}
				d0--} while(d0 >= 0);
				if(d7 < d6) d0 = 4; else d0 = 2
			}
			else if(X.c("'MMME'",a0+0x20)) d0 = 2; else d0 = 0;
			break;
		}
		d0 -= 2;
		d1--
	}
	if(d0 != 2) return false;
	if(X.c("'LSMP'",a0+0x1C)) lsmp = 1; else lsmp = 0;
	//a0 -= a4; if(a0 && debug)_log("pc+@e170 (mod ofs?): "+a0);

	//size calc
	d0 = X.Sz()-a0; d4 = a0; songsz = smpsz = 0; sz = sz1 = -1;
	if(!X.c("'COSO'",a0) && !X.c("'MMME'",a0+0x20)) { sV = "unpacked";
		d6 = a0; d1 = 2; a2 = a0;
		d1 = (2+X.I16(a0+4,_BE)+X.I16(a0+6,_BE)) << 6;
		d2 = (1+X.I16(a0+8,_BE)) * X.I16(a0+0xC,_BE);
		d3 = (1+X.I16(a0+0xA,_BE))*12; //e5f8
		d1 += d2+d3; d2 = 2+X.I16(a0+0x10,_BE);
		d1 += (d2+X.I16(a0+0x12,_BE))*6+0x20;
		d2 = 0x20; d3 = d7 = 0; //e612
		d0 -= d1;
		if(d0 < 0) /*@e814*/ { d0 = 0x1C; bad = bad.addIfNone("!short") }
		else if(d0 > 0) { //e618
			a1 = d1+a2;
			t = X.I16(a1,_BE);
			if(t === 0x80 || t === 0x100) {
				d7 = a1;
				do { d3 = X.I16(a1,_BE); a1 += 8;
				} while(X.I16(a1,_BE) && a1 < X.Sz()); //e63e
				d0 -= d3; if(d0 < 0)
					d0 = 0x1C;
			}
			else { t >>= 8; p = a1+1; msg = [];
				while(t && p < X.Sz()) { msg.push(t); t = X.U8(p++); }
				msg = decEncoding(msg,CPAmiga,1,Chars0to1FLF)
			}
		}
		d0 = d1; smpsz = d3;
		songsz = d1; sz = a0+songsz+smpsz; sz1 = (msg != "" ? sz+msg.length+1 : sz);
		x = X.U16(a0+0x10,_BE); if(x > 100) return false
	} else { sV = "packed"; a1 = a0; d3 = 0;
		d3 = (1+X.I16(a0+0x32,_BE))*6; d2 = d3+X.I32(a0+0x18,_BE);
		d3 = 0; d0 -= d2;
		if(d0 < 0) /*@e814*/ d0 = 0x1C;
		if(d0 > 0) {
			a1 += d2; t = X.I16(a1,_BE);
			if(t == 0x80 && t == 0x100) {
				do { d3 = X.I16(a1,_BE); a1 += 8;
				} while(X.I16(a1,_BE) && a1 < X.Sz());
			d0 -= d2;
			 if(d0 < 0)
				d0 = 0x1C
		} else { t >>= 8; p = a1+1; msg = [];
				while(t && p < X.Sz()) { msg.push(t); t = X.U8(p++); }
				msg = decEncoding(msg,CPAmiga,1,Chars0to1FLF)
			}
		}
		/*d2 += d4;*/ songsz = d2; smpsz = d3; sz = a0+d3+d2;
		if(msg.length < 3) msg = "";
		sz1 = (msg.length ? sz+msg.length+1 : sz);
		x = X.U16(a0+0x30,_BE); if(x > 100) return false;
	}
	return true;
}
if(!bDetected && isJHST()) {
	sName = "Jochen 'Mad Max' Hippel's Atari ST module (.HST,.SOC,.SOG)"; bDetected = 1
	sVersion = sV; if(bad != "") sVersion = sVersion.appendS("malformed"+bad,"/");
	if(X.isVerbose()) {
		sOptionT(msg,'msg:"','"');
		info = "";
		if(lsmp) info = info.appendS("ext.samples"," ")
		if(sz > 0) info = info.appendS("sz:"+outSz(sz, sz1)," ");
		if(smpsz) info = (/*"songsz:"+Hex(songsz)+" "+*/"smpsz:"+Hex(smpsz)).appendS(info," ");
		if(x > 1) info = ("×"+x).appendS(info," ");
		sOption(info)
	}
}


function isEMS() {
	//ref UADE's EMS player disassembly: run with -d then "f d3c2"
	if(X.Sz() <= 0x53A) return false; // fault is @d432
	if(X.U8(0) != 1) return false;
	if(X.U8(1) > 0x25) return false;
	t = X.I16(2,_BE); if(!t || t > 0x80) return false;
	d1 = X.U16(4,_BE); if(d1 > 0x3F) return false; d = d1 & 0xFF;
	p = 6;
	while(p < 0x86) { t = X.U8(p); p++; if(d < t) return false; }
	c = 0;
	for(p = 0x13A; p < 0x53A; p += 4) { //@d40c
		t = X.I16(p,_BE); if(t < 0) return false;
		if(t)
			if([
			  0x6B0, 0x650, 0x5F4, 0x5A0, 0x54C, 0x500, 0x4B8, 0x474,
			  0x434, 0x3F8, 0x3C0, 0x38A, 0x358, 0x328, 0x2FA, 0x2D0,
			  0x2A6, 0x280, 0x25C, 0x23A, 0x21A, 0x1FC, 0x1E0, 0x1C5,
			  0x1AC, 0x194, 0x17D, 0x168, 0x153, 0x140, 0x12E, 0x11D,
			  0x10D, 0xFE, 0xF0, 0xE2, 0xD6, 0xCA, 0xBE, 0xB4,
			  0xAA, 0xA0, 0x97, 0x8F, 0x97, 0x7F, 0x78, 0x71
			  ].indexOf(t) < 0)
				return false;
			else c++;
	}
	if(!c) return false;
	return true
}
if(!bDetected && X.isDeepScan() && isEMS()) {
	sName = "Editeur Musical Sequentiel module (.EMS)"; bDetected = 1;
}


function isDZ() {
	//from https://zakalwe.fi/uade/uade3/uade-3.03.tar.bz2 / uade/amigasrc/playes/dz/DariusZendeh_mod.s
	if(X.Sz() < 0x500 || !X.c("48E7..F0 41FA.... 4CD80600")) return false;
	if(X.c("78",2) && X.c("0C0000FF",0xC)) sversion = "strange";
	else
		if(!X.c("00",2) || !X.fSig(0xC,0x240,"700033FC 000F00DF F09641FA"))
			return false;
	soptions = "";
	if(X.c("4A44",0xC)) {
		sversion = "type 1";
		if(X.isVerbose()) {
			p = X.fSig(0,0x80,"0C04.... 66..41FA");
			if(p >= 0) soption = "×"+(X.U16(p+2,_BE)+3)
		}
	}
	else if(X.c("4A00",0xC)) sversion = "type 2";
	else if(X.c("0C00",0xC)) sversion = "type 3";
	return true
}
if(!bDetected && isDZ()) {
	sName = 'Darius "Mark II" Zendeh module (.DZ)'; sVersion = sversion; sOptions = soptions; bDetected = 1
}


function isMkII() {
	//ref reversing of the eagleplayer
	// & http://old.exotica.org.uk/tunes/formats/mk2/mk2_v1.zip
	if(!X.c("41FA",4) || X.Sz() < 0x500) return false;
	//if(X.fSig(0x200,0x200,"'.ZADS89.'")>=0) ← Some zero-fill it--not used by the replayer
	p = 8; i = 0x120;
	do { t = X.c("E742",p); p += 2; if(t) break; i-- } while(i); if(!i) return false;
	do { t = X.c("41FA",p); p += 2; if(t) break; i-- } while(i); if(!i) return false;
//_log("mkII 41FA search: i="+i+" @"+p)
	a3 = p; p += X.I16(p,_BE); //p goes to sampleinfo
	d5 = X.I16(a3+2,_BE); //d5 is maybe (or not) "move.w (a0+d2+?), (a3+?)"
	i = 0x18; do { t = X.c("D1FA",a3); a3 += 2; if(t) break; i-- } while(i);
//_log("mkII D1FA search: i="+i+" @"+a3)
	if(!i) {
		if(d5 == 0x3770) return false; d5 = 0; a5 = a3-0x1C; i = 0x10;
	} else { a5 = a3; a3 += X.I16(a3,_BE) }
//_log("mkII: 3770 and a5 + a3skip")
	do { t = X.c("41FA",a5); a5 -= 2; if(t) break; i-- } while(i);
//_log("mkII: 5, i="+i+" @"+Hex(a5))
	return i
}
if(isMkII()) {
	//DZ and MkII may detect some modules together, meaning a player of either format will play them
	_setResult("audio","Darius Zendeh's Mark II Sound System module (.MK2)","",""); bDetected = 1 //bDetected is for counting
}


function isJamCracker() {
	//ref https://github.com/tonioni/WinUAE/blob/master/prowizard/rippers/JamCracker.c
	if(!X.isHeuristicScan() && !X.c("'BeEp'")) return false;
	smp = X.U8(5);
	if(!smp || (smp > 0x1F)) return false;
	if(X.U8(6+smp*40) > 0) return false;
	allsmpsz = 0;
	for(k=0; k < smp; k++) {
		l = X.U32(38+k*40,_BE);
		if(!l || (l > X.Sz())) return false;
		allsmpsz += l;
	}
	ord = X.U16(6+smp*40,_BE);
	if(ord > 0xFF) return false;
	ptns = 6+smp*40+2;
	ptn = X.U16(ptns+ord*6,_BE);
	if(ptn > 0xFF) return false;

	ptn0 = X.U32(ptns+2,_BE);
	ptns += ord*6;
	trkdtsz = X.U32(ptns-4,_BE)-ptn0;
	addlns = X.U8(ptns-5)*4*8;
	sz = ptns + 2 + ptn*2 + allsmpsz + trkdtsz + addlns; //doesn't include the Xag tags
	return true
}
if(!bDetected && isJamCracker()) {
	sName = "JamCracker/Pro module (.JAM,.JC)"; bDetected = 1; bad = "";
	if(X.isHeuristicScan()) {
		if(sz+19 == X.Sz()) { //obviously not for rippers
			sVersion = "v1.0a (Xag)";
			if(X.isVerbose()) sOptionT(X.SA(sz,19)); //this version adds these
			sz += 19;
		}
		if(!X.c("'BeEp'")) bad = bad.addIfNone("!badsig");
	}
	if(sz > X.Sz()) bad = bad.addIfNone("!short");
	if(bad != "") sVersion = sVersion.appendS("malformed"+bad,"/");
	if(X.isVerbose())
		sOption("ord:"+ord+" ptn:"+ptn+" smp:"+smp+" sz:"+outSz(sz));
}


function isJBM() {
	//ref https://github.com/adplug/adplug/blob/master/src/jbm.cpp
	//TODO for ex. demo3.jbm has a pointer to outside of the file,
	//  and it seems to work in a player but these are sanity checks ffs... Dunno this format
	if(X.U16(0,_LE) != 2) return false;
	if(!X.c("00FF01FF02FF03FF04FF05FF06FF07FF08FF09FF0AFFFD00", 0x36
)) return false; //just from the files modland has, 'cause the detection's' wonky
	seqt = X.U16(4,_LE); if(!seqt || seqt < 30 || seqt >= X.Sz()) return false;
	inst = X.U16(6,_LE); if(!inst || inst <= seqt || inst >= X.Sz()) return false;
	ins = (X.Sz()-inst) >> 4;
//_log("fs="+Hex(X.Sz())+", inst="+Hex(inst)+", ins="+ins);
	mptn = 0xFFFF; ch = 0; trk=[];
	for(i=0; i < 11; i++) {
		p = X.U16(10+i*2,_LE); trk[i] = p;
		if(p) { ch++;
			if(p <= seqt || p >= inst) return false;
			if(p < mptn) mptn = p
		}
	}
//_log("trk:"+trk);
	ptn = (mptn-seqt) >> 1;
//_log("("+mptn+"-"+seqt+")/2="+ptn);
	for(i=0; i < 5/*ptn*/; i++) { //TODO make a full check when the format is more clear!
		if(trk[i]) {
			p = X.U16(seqt+i*2,_LE);
//_log(seqt+"["+i+"*2]="+Hex(p));
			if(X.U16(p-1,_LE) != 0xFDFF) return false;
			if(p <= seqt || p >= inst) return false;
		}
	}
	i = X.U16(2,_LE); tmr = (1193810.0 / (i ? i : 0xFFFF)).toFixed(2);
	return true
}
if(!bDetected && X.isHeuristicScan() && isJBM()) {
	sName = "Johannes Bjerregaard Adlib module (.JBM)"; bDetected = 1;
	if(X.isVerbose())
		sOption("tmr:"+tmr+" ch:"+ch+" ptn:"+ptn+" ins:"+ins)
}


function isSoundFX() {
	//from https://github.com/OpenMPT/openmpt/blob/master/soundlib/Load_sfx.cpp
	// & https://gitlab.com/uade-music-player/uade/-/raw/master/amigasrc/players/wanted_team/SoundFX/src/SoundFX_V2.asm
	if(X.c("'SONG'",0x3C) && X.Sz() > 0x294) msmp = 15;
	else if(X.c("'SO31'",0x7C) && X.Sz() > 0x4B4) msmp = 31;
	else return false;
	hdrp = 4*msmp; smptp = hdrp+0x14; ordp = smptp+msmp*30;
	ord = X.U8(ordp); if(ord > 128) return false;
	t = X.U16(hdrp+4,_BE); if(t < 178 /*== 0*/) return false;
	tmp0 = (14565*122/t).toFixed(0); spd0 = 6;
	smp = smpsz = ic = 0; smps = [];
	for(i=0; i < msmp; i++) {
		ssz = X.U32(i*4,_BE); if(ssz > 0x20000) return false; smpsz += ssz; if(ssz) smp++;
		t = X.readBytes(smptp+i*30,20);
		for(c=0; c < 20; c++) if(t[c] && t[c] < 0x20) ic++; if(ic >= 128) return false;
		t = decEncoding(t,CPAmiga);
		if(t.trim() != "") smps.push(t.trim())
		smth = X.U16(smptp+i*30+2,_BE);
	}
	return true
}
if(!bDetected && isSoundFX()) {
	sName = msmp == 15 ? "SoundFX module (.SFX)" : "SoundFX 2 / MultiMedia Sound module (.MMS)";
	if(msmp == 15) sVersion = 'v1.0-8'; bDetected = 1;
	if(X.isVerbose()) {
		p += 2; ptn = 0;
		for(i=ordp+2; i < ordp+2+ord; i++) { t = X.U8(i); if(t > ptn) ptn = t } ptn++;
		sz = ordp + 0x82 + ptn*0x400 + smpsz; if(msmp == 31) sz += 6;
		lp = X.U8(ordp+1); if(lp > ord) sVersion = sVersion.appendS('malformed!badloop','/');
		//unk = X.readBytes(hdrp+6,14); unksum = 0; for(i=0;i<unk.length;i++) unksum += unk[i];
		//if(unksum > 0) sOption('unk.bytes:'+unk);
		sOption('tmp0:'+tmp0+' ord:'+ord+' ptn:'+ptn+' smp:'+smp+(lp?' loop:'+lp:'')+' sz:'+outSz(sz))
	}
}


function isJesperOlsen() {
	//ref https://gitlab.com/uade-music-player/uade/-/raw/master/amigasrc/players/wanted_team/Jesper_Olsen/Jesper Olsen_v1.asm
	fmt = ''; smp = smpp = 0; sz = -1; x = 1;
	if(X.c("6000")) {//old
		p = 0; d1 = 3;
		do {
			if(!X.c("6000",p)) return false;
			if((t = X.I16(p+2,_BE)) <= 0 || t % 2) return false; p += 4
		} while(d1--);
		p = p0 = 6+X.I16(6,_BE);
		if(!X.c("4A406B00",p)) { //even older
			p -= 4;
			if(X.c("C0FC",p)) p += 2;
			else
				for(i=0; i < 16 && p < X.Sz(); i++,p+=2)
					if(X.c("02800000",p)) { if(!X.c("00FFC0FC",p+4)) return false; p += 8; break }
			if(p > X.Sz()) return false;
			mulu = X.I16(p,_BE); p += 800; q = p+900;
			while(p < X.Sz()) {
				if(X.c("6AE064E0",p)) break;
				p += 2; if(p === q) return false
			}
			if(p > X.Sz()) return false;
			fmt = 'old'; p = p0;
			while(p < X.Sz()) { t = X.I16(p,_BE); p += 2;
				if(t >= 0 && (t & 0x40FA) == 0x40FA) { t = X.I16(p,_BE); p += 2; if(t >= 0) break } }
			p += t-2; var songp_in_explayp = p;
			x = X.I16(p,_BE)-p;
			if(x < mulu) x = 1; else x = Util.divu64(x,mulu);
			//p is now in a2 and a1 is the filesize
			while(p < X.Sz()) {
				if(X.c("'FORM' 0000???? '8SVXVHDR'",p)) { if(!smpp) smpp = p; smp++; p += 4+X.I32(p+4,_BE)+2 }
				p += 2; if(p >= X.Sz()) break
			}
			if(p > X.Sz()) return false; sz = p;
			p = 0xA+X.I16(0xA,_BE);
			while(p < X.Sz()) { t = X.c("43FA",p); p += 2; if(t) break }
			var Lea = p, VoiceTmp = p+X.I16(p,_BE); p += 2; //skipping the part about a flag
			d1 = X.c("6000",12) ? "7FFE" : "7FFF";
			while(p < X.Sz()) { t = X.c(d1,p); p += 2; if(t) break }
			while(p < X.Sz()) { t = X.c("336C",p); p += 2; if(t) break }
			var Value = X.I16(p+2,_BE)
			//now, I'm not very sure if we _need_ Lea, VoiceTmp, Value for our purposes, but we have'em!
		} else {
			if(!X.c("000641FA",p+4)) return false; p += 8;
			p += X.I16(p,_BE)+4; if(!X.c("00017FFF",p)) return false;
			fmt = 'second';
			p = p0+8+X.I16(p0+8,_BE); var SongPtr = p;
			p += 8; x = 0; d2 = X.I32(p+0xA,_BE);
			while(p < X.Sz()) { if(d2 != X.I32(p+0xA,_BE)) break; x++; p += 0x1A }
			p = smpp = SongPtr+X.I32(SongPtr,_BE); //smpinfo
			while(p < X.Sz()) {
				if(X.c("'FORM' 00?????? '8SVXVHDR'",p)) { smp++; p += 4+X.I32(p+4,_BE)+2 }
				p += 2; if(p >= X.Sz()) break
			}
			if(p > X.Sz()) return false; sz = p;
			p = SongPtr; d1 = X.U32(p,_BE); d2 = X.U32(p+0x12,_BE);
			if(d2 <= d1) p += d1; //that'll be our song size
			else {
				d3 = d2-X.I32(p+0x16,_BE);
				sz = d2+d3+p; if(sz == 35944) x -= 1; //I don't have this file to test
				if(sz > X.Sz()) return false;
				//d3 <<= 2; p += X.I32(p,_BE); d3 += p; songsz = d3; smpsz = sz-songsz
			}
		}
	} else if(X.c("0000 ???? 00017FFF")) { //the Harald Hårdtand game is once again different
		var z = 0; for(p = 8; p < 0x10; p += 2) {
			t = X.I16(p,_BE); if(!t) { z++; continue }
			if(t < 0 || t%2 || !(X.c("7FFF",t-2) || X.c("7FFE",t-4))) return false;
		} if(z > 1) return false;
		p = 2+X.I16(2,_BE);
		if(p > X.Sz() || !X.c("'FORM' 00?????? '8SVXVHDR'",p)) return false;
		while(p < X.Sz()) {
			if(X.c("'FORM' 00?????? '8SVXVHDR'",p)) { smp++; p += 4+X.I32(p+4,_BE)+2 }
			p += 2; if(p >= X.Sz()) break
		}
		if(p > X.Sz()) return false;
		sz = p; fmt = 'harald'
	} else { //this detection is a tad broken in the eagleplayer
		if((p = X.U16(0,_BE)) > 0x200 || p < 4 || p % 2) return false; p += 2;
		for(i=2; i < p; i += 2) if((t = X.I16(i,_BE)) <= 0 || t % 2 || !X.c("7FFF",t-2)) return false;
		fmt = 'latest';
		x = X.U16(0,_BE); p = smptp = X.U16(x+2,_BE); d3 = X.U16(x,_BE); x >>= 1; x--;
		if(x != 1) if(X.c("0000",d3-6) || !X.c("7F00",d3-4)) x--;
		smp0p = X.U16(p,_BE); var lo = hi = X.U24(smp0p+0xF,_BE);
		smp = ssz = 0;
		while(p < smp0p && smp < 128) { if(p > X.Sz()) return false;
			smp++; pp = X.U16(p,_BE);
			sofs = X.U24(pp+0xF,_BE);
			if(lo > sofs) lo = sofs;
			if(hi <= sofs) {
				hi = sofs; ssz = X.I16(pp+0x12,_BE);
				if(!X.c("'BODY'",sofs-8)) { //a raw sample
					//the original test was if(!X.U8(pp+0xE)) - this fails with, say, georgglaxo ingame.jo
					sofs = X.U32(pp+0x14,_BE);
					if(sofs > hi) { hi = sofs; ssz = 2*X.U16(pp+0x18,_BE) }
				}
			}
			if(hi > X.Sz()) return false;
			if(pp < smp0p) smp0p = pp;
//_log('smp '+smp+'. @'+Hex(pp)+' maxofs:'+Hex(hi)+' ssz0E:'+Hex(X.U24(pp+0xF,_BE))+' ssz18:'+Hex(X.U16(pp+0x18,_BE))
//+(X.c("'BODY'",sofs-8)?' IFF ['+Hex(X.U32(sofs-4,_BE))+'-'+Hex(sofs+X.U32(sofs-4,_BE))+']':' raw'))
			p += 2;
		}
		if(X.c("'BODY'00'",lo-8)) lo = X.fSig("'FORM'00??????",lo-150,lo-20); songsz = lo;
		if(X.c("'BODY'00'",hi-8)) hi += X.U32(hi-4,_BE); //BODY tag skip
		else hi += ssz<<1;
		sz = hi;
	}

	return true;
}
if(!bDetected && isJesperOlsen()) {
	sName = "Jesper Olsen's module (.JO)"; sVersion = fmt; bDetected = 1;
	if(X.isVerbose()) {
		if(x > 1) sOption(x,'×'); sOption('smp:'+smp+' sz:'+outSz(sz))
	}
}


function isJasonBrooke() {
	//from reversing the eagleplayer
	// SubSongs at +04; SongSize at +14; Samples at +1C; CalcSize at +24;
	// SamplesSize at +2C; ord at +34; SynthSamples at +3C
	// LoadSize at +0C; SynthSamples at +3C; Steps at +44; AuthorName at +4C => F2;
	// Prefix at +54 => 139
	fmt = ''; p = 0;
	function findPlus(s) { while(p < X.Sz()) { t = X.c(s,p); p += 2; if(t) break } }
	function findOrPlus(s) { while(p < X.Sz()) { if(X.c(s,p)) break; p += 2 } }
	function minusFind(s) { while(p > 0) { p -= 2; if(X.c(s,p)) break } }
	if(X.c("48E7F0F0")) { if(!X.c("424047FA FFF84A2B",4)) return false; fmt = 'old'; // -1
		if(!X.isVerbose()) return true;
		a0 = p = 12; findPlus("43FA"); d7 = p+X.U16(p,_BE)+1;
		findPlus("228A"); p += 2; d2 = p-8+X.U16(p-8,_BE); //the first synsmp ptr?
		findPlus("137B"); d6 = a4 = p+X.U16(p,_BE)-1; x = 1;
		while(d6 < X.Sz()) { d6 += 10; if(!X.U16(d6,_BE)) break; if(X.U8(d6)) break; x++ }
		minusFind("48E7"); var hi, lo = X.U16(a4+2,_BE); //p & a4 -> stack
		for(d0 = x; d0--;) { //d852~62
			a4 += 2; for(d3=4; d3--;) { t = X.U16(a4,_BE); a4 += 2; if(lo > t) lo = t }
		}
		p = lo; lo = hi = X.U16(p,_BE); p += 2;
		do { //d870~84
			t = X.U16(p,_BE); p += 2; if(t) { if(t < lo) lo = t; if(t > hi) hi = t; }
		} while(p < d7);
		a3 = lo; synsmp = (lo-d2)>>7;
		p = hi;
		while(p < X.Sz()) if([0x85,0x87].indexOf(X.U8(p)) >= 0) break; else p++; //d89a~ac
		d0 = 0; do { //d8ae~c6
			while(a3 < X.Sz()) if([0x85,0x87].indexOf(X.U8(a3)) >= 0) break; else a3++;
			a3++; d0++
		} while(a3 < p);
		p++; a3 = d6; if(a3) { //d8c8~f6
			for(d1=0x80,found=false; d1-- && !found;) { t = X.c("41FA",a3); a3 += 2; if(t) found = true }
			if(found) { //d8de
				a3 += X.U16(a3,_BE);
				while(a3 < X.Sz()) { if(X.c("FF",a3+4) || X.c("FF",a3+6)) a3 += 0x12; else break }
				p = a3;
			}
			sz = p; steps = d0 //d8fa
		}
	}
	else {
		p = 0xAC; while(p < 0xE0) { t = X.c("48E7",p); p += 2; if(t) break } if(p > 0xE0) return false; 
		if(!X.c("F8FC",p) && !X.c("F8F8",p)) return false;
		if(!X.c("08F90001 00BFE001 33FC0780 00DFF09A 47FA",p+2)) return false;
		p += 0x14; if(0x10000-p-X.U16(p,_BE)) return false;
		fmt = 'new'; if(!X.isVerbose()) return true; p = 0;
		findPlus("48E7"); findPlus("49F9"); p += 6; findPlus("7600"); d0 = X.U16(p-10,_BE);
		findOrPlus("10320000"); a4 = p-2+X.U16(p-2,_BE);
		findPlus("48E7"); p = a3 = p-6; findPlus("43FA"); p += X.U16(p,_BE)-1;
		a4 -= p; d3 = Util.divu64(a4,0x12); x = 1; d2 = 0;
		for(; p < X.Sz(); x++) { //d74e~5c
			p += 0x12; d2 = X.U16(p,_BE); if(X.U8(p,_BE)) break; d2--; if(d2 <= 0) break; }
		if(d3 < x) x = d3; //d75e~64
		p = a3; minusFind("45FA"); d0 = p = p-2+X.U16(p-2,_BE);
		p += 4; synsmp = smp = d5 = 0; smpp = p;
		while(p < X.Sz()) {
			if((t = X.U32(p,_BE)) > d0) { //d784~a8
				if(!smp) d5 = t; if(t <= d5) d5 = a4 = t; smp++; p += 10; if(p > a4) break;
			} else { synsmp++; p += 10 }
		}
		i = smp+synsmp; sz = 0; p = smpp;
		while(i-- && p < X.Sz()) { t = X.U32(p,_BE)+X.U16(p+4,_BE); if(sz < t) sz = t; p += 10 } //~d7cc
	}
	return true
}
if(!bDetected && isJasonBrooke()) {
	sName = "Jason C. Brooke's module (.JB,.JCB)"; sVersion = fmt; bDetected = 1;
	if(X.isVerbose()) {
		if(x > 1) sOption(x,'×');
		sOption((fmt=='new'?'wf.smp:'+smp:'steps:'+steps)+' syn.smp:'+synsmp+' sz:'+outSz(sz))
	}
}


function isKrisHatlelid() {
	// SubSongs at +04; SamplesSize at +0C; Length at +14; Samples at +1C
	if(!X.c("000003F3 00000000 00000003 00000000 00000002 ???????? 40?????? 00000001 000003E9"))
		return false;
	twofiles = false; p = 0x30;
	d1 = X.U32(0x14,_BE) & ~(1<<30); if(X.U32(0x24,_BE) != d1) return false;
	if(X.c("60000016",0x28)) { if(!X.c("0000ABCD",0x2C)) return false } else p = 0x18;
	if((p == 0x30 && !X.c("B07C0000",0x40)) || p == 0x18) {
		if(!X.c("41F90000 00004E75",p+0x10)) return false; twofiles = true
	}
	p = 0x40; smpp = 0;
	while(!smpp && p < X.Sz()) { t = X.fSig(p,TOEOF,"'FORM'"); if(!t%2) smpp = t; else p = Math.max(t+1,p+1) }
	if(smpp < 0) return false;
	smp = 0;
	while(p < X.Sz()) { if(!X.c("'FORM' ???????? '8SVXVHDR'",p)) break; smp++; p += 4+X.U32(p+4,_BE) }
// var psz = X.fSig(0x24,0x40,"000003EA")+4; if(psz < 4) return false; sz = psz+4+(X.U32(psz,_BE)<<2);
// if((t = (X.U32(sz,_BE)>>3)<<3) != 0x3EC) _log(" the chunk at sz is not 3EC but "+Hex(t));
// else t = X.U32(sz+4,_BE);
// if(p != sz) _log("sz differs! "+Hex(sz)+" vs "+Hex(p))
// TODO parse hunks↑ tooб AND detect/debug SubSongs!
	sz = p;
	return true;
}
if(!bDetected && isKrisHatlelid()) {
	sName = "Kris Hatlelid's module (.KH"+(twofiles?'+SONGPLAY':'')+")"; bDetected = 1;
	if(X.isVerbose()) {
		sOption('sz:'+outSz(sz))
	}
}


function isKefrensSoundMachine() {
	//ref https://github.com/tonioni/WinUAE/blob/master/prowizard/rippers/KefrensSoundMachine.c
	if(X.Sz() < 1537) return false;
	if(X.U8(15) != 0x61) return false;
	for(k=0; k < 15; k++)
		if(X.U8(54+k*32) > 0x40) return false;
	trks = 0;
	for(k=0; k < 1024; k++) {
		k_ = X.U8(k+512);
		if(k_ == 0xFF) break;
		if(k_ > trks) trks = k_;
	}
	if(k == 1024) return false;
	if(trks == 0) return false;
	if(1536 + trks*192 + 64*3 > X.Sz()) return false;
	for(k=0; k <= trks; k++)
		for(l=0; l < 64; l++)
			if(X.U8(1536 + k*192 + l*3) > 0x24) return false;

	allsmpsz = smp = 0;
	for(k=0; k<15; k++) { t = X.U16(52+k*32,_BE); if(t) smp++; allsmpsz += t }
	sz = (trks+1)*192 + allsmpsz + 1536;
	return true
}
if(!bDetected && isKefrensSoundMachine()) {
	sName = "Jess D. 'Razmo' Skov-Nielsen's Kefrens Sound Machine module (.KSM)"; bDetected = 1;
	if(X.isVerbose()) {
		sOptionT(X.SA(2,13));
		sOption("trk:"+trks+(smp?" smp:"+smp:'')+" sz:"+outSz(sz));
		if(sz > X.Sz()) sVersion = "malformed!short"
	}
}


function isPaulRobotham() {
	//from https://gitlab.com/uade-music-player/uade/-/raw/master/amigasrc/players/wanted_teamPaulRobotham/src/Paul%20Robotham_v1.asm
	trk = X.U16(0,_BE); d2 = X.U16(2,_BE); d3 = X.U16(4,_BE); smp = X.U16(6,_BE); p = 8;
	if(!trk || trk > 4 || !d2 || !d3 || !smp) return false;
	mp = 0; trks = [];
	for(d1=trk;d1;d1--) { t = X.U32(p,_BE); p += 4; if((t>>16) || !t) return false; trks.push(t); if(mp < t) mp = t } //stpos
//_log('PaulRobotham: StartPos -> '+Hex(p));
	for(;d2;d2--) { t = X.I32(p,_BE); p += 4; if(t <= 0 || t%2) return false; if(mp < t) mp = t } //Next1
	d2 = X.I32(p,_BE);
//_log('PaulRobotham: Next1 -> '+Hex(p))
	for(;d3;d3--) { t = X.I32(p,_BE); p += 4; if(t <= 0 || t%2) return false; if(mp < t) mp = t } //Next2
//_log('PaulRobotham: Next2 -> '+Hex(p))
	p += smp*12; if(p != d2) return false;
	for(d2=15;d2;d2--,p+=16) if(!X.c("3F3F3F3F 3F3F3F3F 3F3F3F3F 3F3F3F3F",p)) return false;
	return true
}
if(!bDetected && isPaulRobotham()) {
	sName = "Paul Robotham's module (.DAT+.SSD)"; bDetected = 1;
	if(X.isVerbose()) sOption('trk:'+trk+' smp:'+smp)
}


function isDMM() {
	// ref https://bitbucket.org/zxtune/zxtune/src/develop/src/formats/chiptune/digital/digitalmusicmaker.cpp
	for(i=0; i < 6; i++) if(!(0xC0 <= X.U8(i*2+1) <= 0xFF)) return false;
	ptnsz = X.U8(0x0C); if(ptnsz & 0x87) return false;
	ptn = -1; for(i=0; i < 0x32; i++) {
		j = X.U8(0x0E+i); if(j > 0x17) return false;
		if(j > ptn) ptn = j;
	} ptn++;
	tempo = X.U8(0x40); if(tempo < 3 || tempo > 30) return false;
	loop = X.U8(0x41); if(loop > 0x32) return false;
	ord = X.U8(0x43); if(!ord || ord > 0x32) return false;
	hss = X.U8(0x44); if(hss < 2 || hss > 0x38) return false;
	bad = false; smp = 0;
	for(i=0; i < 16; i++) {
		smpst = X.U16(0x5A+i*16+9,_LE);
		smplm = X.U16(0x5A+i*16+12,_LE);
		smplp = X.U16(0x5A+i*16+14,_LE);
		if(smpst > smplm || smpst < 49152 || smplm < 49152) return false;
		if(smplp < 49152 || smplp > smplm || smpst > smplp
		  || (smplm > 49152 && smplm-smplp < 6)) {
			bad = true;
			if(!X.isHeuristicScan()) return false
		}
		if(smpst < smplm) smp++;
	}
	return true
}
if(!bDetected && X.isDeepScan() && isDMM()) {
	bDetected = 1; sVersion = "v1.x"; if(bad) sVersion += "/malformed";
	sName = "Vasilii 'BACA' Pakhomov/LAVE's Digital Music Maker module (.DMM)";
	if(X.isVerbose())
		sOption("tempo:"+tempo+" ord:"+ord+" ptn:"+ptn+" smp:"+smp+" loop:"+loop)
}


function isComposerC67() {
	// from https://github.com/OpenMPT/openmpt/blob/master/soundlib/Load_c67.cpp
	if(X.Sz() < 0xBA2) return false; //7A2+400
	spd = X.U8(0); if(!isWithin(spd,1,15)) return false; ord = 0; ptn = [];
	for (i=0; i < 256; i++) if((o=X.U8(0x6A2+i)) != 0xFF) { // check orders
		if(o >= 128) return false; if(i > ord) ord = i; if(ptn.indexOf(o) < 0) ptn.push(o) }
	ord++; ptn = ptn.length; nonz = false; lp = X.U8(1); if(lp > ord) return false; smp = opl = smpsz = 0;
	for (i=0; i < 32; i++) { // checking sample records
		if(X.U8(2+i*13+12) || X.U32(0x1A2+i*16) || X.U8(0x3A2+i*13+12)) return false;
		var _l = X.U32(0x1A2+i*16+4), _ls = X.U32(0x1A2+i*16+8), _le = X.U32(0x1A2+i*16+12);
		if(_l > 0xFFFFF) return false; if(_l && _le < 0xFFFFF && (_le > _l || _ls > _le)) return false;
		_opl = !X.c("00000000 00000000 000000",0x542+i*11);
		if(X.U8(0x542+i*11) & 0xF0) return false;
		if(X.U8(0x542+i*11+5) & 0xFC) return false;
		if(X.U8(0x542+i*11+10) & 0xFC) return false;
		if(_l) { smp++; smpsz += _l }  if(_opl) opl++;
	}
	bad = false; ptnend = notes = 0;
	for (i=0; i < 128; i++) { // checking patterns
		p = 0xBA2+X.U32(0x7A2+i*4,_LE); ptnlen = X.U32(0x9A2+i*4,_LE);
		if(!bad) bad = (ptnlen < 3 || ptnlen > 0x1000);
		if(p > 0xFFFFFF || (X.Sz() < p+ptnlen)) return false;
		if(p+ptnlen > ptnend) ptnend = p+ptnlen;
		for(r = 0; p < ptnend;) {
			var cmd = X.U8(p++);
			if(cmd <= 0xC) { p += 2; notes++ }
			else if(cmd >= 0x20 && cmd <= 0x2C) p++;
			else if(cmd == 0x40) r += X.U8(p++);
			else if(cmd == 0x60) break;
			else return false
		}
		if(r > 64) return false;
	}
	sz = ptnend+smpsz;
	return smp+opl > 0
}
if(!bDetected && isComposerC67()) {
	sName = "Thomas 'Tran' Pytel's CDFM/Composer 670 module (.C67)"; bDetected = 1;
	if(bad) sVersion = "malformed!badptn";
	if(X.isVerbose()) sOption('spd:'+spd+' ord:'+ord+(lp?' lp:'+lp:'')+' ptn:'+ptn+' smp:'+smp+' fm:'+opl+' notes:'+notes+' sz:'+outSz(sz))
}


function isSeanConnolly() {
	//from UADE player check RE
	if(!X.c("6000.... 6000.... 6000")) return false;
	a0 = 2+X.I16(2,_BE);
	if(X.c("48E77FFE E98841FA",a0)) v = 2;
	else if(X.c("48E740F0 4A006B0A",a0)) v = 3;
	else return false;
	return true
}
if(!bDetected && isSeanConnolly()) {
	sName = "Sean 'Odie' Connolly\'s module (.SCN)"; sVersion = "v"+v; bDetected = 1
}


function isAVP() {
	//from reversing the eagleplayer
	d1 = "48E7FCFE"; a0 = 0;
	if(X.c(d1)) { // @d440 -> d47c
		if(X.c("45FA",0xDC)) return false;
		if(!X.c("E9417000 41FA",4)) return false;
		if(X.c(d1,0x94) || X.c(d1,0xA4) || X.c(d1,0xA8)) {
			fmt = 0; base = 8; return true }
		else return false
	} else if(X.c("2F0841FA")) { //@d444 -> d4ac
		a0 += 4; a0 += X.I16(a0,_BE) + 0x1C;
		if(!X.c("45FA",a0+0xDC)) return false;
		a0 += 4; if(!X.c("E9417000 41FA",a0)) return false; a0 += 4;
		if(X.c(d1,a0+0x8C) || X.c(d1,a0+0x9C) || X.c(d1,a0+0xA0)) {
			fmt = 0; base = a0; return true }
	} else if(X.c(d1,0x1C)) { //@d44c -> d4b0
		a0 += 0x1C; if(!X.c("45FA",a0+0xDC)) return false;
		a0 += 4; if(!X.c("E9417000 41FA",a0)) return false; a0 += 4;
		if(X.c(d1,a0+0x8C) || X.c(d1,a0+0x9C) || X.c(d1,a0+0xA0)) {
			fmt = 0; base = a0; return true }
	} else if(X.c("6000")) { //@d452 -> d4d0
		if(!X.c("6000.... 6000.... 6000.... 6000.... 6000.... 6000.... 6000",4)) return false;
		if(X.c("6000",0x20) && !X.c("6000",0x24)) return false;
		a0 = 0xE; a0 += X.I16(a0,_BE);
		if(X.c(d1,a0)) { //@d50e -> d4be
			if(!X.c("45FA",a0+0x10C) && !X.c("E942",a0+0x112)) return false;
			a0 += 4; if(!X.c("E9417000 41FA",a0)) return false; a0 += 4;
			if(X.c(d1,a0+0x8C) || X.c(d1,a0+0x9C) || X.c(d1,a0+0xA0)) {
				fmt = 0; base = a0; return true }
		}
		a0 = 0x1A+X.I16(0x1A,_BE); if(!X.c(d1+"43FA",a0)) return false;
		fmt = 1; base = a0; return true
	} else { //@d45a
		do { if(X.c("2F0841FA",a0+0x1C)) break; a0 += 2 } while(a0 < 0xA);
		if(a0 == 0xA) return false; //else -> d46e
		i = 0x4C; do { if(X.c(d1,a0)) break; a0 += 2; i-- } while(i);
		if(!i) return false; //else -> d4be
		if(!X.c("45FA",a0+0x10C) && !X.c("E942",a0+0x112)) return false;
		a0 += 4; if(!X.c("E9417000 41FA",a0)) return false; a0 += 4;
		if(X.c(d1,a0+0x8C) || X.c(d1,a0+0x9C) || X.c(d1,a0+0xA0)) {
			fmt = 0; base = a0; return true }
	}
	return false
}
if(!bDetected && isAVP()) {
	sName = "Martin Walker's Activision Pro module (.AVP)"; sVersion = "f."+fmt; bDetected = 1;
	if(X.isVerbose()) {
		sOption(Hex(base),"base:");
		//TODO: derive some info using NP
	}
}


function isJP() {
	//from https://gitlab.com/uade-music-player/uade/-/raw/master/amigasrc/players/wanted_team/JasonPage/src/Jason%20Page_v5.s
	fmt = 0;
	if(X.c("0002") && !(X.U8(3)&1)) for(;;) {
		t = X.I16(4,_BE); if(t&1) break; if(X.I16(t)&1) break;
		d0 = X.I16(0x30,_BE); if(d0 > X.Sz()) break;
		for(a1 = 2; a1 <= 0x2E; a1 += 2) {
			t = X.U16(a1,_BE); if(!t || (t&1) || t > X.Sz() || d0 <= t) break;
		}
		if(a1 < 0x30) break;
		if((X.U16(t,_BE)&0xF00) == 0xF00) fmt = 2; else fmt = 1; break
	}
	if(!fmt)
		if(X.I16(0,_BE) || X.I16(0x80,_BE) || !X.c("00000CBE",0x84)
		  || !X.c("000308BE",0xCB6) || !X.c("000309BE",0xCBA)) return false;
		else fmt = 3;
	smpsz = smp = 0; x = 1;
	if(fmt == 3) {
		sz = X.U32(0x8BA,_BE)+2; if(sz < 0x8BC || sz > X.Sz()) return false;
		t0 = X.U32(0,_BE);
		for(i = 4; i < 0x80; i += 4) {
			t1 = X.U32(i,_BE); t = t1-t0; if(t < 0) return false;
			smpsz += t; if(t) smp++; t0 = t1 }
		smpsz += 0x80;
		if(sz == 7382 && smpsz == 19290) x = 16 /*realms ingame*/;
		else { x = 0; a1 = 0x6BE; do {
				d0 = X.U32(a1,_BE); a1 += 4; if(X.U16(d0,_BE) == 0xFF00) break; x++
			} while(a1 < sz) }
	} else {
		sz = X.U16(0x30,_BE); a0 = X.U16(0xC,_BE)+2; a1 = X.U16(0x1C,_BE); x = (a1-a0)>>1;
		for(i = X.U16(2,_BE); i < sz-4; i += 4) { slen = X.U32(i,_BE); smpsz += slen; if(slen) smp++ }
	}
	return true;
}
if(!bDetected && isJP()) {
	switch(fmt) {
	case 1: sName = "Jason Page's module (.JP)"; break; case 2: sName = "Jason Page's module (.JPN)"; break;
	case 3: sName = "Jason Page's old module (.JP)"; } sVersion = "f."+fmt; bDetected = 1;
	if(X.isVerbose()) {
		if(x > 1) sOption(x,'×');
		sOption("smp:"+smp+" smp.sz:"+smpsz+" song sz:"+outSz(sz))
	}
}


function isMikeDavies() {
	//ref reversing the eagleplayer
	a2 = 0xF2;
	if(!X.c("0C40",a2)) { a2 -= 6; if(!X.c("33C0",a2)) return false }
	if(!X.c("48E7FFFE 61064CDF 7FFF4E75",a2-12)) return false;
	a2 = X.fSig(a2,0xFFFF,"6000")+2; if(a2 < 2) return false;
	t = X.I16(a2,_BE); if(!t || t < 0 || (t&1)) return false;
	a2 += t; if(!X.c("2D58",a2)) return false;
	a2 = 0;
	function findOrPlus(s) { while(a2 < X.Sz()) { if(X.c(s,a2)) break; a2 += 2 } } // couple helper functions
	function plusAndFind(s) { while(a2 < X.Sz()) { a2 += 2; if(X.c(s,a2-2)) break } }
	function errorP(s) { if(debug>1)_log('MikeDavies: '+s); patchClear(); return false }
	var a4 = []; // the "wach get out!!!" memory part. Local vars
	//we skip a4.push(0) *2 unlike the original code, so analogously, a4[] points @d138 instead of d134
	findOrPlus("48E7FFFE"); a4.push(a2>>16); a4.push(a2&0xFFFF); //d3a2~d3ae
	findOrPlus("33FC0001"); //d3b0~d3ba
	a2 += 4; rel = X.U32(a2,_BE);
	plusAndFind("41F9"); a4.push(a2>>16); a4.push(a2&0xFFFF);
	d1 = X.I32(a2,_BE); //d3c0~d3c8
	plusAndFind("B1FC"); d0 = X.U32(a2,_BE); //d3ca-d3d0
	x = d0 = (d0-d1) >> 4;

	plusAndFind("41F9"); a4.push(a2>>16); a4.push(a2&0xFFFF); //d3da~d3e0
	plusAndFind("B1FC"); a4.push(a2>>16); a4.push(a2&0xFFFF); //d3e2~d3e8
	plusAndFind("303C"); //d3ea~d3ee
	findOrPlus("48E7FFFE ........ 6100"); a4.push(a2>>16); a4.push(a2&0xFFFF); //d3f6~d40a
	plusAndFind("41F9"); a4.push(a2>>16); a4.push(a2&0xFFFF);
	d2 = X.U32(a2,_BE); //d40c~d414
	plusAndFind("B1FC"); d3 = X.U32(a2,_BE); //d416~d41c
	d3 = d4 = d3-d2; var mo = d3%0x18; d3 = Util.divu64(d3,0x18); //d41e~d42e
	if(mo) {
		d4 += 4; mo = d4%0x18; d4 = Util.divu64(d4,0x18);
		if(mo) return errorP('@d436 error 5'); //originally, error code 5
		d3 = d4;
		if(X.c("4654443C",0x7D8)) { //d44c~44; patch #1 adjusting 3 positions
			a3 = 0x770;
			for(d4=2; d4 >= 0; d4--) {
				a3 -= 4; wpU32be(a3+4,rpU32be(a3));
			}
		}
	}
	a4.push(d3>>16); a4.push(d3&0xFFFF); //d45a
	plusAndFind("4BF9"); d5 = rpU32be(a2)-rel; //d45c~64
	if(a2 > X.Sz()) return errorP('@d464 a2 too high');
	if(!X.isDeepScan()) { patchClear(); return true }

	a4.push(d5>>16); a4.push(d5&0xFFFF); //d466
	a4.push(0); //d468
	a5 = 0x4ED2; a4.push(a5>>16); a4.push(a5&0xFFFF); //d46a; delitrackerglobals

	//d472~d4fa: patch #2 (changes certain offsets in the player code to relative)
	a3 = (a4[0]<<16)+a4[1];
	p2cycle: while(a3 < X.Sz()) { //from d472...
		switch(rpU16be(a3)) {
		case 0x33EE: case 0x33FC: a3 += 2; //d4ce & ↓
		case 0x3039: case 0x33C0: case 0x33C1: case 0x3C39: case 0x41F9:
		case 0x4279: case 0x4A79: case 0x4BF9: case 0x4DF9: case 0x5379: case 0xB1FC: //d4d0
			a3 += 2; a3_ = rpU32be(a3); if((a3_>>16) == 0x00DF) break;
			wpU32be(a3,a3_-rel); a3 += 4; break;
		case 0x33F9: //d4de
			a3 += 2; a3_ = rpU32be(a3);
			if((a3_>>16) == 0x00DF) a3 += 4; else { wpU32be(a3,a3_-rel); a3 += 4 }
			a3_ = rpU32be(a3); if((a3_>>16) != 0x00DF) { wpU32be(a3,a3_-rel); a3 += 4 } break;
		default: a3 += 2; if(a3 >= d5) break p2cycle;
		}
	}
	d4 = d3; //d4fe
	a2 = (a4[10]<<16)+a4[11]; //d500
	a2 = rpU32be(a2); //d504
	a2_ = rpU32be(a2)-rel; wpU32be(a2,a2_); //d506~8
	d2 = a2_; //d50a~c
	d5 = a2_+2*rpU32be(a2+4); a2 += 0x18; a2_ = rpU32be(a2); d3 -= 2; //patch1; d50e~1a
	if(d3 > 0x80) return errorP('@d51a badsmp:'+Hex(d3));
	while(d3 >= 0) { //d51c & d53a
		a2_ -= rel; wpU32be(a2,a2_); //d51e~d520
		if(d5 <= a2_) d5 = a2_+2*rpU32be(a2+4);
		if(d2 > a2_) d2 = a2_; //d530~d534
		a2 += 0x18; a2_ = rpU32be(a2); //patch1
	d3--}
	a4.push(a2>>16); a4.push(a2&0xFFFF); //d53e. a2+4 = start of samples
	a2 = rpU32be((a4[4]<<16)+a4[5]); //d540~4
	a3 = (a4[6]<<16)+a4[7]; a3_ = rpU32be(a3); //d546
	d3 = Util.divu64(a3_-a2, 0x18); //d54c~50
	a4.push(d3); //d554
	smp = d3+d4; //d556~8
	d4 = d3; a2_ = rpU32be(a2); //d55c
	if(d4 > 0x80) return errorP('@d55e d4='+Hex(d4)+' > 80h');
	while(d4 > 0) { //d55e~60 & d57e
		a2_ -= rel; wpU32be(a2,a2_); //d562~4
		if(d5 <= a2_) d5 = a2_+2*rpU32be(a2+4);
		if(d2 > a2_) d2 = a2_; //d574~8
		a2 += 0x18; a2_ = rpU32be(a2);
	d4--}
	if(a2 > (a4[19]<<16)+a4[20]) { a4[19] = a2>>16; a4[20] = a2&0xFFFF } //d582~8
	d0 <<= 2; //d58e
 	a2 = rpU32be((a4[2]<<16)+a4[3]); a2_ = rpU32be(a2); //d592~6
	d3 = 0;
	do {
		if(a2_ != 0xFFFFFFFF && d3 < a2_-rel) d3 = a2_-rel; //d59c~a8
		a2 += 4; a2_ = rpU32be(a2); //d5aa
	d0--} while(a2 < X.Sz() && d0 > 0);
	a3 = (a4[8]<<16)+a4[9]; //d5b0
	while(a3 < X.Sz()) { a3 += 2; if(X.c("0C40",a3-2)) break } //d5b4~8
	d0 = rpU16be(a3+0x16); a4.push(d0); //d5ba~e
	if(d5 > d3) { //d5c8~a
		if(d5 > X.Sz()) return errorP('@d5ce error 1C'); //d5cc~e; return errorcode 1C
		a2 = d2; a1 = (a4[19]<<16)+a4[20];
		do { //from d5d8...
			if(rpU16be(a1) == d0) { a1 += 6; wpU32be(a1,rpU32be(a1)-rel) } //d5d8~e0; patching #3?...
			a1 += 2; // d5e2
		} while(a1 != a2 && a1 < X.Sz())
		a2 = d5;
	} else { //from d5ee
		if(d3 > X.Sz()) return errorP('@d5ee error 1C'); //d5ee~f0; return errorcode 1C
		a2 = d3; //d5f4; d4 and a3 = filesize
		while(a2 < X.Sz()) { //d5fc -> sub_d65c
			a2 += 2; if([0x94,0xFE,0xFF,0x1FE,0x1FF].indexOf(rpU16be(a2-2)) >= 0) break }
		a1 = d5; a1_ = rpU32be(a1); //d5f8
		do { // from d5fa...
			if(d0 == (a1_>>16)) {
				a1 += 6; a1_ = rpU32be(a1)-rel; wpU32be(a1,a1_);
				if(d3 < a1_) d3 = a1_;
			}
			a1 += 2; a1_ = rpU32be(a1); //d60a
		} while(a2 != a1); //d60c~e to d5fa
		if(a2 <= d3) { //d610~2
			a2 = d3;
			while(a2 < X.Sz()) {
				a2 += 2; if([0x94,0xFE,0xFF,0x1FE,0x1FF].indexOf(rpU16be(a2-2)) >= 0) break; }
		}
	}
	patchClear();
	sz = a2; //d618~a
	smpsz = d5-d2;
	songsz = a2-smpsz;
	return true
}
if(!bDetected && isMikeDavies()) {
	sName = "Mike Davies module (.MD)"; bDetected = 1;
	if(X.isVerbose()) {
		if(x > 1) sOption(x,'×');
		if(X.isDeepScan()) sOption('smp:'+smp+' songsz:'+Hex(songsz)+' smpsz:'+Hex(smpsz)+' sz:'+outSz(sz))
	}
}


function isSilmarils() {
	//from UADE player RE
	a0 = 0; if(!X.c("0016",a0+4)) { a0 += 6; if(!X.c("0016",a0+4)) return }
	if(!X.c("0000 00000000 00000000 ....0020 ....0020",a0+6)) return;
	d1 = X.U32(a0+0xE,_BE); if(d1&1) return;
	if(!X.c("00000058",a0+d1)) return;
	a2 = a0+0x58; a3 = 0; if(X.c("0016",0xA)) a3 += 6;
	a3 += X.U32(a3+0xE,_BE); a3 += X.U32(a3,_BE);
	a2 = a3; x = smp = sz = d0 = d3 = 0;
	while(a3 < X.Sz()) {
		a3 = a2; a2 += 4;
		if(!X.U32(a3,_BE) || X.U8(a3)) { //→e684
			a3 = sz; sz += X.U32(a3+2,_BE); break;
		} else {
			a3 += X.I32(a3,_BE); if(a3 > sz) sz = a3; if(a3 > X.Sz()) return;
			if(X.c("FF02",a3+6)) x++;
			else {
				t = X.U32(a3+2,_BE); if(t > X.Sz()) return;
				smp++; d3 += t; if(d0 < t) d0 = t
			}
		}
	}
	if(a3 > X.Sz() || sz > X.Sz()) return;
	smpsz = d3;
	return true
}
if(!bDetected && isSilmarils()) {
	sName = "Michel Pernot's Silmarils module (.MOK)"; bDetected = 1;
	bad = "";
	if(x > 3) bad = bad.addIfNone("!subsongs>3");
	if(smp > 0x20) bad = bad.addIfNone("!smp>32");
	if(bad != "") sVersion = sVersion.appendS("malformed"+bad,'/');
	if(X.isVerbose()) {
		if(x > 1) sOption(x,'×');
		sOption("smp:"+smp+" smpsz:"+Hex(smpsz)+" sz:"+outSz(sz))
	}
}


function isDUX() {
	//from the UADE player's RE
	t = X.U32(0,_BE); if(t < 0x70 || (t&1) || !X.c("FFFF FFFFFFFF",t-6)) return; sz = t;
	for(smp=0,p=4; p < 0x64; p+=12) { //8 samples
		t = X.U32(p,_BE); if((t&1) || t && t < 0x70 || t > sz) return; if(t) smp++
	} if(!smp) return;
	for(; p < 0x70; p += 4) { t = X.U32(p,_BE); if(t < 0x70 || (t&1) || t > sz) return }
	if(!X.c("FFFF FFFFFFFF",p+t-6)) return;
	smpsz = X.U32(0x64,_BE); p = 0x74+smpsz; //p points to patterns? orderlist?
	// what does this do?..
	//for(q = 0; p < sz; p += 4) if((t = X.I16(p,_BE)) >= 0) q += t; else break;
	//q >>= 4; unpsz = Util.div64(q*14187, 44336);
	return true
}
if(!bDetected && isDUX()) {
	sName = "GT Game Systems module (.DUX)"; bDetected = 1;
	if(X.isVerbose()) {
		ord = 1+Util.div64(X.U32(0x68,_BE)-smpsz, 0x3C); smpsz -= 0x70;
		sOption('ord:'+ord+' smp:'+smp+' smpsz:'+Hex(smpsz)+' sz:'+outSz(sz))
	}
}


function isBuzzic2() {
	//from the app sources
	if(!X.c("'buz2'0200")) return;
	gvol = X.F32(8); if(!isWithin(gvol,0,2)) return;
	bpm = X.U32(0xC); if(!isWithin(bpm,10,200)) return;
	startpos = X.U32(0x10); ord = X.U32(0x14)+1; if(startpos >= ord) return;
	looping = X.U8(0x18); if(looping > 1) return;
	nV = X.U16(6); p = 0x9C; sz = -1; bad = "";
	maxptn = maxtrk = 64; maxtml /*max template melody length*/ = 128;
	ptn = X.U32(p); trk = X.U32(p+4); p += 8;
	if(nV >= 1) { maxptn = X.I32(p); maxtrk = X.I32(p+4); maxtml = X.I32(p+8); p += 12 }
	p += maxptn*16+maxtrk*maxtml;
	trks = [];
	for(i=0; i < 64; i++) { t = X.SA(p,0x40).trim(); if(t != "") trks.push(t); p += 0x40 }
	ptns = [];
	for(i=0; i < 64; i++) { t = X.SA(p,0x40).trim(); if(t != "") ptns.push(t); p += 0x40 }
	ins = X.U32(p); p += 4;
	if(p > X.Sz()) return;
	inss = []; totalops = 0;
	for(i=0; i < ins; i++) {
		t = X.SA(p,0x80).trim(); if(t != "") inss.push(t);
		var opcnt = X.U32(p+0x184); totalops += opcnt; p += 0x2410+opcnt*0x228;
	}
	sz = p; if(sz > X.Sz()) bad = bad.addIfNone('!short');
	return true;
}
if(!bDetected && isBuzzic2()) {
	sName = "Buzzic module (.buz2)"; bDetected = 1; sVersion = 'v2.'+nV.padStart(2,'0');
	if(X.isVerbose()) {
		//sOptionT(addEllipsis(trks.join(','),0xA0,0x80),'trks:"','"');
		//sOptionT(addEllipsis(ptns.join(','),0xA0,0x80),'ptns:"','"');
		//sOptionT(addEllipsis(inss.join(','),0xA0,0x80),'insts:"','"');
		sOption('bpm:'+bpm+' trk:'+trk+' ptn:'+ptn+' ins:'+ins+' g.vol:'+Math.round(gvol*100)+'% ops:'+totalops+' sz:'+outSz(sz))
	}
}


function isInfogramesRH2() {
	if(!X.c("000200")) return; //+04: speed? version? I saw 1 through 7
	if(!X.c("FF000F",X.U16(4,_BE)+1)) return;
	sz = X.U16(0x10,_BE)+2;
	for(i=10,oldp = X.U16(8,_BE); i < 0x20; i += 2) { if(i == 0x10) continue;
		p = X.U16(i,_BE); if(p <= oldp || p+1 > X.Sz()) return;  if(!X.c("FF",p+1)) return;
		oldp = p
	}
	return true;
}
if(!bDetected && isInfogramesRH2()) {
	sName = "Infogrames (RobHubbard2) module (.DUM&.INS)"; bDetected = 1;
	if(X.isVerbose()) sOption(outSz(sz),'sz:')
	//TODO get info
}


function isSpecialFX() {
	//from https://gitlab.com/uade-music-player/uade/-/raw/master/amigasrc/players/wanted_team/SpecialFX/Special%20FX_v2.asm
	if(!X.c("6000")) return;
	d2 = X.U16(2,_BE); if(d2 <= 0 || d2%2) return;  a1 = 2; a0 = 4;
	for(i = 0; i < 3; i++) { if(!X.c("6000",a0)) return;  t = X.U16(a0+2,_BE); a0 += 4; if(t <= 0 || t%2) return }
	a1 += d2;
	if(X.c("6000",a0)) {
		for(i = 0; i < 3; i++) { if(!X.c("6000",a0)) return;  t = X.U16(a0+2,_BE); a0 += 4; if(t <= 0 || t%2) return }
		if(X.c("6000",a0)) return;
	} else { //OldFor
		if(!X.c("6100",a1)) return; a1 += 4
	}
	if(!X.c("41F9",a1)) return; a1 += 6; if(!X.c("43F9",a1)) return;
	//TODO subsongs
	return true;
}
if(!bDetected && isSpecialFX()) {
	sName = "M.Cannon & J.Dunn's Special FX module (.JD)"; bDetected = 1
}


function isSpecialFXST() {
	//from https://gitlab.com/uade-music-player/uade/-/raw/master/amigasrc/players/wanted_team/SpecialFX_ST/src/Special%20FX%20ST_v3.asm
	if(X.Sz() < 2300) return;
	if(!X.c("00000000") && !X.c("0101") && !X.c("00000101") && !X.c("6000")) return;
	var a0 = 0, a1 = 140, a2 = X.Sz()-4;
	while(a0 < X.Sz()) { if(X.c("00090800",a0)) break; a0 += 2; if(a1 == a0) return }
	if(!X.c("01120900", a0+74) && !X.c("02240A00", a0+74*2) && !X.c("00090800", a0+74*3)
	  && !X.c("01120900", a0+74*4) && !X.c("02240A00", a0+74*5)) return;
	a0 = a1+1860;
	while(a0 < X.Sz()) { if(X.c("0EF80E10",a0)) break; a0 += 2; if(a0 >= a2) return }
	//init
	sz = -1; a1 = 0; var d1 = 10, d7 = 0, p1 = 0, p2 = 0;
	while(a1 < X.Sz()) { d0 -= 2; a1 += 2; if(X.c("E740",a1-2)) break } //Find1
	x = X.U16(a1-12,_BE);
	p1 = a1-4+X.U16(a1-4,_BE);
	while(a1 < X.Sz()) { //Find2
		if(X.c("101A234A",a1)) {
			a1 += d1 }
		else if(X.c("4880D040",a1)) break; //noSpec, break to OK2
		a1 += 2
	}
	p2 = a1-2+X.U16(a1-2,_BE); //useful
	while(a1 < X.Sz()) { if(X.c("08C70007",a1)) break; a1 += 2 } //Find3
	a2 = a1; if(a1 >= X.Sz()) return false;
	while(a2 > 0) { a2 -=2; if(X.c("41FA",a2)) break } //Find4
	while(a1 < X.Sz()) //Find5
		if(X.c("41F9",a1)) { a2 = X.U16(a1+2,_BE); break }
		else { t = X.c("41FA",a1); a1 += 2; if(t) { a2 = a1+X.U16(a1,_BE); break }
	}
	while(a1 < X.Sz()) { if(X.c("08870002",a1)) break; a1 += 2 } //Find6
	a2 = a1+6; //OK6
	if(X.c("41F9",a2-2)) { a2 = X.U16(a2,_BE); d7 = a2 }
	else a2 += X.U16(a2,_BE);
	while(a1 < X.Sz()) { if(X.c("08C70002",a1)) break; a1 += 2 } //Find7
	a1 += d1; a2 = a1;
	if(X.c("41F9",a2-2)) a2 = X.U16(a2,_BE); else a2 += X.U16(a2,_BE);
	while(a1 < X.Sz()) { t = X.c("41FA",a1); a1 += 2; if(t) break } //Find8
	a2 = a1+X.U16(a1,_BE);
	var skip = 0; bad = '';
	while(a1 < X.Sz()) { if(X.c("0EF80E10",a1)) { skip = 1; break } if(X.c("7000101A",a1)) break; a1 += 2 } //Find9
	if(!skip) {
		a2 = a1-2+X.U16(a1-2,_BE);
		while(a1 < X.Sz()) //FindA
			if(X.c("0EF80E10",a1)) { skip = 1; break }
			else { t = X.c("41FA",a1); a1 += 2; if(t) break }
		a2 = a1; a1 += X.U16(a1,_BE);
	}
	if(!skip) if(a1 > X.Sz()) { bad = bad.addIfNone('!short'); skip = 2 }
	if(!skip) { sz = a1; if(!d7) skip = 1 }
	if(!skip) while(a2 < X.Sz()) { //FindEnd
		if(X.c("41FA",a2)) sz = a1 = a2+2+X.U16(a2+2,_BE)+14;
		if(!X.c("110010",a2)) break; else a2 += 2;
	}
	//EndTable skipped -> Skip2
	if(skip != 2) {
		d0 = p2-p1;
		if(d0 > 0) d0 >>= 3;
		if(d0 > 0) x = d0
	}
	return true
}
if(!bDetected && isSpecialFXST()) {
	sName = "Special FX ST module (.DODA)"; bDetected = 1;
	if(bad != '') sVersion = sVersion.appendS('malformed'+bad,'/');
	if(X.isVerbose()) {
		if(x > 1) sOption(x,'×');
		if(sz != -1) sOption(outSz(sz),'sz:')
	}
}


function isTronicDP() {
	a0 = d1 = 0x10+X.U16(2,_BE)+X.U16(6,_BE)+X.U16(10,_BE)+X.U16(14,_BE);
	if(a0 > X.Sz() || a0 % 2) return;
	a0 += X.U16(a0,_BE); if(a0 % 2) return;
	if(!X.c("005800B0",a0+4)) return;
	return true
}
if(!bDetected && isTronicDP()) {
	sName = "Tronic Delta Packer module (.DP)"; bDetected = 1; sOption('in:TronicTracker(?)')
}


function isDigitalSonixChrome() {
	//ref UADE player RE
	if(!X.U16(0)) return;  if(!(smp=X.U8(2))) return;  if((ord=X.U8(3)) < 2) return;
	d2 = smpsz = X.U32(4,_BE); if(!d2 || d2%2 || d2 > X.Sz() || d2 > 0x80000) return;
	d3 = X.U32(8,_BE); if(d3 > 0x20000) return;
	a0 = 0xC; x = 0;
	for(i=ord-1; i; i--) {
		d4 = X.I32(a0,_BE); if(d4 < 0 || d4 > 0x20000 || d4%2) return false; if(!X.U8(a0+4)) x++; a0 += 6
	}
	if(!X.c("00000000 0000",a0)) return;  a0 += 6; //@d2de~e4
	d3 <<= 2; a0 += d3; //d2e6~8
	a2 = a0+smp*0x12; //d2ea~f4
	s = 0;
	while(a0 < X.Sz() && a0 < a2) {
		d1 = X.I32(a0+2,_BE); if(!isWithin(d1, 0,d2)) return;
		d0 = X.U32(a0+0xC,_BE); if(d0 > d2) return;
		if(X.U8(a0+0x10) > 0x40 || X.U8(a0+0x11)) return; //this check's not in the player
		a0 += 0x12;
	}
	if(a0 > X.Sz() || d2 != d0+X.I32(a0-0x10,_BE)) return;
	sz = a0+smpsz;
	return true
}
if(!bDetected && isDigitalSonixChrome()) {
	sName = "Andrew Bailey & David Hanlon's Digital Sonix & Chrome module (.DSC)"; bDetected = 1;
	if(X.isVerbose()) {
		if(x > 1) sOption(x,'×');
		sOption('ord:'+ord+' smp:'+smp+' sz:'+outSz(sz))
	}
}


function isPaulShields() {
	//ref https://gitlab.com/uade-music-player/uade/-/raw/master/amigasrc/players/wanted_team/PaulShields/src/Paul%20Shields_v3.asm
	if(!X.c("00000000 0000000 0000")) return;
	if((t = X.U16(0xA4)) === X.U16(0xA8) && t === X.U16(0xAC) && t === X.U16(0xB0)
	  && (t = X.I16(0xA0,_BE)) > 0 && !(t%2) && X.c("00B400B6",t)) { fmt = 1; p = 0xA6 }
	else if((t = X.U16(0x204)) === X.U16(0x208) && t === X.U16(0x20C) && t === X.U16(0x210)
	  && (t = X.I16(0x200,_BE)) > 0 && !(t%2) && X.c("02140216",t)) { fmt = -1; p = 0x206 }
	else if((t = X.U16(0x202)) === X.U16(0x206) && t === X.U16(0x20A) && t === X.U16(0x20E)
	  && (t = X.I16(0x204,_BE)) > 0 && !(t%2) && (X.c("FFEC",t-2) || X.c("FFE8",t-2))) { fmt = 0; p = 0x204 }
	else return;
	mp = 0; a3 = 0;
	for(i=0; i < 4; i++) { t = X.U16(p,_BE); if(mp < t) mp = t; p += 4 }
	a3 += mp; mp = X.U16(a3-2,_BE); while(a3 < X.Sz()) { t = X.U16(a3,_BE); a3 += 2; if(t == mp) break }
	smpp = a3; if(!isWithin(smpp,0x400,0x4000)) return;
	if(fmt == 1) a3 = X.I16(0xAA,_BE); else if(fmt == 0) a3 = X.I16(0x204,_BE); else a3 = X.I16(0x20A,_BE);
	ord = 0; while(a3 < X.Sz()) { t = X.U16(a3,_BE); a3 += 2; if(t == mp) break; ord++ }
	if(!isWithin(ord,1,0x80)) return;
	for(i=p=smp=smpsz=0; i < 0x10; i++) {
		p += 2; if(fmt != 1) p += 20; if(t = X.U16(p,_BE)) { smpsz += t; smp++ } p += 8; if(fmt != 1) p += 2;
	}
	smpsz <<= 1; if(!smp || !isWithin(smpsz,0x2000,0x20000)) return;
	sz = smpp+smpsz;
	return true;
}
if(!bDetected && isPaulShields()) {
	sName = "Paul Shields' module (.PS)"; sVersion = 'f.'+fmt; bDetected = 1;	
	if(X.isVerbose()) {
		sOption('ord:'+ord+' smp:'+smp+' smpsz:'+Hex(smpsz)+' sz:'+outSz(sz))
	}
}


function isROL() {
	// ref https://moddingwiki.shikadi.net/wiki/ROL_Format
	// the "signature" can contain filenames and (cut off) software names
	if(X.Sz() < 0xB6+(45*15+4+2) || !isWithin(X.U32(0), 0x30000,0x40000) || !X.c("'Tempo'00", 0xB6)) return;
	if(!isWithin(X.U16(0x2C), 1,0x30) || !isWithin(X.U16(0x2E), 1,0x10)) return;
	if(!isWithin(X.U16(0x30), 0x10,0x50) || !isWithin(X.U16(0x32), 0x20,0x50)) return;
	if(!isWithin(X.U8(0x34), 0,1) || (mode=X.U8(0x35)) > 1) return;
	bpm0 = X.F16(0xC5);
	q = true;
	ch = voices = X.U8(0x35)? 9: 11; ev = X.U16(0xC7);
	for(i=mclk=0,p=0x36; i < 45; i++,p+=2) {
		if(i < 11) { if(!X.U16(p)) ch--; mclk = Math.max(mclk, X.U16(p)) }
	}
	for(i=0x36; i < 0x4C && q; i += 2) if(X.U16(i) > 0x10FF) q = false;
	for(; i < 0x90 && q; i += 2) if(X.U16(i) > 0x1FF) q = false;
	if(!q) return;
	p = 0xCB+X.U16(0xC9)*6; //skip tempo events
	mtln = mdur = notev = insev = volev = pitev = 0;
	for(i=0; i < voices; i++) {
		p += 0xF; dur = 0;
		tln = X.U16(p); p += 2;
		if(tln) do { dur += X.U16(p+2); p += 4; notev++ } while (dur < tln && p < X.Sz()); //load note events
		if(tln > mtln) mtln = tln; if(dur > mdur) mdur = dur;
		p += 0xF;
		insev += (t=X.U16(p)); p += 2+14*t+0xF; //skip ins.events
		volev += (t=X.U16(p)); p += 2+6*t+0xF; //skip vol.events
		pitev += (t=X.U16(p)); p += 2+6*t; //skip pitch events
	}
	sz = p;
	return true;
}
if(!bDetected && isROL()) {
	sName = "Ad Lib's AdLib Visual Composer pianoroll (.ROL)"; bDetected = 1;
	if(X.isVerbose()) {
		c = X.SA(4,40); if(c != '\\roll\\default') sOption(c);
		sOption('ch:'+(ch!=voices?ch+'/':'')+voices+' rhythm:'+X.U16(0x2C)+'/'+X.U16(0x2E)
			+' bpm0:'+bpm0.toFixed(1)+' len:'+mdur+' notes:'+notev+' ins.ev:'+insev
			+' ins.ev:'+insev+' vol.ev:'+volev+' pitchev:'+pitev+' sz:'+outSz(sz))
	}
}


function isGYMX() {
	if(X.Sz() < 0x1AC || !X.c("'GYMX'")) return;
	for(i=4; i < 0x1A8; i++) if(isWithin(X.U8(i), 1,0xA) || isWithin(X.U8(i),0xE,0x1F)) return;
	unpsz = X.U32(0x1A8);
	if(unpsz <= 2) if(parseMDGYM(0x1AC, X.isDeepScan() ? BCParseToEoF : BCParseToReasonable) <= 0)
		return;
	return true
}
if(!bDetected && isGYMX()) {
	sName = "Sega Genesis/Mega Drive YM2612 chiptune (.GYM)"; bDetected = 1;
	if(unpsz <= 2) sVersion = sVersion.append('unpacked');
	if(X.isVerbose()) {
		sOptionT(X.SC(0x4,0x20,'CP1252'));
		sOptionT(X.SC(0x24,0x20,'CP1252'),'for: ');
		sOptionT(X.SC(0x44,0x20,'CP1252'),'at: ');
		sOptionT(X.SC(0x64,0x20,'CP1252'),'emu: ')
		sOptionT(X.SC(0x84,0x20,'CP1252'),'by: ');
		sOptionT(X.SC(0xA4,0x100,'CP1252'));
		if(unpsz > 2) sOptionT(X.U32(0x1A8),'unp.sz:')
	}
}


function isSoundMaster() {
	//from https://gitlab.com/uade-music-player/uade/-/raw/master/amigasrc/players/wanted_team/SoundMaster/Sound%20Master_v1.asm
	if(!X.c("6000.... 6000.... 6000") || (p=base=a6=X.I16(2,_BE)+2) <= 0 || p%2
		|| (playp=X.I16(6,_BE)+6) <= 0 || playp%2) return;
	for(p1 = p+30; p < p1; p += 2) if(X.c("47FA",p-2)) break;  if(p >= p1) return;
	var lea1 = p; t = Math.min(X.Sz(),0x20000);
	for(; p < t; p += 2) if(X.c("4E75",p-2)) break;  if(p >= X.Sz() || p >= t) return;
	fmt = 'old'; if(X.c("177C0000",p-8)) { fmt = 'new'; p -= 6 }
	if(!X.c("00BFE001",p-6)) return;

	p = base; x = 1; d0 = 0;
	if(X.c("1740",p+6) || X.c("1740",p+4)) {
		for(; p < X.Sz(); p += 2) if(X.c("47FA",p-2)) break;  p += X.I16(p,_BE); a6 = p;
	} else skipold: do { //a one-time "loop" to have a way to break off
		if(X.c("3C00",p)) {
			p = lea1; a3 = a6 = p+X.I16(p,_BE);
			for(; p < X.Sz(); p += 2) if(X.c("7600",p-2)) break;  a3 += X.I16(p-4,_BE)+3; d0 = 7;
			for(; d0; a3+=3,x++,d0--) {
				if(!X.U16(a3,_BE) && X.U8(a3+2) == 1) break;
			}
			d0 = X.U8(a3-1);
			break skipold;
		} else
			if(X.c("4A00",p+0x28)) { x++; d0 = X.I16(p+0x34,_BE) }
		for(; p < X.Sz(); p += 2) if(X.c("47FA",p-2)) break;  p += X.I16(p,_BE); a6 = p;
	} while(0);
	ord = d0;

	for(p = 0xC; p < X.Sz(); p += 2) if(X.c("1743",p-2)) break; a3 = a6; pos = a6+X.I16(p,_BE);
	for(p = 0xC; p < X.Sz(); p += 2) if(X.c("5203",p-2)) break;
	sz = -1; patchable = X.c("177C", p+10);
	if(patchable) {
		a2 = base; a3 += X.I16(p+2,_BE);
		if(!ord) ord = X.U8(a3);
		if(fmt == 'new') {
			for(p = base+2; p < X.Sz(); p += 2) if(X.c("41EB",p-2)) break;
			a3 = a6; a6 += X.I16(p,_BE); a3 += X.I16(p+4,_BE); a6 += X.I32(a3,_BE); smpip = a6;
			songsz = a6+X.I16(a3-2,_BE);
			smp = smpsz = 0;
			for(;a6 < X.Sz();) {
				d3 = X.I32(a6,_BE); a6 += 4;
				if(d3 > 0 && (ssz = X.U16(a6,_BE)<<1)) {
					smp++; d3 += ssz;
					if(smpsz < d3) smpsz = d3;
				}
				a6 += 6; if(a6 >= songsz) break
			}
		} else { // old format
			for(p = base+2; p < X.Sz(); p += 2) if(X.c("3D70",p-2)) break;
			for(; p < X.Sz(); p += 2) if(X.c("D5F0",p-2)) break;
			a3 = a6+X.I16(p-4,_BE); a6 += X.I32(a3,_BE); t = a6; a6 += X.U16(p-0x12,_BE);
			smpip = a6; smp = smpsz = 0; a3 = a6+0x80;
			for(d1 = 32; d1; d1--) {
				if(X.U16(a3,_BE) > 1) {
					smp++; ssz = X.U16(a3,_BE)<<1; d3 = ssz+X.I32(a6,_BE); if(smpsz < d3) smpsz = d3;
				}
				a6 += 4; a3 += 2
			}
			songsz = t+X.U16(p-8,_BE);
		}
		sz = songsz+smpsz
	}
	return true
}
if(!bDetected && isSoundMaster()) {
	sName = "Sound Master module (.SM,.SMPRO,.SM3)"; sVersion = fmt; bDetected = 1;
	if(X.isVerbose()) {
		if(x > 1) sOption(x,'×');
		if(patchable) sOption('ord:'+ord+' sz:'+outSz(sz));
		//else sOption('unpatchable or precooked');
	}
}


function isTomyTracker() {
	//from https://gitlab.com/uade-music-player/uade/-/raw/master/amigasrc/players/wanted_team/TomyTracker/src/Tomy%20Tracker_v2.asm
	if(X.Sz() < 0x6C0) return;
	if((sz=X.U32(0,_BE)) > 0x200000 || sz%2) return;
	if((d2=X.U32(4,_BE)) > sz || d2%2) return;
	d2 -= 0x2C0; if(d2 & 0x3FF) return;
	ptn = d2 >>= 10; d2--; p = 0x1BC; if(X.U8(p)) return;
	ord = d1 = X.I8(p+1); if(ord < 0) return;  p += 4; d2 <<= 10; d3 = 0;
	for(; d1; p+=2,d1--) { if((t=X.I16(p,_BE)) & 0x3FF) return;  if(d3 < t) d3 = t }
	if(d2 != d3) return;
	for(smp = 0, p = 14; p < 0x1C0; p += 0xE) if(X.U16(p,_BE)) smp++;
	return true
}
if(!bDetected && isTomyTracker()) {
	sName = "Tom Pakarinen's Tomy Tracker module (.SG)"; bDetected = 1;
	if(X.isVerbose()) {
		sOption('ord:'+ord+' ptn:'+ptn+' smp:'+smp+' sz:'+outSz(sz))
	}
}


function isSoundcontrol() {
	//from https://gitlab.com/uade-music-player/uade/-/raw/master/amigasrc/players/wanted_team/Soundcontrol/src/Soundcontrol_v3.asm
	if(!X.c("0003",0x20))
		if(!X.c("0002",0x20) || X.U32(0x1C,_BE)) return;
	if(X.U16(0x10,_BE)) return;
	if((p = X.I16(0x12,_BE)) < 0 || p % 2) return;
	if(!X.c("FFFF0000 0400",p+0x3E)) return;
	title = X.readBytes(0,0x10); if(charStat(title,1).indexOf('allasc') < 0) return;
	title = decEncoding(title,"CP437");
	a2 = 0; a3 = sz = 0x40+X.U32(0x10,_BE);
	d2 = X.U32(0x14,_BE); sz += d2; a4 = sz;
	d1 = X.U32(0x18,_BE); sz += d1+X.U32(0x1C,_BE); d2 -= 0x400;
	if (X.Sz() < sz) return;
	ord = d1 = Util.divu64(d1,12); d1--;
	voc = 0;
	for(d3 = 6; d3; d3--,a4 += 2)  for(p = a4,d4 = ord; d4; d4--,p += 12)  if(X.U16(p,_BE)) { voc++; break }
	a3 += 0x43C; smp = d3 = 0;
	while(d2 > d3) { d4 = X.U32(a3,_BE); a3 += d4; d3 += d4; smp++ }
	x = 1; nv = 0; sv = '';
	if(X.U32(0x1C,_BE)) //crude fixes, go
		switch(sz) {
		case 95960: x = 2; //hndongame2; fallthrough
		case 54544: sv = 'v4.0'; break; //hndtitle
		case 81906: sv = 'v5.0'; break //hndintro
		}
	else
		switch(sz) {
		case 126446: x = 3; sv = 'v3.0'; break; //number9
		case 136612: x = 2; sv = 'v3.0'; break; //domination 1
		case 154704: x = 2; //dynatsong; fallthrough
		case 103808: sv = 'v3.2'; break; //eleven6
		}
	for(p = 0x40, ptn = 0; p < 0x240; p += 2) if(X.U16(p,_BE)) ptn++;
	return true
}
if(!bDetected && isSoundcontrol()) {
	sName = "Holger Gehrmann's Soundcontrol module (.SCT)"; sVersion = sv; bDetected = 1;
	if(X.isVerbose()) {
		sOptionT(title); if(x > 1) sOption(x,'×');
		sOption('ch:'+voc+' ord:'+ord+' ptn:'+ptn+' smp:'+smp+' sz:'+outSz(sz))
	}
}


function isSoundfactory() {
	//from https://gitlab.com/uade-music-player/uade/-/raw/master/amigasrc/players/wanted_team/Soundfactory/src/Soundfactory_v2.asm
	if((sz=X.U32(0,_BE)) > 0x200000 || sz > X.Sz()) return;
	x = 16; lastfound = false;
	for(p=0x13; p >= 4; p--) { if((t=X.U8(p)) > 0xF) return; if(!lastfound && t) {x = p-4; lastfound = true } }
	m = X.U32(0x14,_BE);
	for(p = 0x18; p < 0x114; p+=4) { if((t=X.U32(p,_BE)) > 0x200000 || t > sz) return false; if(m > t) m = t }
	if(m != 0x114) return;
	if(X.isDeepScan()) {
		p = 0x114; smp = smpsz = 0;
		for(;p < sz-8;) {
			//seek 84h at an even offset
			do { q = X.fSig(p,sz-8-p,"84"); if(q < 0) { p = sz; continue} else p = q+(q%2) } while(p < sz && !q%2);
//_log("found "+Hex(q))
			if(p < sz && (d1=X.U8(p+1)) <= 0x1F) {
				d1 >>= 2; smpp[d1] = p;
				d4 = X.U16(p+2,_BE)<<1; smpsz += d4-38; smp++; p += d4
			} else p += 2;
		}
		if(!smp || smp > 32) return;
	}
	return true
}
if(!bDetected && isSoundfactory()) {
	sName = "Soundfactory module (.PSF)"; bDetected = 1;
	if(X.isVerbose()) {
		if(x > 1) sOption(x,'×');
		sOption((X.isDeepScan()?'smp:'+smp+' smpsz:'+Hex(smpsz)+' ':'')+'sz:'+outSz(sz))
	}
}


function isBeniTracker() {
	//from https://modland.com/pub/software/trackers/DOS/Beni%20Tracker/Beni%20Tracker%20v1.8.rar / BT18.BAS -> SUB loadmod
	//ref BT18.DOC & https://modland.com/pub/software/trackers/DOS/Beni%20Tracker/Beni%20Tracker%20v1.3.rar / BeniTrk.doc
	ord = X.U8(0); ptn = X.U8(1); ins = X.U8(2);
	if(!ptn || ptn > 0xCC || ins > 0x1F) return;
	var ptnmap = [], insmap = [];
	p = 3; mptn = mins = 0;
	for(i=0; i < ptn; i++) {
		if((t = X.U8(p++)) > 0xCB) return;  if(mptn < t) mptn = t;
		if(ptnmap.indexOf(t) < 0) ptnmap.push(t) }
	if(ins) {
		for(i=0; i < ins; i++) {
			if(!isWithin(t = X.U8(p++),1,0x1F)) return;  if(mins < t) mins = t;
			if(insmap.indexOf(t) < 0) insmap.push(t) }
	}
	rptn = ptnmap.length; rins = insmap.length; mptn++;
	if(rptn != ptn || (ins && rins != ins)) return;
	for(o = 0; o < ord; o++)
		for(i=0; i < 9; i++)
			if(ptnmap.indexOf(X.U8(p++)) < 0) return;
	notes = 0;
	for(i=0; i < ptn*0x40; i++,p+=3) {
		var nt = X.U8(p) >> 4, ns = (X.U8(p) & 1) << 4 | X.U8(p+1) >> 4;
		if(nt > 12 || ns && insmap.indexOf(ns) < 0) return;
		if(nt != 12) notes++;
	}
	p += ins*0xB;
	if(X.c("'B.J.'",p)) { p += 4; sv = 'v1.8' } else sv = 'v1.3';
	if(sv == 'v1.3' && ptn > 100) return;
	sz = p; return true;
}
if(!bDetected && isBeniTracker()) {
	sName = "SPP's Beni Tracker (Adlib) module (.PIS)"; sVersion = sv; bDetected = 1;
	if(X.isVerbose()) {
		sOption('ord:'+ord+' ptn:'+ptn+(ptn!=mptn?'/'+mptn:'')+' ins:'+ins+(ins!=mins?'/'+mins:'')
			+' notes:'+notes+' sz:'+outSz(sz))
	}
}


function isSteveBarrett() {
	//ref reversing the eagleplayer and the module's player code
	for(p=0; p < 0x10; p += 4) if(!X.c("6000",p) || (t=X.U16(p+2,_BE)) > X.Sz() || t%2) return false;
	
	t = X.I16(6,_BE)+6; if(!X.c("49FA....1940....4E75 43FA.... 49FA.... 41FA.... 45FA",t)
		|| !X.c("'FORM'66",t+0x1C)) return false;
	t = X.I16(10,_BE)+10;
	if(!X.c("49FA....4CFA00FF",t) || !X.c("103A.... 660C 1940.... 6100... .6000",t+10)
		|| !X.c("2A4C DAD8 22CD 2A4C DAD8 22CD 2A4C DAD8 22CD",t+0x34)) return false;
	t = X.I16(0xE,_BE)+0xE;
	if(!X.c("70002A7C 00DFF0A8 3A803B40",t) || !X.c("1A801B40 00011B40 00021B40",t+0x1A)) return false; 
	initp = X.I16(2,_BE)+2; if(!X.c("43FA....49FA....41FA....45FA",initp) || !X.c("'FORM'66",initp+0x12)) return false;
	a0 = 0;
	for(p = initp; p < initp+0x10; p += 2) //find the sample ptr
		if(X.c("41FA",p-2)) { a0 = p+X.I16(p,_BE); break }
	if((p=6+X.fSig(initp,0x1000,"E7404281")) < 6) return false; t = p+X.I16(p,_BE); //possibly orderlist ptr
	for(; p < X.Sz(); p += 2) if(X.c("41FA",p-2)) break;
	for(; p < X.Sz(); p += 2) if(X.c("D08043FA",p)) break; p += 4; p += X.I16(p,_BE);
	x = (p-t)>>3;
	for(p = a0,smp=0; p < X.Sz(); smp++) {
		if(!X.c("'FORM'........'8SVXVHDR'",p)) { if(!X.U32(p)) p += 4; break }
		p += 8+X.U32(p+4,_BE);
	}
	sz = p; if(!smp) return false;
	return true;
}
if(!bDetected && isSteveBarrett()) {
	sName = "Steve Barrett's module (.SB)"; bDetected = 1;
	if(X.isVerbose()) {
		if(x > 1) sOption(x,'×');
		sOption('smp:'+smp+' sz:'+outSz(sz))
	}
}


function isQuartetPSG() {
	// ref the eagleplayer RE
	if(X.Sz() < 0x400 || !X.c("49FA",0x10)) return false;
	for(p=0; p < 0x10; p += 4)
		if(!X.c("6000",p) || (t=X.I16(p+2,_BE)) <= 0 || t%2) return false;
	if(!X.c("48E7FFFE 4DFA.... 51EE.... 41FA",p=2+X.I16(2,_BE))) return false; 
	if(!X.c("48E7FFFE 4DFA.... 4A2E.... 6700.... 70033F00 49FA",p=6+X.I16(6,_BE))) return false; 
	if(!X.c("48E7FFFE 4DFA.... 51EE.... 6100",p=10+X.I16(10,_BE))) return false; 

	for(p = 0x10; p < X.Sz(); p += 2) if(X.c("40C2",p)) break;
	for(; p < X.Sz(); p += 2) if(X.c("41FA",p)) break;
	p += 6; sz = p+X.I16(p,_BE); p += 2;
	for(; p < X.Sz(); p += 2) if(X.c("43E9",p-2)) break;
	sz += X.I16(p,_BE) << 1; a3 = sz; if(sz > X.Sz()) return false;
	for(a0 = 0; p < X.Sz(); p += 2) {
		if(X.c("206C0032",p)) { a0 += 8; p += 4 } if(X.c("08380007",p) || X.c("08390007",p)) break
	}
	for(; p < sz; p += 2) if(X.c("40C1",p)) break;
	for(p += 4; p < X.Sz(); p += 2) if(X.c("4CDF",p)) break;
	for(; p < sz; p += 2) if(X.I32(p,_BE) == 0x56) break;
	for(x = 0; p < a3; p += 2) if(X.I16(p,_BE) == 0x46) x++;
	x >>= 2;
	return true
}
if(!bDetected && isQuartetPSG()) {
	sName = "Illusions/Microdeal Quartet module (SQT.)";
	sVersion = "PSG synth"; bDetected = 1;
	if(X.isVerbose()) {
		if(x > 1) sOption(x,'×');
		sOption(outSz(sz),'sz:')
	}
}


function isSonicArranger() {
	//from https://gitlab.com/uade-music-player/uade/-/raw/master/amigasrc/players/wanted_team/Sonic_Arranger/Sonic Arranger_v1.asm
	p = modp = smp = synsmp = ord = ptn = 0; x = 1; sv = bad = ''; owner = [];
	if(X.c("'SOARV1.0STBL'")) {
		x = X.I32(0xC,_BE); spd0 = X.U8(0x11); ptnlen0 = X.U8(0x13); pst0 = X.U16(0x14,_BE);
		ped0 = X.U16(0x16,_BE); lp0 = X.U16(0x18,_BE); irqps0 = X.U16(0x1A,_BE);
		if(!isWithin(spd0,2,16) || !isWithin(irqps0,16,120) || pst0 > ped0 || lp0 > ped0) return false;
		p = 0x10+x*12; if(!X.c("'OVTB'",p)) return false; //"OverTable"
		ord = X.I32(p+4,_BE); if(ped0 > ord) return false;
		p += 0x18+(ord << 4); if(!X.c("'NTBL'",p-0x10)) return false;
		ntbl = X.I32(p-0xC,_BE); p += ntbl << 2; if(!X.c("'INST'",p-8)) return false;
		ptn = Util.divu64(ntbl,0x10); ins = X.I32(p-4,_BE); inss = [];
		for(i=0; i < ins; i++,p+=0x98) {
			if(X.U16(p,_BE)) synsmp++; else smp++;
			t = X.readBytes(p+0x7A,0x1E,true);
			t = decEncoding(t,CPAmiga).trim(); if(t != '') inss.push(t)
		}
		var sc;
		if(!X.c("'SD8B'",p)) return false; smp = X.I32(p+4,_BE); sc = smp; p += 8+smp*0x26;
		pp = p; p += smp << 2; for(; sc-- && p < X.Sz(); pp += 4) p += X.U32(pp,_BE);
		if(!X.c("'SYWT'",p)) return false; synsmp = X.I32(p+4,_BE); p += 8+(synsmp << 7);
		if(!X.c("'SYAR'",p)) return false; t = X.I32(p+4,_BE); p += 8+(t << 7);
		if(!X.c("'SYAF'",p)) return false; t = X.I32(p+4,_BE); p += 8+(t << 7);
		if(!X.c("'EDAT'",p)) return false; sz = p+24; sv = 'song';
		} else {
		if(X.c("4EFA")) { //detect & skip replay
			if((t=X.I16(2,_BE)) <= 0 || t%2) return false;
			if(!X.c("48E7FFFE 41FA",t+2) || !X.c("201045F0 0800228A 43FA",t+0xE)
				|| !X.c("20280004 45F00800 228A 43FA",t+0x1A)) return false;
			if((p=X.I16(t+8,_BE)) <= 0 || p%2) return false;
			p += t+8; modp = p; sv = '+replayer';
		} else sv = 'pure';
		if(p+0x28 > X.Sz() || !X.c("00000028",p)) return false;
		spd0 = X.U16(p+0x28,_BE); irqps0 = X.U16(p+0x32,_BE); lp0 = X.U16(p+0x30,_BE);
		if(!isWithin(spd0,2,16) || !isWithin(irqps0,16,120)) return false;
		x = (X.I32(p+4,_BE)-0x28)/12; if(x == 1 || X.I16(p+0x3C,_BE) < 0) x = 1;
		var ptrs = [0];
		for(i=0; i < 8; i++) {
			t = X.I32(p,_BE); p += 4; if(t <= 0 || t%2 || t < ptrs[ptrs.length-1]) return false; ptrs.push(t)
		}
		var ordp = ptrs[2], ptnp = ptrs[3], insp = ptrs[4], a = ptrs[5];
		sc = X.I32(modp+t,_BE);
		//ord recs contain u16 references and a couple control bytes ST and NT
		ord = Util.div64(ptnp-ordp,0x10); ptn = Util.div64(insp-ptnp,0x40); ins = Util.div64(a-insp,0x98);
		p = modp+t; if(p > X.Sz()) return false;
		for(i=0; i < ins; i++) if(X.U16(modp+insp+i*0x98,_BE)) synsmp++; else smp++;
		if(smp != sc) bad = bad.addIfNone('!inconsistentsmpcnt'+sc); //be cool to understand how that happens...
		p += 4;
		if(smp) for(pp=p, p+=sc<<2; sc-- && p < X.Sz(); pp += 4) p += X.U32(pp,_BE);
		if(!X.c("'deadbeef'",p)) if(X.isHeuristicScan()) bad = bad.addIfNone('!badeof'); else return false;
		p += 12; sc = p; //skip 'deadbeef    '; sc is now for counting the string length
		// There's no real need to include the "programowner" info into calcsize but let's;
		// otherwise the eagleplayer thinks the file is "too short", although nobody really cares...
		for(lim = Math.min(p+0x400,X.Sz()); p < lim;) {
			c = X.U8(p++)^0xFF; if(c == 0xFF || isWithin(c,0,8) || isWithin(c,11,12) || isWithin(c,14,0x1F)) break;
			if(c == 0xA) { c = 0x20; if(owner[owner.length-1] == 0x20) continue }
			owner.push(c)
		}
		if(owner[owner.length-1] == 0x20) owner.pop();
		sz = p;
		if(c != 0xFF && p-sc != 0x50) bad = bad.addIfNone('!badinfo');
	}
	bpm0 = (15*irqps0/spd0).toFixed(1); owner = decEncoding(owner,CPAmiga).trim();
	return true
}
if(!bDetected && isSonicArranger()) {
	sName = "BrainTrace Design's Sonic Arranger module (.SA"+(sv == 'pure'?',.LION':'')+")"; bDetected = 1;
	sVersion = sv; if(bad != '') sVersion = sVersion.appendS('malformed:'+bad,'/');
	if(X.isVerbose()) {
		if(x > 1) sOption(x,'×');
		if(owner != '') sOption(addEllipsis(owner,0xA0,0x80),'info:"','"');
		sOption('bpm0:'+bpm0+(ord?' ord:'+ord:'')+(lp0?' lp0:'+lp0:'')+(ptn?' ptn:'+ptn:'')+(ins?' ins:'+ins:'')
			+(smp?' wf.smp:'+smp:'')+(synsmp?' syn.smp:'+synsmp:'')+' sz:'+outSz(sz))
	}
}
if(!bDetected && X.c("'SASI'00000000000100")) {
	sName = "BrainTrace Design/MEDIA Verlags Sonic Arranger synth instrument"; bDetected = 1
}


function isJT() {
	if(X.Sz() < 1700) return;
	for(p = 0; p < 40; p += 2) if(X.c("02390001",p)) break; if(p == 40) return;
	p += 8; if(!X.c("66..4E75",p++)) return;  if((t = X.I8(p++)) <= 0) return;
	p += t; if(X.c("4A39",p))  for(j=4; j--; p += 0x12) if(!X.c("4A39",p)) return;
	if(!X.c("78001839",p)) return;
	for(j = p;;) { p = X.fSig(j,0x40000,"1400E302"); if(p < 0) return; else if(i % 2) { j = p+1; continue } else break }
	var org = X.U32(p+6,_BE);
	for(;;) { p = X.fSig(j,0x40000,"03580328"); if(p < 0) return; else if(i % 2) { j = p+1; continue } else break }
	org -= p;
	for(p=0; p < 0x40000; p += 2) if(X.c("B23C00FF",p) || X.c("0C0100FF",p)) break;  if(p >= 0x40000) return;
	for(; p < 0x40000; p += 2) if(X.c("267C",p-2)) break;  if(p >= 0x40000) return; p += 4; //?
	for(; p < 0x40000; p += 2) if(X.c("49F9",p-2)) break;  if(p >= 0x40000) return; smpip = X.U32(p,_BE)-org; p += 4;
	for(; p < 0x40000; p += 2) if(X.c("0026267C",p-4)) break;  if(p >= 0x40000) return; p += 4; //?
	for(; p < 0x40000; p += 2) if(X.c("23F4",p)) break;  if(p >= 0x40000) return;
	subsongp = X.U32(p-4,_BE)-org; p += 6; x = 0;
	for(p=subsongp; p < 0x40000; p += 0x12) {
		if(X.U8(p+16) != 12) break; t = X.U32(p,_BE);
		if(X.U32(p+4,_BE) != t || X.U32(p+8,_BE) != t || X.U32(p+12,_BE) != t) x++; else break
	}
	j = X.U32(smpip,_BE)-org; i = smpip+4; info = (j-i) >> 2;
	d0 = X.I32(j+4,_BE); d1 = X.I16(j+2,_BE)*2 + d0;
	for(smp = 1; i < j; i += 4) {
		t = X.U32(i,_BE)-org; d3 = X.U32(t+4,_BE); if(!d3) continue;
		d4 = X.I16(t+2,_BE)*2+d3; if(d3 < d0) d0 = d3; if(d4 > d1) d1 = d4; smp++
	}
	songsz = d0-org; sz = d1-org;
	for(i=0; i < 0x40000; i+=2) if(X.c("4E75",i-2)) break;
	specialmsg = ''; //I've no modules to test this on
	if((d1=X.U8(i-3)) != 2) specialmsg = X.SC(i,0x100,CPAmiga);
	return true;
}
if(!bDetected && isJT()) {
	sName = "Jeroen 'WAVE' Tel's module (.JT)"; bDetected = 1;
	if(X.isVerbose()) {
		if(specialmsg != '') sOption(specialmsg); if(x > 1) sOption(x,'×');
		sOption('smp:'+smp+' songsz:'+Hex(songsz)+' sz:'+outSz(sz))
	}
}


function isTCBTracker() {
	//ref ftp://ftp.scene.org/pub/resources/gotpapers/manuals/tcb_tracker_1.0_manual_1990.pdf
	if(X.Sz() <= 0x132 || !X.c("'AN COOL'") || (ptn = X.U32(8,_BE)) > 127 || (tmp0 = X.U8(0xC)) > 15
		|| X.U8(0xD) || (ord = X.I8(0x8E)) <= 0) return;
	if(X.SA(7,1) == '!') fmt = 0; else if(X.SA(7,1) == '.') fmt = 1; else return;
	if(!fmt) ptnp = 0x110; else ptnp = 0x132; smpp = ptnp+ptn*0x200; if(smpp+0xD4 > X.Sz()) return;
	if(!X.c("FFFFFFFF00000000",smpp+0xCC) || X.U32(smpp+0x44,_BE) != 0xD4) return;
	smp = 0; p = smpp+0x48;
	for(i=0; i < 16 && p < X.Sz(); i++) if(X.U32(p,_BE) != 1) smp++;
	sz = smpp+X.U32(smpp,_BE);
	return true
}
if(!bDetected && isTCBTracker()) {
		sName = "Anders 'AN Cool' Nilsson's TCB Tracker module (.TCB)"; sVersion = 'f.'+fmt; bDetected = 1;
		if(X.isVerbose())
			sOption('tempo:'+tmp0+' ord:'+ord+' ptn:'+ptn+' smp:'+smp+' sz:'+outSz(sz));
	}


function is669() {
	//from https://modland.com/pub/documents/format_documentation/Composer%20669,%20Unis%20669%20(.669).txt
	if(!X.c("'if'") && !X.c("'JN'") || X.Sz() < 0x1F1+0x600) return; //not easy to make the signature more deficient than this one!
	if((smp=X.U8(0x6E)) > 0x40 || (ptn=X.U8(0x6F)) > 0x80 || (lp=X.U8(0x70)) >= 0x80) return;
	for(ic=0,i=2; i < 0x6D; i++) if(isWithin(X.U8(i),1,31) && ++ic > 40) return;
	for(i=ord=0; i < 0x80; i++) { //from 71h: order list; from F1h: ptn tempos; from 171h: ptn breaks
		if((o=X.U8(0x71+i)) >= ptn && o < 0xFE) return; if(o < 0x80 && !(t=X.U8(0xF1+i))) return;
		if(t > 15 || X.U8(0x171+i) >= 0x40) return; if(o < 0xFE) ord++;
	}
	sz = 0x1F1+smp*0x19+ptn*0x600;
	if(X.Sz() < sz) return;
	for(i = rsmp = 0; i < smp; i++) { //smp filename may have custom messages (the lunar forest), but too rarely
		ssz = X.U32(0x1FE+i*0x19); slps = X.U32(0x202+i*0x19); slpe = X.U32(0x206+i*0x19);
		if(ssz) rsmp++; else continue;
		if(ssz > 0x4000000) return; //a sanity check from openmpt
		if([0xFFFFF,0xF0FFFF,0xFFFFFFFF].indexOf(slps) < 0 && slps > ssz) return;
		if([0xFFFFF,0xF0FFFF,0xFFFFFFFF].indexOf(slpe) < 0 && (slpe > ssz || slpe < slps)) return;
		sz += ssz;
	}
	return true
}
if(!bDetected && is669()) {
	sName = ( X.c("'if'") ? "Renaissance's Composer 669"
		: "Jason 'JsNO BAR----' Nunn's UNIS 669 Composer" ) + " module (.669)"; bDetected = 1;
	if(X.isVerbose()) {
		t = X.SC(0x02,36,'CP850').trim(); //the seemingly most-used encoding for'em
		t = t.appendS(X.SC(0x26,36,'CP850').trim(),' '); t = t.appendS(X.SC(0x4A,36,'CP850').trim(),' ');
		sOption(t); sOption('ord:'+ord+(lp?' loop:'+lp:'')+' ptn:'+ptn+' smp:'+smp+' sz:'+outSz(sz))
	}
}


function isBuzzic10() { // doesn't detect empty modules with no sound, as the format's already too sparse
	if(X.Sz() < 0xAEA8) return;
	susv = 0; // suspicious values like zero volume
	ins = X.U32(0x4A00); if(!ins || ins > 128) return;
	trk = X.U32(0x8E8C); if(!trk || trk > 64) return;
	//not that many tracks even fit on screen tho', and there's no horizontal scrolling, so it'd be much fewer
	//however, technically, it does have the format space even to fit 128 tracks. But only 64 names for them.
	ptn = X.U32(0x8E84); if(!ptn || ptn > 128) return;
	gvol = X.F32(0x8E94); if(gvol < 0 || gvol > 2) return; gvol = Math.round(gvol*100)+'%';
	bpm = X.U8(0x8E98); if(bpm < 10) susv++;
	start = X.U32(0x8E9C); ord = X.U32(0x8EA0);
	if(!ord) susv++; if(ord > 127 || start > ord) return; ord++;
	floop = X.U32(0x8EA4); if(floop > 1) return;
	for(i = 0; i < ins; i++) { //test instruments
		p = i*0x94; if(p+0x93 > Math.min(X.Sz(), 0x4A00)) return;
		var mute = X.U8(p+0x80); if(mute > 1) return;
		var note = X.I8(p+0x81); if(!note) susv++;
		if(note < 0 && -note > trk) return; //track link has to call a track
		var nend = X.I8(p+0x82); if(nend < 0 && -nend > trk) return;
		var len = X.U8(p+0x83); if(len > 127) return;
		var vol = X.I8(p+0x84); if(vol < 0 && -vol > trk) return;
		if(!len) susv++; if(!vol) susv++;
		var osc = X.U8(p+0x85); if(osc > 3) return;  if(osc < 3 && !note) susv++; //only noise doesn't need the note
		var dtn = X.I8(p+0x86); if(note > 0 && note+dtn <= 0) return;  if(dtn < -32 || dtn > 64) susv++;
		var atk = X.U8(p+0x87), dcy = X.U8(p+0x88), sus = X.U8(p+0x89);
		if(dcy < atk || sus < dcy || sus > 127 || atk > 127 || sus > 127) return;
		if(!atk && !dcy && !sus) susv++;
		var cut = X.I8(p+0x8A); if(cut < 0 && -cut > trk) return; //cutoff mod @ +0x8B
		var res = X.I8(p+0x8C); if(res < 0 && -res > trk) return;
		var dly = X.U8(p+0x8D), rep = X.U8(p+0x8E), damp = X.U8(p+0x8F);
		if(dly > 127 || rep > 127 || damp > 127) return;
		var lvwrap = X.I8(p+0x90);
		if(lvwrap < 0 && lvwrap > trk) return;
		var lvhard = X.I8(p+0x91);
		if(lvhard < 0 && lvhard > trk) return;
		var comprlo = X.U8(p+0x92), comprhi = X.U8(p+0x93);
		if((comprlo > 127 || comprhi > 127) || (!comprhi && comprlo)
			|| (comprhi && comprhi <= comprlo)) return; // they CAN both be 0...
	}
	if(susv > ins*3) { if(debug>0)_logIt("data too suspicious"); return }
	for(i = 0x4A04; i < 0x4E84; i++) if(X.U8(i) > 127) return; // testing patterns
	for(i = 0x4E84; i < 0x4E84+64*128; i++) //testing tracks... all 64 of'em
		if(X.U8(i) > ptn) { if(debug>0)_logIt("@"+Hex(i)+": bad ptn"); return }
	if(!isAllZeroes(0x4E84+64*128,64*128))
		return false; // the uneditable tracks would stay as 0, fair enough
	// there are absolutely no checks made on the text values so there's no meaning in checking those
	return true
}
if(!bDetected && X.isDeepScan() && isBuzzic10()) {
	sName = "Buzzic module (.BUZ)"; sVersion = 'v1.0'; bDetected = 1;
	sOption('trk:'+trk+' ord:'+ord+(start?' (from '+start+')':'')+' ptn:'+ptn+' ins:'+ins
		+' bpm:'+bpm+' gvol:'+gvol+' sz:44712')
}


function isBuzzic11() { //detection only slightly different from the above
	if(X.Sz() < 0xB0A8) return false;
	susv = 0;
	ins = X.U32(0x4C00); if(!ins || ins > 128) return;
	trk = X.U32(0x908C); if(!trk || trk > 64) return; // this editor has better limits on max tracks
	ptn = X.U32(0x9084); if(!ptn || ptn > 128) return;
	gvol = X.F32(0x9094); if(gvol < 0 | gvol > 1.91) return; gvol = Math.round(gvol*100)+'%';
	bpm = X.U8(0x9098); if(bpm < 10) susv++;
	start = X.U32(0x909C); ord = X.U32(0x90A0);
	if(!ord) susv++; if(ord > 127 || start > ord) return; ord++;
	floop = X.U32(0x90A4); if(floop > 1) return;
	for(i = 0; i < ins; i++) {
		p = i*0x98; if(p+0x97 > Math.min(X.Sz(), 0x4A00)) return;
		var mute = X.U8(p+0x80); if(mute > 1) return;
		var note = X.I8(p+0x81); if(!note) susv++;
		if(note < 0 && -note > trk) return;
		var nend = X.I8(p+0x82); if(nend < 0 && -nend > trk) return;
		var len = X.U8(p+0x83); if(len > 127) return;
		var vol = X.I8(p+0x84); if(vol < 0 && -vol > trk) return;
		if(!len) susv++; if(!vol) susv++;
		var osc = X.U8(p+0x85); if(osc > 3) return; if(osc < 3 && !note) susv++;
		var dtn = X.I8(p+0x86); if(note > 0 && note+dtn <= 0) return;  if(dtn < -32 || dtn > 64) susv++;
		var atk = X.U8(p+0x87), dcy = X.U8(p+0x88), sus = X.U8(p+0x89);
		if(dcy < atk || sus < dcy || sus > 127 || atk > 127 || sus > 127) return;
		if(!atk && !dcy && !sus) susv++;
		var cut = X.I8(p+0x8A); if(cut < 0 && -cut > trk) return;
		var res = X.I8(p+0x8C); if(res < 0 && -res > trk) return;
		var dly = X.U8(p+0x8D), rep = X.U8(p+0x8E), damp = X.U8(p+0x8F);
		if(dly > 127 || rep > 127 || damp > 127) return;
		var lvwrap = X.I8(p+0x90);
		if(lvwrap < 0 && lvwrap > trk) return;
		var lvhard = X.I8(p+0x91);
		if(lvhard < 0 && lvhard > trk) return;
		var comprlo = X.U8(p+0x92), comprhi = X.U8(p+0x93);
		if((comprlo > 127 || comprhi > 127) || (!comprhi && comprlo)
			|| (comprhi && comprhi <= comprlo)) return;
		var pan = X.I8(p+0x94); if(pan > 118) return; // funny—looks like an input filter typo, ah well
		var fltfreq = X.I8(p+0x95), fltres = X.I8(p+0x97); //frqmod @ +0x96
		if((fltfreq < 0 && -fltfreq > trk) || (fltres < 0 && -fltres > trk)) return;
	}
	if(susv > ins*3) { if(debug>0)_logIt("too suspicious"); return }
	for(i = 0x4C04; i < 0x5084; i++) if(X.U8(i) > 127) return; // testing patterns
	for(i = 0x5084; i < 0x7084; i++) //testing tracks... orderlists? all 64 of'em
		if(X.U8(i) > ptn) { if(debug>0)_logIt("@"+Hex(i)); return }
	if(!isAllZeroes(0x7084,0x2000)) return;
	return true
}
if(!bDetected && X.isDeepScan() && isBuzzic11()) {
	sName = "Buzzic module (.BUZ)"; sVersion = 'v1.1'; bDetected = 1;
	sOption('trk:'+trk+' ord:'+ord+(start?' (from '+start+')':'')+' ptn:'+ptn+' ins:'+ins
		+' bpm:'+bpm+' gvol:'+gvol+' sz:45224')
}


function isMXDRV() {
	// fmt https://www.vector.co.jp/download/file/dos/art/fh003454.html / DOC/MDXFORM.DOC
	// ref ditto / SRC/MDX2MUS.ASM
	// useful links: https://gorry.haun.org/mx/index_e.html
function re(p,t) { if(debug>-1)_l2r('mdx',p,t); return false }
//detection starts from here
	if(X.Sz() < 4+2+9*2+9*2) return false; //0d0a1a+00, vd ptr, chn ptrs, f100 f100....
	da1 = X.fSig(0,Math.min(0x400,X.Sz()),"0D0A1A"); //the longest title I saw was that long
	if(da1 < 0) return false;
	crypt = X.c("00'crypt'",da1-6);
	sus = crypt? -10: 0;
	for(i = 0; i < da1; i++) if(X.U8(i) < 0x20 && [0,9,0x1B,0xA,0x1A].indexOf(X.U8(i)) < 0) return false;
	title = X.SC(0,da1,'SJIS');
	p = X.fSig(da1+3,Math.min(15,X.Sz()-da1),"00"); if(p < 0) return false;
	if(da1+3 != p)
		pdxfn = X.SC(da1+3,p,'SJIS');
	else pdxfn = "";
	p++; ofs = p; bad = '';

	comp = false; ch = 9;
	if(X.SA(ofs+4,4) == "LZX "&& (lzxsz = X.U32(ofs+0x12,_BE))) {
		comp = true;
		sz = X.fSig(ofs+0x16,TOEOF,"'[ LZX.X ]'0D0A0000")+13;
		if(sz < 13) bad = bad.addIfNone('!short'); usedch = ch; bpm0 = notes = vdn = 0;
//re(ofs,"←ofs, lzxsz = "+lzxsz+" sz="+Hex(sz));
		return true; // stop right there because omg no
	}

	if(crypt) vd = -1;
	else {
		vd = X.U16(ofs,_BE)+ofs;
		if(vd > X.Sz()) { vd = -1; sus++ }
		//the bytes before voicedata (OPM voices, instruments) will be the F1 command (stop playback),
		//UNLESS, for example, you're Dig Dug2/dd2-10.mdx which is 84k long and the ins ptr just can't reach there
		//It still plays, so we have to accept this mockery, but we're holding a grudge...
		if(X.U8(vd-2) != 0xF1 && X.U8(vd-3) != 0xF1) { vd = -1; sus++ }
//re(vd,'vd')
		p += 2;
	}
	if(!X.c("0014",p) && !X.c("0022",p)) return false;
	m = Math.min(0xFFFFF,X.Sz()-2); //dd2-10.mdx is larger than it'd be allowed but still plays
	if(crypt) {
		sz = X.fSig(ofs+20,m,"'protected by cryptmdx v1.00 (c)1995 H.Yano'00")+0x2B;
		usedch = ch; bpm0 = 0; return true
	}
	var chn0 = chn = oldchn = X.U16(p,_BE)+ofs; //we begin with mml data 0 ptr, stored as offset from post-info point 
	if(!isWithin(chn,p,m)) return re(chn,'!chn'+Hex(p)+'-'+Hex(m)); // it shouldn't point at EoF-ish nor to 0 or 1
	ch = (chn-ofs-2)/2; //0x14 or 0x22, ie. either 9 or 16
	usedch = [0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0]; notes = vdn = mp = 0; vds = []; bpm0 = -1;
	var q = -1, lp = [], v = [0,1]; // v is for registering the presence of expected commands across the file; all must be 1
	if(X.isDeepScan()) { visited = []; for(i=0; i < m; i++) visited[i] = 0 } // to keep looping in check

	for(k=1; k <= ch; k++) { //check the rest of channels
		p += 2;
//re(p,'--= Parsing ch'+k+'/'+ch+' =--');
		if(p > mp) mp = p;
		if(k == ch) if(isWithin(vd, oldchn+2,X.Sz())) chn = vd; else chn = m;
		else {
			chn = X.U16(p,_BE)+ofs;
			if(!isWithin(chn,oldchn+2,m) || (X.U8(chn-2) != 0xF1 && X.U8(chn-3) != 0xF1)) {
//re(chn,'DEBUG2:'+Hex(X.U24(chn-3,_BE))+' ch'+ch);
				chn = oldchn; usedch[k-1] = false;
			}
			if(!isWithin(chn,oldchn+2,m)) { re(chn,'!p='+Hex(p)+',ch['+k+'] not between '+Hex(oldchn+2)+' and '+Hex(m)); sus += 2 }
			if(X.U8(chn-2) != 0xF1 && X.U8(chn-3) != 0xF1) {
				bad = bad.addIfNone('!badchnend@'+Hex(chn-2)+'='+Hex(X.U24(chn-3,_BE))); if(!X.isDeepScan()) sus++
			}
		}
		clk = 0; var cnotes = 0;

		if(X.isDeepScan()) for(q=oldchn,stop=false; !stop && q != chn && q < m; ) {

//_log(''+Hex(q)+' >> '+MDXCmdStr(k-1,q)); // !! the heavy logger of music data here. Much fun, much debug info

			visited[q] = 1; if(q > mp) mp = q;
			var c = X.U8(q++);
			if(c < 0x80) clk += c+1; //rest
			else if(c <= 0xDF) { cnotes++; notes++; usedch[k-1] = true; clk += X.U8(q++)+1 } //note or sample
			else switch(c) { //MMLscript
			case 0xFF: if(bpm0 < 0) bpm0 = X.U8(q); q++; break; //tempo0 (t)
			case 0xFE: a = X.U8(q); b = X.U8(q+1);//OPM reg set: reg and data
				if(!isYM2151Reg()) { sus++; bad = bad.addIfNone('!OPMreg@'+Hex(q-2)) }
				else if(a == 8 && (b&0xC8)) { cnotes++; notes++; usedch[b&7] = true }
				else if(isWithin(a, 0x60,0x7F)) v[0] = true;
				q += 2; break;
			case 0xFD: t = X.U8(q++); if(vds.indexOf(t) < 0) vds.push(t); break; //vd selection (@)
			case 0xFC: q++; break; //pan
			case 0xFB: t = X.U8(q++); if(isWithin(t,0x16,0x7F)) { bad = bad.addIfNone('!FB@'+Hex(q-2)+'='+Hex(t)); sus++ } v[0] = 1;
			break; //volume (v or @v)
			case 0xFA: case 0xF9: case 0xF7: case 0xEE: case 0xE8: break; //vmin, vmax, let ring, PCM8wide, syncwait, useful4Ach-head
			case 0xF8: t = X.U8(q++);
				if(!t/* || isWithin(t,9,0x7f)*/) {  bad = bad.addIfNone('!F8@'+Hex(q-1)); sus++ } break; //snd len (q or @q)
			case 0xF6: if(X.U8(q+1)) q++; else q += 2; break; //repeat start: num, 00 (apparently not always present)
			case 0xF5:
				if(X.U8(q+X.I16(q,_BE)-1) != 0xF6) { bad = bad.addIfNone('!F5→F6@'+Hex(q)); sus++ }
				q += 2; break; //repeat end: ofs to repstart+2
			case 0xF4:
				if(X.U8(q+X.I16(q,_BE)+1) != 0xF5) { bad = bad.addIfNone('!F4→F5@'+Hex(q)); sus++ }
				q += 2; break; //repeat stop: ofs to repend+1
			case 0xF3: q += 2; break; //detune (D)
			case 0xF2: q += 2; break; //portamento (_)
			case 0xF1: //endplay (optionally loopptr)
				//we'll stop here, ignoring possible unused music data that may follow past it,
				//because some files, like dd2-00.mdx, use the space BETWEEN channels to store unrelated data... 
				if(X.U8(q)) {
					lp[k] = q+2+X.I16(q,_BE);
					//normally, the loop is the channel's end but dd1-*.mdx are using F1 like jmp $+...
					//if(!isWithin(lp[k],oldchn,chn)) {
					if(!isWithin(lp[k],chn0,m)) { bad = bad.addIfNone('!loopOOB@'+Hex(q-1)); sus++ }
					if(visited[lp[k]]) { stop = true; q += 2 } else q = lp[k];
				} else q++;
				if(vd > 0 && vd < q) vd = q;
				 break;
			case 0xF0: q++; break; // key-on delay (k)
			case 0xEF: q++; break; //send sync to channel #
			case 0xED: q++; break; //ADPCM/noisewave#, H or P(f command)
			case 0xEC: case 0xEB: case 0xEA: t = X.U8(q++); //LFO ctl for 音程, 音量. OPMLFO ctl
				if(t != 0x80 && t != 0x81) q += 4; break;
			case 0xE9: q++; break; //LFO delay (MD)
			case 0xE7: if(X.U8(q) != 1) { bad = bad.addIfNone('!E7@'+Hex(q-1)); sus++ } q += 2; break //fadeout: 01, spd ($FO)
			default: stop = true; bad = bad.addIfNone('!unk'+Hex(c)); sus++ //unknown commands do stop-play
			}
			if(q > mp) mp = q;
		} // /if isDeepScan scan the channel
		if(cnotes) usedch[k-1] =  true;
//re(oldchn,'ch'+(k-1)+' stopped @'+Hex(q)+', has '+Hex(chn-oldchn)+' data ...'+Hex(X.U24(chn-3,_BE))+']')
//re(q,'ch'+(k-1)+' v:'+v.join('')+' clk:'+clk+' ... cmd:'+Hex(c))
		oldchn = chn;
		if(sus > 8) return re(mp,sus+' '+bad);
	} // /for k=1; k<=ch
	if(X.isDeepScan() && v[0] != '1') { bad = bad.addIfNone('!novol'); sus++ }
	if(sus > 8) return re(mp,sus+' '+bad);
	if(vd > 0 && vd < chn) { bad = bad.addIfNone('!ins@'+Hex(vd)+'<chn@'+Hex(chn)); sus++ }

	if(X.isDeepScan()) {
		vdn = vds.length;
		vds.sort(function(a,b){return a-b});
		var vdsavl = []; m = Math.min(X.Sz(), vd+0x1B00);
//re(vd+vdn*27,'vd used:'+outArray(vds,16))
		for(p=(vd > 0 && vd < chn)?chn:(vd > 0)?vd:p; vds.length && p < m; p+=0x1B) {
			if(!vds.length) break; //found all the instruments we used
			if(!(X.U8(p+1)&0xC0) && !(X.U8(p+2)&0xF0) //test for empty bytes at FL&CON and slot mask
			  && vdsavl.indexOf(t=X.U8(p)) < 0) //a new instrument found
				vdsavl.push(t);
			else break; //test for dupes
			vds = vds.filter(function(a,b,c){return a!=X.U8(p)}); //remove the matching element from the used vd list
			if(p > mp) mp = p;
		}
		vdsavl.sort(function(a,b){return a-b});
//re(p,'vdfound:'+outArray(vdsavl,16))
		if(vds.length) bad += '!missingInst'+outArray(vds,16);
		sz = mp+27;
	}
	sz = X.isDeepScan()? mp: -1; if(vd == X.Sz()) sz = vd;
	return true;
}
if(!bDetected && isMXDRV()) {
	sName = "Konami's X68k Music Data eXtended module (.MDX)"; bDetected = 1;
	if(pdxfn != "") sVersion = sVersion.appendS('+ '+pdxfn,' ');
	if(ch > 9) sVersion += "#EX-PCM";
	if(comp) sVersion = sVersion.appendS('compressed','/');
	if(crypt) sVersion = sVersion.appendS('cryptmdx','/');
	if(bad.length) sVersion = sVersion.appendS('malformed'+bad+(sus?'/sus'+sus:''),'/');
	if(X.isVerbose()) {
		sOption(addEllipsis(title),'"','"');
		for(ch=0,i=0; i < 16; i++) if(usedch[i]) ch++;
		if(!crypt) sOption('ch:'+ch+' bpm0:'+(bpm0 < 0?'default':bpm0)
			+(notes? ' notes:'+notes: '')+(vdn? ' ins:'+vdn: '')+(sz > 0? ' sz:'+outSz(sz): ''));
		else sOption('sz:'+outSz(sz))
	}
}


function isFredF() {
	//ref https://github.com/neumatho/NostalgicPlayer/blob/main/Source/Agents/Players/Fred/FredWorker.cs
	var p = modp = ofsdiff = 0; bad = '';
	function _check0() {
		if(X.Sz() < 0xB0E) return false; if(!X.c("4EFA....4EFA....4EFA....4EFA")) return false;
		p = X.I16(2,_BE)+2; if(p <= 0x10 || p%2) return false;
		// The init routine just has 2 variants it seems, the second I only found in furball*.fred.
		// Maybe out in the wild there are more but:
		if(!X.c("123A.... B0016200 007E47FA 08761680 45FA0873 49FA086E D5C01892 E7887E03 7C0041FA 08D8",p)
		  && !X.c("42380001 123A.... B0016200 007E47FA 08B61680 45FA08B3 49FA08AE D5C01892 E7887E03 7C0041FA 0918",p))
			return false;
		// and this is as-is from NP. Not sure why this specific way of looking for it
		for(i=0; i < 4; i++) if(X.c("123A....B001",p+i*2)) { modp = p+i*2+1+X.I16(p+i*2+2,_BE); break }
		if(i == 4 || modp > X.Sz()) return false;
		for(; i < 60; i++) if(X.c("47FA....D7FA",p+i*2)) { ofsdiff = p+(i+1)*2+X.I16(p+i*2+2,_BE); return true }
		return false
	}
	if(!_check0()) return false;  p = modp+1; x = X.U8(p++)+1; if(x > 10) return false;  spd0 = X.U8(p++);
	spds = []; for(i=0; i < 10; i++) { t = X.U8(p++); if(spds.indexOf(t) < 0) spds.push(t) } p++;
	var insp = ofsdiff+X.U32(p,_BE), trkp = ofsdiff+X.U32(p+4,_BE), ptnsz = Math.abs(insp-trkp);
	// subsong start pos per voice init and read
	var stpt = [], ordt = [], trkt = [], trks = []; for(i=0,ord=[],p+=0x26C/*replay data*/; i < x; i++) { stpt[i] = [0,0,0,0]; ord[i] = 0 }
//_l2r('frf',p,'starts')
	for(i=0; i < x; i++) for(j=0; j < 4 && p < X.Sz(); j++,p+=2) stpt[i][j] = (X.U16(p,_BE) - x*8) >> 1;
//_l2r('frf',p,' -> '+outArray(stpt,16))
	if(p > X.Sz()) return false; ordp = p;
	for(otsz=(Math.min(insp,trkp)-p)>>1,i=0; i < otsz; i++,p+=2) ordt.push(X.I16(p,_BE));
	trksz = Math.abs(insp-trkp);
	for(i=tr=0; i < trksz; tr++) { // for checking ofs in the orderlist
		for(si=i; X.U8(p+(i++)) != 0x80 && i != trksz && p+i < X.Sz();){}
		if(i == trksz && X.U8(p+i-1) != 0x80) break;
		trkt[si] = tr
	}
	for(i=0; i < x && p < X.Sz(); i++) {
		for(j=0; j < 4 && p < X.Sz(); j++) {
			for(on=true,c=stpt[i][j]; c - stpt[i][j] < 0xFF && ordt[c] >= 0; c++)
				if(typeof trkt[ordt[c]] == 'undefined') return false;
		}
	} 
	for(syn=smp=0,msmpp=0x7fffffff,Msmpp=smpe=0,p=insp;;) {
		var smpp = X.U32(p,_BE), tsyn = X.I16(p+4,_BE), ssz = X.U16(p+6,_BE) << 1;
		var tins = X.U8(p+0x27); if(p+0x28 >= X.Sz() || p+0x3F >= msmpp) break;
		if(tins != 0xFF)
			if(smpp) {
				smpp += ofsdiff; if(smpp > X.Sz()) break;
				if(smpp < msmpp) msmpp = smpp;
				if(smpp > Msmpp && smpp < X.Sz()) { Msmpp = smpp; smpe = smpp+ssz } smp++
			} else {
				if(tsyn === -779) /* giants-1.fred fix*/ break;  if(tsyn != -1 && tins != 0) return false;  syn++
			}
		p += 0x40;
	}
	if(smp) sz = smpe; else sz = p;
	return true
}
if(!bDetected && isFredF()) {
	sName = "Fred & Julien Clermonte's Fred Editor module (.FRED,.FRD)"; sVersion = 'final'; bDetected = 1;
	if(bad != '') sVersion = sVersion.appendS('malformed'+bad,'/');
	if(X.isVerbose()) {
		if(x > 1) sOption(x,'×');
		sOption('tempo:'+spds.sort().join('-')+(smp?' smp:'+smp:'')+(syn?' syn:'+syn:'')+' sz:'+outSz(sz))
	}
}


function isDSMIAMF() {
	// ref https://github.com/OpenMPT/openmpt/blob/master/soundlib/Load_amf.cpp
	if(!X.c("'AMF'")) return false; var v = X.U8(3);
	if(v != 1 && !isWithin(v, 8, 0xE)) return false;
	if(v < 9) p = 4; else p = 0x24;
	smp = X.U8(p); ord = X.U8(p+1); trk = X.U16(p+2); ch = v >= 9? X.U8(p+4): 16;
	if(!smp || !ord || !trk) return false;  if(v >= 9) p += 5; else p += 4;
	if(v < 12 && !isWithin(ch,1,16)) return false;  if(!isWithin(ch,1,32)) return false;
	var rc = v >= 11? (v >= 12? 32: 16): v >= 9? 16: 0;
	p += rc;
	if(v >= 13) { tmp0 = X.U8(p++); if(tmp0 < 32) tmp0 = 125; spd0 = X.U8(p++) } else { tmp0 = 125; spd0 = 6 }
	var ptnsz = 0, psz = [], trkp = p; if(v >= 14) trkp += 2;
	for(i=0; i < ord; i++) { if(v >= 14) { psz[ord] = X.U16(p); ptnsz += X.U16(p); p += 2 } p += ch*2 }
	var truncshd = false;
	if(v == 10) {
		var s = p; for(i=0; i < smp; i++) {
			if(s+65 < X.Sz()) if(X.U8(s) > 1 || X.U32(s+46) > smp || (ssz=X.U32(s+50)) > 0x100000
			  || X.U8(s+56) > 0x40 || X.U32(s+57) > ssz || X.U32(s+61) > ssz)
				{ truncshd = true; break } else s += 65;
		}
	}
	smps = []; var smpsz = 0; rsmp = 0;
	for(i=0; i < smp; i++) {
		t = X.SC(p+1,0x20,'CP437').trim(); if(t.length) smps.push(t);
		if(v < 10) { smpsz += (t=X.U16(p+50)); if(t) rsmp++; p += 59 }
		else { smpsz += (t=X.U32(p+50)); if(t) rsmp++; p += (truncshd? 59: 65) }
	}
	trkc = 0; for(i=0; i < trk; i++) { t = X.U16(p); p += 2; if(t > trkc) trkc = t }
	for(i=0; i < trkc; i++) { t = X.U16(p); p += 3; if(t) p += t*3+ (v==1? 3: 0) }
	sz = p+smpsz;
	return true
}
if(!bDetected && isDSMIAMF()) {
	sName = "Digital Sound and Music Interface Advanced Music Format module (.AMF)";
	sVersion = "v"+X.U8(3); bDetected = 1;
	if(X.isVerbose()) {
		sOptionT(X.SC(4,32,'CP437'));
		sOption(addEllipsis(smps.join(' '),0xA0),'smp/msg:"','"');
		sOption('ch:'+ch+' spd0:'+spd0+' tmp0:'+tmp0+' ord:'+ord+' trk:'+(trkc != trk?trkc+'/':'')+trk
			+' smp:'+(rsmp==smp?'':rsmp+'/')+smp+' sz:'+outSz(sz))
	}
}


function isSAdT() {
	//the signature's good enough but there also are sanity checks
	//ref https://github.com/adplug/adplug/blob/master/src/sa2.cpp
	if(!X.c("'SAdT'") || !isWithin(nV=X.U8(4),1,9)) return false;
	const hasArpList = 0x80, hasV7Ptn = 0x40, hasActCh = 0x20, hasTrkOrd = 0x10,
		hasArp = 8, hasOldBPM = 4, hasOldPtn = 2, hasUnk127 = 1; var notedis=0, fl;
	switch(nV) {
	case 1: notedis = 0x18; fl = hasUnk127 | hasOldPtn | hasOldBPM; break;
	case 2: notedis = 0x18; fl = hasOldPtn | hasOldBPM; break;
	case 3: notedis = 0xC; fl = hasOldPtn | hasOldBPM; break;
	case 4: notedis = 0xC; fl = hasArp | hasOldPtn | hasOldBPM; break;
	case 5: notedis = 0xC; //fallthru
	case 6: fl = hasArp | hasArpList | hasOldPtn | hasOldBPM; break;
	case 7: fl = hasArp | hasArpList | hasV7Ptn; break;
	case 8: fl = hasArp | hasArpList | hasTrkOrd; break;
	case 9: fl = hasArp | hasArpList | hasTrkOrd | hasActCh
	}
	p = 5 + 31*(fl & hasArp? 15: 11);
	for(i=0,inst=[]; i < 29; i++) { if((t=X.SC(p+1,X.U8(p),'CP437').trim()) != '') inst.push(t); p += 0x11 }
	p += 3; var orderlist = X.readBytes(p,128); p += 128;
	if(fl & hasUnk127) p += 0x7F;
	if(!isWithin(ptn=X.U16(p),1,64)) return false; p += 2;
	if(!isWithin(ord=X.U8(p++),1,0x80)) return false;
	for(i=rptn=0; i < ord; i++) if((orderlist[i]+1) > rptn) rptn = orderlist[i]+1;  delete orderlist;
	if(rptn > ptn || (lp=X.U8(p++)) >= ord) return false;
	bpm = X.U16(p); p += 2; if(fl & hasOldBPM) bpm = bpm*5/2;
	if(fl & hasArpList) p += 0x200; if(fl & hasTrkOrd) p += 9*0x40;
	if(fl & hasActCh) { actch = X.U16(p) << 16; p += 2 } else actch = -1;
	trk = 0;
	if(fl & hasOldPtn) while(trk < ptn && p < X.Sz()) { p += 64*9*5; trk++ }
	else if(fl & hasV7Ptn) while(trk < ptn && p < X.Sz()) { p += 64*9*3; trk++ }
	else while(trk < 64*9 && p < X.Sz()) { p += 64*3; trk++ }
	sz = p;
	return true
}
if(!bDetected && isSAdT()) {
	if(nV < 8) sName = "Surprise! AdLib Tracker module (.SAT)";
	else sName = "Surprise! AdLib Tracker 2 module (.SA2)";
	sVersion = "v"+nV; bDetected = 1;
	if(X.isVerbose()) {
		sOptionT(addEllipsis(inst.join(" "),0xA0),'ins/msg:"','"');
		sOption('bpm:'+bpm+(actch>=0?'ch:'+actch:'')
			+' ord:'+(lp?lp+'-':'')+ord+' ptn:'+(rptn!=ptn?rptn+'/':'')+ptn+(trk!=ptn?' trk:'+trk:'')+' sz:'+outSz(sz))
	}
}


/*function isTFMX() {
	//from https://gitlab.com/uade-music-player/uade/-/raw/master/amigasrc/players/wanted_team/TFMX/src/TFMX_v4.asm
	if(!X.c("'TFMX'")) return false;
	if(X.U8(4) !== 0x20) {
		if(!X.c("'-SONG '",4)) return false;
		if(!X.c("'by'",10))
			if(!X.c("'(Empty) '",0x10) && X.U8(0x10) !== 0x20 && !X.c("'0='",0x10)) return false;
	}
	if(X.U32(0x1D0)) { _l2r('tfmx',0x1d0,'dw non-zero'); return false }
	if(X.c("0E60",14)) return false;
	if(X.c("0860",14) && !X.c("090C",0x1224)) return false;
	if(X.c("0920",14) && !X.c("9305",0xF24)) return false;
	if((a0=X.I32(0x600,_BE)) <= 0 || (a0&1)) return false;
	if((a1=X.I32(0x7FC,_BE)) <= 0 || (a1&1)) return false; if(a1 > X.Sz()) a1 = X.Sz();
	for(; a0 < a1; a0 += 4) if(X.U8(a0) > 0x24) return false;

	for(i=0, x=1, d5=0x1F; 0x148+i <= X.Sz();) {
		d2 = X.U16(0x142+i,_BE); d3 = X.U16(0x144+i,_BE); if(d2 == d3 && d3 == X.U16(0x146+i,_BE)) break;
		d5--; i += 2; x++; if(x == 0x20) break;
	}
	if(x != 0x20) for(;d5; i += 2,d5--) if(X.U16(0x142+i,_BE)-X.U16(0x102+i,_BE)) x++;
	sz = 0x7FC;
	while(!X.c("07000000",sz) && sz < X.Sz()) sz += 4;
	bad = ''; if(sz >= X.Sz()) bad = bad.addIfNone('!short'); sz += 4;
	return true
}
if(!bDetected && isTFMX()) {
	sName = "Chris Hülsbeck's The Final Musicsystem eXtended module (MDAT.+SMPL.)";
	// sVersion = 'v1.5';
	if(bad.length) sVersion = sVersion.appendS('malformed'+bad,'/');
	if(X.isVerbose()) {
		if(x > 1) sOption(x,'×')
		sOption('sz:'+outSz(sz))
	}
}

function isTFMX_7V() {
	//from reversing the eagleplayer
	if(!X.c("'TFMX-SONG '")) return false;
	if(!(d1=X.U32(0x1D0,_BE))) d1 = 0x800; var found = false;
	if(X.c("08B0",14)) if(X.c("01F4FF00",0x204)) found = true;
	if(!found) for(i=0,a1=0x100; !found && i < 0x20; i++) {
		t = X.U16(a1,_BE); a1 += 2; if(t == 0x1FF) continue;
		t <<= 4; t += d1; a2 = t; if(t > X.Sz()) return false;
		for(; !found && a2 < X.Sz(); ) {
			var effe = X.c("EFFE",a2); a2 += 2;
			if(effe) {
				var is3 = X.c("0003",a2); a2 += 2;
				if(is3)
					if(X.U16(a2,_BE))
						if(X.U8(a2) || !X.U8(a2+3)) { found = true; break }
				a2 += 12
			}
		}
	}
	if(!found) return false
	return true
}
if(!bDetected && isTFMX_7V()) {
	sName = "Chris Hülsbeck's The Final Musicsystem eXtended module (MDAT.,+SMPL.)";
	sVersion = '7V'
}

function isTFMX_ST() {
	//from https://gitlab.com/uade-music-player/uade/-/raw/master/amigasrc/players/wanted_team/TFMX_ST/TFMX%20ST_v1.asm
	if(!X.c("'TFMX-SONG '")) return false;
	if(d1=X.U32(0x1D0,_BE)) if(X.U16(12,_BE)) return false;
	if(d2=X.U32(0x1D4,_BE)) d1 = X.U32(0x1D8,_BE);
	else {
		if((d1=X.I32(0x600,_BE)) <= 0 || (d1&1)) return false;
		if((d2=X.I32(0x7FC,_BE)) <= 0 || (d2&1)) return false;
	}
	d2 = Math.min(d2,X.Sz()); smp = st = 0;
	for(; d1 < d2; d1 += 4)
		if(X.U8(d1) == 0x48) smp++; else if(X.U8(d1) > 0x3F) st++;
	if(!st) return false;
sz=-5;
	// for(i=0, x=1, d5=0x1F; 0x148+i <= X.Sz();) {
	// 	d2 = X.U16(0x142+i,_BE); d3 = X.U16(0x144+i,_BE); if(d2 == d3 && d3 == X.U16(0x146+i,_BE)) break;
	// 	d5--; i += 2; x++; if(x == 0x20) break;
	// }
	// if(x != 0x20) for(;d5; i += 2,d5--) if(X.U16(0x142+i,_BE)-X.U16(0x102+i,_BE)) x++;

	bad = ''; if(sz >= X.Sz()) bad = bad.addIfNone('!short'); sz += 4;
	return true
}
if(!bDetected && isTFMX_ST()) {
	sName = "Chris Hülsbeck's The Final Musicsystem eXtended module ST module (MDST.+SMPL.)";
	if(X.isVerbose()) {
		sOption('smp:'+smp)
	}
}
*/

function isTFMX() {
	//from https://github.com/neumatho/NostalgicPlayer/blob/main/Source/Agents/Players/Tfmx/TfmxWorker.cs / TestModule, Load
	if(X.Sz() < 0x200) return false;
	var hdrsz = tp = mdatsz = smplsz = -1, p0 = 0, maxlen = 0x30000;
	title = album = by = ''; flag5 = -1; tfmxst = false;
	function isStFile(p0) {
		var stidx, edidx = X.U32(p0+0x1D4,_BE);
		if(!edidx) { stidx = X.U32(p0+0x600,_BE); edidx = X.U32(p0+0x7FC,_BE) }
		else { stidx = X.U32(p0+0x1D8,_BE); stidx = X.U32(p0+stidx,_BE) }
		var i, buf = X.readBytes(p0+stidx, edidx-stidx); for(i=0; i < buf.length; i+=4) if(isWithin(buf[i],0x40,0x7F)) return true;
		return false
	}
	if(X.c("'TFMX-MOD'")) { // a PC-originating module wrapper format???
		hdrsz = p0 = 0x14; sz = X.U32(12,_LE); smplsz = sz-X.U32(8,_LE); mdatsz = X.U32(8,_LE)-8;
		if(!mdatsz || !isWithin(sz, 0x212, X.Sz())) return false;
		for(; sz < X.Sz();) {
			t = X.U8(sz); var tagsz = X.U16(sz+1,_LE);
//_l2r('tfmx',sz,'MOD: tag#'+t+' sz:'+tagsz)
			sz += 3; if(!t && !tagsz) break;
			switch(t) {
			case 1: by = X.SC(sz,tagsz,"CP1252"); break;
			case 2: album = X.SC(sz,tagsz,"CP1252"); break;
			case 5: flag5 = X.U8(sz); break;
			case 6: title = X.SC(sz,tagsz,"CP1252"); break;
			}
			if(!t) { sz += 17; break } else sz += tagsz
		}
	}
	if(X.c("'TFHD'")) {
		hdrsz = X.U32(4,_BE); tp = X.U8(8); v = X.U8(9); mdatsz = X.U32(10,_BE); smplsz = X.U32(14,_BE);
		sz = hdrsz+mdatsz+smplsz; if(hdrsz < 18 || !mdatsz || !isWithin(sz, 0x212, X.Sz())) return false;
		if((tp&0x80) || !(tp&0x7F)) p0 = p = hdrsz; else
		switch(tp&0x7F) { case 1: tp = '1.5'; break; case 2: tp = 'pro'; break; case 3: tp = '7v'; break; default: return false }
	} else if(X.c("'TFMX '",p0) && !X.c("'SONG'",p0+5)) tp = '1.5';
		else if(X.c("'TFMX-SONG'",p0) || X.c("'TFMX_SONG'",p0) || X.c("'tfmxsong'",p0)) { //pro or 7v?
			//check the trackstep (info for sequencing and mastering)
			var tm = 0, gottms = false, stp = X.readBytes(p0+0x100,31), trksp = X.U32(p0+0x1D0,_BE); if(!trksp) trksp = 0x800;
			for(i=0; i < 31; i++) {
				var getNext = true, pos = stp[i]; if(pos == 0x1FF) break;
				while(getNext && p < X.Sz()) {
					p = p0+trksp+pos*16; t = X.U16(p,_BE); cmd = X.U16(p+2,_BE); p += 4;
					if(t != 0xEFFE) getNext = false;  else switch(cmd) {
					case 1: //loop a section
						if(!tm) { tm = -1; pos++ } else
							if(tm < 0) { pos = X.U16(p,_BE); tm = X.I16(p+2,_BE)-1; p += 4 }
							else { tm--; pos = X.U16(p,_BE); p += 2 }
						break;
					case 2: case 4: pos++; break; //set tempo + start master vol slide
					case 3: gottms = true; pos++; break; //start master vol slide too
					default: getNext = gottms = false
					}
				}
				if(gottms) break
			}
			if(isStFile()) return false;
			else { tfmxst = true; if(gottms) tp = '7v'; else tp = 'pro' }
		} else return false;
	for(i=0,p=p0+0x10,cmt=''; i < 6; i++,p += 40) cmt = cmt.appendS(decAnsi(p,40,CPAmiga).trim(),'  '); //F0 bytes cmt
	cmt = cmt.trim();
_l2r('tfmxcmt',p0+0x10,decAnsi(p,40*6,CPAmiga).trim())
	var sst = [], sed = [], tmp = [];
	for(i=0; i < 0x20; i++) { sst.push(X.I16(p,_BE)); p += 2 }
	for(i=0; i < 0x20; i++) { sed.push(X.I16(p,_BE)); p += 2 }
	for(i=0; i < 0x20; i++) { tmp.push(X.I16(p,_BE)); p += 2 }
	p += 0x10; // after the st/ed/tmp reads + 20 it's gonna be +60+80 = +E0 post-cmt
	var trkp = X.I32(p,_BE), ptnp = X.I32(p+4,_BE), insp = X.I32(p+8,_BE);
	if(!ptnp) ptnp = 0x200; else ptnp -= 0x200;
	if(!insp) insp = 0x400; else insp -= 0x200;
	if(!trkp) trkp = 0x600; else trkp -= 0x200;
	p += 0x30; if(p > X.Sz()) return false;  var datap = p, maxlen = Math.min(maxlen+datap,X.Sz());
	ptnp += datap; insp += datap; trkp += datap;
	if(hdrsz > 0) len = mdatsz; else len = Math.min(maxlen,X.Sz())-p;
	for(ins=mip=0,p=insp,ino=X.I32(p,_BE); ins < 0x100 && p+4 <= X.Sz(); ins++,p+=4) {
		t = X.I32(p,_BE)-0x200+datap;
		if((t & 3) || !isWithin(t,trkp,maxlen)
			|| (t & !X.c("F0000000",t-4) && !X.c("07000000",t-4)) || (ins && Math.abs(t-ino) > 0x2000)) break;
		// 07000000 is the end-of-data command,
		// F0000000 is the end pattern command (may happen because data may be mixed.
		// 2000h is an empirically appropriate max size of a record, should be good enough to deter FPs either way
		if(mip < t) mip = t;  if(ins) ino = t }
	ptn = Math.min((insp-ptnp)>>2, 128); var lg = X.U32(ptnp,_BE)-0x200+datap;
	pt = ptn; trkst = (lg - trkp) >> 4; if(trkst < 0) trkst = 0;
	if(hdrsz < 0)
		switch(Math.max(insp,ptnp,mip)) { //what's the file ending with? inst.ptr table, ptn.ptr table, instruments?
		case insp:  sz = insp+ins*4; break;
		case mip:
//_l2r('tfmx',mip,'tracing last instrument rec')
			for(p=mip; p < maxlen; p += 4) if(X.c("07000000",p)) break;  sz = p; if(sz < maxlen) sz += 4;
			break;
		case ptnp: //need to parse the pattern table to find out where it should end
_l2r('tfmx',datap,'p:'+Hex(p)+' ptnp:'+Hex(ptnp)+' insp:'+Hex(insp)+' trkp:'+Hex(trkp)+' trkst['+Hex(ptnp)+'='+Hex(lg)+']:'+Hex(trkst)+' len:'+Hex(len)+'/'+Hex(maxlen))
	 		for(p=ptnp+4,po=X.I32(ptnp,_BE),pt=1; pt < 0x100; p += 4,pt++) {
				t = X.I32(p,_BE)-0x200+datap;
//_l2r('tfmx',p,'ptn['+Hex(pt)+'] ptr:'+Hex(t)+'; '+((t&3)?'X':'O')+(!X.c("F0000000",t-4) && !X.c("07000000",t-4)? 'X':'O')+Hex(Math.abs(t-po))  )
 				if((t & 3) || !isWithin(t,trkp,maxlen)
 					|| (!X.c("F0000000",t-4) && !X.c("07000000",t-4)) || Math.abs(t-po) > 0x2000) break;
				po = t
 			}
			sz = p; break
	}
	x = -1; var max0s = 2; ord = [0];// there should be at least one, we all hope (empty files exist but who needs those)
	for(i=0; i < 32 && max0s > 0; i++) {
		x++; if(!sst[i]) max0s--; if(sst[i] == 0x1FF || sed[i] == 0x1FF) break;if(sst[i] <= trkst) ord[x] = sed[i]-sst[i]+1;
		if(sst[i] >= trkst) break; if(sst[i] == sed[i] && !sst[i] && !sst[i+1]) break;
	}
	if(!x) x = 1;
	return true
}
if(!bDetected && isTFMX()) {
	sName = "Chris Hülsbeck's The Final Musicsystem eXtended module (TFX.,.TFM,MDAT.+SMPL.,MDST.+SMPL.)";
	bDetected = 1;
	switch(tp) {
	case '1.5': sVersion = 'v1.5'; break;  case 'pro': sVersion = 'Professional'; break;  case '7v': sVersion = '7 voices'
	}
	if(tfmxst) sVersion = 'ST '+sVersion;
	if(pt < ptn) sVersion += '/malformed!badptn';
	if(X.isVerbose()) {
		sOptionT(title); if(x > 1) sOption(x,'×'); sOption(album,'in: '); sOptionT(by,'by: '); if(flag5 >= 0) sOption(flag5,'flag5:')
		if(cmt == '(Empty)') cmt = ''; sOption(addEllipsis(cmt,0xA0),'msg:"','"');
		sOption('ord:'+ord.join('+')+' ptn:'+ptn+(ptn!=pt? '/'+pt: '')+' ins:'+(ins>0x80? '128/': '')+ins+' sz:'+outSz(sz))
	}
}


function isThePlayer22to41() {
	//ref prowiz /r/TP22a.c & TP30a.c & TP40.c & TP41a.c
	if(!X.c("'P22A'") && !X.c("'P30A'") && !X.c("'P40A'") && !X.c("'P40B'") && !X.c("'P41A'")) return false;
	ptn = X.U8(4); smp = X.U8(6); volofs = (X.SA(1,3) == '41A')? 0x20: 0x22;
	if(ptn > 0x7F || !smp || smp > 0x1F) return false;
	for(i=0; i < smp; i++) {
		if(!isWithin(X.U16(volofs+i*16,_BE),1,0x40)) return false;
		if(volofs == 0x20 && X.U16(volofs+2+i*16,_BE) % 74) return false; //incorrect finetune in 4.1a
	}
	for(i=smpsz=0; i < smp; i++) {
		ssz = X.U16(24+i*16,_BE) << 1;
//_l2r('p2~4',24+i*16,'ssz['+i+'] = '+ssz);
		if(!ssz || ssz > 0xFFFE || (X.U16(30+i*16,_BE) << 1) > ssz+2) return false;
		smpsz += ssz
	}
	if(smpsz <= 4) return false;
	if(X.SA(1,1) < '4') ord = (X.U8(5)>>1)-1; else ord = X.U8(5);
	p = 0x10+smp*0x10;
	var trkp = X.U32(8,_BE), trktp = X.U32(0xC,_BE), smpp = X.U32(0x10,_BE), memofs = 0;
	if(p != trktp) { memofs = trktp-p; trkp -= memofs; trktp -= memofs; smpp -= memofs }
	if(!isWithin(trkp,p,X.Sz()) || !isWithin(trktp,p,X.Sz()) || !isWithin(smpp,p,X.Sz())) return false;
	for(i=p=0; i < smp; i++)
		if(volofs == 0x20 && X.U16(volofs+2+i*16,_BE) > 0x456) continue;
		else if((sp=X.U32(20+i*16,_BE)) > p) { p = sp-memofs; ssz = X.U16(24+i*16,_BE) }
	sz = 4+smpp+p+(ssz<<1);
	return true
}
if(!bDetected && isThePlayer22to41()) {
	sName = "Jarno 'Guru' Paananen's The Player module (."+X.SA(0,2)+'X,.'+X.SA(0,4)+')';
	sVersion = 'v'+X.SA(1,1)+'.'+X.SA(2,2); bDetected = 1;
	if(X.isVerbose()) {
		sOption('ord:'+ord+' ptn:'+ptn+' smp:'+smp+' sz:'+outSz(sz))
	}
}


function isThePlayer56X() {
	//from prowiz /r/TP50a.c & TP60a.c & TP61a.c
	if(X.Sz() < 7) return false;
	z = sig = X.c("'P50A'") || X.c("'P60A'") || X.c("'P61A'")? 4: 0;
	smpp = X.U16(z,_BE)+z; ptn = X.U8(z+2); if(!ptn || ptn > 0x7F) return false;
	smp = X.U8(z+3); if(smp & 0x20) return false;  pksmp = smp & 0x40; dtsmp = smp & 0x80; smp &= 0x3F;
	if(!smp || 7+smp*6 > X.Sz()) return false;
	if(pksmp) z += 4;
	if(smpp < z+4+smp*6+ptn*8 || smpp > X.Sz()) return false;
	smpsz = notes = 0; var unpsmpsz = vols = 0;
	for(i=0,p=z+4; i < smp; i++,p+=6) {
		if(isWithin(ssz=X.U16(p,_BE),0x8000,0xFFDF) || !ssz) return false;
		if(ssz > 0xFFDF && 0xFFFF-ssz > smp) return false;
		var spk = X.U8(p+2)&0x80, ft = X.U8(p+2)&0x7F;
		if(ft > 0xF || X.U8(p+3) > 0x40) return false; vols |= X.U8(p+3);
		if(isWithin(X.U16(p+4,_BE),ssz,0xFFFE)) return false; //lp
		if(ssz < 0xFF00) { smpsz += ssz << (spk? 0: 1); unpsmpsz += ssz << 1 }
	}
	if(!vols || (pksmp && X.U32(z,_BE) != unpsmpsz)) return false;
	for(i=0; i < ptn*4; i++) if(X.U16(z+4+smp*6,_BE)+4+z+smp*6+ptn*8 > smpp) return false;
	i = mptn = 0;
	function _5() {
		for(; (t=X.U8(z+4+smp*6+ptn*8+i)) != 0xFF && i < 0x80; i++) {
			if(t%2 || t > ptn*2) return false; if(t > mptn) mptn = t
		} mptn >>=1; return true
	}
	function _6() {
		for(; (t=X.U8(z+4+smp*6+ptn*8+i)) != 0xFF && i < 0x80; i++) {
			if(t > ptn-1) return false; if(t > mptn) mptn = t
		} return true
	}
	if(_6()) v = 6; else if(_5()) v = 5; else return false;
	if(/*z+4+smp*6+ptn*8+i > smpp ||*/ !i || i == 0x80) return false; ord = i; mptn++;
	function _is5060() {
		for(i=ord+z+5+smp*6+ptn*8; i < smpp; i++) {
			if((t=X.U8(i)) & 0x80) i += 3;
			else {
				if(t > 0x49 || (((t<<4)&0x10) | (X.U8(i+1)>>4)) > smp) return false;
				if(t >= 2) notes++; i += 2
			}
		}
		return (notes > 0)
	}
	function _is61() {
		for(i=ord+z+5+smp*6+ptn*8; i < smpp; i++) {
			if((t=X.U8(i)) == 0x7F) continue;
			else if(t == 0xFF)
				switch(X.U8(i+1) & 0xC0) {
				case 0: i++; continue;  case 0x40: i += 2; continue;
				case 0xC0: if(i < X.U16(i+2,_BE)-1) return false; i += 3; continue
				}
			switch(t & 0xF0) {
			case 0xF0: if((X.U8(i+1)&0x1F) > smp) return false;  i += 2; continue;
			case 0x70: if((X.U8(i+1)&0x1F) > smp) return false;  i += 1; continue;
			case 0xE0: i += 2; continue;  case 0x60: i += 1; continue
			}
			if((t & 0x80) == 0x80) { if((((t<<4)&0x10) | (X.U8(i+1)>>4)) > smp) return false;  i += 3; continue }
			if((((t<<4)&0x10) | (X.U8(i+1)>>4)) > smp) return false;
			i += 2
		}
		return true
	}
	if(v == 6) if(_is5060()) sv = '6.0'; else if(_is61()) sv = '6.1'; else return false;
	else if(_is5060()) sv = '5.0';
	return true
}
if(!bDetected && isThePlayer56X()) {
	sName = "Jarno 'Guru' Paananen's The Player module (.P"+sv[0]+'X,.P'+sv[0]+sv[2]+'A)';
	sVersion = 'v'+sv+'A'; bDetected = 1;
	if(dtsmp) sVersion = sVersion.appendS('deltasmp','/');
	if(pksmp) sVersion = sVersion.appendS('packedsmp','/');
	if(X.isVerbose()) {
		sOption('ord:'+ord+' ptn:'+(mptn!=ptn?mptn+'/':'')+ptn+' smp:'+smp
			+(notes>0?' notes:'+notes:'')+' sz:'+outSz(smpp+smpsz))
	}
}


function isThomasHermann() {
	//ref the eagleplayer RE
	if(X.Sz() < 0x1A90) return false;
	var ofs = X.I32(0x2E,_BE); if(!isWithin(ofs,2,0x4000000) || ofs % 2) return false;
	if(X.I32(0,_BE)-ofs != 0x40) return false; p = 4;
	for(i=0,d4=0x40; i < 4; i++,p+=4) { d4 += 0x400; if(X.I32(p,_BE)-ofs != d4) return false }
	for(i=0; i < 3; i++,p+=4) { d4 += 0x100; if(X.I32(p,_BE)-ofs != d4) return false }
	smp = X.U8(0x23); sz = smp-1; smpsz = -1;
	for(p = 0x44; p < 0x1040; p += 4) if((t=X.U32(p-4,_BE)-ofs) > sz) sz = t;
	sz += 0x40; if((sz-0x1A90)%3) return false;
	if(sz == 0x2E10) x = 5;  else if(sz == 0x2780) x = 3;  else x = 1;
	ptn = Util.divu64(Util.divu64(sz-0x1A90,3), X.U8(0x29)+1);
	bad = ''; if(ptn > 0x400) bad = bad.addIfNone('!badptn');
	if(smp > 0x1F) bad = bad.addIfNone('!badsmp');
	else for(i=0,p=0x14F0; i < smp; i++,p+=0x30) smpsz += X.U16(p,_BE);
	d1 = 0x100+X.U8(0x21)-X.U8(0x22)+1; d1 *= 2+X.U8(0x2C); d2 = d1;
	d1 *= X.U16(0x2A,_BE); d3 = Util.divu64(sz == 0x37EE ? 0xAEC80: 0xAD300,  X.U8(0x29)+1);
	d1 = Util.divu64(d1, d3); dur = secondsToTimeStr(d1)
	return true
}
if(!bDetected && isThomasHermann()) {
	sName = "Thomas Hermann's module (.THM+.SMP)"; bDetected = 1;
	if(bad.length) sVersion = sVersion.appendS('malformed'+bad,'/');
	if(X.isVerbose()) {
		if(x > 1) sOption(x,'×');
		sOption('len.'+dur+' ptn:'+ptn+' smp:'+smp+' smpsz:'+smpsz+' sz:'+outSz(sz))
	}
}


function isJaytrax() {
	//ref https://github.com/pachuco/jaytrax/raw/refs/heads/master/lib_oldjaytrax/jxs.c etc.
//ch/ss = 16, ord/ss = 256, rows/ptn = 64, eff/inst = 4, wf/inst = 16, smp/wf = 256, arps/song = steps/arp = 16,
//se_ch = _FTsteps = 16, _FX = 18
	if(X.Sz() < 26243 || !isWithin(X.U32(0),0xD80,0xD81)) return false;
	if(!(ptn=X.U32(4)) || !(x=X.U32(8)) || !(ins=X.U32(0xC))) return false;
	if(!isAllZeroes(0x10,0x24)) return false; //reserved
	mbpm = 400; Mbpm = ch = smp = 0; ord = []; titles = [];
	for(s=0,p=0x34; s < x && p < X.Sz(); s++,p+=0x40B4) {
		// 0x40 bytes padding, 0x10 channel muting
		for(i=p+0x40; i < p+0x50; i++) if(X.U8(i) > 1) return false;
		if(!isWithin(bpm=X.U32(p+0x50),10,300)) return false;
		if(mbpm > bpm) mbpm = bpm; if(Mbpm < bpm) Mbpm = bpm;
		if(!isWithin(X.U8(p+0x54),0,3)) return false; //groove type
		if((startpos=X.U32(p+0x58)) > (endpos=X.U32(p+0x60))) return false;
		if((looppos=X.U32(p+0x68)) > endpos) return false;
//_l2r('jxs',X.U32(p+0x6C),' - loopstep, '+Hex(X.U16(p+0x70))+' - songloop')
		if(!isWithin(t=X.U16(p+0x92),1,16)) return false; if(ch < t) ch = t;
		if(!isWithin(delay=X.U16(p+0x94),2000,60000)) return false; // delay amts follow, until...
		if(!isWithin(amp=X.U16(p+0xA6),20,999)) return false;
		if(X.isDeepScan()) for(o=p+0xB4; o < p+0x40B4; o += 4) // not so necessary but we can do it so...
			if(X.U16(o) >= ptn || !X.U16(o+2) || (X.U16(o+2) & ~0x78)) return false; //ptnlen constraints
		t = X.SC(p+0x72,0x20,"CP1250"); if(t !== 'Empty' && t.trim() !== '') titles.push(t.trim());
		ord.push(endpos)
	}
	p += ptn*64*5; //patterns
	for(i=0; i < ptn && p < X.Sz(); i++) p += 4+X.U32(p); //pattern names
	for(i=0; i < ins && p < X.Sz(); i++,p+=0x2208) { //inst recs
		if([1234,1235].indexOf(X.U16(p)) < 0) return false; //mugi ver
		// sOption(X.SA(p+2,32)) //inst.name; nobody leaves any messages in them though
		if(X.U16(p+0x22) > 16 || !isWithin(X.U16(p+0x24),1,0x100)) return false; //wf, wlen. Should be 1~16 and 10~256, but faulty files still play!
		if(X.U16(p+0x26) > 0xFF || X.U16(p+0x28) > 16 || X.U16(p+0x2A) > 32) return false; //mastervol, am wave/spd
		if(X.U16(p+0x2E) > 15 || X.U16(p+0x30) > 16 || X.U16(p+0x32) > 64) return false; //ft, fm wave/spd; spd should be <= 32 but kingtut.jxs has 57!
		if(X.U32(p+0x200)) smp++; if(t=X.U32(p+0x204)) p += t; //smp len
	}
	if(p > X.Sz()) return false;
	p += 0x100; //arp table
	return true
}
if(!bDetected && isJaytrax()) {
	sName = "Reinier 'Rhino' van Vliet's Jaytrax/Syntrax module (.JXS)"; sVersion = 'v.'+X.U32(0); bDetected = 1;
	if(X.isVerbose()) {
		if(titles.length > 1) sOption(addEllipsis(titles.join('/'),0xC0),'×'+x+' subsongs:');
		else { if(titles.length) sOption(titles[0]); if(x > 1) sOption(x,'×') }
		if(x > 1) ord = ord.join('+');
		else ord = (startpos? startpos+'~': '')+(looppos > startpos? '('+looppos+'-)': '')+endpos;
		sOption('ch:'+ch+' bpm:'+mbpm+(mbpm!=Mbpm? '-'+Mbpm: '')
			+' ord:'+ord+' ptn:'+ptn+' ins:'+ins+' smp:'+smp+' sz:'+outSz(p));
	}
}


function isiXalanceUnp() {
//ref https://github.com/arnaud-neny/rePlayer/blob/646aeb86f4c0c2e57b38292e88f525680c207004/source/Replays/IXalance/webixs/Module.cpp#L481
	if(X.U8(0) != 0x21 || X.U8(9) != 0x80) return false;
	if(!(ord=X.U16(1)) || !(ins=X.U16(3)) || !(smp=X.U16(5)) || smp > ins || !(ptn=X.U16(7))) return false;
	if(!isWithin(mvol=X.U8(0xA),1,0x20) || !isWithin(spd=X.U8(0xB),1,0x20) || !isWithin(tmp=X.U8(0xC),0x20,0xA0)) return false;
	if(charStat(X.readBytes(0xD,0x1A),1).indexOf('allxsc') < 0) return false;
	for(p=0x27; p < 0x67; p++) if(X.U8(p) > 0x40) return false;
	for(p=0xA7; p < 0xA7+ord; p++) if(X.U8(p) != 0xFF && X.U8(p) >= ptn) return false;
	if(X.U8(p-1) != 0xFF) return false;
	var id; rins = 0; rsmp = 0; bad = '';
//_l2r('ixu',p,'insts')
	for(i=0; i < ins && p < X.Sz(); i++)
		if(['I','i'].indexOf(id=X.SA(p++,1)) < 0) return false // the small 'i' is the kb reset?
		else if(id === 'I') {
			if(X.U8(p+2) > 0x80) return false; // normally all volumes are up to 40h, but SOME files...
			p += X.I8(p+5) >= 0? 244: 6; //vol, kb setup
			for(j=3; j; j--) if(X.U8(p++)) {
				if(X.U8(p+1) > X.U8(p+2) || X.U8(p+3) > X.U8(p+4)) return false; //lp/sustainlp beginning/end tests
				p += 5 + X.U8(p)*3; //env node points
			} rins++
		}
//_l2r('ixu',p,'smps')
	for(i=smpsz=0; i < smp && p < X.Sz(); i++) {
		id = X.SA(p++,1);
		if(['S','s'].indexOf(id) < 0) return false; // the small 's' is the flags reset
		if(id == 'S') {
			if(X.U8(p) > 0x40 || X.U8(p+2) > 0x40) return false; //gvl, volume
			var fl = X.U8(p+1), ssz = X.I32(p+3), lpb = X.U32(p+7), lpe = X.U32(p+0xB),
				slpb = X.U32(p+0xE), slpe = X.U32(p+0x13), fn = X.readBytes(p+0x20,13);
			if(charStat(fn,1).indexOf('allxsc') < 0) return false; fn = decEncoding(fn,CP437);
//_l2r('ixu',p,'smp'+i+': ['+ssz+'] ('+lpb+'~'+lpe+') s('+slpb+'~'+slpe+')'+fn)
			switch(fn) { // broken(?) samples (I guess IT214 compression played a part)
			case '909OH.WAV': ssz = 3424; break;
			case '909SD.WAV': ssz = 3413; break;
			case 'CRASH.WAV': ssz = 16098
			}
			if(lpb > ssz || lpe > ssz || slpb > ssz || slpe > ssz || lpb > lpe || slpb > slpe) return false;
			smpsz += ssz; p += 0x2D; rsmp++
		}
	}
//_l2r('ixu',p,'ptns')
	for(i=0; i < ptn && p < X.Sz(); i++) {
		id = X.SA(p++,1);
		if(['P','p'].indexOf(id) < 0) return false; // the small 'p' is the flags reset
		if(id == 'P') {
			var psz = X.U16(p), rows = X.U16(p+2);
			if(!psz || !rows) return false;
			p += 4+psz
		}
	}
	j = X.U32(p); p += 4;
	// In principle, this marks the end of replayable part, but there's more info about SFXI to potentially account for.
	// In practice, maybe it can be copied from .IT files
//_l2r('ixu',p,'input for wfsmp x'+j)
	for(i=0; i < j && p < X.Sz(); i++,p+=0x10C) {
		if(charStat(X.readBytes(p,8),1).indexOf('allxsc') < 0) break
//_l2r('ixu',p,i+': '+X.SA(p+4,8))
	}
	j = X.U8(p++);
//_l2r('ixu',p,'input for synsmp x'+j)
	return true
}
if(!bDetected && isiXalanceUnp()) {
	sName = "Sahara Surfers' iXalance module (.IXS)"; sVersion = 'unpacked'; bDetected = 1;
	if(bad.length) sVersion = sVersion.appendS('malformed'+bad,'/');
	if(X.isVerbose()) {
		sOptionT(X.SA(0xD,0x1A,'CP1250'));
		sOption('ord:'+ord+' ptn:'+ptn+' ins:'+(rins!=ins?rins+'/':'')+ins
			+' smp:'+(rsmp!=smp?rsmp+'/':'')+smp+' smpsz:'+smpsz+'s sz:'+outSz(p))
	}
}


function isOPX() {
	var p = 0x7E;
	if(X.c("200D0A1A",0x7A)) {
		if(!X.c("0D0A",0x35) || !X.c("0D0A",0x6C)) return; //obligatory line breaks?
	} else if(!X.c("20202020",0x7A)) //wiped out with spaces!
		// 2 fringe cases:
		if(X.c("04C50964c504641A2609810F9919008C82100C830382100CC464C464C464C4641A"))
			p = 0; // no header
		else if(X.c("'msx2'00469E2A4E8A22626C2D'bl-demo.bas'00",0xA2F) && X.c("'OPXPLAY SYS'00",0xB2C))
			p = 0xC7E; //a bit too much of a header :o
		else return;
	if(X.U8(0) != 4) for(i=0; i < 0x35; i++) if(X.U8(i) < 0x20) return;
	if(X.U8(p+1) > 2 || X.U8(p+3) > 0x3F || X.U8(p+4) > 6 || X.U8(p+6) > 5 || X.U8(p+7) > 0x21 || X.U8(p+8) > 1
		|| X.U8(p+9) > 0x4F || X.U8(p+10) || X.U8(p+12) > 1 || X.U8(p+14) > 3 || X.U8(p+16) > 3 || X.U8(p+18) > 3) return;
	return true
}
if(!bDetected && isOPX()) {
	sName = "MSX Performer OPX chiptune (.OPX)"; bDetected = 1;
	if(X.isVerbose()) {
		sOption(addEllipsis(X.SC(0,0x7B,'SJIS')))
	}
}


// new stable detections :go here ↑
// BELOW THIS POINT ARE THE DETECTIONS THAT ARE IFFIER THAN THE REST

// There are sanity-checked formats that nevertheless have some signature in a known place;
// those that have some signatures that aren't just "0000" in calculated places;
// those that are checked for pure mathematical dependencies and possible byte ranges;
// those that are very heavy to that, and those less so.
//
// Some are in dire need of amending, and they're set to go after the other detections,
// so you won't miss the good matches because of these and will hopefully spend less time waiting for detects


function isImagesMusicSystem() { //goes before Soundtracker .STK for speed
	//from https://gitlab.com/uade-music-player/uade/-/raw/master/amigasrc/players/wanted_team/ImagesMusicSystem/src/Images Music System_v3.asm
	//is this a hack of Soundtracker? Vice versa?
	if(X.Sz() < 0x73C) return false;
	smpp = X.U32(0x438,_BE); if(smpp <= 0x43C) return false;
	d1h = (smpp-0x43C) % 0x300; if(d1h) return false;
	ptn = d2 = Util.div64(smpp-0x43C,0x300); ord = X.U8(0x3B6); if(ord > 127) return false;
	mptn = 0;
	for(i = 0; i <= ord; i++) { t = X.U8(0x3B8+i); if(t > 127) return false; if (mptn < t) mptn = t }
	if(ptn != mptn+1) return false;
	smp = smpsz = 0; smps = []; bad = ""; smpns = [];
	for(i = 0; i < 31; i++) {
		if(X.isVerbose()) smpns.push(X.SC(0x14+i*30,0x16,'CP437').trim());
		t = X.U16(0x14+i*30+0x16,_BE);
		if(t) { smp++; smpsz += t<<1 }
	}
	msmp = smpns[smpns.length-1];
	sz = smpp+smpsz;
	if(sz < X.Sz()) bad = bad.addIfNone("!short");

	a1 = 0x3B8; d1 = ord; x = a0 = notes = 0;
	if(X.isVerbose()) {
	  do { //NextPos
		a0 = 0x43C+X.U8(a1)*0x300; a3 = a0+0x300;
		a1++;
		do { //NextPatPos
			var z = X.readBytes(a0,2), nsmp = ((z[0] & 0xC0) >> 2) | (z[1] >> 4);
			if(z[0] & 0x3F) { notes++; if(debug && nsmp > msmp) bad = bad.addIfNone("!missingsmp#"+nsmp) }
			if(z[1] & 0x0F === 0x0B) { x++; break }
			else a0 += 3
		} while(a0 < a3 && a0 < X.Sz()); d1--
	  } while(d1);
	  if(!x) x = 1;
	} else x = 1;
	if(smpp === 28732 && smpsz === 12898 && X.c("'beast-busters1.st'") )
		x = 11;
	return true
}
if(!bDetected && isImagesMusicSystem()) {
	//https://www.exotica.org.uk/wiki/Images_(format)
	sName = "Neil Crossley's Images Music System module (.IMS)"; bDetected = 1;
	sVersion = "v1.0"; if(bad != "") sVersion = sVersion.appendS("malformed"+bad,"/");
	if(X.isVerbose()) {
		sOption(X.SC(0,20,'CP437')); if(x > 1) sOption(x,"×");
		sOption(addEllipsis(smpns.filter(funSampleName).join(","),0x100,0xA0),'smp/msg:"','"');
		sOption("ord:"+ord+" ptn:"+ptn+" smp:"+smp+" sz:"+outSz(sz))
	}
}


function isSoundtrackerMOD() { //goes after IMS for speed
	//ref http://asle.free.fr/prowiz/prowiz.src.zip / r/SoundTracker.c
	// & https://github.com/OpenMPT/openmpt/blob/master/soundlib/Load_mod.cpp
	if(X.Sz() < 555) return false;
	function invalidChars(buf) {
		var c = 0; for(var i = 0; i < buf.length; i++) {
			if(buf[i] && (buf[i] < 0x20 || isWithin(0x80,0xFE)) && [0xA,0xD,0xE].indexOf(buf[i]) < 0) c++;
		} return c
	}
	function re(p,t) { if(debug>1)_l2r('stk',p,t); return false }
	function npTest(n) { const valid = [0, 856,808,762,720,678,640,604,570,538,508,480,453,
		  428,404,381,360,339,320,302,285,269,254,240,226,
		  214,202,190,180,170,160,151,143,135,127,120,113];
		return valid.indexOf(n) >= 0 || valid.indexOf(n-1) >= 0 || valid.indexOf(n+1) >= 0
	}
	const UST1_00=0,UST1_80=1,ST2_00_Exterminator=2,ST_III=3,ST_IX=4,MST1_00=5,ST2_00=6;
	var ic = 0, hasDiskNames = true, minV = UST1_00;
	ic = invalidChars(X.readBytes(0,20));
	bad = ""; allsmpsz = allvols = smp = minV = 0; snames = [];
	for(k=0; k < 15; k++) {
		var sn = X.readBytes(20+k*30,22,true), sname = decEncoding(sn,CPAmiga).trim();
		if(/ST-[0-9A-F]+:/i.test(sname)) ic -= 4; else snames.push(sname);
		ic += invalidChars(sn);
		re(20+k*30,"STKsmp["+k+'] "'+sname.trim()+'" invalid chars:'+ic+" ("+charStat(sn,1)+")")
		if(ic > 10) return re(20+k*30,"!title or sample names too broken");
		if(X.U8(20+k*30) && !/^st-[0-9a-f]\d:/i.test(sname))
			hasDiskNames = false;
		slen = X.U16(42+k*30,_BE)*2;
		if(slen > 0x12000) return re(42+k*30,"!smp #"+k+" too large:"+Hex(slen));
		if(X.U8(44+k*30) > 15) { bad = bad.addIfNone("!finetune"); ic+=16 } //finetune
		svol = X.U8(45+k*30); if(svol > 0x40) return false;
		sls = X.U16(46+k*30,_BE), sll = X.U16(48+k*30,_BE);
		if(slen > 9998 || sls > 4999) minV = Math.max(minV,MST1_00);
		if(slen) smp++;
		allsmpsz += slen; allvols += svol;
//_log("smp["+k+"] <"+X.SA(20+k*30,22)+"> ["+slen+"] @"+Hex(20+k*30)+" -> vol:"+svol+" loopstart:"+sls+" looplen:"+sll);
		if(sll > slen+2) {
			re(42+k*30,"!smp "+k+" LpLen "+sll+" > sSz "+slen); bad = bad.addIfNone("!sLpLen>sSz") }
		if(slen && sls >= slen) {
			re(46+k*30,"!smp "+k+" LpStart "+sls+" >= sSz "+slen); bad = bad.addIfNone("!sLpStart>=sSz") }
		if(sls && !sll) {
			re(42+k*30,"!smp "+k+" LpStart "+sls+" LpSz 0"); bad = bad.addIfNone("!sLpStart0LpSz") }
		if(sls && !slen) {
			re(46+k*30,"!smp "+k+" LpStart "+sls+" sSz 0"); bad = bad.addIfNone("!sLpStart0SSz") }
	}
//_log("5:allsmpsz="+allsmpsz+" allvols="+allvols);
	if(allsmpsz < 8 || !allvols) return re(20,"!smps too small or no smpvols");
	ord = X.U8(0x1D6); if(ord > 128) return false;
	restartp = X.U8(0x1D7); if(restartp > 220) return false;
	if(!restartp || /jjk55/.test(X.SA(0,6))) restartp = 0x78;
	bpm0 = 125;
	if(restartp != 0x78) {
		bpm0 = (709379*125 / (50*122*(240-restartp))).toFixed(2); //between ≈60 and ≈976
		if(minV > UST1_80) minV = Math.max(minV, hasDiskNames?ST_IX:MST1_00);
		else minV = Math.max(minV, hasDiskNames?UST1_80:ST2_00_Exterminator);
	}
	ptn = offptn = badptn = ord_ = 0; usedptns = []; usedsmps = [];
	for(o=0; o < 128; o++) {
		x = X.U8(0x1D8+o); if(x > 63) return re(0x1D8+o,"!impossible ptn #"+x);
		if(ptn <= x) {
			ptn = x+1;
			if(o < ord) { offptn = ptn; if(usedptns.indexOf(x) < 0) usedptns.push(x) }
		}
		if(x >= badptn) badptn = ptn+1;
		if(x) ord_++; //a heuristic to count out suspiciously many zeroes
	} ord_++;
//_log("ptn:"+ptn+" off.ptn:"+offptn+" badptn:"+badptn+" used ptns:"+usedptns)
	songszoffptn = 0x258+0x400*offptn;
	if(songszoffptn > X.Sz()) return re(songszoffptn,"!patterns cut off");
	ic = tnDxx = notes = 0;
	for(i=0; i < ptn; i++) {
		emptycmd = nDxx = badnote = ptnic = ptnotes = 0;
		for(row=0; row < 64; row++) for (chn=0; chn < 4; chn++) {
			var ofs = 0x258+(i<<10)+(row<<4)+(chn<<2);
			x = X.readBytes(ofs, 4);
			if(X.isDeepScan()) {
				if(!emptycmd && !x[0] && !x[1] && !x[2] && !x[3]) {
					emptycmd++; if(emptycmd > 32) minV = ST2_00
				} else emptycmd = 0 }
			var smpn = (x[0]&0xF0) | ((x[2]&0xF0) >> 4), np = ((x[0]&0xF) << 8) + x[1], fx = x[2]&0xF;
//if(debug>2)_log("@"+Hex(ofs)+" ptn "+i+": "+Hex(x[0])+" "+Hex(x[1])+" "+Hex(x[2])+" "+Hex(x[3])+": smp#"+smpn+" np:"+Hex(np)+" fx:"+Hex(fx));
			if(usedptns.indexOf(i) >= 0)
				if(smpn > 15) { ptnic++;
//if(debug>2)_log("   bad smp "+np+" @"+Hex(ofs)+" ptn"+i+" row"+row+" chn"+chn)
				}
				else if(usedsmps.indexOf(smpn) < 0) usedsmps.push(smpn);
			if((X.isDeepScan() || notes < 100) && !npTest(np)) { ptnic += 2; badnote++;
//if(debug>2)_log("   bad note "+np+" @"+Hex(ofs)+" ptn"+i+" row"+row+" chn"+chn)
			} else if(np) ptnotes++;
			if(X.isDeepScan()) switch(fx) {
			case 1: case 2:
				if(x[3] > 0x1F && minV == UST1_80) minV = hasDiskNames ? UST1_80 : UST1_00;
				else if(fx == 1 && x[3] > 0 && x[3] < 3) minV = Math.max(minV,ST2_00_Exterminator);
				else if(fx == 1 && (x[3] == 0x37 || x[3] == 0x47) && minV <= ST2_00_Exterminator)
					minV = hasDiskNames ? UST1_80 : UST1_00;
				break;
			case 0xB: minV = ST2_00; break;
			case 0xC: case 0xD: case 0xE: minV = Math.max(minV,ST2_00_Exterminator);
				if(fx == 0xD) { emptycmd = 1; if(!x[3] && !row) break; nDxx++ } break;
			case 0xF: if(minV < ST_III) minV = ST_III; break;
			}
		}
		if(ptn >= offptn && ptnic > 64) ptn = offptn;
		else { ic += ptnic; notes += ptnotes; if(badnote) bad = bad.addIfNone('!badnotes') }
		if(ic > Math.max(512,ptn*128)) return re('!badbytes:'+ic);
		if(nDxx && nDxx < 3) minV = ST2_00; tnDxx += nDxx;
	}
	if(tnDxx > ptn+32 && minV == ST2_00) minV = MST1_00;
	if(X.isDeepScan()) switch(minV) {
	case UST1_00: tracker = "Karsten Obarski's Ultimate ST 1.0~21"; break;
	case UST1_80: tracker = "Karsten Obarski's Ultimate ST 1.8~2.0"; break;
	case ST2_00_Exterminator: tracker = "The Exterminator's ST 2.0 / D.O.C.'s ST II"; break;
	case ST_III: tracker = "Il Scuro/Defjam's ST III / Alpha Flight ST IV / D.O.C.'s' ST IV / VI"; break;
	case ST_IX: tracker = "D.O.C.'s' ST IX"; break;
	case MST1_00: tracker = "Tip/The New Masters' Master ST 1.0"; break;
	case ST2_00: tracker = "D.O.C.'s ST 2.0~2"; break;
	default: tracker = "???"
	}
	sz = 0x258+0x400*ptn+allsmpsz;
	var sizediff = Math.abs(sz-X.Sz()), orddiff = Math.abs(ord-ord_);
	var nzord = ord?ord:0.0001, sus = 0;
	if(ic > 20) sus++; if(!isWithin(smp,3,15)) sus++;  if(sizediff > 2048) sus++;  if(sz > X.Sz()) sus++;
	if(!restartp) sus++;  if(!ord || !ord_ || badptn > ptn+2) sus++;  if(orddiff/nzord > 0.8) sus++;
	if(!ptn) sus++;  if(notes < 2) sus++;  if(allvols < smp) sus++;
	if(!isWithin(bpm0,20,300)) sus++;
//_log("ic="+ic+" smp="+smp+" sizediff="+sizediff+" ord||ptn="+(ord||ord_||ptn)+" maxptn="+(badptn>ptn+2)+" od%="+(orddiff/nzord).toFixed(1)+" notes:"+notes+" bpm0:"+bpm0+" how sus:"+sus)
	if(sus >= 4) // free strikes
		// If there are enough bad notes, the samples are kinda few or kinda many,
		//the size is more than two patterns' lengths different (especially smaller),
		//either no length or no "official" patterns are there, the orderlist is mostly zeroes,
		//it's just a couple notes or they're strange periods, the avg volume's too low, and the bpm0's weird...
		return re(sus,'!too suspicious');
	if(ic) bad = bad.addIfNone('!baddata='+ic);
	if(sz > X.Sz()) bad = bad.addIfNone('!short');
	if(sus) bad = bad.addIfNone('!sus+'+sus)
	return true;
}
if(!bDetected && isSoundtrackerMOD()) {
	sName = "Karsten Obarski's SoundTracker module (.STK,.MOD)"; bDetected = 1;
	if(bad != "") sVersion = 'malformed'+bad;
	if(X.isVerbose()) {
		sOptionT(X.SA(0,0x14));
		if(X.isDeepScan()) sOption(tracker,"in:");
		sOption(snames.filter(funSampleName).join(" "),'smps/msg:"','"');
		sOption('bpm0:'+bpm0+' ord:'+ord+(ord_!=ord? '/'+ord_: '')+' ptn:'+(offptn!=ptn? offptn+'/': '')+ptn
			+' smp:'+smp+(X.isDeepScan()? ' notes:'+notes: '')+' sz:'+outSz(sz))
	}
}


function isComposer670() {
	spd = X.U8(0); if(!isWithin(spd,1,15)) return false;
	ord = X.U8(1); if(!ord || ord >= 128) return false;
	ptn = X.U8(2); if(!ptn) return false; //while 0 is possible, there's no way we'll reliably detect the file then
	smp = X.U8(3); if(smp > 0x20) return false;
	opl = X.U8(4); lp = X.U8(5); if(lp > ord) return false;
	smpp = X.U32(6); if(smpp > X.Sz()) return false;
	p = 0xA; for(i=0; i < ord; i++) if(X.U8(p++) >= ptn) return false;
	ptnpt = p; p += ptn*4;
	for(i=smpsz=0; i < smp; i++,p+=16) {
		if(X.U32(p)) return false;
		var _l = X.U32(p+4), _ls = X.U32(p+8), _le = X.U32(p+12);
		if(!_l || _l > 0xFFFFF) return false; smpsz += _l;
		if(_le < 0xFFFFF && (_le > _l || _ls > _le)) return false
	}
	for(i=0; i < opl; i++,p+=11) {
		if(X.U8(p) & 0xF0) return false;
		if(X.U8(p+5) & 0xFC) return false;
		if(X.U8(p+10) & 0xFC) return false;
	}
	for(i=notes=0,ptnp=p,bad=false; i < ptn; i++) {
		p = ptnp+X.U32(ptnpt+i*4); if(p > smpp-3) return false;
		if(i >= ptn-1) ptnend = smpp; else ptnend = ptnp+X.U32(ptnpt+i*4+4);
		if(!isWithin(ptnend - p, 3,0x1000)) return false;
		for(r = 0; p < ptnend;) {
			var cmd = X.U8(p++);
			if(cmd <= 0xC) { p += 2; notes++ }
			else if(cmd >= 0x20 && cmd <= 0x2C) p++;
			else if(cmd == 0x40) r += X.U8(p++);
			else if(cmd == 0x60) break;
			else return false
		}
		if(r > 64) return false
	}
	sz = smpsz+smpp;
	return opl+smp > 0
}
if(!bDetected && isComposer670()) {
	sName = "CDFM/Composer 670 module (.670)"; sVersion = 'compact'; bDetected = 1;
	if(bad) sVersion = "malformed!badptn";
	if(X.isVerbose()) sOption('spd:'+spd+' ord:'+ord+(lp?' lp:'+lp:'')+' ptn:'+ptn+' smp:'+smp+' fm:'+opl+' notes:'+notes+' sz:'+outSz(sz))
}


function isLDS() {
	//ref https://github.com/adplug/adplug/blob/master/src/lds.cpp
	if(X.Sz() < 136) return false; if((mode = X.U8(0)) > 2) return false;
	if(X.U8(1) > 0x1F && X.U8(1) != 0xD0) return false; //speed, byte1
	if(X.U8(2) < 0x42 || X.U8(2) > 0x43) return false; //speed, byte2
	if((tempo = X.U8(3)) < 3 || tempo > 0x1F || ((ptnsz = X.U8(4)) & 0x87)) return false;
	for(i=5; i < 14; i++) if(X.U8(i) > 4) return false; if((regbd = X.U8(0xE)) > 2) return false;
	if(!(ins = X.U16(0xF)) || ins > 0x3F) return false;
	for(p = 0x11; p < 0x11+0x2E*ins; p += 0x2E) {
		if(X.U8(p+4) > 3 && X.U8(p+4) != 0xE1 || X.U8(p+9) > 4) return false;
		if(X.U8(p+0xA) > 0x7F || (X.U8(p+0xC) & 0xC0)) return false;
		if(!isWithin(X.I8(p+0x15), -0x30, 0x30) || !isWithin(X.I8(p+0x17), -0x30, 0x30)) return false;
		if(!isWithin(X.I8(p+0x1B), -0x30, 0x30) || !isWithin(X.I8(p+0x20), -0x30, 0x30)) return false;
		if((X.U8(p+0x24) > 0x30) || X.U8(p+0x25) > 1 || !isWithin(X.I8(p+0x27), -4, 0)) return false;
		if(isInside(X.U8(p+0x2B), 0x30, 0xE8) || X.U16(p+0x2C)) return false;
	}
	ord = X.U16(p); if(!ord || ord > 0x60) return false; p += 2; mp = -1; ptnp = p+ord*9*3+2;
	for(i=0; i < ord; i++) for(j=0; j < 9; j++) {
			pt = X.U16(p); if(pt%2 || pt+ptnp > X.Sz() || pt > 0x4000) return false; if(mp < pt) mp = pt; p += 3;
		}
	digisnd = X.U16(p); p += 2; sz = ptnp+digisnd; //digital sounds value seems to point just beyond the patterns!
	if(X.Sz() < sz) return false; ptn = Util.divu64(digisnd,2);
	return true
}
if(!bDetected && isLDS()) {
	sName = 'Loudness Sound System Ad Lib module (.LDS)'; bDetected = 1;
	if(X.isVerbose()) sOption('ch:9 mode:'+mode+' spd:'+X.U16(1)+' tempo:'+tempo
		+' ord:'+ord+' ptn:'+ptn+' ins:'+ins+' sz:'+outSz(sz))
}


function isKSM() {
	for(i=0; i < 4; i++) if(!isWithin(X.U8(i),0,0x57)) return false;
	for(; i < 0xB; i++) if(X.U8(i)) return false;  for(; i < 0x10; i++) if(X.U8(i) < 0xFB) return false;
	for(; i < 0x20; i++) if([0xF0,4,6,8,0xC].indexOf(X.U8(i)) < 0) return false;
	for(c=0; i < 0x25; i++) if(X.U8(i) > 5) return false; else if(!X.U8(i)) c++; if(c > 3) return false;
	for(; i < 0x2A; i++) if(X.U8(i)) return false;  for(; i < 0x30; i++) if(X.U8(i) > 1) return false;
	for(i=0x40; i < 0x50; i++) if(!isWithin(X.U8(i),0x20,0x3F)) return false;
	if(!X.c("32323232 323232",0x44)) return false;
	return true
}
if(!bDetected && isKSM()) {
	sName = "Ken Silverman's Adlib module (.KSM)"; bDetected = 1;
	if(X.isVerbose()) sOption('notes:'+(notes=X.U16(0x50))+' sz:'+outSz(0x52+notes*4))
}


function isTMC() {
	function rt(p,m) { if(debug>1)_l2r('tmc',p,m); return false }
	//ref http://atariki.krap.pl/index.php/TMC
	if(X.Sz() < 0x1D0 || X.U8(0x23) != 0x20) return false; //data starts from 1A0+instr.+ptns., obligatory space
	if((bin = parseAtariBinary())[0] < 0x1D6) return rt('!binsz'); // an Atari binary, of this size
	if(X.U8(bin[1][0][1]+5) != 0xFF) return rt(0,'!ptnend'+outArray(bin,16)); // last ptn ends with FF
	if(!isWithin(spd0=X.U8(0x24),1,0x10) || !isWithin(ticks=X.U8(0x25),1,4)) return rt(0x25,'!spd'+spd0+'/'+ticks);
	for(ins=t0=0,p0=0x10000,p=0x26; p < 0x66; p++) {
		if(!X.U8(p) && !X.U8(p+0x40)) continue; if([0,9].indexOf(X.U8(p)&0xF) < 0) return rt('!susins');
		t = (X.U8(p+0x40) << 8)+X.U8(p)+6-X.U16(2); if(t < 0 || t <= t0) return rt(t,'!-insptr');
		t0 = t; if(t > 0) { if(!isWithin(t,0x1A0,bin[1][0][1]+6)) return rt(t,'!insptr'); else ins++;  if(p0 > t) p0 = t }
	}
	if(!ins) return rt(p0,'!0inst');  if([0,9].indexOf(X.U8(0xA6) & 0xF) < 0) return rt(p0,'!0ptnptr'); //still depends on instrument record length
	for(ptn=t0=0,p=0xA6; p < 0x126; p++) {
		t = (X.U8(p+0x80) << 8)+X.U8(p)-X.U16(2)+6; if(p0 > t) p0 = t; if(t < 0 || t <= t0) return rt(t,'!-ptnptr');  t0 = t;
		if(t && !isWithin(t,0x1A0,bin[1][0][1]+6)) return rt(t,'!ptnptr');  if(t && X.U8(t) != 0xFF) ptn++
	}
	ord = (p0-0x1A6) >> 4;
	for(p=0x1A7,pt=-1,ic=0; p < p0; p += 2)
		if(!X.c("FF",p) && !X.c("7F",p))
			if(X.U8(p) > 0x7F) ic++; else if(X.U8(p) >= pt) pt = X.U8(p)+1;
	return true
}
if(!bDetected && isTMC()) {
	sName = "Marcin 'Jaskier' Lewandowski's Theta Music Composer module (.TMC,.TM4,.TM8)"; sVersion = 'v1.x'; bDetected = 1;
	if(ic) sVersion = sVersion.appendS('malformed!'+ic+'ptns','/');
	if(X.isVerbose()) {
		sOption(decAnsi(6,0x1E,CPATASCII,Chars0to1FATASCII_PL)); //Polish is prevalent, so...
		sOption('spd0:'+spd0+' ticks:'+ticks+' ord:'+ord+' ptn:'+(pt!=ptn?pt+'/':'')+ptn+' ins:'+ins+' sz:'+outSz(bin[0]))
	}
}


function isRMT() {
	function rt(p,m) { if(debug>1)_l2r('rmt',p,m); return false }
	//ref http://atariki.krap.pl/index.php/RMT_%28format_pliku%29 & http://atariki.krap.pl/index.php/Rmt
	if((bin = parseAtariBinary(0))[0] < 0x100) return rt('!binsz');
	base = X.U16(2);
	p = bin[1][0][0]; ch = X.U8(p+3)-0x30;
	if(!X.c("'RMT'",p) || ![4,8].includes(ch)) return;
	var trklen = X.U8(p+4); if(!trklen) trklen = 256;
	spd0 = X.U8(p+5);  if(!spd) return; ticks = X.U8(p+6); if(!ticks) return;  v = X.U8(p+7);
	var instp = p+X.U16(p+8)-base, trktlp = p+X.U16(p+0xA)-base, trkthp = p+X.U16(p+0xC)-base,
	trklstp = p+X.U16(p+0xE)-base;
//_l2r('rmt',p,'instp='+Hex(instp)+' trktlp='+Hex(trktlp)+' trkthp='+Hex(trkthp)+' trklstp='+Hex(trklstp));
	if(instp > X.Sz() || trktlp < instp || trkthp < trktlp || trklstp < trkthp
		|| trktlp > X.Sz() || trkthp > X.Sz() || trklstp > X.Sz()) return;
	//TODO extract more data
	return true
}
if(!bDetected && isRMT()) {
	sName = "Radik 'Raster' Štěrba's Raster Music Tracker module (.RMT)"; sVersion = 'v'+v; bDetected = 1;
	if(X.isVerbose()) {
		if(bin[1].length > 1) sOption('metadata present'); //TODO; don't have sample files
		sOption('ch:'+ch+' spd0:'+spd0+' ticks:'+ticks+' sz:'+outSz(bin[0]));
	}
}


function isPollyTracker() {
	//A trivial RLE compression with AE as the special command and a few glitches.
	//Underneath that, a trivial format that also messes up the lowercase letters for no reason.
	//Everything's space-padded, even the samples.
	if(!X.c("AE") || X.Sz() < 121) return false;
	p = 1; sz = unpsz = -1; var o = end = lc = 0; var u = Array(0x10000); var last = [-1,-2,-3,-4];
	function rB(q) { //this exists just to check for the unpacked sequences, we actually have better checks
		last[lc++] = X.U8(q); if(lc > 3) lc = 0;
		//PT packs sequences over 3 ch long so any we encounter in the file is cause enough to break off
		if(last[0] === last[1] === last[2] === last[3]) { end = 1; return -1 }
		else return X.U8(q)
	}
	function ret(s) { delete u; if(debug)_log(s); return false }
	while(!end && o < (X.isDeepScan() ? 65536 : 0x100) && p < Math.min(X.isDeepScan() ? 65535: 0x100,X.Sz())) {
		t = rB(p++);
		if(t == 0xAE) {
			c = rB(p++); if(c < 0) return ret("PTFault: bad count");
			if(!c) { end = true; break }
			else if(c === 1) { u[o++] = 0xAE; continue }
			b = rB(p++); if(b < 0) return ret("PTFault: bad byte");
			for(;c; c--) u[o++] = b
		}
		else u[o++] = t
	}
	if((tempo = u[0x1FC1]) < 0x10) return ret("PTFault: tempo < 10h");
	if(u[0x1F80] || u[0x1F90]) return ret('PTFault: nonzero smp#0');
	if(o%0x100) //fixing what seems like the packer's glitch. The lil bud always picks up some extra bytes
		while(o%0x100 && u[o-1] === 0x20) o--;
	if(o%0x100 || (X.isDeepScan() && o < 8192) || o > 65535 || p > 65535)
		return ret('PTFault: bad range! o='+Hex(o)+' p='+Hex(p));
	if((modsz = ((u[0x1FC3]+0x10) << 8)) != o)
		return ret('PTFault: allsmpsz mismatch: '+Hex(modsz)+' != '+Hex(o));
	var msmp = [4,12]; smp = 0;
	for(i=0x1F81; i < 0x1F90; i++) { // find max sample, count samples included
		if(u[i] >= 0x10) smp++; if(u[i] > msmp[0]) { msmp[0] = u[i]; msmp[1] = u[i+0x10] }
	}
	if((lastsmpend = ((msmp[0]+0x10)<<8) + (msmp[1]<<8)) != o)
		return ret('PTFault: last smp end '+Hex(lastsmpend)+' != '+Hex(o));
	notes = ic = 0;
	for(i=0; i < Math.min(0x1F00,o); i++) //test all the patterns, it's still fast
		//Notes aren't allowed to have smp#0 or note#0 but be a non-zero byte.
		//But PT v1.2 is glitchy, and earlier versions clearly allowed smp#0, so it does sometimes happen.
		if(u[i] && u[i] != 0xF0) { notes++; if((u[i]&0xF0) == u[i]) ic++ }
	if(X.isDeepScan()) for(i=0x2000; i < o; i++) //test all the samples
		if(u[i] > 0x3F) return ret('PTFault: bad sample @'+Hex(i));
	unpsz = o; sz = p; ord = ptn = 0;
	for(i = 0x1F00; i < 0x1F80; i++) { if(!u[i]) break; ord++; if((pt = u[i]-0xDF) > ptn) ptn = pt }
	if(ic > ptn<<2) return ret('PTFault: '+ic+' bad notes');
	// Let's adapt the text info too.
	for(i = 0x1FA0; i < 0x1FC0; i++) { if(u[i] && u[i] < 0x1A) u[i] += 0x60 }
	info = decEncoding(u.slice(0x1FA0,0x1FB0),CPAmiga);
	composer = decEncoding(u.slice(0x1FB0,0x1FC0),CPAmiga);
	delete u;

	return true;
}
if(!bDetected && isPollyTracker()) {
	sName = "Polly Tracker module (.MOD)"; bDetected = 1;
	if(X.isVerbose() && X.isDeepScan()) {
		sOptionT(info); sOptionT(composer,'by:');
		sOption('tempo:'+Hex(tempo)+' ord:'+ord+' ptn:'+ptn+' smp:'+smp+' notes:'+notes+' unpsz:'+unpsz+' sz:'+outSz(sz))
	}
}


function isSidMUS() {
	//ref NostalgicPlayer Source/Ports/LibSidPlayFp/SidTune/Mus.cs :: Detect, TryLoad
	// & https://github.com/MyDeveloperThoughts/ComputeSidPlayerC64Source/blob/main/notes/musFileFormat.md
	if(X.Sz() < 27) return;
	const HLT = 0x014F;
	var loadaddr = X.U16(0), len1 = X.U16(2), len2 = X.U16(4), len3 = X.U16(6);
	var v1p = 8+len1, v2p = v1p+len2, v3p = v2p+len3;
	if(v3p > X.Sz() || (v1p & 1) || (v2p & 1) || (v3p & 1)) return;
	if(!X.c("014F",v1p-2) || !X.c("014F",v2p-2) || !X.c("014F",v3p-2)) return; // voices must HLT at the end
	sz = v3p; t = "";
	if(X.Sz() > v3p && (p=X.fSig(v3p,0x1000,"00")-v3p+1) > 0) {
		t = decAnsi(v3p,p,CPFullCPETshifted,true).trim(); sz += p
	}
	return true
}
if(!bDetected && isSidMUS()) {
	sName = "COMPUTE!'s Enhanced SidPlayer tune (.MUS+.STR+.WDS)"; bDetected = 1;
	if(X.isVerbose()) {
		sOption((t.length?'"'+addEllipsis(t,0x100,0xA0)+'"':'').append('sz:'+outSz(sz)));
	}
}


function isUltima6M() {
	//ref https://github.com/adplug/adplug/blob/master/src/u6m.cpp
	if(X.Sz() < 421 || !isWithin(unpsz=X.U16(0),0x30E,0xB21) || !X.c('00000007',2)) return false; //a rough filter
	// a finer filter: actually unpacking LZW and double-checking there
	var C, cW, o = 0, lim = Math.min(X.Sz(),unpsz), br = new BitReader(4), end = false,
		pW = 0, cwsz = 9, nextcw = 0x102, stk = [],
		dictsz = defdictsz = 0x1000, dict = [], contains = 0x102;
	for(i=0; i < dictsz-0x100; i++) dict.push({'rt':0, 'cw':0}) //new dictionary
	var dest = []; for(i=0; i < unpsz; i++) dest.push(0); //unpacked module
	function getstring(codeword) { //puts the string into a stack
		var c = codeword; while(c > 0xFF) { stk.push(dict[c-0x100]['rt']); c = dict[c-0x100]['cw'] } stk.push(c & 0xFF)
	}
	while(!end && br.offset <= lim && o <= unpsz) { //lzw decomp
		if(cwsz < 9 || cwsz > 12) return false; cW = br.read(cwsz);
		switch(cW) {
			case 0x100: cwsz = 9; nextcw = 0x102; dictsz = 0x200; contains = 0x102; //re-init the dictionary
				if(cwsz < 9 || cwsz > 12) return false; cW = br.read(cwsz); if(o >= unpsz) return false;
				dest[o++] = cW & 0xFF; break;
			case 0x101: end = true; break; //end of data
			default:
				getstring(cW < nextcw ? cW : pW); //codeword is already in the dictionary? get cW; else, pW
				C = stk[stk.length-1] & 0xFF;
				if(cW < nextcw)
					while(stk.length) { if(o >= unpsz) return false; dest[o++] = stk.pop() & 0xFF
					}
				else { //codeword not yet defined
					while(stk.length) { if(o >= unpsz) return false; dest[o++] = C; stk.pop() }
					if(cW != nextcw) return false;
				}
				if(contains < dictsz) { dict[contains-0x100] = { 'rt':C, 'cw':pW }; contains++ }
				nextcw++; if(nextcw >= dictsz) if(cwsz < 12) { cwsz++; dictsz <<= 1 }					
		}
		pW = cW
	}
	if(!end) return false;
	sz = br.offset;
	if(dest[0] != 0x83 || dest[1] || (dest[2]&8) || (dest[3]&0x20) || (dest[5]&0x80) || dest[6] > 1) return false;
//_logBase64(toBase64(dest)); //outputs the unpacked module for further saving
	return true
}
if(!bDetected && isUltima6M()) {
	sName = 'Ultima 6 Adlib module (.M)'; sOption('unpsz:'+unpsz+' sz:'+outSz(sz)); bDetected = 1
}


function isQuartetST() {
	if(!isWithin(X.U16(0,_BE),4,0x10)) return false;
	if(!isWithin(X.U16(2,_BE),8,0x20)) return false;
	if(!isWithin(X.U16(4,_BE),4,0x30)) return false;
	if(!isWithin(X.U8(6),1,4) || !X.c("0400000000",7)) return false;
	if(!X.c("'WT'",0xC) && X.U32(0xC,_BE)) return false;
	if(!isWithin(X.U8(0x17),3,0x77)) return false;
	if((t=X.U32(0x18,_BE)) > 0x4C || (t&3)) return false;
	sz = 0x10; var szlim = Math.min(X.Sz(),0xFFFF); x = 0;
	for(p=0x1C; p < Math.min(X.Sz()-1,0x400); p+=12) if(!isWithin(X.U16(p,_BE),0x46,0x6C)) return false;
	while(sz < szlim) {
		if(!X.c("0056",sz)) break; x++;
		for(i=0; i < 4; i++) for(sz += 2; sz < szlim; sz+=2) if(X.c("0046",sz)) break;
		sz += 0xC;
		break //TODO? Fugue.4v or Sweet Dreams.4v have more info after the 4 iterations, but it looks funny there
//_log(Hex(sz))
	}
	spd0 = X.U16(0xE,_BE);
	return !x || sz <= szlim
}
if(!bDetected && isQuartetST()) {
	sName = "Illusions/Microdeal Quartet module (QTS.+SMP., .4V+.SET)";
	sVersion = "samples"; bDetected = 1;
	if(X.isVerbose()) {
		//if(x > 1) sOption(x,'voices?'); //TODO for the same reason
		sOption((spd0?'spd0:'+spd0+' ':'')+'sz:'+outSz(sz))
	}
}


function isSQDigitalTracker() {
	//ref https://bitbucket.org/zxtune/zxtune/src/develop/src/formats/chiptune/digital/sqdigitaltracker.cpp
	if(X.Sz() < 0x4400) return false;
	if(X.U8(0x100) != 0x7C &&  X.U8(0x11F) != 0x7C) return false; //title surrounded by | |
	if(charStat(X.readBytes(0x100,0x20),1).indexOf('allasc') < 0) return false;
	if(X.U8(0x204) != 0xFF) return false;
	tempo = X.U8(0x210); if(!isWithin(tempo,2,10)) return false;
	ord = X.U8(0x212)+1; if(!isWithin(ord,1,0x65)) return false;
	loop = X.U8(0x211); if(loop >= ord) return false;
	if(charStat(X.readBytes(0x300,0x80),1).indexOf('allasc') < 0) return false;
	sz = 0x4000; //var regions = [];
	for(i=0; i < 8; i++) { // check layouts, calc size
		if(X.U8(0xC0+i*4)) return false; //address, lower byte
		if(!isWithin(X.U8(0xC0+i*4+1),0x80,0xC0)) return false; //address, higher byte
		if(!isWithin((bnk=X.U8(0xC0+i*4+2)),0x58, 0x5F)) return false; //bank
		if(!isWithin((sec=X.U8(0xC0+i*4+3)),1,0x80)) return false; //sectors
		var lst = X.U16(0xC0+i*4), lsz = sec << 8; bnk &= 7;
		//if(lst >= 0xC000 && lst+lsz <= 0x10000) regions[bnk] = [sz, lsz];
		sz += lsz
	}
	if(sz > X.Sz()) return false;
	sz += 0x400; // data at the end; tracker settings? nobody'll miss it, but...
//_l2r('sqd',0x4400,'regions:'+outArray(regions,16))
	title = X.SC(0x101,0x1E,"CP1251").trim();
	for(i=smp=0; i < 0x10; i++) { //sample info parser
		if(X.U8(0x120+i*8+4) > 1) return false;
// 			var rst = X.U16(0x120+i*8), rlp = X.U16(0x120+i*8+2);
// 			if(rst < 0x8000 || rst < rlp) continue;
// 			bnk = X.U8(0x120+i*8+5) & 7; var smpbase = rst < 0xC000? 0x8000: 0xC000;
// //_l2r('sqd',0x120+i*4,'used bank #'+bnk+' at '+Hex(rst)+'..'+Hex(rlp))
// 			if(typeof regions[bnk] != "undefined") {
// 				smp++;
// 				var ssz = Math.min(0x10000-rst,regions[bnk][1]), sp = regions[bnk][0] + rst - smpbase;
// 			}
	}
	for(i=ptn=0; i < 100; i++) if((t = X.U8(0x1A0+i)) > 0x1F) return false;  else if(ptn < t) ptn = t;
	ptn++;
	return true
}
if(isSQDigitalTracker()) {
	sName = "SQ Digital Tracker module (.SQD,.M)"; bDetected = 1;
	if(X.isVerbose()) {
		sOptionT(title); sOption('tempo:'+tempo+' ord:'+ord+(loop?' loop:'+loop:'')+' ptn:'+ptn+' sz:'+outSz(sz));
	}
}


function isSAS() {
	//from the eagleplayer RE
	if(X.U8(0) || !X.c("00000200",0xC)) return false;
	for(p=0x10,ptn=0; p < 0x1010; p += 2) { //orderlist?
		if(X.I32(p) == -1) break; if(X.U8(p)) return false; if(X.U8(p+1) > ptn) ptn = X.U8(p+1)
	}
	ptn++; ord = (p-0x10) >> 3; d2 = p; p = 0x1010 + ptn*0x30;
	while(p < Math.min(X.Sz(),0x30000)) { if(X.U32(p,_BE)) break; p += 4 }
	if(!X.c("'FORM'........'8SVXVHDR'",p+0x280)) return false;
	smptp = p; smp = 0; smps = '';
	for(i=0x20,sz=0; i--; p+=4) { //q for smpstart, p for smpsz?
		if(!X.U8(p)) {
			var sp = smptp+X.I32(p,_BE), ssz = X.I32(p+0x80,_BE);
			if(sp < 0x280 || !X.c("'FORM'........'8SVXVHDR'",sp)) return false;
			smp++; if(sp+ssz > sz) sz = sp+ssz;
			//if((t=X.fSig(sp,0x30,"'NAME'00")) > -1) smps = smps.appendS(X.SA(t+8,X.U32(t+4,_BE)),' ') //not much to show
		}
	}
	return true
}
if(!bDetected && isSAS()) {
	sName = "Michael Winterberg's Speedy A1 System module (.SAS)"; bDetected = 1;
	if(X.isVerbose()) {
		//sOption(smps,'smps:"','"');
		sOption('ord:'+ord+' ptn:'+ptn+' smp:'+smp+' sz:'+outSz(sz))
	}
}


function isMFP() {
	// from the binary eagleplayer REing (it's actually different for some reason)
	// ref https://gitlab.com/uade-music-player/uade/-/raw/master/amigasrc/players/wanted_team/MagneticFieldsPacker/src/Magnetic Fields Packer_v3.asm
	if(X.Sz() < 0x17E || !isWithin(ord=X.U8(0xF8), 1, 0x7F) || X.U8(0xF9) != 0x7F) return false;
	var x17a = X.U16(0x17A,_BE); p = 0x179; allsmpsz = 0;
	if(x17a != X.U16(0x17C,_BE) || x17a != ord) return false;
	ord_ = -1; ptn = 0;
	for(; p > 0xF9; p--) { if((o=X.U8(p)) > 100) return false; if(o && ord_ < 0) ord_ = p-0xF9; if(o >= ptn) ptn = o+1 }
	if(ord_ < 0) ord_ = 1;
	for(p=smp=i=0; i < 31; i++, p += 8) if(ssz=X.U16(p,_BE)) { smp++; allsmpsz += ssz } allsmpsz <<= 1;
	for(p=0x17E,i=m=0; i < x17a*4; i++,p+=2) m = Math.max(m,X.U16(p,_BE));  p += m;
	//the source and player diverge from here, I mix them together
	if(ptn == 1) for(t=X.U16(p-2,_BE),p+=2; p < X.Sz(); p += 2) if(X.U16(p-2,_BE) == t) return true; //from src
	//from bin:
	for(q=p,m=0; q < p+4; q++) m = Math.max(m,X.U8(q)); //@e4b2, ofs 4ca+
	for(a3=p+m+4,m=0; q < a3; q++) m = Math.max(m,X.U8(q)); //@e4c4 ofs 4e2+
	for(a3=p+m+4,m=0; q < a3; q++) m = Math.max(m,X.U8(q)); //@e4d6 ofs 4e2+
	p += m*2+4; songsz = p;
	// back to src which continues where the bin left off:
	while(X.c("04040404 08080808",p) && p < 0xFFFF) p += 0x10;
	return true
}
if(!bDetected && isMFP()) {
	sName = "Shaun Southern's Magnetic Fields Packer module (.MFP+.SMP)"; bDetected = 1;
	//without the smp file, the smp.*.set will be used
	if(X.isVerbose()) {
		sOption('ord:'+ord+(ord != ord_? '/'+ord_: '')+' ptn:'+ptn+' smp:'+smp
			+' smpsz:'+allsmpsz+' songsz:'+songsz+' sz:'+outSz(p))
	}
}


function isADL() {
	//ref https://github.com/adplug/adplug/blob/master/src/adl.cpp -> CadlPlayer::load
	// & https://www.vgmpf.com/Wiki/index.php?title=ADL_(Westwood)
	if(X.Sz() < 720) return; //v1 min. size
	// Active Pointers, Zeroes and Invalid Chars are heuristics to filter out random sparse files in the obscurity that is ADL
	// There must be at least some active pointers, and too many zeroes at prog start is suspicious. IC is when a prog doesn't start on a well-expected byte
	p = ap = z = ic = 0;
	nV = 4;
	ofs = 500; //track entries
	for(; p < ofs; p+=2) { //500=1F4h is the trackentries buffer
		if(nV == 4 && isWithin(w = X.U16(p), 0x1F4,0xFFFE)) { //in v4, all entries are either within 500 or FFFF
if(debug>1)_logIt('@'+Hex(p)+': '+Hex(w))
			nV = 3 /*or less*/; ofs = X.c("6C776DFFFFFF6E6F",0x78) ? 250 : 120 // 78h, or FAh just for loresfx.adl
		}
	}
	const M = Math.min(0xFFFF+ofs,X.Sz());
	p = ofs;
// v1: EoB; v2: EoB2, Kyr1, Dune2; v3: LandsOfLore, Kyr2
// _trackEntries[]      _soundData[]   -- program/instrument offsets are ofs-relative
// | pt ids (ord?) | Program pt: sound/music tracks | Instrument pt: adlib ins | Trk data | Ins data |
// v1:   120 bytes          | 150 words         | 150 words     | @2D0h
// v2: 120 bytes          | 250 words         | 250 words     | @460h
// loresfx.adl: 250 bytes | 250 words     | 250 words    | @4E2h
// v3:   250 words          | 500 words         | 500 words     | @9C4h

//if(debug>0)_logIt('  v'+nV+' ofs:'+ofs+'/'+Hex(ofs))
	// test prog offsets: start with the first 150
	if(nV < 4) {
		nprog = 150; //for v1
		for(i=0; i < nprog; i++) {
			w = X.U16(p); p += 2;
			if(w && w != 0xFFFF) { ap++;
				if(w < 1000) { nV = 1; if(X.U16(0x78) != 0x258 /*in v1, the 0th prog always starts at 2D0*/) return }
				if(!isWithin(w, 600,M)) { if(debug>0)_logIt('!w='+w+'<600 or > sz'); return }
				w += ofs; y = X.U8(w);
				if(y != 0x8E && y > 9) { // progs always start with 1~9 or, seldom, 0 or 8E
//if(debug>1)_logIt('!v'+nV+'@'+Hex(p-2)+': ['+Hex(w)+'] = '+Hex(y))
					z++
				}
			}
		}
//if(debug>0)_logIt('  v'+nV+'@'+Hex(p))
		// check the rest of the program offsets:
		if(nV > 1) {
			if(X.Sz() < 1120) return; //v2/3 min. size
			nprog = 250; p = ofs+300;
			for(i=150; i < nprog; i++,p+=2) {
				w = X.U16(p);
				if(w && w != 0xFFFF) { ap++;
					if(!isWithin(w, 1000,M)) { if(debug>0)_logIt('!w='+w+'<1000 or > sz'); return }
					w += ofs; y = X.U8(w);
					if(!y) z++;
					if(y != 0x8E && y > 9) {
//if(debug>1)_logIt('!v'+nV+'@'+Hex(p)+': ['+Hex(w)+'] = '+Hex(y))
						ic++
					}
				}
			}
		}
//if(debug>0)_logIt('v'+nV+'@'+Hex(p)+' ')
	} else { //v4
		if(X.Sz() < 2500) return;
		nprog = 500; p = ofs;
		for(i=0; i < nprog; i++,p+=2) {
			w = X.U16(p);
			if(w && w != 0xFFFF) { ap++;
				if(!isWithin(w, 2000,M)) { if(debug>0)_logIt('!w='+w+'< 2000 or > sz'); return }
				w += ofs; y = X.U8(w);
				if(!y) z++;
				if(y != 0x8E && y > 9) {
//if(debug>1)_logIt('!v'+nV+'@'+Hex(p)+': ['+Hex(w)+'] = '+Hex(y))
					ic++;
				}
			}
		}
	}
	// also test ins offsets and find the max one+11 = filesize:
	for(mw=i=0,mo=(nprog<<1)+ofs; i < nprog; i++,p+=2) {
		w = X.U16(p);
		if(w && w != 0xFFFF) { ap++;
			w += ofs;
//if(debug>1)_logIt('@'+Hex(p)+': ['+Hex(w)+'] = '+Hex(X.U8(w)))
			if (!isWithin(w, mo,X.Sz())) { if(debug>0)_logIt(!'@'+Hex(p)); return; }
			mw = Math.max(mw,w)
		}
	}
	sz = mw+11; if(sz < 6900) return;
	ptn = [];
	if(nV == 4) { for(p=0; p < 500; p+=2) if(isWithin(y=X.U16(p),1,0xFFFE)) if(!ptn.includes(y)) ptn.push(y); }
	else for(p=ofs; p < 250; p+=2) if(isWithin(y=X.U16(p),1,0xFFFE)) if(!ptn.includes(y)) ptn.push(y);
	ptn = ptn.length;
if(debug>0)_log('@'+Hex(p)+' v:'+nV+' ic:'+ic+' zeros:'+z+' ap:'+ap+' ptn:'+ptn)
	return ic < 5 && z < 50 && ap > 1 // heuristics
}
if(!bDetected && isADL()) {
	sType = 'audio'; sName = "Westwood ADL module (.ADL)"; bDetected = 1;
	if(ofs == 250) sVersion = 'v.sfx'; else sVersion = 'v'+[,'1','2','2','3'][nV];
	
	if(X.isVerbose()) sOption('ptn:'+ptn+' sz:'+outSz(sz))
}


function isFLS() {
	//from https://ay.strangled.net/Ay_Emul30.src.7z/Players.pas -> FoundFLS
	if(X.Sz() < 0x34) return;
	var orda, ptna, smpa, p, cura, mp, maxd, dif, i, j;
	function re(m) { if(debug>0)_log('[isFLS]@'+Hex(p)+': '+m); return }
	function ValidSmpP() { return p >= 12 + orn*2 && !((p - orn*2) % 6) }
	function ValidSmpParams(o) {
		var lp = X.U8(o), lpsz = X.U8(o+1); if(lp > 0x20) return;
		if(!lp) { if(!isWithin(lpsz, 1,0x20)) return; } else if(!isWithin(lpsz, 1,0x21-lp)) return;
		return true
	}
	ordp = X.U16(0); ornp = X.U16(2); smpp = X.U16(4);
	orn = smpp-ornp; if(!isWithin(orn, 0,30) || orn % 2) return;
	orn >>= 1; orda = ordp; smp = ordp-smpp;
	if(smp <= 0 || smp > 16*4 || smp % 4) return;
	smp >>= 2; ptna = X.U16(6); ord = ptna-orda-1; if(!isWithin(ord, 1,256)) return;
	if(X.Sz() < 12+ord+1+36*smp+34*orn+3-1) return;
	p = 6; mp = 6+orn*2+smp*4;
	maxd = X.Sz()+1-mp; mp += 31*6;
	if(mp > X.Sz()+1-ord-1-3) { mp = X.Sz()+1-ord-1-3; if(mp % 2) --mp; }
	while(p < mp) {
		cura = X.U16(p); dif = cura-orda;
		if(!isWithin(dif, 1,maxd)) break;
		if(ValidSmpP() && p+6 < mp && ValidSmpParams(p)) {
			smpa = X.U16(p+2);
			if((!orn || smpa-X.I16(p-2) == 32) &&
				( smp == 1 && orda-smpa == 32*3 || 
				  smp > 1 && ValidSmpParams(p+4) && X.I16(p+6)-smpa == 32*3
				)
			) break;
		}
		if(cura < ptna) return re('!cura < ptna: '+Hex(cura)+' < '+Hex(ptna));
		p += 2;
	}
	if(!ValidSmpP()) return re('!post-cycle invalid smp ofs');
	mp = p+smp*4; if(mp > X.Sz()+1-ord-1-3) return;
	maxd = X.Sz()+1-mp;
	var ic = 0; //heuristic time
	for(i=0; i < smp; i++,p+=4) {
		if(!ValidSmpParams(p)) ic++; if(ic > 1) return re('!badsmpparams');
		cura = X.U16(p+2); dif = cura-orda; if(!isWithin(dif, 1, maxd)) return re('!bad cura-orda');
	}
	for(i=0; i < ord; i++,p++) if(!isWithin(X.U8(p), 1,31)) return re('!badpos');
	if(X.U8(p)) return re('!badordendmarker');
	j = ptna-p-1;
	if((ornp-j) % 2 || (smpp-j) % 2) return re('!odd orna or smpa');
	orda -= j; if(orda % 2) return re('!odd orda');
	sz = X.U16(orda-2)+0x60-j;
	if(sz <= orda) return re('!size<=orda');
	return true;
}
if(!bDetected && isFLS()) {
	sName = "Amadeus Voxon/Flash Inc.'s Flash Tracker module (.FLS)"; bDetected = 1;
	if(X.isVerbose()) {
		sOption('ord:'+ord+' ptn:'+ptn+' smp:'+smp+' orn:'+orn+' sz:'+outSz(sz))
	}
}



//the following signature detectors seem shaky to me [Kae] so they're heuristic and in parallel
//use _setResult(TYPE, NAME, VERSION, OPTIONS)

if(X.isHeuristicScan()) { //parallel block

	// Let's start with just judging by the extensions:
	if(extIs("imf") || extIs("wlf")) { //a detector is quite impossible to produce...
		if(extIs("imf"))
			freq = "560Hz (or 280Hz if Duke Nukem II)";
		else
			freq = "700Hz";
		_setResult("~audio","id/Apogee Music Format chiptune (.IMF)","","freq: "+freq);
	}
	else if(extIs("svar") && X.c("'PK'")) { //TODO detect in-zip
		_setResult("~audio","SVArTracker module (.SVAR)","","");
	}

	//these are compares at 0, so they can be non-parallel, to optimise it all
	if(X.c("8400")) {
		if(X.U8(2) == 0xF0) sversion = "adv."; else sversion = "";
		_setResult("~audio","Sierra Adlib chiptune (.SCI)",sversion,"")
	}
	else if(X.c("1C52")) {
		_setResult("~audio","Sound Interface System module (.LEM)","","")
	}
	else if(X.c("'NED'")) {
		_setResult("~audio","Nerd Tracker ][ module (.NED)","","")
	}
	else if(X.c("'MODU'"))
		_setResult("~audio","NovoTrade Packer module (.NTP)","",X.isVerbose()?X.SA(4,16):'');
	else if((X.c("'Ice!'") || X.c("'ICE!'"))) {
	//TODO find a sanity check to narrow it down, then move it to the other sanities
		dsize = X.U32(8,_BE);
		_setResult("~audio","Atari ST module (.SND,.SNDH)","compressed",X.isVerbose()? "orig.sz:"+dsize+" sz:"+outSz(X.U32(4,_BE)): '')
	}

	function isVortex1() {
		//ref https://ay.strangled.net/Ay_Emul30alpha11.src.7z / Formats.pas
		// & https://f.rdw.se/AY-3-8910-datasheet.pdf
		hdr = X.SA(0,2);
		if(['AY','YM','ay','ym'].indexOf(hdr) < 0 || X.U8(2) > 6) return false;
		hasyear = ['AY','YM'].indexOf(hdr) < 0; yr = 0;
		if(hasyear) {
			yr = X.U16(0xA,_LE);
			if(yr && (yr < 1980 || yr > 2050)) return false;
		}
		unpsz = X.U32(hasyear?0xC:0xA,_LE);
		if(unpsz < 2 || unpsz > 0xA00000) return false; // maxsize set a bit up from enlight '97 megamix.vtx
		chipfrq = X.U32(5,_LE); if(chipfrq < 800000 || chipfrq > 4000000) return false; //2M would be ok
		intfrq = X.U8(9);
		return true
	}
	if(isVortex1()) {
		if(!X.U8(2)) sversion = "1ch"; else sversion = "2ch";
		switch (hdr) {
			case 'AY': case 'ay': sversion += " AY-3-8910/12"; break;
			case 'YM': case 'ym': sversion += " YM2149"; break;
			default: sversion += " unk.chip" // never reached
		}
		if(X.isVerbose()) {
			p=hasyear?0x10:0x0E; t = X.SC(p,Math.min(256,X.Sz()-p),'CP1251'); sOptionT(t);
			p+=t.length+1; a = X.SC(p,Math.min(256,X.Sz()-p),'CP1251'); sOptionT(a,"by:");
			p+=a.length+1; if(hasyear) {
				if(yr) sOption(yr,"'");
				pr = X.SC(p,Math.min(256,X.Sz()-p),'CP1251'); p+=pr.length+1;
				sOptionT(pr,"for:");
				tn = X.SA(p,Math.min(256,X.Sz()-p)); sOptionT(tn,"in:"); p+=tn.length+1;
				c = X.SA(p,Math.min(256,X.Sz()-p)); sOptionT(c); //p+=c.length+1;
			}
			sOption((X.U8(2)&7).toString(2).padStart(3,'0'),"mode:");
			sOption(chipfrq,"chip freq:","Hz"); sOption(intfrq,"int.freq:","kHz")
			t = Util.div64(unpsz,intfrq*14); sOption("time:"+secondsToTimeStr(t));
			loop = X.U16(3,_LE); if(loop) sOption(loop,"loop:");
		}
		_setResult("~audio","Vortex Project chiptune (.VTX)",sversion,sOptions); sOptions = ""
	}


	function isMXDRVPDX() {
		// the format is very very very basic so FPs happen
		//and it's not possible to test more
		//the first bytes until the first sample starts are a table of addresses and sizes of the samples
		// (if the address is 0 just skip it),
		// then the samples go. That's all.
		// the table can be tons of bytes long, much longer than the max reasonable 800h
		if(X.Sz() < 0x300 || X.Sz() > 0x4000+0xFFFFFF) return false;
		sz = en = 0; var oldendp  = nonzeroaddr = -1, smpst = 0xFFFFFFFF, ptrs = []; sus = 0; bad = '';
		for(i=0; i*8 < (smpst == 0xFFFFFFFF? 0x300: Math.min(smpst,X.Sz())); i++) {
			var p = X.U32(i*8,_BE), s = X.U32(i*8+4,_BE);
			if(!p) continue;
//if(p < i*8) _l2r('pdx',p,i+'*8 is higher?!');
			if(p < i*8 || s > 0xFFFFF) return false;
			if(nonzeroaddr < 0) nonzeroaddr = i;
			if(smpst > p && p >= 0x300) smpst = p;
			oldendp = p+s; ptrs.push([p,s]);
			en++;
			if(p+s > sz) sz = p+s
		}
		if(!en || findGaps(ptrs,2).length > 4) return false;
		ptrs = ptrs.sort(function (a, b) { if(a[0] != b[0]) return a[0]-b[0]; else return a[1]-b[1] });
//if(ptrs[0][0] % 8 || !isWithin(ptrs[0][0],0x300,0x4000)) _l2r('pdx',i*8,'!smpst='+Hex(ptrs[0][0]));
		if(ptrs[0][0] % 8 || !isWithin(ptrs[0][0],0x300,0x4000)) return false;

//_l2r('pdx',sz,outArray(findGaps(ptrs,2),16))
		return true
	}
	if(isMXDRVPDX()) {
		sversion = ''; if(sz > X.Sz() && !X.isVerbose()) bad += '!short';
		if(bad.length) sversion = sversion.appendS('malformed'+bad+(sus?'sus'+sus:''),'/');
		_setResult("~audio","Konami's MXDRV PCM resource (.PDX)", sversion,
			X.isVerbose()? 'entries:'+en+' sz:'+outSz(sz): '')
	}


	function isKDM() {
		// ref http://advsys.net/ken/kdmsongs.zip/KDMSRC.ZIP/kdmwin/KDMENG.C
		if(X.Sz() < 12) return false;
		if(X.U32(0,_LE)) return false; //kdmversionum
		numnotes = X.U32(4,_LE); if(!numnotes || numnotes > 8192) return false;
		numtracks = X.U32(8,_LE); if(!numtracks || numtracks > 256) return false;
		sz = 12+numtracks*4+numnotes*11;
		if((sz != X.Sz()) && (sz != X.Sz()+numnotes)) return false;
		return true
	}
	if(isKDM()) {
		if(sz > X.Sz()) sversion = "no panning effects"; else sversion = "";
		_setResult("~audio","Ken Silverman's Digital Music module (.KDM)",sversion,X.isVerbose()? "trk:"+numtracks+" notes:"+numnotes: '');
	}


	function isDigitalStudio() {
		//ref https://bitbucket.org/zxtune/zxtune/src/develop/src/formats/chiptune/digital/digitalstudio.cpp
		if(X.Sz() < 111104) return false;
		lp = X.U8(0); if(lp > 63) return false;
		ptn = 0;
		for(i = 1; i < 100; i++) { pt = X.U8(i); if(pt > 0x1F) return false; if(pt > ptn) ptn = pt } ptn++;
		tmp = X.U8(0x64); ord = X.U8(0x65); if(!ord || ord > 100) return false; if(lp > ord) return false;
		title = X.readBytes(0x66,28); for(i=0; i < 28; i++) if(title[i] < 0x20 || title[i] > 0x7F) return false;
		title = decEncoding(title,CPSpeccy);
		compiled = !isAllZeroes(0xC8,0x38); allsmp = smp4bit = 0;
		smps = [];
		for(p=0x100; p < 0x200; p += 0x10) {
			var sst = X.U16(p), stlp = X.U16(p+2), spage = X.U8(p+4), sninbank = X.U8(p+5), ssz = X.U16(p+6);
			for(i=8; i < 16; i++) if(X.I8(p+i) < 0x20) return false;
			t = decAnsi(p+8,8,CPSpeccy,false).trim(); if(t != '') smps.push(t)
		}

		return true;
	}
	if(isDigitalStudio()) {
		sname = "Underground Systems Digital Studio module (.DST)";
		sversion = compiled?'compiled':'';
		if(X.isVerbose()) {
			sOptionT(title); sOptionT(smps.filter(funSampleName).join(' '),'smp/msg:"','"');
			sOption('ord:'+ord+' ptn:'+ptn+(lp?' loop:'+lp:'')+' page0:'+X.U8(0x200)+' sz:'+outSz(compiled?0x1C200:0x1B200))
			soption = sOptions; sOptions = ''
		} else soption = '';
		_setResult("~audio",sname,sversion,soption)
	}


	function isExtremeTrackerZX() {
		//from https://bitbucket.org/zxtune/zxtune/src/develop/src/formats/chiptune/digital/extremetracker1.cpp
		if(X.Sz() < 112640) return false;
		if((lp = X.U8(0)) > 63) return false;
		if((tmp = X.U8(1)) < 3 || tmp > 0xF) return false;
		if((ord = X.U8(2)) > 64) return false;
		title = X.readBytes(3,30); for(i=0; i < 30; i++) if(title[i] < 0x20 || title[i] > 0x7F) return false;
		title = decEncoding(title,CPSpeccy);
		ptn = 0;
		for(p = 0x22; p < 0x86; p++) { if((pt = X.U8(p)) > 0x1F) return false; if(pt > ptn) ptn = pt } ptn++;
		for(; p < 0xA6; p++) { var ps = X.U8(p); if(ps < 4 || ps > 0x40) return false }
		var qstvw = [0x51,0x53,0x54,0x56,0x57];
		for(i=0; p < 0xBA; i++, p += 4) {
			if(X.U8(p) > 0x7C) return false; if(X.U8(p+1) != qstvw[i]) return false;
			if(X.U8(p+2) && X.U8(p+2) < 0x84) return false; if(X.U8(p+3) > 0x10) return false
		}
		var sus = 0; for(p=0xBC; p < 0xCB; p++) if(X.U8(p)) sus++; if(sus > 3) return false;
		var sig = X.readBytes(0xCB,0x35,true); if(charStat(sig,true).indexOf('allasc') <= 0) return false;
		if(X.U8(0xFF)) return false;
		smps = [];
		for(p = 0x100; p < 0x200; p+= 0x10) {
			if(qstvw.indexOf(X.U8(p+4)) < 0) return false;
			if(X.U8(p+5) > 0x10) return false; if(X.U8(p+6) > 0x7C) return false;
			t = X.readBytes(p+8,8); for(i=0; i < 8; i++) if(t[i] < 0x20 || t[i] > 0x7F) return false;
			t = decEncoding(title,CPSpeccy).trim(); if(t != '') smps.push(t)
		}
	}
	if(isExtremeTrackerZX()) {
		sname = "Red Limited's Extreme Tracker module (.ET1)";
		if(X.isVerbose()) {
			sOptionT(title); sOptionT(smps.filter(funSampleName).join(' '),'smp/msg:"','"');
			sOption('tmp0:'+tmp+' ord:'+ord+' ptn:'+ptn+(lp?' loop:'+lp:'')+' sz:'+outSz(112640))
			soption = sOptions; sOptions = ''
		} else soption = '';
		_setResult("~audio",sname,sversion,soption)
	}


	function isMON() {
		if(!X.c("4EFA....4EFA....4EFA")) return false;
		var t = X.U16(2,_BE); if (t > X.Sz() - 10) return false;
		return X.c("4BFA.... 08AD 0000",t+2)
	}
	if(isMON())
		_setResult("~audio","Jeroen 'WAVE' Tel & Charles Deenen's Maniacs of Noise module (.MON)","","")


	function isJHSTSMP() {
		if(X.Sz() < 0x80) return false;
		oldp = smp = 0;
		for(i = 0; i < 0x80; i += 8) {
			p = X.U16(i,_BE); if(p > X.Sz()) return false;
			if(p) { if(p < oldp) return false; oldp = p; smp++ }
			if(!X.c("0010 01000000",i+2) && !X.c("0000 00000000",i+2)) return false;
		}
		if(smp < 8) return false; // I only saw files with at least 10
		return true
	}
	if(isJHSTSMP()) _setResult("~audio","Jochen 'Mad Max' Hippel's Atari ST sample set (SMP.set)","",
			X.isVerbose()? "smp:"+smp+" sz:"+outSz(oldp): '')


	function isV2M() {
		//ref https://github.com/farbrausch/fr_public/blob/master/v2/v2mplayer.cpp ::InitBase
		//with kind commentary from Tammo "kb" Hinrichs
		notes = chs = 0;
		if(X.Sz() < 480) return false;
		timediv = X.U32(0,_LE); if(!timediv || [0x60,0x80,480].indexOf(timediv) < 0) return false; //empirical
		maxtime = X.U32(4); if(!maxtime) return false;
		gdnum = X.U32(8); if(!gdnum) return false;
		p = 12+10*gdnum; var pcs = pbs = ccs = 0;
		for(ch=0; ch < 16; ch++) {
			notenum = X.U32(p); p += 4; notes += notenum;
			if(notenum > 1000000) return false; // nobody'll write a million-note synth epic, will they @_@
			if(notenum) { chs++;
				p += 5*notenum; if(p > X.Sz()) return false;
				pcnum = X.U32(p); pcs += pcnum; p += 4+4*pcnum; //pgm change events
				pbnum = X.U32(p); pbs += pbnum; p += 4+5*pbnum; //pitch bends
				for(cn=0; cn < 7; cn++) {
					ccnum = X.U32(p); ccs += ccnum; p += 4+4*ccnum; //control changes
				}
			}
			if(p > X.Sz()) return false;
		}
		if(!notes || ccs > notes*50 || pbs > notes*3 || pcs > notes*3 || maxtime < notes) { //*50 & 3 are arbitrary
//_l2r('V2M',p,'notes:'+notes+' pcs:'+pcs+' pbs:'+pbs+' ccs:'+ccs)
			return false }
		size = X.U32(p); if(size > 0x4000) return false; //globals
		p += 4+size; if(p > X.Sz()) return false;
		size = X.U32(p); if(size > 0x100000) return false; //patchmap
		p += 4+size; if(p > X.Sz()) return false;
//_l2r('V2M',p,'speech -> '+Hex(p+4+X.U32(p)))
		spsize = X.U32(p); p += 4; if(spsize < 0x2000) {
			for(i=p+4,q=X.U32(p)*4,t=Math.min(X.Sz,p+4+q); i < t; i+=4) if(!isWithin(X.U32(i), q, p-4+spsize)) return false;
			p += spsize //speech size -- except tons of files are shorter than that, so we'll read the pointers!
		}
		sz = p; return true;
	}
	if(!bDetected && X.isDeepScan() && isV2M()) {
		sName = "farbrausch V2 Synthesizer module (.V2M)"; bDetected = 1;
		if(X.isVerbose())
			sOptions = 'ch:'+chs+' notes:'+notes+' timediv:'+timediv+' maxtime:'+maxtime
				+(spsize?' syn.speech':'')+' sz:'+outSz(sz);
	}


	function isTwinTeam() {
		if(!X.c("01000000", 0x06) || X.Sz() < 1500 || X.Sz() > 9000 || X.calculateEntropy(12,1200) < 7.8)
			return false; //a quick check because that's a lot of algo
		//ideally, get the checks from https://github.com/adplug/adplug/blob/master/src/dmo.cpp
		return true
	}
	if(isTwinTeam()) {
		_setResult("~audio","TwinTeam's Twin Trackplayer module (.DMO)","",X.isVerbose()?'unp.sz:'+X.U16(0xC):'')
	}


	function isVoices_8() { //this is a WIP until someone manages to test the tracker without it freezing up on I/O
		soption = ''; smp = ptn = smpsz = 0;
		for(i=0; i < 0x20; i++) {
			if(charStat(t=X.readBytes(i*0x20,0x10),1).indexOf('allxsc') < 0) return false;
			var c = t.indexOf(0); if(!c) continue; if(c < 0) return false;
			for(j=15; j > c && !t[j];) j--; if(j > c) return false;
			var ssz = X.U32(i*0x20+0x14,_BE); if(ssz) { smpsz += ssz; smp++ }
			}
		tmp0 = X.U8(0x480); ord = X.U8(0x481); if(!smpsz || !tmp0 || tmp0 > 0xF || !ord || ord > 0x80) return false;
		for(i=0; i < ord; i++) if((t=X.U8(0x482+i)+1) > ptn) ptn = t;  if(ptn > 32) return false;
		for(; i < 128; i++) if(X.U8(0x482+i)) return false; sz = 0x502+ptn*0x800+smpsz;
		if(X.Sz() < sz) return false;
		soption = 'tmp0:'+tmp0+' ord:'+ord+' ptn:'+ptn+' smp:'+smp+' sz:'+outSz(sz);
		return true
	}
	if(isVoices_8())
		_setResult("~audio","SHINING 8's Voices_8/8CHNL Soundtracker module",'',X.isVerbose()?soption:'')


	function isHSC() {
		//ref https://github.com/libxmp/libxmp/blob/master/src/bitrot/loaders/hsc_load.c
		// & https://github.com/libxmp/libxmp/raw/refs/heads/master/docs/formats/hsc.txt
		if(X.Sz() < 1587+1152 || X.Sz() > 59187+1) return;
		if(isAllZeroes(0,127*12)) return;
		p = t = ic = 0;
		//a heuristic run on lots of HSCs dictates these things:
		const
		  rangesb01 = [[0x0A,0x0D], [0x17,0x0E], [0x23,0x26], [0x29,0x2D], [0x3A,0x3F], [0x45,0x50],
			[0x5A,0x5E], [0x69,0x6B], [0x75,0x77], [0x7A,0x7D], [0x83,0x85], [0x89,0x8A], [0xB3,0xBF], [0xD3,0xDF], [0xFA,0xFE]],
		  rangesb2 = [[0x1B,0x1C], [0x21,0x2F], [0x34,0x3E], [0x41,0x47], [0x49,0x54], [0x5A,0x5E], [0x66,0x72],
			[0x75,0x7F], [0x81,0x87], [0x89,0xBF], [0xC9,0xCF], [0xD1,0xDF]],
		  rangesb3 = [[0x2B,0x2E],, [0x36,0x3E], [0x45,0x4C], [0x51,0x57], [0x59,0x67], [0x69,0x6C], [0x6E,0x71], [0x76,0x80],
			[0x88,0x8F], [0x9D,0xAF], [0xB1,0xBF], [0xC1,0xC9], [0xCB,0xCF]],
		  rangesb4 = [[1,5], [7,0x10], [0x15,0x1E], [0x27,0x2C], [0x39,0x3E], [0x45,0x4F], [0x57,0x5F], [0x69,0x6D], [0x8A,0x8E],
			[0x9A,0x9F], [0xAC,0xB0], [0xB9,0xBF], [0xC7,0xD1], [0xD7,0xDF], [0xE8,0xEF], [0xFA,0xFD]],
		  rangesb5 =[[1,0xF], [0x19,0x1E], [0x27,0x2E], [0x37,0x3F], [0x49,0x4F], [0x58,0x5F], [0x6A,0x6E], [0x78,0x7F], [0x89,0x8F],
			[0x9A,0x9F], [0xAC,0xAE], [0xBA,0xC6], [0xC9,0xD2], [0xD5,0xE3], [0xEA,0xEE], [0xFA,0xFC]],
		  rangesb6 = [[0xA,0xE], [0x1B,0x1E], [0x24,0x26], [0x37,0x3A], [0x3C,0x3F], [0x49,0x4E], [0x5A,0x60], [0x6B,0x6E],
			[0x7C,0x7E], [0x80,0x83], [0x8A,0x90], [0x9A,0xA1], [0xA7,0xAF], [0xB1,0xC6], [0xC8,0xCD], [0xCF,0xDC], [0xDE,0xE0],
			[0xE2,0xE6], [0xE8,0xED]],
		  rangesb7 = [[0xC,0xE], [0x19,0x1E], [0x27,0x2E], [0x39,0x41], [0x59,0x5E], [0x6A,0x6E], [0x79,0x7F], [0x8A,0x95],
			[0x9A,0xA5], [0xAB,0xC6], [0xCA,0xCD], [0xD1,0xDE], [0xE0,0xED]],
		  rangesb8 = [[0x12,0x15], [0x17,0x1F], [0x23,0x36], [0x38,0x3F], [0x41,0x4D], [0x4F,0x51], [0x5A,0x60], [0x62,0x68],
			[0x6A,0x6D], [0x6F,0x7E], [0x80,0x93], [0x95,0xF5]],
		  rangesb9 = [[0x10,0x17], [0x19,0x40], [0x42,0x60], [0x65,0x68], [0x6A,0x76], [0x7A,0x90], [0x92,0xB1]],
		  rangesbA = [[0x10,0x1C], [0x21,0x2F], [0x31,0x42], [0x49,0x54], [0x56,0x5D], [0x60,0x6D], [0x6F,0x72],
			[0x74,0x88], [0x78A,0x98]],
		  rangesbB = [[0xA,0xF], [0x12,0x1F], [0x21,0x2F], [0x31,0x36], [0x38,0x3F], [0x45,0x56], [0x58,0x5F], [0x61,0x66],
			[0x68,0x6F], [0x78,0x7F], [0x81,0x96], [0x9A,0xEF], [0xF1,0xFE]];

		for(i=0,m=X.isDeepScan()?127:16; i < m; i++,p+=12) {
			t = X.U8(p); if(isWithinRanges(t, rangesb01) || isWithin(t, 0xA5,0xB1)) ic++;
			t = X.U8(p+1); if(isWithinRanges(t, rangesb01) || isWithin(t, 0x92,0xA7)) ic++;
			t = X.U8(p+2); if(t >= 0xE1 || isWithinRanges(t, rangesb2)) ic++;
			t = X.U8(p+3); if(t >= 0xD1 || isWithinRanges(t, rangesb3)) ic++;
			t = X.U8(p+4); if(isWithinRanges(t, rangesb4)) ic++;
			t = X.U8(p+5); if(isWithinRanges(t, rangesb5)) ic++;
			t = X.U8(p+6); if(isWithinRanges(t, rangesb6)) ic++;
			t = X.U8(p+7); if(isWithinRanges(t, rangesb7)) ic++;
			t = X.U8(p+8); if(t >= 0xF7 || isWithinRanges(t, rangesb8)) ic++;
			t = X.U8(p+9); if(t >= 0xB3 || isWithinRanges(t, rangesb9)) ic++;
			t = X.U8(p+0xA); if(t >= 0x9A || isWithinRanges(t, rangesbA)) ic++;
			t = X.U8(p+0xB); if(isWithinRanges(t, rangesbB)) ic++;
			if(ic > 2) return
		}
		//test the orderlist:
		ptn = ord = x = 0;
//_logIt('@'+Hex(p)+' testing ords='+outArray(X.readBytes(0x600,0x33), 16))
		if(X.U8(0x600) > 0xB1) return;
		visited = []; for(i=0; i < 0x32; i++) visited[i] = false; // prep for the loops
		for(p=0x600; p < 0x633; p++) {
			if(visited[p-0x600]) break; visited[p-0x600] = true;
			if(isInside(t, 0x31,0x80)) t = 0xFF; //pattern out of range = end song
			if((t = X.U8(p)) > 0xB1) break; //rainyday.hsc doesn't have FF
			else if((t & 0x80) && t <= 0xB1) { //løøps
				t &= 0x3F; t += 0x600;
//_logIt('@'+Hex(p)+' -=loop=-')
				if(t > 0x632 || X.U8(t) > 0xB1 || t == p) { if(debug>0)_logIt('@'+Hex(p)+', t:'+Hex(t)+' :: '+Hex(X.U8(t))); return; }
				else p = 0x600+t-1;
			}
			else if(t & 0x80) ic++; else { if(ptn < t) ptn = t;  ord++ }
		}
		for(q=0x632; q > 0x600; q--) if(X.U8(q) == 0xFF) break;
//_logIt('ord:'+ord+' ptn:'+ptn+' last:'+Hex(X.U8(p)))
		if(!ord || !ptn && X.U8(p) != 0xFF) return; if(!x) x++;
		ptn++; if(ptn > 50) return; //test number of patterns
		p = 0x633;  sz = p + ptn*0x480;
		var safeptns = 0;
		for(i=0; i < ptn*0x240 && safeptns < 100; i++) { // just test a hundred patterns to be safe and sure it's a HSC
			n = X.U8(p++); m = X.U8(p++);
			if(n || m) {
				//the same heuristic run on lots of HSCs says these things too:
				if(n >= 0xCA || isWithinRanges(n, [[0x62,0x7E], [0x81,0x8F], [0x91,0xC8]])) return;
//_logIt('@'+Hex(p-2)+' n is within range')
				if(isWithinRanges(m, [[0x38,0x43], [0x45,0x51], [0x56,0x5F], [0x69,0x80], [0x82,0x88], [0x8A,0x9B],
				  [0x9D,0x9F], [0xAA,0xAE], [0xD7,0xDD], [0xDF,0xE6], [0xE8,0xEF], [0xF9,0xFE]])) return;
//_logIt('@'+Hex(p-1)+' m is within range')
				if(isWithin(m, 7,0xF)) if(n != 0x80 /*fmtrk2.hsc should be valid*/) {
					if(n != 0) /*return;*/ ic++; //2.hsc starts with a single broken n = 14h
					} //test effects 07..0F
//_logIt('@'+Hex(p-2)+' test1 passed')
				if(isWithin(m, 0x70,0x9F)) { //test effects 7x..9x
					if(m != 0x81 && m != 0x89) return; //rainyday.hsc should be valid
					ic++;
//_logIt('@'+Hex(p-2)+" test2 IC'ed -> "+ic)
				}
				if(ic > 20) return;
				safeptns++;
			}
		}
//		if(sz > X.Sz()) return;
		//if(sz%2) sz++; // all HSC files are word-aligned like it were Amiga, but it aint. Not sure whether to include
		if(ic) bad = '!ic'+ic;
		return true
	}
	if(isHSC()) {
			var soption = ''; if(X.isVerbose()) {
			if(x > 1) soption = '×'+x;
			soption = soption.appendS('ord:'+ord+' ptn:'+ptn+' sz:'+outSz(sz),', ')
		}
		_setResult("~audio","Hannes Seifert's HSC Adlib Composer/ECR HSC-Tracker module (.HSC)",(bad.length?'malformed'+bad:''),soption)
	}


	function isFMX() {
		if(X.Sz() < Math.max(X.U16(0x1E),0x30)) return false;
		ch = 0; var t;
		for(i=oldp=0; i < 0x10; i++) {
			if(!isWithin(t = X.U16(i*2), Math.max(0x30,oldp),X.Sz())) return false;
			if(i && X.U8(t-1) != 0x80) return false;
			if(X.U8(t) != 0x80) ch++;
			oldp = t
		}
		return true
	}
	if(isFMX()) _setResult("~audio","K.Ohshima's FMX chiptune (.FMX)",'',X.isVerbose()?'ch:'+ch:'');



// these are HEAVY as well as heuristic, so they must come last
	function isGYM() {
		if(X.Sz() < 0x160) return;
		_gyminfo = parseMDGYM(0, X.isDeepScan()? BCParseToEoF: BCParseToReasonable);
if(debug>0)_logIt('gym?'+outArray(_gyminfo,16))
		if(_gyminfo[0] < 0) return;
		return true
	}
	if(isGYM()) _setResult("~audio", "Sega Genesis/Mega Drive YM2612 chiptune (.GYM)", "headerless",
		X.isDeepScan() ? "notes:" + _gyminfo[0] + " sz:"+outSz(_gyminfo[1]) : "");

	function isOPMLog() { // the headerless one's nooooot easy to check
		if(X.Sz() < 0x160) return;
		var tmr;
if(debug>0){ tmr = new CheckpointTimer(); tmr.init(300); }
		_cyminfo = parseYM2151RegLog(0, X.isDeepScan()? BCParseToEoF: BCParseToReasonable);
if(debug>0)tmr.next('OPM (.cym) parsed');
if(debug>0)_logIt('cym?'+outArray(_cyminfo,16));
		if(_cyminfo[0] < 0) return;
		return true
	}
	if(!X.isOverlay() && isOPMLog()) _setResult("~audio","YM2151 OPM/Callus chiptune (.CYM)","headerless",
		X.isDeepScan() ? "notes:" + _cyminfo[0] + " sz:"+outSz(_cyminfo[1]) : "");

	function isAdlibLog() {
		if(X.Sz() < 0x160) return;
		var tmr;
if(debug>0){ tmr = new CheckpointTimer(); tmr.init(300); }
		_opl2info = parseYM3812RegLog(0, X.isDeepScan()? BCParseToEoF: BCParseToReasonable);
if(debug>0)tmr.next('OPM (.cym) parsed');
if(debug>0)_logIt('cym?'+outArray(_cyminfo,16));
		if(_opl2info[0] < 0) return;
		return true
	}
	if(!X.isOverlay() && isAdlibLog()) _setResult("~audio","YM3812/OPL2/Ad Lib chiptune","headerless",
		X.isDeepScan() ? "notes:" + _opl2info[0] + " sz:"+outSz(_opl2info[1]) : "")

} //isHeuristicScan parallel block

	return result();
}
/* beautify ignore:end */
