tggf# PHASE02: Linux Audio - OpenAL Integration
Status: COMPLETE Created: 2026-02-08 Updated: 2026-02-21 Completed: 2026-02-21 (Session 54) Prerequisites: Phase 1 complete ✅ (DXVK + SDL3 + mouse input working)
Replace Miles Sound System with OpenAL for native Linux audio, using jmarshall's implementation as reference while adapting for Zero Hour's expanded audio features.
✅ Spike complete — SDL3 audio vs OpenAL decision made
✅ Phase 1 complete (graphics/input working, menus clickable)
✅ Implementation complete — fighter19 OpenAL pipeline ported, sound confirmed working
✅ PHASE 2 ACCEPTED — Menu music plays on Linux
OpenAL selected over SDL3 audio. See full analysis: docs/WORKDIR/support/SPIKE_AUDIO_SDL3_VS_OPENAL.md
Key reasons:
- SDL3 has no 3D audio — essential for RTS (unit voices, explosions attenuate with distance)
- Fighter19 has 3,490 lines of working Zero Hour OpenAL code — zero reference exists for SDL3 audio
- SDL3 still needs FFmpeg for MP3 — no dependency reduction, worse capabilities
Audio stack (fighter19 pattern):
AudioManager interface (AudioEventRTS, AudioRequest)
↓
OpenALAudioManager ← event scheduling, priority, volume, 3D
↓
OpenALAudioFileCache ← FFmpeg decodes MP3/WAV → PCM → AL buffer
↓
OpenAL (libopenal) ← PCM playback, 3D positioning, listener
Current State (Phase 1):
- ✅ OpenALAudioManager exists (~550 lines)
- ✅ Device/context lifecycle implemented
- ✅ Source pooling ready (32 2D + 128 3D + 4 stream)
⚠️ Audio events stubbed (no actual audio playback)⚠️ Music tracks stubbed (no file loading/streaming)⚠️ 3D audio listener not wired (update() stub)
Phase 2 Focus: Wire OpenAL backend to game audio system, implement audio event tracking, music streaming, and 3D audio positioning.
- Audio event tracking (addAudioEvent with handle management)
- Sound effect playback (gunshots, explosions, unit responses)
- Music streaming (background tracks, victory/defeat themes)
- 3D audio positioning (listener + source positions)
- Audio format conversion (Miles formats → OpenAL formats)
- Volume/pan/pitch controls
- Buffer management and streaming
- Miles→OpenAL API compatibility layer
- Video audio sync (Phase 3)
- EAX/reverb effects (future enhancement)
- Lip-sync/dialogue system (future)
- Network audio sync (future)
Strategy: Port fighter19's complete OpenAL implementation (3,490 LOC across 3 files).
Do NOT build from scratch. Adapt fighter19 to Zero Hour's extended feature set where needed.
- Add
cmake/audio.cmakewith FFmpeg + OpenAL detection - Update Docker image:
apt install -y libopenal-dev libavcodec-dev libavformat-dev libavutil-dev libswresample-dev - Wire
cmake/audio.cmakeinto mainCMakeLists.txtunderSAGE_USE_OPENALguard
- Port
references/fighter19-dxvk-port/GeneralsMD/Code/GameEngineDevice/Source/OpenALAudioDevice/OpenALAudioCache.cpp - Port matching header
- Locate
FFmpegFilewrapper in fighter19 and port it - Test: load one
.wavor.mp3file and print duration
- Port
references/fighter19-dxvk-port/GeneralsMD/Code/GameEngineDevice/Source/OpenALAudioDevice/OpenALAudioStream.cpp - Port matching header
- Minimal changes expected (91 lines, no game-specific logic)
- Replace our 643-line stub with fighter19's 3,098-line implementation
- Adapt includes/namespaces (fighter19 uses
OpenALAudioDevice/subdirectory) - Resolve Zero Hour–specific hooks (fighter19 targets ZH, so diff should be minimal)
- Remove all
fprintf(stderr, "DEBUG:...")leftover from our stub phase
- Launch game → verify menu music plays
- Start skirmish → verify unit response sounds (gunshots, voices)
- Verify no crash on
addAudioEvent/removeAudioEvent - Run for 10+ minutes without audio memory leak
- Confirm Windows builds (vc6/win32) still compile with Miles Sound System
- Confirm
SAGE_USE_OPENALguard correctly excludes OpenAL on Windows
jmarshall Reference Analysis:
- Study
references/jmarshall-win64-modern/Code/Audio/structure - Document MusicManager implementation (streaming patterns)
- Document AudioManager event system (handle tracking)
- Document buffer lifecycle (creation, queueing, cleanup)
- Identify Generals vs Zero Hour differences
- Create adaptation strategy document
Key Files to Analyze (jmarshall):
Code/Audio/
├── AudioManager.cpp # Core audio event system
├── MusicManager.cpp # Background music streaming
├── AudioEventRTS.cpp # RTS-specific audio events
└── ALBuffer.cpp # Buffer management
fighter19 Audio Reference (if applicable):
- Check if fighter19 has any audio improvements beyond stubs
- Document any DXVK-specific audio considerations
Goal: Wire OpenALAudioManager::addAudioEvent() to actual playback
Tasks:
- Create
AudioEventstruct (handle, source, state, 3D params) - Implement handle generation (unique ID per event)
- Implement
m_ActiveEventstracking (std::map<AudioEventHandle, AudioEvent>) - Wire
addAudioEvent():AudioEventHandle addAudioEvent(const AudioEventInfo* info) { // 1. Allocate source from pool // 2. Load buffer (implement loadAudioBuffer) // 3. Set source properties (gain, pitch, position) // 4. alSourcePlay(source) // 5. Return handle }
- Implement
removeAudioEvent(handle):void removeAudioEvent(AudioEventHandle handle) { // 1. Find event in m_ActiveEvents // 2. alSourceStop(source) // 3. Release source to pool // 4. Remove from map }
- Implement
update():void update() { // 1. Poll all active sources (alGetSourcei AL_SOURCE_STATE) // 2. Remove finished one-shot sounds // 3. Update 3D listener position (see Section D) }
Testing:
- Simple test: Load
.wavfile, play on button press - Verify handle lifecycle (create → play → remove)
- Check source pool recycling
Goal: Load game audio files into OpenAL buffers
Tasks:
- Implement
loadAudioBuffer(const char* filename):- Detect format (WAV, MP3, OGG - check game assets)
- Decode audio data (may need libsndfile or similar)
- Create AL buffer:
alGenBuffers(),alBufferData() - Return ALuint buffer ID
- Create buffer cache (avoid reloading same file):
std::map<std::string, ALuint> m_BufferCache;
- Implement buffer cleanup (destructor, shutdown):
void releaseAllBuffers() { for (auto& pair : m_BufferCache) { alDeleteBuffers(1, &pair.second); } m_BufferCache.clear(); }
Audio File Locations (Zero Hour):
- Check
Data/Audio/for.wav,.mp3files - Check if audio is in
.bigarchives (may need decompression) - Document format used by Miles in original game
Dependencies:
- May need: libsndfile, libvorbis, or mpg123 for decoding
- Check CMake dependencies:
find_package(SndFile)
Goal: Update listener and source positions for spatial audio
Tasks:
- Wire listener position in
update():void update() { // Get camera/listener position from game engine Vector3 position = getListenerPosition(); Vector3 velocity = getListenerVelocity(); Vector3 forward = getListenerForward(); Vector3 up = getListenerUp(); alListener3f(AL_POSITION, position.x, position.y, position.z); alListener3f(AL_VELOCITY, velocity.x, velocity.y, velocity.z); ALfloat orientation[] = {forward.x, forward.y, forward.z, up.x, up.y, up.z}; alListenerfv(AL_ORIENTATION, orientation); }
- Implement per-source 3D positioning:
void setAudioEventPosition(AudioEventHandle handle, const Vector3& pos) { AudioEvent* event = findEvent(handle); if (event) { alSource3f(event->source, AL_POSITION, pos.x, pos.y, pos.z); } }
- Add 3D audio parameters to AudioEventInfo:
- Position, velocity, reference distance, max distance
- Rolloff factor for distance attenuation
Game Integration:
- Hook into GameEngine camera updates (listener follows camera)
- Hook into unit positions (gunshots, explosions at unit location)
Goal: Background music playback with track management
Reference: jmarshall's MusicManager.cpp
Tasks:
- Implement streaming source (separate from SFX pool):
ALuint m_MusicSource; ALuint m_MusicBuffers[4]; // 4 buffers for streaming
- Implement
playMusicTrack(const char* filename):- Open file stream (libsndfile or similar)
- Queue initial buffers (
alSourceQueueBuffers) - Start playback (
alSourcePlay) - Set looping behavior
- Implement streaming update loop:
void updateMusicStream() { int buffersProcessed = 0; alGetSourcei(m_MusicSource, AL_BUFFERS_PROCESSED, &buffersProcessed); while (buffersProcessed--) { ALuint buffer; alSourceUnqueueBuffers(m_MusicSource, 1, &buffer); // Read next chunk from file streamMusicData(buffer); // Re-queue buffer alSourceQueueBuffers(m_MusicSource, 1, &buffer); } }
- Implement
nextMusicTrack()/prevMusicTrack():- Stop current track
- Load next from playlist
- Crossfade (optional, Phase 4)
- Wire to game music system (menu themes, battle music, victory/defeat)
Music File Locations (Zero Hour):
- Check
Data/Audio/Music/for tracks - Document playlist system (XML? INI?)
Goal: Minimize game code changes by wrapping Miles API
Strategy (if feasible):
- Create
miles_compat.hwith OpenAL-backed Miles stubs - Map Miles functions to OpenAL equivalents:
// Miles API // OpenAL equivalent AIL_open_stream() → OpenAL streaming source AIL_set_stream_volume() → alSourcef(AL_GAIN) AIL_stream_status() → alGetSourcei(AL_SOURCE_STATE)
- Document unmapped features (EAX, MIDI, etc.)
Alternative Approach:
- Direct replacement: Change game code to call OpenALAudioManager directly
- Pros: Cleaner, no legacy API baggage
- Cons: More game code changes
Decision: Choose based on game code inspection (how tightly coupled is Miles?)
Unit Tests (manual, no automated tests yet):
- Load and play single
.wavfile - Multiple simultaneous sounds (source pool stress test)
- 3D audio (walk around sound source, verify attenuation)
- Music streaming (long track, verify no gaps/stutters)
- Volume controls (master, SFX, music volumes independent)
Integration Tests:
- Launch game, verify menu music plays
- Start skirmish, verify unit response sounds
- Trigger explosions, verify 3D positioning
- Play for 30 minutes, check for audio glitches/leaks
Performance:
- Profile OpenAL overhead (should be <1% CPU)
- Check memory usage (buffer cache size)
- Verify no audio/video desync (Phase 3 dependency)
Tasks:
- Add OpenAL detection to CMake:
if(SAGE_USE_OPENAL) find_package(OpenAL REQUIRED) target_link_libraries(GameEngineDevice PRIVATE OpenAL::OpenAL) endif()
- Add audio decoding library (libsndfile):
find_package(SndFile REQUIRED) target_link_libraries(GameEngineDevice PRIVATE SndFile::sndfile)
- Update Docker build to include dependencies:
RUN apt install -y libopenal-dev libsndfile1-dev
Phase 2 is COMPLETE when:
- Game plays menu music on launch (Linux build) — confirmed 2026-02-21
- Sound effects work (gunshots, explosions, unit voices) — not yet tested in skirmish
- 3D audio positioning works (sounds attenuate with distance) — not yet tested
- Music tracks advance (victory/defeat themes trigger) — not yet tested
- No audio crashes or memory leaks (30-minute stress test) — not yet tested
- Windows builds still use Miles Sound System (no regressions) — pending Windows VM
| Risk | Impact | Mitigation |
|---|---|---|
| Audio format incompatibility | High | Research game archive structure early, test sample files |
| Miles API deeply coupled | Medium | Create compatibility layer or document replacement strategy |
| OpenAL performance issues | Low | Profile early, OpenAL is mature and efficient |
| Buffer memory leaks | Medium | Implement strict buffer lifecycle, stress test |
| 3D audio artifacts | Low | Use reference distances from original game |
- jmarshall OpenAL implementation:
references/jmarshall-win64-modern/Code/Audio/ - OpenAL Programming Guide:
docs/ETC/(download if needed) - Miles Sound System docs:
docs/ETC/(reverse-engineer if no docs available)
- Document Zero Hour audio file locations
- Document audio format (WAV? MP3? Compressed?)
- Document playlist system
- Document 3D audio parameters (reference distances, rolloff)
- Week 1: Research (jmarshall patterns, game audio structure)
- Week 2: Audio event system + buffer management
- Week 3: 3D audio positioning + music streaming
- Week 4: Testing, integration, bug fixes
Total Estimate: 4 weeks (blockers: game audio format discovery, Miles API coupling)
- Updated
OpenALAudioManager.{h,cpp}with full event system - Audio buffer management implementation
- Music streaming system
- CMake dependencies for OpenAL + audio decoding
- Phase 2 test results (manual tests documented)
- Dev blog updates
Prerequisites for Phase 3 (Video Playback):
- Audio system working (Phase 2 complete)
- SDL3 windowing stable (Phase 1 complete)
- Game launches and runs without crashes
Phase 3 Blockers:
- Bink video codec (proprietary, may need alternative)
- Audio/video sync requirements
- Intro video skip mechanism
- jmarshall reference is Generals base game ONLY: Zero Hour may have additional audio features (new unit voices, expanded music system). Adapt accordingly.
- Audio file discovery is critical: If audio is compressed in
.bigarchives, we need decompression first. - OpenAL is mature: Expect fewer issues than DXVK/graphics layer.
- Preserve Windows builds: Miles Sound System must remain functional behind compile guards.
Status Tracking:
- Spike: SDL3 audio vs OpenAL decision (2026-02-21 → OpenAL selected)
- Step 1: CMake + Docker dependencies (FFmpeg pkg_check_modules + libopenal-dev)
- Step 2: OpenALAudioFileCache (FFmpeg decoder) — ported from fighter19
- Step 3: OpenALAudioStream (music streaming) — ported from fighter19
- Step 4: OpenALAudioManager full implementation — 3,099 lines ported, 7 API fixes applied
- Step 5: Integration testing — menu music confirmed working
- Step 6: Windows preservation validation — not yet tested, pending Windows VM
Progress: 6/7 steps complete (Windows validation deferred)