这样一来,我只需要编写一个专门用来识别并提取AT3文件的程序就行了。事实上,我用 WinHex
将AT3文件打开后,发现其头部令人感到非常眼熟,像极了WAV文件,因为其开头赫然可见RIFF
WAVEfmt
。
所以我只需要写一个专门识别 RIFF
魔法数字,然后根据紧随其后的文件剩余大小数据来判断整个音频文件的大小,再做提取就行了。
代码的编写
我们需要遍历一个具有一定体积的文件,判断其中是否有 RIFF
标识。假定二进制文件中的音频文件的位置并非4字节对齐存储,因此需要对其中的每个字节开始的4个字节进行判断。并且有时候就算找到了 RIFF
标识,它也不一定真就是一个音频文件的开头,有可能是别的数据正好是 0x46464952
,然后将其当作小端存储的数值,并以字符串的方式来看它的话,它就是 RIFF
了,所以要排除这样的干扰,避免做出错误的Rip。这里我的做法是继续判断其后应该有的 WAVEfmt
标识是否存在,如果其存在,才进行Rip操作。
因为文件具有一定体积,所以如果我进行频繁的读取和判断的操作,整个过程就会非常没有效率。合理的做法是申请一块大小合适的内存作为缓存,每次读取文件的时候将一定量的文件内容读入这个缓存,再对缓存中的数据做比对判断操作。
需要注意的是,当使用 for
循环语句对缓冲区中的每个字节做判断的时候, 缓冲区尾部 的数据并不能被完整判断。因此需要做一定的处理来保证这些数据在下次循环遍历缓冲区的时候也要被遍历到。
在找到符合AT3文件特征的地方的时候,我们就要把这个AT3文件给它Rip出来。而这个时候,如何给AT3文件起名就成了一个问题。不过我用了一个简单的办法——直接用这个AT3文件在资源文件中的存储位置十六进制值作为其名字来存储。
整个代码并不长。我的处理方式也很简单。也没写注释。
#include <stdio.h>
#include <stdlib.h>
#include <inttypes.h>
#include <string.h>
#include <errno.h>
#include <sys/stat.h>
#define BUFFER_SIZE (1024 * 1024)
int rip_from_file(const char *file, fpos_t Offset, uint32_t size)
{
char *Buffer = NULL;
char TargetFileName[64];
uint32_t remain = size;
struct stat fstat_buffer;
int FileNameAlt = 0;
FILE *fpr = NULL;
FILE *fpw = NULL;
fpr = fopen(file, "rb");
if (!fpr)
{
fprintf(stderr, "Read file \"%s\" failed: %s.\n", file, strerror(errno));
goto FailExit;
}
fsetpos(fpr, &Offset);
for(;;)
{
if (!FileNameAlt)
snprintf(TargetFileName, sizeof TargetFileName, "%08"PRIx32".wav", ftell(fpr));
else snprintf(TargetFileName, sizeof TargetFileName, "%08"PRIx32"_%d.wav", ftell(fpr), FileNameAlt);
if (!stat(TargetFileName, &fstat_buffer))
{
printf("Warning: output file \'%s\' already exists.\n");
FileNameAlt ++;
}
else
{
errno = 0;
break;
}
}
printf("Writing to \"%s\" (%"PRIu32", 0x%"PRIx32" bytes)\n", TargetFileName, size, size);
fpw = fopen(TargetFileName, "wb");
if (!fpw)
{
fprintf(stderr, "Write file \"%s\" failed: %s.\n", TargetFileName, strerror(errno));
goto FailExit;
}
Buffer = malloc(BUFFER_SIZE);
if (!Buffer)
{
perror("Allocating memory for copying");
goto FailExit;
}
while(!feof(fpr))
{
size_t ReadAmount = BUFFER_SIZE;
if (ReadAmount > remain) ReadAmount = remain;
ReadAmount = fread(Buffer, 1, ReadAmount, fpr);
if (fwrite(Buffer, 1, ReadAmount, fpw) != ReadAmount)
{
perror("Attempting to write ripped file");
}
remain -= ReadAmount;
if (!remain) break;
}
fclose(fpr);
fclose(fpw);
free(Buffer);
return 1;
FailExit:
if (fpr) fclose(fpr);
if (fpw) fclose(fpw);
free(Buffer);
return 0;
}
uint32_t MagicOf(const char *feature)
{
return *(uint32_t*)feature;
}
#define FEATURE_SIZE 16
int scan_file(const char *file)
{
int Remain = 0;
size_t ReadAmount;
char *Buffer = NULL;
FILE *fp = fopen(file, "rb");
if (!fp)
{
fprintf(stderr, "Read file \"%s\" failed: %s.\n", file, strerror(errno));
goto FailExit;
}
Buffer = malloc(BUFFER_SIZE);
if (!Buffer)
{
perror("Allocating memory for scanning");
goto FailExit;
}
for(;;)
{
size_t i;
ReadAmount = fread(Buffer, 1, BUFFER_SIZE, fp);
for(i = 0; i < ReadAmount - FEATURE_SIZE; i++)
{
uint32_t *Fields = (uint32_t*)&Buffer[i];
if (Fields[0] == MagicOf("RIFF") &&
Fields[2] == MagicOf("WAVE") &&
Fields[3] == MagicOf("fmt "))
{
fpos_t CurPos;
fseek(fp, i - BUFFER_SIZE, SEEK_CUR);
fgetpos(fp, &CurPos);
fseek(fp, BUFFER_SIZE - i, SEEK_CUR);
rip_from_file(file, CurPos, Fields[1] + 4);
}
}
if (ReadAmount < BUFFER_SIZE) break;
fseek(fp, -FEATURE_SIZE, SEEK_CUR);
}
free(Buffer);
fclose(fp);
return 1;
FailExit:
free(Buffer);
if (fp) fclose(fp);
return 0;
}
int main(int argc, char const *argv[])
{
int i;
for (i = 1; i < argc; i++)
{
printf("Scanning file \"%s\":\n", argv[i]);
printf(scan_file(argv[i]) ? "Success\n" : "Not completed\n");
}
return 0;
}
写好后,懒得用VS开工程了,直接用WSL编译,然后运行,很快就把《流行り神2》的音频都Rip出来了。