Files
rix2midi/rix2midi.cpp

449 lines
21 KiB
C++
Raw Normal View History

2026-08-22 05:52:55 +00:00
// ============================================================================
// rix2midi.cpp —— 「大富翁 2」RIX 音樂 → MIDI 轉換器
// ============================================================================
//
// 【這是什麼】
// 把 DOS 遊戲《大富翁 2》裡的 .RIX 音樂檔,轉成標準 .MIDI 樂譜。
// 同時可一次處理一整個資料夾、或單一檔。
//
// 【RIX 是什麼、為什麼不能直接轉】
// RIX 不是「存音符」的格式,而是 **AdLib OPL 晶片** 的「機器碼」:
// 它是一小段「時鐘節拍tick」驅動的下載程式碼每個 tick 告訴
// OPL 晶片要寫哪些「暫存器」frequency、key-on、音量……
// 所以 RIX 裡面沒有現成的音符,只有「要讓硬體發出什麼」的指令。
//
// 【我們的策略:劫持 OPL 的暫存器寫入】
// 我們不自己寫 RIX 解碼器那很複雜AdPlug 已經寫得好)。
// 而是:
// 1. 用 AdPlug 載入 RIX、執行每 tick 的「樂曲排程」。
// 2. AdPlug 內部透過一個「OPL 晶片模擬器」CEmuopl是 Copl 的子類)
// 來真的寫 OPL 暫存器。我們「繼承」它、攔截 write(),就能拿到
// 每個 tick、每個聲道的 A / B 暫存器值。
// 3. 由 A / B 暫存器還原出「頻率」→ 換算成 MIDI 音符。
// 4. 把「開/關、音高、時間」寫成標準 MIDI 檔。
//
// 【OPL 暫存器 → 音符 的數學(核心原理)】
// OPL 的頻率由「F-number (10 bits)」加上「block (3 bits)」決定的:
// F-number 低 8 位 放在 A 暫存器 (0xA0~0xA8)
// F-number 高 2 位 + block + key-on 放在 B 暫存器 (0xB0~0xB8)
// · B bit0-1 = F-number 高 2 位
// · B bit2-4 = block半音倍頻、決定八度
// · B bit5 = Key-on1=開聲0=關聲)
// 實際頻率(單位 Hz
// freq = F-number × 2^block × 晶振頻率 / (72 × 2^21)
// 其中 AdLib OPL 晶振 ≈ 3579545 Hz。
// 最後用十二平均律把 Hz 換成 MIDI 音符60 = 中音 C
// midi = round( 69 + 12 × log2( freq / 440 ) )
//
// 【音高 vs 時值(時間)】
// RIX 是「每 tick 一執行節」的排程。《大富翁 2》的 RIX 用固定時基。
// 我們量出RIX 一個 tick = 1/70 秒(用 RICHU 驗證:
// 8051 ticks ÷ 70 = 115 秒,正好等於 adplug 產出的 WAV 長度)。
// 為了寫進 MIDI我們用「96 ticks / beat四分音符並把 MIDI 速度
// 設成 tempo = (1e6 ÷ 70) × 96 微秒/四分音符,
// 這樣「1 個 MIDI tick = 1 個 RIX tick」節奏才和原曲一致。
//
// 【樂器GM Program怎麼選】
// OPL 只有 8~9 個合成聲道、不分旋律/和聲,所以由我們「猜」:
// 看每個聲道用的音高中位數——偏低 → 指派貝斯;其餘 → 指撥/方波 Lead。
// (這是「聽起來像原曲」的務實選擇,不是精確編曲。)
//
// 【如何編譯】
// 需要 AdPlugRIX 解碼 + OPL 模擬)。請先執行同目錄的 build.sh
// ./build.sh
// 它會自動找到/安裝 AdPlug 與 C++ 庫,再編譯出 rix2midi 執行檔。
//
// 【如何使用】
// ./rix2midi 某個.RIX # 轉单个檔 → 同名 .mid
// ./rix2midi 某個.RIX 輸出.mid # 指定輸出檔
// ./rix2midi 整個資料夾/ [輸出/]/ # 批次轉該資料夾所有 .RIX
//
// ============================================================================
#include <cstdio>
#include <cstdlib>
#include <cstdint>
#include <cstring>
#include <cmath>
#include <string>
#include <vector>
#include <map>
#include <tuple>
#include <algorithm>
#include <fstream>
#include <sys/stat.h>
#include <dirent.h>
// ── AdPlugRIX 解碼 + OPL 晶片模擬)──────────────────────────────
#include "opl.h" // Copl 基類OPL 晶片寫入介面)
#include "emuopl.h" // CEmuopl真正的 OPL 模擬實作
#include "rix.h" // CrixPlayerRIX 檔案的「排程器」
#include "fprovide.h" // CProvider_Filesystem從硬碟讀檔
#include "player.h" // CPlayer 基類、通用介面
#include "database.h" // CAdPlugDatabaseAdPlug 需要的資料庫單例)
// AdPlug 內部有一個「全域單例」CAdPlugDatabase需要外部定義其位址。
// AdPlug 的 .so 期待一個 CAdPlug::database 符號;我們提供它。)
class CAdPlug { public: static class CAdPlugDatabase *database; };
CAdPlugDatabase *CAdPlug::database = nullptr;
// ── 常數 ──
// AdLib OPL 晶振頻率Hz。不同機器略有差異這裡用標準 AdLib 值。
static const int OPL_CLK = 3579545;
// OPL 頻率的「除數」= 72 × 2^21 × 8把 2^21 換成 2^24 的 8 倍寫法,見下)。
// freq = Fnum × 2^block × OPL_CLK / (72 × 2^24) × 8 → 整理成一個常數 K
static const double K = (double)OPL_CLK * 8.0 / (72.0 * 16777216.0);
// MIDI 節奏:一個 RIX tick 對應的 MIDI 拍數用 9696 ticks/beat
static const int MIDI_DIV = 96;
// RIX 每 70 tick = 1 秒用量表得出8051 ticks ÷ 70 = 115 秒WAV 長度)。
static const double RIX_TICKS_PER_SEC = 70.0;
// ============================================================================
// 1. 攔截 OPL 暫存器寫入
// ============================================================================
//
// CEmuopl 是「真正的 OPL 晶片模擬器」(負責發出聲音)。
// 我們繼承它、只多寫一個東西:把每個聲道的 A / B 暫存器「也記下來」。
//
// 為什麼可行Copl 有一个「純虛」的 write(reg,val)CEmuopl 實作它來驅動
// 晶片。我們再覆寫它先「記下」A/B 暫存器,再交給原本的去發聲,兩不誤。
//
struct OPLTap : CEmuopl
{
// 每個聲道0~8的 A 暫存器F-number 低 8 位)與 B 暫存器(高 2 位+block+key
int a0[9] = {0,0,0,0,0,0,0,0,0};
int b0[9] = {0,0,0,0,0,0,0,0,0};
OPLTap(int sample_rate) : CEmuopl(sample_rate, /*16bit=*/true, /*stereo=*/false) {}
// Copl/CEmuopl 的 write(reg, val)
// reg = OPL 暫存器位址0..255val = 要寫入的值。
// 聲道 = reg 的低 4 位元reg & 0x0F
void write(int reg, int val) override
{
int r = reg & 0xFF; // 暫存器位址
int v = val & 0xFF; // 寫入值
int ch = r & 0x0F; // 第幾個 OPL 聲道
if (ch <= 8) { // OPL2 / AdLib 前 9 個聲道RIX 用到 0~8
if (r >= 0xA0 && r <= 0xA8) a0[ch] = v; // A 暫存器 → F-number 低位
else if(r >= 0xB0 && r <= 0xB8) b0[ch] = v; // B 暫存器 → 高位/block/key
}
// 照樣交給原本的 OPL 模擬器去「發聲」(我們只是順手記一筆)。
CEmuopl::write(reg, val);
}
};
// ============================================================================
// 2. 由 A / B 暫存器還原「頻率」與「開/關」
// ============================================================================
// 拼出完整的 F-number10 位低位)+ block高幾位方便統一取位。
// value = A (8 位低位) | (B 低 5 位 << 8)
// 之後:
// F-number(10位) = value 的低 10 位 → (value & 0x3FF)
// block(3位) = value 的第 10~12 位 → (value >> 10) & 7
// key-on = B 的 bit5 → (b0 >> 5) & 1
static inline unsigned pack_freq(OPLTap *o, int ch)
{
return (unsigned)o->a0[ch] | ((unsigned)(o->b0[ch] & 0x1F) << 8);
}
// 由暫存器算出「實際頻率 Hz」。
static double freq_of(OPLTap *o, int ch)
{
unsigned raw = pack_freq(o, ch);
unsigned fnum = raw & 0x3FF; // F-number10 位)
int blk = (raw >> 10) & 7; // block3 位,決定八度倍頻)
return K * (double)fnum * (double)(1u << blk); // = fnum×2^blk × 晶振 / (72×2^24)×8
}
// 目前這個聲道「有沒有在發聲」Key-on 旗標在 B 的 bit5
static inline bool key_on(OPLTap *o, int ch)
{
return (o->b0[ch] >> 5) & 1;
}
// 把頻率 Hz 換成 MIDI 音符69=A4=440Hz每半音 ×2^(1/12))。
static int freq_to_midi(double hz)
{
if (hz <= 0.0) return 0;
int m = (int)std::lround(69.0 + 12.0 * std::log2(hz / 440.0));
if (m < 0) m = 0;
if (m > 127) m = 127;
return m;
}
// ============================================================================
// 3. 寫 MIDI 的「多組定長變數VLQ」與樂器指派
// ============================================================================
// MIDI 用「變長位元組」表示 delta-time
// 把數字拆成 7 一組;**高組在前**、每組若還有更高位就在該位元組最高位bit7
// 加 1 表示「後面還有」;最後一組(最低組)不加。
// (這跟某些人的「低位在前」理解相反,寫反了整首曲都會跑调跑速。)
static void write_vlq(std::vector<char> &buf, int n)
{
unsigned x = (unsigned)n;
unsigned char g[8]; int cnt = 0;
while (x) { g[cnt++] = (unsigned char)(x & 0x7F); x >>= 7; }
if (cnt == 0) { buf.push_back(0); return; } // 特例0 就一個 0x00
for (int i = cnt - 1; i >= 0; --i) // 高組在前
buf.push_back((char)(g[i] | ((i > 0) ? 0x80 : 0)));// 「不是最後一組」就加 0x80
}
// 依每個聲道使用的「音高中位數」選 GM Program31=方波Lead、32=鋸齒貝斯、35=方波2
static int pick_instrument(const std::vector<int> &midi_notes)
{
if (midi_notes.empty()) return 31;
std::vector<int> v = midi_notes;
std::sort(v.begin(), v.end());
int median = v[v.size() / 2];
if (median < 37) return 32; // 音域偏低 → 當貝斯
else if (median < 49) return 35; // 中低音 → 另一種 Lead
else return 31; // 其餘 → 方波 Lead
}
// ============================================================================
// 4. 核心:讀 RIX → 抓事件 → 寫 MIDI
// ============================================================================
struct NoteEv { int ch; int midi; unsigned tick_on; unsigned tick_off; };
// 把一個 .RIX 檔轉成 .MIDI。回傳 0 = 成功。
static int convert_one(const std::string &in_path, const std::string &out_path,
bool verbose, int &out_note_count)
{
OPLTap opl(44100); // 用 44.1kHz 的 OPL 模擬器(只借它的暫存器邏輯)
int prev_key[9] = {0,0,0,0,0,0,0,0,0}; // 上一 tick 每個聲道的 key 狀態
int cur_note[9] = {-1,-1,-1,-1,-1,-1,-1,-1,-1}; // 目前該聲道「正開著」的音符
std::vector<NoteEv> notes;
unsigned t = 0; // 目前 tick放在外層好讓 verbose 訊息也能用)
{
CProvider_Filesystem fs; // 從檔案系統讀 RIX
CPlayer *player = CrixPlayer::factory(&opl); // ★ 把我們的 Tap 傳進去
if (!player) { if (verbose) fprintf(stderr, " [!] factory 回傳空\n"); return 1; }
if (!player->load(in_path, fs)){ if (verbose) fprintf(stderr, " [!] 載入失敗\n"); return 2; }
// 一個 tick 一 tick「播放」在每個 tick 檢查 9 個聲道的 key/音高變化:
const unsigned MAX_TICK = 2000000; // 安全上限,防止無窮迴圈
for (; t < MAX_TICK && player->update(); ++t) {
for (int ch = 0; ch < 9; ++ch) {
bool on = key_on(&opl, ch);
int mid = on ? freq_to_midi(freq_of(&opl, ch)) : -1;
if (prev_key[ch] == 0 && on) {
// 0 → 1新開一個音符
notes.push_back({ch, mid, t, (unsigned)-1});
cur_note[ch] = mid;
}
else if (prev_key[ch] == 1 && on && mid != cur_note[ch]) {
// 一直開著、但音高變了:先「結束」舊音、再「開」新音
// 這樣不會產生一長串同音MIDI 會比較乾淨)
for (auto &ev : notes)
if (ev.ch == ch && ev.tick_off == (unsigned)-1) { ev.tick_off = t; break; }
notes.push_back({ch, mid, t, (unsigned)-1});
cur_note[ch] = mid;
}
else if (prev_key[ch] == 1 && !on) {
// 1 → 0關閉該聲道 → 給「還沒結束」的那個音一個結束時間
for (auto &ev : notes)
if (ev.ch == ch && ev.tick_off == (unsigned)-1) { ev.tick_off = t; break; }
}
prev_key[ch] = on ? 1 : 0;
}
}
// 到結尾時還沒關的音,一律補上結束時間。
for (auto &ev : notes) if (ev.tick_off == (unsigned)-1) ev.tick_off = t;
}
out_note_count = (int)notes.size();
if (notes.empty()) return 3; // 什麼都沒抓到
// ── 把「開/關」攤平成按時間排序的事件流 ──
using Tev = std::tuple<int, int, int, int>; // (tick, 0=on/1=off, ch, note)
std::vector<Tev> stream;
stream.reserve(notes.size() * 2);
for (auto &ev : notes) {
stream.push_back({(int)ev.tick_on, 0, ev.ch, ev.midi});
stream.push_back({(int)ev.tick_off, 1, ev.ch, ev.midi});
}
std::sort(stream.begin(), stream.end(),
[](const Tev &a, const Tev &b){
if (std::get<0>(a) != std::get<0>(b)) return std::get<0>(a) < std::get<0>(b);
return std::get<1>(a) < std::get<1>(b); // 同一 tick 先 on 後 off
});
// ── 為每個用到的聲道挑樂器GM program──
std::vector<int> used[9]; // 每聲道用過哪些音符
bool active[9] = {0,0,0,0,0,0,0,0,0};
for (auto &ev : notes) { if (ev.tick_off > ev.tick_on) { used[ev.ch].push_back(ev.midi); active[ev.ch] = 1; } }
int prog[9]; for (int c = 0; c < 9; ++c) prog[c] = pick_instrument(used[c]);
// ── 組 MIDI「音軌」位元組 ──
// MIDI 速度(微秒/四分音符讓「1 RIX tick = 1 MIDI tick」節奏就和原曲一致。
// 推導MIDI 每秒 tick 數 = (1e6/tempo)×96設它RIX 每秒 tick 數70
// 得 tempo = 1e6 × 96 ÷ 70 ≈ 1,371,428。
std::vector<char> track;
int tempo = (int)std::lround(1000000.0 * MIDI_DIV / RIX_TICKS_PER_SEC);
write_vlq(track, 0); // 開頭 delta=0
track.push_back(0xFF); track.push_back(0x51); track.push_back(0x03); // set_tempometa
track.push_back((char)((tempo >> 16) & 0xFF));
track.push_back((char)((tempo >> 8) & 0xFF));
track.push_back((char)(tempo & 0xFF));
for (int c = 0; c < 9; ++c) { // 各聲道的樂器(每個前補 delta=0避免 running-status 歧義)
if (!active[c]) continue;
track.push_back(0x00);
track.push_back((char)(0xC0 + c)); // program change聲道 c
track.push_back(prog[c]);
}
int last_tick = 0;
for (auto &tv : stream) {
int tick = std::get<0>(tv);
int delta = tick - last_tick; last_tick = tick;
write_vlq(track, delta);
int ch = std::get<2>(tv), note = std::get<3>(tv);
if (std::get<1>(tv) == 0) { // note on
track.push_back((char)(0x90 + ch)); track.push_back(note); track.push_back(110);
} else { // note offvelocity=0
track.push_back((char)(0x80 + ch)); track.push_back(note); track.push_back(0);
}
}
write_vlq(track, 0);
track.push_back(0xFF); track.push_back(0x2F); track.push_back(0x00); // End Of Track
// ── 寫 MIDI 檔MThd 標頭 + MTrk 音軌)──
FILE *fp = fopen(out_path.c_str(), "wb");
if (!fp) { if (verbose) fprintf(stderr, " [!] 打不開輸出檔\n"); return 4; }
auto W16 = [](FILE *f, int v){ char b[2]; b[0]=(v>>8)&0xFF; b[1]=v&0xFF; fwrite(b,1,2,f); };
auto W32 = [](FILE *f, int v){ char b[4]; b[0]=(v>>24)&0xFF; b[1]=(v>>16)&0xFF; b[2]=(v>>8)&0xFF; b[3]=v&0xFF; fwrite(b,1,4,f); };
fwrite("MThd", 1, 4, fp);
W32(fp, 6); // 標頭長度固定 6
W16(fp, 0); // format 0單一音軌
W16(fp, 1); // 音軌數 = 1
W16(fp, MIDI_DIV); // 96 ticks/beat
fwrite("MTrk", 1, 4, fp);
W32(fp, (int)track.size()); // 音軌位元組數
fwrite(track.data(), 1, track.size(), fp);
fclose(fp);
if (verbose) fprintf(stderr, " ticks=%u\n", t);
return 0;
}
// ============================================================================
// 5. 批次處理 / 主程式
// ============================================================================
static int process_file(const std::string &in, const std::string &outdir,
const std::string &out_path_override,
bool verbose, int &note_count, std::string &err)
{
// 先從路徑取「檔名」(去掉資料夾),再去掉 .RIX/.rix 副檔名。
std::string base = in;
auto slash = base.find_last_of('/');
if (slash != std::string::npos) base = base.substr(slash + 1);
if (base.size() > 4 && base.compare(base.size()-4, 4, ".RIX") == 0) base.erase(base.size()-4);
if (base.size() > 4 && base.compare(base.size()-4, 4, ".rix") == 0) base.erase(base.size()-4);
std::string out_path = out_path_override.empty() ? (outdir + "/" + base + ".mid") : out_path_override;
// 建輸出目錄(若不存在)
if (!out_path_override.empty()) {
::mkdir(outdir.c_str(), 0755); // 忽略「已存在」的回錯
}
int rc = convert_one(in, out_path, verbose, note_count);
if (rc == 0)
printf(" [OK] %s -> %s (%d 音符)\n", in.c_str(), out_path.c_str(), note_count);
else if (rc == 3)
printf(" [空] %s (抓到 0 個音符;可能太短或純效果音)\n", in.c_str());
else {
err = out_path;
printf(" [失敗 rc=%d] %s\n", rc, in.c_str());
}
return rc;
}
static void print_usage(const char *prog)
{
printf(
"rix2midi —— 把《大富翁 2》RIX 音樂轉成 MIDI\n"
"\n"
"用法:\n"
" %s <檔案.RIX> [輸出.mid] 轉換單一個 .RIX\n"
" %s <資料夾>/ [輸出資料夾]/ 批次轉換資料夾裡所有 .RIX\n"
" %s -v <…> 顯示詳細訊息tick 數等)\n"
"\n"
"範例:\n"
" %s RICHU.RIX # → RICHU.mid\n"
" %s ./rix ./mid # 批次 → ./mid/*.mid\n"
"\n", prog, prog, prog, prog, prog);
}
int main(int argc, char **argv)
{
bool verbose = false;
std::vector<std::string> args;
for (int i = 1; i < argc; ++i) {
std::string a = argv[i];
if (a == "-v" || a == "--verbose") verbose = true;
else if (a == "-h" || a == "--help") { print_usage(argv[0]); return 0; }
else args.push_back(a);
}
if (args.empty()) { print_usage(argv[0]); return 2; }
std::string first = args[0];
while (first.size() > 1 && first.back() == '/') first.pop_back(); // 去掉結尾多餘的 /
std::string out = (args.size() > 1) ? args[1] : "";
if (first.size() > 4 &&
(first.compare(first.size()-4, 4, ".RIX") == 0 || first.compare(first.size()-4, 4, ".rix") == 0)) {
// 單一檔
int n = 0; std::string err;
std::string outpath = out.empty() ? "" : out;
std::string base = first;
if (base.size() > 4) base.erase(base.size()-4);
if (base.size() > 4) base.erase(base.size()-4);
if (out.empty()) outpath = base + ".mid";
int rc = convert_one(first, outpath, verbose, n);
if (rc == 0) printf(" [OK] %-20s -> %s (%d 音符)\n", first.c_str(), outpath.c_str(), n);
else if (rc == 3) printf(" [空] %s\n", first.c_str());
else { printf(" [失敗 rc=%d] %s\n", rc, first.c_str()); return rc; }
return (rc == 0 || rc == 3) ? 0 : rc;
}
// 批次:把 first 當資料夾
struct dirent **list; int n = ::scandir(first.c_str(), &list, 0, 0);
if (n <= 0) { printf("[!] 資料夾不存在:%s\n", first.c_str()); return 4; }
std::string outdir = out.empty() ? (first + "-mid") : out;
::mkdir(outdir.c_str(), 0755);
int ok = 0, empty = 0, fail = 0;
// 依檔名排序,好讀
std::vector<std::string> files;
for (int i = 0; i < n; ++i) {
std::string fn = list[i]->d_name;
if (fn.size() > 4 && (fn.compare(fn.size()-4, 4, ".RIX") == 0 || fn.compare(fn.size()-4, 4, ".rix") == 0))
files.push_back(first + "/" + fn);
}
std::sort(files.begin(), files.end());
for (auto &f : files) {
int rcn = 0; std::string err;
int rc = process_file(f, outdir, "", verbose, rcn, err);
if (rc == 0) ++ok; else if (rc == 3) ++empty; else ++fail;
}
printf("批次完成:成功 %d空樂曲 %d失敗 %d → 輸出到 %s/\n", ok, empty, fail, outdir.c_str());
return fail ? 1 : 0;
}