Skip to content

Commit

Permalink
Attempt at getting an async audio player working (#36)
Browse files Browse the repository at this point in the history
* Attempt at getting an async audio player working

* Make the async part actually work

* Async playback updates
  • Loading branch information
bijington authored Sep 11, 2023
1 parent ad1d2ff commit 748f960
Show file tree
Hide file tree
Showing 6 changed files with 134 additions and 10 deletions.
4 changes: 4 additions & 0 deletions samples/Plugin.Maui.Audio.Sample/Pages/AudioRecorderPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@
Text="Play"
Command="{Binding PlayCommand}" />

<Button
Text="StopPlay"
Command="{Binding StopPlayCommand}" />

<Label
Text="{Binding RecordingTime, Converter={StaticResource SecondsToStringConverter}}" />
</VerticalStackLayout>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,27 @@ public class AudioRecorderPageViewModel : BaseViewModel
readonly IAudioManager audioManager;
readonly IDispatcher dispatcher;
IAudioRecorder audioRecorder;
IAudioPlayer audioPlayer;
AsyncAudioPlayer audioPlayer;
IAudioSource audioSource = null;
readonly Stopwatch recordingStopwatch = new Stopwatch();
bool isPlaying;

public double RecordingTime
{
get => recordingStopwatch.ElapsedMilliseconds / 1000;
}

public bool IsPlaying
{
get => isPlaying;
set
{
isPlaying = value;
PlayCommand.ChangeCanExecute();
StopPlayCommand.ChangeCanExecute();
}
}

public bool IsRecording
{
get => audioRecorder?.IsRecording ?? false;
Expand All @@ -26,29 +38,40 @@ public bool IsRecording
public Command PlayCommand { get; }
public Command StartCommand { get; }
public Command StopCommand { get; }
public Command StopPlayCommand { get; }

public AudioRecorderPageViewModel(
IAudioManager audioManager,
IDispatcher dispatcher)
{
StartCommand = new Command(Start, () => !IsRecording);
StopCommand = new Command(Stop, () => IsRecording);
PlayCommand = new Command(PlayAudio);
PlayCommand = new Command(PlayAudio, () => !IsPlaying);
StopPlayCommand = new Command(StopPlay, () => IsPlaying);

this.audioManager = audioManager;
this.dispatcher = dispatcher;
}

public void PlayAudio()
async void PlayAudio()
{
if (audioSource != null)
{
audioPlayer = this.audioManager.CreatePlayer(((FileAudioSource)audioSource).GetAudioStream());
audioPlayer = this.audioManager.CreateAsyncPlayer(((FileAudioSource)audioSource).GetAudioStream());

audioPlayer.Play();
IsPlaying = true;

await audioPlayer.PlayAsync(CancellationToken.None);

IsPlaying = false;
}
}

void StopPlay()
{
audioPlayer.Stop();
}

async void Start()
{
if (await CheckPermissionIsGrantedAsync<Microphone>())
Expand Down
12 changes: 9 additions & 3 deletions src/Plugin.Maui.Audio/AudioManager.shared.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,18 @@ public IAudioPlayer CreatePlayer(string fileName)
{
ArgumentNullException.ThrowIfNull(fileName);

return new AudioPlayer(fileName);
}
return new AudioPlayer(fileName);
}

/// <inheritdoc />
public AsyncAudioPlayer CreateAsyncPlayer(Stream audioStream) => new (CreatePlayer(audioStream));

/// <inheritdoc />
public AsyncAudioPlayer CreateAsyncPlayer(string fileName) => new (CreatePlayer(fileName));

/// <inheritdoc />
public IAudioRecorder CreateRecorder()
{
return new AudioRecorder();
}
}
}
51 changes: 51 additions & 0 deletions src/Plugin.Maui.Audio/AudioPlayer/AsyncAudioPlayer.shared.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
namespace Plugin.Maui.Audio;

