[Astro] #120 SID Player — reSID + fake6502 WASM + AudioWorklet によるブラウザ完結型 Commodore 64 SIDプレイヤー

[Astro] #120 SID Player — reSID + fake6502 WASM + AudioWorklet によるブラウザ完結型 Commodore 64 SIDプレイヤー

概要

1980年代に世界で最も売れたパソコン「Commodore 64(C64)」に搭載された伝説的音源チップSID(MOS 6581/8580)を、ブラウザ上で完全にエミュレートし、.sidファイルをリアルタイムに再生するWebプレイヤーを構築した。

C++製のSIDチップエミュレーター「reSID」とC製の6502 CPUエミュレーター「fake6502」をEmscriptenでWASMビルド(82KB)し、AudioWorkletスレッド内で直接instantiateしてリアルタイム音声合成を行っている。

スクリーンショット

SID Player スクリーンショット

動画(GIF)

SID Player 動作動画

.sidファイルの入手先

今回の実装内容

機能・モジュール概要
SIDチップエミュレーションreSID(MOS 6581/8580)をEmscriptenでWASMビルド
6502 CPUエミュレーションfake6502(パブリックドメイン)をCブリッジで分離コンパイル
PSIDヘッダー解析.sidファイルのマジック、init/playアドレス、曲数、タイトル等をパース
C64メモリマップ64KB RAM + SIDレジスタ(D400D400-D41F)フック
AudioWorklet再生WASM直接instantiate、128サンプル単位のリアルタイム合成
3Voice分離波形reSIDの各Voice出力を個別に取り出しリングバッファで表示
チップモデル切替MOS 6581 / MOS 8580 をリアルタイムに切替
AnalyserNode連携ミックス波形 + 周波数スペクトラム表示
SIDレジスタモニター32レジスタの16進数値をリアルタイム更新
再生制御再生/一時停止/停止/前曲/次曲、ボリューム調整

背景

C64のSIDチップ(Sound Interface Device)は、単なるゲーム用音源ではなく、3基のオシレーター(鋸歯状波、三角波、パルス波/PWM、ノイズ)、ハードウェア・リングモジュレーション、本格的なADSRエンベロープ、アナログフィルター(LP/BP/HP)を備えた「1チップ・シンセサイザー」だった。

.sidファイルはMIDIや音声データではなく、C64の6502 CPU用マシン語プログラムそのもの。これを再生するには、SIDチップだけでなく6502 CPUのエミュレーションも必要になる。

スクリーンショット

3Voice分離波形表示

SID Player - 3Voice分離波形

波形 + スペクトラム(Bothモード)

SID Player - Both Mode

技術実装の詳細

1. アーキテクチャ

[ Main Thread (Astro UI) ]
│  .sidファイル読み込み → AudioWorkletNode作成

[ AudioWorklet Thread (sid-processor.js) ]
│  WebAssembly.instantiate(wasmBytes) → WASM直接ロード
│  128サンプルごとにprocess() → sid_render_float_multi()

[ WASM (reSID + fake6502 + PSID Parser) 82KB ]
│  1サンプルごとに:
│    SIDチップをクロック → 50Hzでplayルーチン実行
│    → 6502がSIDレジスタに書き込み → reSIDに転送
└─► Float32Array → AudioWorklet出力 → スピーカー

2. C/C++分離コンパイル

fake6502はC言語で書かれており、C++の予約語(and, or, not, xor)と衝突する。fake6502_bridge.cを作成してCコンパイラ(emcc)で別途コンパイルし、C++のsid_wrapper.cppとリンクする。

emcc fake6502_bridge.c -O3 -c -o fake6502_bridge.o
em++ sid_wrapper.cpp resid/src/*.cc fake6502_bridge.o -O3 \
  -I./resid/src --no-entry -s STANDALONE_WASM=1 \
  -s EXPORTED_FUNCTIONS='[...]' -o sid_core.wasm

3. SIDレジスタフック(D400D400-D41F)

6502コードがC64のSIDレジスタアドレスに書き込むと、write6502()コールバック内でreSIDに値を転送する。

void write6502(uint16_t address, uint8_t value) {
    g_ram[address] = value;
    if (address >= 0xD400 && address <= 0xD41F) {
        g_sid.write(address & 0x1F, value);
    }
}

4. 3Voice個別出力

reSIDのvoice[3]配列から各Voiceの出力を個別に取り出し、4本のfloatバッファ(mix + V1/V2/V3)に書き出す。

void sid_render_float_multi(float* out_mix, float* out_v1,
                            float* out_v2, float* out_v3, int num_samples) {
    for (int i = 0; i < num_samples; ++i) {
        g_sid.clock(cycles);
        out_v1[i] = (float)g_sid.voice[0].output() / 32768.0f;
        out_v2[i] = (float)g_sid.voice[1].output() / 32768.0f;
        out_v3[i] = (float)g_sid.voice[2].output() / 32768.0f;
        out_mix[i] = /* DCオフセット除去済みミックス */;
    }
}

5. AudioWorklet内でのWASM直接instantiate

AudioWorkletスレッドではWeb Workerと異なりimportScripts()でEmscriptenグルーコードを読めない。STANDALONE_WASMビルドにより、WebAssembly.instantiate()で直接ロードする。

const { instance } = await WebAssembly.instantiate(wasmBytes);
this.exports = instance.exports;
this.exports._initialize(); // C++ staticオブジェクトのコンストラクタ実行
this.exports.sid_init(sampleRate);

6. WebAudioノードチェーン

AudioWorkletNode (SID) → GainNode (音量) → AnalyserNode (波形解析) → スピーカー

AnalyserNodeでミックス波形と周波数スペクトラムを取得し、Canvas2Dで描画。3Voice個別波形はWASMから直接取得してリングバッファに蓄積。

技術スタック

  • Astro — ページ構築
  • reSID — SIDチップエミュレーション(C++, MOS 6581/8580対応)
  • fake6502 — 6502 CPUエミュレーション(C, パブリックドメイン)
  • Emscripten — C/C++ → WASM ビルド(STANDALONE_WASM)
  • AudioWorklet — リアルタイム音声処理スレッド
  • WebAudio API — GainNode, AnalyserNode
  • Canvas 2D — 波形/スペクトラム/レジスタ描画

更新履歴

  • v0.1: reSIDのEmscriptenビルド成功、AudioWorkletでテストトーン(ノコギリ波)再生。
  • v0.2: fake6502組み込み、PSIDヘッダーパーサー実装、.sidファイル再生成功。
  • v0.3: Astroページ化、AnalyserNode連携、波形/スペクトラムビジュアライザー、SIDレジスタモニター。
  • v0.5: 3Voice分離波形表示(WASM改造)、チップモデル切替(6581/8580)、リングバッファスクロール表示、一時停止/再開機能。
  • v1.0: 公開。

関連リンク