Add automatic host API selection to the portaudio player
Portaudio defaults to using the most stable widely available host API, rather than the highest performance or quality, and as a result the default host API on windows (MME) is really quite bad. As such, add logic to select the best host API for the selected output device. Closes #1375. Originally committed to SVN as r6346.
This commit is contained in:
parent
e6252ae11b
commit
f2aadc7439
2 changed files with 120 additions and 52 deletions
|
@ -54,14 +54,69 @@ DEFINE_SIMPLE_EXCEPTION(PortAudioError, agi::AudioPlayerOpenError, "audio/player
|
|||
// Uncomment to enable extremely spammy debug logging
|
||||
//#define PORTAUDIO_DEBUG
|
||||
|
||||
PortAudioPlayer::PortAudioPlayer() {
|
||||
/// Order that the host APIs should be tried if there are multiple available
|
||||
static const PaHostApiTypeId pa_host_api_priority[] = {
|
||||
// No WDMKS or ASIO as they don't support shared mode (and WDMKS is pretty broken)
|
||||
paWASAPI,
|
||||
paDirectSound,
|
||||
paMME,
|
||||
|
||||
paCoreAudio,
|
||||
#ifdef __APPLE__
|
||||
paAL,
|
||||
#endif
|
||||
|
||||
paALSA,
|
||||
paOSS
|
||||
};
|
||||
static const size_t pa_host_api_priority_count = sizeof(pa_host_api_priority) / sizeof(pa_host_api_priority[0]);
|
||||
|
||||
PortAudioPlayer::PortAudioPlayer()
|
||||
: volume(1.0f)
|
||||
, pa_start(0.0)
|
||||
{
|
||||
PaError err = Pa_Initialize();
|
||||
|
||||
if (err != paNoError)
|
||||
throw PortAudioError(std::string("Failed opening PortAudio:") + Pa_GetErrorText(err), 0);
|
||||
|
||||
volume = 1.0f;
|
||||
pa_start = 0.0;
|
||||
// Build a list of host API-specific devices we can use
|
||||
// Some host APIs may not support all audio formats, so build a priority
|
||||
// list of host APIs for each device rather than just always using the best
|
||||
for (size_t i = 0; i < pa_host_api_priority_count; ++i) {
|
||||
PaHostApiIndex host_idx = Pa_HostApiTypeIdToHostApiIndex(pa_host_api_priority[i]);
|
||||
if (host_idx >= 0)
|
||||
GatherDevices(host_idx);
|
||||
}
|
||||
GatherDevices(Pa_GetDefaultHostApi());
|
||||
|
||||
if (devices.empty())
|
||||
throw PortAudioError("No PortAudio output devices found", 0);
|
||||
}
|
||||
|
||||
void PortAudioPlayer::GatherDevices(PaHostApiIndex host_idx) {
|
||||
const PaHostApiInfo *host_info = Pa_GetHostApiInfo(host_idx);
|
||||
if (!host_info) return;
|
||||
|
||||
for (int host_device_idx = 0; host_device_idx < host_info->deviceCount; ++host_device_idx) {
|
||||
PaDeviceIndex real_idx = Pa_HostApiDeviceIndexToDeviceIndex(host_idx, host_device_idx);
|
||||
if (real_idx < 0) continue;
|
||||
|
||||
const PaDeviceInfo *device_info = Pa_GetDeviceInfo(real_idx);
|
||||
if (!device_info) continue;
|
||||
if (device_info->maxOutputChannels <= 0) continue;
|
||||
|
||||
// MME truncates device names so check for prefix rather than exact match
|
||||
std::map<std::string, DeviceVec>::iterator dev_it = devices.lower_bound(device_info->name);
|
||||
if (dev_it == devices.end() || dev_it->first.find(device_info->name) != 0) {
|
||||
devices[device_info->name];
|
||||
--dev_it;
|
||||
}
|
||||
|
||||
dev_it->second.push_back(real_idx);
|
||||
if (real_idx == host_info->defaultOutputDevice)
|
||||
default_device.push_back(real_idx);
|
||||
}
|
||||
}
|
||||
|
||||
PortAudioPlayer::~PortAudioPlayer() {
|
||||
|
@ -69,52 +124,52 @@ PortAudioPlayer::~PortAudioPlayer() {
|
|||
}
|
||||
|
||||
void PortAudioPlayer::OpenStream() {
|
||||
PaDeviceIndex pa_device = paNoDevice;
|
||||
|
||||
DeviceVec *device_ids = 0;
|
||||
std::string device_name = OPT_GET("Player/Audio/PortAudio/Device Name")->GetString();
|
||||
|
||||
if (device_name.size() && device_name != "Default") {
|
||||
int devices = Pa_GetDeviceCount();
|
||||
for (int i = 0; i < devices; i++) {
|
||||
const PaDeviceInfo *info = Pa_GetDeviceInfo(i);
|
||||
if (info->maxOutputChannels > 0 && info->name == device_name) {
|
||||
pa_device = i;
|
||||
LOG_D("audio/player/portaudio") << "using config device: " << device_name << ": " << pa_device;
|
||||
break;
|
||||
}
|
||||
if (devices.count(device_name)) {
|
||||
device_ids = &devices[device_name];
|
||||
LOG_D("audio/player/portaudio") << "using config device: " << device_name;
|
||||
}
|
||||
|
||||
if (!device_ids || device_ids->empty()) {
|
||||
device_ids = &default_device;
|
||||
LOG_D("audio/player/portaudio") << "using default output device";
|
||||
}
|
||||
|
||||
std::string error;
|
||||
|
||||
for (size_t i = 0; i < device_ids->size(); ++i) {
|
||||
const PaDeviceInfo *device_info = Pa_GetDeviceInfo((*device_ids)[i]);
|
||||
PaStreamParameters pa_output_p;
|
||||
pa_output_p.device = (*device_ids)[i];
|
||||
pa_output_p.channelCount = provider->GetChannels();
|
||||
pa_output_p.sampleFormat = paInt16;
|
||||
pa_output_p.suggestedLatency = device_info->defaultLowOutputLatency;
|
||||
pa_output_p.hostApiSpecificStreamInfo = NULL;
|
||||
|
||||
LOG_D("audio/player/portaudio") << "OpenStream:"
|
||||
<< " output channels: " << pa_output_p.channelCount
|
||||
<< " latency: " << pa_output_p.suggestedLatency
|
||||
<< " sample rate: " << provider->GetSampleRate()
|
||||
<< " sample format: " << pa_output_p.sampleFormat;
|
||||
|
||||
PaError err = Pa_OpenStream(&stream, NULL, &pa_output_p, provider->GetSampleRate(), 0, paPrimeOutputBuffersUsingStreamCallback, paCallback, this);
|
||||
|
||||
if (err == paNoError) {
|
||||
LOG_D("audo/player/portaudio") << "Using device " << pa_output_p.device << " " << device_info->name << " " << Pa_GetHostApiInfo(device_info->hostApi)->name;
|
||||
return;
|
||||
}
|
||||
else {
|
||||
const PaHostErrorInfo *pa_err = Pa_GetLastHostErrorInfo();
|
||||
LOG_D_IF(pa_err->errorCode != 0, "audio/player/portaudio") << "HostError: API: " << pa_err->hostApiType << ", " << pa_err->errorText << ", " << pa_err->errorCode;
|
||||
LOG_D("audio/player/portaudio") << "Failed initializing PortAudio stream with error: " << Pa_GetErrorText(err);
|
||||
error += Pa_GetErrorText(err);
|
||||
error += " ";
|
||||
}
|
||||
|
||||
if (pa_device == paNoDevice)
|
||||
LOG_D("audio/player/portaudio") << "config device " << device_name << " not found, using default";
|
||||
}
|
||||
|
||||
if (pa_device == paNoDevice) {
|
||||
pa_device = Pa_GetDefaultOutputDevice();
|
||||
if (pa_device == paNoDevice)
|
||||
throw PortAudioError("No PortAudio output devices found", 0);
|
||||
LOG_D("audio/player/portaudio") << "using default output device:" << pa_device;
|
||||
}
|
||||
|
||||
PaStreamParameters pa_output_p;
|
||||
pa_output_p.device = pa_device;
|
||||
pa_output_p.channelCount = provider->GetChannels();
|
||||
pa_output_p.sampleFormat = paInt16;
|
||||
pa_output_p.suggestedLatency = Pa_GetDeviceInfo(pa_device)->defaultLowOutputLatency;
|
||||
pa_output_p.hostApiSpecificStreamInfo = NULL;
|
||||
|
||||
LOG_D("audio/player/portaudio") << "OpenStream:"
|
||||
<< " output channels: " << pa_output_p.channelCount
|
||||
<< " latency: " << pa_output_p.suggestedLatency
|
||||
<< " sample rate: " << pa_output_p.sampleFormat;
|
||||
|
||||
PaError err = Pa_OpenStream(&stream, NULL, &pa_output_p, provider->GetSampleRate(), 0, paPrimeOutputBuffersUsingStreamCallback, paCallback, this);
|
||||
|
||||
if (err != paNoError) {
|
||||
const PaHostErrorInfo *pa_err = Pa_GetLastHostErrorInfo();
|
||||
LOG_D_IF(pa_err->errorCode != 0, "audio/player/portaudio") << "HostError: API: " << pa_err->hostApiType << ", " << pa_err->errorText << ", " << pa_err->errorCode;
|
||||
LOG_D("audio/player/portaudio") << "Failed initializing PortAudio stream with error: " << Pa_GetErrorText(err);
|
||||
throw PortAudioError("Failed initializing PortAudio stream with error: " + std::string(Pa_GetErrorText(err)), 0);
|
||||
}
|
||||
throw PortAudioError("Failed initializing PortAudio stream: " + error, 0);
|
||||
}
|
||||
|
||||
void PortAudioPlayer::CloseStream() {
|
||||
|
@ -223,17 +278,17 @@ int64_t PortAudioPlayer::GetCurrentPosition() {
|
|||
}
|
||||
|
||||
wxArrayString PortAudioPlayer::GetOutputDevices() {
|
||||
PortAudioPlayer player; // temp player to ensure PA is initialized
|
||||
|
||||
int devices = Pa_GetDeviceCount();
|
||||
|
||||
wxArrayString list;
|
||||
list.push_back("Default");
|
||||
|
||||
for (int i = 0; i < devices; i++) {
|
||||
const PaDeviceInfo *info = Pa_GetDeviceInfo(i);
|
||||
if (info->maxOutputChannels > 0)
|
||||
list.push_back(wxString(info->name, wxConvUTF8));
|
||||
try {
|
||||
PortAudioPlayer player;
|
||||
|
||||
for (std::map<std::string, DeviceVec>::iterator it = player.devices.begin(); it != player.devices.end(); ++it)
|
||||
list.push_back(lagi_wxString(it->first));
|
||||
}
|
||||
catch (PortAudioError const&) {
|
||||
// No output devices, just return the list with only Default
|
||||
}
|
||||
|
||||
return list;
|
||||
|
|
|
@ -45,12 +45,20 @@ extern "C" {
|
|||
#ifndef AGI_PRE
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#endif
|
||||
|
||||
/// @class PortAudioPlayer
|
||||
/// @brief PortAudio Player
|
||||
///
|
||||
class PortAudioPlayer : public AudioPlayer {
|
||||
typedef std::vector<PaDeviceIndex> DeviceVec;
|
||||
/// Map of supported output devices from name -> device index
|
||||
std::map<std::string, DeviceVec> devices;
|
||||
|
||||
/// The index of the default output devices sorted by host API priority
|
||||
DeviceVec default_device;
|
||||
|
||||
float volume; ///< Current volume level
|
||||
int64_t current; ///< Current position
|
||||
int64_t start; ///< Start position
|
||||
|
@ -81,9 +89,14 @@ class PortAudioPlayer : public AudioPlayer {
|
|||
/// @param userData Local data to be handed to the callback.
|
||||
static void paStreamFinishedCallback(void *userData);
|
||||
|
||||
/// Gather the list of output devices supported by a host API
|
||||
/// @param host_idx Host API ID
|
||||
void GatherDevices(PaHostApiIndex host_idx);
|
||||
|
||||
public:
|
||||
/// @brief Constructor
|
||||
PortAudioPlayer();
|
||||
|
||||
/// @brief Destructor
|
||||
~PortAudioPlayer();
|
||||
|
||||
|
|
Loading…
Reference in a new issue