Portaudio device

I have written an AudioIODeviceType implementation for portaudio devices. My motivation was to test portaudio’s WDM/KS implementation. It may also be useful for testing other hostapis and compare them with the juce implementation – for example portaudio has WASAPI in exclusive mode, and also ALSA soft-devices . The problem with portaudio is that it does not provide a list of buffersizes, and does not provides the current buffer size outside of audiocallbacks. Although the code should support audio input, I have only tested audio output, so it is quite likely that there are bugs with audio input. The code is public domain, you can do whatever you want with it:

[code]#include <portaudio.h>
#include <juce.h>

namespace {

struct DeviceInfo {
PaDeviceIndex index;
const PaDeviceInfo *pa_info;
DeviceInfo() : index(-1), pa_info(0) {}
bool empty() const { return pa_info == 0; }
// the PA devs have decided to use uft8 inside all strings – this is at least the case for the wdmks stuff.
String name() const { return pa_info ? String::fromUTF8(pa_info->name) : String::empty; }
};

}

class PAAudioIODevice : public AudioIODevice {
const PaHostApiInfo *hostapi_info;
DeviceInfo input_di, output_di;

Array sample_rates, buffer_sizes;
int default_buffer_size;
int default_sample_rate;

int current_sample_rate;
int current_buffer_size;

BigInteger channels_in, channels_out;

String last_error;
PaStream pa_stream;
AudioIODeviceCallback
callback;
bool started;
public:
StringArray getOutputChannelNames() {
StringArray outChannels;
if (output_di.pa_info >= 0) {
for (int i = 1; i <= output_di.pa_info->maxOutputChannels; ++i)
outChannels.add("Output channel " + String (i));
}
return outChannels;
}

StringArray getInputChannelNames() {
StringArray inChannels;
if (input_di.pa_info) {
for (int i = 1; i <= input_di.pa_info->maxInputChannels; ++i)
inChannels.add ("Input channel " + String (i));
}
return inChannels;
}

int getNumSampleRates() { return sample_rates.size(); }
double getSampleRate (int index) { return sample_rates[index]; }
int getNumBufferSizesAvailable() { return buffer_sizes.size(); }
int getBufferSizeSamples (int index) { return buffer_sizes[index]; }
int getDefaultBufferSize() { return default_buffer_size; }

int getCurrentBufferSizeSamples() { return current_buffer_size; }
double getCurrentSampleRate() { return current_sample_rate; }
int getCurrentBitDepth() { return 32; }
int getOutputLatencyInSamples() { return 0; }
int getInputLatencyInSamples() { return 0; }
BigInteger getActiveOutputChannels() const { return channels_out; }
BigInteger getActiveInputChannels() const { return channels_in; }
String getLastError() { return last_error; }

PAAudioIODevice(const String &device_name, const String &type_name,
const PaHostApiInfo *info, const DeviceInfo *p_input_di, const DeviceInfo *p_output_di)
: AudioIODevice(device_name, type_name)
{
hostapi_info = info;
if (p_input_di)
input_di = *p_input_di;
if (p_output_di)
output_di = *p_output_di;
default_buffer_size = 0;
current_sample_rate = 0;
current_buffer_size = 0;
pa_stream = 0;
callback = 0;
started = false;
}

~PAAudioIODevice() {
close();
}

void initPaStreamParameters(PaStreamParameters &in, PaStreamParameters &out) {
memset(&in, 0, sizeof in); memset(&out, 0, sizeof out);

if (input_di.pa_info) {
  in.device = input_di.index;
  in.channelCount = input_di.pa_info->maxInputChannels;
  in.sampleFormat = paFloat32 | paNonInterleaved;
  in.suggestedLatency = input_di.pa_info->defaultLowOutputLatency;
  in.hostApiSpecificStreamInfo = 0;
}

if (output_di.pa_info) {
  out.device = output_di.index;
  out.channelCount = output_di.pa_info->maxOutputChannels;
  out.sampleFormat = paFloat32 | paNonInterleaved;
  out.suggestedLatency = output_di.pa_info->defaultLowOutputLatency;
  out.hostApiSpecificStreamInfo = 0;
}

}

String open (const BigInteger& inputChannels, const BigInteger& outputChannels,
double sampleRate, int bufferSizeSamples) {
close();
last_error = String::empty;

if (sample_rates.size() == 0 && input_di.pa_info && output_di.pa_info) {
  last_error = "The input and output devices don't share a common sample rate!";
  return last_error;
}

current_buffer_size = bufferSizeSamples <= 0 ? default_buffer_size : jmax (bufferSizeSamples, buffer_sizes[0]);
current_sample_rate = sampleRate > 0 ? sampleRate : default_sample_rate;

PaStreamParameters in, out;
initPaStreamParameters(in, out);
if (input_di.pa_info && !inputChannels.isZero()) {
  in.channelCount = jmin(inputChannels.getHighestBit() + 1, in.channelCount);
  channels_in = inputChannels; 
  channels_in.setRange(in.channelCount, channels_in.getHighestBit() + 1 - in.channelCount, false);
  in.suggestedLatency = current_buffer_size / (double)jmax(current_sample_rate, 1);
} else channels_in = 0;

if (output_di.pa_info && !outputChannels.isZero()) {
  out.channelCount = jmin(outputChannels.getHighestBit() + 1, out.channelCount);
  channels_out = outputChannels; 
  channels_out.setRange(out.channelCount, channels_out.getHighestBit() + 1 - out.channelCount, false);
  out.suggestedLatency = current_buffer_size / (double)jmax(current_sample_rate, 1);
} else channels_out = 0;

PaError err;

jassert(pa_stream == 0);
err = Pa_OpenStream(&pa_stream, 
                    (channels_in.isZero() ? 0 : &in), 
                    (channels_out.isZero() ? 0 : &out), 
                    current_sample_rate, 0 /*current_buffer_size*/, paNoFlag,
                    &streamCallback_static, this);
if (err != paNoError) {
  last_error = Pa_GetErrorText(err);
  return last_error;
}

const PaStreamInfo *stream_info = Pa_GetStreamInfo(pa_stream);
if (stream_info) {
  current_sample_rate = stream_info->sampleRate;
  if (!sample_rates.contains(current_sample_rate))
    sample_rates.addUsingDefaultSort(current_sample_rate);

  // for this part to be really correct, it would require that portaudio uses only the 'framesPerBuffer'
  // stuff when computing the latency. Unfortunately it is not always the case, for example the wdm/ks 
  // driver adds hwFifoLatency to framesPerBuffer when computing outputLatency ...
  int buffer_size = stream_info->outputLatency * stream_info->sampleRate;
  if (buffer_size > 0 && buffer_size < 65536 && buffer_size != current_buffer_size) {
    current_buffer_size = buffer_size;
    if (!buffer_sizes.contains(current_buffer_size))
      buffer_sizes.addUsingDefaultSort(current_buffer_size);
  }

}

return last_error;

}

void close() {
if (pa_stream) {
stop();
PaError err = Pa_CloseStream(pa_stream);
if (err != paNoError) {
last_error = Pa_GetErrorText(err);
}
pa_stream = 0;
}
}

bool isOpen() { return pa_stream != 0; }
bool isPlaying() { return pa_stream != 0 && Pa_IsStreamActive(pa_stream); }

void start(AudioIODeviceCallback* call) {
if (isOpen() && call != nullptr && !Pa_IsStreamActive(pa_stream)) {
callback = call;
callback->audioDeviceAboutToStart(this);
started = true;
PaError err = Pa_StartStream(pa_stream);
if (err != paNoError) {
last_error = Pa_GetErrorText(err);
callback->audioDeviceStopped();
callback = 0;
}
}
}

void stop() {
if (Pa_IsStreamActive(pa_stream)) {
PaError err = Pa_StopStream(pa_stream);
started = false;
if (err != paNoError) {
last_error = Pa_GetErrorText(err);
}
if (callback) { callback->audioDeviceStopped(); }
callback = 0;
}
}

bool initialise() {
updateSupportedBufferSizes();
updateSupportedSampleRates();
return sample_rates.size() && buffer_sizes.size();
}

private:
void updateSupportedSampleRates() {
sample_rates.clear();
bool default_sample_rate_found = false;
DeviceInfo &di = (input_di.pa_info ? input_di : output_di);
jassert(di.pa_info);
default_sample_rate = di.pa_info->defaultSampleRate;
float freqs[] = { 44100, 48000, 88200, 96000, 176400, 192000, 0 };
for (size_t i=0; freqs[i]; ++i) {
double f = freqs[i];
PaStreamParameters in, out;
initPaStreamParameters(in, out);
PaError err = Pa_IsFormatSupported(input_di.pa_info ? &in : 0,
output_di.pa_info ? &out : 0, f);
if (err == paFormatIsSupported) {
sample_rates.add(f);
if (std::abs(default_sample_rate - f) < 1.0)
default_sample_rate_found = true;
}
}
if (!default_sample_rate_found)
sample_rates.add(default_sample_rate);
DefaultElementComparator cmp;
sample_rates.sort(cmp);
xassert(sample_rates.size());
}

void updateSupportedBufferSizes() {
buffer_sizes.clear();
buffer_sizes.add(32);
buffer_sizes.add(64);
buffer_sizes.add(96);
buffer_sizes.add(128);
buffer_sizes.add(192);
buffer_sizes.add(256);
buffer_sizes.add(384);
buffer_sizes.add(512);
buffer_sizes.add(768);
buffer_sizes.add(1024);
default_buffer_size = 256;
}

static int streamCallback_static(const void input, void output,
unsigned long frameCount,
const PaStreamCallbackTimeInfo
timeInfo,
PaStreamCallbackFlags statusFlags,
void userData) {
return ((PAAudioIODevice
)userData)->streamCallback((const float
*)input, (float**)output,
frameCount, timeInfo, statusFlags);
}

// data was requested with the paNonInterleaved flag => it is given as an array of pointers.
int streamCallback(const float **input, float **output,
unsigned long frame_count,
const PaStreamCallbackTimeInfo *,
PaStreamCallbackFlags) {
if (callback) {
jassert(started);
int num_input_buffers = 0, num_output_buffers = 0;
const float *in[64];
float *out[64];
if (!channels_in.isZero() && input != 0) {
for (int i=0; i <= channels_in.getHighestBit() && num_input_buffers < 64; ++i) {
if (channels_in[i]) in[num_input_buffers++] = input[i];
}
xassert(num_input_buffers == 64 || num_input_buffers == channels_in.countNumberOfSetBits());
}

  if (!channels_out.isZero() && output != 0) {
    for (int i=0; i <= channels_out.getHighestBit() && num_output_buffers < 64; ++i) {
      if (channels_out[i]) out[num_output_buffers++] = output[i];
    }
    xassert(num_output_buffers == 64 || num_output_buffers == channels_out.countNumberOfSetBits());
  }
  callback->audioDeviceIOCallback (in, num_input_buffers, out, num_output_buffers, frame_count);
}
return paContinue;

}

};

