Merge pull request #602 from GMMan/add-audio-export
Some checks are pending
FModel QA Builder / build (push) Waiting to run

Add save audio option to context menus
This commit is contained in:
Valentin 2025-11-02 20:23:18 +01:00 committed by GitHub
commit 69d83d5257
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 97 additions and 39 deletions

View File

@ -114,5 +114,6 @@ public enum EBulkType
Textures = 1 << 2,
Meshes = 1 << 3,
Skeletons = 1 << 4,
Animations = 1 << 5
Animations = 1 << 5,
Audio = 1 << 6
}

View File

@ -103,9 +103,6 @@
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem Header="Auto Open Sounds" IsCheckable="True" StaysOpenOnClick="True"
IsChecked="{Binding IsAutoOpenSounds, Source={x:Static settings:UserSettings.Default}}" />
</MenuItem>
<MenuItem Header="Views">
<MenuItem Header="3D Viewer" Command="{Binding MenuCommand}" CommandParameter="Views_3dViewer">
@ -376,6 +373,15 @@
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Save Folder's Packages Audio" Click="OnFolderAudioClick">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource AudioIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem Header="Favorite Directory" Click="OnFavoriteDirectoryClick">
<MenuItem.Icon>
@ -608,6 +614,21 @@
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Save Audio" Command="{Binding DataContext.RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Assets_Save_Audio" />
<Binding Path="SelectedItems" />
</MultiBinding>
</MenuItem.CommandParameter>
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource AudioIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem Header="Copy">
<MenuItem.Icon>
@ -838,19 +859,6 @@
</Viewbox>
</StatusBarItem>
<StatusBarItem Width="30" HorizontalContentAlignment="Stretch" ToolTip="Auto Open Sounds Enabled">
<StatusBarItem.Style>
<Style TargetType="StatusBarItem">
<Style.Triggers>
<DataTrigger Binding="{Binding IsAutoOpenSounds, Source={x:Static settings:UserSettings.Default}}" Value="False">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
</StatusBarItem.Style>
<TextBlock HorizontalAlignment="Center" FontWeight="SemiBold" Text="SND" />
</StatusBarItem>
<StatusBarItem Margin="10 0 0 0">
<TextBlock Text="{Binding LastUpdateCheck, Source={x:Static local:Settings.UserSettings.Default}, Converter={x:Static converters:RelativeDateTimeConverter.Instance}, StringFormat=Last Refresh: {0}}" />
</StatusBarItem>

View File

@ -238,6 +238,19 @@ public partial class MainWindow
}
}
private async void OnFolderAudioClick(object sender, RoutedEventArgs e)
{
if (AssetsFolderName.SelectedItem is TreeItem folder)
{
await _threadWorkerView.Begin(cancellationToken => { _applicationView.CUE4Parse.AudioFolder(cancellationToken, folder); });
FLogger.Append(ELog.Information, () =>
{
FLogger.Text("Successfully saved audio from ", Constants.WHITE);
FLogger.Link(folder.PathAtThisPoint, UserSettings.Default.AudioDirectory, true);
});
}
}
private void OnFavoriteDirectoryClick(object sender, RoutedEventArgs e)
{
if (AssetsFolderName.SelectedItem is not TreeItem folder) return;

View File

@ -140,13 +140,6 @@ namespace FModel.Settings
set => SetProperty(ref _lastOpenedSettingTab, value);
}
private bool _isAutoOpenSounds = true;
public bool IsAutoOpenSounds
{
get => _isAutoOpenSounds;
set => SetProperty(ref _isAutoOpenSounds, value);
}
private bool _isLoggerExpanded = true;
public bool IsLoggerExpanded
{

View File

@ -583,6 +583,9 @@ public class CUE4ParseViewModel : ViewModel
public void AnimationFolder(CancellationToken cancellationToken, TreeItem folder)
=> BulkFolder(cancellationToken, folder, asset => Extract(cancellationToken, asset, TabControl.HasNoTabs, EBulkType.Animations | EBulkType.Auto));
public void AudioFolder(CancellationToken cancellationToken, TreeItem folder)
=> BulkFolder(cancellationToken, folder, asset => Extract(cancellationToken, asset, TabControl.HasNoTabs, EBulkType.Audio | EBulkType.Auto));
public void Extract(CancellationToken cancellationToken, GameFile entry, bool addNewTab = false, EBulkType bulk = EBulkType.None)
{
Log.Information("User DOUBLE-CLICKED to extract '{FullPath}'", entry.Path);
@ -594,6 +597,7 @@ public class CUE4ParseViewModel : ViewModel
var updateUi = !HasFlag(bulk, EBulkType.Auto);
var saveProperties = HasFlag(bulk, EBulkType.Properties);
var saveTextures = HasFlag(bulk, EBulkType.Textures);
var saveAudio = HasFlag(bulk, EBulkType.Audio);
switch (entry.Extension)
{
case "uasset":
@ -740,7 +744,7 @@ public class CUE4ParseViewModel : ViewModel
var medias = WwiseProvider.ExtractBankSounds(wwise);
foreach (var media in medias)
{
SaveAndPlaySound(media.OutputPath, media.Extension, media.Data);
SaveAndPlaySound(media.OutputPath, media.Extension, media.Data, saveAudio);
}
break;
@ -755,7 +759,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);
SaveAndPlaySound(entry.PathWithoutExtension, entry.Extension, data, saveAudio);
break;
}
@ -870,6 +874,7 @@ public class CUE4ParseViewModel : ViewModel
var isNone = bulk == EBulkType.None;
var updateUi = !HasFlag(bulk, EBulkType.Auto);
var saveTextures = HasFlag(bulk, EBulkType.Textures);
var saveAudio = HasFlag(bulk, EBulkType.Audio);
var pointer = new FPackageIndex(pkg, index + 1).ResolvedObject;
if (pointer?.Object is null) return false;
@ -940,37 +945,37 @@ public class CUE4ParseViewModel : ViewModel
TabControl.SelectedTab.AddImage(sourceFile.SubstringAfterLast('/'), false, bitmap, false, updateUi);
return false;
}
case UAkAudioEvent when isNone && pointer.Object.Value is UAkAudioEvent audioEvent:
case UAkAudioEvent when (isNone || saveAudio) && pointer.Object.Value is UAkAudioEvent audioEvent:
{
var extractedSounds = WwiseProvider.ExtractAudioEventSounds(audioEvent);
foreach (var sound in extractedSounds)
{
SaveAndPlaySound(sound.OutputPath, sound.Extension, sound.Data);
SaveAndPlaySound(sound.OutputPath, sound.Extension, sound.Data, saveAudio);
}
return false;
}
case UFMODEvent when isNone && pointer.Object.Value is UFMODEvent fmodEvent:
case UFMODEvent when (isNone || saveAudio) && pointer.Object.Value is UFMODEvent fmodEvent:
{
var extractedSounds = FmodProvider.ExtractEventSounds(fmodEvent);
var directory = Path.GetDirectoryName(fmodEvent.Owner?.Name) ?? "/FMOD/Desktop/";
foreach (var sound in extractedSounds)
{
SaveAndPlaySound(Path.Combine(directory, sound.Name), sound.Extension, sound.Data);
SaveAndPlaySound(Path.Combine(directory, sound.Name), sound.Extension, sound.Data, saveAudio);
}
return false;
}
case UFMODBank when isNone && pointer.Object.Value is UFMODBank fmodBank:
case UFMODBank when (isNone || saveAudio) && pointer.Object.Value is UFMODBank fmodBank:
{
var extractedSounds = FmodProvider.ExtractBankSounds(fmodBank);
var directory = Path.GetDirectoryName(fmodBank.Owner?.Name) ?? "/FMOD/Desktop/";
foreach (var sound in extractedSounds)
{
SaveAndPlaySound(Path.Combine(directory, sound.Name), sound.Extension, sound.Data);
SaveAndPlaySound(Path.Combine(directory, sound.Name), sound.Extension, sound.Data, saveAudio);
}
return false;
}
case UAkMediaAssetData when isNone:
case USoundWave when isNone:
case UAkMediaAssetData when isNone || saveAudio:
case USoundWave when isNone || saveAudio:
{
var shouldDecompress = UserSettings.Default.CompressedAudioMode == ECompressedAudio.PlayDecompressed;
pointer.Object.Value.Decode(shouldDecompress, out var audioFormat, out var data);
@ -981,7 +986,7 @@ public class CUE4ParseViewModel : ViewModel
return false;
}
SaveAndPlaySound(TabControl.SelectedTab.Entry.PathWithoutExtension.Replace('\\', '/'), audioFormat, data);
SaveAndPlaySound(TabControl.SelectedTab.Entry.PathWithoutExtension.Replace('\\', '/'), audioFormat, data, saveAudio);
return false;
}
case UWorld when isNone && UserSettings.Default.PreviewWorlds:
@ -1102,13 +1107,13 @@ public class CUE4ParseViewModel : ViewModel
TabControl.SelectedTab.SetDocumentText(cpp, false, false);
}
private void SaveAndPlaySound(string fullPath, string ext, byte[] data)
private void SaveAndPlaySound(string fullPath, string ext, byte[] data, bool isBulk)
{
if (fullPath.StartsWith('/')) fullPath = fullPath[1..];
var savedAudioPath = Path.Combine(UserSettings.Default.AudioDirectory,
UserSettings.Default.KeepDirectoryStructure ? fullPath : fullPath.SubstringAfterLast('/')).Replace('\\', '/') + $".{ext.ToLowerInvariant()}";
if (!UserSettings.Default.IsAutoOpenSounds)
if (isBulk)
{
Directory.CreateDirectory(savedAudioPath.SubstringBeforeLast('/'));
using var stream = new FileStream(savedAudioPath, FileMode.Create, FileAccess.Write);

View File

@ -92,6 +92,14 @@ public class RightClickMenuCommand : ViewModelCommand<ApplicationViewModel>
contextViewModel.CUE4Parse.Extract(cancellationToken, entry, false, EBulkType.Animations | updateUi);
}
break;
case "Assets_Save_Audio":
foreach (var entry in entries)
{
Thread.Yield();
cancellationToken.ThrowIfCancellationRequested();
contextViewModel.CUE4Parse.Extract(cancellationToken, entry, false, EBulkType.Audio | updateUi);
}
break;
}
});
}

