Wav File
Play a .wav read from disk at runtime, with no compiled asset: LoadWav decodes the file into memory and each CreateInstance is an independent playback. Space fires a chime - press it fast and the instances overlap - L toggles a looping pad, J and K set the volume, N and M the pan. The overlay shows what was decoded and how many instances are alive, and finished instances are disposed once they report Stopped.
The Program.cs file shows how to:
- Loading a .wav at runtime with game.Audio.LoadWav instead of the asset pipeline
- One WavSound, many overlapping instances
- Looping, volume and pan on a SoundInstance
- The whole lifetime of a SoundInstance: created, played, stopped, disposed when played out or at exit
- Why the files are mono: spatialisation needs a single channel, and so does pan on OpenAL
- Using helpers: SceneSetup3D, CreatePrimitive, TextOverlay

View on GitHub.
using Stride.Audio;
using Stride.CommunityToolkit.Audio;
using Stride.CommunityToolkit.Bepu;
using Stride.CommunityToolkit.Engine;
using Stride.CommunityToolkit.Rendering.ProceduralModels;
using Stride.CommunityToolkit.Rendering.Overlay;
using Stride.CommunityToolkit.Skyboxes;
using Stride.Core.Mathematics;
using Stride.Engine;
using Stride.Games;
using Stride.Input;
using Stride.Media;
using System.Reflection;
// Play a .wav from disk with no compiled asset: read the file at runtime, keep the samples in
// memory, and create as many instances as there are things to play.
//
// This is the toolkit's founding pattern - load at runtime instead of through the asset pipeline
// - applied to the one subsystem where it had no answer. LoadWav decodes RIFF/WAVE (8/16/24/32-bit
// PCM or float, mono or stereo) into a WavSound; each CreateInstance is an independent playback,
// which is what a sound effect needs: press Space quickly and the chimes overlap instead of
// cutting each other off. The pad is one instance set to loop, the usual shape for music.
//
// Two engine rules worth knowing: an instance reports Stopped once it has played out and should be
// disposed then (each holds a native source), and OpenAL pans only mono sources - which both files
// are, deliberately.
var directory = Path.GetDirectoryName(Assembly.GetEntryAssembly()!.Location)!;
WavSound? chime = null;
WavSound? pad = null;
SoundInstance? padInstance = null;
var chimes = new List<SoundInstance>();
var volume = 0.8f;
var pan = 0f;
Entity? speaker = null;
var bump = 0f;
using var game = new Game();
var sceneSetup = new SceneSetup3D
{
Camera = CameraSetup.Perspective(position: new Vector3(0, 2.5f, -6), rotation: new Vector3(180, -12, 0)).WithController(),
Profiler = true
}.UseBepu().UseSkybox();
game.Run(sceneSetup, start: Start, update: Update);
// The game has exited; whatever was still playing holds a native voice until it is disposed
padInstance?.Dispose();
foreach (var instance in chimes) instance.Dispose();
void Start(Scene scene)
{
// Scenery only, so no physics body
speaker = game.CreatePrimitive(Primitive3D.Cube(), new PrimitiveEntityOptions
{
Body = BodyMode.None,
Material = game.CreateMaterial(new Color(90, 160, 255)),
Position = new Vector3(0, 1, 0),
});
speaker.Scene = scene;
// The files are copied next to the executable by the project file; both are mono 16-bit PCM,
// generated by build/generate-example-sounds.cs.
chime = game.Audio.LoadWav(Path.Combine(directory, "chime.wav"));
pad = game.Audio.LoadWav(Path.Combine(directory, "pad-loop.wav"));
padInstance = pad.CreateInstance();
padInstance.IsLooping = true;
padInstance.Volume = volume * 0.5f;
AddInstructions();
}
void Update(Scene scene, GameTime time)
{
var input = game.Input;
if (input.IsKeyPressed(Keys.Space) && chime is not null)
{
var instance = chime.CreateInstance();
instance.Volume = volume;
instance.Pan = pan;
instance.Play();
chimes.Add(instance);
bump = 1;
}
if (input.IsKeyPressed(Keys.L) && padInstance is not null)
{
if (padInstance.PlayState == PlayState.Playing) padInstance.Stop(); else padInstance.Play();
}
var dt = (float)time.Elapsed.TotalSeconds;
var volumeChange = (input.IsKeyDown(Keys.K) ? 1 : 0) - (input.IsKeyDown(Keys.J) ? 1 : 0);
var panChange = (input.IsKeyDown(Keys.M) ? 1 : 0) - (input.IsKeyDown(Keys.N) ? 1 : 0);
if (volumeChange != 0)
{
volume = Math.Clamp(volume + volumeChange * dt, 0, 1);
if (padInstance is not null) padInstance.Volume = volume * 0.5f;
}
if (panChange != 0)
{
pan = Math.Clamp(pan + panChange * dt, -1, 1);
if (padInstance is not null) padInstance.Pan = pan;
}
// A finished instance still holds a native source: dispose it once it reports Stopped.
chimes.RemoveAll(instance =>
{
if (instance.PlayState != PlayState.Stopped) return false;
instance.Dispose();
return true;
});
if (speaker is not null)
{
bump = MathF.Max(0, bump - dt * 4);
speaker.Transform.Scale = new Vector3(1 + bump * 0.35f);
}
}
void AddInstructions()
{
TextOverlay.GetOrCreate(game).AddSection("Wav file", () =>
{
var padOn = padInstance?.PlayState == PlayState.Playing;
return
[
new("Space", $"Chime, {chimes.Count} playing (press fast: they overlap)", chimes.Count > 0 ? Color.LightGreen : Color.Yellow),
new("L", $"Pad loop {(padOn ? "playing" : "stopped")}", padOn ? Color.LightGreen : Color.Yellow),
new(["J", "K"], $"Volume {volume:0.00}", Color.Yellow),
new(["N", "M"], $"Pan {pan:+0.00;-0.00;0.00}", Color.Yellow),
new(""),
new($"chime.wav: {Describe(chime)}", Color.LightGray),
new($"pad-loop.wav: {Describe(pad)}", Color.LightGray),
];
});
static string Describe(WavSound? sound)
=> sound is null ? "not loaded" : $"{sound.SampleRate} Hz, {(sound.Channels == 1 ? "mono" : "stereo")}, {sound.Duration.TotalSeconds:0.00} s, {sound.Samples.Length * 2 / 1024} KB in memory";
}