aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authortrialuser02 <trialuser02@90c681e8-e032-0410-971d-27865f9a5e38>2021-04-24 21:09:34 +0000
committertrialuser02 <trialuser02@90c681e8-e032-0410-971d-27865f9a5e38>2021-04-24 21:09:34 +0000
commit6055e4c2f8c3055ac90f20e2214b5923cd606d72 (patch)
tree4e7931fa521306afd476ed75364301e490c93056
parent8d76bb255a357f444cb68f6090dc3a9c5a410729 (diff)
downloadqmmp-6055e4c2f8c3055ac90f20e2214b5923cd606d72.tar.gz
qmmp-6055e4c2f8c3055ac90f20e2214b5923cd606d72.tar.bz2
qmmp-6055e4c2f8c3055ac90f20e2214b5923cd606d72.zip
prepare for pipewire output implementation
git-svn-id: http://svn.code.sf.net/p/qmmp-dev/code/trunk/qmmp@9839 90c681e8-e032-0410-971d-27865f9a5e38
-rw-r--r--src/plugins/Output/pipewire/outputpipewire.cpp362
-rw-r--r--src/plugins/Output/pipewire/outputpipewire.h100
-rw-r--r--src/plugins/Output/pipewire/outputpipewirefactory.cpp62
-rw-r--r--src/plugins/Output/pipewire/outputpipewirefactory.h49
-rw-r--r--src/plugins/Output/pipewire/pipewire.pro20
-rw-r--r--src/plugins/Output/pipewire/pipewire.pro.autosave20
6 files changed, 613 insertions, 0 deletions
diff --git a/src/plugins/Output/pipewire/outputpipewire.cpp b/src/plugins/Output/pipewire/outputpipewire.cpp
new file mode 100644
index 000000000..b11842294
--- /dev/null
+++ b/src/plugins/Output/pipewire/outputpipewire.cpp
@@ -0,0 +1,362 @@
+/***************************************************************************
+ * Copyright (C) 2006-2021 by Ilya Kotov *
+ * forkotov02@ya.ru *
+ * *
+ * 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 2 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, write to the *
+ * Free Software Foundation, Inc., *
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *
+ ***************************************************************************/
+
+#include <QtDebug>
+#include <QSettings>
+#include <QThread>
+#include <QtMath>
+#include "outputpipewire.h"
+
+OutputPipeWire::OutputPipeWire(): Output()
+{
+ m_pw_channels = {
+ { Qmmp::CHAN_NULL, SPA_AUDIO_CHANNEL_NA },
+ { Qmmp::CHAN_FRONT_CENTER, SPA_AUDIO_CHANNEL_MONO },
+ { Qmmp::CHAN_FRONT_LEFT, SPA_AUDIO_CHANNEL_FL },
+ { Qmmp::CHAN_FRONT_RIGHT, SPA_AUDIO_CHANNEL_FR },
+ { Qmmp::CHAN_REAR_LEFT, SPA_AUDIO_CHANNEL_RL },
+ { Qmmp::CHAN_REAR_RIGHT, SPA_AUDIO_CHANNEL_RR },
+ { Qmmp::CHAN_FRONT_CENTER, SPA_AUDIO_CHANNEL_FC },
+ { Qmmp::CHAN_LFE, SPA_AUDIO_CHANNEL_LFE },
+ { Qmmp::CHAN_SIDE_LEFT, SPA_AUDIO_CHANNEL_SL },
+ { Qmmp::CHAN_SIDE_RIGHT, SPA_AUDIO_CHANNEL_SR },
+ { Qmmp::CHAN_REAR_CENTER, SPA_AUDIO_CHANNEL_RC }
+ };
+
+ pw_init(nullptr, nullptr);
+}
+
+OutputPipeWire::~OutputPipeWire()
+{
+ uninitialize();
+}
+
+void OutputPipeWire::onStateChanged (void *data, enum pw_stream_state old,
+ enum pw_stream_state state, const char *error)
+{
+ qDebug() << Q_FUNC_INFO << state << error;
+ OutputPipeWire *o = static_cast<OutputPipeWire *>(data);
+ pw_thread_loop_signal(o->m_loop, false);
+}
+
+bool OutputPipeWire::initialize(quint32 freq, ChannelMap map, Qmmp::AudioFormat format)
+{
+ static const pw_core_events coreEvents = {
+ .version = PW_VERSION_CORE_EVENTS,
+ .done = OutputPipeWire::onCoreEventDone
+ };
+
+ static const pw_registry_events registryEvents = {
+ .version = PW_VERSION_REGISTRY_EVENTS,
+ .global = OutputPipeWire::onRegistryEventGlobal
+ };
+
+ static const struct pw_stream_events streamEvents = {
+ .version = PW_VERSION_STREAM_EVENTS,
+ .state_changed = OutputPipeWire::onStateChanged,
+ .process = OutputPipeWire::onProcess,
+ };
+
+ if(!(m_loop = pw_thread_loop_new("pipewire-main-loop", nullptr)))
+ {
+ qWarning("OutputPipeWire: unable to create main loop");
+ return false;
+ }
+
+ if (!(m_context = pw_context_new(pw_thread_loop_get_loop(m_loop), nullptr, 0)))
+ {
+ qWarning("OutputPipeWire: unable to create context");
+ return false;
+ }
+
+ if(!(m_core = pw_context_connect(m_context, nullptr, 0)))
+ {
+ qWarning("OutputPipeWire: unable to connect context");
+ return false;
+ }
+ pw_core_add_listener(m_core, &m_coreListener, &coreEvents, this);
+
+ if(!(m_registry = pw_core_get_registry(m_core, PW_VERSION_REGISTRY, 0)))
+ {
+ qWarning("OutputPipeWire: unable to get registry interface");
+ return false;
+ }
+ pw_registry_add_listener(m_registry, &m_registryListener, &registryEvents, this);
+
+ m_coreInitSeq = pw_core_sync(m_core, PW_ID_CORE, m_coreInitSeq);
+
+
+ if (pw_thread_loop_start(m_loop) != 0)
+ {
+ //m_err = true;
+ return false;
+ }
+
+ pw_thread_loop_lock(m_loop);
+ while (!m_initDone)
+ {
+ if (pw_thread_loop_timed_wait(m_loop, 2) != 0)
+ break;
+ }
+ pw_thread_loop_unlock(m_loop);
+
+ if(!m_initDone || !m_hasSinks)
+ return false;
+
+ m_stride = AudioParameters::sampleSize(format) * map.count();
+ m_nFrames = qBound(64, qCeil((2048.0 / 48000.0) * freq), 8192);
+ m_bufferSize = m_nFrames * m_stride;
+ m_writeBufferPos = 0;
+ m_buffer = new unsigned char[m_bufferSize];
+
+ auto props = pw_properties_new(
+ PW_KEY_MEDIA_TYPE, "Audio",
+ PW_KEY_MEDIA_CATEGORY, "Playback",
+ PW_KEY_MEDIA_ROLE, "Music",
+ PW_KEY_MEDIA_ICON_NAME, "qmmp",
+ nullptr
+ );
+ pw_properties_setf(props, PW_KEY_NODE_LATENCY, "%u/%u", m_nFrames, freq);
+
+ pw_thread_loop_lock(m_loop);
+
+ m_stream = pw_stream_new(m_core, "Playback", props);
+ if (!(m_stream = pw_stream_new(m_core, "Playback", props)))
+ {
+ qWarning("OutputPipeWire: unable to create stream");
+ pw_thread_loop_unlock(m_loop);
+ return false;
+ }
+
+ m_streamListener = {};
+ pw_stream_add_listener(m_stream, &m_streamListener, &streamEvents, this);
+
+ uint8_t buffer[1024];
+ spa_pod_builder b = {
+ .data = buffer,
+ .size = sizeof(buffer)
+ };
+
+ spa_audio_info_raw info = {
+ .format = SPA_AUDIO_FORMAT_F32,
+ .flags = SPA_AUDIO_FLAG_NONE,
+ .rate = freq,
+ .channels = map.count()
+ };
+
+ switch (format)
+ {
+ case Qmmp::PCM_S8:
+ info.format = SPA_AUDIO_FORMAT_S8;
+ break;
+ case Qmmp::PCM_S16LE:
+ info.format = SPA_AUDIO_FORMAT_S16_LE;
+ break;
+ case Qmmp::PCM_S24LE:
+ info.format = SPA_AUDIO_FORMAT_S24_32_LE;
+ break;
+ case Qmmp::PCM_S32LE:
+ info.format = SPA_AUDIO_FORMAT_S32_LE;
+ break;
+ case Qmmp::PCM_FLOAT:
+ info.format = SPA_AUDIO_FORMAT_F32_LE;
+ break;
+ default:
+ info.format = SPA_AUDIO_FORMAT_S16_LE;
+ }
+
+ for(int i = 0; i < map.count(); i++)
+ info.position[i] = m_pw_channels[map.value(i)];
+
+ const spa_pod *params[2];
+ params[0] = spa_format_audio_raw_build(
+ &b,
+ SPA_PARAM_EnumFormat,
+ &info
+ );
+ /*params[1] = static_cast<spa_pod *>(spa_pod_builder_add_object(
+ &b,
+ SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers,
+ SPA_PARAM_BUFFERS_size, m_bufferSize)
+ );*/
+
+
+
+
+ qDebug() << pw_stream_connect(m_stream,
+ PW_DIRECTION_OUTPUT,
+ PW_ID_ANY,
+ static_cast<pw_stream_flags>(PW_STREAM_FLAG_AUTOCONNECT |
+ PW_STREAM_FLAG_MAP_BUFFERS |
+ PW_STREAM_FLAG_RT_PROCESS),
+ params, 1);
+
+
+
+ Output::configure(freq, map, format);
+ pw_thread_loop_unlock(m_loop);
+ qDebug("OutputPipeWire: ready");
+ return true;
+}
+
+qint64 OutputPipeWire::latency()
+{
+ /*pa_usec_t usec;
+ int negative;
+
+ int r = pa_stream_get_latency(m_stream, &usec, &negative);
+ return (r == PA_OK && negative == 0) ? (usec / 1000) : 0;*/
+ return 0;
+}
+
+qint64 OutputPipeWire::writeAudio(unsigned char *data, qint64 maxSize)
+{
+ //qDebug() << Q_FUNC_INFO;
+ pw_thread_loop_lock(m_loop);
+
+ if(m_buffer_at == m_bufferSize)
+ {
+ if (pw_thread_loop_timed_wait(m_loop, 1) != 0)
+ {
+ pw_thread_loop_unlock(m_loop);
+ return 0;
+ }
+ }
+
+ qint64 size = qMin(qint64(m_bufferSize - m_buffer_at), maxSize);
+ memcpy(m_buffer + m_buffer_at, data, size);
+ m_buffer_at += size;
+ pw_thread_loop_unlock(m_loop);
+ return size;
+}
+
+void OutputPipeWire::drain()
+{
+ //pa_operation *op = pa_stream_drain(m_stream, OutputPipeWire::stream_success_cb, nullptr);
+ //process(op);
+}
+
+void OutputPipeWire::reset()
+{
+ //pa_operation *op = pa_stream_flush(m_stream, OutputPipeWire::stream_success_cb, nullptr);
+ //process(op);
+}
+
+void OutputPipeWire::suspend()
+{
+ //pa_operation *op = pa_stream_cork(m_stream, 1, OutputPipeWire::stream_success_cb, nullptr);
+ //process(op);
+}
+
+void OutputPipeWire::resume()
+{
+ //pa_operation *op = pa_stream_cork(m_stream, 0, OutputPipeWire::stream_success_cb, nullptr);
+ //process(op);
+}
+
+void OutputPipeWire::setMuted(bool mute)
+{
+ //pa_operation *op = pa_context_set_sink_input_mute(m_ctx, pa_stream_get_index(m_stream), mute,
+ // OutputPipeWire::context_success_cb, nullptr);
+ //pa_operation_unref(op);
+}
+
+void OutputPipeWire::uninitialize()
+{
+ if(m_loop)
+ {
+ pw_thread_loop_stop(m_loop);
+ pw_thread_loop_destroy(m_loop);
+ m_loop = nullptr;
+ }
+
+ if(m_stream)
+ {
+ pw_stream_disconnect(m_stream);
+ pw_stream_destroy(m_stream);
+ m_stream = nullptr;
+ }
+
+ pw_deinit();
+}
+
+//callbacks
+void OutputPipeWire::onProcess(void *data)
+{
+ OutputPipeWire *o = static_cast<OutputPipeWire *>(data);
+
+ if(!o->m_buffer_at)
+ {
+ pw_thread_loop_signal(o->m_loop, false);
+ return;
+ }
+
+ struct pw_buffer *b = pw_stream_dequeue_buffer(o->m_stream);
+ struct spa_buffer *buf = b->buffer;
+
+ uint32_t size = qMin(buf->datas[0].maxsize, uint32_t(o->m_buffer_at));
+ memcpy(buf->datas[0].data, o->m_buffer, size);
+ o->m_buffer_at -= size;
+ memmove(o->m_buffer, o->m_buffer + size, o->m_buffer_at);
+
+ b->buffer->datas[0].chunk->offset = 0;
+ b->buffer->datas[0].chunk->size = o->m_bufferSize;
+ b->buffer->datas[0].chunk->stride = o->m_stride;
+
+ pw_stream_queue_buffer(o->m_stream, b);
+ pw_thread_loop_signal(o->m_loop, false);
+}
+
+void OutputPipeWire::onCoreEventDone(void *data, uint32_t id, int seq)
+{
+ OutputPipeWire *o = static_cast<OutputPipeWire *>(data);
+
+ if (id == PW_ID_CORE && seq == o->m_coreInitSeq)
+ {
+ spa_hook_remove(&o->m_registryListener);
+ spa_hook_remove(&o->m_coreListener);
+
+ o->m_initDone = true;
+ pw_thread_loop_signal(o->m_loop, false);
+ }
+}
+
+void OutputPipeWire::onRegistryEventGlobal(void *data, uint32_t id, uint32_t permissions, const char *type, uint32_t version, const spa_dict *props)
+{
+ Q_UNUSED(id)
+ Q_UNUSED(permissions)
+ Q_UNUSED(version)
+
+ OutputPipeWire *o = static_cast<OutputPipeWire *>(data);
+
+ if (qstrcmp(type, PW_TYPE_INTERFACE_Node) != 0)
+ return;
+
+ auto media_class = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS);
+ if (!media_class)
+ return;
+
+ if (qstrcmp(media_class, "Audio/Sink") != 0)
+ return;
+
+ o->m_hasSinks = true;
+
+ o->m_coreInitSeq = pw_core_sync(o->m_core, PW_ID_CORE, o->m_coreInitSeq);
+}
diff --git a/src/plugins/Output/pipewire/outputpipewire.h b/src/plugins/Output/pipewire/outputpipewire.h
new file mode 100644
index 000000000..1b70a94d3
--- /dev/null
+++ b/src/plugins/Output/pipewire/outputpipewire.h
@@ -0,0 +1,100 @@
+/***************************************************************************
+ * Copyright (C) 2006-2021 by Ilya Kotov *
+ * forkotov02@ya.ru *
+ * *
+ * 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 2 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, write to the *
+ * Free Software Foundation, Inc., *
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *
+ ***************************************************************************/
+
+#ifndef OUTPUTPIPEWIRE_H
+#define OUTPUTPIPEWIRE_H
+
+#include <QObject>
+#include <QHash>
+#include <pipewire/pipewire.h>
+#include <spa/param/audio/format-utils.h>
+#include <atomic>
+#include <qmmp/output.h>
+#include <qmmp/volume.h>
+
+class VolumePulseAudio;
+
+/**
+ @author Ilya Kotov <forkotov02@ya.ru>
+*/
+class OutputPipeWire : public Output
+{
+public:
+ OutputPipeWire();
+ ~OutputPipeWire();
+
+ bool initialize(quint32, ChannelMap map, Qmmp::AudioFormat format) override;
+ //output api
+ qint64 latency() override;
+ qint64 writeAudio(unsigned char *data, qint64 maxSize) override;
+ void drain() override;
+ void reset() override;
+ void suspend() override;
+ void resume() override;
+ void setMuted(bool mute);
+
+private:
+ // helper functions
+ void uninitialize();
+ //callbacks
+ static void onProcess(void *data);
+ static void onCoreEventDone(void *data, uint32_t id, int seq);
+ static void onRegistryEventGlobal(void *data, uint32_t id,
+ uint32_t permissions, const char *type, uint32_t version,
+ const struct spa_dict *props);
+
+ static void onStateChanged (void *data, enum pw_stream_state old,
+ enum pw_stream_state state, const char *error);
+
+ struct pw_thread_loop *m_loop = nullptr;
+ struct pw_stream *m_stream = nullptr;
+ pw_context *m_context = nullptr;
+ pw_core *m_core = nullptr;
+ pw_registry *m_registry = nullptr;
+ spa_hook m_coreListener = {};
+ spa_hook m_streamListener = {};
+ spa_hook m_registryListener = {};
+
+ uint32_t m_stride = 0;
+ uint32_t m_nFrames = 0;
+ uint32_t m_bufferSize = 0;
+ uint32_t m_writeBufferPos = 0;
+
+ int m_coreInitSeq = 0;
+
+ std::atomic_bool m_hasSinks {false};
+ std::atomic_bool m_initDone {false};
+ std::atomic_bool m_bufferHasData {false};
+ std::atomic_bool m_processed {false};
+ std::atomic_bool m_paused {false};
+ std::atomic_bool m_silence {false};
+ std::atomic_bool m_streamPaused {false};
+ std::atomic_bool m_ignoreStateChange {false};
+ std::atomic_bool m_err {false};
+
+
+ //uint8_t m_stream_buffer[1024];
+ //unsigned char m_buffer[4096];
+ unsigned char *m_buffer = nullptr;
+ uint32_t m_buffer_at = 0;
+ QHash <Qmmp::ChannelPosition, spa_audio_channel> m_pw_channels;
+};
+
+#endif // OUTPUTPIPEWIRE_H
diff --git a/src/plugins/Output/pipewire/outputpipewirefactory.cpp b/src/plugins/Output/pipewire/outputpipewirefactory.cpp
new file mode 100644
index 000000000..fa13c7d62
--- /dev/null
+++ b/src/plugins/Output/pipewire/outputpipewirefactory.cpp
@@ -0,0 +1,62 @@
+/***************************************************************************
+ * Copyright (C) 2007-2021 by Ilya Kotov *
+ * forkotov02@ya.ru *
+ * *
+ * 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 2 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, write to the *
+ * Free Software Foundation, Inc., *
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *
+ ***************************************************************************/
+
+#include <QMessageBox>
+#include <qmmp/qmmp.h>
+#include "outputpipewire.h"
+#include "outputpipewirefactory.h"
+
+
+OutputProperties OutputPipeWireFactory::properties() const
+{
+ OutputProperties properties;
+ properties.name = tr("PipeWire Plugin");
+ properties.hasAbout = true;
+ properties.hasSettings = false;
+ properties.shortName = "pipewire";
+ return properties;
+}
+
+Output* OutputPipeWireFactory::create()
+{
+ return new OutputPipeWire();
+}
+
+Volume *OutputPipeWireFactory::createVolume()
+{
+ return nullptr;
+}
+
+void OutputPipeWireFactory::showSettings(QWidget* parent)
+{
+ Q_UNUSED(parent);
+}
+
+void OutputPipeWireFactory::showAbout(QWidget *parent)
+{
+ QMessageBox::about (parent, tr("About PipeWire Output Plugin"),
+ tr("Qmmp PipeWireo Output Plugin")+"\n"+
+ tr("Written by: Ilya Kotov <forkotov02@ya.ru>"));
+}
+
+QString OutputPipeWireFactory::translation() const
+{
+ return QLatin1String(":/pipwire_plugin_");
+}
diff --git a/src/plugins/Output/pipewire/outputpipewirefactory.h b/src/plugins/Output/pipewire/outputpipewirefactory.h
new file mode 100644
index 000000000..64697bbbe
--- /dev/null
+++ b/src/plugins/Output/pipewire/outputpipewirefactory.h
@@ -0,0 +1,49 @@
+/***************************************************************************
+ * Copyright (C) 2007-2021 by Ilya Kotov *
+ * forkotov02@ya.ru *
+ * *
+ * 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 2 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, write to the *
+ * Free Software Foundation, Inc., *
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *
+ ***************************************************************************/
+#ifndef OUTPUTPIPEWIREFACTORY_H
+#define OUTPUTPIPEWIREFACTORY_H
+
+
+#include <QObject>
+#include <QString>
+#include <QIODevice>
+#include <QWidget>
+
+#include <qmmp/output.h>
+#include <qmmp/outputfactory.h>
+
+
+class OutputPipeWireFactory : public QObject,
+ OutputFactory
+{
+Q_OBJECT
+Q_PLUGIN_METADATA(IID "org.qmmp.qmmp.OutputFactoryInterface.1.0")
+Q_INTERFACES(OutputFactory)
+
+public:
+ OutputProperties properties() const override;
+ Output* create() override;
+ Volume *createVolume() override;
+ void showSettings(QWidget* parent) override;
+ void showAbout(QWidget *parent) override;
+ QString translation() const override;
+};
+
+#endif
diff --git a/src/plugins/Output/pipewire/pipewire.pro b/src/plugins/Output/pipewire/pipewire.pro
new file mode 100644
index 000000000..4610bcc88
--- /dev/null
+++ b/src/plugins/Output/pipewire/pipewire.pro
@@ -0,0 +1,20 @@
+include(../../plugins.pri)
+
+TARGET = $$PLUGINS_PREFIX/Output/pipewire
+
+HEADERS += outputpipewirefactory.h \
+ outputpipewire.h
+
+SOURCES += outputpipewirefactory.cpp \
+ outputpipewire.cpp
+
+INCLUDEPATH += /usr/include/pipewire-0.3 /usr/include/spa-0.2
+
+#RESOURCES = translations/translations.qrc
+
+PKGCONFIG += libpipewire-0.3
+
+QMAKE_CXXFLAGS += -Wno-missing-field-initializers
+
+target.path = $$PLUGIN_DIR/Output
+INSTALLS += target
diff --git a/src/plugins/Output/pipewire/pipewire.pro.autosave b/src/plugins/Output/pipewire/pipewire.pro.autosave
new file mode 100644
index 000000000..549c3aef3
--- /dev/null
+++ b/src/plugins/Output/pipewire/pipewire.pro.autosave
@@ -0,0 +1,20 @@
+include(../../plugins.pri)
+
+TARGET = $$PLUGINS_PREFIX/Output/pipewire
+
+HEADERS += outputpipewirefactory.h \
+ outputpipewire.h
+
+SOURCES += outputpipewirefactory.cpp \
+ outputpipewire.cpp
+
+INCLUDEPATH += /usr/include/pipewire-0.3 /usr/include/spa-0.2
+
+#RESOURCES = translations/translations.qrc
+
+PKGCONFIG += libpipewire-0.3 lib
+
+QMAKE_CXXFLAGS += -Wno-missing-field-initializers
+
+target.path = $$PLUGIN_DIR/Output
+INSTALLS += target