Create the audio service once with AudioService.Create(), load the clip with game.Assets.LoadSound("data/chime.wav"), and call audio.Play(clip, new PlayOptions { ... }) when something happens. The program below rings a bell when you press Space: the simulation counts the rings, and the game plays the chime for each new one, at one of three pitches so repeats don’t sound identical.
dotnet tool install -g RecursiveAI.OneGame.Cli --version 1.15.2 onegame new chime && cd chime rm -r art scenarios/* curl -s --create-dirs -o data/chime.wav \ https://recursiveai.org/onegame/sounds/chime.wav
You need the .NET 10 SDK. data/chime.wav is a 0.6-second bell; any WAV, Ogg Vorbis, Opus, FLAC or MP3 file works. Audio comes with the game SDK, so there is no package to add. Replace Program.cs with:
using System; using Chime; using OneGame.Audio; using OneGame.Host; internal static class Program { internal static int Main(string[] args) { // Open the speakers once, at startup. A headless run (onegame test, a replay) // records instead, so tests make no noise and can still check what played. bool headless = Array.IndexOf(args, "--headless") >= 0; AudioServiceOptions? options = headless ? new AudioServiceOptions { Capture = true } : null; using AudioService audio = AudioService.Create(options); return GameApp.Run(new ChimeGame(audio), args); } }
Replace Game.cs with:
using OneGame.Audio; using OneGame.Core; using OneGame.Host; using OneGame.Platform; using OneGame.UI; namespace Chime; /// <summary>A bell. The simulation counts rings and never plays sound.</summary> [Entity] public partial class Bell : Entity { [Synced] public partial int Rings { get; private set; } [Command] public void Ring() => Rings++; } public sealed class ChimeGame(AudioService audio) : GameDefinition { private Asset<Sound>? _chime; private Bell? _bell; private int _heard; public override string Name => "chime"; public override void RegisterEntities(EntityRegistry registry) => OneGame.Generated.Chime.EntityTypes.Register(registry); public override void Setup(World world) => world.Add(new Bell()); public override void DefineActions(ActionMap actions) => actions.Context("play", c => c.Button("ring", Bind.Key(Key.Space), Bind.Pad(GamepadButton.South))); public override void OnEachFrame(GameInstance game) { // Decoded once and cached. In a Debug run, saving data/chime.wav swaps it in. _chime ??= game.Assets.LoadSound("data/chime.wav"); var bell = (Bell)game.World.Entities[0]; if (!ReferenceEquals(bell, _bell)) { // A new world (a loaded save, a replay, the next test): old rings aren't news. (_bell, _heard) = (bell, bell.Rings); } if (game.Actions.Pressed("ring")) { bell.Send.Ring(); } // Sound is output, like drawing: play it when the state changes, // so replays and every client hear the same. if (bell.Rings != _heard) { _heard = bell.Rings; audio.Play(_chime.Value, new PlayOptions { Bus = AudioBus.Sfx, Volume = 0.8f, Pitch = 1f + 0.06f * (_heard % 3), // so repeats don't sound identical }); } } public override void DefineScreens(ScreenSet screens) => screens.Add("main", game => Ui .Label(() => $"Space rings the bell: {((Bell)game.World.Entities[0]).Rings}") .Padding(Length.Px(8)) .TextColor(Color.White)); }
onegame run opens a window: press Space and the chime plays through the default output device.
PlayOptions:Volume(linear, 1 is as recorded),Pan(-1 left to 1 right),Pitch(speed and pitch together, 0.0625 to 16),Loop,FadeInSeconds,Priority(which voice wins when all are busy) andBus.Playreturns aVoiceId. Keep it to change a playing sound withaudio.SetVolume(id, 0.2f, rampSeconds: 0.1),SetPanorStop(id, fadeSeconds). A stale id is ignored, so stopping a sound that already ended is safe.- Any number at once. A
Soundis decoded into memory and can play many times at once, each play its own voice. - A new world isn’t news. The host replaces the world when a save, a replay or a test loads, so the game remembers which
Bellit was watching and starts counting again without playing. - No device, no crash. On a machine without audio output the service runs silently and the game is otherwise unchanged.
Test that it played
A scenario can check which sounds the game asked for, with no sound card. Save this as scenarios/ring.yaml:
name: ring description: Each press of ring plays the chime once. steps: - step: 2 - action: ring - step: 2 - expect: { sound: { name: chime, count: 1 } } - action: ring - step: 2 - action: ring - step: 2 expect: - sound: { name: chime, count: 3 } - state: { entity: Bell, property: Rings, equals: 3 }
onegame testPASS ring (44 ms, tick 8, hash bdf10b7fc3c12c4d) 1 passed, 0 failed
sound: counts the plays since the scenario began, by the clip’s name: chime matches the clip loaded from data/chime.wav. Add frames: 5 to count only the last five frames. onegame test --json lists every play in each result’s sounds, with the frame, bus, volume and whether it started.
Volume settings and music
The service has three buses: AudioBus.Music, AudioBus.Sfx and AudioBus.Ui. Each play is scaled by its bus volume and by MasterVolume.
In a settings screen or a pause menu:
audio.SetBusVolume(AudioBus.Sfx, 0.5f); // effects at half volume audio.SetBusMuted(AudioBus.Music, true); // mute keeps the volume for later audio.MasterVolume = 0.8f; audio.Paused = true; // a pause menu: sounds hold where they are
Music streams from the file instead of loading it whole, and a new track replaces the old one with a crossfade: audio.PlayMusic(File.OpenRead("data/theme.ogg"), new MusicOptions { CrossfadeSeconds = 2 }).
Limits
- How this page was checked. The test output above is a headless run, which records instead of opening the device. Separately, the same clip was played through this machine’s ALSA output with the 1.15.2 package (resampled from 44.1 to 48 kHz, no underruns); the windowed game wasn’t run for this page.
- Clips are held in memory, up to 120 seconds each by default. Stream anything longer with
PlayMusic. - Stereo pan, not 3D. There is no listener or distance model: set
PanandVolumefrom the source’s position yourself. - The web needs a click first. Browsers keep audio suspended until the player interacts with the page, and sounds asked for before that are dropped.
- Create the service before loading.
LoadSoundthrowsAssetExceptionwith codeno_audioif no service exists yet.
Loading files, caching and reloading are in Assets, and scenario checks in Driving a game.