Audio and input architecture
Evidence basis: source-verified at the pinned commit; tests exist (not executed for this page). Claims on this page were checked by reading the CNA source at commit 009d40f5; unless a sentence says otherwise, nothing here was built or executed. The layers and the input flow were read from the audio and input modules, CMake and Game::PollEvents, and cross-checked against CNA's input-backend and ALSA documents; the named tests exist but were not executed for this page, and no audio device or input hardware was exercised.
Audio and input both expose stable, game-facing state while platform-specific code handles devices and events, and both are real-time services: audio is pulled by a device callback on its own thread, and input is pushed once per frame by the game loop. Neither should be collapsed into “whatever SDL does”, because CNA also supports paths with no SDL in them. This page maps the layers, the event-to-snapshot flow that input actually uses at this snapshot, and the threading assumptions each subsystem relies on.
Audio layers
SoundEffect, SoundEffectInstance, DynamicSoundEffectInstance, Microphone, XACT (AudioEngine, WaveBank, SoundBank, Cue)
│ FrameworkDispatcher::Update() pumps streams and media once per Update
▼
MixerEngine (facade, internal header) ← SOUND_ENABLED: exists only for SDL3 and ALSA
├─ SDL3_mixer route src/Backend/Sdl3Mixer (CNA_AUDIO_PLATFORM=SDL3)
└─ CNA's own mixer src/Backend/CnaMixer (CNA_AUDIO_PLATFORM=ALSA; stb_vorbis and dr_libs decoders)
│ both open the selected device through CreateSelectedAudioDevice()
▼
IAudioDevice / IAudioRecordingDeviceProvider (independent of IPlatform)
SDL3 · SDL2 · ALSA · NULL → sound hardware, or a paced silent callback for NULL
Public APIs include SoundEffect, SoundEffectInstance, DynamicSoundEffectInstance, Microphone and the XACT-shaped types. MixerEngine (MixerEngine.hpp) is the internal facade the XNA classes are written against: the build supplies exactly one implementation, either the SDL3_mixer route or CNA's own mixer. CNA_AUDIO_PLATFORM selects the device implementation (SDL3, SDL2, NULL or ALSA; OPENAL and WASAPI are refused), independently of CNA_PLATFORM: a headless application may use SDL3 audio and a graphical one may choose deterministic NULL audio. Audio is not part of IPlatform.
Only SDL3 and ALSA have a mixer, and SOUND_ENABLED is defined for exactly those two. The two mixer backends are the only callers of CreateSelectedAudioDevice(), so at this snapshot the SDL2 and NULL devices are transports that no production playback path opens: with either selected, the XNA facade is compiled without a mixer, SoundEffect::Play() reports false and instances stay stopped, and SDL2 and NULL also have no capture provider. ALSA uses the ALSA device, loads libasound at run time and mixes with CNA's own engine. FrameworkDispatcher::Update() lives in this module but reaches into media and input: it pumps dynamic sound streams, microphone buffers, MediaPlayer and, when a touch device exists, TouchPanel::Update(); that coupling is the reason for the declared audio ↔ media cycle in the module graph.
Decode, voice state, mixing and device-callback lifetimes are distinct. IAudioBufferCallback::FillBuffer may run on a real-time native thread and, by contract, must not block, allocate or throw (avoid logging and any destruction that waits on the callback itself, too). IAudioDevice::Stop() is a callback barrier that waits for an in-flight callback, Close() guarantees the callback never runs again, and neither may be called from inside the callback. Shutdown therefore needs a callback barrier before buffers or voices the callback could still observe are released; AudioDeviceConformanceTests.StartStopAndCloseFormCallbackBarriers pins the device half, and the SDL3 mixer guards its borrowed tracks with a mixer-generation counter. The object-level lifetimes are in the ownership map and audio engine internals.
Native event to public snapshot
IPlatform::PollEvents(batch) each implementation maps native events to PlatformEvent values and keeps
└─ Game::PollEvents, per event its own keyboard / mouse / gamepad / joystick state
├─ PlatformInputBridge::ProcessEvent (always first)
│ ├─ legacy InputManager accumulators (compatibility state for the raw bridge)
│ ├─ mouse-click callbacks, text input and IME editing, device connect / disconnect
│ └─ touch → TouchPanel event map → gesture detector
└─ Game handling: quit, window, lifecycle, drop
end of the batch, once per frame
├─ IPlatformKeyboard::Update() and IPlatformMouse::Update() every frame
└─ IPlatformGamepad::Update() and IPlatformJoystick::Update() only once the gamepad subsystem is up
▼
public queries in Update / Draw: Keyboard::GetState ← keyboard snapshot · Mouse::GetState ← mouse snapshot
GamePad::GetState ← gamepad snapshot · TouchPanel::GetState ← the panel's event map
The keyboard, mouse, gamepad and joystick state that game code reads is published by the platform services, not by the input bridge. PlatformInputBridge::ProcessEvent (SdlInputBridge.cpp) feeds the compatibility accumulators, text input and editing, mouse-click callbacks, device hot-plug notifications and the touch map; then the runtime calls each service's Update() once per frame, after the batch and before Update and Draw. Keyboard::GetState(), Mouse::GetState() and GamePad::GetState() read those stored snapshots and never poll a native API, so repeated reads within a frame return the same value. Touch is the exception in the other direction: TouchPanel accumulates from events itself and TouchPanel::Update() alone advances previous locations and retires releases. Controller and joystick services are updated lazily: Game no longer acquires the controller subsystem at startup, and the update runs only once something has asked for the gamepad or joystick service.
Relative mouse movement is the notable consume-on-read exception. In relative mode Mouse::GetState() calls IPlatformMouse::ConsumeRelativeDelta(), so a second read with no intervening motion returns zero; do not treat every getter as an immutable snapshot. The mapping table and the per-device fidelity notes are in CNA's own input backend document; the test suites live under modules/input/tests (for example the SdlInputBridge* golden, mouse, keyboard, text-input and touch suites and InputResetTests.cpp). Older prose that describes the bridge as the source of the keyboard and mouse snapshot is stale.
Threading assumption
The input pipeline assumes that the game-loop thread owns event processing and snapshot publication. Its process-wide state (the compatibility accumulators, the gesture detector, the touch panel's arrays and queues, the static click and text-input callbacks) is deliberately unsynchronized, and InputManager's own header states the rule: writes come from Game::PollEvents(), reads come from Update() and Draw() on the same thread, and nothing may call the setters or getters from a background thread. Absence of locks is not evidence of arbitrary thread safety. If a platform callback arrives on another thread, it must be marshalled into the platform event path rather than touching input state directly.
Audio is the opposite case: the device callback runs on another thread by design, so shared mixer state is protected (MixerLock for short control updates, a mutex around the dynamic-stream registry that FrameworkDispatcher::Update() snapshots before running each instance) and the callback itself is held to the no-block, no-allocate rule above. The full thread and callback map is in Thread and callback map.
First debugging points
| Symptom | Inspect |
|---|---|
| No native input | the platform's event pump, its capability flags and whether the accessor (GetKeyboard, GetMouse, GetGamepad) returns a service at all |
| Native event exists, public state stale | the end of Game::PollEvents (the service Update() calls), then PlatformInputBridge::ProcessEvent for text, touch and click paths, and the frame snapshot boundary |
| Controller only | lazy subsystem acquisition (IsSubsystemInitialized for the gamepad), device enumeration and update, and the four player-slot mapping |
| Audio starts then crashes | the callback barrier in Stop and Close, voice and buffer lifetime, mixer generation, and backend shutdown ordering |
Silent NULL or SDL2 audio | this is the expected result of the selection: no mixer, so SoundEffect::Play() reports false. Verify CNA_AUDIO_PLATFORM before debugging anything else |
Continue with input internals, audio engine internals and the platform architecture.
Deep dives on this topic
Long-form pages that explain the exact semantics, invariants and evidence behind this subject.
- Gamepads, joysticks, haptics and host power — How CNA maps controllers to the four XNA slots, normalizes and dead-zones axes, what each GamePad extension does, and how the joystick, haptics, power and sensor classes of CNA::Input behave.
- Songs, the media library and video: the public contract — The caller-visible contract of CNA's Song, MediaLibrary, MediaPlayer, visualization data and VideoPlayer, including where file extensions and decoders disagree and how video availability changed.
- Sound effects, streaming, capture and the audio evidence — Caller-visible semantics of SoundEffect, 3D audio, XNB sound decoding, DynamicSoundEffectInstance buffer accounting and Microphone capture in CNA, and what the audio tests do and do not prove.
- Text input and IME composition — The exact TextInputEXT contract in CNA: committed text, IME composition and candidate events, the window-bound text mode, the input-type hint, composition placement and the clipboard classes.
- The input model: snapshots, keys, the mouse and logical coordinates — What Keyboard and Mouse GetState return and when, the Keys numbering and layout helpers, the mouse extensions and cursors, and how renderers map window pixels to logical coordinates.
- Touch panel and gesture semantics — When CNA's touch state advances, which connected flag to trust, TouchCollection and TouchLocation contracts, gesture filtering, timestamps and the deliberate differences from FNA.
- XACT at run time: engine, banks, cues and their limits — The run-time contract of CNA's XACT classes: load failures, cue ownership and registration, AudioEngine::Update, variations, RPC curves, categories, instance limits, filters and wave banks.
Related pages
The same subsystem is explained at four altitudes. These are the neighbouring pages at each one.
- User guide
- Audio: implementations · Platform Support: audio implementations · Input · Game loop: FrameworkDispatcher.Update
- Architecture
- Architecture overview · Platform architecture · Runtime lifecycle · Module graph: declared cycles
- Maintainer workflow
- Thread and callback map · Ownership: caches and asynchronous audio
- Tests and validation
- Test architecture