diff --git a/CMakeLists.txt b/CMakeLists.txt
index d0bae7d7..c305d406 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.2)
-project(snapcast LANGUAGES CXX VERSION 0.23.101)
+project(snapcast LANGUAGES CXX VERSION 0.23.102)
set(PROJECT_DESCRIPTION "Multiroom client-server audio player")
set(PROJECT_URL "https://github.com/badaix/snapcast")
diff --git a/client/Makefile b/client/Makefile
index 184b6fc5..1cd5f53d 100644
--- a/client/Makefile
+++ b/client/Makefile
@@ -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.23.101
+VERSION = 0.23.102
BIN = snapclient
ifeq ($(TARGET), FREEBSD)
diff --git a/client/player/alsa_player.cpp b/client/player/alsa_player.cpp
index fff67c43..b520f726 100644
--- a/client/player/alsa_player.cpp
+++ b/client/player/alsa_player.cpp
@@ -16,52 +16,287 @@
along with this program. If not, see .
***/
-#include
-
#include "alsa_player.hpp"
#include "common/aixlog.hpp"
#include "common/snap_exception.hpp"
#include "common/str_compat.hpp"
+#include "common/utils/string_utils.hpp"
-//#define BUFFER_TIME 120000
-#define PERIOD_TIME 30000
-
+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)))
+
+
static constexpr auto LOG_TAG = "Alsa";
+static constexpr auto DEFAULT_MIXER = "PCM";
+
AlsaPlayer::AlsaPlayer(boost::asio::io_context& io_context, const ClientSettings::Player& settings, std::shared_ptr stream)
- : Player(io_context, settings, stream), handle_(nullptr)
+ : Player(io_context, settings, stream), handle_(nullptr), ctl_(nullptr), mixer_(nullptr), elem_(nullptr), sd_(io_context), timer_(io_context)
{
+ if (settings_.mixer.mode == ClientSettings::Mixer::Mode::hardware)
+ {
+ string tmp;
+ if (settings_.mixer.parameter.empty())
+ mixer_name_ = DEFAULT_MIXER;
+ else
+ mixer_name_ = utils::string::split_left(settings_.mixer.parameter, ':', tmp);
+
+ string card;
+ // default:CARD=ALSA[,DEV=x] => default
+ mixer_device_ = utils::string::split_left(settings_.pcm_device.name, ':', card);
+ if (!card.empty())
+ {
+ auto pos = card.find("CARD=");
+ if (pos != string::npos)
+ {
+ card = card.substr(pos + 5);
+ card = utils::string::split_left(card, ',', tmp);
+ int card_idx = snd_card_get_index(card.c_str());
+ if ((card_idx >= 0) && (card_idx < 32))
+ mixer_device_ = "hw:" + std::to_string(card_idx);
+ }
+ }
+
+ 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";
+}
+
+
+void AlsaPlayer::setHardwareVolume(double volume, bool muted)
+{
+ std::lock_guard lock(mutex_);
+ if (elem_ == nullptr)
+ return;
+
+ last_change_ = std::chrono::steady_clock::now();
+ try
+ {
+ int val = muted ? 0 : 1;
+ int err = snd_mixer_selem_set_playback_switch_all(elem_, val);
+ if (err < 0)
+ LOG(ERROR, LOG_TAG) << "Failed to mute, error: " << snd_strerror(err) << "\n";
+
+ long minv, maxv;
+ if ((err = snd_mixer_selem_get_playback_dB_range(elem_, &minv, &maxv)) == 0)
+ {
+ double min_norm = exp10((minv - maxv) / 6000.0);
+ volume = volume * (1 - min_norm) + min_norm;
+ double mixer_volume = 6000.0 * log10(volume) + maxv;
+
+ LOG(DEBUG, LOG_TAG) << "Mixer playback dB range [" << minv << ", " << maxv << "], volume: " << volume << ", mixer volume: " << mixer_volume << "\n";
+ if ((err = snd_mixer_selem_set_playback_dB_all(elem_, mixer_volume, 0)) < 0)
+ throw SnapException(std::string("Failed to set playback volume, error: ") + snd_strerror(err));
+ }
+ else
+ {
+ if ((err = snd_mixer_selem_get_playback_volume_range(elem_, &minv, &maxv)) < 0)
+ throw SnapException(std::string("Failed to get playback volume range, error: ") + snd_strerror(err));
+
+ auto mixer_volume = volume * (maxv - minv) + minv;
+ LOG(DEBUG, LOG_TAG) << "Mixer playback volume range [" << minv << ", " << maxv << "], volume: " << volume << ", mixer volume: " << mixer_volume
+ << "\n";
+ if ((err = snd_mixer_selem_set_playback_volume_all(elem_, mixer_volume)) < 0)
+ throw SnapException(std::string("Failed to set playback volume, error: ") + snd_strerror(err));
+ }
+ }
+ catch (const std::exception& e)
+ {
+ LOG(ERROR, LOG_TAG) << "Exception: " << e.what() << "\n";
+ uninitMixer();
+ }
+}
+
+
+bool AlsaPlayer::getHardwareVolume(double& volume, bool& muted)
+{
+ try
+ {
+ std::lock_guard lock(mutex_);
+ if (elem_ == nullptr)
+ throw SnapException("Mixer not initialized");
+
+ long vol;
+ int err = 0;
+ while (snd_mixer_handle_events(mixer_) > 0)
+ this_thread::sleep_for(1us);
+ long minv, maxv;
+ if ((err = snd_mixer_selem_get_playback_dB_range(elem_, &minv, &maxv)) == 0)
+ {
+ if ((err = snd_mixer_selem_get_playback_dB(elem_, SND_MIXER_SCHN_MONO, &vol)) < 0)
+ throw SnapException(std::string("Failed to get playback volume, error: ") + snd_strerror(err));
+
+ volume = pow(10, (vol - maxv) / 6000.0);
+ if (minv != SND_CTL_TLV_DB_GAIN_MUTE)
+ {
+ double min_norm = pow(10, (minv - maxv) / 6000.0);
+ volume = (volume - min_norm) / (1 - min_norm);
+ }
+ }
+ else
+ {
+ if ((err = snd_mixer_selem_get_playback_volume_range(elem_, &minv, &maxv)) < 0)
+ throw SnapException(std::string("Failed to get playback volume range, error: ") + snd_strerror(err));
+ if ((err = snd_mixer_selem_get_playback_volume(elem_, SND_MIXER_SCHN_MONO, &vol)) < 0)
+ throw SnapException(std::string("Failed to get playback volume, error: ") + snd_strerror(err));
+
+ vol -= minv;
+ maxv = maxv - minv;
+ volume = static_cast(vol) / static_cast(maxv);
+ }
+ int val;
+ if ((err = snd_mixer_selem_get_playback_switch(elem_, SND_MIXER_SCHN_MONO, &val)) < 0)
+ throw SnapException(std::string("Failed to get mute state, error: ") + snd_strerror(err));
+ muted = (val == 0);
+ LOG(DEBUG, LOG_TAG) << "Get volume, mixer volume range [" << minv << ", " << maxv << "], volume: " << volume << ", muted: " << muted << "\n";
+ snd_mixer_handle_events(mixer_);
+ return true;
+ }
+ catch (const std::exception& e)
+ {
+ LOG(ERROR, LOG_TAG) << "Exception: " << e.what() << "\n";
+ return false;
+ }
+}
+
+
+void AlsaPlayer::waitForEvent()
+{
+ sd_.async_wait(boost::asio::posix::stream_descriptor::wait_read, [this](const boost::system::error_code& ec) {
+ if (ec)
+ {
+ // TODO: fd is "Bad" after unplugging/plugging USB DAC, i.e. after init/uninit/init cycle
+ LOG(DEBUG, LOG_TAG) << "waitForEvent error: " << ec.message() << "\n";
+ return;
+ }
+
+ std::lock_guard lock(mutex_);
+ if (ctl_ == nullptr)
+ return;
+
+ unsigned short revents;
+ snd_ctl_poll_descriptors_revents(ctl_, fd_.get(), 1, &revents);
+ if (revents & POLLIN || (revents == 0))
+ {
+ snd_ctl_event_t* event;
+ snd_ctl_event_alloca(&event);
+
+ if (((snd_ctl_read(ctl_, event) >= 0) && (snd_ctl_event_get_type(event) == SND_CTL_EVENT_ELEM)) || (revents == 0))
+ {
+ auto now = std::chrono::steady_clock::now();
+ if (now - last_change_ < 1s)
+ {
+ LOG(DEBUG, LOG_TAG) << "Last volume change by server: " << std::chrono::duration_cast(now - last_change_).count()
+ << " ms => ignoring volume change\n";
+ waitForEvent();
+ return;
+ }
+ // Sometimes the old volume is reported after this event has been raised.
+ // As workaround we defer getting the volume by 20ms.
+ timer_.cancel();
+ timer_.expires_after(20ms);
+ timer_.async_wait([this](const boost::system::error_code& ec) {
+ if (!ec)
+ {
+ double volume;
+ bool muted;
+ if (getHardwareVolume(volume, muted))
+ {
+ LOG(DEBUG, LOG_TAG) << "Volume: " << volume << ", muted: " << muted << "\n";
+ notifyVolumeChange(volume, muted);
+ }
+ }
+ });
+ }
+ }
+ waitForEvent();
+ });
+}
+
+void AlsaPlayer::initMixer()
+{
+ if (settings_.mixer.mode != ClientSettings::Mixer::Mode::hardware)
+ return;
+
+ LOG(DEBUG, LOG_TAG) << "initMixer\n";
+ std::lock_guard lock(mutex_);
+ int err;
+ if ((err = snd_ctl_open(&ctl_, mixer_device_.c_str(), SND_CTL_READONLY)) < 0)
+ throw SnapException("Can't open control for " + mixer_device_ + ", error: " + snd_strerror(err));
+ if ((err = snd_ctl_subscribe_events(ctl_, 1)) < 0)
+ throw SnapException("Can't subscribe for events for " + mixer_device_ + ", error: " + snd_strerror(err));
+ fd_ = std::unique_ptr>(new pollfd(), [](pollfd* p) {
+ close(p->fd);
+ delete p;
+ });
+ err = snd_ctl_poll_descriptors(ctl_, fd_.get(), 1);
+ LOG(DEBUG, LOG_TAG) << "Filled " << err << " poll descriptors, poll descriptor count: " << snd_ctl_poll_descriptors_count(ctl_) << ", fd: " << fd_->fd
+ << "\n";
+
+ snd_mixer_selem_id_t* sid;
+ snd_mixer_selem_id_alloca(&sid);
+ int mix_index = 0;
+ // sets simple-mixer index and name
+ snd_mixer_selem_id_set_index(sid, mix_index);
+ snd_mixer_selem_id_set_name(sid, mixer_name_.c_str());
+
+ if ((err = snd_mixer_open(&mixer_, 0)) < 0)
+ throw SnapException(std::string("Failed to open mixer, error: ") + snd_strerror(err));
+ if ((err = snd_mixer_attach(mixer_, mixer_device_.c_str())) < 0)
+ throw SnapException("Failed to attach mixer to " + mixer_device_ + ", error: " + snd_strerror(err));
+ if ((err = snd_mixer_selem_register(mixer_, NULL, NULL)) < 0)
+ throw SnapException(std::string("Failed to register selem, error: ") + snd_strerror(err));
+ if ((err = snd_mixer_load(mixer_)) < 0)
+ throw SnapException(std::string("Failed to load mixer, error: ") + snd_strerror(err));
+ elem_ = snd_mixer_find_selem(mixer_, sid);
+ if (!elem_)
+ throw SnapException("Failed to find mixer: " + mixer_name_);
+
+ sd_ = boost::asio::posix::stream_descriptor(io_context_, fd_->fd);
+ waitForEvent();
}
void AlsaPlayer::initAlsa()
{
- unsigned int tmp, rate;
- int pcm, channels;
- snd_pcm_hw_params_t* params;
+ std::lock_guard lock(mutex_);
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 */
- if ((pcm = 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 + " PCM device: " + snd_strerror(pcm));
+ // 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*/
+ // 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 ((pcm = snd_pcm_hw_params_any(handle_, params)) < 0)
- throw SnapException("Can't fill params: " + string(snd_strerror(pcm)));
+ if ((err = snd_pcm_hw_params_any(handle_, params)) < 0)
+ throw SnapException("Can't fill params: " + string(snd_strerror(err)));
snd_output_t* output;
if (snd_output_buffer_open(&output) == 0)
@@ -75,9 +310,9 @@ void AlsaPlayer::initAlsa()
snd_output_close(output);
}
- /* Set parameters */
- if ((pcm = snd_pcm_hw_params_set_access(handle_, params, SND_PCM_ACCESS_RW_INTERLEAVED)) < 0)
- throw SnapException("Can't set interleaved mode: " + string(snd_strerror(pcm)));
+ // 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)));
snd_pcm_format_t snd_pcm_format;
if (format.bits() == 8)
@@ -91,8 +326,8 @@ void AlsaPlayer::initAlsa()
else
throw SnapException("Unsupported sample format: " + cpt::to_string(format.bits()));
- pcm = snd_pcm_hw_params_set_format(handle_, params, snd_pcm_format);
- if (pcm == -EINVAL)
+ err = snd_pcm_hw_params_set_format(handle_, params, snd_pcm_format);
+ if (err == -EINVAL)
{
if (snd_pcm_format == SND_PCM_FORMAT_S24_LE)
{
@@ -105,12 +340,11 @@ void AlsaPlayer::initAlsa()
}
}
- pcm = snd_pcm_hw_params_set_format(handle_, params, snd_pcm_format);
- if (pcm < 0)
+ err = snd_pcm_hw_params_set_format(handle_, params, snd_pcm_format);
+ if (err < 0)
{
- cerr << "error: " << pcm << "\n";
stringstream ss;
- ss << "Can't set format: " << string(snd_strerror(pcm)) << ", supported: ";
+ ss << "Can't set format: " << string(snd_strerror(err)) << ", supported: ";
for (int format = 0; format <= (int)SND_PCM_FORMAT_LAST; format++)
{
snd_pcm_format_t snd_pcm_format = static_cast(format);
@@ -120,31 +354,58 @@ void AlsaPlayer::initAlsa()
throw SnapException(ss.str());
}
- if ((pcm = snd_pcm_hw_params_set_channels(handle_, params, channels)) < 0)
- throw SnapException("Can't set channels number: " + string(snd_strerror(pcm)));
+ if ((err = snd_pcm_hw_params_set_channels(handle_, params, channels)) < 0)
+ throw SnapException("Can't set channel count: " + string(snd_strerror(err)));
- if ((pcm = snd_pcm_hw_params_set_rate_near(handle_, params, &rate, nullptr)) < 0)
- throw SnapException("Can't set rate: " + string(snd_strerror(pcm)));
+ 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 period_time = buffer_time_.count() / periods_;
+ unsigned int max_period_time = period_time;
+ if ((err = snd_pcm_hw_params_get_period_time_max(params, &max_period_time, nullptr)) < 0)
+ {
+ LOG(ERROR, LOG_TAG) << "Can't get max period time: " << snd_strerror(err) << "\n";
+ }
+ else
+ {
+ if (period_time > max_period_time)
+ {
+ LOG(INFO, LOG_TAG) << "Period time too large, changing from " << period_time << " to " << max_period_time << "\n";
+ period_time = max_period_time;
+ }
+ }
+ unsigned int min_period_time = period_time;
+ if ((err = snd_pcm_hw_params_get_period_time_min(params, &min_period_time, nullptr)) < 0)
+ {
+ LOG(ERROR, LOG_TAG) << "Can't get min period time: " << snd_strerror(err) << "\n";
+ }
+ else
+ {
+ if (period_time < min_period_time)
+ {
+ LOG(INFO, LOG_TAG) << "Period time too small, changing from " << period_time << " to " << min_period_time << "\n";
+ period_time = min_period_time;
+ }
+ }
- unsigned int buffer_time = 4 * period_time;
+ if ((err = snd_pcm_hw_params_set_period_time_near(handle_, params, &period_time, nullptr)) < 0)
+ throw SnapException("Can't set period time: " + string(snd_strerror(err)));
- 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, 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";
+ // 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)));
- /* Write parameters */
- if ((pcm = snd_pcm_hw_params(handle_, params)) < 0)
- throw SnapException("Can't set hardware parameters: " + string(snd_strerror(pcm)));
+ // Write parameters
+ if ((err = snd_pcm_hw_params(handle_, params)) < 0)
+ throw SnapException("Can't set hardware parameters: " + string(snd_strerror(err)));
- /* Resume information */
+ // Resume information
unsigned int periods;
if (snd_pcm_hw_params_get_periods(params, &periods, nullptr) < 0)
periods = round((double)buffer_time / (double)period_time);
@@ -153,21 +414,7 @@ void AlsaPlayer::initAlsa()
<< ", buffer time: " << buffer_time << " us, periods: " << periods << ", period time: " << period_time
<< " us, period frames: " << frames_ << "\n";
- 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 */
- snd_pcm_hw_params_get_period_size(params, &frames_, nullptr);
- LOG(INFO, LOG_TAG) << "frames: " << frames_ << "\n";
-
- snd_pcm_hw_params_get_period_time(params, &tmp, nullptr);
- LOG(DEBUG, LOG_TAG) << "period time: " << tmp << "\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);
@@ -176,23 +423,74 @@ void AlsaPlayer::initAlsa()
snd_pcm_sw_params_set_start_threshold(handle_, swparams, frames_);
// 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();
}
-void AlsaPlayer::uninitAlsa()
+void AlsaPlayer::uninitAlsa(bool uninit_mixer)
{
+ std::lock_guard lock(mutex_);
+ if (uninit_mixer)
+ uninitMixer();
+
if (handle_ != nullptr)
{
- snd_pcm_drain(handle_);
+ snd_pcm_drop(handle_);
snd_pcm_close(handle_);
handle_ = nullptr;
}
}
+void AlsaPlayer::uninitMixer()
+{
+ if (settings_.mixer.mode != ClientSettings::Mixer::Mode::hardware)
+ return;
+
+ LOG(DEBUG, LOG_TAG) << "uninitMixer\n";
+ std::lock_guard lock(mutex_);
+ if (sd_.is_open())
+ {
+ boost::system::error_code ec;
+ sd_.cancel(ec);
+ }
+ if (ctl_ != nullptr)
+ {
+ snd_ctl_close(ctl_);
+ ctl_ = nullptr;
+ }
+ if (mixer_ != nullptr)
+ {
+ snd_mixer_close(mixer_);
+ mixer_ = nullptr;
+ }
+ fd_ = nullptr;
+ elem_ = nullptr;
+}
+
+
void AlsaPlayer::start()
{
- initAlsa();
+ try
+ {
+ initAlsa();
+ }
+ catch (const SnapException& e)
+ {
+ LOG(ERROR, LOG_TAG) << "Exception: " << e.what() << ", code: " << e.code() << "\n";
+ // Accept "Device or ressource busy", the worker loop will retry
+ if (e.code() != -EBUSY)
+ throw;
+ }
+
Player::start();
}
@@ -206,7 +504,7 @@ AlsaPlayer::~AlsaPlayer()
void AlsaPlayer::stop()
{
Player::stop();
- uninitAlsa();
+ uninitAlsa(true);
}
@@ -215,6 +513,36 @@ bool AlsaPlayer::needsThread() const
return true;
}
+
+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;
@@ -222,7 +550,6 @@ void AlsaPlayer::worker()
snd_pcm_sframes_t framesAvail;
long lastChunkTick = chronos::getTickCount();
const SampleFormat& format = stream_->getFormat();
-
while (active_)
{
if (handle_ == nullptr)
@@ -230,65 +557,64 @@ void AlsaPlayer::worker()
try
{
initAlsa();
+ // set the hardware volume. It might have changed when we were not initialized
+ if (settings_.mixer.mode == ClientSettings::Mixer::Mode::hardware)
+ setHardwareVolume(volume_, muted_);
}
catch (const std::exception& e)
{
LOG(ERROR, LOG_TAG) << "Exception in initAlsa: " << e.what() << endl;
chronos::sleep(100);
}
+ if (handle_ == nullptr)
+ continue;
}
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)
{
LOG(ERROR, LOG_TAG) << "ERROR. Can't wait for PCM to become ready: " << snd_strerror(wait_result) << "\n";
- uninitAlsa();
+ uninitAlsa(true);
+ continue;
}
else if (wait_result == 0)
{
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(5ms);
+ // continue;
+ // }
+ if (framesAvail == 0)
+ {
+ auto frame_time = std::chrono::microseconds(static_cast(frames_ / settings_.sample_format.usRate()));
+ std::chrono::microseconds wait = std::min(frame_time / 5, std::chrono::microseconds(10ms));
+ LOG(DEBUG, LOG_TAG) << "No frames available, waiting for " << wait.count() << " us\n";
+ this_thread::sleep_for(wait);
+ continue;
+ }
+
+ // LOG(TRACE, LOG_TAG) << "res: " << result << ", framesAvail: " << framesAvail << ", delay: " << framesDelay << ", frames: " << frames_ << "\n";
chronos::usec delay(static_cast(1000 * (double)framesDelay / format.msRate()));
// LOG(TRACE, LOG_TAG) << "delay: " << framesDelay << ", delay[ms]: " << delay.count() / 1000 << ", avail: " << framesAvail << "\n";
if (buffer_.size() < static_cast(framesAvail * format.frameSize()))
{
- LOG(INFO, LOG_TAG) << "Resizing buffer from " << buffer_.size() << " to " << framesAvail * format.frameSize() << "\n";
+ LOG(DEBUG, LOG_TAG) << "Resizing buffer from " << buffer_.size() << " to " << framesAvail * format.frameSize() << "\n";
buffer_.resize(framesAvail * format.frameSize());
}
if (stream_->getPlayerChunk(buffer_.data(), delay, framesAvail))
@@ -297,13 +623,13 @@ void AlsaPlayer::worker()
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)
{
LOG(ERROR, LOG_TAG) << "ERROR. Can't write to PCM device: " << snd_strerror(pcm) << "\n";
- uninitAlsa();
+ uninitAlsa(true);
}
}
else
@@ -315,7 +641,7 @@ void AlsaPlayer::worker()
if ((handle_ != nullptr) && (chronos::getTickCount() - lastChunkTick > 5000))
{
LOG(NOTICE, LOG_TAG) << "No chunk received for 5000ms. Closing ALSA.\n";
- uninitAlsa();
+ uninitAlsa(false);
stream_->clearChunks();
}
}
@@ -368,4 +694,4 @@ vector AlsaPlayer::pcm_list()
return result;
}
-} // namespace player
+} // namespace player
\ No newline at end of file
diff --git a/client/player/alsa_player.hpp b/client/player/alsa_player.hpp
index 81a90330..d68c9a36 100644
--- a/client/player/alsa_player.hpp
+++ b/client/player/alsa_player.hpp
@@ -16,11 +16,13 @@
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
@@ -38,26 +40,52 @@ public:
AlsaPlayer(boost::asio::io_context& io_context, const ClientSettings::Player& settings, std::shared_ptr stream);
~AlsaPlayer() override;
- /// Set audio volume in range [0..1]
void start() override;
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;
bool needsThread() const override;
private:
+ /// initialize alsa and the mixer (if neccessary)
void initAlsa();
- void uninitAlsa();
+ /// 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();
+
+ bool getHardwareVolume(double& volume, bool& muted) override;
+ void setHardwareVolume(double volume, bool muted) override;
+
+ void waitForEvent();
snd_pcm_t* handle_;
+ snd_ctl_t* ctl_;
+
+ snd_mixer_t* mixer_;
+ snd_mixer_elem_t* elem_;
+ std::string mixer_name_;
+ std::string mixer_device_;
+
+ std::unique_ptr> fd_;
std::vector buffer_;
snd_pcm_uframes_t frames_;
+ boost::asio::posix::stream_descriptor sd_;
+ 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
+#endif
\ No newline at end of file
diff --git a/server/Makefile b/server/Makefile
index fc3b9a51..0df9df8e 100644
--- a/server/Makefile
+++ b/server/Makefile
@@ -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.23.101
+VERSION = 0.23.102
BIN = snapserver
ifeq ($(TARGET), FREEBSD)