“How hard would it be to make the app play this ‘dinging’ sound whenever the user selects the control?” the product owner asked.
“Should be pretty straight-forward,” I responded like a newb. “Not hard at all.”
Well, as it turns out, playing audio from within a Xamarin.Forms project isn’t as straight-forward as I had assumed. However, I was correct that it isn’t very hard to do.
It all boils down to a shared Interface, platform specific code implemented in each project file, and leveraging the DependencyService
.
The Interface
IAudioService.cs
using System.Threading.Tasks; namespace WSAudioApp.Interfaces { public interface IAudioService { Task PlayAudioFile(string fileName); } }
The Platform Code
The first key to making this work is that each implementation of the interface is registered with the DependencyService
by applying this Dependency
metadata attribute to the class:
[assembly: Dependency(typeof(AudioServiceImplementation))]
Everything else is just platform specific code and procedures. I emphasize the procedures here because the each platform presents a specific ‘gotcha’ or two before this will all work.
Android
AudioServiceImplementation.cs
using WSAudioApp.Interfaces; using WSAudioApp.Droid.Implementations; using Android.Media; using System.Threading.Tasks; using Xamarin.Forms; [assembly: Dependency(typeof(AudioServiceImplementation))] namespace WSAudioApp.Droid.Implementations { public class AudioServiceImplementation : IAudioService { private MediaPlayer _mediaPlayer; public async Task PlayAudioFile(string fileName) { _mediaPlayer = new MediaPlayer(); var fd = global::Android.App.Application.Context.Assets.OpenFd(fileName); _mediaPlayer.Prepared += (s, e) => { _mediaPlayer.Start(); }; await _mediaPlayer.SetDataSourceAsync(fd.FileDescriptor, fd.StartOffset, fd.Length); _mediaPlayer.Prepare(); } } }
The Android implementation leverages the MediaPlayer
class, and it pulls the files from the app’s Assets
.
Gotchas
- The
fileName
property must contain the name and extension, it must exactly match the file you want to play, and it is case-sensitive. - These files must be located in the Android project’s
Assets
folder, and they must have theBuild Action
property set toAndroidAsset
or your app will crash at runtime.
iOS
AudioServiceImplementation.cs
using Xamarin.Forms; using WSAudioApp.iOS.Implementations; using WSAudioApp.Interfaces; using System.Threading.Tasks; using AVFoundation; using Foundation; using System.IO; [assembly: Dependency(typeof(AudioServiceImplementation))] namespace WSAudioApp.iOS.Implementations { public class AudioServiceImplementation : IAudioService { private AVAudioPlayer _mediaPlayer; public async Task PlayAudioFile(string fileName) { string sFilePath = NSBundle.MainBundle.PathForResource(Path.GetFileNameWithoutExtension(fileName), Path.GetExtension(fileName)); var url = NSUrl.FromString(sFilePath); var _mediaPlayer = AVAudioPlayer.FromUrl(url); _mediaPlayer.FinishedPlaying += (object sender, AVStatusEventArgs e) => { _mediaPlayer = null; }; _mediaPlayer.Play(); } } }
The iOS implementation leverages the AVAudioPlayer
class and pulls the files from the app’s MainBundle
. Note that this method will generate a warning in its current state due to the lack of any await
keywords, but it will build.
Gotchas
- The
fileName
property must contain the name and extension, it must exactly match the file you want to play, and it is case-sensitive. - These files must be located in the iOS project’s
Resources
folder, and they must have theBuild Action
property set toBundleResource
or your app will crash at runtime.
UWP
AudioServiceImplementation.cs
using WSAudioApp.Interfaces; using WSAudioApp; using System; using System.Threading.Tasks; using Windows.Storage; using Windows.UI.Xaml.Controls; using Windows.ApplicationModel; using System.Diagnostics; using Xamarin.Forms; using WSAudioApp.UWP.Implementations; [assembly: Dependency(typeof(AudioServiceImplementation))] namespace WSAudioApp.UWP.Implementations { public class AudioServiceImplementation : IAudioService { public async Task PlayAudioFile(string fileName) { StorageFolder Folder = await Package.Current.InstalledLocation.GetFolderAsync(@"Sounds"); StorageFile sf = await Folder.GetFileAsync(fileName); var PlayMusic = new MediaElement(); PlayMusic.AudioCategory = Windows.UI.Xaml.Media.AudioCategory.Media; PlayMusic.SetSource(await sf.OpenAsync(FileAccessMode.Read), sf.ContentType); PlayMusic.Play(); } } }
The UWP implementation leverages the MediaElement
class. It is also somewhat more flexible than its Android and iOS counterparts in that it pulls the files from a custom defined folder name, in this case the Sounds
folder.
Gotchas
- Again, the
fileName
property must contain the name and extension, it must exactly match the file you want to play, and it is case-sensitive. - These files must be located in the UWP project’s
Sounds
folder, or whichever folder you decide to use, and they must have theBuild Action
property set toEmbedded Resource
.
The Calling Code
When we need to call one of these methods we have to first grab the implementation from the DependencyService
:
IAudioService audioService = DependencyService.Get<IAudioService>(); audioService.PlayAudioFile("Tone1.mp3");
So that’s it. Not too challenging, but you can spend a lot of time spinning your wheels if you aren’t aware of each platform’s file system location and property requirements. If you want to learn more about the DependencyService and what it can do, read up on the Xamarin docs here.