0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

kshをjson化した変な話

Posted at

#まえがき
こちらの記事は、CCC Advent Calendar 2018の21日目の記事です。

#前振り
大半の人はタイトルを読んだとき、「ん?」って思ったかもしれません。
一部の人にとっては、タイトルを読んだとき「は?」って思ったかもしれません。

つまりは、誰からしても変なことしているなと、思ったことでしょう。
まさにその通りで、この記事は誰得極まりないものですw
だけど自分にとっては需要があったということなだけですw

それは置いといて。

##ksh?

kshとは何か。
一言で言えば「音楽ゲームにおける譜面データ」ということです。

K-Shoot Mania (以降KSM)」 という某K○NAMIさんから出ている音楽ゲーム「SD○X」を模したPCゲームがあるんです。
自分はS○VXが好きだったこともあって、遊ぶことが多々ありました。
KSMには譜面作成機能というのがありまして、作成した譜面を保存するときのファイル形式が「.ksh」なのです。

##なんでkshをjson化したの?

細かい経緯は端折るんですが、
あるときですね、自分は音楽ゲームの譜面データがjson形式で必要になってしまいました。

jsonには譜面を提示するタイミング(ms)とどのボタンに割り当てるか。

この二つの情報が必要になってしまいました。
提示するタイミングがmsで必要なので、いちいち計算するの面倒くさい・・・。

さてどうするか。

「json形式で必要な音楽ゲームがパネル4枚。」
「KSMの白鍵の数は4つ。」
「kshで作ったものをjson化できるなら、GUIで楽に譜面生成できそう」

そんな変なことに気が付いた私は、「kshをjson化しよう」そんな変なことを思いついてしまいました。

#本題

さて本題。
音楽ゲームにおける譜面の要素で必要になってくるのが
BPMとどのタイミングでどこに譜面を配置するか。

##BPM2ms
BPMというのは「Beat Per Minute」の略です。
日本語に訳すと、「1分間に何回4音符を鳴らすか」ということです。
これはもう計算式が決まっています。
一応、しっかり導出してみましょう。

BPM160を例にとりましょう。
これは1分間に160回4分音符が鳴ることを意味します。(はやっ)
つまり、
$ \dfrac{60(s)}{160(回)}=0.375(s)=375(ms)\quad -①$

ということです。
つまり、BPM160は375ms間隔でというのは4分音符が鳴ることを示しています。

それっぽく言うならば、
BPM160で、4分音符間隔でなっている音が時刻 $t[ms]$ で$n$番目に鳴ったとする(これを $S_n$ とする)。
$S_{n+1}$ で鳴る音は、$S_n$ の375[ms]後ということです。
(要らん説明でしたかね)

もし、八分音符ならばこの値の半分になり、二分音符の場合は倍になるのは自明だと思います。
例えば、BPM100の8分音符とBPM200の4分音符は同じこと言っていることが分かれば、すぐ気づくと思います。

では、先ほどの式①を少し変えましょう。
$\text{音符の長さ}[ms] = 60\times 1000[ms]\times A_{p}\div \text{BPM}$
ただし、$A_p$ は $p$ 分音符の時の定数をさします。

補足
$A_1=4$ $A_2=2$ $A_4=1$ $A_8=0.5$ $A_{12}=0.333..$ $A_{16}=0.25$ $A_n= \dfrac{4}{n}$

これで、音符の長さが分かります。

例えば、BPM200の12分音符の長さはどれくらいでしょうか?

こたえ
$\text{音符の長さ}[ms] = 60\times 1000[ms]\times 1/3\div 200$ $\text{音符の長さ} = 100[ms] = 0.1[s]$

ではこれをコードに起こします。
今回は自分がよく使っているprocessingを使用します。
正直なところ、書くまでもないんだけど、一応書いておきます。

bpm2ms.pde
float bpm2ms (float bpm, int n) {
  float ms = 60.0 / bpm * 4.0 / n * 1000;
  return ms;
}

BPM170の8分音符を計算してみましょう。

bpm2ms_result.pde
 176.4706

はい。出ましたね。
ネット上には自動で計算してくれるWebサイトがあるので確認してみます。
image.png
↑のサイトはディレイタイム計算機で確認できます。
四捨五入すれば、一致することが確認できました。問題なさそうです。

では次です。

##kshの構造に迫る
では早速kshの構造について迫っていきましょう。
一例をぶん投げます。長いので、たたんでおきます。

