diff --git a/LICENCE.md b/LICENCE.md
new file mode 100644
index 0000000..cbe5ad1
--- /dev/null
+++ b/LICENCE.md
@@ -0,0 +1,437 @@
+Attribution-NonCommercial-ShareAlike 4.0 International
+
+=======================================================================
+
+Creative Commons Corporation ("Creative Commons") is not a law firm and
+does not provide legal services or legal advice. Distribution of
+Creative Commons public licenses does not create a lawyer-client or
+other relationship. Creative Commons makes its licenses and related
+information available on an "as-is" basis. Creative Commons gives no
+warranties regarding its licenses, any material licensed under their
+terms and conditions, or any related information. Creative Commons
+disclaims all liability for damages resulting from their use to the
+fullest extent possible.
+
+Using Creative Commons Public Licenses
+
+Creative Commons public licenses provide a standard set of terms and
+conditions that creators and other rights holders may use to share
+original works of authorship and other material subject to copyright
+and certain other rights specified in the public license below. The
+following considerations are for informational purposes only, are not
+exhaustive, and do not form part of our licenses.
+
+ Considerations for licensors: Our public licenses are
+ intended for use by those authorized to give the public
+ permission to use material in ways otherwise restricted by
+ copyright and certain other rights. Our licenses are
+ irrevocable. Licensors should read and understand the terms
+ and conditions of the license they choose before applying it.
+ Licensors should also secure all rights necessary before
+ applying our licenses so that the public can reuse the
+ material as expected. Licensors should clearly mark any
+ material not subject to the license. This includes other CC-
+ licensed material, or material used under an exception or
+ limitation to copyright. More considerations for licensors:
+ wiki.creativecommons.org/Considerations_for_licensors
+
+ Considerations for the public: By using one of our public
+ licenses, a licensor grants the public permission to use the
+ licensed material under specified terms and conditions. If
+ the licensor's permission is not necessary for any reason--for
+ example, because of any applicable exception or limitation to
+ copyright--then that use is not regulated by the license. Our
+ licenses grant only permissions under copyright and certain
+ other rights that a licensor has authority to grant. Use of
+ the licensed material may still be restricted for other
+ reasons, including because others have copyright or other
+ rights in the material. A licensor may make special requests,
+ such as asking that all changes be marked or described.
+ Although not required by our licenses, you are encouraged to
+ respect those requests where reasonable. More considerations
+ for the public:
+ wiki.creativecommons.org/Considerations_for_licensees
+
+=======================================================================
+
+Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International
+Public License
+
+By exercising the Licensed Rights (defined below), You accept and agree
+to be bound by the terms and conditions of this Creative Commons
+Attribution-NonCommercial-ShareAlike 4.0 International Public License
+("Public License"). To the extent this Public License may be
+interpreted as a contract, You are granted the Licensed Rights in
+consideration of Your acceptance of these terms and conditions, and the
+Licensor grants You such rights in consideration of benefits the
+Licensor receives from making the Licensed Material available under
+these terms and conditions.
+
+
+Section 1 -- Definitions.
+
+ a. Adapted Material means material subject to Copyright and Similar
+ Rights that is derived from or based upon the Licensed Material
+ and in which the Licensed Material is translated, altered,
+ arranged, transformed, or otherwise modified in a manner requiring
+ permission under the Copyright and Similar Rights held by the
+ Licensor. For purposes of this Public License, where the Licensed
+ Material is a musical work, performance, or sound recording,
+ Adapted Material is always produced where the Licensed Material is
+ synched in timed relation with a moving image.
+
+ b. Adapter's License means the license You apply to Your Copyright
+ and Similar Rights in Your contributions to Adapted Material in
+ accordance with the terms and conditions of this Public License.
+
+ c. BY-NC-SA Compatible License means a license listed at
+ creativecommons.org/compatiblelicenses, approved by Creative
+ Commons as essentially the equivalent of this Public License.
+
+ d. Copyright and Similar Rights means copyright and/or similar rights
+ closely related to copyright including, without limitation,
+ performance, broadcast, sound recording, and Sui Generis Database
+ Rights, without regard to how the rights are labeled or
+ categorized. For purposes of this Public License, the rights
+ specified in Section 2(b)(1)-(2) are not Copyright and Similar
+ Rights.
+
+ e. Effective Technological Measures means those measures that, in the
+ absence of proper authority, may not be circumvented under laws
+ fulfilling obligations under Article 11 of the WIPO Copyright
+ Treaty adopted on December 20, 1996, and/or similar international
+ agreements.
+
+ f. Exceptions and Limitations means fair use, fair dealing, and/or
+ any other exception or limitation to Copyright and Similar Rights
+ that applies to Your use of the Licensed Material.
+
+ g. License Elements means the license attributes listed in the name
+ of a Creative Commons Public License. The License Elements of this
+ Public License are Attribution, NonCommercial, and ShareAlike.
+
+ h. Licensed Material means the artistic or literary work, database,
+ or other material to which the Licensor applied this Public
+ License.
+
+ i. Licensed Rights means the rights granted to You subject to the
+ terms and conditions of this Public License, which are limited to
+ all Copyright and Similar Rights that apply to Your use of the
+ Licensed Material and that the Licensor has authority to license.
+
+ j. Licensor means the individual(s) or entity(ies) granting rights
+ under this Public License.
+
+ k. NonCommercial means not primarily intended for or directed towards
+ commercial advantage or monetary compensation. For purposes of
+ this Public License, the exchange of the Licensed Material for
+ other material subject to Copyright and Similar Rights by digital
+ file-sharing or similar means is NonCommercial provided there is
+ no payment of monetary compensation in connection with the
+ exchange.
+
+ l. Share means to provide material to the public by any means or
+ process that requires permission under the Licensed Rights, such
+ as reproduction, public display, public performance, distribution,
+ dissemination, communication, or importation, and to make material
+ available to the public including in ways that members of the
+ public may access the material from a place and at a time
+ individually chosen by them.
+
+ m. Sui Generis Database Rights means rights other than copyright
+ resulting from Directive 96/9/EC of the European Parliament and of
+ the Council of 11 March 1996 on the legal protection of databases,
+ as amended and/or succeeded, as well as other essentially
+ equivalent rights anywhere in the world.
+
+ n. You means the individual or entity exercising the Licensed Rights
+ under this Public License. Your has a corresponding meaning.
+
+
+Section 2 -- Scope.
+
+ a. License grant.
+
+ 1. Subject to the terms and conditions of this Public License,
+ the Licensor hereby grants You a worldwide, royalty-free,
+ non-sublicensable, non-exclusive, irrevocable license to
+ exercise the Licensed Rights in the Licensed Material to:
+
+ a. reproduce and Share the Licensed Material, in whole or
+ in part, for NonCommercial purposes only; and
+
+ b. produce, reproduce, and Share Adapted Material for
+ NonCommercial purposes only.
+
+ 2. Exceptions and Limitations. For the avoidance of doubt, where
+ Exceptions and Limitations apply to Your use, this Public
+ License does not apply, and You do not need to comply with
+ its terms and conditions.
+
+ 3. Term. The term of this Public License is specified in Section
+ 6(a).
+
+ 4. Media and formats; technical modifications allowed. The
+ Licensor authorizes You to exercise the Licensed Rights in
+ all media and formats whether now known or hereafter created,
+ and to make technical modifications necessary to do so. The
+ Licensor waives and/or agrees not to assert any right or
+ authority to forbid You from making technical modifications
+ necessary to exercise the Licensed Rights, including
+ technical modifications necessary to circumvent Effective
+ Technological Measures. For purposes of this Public License,
+ simply making modifications authorized by this Section 2(a)
+ (4) never produces Adapted Material.
+
+ 5. Downstream recipients.
+
+ a. Offer from the Licensor -- Licensed Material. Every
+ recipient of the Licensed Material automatically
+ receives an offer from the Licensor to exercise the
+ Licensed Rights under the terms and conditions of this
+ Public License.
+
+ b. Additional offer from the Licensor -- Adapted Material.
+ Every recipient of Adapted Material from You
+ automatically receives an offer from the Licensor to
+ exercise the Licensed Rights in the Adapted Material
+ under the conditions of the Adapter's License You apply.
+
+ c. No downstream restrictions. You may not offer or impose
+ any additional or different terms or conditions on, or
+ apply any Effective Technological Measures to, the
+ Licensed Material if doing so restricts exercise of the
+ Licensed Rights by any recipient of the Licensed
+ Material.
+
+ 6. No endorsement. Nothing in this Public License constitutes or
+ may be construed as permission to assert or imply that You
+ are, or that Your use of the Licensed Material is, connected
+ with, or sponsored, endorsed, or granted official status by,
+ the Licensor or others designated to receive attribution as
+ provided in Section 3(a)(1)(A)(i).
+
+ b. Other rights.
+
+ 1. Moral rights, such as the right of integrity, are not
+ licensed under this Public License, nor are publicity,
+ privacy, and/or other similar personality rights; however, to
+ the extent possible, the Licensor waives and/or agrees not to
+ assert any such rights held by the Licensor to the limited
+ extent necessary to allow You to exercise the Licensed
+ Rights, but not otherwise.
+
+ 2. Patent and trademark rights are not licensed under this
+ Public License.
+
+ 3. To the extent possible, the Licensor waives any right to
+ collect royalties from You for the exercise of the Licensed
+ Rights, whether directly or through a collecting society
+ under any voluntary or waivable statutory or compulsory
+ licensing scheme. In all other cases the Licensor expressly
+ reserves any right to collect such royalties, including when
+ the Licensed Material is used other than for NonCommercial
+ purposes.
+
+
+Section 3 -- License Conditions.
+
+Your exercise of the Licensed Rights is expressly made subject to the
+following conditions.
+
+ a. Attribution.
+
+ 1. If You Share the Licensed Material (including in modified
+ form), You must:
+
+ a. retain the following if it is supplied by the Licensor
+ with the Licensed Material:
+
+ i. identification of the creator(s) of the Licensed
+ Material and any others designated to receive
+ attribution, in any reasonable manner requested by
+ the Licensor (including by pseudonym if
+ designated);
+
+ ii. a copyright notice;
+
+ iii. a notice that refers to this Public License;
+
+ iv. a notice that refers to the disclaimer of
+ warranties;
+
+ v. a URI or hyperlink to the Licensed Material to the
+ extent reasonably practicable;
+
+ b. indicate if You modified the Licensed Material and
+ retain an indication of any previous modifications; and
+
+ c. indicate the Licensed Material is licensed under this
+ Public License, and include the text of, or the URI or
+ hyperlink to, this Public License.
+
+ 2. You may satisfy the conditions in Section 3(a)(1) in any
+ reasonable manner based on the medium, means, and context in
+ which You Share the Licensed Material. For example, it may be
+ reasonable to satisfy the conditions by providing a URI or
+ hyperlink to a resource that includes the required
+ information.
+ 3. If requested by the Licensor, You must remove any of the
+ information required by Section 3(a)(1)(A) to the extent
+ reasonably practicable.
+
+ b. ShareAlike.
+
+ In addition to the conditions in Section 3(a), if You Share
+ Adapted Material You produce, the following conditions also apply.
+
+ 1. The Adapter's License You apply must be a Creative Commons
+ license with the same License Elements, this version or
+ later, or a BY-NC-SA Compatible License.
+
+ 2. You must include the text of, or the URI or hyperlink to, the
+ Adapter's License You apply. You may satisfy this condition
+ in any reasonable manner based on the medium, means, and
+ context in which You Share Adapted Material.
+
+ 3. You may not offer or impose any additional or different terms
+ or conditions on, or apply any Effective Technological
+ Measures to, Adapted Material that restrict exercise of the
+ rights granted under the Adapter's License You apply.
+
+
+Section 4 -- Sui Generis Database Rights.
+
+Where the Licensed Rights include Sui Generis Database Rights that
+apply to Your use of the Licensed Material:
+
+ a. for the avoidance of doubt, Section 2(a)(1) grants You the right
+ to extract, reuse, reproduce, and Share all or a substantial
+ portion of the contents of the database for NonCommercial purposes
+ only;
+
+ b. if You include all or a substantial portion of the database
+ contents in a database in which You have Sui Generis Database
+ Rights, then the database in which You have Sui Generis Database
+ Rights (but not its individual contents) is Adapted Material,
+ including for purposes of Section 3(b); and
+
+ c. You must comply with the conditions in Section 3(a) if You Share
+ all or a substantial portion of the contents of the database.
+
+For the avoidance of doubt, this Section 4 supplements and does not
+replace Your obligations under this Public License where the Licensed
+Rights include other Copyright and Similar Rights.
+
+
+Section 5 -- Disclaimer of Warranties and Limitation of Liability.
+
+ a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
+ EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
+ AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
+ ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
+ IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
+ WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
+ PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
+ ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
+ KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
+ ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
+
+ b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
+ TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
+ NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
+ INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
+ COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
+ USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
+ ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
+ DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
+ IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
+
+ c. The disclaimer of warranties and limitation of liability provided
+ above shall be interpreted in a manner that, to the extent
+ possible, most closely approximates an absolute disclaimer and
+ waiver of all liability.
+
+
+Section 6 -- Term and Termination.
+
+ a. This Public License applies for the term of the Copyright and
+ Similar Rights licensed here. However, if You fail to comply with
+ this Public License, then Your rights under this Public License
+ terminate automatically.
+
+ b. Where Your right to use the Licensed Material has terminated under
+ Section 6(a), it reinstates:
+
+ 1. automatically as of the date the violation is cured, provided
+ it is cured within 30 days of Your discovery of the
+ violation; or
+
+ 2. upon express reinstatement by the Licensor.
+
+ For the avoidance of doubt, this Section 6(b) does not affect any
+ right the Licensor may have to seek remedies for Your violations
+ of this Public License.
+
+ c. For the avoidance of doubt, the Licensor may also offer the
+ Licensed Material under separate terms or conditions or stop
+ distributing the Licensed Material at any time; however, doing so
+ will not terminate this Public License.
+
+ d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
+ License.
+
+
+Section 7 -- Other Terms and Conditions.
+
+ a. The Licensor shall not be bound by any additional or different
+ terms or conditions communicated by You unless expressly agreed.
+
+ b. Any arrangements, understandings, or agreements regarding the
+ Licensed Material not stated herein are separate from and
+ independent of the terms and conditions of this Public License.
+
+
+Section 8 -- Interpretation.
+
+ a. For the avoidance of doubt, this Public License does not, and
+ shall not be interpreted to, reduce, limit, restrict, or impose
+ conditions on any use of the Licensed Material that could lawfully
+ be made without permission under this Public License.
+
+ b. To the extent possible, if any provision of this Public License is
+ deemed unenforceable, it shall be automatically reformed to the
+ minimum extent necessary to make it enforceable. If the provision
+ cannot be reformed, it shall be severed from this Public License
+ without affecting the enforceability of the remaining terms and
+ conditions.
+
+ c. No term or condition of this Public License will be waived and no
+ failure to comply consented to unless expressly agreed to by the
+ Licensor.
+
+ d. Nothing in this Public License constitutes or may be interpreted
+ as a limitation upon, or waiver of, any privileges and immunities
+ that apply to the Licensor or You, including from the legal
+ processes of any jurisdiction or authority.
+
+=======================================================================
+
+Creative Commons is not a party to its public
+licenses. Notwithstanding, Creative Commons may elect to apply one of
+its public licenses to material it publishes and in those instances
+will be considered the “Licensor.” The text of the Creative Commons
+public licenses is dedicated to the public domain under the CC0 Public
+Domain Dedication. Except for the limited purpose of indicating that
+material is shared under a Creative Commons public license or as
+otherwise permitted by the Creative Commons policies published at
+creativecommons.org/policies, Creative Commons does not authorize the
+use of the trademark "Creative Commons" or any other trademark or logo
+of Creative Commons without its prior written consent including,
+without limitation, in connection with any unauthorized modifications
+to any of its public licenses or any other arrangements,
+understandings, or agreements concerning use of licensed material. For
+the avoidance of doubt, this paragraph does not form part of the
+public licenses.
+
+Creative Commons may be contacted at creativecommons.org.
diff --git a/app.py b/app.py
index d18fbec..9798188 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,76 +73,142 @@ 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)
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.pckLoadTypeCombo.addItems(["Folder", "File"])
+ self.pckLoadTypeCombo.currentIndexChanged.connect(self.loadTypeChange)
+ self.loadType = "folder"
+
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.actionExpand_all.triggered.connect(lambda: self.treeWidget.expandAll())
+ self.actionCollapse_all.triggered.connect(lambda: self.treeWidget.collapseAll())
+
+ 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)
+
+ # utils
+ def loadTypeChange(self, event):
+ if event == 0:
+ self.pckSubFold.setEnabled(True)
+ self.loadType = "folder"
+ elif event == 1:
+ self.pckSubFold.setEnabled(False)
+ self.loadType = "file"
+
+ 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)
+
+ def displaySize(self, size):
+ if size < 1024:
+ return f"{size} b"
+ elif size > 1024 and size < 1048576:
+ return f"{size//1024} KiB"
+ elif size > 1048576 and size < 1073741824:
+ return f"{size//1048576} MiB"
+ elif size > 1073741824:
+ return f"{size//1073741824} GiB"
+
# workers
@pyqtSlot(list)
def progressBarSlot(self, progress):
- if progress[0] == "load":
- self.loadProgress.setValue(math.ceil(progress[1]))
if progress[0] == "total":
self.totalProgress.setValue(math.ceil(progress[1]))
elif progress[0] == "file":
@@ -152,23 +219,19 @@ 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.loadFilesButton.setEnabled(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)
+ self.loadFilesButton.setEnabled(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":
@@ -176,22 +239,38 @@ class AnimeWwise(QMainWindow):
# page 1 - config
def loadFiles(self):
- if self.folders["input"] == "":
- QMessageBox.warning(None, "Warning", "Missing input folder !", QMessageBox.Ok)
+ if self.loadType == "folder":
+ self.setFolder(folder="input")
+ files = []
+ if self.folders["input"]:
+ if self.pckSubFold.isChecked():
+ files = [os.path.join(root, f) for root, dirs, files_in_dir in os.walk(self.folders["input"]) for f in files_in_dir if f.endswith(".pck")]
+ else:
+ 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())
+ files = [path[0]]
+
+ if len(files) == 0 or files[0] == "":
+ QMessageBox.warning(None, "Warning", "Nothing to load !", QMessageBox.Ok)
return
+ self.currentInput = self.folders["input"]
+ if not self.folders["input"]:
+ self.currentInput = os.path.dirname(path[0])
+
_map = self.assetMap.currentIndex()
if _map != 0:
_map = self.maps["maps"][_map-1]["name"]
else:
_map = None
- self.tabs.setTabEnabled(0, False)
self.resetTreeWidget()
+ self.loadFilesButton.setEnabled(False)
# why is all this required for threading damnit
self.backgroundThread = QThread()
- self.backgroundWorker = BackgroundWorker("load", self.extract, {"input": self.folders["input"], "map": _map, "diff": self.folders["diff"]})
+ self.backgroundWorker = BackgroundWorker("load", self.extract, {"input": files, "map": _map, "diff": self.folders["diff"]})
self.backgroundWorker.moveToThread(self.backgroundThread)
self.backgroundThread.started.connect(self.backgroundWorker.run)
self.backgroundWorker.finished.connect(self.handleFinished)
@@ -211,7 +290,7 @@ class AnimeWwise(QMainWindow):
result = self.searchFiles(self.fileStructure, search)
self.updateTreeWidget(result)
- def searchFiles(self, data, substring, current_path=""):
+ def searchFiles(self, data, substring, current_path="", flatten=False):
result = {"folders": {}, "files": []}
result["files"] = [file for file in data.get("files", []) if substring in file[0]]
@@ -221,32 +300,69 @@ class AnimeWwise(QMainWindow):
if subfolder_result["files"] or subfolder_result["folders"]:
result["folders"][folder_name] = subfolder_result
+ if flatten:
+ while result["files"] == []:
+ if len(result["folders"]) == 0:
+ break
+ result = list(result["folders"].values())[0]
+
return result
def resetTreeWidget(self):
self.treeWidget.clear()
- self.tabs.setTabEnabled(1, False)
+ self.fileStructure = {"folders": {}, "files": []}
+ self.audioInfoLabel.setText("Click on an audio file to get more infos !")
+ 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", "Compressed Size", "Source", "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 computeFolderSize(self, folder):
+ total_size = 0
+
+ for file in folder.get("files", []):
+ total_size += file[1]["size"]
+
+ for subfolder_name, subfolder in folder.get("folders", {}).items():
+ subfolder_size = self.computeFolderSize(subfolder)
+ total_size += subfolder_size
+
+ return total_size
+
+ def updateAudioPreview(self, item, column):
+ file_data = self.searchFiles(self.fileStructure, item.text(0), flatten=True)
+
+ if file_data == {"folders": {}, "files": []}:
+ self.audioInfoLabel.setText("Click on an audio file to get more infos !")
+ return
+
+ meta = file_data["files"][0][1]["metadata"]
+
+ # show meta
+ text = f'Infos for {item.text(0)} => Channels : {meta["channels"]} | Sample rate : {meta["sampleRate"]} Hz | Bitrate : {meta["avgBitrate"]} kbps | Codec : {meta["codecDisplay"]} | Layout type : {meta["layoutType"]}'
+ self.audioInfoLabel.setText(text)
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, "", self.displaySize(self.computeFolderSize(folder_content)), "", ""])
folder_item.setFlags(folder_item.flags() | Qt.ItemIsTristate | Qt.ItemIsUserCheckable)
folder_item.setCheckState(0, Qt.Unchecked)
if parent is None:
@@ -255,8 +371,9 @@ class AnimeWwise(QMainWindow):
parent.addChild(folder_item)
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])])
+ for file in sorted(element.get("files", []), key=lambda x: x[0]):
+ file_meta = file[1]
+ file_item = QTreeWidgetItem([file[0], f'{round(file_meta["metadata"]["duration"], 1)} seconds', self.displaySize(file_meta["size"]), file_meta["source"], str(hex(file_meta["offset"]))])
file_item.setFlags(file_item.flags() | Qt.ItemIsUserCheckable)
file_item.setCheckState(0, Qt.Unchecked)
if parent is None:
@@ -266,9 +383,7 @@ class AnimeWwise(QMainWindow):
# page 3 - extraction
def extractItems(self, _all):
- if self.folders["output"] == "":
- QMessageBox.warning(None, "Warning", "Missing output folder !", QMessageBox.Ok)
- return
+ self.setFolder(folder="output")
checked_items = []
@@ -282,13 +397,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.currentInput, "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)
@@ -306,22 +419,28 @@ class AnimeWwise(QMainWindow):
while current_item is not None:
path.insert(0, current_item.text(0))
current_item = current_item.parent()
-
+
+ meta = self.searchFiles(self.fileStructure, item.text(0), flatten=True)["files"][0]
+ name = meta[0]
+ meta = meta[1] # move inside
+
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))
+ "path": path[:-1],
+ "source": meta["source"],
+ "offset": meta["offset"],
+ "size": meta["size"]
}
# 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.currentInput = None
+ self.setExtractionState(False)
+ self.tabs.setCurrentIndex(0)
+ self.totalProgress.setValue(0)
+ self.fileProgress.setValue(0)
print("Reset !")
def _appendText(self, text):
diff --git a/extract.py b/extract.py
index d7b4bd0..e240d05 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
@@ -16,19 +17,43 @@ class WwiseExtract:
def __init__(self):
self.allocator = Allocator()
self.hdiff_dir = None
+ self.maps = {}
### loading files ###
- def load_folder(self, _map, folder_path, diff_path, progress):
+ def load_map(self, _map):
+ map_name = _map.split(".")[0]
+
+ if map_name not in self.maps or self.maps[map_name] is None:
+ print("Map load required !")
+ mapper = Mapper(path(cwd, f"maps/{_map}"))
+ self.maps[map_name] = mapper
+ else:
+ print("Mapping already loaded, skipping")
+
+ return self.maps[map_name]
+
+ def load_folder(self, _map, files, diff_path, progress):
+ self.progress = progress
+ self.steps = 1
+
self.mapper = None
if _map is not None:
- self.mapper = Mapper(path(cwd, f"maps/{_map}"))
+ self.mapper = self.load_map(_map)
+
self.file_structure = {"folders": {}, "files": []}
- files = [f for f in os.listdir(folder_path) if f.endswith(".pck")]
hdiff_files = []
if diff_path != "":
hdiff_files = [f for f in os.listdir(diff_path) if f.endswith(".pck.hdiff")]
+
+ # TODO: hdiff mode will only use .hdiff files and ignore .pck even in the update folder, i need to implement it, eventually
+
+ # remove alone pck / hdiff
+ base_files = [os.path.basename(f) for f in files]
+ hdiff_files = [f for f in hdiff_files if os.path.basename(f.replace(".hdiff", "")) in base_files]
+ base_hfiles = [os.path.basename(f) for f in hdiff_files]
+ files = [f for f in files if f"{os.path.basename(f)}.hdiff" in base_hfiles]
if len(files) == 0:
return None
@@ -37,12 +62,12 @@ class WwiseExtract:
print(f"\nLoading {len(files)} files...")
for file in files:
pos += 1
- progress(["load", pos * 100 // len(files)])
+ self.update_progress(pos, len(files), 1)
hdiff = None
- if f"{file}.hdiff" in hdiff_files:
- hdiff = path(diff_path, hdiff_files[hdiff_files.index(f"{file}.hdiff")])
- self.load_file(path(folder_path, file), hdiff)
+ if f"{os.path.basename(file)}.hdiff" in hdiff_files:
+ hdiff = path(diff_path, hdiff_files[hdiff_files.index(f"{os.path.basename(file)}.hdiff")])
+ self.load_file(file, hdiff)
return self.file_structure
@@ -55,14 +80,16 @@ class WwiseExtract:
def get_wems(self, data, filename, hdiff):
reader = FileReader(io.BytesIO(data), "little")
files = wavescan.get_data(reader, filename)
+
if hdiff is not None:
with open(hdiff, "rb") as f:
hdiff_data = f.read()
f.close()
- hdiff_files = self.get_hdiff_files(data, hdiff_data, filename)
+
+ hdiff_files, data = 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}
@@ -97,6 +124,10 @@ class WwiseExtract:
call(args)
+ if not os.path.exists(path(working_dir.name, "patch.pck")):
+ print(f"[ERROR] failed to patch {source_name}, skipping")
+ return []
+
with open(path(working_dir.name, "patch.pck"), "rb") as f:
data = f.read()
f.close()
@@ -110,9 +141,9 @@ class WwiseExtract:
working_dir.cleanup()
- return files
+ return files, data
- 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 +159,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", name=f"{file[3]}:{file[0]}:{file[1]}"))
+
+ file_data["metadata"] = parsed_wem
+
if key is not None:
if hdiff:
if file in old_files[0]:
@@ -139,7 +182,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 +204,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 +218,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 ###
@@ -331,8 +374,10 @@ class WwiseExtract:
self.progress(["file", current * 100 // total])
def reset(self):
- if self.mapper is not None:
- self.mapper.reset()
+ self.mapper = None
+ for e in self.maps.values():
+ e.reset()
+ self.maps.clear()
self.allocator.free_mem()
if self.hdiff_dir is not None:
self.hdiff_dir.cleanup()
diff --git a/filereader.py b/filereader.py
index 86c5a9b..507a8ff 100644
--- a/filereader.py
+++ b/filereader.py
@@ -8,52 +8,63 @@ class FileReader:
File reader for files, not much too say
"""
- def __init__(self, file, endianness:str):
+ def __init__(self, file, endianness:str, name:str=None):
self.stream = file
self.endianness = endianness
+ if name:
+ self.name = name
- def _read(self, mode:str, bufferLength:int, endianness:str=None) -> bytes:
+ def _read(self, mode:str, bufferLength:int, endianness:str=None, pos:int=None) -> bytes:
# endianness override
if endianness is None:
endianness = self.endianness
endianness = "<" if endianness == "little" else ">"
- return struct.unpack(f"{endianness}{mode}", bytearray(self.stream.read(bufferLength)))[0]
+ if pos:
+ pos_backup = self.GetBufferPos()
+ self.SetBufferPos(pos)
+
+ data = struct.unpack(f"{endianness}{mode}", bytearray(self.stream.read(bufferLength)))[0]
+
+ if pos:
+ self.SetBufferPos(pos_backup)
+
+ return data
# read methods
- def ReadInt8(self, endianness:str=None) -> int:
- return self._read("b", 1, endianness)
+ def ReadInt8(self, endianness:str=None, pos:int=None) -> int:
+ return self._read("b", 1, endianness, pos)
- def ReadUInt8(self, endianness:str=None) -> int:
- return self._read("B", 1, endianness)
+ def ReadUInt8(self, endianness:str=None, pos:int=None) -> int:
+ return self._read("B", 1, endianness, pos)
- def ReadInt16(self, endianness:str=None) -> int:
- return self._read("h", 2, endianness)
+ def ReadInt16(self, endianness:str=None, pos:int=None) -> int:
+ return self._read("h", 2, endianness, pos)
- def ReadUInt16(self, endianness:str=None) -> int:
- return self._read("H", 2, endianness)
+ def ReadUInt16(self, endianness:str=None, pos:int=None) -> int:
+ return self._read("H", 2, endianness, pos)
- def ReadInt32(self, endianness:str=None) -> int:
- return self._read("i", 4, endianness)
+ def ReadInt32(self, endianness:str=None, pos:int=None) -> int:
+ return self._read("i", 4, endianness, pos)
- def ReadUInt32(self, endianness:str=None) -> int:
- return self._read("I", 4, endianness)
+ def ReadUInt32(self, endianness:str=None, pos:int=None) -> int:
+ return self._read("I", 4, endianness, pos)
- def ReadLong(self, endianness:str=None) -> int:
- return self._read("l", 4, endianness)
+ def ReadLong(self, endianness:str=None, pos:int=None) -> int:
+ return self._read("l", 4, endianness, pos)
- def ReadULong(self, endianness:str=None) -> int:
- return self._read("L", 4, endianness)
+ def ReadULong(self, endianness:str=None, pos:int=None) -> int:
+ return self._read("L", 4, endianness, pos)
- def ReadLongLong(self, endianness:str=None) -> int:
- return self._read("q", 8, endianness)
+ def ReadLongLong(self, endianness:str=None, pos:int=None) -> int:
+ return self._read("q", 8, endianness, pos)
- def ReadULongLong(self, endianness:str=None) -> int:
- return self._read("Q", 8, endianness)
+ def ReadULongLong(self, endianness:str=None, pos:int=None) -> int:
+ return self._read("Q", 8, endianness, pos)
- def ReadBytes(self, length:int, endianness:str=None) -> bytes:
- return self._read(f"{str(length)}s", int(length), endianness)
+ def ReadBytes(self, length:int, endianness:str=None, pos:int=None) -> bytes:
+ return self._read(f"{str(length)}s", int(length), endianness, pos)
# buffer utils
def GetBufferPos(self) -> int:
@@ -76,3 +87,8 @@ class FileReader:
def GetRemainingLength(self) -> int:
return self.GetStreamLength() - self.GetBufferPos()
+
+ def GetName(self) -> str:
+ if self.name:
+ return self.name
+ return ""
diff --git a/gui.ui b/gui.ui
index 5b633ec..2849f4f 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
@@ -74,69 +77,172 @@
-
-
-
-
-
-
- true
+
+
+
+
+
+
+ -
+
+
+ 2
+
+
-
+
+
+
+ 32
+ 75
+ true
+
- Select
+ Welcome to AnimeWwise !
-
-
- -
-
-
- Select
-
-
- false
-
-
- false
-
-
-
- -
-
-
- true
-
-
- true
-
-
-
- -
-
-
- Diff folder (optional)
-
-
-
- -
-
-
- Input folder
-
-
-
- -
-
-
- true
-
-
-
-
-
- true
+
+ Qt::AlignCenter
+ -
+
+
-
+
+
+
+
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ -
+
+
+ true
+
+
+ 0
+
+
+
+ Extract audio package (.pck)
+
+
+
+
+ 10
+ 10
+ 1041
+ 111
+
+
+
+
-
+
+
+ -
+
+
+ What to load :
+
+
+
+ -
+
+
+ true
+
+
+ Include subfolders ?
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+ Extract update package (.hdiff)
+
+
+
+
+ 10
+ 10
+ 1041
+ 131
+
+
+
+ -
+
+
+ Diff folder
+
+
+
+ -
+
+
+ true
+
+
+ Select
+
+
+
+ -
+
+
+ true
+
+
+ true
+
+
+
+ -
+
+
+
+ 75
+ true
+
+
+
+ Select here the folder containing the .hdiff files present in the game update package. And for the input folder asked upon loading, select the game audio folder before the update !
+Subfolders are disabled in this mode, make sure to be in the correct place. For any help check the README.md or ask on discord.
+
+
+ Qt::AlignCenter
+
+
+ true
+
+
+
+
+
+
+
+
-
@@ -144,11 +250,15 @@
+ -
+
+
+
+
+
+
-
-
-
-
-
-
@@ -156,8 +266,18 @@
+ -
+
+
+ -
+
+
+
+
+
+
-
@@ -167,6 +287,13 @@
-
+
-
+
+
+
+
+
+
-
@@ -175,25 +302,11 @@
-
-
-
-
-
-
- Progress
-
-
-
- -
-
-
- 0
-
-
- false
-
-
-
-
+
+
+
+
+
@@ -210,7 +323,7 @@
0
20
1081
- 591
+ 551
@@ -235,136 +348,23 @@
Search something...
-
-
-
- Extract
-
-
+
- 9
- 9
+ 10
+ 580
1061
- 601
+ 31
-
+
-
-
-
-
-
-
- Output folder
-
-
-
- -
-
-
- true
-
-
-
- -
-
-
- Select
-
-
-
-
-
- -
-
-
-
-
-
- Output format
-
-
-
- -
-
-
-
-
-
-
-
-
- -
-
-
- Qt::Horizontal
+
+
+ Click on an audio file to get more infos !
- -
-
-
-
-
-
-
-
-
- Total progress
-
-
-
- -
-
-
- 0
-
-
-
-
-
- -
-
-
-
-
-
- Per file progress
-
-
-
- -
-
-
- 0
-
-
-
-
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- -
-
-
-
-
-
- Extract All
-
-
-
- -
-
-
- Extract Selected
-
-
-
-
-
@@ -373,9 +373,9 @@
10
- 640
+ 720
1081
- 131
+ 151
@@ -394,6 +394,54 @@
true
+
+
+
+ 16
+ 650
+ 1071
+ 61
+
+
+
+ -
+
+
-
+
+
+ Total progress
+
+
+
+ -
+
+
+ 0
+
+
+
+
+
+ -
+
+
-
+
+
+ Per file progress
+
+
+
+ -
+
+
+ 0
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -439,12 +520,66 @@
Exit
+
+
+ false
+
+
+ Extract All
+
+
+
+
+ false
+
+
+ Extract Selected
+
+
+
+
+ Report a bug
+
+
+
+
+ Source code
+
+
+
+
+ Discord
+
+
+
+
+ false
+
+
+ format
+
+
+ false
+
+
+
+
+ false
+
+
+ Expand all
+
+
+
+
+ false
+
+
+ Collapse all
+
+
- inputPath
- changeInput
- altInputPath
- changeAltInput
tabs
diff --git a/maps/hk4e.map b/maps/hk4e.map
index bf87b60..1876d0e 100644
Binary files a/maps/hk4e.map and b/maps/hk4e.map differ
diff --git a/maps/index.json b/maps/index.json
index 18adbb9..96d61ca 100644
--- a/maps/index.json
+++ b/maps/index.json
@@ -2,7 +2,7 @@
{
"name": "hk4e.map",
"game": "Genshin Impact",
- "version": "5.1"
+ "version": "5.2"
},
{
"name": "hkrpg.map",
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..0a0dd4b
--- /dev/null
+++ b/wwise.py
@@ -0,0 +1,187 @@
+# wwise riff header parser
+# thanks to hcs and bnnm work
+
+def parse_wwise(reader):
+ # default meta config
+ metadata = {
+ "format": 0,
+ "channels": 0,
+ "sampleRate": 0,
+ "avgBitrate": 0,
+ "blockSize": 0,
+ "bitsPerSample": 0,
+ "extraSize": 0,
+ "channelLayout": None,
+ "channelType": None,
+ "codec": None,
+ "codecDisplay": None,
+ "layoutType": None,
+ "interleaveBlockSize": None,
+ "numSamples": None,
+ "duration": 0
+ }
+
+ if reader.GetStreamLength() == 0:
+ print(f"[WARNING] null stream size at {reader.GetName()}, unreadable block")
+ return metadata
+
+ header = reader.ReadBytes(4)
+
+ # endian check header
+ if header == b"RIFX":
+ reader.endianness = "big"
+ elif header == b"RIFF":
+ reader.endianness = "little"
+ else:
+ print(f"[WARNING] invalid header {header} at {reader.GetName()}, assuming unreadable")
+ return metadata
+
+ # additional check
+ reader.SetBufferPos(0x08)
+ check = reader.ReadBytes(4)
+
+ if check != b"WAVE" and check != "XWMA":
+ print(f"[WARNING] invalid check mark {check}, assuming unreadable")
+ return metadata
+
+ # read chunks
+ reader.SetBufferPos(0x0C)
+
+ chunks = {}
+
+ while reader.GetBufferPos() < reader.GetStreamLength():
+ chunk_type = reader.ReadBytes(4)
+
+ if chunk_type not in [b"fmt ", b"JUNK", b"data", b"akd ", b"cue ", b"LIST", b"smpl"]:
+ print(f"[WARNING] unexpected chunk {chunk_type} at {reader.GetName()}")
+
+ formatted_chunk_type = chunk_type.decode("utf-8").replace(" ", "")
+ chunk_length = reader.ReadUInt32()
+
+ if chunk_length > reader.GetRemainingLength():
+ chunk_length = reader.GetRemainingLength()
+
+ chunks[formatted_chunk_type] = {
+ "length": chunk_length,
+ "offset": reader.GetBufferPos(),
+ "data": reader.ReadBytes(chunk_length)
+ }
+
+ # reader fmt header
+ fmt_length = chunks["fmt"]["length"]
+ if fmt_length < 0x10:
+ print(f"[WARNING] invalid fmt chunk length {fmt_length} at {reader.GetName()}, skipping")
+ return metadata
+
+ reader.SetBufferPos(chunks["fmt"]["offset"])
+
+ metadata["format"] = reader.ReadUInt16()
+ metadata["channels"] = reader.ReadUInt16()
+ metadata["sampleRate"] = reader.ReadUInt32()
+ metadata["avgBitrate"] = reader.ReadUInt32()
+ metadata["blockSize"] = reader.ReadUInt16()
+ metadata["bitsPerSample"] = reader.ReadUInt16()
+
+ 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:
+ print(f"[WARNING] XMA2WAVEFORMATEX in fmt at {reader.GetName()}")
+ return metadata
+
+ # 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 *mostly* PTADPCM
+ # hsr and zzz should be VORBIS
+
+ if metadata["format"] not in codecs:
+ print(f'[WARNING] unknown codec {metadata["format"]} at {reader.GetName()}')
+ return metadata
+
+ codec = codecs[metadata["format"]]
+
+ if codec not in ["PTADPCM", "VORBIS"]: # Platinum "PtADPCM" custom ADPCM for Wwise
+ print(f"[WARNING] unhandled codec {codec}, need to implement this later")
+
+ metadata["codec"] = codec
+
+ # codec name
+ codecs_names = {
+ "PTADPCM": "Platinum 4-bit ADPCM",
+ "VORBIS": "Custom Vorbis"
+ }
+
+ if codec in codecs_names:
+ metadata["codecDisplay"] = codecs_names[codec]
+ else:
+ metadata["codecDisplay"] = codec
+
+ # parse duration
+ 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"]
+
+ elif metadata["codec"] == "VORBIS":
+ if (metadata["blockSize"] != 0 or metadata["bitsPerSample"] != 0):
+ print(f"[WARNING] worbis type at {reader.GetName()}, skipping")
+ return metadata
+
+ 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
+
+ extra_offset = chunks["fmt"]["offset"] + 0x18
+
+ if metadata["extraSize"] != 0x30:
+ print(f"[WARNING] unknown extra wwise size at {reader.GetName()}, skipping")
+ return metadata
+
+ data_offset = 0x10
+ blocks_offset = 0x28
+ # define header to type 2, packet to modified and codebook to aoTuV603, required ?
+
+ metadata["numSamples"] = reader.ReadInt32(extra_offset)
+ setup_offset = reader.ReadUInt32(extra_offset + data_offset)
+ audio_offset = reader.ReadUInt32(extra_offset + data_offset + 0x04)
+
+ block_size_1_exp = reader.ReadUInt8(extra_offset + blocks_offset)
+ block_size_0_exp = reader.ReadUInt8(extra_offset + blocks_offset + 0x01)
+ # if both exp are equals and extra size is 0x30, then reset packet type to standard
+
+ chunks["data"]["offset"] -= audio_offset
+
+ # ignore packets update and codebooks parse attempts, not implemented
+ metadata["layoutType"] = "none"
+ metadata["duration"] = metadata["numSamples"] / metadata["sampleRate"]
+
+ return metadata