0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Swiftでお気楽GBA開発サウンド編

0
Last updated at Posted at 2026-08-18

前回

前回の記事でGBAのROMを作って実行できるところまでやりました。

その後Tile/Objを基本とした表示周りはわりと素直に動いてくれて、VBlank中でないとVRAMの書き込みに失敗するとか、2byte単位でアクセスすべしとかDMAで一気に転送しないと間に合わないとか、色々気をつける部分はあるものの概ね問題なく表示が終わりました。

VRAMの書き込みタイミングさえ気をつけていれば、後はメモリさえいじってればなんとかなる世界。

……パレットのやりくりは大変でしたが(結局パレットダイエットツールを作った)
スクリーンショット 2026-08-16 195629.png

フォント周りは、テキストを吸い出して、PNG画像にして、Tileに変換するツールを作ったり

スクリーンショット 2026-08-13 193306.png

それで次は音楽だ!となるんですが、こちらはちょっと紆余曲折あって結局ライブラリに頼ったので、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を使いました

たぶん今回で一番時間がかかった部分です。
まず音がならないところからのスタートやで(´・ω:;.:...

スクリーンショット 2026-08-18 160340.png

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で音を再生させるには、

  1. mmInitDefaultを使って初期化を行う
    mmInitで設定もできるみたいだけど、お気楽GBA開発なのでDefault設定で
  2. mmEffect/mmStartを使って再生を指定する
  3. 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.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:swift
// 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にワークエリアを確保します。

mapファイル抜粋
 .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のバッティングにはご注意を(´・ω:;.:...

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?