From 5d78ad5b8ec88892f3b84466a886da5042e57f2f Mon Sep 17 00:00:00 2001 From: Rovetown <110688035+Rovetown@users.noreply.github.com> Date: Wed, 3 Sep 2025 14:05:26 +0200 Subject: [PATCH 1/8] Updated Bulk Sound Extraction and added updated conversion script --- .../SatisfactoryAudioRenamer/convert.py | 218 ++++++++++++++++-- .../pages/Development/ExtractGameFiles.adoc | 43 +++- 2 files changed, 236 insertions(+), 25 deletions(-) diff --git a/modules/ROOT/attachments/Development/SatisfactoryAudioRenamer/convert.py b/modules/ROOT/attachments/Development/SatisfactoryAudioRenamer/convert.py index 3164a3f6..dd4e4320 100644 --- a/modules/ROOT/attachments/Development/SatisfactoryAudioRenamer/convert.py +++ b/modules/ROOT/attachments/Development/SatisfactoryAudioRenamer/convert.py @@ -1,31 +1,215 @@ import os +import subprocess from pathlib import Path +import shutil +import logging -def convert(filename): +# Full path to vgmstream-cli.exe, usually in your FModel's Output Directory +# Example Path +VGMSTREAM = Path(r"C:/FModel/Output/.data/vgmstream-cli.exe") - my_file = open("./txtp/" + filename, "r") - data = my_file.read() +# Logs +MAIN_LOG = "conversion_main.log" +FAILED_LOG = "conversion_errors.log" - data_into_list = data.replace('\n', ' ').split(" ") +# Setup main logging +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(message)s", + handlers=[ + logging.FileHandler(MAIN_LOG, mode='w', encoding='utf-8'), + logging.StreamHandler() + ] +) +# Setup failed conversion logging (Will overwrite each run, could be replaced with RotatingFileHandler but needs script changes) +failed_logger = logging.getLogger("failed") +failed_handler = logging.FileHandler(FAILED_LOG, mode='w', encoding='utf-8') +failed_handler.setLevel(logging.ERROR) +failed_logger.addHandler(failed_handler) +failed_logger.propagate = False - for i in range(len(data_into_list)): - if data_into_list[i].startswith('wem'): +# Counters for summary +total_wems = 0 +converted_count = 0 +skipped_count = 0 +failed_count = 0 - wavname = "./txtp/" + data_into_list[i].split('.')[0] + '.wav' +# Step 1: Convert all .wem files into ./out_temp/wem/ (flat), mapping to digit folders +def wem_to_wav(input_root, temp_root): + global total_wems, converted_count, skipped_count, failed_count + input_root = Path(input_root) + temp_wem_root = Path(temp_root) / "wem" - if os.path.isfile(wavname): - os.rename(wavname, "./out/" + filename.split('.')[0] + '_' + str(i) + '.wav') + # CLEAN temp folder + if temp_wem_root.exists(): + shutil.rmtree(temp_wem_root) + temp_wem_root.mkdir(parents=True, exist_ok=True) + + mapping = {} # wav filename -> digit folder + + for folder, _, files in os.walk(input_root): + folder_path = Path(folder) + + # If we are in root (txtp/wem) use "root" as folder name + digit_folder = "root" if folder_path == input_root else folder_path.name + + for file in files: + ext = Path(file).suffix.lower() + base_name = Path(file).stem + wav_name = base_name + ".wav" + + wem_path = folder_path / file + wav_path = temp_wem_root / wav_name + mapping[wav_name] = digit_folder + + final_out_path = Path("out") / digit_folder / wav_name + if wav_path.exists() or final_out_path.exists(): + skipped_count += 1 + logging.info(f"Skipping existing WAV: {wav_path} or {final_out_path}") + continue + + if ext == ".wem": + # Convert wem → wav + logging.info(f"Converting: {wem_path} → {wav_path}") + result = subprocess.run( + [str(VGMSTREAM), "-o", str(wav_path), str(wem_path)], + capture_output=True, + text=True + ) + if result.returncode != 0 or not wav_path.exists(): + failed_count += 1 + logging.error(f"Conversion failed for {wem_path}: {result.stderr}") + failed_logger.error(str(wem_path)) + else: + converted_count += 1 + logging.info(f"Converted {wem_path} successfully") + + elif ext == ".wav": + # Copy pre-existing wav into temp for rename step + try: + shutil.copy2(wem_path, wav_path) + skipped_count += 1 + logging.info(f"Using existing WAV instead of converting: {wem_path} → {wav_path}") + except Exception as e: + failed_count += 1 + logging.error(f"Failed to copy existing WAV {wem_path}: {e}") + failed_logger.error(str(wem_path)) + return mapping + +# Step 2: Rename .wav files based on .txtp references +def convert(filename, wav_root, out_root, mapping): + wav_root = Path(wav_root) + out_root = Path(out_root) + txtp_path = Path("txtp") / filename + + try: + with open(txtp_path, "r", encoding='utf-8') as my_file: + data = my_file.read() + except Exception as e: + logging.error(f"Failed to read {txtp_path}: {e}") + return + + tokens = data.replace('\n', ' ').split(" ") + + for i, token in enumerate(tokens): + if token.startswith('wem'): + wav_file_only = Path(token).stem + ".wav" + wavname = wav_root / wav_file_only + digit_folder = mapping.get(wavname.name, "unknown") + out_folder = out_root / digit_folder + out_folder.mkdir(parents=True, exist_ok=True) + new_name = out_folder / f"{filename.split('.')[0]}_{i}.wav" + + if new_name.exists(): + logging.info(f"Skipping already renamed WAV: {new_name}") + continue + + if wavname.exists(): + try: + shutil.move(str(wavname), str(new_name)) + logging.info(f"Renamed {wavname} → {new_name}") + except Exception as e: + logging.error(f"Failed to rename {wavname}: {e}") else: - print(wavname + " not found.") + logging.warning(f"{wavname} not found.") + +# Step 3: Retry failed conversions +def retry_failed_conversions(temp_wav_root): + global converted_count, failed_count + failed_path = Path(FAILED_LOG) + if not failed_path.exists(): + logging.info("No failed conversions to retry.") + return + + logging.info("Retrying failed conversions...") + + # Read and truncate the failed log for this retry + with open(failed_path, "r+", encoding="utf-8") as f: + failed_files = [line.strip() for line in f.readlines() if line.strip()] + f.seek(0) + f.truncate(0) + + new_failures = 0 # counter for files that fail again + + for wem_path_str in failed_files: + wem_path = Path(wem_path_str) + wav_name = wem_path.stem + ".wav" + wav_path = temp_wav_root / wav_name + + if wav_path.exists(): + logging.info(f"Skipping existing WAV: {wav_path}") + continue + + logging.info(f"Retrying conversion: {wem_path} → {wav_path}") + result = subprocess.run( + [str(VGMSTREAM), "-o", str(wav_path), str(wem_path)], + capture_output=True, + text=True + ) + if result.returncode != 0 or not wav_path.exists(): + new_failures += 1 + logging.error( + f"Conversion failed a 2nd time: {wem_path}. " + "Either the .wem file is corrupt, broken, or there is no .txtp path for that file. " + "Consider a manual approach or ask for help in the Discord." + ) + failed_logger.error(str(wem_path)) + else: + # Count as converted only if it actually succeeds now + converted_count += 1 + logging.info(f"Successfully converted on retry: {wem_path}") + + # Update failed_count to reflect files that truly failed after retry + failed_count = new_failures + +# Main driver +if __name__ == "__main__": + wem_root = Path("txtp/wem") + wav_temp_root = Path("out_temp") / "wem" + out_root = Path("out") + + logging.info("Starting .wem → .wav conversion") + mapping = wem_to_wav(wem_root, Path("out_temp")) - my_file.close() + logging.info("Starting .wav renaming based on .txtp files") + txtp_files = [f for f in Path("txtp").glob("*.txtp")] + for file_path in txtp_files: + convert(file_path.name, wav_temp_root, out_root, mapping) -relevant_path = "./txtp/" -included_extensions = ['txtp'] -file_names = [fn for fn in os.listdir(relevant_path) - if any(fn.endswith(ext) for ext in included_extensions)] + # Retry any failed conversions + retry_failed_conversions(wav_temp_root) + # Clean up temp folder + if wav_temp_root.parent.exists(): + shutil.rmtree(wav_temp_root.parent) + logging.info(f"Temporary folder {wav_temp_root.parent} deleted") -for file_name in file_names: - convert(file_name) \ No newline at end of file + # Final summary + logging.info("===================================") + logging.info(f"Total .wem files found: {total_wems}") + logging.info(f"Successfully converted: {converted_count}") + logging.info(f"Skipped (already exists): {skipped_count}") + logging.info(f"Failed conversions: {failed_count}") + logging.info("Conversion and renaming complete") + logging.info("===================================") diff --git a/modules/ROOT/pages/Development/ExtractGameFiles.adoc b/modules/ROOT/pages/Development/ExtractGameFiles.adoc index 6e60f495..962ade86 100644 --- a/modules/ROOT/pages/Development/ExtractGameFiles.adoc +++ b/modules/ROOT/pages/Development/ExtractGameFiles.adoc @@ -104,7 +104,7 @@ Press `OK` to save your changes. image:ExtractingGameFiles/FModelModelSettings.png[FModel Model Export Settings] -[WARNING] +[WARNING] ==== Any other changes made are at user discretion, and dependent on level of knowledge. ==== @@ -245,7 +245,7 @@ go back to grab the `wwnames.db3` from the GitHub release and put it in the same // cspell:ignore txtp Next, select `Generate TXTP` which will create a folder in the same directory as the bnk file containing a txtp file for the event. -// Need the + symbols to make sure Asciidoc doesn't see them as attributes +// Need the + symbols to make sure Asciidoc doesn't see them as attributes (ex. `+Play_EQ_JetPack_Activate {s} {m}.txtp+`) Open the txtp file in a text editor of your choice. @@ -272,6 +272,14 @@ After optionally previewing the sound file in the player, right click on it in the player's playlist and select Save, prompting a system dialog to select a save location. +[TIP] +==== +You can also select the entire `Media` folder to extract all sound files at once. It will export the Audio Files as `.wem` files. +This will be useful if you need to extract a large number of sounds quickly and works great with the Script mentioned further down. + +Be aware though that this method will extract all sounds, including those you may not need, and can result in file sizes exceeding 5 GB. If you only need specific sounds, it's recommended to extract them individually. +==== + [WARNING] ==== Some users have reported issues with FModel's audio player, @@ -290,17 +298,36 @@ if that didn't work (mod developer discord role required to view). === Bulk Audio Renamer Community member MrCheese has created a python script that enables mass renaming of exported wem files to their associated named bnk files. -If you decide to extract a large number of sounds, this script can save you a lot of time. - -To use it: +This has since been enhanced by community member Rovetown to fully automate the process. If you decide to extract a large number of sounds, this script can save you a lot of time. 1. Create a folder somewhere named `SatisfactoryAudioRenamer`. 2. Create a subfolder named `out` 3. Create a subfolder named `txtp` -4. link:{attachmentsdir}/Development/SatisfactoryAudioRenamer/convert.py[Download this python file (convert.py)] - and place it in the SatisfactoryAudioRenamer folder -5. Move all the txtp files that wwiser generated earlier to the txtp subfolder +4. Create a subfolder inside `txtp` named `wem` +5. Place the extracted `.wem` files (including their parent folder if you decided to extract the full folder structure) into the `wem` subfolder. +6. link:{attachmentsdir}/Development/SatisfactoryAudioRenamer/convert.py[Download this python file (convert.py)] + and place it in the SatisfactoryAudioRenamer folder. +7. Be sure to change the `VGMSTREAM` Path inside the downloaded `converter.py` to the location of your `vgmstream-cli.exe` +8. Move all the txtp files that wwiser generated earlier to the txtp subfolder and run `python .\convert.py` from a terminal in that SatisfactoryAudioRenamer folder. +9. You now have the renamed `.wav` files in the `out` subfolder (and in their original folder structure). + +[NOTE] +==== +The Script will try and rerun if it encounters any errors. +However, it may not always succeed in properly renaming all files. +This can be due to several reasons, so if you encounter this, feel free to ask for help in the discord. + +If you get any Errors regarding Python Libraries, make sure you have all the required dependencies installed. +These are: + +* os +* subprocess +* pathlib +* shutil +* logging + +==== == Generating a Complete Starter Project From 3e6e867a33eb9ba9dc6768f73be2b17807e1f39e Mon Sep 17 00:00:00 2001 From: Rob B Date: Sun, 9 Nov 2025 18:45:18 -0500 Subject: [PATCH 2/8] Remove note about python dependencies as they are all core libraries --- .../ROOT/pages/Development/ExtractGameFiles.adoc | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/modules/ROOT/pages/Development/ExtractGameFiles.adoc b/modules/ROOT/pages/Development/ExtractGameFiles.adoc index 962ade86..1a00b9d6 100644 --- a/modules/ROOT/pages/Development/ExtractGameFiles.adoc +++ b/modules/ROOT/pages/Development/ExtractGameFiles.adoc @@ -314,19 +314,9 @@ This has since been enhanced by community member Rovetown to fully automate the [NOTE] ==== -The Script will try and rerun if it encounters any errors. +The script will try and rerun if it encounters any errors. However, it may not always succeed in properly renaming all files. -This can be due to several reasons, so if you encounter this, feel free to ask for help in the discord. - -If you get any Errors regarding Python Libraries, make sure you have all the required dependencies installed. -These are: - -* os -* subprocess -* pathlib -* shutil -* logging - +This can be due to several reasons, so if you encounter this, feel free to ask for help in the Discord. ==== == Generating a Complete Starter Project From 4386e304b06b0def97b6db0cc8b0a5e69bd1a703 Mon Sep 17 00:00:00 2001 From: Rob B Date: Sun, 9 Nov 2025 19:00:10 -0500 Subject: [PATCH 3/8] Update conversion script: - Format with Black - Create subfolders for you - Check and error early if VGSTREAM does not exist - Check output of subprocesses --- .../SatisfactoryAudioRenamer/convert.py | 55 ++++++++++++++----- .../pages/Development/ExtractGameFiles.adoc | 18 +++--- 2 files changed, 49 insertions(+), 24 deletions(-) diff --git a/modules/ROOT/attachments/Development/SatisfactoryAudioRenamer/convert.py b/modules/ROOT/attachments/Development/SatisfactoryAudioRenamer/convert.py index dd4e4320..fe5c2cd9 100644 --- a/modules/ROOT/attachments/Development/SatisfactoryAudioRenamer/convert.py +++ b/modules/ROOT/attachments/Development/SatisfactoryAudioRenamer/convert.py @@ -1,13 +1,17 @@ +# pylint: disable=logging-fstring-interpolation, global-statement, missing-function-docstring, missing-module-docstring, broad-exception-caught import os import subprocess from pathlib import Path import shutil import logging +import sys # Full path to vgmstream-cli.exe, usually in your FModel's Output Directory # Example Path VGMSTREAM = Path(r"C:/FModel/Output/.data/vgmstream-cli.exe") +# ================================================================================================== + # Logs MAIN_LOG = "conversion_main.log" FAILED_LOG = "conversion_errors.log" @@ -17,14 +21,29 @@ level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s", handlers=[ - logging.FileHandler(MAIN_LOG, mode='w', encoding='utf-8'), - logging.StreamHandler() - ] + logging.FileHandler(MAIN_LOG, mode="w", encoding="utf-8"), + logging.StreamHandler(), + ], ) + +# Set up subfolders +currentDirectory = Path(os.path.dirname(os.path.abspath(__file__))) +(currentDirectory / "out").mkdir(exist_ok=True) +(currentDirectory / "txtp" / "wem").mkdir(parents=True, exist_ok=True) + +# Check if vgmstream exists +if not VGMSTREAM.exists(): + logging.critical( + f"vgmstream-cli.exe not found at {VGMSTREAM}. It should have been downloaded by FModel. Edit the script if necessary to change the path." + ) + sys.exit(1) +else: + logging.info(f"Using vgmstream-cli.exe at {VGMSTREAM}") + # Setup failed conversion logging (Will overwrite each run, could be replaced with RotatingFileHandler but needs script changes) failed_logger = logging.getLogger("failed") -failed_handler = logging.FileHandler(FAILED_LOG, mode='w', encoding='utf-8') +failed_handler = logging.FileHandler(FAILED_LOG, mode="w", encoding="utf-8") failed_handler.setLevel(logging.ERROR) failed_logger.addHandler(failed_handler) failed_logger.propagate = False @@ -35,9 +54,10 @@ skipped_count = 0 failed_count = 0 + # Step 1: Convert all .wem files into ./out_temp/wem/ (flat), mapping to digit folders def wem_to_wav(input_root, temp_root): - global total_wems, converted_count, skipped_count, failed_count + global converted_count, skipped_count, failed_count input_root = Path(input_root) temp_wem_root = Path(temp_root) / "wem" @@ -46,7 +66,7 @@ def wem_to_wav(input_root, temp_root): shutil.rmtree(temp_wem_root) temp_wem_root.mkdir(parents=True, exist_ok=True) - mapping = {} # wav filename -> digit folder + wav_filename_to_digit_folder = {} for folder, _, files in os.walk(input_root): folder_path = Path(folder) @@ -61,7 +81,7 @@ def wem_to_wav(input_root, temp_root): wem_path = folder_path / file wav_path = temp_wem_root / wav_name - mapping[wav_name] = digit_folder + wav_filename_to_digit_folder[wav_name] = digit_folder final_out_path = Path("out") / digit_folder / wav_name if wav_path.exists() or final_out_path.exists(): @@ -75,7 +95,8 @@ def wem_to_wav(input_root, temp_root): result = subprocess.run( [str(VGMSTREAM), "-o", str(wav_path), str(wem_path)], capture_output=True, - text=True + text=True, + check=True, ) if result.returncode != 0 or not wav_path.exists(): failed_count += 1 @@ -90,12 +111,15 @@ def wem_to_wav(input_root, temp_root): try: shutil.copy2(wem_path, wav_path) skipped_count += 1 - logging.info(f"Using existing WAV instead of converting: {wem_path} → {wav_path}") + logging.info( + f"Using existing WAV instead of converting: {wem_path} → {wav_path}" + ) except Exception as e: failed_count += 1 logging.error(f"Failed to copy existing WAV {wem_path}: {e}") failed_logger.error(str(wem_path)) - return mapping + return wav_filename_to_digit_folder + # Step 2: Rename .wav files based on .txtp references def convert(filename, wav_root, out_root, mapping): @@ -104,16 +128,16 @@ def convert(filename, wav_root, out_root, mapping): txtp_path = Path("txtp") / filename try: - with open(txtp_path, "r", encoding='utf-8') as my_file: + with open(txtp_path, "r", encoding="utf-8") as my_file: data = my_file.read() except Exception as e: logging.error(f"Failed to read {txtp_path}: {e}") return - tokens = data.replace('\n', ' ').split(" ") + tokens = data.replace("\n", " ").split(" ") for i, token in enumerate(tokens): - if token.startswith('wem'): + if token.startswith("wem"): wav_file_only = Path(token).stem + ".wav" wavname = wav_root / wav_file_only digit_folder = mapping.get(wavname.name, "unknown") @@ -134,6 +158,7 @@ def convert(filename, wav_root, out_root, mapping): else: logging.warning(f"{wavname} not found.") + # Step 3: Retry failed conversions def retry_failed_conversions(temp_wav_root): global converted_count, failed_count @@ -165,7 +190,8 @@ def retry_failed_conversions(temp_wav_root): result = subprocess.run( [str(VGMSTREAM), "-o", str(wav_path), str(wem_path)], capture_output=True, - text=True + text=True, + check=True, ) if result.returncode != 0 or not wav_path.exists(): new_failures += 1 @@ -183,6 +209,7 @@ def retry_failed_conversions(temp_wav_root): # Update failed_count to reflect files that truly failed after retry failed_count = new_failures + # Main driver if __name__ == "__main__": wem_root = Path("txtp/wem") diff --git a/modules/ROOT/pages/Development/ExtractGameFiles.adoc b/modules/ROOT/pages/Development/ExtractGameFiles.adoc index 1a00b9d6..dc116bde 100644 --- a/modules/ROOT/pages/Development/ExtractGameFiles.adoc +++ b/modules/ROOT/pages/Development/ExtractGameFiles.adoc @@ -301,16 +301,14 @@ Community member MrCheese has created a python script that enables mass renaming This has since been enhanced by community member Rovetown to fully automate the process. If you decide to extract a large number of sounds, this script can save you a lot of time. 1. Create a folder somewhere named `SatisfactoryAudioRenamer`. -2. Create a subfolder named `out` -3. Create a subfolder named `txtp` -4. Create a subfolder inside `txtp` named `wem` -5. Place the extracted `.wem` files (including their parent folder if you decided to extract the full folder structure) into the `wem` subfolder. -6. link:{attachmentsdir}/Development/SatisfactoryAudioRenamer/convert.py[Download this python file (convert.py)] - and place it in the SatisfactoryAudioRenamer folder. -7. Be sure to change the `VGMSTREAM` Path inside the downloaded `converter.py` to the location of your `vgmstream-cli.exe` -8. Move all the txtp files that wwiser generated earlier to the txtp subfolder - and run `python .\convert.py` from a terminal in that SatisfactoryAudioRenamer folder. -9. You now have the renamed `.wav` files in the `out` subfolder (and in their original folder structure). +2. link:{attachmentsdir}/Development/SatisfactoryAudioRenamer/convert.py[Download this python file (convert.py)] + and place it in the folder. +3. Run the script once (`python .\convert.py`) to verify it can find your vgmstream-cli.exe and create the required subfolders. + Edit the script's definition for `VGMSTREAM` if required to point to your vgmstream-cli.exe location. +4. Place the extracted `.wem` files (including their parent folder if you decided to extract the full folder structure) into the `SatisfactoryAudioRenamer/txtp/wem` folder. +5. Move all the txtp files that wwiser generated earlier to the `SatisfactoryAudioRenamer/txtp` subfolder +6. Run `python .\convert.py` from a terminal in the SatisfactoryAudioRenamer folder. +7. You now have the renamed `.wav` files in the `out` subfolder (and in their original folder structure). [NOTE] ==== From 77d8b25a5b0f7bc2a536945f3472d92a84445598 Mon Sep 17 00:00:00 2001 From: Rob B Date: Sun, 9 Nov 2025 19:00:48 -0500 Subject: [PATCH 4/8] Modern FModel correctly labels raw data type --- modules/ROOT/pages/Development/ExtractGameFiles.adoc | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/modules/ROOT/pages/Development/ExtractGameFiles.adoc b/modules/ROOT/pages/Development/ExtractGameFiles.adoc index dc116bde..e3a9f3c2 100644 --- a/modules/ROOT/pages/Development/ExtractGameFiles.adoc +++ b/modules/ROOT/pages/Development/ExtractGameFiles.adoc @@ -210,8 +210,7 @@ so you can search for `Play something bnk` to find a list of valid sound bnk fil Once you have found the bnk you want to extract (for example, `FactoryGame/Content/WwiseAudio/Event/19/Play_EQ_JetPack_Activate.bnk`) -right click on it in FModel's "Packages" tab list and select `Export Raw Data (.uasset)`, -which will export the bnk file despite the tooltip option claiming it will be a uasset. +right click on it in FModel's "Packages" tab list and select `Export Raw Data`. Alternatively, you can extract the entire `Event` folder at once by right clicking on it in the "Folders" tab and selecting `Export Folder's Packages Raw Data (.uasset)`. From 7ff5f39b00d3a76c96fe13f5d8226fa503fba62c Mon Sep 17 00:00:00 2001 From: Rob B Date: Sun, 9 Nov 2025 20:08:04 -0500 Subject: [PATCH 5/8] Fix conversion script check behavior --- .../Development/SatisfactoryAudioRenamer/convert.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/ROOT/attachments/Development/SatisfactoryAudioRenamer/convert.py b/modules/ROOT/attachments/Development/SatisfactoryAudioRenamer/convert.py index fe5c2cd9..ff4b5d38 100644 --- a/modules/ROOT/attachments/Development/SatisfactoryAudioRenamer/convert.py +++ b/modules/ROOT/attachments/Development/SatisfactoryAudioRenamer/convert.py @@ -96,7 +96,7 @@ def wem_to_wav(input_root, temp_root): [str(VGMSTREAM), "-o", str(wav_path), str(wem_path)], capture_output=True, text=True, - check=True, + check=False, ) if result.returncode != 0 or not wav_path.exists(): failed_count += 1 @@ -191,7 +191,7 @@ def retry_failed_conversions(temp_wav_root): [str(VGMSTREAM), "-o", str(wav_path), str(wem_path)], capture_output=True, text=True, - check=True, + check=False, ) if result.returncode != 0 or not wav_path.exists(): new_failures += 1 From 509b58a0b87ab229d68eb8ca16184c9893121c60 Mon Sep 17 00:00:00 2001 From: Rob B Date: Sun, 9 Nov 2025 20:08:41 -0500 Subject: [PATCH 6/8] Increase site-wide Table of Contents max nesting level to 3 --- antora.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/antora.yml b/antora.yml index f1568a0e..cf5edf81 100644 --- a/antora.yml +++ b/antora.yml @@ -8,6 +8,8 @@ asciidoc: # Custom text replacements. Built-in ones can be found here: # https://github.com/asciidoctor/asciidoctor/blob/917d3800a08a8f283a8d05beb08bb75af1673de5/lib/asciidoctor.rb#L391 attributes: + # Increase max table of content nesting levels from 2 to 3 https://docs.antora.org/antora/latest/page/page-attributes/#access-attributes-from-ui-template + page-toclevels: 3@ # Uses zero-width joiner (U+200D) to avoid being split across lines cpp: C‍+‍+ # Use backslash without it escaping things in other situations From a6af5aff27d356387986efbc16f97df05610fa86 Mon Sep 17 00:00:00 2001 From: Rob B Date: Sun, 9 Nov 2025 20:09:47 -0500 Subject: [PATCH 7/8] Add `zero-width-join` asciidoc attribute --- antora.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/antora.yml b/antora.yml index cf5edf81..233b1ff3 100644 --- a/antora.yml +++ b/antora.yml @@ -10,9 +10,11 @@ asciidoc: attributes: # Increase max table of content nesting levels from 2 to 3 https://docs.antora.org/antora/latest/page/page-attributes/#access-attributes-from-ui-template page-toclevels: 3@ + # Zero-width joiner (U+200D) to avoid being split across lines + zero-width-join: ‍ # Uses zero-width joiner (U+200D) to avoid being split across lines cpp: C‍+‍+ # Use backslash without it escaping things in other situations literal-backslash: \ # To keep paths from breaking across lines - NoBreakBackslash: ‍\‍ \ No newline at end of file + NoBreakBackslash: ‍\‍ From 0fa13cf24e44d37d2aacd10b111931aeb71e8df7 Mon Sep 17 00:00:00 2001 From: Rob B Date: Sun, 9 Nov 2025 21:11:26 -0500 Subject: [PATCH 8/8] Additional ExtractGameFiles headings. Update bulk audio renamer directions to new script behavior. --- cspell.json | 1 + .../pages/Development/ExtractGameFiles.adoc | 117 +++++++++++------- 2 files changed, 72 insertions(+), 46 deletions(-) diff --git a/cspell.json b/cspell.json index 11dcb265..1dc77caa 100644 --- a/cspell.json +++ b/cspell.json @@ -109,6 +109,7 @@ "structs", "subfolder", "subfolders", + "toclevels", "Tolgee", "Treelo", "uasset", diff --git a/modules/ROOT/pages/Development/ExtractGameFiles.adoc b/modules/ROOT/pages/Development/ExtractGameFiles.adoc index e3a9f3c2..db6c0e3e 100644 --- a/modules/ROOT/pages/Development/ExtractGameFiles.adoc +++ b/modules/ROOT/pages/Development/ExtractGameFiles.adoc @@ -204,6 +204,9 @@ You may still find the `Events` folder useful for leaning the context of sound e For example, `/Events/World_Events_FilatovD/Environment/Caves/` presumably contains environmental sounds that would play while in caves. +[id="bnk_single"] +==== Extracting a Specific `.bnk` + FModel's link:#_searching_for_files[Package Search functionality] is beneficial for finding specific sounds. Events that begin sound playback follow the naming scheme `Play_something.bnk`, so you can search for `Play something bnk` to find a list of valid sound bnk files containing `something`. @@ -212,55 +215,73 @@ Once you have found the bnk you want to extract (for example, `FactoryGame/Content/WwiseAudio/Event/19/Play_EQ_JetPack_Activate.bnk`) right click on it in FModel's "Packages" tab list and select `Export Raw Data`. -Alternatively, you can extract the entire `Event` folder at once -by right clicking on it in the "Folders" tab and selecting `Export Folder's Packages Raw Data (.uasset)`. +Click on the text in the FModel log (ex. `Play_EQ_JetPack_Activate.bnk`) to open your system's file browser to the folder that contains the exported bnk. + +[id="bnk_all"] +==== Extracting All Sound `.bnk`{zero-width-join}s + +You can extract all bnk files from the `Event` folder +by right clicking on it in the "Folders" tab and selecting `Export Folder's Packages Raw Data`. -Click on the text -(ex. `Play_EQ_JetPack_Activate.bnk`) -in the FModel log to open your system's file browser to the folder that contains the exported bnk, -or the top-level export folder if you exported the entire `Event` folder. +The directory folder structure will be preserved. +Clicking the text in FModel's log will take you to your FModel Output folder. === Extracting sourceIDs with wwiser +// cspell:ignore txtp +SourceIDs are the unique numbers that associate a bnk file with the sound files it uses. +The tool wwiser can extract metadata, in the form of a `.txtp` file, from a bnk file. + You will need Python installed to utilize wwiser. Python 3.8.10 is known to work, and https://github.com/pyenv-win/pyenv-win[pyenv] is the suggested method of install. Installing python is out of the scope of this guide. -Download the latest copy of wwiser from its https://github.com/bnnm/wwiser/releases[releases page]. // cspell:ignore wwnames -You'll want both the `wwiser.pyz` and `wwnames.db3` files from the release. - -Use a zip extracting program of your choice to extract the files from `wwiser.pyz` -and place the `wwnames.db3` file in the same directory as the extracted files. - -To open the wwiser interface, run `python .\wwiser.py` in a terminal in that folder. -Next, select `Load banks...` and select the bnk file you extracted earlier. - -Check wwiser's log panel before continuing. -If it contains the message `names: couldn't find .\wwnames.db3 name file`, -go back to grab the `wwnames.db3` from the GitHub release and put it in the same folder as the bnk file you opened. - -// cspell:ignore txtp -Next, select `Generate TXTP` which will create a folder in the same directory as the bnk file -containing a txtp file for the event. +1. Download the latest copy of wwiser from its https://github.com/bnnm/wwiser/releases[releases page]. + You'll want both the `wwiser.pyz` and `wwnames.db3` files from the release. + If the release doesn't have a `wwnames.db3` file, grab one from a previous release. +2. Use a zip extracting program of your choice to extract the files from `wwiser.pyz` + and place the `wwnames.db3` file in the same directory as the extracted files. +3. Open the wwiser interface by running `python .\wwiser.py` in a terminal in that folder. + +[id="txtp_single"] +==== Generating a Specific `.txtp`{zero-width-join} + +1. In wwiser, click `Load banks...` and select the bnk file you extracted earlier. +2. Check wwiser's log panel before continuing. + If it contains the message `names: couldn't find .\wwnames.db3 name file`, + go back to grab the `wwnames.db3` from the GitHub release and put it in the same folder as the bnk file you opened. +3. Click `Generate TXTP` which will create a `txtp` subfolder in the same directory as the bnk file + containing a txtp file for the event // Need the + symbols to make sure Asciidoc doesn't see them as attributes -(ex. `+Play_EQ_JetPack_Activate {s} {m}.txtp+`) - -Open the txtp file in a text editor of your choice. -If the bnk is linked to any sound files -their sourceID numbers will be displayed at the top of the file -(ex. `wem/633850317.wem` has the sourceID number `633850317`) -along with additional audio information. + (ex. `+Play_EQ_JetPack_Activate {s} {m}.txtp+`). +4. Open the txtp file in a text editor of your choice. + If the bnk is linked to any sound files + their _sourceID numbers_ will be displayed at the top of the file + (ex. `wem/633850317.wem` has the sourceID number `633850317`) + along with additional audio information. Sound events typically consistent of multiple sounds played at different volumes. Take note of all the sourceIDs of the event as you will likely need to review a few raw sounds to find the exact one you're looking for. +[id="txtp_all"] +==== Generating `.txtp`{zero-width-join}s for All Sounds + +If you extracted the entire Event folder earlier, +use wwiser's `Load dirs...` button instead of `Load banks...` button. +Select the `Event` folder in the FModel export hierarchy. + +The `txtp/` folder will be created inside the `Event` folder. + === Extracting Sound Files Now that we have sourceIDs we can use FModel to locate and extract their corresponding sound files. +[id="wem_single"] +==== Extracting a Specific `.wem` + First, locate the sound file in FModel via its sourceID, which will be its package file name. Using FModel's link:#_searching_for_files[Package Search functionality] is beneficial here. All game audio can be found in subfolders of the pak's (note - NOT the utoc!) `FactoryGame/Content/WwiseAudio/Media/` folder. @@ -271,14 +292,6 @@ After optionally previewing the sound file in the player, right click on it in the player's playlist and select Save, prompting a system dialog to select a save location. -[TIP] -==== -You can also select the entire `Media` folder to extract all sound files at once. It will export the Audio Files as `.wem` files. -This will be useful if you need to extract a large number of sounds quickly and works great with the Script mentioned further down. - -Be aware though that this method will extract all sounds, including those you may not need, and can result in file sizes exceeding 5 GB. If you only need specific sounds, it's recommended to extract them individually. -==== - [WARNING] ==== Some users have reported issues with FModel's audio player, @@ -294,20 +307,32 @@ https://discord.com/channels/555424930502541343/1036634533077979146/128694206712 if that didn't work (mod developer discord role required to view). ==== +[id="wem_all"] +==== Extracting All Sound `.wem`{zero-width-join}s + +You can also select the entire `Media` folder to extract all sound files to .`wem` files at once. +Right click on it and select `Save Folder's Packages Audio`. + +Be aware though that this method will extract ALL sounds and can consume more than 5 GB of disk space. +If you only need specific sounds, it's recommended to extract them individually. + +[id="BulkAudioRenamer"] === Bulk Audio Renamer -Community member MrCheese has created a python script that enables mass renaming of exported wem files to their associated named bnk files. -This has since been enhanced by community member Rovetown to fully automate the process. If you decide to extract a large number of sounds, this script can save you a lot of time. +Community members MrCheese and Rovetown have created a python script that enables mass renaming of exported wem files to their associated named bnk files. +If you decide to extract a large number of sounds, this script can save you a lot of time. -1. Create a folder somewhere named `SatisfactoryAudioRenamer`. -2. link:{attachmentsdir}/Development/SatisfactoryAudioRenamer/convert.py[Download this python file (convert.py)] +1. Follow the directions in the headings above to obtain the required `.txtp` files from wwiser and `.wem` files from FModel. +2. Create a folder somewhere named `SatisfactoryAudioRenamer`. +3. link:{attachmentsdir}/Development/SatisfactoryAudioRenamer/convert.py[Download this python file (convert.py)] and place it in the folder. -3. Run the script once (`python .\convert.py`) to verify it can find your vgmstream-cli.exe and create the required subfolders. +4. Run the script once (`python .\convert.py`) to verify it can find your vgmstream-cli.exe and create the required subfolders. Edit the script's definition for `VGMSTREAM` if required to point to your vgmstream-cli.exe location. -4. Place the extracted `.wem` files (including their parent folder if you decided to extract the full folder structure) into the `SatisfactoryAudioRenamer/txtp/wem` folder. -5. Move all the txtp files that wwiser generated earlier to the `SatisfactoryAudioRenamer/txtp` subfolder -6. Run `python .\convert.py` from a terminal in the SatisfactoryAudioRenamer folder. -7. You now have the renamed `.wav` files in the `out` subfolder (and in their original folder structure). +5. Move all the txtp files that wwiser generated to the `SatisfactoryAudioRenamer/txtp` subfolder +6. Place the extracted `.wem` files (including their parent `Media` folder if you decided to extract the full folder structure) + into the `SatisfactoryAudioRenamer/txtp/wem` folder. +7. Run `python .\convert.py` from a terminal in the SatisfactoryAudioRenamer folder. +8. You now have the renamed `.wav` files in the `out` subfolder (and in their original folder structure). [NOTE] ====