ゲームギアのゲームにPSGlibで音楽を入れる方法

# はじめに

現在、ゲームギア用のゲームを開発中ですが、サウンドドライバの実装や関連ツールの開発は物凄く大変なので、OSS のライブラリとツールを使うことにしました。

本書では私が今回採用した OSS のライブラリとツールを紹介します。

# 採用した OSS

下表が今回採用したライブラリとツールの一覧表です。

|Name|Description|
|:---|:----------|
|[PSGlib](https://github.com/sverx/PSGlib/)|SN76489 のサウンドドライバ (Z80)|
|[PSGTool](https://www.smspower.org/forums/16925-PSGToolAVGMToPSGConvertor)|VGM ファイルを PSGlib で扱える形式に変換 & 最適化|
|[XPMCK](http://jiggawatt.org/muzak/xpmck/)|MMLコンパイラ|
|[vgmplay-legacy](https://github.com/vgmrips/vgmplay-legacy)|コマンドラインで動作する .vgm 再生ツール|

PSGlib は .vgm 形式のデータを BGM や効果音として再生できるライブラリとのことです。

.vgm の作成ツールを色々と調べた結果、XPMCK (MML コンパイラ) と vgmplay-legacy (.vgm の再生チェックツール) を採用しました。

当初、mml2vgm と MDPlayer というツールを使おうとしたのですが、

- mml2vgm の mml サンプルデータがネット上に転がっていなかった
  - これは単に私の調査不足かも :sweat_smile: 
- GUI のツールが全般的に苦手（コマンドのみで操作したい CLI 派）

mml2vgm は人気があるツールらしいので、一点目は恐らく探せばサンプルを公開している方も居るかもしれませんが、XPMCK ならツール内にサンプルの mml が同梱されていたので容易に導入できそうだと考えました。

ただし、mml2vgm は GUI のエディタもついているので、コマンド操作が苦手な方なら mml2vgm の方がとっつきやすいかもしれません。

SN76489 の .vgm ファイルさえ出力できればツールは何でも良いので、各々が使い易いツールを選択すれば良いかと思われます。

なお、XPMCK と vgmplay-legacy は Linux でも利用できそうだったので、メイン PC (Ubuntu) でビルドして使おうとしたのですが、XPMCK のビルドが通らなかった（Euphoria のアーカイブ形式がおかしいとのことでリンクが通らなかった）ため、音楽データの開発はサブ PC (Windows) で行うことにしました。

> _念のため Euphoria のウェブサイトで原因を調べようとしたのですが、ドットコムバブル時代の香りがするデザインのウェブサイトだったのでそっ閉じ..._

# MML 作成（Vim）

私が [VGS; Video Game Sound](https://qiita.com/suzukiplan/items/4f22dbeb16fb112c3f5f) で作曲した「[Battle Marine March](https://github.com/suzukiplan/bmarine-zero/blob/master/src/song1.mml)」という曲を XPMCK の MML に変換したものが以下です。

```song1_xpmck.mml
;---------------------------------------------------------------
#TITLE		Battle Marine March
#GAME		Battle Marine
#COMPOSER	Yoji Suzuki
#PROGRAMER	Yoji Suzuki
#OCTAVE-REV 0
#TUNE
;---------------------------------------------------------------
@v0 = {9 9 8 8 7 7} 
@v1 = {9 9 8 8 5 4}

ABC t120 K2
D t120

A o4 l16 v13< L @v1() 
B o4 l16 v13> L @v1()
C o4 l16 v13> L @v1()
D o4 l16 v4

;---------------------------------------------------------------
; Intro
;---------------------------------------------------------------
A d4.d32r32d32r32
A <b-8b-32r32b-32r32>
A c8c32r32c32r32
A <a8a32r32a32r32
A g8g32r32g32r32
A f4.r8

B f8.rg8.r
B fref<ar>d8
B crc+rdrer
B f4.r8

C d8.re8.r
C dr8.<frb-8
C argrfrgr
C f4.r8

D arr4>a32r32a32r32<
D ar>a32r32a32r32< ar>a32r32a32r32<
D ar>a32r32a32r32< arar
D a+32r32a+32r32a+32r32a+32r32 ara+r

;---------------------------------------------------------------
; A
;---------------------------------------------------------------
A f4f32r32f32r32f32r32f32r32 gdfg ff32r32e8 f8.f32r32e8.e32r32d8.d32r32c+8.c+32r32
A f4f32r32f32r32f32r32f32r32 gdfg ff32r32e8 f8.f32r32e8.e32r32f4.r8
A f4f32r32f32r32f32r32f32r32 gdfg ff32r32e8 f8.f32r32e8.e32r32d8.d32r32c+8.c+32r32
A f4f32r32f32r32f32r32f32r32 gdfg ff32r32e8 f8.f32r32e8.e32r32f4.r8

B < @v0()
B c8raara8 g+a>c<b- arg8 cra8 efg8 f8.f grg8
B c8raara8 g+a>c<b- arg8 cra8efg8 f4r4
B c8raara8 g+a>c<b- arg8 cra8 efg8 f8.f grg8
B c8raara8 g+a>c<b- arg8 cra8efg8 f4.g+a+

C < @v0()
C a8r>ffrf8 efgg fre8 <ar>f8 cde8d8.dcrc8<
C a8r>ffrf8 efgg fre8 <ar>f8 cr<b-8 a4r4
C a8r>ffrf8 efgg fre8 <ar>f8 cde8d8.dcrc8<
C a8r>ffrf8 efgg fre8 <ar>f8 cr<b-8 a4r4

D <a>r8. cr<bb>
D <arar> cr<bb>
D <a>r8. cr<bb>
D <arar> crcr
D <a>r8. cr<bb>
D <arar> cr<bb>
D <a>r8. cr<bb>
D a+32r32a+32r32a+32r32a+32r32 crcr
D <a>r8. cr<bb>
D <arar> cr<bb>
D <a>r8. cr<bb>
D <arar> crcr
D <a>r8. cr<bb>
D <arar> cr<bb>
D <a>r8. cr<bb>
D a+32r32a+32r32a+32r32a+32r32 cde8

;---------------------------------------------------------------
; B1
;---------------------------------------------------------------
A g+4.g+r a+4.a+r >c+8c8<a+4 g+4f+4 g+4.g+r a+4.a+r f+2.f+f+>f+<f+
B >c4<a+8>d+8 c4<g+4 a+4>d+4< g+4.g+a+ >c4<a+8>d+8 c4<g+4 a+2.r8g+a+
C @v1() >g+4r4f4r4 f+4r4 f4.r8 g+4r4f4r4 f+2.r4
D d+4 a+32r32r8. a+32r32r8. a+32r32r8.
D a+32r32r8. a+32r32r8. a+32r32r8. a+32r32r8.
D a+32r32r8. a+32r32r8. a+32r32r8. a+32r32r8.
D a+32r32r8. a+32r32r8. a+32r32r8. aa+c<c>

;---------------------------------------------------------------
; B2
;---------------------------------------------------------------
A g+g+>g+<g+ g+>g+<g+>g+< a+a+>a+<a+ a+a+g+f f+8.g+8.f+r f4ff>f<f
A g+g+>g+<g+ g+>g+<g+>g+< a+a+>a+<a+ a+>a+<a+>a+ cc>cc< cc>cc< c>c<ca+cgce
B @v1() >c4<a+<a+>>d+<d+> c4f<f>g+<g+> a+<a+>g+<g+>g<g>d+<d+> f4.fg
B g+2^8a+<a+>g+<g+>a+<a+> g4.g+32a32a+32b32>c2
C r8g+8f+8d+8 c8d+8f8g+8 a+8g+8g8d+8 f4.r8
C >f2^8f8d+8f8 e4.f32f+32g32g+32a2

D <a>rcr <a>rcr <a>rcr <a>rcr
D c<a32r32a+32r32> c<a+32r32a32r32> cr r4 d4
D <a>rcr <a>rcr <a>rcr <a>rcr
D <a>rcr <a>rcr <a>rcr crcr

;---------------------------------------------------------------
; C
;---------------------------------------------------------------
A d8ad d32r32dfd8 d32r32da dgdf
A c8ec cc32r32c8 c32r32c32r32c8 >c<egc
A <b-8b->d< b-32r32b-b-b- b-ag8 g32r32g+ab->
A c8cc c32r32cc8 <aa>a<a >e<a>c<a>
A d8ad d32r32dfd8 d32r32da dgdf
A c8ec cc32r32c8 d4.dd d2.d32r32d32r32d32r32d32r32

B <a8.>d drd4 c+d <arg8 a>c<ag ar>c8< a2
C f8.a  ara4 r8   fre8 r4     fr a8  f2
B g8.g grg4ag frg8 a>c<ag ar>c8< a2
C e8.e ere4r8 dre8 r4     fr a8  f2
B a8.>d drd4 c+d <arg8 a>c<ag ar>c8  d4.de
C f8.a  ara4 r8   fre8 r4     fr a8  a2
B  f8. f frf4ed <arg8
C >c8.<a ara4r8  cr<b-8

D <a>rcr <a>rcr <a>rcr <a>rcr
D <a>rcr <a>rcr <a>rcr crcr
D <a>rcr <a>rcr <a>rcr <a>rcr
D <a>rcr <a>rcr <a>rcr crcr
D <a>rcr <a>rcr <a>rcr <a>rcr
D <a>rcr <a>rcr cr8. cr8. c+2
```

[VGS; Video Game Sound](https://qiita.com/suzukiplan/items/4f22dbeb16fb112c3f5f) との仕様差で注意すべき点としては以下のものがあります。

1. チャネル数が少ない（`VGS = 6, SN76489 = 3 (tons) + 1 (noise)`）
2. 使える音階の幅が狭い
3. 64分音符が使えない（最小は 32 分音符）
4. 周波数仕様差でそのままの調だと若干不自然になる（要トランスポーズ）
5. B# (B+), E# (E+), C♭ (C-), F♭ (F-) などの楽典に存在しない半音上げ下げの記号が使えない

5 は単純置換で何とかなりますが、1 ～ 4 の影響で自動変換が不可能なので、手動変換（編曲）をしています。

トランスポーズは K コマンドで簡単にできました。

# MML をコンパイル（XPMCK）

.mml ファイルを XPMCK で次のようにコンパイルすればゲームギア（SN76489）用の .vgm ファイルを生成できます。

```dosbatch
xpmc -sgg song1_xpmck.mml song1_xpmck.vgm
```

# VGM を視聴（VGMPlay）

.vgm ファイルを VGMPlay で再生してチェックすることができます。

```dosbatch
VGMPlay song1_xpmck.vgm
```

再生中に左右カーソルで曲のシークができます。

# PSGlib 形式に変換（PSGTool）

PSGlib は .vgm ファイルを直接読み込めないため、[PSGTool](https://www.smspower.org/forums/16925-PSGToolAVGMToPSGConvertor)を使って .vgm ファイルを .psg ファイルに変換します。

```dosbatch
java -jar PSGTool.jar song1_xpmck.vgm song1_xpmck.psg
```

参考までに、mml, vgm, psg のファイルサイズは次の通りです。

- song1_xpmck.mml = 4,166 bytes
- song1_xpmck.vgm = 16,292 bytes
- song1_xpmck.psg = __2,257 bytes__

.vgm 形式だとゲームギアに入れるには大きすぎますが .psg 形式なら丁度良さそうなサイズ感です。

:::note info
曲データはページ2（0x8000〜0xBFFF）のバンク切り替えで読み込む想定なので、実用的な __1曲の最大サイズは 16KB 以下__ です。理論限界は 48KB ですが、2 ページ（32KB）使うのは色々と面倒くさく、3ページ（48KB）使うのは超絶面倒くさくなります。
:::

# PSGlib の組み込み

今回のゲームギア用ゲームの開発には z88dk のアセンブラ（z80asm）を使っているのですが、PSGlib のソースファイルは別のアセンブラ用になっているので、組み込むには若干の構文修正が必要です。

z88dk のアセンブラ用に構文修正したものを [gist](https://gist.github.com/suzukiplan/272930bf40b6e476f743d431ce78af45
) に公開しておいたので、z88dk を使っている方はこちらをお使いください。（ライセンス表記はオリジナルの PSGlib だけ記載して頂ければ OK です）

> なお、上記 gist のソースコードは、RAM アドレス定義の [defvars](https://qiita.com/suzukiplan/items/5bf066a150d002ca47ea) をオリジナルに合わせて `$c000` にしているので、都合の良いアドレスに書き換えるか自前のグローバル変数テーブル（defvars）に移すなどの修正が必要です。

初期化と再生指示の箇所の実装は以下のような形です。

```main.asm
; ブート処理 (0x0000〜0x0037）
section x0000
org $0000
.init
    im 1
    ld sp, $dff0
    call init_vdp

    ; 初期化
    call PSGInit

    jmp main

; 割り込み処理（0x0038〜0x00FF）
section x0038
org $0038
    ; 当初割り込みで PSGFrame をコールしようとしたがタイミングが安定しない
    ; ので割り込みでは呼ばない
    retn

; メインコード（0x0100〜0x7FFF）
section x0100
org $0100
.main
    call load_pattern
    call load_palette

    ; 再生指示
    ld hl, song1
    call PSGPlay

    call title

（中略）

; NOTE: 最終的には page2 (0x8000〜0xBFFF) のデータ領域に入れる予定
;       だがテスト用にプログラムバンクに曲データを入れておく
song1:
    BINARY "../data/song1_xpmck.psg"
```

再生はタイミングが安定するループ処理で `PSGFrame` と `PSGSFXFrame` を呼び出します。

```title.asm
.title

（中略）

    ; ====== メインループ =====
title_loop:
    ; V-Sync待機
    call wait_vsync

    ; ここで PSGlib のループ処理を実行
    call PSGFrame
    call PSGSFXFrame

    ; ボタンを押したら抜ける
    call io_joypad
    and %10110000
    xor %10110000
    jz title_loop

（以下略）
```

> BGM と効果音の Frame 処理が別々になっているので、効果音のデータバンクを BGM と別にすることができる形になっています。

この実装で実際に鳴らしてみたものが以下ツイートの動画です。

https://twitter.com/suzukiplan/status/1767531637821383012