Add save audio option to context menus

This commit is contained in:
Yukai Li
2025-10-17 14:41:17 -06:00
parent b51d539c24
commit 570bdd6180
8 changed files with 98 additions and 17 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

@@ -376,6 +376,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 +617,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>

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.ModelDirectory, true);
});
}
}
private void OnFavoriteDirectoryClick(object sender, RoutedEventArgs e)
{
if (AssetsFolderName.SelectedItem is not TreeItem folder) return;

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":
@@ -738,7 +742,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;
@@ -753,7 +757,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;
}
@@ -868,6 +872,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;
@@ -938,37 +943,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);
@@ -979,7 +984,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:
@@ -1100,13 +1105,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 (!UserSettings.Default.IsAutoOpenSounds || isBulk)
{
Directory.CreateDirectory(savedAudioPath.SubstringBeforeLast('/'));
using var stream = new FileStream(savedAudioPath, FileMode.Create, FileAccess.Write);
@@ -1188,4 +1193,4 @@ public class CUE4ParseViewModel : ViewModel
{
return (a & b) == b;
}
}
}

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>