diff --git a/Harmonia.Core/Engine/BaseMediaPlayer.cs b/Harmonia.Core/Engine/BaseMediaPlayer.cs
new file mode 100644
index 0000000..69a5562
--- /dev/null
+++ b/Harmonia.Core/Engine/BaseMediaPlayer.cs
@@ -0,0 +1,410 @@
+using ManagedBass;
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+
+namespace Harmonia.Core.Engine;
+
+///
+/// A Reusable Channel which can Load files like a Player.
+/// is perfect for UIs, as it implements .
+/// Also, unlike normal, Properties/Effects set on a persist through subsequent loads.
+///
+public class BaseMediaPlayer : INotifyPropertyChanged, IDisposable
+{
+ #region Fields
+ readonly SynchronizationContext? _syncContext;
+ int _handle;
+
+ ///
+ /// Channel Handle of the loaded audio file.
+ ///
+ 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);
+ }
+
+ ///
+ /// Creates a new instance of .
+ ///
+ public BaseMediaPlayer() { _syncContext = SynchronizationContext.Current; }
+
+ #region Events
+ ///
+ /// Fired when this Channel is Disposed.
+ ///
+ public event EventHandler? Disposed;
+
+ ///
+ /// Fired when the Media Playback Ends
+ ///
+ public event EventHandler? MediaEnded;
+
+ ///
+ /// Fired when the Playback fails
+ ///
+ public event EventHandler? MediaFailed;
+ #endregion
+
+ #region Frequency
+ double _freq = 44100;
+
+ ///
+ /// Gets or Sets the Playback Frequency in Hertz.
+ /// Default is 44100 Hz.
+ ///
+ public double Frequency
+ {
+ get => _freq;
+ set
+ {
+ if (!Bass.ChannelSetAttribute(Handle, ChannelAttribute.Frequency, value))
+ return;
+
+ _freq = value;
+ OnPropertyChanged();
+ }
+ }
+ #endregion
+
+ #region Balance
+ double _pan;
+
+ ///
+ /// Gets or Sets Balance (Panning) (-1 ... 0 ... 1).
+ /// -1 Represents Completely Left.
+ /// 1 Represents Completely Right.
+ /// Default is 0.
+ ///
+ public double Balance
+ {
+ get => _pan;
+ set
+ {
+ if (!Bass.ChannelSetAttribute(Handle, ChannelAttribute.Pan, value))
+ return;
+
+ _pan = value;
+ OnPropertyChanged();
+ }
+ }
+ #endregion
+
+ #region Device
+ int _dev = -1;
+
+ ///
+ /// Gets or Sets the Playback Device used.
+ ///
+ 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;
+
+ ///
+ /// Gets or Sets the Playback Volume.
+ ///
+ public double Volume
+ {
+ get => _vol;
+ set
+ {
+ if (!Bass.ChannelSetAttribute(Handle, ChannelAttribute.Volume, value))
+ return;
+
+ _vol = value;
+ OnPropertyChanged();
+ }
+ }
+ #endregion
+
+ #region Loop
+ bool _loop;
+
+ ///
+ /// Gets or Sets whether the Playback is looped.
+ ///
+ public bool Loop
+ {
+ get => _loop;
+ set
+ {
+ if (value ? !Bass.ChannelAddFlag(Handle, BassFlags.Loop) : !Bass.ChannelRemoveFlag(Handle, BassFlags.Loop))
+ return;
+
+ _loop = value;
+ OnPropertyChanged();
+ }
+ }
+ #endregion
+
+ ///
+ /// Override this method for custom loading procedure.
+ ///
+ /// Path to the File to Load.
+ /// on Success, on failure
+ protected virtual int OnLoad(string FileName) => Bass.CreateStream(FileName);
+
+ #region Tags
+ string _title = "", _artist = "", _album = "";
+
+ ///
+ /// Title of the Loaded Media.
+ ///
+ public string Title
+ {
+ get => _title;
+ private set
+ {
+ _title = value;
+ OnPropertyChanged();
+ }
+ }
+
+ ///
+ /// Artist of the Loaded Media.
+ ///
+ public string Artist
+ {
+ get => _artist;
+ private set
+ {
+ _artist = value;
+ OnPropertyChanged();
+ }
+ }
+
+ ///
+ /// Album of the Loaded Media.
+ ///
+ public string Album
+ {
+ get => _album;
+ private set
+ {
+ _album = value;
+ OnPropertyChanged();
+ }
+ }
+ #endregion
+
+ ///
+ /// Gets the Playback State of the Channel.
+ ///
+ public PlaybackState State => Handle == 0 ? PlaybackState.Stopped : Bass.ChannelIsActive(Handle);
+
+ #region Playback
+ ///
+ /// Starts the Channel Playback.
+ ///
+ public bool Play()
+ {
+ try
+ {
+ var result = Bass.ChannelPlay(Handle, _restartOnNextPlayback);
+
+ if (result)
+ _restartOnNextPlayback = false;
+
+ return result;
+ }
+ finally { OnStateChanged(); }
+ }
+
+ ///
+ /// Pauses the Channel Playback.
+ ///
+ public bool Pause()
+ {
+ try { return Bass.ChannelPause(Handle); }
+ finally { OnStateChanged(); }
+ }
+
+ ///
+ /// Stops the Channel Playback.
+ ///
+ /// Difference from : Playback is restarted when is called.
+ public bool Stop()
+ {
+ try
+ {
+ _restartOnNextPlayback = true;
+ return Bass.ChannelStop(Handle);
+ }
+ finally { OnStateChanged(); }
+ }
+ #endregion
+
+ ///
+ /// Gets the Playback Duration.
+ ///
+ public TimeSpan Duration => TimeSpan.FromSeconds(Bass.ChannelBytes2Seconds(Handle, Bass.ChannelGetLength(Handle)));
+
+ ///
+ /// Gets or Sets the Playback Position.
+ ///
+ public TimeSpan Position
+ {
+ get => TimeSpan.FromSeconds(Bass.ChannelBytes2Seconds(Handle, Bass.ChannelGetPosition(Handle)));
+ set => Bass.ChannelSetPosition(Handle, Bass.ChannelSeconds2Bytes(Handle, value.TotalSeconds));
+ }
+
+ ///
+ /// Loads a file into the player.
+ ///
+ /// Path to the file to Load.
+ /// on succes, on failure.
+ public async Task 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;
+ }
+
+ ///
+ /// Fired when a Media is Loaded.
+ ///
+ public event Action? MediaLoaded;
+
+ ///
+ /// Frees all resources used by the player.
+ ///
+ public virtual void Dispose()
+ {
+ try
+ {
+ if (Bass.StreamFree(Handle))
+ _handle = 0;
+ }
+ finally
+ {
+ OnStateChanged();
+ GC.SuppressFinalize(this);
+ }
+ }
+
+ ///
+ /// Initializes Properties on every call to .
+ ///
+ protected virtual void InitProperties()
+ {
+ Frequency = _freq;
+ Balance = _pan;
+ Volume = _vol;
+ Loop = _loop;
+ }
+
+ void OnStateChanged() => OnPropertyChanged(nameof(State));
+
+ ///
+ /// Fired when a property value changes.
+ ///
+ public event PropertyChangedEventHandler? PropertyChanged;
+
+ ///
+ /// Fires the event.
+ ///
+ 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);
+ }
+}
\ No newline at end of file
diff --git a/Harmonia.Core/Engine/BassAudioEngine.cs b/Harmonia.Core/Engine/BassAudioEngine.cs
new file mode 100644
index 0000000..237d38b
--- /dev/null
+++ b/Harmonia.Core/Engine/BassAudioEngine.cs
@@ -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? PlaybackStopped;
+ public event EventHandler? StreamFinished;
+ public event EventHandler? 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 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 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 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);
+ }
+}
\ No newline at end of file
diff --git a/Harmonia.Core/Engine/IAudioEngine.cs b/Harmonia.Core/Engine/IAudioEngine.cs
index 686a219..deb5bf4 100644
--- a/Harmonia.Core/Engine/IAudioEngine.cs
+++ b/Harmonia.Core/Engine/IAudioEngine.cs
@@ -3,7 +3,7 @@
public interface IAudioEngine
{
string[] SupportedFormats { get; }
- string Source { get; }
+ string? Source { get; }
TimeSpan Position { get; set; }
TimeSpan Length { get; }
float Volume { get; set; }
diff --git a/Harmonia.Core/Engine/MediaPlayer.cs b/Harmonia.Core/Engine/MediaPlayer.cs
new file mode 100644
index 0000000..0e2a9e6
--- /dev/null
+++ b/Harmonia.Core/Engine/MediaPlayer.cs
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/Harmonia.Core/Harmonia.Core.csproj b/Harmonia.Core/Harmonia.Core.csproj
index 3afc37a..63aedb5 100644
--- a/Harmonia.Core/Harmonia.Core.csproj
+++ b/Harmonia.Core/Harmonia.Core.csproj
@@ -7,6 +7,9 @@
+
+
+
diff --git a/Harmonia.Core/Player/AudioPlayer.cs b/Harmonia.Core/Player/AudioPlayer.cs
new file mode 100644
index 0000000..afb6918
--- /dev/null
+++ b/Harmonia.Core/Player/AudioPlayer.cs
@@ -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 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 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 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));
+ }
+}
\ No newline at end of file
diff --git a/Harmonia.Core/Player/IAudioPlayer.cs b/Harmonia.Core/Player/IAudioPlayer.cs
new file mode 100644
index 0000000..a5d2388
--- /dev/null
+++ b/Harmonia.Core/Player/IAudioPlayer.cs
@@ -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 LoadAsync(int index, PlaybackMode mode);
+ Task LoadAsync(PlaylistSong song, PlaybackMode mode);
+
+ void Play();
+ void Pause();
+ void Stop();
+ Task PreviousAsync();
+ Task NextAsync();
+
+ event PropertyChangedEventHandler PropertyChanged;
+}
\ No newline at end of file
diff --git a/Harmonia.Core/Player/PlaybackMode.cs b/Harmonia.Core/Player/PlaybackMode.cs
new file mode 100644
index 0000000..76fa086
--- /dev/null
+++ b/Harmonia.Core/Player/PlaybackMode.cs
@@ -0,0 +1,7 @@
+namespace Harmonia.Core.Player;
+
+public enum PlaybackMode
+{
+ LoadOnly,
+ LoadAndPlay
+}
\ No newline at end of file
diff --git a/Harmonia.Core/Player/RepeatState.cs b/Harmonia.Core/Player/RepeatState.cs
new file mode 100644
index 0000000..66880b2
--- /dev/null
+++ b/Harmonia.Core/Player/RepeatState.cs
@@ -0,0 +1,8 @@
+namespace Harmonia.Core.Player;
+
+public enum RepeatState
+{
+ Off = 0,
+ RepeatAll = 1,
+ RepeatOne = 2
+}
\ No newline at end of file
diff --git a/Harmonia.Core/Playlists/IPlaylistRepository.cs b/Harmonia.Core/Playlists/IPlaylistRepository.cs
new file mode 100644
index 0000000..fd22cf6
--- /dev/null
+++ b/Harmonia.Core/Playlists/IPlaylistRepository.cs
@@ -0,0 +1,8 @@
+using Harmonia.Core.Data;
+
+namespace Harmonia.Core.Playlists;
+
+public interface IPlaylistRepository : IRepository
+{
+ Playlist? GetPlaylist(PlaylistSong playlistSong);
+}
\ No newline at end of file
diff --git a/Harmonia.Core/Playlists/Playlist.cs b/Harmonia.Core/Playlists/Playlist.cs
index b6caf55..8088bde 100644
--- a/Harmonia.Core/Playlists/Playlist.cs
+++ b/Harmonia.Core/Playlists/Playlist.cs
@@ -5,11 +5,9 @@ namespace Harmonia.Core.Playlists;
public class Playlist
{
- private readonly List _songs = [];
-
public string UID { get; init; } = Guid.NewGuid().ToString();
public string? Name { get; set; }
- public IReadOnlyList Songs => _songs;
+ public List Songs { get; } = [];
public List GroupOptions { get; set; } = [];
public List SortOptions { get; set; } = [];
public bool IsLocked { get; set; }
@@ -35,7 +33,7 @@ public class Playlist
int insertIndex = index ?? Songs.Count;
- _songs.InsertRange(insertIndex, playlistSongs);
+ Songs.InsertRange(insertIndex, playlistSongs);
PlaylistUpdatedEventArgs eventArgs = new()
{
@@ -50,7 +48,7 @@ public class Playlist
public void MoveSong(PlaylistSong playlistSong, int newIndex)
{
- int currentIndex = _songs.IndexOf(playlistSong);
+ int currentIndex = Songs.IndexOf(playlistSong);
MoveSong(currentIndex, newIndex);
}
@@ -65,8 +63,8 @@ public class Playlist
PlaylistSong playlistSong = Songs[oldIndex];
- _songs.Remove(playlistSong);
- _songs.Insert(newIndex, playlistSong);
+ Songs.Remove(playlistSong);
+ Songs.Insert(newIndex, playlistSong);
PlaylistUpdatedEventArgs eventArgs = new()
{
@@ -83,8 +81,8 @@ public class Playlist
public void SortSongs(PlaylistSong[] playlistSongs, SortOption[] sortOptions)
{
Dictionary oldPlaylistSongs = playlistSongs
- .OrderBy(_songs.IndexOf)
- .ToDictionary(_songs.IndexOf, playlistSong => playlistSong);
+ .OrderBy(Songs.IndexOf)
+ .ToDictionary(Songs.IndexOf, playlistSong => playlistSong);
Song[] songs = playlistSongs.Select(playlistSong => playlistSong.Song).ToArray();
Song[] sortedSongs = [.. songs.SortBy(sortOptions)];
@@ -100,8 +98,8 @@ public class Playlist
if (newPlaylistSong == playlistSong)
continue;
- _songs.RemoveAt(index);
- _songs.Insert(index, newPlaylistSong);
+ Songs.RemoveAt(index);
+ Songs.Insert(index, newPlaylistSong);
}
PlaylistUpdatedEventArgs eventArgs = new()
@@ -122,7 +120,7 @@ public class Playlist
public void RemoveSongs(int index, int count)
{
- PlaylistSong[] playlistSongs = [.. _songs.GetRange(index, count)];
+ PlaylistSong[] playlistSongs = [.. Songs.GetRange(index, count)];
RemoveSongs(playlistSongs);
}
@@ -141,7 +139,7 @@ public class Playlist
foreach (PlaylistSong playlistSong in playlistSongs)
{
- if (_songs.Remove(playlistSong))
+ if (Songs.Remove(playlistSong))
{
removedSongs.Add(playlistSong);
}
diff --git a/Harmonia.Core/Playlists/PlaylistRepository.cs b/Harmonia.Core/Playlists/PlaylistRepository.cs
index 359d891..3e3ea05 100644
--- a/Harmonia.Core/Playlists/PlaylistRepository.cs
+++ b/Harmonia.Core/Playlists/PlaylistRepository.cs
@@ -2,10 +2,15 @@
namespace Harmonia.Core.Playlists;
-public class PlaylistRepository : JsonFileRepository
+public class PlaylistRepository : JsonFileRepository, IPlaylistRepository
{
protected override string DirectoryName => string.Empty;
+ public Playlist? GetPlaylist(PlaylistSong playlistSong)
+ {
+ return Get().FirstOrDefault(playlist => playlist.Songs.Contains(playlistSong));
+ }
+
protected override string GetNewFileName()
{
for (int i = 0; i < 1000; i++)
diff --git a/Harmonia.Tests/AudioPlayerTests.cs b/Harmonia.Tests/AudioPlayerTests.cs
new file mode 100644
index 0000000..f108b60
--- /dev/null
+++ b/Harmonia.Tests/AudioPlayerTests.cs
@@ -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();
+ _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();
+ _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();
+ }
+}
\ No newline at end of file
diff --git a/Harmonia.Tests/Harmonia.Tests.csproj b/Harmonia.Tests/Harmonia.Tests.csproj
index f7a0fbe..4085742 100644
--- a/Harmonia.Tests/Harmonia.Tests.csproj
+++ b/Harmonia.Tests/Harmonia.Tests.csproj
@@ -1,33 +1,37 @@
-
- net9.0
- enable
- enable
- false
-
+
+ net9.0
+ enable
+ enable
+ false
-
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
-
-
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
+ true
+ Exe
+ true
+
-
-
-
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
-
-
-
+
+
+
+
+
+
+