首先,说明一下环境:Linux 系统(确切的说是 Ubuntu 12.04) / 从你懂得的地方弄来的 main.pak ,目标是把背景音乐提出来并且转换成常规 PMP 能播放的格式( Rockbox 用户自重!)。
首先,参照前辈( http://blog.163.com/iamyuguo@126/blog/static/32803330201031310228784/ )的方法,对 main.pak 里面的每个字节跟 0xf7 做异或操作,然后再根据里面的路径和大小解包。由于来源处的代码无法被下载,再加上这段代码在 POSIX 风格的系统下跑有点问题(主要是路径里面斜杠和反斜杠的问题,真蛋疼),所以干脆自己照图手抄( tesseract / gocr 等一票 OCR 处理截图就没一个能凑合的)然后稍作修改。以下修改过的代码无法用于 Windows 系统,仅针对 POSIX 系统,请悉知。
- xor.c :
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#define BUFSIZE (8 * 1024)
void error(char const *msg, ... ) {
va_list vlst;
va_start(vlst, msg);
vfprintf(stderr, msg, vlst);
va_end(vlst);
exit(1);
}
int xatoi(char const *s, int radio) {
int ans = 0, i = 0;
static char *st = "0123456789abcdefghijklmnopqrstuvwxyz";
static int t[128] = {0};
static int initnized = 0;
if(!initnized) {
for(i = 0; i < 128; ++ i) t[i] = -1;
for(i = 0; st[i]; ++ i) {
t[(int)st[i]] = i;
if(st[i] >= 'a' && st[i] <= 'z') t[(int)(st[i] - 'a' + 'A')] = i;
}
initnized = 1;
}
for(i = 0; s[i]; ++ i) {
if(t[(int)s[i]] == -1) error("ERROR: unrecognized number: %s\n", s);
ans *= radio;
ans += t[(int)s[i]];
}
return ans;
}
unsigned char str2uchar(char const *s) {
unsigned char ans = 0;
if(s[0] == '0') {
switch(s[1]) {
case 0:
ans = 0;
break;
case 'x':
case 'X':
ans = xatoi(s + 2, 16);
break;
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
ans = xatoi(s + 1, 8);
default:
goto onerror;
}
}
else if('0' <= s[0] && s[0] <= '9') {
ans = xatoi(s, 10);
}
else goto onerror;
return ans;
onerror:
error("ERROR: unknown number: %s\n", s);
return 0;
}
int main(int argc, char *argv[]) {
FILE *pfIn = NULL, *pfOut = NULL;
size_t nRead = 0;
unsigned char buff[BUFSIZE] = {0};
unsigned char markByte = 0;
if(argc != 2 && argc != 4) error("Usage: %s Mark_Byte [main.pak] [Output.pak]\n\nIf filenames are not specified, stdin and stdout will be used.\nDefault value of Mark_Byte should be 0xf7.\n", argv[0]);
markByte = str2uchar(argv[1]);
if(argc == 2) {
pfIn = stdin;
pfOut = stdout;
}
else {
pfIn = fopen(argv[2], "rb");
if(!pfIn) error("ERROR: failed to open file \"%s\" as input!\n", argv[2]);
pfOut = fopen(argv[3], "wb+");
if(!pfOut) error("ERROR: failed to open file \"%s\" as output!\n", argv[3]);
}
while((nRead = fread(buff, 1, BUFSIZE, pfIn))) {
int i;
for(i = 0; i < nRead; ++ i) buff[i] ^= markByte;
if(fwrite(buff, 1, nRead, pfOut) != nRead) error("ERROR: failed to write to output!\n");
if(nRead != BUFSIZE) break;
}
fflush(NULL);
if(argc != 2) {
fclose(pfIn);
fclose(pfOut);
}
pfIn = NULL;
pfOut = NULL;
return 0;
}
- dec.c :
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <stdarg.h>
#include <sys/stat.h>
#include <sys/types.h>
#define BUFSIZE (8 * 1024)
FILE *logfile;
void error(char const *msg, ... ) {
va_list vlst;
va_start(vlst, msg);
vfprintf(stderr, msg, vlst);
va_end(vlst);
exit(1);
}
void error_reading(char const *fn) {
error("ERROR: failed to read from \"%s\"\n", fn);
}
void error_writing(char const *fn) {
error("ERROR: failed to write to \"%s\"\n", fn);
}
void print(char const *msg, ... ) {
va_list vlst;
va_start(vlst, msg);
vfprintf(logfile, msg, vlst);
va_end(vlst);
}
void make_dir(char const *dir_) {
char dir[256];
int i;
strcpy(dir, dir_);
for(i = 0; dir[i] && dir[i] != '/'; ++ i);
if(dir[i] == '/') {
dir[i] = 0;
mkdir(dir, 0755);
if(dir[i + 1] != 0) {
chdir(dir);
make_dir(dir + i + 1);
chdir("..");
}
}
else {
// Shouldn't we do this?
// Maybe not, dir_ is full path with filename.
//mkdir(dir, 0755);
}
}
void save2file(FILE *stream, char const *filename, size_t offset, size_t size) {
unsigned char buff[BUFSIZE];
char dir[256];
char posixfn[256];
size_t pos = ftell(stream);
size_t nRead = 0;
int i;
strcpy(dir, filename);
for(i = 0; dir[i]; ++ i) if(dir[i] == '\\') dir[i] = '/';
for(-- i; i >= 0; -- i) if(dir[i] == '/') {
dir[i + 1] = 0;
break;
}
if(i > 0) make_dir(dir);
strcpy(posixfn, filename);
for(i = 0; posixfn[i]; ++ i) if(posixfn[i] == '\\') posixfn[i] = '/';
FILE *pfOut = fopen(posixfn, "wb+");
if(!pfOut) error("ERROR: failed to open file \"%s\" as output!\n", posixfn);
fseek(stream, offset, SEEK_SET);
while(size > 0) {
if(size > BUFSIZE) {
if((nRead = fread(buff, 1, BUFSIZE, stream)) != BUFSIZE) error_reading("stream");
if(fwrite(buff, 1, BUFSIZE, pfOut) != nRead) error_writing(posixfn);
print(".");
size -= BUFSIZE;
}
else {
if((nRead = fread(buff, 1, size, stream)) != size) error_reading("stream");
if(fwrite(buff, 1, size, pfOut) != nRead) error_writing(posixfn);
print(".");
size = 0;
}
}
fflush(pfOut);
fclose(pfOut);
pfOut = NULL;
fseek(stream, pos, SEEK_SET);
}
int main(int argc, char *argv[]) {
char path[1024];
char fn[256];
unsigned char fnSize;
unsigned char magic[64];
unsigned char *pEndBlock;
int *pBeginBlock;
size_t offset = 0;
FILE *pfIn;
int i = 0;
char *name = "[stdin]";
logfile = fopen("dec.log", "w+");
if(!logfile) logfile = stdout;
pBeginBlock = (int *)magic;
pEndBlock = magic + 12;
if(argc > 2) error("Usage: %s [filename]\n\nIf filename is unspecified, stdin will be used.\nLog is saved to \"dec.log\".\n", argv[0]);
if(argc == 1) pfIn = stdin;
else {
strcpy(path, argv[1]);
name = argv[1];
for(i = 0; path[i]; ++ i) if(path[i] == '\\') path[i] = '/';
for(-- i; i >= 0; -- i) if(path[i] == '/') {
path[i + 1] = 0;
break;
}
if(i > 0) {
print("chdir to \"%s\".\n", path);
chdir(path);
}
pfIn = fopen(argv[1], "rb");
if(!pfIn) error("ERROR: failed to open file \"%s\" as input!\n", argv[1]);
}
if(9 != fread(magic, 1, 9, pfIn)) error_reading(name);
do {
if(1 != fread(&fnSize, 1, 1, pfIn)) error_reading(name);
if(fnSize != fread(fn, 1, fnSize, pfIn)) error_reading(name);
fn[fnSize] = 0;
if(13 != fread(magic, 1, 13, pfIn)) error_reading(name);
print("Found:\t\t%s\t\t%d bytes...\n", fn, *pBeginBlock);
} while(*pEndBlock != 0x80 && *pEndBlock == 0);
offset = ftell(pfIn);
fseek(pfIn, 9, SEEK_SET);
do {
if(1 != fread(&fnSize, 1, 1, pfIn)) error_reading(name);
if(fnSize != fread(fn, 1, fnSize, pfIn)) error_reading(name);
fn[fnSize] = 0;
if(13 != fread(magic, 1, 13, pfIn)) error_reading(name);
print("Extracting file:\t\t%s\t\t0x%08x -> 0x%08x\t\t", fn, offset, offset + *pBeginBlock);
save2file(pfIn, fn, offset, *pBeginBlock);
print("\t\tdone.\n");
offset += *pBeginBlock;
} while(*pEndBlock != 0x80 && *pEndBlock == 0);
if(pfIn != stdin) fclose(pfIn);
fflush(logfile);
if(logfile != stdout) fclose(logfile);
fflush(NULL);
return 0;
}
然后编译它们:
gcc -Wall -o xor xor.c gcc -Wall -o dec dec.c
接着解码 main.pak 然后将解码后的文件解包(请确保你的硬盘还有 500MB 以上剩余空间):
./xor 0xf7 main.pak xored.pak ./dec xored.pak
然后现在你应该可以看到一票子目录了,其中有个 sounds 目录,所有的音频文件都在这个里面。然而,你得先小失望一下,因为除了 ZombiesOnYourLawn.ogg 以外,其他的 Ogg Vorbis 文件全部是音效,另外还有两个奇怪的玩意,mainmusic.mo3 跟 mainmusic_hihats.mo3 ,加起来还不到 2.2MB 。那么我们在找的玩意去哪了呢?
打住,先来谈点别的事情。大家都知道 MIDI 吧?(啥?没听过?95 后?)这种在以前用 Modem 上网的时候在 Web 上和手机铃声中间甚至 Java 游戏里面超流行的音频格式存储的实际上不是音频信息,而是一连串乐器指令。但是系统自带的乐器就那么一坨,万一你想搞点奇怪的东西该怎么办?这个时候就出现了一类叫做 Module 的玩意,除了演奏指令以外还自带一票波形片段和乐器定义,使得音乐创作手段变得更加丰富了。Module 有很多种格式,其中 FastTracker 2 的 eXtended Module ( XM )格式和 ProTracker 的 MOD 格式尤为流行(用来编辑制作 Module 的软件通常被成为 Tracker )。Module 不仅允许自带乐器样本,还提供了丰富的乐器特性设定,以及更多的演奏音效,一个优秀的 Module 所生成的音频的效果往往可以非常震撼,同时保证体积在 MB 级别以下。现在 Module 仍然广泛用于各种游戏中。
然而,自带乐器波形样本就意味着增大体积,在寸 KB 寸金的年代,这怎么能行呢?于是 Module 自带的乐器样本往往只能以非常差的质量存储。同一时间,MPEG 1 Layer 3 等有损音频流压缩算法的成熟,让人们开始思考这样一个问题:能不能用 MP3 或者 Ogg Vorbis 片段取代未压缩的波形文件作为乐器样本插入在 Module 中呢?于是 MO3 ,也就是你在 PvZ 里面见到的奇怪的音乐格式,就诞生了。
MO3 的基础是 XM ,通过一个叫“ unmo3 ”的小程序可以将其解压成 XM 格式,然后就可以经由各种支持 XM 格式的软件处理了。于是,下面接着行动:
sudo apt-get install mikmod milkytracker wget http://us.un4seen.com/files/mo324-linux.zip unzip mo324-linux.zip chmod +x unmo3
在这里安装了两个软件包,其中 mikmod 是一个 CLI 界面的多格式通用 Module 播放器,而 milkytracker 是一个基于 SDL 并且能在 Linux 下运行的 Tracker ,可用于编辑 / 浏览 XM 的内容。Ubuntu 的源里面有个开源的 unmo3 版本,但是似乎不工作,所以还是选择了另行下载。
./unmo3 sounds/mainmusic.mo3 ./unmo3 sounds/mainmusic_hihats.mo3 aoss mikmod sounds/mainmusic.xm aoss mikmod sounds/mainmusic_hihats.xm
解压,然后试听。由于 MikMod 的音频驱动模块是基于 OSS 的,所以在现在的系统中需要在前面套一个“ aoss ”再来执行。
然后你就会发现,PopCap 这个挫货为了节省空间(共用乐器)把所有的 BGM 都塞到 mainmusic.mo3 一个 Module 里面了…… mainmusic_hihats.mo3 里面似乎都是些咚次大慈咚次哒慈的玩意,读者若有兴趣的话也可以研究一下。
接着用 MilkyTracker 打开 mainmusic.xm 开始分析:
milkytracker sounds/mainmusic.xm
你可以点击“ PLAY ”,然后看看这个 Module 是如何在单曲里面 Loop 啊 Loop 啊 Loop 的,然后点击左上角一个小框里面内容在 order 中的项目里面来回切换。
经过分析,mainmusic.xm 的内容如下:
Order number Song Name ===================================== 0x00 - 0x2f: Day Theme Remix 0x30 - 0x4b: Night Theme 0x4c - 0x5b: Night Theme Remix 0x5e - 0x78: Pool Theme Remix 0x7a - 0x7b: Choose Your Seed Loop 0x7d - 0x96: Fog Theme Remix 0x98 - 0x9c: Crazy Dave Theme 0x9e - 0xa4: Boss Theme 0xa6 - 0xaf: Loonboon 0xb1 - 0xb6: Cerebrawl 0xb8 - 0xd2: Roof Theme Remix 0xd4 - 0xdb: Conveyor Theme 0xdd - 0xeb: Zen Garden Theme
如果觉得音乐跟 iOS 上的版本不一样,就 Mute 掉 Channel 20 到 30 再试试吧。
接着要把这些片段渲染成波形,应为一般的 PMP 是没法播放 XM 的。MikMod 自带了 Disk Writer ,但是很不凑巧这二货居然不支持选段,并且上一段乐器演奏的效果有时候会拖到下一段。于是下面重点来了,我花了点时间炮制了一小段使用 libmikmod 的程序(如果需要的话,代码以 GNU GPL 授权):
- segmod.c :
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <stdarg.h>
#include <unistd.h>
#include <mikmod.h>
char *banner = "\
Segmentable Audio Module Renderer\n\
Compiled with MikMod Sound Library version %ld.%ld.%ld\n\n";
char *usage = "\
Usage:\t\t%s Order_Begin Order_End Input_Filename [Output_Filename]\n\n\
Order_Begin and Order_End can be given in dec/oct/hex forms.\n\
If Output_Filename is left blank, \"music.wav\" will be used.\n\
Position control is not accurate, limited by the buffer size of MikMod's drv_wav.\n";
void error(char const *msg, ... ) {
va_list vlst;
va_start(vlst, msg);
vfprintf(stderr, msg, vlst);
va_end(vlst);
exit(1);
}
int xatoi(char const *s, int radio) {
int ans = 0, i = 0;
static char *st = "0123456789abcdefghijklmnopqrstuvwxyz";
static int t[128] = {0};
static int initnized = 0;
if(!initnized) {
for(i = 0; i < 128; ++ i) t[i] = -1;
for(i = 0; st[i]; ++ i) {
t[(int)st[i]] = i;
if(st[i] >= 'a' && st[i] <= 'z') t[(int)(st[i] - 'a' + 'A')] = i;
}
initnized = 1;
}
for(i = 0; s[i]; ++ i) {
if(t[(int)s[i]] == -1) error("ERROR: unrecognized number: %s\n", s);
ans *= radio;
ans += t[(int)s[i]];
}
return ans;
}
unsigned char str2uchar(char const *s) {
unsigned char ans = 0;
if(s[0] == '0') {
switch(s[1]) {
case 0:
ans = 0;
break;
case 'x': case 'X':
ans = xatoi(s + 2, 16);
break;
case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7':
ans = xatoi(s + 1, 8);
break;
default: goto onerror;
}
}
else if('0' <= s[0] && s[0] <= '9') ans = xatoi(s, 10);
else goto onerror;
return ans;
onerror:
error("ERROR: unknown number: %s\n", s);
return 0;
}
int main(int argc, char *argv[]) {
printf(banner, LIBMIKMOD_VERSION_MAJOR, LIBMIKMOD_VERSION_MINOR, LIBMIKMOD_REVISION);
if((argc < 4) || (argc > 5) || ((argc > 1) && (argv[1][0] == '-'))) error(usage, argv[0]);
int ord_start = str2uchar(argv[1]);
int ord_end = str2uchar(argv[2]);
if(ord_start > ord_end) {
fprintf(stderr, "NO MAKING FUN OF ME, Okay?\n\n");
error(usage, argv[0]);
}
MODULE *module;
char *infn = argv[3];
char *outfn = "";
if(argc == 5) outfn = argv[4];
MikMod_RegisterDriver(&drv_wav);
MikMod_RegisterAllLoaders();
md_device = 1;
md_mixfreq = 48000;
md_mode = DMODE_INTERP | DMODE_SOFT_MUSIC | DMODE_SURROUND | DMODE_16BITS | DMODE_HQMIXER | DMODE_SOFT_SNDFX | DMODE_STEREO;
if(MikMod_Init(outfn)) error("Could not initialize sound, reason: %s\n", MikMod_strerror(MikMod_errno));
module = Player_Load(infn, 256, 0);
if(module) {
printf("Name: %s \tType: %s\nComments: %s\n", module -> songname, module -> modtype, module -> comment);
printf("%d Channels, %d Patterns, %d Instruments, %d Samples.\n", module -> numchn, module -> numpat, module -> numins, module -> numsmp);
printf("Available order range: 0x0000 - 0x%04x ( 0 - %d )\n", module -> numpos - 1, module -> numpos - 1);
printf(">Selected order range: 0x%04x - 0x%04x ( %d - %d )\n", ord_start, ord_end, ord_start, ord_end);
if(ord_start > (module -> numpos)) {
fprintf(stderr, "Sorry, but it seems that ends does not meet.\n\n");
error(usage, argv[0]);
}
module -> wrap = 0;
module -> loop = 0;
Player_Start(module);
Player_SetPosition(ord_start);
while (Player_Active() && (module -> sngpos <= ord_end)) MikMod_Update();
Player_Stop();
Player_Free(module);
}
else error("Could not load module, reason: %s\n", MikMod_strerror(MikMod_errno));
printf("\nAll done! Enjoy!\n");
MikMod_Exit();
return 0;
}
经过试验,我欣喜地发现它工作得非常的好,虽然由于某些原因输出文件的名字总是 music.wav ……( libmikmod 的文档打死都不说 MikMod_Init 的 Parameter 是啥格式啊,单独喂文件名又不认,改天再研究好了。)
使用这段代码的步骤如下:
sudo apt-get install libmikmod2-dev gcc -o segmod segmod.c `libmikmod-config --cflags` `libmikmod-config --libs` ./segmod 0x00 0x2f sounds/mainmusic.xm mv music.wav Day\ Theme\ Remix.wav ./segmod 0x30 0x4b sounds/mainmusic.xm mv music.wav Night\ Theme.wav
如此,把 13 个文件都提取并渲染出来,最后再用喜好的编码器将 wav 转成 FLAC 、Ogg 一类的文件,塞进 PMP ,就大功告成了。