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 windowed game that uses ALSA for its sound.
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 platform workflow’s sdl-free job builds an SDL-free Headless + ALSA 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, plus a short smoke run of the 2D demo. That proves the mixer and the device layer. Nothing in CI listens to real speakers or records from a real microphone, no workflow builds the windowed SDL3 + ALSA pairing the game below uses, and the cna_demo_sound demo is not among the targets CI builds.
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 commit c1c316b9 on the apple/m4-stabilization branch, and it needs sharp-runtime on its branch of the same name. With the headless and software renderers used below you do not need easy-gl or meta-gl.
mkdir cna-workspace && cd cna-workspace
git clone -b apple/m4-stabilization https://github.com/libcna/cna.git
git clone -b apple/m4-stabilization https://github.com/libcna/sharp-runtime.git
cd cna
git checkout c1c316b9
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 ALSA headers. The SDL-free build in the first half needs nothing else; the windowed game in the second half also needs the X11 or Wayland development packages for SDL3’s video drivers (see Building):
# Debian / Ubuntu
sudo apt install g++ cmake pkg-config \
libasound2-dev
# Fedora: alsa-lib-devel provides the ALSA headers
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 an SDL-free test tree
The first build is the configuration CNA’s CI builds: no SDL at all, so no window — the Headless platform and renderer — and ALSA for sound. It is enough to run every audio suite.
cmake -S . -B build-alsa -G Ninja \
-DCNA_ENABLE_SDL=OFF \
-DCNA_PLATFORM=HEADLESS \
-DCNA_AUDIO_PLATFORM=ALSA \
-DCNA_GRAPHICS_RENDERER=HEADLESS \
-DCNA_BUILD_TESTS=ON \
-DCNA_ENABLE_NET=OFF
cmake --build build-alsa --target CnaAudioTests --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=HEADLESS | A platform with no window. SDL3 is CNA’s only windowing implementation, so an SDL-free build is a windowless one; the default SDL3 would be refused by CNA_ENABLE_SDL=OFF. |
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=HEADLESS | Validates draw calls without producing pixels, which is all the audio checks below need. |
CNA_ENABLE_NET=OFF | Optional. Skips the vendored ENet and networking layer, as CI’s SDL-free job does. |
Read the configure output. You should see CNA: Using ALSA audio platform implementation and CNA: SDL is NOT configured (CNA_ENABLE_SDL=OFF).
Prove that nothing links SDL or libasound
These are equivalent to the checks CI runs (its SDL-free job uses find, readelf and nm). None of them should print a match:
find build-alsa -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 game opens a window, so it uses the default SDL3 platform and asks only for ALSA audio; SDL3 is still built for the window, and the sound goes through ALSA and CNA’s own mixer rather than SDL3_mixer. The audio and renderer 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")
# An SDL3 window (the default platform) + ALSA audio through CNA's own mixer
set(CNA_AUDIO_PLATFORM ALSA CACHE STRING "")
set(CNA_GRAPHICS_RENDERER OPENGLES3 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, looked for first beside the executable (under TitleLocation.Path) and then in the working directory. CNA’s own audio demo, cna_demo_sound, needs a window too: build it in a tree configured like this game (SDL3 platform, ALSA audio); its controls are the ones Tutorial 14 lists.
Honest limits
- Linux only.
CNA_AUDIO_PLATFORM=ALSAis a configure error on any other target. Elsewhere there is no SDL-free audio path: withCNA_ENABLE_SDL=OFFonlyNULL(silent) is available there. - SDL-free means windowless. A game that needs a window uses the SDL3 platform, so ALSA in a windowed game replaces SDL3_mixer, not SDL itself.
- 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=HEADLESS (or TERMINAL) and CNA_AUDIO_PLATFORM explicitly, and pick a renderer other than SDL_RENDERER, SDL_GPU or FNA3D. A windowed game cannot be SDL-free; leave CNA_ENABLE_SDL at AUTO. |
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: Running on X11 through SDL3 and Tutorial 136: Running on Wayland through SDL3 — the window half of a Linux game
- Tutorial 138: Building Without SDL
- Tutorial 118: Procedural audio and Tutorial 120: XACT — both work on ALSA
- Audio reference: implementations and Windows, X11 and Wayland
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