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 |
| --------- | ------- | ------------ | ---------- |
| 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 | |
| opensl | Android | OpenSL ES | |
| coreaudio | macOS | Core Audio | |

View file

@ -31,7 +31,7 @@ using namespace std;
namespace player
{
static constexpr std::chrono::milliseconds BUFFER_TIME = 80ms;
static constexpr std::chrono::milliseconds BUFFER_TIME = 100ms;
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/
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); });
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_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;
pa_context_set_state_callback(
@ -82,10 +88,13 @@ vector<PcmDevice> PulsePlayer::pcm_list()
if (now - wait_start > 5s)
throw SnapException("Timeout while waiting for PulseAudio to become ready");
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);
}
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;
auto op = pa_context_get_sink_info_list(
pa_ctx.get(),
@ -99,6 +108,9 @@ vector<PcmDevice> PulsePlayer::pcm_list()
},
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();
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)
: 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, ',', '=');
if (params.find("buffer_time") != params.end())
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)
pa_ss_.format = PA_SAMPLE_S32LE;
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
pa_ready_ = 0;
pa_ml_ = pa_mainloop_new();
pa_mainloop_api* pa_mlapi = pa_mainloop_get_api(pa_ml_);
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.
// 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)
throw SnapException("Timeout while waiting for PulseAudio to become ready");
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);
}
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);
if (!playstream_)

View file

@ -22,10 +22,12 @@
#include "player.hpp"
#include <atomic>
#include <boost/optional.hpp>
#include <cstdio>
#include <memory>
#include <pulse/pulseaudio.h>
namespace player
{
@ -44,7 +46,7 @@ public:
void stop() override;
/// List the system's audio output devices
static std::vector<PcmDevice> pcm_list();
static std::vector<PcmDevice> pcm_list(const std::string& parameter);
protected:
bool needsThread() const override;
@ -71,6 +73,7 @@ protected:
pa_mainloop* pa_ml_;
pa_context* pa_ctx_;
pa_stream* playstream_;
boost::optional<std::string> server_;
// cache of the last volume 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";
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)
vector<PcmDevice> pcm_devices;
@ -65,7 +65,7 @@ PcmDevice getPcmDevice(const std::string& player, const std::string& soundcard)
#endif
#if defined(HAS_PULSE)
if (player == player::PULSE)
pcm_devices = PulsePlayer::pcm_list();
pcm_devices = PulsePlayer::pcm_list(parameter);
#endif
#if defined(HAS_WASAPI)
if (player == player::WASAPI)
@ -87,6 +87,7 @@ PcmDevice getPcmDevice(const std::string& player, const std::string& soundcard)
return dev;
#endif
std::ignore = player;
std::ignore = parameter;
PcmDevice pcm_device;
pcm_device.name = soundcard;
return pcm_device;
@ -208,30 +209,37 @@ int main(int argc, char** argv)
#if defined(HAS_ALSA) || defined(HAS_PULSE) || defined(HAS_WASAPI)
if (listSwitch->is_set())
{
vector<PcmDevice> pcm_devices;
try
{
vector<PcmDevice> pcm_devices;
#if defined(HAS_ALSA)
if (settings.player.player_name == player::ALSA)
pcm_devices = AlsaPlayer::pcm_list();
if (settings.player.player_name == player::ALSA)
pcm_devices = AlsaPlayer::pcm_list();
#endif
#if defined(HAS_PULSE)
if (settings.player.player_name == player::PULSE)
pcm_devices = PulsePlayer::pcm_list();
if (settings.player.player_name == player::PULSE)
pcm_devices = PulsePlayer::pcm_list(settings.player.parameter);
#endif
#if defined(HAS_WASAPI)
if (settings.player.player_name == player::WASAPI)
pcm_devices = WASAPIPlayer::pcm_list();
if (settings.player.player_name == player::WASAPI)
pcm_devices = WASAPIPlayer::pcm_list();
#endif
#ifdef WINDOWS
// Set console code page to UTF-8 so console known how to interpret string data
SetConsoleOutputCP(CP_UTF8);
// Enable buffering to prevent VS from chopping up UTF-8 byte sequences
setvbuf(stdout, nullptr, _IOFBF, 1000);
// Set console code page to UTF-8 so console known how to interpret string data
SetConsoleOutputCP(CP_UTF8);
// Enable buffering to prevent VS from chopping up UTF-8 byte sequences
setvbuf(stdout, nullptr, _IOFBF, 1000);
#endif
for (const auto& dev : pcm_devices)
cout << dev.idx << ": " << dev.name << "\n" << dev.description << "\n\n";
for (const auto& dev : pcm_devices)
cout << dev.idx << ": " << dev.name << "\n" << dev.description << "\n\n";
if (pcm_devices.empty())
cout << "No PCM device available for audio backend \"" << settings.player.player_name << "\"\n";
if (pcm_devices.empty())
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);
}
#endif
@ -324,7 +332,7 @@ int main(int argc, char** argv)
}
#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 (settings.player.pcm_device.idx == -1)
{
@ -361,7 +369,8 @@ int main(int argc, char** argv)
else if (settings.player.player_name == player::PULSE)
{
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
#ifdef HAS_ALSA