かつかれーのメモ帳

実験ノートか勉強記録

beatmania IIDX INFINITASを自作コントローラーで遊ぶ

IIDX自作コントローラー記事のソフト編(その2)です。
beatmania IIDX INFINITAS(beatmania IIDXのPC移植版。以下、略してINFと呼ぶ。)をプレーできる自作コントローラー仕様について調べてまとめました。

使えるコントローラーについて

とりあえず、INFの公式サイトはこちら
動作環境のページにもあるように、INFにおいてUSB HIDゲームパッドとしては公式コントローラーのみ動作することになっています。サードパーティーの作成したUSB HIDゲームパッド(有名どころだとDAOコンとか)の動作はサポートされておらず、JoyToKeyなどによってキーボード入力に変換して使用するのが一般的です*1。キーボードによる操作はサポートされているためです。
まあアプリをいちいち立ち上げて変換するのが面倒だったり、変換の際の遅延が心配だったり、なによりキーボードには出来ない動作が仕様上存在していて*2不便だったりといった問題があるため、今回は公式コン準拠の動作をするゲームパッドを作っていきましょう。今のところ発売されている公式コンは

の2種類。今回はフル機能を有するプレミアムモデルの再現をしていきます。
公式コンは、つないでゲームを起動するだけで普通に使えるみたいです。一方、ほかのコントローラーは一切反応しません。やはりここにも何かしらの認証プロセスが存在しているようです。

コントローラーが満たすべき仕様

いきなり結論なのですが、INFはコントローラーのベンダーID/プロダクトIDしか見ていません。これらの値が正しくてHIDレポートが同じ形式になっていれば、とりあえず動きます。細かいところは違っていて大丈夫です。
ベンダーIDは公開情報なのでweb上のデータベースを引けばよくて、例えば以下のようなサイトを使うことで0x1CCFだとわかります。
USB ID Database::Vendor ID and Product ID list - the sz development
プロダクトIDは某掲示板で0x1CCFを手掛かりにちょっと調べると分かります。あるいは、DJ DAOの配布しているファームウエアをエディタで開き、0x1CCFに隣接する2バイトを読んできても良いです*3
f:id:DSKK:20200529184459p:plain
まあ、見ての通りです。リトルエンディアン(上位バイトが後)であることに注意。CF1C→1CCFと同じように、4880を2文字+2文字に区切ってからひっくり返して読んでください。
コントローラー仕様はDJ DAOが詳細に公開しちゃってます。怒られないのかな…
youtu.be
まず、デバイス仕様は「2軸16ボタン」ですね。
キーアサインは

1鍵: 1
2鍵: 2
3鍵: 3
4鍵: 4
5鍵: 5
6鍵: 6
7鍵: 7
E1:  9
E2: 10
E3: 11
E4: 12
ターンテーブル: X軸循環

のようになっています。
各種ディスクリプターとして用いることができるものの例です。

/* Device Descriptor */
    18,                // bLength
    1,                 // bDescriptorType
    0x00, 0x02,        // bcdUSB
    0,                 // bDeviceClass
    0,                 // bDeviceSubClass
    0,                 // bDeviceProtocol
    64,                // bMaxPacketSize0
    0xcf,              // idVendor[0]
    0x1c,              // idVendor[1]
    0x48,              // idProduct[0]
    0x80,              // idProduct[1]
    0x00, 0x10,        // bcdDevice (=version number)
    1,                 // iManufacturer
    2,                 // iProduct
    0,                 // iSerialNumber
    1                  // bNumConfigurations

