quickshell/src/services/pipewire/node.hpp
2025-06-07 03:26:55 -07:00

258 lines
7.6 KiB
C++

#pragma once
#include <pipewire/core.h>
#include <pipewire/node.h>
#include <pipewire/type.h>
#include <qcontainerfwd.h>
#include <qflags.h>
#include <qmap.h>
#include <qobject.h>
#include <qqmlintegration.h>
#include <qtclasshelpermacros.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include <spa/param/audio/raw.h>
#include <spa/pod/pod.h>
#include "core.hpp"
#include "registry.hpp"
namespace qs::service::pipewire {
class PwDevice;
///! Audio channel of a pipewire node.
/// See @@PwNodeAudio.channels.
class PwAudioChannel: public QObject {
Q_OBJECT;
QML_ELEMENT;
QML_SINGLETON;
public:
enum Enum {
Unknown = SPA_AUDIO_CHANNEL_UNKNOWN,
NA = SPA_AUDIO_CHANNEL_NA,
Mono = SPA_AUDIO_CHANNEL_MONO,
FrontCenter = SPA_AUDIO_CHANNEL_FC,
FrontLeft = SPA_AUDIO_CHANNEL_FL,
FrontRight = SPA_AUDIO_CHANNEL_FR,
FrontLeftCenter = SPA_AUDIO_CHANNEL_FLC,
FrontRightCenter = SPA_AUDIO_CHANNEL_FRC,
FrontLeftWide = SPA_AUDIO_CHANNEL_FLW,
FrontRightWide = SPA_AUDIO_CHANNEL_FRW,
FrontCenterHigh = SPA_AUDIO_CHANNEL_FCH,
FrontLeftHigh = SPA_AUDIO_CHANNEL_FLH,
FrontRightHigh = SPA_AUDIO_CHANNEL_FRH,
LowFrequencyEffects = SPA_AUDIO_CHANNEL_LFE,
LowFrequencyEffects2 = SPA_AUDIO_CHANNEL_LFE2,
LowFrequencyEffectsLeft = SPA_AUDIO_CHANNEL_LLFE,
LowFrequencyEffectsRight = SPA_AUDIO_CHANNEL_RLFE,
SideLeft = SPA_AUDIO_CHANNEL_SL,
SideRight = SPA_AUDIO_CHANNEL_SR,
RearCenter = SPA_AUDIO_CHANNEL_RC,
RearLeft = SPA_AUDIO_CHANNEL_RL,
RearRight = SPA_AUDIO_CHANNEL_RR,
RearLeftCenter = SPA_AUDIO_CHANNEL_RLC,
RearRightCenter = SPA_AUDIO_CHANNEL_RRC,
TopCenter = SPA_AUDIO_CHANNEL_TC,
TopFrontCenter = SPA_AUDIO_CHANNEL_TFC,
TopFrontLeft = SPA_AUDIO_CHANNEL_TFL,
TopFrontRight = SPA_AUDIO_CHANNEL_TFR,
TopFrontLeftCenter = SPA_AUDIO_CHANNEL_TFLC,
TopFrontRightCenter = SPA_AUDIO_CHANNEL_TFRC,
TopSideLeft = SPA_AUDIO_CHANNEL_TSL,
TopSideRight = SPA_AUDIO_CHANNEL_TSR,
TopRearCenter = SPA_AUDIO_CHANNEL_TRC,
TopRearLeft = SPA_AUDIO_CHANNEL_TRL,
TopRearRight = SPA_AUDIO_CHANNEL_TRR,
BottomCenter = SPA_AUDIO_CHANNEL_BC,
BottomLeftCenter = SPA_AUDIO_CHANNEL_BLC,
BottomRightCenter = SPA_AUDIO_CHANNEL_BRC,
/// The start of the aux channel range.
///
/// Values between AuxRangeStart and AuxRangeEnd are valid.
AuxRangeStart = SPA_AUDIO_CHANNEL_START_Aux,
/// The end of the aux channel range.
///
/// Values between AuxRangeStart and AuxRangeEnd are valid.
AuxRangeEnd = SPA_AUDIO_CHANNEL_LAST_Aux,
/// The end of the custom channel range.
///
/// Values starting at CustomRangeStart are valid.
CustomRangeStart = SPA_AUDIO_CHANNEL_START_Custom,
};
Q_ENUM(Enum);
/// Print a human readable representation of the given channel,
/// including aux and custom channel ranges.
Q_INVOKABLE static QString toString(qs::service::pipewire::PwAudioChannel::Enum value);
};
///! The type of a pipewire node.
/// Use bitwise comparisons to filter for audio, video, sink, source or stream nodes
class PwNodeType: public QObject {
Q_OBJECT;
QML_ELEMENT;
QML_SINGLETON;
public:
enum Flag : quint8 {
// A Pipewire node which is not being managed.
Untracked = 0b0,
// This flag is set when this node is an Audio node.
Audio = 0b1,
// This flag is set when this node is an Video node.
Video = 0b10,
// This flag is set when this node is a stream node.
Stream = 0b100,
// This flag is set when this node is producing some form of data,
// such as a microphone, screenshare or webcam.
Source = 0b1000,
// This flag is set when this node is receiving data.
Sink = 0b10000,
// A sink for audio samples, like an audio card.
//
// This is equivalent to the media class `Video/Source` and is
// composed of the @@PwNodeType.Audio and @@PwNodeType.Sink flags.
AudioSink = Audio | Sink,
// A source of audio samples like a microphone.
//
// This is quivalent to the media class `Video/Sink` and is composed
// of the @@PwNodeType.Audio and @@PwNodeType.Source flags.
AudioSource = Audio | Source,
// A node that is both a sink and a source.
//
// This is equivalent to the media class `Audio/Duplex` and is composed of the
// @@PwNodeType.Audio, @@PwNodeType.Source and @@PwNodeType.Sink flags.
AudioDuplex = Audio | Sink | Source,
// A playback stream.
//
// This is equivalent to the media class `Stream/Output/Audio` and is composed
// of the @@PwNodeType.Audio, @@PwNodeType.Sink and @@PwNodeType.Stream flags.
AudioOutStream = Audio | Sink | Stream,
// A capture stream.
//
// This is equivalent to the media class `Stream/Input/Audio` and is composed
// of the @@PwNodeType.Audio, @@PwNodeType.Source and @@PwNodeType.Stream flags.
AudioInStream = Audio | Source | Stream,
// A producer of video, like a webcam or a screenshare.
//
// This is equivalent to the media class `Video/Source` and is composed
// of the @@PwNodeType.Video and @@PwNodeType.Source flags.
VideoSource = Video | Source,
// A consumer of video, such as a program that is recieving a video stream.
//
// This is equivalent to the media class `Video/Sink` and is composed of the
// @@PwNodeType.Video and @@PwNodeType.Sink flags.
VideoSink = Video | Sink,
};
Q_ENUM(Flag)
Q_DECLARE_FLAGS(Flags, Flag)
Q_INVOKABLE static QString toString(qs::service::pipewire::PwNodeType::Flags type);
};
Q_DECLARE_OPERATORS_FOR_FLAGS(PwNodeType::Flags)
class PwNode;
struct PwVolumeProps {
QVector<PwAudioChannel::Enum> channels;
QVector<float> volumes;
bool mute = false;
static PwVolumeProps parseSpaPod(const spa_pod* param);
};
class PwNodeBoundData {
public:
PwNodeBoundData() = default;
virtual ~PwNodeBoundData() = default;
Q_DISABLE_COPY_MOVE(PwNodeBoundData);
virtual void onInfo(const pw_node_info* /*info*/) {}
virtual void onSpaParam(quint32 /*id*/, quint32 /*index*/, const spa_pod* /*param*/) {}
virtual void onUnbind() {}
};
class PwNodeBoundAudio
: public QObject
, public PwNodeBoundData {
Q_OBJECT;
public:
explicit PwNodeBoundAudio(PwNode* node);
void onInfo(const pw_node_info* info) override;
void onSpaParam(quint32 id, quint32 index, const spa_pod* param) override;
void onUnbind() override;
[[nodiscard]] bool isMuted() const;
void setMuted(bool muted);
[[nodiscard]] float averageVolume() const;
void setAverageVolume(float volume);
[[nodiscard]] QVector<PwAudioChannel::Enum> channels() const;
[[nodiscard]] QVector<float> volumes() const;
void setVolumes(const QVector<float>& volumes);
signals:
void volumesChanged();
void channelsChanged();
void mutedChanged();
private slots:
void onDeviceReady();
private:
void updateVolumeProps(const spa_pod* param);
bool mMuted = false;
QVector<PwAudioChannel::Enum> mChannels;
QVector<float> mVolumes;
QVector<float> mServerVolumes;
QVector<float> mDeviceVolumes;
QVector<float> waitingVolumes;
PwNode* node;
};
class PwNode: public PwBindable<pw_node, PW_TYPE_INTERFACE_Node, PW_VERSION_NODE> {
Q_OBJECT;
public:
void bindHooks() override;
void unbindHooks() override;
void initProps(const spa_dict* props) override;
QString name;
QString description;
QString nick;
QMap<QString, QString> properties;
PwNodeType::Flags type = PwNodeType::Untracked;
bool ready = false;
PwNodeBoundData* boundData = nullptr;
PwDevice* device = nullptr;
qint32 routeDevice = -1;
signals:
void propertiesChanged();
void readyChanged();
private slots:
void onCoreSync(quint32 id, qint32 seq);
private:
static const pw_node_events EVENTS;
static void onInfo(void* data, const pw_node_info* info);
static void
onParam(void* data, qint32 seq, quint32 id, quint32 index, quint32 next, const spa_pod* param);
qint32 syncSeq = 0;
SpaHook listener;
};
} // namespace qs::service::pipewire