Skip to content

Fix WASAPI haptic playback truncating clips when endpoint sample rate differs from source#75

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

Fix WASAPI haptic playback truncating clips when endpoint sample rate differs from source#75
tomaabe wants to merge 1 commit into
microsoft:mainfrom
tomaabe:fix/haptics-wasapi-resampler-truncation

Conversation

@tomaabe

@tomaabe tomaabe commented Jun 8, 2026

Copy link
Copy Markdown

Summary

Fixes Advanced Haptics WASAPI playback for WAV files when the haptic audio endpoint sample rate is not an integer multiple of the source WAV sample rate. The previous resampling loop could under/overproduce destination frames and silently drop the tail of longer clips. This change also fixes RenderBuffer leaks in the sample queue cleanup paths.

Repro steps

  1. Build the Advanced Haptics sample.
  2. Connect a controller whose haptic audio endpoint sample rate differs from 48 kHz.
  3. Select a multi-second WAV clip, such as an 8-beat 48 kHz clip.
  4. Press Play WASAPI (L1).
    • Before this fix: only the beginning plays/vibrates, for example only the first beat.
  5. Press Play XAudio2 (R1).
    • XAudio2 playback works because XAudio2 performs resampling natively.

Root cause

WaveSampleGenerator::GenerateSampleBuffer computed sampleRatio as a float, but used it as the upper bound of an integer loop:

for (unsigned int j = 0; j < sampleRatio; j++)
{
    writer.WriteBlock(channelData, nMaxChannels);
}

For non-integer ratios, this writes a whole-integer number of destination blocks per source block. As a result, the pre-sized render buffers can be exhausted before all source frames are consumed, silently truncating the tail of longer clips.

Fix

  • Replaced the integer-loop resampler with a phase-accumulator nearest-neighbor resampler.
    • The source position advances by exactly 1 / sampleRatio source frames per destination frame.
    • Fractional ratios and downsampling are handled correctly.
  • Fixed RenderBuffer ownership cleanup:
    • FillSampleBuffer now deletes each consumed RenderBuffer.
    • Flush now deletes all remaining queued RenderBuffer objects and restores the queue tail pointer.

Testing

  • Built Debug|x64 with Visual Studio.
  • Verified an 8-beat 48 kHz clip now plays fully through both Play WASAPI (L1) and Play XAudio2 (R1).

Notes

  • Nearest-neighbor resampling is consistent with the intent and complexity level of this sample.
  • No public API changes.
  • Single-file change in Samples/System/Haptics/HapticsManager/Audio/WaveSampleGenerator.cpp.

…s from source

The WASAPI playback path in the Advanced Haptics sample resamples the source
WAV to the haptic audio endpoint's sample rate in
WaveSampleGenerator::GenerateSampleBuffer. The previous code wrote a
whole-integer number of destination blocks per source block via
for (unsigned int j = 0; j < sampleRatio; j++), where sampleRatio is a
float. For any non-integer ratio (endpoint rate != source rate) the block
counts are wrong and the writer reaches the end of the pre-sized render
buffers before all source frames are consumed, so the tail of longer clips
is silently dropped. Symptom: on Play WASAPI (L1) a multi-second clip only
plays/vibrates at the very beginning; Play XAudio2 (R1) is unaffected
because XAudio2 resamples natively.

Replace the integer loop with a phase-accumulator nearest-neighbor resampler
that advances the source by 1 / sampleRatio source frames per destination
frame, correctly handling fractional and downsampling ratios.

Also fix two memory leaks in the same file: FillSampleBuffer now frees each
consumed RenderBuffer, and Flush now frees the remaining RenderBuffers and
restores the queue tail pointer.
@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