fix slow-pitched audio by patching file directly (#4)
This commit is contained in:
parent
7bbf65660e
commit
c121dc3018
|
@ -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...
|
||||
|
|
|
@ -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...
|
||||
|
|
|
@ -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..."
|
||||
|
|
|
@ -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}`);
|
||||
};
|
|
@ -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");
|
||||
|
|
Loading…
Reference in New Issue