前回
前回の記事でGBAのROMを作って実行できるところまでやりました。
その後Tile/Objを基本とした表示周りはわりと素直に動いてくれて、VBlank中でないとVRAMの書き込みに失敗するとか、2byte単位でアクセスすべしとかDMAで一気に転送しないと間に合わないとか、色々気をつける部分はあるものの概ね問題なく表示が終わりました。
VRAMの書き込みタイミングさえ気をつけていれば、後はメモリさえいじってればなんとかなる世界。
……パレットのやりくりは大変でしたが(結局パレットダイエットツールを作った)

フォント周りは、テキストを吸い出して、PNG画像にして、Tileに変換するツールを作ったり
それで次は音楽だ!となるんですが、こちらはちょっと紆余曲折あって結局ライブラリに頼ったので、GBAの仕様書だけでは資料が足りないので覚書として書いておきます。
maxmodを使って音を鳴らす
PCMミキサーがついていて、GBAの2ch以上の音を流してもソフトウェアで合成してくれるらしいので8chとか16chとか鳴らせます。もちろん重ねたら当然音質は落ちますし、あと容量_(:3」∠)_
PCMで素直に1曲BGMを用意してしまうとROM容量がエライことになってしまうし、ogg/mp3はCPU使用率がエライことになってしまうし、PSGを使うと開発難易度がエライことになってしまう。
そこでmod(Tracker)ですよ。
SEみたいな短い音はPCMの8bit/11~16khz/monoで作って、BGMはmod(今回はs3m)を使ってPCM再生をしつつも容量は抑える。
それらをmaxmodのmmutilというコマンドでsoundbank形式にまとめて、libmmを使って再生させる、という流れです。
maxmodは前回の記事で使用したDevkitProのコンテナの中に最初から入っており、特に別途何かを用意する必要はありません。作曲用にトラッカーを準備するだけです。
s3m出力にOpenMPTを使いました
たぶん今回で一番時間がかかった部分です。
まず音がならないところからのスタートやで(´・ω:;.:...
ChipTuneの世界は音を作るところからスタートするらしく、最初は結構無音だったりデフォルト1音だったりするのでまずは楽器を作らねばならないらしいのですが、素人には敷居が高くありませんかね?😇
と思っていたら実は左上の枠の中をよく見ると最初からGM音源のSampleが入っていたので、適当な音をSamplerタブの中の波形ウインドウに投げ込んで、PatternタブのInstrumentに設定すればOKでした。
GBAで再生するにはmod(Amiga)のモジュールを使うのが一番相性がいいと思うんですが、サンプラーのクセが強くて普通に周波数で表示してほしいよね、ってことでs3mを使うことにしました。チャンネル数は少なめでEffectもほぼ使わず複雑にならないようにすれば普通にGBAでも鳴るみたいです。
※mod/s3m等のフォーマットを扱うpythonパッケージnodmodを見つけたので、Furnaceとかで作ってTextで出したものを変換、とかもできそうです
soundbankを作る
SE用のwavとBGM用のMOD/S3M/XM/IT形式をmmutilというコマンドを使って、複数のサウンドデータを1つのsoubnbankという形式に変換します。
後はそれをROMに入れておけば、maxmodのライブラリを使って簡単に再生できます。
wavとmod系は別管理になっていて、-hオプションでCヘッダを出力するとdefineで管理番号が振られており、プログラム内ではその管理番号を使って再生したりします。
$ podman run --rm -v "$(pwd):/work" -w /work
devkitpro/devkitarm mmutil "SE用のwav" "BGM用のs3m" -osoundbank.bin [-hsoundbank.h]
swiftでmaxmodライブラリを使う
今回のメインコンテンツです。maxmodで音を再生させるには、
- mmInitDefaultを使って初期化を行う
mmInitで設定もできるみたいだけど、お気楽GBA開発なのでDefault設定で - mmEffect/mmStartを使って再生を指定する
- VBlank待ちの「前」にmmFrame、VBlank割り込みでmmVBlank
ここで、mmVBlankがVBlank割り込みにフックしなければならないんですが、IRQの初期化がTableを登録してビットみて関数呼び出し、程度ではダメのようです。
VBlank/HBlank等の割り込み時はDISPCNTの割り込み設定も同時に設定しろ、とか、Thumbモードの関数ポインタのビット0は1にしなければならない、とかはまだいいとしてリターン命令(BX LR)の欠如の可能性とかいわれてswiftだけだとちょっと対応が難しい内容もでてきました。
お気楽にSwiftだけでやりたいねん(´・ω・`)
というわけで、IRQの設定についてはmaxmodを眺めてた時に見つけたlibgbaというライブラリを使うことにしました。
リンク側の準備
ライブラリ自体はDevkitProに同梱されているので、LINK時のLIBSに-lmm -lgbaを、LINKフラグに-L /opt/devkitpro/libgba/libを追加するようにします
podman run --rm -v "$(pwd):/work" -w /work devkitpro/devkitarm
/opt/devkitpro/devkitARM/bin/arm-none-eabi-gcc
-mthumb -mthumb-interwork -specs=gba.specs
-Wl,-z,noexecstack,-Map=build/tmp/output.map
-L /opt/devkitpro/libgba/lib # ここ
build/obj/swift-ui-gba.o -o build/tmp/output.elf
-lmm -lgba # ここ
Swiftのラッパーを準備
関数定義はmaxmodのGitHubを見に行って、C言語のコードを見ながらSwiftで呼び出せる形に置き換えます。
// Sound関係(libmm)
// ****************************************************************************
public enum MM_PLAY_MODE: UInt32 {
case LOOP = 0
case ONCE = 1
}
// Maxmodの型や関数をSwift側に宣言
// 初期化
// --------------------------------------------------------
@_silgen_name("mmInitDefault")
public func mmInitDefault(soundbank: UInt, channels: UInt16)
@_silgen_name("mmFrame")
public func mmFrame()
@_silgen_name("mmVBlank")
public func mmVBlank() // Maxmod の VBlank 更新関数
// SE
// --------------------------------------------------------
@_silgen_name("mmEffect")
public func mmEffect(sfxID: UInt16) -> UInt
@_silgen_name("mmEffectVolume")
public func mmEffectVolume(sfxHandle: UInt16, volume: UInt8)
@_silgen_name("mmSetEffectsVolume")
public func mmSetEffectsVolume(volume: UInt8)
@_silgen_name("mmEffectCancel")
public func mmEffectCancel(sfxHandle: UInt16)
@_silgen_name("mmEffectCancelAll")
public func mmEffectCancelAll()
@_silgen_name("mmLoadEffect")
public func mmLoadEffect(sfxID: UInt16)
@_silgen_name("mmUnloadEffect")
public func mmUnloadEffect(sfxID: UInt16)
// BGM
// --------------------------------------------------------
@_silgen_name("mmStart")
private func _mmStart(modID: UInt16, mode: UInt32)
public func mmStart(modID: UInt16, mode: MM_PLAY_MODE) {
_mmStart(modID: modID, mode: mode.rawValue)
}
@_silgen_name("mmStop")
public func mmStop()
@_silgen_name("mmPause")
public func mmPause()
@_silgen_name("mmResume")
public func mmResume()
@_silgen_name("mmLoad")
public func mmLoad(modID: UInt16)
@_silgen_name("mmUnload")
public func mmUnload(modID: UInt16)
@_silgen_name("mmSetModuleVolume")
public func mmSetModuleVolume(volume: UInt16)
// IRQ関係
// ****************************************************************************
public enum IRQ: UInt32 {
case VBLANK = 0
case HBLANK = 1
case RASTER = 2
case TIMER0 = 3
case TIMER1 = 4
case TIMER2 = 5
case TIMER3 = 6
case SERIAL = 7
case DMA0 = 8
case DMA1 = 9
case DMA2 = 10
case DMA3 = 11
case KEYPAD = 12
case GAMEPAK = 13
}
@_silgen_name("irqInit")
public func irqInit()
@_silgen_name("irqEnable")
private func _irqEnable(_ irq: UInt32)
public func irqEnable(irq: IRQ) {
_irqEnable(1 << irq.rawValue)
}
@_silgen_name("irqDisable")
private func _irqDisable(_ irq: UInt32)
public func irqDisable(irq: IRQ) {
_irqDisable(1 << irq.rawValue)
}
@_silgen_name("irqSet")
private func _irqSet(_ irq: UInt32, _ handler: @convention(c) () -> Void)
public func irqSet(irq: IRQ, handler: @convention(c) () -> Void) {
_irqSet(1 << irq.rawValue, handler)
}
さぁ、鳴らしてみよう!
自分のプログラムの最初の方で次のように初期化します
@_optimize(none)
@_cdecl("main")
public func main() {
// IRQテーブルの初期化
irqInit()
// サウンド用の処理をVBLank割り込みにフック
irqEnable(irq: IRQ.VBLANK)
irqSet(irq: IRQ.VBLANK, handler: mmVBlank) // これが重要
// サウンドバンクのロード
mmInitDefault(soundbank: ROM_ADDR + UInt(ROM_SOUNDBANK), channels: 8)
VBlank待ちの前にmmFrameを入れておきます.
while true {
mmFrame() // VBlankの前で
waitForVBlank()
後は適当にmmEffectやmmStartを呼び出せば音が鳴ります。
mmStart(modID: 0, mode: MM_PLAY_MODE.LOOP)
let _ = mmEffect(sfxID: 0)
IWRAM/EWRAMを使うので注意
libmmを使うとIWRAMにワークエリアを確保します。
.iwram 0x03000000 0x554 /opt/devkitpro/libgba/lib/libmm.a(mm_mas.o)
0x03000000 mppProcessTick
0x030000e4 mpp_Channel_NewNote
0x030001c4 mpp_Update_ACHN_notest
.iwram 0x03000554 0x648 /opt/devkitpro/libgba/lib/libmm.a(mm_mas_arm.o)
0x03000554 mmAllocChannel
0x030005c8 mmReadPattern
0x030006b4 mmUpdateChannel_T0
0x030008f4 mmUpdateChannel_TN
0x03000a94 mmGetPeriod
0x03000ad8 note_table_oct
0x03000af6 note_table_mod
使えるIWRAMが大きく減るので注意してください。
0x03003544 __iheap_start = .
さらに16kHzのミキシングレート、 EWRAMのチャネルバッファ、EWRAMの波式バッファ、およびIWRAMにおける混合バッファ。また、VBlankも連携していますとのことで、EWRAMに波形データ等を入れたりするようです。
// Allocate data for channel buffers & wave buffer (memory returned by
// malloc is in EWRAM). Use the SIZEOF definitions to calculate how many
// bytes to reserve
u8 *myData = malloc((8 * (MM_SIZEOF_MODCH + MM_SIZEOF_ACTCH + MM_SIZEOF_MIXCH))
+ MM_MIXLEN_16KHZ);
EWRAMのバッティングにはご注意を(´・ω:;.:...

