From f55409080ba7eca9335f918e4c28f9badfd416ac Mon Sep 17 00:00:00 2001 From: tomaabe <19969980+tomaabe@users.noreply.github.com> Date: Mon, 8 Jun 2026 14:35:36 +0900 Subject: [PATCH] Fix WASAPI haptic playback truncating clips when endpoint rate differs 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. --- .../Audio/WaveSampleGenerator.cpp | 47 +++++++++++++++---- 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/Samples/System/Haptics/HapticsManager/Audio/WaveSampleGenerator.cpp b/Samples/System/Haptics/HapticsManager/Audio/WaveSampleGenerator.cpp index f298e62c..fffce7a8 100644 --- a/Samples/System/Haptics/HapticsManager/Audio/WaveSampleGenerator.cpp +++ b/Samples/System/Haptics/HapticsManager/Audio/WaveSampleGenerator.cpp @@ -292,6 +292,21 @@ namespace ATG WaveSampleReader reader(pWaveData, waveSize, pSourceWfx); + // Resample the source to the destination sample rate using nearest-neighbor + // selection. A phase accumulator tracks how many source frames have been consumed + // versus destination frames written: each destination frame maps back to source + // frame (dstFrame / sampleRatio), and the source is advanced until it reaches that + // position. This works for any ratio, including fractional ratios and downsampling. + constexpr uint32_t nMaxChannels = 8; + float currentBlock[nMaxChannels] = {}; + double srcFramesConsumed = 0.0; // number of source frames read so far + double dstFramesWritten = 0.0; // number of destination frames written so far + bool readerEOF = false; + + // Prime the first source frame. + reader.ReadBlock(currentBlock, nMaxChannels); + srcFramesConsumed = 1.0; + for (uint32_t i = 0; i < renderBufferCount; i++) { RenderBuffer* sampleBuffer = new RenderBuffer(); @@ -304,18 +319,23 @@ namespace ATG while (!writer.IsEOF()) { - constexpr uint32_t nMaxChannels = 8; - float channelData[nMaxChannels]; - - // Get a block of channel data (as much as we're interested in). - // If the reader is at end of file then this returns a block - // of zeroed samples. - reader.ReadBlock(channelData, nMaxChannels); - - for (unsigned int j = 0; j < sampleRatio; j++) + // Advance the source until it corresponds to the destination frame we are + // about to write (source frame = dstFrame / sampleRatio). + double neededSrcFrames = (dstFramesWritten + 1.0) / (double)sampleRatio; + while (srcFramesConsumed < neededSrcFrames && !readerEOF) { - writer.WriteBlock(channelData, nMaxChannels); + if (reader.IsEOF()) + { + readerEOF = true; + ZeroMemory(currentBlock, sizeof(currentBlock)); + break; + } + reader.ReadBlock(currentBlock, nMaxChannels); + srcFramesConsumed += 1.0; } + + writer.WriteBlock(currentBlock, nMaxChannels); + dstFramesWritten += 1.0; } *m_SampleQueueTail = sampleBuffer; @@ -348,6 +368,11 @@ namespace ATG CopyMemory(pData, sampleBuffer->Buffer, bytesToRead); m_SampleQueue = m_SampleQueue->Next; + if (m_SampleQueue == nullptr) + { + m_SampleQueueTail = &m_SampleQueue; + } + delete sampleBuffer; // free the consumed buffer (~RenderBuffer frees Buffer) return S_OK; } @@ -364,6 +389,8 @@ namespace ATG { RenderBuffer* sampleBuffer = m_SampleQueue; m_SampleQueue = sampleBuffer->Next; + delete sampleBuffer; // ~RenderBuffer frees Buffer } + m_SampleQueueTail = &m_SampleQueue; } }