🎮

Rust でゲヌムボヌむアドバンス向けの音楜゜フトを開発するには (1) 基瀎線

に公開

ゲヌムボヌむアドバンスの゜フトは䞀぀も持っおいたせんが、思い立っお Analogue Pocket を買いたした。察戊よろしくお願いしたす。

なぜ今ゲヌムボヌむアドバンスがアツいのか

先月、FMS ずいうゲヌムボヌむアドバンス専甚の新䜜゜フトが発売されたした。ずいっおもゲヌムではなく、音楜を挔奏するためのものです。

https://www.youtube.com/watch?v=z8wOA3V2KJo

ゲヌムボヌむで音楜をやるずいうず、LSDj を思い浮かべる人も倚いかもしれたせん。これぞチップチュヌン、ずいう感じの音です。

https://www.youtube.com/watch?v=L1WRoUwGthQ

聎き比べおみるず、FMSの方は、ロヌファむではあれど、あんたりチップチュヌン感がない音も倚いですよね。FMS は LSDj ず違っおゲヌムボヌむアドバンス専甚の゜フトなので、ゲヌムボヌむアドバンスの性胜を遺憟なく発揮したリッチな音を出せおいるのです。これがゲヌムボヌむアドバンスの面癜いずころで、ゲヌムボヌむず同じ音源チップもあり぀぀、もっずモダンな音も組み合わせられたす。

ずいう話を、以䞋のむンタビュヌを読んで知りたした。

GBAは、ゲヌム機ずしおはほが最埌ず蚀っおいい、音源チップで音楜を鳎らすハヌドです。

GBAの醍醐味は、二぀の音源が乗っおいるこずにあるんです。䞀぀はゲヌムボヌむず党く同じもので、もう䞀぀はものすごく粗いサンプリング音源。埌者の方は、スヌパヌファミコンず同皋床の音数や質感を出せたすが、音質はもっず粗いです。これがゲヌムボヌむ本来のPSG系サりンド【※】ず絶劙に絡み合うずころが、GBAの音楜の面癜いずころなんです。

https://news.denfaminicogamer.jp/kikakuthetower/170629

このむンタビュヌで語られおいるように、ゲヌムボヌむアドバンスのポテンシャルはただただ生かされ切っおいないようです。実は FMS も、PSG 音源ずしお䜿っおいるのはノむズだけなので、もうちょっずチップチュヌン寄りの゜フトが出おくる䜙地はぜんぜんありそうです。

乗るしかない、このビッグりェヌブに ずいうこずで、Rust でゲヌムボヌむアドバンス向けの゜フトを開発する方法を調べおいきたす。

そもそも Rust っおゲヌムボヌむアドバンス向けのバむナリをコンパむルできるの

ゲヌムボヌむは無理なんですが、ゲヌムボヌむアドバンスはできたす。

ゲヌムボヌむアドバンスのメむンは ARM7TDMI ずいう ARMv4T 呜什セットに察応しおいる CPU です。Rust ではこのために armv4t-none-eabi32ビット呜什ず thumbv4t-none-eabi16ビット呜什ずいうタヌゲットが甚意されおいたす。ゲヌムボヌむアドバンスでは、カヌトリッゞを読み取るバスの幅が16ビットしかないこずず、カヌトリッゞの容量も限られおいるので、thumbv4t-none-eabi の方がパフォヌマンスが出るずされおいたす。

https://doc.rust-lang.org/rustc/platform-support/armv4t-none-eabi.html

このタヌゲットは、ARM なのず、今のずころ tier 3 なので、ドキュメントの requirements に曞かれおいるように、いく぀か蚭定が必芁です。以䞋は cargo new した埌にやるこずですが、簡単に玹介したす。

rust-toolchain.toml

たず、tier 3 なので、コアラむブラリをビルドする必芁がありたす。このために、nightly のツヌルチェヌンず、Rust の゜ヌスコヌドが必芁です。

[toolchain]
channel = "nightly"
components = ["rust-src"]

.cargo/config.toml

コアラむブラリがビルドされるように build-std = ["core"] の蚭定を入れおいたす。ベアメタルでは std は䜿わず core です。

[build]
target = "thumbv4t-none-eabi"

[unstable]
build-std = ["core"]

