Wat!

Playing Sound Effects in Avalonia UI

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.

Sam Afshari's Notes April 10, 2023
6

Rejoining the server...

Rejoin failed... trying again in seconds.

Failed to rejoin.
Please retry or reload the page.

The session has been paused by the server.

Failed to resume the session.
Please retry or reload the page.

An unhandled error has occurred. Reload 🗙