mirror of
https://github.com/Escartem/AnimeWwise.git
synced 2026-06-11 20:20:25 +08:00
20
README.md
20
README.md
@@ -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.
|
||||||
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
# 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)
|
||||||

|

|
||||||
> [!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.
|
||||||

|

|
||||||
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
|
||||||
|
|||||||
11
allocator.py
11
allocator.py
@@ -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
125
app.py
@@ -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
10
bnk.py
@@ -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()
|
||||||
|
|||||||
30
extract.py
30
extract.py
@@ -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
|
||||||
|
|||||||
@@ -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
135
mapper.py
@@ -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
|
||||||
|
|||||||
@@ -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
91
updater.ui
Normal 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>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"version": 211,
|
"version": 220,
|
||||||
"mapsVersion": 89
|
"mapsVersion": 90
|
||||||
}
|
}
|
||||||
@@ -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])
|
||||||
|
|||||||
20
wwise.py
20
wwise.py
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user