上傳檔案到「/」
This commit is contained in:
85
README.md
Normal file
85
README.md
Normal file
@@ -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/`)。
|
||||||
63
build.sh
Normal file
63
build.sh
Normal file
@@ -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"
|
||||||
448
rix2midi.cpp
Normal file
448
rix2midi.cpp
Normal file
@@ -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 <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>
|
||||||
|
|
||||||
|
// ── 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<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 Program(31=方波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_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<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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user