Let's hoot !!

ここに書いてある情報が全て正しいとは限りません。

独自に解析したものです。間違いなどあればご連絡頂けるとありがたいです。


hoot搭載のX68k標準ドライバ仕様

hoot搭載のX68k標準ドライバ(以降 VM:仮想マシン)の仕様です。

ソースアーカイブの/drivers/x68k.cpp(Fu-.様作)を眺めてなんとなく理解したつもり。


VMのメモリマップ

一例としてFu-.様提供のデータ(悪魔城ドラキュラX68k)を参照してみました。

        offset       :size:  value    (comment)
---------------------:----:----------
                                       仮想ROM領域先頭
                                       実際は当然RAMなので書きこみ可能です。
---------------------:----:----------
$00000000            :LONG: $00F0FFFE (起動時のスタックポインタ)
---------------------:----:----------
$00000004            :LONG: $00001010 (起動時のPC)
                                       リセット処理ルーチンのアドレス
---------------------:----:----------
$00000008〜$00000074 :LONG: $00001000 (例外処理ベクタ(共通))
                                       VMでは一般の例外の発生=異常事態なので
                                       無限ループにジャンプします。
---------------------:----:----------
$00000078            :LONG: $00001062 (IRQ-6)
                                       VMのOPM割り込みベクタ。
                                       VMはMFPが未実装なので$0000010Cは使用されません。
