using ManagedBass; using System.ComponentModel; using System.Runtime.CompilerServices; namespace Harmonia.Core.Engine; /// /// A Reusable Channel which can Load files like a Player. /// is perfect for UIs, as it implements . /// Also, unlike normal, Properties/Effects set on a persist through subsequent loads. /// public class BaseMediaPlayer : INotifyPropertyChanged, IDisposable { #region Fields readonly SynchronizationContext? _syncContext; int _handle; /// /// Channel Handle of the loaded audio file. /// protected internal int Handle { get => _handle; private set { if (!Bass.ChannelGetInfo(value, out var info)) throw new ArgumentException("Invalid Channel Handle: " + value); _handle = value; // Init Events Bass.ChannelSetSync(Handle, SyncFlags.Free, 0, GetSyncProcedure(() => Disposed?.Invoke(this, EventArgs.Empty))); Bass.ChannelSetSync(Handle, SyncFlags.Stop, 0, GetSyncProcedure(() => MediaFailed?.Invoke(this, EventArgs.Empty))); Bass.ChannelSetSync(Handle, SyncFlags.End, 0, GetSyncProcedure(() => { try { if (!Bass.ChannelHasFlag(Handle, BassFlags.Loop)) MediaEnded?.Invoke(this, EventArgs.Empty); } finally { OnStateChanged(); } })); } } bool _restartOnNextPlayback; #endregion SyncProcedure GetSyncProcedure(Action Handler) { return (SyncHandle, Channel, Data, User) => { if (Handler == null) return; if (_syncContext == null) Handler(); else _syncContext.Post(S => Handler(), null); }; } static BaseMediaPlayer() { var currentDev = Bass.CurrentDevice; Bass.Configure(Configuration.IncludeDefaultDevice, true); if (currentDev == -1 || !Bass.GetDeviceInfo(Bass.CurrentDevice).IsInitialized) Bass.Init(currentDev); } /// /// Creates a new instance of . /// public BaseMediaPlayer() { _syncContext = SynchronizationContext.Current; } #region Events /// /// Fired when this Channel is Disposed. /// public event EventHandler? Disposed; /// /// Fired when the Media Playback Ends /// public event EventHandler? MediaEnded; /// /// Fired when the Playback fails /// public event EventHandler? MediaFailed; #endregion #region Frequency double _freq = 44100; /// /// Gets or Sets the Playback Frequency in Hertz. /// Default is 44100 Hz. /// public double Frequency { get => _freq; set { if (!Bass.ChannelSetAttribute(Handle, ChannelAttribute.Frequency, value)) return; _freq = value; OnPropertyChanged(); } } #endregion #region Balance double _pan; /// /// Gets or Sets Balance (Panning) (-1 ... 0 ... 1). /// -1 Represents Completely Left. /// 1 Represents Completely Right. /// Default is 0. /// public double Balance { get => _pan; set { if (!Bass.ChannelSetAttribute(Handle, ChannelAttribute.Pan, value)) return; _pan = value; OnPropertyChanged(); } } #endregion #region Device int _dev = -1; /// /// Gets or Sets the Playback Device used. /// public int Device { get => (_dev = _dev == -1 ? Bass.ChannelGetDevice(Handle) : _dev); set { if (!Bass.GetDeviceInfo(value).IsInitialized) if (!Bass.Init(value)) return; if (!Bass.ChannelSetDevice(Handle, value)) return; _dev = value; OnPropertyChanged(); } } #endregion #region Volume double _vol = 0.5; /// /// Gets or Sets the Playback Volume. /// public double Volume { get => _vol; set { if (!Bass.ChannelSetAttribute(Handle, ChannelAttribute.Volume, value)) return; _vol = value; OnPropertyChanged(); } } #endregion #region Loop bool _loop; /// /// Gets or Sets whether the Playback is looped. /// public bool Loop { get => _loop; set { if (value ? !Bass.ChannelAddFlag(Handle, BassFlags.Loop) : !Bass.ChannelRemoveFlag(Handle, BassFlags.Loop)) return; _loop = value; OnPropertyChanged(); } } #endregion /// /// Override this method for custom loading procedure. /// /// Path to the File to Load. /// on Success, on failure protected virtual int OnLoad(string FileName) => Bass.CreateStream(FileName); #region Tags string _title = "", _artist = "", _album = ""; /// /// Title of the Loaded Media. /// public string Title { get => _title; private set { _title = value; OnPropertyChanged(); } } /// /// Artist of the Loaded Media. /// public string Artist { get => _artist; private set { _artist = value; OnPropertyChanged(); } } /// /// Album of the Loaded Media. /// public string Album { get => _album; private set { _album = value; OnPropertyChanged(); } } #endregion /// /// Gets the Playback State of the Channel. /// public PlaybackState State => Handle == 0 ? PlaybackState.Stopped : Bass.ChannelIsActive(Handle); #region Playback /// /// Starts the Channel Playback. /// public bool Play() { try { var result = Bass.ChannelPlay(Handle, _restartOnNextPlayback); if (result) _restartOnNextPlayback = false; return result; } finally { OnStateChanged(); } } /// /// Pauses the Channel Playback. /// public bool Pause() { try { return Bass.ChannelPause(Handle); } finally { OnStateChanged(); } } /// /// Stops the Channel Playback. /// /// Difference from : Playback is restarted when is called. public bool Stop() { try { _restartOnNextPlayback = true; return Bass.ChannelStop(Handle); } finally { OnStateChanged(); } } #endregion /// /// Gets the Playback Duration. /// public TimeSpan Duration => TimeSpan.FromSeconds(Bass.ChannelBytes2Seconds(Handle, Bass.ChannelGetLength(Handle))); /// /// Gets or Sets the Playback Position. /// public TimeSpan Position { get => TimeSpan.FromSeconds(Bass.ChannelBytes2Seconds(Handle, Bass.ChannelGetPosition(Handle))); set => Bass.ChannelSetPosition(Handle, Bass.ChannelSeconds2Bytes(Handle, value.TotalSeconds)); } /// /// Loads a file into the player. /// /// Path to the file to Load. /// on succes, on failure. public async Task LoadAsync(string FileName) { try { if (Handle != 0) Bass.StreamFree(Handle); } catch { } if (_dev != -1) Bass.CurrentDevice = _dev; var currentDev = Bass.CurrentDevice; if (currentDev == -1 || !Bass.GetDeviceInfo(Bass.CurrentDevice).IsInitialized) Bass.Init(currentDev); var h = await Task.Run(() => OnLoad(FileName)); if (h == 0) return false; Handle = h; // Tag reading logic can cause exceptions //var tags = TagReader.Read(Handle); //Title = !string.IsNullOrWhiteSpace(tags.Title) ? tags.Title // : Path.GetFileNameWithoutExtension(FileName); //Artist = tags.Artist; //Album = tags.Album; InitProperties(); MediaLoaded?.Invoke(h); OnPropertyChanged(""); return true; } /// /// Fired when a Media is Loaded. /// public event Action? MediaLoaded; /// /// Frees all resources used by the player. /// public virtual void Dispose() { try { if (Bass.StreamFree(Handle)) _handle = 0; } finally { OnStateChanged(); GC.SuppressFinalize(this); } } /// /// Initializes Properties on every call to . /// protected virtual void InitProperties() { Frequency = _freq; Balance = _pan; Volume = _vol; Loop = _loop; } void OnStateChanged() => OnPropertyChanged(nameof(State)); /// /// Fired when a property value changes. /// public event PropertyChangedEventHandler? PropertyChanged; /// /// Fires the event. /// protected virtual void OnPropertyChanged([CallerMemberName] string? PropertyName = null) { void f() => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(PropertyName)); if (_syncContext == null) f(); else _syncContext.Post(S => f(), null); } }