1
0
mirror of https://github.com/Escartem/AnimeWwise.git synced 2026-06-05 16:00:27 +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.
![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
@@ -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`
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)
![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]
> The audio folder can be found in the following locations
> - `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
6. Select a mapping
> [!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.
![image](https://github.com/user-attachments/assets/9714b6ab-527a-49d9-ae98-354d1979a2b9)
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
![image](https://github.com/user-attachments/assets/73e2ece9-9fa7-4149-8674-762adb1ef50c)
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]
> 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 !
@@ -42,3 +42,13 @@ The program has been tested and proved to be very efficient with extraction (not
# 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
# 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):
self.files = {}
def load_file(self, path):
filename = os.path.basename(path)
def load_file(self, path, name):
with open(path, "r+b") as f:
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):
self.files[os.path.basename(name)].close()
self.files[name].close()
def read_at(self, file, offset, size):
mmap_object = self.files[os.path.basename(file)]
mmap_object = self.files[file]
mmap_object.seek(offset)
data = mmap_object.read(size)
return data
def free_mem(self):
for file in list(self.files.keys()):
self.files[os.path.basename(file)].close()
self.files[file].close()
self.files.clear()

125
app.py
View File

@@ -2,14 +2,16 @@ import os
import sys
import json
import math
import time
import extract
import platform
import urllib
import webbrowser
from PyQt5 import uic
from requests import get
from PyQt5.QtGui import QTextCursor
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")
@@ -70,6 +72,111 @@ class BackgroundWorker(QObject):
self.extract.extract_files(self.input, self.files, self.output, self.format, progress=self.progress.emit)
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):
def __init__(self):
super(AnimeWwise, self).__init__()
@@ -87,6 +194,8 @@ class AnimeWwise(QMainWindow):
sys.stdout = TextEditStream(self.console)
self.extract = extract.WwiseExtract()
self.checkUpdates()
self.totalProgress.setMaximum(10000)
self.fileProgress.setMaximum(10000)
# utils
self.selectFolder = lambda: QFileDialog.getExistingDirectory(self, "Select Folder")
@@ -102,10 +211,12 @@ class AnimeWwise(QMainWindow):
if currentVersion["version"] < latestVersion["version"]:
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"]:
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:
print("No updates")
except:
@@ -210,10 +321,13 @@ class AnimeWwise(QMainWindow):
# workers
@pyqtSlot(list)
def progressBarSlot(self, progress):
progress_value = math.ceil(progress[1]*100)
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":
self.fileProgress.setValue(math.ceil(progress[1]))
self.fileProgress.setValue(progress_value)
self.fileProgress.setFormat("%.02f %%" % (progress_value / 100))
@pyqtSlot(dict)
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")]
elif self.loadType == "file":
path = QFileDialog.getOpenFileName(self, "Select .pck File", "", "PCK Files (*.pck)", options=QFileDialog.Options())
self.folders["input"] = os.path.dirname(path[0])
files = [path[0]]
if len(files) == 0 or files[0] == "":

10
bnk.py
View File

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

View File

@@ -1,5 +1,6 @@
import os
import io
import json
import wwise
import tempfile
import wavescan
@@ -11,7 +12,12 @@ from filereader import FileReader
cwd = os.getcwd()
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:
def __init__(self):
@@ -154,9 +160,22 @@ class WwiseExtract:
filename = f"{filename} (hdiff)"
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:
if mapper is not None:
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:
key = None
@@ -170,6 +189,9 @@ class WwiseExtract:
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]}"))
if not parsed_wem:
continue
file_data["metadata"] = parsed_wem
if key is not None:
@@ -285,7 +307,7 @@ class WwiseExtract:
if os.path.isfile(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
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):
base = 100 / self.steps
self.progress(["total", current * base // total + base * (step - 1)])
self.progress(["file", current * 100 // total])
self.progress(["total", current * base / total + base * (step - 1)])
self.progress(["file", current * 100 / total])
def reset(self):
self.mapper = None

View File

@@ -11,8 +11,7 @@ class FileReader:
def __init__(self, file, endianness:str, name:str=None):
self.stream = file
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:
# endianness override

135
mapper.py
View File

@@ -15,15 +15,12 @@ class Mapper:
reader.ReadBytes(2)
map_version = reader.ReadBytes(2)
if map_version == b"\x56\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.")
raise Exception("outdated mapping")
elif map_version == b"\x56\x32":
self.reader = reader
self.process_map()
else:
file.close()
raise Exception("invalid mapping version")
if map_version != b"\x32\x31":
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("incompatible mapping")
self.reader = reader
self.process_map()
def process_map(self):
reader = self.reader
@@ -33,15 +30,14 @@ class Mapper:
vl2 = lambda data: int.from_bytes(data, "little")
raw = lambda length: rw2(reader.ReadBytes(length))
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
reader.ReadBytes(2)
games = {
"ys": "Genshin",
"sr": "Star Rail",
"zzz": "Zenless Zone Zero"
"hk4e": "Genshin",
"hkrpg": "Star Rail",
"nap": "Zenless Zone Zero"
# more later
}
@@ -54,18 +50,46 @@ class Mapper:
"sfx"
]
header_size = val(1) # header size
block_size = 4
header_blocks = [reader.ReadBytes(block_size) for _ in range(header_size // block_size)]
# read sectors
sectors_signature = reader.ReadBytes(9)
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 = {
"game": games[rw2(header_blocks[0])],
"version": list(rw2(header_blocks[1])),
"coverage": int(rw2(header_blocks[2])),
# more later
"game": games[config["game"]],
"version": config["verS"],
# "coverage": config["covR"],
"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
prefixes = {}
@@ -77,6 +101,11 @@ class Mapper:
marker = reader.ReadBytes(1)
prefixes[marker] = prefix
# sector jump here
reader.SetBufferPos(sectors["ITEMS"]["offset"])
items_sec_sig = reader.ReadBytes(7) # hardcoded too
# read languages
langs_offsets = {}
n_langs = reader.ReadUInt8()
@@ -90,7 +119,7 @@ class Mapper:
# read folders
folder_offsets = {}
n_folders = reader.ReadUInt8()
n_folders = reader.ReadUInt16()
for i in range(n_folders):
offset = reader.GetBufferPos()
@@ -128,42 +157,68 @@ class Mapper:
self.files_offsets = files_offsets
# read keys
# GI 3649050
# GI 3649050 (outdated value, use items sector size instead)
keys_data = {}
n_keys = val(3)
left = reader.GetRemainingLength()
if infos["useBanksSector"] == "TRUE":
left -= sectors["BANKS"]["size"]
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)}
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
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_keys} available keys")
print(f"")
print(f"> Mapping coverage")
coverage = n2p(infos["coverage"])
for val in coverage:
if val%2 == 0:
print(f": partial {coverages[val//2-1]}")
else:
print(f": {coverages[(val-1)//2]}")
print(f": {n_keys} keys")
if infos["useBanksSector"] == "TRUE":
print(f"=-=-= Music sector =-=-=")
print(f": {n_bank_keys} keys")
def get_key(self, key, lang=False):
keys_data = self.keys_data
if key not in keys_data.keys():
return None
banks_data = self.bank_keys
key_data = keys_data[key]
data = [self.files_offsets[int.from_bytes(key_data[2:], "little")]]
if key in keys_data.keys():
key_data = keys_data[key]
data = [self.files_offsets[int.from_bytes(key_data[2:], "little")]]
if lang:
data.append(self.langs_offsets[int.from_bytes(key_data[:1], "little")])
if lang:
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):
self.reader = None

View File

@@ -1,17 +1,19 @@
{"maps": [
{
"name": "hk4e.map",
"game": "Genshin Impact",
"version": "5.2"
},
{
"name": "hkrpg.map",
"game": "Star Rail",
"version": "2.6"
},
{
"name": "nap.map",
"game": "Zenless Zone Zero",
"version": "1.2"
}
]}
{
"maps": [
{
"name": "hk4e.map",
"game": "Genshin Impact",
"version": "5.2"
},
{
"name": "hkrpg.map",
"game": "Honkai: Star Rail",
"version": "2.6"
},
{
"name": "nap.map",
"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,
"mapsVersion": 89
"version": 220,
"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)
reader.SetBufferPos(pos)
wems = bnk2wem(bnk_data)
wems = bnk2wem(bnk_data, f"{filename}@{pos}.{size}")
for wem in wems:
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:
print(f"[WARNING] null stream size at {reader.GetName()}, unreadable block")
return metadata
return None
header = reader.ReadBytes(4)
@@ -34,7 +34,7 @@ def parse_wwise(reader):
reader.endianness = "little"
else:
print(f"[WARNING] invalid header {header} at {reader.GetName()}, assuming unreadable")
return metadata
return None
# additional check
reader.SetBufferPos(0x08)
@@ -42,7 +42,7 @@ def parse_wwise(reader):
if check != b"WAVE" and check != "XWMA":
print(f"[WARNING] invalid check mark {check}, assuming unreadable")
return metadata
return None
# read chunks
reader.SetBufferPos(0x0C)
@@ -71,7 +71,7 @@ def parse_wwise(reader):
fmt_length = chunks["fmt"]["length"]
if fmt_length < 0x10:
print(f"[WARNING] invalid fmt chunk length {fmt_length} at {reader.GetName()}, skipping")
return metadata
return None
reader.SetBufferPos(chunks["fmt"]["offset"])
@@ -94,7 +94,7 @@ def parse_wwise(reader):
if metadata["format"] == 0x0166:
print(f"[WARNING] XMA2WAVEFORMATEX in fmt at {reader.GetName()}")
return metadata
return None
# parse codec
codecs = {
@@ -122,7 +122,7 @@ def parse_wwise(reader):
if metadata["format"] not in codecs:
print(f'[WARNING] unknown codec {metadata["format"]} at {reader.GetName()}')
return metadata
return None
codec = codecs[metadata["format"]]
@@ -153,23 +153,25 @@ def parse_wwise(reader):
elif metadata["codec"] == "VORBIS":
if (metadata["blockSize"] != 0 or metadata["bitsPerSample"] != 0):
print(f"[WARNING] worbis type at {reader.GetName()}, skipping")
return metadata
return None
if "vorb" in chunks:
# 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 ?")
return metadata
return None
extra_offset = chunks["fmt"]["offset"] + 0x18
if metadata["extraSize"] != 0x30:
print(f"[WARNING] unknown extra wwise size at {reader.GetName()}, skipping")
return metadata
return None
data_offset = 0x10
blocks_offset = 0x28
# 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)
setup_offset = reader.ReadUInt32(extra_offset + data_offset)
audio_offset = reader.ReadUInt32(extra_offset + data_offset + 0x04)