.NET library for playing notification audio on Linux with priority-based fallback between PipeWire, PulseAudio, and FFmpeg.
- Stream-First Design - Optimized for ephemeral TTS audio data
- Priority-Based Fallback - Automatically selects best available audio backend:
- PipeWire (
pw-play) - Modern, low-latency (Priority 1) - PulseAudio (
paplay) - Widely supported (Priority 2) - FFmpeg (
ffplay) - Universal fallback (Priority 3)
- PipeWire (
- Audio Sink Selection - Route notifications to specific output device
- Dependency Injection - Easy setup with
AddNotificationAudio() - Async/Await - Non-blocking audio playback
- GUID-Based Temp Files - Safe concurrent playback
Install via NuGet:
dotnet add package Olbrasoft.NotificationAudio.Providers.LinuxThis package includes all dependencies:
Olbrasoft.NotificationAudio.AbstractionsOlbrasoft.NotificationAudio.Core
using Olbrasoft.NotificationAudio.Providers.Linux;
var builder = WebApplication.CreateBuilder(args);
// Register all NotificationAudio services
builder.Services.AddNotificationAudio();
var app = builder.Build();using Olbrasoft.NotificationAudio.Abstractions;
public class NotificationService
{
private readonly INotificationPlayer _player;
public NotificationService(INotificationPlayer player)
{
_player = player;
}
public async Task SpeakAsync(Stream audioStream)
{
// Play MP3 audio from stream
await _player.PlayAsync(audioStream, ".mp3");
}
}public async Task PlayNotificationAsync(string filePath)
{
await _player.PlayAsync(filePath);
}Route notifications to specific output device via appsettings.json:
{
"NotificationAudio": {
"AudioSink": "49"
}
}Get available sinks:
# PipeWire
pw-cli list-objects | grep -A 5 "node.name"
# PulseAudio
pactl list sinks | grep -E "Sink #|Name:"If not configured, uses system default output.
NotificationAudio/
├── src/
│ ├── NotificationAudio.Abstractions/ # Interfaces
│ ├── NotificationAudio.Core/ # Core implementation
│ └── NotificationAudio.Providers.Linux/ # Linux audio providers + DI
└── tests/
├── NotificationAudio.Core.Tests/
└── NotificationAudio.Providers.Linux.Tests/
Main API for audio playback.
public interface INotificationPlayer
{
Task PlayAsync(Stream audioStream, string fileExtension = ".mp3", CancellationToken cancellationToken = default);
Task PlayAsync(string filePath, CancellationToken cancellationToken = default);
void Stop();
}Abstraction for audio backends (pw-play, paplay, ffplay).
public interface IPlaybackProvider
{
string Name { get; }
int Priority { get; }
bool IsAvailable();
Task PlayAsync(string filePath, string? audioSink, CancellationToken cancellationToken);
}Determines target output device from configuration.
public interface IAudioSinkSelector
{
string? GetNotificationSink();
}-
Provider Selection
NotificationPlayerqueries all registeredIPlaybackProviderinstances- Sorted by
Priority(1 = highest) - First available provider is selected via
IsAvailable()
-
Stream Handling
- Stream → GUID-based temp file in
/tmp/ - Pass temp file path to provider
- Delete temp file after playback (even on errors)
- Stream → GUID-based temp file in
-
Audio Sink Routing
LinuxAudioSinkSelectorreadsNotificationAudio:AudioSinkfrom configuration- Each provider uses sink-specific command-line syntax:
- PipeWire:
pw-play --target <sink> <file> - PulseAudio:
paplay --device=<sink> <file> - FFmpeg:
PULSE_SINK=<sink> ffplay -nodisp -autoexit <file>
- PipeWire:
Perfect for playing ephemeral TTS audio:
public class TtsService
{
private readonly INotificationPlayer _player;
private readonly ITtsEngine _tts;
public async Task SpeakAsync(string text)
{
using var audioStream = await _tts.SynthesizeAsync(text);
await _player.PlayAsync(audioStream, ".mp3");
// Stream automatically disposed, temp file cleaned up
}
}Combine with system notifications:
public class NotificationManager
{
private readonly INotificationPlayer _player;
public async Task NotifyAsync(string message, string soundFile)
{
// Show visual notification
ShowToast(message);
// Play notification sound
await _player.PlayAsync(soundFile);
}
}At least one of the following must be installed:
- PipeWire (Recommended):
sudo apt install pipewire pipewire-pulse - PulseAudio:
sudo apt install pulseaudio pulseaudio-utils - FFmpeg:
sudo apt install ffmpeg
- .NET 10.0 or higher
Run all tests:
dotnet testTest coverage:
- 43 tests total
- 11 tests for Core (NotificationPlayer)
- 32 tests for Providers.Linux (3 providers + LinuxAudioSinkSelector + DI)
| Issue | Cause | Solution |
|---|---|---|
InvalidOperationException: No audio playback provider found |
No audio backends installed | Install pw-play, paplay, or ffplay |
| Audio plays on wrong device | Incorrect sink ID | Check pw-cli list-objects or pactl list sinks |
FileNotFoundException |
File path doesn't exist | Verify file path is correct |
Auto-increment versioning: 1.0.${{ github.run_number }}
Example versions: 1.0.11, 1.0.12, 1.0.13, ...
Three packages are published on every push to main (when tests pass):
Olbrasoft.NotificationAudio.AbstractionsOlbrasoft.NotificationAudio.CoreOlbrasoft.NotificationAudio.Providers.Linux
MIT License - see LICENSE file for details.
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
- NuGet Package: https://www.nuget.org/packages/Olbrasoft.NotificationAudio.Providers.Linux/
- GitHub Repository: https://github.com/Olbrasoft/NotificationAudio
- Issues: https://github.com/Olbrasoft/NotificationAudio/issues