[Astro] #131 SID Player v3.2 — RSID IRQ駆動再生対応・CIA1/VIC-II エミュレーション追加
概要
SID Player に RSID フォーマット(playAddr=0)の IRQ 駆動再生を追加しました。
これまでの SID Player は PSID(Play SID)形式、つまり playAddr が指定されている .sid ファイルのみ対応していました。PSID では 50Hz(PAL)/ 60Hz(NTSC)の固定間隔で play ルーチンを JSR 呼び出しするだけで音楽が再生されます。
しかし HVSC(High Voltage SID Collection)には RSID(Real SID) 形式のファイルも多数収録されており、特にゲーム音楽(Konami 等)にこの形式が使われています。RSID で playAddr=0 のファイルは、init ルーチンが自前で IRQ ハンドラを設定し、CPU 割り込みによって音楽を駆動する仕組みです。
SID Player
Commodore 64 SID音源プレイヤー。.sidファイルをドロップして再生。PSID/RSID対応、3Voice分離波形表示、チップモデル切替。
lain-lab.comスクリーンショット
Nemesis(1987 Konami)— RSID v2, playAddr=0, PAL/6581, IRQ 駆動再生成功
デバッグログ — IRQ 発火カウント・$0314/$0315 ベクタ・CPU ステート確認
問題の発見
Nemesis.sid(1987 Konami)を再生しようとしたところ、波形が一切表示されず無音だった。
HVSC(High Voltage SID Collection)でファイルの詳細を確認すると:
| フィールド | 値 | 意味 |
|---|---|---|
| File Format | RSID | Real SID(厳密なC64互換が必要) |
| Format Version | 2 | v2ヘッダー(flags フィールドあり) |
| Load Address | $0000 | ヘッダーに格納されず、データ先頭2バイトから取得 |
| Init Address | $5500 | init ルーチンのアドレス |
| Play Address | $0000 | play ルーチンなし — IRQ 駆動 |
| Clock | PAL | 50Hz |
| SID Model | 6581 | — |
PSID vs RSID の違い
PSID(Play SID)— 従来の対応
Host: 50/60Hz間隔で → JSR play_addr → 6502がSIDレジスタに書き込み → 音が出る
play_addr が指定されているので、ホスト側が固定間隔で呼ぶだけ。CPU エミュレーションは play ルーチン実行時のみ。
RSID(Real SID)— playAddr=0
init → IRQハンドラを\$0314/\$0315に設定 → CPU常時実行 → IRQ割り込みで音楽駆動
play_addr=0 は「play ルーチンを直接呼ぶな、IRQ で自走する」という意味。実機の C64 と同様に:
- init ルーチンが IRQ ベクタ(
$0314/$0315)に自前のプレイヤーアドレスを設定 - CPU がアイドルループで待機
- 50/60Hz で IRQ が発火
- IRQ ディスパッチャ(
$FF48)→ JMP ($0314) → プレイヤー実行 - RTI でアイドルループに復帰
修正内容
1. C64 カーネル環境構築
RSID の init ルーチンが期待する C64 カーネルの最低限の環境を RAM 上に構築:
static void setup_c64_environment() {
// IRQディスパッチャ at $FF48: JMP ($0314)
g_ram[0xFF48] = 0x6C; // JMP indirect
g_ram[0xFF49] = 0x14;
g_ram[0xFF4A] = 0x03;
// ハードウェアIRQベクタ → $FF48
g_ram[0xFFFE] = 0x48;
g_ram[0xFFFF] = 0xFF;
// デフォルトIRQハンドラ(RTI)— initが$0314/$0315を上書きする
g_ram[0xEA31] = 0x40; // RTI
g_ram[0x0314] = 0x31;
g_ram[0x0315] = 0xEA;
// NMIベクタ → RTI
g_ram[0xFE47] = 0x40;
g_ram[0xFFFA] = 0x47;
g_ram[0xFFFB] = 0xFE;
}
実機 C64 のメモリマップ:
| アドレス | 役割 |
|---|---|
| $0314/$0315 | ソフトウェア IRQ ベクタ(RAM、init が上書き) |
| $FF48 | IRQ ディスパッチャ(JMP ($0314)) |
| $FFFE/$FFFF | ハードウェア IRQ ベクタ(→ $FF48) |
| $FFFA/$FFFB | NMI ベクタ |
| $EA31 | デフォルト IRQ ハンドラ(RTI) |
2. CPU アイドルループ
init 完了後、playAddr=0 の場合は CPU がアイドルループで待機するよう設定:
static void setup_idle_loop() {
// CLI + JMP self at $EA7E(KERNALエリア、SIDデータと衝突しない)
g_ram[0xEA7E] = 0x58; // CLI — 割り込み許可
g_ram[0xEA7F] = 0x4C; // JMP $EA7E
g_ram[0xEA80] = 0x7E;
g_ram[0xEA81] = 0xEA;
pc = 0xEA7E;
status &= ~0x04; // Iフラグクリア(IRQ許可)
}
3. 手動 IRQ トリガー
fake6502 の irq6502() に頼らず、手動でスタック操作と PC 変更を行う IRQ トリガーを実装:
static void trigger_irq_manual() {
if (status & 0x04) return; // I flag set → skip
// CIA1 + VIC-II 割り込みステータスをセット
g_cia1_icr_data = 0x81; // Timer A + IRQ
g_ram[0xD019] = 0x81; // Raster + IRQ
// Push PC (hi, lo) + Status onto stack
g_ram[0x0100 + sp] = (pc >> 8) & 0xFF; sp--;
g_ram[0x0100 + sp] = pc & 0xFF; sp--;
g_ram[0x0100 + sp] = (status & ~0x10) | 0x20; sp--;
status |= 0x04; // Set I flag
pc = g_ram[0xFFFE] | (g_ram[0xFFFF] << 8); // Jump to IRQ vector
}
4. CIA1 / VIC-II 最低限エミュレーション
多くの RSID チューンの IRQ ハンドラは、割り込み原因を確認するために $DC0D(CIA1)や $D019(VIC-II)を読む。これらが 0 を返すとハンドラが音楽処理をスキップする問題があった。
// CIA1 ($DC0D) — 読むとクリア(実機挙動)
uint8_t read6502(uint16_t address) {
if (address == 0xDC0D) {
uint8_t val = g_cia1_icr_data;
g_cia1_icr_data = 0;
return val;
}
return g_ram[address];
}
// VIC-II ($D019) — 書き込みでアクノリッジ(ビットクリア)
void write6502(uint16_t address, uint8_t value) {
if (address == 0xD019) {
g_ram[0xD019] &= ~(value & 0x0F);
if ((g_ram[0xD019] & 0x0F) == 0) g_ram[0xD019] &= 0x7F;
return;
}
// ...
}
| レジスタ | 役割 | ハンドラの典型的な使用 |
|---|---|---|
| $DC0D | CIA1 割り込みステータス | LDA $DC0D; AND #$01; BEQ skip |
| $D019 | VIC-II ラスタ割り込みステータス | LDA $D019; STA $D019(アクノリッジ) |
5. レンダーループの分岐
playAddr の値に応じて処理を分岐:
if (g_play_addr != 0) {
// PSID: 従来通り JSR play_addr
jsr6502(g_play_addr);
} else {
// RSID IRQ駆動: CPU継続実行 + IRQ定期発火
exec6502(cycles);
if (/* 50/60Hz タイミング */) {
trigger_irq_manual();
}
}
6. jsr6502() の修正
従来の jsr6502() は RTS トラップを $FFFF に配置していたため、IRQ ベクタ $FFFE/$FFFF を破壊していた。トラップ先を $FFFC(RESET ベクタ、実行時には使用しない)に変更し、save/restore するようにした。
7. parseSidHeader の修正(JS 側)
SID ヘッダーの dataOffset を正しく読み取り、loadAddr=0 の場合にデータ先頭 2 バイト(リトルエンディアン)から実アドレスを取得するよう修正。
var dataOffset = (data[6] << 8) | data[7];
var loadAddr = (data[8] << 8) | data[9];
if (loadAddr === 0 && data.length > dataOffset + 1) {
loadAddr = data[dataOffset] | (data[dataOffset + 1] << 8);
}
デバッグ記録
問題1: WASM モジュール読み込み失敗
症状: ドロップしても反応なし
原因: await init() がモジュール読み込み失敗でスクリプト全体がブロック
解決: import パスを修正(Vite ベアモジュール指定子)
問題2: IRQ が発火してもハンドラが実行されない
症状: IRQ カウントは増加するが SID レジスタが全 00
原因: setup_c64_environment() を init の後に再度呼んでいたため、init が設定した $0314/$0315 を RTI($EA31)で上書きしていた
解決: init 後の setup_c64_environment() 呼び出しを削除
問題3: IRQ ハンドラが音楽処理をスキップ
症状: IRQ 発火・ハンドラ実行中だが SID に書き込みなし(Metal_Gear.sid)
原因: IRQ ハンドラが $DC0D(CIA1)や $D019(VIC-II)を読んで割り込み原因を確認 → 0 が返るため音楽処理をスキップ
解決: CIA1 / VIC-II の最低限エミュレーション追加。IRQ 発火時に $DC0D=0x81 / $D019=0x81 をセット
問題4: Metal_Gear.sid — ラスタチェーン
症状: Nemesis.sid は再生できるが Metal_Gear.sid は一瞬鳴って止まる
原因: Konami のプレイヤーが複数のラスタ割り込みをチェーンさせる方式(各 IRQ 内で次の $D012 を書き換え、フレーム内で複数回 IRQ を発生させる)を使用している可能性。単純な 50/60Hz 固定発火では再現できない
状態: 未解決 — 本格的な VIC-II ラスタラインエミュレーションが必要。DeepSID(WebSid エミュレータ)では正常再生を確認済み
デバッグ用エクスポート関数
今回のデバッグで追加した WASM エクスポート関数:
| 関数 | 戻り値 |
|---|---|
sid_debug_irq_state() | [31:16]=IRQ_hard ( $FFFE ), [15:0]=IRQ_soft ( $0314 ) |
sid_debug_cpu_state() | [31:16]=PC, [15:8]=SP, [7:0]=Status |
sid_debug_irq_count() | IRQ 発火回数の累計 |
対応状況
| ファイル | 形式 | playAddr | 割り込み方式 | 結果 |
|---|---|---|---|---|
| Commando.sid | PSID | $1003 | — | ✅ 再生可能(従来通り) |
| Gradius.sid | PSID | $C858 | — | ✅ 再生可能(従来通り) |
| Nemesis.sid | RSID | $0000 | CIA1 ($DC0D) | ✅ 今回対応 |
| Metal_Gear.sid | RSID | $0000 | ラスタチェーン? | ❌ 未対応(一瞬鳴って停止) |
既知の制限事項
- ラスタチェーン方式の RSID —
$D012を動的に書き換えて1フレーム内に複数 IRQ を発生させる方式は未対応。VIC-II のラスタラインカウンタエミュレーションが必要 - CIA タイマー — CIA1 Timer A/B (
$DC04-$DC07) のカウントダウンは未実装。固定 50/60Hz のタイミングで代替 - NMI 駆動の RSID — CIA2 (
$DD0D) による NMI 方式は未テスト
技術スタック
- reSID — SID チップエミュレーション(C++, MOS 6581/8580)
- fake6502 — 6502 CPU エミュレーション(C, パブリックドメイン)
- Emscripten — C/C++ → WASM ビルド(STANDALONE_WASM, 84KB)
- AudioWorklet — リアルタイム音声処理スレッド
- Astro — ページ構築
更新履歴
- [v3.2] (2026-09-07): RSID IRQ 駆動再生対応
- C64 カーネル環境構築(IRQ ディスパッチャ $FF48、ベクタ $FFFE/$FFFF、$0314/$0315)
- CPU アイドルループ(CLI + JMP self at
$EA7E) - 手動 IRQ トリガー(fake6502 の irq6502() に依存しない実装)
- CIA1 割り込み制御レジスタ ( $DC0D ) エミュレーション(read-clear 挙動)
- VIC-II ラスタ割り込みステータス ( $D019 ) エミュレーション(write-acknowledge 挙動)
- レンダーループ分岐(playAddr=0 時は exec6502 + trigger_irq_manual)
- jsr6502() のトラップ先を $FFFF → $FFFC に変更(IRQ ベクタ破壊防止)
- parseSidHeader 修正(dataOffset 読み取り、loadAddr=0 対応)
- デバッグ用エクスポート関数追加(sid_debug_irq_state / cpu_state / irq_count)
- ビルドスクリプト build_sid.sh 作成