From f3ffe06992378fbdbc0b237e4101dfd9e8d10e0c Mon Sep 17 00:00:00 2001 From: Mark-James McDougall Date: Sat, 1 Mar 2025 22:05:39 -0500 Subject: [PATCH 01/22] Remove NAudio, add SoundFlow library. --- src/MusicSharp.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MusicSharp.csproj b/src/MusicSharp.csproj index d92021d..0ee3c59 100644 --- a/src/MusicSharp.csproj +++ b/src/MusicSharp.csproj @@ -19,7 +19,7 @@ - + From ca78dff4fb8b0e4835b7063e6828d78135dec1d9 Mon Sep 17 00:00:00 2001 From: Mark-James McDougall Date: Sat, 1 Mar 2025 22:06:52 -0500 Subject: [PATCH 02/22] Refactor IPlayer to ISoundEngine.cs as it makes more sense conceptually. Create SoundFlow.cs class for cross-platform audio support. --- .../{IPlayer.cs => ISoundEngine.cs} | 4 +- src/SoundEngines/SoundFlow.cs | 69 +++++++++++++++++++ src/view/Tui.cs | 4 +- 3 files changed, 73 insertions(+), 4 deletions(-) rename src/SoundEngines/{IPlayer.cs => ISoundEngine.cs} (95%) create mode 100644 src/SoundEngines/SoundFlow.cs diff --git a/src/SoundEngines/IPlayer.cs b/src/SoundEngines/ISoundEngine.cs similarity index 95% rename from src/SoundEngines/IPlayer.cs rename to src/SoundEngines/ISoundEngine.cs index 7827f12..4a9ca2f 100644 --- a/src/SoundEngines/IPlayer.cs +++ b/src/SoundEngines/ISoundEngine.cs @@ -1,4 +1,4 @@ -// +// // Licensed under the GNU GPL v3 License. See LICENSE in the project root for license information. // @@ -9,7 +9,7 @@ namespace MusicSharp.SoundEngines; /// /// Defines the methods an audio player class should implement. /// -public interface IPlayer +public interface ISoundEngine { /// /// Gets or sets a value indicating whether the audio player is playing. diff --git a/src/SoundEngines/SoundFlow.cs b/src/SoundEngines/SoundFlow.cs new file mode 100644 index 0000000..1cdc3a0 --- /dev/null +++ b/src/SoundEngines/SoundFlow.cs @@ -0,0 +1,69 @@ +using System; +using MusicSharp.Enums; +using SoundFlow.Abstracts; +using SoundFlow.Backends.MiniAudio; +using SoundFlow.Components; +using SoundFlow.Enums; +using SoundFlow.Providers; + +namespace MusicSharp.SoundEngines; + +public class SoundFlow: ISoundEngine +{ + public ePlayerStatus PlayerStatus { get; set; } + public string LastFileOpened { get; set; } + public void OpenFile(string path) + { + throw new NotImplementedException(); + } + + public void OpenStream(string streamUrl) + { + throw new NotImplementedException(); + } + + public void PlayPause() + { + throw new NotImplementedException(); + } + + public void Stop() + { + throw new NotImplementedException(); + } + + public void IncreaseVolume() + { + throw new NotImplementedException(); + } + + public void DecreaseVolume() + { + throw new NotImplementedException(); + } + + public void PlayFromPlaylist(string path) + { + throw new NotImplementedException(); + } + + public TimeSpan CurrentTime() + { + throw new NotImplementedException(); + } + + public TimeSpan TrackLength() + { + throw new NotImplementedException(); + } + + public void SeekForward() + { + throw new NotImplementedException(); + } + + public void SeekBackwards() + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/src/view/Tui.cs b/src/view/Tui.cs index ebf4dc2..add041a 100644 --- a/src/view/Tui.cs +++ b/src/view/Tui.cs @@ -28,7 +28,7 @@ public class Tui /// /// Create a new instance of the audio player engine. /// - private readonly IPlayer _player; + private readonly ISoundEngine _player; private object _mainLoopTimeout = null; @@ -38,7 +38,7 @@ public class Tui /// Initializes a new instance of the class. /// /// The player to be injected. - public Tui(IPlayer player) + public Tui(ISoundEngine player) { _player = player; } From a5642b20d0df7bee818cce0f0b54ef5be48f9930 Mon Sep 17 00:00:00 2001 From: Mark-James McDougall Date: Sat, 1 Mar 2025 22:15:55 -0500 Subject: [PATCH 03/22] Rename SoundFlow classto SoundEngine. --- src/SoundEngines/{SoundFlow.cs => SoundEngine.cs} | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) rename src/SoundEngines/{SoundFlow.cs => SoundEngine.cs} (90%) diff --git a/src/SoundEngines/SoundFlow.cs b/src/SoundEngines/SoundEngine.cs similarity index 90% rename from src/SoundEngines/SoundFlow.cs rename to src/SoundEngines/SoundEngine.cs index 1cdc3a0..2622c5f 100644 --- a/src/SoundEngines/SoundFlow.cs +++ b/src/SoundEngines/SoundEngine.cs @@ -8,10 +8,15 @@ namespace MusicSharp.SoundEngines; -public class SoundFlow: ISoundEngine + +// Cross-platform sound engine that works for all devices which +// the .NET platform runs on. +public class SoundEngine: ISoundEngine { public ePlayerStatus PlayerStatus { get; set; } public string LastFileOpened { get; set; } + + public void OpenFile(string path) { throw new NotImplementedException(); From 0e71076362f43abd4111c3c13dab5a80e86090bb Mon Sep 17 00:00:00 2001 From: Mark-James McDougall Date: Sun, 2 Mar 2025 15:42:24 -0500 Subject: [PATCH 04/22] Working on implementing new cross-platform sound engine using SoundFlow library. --- MusicSharpTests/WinPlayerTests.cs | 4 +- src/Program.cs | 7 +-- src/SoundEngines/SoundEngine.cs | 73 +++++++++++++++++++++++++++---- src/SoundEngines/WinPlayer.cs | 8 ++-- src/{view => Tui}/Tui.cs | 14 +++--- 5 files changed, 83 insertions(+), 23 deletions(-) rename src/{view => Tui}/Tui.cs (97%) diff --git a/MusicSharpTests/WinPlayerTests.cs b/MusicSharpTests/WinPlayerTests.cs index c1b4310..7046041 100644 --- a/MusicSharpTests/WinPlayerTests.cs +++ b/MusicSharpTests/WinPlayerTests.cs @@ -3,7 +3,7 @@ namespace MusicSharpTests; public class WinPlayerTests -{ +{ /* [Test] public void Play_NullFile() { @@ -22,5 +22,5 @@ public void PlayFromPlaylist_NullFile() // act and assert Assert.Throws(() => player.PlayFromPlaylist(null)); - } + } */ } \ No newline at end of file diff --git a/src/Program.cs b/src/Program.cs index bf00623..7dbba4e 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -3,7 +3,8 @@ // using MusicSharp.SoundEngines; -using MusicSharp.View; + +namespace MusicSharp; /// /// Entry Point class. @@ -15,8 +16,8 @@ public static class Program /// public static void Main() { - var player = new WinPlayer(); - var gui = new Tui(player); + using var player = new SoundEngine(); + var gui = new Tui.Tui(player); gui.Start(); } diff --git a/src/SoundEngines/SoundEngine.cs b/src/SoundEngines/SoundEngine.cs index 2622c5f..f3171f9 100644 --- a/src/SoundEngines/SoundEngine.cs +++ b/src/SoundEngines/SoundEngine.cs @@ -1,6 +1,6 @@ using System; +using System.IO; using MusicSharp.Enums; -using SoundFlow.Abstracts; using SoundFlow.Backends.MiniAudio; using SoundFlow.Components; using SoundFlow.Enums; @@ -11,15 +11,32 @@ namespace MusicSharp.SoundEngines; // Cross-platform sound engine that works for all devices which // the .NET platform runs on. -public class SoundEngine: ISoundEngine +public class SoundEngine: ISoundEngine, IDisposable { + private readonly MiniAudioEngine _soundEngine; + private SoundPlayer _player; + public ePlayerStatus PlayerStatus { get; set; } public string LastFileOpened { get; set; } - + + + public SoundEngine() + { + _soundEngine = new MiniAudioEngine(44100, Capability.Playback); + } public void OpenFile(string path) { - throw new NotImplementedException(); + if (File.Exists(path)) + { + _player = new SoundPlayer(new StreamDataProvider(File.OpenRead(path))); + + // Add the player to the master mixer. This connects the player's output to the audio engine's output. + Mixer.Master.AddComponent(_player); + + _player.Play(); + PlayerStatus = ePlayerStatus.Playing; + } } public void OpenStream(string streamUrl) @@ -29,22 +46,57 @@ public void OpenStream(string streamUrl) public void PlayPause() { - throw new NotImplementedException(); + switch (PlayerStatus) + { + case ePlayerStatus.Playing: + _player.Pause(); + PlayerStatus = ePlayerStatus.Paused; + break; + case ePlayerStatus.Paused: + _player.Play(); + PlayerStatus = ePlayerStatus.Playing; + break; + case ePlayerStatus.Stopped: + _player.Play(); + PlayerStatus = ePlayerStatus.Playing; + break; + default: + throw new ArgumentOutOfRangeException(); + } } public void Stop() { - throw new NotImplementedException(); + if (PlayerStatus != ePlayerStatus.Stopped) + { + _player.Stop(); + PlayerStatus = ePlayerStatus.Stopped; + } } public void IncreaseVolume() { - throw new NotImplementedException(); + // Need to verify what SoundFlow's max volume level is + // For now this should be enough based on testing + if (_player.Volume < 2.0f) + { + _player.Volume += .1f; + } } public void DecreaseVolume() { - throw new NotImplementedException(); + // Ensure that the volume isn't negative + // otherwise the player will crash + if (_player.Volume > .1f) + { + _player.Volume -= .1f; + } + + if (_player.Volume <= .1f) + { + _player.Volume = 0f; + } } public void PlayFromPlaylist(string path) @@ -71,4 +123,9 @@ public void SeekBackwards() { throw new NotImplementedException(); } + + public void Dispose() + { + _soundEngine.Dispose(); + } } \ No newline at end of file diff --git a/src/SoundEngines/WinPlayer.cs b/src/SoundEngines/WinPlayer.cs index 97e2934..63585f6 100644 --- a/src/SoundEngines/WinPlayer.cs +++ b/src/SoundEngines/WinPlayer.cs @@ -1,7 +1,7 @@ // // Licensed under the GNU GPL v3 License. See LICENSE in the project root for license information. // - +/* using MusicSharp.Enums; using System; using System.IO; @@ -12,8 +12,9 @@ namespace MusicSharp.SoundEngines; /// /// The audio player implementation for Windows using NAudio. /// -public class WinPlayer : IPlayer +public class WinPlayer : ISoundEngine { + private readonly WaveOutEvent _outputDevice; private AudioFileReader _audioFileReader; @@ -211,4 +212,5 @@ public void SeekBackwards() _audioFileReader.CurrentTime = _audioFileReader.CurrentTime.Subtract(TimeSpan.FromSeconds(5)); } } -} \ No newline at end of file +} +*/ \ No newline at end of file diff --git a/src/view/Tui.cs b/src/Tui/Tui.cs similarity index 97% rename from src/view/Tui.cs rename to src/Tui/Tui.cs index add041a..7655913 100644 --- a/src/view/Tui.cs +++ b/src/Tui/Tui.cs @@ -2,15 +2,15 @@ // Licensed under the GNU GPL v3 License. See LICENSE in the project root for license information. // -using MusicSharp.Enums; -using MusicSharp.SoundEngines; using System; using System.Collections.Generic; using System.IO; +using MusicSharp.Enums; using MusicSharp.Models; +using MusicSharp.SoundEngines; using Terminal.Gui; -namespace MusicSharp.View; +namespace MusicSharp.Tui; /// /// The Gui class houses the CLI elements of MusicSharp. @@ -242,13 +242,13 @@ private void OpenFile() _player.LastFileOpened = d.FilePath.ToString(); _player.OpenFile(_player.LastFileOpened); NowPlaying(_player.LastFileOpened); - AudioProgressBar.Fraction = 0F; - UpdateProgressBar(); - TimePlayedLabel(); + // AudioProgressBar.Fraction = 0F; + // UpdateProgressBar(); + // TimePlayedLabel(); } else { - // This is a good spot for an error message, should one be wanted/needed + MessageBox.Query("Warning", "Invalid file path.", "Close"); } } } From 39b8a6ab8f29788f115a44388b3c30c085a1ed79 Mon Sep 17 00:00:00 2001 From: Mark-James McDougall Date: Sun, 2 Mar 2025 19:43:08 -0500 Subject: [PATCH 05/22] Implement CurrentTime & TrackLength methods. --- src/SoundEngines/ISoundEngine.cs | 4 ++-- src/SoundEngines/SoundEngine.cs | 14 +++++++------- src/Tui/Tui.cs | 25 +++++++++++++------------ 3 files changed, 22 insertions(+), 21 deletions(-) diff --git a/src/SoundEngines/ISoundEngine.cs b/src/SoundEngines/ISoundEngine.cs index 4a9ca2f..a7c91b4 100644 --- a/src/SoundEngines/ISoundEngine.cs +++ b/src/SoundEngines/ISoundEngine.cs @@ -63,13 +63,13 @@ public interface ISoundEngine /// Returns the current playtime of the audioFileReader instance. /// /// The current time played as TimeSpan. - System.TimeSpan CurrentTime(); + float CurrentTime(); /// /// Returns the total track length in timespan format. /// /// The length of the track in timespan format. - System.TimeSpan TrackLength(); + float TrackLength(); /// /// Skip ahead in the audio file 5s. diff --git a/src/SoundEngines/SoundEngine.cs b/src/SoundEngines/SoundEngine.cs index f3171f9..4ce4f72 100644 --- a/src/SoundEngines/SoundEngine.cs +++ b/src/SoundEngines/SoundEngine.cs @@ -103,15 +103,15 @@ public void PlayFromPlaylist(string path) { throw new NotImplementedException(); } + + public float CurrentTime() + { + return _player.Time; + } - public TimeSpan CurrentTime() - { - throw new NotImplementedException(); - } - - public TimeSpan TrackLength() + public float TrackLength() { - throw new NotImplementedException(); + return _player.Duration; } public void SeekForward() diff --git a/src/Tui/Tui.cs b/src/Tui/Tui.cs index 7655913..c99f494 100644 --- a/src/Tui/Tui.cs +++ b/src/Tui/Tui.cs @@ -232,7 +232,7 @@ private void OpenFile() d.DirectoryPath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); // This will filter the dialog on basis of the allowed file types in the array. - d.AllowedFileTypes = new string[] { ".mp3", ".wav", ".flac" }; + d.AllowedFileTypes = [".mp3", ".wav", ".flac"]; Application.Run(d); if (!d.Canceled) @@ -242,9 +242,9 @@ private void OpenFile() _player.LastFileOpened = d.FilePath.ToString(); _player.OpenFile(_player.LastFileOpened); NowPlaying(_player.LastFileOpened); - // AudioProgressBar.Fraction = 0F; - // UpdateProgressBar(); - // TimePlayedLabel(); + AudioProgressBar.Fraction = 0F; + UpdateProgressBar(); + TimePlayedLabel(); } else { @@ -265,7 +265,7 @@ private void OpenStream() Width = Dim.Fill(), }; - var streamURL = new TextField(string.Empty) + var streamUrl = new TextField(string.Empty) { X = 3, Y = 4, @@ -275,7 +275,7 @@ private void OpenStream() var loadStream = new Button(12, 7, "Load Stream"); loadStream.Clicked += () => { - _player.OpenStream(streamURL.Text.ToString()); + _player.OpenStream(streamUrl.Text.ToString()); Application.RequestStop(); }; @@ -287,7 +287,7 @@ private void OpenStream() d.AddButton(loadStream); d.AddButton(cancelStream); - d.Add(editLabel, streamURL); + d.Add(editLabel, streamUrl); Application.Run(d); } @@ -297,7 +297,7 @@ private void LoadPlaylist() var d = new OpenDialog("Open", "Open a playlist") { AllowsMultipleSelection = false }; // This will filter the dialog on basis of the allowed file types in the array. - d.AllowedFileTypes = new string[] { ".m3u" }; + d.AllowedFileTypes = [".m3u"]; Application.Run(d); if (!d.Canceled) @@ -336,8 +336,9 @@ private void TimePlayedLabel() { if (_player.PlayerStatus != ePlayerStatus.Stopped) { - var timePlayed = _player.CurrentTime().ToString(@"mm\:ss"); - var trackLength = _player.TrackLength().ToString(@"mm\:ss"); + var timePlayed = TimeSpan.FromSeconds((double)(new decimal(_player.CurrentTime()))).ToString(@"hh\:mm\:ss"); + var trackLength = TimeSpan.FromSeconds((double)(new decimal(_player.TrackLength()))).ToString(@"hh\:mm\:ss"); + _trackName = new Label($"{timePlayed} / {trackLength}") { X = Pos.Right(AudioProgressBar), @@ -360,9 +361,9 @@ private void UpdateProgressBar() { _mainLoopTimeout = Application.MainLoop.AddTimeout(TimeSpan.FromSeconds(1), (updateTimer) => { - while (_player.CurrentTime().Seconds < _player.TrackLength().TotalSeconds && _player.PlayerStatus is not ePlayerStatus.Stopped) + while (_player.CurrentTime() < _player.TrackLength() && _player.PlayerStatus is not ePlayerStatus.Stopped) { - AudioProgressBar.Fraction = (float)(_player.CurrentTime().Seconds / _player.TrackLength().TotalSeconds); + AudioProgressBar.Fraction = _player.CurrentTime() / _player.TrackLength(); TimePlayedLabel(); return true; From 041cf3b72d7246fda1f81eb839d52fc5ddb223c3 Mon Sep 17 00:00:00 2001 From: Mark-James McDougall Date: Sun, 2 Mar 2025 20:29:45 -0500 Subject: [PATCH 06/22] Implement seek functionality --- src/SoundEngines/SoundEngine.cs | 14 ++++++++++---- src/Tui/Tui.cs | 4 ++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/SoundEngines/SoundEngine.cs b/src/SoundEngines/SoundEngine.cs index 4ce4f72..4e0b3b2 100644 --- a/src/SoundEngines/SoundEngine.cs +++ b/src/SoundEngines/SoundEngine.cs @@ -8,7 +8,6 @@ namespace MusicSharp.SoundEngines; - // Cross-platform sound engine that works for all devices which // the .NET platform runs on. public class SoundEngine: ISoundEngine, IDisposable @@ -101,7 +100,8 @@ public void DecreaseVolume() public void PlayFromPlaylist(string path) { - throw new NotImplementedException(); + _player.Play(); + PlayerStatus = ePlayerStatus.Playing; } public float CurrentTime() @@ -116,12 +116,18 @@ public float TrackLength() public void SeekForward() { - throw new NotImplementedException(); + if (_player.Time < _player.Duration - 5f) + { + _player.Seek(_player.Time + 5f); + } } public void SeekBackwards() { - throw new NotImplementedException(); + if (_player.Time > 5f) + { + _player.Seek(_player.Time - 5f); + } } public void Dispose() diff --git a/src/Tui/Tui.cs b/src/Tui/Tui.cs index c99f494..0dc01d1 100644 --- a/src/Tui/Tui.cs +++ b/src/Tui/Tui.cs @@ -53,7 +53,7 @@ public Tui(ISoundEngine player) /// public void Start() { - // Creates a instance of MainLoop to process input events, handle timers and other sources of data. + // Creates an instance of MainLoop to process input events, handle timers and other sources of data. Application.Init(); var top = Application.Top; @@ -77,7 +77,7 @@ public void Start() { new MenuItem("_About MusicSharp", string.Empty, () => { - MessageBox.Query("Music Sharp 0.7.5", "\nMusic Sharp is a lightweight CLI\n music player written in C#.\n\nDeveloped by Mark-James McDougall\nand licensed under the GPL v3.\n ", "Close"); + MessageBox.Query("Music Sharp 0.8.5", "\nMusic Sharp is a lightweight CLI\n music player written in C#.\n\nDeveloped by Mark-James McDougall\nand licensed under the GPL v3.\n ", "Close"); }), }), }); From 375103f6f0236d00d505040342e40ec59a040bd3 Mon Sep 17 00:00:00 2001 From: Mark-James McDougall Date: Sat, 8 Mar 2025 22:50:29 -0500 Subject: [PATCH 07/22] Fix OpenFile() logic to make sure player is stopped before playing new file. --- src/SoundEngines/SoundEngine.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/SoundEngines/SoundEngine.cs b/src/SoundEngines/SoundEngine.cs index 4e0b3b2..1050d76 100644 --- a/src/SoundEngines/SoundEngine.cs +++ b/src/SoundEngines/SoundEngine.cs @@ -10,7 +10,7 @@ namespace MusicSharp.SoundEngines; // Cross-platform sound engine that works for all devices which // the .NET platform runs on. -public class SoundEngine: ISoundEngine, IDisposable +public sealed class SoundEngine: ISoundEngine, IDisposable { private readonly MiniAudioEngine _soundEngine; private SoundPlayer _player; @@ -28,8 +28,15 @@ public void OpenFile(string path) { if (File.Exists(path)) { - _player = new SoundPlayer(new StreamDataProvider(File.OpenRead(path))); + if (_player != null) + { + _player.Stop(); + _player = new SoundPlayer(new StreamDataProvider(File.OpenRead(path))); + + } + _player = new SoundPlayer(new StreamDataProvider(File.OpenRead(path))); + // Add the player to the master mixer. This connects the player's output to the audio engine's output. Mixer.Master.AddComponent(_player); From 8d683335f98f4db0be838da5ae0fcb654c968eff Mon Sep 17 00:00:00 2001 From: Mark-James McDougall Date: Mon, 10 Mar 2025 20:47:08 -0400 Subject: [PATCH 08/22] Implement Play From Playlist functionality using new sound engine. --- src/Models/PlaylistLoader.cs | 10 ++--- src/Program.cs | 5 ++- src/SoundEngines/ISoundEngine.cs | 26 +++++-------- src/SoundEngines/SoundEngine.cs | 64 +++++++++++++------------------- src/Tui/Tui.cs | 27 ++++++++------ 5 files changed, 58 insertions(+), 74 deletions(-) diff --git a/src/Models/PlaylistLoader.cs b/src/Models/PlaylistLoader.cs index ac6ba0a..41c62c1 100644 --- a/src/Models/PlaylistLoader.cs +++ b/src/Models/PlaylistLoader.cs @@ -3,6 +3,7 @@ // using System.Collections.Generic; +using System.Linq; using ATL.Playlist; namespace MusicSharp.Models; @@ -22,14 +23,9 @@ public static class PlaylistLoader /// The user specified playlist path. public static List LoadPlaylist(string userPlaylist) { - var filePaths = new List(); var theReader = PlaylistIOFactory.GetInstance().GetPlaylistIO(userPlaylist); - foreach (var s in theReader.FilePaths) - { - filePaths.Add(s); - } - - return filePaths; + // Fix space formatting as SoundFlow doesn't support encoded spaces + return theReader.FilePaths.Select(s => s.Replace("%20", " ")).ToList(); } } \ No newline at end of file diff --git a/src/Program.cs b/src/Program.cs index 7dbba4e..3dc7546 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -3,6 +3,8 @@ // using MusicSharp.SoundEngines; +using SoundFlow.Backends.MiniAudio; +using SoundFlow.Enums; namespace MusicSharp; @@ -16,7 +18,8 @@ public static class Program /// public static void Main() { - using var player = new SoundEngine(); + using var soundEngine = new MiniAudioEngine(44100, Capability.Playback); + var player = new SoundEngine(soundEngine); var gui = new Tui.Tui(player); gui.Start(); diff --git a/src/SoundEngines/ISoundEngine.cs b/src/SoundEngines/ISoundEngine.cs index a7c91b4..f196ed7 100644 --- a/src/SoundEngines/ISoundEngine.cs +++ b/src/SoundEngines/ISoundEngine.cs @@ -25,24 +25,24 @@ public interface ISoundEngine /// Method to play audio. /// /// The filepath of the audio file to play. - void OpenFile(string path); - + void Play(string path); + /// - /// Method to play an audio stream from a URL. - /// - /// The stream URL of the audio file to play. - void OpenStream(string streamUrl); - - /// - /// Method to pause audio playback. + /// Method to play or pause depending on state. /// void PlayPause(); - + /// /// Method to stop audio playback. /// void Stop(); + /// + /// Method to play an audio stream from a URL. + /// + /// The stream URL of the audio file to play. + void OpenStream(string streamUrl); + /// /// Method to increase audio playback volume. /// @@ -53,12 +53,6 @@ public interface ISoundEngine /// void DecreaseVolume(); - /// - /// Play an audio file contained in a playlist. - /// - /// The path to the audio file. - void PlayFromPlaylist(string path); - /// /// Returns the current playtime of the audioFileReader instance. /// diff --git a/src/SoundEngines/SoundEngine.cs b/src/SoundEngines/SoundEngine.cs index 1050d76..ec04e1b 100644 --- a/src/SoundEngines/SoundEngine.cs +++ b/src/SoundEngines/SoundEngine.cs @@ -3,46 +3,40 @@ using MusicSharp.Enums; using SoundFlow.Backends.MiniAudio; using SoundFlow.Components; -using SoundFlow.Enums; using SoundFlow.Providers; namespace MusicSharp.SoundEngines; // Cross-platform sound engine that works for all devices which // the .NET platform runs on. -public sealed class SoundEngine: ISoundEngine, IDisposable +public sealed class SoundEngine : ISoundEngine, IDisposable { private readonly MiniAudioEngine _soundEngine; private SoundPlayer _player; - + public ePlayerStatus PlayerStatus { get; set; } public string LastFileOpened { get; set; } - public SoundEngine() + public SoundEngine(MiniAudioEngine soundEngine) { - _soundEngine = new MiniAudioEngine(44100, Capability.Playback); + _soundEngine = soundEngine; } - - public void OpenFile(string path) + + public void Play(string path) { - if (File.Exists(path)) + if (_player != null) { - if (_player != null) - { - _player.Stop(); - _player = new SoundPlayer(new StreamDataProvider(File.OpenRead(path))); - - } - - _player = new SoundPlayer(new StreamDataProvider(File.OpenRead(path))); - - // Add the player to the master mixer. This connects the player's output to the audio engine's output. - Mixer.Master.AddComponent(_player); - - _player.Play(); - PlayerStatus = ePlayerStatus.Playing; + _player.Stop(); } + + _player = new SoundPlayer(new StreamDataProvider(File.OpenRead(path))); + + // Add the player to the master mixer. This connects the player's output to the audio engine's output. + Mixer.Master.AddComponent(_player); + + _player.Play(); + PlayerStatus = ePlayerStatus.Playing; } public void OpenStream(string streamUrl) @@ -104,23 +98,7 @@ public void DecreaseVolume() _player.Volume = 0f; } } - - public void PlayFromPlaylist(string path) - { - _player.Play(); - PlayerStatus = ePlayerStatus.Playing; - } - public float CurrentTime() - { - return _player.Time; - } - - public float TrackLength() - { - return _player.Duration; - } - public void SeekForward() { if (_player.Time < _player.Duration - 5f) @@ -136,6 +114,16 @@ public void SeekBackwards() _player.Seek(_player.Time - 5f); } } + + public float CurrentTime() + { + return _player.Time; + } + + public float TrackLength() + { + return _player.Duration; + } public void Dispose() { diff --git a/src/Tui/Tui.cs b/src/Tui/Tui.cs index 0dc01d1..bb7f56f 100644 --- a/src/Tui/Tui.cs +++ b/src/Tui/Tui.cs @@ -173,7 +173,7 @@ public void Start() _playlistView.OpenSelectedItem += (a) => { _player.LastFileOpened = a.Value.ToString(); - _player.PlayFromPlaylist(_player.LastFileOpened); + _player.Play(_player.LastFileOpened); NowPlaying(_player.LastFileOpened); UpdateProgressBar(); }; @@ -239,16 +239,19 @@ private void OpenFile() { if (File.Exists(d.FilePath.ToString())) { - _player.LastFileOpened = d.FilePath.ToString(); - _player.OpenFile(_player.LastFileOpened); - NowPlaying(_player.LastFileOpened); - AudioProgressBar.Fraction = 0F; - UpdateProgressBar(); - TimePlayedLabel(); - } - else - { - MessageBox.Query("Warning", "Invalid file path.", "Close"); + try + { + _player.LastFileOpened = d.FilePath.ToString(); + _player.Play(_player.LastFileOpened); + NowPlaying(_player.LastFileOpened); + AudioProgressBar.Fraction = 0F; + UpdateProgressBar(); + TimePlayedLabel(); + } + catch (FileNotFoundException ex) + { + MessageBox.Query("Warning", "Invalid file path.", "Close"); + } } } } @@ -320,7 +323,7 @@ private void LoadPlaylist() } } - private void NowPlaying(string track) + private static void NowPlaying(string track) { _trackName = new Label(track) { From c1f16d92ba59514ad3f86ce5c21b1833c78763f4 Mon Sep 17 00:00:00 2001 From: Mark-James McDougall Date: Mon, 10 Mar 2025 20:47:22 -0400 Subject: [PATCH 09/22] Tests for new SoundEngine. --- ...{WinPlayerTests.cs => SoundEngineTests.cs} | 13 +- src/SoundEngines/WinPlayer.cs | 216 ------------------ 2 files changed, 8 insertions(+), 221 deletions(-) rename MusicSharpTests/{WinPlayerTests.cs => SoundEngineTests.cs} (55%) delete mode 100644 src/SoundEngines/WinPlayer.cs diff --git a/MusicSharpTests/WinPlayerTests.cs b/MusicSharpTests/SoundEngineTests.cs similarity index 55% rename from MusicSharpTests/WinPlayerTests.cs rename to MusicSharpTests/SoundEngineTests.cs index 7046041..038e3c9 100644 --- a/MusicSharpTests/WinPlayerTests.cs +++ b/MusicSharpTests/SoundEngineTests.cs @@ -1,9 +1,11 @@ using MusicSharp.SoundEngines; +using SoundFlow.Backends.MiniAudio; +using SoundFlow.Enums; namespace MusicSharpTests; -public class WinPlayerTests -{ /* +public class SoundEngineTests +{ [Test] public void Play_NullFile() { @@ -18,9 +20,10 @@ public void Play_NullFile() public void PlayFromPlaylist_NullFile() { // arrange - var player = new WinPlayer(); + using var soundEngine = new MiniAudioEngine(44100, Capability.Playback); + using var player = new SoundEngine(soundEngine); // act and assert - Assert.Throws(() => player.PlayFromPlaylist(null)); - } */ + Assert.Throws(() => player.Play(null)); + } } \ No newline at end of file diff --git a/src/SoundEngines/WinPlayer.cs b/src/SoundEngines/WinPlayer.cs deleted file mode 100644 index 63585f6..0000000 --- a/src/SoundEngines/WinPlayer.cs +++ /dev/null @@ -1,216 +0,0 @@ -// -// Licensed under the GNU GPL v3 License. See LICENSE in the project root for license information. -// -/* -using MusicSharp.Enums; -using System; -using System.IO; -using NAudio.Wave; - -namespace MusicSharp.SoundEngines; - -/// -/// The audio player implementation for Windows using NAudio. -/// -public class WinPlayer : ISoundEngine -{ - - private readonly WaveOutEvent _outputDevice; - private AudioFileReader _audioFileReader; - - /// - /// Initializes a new instance of the class. - /// - public WinPlayer() - { - _outputDevice = new WaveOutEvent(); - _outputDevice.PlaybackStopped += OnPlaybackStopped; - } - - /// - public ePlayerStatus PlayerStatus { get; set; } = ePlayerStatus.Stopped; - - /// - public string LastFileOpened { get; set; } - - /// - public void Stop() - { - _outputDevice.Stop(); - PlayerStatus = ePlayerStatus.Stopped; - } - - /// - /// Opens an audio file and then plays it. - /// - /// The filepath. - public void OpenFile(string path) - { - var isFileValid = File.Exists(path); - - if (isFileValid) - { - try - { - _audioFileReader = new AudioFileReader(path); - _outputDevice.Init(_audioFileReader); - _outputDevice.Play(); - PlayerStatus = ePlayerStatus.Playing; - } - catch (FileNotFoundException) - { - } - } - - // Space for error message, should one be wanted/needed. - } - - /// - /// Method to play and pause audio playback depending on PlaybackState. - /// - public void PlayPause() - { - if (_outputDevice.PlaybackState == PlaybackState.Stopped) - { - _outputDevice.Play(); - PlayerStatus = ePlayerStatus.Playing; - return; - } - if (_outputDevice.PlaybackState == PlaybackState.Paused) - { - _outputDevice.Play(); - PlayerStatus = ePlayerStatus.Playing; - return; - } - if (_outputDevice.PlaybackState == PlaybackState.Playing) - { - _outputDevice.Pause(); - PlayerStatus = ePlayerStatus.Paused; - } - } - - /// - public void PlayFromPlaylist(string path) - { - if (_outputDevice != null) - { - _outputDevice.Dispose(); - - try - { - _audioFileReader = new AudioFileReader(path); - _outputDevice.Init(_audioFileReader); - _outputDevice.Play(); - PlayerStatus = ePlayerStatus.Playing; - } - catch (FileNotFoundException) - { - } - } - } - - /// - /// Dispose of our device once playback is stopped. - /// - /// The object sender. - /// The StoppedEventArgs. - public void OnPlaybackStopped(object sender, StoppedEventArgs args) - { - if (_audioFileReader is not null) - { - _audioFileReader.Dispose(); - } - - _outputDevice.Dispose(); - } - - /// - /// Method to increase audio playback volume. - /// - public void IncreaseVolume() - { - // Use this construct to prevent edge cases going over 1.0f - // This is caused by using floats in WaveOutEvent - if (_outputDevice.Volume > 0.9f) - { - _outputDevice.Volume = 1.0f; - return; - } - - _outputDevice.Volume += 0.1f; - } - - /// - /// Method to decrease audio playback volume. - /// - public void DecreaseVolume() - { - // Use this construct to prevent edge cases going under 0.0f - // This is caused by using floats in WaveOutEvent - if (_outputDevice.Volume < 0.1f) - { - _outputDevice.Volume = 0.0f; - return; - } - - _outputDevice.Volume -= 0.1f; - } - - /// - /// Method to open an audio stream. - /// - /// The URL of the stream. - public void OpenStream(string streamUrl) - { - try - { - using var mf = new MediaFoundationReader(streamUrl); - - _outputDevice.Init(mf); - _outputDevice.Play(); - } - catch (NullReferenceException) - { - } - } - - /// - public TimeSpan CurrentTime() - { - var zeroTime = new TimeSpan(0); - - if (_outputDevice.PlaybackState != PlaybackState.Stopped) - { - return _audioFileReader.CurrentTime; - } - else - { - return zeroTime; - } - } - - /// - public TimeSpan TrackLength() - { - return _audioFileReader.TotalTime; - } - - /// - public void SeekForward() - { - if (_audioFileReader != null && _audioFileReader.CurrentTime <= _audioFileReader.TotalTime) - { - _audioFileReader.CurrentTime = _audioFileReader.CurrentTime.Add(TimeSpan.FromSeconds(5)); - } - } - - /// - public void SeekBackwards() - { - if (_audioFileReader != null && _audioFileReader.CurrentTime >= TimeSpan.FromSeconds(5)) - { - _audioFileReader.CurrentTime = _audioFileReader.CurrentTime.Subtract(TimeSpan.FromSeconds(5)); - } - } -} -*/ \ No newline at end of file From 62e3de5a837a6f4c5a0c6e3bd9100e9b36c977a8 Mon Sep 17 00:00:00 2001 From: Mark-James McDougall Date: Mon, 10 Mar 2025 20:53:30 -0400 Subject: [PATCH 10/22] Display not implemented yet notice for streaming playback. --- src/Tui/Tui.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Tui/Tui.cs b/src/Tui/Tui.cs index bb7f56f..b240f6e 100644 --- a/src/Tui/Tui.cs +++ b/src/Tui/Tui.cs @@ -259,6 +259,9 @@ private void OpenFile() // Open and play an audio stream. private void OpenStream() { + MessageBox.Query("Notice", "Streaming support is not yet implemented.", "Close"); + + /* var d = new Dialog("Open Stream", 50, 15); var editLabel = new Label("Enter the url of the audio stream to load:\n(.mp3 only)") @@ -292,6 +295,7 @@ private void OpenStream() d.AddButton(cancelStream); d.Add(editLabel, streamUrl); Application.Run(d); + */ } // Load a playlist file. Currently, only M3U is supported. From ee0eaa8d74754df8f6692178b26306dc9c0c5f2a Mon Sep 17 00:00:00 2001 From: Mark-James McDougall Date: Mon, 10 Mar 2025 21:23:32 -0400 Subject: [PATCH 11/22] Working on implementing streaming functionality. --- src/Enums/eFileType.cs | 7 +++++++ src/Enums/ePlayerStatus.cs | 2 +- src/Program.cs | 4 +++- src/SoundEngines/ISoundEngine.cs | 9 ++------- src/SoundEngines/SoundEngine.cs | 21 ++++++++++++++------- src/Tui/Tui.cs | 22 +++++++++++----------- 6 files changed, 38 insertions(+), 27 deletions(-) create mode 100644 src/Enums/eFileType.cs diff --git a/src/Enums/eFileType.cs b/src/Enums/eFileType.cs new file mode 100644 index 0000000..4be6762 --- /dev/null +++ b/src/Enums/eFileType.cs @@ -0,0 +1,7 @@ +namespace MusicSharp.Enums; + +public enum eFileType +{ + File, + Stream +} \ No newline at end of file diff --git a/src/Enums/ePlayerStatus.cs b/src/Enums/ePlayerStatus.cs index e998289..a7e9af9 100644 --- a/src/Enums/ePlayerStatus.cs +++ b/src/Enums/ePlayerStatus.cs @@ -8,4 +8,4 @@ public enum ePlayerStatus Playing, Paused, Stopped -} +} \ No newline at end of file diff --git a/src/Program.cs b/src/Program.cs index 3dc7546..89bb767 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -2,6 +2,7 @@ // Licensed under the GNU GPL v3 License. See LICENSE in the project root for license information. // +using System.Net.Http; using MusicSharp.SoundEngines; using SoundFlow.Backends.MiniAudio; using SoundFlow.Enums; @@ -18,9 +19,10 @@ public static class Program /// public static void Main() { + using var httpClient = new HttpClient(); using var soundEngine = new MiniAudioEngine(44100, Capability.Playback); var player = new SoundEngine(soundEngine); - var gui = new Tui.Tui(player); + var gui = new Tui.Tui(player, httpClient); gui.Start(); } diff --git a/src/SoundEngines/ISoundEngine.cs b/src/SoundEngines/ISoundEngine.cs index f196ed7..cc0098a 100644 --- a/src/SoundEngines/ISoundEngine.cs +++ b/src/SoundEngines/ISoundEngine.cs @@ -25,7 +25,8 @@ public interface ISoundEngine /// Method to play audio. /// /// The filepath of the audio file to play. - void Play(string path); + /// /// The type of audio file (. + void Play(object path, eFileType fileType); /// /// Method to play or pause depending on state. @@ -37,12 +38,6 @@ public interface ISoundEngine /// void Stop(); - /// - /// Method to play an audio stream from a URL. - /// - /// The stream URL of the audio file to play. - void OpenStream(string streamUrl); - /// /// Method to increase audio playback volume. /// diff --git a/src/SoundEngines/SoundEngine.cs b/src/SoundEngines/SoundEngine.cs index ec04e1b..ba8d1b7 100644 --- a/src/SoundEngines/SoundEngine.cs +++ b/src/SoundEngines/SoundEngine.cs @@ -23,14 +23,26 @@ public SoundEngine(MiniAudioEngine soundEngine) _soundEngine = soundEngine; } - public void Play(string path) + public void Play(object path, eFileType fileType) { if (_player != null) { _player.Stop(); } - _player = new SoundPlayer(new StreamDataProvider(File.OpenRead(path))); + switch (fileType) + { + case eFileType.File: + _player = new SoundPlayer(new StreamDataProvider(File.OpenRead((string)path))); + break; + + case eFileType.Stream: + _player = new SoundPlayer(new StreamDataProvider((Stream)path)); + break; + + default: + throw new ArgumentOutOfRangeException(); + } // Add the player to the master mixer. This connects the player's output to the audio engine's output. Mixer.Master.AddComponent(_player); @@ -39,11 +51,6 @@ public void Play(string path) PlayerStatus = ePlayerStatus.Playing; } - public void OpenStream(string streamUrl) - { - throw new NotImplementedException(); - } - public void PlayPause() { switch (PlayerStatus) diff --git a/src/Tui/Tui.cs b/src/Tui/Tui.cs index b240f6e..9395011 100644 --- a/src/Tui/Tui.cs +++ b/src/Tui/Tui.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Net.Http; using MusicSharp.Enums; using MusicSharp.Models; using MusicSharp.SoundEngines; @@ -33,14 +34,17 @@ public class Tui private object _mainLoopTimeout = null; private List _playlist = new List(); + + private HttpClient _httpClient; /// /// Initializes a new instance of the class. /// /// The player to be injected. - public Tui(ISoundEngine player) + public Tui(ISoundEngine player, HttpClient httpClient) { _player = player; + _httpClient = httpClient; } /// @@ -173,7 +177,7 @@ public void Start() _playlistView.OpenSelectedItem += (a) => { _player.LastFileOpened = a.Value.ToString(); - _player.Play(_player.LastFileOpened); + _player.Play(_player.LastFileOpened, eFileType.File); NowPlaying(_player.LastFileOpened); UpdateProgressBar(); }; @@ -242,7 +246,7 @@ private void OpenFile() try { _player.LastFileOpened = d.FilePath.ToString(); - _player.Play(_player.LastFileOpened); + _player.Play(_player.LastFileOpened, eFileType.File); NowPlaying(_player.LastFileOpened); AudioProgressBar.Fraction = 0F; UpdateProgressBar(); @@ -259,12 +263,9 @@ private void OpenFile() // Open and play an audio stream. private void OpenStream() { - MessageBox.Query("Notice", "Streaming support is not yet implemented.", "Close"); - - /* var d = new Dialog("Open Stream", 50, 15); - var editLabel = new Label("Enter the url of the audio stream to load:\n(.mp3 only)") + var editLabel = new Label("Enter the url of the audio stream to load:") { X = 0, Y = 0, @@ -279,10 +280,10 @@ private void OpenStream() }; var loadStream = new Button(12, 7, "Load Stream"); - loadStream.Clicked += () => + loadStream.Clicked += async () => { - _player.OpenStream(streamUrl.Text.ToString()); - Application.RequestStop(); + var stream = await _httpClient.GetStreamAsync(streamUrl.ToString()); + _player.Play(stream, eFileType.Stream); }; var cancelStream = new Button(29, 7, "Cancel"); @@ -295,7 +296,6 @@ private void OpenStream() d.AddButton(cancelStream); d.Add(editLabel, streamUrl); Application.Run(d); - */ } // Load a playlist file. Currently, only M3U is supported. From c38682184bb69d1de4fbe54ce6511930fd685680 Mon Sep 17 00:00:00 2001 From: Mark-James McDougall Date: Mon, 10 Mar 2025 21:25:05 -0400 Subject: [PATCH 12/22] Update tests. --- MusicSharpTests/SoundEngineTests.cs | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/MusicSharpTests/SoundEngineTests.cs b/MusicSharpTests/SoundEngineTests.cs index 038e3c9..024861b 100644 --- a/MusicSharpTests/SoundEngineTests.cs +++ b/MusicSharpTests/SoundEngineTests.cs @@ -1,7 +1,3 @@ -using MusicSharp.SoundEngines; -using SoundFlow.Backends.MiniAudio; -using SoundFlow.Enums; - namespace MusicSharpTests; public class SoundEngineTests @@ -15,15 +11,4 @@ public void Play_NullFile() // act and assert Assert.That(isFileValid, Is.False); } - - [Test] - public void PlayFromPlaylist_NullFile() - { - // arrange - using var soundEngine = new MiniAudioEngine(44100, Capability.Playback); - using var player = new SoundEngine(soundEngine); - - // act and assert - Assert.Throws(() => player.Play(null)); - } } \ No newline at end of file From ff137b3f3c449538c6e5ce4c55ce751650681d3a Mon Sep 17 00:00:00 2001 From: Mark-James McDougall Date: Mon, 10 Mar 2025 22:30:23 -0400 Subject: [PATCH 13/22] Refactor Tui namespace to Interface. --- src/Program.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Program.cs b/src/Program.cs index 89bb767..8ae328a 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -4,6 +4,7 @@ using System.Net.Http; using MusicSharp.SoundEngines; +using MusicSharp.Interface; using SoundFlow.Backends.MiniAudio; using SoundFlow.Enums; @@ -20,9 +21,9 @@ public static class Program public static void Main() { using var httpClient = new HttpClient(); - using var soundEngine = new MiniAudioEngine(44100, Capability.Playback); - var player = new SoundEngine(soundEngine); - var gui = new Tui.Tui(player, httpClient); + var soundEngine = new MiniAudioEngine(44100, Capability.Playback); + using var player = new SoundEngine(soundEngine); + var gui = new Tui(player, httpClient); gui.Start(); } From 2881945053acc2cc6a19fed2dcdcd7779d5b9c5b Mon Sep 17 00:00:00 2001 From: Mark-James McDougall Date: Mon, 10 Mar 2025 22:30:36 -0400 Subject: [PATCH 14/22] Finish implementing streaming functionality. --- src/{Tui => Interface}/Tui.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) rename src/{Tui => Interface}/Tui.cs (98%) diff --git a/src/Tui/Tui.cs b/src/Interface/Tui.cs similarity index 98% rename from src/Tui/Tui.cs rename to src/Interface/Tui.cs index 9395011..347686f 100644 --- a/src/Tui/Tui.cs +++ b/src/Interface/Tui.cs @@ -11,7 +11,7 @@ using MusicSharp.SoundEngines; using Terminal.Gui; -namespace MusicSharp.Tui; +namespace MusicSharp.Interface; /// /// The Gui class houses the CLI elements of MusicSharp. @@ -35,7 +35,7 @@ public class Tui private List _playlist = new List(); - private HttpClient _httpClient; + private readonly HttpClient _httpClient; /// /// Initializes a new instance of the class. @@ -265,7 +265,7 @@ private void OpenStream() { var d = new Dialog("Open Stream", 50, 15); - var editLabel = new Label("Enter the url of the audio stream to load:") + var editLabel = new Label("Enter the url of the audio stream to load\n (.mp3 streams only):") { X = 0, Y = 0, @@ -282,11 +282,11 @@ private void OpenStream() var loadStream = new Button(12, 7, "Load Stream"); loadStream.Clicked += async () => { - var stream = await _httpClient.GetStreamAsync(streamUrl.ToString()); + var stream = await _httpClient.GetStreamAsync(streamUrl.Text.ToString()); _player.Play(stream, eFileType.Stream); }; - var cancelStream = new Button(29, 7, "Cancel"); + var cancelStream = new Button(29, 7, "Close"); cancelStream.Clicked += () => { Application.RequestStop(); From 1e189c19205f80fbfc242088bc3bfdc22b7c8460 Mon Sep 17 00:00:00 2001 From: Mark-James McDougall Date: Tue, 11 Mar 2025 21:35:50 -0400 Subject: [PATCH 15/22] Update README.md to reflect new version. --- README.md | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index e8f6439..bde82bf 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ MusicSharp is a cross-platform Terminal User Interface (TUI) music player written in C# (.NET 8) with the goal of being minimalistic and light on resources. -Currently in beta, MusicSharp makes use of the [NAudio](https://github.com/naudio/NAudio) and [Terminal.Gui](https://github.com/migueldeicaza/gui.cs) libraries. A project build log can be [found here](https://markjames.dev/blog/developing-a-cli-music-player-csharp/) +Currently in beta, MusicSharp makes use of the [SoundFlow](https://github.com/LSXPrime/SoundFlow) and [Terminal.Gui](https://github.com/migueldeicaza/gui.cs) libraries. A project build log can be [found here](https://markjames.dev/blog/developing-a-cli-music-player-csharp/) ## Screenshot @@ -11,15 +11,10 @@ Currently in beta, MusicSharp makes use of the [NAudio](https://github.com/naudi ## Features +- Cross-platform (Windows, Mac, Linux) - Play audio files. - Load music playlists (M3U) - Audio streaming. -- Lightweight - -## Planned - -- Save playlists. -- Cross platform support. ## Installation From 18fd2264fcc7ea210e67c551d50c94d326bc5967 Mon Sep 17 00:00:00 2001 From: Mark-James McDougall Date: Tue, 11 Mar 2025 22:30:15 -0400 Subject: [PATCH 16/22] Enable nullable, update version number. --- src/MusicSharp.csproj | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/MusicSharp.csproj b/src/MusicSharp.csproj index 0ee3c59..511bcf9 100644 --- a/src/MusicSharp.csproj +++ b/src/MusicSharp.csproj @@ -3,9 +3,10 @@ Exe net8.0 - 0.4.9 + 0.8.9 Mark-James McDougall Mark-James McDougall + enable MusicSharp.ico From 499d7a4c1b48b05e897b5895c6d0cc6a28539c1c Mon Sep 17 00:00:00 2001 From: Mark-James McDougall Date: Tue, 11 Mar 2025 22:31:23 -0400 Subject: [PATCH 17/22] Refactor player status cases. --- src/SoundEngines/SoundEngine.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/SoundEngines/SoundEngine.cs b/src/SoundEngines/SoundEngine.cs index ba8d1b7..2bbe0d4 100644 --- a/src/SoundEngines/SoundEngine.cs +++ b/src/SoundEngines/SoundEngine.cs @@ -60,9 +60,6 @@ public void PlayPause() PlayerStatus = ePlayerStatus.Paused; break; case ePlayerStatus.Paused: - _player.Play(); - PlayerStatus = ePlayerStatus.Playing; - break; case ePlayerStatus.Stopped: _player.Play(); PlayerStatus = ePlayerStatus.Playing; From e12cc4fa8da4eae46467d50079e16f5bda7b010e Mon Sep 17 00:00:00 2001 From: Mark-James McDougall Date: Wed, 12 Mar 2025 17:08:06 -0400 Subject: [PATCH 18/22] Update enums to use proper naming convention. --- src/Enums/{eFileType.cs => EFileType.cs} | 2 +- .../{ePlayerStatus.cs => EPlayerStatus.cs} | 2 +- src/Interface/Tui.cs | 14 +++++------ src/SoundEngines/ISoundEngine.cs | 4 ++-- src/SoundEngines/SoundEngine.cs | 24 +++++++++---------- 5 files changed, 23 insertions(+), 23 deletions(-) rename src/Enums/{eFileType.cs => EFileType.cs} (70%) rename src/Enums/{ePlayerStatus.cs => EPlayerStatus.cs} (84%) diff --git a/src/Enums/eFileType.cs b/src/Enums/EFileType.cs similarity index 70% rename from src/Enums/eFileType.cs rename to src/Enums/EFileType.cs index 4be6762..1ea3943 100644 --- a/src/Enums/eFileType.cs +++ b/src/Enums/EFileType.cs @@ -1,6 +1,6 @@ namespace MusicSharp.Enums; -public enum eFileType +public enum EFileType { File, Stream diff --git a/src/Enums/ePlayerStatus.cs b/src/Enums/EPlayerStatus.cs similarity index 84% rename from src/Enums/ePlayerStatus.cs rename to src/Enums/EPlayerStatus.cs index a7e9af9..0870196 100644 --- a/src/Enums/ePlayerStatus.cs +++ b/src/Enums/EPlayerStatus.cs @@ -3,7 +3,7 @@ /// /// The status of the audio player. /// -public enum ePlayerStatus +public enum EPlayerStatus { Playing, Paused, diff --git a/src/Interface/Tui.cs b/src/Interface/Tui.cs index 347686f..bd66334 100644 --- a/src/Interface/Tui.cs +++ b/src/Interface/Tui.cs @@ -110,7 +110,7 @@ public void Start() { PlayPause(); - if (_player.PlayerStatus != ePlayerStatus.Stopped) + if (_player.PlayerStatus != EPlayerStatus.Stopped) { UpdateProgressBar(); } @@ -177,7 +177,7 @@ public void Start() _playlistView.OpenSelectedItem += (a) => { _player.LastFileOpened = a.Value.ToString(); - _player.Play(_player.LastFileOpened, eFileType.File); + _player.Play(_player.LastFileOpened, EFileType.File); NowPlaying(_player.LastFileOpened); UpdateProgressBar(); }; @@ -217,7 +217,7 @@ private void PlayPause() { _player.PlayPause(); - if (_player.PlayerStatus == ePlayerStatus.Playing) + if (_player.PlayerStatus == EPlayerStatus.Playing) { UpdateProgressBar(); } @@ -246,7 +246,7 @@ private void OpenFile() try { _player.LastFileOpened = d.FilePath.ToString(); - _player.Play(_player.LastFileOpened, eFileType.File); + _player.Play(_player.LastFileOpened, EFileType.File); NowPlaying(_player.LastFileOpened); AudioProgressBar.Fraction = 0F; UpdateProgressBar(); @@ -283,7 +283,7 @@ private void OpenStream() loadStream.Clicked += async () => { var stream = await _httpClient.GetStreamAsync(streamUrl.Text.ToString()); - _player.Play(stream, eFileType.Stream); + _player.Play(stream, EFileType.Stream); }; var cancelStream = new Button(29, 7, "Close"); @@ -341,7 +341,7 @@ private static void NowPlaying(string track) private void TimePlayedLabel() { - if (_player.PlayerStatus != ePlayerStatus.Stopped) + if (_player.PlayerStatus != EPlayerStatus.Stopped) { var timePlayed = TimeSpan.FromSeconds((double)(new decimal(_player.CurrentTime()))).ToString(@"hh\:mm\:ss"); var trackLength = TimeSpan.FromSeconds((double)(new decimal(_player.TrackLength()))).ToString(@"hh\:mm\:ss"); @@ -368,7 +368,7 @@ private void UpdateProgressBar() { _mainLoopTimeout = Application.MainLoop.AddTimeout(TimeSpan.FromSeconds(1), (updateTimer) => { - while (_player.CurrentTime() < _player.TrackLength() && _player.PlayerStatus is not ePlayerStatus.Stopped) + while (_player.CurrentTime() < _player.TrackLength() && _player.PlayerStatus is not EPlayerStatus.Stopped) { AudioProgressBar.Fraction = _player.CurrentTime() / _player.TrackLength(); TimePlayedLabel(); diff --git a/src/SoundEngines/ISoundEngine.cs b/src/SoundEngines/ISoundEngine.cs index cc0098a..c7e937d 100644 --- a/src/SoundEngines/ISoundEngine.cs +++ b/src/SoundEngines/ISoundEngine.cs @@ -14,7 +14,7 @@ public interface ISoundEngine /// /// Gets or sets a value indicating whether the audio player is playing. /// - ePlayerStatus PlayerStatus { get; set; } + EPlayerStatus PlayerStatus { get; set; } /// /// Gets or sets the last file opened by the player. @@ -26,7 +26,7 @@ public interface ISoundEngine /// /// The filepath of the audio file to play. /// /// The type of audio file (. - void Play(object path, eFileType fileType); + void Play(object path, EFileType fileType); /// /// Method to play or pause depending on state. diff --git a/src/SoundEngines/SoundEngine.cs b/src/SoundEngines/SoundEngine.cs index 2bbe0d4..5ad9a38 100644 --- a/src/SoundEngines/SoundEngine.cs +++ b/src/SoundEngines/SoundEngine.cs @@ -14,7 +14,7 @@ public sealed class SoundEngine : ISoundEngine, IDisposable private readonly MiniAudioEngine _soundEngine; private SoundPlayer _player; - public ePlayerStatus PlayerStatus { get; set; } + public EPlayerStatus PlayerStatus { get; set; } public string LastFileOpened { get; set; } @@ -23,7 +23,7 @@ public SoundEngine(MiniAudioEngine soundEngine) _soundEngine = soundEngine; } - public void Play(object path, eFileType fileType) + public void Play(object path, EFileType fileType) { if (_player != null) { @@ -32,11 +32,11 @@ public void Play(object path, eFileType fileType) switch (fileType) { - case eFileType.File: + case EFileType.File: _player = new SoundPlayer(new StreamDataProvider(File.OpenRead((string)path))); break; - case eFileType.Stream: + case EFileType.Stream: _player = new SoundPlayer(new StreamDataProvider((Stream)path)); break; @@ -48,21 +48,21 @@ public void Play(object path, eFileType fileType) Mixer.Master.AddComponent(_player); _player.Play(); - PlayerStatus = ePlayerStatus.Playing; + PlayerStatus = EPlayerStatus.Playing; } public void PlayPause() { switch (PlayerStatus) { - case ePlayerStatus.Playing: + case EPlayerStatus.Playing: _player.Pause(); - PlayerStatus = ePlayerStatus.Paused; + PlayerStatus = EPlayerStatus.Paused; break; - case ePlayerStatus.Paused: - case ePlayerStatus.Stopped: + case EPlayerStatus.Paused: + case EPlayerStatus.Stopped: _player.Play(); - PlayerStatus = ePlayerStatus.Playing; + PlayerStatus = EPlayerStatus.Playing; break; default: throw new ArgumentOutOfRangeException(); @@ -71,10 +71,10 @@ public void PlayPause() public void Stop() { - if (PlayerStatus != ePlayerStatus.Stopped) + if (PlayerStatus != EPlayerStatus.Stopped) { _player.Stop(); - PlayerStatus = ePlayerStatus.Stopped; + PlayerStatus = EPlayerStatus.Stopped; } } From 280ab785589cc2b21f8a275fd57f9aad8d8fd81f Mon Sep 17 00:00:00 2001 From: Mark-James McDougall Date: Wed, 12 Mar 2025 17:30:15 -0400 Subject: [PATCH 19/22] Refactor project to make program sections clearer. --- .../{SoundEngineTests.cs => SoundFlowPlayerTests.cs} | 2 +- .../ISoundEngine.cs => AudioPlayer/IPlayer.cs} | 7 ++++--- .../SoundEngine.cs => AudioPlayer/SoundFlowPlayer.cs} | 6 +++--- src/MusicSharp.csproj | 2 +- src/Program.cs | 6 +++--- src/{Interface => UI}/Tui.cs | 10 +++++----- 6 files changed, 17 insertions(+), 16 deletions(-) rename MusicSharpTests/{SoundEngineTests.cs => SoundFlowPlayerTests.cs} (87%) rename src/{SoundEngines/ISoundEngine.cs => AudioPlayer/IPlayer.cs} (92%) rename src/{SoundEngines/SoundEngine.cs => AudioPlayer/SoundFlowPlayer.cs} (95%) rename src/{Interface => UI}/Tui.cs (97%) diff --git a/MusicSharpTests/SoundEngineTests.cs b/MusicSharpTests/SoundFlowPlayerTests.cs similarity index 87% rename from MusicSharpTests/SoundEngineTests.cs rename to MusicSharpTests/SoundFlowPlayerTests.cs index 024861b..694b052 100644 --- a/MusicSharpTests/SoundEngineTests.cs +++ b/MusicSharpTests/SoundFlowPlayerTests.cs @@ -1,6 +1,6 @@ namespace MusicSharpTests; -public class SoundEngineTests +public class SoundFlowPlayerTests { [Test] public void Play_NullFile() diff --git a/src/SoundEngines/ISoundEngine.cs b/src/AudioPlayer/IPlayer.cs similarity index 92% rename from src/SoundEngines/ISoundEngine.cs rename to src/AudioPlayer/IPlayer.cs index c7e937d..d304835 100644 --- a/src/SoundEngines/ISoundEngine.cs +++ b/src/AudioPlayer/IPlayer.cs @@ -1,15 +1,16 @@ -// +// // Licensed under the GNU GPL v3 License. See LICENSE in the project root for license information. // +using System; using MusicSharp.Enums; -namespace MusicSharp.SoundEngines; +namespace MusicSharp.AudioPlayer; /// /// Defines the methods an audio player class should implement. /// -public interface ISoundEngine +public interface IPlayer: IDisposable { /// /// Gets or sets a value indicating whether the audio player is playing. diff --git a/src/SoundEngines/SoundEngine.cs b/src/AudioPlayer/SoundFlowPlayer.cs similarity index 95% rename from src/SoundEngines/SoundEngine.cs rename to src/AudioPlayer/SoundFlowPlayer.cs index 5ad9a38..b196fad 100644 --- a/src/SoundEngines/SoundEngine.cs +++ b/src/AudioPlayer/SoundFlowPlayer.cs @@ -5,11 +5,11 @@ using SoundFlow.Components; using SoundFlow.Providers; -namespace MusicSharp.SoundEngines; +namespace MusicSharp.AudioPlayer; // Cross-platform sound engine that works for all devices which // the .NET platform runs on. -public sealed class SoundEngine : ISoundEngine, IDisposable +public sealed class SoundFlowPlayer : IPlayer, IDisposable { private readonly MiniAudioEngine _soundEngine; private SoundPlayer _player; @@ -18,7 +18,7 @@ public sealed class SoundEngine : ISoundEngine, IDisposable public string LastFileOpened { get; set; } - public SoundEngine(MiniAudioEngine soundEngine) + public SoundFlowPlayer(MiniAudioEngine soundEngine) { _soundEngine = soundEngine; } diff --git a/src/MusicSharp.csproj b/src/MusicSharp.csproj index 511bcf9..d8bcba9 100644 --- a/src/MusicSharp.csproj +++ b/src/MusicSharp.csproj @@ -3,7 +3,7 @@ Exe net8.0 - 0.8.9 + 1.0.0 Mark-James McDougall Mark-James McDougall enable diff --git a/src/Program.cs b/src/Program.cs index 8ae328a..38f9692 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -3,8 +3,8 @@ // using System.Net.Http; -using MusicSharp.SoundEngines; -using MusicSharp.Interface; +using MusicSharp.UI; +using MusicSharp.AudioPlayer; using SoundFlow.Backends.MiniAudio; using SoundFlow.Enums; @@ -22,7 +22,7 @@ public static void Main() { using var httpClient = new HttpClient(); var soundEngine = new MiniAudioEngine(44100, Capability.Playback); - using var player = new SoundEngine(soundEngine); + using IPlayer player = new SoundFlowPlayer(soundEngine); var gui = new Tui(player, httpClient); gui.Start(); diff --git a/src/Interface/Tui.cs b/src/UI/Tui.cs similarity index 97% rename from src/Interface/Tui.cs rename to src/UI/Tui.cs index bd66334..08df1b8 100644 --- a/src/Interface/Tui.cs +++ b/src/UI/Tui.cs @@ -8,10 +8,10 @@ using System.Net.Http; using MusicSharp.Enums; using MusicSharp.Models; -using MusicSharp.SoundEngines; +using MusicSharp.AudioPlayer; using Terminal.Gui; -namespace MusicSharp.Interface; +namespace MusicSharp.UI; /// /// The Gui class houses the CLI elements of MusicSharp. @@ -29,7 +29,7 @@ public class Tui /// /// Create a new instance of the audio player engine. /// - private readonly ISoundEngine _player; + private readonly IPlayer _player; private object _mainLoopTimeout = null; @@ -41,7 +41,7 @@ public class Tui /// Initializes a new instance of the class. /// /// The player to be injected. - public Tui(ISoundEngine player, HttpClient httpClient) + public Tui(IPlayer player, HttpClient httpClient) { _player = player; _httpClient = httpClient; @@ -81,7 +81,7 @@ public void Start() { new MenuItem("_About MusicSharp", string.Empty, () => { - MessageBox.Query("Music Sharp 0.8.5", "\nMusic Sharp is a lightweight CLI\n music player written in C#.\n\nDeveloped by Mark-James McDougall\nand licensed under the GPL v3.\n ", "Close"); + MessageBox.Query("Music Sharp 1.0.0", "\nMusic Sharp is a lightweight CLI\n music player written in C#.\n\nDeveloped by Mark-James McDougall\nand licensed under the GPL v3.\n ", "Close"); }), }), }); From a3df4e67602773d33c90d7527b521f14f960c942 Mon Sep 17 00:00:00 2001 From: Mark-James McDougall Date: Wed, 12 Mar 2025 18:48:13 -0400 Subject: [PATCH 20/22] Add Converters.cs class to convert files and URLs to Stream type. --- src/AudioPlayer/IPlayer.cs | 5 ++-- src/AudioPlayer/SoundFlowPlayer.cs | 18 +++------------ src/Helpers/Converters.cs | 26 +++++++++++++++++++++ src/Program.cs | 10 +++++--- src/UI/Tui.cs | 37 ++++++++++++++++++++++-------- 5 files changed, 66 insertions(+), 30 deletions(-) create mode 100644 src/Helpers/Converters.cs diff --git a/src/AudioPlayer/IPlayer.cs b/src/AudioPlayer/IPlayer.cs index d304835..765dec8 100644 --- a/src/AudioPlayer/IPlayer.cs +++ b/src/AudioPlayer/IPlayer.cs @@ -3,6 +3,7 @@ // using System; +using System.IO; using MusicSharp.Enums; namespace MusicSharp.AudioPlayer; @@ -26,8 +27,8 @@ public interface IPlayer: IDisposable /// Method to play audio. /// /// The filepath of the audio file to play. - /// /// The type of audio file (. - void Play(object path, EFileType fileType); + /// /// The audio stream. + void Play(Stream stream); /// /// Method to play or pause depending on state. diff --git a/src/AudioPlayer/SoundFlowPlayer.cs b/src/AudioPlayer/SoundFlowPlayer.cs index b196fad..cc65b1e 100644 --- a/src/AudioPlayer/SoundFlowPlayer.cs +++ b/src/AudioPlayer/SoundFlowPlayer.cs @@ -9,7 +9,7 @@ namespace MusicSharp.AudioPlayer; // Cross-platform sound engine that works for all devices which // the .NET platform runs on. -public sealed class SoundFlowPlayer : IPlayer, IDisposable +public sealed class SoundFlowPlayer : IPlayer { private readonly MiniAudioEngine _soundEngine; private SoundPlayer _player; @@ -23,26 +23,14 @@ public SoundFlowPlayer(MiniAudioEngine soundEngine) _soundEngine = soundEngine; } - public void Play(object path, EFileType fileType) + public void Play(Stream stream) { if (_player != null) { _player.Stop(); } - switch (fileType) - { - case EFileType.File: - _player = new SoundPlayer(new StreamDataProvider(File.OpenRead((string)path))); - break; - - case EFileType.Stream: - _player = new SoundPlayer(new StreamDataProvider((Stream)path)); - break; - - default: - throw new ArgumentOutOfRangeException(); - } + _player = new SoundPlayer(new StreamDataProvider(stream)); // Add the player to the master mixer. This connects the player's output to the audio engine's output. Mixer.Master.AddComponent(_player); diff --git a/src/Helpers/Converters.cs b/src/Helpers/Converters.cs new file mode 100644 index 0000000..9607934 --- /dev/null +++ b/src/Helpers/Converters.cs @@ -0,0 +1,26 @@ +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; + +namespace MusicSharp.Helpers; + +public class Converters +{ + private readonly HttpClient _httpClient; + + public Converters(HttpClient httpClient) + { + _httpClient = httpClient; + } + + public static Stream ConvertFileToStream(string path) + { + return File.OpenRead(path); + } + + public async Task ConvertUrlToStream(string url) + { + var stream = await _httpClient.GetStreamAsync(url); + return stream; + } +} \ No newline at end of file diff --git a/src/Program.cs b/src/Program.cs index 38f9692..b205d50 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -5,6 +5,7 @@ using System.Net.Http; using MusicSharp.UI; using MusicSharp.AudioPlayer; +using MusicSharp.Helpers; using SoundFlow.Backends.MiniAudio; using SoundFlow.Enums; @@ -20,11 +21,14 @@ public static class Program /// public static void Main() { - using var httpClient = new HttpClient(); var soundEngine = new MiniAudioEngine(44100, Capability.Playback); using IPlayer player = new SoundFlowPlayer(soundEngine); - var gui = new Tui(player, httpClient); + + using var httpClient = new HttpClient(); + var converters = new Converters(httpClient); + + var ui = new Tui(player, converters); - gui.Start(); + ui.Start(); } } \ No newline at end of file diff --git a/src/UI/Tui.cs b/src/UI/Tui.cs index 08df1b8..a2757a6 100644 --- a/src/UI/Tui.cs +++ b/src/UI/Tui.cs @@ -9,6 +9,7 @@ using MusicSharp.Enums; using MusicSharp.Models; using MusicSharp.AudioPlayer; +using MusicSharp.Helpers; using Terminal.Gui; namespace MusicSharp.UI; @@ -35,16 +36,17 @@ public class Tui private List _playlist = new List(); - private readonly HttpClient _httpClient; + private readonly Converters _converters; /// /// Initializes a new instance of the class. /// /// The player to be injected. - public Tui(IPlayer player, HttpClient httpClient) + /// Helper class to convert files and urls to Stream type. + public Tui(IPlayer player, Converters converters) { _player = player; - _httpClient = httpClient; + _converters = converters; } /// @@ -176,10 +178,17 @@ public void Start() // Play the selection when a playlist path is clicked. _playlistView.OpenSelectedItem += (a) => { - _player.LastFileOpened = a.Value.ToString(); - _player.Play(_player.LastFileOpened, EFileType.File); - NowPlaying(_player.LastFileOpened); - UpdateProgressBar(); + try + { + _player.LastFileOpened = a.Value.ToString(); + _player.Play(Converters.ConvertFileToStream(_player.LastFileOpened)); + NowPlaying(_player.LastFileOpened); + UpdateProgressBar(); + } + catch (FileNotFoundException ex) + { + MessageBox.Query("Warning", "Invalid file path.", "Close"); + } }; _playlistPane.Add(_playlistView); @@ -246,7 +255,8 @@ private void OpenFile() try { _player.LastFileOpened = d.FilePath.ToString(); - _player.Play(_player.LastFileOpened, EFileType.File); + var stream = Converters.ConvertFileToStream(d.FilePath.ToString()); + _player.Play(stream); NowPlaying(_player.LastFileOpened); AudioProgressBar.Fraction = 0F; UpdateProgressBar(); @@ -282,8 +292,15 @@ private void OpenStream() var loadStream = new Button(12, 7, "Load Stream"); loadStream.Clicked += async () => { - var stream = await _httpClient.GetStreamAsync(streamUrl.Text.ToString()); - _player.Play(stream, EFileType.Stream); + try + { + var stream = await _converters.ConvertUrlToStream(streamUrl.Text.ToString()); + _player.Play(stream); + } + catch (Exception ex) + { + MessageBox.Query("Warning", "Invalid URL.", "Close"); + } }; var cancelStream = new Button(29, 7, "Close"); From 7ccfc63390fa9e39db9d6e9ac0eb26d36a889ffd Mon Sep 17 00:00:00 2001 From: Mark-James McDougall Date: Wed, 12 Mar 2025 18:48:41 -0400 Subject: [PATCH 21/22] Remove unnecessary using statement. --- src/UI/Tui.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/UI/Tui.cs b/src/UI/Tui.cs index a2757a6..7d7d179 100644 --- a/src/UI/Tui.cs +++ b/src/UI/Tui.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Net.Http; using MusicSharp.Enums; using MusicSharp.Models; using MusicSharp.AudioPlayer; From 9d85c1d502ce10f0a7fbb7112c8cbc89058cab51 Mon Sep 17 00:00:00 2001 From: Mark-James McDougall Date: Wed, 12 Mar 2025 18:51:31 -0400 Subject: [PATCH 22/22] Update README.md --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index bde82bf..38fbe9a 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # MusicSharp [![.NET](https://github.com/markjamesm/Baseball-Sharp/actions/workflows/dotnet.yml/badge.svg?branch=master)](https://github.com/markjamesm/MusicSharp/actions) [![C#](https://img.shields.io/badge/Language-CSharp-darkgreen.svg)](https://en.wikipedia.org/wiki/C_Sharp_(programming_language)) [![License](https://img.shields.io/badge/License-GPL-orange.svg)](https://www.gnu.org/licenses/gpl-3.0.en.html) -MusicSharp is a cross-platform Terminal User Interface (TUI) music player written in C# (.NET 8) with the goal of being minimalistic and light on resources. +A cross-platform Terminal User Interface (TUI) music player written in C# (.NET 8) with the goal of being minimalistic and light on resources. -Currently in beta, MusicSharp makes use of the [SoundFlow](https://github.com/LSXPrime/SoundFlow) and [Terminal.Gui](https://github.com/migueldeicaza/gui.cs) libraries. A project build log can be [found here](https://markjames.dev/blog/developing-a-cli-music-player-csharp/) +MusicSharp makes use of the [SoundFlow](https://github.com/LSXPrime/SoundFlow) and [Terminal.Gui](https://github.com/migueldeicaza/gui.cs) libraries. ## Screenshot @@ -11,10 +11,10 @@ Currently in beta, MusicSharp makes use of the [SoundFlow](https://github.com/LS ## Features -- Cross-platform (Windows, Mac, Linux) +- Cross-platform support (Windows, Mac, Linux). - Play audio files. -- Load music playlists (M3U) -- Audio streaming. +- Load and play from music playlists (M3U). +- Streaming support. ## Installation