View File

@ -1,4 +1,4 @@
using System.Windows;
using System.Windows;
using AdonisUI.Controls;
using FModel.Framework;
using FModel.Services;
@ -58,6 +58,12 @@ public class TabCommand : ViewModelCommand<TabItem>
_applicationView.CUE4Parse.Extract(cancellationToken, tabViewModel.Entry, false, EBulkType.Animations);
});
break;
case "Asset_Save_Audio":
await _threadWorkerView.Begin(cancellationToken =>
{
_applicationView.CUE4Parse.Extract(cancellationToken, tabViewModel.Entry, false, EBulkType.Audio);
});
break;
case "Open_Properties":
if (tabViewModel.Header == "New Tab" || tabViewModel.Document == null) return;
Helper.OpenWindow<AdonisWindow>(tabViewModel.Header + " (Properties)", () =>

View File

@ -909,6 +909,15 @@
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Save Audio" Command="{Binding TabCommand}" CommandParameter="Asset_Save_Audio">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource AudioIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem Header="Open Properties" Command="{Binding TabCommand}" CommandParameter="Open_Properties">
<MenuItem.Icon>

View File

@ -1,4 +1,4 @@
<adonisControls:AdonisWindow x:Class="FModel.Views.SearchView"
<adonisControls:AdonisWindow x:Class="FModel.Views.SearchView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="clr-namespace:FModel.Views.Resources.Converters"
@ -254,6 +254,21 @@
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Save Audio" Command="{Binding DataContext.RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Assets_Save_Audio" />
<Binding Path="SelectedItems" />
</MultiBinding>
</MenuItem.CommandParameter>
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource AudioIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem Header="Copy">
<MenuItem.Icon>