Skip to content

Eliminate WASAPI haptic start/stop click by keeping the stream running#76

Closed
tomaabe wants to merge 1 commit into
microsoft:mainfrom
tomaabe:fix/haptics-wasapi-click
Closed

Eliminate WASAPI haptic start/stop click by keeping the stream running#76
tomaabe wants to merge 1 commit into
microsoft:mainfrom
tomaabe:fix/haptics-wasapi-click

Conversation

@tomaabe

@tomaabe tomaabe commented Jun 8, 2026

Copy link
Copy Markdown

Summary

The Advanced Haptics WASAPI playback path produced an audible click/pop at the start of every clip. This was caused by starting and stopping the IAudioClient (and therefore the haptic actuator) on each play. This change keeps the WASAPI render stream running continuously — mirroring how the XAudio2 path already behaves — so the device is never started/stopped mid-session and the click is eliminated.

Repro steps

  1. Build and run the Advanced Haptics sample.
  2. Connect a controller with haptic support.
  3. Press Play WASAPI (L1) to play any clip.
    • Before this change: an audible click/pop is felt/heard at the very start of playback. Repeating play makes it obvious.
    • Play XAudio2 (R1) does not exhibit the click.

Root cause

The WASAPI path called IAudioClient::Start() on each Play() and IAudioClient::Stop() when the clip ended. Each start/stop drives the actuator through a transient, which is audible as a click. The XAudio2 path does not have this issue because its mastering voice runs continuously and only the source voice starts/stops.

Fix

  • Start the WASAPI render stream once in InitializeDevice and keep it running for the lifetime of the device.
  • Idle time renders silence; Play() installs a sample generator; end-of-clip and Stop() clear the generator instead of stopping the device.
  • Track playback completion with an atomic m_clipActive flag so IsPlaying() reflects the active clip — set false both when the generator reaches EOF and when it is found exhausted on the idle render path. This also keeps the UI's play buttons enabled/disabled correctly.
  • Tear down the render stream and worker thread in the destructor.

Testing

  • Built Debug|x64 with Visual Studio.
  • Verified the start-of-playback click is gone on the WASAPI path, that repeated plays no longer click, and that playback still starts/stops correctly and the play buttons re-enable when a clip finishes.

Notes

  • No public API changes; the change is contained to WASAPIManager.
  • Brings the WASAPI stream lifecycle in line with the existing XAudio2 path.

The Advanced Haptics WASAPI path started the IAudioClient on each Play and
stopped it when the clip ended, so the actuator was driven through a fresh
start/stop every time. That produced an audible click/pop at the start of
playback. The XAudio2 path does not have this problem because its mastering
voice runs continuously and only the source voice starts and stops.

Mirror that lifecycle for WASAPI: start the render stream once in
InitializeDevice and keep it running for the life of the device. Idle time
renders silence, Play installs a sample generator, and end-of-clip/Stop simply
clears the generator instead of stopping the device. Playback completion is now
tracked with an atomic m_clipActive flag so IsPlaying() reflects the active clip
(both when the generator reaches EOF and when it is consumed on the idle path),
which also keeps the UI's play buttons enabled/disabled correctly. The render
stream and worker thread are torn down in the destructor.
@BrianPeekMSFT

Copy link
Copy Markdown

Fixed internally, will be in the next github publish.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants