なぜこのマイナーなオーディオコーデックはN64に最適なのか
Why This Obscure Audio Codec Is a Perfect Fit for the N64 (phoboslab.org)
要約
Nintendo 64 (N64) の限られたカートリッジ容量とCPUパワーという制約の中で、高音質なオーディオ再生を実現する課題について論じています。この記事では、Opusに近い品質を低ビットレートで実現し、CPUリソースを最小限に抑える効率的なソリューションとして、Ultra Low Complexity Codec (ULC) を紹介しています。著者は、ULCがN64のRSPコプロセッサ向けにどのように適合・最適化されたかを詳述し、Xibalba 64やHalf-Life N64ポートのようなゲームでの高品質オーディオ再生を可能にした経緯を説明しています。
全文翻訳
なぜこのマイナーなオーディオコーデックはN64に最適なのか
— 2026年11月3日(火曜日)
クリックベイトなタイトルで申し訳ありません。この記事は期待に応えるものと約束します。44100 Hzステレオで、Opusに近いビットレートで、N64のCPU時間の約4%しか使用しません。
しかし、まず歴史から始めましょう。N64でオーディオデータを再生および保存するための選択肢は悲惨なものでした。PlayStationは、圧縮されていないレッドブックオーディオをCDから直接ストリーミングできました。N64では、カートリッジのスペースは貴重であり、93 MHz CPUの時間は限られていました。
さらに悪いことに、ベクトルコプロセッサであるRSPへのアクセスは、任天堂によって長い間制限されていました。任天堂自身のプラットフォームライブラリであるLibultraは、内部的にRSPをさまざまなグラフィックスおよびサウンド関連タスクに使用していましたが、任天堂はサードパーティの開発者が直接RSP用のコードを書くことを許可しませんでした。
Libultraは、ADPCMスキーム(アダプティブ差分パルス符号変調)を使用して圧縮オーディオを透過的に再生するためのいくつかの機能を提供しました。RSPは、オーディオが出力バッファに送信される直前に、オンザフライでデコードを処理しました。
これは十分に機能し、サンプルの必要なスペースを4分の1に削減します。それでも、44100 Hzの5分間のステレオオーディオトラックは、約12MBになります。これは、サンプルレートを半分にしても、N64カートリッジには大きすぎます。Mario 64のROM全体はわずか8MBです。最大のN64ゲームは64MBでした。
そのため、N64のゲームはトラッカーに似た方法で音楽を生成していました。短いサンプルをスケジュールし、適切なタイミングで限定的なエフェクトとともに再生していました。カートリッジのスペースの制約では、完全に「録音された」オーディオは事実上不可能でした。
N64のライフサイクルの後半で、一部の選ばれた開発者は任天堂からRSPに直接アクセスすることを許可されました。最も有名なのは、Conker's Bad Fur Dayで、ゲーム内の無数の音声サンプルをデコードするためにこれを使用しました。MP3のデコードは非常に負荷が高く、ゲームのフレームレートが低下しました。カットシーンには十分でしたが、ゲームプレイ中の連続したBGMや効果音にはあまりにも重すぎました。
オープンソースLibdragonにおけるオーディオ
任天堂のLibultraと同様に、オープンソースの代替であるLibdragonには、RSPで実行されるリアルタイムADPCMデコーダーがあります。そのため、ROMスペースが今日では安価になったため、Xibalba 64のすべての音楽をADPCMで圧縮しました。32MBのROMのうち約31MBがサウンドと音楽に使用されています。Mario 64はわずか8MBのROMしか使用しておらず、Xibalba 64は15分の音楽しかないはるかにシンプルなゲームであることを思い出してください。それは機能しましたが、このように音楽を保存するのはスペースの無駄遣いです。
現在、Libdragonには、RSPでアクセラレーションされたリアルタイムのOpusデコーダーもあります。それ自体が驚異です。OpusフォーマットはN64の16年後に公開されましたが、Giovanni Bajoはこれを「時代遅れ」なハードウェアで動作させることに成功しました。圧縮と音質は優れていますが、再び、ゲームプレイ中に使用するには遅すぎます。
では、代替手段は何でしょうか?RSPアクセラレーションMP3デコーダーを構築することもできますが、Opusに対するパフォーマンスの向上はわずかであり、品質と圧縮は劣ります。
QOAはADPCMよりもわずかに改善しますが、ベクトルコプロセッサには不向きです。各サンプルは前のデコードされたサンプルに依存します。ベクトル化が困難です。
単純な変換コーデック、MP3のようなものですが、コンソーシアムによる設計のすべての負担なしで、機能するかもしれません…
Ultra Low Complexity Codec
深夜の「リサーチ」中に、わずか70kbpsで録音された音楽を再生する古いGame Boy Advanceのデモにつまずきました。これは典型的なMP3よりも低いですが、同等の品質です。繰り返します。GBAで。16 MHz ARM CPUで。ベクトル命令なしで、GitHubページによると、まだ余裕があります。
ulcplayer-gba by Aikku93、Game Boy Advanceエミュレータで録画
時間領域コーデック(ADPCMなど)は、この品質を得るには大幅に高いビットレートが必要です。サブバンドコーデック(MP2など)は、同等のビットレートでは品質がはるかに悪く、より「モダン」な変換コーデック(MP3など)は通常、少なくとも80〜100 MHzのCPUを必要とします。このGBAデモでは、既存のコーデックはどれも適していませんでした。
そのため、Aikku93はARMアセンブリでデコーダーを書き起こしただけでなく、まったく新しいコーデックであるUltra Low Complexity Codec(ULC)を作成しました。
ULCは変換コーデックであり、MP3にいくらか似ています。ULCはPCMデータのスライスを周波数領域に変換します。MP3と同様に、これを行うために修正離散コサイン変換(MDCT)を使用します。しかしMP3とは異なり、信号を複数のバンドに分割しません(ULCのMDCTは「フルバンド」です)。MP3が古典的なエントロピー符号化(特にハフマン)を使用するのに対し、ULCは…使用しません。代わりに、変換された係数は非常にシンプルなニブルベースの構文で格納されます。
要するに、ULCはMP3がなり得たものです。同じコアアイデアですが、その周りの無駄がありません。「超低複雑性」は異なるアルゴリズムから来るのではなく、慎重に適用しパッケージ化することから来ています。
ニブルベースのパッケージングは、おそらくULCの最もエレガントなアイデアです。各係数はわずか4ビットで格納され、符号付き整数として解釈されます。一部の値は、ゼロフィル、ノイズフィル、または量子化器の変更を示すために予約されています。
-7 … -2、2 … 7- 係数値0- ゼロラン、ランレングス(1 … 16)は次のニブルで続きます1- ロングゼロラン、ランレングス(17 … 288)は次の2つのニブルで続きます-1- 量子化器の変更、値は次のニブルで続きます-8- ノイズフィル、長さと振幅は次の3つのニブルで続きます
すべてニブルアラインされています。これは、コーデック全体が任意のビット読み取りやエントロピーデコードなしで機能することを意味します。係数ブロックの読み取りと展開には、Cで約150行しかかかりません。
また、ノイズフィルは貴重な追加機能であることに注意してください。シンバル、ハイハット、スネアドラムはこれで大きくメリットがあります。MP3には同様のものはありません。 quasi-random係数をそのままエンコードする必要があります。
ULCは16kbpsでも実用的な結果を生成できます。品質はもちろん低下しますが、それでも一部のアプリケーションには十分かもしれません。
Aikku93は2019年からULCに取り組んでおり、エンコーディング品質を着実に向上させています。すべてのコードは許可ライセンスです。N64でこれを試す時が来ました!
N64でのULC
LLM時代以前のある晩、ULCリファレンスデコーダーをN64用にコンパイルし、小さなデモを作成しました。問題なく動作しました。
唯一の問題は、CPUを非常に忙しくさせていたことでした。RSPに何もオフロードされていませんでした。概念実証としては、あまり意味がありませんでした。変換ベースのコーデックをデコードするには93 MHz CPUで十分であることをすでに知っていましたが、「音楽プレーヤー」がそれ以外何もできない必要はありません。ゲームプレイ中のBGMや音声ダイアログにはこれが必要です。CPUを占有してはなりません。
Floatから16ビット固定小数点へ
明らかに、IMDCTはRSPコプロセッサに移動する必要があります。デコードされた係数の一つのブロックをRSPのメモリにDMA転送し、変換を実行し、完了したサンプルをRDRAMの出力バッファにDMA転送することができます。
それは言うほど簡単ではありません。RSPには4096バイトのデータメモリ(DMEM)と、命令用の4096バイト(IMEM)しかありません。ベクトル命令は16ビットまたは32ビット固定小数点(浮動小数点なし)で動作します。IMDCTは、変換に必要なすべてのデータが一度にRSPのDMEMに収まる場合にのみ効率的です。一時値、そして可能であればラッピングバッファ(以前にデコードされたブロックの半分)のためにもDMEMの一部が必要です。ULCのデフォルトの2048サンプルブロックサイズでは、16ビット固定小数点でもうまくいきません。
ULCはより小さなMDCTブロックサイズで動作できますが、2048に最適化されているため、可能な限り大きく保つべきです。さらに、ULCリファレンスエンコーダー/デコーダーは浮動小数点を使用するため、品質を維持するためには32ビット固定小数点が明白な選択肢ですが、16ビットでも機能するかもしれません。したがって、両方とも3072バイトに達する2つの選択肢があります。
- 32ビット値、512サンプルブロックサイズ(256サンプルラップバッファ):
(512 + 256) * 4バイト - 16ビット値、1024サンプルブロックサイズ(512サンプルラップバッファ):
(1024 + 512) * 2バイト
16ビット固定小数点を使用すると、DMAトラフィックが削減されるだけでなく、RSPでの計算も高速化されます。トレードオフは、より高いノイズフロアになります。どのくらい高いでしょうか?わかりません。そこで、N64 CPUでまだ実行されているリファレンスデコーダーを慎重にリファクタリングし、16ビット固定小数点(Q1.14)で動作するようにしました。
何度も耳を塞ぎそうになりながら、動作させることができ、音量を上げてもかすかなホワイトノイズしか聞こえない(または想像できる?)だけでした。決定しました。16ビットにします。
RSPへの詰め込み
これで、このCコードをRSPに移植するだけが必要になりました。MIPSアセンブリの知識は非常に基本的であり、RSPデコーダーを書くことは серьезное endeavorになります。
ここでこのプロジェクトから一時離脱しました。完成させるべきゲームがありました。今年初めにプロジェクトに戻ったとき、LLMはかなり改善されていました。それでも、GPT-5をこれに信頼する気にはなれませんでした。以前のいくつかの実験では、遅延スロットやRSPのその他の複雑さを無視して、仕事を完了しないことが多く、非常に悪い結果でした。
(余談:GPT-6またはOpus-5なら、今なら一発でできるでしょう)
そこで、より反復的なアプローチを選択しました。Max Bebökは、RSP用のコードを書くための小さなCライク言語を作成しました。その名はRSPLです。言語ドキュメントを手に、CodexにCコードを piece by piece RSPLに翻訳するように依頼し、常に結果を確認しました。移植と適切なAPI設計の決定に数日かかりました。その後、CodexはRSPLのアセンブリ出力をさらに最適化しました。まず一部を書き直し(今回は直接アセンブリで)、次にrspoptのオプティマイザーを適応させてパイプラインストールを最小限に抑えました。
最終的な結果は、IMDCT、ラッピング、ステレオインターリーブ、および出力スケーリングを処理する約1400行のMIPSアセンブリです。RSPの4096バイトIMEMにぎりぎり収まります。
デコーダーのC側は手書きのままですが、それはLLMにセンスがないからです。プロセス全体で数週間かかりましたが、それが機能したこと自体にまだ驚いています。
私のulc64デモは、さまざまなビットレートでの品質を示しています
ULCを簡単にシーク可能にするために、ファイルの末尾にスパーステーブルを追加しました。これは、8フレームごとのバイトオフセットを含んでいます。デコーダーは、目的のフレームに最も近いフレームから素早くスキップします。これは、1秒あたり約20バイトを追加します。16kbpsオーディオでも管理可能です。
ULCは、N64でのADPCMやOpusの非常に実行可能な代替手段であることが証明されました。品質とビットレートはOpusに近く、複雑さはADPCMのわずか4倍です。単一の44100 Hzステレオトラックは、CPU時間の約4%、RSP時間の5%を消費します。BGM、音声、アンビエンス、および長い効果音に完全に適しています。
デコーダーは、他のすべてのオーディオコーデックと同様に、Libdragon内で透過的に機能します。しばらく前にプレビューブランチにマージされました。
追加情報
数日前、Half-LifeのN64への優れた移植版が公開されました。READMEには次のように書かれています。
[...] 2,500のサウンドと27のミュージックトラックのためのlibdragonのULCオーディオコーデックが、すべてを64MBのカートリッジイメージに収めることを可能にしています。
~ github.com/ToxyCola/Half-Life-Nintendo-64-Port
ULCを作成してくれたAikku93に多大な感謝を。素晴らしいコーデックです。私はそれを見つけて適応させただけです。私はすべてのものにシンプルさとエレガンスを求め、ULCは私が本当に愛せるものです。称賛を!