Skip to content
This repository was archived by the owner on Jul 31, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions build.bat
Original file line number Diff line number Diff line change
Expand Up @@ -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...
Expand Down
7 changes: 7 additions & 0 deletions build_release.bat
Original file line number Diff line number Diff line change
Expand Up @@ -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...
Expand Down
8 changes: 8 additions & 0 deletions build_release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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..."
Expand Down
283 changes: 283 additions & 0 deletions patches/audiofix.js
Original file line number Diff line number Diff line change
@@ -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}`);
};
28 changes: 0 additions & 28 deletions src/MagiaClient.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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");
Expand Down