From 500fa59f852ff95f5e80edc807862fe49c0cb1bd Mon Sep 17 00:00:00 2001 From: Masusder <59669685+Masusder@users.noreply.github.com> Date: Wed, 18 Feb 2026 17:40:15 +0100 Subject: [PATCH] Option for bulk audio conversion (#646) --- CUE4Parse | 2 +- FModel/Settings/UserSettings.cs | 7 +++ FModel/ViewModels/AudioPlayerViewModel.cs | 2 +- FModel/ViewModels/CUE4ParseViewModel.cs | 59 ++++++++++++------- FModel/ViewModels/Commands/MenuCommand.cs | 2 +- FModel/ViewModels/GameFileViewModel.cs | 12 +++- .../Controls/Rtb/CustomRichTextBox.cs | 6 ++ FModel/Views/SettingsView.xaml | 36 ++++++++--- 8 files changed, 91 insertions(+), 35 deletions(-) diff --git a/CUE4Parse b/CUE4Parse index a451f8e3..1861416d 160000 --- a/CUE4Parse +++ b/CUE4Parse @@ -1 +1 @@ -Subproject commit a451f8e3140c806da8b438e9b44dafe4c1c0a37b +Subproject commit 1861416d2ab765cde91eec6f46305fc316dfae6e diff --git a/FModel/Settings/UserSettings.cs b/FModel/Settings/UserSettings.cs index d3d83f3b..f6a77b19 100644 --- a/FModel/Settings/UserSettings.cs +++ b/FModel/Settings/UserSettings.cs @@ -266,6 +266,13 @@ namespace FModel.Settings set => SetProperty(ref _readShaderMaps, value); } + private bool _convertAudioOnBulkExport; + public bool ConvertAudioOnBulkExport + { + get => _convertAudioOnBulkExport; + set => SetProperty(ref _convertAudioOnBulkExport, value); + } + private IDictionary _perDirectory = new Dictionary(); public IDictionary PerDirectory { diff --git a/FModel/ViewModels/AudioPlayerViewModel.cs b/FModel/ViewModels/AudioPlayerViewModel.cs index edd717b4..d12b2e7b 100644 --- a/FModel/ViewModels/AudioPlayerViewModel.cs +++ b/FModel/ViewModels/AudioPlayerViewModel.cs @@ -655,7 +655,7 @@ public class AudioPlayerViewModel : ViewModel, ISource, IDisposable } private bool TryConvert(out string wavFilePath) => TryConvert(SelectedAudioFile.FilePath, SelectedAudioFile.Data, out wavFilePath); - private bool TryConvert(string inputFilePath, byte[] inputFileData, out string wavFilePath) + public static bool TryConvert(string inputFilePath, byte[] inputFileData, out string wavFilePath) { wavFilePath = string.Empty; var vgmFilePath = Path.Combine(UserSettings.Default.OutputDirectory, ".data", "test.exe"); diff --git a/FModel/ViewModels/CUE4ParseViewModel.cs b/FModel/ViewModels/CUE4ParseViewModel.cs index 56f5cdff..18e74bab 100644 --- a/FModel/ViewModels/CUE4ParseViewModel.cs +++ b/FModel/ViewModels/CUE4ParseViewModel.cs @@ -780,7 +780,7 @@ public class CUE4ParseViewModel : ViewModel case "bank": { var archive = entry.CreateReader(); - if (!FModProvider.TryLoadBank(archive, entry.NameWithoutExtension, out var fmodReader)) + if (!FmodProvider.TryLoadBank(archive, entry.NameWithoutExtension, out var fmodReader)) { Log.Error($"Failed to load FMOD bank {entry.Path}"); break; @@ -792,7 +792,7 @@ public class CUE4ParseViewModel : ViewModel var directory = Path.GetDirectoryName(entry.Path) ?? "/FMOD/Desktop/"; foreach (var sound in extractedSounds) { - SaveAndPlaySound(Path.Combine(directory, sound.Name), sound.Extension, sound.Data, saveAudio); + SaveAndPlaySound(Path.Combine(directory, sound.Name), sound.Extension, sound.Data, saveAudio, updateUi); } break; @@ -807,7 +807,7 @@ public class CUE4ParseViewModel : ViewModel var medias = WwiseProvider.ExtractBankSounds(wwise); foreach (var media in medias) { - SaveAndPlaySound(media.OutputPath, media.Extension, media.Data, saveAudio); + SaveAndPlaySound(media.OutputPath, media.Extension, media.Data, saveAudio, updateUi); } break; @@ -823,7 +823,7 @@ public class CUE4ParseViewModel : ViewModel var extractedSounds = CriWareProvider.ExtractCriWareSounds(awbReader, archive.Name); foreach (var sound in extractedSounds) { - SaveAndPlaySound(Path.Combine(directory, sound.Name), sound.Extension, sound.Data, saveAudio); + SaveAndPlaySound(Path.Combine(directory, sound.Name), sound.Extension, sound.Data, saveAudio, updateUi); } break; @@ -839,7 +839,7 @@ public class CUE4ParseViewModel : ViewModel var extractedSounds = CriWareProvider.ExtractCriWareSounds(acbReader, archive.Name); foreach (var sound in extractedSounds) { - SaveAndPlaySound(Path.Combine(directory, sound.Name), sound.Extension, sound.Data, saveAudio); + SaveAndPlaySound(Path.Combine(directory, sound.Name), sound.Extension, sound.Data, saveAudio, updateUi); } break; @@ -854,7 +854,7 @@ public class CUE4ParseViewModel : ViewModel // todo: CSCore.MediaFoundation.MediaFoundationException The byte stream type of the given URL is unsupported. case "aif": { var data = Provider.SaveAsset(entry); - SaveAndPlaySound(entry.PathWithoutExtension, entry.Extension, data, saveAudio); + SaveAndPlaySound(entry.PathWithoutExtension, entry.Extension, data, saveAudio, updateUi); break; } @@ -1114,7 +1114,7 @@ public class CUE4ParseViewModel : ViewModel case UExternalSource when (isNone || saveAudio) && pointer.Object.Value is UExternalSource externalSource: { var audioName = Path.GetFileNameWithoutExtension(externalSource.ExternalSourcePath); - SaveAndPlaySound(audioName, "wem", externalSource.Data?.WemFile ?? [], saveAudio); + SaveAndPlaySound(audioName, "wem", externalSource.Data?.WemFile ?? [], saveAudio, updateUi); return false; } case UAkAudioEvent when (isNone || saveAudio) && pointer.Object.Value is UAkAudioEvent audioEvent: @@ -1122,7 +1122,7 @@ public class CUE4ParseViewModel : ViewModel var extractedSounds = WwiseProvider.ExtractAudioEventSounds(audioEvent); foreach (var sound in extractedSounds) { - SaveAndPlaySound(sound.OutputPath, sound.Extension, sound.Data, saveAudio); + SaveAndPlaySound(sound.OutputPath, sound.Extension, sound.Data, saveAudio, updateUi); } return false; } @@ -1132,7 +1132,7 @@ public class CUE4ParseViewModel : ViewModel var directory = Path.GetDirectoryName(fmodEvent.Owner?.Name) ?? "/FMOD/Desktop/"; foreach (var sound in extractedSounds) { - SaveAndPlaySound(Path.Combine(directory, sound.Name), sound.Extension, sound.Data, saveAudio); + SaveAndPlaySound(Path.Combine(directory, sound.Name), sound.Extension, sound.Data, saveAudio, updateUi); } return false; } @@ -1142,7 +1142,7 @@ public class CUE4ParseViewModel : ViewModel var directory = Path.GetDirectoryName(fmodBank.Owner?.Name) ?? "/FMOD/Desktop/"; foreach (var sound in extractedSounds) { - SaveAndPlaySound(Path.Combine(directory, sound.Name), sound.Extension, sound.Data, saveAudio); + SaveAndPlaySound(Path.Combine(directory, sound.Name), sound.Extension, sound.Data, saveAudio, updateUi); } return false; } @@ -1161,7 +1161,7 @@ public class CUE4ParseViewModel : ViewModel directory = Path.GetDirectoryName(atomObject.Owner.Provider.FixPath(directory)); foreach (var sound in extractedSounds) { - SaveAndPlaySound(Path.Combine(directory, sound.Name).Replace("\\", "/"), sound.Extension, sound.Data, saveAudio); + SaveAndPlaySound(Path.Combine(directory, sound.Name).Replace("\\", "/"), sound.Extension, sound.Data, saveAudio, updateUi); } return false; } @@ -1181,7 +1181,7 @@ public class CUE4ParseViewModel : ViewModel return false; } - SaveAndPlaySound(TabControl.SelectedTab.Entry.PathWithoutExtension.Replace('\\', '/'), audioFormat, data, saveAudio); + SaveAndPlaySound(TabControl.SelectedTab.Entry.PathWithoutExtension.Replace('\\', '/'), audioFormat, data, saveAudio, updateUi); return false; } case UAkMediaAsset when (isNone || saveAudio) && pointer.Object.Value is UAkMediaAsset akMediaAsset: @@ -1192,7 +1192,7 @@ public class CUE4ParseViewModel : ViewModel var shouldDecompress = UserSettings.Default.CompressedAudioMode is ECompressedAudio.PlayDecompressed; akMediaAssetData.Decode(shouldDecompress, out var audioFormat, out var data); - SaveAndPlaySound(audioName, audioFormat, data, saveAudio); + SaveAndPlaySound(audioName, audioFormat, data, saveAudio, updateUi); } return false; } @@ -1208,7 +1208,7 @@ public class CUE4ParseViewModel : ViewModel var audioName = akMediaAsset.MediaName ?? $"{akAudioEventData.Outer.Name} ({akMediaAsset.ID})"; akMediaAssetData.Decode(shouldDecompress, out var audioFormat, out var data); - SaveAndPlaySound(audioName, audioFormat, data, saveAudio); + SaveAndPlaySound(audioName, audioFormat, data, saveAudio, updateUi); } } } @@ -1225,7 +1225,7 @@ public class CUE4ParseViewModel : ViewModel var extractedSounds = WwiseProvider.ExtractAudioEventBorderlands4(faceFXAnimData.ID.Name, false); foreach (var sound in extractedSounds) { - SaveAndPlaySound(sound.OutputPath, sound.Extension, sound.Data, saveAudio); + SaveAndPlaySound(sound.OutputPath, sound.Extension, sound.Data, saveAudio, updateUi); } } @@ -1239,7 +1239,7 @@ public class CUE4ParseViewModel : ViewModel var extractedSounds = WwiseProvider.ExtractAudioEventBorderlands4(eventName, useSoundTag); foreach (var sound in extractedSounds) { - SaveAndPlaySound(sound.OutputPath, sound.Extension, sound.Data, saveAudio); + SaveAndPlaySound(sound.OutputPath, sound.Extension, sound.Data, saveAudio, updateUi); } } @@ -1379,7 +1379,7 @@ public class CUE4ParseViewModel : ViewModel TabControl.SelectedTab.SetDocumentText(cpp, false, false); } - private void SaveAndPlaySound(string fullPath, string ext, byte[] data, bool isBulk) + private void SaveAndPlaySound(string fullPath, string ext, byte[] data, bool isBulk, bool updateUi) { if (fullPath.StartsWith('/')) fullPath = fullPath[1..]; var savedAudioPath = Path.Combine(UserSettings.Default.AudioDirectory, @@ -1389,9 +1389,28 @@ public class CUE4ParseViewModel : ViewModel { Directory.CreateDirectory(savedAudioPath.SubstringBeforeLast('/')); using var stream = new FileStream(savedAudioPath, FileMode.Create, FileAccess.Write); - using var writer = new BinaryWriter(stream); - writer.Write(data); - writer.Flush(); + using (var writer = new BinaryWriter(stream)) + { + writer.Write(data); + writer.Flush(); + } + + if (UserSettings.Default.ConvertAudioOnBulkExport) + { + AudioPlayerViewModel.TryConvert(savedAudioPath, data, out string wavFilePath); + savedAudioPath = wavFilePath; + } + + Log.Information("Successfully saved {FilePath}", savedAudioPath); + if (updateUi) + { + FLogger.Append(ELog.Information, () => + { + FLogger.Text("Successfully saved ", Constants.WHITE); + FLogger.Link(Path.GetFileName(savedAudioPath), savedAudioPath, true); + }); + } + return; } diff --git a/FModel/ViewModels/Commands/MenuCommand.cs b/FModel/ViewModels/Commands/MenuCommand.cs index 1c3e7dbb..fe223f88 100644 --- a/FModel/ViewModels/Commands/MenuCommand.cs +++ b/FModel/ViewModels/Commands/MenuCommand.cs @@ -65,7 +65,7 @@ public class MenuCommand : ViewModelCommand Process.Start(new ProcessStartInfo { FileName = Constants.DISCORD_LINK, UseShellExecute = true }); break; case "ToolBox_Clear_Logs": - FLogger.Logger.Text = string.Empty; + FLogger.ClearLogs(); break; case "ToolBox_Open_Output_Directory": Process.Start(new ProcessStartInfo { FileName = UserSettings.Default.OutputDirectory, UseShellExecute = true }); diff --git a/FModel/ViewModels/GameFileViewModel.cs b/FModel/ViewModels/GameFileViewModel.cs index 8ae82022..afd53544 100644 --- a/FModel/ViewModels/GameFileViewModel.cs +++ b/FModel/ViewModels/GameFileViewModel.cs @@ -21,6 +21,7 @@ using CUE4Parse.UE4.Assets.Exports.CustomizableObject; using CUE4Parse.UE4.Assets.Exports.Engine; using CUE4Parse.UE4.Assets.Exports.Engine.Font; using CUE4Parse.UE4.Assets.Exports.Fmod; +using CUE4Parse.UE4.Assets.Exports.FMod; using CUE4Parse.UE4.Assets.Exports.Foliage; using CUE4Parse.UE4.Assets.Exports.Internationalization; using CUE4Parse.UE4.Assets.Exports.LevelSequence; @@ -240,6 +241,10 @@ public class GameFileViewModel(GameFile asset) : ViewModel USoundAtomCue or UAkAudioEvent or USoundCue or UFMODEvent or UAkAssetData or UAkAssetPlatformData => (EAssetCategory.AudioEvent, EBulkType.Audio), + UFMODBankLookup => (EAssetCategory.Data, EBulkType.None), + + UFMODBus or UFMODSnapshot or UFMODSnapshotReverb or UFMODVCA => (EAssetCategory.Audio, EBulkType.None), + UFMODBank or UAkAudioBank or UAtomWaveBank or UAkInitBank => (EAssetCategory.SoundBank, EBulkType.Audio), UWwiseAssetLibrary or USoundBase or UAkMediaAssetData or UAtomCueSheet @@ -320,7 +325,8 @@ public class GameFileViewModel(GameFile asset) : ViewModel private Task ResolveByExtensionAsync(EResolveCompute resolve) { Resolved |= EResolveCompute.Preview; - switch (Asset.Extension) + var lowercaseExtension = Asset.Extension.ToLowerInvariant(); + switch (lowercaseExtension) { case "uproject": case "uefnproject": @@ -393,7 +399,7 @@ public class GameFileViewModel(GameFile asset) : ViewModel stream.Position = 0; SKBitmap bitmap; - if (Asset.Extension == "svg") + if (lowercaseExtension == "svg") { var svg = new SKSvg(); svg.Load(stream); @@ -415,7 +421,7 @@ public class GameFileViewModel(GameFile asset) : ViewModel bitmap = SKBitmap.Decode(stream); } - using var image = bitmap.Encode(Asset.Extension == "jpg" ? SKEncodedImageFormat.Jpeg : SKEncodedImageFormat.Png, 100); + using var image = bitmap.Encode(lowercaseExtension == "jpg" ? SKEncodedImageFormat.Jpeg : SKEncodedImageFormat.Png, 100); SetPreviewImage(image); bitmap.Dispose(); diff --git a/FModel/Views/Resources/Controls/Rtb/CustomRichTextBox.cs b/FModel/Views/Resources/Controls/Rtb/CustomRichTextBox.cs index c480a4bf..8b995c01 100644 --- a/FModel/Views/Resources/Controls/Rtb/CustomRichTextBox.cs +++ b/FModel/Views/Resources/Controls/Rtb/CustomRichTextBox.cs @@ -155,6 +155,12 @@ public class FLogger : ITextFormatter { new TextRange(document.ContentStart, document.ContentEnd).Text = text; } + + public static void ClearLogs() + { + Logger.Document.Blocks.Clear(); + _previous = 0; + } } public class CustomRichTextBox : RichTextBox diff --git a/FModel/Views/SettingsView.xaml b/FModel/Views/SettingsView.xaml index 3869882d..450f97a3 100644 --- a/FModel/Views/SettingsView.xaml +++ b/FModel/Views/SettingsView.xaml @@ -43,6 +43,7 @@ + @@ -157,7 +158,8 @@ + IsChecked="{Binding KeepDirectoryStructure, Source={x:Static local:Settings.UserSettings.Default}, Mode=TwoWay}" Margin="0 5 0 0" + Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" /> @@ -207,7 +209,8 @@ + IsChecked="{Binding SettingsView.MappingEndpoint.Overwrite, Mode=TwoWay}" + Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" /> + IsChecked="{Binding ReadScriptData, Source={x:Static local:Settings.UserSettings.Default}, Mode=TwoWay}" Margin="0 5 0 10" + Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" /> + IsChecked="{Binding ReadShaderMaps, Source={x:Static local:Settings.UserSettings.Default}, Mode=TwoWay}" Margin="0 5 0 10" + Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" /> + IsChecked="{Binding ShowDecompileOption, Source={x:Static local:Settings.UserSettings.Default}, Mode=TwoWay}" Margin="0 5 0 10" + Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" /> - - + + + + -