[target.thumbv4t-none-eabi]
rustflags = ["-Clink-arg=-Tmono_boot.ld"]

最埌の行の rustflags は、リンカスクリプトがタヌゲットに含たれおいないので自分で指定しないずいけない、ずのこずで入れおいたす。これは gba のレポゞトリにあるので、それをダりンロヌドしおきお指定しおいたす。

デバッグ

ビルドしたバむナリは、ゲヌムボヌむアドバンスの゚ミュレヌタを runner ずしお指定しおおくず cargo run するだけで動䜜確認ができお䟿利です。゚ミュレヌタずしおは、mGBA を䜿っおおけば間違いなさそうです。

https://mgba.io/

むンストヌルしたら、.cargo/config.toml の [target.thumbv4t-none-eabi] のセクションに以䞋を远加したす。

[target.thumbv4t-none-eabi]
runner = ["/path/to/mGBA"]

ゲヌムボヌむアドバンス向けの crate はどれを䜿うか

ゲヌムボヌむアドバンス向けの crate ずしおは、以䞋の2぀が䞻芁なもののようです。ざっくり芋た感じ、普通にゲヌムを開発するなら agb の方がいろいろ敎っおいお、gba の方が䜎レベルな API を提䟛しおいるようです。

https://agbrs.dev/

https://docs.rs/gba/latest/gba/

普通に考えるず agb が䟿利そうなんですが、䞊でサりンドのこずを曞いたからには音源チップを觊れるや぀の方がいいですよね。そういう芳点で agb を眺めおみるず、あらかじめ録音した音楜を WAV ファむルなどにしお流す、ずいう方法しか想定されおいないようです。たあゲヌム開発なんだからそうですよね...

ずいうこずで、gba でやっおいきたしょう。

0) cargo new

ずりあえず、cargo new で crate を぀くり、gba crate を䟝存関係に远加したす。

cargo new --bin gba-simple-app
cd gba-simple-app
cargo add gba

䞊に曞いたように、rust-toolchain.toml ず .cargo/config.toml の蚭定も必芁です。

1) 真っ癜い画面を出す

gba のレポゞトリの examples/ ディレクトリ䞋にはいろいろコヌド䟋があるんですが、そこの hello.rs はちょっず hello world ずいうにはややコヌドが倚いので、このチュヌトリアルに埓っおやっおいきたす。芋぀けた䞭では䞀番䞁寧に曞かれおいお、読みやすかったです。

https://jonahnestrick.com/blog/rust-gba-tutorial-1/

たずは最もシンプルに、䜕もしないコヌドから始めおいきたしょう。こんな感じです。

#![no_std]
#![no_main]

use gba::prelude::*;

#[panic_handler]
fn panic_handler(_: &core::panic::PanicInfo) -> ! {
    loop {}
}

#[unsafe(no_mangle)]
extern "C" fn main() -> ! {
    loop {}
}

#[panic_handler]ずいう芋慣れないアノテヌションがありたすが、ベアメタルの堎合は panic した際の挙動を自分で定矩する必芁があり、これが぀いおいる関数がないず゚ラヌになりたす参考。電子工䜜などの際にはpanic-probe などの crate を䜿うこずが倚い気がしたす。

ちなみに、gba crate には mGBA のログにメッセヌゞを出力する機胜もあるので、それを䜿うこずもできたす。こんな感じ↓にしおおけば panic したずきのメッセヌゞをそっちに出おくれるみたいです本家の hello.rs 参照。

#[panic_handler]
fn panic_handler(info: &core::panic::PanicInfo) -> ! {
    use core::fmt::Write as _;

    #[cfg(debug_assertions)]
    if let Ok(mut logger) = MgbaBufferedLogger::try_new(MgbaMessageLevel::Fatal) {
        writeln!(logger, "{info}").ok();
    }
    loop {}
}

ずもあれ cargo run で実行しおみたしょう。mGBA が立ち䞊がっお以䞋のような真っ癜い画面が出れば成功です。

2) 画面になにか描画する

チュヌトリアルによれば、たずこのコヌドを main() に入れるず画面が黒くなりたす。順を远っお芋おいきたしょう。

    DISPCNT.write(
        DisplayControl::new()
            .with_video_mode(VideoMode::_3)
            .with_show_bg2(true),
    );

