从 PvZ 的 main.pak 里面提取背景音乐

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

发表评论

电子邮件地址不会被公开。 必填项已用*标注