class PAAudioIODeviceType : public AudioIODeviceType
{
PaHostApiTypeId hostapi_type;
PaHostApiIndex hostapi_index;
bool pa_initialized;
const PaHostApiInfo *hostapi_info;
std::vector devices;
StringArray output_devices_names, input_devices_names;
public:
PAAudioIODeviceType(const String &hostapi_id, const String &hostapi_name) :
AudioIODeviceType(hostapi_name)
{
pa_initialized = (Pa_Initialize() == paNoError);

if (hostapi_id == "WASAPI") hostapi_type = paWASAPI;
else if (hostapi_id == "WDMKS") hostapi_type = paWDMKS;
else if (hostapi_id == "MME") hostapi_type = paMME;
else if (hostapi_id == "ASIO") hostapi_type = paASIO;
else if (hostapi_id == "DS") hostapi_type = paDirectSound;
else if (hostapi_id == "ALSA") hostapi_type = paALSA;
else if (hostapi_id == "OSS") hostapi_type = paOSS;
else xassert(0);

hostapi_index = -1;  
hostapi_info = 0;  // init by scanForDevices

if (pa_initialized) {
  // if hostapi_index <= 0 , that means portaudio has not been built with support for that hostapi.
  hostapi_index = Pa_HostApiTypeIdToHostApiIndex(hostapi_type);
}

}

~PAAudioIODeviceType() {
if (pa_initialized) {
Pa_Terminate();
}
}

bool isHostApiAvailable() const { return hostapi_index >= 0; }

void scanForDevices() {
if (hostapi_info == 0 && pa_initialized) {
if (hostapi_index >= 0) {
// pointer returned is owned by PA, valid until Pa_Terminate is called
hostapi_info = Pa_GetHostApiInfo(hostapi_index);
if (hostapi_info) xassert(hostapi_info->type == hostapi_type);
}
}
devices.clear();
output_devices_names.clear();
input_devices_names.clear();
if (hostapi_info) {
for (int idx = 0; idx < hostapi_info->deviceCount; ++idx) {
DeviceInfo di;
di.index = Pa_HostApiDeviceIndexToDeviceIndex(hostapi_index, idx);
if (di.index < 0) continue; // there was an error…

    di.pa_info = Pa_GetDeviceInfo(di.index);
    if (di.pa_info) {
      devices.push_back(di);
      if (di.pa_info->maxOutputChannels > 0) {
        output_devices_names.add(di.name());
      }
      if (di.pa_info->maxInputChannels > 0) {
        input_devices_names.add(di.name());
      }
    }
  }
}

}

StringArray getDeviceNames (bool wantInputNames) const {
return wantInputNames ? input_devices_names : output_devices_names;
}

int getDefaultDeviceIndex(bool forInput) const {
if (hostapi_info) {
for (size_t i=0; i < devices.size(); ++i) {
if (forInput) {
if (devices[i].index == hostapi_info->defaultInputDevice) {
return input_devices_names.indexOf(devices[i].name());
}
} else {
if (devices[i].index == hostapi_info->defaultOutputDevice) {
return output_devices_names.indexOf(devices[i].name());
}
}
}
}
return 0;
}

int getIndexOfDevice (AudioIODevice* device, bool asInput) const
{
PAAudioIODevice* const d = dynamic_cast <PAAudioIODevice*> (device);
return d == nullptr ? -1 : (asInput ? input_devices_names.indexOf(device->getName())
: output_devices_names.indexOf(device->getName()));
}

bool hasSeparateInputsAndOutputs() const { return true; }

DeviceInfo *findDeviceInfo(const String &name) {
for (size_t i=0; i < devices.size(); ++i) {
DeviceInfo *d = &devices[i];
if (!d->empty()) {
if (name == d->name()) return d;
}
}
return 0;
}

AudioIODevice *createDevice(const String &outputDeviceName, const String &inputDeviceName) {
ScopedPointer device;

DeviceInfo *in  = findDeviceInfo(inputDeviceName);
DeviceInfo *out = findDeviceInfo(outputDeviceName);

if (in && in->pa_info->maxInputChannels == 0) in = 0;
if (out && out->pa_info->maxOutputChannels == 0) out = 0;
if (in == 0 && out == 0) return 0;

device = new PAAudioIODevice(out ? outputDeviceName : inputDeviceName, AudioIODeviceType::getTypeName(), hostapi_info, in, out);
if (! device->initialise()) {
  device = nullptr;
}
return device.release();

}

};