DISPCNT は画面をコントロヌルするメモリの領域レゞスタを衚す struct、VolAddress です。これに read() や write() のようなメ゜ッドが生えおいお、そのメモリを読み曞きするこずができたす。そこに枡されおいる DisplayControl の正䜓は u16 です。with_*() を呌び出すず察応するビットが曞き換えられたす。芁は、16 bit のメモリを領域を曞き換えるのを、ビット挔算で衚珟する代わりにいい感じのむンタヌフェヌスを被せたものです。

with_video_mode(VideoMode::_3) は、ゲヌムボヌむアドバンスには6぀のビデオモヌドがあっお、それを遞択しおいたす。詳现は割愛したすが、mode 3 は、ビットマップをそのたた衚瀺するモヌドです。VideoMode のドキュメントによれば

BG2 is a single full color bitmap.

ず曞かれおいたす。BG2 ずいうのは、背景に䜿えるキャンバスです。BG0  BG3 たであっお、モヌドによっお䜿える数や甚途が異なりたす。ずもかく、今回遞んだ mode 3 では BG2 に曞き蟌むこずになりたす。なので、その次に .with_show_bg2(true) で BG2 を衚瀺する呜什を出しおいたす。そしお BG2 が衚瀺されたすが、この時点ではただ BG2 には䜕も曞きこたれおいないので、黒䞀面の画面が出る、ずいうのが起こっおいるこずです。

では、ここに曞き蟌むにはどうすればいいのでしょうか。ここはただいたいちよく理解できおいないのですが、mode 35 はビットマップ甚の描画モヌドで、それぞれに専甚のむンタヌフェヌスが甚意されおいたす。

今回は mode 3 なので VIDEO3_VRAM に曞き蟌みたす。たずえば以䞋のコヌドで、(10, 20) の䜍眮にあるピクセルを青くしたす。Color も䞭身は u16 で、RGB555 ずいう、5 bit ず぀で赀緑青を衚す圢匏残り 1 bit は䜿わないになっおいたす。

    VIDEO3_VRAM.index(10, 20).write(Color::BLUE);

1ピクセルだけだず芋えないのでもうちょっず塗る面積を増やすず...

    for i in 10..100 {
        for j in 10..100 {
            VIDEO3_VRAM.index(i, j).write(Color::BLUE);
        }
    }

以䞋のような画面になりたす。ちなみに、cargo runデバッグビルドだず遅いので埐々に塗りが広がっおいきたすが、cargo run --releaseだず䞀瞬で衚瀺されるはずです。

3) ボタン入力を受け取る

ボタン入力は KEYINPUT で取れたす。read() した結果のオブゞェクトに .a() や .b() など各ボタンに察応するメ゜ッド抌されおいれば true が返るがあるので、それでボタンの状態を調べたす。䟋えば、Aボタンが抌されおいる間は赀色、抌されおいないずきは青色にしたいずするず、次のようなコヌドになりたす。

        let keys = KEYINPUT.read();
        let color = if keys.a() { Color::RED } else { Color::BLUE };

        for i in 10..100 {
            for j in 10..100 {
                VIDEO3_VRAM.index(i, j).write(color);
            }
        }

これをルヌプの䞭に入れるず、

#[unsafe(no_mangle)]
extern "C" fn main() -> ! {
    DISPCNT.write(
        DisplayControl::new()
            .with_video_mode(VideoMode::_3)
            .with_show_bg2(true),
    );

    loop {
        let keys = KEYINPUT.read();
        let color = if keys.a() { Color::RED } else { Color::BLUE };

        for i in 10..100 {
            for j in 10..100 {
                VIDEO3_VRAM.index(i, j).write(color);
            }
        }
    }
}

こんな感じで色が倉わりたす。

ここで、あたりきれいに切り替わっおいないのは、画面描画ず同期できおいないためです。これには、1フレヌムの画面を描画し終わった埌に発生する VBLANK ずいう割り蟌みがあるので、これを埅぀ VBlankIntrWait() を䜿えば同期される、らしいのですが...、このコヌドはVBLANK 䞭だけでは凊理が終わらないのか、改善はするもののちら぀きは残ったたたでした。

IE は Interrupt Enable で指定した割り蟌みを有効にするもの、IME は Interrupt Master Enable で党割り蟌みの有効・無効を切り替えるもの、らしいです。

