Tutorial 137: Native Linux Audio with ALSA
What you’ll learn: how to configure an SDL-free ALSA audio build, prove it links no SDL or libasound, pick devices with CNA_AUDIO_DEVICE and CNA_AUDIO_RECORDING_DEVICE, and play a SoundEffect, a Song and microphone capture from one small game.
Before you start — Tutorial 03: Your First CNA Window for the project layout and CMakeLists.txt this tutorial reuses, and Tutorial 14 and Tutorial 15 for SoundEffect, Song and MediaPlayer. You need a Linux machine; ALSA does not exist anywhere else.
Until this snapshot, sound in CNA meant SDL3: the SDL3 audio device plus SDL3_mixer. CNA_AUDIO_PLATFORM=ALSA is a second implementation of the same XNA audio surface that uses no SDL at all. It opens Linux’s own audio interface, ALSA, and mixes with CNA’s own mixer (CnaMixer), so SoundEffect, SoundEffectInstance, DynamicSoundEffectInstance, MediaPlayer, the XACT runtime and Microphone all work in a build that never touches SDL. In this tutorial you configure such a build, prove it is SDL-free, choose an ALSA device with an environment variable, and play a sound effect, a song and read the microphone from one small game.
What is and is not proven. CI runs the ALSA path on a runner with no sound card: the x11-sdl-free-gpu job builds this configuration, checks that the executables link neither SDL nor libasound, and runs the mixer, ALSA device, microphone, category and engine test suites on ALSA’s silent null device. That proves the mixer and the device layer. Nothing in CI listens to real speakers or records from a real microphone, and the cna_demo_sound demo is not among the targets CI builds in this configuration.
How ALSA fits in
| Piece | What it does in the ALSA build |
|---|---|
AlsaAudioDevice | Plays through the ALSA PCM named by CNA_AUDIO_DEVICE (default: default). About 10 ms periods, four buffered, on a playback thread. The device may answer with another rate, channel count or sample format than requested; CNA reports and uses what it chose. |
libasound.so.2 | Loaded at run time, never linked. A machine without it still starts your program; opening the device then fails with a message that says why, which the XNA layer reports as NoAudioHardwareException. Only the ALSA headers are needed to build. |
CnaMixer | Implements the same internal mixer facade as SDL3_mixer: per-track resampling (linear interpolation), gain, a per-track mix callback where the instance’s filter and pan run, then the master mix and a post-mix callback. Loops, pause and per-track stop callbacks follow SDL3_mixer’s semantics; one documented difference is that a track’s stop callback fires after its last audio has passed through the mix callback. |
| Decoders | WAV (PCM 8/16/24/32-bit, IEEE float, MS-ADPCM, IMA-ADPCM) written by CNA; Ogg Vorbis through stb_vorbis; MP3 through dr_mp3; FLAC (native or Ogg) through dr_flac. Formats are recognised by content, not file extension. |
AlsaAudioRecordingDevice | Capture for Microphone. A thread reads the PCM and queues up to eight seconds, so a game that polls once a frame loses nothing between polls. |
Because ALSA is on every Linux system, one backend reaches the whole desktop: on a PipeWire system ALSA’s default device is PipeWire (through pipewire-alsa), on a PulseAudio system it is PulseAudio (through the ALSA PulseAudio plugin), and on a bare system it is the sound card through dmix. CNA does not talk to PipeWire or PulseAudio directly, and there is no OpenAL or WASAPI backend (OPENAL and WASAPI are reserved names that fail configuration).
Prerequisites
This snapshot of CNA is the next branch, and it needs next of sharp-runtime too. With the software renderer used below you do not need easy-gl or meta-gl.
mkdir cna-workspace && cd cna-workspace
git clone -b next https://github.com/libcna/cna.git
git clone -b next https://github.com/libcna/sharp-runtime.git
cd cna
git submodule update --init # non-recursive is correct
Install a compiler (the CI runs GCC 14), CMake 3.20 or newer, pkg-config and the development packages for the X11 window that this walkthrough uses, plus the ALSA headers:
# Debian / Ubuntu
sudo apt install g++ cmake pkg-config \
libx11-dev libxext-dev libasound2-dev
# Fedora: alsa-lib-devel provides the ALSA headers
The list above is what CMake’s X11 check makes mandatory (libX11, libXext and X11/XKBlib.h) plus the ALSA headers; CI installs a larger set, and I have not measured the minimum. The optional X libraries (libxrandr-dev, libxi-dev, libxcursor-dev and so on) each turn on one capability, and Native Platforms lists them. The libasound2 runtime library is what libasound2-dev depends on and is present on any desktop; for PipeWire or PulseAudio routing, also install that sound server’s ALSA plugin. FFmpeg is not needed.
Configure and build
cmake -S . -B build-alsa \
-DCNA_ENABLE_SDL=OFF \
-DCNA_PLATFORM=X11 \
-DCNA_AUDIO_PLATFORM=ALSA \
-DCNA_GRAPHICS_RENDERER=SOFTWARE \
-DCNA_ENABLE_NET=OFF
cmake --build build-alsa --target CnaAudioTests cna_demo_sound --parallel
| Option | Why |
|---|---|
CNA_ENABLE_SDL=OFF | SDL is not downloaded, built, found or linked. Any selection that needs it is refused by name. |
CNA_PLATFORM=X11 | The native Xlib backend. The default SDL3 would be refused by CNA_ENABLE_SDL=OFF. (WAYLAND works the same way; see Tutorial 136.) |
CNA_AUDIO_PLATFORM=ALSA | The subject of this tutorial. Fails at configure time on a non-Linux target, and without the ALSA headers. |
CNA_GRAPHICS_RENDERER=SOFTWARE | A CPU renderer that presents through X11 and needs no GL or Vulkan packages. OPENGL33 (needs the easy-gl and meta-gl siblings) is what CI’s SDL-free GPU job uses. |
CNA_ENABLE_NET=OFF | Optional. Skips the vendored ENet and networking layer, as CI’s SDL-free jobs do. |
Read the configure output. You should see CNA: Using X11 platform implementation, CNA: Using ALSA audio platform implementation and CNA: SDL is NOT configured (CNA_ENABLE_SDL=OFF). If you would rather keep an SDL3 window and only replace the audio, -DCNA_PLATFORM=SDL3 -DCNA_AUDIO_PLATFORM=ALSA (without CNA_ENABLE_SDL=OFF) is accepted by the selection rules; CI does not exercise that pairing.
Mixing SDL2 and SDL3 is refused; ALSA mixes with neither. -DCNA_PLATFORM=SDL2 alone fails, because the audio default is SDL3. The refusal message itself recommends ALSA (or NULL) as an audio platform an SDL2 host can share a process with.
Prove that nothing links SDL or libasound
These are the checks CI runs (its SDL-free jobs use readelf, ldd and find). None of them should print a match:
find build-alsa -iname '*SDL2*' -o -iname '*SDL3*'
ldd build-alsa/CnaAudioTests | grep -i sdl
ldd build-alsa/CnaAudioTests | grep -i asound
The first two prove the SDL-free claim (no SDL artifact was built, and none is loaded, including through CNA’s own libcna.so, which native Linux builds link by default when CMake is 3.27 or newer). The third proves libasound is opened with dlopen at run time rather than linked, so the binary starts on a machine without it.
Run the audio suites on a silent device
Point ALSA at its null device for both playback and capture and run the suites CI runs. No test makes a sound or records a room:
CNA_AUDIO_DEVICE=null CNA_AUDIO_RECORDING_DEVICE=null \
./build-alsa/CnaAudioTests \
--gtest_filter='CnaMixer.*:CnaMixerXna.*:AlsaAudioDevice.*:AlsaAudioRecordingDevice.*:Microphone*:AudioCategory*:*AudioDeviceConformanceTests*:AudioEngine*:RendererDetail*'
These cover the mixer arithmetic (CnaMixer), the XNA facade running on it (CnaMixerXna), the ALSA device and capture sessions, and the XACT engine and categories. ALSA’s null device has no clock and swallows any amount at once, so CNA paces it in real time: a one-second sound still takes one second.
Choosing devices
| Variable | Value | Meaning |
|---|---|---|
CNA_AUDIO_DEVICE | (unset) | The ALSA default PCM — the sound server’s device on PipeWire and PulseAudio systems. |
hw:0,0, plughw:1,0 | A specific card and device. hw: is exact and may refuse a format the hardware lacks; plughw: converts. | |
null | Silent. Everything runs, nothing is audible. What the test suites use. | |
file:FILE=out.raw,FORMAT=raw | Writes exactly what was played to out.raw, in whatever format the device negotiated. | |
CNA_AUDIO_RECORDING_DEVICE | any capture PCM | Exactly that PCM becomes the default microphone. null keeps tests from recording. Unset: ALSA’s default capture PCM first, then every sound card’s capture devices opened through plughw. |
ALSA’s own tools list what exists on your machine: aplay -L for playback PCM names and arecord -L for capture (package alsa-utils). The variables are read when the device is created, so set them in the environment of the process. They are ALSA-only: under the SDL3 audio implementation the equivalent knob is SDL’s own SDL_AUDIODRIVER.
The game
The program below needs no audio file for the sound effect: it synthesises a 200 ms beep as headerless 16-bit PCM and hands it to the SoundEffect(buffer, sampleRate, channels) constructor. For the song, put any Ogg Vorbis, MP3 or FLAC file at Content/music/theme.ogg (for example ffmpeg -f lavfi -i "sine=frequency=440:duration=5" -c:a libvorbis Content/music/theme.ogg if you have the FFmpeg command-line tool). Space plays the beep, M starts, pauses and resumes the song, Escape quits, and the window title shows the microphone’s peak level.
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <exception>
#include <iostream>
#include <optional>
#include <string>
#include <vector>
#include "Microsoft/Xna/Framework/Color.hpp"
#include "Microsoft/Xna/Framework/Game.hpp"
#include "Microsoft/Xna/Framework/GraphicsDeviceManager.hpp"
#include "Microsoft/Xna/Framework/Audio/AudioChannels.hpp"
#include "Microsoft/Xna/Framework/Audio/Microphone.hpp"
#include "Microsoft/Xna/Framework/Audio/NoAudioHardwareException.hpp"
#include "Microsoft/Xna/Framework/Audio/SoundEffect.hpp"
#include "Microsoft/Xna/Framework/Input/Keyboard.hpp"
#include "Microsoft/Xna/Framework/Input/Keys.hpp"
#include "Microsoft/Xna/Framework/Media/MediaPlayer.hpp"
#include "Microsoft/Xna/Framework/Media/Song.hpp"
using namespace Microsoft::Xna::Framework;
using namespace Microsoft::Xna::Framework::Audio;
using namespace Microsoft::Xna::Framework::Input;
using namespace Microsoft::Xna::Framework::Media;
// A 200 ms, 660 Hz mono beep built in memory: headerless 16-bit PCM, no file needed.
static SoundEffect MakeBeep()
{
constexpr int kRate = 44100;
constexpr int kFrames = kRate / 5;
std::vector<SharpRuntime::bytecs> pcm(static_cast<std::size_t>(kFrames) * 2);
for (int i = 0; i < kFrames; ++i)
{
const double fade = 1.0 - static_cast<double>(i) / kFrames;
const auto sample = static_cast<std::int16_t>(
std::sin(2.0 * 3.14159265358979 * 660.0 * i / kRate) * 0.3 * fade * 32767.0);
pcm[static_cast<std::size_t>(i) * 2 + 0] = static_cast<SharpRuntime::bytecs>(sample & 0xFF);
pcm[static_cast<std::size_t>(i) * 2 + 1] = static_cast<SharpRuntime::bytecs>((sample >> 8) & 0xFF);
}
return SoundEffect(pcm, kRate, AudioChannels::Mono);
}
class AlsaAudioDemo final : public Game
{
public:
AlsaAudioDemo() : graphics_(this) {}
protected:
void LoadContent() override
{
try
{
beep_ = MakeBeep();
}
catch (const NoAudioHardwareException& ex) // no libasound, or the PCM cannot be opened
{
std::cerr << "No audio output: " << ex.what() << "\n";
}
try
{
song_ = getContentProperty().Load<Song>("music/theme"); // .mp3 .ogg .wav .flac ...
}
catch (const std::exception& ex)
{
std::cerr << "No song: " << ex.what() << "\n";
}
mic_ = Microphone::getDefaultProperty(); // nullptr when there is none
if (mic_ != nullptr)
{
mic_->setBufferDurationProperty(System::TimeSpan::FromMilliseconds(100));
mic_->BufferReady += [this](System::Object*, const System::EventArgs&) { OnMicBuffer(); };
mic_->Start();
}
}
void UnloadContent() override
{
MediaPlayer::Stop();
if (mic_ != nullptr) { mic_->Stop(); }
}
void Update(GameTime& gameTime) override
{
Game::Update(gameTime); // pumps FrameworkDispatcher: Microphone.BufferReady, MediaPlayer
const KeyboardState kb = Keyboard::GetState();
auto pressed = [&](Keys key) { return kb.IsKeyDown(key) && prev_.IsKeyUp(key); };
if (pressed(Keys::Space) && beep_) { beep_->Play(0.8f, 0.0f, 0.0f); }
if (pressed(Keys::M) && song_)
{
switch (MediaPlayer::getStateProperty())
{
case MediaState::Playing: MediaPlayer::Pause(); break;
case MediaState::Paused: MediaPlayer::Resume(); break;
case MediaState::Stopped: MediaPlayer::setIsRepeatingProperty(true);
MediaPlayer::Play(&*song_); break;
}
}
if (pressed(Keys::Escape)) { Exit(); }
prev_ = kb;
}
void Draw(const GameTime&) override
{
auto& gd = getGraphicsDeviceProperty();
gd.Clear(Color::CornflowerBlue);
// No gd.Present(): Game presents in EndDraw, after Draw() returns.
}
private:
void OnMicBuffer()
{
std::vector<SharpRuntime::bytecs> buf(
static_cast<std::size_t>(mic_->GetSampleSizeInBytes(System::TimeSpan::FromMilliseconds(100))));
const SharpRuntime::intcs got = mic_->GetData(buf); // bytes of mono int16
int peak = 0;
for (SharpRuntime::intcs i = 0; i + 1 < got; i += 2)
{
const auto s = static_cast<std::int16_t>(buf[i] | (buf[i + 1] << 8));
peak = std::max(peak, std::abs(static_cast<int>(s)));
}
getWindowProperty().setTitleProperty("mic peak " + std::to_string(peak * 100 / 32768) + " %");
}
GraphicsDeviceManager graphics_;
std::optional<SoundEffect> beep_;
std::optional<Song> song_;
Microphone* mic_ = nullptr;
KeyboardState prev_;
};
int main()
{
AlsaAudioDemo game;
game.Run();
}
Things worth noticing. Load<Song> returns the Song by value and MediaPlayer::Play takes a Song*, hence the std::optional and &*song_. SoundEffect is move-only, hence MakeBeep() returning by value into an optional. The microphone delivers mono, signed 16-bit at a requested 44,100 Hz, BufferDuration must be 100–1000 ms in steps of 10 ms, and BufferReady is raised from Game::Update through FrameworkDispatcher, so it only fires if your Update calls the base class. Microphone::getDefaultProperty() is nullptr when there is no capture device, which is also what you get if libasound is missing.
Project files
Lay the project out as a sibling of cna/, as in Tutorial 03: alsa-audio-demo/CMakeLists.txt, alsa-audio-demo/main.cpp (the program above) and alsa-audio-demo/Content/music/theme.ogg. The platform, audio and SDL selections are cache variables that must be set before add_subdirectory; a value already set on the command line wins.
cmake_minimum_required(VERSION 3.20)
project(AlsaAudioDemo LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CNA_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../cna")
# SDL-free X11 window + ALSA audio + a CPU renderer
set(CNA_ENABLE_SDL OFF CACHE STRING "")
set(CNA_PLATFORM X11 CACHE STRING "")
set(CNA_AUDIO_PLATFORM ALSA CACHE STRING "")
set(CNA_GRAPHICS_RENDERER SOFTWARE CACHE STRING "")
set(CNA_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(CNA_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
add_subdirectory(${CNA_DIR} ${CMAKE_BINARY_DIR}/cna)
add_executable(AlsaAudioDemo main.cpp)
target_link_libraries(AlsaAudioDemo PRIVATE CNA)
# Ship the Content directory next to the executable
add_custom_command(TARGET AlsaAudioDemo POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_CURRENT_SOURCE_DIR}/Content
$<TARGET_FILE_DIR:AlsaAudioDemo>/Content)
cd alsa-audio-demo
cmake -S . -B build
cmake --build build
cd build
./AlsaAudioDemo # your default output and microphone
CNA_AUDIO_DEVICE=null ./AlsaAudioDemo # everything runs, silently
The default content root is Content, resolved against the working directory, which is why you run from the directory that holds it. To see CNA’s own audio demo on the build from the first half of this tutorial, run cd build-alsa && ./cna_demo_sound; its controls are the ones Tutorial 14 lists.
Honest limits
- Linux only.
CNA_AUDIO_PLATFORM=ALSAis a configure error on any other target. On Windows and macOS there is no SDL-free audio path: withCNA_ENABLE_SDL=OFFonlyNULL(silent) is available there. - Formats. WAV, Ogg Vorbis, MP3 and FLAC. No Opus (an Ogg Opus file is refused with a message naming the formats CNA does play), no WMA/xWMA, no XMA, no AAC or
.m4a. AContentManagername resolving to a.opus,.aacor.wmafile does not make it playable. - Songs sit in memory. A
Songis not pre-decoded, but its compressed bytes are read into memory and decoded as they play. - Resampling is linear interpolation — the same method FAudio, and therefore FNA, uses.
- Hardware evidence is thin. CI uses the
nulldevice. Whether it sounds right on your sound card, over Bluetooth or through your particular PipeWire graph is not something a workflow has checked. - No XNA reverb or HRTF on either mixer; see the Audio reference for the XACT and 3D limits, which are the same on SDL3 and ALSA.
Troubleshooting
| Symptom | Cause and fix |
|---|---|
| Configure fails: “needs the ALSA development headers” | Install libasound2-dev (Debian/Ubuntu) or alsa-lib-devel (Fedora). |
| Configure fails: “CNA_ENABLE_SDL=OFF, but this configuration genuinely requires SDL” | The message names the selections that need SDL. The defaults for platform and audio are both SDL3: set CNA_PLATFORM and CNA_AUDIO_PLATFORM explicitly, and pick a renderer other than SDL_RENDERER, SDL_GPU, FNA3D or FREEDIRECT. |
| Configure fails: “CNA_PLATFORM=X11 was requested but this machine cannot build it” | The message names the missing X package. It never falls back to SDL3. |
NoAudioHardwareException at run time | libasound.so.2 is missing or the PCM cannot be opened; the exception text carries ALSA’s reason. Try CNA_AUDIO_DEVICE=null to separate “the device is wrong” from “the code is wrong”, and aplay -L for valid names. |
| The song does not play | The file is Opus, AAC or WMA, or is not what its extension says. CNA recognises formats by content and names the ones it plays in the error. Convert to Ogg Vorbis. |
Microphone::getDefaultProperty() is nullptr | No capture PCM is enumerated, libasound is missing, or CNA_AUDIO_RECORDING_DEVICE names something that does not exist. |
Where to go next
- Tutorial 127: Choose Platform, Renderer, and Audio Independently — the full rules for combining the axes
- Tutorial 135: A Native X11 Build — the window half of an SDL-free game
- Tutorial 138: Building Without SDL
- Tutorial 118: Procedural audio and Tutorial 120: XACT — both work on ALSA
- Audio reference: implementations and Native Platforms
Known issues in this area
Current defects, gaps and limitations at this snapshot that touch this subject.
- CNA-VGAP-005: The XNA audio facade on CNA's own ALSA mixer is covered by seven facade tests plus the XACT category suite; the SoundEffect, instance, dynamic-instance, Cue, SoundBank and WaveBank suites run only on SDL3 — Under CNA_AUDIO_PLATFORM=ALSA, eight SDL3_mixer-bound suites (about 440 test definitions) are filtered out, so SoundEffect, SoundEffectInstance and DynamicSoundEffectInstance behaviour on CNA's mixer rests on seven facad