diff --git a/app.py b/app.py
index d18fbec..ce266f8 100644
--- a/app.py
+++ b/app.py
@@ -4,11 +4,12 @@ import json
import math
import extract
import platform
+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
+from PyQt5.QtWidgets import QMessageBox, QMainWindow, QApplication, QFileDialog, QHeaderView, QAbstractItemView, QTreeWidgetItem, QAction, QActionGroup
QMetaType.type("QTextCursor")
@@ -72,71 +73,114 @@ class AnimeWwise(QMainWindow):
def __init__(self):
super(AnimeWwise, self).__init__()
uic.loadUi("gui.ui", self)
- self.maps = self.getMaps()
+ self.maps = self.getJson("maps/index")
+ self.setWindowTitle(f'AnimeWwise | v{".".join(list(str(self.getJson("version")["version"])))}')
self.folders = {
"input": "",
"output": "",
"diff": ""
}
+ self.format = "wav"
+ self.fileStructure = {"folders": {}, "files": []}
self.setupActions()
- sys.stdout = TextEditStream(self.console)
+ # sys.stdout = TextEditStream(self.console)
self.extract = extract.WwiseExtract()
- self.checkMapsUpdates()
+ self.checkUpdates()
# utils
self.selectFolder = lambda: QFileDialog.getExistingDirectory(self, "Select Folder")
- def checkMapsUpdates(self):
- print("Checking updates")
+ def checkUpdates(self):
+ print("Checking for updates...")
try:
- getVersion = lambda m: sum([int(e["version"].replace(".", "")) for e in m["maps"]])
- mapsVersion = getVersion(self.maps)
- latestMaps = get("https://raw.githubusercontent.com/Escartem/AnimeWwise/master/maps/index.json")
+ currentVersion = self.getJson("version")
+ latestVersionReq = get("https://raw.githubusercontent.com/Escartem/AnimeWwise/master/version.json")
- if latestMaps.status_code == 200:
- latestVersion = getVersion(json.loads(latestMaps.text))
+ if latestVersionReq.status_code == 200:
+ latestVersion = json.loads(latestMaps.text)
- if mapsVersion < latestVersion:
- print("Update found")
- QMessageBox.information(None, "Info", "Newer version of the mappings are availble, please update the program", QMessageBox.Ok)
+ if currentVersion["version"] < latestVersion["version"]:
+ print("Update found !")
+ QMessageBox.information(None, "Info", "Newer version of the program is availble, please update.", 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)
else:
print("No updates")
except:
- print("Failed to check updates")
+ print("Failed to check updates :(")
- def getMaps(self):
- with open("maps/index.json", "r") as f:
- maps = json.loads(f.read())
+ def getJson(self, path):
+ with open(f"{path}.json", "r") as f:
+ data = json.loads(f.read())
f.close()
- return maps
+ return data
- def setFolder(self, elem, folder):
+ def setFolder(self, elem=None, folder=None):
path = self.selectFolder()
self.folders[folder] = path
- elem.setText(path)
+ if elem:
+ elem.setText(path)
def setupActions(self):
self.changeInput.clicked.connect(lambda: self.setFolder(self.inputPath, "input"))
self.changeAltInput.clicked.connect(lambda: self.setFolder(self.altInputPath, "diff"))
- self.changeOutput.clicked.connect(lambda: self.setFolder(self.outputPath, "output"))
- self.outputFormat.addItems(["wem (fastest)", "wav (fast)", "mp3 (slow)", "ogg (slow)"])
self.assetMap.addItems(["No map", *[f'{e["game"]} - v{e["version"]}' for e in self.maps["maps"]]])
- self.tabs.setTabEnabled(1, False)
- self.tabs.setTabEnabled(2, False)
+ self.setExtractionState(False)
+
+ self.updateTreeWidget(self.fileStructure)
self.loadFilesButton.clicked.connect(lambda: self.loadFiles())
self.actionReset.triggered.connect(lambda: self.resetApp())
self.actionExit.triggered.connect(lambda: self.close())
- self.extractSelected.clicked.connect(lambda: self.extractItems(False))
- self.extractAll.clicked.connect(lambda: self.extractItems(True))
+ self.actionExtract_Selected.triggered.connect(lambda: self.extractItems(False))
+ self.actionExtract_All.triggered.connect(lambda: self.extractItems(True))
+
+ self.actionReport_a_bug.triggered.connect(lambda: self.openLink(0))
+ self.actionSource_code.triggered.connect(lambda: self.openLink(1))
+ self.actionDiscord.triggered.connect(lambda: self.openLink(2))
self.searchAsset.textChanged.connect(lambda: self.filterAsset())
+ # output format
+ formats = ["wem (fastest)", "wav (fast)", "mp3 (slow)", "ogg (slow)"]
+ action_group = QActionGroup(self)
+ action_group.setExclusive(True)
+
+ for index, item_name in enumerate(formats):
+ action = QAction(item_name, self)
+ action.setCheckable(True)
+ if index == 1:
+ action.setChecked(True)
+ self.menuOutput_format.addAction(action)
+ action_group.addAction(action)
+
+ action_group.triggered.connect(self.updateFormat)
+
+ def updateFormat(self, event):
+ text = event.text()
+ self.format = text.split(" ")[0]
+
+ def openLink(self, id):
+ urls = [
+ "https://github.com/Escartem/AnimeWwise/issues/new",
+ "https://github.com/Escartem/AnimeWwise",
+ "https://discord.gg/fzRdtVh"
+ ]
+
+ webbrowser.open(urls[id])
+
+ def setExtractionState(self, state):
+ self.actionExtract_Selected.setEnabled(state)
+ self.actionExtract_All.setEnabled(state)
+ self.actionExpand_all.setEnabled(state)
+ self.actionCollapse_all.setEnabled(state)
+
# workers
@pyqtSlot(list)
def progressBarSlot(self, progress):
@@ -152,23 +196,16 @@ class AnimeWwise(QMainWindow):
if data["action"] == "load":
self.fileStructure = data["content"]
self.updateTreeWidget(self.fileStructure)
- self.tabs.setTabEnabled(0, False)
- self.tabs.setTabEnabled(1, True)
- self.tabs.setTabEnabled(2, True)
+ self.setExtractionState(True)
self.tabs.setCurrentIndex(1)
print("Done !")
if data["action"] == "error":
QMessageBox.warning(None, "Warning", data["content"]["msg"], QMessageBox.Ok)
state = data["content"]["state"]
- if state == 1:
- self.tabs.setTabEnabled(0, True)
- elif state == 2:
- self.tabs.setTabEnabled(1, True)
- self.tabs.setTabEnabled(2, True)
+ if state == 2:
+ self.setExtractionState(True)
if data["action"] == "extract":
- self.tabs.setTabEnabled(1, True)
- self.tabs.setTabEnabled(2, True)
- self.tabs.setCurrentIndex(2)
+ self.setExtractionState(True)
print("Finished extracting everything !")
if platform.system() == "Windows":
@@ -186,7 +223,6 @@ class AnimeWwise(QMainWindow):
else:
_map = None
- self.tabs.setTabEnabled(0, False)
self.resetTreeWidget()
# why is all this required for threading damnit
@@ -225,28 +261,36 @@ class AnimeWwise(QMainWindow):
def resetTreeWidget(self):
self.treeWidget.clear()
- self.tabs.setTabEnabled(1, False)
+ self.fileStructure = {"folders": {}, "files": []}
+ self.setExtractionState(False)
def updateTreeWidget(self, structure):
self.treeWidget.clear()
- self.treeWidget.setColumnCount(3)
- self.treeWidget.setHeaderLabels(["Name", "Offset", "Size", "Source"])
+ self.treeWidget.setColumnCount(4)
+ self.treeWidget.setHeaderLabels(["Name", "Duration", "Source", "Size", "Offset"])
self.addItems(None, structure)
self.treeWidget.expandAll()
- self.treeWidget.header().setSectionResizeMode(0, QHeaderView.Stretch)
- self.treeWidget.header().setSectionResizeMode(1, QHeaderView.ResizeToContents)
- self.treeWidget.header().setSectionResizeMode(2, QHeaderView.ResizeToContents)
+
+ self.treeWidget.header().setSectionResizeMode(0, QHeaderView.ResizeToContents)
+ self.treeWidget.header().setSectionResizeMode(1, QHeaderView.Stretch)
+ self.treeWidget.header().setSectionResizeMode(2, QHeaderView.Stretch)
+ self.treeWidget.header().setSectionResizeMode(3, QHeaderView.Stretch)
+
self.treeWidget.setHeaderHidden(False)
self.treeWidget.setEditTriggers(QAbstractItemView.NoEditTriggers)
self.treeWidget.setDragDropMode(QAbstractItemView.NoDragDrop)
+ self.treeWidget.itemClicked.connect(self.updateAudioPreview)
+
+ def updateAudioPreview(self, item, column):
+ print(item.text(0))
def addItems(self, parent, element):
for folder_name in sorted(element.get("folders", {}).keys()):
folder_content = element["folders"][folder_name]
- folder_item = QTreeWidgetItem([folder_name, "", "", ""])
+ folder_item = QTreeWidgetItem([folder_name, "", "", "", ""])
folder_item.setFlags(folder_item.flags() | Qt.ItemIsTristate | Qt.ItemIsUserCheckable)
folder_item.setCheckState(0, Qt.Unchecked)
if parent is None:
@@ -256,7 +300,8 @@ class AnimeWwise(QMainWindow):
self.addItems(folder_item, folder_content)
for file in sorted(element.get("files", [])):
- file_item = QTreeWidgetItem([str(file[0]), str(hex(file[1])), str(file[2]), str(file[3])])
+ file_meta = file[1]
+ file_item = QTreeWidgetItem([file[0], f'{round(file_meta["metadata"]["duration"], 2)} seconds', file_meta["source"], str(file_meta["size"]), str(hex(file_meta["offset"]))])
file_item.setFlags(file_item.flags() | Qt.ItemIsUserCheckable)
file_item.setCheckState(0, Qt.Unchecked)
if parent is None:
@@ -266,12 +311,15 @@ class AnimeWwise(QMainWindow):
# page 3 - extraction
def extractItems(self, _all):
+ self.setFolder(folder="output")
+
if self.folders["output"] == "":
QMessageBox.warning(None, "Warning", "Missing output folder !", QMessageBox.Ok)
return
checked_items = []
+ # todo: use file structure instead of tree view
def check_items(item, _all):
if item.checkState(0) == Qt.Checked or _all:
if item.text(1) != "":
@@ -282,13 +330,11 @@ class AnimeWwise(QMainWindow):
for i in range(self.treeWidget.topLevelItemCount()):
check_items(self.treeWidget.topLevelItem(i), _all)
- self.tabs.setTabEnabled(1, False)
- self.tabs.setTabEnabled(2, False)
- self.tabs.setCurrentIndex(2)
+ self.setExtractionState(False)
# yet another block of threading bs
self.backgroundThread = QThread()
- self.backgroundWorker = BackgroundWorker("extract", self.extract, {"input": self.folders["input"], "files": checked_items, "format": self.outputFormat.currentText()[:3], "output": self.folders["output"]})
+ self.backgroundWorker = BackgroundWorker("extract", self.extract, {"input": self.folders["input"], "files": checked_items, "format": self.format, "output": self.folders["output"]})
self.backgroundWorker.moveToThread(self.backgroundThread)
self.backgroundThread.started.connect(self.backgroundWorker.run)
self.backgroundWorker.finished.connect(self.handleFinished)
@@ -310,18 +356,16 @@ class AnimeWwise(QMainWindow):
return {
"name": item.text(0),
"path": path[:-1] if path[0] in ["changed_files", "new_files"] else path[1:-1],
- "source": item.text(3),
- "offset": int(item.text(1), 16),
- "size": int(item.text(2))
+ "source": item.text(2),
+ "offset": int(item.text(4), 16),
+ "size": int(item.text(3))
}
# misc
def resetApp(self):
self.resetTreeWidget()
self.extract.reset()
- self.tabs.setTabEnabled(0, True)
- self.tabs.setTabEnabled(1, False)
- self.tabs.setTabEnabled(2, False)
+ self.setExtractionState(False)
print("Reset !")
def _appendText(self, text):
diff --git a/extract.py b/extract.py
index d7b4bd0..b9b8613 100644
--- a/extract.py
+++ b/extract.py
@@ -1,9 +1,10 @@
import os
import io
+import wwise
import tempfile
import wavescan
-import subprocess
import platform
+import subprocess
from mapper import Mapper
from allocator import Allocator
from filereader import FileReader
@@ -62,7 +63,7 @@ class WwiseExtract:
hdiff_files = self.get_hdiff_files(data, hdiff_data, filename)
files = self.compare_diff(files, hdiff_files)
- self.map_names(files, filename, hdiff is not None)
+ self.map_names(files, filename, hdiff is not None, data)
def compare_diff(self, old, new):
old_dict = {file[0]:file[2] for file in old}
@@ -112,7 +113,7 @@ class WwiseExtract:
return files
- def map_names(self, files, filename, hdiff=False, skip_source=True):
+ def map_names(self, files, filename, hdiff=False, data=None, skip_source=True):
# disable skip source if required
mapper = self.mapper
base = self.file_structure
@@ -128,6 +129,18 @@ class WwiseExtract:
else:
key = None
+ file_data = {
+ "source": file[3],
+ "size": file[2],
+ "offset": file[1],
+ "metadata": {}
+ }
+
+ wem_data = data[file_data["offset"]:file_data["offset"]+file_data["size"]]
+ parsed_wem = wwise.parse_wwise(FileReader(io.BytesIO(wem_data), "little"))
+
+ file_data["metadata"] = parsed_wem
+
if key is not None:
if hdiff:
if file in old_files[0]:
@@ -139,7 +152,7 @@ class WwiseExtract:
if skip_source:
parts = parts[1:]
- self.add_to_structure(parts, [file[1], file[2], file[3]])
+ self.add_to_structure(parts, file_data)
else:
temp = base["folders"]
@@ -161,7 +174,7 @@ class WwiseExtract:
if "unmapped" not in temp:
temp["unmapped"] = {"folders": {}, "files": []}
- temp["unmapped"]["files"].append(file)
+ temp["unmapped"]["files"].append([file[0], file_data])
self.file_structure = base
@@ -175,7 +188,7 @@ class WwiseExtract:
current_level = current_level["folders"][part]
if "files" not in current_level:
current_level["files"] = []
- current_level["files"].append([parts[-1], meta[0], meta[1], meta[2]])
+ current_level["files"].append([parts[-1], meta])
### extracting files ###
diff --git a/gui.ui b/gui.ui
index 5b633ec..6e641bb 100644
--- a/gui.ui
+++ b/gui.ui
@@ -5,24 +5,27 @@
Qt::NonModal
+
+ true
+
0
0
1100
- 800
+ 900
1100
- 800
+ 900
1100
- 800
+ 900
@@ -39,7 +42,7 @@
- 1
+ 0
true
@@ -210,7 +213,7 @@
0
20
1081
- 591
+ 551
@@ -235,135 +238,49 @@
Search something...
-
-
-
- Extract
-
-
+
- 9
- 9
- 1061
- 601
+ 0
+ 580
+ 1081
+ 31
-
+
-
-
-
-
-
-
- Output folder
-
-
-
- -
-
-
- true
-
-
-
- -
-
-
- Select
-
-
-
-
-
- -
-
-
-
-
-
- Output format
-
-
-
- -
-
-
-
-
-
-
-
-
- -
-
-
- Qt::Horizontal
+
+
+ sample_long_name.wem | Duration : 00:00 | Stereo (48000Hz)
-
-
-
-
-
-
-
-
-
- Total progress
-
-
-
- -
-
-
- 0
-
-
-
-
-
- -
-
-
-
-
-
- Per file progress
-
-
-
- -
-
-
- 0
-
-
-
-
-
-
-
- -
-
-
- Qt::Horizontal
+
+
+
-
-
-
-
-
-
- Extract All
-
-
-
- -
-
-
- Extract Selected
-
-
-
-
+
+
+ false
+
+
+ Play
+
+
+
+ -
+
+
+ false
+
+
+ Stop
+
+
@@ -373,9 +290,9 @@
10
- 640
+ 720
1081
- 131
+ 151
@@ -394,6 +311,54 @@
true
+
+
+
+ 16
+ 650
+ 1071
+ 61
+
+
+
+ -
+
+
-
+
+
+ Total progress
+
+
+
+ -
+
+
+ 0
+
+
+
+
+
+ -
+
+
-
+
+
+ Per file progress
+
+
+
+ -
+
+
+ 0
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -439,6 +437,64 @@
Exit
+
+
+ false
+
+
+ Extract All
+
+
+
+
+ false
+
+
+ Extract Selected
+
+
+
+
+ Report a bug
+
+
+
+
+ Source code
+
+
+
+
+ Discord
+
+
+
+
+ false
+
+
+ format
+
+
+ false
+
+
+
+
+ false
+
+
+ Expand all
+
+
+
+
+ false
+
+
+ Collapse all
+
+
inputPath
diff --git a/version.json b/version.json
new file mode 100644
index 0000000..8e94654
--- /dev/null
+++ b/version.json
@@ -0,0 +1,4 @@
+{
+ "version": 21,
+ "mapsVersion": 89
+}
\ No newline at end of file
diff --git a/wwise.py b/wwise.py
new file mode 100644
index 0000000..056388e
--- /dev/null
+++ b/wwise.py
@@ -0,0 +1,127 @@
+# wwise riff header parser
+# thanks to hcs and bnnm
+
+def parse_wwise(reader):
+ header = reader.ReadBytes(4)
+
+ # endian check header
+ if header == b"RIFX":
+ reader.endianness = "big"
+ elif header == b"RIFF":
+ reader.endianness = "little"
+ else:
+ raise Exception("invalid header")
+
+ # additional check
+ reader.SetBufferPos(0x08)
+ check = reader.ReadBytes(4)
+
+ if check != b"WAVE" and check != "XWMA":
+ raise Exception("invalid file")
+
+ # read chunks
+ reader.SetBufferPos(0x0C)
+
+ chunks = {}
+
+ while reader.GetBufferPos() < reader.GetStreamLength():
+ # relevants chunks types
+ # "fmt "
+ # "data"
+ # "JUNK"
+
+ chunk_type = reader.ReadBytes(4)
+
+ if chunk_type not in [b"fmt ", b"JUNK", b"data"]:
+ raise Exception("invalid chunk")
+
+ formatted_chunk_type = chunk_type.decode("utf-8").replace(" ", "")
+ chunk_length = reader.ReadUInt32()
+ chunks[formatted_chunk_type] = {
+ "length": chunk_length,
+ "offset": reader.GetBufferPos(),
+ "data": reader.ReadBytes(chunk_length)
+ }
+
+ # reader fmt header
+ if chunks["fmt"]["length"] < 0x10:
+ raise Exception("invalid fmt chunk length")
+
+ reader.SetBufferPos(chunks["fmt"]["offset"])
+
+ metadata = {
+ "format": reader.ReadUInt16(),
+ "channels": reader.ReadUInt16(),
+ "sampleRate": reader.ReadUInt32(),
+ "avgBitrate": reader.ReadUInt32(),
+ "blockSize": reader.ReadUInt16(),
+ "bitsPerSample": reader.ReadUInt16(),
+ "extraSize": 0,
+ "channelLayout": None,
+ "channelType": None,
+ "codec": None,
+ "layoutType": None,
+ "interleaveBlockSize": None,
+ "numSamples": None,
+ "duration": None
+ }
+
+ if chunks["fmt"]["length"] > 0x10 and metadata["format"] != 0x0165 and metadata["format"] != 0x0166:
+ metadata["extraSize"] = reader.ReadUInt16()
+
+ if metadata["extraSize"] >= 0x06:
+ metadata["channelLayout"] = reader.ReadUInt32()
+
+ if metadata["channelLayout"] & 0xFF == metadata["channels"]:
+ metadata["channelType"] = (metadata["channelLayout"] >> 8) & 0x0F
+ metadata["channelLayout"] = metadata["channelLayout"] >> 12
+
+ if metadata["format"] == 0x0166:
+ raise Exception("XMA2WAVEFORMATEX in fmt")
+
+ # parse codec
+ codecs = {
+ 0x0001: "PCM",
+ 0x0002: "IMA",
+ 0x0069: "IMA",
+ 0x0161: "XWLA",
+ 0x0162: "XWMA",
+ 0x0165: "XMA2",
+ 0x0166: "XMA2",
+ 0xAAC0: "AAC",
+ 0xFFF0: "DSP",
+ 0xFFFB: "HEVAG",
+ 0xFFFC: "ATRAC9",
+ 0xFFFE: "PCM",
+ 0xFFFF: "VORBIS",
+ 0x3039: "OPUSNX",
+ 0x3040: "OPUS",
+ 0x3041: "OPUSWW",
+ 0x8311: "PTADPCM"
+ }
+
+ # genshin should be PTADPCM
+ # hsr and zzz should be VORBIS
+
+ if metadata["format"] not in codecs:
+ raise Exception("unknown codec")
+
+ codec = codecs[metadata["format"]]
+
+ if codec not in ["PTADPCM", "VORBIS"]: # Platinum "PtADPCM" custom ADPCM for Wwise
+ raise Exception(f"unhandled codec -> {codec}")
+
+ metadata["codec"] = codec
+
+ # parse more infos
+ if metadata["codec"] == "PTADPCM":
+ metadata["layoutType"] = "interleave"
+ metadata["interleaveBlockSize"] = metadata["blockSize"] // metadata["channels"]
+
+ metadata["numSamples"] = int((chunks["data"]["length"] / (metadata["channels"] * metadata["interleaveBlockSize"])) * (2 + (metadata["interleaveBlockSize"] - 0x05) * 2))
+ metadata["duration"] = metadata["numSamples"] / metadata["sampleRate"]
+
+ return metadata
+
+ # TODO: parse VORBIS
+ # TODO: rewrite codec ?