FModel/FModel/ViewModels/AudioPlayerViewModel.cs

592 lines
17 KiB
C#

using CSCore;
using CSCore.DSP;
using CSCore.SoundOut;
using CSCore.Streams;
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.CoreAudioAPI;
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);
}
});
}
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);
FLogger.AppendInformation();
FLogger.AppendText($"Successfully saved '{fileToSave.FileName}'", Constants.WHITE, true);
}
else
{
Log.Error("{FileName} could not be saved", fileToSave.FileName);
FLogger.AppendError();
FLogger.AppendText($"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 "adpcm":
case "opus":
case "wem":
{
if (TryConvert(out var wavFilePath) && !string.IsNullOrEmpty(wavFilePath))
{
var newAudio = new AudioFile(SelectedAudioFile.Id, new FileInfo(wavFilePath));
Replace(newAudio);
return true;
}
return false;
}
}
return true;
}
private bool TryConvert(out string wavFilePath)
{
wavFilePath = string.Empty;
var vgmFilePath = Path.Combine(UserSettings.Default.OutputDirectory, ".data", "test.exe");
if (!File.Exists(vgmFilePath)) return false;
Directory.CreateDirectory(SelectedAudioFile.FilePath.SubstringBeforeLast("/"));
File.WriteAllBytes(SelectedAudioFile.FilePath, SelectedAudioFile.Data);
wavFilePath = Path.ChangeExtension(SelectedAudioFile.FilePath, ".wav");
var vgmProcess = Process.Start(new ProcessStartInfo
{
FileName = vgmFilePath,
Arguments = $"-o \"{wavFilePath}\" \"{SelectedAudioFile.FilePath}\"",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
});
vgmProcess?.WaitForExit();
File.Delete(SelectedAudioFile.FilePath);
return vgmProcess?.ExitCode == 0 && File.Exists(wavFilePath);
}
}