1
0
mirror of https://github.com/Escartem/AnimeWwise.git synced 2026-06-11 20:20:25 +08:00

Merge pull request #18 from Escartem/2.2

2.2
This commit is contained in:
Escartem
2025-02-03 11:57:11 +01:00
committed by GitHub
12 changed files with 393 additions and 94 deletions

View File

@@ -2,7 +2,7 @@
An easy to use tool to extract audio from some anime games, with the original filenames and paths. An easy to use tool to extract audio from some anime games, with the original filenames and paths.
![image](https://github.com/user-attachments/assets/e66048df-4d71-4bda-8201-1c2c67f44de7) ![image](https://github.com/user-attachments/assets/ce2c8b19-82a2-42fc-a149-ed9ffbb7c54b)
# Usage # Usage
@@ -12,7 +12,7 @@ An easy to use tool to extract audio from some anime games, with the original fi
2. Install dependencies -> `pip install -r requirements.txt` 2. Install dependencies -> `pip install -r requirements.txt`
3. Run the app with `python app.py` 3. Run the app with `python app.py`
4. Select your input folder containing your `.pck` files, it can be your game audio folder directly (if you decide to use this one, make sure the game is not running) 4. Select your input folder containing your `.pck` files, it can be your game audio folder directly (if you decide to use this one, make sure the game is not running)
![image](https://github.com/user-attachments/assets/72cf7983-00d0-4e98-b0d0-8b5547057a56) ![image](https://github.com/user-attachments/assets/e877a57a-a115-4c2e-beac-27d927d1a37e)
> [!TIP] > [!TIP]
> The audio folder can be found in the following locations > The audio folder can be found in the following locations
> - `GenshinImpact_Data\StreamingAssets\AudioAsset\...` > - `GenshinImpact_Data\StreamingAssets\AudioAsset\...`
@@ -23,10 +23,10 @@ An easy to use tool to extract audio from some anime games, with the original fi
> Diff files are `.hdiff` present in the update patches of the games. If you want to extract an hdiff content, you must have the pck file with the *same name before patch* in the input folder, pck's that do not have a corresponding hdiff file will be extracted normally, when they do have a corresponding hdiff file, *only the hdiff file content is extracted* and not the full pck > Diff files are `.hdiff` present in the update patches of the games. If you want to extract an hdiff content, you must have the pck file with the *same name before patch* in the input folder, pck's that do not have a corresponding hdiff file will be extracted normally, when they do have a corresponding hdiff file, *only the hdiff file content is extracted* and not the full pck
6. Select a mapping 6. Select a mapping
> [!WARNING] > [!WARNING]
> By default, the files extracted from the game don't have names, the mappings are here to help restore the original filenames and paths so it's easier to search, but not all games are supported, not at every version and the mapping does not guarantee to have every file named > By default, the files extracted from the game don't have names, the mappings are here to help restore the original filenames and paths so it's easier to search, there are only mappings for hoyo games and their coverage varies
7. After that, you can browse the files you loaded, if you messed up and wanna go back, you can select File > Reset to unload everything and go back to the starting screen. 7. After that, you can browse the files you loaded, if you messed up and wanna go back, you can select File > Reset to unload everything and go back to the starting screen.
![image](https://github.com/user-attachments/assets/9714b6ab-527a-49d9-ae98-354d1979a2b9) ![image](https://github.com/user-attachments/assets/73e2ece9-9fa7-4149-8674-762adb1ef50c)
8. In the `Extract` tab, you will be able to select what audio you want, choosing the output folder and audio format. You can extract everything or extract the files you selected in the `Browse` tab 8. In the `Extract` menu, you will be able to select what audio you want, choosing the output folder and audio format. You can extract everything or extract the files you selected
> [!NOTE] > [!NOTE]
> The program does not check for existing files in the output folder, it will overwrite them, make sure to check your folder before starting the extraction > The program does not check for existing files in the output folder, it will overwrite them, make sure to check your folder before starting the extraction
9. Extract your files, and enjoy ! 9. Extract your files, and enjoy !
@@ -42,3 +42,13 @@ The program has been tested and proved to be very efficient with extraction (not
# Contribute # Contribute
Feel free to contribute to this project as much as you want, a share would be very appreciated aswell, I'll be glad to know if this helped anyone <3 Feel free to contribute to this project as much as you want, a share would be very appreciated aswell, I'll be glad to know if this helped anyone <3
# Credits
- [@Razmoth](https://github.com/Razmoth) - help on figuring out keys parsing to recover names for genshin and zzz
- [@Dimbreath](https://github.com/Dimbreath) - AnimeGameData, TurnBasedGameData and ZZZData
- [@Kei-Luna](https://github.com/Kei-Luna) - instructions on recovering names for genshin music
- [@davispuh](https://github.com/davispuh) - star rail keys bruteforce tool
- [@bnnm](https://github.com/bnnm) - wwise audio exploration tool
- @hcs - wwise audio extraction script
- [@vgmstream](https://github.com/vgmstream) and their contributors - wwise headers parsing

View File

@@ -6,23 +6,22 @@ class Allocator:
def __init__(self): def __init__(self):
self.files = {} self.files = {}
def load_file(self, path): def load_file(self, path, name):
filename = os.path.basename(path)
with open(path, "r+b") as f: with open(path, "r+b") as f:
mmap_object = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) mmap_object = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ)
self.files[filename] = mmap_object self.files[name] = mmap_object
def unload_file(self, name): def unload_file(self, name):
self.files[os.path.basename(name)].close() self.files[name].close()
def read_at(self, file, offset, size): def read_at(self, file, offset, size):
mmap_object = self.files[os.path.basename(file)] mmap_object = self.files[file]
mmap_object.seek(offset) mmap_object.seek(offset)
data = mmap_object.read(size) data = mmap_object.read(size)
return data return data
def free_mem(self): def free_mem(self):
for file in list(self.files.keys()): for file in list(self.files.keys()):
self.files[os.path.basename(file)].close() self.files[file].close()
self.files.clear() self.files.clear()

125
app.py
View File

@@ -2,14 +2,16 @@ import os
import sys import sys
import json import json
import math import math
import time
import extract import extract
import platform import platform
import urllib
import webbrowser import webbrowser
from PyQt5 import uic from PyQt5 import uic
from requests import get from requests import get
from PyQt5.QtGui import QTextCursor from PyQt5.QtGui import QTextCursor
from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, QThread, QMetaType, Qt from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, QThread, QMetaType, Qt
from PyQt5.QtWidgets import QMessageBox, QMainWindow, QApplication, QFileDialog, QHeaderView, QAbstractItemView, QTreeWidgetItem, QAction, QActionGroup from PyQt5.QtWidgets import QDesktopWidget, QDialog, QMessageBox, QMainWindow, QApplication, QFileDialog, QHeaderView, QAbstractItemView, QTreeWidgetItem, QAction, QActionGroup
QMetaType.type("QTextCursor") QMetaType.type("QTextCursor")
@@ -70,6 +72,111 @@ class BackgroundWorker(QObject):
self.extract.extract_files(self.input, self.files, self.output, self.format, progress=self.progress.emit) self.extract.extract_files(self.input, self.files, self.output, self.format, progress=self.progress.emit)
self.finished.emit({"action": "extract"}) self.finished.emit({"action": "extract"})
class UpdaterWorker(QObject):
finished = pyqtSignal(bool)
progress = pyqtSignal(list)
def __init__(self):
super().__init__()
def run(self):
try:
# know current and latest maps
self.progress.emit([0, "Fetching index..."])
ver = lambda s: int(s.replace(".", ""))
index = open("maps/index.json", "r")
currentMaps = json.loads(index.read())
index.close()
latestMaps = get("https://raw.githubusercontent.com/Escartem/AnimeWwise/master/maps/index.json")
if latestMaps.status_code == 200:
latestMaps = json.loads(latestMaps.text)
# do each game
n_games = len(latestMaps["maps"])
game_size = 95 // n_games
for i in range(n_games):
current = currentMaps["maps"][i]
latest = latestMaps["maps"][i]
name = f"maps/{latest['name']}"
if (ver(current["version"]) < ver(latest["version"])) or not os.path.isfile(name):
self.progress.emit([5 + game_size * i, f'Updating {latest["game"]} to {latest["version"]}'])
url = f"https://raw.githubusercontent.com/Escartem/AnimeWwise/master/{name}"
urllib.request.urlretrieve(url, "maps/temp.map")
if os.path.isfile(name):
os.remove(name)
os.rename("maps/temp.map", name)
# update index
currentMaps["maps"][i]["version"] = latest["version"]
# save new index
index = open("maps/index.json", "w+")
index.write(json.dumps(currentMaps, indent=4))
index.close()
index_sum = sum([ver(e["version"]) for e in currentMaps["maps"]])
with open("version.json", "r+") as f:
data = json.loads(f.read())
data["mapsVersion"] = index_sum
f.seek(0)
f.write(json.dumps(data, indent=4))
f.truncate()
f.close()
# done
self.progress.emit([100, "Update finished ! The program will start shortly..."])
except Exception as e:
# failure :(
self.progress.emit([100, f"Update failed ! The program will start shortly... | {e}"])
time.sleep(3)
self.finished.emit(True)
class Updater(QDialog):
def __init__(self):
super(Updater, self).__init__()
uic.loadUi("updater.ui", self)
self.setWindowFlag(Qt.WindowCloseButtonHint, False)
self.center()
self.update()
def center(self):
qr = self.frameGeometry()
cp = QDesktopWidget().availableGeometry().center()
qr.moveCenter(cp)
self.move(qr.topLeft())
def update(self):
self.backgroundThread = QThread()
self.backgroundWorker = UpdaterWorker()
self.backgroundWorker.moveToThread(self.backgroundThread)
self.backgroundThread.started.connect(self.backgroundWorker.run)
self.backgroundWorker.finished.connect(self.updateFinished)
self.backgroundWorker.finished.connect(self.backgroundThread.quit)
self.backgroundWorker.finished.connect(self.backgroundWorker.deleteLater)
self.backgroundThread.finished.connect(self.backgroundThread.deleteLater)
self.backgroundWorker.progress.connect(self.updateProgress)
self.backgroundThread.start()
@pyqtSlot(bool)
def updateFinished(self):
self.close()
@pyqtSlot(list)
def updateProgress(self, data):
self.progressBar.setValue(data[0])
if len(data) == 2:
self.status.setText(data[1])
class AnimeWwise(QMainWindow): class AnimeWwise(QMainWindow):
def __init__(self): def __init__(self):
super(AnimeWwise, self).__init__() super(AnimeWwise, self).__init__()
@@ -87,6 +194,8 @@ class AnimeWwise(QMainWindow):
sys.stdout = TextEditStream(self.console) sys.stdout = TextEditStream(self.console)
self.extract = extract.WwiseExtract() self.extract = extract.WwiseExtract()
self.checkUpdates() self.checkUpdates()
self.totalProgress.setMaximum(10000)
self.fileProgress.setMaximum(10000)
# utils # utils
self.selectFolder = lambda: QFileDialog.getExistingDirectory(self, "Select Folder") self.selectFolder = lambda: QFileDialog.getExistingDirectory(self, "Select Folder")
@@ -102,10 +211,12 @@ class AnimeWwise(QMainWindow):
if currentVersion["version"] < latestVersion["version"]: if currentVersion["version"] < latestVersion["version"]:
print("Update found !") print("Update found !")
QMessageBox.information(None, "Info", "Newer version of the program is availble, please update.", QMessageBox.Ok) QMessageBox.information(None, "Info", "Newer version of the program is availble, please update it.", QMessageBox.Ok)
elif currentVersion["mapsVersion"] < latestVersion["mapsVersion"]: elif currentVersion["mapsVersion"] < latestVersion["mapsVersion"]:
print("Update found !") print("Update found !")
QMessageBox.information(None, "Info", "Newer version of the mappings are availble, please update the program.", QMessageBox.Ok) QMessageBox.information(None, "Info", "Newer version of the mappings are availble, the program will update them now.", QMessageBox.Ok)
self.updaterWindow = Updater()
self.updaterWindow.exec_()
else: else:
print("No updates") print("No updates")
except: except:
@@ -210,10 +321,13 @@ class AnimeWwise(QMainWindow):
# workers # workers
@pyqtSlot(list) @pyqtSlot(list)
def progressBarSlot(self, progress): def progressBarSlot(self, progress):
progress_value = math.ceil(progress[1]*100)
if progress[0] == "total": if progress[0] == "total":
self.totalProgress.setValue(math.ceil(progress[1])) self.totalProgress.setValue(progress_value)
self.totalProgress.setFormat("%.02f %%" % (progress_value / 100))
elif progress[0] == "file": elif progress[0] == "file":
self.fileProgress.setValue(math.ceil(progress[1])) self.fileProgress.setValue(progress_value)
self.fileProgress.setFormat("%.02f %%" % (progress_value / 100))
@pyqtSlot(dict) @pyqtSlot(dict)
def handleFinished(self, data): def handleFinished(self, data):
@@ -250,6 +364,7 @@ class AnimeWwise(QMainWindow):
files = [os.path.join(self.folders["input"], f) for f in os.listdir(self.folders["input"]) if f.endswith(".pck")] files = [os.path.join(self.folders["input"], f) for f in os.listdir(self.folders["input"]) if f.endswith(".pck")]
elif self.loadType == "file": elif self.loadType == "file":
path = QFileDialog.getOpenFileName(self, "Select .pck File", "", "PCK Files (*.pck)", options=QFileDialog.Options()) path = QFileDialog.getOpenFileName(self, "Select .pck File", "", "PCK Files (*.pck)", options=QFileDialog.Options())
self.folders["input"] = os.path.dirname(path[0])
files = [path[0]] files = [path[0]]
if len(files) == 0 or files[0] == "": if len(files) == 0 or files[0] == "":

10
bnk.py
View File

@@ -2,24 +2,27 @@
import io import io
from filereader import FileReader from filereader import FileReader
def bnk2wem(data): def bnk2wem(data, name):
# gets raw data from object # gets raw data from object
reader = FileReader(io.BytesIO(data), "little") reader = FileReader(io.BytesIO(data), "little", name=name)
bkhd_signature = reader.ReadBytes(4) bkhd_signature = reader.ReadBytes(4)
if bkhd_signature != b"\x42\x4B\x48\x44": if bkhd_signature != b"\x42\x4B\x48\x44":
raise Exception("not a valid bnk") print(f"[WARNING] invalid bkhd signature at {reader.GetName()}")
return []
bkhd_size = reader.ReadUInt32() bkhd_size = reader.ReadUInt32()
reader.ReadBytes(bkhd_size) reader.ReadBytes(bkhd_size)
if reader.GetBufferPos() == reader.GetStreamLength(): if reader.GetBufferPos() == reader.GetStreamLength():
print(f"[WARNING] empty bnk file at {reader.GetName()}")
return [] # empty bnk return [] # empty bnk
didx_signature = reader.ReadBytes(4) didx_signature = reader.ReadBytes(4)
if didx_signature != b"\x44\x49\x44\x58": if didx_signature != b"\x44\x49\x44\x58":
print(f"[WARNING] invalid didx signature at {reader.GetName()}")
return [] # invalid index signature (hirc block instead ?) return [] # invalid index signature (hirc block instead ?)
didx_size = reader.ReadUInt32() didx_size = reader.ReadUInt32()
@@ -35,6 +38,7 @@ def bnk2wem(data):
data_signature = reader.ReadBytes(4) data_signature = reader.ReadBytes(4)
if data_signature != b"\x44\x41\x54\x41": if data_signature != b"\x44\x41\x54\x41":
print(f"[WARNING] invalid data signature at {reader.GetName()}")
return [] # invalid data signature (missing sector ?) return [] # invalid data signature (missing sector ?)
data_size = reader.ReadUInt32() data_size = reader.ReadUInt32()

View File

@@ -1,5 +1,6 @@
import os import os
import io import io
import json
import wwise import wwise
import tempfile import tempfile
import wavescan import wavescan
@@ -11,7 +12,12 @@ from filereader import FileReader
cwd = os.getcwd() cwd = os.getcwd()
path = lambda *args: os.path.join(*args) path = lambda *args: os.path.join(*args)
call = lambda args: subprocess.call(args, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
def call(args):
try:
subprocess.call(args, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
except Exception as e:
print(f"[WARNING] failed to extract, {e}")
class WwiseExtract: class WwiseExtract:
def __init__(self): def __init__(self):
@@ -154,9 +160,22 @@ class WwiseExtract:
filename = f"{filename} (hdiff)" filename = f"{filename} (hdiff)"
files = [*files[0], *files[1]] files = [*files[0], *files[1]]
# in case of manual use of mapping, use this
# load json here
# handle = open("banks.json", "r")
# banks = json.loads(handle.read())
# handle.close()
for file in files: for file in files:
if mapper is not None: if mapper is not None:
key = mapper.get_key(file[0].split(".")[0]) key = mapper.get_key(file[0].split(".")[0])
# and override the method with a manual dict lookup
# _id = file[0].split(".")[0]
# if _id in list(banks["banks"].keys()):
# key = [banks["banks"][_id], ""]
else: else:
key = None key = None
@@ -170,6 +189,9 @@ class WwiseExtract:
wem_data = data[file_data["offset"]:file_data["offset"]+file_data["size"]] wem_data = data[file_data["offset"]:file_data["offset"]+file_data["size"]]
parsed_wem = wwise.parse_wwise(FileReader(io.BytesIO(wem_data), "little", name=f"{file[3]}:{file[0]}:{file[1]}")) parsed_wem = wwise.parse_wwise(FileReader(io.BytesIO(wem_data), "little", name=f"{file[3]}:{file[0]}:{file[1]}"))
if not parsed_wem:
continue
file_data["metadata"] = parsed_wem file_data["metadata"] = parsed_wem
if key is not None: if key is not None:
@@ -285,7 +307,7 @@ class WwiseExtract:
if os.path.isfile(hdiff_path): if os.path.isfile(hdiff_path):
load_path = hdiff_path load_path = hdiff_path
self.allocator.load_file(load_path) self.allocator.load_file(load_path, source)
# extract every file from this one # extract every file from this one
for file in [file for file in files if file["source"] == source]: for file in [file for file in files if file["source"] == source]:
@@ -371,8 +393,8 @@ class WwiseExtract:
def update_progress(self, current, total, step): def update_progress(self, current, total, step):
base = 100 / self.steps base = 100 / self.steps
self.progress(["total", current * base // total + base * (step - 1)]) self.progress(["total", current * base / total + base * (step - 1)])
self.progress(["file", current * 100 // total]) self.progress(["file", current * 100 / total])
def reset(self): def reset(self):
self.mapper = None self.mapper = None

View File

@@ -11,8 +11,7 @@ class FileReader:
def __init__(self, file, endianness:str, name:str=None): def __init__(self, file, endianness:str, name:str=None):
self.stream = file self.stream = file
self.endianness = endianness self.endianness = endianness
if name: self.name = name
self.name = name
def _read(self, mode:str, bufferLength:int, endianness:str=None, pos:int=None) -> bytes: def _read(self, mode:str, bufferLength:int, endianness:str=None, pos:int=None) -> bytes:
# endianness override # endianness override

135
mapper.py
View File

@@ -15,15 +15,12 @@ class Mapper:
reader.ReadBytes(2) reader.ReadBytes(2)
map_version = reader.ReadBytes(2) map_version = reader.ReadBytes(2)
if map_version == b"\x56\x31": if map_version != b"\x32\x31":
print(f"Warning: you are using an old version of the mapping that is no longer supported, please use a newer one or download an older version of this tool.") print(f"Warning: you are using an unknown / unsupported version of the mapping that is no longer supported, please use a newer one or download an older version of this tool.")
raise Exception("outdated mapping") raise Exception("incompatible mapping")
elif map_version == b"\x56\x32":
self.reader = reader self.reader = reader
self.process_map() self.process_map()
else:
file.close()
raise Exception("invalid mapping version")
def process_map(self): def process_map(self):
reader = self.reader reader = self.reader
@@ -33,15 +30,14 @@ class Mapper:
vl2 = lambda data: int.from_bytes(data, "little") vl2 = lambda data: int.from_bytes(data, "little")
raw = lambda length: rw2(reader.ReadBytes(length)) raw = lambda length: rw2(reader.ReadBytes(length))
rw2 = lambda data: data.rstrip(b"\x00").decode("utf-8") rw2 = lambda data: data.rstrip(b"\x00").decode("utf-8")
n2p = lambda val: [e[0] for e in enumerate(list(bin(val)[2:][::-1])) if e[1] == "1"]
# get map meta # get map meta
reader.ReadBytes(2) reader.ReadBytes(2)
games = { games = {
"ys": "Genshin", "hk4e": "Genshin",
"sr": "Star Rail", "hkrpg": "Star Rail",
"zzz": "Zenless Zone Zero" "nap": "Zenless Zone Zero"
# more later # more later
} }
@@ -54,18 +50,46 @@ class Mapper:
"sfx" "sfx"
] ]
header_size = val(1) # header size # read sectors
block_size = 4 sectors_signature = reader.ReadBytes(9)
header_blocks = [reader.ReadBytes(block_size) for _ in range(header_size // block_size)] if sectors_signature != b"\xFF\x53\x45\x43\x54\x4F\x52\x53\xFF": # ff sectors ff
raise Exception("invalid mapping sectors signature")
n_sectors = val(1)
sectors = {}
for i in range(n_sectors):
name_length = val(1)
name = raw(name_length)
offset = val(4)
size = val(4)
sectors[name] = {
"offset": offset,
"size": size
}
# read config
reader.SetBufferPos(sectors["HEADER"]["offset"])
header_sig = reader.ReadBytes(8) # hardcoded but lazy, this value is for this sector only
n_configs = val(1)
config = {}
for i in range(n_configs):
name = raw(4)
value = raw(5)
config[name] = value
infos = { infos = {
"game": games[rw2(header_blocks[0])], "game": games[config["game"]],
"version": list(rw2(header_blocks[1])), "version": config["verS"],
"coverage": int(rw2(header_blocks[2])), # "coverage": config["covR"],
# more later "useBanksSector": config["bnkS"],
# "bankSectorCoverage": config["bCov"]
} }
print(f"> Loading mapping for {infos['game']} v{infos['version'][0]}.{infos['version'][1]}, this may take a few seconds...") print(f"> Loading mapping for {infos['game']} v{infos['version']}, this may take a few seconds...")
# read prefixes # read prefixes
prefixes = {} prefixes = {}
@@ -77,6 +101,11 @@ class Mapper:
marker = reader.ReadBytes(1) marker = reader.ReadBytes(1)
prefixes[marker] = prefix prefixes[marker] = prefix
# sector jump here
reader.SetBufferPos(sectors["ITEMS"]["offset"])
items_sec_sig = reader.ReadBytes(7) # hardcoded too
# read languages # read languages
langs_offsets = {} langs_offsets = {}
n_langs = reader.ReadUInt8() n_langs = reader.ReadUInt8()
@@ -90,7 +119,7 @@ class Mapper:
# read folders # read folders
folder_offsets = {} folder_offsets = {}
n_folders = reader.ReadUInt8() n_folders = reader.ReadUInt16()
for i in range(n_folders): for i in range(n_folders):
offset = reader.GetBufferPos() offset = reader.GetBufferPos()
@@ -128,42 +157,68 @@ class Mapper:
self.files_offsets = files_offsets self.files_offsets = files_offsets
# read keys # read keys
# GI 3649050 # GI 3649050 (outdated value, use items sector size instead)
keys_data = {} keys_data = {}
n_keys = val(3) n_keys = val(3)
left = reader.GetRemainingLength() left = reader.GetRemainingLength()
if infos["useBanksSector"] == "TRUE":
left -= sectors["BANKS"]["size"]
data = bytearray(reader.ReadBytes(left)) data = bytearray(reader.ReadBytes(left))
keys_data = {rw2(data[i:i+16]): bytes(data[i+16:i+21]) for i in range(0, len(data), 21)} keys_data = {rw2(data[i:i+16]): bytes(data[i+16:i+21]) for i in range(0, len(data), 21)}
self.keys_data = keys_data self.keys_data = keys_data
# read banks sector
bank_keys = {}
if infos["useBanksSector"] == "TRUE":
reader.SetBufferPos(sectors["BANKS"]["offset"])
banks_sec_sig = reader.ReadBytes(7) # hardcoded
global_path_size = val(1)
global_path = raw(global_path_size)
n_bank_keys = val(2)
for i in range(n_bank_keys):
key_length = val(1)
key = raw(key_length)
value_length = val(1)
value = raw(value_length)
bank_keys[key] = f"{global_path}\\{value}"
self.bank_keys = bank_keys
# done # done
print(f"> Finished loading mapping") print(f"> Finished loading mapping")
print(f": {n_langs} supported languages") print(f"=-=-= Voicelines sector =-=-=")
print(f": {n_langs} languages")
print(f": {n_files} mapped files") print(f": {n_files} mapped files")
print(f": {n_keys} available keys") print(f": {n_keys} keys")
print(f"") if infos["useBanksSector"] == "TRUE":
print(f"> Mapping coverage") print(f"=-=-= Music sector =-=-=")
coverage = n2p(infos["coverage"]) print(f": {n_bank_keys} keys")
for val in coverage:
if val%2 == 0:
print(f": partial {coverages[val//2-1]}")
else:
print(f": {coverages[(val-1)//2]}")
def get_key(self, key, lang=False): def get_key(self, key, lang=False):
keys_data = self.keys_data keys_data = self.keys_data
if key not in keys_data.keys(): banks_data = self.bank_keys
return None
key_data = keys_data[key] if key in keys_data.keys():
data = [self.files_offsets[int.from_bytes(key_data[2:], "little")]] key_data = keys_data[key]
data = [self.files_offsets[int.from_bytes(key_data[2:], "little")]]
if lang: if lang:
data.append(self.langs_offsets[int.from_bytes(key_data[:1], "little")]) data.append(self.langs_offsets[int.from_bytes(key_data[:1], "little")])
return data return data
if key in banks_data.keys():
return [banks_data[str(key)], ""]
return None
def reset(self): def reset(self):
self.reader = None self.reader = None

View File

@@ -1,17 +1,19 @@
{"maps": [ {
{ "maps": [
"name": "hk4e.map", {
"game": "Genshin Impact", "name": "hk4e.map",
"version": "5.2" "game": "Genshin Impact",
}, "version": "5.2"
{ },
"name": "hkrpg.map", {
"game": "Star Rail", "name": "hkrpg.map",
"version": "2.6" "game": "Honkai: Star Rail",
}, "version": "2.6"
{ },
"name": "nap.map", {
"game": "Zenless Zone Zero", "name": "nap.map",
"version": "1.2" "game": "Zenless Zone Zero",
} "version": "1.2"
]} }
]
}

91
updater.ui Normal file
View File

@@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>UpdaterWindow</class>
<widget class="QWidget" name="UpdaterWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>100</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>400</width>
<height>100</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>400</width>
<height>100</height>
</size>
</property>
<property name="windowTitle">
<string>Updater</string>
</property>
<widget class="QWidget" name="centralwidget" native="true">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>404</width>
<height>83</height>
</rect>
</property>
<widget class="QLabel" name="title">
<property name="geometry">
<rect>
<x>6</x>
<y>0</y>
<width>391</width>
<height>31</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>16</pointsize>
<weight>75</weight>
<bold>true</bold>
<underline>true</underline>
</font>
</property>
<property name="text">
<string>Updating...</string>
</property>
</widget>
<widget class="QProgressBar" name="progressBar">
<property name="geometry">
<rect>
<x>7</x>
<y>42</y>
<width>381</width>
<height>16</height>
</rect>
</property>
<property name="value">
<number>0</number>
</property>
<property name="textVisible">
<bool>false</bool>
</property>
</widget>
<widget class="QLabel" name="status">
<property name="geometry">
<rect>
<x>16</x>
<y>62</y>
<width>361</width>
<height>21</height>
</rect>
</property>
<property name="text">
<string>Status</string>
</property>
</widget>
</widget>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -1,4 +1,4 @@
{ {
"version": 211, "version": 220,
"mapsVersion": 89 "mapsVersion": 90
} }

View File

@@ -220,7 +220,7 @@ def extract_sector(section_size, is_sounds, is_externals, ext, endianness, lang_
bnk_data = reader.ReadBytes(size) bnk_data = reader.ReadBytes(size)
reader.SetBufferPos(pos) reader.SetBufferPos(pos)
wems = bnk2wem(bnk_data) wems = bnk2wem(bnk_data, f"{filename}@{pos}.{size}")
for wem in wems: for wem in wems:
wwise_data.append([f"{os.path.basename(name).split('.')[0]}_{wem[0]}.wem", offset+wem[1], wem[2], filename]) wwise_data.append([f"{os.path.basename(name).split('.')[0]}_{wem[0]}.wem", offset+wem[1], wem[2], filename])

View File

@@ -23,7 +23,7 @@ def parse_wwise(reader):
if reader.GetStreamLength() == 0: if reader.GetStreamLength() == 0:
print(f"[WARNING] null stream size at {reader.GetName()}, unreadable block") print(f"[WARNING] null stream size at {reader.GetName()}, unreadable block")
return metadata return None
header = reader.ReadBytes(4) header = reader.ReadBytes(4)
@@ -34,7 +34,7 @@ def parse_wwise(reader):
reader.endianness = "little" reader.endianness = "little"
else: else:
print(f"[WARNING] invalid header {header} at {reader.GetName()}, assuming unreadable") print(f"[WARNING] invalid header {header} at {reader.GetName()}, assuming unreadable")
return metadata return None
# additional check # additional check
reader.SetBufferPos(0x08) reader.SetBufferPos(0x08)
@@ -42,7 +42,7 @@ def parse_wwise(reader):
if check != b"WAVE" and check != "XWMA": if check != b"WAVE" and check != "XWMA":
print(f"[WARNING] invalid check mark {check}, assuming unreadable") print(f"[WARNING] invalid check mark {check}, assuming unreadable")
return metadata return None
# read chunks # read chunks
reader.SetBufferPos(0x0C) reader.SetBufferPos(0x0C)
@@ -71,7 +71,7 @@ def parse_wwise(reader):
fmt_length = chunks["fmt"]["length"] fmt_length = chunks["fmt"]["length"]
if fmt_length < 0x10: if fmt_length < 0x10:
print(f"[WARNING] invalid fmt chunk length {fmt_length} at {reader.GetName()}, skipping") print(f"[WARNING] invalid fmt chunk length {fmt_length} at {reader.GetName()}, skipping")
return metadata return None
reader.SetBufferPos(chunks["fmt"]["offset"]) reader.SetBufferPos(chunks["fmt"]["offset"])
@@ -94,7 +94,7 @@ def parse_wwise(reader):
if metadata["format"] == 0x0166: if metadata["format"] == 0x0166:
print(f"[WARNING] XMA2WAVEFORMATEX in fmt at {reader.GetName()}") print(f"[WARNING] XMA2WAVEFORMATEX in fmt at {reader.GetName()}")
return metadata return None
# parse codec # parse codec
codecs = { codecs = {
@@ -122,7 +122,7 @@ def parse_wwise(reader):
if metadata["format"] not in codecs: if metadata["format"] not in codecs:
print(f'[WARNING] unknown codec {metadata["format"]} at {reader.GetName()}') print(f'[WARNING] unknown codec {metadata["format"]} at {reader.GetName()}')
return metadata return None
codec = codecs[metadata["format"]] codec = codecs[metadata["format"]]
@@ -153,23 +153,25 @@ def parse_wwise(reader):
elif metadata["codec"] == "VORBIS": elif metadata["codec"] == "VORBIS":
if (metadata["blockSize"] != 0 or metadata["bitsPerSample"] != 0): if (metadata["blockSize"] != 0 or metadata["bitsPerSample"] != 0):
print(f"[WARNING] worbis type at {reader.GetName()}, skipping") print(f"[WARNING] worbis type at {reader.GetName()}, skipping")
return metadata return None
if "vorb" in chunks: if "vorb" in chunks:
# vorb chunk only in wwise earlier to 2012, therefore impossible for mihoyo games # vorb chunk only in wwise earlier to 2012, therefore impossible for mihoyo games
print(f"[WARNING] found vorb chunk at {reader.GetName()}, is this the correct game ?") print(f"[WARNING] found vorb chunk at {reader.GetName()}, is this the correct game ?")
return metadata return None
extra_offset = chunks["fmt"]["offset"] + 0x18 extra_offset = chunks["fmt"]["offset"] + 0x18
if metadata["extraSize"] != 0x30: if metadata["extraSize"] != 0x30:
print(f"[WARNING] unknown extra wwise size at {reader.GetName()}, skipping") print(f"[WARNING] unknown extra wwise size at {reader.GetName()}, skipping")
return metadata return None
data_offset = 0x10 data_offset = 0x10
blocks_offset = 0x28 blocks_offset = 0x28
# define header to type 2, packet to modified and codebook to aoTuV603, required ? # define header to type 2, packet to modified and codebook to aoTuV603, required ?
# this somehow breaks and don't read correctly, why :c
# stream_size * 8 * sample_rate / num_samples = bitrate * 1000
metadata["numSamples"] = reader.ReadInt32(extra_offset) metadata["numSamples"] = reader.ReadInt32(extra_offset)
setup_offset = reader.ReadUInt32(extra_offset + data_offset) setup_offset = reader.ReadUInt32(extra_offset + data_offset)
audio_offset = reader.ReadUInt32(extra_offset + data_offset + 0x04) audio_offset = reader.ReadUInt32(extra_offset + data_offset + 0x04)