WaveOut: Target a specific latency to ensure a consistent experience across different sample rates

With WaveOut, typically one defines the following values before starting a WaveOut thread: sample rate, number of blocks, and size of each block in frames. Based on these values, a particular latency will be calculated. StepMania historically follows this approach.

However, it is possible to target a specific latency instead, and extrapolate the needed number of blocks if the sample rate and block size values are known.

This PR refactors the driver to calculate the ideal number of blocks based on a pre-determined optimal target latency and block size. This allows us to guarantee a nearly-imperceptible difference in WaveOut latency (0.29 ms) between 44.1 kHz and 48 kHz.

Nothing related to driver functionality is changed. We just change the way the buffer parameters are determined. This PR only changes code which runs before the WaveOut thread is launched.

When we consider how the WaveOut API works, we need to provide three of four values and the fourth is calculated. So, at 44100 Hz, with 15 blocks of 256 frames each, 15 * 256 gives us 3840 frames of latency (3840 / 44100 = ~87 ms). If we use this same configuration with 48000 Hz, we get a latency of 80ms which is a significant difference.

However, if we increase to 16 blocks at 48000 Hz, we get 4096 frames of latency (~85 ms), which is much closer to the 87 ms when using 15 blocks at 44100 Hz.

A target latency of 118 ms allows for the smallest possible delta when using a block size of 512 frames, and testing with sample rates of 44100 and 48000. At 44100 Hz / 512 frames / 118 ms target latency, we have a calculated latency of 127.7098 ms. At 48000 Hz / 512 frames/ 118 ms target latency, we have a calculated latency of 128 ms - a 0.29 ms difference.

I determined this by simulating the CalculteNumBufferChunks calculation for a range of 100 to 300.
This commit is contained in:
sukibaby
2025-06-20 00:56:55 -07:00
committed by teejusb
parent 054fd4640a
commit d1bddbd3ab
2 changed files with 42 additions and 15 deletions
+38 -13
View File
@@ -20,13 +20,29 @@
REGISTER_SOUND_DRIVER_CLASS( WaveOut );
namespace {
const int CHANNELS = 2;
const int BYTES_PER_FRAME = CHANNELS * 2; // 16 bit
const int BUFFERSIZE_FRAMES = 512 * RageSoundDriver_WaveOut::NUM_BUFFERS; // in frames
const int BUFFERSIZE = BUFFERSIZE_FRAMES * BYTES_PER_FRAME; // in bytes
const int NUM_CHUNKS = RageSoundDriver_WaveOut::NUM_BUFFERS;
const int CHUNKSIZE_FRAMES = BUFFERSIZE_FRAMES / NUM_CHUNKS; // in frames
const int CHUNKSIZE = CHUNKSIZE_FRAMES * BYTES_PER_FRAME; // in bytes
// We want to target a specific latency (118 ms) to ensure a consistent
// experience when using either 44100 or 48000 Hz sample rate.
// This value was chosen because it has the smallest difference in actual
// latency (0.29 ms) between these two 44100 and 48000 Hz.
// To achieve this, we use 512 frames per block and determine the number
// of blocks and buffer size in frames based on the sample rate.
constexpr int kTargetLatency = 118;
// CHANNELS should be renamed CHANNELS
constexpr int CHANNELS = 2;
// CHUNKSIZE_FRAMES should be renamed kBlockSizeFrames
constexpr int CHUNKSIZE_FRAMES = 512;
// BYTES_PER_FRAME should be renamed kBytesPerFrame
constexpr int BYTES_PER_FRAME = CHANNELS * 2; // 16 bit
inline int CalculateNumBlocks( int sampleRate )
{
return (sampleRate * kTargetLatency +
(1000 * CHUNKSIZE_FRAMES - 1)) /
(1000 * CHUNKSIZE_FRAMES);
}
} // namespace
static RString wo_ssprintf( MMRESULT err, const char *szFmt, ...)
@@ -111,13 +127,18 @@ int64_t RageSoundDriver_WaveOut::GetPosition() const
}
RageSoundDriver_WaveOut::RageSoundDriver_WaveOut()
: m_hWaveOut(nullptr)
, m_hSoundEvent(CreateEvent(nullptr, false, true, nullptr))
, m_iLastCursorPos(0)
, m_iSampleRate(0)
, m_bShutdown(false)
, b_InitSuccess(false)
, NUM_CHUNKS(1)
, BUFFERSIZE_FRAMES(0)
, CHUNKSIZE(0)
, m_aBuffers{}
, MixingThread()
{
m_bShutdown = false;
m_iLastCursorPos = 0;
m_hSoundEvent = CreateEvent( nullptr, false, true, nullptr );
m_hWaveOut = nullptr;
}
RString RageSoundDriver_WaveOut::Init()
@@ -129,6 +150,10 @@ RString RageSoundDriver_WaveOut::Init()
m_iSampleRate = kFallbackSampleRate;
}
NUM_CHUNKS = CalculateNumBlocks( m_iSampleRate );
BUFFERSIZE_FRAMES = CHUNKSIZE_FRAMES * NUM_CHUNKS;
CHUNKSIZE = CHUNKSIZE_FRAMES * BYTES_PER_FRAME;
WAVEFORMATEX fmt;
fmt.wFormatTag = WAVE_FORMAT_PCM;
fmt.nChannels = CHANNELS;
+4 -2
View File
@@ -18,7 +18,6 @@ public:
int64_t GetPosition() const;
float GetPlayLatency() const;
int GetSampleRate() const { return m_iSampleRate; }
static const int NUM_BUFFERS = 32;
private:
static int MixerThread_start( void *p );
void MixerThread();
@@ -28,11 +27,14 @@ private:
HWAVEOUT m_hWaveOut;
HANDLE m_hSoundEvent;
WAVEHDR m_aBuffers[NUM_BUFFERS];
WAVEHDR m_aBuffers[32]; // Maximum of 32 output blocks (frame blocks) allowed.
int m_iSampleRate;
bool m_bShutdown;
int m_iLastCursorPos;
bool b_InitSuccess;
int NUM_CHUNKS; // TODO rename wo_num_blocks
int BUFFERSIZE_FRAMES; // TODO rename wo_num_frames_per_block
int CHUNKSIZE; // TODO rename wo_block_size
};
#endif