Tutorial 14: Playing Sound Effects
What you’ll learn
- Loading a
SoundEffectand firing it withPlay(). - When you need a
SoundEffectInstanceinstead, and controlling volume, pitch and pan. - Positioning a sound in 3D.
Before you start — Tutorial 08: Loading and Drawing Textures for the ContentManager::Load pattern and Tutorial 10: Handling Keyboard Input to trigger a sound from a key press.
Build requirement: this tutorial requires CNA_AUDIO_PLATFORM=SDL3, the only alpha.1 selection that defines SOUND_ENABLED and links the SDL3_mixer playback/decoding engine. The SDL2 and Null selections implement the low-level audio-device boundary but do not provide equivalent SoundEffect playback.
CNA implements the XNA 4.0 audio API on top of SDL3_mixer. Short, one-shot sounds — footsteps, explosions, coin pickups — are handled by SoundEffect. For looping or controllable playback you use a SoundEffectInstance. This tutorial covers both.
SoundEffect overview
SoundEffect lives in Microsoft::Xna::Framework::Audio. It wraps a decoded audio buffer and exposes a simple Play() call that fires and forgets — perfect for short one-shot sounds. Multiple simultaneous instances can play from the same SoundEffect object without any extra management.
CNA uses SDL3_mixer under the hood. The common formats are supported: WAV, OGG Vorbis, FLAC, and MP3. WAV is recommended for short effects because it needs no decoding step at play time. One gap to know about: .m4a and .aac are not playable at all, because SDL3_mixer ships no AAC decoder.
ContentManager::Load<SoundEffect>
Sound assets are loaded through ContentManager like any other asset. The Load method is templated on the asset type:
#include "Microsoft/Xna/Framework/Audio/SoundEffect.hpp"
#include "Microsoft/Xna/Framework/Content/ContentManager.hpp"
using namespace Microsoft::Xna::Framework::Audio;
using namespace Microsoft::Xna::Framework::Content;
// In LoadContent():
auto explosionSfx = getContentProperty().Load<SoundEffect>("Audio/explosion");
ContentManager appends the extension automatically. For SoundEffect there is exactly one: .wav. Compressed formats such as .ogg, .mp3 and .flac are handled by Song, which streams them, rather than by SoundEffect, which decodes fully into memory.
There is no sound descriptor
A SoundEffect has no sidecar or descriptor file — the WAV is read directly and nothing else is consulted. This matches XNA's model, where volume, pitch and pan are not properties of the asset but of each individual playback, so they are supplied at the call site instead:
// Per-call: volume, pitch, pan
explosionSfx->Play(0.85f, 0.0f, 0.0f);
For anything that needs to persist across a sound's lifetime — including looping — create a SoundEffectInstance and set its properties in code:
// CreateInstance() returns a SoundEffectInstance by value.
auto instance = explosionSfx->CreateInstance();
instance.setVolumeProperty(0.85f);
instance.setIsLoopedProperty(true);
instance.Play();
SoundEffect::Play()
The simplest path is a single fire-and-forget call:
// Fire and forget — volume 1.0, pitch 0.0, pan 0.0
explosionSfx->Play();
// With explicit parameters (volume, pitch, pan)
// volume : 0.0 (silent) … 1.0 (full)
// pitch : -1.0 (one octave down) … 0.0 (normal) … 1.0 (one octave up)
// pan : -1.0 (full left) … 0.0 (centre) … 1.0 (full right)
explosionSfx->Play(0.75f, 0.1f, -0.3f);
Play() returns true if playback started and false if the voice pool was exhausted (SDL3_mixer limit).
SoundEffectInstance for control
When you need to pause, resume, stop, or loop a sound you create a SoundEffectInstance via SoundEffect::CreateInstance(). Each instance owns a dedicated mixing channel.
#include "Microsoft/Xna/Framework/Audio/SoundEffectInstance.hpp"
// Create an instance
auto loopInstance = explosionSfx->CreateInstance();
// Configure before playing
loopInstance->setIsLoopedProperty(true);
loopInstance->setVolumeProperty(0.6f);
loopInstance->setPitchProperty(-0.2f);
loopInstance->setPanProperty(0.0f);
// Play, pause, resume, stop
loopInstance->Play();
loopInstance->Pause();
loopInstance->Resume();
loopInstance->Stop(); // Stop with no fade
loopInstance->Stop(false); // Stop immediately
Checking playback state
SoundState state = loopInstance->getStateProperty();
// SoundState::Playing / SoundState::Paused / SoundState::Stopped
if (state == SoundState::Playing) { ... }
Volume, Pitch, and Pan
All three properties can be changed while a sound is playing — SDL3_mixer applies the change immediately on the next audio buffer callback.
| Property | Type | Range | Notes |
|---|---|---|---|
Volume | Single | 0.0 – 1.0 | Linear amplitude scale |
Pitch | Single | -1.0 – 1.0 | Semitone shift via resampling |
Pan | Single | -1.0 – 1.0 | Left/right stereo position |
A Single is float in the sharp-runtime type system (using Single = float;).
3D Positional Audio
CNA provides a subset of XNA's 3D audio API. You need an AudioListener (the camera / player) and an AudioEmitter (the sound source), then call Apply3D() on a SoundEffectInstance:
#include "Microsoft/Xna/Framework/Audio/AudioListener.hpp"
#include "Microsoft/Xna/Framework/Audio/AudioEmitter.hpp"
AudioListener listener;
listener.Position = Vector3(0.0f, 0.0f, 0.0f);
listener.Forward = Vector3(0.0f, 0.0f, -1.0f);
listener.Up = Vector3(0.0f, 1.0f, 0.0f);
AudioEmitter emitter;
emitter.Position = Vector3(5.0f, 0.0f, -3.0f);
// CreateInstance() returns a SoundEffectInstance by value.
auto sfxInstance = explosionSfx->CreateInstance();
sfxInstance.Apply3D(listener, emitter);
sfxInstance.Play();
CNA computes pan and volume attenuation from the listener-relative position. Full HRTF and distance models are not yet implemented — pan and distance attenuation cover most 2D-style positional needs.
Exactly one listener. XNA's multi-listener Apply3D(const AudioListener*, int, const AudioEmitter&) overload exists, but passing more than one listener throws System::NotSupportedException("Only one listener is supported."). Split-screen games that want per-viewport audio have to mix that themselves.
Complete example: spacebar plays a sound
#include <memory>
#include "Microsoft/Xna/Framework/Game.hpp"
#include "Microsoft/Xna/Framework/Color.hpp"
#include "Microsoft/Xna/Framework/Input/Keyboard.hpp"
#include "Microsoft/Xna/Framework/Input/Keys.hpp"
#include "Microsoft/Xna/Framework/GraphicsDeviceManager.hpp"
#include "Microsoft/Xna/Framework/Graphics/SpriteBatch.hpp"
#include "Microsoft/Xna/Framework/Audio/SoundEffect.hpp"
#include "Microsoft/Xna/Framework/Audio/SoundEffectInstance.hpp"
using namespace Microsoft::Xna::Framework;
using namespace Microsoft::Xna::Framework::Graphics;
using namespace Microsoft::Xna::Framework::Audio;
using namespace Microsoft::Xna::Framework::Input;
class SoundDemo final : public Game {
public:
SoundDemo() : graphics_(this) {}
protected:
void LoadContent() override {
spriteBatch_ = std::make_unique<SpriteBatch>(getGraphicsDeviceProperty());
// One-shot explosion sound
explosion_ = getContentProperty().Load<SoundEffect>("Audio/explosion");
// Looping engine hum
engine_ = getContentProperty().Load<SoundEffect>("Audio/engine_loop");
engineInst_= engine_->CreateInstance();
engineInst_->setIsLoopedProperty(true);
engineInst_->setVolumeProperty(0.4f);
engineInst_->Play();
}
void Update(GameTime& gameTime) override {
auto kb = Keyboard::GetState();
// Play explosion on spacebar press (not hold)
if (kb.IsKeyDown(Keys::Space) && !prevKb_.IsKeyDown(Keys::Space)) {
explosion_->Play(1.0f, 0.0f, 0.0f);
}
// Toggle engine hum with E key
if (kb.IsKeyDown(Keys::E) && !prevKb_.IsKeyDown(Keys::E)) {
if (engineInst_->getStateProperty() == SoundState::Playing)
engineInst_->Pause();
else
engineInst_->Resume();
}
// Pitch-shift with Up/Down arrows (while held)
if (kb.IsKeyDown(Keys::Up)) {
Single p = engineInst_->getPitchProperty();
engineInst_->setPitchProperty(std::min(1.0f, p + 0.01f));
}
if (kb.IsKeyDown(Keys::Down)) {
Single p = engineInst_->getPitchProperty();
engineInst_->setPitchProperty(std::max(-1.0f, p - 0.01f));
}
prevKb_ = kb;
}
void Draw(const GameTime&) override {
auto& gd = getGraphicsDeviceProperty();
gd.Clear(Color::CornflowerBlue);
spriteBatch_->Begin();
// Draw UI text here using a SpriteFont (see Tutorial 7)
spriteBatch_->End();
gd.Present();
}
private:
GraphicsDeviceManager graphics_;
std::unique_ptr<SpriteBatch> spriteBatch_;
std::optional<SoundEffect> explosion_;
std::optional<SoundEffect> engine_;
std::optional<SoundEffectInstance> engineInst_;
KeyboardState prevKb_;
};
int main() { SoundDemo game; game.Run(); }
Common pitfalls
- Calling Play() every frame — guard with a previous-frame key state comparison as shown above, otherwise one keypress spawns 60 overlapping instances per second.
- Destroying a SoundEffect while an instance plays — the
SoundEffectInstanceholds a shared reference. Safe to destroy theSoundEffect; the audio buffer stays alive until the instance stops. - Very short sounds on Android — SDL3_mixer on Android may cut sounds shorter than 100 ms. Pad short effects with silence or use a 0.1 s minimum duration.
- Volume range — XNA uses a linear 0–1 range, not decibels. 0.5 is half amplitude (about -6 dB), not half perceived loudness.