diff --git a/README.md b/README.md index 5c1374df..2ebed8ed 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,7 @@ Available audio backends are configured using the `--player` command line parame | Backend | OS | Description | Parameters | | --------- | ------- | ------------ | ---------- | | alsa | Linux | ALSA | `buffer_time=` (default 80, min 10)
`fragments=` (default 4, min 2) | -| pulse | Linux | PulseAudio | `buffer_time=` (default 80, min 10) | +| pulse | Linux | PulseAudio | `buffer_time=` (default 100, min 10)
`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 | | diff --git a/client/player/pulse_player.cpp b/client/player/pulse_player.cpp index 71722ff0..8d20fc47 100644 --- a/client/player/pulse_player.cpp +++ b/client/player/pulse_player.cpp @@ -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 PulsePlayer::pcm_list() +vector PulsePlayer::pcm_list(const std::string& parameter) { auto pa_ml = std::shared_ptr(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 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 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 devices; auto op = pa_context_get_sink_info_list( pa_ctx.get(), @@ -99,6 +108,9 @@ vector 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 PulsePlayer::pcm_list() PulsePlayer::PulsePlayer(boost::asio::io_context& io_context, const ClientSettings::Player& settings, std::shared_ptr 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_) diff --git a/client/player/pulse_player.hpp b/client/player/pulse_player.hpp index 8bf4a5b0..075d3239 100644 --- a/client/player/pulse_player.hpp +++ b/client/player/pulse_player.hpp @@ -22,10 +22,12 @@ #include "player.hpp" #include +#include #include #include #include + namespace player { @@ -44,7 +46,7 @@ public: void stop() override; /// List the system's audio output devices - static std::vector pcm_list(); + static std::vector 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 server_; // cache of the last volume change std::chrono::time_point last_change_; diff --git a/client/snapclient.cpp b/client/snapclient.cpp index c179e81f..5adf6521 100644 --- a/client/snapclient.cpp +++ b/client/snapclient.cpp @@ -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 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 pcm_devices; + try + { + vector 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=\" - default 80, min 10\n"; + << " \"buffer_time=\" - default 100, min 10\n" + << " \"server=\" - default not-set: use the default server\n"; } #endif #ifdef HAS_ALSA