首先,说明一下环境: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 ,就大功告成了。