textage.cc/score 譜面javascriptの独力parseメモ

1 文字ずつ読んでいって構文解析していく、有限オートマトンの話。大学 1 年のときにやったはず。

……と思っていたが、Node.js に実行させてspなどを TSV などで出力することでも代用できそう。(終)


nodejs.org

Node.js をインストールして、

console.log("hello, world.");

とだけ記述した first.js というテキストファイルを作り、node first.jsで実行したとき、コンソールに「hello, world.」と表示されたらインストール成功。

javascript部分の抽出

HTML ファイルは、

<html><head><META http-equiv="Content-Type" content="text/html; charset=shift_jis">
<title>"EPIC TRANCE" ABSOLUTE</title>
<script type="text/javascript" src="../bms2jsh.js"></script>
</head><body><script type="text/javascript"><!--
genre="EPIC TRANCE";title="ABSOLUTE";

// (中略)

  dp=[ /* 中略 */ ,"#ODAAY","80"];}
}

hd();w("<table><tr>");for(x=0;x<measure;x+=4){w("<td valign=bottom>");
if(x==36){b(36,37);x-=2;}else b(x,x+3);w("<\/td>");}ft();
//--></script></body></html>

のような記述をしている。

譜面データ定義部

そのため、主となる譜面データ定義部は<!--から-->で囲まれた箇所として、hd();呼び出し以降を読み捨てれば良さそう。この javascript 部分を「譜面データ定義部」と呼ぶ。

変数初期化部

「譜面データ定義部」より前に、bms2jsh.js ファイルの変数初期化部分である

ln=[],sp=[],dp=[],tc=[],c1=[],c2=[],cn=[];
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;

を挿入する。この javascript を「変数初期化部」と呼ぶ。

譜面種別の分岐

「変数初期化部」と「譜面データ定義部」の間に、どの譜面種別を読み込みたいかを決定する。DP 譜面に限れば、l, hps, a, kuroの 4 変数に下記表のような0, 1の値が設定されていればよい。

- l hps a kuro
NORMAL 1 1 0 0
HYPER 0 1 0 0
ANOTHER 0 0 1 0
LEGGENDARIA 0 0 1 1
const SCORE_FLAGS = {'N': [1, 1, 0, 0], 'H': [0, 1, 0, 0], 'A': [0, 0, 1, 0], 'L': [0, 0, 1, 1]};
for (const SCORE_TYPE in SCORE_FLAGS) {

  // 「変数初期化部」

  l = SCORE_FLAGS[SCORE_TYPE][0];
  hps = SCORE_FLAGS[SCORE_TYPE][1];
  a = SCORE_FLAGS[SCORE_TYPE][2];
  kuro = SCORE_FLAGS[SCORE_TYPE][3];

  // 「譜面データ定義部」

  // 「TSV 出力部」(後述)
}

のようなループ処理を行えば node の実行 1 回で、1 ファイルの DP 全パターンを TSV ファイルに出力できるはず。

TSV 出力部

譜面種別が決定されて「譜面データ定義部」を通過すると、sp, dp, c1, c2, tc, lnが確定する。楽曲によっては、チャージノーツがなかったり BPM 変動がなかったりするため、その場合はc1, tcなどは空配列になるだろう。
ただ、tcが未定義だと BPM の初期情報が出力できないので、ちょっと工夫が必要そうだ。

TSV ファイルは以下のような項目で構成する。

楽曲ID プレースタイル 譜面種別 出力配列 小節番号 データ

それぞれ、

楽曲ID "absolute""pperplex""_sakura"など
プレースタイル "SP"または"DP"
譜面種別 "B", "N", "H", "A", "L"
出力配列 "sp", "dp", "c1", "c2", "tc", "ln"
小節番号 0以上の整数
データ 「出力配列」の「小節番号」要素に格納された文字列や配列の文字列表現。undefinedの場合は空文字列

を想定する。データ部は、"c1""tc"のときに配列なので、文字列表現された配列を元に戻すときに少し処理が必要だろう。

楽曲 ID の決定

Node.js で実行する js ファイル名を、楽曲 ID とする(例: absolute.js )。このとき、Node.js は__filenameという予約語で実行する js ファイルのフルパスが獲得できる。

const FILENAME_PATHS = __filename.split('\\');
const FILENAME = FILENAME_PATHS[FILENAME_PATHS.length - 1].replace('\.js', '');
const MUSIC_ID = FILENAME;

