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 + + + + + + + @@ -412,7 +460,40 @@ + + + Extract + + + + Output format + + + + + + + + + + + Other + + + + + + + + View + + + + + + + @@ -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