[wdmaudiodev] Re: WASAPI exclusive mode audio render issue

  • From: "Jerry Evans" <jerry@xxxxxxxxxxx>
  • To: <wdmaudiodev@xxxxxxxxxxxxx>
  • Date: Tue, 15 Sep 2015 19:49:09 +0100

Hi Matthew (and indeed anyone else interested),

Attached is the most minimal example I could muster.

Just compile with VS2013/2015. It is completely self-contained. When run it
spins up a single rendering thread that opens the default render client
@44K1/16/stereo. it'll then generate a stereo 440Hz sine tone. That's it.
The proverbial 'hit any key' should clean up and exit.

2 varieties of problem to demonstrate here:

'Ex-wasapi.exe p' shows the effect of calling GetPadding() before
GetBuffer()

i.e. this glitches on a USB device. On the test HDA it simply emits silence
with no apparent errors.

'Ex-wasapi.exe m' fails on HDA as AudioClient->Initialize tries to use the
minimum from GetDevicePeriod().

I have not scoped the HDA output using the default intervals but the tone
sounds OK on rubbish internal laptop speakers.

Hope you get similar failures at your end :)

Jerry.

P.S. My guess is you may not have a M-Audio Delta card so that experiment is
off the table.
/*

Minimal WASAPI exclusive mode glitch demonstration

This is an example and is *not* production code.

It picks the default audio render device and opens in
exclusive mode. Thread priority is set up and
the thread loops rendering a stereo sine tone at 440Hz

N.B. Hardware is expected to be configured for 44K1, 16 bit stereo

Win7+ VS2013 Pro.
*/

#include <windows.h>
#include <stdint.h>
#include <audioclient.h>
#include <mmdeviceapi.h>
#include <avrt.h>
#include <conio.h>
#pragma comment(lib, "avrt.lib")
#pragma comment(lib, "ole32.lib")

//-----------------------------------------------------------------------------
#include <iostream>
#include <vector>
#include <thread>

//-----------------------------------------------------------------------------
// Generate sine tones at 440HZ
class SineGen
{

//-----------------------------------------------------------------------------
inline int16_t toInt16(float arg)
{
return static_cast<int16_t>(arg * 32767.0f);
}

float m_frequency;
float m_amplitude;
float m_dc;
float m_phase;

int m_samplerate;
int m_channels;
int m_bitsPerSample;
//
float m_phaseInc;
float m_twoPI;

public:

//
static double pi() { return 3.14159265358979323846; }

SineGen(float frequency = 440, int sameplerate = 44100, int channels =
2, int bitsPerSample = 16)
: m_frequency(frequency), m_amplitude(0.5), m_dc(0),
m_phase(0.0),
m_samplerate(sameplerate), m_channels(channels),
m_bitsPerSample(bitsPerSample), m_twoPI(pi() * 2)
{
m_phaseInc = 2 * pi()* m_frequency / m_samplerate;
}

//
void assign(float frequency = 440, int sameplerate = 44100, int
channels = 2, int bitsPerSample = 16)
{
m_frequency = frequency;
m_amplitude = 0.5;
m_dc = 0;
m_phase = 0.0;
m_samplerate = (sameplerate);
m_channels = (channels);
m_bitsPerSample = (bitsPerSample);
m_twoPI = (pi() * 2);
m_phaseInc = 2 * pi()* m_frequency / m_samplerate;
}
// generate a sine wave on the fly ...
virtual uint32_t Render(uint8_t* pdata, uint32_t frames)
{
uint32_t ret = 0;
float value = 0;
if (m_bitsPerSample == 16)
{
int16_t* data = reinterpret_cast<int16_t*>(pdata);
for (int frame = 0; frame < frames; frame++)
{
// not something we'd do in the real world
value = m_amplitude * sinf(m_phase);
for (uint32_t channel = 0; channel <
m_channels; channel++)
{
*data++ = toInt16(value);
}
m_phase += m_phaseInc;
}
// ditto
m_phase = fmodf(m_phase, m_twoPI);
ret = frames;
}
return ret;
}

};

