From b728d95a78a9bd66647086a4fca079b8fba5b167 Mon Sep 17 00:00:00 2001 From: Wiwi Kuan Date: Sat, 22 Aug 2026 05:52:55 +0000 Subject: [PATCH] =?UTF-8?q?=E4=B8=8A=E5=82=B3=E6=AA=94=E6=A1=88=E5=88=B0?= =?UTF-8?q?=E3=80=8C/=E3=80=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 85 ++++++++++ build.sh | 63 ++++++++ rix2midi | Bin 0 -> 116064 bytes rix2midi.cpp | 448 +++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 596 insertions(+) create mode 100644 README.md create mode 100644 build.sh create mode 100644 rix2midi create mode 100644 rix2midi.cpp 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 0000000000000000000000000000000000000000..325fab638f40bd9484073cf48df1c93ec375cc1d GIT binary patch literal 116064 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 literal 0 HcmV?d00001 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; +}