forked from mia/Aegisub
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
|
// Uncomment to enable extremely spammy debug logging
|
||||||
//#define PORTAUDIO_DEBUG
|
//#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();
|
PaError err = Pa_Initialize();
|
||||||
|
|
||||||
if (err != paNoError)
|
if (err != paNoError)
|
||||||
throw PortAudioError(std::string("Failed opening PortAudio:") + Pa_GetErrorText(err), 0);
|
throw PortAudioError(std::string("Failed opening PortAudio:") + Pa_GetErrorText(err), 0);
|
||||||
|
|
||||||
volume = 1.0f;
|
// Build a list of host API-specific devices we can use
|
||||||
pa_start = 0.0;
|
// 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() {
|
PortAudioPlayer::~PortAudioPlayer() {
|
||||||
|
@ -69,52 +124,52 @@ PortAudioPlayer::~PortAudioPlayer() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void PortAudioPlayer::OpenStream() {
|
void PortAudioPlayer::OpenStream() {
|
||||||
PaDeviceIndex pa_device = paNoDevice;
|
DeviceVec *device_ids = 0;
|
||||||
|
|
||||||
std::string device_name = OPT_GET("Player/Audio/PortAudio/Device Name")->GetString();
|
std::string device_name = OPT_GET("Player/Audio/PortAudio/Device Name")->GetString();
|
||||||
|
|
||||||
if (device_name.size() && device_name != "Default") {
|
if (devices.count(device_name)) {
|
||||||
int devices = Pa_GetDeviceCount();
|
device_ids = &devices[device_name];
|
||||||
for (int i = 0; i < devices; i++) {
|
LOG_D("audio/player/portaudio") << "using config device: " << device_name;
|
||||||
const PaDeviceInfo *info = Pa_GetDeviceInfo(i);
|
}
|
||||||
if (info->maxOutputChannels > 0 && info->name == device_name) {
|
|
||||||
pa_device = i;
|
if (!device_ids || device_ids->empty()) {
|
||||||
LOG_D("audio/player/portaudio") << "using config device: " << device_name << ": " << pa_device;
|
device_ids = &default_device;
|
||||||
break;
|
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) {
|
throw PortAudioError("Failed initializing PortAudio stream: " + error, 0);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void PortAudioPlayer::CloseStream() {
|
void PortAudioPlayer::CloseStream() {
|
||||||
|
@ -223,17 +278,17 @@ int64_t PortAudioPlayer::GetCurrentPosition() {
|
||||||
}
|
}
|
||||||
|
|
||||||
wxArrayString PortAudioPlayer::GetOutputDevices() {
|
wxArrayString PortAudioPlayer::GetOutputDevices() {
|
||||||
PortAudioPlayer player; // temp player to ensure PA is initialized
|
|
||||||
|
|
||||||
int devices = Pa_GetDeviceCount();
|
|
||||||
|
|
||||||
wxArrayString list;
|
wxArrayString list;
|
||||||
list.push_back("Default");
|
list.push_back("Default");
|
||||||
|
|
||||||
for (int i = 0; i < devices; i++) {
|
try {
|
||||||
const PaDeviceInfo *info = Pa_GetDeviceInfo(i);
|
PortAudioPlayer player;
|
||||||
if (info->maxOutputChannels > 0)
|
|
||||||
list.push_back(wxString(info->name, wxConvUTF8));
|
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;
|
return list;
|
||||||
|
|
|
@ -45,12 +45,20 @@ extern "C" {
|
||||||
#ifndef AGI_PRE
|
#ifndef AGI_PRE
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
/// @class PortAudioPlayer
|
/// @class PortAudioPlayer
|
||||||
/// @brief PortAudio Player
|
/// @brief PortAudio Player
|
||||||
///
|
///
|
||||||
class PortAudioPlayer : public AudioPlayer {
|
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
|
float volume; ///< Current volume level
|
||||||
int64_t current; ///< Current position
|
int64_t current; ///< Current position
|
||||||
int64_t start; ///< Start position
|
int64_t start; ///< Start position
|
||||||
|
@ -81,9 +89,14 @@ class PortAudioPlayer : public AudioPlayer {
|
||||||
/// @param userData Local data to be handed to the callback.
|
/// @param userData Local data to be handed to the callback.
|
||||||
static void paStreamFinishedCallback(void *userData);
|
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:
|
public:
|
||||||
/// @brief Constructor
|
/// @brief Constructor
|
||||||
PortAudioPlayer();
|
PortAudioPlayer();
|
||||||
|
|
||||||
/// @brief Destructor
|
/// @brief Destructor
|
||||||
~PortAudioPlayer();
|
~PortAudioPlayer();
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue