bugfix(audiomanager): Fix dummy audio manager implementation for failing audio length script conditions in headless mode#2313
Conversation
Renamed dummy to 'MilesAudioManagerDummy'.
7751f9e to
477d80b
Compare
|
| Filename | Overview |
|---|---|
| Core/GameEngineDevice/Include/MilesAudioDevice/MilesAudioManager.h | Adds MilesAudioManagerDummy class inheriting MilesAudioManager to fix getFileLengthMS in headless mode; has a logic risk where m_digitalHandle stays null if m_audioOn is false, and a style issue with a 2025-dated comment placed newly in this file. |
| Core/GameEngine/Include/Common/GameAudio.h | Removes the AudioManagerDummy class from the shared core header; the replacement (MilesAudioManagerDummy) lives in the device-layer header as intended. |
| GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp | Replaces inline ternary instantiation of AudioManagerDummy/RadarDummy with calls to createAudioManager(Bool) and createRadar(Bool) factory methods, correctly delegating the dummy-vs-real decision to the concrete engine subclass. |
| GeneralsMD/Code/GameEngineDevice/Include/Win32Device/Common/Win32GameEngine.h | Updates createRadar and createAudioManager signatures and inline implementations to accept a Bool dummy parameter and conditionally return the dummy or real implementation. |
| GeneralsMD/Code/GameEngine/Include/Common/GameEngine.h | Pure virtual signatures for createAudioManager and createRadar updated to accept Bool dummy; straightforward, no issues. |
Sequence Diagram
sequenceDiagram
participant GE as GameEngine::init()
participant W32 as Win32GameEngine
participant MAMD as MilesAudioManagerDummy
participant MAM as MilesAudioManager
participant Miles as Miles Sound System
GE->>W32: createAudioManager(m_headless=true)
W32-->>GE: NEW MilesAudioManagerDummy
GE->>MAMD: initSubsystem / init()
MAMD->>MAM: MilesAudioManager::init()
MAM->>MAMD: openDevice() [inherited, not overridden]
MAMD->>MAM: MilesAudioManager::openDevice()
alt m_audioOn == true
MAM->>Miles: AIL_startup() + AIL_quick_startup()
Miles-->>MAM: m_digitalHandle (valid)
MAM-->>MAMD: device ready
else m_audioOn == false
MAM-->>MAMD: early return, m_digitalHandle stays nullptr
end
Note over GE,Miles: Script condition queries audio length
GE->>MAMD: getFileLengthMS(filename)
MAMD->>MAM: MilesAudioManager::getFileLengthMS() [inherited]
alt m_digitalHandle != nullptr
MAM->>Miles: AIL_open_stream(m_digitalHandle, filename)
Miles-->>MAM: valid stream
MAM-->>GE: actual length in ms ✓
else m_digitalHandle == nullptr
MAM->>Miles: AIL_open_stream(nullptr, filename)
Miles-->>MAM: null stream
MAM-->>GE: 0.0f (fix silently fails) ✗
end
Last reviewed commit: 9432bea
Core/GameEngineDevice/Include/MilesAudioDevice/MilesAudioManager.h
Outdated
Show resolved
Hide resolved
GeneralsMD/Code/GameEngineDevice/Include/Win32Device/Common/Win32GameEngine.h
Outdated
Show resolved
Hide resolved
Core/GameEngineDevice/Include/MilesAudioDevice/MilesAudioManager.h
Outdated
Show resolved
Hide resolved
GeneralsMD/Code/GameEngineDevice/Include/Win32Device/Common/Win32GameEngine.h
Outdated
Show resolved
Hide resolved
Core/GameEngineDevice/Include/MilesAudioDevice/MilesAudioManager.h
Outdated
Show resolved
Hide resolved
|
Addressed feedback. |
| // TheSuperHackers @feature helmutbuhler 17/05/2025 AudioManager that does almost nothing. Useful for headless mode. | ||
| // @bugfix Caball009 16/02/2026 Scripts may require the actual audio file length to function properly, which is important for the CRC computation. |
There was a problem hiding this comment.
Stale date in newly added comment
The @feature comment referencing 17/05/2025 is newly placed in this file (it was moved from GameAudio.h). Per the project convention, newly created comments should not reference dates prior to the current year (2026). Consider updating the year in this line to 2026, or consolidating the two-line header into a single comment block with a 2026 date.
| // TheSuperHackers @feature helmutbuhler 17/05/2025 AudioManager that does almost nothing. Useful for headless mode. | |
| // @bugfix Caball009 16/02/2026 Scripts may require the actual audio file length to function properly, which is important for the CRC computation. | |
| // TheSuperHackers @bugfix helmutbuhler/Caball009 2026 AudioManager that does almost nothing. Useful for headless mode. | |
| // Scripts may require the actual audio file length to function properly, which is important for the CRC computation. | |
| // The Miles AudioManager handles the device opening / closure, so that getFileLengthMS can function as intended. |
Context Used: Rule from dashboard - What: Flag newly created code comments that reference dates prior to the current year (2026).
Why: ... (source)
Prompt To Fix With AI
This is a comment left during a code review.
Path: Core/GameEngineDevice/Include/MilesAudioDevice/MilesAudioManager.h
Line: 333-334
Comment:
**Stale date in newly added comment**
The `@feature` comment referencing `17/05/2025` is newly placed in this file (it was moved from `GameAudio.h`). Per the project convention, newly created comments should not reference dates prior to the current year (2026). Consider updating the year in this line to 2026, or consolidating the two-line header into a single comment block with a 2026 date.
```suggestion
// TheSuperHackers @bugfix helmutbuhler/Caball009 2026 AudioManager that does almost nothing. Useful for headless mode.
// Scripts may require the actual audio file length to function properly, which is important for the CRC computation.
// The Miles AudioManager handles the device opening / closure, so that getFileLengthMS can function as intended.
```
**Context Used:** Rule from `dashboard` - What: Flag newly created code comments that reference dates prior to the current year (2026).
Why: ... ([source](https://app.greptile.com/review/custom-context?memory=fd72a556-4fd8-4db4-8b08-8e51516a64ad))
How can I resolve this? If you propose a fix, please make it concise.| class MilesAudioManagerDummy : public MilesAudioManager | ||
| { | ||
| #if defined(RTS_DEBUG) | ||
| virtual void audioDebugDisplay(DebugDisplayInterface* dd, void* userData, FILE* fp) {} | ||
| #endif | ||
| virtual void stopAudio(AudioAffect which) {} | ||
| virtual void pauseAudio(AudioAffect which) {} | ||
| virtual void resumeAudio(AudioAffect which) {} | ||
| virtual void pauseAmbient(Bool shouldPause) {} | ||
| virtual void killAudioEventImmediately(AudioHandle audioEvent) {} | ||
| virtual void nextMusicTrack() {} | ||
| virtual void prevMusicTrack() {} | ||
| virtual Bool isMusicPlaying() const { return false; } | ||
| virtual Bool hasMusicTrackCompleted(const AsciiString& trackName, Int numberOfTimes) const { return false; } | ||
| virtual AsciiString getMusicTrackName() const { return ""; } | ||
| //virtual void openDevice() {} | ||
| //virtual void closeDevice() {} | ||
| //virtual void* getDevice() { return nullptr; } | ||
| virtual void notifyOfAudioCompletion(UnsignedInt audioCompleted, UnsignedInt flags) {} | ||
| virtual UnsignedInt getProviderCount() const { return 0; }; | ||
| virtual AsciiString getProviderName(UnsignedInt providerNum) const { return ""; } | ||
| virtual UnsignedInt getProviderIndex(AsciiString providerName) const { return 0; } | ||
| virtual void selectProvider(UnsignedInt providerNdx) {} | ||
| virtual void unselectProvider() {} | ||
| virtual UnsignedInt getSelectedProvider() const { return 0; } | ||
| virtual void setSpeakerType(UnsignedInt speakerType) {} | ||
| virtual UnsignedInt getSpeakerType() { return 0; } | ||
| virtual UnsignedInt getNum2DSamples() const { return 0; } | ||
| virtual UnsignedInt getNum3DSamples() const { return 0; } | ||
| virtual UnsignedInt getNumStreams() const { return 0; } | ||
| virtual Bool doesViolateLimit(AudioEventRTS* event) const { return false; } | ||
| virtual Bool isPlayingLowerPriority(AudioEventRTS* event) const { return false; } | ||
| virtual Bool isPlayingAlready(AudioEventRTS* event) const { return false; } | ||
| virtual Bool isObjectPlayingVoice(UnsignedInt objID) const { return false; } | ||
| virtual void adjustVolumeOfPlayingAudio(AsciiString eventName, Real newVolume) {} | ||
| virtual void removePlayingAudio(AsciiString eventName) {} | ||
| virtual void removeAllDisabledAudio() {} | ||
| virtual Bool has3DSensitiveStreamsPlaying() const { return false; } | ||
| virtual void* getHandleForBink() { return nullptr; } | ||
| virtual void releaseHandleForBink() {} | ||
| virtual void friend_forcePlayAudioEventRTS(const AudioEventRTS* eventToPlay) {} | ||
| virtual void setPreferredProvider(AsciiString providerNdx) {} | ||
| virtual void setPreferredSpeaker(AsciiString speakerType) {} | ||
| //virtual Real getFileLengthMS(AsciiString strToLoad) const { return 0.0f; } | ||
| virtual void closeAnySamplesUsingFile(const void* fileToClose) {} | ||
| virtual void setDeviceListenerPosition() {} | ||
| }; |
There was a problem hiding this comment.
getFileLengthMS may silently return 0 if m_audioOn is false
MilesAudioManagerDummy intentionally does not override openDevice() so that MilesAudioManager::openDevice() runs and populates m_digitalHandle. However, MilesAudioManager::openDevice() contains an early return guard:
if (!TheGlobalData->m_audioOn) {
return;
}m_digitalHandle is initialized to nullptr in the constructor and remains nullptr if that guard fires. When getFileLengthMS is subsequently called it reaches:
HSTREAM stream = AIL_open_stream(m_digitalHandle, strToLoad.str(), 0);
if (!stream) {
return 0.0f;
}Because AIL_open_stream with a null HDIGDRIVER will return a null stream, getFileLengthMS silently returns 0.0f — the same value as before this fix. If m_audioOn is false in headless environments (e.g. CI runners without audio hardware), the CRC mismatch that motivated this PR will persist.
Consider either:
- Overriding
openDevice()inMilesAudioManagerDummyto callAIL_startup()+AIL_quick_handles()unconditionally (bypassing them_audioOnguard), or - Documenting explicitly that this fix only works when
m_audioOnistrueand ensuring headless mode forcesm_audioOn = true.
Prompt To Fix With AI
This is a comment left during a code review.
Path: Core/GameEngineDevice/Include/MilesAudioDevice/MilesAudioManager.h
Line: 336-382
Comment:
**`getFileLengthMS` may silently return `0` if `m_audioOn` is false**
`MilesAudioManagerDummy` intentionally does not override `openDevice()` so that `MilesAudioManager::openDevice()` runs and populates `m_digitalHandle`. However, `MilesAudioManager::openDevice()` contains an early return guard:
```cpp
if (!TheGlobalData->m_audioOn) {
return;
}
```
`m_digitalHandle` is initialized to `nullptr` in the constructor and remains `nullptr` if that guard fires. When `getFileLengthMS` is subsequently called it reaches:
```cpp
HSTREAM stream = AIL_open_stream(m_digitalHandle, strToLoad.str(), 0);
if (!stream) {
return 0.0f;
}
```
Because `AIL_open_stream` with a null `HDIGDRIVER` will return a null stream, `getFileLengthMS` silently returns `0.0f` — the same value as before this fix. If `m_audioOn` is `false` in headless environments (e.g. CI runners without audio hardware), the CRC mismatch that motivated this PR will persist.
Consider either:
- Overriding `openDevice()` in `MilesAudioManagerDummy` to call `AIL_startup()` + `AIL_quick_handles()` unconditionally (bypassing the `m_audioOn` guard), or
- Documenting explicitly that this fix only works when `m_audioOn` is `true` and ensuring headless mode forces `m_audioOn = true`.
How can I resolve this? If you propose a fix, please make it concise.
Our current implementation of dummy audio manager is incomplete, because it cannot return the actual length of audio files. Replays will mismatch if they contain scripts that rely on this.
TODO: