リズムゲームを作るのに必要っぽいこと(準備編)
この記事はDark - Developers at Real Kommunity Advent Calendar 2015の22日目です。
寝るまではまだ22日目なんだ…!
って書いてからその認識さえ一日遅れていることに気づきました。本当です。
このカレンダーも終わりが近づいて来ましたね…!
みなさん、音ゲーやってますか? 僕は某フェスと某ステージのイベントがたまに被ったりして大忙しです。
こういったリズムゲームをやっていると、たまに以下のような不満を感じることがあります。
- 好きな曲なのにリズムパターンやノートの置き方が気に食わなくていまいち盛り上がれない
- もう少し難しくてもいいのにと思う
- 逆に難しすぎだろアホと思う
ざっくり言ってしまえば、「なんかリズムが気に入らないな」という話。
リズムゲームのパターンの作り方はゲームによっても曲によっても様々で、分かりやすい所ではパターンがドラムスに寄っているか旋律に寄っているか、そのゲーム特有の特殊な打ち方(押し続けやフリックなど)をどの程度用いるかというような違いがあります。
このパターンが自分の好みに合わない場合、とくに好きな曲をプレイしていると、とても残念な気持ちになってしまうことがあります。
自分で作る
もしそんな不満を抱えているあなたがエンジニアなら、気に入らないものは自分で作ってしまいましょう!
この記事では、リズムゲームを作るのに必要そうな技術的課題以外の、準備の部分を中心に取り組んでみました。
もちろん僕はリズムゲームを作るプロではないので、本場の方法と大きく異なる部分があるかもしれませんんが、一応そろえれば作れるよ、というものとして書いています。
この記事を見てもおもしろくなさそうな方:
- 今あるリズムゲームに特に不満がないか、リズムゲームに興味のない方
- 既にリズムゲームを自分で作れる、仕事で作っているという方 (→ 生暖かい目で見守ってください)
- DTMをやっているエンジニア (→ たぶんすべて知っている内容です)
- リズムゲームの演出とか運用とかについて知りたい方 (→ 僕も知らないっす)
こんな手順になります。
音源にあわせてリズムパターンをつくり、最終的に音源の再生時間基準でどこをタップするか、みたいな情報を含んだデータに落としこむのが基本的な流れ。
何かアプリケーションを作るところまでできれば良かったんですが、打ち込みに3時間くらいかかって力尽きました……
ちなみにこのうち、*
の付いている手順は、少なくともサービスとしてまっとうな方法でリズムゲームを作る場合必要のない手順です。
1. 音源を用意する
何はなくとも、曲の音源です。
今あるリズムゲームに不満がある方は、実際のリズムゲームから吸い出すのもよいかもしれません。 某ステージには自分のプレイ音が混じらないMVというモードがあるので余裕ですね! (こうして吸いだした音源を他人に頒布したり自分で楽しむ以外の諸々の用途に供するのはいけないことなのでやめましょう!!あと曲を聞きたいなら買おう)
自分で用意する場合も、1曲のプレイ時間として適当な(あと打ち込みで死なない)長さに気を付けます。
2. DAW的なものを用意する
DAW = Digital Audio Workstation
です。
実際のところ、MIDIと音声ファイル等を同時に読込み・編集・書出しできるものならなんでもOKですが、こういうのはDAW
で検索するのが一番でてきやすいです。
ちなみにMIDIは今のところ、いつどんな音が鳴るかを書き込める電子的な楽譜みたいなものと思っておけばとりあえずよいでしょう。
今はフリーでもちゃんと使えるものがたくさん出ていて最高ですね…昔の無料シーケンサーなんて...うっ‥頭が…
今回はTracktionを使いました。少し昔のバージョン(Tracktion 4)が無償で提供されています。
3. 曲のテンポを計測する
今どきの音楽でDAW的なものを介さずに制作されているものは恐らくないと思われ、その場合、打ち込みの部分と生演奏・録音の同期をとるために、必ず曲全体が精確なテンポに従ったタイムラインの上で管理されているはずです。
しかしながら、今回のようにひとの曲を勝手に使ってどうこうしようという場合、自力でテンポを計測して知る必要があります。
今回使ったのはこちら。非常に典型的なBPM(= beats per minute)測定アプリで、テンポに従ってタップすることでタップ間隔の平均的なテンポを知ることができます。
曲を聞きながら、ひたすら無心でタップしましょう。
リズムゲームで培った精確なタップ力が試されますね。
4. 音源の位置合わせをする
DAWを起動して新しいプロジェクトを作り、計測したテンポを設定したら、用意した曲の音源を適当なトラックにロードします。
音源の内容がトラックにべーっと貼り付けられたら、音源の最初の表拍などをDAWのタイムラインの拍と合うように、音声トラック上を移動します。
プロジェクト全体を再生しながら、録音用のシグナル音みたいなものがあればその音と、なければ適当な音をMIDIトラックに打ち込んで、DAWの拍と音楽の拍が合うようにします。
当然ですが、テンポが合っていない場合は曲と他の音がだんだんズレてきます。 そうでない場合は初めから終わりまで同じ時間のズレが維持されてかなり判定が難しいですが、頑張って心の耳で合わせます。
5. リズムパターンを打ち込む
いよいよリズムパターンの打ち込みです。
自分の魂のビートを打ち込むことで、リズムゲームでしっくり来なかった鬱憤を存分に晴らします。
ただし、この作業は恐ろしく時間がかかります…
ピアノロール(画像)というのはおおよそ人間の使用に耐えるものではなく、プロの人は電子楽器等で演奏したものを自動補正することで打ち込むため、今後もまともな進化は期待できない[要出典]UIです。
気に食わないリズムパターンを叩くよりここでよっぽどフラストレーションがたまっている気がしますが、まあ気にしないことにします。
タイミングのデータを作っているだけなので楽器を選ぶ意味はありませんが、ここでトラックや音の高さなど打ち分けておくと、後 の処理で参照して何かの目印に使うことができます。
6. MIDIファイルと音源を吐き出す
打ち込みが終わったら、打ち込んだリズムパターンの部分をMIDIファイルにして出力します。
また別に、打ち込んだパターンをミュートにして音源の部分も出力します。これによって、位置合わせした分の空白が反映された新しい音源を作ります。
7. MIDIファイルの中身を変換する
ここに来てやっとエンジニアらしい作業です。
MIDIファイルから情報を抜き出し、曲の音声ファイルの特定の再生時間でどこをタップしてください、みたいなデータを作ります。
MIDIファイルを扱うためのライブラリはきっと言語ごとに一つくらいはあると思うので、そういうのを使います。今回はPython製の以下を使いました。
こんな感じでダイレクトにファイルを読むインターフェイスを用意してくれているので、すぐに本来の作業に取りかかれますね。
import iomidi song = iomidi.read('midifile.mid')
MIDIの構造
MIDIに変換をかますには、MIDIファイルの構造を知る必要があるでしょう。
iomidi
のオブジェクトのプロパティを参考にざっくり見てみると、以下のようになっています(もちろん実際はJSONではありません)。
// song { header: { trackCount: 5, division: 960, frmt: 1 }, tracks: [ trackObject, trackobject, ... ] }
ファイル全体の情報が書かれたヘッダに、独立したトラックがいくつか含まれていることがわかります。
ヘッダで重要なのはdivision
です。ファイルの中の時間は全てtick
と呼ばれる単位の整数倍で表されていますが、このtick
の分解能を表すのがこの値で、tick
が1秒の何分の一かを示しています。
ここでは960なので、おおよそ1tickは1msくらいになります。
トラックは先に打ち込みで作った数+1だけ存在するはず(トラック0はなんかシステム的なやつが入っています)。トラックの中身を見てみます。
// song.tracks[1] { events: [ eventObject, eventObject, ... ] }
うん、もっと中を見ろってことですね。
event
の中身はこんな感じです。
// song.tracks[0][N] { NoteOnEvent: { velocity: 96, channel: 0, key: 50, delta: 1650 } }
案外シンプル(?)ですね。
理解しやすいものの定義はこんな感じです。
key
: 打ち込みで入れた音の高さ。ドラムとかにもちゃんとついている。
ごく普通の音を鳴らすイベントは、基本的に音を鳴らし始めるNoteOnEvent
と鳴らし終えるNoteOffEvent
の合わせ技でできていて、同じkey
でNoteOffEvent
が発生すると前回のNoteOnEvent
の音が止まる感じになります。
velocity
: 速さ…ではなく音の強さを言います。今回の処理には関係なし。
channel
: MIDIのしくみ系の値。今回の処理には関係なし。
delta
について
MIDIはそもそも、電子楽器などの演奏データを楽器や機器の間でリアルタイムに交換するための信号の規格で、MIDIファイルの中身も少し変わった形で記述されています。
delta
はその事情を反映した値になっている気がします。
delta
は、そのトラックの前回のイベントから自分までの時間差をtick
で表した値です。
今回欲しいのは音源のどの再生時刻にタップが入るかなので、トラックの最初のdelta
から順次足し合わせる必要があります。
なんかこんな雰囲気ですね。
def to_tap_sequence(midi_track): accumulated_time_ticks = 0 for event in midi_track.events: accumulated_time_ticks += event.delta yield dict( tap_position=select_position(event), time_ticks=accumulated_time_ticks)
このdelta
さえなんとかすれば、いい感じのデータが作れそうな感じがします。
必要に応じて、NoteOnEvent
だけを選出する必要はあるかもしれません。
あとは時間単位をミリ秒等になおして、
import decimal def tap_sequence_with_ms(seq, midi_header): for event in seq: event['time_ms'] = ticks_to_ms( event['time_ticks'], midi_header.division) yield event def ticks_to_ms(ticks, time_division): return float( # a tick as milliseconds 1 / decimal.Decimal(time_division) * 1000 # multiplied by #ticks * ticks)
必要に応じてトラックのイベントをマージすれば、
def merge_tap_sequences(seq_a, seq_b): # 効率悪.. rev_seq_a = list(reversed(list(seq_a))) rev_seq_b = list(reversed(list(seq_b))) while rev_seq_a and rev_seq_b: if rev_seq_a[-1]['time_ticks'] <= rev_seq_b[-1]['time_ticks']: yield rev_seq_a.pop() else: yield rev_seq_b.pop() while rev_seq_a: yield rev_seq_a.pop() while rev_seq_b: yield rev_seq_b.pop()
なんかできた気がする!
こちらがここまでで作ったものになります。
一応CLIが付いていて、なんか結果を吐いて音源と比較してみるとどうもtime_ms
の時間がおかしい気がするんですが、まあまだこの先を作っていないので仕方なし…とりあえずデータの作り方のコンセプトは合っているはずです。
あとは、作りたいゲームの特性に応じて情報を作ってイベントにくっつけくっつけしていけば、データは完成するのではないでしょうか(適当)。
さあ、ゲームらしきものを作る段には全然入ってませんが、ここまでのデータがそろっていれば、エンジニアならあとは何となく作れる気がして来たのではないでしょうか(自分も年末にやり切りたいです…いつかゲーム実装編が書かれる…かどうかはわからない)。
音楽ゲーム系のアプリケーションは何となく難しそうなイメージがありますが、本当に難しいのは気持ちよくプレイするためのタイムラグとか処理落ちをなくすとかの部分で、DAWをさわれてMIDIの知識をつければ、案外その前までは作れてしまうものです。
これを読んで頂けたエンジニアの人は、興味があったら是非作ってみて下さい!
そしてできたら僕にも遊ばせてください!
次は@sinamon129です!
あれ…もう書かれているぞ…?!おかしいな…(おかしくない)