1
0
mirror of https://github.com/Escartem/AnimeWwise.git synced 2026-06-12 04:42:12 +08:00

begin new version

This commit is contained in:
Escartem
2024-11-03 22:02:16 +01:00
parent 358648f716
commit da8ee97264
5 changed files with 430 additions and 186 deletions

158
app.py
View File

@@ -4,11 +4,12 @@ import json
import math import math
import extract import extract
import platform import platform
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 from PyQt5.QtWidgets import QMessageBox, QMainWindow, QApplication, QFileDialog, QHeaderView, QAbstractItemView, QTreeWidgetItem, QAction, QActionGroup
QMetaType.type("QTextCursor") QMetaType.type("QTextCursor")
@@ -72,71 +73,114 @@ class AnimeWwise(QMainWindow):
def __init__(self): def __init__(self):
super(AnimeWwise, self).__init__() super(AnimeWwise, self).__init__()
uic.loadUi("gui.ui", self) 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 = { self.folders = {
"input": "", "input": "",
"output": "", "output": "",
"diff": "" "diff": ""
} }
self.format = "wav"
self.fileStructure = {"folders": {}, "files": []}
self.setupActions() self.setupActions()
sys.stdout = TextEditStream(self.console) # sys.stdout = TextEditStream(self.console)
self.extract = extract.WwiseExtract() self.extract = extract.WwiseExtract()
self.checkMapsUpdates() self.checkUpdates()
# utils # utils
self.selectFolder = lambda: QFileDialog.getExistingDirectory(self, "Select Folder") self.selectFolder = lambda: QFileDialog.getExistingDirectory(self, "Select Folder")
def checkMapsUpdates(self): def checkUpdates(self):
print("Checking updates") print("Checking for updates...")
try: try:
getVersion = lambda m: sum([int(e["version"].replace(".", "")) for e in m["maps"]]) currentVersion = self.getJson("version")
mapsVersion = getVersion(self.maps) latestVersionReq = get("https://raw.githubusercontent.com/Escartem/AnimeWwise/master/version.json")
latestMaps = get("https://raw.githubusercontent.com/Escartem/AnimeWwise/master/maps/index.json")
if latestMaps.status_code == 200: if latestVersionReq.status_code == 200:
latestVersion = getVersion(json.loads(latestMaps.text)) latestVersion = json.loads(latestMaps.text)
if mapsVersion < latestVersion: if currentVersion["version"] < latestVersion["version"]:
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 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: else:
print("No updates") print("No updates")
except: except:
print("Failed to check updates") print("Failed to check updates :(")
def getMaps(self): def getJson(self, path):
with open("maps/index.json", "r") as f: with open(f"{path}.json", "r") as f:
maps = json.loads(f.read()) data = json.loads(f.read())
f.close() f.close()
return maps return data
def setFolder(self, elem, folder): def setFolder(self, elem=None, folder=None):
path = self.selectFolder() path = self.selectFolder()
self.folders[folder] = path self.folders[folder] = path
elem.setText(path) if elem:
elem.setText(path)
def setupActions(self): def setupActions(self):
self.changeInput.clicked.connect(lambda: self.setFolder(self.inputPath, "input")) self.changeInput.clicked.connect(lambda: self.setFolder(self.inputPath, "input"))
self.changeAltInput.clicked.connect(lambda: self.setFolder(self.altInputPath, "diff")) 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.assetMap.addItems(["No map", *[f'{e["game"]} - v{e["version"]}' for e in self.maps["maps"]]])
self.tabs.setTabEnabled(1, False) self.setExtractionState(False)
self.tabs.setTabEnabled(2, False)
self.updateTreeWidget(self.fileStructure)
self.loadFilesButton.clicked.connect(lambda: self.loadFiles()) self.loadFilesButton.clicked.connect(lambda: self.loadFiles())
self.actionReset.triggered.connect(lambda: self.resetApp()) self.actionReset.triggered.connect(lambda: self.resetApp())
self.actionExit.triggered.connect(lambda: self.close()) self.actionExit.triggered.connect(lambda: self.close())
self.extractSelected.clicked.connect(lambda: self.extractItems(False)) self.actionExtract_Selected.triggered.connect(lambda: self.extractItems(False))
self.extractAll.clicked.connect(lambda: self.extractItems(True)) 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()) 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 # workers
@pyqtSlot(list) @pyqtSlot(list)
def progressBarSlot(self, progress): def progressBarSlot(self, progress):
@@ -152,23 +196,16 @@ class AnimeWwise(QMainWindow):
if data["action"] == "load": if data["action"] == "load":
self.fileStructure = data["content"] self.fileStructure = data["content"]
self.updateTreeWidget(self.fileStructure) self.updateTreeWidget(self.fileStructure)
self.tabs.setTabEnabled(0, False) self.setExtractionState(True)
self.tabs.setTabEnabled(1, True)
self.tabs.setTabEnabled(2, True)
self.tabs.setCurrentIndex(1) self.tabs.setCurrentIndex(1)
print("Done !") print("Done !")
if data["action"] == "error": if data["action"] == "error":
QMessageBox.warning(None, "Warning", data["content"]["msg"], QMessageBox.Ok) QMessageBox.warning(None, "Warning", data["content"]["msg"], QMessageBox.Ok)
state = data["content"]["state"] state = data["content"]["state"]
if state == 1: if state == 2:
self.tabs.setTabEnabled(0, True) self.setExtractionState(True)
elif state == 2:
self.tabs.setTabEnabled(1, True)
self.tabs.setTabEnabled(2, True)
if data["action"] == "extract": if data["action"] == "extract":
self.tabs.setTabEnabled(1, True) self.setExtractionState(True)
self.tabs.setTabEnabled(2, True)
self.tabs.setCurrentIndex(2)
print("Finished extracting everything !") print("Finished extracting everything !")
if platform.system() == "Windows": if platform.system() == "Windows":
@@ -186,7 +223,6 @@ class AnimeWwise(QMainWindow):
else: else:
_map = None _map = None
self.tabs.setTabEnabled(0, False)
self.resetTreeWidget() self.resetTreeWidget()
# why is all this required for threading damnit # why is all this required for threading damnit
@@ -225,28 +261,36 @@ class AnimeWwise(QMainWindow):
def resetTreeWidget(self): def resetTreeWidget(self):
self.treeWidget.clear() self.treeWidget.clear()
self.tabs.setTabEnabled(1, False) self.fileStructure = {"folders": {}, "files": []}
self.setExtractionState(False)
def updateTreeWidget(self, structure): def updateTreeWidget(self, structure):
self.treeWidget.clear() self.treeWidget.clear()
self.treeWidget.setColumnCount(3) self.treeWidget.setColumnCount(4)
self.treeWidget.setHeaderLabels(["Name", "Offset", "Size", "Source"]) self.treeWidget.setHeaderLabels(["Name", "Duration", "Source", "Size", "Offset"])
self.addItems(None, structure) self.addItems(None, structure)
self.treeWidget.expandAll() self.treeWidget.expandAll()
self.treeWidget.header().setSectionResizeMode(0, QHeaderView.Stretch)
self.treeWidget.header().setSectionResizeMode(1, QHeaderView.ResizeToContents) self.treeWidget.header().setSectionResizeMode(0, QHeaderView.ResizeToContents)
self.treeWidget.header().setSectionResizeMode(2, 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.setHeaderHidden(False)
self.treeWidget.setEditTriggers(QAbstractItemView.NoEditTriggers) self.treeWidget.setEditTriggers(QAbstractItemView.NoEditTriggers)
self.treeWidget.setDragDropMode(QAbstractItemView.NoDragDrop) 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): def addItems(self, parent, element):
for folder_name in sorted(element.get("folders", {}).keys()): for folder_name in sorted(element.get("folders", {}).keys()):
folder_content = element["folders"][folder_name] 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.setFlags(folder_item.flags() | Qt.ItemIsTristate | Qt.ItemIsUserCheckable)
folder_item.setCheckState(0, Qt.Unchecked) folder_item.setCheckState(0, Qt.Unchecked)
if parent is None: if parent is None:
@@ -256,7 +300,8 @@ class AnimeWwise(QMainWindow):
self.addItems(folder_item, folder_content) self.addItems(folder_item, folder_content)
for file in sorted(element.get("files", [])): 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.setFlags(file_item.flags() | Qt.ItemIsUserCheckable)
file_item.setCheckState(0, Qt.Unchecked) file_item.setCheckState(0, Qt.Unchecked)
if parent is None: if parent is None:
@@ -266,12 +311,15 @@ class AnimeWwise(QMainWindow):
# page 3 - extraction # page 3 - extraction
def extractItems(self, _all): def extractItems(self, _all):
self.setFolder(folder="output")
if self.folders["output"] == "": if self.folders["output"] == "":
QMessageBox.warning(None, "Warning", "Missing output folder !", QMessageBox.Ok) QMessageBox.warning(None, "Warning", "Missing output folder !", QMessageBox.Ok)
return return
checked_items = [] checked_items = []
# todo: use file structure instead of tree view
def check_items(item, _all): def check_items(item, _all):
if item.checkState(0) == Qt.Checked or _all: if item.checkState(0) == Qt.Checked or _all:
if item.text(1) != "": if item.text(1) != "":
@@ -282,13 +330,11 @@ class AnimeWwise(QMainWindow):
for i in range(self.treeWidget.topLevelItemCount()): for i in range(self.treeWidget.topLevelItemCount()):
check_items(self.treeWidget.topLevelItem(i), _all) check_items(self.treeWidget.topLevelItem(i), _all)
self.tabs.setTabEnabled(1, False) self.setExtractionState(False)
self.tabs.setTabEnabled(2, False)
self.tabs.setCurrentIndex(2)
# yet another block of threading bs # yet another block of threading bs
self.backgroundThread = QThread() 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.backgroundWorker.moveToThread(self.backgroundThread)
self.backgroundThread.started.connect(self.backgroundWorker.run) self.backgroundThread.started.connect(self.backgroundWorker.run)
self.backgroundWorker.finished.connect(self.handleFinished) self.backgroundWorker.finished.connect(self.handleFinished)
@@ -310,18 +356,16 @@ class AnimeWwise(QMainWindow):
return { return {
"name": item.text(0), "name": item.text(0),
"path": path[:-1] if path[0] in ["changed_files", "new_files"] else path[1:-1], "path": path[:-1] if path[0] in ["changed_files", "new_files"] else path[1:-1],
"source": item.text(3), "source": item.text(2),
"offset": int(item.text(1), 16), "offset": int(item.text(4), 16),
"size": int(item.text(2)) "size": int(item.text(3))
} }
# misc # misc
def resetApp(self): def resetApp(self):
self.resetTreeWidget() self.resetTreeWidget()
self.extract.reset() self.extract.reset()
self.tabs.setTabEnabled(0, True) self.setExtractionState(False)
self.tabs.setTabEnabled(1, False)
self.tabs.setTabEnabled(2, False)
print("Reset !") print("Reset !")
def _appendText(self, text): def _appendText(self, text):

