Merge pull request #421 from clangen/clangen/pipewire

PipeWire output support
This commit is contained in:
casey langen 2021-03-19 23:30:56 -07:00 committed by GitHub
commit f416a1dbd7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 949 additions and 34 deletions

View File

@ -42,6 +42,20 @@ jobs:
- run: mkdir -p /root/debs && mv /root/project/*.deb /root/debs/
- store_artifacts:
path: /root/debs/
build_ubuntu_hirsute:
docker:
- image: ubuntu:hirsute
steps:
- run: apt-get update
- run: DEBIAN_FRONTEND="noninteractive" TZ="America/Los_Angeles" apt-get install -y git ssh sshpass build-essential clang cmake libboost-thread1.71-dev libboost-system1.71-dev libboost-filesystem1.71-dev libboost-date-time1.71-dev libboost-atomic1.71-dev libboost-chrono1.71-dev libogg-dev libvorbis-dev libavutil-dev libavformat-dev libswresample-dev libncursesw5-dev libasound2-dev libpulse-dev pulseaudio libmicrohttpd-dev libmp3lame-dev libcurl4-openssl-dev libev-dev libssl-dev libtag1-dev libsystemd-dev libpipewire-0.3-dev libspa-0.2-dev
- checkout
- run: cmake -DENABLE_PCH=true -DENABLE_PIPEWIRE=true -DGENERATE_DEB=1 -DDEB_ARCHITECTURE=amd64 -DDEB_PLATFORM=ubuntu -DDEB_DISTRO=groovy -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Release .
- run: make -j2
- run: make package
- run: ./script/copy-artifacts-to-host.sh /root/project/*.deb
- run: mkdir -p /root/debs && mv /root/project/*.deb /root/debs/
- store_artifacts:
path: /root/debs/
build_fedora_31:
docker:
- image: fedora:31

View File

@ -120,7 +120,7 @@ else()
add_subdirectory(src/plugins/libopenmptdecoder)
endif()
if (${FFMPEG_ENABLED} MATCHES "false")
if (${ENABLE_FFMPEG} MATCHES "false")
message(STATUS "[ffmpeg] enabled = false")
add_subdirectory(src/plugins/m4adecoder)
add_subdirectory(src/plugins/oggdecoder)
@ -155,6 +155,10 @@ add_dependencies(musikcubed musikcube)
if (CMAKE_SYSTEM_NAME MATCHES "Linux")
add_subdirectory(src/plugins/alsaout)
add_subdirectory(src/plugins/pulseout)
if (${ENABLE_PIPEWIRE} MATCHES "true")
add_subdirectory(src/plugins/pipewireout)
add_dependencies(musikcube pipewireout)
endif()
if (${ENABLE_MPRIS} MATCHES "true")
add_subdirectory(src/plugins/mpris)
add_dependencies(musikcube mpris)
@ -295,7 +299,7 @@ if (GENERATE_DEB MATCHES "1")
set(DEPENDENCIES "libboost-thread${DEB_BOOST_VERSION}, libboost-system${DEB_BOOST_VERSION}, libboost-chrono${DEB_BOOST_VERSION}, libboost-filesystem${DEB_BOOST_VERSION}, libboost-date-time${DEB_BOOST_VERSION}, libmicrohttpd${DEB_MICROHTTPD_VERSION}, libcurl${DEB_LIBCURL_VERSION}, libogg0, libvorbis0a, libvorbisfile3, libncursesw${DEB_NCURSES_VERSION}, libasound2, libpulse0, pulseaudio, libmp3lame0, libev4, libopenmpt0, libssl1.1")
if (${FFMPEG_ENABLED} MATCHES "false")
if (${ENABLE_FFMPEG} MATCHES "false")
set(DEPENDENCIES "${DEPENDENCIES}, libflac8, libfaad2")
else()
set(DEPENDENCIES "${DEPENDENCIES}, libavcodec-extra, libavutil${DEB_AVUTIL_VERSION}, libavformat${DEB_AVFORMAT_VERSION}, libswresample${DEB_SWRESAMPLE_VERSION}")

View File

@ -12,7 +12,7 @@ SCRIPTDIR=`dirname "$0"`
rm -rf bin/
${SCRIPTDIR}/clean-nix.sh
cmake -DCMAKE_BUILD_TYPE=Release -DLINK_STATICALLY=true -DFFMPEG_ENABLED=false .
cmake -DCMAKE_BUILD_TYPE=Release -DLINK_STATICALLY=true -DENABLE_FFMPEG=false .
make -j7
DIRNAME="musikcube_macos_$VERSION"

View File

@ -73,10 +73,10 @@ Crossfader::Crossfader(ITransport& transport)
}
Crossfader::~Crossfader() {
this->messageQueue.Unregister(this);
this->quit = true;
this->messageQueue.Post(Message::Create(this, MESSAGE_QUIT, 0, 0));
this->thread->join();
this->messageQueue.Unregister(this);
}
void Crossfader::Fade(

View File

@ -454,8 +454,8 @@ void PlaybackService::ProcessMessage(IMessage &message) {
if (masterTransport) {
const TransportType transportType = static_cast<TransportType>(
playbackPrefs->GetInt(
keys::Transport.c_str()),
static_cast<int>(TransportType::Gapless));
keys::Transport,
static_cast<int>(TransportType::Gapless)));
if (masterTransport->GetType() != transportType) {
masterTransport->SwitchTo(transportType);

View File

@ -123,6 +123,14 @@ static class Debug: public IDebug {
}
} debugger;
static class NullDebug: public IDebug { /* used during shutdown */
public:
void Verbose(const char* tag, const char* message) override {}
void Info(const char* tag, const char* message) override {}
void Warning(const char* tag, const char* message) override {}
void Error(const char* tag, const char* message) override {}
} nullDebugger;
static class Environment: public IEnvironment {
public:
virtual size_t GetPath(PathType type, char* dst, int size) override {
@ -439,7 +447,7 @@ namespace musik { namespace core { namespace plugin {
PluginFactory::Instance().QueryFunction<SetDebug>(
"SetDebug",
[](musik::core::sdk::IPlugin* plugin, SetDebug func) {
func(nullptr);
func(&nullDebugger);
});
}

View File

@ -379,7 +379,7 @@ struct Theme {
/* main */
init_pair(Color::ContentColorDefault, foregroundId, backgroundId);
init_pair(Color::FrameDefault, foregroundId, backgroundId);
init_pair(Color::FrameFocused, focusedFrame.Id(mode, COLOR_RED),backgroundId);
init_pair(Color::FrameFocused, focusedFrame.Id(mode, COLOR_RED), backgroundId);
init_pair(Color::FrameImportant, importantFrame.Id(mode, COLOR_BLUE), backgroundId);
/* text */

View File

@ -419,11 +419,10 @@ void Window::Redraw() {
}
if (this->frame) {
#ifdef __FreeBSD__
/* somehow related to curses version, but i don't know how;
if we don't do this on FreeBSD we get missing background
colors for things like overlays. if we do this on other
platforms we get weird repainting artifacts. */
#if defined(__FreeBSD__) || (NCURSES_VERSION_PATCH >= 20200301)
/* but depending on curses version we'll get redraw artifacts if we do or don't
repaint the background. the changelog mentions changes to wbkgd() and wbkgrnd()
at revision 20200301, but it's unclear if this is the actual root cause or not. */
this->RepaintBackground();
#endif
this->OnRedraw();

View File

@ -5,6 +5,7 @@
#include <musikcore/sdk/IPlugin.h>
#include <mutex>
#include <thread>
#include <memory>
extern "C" {
#include <systemd/sd-bus.h>

View File

@ -0,0 +1,14 @@
set (pipewireout_SOURCES
pipewireout_plugin.cpp
PipeWireOut.cpp
)
message(STATUS "[pipewireout] plugin enabled")
include_directories("/usr/include/spa-0.2")
include_directories("/usr/local/include/spa-0.2")
include_directories("/usr/include/pipewire-0.3")
include_directories("/usr/local/include/pipewire-0.3")
add_library(pipewireout SHARED ${pipewireout_SOURCES})
target_link_libraries(pipewireout ${musikcube_LINK_LIBS} pipewire-0.3)

View File

@ -0,0 +1,515 @@
//////////////////////////////////////////////////////////////////////////////
//
// Copyright (c) 2004-2020 musikcube team
//
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
//
// * Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// * Neither the name of the author nor the names of other contributors may
// be used to endorse or promote products derived from this software
// without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
//
//////////////////////////////////////////////////////////////////////////////
#include "PipeWireOut.h"
#include <musikcore/sdk/constants.h>
#include <musikcore/sdk/IPreferences.h>
#include <musikcore/sdk/ISchema.h>
#include <musikcore/sdk/IDebug.h>
#include <musikcore/sdk/String.h>
#include <spa/param/audio/format-utils.h>
#include <spa/param/props.h>
#include <spa/utils/result.h>
#include <unistd.h>
#include <chrono>
constexpr const char* TAG = "PipeWireOut";
constexpr size_t SAMPLES_PER_BUFFER = 2048;
constexpr size_t SAMPLE_SIZE_BYTES = sizeof(float);
constexpr size_t MAX_BUFFERS = 16;
constexpr const char* PREF_DEVICE_ID = "device_id";
static std::atomic<bool> pipeWireInitialized(false);
static IPreferences* prefs = nullptr;
static IDebug* debug = nullptr;
extern "C" void SetDebug(IDebug* debug) {
::debug = debug;
}
extern "C" void SetPreferences(IPreferences* prefs) {
::prefs = prefs;
prefs->GetString(PREF_DEVICE_ID, nullptr, 0, "");
prefs->Save();
}
extern "C" musik::core::sdk::ISchema* GetSchema() {
auto schema = new TSchema<>();
return schema;
}
static std::string getDeviceId() {
return getPreferenceString<std::string>(prefs, PREF_DEVICE_ID, "");
}
void PipeWireOut::OnStreamStateChanged(void* data, enum pw_stream_state old, enum pw_stream_state state, const char* error) {
::debug->Info(TAG, str::format("state changed from %d to %d", old, state).c_str());
}
void PipeWireOut::OnCoreDone(void* userdata, uint32_t id, int seq) {
auto context = static_cast<DeviceListContext*>(userdata);
if (seq == context->eventId) {
::debug->Info(TAG, "device refresh finished");
pw_main_loop_quit(context->loop);
}
}
void PipeWireOut::OnCoreError(void* userdata, uint32_t id, int seq, int res, const char *message) {
auto context = static_cast<DeviceListContext*>(userdata);
pw_main_loop_quit(context->loop);
}
void PipeWireOut::OnRegistryGlobal(void *userdata, uint32_t id, uint32_t permissions, const char *type, uint32_t version, const struct spa_dict *props) {
auto context = static_cast<DeviceListContext*>(userdata);
if (std::string(type) == "PipeWire:Interface:Device") {
std::string formattedName = "unknown device";
if (props) {
auto dict = pw_properties_new_dict(props);
auto name = pw_properties_get(dict, "device.nick");
if (!name || !strlen(name)) name = pw_properties_get(dict, "alsa.card_name");
if (!name || !strlen(name)) name = pw_properties_get(dict, "api.alsa.card.name");
if (!name || !strlen(name)) name = "unknown device";
auto addr = pw_properties_get(dict, "object.path");
if (!addr || !strlen(addr)) addr = pw_properties_get(dict, "device.bus-path");
formattedName = name;
if (addr && strlen(addr)) {
formattedName += " (" + std::string(addr) + ")";
}
pw_properties_free(dict);
}
::debug->Info(TAG, str::format("detected PipeWire:Interface:Device with id=%d", id).c_str());
context->instance->deviceList.Add(std::to_string(id), formattedName);
}
}
void PipeWireOut::OnDrained(void* data) {
::debug->Info(TAG, "drained");
PipeWireOut* self = static_cast<PipeWireOut*>(data);
self->drainCondition.notify_all();
}
void PipeWireOut::OnStreamProcess(void* data) {
PipeWireOut* self = static_cast<PipeWireOut*>(data);
{
std::unique_lock<std::recursive_mutex> lock(self->mutex);
if (self->state != State::Playing) {
return;
}
OutBufferContext& outContext = self->outBufferContext;
if (!outContext.Valid()) {
outContext.Initialize(pw_stream_dequeue_buffer(self->pwStream));
if (!outContext.Valid()) {
// std::cerr << "[PipeWire] no more output buffers available to fill\n";
return;
}
}
uint32_t channelCount;
while (outContext.remaining > 0 && !self->buffers.empty()) {
//std::cerr << "[PipeWire] " << outContext.remaining << " bytes still need to be filled...\n";
InBufferContext* inContext = self->buffers.front();
channelCount = (uint32_t) inContext->buffer->Channels();
uint32_t inBufferSize = (uint32_t) inContext->buffer->Bytes();
uint32_t bytesToCopy = std::min(outContext.remaining, inContext->remaining);
if (outContext.remaining >= inContext->remaining) {
self->buffers.pop_front();
self->bufferCondition.notify_all();
}
memcpy(outContext.writePtr, inContext->readPtr, bytesToCopy);
inContext->Advance(bytesToCopy); /* will `delete this` if emptied */
outContext.Advance(bytesToCopy);
}
if (outContext.remaining == 0) {
outContext.Finalize(self->pwStream, SAMPLE_SIZE_BYTES * channelCount);
}
}
}
PipeWireOut::PipeWireOut() {
this->pwStreamEvents = { PW_VERSION_STREAM_EVENTS };
this->pwStreamEvents.state_changed = PipeWireOut::OnStreamStateChanged;
this->pwStreamEvents.process = PipeWireOut::OnStreamProcess;
this->pwStreamEvents.drained = PipeWireOut::OnDrained;
}
PipeWireOut::~PipeWireOut() {
this->StopPipeWire();
}
void PipeWireOut::Release() {
delete this;
}
void PipeWireOut::Pause() {
{
std::unique_lock<std::recursive_mutex> lock(this->mutex);
this->state = State::Paused;
}
{
if (this->pwThreadLoop && this->pwStream) {
pw_thread_loop_lock(this->pwThreadLoop);
pw_stream_set_active(this->pwStream, false);
pw_thread_loop_unlock(this->pwThreadLoop);
}
}
}
void PipeWireOut::Resume() {
{
std::unique_lock<std::recursive_mutex> lock(this->mutex);
this->state = State::Playing;
}
{
if (this->pwThreadLoop && this->pwStream) {
pw_thread_loop_lock(this->pwThreadLoop);
pw_stream_set_active(this->pwStream, true);
pw_thread_loop_unlock(this->pwThreadLoop);
}
}
}
void PipeWireOut::SetVolume(double volume) {
std::unique_lock<std::recursive_mutex> lock(this->mutex);
if (this->pwThreadLoop && this->pwStream) {
pw_thread_loop_lock(this->pwThreadLoop);
float* channelVolumes = new float[this->channelCount];
for (long i = 0; i < this->channelCount; i++) {
channelVolumes[i] = volume;
}
pw_stream_set_control(
this->pwStream,
SPA_PROP_channelVolumes,
this->channelCount,
channelVolumes,
0);
delete[] channelVolumes;
pw_thread_loop_unlock(this->pwThreadLoop);
}
this->volume = volume;
}
double PipeWireOut::GetVolume() {
std::unique_lock<std::recursive_mutex> lock(this->mutex);
return this->volume;
}
void PipeWireOut::Stop() {
std::unique_lock<std::recursive_mutex> lock(this->mutex);
this->DiscardInputBuffers();
this->state = State::Stopped;
if (this->pwThreadLoop && this->pwStream) {
pw_thread_loop_lock(this->pwThreadLoop);
pw_stream_set_active(this->pwStream, false);
pw_stream_flush(this->pwStream, false);
pw_thread_loop_unlock(this->pwThreadLoop);
}
}
void PipeWireOut::DiscardInputBuffers() {
std::unique_lock<std::recursive_mutex> lock(this->mutex);
for (auto& buffer : this->buffers) {
buffer->Discard();
}
this->buffers.clear();
this->bufferCondition.notify_all();
}
void PipeWireOut::Drain() {
std::unique_lock<std::recursive_mutex> lock(this->mutex);
while (this->buffers.size()) {
bufferCondition.wait(lock);
}
if (this->pwThreadLoop && this->pwStream) {
pw_thread_loop_lock(this->pwThreadLoop);
pw_stream_flush(this->pwStream, true);
pw_thread_loop_unlock(this->pwThreadLoop);
drainCondition.wait_for(lock, std::chrono::milliseconds(10000));
}
}
IDeviceList* PipeWireOut::GetDeviceList() {
std::unique_lock<std::recursive_mutex> lock(this->mutex);
this->RefreshDeviceList();
return this->deviceList.Clone();
}
bool PipeWireOut::SetDefaultDevice(const char* deviceId) {
if (getDeviceId() != deviceId) {
setDefaultDevice<IPreferences, Device, IOutput>(prefs, this, PREF_DEVICE_ID, deviceId);
}
return true;
}
IDevice* PipeWireOut::GetDefaultDevice() {
std::unique_lock<std::recursive_mutex> lock(this->mutex);
this->RefreshDeviceList();
return this->deviceList.ResolveDevice(getDeviceId());
}
void PipeWireOut::StopPipeWire() {
::debug->Info(TAG, "shutdown started");
this->Stop();
{
std::unique_lock<std::recursive_mutex> lock(this->mutex);
this->state = State::Shutdown;
}
if (this->pwThreadLoop) {
pw_thread_loop_stop(this->pwThreadLoop);
if (this->pwStream) {
this->outBufferContext.Finalize(
this->pwStream,
SAMPLE_SIZE_BYTES * this->channelCount);
pw_stream_destroy(this->pwStream);
this->pwStream = nullptr;
}
pw_thread_loop_destroy(this->pwThreadLoop);
this->pwThreadLoop = nullptr;
}
this->initialized = false;
this->channelCount = 0;
this->sampleRate = 0;
::debug->Info(TAG, "shutdown complete");
}
bool PipeWireOut::StartPipeWire(IBuffer* buffer) {
std::unique_lock<std::recursive_mutex> lock(this->mutex);
this->pwThreadLoop = pw_thread_loop_new("musikcube", nullptr);
if (this->pwThreadLoop) {
int result;
if ((result = pw_thread_loop_start(this->pwThreadLoop)) != 0) {
::debug->Error(TAG, str::format("error starting thread loop: %s", spa_strerror(result)).c_str());
goto cleanup;
};
pw_thread_loop_lock(this->pwThreadLoop);
this->pwStream = pw_stream_new_simple(
pw_thread_loop_get_loop(this->pwThreadLoop),
"musikcube",
pw_properties_new(
PW_KEY_MEDIA_TYPE, "Audio",
PW_KEY_MEDIA_CATEGORY, "Playback",
PW_KEY_MEDIA_ROLE, "Music",
NULL),
&this->pwStreamEvents,
this);
if (this->pwStream) {
uint8_t builderBuffer[4096];
spa_pod_builder builder = SPA_POD_BUILDER_INIT(builderBuffer, sizeof(builderBuffer));
const spa_pod *params[1];
this->channelCount = buffer->Channels();
this->sampleRate = buffer->SampleRate();
spa_audio_info_raw audioInfo;
spa_zero(audioInfo);
audioInfo.format = SPA_AUDIO_FORMAT_F32;
audioInfo.channels = (uint32_t) this->channelCount;
audioInfo.rate = (uint32_t) this->sampleRate;
params[0] = spa_format_audio_raw_build(&builder, SPA_PARAM_EnumFormat, &audioInfo);
if (!params[0]) {
::debug->Error(TAG, str::format(
"failed to create audio format. channels=%d, rate=%d",
this->channelCount,
this->sampleRate).c_str());
goto cleanup;
}
// params[1] = (spa_pod*) spa_pod_builder_add_object(
// &builder,
// SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers,
// SPA_PARAM_BUFFERS_buffers, SPA_POD_Int(MAX_BUFFERS),
// SPA_PARAM_BUFFERS_size, SPA_POD_Int(SAMPLES_PER_BUFFER * SAMPLE_SIZE_BYTES * buffer->Channels()),
// SPA_PARAM_BUFFERS_stride, SPA_POD_Int(SAMPLE_SIZE_BYTES * audioInfo.channels));
pw_stream_flags streamFlags = (pw_stream_flags)(
PW_STREAM_FLAG_AUTOCONNECT |
PW_STREAM_FLAG_MAP_BUFFERS);
result = pw_stream_connect(
this->pwStream,
PW_DIRECTION_OUTPUT,
this->deviceList.ResolveId(getDeviceId()),
streamFlags,
params,
1);
if (result == 0) {
pw_thread_loop_unlock(this->pwThreadLoop);
::debug->Info(TAG, "new stream created and connected successfully");
this->initialized = true;
return true;
}
else {
::debug->Error(TAG, str::format(
"error starting stream: %s",
spa_strerror(result)).c_str());
}
}
}
cleanup:
pw_thread_loop_unlock(this->pwThreadLoop);
::debug->Error(TAG, "stream not initialized");
this->StopPipeWire();
return false;
}
OutputState PipeWireOut::Play(IBuffer *buffer, IBufferProvider *provider) {
if (!this->initialized) {
std::unique_lock<std::recursive_mutex> lock(this->mutex);
if (!pipeWireInitialized) {
pw_init(nullptr, nullptr);
pipeWireInitialized = true;
}
this->RefreshDeviceList();
if (!this->StartPipeWire(buffer)) {
return OutputState::InvalidState;
}
this->SetVolume(this->volume);
}
if (this->channelCount != buffer->Channels() || this->sampleRate != buffer->SampleRate()) {
State lastState = this->state;
this->Drain();
this->StopPipeWire();
if (!this->StartPipeWire(buffer)) {
return OutputState::InvalidState;
}
this->state = lastState;
}
if (this->state != State::Playing) {
return OutputState::InvalidState;
}
{
std::unique_lock<std::recursive_mutex> lock(this->mutex);
if (this->buffers.size() >= MAX_BUFFERS) {
return OutputState::BufferFull;
}
this->buffers.push_back(new InBufferContext(buffer, provider));
bufferCondition.notify_all();
}
return OutputState::BufferWritten;
}
double PipeWireOut::Latency() {
/* CAL TODO: i see how to set latency, but not a good way to query
PipeWire to see what it actually is */
return 0.0;
}
void PipeWireOut::RefreshDeviceList() {
/* https://github.com/PipeWire/pipewire/blob/master/src/tools/pw-dump.c */
if (!pipeWireInitialized) {
pw_init(nullptr, nullptr);
pipeWireInitialized = true;
}
DeviceListContext deviceListContext(this);
deviceListContext.loop = pw_main_loop_new(nullptr);
if (!deviceListContext.loop) {
::debug->Error(TAG, "RefreshDeviceList() could not create main loop");
return;
}
auto loop = pw_main_loop_get_loop(deviceListContext.loop);
if (!loop) {
::debug->Error(TAG, "RefreshDeviceList() could not resolve loop from main_loop??");
return;
}
deviceListContext.context = pw_context_new(loop, nullptr, 0);
if (!deviceListContext.context) {
::debug->Error(TAG, "RefreshDeviceList() could not create context");
return;
}
deviceListContext.core = pw_context_connect(deviceListContext.context, nullptr, 0);
if (deviceListContext.core == nullptr) {
::debug->Error(TAG, "RefreshDeviceList() could not connect to core");
return;
}
pw_core_add_listener(
deviceListContext.core,
&deviceListContext.coreListener,
&deviceListContext.coreEvents,
&deviceListContext);
deviceListContext.registry = pw_core_get_registry(
deviceListContext.core, PW_VERSION_REGISTRY, 0);
if (deviceListContext.registry) {
pw_registry_add_listener(
deviceListContext.registry,
&deviceListContext.registryListener,
&deviceListContext.registryEvents,
&deviceListContext);
deviceListContext.eventId = pw_core_sync(
deviceListContext.core, PW_ID_CORE, 0);
this->deviceList.Reset();
pw_main_loop_run(deviceListContext.loop);
}
}

View File

@ -0,0 +1,297 @@
//////////////////////////////////////////////////////////////////////////////
//
// Copyright (c) 2004-2020 musikcube team
//
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
//
// * Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// * Neither the name of the author nor the names of other contributors may
// be used to endorse or promote products derived from this software
// without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
//
//////////////////////////////////////////////////////////////////////////////
#pragma once
#include <musikcore/sdk/IOutput.h>
#include <pipewire/pipewire.h>
#include <atomic>
#include <thread>
#include <mutex>
#include <deque>
#include <vector>
#include <condition_variable>
using namespace musik::core::sdk;
class PipeWireOut : public IOutput {
public:
PipeWireOut();
~PipeWireOut();
/* IPlugin */
const char* Name() override { return "PipeWire"; };
void Release() override;
/* IOutput */
void Pause() override;
void Resume() override;
void SetVolume(double volume) override;
double GetVolume() override;
void Stop() override;
OutputState Play(IBuffer *buffer, IBufferProvider *provider) override;
double Latency() override;
void Drain() override;
IDeviceList* GetDeviceList() override;
bool SetDefaultDevice(const char* deviceId) override;
IDevice* GetDefaultDevice() override;
private:
bool StartPipeWire(IBuffer* buffer);
void StopPipeWire();
void DiscardInputBuffers();
void RefreshDeviceList();
static void OnCoreDone(
void* userdata,
uint32_t id,
int seq);
static void OnCoreError(
void *userdata,
uint32_t id,
int seq,
int res,
const char *message
);
static void OnRegistryGlobal(
void *userdata,
uint32_t id,
uint32_t permissions,
const char *type,
uint32_t version,
const struct spa_dict *props);
static void OnStreamStateChanged(
void* userdata,
enum pw_stream_state old,
enum pw_stream_state state,
const char* error);
static void OnStreamProcess(void* userdata);
static void OnDrained(void* userdata);
struct InBufferContext {
InBufferContext(IBuffer* buffer, IBufferProvider* provider) {
this->buffer = buffer; this->provider = provider;
this->readPtr = (char*) buffer->BufferPointer();
this->remaining = (uint32_t) buffer->Bytes();
}
void Advance(int count) {
bool release = count >= remaining;
this->remaining -= count;
this->readPtr += count;
if (release) {
this->provider->OnBufferProcessed(this->buffer);
delete this;
}
}
void Discard() {
this->provider->OnBufferProcessed(this->buffer);
delete this;
}
IBuffer* buffer{nullptr};
IBufferProvider* provider{nullptr};
uint32_t remaining{0};
char* readPtr;
};
struct OutBufferContext {
void Initialize(pw_buffer* buffer) {
this->buffer = buffer;
if (buffer) {
struct spa_buffer* spaBuffer = buffer->buffer;
this->writePtr = (char*) spaBuffer->datas[0].data;
this->remaining = spaBuffer->datas[0].maxsize;
this->total = this->remaining;
}
else {
this->Reset();
}
}
void Reset() {
this->buffer = nullptr;
this->writePtr = nullptr;
this->remaining = 0;
this->total = 0;
}
void Advance(int count) {
this->remaining -= count;
this->writePtr += count;
}
void Finalize(pw_stream* stream, uint32_t stride) {
if (this->Valid()) {
spa_data& data = this->buffer->buffer->datas[0];
data.chunk->offset = 0;
data.chunk->stride = stride;
data.chunk->size = this->total - this->remaining;
pw_stream_queue_buffer(stream, this->buffer);
this->Reset();
}
}
bool Valid() {
return this->buffer != nullptr;
}
pw_buffer* buffer{nullptr};
uint32_t remaining{0};
uint32_t total{0};
char* writePtr{nullptr};
};
class Device: public musik::core::sdk::IDevice {
public:
Device(const std::string& id, const std::string& name) {
this->id = id;
this->name = name;
}
void Release() override {
delete this;
}
const char* Name() const override {
return name.c_str();
}
const char* Id() const override {
return id.c_str();
}
Device* Clone() {
return new Device(this->id, this->name);
}
private:
std::string id, name;
};
class DeviceList: public musik::core::sdk::IDeviceList {
public:
void Release() override {
delete this;
}
size_t Count() const override {
return devices.size();
}
const Device* At(size_t index) const override {
return &devices.at(index);
}
void Add(const std::string& id, const std::string& name) {
devices.push_back(Device(id, name));
}
Device* Default() {
return this->devices.empty() ? nullptr : this->devices.at(0).Clone();
}
void Reset() {
this->devices.clear();
}
uint32_t ResolveId(const std::string& id) {
for (auto device: this->devices) {
if (device.Id() == id) {
try {
return (uint32_t) std::stoi(id);
}
catch(...) {
/* return default below... */
}
}
}
return PW_ID_ANY;
}
Device* ResolveDevice(const std::string& id) {
for (auto device: this->devices) {
if (device.Id() == id) {
return device.Clone();
}
}
return nullptr;
}
DeviceList* Clone() {
auto result = new DeviceList();
result->devices = this->devices;
return result;
}
private:
std::vector<Device> devices;
};
struct DeviceListContext {
DeviceListContext(PipeWireOut* instance) {
this->instance = instance;
this->coreEvents = { PW_VERSION_CORE_EVENTS };
this->coreEvents.done = PipeWireOut::OnCoreDone;
this->coreEvents.error = PipeWireOut::OnCoreError;
spa_zero(this->coreListener);
this->registryEvents = { PW_VERSION_REGISTRY_EVENTS };
this->registryEvents.global = PipeWireOut::OnRegistryGlobal;
spa_zero(this->registryListener);
}
~DeviceListContext() {
if (this->registry) {
pw_proxy_destroy(reinterpret_cast<pw_proxy*>(this->registry));
}
if (this->context) {
pw_context_destroy(this->context);
}
if (this->loop) {
pw_main_loop_destroy(this->loop);
}
}
pw_main_loop* loop{nullptr};
pw_context* context{nullptr};
pw_core* core{nullptr};
spa_hook coreListener;
pw_core_events coreEvents;
pw_registry* registry{nullptr};
spa_hook registryListener;
pw_registry_events registryEvents;
int eventId{0};
PipeWireOut* instance{nullptr};
};
enum class State {
Stopped, Paused, Playing, Shutdown
};
std::deque<InBufferContext*> buffers;
std::recursive_mutex mutex;
std::atomic<bool> initialized{false};
std::atomic<State> state{State::Stopped};
std::condition_variable_any bufferCondition, drainCondition;
double volume{1.0};
pw_stream_events pwStreamEvents;
pw_thread_loop* pwThreadLoop {nullptr};
pw_stream* pwStream{nullptr};
OutBufferContext outBufferContext;
long channelCount{0};
long sampleRate{0};
DeviceList deviceList;
};

View File

@ -0,0 +1,63 @@
//////////////////////////////////////////////////////////////////////////////
//
// Copyright (c) 2004-2020 musikcube team
//
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
//
// * Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// * Neither the name of the author nor the names of other contributors may
// be used to endorse or promote products derived from this software
// without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
//
//////////////////////////////////////////////////////////////////////////////
#include "config.h"
#include <musikcore/sdk/constants.h>
#include <musikcore/sdk/IPlugin.h>
#include <musikcore/sdk/ISchema.h>
#include "PipeWireOut.h"
class PipeWirePlugin : public musik::core::sdk::IPlugin {
public:
void Release() noexcept override { delete this; }
const char* Name() override { return "PipeWire IOutput"; }
const char* Version() override { return "0.1.0"; }
const char* Author() override { return "clangen"; }
const char* Guid() override { return "ab79e0f2-53d8-4774-ad00-266a30c50427"; }
bool Configurable() override { return false; }
void Configure() override { }
void Reload() override { }
int SdkVersion() override { return musik::core::sdk::SdkVersion; }
};
extern "C" musik::core::sdk::IPlugin* GetPlugin() {
return new PipeWirePlugin();
}
extern "C" musik::core::sdk::IOutput* GetAudioOutput() {
return new PipeWireOut();
}
extern "C" musik::core::sdk::ISchema* GetSchema();

View File

@ -6,11 +6,11 @@ set (stockencoders_SOURCES
add_library(stockencoders SHARED ${stockencoders_SOURCES})
if (${FFMPEG_ENABLED} MATCHES "false")
message(STATUS "[stockencoders] *not* defining FFMPEG_ENABLED")
if (${ENABLE_FFMPEG} MATCHES "false")
message(STATUS "[stockencoders] *not* defining ENABLE_FFMPEG")
else()
message(STATUS "[stockencoders] defining FFMPEG_ENABLED")
add_definitions(-DFFMPEG_ENABLED)
message(STATUS "[stockencoders] defining ENABLE_FFMPEG")
add_definitions(-DENABLE_FFMPEG)
# fedora (and probably other RPM-based distros) put ffmpeg includes here...
include_directories("/usr/include/ffmpeg")
include_directories("/usr/local/include/ffmpeg")

View File

@ -34,7 +34,7 @@
#include "FfmpegEncoder.h"
#ifdef FFMPEG_ENABLED
#ifdef ENABLE_FFMPEG
#include "shared.h"
#include <algorithm>
@ -637,4 +637,4 @@ AVFrame* FfmpegEncoder::ReallocFrame(
return original;
}
#endif // FFMPEG_ENABLED
#endif // ENABLE_FFMPEG

View File

@ -35,10 +35,10 @@
#pragma once
#ifdef WIN32
#define FFMPEG_ENABLED
#define ENABLE_FFMPEG
#endif
#ifdef FFMPEG_ENABLED
#ifdef ENABLE_FFMPEG
#include <musikcore/sdk/IBlockingEncoder.h>
#include <musikcore/sdk/DataBuffer.h>
@ -98,4 +98,4 @@ class FfmpegEncoder : public musik::core::sdk::IBlockingEncoder {
int inputSampleRate;
};
#endif // FFMPEG_ENABLED
#endif // ENABLE_FFMPEG

View File

@ -62,7 +62,7 @@ static IEnvironment* environment = nullptr;
static std::set<std::string> supportedFormats = {
".mp3",
#ifdef FFMPEG_ENABLED
#ifdef ENABLE_FFMPEG
"audio/mpeg",
".ogg",
"audio/ogg",
@ -86,10 +86,10 @@ static class Plugin : public IPlugin {
}
virtual void Release() { }
#if defined(FFMPEG_ENABLED) || defined(WIN32)
virtual const char* Name() { return "Stock Encoders (MP3 + ffmpeg)"; }
#if defined(ENABLE_FFMPEG) || defined(WIN32)
virtual const char* Name() { return "Stock Encoders (lame + ffmpeg)"; }
#else
virtual const char* Name() { return "Stock Encoders (MP3)"; }
virtual const char* Name() { return "Stock Encoders (lame)"; }
#endif
virtual const char* Version() { return "0.7.0"; }
virtual const char* Author() { return "clangen"; }
@ -116,7 +116,7 @@ static class EncoderFactory: public IEncoderFactory {
if (isMp3(lowerType)) {
return new LameEncoder();
}
#ifdef FFMPEG_ENABLED
#ifdef ENABLE_FFMPEG
else if (supportedFormats.find(lowerType) != supportedFormats.end()) {
return new FfmpegEncoder(lowerType);
}

View File

@ -3,11 +3,11 @@ set (taglibreader_SOURCES
TaglibMetadataReader.cpp
)
if (${FFMPEG_ENABLED} MATCHES "false")
message(STATUS "[taglibmetadatareader] *not* defining FFMPEG_ENABLED")
if (${ENABLE_FFMPEG} MATCHES "false")
message(STATUS "[taglibmetadatareader] *not* defining ENABLE_FFMPEG")
else()
message(STATUS "[taglibmetadatareader] defining FFMPEG_ENABLED")
add_definitions(-DFFMPEG_ENABLED)
message(STATUS "[taglibmetadatareader] defining ENABLE_FFMPEG")
add_definitions(-DENABLE_FFMPEG)
endif()
add_library(taglibreader SHARED ${taglibreader_SOURCES})

View File

@ -93,7 +93,7 @@
#include <string.h>
#ifdef WIN32
#define FFMPEG_ENABLED
#define ENABLE_FFMPEG
#endif
using namespace musik::core::sdk;
@ -243,7 +243,7 @@ bool TaglibMetadataReader::CanRead(const char *extension) {
if (extension && strlen(extension)) {
std::string ext(str::lower(extension[0] == '.' ? &extension[1] : extension));
return
#ifdef FFMPEG_ENABLED
#ifdef ENABLE_FFMPEG
ext.compare("opus") == 0 ||
ext.compare("wv") == 0 ||
ext.compare("wma") == 0 ||

View File

@ -52,7 +52,7 @@
class TaglibPlugin : public musik::core::sdk::IPlugin {
public:
virtual void Release() { delete this; }
#if defined(FFMPEG_ENABLED) || defined(WIN32)
#if defined(ENABLE_FFMPEG) || defined(WIN32)
virtual const char* Name() { return "Taglib 1.11 ITagReader (+ffmpeg)"; }
#else
virtual const char* Name() { return "Taglib 1.11 ITagReader"; }