Added audio player logic and tests.
This commit is contained in:
410
Harmonia.Core/Engine/BaseMediaPlayer.cs
Normal file
410
Harmonia.Core/Engine/BaseMediaPlayer.cs
Normal file
@@ -0,0 +1,410 @@
|
|||||||
|
using ManagedBass;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace Harmonia.Core.Engine;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A Reusable Channel which can Load files like a Player.
|
||||||
|
/// <para><see cref="MediaPlayer"/> is perfect for UIs, as it implements <see cref="INotifyPropertyChanged"/>.</para>
|
||||||
|
/// <para>Also, unlike normal, Properties/Effects set on a <see cref="MediaPlayer"/> persist through subsequent loads.</para>
|
||||||
|
/// </summary>
|
||||||
|
public class BaseMediaPlayer : INotifyPropertyChanged, IDisposable
|
||||||
|
{
|
||||||
|
#region Fields
|
||||||
|
readonly SynchronizationContext? _syncContext;
|
||||||
|
int _handle;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Channel Handle of the loaded audio file.
|
||||||
|
/// </summary>
|
||||||
|
protected internal int Handle
|
||||||
|
{
|
||||||
|
get => _handle;
|
||||||
|
private set
|
||||||
|
{
|
||||||
|
if (!Bass.ChannelGetInfo(value, out var info))
|
||||||
|
throw new ArgumentException("Invalid Channel Handle: " + value);
|
||||||
|
|
||||||
|
_handle = value;
|
||||||
|
|
||||||
|
// Init Events
|
||||||
|
Bass.ChannelSetSync(Handle, SyncFlags.Free, 0, GetSyncProcedure(() => Disposed?.Invoke(this, EventArgs.Empty)));
|
||||||
|
Bass.ChannelSetSync(Handle, SyncFlags.Stop, 0, GetSyncProcedure(() => MediaFailed?.Invoke(this, EventArgs.Empty)));
|
||||||
|
Bass.ChannelSetSync(Handle, SyncFlags.End, 0, GetSyncProcedure(() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!Bass.ChannelHasFlag(Handle, BassFlags.Loop))
|
||||||
|
MediaEnded?.Invoke(this, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
finally { OnStateChanged(); }
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _restartOnNextPlayback;
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
SyncProcedure GetSyncProcedure(Action Handler)
|
||||||
|
{
|
||||||
|
return (SyncHandle, Channel, Data, User) =>
|
||||||
|
{
|
||||||
|
if (Handler == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (_syncContext == null)
|
||||||
|
Handler();
|
||||||
|
else _syncContext.Post(S => Handler(), null);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static BaseMediaPlayer()
|
||||||
|
{
|
||||||
|
var currentDev = Bass.CurrentDevice;
|
||||||
|
|
||||||
|
Bass.Configure(Configuration.IncludeDefaultDevice, true);
|
||||||
|
|
||||||
|
if (currentDev == -1 || !Bass.GetDeviceInfo(Bass.CurrentDevice).IsInitialized)
|
||||||
|
Bass.Init(currentDev);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new instance of <see cref="MediaPlayer"/>.
|
||||||
|
/// </summary>
|
||||||
|
public BaseMediaPlayer() { _syncContext = SynchronizationContext.Current; }
|
||||||
|
|
||||||
|
#region Events
|
||||||
|
/// <summary>
|
||||||
|
/// Fired when this Channel is Disposed.
|
||||||
|
/// </summary>
|
||||||
|
public event EventHandler? Disposed;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fired when the Media Playback Ends
|
||||||
|
/// </summary>
|
||||||
|
public event EventHandler? MediaEnded;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fired when the Playback fails
|
||||||
|
/// </summary>
|
||||||
|
public event EventHandler? MediaFailed;
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Frequency
|
||||||
|
double _freq = 44100;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or Sets the Playback Frequency in Hertz.
|
||||||
|
/// Default is 44100 Hz.
|
||||||
|
/// </summary>
|
||||||
|
public double Frequency
|
||||||
|
{
|
||||||
|
get => _freq;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (!Bass.ChannelSetAttribute(Handle, ChannelAttribute.Frequency, value))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_freq = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Balance
|
||||||
|
double _pan;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or Sets Balance (Panning) (-1 ... 0 ... 1).
|
||||||
|
/// -1 Represents Completely Left.
|
||||||
|
/// 1 Represents Completely Right.
|
||||||
|
/// Default is 0.
|
||||||
|
/// </summary>
|
||||||
|
public double Balance
|
||||||
|
{
|
||||||
|
get => _pan;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (!Bass.ChannelSetAttribute(Handle, ChannelAttribute.Pan, value))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_pan = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Device
|
||||||
|
int _dev = -1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or Sets the Playback Device used.
|
||||||
|
/// </summary>
|
||||||
|
public int Device
|
||||||
|
{
|
||||||
|
get => (_dev = _dev == -1 ? Bass.ChannelGetDevice(Handle) : _dev);
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (!Bass.GetDeviceInfo(value).IsInitialized)
|
||||||
|
if (!Bass.Init(value))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!Bass.ChannelSetDevice(Handle, value))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_dev = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Volume
|
||||||
|
double _vol = 0.5;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or Sets the Playback Volume.
|
||||||
|
/// </summary>
|
||||||
|
public double Volume
|
||||||
|
{
|
||||||
|
get => _vol;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (!Bass.ChannelSetAttribute(Handle, ChannelAttribute.Volume, value))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_vol = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Loop
|
||||||
|
bool _loop;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or Sets whether the Playback is looped.
|
||||||
|
/// </summary>
|
||||||
|
public bool Loop
|
||||||
|
{
|
||||||
|
get => _loop;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value ? !Bass.ChannelAddFlag(Handle, BassFlags.Loop) : !Bass.ChannelRemoveFlag(Handle, BassFlags.Loop))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_loop = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Override this method for custom loading procedure.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="FileName">Path to the File to Load.</param>
|
||||||
|
/// <returns><see langword="true"/> on Success, <see langword="false"/> on failure</returns>
|
||||||
|
protected virtual int OnLoad(string FileName) => Bass.CreateStream(FileName);
|
||||||
|
|
||||||
|
#region Tags
|
||||||
|
string _title = "", _artist = "", _album = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Title of the Loaded Media.
|
||||||
|
/// </summary>
|
||||||
|
public string Title
|
||||||
|
{
|
||||||
|
get => _title;
|
||||||
|
private set
|
||||||
|
{
|
||||||
|
_title = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Artist of the Loaded Media.
|
||||||
|
/// </summary>
|
||||||
|
public string Artist
|
||||||
|
{
|
||||||
|
get => _artist;
|
||||||
|
private set
|
||||||
|
{
|
||||||
|
_artist = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Album of the Loaded Media.
|
||||||
|
/// </summary>
|
||||||
|
public string Album
|
||||||
|
{
|
||||||
|
get => _album;
|
||||||
|
private set
|
||||||
|
{
|
||||||
|
_album = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the Playback State of the Channel.
|
||||||
|
/// </summary>
|
||||||
|
public PlaybackState State => Handle == 0 ? PlaybackState.Stopped : Bass.ChannelIsActive(Handle);
|
||||||
|
|
||||||
|
#region Playback
|
||||||
|
/// <summary>
|
||||||
|
/// Starts the Channel Playback.
|
||||||
|
/// </summary>
|
||||||
|
public bool Play()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = Bass.ChannelPlay(Handle, _restartOnNextPlayback);
|
||||||
|
|
||||||
|
if (result)
|
||||||
|
_restartOnNextPlayback = false;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
finally { OnStateChanged(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pauses the Channel Playback.
|
||||||
|
/// </summary>
|
||||||
|
public bool Pause()
|
||||||
|
{
|
||||||
|
try { return Bass.ChannelPause(Handle); }
|
||||||
|
finally { OnStateChanged(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stops the Channel Playback.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Difference from <see cref="Bass.ChannelStop"/>: Playback is restarted when <see cref="Play"/> is called.</remarks>
|
||||||
|
public bool Stop()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_restartOnNextPlayback = true;
|
||||||
|
return Bass.ChannelStop(Handle);
|
||||||
|
}
|
||||||
|
finally { OnStateChanged(); }
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the Playback Duration.
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan Duration => TimeSpan.FromSeconds(Bass.ChannelBytes2Seconds(Handle, Bass.ChannelGetLength(Handle)));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or Sets the Playback Position.
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan Position
|
||||||
|
{
|
||||||
|
get => TimeSpan.FromSeconds(Bass.ChannelBytes2Seconds(Handle, Bass.ChannelGetPosition(Handle)));
|
||||||
|
set => Bass.ChannelSetPosition(Handle, Bass.ChannelSeconds2Bytes(Handle, value.TotalSeconds));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loads a file into the player.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="FileName">Path to the file to Load.</param>
|
||||||
|
/// <returns><see langword="true"/> on succes, <see langword="false"/> on failure.</returns>
|
||||||
|
public async Task<bool> LoadAsync(string FileName)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Handle != 0)
|
||||||
|
Bass.StreamFree(Handle);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
|
||||||
|
if (_dev != -1)
|
||||||
|
Bass.CurrentDevice = _dev;
|
||||||
|
|
||||||
|
var currentDev = Bass.CurrentDevice;
|
||||||
|
|
||||||
|
if (currentDev == -1 || !Bass.GetDeviceInfo(Bass.CurrentDevice).IsInitialized)
|
||||||
|
Bass.Init(currentDev);
|
||||||
|
|
||||||
|
var h = await Task.Run(() => OnLoad(FileName));
|
||||||
|
|
||||||
|
if (h == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
Handle = h;
|
||||||
|
|
||||||
|
// Tag reading logic can cause exceptions
|
||||||
|
//var tags = TagReader.Read(Handle);
|
||||||
|
|
||||||
|
//Title = !string.IsNullOrWhiteSpace(tags.Title) ? tags.Title
|
||||||
|
// : Path.GetFileNameWithoutExtension(FileName);
|
||||||
|
//Artist = tags.Artist;
|
||||||
|
//Album = tags.Album;
|
||||||
|
|
||||||
|
InitProperties();
|
||||||
|
|
||||||
|
MediaLoaded?.Invoke(h);
|
||||||
|
|
||||||
|
OnPropertyChanged("");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fired when a Media is Loaded.
|
||||||
|
/// </summary>
|
||||||
|
public event Action<int>? MediaLoaded;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Frees all resources used by the player.
|
||||||
|
/// </summary>
|
||||||
|
public virtual void Dispose()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Bass.StreamFree(Handle))
|
||||||
|
_handle = 0;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
OnStateChanged();
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes Properties on every call to <see cref="LoadAsync"/>.
|
||||||
|
/// </summary>
|
||||||
|
protected virtual void InitProperties()
|
||||||
|
{
|
||||||
|
Frequency = _freq;
|
||||||
|
Balance = _pan;
|
||||||
|
Volume = _vol;
|
||||||
|
Loop = _loop;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnStateChanged() => OnPropertyChanged(nameof(State));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fired when a property value changes.
|
||||||
|
/// </summary>
|
||||||
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fires the <see cref="PropertyChanged"/> event.
|
||||||
|
/// </summary>
|
||||||
|
protected virtual void OnPropertyChanged([CallerMemberName] string? PropertyName = null)
|
||||||
|
{
|
||||||
|
void f() => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(PropertyName));
|
||||||
|
|
||||||
|
if (_syncContext == null)
|
||||||
|
f();
|
||||||
|
else _syncContext.Post(S => f(), null);
|
||||||
|
}
|
||||||
|
}
|
||||||
239
Harmonia.Core/Engine/BassAudioEngine.cs
Normal file
239
Harmonia.Core/Engine/BassAudioEngine.cs
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
using ManagedBass;
|
||||||
|
using System.ComponentModel;
|
||||||
|
|
||||||
|
namespace Harmonia.Core.Engine;
|
||||||
|
|
||||||
|
public class BassAudioEngine : IAudioEngine, IDisposable
|
||||||
|
{
|
||||||
|
private readonly BaseMediaPlayer _mediaPlayer;
|
||||||
|
|
||||||
|
private CancellationTokenSource? _cancellationTokenSource;
|
||||||
|
|
||||||
|
public event EventHandler<PlaybackStoppedEventArgs>? PlaybackStopped;
|
||||||
|
public event EventHandler? StreamFinished;
|
||||||
|
public event EventHandler<PlaybackStateChangedEventArgs>? StateChanged;
|
||||||
|
|
||||||
|
public string? Source { get; private set; }
|
||||||
|
public string[] SupportedFormats { get; }
|
||||||
|
|
||||||
|
public TimeSpan Position
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _mediaPlayer.Position;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_mediaPlayer.Position = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public TimeSpan Length => _mediaPlayer.Duration;
|
||||||
|
|
||||||
|
private float _volume;
|
||||||
|
public float Volume
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _volume;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
var newVolume = value;
|
||||||
|
|
||||||
|
if (newVolume > 1.0)
|
||||||
|
newVolume = 1.0f;
|
||||||
|
else if (newVolume < 0.0)
|
||||||
|
newVolume = 0.0f;
|
||||||
|
|
||||||
|
_isMuted = false;
|
||||||
|
_volume = newVolume;
|
||||||
|
_mediaPlayer.Volume = newVolume;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CanPause => State == AudioPlaybackState.Playing;
|
||||||
|
|
||||||
|
private bool _isMuted;
|
||||||
|
public bool IsMuted
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _isMuted;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_isMuted = value;
|
||||||
|
_mediaPlayer.Volume = value ? 0 : Volume;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private AudioPlaybackState _state;
|
||||||
|
public AudioPlaybackState State
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _state;
|
||||||
|
}
|
||||||
|
private set
|
||||||
|
{
|
||||||
|
AudioPlaybackState oldValue = _state;
|
||||||
|
_state = value;
|
||||||
|
|
||||||
|
StateChanged?.Invoke(this, new(oldValue, value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public BassAudioEngine()
|
||||||
|
{
|
||||||
|
_mediaPlayer = new MediaPlayer();
|
||||||
|
_mediaPlayer.MediaLoaded += OnMediaLoaded;
|
||||||
|
_mediaPlayer.MediaFailed += OnPlaybackStopped;
|
||||||
|
_mediaPlayer.MediaEnded += OnStreamFinished;
|
||||||
|
_mediaPlayer.PropertyChanged += OnMediaPlayerPropertyChanged;
|
||||||
|
|
||||||
|
List<string> supportedFormats = [.. Bass.SupportedFormats.Split(';')];
|
||||||
|
supportedFormats.Add(".aac");
|
||||||
|
supportedFormats.Add(".m4a");
|
||||||
|
supportedFormats.Add(".flac");
|
||||||
|
supportedFormats.Add(".opus");
|
||||||
|
supportedFormats.Add(".wma");
|
||||||
|
|
||||||
|
SupportedFormats = [.. supportedFormats];
|
||||||
|
|
||||||
|
IsMuted = false;
|
||||||
|
Volume = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> LoadAsync(string fileName)
|
||||||
|
{
|
||||||
|
if (fileName == Source)
|
||||||
|
{
|
||||||
|
Position = TimeSpan.Zero;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (File.Exists(fileName) == false)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (await LoadWaveSourceAsync(fileName) == false)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
UpdateSource(fileName);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> LoadWaveSourceAsync(string fileName)
|
||||||
|
{
|
||||||
|
_cancellationTokenSource?.Cancel();
|
||||||
|
_cancellationTokenSource = new CancellationTokenSource();
|
||||||
|
|
||||||
|
CancellationToken token = _cancellationTokenSource.Token;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _mediaPlayer.LoadAsync(fileName);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
if (token.IsCancellationRequested)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
//return new Result(State.Exception, ex.Message);
|
||||||
|
throw new Exception("An error occurred - " + fileName, ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token.IsCancellationRequested)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateSource(string fileName)
|
||||||
|
{
|
||||||
|
if (State != AudioPlaybackState.Stopped)
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
Source = fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Play()
|
||||||
|
{
|
||||||
|
bool result = _mediaPlayer.Play();
|
||||||
|
|
||||||
|
if (!result)
|
||||||
|
{
|
||||||
|
if (Bass.LastError == Errors.Start)
|
||||||
|
{
|
||||||
|
Bass.Start();
|
||||||
|
_mediaPlayer.Play();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Pause()
|
||||||
|
{
|
||||||
|
_mediaPlayer.Pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Stop()
|
||||||
|
{
|
||||||
|
_mediaPlayer.Stop();
|
||||||
|
Position = TimeSpan.Zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnMediaLoaded(int obj)
|
||||||
|
{
|
||||||
|
if (_mediaPlayer.Volume != Volume)
|
||||||
|
_mediaPlayer.Volume = Volume;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPlaybackStopped(object? sender, EventArgs args)
|
||||||
|
{
|
||||||
|
PlaybackStoppedEventArgs eventArgs = new($"Playback stopped unexpectedly: Last Error = {Bass.LastError}");
|
||||||
|
PlaybackStopped?.Invoke(sender, eventArgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnStreamFinished(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
StreamFinished?.Invoke(sender, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnMediaPlayerPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
switch (e.PropertyName)
|
||||||
|
{
|
||||||
|
case "State":
|
||||||
|
State = GetPlaybackState(_mediaPlayer.State);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AudioPlaybackState GetPlaybackState(PlaybackState playbackState)
|
||||||
|
{
|
||||||
|
return playbackState switch
|
||||||
|
{
|
||||||
|
PlaybackState.Stopped => AudioPlaybackState.Stopped,
|
||||||
|
PlaybackState.Playing => AudioPlaybackState.Playing,
|
||||||
|
PlaybackState.Stalled => AudioPlaybackState.Stalled,
|
||||||
|
PlaybackState.Paused => AudioPlaybackState.Paused,
|
||||||
|
_ => throw new Exception($"Unknown audio playback state: {playbackState}")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_mediaPlayer.Dispose();
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
public interface IAudioEngine
|
public interface IAudioEngine
|
||||||
{
|
{
|
||||||
string[] SupportedFormats { get; }
|
string[] SupportedFormats { get; }
|
||||||
string Source { get; }
|
string? Source { get; }
|
||||||
TimeSpan Position { get; set; }
|
TimeSpan Position { get; set; }
|
||||||
TimeSpan Length { get; }
|
TimeSpan Length { get; }
|
||||||
float Volume { get; set; }
|
float Volume { get; set; }
|
||||||
|
|||||||
34
Harmonia.Core/Engine/MediaPlayer.cs
Normal file
34
Harmonia.Core/Engine/MediaPlayer.cs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
using ManagedBass.Flac;
|
||||||
|
using ManagedBass;
|
||||||
|
|
||||||
|
namespace Harmonia.Core.Engine;
|
||||||
|
|
||||||
|
public class MediaPlayer : BaseMediaPlayer
|
||||||
|
{
|
||||||
|
protected override int OnLoad(string FileName)
|
||||||
|
{
|
||||||
|
string extension = Path.GetExtension(FileName).ToLower();
|
||||||
|
|
||||||
|
switch (extension)
|
||||||
|
{
|
||||||
|
case ".flac":
|
||||||
|
return BassFlac.CreateStream(FileName);
|
||||||
|
//case ".opus":
|
||||||
|
// return BassOpus.CreateStream(FileName);
|
||||||
|
//case ".wma":
|
||||||
|
// return BassWma.CreateStream(FileName);
|
||||||
|
//case ".mid":
|
||||||
|
// return BassMidi.CreateStream(FileName);
|
||||||
|
default:
|
||||||
|
return base.OnLoad(FileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void InitProperties()
|
||||||
|
{
|
||||||
|
base.InitProperties();
|
||||||
|
|
||||||
|
ChannelInfo channelInfo = Bass.ChannelGetInfo(Handle);
|
||||||
|
Frequency = channelInfo.Frequency;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,9 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="ManagedBass" Version="3.1.1" />
|
||||||
|
<PackageReference Include="ManagedBass.Flac" Version="3.1.1" />
|
||||||
|
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
|
||||||
<PackageReference Include="TagLibSharp" Version="2.3.0" />
|
<PackageReference Include="TagLibSharp" Version="2.3.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
283
Harmonia.Core/Player/AudioPlayer.cs
Normal file
283
Harmonia.Core/Player/AudioPlayer.cs
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
using Harmonia.Core.Engine;
|
||||||
|
using Harmonia.Core.Playlists;
|
||||||
|
using System;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace Harmonia.Core.Player;
|
||||||
|
|
||||||
|
public class AudioPlayer : IAudioPlayer
|
||||||
|
{
|
||||||
|
private readonly IAudioEngine _audioEngine;
|
||||||
|
private readonly IPlaylistRepository _playlistRepository;
|
||||||
|
|
||||||
|
private Playlist? _playlist;
|
||||||
|
public Playlist? Playlist
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _playlist;
|
||||||
|
}
|
||||||
|
private set
|
||||||
|
{
|
||||||
|
_playlist = value;
|
||||||
|
NotifyPropertyChanged(nameof(Playlist));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private PlaylistSong? _playingSong;
|
||||||
|
public PlaylistSong? PlayingSong
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _playingSong;
|
||||||
|
}
|
||||||
|
private set
|
||||||
|
{
|
||||||
|
_playingSong = value;
|
||||||
|
NotifyPropertyChanged(nameof(PlayingSong));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public double Position
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _audioEngine.Position.TotalSeconds;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_audioEngine.Position = TimeSpan.FromSeconds(value);
|
||||||
|
NotifyPropertyChanged(nameof(Position));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private RepeatState _repeatState;
|
||||||
|
public RepeatState RepeatState
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _repeatState;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_repeatState = value;
|
||||||
|
NotifyPropertyChanged(nameof(RepeatState));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public double Volume
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _audioEngine.Volume;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
// Should the mute logic be here instead of the in view model?
|
||||||
|
_audioEngine.Volume = Convert.ToSingle(value);
|
||||||
|
NotifyPropertyChanged(nameof(Volume));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool _isRandom;
|
||||||
|
public bool IsRandom
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _isRandom;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_isRandom = value;
|
||||||
|
NotifyPropertyChanged(nameof(IsRandom));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsMuted
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _audioEngine.IsMuted;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_audioEngine.IsMuted = value;
|
||||||
|
NotifyPropertyChanged(nameof(IsMuted));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public AudioPlaybackState State => _audioEngine.State;
|
||||||
|
|
||||||
|
protected virtual int PreviousSongSecondsThreshold => 5;
|
||||||
|
|
||||||
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
|
||||||
|
public AudioPlayer(IAudioEngine audioEngine, IPlaylistRepository playlistRepository)
|
||||||
|
{
|
||||||
|
_audioEngine = audioEngine;
|
||||||
|
_audioEngine.StreamFinished += OnAudioEngineStreamFinished;
|
||||||
|
_audioEngine.StateChanged += OnMusicEngineStateChanged;
|
||||||
|
|
||||||
|
_playlistRepository = playlistRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnAudioEngineStreamFinished(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (RepeatState == RepeatState.RepeatOne)
|
||||||
|
{
|
||||||
|
// Alternative: Set the position to 0 and play again
|
||||||
|
if (PlayingSong != null)
|
||||||
|
{
|
||||||
|
await LoadAsync(PlayingSong, PlaybackMode.LoadAndPlay);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await NextAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnMusicEngineStateChanged(object? sender, PlaybackStateChangedEventArgs e)
|
||||||
|
{
|
||||||
|
NotifyPropertyChanged(nameof(State));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Play()
|
||||||
|
{
|
||||||
|
_audioEngine.Play();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Pause()
|
||||||
|
{
|
||||||
|
_audioEngine.Pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Stop()
|
||||||
|
{
|
||||||
|
_audioEngine.Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task NextAsync()
|
||||||
|
{
|
||||||
|
if (Playlist == null || PlayingSong == null || Playlist.Songs.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (IsRandom)
|
||||||
|
{
|
||||||
|
int randomIndex = new Random().Next(0, Playlist.Songs.Count - 1);
|
||||||
|
await LoadAsync(randomIndex);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int currentIndex = Playlist.Songs.IndexOf(PlayingSong);
|
||||||
|
int nextIndex = currentIndex + 1;
|
||||||
|
|
||||||
|
if (nextIndex > Playlist.Songs.Count - 1)
|
||||||
|
{
|
||||||
|
PlaybackMode playbackMode = RepeatState == RepeatState.RepeatAll
|
||||||
|
? PlaybackMode.LoadAndPlay
|
||||||
|
: PlaybackMode.LoadOnly;
|
||||||
|
|
||||||
|
await LoadAsync(0, playbackMode);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await LoadAsync(nextIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task PreviousAsync()
|
||||||
|
{
|
||||||
|
if (Playlist == null || PlayingSong == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (Position > PreviousSongSecondsThreshold)
|
||||||
|
{
|
||||||
|
Position = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int currentIndex = Playlist.Songs.IndexOf(PlayingSong);
|
||||||
|
int nextIndex = currentIndex - 1;
|
||||||
|
|
||||||
|
if (nextIndex < 0)
|
||||||
|
{
|
||||||
|
if (RepeatState == RepeatState.RepeatAll && Playlist.Songs.Count > 0)
|
||||||
|
{
|
||||||
|
await LoadAsync(Playlist.Songs.Count - 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Stop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await LoadAsync(nextIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> LoadAsync(int index, PlaybackMode mode = PlaybackMode.LoadAndPlay)
|
||||||
|
{
|
||||||
|
if (Playlist == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (index < 0 || index > Playlist.Songs.Count - 1)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return await LoadAsync(Playlist.Songs[index], mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> LoadAsync(PlaylistSong song, PlaybackMode mode = PlaybackMode.LoadAndPlay)
|
||||||
|
{
|
||||||
|
if (Playlist == null || Playlist.Songs.Contains(song) == false)
|
||||||
|
{
|
||||||
|
//Playlist? newPlaylist = _playlistRepository.GetPlaylist(song);
|
||||||
|
|
||||||
|
Playlist? newPlaylist = _playlistRepository.Get().FirstOrDefault(playlist =>
|
||||||
|
playlist.Songs.Contains(song));
|
||||||
|
|
||||||
|
if (newPlaylist == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
Playlist = newPlaylist;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isLoaded = await TryLoadAsync(song);
|
||||||
|
|
||||||
|
if (isLoaded == false)
|
||||||
|
{
|
||||||
|
if (mode == PlaybackMode.LoadAndPlay)
|
||||||
|
{
|
||||||
|
await NextAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
PlayingSong = song;
|
||||||
|
Position = 0;
|
||||||
|
|
||||||
|
if (mode == PlaybackMode.LoadAndPlay)
|
||||||
|
{
|
||||||
|
Play();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> TryLoadAsync(PlaylistSong song)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await _audioEngine.LoadAsync(song.Song.FileName);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void NotifyPropertyChanged(string propertyName)
|
||||||
|
{
|
||||||
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||||
|
}
|
||||||
|
}
|
||||||
28
Harmonia.Core/Player/IAudioPlayer.cs
Normal file
28
Harmonia.Core/Player/IAudioPlayer.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
using Harmonia.Core.Engine;
|
||||||
|
using Harmonia.Core.Playlists;
|
||||||
|
using System.ComponentModel;
|
||||||
|
|
||||||
|
namespace Harmonia.Core.Player;
|
||||||
|
|
||||||
|
public interface IAudioPlayer
|
||||||
|
{
|
||||||
|
Playlist? Playlist { get; }
|
||||||
|
PlaylistSong? PlayingSong { get; }
|
||||||
|
double Position { get; set; }
|
||||||
|
RepeatState RepeatState { get; set; }
|
||||||
|
double Volume { get; set; }
|
||||||
|
bool IsRandom { get; set; }
|
||||||
|
bool IsMuted { get; set; }
|
||||||
|
AudioPlaybackState State { get; }
|
||||||
|
|
||||||
|
Task<bool> LoadAsync(int index, PlaybackMode mode);
|
||||||
|
Task<bool> LoadAsync(PlaylistSong song, PlaybackMode mode);
|
||||||
|
|
||||||
|
void Play();
|
||||||
|
void Pause();
|
||||||
|
void Stop();
|
||||||
|
Task PreviousAsync();
|
||||||
|
Task NextAsync();
|
||||||
|
|
||||||
|
event PropertyChangedEventHandler PropertyChanged;
|
||||||
|
}
|
||||||
7
Harmonia.Core/Player/PlaybackMode.cs
Normal file
7
Harmonia.Core/Player/PlaybackMode.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Harmonia.Core.Player;
|
||||||
|
|
||||||
|
public enum PlaybackMode
|
||||||
|
{
|
||||||
|
LoadOnly,
|
||||||
|
LoadAndPlay
|
||||||
|
}
|
||||||
8
Harmonia.Core/Player/RepeatState.cs
Normal file
8
Harmonia.Core/Player/RepeatState.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Harmonia.Core.Player;
|
||||||
|
|
||||||
|
public enum RepeatState
|
||||||
|
{
|
||||||
|
Off = 0,
|
||||||
|
RepeatAll = 1,
|
||||||
|
RepeatOne = 2
|
||||||
|
}
|
||||||
8
Harmonia.Core/Playlists/IPlaylistRepository.cs
Normal file
8
Harmonia.Core/Playlists/IPlaylistRepository.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
using Harmonia.Core.Data;
|
||||||
|
|
||||||
|
namespace Harmonia.Core.Playlists;
|
||||||
|
|
||||||
|
public interface IPlaylistRepository : IRepository<Playlist>
|
||||||
|
{
|
||||||
|
Playlist? GetPlaylist(PlaylistSong playlistSong);
|
||||||
|
}
|
||||||
@@ -5,11 +5,9 @@ namespace Harmonia.Core.Playlists;
|
|||||||
|
|
||||||
public class Playlist
|
public class Playlist
|
||||||
{
|
{
|
||||||
private readonly List<PlaylistSong> _songs = [];
|
|
||||||
|
|
||||||
public string UID { get; init; } = Guid.NewGuid().ToString();
|
public string UID { get; init; } = Guid.NewGuid().ToString();
|
||||||
public string? Name { get; set; }
|
public string? Name { get; set; }
|
||||||
public IReadOnlyList<PlaylistSong> Songs => _songs;
|
public List<PlaylistSong> Songs { get; } = [];
|
||||||
public List<GroupOption> GroupOptions { get; set; } = [];
|
public List<GroupOption> GroupOptions { get; set; } = [];
|
||||||
public List<SortOption> SortOptions { get; set; } = [];
|
public List<SortOption> SortOptions { get; set; } = [];
|
||||||
public bool IsLocked { get; set; }
|
public bool IsLocked { get; set; }
|
||||||
@@ -35,7 +33,7 @@ public class Playlist
|
|||||||
|
|
||||||
int insertIndex = index ?? Songs.Count;
|
int insertIndex = index ?? Songs.Count;
|
||||||
|
|
||||||
_songs.InsertRange(insertIndex, playlistSongs);
|
Songs.InsertRange(insertIndex, playlistSongs);
|
||||||
|
|
||||||
PlaylistUpdatedEventArgs eventArgs = new()
|
PlaylistUpdatedEventArgs eventArgs = new()
|
||||||
{
|
{
|
||||||
@@ -50,7 +48,7 @@ public class Playlist
|
|||||||
|
|
||||||
public void MoveSong(PlaylistSong playlistSong, int newIndex)
|
public void MoveSong(PlaylistSong playlistSong, int newIndex)
|
||||||
{
|
{
|
||||||
int currentIndex = _songs.IndexOf(playlistSong);
|
int currentIndex = Songs.IndexOf(playlistSong);
|
||||||
|
|
||||||
MoveSong(currentIndex, newIndex);
|
MoveSong(currentIndex, newIndex);
|
||||||
}
|
}
|
||||||
@@ -65,8 +63,8 @@ public class Playlist
|
|||||||
|
|
||||||
PlaylistSong playlistSong = Songs[oldIndex];
|
PlaylistSong playlistSong = Songs[oldIndex];
|
||||||
|
|
||||||
_songs.Remove(playlistSong);
|
Songs.Remove(playlistSong);
|
||||||
_songs.Insert(newIndex, playlistSong);
|
Songs.Insert(newIndex, playlistSong);
|
||||||
|
|
||||||
PlaylistUpdatedEventArgs eventArgs = new()
|
PlaylistUpdatedEventArgs eventArgs = new()
|
||||||
{
|
{
|
||||||
@@ -83,8 +81,8 @@ public class Playlist
|
|||||||
public void SortSongs(PlaylistSong[] playlistSongs, SortOption[] sortOptions)
|
public void SortSongs(PlaylistSong[] playlistSongs, SortOption[] sortOptions)
|
||||||
{
|
{
|
||||||
Dictionary<int, PlaylistSong> oldPlaylistSongs = playlistSongs
|
Dictionary<int, PlaylistSong> oldPlaylistSongs = playlistSongs
|
||||||
.OrderBy(_songs.IndexOf)
|
.OrderBy(Songs.IndexOf)
|
||||||
.ToDictionary(_songs.IndexOf, playlistSong => playlistSong);
|
.ToDictionary(Songs.IndexOf, playlistSong => playlistSong);
|
||||||
|
|
||||||
Song[] songs = playlistSongs.Select(playlistSong => playlistSong.Song).ToArray();
|
Song[] songs = playlistSongs.Select(playlistSong => playlistSong.Song).ToArray();
|
||||||
Song[] sortedSongs = [.. songs.SortBy(sortOptions)];
|
Song[] sortedSongs = [.. songs.SortBy(sortOptions)];
|
||||||
@@ -100,8 +98,8 @@ public class Playlist
|
|||||||
if (newPlaylistSong == playlistSong)
|
if (newPlaylistSong == playlistSong)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
_songs.RemoveAt(index);
|
Songs.RemoveAt(index);
|
||||||
_songs.Insert(index, newPlaylistSong);
|
Songs.Insert(index, newPlaylistSong);
|
||||||
}
|
}
|
||||||
|
|
||||||
PlaylistUpdatedEventArgs eventArgs = new()
|
PlaylistUpdatedEventArgs eventArgs = new()
|
||||||
@@ -122,7 +120,7 @@ public class Playlist
|
|||||||
|
|
||||||
public void RemoveSongs(int index, int count)
|
public void RemoveSongs(int index, int count)
|
||||||
{
|
{
|
||||||
PlaylistSong[] playlistSongs = [.. _songs.GetRange(index, count)];
|
PlaylistSong[] playlistSongs = [.. Songs.GetRange(index, count)];
|
||||||
|
|
||||||
RemoveSongs(playlistSongs);
|
RemoveSongs(playlistSongs);
|
||||||
}
|
}
|
||||||
@@ -141,7 +139,7 @@ public class Playlist
|
|||||||
|
|
||||||
foreach (PlaylistSong playlistSong in playlistSongs)
|
foreach (PlaylistSong playlistSong in playlistSongs)
|
||||||
{
|
{
|
||||||
if (_songs.Remove(playlistSong))
|
if (Songs.Remove(playlistSong))
|
||||||
{
|
{
|
||||||
removedSongs.Add(playlistSong);
|
removedSongs.Add(playlistSong);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,15 @@
|
|||||||
|
|
||||||
namespace Harmonia.Core.Playlists;
|
namespace Harmonia.Core.Playlists;
|
||||||
|
|
||||||
public class PlaylistRepository : JsonFileRepository<Playlist>
|
public class PlaylistRepository : JsonFileRepository<Playlist>, IPlaylistRepository
|
||||||
{
|
{
|
||||||
protected override string DirectoryName => string.Empty;
|
protected override string DirectoryName => string.Empty;
|
||||||
|
|
||||||
|
public Playlist? GetPlaylist(PlaylistSong playlistSong)
|
||||||
|
{
|
||||||
|
return Get().FirstOrDefault(playlist => playlist.Songs.Contains(playlistSong));
|
||||||
|
}
|
||||||
|
|
||||||
protected override string GetNewFileName()
|
protected override string GetNewFileName()
|
||||||
{
|
{
|
||||||
for (int i = 0; i < 1000; i++)
|
for (int i = 0; i < 1000; i++)
|
||||||
|
|||||||
106
Harmonia.Tests/AudioPlayerTests.cs
Normal file
106
Harmonia.Tests/AudioPlayerTests.cs
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
using Harmonia.Core.Engine;
|
||||||
|
using Harmonia.Core.Models;
|
||||||
|
using Harmonia.Core.Player;
|
||||||
|
using Harmonia.Core.Playlists;
|
||||||
|
using NSubstitute;
|
||||||
|
using NSubstitute.ExceptionExtensions;
|
||||||
|
using Shouldly;
|
||||||
|
|
||||||
|
namespace Harmonia.Tests;
|
||||||
|
|
||||||
|
public class AudioPlayerTests
|
||||||
|
{
|
||||||
|
private readonly IAudioEngine _audioEngine;
|
||||||
|
private readonly IPlaylistRepository _playlistRepository;
|
||||||
|
private readonly PlaylistSong[] _playlistSongs;
|
||||||
|
private readonly IAudioPlayer _audioPlayer;
|
||||||
|
|
||||||
|
public AudioPlayerTests()
|
||||||
|
{
|
||||||
|
_audioEngine = Substitute.For<IAudioEngine>();
|
||||||
|
_audioEngine.LoadAsync("Song1.mp3").Returns(true);
|
||||||
|
_audioEngine.LoadAsync("Song2.mp3").Returns(false);
|
||||||
|
_audioEngine.LoadAsync("Song3.mp3").Returns(true);
|
||||||
|
_audioEngine.LoadAsync("Song4.mp3").Throws(new FileNotFoundException());
|
||||||
|
_audioEngine.LoadAsync("Song5.mp3").Returns(false);
|
||||||
|
_audioEngine.LoadAsync("Song6.mp3").Returns(true);
|
||||||
|
_audioEngine.LoadAsync("Song7.mp3").Returns(true);
|
||||||
|
|
||||||
|
Song[] songs =
|
||||||
|
[
|
||||||
|
new Song() { FileName = "Song1.mp3" },
|
||||||
|
new Song() { FileName = "Song2.mp3" },
|
||||||
|
new Song() { FileName = "Song3.mp3" },
|
||||||
|
new Song() { FileName = "Song4.mp3" },
|
||||||
|
new Song() { FileName = "Song5.mp3" },
|
||||||
|
new Song() { FileName = "Song6.mp3" },
|
||||||
|
new Song() { FileName = "Song7.mp3" }
|
||||||
|
];
|
||||||
|
|
||||||
|
Playlist playlist = new()
|
||||||
|
{
|
||||||
|
Name = "Playlist1"
|
||||||
|
};
|
||||||
|
|
||||||
|
playlist.AddSongs(songs);
|
||||||
|
|
||||||
|
_playlistSongs = [.. playlist.Songs];
|
||||||
|
|
||||||
|
_playlistRepository = Substitute.For<IPlaylistRepository>();
|
||||||
|
_playlistRepository.Get().Returns([playlist]);
|
||||||
|
//_playlistRepository.GetPlaylist(_playlistSongs[0]).Returns(playlist);
|
||||||
|
|
||||||
|
_audioPlayer = new AudioPlayer(_audioEngine, _playlistRepository);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Load_Song_Success()
|
||||||
|
{
|
||||||
|
(await _audioPlayer.LoadAsync(_playlistSongs[0], PlaybackMode.LoadAndPlay)).ShouldBe(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Load_Song_Failure()
|
||||||
|
{
|
||||||
|
(await _audioPlayer.LoadAsync(_playlistSongs[1], PlaybackMode.LoadAndPlay)).ShouldBe(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Load_Song_Failure_With_Exception()
|
||||||
|
{
|
||||||
|
(await _audioPlayer.LoadAsync(_playlistSongs[3], PlaybackMode.LoadAndPlay)).ShouldBe(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Load_Next_Song_Success()
|
||||||
|
{
|
||||||
|
await _audioPlayer.LoadAsync(_playlistSongs[5], PlaybackMode.LoadAndPlay);
|
||||||
|
await _audioPlayer.NextAsync();
|
||||||
|
|
||||||
|
_audioPlayer.PlayingSong.ShouldBe(_playlistSongs[6]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Load_Next_Song_Failure()
|
||||||
|
{
|
||||||
|
await _audioPlayer.LoadAsync(_playlistSongs[0], PlaybackMode.LoadAndPlay);
|
||||||
|
await _audioPlayer.NextAsync();
|
||||||
|
|
||||||
|
_audioPlayer.PlayingSong.ShouldBe(_playlistSongs[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Load_Next_Song_From_Last_Song()
|
||||||
|
{
|
||||||
|
await _audioPlayer.LoadAsync(_playlistSongs[6], PlaybackMode.LoadAndPlay);
|
||||||
|
await _audioPlayer.NextAsync();
|
||||||
|
|
||||||
|
_audioPlayer.PlayingSong.ShouldBe(_playlistSongs[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Random_Test()
|
||||||
|
{
|
||||||
|
_audioPlayer.IsRandom.ShouldBeFalse();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,33 +1,37 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
<UseMicrosoftTestingPlatformRunner>true</UseMicrosoftTestingPlatformRunner>
|
||||||
<PackageReference Include="coverlet.collector" Version="6.0.4">
|
<OutputType>Exe</OutputType>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<TestingPlatformDotnetTestSupport>true</TestingPlatformDotnetTestSupport>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
</PropertyGroup>
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
|
||||||
<PackageReference Include="NSubstitute" Version="5.3.0" />
|
|
||||||
<PackageReference Include="Shouldly" Version="4.3.0" />
|
|
||||||
<PackageReference Include="xunit" Version="2.9.3" />
|
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.2">
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
</PackageReference>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Harmonia.Core\Harmonia.Core.csproj" />
|
<PackageReference Include="coverlet.collector" Version="6.0.4">
|
||||||
</ItemGroup>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
||||||
|
<PackageReference Include="NSubstitute" Version="5.3.0" />
|
||||||
|
<PackageReference Include="Shouldly" Version="4.3.0" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.2">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="xunit.v3" Version="1.1.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Using Include="Xunit" />
|
<ProjectReference Include="..\Harmonia.Core\Harmonia.Core.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="Xunit" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
Reference in New Issue
Block a user