Skip to content

Audio: SoundGraph voices can only be muted, not suspended — virtualization does not reclaim DSP cost #745

Description

@drsnuggles8

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

  1. 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.
  2. 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).
  3. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    featureNew feature or requestperformancePerformance / data-oriented optimizationrobustnessProduction hardening / shipping robustness

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions