Added audio player logic and tests.
This commit is contained in:
410
Harmonia.Core/Engine/BaseMediaPlayer.cs
Normal file
410
Harmonia.Core/Engine/BaseMediaPlayer.cs
Normal file
@@ -0,0 +1,410 @@
|
||||
using ManagedBass;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Harmonia.Core.Engine;
|
||||
|
||||
/// <summary>
|
||||
/// A Reusable Channel which can Load files like a Player.
|
||||
/// <para><see cref="MediaPlayer"/> is perfect for UIs, as it implements <see cref="INotifyPropertyChanged"/>.</para>
|
||||
/// <para>Also, unlike normal, Properties/Effects set on a <see cref="MediaPlayer"/> persist through subsequent loads.</para>
|
||||
/// </summary>
|
||||
public class BaseMediaPlayer : 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 BaseMediaPlayer()
|
||||
{
|
||||
var currentDev = Bass.CurrentDevice;
|
||||
|
||||
Bass.Configure(Configuration.IncludeDefaultDevice, true);
|
||||
|
||||
if (currentDev == -1 || !Bass.GetDeviceInfo(Bass.CurrentDevice).IsInitialized)
|
||||
Bass.Init(currentDev);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of <see cref="MediaPlayer"/>.
|
||||
/// </summary>
|
||||
public BaseMediaPlayer() { _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 Frequency
|
||||
double _freq = 44100;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets the Playback Frequency in Hertz.
|
||||
/// Default is 44100 Hz.
|
||||
/// </summary>
|
||||
public double Frequency
|
||||
{
|
||||
get => _freq;
|
||||
set
|
||||
{
|
||||
if (!Bass.ChannelSetAttribute(Handle, ChannelAttribute.Frequency, value))
|
||||
return;
|
||||
|
||||
_freq = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Balance
|
||||
double _pan;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets Balance (Panning) (-1 ... 0 ... 1).
|
||||
/// -1 Represents Completely Left.
|
||||
/// 1 Represents Completely Right.
|
||||
/// Default is 0.
|
||||
/// </summary>
|
||||
public double Balance
|
||||
{
|
||||
get => _pan;
|
||||
set
|
||||
{
|
||||
if (!Bass.ChannelSetAttribute(Handle, ChannelAttribute.Pan, value))
|
||||
return;
|
||||
|
||||
_pan = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
#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
|
||||
|
||||
#region Loop
|
||||
bool _loop;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets whether the Playback is looped.
|
||||
/// </summary>
|
||||
public bool Loop
|
||||
{
|
||||
get => _loop;
|
||||
set
|
||||
{
|
||||
if (value ? !Bass.ChannelAddFlag(Handle, BassFlags.Loop) : !Bass.ChannelRemoveFlag(Handle, BassFlags.Loop))
|
||||
return;
|
||||
|
||||
_loop = 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);
|
||||
|
||||
#region Tags
|
||||
string _title = "", _artist = "", _album = "";
|
||||
|
||||
/// <summary>
|
||||
/// Title of the Loaded Media.
|
||||
/// </summary>
|
||||
public string Title
|
||||
{
|
||||
get => _title;
|
||||
private set
|
||||
{
|
||||
_title = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Artist of the Loaded Media.
|
||||
/// </summary>
|
||||
public string Artist
|
||||
{
|
||||
get => _artist;
|
||||
private set
|
||||
{
|
||||
_artist = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Album of the Loaded Media.
|
||||
/// </summary>
|
||||
public string Album
|
||||
{
|
||||
get => _album;
|
||||
private set
|
||||
{
|
||||
_album = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
/// <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 async Task<bool> LoadAsync(string FileName)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Handle != 0)
|
||||
Bass.StreamFree(Handle);
|
||||
}
|
||||
catch { }
|
||||
|
||||
if (_dev != -1)
|
||||
Bass.CurrentDevice = _dev;
|
||||
|
||||
var currentDev = Bass.CurrentDevice;
|
||||
|
||||
if (currentDev == -1 || !Bass.GetDeviceInfo(Bass.CurrentDevice).IsInitialized)
|
||||
Bass.Init(currentDev);
|
||||
|
||||
var h = await Task.Run(() => OnLoad(FileName));
|
||||
|
||||
if (h == 0)
|
||||
return false;
|
||||
|
||||
Handle = h;
|
||||
|
||||
// Tag reading logic can cause exceptions
|
||||
//var tags = TagReader.Read(Handle);
|
||||
|
||||
//Title = !string.IsNullOrWhiteSpace(tags.Title) ? tags.Title
|
||||
// : Path.GetFileNameWithoutExtension(FileName);
|
||||
//Artist = tags.Artist;
|
||||
//Album = tags.Album;
|
||||
|
||||
InitProperties();
|
||||
|
||||
MediaLoaded?.Invoke(h);
|
||||
|
||||
OnPropertyChanged("");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <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();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes Properties on every call to <see cref="LoadAsync"/>.
|
||||
/// </summary>
|
||||
protected virtual void InitProperties()
|
||||
{
|
||||
Frequency = _freq;
|
||||
Balance = _pan;
|
||||
Volume = _vol;
|
||||
Loop = _loop;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
void f() => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(PropertyName));
|
||||
|
||||
if (_syncContext == null)
|
||||
f();
|
||||
else _syncContext.Post(S => f(), null);
|
||||
}
|
||||
}
|
||||
239
Harmonia.Core/Engine/BassAudioEngine.cs
Normal file
239
Harmonia.Core/Engine/BassAudioEngine.cs
Normal file
@@ -0,0 +1,239 @@
|
||||
using ManagedBass;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace Harmonia.Core.Engine;
|
||||
|
||||
public class BassAudioEngine : IAudioEngine, IDisposable
|
||||
{
|
||||
private readonly BaseMediaPlayer _mediaPlayer;
|
||||
|
||||
private CancellationTokenSource? _cancellationTokenSource;
|
||||
|
||||
public event EventHandler<PlaybackStoppedEventArgs>? PlaybackStopped;
|
||||
public event EventHandler? StreamFinished;
|
||||
public event EventHandler<PlaybackStateChangedEventArgs>? StateChanged;
|
||||
|
||||
public string? Source { get; private set; }
|
||||
public string[] SupportedFormats { get; }
|
||||
|
||||
public TimeSpan Position
|
||||
{
|
||||
get
|
||||
{
|
||||
return _mediaPlayer.Position;
|
||||
}
|
||||
set
|
||||
{
|
||||
_mediaPlayer.Position = value;
|
||||
}
|
||||
}
|
||||
|
||||
public TimeSpan Length => _mediaPlayer.Duration;
|
||||
|
||||
private float _volume;
|
||||
public float Volume
|
||||
{
|
||||
get
|
||||
{
|
||||
return _volume;
|
||||
}
|
||||
set
|
||||
{
|
||||
var newVolume = value;
|
||||
|
||||
if (newVolume > 1.0)
|
||||
newVolume = 1.0f;
|
||||
else if (newVolume < 0.0)
|
||||
newVolume = 0.0f;
|
||||
|
||||
_isMuted = false;
|
||||
_volume = newVolume;
|
||||
_mediaPlayer.Volume = newVolume;
|
||||
}
|
||||
}
|
||||
|
||||
public bool CanPause => State == AudioPlaybackState.Playing;
|
||||
|
||||
private bool _isMuted;
|
||||
public bool IsMuted
|
||||
{
|
||||
get
|
||||
{
|
||||
return _isMuted;
|
||||
}
|
||||
set
|
||||
{
|
||||
_isMuted = value;
|
||||
_mediaPlayer.Volume = value ? 0 : Volume;
|
||||
}
|
||||
}
|
||||
|
||||
private AudioPlaybackState _state;
|
||||
public AudioPlaybackState State
|
||||
{
|
||||
get
|
||||
{
|
||||
return _state;
|
||||
}
|
||||
private set
|
||||
{
|
||||
AudioPlaybackState oldValue = _state;
|
||||
_state = value;
|
||||
|
||||
StateChanged?.Invoke(this, new(oldValue, value));
|
||||
}
|
||||
}
|
||||
|
||||
public BassAudioEngine()
|
||||
{
|
||||
_mediaPlayer = new MediaPlayer();
|
||||
_mediaPlayer.MediaLoaded += OnMediaLoaded;
|
||||
_mediaPlayer.MediaFailed += OnPlaybackStopped;
|
||||
_mediaPlayer.MediaEnded += OnStreamFinished;
|
||||
_mediaPlayer.PropertyChanged += OnMediaPlayerPropertyChanged;
|
||||
|
||||
List<string> supportedFormats = [.. Bass.SupportedFormats.Split(';')];
|
||||
supportedFormats.Add(".aac");
|
||||
supportedFormats.Add(".m4a");
|
||||
supportedFormats.Add(".flac");
|
||||
supportedFormats.Add(".opus");
|
||||
supportedFormats.Add(".wma");
|
||||
|
||||
SupportedFormats = [.. supportedFormats];
|
||||
|
||||
IsMuted = false;
|
||||
Volume = 1;
|
||||
}
|
||||
|
||||
public async Task<bool> LoadAsync(string fileName)
|
||||
{
|
||||
if (fileName == Source)
|
||||
{
|
||||
Position = TimeSpan.Zero;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (File.Exists(fileName) == false)
|
||||
return false;
|
||||
|
||||
if (await LoadWaveSourceAsync(fileName) == false)
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
UpdateSource(fileName);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task<bool> LoadWaveSourceAsync(string fileName)
|
||||
{
|
||||
_cancellationTokenSource?.Cancel();
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
CancellationToken token = _cancellationTokenSource.Token;
|
||||
|
||||
try
|
||||
{
|
||||
await _mediaPlayer.LoadAsync(fileName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (token.IsCancellationRequested)
|
||||
return false;
|
||||
|
||||
//return new Result(State.Exception, ex.Message);
|
||||
throw new Exception("An error occurred - " + fileName, ex);
|
||||
}
|
||||
|
||||
if (token.IsCancellationRequested)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void UpdateSource(string fileName)
|
||||
{
|
||||
if (State != AudioPlaybackState.Stopped)
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
|
||||
Source = fileName;
|
||||
}
|
||||
|
||||
public void Play()
|
||||
{
|
||||
bool result = _mediaPlayer.Play();
|
||||
|
||||
if (!result)
|
||||
{
|
||||
if (Bass.LastError == Errors.Start)
|
||||
{
|
||||
Bass.Start();
|
||||
_mediaPlayer.Play();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Pause()
|
||||
{
|
||||
_mediaPlayer.Pause();
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
_mediaPlayer.Stop();
|
||||
Position = TimeSpan.Zero;
|
||||
}
|
||||
|
||||
private void OnMediaLoaded(int obj)
|
||||
{
|
||||
if (_mediaPlayer.Volume != Volume)
|
||||
_mediaPlayer.Volume = Volume;
|
||||
}
|
||||
|
||||
private void OnPlaybackStopped(object? sender, EventArgs args)
|
||||
{
|
||||
PlaybackStoppedEventArgs eventArgs = new($"Playback stopped unexpectedly: Last Error = {Bass.LastError}");
|
||||
PlaybackStopped?.Invoke(sender, eventArgs);
|
||||
}
|
||||
|
||||
private void OnStreamFinished(object? sender, EventArgs e)
|
||||
{
|
||||
StreamFinished?.Invoke(sender, e);
|
||||
}
|
||||
|
||||
private void OnMediaPlayerPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
switch (e.PropertyName)
|
||||
{
|
||||
case "State":
|
||||
State = GetPlaybackState(_mediaPlayer.State);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static AudioPlaybackState GetPlaybackState(PlaybackState playbackState)
|
||||
{
|
||||
return playbackState switch
|
||||
{
|
||||
PlaybackState.Stopped => AudioPlaybackState.Stopped,
|
||||
PlaybackState.Playing => AudioPlaybackState.Playing,
|
||||
PlaybackState.Stalled => AudioPlaybackState.Stalled,
|
||||
PlaybackState.Paused => AudioPlaybackState.Paused,
|
||||
_ => throw new Exception($"Unknown audio playback state: {playbackState}")
|
||||
};
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_mediaPlayer.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
public interface IAudioEngine
|
||||
{
|
||||
string[] SupportedFormats { get; }
|
||||
string Source { get; }
|
||||
string? Source { get; }
|
||||
TimeSpan Position { get; set; }
|
||||
TimeSpan Length { get; }
|
||||
float Volume { get; set; }
|
||||
|
||||
34
Harmonia.Core/Engine/MediaPlayer.cs
Normal file
34
Harmonia.Core/Engine/MediaPlayer.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using ManagedBass.Flac;
|
||||
using ManagedBass;
|
||||
|
||||
namespace Harmonia.Core.Engine;
|
||||
|
||||
public class MediaPlayer : BaseMediaPlayer
|
||||
{
|
||||
protected override int OnLoad(string FileName)
|
||||
{
|
||||
string extension = Path.GetExtension(FileName).ToLower();
|
||||
|
||||
switch (extension)
|
||||
{
|
||||
case ".flac":
|
||||
return BassFlac.CreateStream(FileName);
|
||||
//case ".opus":
|
||||
// return BassOpus.CreateStream(FileName);
|
||||
//case ".wma":
|
||||
// return BassWma.CreateStream(FileName);
|
||||
//case ".mid":
|
||||
// return BassMidi.CreateStream(FileName);
|
||||
default:
|
||||
return base.OnLoad(FileName);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void InitProperties()
|
||||
{
|
||||
base.InitProperties();
|
||||
|
||||
ChannelInfo channelInfo = Bass.ChannelGetInfo(Handle);
|
||||
Frequency = channelInfo.Frequency;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user