example.ksh
example.ksh
title=example
artist=dj example
effect=
jacket=.jpg
illustrator=
difficulty=extended
level=1
t=150
m=example.mp3
mvol=75
o=0
bg=desert
layer=arrow
po=0
plength=15000
pfiltergain=50
filtertype=peak
chokkakuautovol=0
chokkakuvol=50
ver=160
--
beat=4/4
0000|00|--
--
0001|00|--
--
0001|00|--
1000|00|--
--
0001|00|--
0000|00|--
0000|00|--
1000|00|--
--
0001|00|--
0000|00|--
0000|00|--
0000|00|--
0010|00|--
0100|00|--
1000|00|--
0000|00|--
--
0001|00|--
0010|00|--
0100|00|--
1000|00|--
0001|00|--
0000|00|--
0000|00|--
0000|00|--
1000|00|--
0000|00|--
0000|00|--
0000|00|--
0001|00|--
0000|00|--
0000|00|--
0000|00|--
--
0001|00|--
0010|00|--
0100|00|--
0010|00|--
0100|00|--
1000|00|--
0001|00|--
0000|00|--
0000|00|--
0000|00|--
0100|00|--
0000|00|--
--
0001|00|--
0000|00|--
1000|00|--
0100|00|--
0010|00|--
0000|00|--
0100|00|--
0000|00|--
--

kshを詳しく見ていきましょう。
最初の18行くらいまではスルーして、(楽曲情報など今回には関係ないため)その先をよく見てましょう。

--だけの行は小節の区切りをさしていて、----間が1小節であることを意味します。
少しずつ掘り起こしていきましょう。

※ここでは0000|00|--|--は無視します。

beat=4/4のため、以降4分の4拍子として説明していきます。
4分の4拍子とは、1小節が4分が4個で構成されていることを意味しています。
(これは音楽の授業で習ったよね?)

cite1小節.txt
--
0001|00|--
--

これは一番右の白い鍵盤が小節の冒頭(1拍目)に割り当てられていることが書かれています。
それ以外に情報はないので、記述はそれのみとなっています。

cite2.txt
--
0001|00|--
0000|00|--
0000|00|--
1000|00|--
--

これは、1小節に4つの行があります。つまり、4分で1拍目と4拍目にそれぞれ譜面が割り当てられていることが分かります。
しかし、2拍目、3拍目には譜面は割り当てられてないので0のみの記述になっています。

cite3.txt
--
0001|00|--
0010|00|--
0100|00|--
1000|00|--
0001|00|--
0000|00|--
0000|00|--
0000|00|--
1000|00|--
0000|00|--
0000|00|--
0000|00|--
0001|00|--
0000|00|--
0000|00|--
0000|00|--
--

これは16行で小節が構成されています。つまり、16分で表現されていることを意味します。4分の4拍子は16分の16拍子の相当するので、1~5個目までは16分でできていますが、5個目以降は4分で3拍分なので間に16分の3拍分は空欄になっています。

つまり、小節の表現の構成は表現できる最小の単位(n分)まで記述するということになっていそうです。

kshの大まかな構造理解はこれで良いとしましょう。

##json化する
言葉で説明するのは正直なところ難しいのですが、頑張って説明していきます
kshの構造はこんな感じでした。

kshspl.txt
曲や譜面などの基本情報(~20行目)
BPMは8行目の"t="以降
offsetは11行目の"o="以降、単位は(ms)
--
1小節目
--
2小節目
--
3小節目
--
:
:
--
n小節目
--

offsetは音源と譜面の軽微な調整に使います。これは譜面生成タイミングに影響するので、抽出する必要があります。

パースのポイントは0000|00|--のある行がいくつあるかで、表現されている刻みが分かるということ。譜面情報はその中に含まれているということ。

つまり、--間に、幾つの0000|00|--の行があるかを調べ、1000(左から1番目)や0010(左から3番目)のようにどこに割り当てられているかを取得できればいいですね。

それをコードに起こします。

かなり頭の悪い実装の仕方をしてますので、簡易版で。

