Option for bulk audio conversion (#646)
Some checks failed
FModel QA Builder / build (push) Has been cancelled

This commit is contained in:
Masusder 2026-02-18 17:40:15 +01:00 committed by GitHub
parent 1c48a27f8e
commit 500fa59f85
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 91 additions and 35 deletions

@ -1 +1 @@
Subproject commit a451f8e3140c806da8b438e9b44dafe4c1c0a37b
Subproject commit 1861416d2ab765cde91eec6f46305fc316dfae6e

View File

@ -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<string, DirectorySettings> _perDirectory = new Dictionary<string, DirectorySettings>();
public IDictionary<string, DirectorySettings> PerDirectory
{

View File

@ -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");

View File

@ -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;
}

View File

@ -65,7 +65,7 @@ public class MenuCommand : ViewModelCommand<ApplicationViewModel>
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 });

View File

@ -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();

View File

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

View File

@ -43,6 +43,7 @@
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
@ -157,7 +158,8 @@
<TextBlock Grid.Row="9" Grid.Column="0" Text="Keep Directory Structure" VerticalAlignment="Center" Margin="0 5 0 5" ToolTip="Auto-save packages following their game directory" />
<CheckBox Grid.Row="9" Grid.Column="2" Content="{Binding IsChecked, RelativeSource={RelativeSource Self}, Converter={x:Static converters:BoolToToggleConverter.Instance}}"
IsChecked="{Binding KeepDirectoryStructure, Source={x:Static local:Settings.UserSettings.Default}, Mode=TwoWay}" Margin="0 5 0 0"/>
IsChecked="{Binding KeepDirectoryStructure, Source={x:Static local:Settings.UserSettings.Default}, Mode=TwoWay}" Margin="0 5 0 0"
Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" />
<Separator Grid.Row="10" Grid.Column="0" Grid.ColumnSpan="7" Style="{StaticResource CustomSeparator}" Tag="ADVANCED"></Separator>
@ -207,7 +209,8 @@
<CheckBox Grid.Row="14" Grid.Column="2" Margin="0 5 0 10"
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.SettingsView}}}"
Content="{Binding IsChecked, RelativeSource={RelativeSource Self}, Converter={x:Static converters:BoolToToggleConverter.Instance}}"
IsChecked="{Binding SettingsView.MappingEndpoint.Overwrite, Mode=TwoWay}" />
IsChecked="{Binding SettingsView.MappingEndpoint.Overwrite, Mode=TwoWay}"
Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" />
<TextBlock Grid.Row="15" Grid.Column="0" Text="Mapping File Path" VerticalAlignment="Center" Margin="0 0 0 5"
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.SettingsView}}}"
@ -221,29 +224,44 @@
<TextBlock Grid.Row="16" Grid.Column="0" Text="Serialize Script Bytecode" VerticalAlignment="Center" Margin="0 0 0 5" />
<CheckBox Grid.Row="16" Grid.Column="2" Content="{Binding IsChecked, RelativeSource={RelativeSource Self}, Converter={x:Static converters:BoolToToggleConverter.Instance}}"
IsChecked="{Binding ReadScriptData, Source={x:Static local:Settings.UserSettings.Default}, Mode=TwoWay}" Margin="0 5 0 10"/>
IsChecked="{Binding ReadScriptData, Source={x:Static local:Settings.UserSettings.Default}, Mode=TwoWay}" Margin="0 5 0 10"
Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" />
<TextBlock Grid.Row="17" Grid.Column="0" Text="Serialize Inlined Shader Maps" VerticalAlignment="Center" Margin="0 0 0 5" />
<CheckBox Grid.Row="17" Grid.Column="2" Content="{Binding IsChecked, RelativeSource={RelativeSource Self}, Converter={x:Static converters:BoolToToggleConverter.Instance}}"
IsChecked="{Binding ReadShaderMaps, Source={x:Static local:Settings.UserSettings.Default}, Mode=TwoWay}" Margin="0 5 0 10"/>
IsChecked="{Binding ReadShaderMaps, Source={x:Static local:Settings.UserSettings.Default}, Mode=TwoWay}" Margin="0 5 0 10"
Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" />
<TextBlock Grid.Row="18" Grid.Column="0" Text="Decompile Blueprint to Pseudo C++" VerticalAlignment="Center" Margin="0 0 0 5" ToolTip="Adds a right click option to decompile UClass packages into a pseudo C++ friendly format" />
<CheckBox Grid.Row="18" Grid.Column="2" Content="{Binding IsChecked, RelativeSource={RelativeSource Self}, Converter={x:Static converters:BoolToToggleConverter.Instance}}"
IsChecked="{Binding ShowDecompileOption, Source={x:Static local:Settings.UserSettings.Default}, Mode=TwoWay}" Margin="0 5 0 10"/>
IsChecked="{Binding ShowDecompileOption, Source={x:Static local:Settings.UserSettings.Default}, Mode=TwoWay}" Margin="0 5 0 10"
Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" />
<TextBlock Grid.Row="19" Grid.Column="0" Text="Max Wwise Bank (.BNK) Prefetch" VerticalAlignment="Center" Margin="0 0 0 5" />
<Slider Grid.Row="19" Grid.Column="2" Grid.ColumnSpan="5" TickPlacement="None" Minimum="0" Maximum="2048" Ticks="0,8,32,128,256,512,1024,2048"
<TextBlock Grid.Row="19"
Grid.Column="0"
Text="Convert Audio During Export (.wav)"
VerticalAlignment="Center"
Margin="0 0 0 5" />
<CheckBox Grid.Row="19"
Grid.Column="2"
Content="{Binding IsChecked, RelativeSource={RelativeSource Self}, Converter={x:Static converters:BoolToToggleConverter.Instance}}"
IsChecked="{Binding ConvertAudioOnBulkExport, Source={x:Static local:Settings.UserSettings.Default}, Mode=TwoWay}"
Margin="0 5 0 10"
Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" />
<TextBlock Grid.Row="20" Grid.Column="0" Text="Max Wwise Bank (.BNK) Prefetch" VerticalAlignment="Center" Margin="0 0 0 5" />
<Slider Grid.Row="20" Grid.Column="2" Grid.ColumnSpan="5" TickPlacement="None" Minimum="0" Maximum="2048" Ticks="0,8,32,128,256,512,1024,2048"
AutoToolTipPlacement="BottomRight" IsMoveToPointEnabled="True" IsSnapToTickEnabled="True" Margin="0 5 0 5"
Value="{Binding WwiseMaxBnkPrefetch, Source={x:Static local:Settings.UserSettings.Default}, Mode=TwoWay}"/>
<TextBlock Grid.Row="20"
<TextBlock Grid.Row="21"
Grid.Column="0"
Text="CRIWARE Decryption Key"
VerticalAlignment="Center"
Margin="0 0 0 10" />
<TextBox x:Name="CriwareKeyBox"
Grid.Row="20"
Grid.Row="21"
Grid.Column="2"
Grid.ColumnSpan="5"
Margin="0 5 0 10"