textage.cc/score 譜面解釈メモ

曲別のHTMLファイルを取得して、内部のJavaScript部を解析するところまで

曲別のHTMLを取得

曲別のHTMLファイルから呼び出される javascript ファイルは以下の通り

  • bms2jsh.js

bms2jsh.js の譜面種別判定部

先頭の定数定義部

b64="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
s=location.search,LNDEF=384,cob=["s","w","b","w","b","w","b","w"];

b64Base64 encode/decode用の独自文字列。
sは URL のクエリ文字列で、?1H800のように(?を含む)文字列が格納される。

obr=[[0,1,2,3,4,5,6,7],[0,1,2,3,4,5,6,7]];kc=[[0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0]];
dpalls=[[0,1,2,3,4,5,6,7],[0,7,6,5,4,3,2,1]];imgdir="../";diftype="";twstr="";memo="";
ln=[],sp=[],dp=[],tc=[],c1=[],c2=[],cn=[],sides=["",2,2],csd=["","left","right"];cnc=[0,0,0];
dw=[134,121],dr=[38,4],df=[134,119],ms=["",""," class=m1"," class=m2"];tm=new Date();hsa=8;

各種の配列などの初期化。

genre=title=artist=bpm=opt=lnse=lnhs="",key=ky=back=7,hs=gap=ty=k=1;cncnt=bsscnt=legacy=prt=pty=0;
soflan=level=notes=measure=a=l=m=g=db=p1o=hps=flp=off=lnln=lnst=lned=alls=hids=sran=kuro=sftkey=os=hcn=ttl=0;

空文字("")または01などで変数初期化。

char1=s.charAt(1);
char2=s.charAt(2);
level=s.charAt(3)? s.charAt(3):0;
char2lower=char2.toLowerCase();
if(char2==char2lower){prt=1;imgdir="../prt/";}
char2=char2.toUpperCase();

クエリ文字列の 2 文字目をchar1に設定し、3 文字目をchar2に設定。

if(char2=="X"){a=1;kuro=1;}
if(char2=="A")a=1;
if(char2=="L")l=1;
if(char2=="N"){l=1;hps=1;}
if(char2=="H")hps=1;
if(char2=="B"){l=1;g=1;}
if(char2=="G" || char2=="R"){l=1;hps=1;g=1;}
if(char2=="P"){l=1;hps=1;pty=1;g=1;}

char2は、"P"がBEGINNER、"N"がNORMAL、"H"がHYPER、"A"がANOTHER、"X"がLEGGENDARIAに相当。

譜面種別と変数の対応表

- l hps a kuro pty g
P (BEGINNER) 1 1 0 0 1 1
N (NORMAL) 1 1 0 0 0 0
H (HYPER) 0 1 0 0 0 0
A (ANOTHER) 0 0 1 0 0 0
X (LEGGENDARIA) 0 0 1 1 0 0
if(char1=="1")sides[1]=1;
if(char1=="D"){k=0;key=14;}
if(char1=="L"){k=0;key=14;os=1;sides[1]=1;}
if(char1=="R"){k=0;key=14;os=2;}
if(char1=="F"){flp=1;k=0;key=14;}
if(char1=="M"){m=1;k=0;key=14;}
if(char1=="B"){db=1;key=14;}

char1は、"1"がSINGLE(1P)、"2"がSINGLE(2P)、"D"がDOUBLEに相当。

モードと変数の対応表

- sides[1] k key
SINGLE(1P) 1 1 7
SINGLE(2P) 2 1 7
DOUBLE 2 0 14

曲別のHTML直書き部

最序盤

genre="EPIC TRANCE";title="ABSOLUTE";
artist="dj TAKA";bpm="60~144";measure=69;tc[0]=["14400"];
tc[67]=["13364","12096"];tc[68]=["11200"," 8932"," 8564"," 6096"];tc[69]=[" 8000"];

