Skip to content

Commit

Permalink
Add Speed property (#26)
Browse files Browse the repository at this point in the history
* Add Speed property to AudioPlayer

* Add Speed property to sample app and handle speed limitations on different Android devices

* Add handling for exceptions, speed out of range and Android API level < 23

* Add SpeedOutOfRangeException handling in sample application

* Add Speed property to NET6_0 definition

* Add CanSetSpeed handing in Sample App

* Fix bad merge whoops

* Fix speed clamping on iOS and Android

* Fix Windows Speed

* PR Feedback

* Update Sample

* Update AudioPlayer.net.cs

* Update MusicPlayerPageViewModel.cs

* Update src/Plugin.Maui.Audio/AudioPlayer.android.cs

* Update src/Plugin.Maui.Audio/AudioPlayer.net.cs

Co-authored-by: Shaun Lawrence <[email protected]>

Co-authored-by: Johan Svensson <[email protected]>
Co-authored-by: Johan Svensson <[email protected]>
Co-authored-by: Gerald Versluis <[email protected]>
Co-authored-by: Shaun Lawrence <[email protected]>
Co-authored-by: Gerald Versluis <[email protected]>
  • Loading branch information
6 people authored Nov 29, 2022
1 parent df5e494 commit a4a2e0f
Show file tree
Hide file tree
Showing 8 changed files with 268 additions and 103 deletions.
22 changes: 18 additions & 4 deletions samples/AudioPlayerSample/Pages/MusicPlayerPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<ScrollView>
<VerticalStackLayout
Spacing="25"
Padding="30,0"
Padding="30,0,30,30"
VerticalOptions="Center"
IsEnabled="{Binding HasAudioSource}">

Expand Down Expand Up @@ -76,13 +76,27 @@
Maximum="1"
Value="{Binding Volume}" />

<Label Text="Balance:" />
<Slider
<Label Text="Balance:" />
<Slider
Minimum="-1"
Maximum="1"
Value="{Binding Balance}" />

</VerticalStackLayout>

<Label IsVisible="{Binding CanSetSpeed}">
<Label.FormattedText>
<FormattedString>
<Span Text="Speed" />
<Span Text="{Binding Speed, StringFormat=' ({0:F1}):'}"/>
</FormattedString>
</Label.FormattedText>
</Label>
<Slider
IsVisible="{Binding CanSetSpeed}"
Minimum="{Binding MinimumSpeed}"
Maximum="{Binding MaximumSpeed}"
Value="{Binding Speed}" />
</VerticalStackLayout>
</ScrollView>

</ContentPage>
Expand Down
28 changes: 28 additions & 0 deletions samples/AudioPlayerSample/ViewModels/MusicPlayerPageViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ public async void ApplyQueryAttributes(IDictionary<string, object> query)

NotifyPropertyChanged(nameof(HasAudioSource));
NotifyPropertyChanged(nameof(Duration));
NotifyPropertyChanged(nameof(CanSetSpeed));
NotifyPropertyChanged(nameof(MinimumSpeed));
NotifyPropertyChanged(nameof(MaximumSpeed));
}
}

Expand Down Expand Up @@ -109,6 +112,31 @@ public double Balance
}
}

public bool CanSetSpeed => audioPlayer?.CanSetSpeed ?? false;

public double Speed
{
get => audioPlayer?.Speed ?? 1;
set
{
try
{
if (audioPlayer?.CanSetSpeed ?? false)
{
audioPlayer.Speed = Math.Round(value, 1, MidpointRounding.AwayFromZero);
NotifyPropertyChanged();
}
}
catch (Exception ex)
{
App.Current.MainPage.DisplayAlert("Speed", ex.Message, "OK");
}
}
}

public double MinimumSpeed => audioPlayer?.MinimumSpeed ?? 1;
public double MaximumSpeed => audioPlayer?.MaximumSpeed ?? 1;

public bool Loop
{
get => audioPlayer?.Loop ?? false;
Expand Down
35 changes: 33 additions & 2 deletions src/Plugin.Maui.Audio/AudioPlayer.android.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Runtime.Versioning;
using Android.Content.Res;
using Android.Media;
using Stream = System.IO.Stream;
Expand Down Expand Up @@ -31,7 +32,37 @@ public double Balance
set => SetVolume(Volume, balance = value);
}

public bool IsPlaying => player.IsPlaying;
[SupportedOSPlatform("Android23.0")]
public double Speed
{
get => player.PlaybackParams.Speed;
set
{
// Check if set speed is supported
if (CanSetSpeed)
{
// Speed on Android can be between 0 and 6
var speedValue = Math.Clamp((float)value, 0.0f, 6.0f);

if (float.IsNaN(speedValue))
speedValue = 1.0f;

player.PlaybackParams = player.PlaybackParams.SetSpeed(speedValue) ?? player.PlaybackParams;
}
else
{
throw new NotSupportedException("Set playback speed is not supported!");
}
}
}

public double MinimumSpeed => 0;

public double MaximumSpeed => 6;

public bool CanSetSpeed => Android.OS.Build.VERSION.SdkInt >= Android.OS.BuildVersionCodes.M;

public bool IsPlaying => player.IsPlaying;

public bool Loop
{
Expand Down Expand Up @@ -107,7 +138,7 @@ void PreparePlayerLegacy(Stream audioStream)
}

player.Prepare();
}
}

