Playing Sound Effects in Avalonia UI - Mon, Apr 10, 2023
If you’re looking for a way to develop cross-platform desktop-oriented applications, Avalonia UI might just be the framework for you. However, the documentation and third-party libraries can still be a bit immature. In this post, I’ll be sharing my experience developing an application that required playing audio files on different platforms, and how I managed to get it to work.
Goal
Our goal is to play audio files from a local file or a URL. To achieve this, the cross-platform code calls a function pointer that is defined separately on each platform as Func<string, Task>
, where the input is the path/URL to the audio file. You can set this up with interfaces and dependency injection, but I chose to keep it old school.
Windows
Avalonia’s default template comes with one single Desktop project. However, to make platform-specific calls, we’ll need to fork that into two separate projects for Windows and macOS. To get started with Windows, change the TargetFramework
of your Windows project to net7.0-windows10.0.17763.0
. Note that you can’t target Windows 7, and at a minimum, you’ll need to target Windows 10 to get access to the APIs we’ll be using. Your csproj should look like this:
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net7.0-windows10.0.17763.0</TargetFramework>
<Nullable>enable</Nullable>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
</PropertyGroup>
Once you’ve made this change and reloaded your project, you’ll gain access to the Windows.Media
namespaces, which allow you to easily play media files using the Windows.Media.Playback.MediaPlayer
class:
var player = new MediaPlayer();
player.Source = MediaSource.CreateFromUri(new Uri("file://c:/media.m4a"));
player.Play();
MediaSource
supports loading local files using file://
Uris or remote files.
macOS
On macOS, we can use the afplay
command-line utility to play audio files. To do this, we can simply write a function that runs a new process for afplay [path]
to play the sound effect we want:
new Process()
{
StartInfo = new ProcessStartInfo
{
FileName = "/bin/bash",
Arguments = $"-c \"afplay 'media.mp4'\"",
RedirectStandardOutput = true,
RedirectStandardInput = true,
UseShellExecute = false,
CreateNoWindow = true,
}
}.Start();
This is exactly what the NetCoreAudio project does for its macOS implementation and in general on Unix.
If you want to download a remote file, you can use System.Net.Http.HttpClient
to do that:
using var client = new HttpClient();
var bytes = await client.GetByteArrayAsync(url);
File.WriteAllBytes(localPath, bytes);
WebAssembly
Playing audio on the web can be done using an Audio
object in JavaScript and calling the play()
method on it. To call JavaScript functions from your C# code, you can use the JSImport
attribute.
First, define a function in main.js
that plays an audio file, for example:
globalThis.playSound = function (url) {
var audio = new Audio(url);
audio.play();
}
Then you can JSImport
this as a C# method anywhere you want like this:
using System.Runtime.InteropServices.JavaScript;
...
[JSImport("globalThis.playSound")]
public static partial void PlaySound(string url);
Finally, call the PlaySound
method in C# to run the globalThis.playSound
JavaScript function:
PlaySound("http://sound.com/audio.mp4");
However, when dealing with a website, you might run into CORS (Cross-Origin Resource Sharing) issues. Make sure the remote host allows you to download the file you want. It’s always a good idea to look at your browser’s DevTools console for hints about any issues related to cross-site scripting errors.