在上大一,周末闲得没事搞了这个,就当记录下学习wav的历程了。编程技术尚且稚嫩,如有错误欢迎指正,大佬轻喷。
目前只实现了去杂音,单声道转双声道有待进一步研究。
先上结论:用"xor "里面的数据对"data"逐字节异或运算,并去除文件头中的"dep "和"xor "两个chunk,杂音去除完成。
这一切要从六年前说起,那会儿还在上初中,打完一周目植物大战僵尸冒险模式之后突然对游戏里音频的变化燃起了兴趣,于是开始扒游戏文件。最浅层的就是cached/sounds文件夹里的一堆wav音频(后来才发现上当了,真正的音频在main.pak里,有mo3格式的还得用xmplay打开),结果用常规的播放器提示音频损坏,打不开。用Audition倒是打开了,但是杂音特别大。加上自己不会用Audition,一通操作之后发现杂音更大了……
咳咳,扯远了。现在学了c语言文件操作,又心血来潮重启了六年前的研究,用了一个上午+半个下午的时间研究了一下wav格式,并分析了这些音频。
先来简单了解下wav文件:
wav文件遵循RIFF格式,文件结构如下:
每个chunk包括ID和Size组成的开头部分和其他数据组成的数据部分。ID为四字节字符串(无\0,用空格填空),如RIFF chunk的ID为"RIFF",fmt chunk的ID为"fmt "。Size为long类型,四个字节,代表此chunk除ID和Size外占的大小。字符串为大端存储,其他数据为小端存储。
RIFF chunk为所有chunk的母chunk,数据部分包含一个Form Type表示文件格式,wav文件为"WAVE";Form Type下面为subchunk,对于wav一般含有"fmt "(Audio Format,存储声道数、采样率等信息)和"data"(音频数据,为最后一个chunk),有些还有"LIST"(可以包含subchunk)。RIFF的所有chunk可以在RIFF Tags (exiftool.org)中查到。
常规的wav结构如下图:
subchunk1为"fmt ",subchunk2为"data",不包含"LIST"等其他chunk。
了解了这些,我们先列出所有的chunk(没考虑大端机):
//chunk的开头部分
typedef struct __attribute__((packed)) CHUNK_HEAD {
char chunk_id[4];
uint32_t chunk_size;
} chunk_head;
//因为没有\0,所以自己写了相关操作
//比较字符串
int chunkID_compare(const char *ID1, const char *ID2) {
return ID1[0] == ID2[0] && ID1[1] == ID2[1] && ID1[2] == ID2[2] && ID1[3] == ID2[3];
}
//输出字符串
void printID(const char *ID) {
for (int i = 0;i < 4;i++) {
printf("%c", ID[i]);
}
}
void list_chunks(const char *file) {
//打开文件,不存在时报错
FILE *f = fopen(file, "rb");
if (f == NULL) {
printf("文件打开失败!\n");
return;
}
//读取RIFF chunk
char format[4];
chunk_head *head = (chunk_head *) malloc(sizeof(chunk_head));
fread(head, sizeof(chunk_head), 1, f);
fread(format, 4, 1, f);
printf("ID: ");
printID(head->chunk_id);
printf("\nSize: %u\n", head->chunk_size);
printf("Format: ");
printID(format);
printf("\n");
//读取subchunk,不包括LIST的subchunk,读完data后停止
while (!chunkID_compare(head->chunk_id, "data")) {
fread(head, sizeof(chunk_head), 1, f);
//输出ID
printf(" ID: ");
printID(head->chunk_id);
//输出Size
printf(" Size: %u\n", head->chunk_size);
//跳过数据部分
fseek(f, head->chunk_size, SEEK_CUR);
}
free(head);
fclose(f);
}
以舞王僵尸出场音效为例,用list_chunks()函数获取dancer.wav的所有chunks,结果如下:
ID: RIFF
Size: 732396
Format: WAVE
ID: fmt Size: 16
ID: dep Size: 27
ID: xor Size: 1
ID: data Size: 732360
发现这个文件比常规文件多出了"dep "和"xor "两个chunk,在网上查了很久也没有发现其他文件有相关案例,基本可以判定是这两个chunk影响了播放器对格式的检验。且Size大小也不正确,应为732440。
我们先修改Size(虽然不影响播放):
void recalculate_size(const char *file, const char *dest) {
//打开文件,不存在时报错
FILE *f = fopen(file, "rb");
if (f == NULL) {
printf("文件打开失败!");
return;
}
//创建目标文件
FILE *d = fopen(dest, "wb");
if (d == NULL) {
printf("文件创建失败!");
fclose(f);
return;
}
//读取和写入RIFF chunk的ID、Size和Format
char format[4];
chunk_head *head = (chunk_head *) calloc(1, sizeof(chunk_head));
fread(head, sizeof(chunk_head), 1, f);
fread(format, 4, 1, f);
fwrite(head, sizeof(chunk_head), 1, d);
fwrite(format, 4, 1, d);
//读取和写入各个subchunk,并计算大小
char *content;
uint32_t size = 4;//初始值为4,即format("WAVE")的大小
while (!chunkID_compare(head->chunk_id, "data")) {
fread(head, sizeof(chunk_head), 1, f);
content = (char *) malloc(head->chunk_size);
fread(content, head->chunk_size, 1, f);
fwrite(head, sizeof(chunk_head), 1, d);
fwrite(content, head->chunk_size, 1, d);
free(content);
size += 8 + head->chunk_size;//8为ID和Size的大小,head->chunk_size为数据大小
}
//将文件指针调整到开头,覆盖先前写入的ID和Size
fseek(f, 0, SEEK_SET);
fseek(d, 0, SEEK_SET);
fread(head, sizeof(chunk_head), 1, f);
head->chunk_size = size;
fwrite(head, sizeof(chunk_head), 1, d);
free(head);
fclose(f);
fclose(d);
}
修改过后再调用list_chunks(),结果如下:
ID: RIFF
Size: 732440
Format: WAVE
ID: fmt Size: 16
ID: dep Size: 27
ID: xor Size: 1
ID: data Size: 732360
可以发现Size正确了。
接下来处理用xor处理data:
void data_xor(const char *file, const char *dest) {
//打开文件,不存在时报错
FILE *f = fopen(file, "rb");
if (f == NULL) {
printf("文件打开失败!");
return;
}
//创建目标文件
FILE *d = fopen(dest, "wb");
if (d == NULL) {
printf("文件创建失败!");
fclose(f);
return;
}
//读取和写入RIFF chunk的ID、Size和Format
char format[4];
chunk_head *head = (chunk_head *) malloc(sizeof(chunk_head));
fread(head, sizeof(chunk_head), 1, f);
fread(format, 4, 1, f);
fwrite(head, sizeof(chunk_head), 1, d);
fwrite(format, 4, 1, d);
//读取和写入各个subchunk,并寻找xor chunk
char *content;
char *xor = NULL;
while (1) {
fread(head, sizeof(chunk_head), 1, f);
content = (char *) malloc(head->chunk_size);
fread(content, sizeof(char), head->chunk_size, f);
//找到xor chunk,为xor分配内存并存储数据
if (chunkID_compare(head->chunk_id, "xor ")) {
xor = (char *) malloc(sizeof(char));
*xor = *content;
}
//读取到data chunk时终止,保留head和content
if (chunkID_compare(head->chunk_id, "data")) {
//xor为NULL代表未找到xor chunk,终止操作
if (xor == NULL) {
printf("不含xor chunk!\n");
free(content);
free(head);
fclose(f);
fclose(d);
remove(dest);
return;
}
break;
}
//写入数据到新文件
fwrite(head, sizeof(chunk_head), 1, d);
fwrite(content, sizeof(char), head->chunk_size, d);
free(content);
}
//执行xor运算
for (int i = 0;i < head->chunk_size;i++) {
content[i] = content[i] ^ *xor;
}
//写入操作后的data chunk
fwrite(head, sizeof(chunk_head), 1, d);
fwrite(content, sizeof(char), head->chunk_size, d);
free(xor);
free(content);
free(head);
fclose(f);
fclose(d);
}
至此,数据修复完成。但因为两个多余chunk的存在,播放器仍不能识别文件。接下来移除多余的chunk:
void remove_chunk(const char *file, const char *dest, const char *chunk) {
//打开文件,不存在时报错
FILE *f = fopen(file, "rb");
if (f == NULL) {
printf("文件打开失败!");
return;
}
//创建目标文件
FILE *d = fopen(dest, "wb");
if (d == NULL) {
printf("文件创建失败!");
fclose(f);
return;
}
//读取和写入RIFF chunk的ID、Size和Format
char format[4];
chunk_head *head = (chunk_head *) calloc(1, sizeof(chunk_head));
fread(head, sizeof(chunk_head), 1, f);
fread(format, 4, 1, f);
fwrite(head, sizeof(chunk_head), 1, d);
fwrite(format, 4, 1, d);
//记录当前riff chunk大小
unsigned long size = head->chunk_size;
//读取和写入各个subchunk,跳过指定的subchunk
char *content;
while (!chunkID_compare(head->chunk_id, "data")) {
fread(head, sizeof(chunk_head), 1, f);
content = (char *) malloc(head->chunk_size);
fread(content, sizeof(char), head->chunk_size, f);
//若非指定的chunk,则写入新文件,否则减去该chunk的大小
if (!chunkID_compare(head->chunk_id, chunk)) {
fwrite(head, sizeof(chunk_head), 1, d);
fwrite(content, sizeof(char), head->chunk_size, d);
} else {
size -= head->chunk_size + 8;
}
free(content);
}
//重写开头,覆盖掉先前的Size
fseek(f, 0, SEEK_SET);
fseek(d, 0 ,SEEK_SET);
fread(head, sizeof(chunk_head), 1, f);
head->chunk_size = size;
fwrite(head, sizeof(chunk_head), 1, d);
free(head);
fclose(f);
fclose(d);
}
通过remove_chunk()函数移除"xor "和"dep "两个chunk。至此,文件已可以正常播放且没有杂音。
但此时如果读取fmt chunk的内容的话可以发现channels为1,即单声道,而解包main.pak文件中的音频都为双声道。可想而知dep chunk的作用和声道有关,但本人还没有研究出dep chunk的使用方法,所以暂且就直接删掉了。日后了解了会补充上的。
总之,通过xor和dep两把钥匙,能够将音频逆向处理为先前的样子。我不是很理解开发者为什么在处理这些根本不会使用的音频后还要留下处理的方式。如果这是某种常用的处理方式的话,欢迎大佬来科普。