銀月の符号

Python 使い見習いの日記・雑記

東方星蓮船で遊ぶ

東方星蓮船』の曲データを Python 言語で取り出してみる、という遊びの紹介。

用意するもの

  • 東方星蓮船 〜 Undefined Fantastic Object.
  • thbgm.dat のどこからどこまでが曲なのかという情報
  • Python

東方星蓮船は同人ショップから(体験版なら無料で DL 可能)、曲情報は 東方シリーズ音楽抜き出し機 1.4 のソースコードや曲目ファイルから、 Pythonhttp://www.python.org/ から得られる。

この遊び方の場合、ゲームパッドはなくても大丈夫。弾幕の耐性がなくても大丈夫。スペルカード好きのほうがより楽曲を楽しめるのはたしかだけれども。

手順

Python インタラクティブシェルを立ち上げる。

thbgm.dat における曲の部分を読み込む。たとえば 1 曲目『青空の影』ならば 0x10 バイト目から 0xF8040 バイト分がイントロ部、 0x909FC0 バイト分がループ部なのでこのような感じ。

>>> dat_path = ur'C:\Program Files\上海アリス幻樂団\東方星蓮船\thbgm.dat'
>>> f = open(dat_path, 'rb')
>>> f.seek(0x10)
>>> intro = f.read(0xf8040)
>>> loop = f.read(0x909fc0)
>>> f.close()

wave モジュールをインポートして、 WAV ファイルをオープンし、データを書き出す準備をする。

>>> import wave
>>> w = wave.open(u'th12_01.wav', 'wb')
>>> w.setnchannels(2)
>>> w.setsampwidth(2)
>>> w.setframerate(44100)

データを書き出す。たとえば 3 ループさせるなら次のようにする。 3 回入力する代わりに for i in range(3) とか、 for i in itertools.repeat(None, 3) を使っても可。終わったらファイルをクローズする。

>>> w.writeframes(intro)
>>> w.writeframes(loop)
>>> w.writeframes(loop)
>>> w.writeframes(loop)
>>> w.close()

以上。たったこれだけで曲が WAV データとして取り出せる。

この先

インタラクティブシェルで音楽を取り出してみた。できた。やった!」 で終わらせずに、ライブラリとして育てていくために。

東方シリーズ音楽抜き出し機の曲目ファイルを読んでみる

書式が簡単なので正規表現で十分読める。たとえば、こんな感じ。 thbgm.dat における位置だけでなく、曲名や作品名、頒布年月日、 MD5 ハッシュ値などもとれる。

    re_hex = re.compile(ur'[\dABCDEFabcdef]+')
    re_music = re.compile(
            ur'''^
            (?P<start>{0.pattern}),
            (?P<pre>{0.pattern}),
            (?P<main>{0.pattern}),
            (?P<title>.+)
            $'''.format(re_hex),
            re.DOTALL | re.VERBOSE)
    re_default_dat_path = re.compile(
            ur'^@(?P<default_dat_path>[^,]+)(,(?P<product>[^,]+))?,(?P<title>[^,]+)$',
            re.DOTALL)
    re_product_info = re.compile(ur'^#=ProductInfo,(?P<product_info>.*)$',
            re.DOTALL)
WAV データを加工してみる(フェードアウト処理など)

「ステレオ = 2ch」の WAV には L0, R0, L1, R1, L2, R2 ... という感じで左右の音データが交互に収まっている。 1 サンプルは「16 ビット = 2 バイト」なので L0, R0 はともに 2 バイト。「44.1KHz」なので 1 秒あたり 44100 サンプル、 176400 バイト。

しかしファイルから読み出した直後のデータは当然 8 ビット文字列データなので、少し扱いにくい。 array.array を使うと数値の列に変換できる。 'h' で signed short の配列になる。たぶん 2 バイト長だけど心配だったら itemsize 属性で確認できる。

>>> dat_path = ur'C:\Program Files\上海アリス幻樂団\東方星蓮船\thbgm.dat'
>>> f = open(dat_path, 'rb')
>>> f.seek(0x10)
>>> intro = f.read(0xf8040)
>>> f.close()
>>>
>>> from array import array
>>> len(intro)
1015872
>>> intro = array('h', intro)
>>> len(intro)
507936
>>> intro.itemsize
2
>>>
>>> intro[:8]
array('h', [0, 0, 0, 0, 0, 0, 0, 0])
>>> intro[100000:100008]
array('h', [-648, 6340, -1686, 5244, -2318, 4166, -2416, 3924])

数値列になったら、1.0 から 0.0 に向かって小さくなっていく値を作る関数をつくって任意の要素に乗算していけばフェードアウト処理ができる。

WAV ファイルに書き出すため 8 ビット文字列データに戻したい時は tostring メソッドを使えばよい。

>>> intro = intro.tostring()
圧縮形式にする(mp3 とか ogg とか)

subprocess モジュールを使うと外部プログラムを呼び出せる。また ctypes モジュールを使うとライブラリを呼び出せる。あとはお気に入りの形式に変換するプログラム、ライブラリをもってくるのみ。

たとえば、 Ogg Vorbis (oggenc2) 。

>>> import subprocess
>>> subprocess.call(['oggenc2', '-q', '4', 'th12_01.wav'])
Opening with wav module: WAV file reader
Encoding "th12_01.wav" to
         "th12_01.ogg"
at quality 4.00
        [ 99.8%] [ 0m00s remaining] \

Done encoding file "th12_01.ogg"

        File length:  1m 58.0s
        Elapsed time: 0m 07.0s
        Rate:         16.8886
        Average bitrate: 136.0 kb/s

0

たとえば、 TTA (ttaenc.exe) 。

>>> subprocess.call(['ttaenc', '-e', 'th12_01.wav'])
TTA1 lossless audio encoder/decoder, release 3.4.1
Copyright (c) 2007 Aleksander Djuric. All rights reserved.
For more information see http://tta.sourceforge.net
------------------------------------------------------------
File:   [th12_01.wav]
Encode: complete, wrote 15048463 bytes, ratio: 0.72, time: 4
------------------------------------------------------------
Total:  [1/1, 14.4/19.9 Mb], ratio: 0.722, time: 0'04
------------------------------------------------------------

0

やっぱり普通に遊ぶ

東方は人間を操作し、ショットで敵を倒す巫女さんシューティングゲームであり、 Python で音楽を抜き出すゲームではないので、正規の方法で遊んでみた。 Easy は 2 プレイ目でクリアできたけれど Normal は 5 プレイしてもまだクリアできてなかったり(稼ぎ練習のぞく、なりふりかまわずクリアしにいったのが 5 回)。腕の錆び付きっぷりにがっくしきてる。