Make PulseAudio server configurable

This commit is contained in:
badaix 2021-02-01 21:40:46 +01:00
parent 77b659824d
commit ed9a8c6462
4 changed files with 61 additions and 33 deletions

View file

@ -116,7 +116,7 @@ Available audio backends are configured using the `--player` command line parame
| Backend | OS | Description | Parameters | | Backend | OS | Description | Parameters |
| --------- | ------- | ------------ | ---------- | | --------- | ------- | ------------ | ---------- |
| alsa | Linux | ALSA | `buffer_time=<total buffer size [ms]>` (default 80, min 10)<br />`fragments=<number of buffers>` (default 4, min 2) | | alsa | Linux | ALSA | `buffer_time=<total buffer size [ms]>` (default 80, min 10)<br />`fragments=<number of buffers>` (default 4, min 2) |
| pulse | Linux | PulseAudio | `buffer_time=<buffer size [ms]>` (default 80, min 10) | | pulse | Linux | PulseAudio | `buffer_time=<buffer size [ms]>` (default 100, min 10)<br />`server=<PulseAudio server>` - default not-set: use the default server |
| oboe | Android | Oboe, using OpenSL ES on Android 4.1 and AAudio on 8.1 | | | oboe | Android | Oboe, using OpenSL ES on Android 4.1 and AAudio on 8.1 | |
| opensl | Android | OpenSL ES | | | opensl | Android | OpenSL ES | |
| coreaudio | macOS | Core Audio | | | coreaudio | macOS | Core Audio | |

View file

@ -31,7 +31,7 @@ using namespace std;
namespace player namespace player
{ {
static constexpr std::chrono::milliseconds BUFFER_TIME = 80ms; static constexpr std::chrono::milliseconds BUFFER_TIME = 100ms;
static constexpr auto LOG_TAG = "PulsePlayer"; static constexpr auto LOG_TAG = "PulsePlayer";
@ -41,7 +41,7 @@ static constexpr auto LOG_TAG = "PulsePlayer";
// https://www.freedesktop.org/wiki/Software/PulseAudio/Documentation/Developer/Clients/Samples/AsyncPlayback/ // https://www.freedesktop.org/wiki/Software/PulseAudio/Documentation/Developer/Clients/Samples/AsyncPlayback/
vector<PcmDevice> PulsePlayer::pcm_list() vector<PcmDevice> PulsePlayer::pcm_list(const std::string& parameter)
{ {
auto pa_ml = std::shared_ptr<pa_mainloop>(pa_mainloop_new(), [](pa_mainloop* pa_ml) { pa_mainloop_free(pa_ml); }); auto pa_ml = std::shared_ptr<pa_mainloop>(pa_mainloop_new(), [](pa_mainloop* pa_ml) { pa_mainloop_free(pa_ml); });
pa_mainloop_api* pa_mlapi = pa_mainloop_get_api(pa_ml.get()); pa_mainloop_api* pa_mlapi = pa_mainloop_get_api(pa_ml.get());
@ -49,8 +49,14 @@ vector<PcmDevice> PulsePlayer::pcm_list()
pa_context_disconnect(pa_ctx); pa_context_disconnect(pa_ctx);
pa_context_unref(pa_ctx); pa_context_unref(pa_ctx);
}); });
if (pa_context_connect(pa_ctx.get(), nullptr, PA_CONTEXT_NOFLAGS, nullptr) < 0)
throw SnapException("Failed to connect to PulseAudio context: " + std::string(pa_strerror(pa_context_errno(pa_ctx.get())))); std::string pa_server;
auto params = utils::string::split_pairs(parameter, ',', '=');
if (params.find("server") != params.end())
pa_server = params["server"];
if (pa_context_connect(pa_ctx.get(), pa_server.empty() ? nullptr : pa_server.c_str(), PA_CONTEXT_NOFLAGS, nullptr) < 0)
throw SnapException("Failed to connect to PulseAudio context, error: " + std::string(pa_strerror(pa_context_errno(pa_ctx.get()))));
static int pa_ready = 0; static int pa_ready = 0;
pa_context_set_state_callback( pa_context_set_state_callback(
@ -82,10 +88,13 @@ vector<PcmDevice> PulsePlayer::pcm_list()
if (now - wait_start > 5s) if (now - wait_start > 5s)
throw SnapException("Timeout while waiting for PulseAudio to become ready"); throw SnapException("Timeout while waiting for PulseAudio to become ready");
if (pa_mainloop_iterate(pa_ml.get(), 1, nullptr) < 0) if (pa_mainloop_iterate(pa_ml.get(), 1, nullptr) < 0)
throw SnapException("Error while waiting for PulseAudio to become ready: " + std::string(pa_strerror(pa_context_errno(pa_ctx.get())))); throw SnapException("Error while waiting for PulseAudio to become ready, error: " + std::string(pa_strerror(pa_context_errno(pa_ctx.get()))));
this_thread::sleep_for(1ms); this_thread::sleep_for(1ms);
} }
if (pa_ready == 2)
throw SnapException("PulseAudio context failed, error: " + std::string(pa_strerror(pa_context_errno(pa_ctx.get()))));
static std::vector<PcmDevice> devices; static std::vector<PcmDevice> devices;
auto op = pa_context_get_sink_info_list( auto op = pa_context_get_sink_info_list(
pa_ctx.get(), pa_ctx.get(),
@ -99,6 +108,9 @@ vector<PcmDevice> PulsePlayer::pcm_list()
}, },
nullptr); nullptr);
if (op == nullptr)
throw SnapException("PulseAudio get sink info list failed, error: " + std::string(pa_strerror(pa_context_errno(pa_ctx.get()))));
wait_start = std::chrono::steady_clock::now(); wait_start = std::chrono::steady_clock::now();
while (pa_operation_get_state(op) != PA_OPERATION_DONE) while (pa_operation_get_state(op) != PA_OPERATION_DONE)
@ -121,13 +133,15 @@ vector<PcmDevice> PulsePlayer::pcm_list()
PulsePlayer::PulsePlayer(boost::asio::io_context& io_context, const ClientSettings::Player& settings, std::shared_ptr<Stream> stream) PulsePlayer::PulsePlayer(boost::asio::io_context& io_context, const ClientSettings::Player& settings, std::shared_ptr<Stream> stream)
: Player(io_context, settings, stream), latency_(BUFFER_TIME), pa_ml_(nullptr), pa_ctx_(nullptr), playstream_(nullptr) : Player(io_context, settings, stream), latency_(BUFFER_TIME), pa_ml_(nullptr), pa_ctx_(nullptr), playstream_(nullptr), server_(boost::none)
{ {
auto params = utils::string::split_pairs(settings.parameter, ',', '='); auto params = utils::string::split_pairs(settings.parameter, ',', '=');
if (params.find("buffer_time") != params.end()) if (params.find("buffer_time") != params.end())
latency_ = std::chrono::milliseconds(std::max(cpt::stoi(params["buffer_time"]), 10)); latency_ = std::chrono::milliseconds(std::max(cpt::stoi(params["buffer_time"]), 10));
if (params.find("server") != params.end())
server_ = params["server"];
LOG(INFO, LOG_TAG) << "Using buffer_time: " << latency_.count() / 1000 << " ms\n"; LOG(INFO, LOG_TAG) << "Using buffer_time: " << latency_.count() / 1000 << " ms, server: " << server_.value_or("default") << "\n";
} }
@ -318,15 +332,17 @@ void PulsePlayer::start()
else if (format.bits() == 32) else if (format.bits() == 32)
pa_ss_.format = PA_SAMPLE_S32LE; pa_ss_.format = PA_SAMPLE_S32LE;
else else
throw SnapException("Unsupported sample format: " + cpt::to_string(format.bits())); throw SnapException("Unsupported sample format \"" + cpt::to_string(format.bits()) + "\"");
// Create a mainloop API and connection to the default server // Create a mainloop API and connection to the default server
pa_ready_ = 0; pa_ready_ = 0;
pa_ml_ = pa_mainloop_new(); pa_ml_ = pa_mainloop_new();
pa_mainloop_api* pa_mlapi = pa_mainloop_get_api(pa_ml_); pa_mainloop_api* pa_mlapi = pa_mainloop_get_api(pa_ml_);
pa_ctx_ = pa_context_new(pa_mlapi, "Snapcast"); pa_ctx_ = pa_context_new(pa_mlapi, "Snapcast");
if (pa_context_connect(pa_ctx_, nullptr, PA_CONTEXT_NOFLAGS, nullptr) < 0)
throw SnapException("Failed to connect to PulseAudio context: " + std::string(pa_strerror(pa_context_errno(pa_ctx_)))); const char* server = server_.has_value() ? server_.value().c_str() : nullptr;
if (pa_context_connect(pa_ctx_, server, PA_CONTEXT_NOFLAGS, nullptr) < 0)
throw SnapException("Failed to connect to PulseAudio context, error: " + std::string(pa_strerror(pa_context_errno(pa_ctx_))));
// This function defines a callback so the server will tell us it's state. // This function defines a callback so the server will tell us it's state.
// Our callback will wait for the state to be ready. The callback will // Our callback will wait for the state to be ready. The callback will
@ -350,12 +366,12 @@ void PulsePlayer::start()
if (now - wait_start > 5s) if (now - wait_start > 5s)
throw SnapException("Timeout while waiting for PulseAudio to become ready"); throw SnapException("Timeout while waiting for PulseAudio to become ready");
if (pa_mainloop_iterate(pa_ml_, 1, nullptr) < 0) if (pa_mainloop_iterate(pa_ml_, 1, nullptr) < 0)
throw SnapException("Error while waiting for PulseAudio to become ready: " + std::string(pa_strerror(pa_context_errno(pa_ctx_)))); throw SnapException("Error while waiting for PulseAudio to become ready, error: " + std::string(pa_strerror(pa_context_errno(pa_ctx_))));
this_thread::sleep_for(1ms); this_thread::sleep_for(1ms);
} }
if (pa_ready_ == 2) if (pa_ready_ == 2)
throw SnapException("PulseAudio is not ready"); throw SnapException("PulseAudio is not ready, error: " + std::string(pa_strerror(pa_context_errno(pa_ctx_))));
playstream_ = pa_stream_new(pa_ctx_, "Playback", &pa_ss_, nullptr); playstream_ = pa_stream_new(pa_ctx_, "Playback", &pa_ss_, nullptr);
if (!playstream_) if (!playstream_)

