I'm writing a piano simulator where I continuously send buffers to the WASAPI API. I'm trying to do it in AUDCLNT_SHAREMODE_EXCLUSIVE
mode but I still don't understand how to handle it.
With the code below, I instantiate a separate thread for each call to PlayBuf()
.
The problem is that after instantiating the first thread, if I try to instantiate a second one, the AUDCLNT_E_DEVICE_IN_USE
message appears.
It is certainly my fault as I have not yet understood how to use wasapi in EXCLUSIVE mode.
Thanks
void PlayBuf(short *fileBytes, int fileSize)
{
HRESULT hr;
IMMDeviceEnumerator *deviceEnumerator = NULL;
IMMDevice* audioDevice;
IAudioClient2* audioClient;
WAVEFORMATEX wfx = {};
IAudioRenderClient* audioRenderClient;
UINT32 bufferSizeInFrames;
UINT32 bufferPadding;
int16_t* buffer;
CoInitialize(NULL);
hr = CoCreateInstance(__uuidof(MMDeviceEnumerator),NULL,CLSCTX_ALL, __uuidof(IMMDeviceEnumerator),(LPVOID *)(&deviceEnumerator));
assert (hr == S_OK);
hr = deviceEnumerator->GetDefaultAudioEndpoint(eRender,eConsole,&audioDevice);
assert(hr == S_OK);
deviceEnumerator->Release();
hr = audioDevice->Activate(__uuidof(IAudioClient2),CLSCTX_ALL,NULL,(LPVOID*)(&audioClient));
assert(hr == S_OK);
audioDevice->Release();
wfx.wFormatTag = WAVE_FORMAT_PCM;
wfx.nChannels = 2;
wfx.nSamplesPerSec = 44100;
wfx.wBitsPerSample = 16;
wfx.nBlockAlign = (wfx.nChannels * wfx.wBitsPerSample) / 8;
wfx.nAvgBytesPerSec = wfx.nSamplesPerSec * wfx.nBlockAlign;
const int64_t REFTIMES_PER_SEC = 10000000;
REFERENCE_TIME requestedSoundBufferDuration = REFTIMES_PER_SEC * DurataSuono;
DWORD initStreamFlags = ( AUDCLNT_STREAMFLAGS_RATEADJUST);
hr = audioClient->Initialize(AUDCLNT_SHAREMODE_EXCLUSIVE,initStreamFlags,requestedSoundBufferDuration,0, &wfx, NULL);
assert(hr == S_OK);
hr = audioClient->GetService(__uuidof(IAudioRenderClient),
(LPVOID*)(&audioRenderClient));
assert(hr == S_OK);
hr = audioClient->GetBufferSize(&bufferSizeInFrames);
assert(hr == S_OK);
audioClient->Reset();
hr = audioClient->Start();
assert(hr == S_OK);
hr = audioRenderClient->GetBuffer(fileSize, (BYTE**)(&buffer));
assert(hr == S_OK);
hr = audioRenderClient->ReleaseBuffer(fileSize, 0);
assert(hr == S_OK);
Sleep(2000);
audioClient->Stop();
audioClient->Release();
audioRenderClient->Release();
}
I took an hour to whip up a basic sample for you. It's in C# using my own audio I/O library XT-Audio (so plug intended) but using raw wasapi in C++ it'd probably take me half a day. Anyway i believe this comes really close to what you're looking for. As you see below this app has the world's most awesome GUI:
(source: github.io)
As soon as you press start, the app starts translating keyboard input to audio. You can press & hold the c, d, e, f and g keyboard keys to generate musical notes. It handles multiple overlapping notes (chords), too. I chose to select wasapi shared mode as the backend because it supports floating point audio, but this will work just as well with exclusive mode if you translate the audio to 16-bit integer format.
A difference when working with this library vs raw wasapi, is that the audio thread is managed by the library and the application gets it's audio callback function invoked periodically to synthesize audio data. However this translates easily back to native wasapi using c++: just call IAudioRenderClient::GetBuffer/ReleaseBuffer in a loop on a background thread, and do your processing in between these calls.
Anyway the key part is this: this app only uses 2 threads, one for UI (managed by winforms), and one for audio (managed by the audio library), and yet it is capable of playing multiple musical notes simultaneously, which i believe is at the heart of your question.
I uploaded the full visual studio solution and binaries here: WasapiSynthSample but for completeness i'll post the interesting parts of the code below.
using System;
using System.Threading;
using System.Windows.Forms;
using Xt;
namespace WasapiSynthSample
{
public partial class Program : Form
{
// sampling rate
const int Rate = 48000;
// stereo
const int Channels = 2;
// default format for wasapi shared mode
const XtSample Sample = XtSample.Float32;
// C, D, E, F, G
static readonly float[] NoteFrequencies = { 523.25f, 587.33f, 659.25f, 698.46f, 783.99f };
[STAThread]
static void Main()
{
// initialize audio library
using (var platform = XtAudio.Init(null, IntPtr.Zero, null))
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.ThreadException += OnApplicationThreadException;
AppDomain.CurrentDomain.UnhandledException += OnCurrentDomainUnhandledException;
Application.Run(new Program(platform));
}
}
// pop a messagebox on any error
static void OnApplicationThreadException(object sender, ThreadExceptionEventArgs e)
=> OnError(e.Exception);
static void OnCurrentDomainUnhandledException(object sender, UnhandledExceptionEventArgs e)
=> OnError((Exception)e.ExceptionObject);
static void OnError(Exception e)
{
var text = e.ToString();
if (e is XtException xte) text = XtAudio.GetErrorInfo(xte.GetError()).ToString();
MessageBox.Show(text);
}
XtStream _stream;
readonly XtPlatform _platform;
// note phases
readonly float[] _phases = new float[5];
// tracks key down/up
readonly bool[] _notesActive = new bool[5];
public Program(XtPlatform platform)
{
_platform = platform;
InitializeComponent();
}
// activate note
protected override void OnKeyDown(KeyEventArgs e)
{
base.OnKeyDown(e);
if (e.KeyCode == Keys.C) _notesActive[0] = true;
if (e.KeyCode == Keys.D) _notesActive[1] = true;
if (e.KeyCode == Keys.E) _notesActive[2] = true;
if (e.KeyCode == Keys.F) _notesActive[3] = true;
if (e.KeyCode == Keys.G) _notesActive[4] = true;
}
// deactive note
protected override void OnKeyUp(KeyEventArgs e)
{
base.OnKeyUp(e);
if (e.KeyCode == Keys.C) _notesActive[0] = false;
if (e.KeyCode == Keys.D) _notesActive[1] = false;
if (e.KeyCode == Keys.E) _notesActive[2] = false;
if (e.KeyCode == Keys.F) _notesActive[3] = false;
if (e.KeyCode == Keys.G) _notesActive[4] = false;
}
// stop stream
void OnStop(object sender, EventArgs e)
{
_stream?.Stop();
_stream?.Dispose();
_stream = null;
_start.Enabled = true;
_stop.Enabled = false;
}
// start stream
void OnStart(object sender, EventArgs e)
{
var service = _platform.GetService(XtSystem.WASAPI);
var id = service.GetDefaultDeviceId(true);
using (var device = service.OpenDevice(id))
{
var mix = new XtMix(Rate, Sample);
var channels = new XtChannels(0, 0, Channels, 0);
var format = new XtFormat(in mix, in channels);
var buffer = device.GetBufferSize(in format).current;
var streamParams = new XtStreamParams(true, OnBuffer, null, null);
var deviceParams = new XtDeviceStreamParams(in streamParams, in format, buffer);
_stream = device.OpenStream(in deviceParams, null);
_stream.Start();
_start.Enabled = false;
_stop.Enabled = true;
}
}
// this gets called on the audio thread by the audio library
// but could just as well be your c++ code managing its own threads
unsafe int OnBuffer(XtStream stream, in XtBuffer buffer, object user)
{
// process audio buffer of N frames
for (int f = 0; f < buffer.frames; f++)
{
// compose current sample of all currently active notes
float sample = 0.0f;
for (int n = 0; n < NoteFrequencies.Length; n++)
{
if (_notesActive[n])
{
_phases[n] += NoteFrequencies[n] / Rate;
if (_phases[n] >= 1.0f) _phases[n] = -1.0f;
float noteSample = (float)Math.Sin(2.0 * _phases[n] * Math.PI);
sample += noteSample / NoteFrequencies.Length;
}
}
// write current sample to output buffer
for (int c = 0; c < Channels; c++)
((float*)buffer.output)[f * Channels + c] = sample;
}
return 0;
}
}
}