From 6ae1a8e811f72bda251f8718fb148fcb6853bcc4 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Tue, 9 Sep 2025 16:31:02 +1000 Subject: plugin/ac: not singleton Also add nodeId prop BeatTracker not singleton Move BeatTracker and Cava to Audio service --- modules/background/Visualiser.qml | 4 +-- modules/dashboard/Media.qml | 9 +++-- modules/dashboard/dash/Media.qml | 5 ++- plugin/src/Caelestia/audiocollector.cpp | 56 ++++++++++++++++++++++--------- plugin/src/Caelestia/audiocollector.hpp | 28 +++++++++++----- plugin/src/Caelestia/audioprovider.cpp | 59 ++++++++++++++++++++++++++++----- plugin/src/Caelestia/audioprovider.hpp | 18 +++++++--- plugin/src/Caelestia/beattracker.cpp | 50 +++++++++++++++++++++++----- plugin/src/Caelestia/beattracker.hpp | 7 ++-- plugin/src/Caelestia/cavaprovider.cpp | 54 ++++++++++++++++++++---------- plugin/src/Caelestia/cavaprovider.hpp | 6 +++- services/Audio.qml | 21 ++++++++++++ services/Cava.qml | 18 ---------- 13 files changed, 241 insertions(+), 94 deletions(-) delete mode 100644 services/Cava.qml diff --git a/modules/background/Visualiser.qml b/modules/background/Visualiser.qml index a8ada1e..ad726cc 100644 --- a/modules/background/Visualiser.qml +++ b/modules/background/Visualiser.qml @@ -16,7 +16,7 @@ Item { required property Wallpaper wallpaper ServiceRef { - service: Cava.provider + service: Audio.cava } MultiEffect { @@ -65,7 +65,7 @@ Item { id: bar required property int modelData - property real value: Math.max(0, Math.min(1, Cava.values[side.isRight ? modelData : side.count - modelData - 1])) + property real value: Math.max(0, Math.min(1, Audio.cava.values[side.isRight ? modelData : side.count - modelData - 1])) clip: true diff --git a/modules/dashboard/Media.qml b/modules/dashboard/Media.qml index 9d61eb5..163d271 100644 --- a/modules/dashboard/Media.qml +++ b/modules/dashboard/Media.qml @@ -2,7 +2,6 @@ pragma ComponentBehavior: Bound import qs.components import qs.components.effects -import qs.components.misc import qs.components.controls import qs.services import qs.utils @@ -56,11 +55,11 @@ Item { } ServiceRef { - service: Cava.provider + service: Audio.cava } ServiceRef { - service: BeatTracker + service: Audio.beatTracker } Shape { @@ -91,7 +90,7 @@ Item { id: visualiserBar required property int modelData - readonly property real value: Math.max(1e-3, Math.min(1, Cava.values[modelData])) + readonly property real value: Math.max(1e-3, Math.min(1, Audio.cava.values[modelData])) readonly property real angle: modelData * 2 * Math.PI / Config.services.visualiserBars readonly property real magnitude: value * Config.dashboard.sizes.mediaVisualiserSize @@ -530,7 +529,7 @@ Item { height: visualiser.height * 0.75 playing: Players.active?.isPlaying ?? false - speed: BeatTracker.bpm / 300 + speed: Audio.beatTracker.bpm / 300 source: Paths.absolutePath(Config.paths.mediaGif) asynchronous: true fillMode: AnimatedImage.PreserveAspectFit diff --git a/modules/dashboard/dash/Media.qml b/modules/dashboard/dash/Media.qml index f3fbdbe..d4cccc8 100644 --- a/modules/dashboard/dash/Media.qml +++ b/modules/dashboard/dash/Media.qml @@ -1,5 +1,4 @@ import qs.components -import qs.components.misc import qs.services import qs.config import qs.utils @@ -34,7 +33,7 @@ Item { } ServiceRef { - service: BeatTracker + service: Audio.beatTracker } Shape { @@ -214,7 +213,7 @@ Item { anchors.margins: Appearance.padding.large * 2 playing: Players.active?.isPlaying ?? false - speed: BeatTracker.bpm / 300 + speed: Audio.beatTracker.bpm / 300 source: Paths.absolutePath(Config.paths.mediaGif) asynchronous: true fillMode: AnimatedImage.PreserveAspectFit diff --git a/plugin/src/Caelestia/audiocollector.cpp b/plugin/src/Caelestia/audiocollector.cpp index b9dfc23..04d609c 100644 --- a/plugin/src/Caelestia/audiocollector.cpp +++ b/plugin/src/Caelestia/audiocollector.cpp @@ -3,9 +3,9 @@ #include "service.hpp" #include #include -#include #include #include +#include #include #include #include @@ -25,6 +25,7 @@ PipeWireWorker::PipeWireWorker(std::stop_token token, AudioCollector* collector) m_loop = pw_main_loop_new(nullptr); if (!m_loop) { qWarning() << "PipeWireWorker::init: failed to create PipeWire main loop"; + pw_deinit(); return; } @@ -66,10 +67,17 @@ PipeWireWorker::PipeWireWorker(std::stop_token token, AudioCollector* collector) m_stream = pw_stream_new_simple(pw_main_loop_get_loop(m_loop), "caelestia-shell", props, &events, this); - pw_stream_connect(m_stream, PW_DIRECTION_INPUT, PW_ID_ANY, + const int success = pw_stream_connect(m_stream, PW_DIRECTION_INPUT, collector->nodeId(), static_cast( PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS | PW_STREAM_FLAG_RT_PROCESS), params, 1); + if (success < 0) { + qWarning() << "PipeWireWorker::init: failed to connect stream"; + pw_stream_destroy(m_stream); + pw_main_loop_destroy(m_loop); + pw_deinit(); + return; + } pw_main_loop_run(m_loop); @@ -155,27 +163,20 @@ unsigned int PipeWireWorker::nextPowerOf2(unsigned int n) { return n; } -AudioCollector::AudioCollector(uint32_t sampleRate, uint32_t chunkSize, QObject* parent) +AudioCollector::AudioCollector(QObject* parent) : Service(parent) - , m_buffer1(chunkSize) - , m_buffer2(chunkSize) + , m_sampleRate(44100) + , m_chunkSize(512) + , m_nodeId(PW_ID_ANY) + , m_buffer1(m_chunkSize) + , m_buffer2(m_chunkSize) , m_readBuffer(&m_buffer1) - , m_writeBuffer(&m_buffer2) - , m_sampleRate(sampleRate) - , m_chunkSize(chunkSize) {} + , m_writeBuffer(&m_buffer2) {} AudioCollector::~AudioCollector() { stop(); } -AudioCollector* AudioCollector::instance() { - std::lock_guard lock(s_mutex); - if (s_instance == nullptr) { - s_instance = new AudioCollector(); - } - return s_instance; -} - uint32_t AudioCollector::sampleRate() const { return m_sampleRate; } @@ -184,6 +185,29 @@ uint32_t AudioCollector::chunkSize() const { return m_chunkSize; } +uint32_t AudioCollector::nodeId() { + QMutexLocker locker(&m_nodeIdMutex); + return m_nodeId; +} + +void AudioCollector::setNodeId(uint32_t nodeId) { + { + QMutexLocker locker(&m_nodeIdMutex); + + if (nodeId == m_nodeId) { + return; + } + + m_nodeId = nodeId; + } + emit nodeIdChanged(); + + if (m_thread.joinable()) { + stop(); + start(); + } +} + void AudioCollector::clearBuffer() { auto* writeBuffer = m_writeBuffer.load(std::memory_order_relaxed); std::fill(writeBuffer->begin(), writeBuffer->end(), 0.0f); diff --git a/plugin/src/Caelestia/audiocollector.hpp b/plugin/src/Caelestia/audiocollector.hpp index 283b758..ecfae09 100644 --- a/plugin/src/Caelestia/audiocollector.hpp +++ b/plugin/src/Caelestia/audiocollector.hpp @@ -3,8 +3,9 @@ #include "service.hpp" #include #include -#include #include +#include +#include #include #include #include @@ -41,24 +42,35 @@ private: class AudioCollector : public Service { Q_OBJECT + QML_ELEMENT + + Q_PROPERTY(uint32_t nodeId READ nodeId WRITE setNodeId NOTIFY nodeIdChanged) public: - explicit AudioCollector(uint32_t sampleRate = 44100, uint32_t chunkSize = 512, QObject* parent = nullptr); + explicit AudioCollector(QObject* parent = nullptr); ~AudioCollector(); - static AudioCollector* instance(); - [[nodiscard]] uint32_t sampleRate() const; [[nodiscard]] uint32_t chunkSize() const; + [[nodiscard]] uint32_t nodeId(); + void setNodeId(uint32_t nodeId); + void clearBuffer(); void loadChunk(const int16_t* samples, uint32_t count); uint32_t readChunk(float* out, uint32_t count = 0); uint32_t readChunk(double* out, uint32_t count = 0); +signals: + void sampleRateChanged(); + void chunkSizeChanged(); + void nodeIdChanged(); + private: - inline static AudioCollector* s_instance = nullptr; - inline static std::mutex s_mutex; + const uint32_t m_sampleRate; + const uint32_t m_chunkSize; + uint32_t m_nodeId; + QMutex m_nodeIdMutex; std::jthread m_thread; std::vector m_buffer1; @@ -67,9 +79,7 @@ private: std::atomic*> m_writeBuffer; uint32_t m_sampleCount; - const uint32_t m_sampleRate; - const uint32_t m_chunkSize; - + void reload(); void start() override; void stop() override; }; diff --git a/plugin/src/Caelestia/audioprovider.cpp b/plugin/src/Caelestia/audioprovider.cpp index 4188460..f31480e 100644 --- a/plugin/src/Caelestia/audioprovider.cpp +++ b/plugin/src/Caelestia/audioprovider.cpp @@ -7,10 +7,9 @@ namespace caelestia { -AudioProcessor::AudioProcessor(QObject* parent) +AudioProcessor::AudioProcessor(AudioCollector* collector, QObject* parent) : QObject(parent) - , m_sampleRate(AudioCollector::instance()->sampleRate()) - , m_chunkSize(AudioCollector::instance()->chunkSize()) {} + , m_collector(collector) {} AudioProcessor::~AudioProcessor() { stop(); @@ -18,26 +17,53 @@ AudioProcessor::~AudioProcessor() { void AudioProcessor::init() { m_timer = new QTimer(this); - m_timer->setInterval(static_cast(m_chunkSize * 1000.0 / m_sampleRate)); + if (m_collector) { + m_timer->setInterval(static_cast(m_collector->chunkSize() * 1000.0 / m_collector->sampleRate())); + } connect(m_timer, &QTimer::timeout, this, &AudioProcessor::process); } -void AudioProcessor::start() { - AudioCollector::instance()->ref(); +void AudioProcessor::setCollector(AudioCollector* collector) { + if (m_collector == collector) { + return; + } + if (m_timer) { + if (m_timer->isActive()) { + if (m_collector) { + m_collector->unref(); + } + if (collector) { + collector->ref(); + } + } + if (collector) { + m_timer->setInterval(static_cast(collector->chunkSize() * 1000.0 / collector->sampleRate())); + } else { + m_timer->stop(); + } + } + + m_collector = collector; +} + +void AudioProcessor::start() { + if (m_timer && m_collector) { + m_collector->ref(); m_timer->start(); } } void AudioProcessor::stop() { - if (m_timer) { + if (m_timer && m_collector) { m_timer->stop(); + m_collector->unref(); } - AudioCollector::instance()->unref(); } AudioProvider::AudioProvider(QObject* parent) : Service(parent) + , m_collector(nullptr) , m_processor(nullptr) , m_thread(nullptr) {} @@ -48,6 +74,23 @@ AudioProvider::~AudioProvider() { } } +AudioCollector* AudioProvider::collector() const { + return m_collector; +} + +void AudioProvider::setCollector(AudioCollector* collector) { + if (m_collector == collector) { + return; + } + + m_collector = collector; + emit collectorChanged(); + + if (m_processor) { + QMetaObject::invokeMethod(m_processor, "setCollector", Qt::QueuedConnection, Q_ARG(AudioCollector*, collector)); + } +} + void AudioProvider::init() { if (!m_processor) { qWarning() << "AudioProvider::init: attempted to init with no processor set"; diff --git a/plugin/src/Caelestia/audioprovider.hpp b/plugin/src/Caelestia/audioprovider.hpp index aa7fb83..c92b965 100644 --- a/plugin/src/Caelestia/audioprovider.hpp +++ b/plugin/src/Caelestia/audioprovider.hpp @@ -1,7 +1,7 @@ #pragma once +#include "audiocollector.hpp" #include "service.hpp" -#include #include #include @@ -11,14 +11,15 @@ class AudioProcessor : public QObject { Q_OBJECT public: - explicit AudioProcessor(QObject* parent = nullptr); + explicit AudioProcessor(AudioCollector* collector, QObject* parent = nullptr); ~AudioProcessor(); void init(); protected: - uint32_t m_sampleRate; - uint32_t m_chunkSize; + AudioCollector* m_collector; + + Q_INVOKABLE virtual void setCollector(AudioCollector* collector); private: QTimer* m_timer; @@ -32,11 +33,20 @@ private: class AudioProvider : public Service { Q_OBJECT + Q_PROPERTY(AudioCollector* collector READ collector WRITE setCollector NOTIFY collectorChanged) + public: explicit AudioProvider(QObject* parent = nullptr); ~AudioProvider(); + AudioCollector* collector() const; + void setCollector(AudioCollector* collector); + +signals: + void collectorChanged(); + protected: + AudioCollector* m_collector; AudioProcessor* m_processor; void init(); diff --git a/plugin/src/Caelestia/beattracker.cpp b/plugin/src/Caelestia/beattracker.cpp index a23d052..501739f 100644 --- a/plugin/src/Caelestia/beattracker.cpp +++ b/plugin/src/Caelestia/beattracker.cpp @@ -6,20 +6,52 @@ namespace caelestia { -BeatProcessor::BeatProcessor(QObject* parent) - : AudioProcessor(parent) - , m_tempo(new_aubio_tempo("default", 1024, m_chunkSize, m_sampleRate)) - , m_in(new_fvec(m_chunkSize)) - , m_out(new_fvec(2)) {}; +BeatProcessor::BeatProcessor(AudioCollector* collector, QObject* parent) + : AudioProcessor(collector, parent) + , m_tempo(nullptr) + , m_in(nullptr) + , m_out(new_fvec(2)) { + if (collector) { + m_tempo = new_aubio_tempo("default", 1024, collector->chunkSize(), collector->sampleRate()); + m_in = new_fvec(collector->chunkSize()); + } +}; BeatProcessor::~BeatProcessor() { - del_aubio_tempo(m_tempo); - del_fvec(m_in); + if (m_tempo) { + del_aubio_tempo(m_tempo); + } + if (m_in) { + del_fvec(m_in); + } del_fvec(m_out); } +void BeatProcessor::setCollector(AudioCollector* collector) { + AudioProcessor::setCollector(collector); + + if (m_tempo) { + del_aubio_tempo(m_tempo); + } + if (m_in) { + del_fvec(m_in); + } + + if (collector) { + m_tempo = new_aubio_tempo("default", 1024, collector->chunkSize(), collector->sampleRate()); + m_in = new_fvec(collector->chunkSize()); + } else { + m_tempo = nullptr; + m_in = nullptr; + } +} + void BeatProcessor::process() { - AudioCollector::instance()->readChunk(m_in->data, m_chunkSize); + if (!m_collector || !m_tempo || !m_in) { + return; + } + + m_collector->readChunk(m_in->data); aubio_tempo_do(m_tempo, m_in, m_out); if (m_out->data[0] != 0.0f) { @@ -30,7 +62,7 @@ void BeatProcessor::process() { BeatTracker::BeatTracker(QObject* parent) : AudioProvider(parent) , m_bpm(120) { - m_processor = new BeatProcessor(); + m_processor = new BeatProcessor(m_collector); init(); connect(static_cast(m_processor), &BeatProcessor::beat, this, &BeatTracker::updateBpm); diff --git a/plugin/src/Caelestia/beattracker.hpp b/plugin/src/Caelestia/beattracker.hpp index c7737e1..ab18373 100644 --- a/plugin/src/Caelestia/beattracker.hpp +++ b/plugin/src/Caelestia/beattracker.hpp @@ -1,5 +1,6 @@ #pragma once +#include "audiocollector.hpp" #include "audioprovider.hpp" #include #include @@ -10,12 +11,15 @@ class BeatProcessor : public AudioProcessor { Q_OBJECT public: - explicit BeatProcessor(QObject* parent = nullptr); + explicit BeatProcessor(AudioCollector* collector, QObject* parent = nullptr); ~BeatProcessor(); signals: void beat(smpl_t bpm); +protected: + void setCollector(AudioCollector* collector) override; + private: aubio_tempo_t* m_tempo; fvec_t* m_in; @@ -27,7 +31,6 @@ private: class BeatTracker : public AudioProvider { Q_OBJECT QML_ELEMENT - QML_SINGLETON Q_PROPERTY(smpl_t bpm READ bpm NOTIFY bpmChanged) diff --git a/plugin/src/Caelestia/cavaprovider.cpp b/plugin/src/Caelestia/cavaprovider.cpp index d053702..76a1a10 100644 --- a/plugin/src/Caelestia/cavaprovider.cpp +++ b/plugin/src/Caelestia/cavaprovider.cpp @@ -9,16 +9,38 @@ namespace caelestia { -CavaProcessor::CavaProcessor(QObject* parent) - : AudioProcessor(parent) +CavaProcessor::CavaProcessor(AudioCollector* collector, QObject* parent) + : AudioProcessor(collector, parent) , m_plan(nullptr) - , m_in(new double[static_cast(m_chunkSize)]) + , m_in(nullptr) , m_out(nullptr) - , m_bars(0) {}; + , m_bars(0) { + if (collector) { + m_in = new double[collector->chunkSize()]; + } +}; CavaProcessor::~CavaProcessor() { cleanup(); - delete[] m_in; + if (m_in) { + delete[] m_in; + } +} + +void CavaProcessor::setCollector(AudioCollector* collector) { + AudioProcessor::setCollector(collector); + + if (m_in) { + delete[] m_in; + } + + if (collector) { + m_in = new double[collector->chunkSize()]; + } else { + m_in = nullptr; + } + + reload(); } void CavaProcessor::setBars(int bars) { @@ -39,13 +61,11 @@ void CavaProcessor::reload() { } void CavaProcessor::cleanup() { - if (!m_plan) { - return; + if (m_plan) { + cava_destroy(m_plan); + m_plan = nullptr; } - cava_destroy(m_plan); - m_plan = nullptr; - if (m_out) { delete[] m_out; m_out = nullptr; @@ -53,11 +73,11 @@ void CavaProcessor::cleanup() { } void CavaProcessor::initCava() { - if (m_plan || m_bars == 0) { + if (m_plan || m_bars == 0 || !m_collector) { return; } - m_plan = cava_init(m_bars, static_cast(m_sampleRate), 1, 1, 0.85, 50, 10000); + m_plan = cava_init(m_bars, m_collector->sampleRate(), 1, 1, 0.85, 50, 10000); if (m_plan->status == -1) { qWarning() << "CavaProcessor::initCava: failed to initialise cava plan"; @@ -69,11 +89,11 @@ void CavaProcessor::initCava() { } void CavaProcessor::process() { - if (!m_plan || m_bars == 0) { + if (!m_plan || m_bars == 0 || !m_collector || !m_in || !m_out) { return; } - const int count = static_cast(AudioCollector::instance()->readChunk(m_in)); + const int count = static_cast(m_collector->readChunk(m_in)); // Process in data via cava cava_execute(m_in, count, m_out, m_plan); @@ -100,8 +120,8 @@ void CavaProcessor::process() { CavaProvider::CavaProvider(QObject* parent) : AudioProvider(parent) , m_bars(0) - , m_values(m_bars) { - m_processor = new CavaProcessor(); + , m_values(m_bars, 0.0) { + m_processor = new CavaProcessor(m_collector); init(); connect(static_cast(m_processor), &CavaProcessor::valuesChanged, this, &CavaProvider::updateValues); @@ -121,7 +141,7 @@ void CavaProvider::setBars(int bars) { return; } - m_values.resize(bars); + m_values.resize(bars, 0.0); m_bars = bars; emit barsChanged(); emit valuesChanged(); diff --git a/plugin/src/Caelestia/cavaprovider.hpp b/plugin/src/Caelestia/cavaprovider.hpp index 131e166..6dab635 100644 --- a/plugin/src/Caelestia/cavaprovider.hpp +++ b/plugin/src/Caelestia/cavaprovider.hpp @@ -1,5 +1,6 @@ #pragma once +#include "audiocollector.hpp" #include "audioprovider.hpp" #include #include @@ -10,12 +11,15 @@ class CavaProcessor : public AudioProcessor { Q_OBJECT public: - explicit CavaProcessor(QObject* parent = nullptr); + explicit CavaProcessor(AudioCollector* collector, QObject* parent = nullptr); ~CavaProcessor(); signals: void valuesChanged(QVector values); +protected: + void setCollector(AudioCollector* collector) override; + private: struct cava_plan* m_plan; double* m_in; diff --git a/services/Audio.qml b/services/Audio.qml index a0475d0..e638af5 100644 --- a/services/Audio.qml +++ b/services/Audio.qml @@ -1,6 +1,7 @@ pragma Singleton import qs.config +import Caelestia import Quickshell import Quickshell.Services.Pipewire @@ -32,6 +33,9 @@ Singleton { readonly property bool sourceMuted: !!source?.audio?.muted readonly property real sourceVolume: source?.audio?.volume ?? 0 + readonly property alias cava: cava + readonly property alias beatTracker: beatTracker + function setVolume(newVolume: real): void { if (sink?.ready && sink?.audio) { sink.audio.muted = false; @@ -73,4 +77,21 @@ Singleton { PwObjectTracker { objects: [...root.sinks, ...root.sources] } + + AudioCollector { + id: collector + } + + CavaProvider { + id: cava + + collector: collector + bars: Config.services.visualiserBars + } + + BeatTracker { + id: beatTracker + + collector: collector + } } diff --git a/services/Cava.qml b/services/Cava.qml deleted file mode 100644 index 3291bcd..0000000 --- a/services/Cava.qml +++ /dev/null @@ -1,18 +0,0 @@ -pragma Singleton - -import qs.config -import Caelestia -import Quickshell - -Singleton { - id: root - - readonly property alias provider: provider - readonly property alias values: provider.values - - CavaProvider { - id: provider - - bars: Config.services.visualiserBars - } -} -- cgit v1.2.3-freya