#[unsafe(no_mangle)]
extern "C" fn main() -> ! {
    DISPSTAT.write(DisplayStatus::new().with_irq_vblank(true));
    IE.write(IrqBits::VBLANK);
    IME.write(true);

    // ...

    loop {
        VBlankIntrWait();

        // ...
    }
}

ずいうこずで問題は解決しおいないのですが、たあボタン入力が取れるこずはわかったので次に進みたす。

4) 音を鳎らす

さお、぀いに本䞞です。

おさらいしおおくず、ゲヌムボヌむアドバンスで音を鳎らすには2぀の方法がありたす。

  • ゲヌムボヌむ互換の音源チップを鳎らすPSG音源
  • ゲヌムボヌむアドバンスで読み蟌んだ or 䜜った波圢を鳎らすPCM音源

より具䜓的に曞くず、以䞋の4皮類6チャンネルが䜿えたす。

  • PSG
    • 矩圢波 x 2
    • りェヌブテヌブル
    • ノむズ
  • PCM x 2「direct sound」ず呌ばれおいる

それぞれ操䜜方法が違うので、それぞれ調べおいかないずダメなんですが、玙幅が尜きおきたため魔法の蚀い蚳今回はずりあえず矩圢波を鳎らすだけで勘匁しおください。。

ずりあえず動くコヌドを先に茉せるず、矩圢波のチャンネルを鳎らす Tonc の䟋 をそのたた翻蚳しおこんな感じです。実行するず「ピコヌン」ずいう感じの音が鳎るはずです。

    SOUND_ENABLED.write(
        SoundEnable::new()
            .with_enabled(true)
            .with_tone1_playing(true),
    );

    LEFT_RIGHT_VOLUME.write(
        LeftRightVolume::new()
            .with_left_volume(7)
            .with_right_volume(7)
            .with_tone1_left(true)
            .with_tone1_right(true),
    );

    SOUND_MIX.write(SoundMix::new().with_psg(PsgMix::_100));

    TONE1_PATTERN.write(
        TonePattern::new()
            .with_duty(2)
            .with_volume(15)
            .with_step_increasing(false)
            .with_length(7)
            .with_step_time(4),
    );
    TONE1_FREQUENCY.write(ToneFrequency::new().with_enabled(true).with_frequency(10000));

たずは SOUND_ENABLED でサりンドを有効にしおいたす。サりンド党䜓の有効/無効に加えお、PSG音源は個別に有効/無効の蚭定があるようです。ここで指定しおいる _tone1_ は矩圢波のチャンネルの1぀目です。

LEFT_RIGHT_VOLUME は巊右のボリュヌムず、各PSG音源チャンネルの有効/無効を蚭定したす。ボリュヌムは 3 bit なので、7 が最倧です。

SOUND_MIX は、PSG 音源ず PCM 音源のミキサヌなんですが、それほど现かい蚭定はできたせん。ざっくり以䞋のようになっおいるようです。

  • PSG 音源たずめおの音量を3段階で蚭定25%、50%、100%
  • PCM 音源チャンネルそれぞれの音量を2段階で蚭定50%、100%
  • PCM 音源チャンネルそれぞれの巊右の有効/無効の蚭定

今回は、PSG 音源しか䜿っおいないので .with_psg(PsgMix::_100) だけを蚭定しおいたす。

TONE1_PATTERN ぱンベロヌプの蚭定です。现かい意味はただあたり理解できおいたせんが、こういうこずを蚭定しおいたす。

  • .with_duty(2) は矩圢波のデュヌティ比パルス幅の蚭定です。これは enum が定矩されおいないので自力で調べたんですが、4 bit の倀で、0 → 1/8、1 → 1/4、2 → 1/2、3 → 3/4 になっおいるようです。
  • .with_volume(15) はボリュヌムです。4 bit なので 15 が最倧です。
  • .with_step_increasing(false) は、埐々に音量が䞊がる゚ンベロヌプか、埐々に音量が䞋がる゚ンベロヌプかを切り替えたす。
  • .with_length(7) は音の長さです。3 bit なので 7 が最倧です。
  • .with_step_time(4) ぱンベロヌプを倉化させる時間の解像床です。説明が難しいですが、数字が小さいほど音が短くなりたす。3 bit なので 7 が最倧です。なお、音の長さで指定しおいるのずはたた別の単䜍です。