//-----------------------------------------------------------------------------
// simplify things here. when set to false thread cleans up and exits ...
static bool run = true;

//-----------------------------------------------------------------------------
// complete audio task
void DoAudio(bool padTest,bool minTest)
{
IMMDevice *pDevice = nullptr;
IMMDeviceEnumerator *pDeviceEnumerator = nullptr;
IAudioClient *pAudioClient = nullptr;
IAudioRenderClient *pAudioRenderClient = nullptr;
HANDLE hEvent = nullptr;
HANDLE hTask = nullptr;
try
{
const int frequency = 880;
const int channels = 2;
const int samplerate = 44100;
const int bitspersample = 16;

// COM result
HRESULT hr = S_OK;

hr = CoInitialize(nullptr);
if (FAILED(hr)) throw std::runtime_error("CoInitialize error");

hr = CoCreateInstance(
__uuidof(MMDeviceEnumerator),
nullptr,
CLSCTX_ALL,
__uuidof(IMMDeviceEnumerator),
(void**)&pDeviceEnumerator);
if (FAILED(hr)) throw std::runtime_error("CoCreateInstance
error");

hr = pDeviceEnumerator->GetDefaultAudioEndpoint(
eRender,
eConsole,
&pDevice);
if (FAILED(hr)) throw
std::runtime_error("IMMDeviceEnumerator.GetDefaultAudioEndpoint error");
std::cout <<
"IMMDeviceEnumerator.GetDefaultAudioEndpoint()->OK" << std::endl;

IAudioClient *pAudioClient = nullptr;
hr = pDevice->Activate(
__uuidof(IAudioClient),
CLSCTX_ALL,
nullptr,
(void**)&pAudioClient);
if (FAILED(hr)) throw std::runtime_error("IMMDevice.Activate
error");
std::cout << "IMMDevice.Activate()->OK" << std::endl;

REFERENCE_TIME DefaultDevicePeriod = 0;
REFERENCE_TIME MinimumDevicePeriod = 0;
hr = pAudioClient->GetDevicePeriod(&DefaultDevicePeriod,
&MinimumDevicePeriod);
if (FAILED(hr)) throw
std::runtime_error("IAudioClient.GetDevicePeriod error");
std::cout << "default device period=" << DefaultDevicePeriod <<
"[nano seconds]" << std::endl;
std::cout << "minimum device period=" << MinimumDevicePeriod <<
"[nano seconds]" << std::endl;

WAVEFORMATEX wave_format = {};
wave_format.wFormatTag = WAVE_FORMAT_PCM;
wave_format.nChannels = channels;
wave_format.nSamplesPerSec = samplerate;
wave_format.nAvgBytesPerSec = samplerate * channels *
bitspersample / 8;
wave_format.nBlockAlign = channels * bitspersample / 8;
wave_format.wBitsPerSample = bitspersample;

REFERENCE_TIME rt = (minTest ? MinimumDevicePeriod :
DefaultDevicePeriod);
hr = pAudioClient->Initialize(
AUDCLNT_SHAREMODE_EXCLUSIVE, // exclusive mode
AUDCLNT_STREAMFLAGS_EVENTCALLBACK,
rt,
rt,
&wave_format,
nullptr);
if (FAILED(hr)) throw
std::runtime_error("IAudioClient.Initialize error");
std::cout << "IAudioClient.Initialize()->OK Period is " << rt
<< "ns" << std::endl;

// event
hEvent = CreateEvent(nullptr, false, false, nullptr);
if (FAILED(hr)) throw std::runtime_error("CreateEvent error");

hr = pAudioClient->SetEventHandle(hEvent);
if (FAILED(hr)) throw
std::runtime_error("IAudioClient.SetEventHandle error");

UINT32 NumBufferFrames = 0;
hr = pAudioClient->GetBufferSize(&NumBufferFrames);
if (FAILED(hr)) throw
std::runtime_error("IAudioClient.GetBufferSize error");
std::cout << "buffer frame size=" << NumBufferFrames <<
"[frames]" << std::endl;

hr = pAudioClient->GetService(
__uuidof(IAudioRenderClient),
(void**)&pAudioRenderClient);
if (FAILED(hr)) throw
std::runtime_error("IAudioClient.GetService error");

// exclusive mode
DWORD taskIndex = 0;
hTask = AvSetMmThreadCharacteristics(TEXT("Pro Audio"),
&taskIndex);
if (hTask == nullptr) throw
std::runtime_error("AvSetMmThreadCharacteristics error");
std::cout << "exclusive mode->OK" << std::endl;

// zero any garbage
BYTE *pData = nullptr;
hr = pAudioRenderClient->GetBuffer(NumBufferFrames, &pData);
if (FAILED(hr)) throw
std::runtime_error("IAudioRenderClient.GetBuffer error");

// zero padding
memset(&pData[0], 0, (NumBufferFrames)* channels *
(bitspersample / 8));

hr = pAudioRenderClient->ReleaseBuffer(NumBufferFrames, 0);
if (FAILED(hr)) throw
std::runtime_error("IAudioRenderClient.ReleaseBuffer error");

hr = pAudioClient->Start();
if (FAILED(hr)) throw std::runtime_error("IAudioClient.Start
error");
std::cout << "IAudioClient.Start()->OK" << std::endl;

//
SineGen sg(440, samplerate, channels, bitspersample);

//
UINT32 framesToProcess = 0;
UINT32 NumPaddingFrames = 0;
// run forever ...
while (run)
{
DWORD dw = WaitForSingleObject(hEvent, INFINITE);
if (dw != WAIT_OBJECT_0)
{
if (FAILED(hr)) throw
std::runtime_error("WaitForSingleObject error");
}

// do this and it'll glitch on USB devices too.
if (padTest)
{
hr =
pAudioClient->GetCurrentPadding(&NumPaddingFrames);
if (FAILED(hr)) throw
std::runtime_error("IAudioClient.GetCurrentPadding error");
}

framesToProcess = NumBufferFrames - NumPaddingFrames;
if (framesToProcess > 0)
{
hr =
pAudioRenderClient->GetBuffer(NumBufferFrames, &pData);
if (FAILED(hr)) throw
std::runtime_error("IAudioRenderClient.GetBuffer error");

// do the sine thing
sg.Render(pData, framesToProcess);

hr =
pAudioRenderClient->ReleaseBuffer(framesToProcess, 0);
if (FAILED(hr)) throw
std::runtime_error("IAudioRenderClient.ReleaseBuffer error");
}
}

hr = pAudioClient->Stop();
if (FAILED(hr)) throw std::runtime_error("IAudioClient.Stop
error");
std::cout << "IAudioClient.Stop()->OK" << std::endl;

}
catch (std::exception& ex)
{
std::cout << "error:" << ex.what() << std::endl;
}

if (hEvent)
{
CloseHandle(hEvent);
}
if (hTask)
{
AvRevertMmThreadCharacteristics(hTask);
}
if (pDeviceEnumerator) pDeviceEnumerator->Release();
if (pDevice) pDevice->Release();
if (pAudioClient) pAudioClient->Release();
if (pAudioRenderClient) pAudioRenderClient->Release();
CoUninitialize();
}

//-----------------------------------------------------------------------------
int main(int argc,char* argv[])
{
// call GetPadding() before we GetBuffer() ?
bool padTest = false;
// use minimum period or the default?
bool minTest = false;
//
for (int i = 0; argv[i] != 0; i++)
{
switch (tolower(argv[i][0]))
{
//
case 'p':
padTest = true;
break;
case 'm':
minTest = true;
break;
}
}
// any command line arg will switch on GetPadding test
std::thread audioThread(DoAudio,padTest,minTest);

// wait for a key
_getch();

// halt thread ...
run = false;

// wait for shutdown
audioThread.join();

return 0;
}

Other related posts: