diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 3354d7c..142a1d1 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -16,10 +16,8 @@ jobs:
- name: Setup .NET Core
uses: actions/setup-dotnet@v1
with:
- dotnet-version: 8.0.*
+ dotnet-version: 9.0.*
- name: Install dependencies
run: dotnet restore
- name: Build
- run: dotnet build --configuration Release --no-restore
- - name: Test
- run: dotnet test --no-restore --verbosity normal
+ run: dotnet build --configuration Release --no-restore
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index e553c84..62540f3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -364,3 +364,5 @@ FodyWeavers.xsd
# Visual Studio Code
.vscode
+
+.idea/
\ No newline at end of file
diff --git a/.idea/.idea.MusicSharp/.idea/.gitignore b/.idea/.idea.MusicSharp/.idea/.gitignore
deleted file mode 100644
index 97791b0..0000000
--- a/.idea/.idea.MusicSharp/.idea/.gitignore
+++ /dev/null
@@ -1,13 +0,0 @@
-# Default ignored files
-/shelf/
-/workspace.xml
-# Rider ignored files
-/modules.xml
-/contentModel.xml
-/projectSettingsUpdater.xml
-/.idea.MusicSharp.iml
-# Editor-based HTTP Client requests
-/httpRequests/
-# Datasource local storage ignored files
-/dataSources/
-/dataSources.local.xml
diff --git a/.idea/.idea.MusicSharp/.idea/encodings.xml b/.idea/.idea.MusicSharp/.idea/encodings.xml
deleted file mode 100644
index df87cf9..0000000
--- a/.idea/.idea.MusicSharp/.idea/encodings.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/.idea/.idea.MusicSharp/.idea/indexLayout.xml b/.idea/.idea.MusicSharp/.idea/indexLayout.xml
deleted file mode 100644
index 323a9b7..0000000
--- a/.idea/.idea.MusicSharp/.idea/indexLayout.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
- .
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/.idea.MusicSharp/.idea/vcs.xml b/.idea/.idea.MusicSharp/.idea/vcs.xml
deleted file mode 100644
index 94a25f7..0000000
--- a/.idea/.idea.MusicSharp/.idea/vcs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/MusicSharp.sln b/MusicSharp.sln
index 7120784..d0869fd 100644
--- a/MusicSharp.sln
+++ b/MusicSharp.sln
@@ -12,8 +12,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
README.md = README.md
EndProjectSection
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MusicSharpTests", "MusicSharpTests\MusicSharpTests.csproj", "{B0797164-0016-4ED1-BDDA-42DE8D414CD6}"
-EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -24,10 +22,6 @@ Global
{A1943162-1979-46C6-9082-7F49BEC191BA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A1943162-1979-46C6-9082-7F49BEC191BA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A1943162-1979-46C6-9082-7F49BEC191BA}.Release|Any CPU.Build.0 = Release|Any CPU
- {B0797164-0016-4ED1-BDDA-42DE8D414CD6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {B0797164-0016-4ED1-BDDA-42DE8D414CD6}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {B0797164-0016-4ED1-BDDA-42DE8D414CD6}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {B0797164-0016-4ED1-BDDA-42DE8D414CD6}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/MusicSharpTests/MusicSharpTests.csproj b/MusicSharpTests/MusicSharpTests.csproj
deleted file mode 100644
index 74f3a7b..0000000
--- a/MusicSharpTests/MusicSharpTests.csproj
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
-
- net8.0
- enable
- enable
-
- false
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/MusicSharpTests/PlaylistLoaderTests.cs b/MusicSharpTests/PlaylistLoaderTests.cs
deleted file mode 100644
index ae20955..0000000
--- a/MusicSharpTests/PlaylistLoaderTests.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-using MusicSharp.PlaylistHandlers;
-
-namespace MusicSharpTests;
-
-public class Tests
-{
- [Test]
- public void Load_NullPlaylist()
- {
- // Act and assert
- Assert.Throws(() => PlaylistLoader.LoadPlaylist(null));
- }
-}
\ No newline at end of file
diff --git a/MusicSharpTests/SoundFlowPlayerTests.cs b/MusicSharpTests/SoundFlowPlayerTests.cs
deleted file mode 100644
index 694b052..0000000
--- a/MusicSharpTests/SoundFlowPlayerTests.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-namespace MusicSharpTests;
-
-public class SoundFlowPlayerTests
-{
- [Test]
- public void Play_NullFile()
- {
- // arrange
- var isFileValid = File.Exists("thisisafail.exe");
-
- // act and assert
- Assert.That(isFileValid, Is.False);
- }
-}
\ No newline at end of file
diff --git a/MusicSharpTests/Usings.cs b/MusicSharpTests/Usings.cs
deleted file mode 100644
index cefced4..0000000
--- a/MusicSharpTests/Usings.cs
+++ /dev/null
@@ -1 +0,0 @@
-global using NUnit.Framework;
\ No newline at end of file
diff --git a/Properties/Resources.Designer.cs b/Properties/Resources.Designer.cs
deleted file mode 100644
index d836cf5..0000000
--- a/Properties/Resources.Designer.cs
+++ /dev/null
@@ -1,63 +0,0 @@
-//------------------------------------------------------------------------------
-//
-// This code was generated by a tool.
-// Runtime Version:4.0.30319.42000
-//
-// Changes to this file may cause incorrect behavior and will be lost if
-// the code is regenerated.
-//
-//------------------------------------------------------------------------------
-
-namespace MusicSharp.Properties {
- using System;
-
-
- ///
- /// A strongly-typed resource class, for looking up localized strings, etc.
- ///
- // This class was auto-generated by the StronglyTypedResourceBuilder
- // class via a tool like ResGen or Visual Studio.
- // To add or remove a member, edit your .ResX file then rerun ResGen
- // with the /str option, or rebuild your VS project.
- [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
- [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
- [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
- internal class Resources {
-
- private static global::System.Resources.ResourceManager resourceMan;
-
- private static global::System.Globalization.CultureInfo resourceCulture;
-
- [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
- internal Resources() {
- }
-
- ///
- /// Returns the cached ResourceManager instance used by this class.
- ///
- [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
- internal static global::System.Resources.ResourceManager ResourceManager {
- get {
- if (object.ReferenceEquals(resourceMan, null)) {
- global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("MusicSharp.Properties.Resources", typeof(Resources).Assembly);
- resourceMan = temp;
- }
- return resourceMan;
- }
- }
-
- ///
- /// Overrides the current thread's CurrentUICulture property for all
- /// resource lookups using this strongly typed resource class.
- ///
- [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
- internal static global::System.Globalization.CultureInfo Culture {
- get {
- return resourceCulture;
- }
- set {
- resourceCulture = value;
- }
- }
- }
-}
diff --git a/Properties/Resources.resx b/Properties/Resources.resx
deleted file mode 100644
index 4fdb1b6..0000000
--- a/Properties/Resources.resx
+++ /dev/null
@@ -1,101 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- text/microsoft-resx
-
-
- 1.3
-
-
- System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
-
-
- System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
-
-
\ No newline at end of file
diff --git a/README.md b/README.md
index 1888844..e194387 100644
--- a/README.md
+++ b/README.md
@@ -1,31 +1,33 @@
# MusicSharp
[](https://github.com/markjamesm/MusicSharp/actions) [](https://en.wikipedia.org/wiki/C_Sharp_(programming_language)) [](https://www.gnu.org/licenses/gpl-3.0.en.html)
-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# with the goal of being minimalist
+and light on resources.
-**Note**: A major rewrite of MusicSharp (version 2) is actively being developed in the
-[musicsharp-v2 branch](https://github.com/markjamesm/MusicSharp/tree/musicsharp-v2). This version will feature:
-
-- A completely rewritten UI, making use of Terminal.Gui 2.0.
-- Library functionality to manage audio files.
-- Standalone application (no .NET runtime required).
+
## Features
-- Cross-platform support (Windows, Mac, Linux).
-- Play audio files.
-- Load and play from music playlists (M3U).
-- Streaming support.
+- Plays MP3, FLAC, and WAV files, with support for internet radio streams.
+- Cross-platform (Windows, Mac, Linux).
+- Standalone application (no .NET runtime required).
+- M3U playlist support.
## Installation
-Download the [latest release of MusicSharp](https://github.com/markjamesm/MusicSharp/releases) and follow the installation instructions.
+Download the [latest release of MusicSharp](https://github.com/markjamesm/MusicSharp/releases) for your platform and follow the instructions below to launch it:
+
+### Windows
+
+Open MusicSharp.exe.
-## Screenshot
+### Linux & Mac
-
+Execute MusicSharp from your preferred terminal:
-MusicSharp makes use of the [SoundFlow](https://github.com/LSXPrime/SoundFlow) and [Terminal.Gui](https://github.com/migueldeicaza/gui.cs) libraries.
+```bash
+./MusicSharp
+```
## Want to Contribute?
diff --git a/screenshots/musicsharp.png b/screenshots/musicsharp.png
new file mode 100644
index 0000000..0810c6f
Binary files /dev/null and b/screenshots/musicsharp.png differ
diff --git a/src/AudioPlayer/IPlayer.cs b/src/AudioPlayer/IPlayer.cs
index ddafe1e..0133965 100644
--- a/src/AudioPlayer/IPlayer.cs
+++ b/src/AudioPlayer/IPlayer.cs
@@ -1,10 +1,7 @@
-//
-// Licensed under the GNU GPL v3 License. See LICENSE in the project root for license information.
-//
-
-using System;
-using System.IO;
-using MusicSharp.Enums;
+using System;
+using MusicSharp.Data;
+using SoundFlow.Enums;
+using SoundFlow.Structs;
namespace MusicSharp.AudioPlayer;
@@ -14,52 +11,50 @@ namespace MusicSharp.AudioPlayer;
public interface IPlayer: IDisposable
{
///
- /// Gets or sets a value indicating whether the audio player is playing.
+ /// Gets a value indicating whether the audio player is playing.
///
- EPlayerStatus PlayerStatus { get; set; }
+ PlaybackState State { get; }
///
- /// Gets or sets the last file opened by the player.
+ /// Gets the list of audio playback devices.
///
- string LastFileOpened { get; set; }
-
+ DeviceInfo[] PlaybackDevices { get; }
+
///
- /// Method to play audio.
+ /// Returns the total length of the audio file.
///
- /// The audio stream.
- void Play(Stream stream);
+ float TrackLength { get; }
///
- /// Method to play or pause depending on state.
+ /// Returns the current time played.
///
- void PlayPause();
+ float CurrentTime { get; }
///
- /// Method to stop audio playback.
+ /// Returns the currently playing track.
///
- void Stop();
+ AudioFile? NowPlaying { get; set; }
///
- /// Method to increase audio playback volume.
+ /// Changes the current audio playback device.
///
- void IncreaseVolume();
-
+ void ChangePlaybackDevice(DeviceInfo device);
+
///
- /// Method to decrease audio playback volume.
+ /// Method to play and pause audio.
///
- void DecreaseVolume();
-
+ /// The AudioFile.
+ void PlayPause(AudioFile audioFile);
+
///
- /// Returns the current playtime of the audioFileReader instance.
+ /// Method to stop audio playback.
///
- /// The current time played as TimeSpan.
- float CurrentTime();
+ void Stop();
///
- /// Returns the total track length in timespan format.
+ /// Change audio volume
///
- /// The length of the track in timespan format.
- float TrackLength();
+ void ChangeVolume(float amount);
///
/// Skip ahead in the audio file 5s.
@@ -69,5 +64,5 @@ public interface IPlayer: IDisposable
///
/// Skip back in the audio file 5s.
///
- public void SeekBackwards();
+ public void SeekBackward();
}
\ No newline at end of file
diff --git a/src/AudioPlayer/IStreamConverter.cs b/src/AudioPlayer/IStreamConverter.cs
deleted file mode 100644
index eccf77c..0000000
--- a/src/AudioPlayer/IStreamConverter.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-using System.IO;
-using System.Threading.Tasks;
-
-namespace MusicSharp.AudioPlayer;
-
-public interface IStreamConverter
-{
- public Stream ConvertFileToStream(string path);
-
- public Task ConvertUrlToStream(string url);
-}
\ No newline at end of file
diff --git a/src/AudioPlayer/SoundFlowPlayer.cs b/src/AudioPlayer/SoundFlowPlayer.cs
index 141d622..1a26dca 100644
--- a/src/AudioPlayer/SoundFlowPlayer.cs
+++ b/src/AudioPlayer/SoundFlowPlayer.cs
@@ -1,103 +1,146 @@
using System;
using System.IO;
+using System.Linq;
+using MusicSharp.Data;
using MusicSharp.Enums;
+using SoundFlow.Abstracts.Devices;
using SoundFlow.Backends.MiniAudio;
using SoundFlow.Components;
+using SoundFlow.Enums;
+using SoundFlow.Interfaces;
using SoundFlow.Providers;
+using SoundFlow.Structs;
namespace MusicSharp.AudioPlayer;
-// Cross-platform sound engine that works for all devices which
-// the .NET platform runs on.
public sealed class SoundFlowPlayer : IPlayer
{
- private readonly MiniAudioEngine _soundEngine;
- private SoundPlayer _player;
-
- public EPlayerStatus PlayerStatus { get; set; }
- public string LastFileOpened { get; set; }
-
-
- public SoundFlowPlayer(MiniAudioEngine soundEngine)
- {
- _soundEngine = soundEngine;
- }
-
- public void Play(Stream stream)
+ private readonly MiniAudioEngine _audioEngine;
+ private AudioPlaybackDevice _audioPlaybackDevice;
+ private readonly AudioFormat _audioFormat;
+ private ISoundDataProvider? _streamDataProvider;
+ private SoundPlayer? _player;
+ private float _volume;
+
+ // If we don't know the state of the player, default to stopped?
+ public PlaybackState State => _player?.State ?? PlaybackState.Stopped;
+ public float TrackLength => _player?.Duration ?? 0;
+ public float CurrentTime => _player?.Time ?? 0;
+ public AudioFile? NowPlaying { get; set; }
+ public DeviceInfo[] PlaybackDevices => _audioEngine.PlaybackDevices;
+
+ public SoundFlowPlayer(MiniAudioEngine audioEngine)
{
- if (_player != null)
+ _audioEngine = audioEngine;
+
+ var defaultPlaybackDevice = _audioEngine.PlaybackDevices.FirstOrDefault(d => d.IsDefault);
+
+ // Handle case no default playback device is found.
+ if (defaultPlaybackDevice.Id == IntPtr.Zero)
{
- _player.Stop();
}
-
- _player = new SoundPlayer(new StreamDataProvider(stream));
- Mixer.Master.AddComponent(_player);
- _player.Play();
- PlayerStatus = EPlayerStatus.Playing;
+ _audioFormat = AudioFormat.DvdHq;
+ _audioPlaybackDevice = _audioEngine.InitializePlaybackDevice(defaultPlaybackDevice, _audioFormat);
+ _volume = 1f;
}
- public void PlayPause()
+ public void ChangePlaybackDevice(DeviceInfo device)
{
- switch (PlayerStatus)
+ _audioPlaybackDevice = _audioEngine.SwitchDevice(_audioPlaybackDevice, device);
+ }
+
+ public void PlayPause(AudioFile audioFile)
+ {
+ if (_player != null && audioFile.Path.Equals(NowPlaying?.Path))
+ {
+ switch (_player.State)
+ {
+ case PlaybackState.Playing:
+ _player.Pause();
+ break;
+ case PlaybackState.Paused:
+ case PlaybackState.Stopped:
+ _player.Play();
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+
+ if (!audioFile.Path.Equals(NowPlaying?.Path))
{
- case EPlayerStatus.Playing:
- _player.Pause();
- PlayerStatus = EPlayerStatus.Paused;
- break;
- case EPlayerStatus.Paused:
- case EPlayerStatus.Stopped:
+ if (_player != null && _player.State != PlaybackState.Stopped)
+ {
+ _player.Stop();
+ _audioPlaybackDevice.Stop();
+ NowPlaying = null;
+ }
+
+ _streamDataProvider = audioFile.Type switch
+ {
+ EFileType.File => new StreamDataProvider(_audioEngine, _audioFormat, File.OpenRead(audioFile.Path)),
+ EFileType.Stream => new NetworkDataProvider(_audioEngine, _audioFormat, audioFile.Path),
+ _ => _streamDataProvider
+ };
+
+ if (_streamDataProvider != null)
+ {
+ _player = new SoundPlayer(_audioEngine, _audioFormat, _streamDataProvider);
+ _audioPlaybackDevice.MasterMixer.AddComponent(_player);
+ _audioPlaybackDevice.Start();
+ _player.Volume = _volume;
_player.Play();
- PlayerStatus = EPlayerStatus.Playing;
- break;
- default:
- throw new ArgumentOutOfRangeException();
+ NowPlaying = audioFile;
+ }
}
}
public void Stop()
{
- if (PlayerStatus != EPlayerStatus.Stopped)
+ if (_player != null && _player.State != PlaybackState.Stopped)
{
- _player.Stop();
- PlayerStatus = EPlayerStatus.Stopped;
+ _player?.Stop();
+ _audioPlaybackDevice.Stop();
+ NowPlaying = null;
}
}
- public void IncreaseVolume()
+ public void ChangeVolume(float amount)
{
- // Need to verify what SoundFlow's max volume level is
- // For now this should be enough based on testing
- _player.Volume = Math.Clamp(_player.Volume + .1f, 0f, 2f);
- }
-
- public void DecreaseVolume()
- {
- _player.Volume = Math.Clamp(_player.Volume - .1f, 0f, 2f);
+ if (_player != null)
+ {
+ _player.Volume = Math.Clamp(amount, 0f, 2f);
+ _volume = _player.Volume;
+ }
}
public void SeekForward()
{
- _player.Seek(Math.Clamp(_player.Time + 5f, 0f, _player.Duration - 0.1f));
- }
-
- public void SeekBackwards()
- {
- _player.Seek(Math.Clamp(_player.Time - 5f, 0f, _player.Duration));
- }
-
- public float CurrentTime()
- {
- return _player.Time;
+ if (_streamDataProvider is StreamDataProvider && _player != null)
+ {
+ if (_player.State is PlaybackState.Playing or PlaybackState.Paused)
+ {
+ _player.Seek(Math.Clamp(_player.Time + 5f, 0f, _player.Duration - 0.1f));
+ }
+ }
}
- public float TrackLength()
+ public void SeekBackward()
{
- return _player.Duration;
+ if (_streamDataProvider is StreamDataProvider && _player != null)
+ {
+ if (_player.State is PlaybackState.Playing or PlaybackState.Paused)
+ {
+ _player.Seek(Math.Clamp(_player.Time - 5f, 0f, _player.Duration));
+ }
+ }
}
public void Dispose()
{
- _soundEngine.Dispose();
+ _audioPlaybackDevice.Dispose();
+ _streamDataProvider?.Dispose();
+ _player?.Dispose();
}
}
\ No newline at end of file
diff --git a/src/AudioPlayer/SoundFlowPlayerStreamConverter.cs b/src/AudioPlayer/SoundFlowPlayerStreamConverter.cs
deleted file mode 100644
index 7522321..0000000
--- a/src/AudioPlayer/SoundFlowPlayerStreamConverter.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-using System.IO;
-using System.Net.Http;
-using System.Threading.Tasks;
-
-namespace MusicSharp.AudioPlayer;
-
-public class SoundFlowPlayerStreamConverter : IStreamConverter
-{
- private readonly HttpClient _httpClient;
-
- public SoundFlowPlayerStreamConverter(HttpClient httpClient)
- {
- _httpClient = httpClient;
- }
-
- public 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/Data/AudioFile.cs b/src/Data/AudioFile.cs
new file mode 100644
index 0000000..657515e
--- /dev/null
+++ b/src/Data/AudioFile.cs
@@ -0,0 +1,11 @@
+using ATL;
+using MusicSharp.Enums;
+
+namespace MusicSharp.Data;
+
+public class AudioFile(string filepath, EFileType fileType)
+{
+ public Track TrackInfo { get; } = new(filepath);
+ public string Path { get; } = filepath;
+ public EFileType Type { get; } = fileType;
+}
\ No newline at end of file
diff --git a/src/Enums/EFileType.cs b/src/Enums/EFileType.cs
index 1ea3943..9b80434 100644
--- a/src/Enums/EFileType.cs
+++ b/src/Enums/EFileType.cs
@@ -3,5 +3,6 @@ namespace MusicSharp.Enums;
public enum EFileType
{
File,
- Stream
+ Stream,
+ NotLoaded
}
\ No newline at end of file
diff --git a/src/Enums/EPlayerStatus.cs b/src/Enums/EPlayerStatus.cs
deleted file mode 100644
index 0870196..0000000
--- a/src/Enums/EPlayerStatus.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-namespace MusicSharp.Enums;
-
-///
-/// The status of the audio player.
-///
-public enum EPlayerStatus
-{
- Playing,
- Paused,
- Stopped
-}
\ No newline at end of file
diff --git a/src/MusicSharp.csproj b/src/MusicSharp.csproj
index d8bcba9..3eb367c 100644
--- a/src/MusicSharp.csproj
+++ b/src/MusicSharp.csproj
@@ -2,42 +2,28 @@
Exe
- net8.0
- 1.0.0
+ net9.0
+ true
+ true
+ true
+ osx-x64
+ 2.0.0
Mark-James McDougall
Mark-James McDougall
enable
-
- MusicSharp.ico
-
+
+
-
+
+
+
+
+
-
-
-
-
-
-
-
-
-
- True
- True
- Resources.resx
-
-
-
-
-
- ResXFileCodeGenerator
- Resources.Designer.cs
-
-
-
+
diff --git a/src/MusicSharp.ico b/src/MusicSharp.ico
deleted file mode 100644
index dd2843d..0000000
Binary files a/src/MusicSharp.ico and /dev/null differ
diff --git a/src/Playlist/PlaylistHelpers.cs b/src/Playlist/PlaylistHelpers.cs
new file mode 100644
index 0000000..52621e1
--- /dev/null
+++ b/src/Playlist/PlaylistHelpers.cs
@@ -0,0 +1,34 @@
+using System.Collections.Generic;
+using System.Linq;
+using ATL.Playlist;
+using MusicSharp.Data;
+
+namespace MusicSharp.Playlist;
+
+public static class PlaylistHelpers
+{
+ ///
+ /// Load an M3U playlist.
+ ///
+ /// Returns a list of playlist information.
+ /// The user specified playlist path.
+ public static List LoadPlaylist(string playlist)
+ {
+ var theReader = PlaylistIOFactory.GetInstance().GetPlaylistIO(playlist);
+
+ // Fix space formatting as SoundFlow doesn't support encoded spaces
+ return theReader.FilePaths.Select(s => s.Replace("%20", " ")).ToList();
+ }
+
+ public static void SavePlaylistToFile(string playlistFilePath, List filesForPlaylist)
+ {
+ var pls = PlaylistIOFactory.GetInstance().GetPlaylistIO(playlistFilePath);
+
+ foreach (var file in filesForPlaylist)
+ {
+ pls.FilePaths.Add(file.Path);
+ }
+
+ pls.Save();
+ }
+}
\ No newline at end of file
diff --git a/src/PlaylistHandlers/PlaylistLoader.cs b/src/PlaylistHandlers/PlaylistLoader.cs
deleted file mode 100644
index 071989b..0000000
--- a/src/PlaylistHandlers/PlaylistLoader.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-//
-// Licensed under the GNU GPL v3 License. See LICENSE in the project root for license information.
-//
-
-using System.Collections.Generic;
-using System.Linq;
-using ATL.Playlist;
-
-namespace MusicSharp.PlaylistHandlers;
-
-///
-/// The PlaylistLoader class loads a playlist of a given type.
-///
-public static class PlaylistLoader
-{
- // This will be used in the future to allow for playlist types beyond M3U.
- // public virtual void LoadPlaylist() { }
-
- ///
- /// Load an M3U playlist.
- ///
- /// Returns a list of playlist information.
- /// The user specified playlist path.
- public static List LoadPlaylist(string userPlaylist)
- {
- var theReader = PlaylistIOFactory.GetInstance().GetPlaylistIO(userPlaylist);
-
- // 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 b803d31..e477e9a 100644
--- a/src/Program.cs
+++ b/src/Program.cs
@@ -1,31 +1,20 @@
-//
-// Licensed under the GNU GPL v3 License. See LICENSE in the project root for license information.
-//
-
-using System.Net.Http;
-using MusicSharp.UI;
+using MusicSharp.UI;
using MusicSharp.AudioPlayer;
using SoundFlow.Backends.MiniAudio;
-using SoundFlow.Enums;
+using Terminal.Gui.App;
namespace MusicSharp;
-///
-/// Entry Point class.
-///
public static class Program
{
- ///
- /// Entry point.
- ///
public static void Main()
{
- var soundEngine = new MiniAudioEngine(44100, Capability.Playback);
- using IPlayer player = new SoundFlowPlayer(soundEngine);
- using var httpClient = new HttpClient();
- IStreamConverter streamConverter = new SoundFlowPlayerStreamConverter(httpClient);
- var ui = new Tui(player, streamConverter);
-
- ui.Start();
+ using var audioEngine = new MiniAudioEngine();
+ using IPlayer player = new SoundFlowPlayer(audioEngine);
+ using var ui = new Tui(player);
+
+ Application.Init();
+ Application.Run(ui);
+ Application.Shutdown();
}
}
\ No newline at end of file
diff --git a/src/UI/Tui.cs b/src/UI/Tui.cs
index 3d3146a..920f776 100644
--- a/src/UI/Tui.cs
+++ b/src/UI/Tui.cs
@@ -1,397 +1,1008 @@
-//
-// Licensed under the GNU GPL v3 License. See LICENSE in the project root for license information.
-//
-
using System;
+using System.Collections;
using System.Collections.Generic;
-using System.IO;
-using MusicSharp.Enums;
-using MusicSharp.PlaylistHandlers;
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+using System.Linq;
+using System.Text;
using MusicSharp.AudioPlayer;
-using Terminal.Gui;
+using MusicSharp.Data;
+using MusicSharp.Enums;
+using MusicSharp.Playlist;
+using SoundFlow.Enums;
+using SoundFlow.Structs;
+using Terminal.Gui.App;
+using Terminal.Gui.Drawing;
+using Terminal.Gui.Input;
+using Terminal.Gui.Text;
+using Terminal.Gui.ViewBase;
+using Terminal.Gui.Views;
+using Attribute = Terminal.Gui.Drawing.Attribute;
namespace MusicSharp.UI;
-///
-/// The Gui class houses the CLI elements of MusicSharp.
-///
-public class Tui
+public class Tui : Toplevel
{
- private static List _playlistTracks;
- private static ListView _playlistView;
- private static FrameView _playlistPane;
- private static FrameView _playbackControls;
- private static FrameView _nowPlaying;
- private static StatusBar _statusBar;
- private static Label _trackName;
-
- ///
- /// Create a new instance of the audio player engine.
- ///
private readonly IPlayer _player;
-
- private object _mainLoopTimeout = null;
-
- private List _playlist = new List();
-
- private readonly IStreamConverter _streamConverter;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The player to be injected.
- /// Helper class to convert files and urls to Stream type.
- public Tui(IPlayer player, IStreamConverter streamConverter)
+ private readonly ProgressBar _progressBar;
+ private readonly Label _nowPlayingLabel;
+ private readonly Label _timePlayedLabel;
+ private readonly Button _playPauseButton;
+ private readonly ListView _playlistView;
+ private readonly ObservableCollection? _loadedPlaylist = [];
+ private int _playlistIndex;
+ private object? _mainLoopTimeout;
+
+ private const uint MainLoopTimeoutTick = 100; // ms
+
+ public Tui(IPlayer player)
{
_player = player;
- _streamConverter = streamConverter;
- }
-
- ///
- /// Gets and sets the current audio file play progress.
- ///
- internal ProgressBar AudioProgressBar { get; private set; }
- ///
- /// Start the UI.
- ///
- public void Start()
- {
- // Creates an instance of MainLoop to process input events, handle timers and other sources of data.
- Application.Init();
+ #region Menus
- var top = Application.Top;
- var tframe = top.Frame;
-
- // Create the menubar.
- var menu = new MenuBar(new MenuBarItem[]
+ var menuBar = new MenuBarv2()
{
- new MenuBarItem("_File", new MenuItem[]
+ Title = "MusicSharp",
+ BorderStyle = LineStyle.Rounded,
+ Menus =
+ [
+ new MenuBarItemv2(
+ Title = "_File",
+ new MenuItemv2[]
+ {
+ new("_Open file", "Open audio file", OpenFile, Key.F1),
+ new("Open _stream", "Open a stream URL", OpenStream, Key.F2),
+ new("_Quit", "Quit MusicSharp", RequestStop, Key.Esc)
+ }
+ ),
+ new MenuBarItemv2(
+ Title = "_Playback",
+ new MenuItemv2[]
+ {
+ new("Seek backward", string.Empty, _player.SeekBackward, Key.N.WithCtrl),
+ new("Seek forward", string.Empty, _player.SeekForward, Key.M.WithCtrl),
+ new("Previous", string.Empty, SkipBackward, Key.CursorLeft.WithShift),
+ new("Next", string.Empty, SkipForward, Key.CursorRight.WithShift),
+ }
+ ),
+ new MenuBarItemv2(
+ Title = "Playlist",
+ new MenuItemv2[]
+ {
+ new("_Add to playlist", "Add track(s) to playlist", AddToPlaylist, Key.A.WithCtrl),
+ new("_Remove from playlist", "Remove selected track from playlist", RemoveFromPlaylist,
+ Key.R.WithCtrl),
+ new("Load _playlist", "Load a playlist", OpenPlaylist, Key.L.WithCtrl),
+ new("_Save playlist", "Save to playlist", SavePlaylist, Key.S.WithCtrl)
+ }
+ ),
+ new MenuBarItemv2(
+ Title = "Settings",
+ new MenuItemv2[]
+ {
+ new("Audio device", "Select playback device", SelectAudioDevice, Key.D.WithCtrl),
+ }
+ ),
+ new MenuBarItemv2(
+ Title = "Help",
+ new MenuItemv2[]
+ {
+ new("_About...", "About MusicSharp", () => MessageBox.Query(
+ "",
+ GetAboutMessage(),
+ wrapMessage: false,
+ buttons: "_Ok"
+ ),
+ Key.I.WithCtrl
+ )
+ }
+ ),
+ ]
+ };
+
+ var statusBar = new StatusBar([
+ new Shortcut
+ {
+ Text = "Open file",
+ Key = Key.F1,
+ Action = OpenFile
+ },
+ new Shortcut
+ {
+ Text = "Open stream",
+ Key = Key.F2,
+ Action = OpenStream
+ },
+ new Shortcut
{
- new MenuItem("_Open", "Open a music file", () => OpenFile()),
+ Text = "Load playlist",
+ Key = Key.L.WithCtrl,
+ Action = OpenPlaylist
+ },
+ new Shortcut
+ {
+ Text = "Quit",
+ Key = Key.Esc,
+ Action = RequestStop
+ },
+ ]);
- new MenuItem("Open S_tream", "Open a music stream", () => OpenStream()),
+ #endregion Menus
- new MenuItem("Open Pla_ylist", "Load a playlist", () => LoadPlaylist()),
+ _playlistView = new ListView
+ {
+ Title = "Playlist",
+ X = 0,
+ Y = Pos.Bottom(menuBar),
+ Width = Dim.Fill(),
+ Height = Dim.Fill(11),
+ CanFocus = true,
+ BorderStyle = LineStyle.Rounded,
+ Source = new TrackListDataSource(_loadedPlaylist),
+ AllowsMarking = false,
+ AllowsMultipleSelection = false
+ };
+ _playlistView.RowRender += PlaylistView_RowRender;
+ _playlistView.VerticalScrollBar.AutoShow = true;
+ _playlistView.HorizontalScrollBar.AutoShow = true;
+ _playlistView.OpenSelectedItem += (sender, args) =>
+ {
+ if (args.Value != null)
+ {
+ _playlistIndex = args.Item;
+ PlayHandler((AudioFile)args.Value);
+ }
+ };
- new MenuItem("_Quit", "Exit MusicSharp", () => Application.RequestStop()),
- }),
+ _progressBar = new ProgressBar()
+ {
+ Title = "Progress",
+ X = 0,
+ Y = Pos.Bottom(_playlistView),
+ Width = Dim.Fill(),
+ Height = 3,
+ CanFocus = false,
+ BorderStyle = LineStyle.Rounded,
+ ProgressBarStyle = ProgressBarStyle.Continuous,
+ Fraction = 0f,
+ };
- new MenuBarItem("_Help", new MenuItem[]
- {
- new MenuItem("_About MusicSharp", string.Empty, () =>
- {
- 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");
- }),
- }),
- });
+ #region PlayBackControls
- _statusBar = new StatusBar(new StatusItem[]
+ var playbackControls = new View()
{
- new StatusItem(Key.F1, "~F1~ Open file", () => OpenFile()),
- new StatusItem(Key.F2, "~F2~ Open stream", () => OpenStream()),
- new StatusItem(Key.F3, "~F3~ Load playlist", () => LoadPlaylist()),
- new StatusItem(Key.F4, "~F4~ Quit", () => Application.RequestStop()),
- new StatusItem(Key.Space, "~Space~ Play/Pause", () => PlayPause()),
- });
+ Title = "Playback",
+ X = 0,
+ Y = Pos.Bottom(_progressBar),
+ Width = Dim.Auto(),
+ Height = Dim.Auto(),
+ CanFocus = true,
+ BorderStyle = LineStyle.Rounded
+ };
- // Create the playback controls frame.
- _playbackControls = new FrameView("Playback")
+ var seekBackwardButton = new Button
{
X = 0,
- Y = 24,
- Width = 55,
- Height = 5,
+ Y = 0,
CanFocus = true,
+ Text = "<<"
+ };
+ seekBackwardButton.Accepting += (s, e) =>
+ {
+ _player.SeekBackward();
+ e.Handled = true;
};
- var playPauseButton = new Button(1, 1, "Play/Pause");
- playPauseButton.Clicked += () =>
+ _playPauseButton = new Button
+ {
+ X = Pos.Right(seekBackwardButton),
+ Y = 0,
+ CanFocus = true,
+ Text = "▶"
+ };
+ _playPauseButton.Accepting += (s, e) =>
{
- PlayPause();
+ var selected = _playlistView.SelectedItem;
+ _playlistIndex = selected;
+ var selectedTrack = _loadedPlaylist?.ElementAtOrDefault(selected);
+
+ if (selectedTrack != null)
+ {
+ PlayHandler(selectedTrack);
+ }
- if (_player.PlayerStatus != EPlayerStatus.Stopped)
+ if (selectedTrack == null && _player.NowPlaying != null)
{
- UpdateProgressBar();
+ PlayHandler(_player.NowPlaying);
}
+
+ e.Handled = true;
};
- var stopButton = new Button(16, 1, "Stop");
- stopButton.Clicked += () =>
+ var seekForwardButton = new Button
{
- _player.Stop();
- AudioProgressBar.Fraction = 0F;
- TimePlayedLabel();
+ X = Pos.Right(_playPauseButton),
+ Y = 0,
+ CanFocus = true,
+ Text = ">>"
};
-
- var seekForward = new Button(26, 0, "Seek 5s");
- seekForward.Clicked += () =>
+ seekForwardButton.Accepting += (s, e) =>
{
_player.SeekForward();
+ e.Handled = true;
};
- var seekBackward = new Button(26, 2, "Seek -5s");
- seekBackward.Clicked += () =>
+ var stopButton = new Button
{
- _player.SeekBackwards();
+ X = Pos.Right(seekForwardButton),
+ Y = 0,
+ CanFocus = true,
+ Text = "⏹︎",
};
-
- var increaseVolumeButton = new Button(39, 0, "+ Volume");
- increaseVolumeButton.Clicked += () =>
+ stopButton.Accepting += (s, e) =>
{
- _player.IncreaseVolume();
+ _player.Stop();
+ _progressBar.Fraction = 0;
+ TimePlayedLabel();
+ _nowPlayingLabel!.Text = string.Empty;
+ _playPauseButton.Text = "▶";
+ e.Handled = true;
};
- var decreaseVolumeButton = new Button(39, 2, "- Volume");
- decreaseVolumeButton.Clicked += () =>
+ // Verify what SoundFlow's max volume level is
+ // For now this should be enough based on testing
+ List