fix slow-pitched audio by patching file directly (#4)

This commit is contained in:
segfault-bilibili 2023-06-25 07:08:00 +08:00 committed by GitHub
parent 7bbf65660e
commit c121dc3018
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 306 additions and 28 deletions

View File

@ -45,6 +45,14 @@ echo Applying misc patches...
REM call copy /Y "%~dp0\patches\images\story_ui_sprites00_patch.plist" "%~dp0\build\app\assets\package\story\story_ui_sprites00.plist"
REM call copy /Y "%~dp0\patches\images\story_ui_sprites00_patch.png" "%~dp0\build\app\assets\package\story\story_ui_sprites00.png"
REM Fix low-pitched audio bug since magireco 3.0.1
REM This was once done with MagiaHook.
REM However, due to unexplained reason,
REM that hook made the game engine probabilistically fail to create OpenSLES player,
REM thus the game would get silenced in that way.
node "%~dp0/patches/audiofix.js" --wdir "%~dp0/build/app" --overwrite
if errorlevel 1 goto errorexit
call copy /Y "%~dp0\patches\koruri-semibold.ttf" "%~dp0\build\app\assets\fonts\koruri-semibold.ttf"
echo Updating sprites and AndroidManifest.xml...

View File

@ -45,6 +45,13 @@ echo Applying misc patches...
REM call copy /Y "%~dp0\patches\images\story_ui_sprites00_patch.plist" "%~dp0\build\app\assets\package\story\story_ui_sprites00.plist"
REM call copy /Y "%~dp0\patches\images\story_ui_sprites00_patch.png" "%~dp0\build\app\assets\package\story\story_ui_sprites00.png"
REM Fix low-pitched audio bug since magireco 3.0.1
REM This was once done with MagiaHook.
REM However, due to unexplained reason,
REM that hook made the game engine probabilistically fail to create OpenSLES player,
REM thus the game would get silenced in that way.
node "%~dp0/patches/audiofix.js" --wdir "%~dp0/build/app" --overwrite
call copy /Y "%~dp0\patches\koruri-semibold.ttf" "%~dp0\build\app\assets\fonts\koruri-semibold.ttf"
echo Updating sprites and AndroidManifest.xml...

View File

@ -8,6 +8,7 @@ NINJA="${MT_NINJA:-ninja}" # /usr/bin/ninja
CURL="${MT_CURL:-curl}" # /usr/bin/curl
JAVA="${MT_JAVA:-java}" # /usr/bin/java
PYTHON="${MT_PYTHON:-python3}" # /usr/bin/python3.8
NODEJS="${MT_NODEJS:-node}" # /usr/bin/node
APKTOOL="${MT_APKTOOL:-apktool_2.6.0.jar}"
ZIPALIGN="${MT_ZIPALIGN:-zipalign}" # ~/android-sdk/build-tools/zipalign
APKSIGNER="${MT_APKSIGNER:-apksigner}" # ~/android-sdk/build-tools/apksigner
@ -76,6 +77,13 @@ _create() {
# cp "${BASEDIR}/patches/images/story_ui_sprites00_patch.plist" "${BASEDIR}/build/app/assets/package/story/story_ui_sprites00.plist"
# cp "${BASEDIR}/patches/images/story_ui_sprites00_patch.png" "${BASEDIR}/build/app/assets/package/story/story_ui_sprites00.png"
# Fix low-pitched audio bug since magireco 3.0.1
# This was once done with MagiaHook.
# However, due to unexplained reason,
# that hook made the game engine probabilistically fail to create OpenSLES player,
# thus the game would get silenced in that way.
"${NODEJS}" "${BASEDIR}/patches/audiofix.js" --wdir "${BASEDIR}/build/app" --overwrite
cp "${BASEDIR}/patches/koruri-semibold.ttf" "${BASEDIR}/build/app/assets/fonts/koruri-semibold.ttf"
echo "Updating sprites and AndroidManifest.xml..."

283
patches/audiofix.js Normal file
View File

@ -0,0 +1,283 @@
const fs = require("fs");
const path = require("path");
// workaround audio bug since Magia Record 3.0.1
// author: segfault-bilibili
// English README
//
// Since Magia Record 3.0.1, there appears to be a bug affecting minor fraction of players.
//
// Such bug makes the game audio (including BGM, sound effects etc) sound strange:
// (1) the pitch sounds to be lower than normal;
// (2) the time sounds to be "stretched" longer/slower than normal.
//
// There's currently an experimental modification to workaround this bug.
// To distinguish from other EX versions without this experimental modification,
// the APK file under this subdirectory come with "-soundfix" suffix in its file name, like:
// "magireco-3.0.2-EX-soundfix.apk".
//
// The root cause of this bug is still unclear. It should be some kind of sample rate mismatch.
//
// It's observed that the audio is "stretched" exactly 8.84% longer than it should be,
// which matches exactly with 48 / 44.1 = 108.84%;
// plus the currently observed fact that only environments with a 44.1kHz system audio output
// sample rate seem to have this problem;
// it's guessed that deceiving the game to make it think the system audio output sample rate
// was 48kHz (instead of actual 44.1kHz) might make this problem go away - and luckily, it does,
// at least in our limited tests.
//
// However, it's still confusing why such trick seems to work.
// 中文说明
//
// 自从魔法纪录3.0.1版开始出现了一个影响少数玩家的bug。
//
// 这个bug会让音频包括背景音乐、音效等等听起来很奇怪
// (1) 音调听起来比正常低;
// (2) 时间听起来也被拉长/变慢了。
//
// 目前有一个实验性的小修改来绕过这个bug。
// 为了与不带这个修改的其他EX版区分此目录下的APK文件都带有"-soundfix"文件名后缀,比如:
// "magireco-3.0.2-EX-soundfix.apk"。
//
// 导致这个bug的根本原因还不太清楚。可能是某种采样率不匹配。
//
// 据观察音频被拉长到正好108.84%,和 48 / 44.1 = 108.84% 吻合。
// 再加上目前观察到只有系统音频输出采样率是44.1kHz的环境才有这个问题;
// 就可以猜测如果欺骗游戏、使其认为系统音频输出采样率是48kHz而不是实际值44.1kHz
// 就能让问题消失——实际上也确实消失了,至少在有限的测试里是这样。
//
// 然而,现在还并不清楚为什么这一招看上去能奏效。
// usage:
// apktool d --no-src --no-res magireco-3.0.2-EX.apk
// node audiofix.js --wdir magireco-3.0.2-EX --overwrite
// apktool b magireco-3.0.2-EX
// zipalign -p -f -v 4 magireco-3.0.2-EX/dist/magireco-3.0.2-EX.apk magireco-3.0.2-EX-soundfix.apk
// apksigner sign --ks keystore.jks --ks-pass pass:12345678 magireco-3.0.2-EX-soundfix.apk
const EM_AARCH64 = 0xb7, EM_ARM = 0x28;
const ELFCLASS64 = 2, ELFCLASS32 = 1;
function parseElf(elf) {
let result = {};
// parse elf header
const read_e_ident = elf.subarray(0, 16);
if (Buffer.compare(Buffer.from([0x7f, 0x45, 0x4c, 0x46]), read_e_ident.subarray(0, 4)) != 0) {
throw new Error("not ELF");
}
const eh = result.elf_header = {
e_ident: {
ei_class_2: read_e_ident.readUInt8(4),
ei_data: read_e_ident.readUInt8(5),
ei_version: read_e_ident.readUInt8(6),
ei_osabi: read_e_ident.readUInt8(7),
ei_abiversion: read_e_ident.readUInt8(8),
ei_nident_SIZE: read_e_ident.readUInt8(0xf),
},
e_type: elf.readUInt16LE(0x10),
e_machine: elf.readUInt16LE(0x12),
e_version: elf.readUInt32LE(0x14),
}
if (result.elf_header.e_ident.ei_nident_SIZE != 0) {
throw new Error("ei_nident_SIZE != 0");
}
let is64 = true;
let e_flags_offset = 0x30;
switch (eh.e_ident.ei_class_2) {
case ELFCLASS64:
if (eh.e_machine != EM_AARCH64) {
throw new Error(`e_machine (${eh.e_machine}) != EM_AARCH64`);
}
is64 = true;
eh.e_entry_START_ADDRESS = Number(elf.readBigUInt64LE(0x18));
eh.e_phoff_PROGRAM_HEADER_OFFSET_IN_FILE = Number(elf.readBigUInt64LE(0x20));
eh.e_shoff_SECTION_HEADER_OFFSET_IN_FILE = Number(elf.readBigUInt64LE(0x28));
e_flags_offset = 0x30;
break;
case ELFCLASS32:
if (eh.e_machine != EM_ARM) {
throw new Error(`eh.e_machine (${eh.e_machine}) != EM_ARM`);
}
is64 = false;
eh.e_entry_START_ADDRESS = elf.readUInt32LE(0x18);
eh.e_phoff_PROGRAM_HEADER_OFFSET_IN_FILE = elf.readUInt32LE(0x1c);
eh.e_shoff_SECTION_HEADER_OFFSET_IN_FILE = elf.readUInt32LE(0x20);
e_flags_offset = 0x24;
break;
default:
throw new Error(`unknown ei_class_2 = ${eh.e_ident.ei_class_2}`);
}
eh.e_flags = elf.readUInt32LE(e_flags_offset);
eh.e_ehsize_ELF_HEADER_SIZE = elf.readUInt16LE(e_flags_offset + 4);
eh.e_phentsize_PROGRAM_HEADER_ENTRY_SIZE_IN_FILE = elf.readUInt16LE(e_flags_offset + 6);
eh.e_phnum_NUMBER_OF_PROGRAM_HEADER_ENTRIES = elf.readUInt16LE(e_flags_offset + 8);
eh.e_shentsize_SECTION_HEADER_ENTRY_SIZE = elf.readUInt16LE(e_flags_offset + 10);
eh.e_shnum_NUMBER_OF_SECTION_HEADER_ENTRIES = elf.readUInt16LE(e_flags_offset + 12);
eh.e_shtrndx_STRING_TABLE_INDEX = elf.readUInt16LE(e_flags_offset + 14);
// parse section header
const sh = result.section_header_table = [];
const shoff = eh.e_shoff_SECTION_HEADER_OFFSET_IN_FILE;
const shnum = eh.e_shnum_NUMBER_OF_SECTION_HEADER_ENTRIES;
const shentsize = eh.e_shentsize_SECTION_HEADER_ENTRY_SIZE;
const read_shtab = elf.subarray(shoff, shoff + shentsize * shnum);
const shtrndx = eh.e_shtrndx_STRING_TABLE_INDEX;
const read_shstrtab = elf.subarray(shoff + shentsize * shtrndx);
const strtab_offset = is64 ? Number(read_shstrtab.readBigUInt64LE(24)) : read_shstrtab.readUInt32LE(16);
const strtab_size = is64 ? Number(read_shstrtab.readBigUInt64LE(32)) : read_shstrtab.readUInt32LE(20);
const read_strtab = elf.subarray(strtab_offset, strtab_offset + strtab_size);
for (let i = 0, offset = 0; i < shnum; i++, offset += shentsize) {
let read_entry = read_shtab.subarray(offset, offset + shentsize);
let s_name_off = read_entry.readUInt32LE(0);
let s_name_str = read_strtab.subarray(s_name_off, read_strtab.indexOf(0x00, s_name_off)).toString("ascii");
sh.push({
s_name: {
s_name_off: s_name_off,
s_name_str: s_name_str,
},
s_type: read_entry.readUInt32LE(4),
s_flags: read_entry.readUInt32LE(8),
s_addr: is64 ? Number(read_entry.readBigUInt64LE(16)) : read_entry.readUInt32LE(12),
s_offset: is64 ? Number(read_entry.readBigUInt64LE(24)) : read_entry.readUInt32LE(16),
s_size: is64 ? Number(read_entry.readBigUInt64LE(32)) : read_entry.readUInt32LE(20),
s_link: read_entry.readUInt32LE(is64 ? 40 : 24),
s_info: read_entry.readUInt32LE(is64 ? 44 : 28),
s_addralign: is64 ? Number(read_entry.readBigUInt64LE(48)) : read_entry.readUInt32LE(32),
s_entsize: is64 ? Number(read_entry.readBigUInt64LE(56)) : read_entry.readUInt32LE(36),
});
}
//parse dynamic symbol table
const dynsym = result.dynamic_symbol_table = [];
const dynsym_sec = sh.find((entry) => entry.s_name.s_name_str === ".dynsym");
const dynsym_secoffset = dynsym_sec.s_offset;
const dynsym_secsize = dynsym_sec.s_size;
const dynsym_entsize = dynsym_sec.s_entsize;
if (dynsym_entsize <= 0) {
throw new Error(`dynsym_entsize ${dynsym_entsize} <= 0`);
}
const read_dynsym = elf.subarray(dynsym_secoffset, dynsym_secoffset + dynsym_secsize);
const dynstr_sec = sh.find((entry) => entry.s_name.s_name_str === ".dynstr");
const dynstr_secoffset = dynstr_sec.s_offset;
const dynstr_secsize = dynstr_sec.s_size;
const read_dynstr = elf.subarray(dynstr_secoffset, dynstr_secoffset + dynstr_secsize);
for (
let i = 0, offset = 0;
offset + dynsym_entsize <= dynsym_secsize;
i++, offset += dynsym_entsize
) {
let read_entry = read_dynsym.subarray(offset, offset + dynsym_entsize);
let sym_name_off = read_entry.readUInt32LE(0);
let sym_name_str = read_dynstr.subarray(sym_name_off, read_dynstr.indexOf(0x00, sym_name_off)).toString("ascii");
dynsym.push({
sym_name: {
sym_name_off: sym_name_off,
sym_name_str: sym_name_str,
},
sym_info: read_entry.readUInt8(is64 ? 4 : 12),
sym_other: read_entry.readUInt8(is64 ? 5 : 13),
sym_shndx: read_entry.readUInt16LE(is64 ? 6 : 14),
sym_value: is64 ? Number(read_entry.readBigUInt64LE(8)) : read_entry.readUInt32LE(4),
sym_size: is64 ? Number(read_entry.readBigUInt64LE(16)) : read_entry.readUInt32LE(8),
});
}
return result;
}
function getTargetFunction(elf, info, funcName, funcOffset, bufLen) {
const syment = info.dynamic_symbol_table.find((entry) => entry.sym_name.sym_name_str === funcName);
const offset = syment.sym_value;
const size = syment.sym_size;
const func = elf.subarray(offset, offset + size);
if (funcOffset + bufLen > func.length) throw new Error("funcOffset + bufLen > func.length");
return func.subarray(funcOffset, funcOffset + bufLen);
}
function checkFunction(elf, info, funcName, funcOffset, buf) {
const target = getTargetFunction(elf, info, funcName, funcOffset, buf.length);
return Buffer.compare(target, buf) == 0;
}
function patchFunction(elf, info, funcName, funcOffset, buf) {
const target = getTargetFunction(elf, info, funcName, funcOffset, buf.length);
buf.copy(target);
}
const wdirIndex = process.argv.findIndex((arg) => arg === "--wdir");
if (wdirIndex == -1 || wdirIndex == process.argv.length - 1) throw new Error("please specify --wdir");
const wdir = process.argv[wdirIndex + 1];
const overwrite = process.argv.findIndex((arg) => arg === "--overwrite") != -1;
const libname = "libmadomagi_native.so";
const funcToPatch = "criNcv_GetHardwareSamplingRate_ANDROID";
const abiList = {
"arm64-v8a": [
{
funcName: funcToPatch,
checkList: [
{
offset: 8,
buf: [0xc0, 0x03, 0x5f, 0xd6],
}
],
patchList: [
{
offset: 4,
buf: [0x00, 0x70, 0x97, 0x52],
},
],
},
],
"armeabi-v7a": [
{
funcName: funcToPatch,
checkList: [
{
offset: 8,
buf: [0x1e, 0xff, 0x2f, 0xe1],
}
],
patchList: [
{
offset: 4,
buf: [0x80, 0x0b, 0x0b, 0xe3],
},
],
},
],
}
for (let abi in abiList) {
let filepath = path.join(wdir, "lib", abi, libname);
if (!fs.existsSync(filepath)) {
console.log(`skipped nonexist file ${filepath}`);
continue;
}
let filedata = fs.readFileSync(filepath);
console.log(`patching ${filepath}`);
let info = parseElf(filedata);
abiList[abi].forEach((patchInfo) => {
let mismatch = patchInfo.checkList.find((check) => !checkFunction(filedata, info, patchInfo.funcName, check.offset, Buffer.from(check.buf)));
if (mismatch != null) throw new Error("check failed");
patchInfo.patchList.forEach((patch) => patchFunction(filedata, info, patchInfo.funcName, patch.offset, Buffer.from(patch.buf)));
});
let writeToPath = path.join(wdir, "lib", abi, overwrite ? libname : libname.replace(/\.so$/, "-soundfix.so"));
fs.writeFileSync(writeToPath, filedata);
console.log(`written patched file to ${writeToPath}`);
};

View File

@ -457,16 +457,6 @@ pthread_mutex_t *http2SessionSetMaxConnectionNum(uintptr_t *session, int max) {
return http2SessionSetMaxConnectionNumOld(session, max);
}
uint32_t (*criNcv_GetHardwareSamplingRate_ANDROID_Hooked)();
uint32_t criNcv_GetHardwareSamplingRate_ANDROID() {
auto value = criNcv_GetHardwareSamplingRate_ANDROID_Hooked();
if (value == 44100) {
return 48000;
}
return value;
}
void initialization_error(const char* error) {
LOGE("%s", error);
auto errorMsg = string_format("A critical error has occurred, MagiaTranslate will not work properly and may crash. Please report this error on GitHub or Discord.\n%s", error);
@ -522,24 +512,6 @@ void *hook_loop(void *arguments) {
// For debugging
//DobbyHook(lookup_symbol(libLocation, "_ZN5http212Http2Session6setURIERKSs"), (void *)setUriDebug, (void **)&setUriDebugOld); - crashes arm32 now.
// speed fix
void *audioSampleRateFix = lookup_symbol(libLocation, "criNcv_GetHardwareSamplingRate_ANDROID");
if (audioSampleRateFix != nullptr) {
LOGD("Found criNcv_GetHardwareSamplingRate_ANDROID at %p.", (void *)audioSampleRateFix);
if (DobbyHook(audioSampleRateFix, (void *)criNcv_GetHardwareSamplingRate_ANDROID, (void **)&criNcv_GetHardwareSamplingRate_ANDROID_Hooked) == RS_SUCCESS) {
LOGI("Successfully hooked criNcv_GetHardwareSamplingRate_ANDROID.");
}
else {
initialization_error("Unable to hook criNcv_GetHardwareSamplingRate_ANDROID.");
pthread_exit(NULL);
}
}
else {
initialization_error("Unable to hook criNcv_GetHardwareSamplingRate_ANDROID.");
pthread_exit(NULL);
}
// Hooks
void *cocos2dnodeSetPosition = lookup_symbol(libLocation, "_ZN7cocos2d4Node11setPositionERKNS_4Vec2E");