diff -Nru snapcast-0.22.0+dfsg1/changelog.md snapcast-0.23.0+dfsg1/changelog.md --- snapcast-0.22.0+dfsg1/changelog.md 2020-10-15 13:12:38.000000000 +0000 +++ snapcast-0.23.0+dfsg1/changelog.md 2021-01-09 21:43:59.000000000 +0000 @@ -1,5 +1,34 @@ # Snapcast changelog +## Version 0.23.0 + +### Features + +- Client: Add PulseAudio player backend (Issue #722) +- Client: Configurable buffer time for alsa and pulse players +- Server: If docroot is not configured, a default page is served (Issue #711) +- Server: airplay source supports "password" parameter (Issue #754) + +### Bugfixes + +- Server: airplay source deletes Shairport's meta pipe on exit (Issue #672) +- Server: alsa source will not send silece when going idle (Issue #729) +- Server: pipe source will not send silence after 3h idle (Issue #741) +- Server: Fix build error on FreeBSD (Issue #752) +- Client: "make install" will set correct user/group for snapclient (Issue #728) +- Client: Fix printing UTF-8 device names on Windows (Issue #732) +- Client: Fix stuttering on alsa player backend (Issue #722, #727) +- Client: Terminate if host is not configured and mDNS is unavailable + +### General + +- Server: Change librespot parameter "killall" default to false (Issue #746, #724) +- Client: Android uses performance mode "none" to allow effects (Issue #766) +- Snapweb: Update to v0.1.0 +- Build: Update CMakeLists.txt to build Snapclient on Android + +_Johannes Pohl Sun, 10 Jan 2021 00:13:37 +0200_ + ## Version 0.22.0 ### Features diff -Nru snapcast-0.22.0+dfsg1/client/browseZeroConf/browse_bonjour.cpp snapcast-0.23.0+dfsg1/client/browseZeroConf/browse_bonjour.cpp --- snapcast-0.22.0+dfsg1/client/browseZeroConf/browse_bonjour.cpp 2020-10-15 13:12:38.000000000 +0000 +++ snapcast-0.23.0+dfsg1/client/browseZeroConf/browse_bonjour.cpp 2021-01-09 21:43:59.000000000 +0000 @@ -176,15 +176,16 @@ deque replyCollection; { DNSServiceHandle service(new DNSServiceRef(NULL)); - CHECKED(DNSServiceBrowse(service.get(), 0, 0, serviceName.c_str(), "local.", - [](DNSServiceRef /*service*/, DNSServiceFlags /*flags*/, uint32_t /*interfaceIndex*/, DNSServiceErrorType errorCode, - const char* serviceName, const char* regtype, const char* replyDomain, void* context) { - auto replyCollection = static_cast*>(context); - - CHECKED(errorCode); - replyCollection->push_back(mDNSReply{string(serviceName), string(regtype), string(replyDomain)}); - }, - &replyCollection)); + CHECKED(DNSServiceBrowse( + service.get(), 0, 0, serviceName.c_str(), "local.", + [](DNSServiceRef /*service*/, DNSServiceFlags /*flags*/, uint32_t /*interfaceIndex*/, DNSServiceErrorType errorCode, const char* serviceName, + const char* regtype, const char* replyDomain, void* context) { + auto replyCollection = static_cast*>(context); + + CHECKED(errorCode); + replyCollection->push_back(mDNSReply{string(serviceName), string(regtype), string(replyDomain)}); + }, + &replyCollection)); runService(service); } @@ -194,16 +195,16 @@ { DNSServiceHandle service(new DNSServiceRef(NULL)); for (auto& reply : replyCollection) - CHECKED(DNSServiceResolve(service.get(), 0, 0, reply.name.c_str(), reply.regtype.c_str(), reply.domain.c_str(), - [](DNSServiceRef /*service*/, DNSServiceFlags /*flags*/, uint32_t /*interfaceIndex*/, DNSServiceErrorType errorCode, - const char* /*fullName*/, const char* hosttarget, uint16_t port, uint16_t /*txtLen*/, - const unsigned char* /*txtRecord*/, void* context) { - auto resultCollection = static_cast*>(context); - - CHECKED(errorCode); - resultCollection->push_back(mDNSResolve{string(hosttarget), ntohs(port)}); - }, - &resolveCollection)); + CHECKED(DNSServiceResolve( + service.get(), 0, 0, reply.name.c_str(), reply.regtype.c_str(), reply.domain.c_str(), + [](DNSServiceRef /*service*/, DNSServiceFlags /*flags*/, uint32_t /*interfaceIndex*/, DNSServiceErrorType errorCode, const char* /*fullName*/, + const char* hosttarget, uint16_t port, uint16_t /*txtLen*/, const unsigned char* /*txtRecord*/, void* context) { + auto resultCollection = static_cast*>(context); + + CHECKED(errorCode); + resultCollection->push_back(mDNSResolve{string(hosttarget), ntohs(port)}); + }, + &resolveCollection)); runService(service); } @@ -216,25 +217,25 @@ for (auto& resolve : resolveCollection) { resultCollection[i].port = resolve.port; - CHECKED(DNSServiceGetAddrInfo(service.get(), kDNSServiceFlagsLongLivedQuery, 0, kDNSServiceProtocol_IPv4, resolve.fullName.c_str(), - [](DNSServiceRef /*service*/, DNSServiceFlags /*flags*/, uint32_t interfaceIndex, DNSServiceErrorType /*errorCode*/, - const char* hostname, const sockaddr* address, uint32_t /*ttl*/, void* context) { - auto result = static_cast(context); - - result->host = string(hostname); - result->ip_version = (address->sa_family == AF_INET) ? (IPVersion::IPv4) : (IPVersion::IPv6); - result->iface_idx = static_cast(interfaceIndex); - - char hostIP[NI_MAXHOST]; - char hostService[NI_MAXSERV]; - if (getnameinfo(address, sizeof(*address), hostIP, sizeof(hostIP), hostService, sizeof(hostService), - NI_NUMERICHOST | NI_NUMERICSERV) == 0) - result->ip = string(hostIP); - else - return; - result->valid = true; - }, - &resultCollection[i++])); + CHECKED(DNSServiceGetAddrInfo( + service.get(), kDNSServiceFlagsLongLivedQuery, 0, kDNSServiceProtocol_IPv4, resolve.fullName.c_str(), + [](DNSServiceRef /*service*/, DNSServiceFlags /*flags*/, uint32_t interfaceIndex, DNSServiceErrorType /*errorCode*/, const char* hostname, + const sockaddr* address, uint32_t /*ttl*/, void* context) { + auto result = static_cast(context); + + result->host = string(hostname); + result->ip_version = (address->sa_family == AF_INET) ? (IPVersion::IPv4) : (IPVersion::IPv6); + result->iface_idx = static_cast(interfaceIndex); + + char hostIP[NI_MAXHOST]; + char hostService[NI_MAXSERV]; + if (getnameinfo(address, sizeof(*address), hostIP, sizeof(hostIP), hostService, sizeof(hostService), NI_NUMERICHOST | NI_NUMERICSERV) == 0) + result->ip = string(hostIP); + else + return; + result->valid = true; + }, + &resultCollection[i++])); } runService(service); } diff -Nru snapcast-0.22.0+dfsg1/client/client_connection.hpp snapcast-0.23.0+dfsg1/client/client_connection.hpp --- snapcast-0.22.0+dfsg1/client/client_connection.hpp 2020-10-15 13:12:38.000000000 +0000 +++ snapcast-0.23.0+dfsg1/client/client_connection.hpp 2021-01-09 21:43:59.000000000 +0000 @@ -61,7 +61,7 @@ /// @param value the response message void setValue(std::unique_ptr value) { - boost::asio::post(strand_, [ this, self = shared_from_this(), val = std::move(value) ]() mutable { + boost::asio::post(strand_, [this, self = shared_from_this(), val = std::move(value)]() mutable { timer_.cancel(); if (handler_) handler_({}, std::move(val)); @@ -79,7 +79,7 @@ void startTimer(const chronos::usec& timeout) { timer_.expires_after(timeout); - timer_.async_wait(boost::asio::bind_executor(strand_, [ this, self = shared_from_this() ](boost::system::error_code ec) { + timer_.async_wait(boost::asio::bind_executor(strand_, [this, self = shared_from_this()](boost::system::error_code ec) { if (!handler_) return; if (!ec) diff -Nru snapcast-0.22.0+dfsg1/client/client_settings.hpp snapcast-0.23.0+dfsg1/client/client_settings.hpp --- snapcast-0.22.0+dfsg1/client/client_settings.hpp 2020-10-15 13:12:38.000000000 +0000 +++ snapcast-0.23.0+dfsg1/client/client_settings.hpp 2021-01-09 21:43:59.000000000 +0000 @@ -60,7 +60,7 @@ std::string player_name{""}; std::string parameter{""}; int latency{0}; - PcmDevice pcm_device; + player::PcmDevice pcm_device; SampleFormat sample_format; SharingMode sharing_mode{SharingMode::unspecified}; Mixer mixer; diff -Nru snapcast-0.22.0+dfsg1/client/CMakeLists.txt snapcast-0.23.0+dfsg1/client/CMakeLists.txt --- snapcast-0.22.0+dfsg1/client/CMakeLists.txt 2020-10-15 13:12:38.000000000 +0000 +++ snapcast-0.23.0+dfsg1/client/CMakeLists.txt 2021-01-09 21:43:59.000000000 +0000 @@ -30,10 +30,10 @@ find_library(IOKIT_LIB IOKit) find_library(AUDIOTOOLBOX_LIB AudioToolbox) list(APPEND CLIENT_LIBRARIES ${COREAUDIO_LIB} ${COREFOUNDATION_LIB} ${IOKIT_LIB} ${AUDIOTOOLBOX_LIB}) -elseif (WIN32) +elseif (WIN32) list(APPEND CLIENT_SOURCES player/wasapi_player.cpp) list(APPEND CLIENT_LIBRARIES wsock32 ws2_32 avrt ksuser iphlpapi) -else() +elseif(NOT ANDROID) # Avahi if (AVAHI_FOUND) list(APPEND CLIENT_SOURCES browseZeroConf/browse_avahi.cpp) @@ -47,40 +47,66 @@ list(APPEND CLIENT_LIBRARIES ${ALSA_LIBRARIES}) list(APPEND CLIENT_INCLUDE ${ALSA_INCLUDE_DIRS}) endif (ALSA_FOUND) + + if (PULSE_FOUND) + list(APPEND CLIENT_SOURCES player/pulse_player.cpp) + list(APPEND CLIENT_LIBRARIES ${PULSE_LIBRARIES}) + list(APPEND CLIENT_INCLUDE ${PULSE_INCLUDE_DIRS}) + endif (PULSE_FOUND) endif (MACOSX) -# if OGG then tremor or vorbis -if (OGG_FOUND) +if (ANDROID) + list(APPEND CLIENT_LIBRARIES oboe::oboe) + list(APPEND CLIENT_LIBRARIES boost::boost) + list(APPEND CLIENT_LIBRARIES flac::flac) + list(APPEND CLIENT_LIBRARIES opus::opus) + list(APPEND CLIENT_LIBRARIES tremor::tremor) + list(APPEND CLIENT_LIBRARIES ogg::ogg) + list(APPEND CLIENT_SOURCES player/oboe_player.cpp) + list(APPEND CLIENT_SOURCES player/opensl_player.cpp) list(APPEND CLIENT_SOURCES decoder/ogg_decoder.cpp) - list(APPEND CLIENT_LIBRARIES ${OGG_LIBRARIES}) - list(APPEND CLIENT_INCLUDE ${OGG_INCLUDE_DIRS}) -endif (OGG_FOUND) - -# Tremor (fixed-point) or libvorbis (floating-point) -if (TREMOR_FOUND) - list(APPEND CLIENT_LIBRARIES ${TREMOR_LIBRARIES}) - list(APPEND CLIENT_INCLUDE ${TREMOR_INCLUDE_DIRS}) -elseif (VORBIS_FOUND) - list(APPEND CLIENT_LIBRARIES ${VORBIS_LIBRARIES}) - list(APPEND CLIENT_INCLUDE ${VORBIS_INCLUDE_DIRS}) -endif (TREMOR_FOUND) - -if (FLAC_FOUND) list(APPEND CLIENT_SOURCES decoder/flac_decoder.cpp) - list(APPEND CLIENT_LIBRARIES ${FLAC_LIBRARIES}) - list(APPEND CLIENT_INCLUDE ${FLAC_INCLUDE_DIRS}) -endif (FLAC_FOUND) - -if (OPUS_FOUND) - list(APPEND CLIENT_SOURCES decoder/opus_decoder.cpp) - list(APPEND CLIENT_LIBRARIES ${OPUS_LIBRARIES}) - list(APPEND CLIENT_INCLUDE ${OPUS_INCLUDE_DIRS}) -endif (OPUS_FOUND) + list(APPEND CLIENT_LIBRARIES OpenSLES) + +else() + # if OGG then tremor or vorbis + if (OGG_FOUND) + list(APPEND CLIENT_SOURCES decoder/ogg_decoder.cpp) + list(APPEND CLIENT_LIBRARIES ${OGG_LIBRARIES}) + list(APPEND CLIENT_INCLUDE ${OGG_INCLUDE_DIRS}) + endif (OGG_FOUND) + + # Tremor (fixed-point) or libvorbis (floating-point) + if (TREMOR_FOUND) + list(APPEND CLIENT_LIBRARIES ${TREMOR_LIBRARIES}) + list(APPEND CLIENT_INCLUDE ${TREMOR_INCLUDE_DIRS}) + elseif (VORBIS_FOUND) + list(APPEND CLIENT_LIBRARIES ${VORBIS_LIBRARIES}) + list(APPEND CLIENT_INCLUDE ${VORBIS_INCLUDE_DIRS}) + endif (TREMOR_FOUND) + + if (FLAC_FOUND) + list(APPEND CLIENT_SOURCES decoder/flac_decoder.cpp) + list(APPEND CLIENT_LIBRARIES ${FLAC_LIBRARIES}) + list(APPEND CLIENT_INCLUDE ${FLAC_INCLUDE_DIRS}) + endif (FLAC_FOUND) + + if (OPUS_FOUND) + list(APPEND CLIENT_SOURCES decoder/opus_decoder.cpp) + list(APPEND CLIENT_LIBRARIES ${OPUS_LIBRARIES}) + list(APPEND CLIENT_INCLUDE ${OPUS_INCLUDE_DIRS}) + endif (OPUS_FOUND) +endif() include_directories(${CLIENT_INCLUDE}) -add_executable(snapclient ${CLIENT_SOURCES}) -target_link_libraries(snapclient ${CLIENT_LIBRARIES}) +if (ANDROID) + add_executable(libsnapclient.so ${CLIENT_SOURCES}) + target_link_libraries(libsnapclient.so ${CLIENT_LIBRARIES} log OpenSLES) +else() + add_executable(snapclient ${CLIENT_SOURCES}) + target_link_libraries(snapclient ${CLIENT_LIBRARIES}) -install(TARGETS snapclient COMPONENT client DESTINATION "${CMAKE_INSTALL_BINDIR}") -install(FILES snapclient.1 COMPONENT client DESTINATION "${CMAKE_INSTALL_MANDIR}/man1") + install(TARGETS snapclient COMPONENT client DESTINATION "${CMAKE_INSTALL_BINDIR}") + install(FILES snapclient.1 COMPONENT client DESTINATION "${CMAKE_INSTALL_MANDIR}/man1") +endif() diff -Nru snapcast-0.22.0+dfsg1/client/controller.cpp snapcast-0.23.0+dfsg1/client/controller.cpp --- snapcast-0.22.0+dfsg1/client/controller.cpp 2020-10-15 13:12:38.000000000 +0000 +++ snapcast-0.23.0+dfsg1/client/controller.cpp 2021-01-09 21:43:59.000000000 +0000 @@ -35,6 +35,9 @@ #ifdef HAS_ALSA #include "player/alsa_player.hpp" #endif +#ifdef HAS_PULSE +#include "player/pulse_player.hpp" +#endif #ifdef HAS_OPENSL #include "player/opensl_player.hpp" #endif @@ -63,6 +66,7 @@ #include using namespace std; +using namespace player; static constexpr auto LOG_TAG = "Controller"; static constexpr auto TIME_SYNC_INTERVAL = 1s; @@ -89,21 +93,24 @@ { std::vector result; #ifdef HAS_ALSA - result.emplace_back("alsa"); + result.emplace_back(player::ALSA); +#endif +#ifdef HAS_PULSE + result.emplace_back(player::PULSE); #endif #ifdef HAS_OBOE - result.emplace_back("oboe"); + result.emplace_back(player::OBOE); #endif #ifdef HAS_OPENSL - result.emplace_back("opensl"); + result.emplace_back(player::OPENSL); #endif #ifdef HAS_COREAUDIO - result.emplace_back("coreaudio"); + result.emplace_back(player::COREAUDIO); #endif #ifdef HAS_WASAPI - result.emplace_back("wasapi"); + result.emplace_back(player::WASAPI); #endif - result.emplace_back("file"); + result.emplace_back(player::FILE); return result; } @@ -178,26 +185,30 @@ #ifdef HAS_ALSA if (!player_) - player_ = createPlayer(settings_.player, "alsa"); + player_ = createPlayer(settings_.player, player::ALSA); +#endif +#ifdef HAS_PULSE + if (!player_) + player_ = createPlayer(settings_.player, player::PULSE); #endif #ifdef HAS_OBOE if (!player_) - player_ = createPlayer(settings_.player, "oboe"); + player_ = createPlayer(settings_.player, player::OBOE); #endif #ifdef HAS_OPENSL if (!player_) - player_ = createPlayer(settings_.player, "opensl"); + player_ = createPlayer(settings_.player, player::OPENSL); #endif #ifdef HAS_COREAUDIO if (!player_) - player_ = createPlayer(settings_.player, "coreaudio"); + player_ = createPlayer(settings_.player, player::COREAUDIO); #endif #ifdef HAS_WASAPI if (!player_) - player_ = createPlayer(settings_.player, "wasapi"); + player_ = createPlayer(settings_.player, player::WASAPI); #endif - if (!player_ && (settings_.player.player_name == "file")) - player_ = createPlayer(settings_.player, "file"); + if (!player_ && (settings_.player.player_name == player::FILE)) + player_ = createPlayer(settings_.player, player::FILE); if (!player_) throw SnapException("No audio player support" + (settings_.player.player_name.empty() ? "" : " for: " + settings_.player.player_name)); @@ -250,34 +261,35 @@ void Controller::sendTimeSyncMessage(int quick_syncs) { auto timeReq = std::make_shared(); - clientConnection_->sendRequest(timeReq, 2s, [this, quick_syncs](const boost::system::error_code& ec, - const std::unique_ptr& response) mutable { - if (ec) - { - LOG(ERROR, LOG_TAG) << "Time sync request failed: " << ec.message() << "\n"; - reconnect(); - return; - } - else - { - TimeProvider::getInstance().setDiff(response->latency, response->received - response->sent); - } + clientConnection_->sendRequest( + timeReq, 2s, [this, quick_syncs](const boost::system::error_code& ec, const std::unique_ptr& response) mutable { + if (ec) + { + LOG(ERROR, LOG_TAG) << "Time sync request failed: " << ec.message() << "\n"; + reconnect(); + return; + } + else + { + TimeProvider::getInstance().setDiff(response->latency, response->received - response->sent); + } - std::chrono::microseconds next = TIME_SYNC_INTERVAL; - if (quick_syncs > 0) - { - if (--quick_syncs == 0) - LOG(INFO, LOG_TAG) << "diff to server [ms]: " << (float)TimeProvider::getInstance().getDiffToServer().count() / 1000.f << "\n"; - next = 100us; - } - timer_.expires_after(next); - timer_.async_wait([this, quick_syncs](const boost::system::error_code& ec) { - if (!ec) + std::chrono::microseconds next = TIME_SYNC_INTERVAL; + if (quick_syncs > 0) { - sendTimeSyncMessage(quick_syncs); + if (--quick_syncs == 0) + LOG(INFO, LOG_TAG) << "diff to server [ms]: " << (float)TimeProvider::getInstance().getDiffToServer().count() / 1000.f + << "\n"; + next = 100us; } + timer_.expires_after(next); + timer_.async_wait([this, quick_syncs](const boost::system::error_code& ec) { + if (!ec) + { + sendTimeSyncMessage(quick_syncs); + } + }); }); - }); } void Controller::browseMdns(const MdnsHandler& handler) diff -Nru snapcast-0.22.0+dfsg1/client/controller.hpp snapcast-0.23.0+dfsg1/client/controller.hpp --- snapcast-0.22.0+dfsg1/client/controller.hpp 2020-10-15 13:12:38.000000000 +0000 +++ snapcast-0.23.0+dfsg1/client/controller.hpp 2021-01-09 21:43:59.000000000 +0000 @@ -16,8 +16,8 @@ along with this program. If not, see . ***/ -#ifndef CONTROLLER_H -#define CONTROLLER_H +#ifndef CONTROLLER_HPP +#define CONTROLLER_HPP #include "client_connection.hpp" #include "client_settings.hpp" @@ -55,7 +55,7 @@ void browseMdns(const MdnsHandler& handler); template - std::unique_ptr createPlayer(ClientSettings::Player& settings, const std::string& player_name); + std::unique_ptr createPlayer(ClientSettings::Player& settings, const std::string& player_name); void getNextMessage(); void sendTimeSyncMessage(int quick_syncs); @@ -68,7 +68,7 @@ std::unique_ptr clientConnection_; std::shared_ptr stream_; std::unique_ptr decoder_; - std::unique_ptr player_; + std::unique_ptr player_; std::unique_ptr meta_; std::unique_ptr serverSettings_; std::unique_ptr headerChunk_; diff -Nru snapcast-0.22.0+dfsg1/client/decoder/flac_decoder.cpp snapcast-0.23.0+dfsg1/client/decoder/flac_decoder.cpp --- snapcast-0.22.0+dfsg1/client/decoder/flac_decoder.cpp 2020-10-15 13:12:38.000000000 +0000 +++ snapcast-0.23.0+dfsg1/client/decoder/flac_decoder.cpp 2021-01-09 21:43:59.000000000 +0000 @@ -206,7 +206,8 @@ if (metadata->type == FLAC__METADATA_TYPE_STREAMINFO) { static_cast(client_data)->cacheInfo_.sampleRate_ = metadata->data.stream_info.sample_rate; - sampleFormat.setFormat(metadata->data.stream_info.sample_rate, metadata->data.stream_info.bits_per_sample, metadata->data.stream_info.channels); + sampleFormat.setFormat(metadata->data.stream_info.sample_rate, static_cast(metadata->data.stream_info.bits_per_sample), + static_cast(metadata->data.stream_info.channels)); } } diff -Nru snapcast-0.22.0+dfsg1/client/decoder/ogg_decoder.cpp snapcast-0.23.0+dfsg1/client/decoder/ogg_decoder.cpp --- snapcast-0.22.0+dfsg1/client/decoder/ogg_decoder.cpp 2020-10-15 13:12:38.000000000 +0000 +++ snapcast-0.23.0+dfsg1/client/decoder/ogg_decoder.cpp 2021-01-09 21:43:59.000000000 +0000 @@ -225,7 +225,7 @@ /// local state for most of the decode so multiple block decodes can proceed /// in parallel. We could init multiple vorbis_block structures for vd here - sampleFormat_.setFormat(vi.rate, 16, vi.channels); + sampleFormat_.setFormat(vi.rate, 16, static_cast(vi.channels)); /* Throw the comments plus a few lines about the bitstream we're decoding */ char** ptr = vc.user_comments; diff -Nru snapcast-0.22.0+dfsg1/client/Makefile snapcast-0.23.0+dfsg1/client/Makefile --- snapcast-0.22.0+dfsg1/client/Makefile 2020-10-15 13:12:38.000000000 +0000 +++ snapcast-0.23.0+dfsg1/client/Makefile 2021-01-09 21:43:59.000000000 +0000 @@ -14,7 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -VERSION = 0.22.0 +VERSION = 0.23.0 BIN = snapclient ifeq ($(TARGET), FREEBSD) @@ -84,9 +84,9 @@ else CXX = g++ -CXXFLAGS += -pthread -DHAS_VORBIS -DHAS_ALSA -DHAS_AVAHI -DHAS_DAEMON -LDFLAGS += -lrt -lasound -lvorbis -lavahi-client -lavahi-common -latomic -OBJ += ../common/daemon.o player/alsa_player.o browseZeroConf/browse_avahi.o +CXXFLAGS += -pthread -DHAS_VORBIS -DHAS_ALSA -DHAS_PULSE -DHAS_AVAHI -DHAS_DAEMON +LDFLAGS += -lrt -lasound -lpulse -lvorbis -lavahi-client -lavahi-common -latomic +OBJ += ../common/daemon.o player/alsa_player.o player/pulse_player.o browseZeroConf/browse_avahi.o endif @@ -178,9 +178,7 @@ /etc/init.d/$(BIN) start; \ adduser: - @if ! getent passwd snapclient >/dev/null; then \ - useradd --gid audio --system snapclient; \ - fi; \ + sh ../debian/snapclient.postinst configure ifeq ($(TARGET), MACOS) @@ -227,5 +225,5 @@ systemctl daemon-reload; \ deluser: - @userdel --force snapclient > /dev/null || true; \ + sh ../debian/snapclient.postrm purge diff -Nru snapcast-0.22.0+dfsg1/client/player/alsa_player.cpp snapcast-0.23.0+dfsg1/client/player/alsa_player.cpp --- snapcast-0.22.0+dfsg1/client/player/alsa_player.cpp 2020-10-15 13:12:38.000000000 +0000 +++ snapcast-0.23.0+dfsg1/client/player/alsa_player.cpp 2021-01-09 21:43:59.000000000 +0000 @@ -22,11 +22,17 @@ #include "common/str_compat.hpp" #include "common/utils/string_utils.hpp" -//#define BUFFER_TIME 120000 -#define PERIOD_TIME 15000 +using namespace std::chrono_literals; +using namespace std; + +namespace player +{ + +static constexpr std::chrono::milliseconds BUFFER_TIME = 80ms; +static constexpr int PERIODS = 4; + #define exp10(x) (exp((x)*log(10))) -using namespace std; static constexpr auto LOG_TAG = "Alsa"; static constexpr auto DEFAULT_MIXER = "PCM"; @@ -61,6 +67,16 @@ LOG(DEBUG, LOG_TAG) << "Mixer: " << mixer_name_ << ", device: " << mixer_device_ << "\n"; } + + buffer_time_ = BUFFER_TIME; + periods_ = PERIODS; + auto params = utils::string::split_pairs(settings.parameter, ',', '='); + if (params.find("buffer_time") != params.end()) + buffer_time_ = std::chrono::milliseconds(std::max(cpt::stoi(params["buffer_time"]), 10)); + if (params.find("fragments") != params.end()) + periods_ = std::max(cpt::stoi(params["fragments"]), 2); + + LOG(INFO, LOG_TAG) << "Using buffer_time: " << buffer_time_.count() / 1000 << " ms, fragments: " << periods_ << "\n"; } @@ -261,30 +277,28 @@ void AlsaPlayer::initAlsa() { std::lock_guard lock(mutex_); - unsigned int tmp, rate; - int err, channels; - snd_pcm_hw_params_t* params; const SampleFormat& format = stream_->getFormat(); - rate = format.rate(); - channels = format.channels(); + unsigned int rate = format.rate(); + int channels = format.channels(); + int err; - /* Open the PCM device in playback mode */ + // Open the PCM device in playback mode if ((err = snd_pcm_open(&handle_, settings_.pcm_device.name.c_str(), SND_PCM_STREAM_PLAYBACK, 0)) < 0) throw SnapException("Can't open " + settings_.pcm_device.name + ", error: " + snd_strerror(err), err); - /* struct snd_pcm_playback_info_t pinfo; - if ( (pcm = snd_pcm_playback_info( pcm_handle, &pinfo )) < 0 ) - fprintf( stderr, "Error: playback info error: %s\n", snd_strerror( err ) ); - printf("buffer: '%d'\n", pinfo.buffer_size); - */ - /* Allocate parameters object and fill it with default values*/ - snd_pcm_hw_params_alloca(¶ms); + // struct snd_pcm_playback_info_t pinfo; + // if ((pcm = snd_pcm_playback_info( pcm_handle, &pinfo)) < 0) + // fprintf(stderr, "Error: playback info error: %s\n", snd_strerror(err)); + // printf("buffer: '%d'\n", pinfo.buffer_size); + // Allocate parameters object and fill it with default values + snd_pcm_hw_params_t* params; + snd_pcm_hw_params_alloca(¶ms); if ((err = snd_pcm_hw_params_any(handle_, params)) < 0) throw SnapException("Can't fill params: " + string(snd_strerror(err))); - /* Set parameters */ + // Set parameters if ((err = snd_pcm_hw_params_set_access(handle_, params, SND_PCM_ACCESS_RW_INTERLEAVED)) < 0) throw SnapException("Can't set interleaved mode: " + string(snd_strerror(err))); @@ -333,41 +347,34 @@ if ((err = snd_pcm_hw_params_set_rate_near(handle_, params, &rate, nullptr)) < 0) throw SnapException("Can't set rate: " + string(snd_strerror(err))); + if (rate != format.rate()) + LOG(WARNING, LOG_TAG) << "Could not set sample rate to " << format.rate() << " Hz, using: " << rate << " Hz\n"; - unsigned int period_time; - snd_pcm_hw_params_get_period_time_max(params, &period_time, nullptr); - if (period_time > PERIOD_TIME) - period_time = PERIOD_TIME; - - unsigned int buffer_time = 4 * period_time; - - snd_pcm_hw_params_set_period_time_near(handle_, params, &period_time, nullptr); - snd_pcm_hw_params_set_buffer_time_near(handle_, params, &buffer_time, nullptr); + unsigned int buffer_time = buffer_time_.count(); + if ((err = snd_pcm_hw_params_set_buffer_time_near(handle_, params, &buffer_time, nullptr)) < 0) + throw SnapException("Can't set buffer time: " + string(snd_strerror(err))); + + unsigned int periods = periods_; + if ((err = snd_pcm_hw_params_set_periods_near(handle_, params, &periods, 0)) < 0) + throw SnapException("Can't set periods: " + string(snd_strerror(err))); // long unsigned int periodsize = stream_->format.msRate() * 50;//2*rate/50; // if ((pcm = snd_pcm_hw_params_set_buffer_size_near(pcm_handle, params, &periodsize)) < 0) // LOG(ERROR, LOG_TAG) << "Unable to set buffer size " << (long int)periodsize << ": " << snd_strerror(pcm) << "\n"; - /* Write parameters */ + // Write parameters if ((err = snd_pcm_hw_params(handle_, params)) < 0) throw SnapException("Can't set hardware parameters: " + string(snd_strerror(err))); - /* Resume information */ - LOG(DEBUG, LOG_TAG) << "PCM name: " << snd_pcm_name(handle_) << "\n"; - LOG(DEBUG, LOG_TAG) << "PCM state: " << snd_pcm_state_name(snd_pcm_state(handle_)) << "\n"; - snd_pcm_hw_params_get_channels(params, &tmp); - LOG(DEBUG, LOG_TAG) << "channels: " << tmp << "\n"; - - snd_pcm_hw_params_get_rate(params, &tmp, nullptr); - LOG(DEBUG, LOG_TAG) << "rate: " << tmp << " bps\n"; - - /* Allocate buffer to hold single period */ + // Resume information + unsigned int period_time; + snd_pcm_hw_params_get_period_time(params, &period_time, nullptr); snd_pcm_hw_params_get_period_size(params, &frames_, nullptr); - LOG(DEBUG, LOG_TAG) << "frames: " << frames_ << "\n"; - - snd_pcm_hw_params_get_period_time(params, &tmp, nullptr); - LOG(DEBUG, LOG_TAG) << "period time: " << tmp << "\n"; + LOG(INFO, LOG_TAG) << "PCM name: " << snd_pcm_name(handle_) << ", sample rate: " << rate << " Hz, channels: " << channels + << ", buffer time: " << buffer_time << " us, periods: " << periods << ", period time: " << period_time + << " us, period frames: " << frames_ << "\n"; + // Allocate buffer to hold single period snd_pcm_sw_params_t* swparams; snd_pcm_sw_params_alloca(&swparams); snd_pcm_sw_params_current(handle_, swparams); @@ -377,6 +384,12 @@ // snd_pcm_sw_params_set_stop_threshold(pcm_handle, swparams, frames_); snd_pcm_sw_params(handle_, swparams); + if (snd_pcm_state(handle_) == SND_PCM_STATE_PREPARED) + { + if ((err = snd_pcm_start(handle_)) < 0) + LOG(DEBUG, LOG_TAG) << "Failed to start PCM: " << snd_strerror(err) << "\n"; + } + if (ctl_ == nullptr) initMixer(); } @@ -461,6 +474,35 @@ } +bool AlsaPlayer::getAvailDelay(snd_pcm_sframes_t& avail, snd_pcm_sframes_t& delay) +{ + int result = snd_pcm_avail_delay(handle_, &avail, &delay); + if (result < 0) + { + LOG(WARNING, LOG_TAG) << "snd_pcm_avail_delay failed: " << snd_strerror(result) << " (" << result << "), avail: " << avail << ", delay: " << delay + << ", using snd_pcm_avail amd snd_pcm_delay.\n"; + this_thread::sleep_for(1ms); + avail = snd_pcm_avail(handle_); + result = snd_pcm_delay(handle_, &delay); + if ((result < 0) || (delay < 0)) + { + LOG(WARNING, LOG_TAG) << "snd_pcm_delay failed: " << snd_strerror(result) << " (" << result << "), avail: " << avail << ", delay: " << delay + << "\n"; + return false; + } + // LOG(DEBUG, LOG_TAG) << "snd_pcm_delay: " << delay << ", snd_pcm_avail: " << avail << "\n"; + } + + if (avail < 0) + { + LOG(DEBUG, LOG_TAG) << "snd_pcm_avail failed: " << snd_strerror(avail) << " (" << avail << "), using " << frames_ << "\n"; + avail = frames_; + } + + return true; +} + + void AlsaPlayer::worker() { snd_pcm_sframes_t pcm; @@ -491,7 +533,7 @@ int wait_result = snd_pcm_wait(handle_, 100); if (wait_result == -EPIPE) { - LOG(ERROR, LOG_TAG) << "XRUN: " << snd_strerror(wait_result) << "\n"; + LOG(ERROR, LOG_TAG) << "XRUN while waiting for PCM: " << snd_strerror(wait_result) << "\n"; snd_pcm_prepare(handle_); } else if (wait_result < 0) @@ -505,38 +547,16 @@ continue; } - int result = snd_pcm_avail_delay(handle_, &framesAvail, &framesDelay); - if (result < 0) + if (!getAvailDelay(framesAvail, framesDelay)) { - // if (result == -EPIPE) - // snd_pcm_prepare(handle_); - // else - // uninitAlsa(); - LOG(WARNING, LOG_TAG) << "snd_pcm_avail_delay failed: " << snd_strerror(result) << ", avail: " << framesAvail << ", delay: " << framesDelay - << ", retrying.\n"; - this_thread::sleep_for(5ms); - int result = snd_pcm_avail_delay(handle_, &framesAvail, &framesDelay); - if (result < 0) - { - this_thread::sleep_for(5ms); - LOG(WARNING, LOG_TAG) << "snd_pcm_avail_delay failed again: " << snd_strerror(result) << ", avail: " << framesAvail - << ", delay: " << framesDelay << ", using snd_pcm_avail and snd_pcm_delay.\n"; - framesAvail = snd_pcm_avail(handle_); - result = snd_pcm_delay(handle_, &framesDelay); - if ((result < 0) || (framesAvail <= 0) || (framesDelay <= 0)) - { - LOG(WARNING, LOG_TAG) << "snd_pcm_avail and snd_pcm_delay failed: " << snd_strerror(result) << ", avail: " << framesAvail - << ", delay: " << framesDelay << "\n"; - this_thread::sleep_for(10ms); - snd_pcm_prepare(handle_); - continue; - } - } + this_thread::sleep_for(10ms); + snd_pcm_prepare(handle_); + continue; } if (framesAvail < static_cast(frames_)) { - this_thread::sleep_for(10ms); + this_thread::sleep_for(5ms); continue; } @@ -555,7 +575,7 @@ adjustVolume(buffer_.data(), framesAvail); if ((pcm = snd_pcm_writei(handle_, buffer_.data(), framesAvail)) == -EPIPE) { - LOG(ERROR, LOG_TAG) << "XRUN: " << snd_strerror(pcm) << "\n"; + LOG(ERROR, LOG_TAG) << "XRUN while writing to PCM: " << snd_strerror(pcm) << "\n"; snd_pcm_prepare(handle_); } else if (pcm < 0) @@ -625,3 +645,5 @@ snd_device_name_free_hint(hints); return result; } + +} // namespace player diff -Nru snapcast-0.22.0+dfsg1/client/player/alsa_player.hpp snapcast-0.23.0+dfsg1/client/player/alsa_player.hpp --- snapcast-0.22.0+dfsg1/client/player/alsa_player.hpp 2020-10-15 13:12:38.000000000 +0000 +++ snapcast-0.23.0+dfsg1/client/player/alsa_player.hpp 2021-01-09 21:43:59.000000000 +0000 @@ -16,12 +16,19 @@ along with this program. If not, see . ***/ -#ifndef ALSA_PLAYER_H -#define ALSA_PLAYER_H +#ifndef ALSA_PLAYER_HPP +#define ALSA_PLAYER_HPP #include "player.hpp" + #include +#include + +namespace player +{ + +static constexpr auto ALSA = "alsa"; /// Audio Player /** @@ -37,7 +44,7 @@ void stop() override; /// List the system's audio output devices - static std::vector pcm_list(void); + static std::vector pcm_list(); protected: void worker() override; @@ -49,6 +56,7 @@ /// free alsa and optionally the mixer /// @param uninit_mixer free the mixer void uninitAlsa(bool uninit_mixer); + bool getAvailDelay(snd_pcm_sframes_t& avail, snd_pcm_sframes_t& delay); void initMixer(); void uninitMixer(); @@ -73,7 +81,11 @@ std::chrono::time_point last_change_; std::recursive_mutex mutex_; boost::asio::steady_timer timer_; + + std::chrono::microseconds buffer_time_; + unsigned int periods_; }; +} // namespace player #endif diff -Nru snapcast-0.22.0+dfsg1/client/player/coreaudio_player.cpp snapcast-0.23.0+dfsg1/client/player/coreaudio_player.cpp --- snapcast-0.22.0+dfsg1/client/player/coreaudio_player.cpp 2020-10-15 13:12:38.000000000 +0000 +++ snapcast-0.23.0+dfsg1/client/player/coreaudio_player.cpp 2021-01-09 21:43:59.000000000 +0000 @@ -19,6 +19,9 @@ #include "coreaudio_player.hpp" #include +namespace player +{ + #define NUM_BUFFERS 2 static constexpr auto LOG_TAG = "CoreAudioPlayer"; @@ -45,7 +48,7 @@ /// TODO: experimental. No output device can be configured yet. -std::vector CoreAudioPlayer::pcm_list(void) +std::vector CoreAudioPlayer::pcm_list() { UInt32 propsize; @@ -210,3 +213,5 @@ pubStream_->clearChunks(); CFRunLoopStop(CFRunLoopGetCurrent()); } + +} // namespace player diff -Nru snapcast-0.22.0+dfsg1/client/player/coreaudio_player.hpp snapcast-0.23.0+dfsg1/client/player/coreaudio_player.hpp --- snapcast-0.22.0+dfsg1/client/player/coreaudio_player.hpp 2020-10-15 13:12:38.000000000 +0000 +++ snapcast-0.23.0+dfsg1/client/player/coreaudio_player.hpp 2021-01-09 21:43:59.000000000 +0000 @@ -16,8 +16,8 @@ along with this program. If not, see . ***/ -#ifndef CORE_AUDIO_PLAYER_H -#define CORE_AUDIO_PLAYER_H +#ifndef CORE_AUDIO_PLAYER_HPP +#define CORE_AUDIO_PLAYER_HPP #include #include @@ -25,6 +25,11 @@ #include "player.hpp" +namespace player +{ + +static constexpr auto COREAUDIO = "coreaudio"; + /// Audio Player /** * Audio player implementation using CoreAudio @@ -41,7 +46,7 @@ virtual ~CoreAudioPlayer(); void playerCallback(AudioQueueRef queue, AudioQueueBufferRef bufferRef); - static std::vector pcm_list(void); + static std::vector pcm_list(); protected: void worker() override; @@ -58,5 +63,6 @@ long lastChunkTick; }; +} // namespace player #endif diff -Nru snapcast-0.22.0+dfsg1/client/player/file_player.cpp snapcast-0.23.0+dfsg1/client/player/file_player.cpp --- snapcast-0.22.0+dfsg1/client/player/file_player.cpp 2020-10-15 13:12:38.000000000 +0000 +++ snapcast-0.23.0+dfsg1/client/player/file_player.cpp 2021-01-09 21:43:59.000000000 +0000 @@ -27,6 +27,9 @@ using namespace std; +namespace player +{ + static constexpr auto LOG_TAG = "FilePlayer"; static constexpr auto kDefaultBuffer = 50ms; @@ -47,7 +50,7 @@ { file_.reset(stderr, [](auto p) { std::ignore = p; }); } - else + else if (filename != "null") { std::string mode = "w"; if (params.find("mode") != params.end()) @@ -91,8 +94,11 @@ { adjustVolume(static_cast(buffer_.data()), numFrames); } - fwrite(buffer_.data(), 1, needed, file_.get()); - fflush(file_.get()); + if (file_) + { + fwrite(buffer_.data(), 1, needed, file_.get()); + fflush(file_.get()); + } loop(); } @@ -125,3 +131,5 @@ LOG(INFO, LOG_TAG) << "Stop\n"; timer_.cancel(); } + +} // namespace player diff -Nru snapcast-0.22.0+dfsg1/client/player/file_player.hpp snapcast-0.23.0+dfsg1/client/player/file_player.hpp --- snapcast-0.22.0+dfsg1/client/player/file_player.hpp 2020-10-15 13:12:38.000000000 +0000 +++ snapcast-0.23.0+dfsg1/client/player/file_player.hpp 2021-01-09 21:43:59.000000000 +0000 @@ -23,6 +23,11 @@ #include #include +namespace player +{ + +static constexpr auto FILE = "file"; + /// File Player /// Used for testing and doesn't even write the received audio to file at the moment, /// but just discards it @@ -42,8 +47,9 @@ boost::asio::steady_timer timer_; std::vector buffer_; std::chrono::time_point next_request_; - std::shared_ptr file_; + std::shared_ptr<::FILE> file_; }; +} // namespace player #endif diff -Nru snapcast-0.22.0+dfsg1/client/player/oboe_player.cpp snapcast-0.23.0+dfsg1/client/player/oboe_player.cpp --- snapcast-0.22.0+dfsg1/client/player/oboe_player.cpp 2020-10-15 13:12:38.000000000 +0000 +++ snapcast-0.23.0+dfsg1/client/player/oboe_player.cpp 2021-01-09 21:43:59.000000000 +0000 @@ -26,6 +26,9 @@ using namespace std; +namespace player +{ + static constexpr auto LOG_TAG = "OboePlayer"; static constexpr double kDefaultLatency = 50; @@ -76,11 +79,12 @@ // The builder set methods can be chained for convenience. oboe::AudioStreamBuilder builder; auto result = builder.setSharingMode(sharing_mode) - ->setPerformanceMode(oboe::PerformanceMode::LowLatency) + ->setPerformanceMode(oboe::PerformanceMode::None) ->setChannelCount(stream_->getFormat().channels()) ->setSampleRate(stream_->getFormat().rate()) ->setFormat(oboe::AudioFormat::I16) - ->setCallback(this) + ->setDataCallback(this) + ->setErrorCallback(this) ->setDirection(oboe::Direction::Output) //->setFramesPerCallback((8 * stream->getFormat().rate) / 1000) //->setFramesPerCallback(2 * oboe::DefaultStreamValues::FramesPerBurst) @@ -200,3 +204,5 @@ if (result != oboe::Result::OK) LOG(ERROR, LOG_TAG) << "Error in requestStop: " << oboe::convertToText(result) << "\n"; } + +} // namespace player diff -Nru snapcast-0.22.0+dfsg1/client/player/oboe_player.hpp snapcast-0.23.0+dfsg1/client/player/oboe_player.hpp --- snapcast-0.22.0+dfsg1/client/player/oboe_player.hpp 2020-10-15 13:12:38.000000000 +0000 +++ snapcast-0.23.0+dfsg1/client/player/oboe_player.hpp 2021-01-09 21:43:59.000000000 +0000 @@ -24,12 +24,16 @@ #include "player.hpp" +namespace player +{ + +static constexpr auto OBOE = "oboe"; /// Android Oboe Audio Player /** * Player implementation for Android Oboe */ -class OboePlayer : public Player, public oboe::AudioStreamCallback +class OboePlayer : public Player, public oboe::AudioStreamDataCallback, public oboe::AudioStreamErrorCallback { public: OboePlayer(boost::asio::io_context& io_context, const ClientSettings::Player& settings, std::shared_ptr stream); @@ -39,8 +43,10 @@ void stop() override; protected: - // AudioStreamCallback overrides + // AudioStreamDataCallback overrides oboe::DataCallbackResult onAudioReady(oboe::AudioStream* oboeStream, void* audioData, int32_t numFrames) override; + + // AudioStreamErrorCallback overrides void onErrorBeforeClose(oboe::AudioStream* oboeStream, oboe::Result error) override; void onErrorAfterClose(oboe::AudioStream* oboeStream, oboe::Result error) override; @@ -54,5 +60,6 @@ std::unique_ptr mLatencyTuner; }; +} // namespace player #endif diff -Nru snapcast-0.22.0+dfsg1/client/player/opensl_player.cpp snapcast-0.23.0+dfsg1/client/player/opensl_player.cpp --- snapcast-0.22.0+dfsg1/client/player/opensl_player.cpp 2020-10-15 13:12:38.000000000 +0000 +++ snapcast-0.23.0+dfsg1/client/player/opensl_player.cpp 2021-01-09 21:43:59.000000000 +0000 @@ -26,6 +26,9 @@ using namespace std; +namespace player +{ + static constexpr auto LOG_TAG = "OpenSlPlayer"; static constexpr auto kPhaseInit = "Init"; @@ -279,6 +282,9 @@ //// SLint32 streamType = SL_ANDROID_STREAM_VOICE; result = (*playerConfig)->SetConfiguration(playerConfig, SL_ANDROID_KEY_STREAM_TYPE, &streamType, sizeof(SLint32)); throwUnsuccess(kPhaseInit, "PlayerConfig::SetConfiguration", result); + // Set the performance mode. + SLuint32 performanceMode = SL_ANDROID_PERFORMANCE_NONE; + result = (*playerConfig)->SetConfiguration(playerConfig, SL_ANDROID_KEY_PERFORMANCE_MODE, &performanceMode, sizeof(performanceMode)); result = (*bqPlayerObject)->Realize(bqPlayerObject, SL_BOOLEAN_FALSE); throwUnsuccess(kPhaseInit, "PlayerObject::Realize", result); @@ -375,3 +381,5 @@ (*bqPlayerBufferQueue)->Clear(bqPlayerBufferQueue); throwUnsuccess(kPhaseStop, "PlayerPlay::SetPlayState", result); } + +} // namespace player diff -Nru snapcast-0.22.0+dfsg1/client/player/opensl_player.hpp snapcast-0.23.0+dfsg1/client/player/opensl_player.hpp --- snapcast-0.22.0+dfsg1/client/player/opensl_player.hpp 2020-10-15 13:12:38.000000000 +0000 +++ snapcast-0.23.0+dfsg1/client/player/opensl_player.hpp 2021-01-09 21:43:59.000000000 +0000 @@ -25,6 +25,11 @@ #include "player.hpp" +namespace player +{ + +static constexpr auto OPENSL = "opensl"; + typedef int (*AndroidAudioCallback)(short* buffer, int num_samples); @@ -72,5 +77,6 @@ std::shared_ptr pubStream_; }; +} // namespace player #endif diff -Nru snapcast-0.22.0+dfsg1/client/player/pcm_device.hpp snapcast-0.23.0+dfsg1/client/player/pcm_device.hpp --- snapcast-0.22.0+dfsg1/client/player/pcm_device.hpp 2020-10-15 13:12:38.000000000 +0000 +++ snapcast-0.23.0+dfsg1/client/player/pcm_device.hpp 2021-01-09 21:43:59.000000000 +0000 @@ -16,15 +16,19 @@ along with this program. If not, see . ***/ -#ifndef PCM_DEVICE_H -#define PCM_DEVICE_H +#ifndef PCM_DEVICE_HPP +#define PCM_DEVICE_HPP #include +namespace player +{ + +static constexpr char DEFAULT_DEVICE[] = "default"; struct PcmDevice { - PcmDevice() : idx(-1), name("default"){}; + PcmDevice() : idx(-1), name(DEFAULT_DEVICE){}; PcmDevice(int idx, const std::string& name, const std::string& description = "") : idx(idx), name(name), description(description){}; @@ -33,5 +37,6 @@ std::string description; }; +} // namespace player #endif diff -Nru snapcast-0.22.0+dfsg1/client/player/player.cpp snapcast-0.23.0+dfsg1/client/player/player.cpp --- snapcast-0.22.0+dfsg1/client/player/player.cpp 2020-10-15 13:12:38.000000000 +0000 +++ snapcast-0.23.0+dfsg1/client/player/player.cpp 2021-01-09 21:43:59.000000000 +0000 @@ -41,6 +41,9 @@ using namespace std; +namespace player +{ + static constexpr auto LOG_TAG = "Player"; Player::Player(boost::asio::io_context& io_context, const ClientSettings::Player& settings, std::shared_ptr stream) @@ -149,7 +152,6 @@ std::ignore = volume; std::ignore = muted; throw SnapException("Failed to get hardware mixer volume: not supported"); - return false; } @@ -247,3 +249,5 @@ } } } + +} // namespace player diff -Nru snapcast-0.22.0+dfsg1/client/player/player.hpp snapcast-0.23.0+dfsg1/client/player/player.hpp --- snapcast-0.22.0+dfsg1/client/player/player.hpp 2020-10-15 13:12:38.000000000 +0000 +++ snapcast-0.23.0+dfsg1/client/player/player.hpp 2021-01-09 21:43:59.000000000 +0000 @@ -16,8 +16,8 @@ along with this program. If not, see . ***/ -#ifndef PLAYER_H -#define PLAYER_H +#ifndef PLAYER_HPP +#define PLAYER_HPP #include "client_settings.hpp" #include "common/aixlog.hpp" @@ -32,6 +32,8 @@ #include #include +namespace player +{ /// Audio Player /** @@ -112,5 +114,6 @@ } }; +} // namespace player #endif diff -Nru snapcast-0.22.0+dfsg1/client/player/pulse_player.cpp snapcast-0.23.0+dfsg1/client/player/pulse_player.cpp --- snapcast-0.22.0+dfsg1/client/player/pulse_player.cpp 1970-01-01 00:00:00.000000000 +0000 +++ snapcast-0.23.0+dfsg1/client/player/pulse_player.cpp 2021-01-09 21:43:59.000000000 +0000 @@ -0,0 +1,466 @@ +/*** + This file is part of snapcast + Copyright (C) 2014-2020 Johannes Pohl + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +***/ + +#include +#include + +#include "common/aixlog.hpp" +#include "common/snap_exception.hpp" +#include "common/str_compat.hpp" +#include "common/utils/string_utils.hpp" +#include "pulse_player.hpp" + +using namespace std::chrono_literals; +using namespace std; + +namespace player +{ + +static constexpr std::chrono::milliseconds BUFFER_TIME = 80ms; + +static constexpr auto LOG_TAG = "PulsePlayer"; + +// 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/ + + +vector PulsePlayer::pcm_list() +{ + 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()); + auto pa_ctx = std::shared_ptr(pa_context_new(pa_mlapi, "Snapcast"), [](pa_context* pa_ctx) { + 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())))); + + static int pa_ready = 0; + pa_context_set_state_callback( + pa_ctx.get(), + [](pa_context* c, void* userdata) { + std::ignore = userdata; + pa_context_state_t state = pa_context_get_state(c); + switch (state) + { + case PA_CONTEXT_FAILED: + case PA_CONTEXT_TERMINATED: + pa_ready = 2; + break; + case PA_CONTEXT_READY: + pa_ready = 1; + break; + default: + break; + } + }, + nullptr); + + // We can't do anything until PA is ready, so just iterate the mainloop + // and continue + auto wait_start = std::chrono::steady_clock::now(); + while (pa_ready == 0) + { + auto now = std::chrono::steady_clock::now(); + 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())))); + this_thread::sleep_for(1ms); + } + + static std::vector devices; + auto op = pa_context_get_sink_info_list( + pa_ctx.get(), + [](pa_context* ctx, const pa_sink_info* i, int eol, void* userdata) mutable { + std::ignore = ctx; + std::ignore = userdata; + // auto self = static_cast(userdata); + // If eol is set to a positive number, you're at the end of the list + if (eol <= 0) + devices.emplace_back(i->index, i->name, i->description); + }, + nullptr); + + wait_start = std::chrono::steady_clock::now(); + + while (pa_operation_get_state(op) != PA_OPERATION_DONE) + { + if (pa_operation_get_state(op) == PA_OPERATION_CANCELED) + throw SnapException("PulseAudio operation canceled"); + + auto now = std::chrono::steady_clock::now(); + if (now - wait_start > 2s) + break; + pa_mainloop_iterate(pa_ml.get(), 1, nullptr); + } + int max_idx = -1; + for (const auto& device : devices) + max_idx = std::max(max_idx, device.idx); + + devices.emplace(devices.begin(), max_idx + 1, DEFAULT_DEVICE, "Let PulseAudio server choose the device"); + return devices; +} + + +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) +{ + 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)); + + LOG(INFO, LOG_TAG) << "Using buffer_time: " << latency_.count() / 1000 << " ms\n"; +} + + +PulsePlayer::~PulsePlayer() +{ + LOG(DEBUG, LOG_TAG) << "Destructor\n"; + stop(); +} + + +bool PulsePlayer::needsThread() const +{ + return true; +} + + +void PulsePlayer::worker() +{ + pa_mainloop_run(pa_ml_, nullptr); +} + + +void PulsePlayer::setHardwareVolume(double volume, bool muted) +{ + last_change_ = std::chrono::steady_clock::now(); + pa_cvolume cvolume; + if (muted) + pa_cvolume_set(&cvolume, stream_->getFormat().channels(), PA_VOLUME_MUTED); + else + pa_cvolume_set(&cvolume, stream_->getFormat().channels(), volume * PA_VOLUME_NORM); + pa_context_set_sink_input_volume(pa_ctx_, pa_stream_get_index(playstream_), &cvolume, nullptr, nullptr); +} + + +bool PulsePlayer::getHardwareVolume(double& volume, bool& muted) +{ + // This is called during start to send the initial volume to the server + // Because getting the volume works async, we return false here + // and instead trigger volume notification in pa_context_subscribe + std::ignore = volume; + std::ignore = muted; + return false; +} + + +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"; + + 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(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) + { + LOG(DEBUG, LOG_TAG) << "event_type: " << event_type << ", facility: " << facility << "\n"; + if (playstream_ && (idx == pa_stream_get_index(playstream_))) + triggerVolumeUpdate(); + } +} + + +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_.count() / 1000 << " ms\n"; + if (underflows_ >= 6 && latency_ < 500ms) + { + latency_ = (latency_ * 3) / 2; + 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_.count() / 1000 << " ms\n"; + } +} + + +void PulsePlayer::stateCallback(pa_context* 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 + 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"; +} + + +void PulsePlayer::writeCallback(pa_stream* stream, size_t nbytes) +{ + pa_usec_t usec; + int neg; + pa_stream_get_latency(stream, &usec, &neg); + + auto numFrames = nbytes / stream_->getFormat().frameSize(); + if (buffer_.size() < nbytes) + buffer_.resize(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"; + memset(buffer_.data(), 0, numFrames); + } + else + { + adjustVolume(static_cast(buffer_.data()), numFrames); + } + + pa_stream_write(stream, buffer_.data(), nbytes, nullptr, 0LL, PA_SEEK_RELATIVE); +} + + +void PulsePlayer::start() +{ + if (settings_.pcm_device.idx == -1) + throw SnapException("Can't open " + settings_.pcm_device.name + ", error: No such device"); + + const SampleFormat& format = stream_->getFormat(); + pa_ss_.rate = format.rate(); + pa_ss_.channels = format.channels(); + if (format.bits() == 8) + pa_ss_.format = PA_SAMPLE_U8; + else if (format.bits() == 16) + pa_ss_.format = PA_SAMPLE_S16LE; + else if ((format.bits() == 24) && (format.sampleSize() == 3)) + pa_ss_.format = PA_SAMPLE_S24LE; + else if ((format.bits() == 24) && (format.sampleSize() == 4)) + pa_ss_.format = PA_SAMPLE_S24_32LE; + else if (format.bits() == 32) + pa_ss_.format = PA_SAMPLE_S32LE; + 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"); + 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_)))); + + // 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 + auto wait_start = std::chrono::steady_clock::now(); + while (pa_ready_ == 0) + { + auto now = std::chrono::steady_clock::now(); + 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_)))); + this_thread::sleep_for(1ms); + } + + 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); + 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_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); + + bufattr_.fragsize = pa_usec_to_bytes(latency_.count(), &pa_ss_); + bufattr_.maxlength = pa_usec_to_bytes(latency_.count(), &pa_ss_); + bufattr_.minreq = static_cast(-1); + bufattr_.prebuf = static_cast(-1); + bufattr_.tlength = pa_usec_to_bytes(latency_.count(), &pa_ss_); + + const char* device = nullptr; + if (settings_.pcm_device.name != DEFAULT_DEVICE) + device = settings_.pcm_device.name.c_str(); + + int result = pa_stream_connect_playback( + playstream_, device, &bufattr_, static_cast(PA_STREAM_INTERPOLATE_TIMING | PA_STREAM_ADJUST_LATENCY | PA_STREAM_AUTO_TIMING_UPDATE), + nullptr, nullptr); + if (result < 0) + { + // Old pulse audio servers don't like the ADJUST_LATENCY flag, so retry without that + result = pa_stream_connect_playback(playstream_, device, &bufattr_, + static_cast(PA_STREAM_INTERPOLATE_TIMING | PA_STREAM_AUTO_TIMING_UPDATE), nullptr, nullptr); + } + if (result < 0) + throw SnapException("Failed to connect PulseAudio playback stream"); + + Player::start(); +} + + +void PulsePlayer::stop() +{ + LOG(INFO, LOG_TAG) << "Stop\n"; + if (pa_ml_) + { + pa_mainloop_quit(pa_ml_, 0); + } + + Player::stop(); + + if (pa_ctx_) + { + pa_context_disconnect(pa_ctx_); + pa_context_unref(pa_ctx_); + pa_ctx_ = nullptr; + } + + if (pa_ml_) + { + pa_mainloop_free(pa_ml_); + pa_ml_ = nullptr; + } + + if (playstream_) + { + pa_stream_set_state_callback(playstream_, nullptr, nullptr); + pa_stream_set_read_callback(playstream_, nullptr, nullptr); + pa_stream_set_underflow_callback(playstream_, nullptr, nullptr); + pa_stream_set_overflow_callback(playstream_, nullptr, nullptr); + + pa_stream_disconnect(playstream_); + pa_stream_unref(playstream_); + playstream_ = nullptr; + } +} + +} // namespace player diff -Nru snapcast-0.22.0+dfsg1/client/player/pulse_player.hpp snapcast-0.23.0+dfsg1/client/player/pulse_player.hpp --- snapcast-0.22.0+dfsg1/client/player/pulse_player.hpp 1970-01-01 00:00:00.000000000 +0000 +++ snapcast-0.23.0+dfsg1/client/player/pulse_player.hpp 2021-01-09 21:43:59.000000000 +0000 @@ -0,0 +1,81 @@ +/*** + This file is part of snapcast + Copyright (C) 2014-2020 Johannes Pohl + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +***/ + +#ifndef PULSE_PLAYER_HPP +#define PULSE_PLAYER_HPP + +#include "player.hpp" + +#include +#include +#include +#include + +namespace player +{ + +static constexpr auto PULSE = "pulse"; + +/// File Player +/// Used for testing and doesn't even write the received audio to file at the moment, +/// but just discards it +class PulsePlayer : public Player +{ +public: + PulsePlayer(boost::asio::io_context& io_context, const ClientSettings::Player& settings, std::shared_ptr stream); + virtual ~PulsePlayer(); + + void start() override; + void stop() override; + + /// List the system's audio output devices + static std::vector pcm_list(); + +protected: + bool needsThread() const override; + void worker() override; + + bool getHardwareVolume(double& volume, bool& muted) override; + void setHardwareVolume(double volume, bool muted) override; + + void triggerVolumeUpdate(); + + void underflowCallback(pa_stream* stream); + void stateCallback(pa_context* ctx); + void writeCallback(pa_stream* stream, size_t nbytes); + void subscribeCallback(pa_context* ctx, pa_subscription_event_type_t event_type, uint32_t idx); + + std::vector buffer_; + + std::chrono::microseconds latency_; + int underflows_ = 0; + std::atomic pa_ready_; + + pa_buffer_attr bufattr_; + pa_sample_spec pa_ss_; + pa_mainloop* pa_ml_; + pa_context* pa_ctx_; + pa_stream* playstream_; + + // cache of the last volume change + std::chrono::time_point last_change_; +}; + +} // namespace player + +#endif diff -Nru snapcast-0.22.0+dfsg1/client/player/wasapi_player.cpp snapcast-0.23.0+dfsg1/client/player/wasapi_player.cpp --- snapcast-0.22.0+dfsg1/client/player/wasapi_player.cpp 2020-10-15 13:12:38.000000000 +0000 +++ snapcast-0.23.0+dfsg1/client/player/wasapi_player.cpp 2021-01-09 21:43:59.000000000 +0000 @@ -20,6 +20,9 @@ using namespace std::chrono; using namespace std::chrono_literals; +namespace player +{ + static constexpr auto LOG_TAG = "WASAPI"; template @@ -102,8 +105,10 @@ CHECK_HR(hr); desc.idx = idx; - desc.name = wstring_convert, wchar_t>().to_bytes(id); - desc.description = wstring_convert, wchar_t>().to_bytes(deviceName.pwszVal); + + using converter = wstring_convert, wchar_t>; + desc.name = converter{}.to_bytes(id); + desc.description = converter{}.to_bytes(deviceName.pwszVal); CoTaskMemFree(id); @@ -140,7 +145,7 @@ CHECK_HR(hr); auto dev = convertToDevice(0, defaultDevice); - dev.name = "default"; + dev.name = DEFAULT_DEVICE; deviceList.push_back(dev); } @@ -156,6 +161,8 @@ return deviceList; } +#pragma warning(push) +#pragma warning(disable : 4127) void WASAPIPlayer::worker() { assert(sizeof(char) == sizeof(BYTE)); @@ -396,6 +403,7 @@ } } } +#pragma warning(pop) HRESULT STDMETHODCALLTYPE AudioSessionEventListener::QueryInterface(REFIID riid, VOID** ppvInterface) { @@ -419,6 +427,7 @@ HRESULT STDMETHODCALLTYPE AudioSessionEventListener::OnSimpleVolumeChanged(float NewVolume, BOOL NewMute, LPCGUID EventContext) { + std::ignore = EventContext; volume_ = NewVolume; muted_ = NewMute; @@ -502,4 +511,6 @@ muted_ = pNotify->bMuted; return S_OK; -} \ No newline at end of file +} + +} // namespace player diff -Nru snapcast-0.22.0+dfsg1/client/player/wasapi_player.hpp snapcast-0.23.0+dfsg1/client/player/wasapi_player.hpp --- snapcast-0.22.0+dfsg1/client/player/wasapi_player.hpp 2020-10-15 13:12:38.000000000 +0000 +++ snapcast-0.23.0+dfsg1/client/player/wasapi_player.hpp 2021-01-09 21:43:59.000000000 +0000 @@ -19,10 +19,16 @@ #ifndef WASAPI_PLAYER_HPP #define WASAPI_PLAYER_HPP +#pragma warning(push) +#pragma warning(disable : 4100) + #include "player.hpp" #include #include +namespace player +{ + class AudioSessionEventListener : public IAudioSessionEvents { LONG _cRef; @@ -166,13 +172,15 @@ HRESULT STDMETHODCALLTYPE OnNotify(PAUDIO_VOLUME_NOTIFICATION_DATA pNotify); }; +static constexpr auto WASAPI = "wasapi"; + class WASAPIPlayer : public Player { public: WASAPIPlayer(boost::asio::io_context& io_context, const ClientSettings::Player& settings, std::shared_ptr stream); virtual ~WASAPIPlayer(); - static std::vector pcm_list(void); + static std::vector pcm_list(); protected: virtual void worker(); @@ -187,4 +195,8 @@ ClientSettings::SharingMode mode_; }; +#pragma warning(pop) + +} // namespace player + #endif diff -Nru snapcast-0.22.0+dfsg1/client/snapclient.cpp snapcast-0.23.0+dfsg1/client/snapclient.cpp --- snapcast-0.22.0+dfsg1/client/snapclient.cpp 2020-10-15 13:12:38.000000000 +0000 +++ snapcast-0.23.0+dfsg1/client/snapclient.cpp 2021-01-09 21:43:59.000000000 +0000 @@ -29,9 +29,13 @@ #ifdef HAS_ALSA #include "player/alsa_player.hpp" #endif +#ifdef HAS_PULSE +#include "player/pulse_player.hpp" +#endif #ifdef HAS_WASAPI #include "player/wasapi_player.hpp" #endif +#include "player/file_player.hpp" #ifdef HAS_DAEMON #include "common/daemon.hpp" #endif @@ -45,25 +49,32 @@ using namespace std; using namespace popl; +using namespace player; using namespace std::chrono_literals; static constexpr auto LOG_TAG = "Snapclient"; -PcmDevice getPcmDevice(const std::string& soundcard) +PcmDevice getPcmDevice(const std::string& player, const std::string& soundcard) { -#if defined(HAS_ALSA) || defined(HAS_WASAPI) - vector pcmDevices = -#ifdef HAS_ALSA - AlsaPlayer::pcm_list(); -#else - WASAPIPlayer::pcm_list(); +#if defined(HAS_ALSA) || defined(HAS_PULSE) || defined(HAS_WASAPI) + vector pcm_devices; +#if defined(HAS_ALSA) + if (player == player::ALSA) + pcm_devices = AlsaPlayer::pcm_list(); +#endif +#if defined(HAS_PULSE) + if (player == player::PULSE) + pcm_devices = PulsePlayer::pcm_list(); +#endif +#if defined(HAS_WASAPI) + if (player == player::WASAPI) + pcm_devices = WASAPIPlayer::pcm_list(); #endif - try { int soundcardIdx = cpt::stoi(soundcard); - for (auto dev : pcmDevices) + for (auto dev : pcm_devices) if (dev.idx == soundcardIdx) return dev; } @@ -71,15 +82,14 @@ { } - for (auto dev : pcmDevices) + for (auto dev : pcm_devices) if (dev.name.find(soundcard) != string::npos) return dev; - std::ignore = soundcard; #endif - - PcmDevice pcmDevice; - pcmDevice.name = soundcard; - return pcmDevice; + std::ignore = player; + PcmDevice pcm_device; + pcm_device.name = soundcard; + return pcm_device; } #ifdef WINDOWS @@ -113,7 +123,7 @@ { string meta_script(""); ClientSettings settings; - string pcm_device("default"); + string pcm_device(player::DEFAULT_DEVICE); OptionParser op("Allowed options"); auto helpSwitch = op.add("", "help", "produce help message"); @@ -125,9 +135,9 @@ op.add>("", "hostID", "unique host id, default is MAC address", "", &settings.host_id); // PCM device specific -#if defined(HAS_ALSA) || defined(HAS_WASAPI) +#if defined(HAS_ALSA) || defined(HAS_PULSE) || defined(HAS_WASAPI) auto listSwitch = op.add("l", "list", "list PCM devices"); - /*auto soundcardValue =*/op.add>("s", "soundcard", "index or name of the pcm device", "default", &pcm_device); + /*auto soundcardValue =*/op.add>("s", "soundcard", "index or name of the pcm device", pcm_device, &pcm_device); #endif /*auto latencyValue =*/op.add>("", "latency", "latency of the PCM device", 0, &settings.player.latency); #ifdef HAS_SOXR @@ -193,19 +203,35 @@ exit(EXIT_SUCCESS); } -#if defined(HAS_ALSA) || defined(HAS_WASAPI) + settings.player.player_name = utils::string::split_left(settings.player.player_name, ':', settings.player.parameter); + +#if defined(HAS_ALSA) || defined(HAS_PULSE) || defined(HAS_WASAPI) if (listSwitch->is_set()) { - vector pcmDevices = -#ifdef HAS_ALSA - AlsaPlayer::pcm_list(); -#else - WASAPIPlayer::pcm_list(); + vector pcm_devices; +#if defined(HAS_ALSA) + if (settings.player.player_name == player::ALSA) + pcm_devices = AlsaPlayer::pcm_list(); #endif - for (auto dev : pcmDevices) - { +#if defined(HAS_PULSE) + if (settings.player.player_name == player::PULSE) + pcm_devices = PulsePlayer::pcm_list(); +#endif +#if defined(HAS_WASAPI) + 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); +#endif + 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"; exit(EXIT_SUCCESS); } #endif @@ -262,6 +288,12 @@ else throw SnapException("Invalid log sink: " + settings.logging.sink); +#if !defined(HAS_AVAHI) && !defined(HAS_BONJOUR) + if (settings.server.host.empty()) + throw SnapException("Snapserver host not configured and mDNS not available, please configure with \"--host\"."); +#endif + + #ifdef HAS_DAEMON std::unique_ptr daemon; if (daemonOption->is_set()) @@ -292,7 +324,7 @@ } #endif - settings.player.pcm_device = getPcmDevice(pcm_device); + settings.player.pcm_device = getPcmDevice(settings.player.player_name, pcm_device); #if defined(HAS_ALSA) if (settings.player.pcm_device.idx == -1) { @@ -317,15 +349,29 @@ settings.player.sharing_mode = (sharing_mode->value() == "exclusive") ? ClientSettings::SharingMode::exclusive : ClientSettings::SharingMode::shared; #endif - settings.player.player_name = utils::string::split_left(settings.player.player_name, ':', settings.player.parameter); if (settings.player.parameter == "?") { - if (settings.player.player_name == "file") + if (settings.player.player_name == player::FILE) + { + cout << "Options are a comma separated list of:\n" + << " \"filename=\" - with = \"stdout\", \"stderr\", \"null\" or a filename\n" + << " \"mode=[w|a]\" - w: write (discarding the content), a: append (keeping the content)\n"; + } +#ifdef HAS_PULSE + else if (settings.player.player_name == player::PULSE) { 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"; + << " \"buffer_time=\" - default 80, min 10\n"; } +#endif +#ifdef HAS_ALSA + else if (settings.player.player_name == player::ALSA) + { + cout << "Options are a comma separated list of:\n" + << " \"buffer_time=\" - default 80, min 10\n" + << " \"fragments=\" - default 4, min 2\n"; + } +#endif else { cout << "No options available for \"" << settings.player.player_name << "\n"; @@ -383,6 +429,6 @@ exitcode = EXIT_FAILURE; } - LOG(NOTICE, LOG_TAG) << "daemon terminated." << endl; + LOG(NOTICE, LOG_TAG) << "Snapclient terminated." << endl; exit(exitcode); } diff -Nru snapcast-0.22.0+dfsg1/client/stream.cpp snapcast-0.23.0+dfsg1/client/stream.cpp --- snapcast-0.22.0+dfsg1/client/stream.cpp 2020-10-15 13:12:38.000000000 +0000 +++ snapcast-0.23.0+dfsg1/client/stream.cpp 2021-01-09 21:43:59.000000000 +0000 @@ -22,6 +22,8 @@ #include "stream.hpp" #include "common/aixlog.hpp" +#include "common/snap_exception.hpp" +#include "common/str_compat.hpp" #include "time_provider.hpp" #include #include @@ -115,7 +117,7 @@ std::shared_ptr front_; while (chunks_.front_copy(front_)) { - auto age = std::chrono::duration_cast(TimeProvider::serverNow() - front_->start()); + age = std::chrono::duration_cast(TimeProvider::serverNow() - front_->start()); if ((age > 5s + bufferMs_) && chunks_.try_pop(front_)) LOG(TRACE, LOG_TAG) << "Oldest chunk too old: " << age.count() << " ms, removing. Chunks in queue left: " << chunks_.size() << "\n"; else @@ -141,15 +143,15 @@ cs::time_point_clk Stream::getNextPlayerChunk(void* outputBuffer, uint32_t frames) { if (!chunk_ && !chunks_.try_pop(chunk_)) - throw 0; + throw SnapException("No chunks available, requested frames: " + cpt::to_string(frames)); cs::time_point_clk tp = chunk_->start(); uint32_t read = 0; while (read < frames) { read += chunk_->readFrames(static_cast(outputBuffer) + read * format_.frameSize(), frames - read); - if (chunk_->isEndOfChunk() && !chunks_.try_pop(chunk_)) - throw 0; + if ((read < frames) && chunk_->isEndOfChunk() && !chunks_.try_pop(chunk_)) + throw SnapException("Not enough frames available, requested frames: " + cpt::to_string(frames) + ", available: " + cpt::to_string(read)); } return tp; } @@ -252,7 +254,7 @@ if (now != lastUpdate_) { lastUpdate_ = now; - LOG(INFO, LOG_TAG) << "no chunks available\n"; + LOG(INFO, LOG_TAG) << "No chunks available\n"; } return false; } @@ -432,9 +434,9 @@ } return (abs(cs::duration(age)) < 500); } - catch (int e) + catch (const std::exception& e) { - LOG(INFO, LOG_TAG) << "Exception: " << e << "\n"; + LOG(INFO, LOG_TAG) << "Exception: " << e.what() << "\n"; hard_sync_ = true; return false; } diff -Nru snapcast-0.22.0+dfsg1/CMakeLists.txt snapcast-0.23.0+dfsg1/CMakeLists.txt --- snapcast-0.22.0+dfsg1/CMakeLists.txt 2020-10-15 13:12:38.000000000 +0000 +++ snapcast-0.23.0+dfsg1/CMakeLists.txt 2021-01-09 21:43:59.000000000 +0000 @@ -1,12 +1,49 @@ cmake_minimum_required(VERSION 3.2) -project(snapcast LANGUAGES CXX VERSION 0.22.0) +project(snapcast LANGUAGES CXX VERSION 0.23.0) set(PROJECT_DESCRIPTION "Multiroom client-server audio player") set(PROJECT_URL "https://github.com/badaix/snapcast") option(BUILD_SHARED_LIBS "Build snapcast in a shared context" ON) option(BUILD_STATIC_LIBS "Build snapcast in a static context" ON) option(BUILD_TESTS "Build tests (run tests with make test)" ON) +option(WERROR "Treat warnings as errors" OFF) + +option(ASAN "Enable AddressSanitizer" OFF) +option(TSAN "Enable ThreadSanitizer" OFF) +option(UBSAN "Enable UndefinedBehaviorSanitizer" OFF) + +if (MSVC) + # warning level 4 and all warnings as errors + # warning C4505: 'getArch': unreferenced local function has been removed + # warning C4458: declaration of 'size' hides class member + # warning C4459: declaration of 'query' hides global declaration + add_compile_options(/W4 /wd4458 /wd4459 /wd4505) + if (WERROR) + add_compile_options(/WX) + endif() +else() + # lots of warnings and all warnings as errors + add_compile_options(-Wall -Wextra -pedantic -Wno-unused-function) + if (WERROR) + add_compile_options(-Werror) + endif() + + if (ASAN) + add_compile_options(-fsanitize=address) + add_link_options(-fsanitize=address) + endif() + + if (TSAN) + add_compile_options(-fsanitize=thread) + add_link_options(-fsanitize=thread) + endif() + + if (UBSAN) + add_compile_options(-fsanitize=undefined) + add_link_options(-fsanitize=undefined) + endif() +endif() include(GNUInstallDirs) @@ -22,7 +59,7 @@ option(BUILD_WITH_OPUS "Build with OPUS support" ON) option(BUILD_WITH_AVAHI "Build with AVAHI support" ON) option(BUILD_WITH_EXPAT "Build with EXPAT support" ON) - +option(BUILD_WITH_PULSE "Build with PulseAudio support" ON) if (NOT BUILD_SHARED_LIBS AND NOT BUILD_STATIC_LIBS) message(FATAL_ERROR "One or both of BUILD_SHARED_LIBS or BUILD_STATIC_LIBS must be set to ON to build") @@ -41,7 +78,6 @@ endif() elseif (${CMAKE_SYSTEM_NAME} MATCHES "Android") set (ANDROID TRUE) - add_definitions("-DASIO_DISABLE_STD_FUTURE") if (BUILD_SERVER) message(FATAL_ERROR "Snapserver not yet supported for Android, use \"-DBUILD_SERVER=OFF\"") endif() @@ -69,9 +105,11 @@ set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/") add_definitions(-DVERSION="${PROJECT_VERSION}") -set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/bin) -set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) -set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/bin) +if(NOT ANDROID) + set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/bin) + set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/bin) +endif() # Configure compiler options set(CMAKE_CXX_STANDARD 14) @@ -112,7 +150,7 @@ endif() -if(NOT WIN32) +if(NOT WIN32 AND NOT ANDROID) if(MACOSX) set(BONJOUR_FOUND true) @@ -123,8 +161,6 @@ add_definitions(-DFREEBSD -DMACOS -DHAS_DAEMON) link_directories("/usr/local/lib") list(APPEND INCLUDE_DIRS "/usr/local/include") - elseif(ANDROID) - # add_definitions("-DNO_CPP11_STRING") else() pkg_search_module(ALSA REQUIRED alsa) @@ -132,6 +168,13 @@ add_definitions(-DHAS_ALSA) endif (ALSA_FOUND) + if(BUILD_WITH_PULSE) + pkg_search_module(PULSE libpulse) + if (PULSE_FOUND) + add_definitions(-DHAS_PULSE) + endif (PULSE_FOUND) + endif(BUILD_WITH_PULSE) + if(BUILD_WITH_AVAHI) pkg_search_module(AVAHI avahi-client) if (AVAHI_FOUND) @@ -221,7 +264,25 @@ endif() endif() -find_package(Boost 1.70 REQUIRED) +if(NOT ANDROID) + find_package(Boost 1.70 REQUIRED) +else() + find_package(oboe REQUIRED CONFIG) + find_package(flac REQUIRED CONFIG) + find_package(ogg REQUIRED CONFIG) + find_package(opus REQUIRED CONFIG) + find_package(soxr REQUIRED CONFIG) + find_package(tremor REQUIRED CONFIG) + find_package(boost REQUIRED CONFIG) + + add_definitions("-DHAS_OBOE") + add_definitions("-DHAS_OPENSL") + add_definitions("-DHAS_FLAC") + add_definitions("-DHAS_OGG") + add_definitions("-DHAS_SOXR") + add_definitions("-DHAS_TREMOR") +endif() + add_definitions("-DBOOST_ERROR_CODE_HEADER_ONLY") if(WIN32) @@ -275,7 +336,9 @@ client/*.[ch]pp server/*.[ch]pp ) - + + list(REMOVE_ITEM CHECK_CXX_SOURCE_FILES "${CMAKE_SOURCE_DIR}/common/json.hpp") + ADD_CUSTOM_TARGET( reformat COMMAND diff -Nru snapcast-0.22.0+dfsg1/common/aixlog.hpp snapcast-0.23.0+dfsg1/common/aixlog.hpp --- snapcast-0.22.0+dfsg1/common/aixlog.hpp 2020-10-15 13:12:38.000000000 +0000 +++ snapcast-0.23.0+dfsg1/common/aixlog.hpp 2021-01-09 21:43:59.000000000 +0000 @@ -178,7 +178,7 @@ static Severity to_severity(std::string severity, Severity def = Severity::info) { - std::transform(severity.begin(), severity.end(), severity.begin(), [](unsigned char c) { return std::tolower(c); }); + std::transform(severity.begin(), severity.end(), severity.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); if (severity == "trace") return Severity::trace; else if (severity == "debug") @@ -841,7 +841,7 @@ { } - void log(const Metadata& metadata, const std::string& message) override + void log(const Metadata& /*metadata*/, const std::string& message) override { std::wstring wide = std::wstring(message.begin(), message.end()); OutputDebugString(wide.c_str()); diff -Nru snapcast-0.22.0+dfsg1/common/CMakeLists.txt snapcast-0.23.0+dfsg1/common/CMakeLists.txt --- snapcast-0.22.0+dfsg1/common/CMakeLists.txt 2020-10-15 13:12:38.000000000 +0000 +++ snapcast-0.23.0+dfsg1/common/CMakeLists.txt 2021-01-09 21:43:59.000000000 +0000 @@ -2,7 +2,7 @@ resampler.cpp sample_format.cpp) -if(NOT WIN32) +if(NOT WIN32 AND NOT ANDROID) list(APPEND SOURCES daemon.cpp) endif() @@ -12,6 +12,8 @@ add_library(common STATIC ${SOURCES}) -if (SOXR_FOUND) +if (ANDROID) + target_link_libraries(common soxr::soxr) +elseif(SOXR_FOUND) target_link_libraries(common ${SOXR_LIBRARIES}) -endif (SOXR_FOUND) +endif() diff -Nru snapcast-0.22.0+dfsg1/common/endian.hpp snapcast-0.23.0+dfsg1/common/endian.hpp --- snapcast-0.22.0+dfsg1/common/endian.hpp 2020-10-15 13:12:38.000000000 +0000 +++ snapcast-0.23.0+dfsg1/common/endian.hpp 2021-01-09 21:43:59.000000000 +0000 @@ -42,6 +42,6 @@ { return SWAP_64(val); } -} +} // namespace endian #endif diff -Nru snapcast-0.22.0+dfsg1/common/json.hpp snapcast-0.23.0+dfsg1/common/json.hpp --- snapcast-0.22.0+dfsg1/common/json.hpp 2020-10-15 13:12:38.000000000 +0000 +++ snapcast-0.23.0+dfsg1/common/json.hpp 2021-01-09 21:43:59.000000000 +0000 @@ -1,11 +1,12 @@ /* __ _____ _____ _____ __| | __| | | | JSON for Modern C++ -| | |__ | | | | | | version 3.1.2 +| | |__ | | | | | | version 3.9.1 |_____|_____|_____|_|___| https://github.com/nlohmann/json Licensed under the MIT License . -Copyright (c) 2013-2018 Niels Lohmann . +SPDX-License-Identifier: MIT +Copyright (c) 2013-2019 Niels Lohmann . Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -26,2300 +27,6511 @@ SOFTWARE. */ -#ifndef NLOHMANN_JSON_HPP -#define NLOHMANN_JSON_HPP +#ifndef INCLUDE_NLOHMANN_JSON_HPP_ +#define INCLUDE_NLOHMANN_JSON_HPP_ #define NLOHMANN_JSON_VERSION_MAJOR 3 -#define NLOHMANN_JSON_VERSION_MINOR 1 -#define NLOHMANN_JSON_VERSION_PATCH 2 +#define NLOHMANN_JSON_VERSION_MINOR 9 +#define NLOHMANN_JSON_VERSION_PATCH 1 -#include // all_of, find, for_each -#include // assert -#include // and, not, or -#include // nullptr_t, ptrdiff_t, size_t -#include // hash, less +#include // all_of, find, for_each +#include // nullptr_t, ptrdiff_t, size_t +#include // hash, less #include // initializer_list -#include // istream, ostream -#include // iterator_traits, random_access_iterator_tag -#include // accumulate -#include // string, stoi, to_string -#include // declval, forward, move, pair, swap +#include // istream, ostream +#include // random_access_iterator_tag +#include // unique_ptr +#include // accumulate +#include // string, stoi, to_string +#include // declval, forward, move, pair, swap +#include // vector -// #include -#ifndef NLOHMANN_JSON_FWD_HPP -#define NLOHMANN_JSON_FWD_HPP +// #include -#include // int64_t, uint64_t -#include // map -#include // allocator -#include // string -#include // vector -#include "str_compat.hpp" +#include +// #include -/*! -@brief namespace for Niels Lohmann -@see https://github.com/nlohmann -@since version 1.0.0 -*/ -namespace nlohmann -{ -/*! -@brief default JSONSerializer template argument -This serializer ignores the template arguments and uses ADL -([argument-dependent lookup](http://en.cppreference.com/w/cpp/language/adl)) -for serialization. -*/ -template -struct adl_serializer; +#include // transform +#include // array +#include // forward_list +#include // inserter, front_inserter, end +#include // map +#include // string +#include // tuple, make_tuple +#include // is_arithmetic, is_same, is_enum, underlying_type, is_convertible +#include // unordered_map +#include // pair, declval +#include // valarray -template