SPEED UP!与 galgame 解封包
本文主要包含了我对 galgame 语音加速探索的全过程。如果您对技术内容不感兴趣,只想使用 galgame 语音加速工具:请前往 GalgameManager 安装,添加游戏并设置语音加速插件,启动游戏即可。
本文各章节大致按照时间顺序排布。主要内容在:二试封包 和 dll wrapper v0 v1。
我打 galgame 已经有几年了,不过也只接触了几部能够语音加速的游戏:紫社全套和 GINKA(后来还玩了更多不一一列举了)。游玩这几部作品让我非常兴奋:使用二倍速播放音频,我就能节省一半的游戏时间,相当于延长了一倍的生命。经历过加速后,再次玩其他语音速度极低的 galgame(真红真红真?)让我感觉像是在浪费生命。因此我尝试寻找能够让我节省时间的游戏语音加速方式。
音频处理软件
音频处理领域很多软件都是商业非自由软件,要交钱的,毕竟音频处理本身也是天坑。而我找不到能对其他 APP 进行音频加速的软件。
写在这里的只是我试过的一部分:
- Equalizer APO:提供了易于编辑的 GUI 界面,用户可以自己创建不同的效果器和 filter。不过无法做到音频加速或音量均衡。
- Fxsound:开源效果器,可以让游戏加强脚步声等。不过延迟有点高,而且无法实现音频加速。
CE
Cheat Engine 想必大家都不陌生,我也会使用 CE 进行 RPG galgame(兰斯系列)或者动效太多(型月)gal 的加速。CE 中有一个变速精灵的功能非常好用,用鼠标点几次即可轻松修改程序速度。但是 CE 不能加速音频(因为音频播放是调用操作系统 API),这也是 CE 的最大败笔。因此我需要另寻出路。
解包与封包
20240720 我尝试了一个想法:将 galgame 音频文件解包,加速后再封包回去。由于我不会逆向,因此使用的是 GARbro,这是一个非常泛用的,多目标 galgame 资源提取器。我使用 いろとりどりのセカイ HD 尝试,这个游戏把 voice.bin 单文件放在根目录下,非常明显,我很轻易地就提取出了游戏的所有 ogg 格式的语音。
那么我要如何将加速后的 ogg 封装回一个 voice.bin 呢?我被卡在了这一步下。GARbro 根据预设的解包规则检测出格式并解了包,但是却不告诉我这个包用的究竟是什么格式。Asuka Minato 也提醒:解包容易,但封包可能非常困难。因此我暂时打消了这个想法。二试封包
pyaudio
20240724 我进行了初次尝试,写一个 python 脚本建立了一个系统音频合成器到扬声器的音频流,在中间使用脚本进行音频加速。
首先安装一个 VB Cable,其作用是修改输入输出设备,防止音频合成器直接将音频输出到扬声器,覆盖掉我处理后的输入。然后脚本大概是这样:
import time
import pyaudio
# 配置音频流参数
FORMAT = pyaudio.paInt16
CHANNELS = 2
RATE = 44100
CHUNK = 1024
SPEED_FACTOR = 1.5
p = pyaudio.PyAudio()
def list_audio_devices_and_return_vb_cable():
info = p.get_host_api_info_by_index(0)
num_devices = info.get("deviceCount")
device_map = {}
for i in range(num_devices):
device_info = p.get_device_info_by_host_api_device_index(0, i)
device_name = device_info.get("name")
device_map[device_name] = i
print(
f"Device ID: {i} - Name: {device_info.get('name')} - Input Channels: {device_info['maxInputChannels']}, Output Channels: {device_info['maxOutputChannels']}"
)
return 0, 6 # 直接返回 device id, for testing
# 获取设备索引
input_index, output_index = list_audio_devices_and_return_vb_cable()
print(f"VB-Cable Input: {input_index}, VB-Cable Output: {output_index}")
input_stream = p.open(
format=FORMAT,
channels=CHANNELS,
rate=RATE,
# rate=int(RATE * SPEED_FACTOR),
input=True,
frames_per_buffer=CHUNK,
input_device_index=input_index,
)
def callback(in_data, frame_count, time_info, status):
out_data = input_stream.read(frame_count, exception_on_overflow=False)
return (out_data, pyaudio.paContinue)
output_stream = p.open(
format=FORMAT,
channels=CHANNELS,
rate=int(RATE * SPEED_FACTOR),
# rate=RATE,
output=True,
# frames_per_buffer=int(CHUNK / SPEED_FACTOR),
frames_per_buffer=CHUNK,
output_device_index=output_index,
stream_callback=callback,
)
# 检查是否成功打开
if input_stream.is_active() and output_stream.is_active():
print("输入和输出流已成功打开。")
try:
while True:
time.sleep(0.1)
except KeyboardInterrupt:
# 用户中断程序时,关闭音频流
input_stream.stop_stream()
input_stream.close()
output_stream.stop_stream()
output_stream.close()
p.terminate()
exit(0)
音频的模型可以想象成拥有一个 trunk 缓冲区,音频先被输入到缓冲区,然后再播放出去,而这个缓冲区归我所有,我可以对其进行任意操作。
先列出所有音频设备,然后指定音频合成器作为输入流,扬声器作为输出流,定义了一个 callback 函数让输出流可以直接通过 callback 去请求输入(也可以不用 callback,将逻辑写在 while True 里)。至于音频加速则体现在 output stream 的 rate 上,因此实际上没有对音频进行任何处理,只是改变了输出速率。
实际测试中,音频输出速度和频率确实升高了,但是表现为 (输出一段加速的音频 -> 静音一段时间) 的循环。这是因为 trunk 的输入速率是恒定的,pyaudio 会 wait trunk 填满。如果我加速把 trunk 的音频打出去,那么就会有一段时间,trunk 里不存在音频,所以没有输出。
把 CE 和此脚本一起使用也还是一样的效果,因为 CE 本身不能加速音频,也就无法加速程序对 trunk 的填充速度。因此此次尝试失败,感觉尝试的方向就是错的,没法通过音频倒逼游戏速度。
TAS
20240727(也就是本文撰写时间),我突然想到,TAS 是把游戏慢放,那么反向 TAS 是否就能够加速游戏呢?并且 TAS 视频中游戏声音是正常的,显然 TAS 有自己的办法处理游戏音频速度,所以理论上一定是行得通的。
libTAS
首先我尝试了 libTAS,这是一个泛目标开源 tas 库 + 软件,许多其他 TAS 生态是建立在 libTAS 上的。libTAS 只能运行在 linux。libTAS 在 NixOS 上的打包不算太好,低了一个版本,也缺失了 libtas32.so 库导致没法运行 32 位 exe,需要手动用 LIBTAS_SO_PATH 拉;然后 libTAS 运行时会去找 libalsa,NixOS 的 dynamic link 管理又是混沌与邪恶的,因此我还需要大费周章手动用 LD_LIBRARY_PATH 把运行库拉进来。
这一切做好以后,游戏还是打不开,显示 HD 不是一个有效的 binary… (HD 是游戏路径上的一个词)我怀疑它 parse 参数出了问题,但是我没有证据,它也不输出它用的启动指令。
后来我自己打了 1.4.6 的 libTAS 包,然后自己打了一串超级长的启动指令:
LD_LIBRARY_PATH=/tmp:/nix/store/1ry2s2jgqbl3w7w54b8biylwqdxy52zw-steam-fhs/usr/lib32/:LD_LIBRARY_PATH LIBTAS_SO_PATH=/tmp/libtas.so WINEPREFIX=/home/absx/.local/share/wineprefixes/origin libTAS嗯,steam-fhs 还是好用的。现在我的游戏已经起来了,但是我发现我不会用 libTAS,无法调速。。反正 README 说的快捷键,TAB 和 V 这些都是用不了的,在 Qt GUI 窗口里设置速度也没有任何效果。
尝试在 windows WSL2 里用 libTAS,结果还要起 wine,那跟我在 linux 没有任何区别。不测了。
hourglass
转战 windows,hourglass 也是一个泛用 TAS 工具,仅支持 win32,不过它已经非常老了。我只是想试试看,没想到 hourglass 真的可以让我的游戏跑起来,并且能够改变游戏音频速度!但是 hourglass 有一些致命的缺陷:
- 不支持鼠标
- 减速有多档位可调;但是加速只有一个 fastforward 用于快进的,大概有 10 倍速并且不可调。这个速度还不如不听语音(骂人)。
基于这些原因,hourglass 也无法使用。但它的存在是有意义的:它至少给了我一个成功的例子,告诉我前方并非是一片黑暗,我做的事情并不是毫无希望的。
其他
尝试其他 TAS,但几乎找不到其他泛用的 TAS 了。TAS 世界的 tools 大多还是针对某个游戏或某个模拟器,这种任意进程加速的工具真是少之又少。
- BizHawk:泛用 TAS 工具,但仅支持镜像加载,主要针对游戏机,不符合我的需求。
- 不过也有一些 galgame 是使用 CD 的,我只能说保留尝试的可能性。
- UniTAS:专门针对 unity 游戏,有的 galgame 使用 unity 引擎的可能可以尝试。不过真心不多。
- Hourglass-Resurrection:一个 hourglass fork,但是也已经停更了。没有提供二进制,我尝试自己构建也失败了(VS 除了点构建运行就啥也不会干了)。后面有更详细的尝试过程。
libspeedhack
20240729:虽然很麻烦,但是我还是为了仅存的一点希望去尝试一下 libspeedhack。作者已经消失两年了。我按照说明直接用,不出意外地出了意外: libspeedhack32.so: undefined symbol: floor. 然后去瞄了下 issue,果然有一个一样的问题,并且别人也修了,开了 pr 在构建时加了个 -lm 参数。然后我很开心,拉下来想构建,结果:
我们的 NixOS 实在是太棒啦!环境根本搞不起来,它的构建指令需要静态链接 + cross 32 位,什么 libclang,glibc_multi 的包直接加进去根本满足不了需求,再加上我根本不知道在 buildInputs 里能写哪些玩意,比如 glibc 就能写 glibc.static,glibc_multi 能写 glibc_multi.dev,这种有个点后面跟着啥玩意的我除了读 nix 源码也想不到要去哪查。
然后去我的 debian 云服务器上尝试构建,东西都装好了,然后没有数学库:ld: cannot find -lm: No such file or directory。我又把 libm.so 拿出来装到 LD_LIBRARY_PATH 里,继续构建,又会得到:
/libc.a(malloc.o): relocation R_X86_64_TPOFF32 against 'tcache' can not be used when making a shared object; recompile with -fPIC
ld: failed to set dynamic section sizes: bad value
collect2: 错误:ld 返回 1一度让我想放弃。问了下 gpt-4o,让我不要把 libm.so 在构建时链进去,应该在运行时加载。于是我最后的尝试成功,修改它给的启动脚本:
#!/usr/bin/env bash
rm -f /tmp/speedhack_{pipe,log}
mkfifo /tmp/speedhack_pipe
_libdir=/home/absx/lib # (我把东西存这里)
LD_LIBRARY_PATH="${_libdir}/lib:${_libdir}/lib64:${_libdir}/lib32${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" \
LD_PRELOAD="libspeedhack.so:${_libdir}/libm.so:${_libdir}/libm32.so:${LD_PRELOAD:+:$LD_PRELOAD}" \
exec "$@"这样把 libm.so 作为 LD_PRELOAD 运行时加载进 ./start.sh wine xxx.exe 即可。
游戏打开了,但是……我加速呢?我 echo 2.0 > /tmp/speedhack_pipe 都要按冒烟了,所以我的加速呢??不只声音,就连游戏本身也没有加速,就跟 libspeedhack 不存在一样地和谐。于是此次尝试又宣告失败,骂这个跑路项目也解决不了什么,还是不骂了。
ps. 事实上,通过源码也能知道,Github 上的这些 libspeedhack、Letomaniy/Speed-Hack、Hirtol/speedhack-rs 等 speedhack 并不涉及音频 api 的修改,因此不可能有音频加速效果。妄图通过简单的一点点源码实现加速只能说是天方夜谭。
Speedy
一个简单点点点就能加速窗口的国人开源软件,比较电脑小白向。最高支持数千倍加速,体验还不错,但是跟 CE 一样,也不支持音频加速。
源码构建
20240803:到了这一步,市面上的各种现有软件看起来已经是山穷水尽了,我开始把目光转向修改代码。
一个选择是读 wine 的音频 api 然后写点 lib 注入。另一个选择看起来成功率更大一些,那就是直接修改 hourglass 的代码,毕竟人家的功能可行性已经通过了验证。
hourglass 是 C++ 写成,调的都是 windows api,项目管理用 vs sln。我之前从未用 vs 写过 C/C++ 代码,而且 C++ 构建本来就是一坨,还是去构建已经不再维护的代码,因此预感此次修改也不会顺利。
Hourglass-Resurrection
这是一份 7 年前的代码,Hourglass 的重生版。不抱希望搜了下 fork,没啥大修改,因此开始 clone。
想修改代码第一步就是跑过编译,因此我开始折腾编译。扔到 vs 2022,一编译,一大堆报错。这些报错还都挺抽象的,例如有个叫 IDirectSoundSinkFactory 的玩意找不到声明,但是我把它宏解了能看到声明。Google 也搜不到这玩意。
我看 Hourglass-Resurrection 有一个 7 年前跑的 CI 是可以过构建的,用的是 msbuild。于是我下载 .Net sdk,发现:
- msbuild 本身不会在安装时被加入环境变量,要用绝对路径调用。
- windows sdk 版本不匹配,于是我去下载了 10.0.22621,重新 build。
- 报
error MSB6001: “CL.exe”的命令行开关无效。猜测应该是本机的 clang 太新导致的。
我也想过装老版本 vs,下载后发现不同版本 vs 不能共存。于是打消念头。
不想再折腾本机,跑去写 Github CI。结果找的 install windows sdk CI 没一个能用的,我指定的 22621 version 要么找不到,要么就是脚本内部错误。然后我也不指定版本了,改用 choco install visualstudio2022buildtools windows-sdk-10.0,结果花了好几分钟安装后告诉我还是找不到 msbuild。。太经典了。
又继续啃了一会,给一堆 struct 和 macro 移形换位,编译不报错了,取而代之是链接爆了一大堆 error:LNK2001 无法解析的外部符号。。这下我是真没辙了。
hourglass-win32
这个是原版 hourglass 代码,年代更加久远了,距今有足足 14 年的历史。
用 vs 2022 打开,跑一次构建,没想到居然能够把 GUI 跑起来,已经很厉害了。不过逻辑是没有的,构建时出了挺多错误。
这些 error 看起来比 Hourglass-Resurrection 好读多了,我猜测是 C++ 编译器标准太高导致的编译低版本不兼容的问题。尝试降低 C++ 编译器版本,vs 告诉我:我们接受指定的最低版本是 C++14 😅。
自己写
干到现在我已经不想再折腾构建了,不如干脆读代码,然后开抄,写一个自己的注入 lib。我的想法很简单,只加速音频播放,不加速游戏本身(如果想要两个都加速,那就再开一个 CE)。
从读代码角度来说,Hourglass-Resurrection 还是比原版要好很多的,虽然 C++ 项目一直都很难读。最重要的显然是 source\hooks\hooks\soundhooks.cpp,它是整个音频加速的核心;并且可以看到文件在最后定义了注入/拦截(Intercept),将以下函数全部干掉了:
WinSound
- PlaySound 系列:PlaySoundA, PlaySoundW
- 这是 windows 早期的简单音频播放 api,只需要提供文件名即可。W 和 A 指文件名的编码。由于我们不能直接获取到音频 buffer,因此只能将音频加速后保存到 temp 再调用函数。
- 但是 Hourglass-Resurrection 把这两个 api suppress 了,那我就干脆不管了,遇到问题再说。
- waveOut 系列,包含 waveOutWrite, waveOutGetPosition, waveOutReset, waveOutOpen。这几个貌似就没法操作 buffer 了,毕竟这个 api 就是操作 tick 用的,可能只能用 tick 魔法了。不过理论上应该也不难。
- 保留用 waveOutSetPlaybackRate 强改的可能。
- Beep 系列,MessageBeep 和 Beep。这也是 suppress 的,目的应该是 TAS 那边的,不用管。
DirectSound
- DirectSoundCreate(8), DirectSoundEnumerate(A/W), DirectSoundCaptureEnumerate(A/W),这几个是重点,毕竟游戏都调用的 DirectSound,因为可以叠加音频播放。
- 这些函数可就麻烦了,Hourglass-Resurrection 自己写了一个 DirectSound 的音频驱动程序,包括前面的 EmulatedDirectSoundBuffer 也是。我这才知道文件开头的一堆 magic number 是干什么用的。显然我没有自己写驱动水平。
- 所以如果不写驱动,那么需要 hook 的函数就不能是这些,这些函数是用于创建驱动用的。需要 hook 的应该是
IDirectSoundBuffer::Lock/Unlock/SetFrequency等。
- 所以如果不写驱动,那么需要 hook 的函数就不能是这些,这些函数是用于创建驱动用的。需要 hook 的应该是
- 往细想下去就更不得了了:学它直接操作 tick 可能并不能实现不变调加速。或许我听到的 Hourglass 的音频加速只是破碎的 buffer 拼成的音频;10 倍速下确实听不出音频究竟还是否完整。
- 这些函数可就麻烦了,Hourglass-Resurrection 自己写了一个 DirectSound 的音频驱动程序,包括前面的 EmulatedDirectSoundBuffer 也是。我这才知道文件开头的一堆 magic number 是干什么用的。显然我没有自己写驱动水平。
而起初我并没有发现这些问题,我先写了点操作 buffer 的代码……
rust
我不想碰 C++(即使已有代码作参考),先用 rust 碰碰壁再说。
试了一下 detour-rs,一个 detour 的 rs 实现,但是拉下来发现编译不过。issue 看到了一个 fork 解决了这个问题,才发觉 detour-rs 已经断更三年了。所以使用此 fork 版本的 retour-rs。fork 版的文档也更详细,我用它的 example 配合文档推荐的 dll-syringe 跑了一下,成功注入了 MessageBoxW,完成了新手教程。
但接下来才是痛苦开始。rust 音频处理生态本来就挺烂的。我先尝试的肯定是 rubato,毕竟简介就是速率变换。结果这玩意并不咋样,几乎无文档,examples 里面几百行也没有解释。我把 wav 用 python 转成 f64 raw,跑一遍 example 再转回去,并不是一个可用的音频。
dll 调库
退一万步,如果用现有的 dll 库呢?我尝试了一下行业用得比较多的 soundtouch,写了个 python 小脚本做测试。soundtouch 的 api 设计就要好得多,用户只需要 put 和 receive 就行了。但是我用 receiveSamples (处理 float 数组) 系列测试就返回值为 0(应该要返回数组长度),数组没有被改动;用 putSamples_i16 系函数(i16 系是 float 系的包装,包了一层转换)甚至有 bug,直接 internal OSError: exception: integer divide by zero。非常郁闷。
打击
此时我终于读懂了 Hourglass-Resurrection 的代码是自己写 DirectSound 的驱动,并且也发现操作 tick 并不能实现真正的音频加速,最终的道路一定是 buffer。于是我陷入了消沉:做一个音频加速的复杂度已经远超出了我的想象,即使知晓了音频加速原理,也很难面对一大坨 windows sound api。我目前摸索的这些知识在脑子里并不能合到一起。
二试封包
转眼过了半年来到 2025,我在推 空に刻んだパラレログラム 时看到 xp3 格式的文件,又忍不住想起了拆包封包的思路。由于 xp3 是老牌(20+years)的广泛使用的格式,网上的资料非常多,可谓是帮了大忙。我先尝试用 GARbro 拆包,接着用网上找的脚本封回去。封完的包比原先的大了十多 MB,心里还是有一些不安的。打开游戏,发现居然可行!!游戏可以读到音频文件!
于是我激动得立刻开始 LLM 写脚本,用 ffmpeg 加速音频。写完运行,发现脚本解包有问题。我怀疑是这个 krkr-xp3 只支持 xp3 v1,而 galgame 用了 v2 打包。不过无伤大雅,我只需要封包能力即可,所以我利用 GARbro 解包后,改一下脚本,将包封回 xp3 即可。实测可以正常游玩。(详细方法,请点击下方表格中 xp3 展开查看)
上述操作让我可以倍速语音游玩所有具有 krkr 移植的 galgame,简直不要太爽。
尝到甜头后,我又开始审视起我仓库中的其他 galgame。下面的表格记录了我的游戏语音加速尝试与心得(时间顺序),可以点击对应条目查看详细内容。
因为我的每个 galgame 都会折腾一番音频加速,因此也写了不少工具。这些工具可以在我的 Github profile 下方展开 Galgame tools 查看,下面的表格展开中有的代码里用到的指令就是这些工具。
krkr_xp3 | ||||
注意,xp3 引擎的游戏需要看一眼 | ||||
krkr_xp3_enc | ||||
总结:
加密的 xp3 就没法直接用 xp3-pack-unpack 解封包了。不过 GARbro 可以解封加密的 xp3,只要游戏有被收录到密钥列表里就行,这里列举的游戏都属于此类。 GARbro 打 xp3 有不同版本(1,2,Z),如果没声的话建议多试试。还有路径压缩就没必要开了,
至于上述那些不行的……那当然是因为 GARbro 不更新了,并且其他的大部分 fork/mod 都没有添加这些加密方式,因此没法解。虽然用 KrkrExtract 也可以,但是工作量太大,而且会被剧透,因此不干。
| ||||
Artemis | ||||
解封包工具:sakarie9/pfs_rs,这也是一个使用 rust 写的的解包封包工具,我对 rust 工具天生具有好感,项目的质量也不错。顺带一提作者还写了篇文章讲述 pfs 封包的技术细节。 解包樱之刻,我一搜 Artemis Engine,出来一篇文章告诉我其实这个引擎不需要封包……实测了一下,果然如此。只需要用 GARbro/其他解封包工具解出来,批量加速后放到 exe 所在文件夹即可。 不过不封包会导致 luna translator 的 hook 失效,估计是不封包就直接读 .ast 文本,不会再经过 hooked 函数了。因此我们可以把 script 重新封回 pfs archive,这样就可以让 luna translator 读到文本。封包时要注意,pfs_rs 会把 input 的所有子文件/文件夹封到 pfs archive 的根下,因此不能直接 ps. 也可以 GARbro 直解,反正不用封包。 其他心得:
| ||||
favorite | ||||
FAVORITE 的 .bin 格式比较简单,也有工具 Leticiel/fvp-tools (Pytho) 和 Nikaidou-Shinku/fvp-unpacker (Rust) (这个比较全能,可以解不同资源类型。看在它用了 rayon 的份上,就承认它的 blazing fast 吧)。 然而有些工具解出来的 filename 有点问题:本来应该是类似 我先 fork 了一个 fvp-tools,保证解封包是对偶的(八位数文件名)。过了一阵子,让 LLM 写了个 Rust 版本 fvp-rs,允许自动添加/删除后缀。这里推荐使用 fvp-rs。 总之,SPEED UP 的初衷就是干翻 FAVORITE,我的目的已经达成了。 | ||||
WOH | ||||
魔法使之夜的解包遇到了一点麻烦。首先一大堆 然后在网上找了一个 mahoyo_tools,但是这玩意只说能解图像和脚本,不包含音频。而且 star 数和 README 讲的都很糊,根本不会用。(不过代码质量倒是挺高的) | ||||
ypf | ||||
首次尝试:失败 尝试解包/封包一个 YU-RIS 引擎的游戏,这玩意也是个老牌引擎了。 GARbro 打开 .ypf 时会提示猜测密钥,能够顺利解包。但是不得不骂,GARbro 并不会告诉我猜到的密钥是啥,并且封包的时候要求必须提供 8 位密钥。 尝试其他工具:
受不了了,直接(用 LLM)读 GARbro 源码,找到它猜测密钥的方法,然后让 LLM 写一个同功能脚本: py
得到密钥为 继续找打包工具,找到 crskycode/YPF_Tool,免密钥打包,放到游戏,能正常运行。人家的也是 C# 写的怎么爆杀你 GARbro 啊。 结果发现 过程中的其他发现:
后来我又装了个日版 windows,尝试了一下最后的希望,也就是用 YSPac 封包。结果 http://yu-ris.net/ 的都测过一遍,要么 gal 崩溃要么无声。打开 ypf 版本一看,YU-RIS β4.8 2025/04/30 打出来的 ypf 版本还是 492,还没有本游戏的 500 高。而这游戏已经是三年前的了。属实是活全家引擎。 所以,要么再等几年等 YU-RIS 发新包,要么就直接 quit 下桌不玩。 update1 下载猫忍之心,也是 ypf 引擎,打印出版本也是 500,心已经凉了一半。不死心,再次尝试封 ypf,找到一篇 flamecho 的文章介绍了两个 ypf 工具,其中一个是 YPF_Manager_Tool,源码貌似是 Xorboth/YPF-Manager 的。抱着一丝微弱的希望尝试了一下,没想到啊没想到,它居然真的能封出可用的包!!!千古(6 个月)难题迎刃而解,还是有点激动的。 sh
另一个程序好像是专门用于文本提取翻译的,这里不再尝试了。 然后我 fork 了 YPF-Manager 的源码,并且写了一个 github CI 来 build binary。 | ||||
escude | ||||
escude 家的游戏是 bin 格式,GARbro 可解不可封。 工具尝试:
| ||||
unity | ||||
尝试解包 旭光のマリアージュ。用 bandizip 打开 .dat 文件,显示是一个 zip 压缩包。文件名没有加密,可以看到语音(sound)在 加速完(注意避开 se 和 bgm),准备封包,结果 bandizip 打出的 zip 用不了,开游戏有出场动画,但是进不去标题,黑屏。无论开不开压缩级别都是这样。 查看源 dat,随便开了个格式识别,显示 Zip archive data, at least v2.0 to extract, compression method=deflate。然后我又用了 7z 命令行,指定了 py
然后就可以用了。 ps. 后来发现用 7-zip 的仅存储打出来也是可以用的,都怪 bandizip!😡 | ||||
bgi | ||||
首先,用 GARbro 是可以解 arc 的。不过需要勾选将音频转为常见格式。我想找一个命令行自动化解包 arc 的工具:
对 千の刃涛、桃花染の皇姫 来说,只需要解包 至于封包,AllanZyne/bgi_tools 里有一个非常短小精悍的封包脚本,并且不依赖于任何 dll/C/C++ 代码,激起了我的尝试欲望。嗯,虽然打出来的包和原先的有一些差别,不过至少能给那些 ogg 自动加上 headers,可以说是看起来挺先进的。结果不出意外地失败了:播放第一句语音时,指定されたファイル [ data04xxx.arc : sen010100010 ] の登録中に致命的なエラーが発生しました。 然后我尝试用 GARbro 自带的封包,采用 ARC/WillV2 格式。当然,此时的 一共 40 bytes 的 headers,在同一个 .arc 中对比了几个文件后,发现只有 00000000 [08, 0F] 的 bytes 是有变化的。跟原 .ogg 做对比,很容易看出 [08, 09] LE 刚好是 继续了解,发现 ogg 的 Vorbis 编码并不是定长帧,而是一个个的不定长 packet 组成的。当我打印出一个 ogg 的所有 packet 的 length 之和以后,神奇的事情发生了,这个 sum 的值刚好就等于 [0C, 0F] LE 的值!于是 ogg 封包就这样破解了。 ![]() 然后后来又花了亿点点时间优化 arc-reader-rs 的代码,添加封包功能,添加测试。最后再用脚本一跑,轻松秒杀。 抬头一看,怎么一天的时间已经过去了。。。 多 arc 加速代码示例: py
也可以使用 fd 来快速操作: sh
PS. 由于 const for audio pack 部分还有一些内嵌的数字,又让 AI 分析了下, 2026.07 我又给 arc-reader-rs 添加了图像封包能力,不过这都是后话了。 | ||||
silky | ||||
きまぐれテンプテーション
忍不了了,由于 SilkyArcTool 的解包是好的,我重新用 Gemini 2.5 Pro Preview 03-25 写了解包代码 lxl66566/SilkyArcTool-rs,并让它根据解包代码反推封包代码,不要按原样写。结果真的能用!!于是结束了。 不过注意,pack 语音时不能用 compress flag,否则在对应语音会 Butterfly Seeker 解 arc 也是非常顺利,加速均衡一气呵成,结果封包回去进入游戏发现并不能播放语音。 排查程序,对比同一个 进入游戏发现还是没法播放语音。然后注意到我的音量均衡 v0.1.2 会把大写 所以 Silky’s engine 对 header 还是蛮宽容的,即使值偏差了 4 也可以正常进游戏,导致玩了好久都没发现这个 bug,吐了。 ふゆから、くるる。 已经研究出 dll wrapper v1 了,本来已经没有必要解封包;但是这一作的语音音量非常不均衡,只好再拆开进行一次 loudness normalize。好在我写的工具没有再出问题,一遍过。 封完包,惊讶地发现 voice.arc 的大小从 2.13 GB 减小到 807 MB,但是我并没有改变任何速度。喂!你的 ogg 到底是怎么编码的啊! | ||||
softpal | ||||
网上有一篇文章讲了 softpal 可知 softpal 引擎优先读取文件夹,所以实际上无需封包。softpal 是目前为止 speed up 最简单的格式,只需要用 GARbro 将 | ||||
npa | ||||
然后老的 | ||||
AKABEiSOFT3 | ||||
没有汉化只有机翻补丁,GARbro 打不开,网上搜不到任何信息。无解,除非去做整套 crack 流程。 然后尝试了下 GARbro Mod,能打开并且能正常提取音频。按照惯例,procmon 看下能不能免封包,很遗憾,不能。接下来只要解决封包即可。 AKABEiSOFT3 官网没有给出任何工具下载;GARbro Mod 里有创建 Github 搜关键词,有效的只搜到一个 akabeisoft3Danshi 汉化.md,里面说到 手垢塗れの堕天使 使用的引擎是 krkrz。。emmm,确实,虽然同为 AKABEiSOFT3 但是它们的引擎确实是不一样的,参考 vndb,让我白高兴一场。 既然 AKABEiSOFT3 引擎各种各样,那就不能用它做关键词。使用 yaneurao 搜索,定位到其当前官网,这是一个开源引擎,并且提供了工具下载。尝试下载了 yanePack101 和 yanePackEx102 用来打包,首先它的输入只能是文件而不是文件夹,其次打出来的包跟游戏里的 voice.dat 也不太像啊,一堆 80 都没有出现。 | ||||
LCSE | ||||
工具:cqjjjzr/LCSELocalizationTools sh
能用,不过吐槽一下,这个 patch 而不是 pack 的形式非常慢,1w5 的音频跑了几十分钟( | ||||
CatSystem2 | ||||
白昼夢の青写真 我尝试使用原版 问题出在 GARbro 没法封包 int 类型存档:尚不支持创建加密压缩文件。 因此最好还是另寻出路。 这玩意属于是把饭喂到嘴边,专为翻译人准备的,python 脚本解包也只是把文本提取出来,中间产物全部删掉,或者不解(pcm_xxx.int 都跳过了)。但是我不是为了翻译,因此还是需要读源码。 解包很简单,它使用 但是这玩意的封包用了一个 看起来像是一个资源统合站,并且有一个比较详细的 wiki,介绍了 MakeInt.exe 工具和 KIF Archive(也就是 .int)封包的格式。KIF Archive 的介绍等于是个兜底,即使最后没找到现成工具也大致可以照猫画虎搞一个出来。 在给出的 CatSystem2 首页下载了开发包后,进入 cs2_core_v401/マスター作成,执行 总之最重要的是先试一试,搞个脚本把所有音频解包后重新封进去: py
然后祈祷 CatSystem2 引擎可以识别不加密的 sh
怀着忐忑的心打开游戏,成功!CatSystem2 引擎可以识别不加密的 这个游戏还有一堆 update0x.int 也需要解包。
ISLAND 同样一套流程,但是 ISLAND 的 吗? CatSystem2 启动会检测各种 | ||||
QLIE | ||||
总结:
FilePackVer3.0 包含的游戏:
解美少女万华镜 1。尝试:
不过在尝试过程中,用 Process Monitor 监视发现进程会去读游戏目录下的一些文件夹的 ogg,于是猜想该游戏可以免打包读取音频。尝试将 ogg 扔到 Voice 下发现可以读取并播放,验证了免封包的猜想。因此只剩下了最后一个难关:将 ogg 解密,即可实现加速。 搜索引擎上经常被 超详细!解包某知名 Galgame(万华镜 5)引擎——Galgame 汉化中的逆向#Qlie 引擎 这篇文章刷屏,是上面 Github 的 Aobanana-chan 写的,看着是挺详细的,实际上对我解包没有贡献什么信息。我又去读了他的源码,一堆 mmx 指令集也不具备可读性。正如 Dir-A 对此文的评价,不对胃口。 imhex 打开拉到末尾发现格式是 为了排除 LLM 移除 mmx 指令集把代码逻辑搞坏的影响,我又去网上找编译好的二进制。在 asmodean-tools archive 里找到了预编译的 美少女万华镜 3 美少女万华镜 3 也是 FilePackVer3.0,然而 网上一搜,发现也有人遇到相同的问题,而且下面的音频补档链接已经没了,fuck you mediafire。 继续喵源码,发现 FuckGalEngine 给出的源码完全是错误的,比如开了 继续网上冲浪,看到 乜都讲 D 的博客 提到了关键信息 FilePackVer3.1 这个就简单了,GARbro 可以直接解出音频来,上述的 hz86/filepack 估计也可以用吧,懒的试了。 美少女万华镜 5 不知道为什么,5 不能免封包读取音频,会弹报错。 但是 FilePackVer3.1 工具多,直接封回去就好了。 美少女万華鏡異聞 雪おんな 这一作封包也是 FilePackVer3.1,但是 GARbro 和 hz86/filepack 是解不了的,估计又是哪里做了小改动。挺莫名其妙的,为啥网上某些博客搜出来都说 GARbro 可用。。 尝试下 Dir-A 贡献过的 RxQLIE,Release 里的 dll 太早了而且试了下不可用(随便输入一个 Sequence 会直接崩溃)。想自己编译。虽说看到 xmake.lua 感觉非常亲切,以为这次编译不用花太多功夫了,结果直接 xmake 还是报错,找不到已存在的某个 .h。即使添加了 但是编出来的产物里并没有任何 version.dll,我也不知道他 release 里的 dll 是哪来的。 | ||||
LiLiM | ||||
LiLiM DARKNESS 社的引擎,资料有点少。 GARbro 可解,procmon 扫一遍不能免封,于是需要找封包工具。
看了下,这个 aos 的封包格式是真的简单。。实际上只要随便用 imhex 打开看看就能发现一大堆的裸 ogg 文件名和 当然事情并不会如此顺利,在 exaosv2.cpp 里对 5 - 8 字节是直接当废弃字节跳过的。结果封包回去并不能正常运行游戏,在语音处会弹窗报错。所以我猜这四个字节也是有用的。跟上次 BGI 的封包一样寻找蛛丝马迹,盲猜和文件数量有关(懒得去拉所有 ogg 的 frame 总和,先猜个数量再说),还真猜对了,这四个字节就是 文件头大小 + 目录表总大小 = 273 + 文件数 * 40。于是瞬秒,打开游戏测试,语音加速正常。 | ||||
AVG32 | ||||
AIR 的音频没有封包,是 wav 格式,mpv 可以正常播放,见到的第一眼我就觉得我要秒杀了。然后就被狠狠打脸,这个 .wav 容器根本不是 PCM 编码,imhex 看二进制更像是 mp3;但是它又不是一个正常的 mp3,至少 ffmpeg 重编码会报错: 我的 loudness-normalize 用的 symphonia 更是会直接 panic。 但是 ffmpeg 重编码是可以编出一个正常的结果的。于是尝试 batch 重编码,还踩了一个 python multiprocessing 的坑。最后在 cmd 里用 fd 跑了一遍: 跑出来就不是 mp3 类似物而是真正的 wav 了,共计 4.88GB 是真没绷住。先不管啥编码,捞到游戏里跑一遍再说,还真能播放。于是加速结束。 后来从 inmm 处了解到,这些 wav 应该是 KOE 格式。文章还说可以用 koeunpac.exe 转为 wav 格式,但是我尝试了发现并没有什么卵用,会报错 レジストリ Software\KEY\AIR を開けません。 感觉又要用日文系统才能进行格式转换了;而且我这也不是光盘版的,这个路径看起来得在符合 CD 的文件夹路径里才能进行转换吧。 感慨一下,在那个音频编码没有统一标准,mp3 要收费的时代,搞点游戏音频确实不太容易。虽然 KOE 这玩意吧看起来就跟 mp3 差不多,有可能就是为了避免专利费而进行的小改造…… 其他问题
| ||||
TyranoScript | ||||
没想到真的有用 electron 做的 galgame,让我感动了一秒钟。 直接解 resources/app.asar: 多看了一眼,传述之魔女用的其实是 TyranoScript,这个引擎是基于 electron 的。 虽然用的 electron,但是程序写得很烂。 | ||||
unity2 | ||||
py
然而并没有什么卵用。再看看源码,clip.samples 只是一个 继续:
折腾了好久都没搞出来,后面用 dll 注入实现加速了。 | ||||
lucasystem | ||||
GARbro 直解,看二进制能看到 看了下 LuckSystem,感觉文档有点简略。而且只能替换不能解封包的设计感觉很屎啊。 看源码不复杂,直接让 gemini 帮我 vibe 一个 RIIR 版本抽奖。vibe 出来的加速完,满怀期待地打开游戏,结果语音全部乱序,之后数轮修改还会让问题扩大化,尝试了许久遂放弃。 老实用 LuckSystem 吧: sh
打开游戏,确实可以实现加速,已老实。 但是不得不吐槽,这种 replace 写得就是很屎啊,我加速后音频大小只有 1/3,但是 replace 后还是一样大小,合着根本不改数据块 offset 啊。而且实际游玩时偶尔也有几句语音没法播放,这玩意写得还是有点问题。我尝试用大序号(voice2.pak)覆盖掉原有语音,想着对这些加速失败的语音能不能走到 fallback,结果也是不行。 | ||||
renpy | ||||
开源的引擎,解封包应该不会有什么问题。 浏览一下,发现 voice 根本就没有打包,直接放在文件夹里了。感动。 | ||||
SiglusEngine | ||||
SiglusEngine 公认的工具是 xmoezzz/SiglusExtract,虽然官宣停止开发了但是还能用。 然后发现找不到音频文件,用 dust 一看,大的全是视频,其他零零散散的根本找不到啥是语音 pack。那我问你,你的音频文件在哪。。看到 wav 文件夹里全是 SE,有 700 多个。不知道音频有没有混在里面,我想着总不可能是 700 多个音效吧,解出来一看还真是,草。每个文件夹挨个看过去,然后发现音频在 这个 SiglusExtract 解出来的 ogg 格式是非标的,至少有 我承认这个作者很强,甚至开发了 siglus_rs 这种 SiglusEngine 模拟器。但是这个 SiglusExtract 并不好用也是真的。 GARbro 可解不可封,因此还是得想别的办法。imhex 打开一看,里面 OggS 是直接打进去未变换的,那么最后就剩下了解 header 的含义了。 然后懒得搞了,《虹彩都市》这游戏本来也是小短篇,没有太多折腾的必要,AudioSpeedHack 一把梭就完了。 后续 SiglusEngine 的游戏实在是有点多,而且好多游戏音量均衡非常垃圾,还是得花点时间研究下。 看到 SiglusSceneScriptUtility-GUI,2026.07 创建的仓库,一眼 vibe coding,还有几个恶心的 bat 脚本。想着还是尝试一下,至少给点起码的尊重。拉下来 实际上这个 GUI 内部包的还是 SiglusSceneScriptUtility,这个玩意看着就正常多了。想要实现解封包,直接用 SiglusSceneScriptUtility CLI 也不是不行。 | ||||
箱庭ロジック | ||||
这游戏还挺小众的,连引擎叫啥都没查到。 随便摸一摸,发现音频大概是在 res/s/v 里。所有文件都是以 | ||||
EntisGLS | ||||
EntisGLS,比较早的引擎了,这个引擎的优点是源码公开,因此最终一定是有办法解包的,只是时间成本问题。 至于封包……Dir-A 的这篇文章说 EntisGLS 是「支持少量修改或直接支持免封包处理功能的引擎」,然而我自己(去除 noa 是封包格式,封包内部的音频是 mio 格式(私有编码格式)。那么现在对 EntisGLS 加速就需要完成 4 步:解 noa,解 mio,封 mio,封 noa。
探索过程中的其他发现:
| ||||
STEINSGATE | ||||
2026 年末法时代了,已经不需要自己研究封包,丢给 agent 完事。 STEINS;GATE RE:BOOT 引擎的解封包有两层工作,一层是封包格式本身,这个使用 FreeMote Toolkit 没啥问题;另一层比较麻烦点,解包出来的音频格式是 .xwma,XBOX 游戏的音频格式,不能直接加速。ffmpeg 可以解码,但是不能编码,并且微软也不给开放下载 xWMAEncode.exe 编码器,非常傻逼。不过好在后来都解决了。我估摸着用 Yakitori Audio Converter 应该也是能编码的,不过还是不如 agent 搓脚本来得快。 总之最后搓出的这个脚本就可以用于音频加速和解包封包了。一些坑点也都直接写在脚本注释了,比如 24bit 到 16bit 的转换等。 |
二试封包总结
二试封包的成功确实给我带来了许多喜悦之情与实用价值。但是仍有一批引擎无法简单地通过这种方式进行加速。在面对它们使尽浑身解数仍无法战胜时,我内心里总有一股深深的无力感。因此我认为 拆包 - 加速 - 封包 只是旁门左道,只有注入才是正道。
不过由于能力不足,在接近一年的时间内,我的研究重心全部放在了解封包上。毕竟这对目前的我来说是唯一能实现语音加速的方法。
GARbro 相关
解包过程中大部分都是用 GARbro,真的很无脑,一键直解;但是 GARbro 主仓库(morkt)已经不更新了,对于新出的游戏和加密算法,可能也需要寻找其他的 GARbro fork 实现才能解包。这里有一些备选项:
- crskycode/GARbro:最流行的一个 GARbro fork,但是 2025 年作者删库删帐号了。(2026.04 又打赢了复活赛)
- nanami5270/GARbro-Mod:一个活跃的 GARbro fork,但是目前没有提供 Release。
音频 API hook
公司某些傻逼软件促使我去 hook 它们,这次 hook 经验便成为了此次尝试的契机。
重新回到 pyaudio 章节,当前面临的最主要问题就是没法控制游戏本身往音频缓冲区里写入的速度。那我就想了,如果我使用 hook 注入音频 API,人为调整音频 API 的播放速率(可以通过调整播放 sample rate 达到目的),再用软件处理音高,这不就能实现音频加速了?
于是立刻开整。由于我 hook 公司软件,使用 rust retour 测试有点问题(字符串的问题),于是我这次使用了 C++。首先 vibe 了一个用 detours 的音频 API 注入 dll,然后再搞一个 injector。过程中发现 xmake 引三方包真的太好用了,之前编译 detours 那些指令可以全部扬掉,直接在 xmake 里 require 就行。于是又用 ftxui 给 injector vibe 了一个 TUI 界面方便选进程。repo: AudioSpeedHack(inject)。
但是回到家测试,实测并不能成功注入音频 API,我也不清楚为啥。我后来又自己搞了一个 debugger,注入音频 API 专门打日志用的,也没有任何输出,不管是游戏还是手写的调 dsound 播放音频的小脚本,都没有打出信息,也就是没有成功 hook 到。感觉还是不熟注入和过于依赖 vibe coding 的锅,但是目前的我也没有这方面的能力。
关于其他 hook lib:
- PolyHook2:这货基本没有文档,蒜鸟蒜鸟。
dll wrapper V0
沿用之前思路。如果不好 hook 音频,那么就直接改源码,编出一个 dsound wrapper 用会怎么样呢?
这些音频 API 都是闭源的,但是有一些开源第三方实现可以使用。
dsound.dll
当前有这些常见 dsound wrapper:
- dsoal
- IndirectSound
- Creative ALchemy
它们主要是用来强制开启 windows 环绕声和其他音效的。其中只有 dsoal 是开源的,因此只能选择改它。
fork 了一个 dsoal,改了两个 frequency 值,然后把 dsound.dll 编译出来。项目质量也确实不错,一次编译过,少有的不需要折腾构建的 C++ 项目。
可能还需要用 DSWRP(改注册表的脚本)让游戏可以从同目录加载 dsound.dll。不设可能也行,可以先尝试一下。
找了一些明确加载 dsound.dll 的游戏,按照 dsoal README 尝试,并没有什么卵用:游戏优先找 C:\Windows\SysWOW64\dsound.dll,强制其优先加载本地(同目录下)dsound.dll 需要把系统的 dll 改名。加载本地 dll 后游戏无法播放音频,也没有观察到 DSOAL_LOGFILE 的创建。
怀疑是编出来的 dll 有问题,用 dsoul 的 CI 编译了一下,看到出来 Win32 和 Win64 的两个产物,怀疑是游戏调的不是 64 位的 dll 的问题。把 Win32 产物放到游戏文件夹下,打开游戏,发现音频可以播放,速度和音高变为之前的 2 倍!符合预期。
然后就是编写程序将音高降回去。我先在前文 pyaudio 的基础上尝试,使用 librosa 降低音高:
RATE = 44100
def callback(in_data, frame_count, time_info, status):
audio_data = input_stream.read(frame_count, exception_on_overflow=False)
audio_array = np.frombuffer(audio_data, dtype=np.int16)
# i16 转 f32
audio_float = audio_array.astype(np.float32) / 32768.0
# 使用 librosa 降低一个八度(-12 个半音)
shifted_audio_float = librosa.effects.pitch_shift(
y=audio_float,
sr=RATE,
n_steps=-12, # 负数表示降低音调,-12 个半音即一个八度
)
# f32 转回 int16
shifted_audio_int16 = (shifted_audio_float * 32767).astype(np.int16)
# 转换回字节流
out_data = shifted_audio_int16.tobytes()
return (out_data, pyaudio.paContinue)
音高确实降回去了,加速是完全可行的!!但是在 chunk 之间还是有许多割裂的部分(无声或噪音部分),对于 galgame 游玩体验的影响很大。我也尝试过缓冲区,但是仍然会出现割裂,只能归结于 librosa 降调后的数据点有问题,要么就是性能不够。
尝试 vibe 调研 + 抽奖一个 RIIR 版本,抽了几次抽到了一个使用 pitch_shift 实现的音调降低、没有任何割裂的 rust 版本。这个版本还有一些小问题,比如音频有点失真,但是比起初版音频割裂要好太多了,至少是个能玩的水平了。然后就是调参,我把 pitch_shift 的 WINDOW_DURATION_MS: usize 从 50 调成 18,失真也减缓了很多,这个音质我感觉已经可以接受了。很高兴,去楼下买了手撕鸡吃(午饭 + 晚饭),回来发了一篇频道庆祝。
然后就是漫长的程序优化了。
- 我希望这个程序是傻瓜式的,可以帮人自动判断游戏是否调用了 dsound.dll,但是……尝试了下发现挺困难的,静态分析基本没辙,好多汉化组的 patch 可是会 spawn 其他 galgame 进程的。至于动态分析那又是 windows api on rust 那一套,感觉我能写一年,这个优化先放着。
- 为了同时支持不同的倍速,我可以提前编好一堆 dll,然后根据用户的选择释放出某一个。这要求这些 dll 在我的二进制里是整体压缩的,否则这个大小我无法接受。当然使用 include_assets 是可以,这玩意强调了它是唯一一个做了 solid 压缩并且还支持 zstd 的。但是我又不想把所有 dll 存在 git 里,否则这个 git 仓库的大小我又不能接受;而且 include_assets 也说了它的缺陷是运行时会全部解压到内存……想来想去还是直接全程用 zip 比较好,这样仓库也不会太大,运行时也不需要全部解压。
- 然而 deflate 字典太小,压缩率实在是太差,被 zstd 和 lzma 爆杀了。想了想,还是用 include_assets 吧。
- 再想想?再想想?为啥要编一大堆 dll 出来,直接用环境变量就行了(笑)。我怎么一开始没想到呢。
- 程序同时支持 cli 和 tui,tui 的话我有思考过要不要用 ratatui 做,后面想想这个可以慢慢来,先用 terminal-menu 糊一个。
- 糊出来发现还挺好用的,这玩意有对
Vec<&str>专门做过优化,用起来还不错。
- 糊出来发现还挺好用的,这玩意有对
最终糊出了初版的 AudioSpeedHack v0.1.0。不过实际测下来问题还是挺多,详见 issue 与 TODO。
MMDevAPI.dll
hook dsound.dll 只能在一部分引擎上使用,对于较新引擎的 galgame 仍然无法加速。当务之急是 hook 掉更多的 dll,于是我把目光投向了 unity 使用的 MMDevAPI.dll 上。这是因为 unity 目前还没有办法通过脚本封包,无法进行 SPEEDUP;而使用 unity 引擎制作的《魔法少女的魔女审判》是我一直很想推的 galgame。
微软实在是可恶,这个 dll 不像 dsound 有 dsoal,xaudio 有 faudio,它没有现成的 wrapper 源码。经过一番搜索,我决定参考 wine 的 MMDevAPI 进行修改。wine 的源码虽然不能直接编译成 windows x64/x86 dll 无缝替换,但是我可以丢给 LLM 参考和抽奖。我尝试使用 wrap_dll 导出 dll wrapper,然后将更改后的源码、所有导出的内容一起喂给 LLM,几轮对话通过编译。
编出的 dll 拿来测试,但是 unity 始终不会加载同目录下的 MMDevAPI.dll,估计是有什么安全机制。尝试把系统 System32 里的 MMDevAPI.dll 替换掉,打开游戏,成功观察到音频加速!再打开我的音高处理软件,就可以愉快游玩 SPEEDUP 版《魔法少女的魔女审判》了!!
这对我来说又是一次鼓舞:因为我之前折腾 unity 封包一直受挫,在深刻体会到解封包的局限性和无力感后,能找到一个可行的方向,实在是……意义党逢意义。
V0 版本遇到的问题
在开发 dll wrapper 的时候也遇到了许多问题,这里记录一下。
音质下降问题
实际测试发现,AudioSpeedHack 每运行 20 分钟左右音质都会大幅下降,人声都带上了“钢管音”,需要重启。
dll wrapper 修改的逻辑不多,应该不是那里面的问题,那就只可能是我的音高降低程序的问题了。审查发现是 pitch_shift 库里的 f32 精度丢失问题,打了个 patch。
MMDevAPI.dll 加载问题
这个也是老大难了,MMDevAPI 有 windows 系统保护,不会加载当前目录的 dll。
本来我以为保护在 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs 里,但是这里面并没有找到。然后我尝试写了一个 hook,hook 掉程序的 LoadLibraryA/W,但是也没用,所以这个 dll 的系统保护涉及到更底层的机制。
注册表里搜 mmdevapi,搜出几个结果,然后我尝试把它们修改后搬到 HKEY_CURRENT_USER 表里,然后就可以用了。
dsound 在 2 倍速以上循环播放
使用 dsoal 修改而来的 dsound 会出现 2 倍速以上语音重复播放的问题,某一段 buffer 的语音会被重放,而且倍速越高,重放的频率越大,这几乎肯定是应用的音频数据填充 buffer 的速率不足的原因。查看 OpenAL 代码发现 AL_PITCH 有一个硬性限制:
/**
* Source pitch.
* Type: ALfloat
* Range: [0.5 - 2.0]
* Default: 1.0
*
* A multiplier for the sample rate of the source's buffer.
*/
#define AL_PITCH 0x1003emmm,难道要我再改 OpenAL 代码吗?
后来不再基于 OpenAL,直接 hook dsound,结果还是发现有这个问题,原来这是 dsound 本身的限制啊。
有试过手动降采样,直接硬去掉一半数据点肯定不行,有混叠;用 speex 库做降采样吧,然后发现降采样后程序给的音频数据仍然是以 2.0 速率给出,说明这是一个比直接操作 buffer 还要更底层的限制。那我真没辙了。
MMDevAPI 特定倍速无声
最初版的 MMDevAPI 在 1.5 和 2.0 倍速下可以正常工作,但是 1.7 或 1.8 倍速就无声了。
LLM 提出应该是一些小数计算导致采样率没对上。然后增加了四舍五入机制,1.7、1.8 倍速就可以使用了;但是仍然有一些倍速,例如 2.3 倍速仍然无声,可能是程序还有一些问题。
暂时先放着了,现在全身心投入到 V1 的开发中。
dll wrapper V1
V0 已经勉强能用了,比如我用 V0 推完了魔裁,但是问题还是比较大的。
最大的问题还是音质,即使修了音质下降问题,pitch_shift 还是难担加速重任,其使用的经典相位声码器比较初级,在处理人声时效果较差,表现就是钢管音,我推 gal 基本都无法开外放,只能戴耳机,隔绝环境音拯救一下音质。
于是将目光投向 SoundTouch:如果我直接拿到 buffer 数据然后用 ST 对原始数据进行不变调加速呢?WSOLA 的效果还是相当值得信任的,毕竟天天解封包然后 ffmpeg 加速也是用的 WSOLA(严格来说不是),质量有保证。
在更早的时候,dll wrapper 的可行性刚刚确定,我就已经有了这个想法。当时尝试 vibe 了一个使用 SoundTouch 直接对音频数据进行加速的 dll wrapper,可惜的是其无法正常使用,我就暂时搁置了这个想法。直到后来 V0 已经相对成熟,恰逢 Gemini 出了 3 pro,我便乘着 LLM 升级的西风开始了 V1 的迁移工作。
dsound.dll
由于有了 V0 的 code base,我不必从 0 开始让 LLM 生成,直接把 V0 code 和需求喂给它即可。经过几轮 review 和性能优化,直接成功加速。顺带分析了一下 APP 可能调用的函数,增大 hook 覆盖面,解决掉了 issue #3。Gemini 3 pro,神!
MMDevAPI.dll
首先将 dsound.dll 里使用的 SoundTouch 提取成一个单独的 AudioProcessor.h,方便复用。然后如法炮制。但是这次则没有 dsound.dll 那么好的运气了,这样写出的 dll 有两个问题:
- 魔裁的背景音乐没问题,但是人声断续
- 在 恋狱~月狂病~/ふゆから、くるる。 上均有不同程度的卡顿、音量飘忽不定或静音问题。
第一个问题好解决,随便尝试了下,给 ST 到声卡加了个缓冲区就解决了,甚至没有正经排查过。(根据后续日志,可以认为原因是 ST 会攒一定数据才会跑一次加速,默认参数 ST 的单次输出数据量可能超出声卡缓冲区大小。)
第二个问题就太棘手了,搞了好几天都查不出问题;即使给出 log,LLM 也会在几个错误解法上反复横跳。我尝试了很多方向:
- 双缓冲,input 侧也放了一个 RingBuf。(后来发现完全没用,SoundTouch 内部已经有缓冲区了,自己会攒一定数据才进行一次加速。)
- 修改 input 缓冲的驱动数据量。(也没用,理由同上)
- 发现这个问题有点复杂,做个 logger 吧,把 APP 喂音频时,缓冲区状态和声卡状态都打出来。后续还加了更多打印的日志,比如 ST 每次处理的输入和输出量(然后才发现 ST 的工作逻辑)。
- Backpressure 策略,GetCurrentPadding 不能进入“长时间一直返回 0”的循环,否则应用会快速输出数据顶爆 RingBuf。目前的策略是根据 output RingBuf 做二值化。
- 当前架构是专门开一个 Worker 线程,其一个职责是把输出缓冲区的数据喂给声卡。Worker 线程处理完数据后,因为系统调度原因实际 sleep 得比较久,硬件缓冲区就会见底导致卡顿。本来 Worker 线程是靠 sleep 处理的,这个精度太差了,16ms 够声卡爆炸好几次了。先优化成 timeBeginPeriod(1),后来直接改成靠声卡的 event 驱动。
- 给 ST 的 output 加了一个写数据到 WAV 的过程用于调试。这时候的音频是连续非卡顿的,但也有高糊、音量不定问题。
然而所有这些修改都没有解决问题。
直到我把 ST 的 input 也打到 WAV 里,发现从 input 开始音频就已经不对劲,然后才定位到问题根因:初始化 IAudioClient 时如果使用的是 WAVEFORMATEXTENSIBLE 而非 WAVEFORMATEX,则真正的格式信息存储在扩展部分的 SubFormat 中。当前 AudioProcessor 的格式处理错了,然后跟 ST 对异常响度处理的内部机制混在一起,导致问题不明显。
问题解决后,再做一下 dsound 兼容、调参平衡延迟和稳定性,就可以投入使用了。立刻开打《ふゆから、くるる。》!不对我怎么还要上班 T_T
回家一测试,AudioSpeedHack 在我硬盘上的所有 18 个 galgame 的加速成功率是 100%。然后我还发现新版的 MMDevAPI 真的非常牛逼,只 MMDevAPI.dll 一个就实现了 100% 覆盖率,它就是 WASAPI 的化身,我为了它付出的时间都是值得的。dsound 可有可无,不过可以作为叠加加速的工具,还是有用武之地的。
V1 版本遇到的问题
V1 版本遇到这些问题已经是 2026 中旬了,此时正是 AI agent 爆火时期,因此这些问题基本也都是 agent 排查,简单解决。
MMDevAPI 在部分游戏无声
DRACU-RIOT! QHD Edition、KANADE 两个游戏使用 MMDevAPI 加速音频时会变为无声;dsound 加速正常。排查发现是游戏进程限制了 DLL 搜索顺序,而我的实现是注册表里将 InprocServer32 设为裸名 MMDevAPI.dll,游戏无法加载根目录下的 MMDevAPI.dll proxy 而判定为没有音频设备。
鬼哭街黑屏
加载 MMDevAPI 后启动鬼哭街,直接黑屏无法进入游戏。
根因:NScripter 系引擎每秒新建/销毁上千个音频 client(什么狗屎玩意),而我在
后话
开发支持语音加速的 GalgameManager
大概 2025.12 月底,由于有多地游玩 galgame 的需求,并且现存的产品我都不是很满意,我开始实现我之前的灵感:做一个 GalgameManager。最初主要用于存档同步,之后又逐步添加了许多我执着的功能。
但是如何把 SPEED UP 优雅地集成到 GalgameManager 里是个难题,既然都做了启动游戏的魔改,那么作为启动链的一环,就不得不跟 Locale Emulator 和 translator 一起考虑。我在脑子里构想许久,感觉还是得搞一个灵活的插件系统。不过虽然方案已经逐渐在脑子里成型,但这个代码量实在是有点大,所以我一直找不到足够的动力去做这个事情。
试用了一下 GLM 5.1 感觉还不错,至少能写出生产级别的代码了。过了一段时间后,GalgameManager 迎来了 1.0.0 大更新,在 AI 的帮助下我搞了个插件系统,集成到了 GalgameManager 里,这其中就包括了我心心念念的语音加速功能。于是 AudioSpeedHack 那个 TUI 就正式退役了。
GalgameManager 刚开始的时候基本就是古法编程,倾注了我最多的心血;虽然 1.0.0 之后开始用 Agent 写大量代码,不过我仍然会做代码质量把控。
Wine 上的语音加速
闲的无聊,GalgameManager 1.2.0 优化了在 Linux 原生应用使用 Wine 启动 Galgame 的体验,当然语音加速我肯定也是想带过去的。
Wine 有一个 WINEDLLOVERRIDES env 可以控制从哪里加载,native 代表优先用游戏提供的 dll,builtin 是优先加载系统 dll。让游戏加载我的 dsound dll 可以通过 WINEDLLOVERRIDES 设置,非常简单,我的 dll proxy 到 System32/SysWOW64 也没啥问题,wine 会自动处理这个路径。但是 MMDevAPI 就没那么简单了,我使尽浑身解数也没法让游戏去加载我的 MMDevAPI wrapper:
- 原先在 Windows 上有效的修改注册表(在 wine 上使用 wine regedit)是不生效的。
- 为了排除 Wine 对 MMDevAPI.dll 本身的特殊 hack 的影响,我更换 dll 名字为
MMDevAPI_proxy.dll,并同步修改注册表的加载位置,也无效。
- 为了排除 Wine 对 MMDevAPI.dll 本身的特殊 hack 的影响,我更换 dll 名字为
- 直接替换 System32/SysWOW64 里的 dll(然后原先的 dll 重命名一下,让我的 proxy 去找新的 dll)也是不行的。
一个置信度比较高的说法是,Wine 在 C:\windows\system32\MMDevAPI.dll 放的是一个 fake DLL;load_builtin 检测到 wine_fakedll 标志后,强制覆盖 n,b 设置为 LO_BUILTIN(dlls/ntdll/unix/loader.c:1270)。所以只要加载路径经过 system32 的 fake DLL,就会强制加载 builtin。那这样的话就难解了,除非我做出注入游戏进程的 SPEED UP,否则目前的路子好像都行不通。
所以目前在 Wine 上玩 galgame,只能对支持 dsound 的游戏使用 SPEED UP。
番外篇:galgame 语音不中断
做完 SPEEDUP,虽然我个人比较激动,然而发到 B 站和两个 gal 群没啥太大反响(当然也有非常喜欢的,同道中人说是)。gal 群复读道:我更需要语音不中断。于是尝试研究一下。
MMDevAPI 基本是无法完成这个需求的,因为 MMDevAPI 拿到的一般是游戏软件混音完毕后的音频,无法区分不同段的人声/BGM。而 dsound 时代一般会把混音交给 API,于是我可以在 dsound 内部拿到不同的 buffer,自然也可以区分人声和 BGM 了。
于是就从 dsound 入手。在开发(vibe)过程随便记录一点:
- 基本架构就是把 Voice 暂存到一个 FIFO 缓冲区里,每个时刻只允许一段音频播放。然后再实现一个 feeder 线程负责把音频喂给声卡,其他类型的音频就直通。这个没啥问题。
- 欺骗游戏速度:这个是我的突发奇想,我完全可以套用之前 SPEEDUP 的成果,欺骗游戏语音播放的进度,让它飞速填满此段语音的缓冲 buffer。这样可以将后一句语音对前一句的影响降低至最小。[1]
- 区分人声和语音:
- 只有在 SPEEDUP 状态下才能使用音频长度这个判据。 这个通常来说是最准确的判据。
- 无法通过 DSBPLAY_LOOPING 判断人声,有的引擎会偷懒把所有音频都标上 DSBPLAY_LOOPING。
- 无法通过 buffer size 判断人声。有的引擎不管是啥音频都会申请一段固定长度的 buffer。
- 不过可以通过长度筛掉一部分 SE,SE 太短了。
- 后来直接通过音频处理里的人声特征判定去做了。
- 初级频域分析的效果很差,我决定直接做纯时域分析,包含时长、连续高能量帧、低能量帧比率、动态范围等。
- 如何判断同一语音:每次 Play 判定语音并入队。一般 buffer 可以被 Lock 很多次,但是 Play 一般每段语音只有一次。
经过一些测试,可以在 ジュエリー・ハーツ・アカデミア 上正常运行了……
が、事情真的有这么简单吗?当我将逻辑搬到 Artemis 上就出了问题。
- 最大的问题其实是 APP 给音频的逻辑,给出的一些语音会在中间插入一段较长的静音,这点也是我 dump WAV 才发现的。我尝试了很多方法都无法解决这个问题,只好在入队时多做一次去 0 的处理。
然后在 Artemis 上跑通以后,Krkr 又出了问题,而且一个比一个棘手。。。这时候我已经隐约感觉到了,搞语音不中断其实比加速还难。
- krkr 里所有语音、音效都是 0.5s 一个 buffer。如果按照当前做法,把 0.5s 判为 SE 则没有任何效果;把 0.5s 判为语音则每句话只说前 0.5s,并且由于每次点击/鼠标进入按钮/移出按钮都会触发 SE(即使已经把 SE 设为静音),会导致语音被 SE 抢占,延后几秒钟。
- krkr 里 SE 的判定也比较难,之前的时域分析会将其判为 voice;而在 buffer 延长的机制下要通过时长判断 SE 更加困难。
- 在 krkr 的场合,SPEEDUP 效果无法达到预期,最多只能进行 2x 播放(这是已知问题,之前 dsound 的加速上限就是 2。)。所以一段 10s 的语音要等 5s 才能播放出来,如果此时跳转下一句就又被切断了。这是无法接受的。
解决方法是:
- 对于 krkr 的流式播放问题,采用 stream ctx + 队列进行流控,封装一个播放器进行音频播放;拦截游戏的停止播放事件,给流标上 End of stream 标记,最终停止行为由播放器控制。
- 提高人声识别准确率,这里使用了一些更极致的做法,直接上模型,silero-vad + onnxruntime 来识别人声。
- 由于 silero-vad 强制要求输入为 16kHz 音频,需要一次重采样。之前 AI 乱搞了一版线性插值,听着就感觉不妙,然后换成了 WMF 的
CResamplerMediaObject,SetHalfFilterLength到最高。虽然也不算顶级重采样,但是对语音识别来说够用了。
- 由于 silero-vad 强制要求输入为 16kHz 音频,需要一次重采样。之前 AI 乱搞了一版线性插值,听着就感觉不妙,然后换成了 WMF 的
在 Ever17(MAGES. Engine 引擎)上,之前的零中断实现又无法使用。观察日志,每个语音都会经历 SetVolume(-10000)(静音)、Play()、恢复音量的过程(后来发现还有中间状态),因此我们也需要处理音量变更的拦截逻辑。写了一版 lock volume 到「播放开始时音量」还不行,因为播放开始时的音量就可能不是正常的,而是后面再 SetVolume 成正常音量。于是最终实现成音量只增不减,这样才能有比较好的效果。(题外话,Ever17 这 B 引擎完全搞不懂是什么时候会触发改音量,我鼠标点快一些和慢一些的表现甚至还不一样的)
这其实跟网关流式转发还是集齐 body 后再转发其实是类似的逻辑。刚开始用加速去拉完整个音频属于是步入歧途,流式转发才是未来。 ↩︎
