Follow-up from #730 (concurrent-voice budget with priority/distance stealing and virtualization).
The gap
Audio::VoiceManager virtualizes a voice by calling IVoiceHost::OnVoiceStop() and brings it back with OnVoiceStart(positionSeconds). The two backends implement that seam very differently:
|
clip path (AudioSource) |
graph path (SoundGraphSound) |
| virtualize |
ma_sound_stop — frees mixer and DSP cost |
mute the graph's Volume input |
| devirtualize |
ma_sound_seek_to_pcm_frame(position) + start |
un-mute |
| resume phase |
exact (sample cursor) |
exact for free (the graph never stopped) |
| reclaims CPU |
yes |
no |
Audio::SoundGraph::SoundGraphSource exposes SendPlayEvent() and nothing else transport-shaped — no stop, no pause, no seek. So SoundGraphSound::OnVoiceStop() can only set its gain scale to 0 and let the graph keep running.
Why it was left this way in #730
This still satisfies #730's acceptance criteria: the audible voice count is capped, graph voices participate in scoring and can steal or be stolen, and resume-in-phase is trivially correct because the graph never stopped advancing.
The tempting "fix" is actively wrong: stopping the graph and re-raising SendPlayEvent() on devirtualization would restart it from its initial state, which is precisely the restart-instead-of-resume bug virtualization exists to prevent (#730 acceptance criterion 2). So muting was the correct call given the current API, not a shortcut.
What this costs
A scene with many SoundGraph voices pays full graph-evaluation cost for every voice past the budget. The budget bounds what you hear, not what the CPU does. On the clip path both are bounded.
What a fix needs
- A transport API on
SoundGraphSource — at minimum suspend/resume; ideally a seek or a "advance N frames without producing output" fast-forward so a suspended voice can be re-synchronised to the logical position VoiceManager maintained while it slept.
SoundGraphSound::OnVoiceStart / OnVoiceStop then use it instead of ApplyEffectiveGain(), and OnVoiceStart finally honours its positionSeconds argument (today it deliberately ignores it — see the comment there).
- A test proving a suspended-and-resumed graph voice lands at the same phase as one that ran continuously. Muting makes this pass trivially today, so the test must be written against the suspend implementation, not before it.
Worth checking whether the graph runtime can even be resumed deterministically — stateful nodes (envelopes, filters, WavePlayer cursors) may need their own save/restore before a suspend is anything other than a restart in disguise. If it can't, that is itself the answer and this issue should be closed as won't-fix with the muting behaviour documented as permanent.
References
OloEngine/src/OloEngine/Audio/SoundGraph/SoundGraphSound.{h,cpp} — the class comment states this limitation; OnVoiceStart/OnVoiceStop/ApplyEffectiveGain
OloEngine/src/OloEngine/Audio/VoiceManager.h — the IVoiceHost contract
docs/agent-rules/audio-voice-budget.md §7 — the comparison table above and the "don't just stop the graph" warning
Follow-up from #730 (concurrent-voice budget with priority/distance stealing and virtualization).
The gap
Audio::VoiceManagervirtualizes a voice by callingIVoiceHost::OnVoiceStop()and brings it back withOnVoiceStart(positionSeconds). The two backends implement that seam very differently:AudioSource)SoundGraphSound)ma_sound_stop— frees mixer and DSP costVolumeinputma_sound_seek_to_pcm_frame(position)+ startAudio::SoundGraph::SoundGraphSourceexposesSendPlayEvent()and nothing else transport-shaped — no stop, no pause, no seek. SoSoundGraphSound::OnVoiceStop()can only set its gain scale to 0 and let the graph keep running.Why it was left this way in #730
This still satisfies #730's acceptance criteria: the audible voice count is capped, graph voices participate in scoring and can steal or be stolen, and resume-in-phase is trivially correct because the graph never stopped advancing.
The tempting "fix" is actively wrong: stopping the graph and re-raising
SendPlayEvent()on devirtualization would restart it from its initial state, which is precisely the restart-instead-of-resume bug virtualization exists to prevent (#730 acceptance criterion 2). So muting was the correct call given the current API, not a shortcut.What this costs
A scene with many SoundGraph voices pays full graph-evaluation cost for every voice past the budget. The budget bounds what you hear, not what the CPU does. On the clip path both are bounded.
What a fix needs
SoundGraphSource— at minimum suspend/resume; ideally a seek or a "advance N frames without producing output" fast-forward so a suspended voice can be re-synchronised to the logical positionVoiceManagermaintained while it slept.SoundGraphSound::OnVoiceStart/OnVoiceStopthen use it instead ofApplyEffectiveGain(), andOnVoiceStartfinally honours itspositionSecondsargument (today it deliberately ignores it — see the comment there).Worth checking whether the graph runtime can even be resumed deterministically — stateful nodes (envelopes, filters,
WavePlayercursors) may need their own save/restore before a suspend is anything other than a restart in disguise. If it can't, that is itself the answer and this issue should be closed as won't-fix with the muting behaviour documented as permanent.References
OloEngine/src/OloEngine/Audio/SoundGraph/SoundGraphSound.{h,cpp}— the class comment states this limitation;OnVoiceStart/OnVoiceStop/ApplyEffectiveGainOloEngine/src/OloEngine/Audio/VoiceManager.h— theIVoiceHostcontractdocs/agent-rules/audio-voice-budget.md§7 — the comparison table above and the "don't just stop the graph" warning