mirror of
https://github.com/badaix/snapcast.git
synced 2025-05-14 01:26:42 +02:00
Add support for PulseAudio (WIP)
This commit is contained in:
parent
bf7f986faa
commit
d9e0b7792d
5 changed files with 334 additions and 0 deletions
|
@ -132,6 +132,11 @@ if(NOT WIN32)
|
|||
add_definitions(-DHAS_ALSA)
|
||||
endif (ALSA_FOUND)
|
||||
|
||||
pkg_search_module(PULSE libpulse)
|
||||
if (PULSE_FOUND)
|
||||
add_definitions(-DHAS_PULSE)
|
||||
endif (PULSE_FOUND)
|
||||
|
||||
if(BUILD_WITH_AVAHI)
|
||||
pkg_search_module(AVAHI avahi-client)
|
||||
if (AVAHI_FOUND)
|
||||
|
|
|
@ -47,6 +47,12 @@ else()
|
|||
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
|
||||
|
|
|
@ -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
|
||||
|
@ -91,6 +94,9 @@ std::vector<std::string> Controller::getSupportedPlayerNames()
|
|||
#ifdef HAS_ALSA
|
||||
result.emplace_back("alsa");
|
||||
#endif
|
||||
#ifdef HAS_PULSE
|
||||
result.emplace_back("pulse");
|
||||
#endif
|
||||
#ifdef HAS_OBOE
|
||||
result.emplace_back("oboe");
|
||||
#endif
|
||||
|
@ -180,6 +186,10 @@ void Controller::getNextMessage()
|
|||
if (!player_)
|
||||
player_ = createPlayer<AlsaPlayer>(settings_.player, "alsa");
|
||||
#endif
|
||||
#ifdef HAS_PULSE
|
||||
if (!player_)
|
||||
player_ = createPlayer<PulsePlayer>(settings_.player, "pulse");
|
||||
#endif
|
||||
#ifdef HAS_OBOE
|
||||
if (!player_)
|
||||
player_ = createPlayer<OboePlayer>(settings_.player, "oboe");
|
||||
|
|
252
client/player/pulse_player.cpp
Normal file
252
client/player/pulse_player.cpp
Normal file
|
@ -0,0 +1,252 @@
|
|||
/***
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
***/
|
||||
|
||||
#include <assert.h>
|
||||
#include <iostream>
|
||||
|
||||
#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;
|
||||
|
||||
static constexpr auto LOG_TAG = "PulsePlayer";
|
||||
static constexpr auto kDefaultBuffer = 50ms;
|
||||
|
||||
|
||||
PulsePlayer::PulsePlayer(boost::asio::io_context& io_context, const ClientSettings::Player& settings, std::shared_ptr<Stream> stream)
|
||||
: Player(io_context, settings, stream), latency_(100000), pa_ml_(nullptr), pa_ctx_(nullptr), playstream_(nullptr)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
PulsePlayer::~PulsePlayer()
|
||||
{
|
||||
LOG(DEBUG, LOG_TAG) << "Destructor\n";
|
||||
stop();
|
||||
}
|
||||
|
||||
|
||||
bool PulsePlayer::needsThread() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
void PulsePlayer::worker()
|
||||
{
|
||||
// Run the mainloop until pa_mainloop_quit() is called
|
||||
// (this example never calls it, so the mainloop runs forever).
|
||||
pa_mainloop_run(pa_ml_, nullptr);
|
||||
}
|
||||
|
||||
|
||||
void PulsePlayer::underflowCallback(pa_stream* s)
|
||||
{
|
||||
// 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_ / 1000 << " ms\n";
|
||||
if (underflows_ >= 6 && latency_ < 2000000)
|
||||
{
|
||||
latency_ = (latency_ * 3) / 2;
|
||||
bufattr_.maxlength = pa_usec_to_bytes(latency_, &ss_);
|
||||
bufattr_.tlength = pa_usec_to_bytes(latency_, &ss_);
|
||||
pa_stream_set_buffer_attr(s, &bufattr_, nullptr, nullptr);
|
||||
underflows_ = 0;
|
||||
LOG(INFO, LOG_TAG) << "latency increased to " << latency_ << " ms\n";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void PulsePlayer::stateCallback(pa_context* c)
|
||||
{
|
||||
pa_context_state_t state;
|
||||
state = pa_context_get_state(c);
|
||||
switch (state)
|
||||
{
|
||||
// These are just here for reference
|
||||
case PA_CONTEXT_UNCONNECTED:
|
||||
case PA_CONTEXT_CONNECTING:
|
||||
case PA_CONTEXT_AUTHORIZING:
|
||||
case PA_CONTEXT_SETTING_NAME:
|
||||
default:
|
||||
break;
|
||||
case PA_CONTEXT_FAILED:
|
||||
case PA_CONTEXT_TERMINATED:
|
||||
pa_ready_ = 2;
|
||||
break;
|
||||
case PA_CONTEXT_READY:
|
||||
pa_ready_ = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void PulsePlayer::writeCallback(pa_stream* p, size_t nbytes)
|
||||
{
|
||||
pa_usec_t usec;
|
||||
int neg;
|
||||
pa_stream_get_latency(p, &usec, &neg);
|
||||
|
||||
auto rest = nbytes % stream_->getFormat().frameSize();
|
||||
if (rest != 0)
|
||||
LOG(INFO, LOG_TAG) << "Rest: " << rest << "\n";
|
||||
|
||||
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<char*>(buffer_.data()), numFrames);
|
||||
}
|
||||
|
||||
pa_stream_write(p, buffer_.data(), nbytes, nullptr, 0LL, PA_SEEK_RELATIVE);
|
||||
}
|
||||
|
||||
|
||||
void PulsePlayer::start()
|
||||
{
|
||||
// Create a mainloop API and connection to the default server
|
||||
pa_ml_ = pa_mainloop_new();
|
||||
pa_mainloop_api* pa_mlapi = pa_mainloop_get_api(pa_ml_);
|
||||
pa_ctx_ = pa_context_new(pa_mlapi, "Snapcast");
|
||||
pa_context_connect(pa_ctx_, nullptr, PA_CONTEXT_NOFLAGS, nullptr);
|
||||
|
||||
// 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<PulsePlayer*>(userdata);
|
||||
self->stateCallback(c);
|
||||
},
|
||||
this);
|
||||
|
||||
// We can't do anything until PA is ready, so just iterate the mainloop
|
||||
// and continue
|
||||
while (pa_ready_ == 0)
|
||||
pa_mainloop_iterate(pa_ml_, 1, nullptr);
|
||||
|
||||
if (pa_ready_ == 2)
|
||||
throw SnapException("PulseAudio is not ready");
|
||||
|
||||
const SampleFormat& format = stream_->getFormat();
|
||||
ss_.rate = format.rate();
|
||||
ss_.channels = format.channels();
|
||||
if (format.bits() == 8)
|
||||
ss_.format = PA_SAMPLE_U8;
|
||||
else if (format.bits() == 16)
|
||||
ss_.format = PA_SAMPLE_S16LE;
|
||||
else if ((format.bits() == 24) && (format.sampleSize() == 3))
|
||||
ss_.format = PA_SAMPLE_S24LE;
|
||||
else if ((format.bits() == 24) && (format.sampleSize() == 4))
|
||||
ss_.format = PA_SAMPLE_S24_32LE;
|
||||
else if (format.bits() == 32)
|
||||
ss_.format = PA_SAMPLE_S32LE;
|
||||
else
|
||||
throw SnapException("Unsupported sample format: " + cpt::to_string(format.bits()));
|
||||
|
||||
playstream_ = pa_stream_new(pa_ctx_, "Playback", &ss_, nullptr);
|
||||
if (!playstream_)
|
||||
throw SnapException("Failed to create PulseAudio stream");
|
||||
|
||||
pa_stream_set_write_callback(
|
||||
playstream_,
|
||||
[](pa_stream* s, size_t length, void* userdata) {
|
||||
auto self = static_cast<PulsePlayer*>(userdata);
|
||||
self->writeCallback(s, length);
|
||||
},
|
||||
this);
|
||||
|
||||
pa_stream_set_underflow_callback(
|
||||
playstream_,
|
||||
[](pa_stream* s, void* userdata) {
|
||||
auto self = static_cast<PulsePlayer*>(userdata);
|
||||
self->underflowCallback(s);
|
||||
},
|
||||
this);
|
||||
|
||||
bufattr_.fragsize = (uint32_t)-1;
|
||||
bufattr_.maxlength = pa_usec_to_bytes(latency_, &ss_);
|
||||
bufattr_.minreq = pa_usec_to_bytes(0, &ss_);
|
||||
bufattr_.prebuf = (uint32_t)-1;
|
||||
bufattr_.tlength = pa_usec_to_bytes(latency_, &ss_);
|
||||
|
||||
int result = pa_stream_connect_playback(
|
||||
playstream_, nullptr, &bufattr_, static_cast<pa_stream_flags>(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_, nullptr, &bufattr_,
|
||||
static_cast<pa_stream_flags>(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;
|
||||
}
|
||||
}
|
61
client/player/pulse_player.hpp
Normal file
61
client/player/pulse_player.hpp
Normal file
|
@ -0,0 +1,61 @@
|
|||
/***
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
***/
|
||||
|
||||
#ifndef PULSE_PLAYER_HPP
|
||||
#define PULSE_PLAYER_HPP
|
||||
|
||||
#include "player.hpp"
|
||||
#include <cstdio>
|
||||
#include <memory>
|
||||
#include <pulse/pulseaudio.h>
|
||||
|
||||
/// 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> stream);
|
||||
virtual ~PulsePlayer();
|
||||
|
||||
void start() override;
|
||||
void stop() override;
|
||||
|
||||
protected:
|
||||
bool needsThread() const override;
|
||||
void worker() override;
|
||||
|
||||
void underflowCallback(pa_stream* s);
|
||||
void stateCallback(pa_context* c);
|
||||
void writeCallback(pa_stream* p, size_t nbytes);
|
||||
|
||||
std::vector<char> buffer_;
|
||||
|
||||
int latency_; //< start latency in micro seconds
|
||||
pa_buffer_attr bufattr_;
|
||||
int underflows_ = 0;
|
||||
pa_sample_spec ss_;
|
||||
|
||||
pa_mainloop* pa_ml_;
|
||||
pa_context* pa_ctx_;
|
||||
pa_stream* playstream_;
|
||||
int pa_ready_ = 0;
|
||||
};
|
||||
|
||||
|
||||
#endif
|
Loading…
Add table
Add a link
Reference in a new issue