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"));