View File

@@ -1,9 +1,10 @@
import os import os
import io import io
import wwise
import tempfile import tempfile
import wavescan import wavescan
import subprocess
import platform import platform
import subprocess
from mapper import Mapper from mapper import Mapper
from allocator import Allocator from allocator import Allocator
from filereader import FileReader from filereader import FileReader
@@ -62,7 +63,7 @@ class WwiseExtract:
hdiff_files = self.get_hdiff_files(data, hdiff_data, filename) hdiff_files = self.get_hdiff_files(data, hdiff_data, filename)
files = self.compare_diff(files, hdiff_files) 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): def compare_diff(self, old, new):
old_dict = {file[0]:file[2] for file in old} old_dict = {file[0]:file[2] for file in old}
@@ -112,7 +113,7 @@ class WwiseExtract:
return files 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 # disable skip source if required
mapper = self.mapper mapper = self.mapper
base = self.file_structure base = self.file_structure
@@ -128,6 +129,18 @@ class WwiseExtract:
else: else:
key = None 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 key is not None:
if hdiff: if hdiff:
if file in old_files[0]: if file in old_files[0]:
@@ -139,7 +152,7 @@ class WwiseExtract:
if skip_source: if skip_source:
parts = parts[1:] parts = parts[1:]
self.add_to_structure(parts, [file[1], file[2], file[3]]) self.add_to_structure(parts, file_data)
else: else:
temp = base["folders"] temp = base["folders"]
@@ -161,7 +174,7 @@ class WwiseExtract:
if "unmapped" not in temp: if "unmapped" not in temp:
temp["unmapped"] = {"folders": {}, "files": []} temp["unmapped"] = {"folders": {}, "files": []}
temp["unmapped"]["files"].append(file) temp["unmapped"]["files"].append([file[0], file_data])
self.file_structure = base self.file_structure = base
@@ -175,7 +188,7 @@ class WwiseExtract:
current_level = current_level["folders"][part] current_level = current_level["folders"][part]
if "files" not in current_level: if "files" not in current_level:
current_level["files"] = [] current_level["files"] = []
current_level["files"].append([parts[-1], meta[0], meta[1], meta[2]]) current_level["files"].append([parts[-1], meta])
### extracting files ### ### extracting files ###