---------------------:----:----------
$0000007C〜$000000B8 :LONG: $00001000 (例外処理ベクタ(共通))
---------------------:----:----------
$000000BC            :LONG: $0000107E (trap #$0F処理ベクタ)
                                       VMのIOCS用です(ADPCM周りで利用します)。
---------------------:----:----------
$000000C0〜$000000FC :LONG: $00001000 (例外処理ベクタ(共通))
---------------------:----:----------
$00000100〜$000003FF :BYTE: $00       (未使用)
                                       実機ではX68k内の各デバイスが使用する
                                       割り込みベクタです。
---------------------:----:----------
                                       実機では$00000400以降はIOCSのベクタです。
---------------------:----:----------
$00000400            :LONG: $000????? (音源ドライバ初期化ルーチンのアドレス)
                                       ワークの初期化などを行う。
---------------------:----:----------
$00000404            :LONG: $000????? (音源デバイス初期化ルーチンのアドレス)
                                       OPMのTIMERの設定など。
---------------------:----:----------
$00000408            :LONG: $000????? (サウンドコード発行ルーチンのアドレス)
                                       サウンドコード(曲や効果音の番号)のやり取り。
---------------------:----:----------
$0000040C            :LONG: $000????? (音源ドライバ内割り込みルーチンのアドレス)
                                      (注) rte ではなく rts で戻る必要があります。
                                           或いはIRQ-6のベクタにドライバ内部の
                                           割り込みルーチンを直に設定します。
---------------------:----:----------
$00000410〜$000007FF :BYTE: $00       (未使用)
---------------------:----:----------
                                       実機では$00000800以降はIOCSのワークです。
---------------------:----:----------
$00000800            :LONG: $000????? (ダンプアドレス)
                                       推測ですが音源デバイスの
                                       レジスタ設定データの保存先だと思われます。
---------------------:----:----------
$00000804〜$0000087F :BYTE: $00       (未使用)
---------------------:----:----------
$00000880〜$00000FFF?:BYTE: ?         (ID文字列)
                                       ファイルをダンプした時の識別ぐらいにしか
                                       用途が思い浮かびません。
---------------------:----:----------
$00001000〜          :WORD:           (無限ループルーチン)
                                       一般の例外が発生すると実行されるルーチンです。
                                       当然ですがここに来るようでは問題ありです。

                           nop
                           nop
                           nop
                           nop
                           nop
                           nop
                           nop
                           bra.s    -$10(pc)        ;to $00001000

---------------------:----:----------
$00001010〜          :WORD:           (VM起動ルーチン)
                                       リセット時にここから実行されます。
                                       典型的なm68000の起動時の処理といえます。

                           move.w   #$2700,sr
                           reset
                           clr.l    d0
                           clr.l    d1
                           clr.l    d2
                           clr.l    d3
                           clr.l    d4
                           clr.l    d5
                           clr.l    d6
                           clr.l    d7
                           movea.l  d0,a0
                           movea.l  d0,a1
                           movea.l  d0,a2
                           movea.l  d0,a3
                           movea.l  d0,a4
                           movea.l  d0,a5
                           movea.l  d0,a6

---------------------:----:----------
$00001034〜          :WORD:           (VM初期化ルーチン)
                                       音源ドライバのワークと
                                       VMの音源デバイスを初期化します。

                           movea.l  $0400.w,a6
                           jsr      (a6)
                           movea.l  $0404.w,a6
                           jsr      (a6)
                           move.w   #$2500,sr

---------------------:----:----------
$00001044〜          :WORD:           (VMメインループ)
                                       hoot本体から受け取るサウンドコードを
                                       pollingする部分です。

                           tst.b    $00E00000.l
                           bne.s    $08(pc)         ;to $00001054
                           tst.b    $00E00800.l
                           bra.s    -$10(pc)        ;to $00001044

---------------------:----:----------
$00001054〜          :WORD:           (VMサウンドコード発行)
                                       hoot本体から受け取ったサウンドコードを
                                       音源ドライバに渡してからメインループに戻ります。

                           clr.b    $00E00000.l
                           movea.l  $0408.w,a6
                           jsr      (a6)
                           bra.s    -$1E(pc)        ;to $00001044

---------------------:----:----------
$00001062〜          :WORD:           (VMのIRQ-6(OPM割り込み)ルーチン)
                                       音源ドライバ内部の演奏ルーチンを実行します。

                           move.w   #$2700,sr
                           movem.l  d0-d7/a0-a6,-(sp)
                           movea.l  $040C.w,a6
                           jsr      (a6)
                           movem.l  (sp)+,d0-d7/a0-a6
                           move.w   #$2500,sr
                           rte

---------------------:----:----------
$0000107A            :LONG: $00000000 (VMのIOCS用のワーク)
                                       IOCS _ADPCMSNSが返す値の格納場所です。

---------------------:----:----------
$0000107E〜          :WORD:           (VMのIOCSルーチン)
                                       _ADPCMOUT,_ADPCMSNS,_ADPCMMODしか
                                       実行しません。

                           cmpi.b   #$60,d0
                           beq.s    $12(pc)         ;to $00001096 (_ADPCMOUT)
                           cmpi.b   #$66,d0
                           beq.s    $7E(pc)         ;to $00001108 (_ADPCMSNS)
                           cmpi.b   #$67,d0
                           beq.w    $0088(pc)       ;to $00001118 (_ADPCMMOD)
                           moveq.l  #$FF,d0
                           rte

---------------------:----:----------
$00001096〜          :WORD:           (VMのIOCS _ADPCMOUT ルーチン)
                                       VM上のDMACでは転送終了割り込みなどが
                                       機能しないため、転送を開始した直後に
                                       _ADPCMSNSが返す値を0にしてしまいます。

                           move.w   sr,-(sp)
                           ori.w    #$0700,sr
                           move.l   #$00000002,$0000107A.l
                           move.b   #$32,$00E840C5.l
                           move.b   #$FF,$00E840C0.l
                           move.l   a1,$00E840CC.l
                           move.w   d2,$00E840CA.l
                           move.w   d1,d0
                           andi.b   #$03,d0
                           beq.s    $06(pc)         ;to $000010D0
                           cmpi.b   #$03,d0
                           bne.s    $04(pc)         ;to $000010D4
$000010D0                  eori.b   #$03,d0
$000010D4                  subi.w   #$0200,d1
                           lsr.w    #6,d1
                           or.b     d0,d1
                           move.b   $00E9A005.l,d0
                           andi.b   #$F0,d0
                           or.b     d1,d0
                           move.b   d0,$E9A005.l
                           move.b   #$88,$E840C7.l
                           move.b   #$02,$E92001.l
                           clr.l    $0000107A.l
                           move.w   (sp)+,sr
                           rte

---------------------:----:----------
$00001108〜          :WORD:           (VMのIOCS _ADPCMSNS ルーチン)
                                       上記_ADPCMOUTが割り込み禁止で動作して
                                       いるので_ADPCMSNSは実質0しか返しません。

                           move.w   sr,-(sp)
                           ori.w    #$0700,sr
                           move.l   $0000107A.l,d0
                           move.w   (sp)+,sr
                           rte

---------------------:----:----------
$00001118〜          :WORD:           (VMのIOCS _ADPCMMOD ルーチン)
                                       実質的には入力d0.lが0(再生終了)
                                       の場合しか処理されません。

                           move.w   sr,-(sp)
                           ori.w    #$0700,sr
                           tst.l    d0
                           bmi.s    $28(pc)         ;to $0000114A
                           cmpi.l   #$00000003,d0
                           bcc.s    $20(pc)         ;to $0000114A
                           tst.b    d0
                           beq.s    $04(pc)         ;to $00001132
                           moveq.l  #$00,d0
                           bra.s    $1A(pc)         ;to $0000114C
$00001132                  move.b   #$10,$00E840C7.l
                           st.b     $00E840C0.l
                           move.b   #$80,$E92003.l
                           bra.s    $02(pc)         ;to $0000114C
$0000114A                  moveq.l  #$FF,d0
$0000114C                  move.w   (sp)+,sr
                           rte

---------------------:----:----------
$00001150〜$0007FFFF :????:          (各ドライバ個別処理部分)
                                      VM上のフリーエリアといって良いでしょう。
                                      仮想ROMイメージサイズの上限は512KBです。
                                      (最近の版では増設されているかもしれません。)
---------------------:----:----------
                                       仮想ROM領域終端
---------------------:----:----------
$00080000〜$00DFFFFF :????:          (未実装領域)
                                       VM上ではアクセス禁止だと思われます。
                                       もしくは単に無視されるだけかも。
---------------------:----:----------
                                       実機では$00E00000〜$00E7FFFFは
                                       テキストVRAMです。
---------------------:----:----------
$00E00000            :BYTE: $??      (hootサウンドコード発行フラグ)
                                      hoot側から非0値が書きこまれることによって
                                      VM側にサウンドコード発行が通知されます。
                                      VM側は0を書きこむことでhoot側に
                                      サウンドコード受け取り完了を通知します。
---------------------:----:----------
$00E00001            :BYTE: $??      (hootサウンドコード値(下位8bit))
                                      hoot側が設定したサウンドコード値を
                                      VM側で読み取ります。
---------------------:----:----------
$00E00002            :BYTE: $??      (hootサウンドコード値(上位8bit))
                                      サウンドコード値が2bytesの時に使用されます。
---------------------:----:----------
$00E00003〜$00E007FF :BYTE:          (未使用領域)
---------------------:----:----------
$00E00800            :BYTE: $??      (エミュレータIDLE)
                                      推測ですがVMをIDLE状態にするために
                                      使用されるものと思われます。
---------------------:----:----------
$00E00801〜$00E840BF :BYTE:          (未使用領域)
---------------------:----:----------
                                      DMAC(HD63450)
                                      VM上ではDMACはCH3の出力のみ機能します。
---------------------:----:----------
$00E840C0            :BYTE: $??      (VM/DMAC:#3:CSR)
                                      チャンネルステータスレジスタです。
                                      読み込みはできません。
                                      書き込みのみ可能です。
---------------------:----:----------
$00E840C1〜$00E840C6 :BYTE:          (未使用)
                                      CER,DCR,OCR,SCRは設定できません。
                                      上記IOCSルーチンでは設定していますが
                                      VM上では固定値設定と思われます。
---------------------:----:----------
$00E840C7            :BYTE: $??      (VM/DMAC:#3:CCR)
                                      チャンネルコントロールレジスタです。
                                      STR(bit7)が'1'で転送開始です。
                                      SAB(bit4)が'1'で転送停止です。
                                      INT(bit3)は'1'にしても割り込みは発生しません。
---------------------:----:----------
$00E840C8            :BYTE:           空き
---------------------:----:----------
$00E840CA            :WORD: $??      (VM/DMAC:#3:MTC)
                                      メモリトランスファカウンタです。
                                      ADPCMのサイズを設定します。
                                      64KBが上限です。
---------------------:----:----------
$00E840CC            :LONG: $??      (VM/DMAC:#3:MAR)
                                      メモリアドレスレジスタです。
                                      ADPCM転送元アドレスを設定します。
---------------------:----:----------
$00E840D0            :LONG:           空き
---------------------:----:----------
$00E840D4            :LONG:           (未使用)
                                      DAR(デバイスアドレスレジスタ)は
                                      $00E92003に設定されていると考えます。
                                      実際にはVM上のエミュレーションは
                                      省略されているはずです。
---------------------:----:----------
$00E840D8〜$00E840FF :WorB:          (未使用)
                                      BTC,BAR,NIV,EIV,MFC,CPR,DFC,BFC,GCRは
                                      設定できません。
                                      VM上では固定値設定と思われます。
---------------------:----:----------
$00E84100〜$00E85FFF                  空き
---------------------:----:----------
                                      $00E86000〜は奇数番地のみ有効です。
---------------------:----:----------
$00E86001〜$00E8FFFF :BYTE:          (未使用領域)
---------------------:----:----------
                                      OPM(YM2151)
                                      レジスタ$1BのCT1(bit7)とCT2(bit6)は
                                      無効です。ADPCM(MSM6258V)の入力は
                                      8MHz固定かもしれません。
---------------------:----:----------
$00E90001            :BYTE:          (OPMレジスタ番号設定ポート)
---------------------:----:----------
$00E90003            :BYTE:          (OPMデータRead/Writeポート)
---------------------:----:----------
$00E90005〜$00E91FFF :BYTE:           空き
---------------------:----:----------
                                      ADPCM(MSM6258V)
---------------------:----:----------
$00E92001〜$00E92003 :BYTE:          (未実装)
                                      VM上からはR/Wともに無視されます。
                                      常に出力モードで動作していて
                                      DMACでの制御のみ行われるようです。
---------------------:----:----------
$00E92005〜$00E9A003 :BYTE:           空き
---------------------:----:----------
                                      PPI(i8255)
---------------------:----:----------
$00E9A005            :BYTE:          (SampringRate/PCM PAN)
                                      bit3〜2:10=15625Hz/01=10416.7Hz/00=7812.5Hz
                                      bit1〜0:11=MUTE/10=L/01=R/00=L+R
---------------------:----:----------
$00E9A007〜$00EFFFFF :BYTE:          (未使用領域)
---------------------:----:----------
                                       仮想RAM領域先頭
                                       実機ではCGROM領域です。
---------------------:----:----------
$00F00000〜$00F0FFFE :WORD:          (システムスタック領域)
                                      起動時にspには$00F0FFFEが設定されます。
---------------------:----:----------
                                       仮想RAM領域終端
---------------------:----:----------
$00F10000〜$00FFFFFF :BYTE:          (未使用領域)
---------------------:----:----------

本来ならTABLEで記述するものなんでしょうがソースコードも入っているということで……。


hoot 実践編

ここではMZLがこれまでに解析したタイトルについてのトピックを紹介します。

はっきりいってただの雑記ですが、自分でhoot用データを作成してみようという方の参考になれば幸いです。


Ys - III 解析記


動機(あるいは長い前置き)


きっかけは何気なく入手したromeoというFM音源カードでした。

MZLは特に耳が良いというわけでもなく、巷に出回っているFM音源エミュレータの音でも十分と思っている派でしたが、丁度その頃手持ちのX68k( 1)が2台ほぼ同時期に電源故障した( 2)こともあって、OPM(YM2151)が底面基板から手に入るな( 3)と軽く考えて、せっかくだから買ってしまえ、と衝動買い。

購入後しばらくはMXDRVgで手持ちの過去に集めたMDXデータを演奏させている程度でしたが、遊んでいるOPN3L(YMF288)が勿体無くて何か無いか探し回っているときに、hootの存在を知りました(遅)。

これで The Scheme ( 4) の音楽( 5)が演奏可能( 6)だということを知り、ディスクイメージをWin上に持って来て演奏データ作成と、ここまでは順調だったのですが……。

いざ演奏させてみると肝心のADPCMパートだけ鳴らない!!データ作成プログラムの付属テキストを調べても、hoot.xmlの該当部分をチェックしても状況は変わらず。remeoで演奏させているのが原因( 7)か?と思ってromeoのデバイスドライバを外しても駄目。

いよいよ困って本家の掲示板にて泣きついてみたところ、DMP SOFT様とUME-3様からの指摘によりファイルのリネームをミスしていること( 8)が判明。感謝感激雨あられでした。

Give & Takeを信条としているMZLとしては何か恩返しできないかと考え、自分に出来ることは未だデータ化されていないX68kのゲームタイトルをhootに対応させることだと思い至りました。

そこでゲームタイトルですが、未だデータ化されていないもので

  1. 音楽的に印象の良いもの(好みの問題が大)。
  2. 過去に吸出しMDX等が存在していないか、あるとしても再現性に問題のあるもの。( 9)
  3. 何らかの原因でhoot化が困難であるもの。(10)

であるものの中から、最初にYs - IIIを選択しました。理由は上記の条件で

  1. 「アレンジバージョンを超えてやろう」(11)という製作スタッフの意気込みを感じたこと。
  2. Yatsube様が過去に吸出したMDXが存在するが、氏が自ら再現性に問題がある旨を記していた(12)こと。
  3. OSがHuman68kではないオリジナルなもの(13)であるためファイルの抽出が困難であること。

を全て満たしていることでした。


解析開始


最初に取り掛かるものとしてはちょっと敷居が高いかな?とも思いましたがこうなったら後は勢いあるのみです。ここで解析に使用した環境を挙げておきます。

まず、Ys - IIIはX68030以前に発売されたタイトルであるため060turboでの動作は期待できません(15)から、検証はEX68で行う(16)ことにしました。

そこで、MO経由でREADFD.Xを実機に移し、ゲームディスクから2HDイメージを作成(17)したものを再度MO経由でWin上に移します。

上記にある通りこのディスクイメージはHuman68kからファイルが見えないのでイメージを丸ごと解析対象とします。ここでいきなりイメージをDIS.Xに食わせても良いのですが予め一工夫します。

今回の方針はWin上で出来ることはWin上で行う、というものです。まず、ディスクイメージファイルを「あふ」のファイル分割機能を用いてセクタ単位に分割して(18)おきます。

C COMPILER PRO 68k付属の「プログラマーズマニュアル」(19)p698〜p699の見開きによると、X68kのブート時の手順としてセクタ番号$0000の1セクタ分をメモリアドレス$00002000に読み込んで実行させていることが判ると思います。

要するにセクタ番号$0000の1024bytesが2HDのIPLプログラム本体な訳ですからここからプログラムを解析し始めることにします。ここでも一工夫します。まずIPL2HD.HASというファイル名(内容は以下)

        .text
        .even
        
        bra.w   IPL_START
        
        .ds.b   $2000-4
        
IPL_START:
        .end

をEX68のHuman68k上でアセンブル・リンク・コンバートしてIPL2HD.Rを作成します。このファイルと先ほど分割したファイルのセクタ番号$0000のものに「あふ」のファイル結合機能を適用してIPL_2HD.Rというファイルを作成します。

このIPL_2HD.RをDIS.Xに食わせることで比較的読みやすいソースファイルを得ることが出来ます。

ソースを読み進めていくうちに以下のことが判りました。

ゲームディスクを3枚とも確認したところどれもセクタ番号$0008からFATらしきものが見つかります。構造はC言語風にすると

typedef struct typeYs3Fat {
  char           aFileName[7]; //ファイル名文字列。
  char           dummy;        //0x4D
  unsigned short sFileOffset;  //値はほとんど0。リトルエンディアン
  unsigned short sFileSize;    //ファイルサイズ(byte)−1。リトルエンディアン
  unsigned short sTopCluster;  //開始クラスタ番号。リトルエンディアン
  unsigned short sEndCluster;  //終了クラスタ番号。リトルエンディアン
} YS3_FAT;

の様な感じ(20)でしょうか。ここまで判れば上等です。早速ファイル抽出プログラム(21)を作成してディスク3枚から全てのファイルを抽出しました。


BIOS68解析


次のターゲットは"BIOS68"のファイルです。今度もIPLのときと同様に

        .text
        .even
        
        bra.w   BIOS68_START
        
        .ds.b   $2400-4
        
BIOS68_START:
        .end

で作成したバイナリと結合させてから逆アセンブルします。が、これでも少々読み辛いソースになってしまいました。

FES.X搭載の逆アセンブラモードで"BIOS68"を調べると先頭で$10bytes分先に再度絶対番地ジャンプするようなコードだったので、こちらも一捻りして

        .text
        .even
        
        bra.w   BIOS68_START
        
        .ds.b   $2400-4+$10
        
BIOS68_START:
        .end

で作成したバイナリをBz上で後ろ$10bytes分削ったところから"BIOS68"と結合させてから再度逆アセンブル。今度はそれなりのソースになりました。

このソースを元に絶対番地指定の部分をラベルに置きかえる作業などをしながら再アセンブルとバイナリ比較と再逆アセンブル、の繰り返し(22)解析していきました。その結果、以下の項目

が明らかになりました。久しぶりのアセンブリコードの解析は困難を極めましたが(29)最終的には音源ドライバ部分のみを抽出(30)することが出来ました(31)


曲が足りない?


次に曲データを探します。ディスク3枚で合計31個の"???MUS"ファイル(エンディングらしき"EDMUS1"を含む)が発見されましたが、そのうち2ファイルは全て$FFで埋め尽くされたダミー(32)でした。

Yatsube様の吸出しMDXの曲数から見ても全然足りません。それに、基本的にファイルサイズが8KBとバイナリにしては大きすぎる(33)のも気になります。

仕方なく上記のBGM演奏開始BIOSコール$FF10の内容を詳しく追いかけます(最初からそうしとかんかい)。結果どうやら1ファイルに数曲アーカイブされていて(28)で不明だったワーク(メモリアドレス$00002406)にアーカイブ中の何曲目を演奏するかの指定があらかじめ与えられていることが判りました。

そこで、ようやくHuman68K上で動作する簡易演奏プログラム作成に取り掛かります。演奏開始前にBGMデータ領域にファイル部分を転送してからBIOSコール先を直接サブルーチンコール(34)します。

ワーク(メモリアドレス$00002406)の値を変えながら実行させては暴走(35)の繰り返しで、全BGMデータ(36)をHuman68k上で演奏できる様になりました。


hootで曲が鳴らない?


次にhoot用データの作成に取り掛かりました。手掛かりはダウンロードしたhootのX68k標準ドライバのソースと現時点で公開されているデータ群のみです。

データはADPCMをIOCSで制御しているものとしてFu-.様提供のデータ(悪魔城ドラキュラX68k)を解析対象としました。

hoot用のルーチンを先ほどの簡易演奏プログラム先頭に配置してサウンドコード受け渡し部分へのやり取りを追加して完成。と、思いきやhoot上ではウンともスンとも鳴りません。簡易演奏プログラムでは完璧なのにいったい何故?

Human68k上ではプログラムの途中にメッセージ表示をすることでプログラムの進行をチェックしたり(37)、デバッガを使用することで(割り込み関連ではあまり役に立ちませんが)どんな異常が発生しているかの見当はある程度はつくのですがhootのVM上ではそういう訳にもいきません。一体どうすれば良いのでしょうか?


必殺「Yコマンド(38)」デバッグ


何の反応も示さないhootの画面を眺めているうちにふと気が付きました。ちゃんとOPMのTimer-B関連のレジスタには書き込みが行われているのです。つまり、音源の初期設定ルーチンまでは正常に動作しているはずなのです。

このことから思いついたのは、OPMの未使用レジスタに直接何か値を書きこむこと(39)でhoot上での動作がある程度把握できるのではないかということです。

とりあえず、レジスタ番号2にプログラムの進行カウント値を設定し、レジスタ番号3には発生した例外のベクタ番号を設定する様に(40)細工しました。

これを実行してみると、どうも最初の割り込み処理の直後に未定義命令例外が発生していることが確認できました。そこで、割り込み処理の部分を追っていくと(以下抜粋)

$00001062〜          :WORD:           (VMのIRQ-6(OPM割り込み)ルーチン)
                                       音源ドライバ内部の演奏ルーチンを実行します。

                           move.w   #$2700,sr
                           movem.l  d0-d7/a0-a6,-(sp)
                           movea.l  $040C.w,a6
                           jsr      (a6)
                           movem.l  (sp)+,d0-d7/a0-a6
                           move.w   #$2500,sr
                           rte

実行時に$040C.wには"BIOS68"から抽出した音源ドライバのOPM割り込み処理アドレスが入っています。冷静に考えればなんと割り込み処理部分をサブルーチンコールしています!!これでは動作しないのも当たり前(41)でした。

早速音源ドライバのOPM割り込み処理終了部分のrteをrtsにパッチしてやるとようやく曲が鳴るようになりました。めでたしめでたし。つーかこんなミスだったなんて……。


終わりに


後は先ほどのファイル抽出プログラムにパッチ書き込み部分を追加してリッピングツールの完成(42)です。

結論から言ってメモリアドレス$00002400から配置される絶対番地プログラムというのがかえってhoot化するのには簡単だった様に思います。

後はこのパッチ部分のプログラムで何でも対応できる、とこのときは軽く考えていましたのですが……。


( 1)MZLは計7台X68kを所有し、現役で使用している大バカ野郎です。

( 2)壊れたのはXVI-HDとEXPERT-HDでした。合掌。

( 3)結局底面基板に手を付けるのが勿体無くなってOPM一式(YM2151+YM3012)は別途入手しましたが。

( 4)MZLは当時PC-88本体も持っていないのに音楽聴きたさに The Scheme を買って、パソコンショップに持ち込んで遊んでいた大バカ野郎です(笑)。アルミ弁当箱チックなパッケージ、青ディスク、赤ディスクを入れ替えた状態でも起動するのが今でも印象深いです。

( 5)MZLは1989年当時、オリジナルサウンドトラックをカセットテープとCDで買い、2002年に復刻されたCDも買った大バカ野郎です(笑)。ただ、収録されている"I'll save you all my justice"と"perpetual dark"(2曲とも大のお気に入り)のADPCMパートがゲーム中で演奏されるバージョンと異なっているのに違和感を覚えたものです。

( 6)実際、音楽を聴くために実機やM88でゲームをいちいちプレイするのはかなり辛いですよ。ちなみに"CHALLENGE A.V.G & R.P.G [IV]"のp352〜p353の見開きは手垢で真黄色になっています。

( 7)OPN3L(YMF288)はOPNA(YM2608)に存在したADPCM機能が省略されていて、hootではADPCM部分のみを音源エミュレーションで再生しています。

( 8)ADPCMデータの"V_A_"等はちゃんと"VA"の様にリネームしていたのですが、肝心のADPCMテーブル"ADR"が"ADR_"のままだったと言うオチ。

( 9)吸出しデータとはいえ音源ドライバの仕様で完全再現は不可能なものもあります。こういうもののためにhootが存在しているのです。

(10)失礼を承知で言えば、ぶっちゃけた話「hoot化が面倒くさい」もののことです。

(11)サウンドトラックCDにそのような記述があったことを覚えています。

(12)MDX付属のドキュメントファイルは"X68OOO Noo!!"で始まる形式でエンコードされていてそのままでは読めませんでしたが断片的にそれらしきことが書いてありました。また「翼を持った少年」のドラムパートが個人的に「違う」と感じたものです。

(13)他に具体的なものとしてEXACTの一連の作品やDEMPAのモトス以降の作品などが挙げられます。(DEMPAのYsもオリジナルOSですがHuman68k互換のFATからゲームファイルにアクセスできます。)

(14)クロックアップ耐性を上げようとあがいた名残です。結局12.5MHzに落ち着いていますが(EXPERT-HD現役時は20MHzでも起動した!!)。

(15)060モードでは効果音が鳴った時点で異音を発したままBGM再生が途切れ、マップ選択後のディスクアクセスを境にフリーズします。030モードではBGMと効果音の異常はあるものの動作はする様でしたが音源ドライバの動作検証用としては致命的です。

(16)サブマシン(ACE-HD)と行き来するのが面倒くさかったので。不具合などでどうしても実機が必要な場合には仕方なくサブの方で検証します。

(17)ベタイメージを作成するものであれば何でもかまわないです。実際DB.Xでも出来ます(手順は省略)。

(18)X68kの2HDはセクタ長1024bytesなので分割サイズを「1024」にします。「分割数が1000個を超えます 本気ですか?」と警告してきますが(笑)、怯まずに「はい(Y)」を選択します。

(19)X68kプログラマの「バイブル(その1)」です。持っていない方は代用として「ぷにぐらま〜ずまにゅある」をどうぞ。

(20)1クラスタ=1セクタ換算です。

(21)手抜きしてC言語でサックリ。後で配布用のリッピングツールとして使えるように少々の細工しておきます。

(22)DIS.Xが間違えて命令部分をデータとして解釈してしまった部分をソース上で命令コードに書き換えていきます。またDIS.Xが間違えてデータ部分を命令として解釈した場合、バイト操作命令の不定バイト部分が変化してしまうと再アセンブル時に元のバイナリに戻らなくなるので、該当部分をデータに展開していきます。実はこの作業中にOPMのPAN設定のワークを間違って書き換えてしまい、特定の曲であるパートが全然発音されなくなるというバグを誘発させてしばらく悩んだのを覚えています。

(23)$F???の1WORD分が実行されることによる例外処理。Human68kでは$FF??がDOS CALLに、$FE??が浮動小数点演算ドライバに使用されています。

(24)ソースコード内が何の脈絡も無いDOS CALLだらけでちょっと不気味でした(笑)。

(25)上記のOPM書き込みルーチン先頭のnop($4E71)をrts($4E75)に書き換えます(030以上での効果音の不具合はこれが原因の様子です)。PI.様のソーサリアン内部解析からもわかるようにFalcomの伝統芸のようです。ただ、効果音がBGMのテンポに左右されてそうな気配があるのは少し以外でした(実際効果音は未解析なので推測です)。

(26)BGMのファイルサイズはエンディングのものが最大で12KBあります。上記BGMデータ領域が8KBしかないのでエンディング時には効果音領域まで上書きして潰すことになっている様です。

(27)このBIOSでは1ファイルのサイズが64KB未満が上限の様です。ちなみに"PCMDT0"のディスク上の64KB目のデータは$00でした。

(28)パラメータ入力としてメモリアドレス$00002406(BYTE)が使用されているのですがこの時点ではまだ何のためのものか不明でした。

(29)解析がほぼ終了した頃、DMP SOFT.様からファイルの後半にシンボルテーブルがそのまま残っている旨を指摘されました。もっと早く気が付いていれば楽が出来たのに。

(30)解析中にプロテクトルーチンも見つかりました(笑)。ここを潰せばバックアップでも動作するのですが見なかったことにして音源ドライバ部分のみの抽出となった次第です。

(31)はっきりいってコード内容は思わず最適化したくなるぐらいの冗長さでした。割り込みルーチン先頭ではスーパーバイザモードに切り替わっているのにIOCS _B_SUPERで再度スーパーバイザモードに切り替えようとする(当然毎回失敗するわな)。メモリの先頭から32KB内に収まっているのにABS.Wを全く使用せずにABS.Lばかり。ショートブランチ(bra.s)で届く距離にjmp ABS.L使用。clrを使うところを#0書き込み。etc.etc.……。まあ、バグの無いプログラムでさえあればそれで良しなのかもしれません。

(32)元々はサントラに収録されていた曲(漆黒の魔獣:旧中ボス曲)が存在していたかと思うと無念です。

(33)MDXのサイズが大きくても大体4KB強。ほぼ倍のサイズはゲームのデータとしては大き過ぎやしないかと感じたものです。

(34)Human68k上では当然F系未実装命令例外処理はDOS CALLで使用されているのでこのプログラムから使うことは出来ません。

(35)アーカイブされている曲数以上の値を設定すると有らぬところにアクセスしてしまいますから当然ですね。

(36)Yatsube様の吸出しMDXに未収録の効果音扱いBGMも発掘出来ました。

(37)いわゆるprintf()デバッグなんかがこれに当たりますね。

(38)MMLのコマンドで音源レジスタに値を直に書きこむ命令です。よく知られる例ではKFレジスタの値を操作してCHデチューン効果(周波数ずらし)を与えたりOPM 8CHのノイズスロットを操作するなどがあります。MZLは初めてX68k上で作成したMMLにKFレジスタ操作のYコマンドをちりばめてポルタメントを実現していた大バカ野郎です。

(39)その昔、OPMDRV.XでのMMLデータ演奏にADPCMを同期再生させる常駐プログラムでOPMA.XやOPMZ.Xがありました。IOCS _OPMSETのベクタをフックすることでMML中の"y2,??"でADPCMの楽器の種類を、"y3,?"でADPCMのPANを操作可能にしていました。対応MMLファイルはYコマンドで溢れかえったものです。

(40)VMのメモリマップでの共通ベクタの部分を細工してベクタ番号を持って無限ループに飛び込み、無限ループ内でベクタ番号値をレジスタ番号3に書き込む様にしました。この部分はソースが見辛くなるのを嫌ってアセンブラのマクロ命令を使って展開する様にしました。

(41)例外処理時の情報をスタックに積んだままその内容をリターンアドレスとして戻るのですから……。くわばらくわばら。

(42)せっかくなのでHuman68k上での簡易演奏プログラムもついでに生成する様にしました。


Knight Arms 解析記


動機


Ys - IIIのデータを公開してからすぐに別の作品に取り掛かろうと思いました。今度は条件をちょっとゆるくして演習問題のつもりで軽くやってしまおうというハラ積もりだったのですが……。

で、候補に上がったのがKnight Armsでした。理由は

  1. ADPCM同期演奏モノに劣らない(むしろ勝っている)OPMを使い尽くしたBGM群。
  2. 自分でも過去に一度吸い出ししたこと[ 1]もあり、Yatsube様が過去に吸出したMDX[ 2]が存在するが、Arsys独特のLFO[ 3]が再現できていなかったこと。
  3. 音楽データファイルの構造がわかりやすく[ 4]解析も簡単なはずなのに、なぜ誰も手を出していないのだろうか?という疑問。

てな感じです。特に自分で過去に吸い出そうとして失敗していることへのリベンジ意識もあったかもしれません。サウンドトラックCDも全曲収録されていませんでしたし。何はともあれLet's try!!


解析開始


解析に使用した環境ですが1点だけ変更しました。

けろぴー(WinX68k)は過去に初期のバージョンを一度試したことはありましたがHDFからうまくブートできなくて放ったらかしにしていました。高速版のほうを最近になって試してみるとちゃんとブートしました……。深く考えないでおこう。

XM6はWINDRV未対応なので最近まで手を出していなかったのですが、VMのスケジュール管理の完璧さ、強力なデバッグ環境には捨てがたいものがあり、XDFを介してWinX68k高速版とやり取りすることを考えました。こうしてエミュレータプログラムを2個同時に駆動する[ 5]という一昔前では考えられない解析体制が整いました。

では早速解析開始です。以前(ゲームの発売当初)にも一度解析した経験があるので過去のことを思い出しながらの作業です。まずメインプログラムである"KNIGHT.X"の逆アセンブルを行い、得られたソースを整形します[ 6]。この作業によって得られた情報は以下の通り。

Ys - IIIの時のhoot用ルーチン+簡易演奏プログラムを流用改変して今回の基礎プログラムにしました。まず、"KNIGHT.X"の解析後のソースを先頭から64KBのオフセットに配置します[ 10]。空いた領域には丁度a6レジスタでポイントするワークと"MUSICS"ファイルが収まるぐらいの大きさだったのでここに配置することにします。

"KNIGHT.X"領域の後ろに"SHAON"と"EFFS"のファイルを配置しておしまい、とここまでは順調でした。


曲がならない?


いざ実行、と案の定曲がなりません。が、今回は暴走しているわけでもなくOPMのTimer-B割り込みも正常に動作している[11]様子です。しかし何時まで経ってもOPMの他のレジスタに値が書きこまれることはありません。

仕方が無いのでもう一度解析ソースとにらめっこ。よくよく調べるとOPMのTimer-Bの設定(レジスタ番号$13)に固定値を書きこんでいます。これでは音楽のテンポが変えられないので、どうもOPMとは別の割り込みでBGM演奏がされているに違いありません。

解析ソース中でOPM割り込みを設定しているサブルーチンを探してみると、同じ個所でMFPのTimer-D割り込みも設定していることが判明しました。試しにゲームをXM6で実行してMFPのレジスタを表示[12]してみると、BGMが変わる度にTimer-Dのデータレジスタの値が変わっています。MFPのTimer-D割り込みがBGM演奏のために使用されていた[13]のです。

ではMFPのTimer-D割り込みを設定すれば良いかというとそうはいきません。MFPのTimer-D割り込みはHuman68k上ではあまり利用したくないもの[14]ですし、そもそもhootのVM上にMFPは実装すらされていません。一体どうすれば良いのでしょうか?


16μs!!


MDXでLFOが再現できないのも、いままでKnight Armsがhoot化されていなかったのも、全てはこの2種類の割り込み処理が原因だったわけです。

「もうだめぽ……」XM6上で動作しているKnight Armsのデモを眺めながら半ば諦めかけていたその時、ふと思い立ってMFPのレジスタをもう一度表示させて見ました。

Timer-DのCountの部分に(16μs)と表示されています。どこかで見覚えのある値でした。あわてて「Inside X68000」[15]のOPMのTimer-A設定レジスタ[16]の説明図をみると

                  64×(1024−CLKA)
タイマ時間 TA = ───────── (ms)
                        4000

となっています。最小分解能を計算してみると0.016ms=16μs!!"Oh my pudding!!"[17]と叫んでしまいました。

これで方針は決定です。まず、OPM割り込み処理ルーチンを自前で用意してその先頭でOPMステータスレジスタのbit0とbit1のどちらが立っているか[18]によってドライバのOPMのTimer-BルーチンとMFPのTimer−Dルーチンを呼びわける用にします。

予めOPMのタイマ制御レジスタ(番号$14)はそれぞれのルーチンに入る前に以下のコード(抜粋)でオーバーフローフラグのリセットをかけることにします。

OPM_INT::
        bsr     OPM_INT0
        rte

OPM_INT0::
        movem.l d0-d2/a0-a1,-(sp)
        move.b  $00E90003.l,d2
        andi.b  #$03,d2
        lsl.b   #4,d2
        ori.b   #$0F,d2
        moveq.l #$14,d1
        bsr     OPM_WRITE       ;OPMレジスタ番号d1.bに値d2.bを書きこむサブルーチン
以下略

音源ドライバのOPMのTimer-B割り込みルーチンはレジスタ番号$14に書きこんでいる部分のコードをnopで潰し、割り込み終了時のrteもrtsにパッチします。

一方のMFPのTimer-D割り込みルーチンはTimer-Dデータレジスタに書きこむ値をOPMのTimer-A設定レジスタ($10〜$11)に与える値に変換して書き込み、それ以外のMFPレジスタに書きこんでいる部分のコードをnopで潰し、割り込み終了時のrteも忘れずにrtsにパッチします。

これでMFPのTimer-D割り込みをOPMのTimer-Aで代用することが可能になった訳です。


デッドロック!?


早速実行させて見ました。ちゃんと演奏されています。

……が、突然演奏が停止してしまいました。何度か試してみたところOPMのTimer-A側の割り込みが起動しなくなってしまっています。

散々調べたところ、片方のタイマのオーバーフローで割り込みが起動して、その処理をしている間にもう片方のタイマがオーバーフローした場合にこのような症状になる様子です。

いろいろ試した結果上記のコードを書きなおして以下のコード(抜粋)にすることで演奏が停止することは無くなりました。

OPM_INT::
        bsr     OPM_INT0
        rte

OPM_INT0::
        move.b  $E90003,OPM_STAT        ;とりあえず保存
        btst.b  #0,OPM_STAT             ;今現在Timer-Aがover flowしているか?
        beq     OPM_TIMER_B             ;0ならTimer-Bがover flowしているはず

OPM_TIMER_A::
        movem.l d0-d2/a0-a1,-(sp)
        moveq.l #$14,d1
        moveq.l #$1F,d2
        bsr     OPM_WRITE
        
        tst.w   KA_OPM_WK0
        beq     0f
        subq.w  #1,KA_OPM_WK0
        bra     1f
0:
        move.w  KA_OPM_WK1,KA_OPM_WK0
        tst.w   KA_B_WK
        bne     1f
        
        moveq.l #$4,d1
        move.b  OPM_INT_WORK_A(pc),d2
        bsr     OPM_WRITE
        jsr     KA_MFP_TIMER_D_INT
        addq.b  #1,OPM_INT_WORK_A
1:
        movem.l (sp)+,d0-d2/a0-a1
        
        btst.b  #1,OPM_STAT             ;割り込み時点でTimer-Bがover flowしていたか?
        bne     OPM_TIMER_B
        btst.b  #1,$E90003              ;今現在Timer-Bがover flowしているか?
        bne     OPM_TIMER_B
        
        rts

OPM_TIMER_B::
        movem.l d0-d2/a0-a1,-(sp)
        moveq.l #$14,d1
        moveq.l #$2F,d2
        bsr     OPM_WRITE
        moveq.l #$5,d1
        move.b  OPM_INT_WORK_B(pc),d2
        bsr     OPM_WRITE
        jsr     KA_OPMINT
        addq.b  #1,OPM_INT_WORK_B
        movem.l (sp)+,d0-d2/a0-a1
        btst.b  #0,$E90003              ;今現在Timer-Aがover flowしているか?
        bne     OPM_TIMER_A
        rts

OPMのTimer-A/Bの両方を同時に駆動するルーチン[19]を書いたのはこれが初めてでしたが、何とか動作するようになって一息、といったところです。

後はリッピング & パッチプログラムを作成して一丁あがり!!です。


大団円(笑)


今回はhoot化したものも一発で動作しました。ただし、ADPCMの再生終了部分で発生するプチノイズを除去することはどうしても不可能[20]だったので諦めました。

X68k実機のOPMとMFPの双方に4MHzのクロックが入力されていたこと、Arsysのドライバが16μsのモードを使用していたことなど見えない偶然にいろいろ助けられた気がします。


[ 1]移植対象ドライバがOPMDRV.Xでした。吸出し結果はソフトウェアLFOも再現できないお粗末なものでした。

[ 2]自分は持っていないのですがOtohime様もMDXに吸出しされていたようです。

[ 3]ソフトウェアLFOの大半は曲のテンポとLFO変化速度に相関が出来てしまうのですがArsysのLFO変化速度は曲のテンポに左右されないものだったのです。

[ 4]ゲーム用のデータとしては珍しいことにオリジナルのMMLで記述されています。音源ドライバにオリジナルのMMLインタープリタが内蔵されているというのはある意味すごいかも(OPMDRV.Xをドライバとして使用しているものは全部MMLだしそんなに感動する程ではないか……)。

[ 5]XM6は裏でWinX68k高速版が動作していると極端に動作が遅くなる(マルチメディアタイマの奪い合いに負ける?)のでXM6に切り替えるときはWinX68k高速版で逐一オプションのダイアログを出してVMの動作をとめます。こうすることでXM6が本来の動作をする様になります。

[ 6]上記Ys - III解析(22)を参照。

[ 7]MDXにも収録されていなく(当たり前か)、この曲名が不明だったので勝手に「Arsys Presents」とつけました。

[ 8]ファイル"STAGED"がデモを実行するオーバーレイのプログラムでした。このファイルをダンプすると判ります。

[ 9]$A???の1WORD分が実行されることによる例外処理。Human68kではSX-WINDOWで使用されています。

[10]小細工なのですが、64KBずらすことによって、後で行うパッチ作業での32bitアドレス指定部分一箇所につき1byteの書き換えで済むのです。

[11]例の「Yコマンド」デバッグで確認可能です。このためYs - IIIのときからOPMレジスタへの書き込み部分をフックして自前のバッファに値をバックアップおいて、画面に表示する様に作っていたのです。

[12]XM6が実機以上にデバッグ環境が整っているという例の1つです。

[13]ちなみに初代X68kに付属の「グラディウス」はBGM演奏にV-DISP信号割り込みかMFPのTimer-A割り込みのどちらかが使用されていました(良く覚えていない)。MZLはマルチスキャンディスプレイに水平同期周波数15kHzモードの画面を表示させるためにドットクロックオシレータ(38.863MHz)を80MHzのものに交換した大バカ野郎でしたが、これで15kHzモードの「グラディウス」をプレイすると表示だけでなくBGM演奏も倍速になって大笑いした記憶があります。

[14]Human68kはVer2.0以降でBGプロセス動作のためMFPのTimer-D割り込みが使用されます。割り込みベクタを直接書きかえることは不可能ではありませんが、MFPへの設定値を保存して使用後に復帰する必要があるため「利用したくない」のです。

[15]著者は桑野雅彦様(正確な漢字が出ないんですぅ)。X68kプログラマの「バイブル(その2)」です。持っていない方はやはり代用として「ぷにぐらま〜ずまにゅある」をどうぞ。

[16]10bit幅なのでレジスタ番号$10〜$11の2個に跨っています。

[17]日曜日朝の「ペコラ」って観てないですか……。観てませんね。

[18]「Inside X68000」ではp267の図6のTimer-A/Bのbitが間違って入れ替わっています。そういえばシャープ提供の資料が既に間違えていたとかZMUSIC ver1のムックに書いてあったっけ。

[19]調べたところZMUSIC ver3がOPMのTimer-A/Bの両方を駆動するドライバです。これ以外で両駆動の音源ドライバは見た覚えがありませんmcdrvとかmndrvとかいろいろ存在してることが判明。

[20]hootのVMにはDMAC転送終了割り込み機能が実装されていないので、こればかりはどうにもなりませんでした。将来的に実装されるようなことがあればいろいろなことが出来るようになると思います。


一階層戻る