全体の流れ
tools/extract_disks.pyがディスクイメージから曲(MCM)を取り出し、データベースtools/mcm_db.csvを元にプレイリストplayer/playlist.txtを作る。player/build_rom.pyがプレイリストの曲・曲名・CG と、ディスクイメージから取り出したゲームのドライバと表(音色など)を ROM(ASCII16 メガロム)にまとめる。- ROM の中のプレーヤー(
player/player.asm)が、曲ごとにそのゲームのドライバを RAM へ写して鳴らす。
曲の取り出し(extract_disks.py)
disk_imageの下のフォルダ 1 つが 1 ゲームです。ディスク番号は、ファイル名で並べた順に 1 から付けます。- ディスクの全位置を調べて MCM の形のデータを探します(
tools/mcm_scan.py)。MCM の形: 先頭のワードがチャンネルのブロック数、続いて各ブロックが「次のブロックの位置」で始まる鎖になっていて、0000 で終わる。各チャンネルのコマンド列の終わりの00がちょうどブロックの最後に来ることも確かめます(tools/mcm.py)。 - 取り出すのは、FM 音源を使う曲と、データベースに登録した PSG の曲だけです。データベースで除外にした曲(効果音など)は取り出しません(
--allで全部)。 - ファイル名は「フォルダ名の先頭 4 文字 + ディスク番号 1 桁 + 連番 2 桁」です(例
ILLU102)。FRAY_M2 / FRAY_TR はFRYM2/FRYTRで始めます。PSG だけの曲はディスク番号の前にP、Xak のおまけゲーム(BASIC の PLAY 文)を MCM にした曲はOを付けます(例XAKP101、XAKO301)。 - Xak のおまけゲーム「GoGo ピクシー」のタイトルの曲は、ディスクの OMAKE.BAS の PLAY 文・SOUND 文から MCM を作ります(
tools/omake.py)。 - 使い回しの曲(同じデータが複数のゲームにある)は、データベースの行ごとに、それぞれのゲームのフォルダへ書きます。
- 最後に、取り出した曲とデータベースからプレイリストと
extract_log.txt(ファイルとデータベースの行の対応)を作ります。プレイリストがもうあればplaylist_new.txtに書きます。
データベース(mcm_db.csv)
UTF-8(BOM 付き)の CSV で、1 行が「あるゲームの 1 曲」です。曲は中身(MD5)で見分けるので、ファイル名やディスク上の位置が違っても、中身が同じなら同じ曲です。同じデータを別のゲームでも使っているときは、ゲームごとに 1 行ずつ持てるので、曲名もゲームごとに付けられます(例: 同じデータが Xak II では「ボローズの森」、ガゼルの塔では「森」)。
| 欄 | 内容 |
|---|---|
| id | 行の番号(S0001 など) |
| game / game_en | ゲーム名 / 英語のゲーム名 |
| order | ゲームの中の並び |
| title / title_en | 曲名 / 英語の曲名 |
| exclude | 除外の理由(空なら収録) |
| loops | ループ回数(空なら既定) |
| folder / file | この曲に使うファイル(disk_image のフォルダ名とファイル名) |
| md5 ほか | 照合用の値、見つかった場所 |
管理用のコマンド(tools/mcm_db.py):
python tools/mcm_db.py sync # 詳しい形式の playlist.txt の曲名などをデータベースに反映 python tools/mcm_db.py playlist # データベース + data -> playlist.txt python tools/mcm_db.py find ファイル # ファイルがどの曲か調べる (チャンネル単位で似た曲も出す)
extract_disks.py --full は、詳しい形式(曲名などの欄も書いたもの)のプレイリストを作ります。
プレイリストの書式
# コメント @startup ../data/CABIN/CABI112.MCM | タイトル | Title # 起動時に鳴らす曲 (曲のリストには出ない) @startup_cg ../data/cabin.png # 起動時に表示する CG @game Xak | Xak # 以降の曲のゲーム名 | 英語のゲーム名 @cg ../data/xak.png # このゲームの CG ../data/XAK/XAK201.MCM # 曲名を省略するとデータベースの曲名 ../data/XAK/XAK202.MCM | 教会 # 曲名を指定 ../data/XAK/XAK202.MCM | 教会 | | | Church # 5 番目の欄は英語の曲名 @song_cg ../data/xak_botu.png # 前の行の曲を鳴らしている間だけの CG ../data/ILLUSION/ILLU110.MCM | City | 1 # ループ回数を指定 ../data/FFANTASY/FFAN101.MCM | | 0 # 0 = 自然に終わるか最大演奏時間までループ
- パスはプレイリストのファイルからの相対パスです。
- 曲名を省略するとデータベースの曲名、データベースにもなければ「Track 番号」になります。ループ回数を省略するとデータベースの値、なければ
--loopsの値(既定 2)です。 - 4 番目の欄(以前のドライバの指定)は使いません。
- 英語の曲名・ゲーム名は、本体の文字セット(BIOS の 002Bh 番地)が日本以外のときに表示します。英語がなければ日本語で表示します。ROM の 001Eh 番地(目印
MCPLAYER LANG:の直後)の 1 バイトで言語を固定できます(00= 自動、01= 英語、02= 日本語。ビルド時なら--lang)。 - 使える文字は英数字・記号、かな、漢字(JIS 第一・第二水準)です。フォントにない文字は
?になり、ビルド時に警告が出ます。全角英数は半角に、半角カナは全角にして表示します。#以降はコメントなので、曲名に#は使えません。 - 1 行は 256 ドット(英数字は約 42 文字、全角は 32 文字)までで、はみ出す分は切ります。
- 最大 999 曲。中身が同じ曲データは 1 つだけ ROM に入ります。
@song_cgは最大 24 曲です。
ドライバ(ゲームごと)
曲は、その曲を取ったゲーム自身のサウンドドライバで鳴らします。ROM を作るとき、曲ファイルのフォルダと同じ名前の disk_image のフォルダのディスクイメージからドライバを取り出して手を入れ、ROM に入れます。曲を鳴らす前に、そのドライバを元の番地(RAM)へ写します。
| 版(drivers.json) | ゲーム | 置く番地 |
|---|---|---|
| PMAKER | プリンセスメーカー | CE00h |
| RISK2 | キャンペーン版大戦略II | CCC0h |
| CABIN_CD00 | The Tower of Cabin(ディスク 1・2) | CCC0h |
| XAKII / GAZZEL | Xak II、ガゼルの塔 | CE00h |
| CABIN_CF00 | FRAY(MSX2)、The Tower of Cabin(ディスク 3) | CE00h |
| FRAY_TR | FRAY(turboR) | CE00h |
| ILLUSION | 幻影都市 | 8000h |
| XAK | Xak | D000h |
| FFANTASY | ファイナルファンタジー | 752Fh(ページ1。ワークは BD00h〜) |
- 使える版は
drivers.jsonに載っているものだけです。中身は版ごとの番地などと、版を見分ける md5 で、ドライバのプログラムは入っていません。ドライバの入口の形(jp 割込み処理/di,push ix...)かsig(ドライバの先頭のバイト列)で候補を探し、先頭 600h バイトの md5 で版を決め、全体の md5 で確かめます。 - 曲のドライバは、曲が見つかったディスクにあるドライバ、なければ同じフォルダのほかのディスクのドライバです。1 枚のディスクに 2 つの版(turboR 用など)があるときは、drivers.json に載っている方を使います。
- 番地などは、Roberto Vargas さんによる幻影都市のドライバの逆アセンブル(Manuel Pazos さんのサイトで公開されている mcdrv.asm)と命令列を突き合わせて求めました。
- コマンド番号はドライバによって違います(初期化 3、フェードアウト 4、演奏中か 6 / 5、演奏開始 9 / 2)。チャンネルのワークの大きさ(40h / 2Eh / 2Dh)、ループ回数の位置なども drivers.json の値を使います。
ROM を作るとき、ドライバに次の手を入れます(build_rom.py の Drivers)。
- ループの終わりの
ld a,(ix+n)(ループ先を読む命令)→call HookLoop(無限ループの折り返しを数える) - 音符を鳴らす処理の
ld c,(ix+4)→call HookNote(レベルメーターに発音を知らせる) - 表のポインタ 4 つ → 表を置く番地
- 曲の番地が決まっているドライバ(Xak・Xak II・ガゼルの塔・FRAY・ファイナルファンタジー):
ld hl,C000hなどの曲の番地 → A000h - 幻影都市・FRAY(turboR): turboR のタイマー(E6h)で待つ FM 音源の書き込みを待たない形に、待つだけのルーチンを
retに(turboR は起動時に Z80 モードにする) - ファイナルファンタジー: コマンドの出口の
eiをnopに
ファイナルファンタジーのドライバ(ページ1)
FF のドライバはゲーム本体(4000h〜)の中にあり、ページ1で動きます。ページ1はこの ROM なので、ドライバはページ1の裏の RAM(ページ3と同じスロットの RAM)に置き、呼ぶ間だけページ1を RAM に切り替えます(割込みを止めて A8h と拡張スロットのレジスタを切り替え、終わったら ROM に戻す)。そのため、ドライバから呼ばれるフック(HookLoop・HookNote)と切り替えの処理はページ3の RAM(RamHooks)に置いています。
- チャンネルのワークは 2Dh バイトで、FM の 7〜9 チャンネルは常にリズムとして扱います(
D_RHY= 0FFh)。 - ワークがページ2の BD00h からにあるので、曲は 7424 バイトまでです。
- ページ1に RAM がない機種では、FF の曲は鳴らさずに次へ進みます。
- FF のディスクにドライバが見つからないときは、Xak のドライバ(なければ Xak II などのドライバ)で鳴らします(
DRV_FALLBACK)。
ゲームごとの表(音色など)
エンベロープ・ビブラート・エフェクト・FM 音色(ソフト音色 32 個)の表(0x1E0 バイト)はゲームごとに違います。ROM を作るとき、ディスクイメージから表を集めて ROM に入れ、曲を鳴らす前にその曲の表をページ2の RAM へ写します。
- ディスクの中では、表は曲と同じ並びに置かれていて、後ろに続く曲で使われます(Cabin Times のディスクでは Xak II の表の後ろに Xak II の曲、FRAY の表の後ろに FRAY の曲)。そこで、曲をディスクイメージの中で探し、同じディスクで曲より前にある一番近い表を使います。
- 表を置く番地は、ゲームの中での番地の下位 12 ビットをそのままにして 9000h〜9FFFh に移したものです(ガゼルの塔は入らないので 8F00h〜)。下位の並びがゲームと同じなので、表の外を読む音(37 番目の音など)もゲームと同じになります。
- 表が見つからない曲は、幻影都市などの表で鳴らします。
- どの曲にどのドライバと表を使ったかは、ROM を作ったときに表示されます。
- Manuel Pazos さんの Windows 版 PCPLAYER の
.fmins(FANTASY / FRAY / MCDRV(=幻影都市) / PMAKER / XAK)は、ディスクにある表の FM 音色の部分と一致しました。
動き
- 起動の最初に、マッパーが ASCII16 か確かめます。6000h にバンク 1 を選んで 4000h にバンク 1 の目印(
MC16、build_rom.py が入れる)が見えるかを、RAM に写した小さなルーチンで調べ、見えなければ BIOS でメッセージを出して、RAM の中で割込みを止めて HALT します。ASCII8 などでは 6000h〜7FFFh に ROM の正しい部分が見えず、そこのルーチンを呼ぶと暴走するため、ここまでのコードは先頭 8KB(4000h〜5FFFh)に置いています。 - ドライバの割込み処理は H.TIMI のフックから呼びます。画面の描き直しに時間がかかってもテンポは乱れません。キーも割込みで毎フレーム読んで貯めておきます。
- 曲の終わりの判定
- 全チャンネルが無限ループを指定回数折り返したら、フェードアウトして次の曲へ進みます(
HookLoopで数える)。 - ループしない曲(どのチャンネルにも無限ループがない曲)は、ROM を作るときに見分けてループ回数を 1 にし、自然に終わった時点で次へ進みます(LOOP は 01/01)。
- 鳴り終わった後に音を出さない無限ループ(休符だけの繰り返し)で待つ曲(Xak の GET アイテムなど)は、ROM を作るときにそのループの所でチャンネルを終わらせ、ループしない曲として扱います。
- 10 分を超えたら、強制的にフェードアウトします(
MAX_SECONDS)。
- 全チャンネルが無限ループを指定回数折り返したら、フェードアウトして次の曲へ進みます(
- 曲が鳴り終わって次の曲へ進むときは、音源を初期化しません(FM の余韻の途中で音量を最小にすると「プチッ」と鳴るため)。
- 13 チャンネル以上ある曲は、13 チャンネル目以降を演奏しません。
- PAL(50Hz)の本体では、割込み 5 回に 1 回ドライバをもう一度動かして、NTSC と同じ速さ(60 回/秒)で演奏します。50Hz かどうかは、MSX2 以降は VDP の設定(R#9)、MSX1 は本体の ID で決めます。
- 音源: MSX-MUSIC(内蔵)と FM-PAC を自動で見つけます(FM-PAC は 7FF6h で I/O を有効にする)。なければ FM 用の曲データの PSG のパートだけが鳴ります(ゲームが FM 音源のないときに使う PSG 用の曲データは使いません)。turboR では起動時に Z80 モードにします。
- 起動時の動作
- MSX2 以降: 起動時の CG を出し、起動時の曲があればループで鳴らします(なければ無音)。何かキーで曲を止めて曲選択の画面へ。H キーなら曲は止めずに曲選択の画面にしてヘルプを出し、起動時の曲は次のループの所で止めます。
- MSX1(と V9990 があるとき): 曲選択の画面で、起動時の曲を 1 周だけ鳴らして止まります。
--autoplay: 起動時の曲は 1 回だけで、終わったらリストの先頭の曲から自動で鳴らします(起動時の曲がなければ CG を約 3 秒見せてから)。プレイリスト表の +2 の bit0。
- 再生モード(E・R キー)と、曲が終わったときの動き:
REPEAT ONCE ALL 次の曲へ。最後まで行ったら先頭へ 次の曲へ。最後で止まる GAME そのゲームの次の曲へ。最後まで行ったらそのゲームの先頭へ そのゲームの最後で止まる ONE 同じ曲を頭からもう一度 止まる - V キー(MSX2 以降): 再生中のゲームの CG とリストを切り替えます。CG の表示中は、最下行に演奏中の「ゲーム名 / 曲名」を出し、↑↓←→ で選択を動かすと選択中の曲を出します。
- V9990(GFX9000 など): 起動時に見つけたら、CG は常に V9990 にだけ出し、内蔵 VDP は曲選択の画面だけにします(V キーは使いません。MSX1 でも CG が出ます)。SCREEN 5 の CG は B1(256×212)の BP4、SCREEN 7 は B3(512×212、インターレースは 512×424)の BP4、SCREEN 8 は B1 の BD8 で表示します。見つけ方: ポート 67h でソフトリセットしたあと、R#15 に書いた値が読み返せるか。
- I/O 88h の VDP(V9968 カートリッジ、V9938 / V9958 のアップグレードキットなど): V9990 がなければ調べ、見つけたら V9990 と同じ扱い(CG はそちらにだけ出す)にします(
HasExtVdp= 2)。V9958 として使い、V9968 の拡張レジスタ(R#20 / R#21)は触りません。SCREEN 5 / 7 / 8 を内蔵 VDP と同じ画面モードと VRAM 配置で出し、インターレースは SCREEN 7 のページ 0(偶数ライン)とページ 1(奇数ライン)を交互に表示します(R#9 = 8Ch、R#2 = 3Fh)。割込みは使いません。見つけ方: S#1 の ID が V9938(0)か V9958(2)で、VRAM 0 に書いた 55h・AAh が読み返せるか。本体の VDP が 88h にある機種(BIOS の 0006h が 88h)では調べません。 - 本体の VDP が V9968 の機種(I/O 98h): 本体の VDP の種類は調べないので、V9958 の MSX2+ と同じ扱いです。V9990 もあれば、上と同じく CG は V9990 にだけ出します。
CG
- 読める画像: Pillow で読める画像(PNG・JPEG・BMP・GIF など)と BSAVE 形式の
.SC5/.SC8。透明な部分は黒にします。 - 大きさ
- 256×212: そのまま。256×192 / 512×192 は下を黒で埋めます。
- 512×212: 横 512 ドットの画面として SCREEN 7(16 色)。
- 320×240 / 640×480: エミュレータのスクリーンショットとして、周りの枠を切り取ります(212 ライン表示なら上下 14 ドット、192 ライン表示なら 24 ドット、左右 32 ドット)。640×480 で横が 2 倍の絵でなければ横 512 ドットの画面、行ごとに違えばインターレース(512×424。SCREEN 7 の 2 ページを交互に表示)。
- それ以外の大きさは 256×212 に引き伸ばします(縦横比は保たない。警告が出ます)。
- 色
- 横 256 ドット: MSX2 の 512 色(RGB 各 3 ビット)に直して 16 色以下なら SCREEN 5(色はそのまま)。それより多ければ SCREEN 8(固定の 256 色)に誤差拡散で減らします。
- 横 512 ドット: 16 色より多ければ 16 色に減らします。
- CG 表示中の最下行の文字は白、背景は黒です。SCREEN 5・7 で 16 色すべてを使っていて足す空きがないときは、絵の中で最も明るい色・最も暗い色を使います。
- エミュレータのスクリーンショット(openMSX の
screenshot -raw)から作るのが確実です。ゲームはパレットを VDP に直接書くので、VRAM を保存する方法ではパレットが残らないことがあります。スキャンラインやぼかし入りの画像は色が増えて SCREEN 8 になってしまいます。 - 圧縮: ROM には「パレット 32 バイト + ランレングス圧縮した画面のデータ」で入れ、プレーヤーが展開しながら VDP / V9990 へ送ります(
cgrle.py。01h-7Fh = そのまま n バイト、80h+n = 1 バイトを n+3 回)。まとまりは 256 バイトの区切りと ROM のバンクの境目をまたがないので、展開は 256 バイトずつ(間に割込みを通す)、そのままのバイトは OTIR で送ります。写真のような絵は縮みにくく、表示にも時間がかかります(SCREEN 7 の 1 枚で約 1 秒、写真に近いインターレースの絵で約 1.9 秒)。
画面
SCREEN 2(256×192)に、漢字・かなは k8x12(8×12 ドット)、英数字は k6x8 の全角の英数字(6×8 ドット。12 ドットの行の上から 2 ドット目に置く)で文字を描きます。ASCII のうち " ' - ~ は全角の字がないので、似た字(” ’ − 〜)で代用しています。文字の位置は 2 ドット単位で、半角は 6 ドット、全角は 8 ドット進めます。名前表を 0〜255 の連番にしてビットマップとして使うので、1 画面に出せる漢字の種類に制限はありません。1 行は 12 ドットで、画面は 16 行です。
| 行 | 左 | 右 |
|---|---|---|
| 0 | タイトル | レベルメーター(12 本: PSG 3 + FM 9) |
| 1 | 音源 | 〃 |
| 2 | TRACK・LOOP(何周目か / ループ回数) | 〃 |
| 3 | TIME・再生モード | チャンネル名(PSG は A B C、FM は 1〜9。リズムモードの曲は FM の最後の 3 つが B S H。使うチャンネルは白、使わないチャンネルは灰色) |
| 4 | 演奏中の「ゲーム名 / 曲名」(止まっているときは STOP) | |
| 5 | 前のゲーム <- 【選んでいるゲーム】 -> 次のゲーム | |
| 6〜15 | 選んでいるゲームの曲リスト(10 曲) |
- レベルメーターは、新しい音が鳴るとその音量まで跳ね上がり、あとは 3 フレームに 1 段ずつ下がります。音が続いている間は音量の半分で止まり、休符では 0 まで下がります。発音は
HookNoteで知り、音量は PSG はレジスタ(R7, R8〜R10)、FM はドライバのチャンネルのワーク(キーオンと音量)を読みます。 - 使うチャンネルは、曲を始めるときに各チャンネルに音符があるかで決めます(PSG のチャンネルでは、音色の命令 42h〜61h・FCh もノイズを鳴らすので数える)。
- H キーのヘルプ(バージョン・クレジット・キー操作)は、リストの上にウィンドウで出します。
ROM の構成(ASCII16)
| バンク | 配置 | 内容 |
|---|---|---|
| 0 | 4000h-7FFFh | プログラム、ドライバの情報(DrvDir: 1 つ 40 バイト、最大 12)、ドライバと表の組(SetDir: 最大 32 組)、プレイリスト表(1 曲 12 バイト)、ゲームの情報(CG の場所とゲーム名)、半角フォント |
| 1〜 | 読み出すときだけページ1に出す | ゲームのドライバ(1 つ約 3KB)、ゲームの表、曲名(日本語と英語)、曲データ(バンク境界をまたがないように詰める)、CG(バンクの先頭から続けて置く) |
| 最後 | 〃 | 全角フォント(曲名・ゲーム名で使う文字だけ。1 字 16 バイト) |
プレイリスト表の 1 曲(12 バイト): 曲データのバンク・アドレス・長さ、曲名のバンク・アドレス、ループ回数、ドライバと表の組の番号(SetDir の 1 から)、ゲームの情報のアドレス。
メモリ
| 場所 | 内容 |
|---|---|
| ページ1の裏の RAM 752Fh-7FAFh | ファイナルファンタジーのドライバ(呼ぶ間だけページ1を RAM にする) |
| ページ2の RAM 8000h-8D76h | 幻影都市のドライバ |
| 8F00h-9FFFh | ゲームの表(曲を始めるたびにコピー。番地はドライバごと) |
| A000h-BFFFh | 演奏中の曲データ(曲を始めるたびにコピー)。FF のドライバのときは BD00h からがそのワーク |
| C000h- | プレーヤーのワーク、RAM に置くルーチン(バンク読み出し・CG の展開・フック) |
| -CCBFh | スタック |
| CCC0h-DDFFh | ゲームのドライバ(番地はドライバごと。後ろはドライバのワーク) |
- ドライバはゲームの中で決まった番地に置かれる作りで動かせないため、元の番地に置きます。ドライバが替わる曲を始めるときに、ROM からコードと表・初期値の部分を写し、ワークを 0 にして初期化します。
- HIMEM が DE00h より下の機種では、その上のシステムのワークとドライバが重なるので、元の H.TIMI フック(ディスクのタイマー処理)は呼びません。
- ページ2に RAM が必要なので、RAM 32KB 以上で動きます(FF の曲は 64KB)。
確かめたこと
- openMSX の turboR(FS-A1ST)と C-BIOS の MSX2+・MSX1 で、10 の版すべてのドライバで演奏・曲の終わりの判定・ループの計測が動くこと。
- 全 255 曲を前の版(幻影都市のドライバの逆アセンブル mcdrv.asm を ROM 用に直したドライバと、PMAKER のドライバ)と通しで鳴らし、247 曲で曲の終わりの時刻が一致すること。違ったのは Xak の 3 曲(XAK210・XAK301・XAK306。2〜7 秒長い)と大戦略 II の RISK202(MCDRV では途中で止まっていたのがループするようになった)。録音して比べた音もほぼ同じ。
- 自動再生(キー入力なし)、V9990 の表示、I/O 88h の V9958(openMSX の拡張で再現。SCREEN 5・7・8・インターレースの VRAM とパレットが元のデータと一致。実機の turboR + V9968 カートリッジでも、CG の表示・切り替え・インターレースを確認)、インターレースの CG、PAL の速さ、英語表示。
- マッパーの確認: openMSX(FS-A1ST・C-BIOS MSX1)と実機の ESERAMair(8KB バンクにして起動)で、ASCII16 でなければメッセージを出して止まること。ASCII16 では実機(turboR + V9968)で正常に動くこと。
- 音そのものは耳でまだ十分に確かめていません。
フォント
門真 なむ氏の「k8x12」と「k6x8」(英数字)を使っています。どちらも自由に利用・改変・再配布できます(font/misaki_12x8/k8x12.txt、font/k6x8/k6x8.txt を参照)。