Skip to content
Client v5: BLE, BLE Hosting, HTTP, Jobs - Linux, MacOS, & Blazor Support! Full AOT, RX on BLE only & MANY other features! Power up!

Audio (Monitor & Devices)

Shiny.Audio is the standalone audio backbone under Shiny.Speech — usable on its own for recording and playback without the speech stack. Alongside capture (IAudioSource) and playback (IAudioPlayer) it provides a live microphone monitor (IAudioMonitor) and audio route enumeration/selection (IAudioDevices), all discoverable through a single IAudio facade.

builder
.UseMauiApp<App>()
.UseShiny(); // required on Android for RECORD_AUDIO permission routing
builder.Services.AddAudioServices(); // IAudioPlayer + IAudioSource + IAudioMonitor + IAudioDevices + IAudio

AddSpeechServices() already registers the capture/playback pieces; call AddAudioServices() when you also want the monitor/devices/facade. The individual registrations still exist (AddAudioSource(), AddAudioPlayer(), AddAudioMonitor(), AddAudioDevices()), and every focused interface remains injectable directly — the facade doesn’t replace them.

Inject one service to discover the whole surface. Each member is the same service you could inject directly; lifetimes are preserved (player/monitor/devices singletons, IAudioSource a fresh transient per access).

using Shiny.Audio;
public class AudioViewModel(IAudio audio)
{
Task Play() => audio.Player.PlayAsync("https://example.com/clip.mp3");
Task StartMic() => audio.Monitor.Start();
var Outputs => audio.Devices.GetOutputs();
}

Monitor and Devices are implemented on iOS / Mac Catalyst and Android only; accessing them on other platforms throws PlatformNotSupportedException.

Routes the microphone straight to the current audio output in near-real-time — a PA / megaphone. Ideal for “talk into the phone, hear it over a Bluetooth speaker”.

using Shiny.Audio;
public partial class MicViewModel(IAudio audio) : ObservableObject
{
[ObservableProperty] double level; // bind to a VU bar
public async Task Toggle()
{
var monitor = audio.Monitor;
if (monitor.IsMonitoring)
{
await monitor.Stop();
return;
}
if (await monitor.RequestAccess() != AccessState.Available)
return;
monitor.InputLevelChanged += (_, l) =>
MainThread.BeginInvokeOnMainThread(() => Level = l);
// No Processing → routes to a Bluetooth A2DP speaker (see the trade-off below).
await monitor.Start(new AudioMonitorOptions { Gain = 1.0 });
}
}

The output goes to the current route — the **built-in speaker, a wired output, or a connected Bluetooth speaker (A2DP) using the phone’s own mic. Gain (0.0–1.0) is adjustable live; InputLevelChanged emits a normalized level for a meter.

Implementation: iOS/Mac Catalyst use AVAudioEngine (input node → main mixer → output) in a PlayAndRecord session (no DefaultToSpeaker, so it follows a Bluetooth/wired route; Default mode unless processing is requested); the engine rebuilds on route changes so monitoring follows a newly-connected output. Android uses AudioRecordAudioTrack. The audio session is snapshotted and restored on Stop.

AirPlay (HomePod / Apple TV) is not supported for a live mic — iOS only permits AirPlay for playback, not while recording. Use Bluetooth for a wireless live PA.

Its most reliable use is displaying where audio is flowing and reacting to route changes — e.g. “Output: JBL Flip · Bluetooth”. Read CurrentInput / CurrentOutput (each an AudioDevice with a normalized Type) and subscribe to Changed:

using Shiny.Audio;
var devices = audio.Devices;
string input = devices.CurrentInput?.Name ?? "—"; // .Type: BuiltInMic, Bluetooth, …
string output = devices.CurrentOutput?.Name ?? "—"; // .Type: BuiltInSpeaker, BluetoothA2dp, …
devices.Changed += (_, _) => Refresh(); // route added / removed / switched

Each AudioDevice carries Id, Name, Io (input/output), a normalized Type (BuiltInMic, BluetoothA2dp, WiredHeadphones, BuiltInSpeaker, …) and IsCurrent.

Selection (GetInputs/GetOutputs + IAudioMonitor.SetInputDevice/SetOutputDevice) is a full selector on Android only:

await audio.Monitor.SetInputDevice(chosenInput); // Android + iOS
await audio.Monitor.SetOutputDevice(chosenOutput); // Android only (best-effort elsewhere)

IAudioPlayer exposes the device media volume — the level the hardware buttons control, normalized 0.01.0 and independent of any per-request TTS Volume. Reading works on every platform; setting is platform-limited — always guard a set with IsVolumeControlSupported.

using Shiny.Audio;
public partial class VolumeViewModel(IAudio audio) : ObservableObject
{
[ObservableProperty] float volume; // bind to a Slider
public bool CanSet => audio.Player.IsVolumeControlSupported;
public VolumeViewModel(IAudio audio) : this(audio)
{
Volume = audio.Player.Volume; // reading works everywhere
audio.Player.VolumeChanged += (_, v) => // hardware buttons, OS UI, or a successful set
MainThread.BeginInvokeOnMainThread(() => Volume = v);
}
partial void OnVolumeChanged(float value)
{
if (audio.Player.IsVolumeControlSupported) // iOS / Mac Catalyst setter throws
audio.Player.Volume = value;
}
}
PlatformReadSetVolumeChanged sourceBacking API
Androidsystem settings observerAudioManager STREAM_MUSIC (integer-stepped → set values quantize)
Windowsendpoint volume callbackWASAPI IAudioEndpointVolume (default render endpoint)
macOS✅ †CoreAudio property listenerHAL virtual main volume of the default output device
iOS / Mac CatalystKVO on outputVolumeAVAudioSession.OutputVolume (read-only)
Browser (WASM)echoed on setHTMLAudioElement.volume — app-local, not the OS volume
CapabilityiOS / Mac CatalystAndroidWindowsBrowser
IAudioPlayer / IAudioSource
IAudioPlayer.Volume (read)
IAudioPlayer.Volume (set)❌ ¹
IAudioMonitor (live monitor)
IAudioDevices (routes)

¹ Native macOS (not Mac Catalyst) can set the volume via CoreAudio — see the Volume section.