getNotes.pde
String ksh2json(String str[]){
   //どこからどこまでがn小節目かを取得する。
   //ここから
   int getSectionTop = -1;//n小節目の先頭の行を参照する
   int getSectionBottom = -1;//n小節目の最後の行を参照する
   int top = 0;//さいごに参照した行
   int bottom = str.length;//kshファイル自体の行の大きさ
   int bpm = ~;//BPM保存用
   int offset = ~;//ある行にoffsetがあるので取得する
   int nowMs += offset;//現在の何msかを計算する。
   //ここまでは全部 global変数
   while (/*topがbottomに到達するまで*/){
      getRange(top, bottom, str);
      jsn += picNotes(str);//jsonを更新する
   }
   return jsn;
}
void getRange(int above, int under, String str[]){
   //範囲を取得して、picNotesに役立てる
    for (int i=above; i<under; i++) {
       if (getSectionTop == -1) {
         if (str[i].equals("--")) {
           getSectionTop = i;
         }
       } else if (getSectionBottom == -1) {
         if (str[i].equals("--")) {
           getSectionBottom = i;
         }
    }
}
String picNotes(String str[]){
   int barRange;//何分かどうか
   boolean noNote[] = new boolean [barRange];//ノートがあるかどうかの判定
   int tiles[] = new int[barRange];//譜面データ
   for (int i=getSectionTop+1; i<getSectionBottom; i++){
      //ノーツ情報の有無を取得->noNoteに保存
   }
   for (int i=0; i<tiles.length; i++){
      //ノート情報を記録する
   }
   ms = bpm2ms(~);//分(ぶ)数ごとのmsを計算し、json化するときにタイミングを合わせられるようにする。
   for (int i=0; i<tiles.length; i++){
      //ノート情報をjson化する
      //msを逐一計算
      //ノート情報がなければすっ飛ばして、jsonを作る
   }
   //次の小節を取得するために更新。
   top = getSectionBottom+1;
   getSectionTop = getSectionBottom;
   getSectionBottom = -1;
   return json;
} 
B4U.ksh
B4U.ksh
title=B4U
artist=
effect=
jacket=.jpg
illustrator=
difficulty=extended
level=1
t=170
m=b4u cut.mp3
mvol=75
o=40
bg=desert
layer=arrow
po=0
plength=15000
pfiltergain=50
filtertype=peak
chokkakuautovol=0
chokkakuvol=50
ver=160
--
beat=4/4
0000|00|--
--
0000|00|--
0100|00|--
--
0000|00|--
0000|00|--
0010|00|--
1000|00|--
--
0001|00|--
0100|00|--
0010|00|--
0000|00|--
--
0000|00|--
0000|00|--
1000|00|--
0100|00|--
--
0010|00|--
0001|00|--
1000|00|--
0000|00|--
--
0000|00|--
0000|00|--
0010|00|--
0100|00|--
--
0010|00|--
0100|00|--
0010|00|--
0000|00|--
--
0000|00|--
1000|00|--
--
0000|00|--
0001|00|--
0010|00|--
0000|00|--
--
0100|00|--
0000|00|--
0001|00|--
0010|00|--
--
0100|00|--
0000|00|--
1000|00|--
0000|00|--
0100|00|--
0000|00|--
0000|00|--
0010|00|--
--
0000|00|--
0100|00|--
0010|00|--
0000|00|--
--
0100|00|--
0000|00|--
0000|00|--
0010|00|--
0000|00|--
0000|00|--
0100|00|--
0000|00|--
--
0000|00|--
0000|00|--
0001|00|--
0100|00|--
--
0010|00|--
1000|00|--
0001|00|--
0000|00|--
--
1000|00|--
0010|00|--
--
0100|00|--
0010|00|--
0001|00|--
0000|00|--
--
0000|00|--
0100|00|--
--
0000|00|--
0010|00|--
--
0000|00|--
0100|00|--
--
0000|00|--
1000|00|--
--
0000|00|--
0001|00|--
--
0000|00|--
0010|00|--
--
0100|00|--
0010|00|--
--
0000|00|--
1000|00|--
0001|00|--
0000|00|--
--
0000|00|--
0010|00|--
--
0000|00|--
--
0000|00|--
1000|00|--
--
0000|00|--
--
0000|00|--
0001|00|--
--
0000|00|--
--
0000|00|--
0100|00|--
--
0010|00|--
0000|00|--
0000|00|--
0000|00|--
0100|00|--
0000|00|--
0000|00|--
0010|00|--
--
0000|00|--
0000|00|--
0001|00|--
0000|00|--
0010|00|--
0000|00|--
0000|00|--
0100|00|--
--
0000|00|--
1000|00|--
0100|00|--
0000|00|--
--

これをjson化すると、
B4U.json
B4U.json
[
    {
        "start": "2711",
        "tileNum": "1"
    },
    {
        "start": "5885",
        "tileNum": "2"
    },
    {
        "start": "6842",
        "tileNum": "0"
    },
    {
        "start": "7799",
        "tileNum": "3"
    },
    {
        "start": "8756",
        "tileNum": "1"
    },
    {
        "start": "9713",
        "tileNum": "2"
    },
    {
        "start": "13541",
        "tileNum": "0"
    },
    {
        "start": "14498",
        "tileNum": "1"
    },
    {
        "start": "15455",
        "tileNum": "2"
    },
    {
        "start": "16412",
        "tileNum": "3"
    },
    {
        "start": "17369",
        "tileNum": "0"
    },
    {
        "start": "21197",
        "tileNum": "2"
    },
    {
        "start": "22154",
        "tileNum": "1"
    },
    {
        "start": "23111",
        "tileNum": "2"
    },
    {
        "start": "24068",
        "tileNum": "1"
    },
    {
        "start": "25025",
        "tileNum": "2"
    },
    {
        "start": "28199",
        "tileNum": "0"
    },
    {
        "start": "30416",
        "tileNum": "3"
    },
    {
        "start": "31373",
        "tileNum": "2"
    },
    {
        "start": "33287",
        "tileNum": "1"
    },
    {
        "start": "35201",
        "tileNum": "3"
    },
    {
        "start": "36158",
        "tileNum": "2"
    },
    {
        "start": "37115",
        "tileNum": "1"
    },
    {
        "start": "37819",
        "tileNum": "0"
    },
    {
        "start": "38523",
        "tileNum": "1"
    },
    {
        "start": "39579",
        "tileNum": "2"
    },
    {
        "start": "40888",
        "tileNum": "1"
    },
    {
        "start": "41845",
        "tileNum": "2"
    },
    {
        "start": "43759",
        "tileNum": "1"
    },
    {
        "start": "44815",
        "tileNum": "2"
    },
    {
        "start": "45871",
        "tileNum": "1"
    },
    {
        "start": "48489",
        "tileNum": "3"
    },
    {
        "start": "49446",
        "tileNum": "1"
    },
    {
        "start": "50403",
        "tileNum": "2"
    },
    {
        "start": "51360",
        "tileNum": "0"
    },
    {
        "start": "52317",
        "tileNum": "3"
    },
    {
        "start": "54231",
        "tileNum": "0"
    },
    {
        "start": "55491",
        "tileNum": "2"
    },
    {
        "start": "56751",
        "tileNum": "1"
    },
    {
        "start": "57708",
        "tileNum": "2"
    },
    {
        "start": "58665",
        "tileNum": "3"
    },
    {
        "start": "61839",
        "tileNum": "1"
    },
    {
        "start": "64359",
        "tileNum": "2"
    },
    {
        "start": "66879",
        "tileNum": "1"
    },
    {
        "start": "69399",
        "tileNum": "0"
    },
    {
        "start": "71919",
        "tileNum": "3"
    },
    {
        "start": "74439",
        "tileNum": "2"
    },
    {
        "start": "75699",
        "tileNum": "1"
    },
    {
        "start": "76959",
        "tileNum": "2"
    },
    {
        "start": "79176",
        "tileNum": "0"
    },
    {
        "start": "80133",
        "tileNum": "3"
    },
    {
        "start": "83307",
        "tileNum": "2"
    },
    {
        "start": "87238",
        "tileNum": "0"
    },
    {
        "start": "91169",
        "tileNum": "3"
    },
    {
        "start": "95100",
        "tileNum": "1"
    },
    {
        "start": "96360",
        "tileNum": "2"
    },
    {
        "start": "97768",
        "tileNum": "1"
    },
    {
        "start": "98824",
        "tileNum": "2"
    },
    {
        "start": "99880",
        "tileNum": "3"
    },
    {
        "start": "100584",
        "tileNum": "2"
    },
    {
        "start": "101640",
        "tileNum": "1"
    },
    {
        "start": "102949",
        "tileNum": "0"
    },
    {
        "start": "103906",
        "tileNum": "1"
    }
]

こんな感じになります。
ソフランや変拍子に対応してないなど、色々と不足部分があるのですが、ひとまず、骨格となる部分は完成です。

さいごに

はい。
誰得な話でしたが、今回の記事は終わりです。
唐突な終わり方ですし、解説とかも皆無。
これ大丈夫なん?って思うかもしれませんが、こんな感じでいいでしょ?(だってロクなことしてないし)
というわけで、大切な時間を無駄にしていただきありがとうございました。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?