tc配列は BPM を定義しており、先頭から 3 文字が BPM の数字、4 文字以降が小節内の BPM 変更位置(単位は 4 分音符 1 つの長さを 32 とする)を表す。BPM が 100 未満のときは、1 文字目を半角スペースとする。

genre="SPIRITUAL";title="桜";
artist="Reven-G";bpm="13~320";measure=104;ln[61]=768;
tc[1]=["3000"];tc[49]=["1500"];tc[51]=["13564","12796"];tc[102]=["3000","27064"];
tc[52]=["1170"," 8932"," 6064"," 4496"," 13112"];tc[53]=["1500"];tc[61]=["3200"];
tc[103]=["2500","22064","17096"];tc[104]=["1490","12932"," 8964"," 6096"];

別の例では、「桜」tc[52][4]" 13112"と 6 文字になっており、112 はいわゆる「4 拍目の裏」(= 8 拍子の 8 拍目)にあたる。

if(kuro){hcn=1;
  if(k){
    if(a){notes=1412;
      // SP-LEGGENDARIA の定義部
    }
  }else{
    if(a){notes=1488;
      // DP-LEGGENDARIA の定義部
    }
  }
}else if(g&&hps){notes=309;
  // SP-BEGINNER の定義部
}else if(k){notes=935;
  // SP-HYPER の定義部
  if(a){notes=1218;
    // SP-ANOTHER の定義部
  }
  if(l){notes=682;
    // SP-NORMAL の定義部
    if(g){notes=514;
      // ????????
    }
  }
}else{notes=934;
  // DP-HYPER の定義部
  if(a){notes=1338;
    // DP-ANOTHER の定義部
  }
  if(l){notes=728;
    // DP-NORMAL の定義部
  }
}

構造が複雑だが、直前で定義した譜面パターンを小節単位で流用したい意図があるようだ。

譜面パターンの記述

DP-NORMAL 譜面から一部抜粋

sp=[,"#O4YTC","#oBADC_","#OEFAI","#O90+u","#O4dDD", /*(中略) */ ,"#OgysG",];

dp=[,"#OEFAI_","#O10+u",sp[12],"#oRRTK","#OEAQI", /* (中略) */, "80"];}

sp配列は、SPまたはDP-1P側の譜面パターン、dp配列は、DP-2P側の譜面パターンを定義している。
配列の要素 1 つにつき、1 小節の譜面パターンを定義している。

配列の要素が"#"で始まる文字列か、そうではない文字列かによって、解釈のアルゴリズムが異なる。

DP-LEGGENDARIA 譜面から一部抜粋

c2[2]=c2[4]=c1[6]=c1[13]=c1[15]=c1[17]=c1[19]=[[7,0,126]];c1[41]=[[7,64,62]];
c1[45]=[[7,64,14],[6,80,14],[5,96,14],[4,112,14]];c2[46]=[[1,0,94],[3,96]];
c2[47]=[[1,0],[2,32],[3,64],[4,96]];c2[48]=[[3,0,94],[4,96]];c2[49]=[[2,0]];

c1配列は、SP または DP-1P 側のチャージノーツ譜面パターン、c2配列は、DP-2P 側のチャージノーツ譜面パターンを定義している。

bms2jsh.js の譜面解析部

通常ノーツ "#" 始まり

