diff --git a/README.md b/README.md index 8507ae14..3f44df85 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,9 @@ Available stream sources are: - [tcp](doc/configuration.md#tcp-server): receives audio from a TCP socket, can act as client or server - [meta](doc/configuration.md#meta): read and mix audio from other stream sources +The client will use as audio backend the system's low level audio API to have the best possible control and most precise timing to achieve perfectly synced playback. On Linux `alsa` is used, on Android `oboe` or `opensl`, on macOS `coreaudio` and on Windows `wasapi`. +There is also a `file` backend available that will write the raw PCM data to a file (or stdout, stderr). The backend can be configured using the `--player` command line parameter. + ## Test You can test your installation by copying random data into the server's fifo file diff --git a/changelog.md b/changelog.md index 2acaeaa8..4473e9ba 100644 --- a/changelog.md +++ b/changelog.md @@ -5,6 +5,7 @@ ### Features - Server: Add Meta stream source (Issue #402, #569, #666) +- Client: Add file audio backend (Issue #681) ### Bugfixes @@ -17,7 +18,7 @@ - Add null encoder for streams used only as input for meta streams - Snapweb: Change latency range to [-10s, 10s] (Issue #695) -_Johannes Pohl Wed, 30 Sep 2020 00:13:37 +0200_ +_Johannes Pohl Sun, 11 Oct 2020 00:13:37 +0200_ ## Version 0.21.0 diff --git a/client/client_settings.hpp b/client/client_settings.hpp index 0963fef8..7900cc1b 100644 --- a/client/client_settings.hpp +++ b/client/client_settings.hpp @@ -58,6 +58,7 @@ struct ClientSettings struct Player { std::string player_name{""}; + std::string parameter{""}; int latency{0}; PcmDevice pcm_device; SampleFormat sample_format; diff --git a/client/controller.cpp b/client/controller.cpp index 3510f07f..b9d7b351 100644 --- a/client/controller.cpp +++ b/client/controller.cpp @@ -85,6 +85,28 @@ std::unique_ptr Controller::createPlayer(ClientSettings::Player& setting return nullptr; } +std::vector Controller::getSupportedPlayerNames() +{ + std::vector result; +#ifdef HAS_ALSA + result.emplace_back("alsa"); +#endif +#ifdef HAS_OBOE + result.emplace_back("oboe"); +#endif +#ifdef HAS_OPENSL + result.emplace_back("opensl"); +#endif +#ifdef HAS_COREAUDIO + result.emplace_back("coreaudio"); +#endif +#ifdef HAS_WASAPI + result.emplace_back("wasapi"); +#endif + result.emplace_back("file"); + return result; +} + void Controller::getNextMessage() { diff --git a/client/controller.hpp b/client/controller.hpp index 94fb66dc..494ec4e9 100644 --- a/client/controller.hpp +++ b/client/controller.hpp @@ -46,6 +46,7 @@ public: Controller(boost::asio::io_context& io_context, const ClientSettings& settings, std::unique_ptr meta); void start(); // void stop(); + static std::vector getSupportedPlayerNames(); private: using MdnsHandler = std::function; diff --git a/client/player/file_player.cpp b/client/player/file_player.cpp index d224ee2c..bb266ff1 100644 --- a/client/player/file_player.cpp +++ b/client/player/file_player.cpp @@ -22,6 +22,7 @@ #include "common/aixlog.hpp" #include "common/snap_exception.hpp" #include "common/str_compat.hpp" +#include "common/utils/string_utils.hpp" #include "file_player.hpp" using namespace std; @@ -31,8 +32,33 @@ static constexpr auto kDefaultBuffer = 50ms; FilePlayer::FilePlayer(boost::asio::io_context& io_context, const ClientSettings::Player& settings, std::shared_ptr stream) - : Player(io_context, settings, stream), timer_(io_context) + : Player(io_context, settings, stream), timer_(io_context), file_(nullptr) { + auto params = utils::string::split_pairs(settings.parameter, ',', '='); + string filename; + if (params.find("filename") != params.end()) + filename = params["filename"]; + + if (filename.empty() || (filename == "stdout")) + { + file_.reset(stdout, [](auto p) { std::ignore = p; }); + } + else if (filename == "stderr") + { + file_.reset(stderr, [](auto p) { std::ignore = p; }); + } + else + { + std::string mode = "w"; + if (params.find("mode") != params.end()) + mode = params["mode"]; + if ((mode != "w") && (mode != "a")) + throw SnapException("Mode must be w (write) or a (append)"); + mode += "b"; + file_.reset(fopen(filename.c_str(), mode.c_str()), [](auto p) { fclose(p); }); + if (!file_) + throw SnapException("Error opening file: '" + filename + "', error: " + cpt::to_string(errno)); + } } @@ -56,7 +82,7 @@ void FilePlayer::requestAudio() if (buffer_.size() < needed) buffer_.resize(needed); - if (!stream_->getPlayerChunk(buffer_.data(), 100ms, numFrames)) + if (!stream_->getPlayerChunk(buffer_.data(), 10ms, numFrames)) { // LOG(INFO, LOG_TAG) << "Failed to get chunk. Playing silence.\n"; memset(buffer_.data(), 0, needed); @@ -65,7 +91,8 @@ void FilePlayer::requestAudio() { adjustVolume(static_cast(buffer_.data()), numFrames); } - fwrite(buffer_.data(), 1, needed, stdout); + fwrite(buffer_.data(), 1, needed, file_.get()); + fflush(file_.get()); loop(); } diff --git a/client/player/file_player.hpp b/client/player/file_player.hpp index aadb697b..e73691e2 100644 --- a/client/player/file_player.hpp +++ b/client/player/file_player.hpp @@ -20,7 +20,8 @@ #define FILE_PLAYER_HPP #include "player.hpp" - +#include +#include /// File Player /// Used for testing and doesn't even write the received audio to file at the moment, @@ -41,6 +42,7 @@ protected: boost::asio::steady_timer timer_; std::vector buffer_; std::chrono::time_point next_request_; + std::shared_ptr file_; }; diff --git a/client/player/player.cpp b/client/player/player.cpp index 2472b51c..d7c18882 100644 --- a/client/player/player.cpp +++ b/client/player/player.cpp @@ -68,7 +68,7 @@ Player::Player(boost::asio::io_context& io_context, const ClientSettings::Player }; LOG(INFO, LOG_TAG) << "Player name: " << not_empty(settings_.player_name) << ", device: " << not_empty(settings_.pcm_device.name) << ", description: " << not_empty(settings_.pcm_device.description) << ", idx: " << settings_.pcm_device.idx - << ", sharing mode: " << sharing_mode << "\n"; + << ", sharing mode: " << sharing_mode << ", parameters: " << not_empty(settings.parameter) << "\n"; string mixer; switch (settings_.mixer.mode) diff --git a/client/snapclient.1 b/client/snapclient.1 index 9742fc99..dac11a16 100644 --- a/client/snapclient.1 +++ b/client/snapclient.1 @@ -46,6 +46,9 @@ latency of the PCM device \fB--sampleformat arg\fR resample audio stream to :: .TP +\fB--player arg (=alsa)\fR +alsa|file[:|?] +.TP \fB--mixer arg (=software)\fR software|hardware|script|none|?[:] .TP diff --git a/client/snapclient.cpp b/client/snapclient.cpp index 994a2777..381b91ae 100644 --- a/client/snapclient.cpp +++ b/client/snapclient.cpp @@ -134,12 +134,11 @@ int main(int argc, char** argv) auto sample_format = op.add>("", "sampleformat", "resample audio stream to ::", ""); #endif -// audio backend -#if defined(HAS_OBOE) && defined(HAS_OPENSL) - op.add>("", "player", "audio backend (oboe, opensl)", "oboe", &settings.player.player_name); -#else - op.add, Attribute::hidden>("", "player", "audio backend (, file)", "", &settings.player.player_name); -#endif + auto supported_players = Controller::getSupportedPlayerNames(); + string supported_players_str; + for (const auto& supported_player : supported_players) + supported_players_str += (!supported_players_str.empty() ? "|" : "") + supported_player; + op.add>("", "player", supported_players_str + "[:|?]", supported_players.front(), &settings.player.player_name); // sharing mode #if defined(HAS_OBOE) || defined(HAS_WASAPI) @@ -318,6 +317,22 @@ int main(int argc, char** argv) 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") + { + 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"; + } + else + { + cout << "No options available for \"" << settings.player.player_name << "\n"; + } + exit(EXIT_SUCCESS); + } + string mode = utils::string::split_left(mixer_mode->value(), ':', settings.player.mixer.parameter); if (mode == "software") settings.player.mixer.mode = ClientSettings::Mixer::Mode::software; diff --git a/common/utils/string_utils.hpp b/common/utils/string_utils.hpp index d713acb6..1feb9311 100644 --- a/common/utils/string_utils.hpp +++ b/common/utils/string_utils.hpp @@ -20,6 +20,7 @@ #define STRING_UTILS_H #include +#include #include #include #include @@ -142,6 +143,25 @@ static std::vector split(const std::string& s, char delim) return elems; } + +static std::map split_pairs(const std::string& s, char pair_delim, char key_value_delim) +{ + std::map result; + auto keyValueList = split(s, pair_delim); + for (auto& kv : keyValueList) + { + auto pos = kv.find(key_value_delim); + if (pos != std::string::npos) + { + std::string key = trim_copy(kv.substr(0, pos)); + std::string value = trim_copy(kv.substr(pos + 1)); + result[key] = value; + } + } + return result; +} + + } // namespace string } // namespace utils diff --git a/debian/changelog b/debian/changelog index 618118b2..5725b8b0 100644 --- a/debian/changelog +++ b/debian/changelog @@ -2,6 +2,7 @@ snapcast (0.22.0-1) unstable; urgency=medium * Features -Server: Add Meta stream source (Issue #402, #569, #666) + -Client: Add file audio backend (Issue #681) * Bugfixes -Add missing define for alsa stream to makefile (Issue #692) -Fix playback when plugging the headset on Android (Issue #699) @@ -10,7 +11,7 @@ snapcast (0.22.0-1) unstable; urgency=medium -Add null encoder for streams used only as input for meta streams -Snapweb: Change latency range to [-10s, 10s] (Issue #695) - -- Johannes Pohl Wed, 30 Sep 2020 00:13:37 +0200 + -- Johannes Pohl Sun, 11 Oct 2020 00:13:37 +0200 snapcast (0.21.0-1) unstable; urgency=medium diff --git a/server/streamreader/meta_stream.cpp b/server/streamreader/meta_stream.cpp index e3dafb2a..d32e303a 100644 --- a/server/streamreader/meta_stream.cpp +++ b/server/streamreader/meta_stream.cpp @@ -105,7 +105,8 @@ void MetaStream::onStateChanged(const PcmStream* pcmStream, ReaderState state) if (active_stream_ != stream) { - LOG(INFO, LOG_TAG) << "Stream: " << name_ << ", switching active stream: " << (active_stream_?active_stream_->getName():"") << " => " << stream->getName() << "\n"; + LOG(INFO, LOG_TAG) << "Stream: " << name_ << ", switching active stream: " << (active_stream_ ? active_stream_->getName() : "") << " => " + << stream->getName() << "\n"; active_stream_ = stream; resampler_ = make_unique(active_stream_->getSampleFormat(), sampleFormat_); } diff --git a/server/streamreader/stream_manager.cpp b/server/streamreader/stream_manager.cpp index bee9eca2..8c0851a7 100644 --- a/server/streamreader/stream_manager.cpp +++ b/server/streamreader/stream_manager.cpp @@ -153,7 +153,7 @@ const PcmStreamPtr StreamManager::getDefaultStream() if (streams_.empty()) return nullptr; - for (const auto stream: streams_) + for (const auto stream : streams_) { if (stream->getCodec() != "null") return stream;