S98Player for iPhone 3.0をリリースしました

S98Player

前回の 2.0 から1年ちょっと、今回の主役は対応形式です。X68000のMDXとPC-98のFMPが加わって、このアプリとしては過去いちばん大きな更新になりました。

S98Player for iPhone – App Store

今回は新機能の紹介に加えて、後半で「どうやって開発しているか」も書きます。前回の記事の最後に「Claude CodeによるAI vibe codingが役に立った」と書きましたが、あれから1年経って、vibeと呼ぶには少し体系立ったやり方に育ってきたので、その話です。

対応形式が2つから5つに増えました

3.0 の一番の目玉はこれです。これまでのS98・PMD(PC-98)に加えて、次の形式が再生できるようになりました。

  • MDX(X68000).mdxを再生できます。ADPCMを使う曲は.pdxを同じフォルダに置いてください。ドライバは portable_mdx(MXDRV)です。PCM8の実機風ミキシングにも対応(既定でオン)。
  • FMP(PC-98).opi / .ovi / .ozi に加えて、.m86 / .m26(YU-NOのPlay6)も再生できます。ADPCM・PPZ8を使う曲は .pvi / .pzi を同じフォルダに。ドライバはfmplayerのfmdriverです。
  • MFM / MF2 — FMPの系統ですが既存のドライバとはコマンド体系が違って鳴らせなかったため、これだけは専用のシーケンサを書き起こしました。実機のS98ログと突き合わせながら50コマンドを解析し、FM全チャンネルが実機と一致するところまで詰めてあります。ついでにOPのボーカル(生PCM)もそのまま再生できます。

チャンネルモニタ(鍵盤表示)とパートのマスクは新形式でもひととおり効きます。MDXはPCM行にも鍵盤が出ますし、FMPはPPZ8・FM3拡張パートまで表示します。

音源エミュレータはymfmに統一しています

ここはこだわった点です。MDXやFMPのドライバには、それぞれ専用の音源エミュレータが付いています(MXDRVならX68Sound、fmplayerならlibopna)。そのまま使えば早いのですが、このアプリではFM音源部をすべてymfmに載せ替えてあります。S98・PMDも含めて、5形式すべてが同じymfmで鳴っています(MDXのADPCMまわりだけはX68Soundの実装をそのまま使っています)。

わざわざ載せ替えたのは、チップの出口から先を作り込みたかったからです。実機の音は、音源チップが計算した波形がそのままスピーカーから出るわけではなく、YM3016やYM3012といったDACの癖、出力段の回路、フィルタの遮断周波数を通って、初めて「あの音」になります。音源エミュレータが形式ごとにバラバラだと、この作り込みを形式の数だけ繰り返すことになりますが、ymfmに揃えてあれば 1 回作れば全形式に効きます。次の「機種ごとの音の再現」は、この統一が土台になっています。

機種ごとの音の再現

対応形式が増えたのに合わせて、「どの実機として鳴らすか」を選べるようにしました。PC-98 / PC-88 / X68000 / X1turboZそれぞれにDAC・出力段・遮断周波数・音源別ミックスを持っています。機種はS98のsystemタグから自動で決まり、タグが無ければ音源の顔ぶれから推し量ります(OPMだけならX68000、など)。X68000とX1turboZのフィルタ特性は回路図とデータシートで裏づけを取りました。

スポンサーリンク

表示まわり

  • シークバーが波形になりました。「イントロ + 1ループ」を描いて、何周目かを色の濃さで表します。曲のどこを聴いているのかが一目で分かります。
  • iPhoneを横にすると2ペイン表示になります。左に鍵盤(全高)、右にスペクトラムアナライザと波形バー。
  • スペクトラムアナライザはセグメント表示 + ピークホールドに刷新しました。低域の間隔がおかしかったのも直しています。
  • 曲情報はファイルにある内容を加工せず全部出すようにしました。PCM バンク(.pdx や .pvi)が欠けているときは ⚠️ で知らせます。欠けたパートは無音で鳴るので、聴いているだけでは「こういう曲なのかも?」と区別がつかないためです。
  • アルバムと曲の情報をアプリから編集できるようになりました。これまで表示名などを変えるにはSettings.plistをMacなどで手で編集してフォルダに置くしかありませんでしたが、アプリの中で完結します。アルバムアートも写真やファイルから選べます。

