update backends for new IAudioBackend

This commit is contained in:
Royce551 2021-08-22 07:56:02 -05:00
parent 40148a8bde
commit 27c441f0d7
12 changed files with 139 additions and 450 deletions

View file

@ -4,13 +4,14 @@ using System.Composition.Hosting;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Threading.Tasks;
namespace FRESHMusicPlayer.Backends
{
static class AudioBackendFactory
{
private static ContainerConfiguration config = new ContainerConfiguration();
private static CompositionHost container;
private readonly static ContainerConfiguration config = new ContainerConfiguration();
private readonly static CompositionHost container;
private static IEnumerable<Assembly> LoadAssemblies(IEnumerable<string> paths)
{
@ -37,26 +38,10 @@ namespace FRESHMusicPlayer.Backends
private static void AddDirectory(string path)
{
//AppDomain.CurrentDomain.AppendPrivatePath(path);
try
{
if (!Directory.Exists(path)) Directory.CreateDirectory(path);
config.WithAssemblies(LoadAssemblies(Directory.GetFiles(path, "*.dll")));
}
catch (DirectoryNotFoundException)
{
try
{
Directory.CreateDirectory(path);
}
catch
{
}
}
catch (Exception ex)
{
Trace.WriteLine(ex);
}
}
static AudioBackendFactory()
{
@ -66,31 +51,18 @@ namespace FRESHMusicPlayer.Backends
container = config.CreateContainer();
}
public static IAudioBackend CreateBackend(string filename)
public static async Task<IAudioBackend> CreateBackendAsync(string filename)
{
var exlist = new List<Exception>();
var problems = new List<BackendLoadResult>();
foreach (var lazybackend in container.GetExports<Lazy<IAudioBackend>>())
{
IAudioBackend backend = null;
try
{
backend = lazybackend.Value;
backend.LoadSong(filename);
return backend;
IAudioBackend backend = lazybackend.Value;
var result = await backend.LoadSongAsync(filename);
if (result != BackendLoadResult.OK) problems.Add(result);
else return backend;
}
catch (Exception ex)
{
try
{
backend.Dispose();
}
catch
{
}
exlist.Add(ex);
}
}
throw new Exception($"No backend could be found to play {filename}.\n\n{String.Join("\n\n", exlist)}\n");
throw new Exception($"A backend couldn't be found to load this file\n{string.Join("\n", problems)}");
}
}
}

View file

@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace FRESHMusicPlayer.Backends
{
@ -29,6 +30,9 @@ namespace FRESHMusicPlayer.Backends
public Track ATLTrack { get; set; }
public FileMetadataProvider(string path) => ATLTrack = new Track(path);
private string path;
public FileMetadataProvider(string path) => this.path = path;
public async Task LoadAsync() => await Task.Run(() => ATLTrack = new Track(path));
}
}

View file

@ -34,6 +34,8 @@ namespace FRESHMusicPlayer.Backends
int TrackTotal { get; }
int DiscNumber { get; }
int DiscTotal { get; }
Task LoadAsync();
}
public enum BackendLoadResult
@ -41,6 +43,7 @@ namespace FRESHMusicPlayer.Backends
OK,
NotSupported,
Invalid,
Corrupt
Corrupt,
UnknownError
}
}

View file

@ -46,8 +46,6 @@ namespace FRESHMusicPlayer.Backends
public async Task<BackendLoadResult> LoadSongAsync(string file)
{
if (!File.Exists(file)) return BackendLoadResult.Invalid;
if (AudioFile != null) AudioFile.Dispose();
try
{
@ -75,6 +73,11 @@ namespace FRESHMusicPlayer.Backends
{
return BackendLoadResult.Invalid;
}
catch (Exception e)
{
Console.WriteLine(e);
return BackendLoadResult.UnknownError;
}
return BackendLoadResult.OK;
}

View file

@ -6,6 +6,7 @@ using System.Reflection;
using FRESHMusicPlayer.Handlers;
using FRESHMusicPlayer.Utilities;
using FRESHMusicPlayer.Backends;
using System.Threading.Tasks;
namespace FRESHMusicPlayer
{
@ -49,7 +50,7 @@ namespace FRESHMusicPlayer
/// </summary>
public string FilePath { get; private set; } = string.Empty;
/// <summary>
/// Whether the audio backend and file has been loaded and things are ready to go. If you interact with the Player everything
/// Whether the audio backend and file has been loaded and things are ready to go. If you interact with the Player when this is false everything
/// will explode.
/// </summary>
public bool FileLoaded { get; set; }
@ -78,18 +79,18 @@ namespace FRESHMusicPlayer
/// <summary>
/// Skips to the previous track in the Queue. If there are no tracks for the player to go back to, nothing will happen.
/// </summary>
public void PreviousSong()
public async Task PreviousAsync()
{
if (Queue.Position <= 1) return;
Queue.Position -= 2;
PlayMusic();
await PlayAsync();
}
/// <summary>
/// Skips to the next track in the Queue. If there are no more tracks, the player will stop.
/// </summary>
/// <param name="avoidNext">Intended to be used only by the player</param>
public void NextSong(bool avoidNext = false)
public async Task NextAsync(bool avoidNext = false)
{
AvoidNextQueue = avoidNext;
if (Queue.RepeatMode == RepeatMode.RepeatOne) Queue.Position--; // Don't advance Queue, play the same thing again
@ -99,21 +100,20 @@ namespace FRESHMusicPlayer
if (Queue.RepeatMode == RepeatMode.RepeatAll) // Go back to the first track and play it again
{
Queue.Position = 0;
PlayMusic();
await PlayAsync();
return;
}
Queue.Clear();
StopMusic();
Stop();
return;
}
PlayMusic();
await PlayAsync();
}
// Music Playing Controls
private void OnPlaybackStopped(object sender, EventArgs args)
private async void OnPlaybackStopped(object sender, EventArgs args)
{
if (!AvoidNextQueue)
NextSong();
await NextAsync();
else
AvoidNextQueue = false;
}
@ -131,23 +131,23 @@ namespace FRESHMusicPlayer
/// Plays a track. This is equivalent to calling Queue.Add() and then PlayMusic()./>
/// </summary>
/// <param name="path">The track to play</param>
public void PlayMusic(string path)
public async Task PlayMusicAsync(string path)
{
Queue.Add(path);
PlayMusic();
await PlayAsync();
}
/// <summary>
/// Starts playing the Queue. In order to play a track, you must first add it to the Queue using <see cref="AddQueue(string)"/>.
/// </summary>
/// <param name="repeat">If true, avoids dequeuing the next track. Not to be used for anything other than the player.</param>
public void PlayMusic(bool repeat = false)
public async Task PlayAsync(bool repeat = false)
{
if (!repeat && Queue.Queue.Count != 0)
FilePath = Queue.Queue[Queue.Position];
Queue.Position++;
void PMusic()
async Task PMusic()
{
CurrentBackend = AudioBackendFactory.CreateBackend(FilePath);
CurrentBackend = await AudioBackendFactory.CreateBackendAsync(FilePath);
CurrentBackend.Play();
CurrentBackend.Volume = Volume;
@ -160,43 +160,18 @@ namespace FRESHMusicPlayer
{
if (FileLoaded != true)
{
PMusic();
await PMusic();
}
else
{
AvoidNextQueue = true;
StopMusic();
PMusic();
Stop();
await PMusic();
}
SongChanged?.Invoke(null,
EventArgs.Empty); // Now that playback has started without any issues, fire the song changed event.
}
//catch (FileNotFoundException) // TODO: move these to NAudioBackend
//{
// var args = new PlaybackExceptionEventArgs {Details = "That's not a valid file path!"};
// SongException?.Invoke(null, args);
//}
//catch (ArgumentException)
//{
// var args = new PlaybackExceptionEventArgs {Details = "That's not a valid file path!"};
// SongException?.Invoke(null, args);
//}
//catch (System.Runtime.InteropServices.COMException)
//{
// var args = new PlaybackExceptionEventArgs {Details = "This isn't a valid audio file!"};
// SongException?.Invoke(null, args);
//}
//catch (FormatException)
//{
// var args = new PlaybackExceptionEventArgs {Details = "This audio file might be corrupt!"};
// SongException?.Invoke(null, args);
//}
//catch (InvalidOperationException)
//{
// var args = new PlaybackExceptionEventArgs {Details = "This audio file uses VBR \nor might be corrupt!"};
// SongException?.Invoke(null, args);
//}
catch (Exception e)
{
var args = new PlaybackExceptionEventArgs(e, $"{e.Message}\n{e.StackTrace}");
@ -207,7 +182,7 @@ namespace FRESHMusicPlayer
/// <summary>
/// Completely stops and disposes the player and resets all playback related variables to their defaults.
/// </summary>
public void StopMusic()
public void Stop()
{
if (!FileLoaded) return;
@ -222,7 +197,7 @@ namespace FRESHMusicPlayer
/// <summary>
/// Pauses playback without disposing. Can later be resumed with <see cref="ResumeMusic()"/>.
/// </summary>
public void PauseMusic()
public void Pause()
{
if (!Paused)
CurrentBackend?.Pause();
@ -232,7 +207,7 @@ namespace FRESHMusicPlayer
/// <summary>
/// Resumes playback.
/// </summary>
public void ResumeMusic()
public void Resume()
{
if (Paused)
CurrentBackend?.Play();
@ -240,41 +215,5 @@ namespace FRESHMusicPlayer
}
#endregion
// Integration
//#region DiscordRPC // TODO: move this to the frontend
///// <summary>
///// Initializes the Discord RPC client. Once it has been initialized, you can set the presence by using <see cref="UpdateRPC(string, string, string)"/>
///// </summary>
///// <param name="applicationID">The application ID of your app</param>
//public void InitDiscordRPC(string applicationID)
//{ // FMP application ID - 656678380283887626
// Client = new DiscordRpcClient(applicationID);
// Client.OnReady += (sender, e) => { Console.WriteLine("Received Ready from user {0}", e.User.Username); };
// Client.OnPresenceUpdate += (sender, e) => { Console.WriteLine("Received Update! {0}", e.Presence); };
// Client.Initialize();
//}
//public void UpdateRPC(string Activity, string Artist = null, string Title = null)
//{
// Client?.SetPresence(new RichPresence()
// {
// Details = PlayerUtils.TruncateBytes(Title, 120),
// State = PlayerUtils.TruncateBytes(Artist, 120),
// Assets = new Assets()
// {
// LargeImageKey = "icon",
// SmallImageKey = Activity
// },
// Timestamps = Timestamps.Now
// }
// );
//}
//public void DisposeRPC() => Client?.Dispose();
//#endregion
}
}

View file

@ -32,5 +32,5 @@ using System.Runtime.InteropServices;
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("3.0.0.0")]
[assembly: AssemblyFileVersion("3.0.0.0")]
[assembly: AssemblyVersion("4.0.0.0")]
[assembly: AssemblyFileVersion("4.0.0.0")]

View file

@ -1,291 +0,0 @@
using ManagedBass;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace FmpBassBackend
{
/// <summary>
/// A modified version of ManagedBass's MediaPlayer that loads songs synchronously
/// (FMP.Core currently does not support async loading)
/// </summary>
public class FMPMediaPlayer : INotifyPropertyChanged, IDisposable
{
#region Fields
readonly SynchronizationContext _syncContext;
int _handle;
/// <summary>
/// Channel Handle of the loaded audio file.
/// </summary>
protected internal int Handle
{
get => _handle;
private set
{
if (!Bass.ChannelGetInfo(value, out var info))
throw new ArgumentException("Invalid Channel Handle: " + value);
_handle = value;
// Init Events
Bass.ChannelSetSync(Handle, SyncFlags.Free, 0, GetSyncProcedure(() => Disposed?.Invoke(this, EventArgs.Empty)));
Bass.ChannelSetSync(Handle, SyncFlags.Stop, 0, GetSyncProcedure(() => MediaFailed?.Invoke(this, EventArgs.Empty)));
Bass.ChannelSetSync(Handle, SyncFlags.End, 0, GetSyncProcedure(() =>
{
try
{
if (!Bass.ChannelHasFlag(Handle, BassFlags.Loop))
MediaEnded?.Invoke(this, EventArgs.Empty);
}
finally { OnStateChanged(); }
}));
}
}
bool _restartOnNextPlayback;
#endregion
SyncProcedure GetSyncProcedure(Action Handler)
{
return (SyncHandle, Channel, Data, User) =>
{
if (Handler == null)
return;
if (_syncContext == null)
Handler();
else _syncContext.Post(S => Handler(), null);
};
}
static FMPMediaPlayer()
{
var currentDev = Bass.CurrentDevice;
if (currentDev == -1 || !Bass.GetDeviceInfo(Bass.CurrentDevice).IsInitialized)
Bass.Init(currentDev);
}
/// <summary>
/// Creates a new instance of <see cref="MediaPlayer"/>.
/// </summary>
public FMPMediaPlayer() { _syncContext = SynchronizationContext.Current; }
#region Events
/// <summary>
/// Fired when this Channel is Disposed.
/// </summary>
public event EventHandler Disposed;
/// <summary>
/// Fired when the Media Playback Ends
/// </summary>
public event EventHandler MediaEnded;
/// <summary>
/// Fired when the Playback fails
/// </summary>
public event EventHandler MediaFailed;
#endregion
#region Device
int _dev = -1;
/// <summary>
/// Gets or Sets the Playback Device used.
/// </summary>
public int Device
{
get => (_dev = _dev == -1 ? Bass.ChannelGetDevice(Handle) : _dev);
set
{
if (!Bass.GetDeviceInfo(value).IsInitialized)
if (!Bass.Init(value))
return;
if (!Bass.ChannelSetDevice(Handle, value))
return;
_dev = value;
OnPropertyChanged();
}
}
#endregion
#region Volume
double _vol = 0.5;
/// <summary>
/// Gets or Sets the Playback Volume.
/// </summary>
public double Volume
{
get => _vol;
set
{
if (!Bass.ChannelSetAttribute(Handle, ChannelAttribute.Volume, value))
return;
_vol = value;
OnPropertyChanged();
}
}
#endregion
/// <summary>
/// Override this method for custom loading procedure.
/// </summary>
/// <param name="FileName">Path to the File to Load.</param>
/// <returns><see langword="true"/> on Success, <see langword="false"/> on failure</returns>
protected virtual int OnLoad(string FileName) => Bass.CreateStream(FileName);
/// <summary>
/// Gets the Playback State of the Channel.
/// </summary>
public PlaybackState State => Handle == 0 ? PlaybackState.Stopped : Bass.ChannelIsActive(Handle);
#region Playback
/// <summary>
/// Starts the Channel Playback.
/// </summary>
public bool Play()
{
try
{
var result = Bass.ChannelPlay(Handle, _restartOnNextPlayback);
if (result)
_restartOnNextPlayback = false;
return result;
}
finally { OnStateChanged(); }
}
/// <summary>
/// Pauses the Channel Playback.
/// </summary>
public bool Pause()
{
try { return Bass.ChannelPause(Handle); }
finally { OnStateChanged(); }
}
/// <summary>
/// Stops the Channel Playback.
/// </summary>
/// <remarks>Difference from <see cref="Bass.ChannelStop"/>: Playback is restarted when <see cref="Play"/> is called.</remarks>
public bool Stop()
{
try
{
_restartOnNextPlayback = true;
return Bass.ChannelStop(Handle);
}
finally { OnStateChanged(); }
}
#endregion
/// <summary>
/// Gets the Playback Duration.
/// </summary>
public TimeSpan Duration => TimeSpan.FromSeconds(Bass.ChannelBytes2Seconds(Handle, Bass.ChannelGetLength(Handle)));
/// <summary>
/// Gets or Sets the Playback Position.
/// </summary>
public TimeSpan Position
{
get => TimeSpan.FromSeconds(Bass.ChannelBytes2Seconds(Handle, Bass.ChannelGetPosition(Handle)));
set => Bass.ChannelSetPosition(Handle, Bass.ChannelSeconds2Bytes(Handle, value.TotalSeconds));
}
/// <summary>
/// Loads a file into the player.
/// </summary>
/// <param name="FileName">Path to the file to Load.</param>
/// <returns><see langword="true"/> on succes, <see langword="false"/> on failure.</returns>
public bool Load(string FileName)
{
try
{
if (Handle != 0)
Bass.StreamFree(Handle);
}
catch { }
if (_dev != -1)
Bass.CurrentDevice = _dev;
var currentDev = Bass.CurrentDevice;
if (currentDev == -1 || !Bass.GetDeviceInfo(Bass.CurrentDevice).IsInitialized)
Bass.Init(currentDev);
var h = OnLoad(FileName);
if (h == 0)
return false;
Handle = h;
InitProperties();
MediaLoaded?.Invoke(h);
OnPropertyChanged("");
return true;
}
/// <summary>
/// Fired when a Media is Loaded.
/// </summary>
public event Action<int> MediaLoaded;
/// <summary>
/// Frees all resources used by the player.
/// </summary>
public virtual void Dispose()
{
try
{
if (Bass.StreamFree(Handle))
_handle = 0;
}
finally { OnStateChanged(); }
}
/// <summary>
/// Initializes Properties on every call to <see cref="LoadAsync"/>.
/// </summary>
protected virtual void InitProperties()
{
Volume = _vol;
}
void OnStateChanged() => OnPropertyChanged(nameof(State));
/// <summary>
/// Fired when a property value changes.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Fires the <see cref="PropertyChanged"/> event.
/// </summary>
protected virtual void OnPropertyChanged([CallerMemberName] string PropertyName = null)
{
Action f = () => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(PropertyName));
if (_syncContext == null)
f();
else _syncContext.Post(S => f(), null);
}
}
}

View file

@ -6,6 +6,7 @@ using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
namespace FmpBassBackend
{
@ -20,7 +21,9 @@ namespace FmpBassBackend
public event EventHandler<EventArgs> OnPlaybackStopped;
private readonly FMPMediaPlayer player = new FMPMediaPlayer();
public IMetadataProvider Metadata { get; private set; }
private readonly MediaPlayer player = new MediaPlayer();
public FmpBassBackend()
{
@ -28,7 +31,7 @@ namespace FmpBassBackend
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) // on windows media foundation already provides flac support,
{ // don't bother
var currentDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
Bass.PluginLoad(Path.Combine(currentDirectory, GetExtensionForCurrentPlatform("libbassflac")));
Bass.PluginLoad(Path.Combine(currentDirectory, GetExtensionForCurrentPlatform("bassflac")));
}
}
@ -36,9 +39,14 @@ namespace FmpBassBackend
public void Dispose() => player.Dispose();
public void LoadSong(string file)
public async Task<BackendLoadResult> LoadSongAsync(string file)
{
if (!player.Load(file)) throw new Exception("loading didn't work :("); // not awaited because fmpcore currently does not support await like this
var wasSuccessful = await player.LoadAsync(file);
Metadata = new FileMetadataProvider(file);
if (!wasSuccessful) return BackendLoadResult.Invalid;
else return BackendLoadResult.OK;
}
public void Pause() => player.Pause();
@ -47,8 +55,8 @@ namespace FmpBassBackend
private string GetExtensionForCurrentPlatform(string name)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) return $"{name}.so";
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) return $"{name}.dylib";
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) return $"lib{name}.so";
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) return $"lib{name}.dylib";
else return $"{name}.dll";
}
}

View file

@ -0,0 +1,41 @@
using CDLib;
using FRESHMusicPlayer.Backends;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace FmpCdLibBackend
{
public class CDLibMetadataProvider : IMetadataProvider
{
public string Title => track.Title;
public string[] Artists => new string[] { track.Artist };
public string Album => track.AlbumTitle;
public byte[] CoverArt => null;
public string[] Genres => null;
public int Year => 0;
public int TrackNumber => (int)track.TrackNumber;
public int TrackTotal => 0;
public int DiscNumber => 0;
public int DiscTotal => 0;
private readonly IAudioCDTrack track;
public CDLibMetadataProvider(IAudioCDTrack track) => this.track = track;
public Task LoadAsync()
{
return Task.CompletedTask;
}
}
}

View file

@ -22,6 +22,8 @@ namespace FmpCdLibBackend
public float Volume { get => (float)player.Volume; set => player.Volume = value; }
public IMetadataProvider Metadata { get; private set; }
public event EventHandler<EventArgs> OnPlaybackStopped;
public FmpCdLibBackend()
@ -46,9 +48,13 @@ namespace FmpCdLibBackend
//player.Dispose();
}
public void LoadSong(string file)
public async Task<BackendLoadResult> LoadSongAsync(string file)
{
if (Path.GetExtension(file).ToUpper() != ".CDA") return BackendLoadResult.NotSupported;
var result = BackendLoadResult.Invalid;
await Task.Run(() =>
{
if (Path.GetExtension(file).ToUpper() != ".CDA") throw new Exception("Not a CD");
// super hacky; assumes that the path is something like D:\Track01.cda, might be a better way to do this
var driveLetter = char.Parse(file.Substring(0, 1));
var trackNumber = int.Parse(file.Substring(8, 2));
@ -61,9 +67,12 @@ namespace FmpCdLibBackend
var trackToPlay = drive.InsertedMedia.Tracks[trackNumber - 1];
TotalTime = trackToPlay.Duration;
player.PlayTrack(trackToPlay);
return;
Metadata = new CDLibMetadataProvider(trackToPlay);
result = BackendLoadResult.OK;
}
}
});
return result;
}
public void Pause()

View file

@ -45,6 +45,7 @@
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="CDLibMetadataProvider.cs" />
<Compile Include="FmpCdLibBackend.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>

View file

@ -48,22 +48,22 @@ namespace WinformsTest
private void button1_Click(object sender, EventArgs e) // pause/resume
{
if (player.Paused) player.ResumeMusic();
else player.PauseMusic();
if (player.Paused) player.Resume();
else player.Pause();
}
private void button2_Click(object sender, EventArgs e) // stop
{
player.StopMusic();
player.Stop();
}
private void button3_Click(object sender, EventArgs e) // play
private async void button3_Click(object sender, EventArgs e) // play
{
var openFileDialog1 = new OpenFileDialog();
if (openFileDialog1.ShowDialog() != DialogResult.OK) return;
player.Queue.Add(openFileDialog1.FileName);
player.PlayMusic();
player.Volume = 0.7f;
await player.PlayAsync();
player.Volume = 0.5f;
}
private void FreshMusicPlayer_Load(object sender, EventArgs e)
@ -71,14 +71,14 @@ namespace WinformsTest
}
private void button4_Click(object sender, EventArgs e) // next
private async void button4_Click(object sender, EventArgs e) // next
{
player.NextSong();
await player.NextAsync();
}
private void button5_Click(object sender, EventArgs e) // previous
private async void button5_Click(object sender, EventArgs e) // previous
{
player.PreviousSong();
await player.PreviousAsync();
}
private void button6_Click(object sender, EventArgs e) // extra button 1