diff --git a/FModel/FModel.csproj b/FModel/FModel.csproj index 85bdb8db..094b6beb 100644 --- a/FModel/FModel.csproj +++ b/FModel/FModel.csproj @@ -110,6 +110,7 @@ + True @@ -120,6 +121,7 @@ + diff --git a/FModel/PakReader/BnkReader.cs b/FModel/PakReader/BnkReader.cs new file mode 100644 index 00000000..539bec20 --- /dev/null +++ b/FModel/PakReader/BnkReader.cs @@ -0,0 +1,322 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace FModel.PakReader +{ + /// + /// http://wiki.xentax.com/index.php/Wwise_SoundBank_(*.bnk) + /// don't copy paste pls, try to understand what you write from this + /// DATASection holds the .wem files + /// STIDSection holds the .wem files name in a dictionary where the key is the .wem file id from DATASection + /// i see no use (for FModel) in other sections atm + /// + public class BnkReader + { + private const uint _BKHD_ID = 0x44484B42; + private const uint _DIDX_ID = 0x58444944; + private const uint _DATA_ID = 0x41544144; + private const uint _HIRC_ID = 0x43524948; + private const uint _STID_ID = 0x44495453; + private const uint _STMG_ID = 0x474D5453; + public Dictionary AudioFiles; + + public BnkReader(BinaryReader reader) + { + DIDXSection didxSection = null; + DATASection dataSection = null; + HIRCSection hircSection = null; + STIDSection stidSection = null; + STMGSection stmgSection = null; + AudioFiles = new Dictionary(); + + while (reader.BaseStream.Position < reader.BaseStream.Length) + { + uint SectionIdentifier = reader.ReadUInt32(); + uint SectionLength = reader.ReadUInt32(); + long Position = reader.BaseStream.Position; + switch (SectionIdentifier) + { + case _BKHD_ID: + BKHDSection _ = new BKHDSection(reader); + break; + case _DIDX_ID: + didxSection = new DIDXSection(reader, Position + SectionLength); + break; + case _DATA_ID: + if (didxSection != null) dataSection = new DATASection(reader, Position, didxSection); + break; + case _HIRC_ID: + hircSection = new HIRCSection(reader); + break; + case _STID_ID: + stidSection = new STIDSection(reader); + break; + case _STMG_ID: + stmgSection = new STMGSection(reader); + break; + } + + reader.BaseStream.Seek(Position + SectionLength, SeekOrigin.Begin); + } + + if (didxSection != null && dataSection != null && didxSection.WemFilesRef.Count == dataSection.WemFiles.Count) + { + for (int i = 0; i < didxSection.WemFilesRef.Count; i++) + { + string key = $"{didxSection.WemFilesRef[i].Id}.wem"; + if (stidSection != null && stidSection.SoundBanks.TryGetValue(didxSection.WemFilesRef[i].Id, out string name)) + key = name; + + AudioFiles[key] = dataSection.WemFiles[i]; + } + } + } + + public class BKHDSection + { + public uint Version; + public uint Id; + + public BKHDSection(BinaryReader reader) + { + Version = reader.ReadUInt32(); + Id = reader.ReadUInt32(); + } + } + + public class DIDXSection + { + public List WemFilesRef; + + public DIDXSection(BinaryReader reader, long length) + { + WemFilesRef = new List(); + while (reader.BaseStream.Position < length) + { + WemFilesRef.Add(new WemObject(reader)); + } + } + + public class WemObject + { + public uint Id; + public uint Offset; + public uint Length; + + public WemObject(BinaryReader reader) + { + Id = reader.ReadUInt32(); + Offset = reader.ReadUInt32(); + Length = reader.ReadUInt32(); + } + } + } + + public class DATASection + { + public List WemFiles; + + public DATASection(BinaryReader reader, long position, DIDXSection didxSection) + { + WemFiles = new List(didxSection.WemFilesRef.Count); + foreach (var fileRef in didxSection.WemFilesRef) + { + reader.BaseStream.Seek(position + fileRef.Offset, SeekOrigin.Begin); + WemFiles.Add(reader.ReadBytes(Convert.ToInt32(fileRef.Length))); + } + } + } + + public class HIRCSection + { + public uint ObjectNumber; + public WwiseObject[] Objects; + + public HIRCSection(BinaryReader reader) + { + ObjectNumber = reader.ReadUInt32(); + Objects = new WwiseObject[ObjectNumber]; + for (int i = 0; i < Objects.Length; i++) + { + Objects[i] = new WwiseObject(reader); + } + } + + public class WwiseObject + { + public WwiseObjectType Type; + public uint Length; + public uint Id; + public byte[] AdditionalData; + + public WwiseObject(BinaryReader reader) + { + Type = (WwiseObjectType)reader.ReadByte(); + Length = reader.ReadUInt32(); + Id = reader.ReadUInt32(); + + AdditionalData = Type switch + { + _ => reader.ReadBytes(Convert.ToInt32(Length - sizeof(uint))), + }; + } + + public enum WwiseObjectType : byte + { + Settings, + SoundSFXVoice, + EventAction, + Event, + SequenceContainer, + SwitchContainer, + AudioBus, + BlendContainer, + MusicSegment, + MusicTrack, + MusicSwitchContainer, + MusicPlaylistContainer, + Attenuation, + DialogueEvent, + MotionBus, + MotionFX, + Effect, + AuxiliaryBus + } + } + } + + public class STIDSection + { + public uint SoundBankNumber; + public Dictionary SoundBanks; + + public STIDSection(BinaryReader reader) + { + reader.ReadUInt32(); + SoundBankNumber = reader.ReadUInt32(); + SoundBanks = new Dictionary(Convert.ToInt32(SoundBankNumber)); + for (int i = 0; i < SoundBankNumber; i++) + { + SoundBanks[reader.ReadUInt32()] = reader.ReadString(); + } + } + } + + public class STMGSection + { + public float VolumeThreshold; + public ushort MaxVoiceInstances; + public uint StateGroupNumber; + public StateGroupObject[] StateGroups; + public uint SwitchGroupNumber; + public SwitchGroupObject[] SwitchGroups; + public uint GameParameterNumber; + public GameParameterObject[] GameParameters; + + public STMGSection(BinaryReader reader) + { + VolumeThreshold = reader.ReadSingle(); + MaxVoiceInstances = reader.ReadUInt16(); + StateGroupNumber = reader.ReadUInt32(); + StateGroups = new StateGroupObject[Convert.ToInt32(StateGroups)]; + for (int i = 0; i < StateGroups.Length; i++) + { + StateGroups[i] = new StateGroupObject(reader); + } + SwitchGroupNumber = reader.ReadUInt32(); + SwitchGroups = new SwitchGroupObject[SwitchGroupNumber]; + for (int i = 0; i < SwitchGroups.Length; i++) + { + SwitchGroups[i] = new SwitchGroupObject(reader); + } + GameParameterNumber = reader.ReadUInt32(); + GameParameters = new GameParameterObject[GameParameterNumber]; + for (int i = 0; i < GameParameters.Length; i++) + { + GameParameters[i] = new GameParameterObject(reader); + } + } + + public class StateGroupObject + { + public uint StateId; + public uint DefaultTransitionTime; + public uint CustomTransitionTimeNumber; + public CustomTransitionTimeObject[] CustomTransitionTimes; + + public StateGroupObject(BinaryReader reader) + { + StateId = reader.ReadUInt32(); + DefaultTransitionTime = reader.ReadUInt32(); + CustomTransitionTimeNumber = reader.ReadUInt32(); + CustomTransitionTimes = new CustomTransitionTimeObject[CustomTransitionTimeNumber]; + for (int i = 0; i < CustomTransitionTimes.Length; i++) + { + CustomTransitionTimes[i] = new CustomTransitionTimeObject(reader); + } + } + + public class CustomTransitionTimeObject + { + public uint FromId; + public uint ToId; + public uint TransitionTime; + + public CustomTransitionTimeObject(BinaryReader reader) + { + FromId = reader.ReadUInt32(); + ToId = reader.ReadUInt32(); + TransitionTime = reader.ReadUInt32(); + } + } + } + + public class SwitchGroupObject + { + public uint SwitchId; + public uint GameParameterId; + public uint PointNumber; + public PointObject[] Points; + + public SwitchGroupObject(BinaryReader reader) + { + SwitchId = reader.ReadUInt32(); + GameParameterId = reader.ReadUInt32(); + PointNumber = reader.ReadUInt32(); + Points = new PointObject[PointNumber]; + for (int i = 0; i < Points.Length; i++) + { + Points[i] = new PointObject(reader); + } + } + + public class PointObject + { + public float GameParameterValue; + public uint SwitchId; + public uint CurveShape; + + public PointObject(BinaryReader reader) + { + GameParameterValue = reader.ReadSingle(); + SwitchId = reader.ReadUInt32(); + CurveShape = reader.ReadUInt32(); + } + } + } + + public class GameParameterObject + { + public uint Id; + public float DefaultValue; + + public GameParameterObject(BinaryReader reader) + { + Id = reader.ReadUInt32(); + DefaultValue = reader.ReadSingle(); + } + } + } + } +} diff --git a/FModel/PakReader/Parsers/Class/UCurveTable.cs b/FModel/PakReader/Parsers/Class/UCurveTable.cs index a0b9c7ed..3d81466a 100644 --- a/FModel/PakReader/Parsers/Class/UCurveTable.cs +++ b/FModel/PakReader/Parsers/Class/UCurveTable.cs @@ -20,7 +20,7 @@ namespace PakReader.Parsers.Class for (int i = 0; i < NumRows; i++) { int num = 1; - string RowName = reader.ReadFName().String; + string RowName = reader.ReadFName().String ?? ""; string baseName = RowName; while (RowMap.ContainsKey(RowName)) { diff --git a/FModel/PakReader/Parsers/Class/UDataTable.cs b/FModel/PakReader/Parsers/Class/UDataTable.cs index 7bfc0f4d..377d7f4e 100644 --- a/FModel/PakReader/Parsers/Class/UDataTable.cs +++ b/FModel/PakReader/Parsers/Class/UDataTable.cs @@ -18,7 +18,7 @@ namespace PakReader.Parsers.Class for (int i = 0; i < NumRows; i++) { int num = 1; - string RowName = reader.ReadFName().String; + string RowName = reader.ReadFName().String ?? ""; string baseName = RowName; while (RowMap.ContainsKey(RowName)) { diff --git a/FModel/PakReader/Parsers/Class/USoundWave.cs b/FModel/PakReader/Parsers/Class/USoundWave.cs index 1d25622c..d457bca8 100644 --- a/FModel/PakReader/Parsers/Class/USoundWave.cs +++ b/FModel/PakReader/Parsers/Class/USoundWave.cs @@ -54,7 +54,7 @@ namespace PakReader.Parsers.Class internal USoundWave(PackageReader reader, Stream ubulk, long ubulkOffset) : base(reader) { // if UE4.25+ && Windows -> True - bStreaming = FModel.Globals.Game.Version >= EPakVersion.PATH_HASH_INDEX ? true : false; + bStreaming = FModel.Globals.Game.Version >= EPakVersion.PATH_HASH_INDEX; bCooked = reader.ReadInt32() != 0; if (this.TryGetValue("bStreaming", out var v) && v is BoolProperty b) diff --git a/FModel/PakReader/Parsers/Class/UTexture2D.cs b/FModel/PakReader/Parsers/Class/UTexture2D.cs index f0ed7a2d..5e353216 100644 --- a/FModel/PakReader/Parsers/Class/UTexture2D.cs +++ b/FModel/PakReader/Parsers/Class/UTexture2D.cs @@ -32,11 +32,20 @@ namespace PakReader.Parsers.Class { var data = new List(1); // Probably gonna be only one texture anyway var PixelFormatName = reader.ReadFName(); - while (!PixelFormatName.IsNone) + if (FModel.Globals.Game.Version < EPakVersion.INDEX_ENCRYPTION) { - _ = reader.ReadInt64(); // SkipOffset + _ = reader.ReadInt32(); // SkipOffset data.Add(new FTexturePlatformData(reader, ubulk, bulkOffset)); - PixelFormatName = reader.ReadFName(); + reader.ReadFName(); + } + else + { + while (!PixelFormatName.IsNone) + { + _ = reader.ReadInt64(); // SkipOffset + data.Add(new FTexturePlatformData(reader, ubulk, bulkOffset)); + PixelFormatName = reader.ReadFName(); + } } PlatformDatas = data.ToArray(); } diff --git a/FModel/PakReader/Parsers/Objects/UScriptStruct.cs b/FModel/PakReader/Parsers/Objects/UScriptStruct.cs index 47c9caaa..144e49e0 100644 --- a/FModel/PakReader/Parsers/Objects/UScriptStruct.cs +++ b/FModel/PakReader/Parsers/Objects/UScriptStruct.cs @@ -50,7 +50,7 @@ namespace PakReader.Parsers.Objects "MovieSceneFloatValue" => new FRichCurveKey(reader), "MovieSceneFloatChannel" => new FMovieSceneFloatChannel(reader), "MovieSceneEvaluationTemplate" => new FMovieSceneEvaluationTemplate(reader), - "SkeletalMeshSamplingLODBuiltData" => new FSkeletalMeshSamplingLODBuiltData(reader), + //"SkeletalMeshSamplingLODBuiltData" => new FSkeletalMeshSamplingLODBuiltData(reader), "VectorMaterialInput" => new FVectorMaterialInput(reader), "ColorMaterialInput" => new FColorMaterialInput(reader), "ExpressionInput" => new FMaterialInput(reader), diff --git a/FModel/PakReader/Parsers/PropertyTagData/BaseProperty.cs b/FModel/PakReader/Parsers/PropertyTagData/BaseProperty.cs index f55d4eed..52127d02 100644 --- a/FModel/PakReader/Parsers/PropertyTagData/BaseProperty.cs +++ b/FModel/PakReader/Parsers/PropertyTagData/BaseProperty.cs @@ -21,8 +21,8 @@ namespace PakReader.Parsers.PropertyTagData "StrProperty" => new StrProperty(reader), "TextProperty" => new TextProperty(reader), "InterfaceProperty" => new InterfaceProperty(reader), - "MulticastDelegateProperty" => new MulticastDelegateProperty(reader, tag), - "LazyObjectProperty" => new LazyObjectProperty(reader, tag), + //"MulticastDelegateProperty" => new MulticastDelegateProperty(reader, tag), + //"LazyObjectProperty" => new LazyObjectProperty(reader, tag), "SoftObjectProperty" => new SoftObjectProperty(reader, readType), "UInt64Property" => new UInt64Property(reader), "UInt32Property" => new UInt32Property(reader), @@ -55,8 +55,8 @@ namespace PakReader.Parsers.PropertyTagData "StrProperty" => new StrProperty(reader).Value, "TextProperty" => new TextProperty(reader).Value, "InterfaceProperty" => new InterfaceProperty(reader).Value, - "MulticastDelegateProperty" => new MulticastDelegateProperty(reader, tag).Value, - "LazyObjectProperty" => new LazyObjectProperty(reader, tag).Value, + //"MulticastDelegateProperty" => new MulticastDelegateProperty(reader, tag).Value, + //"LazyObjectProperty" => new LazyObjectProperty(reader, tag).Value, "SoftObjectProperty" => new SoftObjectProperty(reader, readType).Value, "UInt64Property" => new UInt64Property(reader).Value, "UInt32Property" => new UInt32Property(reader).Value, diff --git a/FModel/Properties/Resources.Designer.cs b/FModel/Properties/Resources.Designer.cs index 1d643752..5f6c5535 100644 --- a/FModel/Properties/Resources.Designer.cs +++ b/FModel/Properties/Resources.Designer.cs @@ -2780,6 +2780,16 @@ namespace FModel.Properties { } } + /// + /// Recherche une ressource localisée de type System.Byte[]. + /// + public static byte[] Xml { + get { + object obj = ResourceManager.GetObject("Xml", resourceCulture); + return ((byte[])(obj)); + } + } + /// /// Recherche une chaîne localisée semblable à Yes. /// diff --git a/FModel/Properties/Resources.resx b/FModel/Properties/Resources.resx index 5b4f05c6..333ea28b 100644 --- a/FModel/Properties/Resources.resx +++ b/FModel/Properties/Resources.resx @@ -1063,4 +1063,7 @@ It's now the most used free software to leak on Fortnite. Position / Value + + ..\Resources\Xml.xshd;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + \ No newline at end of file diff --git a/FModel/Resources/Xml.xshd b/FModel/Resources/Xml.xshd new file mode 100644 index 00000000..2c0db25b --- /dev/null +++ b/FModel/Resources/Xml.xshd @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + <!-- + --> + + + <!\[CDATA\[ + ]]> + + + <!DOCTYPE + > + + + <\? + \?> + + + < + > + + + + " + "|(?=<) + + + ' + '|(?=<) + + [\d\w_\-\.]+(?=(\s*=)) + = + + + + + + + + & + [\w\d\#]+ + ; + + + + & + [\w\d\#]* + #missing ; + + + \ No newline at end of file diff --git a/FModel/Utils/Assets.cs b/FModel/Utils/Assets.cs index ebd80b7b..2f327247 100644 --- a/FModel/Utils/Assets.cs +++ b/FModel/Utils/Assets.cs @@ -25,6 +25,7 @@ using System.Text; using FModel.ViewModels.DataGrid; using static FModel.Creator.FortniteCreator; using static FModel.Creator.ValorantCreator; +using FModel.PakReader; namespace FModel.Utils { @@ -72,6 +73,7 @@ namespace FModel.Utils switch (selected.PakEntry.GetExtension()) { case ".ini": + case ".txt": { using var asset = GetMemoryStream(selected.PakEntry.PakFileName, mount + selected.PakEntry.GetPathWithoutExtension()); asset.Position = 0; @@ -79,6 +81,14 @@ namespace FModel.Utils AvalonEditVm.avalonEditViewModel.Set(reader.ReadToEnd(), mount + selected.PakEntry.Name, AvalonEditVm.IniHighlighter); break; } + case ".xml": + { + using var asset = GetMemoryStream(selected.PakEntry.PakFileName, mount + selected.PakEntry.GetPathWithoutExtension()); + asset.Position = 0; + using var reader = new StreamReader(asset); + AvalonEditVm.avalonEditViewModel.Set(reader.ReadToEnd(), mount + selected.PakEntry.Name, AvalonEditVm.XmlHighlighter); + break; + } case ".uproject": case ".uplugin": case ".upluginmanifest": @@ -132,6 +142,21 @@ namespace FModel.Utils case ".ushaderbytecode": case ".pck": break; + case ".bnk": + { + using var asset = GetMemoryStream(selected.PakEntry.PakFileName, mount + selected.PakEntry.GetPathWithoutExtension()); + asset.Position = 0; + BnkReader bnk = new BnkReader(new BinaryReader(asset)); + Application.Current.Dispatcher.Invoke(delegate + { + DebugHelper.WriteLine("{0} {1} {2}", "[FModel]", "[Window]", $"Opening Audio Player for {selected.PakEntry.GetNameWithExtension()}"); + if (!FWindows.IsWindowOpen(Properties.Resources.AudioPlayer)) + new AudioPlayer().LoadFiles(bnk.AudioFiles, selected.PakEntry.GetPathWithoutFile()); + else + ((AudioPlayer)FWindows.GetOpenedWindow(Properties.Resources.AudioPlayer)).LoadFiles(bnk.AudioFiles, selected.PakEntry.GetPathWithoutFile()); + }); + break; + } default: AvalonEditVm.avalonEditViewModel.Set(GetJsonProperties(selected.PakEntry, mount, true), mount + selected.PakEntry.Name); break; diff --git a/FModel/ViewModels/AvalonEdit/AvalonEditViewModel.cs b/FModel/ViewModels/AvalonEdit/AvalonEditViewModel.cs index f93383f5..f0b95251 100644 --- a/FModel/ViewModels/AvalonEdit/AvalonEditViewModel.cs +++ b/FModel/ViewModels/AvalonEdit/AvalonEditViewModel.cs @@ -87,6 +87,7 @@ namespace FModel.ViewModels.AvalonEdit public static readonly IHighlightingDefinition JsonHighlighter = LoadHighlighter("Json.xshd"); public static readonly IHighlightingDefinition IniHighlighter = LoadHighlighter("Ini.xshd"); + public static readonly IHighlightingDefinition XmlHighlighter = LoadHighlighter("Xml.xshd"); public static IHighlightingDefinition LoadHighlighter(string resourceName) { Assembly executingAssembly = Assembly.GetExecutingAssembly(); diff --git a/FModel/ViewModels/ListBox/ListBoxViewModel.cs b/FModel/ViewModels/ListBox/ListBoxViewModel.cs index b1a1506d..9cfb3e01 100644 --- a/FModel/ViewModels/ListBox/ListBoxViewModel.cs +++ b/FModel/ViewModels/ListBox/ListBoxViewModel.cs @@ -1,12 +1,14 @@ using FModel.Utils; using PakReader.Parsers.Objects; using System; +using System.Collections.ObjectModel; namespace FModel.ViewModels.ListBox { static class ListBoxVm { public static ObservableSortedList gameFiles = new ObservableSortedList(); + public static ObservableCollection soundFiles = new ObservableCollection(); } public class ListBoxViewModel : PropertyChangedBase, IComparable @@ -34,4 +36,39 @@ namespace FModel.ViewModels.ListBox set { this.SetProperty(ref this._pakEntry, value); } } } + + public class ListBoxViewModel2 : PropertyChangedBase + { + private string _content; + public string Content + { + get { return _content; } + + set { this.SetProperty(ref this._content, value); } + } + + private string _fullPath; + public string FullPath + { + get { return _fullPath; } + + set { this.SetProperty(ref this._fullPath, value); } + } + + private string _folder; + public string Folder + { + get { return _folder; } + + set { this.SetProperty(ref this._folder, value); } + } + + private byte[] _data; + public byte[] Data + { + get { return _data; } + + set { this.SetProperty(ref this._data, value); } + } + } } diff --git a/FModel/Windows/Launcher/FLauncher.xaml.cs b/FModel/Windows/Launcher/FLauncher.xaml.cs index 7075534f..59251109 100644 --- a/FModel/Windows/Launcher/FLauncher.xaml.cs +++ b/FModel/Windows/Launcher/FLauncher.xaml.cs @@ -80,7 +80,7 @@ namespace FModel.Windows.Launcher } string spellbreakerFilesPath = Paks.GetSpellbreakPakFilesPath(); - if (!string.IsNullOrEmpty(battlebreakersFilesPath)) + if (!string.IsNullOrEmpty(spellbreakerFilesPath)) { DebugHelper.WriteLine("{0} {1} {2}", "[FModel]", "[LauncherInstalled.dat]", $"Spellbreak found at {spellbreakerFilesPath}"); Globals.gNotifier.ShowCustomMessage("Spellbreak", Properties.Resources.PathAutoDetected, "/FModel;component/Resources/spellbreak.ico"); diff --git a/FModel/Windows/SoundPlayer/AudioPlayer.xaml b/FModel/Windows/SoundPlayer/AudioPlayer.xaml index 0f8f8148..3a4e69ae 100644 --- a/FModel/Windows/SoundPlayer/AudioPlayer.xaml +++ b/FModel/Windows/SoundPlayer/AudioPlayer.xaml @@ -4,9 +4,12 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:properties="clr-namespace:FModel.Properties" + xmlns:utils="clr-namespace:FModel.Utils" mc:Ignorable="d" Style="{StaticResource {x:Type Window}}" - Title="{x:Static properties:Resources.AudioPlayer}" Height="300" MinHeight="300" Width="1000" MinWidth="1000" + Title="{x:Static properties:Resources.AudioPlayer}" + Height="{Binding Source={x:Static SystemParameters.MaximizedPrimaryScreenHeight}, Converter={utils:Screens}, ConverterParameter='0.50' }" + Width="{Binding Source={x:Static SystemParameters.MaximizedPrimaryScreenWidth}, Converter={utils:Screens}, ConverterParameter='0.50' }" WindowStartupLocation="CenterScreen" Icon="/FModel;component/FModel.ico" Closed="OnClosed"> @@ -19,85 +22,116 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + - + + + diff --git a/FModel/Windows/SoundPlayer/AudioPlayer.xaml.cs b/FModel/Windows/SoundPlayer/AudioPlayer.xaml.cs index 8913e2db..b644f549 100644 --- a/FModel/Windows/SoundPlayer/AudioPlayer.xaml.cs +++ b/FModel/Windows/SoundPlayer/AudioPlayer.xaml.cs @@ -1,8 +1,11 @@ using FModel.Discord; +using FModel.ViewModels.ListBox; using FModel.ViewModels.SoundPlayer; using FModel.Windows.SoundPlayer.Visualization; using Microsoft.Win32; using System; +using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Windows; @@ -20,6 +23,7 @@ namespace FModel.Windows.SoundPlayer private UserControls.SpectrumAnalyzer spectrumAnalyzer; private UserControls.Timeline timeline; private UserControls.Timeclock timeclock; + private string _oldPlayedSound = string.Empty; public AudioPlayer() { @@ -31,12 +35,14 @@ namespace FModel.Windows.SoundPlayer private void OnClosed(object sender, EventArgs e) { output.Stop(); + ListBoxVm.soundFiles.Clear(); DiscordIntegration.Restore(); InputFileVm.inputFileViewModel.Reset(); } private void Startup() { DiscordIntegration.SaveCurrentPresence(); + Sound_LstBox.ItemsSource = ListBoxVm.soundFiles; AudioPlayer_TabItm.DataContext = InputFileVm.inputFileViewModel; AudioDevices_CmbBox.ItemsSource = InputFileVm.inputFileViewModel.Devices; AudioDevices_CmbBox.SelectedItem = InputFileVm.inputFileViewModel.Devices.Where(x => x.DeviceId == Properties.Settings.Default.AudioPlayerDevice).FirstOrDefault(); @@ -54,30 +60,69 @@ namespace FModel.Windows.SoundPlayer Time.Content = timeline; } - private void OnOpenClick(object sender, RoutedEventArgs e) + private void OnAddClick(object sender, RoutedEventArgs e) { var ofd = new OpenFileDialog { Title = Properties.Resources.SelectFile, Filter = Properties.Resources.OggFilter, + Multiselect = true, InitialDirectory = Properties.Settings.Default.OutputPath + "\\Sounds\\" }; if ((bool)ofd.ShowDialog()) - LoadFile(ofd.FileName); + { + foreach (string file in ofd.FileNames) + LoadFile(file); + } + } + + public void LoadFiles(Dictionary files, string gameFolder) + { + Focus(); + + ListBoxVm.soundFiles.Clear(); + foreach (var (key, value) in files) + { + ListBoxVm.soundFiles.Add(new ListBoxViewModel2 + { + Content = key, + Data = value, + FullPath = string.Empty, + Folder = gameFolder + }); + } } public void LoadFile(string filepath) { Focus(); - output.Stop(); - output.Load(filepath); - output.Play(); + var item = new ListBoxViewModel2 + { + Content = Path.GetFileName(filepath), + Data = null, + FullPath = filepath, + Folder = string.Empty + }; + ListBoxVm.soundFiles.Add(item); - string name = Path.GetFileName(filepath); - InputFileVm.inputFileViewModel.Set(name, output); - DiscordIntegration.Update(string.Empty, string.Format(Properties.Resources.Listening, name)); - PlayPauseImg.Source = new BitmapImage(new Uri("pack://application:,,,/Resources/pause.png")); + if (ListBoxVm.soundFiles.Count == 1) // auto play if one in queue + { + output.Stop(); + output.Load(filepath); + output.Play(); + Sound_LstBox.SelectedIndex = 0; + + string name = Path.GetFileName(filepath); + InputFileVm.inputFileViewModel.Set(name, output); + DiscordIntegration.Update(string.Empty, string.Format(Properties.Resources.Listening, name)); + PlayPauseImg.Source = new BitmapImage(new Uri("pack://application:,,,/Resources/pause.png")); + } + else + { + Sound_LstBox.SelectedIndex = ListBoxVm.soundFiles.IndexOf(item); + Sound_LstBox.ScrollIntoView(item); + } } private void UpdateVolume(object sender, RoutedEventArgs e) @@ -89,7 +134,18 @@ namespace FModel.Windows.SoundPlayer { if (output.HasMedia) { - if (output.IsPlaying) + if (!output.FileName.Equals(_oldPlayedSound)) + { + output.Stop(); + output.Load(_oldPlayedSound); + output.Play(); + + string name = Path.GetFileName(_oldPlayedSound); + InputFileVm.inputFileViewModel.Set(name, output); + DiscordIntegration.Update(string.Empty, string.Format(Properties.Resources.Listening, name)); + PlayPauseImg.Source = new BitmapImage(new Uri("pack://application:,,,/Resources/pause.png")); + } + else if (output.IsPlaying) { output.Pause(); PlayPauseImg.Source = new BitmapImage(new Uri("pack://application:,,,/Resources/play.png")); @@ -100,6 +156,19 @@ namespace FModel.Windows.SoundPlayer PlayPauseImg.Source = new BitmapImage(new Uri("pack://application:,,,/Resources/pause.png")); } } + else + { + if (Sound_LstBox.SelectedIndex > -1 && Sound_LstBox.SelectedItem is ListBoxViewModel2 selected) + { + output.Stop(); + output.Load(selected.FullPath); + output.Play(); + + InputFileVm.inputFileViewModel.Set(selected.Content, output); + DiscordIntegration.Update(string.Empty, string.Format(Properties.Resources.Listening, selected.Content)); + PlayPauseImg.Source = new BitmapImage(new Uri("pack://application:,,,/Resources/pause.png")); + } + } } private void OnStopClick(object sender, RoutedEventArgs e) @@ -125,5 +194,46 @@ namespace FModel.Windows.SoundPlayer output.SwapDevice(d); } } + + private void OnSelectedItemChanged(object sender, SelectionChangedEventArgs e) + { + if (sender is ListBox listBox && listBox.SelectedItem is ListBoxViewModel2 selectedItem) + { + // vgmstream convert on select + if (string.IsNullOrEmpty(selectedItem.FullPath) && selectedItem.Data != null) + { + string file = Properties.Settings.Default.OutputPath + "\\vgmstream\\test.exe"; + if (File.Exists(file)) + { + string folder = Properties.Settings.Default.OutputPath + "\\Sounds\\" + selectedItem.Folder + "\\"; + Directory.CreateDirectory(folder); + File.WriteAllBytes(folder + selectedItem.Content, selectedItem.Data); + string newFile = Path.ChangeExtension(folder + selectedItem.Content, ".wav"); + var vgmstream = Process.Start(new ProcessStartInfo + { + FileName = file, + Arguments = $"-o \"{newFile}\" \"{folder + selectedItem.Content}\"", + UseShellExecute = false, + RedirectStandardOutput = true, + CreateNoWindow = true + }); + vgmstream.WaitForExit(); + if (vgmstream.ExitCode == 0) + { + ListBoxVm.soundFiles.Remove(selectedItem); + _oldPlayedSound = newFile; + LoadFile(newFile); + } + } + } + else if (!_oldPlayedSound.Equals(selectedItem.FullPath)) + _oldPlayedSound = selectedItem.FullPath; + + if (output.HasMedia && output.FileName.Equals(_oldPlayedSound)) + PlayPauseImg.Source = new BitmapImage(new Uri("pack://application:,,,/Resources/pause.png")); + else + PlayPauseImg.Source = new BitmapImage(new Uri("pack://application:,,,/Resources/play.png")); + } + } } } diff --git a/FModel/Windows/SoundPlayer/Visualization/OutputSource.cs b/FModel/Windows/SoundPlayer/Visualization/OutputSource.cs index 1ddbd068..624c560d 100644 --- a/FModel/Windows/SoundPlayer/Visualization/OutputSource.cs +++ b/FModel/Windows/SoundPlayer/Visualization/OutputSource.cs @@ -12,6 +12,10 @@ namespace FModel.Windows.SoundPlayer.Visualization public class OutputSource : ISource, IDisposable { private string _filename; + public string FileName + { + get { return _filename; } + } private Uri _uri; private IWaveSource _waveSource; private ISampleSource _sampleSource;