0xAA55 发表于 2021-6-28 15:16:53

【C语言】将PSP游戏《流行り神2》里的音乐和音效提取出来

# PSP游戏音频提取

## PSP游戏对于游戏资源存储方式的特点

PSP游戏的音频提取,其实意外地很简单,不少PSP游戏的音频资源和音乐直接以AT3文件的形式存放在其ISO镜像文件的文件目录中。AT3文件是索尼PSP游戏使用的一种特定的音频编码格式文件的后缀,编码名为 `Adaptive Transform Acoustic Coding 3+` ,可以使用 `foobar2000` 播放器配合AT3解码器插件来播放、转换格式。

虽然有的游戏比如《悪魔城X》是这样做的,但有的游戏则不是。比如《流行り神2》和《Gods of War 2》。其中,《流行り神2》的BGM和音效等各种资源都在其 `PSP_GAME/USRDIR/DATA.DAT` 文件里以某种形式的二进制方式编码。但因为这样的数据文件通常不会被压缩,也不会被加密,其内容就像一个接一个的文件被直接拼接进去一样,提取其中的音效素材只需要找到音频文件的特征码,然后根据其文件头指示的文件大小直接将文件复制出来即可。

## PSP游戏《流行り神2》的音频文件提取

我使用二进制编辑器 `WinHex` 查看了《流行り神2》的ISO文件中的每个文件,除了标题的BGM是一个AT3文件以外,其它的音频内容都在 `DATA.DAT` 里。

这样一来,我只需要编写一个专门用来识别并提取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;
                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;
                                if (Fields == MagicOf("RIFF") &&
                                  Fields == MagicOf("WAVE") &&
                                  Fields == 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 + 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);
                        printf(scan_file(argv) ? "Success\n" : "Not completed\n");
                }
                return 0;
        }

写好后,懒得用VS开工程了,直接用WSL编译,然后运行,很快就把《流行り神2》的音频都Rip出来了。

使用 `foobar2000` 播放的时候,我发现这款游戏的所有音乐和音效确实都被Rip出来了。爽。不过如果要问我:为什么是《流行り神2》?因为当年玩这游戏时被这BGM精神污染了,现在不听到它我就会有点不舒服。

流行之神2提取出来的音频的MP3格式请回帖后下载。
**** Hidden Message *****

watermelon 发表于 2021-6-29 12:54:10

可以可以,代码非常优雅!

Golden Blonde 发表于 2021-6-30 08:01:29

希望坛主给个用WSL编译程序的教程。

元始天尊 发表于 2021-7-18 21:39:44

单识别的话,010Editor一个模板就搞定了,自动化批量提取的话是要编程一下。这里主要是安利010Editor真的好用
页: [1]
查看完整版本: 【C语言】将PSP游戏《流行り神2》里的音乐和音效提取出来