Tutorial 14: Playing Sound Effects

Audio  ·  SoundEffect  ·  SoundEffectInstance  ·  3D Audio

ℹ

What you’ll learn

  • Loading a SoundEffect and firing it with Play().
  • When you need a SoundEffectInstance instead, 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 needs an audio implementation that provides a mixer: CNA_AUDIO_PLATFORM=SDL3 (the default, backed by SDL3_mixer) or CNA_AUDIO_PLATFORM=ALSA (native Linux, backed by CNA’s own mixer). Those are the two selections that define SOUND_ENABLED. The NULL selection implements the low-level audio-device boundary but does not provide equivalent SoundEffect playback. See Tutorial 127 and the Audio reference.

CNA implements the XNA 4.0 audio API on top of a mixer — SDL3_mixer in the default build, CNA’s own mixer in the native ALSA build. 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 plays through the mixer of the selected audio implementation. Both mixers handle the common formats: WAV, Ogg Vorbis, FLAC and MP3 (the ALSA mixer also decodes MS-ADPCM and IMA-ADPCM WAV files itself). 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 neither mixer has an AAC decoder, and Opus is not decoded either. The Audio reference has the per-implementation format table.

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(). Load returns the SoundEffect by value (it is move-only).
auto explosionSfx = getContentProperty().Load<SoundEffect>("Audio/explosion");

ContentManager appends the extension automatically (an explicit name such as "Audio/explosion.wav" works too; compiled .xnb and .cnb assets are tried before loose files). For SoundEffect there is exactly one automatic extension: .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 playback-parameter sidecar — the WAV is read directly. (A .cnj envelope can redirect a name to a WAV through its sourceFile field, but it carries no volume, pitch or pan.) 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 it could not start (for example because the instance limit was reached). A pan outside −1…1 throws System::ArgumentOutOfRangeException; pitch is clamped.

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 mixer track.

#include "Microsoft/Xna/Framework/Audio/SoundEffectInstance.hpp"

// Create an instance
auto loopInstance = explosionSfx.CreateInstance();   // by value

// 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 — the mixer applies the change on the next audio buffer callback. (One exception: once an instance is in 3D mode and playing, setPanProperty throws; see the 3D section below.)

PropertyTypeRangeNotes
VolumeSingle0.0 – 1.0Linear amplitude scale
PitchSingle-1.0 – 1.0Octave shift via resampling: -1 is one octave down, +1 one octave up (a value outside the range, or NaN, throws System::ArgumentOutOfRangeException, as in XNA)
PanSingle-1.0 – 1.0Left/right stereo position

A Single is float in the sharp-runtime type system (SharpRuntime::Single); plain float works everywhere in this tutorial.

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.setPositionProperty(Vector3(0.0f, 0.0f, 0.0f));
listener.setForwardProperty(Vector3(0.0f, 0.0f, -1.0f));
listener.setUpProperty(Vector3(0.0f, 1.0f,  0.0f));

AudioEmitter emitter;
emitter.setPositionProperty(Vector3(5.0f, 0.0f, -3.0f));

// CreateInstance() returns a SoundEffectInstance by value.
auto sfxInstance = explosionSfx.CreateInstance();
sfxInstance.Apply3D(listener, emitter);   // aim it before the first Play()
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 counts. XNA’s multi-listener Apply3D(const AudioListener*, int, const AudioEmitter&) overload — and, new in this snapshot, Apply3D(const std::vector<AudioListener>&, const AudioEmitter&) — take any positive listener count. Every listener is evaluated and the nearest one (the one that hears the emitter loudest) decides the attenuation, pan and Doppler, so a split-screen game gets one sensible mix but not a per-viewport mix. A zero count or empty vector throws System::ArgumentOutOfRangeException, a null array System::ArgumentNullException. The 3D audio tutorial has the details, including why you should call Apply3D before the first Play().

Complete example: spacebar plays a sound

#include <algorithm>
#include <memory>
#include <optional>
#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)) {
            float p = engineInst_->getPitchProperty();
            engineInst_->setPitchProperty(std::min(1.0f, p + 0.01f));
        }
        if (kb.IsKeyDown(Keys::Down)) {
            float 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();
        // No gd.Present(): Game presents in EndDraw, after Draw() returns.
    }

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(); }

Try the repository’s audio demo

The repository ships a runnable companion for everything on this page: cna_demo_sound (source in modules/audio/examples/demo_sound). Keys 1–6 fire a click, toggle a looping tone, play a beep with the current volume/pitch/pan, play a 3D-positioned beep and start a DynamicSoundEffectInstance sine generator; the arrow keys and W/S change volume, pan and pitch. It needs a display and, for sound, a working audio device.

cmake --build build --target cna_demo_sound
cd build && ./cna_demo_sound      # the demo's Content/ is copied next to the executable

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 SoundEffectInstance holds a shared reference. Safe to destroy the SoundEffect; 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.
  • Spatialising after Play() — an instance that has been playing without Apply3D is in pan mode and refuses Apply3D with System::InvalidOperationException until it has been stopped. Aim 3D sounds before their first Play().
  • Volume range — XNA uses a linear 0–1 range, not decibels. 0.5 is half amplitude (about -6 dB), not half perceived loudness.