← All examples
Audio
Audio
One-shot and looping playback
Demonstrates AudioSource/AudioSystem: replaying a short one-shot "blip" immediately even while it's still sounding, toggling a looping ambient hum, adjusting the audio device's master gain, and detaching a source's stream back to the shared pool.
Controls
- SPACE Replay the one-shot blip
- L Toggle the looping ambient hum
- ↑ / ↓ Master volume
- D Detach the blip's stream back to the pool

Run it yourself
build just this example
cmake --build --preset windows-debug --target audio_demo
# binary lands under bin/ (e.g. bin/Debug/audio_demo.exe on Windows)
See Installation for the full build setup.
Source
examples/audio_demo/Game.hpp
#pragma once
#include <ASGE/ASGE.hpp>
// Demonstrates AudioSource/AudioSystem: SPACE (re)plays a short one-shot
// "blip" immediately even while it's still sounding (see AudioSource's
// m_Restart), L toggles a looping ambient hum on/off, UP/DOWN adjust the
// device's master volume (AudioDevice::SetGain), and D detaches the blip's
// stream back to the pool (DetachAudioSource) -- SPACE afterwards creates it
// a fresh stream rather than reusing the detached one. Each on-screen
// rectangle is driven straight from its own AudioSource's live state rather
// than a separate visual timer -- the blip rect is lit while its stream
// still has queued audio, the ambient rect while its m_Playing is true, and
// the volume bar's width tracks m_MasterVolume.
class AudioDemoState final : public asge::game::state::IGameState<int>
{
asge::ecs::Registry& m_Registry;
asge::game::asset::AssetManager& m_Assets;
asge::audio::AudioDevice& m_AudioDev;
asge::ecs::Entity m_Ambient{ asge::ecs::Entity::Null() };
asge::ecs::Entity m_Blip{ asge::ecs::Entity::Null() };
float m_MasterVolume{ 1.0f }; // local copy -- AudioDevice has no getter for SDL's device gain
void SpawnSources();
void AdjustMasterVolume( float inDelta );
public:
AudioDemoState(
asge::ecs::Registry& inRegistry,
asge::game::asset::AssetManager& inAssets,
asge::audio::AudioDevice& inAudioDev );
[[nodiscard]] std::optional<asge::game::state::Transition<int>>
Update(float inDeltaTime, asge::input::InputState const& inInput) override;
void Render(asge::video::IRenderer& inRenderer) override;
[[nodiscard]] asge::graphics::RGBA_Color ClearColor() const noexcept override { return { 18, 18, 24, 255 }; }
void OnSystemEvent(asge::event::SystemEvent const& inSysEvent) override;
};
class AudioDemoGame final : public asge::game::Game<int>
{
public:
explicit AudioDemoGame(asge::video::IRenderer& inRenderer, asge::audio::AudioDevice& inAudioDev);
protected:
[[nodiscard]] std::unique_ptr<StateType> CreateState(int inId) override;
};
examples/audio_demo/Game.cpp
#include "Game.hpp"
#include <algorithm>
namespace
{
using namespace asge::game::components;
constexpr char const* kAmbientPath = "audio/ambient.ogg";
constexpr char const* kBlipPath = "audio/blip.ogg";
constexpr float kVolumeStep = 0.1f;
}
AudioDemoState::AudioDemoState(
asge::ecs::Registry& inRegistry,
asge::game::asset::AssetManager& inAssets,
asge::audio::AudioDevice& inAudioDev
)
: m_Registry(inRegistry), m_Assets(inAssets), m_AudioDev(inAudioDev)
{
SpawnSources();
}
void AudioDemoState::SpawnSources()
{
// Unlike Sprite/Animation, resolving an AudioClip doesn't need a
// renderer -- so both sources are resolved eagerly here instead of
// going through AssetManager::ResolveAssets' deferred per-frame pass.
auto ambient = m_Registry.CreateEntity();
if ( !ambient ) { ambient.LogError(); return; }
m_Ambient = ambient.Value();
AudioSource ambientSource{};
auto ambientClip = m_Assets.GetAudio(kAmbientPath);
if ( !ambientClip ) ambientClip.LogError();
else ambientSource.m_Clip = ambientClip.Value();
auto addedAmbient = m_Registry.AddComponent<AudioSource>(m_Ambient, ambientSource);
if ( !addedAmbient ) { addedAmbient.LogError(); return; }
if ( ambientSource.m_Clip ) PlayAudioSource( addedAmbient.Value().get(), /*inLoop=*/true );
auto blip = m_Registry.CreateEntity();
if ( !blip ) { blip.LogError(); return; }
m_Blip = blip.Value();
AudioSource blipSource{};
auto blipClip = m_Assets.GetAudio(kBlipPath);
if ( !blipClip ) blipClip.LogError();
else blipSource.m_Clip = blipClip.Value();
auto addedBlip = m_Registry.AddComponent<AudioSource>(m_Blip, blipSource);
if ( !addedBlip ) addedBlip.LogError();
}
void AudioDemoState::AdjustMasterVolume(float inDelta)
{
m_MasterVolume = std::clamp(m_MasterVolume + inDelta, 0.0f, 1.0f);
auto result = m_AudioDev.SetGain(m_MasterVolume);
if ( !result ) result.LogError();
}
std::optional<asge::game::state::Transition<int>>
AudioDemoState::Update(
[[maybe_unused]] float inDeltaTime, [[maybe_unused]] asge::input::InputState const& inInput)
{
return std::nullopt;
}
void AudioDemoState::Render(asge::video::IRenderer &inRenderer)
{
bool ambientPlaying = false;
if ( auto ambient = m_Registry.GetComponent<AudioSource>(m_Ambient) )
ambientPlaying = ambient.Value().get().m_Playing;
bool blipSounding = false;
if ( auto blip = m_Registry.GetComponent<AudioSource>(m_Blip) )
{
auto& source = blip.Value().get();
blipSounding = source.m_Stream && source.m_Stream->IsDataAvailable();
}
// "AMBIENT" indicator (left) -- lit while the looping background hum plays.
inRenderer.DrawRect(
asge::math::Rect{ 120.0f, 220.0f, 200.0f, 160.0f },
ambientPlaying ? asge::graphics::RGBA_Color{ 80, 220, 140, 255 } : asge::graphics::RGBA_Color{ 55, 55, 60, 255 },
true
);
// "BLIP" indicator (right) -- lit while its stream still has data queued,
// dark red once D has detached it (SPACE then creates a fresh stream).
bool blipDetached = true;
if ( auto blip = m_Registry.GetComponent<AudioSource>(m_Blip) )
blipDetached = blip.Value().get().m_Stream == nullptr;
inRenderer.DrawRect(
asge::math::Rect{ 480.0f, 220.0f, 200.0f, 160.0f },
blipSounding ? asge::graphics::RGBA_Color{ 240, 200, 60, 255 } :
blipDetached ? asge::graphics::RGBA_Color{ 90, 40, 40, 255 } :
asge::graphics::RGBA_Color{ 55, 55, 60, 255 },
true
);
// Master volume bar (bottom): outline is the full 0..1 range, the fill
// tracks m_MasterVolume -- both driven by AudioDevice::SetGain via
// AdjustMasterVolume, not a separate progress animation.
asge::math::Rect const volumeTrack{ 120.0f, 440.0f, 560.0f, 24.0f };
inRenderer.DrawRect(volumeTrack, asge::graphics::RGBA_Color{ 90, 90, 100, 255 }, false);
inRenderer.DrawRect(
asge::math::Rect{ volumeTrack.m_X, volumeTrack.m_Y, volumeTrack.m_Width * m_MasterVolume, volumeTrack.m_Height },
asge::graphics::RGBA_Color{ 100, 170, 240, 255 },
true
);
}
void AudioDemoState::OnSystemEvent(asge::event::SystemEvent const &inSysEvent)
{
auto const* keyEvent = inSysEvent.TryGet<asge::event::KeyboardEvent>();
if ( !keyEvent ) return;
if ( keyEvent->s_Type != asge::event::EventType::KEYBOARD_KEY_PRESSED ) return;
if ( keyEvent->s_Repeat ) return; // edge-triggered: one toggle/replay per physical press
if ( keyEvent->s_Keycode == asge::input::Keycode::SPACE )
{
auto blip = m_Registry.GetComponent<AudioSource>(m_Blip);
if ( blip ) PlayAudioSource( blip.Value().get(), /*inLoop=*/false );
}
else if ( keyEvent->s_Keycode == asge::input::Keycode::L )
{
auto ambient = m_Registry.GetComponent<AudioSource>(m_Ambient);
if ( !ambient ) return;
auto& source = ambient.Value().get();
if ( source.m_Playing ) StopAudioSource( source );
else PlayAudioSource( source, /*inLoop=*/true );
}
else if ( keyEvent->s_Keycode == asge::input::Keycode::UP )
{
AdjustMasterVolume( kVolumeStep );
}
else if ( keyEvent->s_Keycode == asge::input::Keycode::DOWN )
{
AdjustMasterVolume( -kVolumeStep );
}
else if ( keyEvent->s_Keycode == asge::input::Keycode::D )
{
auto blip = m_Registry.GetComponent<AudioSource>(m_Blip);
if ( !blip ) return;
auto result = DetachAudioSource( m_AudioDev, blip.Value().get() );
if ( !result ) result.LogError();
}
}
AudioDemoGame::AudioDemoGame(asge::video::IRenderer& inRenderer, asge::audio::AudioDevice& inAudioDev)
: Game(inRenderer, inAudioDev)
{
// ASGE_AUDIO_DEMO_ASSET_DIR is injected by CMakeLists.txt; mounted once
// so clips are loaded by virtual path (see SpawnSources) instead of a
// hardcoded OS path baked into this demo.
auto mountResult = m_Vfs.Mount("audio", ASGE_AUDIO_DEMO_ASSET_DIR);
if ( !mountResult ) mountResult.LogError();
SetInitialState(0);
}
std::unique_ptr<AudioDemoGame::StateType> AudioDemoGame::CreateState([[maybe_unused]] int inId)
{
return std::make_unique<AudioDemoState>( m_SceneManager.GetRegistry(), m_Assets, m_AudioDev );
}