$NetBSD: patch-client_player_sun__player.cpp,v 1.1 2022/07/03 16:09:15 nia Exp $ Add Sun Audio support for NetBSD. --- client/player/sun_player.cpp.orig 2022-07-03 14:25:19.031712372 +0000 +++ client/player/sun_player.cpp @@ -0,0 +1,305 @@ +/*** + This file is part of snapcast + Copyright (C) 2014-2021 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 +#include +#include "sun_player.hpp" +#include "common/aixlog.hpp" +#include "common/snap_exception.hpp" +#include "common/str_compat.hpp" +#include "common/utils/logging.hpp" +#include "common/utils/string_utils.hpp" + +#ifndef SUN_MAXDEVS +#define SUN_MAXDEVS (16) +#endif + +#ifndef AUDIO_GETBUFINFO +#define AUDIO_GETBUFINFO AUDIO_GETINFO +#endif + +#ifndef AUDIO_ENCODING_SLIENAR +#define AUDIO_ENCODING_SLIENAR AUDIO_ENCODING_LINEAR8; +#endif + +#ifndef AUDIO_ENCODING_SLIENAR_LE +#define AUDIO_ENCODING_SLIENAR_LE AUDIO_ENCODING_LINEAR; +#endif + +using namespace std::chrono_literals; +using namespace std; + +namespace player +{ + +static constexpr std::chrono::milliseconds BUFFER_TIME = 10ms; +static constexpr int PERIODS = 3; +static constexpr int MIN_PERIODS = 1; + +static constexpr auto LOG_TAG = "Sun"; + +SunPlayer::SunPlayer(boost::asio::io_context& io_context, const ClientSettings::Player& settings, std::shared_ptr stream) + : Player(io_context, settings, stream), handle_(-1) +{ +} + +void SunPlayer::initSun() +{ + std::lock_guard lock(mutex_); + const char *dev; + const SampleFormat& format = stream_->getFormat(); + struct audio_info info; + uint32_t rate = format.rate(); + int channels = format.channels(); + + // Open the PCM device in playback mode + if (settings_.pcm_device.name == "default") + dev = "/dev/audio"; + else + dev = settings_.pcm_device.name.c_str(); + + if ((handle_ = open(dev, O_WRONLY)) < 0) + throw SnapException("Can't open " + settings_.pcm_device.name + ", error: " + strerror(errno)); + + AUDIO_INITINFO(&info); + + switch (format.bits()) { + case 8: + info.play.encoding = AUDIO_ENCODING_SLINEAR; + info.play.precision = 8; + break; + case 16: + info.play.encoding = AUDIO_ENCODING_SLINEAR_LE; + info.play.precision = 16; + break; + case 32: + info.play.encoding = AUDIO_ENCODING_SLINEAR_LE; + info.play.precision = 32; + break; + default: + throw SnapException("Unsupported sample format: " + cpt::to_string(format.bits())); + break; + } + + if (ioctl(handle_, AUDIO_SETINFO, &info) < 0) + { + throw SnapException("Unsupported sample format: " + cpt::to_string(format.bits())); + } + + AUDIO_INITINFO(&info); + + info.play.channels = channels; + + if (ioctl(handle_, AUDIO_SETINFO, &info) < 0) + { + throw SnapException("Can't set channel count: " + string(strerror(errno))); + } + + AUDIO_INITINFO(&info); + + info.play.sample_rate = rate; + + if (ioctl(handle_, AUDIO_SETINFO, &info) < 0) + { + throw SnapException("Can't set rate: " + string(strerror(errno))); + } + + (void)ioctl(handle_, AUDIO_GETINFO, &info); + + rate = info.play.sample_rate; + + if (format.rate() != rate) + LOG(WARNING, LOG_TAG) << "Could not set sample rate to " << format.rate() << " Hz, using: " << rate << " Hz\n"; + + AUDIO_INITINFO(&info); + info.hiwat = PERIODS; + info.lowat = MIN_PERIODS; + (void)ioctl(handle_, AUDIO_SETINFO, &info); +} + + +void SunPlayer::uninitSun() +{ + std::lock_guard lock(mutex_); + if (handle_ != -1) + { + close(handle_); + handle_ = -1; + } +} + + +void SunPlayer::start() +{ + try + { + initSun(); + } + 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(); +} + + +SunPlayer::~SunPlayer() +{ + stop(); +} + + +void SunPlayer::stop() +{ + Player::stop(); + uninitSun(); +} + + +bool SunPlayer::needsThread() const +{ + return true; +} + +void SunPlayer::worker() +{ + unsigned int framesDelay; + unsigned int framesAvail; + long lastChunkTick = chronos::getTickCount(); + const SampleFormat& format = stream_->getFormat(); + struct audio_info info; + + while (active_) + { + if (handle_ == -1) + { + try + { + initSun(); + } + catch (const std::exception& e) + { + LOG(ERROR, LOG_TAG) << "Exception in initSun: " << e.what() << endl; + chronos::sleep(100); + } + if (handle_ == -1) + continue; + } + + if (ioctl(handle_, AUDIO_GETBUFINFO, &info) == -1) { + this_thread::sleep_for(10ms); + continue; + } + + framesDelay = info.play.seek / format.frameSize(); + framesAvail = (info.play.buffer_size - info.play.seek) / format.frameSize(); + + if (buffer_.size() < static_cast(info.play.buffer_size)) + { + LOG(DEBUG, LOG_TAG) << "Resizing buffer from " << buffer_.size() << " to " << info.play.buffer_size << "\n"; + buffer_.resize(info.play.buffer_size); + } + + if (framesAvail == 0) + { + auto frame_time = std::chrono::microseconds(static_cast((info.blocksize / format.frameSize()) / format.usRate())); + std::chrono::microseconds wait = std::min(frame_time / 2, std::chrono::microseconds(10ms)); + LOG(DEBUG, LOG_TAG) << "No frames available, waiting for " << wait.count() << " us\n"; + this_thread::sleep_for(wait); + continue; + } + + + chronos::usec delay(static_cast(1000 * static_cast(framesDelay) / format.msRate())); + + if (stream_->getPlayerChunk(buffer_.data(), delay, framesAvail)) + { + lastChunkTick = chronos::getTickCount(); + adjustVolume(buffer_.data(), framesAvail); + if (write(handle_, buffer_.data(), framesAvail * format.frameSize()) < 0) + { + LOG(ERROR, LOG_TAG) << "ERROR. Can't write to PCM device: " << strerror(errno) << "\n"; + uninitSun(); + } + } + else + { + LOG(INFO, LOG_TAG) << "Failed to get chunk\n"; + while (active_ && !stream_->waitForChunk(100ms)) + { + static utils::logging::TimeConditional cond(2s); + LOG(DEBUG, LOG_TAG) << cond << "Waiting for chunk\n"; + if ((handle_ != -1) && (chronos::getTickCount() - lastChunkTick > 5000)) + { + LOG(NOTICE, LOG_TAG) << "No chunk received for 5000ms. Closing audio device.\n"; + uninitSun(); + stream_->clearChunks(); + } + } + } + } +} + + + +vector SunPlayer::pcm_list() +{ + std::string name; + struct audio_device dev; + vector result; + PcmDevice pcmDevice; + int i; + int fd; + int props; + + for (i = 0; i < SUN_MAXDEVS; ++i) + { + name = "/dev/audio" + cpt::to_string(i); + fd = open(name.c_str(), O_WRONLY); + if (fd == -1) + break; + if (ioctl(fd, AUDIO_GETPROPS, &props) == -1) + { + close(fd); + break; + } + if (ioctl(fd, AUDIO_GETDEV, &dev) == -1) + { + close(fd); + break; + } + close(fd); + if ((props & AUDIO_PROP_PLAYBACK) == 0) + { + continue; + } + pcmDevice.name = name; + pcmDevice.description = string(dev.name) + " " + string(dev.version); + pcmDevice.idx = i; + result.push_back(pcmDevice); + } + return result; +} + +} // namespace player