if(sdd.charAt(0)=="#"){
	sft++;
	v2c=0;
	while(sft<sdd.length){
		v2o="";
		v2v=(v2c? 1:3)*ln[n]/6;
		switch(sdd.charAt(sft)){
			case "C":v2s= 0;v2p=192;v2t=0;if(!v2c)v2o=sdd.charAt(++sft);sft++;break;
			case "c":v2s=96;v2p=192;v2t=0;if(!v2c)v2o=sdd.charAt(++sft);sft++;break;
//(中略)
			case "B":v2s= 0;v2p=192;v2t=1;v2b=Math.ceil(v2v/v2p)+1;v2o=sdd.substring(sft+1,sft+v2b);sft+=v2b;break;
			case "b":v2s=96;v2p=192;v2t=1;v2b=Math.ceil(v2v/v2p)+1;v2o=sdd.substring(sft+1,sft+v2b);sft+=v2b;break;
//(中略)                                                                                               
			case "S":v2s= 0;v2p= 64;v2t=1;v2b=Math.ceil(v2v/v2p)+1;v2o=sdd.substring(sft+1,sft+v2b);sft+=v2b;break;
			case "s":v2s=32;v2p= 64;v2t=1;v2b=Math.ceil(v2v/v2p)+1;v2o=sdd.substring(sft+1,sft+v2b);sft+=v2b;break;
//(中略)
			case "1":case "2":case "3":case "4":case "5":case "6":case "7":
				v2o=sdd.substring(sft,sft+3);v2t=2;sft+=3;break;
			case "9":v2o="1"+sdd.substring(sft+2,sft+4);
			case "8":for(i2=0;i2<6;i2++){
						if(b64.indexOf(sdd.charAt(sft+1))&(1<<i2))v2o+=(i2+2)+sdd.substring(sft+2,sft+4);
					}v2t=2;sft+=4;break;

			case "-":v2c=1;sft++;break;
			case "_":v2o=(sft==sdd.length-1)? "AA":sdd.substring(sft+1);v2c=v2t=2;break;

			default:w("<font color=red>error<\/red>");return;
		}
		if(sdd.charAt(sft-1)=="-")continue;

変数

sdd 文字列 sp配列やdp配列の要素文字列が設定されている(例えば"#O4YTC"
sft 整数 初期値0sddの何文字目を読むかを表す
v2c 整数 初期値0sddを読み進めて"-"が登場したら1"_"が登場したら2の値に更新される
v2o 文字列 sddの部分文字列を格納
v2v 整数 なんらかの長さ。ln[n]がデフォルト値384のとき、192または64の値をとる
v2s 整数 0,12,24,48,96または16,32の値をとる
v2p 整数 12,24,48,96,192または16,32,64の値をとる
v2t 整数 0,1,2の値をとる
v2b 整数 Math.ceil(v2v/v2p) + 1 の計算結果が入り、sddから何文字取得するかを意味する
v2k 文字列 v2oを 1 文字ずつ解析した結果を格納
v2t0のとき
	// case "C":v2s= 0;v2p=192;v2t=0;if(!v2c)v2o=sdd.charAt(++sft);sft++;break;
	// ln[n] はたいてい 384
if(v2t==0){
	for(i2=v2s;i2<ln[n];i2+=v2p)v2k+=(v2c ? "1":v2o);
}

この例の場合、i20192を取り、v2kにはv2oの文字列が 2 文字重ねられている。

v2t1のとき
// b64="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

if(v2t==1){
	for(i2=0;i2<v2o.length;i2++){
		if(v2c==0){
			v2x=b64.indexOf(v2o.charAt(i2));
			v2k+=Math.floor(v2x/8)+""+v2x%8;
		}else if(v2c==1){
			v2x=b64.indexOf(v2o.charAt(i2));
			for(i3=5;i3>=0;i3--)v2k+=(v2x>>i3)&1 ? 1:0;
		}
	}
}

v2oに対してBase64 decodeのような処理を行う。v2xには063のいずれかが代入される。
v2k0のとき、その整数を「2 文字の 8 進数表記」としてv2kに格納し、
v2k1のとき、その整数を「6 文字の 2 進数表記」としてv2kに格納する。

v2t2以外の時(= 0または1のとき)

v2kをもとにノーツ描画する。

if(v2t!=2){
	for(v2i=0,i2=v2s;i2<ln[n];v2i++,i2+=v2p){
		if(hids && v2c)continue;
		if((ob2=v2k.charAt(v2i))!=0){
			if(alls&&key==14){
				// (ALL SCRATCH パターンの処理)(省略)
			}else if(v2c&&!alls){
				ob2=0;
				if(conum==0)co[0]=((Math.floor(i2/6/mnbase)%mnloop)==mn)? "r":"s";
			}else if(sran) {
				// (S-RANDOM パターンの処理)(省略)
			}
			if(h==1 && stat_on==0)p1o++;
			objtab[i2] |= (1<<ob2);
			jpos=obr[h-1][ob2];
			if(jpos==8)continue;
			if(stat_on==0){kc[h-1][jpos]++;npos.push((n+gap)*100000+Math.floor(i2*hs/3)+nmergin[n+gap]);}
			// obj!
			stat_insert(h-1, ob2, stat_pos + i2, 0);
			objstr=imgdir+co[jpos]+dstr;
			ttl++;
			o+="<img src="+objstr+".gif"+msc+" name=ttl"+ttl+
				" onMousedown=dragOn('ttl"+ttl+"') style='top:"+
				(nbar*hs-Math.floor(i2*hs/3)+coy)+"px;left:"+
				(ob2 ? jpos*(14-d)-sh*(37-d*8)+60-d*15
				:(sh-1)*(98-d*7))+"px'>";
		}
	}
}

img 要素の style 属性に含まれる

"left:" + (ob2 ? jpos*(14-d)-sh*(37-d*8)+60-d*15 : (sh-1)*(98-d*7))+"px"

部分について検討する。

ob2は、v2kの先頭から順に 1 文字代入される。このときob20ならば読み飛ばす。
また、v2c1または2のとき、ob20として扱う。
次に、jposobr[h-1][ob2]の値が設定される。

obr=[[0,1,2,3,4,5,6,7],[0,1,2,3,4,5,6,7]];

hは、1P 側を描画するときに1、2P 側を描画するときに2が設定されている。
obrは、RANDOM配置のときに、どのレーンがどのレーンに入れ替わるかを保存する二次元配列である。正規譜面のときは上述のデフォルト値から変更されないため、jposにはob2と同じ値が代入される。

よって、jposすなわちob2が分かれば、鍵盤レーンかターンテーブルレーンかが判明する。

同様に、

"top:" + (nbar*hs-Math.floor(i2*hs/3)+coy)+"px"

部分について検討する。

coy=-5;
// (中略)
c4tmp=parseInt(s.charAt(4),16);
d=(c4tmp&1)? 1:0;
hs=((c4tmp&6)+4)/4;
// (中略)

function bars_(n,mn){
	// (中略)
	if(!ln[n])ln[n]=LNDEF;
	nbar=Math.ceil(ln[n]/3);
	if(nbar<4)nbar=4;
	// (中略)
	for(v2i=0,i2=v2s;i2<ln[n];v2i++,i2+=v2p){

s.charAt(4)は標準0なので、c4tmp0で、hs1になる。
ln[n]は標準384なので、nbar128になる。
i2は for 構文において、初期値v2s、増分v2pとなる。

	case "C":v2s= 0;v2p=192;v2t=0;if(!v2c)v2o=sdd.charAt(++sft);sft++;break;
	// (中略)
	case "s":v2s=32;v2p= 64;v2t=1;v2b=Math.ceil(v2v/v2p)+1;v2o=sdd.substring(sft+1,sft+v2b);sft+=v2b;break;||<

case "C" のとき、v2s=0; v2p=192;とあり、i20,192を取り(=4分音符のオモテ)、
case "s" のとき、v2s=32; v2p=64;とあり、i232,96,160,224,288,352を取る(=『6 分音符』のウラ)。
ln[n]の標準である384は、「4 分の 4 拍子」における「4 拍」ぶんを表しており、96で「4 分音符 1 つぶん」に相当する長さである。
1 小節の長さln[n]におけるi2が、小節の先頭からの位置にあたる。

v2t が 2 のとき
// b64="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

for(i2=0;i2<v2o.length;i2+=2){

	if(v2c==0){ob2=v2o.charAt(i2);i2++;}else ob2=0;
	if(hids && ob2==0)continue;
	v2h=b64.indexOf(v2o.charAt(i2))*64+b64.indexOf(v2o.charAt(i2+1))*1;
	if(alls&&key==14){
		// (ALL SCRATCH パターンの処理)(省略)
	}else if(mn!=void(0)&&!alls){
		if(conum==0)co[0]=((Math.floor(v2h/6/mnbase)%mnloop)==mn)? "r":"s";
	}else if(sran) {
		// (S-RANDOM パターンの処理)(省略)
	}
	if(h==1 && stat_on==0)p1o++;
	objtab[v2h] |= (1<<ob2);
	jpos=obr[h-1][ob2];
	if(jpos==8)continue;
		// (省略)
	ttl++;
	o+="<img src="+objstr+".gif"+msc+" name=ttl"+ttl+
		" onMousedown=dragOn('ttl"+ttl+"')  style='top:"+
		(nbar*hs-Math.floor(v2h*hs/3)+coy)+"px;left:"+
		(ob2 ? jpos*(14-d)-sh*(37-d*8)+60-d*15
		:(sh-1)*(98-d*7))+"px'>";

v2c0のとき、v2oを 3 文字ずつ読み込む。そのうち前 1 文字をob2に代入し、後ろ 2 文字はb64を利用して 64 進数の 2 文字とみなしてv2hに代入する。
v2c0以外のとき、ob20を代入する。v2oから 2 文字ずつ読み込み、b64を利用して 64 進数の 2 文字とみなしてv2hに代入する。

正規譜面のとき、jposにはob2と同じ値が代入される。

s.charAt(4)は標準0なので、c4tmp0で、hs1になる。
ln[n]は標準384なので、nbar128になる。
1 小節の長さln[n]におけるv2hが、小節の先頭からの位置にあたる。

通常ノーツ "#" 以外で始まり

sft=div=0;
// (中略)
	if(sdd.charAt(0)=="x"){
		len=parseInt(sdd.substring(1,4),16);
		sft=4;
	}else len=sdd.length;

sddが "x" で始まるときは、sddの 2 ~ 4 文字目にあたる 3 文字を 16 進数とみなして、lenに設定し、sft4とする。
sddが "x" で始まらないときは、lensddの長さとする。この場合、sftは初期値の0である。

sd=[[],sp,dp];
// (中略)
for(mm=0; mm<=m; mm++){
	sdd=sd[h+mm][n];
// (中略)
for(;sft<sdd.length;sft+=2,div+=2){
	while(sdd.charAt(sft)=="@"){
		div+=parseInt(sd[h+mm][n].substring(sft+1,sft+3),16)*2;
		sft+=3;
	}
	if(mn!=void(0)&&conum==0)co[0]=((Math.floor(nbar*div/(len*2*mnbase))%mnloop)==mn)?"r":"s";
	y=parseInt(sdd.substring(sft,sft+2),16);
	for(j=0;j<=ky;j++){
		if(y>>j==0)break;
		if(hids && j==0)continue;
		if(y>>j&1){
			if(h==1 && stat_on==0)p1o++;
			jpos = obr[h-1][j];
			if(jpos==8)continue;
			if(alls&&key==14){
				// (ALL-SCRATCH パターン)(省略)
			}else if (sran) {
				// (S-RANDOM パターン)(省略)
			}
			// (統計情報操作)(省略)
			ttl++;
			o+="<img src="+objstr+".gif"+msc+" name=ttl"+ttl+
				" onMousedown=dragOn('ttl"+ttl+"') style='top:"+
				(nbar*hs-Math.floor(nbar*div*hs/len)+coy)+"px;left:"+
				(jpos? jpos*(14-d)-sh*(37-d*8)+60-d*15:(sh-1)*(98-d*7))+"px'>";
		}
	}
}

sdd文字列を先頭からsft文字読み飛ばし、そこから 2 文字ずつ繰り返し読み込んで処理していく for 構文である。
sdd.charAt(sft)"@"であるとき、"@"の次の 2 文字を 16 進数として数値変換した値を 2 倍してdivに加える。"@"が複数回登場するうちはdivを調整する。ここで、div+=parseInt(sd[h+mm][n].substring(sft+1,sft+3),16)*2;におけるsd[h+mm][n]は、sddと同じ文字列である。

次にsddから 2 文字読み取り、16 進数として数値変換した値をyに設定する。
このyを、0 ビット~ 7 ビット右シフトした最下位ビットが1であるかどうかを判定し、1である場合に右シフトしたビット数(=j)に音符ノーツ(17)またはターンテーブルノーツ(0)があることを意味する。
正規譜面の場合は、jposjと同じ値が設定される。

s.charAt(4)は標準0なので、c4tmp0で、hs1になる。
ln[n]は標準384なので、nbar128になる。
1 小節の長さln[n]におけるdivが、小節の先頭からの位置にあたる。

チャージノーツの書式

「MIRACLE MEETS」DP-NORMAL より抜粋

c2[25]=[/* [4,0,62,2], */[4,112,16,1]];
c2[26]=[[4,0,128,0]];
c2[27]=[[4,0,62,2]];
c2[28]=[[0,0,128,1]];
c2[29]=[[0,0,96,2]];
c1[30]=[[0,0,128,1]];
c1[31]=[[0,0,64,2]];

https://textage.cc/score/17/miraclem.html?DN604~24-31 より

配列には最大 4 要素を持つ。[0]はレーン番号(17: 鍵盤、0: ターンテーブル)を表す。[1]はその小節における描画開始位置(小節の先頭を0、単位は 4 分音符 1 つを32とする)を表す。[2]はその小節における描画の長さを表す(省略時は30)。[3]はこのチャージノーツがこの小節内に収まるか、複数小節にまたがるかを表現する値を表し、0は「前の小節から継続、次の小節に継続」、1は「この小節から開始、次の小節に継続」、2は「前の小節から継続、この小節で終端」、3は「この小節で開始して終端する」、7は MSS(Multi Spin Scratch)のいずれかを取る。[3]の省略時は3が採用される。

c2[52]=c1[60]=[[57,0,62],[46,64],[35,96]];c2[53]=c1[61]=[[24,0,62],[13,96]];

[0]が 10 以上の場合は、2 つ以上のレーンを 1 つの定義にまとめている。57は 5 鍵と 7 鍵の同時押しチャージノーツ。

sc32配列について

「(This Is Not) The Angels」SP-ANOTHER より抜粋

sc32=[],sc32base=[],sc32loop=[];
// (中略)
sc32[19]=sc32[55]=4;

19 小節目にあるのは「32 分音符」間隔のターンテーブルノーツのようだ。

// (中略)
function bars_(n,mn){
	// (中略)
	if(hs>1)mn=void(0);
	else if(sc32[n]>=0)mn=sc32[n];
	mnbase=sc32base[n];
	if(sc32base[n]==undefined)mnbase=8;
	mnloop=sc32loop[n];
	if(sc32loop[n]==undefined)mnloop=mnbase;
	ttlstart=ttl;
	ttlcn=0;

hs1なので else if 節に進み、n19または55のときには、mnsc32[19]すなわち4が設定される。
sc32base[n]は未定義なので、mnbaseにデフォルト値8が設定され、同様にsc32loop[n]も未定義なので、mnloopにも8が設定される。

	}else if(v2c&&!alls){
		ob2=0;
		if(conum==0)co[0]=((Math.floor(i2/6/mnbase)%mnloop)==mn)? "r":"s";
	// (中略)
	objstr=imgdir+co[jpos]+dstr;
	ttl++;
	o+="<img src="+objstr+".gif"+msc+" name=ttl"+ttl+

mnbase, mnloop, mnは、co[0]"r"または"s"のどちらを設定するかを決めるためのもので、これはobjstr文字列を構成して、img 要素に組み込まれる gif ファイル名に使われるだけだ。