static void DeleteFile(string path)
{
Expand Down
207 changes: 118 additions & 89 deletions src/Plugin.Maui.Audio/AudioPlayer.macios.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,73 +5,81 @@ namespace Plugin.Maui.Audio;

partial class AudioPlayer : IAudioPlayer
{
readonly AVAudioPlayer player;
bool isDisposed;

public double Duration => player.Duration;

public double CurrentPosition => player.CurrentTime;

public double Volume
{
get => player.Volume;
set => player.Volume = (float)Math.Clamp(value, 0, 1);
}

public double Balance
{
get => player.Pan;
set => player.Pan = (float)Math.Clamp(value, -1, 1);
}

public bool IsPlaying => player.Playing;

public bool Loop
{
get => player.NumberOfLoops != 0;
set => player.NumberOfLoops = value ? -1 : 0;
}

public bool CanSeek => true;

internal AudioPlayer(Stream audioStream)
{
var data = NSData.FromStream(audioStream)
?? throw new FailedToLoadAudioException("Unable to convert audioStream to NSData.");
player = AVAudioPlayer.FromData(data)
?? throw new FailedToLoadAudioException("Unable to create AVAudioPlayer from data.");

PreparePlayer();
}

internal AudioPlayer(string fileName)
{
player = AVAudioPlayer.FromUrl(NSUrl.FromFilename(fileName))
?? throw new FailedToLoadAudioException("Unable to create AVAudioPlayer from url.");

PreparePlayer();
}

protected virtual void Dispose(bool disposing)
{
if (isDisposed)
{
return;
}

if (disposing)
{
Stop();

player.FinishedPlaying -= OnPlayerFinishedPlaying;
player.Dispose();
}

isDisposed = true;
}

public void Pause() => player.Pause();

readonly AVAudioPlayer player;
bool isDisposed;

public double Duration => player.Duration;

public double CurrentPosition => player.CurrentTime;

public double Volume
{
get => player.Volume;
set => player.Volume = (float)Math.Clamp(value, 0, 1);
}

public double Balance
{
get => player.Pan;
set => player.Pan = (float)Math.Clamp(value, -1, 1);
}

public double Speed
{
get => player.Rate;
set
{
// Check if set speed is supported
if (CanSetSpeed)
{
// Rate property supports values in the range of 0.5 for half-speed playback to 2.0 for double-speed playback.
var speedValue = Math.Clamp((float)value, 0.5f, 2.0f);

if (float.IsNaN(speedValue))
speedValue = 1.0f;

player.Rate = speedValue;
}
else
{
throw new NotSupportedException("Set playback speed is not supported!");
}
}
}

public double MinimumSpeed => 0.5;

public double MaximumSpeed => 2;

public bool CanSetSpeed => true;

public bool IsPlaying => player.Playing;

public bool Loop
{
get => player.NumberOfLoops != 0;
set => player.NumberOfLoops = value ? -1 : 0;
}

public bool CanSeek => true;

internal AudioPlayer(Stream audioStream)
{
var data = NSData.FromStream(audioStream)
?? throw new FailedToLoadAudioException("Unable to convert audioStream to NSData.");
player = AVAudioPlayer.FromData(data)
?? throw new FailedToLoadAudioException("Unable to create AVAudioPlayer from data.");

PreparePlayer();
}

internal AudioPlayer(string fileName)
{
player = AVAudioPlayer.FromUrl(NSUrl.FromFilename(fileName))
?? throw new FailedToLoadAudioException("Unable to create AVAudioPlayer from url.");

PreparePlayer();
}
public void Play()
{
if (player.Playing)
Expand All @@ -84,25 +92,46 @@ public void Play()
}
}

public void Seek(double position) => player.CurrentTime = position;

public void Stop()
{
player.Stop();
Seek(0);
PlaybackEnded?.Invoke(this, EventArgs.Empty);
}

bool PreparePlayer()
{
player.FinishedPlaying += OnPlayerFinishedPlaying;
player.PrepareToPlay();

return true;
}

void OnPlayerFinishedPlaying(object? sender, AVStatusEventArgs e)
{
PlaybackEnded?.Invoke(this, e);
}
}
protected virtual void Dispose(bool disposing)
{
if (isDisposed)
{
return;
}

if (disposing)
{
Stop();

player.FinishedPlaying -= OnPlayerFinishedPlaying;
player.Dispose();
}

isDisposed = true;
}

public void Pause() => player.Pause();

public void Seek(double position) => player.CurrentTime = position;

public void Stop()
{
player.Stop();
Seek(0);
PlaybackEnded?.Invoke(this, EventArgs.Empty);
}

bool PreparePlayer()
{
player.FinishedPlaying += OnPlayerFinishedPlaying;
player.EnableRate = true;
player.PrepareToPlay();

return true;
}

void OnPlayerFinishedPlaying(object? sender, AVStatusEventArgs e)
{
PlaybackEnded?.Invoke(this, e);
}
}
8 changes: 8 additions & 0 deletions src/Plugin.Maui.Audio/AudioPlayer.net.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,12 @@ public void Pause() { }
public void Stop() { }

public void Seek(double position) { }

public double Speed { get; set; }

public double MinimumSpeed { get; }

public double MaximumSpeed { get; }

public bool CanSetSpeed { get; }
}
18 changes: 10 additions & 8 deletions src/Plugin.Maui.Audio/AudioPlayer.shared.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,20 @@
public partial class AudioPlayer : IAudioPlayer
{
#pragma warning disable CS0067

public event EventHandler? PlaybackEnded;

#pragma warning restore CS0067

~AudioPlayer()
{
Dispose(false);
}
{
Dispose(false);
}

public void Dispose()
{
Dispose(true);
public void Dispose()
{
Dispose(true);

GC.SuppressFinalize(this);
}
GC.SuppressFinalize(this);
}
}
Loading

0 comments on commit a4a2e0f

Please sign in to comment.