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 + + + - - - + + + + + + +