FS.GG.Audio.Host
0.4.0
Prefix Reserved
dotnet add package FS.GG.Audio.Host --version 0.4.0
NuGet\Install-Package FS.GG.Audio.Host -Version 0.4.0
<PackageReference Include="FS.GG.Audio.Host" Version="0.4.0" />
<PackageVersion Include="FS.GG.Audio.Host" Version="0.4.0" />
<PackageReference Include="FS.GG.Audio.Host" />
paket add FS.GG.Audio.Host --version 0.4.0
#r "nuget: FS.GG.Audio.Host, 0.4.0"
#:package FS.GG.Audio.Host@0.4.0
#addin nuget:?package=FS.GG.Audio.Host&version=0.4.0
#tool nuget:?package=FS.GG.Audio.Host&version=0.4.0
FS.GG.Audio.Host
The device seam of the FS.GG.Audio component. It turns the
pure AudioEffect values of
FS.GG.Audio.Core into playback through a
narrow IAudioBackend interface, with two implementations: a deterministic Null/record backend
(the default, and the one CI runs against) and a real OpenAL backend on Silk.NET that degrades to
Null when no device is present.
Game-facing code holds an IAudioBackend and never names a concrete backend type.
Install
dotnet add package FS.GG.Audio.Host
Quick start
open FS.GG.Audio.Core
open FS.GG.Audio.Host
// Headless / test: records requests, opens no device.
let backend = NullBackend.create ()
Audio.play (backend :> IAudioBackend) [ Audio.playSfx (SoundId "jump") 0.8 ]
backend.Evidence.Requested // = the batch, volumes normalized
// Real device: the product supplies the id -> PCM mapping.
let resolver =
{ ResolveSound = fun (SoundId id) -> loadWav $"sfx/{id}.wav"
ResolveTrack = fun (TrackId id) -> loadWav $"music/{id}.wav" }
use device = OpenAlBackend.create resolver // falls back to Null if no device
Which sink? Audio.play does not mix
A product wires a per-frame sink of type AudioEffect list -> unit. There are two, and the
difference is load-bearing:
// RAW — plays straight at the backend. No mixing.
let audioSink = Audio.play backend
// MIXING — steps an owned FS.GG.Audio.Engine. Bus volume, ducking, fades and 3D all apply.
let audioSink = Engine.createSink backend
Audio.play is a thin drive over IAudioBackend, and a backend has no mixer and no clock. So on the
raw path SetBusVolume and Duck are discarded and PlaySfx3D plays non-positional — which
means a settings screen's volume slider, wired this way, does nothing at all: the effect is a
well-formed value, the sink accepts it, and no sound changes (#27). Audio.requiresEngine is the
predicate for exactly those effects, and the first batch carrying one logs a diagnostic saying so.
Reach for Audio.play when you want deliberate fire-and-forget playback; reach for
FS.GG.Audio.Engine's Engine.createSink
whenever bus volume, ducking, fades or 3D matter. (Elmish products have the same choice between
Audio.Cmd.ofEffects and Audio.Cmd.ofEngine.)
Surface
Audio.play— the raw imperative drive: folds a batch through the backend in dispatch order. No mixing (see above).Audio.requiresEngine— pure predicate, true for exactly the effects the raw path cannot realize (SetBusVolume,Duck,PlaySfx3D).IAudioBackend—Play: AudioEffect -> unit, plusIDisposable. Never throws; a backend that cannot act degrades to a no-op.IMixingBackend— optional extension (SetBusGain,SetListener,PlayAt) thatFS.GG.Audio.Enginefeature-detects for continuous mixing and 3D. Backends implementing onlyIAudioBackendremain valid.NullBackend.create— the record-only backend; itsEvidenceequalsCore.Audio.interpretof the same batch.OpenAlBackend.create— opens an OpenAL device, or logs the reason and returns a Null backend. The result is always usable, never null, and never throws into game code. That substitution is silent unless you ask — seeBackendbelow.Backend.kindOf/Backend.isDeviceBacked— what did I actually get? AnswersDeviceBacked,RecordOnly of Silence(Requested— you asked for it; orDeviceUnavailable reason— it was substituted under you), orUnknownfor a backend this library did not build. No type test, no exception handling.AssetResolver— caller-suppliedSoundId/TrackId→ WAV bytes. An unresolved id is a recorded no-op, not a throw.Wav.tryParse— a total, device-free minimal PCM WAV reader; returnsNoneon anything it does not understand.
Determinism
NullBackend is the default backend under test: no device is opened and the recorded evidence is
the assertion surface.
The OpenAL path, by contrast, is exercised by whatever the machine happens to have. OpenAlBackend.create
degrades to NullBackend when no device opens (FR-004), so on a headless box a test that drives
playback is asserting against a recorder — and passes because nothing played. A green tick on a
subject that was never constructed is reporting on nothing.
So a test that needs a real device must ask, and skip loudly when it has no subject (#34):
let backend = OpenAlBackend.create resolver
if Backend.isDeviceBacked backend then
// a device really opened — assert against it
else
skiptest "no audio device here — reported Ignored rather than Passed"
isDeviceBacked is false for Unknown on purpose: a record-only fake is an IAudioBackend that
is not NullBackend.T, so a "not-Null means device" rule would wave it through as audible — the same
vacuous green, one level up.
License
MIT — see LICENSE.
This package redistributes the OpenAL Soft native library, which is LGPL-2.0-or-later. The
FS.GG.Audio source is MIT; OpenAL Soft is not, and restoring this package places libopenal into
your output under runtimes/<rid>/native/.
It ships as a separate, dynamically-linked, replaceable shared library — never linked into an
FS.GG.Audio assembly — which is the deliberate design (DEC-001) that keeps the component usable by
closed-source games. You can substitute your own OpenAL, or drop the native and let
OpenAlBackend.create degrade to the Null backend. FS.GG.Audio.Core has no native dependency at
all if you need to avoid it entirely.
See THIRD-PARTY-NOTICES.md (included in this package) for the full notice.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- FS.GG.Audio.Core (>= 0.4.0)
- FSharp.Core (>= 10.1.301)
- Silk.NET.OpenAL (>= 2.23.0)
- Silk.NET.OpenAL.Soft.Native (>= 1.23.1)
NuGet packages (2)
Showing the top 2 NuGet packages that depend on FS.GG.Audio.Host:
| Package | Downloads |
|---|---|
|
FS.GG.Audio.Elmish
Elmish Cmd authoring surface over FS.GG.Audio.Host/.Engine — Audio.Cmd.playSfx/playMusic/stopMusic/setMasterVolume produce Elmish commands that play straight through an IAudioBackend (no mixing), while Audio.Cmd.ofEngine routes a batch through FS.GG.Audio.Engine so bus volume, ducking, and 3D positioning apply. Depends on Elmish (MIT), never on FS.GG.UI. Depends on FS.GG.Audio.Host, so it also redistributes the OpenAL Soft native library (LGPL-2.0-or-later) as a separate, dynamically-linked, replaceable shared library — see THIRD-PARTY-NOTICES.md in this package. |
|
|
FS.GG.Audio.Engine
Mixing/voice layer over FS.GG.Audio.Host — named buses, linear/equal-power fades and cross-fades, side-chain ducking, and 3D listener/emitter positioning, realized through the IAudioBackend seam (IMixingBackend when available) with a deterministic device-free record path. Depends on FS.GG.Audio.Host/.Core only; never on FS.GG.UI. Depends on FS.GG.Audio.Host, so it also redistributes the OpenAL Soft native library (LGPL-2.0-or-later) as a separate, dynamically-linked, replaceable shared library — see THIRD-PARTY-NOTICES.md in this package. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.4.0 | 120 | 7/21/2026 |
| 0.3.0 | 1,003 | 7/16/2026 |
| 0.2.0 | 2,142 | 7/11/2026 |
| 0.1.0 | 2,573 | 7/9/2026 |
| 0.1.0-preview.1 | 81 | 7/7/2026 |