動機 #
- 毎回ISOやSDを弄るのが面倒
- 曲数が少なすぎる
PMEx(?)とかいうModでは独自の拡張方法が実装されているようだがスマブラXのハックの用語、エコシステム等の知識がないと面倒そうだったので利用は断念した。 - ゲーム用のフォーマットに変換すると音質が落ちる
実装 #
実は2023年10月ぐらいにとりあえずの実装は済んでおり、使用上最低限の動作はするので改修することなく使い続けているが当時の記憶を遡ってまとめておく。
どんな機能を実装したかったか、何が必要だったか、どのように達成したか #
1.DolphinでエミュレートされているスマブラX内のシーン(キャラ選択、ステージ選択、リザルト画面、対戦)に合わせて曲を再生する #
ゲーム内の状態を検知するためにゲーム内の状態変数のようなものを定期的に取得しなければならない。
Dolphinのメモリを読むために🔗py-dolphin-memory-engineを使用した。
PythonライブラリだがC++をバインドしたものなのでコアのC++のコードを必要な部分だけ引っこ抜いて利用させてもらった。
ドキュメントは確か無かったが関数名でなんとなく機能と使い方が分かったのでなんとかなった。
曲の再生はQtを使えばGUIもついてくるので後のことを考えてそちらを利用しようと思ったが、そこまで時間をかけられなかったのでGUIは諦めた。
よって、Cライブラリの🔗miniaudioを利用した。
ドキュメントとサンプルを見ながら試行錯誤してなんとかなった。
py-dolphin-memory-engineもminiaudioもWindows, Linuxで動作する。
ゲーム内の状態を検知するのについては、どっかしらに状態変数があると思うが、Dolphin付属のツールでメモリを数回サーチしても見つからなかったので、値が割れている曲IDに注目して現在再生中の曲IDを検索してメモリのアドレスを割り出した。
アドレスはこれ。
auto static constexpr CURRENT_MUSIC_ID_ADDRESS = 0x90e60f06 - 0x80000000;
RSBEを使用しているがRSBJでも正常にツールが動作したのでアドレスは変わらないらしい。(嬉しい誤算?)
2.ゲーム内で再生されている曲IDに合わせて流す曲を設定できるようにする #
コードでべた書きしても良いのだが、後々曲を追加したりすることを考えるとコンフィグを読み込むようにしたほうが楽と思いtomlでプレイリストを実現することにした。
tomlの読み取り等にはC++ライブラリの🔗toml++を利用した。
一応読み込み時にコンフィグに書かれた曲ファイルが存在しなかった場合等のassertionを突っ込んである。
ココらへんもちゃんとやりたかったがRustに慣れてしまってtomlの処理がゲロ面倒だったのでリファクタもしてない。
Rustなら🔗structを定義するだけでserdeを使ってtomlの読み書きができる。
これに限らずC++のほうが手数を求められるので趣味では選択肢がある場合、Rustを利用することが多い。
CMakeを書く必要がないし環境整えるのも楽だし。
コンフィグはこんな感じになった。
[musics]
# ここで曲を登録する
# 書式は
# [曲ID(重複不可), "音楽ファイルへの相対パス", 曲スタートオフセット, 曲終了オフセット],
# もしくはループを設定する場合
# [曲ID(重複不可), "音楽ファイルへの相対パス", 曲スタートオフセット, 曲終了オフセット, ループ開始オフセット, ループ終了オフセット],
# となる
# どちらで登録してもループは必ず行われる
# 曲スタートオフセットは初めから再生したい場合 -1
# 曲終了オフセットは最後まで再生したい場合 -1
# 両方-1だと最初から最後まで再生される
# 組み合わせは自由
# 注意: ファイル、フォルダ名に日本語等の文字を使わないこと
# オフセットはpcm frame
musics=[
[5, "./temp/04.hoshi-no-bohyou -instrumental-.mpga", -1, -1],
[6, "./temp/01.Before I Rise -instrumental-.mpga", -1, -1],
[7, "./temp/02.Burn My Universe -instrumental-.mpga", -1, -1],
[8, "./temp/03.Everlasting Night -instrumental-.mpga", -1, -1],
[9, "./temp/07.Particle Effect -instrumental-.mpga", -1, -1],
[10, "./temp/08.gingaryodan -instrumental-.mpga", -1, -1],
[11, "./temp/11.White Spell -instrumental-.mpga", -1, -1],
[12, "./temp/02.zeitakunakanjyou -piano ver.-.mpga", -1, -1],
[13, "./temp/04.natsukikyuu -memories ver.-.mpga", -1, -1],
[14, "./temp/05.usobakarinosekaide.mpga", -1, -1],
[15, "./temp/08.henbousurudaichi -zyo-.mpga", -1, -1],
[16, "./temp/09.henbousurudaichi -ha-.mpga", -1, -1],
[17, "./temp/10.henbousurudaichi -kyu-.mpga", -1, -1],
[18, "./temp/16.tropical land.mpga", -1, -1],
[19, "./temp/1-25 - Fancy ShopX'mas.flac", -1, -1],
~~~
.
.
.
~~~
]
[menu]
# target brawl music ids
# 対象のスマブラ内の曲ID
target_ids=[0x26FA,0x26FB,0x281c,0x26f9]
# music ids to play
# 流す曲IDを設定する IDは一番上のほうで登録したID
musics=[12,13,14,18,19,3003,3010,3031,5001,5002,5003,5004,5005,5006,5007,5008,5009,5010,5011,5012,5013,5014,5015,5016,5017,5018,5019,5020,5021,5022,5023,5024,5025,5026,5027,5028,5029,5030,5031,5032,5033,5034,5035,5036,5037,5038,5039,5040,5041,5042,5043,5044,5045,5047,6001,6011,6016,6021,
7023,7024,7025,7026,7027,7028,7029,7030,7031,7032,7042,7049,2008,7050,7105,7108,7110,7117,7122,7207,7208,7246,7253,7258,7261,7265]
[FD]
target_ids=[0x26FD,0x27ed,0x27ef,0x2817,0x281d]
musics=[1,2,3,4,5,6,7,8,9,10,11,15,1003,1004,3002,3004,3008,3012,3014,3016,3017,3021,3023,3024,3028,3039,3046,3054,4002,4004,4005,4006,4007,4008,4012,4013,4014,4016,4019,4020,4025,4028,4034,4035,6002,6003,6004,6012,6019,6023,6025,6026,6029,6034,6036,6039,6041,6042,6044,6045,6049,6051,6054,
7001,7002,7013,7015,7051,7201,7205,7213,7215,7218,7225,7239]
.
.
.
3.ネットプレイ時に同じ設定ファイルと曲を利用していれば、同じ曲が流れるようにする #
Mersenne twister(メルセンヌ・ツイスタ)の性質を利用してツールの起動時の時刻がHour単位で同時刻だった場合、同じ乱数が生成されるので選曲を同期することができる。
日本国内の人間としかプレイする機会がないので、タイムゾーンのズレは考慮していない。
PCの時計がずれていると違う曲が流れたりする。
まあ良いでしょう。
現在時刻で初期化。
void Playlist::init_mt() {
// seed mt with current time
std::time_t t = std::time(0); // get time now
std::tm *now = std::gmtime(&t);
std::vector<int> buf{};
buf.push_back(now->tm_year);
buf.push_back(now->tm_mon);
buf.push_back(now->tm_mday);
buf.push_back(now->tm_hour);
auto seed = std::seed_seq(buf.begin(), buf.end());
std::mt19937 mt(seed);
m_mt = mt;
}
こんな感じに現在再生されている曲IDに対応するプレイリストからランダムで1曲選んでいる。
auto const &unique_music_ids = m_brawl_music_id_to_unique_ids_map[music_id];
if (unique_music_ids.size() == 0) {
return std::nullopt;
}
// choose one randomly
std::uniform_int_distribution<> dist(0, unique_music_ids.size() - 1);
auto const unique_music_id = unique_music_ids[dist(m_mt)];
if (!m_music_map.count(unique_music_id)) {
return std::nullopt;
}
auto entry = m_music_map[unique_music_id];
動作 #
問題点等 #
ゲーム内の現在再生されている曲のIDをメモリから読んでそれに応じて曲を再生しているので、メモリの内容が変わったが、ゲーム内では曲が開始されていない状態でも問答無用で再生が開始されてしまう。
動画を見れば分かると思うが試合が始まるより早く曲が流れていたりする。
慣れれば気にならないが。
ループの挙動が若干おかしい。
configから曲の途中からループの設定もできるようにしたが、miniaudioで無限にループさせる方法がいまいちわからない。
もしかしたら曲終了時のcallbackがあるかもしれなくてそこらへんでどうにかできそうな気もするが…。
今の所直すつもりはない。
公開しようと思ったが身内向けツールでプレイヤーもいなくて需要もないだろうしコードも汚いのでやめておくことにした。