FModel/FModel/ViewModels/AudioPlayerViewModel.cs
Masusder 639f21e574
Some checks failed
FModel QA Builder / build (push) Has been cancelled
added links to some exceptions + auto detect ue version (#657)
Co-authored-by: Asval <asval.contactme@gmail.com>
Co-authored-by: Krowe-moh <27891447+Krowe-moh@users.noreply.github.com>
2026-03-13 21:14:56 +01:00

751 lines
23 KiB
C#

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using System.Windows;
using System.Windows.Data;
using CSCore;
using CSCore.CoreAudioAPI;
using CSCore.DSP;
using CSCore.SoundOut;
using CSCore.Streams;
using CUE4Parse.UE4.CriWare.Decoders;
using CUE4Parse.UE4.CriWare.Decoders.ADX;
using CUE4Parse.UE4.CriWare.Decoders.HCA;
using CUE4Parse.Utils;
using FModel.Extensions;
using FModel.Framework;
using FModel.Services;
using FModel.Settings;
using FModel.ViewModels.Commands;
using FModel.Views.Resources.Controls;
using FModel.Views.Resources.Controls.Aup;
using Microsoft.Win32;
using Serilog;
namespace FModel.ViewModels;
public class AudioFile : ViewModel
{
private string _filePath;
public string FilePath
{
get => _filePath;
private set => SetProperty(ref _filePath, value);
}
private string _fileName;
public string FileName
{
get => _fileName;
private set => SetProperty(ref _fileName, value);
}
private long _length;
public long Length
{
get => _length;
private set => SetProperty(ref _length, value);
}
private TimeSpan _duration = TimeSpan.Zero;
public TimeSpan Duration
{
get => _duration;
set => SetProperty(ref _duration, value);
}
private TimeSpan _position = TimeSpan.Zero;
public TimeSpan Position
{
get => _position;
set => SetProperty(ref _position, value);
}
private AudioEncoding _encoding = AudioEncoding.Unknown;
public AudioEncoding Encoding
{
get => _encoding;
set => SetProperty(ref _encoding, value);
}
private PlaybackState _playbackState = PlaybackState.Stopped;
public PlaybackState PlaybackState
{
get => _playbackState;
set => SetProperty(ref _playbackState, value);
}
private int _bytesPerSecond;
public int BytesPerSecond
{
get => _bytesPerSecond;
set => SetProperty(ref _bytesPerSecond, value);
}
public int Id { get; set; }
public byte[] Data { get; set; }
public string Extension { get; }
public AudioFile(int id, byte[] data, string filePath)
{
Id = id;
FilePath = filePath;
FileName = filePath.SubstringAfterLast("/");
Length = data.Length;
Duration = TimeSpan.Zero;
Position = TimeSpan.Zero;
Encoding = AudioEncoding.Unknown;
PlaybackState = PlaybackState.Stopped;
BytesPerSecond = 0;
Extension = filePath.SubstringAfterLast(".");
Data = data;
}
public AudioFile(int id, string fileName)
{
Id = id;
FilePath = string.Empty;
FileName = fileName;
Length = 0;
Duration = TimeSpan.Zero;
Position = TimeSpan.Zero;
Encoding = AudioEncoding.Unknown;
PlaybackState = PlaybackState.Stopped;
BytesPerSecond = 0;
Extension = string.Empty;
Data = null;
}
public AudioFile(int id, FileInfo fileInfo)
{
Id = id;
FilePath = fileInfo.FullName.Replace('\\', '/');
FileName = fileInfo.Name;
Length = fileInfo.Length;
Duration = TimeSpan.Zero;
Position = TimeSpan.Zero;
Encoding = AudioEncoding.Unknown;
PlaybackState = PlaybackState.Stopped;
BytesPerSecond = 0;
Extension = fileInfo.Extension[1..];
Data = File.ReadAllBytes(fileInfo.FullName);
}
public AudioFile(AudioFile audioFile, IAudioSource wave)
{
Id = audioFile.Id;
FilePath = audioFile.FilePath;
FileName = audioFile.FileName;
Length = audioFile.Length;
Duration = wave.GetLength();
Position = audioFile.Position;
Encoding = wave.WaveFormat.WaveFormatTag;
PlaybackState = audioFile.PlaybackState;
BytesPerSecond = wave.WaveFormat.BytesPerSecond;
Extension = audioFile.Extension;
Data = audioFile.Data;
}
public override string ToString()
{
return $"{Id} | {FileName} | {Length}";
}
}
public class AudioPlayerViewModel : ViewModel, ISource, IDisposable
{
private DiscordHandler _discordHandler => DiscordService.DiscordHandler;
private static IWaveSource _waveSource;
private static ISoundOut _soundOut;
private Timer _sourceTimer;
private TimeSpan _length => _waveSource?.GetLength() ?? TimeSpan.Zero;
private TimeSpan _position => _waveSource?.GetPosition() ?? TimeSpan.Zero;
private PlaybackState _playbackState => _soundOut?.PlaybackState ?? PlaybackState.Stopped;
private bool _hideToggle = false;
public SpectrumProvider Spectrum { get; private set; }
public float[] FftData { get; private set; }
private AudioFile _playedFile = new(-1, "No audio file");
public AudioFile PlayedFile
{
get => _playedFile;
private set => SetProperty(ref _playedFile, value);
}
private AudioFile _selectedAudioFile;
public AudioFile SelectedAudioFile
{
get => _selectedAudioFile;
set => SetProperty(ref _selectedAudioFile, value);
}
private MMDevice _selectedAudioDevice;
public MMDevice SelectedAudioDevice
{
get => _selectedAudioDevice;
set => SetProperty(ref _selectedAudioDevice, value);
}
private AudioCommand _audioCommand;
public AudioCommand AudioCommand => _audioCommand ??= new AudioCommand(this);
public bool IsStopped => PlayedFile.PlaybackState == PlaybackState.Stopped;
public bool IsPlaying => PlayedFile.PlaybackState == PlaybackState.Playing;
public bool IsPaused => PlayedFile.PlaybackState == PlaybackState.Paused;
private readonly ObservableCollection<AudioFile> _audioFiles;
public ICollectionView AudioFilesView { get; }
public ICollectionView AudioDevicesView { get; }
public AudioPlayerViewModel()
{
_sourceTimer = new Timer(TimerTick, null, 0, 10);
_audioFiles = new ObservableCollection<AudioFile>();
AudioFilesView = new ListCollectionView(_audioFiles);
var audioDevices = new ObservableCollection<MMDevice>(EnumerateDevices());
AudioDevicesView = new ListCollectionView(audioDevices) { SortDescriptions = { new SortDescription("FriendlyName", ListSortDirection.Ascending) } };
SelectedAudioDevice ??= audioDevices.FirstOrDefault();
}
public void Load()
{
Application.Current.Dispatcher.Invoke(() =>
{
if (!ConvertIfNeeded())
return;
_waveSource = new CustomCodecFactory().GetCodec(SelectedAudioFile.Data, SelectedAudioFile.Extension);
if (_waveSource == null)
return;
PlayedFile = new AudioFile(SelectedAudioFile, _waveSource);
Spectrum = new SpectrumProvider(_waveSource.WaveFormat.Channels, _waveSource.WaveFormat.SampleRate, FftSize.Fft4096);
var notificationSource = new SingleBlockNotificationStream(_waveSource.ToSampleSource());
notificationSource.SingleBlockRead += (s, a) => Spectrum.Add(a.Left, a.Right);
_waveSource = notificationSource.ToWaveSource(16);
RaiseSourceEvent(ESourceEventType.Loading);
LoadSoundOut();
});
}
public void AddToPlaylist(byte[] data, string filePath)
{
Application.Current.Dispatcher.Invoke(() =>
{
_audioFiles.Add(new AudioFile(_audioFiles.Count, data, filePath));
if (_audioFiles.Count > 1) return;
SelectedAudioFile = _audioFiles.Last();
Load();
Play();
});
}
public void AddToPlaylist(string filePath)
{
Application.Current.Dispatcher.Invoke(() =>
{
_audioFiles.Add(new AudioFile(_audioFiles.Count, new FileInfo(filePath)));
if (_audioFiles.Count > 1) return;
SelectedAudioFile = _audioFiles.Last();
Load();
Play();
});
}
public void Remove()
{
if (_audioFiles.Count < 1) return;
Application.Current.Dispatcher.Invoke(() =>
{
_audioFiles.RemoveAt(SelectedAudioFile.Id);
for (var i = 0; i < _audioFiles.Count; i++)
{
_audioFiles[i].Id = i;
}
});
}
public void Replace(AudioFile newAudio)
{
if (_audioFiles.Count < 1) return;
Application.Current.Dispatcher.Invoke(() =>
{
_audioFiles.Insert(SelectedAudioFile.Id, newAudio);
_audioFiles.RemoveAt(SelectedAudioFile.Id + 1);
SelectedAudioFile = newAudio;
});
}
public void SavePlaylist()
{
if (_audioFiles.Count < 1) return;
Application.Current.Dispatcher.Invoke(() =>
{
foreach (var a in _audioFiles)
{
Save(a, true);
}
if (_audioFiles.Count > 1)
{
var dir = new DirectoryInfo(Path.GetDirectoryName(_audioFiles.First().FilePath));
FLogger.Append(ELog.Information, () =>
{
FLogger.Text($"Successfully saved {_audioFiles.Count} audio files to ", Constants.WHITE);
FLogger.Link(dir.Name, dir.FullName, true);
});
}
else
{
FLogger.Append(ELog.Information, () =>
{
FLogger.Text("Successfully saved ", Constants.WHITE);
FLogger.Link(_audioFiles.First().FileName, _audioFiles.First().FilePath, true);
});
}
});
}
public void Save(AudioFile file = null, bool auto = false)
{
var fileToSave = file ?? SelectedAudioFile;
if (_audioFiles.Count < 1 || fileToSave?.Data == null) return;
var path = fileToSave.FilePath;
if (!auto)
{
var saveFileDialog = new SaveFileDialog
{
Title = "Save Audio",
FileName = fileToSave.FileName,
InitialDirectory = UserSettings.Default.AudioDirectory
};
if (!saveFileDialog.ShowDialog().GetValueOrDefault()) return;
path = saveFileDialog.FileName;
}
else
{
Directory.CreateDirectory(path.SubstringBeforeLast('/'));
}
using (var stream = new FileStream(path, FileMode.Create, FileAccess.Write))
using (var writer = new BinaryWriter(stream))
{
writer.Write(fileToSave.Data);
writer.Flush();
}
if (File.Exists(path))
{
Log.Information("{FileName} successfully saved", fileToSave.FileName);
if (!auto)
{
FLogger.Append(ELog.Information, () =>
{
FLogger.Text("Successfully saved ", Constants.WHITE);
FLogger.Link(fileToSave.FileName, path, true);
});
}
}
else
{
Log.Error("{FileName} could not be saved", fileToSave.FileName);
if (!auto)
{
FLogger.Append(ELog.Error, () => FLogger.Text($"Could not save '{fileToSave.FileName}'", Constants.WHITE, true));
}
}
}
public void PlayPauseOnStart()
{
if (IsStopped)
{
Load();
Play();
}
else if (IsPaused)
{
Play();
}
else if (IsPlaying)
{
Pause();
}
}
public void PlayPauseOnForce()
{
if (_audioFiles.Count < 1 || SelectedAudioFile.Id == PlayedFile.Id) return;
Stop();
Load();
Play();
}
public void Next()
{
if (_audioFiles.Count < 1) return;
Stop();
SelectedAudioFile = _audioFiles.Next(PlayedFile.Id);
Load();
Play();
}
public void Previous()
{
if (_audioFiles.Count < 1) return;
Stop();
SelectedAudioFile = _audioFiles.Previous(PlayedFile.Id);
Load();
Play();
}
public void Play()
{
if (_soundOut == null || IsPlaying) return;
_discordHandler.UpdateButDontSavePresence(null, $"Audio Player: {PlayedFile.FileName} ({PlayedFile.Duration:g})");
_soundOut.Play();
}
public void Pause()
{
if (_soundOut == null || IsPaused) return;
_soundOut.Pause();
}
public void Resume()
{
if (_soundOut == null || !IsPaused) return;
_soundOut.Resume();
}
public void Stop()
{
if (_soundOut == null || IsStopped) return;
_soundOut.Stop();
}
public void HideToggle()
{
if (!IsPlaying) return;
_hideToggle = !_hideToggle;
RaiseSourcePropertyChangedEvent(ESourceProperty.HideToggle, _hideToggle);
}
public void SkipTo(double percentage)
{
if (_soundOut == null || _waveSource == null) return;
_waveSource.Position = (long) (_waveSource.Length * percentage);
}
public void Volume()
{
if (_soundOut == null) return;
_soundOut.Volume = UserSettings.Default.AudioPlayerVolume / 100;
}
public void Device()
{
if (_soundOut == null) return;
Pause();
LoadSoundOut();
Play();
}
public void Dispose()
{
Application.Current.Dispatcher.Invoke(() =>
{
if (_waveSource != null)
{
_waveSource.Dispose();
_waveSource = null;
}
if (_soundOut != null)
{
_soundOut.Dispose();
_soundOut = null;
}
if (Spectrum != null)
Spectrum = null;
foreach (var a in _audioFiles)
{
a.Data = null;
}
_audioFiles.Clear();
PlayedFile = new AudioFile(-1, "No audio file");
});
}
private void TimerTick(object state)
{
if (_waveSource == null || _soundOut == null) return;
if (_position != PlayedFile.Position)
{
PlayedFile.Position = _position;
RaiseSourcePropertyChangedEvent(ESourceProperty.Position, PlayedFile.Position);
}
if (_playbackState != PlayedFile.PlaybackState)
{
PlayedFile.PlaybackState = _playbackState;
RaiseSourcePropertyChangedEvent(ESourceProperty.PlaybackState, PlayedFile.PlaybackState);
}
if (Spectrum != null && PlayedFile.PlaybackState == PlaybackState.Playing)
{
FftData = new float[4096];
Spectrum.GetFftData(FftData);
RaiseSourcePropertyChangedEvent(ESourceProperty.FftData, FftData);
}
}
private void LoadSoundOut()
{
if (_waveSource == null) return;
_soundOut = new WasapiOut(true, AudioClientShareMode.Shared, 100, ThreadPriority.Highest) { Device = SelectedAudioDevice };
_soundOut.Initialize(_waveSource.ToSampleSource().ToWaveSource(16));
_soundOut.Volume = UserSettings.Default.AudioPlayerVolume / 100;
}
private IEnumerable<MMDevice> EnumerateDevices()
{
using var deviceEnumerator = new MMDeviceEnumerator();
using var deviceCollection = deviceEnumerator.EnumAudioEndpoints(DataFlow.Render, DeviceState.Active);
foreach (var device in deviceCollection)
{
if (device.DeviceID == UserSettings.Default.AudioDeviceId)
SelectedAudioDevice = device;
yield return device;
}
}
public event EventHandler<SourceEventArgs> SourceEvent;
public event EventHandler<SourcePropertyChangedEventArgs> SourcePropertyChangedEvent = (sender, args) =>
{
if (sender is not AudioPlayerViewModel viewModel) return;
switch (args.Property)
{
case ESourceProperty.PlaybackState:
{
if (viewModel._position == viewModel._length && (PlaybackState) args.Value == PlaybackState.Stopped)
viewModel.Next();
break;
}
}
};
private void RaiseSourceEvent(ESourceEventType e)
{
SourceEvent?.Invoke(this, new SourceEventArgs(e));
}
private void RaiseSourcePropertyChangedEvent(ESourceProperty property, object value)
{
SourcePropertyChangedEvent?.Invoke(this, new SourcePropertyChangedEventArgs(property, value));
}
private bool ConvertIfNeeded()
{
if (SelectedAudioFile?.Data == null)
return false;
switch (SelectedAudioFile.Extension)
{
case "binka":
case "adpcm":
case "xvag":
case "opus":
case "wem":
case "at9":
case "raw":
{
if (TryConvert(out var wavFilePath))
{
var newAudio = new AudioFile(SelectedAudioFile.Id, new FileInfo(wavFilePath));
Replace(newAudio);
return true;
}
return false;
}
case "adx":
case "hca":
return TryConvertCriware();
case "rada":
{
if (TryDecode(SelectedAudioFile.Extension, out var rawFilePath))
{
var newAudio = new AudioFile(SelectedAudioFile.Id, new FileInfo(rawFilePath));
Replace(newAudio);
return true;
}
return false;
}
}
return true;
}
private bool TryConvertCriware()
{
try
{
byte[] wavData = SelectedAudioFile.Extension switch
{
"hca" => HcaWaveStream.ConvertHcaToWav(
SelectedAudioFile.Data,
UserSettings.Default.CurrentDir.CriwareDecryptionKey),
"adx" => AdxDecoder.ConvertAdxToWav(
SelectedAudioFile.Data,
UserSettings.Default.CurrentDir.CriwareDecryptionKey),
_ => throw new NotSupportedException()
};
if (wavData.Length is 0)
{
if (TryConvert(out var wavFilePathFallback))
{
var newAudioFallback = new AudioFile(SelectedAudioFile.Id, new FileInfo(wavFilePathFallback));
Replace(newAudioFallback);
return true;
}
}
string wavFilePath = Path.Combine(
UserSettings.Default.AudioDirectory,
SelectedAudioFile.FilePath.TrimStart('/'));
wavFilePath = Path.ChangeExtension(wavFilePath, ".wav");
Directory.CreateDirectory(Path.GetDirectoryName(wavFilePath)!);
File.WriteAllBytes(wavFilePath, wavData);
var newAudio = new AudioFile(SelectedAudioFile.Id, new FileInfo(wavFilePath));
Replace(newAudio);
return true;
}
catch (CriwareDecryptionException ex)
{
FLogger.Append(ELog.Error, () => FLogger.Text($"Encrypted {SelectedAudioFile.Extension.ToUpper()}: {ex.Message}", Constants.WHITE, true));
Log.Error($"Encrypted {SelectedAudioFile.Extension.ToUpper()}: {ex.Message}");
return false;
}
catch (Exception ex)
{
FLogger.Append(ELog.Error, () => FLogger.Text($"Failed to convert {SelectedAudioFile.Extension.ToUpper()}: {ex.Message}", Constants.WHITE, true));
Log.Error($"Failed to convert {SelectedAudioFile.Extension.ToUpper()}: {ex.Message}");
return false;
}
}
private bool TryConvert(out string wavFilePath) => TryConvert(SelectedAudioFile.FilePath, SelectedAudioFile.Data, out wavFilePath, true);
public static bool TryConvert(string inputFilePath, byte[] inputFileData, out string wavFilePath, bool updateUi = false)
{
wavFilePath = string.Empty;
var vgmFilePath = Path.Combine(UserSettings.Default.OutputDirectory, ".data", "test.exe");
if (!File.Exists(vgmFilePath))
{
vgmFilePath = Path.Combine(UserSettings.Default.OutputDirectory, ".data", "vgmstream-cli.exe");
if (!File.Exists(vgmFilePath))
{
Log.Error("Failed to convert {InputFilePath}, vgmstream is missing", inputFilePath);
FLogger.Append(ELog.Error, () =>
{
FLogger.Text("Failed to convert audio because vgmstream is missing. See: ", Constants.WHITE);
FLogger.Link("→ link ←", Constants.AUDIO_ISSUE_LINK, true);
});
return false;
}
}
Directory.CreateDirectory(inputFilePath.SubstringBeforeLast("/"));
File.WriteAllBytes(inputFilePath, inputFileData);
wavFilePath = Path.ChangeExtension(inputFilePath, ".wav");
var vgmProcess = Process.Start(new ProcessStartInfo
{
FileName = vgmFilePath,
Arguments = $"-o \"{wavFilePath}\" \"{inputFilePath}\"",
UseShellExecute = false,
CreateNoWindow = true
});
vgmProcess?.WaitForExit(5000);
File.Delete(inputFilePath);
var success = vgmProcess?.ExitCode == 0 && File.Exists(wavFilePath);
if (!success)
{
Log.Error("Failed to convert {InputFilePath} to .wav format", inputFilePath);
if (updateUi)
{
FLogger.Append(ELog.Error, () =>
{
FLogger.Text("Failed to convert audio to .wav format. See: ", Constants.WHITE);
FLogger.Link("→ link ←", Constants.AUDIO_ISSUE_LINK, true);
});
}
}
return success;
}
private bool TryDecode(string extension, out string rawFilePath)
{
rawFilePath = string.Empty;
var decoderPath = Path.Combine(UserSettings.Default.OutputDirectory, ".data", $"{extension}dec.exe");
if (!File.Exists(decoderPath))
{
Log.Error("Failed to convert {FilePath}, rada decoder is missing", SelectedAudioFile.FilePath);
FLogger.Append(ELog.Error, () =>
{
FLogger.Text("Failed to convert audio because rada decoder is missing. See: ", Constants.WHITE);
FLogger.Link("→ link ←", Constants.RADA_ISSUE_LINK, true);
});
return false;
}
Directory.CreateDirectory(SelectedAudioFile.FilePath.SubstringBeforeLast("/"));
File.WriteAllBytes(SelectedAudioFile.FilePath, SelectedAudioFile.Data);
rawFilePath = Path.ChangeExtension(SelectedAudioFile.FilePath, ".wav");
var decoderProcess = Process.Start(new ProcessStartInfo
{
FileName = decoderPath,
Arguments = $"-i \"{SelectedAudioFile.FilePath}\" -o \"{rawFilePath}\"",
UseShellExecute = false,
CreateNoWindow = true
});
decoderProcess?.WaitForExit(5000);
File.Delete(SelectedAudioFile.FilePath);
return decoderProcess?.ExitCode == 0 && File.Exists(rawFilePath);
}
}