From 275a53a8456ea17583f66b80509e9cd097da59a4 Mon Sep 17 00:00:00 2001 From: badaix Date: Wed, 13 Mar 2024 20:27:13 +0100 Subject: [PATCH] Remove PosixStream layer --- client/snapclient.1 | 2 +- doc/configuration.md | 2 +- server/CMakeLists.txt | 1 - server/etc/snapserver.conf | 2 +- server/snapserver.1 | 2 +- server/streamreader/airplay_stream.cpp | 10 +- server/streamreader/airplay_stream.hpp | 6 +- server/streamreader/alsa_stream.cpp | 53 +--------- server/streamreader/alsa_stream.hpp | 7 +- server/streamreader/asio_stream.hpp | 133 +++++++++++++++---------- server/streamreader/file_stream.cpp | 6 +- server/streamreader/file_stream.hpp | 10 +- server/streamreader/meta_stream.hpp | 6 +- server/streamreader/pcm_stream.cpp | 58 ++++++++++- server/streamreader/pcm_stream.hpp | 11 +- server/streamreader/pipe_stream.cpp | 6 +- server/streamreader/pipe_stream.hpp | 8 +- server/streamreader/process_stream.cpp | 9 +- server/streamreader/process_stream.hpp | 12 ++- server/streamreader/tcp_stream.cpp | 13 +-- server/streamreader/tcp_stream.hpp | 6 +- 21 files changed, 201 insertions(+), 162 deletions(-) diff --git a/client/snapclient.1 b/client/snapclient.1 index 2a6de6fd..50ffb255 100644 --- a/client/snapclient.1 +++ b/client/snapclient.1 @@ -71,7 +71,7 @@ log filter :[,:]* with tag = * or and level = \fI/etc/default/snapclient\fR the daemon default configuration file .SH "COPYRIGHT" -Copyright (C) 2014-2022 Johannes Pohl (snapcast@badaix.de). +Copyright (C) 2014-2024 Johannes Pohl (snapcast@badaix.de). License GPLv3+: GNU GPL version 3 or later . This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. diff --git a/doc/configuration.md b/doc/configuration.md index b3527fa8..a332e9f0 100644 --- a/doc/configuration.md +++ b/doc/configuration.md @@ -35,7 +35,7 @@ Supported parameters for all source types: - `codec`: Override the global codec - `sampleformat`: Override the global sample format - `chunk_ms`: Override the global `chunk_ms` -- `dryout_ms`: Supported by non-blocking sourced: when no new data is read from the source, send silence to the clients +- `dryout_ms`: Supported by blocking sources: when no new data is read from the source, send silence to the clients - `controlscript`: Script to control the stream source and read and provide meta data, see [stream_plugin.md](json_rpc_api/stream_plugin.md) - `controlscriptparams`: Control script command line arguments, must be url-encoded (use `%20` instead of a space " "), e.g. `--mopidy-host=192.168.42.23%20--debug` diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index cc98a4d9..e446e795 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -21,7 +21,6 @@ set(SERVER_SOURCES streamreader/pcm_stream.cpp streamreader/tcp_stream.cpp streamreader/pipe_stream.cpp - streamreader/posix_stream.cpp streamreader/file_stream.cpp streamreader/airplay_stream.cpp streamreader/librespot_stream.cpp diff --git a/server/etc/snapserver.conf b/server/etc/snapserver.conf index 24f3d288..0afa9fe8 100644 --- a/server/etc/snapserver.conf +++ b/server/etc/snapserver.conf @@ -118,7 +118,7 @@ doc_root = /usr/share/snapserver/snapweb # parameters have the form "key=value", they are concatenated with an "&" character # parameter "name" is mandatory for all sources, while codec, sampleformat and chunk_ms are optional # and will override the default codec, sampleformat or chunk_ms settings -# Non blocking sources support the dryout_ms parameter: when no new data is read from the source, send silence to the clients +# Blocking sources support the dryout_ms parameter: when no new data is read from the source, send silence to the clients # Available types are: # pipe: pipe:///?name=[&mode=create][&dryout_ms=2000], mode can be "create" or "read" # librespot: librespot:///?name=[&dryout_ms=2000][&username=&password=][&devicename=Snapcast][&bitrate=320][&wd_timeout=7800][&volume=100][&onevent=""][&nomalize=false][&autoplay=false][¶ms=] diff --git a/server/snapserver.1 b/server/snapserver.1 index c9b175c5..d8cb3865 100644 --- a/server/snapserver.1 +++ b/server/snapserver.1 @@ -41,7 +41,7 @@ the snapserver configuration file \fI~/.config/snapcast/server.json\fR or (if $HOME is not set) \fI/var/lib/snapcast/server.json\fR persistent server data file .SH "COPYRIGHT" -Copyright (C) 2014-2022 Johannes Pohl (snapcast@badaix.de). +Copyright (C) 2014-2024 Johannes Pohl (snapcast@badaix.de). License GPLv3+: GNU GPL version 3 or later . This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. diff --git a/server/streamreader/airplay_stream.cpp b/server/streamreader/airplay_stream.cpp index dcbebae4..18744589 100644 --- a/server/streamreader/airplay_stream.cpp +++ b/server/streamreader/airplay_stream.cpp @@ -1,6 +1,6 @@ /*** This file is part of snapcast - Copyright (C) 2014-2021 Johannes Pohl + Copyright (C) 2014-2024 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 @@ -209,16 +209,16 @@ void AirplayStream::setParamsAndPipePathFromPort() } -void AirplayStream::do_connect() +void AirplayStream::connect() { - ProcessStream::do_connect(); + ProcessStream::connect(); pipeReadLine(); } -void AirplayStream::do_disconnect() +void AirplayStream::disconnect() { - ProcessStream::do_disconnect(); + ProcessStream::disconnect(); // Shairpot-sync created but does not remove the pipe if (utils::file::exists(pipePath_) && (remove(pipePath_.c_str()) != 0)) LOG(INFO, LOG_TAG) << "Failed to remove metadata pipe \"" << pipePath_ << "\": " << errno << "\n"; diff --git a/server/streamreader/airplay_stream.hpp b/server/streamreader/airplay_stream.hpp index 1356f86d..f9c0f430 100644 --- a/server/streamreader/airplay_stream.hpp +++ b/server/streamreader/airplay_stream.hpp @@ -1,6 +1,6 @@ /*** This file is part of snapcast - Copyright (C) 2014-2021 Johannes Pohl + Copyright (C) 2014-2024 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 @@ -84,8 +84,8 @@ protected: void setParamsAndPipePathFromPort(); - void do_connect() override; - void do_disconnect() override; + void connect() override; + void disconnect() override; void onStderrMsg(const std::string& line) override; void initExeAndPath(const std::string& filename) override; diff --git a/server/streamreader/alsa_stream.cpp b/server/streamreader/alsa_stream.cpp index b6235a24..f16c546a 100644 --- a/server/streamreader/alsa_stream.cpp +++ b/server/streamreader/alsa_stream.cpp @@ -1,6 +1,6 @@ /*** This file is part of snapcast - Copyright (C) 2014-2021 Johannes Pohl + Copyright (C) 2014-2024 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 @@ -76,18 +76,6 @@ AlsaStream::AlsaStream(PcmStream::Listener* pcmListener, boost::asio::io_context device_ = uri_.getQuery("device", "hw:0"); send_silence_ = (uri_.getQuery("send_silence", "false") == "true"); idle_threshold_ = std::chrono::milliseconds(std::max(cpt::stoi(uri_.getQuery("idle_threshold", "100")), 10)); - double silence_threshold_percent = 0.; - try - { - silence_threshold_percent = cpt::stod(uri_.getQuery("silence_threshold_percent", "0")); - } - catch (...) - { - } - int32_t max_amplitude = std::pow(2, sampleFormat_.bits() - 1) - 1; - silence_threshold_ = max_amplitude * (silence_threshold_percent / 100.); - LOG(DEBUG, LOG_TAG) << "Device: " << device_ << ", silence threshold percent: " << silence_threshold_percent - << ", silence threshold amplitude: " << silence_threshold_ << "\n"; } @@ -99,10 +87,6 @@ void AlsaStream::start() // max_idle_bytes_ = sampleFormat_.rate() * sampleFormat_.frameSize() * dryout_ms_ / 1000; initAlsa(); - chunk_ = std::make_unique(sampleFormat_, chunk_ms_); - silent_chunk_ = std::vector(chunk_->payloadSize, 0); - LOG(DEBUG, LOG_TAG) << "Chunk duration: " << chunk_->durationMs() << " ms, frames: " << chunk_->getFrameCount() << ", size: " << chunk_->payloadSize - << "\n"; first_ = true; tvEncodedChunk_ = std::chrono::steady_clock::now(); PcmStream::start(); @@ -205,41 +189,6 @@ void AlsaStream::uninitAlsa() } -bool AlsaStream::isSilent(const msg::PcmChunk& chunk) const -{ - if (silence_threshold_ == 0) - return (std::memcmp(chunk.payload, silent_chunk_.data(), silent_chunk_.size()) == 0); - - if (sampleFormat_.sampleSize() == 1) - { - auto payload = chunk.getPayload(); - for (size_t n = 0; n < payload.second; ++n) - { - if (abs(payload.first[n]) > silence_threshold_) - return false; - } - } - else if (sampleFormat_.sampleSize() == 2) - { - auto payload = chunk.getPayload(); - for (size_t n = 0; n < payload.second; ++n) - { - if (abs(payload.first[n]) > silence_threshold_) - return false; - } - } - else if (sampleFormat_.sampleSize() == 4) - { - auto payload = chunk.getPayload(); - for (size_t n = 0; n < payload.second; ++n) - { - if (abs(payload.first[n]) > silence_threshold_) - return false; - } - } - return true; -} - void AlsaStream::do_read() { try diff --git a/server/streamreader/alsa_stream.hpp b/server/streamreader/alsa_stream.hpp index 9d74d9ac..12ed8b88 100644 --- a/server/streamreader/alsa_stream.hpp +++ b/server/streamreader/alsa_stream.hpp @@ -1,6 +1,6 @@ /*** This file is part of snapcast - Copyright (C) 2014-2022 Johannes Pohl + Copyright (C) 2014-2024 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 @@ -52,16 +52,12 @@ protected: void initAlsa(); void uninitAlsa(); - /// check if the chunk's volume is below the silence threshold - bool isSilent(const msg::PcmChunk& chunk) const; snd_pcm_t* handle_; - std::unique_ptr chunk_; bool first_; std::chrono::time_point nextTick_; boost::asio::steady_timer read_timer_; std::string device_; - std::vector silent_chunk_; std::chrono::microseconds silence_; std::string lastException_; @@ -69,7 +65,6 @@ protected: bool send_silence_; /// silence duration before switching the stream to idle std::chrono::milliseconds idle_threshold_; - int32_t silence_threshold_ = 0; }; } // namespace streamreader diff --git a/server/streamreader/asio_stream.hpp b/server/streamreader/asio_stream.hpp index 3b2881c4..eeddf8e3 100644 --- a/server/streamreader/asio_stream.hpp +++ b/server/streamreader/asio_stream.hpp @@ -1,6 +1,6 @@ /*** This file is part of snapcast - Copyright (C) 2014-2022 Johannes Pohl + Copyright (C) 2014-2024 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 @@ -36,6 +36,8 @@ namespace streamreader { +using namespace std::chrono_literals; + template class AsioStream : public PcmStream { @@ -46,20 +48,21 @@ public: void start() override; void stop() override; - virtual void connect(); +protected: + virtual void connect() = 0; virtual void disconnect(); -protected: - virtual void do_connect() = 0; - virtual void do_disconnect() = 0; virtual void on_connect(); virtual void do_read(); - void check_state(); + /// Start a timer that will change the stream state to idle after \p duration + void check_state(const std::chrono::steady_clock::duration& duration); + /// Use Timer \p timer to call \p handler after \p duration template void wait(Timer& timer, const std::chrono::duration& duration, std::function handler); - std::unique_ptr chunk_; + /// Cache last exception to avoid repeated error logging + std::string lastException_; timeval tv_chunk_; bool first_; std::chrono::time_point nextTick_; @@ -67,7 +70,11 @@ protected: boost::asio::steady_timer read_timer_; boost::asio::steady_timer state_timer_; std::unique_ptr stream_; - std::atomic bytes_read_; + + /// duration of the current silence period + std::chrono::microseconds silence_{0ms}; + /// silence duration before switching the stream to idle + std::chrono::milliseconds idle_threshold_; }; @@ -95,11 +102,11 @@ template AsioStream::AsioStream(PcmStream::Listener* pcmListener, boost::asio::io_context& ioc, const ServerSettings& server_settings, const StreamUri& uri) : PcmStream(pcmListener, ioc, server_settings, uri), read_timer_(strand_), state_timer_(strand_) { - chunk_ = std::make_unique(sampleFormat_, chunk_ms_); LOG(DEBUG, "AsioStream") << "Chunk duration: " << chunk_->durationMs() << " ms, frames: " << chunk_->getFrameCount() << ", size: " << chunk_->payloadSize << "\n"; - bytes_read_ = 0; + idle_threshold_ = std::chrono::milliseconds(std::max(cpt::stoi(uri_.getQuery("idle_threshold", "100")), 10)); + buffer_ms_ = 50; try @@ -113,18 +120,19 @@ AsioStream::AsioStream(PcmStream::Listener* pcmListener, boost::asio template -void AsioStream::check_state() +void AsioStream::check_state(const std::chrono::steady_clock::duration& duration) { - uint64_t last_read = bytes_read_; - wait(state_timer_, std::chrono::milliseconds(500 + chunk_ms_), - [this, last_read] - { - LOG(TRACE, "AsioStream") << "check state last: " << last_read << ", read: " << bytes_read_ << "\n"; - if (bytes_read_ != last_read) - setState(ReaderState::kPlaying); - else + state_timer_.expires_after(duration); + state_timer_.async_wait( + [this, duration](const boost::system::error_code& ec) + { + if (!ec) + { + + LOG(INFO, "AsioStream") << "No data since " << std::chrono::duration_cast(duration).count() + << " ms, switchung to idle\n"; setState(ReaderState::kIdle); - check_state(); + } }); } @@ -133,32 +141,25 @@ template void AsioStream::start() { PcmStream::start(); - check_state(); connect(); } -template -void AsioStream::connect() -{ - do_connect(); -} - - -template -void AsioStream::disconnect() -{ - do_disconnect(); -} - - template void AsioStream::stop() { - active_ = false; read_timer_.cancel(); - state_timer_.cancel(); disconnect(); + PcmStream::stop(); +} + + +template +void AsioStream::disconnect() +{ + if (stream_ && stream_->is_open()) + stream_->close(); + setState(ReaderState::kIdle); } @@ -174,35 +175,60 @@ void AsioStream::on_connect() template void AsioStream::do_read() { - // LOG(DEBUG, "AsioStream") << "do_read\n"; + // Reset the silence timer + check_state(idle_threshold_ + std::chrono::milliseconds(chunk_ms_)); boost::asio::async_read(*stream_, boost::asio::buffer(chunk_->payload, chunk_->payloadSize), [this](boost::system::error_code ec, std::size_t length) mutable { + state_timer_.cancel(); + if (ec) { - LOG(ERROR, "AsioStream") << "Error reading message: " << ec.message() << ", length: " << length << "\n"; - connect(); + if (lastException_ != ec.message()) + { + LOG(ERROR, "AsioStream") << "Error reading message: " << ec.message() << ", length: " << length << ", ec: " << ec << "\n"; + lastException_ = ec.message(); + } + disconnect(); + wait(read_timer_, 100ms, [this] { connect(); }); return; } - bytes_read_ += length; + lastException_.clear(); + + if (isSilent(*chunk_)) + { + silence_ += chunk_->duration(); + if (silence_ >= idle_threshold_) + { + setState(ReaderState::kIdle); + // Avoid overflow + silence_ = idle_threshold_; + } + } + else + { + silence_ = 0ms; + setState(ReaderState::kPlaying); + } + // LOG(DEBUG, "AsioStream") << "Read: " << length << " bytes\n"; // First read after connect. Set the initial read timestamp // the timestamp will be incremented after encoding, // since we do not know how much the encoder actually encoded - if (!first_) - { - auto now = std::chrono::steady_clock::now(); - auto stream2systime_diff = now - tvEncodedChunk_; - if (stream2systime_diff > chronos::sec(5) + chronos::msec(chunk_ms_)) - { - LOG(WARNING, "AsioStream") << "Stream and system time out of sync: " - << std::chrono::duration_cast(stream2systime_diff).count() / 1000. - << " ms, resetting stream time.\n"; - first_ = true; - } - } + // if (!first_) + // { + // auto now = std::chrono::steady_clock::now(); + // auto stream2systime_diff = now - tvEncodedChunk_; + // if (stream2systime_diff > chronos::sec(5) + chronos::msec(chunk_ms_)) + // { + // LOG(WARNING, "AsioStream") << "Stream and system time out of sync: " + // << std::chrono::duration_cast(stream2systime_diff).count() / 1000. + // << " ms, resetting stream time.\n"; + // first_ = true; + // } + // } if (first_) { first_ = false; @@ -236,7 +262,6 @@ void AsioStream::do_read() else { resync(std::chrono::duration_cast(currentTick - nextTick_)); - nextTick_ = currentTick + std::chrono::milliseconds(buffer_ms_); first_ = true; do_read(); } diff --git a/server/streamreader/file_stream.cpp b/server/streamreader/file_stream.cpp index 3955c6a3..8f406709 100644 --- a/server/streamreader/file_stream.cpp +++ b/server/streamreader/file_stream.cpp @@ -1,6 +1,6 @@ /*** This file is part of snapcast - Copyright (C) 2014-2023 Johannes Pohl + Copyright (C) 2014-2024 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 @@ -40,7 +40,7 @@ static constexpr auto LOG_TAG = "FileStream"; FileStream::FileStream(PcmStream::Listener* pcmListener, boost::asio::io_context& ioc, const ServerSettings& server_settings, const StreamUri& uri) - : PosixStream(pcmListener, ioc, server_settings, uri) + : AsioStream(pcmListener, ioc, server_settings, uri) { struct stat buffer; if (stat(uri_.path.c_str(), &buffer) != 0) @@ -60,7 +60,7 @@ FileStream::FileStream(PcmStream::Listener* pcmListener, boost::asio::io_context } -void FileStream::do_connect() +void FileStream::connect() { LOG(DEBUG, LOG_TAG) << "connect\n"; int fd = open(uri_.path.c_str(), O_RDONLY | O_NONBLOCK); diff --git a/server/streamreader/file_stream.hpp b/server/streamreader/file_stream.hpp index ca182c78..87e0d067 100644 --- a/server/streamreader/file_stream.hpp +++ b/server/streamreader/file_stream.hpp @@ -1,6 +1,6 @@ /*** This file is part of snapcast - Copyright (C) 2014-2021 Johannes Pohl + Copyright (C) 2014-2024 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 @@ -20,25 +20,27 @@ #define FILE_STREAM_HPP // local headers -#include "posix_stream.hpp" +#include "asio_stream.hpp" namespace streamreader { +using boost::asio::posix::stream_descriptor; + /// Reads and decodes PCM data from a file /** * Reads PCM from a file and passes the data to an encoder. * Implements EncoderListener to get the encoded data. * Data is passed to the PcmStream::Listener */ -class FileStream : public PosixStream +class FileStream : public AsioStream { public: /// ctor. Encoded PCM data is passed to the PipeListener FileStream(PcmStream::Listener* pcmListener, boost::asio::io_context& ioc, const ServerSettings& server_settings, const StreamUri& uri); protected: - void do_connect() override; + void connect() override; }; } // namespace streamreader diff --git a/server/streamreader/meta_stream.hpp b/server/streamreader/meta_stream.hpp index 8a2df450..e5287beb 100644 --- a/server/streamreader/meta_stream.hpp +++ b/server/streamreader/meta_stream.hpp @@ -1,6 +1,6 @@ /*** This file is part of snapcast - Copyright (C) 2014-2023 Johannes Pohl + Copyright (C) 2014-2024 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 @@ -21,7 +21,7 @@ // local headers #include "common/resampler.hpp" -#include "posix_stream.hpp" +#include "pcm_stream.hpp" // standard headers #include @@ -29,6 +29,8 @@ namespace streamreader { +// Mixing digital audio: +// https://www.vttoth.com/CMS/technical-notes/?view=article&id=68 /// Reads and decodes PCM data /** diff --git a/server/streamreader/pcm_stream.cpp b/server/streamreader/pcm_stream.cpp index 7bc36777..5a2d1a1b 100644 --- a/server/streamreader/pcm_stream.cpp +++ b/server/streamreader/pcm_stream.cpp @@ -1,6 +1,6 @@ /*** This file is part of snapcast - Copyright (C) 2014-2022 Johannes Pohl + Copyright (C) 2014-2024 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 @@ -60,6 +60,10 @@ PcmStream::PcmStream(PcmStream::Listener* pcmListener, boost::asio::io_context& if (uri_.query.find(kUriSampleFormat) == uri_.query.end()) throw SnapException("Stream URI must have a sampleformat"); sampleFormat_ = SampleFormat(uri_.query[kUriSampleFormat]); + chunk_ = std::make_unique(sampleFormat_, chunk_ms_); + silent_chunk_ = std::vector(chunk_->payloadSize, 0); + LOG(DEBUG, LOG_TAG) << "Chunk duration: " << chunk_->durationMs() << " ms, frames: " << chunk_->getFrameCount() << ", size: " << chunk_->payloadSize + << "\n"; LOG(INFO, LOG_TAG) << "PcmStream: " << name_ << ", sampleFormat: " << sampleFormat_.toString() << "\n"; if (uri_.query.find(kControlScript) != uri_.query.end()) @@ -72,6 +76,18 @@ PcmStream::PcmStream(PcmStream::Listener* pcmListener, boost::asio::io_context& if (uri_.query.find(kUriChunkMs) != uri_.query.end()) chunk_ms_ = cpt::stoul(uri_.query[kUriChunkMs]); + + double silence_threshold_percent = 0.; + try + { + silence_threshold_percent = cpt::stod(uri_.getQuery("silence_threshold_percent", "0")); + } + catch (...) + { + } + int32_t max_amplitude = std::pow(2, sampleFormat_.bits() - 1) - 1; + silence_threshold_ = max_amplitude * (silence_threshold_percent / 100.); + LOG(DEBUG, LOG_TAG) << "Silence threshold percent: " << silence_threshold_percent << ", silence threshold amplitude: " << silence_threshold_ << "\n"; } @@ -216,7 +232,6 @@ void PcmStream::start() << "\n"; encoder_->init([this](const encoder::Encoder& encoder, std::shared_ptr chunk, double duration) { chunkEncoded(encoder, chunk, duration); }, sampleFormat_); - active_ = true; if (stream_ctrl_) { @@ -224,12 +239,51 @@ void PcmStream::start() getId(), server_settings_, [this](const jsonrpcpp::Notification& notification) { onControlNotification(notification); }, [this](const jsonrpcpp::Request& request) { onControlRequest(request); }, [this](std::string message) { onControlLog(std::move(message)); }); } + + active_ = true; } void PcmStream::stop() { active_ = false; + setState(ReaderState::kIdle); +} + + +bool PcmStream::isSilent(const msg::PcmChunk& chunk) const +{ + if (silence_threshold_ == 0) + return (std::memcmp(chunk.payload, silent_chunk_.data(), silent_chunk_.size()) == 0); + + if (sampleFormat_.sampleSize() == 1) + { + auto payload = chunk.getPayload(); + for (size_t n = 0; n < payload.second; ++n) + { + if (abs(payload.first[n]) > silence_threshold_) + return false; + } + } + else if (sampleFormat_.sampleSize() == 2) + { + auto payload = chunk.getPayload(); + for (size_t n = 0; n < payload.second; ++n) + { + if (abs(payload.first[n]) > silence_threshold_) + return false; + } + } + else if (sampleFormat_.sampleSize() == 4) + { + auto payload = chunk.getPayload(); + for (size_t n = 0; n < payload.second; ++n) + { + if (abs(payload.first[n]) > silence_threshold_) + return false; + } + } + return true; } diff --git a/server/streamreader/pcm_stream.hpp b/server/streamreader/pcm_stream.hpp index cae64130..45191da6 100644 --- a/server/streamreader/pcm_stream.hpp +++ b/server/streamreader/pcm_stream.hpp @@ -1,6 +1,6 @@ /*** This file is part of snapcast - Copyright (C) 2014-2023 Johannes Pohl + Copyright (C) 2014-2024 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 @@ -162,6 +162,9 @@ public: protected: std::atomic active_; + /// check if the volume of the \p chunk is below the silence threshold + bool isSilent(const msg::PcmChunk& chunk) const; + void setState(ReaderState newState); void chunkRead(const msg::PcmChunk& chunk); void resync(const std::chrono::nanoseconds& duration); @@ -196,6 +199,12 @@ protected: std::atomic req_id_; boost::asio::steady_timer property_timer_; mutable std::recursive_mutex mutex_; + /// If a chunk's max amplitude is below the threshold, it is considered silent + int32_t silence_threshold_ = 0; + /// Current chunk + std::unique_ptr chunk_; + /// Silent chunk (all 0), for fast silence detection (memcmp) + std::vector silent_chunk_; }; diff --git a/server/streamreader/pipe_stream.cpp b/server/streamreader/pipe_stream.cpp index 65cb2864..69f0350b 100644 --- a/server/streamreader/pipe_stream.cpp +++ b/server/streamreader/pipe_stream.cpp @@ -1,6 +1,6 @@ /*** This file is part of snapcast - Copyright (C) 2014-2021 Johannes Pohl + Copyright (C) 2014-2024 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 @@ -38,7 +38,7 @@ static constexpr auto LOG_TAG = "PipeStream"; PipeStream::PipeStream(PcmStream::Listener* pcmListener, boost::asio::io_context& ioc, const ServerSettings& server_settings, const StreamUri& uri) - : PosixStream(pcmListener, ioc, server_settings, uri) + : AsioStream(pcmListener, ioc, server_settings, uri) { umask(0); string mode = uri_.getQuery("mode", "create"); @@ -55,7 +55,7 @@ PipeStream::PipeStream(PcmStream::Listener* pcmListener, boost::asio::io_context } -void PipeStream::do_connect() +void PipeStream::connect() { int fd = open(uri_.path.c_str(), O_RDONLY | O_NONBLOCK); if (fd < 0) diff --git a/server/streamreader/pipe_stream.hpp b/server/streamreader/pipe_stream.hpp index 8f7a140f..c1959292 100644 --- a/server/streamreader/pipe_stream.hpp +++ b/server/streamreader/pipe_stream.hpp @@ -1,6 +1,6 @@ /*** This file is part of snapcast - Copyright (C) 2014-2021 Johannes Pohl + Copyright (C) 2014-2024 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 @@ -20,7 +20,7 @@ #define PIPE_STREAM_HPP // local headers -#include "posix_stream.hpp" +#include "asio_stream.hpp" namespace streamreader { @@ -34,14 +34,14 @@ using boost::asio::posix::stream_descriptor; * Implements EncoderListener to get the encoded data. * Data is passed to the PcmStream::Listener */ -class PipeStream : public PosixStream +class PipeStream : public AsioStream { public: /// ctor. Encoded PCM data is passed to the PipeListener PipeStream(PcmStream::Listener* pcmListener, boost::asio::io_context& ioc, const ServerSettings& server_settings, const StreamUri& uri); protected: - void do_connect() override; + void connect() override; }; } // namespace streamreader diff --git a/server/streamreader/process_stream.cpp b/server/streamreader/process_stream.cpp index 03152eac..8b07a881 100644 --- a/server/streamreader/process_stream.cpp +++ b/server/streamreader/process_stream.cpp @@ -1,6 +1,6 @@ /*** This file is part of snapcast - Copyright (C) 2014-2021 Johannes Pohl + Copyright (C) 2014-2024 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 @@ -41,7 +41,7 @@ static constexpr auto LOG_TAG = "ProcessStream"; ProcessStream::ProcessStream(PcmStream::Listener* pcmListener, boost::asio::io_context& ioc, const ServerSettings& server_settings, const StreamUri& uri) - : PosixStream(pcmListener, ioc, server_settings, uri) + : AsioStream(pcmListener, ioc, server_settings, uri) { params_ = uri_.getQuery("params"); wd_timeout_sec_ = cpt::stoul(uri_.getQuery("wd_timeout", "0")); @@ -95,7 +95,7 @@ void ProcessStream::initExeAndPath(const std::string& filename) } -void ProcessStream::do_connect() +void ProcessStream::connect() { if (!active_) return; @@ -127,10 +127,11 @@ void ProcessStream::do_connect() } -void ProcessStream::do_disconnect() +void ProcessStream::disconnect() { if (process_.running()) ::kill(-process_.native_handle(), SIGINT); + AsioStream::disconnect(); } diff --git a/server/streamreader/process_stream.hpp b/server/streamreader/process_stream.hpp index 4de6dbdd..97ac7090 100644 --- a/server/streamreader/process_stream.hpp +++ b/server/streamreader/process_stream.hpp @@ -1,6 +1,6 @@ /*** This file is part of snapcast - Copyright (C) 2014-2021 Johannes Pohl + Copyright (C) 2014-2024 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 @@ -20,7 +20,7 @@ #define PROCESS_STREAM_HPP // local headers -#include "posix_stream.hpp" +#include "asio_stream.hpp" #include "watchdog.hpp" // standard headers @@ -35,13 +35,15 @@ namespace bp = boost::process; namespace streamreader { +using boost::asio::posix::stream_descriptor; + /// Starts an external process and reads and PCM data from stdout /** * Starts an external process, reads PCM data from stdout, and passes the data to an encoder. * Implements EncoderListener to get the encoded data. * Data is passed to the PcmStream::Listener */ -class ProcessStream : public PosixStream, public WatchdogListener +class ProcessStream : public AsioStream, public WatchdogListener { public: /// ctor. Encoded PCM data is passed to the PipeListener @@ -49,8 +51,8 @@ public: ~ProcessStream() override = default; protected: - void do_connect() override; - void do_disconnect() override; + void connect() override; + void disconnect() override; std::string exe_; std::string path_; diff --git a/server/streamreader/tcp_stream.cpp b/server/streamreader/tcp_stream.cpp index f10e097f..1ec68d6b 100644 --- a/server/streamreader/tcp_stream.cpp +++ b/server/streamreader/tcp_stream.cpp @@ -1,6 +1,6 @@ /*** This file is part of snapcast - Copyright (C) 2014-2021 Johannes Pohl + Copyright (C) 2014-2024 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 @@ -68,7 +68,7 @@ TcpStream::TcpStream(PcmStream::Listener* pcmListener, boost::asio::io_context& } -void TcpStream::do_connect() +void TcpStream::connect() { if (!active_) return; @@ -112,12 +112,13 @@ void TcpStream::do_connect() } -void TcpStream::do_disconnect() +void TcpStream::disconnect() { - if (stream_) - stream_->close(); + reconnect_timer_.cancel(); if (acceptor_) acceptor_->cancel(); - reconnect_timer_.cancel(); + AsioStream::disconnect(); } + + } // namespace streamreader diff --git a/server/streamreader/tcp_stream.hpp b/server/streamreader/tcp_stream.hpp index cb2c9da8..9a9b1b8b 100644 --- a/server/streamreader/tcp_stream.hpp +++ b/server/streamreader/tcp_stream.hpp @@ -1,6 +1,6 @@ /*** This file is part of snapcast - Copyright (C) 2014-2022 Johannes Pohl + Copyright (C) 2014-2024 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 @@ -43,8 +43,8 @@ public: TcpStream(PcmStream::Listener* pcmListener, boost::asio::io_context& ioc, const ServerSettings& server_settings, const StreamUri& uri); protected: - void do_connect() override; - void do_disconnect() override; + void connect() override; + void disconnect() override; std::unique_ptr acceptor_; std::string host_; size_t port_;