/// <summary>
/// Provides async/await support by wrapping an <see cref="IAudioPlayer"/>.
/// </summary>
public class AsyncAudioPlayer
{
readonly IAudioPlayer audioPlayer;
CancellationTokenSource? stopCancellationToken;

/// <summary>
/// Creates a new instance of <see cref="AsyncAudioPlayer"/>.
/// This is particularly useful if you want to customise the audio playback settings before playback.
/// </summary>
/// <param name="audioPlayer">An <see cref="IAudioPlayer"/> implementation to act as the underlying mechanism of playing audio.</param>
public AsyncAudioPlayer(IAudioPlayer audioPlayer)
{
this.audioPlayer = audioPlayer;
}

/// <summary>
/// Begin audio playback asynchronously.
/// </summary>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to allow for canceling the audio playback.</param>
/// <returns>A <see cref="Task"/> that represents the asynchronous operation, awaiting this method will wait until the audio has finished playing before continuing.</returns>
public async Task PlayAsync(CancellationToken cancellationToken)
{
var taskCompletionSource = new TaskCompletionSource();

stopCancellationToken = new();

audioPlayer.PlaybackEnded += (o, e) => taskCompletionSource.TrySetResult();

audioPlayer.Play();

await Task.WhenAny(
taskCompletionSource.Task,
cancellationToken.WhenCanceled(),
stopCancellationToken.Token.WhenCanceled());

audioPlayer.Stop();
}

/// <summary>
/// Stops the currently playing audio.
/// </summary>
public void Stop()
{
stopCancellationToken?.Cancel();
}
}
18 changes: 16 additions & 2 deletions src/Plugin.Maui.Audio/IAudioManager.shared.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,25 @@ public interface IAudioManager
/// </summary>
/// <param name="fileName">The name of the file containing the audio to play.</param>
/// <returns>A new <see cref="IAudioPlayer"/> with the supplied <paramref name="fileName"/> ready to play.</returns>
IAudioPlayer CreatePlayer(string fileName);
IAudioPlayer CreatePlayer(string fileName) => new AudioPlayer(fileName);

/// <summary>
/// Creates a new <see cref="AsyncAudioPlayer"/> with the supplied <paramref name="audioStream"/> ready to play audio using async/await.
/// </summary>
/// <param name="audioStream">The <see cref="Stream"/> containing the audio to play.</param>
/// <returns>A new <see cref="AsyncAudioPlayer"/> with the supplied <paramref name="audioStream"/> ready to play.</returns>
AsyncAudioPlayer CreateAsyncPlayer(Stream audioStream);

/// <summary>
/// Creates a new <see cref="AsyncAudioPlayer"/> with the supplied <paramref name="fileName"/> ready to play audio using async/await.
/// </summary>
/// <param name="fileName">The name of the file containing the audio to play.</param>
/// <returns>A new <see cref="AsyncAudioPlayer"/> with the supplied <paramref name="fileName"/> ready to play.</returns>
AsyncAudioPlayer CreateAsyncPlayer(string fileName);

/// <summary>
/// Creates a new <see cref="IAudioRecorder"/> ready to begin recording audio from the current device.
/// </summary>
/// <returns>A new <see cref="IAudioRecorder"/> ready to begin recording.</returns>
IAudioRecorder CreateRecorder();
}
}
26 changes: 26 additions & 0 deletions src/Plugin.Maui.Audio/TaskExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
namespace Plugin.Maui.Audio;

public static class TaskExtensions
{
/// <summary>
/// Provides a mechanism to await until the supplied <paramref name="cancellationToken"/> has been cancelled.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to await.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public static Task WhenCanceled(this CancellationToken cancellationToken)
{
var completionSource = new TaskCompletionSource<bool>();

cancellationToken.Register(
input =>
{
if (input is TaskCompletionSource<bool> taskCompletionSource)
{
taskCompletionSource.SetResult(true);
}
},
completionSource);

return completionSource.Task;
}
}

0 comments on commit 748f960

Please sign in to comment.