Windows で実行しているときは、ディレクトリ区切り文字が\であるため、このようにしてファイル名の拡張子部分を除いた文字列が、楽曲 ID 文字列としてMUSIC_IDに格納できる。

譜面の有無

すべての楽曲に DP-NORMAL 譜面および DP-HYPER 譜面が用意されているが、DP-ANOTHER 譜面や DP-LEGGENDARIA 譜面は一部の楽曲に実装されていない。実装されていない譜面種別については、処理をスキップさせたほうがよい。

譜面種別ごとのレベル数値は、actbl.js に定義されている。この actbl.js をrequire()で参照できるよう、

// 変更前
actbl = {

// 変更後
exports.actbl={

actblオブジェクトをexportsオブジェクトのメンバとするように修正する。これで

const ACTBL = require('./include/actbl.js');

のようにrequireして、

  l = SCORE_FLAGS[SCORE_TYPE][0];
  hps = SCORE_FLAGS[SCORE_TYPE][1];
  a = SCORE_FLAGS[SCORE_TYPE][2];
  kuro = SCORE_FLAGS[SCORE_TYPE][3];
  k = 0;

  if (SCORE_TYPE == 'A' && ACTBL.actbl[MUSIC_ID][19] < 1) {
    continue;
  }
  if (SCORE_TYPE == 'L' && ACTBL.actbl[MUSIC_ID][21] < 1) {
    continue;
  }

[19]は DP-ANOTHER 譜面のレベル、[21]は DP-LEGGENDARIA 譜面のレベルで、この値が0のときは譜面が実装されていないことを意味する。

二次元配列の文字列表現化

Node.js が二次元配列をconsole.log()で出力するとき、複数行で出力されることがある。TSV ファイルに出力するときは、1 小節 1 行で出力されてほしいので、

// TSV 出力部
let OUTPUT_ARRAYS = {'sp': sp, 'dp': dp, 'c1': c1, 'c2': c2, 'tc': tc, 'ln': ln}
for (const ARRAY_NAME in OUTPUT_ARRAYS) {
  let ARRAY = OUTPUT_ARRAYS[ARRAY_NAME];
  if (ARRAY) {
    for (let ix = 0; ix < ARRAY.length; ix++) {
      let DATA = ARRAY[ix];
      if (DATA === undefined) {
        DATA = '';
      }

      let DATALINE = '';
      if (Array.isArray(DATA)) {
        DATALINE += '[';
        for (let jx = 0; jx < DATA.length; jx++) {
          if (jx > 0) {
            DATALINE += ', ';
          }
          let element = DATA[jx];
          if (Array.isArray(element)) {
            DATALINE += ("[" + element.toString(element) + "]");
          } else {
            DATALINE += element;
          }
        }
        DATALINE += ']';
      } else {
        DATALINE = DATA;
      }
      console.log(MUSIC_ID, HTAB, MODE_DP, HTAB, SCORE_TYPE, HTAB, ARRAY_NAME, HTAB, ix, HTAB, DATALINE);
    }
  }
}

DATAの代わりにDATALINEを構築することで、複数行出力を回避する。

HTML ファイルから「譜面データ定義部」を抽出

Python スクリプトで、読み込む HTML ファイルをargvで取得し、標準出力に書き出す。

import sys

args = sys.argv

if len(args) > 1:
        html_file = args[1]
        fp_html = open(html_file, encoding='MS932')
        text_html = fp_html.read()

        part1_file = 'source/part_1_init.txt'
        fp_part1 = open(part1_file)
        text_part1 = fp_part1.read()

        part2_file = 'source/part_2_output.txt'
        fp_part2 = open(part2_file)
        text_part2 = fp_part2.read()

        comment_start = text_html.find('<!--')
        comment_end = text_html.rfind('-->')
        func_hd_call = text_html.find('hd();')

        if comment_start > 0 and comment_end > 0 and func_hd_call > 0 and func_hd_call < comment_end:
                text_notes = text_html[comment_start + 4 : func_hd_call]

                print(text_part1)
                print(text_notes)
                print(text_part2)

open(path, encoding='MS932')は、ジャンル名、楽曲名、アーティスト名に ASCII 以外が含まれる場合を考慮したもの。
ここで、source/part_1_init.txt は以下のように定義し、

const FILENAME_PATHS = __filename.split('\\');
const FILENAME = FILENAME_PATHS[FILENAME_PATHS.length - 1].replace('\.js', '');
const MUSIC_ID = FILENAME;

const ACTBL = require('./include/actbl.js');

// 譜面種別
const HTAB = '\t';
const MODE_DP = 'DP';
const SCORE_FLAGS = {'N': [1, 1, 0, 0], 'H': [0, 1, 0, 0], 'A': [0, 0, 1, 0], 'L': [0, 0, 1, 1]};
for (const SCORE_TYPE in SCORE_FLAGS) {

// 変数初期化部
ln=[],sp=[],dp=[],tc=[],c1=[],c2=[],cn=[];
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;
LNDEF=384

// 譜面種別確定部
  l = SCORE_FLAGS[SCORE_TYPE][0];
  hps = SCORE_FLAGS[SCORE_TYPE][1];
  a = SCORE_FLAGS[SCORE_TYPE][2];
  kuro = SCORE_FLAGS[SCORE_TYPE][3];
  k = 0;

  if (SCORE_TYPE == 'A' && ACTBL.actbl[MUSIC_ID][19] < 1) {
    continue;
  }
  if (SCORE_TYPE == 'L' && ACTBL.actbl[MUSIC_ID][21] < 1) {
    continue;
  }

// 譜面データ定義部

同様に、source/part_2_output.txt は以下のように定義する。

  // TSV 出力部
  let OUTPUT_ARRAYS = {'sp': sp, 'dp': dp, 'c1': c1, 'c2': c2, 'tc': tc, 'ln': ln}
  for (const ARRAY_NAME in OUTPUT_ARRAYS) {
    let ARRAY = OUTPUT_ARRAYS[ARRAY_NAME];
    if (ARRAY) {
      for (let ix = 0; ix < ARRAY.length; ix++) {
        let DATA = ARRAY[ix];
        if (DATA === undefined) {
          DATA = '';
        }

        let DATALINE = '';
        if (Array.isArray(DATA)) {
          DATALINE += '[';
          for (let jx = 0; jx < DATA.length; jx++) {
            if (jx > 0) {
              DATALINE += ', ';
            }
            let element = DATA[jx];
            if (Array.isArray(element)) {
              DATALINE += ("[" + element.toString(element) + "]");
            } else {
              DATALINE += element;
            }
          }
          DATALINE += ']';
        } else {
          DATALINE = DATA;
        }
        console.log(MUSIC_ID, HTAB, MODE_DP, HTAB, SCORE_TYPE, HTAB, ARRAY_NAME, HTAB, ix, HTAB, DATALINE);
      }

      if (ARRAY_NAME == 'tc' && ARRAY.length == 0) {
        // tc 未定義時の初期BGM補完
        let bpm_init = parseInt(bpm, 10);
        if (bpm_init == bpm) {
          if (bpm_init < 100) {
            bpm_init = ' ' + bpm_init;
          }
          bpm_init += '0';
          console.log(MUSIC_ID, HTAB, MODE_DP, HTAB, SCORE_TYPE, HTAB, ARRAY_NAME, HTAB, 0, HTAB, '["' + bpm_init + '"]');
        }
      }
    }
  }
  console.log(MUSIC_ID, HTAB, MODE_DP, HTAB, SCORE_TYPE, HTAB, 'LNDEF', HTAB, 0, HTAB, LNDEF);
}

標準出力に書き出された文字列を js ファイルにリダイレクトするなどして、その js ファイルを Node.js で実行すると TSV ファイルが入手できる。”tc"0としてbpm値を出力したり、"LNDEF"0としてLNDEF値を出力する処理を追加した。

これにより、一般的な 4 分の 4 拍子の曲では

511 	 DP 	 A 	 dp 	 36 	 #p7P54DA
511 	 DP 	 A 	 tc 	 0 	 ["1010"]
511 	 DP 	 A 	 LNDEF 	 0 	 384

のように TSV 出力され、それ以外の曲では

penta_cb 	 DP 	 A 	 dp 	 67 	 #O7mUpQQAHg6GA
penta_cb 	 DP 	 A 	 tc 	 0 	 ["1830"]
penta_cb 	 DP 	 A 	 LNDEF 	 0 	 480

のように TSV 出力される。