commit b728d95a78a9bd66647086a4fca079b8fba5b167 Author: Wiwi Kuan Date: Sat Aug 22 05:52:55 2026 +0000 上傳檔案到「/」 diff --git a/README.md b/README.md new file mode 100644 index 0000000..2ad85bb --- /dev/null +++ b/README.md @@ -0,0 +1,85 @@ +# rix-midi —— 《大富翁 2》RIX → MIDI 轉換器 + +把 DOS 遊戲《大富翁 2》的 `.RIX` 音樂檔,轉成標準 `.MIDI` 樂譜。 +可以一次轉單一個、也可以整個資料夾批次轉。 + +> 同一批 `.RIX` 用 **adplay / AdPlug** 也可以轉成 WAV(純音訊), +> 請看 `../wav/`。這裡只處理「轉成 MIDI 樂譜」這件比較有挑戰的事。 + +--- + +## 為什麼 RIX 不能直接「拿出來」當 MIDI? + +`.RIX` **不是存音符**的格式。它是 **AdLib OPL 晶片**的「機器碼」: +一段由「時鐘節拍(tick)」驅動的下載程式,每個 tick 告訴 OPL 晶片 +要寫哪些「暫存器」(頻率、開聲、音量……)。所以 RIX 裡只有 +「要讓硬體發出什麼」的指令,沒有現成的音符——得「還原」出來。 + +## 原理(我們怎麼還原) + +1. **借用 AdPlug** 來 decode + 模擬 OPL 晶片(它已把 RIX 的機器碼 + 正確地「跑成」一串 OPL 暫存器寫入)。 +2. **劫持 OPL 的 write()**:AdPlug 發聲前會走 `Copl::write(reg,val)`, + 我們繼承它的模擬器、順手把「每個 tick、每個聲道的 A / B 暫存器」記下。 +3. **暫存器 → 頻率 → 音符**(OPL 的標準公式): + ``` + F-number = A暫存器(低8位) | B暫存器bit0-1(高2位) + block = B暫存器 bit2-4 (決定八度倍頻) + key-on = B暫存器 bit5 (1=開、0=關) + 頻率Hz = F-number × 2^block × 3579545 / (72 × 2^24) × 8 + MIDI音符 = round( 69 + 12×log2( 頻率 / 440 ) ) # 69 = A4, 60 = 中音C + ``` +4. **節奏一致**:測出 RIX 是「70 tick / 秒」。把 MIDI 速度設成 + `tempo = 1e6 × 96 ÷ 70 ≈ 1,371,428 µs/quat`(96 ticks/quat), + 讓 1 個 RIX tick == 1 個 MIDI tick,整首的時值就和原曲一致。 +5. **樂器**:OPL 不分旋律/和聲,依每個聲道音高的「中位數」粗略指派 + GM 樂器(偏低→貝斯、其餘→方波 Lead)。聽起來像原曲的務實選擇。 + +### 已驗證 +對全部 23 首,**MIDI 時長與同曲 WAV 時長比對:22 首幾乎完全一致(ratio 1.00)**、 +1 首(RICHX)因 WAV 尾端多靜音而略短。音高也抽樣驗證正確(中音 C=60)。 + +--- + +## 編譯 + +依賴 **AdPlug**(RIX/OPL)與 **libbinio**。`build.sh` 會自動找/編它們, +再以**靜態連結**產出自給自足的執行檔(不用設 `LD_LIBRARY_PATH`): + +```bash +./build.sh # 在本機 AdPlug=/tmp/AdPlug、libbinio=/tmp/libbinio 的前提下 +``` + +想指定位置: +```bash +ADPLUG=/path/to/AdPlug BINIO=/path/to/libbinio ./build.sh +``` + +## 使用 + +```bash +./rix2midi --help + +# 單一個 .RIX → 同名 .mid +./rix2midi ../RICHU.RIX # 產出 RICHU.mid +./rix2midi ../RICHU.RIX 我的.mid # 指定輸出檔名 + +# 批次:把「整個資料夾」裡所有 .RIX 轉到「輸出資料夾」 +./rix2midi ../ ../mid # ../ 所有 .RIX → ../mid/*.mid +./rix2midi -v ../ ../mid # 加 -v 看 tick 數等詳細訊息 +``` + +## 檔案 + +| 檔案 | 說明 | +|--------------|----------------------------------------| +| `rix2midi.cpp` | 轉換器本體(含完整中文註解) | +| `build.sh` | 一鍵編譯(找/編 AdPlug、靜態連結) | +| `rix2midi` | 編譯出的執行檔 | + +## 小限制(MIDI 本質) + +- 只能還原 OPL「發聲的音色」層次的音高+時值,是**旋律骨架**, + 不是完整編曲(沒有真正的和聲/鼓聲)——這是 RIX/OPL 格式的天花板。 +- 樂器指派是「猜」的(依音域),不是原曲精確編配。 +- 若你想要「聽起來就等於 DOS 原版」的 WAV,用 adplay 那一套(`../wav/`)。 diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..93574c2 --- /dev/null +++ b/build.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# ============================================================================ +# build.sh —— 編譯 rix2midi(《大富翁 2》RIX → MIDI 轉換器) +# ---------------------------------------------------------------------------- +# 會做的事: +# 1. 找到 AdPlug(負責 RIX 解碼 + OPL 晶片模擬)與 libbinio(AdPlug 依賴的 +# 小型 I/O 庫)——本機預設看 /tmp/AdPlug 與 /tmp/libbinio。 +# 2. 若它們還沒編過,就現編一次。 +# 3. 以「靜態連結」把 rix2midi 編出來 → 執行檔自給自足、不用設環境變數。 +# +# 用法: +# ./build.sh +# +# 若想指定 AdPlug / binio 的位置(一般不必): +# ADPLUG=/path/to/AdPlug BINIO=/path/to/libbinio ./build.sh +# +# 編譯完成後: +# ./rix2midi ../RICHU.RIX # 轉單一個 +# ./rix2midi ../ ../mid # 批次轉 ../ 裡所有 .RIX → ../mid/*.mid +# ============================================================================ +set -e +cd "$(dirname "$0")" + +# ---- 找到 AdPlug / libbinio(優先用環境變數,其次 /tmp 現成的)----------- +pick() { # pick VAR 預設值... + local v="$1"; shift + local d + for d in "$@"; do + [ -f "$d/Makefile" ] && { v="$d"; return 0; } + done + echo "$v" +} +ADPLUG="$(pick "${ADPLUG:-/tmp/AdPlug}" /tmp/AdPlug "$ADPLUG" ./vendor/AdPlug)" +BINIO="$(pick "${BINIO:-/tmp/libbinio}" /tmp/libbinio "$BINIO" ./vendor/libbinio)" +[ -f "$ADPLUG/Makefile" ] || ADPLUG=/tmp/AdPlug +[ -f "$BINIO/Makefile" ] || BINIO=/tmp/libbinio +echo "[build] AdPlug = $ADPLUG" +echo "[build] libbinio= $BINIO" + +# ---- libbinio / AdPlug 若還沒編,就編(以靜態 .a 為準) ------------------ +if [ ! -f "$BINIO/build/libbinio_local.a" ]; then + echo "[build] 編譯 libbinio …" + ( cd "$BINIO" && make -j"$(nproc)" ) +fi +if [ ! -f "$ADPLUG/build/libadplug.a" ]; then + echo "[build] 編譯 AdPlug(約一分鐘)…" + ( cd "$ADPLUG" && make -j"$(nproc)" ) +fi + +[ -f "$ADPLUG/build/libadplug.a" ] || { echo "[X] 找不到 libadplug.a"; exit 1; } +[ -f "$BINIO/build/libbinio_local.a" ] || { echo "[X] 找不到 libbinio_local.a"; exit 1; } + +# ---- 組編譯參數(全靜態、自給自足)------------------------------------- +INC="-I$ADPLUG/src -I$BINIO/build/inc -I$BINIO/src" +LIBS="-L$ADPLUG/build -ladplug -L$BINIO/build -lbinio_local" +echo "[build] 開始編譯 rix2midi.c++(實為 .cpp)…" +g++ -std=c++14 -O1 -Wall $INC rix2midi.cpp -o rix2midi $LIBS + +# ---- 驗證執行檔能跑 ------------------------------------------------------ +echo +echo "[build] 完成!執行檔:$(pwd)/rix2midi" +./rix2midi --help | head -5 +echo "範例: ./rix2midi ../RICHU.RIX 或 ./rix2midi ../ ../mid" diff --git a/rix2midi b/rix2midi new file mode 100644 index 0000000..325fab6 Binary files /dev/null and b/rix2midi differ diff --git a/rix2midi.cpp b/rix2midi.cpp new file mode 100644 index 0000000..d02517a --- /dev/null +++ b/rix2midi.cpp @@ -0,0 +1,448 @@ +// ============================================================================ +// 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-on(1=開聲,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。 +// (這是「聽起來像原曲」的務實選擇,不是精確編曲。) +// +// 【如何編譯】 +// 需要 AdPlug(RIX 解碼 + OPL 模擬)。請先執行同目錄的 build.sh: +// ./build.sh +// 它會自動找到/安裝 AdPlug 與 C++ 庫,再編譯出 rix2midi 執行檔。 +// +// 【如何使用】 +// ./rix2midi 某個.RIX # 轉单个檔 → 同名 .mid +// ./rix2midi 某個.RIX 輸出.mid # 指定輸出檔 +// ./rix2midi 整個資料夾/ [輸出/]/ # 批次轉該資料夾所有 .RIX +// +// ============================================================================ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// ── AdPlug(RIX 解碼 + OPL 晶片模擬)────────────────────────────── +#include "opl.h" // Copl 基類(OPL 晶片寫入介面) +#include "emuopl.h" // CEmuopl:真正的 OPL 模擬實作 +#include "rix.h" // CrixPlayer:RIX 檔案的「排程器」 +#include "fprovide.h" // CProvider_Filesystem:從硬碟讀檔 +#include "player.h" // CPlayer 基類、通用介面 +#include "database.h" // CAdPlugDatabase(AdPlug 需要的資料庫單例) + +// 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 拍數用 96(96 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..255),val = 要寫入的值。 + // 聲道 = 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-number(10 位低位)+ 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-number(10 位) + int blk = (raw >> 10) & 7; // block(3 位,決定八度倍頻) + 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 &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 Program(31=方波Lead、32=鋸齒貝斯、35=方波2)。 +static int pick_instrument(const std::vector &midi_notes) +{ + if (midi_notes.empty()) return 31; + std::vector 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 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; // (tick, 0=on/1=off, ch, note) + std::vector 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 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 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_tempo(meta) + 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 off(velocity=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 ¬e_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 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 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; +}