再生まわりの修正と省電力

地味ですが、日常的にはこちらが効くかもしれません。

  • アラームや通話のあと、画面を開かなくても再生が戻るようになりました(以前は復帰処理がフォアグラウンド復帰でしか走らず、画面を開くまで無音でした)
  • コントロールセンターで出力先を切り替えても再生が止まらなくなりました
  • ロック画面の再生/一時停止が指示どおりに効くようになりました
  • チャンネルモニタを表示したフルプレイヤー画面での CPU 使用率を40% → 26%に下げました(iPhone 15 Pro Max / Release 実測)。電池の持ちに効きます

開発の仕方(ここからが本題)

さて、ここからは開発の話です。

この夏に開発を再開してから、2ヶ月足らずで2.1 → 2.2 → 2.3 → 3.0 と4回リリースしました。3.0だけ見ても、2.3からの半月あまりで209コミット。この間にエミュレーションドライバを 2 本(MDXとFMP)持ち込み、専用シーケンサを1本書き起こし、機種別の音再現を入れ、大きめのリファクタリングも済ませています。開発はほぼすべてClaude Codeとのペア作業です。

1年前は「AI vibe coding」と書きました。動くものは驚くほど速くできるのですが、音のアプリでこれをやると、すぐに壁に当たります。変更によって意図せず音が変わってしまったかどうか、耳では分からないのです。エミュレータのコードを1行変えたとき、3000曲のうちどこかの1曲だけ音が濁っていても、まず気づけません。気づけないから触るのが怖くなり、怖いからコードが硬直する。AIに任せる・任せない以前の問題です。

なので3.0の開発では、新機能より先に「変更が正しかったかを自動で確かめる仕組み」を作りました。振り返ると、気軽に機能を増やせたのはほぼこれのおかげです。いくつか紹介します。

出音の回帰テスト — 3323曲をハッシュで突き合わせる

音はdiffできない、と思いがちですが、レンダラをオフラインで直接叩けばdiffできます。各形式のドライバと音源部をCLIから叩く小さなハーネスを作り、手元のライブラリ全曲(現在3323曲)をPCMにレンダリングして、そのSHA-256ハッシュを「基準」として記録しておきます。

./tools/ab-regression.sh --build --record   # 基準を取り直す(意図して音を変えたとき)
./tools/ab-regression.sh                    # 突き合わせ(差があれば終了コード 1)

C/C++側に手を入れたら必ずこれを通します。1バイトでも音が変われば検出されるので、「リファクタリングのつもりが音が変わっていた」は原理的に起きません。逆に、意図して音を変えたときは差分の曲リストが出るので、「この変更でこの曲群の音が変わった。妥当か?」という判断だけに集中できます。

工夫が2つあります。ひとつは、基準ファイルにはどの曲がどのハッシュかだけを入れること。楽曲そのものは市販ゲーム音楽なのでリポジトリには置けませんが、ハッシュなら置けます。もうひとつはカバレッジの穴を合成データで埋めること。手元のライブラリにはOPL系(YM3526/YM3812など)の曲が 1 曲も無く、そのままではOPLのコードを壊してもこのテストは何も検知しません。そこでOPL系チップを鳴らすS98をスクリプトで生成して基準に混ぜてあります。

本家のコードは書き換えずに取り込む

ドライバや音源エミュレータは、先人が公開しているオープンソース実装(PMDWin / portable_mdx / fmplayer / ymfm)を取り込んで使っています。本家は今も更新されることがあるので、新しい版が出たとき追従できるかどうかは、こちらでどれだけ書き換えたかで決まります。方針は2段構えです。

  • ymfm(音源エミュレータ本体)は1文字も変えない。 音量調整などを入れたくなっても、ラッパー側で吸収します。実際、過去にymfm内に入れてしまった変更をラッパーへ移した経緯があります。
  • 書き換えが避けられないドライバ(PMDWin / portable_mdx / fmplayer)は、本家の原本を無改変でリポジトリに並べて置き、比較スクリプトで差分を管理します。意図した改変は理由つきの一覧に載せ、一覧に無いファイルに差分が出たらスクリプトが失敗します。「本家由来のファイルをうっかり触った」事故に、その場で気づけます。

本家の新しい版に上げるときも、先ほどの回帰テストがそのまま効きます。ymfmを最新版に上げたときは、更新前後で PMD 45曲 + S98 58曲がバイト単位で一致することを確認してから取り込みました。ymfmはエミュレーション挙動が変わる修正を普通に含むので、「上げたら音が変わっていた」を黙って通さない仕組みです。

スポンサーリンク

バグを直したら、その「類型」を網にする

前提をひとつ。このアプリは形式ごとのドライバを起動時に1つずつ作り、曲をまたいで使い回します。曲を替えるたびに作り直すのではなく、同じインスタンスに次の曲を読み込ませる作りです。この作りには持病があって、読み込み時に消し忘れた状態が1か所でもあると、前の曲の値が次の曲に化けて出ます。実例だと、S98のsystemタグが前の曲のまま残り、機種の判定が狂って出音そのものが変わっていた、というのがありました。1ファイルだけ読むテストでは原理的に出ない形のバグで、開発中に何件か踏みました。

個別に直して終わりにせず、類型ごと網にしました。「まっさらな状態でBだけを読んだ結果」を正解として、「AのあとにBを読んだ結果」が一致するかを比べるテストです。この形にしておくと、比べる項目を列挙しないので、あとから足したメタデータの消し忘れにも自動で効きます。Swift側・C側の両方に同じ形の網があります。

実機でしか出ないバグは、証拠を取れるようにする

「一時停止のまま放置して自動ロックすると復帰しない」のようなバグは、Macに繋いだiPhoneでは原理的に再現できません。繋いでいる間、iOSは自動ロックもサスペンドもしなくなるからです。ログを見ようとすると再現せず、再現させるとログが見えない。

そこで、重要なログはアプリのDocuments配下のファイルにも書くようにしました。プロセスが落ちても残るので、繋がずに再現させたあとでファイルを吸い上げて調べられます。境目のログには「そのとき再生中だったか一時停止だったか」まで残します。これが無いログは、この種の切り分けには使えないというのが実感です。

ルールは CLAUDE.md に書き溜める

これらの仕組みは、作っただけでは使われなくなります。リポジトリ直下のCLAUDE.mdに、「C/C++ を触ったら出音の回帰テストを通す」「ymfmの中は直さない」「バージョンを上げたらローンチ画面も直す(3.0で実際に漏れた)」といった約束事を、理由つきで書き溜めてあります。AIは毎回これを読んで従うので、書いた約束事は実際に守られます。

ちなみに、このCLAUDE.mdを書くのも AI にお願いしています。バグを直したら「同じ失敗を繰り返さないための約束を CLAUDE.md に足しておいて」と言うだけです。踏んだ失敗が理由つきのルールとして積み上がっていくので、リポジトリが失敗を覚えてくれる感覚があります。

まとめると

AIと気軽に開発するコツは、AIにうまく指示することよりも、間違えたら自動でバレる環境を先に作ることでした。出音のハッシュ突き合わせ、本家との差分監視、類型化されたテスト、実機ログの回収。この網があると、「MDX対応やってみるか」「ついでにリファクタリングするか」が本当に気軽になります。網に掛かればやり直せばいいだけなので。対応形式を一気に3つ増やせたのは、私やAIが急に賢くなったからではなく、失敗のコストが下がったからです。

おわりに

S98Player 3.0、App Storeで公開中です。X68000やPC-98の実機からデータを持っている方はもちろん、当時の音をiPhoneで持ち歩きたい方に使ってもらえたら嬉しいです。

S98Player for iPhone – App Store

コメント

タイトルとURLをコピーしました