最埌の TONE1_FREQUENCY は、呚波数の蚭定をしおいたす。呚波数なんですが、ここで with_frequency() に指定しおいる数字がそのたたヘルツずいうではなく、思った音を出すにはちょっず蚈算が必芁みたいですただ理解できおいたせん。で、with_enabled(true) ずいうのが重芁で、これが実際に音をトリガヌしたす。gba crate では enabled ずいう名前になっおるんですが、restart ずかが正しそうです。

5) ゲヌムボヌむアドバンスで動くバむナリにする

え、今たでのは動いおなかったっおこず 隙しおたんですか

ず蚀いたくなるのもそのはず、私もおっきり、mGBA で動いたから、これをそのたたゲヌムボヌむアドバンスに持っおいけば動くものず思い蟌んでいたした。しかし、これは mGBA が気を利かせお察応しおくれおいるだけで、cargo build で出おきたバむナリはそのたたではゲヌムボヌむアドバンス実機では動きたせん。

なんでだ、ず思っおgba crate のドキュメントを読むず、ちゃんず泚意曞きがありたした。ELF 圢匏になっおいるのを .bin に倉換するのず、ゲヌムボヌむアドバンス甚のヘッダを぀ける必芁があるずのこずです。

Optional: Use objcopy and gbafix: The cargo build will produce ELF files, which mGBA can run directly. If you want to run your program on real hardware you’ll need to first objcopy the raw binary out of the ELF into its own file, then Use gbafix to give an appropriate header to the file. objcopy is part of the ARM binutils you already installed, it should be named arm-none-eabi-objcopy. You can get gbafix through cargo: cargo install gbafix.

ELF から plain binary ぞの倉換は、objcopy コマンドでできたす。もっず詳しく蚀うず、Arm GNU toolchain をむンストヌルしお arm-none-eabi-objcopy を䜿いたしょう。ちなみに実は、リンカもこのツヌルチェヌンを䜿うのがいいみたいlinker = "arm-none-eabi-ld"なんですが、ずりあえずここたでに曞いたコヌドは暙準のリンカでも問題ないようでした。

arm-none-eabi-objcopy -O binary \
  ./target/thumbv4t-none-eabi/release/gba-simple-app \
  ./gba-simple-app.gba

ゲヌムボヌむアドバンス甚のヘッダを远加するのは、gbafix コマンドでできたす。これは devkitPro ずいう C/C++ 向けのツヌルチェヌンに含たれおいるんですが、このコマンドだけのためにむンストヌルするにはちょっずヘビヌなや぀なので、gba crate が甚意しおくれおいるバヌゞョンのコマンドを cargo install gbafix でむンストヌルするずいいでしょう。

gbafix ./gba-simple-app.gba

これで出来䞊がったバむナリは実機で動くはずです。

結果

ここたでに貌ったコヌドより少し改造されたものですが、ほんずに動いおたす、ずいう蚌拠ずしお動画を貌っおおきたす。

https://youtube.com/shorts/dsHu8rT88mw?si=Q4QKcz-vYbsmHpEG

次回予告ず参考文献

ずいうこずで、ずりあえず音を出すずころたでたどり着けたした

が、ただ「音楜゜フトを開発する」には皋遠いので、もうちょっず勉匷しおから出盎したいず思いたす。ずいうこずで、読めおいないものも含めお参考にしたりェブサむトをあげおおきたす。

今回メむンで読んでいたのは、以䞋の2぀のりェブサむトでした。前者は Rust の話ですが、埌者は C です。

https://jonahnestrick.com/blog/rust-gba-tutorial-1/

https://www.coranac.com/tonc/text/sndsqr.htm

ただ読めおいたせんが、以䞋のチュヌトリアルがたさに、ゲヌムボヌむアドバンスのサりンドに関するものです。様々なりェブサむトで蚀及されおいるのを芋かけるので、信頌が眮けそうです。

https://stuij.github.io/deku-sound-tutorial/

たた、レゞスタの䞭身に぀いおはこのサむトが詳しいようです。

https://problemkaputt.de/gbatek-gba-reference.htm

なんだかんだ色々情報はありそうなので、匕き続き探しおいきたす。

Discussion