/* Configuration Descriptor */
    9,                              // bLength;
    2,                              // bDescriptorType;
    34,
    0,                              // wTotalLength
    1,                              // bNumInterfaces
    1,                              // bConfigurationValue
    0,                              // iConfiguration
    0x80,                           // bmAttributes
    50,                             // bMaxPower
    // interface descriptor
    9,                              // bLength
    4,                              // bDescriptorType
    0,                              // bInterfaceNumber
    0,                              // bAlternateSetting
    1,                              // bNumEndpoints (is 2 if the device has INTR_IN)
    0x03,                           // bInterfaceClass (0x03 = HID)
    0x00,                           // bInterfaceSubClass (0x00 = No Boot)
    0x00,                           // bInterfaceProtocol (0x00 = No Protocol)
    0,                              // iInterface
    // HID interface descriptor
    9,                              // bLength
    0x21,                           // bDescriptorType
    0x11, 0x01,                     // bcdHID
    0,                              // bCountryCode
    1,                              // bNumDescriptors
    0x22,                           // bDescriptorType
    50,
    0,                              // wDescriptorLength
    // endpoint descriptor
    7,                              // bLength
    5,                              // bDescriptorType
    1 | 0x80,                       // bEndpointAddress (|0x80=in)
    0x03,                           // bmAttributes (0x03=intr)
    64, 0,                          // wMaxPacketSize
    4                               // bInterval

/* Report Descriptor */
    0x05, 0x01,        // Usage Page (Generic Desktop Ctrls)
    0x09, 0x04,        // Usage (Joystick)
    0xA1, 0x01,        // Collection (Application)
    0x09, 0x01,        //   Usage (Pointer)
    0xA1, 0x00,        //   Collection (Physical)
    0x05, 0x01,        //     Usage Page (Generic Desktop Ctrls)
    0x09, 0x30,        //     Usage (X)
    0x09, 0x31,        //     Usage (Y)
    0x15, 0x81,        //     Logical Minimum (-127)
    0x25, 0x7F,        //     Logical Maximum (127)
    0x75, 0x08,        //     Report Size (8)
    0x95, 0x02,        //     Report Count (2)
    0x81, 0x02,        //     Input (Data,Var,Abs)
    0x05, 0x09,        //     Usage Page (Button)
    0x19, 0x01,        //     Usage Minimum (0x01)
    0x29, 0x10,        //     Usage Maximum (0x10)
    0x15, 0x00,        //     Logical Minimum (0)
    0x25, 0x01,        //     Logical Maximum (1)
    0x75, 0x01,        //     Report Size (1)
    0x95, 0x10,        //     Report Count (16)
    0x81, 0x02,        //     Input (Data,Var,Abs)
    0x75, 0x08,        //     Report Size (8)
    0x95, 0x01,        //     Report Count (1)
    0x81, 0x03,        //     Input (Const,Var,Abs)
    0xC0,              //   End Collection
    0xC0               // End Collection

あとは実装するだけでちゃんと公式コントローラーとして認識されます。なお、レポートディスクリプタにもある通りHIDレポートは5バイトで、

1: X軸(signed char)
2: Y軸(signed char)
3: ボタン(1~7鍵盤。順にbit0~6に対応)
4: ボタン(E1~4。順にbit0~3に対応)
5: 定数(0x00)

という構造になっています。

f:id:DSKK:20200604094055p:plain
動作OK!右下のランプが公式コントローラーとして認識していることを示しています。
次回は実装の解説をやっていきます。

まとめ

PS3への対応と比べてずいぶん簡単なコントローラー仕様解析でした。デバイスディスクリプタの時点で異なる2つのファームウエアを同じボード上に共存させる方法について、次回の記事でソースコードを具体的に示しながら解説していきます。ちょっとだけテクいのでお楽しみに(?)
下のリンクからシェアやツイートなどしてもらえると大変執筆モチベが上がりありがたいです。良かったらぜひ。何か気になる点があったらコメントか
しありす (@cialis438) | Twitter
へのリプをお気軽にどうぞ。感想やコントローラー自作に関する雑談・質問なども歓迎です。

*1:ただし、DAOコンは後述するのと同様のやり方で公式コンとして認識されるようにしています。

*2:ファンクションボタンを使った、選曲時の操作

*3:コントローラーのファームウエアはデバイスディスクリプタを丸々含んでいるはずですよね。ここで、ベンダーIDとプロダクトIDは連続するバイト列なので…という発想。