Added audio player logic and tests.

This commit is contained in:
2025-02-24 00:15:17 -05:00
parent f8cda3105a
commit 3c2c39e659
14 changed files with 1174 additions and 41 deletions

View 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);
}
}