View file

@ -22,10 +22,12 @@
#include "player.hpp" #include "player.hpp"
#include <atomic> #include <atomic>
#include <boost/optional.hpp>
#include <cstdio> #include <cstdio>
#include <memory> #include <memory>
#include <pulse/pulseaudio.h> #include <pulse/pulseaudio.h>
namespace player namespace player
{ {
@ -44,7 +46,7 @@ public:
void stop() override; void stop() override;
/// List the system's audio output devices /// List the system's audio output devices
static std::vector<PcmDevice> pcm_list(); static std::vector<PcmDevice> pcm_list(const std::string& parameter);
protected: protected:
bool needsThread() const override; bool needsThread() const override;
@ -71,6 +73,7 @@ protected:
pa_mainloop* pa_ml_; pa_mainloop* pa_ml_;
pa_context* pa_ctx_; pa_context* pa_ctx_;
pa_stream* playstream_; pa_stream* playstream_;
boost::optional<std::string> server_;
// cache of the last volume change // cache of the last volume change
std::chrono::time_point<std::chrono::steady_clock> last_change_; std::chrono::time_point<std::chrono::steady_clock> last_change_;

View file

@ -55,7 +55,7 @@ using namespace std::chrono_literals;
static constexpr auto LOG_TAG = "Snapclient"; static constexpr auto LOG_TAG = "Snapclient";
PcmDevice getPcmDevice(const std::string& player, const std::string& soundcard) PcmDevice getPcmDevice(const std::string& player, const std::string& parameter, const std::string& soundcard)
{ {
#if defined(HAS_ALSA) || defined(HAS_PULSE) || defined(HAS_WASAPI) #if defined(HAS_ALSA) || defined(HAS_PULSE) || defined(HAS_WASAPI)
vector<PcmDevice> pcm_devices; vector<PcmDevice> pcm_devices;
@ -65,7 +65,7 @@ PcmDevice getPcmDevice(const std::string& player, const std::string& soundcard)
#endif #endif
#if defined(HAS_PULSE) #if defined(HAS_PULSE)
if (player == player::PULSE) if (player == player::PULSE)
pcm_devices = PulsePlayer::pcm_list(); pcm_devices = PulsePlayer::pcm_list(parameter);
#endif #endif
#if defined(HAS_WASAPI) #if defined(HAS_WASAPI)
if (player == player::WASAPI) if (player == player::WASAPI)
@ -87,6 +87,7 @@ PcmDevice getPcmDevice(const std::string& player, const std::string& soundcard)
return dev; return dev;
#endif #endif
std::ignore = player; std::ignore = player;
std::ignore = parameter;
PcmDevice pcm_device; PcmDevice pcm_device;
pcm_device.name = soundcard; pcm_device.name = soundcard;
return pcm_device; return pcm_device;
@ -208,30 +209,37 @@ int main(int argc, char** argv)
#if defined(HAS_ALSA) || defined(HAS_PULSE) || defined(HAS_WASAPI) #if defined(HAS_ALSA) || defined(HAS_PULSE) || defined(HAS_WASAPI)
if (listSwitch->is_set()) if (listSwitch->is_set())
{ {
vector<PcmDevice> pcm_devices; try
{
vector<PcmDevice> pcm_devices;
#if defined(HAS_ALSA) #if defined(HAS_ALSA)
if (settings.player.player_name == player::ALSA) if (settings.player.player_name == player::ALSA)
pcm_devices = AlsaPlayer::pcm_list(); pcm_devices = AlsaPlayer::pcm_list();
#endif #endif
#if defined(HAS_PULSE) #if defined(HAS_PULSE)
if (settings.player.player_name == player::PULSE) if (settings.player.player_name == player::PULSE)
pcm_devices = PulsePlayer::pcm_list(); pcm_devices = PulsePlayer::pcm_list(settings.player.parameter);
#endif #endif
#if defined(HAS_WASAPI) #if defined(HAS_WASAPI)
if (settings.player.player_name == player::WASAPI) if (settings.player.player_name == player::WASAPI)
pcm_devices = WASAPIPlayer::pcm_list(); pcm_devices = WASAPIPlayer::pcm_list();
#endif #endif
#ifdef WINDOWS #ifdef WINDOWS
// Set console code page to UTF-8 so console known how to interpret string data // Set console code page to UTF-8 so console known how to interpret string data
SetConsoleOutputCP(CP_UTF8); SetConsoleOutputCP(CP_UTF8);
// Enable buffering to prevent VS from chopping up UTF-8 byte sequences // Enable buffering to prevent VS from chopping up UTF-8 byte sequences
setvbuf(stdout, nullptr, _IOFBF, 1000); setvbuf(stdout, nullptr, _IOFBF, 1000);
#endif #endif
for (const auto& dev : pcm_devices) for (const auto& dev : pcm_devices)
cout << dev.idx << ": " << dev.name << "\n" << dev.description << "\n\n"; cout << dev.idx << ": " << dev.name << "\n" << dev.description << "\n\n";
if (pcm_devices.empty()) if (pcm_devices.empty())
cout << "No PCM device available for audio backend \"" << settings.player.player_name << "\"\n"; cout << "No PCM device available for audio backend \"" << settings.player.player_name << "\"\n";
}
catch (const std::exception& e)
{
cout << "Failed to get device list: " << e.what() << "\n";
}
exit(EXIT_SUCCESS); exit(EXIT_SUCCESS);
} }
#endif #endif
@ -324,7 +332,7 @@ int main(int argc, char** argv)
} }
#endif #endif
settings.player.pcm_device = getPcmDevice(settings.player.player_name, pcm_device); settings.player.pcm_device = getPcmDevice(settings.player.player_name, settings.player.parameter, pcm_device);
#if defined(HAS_ALSA) #if defined(HAS_ALSA)
if (settings.player.pcm_device.idx == -1) if (settings.player.pcm_device.idx == -1)
{ {
@ -361,7 +369,8 @@ int main(int argc, char** argv)
else if (settings.player.player_name == player::PULSE) else if (settings.player.player_name == player::PULSE)
{ {
cout << "Options are a comma separated list of:\n" cout << "Options are a comma separated list of:\n"
<< " \"buffer_time=<buffer size [ms]>\" - default 80, min 10\n"; << " \"buffer_time=<buffer size [ms]>\" - default 100, min 10\n"
<< " \"server=<PulseAudio server>\" - default not-set: use the default server\n";
} }
#endif #endif
#ifdef HAS_ALSA #ifdef HAS_ALSA