はじめに
VGS-Zero に対応した音楽は MML で作成しますが、私はまず Digital Audio Workstation(DAW)で作曲をしてから譜面(ピアノロール)を見ながら MML 化する手順でデータを準備しています。
私がメインで使っている DAW は Logic Pro と Cubase Pro ですが、私のメイン PC は Linux なので、Logic(macOS のみ)や Cubase(Windows or macOS)を使うことができないため色々と不便でした。
そこで、Linux でも使える DAW を探したところ、FamiStudio というファミコン音源に特化した DAW へ行き着きました。
FamiStudio では、ファミコンライクというかファミコン音源そのものの楽曲を作ることができ、2A03(NES APU)だけでなく VRC6(SCC相当)や VRC7(OPLL相当)などの拡張音源にも対応しています。
VRC6 を使うことができれば VGS-Zero の音源(VGS; Video Game Sound)用の楽曲制作環境として概ね問題ありません。
DAW としてのクオリティもかなり高く、私が普段使っている Logic や Cubase などと比較してもほぼ遜色がありません。
Logic や Cubase と完全に置き換えられるかというと若干微妙ですが。(致命的ではありませんが以下のような不満点ならあります)
- MIDI キーボードから入力時のプレビューが単音(ピアノロールへ入力できるのは単音のみで問題無いのですが、音声のプレビューは和音も入力できた方がサブメロディーの作曲をしたりコード進行を調性したい時などに便利)
- 楽譜プレビューが無い(楽典的なミスをチェックするのに調性に対応した楽譜プレビューがあると便利)
- サンプリング音声のテンポを変えた再生とかはできない(これは耳コピをする時に便利なので作曲時は無くても問題ない)
FamiStudio は NSF; NES Sound Format 形式へのエクスポートもできるので、どうせなら VGS-Zero で NSF ファイルをそのまま再生できるようにすれば更に便利になりそうだと考えて対応してみました。
NSF は枯れたフォーマットなので、高品質な OSS を使って手軽に対応できる筈だと考えたのですが、結果的には想像以上に苦労することとなりました。
NSF の概要
NSF は、ファミコンの ROM から音楽/サウンドコードのみをリッピングしたものにヘッダ情報を付与したフォーマットのバイナリファイルです。
つまり、これを再生するには映像処理(PPU)を除くファミコンのエミュレーションが必要なので結構大変だと想像できます。
そこで、NSF の再生処理は自前実装せず、既存の OSS に頼る方向で調査を進めました。
NSF に対応した OSS
NSF に対応したファミコンエミュレータ自体はかなり多くありますが、その中から今回実装を詳しく調査した以下 3 つに絞って紹介します。
- nosefart
- puNES
- NSFPlay
1. nosefart
MS-DOS の頃からある歴史の長い NSF 特化の OSS で Linux にも対応しています。
私の Linux 環境(Ubuntu)で扱える形にポーティングしてみたのですが、ほぼ無修正で動かすことができたので、ポーティング自体はとても簡単でした。
ただし、適当なファミコンのゲーム ROM からリッピングして NSF を再生してみた限り問題無く再生できていたのですが、FamiStudio からエクスポートした NSF ファイルを再生してみたところ、一部の音程が不正確になる問題 が発生しました。
FamiStudio でエクスポートした .NSF ファイルでは、一部の音程変更にスウィープ機能を使っているらしく、スウィープ関連のエミュレーション実装の完成度に問題がありそうです。
2. puNES
Qt を用いたファミコンエミュレータで NSF を再生する機能があります。
こちらはスウィープ機能も正確に実装されているので、FamiStudio でエクスポートした NSF ファイルも正常な音程で再生することができました。
しかし、NSF に特化している訳ではないので、ソースコードから NSF 再生に関係がある箇所だけライブラリ化して(切り出して)使うのはかなり大変そうです。
3. NSFPlay
NSF のプレイヤを探して最初に行き着いたエミュレータです。
ですが、ソースコードを確認したところ Visual Studio のプロジェクト形式になっていたので、プラットフォームフリー化のポーティングが面倒くさそうだと思い、当初は使うのを躊躇していました。
利用 OSS の選定
その他に目ぼしいエミュレータが見つからなかった(※)ので、私が調べた限りでは VGS-Zero を NSF に対応する手段としては次の 3 通りの方法があるようです。
- nosefart のエミュレーション修正
- puNES の NSF 再生機能の切り出し
- NSFPlay のプラットフォームフリー化
※ その他には Mesen2 もありますが、作業手間は puNES とだいたい同じ & マルチエミュレータなので、切り出し難度は puNES の方が簡単です。
そして、一番簡単なのは NSFPlay のプラットフォームフリー化 と判断しました。
NSFPlay のプラットフォームフリー化
NSFPlay を Linux で使える CLI 化する方式によりプラットフォームフリー化を試みました。
OS 依存部分の削除
オリジナルの NSFPLay のコア実装はあまり OS 依存が無く、xgm という名前空間で纏められていたので OS 依存部分の削除は比較的簡単で、GCC で xgm 配下のモジュールのコンパイルを通しつつリンクするだけで Linux でも動かすことができました。
STL の削除
RaspberryPi Zero 2W のベアメタル環境 (VGS-Zero 実機) 向けにコンパイルを通す場合、C++ の STL を使用することができず、xgm は vector, map, string などの STL をガッツリ使っていたので中々大変でした。
vector については固定サイズの配列に置き換えました。
これについては、要素の入れ替えやソートをしておらず、単純に使用するバスのインスタンスを突っ込む用途で使っていただけなので簡単に(機械的に)修正できました。
しかし、map や string の置き換えは結構大変なので「どのような用途で使っているのか」を確認してみたところ、設定項目の保持とテキストデータの管理に使っていることが分かりました。
設定項目については可変化対応をせず全て固定値にすることで排除しました。
テキストデータは、NSF のプレイリストなどのメタデータですが、そのデータは音楽を再生する用途では不要なので関連処理を全て削除することで排除しました。
これらの対応により、とりあえず Circle でビルドできる状態になりました。
しかし、実機で動作確認をしたところ、NSF を再生した時に同期例外(Synchronous exception)でクラッシュしてしまいました...
Synchronous exception
詳しい原因はノーチェックですが、恐らく原因は性能だろうと考えました。
性能改善
とりあえず、Callgrind (Varglind) を用いてオーバーヘッドが高い箇所を特定して、その箇所のアルゴリズムを最適化する方式で性能向上を試みました。
ですが、NSFPlay は 20 年ぐらい前のソースコードです。
その時代の CPU で動いていたということは、RaspberryPi Zero 2W の ARMv8 (1GHz) で性能が足りなくなるほどのオーバーヘッドがあるとは考えにくいです。
一応、何箇所かアルゴリズムの改善余地が見つかったので対処しましたが、それにより問題解消には至りませんでした。
そうなってくると怪しいのは浮動小数点数です。
浮動小数点数の排除
一応、RaspberryPi Zero 2W には GPU が載っているらしいので FPU は実装されているものと考えられますが、3D 処理についてあまり実用的なレベルでの性能が出ないことを確認しているので、恐らく 20 年前の一般的な PC よりも FPU が弱いものと考えられます。
3D に限らず、RaspberryPi Zero 2W でブラウザを動かすと鬼のように遅いのですが、これも多分(CPUではなく)FPUの性能が良くないためかもしれません。(最近のブラウザの実装は内部的に数値を全て倍精度浮動小数点数とかにしているはず)
そこで、grep で double と float を使っている箇所を全部調べ、それらを全て long 型の固定少数点数に変更しました。
long 型(ARMv8 では 64 bits)の数値は全て 256 倍(8 bits 左シフト)を整数として、整数で扱う時は ÷ 256(8 bits 右シフト)とすることで最大 56 bits の整数にする形です。(これなら FPU 依存しないので高速)
double を使っている箇所が多くあったのでかなり面倒臭かったのですが、その甲斐あって無事 Synchronous exception が発生しなくなりました。
NSF やろうぜ
今回の対応でプラットフォームフリーな NSFPlay のライブラリが出来、その箇所を GPLv3 ライセンスの OSS として切り離して公開しているので、RaspberryPi を使ったファミコン音源関連のプログラム実装の敷居がかなり下がったのではないかと思われます。
ただし、残念ながら上記のコードは(今の所)メモリ所要量がかなり多いです。
RaspberryPi Zero 2W (ベアメタル) は 512MB の大きな RAM が搭載されているので、メモリを削る類の最適化は一切行っていません。
そのため、RaspberryPi Pico、ESP32、Arduino などの低性能なマイコンで動かすことは難しいですが、それらのマイコンでも動かせるように改造できれば組み込み機器でも実機チップ(RP2A03)無しでファミコン音源が楽しめるようになり、電子工作の遊びの幅が広がるかもしれません。
恐らく、RAM 所要量を削る対応は簡単かも?(元々がそんなに大きな RAM を必要とするものではないので)