diff --git a/client/player/pulse_player.cpp b/client/player/pulse_player.cpp index e839e3a2..17da1e3b 100644 --- a/client/player/pulse_player.cpp +++ b/client/player/pulse_player.cpp @@ -28,12 +28,22 @@ using namespace std; static constexpr auto LOG_TAG = "PulsePlayer"; -static constexpr auto kDefaultBuffer = 50ms; + +// Example code: +// https://code.qt.io/cgit/qt/qtmultimedia.git/tree/src/plugins/pulseaudio/qaudioinput_pulse.cpp?h=dev +// http://www.videolan.org/developers/vlc/modules/audio_output/pulse.c +// https://www.freedesktop.org/wiki/Software/PulseAudio/Documentation/Developer/Clients/Samples/AsyncPlayback/ PulsePlayer::PulsePlayer(boost::asio::io_context& io_context, const ClientSettings::Player& settings, std::shared_ptr stream) - : Player(io_context, settings, stream), latency_(100000), pa_ml_(nullptr), pa_ctx_(nullptr), playstream_(nullptr) + : Player(io_context, settings, stream), latency_(100ms), pa_ml_(nullptr), pa_ctx_(nullptr), playstream_(nullptr) { + auto params = utils::string::split_pairs(settings.parameter, ',', '='); + if (params.find("latency") != params.end()) + { + latency_ = std::chrono::milliseconds(std::max(cpt::stoi(params["latency"]), 10)); + } + LOG(INFO, LOG_TAG) << "Using latency: " << latency_.count() / 1000 << " ms\n"; } @@ -52,8 +62,6 @@ bool PulsePlayer::needsThread() const void PulsePlayer::worker() { - // Run the mainloop until pa_mainloop_quit() is called - // (this example never calls it, so the mainloop runs forever). pa_mainloop_run(pa_ml_, nullptr); } @@ -83,37 +91,36 @@ bool PulsePlayer::getHardwareVolume(double& volume, bool& muted) void PulsePlayer::triggerVolumeUpdate() { - pa_context_get_sink_input_info( - pa_ctx_, pa_stream_get_index(playstream_), - [](pa_context* ctx, const pa_sink_input_info* info, int eol, void* userdata) { - std::ignore = ctx; - LOG(DEBUG, LOG_TAG) << "pa_context_get_sink_info_by_index info: " << (info != nullptr) << ", eol: " << eol << "\n"; - if (info) - { - auto self = static_cast(userdata); - auto volume = (double)pa_cvolume_avg(&(info->volume)) / (double)PA_VOLUME_NORM; - bool muted = (info->mute != 0); - LOG(DEBUG, LOG_TAG) << "volume changed: " << volume << ", muted: " << muted << "\n"; + pa_context_get_sink_input_info(pa_ctx_, pa_stream_get_index(playstream_), + [](pa_context* ctx, const pa_sink_input_info* info, int eol, void* userdata) { + std::ignore = ctx; + LOG(DEBUG, LOG_TAG) << "pa_context_get_sink_info_by_index info: " << (info != nullptr) << ", eol: " << eol << "\n"; + if (info) + { + auto self = static_cast(userdata); + auto volume = (double)pa_cvolume_avg(&(info->volume)) / (double)PA_VOLUME_NORM; + bool muted = (info->mute != 0); + LOG(DEBUG, LOG_TAG) << "volume changed: " << volume << ", muted: " << muted << "\n"; - auto now = std::chrono::steady_clock::now(); - if (now - self->last_change_ < 1s) - { - LOG(DEBUG, LOG_TAG) << "Last volume change by server: " - << std::chrono::duration_cast(now - self->last_change_).count() - << " ms => ignoring volume change\n"; - return; - } - self->notifyVolumeChange(volume, muted); - } - }, - this); + auto now = std::chrono::steady_clock::now(); + if (now - self->last_change_ < 1s) + { + LOG(DEBUG, LOG_TAG) << "Last volume change by server: " + << std::chrono::duration_cast(now - self->last_change_).count() + << " ms => ignoring volume change\n"; + return; + } + self->notifyVolumeChange(volume, muted); + } + }, + this); } void PulsePlayer::subscribeCallback(pa_context* ctx, pa_subscription_event_type_t event_type, uint32_t idx) { std::ignore = ctx; - LOG(DEBUG, LOG_TAG) << "subscribeCallback, event type: " << event_type << ", idx: " << idx << "\n"; + LOG(TRACE, LOG_TAG) << "subscribeCallback, event type: " << event_type << ", idx: " << idx << "\n"; unsigned facility = event_type & PA_SUBSCRIPTION_EVENT_FACILITY_MASK; event_type = static_cast(static_cast(event_type) & PA_SUBSCRIPTION_EVENT_TYPE_MASK); if (facility == PA_SUBSCRIPTION_EVENT_SINK_INPUT) @@ -130,40 +137,56 @@ void PulsePlayer::underflowCallback(pa_stream* stream) // We increase the latency by 50% if we get 6 underflows and latency is under 2s // This is very useful for over the network playback that can't handle low latencies underflows_++; - LOG(INFO, LOG_TAG) << "undeflow #" << underflows_ << ", latency: " << latency_ / 1000 << " ms\n"; - if (underflows_ >= 6 && latency_ < 2000000) + LOG(INFO, LOG_TAG) << "undeflow #" << underflows_ << ", latency: " << latency_.count() / 1000 << " ms\n"; + if (underflows_ >= 6 && latency_ < 500ms) { latency_ = (latency_ * 3) / 2; - bufattr_.maxlength = pa_usec_to_bytes(latency_, &pa_ss_); - bufattr_.tlength = pa_usec_to_bytes(latency_, &pa_ss_); + bufattr_.maxlength = pa_usec_to_bytes(latency_.count(), &pa_ss_); + bufattr_.tlength = pa_usec_to_bytes(latency_.count(), &pa_ss_); pa_stream_set_buffer_attr(stream, &bufattr_, nullptr, nullptr); underflows_ = 0; - LOG(INFO, LOG_TAG) << "latency increased to " << latency_ << " ms\n"; + LOG(INFO, LOG_TAG) << "latency increased to " << latency_.count() / 1000 << " ms\n"; } } void PulsePlayer::stateCallback(pa_context* ctx) { - pa_context_state_t state; - state = pa_context_get_state(ctx); + pa_context_state_t state = pa_context_get_state(ctx); + string str_state = "unknown"; + pa_ready_ = 0; switch (state) { - // These are just here for reference + // These are just here for reference case PA_CONTEXT_UNCONNECTED: + str_state = "unconnected"; + break; case PA_CONTEXT_CONNECTING: + str_state = "connecting"; + break; case PA_CONTEXT_AUTHORIZING: + str_state = "authorizing"; + break; case PA_CONTEXT_SETTING_NAME: + str_state = "setting name"; + break; default: + str_state = "unknown"; break; case PA_CONTEXT_FAILED: + str_state = "failed"; + pa_ready_ = 2; + break; case PA_CONTEXT_TERMINATED: + str_state = "terminated"; pa_ready_ = 2; break; case PA_CONTEXT_READY: + str_state = "ready"; pa_ready_ = 1; break; } + LOG(DEBUG, LOG_TAG) << "State changed " << state << ": " << str_state << "\n"; } @@ -179,7 +202,7 @@ void PulsePlayer::writeCallback(pa_stream* stream, size_t nbytes) // LOG(TRACE, LOG_TAG) << "writeCallback latency " << usec << " us, frames: " << numFrames << "\n"; if (!stream_->getPlayerChunk(buffer_.data(), std::chrono::microseconds(usec), numFrames)) { - LOG(INFO, LOG_TAG) << "Failed to get chunk. Playing silence.\n"; + // LOG(INFO, LOG_TAG) << "Failed to get chunk. Playing silence.\n"; memset(buffer_.data(), 0, numFrames); } else @@ -193,33 +216,6 @@ void PulsePlayer::writeCallback(pa_stream* stream, size_t nbytes) void PulsePlayer::start() { - // Create a mainloop API and connection to the default server - pa_ml_ = pa_mainloop_new(); - pa_mainloop_api* pa_mlapi = pa_mainloop_get_api(pa_ml_); - pa_ctx_ = pa_context_new(pa_mlapi, "Snapcast"); - pa_context_connect(pa_ctx_, nullptr, PA_CONTEXT_NOFLAGS, nullptr); - - // 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 - // modify the variable to 1 so we know when we have a connection and it's - // ready. - // If there's an error, the callback will set pa_ready to 2 - pa_context_set_state_callback( - pa_ctx_, - [](pa_context* c, void* userdata) { - auto self = static_cast(userdata); - self->stateCallback(c); - }, - this); - - // We can't do anything until PA is ready, so just iterate the mainloop - // and continue - while (pa_ready_ == 0) - pa_mainloop_iterate(pa_ml_, 1, nullptr); - - if (pa_ready_ == 2) - throw SnapException("PulseAudio is not ready"); - const SampleFormat& format = stream_->getFormat(); pa_ss_.rate = format.rate(); pa_ss_.channels = format.channels(); @@ -236,55 +232,78 @@ void PulsePlayer::start() else 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"); + pa_context_connect(pa_ctx_, nullptr, PA_CONTEXT_NOFLAGS, nullptr); + + // 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 + // modify the variable to 1 so we know when we have a connection and it's + // ready. + // If there's an error, the callback will set pa_ready to 2 + pa_context_set_state_callback(pa_ctx_, + [](pa_context* c, void* userdata) { + auto self = static_cast(userdata); + self->stateCallback(c); + }, + this); + + // We can't do anything until PA is ready, so just iterate the mainloop + // and continue + while (pa_ready_ == 0) + pa_mainloop_iterate(pa_ml_, 1, nullptr); + + if (pa_ready_ == 2) + throw SnapException("PulseAudio is not ready"); + playstream_ = pa_stream_new(pa_ctx_, "Playback", &pa_ss_, nullptr); if (!playstream_) throw SnapException("Failed to create PulseAudio stream"); if (settings_.mixer.mode == ClientSettings::Mixer::Mode::hardware) { - pa_context_set_subscribe_callback( - pa_ctx_, - [](pa_context* ctx, pa_subscription_event_type_t event_type, uint32_t idx, void* userdata) { - auto self = static_cast(userdata); - self->subscribeCallback(ctx, event_type, idx); - }, - this); + pa_context_set_subscribe_callback(pa_ctx_, + [](pa_context* ctx, pa_subscription_event_type_t event_type, uint32_t idx, void* userdata) { + auto self = static_cast(userdata); + self->subscribeCallback(ctx, event_type, idx); + }, + this); const pa_subscription_mask_t mask = static_cast(PA_SUBSCRIPTION_MASK_SINK_INPUT); - pa_context_subscribe( - pa_ctx_, mask, - [](pa_context* ctx, int success, void* userdata) { - std::ignore = ctx; - if (success) - { - auto self = static_cast(userdata); - self->triggerVolumeUpdate(); - } - }, - this); + pa_context_subscribe(pa_ctx_, mask, + [](pa_context* ctx, int success, void* userdata) { + std::ignore = ctx; + if (success) + { + auto self = static_cast(userdata); + self->triggerVolumeUpdate(); + } + }, + this); } - pa_stream_set_write_callback( - playstream_, - [](pa_stream* stream, size_t length, void* userdata) { - auto self = static_cast(userdata); - self->writeCallback(stream, length); - }, - this); + pa_stream_set_write_callback(playstream_, + [](pa_stream* stream, size_t length, void* userdata) { + auto self = static_cast(userdata); + self->writeCallback(stream, length); + }, + this); - pa_stream_set_underflow_callback( - playstream_, - [](pa_stream* stream, void* userdata) { - auto self = static_cast(userdata); - self->underflowCallback(stream); - }, - this); + pa_stream_set_underflow_callback(playstream_, + [](pa_stream* stream, void* userdata) { + auto self = static_cast(userdata); + self->underflowCallback(stream); + }, + this); bufattr_.fragsize = (uint32_t)-1; - bufattr_.maxlength = pa_usec_to_bytes(latency_, &pa_ss_); + bufattr_.maxlength = pa_usec_to_bytes(latency_.count(), &pa_ss_); bufattr_.minreq = pa_usec_to_bytes(0, &pa_ss_); bufattr_.prebuf = (uint32_t)-1; - bufattr_.tlength = pa_usec_to_bytes(latency_, &pa_ss_); + bufattr_.tlength = pa_usec_to_bytes(latency_.count(), &pa_ss_); int result = pa_stream_connect_playback( playstream_, nullptr, &bufattr_, static_cast(PA_STREAM_INTERPOLATE_TIMING | PA_STREAM_ADJUST_LATENCY | PA_STREAM_AUTO_TIMING_UPDATE), diff --git a/client/player/pulse_player.hpp b/client/player/pulse_player.hpp index 39745816..5948b7e6 100644 --- a/client/player/pulse_player.hpp +++ b/client/player/pulse_player.hpp @@ -52,7 +52,7 @@ protected: std::vector buffer_; - int latency_; //< start latency in micro seconds + std::chrono::microseconds latency_; int underflows_ = 0; int pa_ready_ = 0; diff --git a/client/snapclient.cpp b/client/snapclient.cpp index 50ed5070..27859454 100644 --- a/client/snapclient.cpp +++ b/client/snapclient.cpp @@ -329,8 +329,13 @@ int main(int argc, char** argv) if (settings.player.player_name == "file") { cout << "Options are a comma separated list of:\n" - << " \"filename:\" - with = \"stdout\", \"stderr\" or a filename\n" - << " \"mode:[w|a]\" - w: write (discarding the content), a: append (keeping the content)\n"; + << " \"filename=\" - with = \"stdout\", \"stderr\" or a filename\n" + << " \"mode=[w|a]\" - w: write (discarding the content), a: append (keeping the content)\n"; + } + else if (settings.player.player_name == "pulse") + { + cout << "Options are a comma separated list of:\n" + << " \"latency=\" - default 100, min 10\n"; } else {