AudioIODeviceType* createAudioIODeviceType_PortAudio(const String &hostapi, const String &hostapi_name)
{
PAAudioIODeviceType *p = new PAAudioIODeviceType(hostapi, hostapi_name);
if (!p->isHostApiAvailable()) deleteAndZero§;
return p;
}
[/code]

In order to use it, subclass the juce AudioDeviceManager, and override its createAudioDevices(OwnedArray< AudioIODeviceType > &list) method, with, for example:

    AudioDeviceManager::createAudioDeviceTypes(list);
    addIfNotNull(list, createAudioIODeviceType_PortAudio("ALSA", "Portaudio ALSA"));
    addIfNotNull(list, createAudioIODeviceType_PortAudio("WDMKS", "WDM/KS"));
    addIfNotNull(list, createAudioIODeviceType_PortAudio("MME", "Windows Multimedia"));

Was wondering what the results of your test were.
Did you find WDM KS to be superior to Juce’s existing non-exclusive WASAPI?

Yes it was, the latency was almost as low as with asio (with less choices of buffer sizes). However I felt that ASIO4ALL was still a bit better at avoiding cracklings with low buffer sizes and high cpu loads. I had also one tester that had an old xp computer where the wdm/ks initialisation code was crashing. I have been too lazy to try to identify the issue, and gave up on wdm/ks

I tried your class for fun, but I can’t really get it to work properly.

I tried WDMKS, WASAPI, ASIO, but I Couldn’t really get the buffer sizes to stick or apply.
Have you done any more work on the class? I would be quite interested in testing it.

No, since I gave up on wdmks I have not worked further on it. However it seemed to work well. If you’re not getting the actual buffer size that you are requesting, I’m not surprised, this is the way portaudio works: it expects an “output latency” and it picks a buffer size given this latency but there is no simple formula that maps latency to actual buffer size