Add PipeWire capture support

This commit is contained in:
Chris Robinson 2021-08-21 13:27:14 -07:00
parent b1031459c9
commit 365ff8302e
2 changed files with 282 additions and 12 deletions

View File

@ -46,6 +46,7 @@
#include "core/logging.h"
#include "dynload.h"
#include "opthelpers.h"
#include "ringbuffer.h"
/* Ignore warnings caused by PipeWire headers (lots in standard C++ mode). */
_Pragma("GCC diagnostic push")
@ -97,6 +98,7 @@ using std::chrono::nanoseconds;
using uint = unsigned int;
constexpr char pwireDevice[] = "PipeWire Output";
constexpr char pwireInput[] = "PipeWire Input";
#ifdef HAVE_DYNLOAD
@ -937,13 +939,13 @@ spa_audio_info_raw make_spa_info(DeviceBase *device, use_f32p_e use_f32p)
}
else switch(device->FmtType)
{
case DevFmtByte: info.format = SPA_AUDIO_FORMAT_S8;
case DevFmtUByte: info.format = SPA_AUDIO_FORMAT_U8;
case DevFmtShort: info.format = SPA_AUDIO_FORMAT_S16;
case DevFmtUShort: info.format = SPA_AUDIO_FORMAT_U16;
case DevFmtInt: info.format = SPA_AUDIO_FORMAT_S32;
case DevFmtUInt: info.format = SPA_AUDIO_FORMAT_U32;
case DevFmtFloat: info.format = SPA_AUDIO_FORMAT_F32;
case DevFmtByte: info.format = SPA_AUDIO_FORMAT_S8; break;
case DevFmtUByte: info.format = SPA_AUDIO_FORMAT_U8; break;
case DevFmtShort: info.format = SPA_AUDIO_FORMAT_S16; break;
case DevFmtUShort: info.format = SPA_AUDIO_FORMAT_U16; break;
case DevFmtInt: info.format = SPA_AUDIO_FORMAT_S32; break;
case DevFmtUInt: info.format = SPA_AUDIO_FORMAT_U32; break;
case DevFmtFloat: info.format = SPA_AUDIO_FORMAT_F32; break;
}
info.rate = device->Frequency;
@ -971,10 +973,7 @@ spa_audio_info_raw make_spa_info(DeviceBase *device, use_f32p_e use_f32p)
return info;
}
struct PipeWirePlayback final : public BackendBase {
PipeWirePlayback(DeviceBase *device) noexcept : BackendBase{device} { }
~PipeWirePlayback();
class PipeWirePlayback final : public BackendBase {
void stateChangedCallback(pw_stream_state old, pw_stream_state state, const char *error);
static void stateChangedCallbackC(void *data, pw_stream_state old, pw_stream_state state,
const char *error)
@ -1013,6 +1012,10 @@ struct PipeWirePlayback final : public BackendBase {
return ret;
}
public:
PipeWirePlayback(DeviceBase *device) noexcept : BackendBase{device} { }
~PipeWirePlayback();
DEF_NEWDEL(PipeWirePlayback)
};
const pw_stream_events PipeWirePlayback::sEvents{PipeWirePlayback::InitEvent()};
@ -1383,6 +1386,250 @@ ClockLatency PipeWirePlayback::getClockLatency()
return ret;
}
class PipeWireCapture final : public BackendBase {
void stateChangedCallback(pw_stream_state old, pw_stream_state state, const char *error);
static void stateChangedCallbackC(void *data, pw_stream_state old, pw_stream_state state,
const char *error)
{ static_cast<PipeWireCapture*>(data)->stateChangedCallback(old, state, error); }
void inputCallback();
static void inputCallbackC(void *data)
{ static_cast<PipeWireCapture*>(data)->inputCallback(); }
void open(const char *name) override;
void start() override;
void stop() override;
void captureSamples(al::byte *buffer, uint samples) override;
uint availableSamples() override;
uint32_t mTargetId{PwIdAny};
ThreadMainloop mLoop;
PwStreamPtr mStream;
RingBufferPtr mRing{};
static const pw_stream_events sEvents;
static constexpr pw_stream_events InitEvent()
{
pw_stream_events ret{};
ret.version = PW_VERSION_STREAM_EVENTS;
ret.state_changed = &PipeWireCapture::stateChangedCallbackC;
ret.process = &PipeWireCapture::inputCallbackC;
return ret;
}
public:
PipeWireCapture(DeviceBase *device) noexcept : BackendBase{device} { }
~PipeWireCapture();
DEF_NEWDEL(PipeWireCapture)
};
const pw_stream_events PipeWireCapture::sEvents{PipeWireCapture::InitEvent()};
PipeWireCapture::~PipeWireCapture()
{
if(mLoop && mStream)
{
MainloopLockGuard _{mLoop};
mStream = nullptr;
}
}
void PipeWireCapture::stateChangedCallback(pw_stream_state, pw_stream_state, const char*)
{ mLoop.signal(false); }
void PipeWireCapture::inputCallback()
{
pw_buffer *pw_buf{pw_stream_dequeue_buffer(mStream.get())};
if UNLIKELY(!pw_buf) return;
spa_data *bufdata{pw_buf->buffer->datas};
const uint offset{minu(bufdata->chunk->offset, bufdata->maxsize)};
const uint size{minu(bufdata->chunk->size, bufdata->maxsize - offset)};
mRing->write(static_cast<char*>(bufdata->data) + offset, size / mRing->getElemSize());
pw_stream_queue_buffer(mStream.get(), pw_buf);
}
void PipeWireCapture::open(const char *name)
{
static std::atomic<uint> OpenCount{0};
uint32_t targetid{PwIdAny};
std::string devname{};
if(!name)
{
EventWatcherLockGuard _{gEventHandler};
gEventHandler.waitForInit();
auto match = DeviceList.cend();
if(!DefaultSourceDev.empty())
{
auto match_default = [](const DeviceNode &n) -> bool
{ return n.mDevName == DefaultSourceDev; };
match = std::find_if(DeviceList.cbegin(), DeviceList.cend(), match_default);
}
if(match == DeviceList.cend())
{
auto match_capture = [](const DeviceNode &n) -> bool
{ return n.mCapture; };
match = std::find_if(DeviceList.cbegin(), DeviceList.cend(), match_capture);
}
if(match == DeviceList.cend())
{
auto match_playback = [](const DeviceNode &n) -> bool
{ return !n.mCapture; };
match = std::find_if(DeviceList.cbegin(), DeviceList.cend(), match_playback);
if(match == DeviceList.cend())
throw al::backend_exception{al::backend_error::NoDevice,
"No PipeWire capture device found"};
}
targetid = match->mId;
if(match->mCapture) devname = match->mName;
else devname = "Monitor of "+match->mName;
}
else
{
EventWatcherLockGuard _{gEventHandler};
gEventHandler.waitForInit();
auto match_name = [name](const DeviceNode &n) -> bool
{ return n.mCapture && n.mName == name; };
auto match = std::find_if(DeviceList.cbegin(), DeviceList.cend(), match_name);
if(match == DeviceList.cend() && std::strcmp(name, "Monitor of ") == 0)
{
const char *sinkname{name + 11};
auto match_sinkname = [sinkname](const DeviceNode &n) -> bool
{ return !n.mCapture && n.mName == sinkname; };
match = std::find_if(DeviceList.cbegin(), DeviceList.cend(), match_sinkname);
}
if(match == DeviceList.cend())
throw al::backend_exception{al::backend_error::NoDevice,
"Device name \"%s\" not found", name};
targetid = match->mId;
devname = name;
}
if(!mLoop)
{
const uint count{OpenCount.fetch_add(1, std::memory_order_relaxed)};
const std::string thread_name{"ALSoftC" + std::to_string(count)};
mLoop = ThreadMainloop{pw_thread_loop_new(thread_name.c_str(), nullptr)};
if(!mLoop)
throw al::backend_exception{al::backend_error::DeviceError,
"Failed to create PipeWire mainloop (errno: %d)", errno};
if(int res{mLoop.start()})
throw al::backend_exception{al::backend_error::DeviceError,
"Failed to start PipeWire mainloop (res: %d)", res};
}
/* TODO: Ensure the target ID is still valid/usable and accepts streams. */
mTargetId = targetid;
if(!devname.empty())
mDevice->DeviceName = std::move(devname);
else
mDevice->DeviceName = pwireInput;
spa_audio_info_raw info{make_spa_info(mDevice, UseDevType)};
constexpr uint32_t pod_buffer_size{1024};
auto pod_buffer = std::make_unique<al::byte[]>(pod_buffer_size);
spa_pod_builder b{make_pod_builder(pod_buffer.get(), pod_buffer_size)};
const spa_pod *params[]{spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, &info)};
if(!params[0])
throw al::backend_exception{al::backend_error::DeviceError,
"Failed to set PipeWire audio format parameters"};
pw_properties *props{pw_properties_new(
PW_KEY_MEDIA_TYPE, "Audio",
PW_KEY_MEDIA_CATEGORY, "Capture",
PW_KEY_MEDIA_ROLE, "Game",
PW_KEY_NODE_ALWAYS_PROCESS, "true",
nullptr)};
if(!props)
throw al::backend_exception{al::backend_error::DeviceError,
"Failed to create PipeWire stream properties (errno: %d)", errno};
auto&& binary = GetProcBinary();
const char *appname{binary.fname.length() ? binary.fname.c_str() : "OpenAL Soft"};
pw_properties_set(props, PW_KEY_NODE_NAME, appname);
pw_properties_set(props, PW_KEY_NODE_DESCRIPTION, appname);
/* We don't actually care what the latency/update size is, as long as it's
* reasonable. Unfortunately, when unspecified PipeWire seems to default to
* around 40ms, which isn't great. So request 20ms instead.
*/
pw_properties_setf(props, PW_KEY_NODE_LATENCY, "%u/%u", (mDevice->Frequency+25) / 50,
mDevice->Frequency);
MainloopUniqueLock plock{mLoop};
mStream = PwStreamPtr{pw_stream_new_simple(mLoop.getLoop(), "Capture Stream", props,
&sEvents, this)};
if(!mStream)
throw al::backend_exception{al::backend_error::NoDevice,
"Failed to create PipeWire stream (errno: %d)", errno};
constexpr pw_stream_flags Flags{PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_INACTIVE
| PW_STREAM_FLAG_MAP_BUFFERS | PW_STREAM_FLAG_RT_PROCESS};
if(int res{pw_stream_connect(mStream.get(), PW_DIRECTION_INPUT, mTargetId, Flags, params, 1)})
throw al::backend_exception{al::backend_error::DeviceError,
"Error connecting PipeWire stream (res: %d)", res};
/* Wait for the stream to become paused (ready to start streaming). */
pw_stream_state state{};
const char *error{};
while((state=pw_stream_get_state(mStream.get(), &error)) != PW_STREAM_STATE_PAUSED)
{
if(state == PW_STREAM_STATE_ERROR)
throw al::backend_exception{al::backend_error::DeviceError,
"Error connecting PipeWire stream: \"%s\"", error};
mLoop.wait();
}
plock.unlock();
setDefaultWFXChannelOrder();
/* Ensure at least a 100ms capture buffer. */
mRing = RingBuffer::Create(maxu(mDevice->Frequency/10, mDevice->BufferSize),
mDevice->frameSizeFromFmt(), false);
}
void PipeWireCapture::start()
{
MainloopLockGuard _{mLoop};
if(int res{pw_stream_set_active(mStream.get(), true)})
throw al::backend_exception{al::backend_error::DeviceError,
"Failed to start PipeWire stream (res: %d)", res};
}
void PipeWireCapture::stop()
{
MainloopLockGuard _{mLoop};
if(int res{pw_stream_set_active(mStream.get(), false)})
throw al::backend_exception{al::backend_error::DeviceError,
"Failed to stop PipeWire stream (res: %d)", res};
/* Wait for the stream to stop playing. */
pw_stream_state state{};
while((state=pw_stream_get_state(mStream.get(), nullptr)) == PW_STREAM_STATE_STREAMING)
mLoop.wait();
}
uint PipeWireCapture::availableSamples()
{ return static_cast<uint>(mRing->readSpace()); }
void PipeWireCapture::captureSamples(al::byte *buffer, uint samples)
{ mRing->read(buffer, samples); }
} // namespace
@ -1399,7 +1646,7 @@ bool PipeWireBackendFactory::init()
}
bool PipeWireBackendFactory::querySupport(BackendType type)
{ return (type == BackendType::Playback); }
{ return type == BackendType::Playback || type == BackendType::Capture; }
std::string PipeWireBackendFactory::probe(BackendType type)
{
@ -1410,6 +1657,8 @@ std::string PipeWireBackendFactory::probe(BackendType type)
auto match_defsink = [](const DeviceNode &n) -> bool
{ return n.mDevName == DefaultSinkDev; };
auto match_defsource = [](const DeviceNode &n) -> bool
{ return n.mDevName == DefaultSourceDev; };
auto sort_devnode = [](DeviceNode &lhs, DeviceNode &rhs) noexcept -> bool
{ return lhs.mId < rhs.mId; };
@ -1432,6 +1681,23 @@ std::string PipeWireBackendFactory::probe(BackendType type)
}
break;
case BackendType::Capture:
defmatch = std::find_if(defmatch, DeviceList.cend(), match_defsource);
if(defmatch != DeviceList.cend())
{
if(!defmatch->mCapture)
outnames.append("Monitor of ");
outnames.append(defmatch->mName.c_str(), defmatch->mName.length()+1);
}
for(auto iter = DeviceList.cbegin();iter != DeviceList.cend();++iter)
{
if(iter != defmatch && iter->mCapture)
outnames.append(iter->mName.c_str(), iter->mName.length()+1);
}
for(auto iter = DeviceList.cbegin();iter != DeviceList.cend();++iter)
{
if(iter != defmatch && !iter->mCapture)
outnames.append("Monitor of ").append(iter->mName.c_str(), iter->mName.length()+1);
}
break;
}
@ -1442,6 +1708,8 @@ BackendPtr PipeWireBackendFactory::createBackend(DeviceBase *device, BackendType
{
if(type == BackendType::Playback)
return BackendPtr{new PipeWirePlayback{device}};
if(type == BackendType::Capture)
return BackendPtr{new PipeWireCapture{device}};
return nullptr;
}

View File

@ -98,6 +98,8 @@ public:
void writeAdvance(size_t cnt) noexcept
{ mWritePtr.fetch_add(cnt, std::memory_order_acq_rel); }
size_t getElemSize() const noexcept { return mElemSize; }
/**
* Create a new ringbuffer to hold at least `sz' elements of `elem_sz'
* bytes. The number of elements is rounded up to the next power of two