302
gui.ui
View File

@@ -5,24 +5,27 @@
<property name="windowModality"> <property name="windowModality">
<enum>Qt::NonModal</enum> <enum>Qt::NonModal</enum>
</property> </property>
<property name="enabled">
<bool>true</bool>
</property>
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>1100</width> <width>1100</width>
<height>800</height> <height>900</height>
</rect> </rect>
</property> </property>
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>1100</width> <width>1100</width>
<height>800</height> <height>900</height>
</size> </size>
</property> </property>
<property name="maximumSize"> <property name="maximumSize">
<size> <size>
<width>1100</width> <width>1100</width>
<height>800</height> <height>900</height>
</size> </size>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
@@ -39,7 +42,7 @@
</rect> </rect>
</property> </property>
<property name="currentIndex"> <property name="currentIndex">
<number>1</number> <number>0</number>
</property> </property>
<property name="usesScrollButtons"> <property name="usesScrollButtons">
<bool>true</bool> <bool>true</bool>
@@ -210,7 +213,7 @@
<x>0</x> <x>0</x>
<y>20</y> <y>20</y>
<width>1081</width> <width>1081</width>
<height>591</height> <height>551</height>
</rect> </rect>
</property> </property>
<property name="columnCount"> <property name="columnCount">
@@ -235,135 +238,49 @@
<string>Search something...</string> <string>Search something...</string>
</property> </property>
</widget> </widget>
</widget> <widget class="QWidget" name="horizontalLayoutWidget">
<widget class="QWidget" name="extractTab">
<attribute name="title">
<string>Extract</string>
</attribute>
<widget class="QWidget" name="verticalLayoutWidget_2">
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>9</x> <x>0</x>
<y>9</y> <y>580</y>
<width>1061</width> <width>1081</width>
<height>601</height> <height>31</height>
</rect> </rect>
</property> </property>
<layout class="QVBoxLayout" name="mainVLayout2"> <layout class="QHBoxLayout" name="horizontalLayout">
<item> <item>
<layout class="QHBoxLayout" name="outputLayout"> <widget class="QLabel" name="audioInfoLabel">
<item> <property name="text">
<widget class="QLabel" name="outputLabel"> <string>sample_long_name.wem | Duration : 00:00 | Stereo (48000Hz)</string>
<property name="text">
<string>Output folder</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="outputPath">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="changeOutput">
<property name="text">
<string>Select</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="outputFormatLayout">
<item>
<widget class="QLabel" name="outputFormatLabel">
<property name="text">
<string>Output format</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="outputFormat">
<property name="currentText">
<string/>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="Line" name="separatorC">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property> </property>
</widget> </widget>
</item> </item>
<item> <item>
<layout class="QVBoxLayout" name="progressWrapperLayout"> <widget class="QLabel" name="audioPlayPadding">
<item> <property name="text">
<layout class="QHBoxLayout" name="totalProgressLayout"> <string/>
<item>
<widget class="QLabel" name="totalProgressLabel">
<property name="text">
<string>Total progress</string>
</property>
</widget>
</item>
<item>
<widget class="QProgressBar" name="totalProgress">
<property name="value">
<number>0</number>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="fileProgressLayout">
<item>
<widget class="QLabel" name="fileProgressLabel">
<property name="text">
<string>Per file progress</string>
</property>
</widget>
</item>
<item>
<widget class="QProgressBar" name="fileProgress">
<property name="value">
<number>0</number>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<widget class="Line" name="separatorD">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property> </property>
</widget> </widget>
</item> </item>
<item> <item>
<layout class="QHBoxLayout" name="extractLayout"> <widget class="QPushButton" name="playAudioBtn">
<item> <property name="enabled">
<widget class="QPushButton" name="extractAll"> <bool>false</bool>
<property name="text"> </property>
<string>Extract All</string> <property name="text">
</property> <string>Play</string>
</widget> </property>
</item> </widget>
<item> </item>
<widget class="QPushButton" name="extractSelected"> <item>
<property name="text"> <widget class="QPushButton" name="stopAudioBtn">
<string>Extract Selected</string> <property name="enabled">
</property> <bool>false</bool>
</widget> </property>
</item> <property name="text">
</layout> <string>Stop</string>
</property>
</widget>
</item> </item>
</layout> </layout>
</widget> </widget>
@@ -373,9 +290,9 @@
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>10</x> <x>10</x>
<y>640</y> <y>720</y>
<width>1081</width> <width>1081</width>
<height>131</height> <height>151</height>
</rect> </rect>
</property> </property>
<property name="maximumSize"> <property name="maximumSize">
@@ -394,6 +311,54 @@
<bool>true</bool> <bool>true</bool>
</property> </property>
</widget> </widget>
<widget class="QWidget" name="layoutWidget">
<property name="geometry">
<rect>
<x>16</x>
<y>650</y>
<width>1071</width>
<height>61</height>
</rect>
</property>
<layout class="QVBoxLayout" name="progressWrapperLayout">
<item>
<layout class="QHBoxLayout" name="totalProgressLayout">
<item>
<widget class="QLabel" name="totalProgressLabel">
<property name="text">
<string>Total progress</string>
</property>
</widget>
</item>
<item>
<widget class="QProgressBar" name="totalProgress">
<property name="value">
<number>0</number>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="fileProgressLayout">
<item>
<widget class="QLabel" name="fileProgressLabel">
<property name="text">
<string>Per file progress</string>
</property>
</widget>
</item>
<item>
<widget class="QProgressBar" name="fileProgress">
<property name="value">
<number>0</number>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</widget> </widget>
<widget class="QMenuBar" name="menubar"> <widget class="QMenuBar" name="menubar">
<property name="geometry"> <property name="geometry">
@@ -412,7 +377,40 @@
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="actionExit"/> <addaction name="actionExit"/>
</widget> </widget>
<widget class="QMenu" name="menuExtract">
<property name="title">
<string>Extract</string>
</property>
<widget class="QMenu" name="menuOutput_format">
<property name="title">
<string>Output format</string>
</property>
<addaction name="actionformat"/>
</widget>
<addaction name="menuOutput_format"/>
<addaction name="separator"/>
<addaction name="actionExtract_All"/>
<addaction name="actionExtract_Selected"/>
</widget>
<widget class="QMenu" name="menuOther">
<property name="title">
<string>Other</string>
</property>
<addaction name="actionReport_a_bug"/>
<addaction name="actionSource_code"/>
<addaction name="actionDiscord"/>
</widget>
<widget class="QMenu" name="menuView">
<property name="title">
<string>View</string>
</property>
<addaction name="actionExpand_all"/>
<addaction name="actionCollapse_all"/>
</widget>
<addaction name="menuFile"/> <addaction name="menuFile"/>
<addaction name="menuView"/>
<addaction name="menuExtract"/>
<addaction name="menuOther"/>
</widget> </widget>
<action name="actionnot_working_here_yet"> <action name="actionnot_working_here_yet">
<property name="text"> <property name="text">
@@ -439,6 +437,64 @@
<string>Exit</string> <string>Exit</string>
</property> </property>
</action> </action>
<action name="actionExtract_All">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Extract All</string>
</property>
</action>
<action name="actionExtract_Selected">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Extract Selected</string>
</property>
</action>
<action name="actionReport_a_bug">
<property name="text">
<string>Report a bug</string>
</property>
</action>
<action name="actionSource_code">
<property name="text">
<string>Source code</string>
</property>
</action>
<action name="actionDiscord">
<property name="text">
<string>Discord</string>
</property>
</action>
<action name="actionformat">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>format</string>
</property>
<property name="visible">
<bool>false</bool>
</property>
</action>
<action name="actionExpand_all">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Expand all</string>
</property>
</action>
<action name="actionCollapse_all">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Collapse all</string>
</property>
</action>
</widget> </widget>
<tabstops> <tabstops>
<tabstop>inputPath</tabstop> <tabstop>inputPath</tabstop>

4
version.json Normal file
View File

@@ -0,0 +1,4 @@
{
"version": 21,
"mapsVersion": 89
}

127
wwise.py Normal file
View File

@@ -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 ?