plugin: fix crashes

Fixes #629
This commit is contained in:
2 * r + 2 * t 2025-09-16 23:58:44 +10:00
parent 172c1a047d
commit 1984458565
12 changed files with 108 additions and 295 deletions

View file

@ -35,20 +35,20 @@ PipeWireWorker::PipeWireWorker(std::stop_token token, AudioCollector* collector)
auto props = pw_properties_new(
PW_KEY_MEDIA_TYPE, "Audio", PW_KEY_MEDIA_CATEGORY, "Capture", PW_KEY_MEDIA_ROLE, "Music", nullptr);
pw_properties_set(props, PW_KEY_STREAM_CAPTURE_SINK, "true");
pw_properties_setf(props, PW_KEY_NODE_LATENCY, "%u/%u", nextPowerOf2(512 * collector->sampleRate() / 48000),
collector->sampleRate());
pw_properties_setf(
props, PW_KEY_NODE_LATENCY, "%u/%u", nextPowerOf2(512 * ac::SAMPLE_RATE / 48000), ac::SAMPLE_RATE);
pw_properties_set(props, PW_KEY_NODE_PASSIVE, "true");
pw_properties_set(props, PW_KEY_NODE_VIRTUAL, "true");
pw_properties_set(props, PW_KEY_STREAM_DONT_REMIX, "false");
pw_properties_set(props, "channelmix.upmix", "true");
std::vector<uint8_t> buffer(collector->chunkSize());
std::vector<uint8_t> buffer(ac::CHUNK_SIZE);
spa_pod_builder b;
spa_pod_builder_init(&b, buffer.data(), static_cast<quint32>(buffer.size()));
spa_audio_info_raw info{};
info.format = SPA_AUDIO_FORMAT_S16;
info.rate = collector->sampleRate();
info.rate = ac::SAMPLE_RATE;
info.channels = 1;
const spa_pod* params[1];
@ -66,7 +66,7 @@ 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);
const int success = pw_stream_connect(m_stream, PW_DIRECTION_INPUT, collector->nodeId(),
const int success = pw_stream_connect(m_stream, PW_DIRECTION_INPUT, PW_ID_ANY,
static_cast<pw_stream_flags>(
PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS | PW_STREAM_FLAG_RT_PROCESS),
params, 1);
@ -162,49 +162,9 @@ unsigned int PipeWireWorker::nextPowerOf2(unsigned int n) {
return n;
}
AudioCollector::AudioCollector(QObject* parent)
: Service(parent)
, 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) {}
AudioCollector::~AudioCollector() {
stop();
}
quint32 AudioCollector::sampleRate() const {
return m_sampleRate;
}
quint32 AudioCollector::chunkSize() const {
return m_chunkSize;
}
quint32 AudioCollector::nodeId() {
QMutexLocker locker(&m_nodeIdMutex);
return m_nodeId;
}
void AudioCollector::setNodeId(quint32 nodeId) {
{
QMutexLocker locker(&m_nodeIdMutex);
if (nodeId == m_nodeId) {
return;
}
m_nodeId = nodeId;
}
emit nodeIdChanged();
if (m_thread.joinable()) {
stop();
start();
}
AudioCollector& AudioCollector::instance() {
static AudioCollector instance;
return instance;
}
void AudioCollector::clearBuffer() {
@ -216,8 +176,8 @@ void AudioCollector::clearBuffer() {
}
void AudioCollector::loadChunk(const qint16* samples, quint32 count) {
if (count > m_chunkSize) {
count = m_chunkSize;
if (count > ac::CHUNK_SIZE) {
count = ac::CHUNK_SIZE;
}
auto* writeBuffer = m_writeBuffer.load(std::memory_order_relaxed);
@ -230,8 +190,8 @@ void AudioCollector::loadChunk(const qint16* samples, quint32 count) {
}
quint32 AudioCollector::readChunk(float* out, quint32 count) {
if (count == 0 || count > m_chunkSize) {
count = m_chunkSize;
if (count == 0 || count > ac::CHUNK_SIZE) {
count = ac::CHUNK_SIZE;
}
auto* readBuffer = m_readBuffer.load(std::memory_order_acquire);
@ -241,8 +201,8 @@ quint32 AudioCollector::readChunk(float* out, quint32 count) {
}
quint32 AudioCollector::readChunk(double* out, quint32 count) {
if (count == 0 || count > m_chunkSize) {
count = m_chunkSize;
if (count == 0 || count > ac::CHUNK_SIZE) {
count = ac::CHUNK_SIZE;
}
auto* readBuffer = m_readBuffer.load(std::memory_order_acquire);
@ -253,6 +213,17 @@ quint32 AudioCollector::readChunk(double* out, quint32 count) {
return count;
}
AudioCollector::AudioCollector(QObject* parent)
: Service(parent)
, m_buffer1(ac::CHUNK_SIZE)
, m_buffer2(ac::CHUNK_SIZE)
, m_readBuffer(&m_buffer1)
, m_writeBuffer(&m_buffer2) {}
AudioCollector::~AudioCollector() {
stop();
}
void AudioCollector::start() {
if (m_thread.joinable()) {
return;

View file

@ -12,6 +12,13 @@
namespace caelestia {
namespace ac {
constexpr quint32 SAMPLE_RATE = 44100;
constexpr quint32 CHUNK_SIZE = 512;
} // namespace ac
class AudioCollector;
class PipeWireWorker {
@ -29,47 +36,30 @@ private:
std::stop_token m_token;
AudioCollector* m_collector;
void cleanup();
static void handleTimeout(void* data, uint64_t expirations);
void streamStateChanged(pw_stream_state state);
void processStream();
void processSamples(const qint16* samples, quint32 count);
[[nodiscard]] unsigned int nextPowerOf2(unsigned int n);
};
class AudioCollector : public Service {
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(quint32 nodeId READ nodeId WRITE setNodeId NOTIFY nodeIdChanged)
public:
explicit AudioCollector(QObject* parent = nullptr);
~AudioCollector();
AudioCollector(const AudioCollector&) = delete;
AudioCollector& operator=(const AudioCollector&) = delete;
[[nodiscard]] quint32 sampleRate() const;
[[nodiscard]] quint32 chunkSize() const;
[[nodiscard]] quint32 nodeId();
void setNodeId(quint32 nodeId);
static AudioCollector& instance();
void clearBuffer();
void loadChunk(const qint16* samples, quint32 count);
quint32 readChunk(float* out, quint32 count = 0);
quint32 readChunk(double* out, quint32 count = 0);
signals:
void sampleRateChanged();
void chunkSizeChanged();
void nodeIdChanged();
private:
const quint32 m_sampleRate;
const quint32 m_chunkSize;
quint32 m_nodeId;
QMutex m_nodeIdMutex;
explicit AudioCollector(QObject* parent = nullptr);
~AudioCollector();
std::jthread m_thread;
std::vector<float> m_buffer1;

View file

@ -7,9 +7,8 @@
namespace caelestia {
AudioProcessor::AudioProcessor(AudioCollector* collector, QObject* parent)
: QObject(parent)
, m_collector(collector) {}
AudioProcessor::AudioProcessor(QObject* parent)
: QObject(parent) {}
AudioProcessor::~AudioProcessor() {
stop();
@ -17,53 +16,26 @@ AudioProcessor::~AudioProcessor() {
void AudioProcessor::init() {
m_timer = new QTimer(this);
if (m_collector) {
m_timer->setInterval(static_cast<int>(m_collector->chunkSize() * 1000.0 / m_collector->sampleRate()));
}
m_timer->setInterval(static_cast<int>(ac::CHUNK_SIZE * 1000.0 / ac::SAMPLE_RATE));
connect(m_timer, &QTimer::timeout, this, &AudioProcessor::process);
}
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<int>(collector->chunkSize() * 1000.0 / collector->sampleRate()));
} else {
m_timer->stop();
}
}
m_collector = collector;
}
void AudioProcessor::start() {
if (m_timer && m_collector) {
m_collector->ref();
QMetaObject::invokeMethod(&AudioCollector::instance(), &AudioCollector::ref);
if (m_timer) {
m_timer->start();
}
}
void AudioProcessor::stop() {
if (m_timer && m_collector) {
if (m_timer) {
m_timer->stop();
m_collector->unref();
}
QMetaObject::invokeMethod(&AudioCollector::instance(), &AudioCollector::unref);
}
AudioProvider::AudioProvider(QObject* parent)
: Service(parent)
, m_collector(nullptr)
, m_processor(nullptr)
, m_thread(nullptr) {}
@ -74,23 +46,6 @@ 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";
@ -109,13 +64,14 @@ void AudioProvider::init() {
void AudioProvider::start() {
if (m_processor) {
QMetaObject::invokeMethod(m_processor, "start", Qt::QueuedConnection);
AudioCollector::instance(); // Create instance on main thread
QMetaObject::invokeMethod(m_processor, &AudioProcessor::start);
}
}
void AudioProvider::stop() {
if (m_processor) {
QMetaObject::invokeMethod(m_processor, "stop", Qt::QueuedConnection);
QMetaObject::invokeMethod(m_processor, &AudioProcessor::stop);
}
}

View file

@ -1,6 +1,5 @@
#pragma once
#include "audiocollector.hpp"
#include "service.hpp"
#include <qqmlintegration.h>
#include <qtimer.h>
@ -11,42 +10,30 @@ class AudioProcessor : public QObject {
Q_OBJECT
public:
explicit AudioProcessor(AudioCollector* collector, QObject* parent = nullptr);
explicit AudioProcessor(QObject* parent = nullptr);
~AudioProcessor();
void init();
protected:
AudioCollector* m_collector;
public slots:
void start();
void stop();
Q_INVOKABLE virtual void setCollector(AudioCollector* collector);
protected:
virtual void process() = 0;
private:
QTimer* m_timer;
Q_INVOKABLE void start();
Q_INVOKABLE void stop();
virtual void process() = 0;
};
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();

View file

@ -6,16 +6,11 @@
namespace caelestia {
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(QObject* parent)
: AudioProcessor(parent)
, m_tempo(new_aubio_tempo("default", 1024, ac::CHUNK_SIZE, ac::SAMPLE_RATE))
, m_in(new_fvec(ac::CHUNK_SIZE))
, m_out(new_fvec(2)) {};
BeatProcessor::~BeatProcessor() {
if (m_tempo) {
@ -27,31 +22,12 @@ BeatProcessor::~BeatProcessor() {
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() {
if (!m_collector || !m_tempo || !m_in) {
if (!m_tempo || !m_in) {
return;
}
m_collector->readChunk(m_in->data);
AudioCollector::instance().readChunk(m_in->data);
aubio_tempo_do(m_tempo, m_in, m_out);
if (!qFuzzyIsNull(m_out->data[0])) {
@ -62,7 +38,7 @@ void BeatProcessor::process() {
BeatTracker::BeatTracker(QObject* parent)
: AudioProvider(parent)
, m_bpm(120) {
m_processor = new BeatProcessor(m_collector);
m_processor = new BeatProcessor();
init();
connect(static_cast<BeatProcessor*>(m_processor), &BeatProcessor::beat, this, &BeatTracker::updateBpm);

View file

@ -1,6 +1,5 @@
#pragma once
#include "audiocollector.hpp"
#include "audioprovider.hpp"
#include <aubio/aubio.h>
#include <qqmlintegration.h>
@ -11,21 +10,19 @@ class BeatProcessor : public AudioProcessor {
Q_OBJECT
public:
explicit BeatProcessor(AudioCollector* collector, QObject* parent = nullptr);
explicit BeatProcessor(QObject* parent = nullptr);
~BeatProcessor();
signals:
void beat(smpl_t bpm);
protected:
void setCollector(AudioCollector* collector) override;
void process() override;
private:
aubio_tempo_t* m_tempo;
fvec_t* m_in;
fvec_t* m_out;
void process() override;
};
class BeatTracker : public AudioProvider {

View file

@ -9,38 +9,45 @@
namespace caelestia {
CavaProcessor::CavaProcessor(AudioCollector* collector, QObject* parent)
: AudioProcessor(collector, parent)
CavaProcessor::CavaProcessor(QObject* parent)
: AudioProcessor(parent)
, m_plan(nullptr)
, m_in(nullptr)
, m_in(new double[ac::CHUNK_SIZE])
, m_out(nullptr)
, m_bars(0) {
if (collector) {
m_in = new double[collector->chunkSize()];
}
};
, m_bars(0) {};
CavaProcessor::~CavaProcessor() {
cleanup();
if (m_in) {
delete[] m_in;
}
delete[] m_in;
}
void CavaProcessor::setCollector(AudioCollector* collector) {
AudioProcessor::setCollector(collector);
if (m_in) {
delete[] m_in;
void CavaProcessor::process() {
if (!m_plan || m_bars == 0 || !m_out) {
return;
}
if (collector) {
m_in = new double[collector->chunkSize()];
} else {
m_in = nullptr;
const int count = static_cast<int>(AudioCollector::instance().readChunk(m_in));
// Process in data via cava
cava_execute(m_in, count, m_out, m_plan);
// Apply monstercat filter
for (int i = 0; i < m_bars; i++) {
for (int j = i - 1; j >= 0; j--) {
m_out[j] = std::max(m_out[i] / std::pow(1.5, i - j), m_out[j]);
}
for (int j = i + 1; j < m_bars; j++) {
m_out[j] = std::max(m_out[i] / std::pow(1.5, j - i), m_out[j]);
}
}
reload();
// Update values
QVector<double> values(m_bars);
std::copy(m_out, m_out + m_bars, values.begin());
if (values != m_values) {
m_values = std::move(values);
emit valuesChanged(m_values);
}
}
void CavaProcessor::setBars(int bars) {
@ -73,11 +80,11 @@ void CavaProcessor::cleanup() {
}
void CavaProcessor::initCava() {
if (m_plan || m_bars == 0 || !m_collector) {
if (m_plan || m_bars == 0) {
return;
}
m_plan = cava_init(m_bars, m_collector->sampleRate(), 1, 1, 0.85, 50, 10000);
m_plan = cava_init(m_bars, ac::SAMPLE_RATE, 1, 1, 0.85, 50, 10000);
if (m_plan->status == -1) {
qWarning() << "CavaProcessor::initCava: failed to initialise cava plan";
@ -88,40 +95,11 @@ void CavaProcessor::initCava() {
m_out = new double[static_cast<size_t>(m_bars)];
}
void CavaProcessor::process() {
if (!m_plan || m_bars == 0 || !m_collector || !m_in || !m_out) {
return;
}
const int count = static_cast<int>(m_collector->readChunk(m_in));
// Process in data via cava
cava_execute(m_in, count, m_out, m_plan);
// Apply monstercat filter
for (int i = 0; i < m_bars; i++) {
for (int j = i - 1; j >= 0; j--) {
m_out[j] = std::max(m_out[i] / std::pow(1.5, i - j), m_out[j]);
}
for (int j = i + 1; j < m_bars; j++) {
m_out[j] = std::max(m_out[i] / std::pow(1.5, j - i), m_out[j]);
}
}
// Update values
QVector<double> values(m_bars);
std::copy(m_out, m_out + m_bars, values.begin());
if (values != m_values) {
m_values = std::move(values);
emit valuesChanged(m_values);
}
}
CavaProvider::CavaProvider(QObject* parent)
: AudioProvider(parent)
, m_bars(0)
, m_values(m_bars, 0.0) {
m_processor = new CavaProcessor(m_collector);
m_processor = new CavaProcessor();
init();
connect(static_cast<CavaProcessor*>(m_processor), &CavaProcessor::valuesChanged, this, &CavaProvider::updateValues);

View file

@ -1,6 +1,5 @@
#pragma once
#include "audiocollector.hpp"
#include "audioprovider.hpp"
#include <cava/cavacore.h>
#include <qqmlintegration.h>
@ -11,14 +10,14 @@ class CavaProcessor : public AudioProcessor {
Q_OBJECT
public:
explicit CavaProcessor(AudioCollector* collector, QObject* parent = nullptr);
explicit CavaProcessor(QObject* parent = nullptr);
~CavaProcessor();
signals:
void valuesChanged(QVector<double> values);
protected:
void setCollector(AudioCollector* collector) override;
void process() override;
private:
struct cava_plan* m_plan;
@ -33,8 +32,6 @@ private:
void reload();
void initCava();
void cleanup();
void process() override;
};
class CavaProvider : public AudioProvider {

View file

@ -10,60 +10,28 @@ Service::Service(QObject* parent)
, m_refCount(0) {}
int Service::refCount() const {
QMutexLocker locker(&m_mutex);
return m_refCount;
}
void Service::ref() {
bool needsStart = false;
{
QMutexLocker locker(&m_mutex);
if (m_refCount == 0) {
needsStart = true;
}
m_refCount++;
if (m_refCount == 0) {
start();
}
m_refCount++;
emit refCountChanged();
if (needsStart) {
const QPointer<Service> self(this);
QMetaObject::invokeMethod(
this,
[self]() {
if (self) {
self->start();
}
},
Qt::QueuedConnection);
}
}
void Service::unref() {
bool needsStop = false;
{
QMutexLocker locker(&m_mutex);
if (m_refCount == 0) {
return;
}
m_refCount--;
if (m_refCount == 0) {
needsStop = true;
}
if (m_refCount == 0) {
return;
}
m_refCount--;
emit refCountChanged();
if (needsStop) {
const QPointer<Service> self(this);
QMetaObject::invokeMethod(
this,
[self]() {
if (self) {
self->stop();
}
},
Qt::QueuedConnection);
if (m_refCount == 0) {
stop();
}
}

View file

@ -23,7 +23,6 @@ signals:
private:
int m_refCount;
mutable QMutex m_mutex;
virtual void start() = 0;
virtual void stop() = 0;

View file

@ -1,6 +1,7 @@
#pragma once
#include "service.hpp"
#include <qpointer.h>
#include <qqmlintegration.h>
namespace caelestia {
@ -22,7 +23,7 @@ signals:
void serviceChanged();
private:
Service* m_service;
QPointer<Service> m_service;
};
} // namespace caelestia

View file

@ -78,20 +78,13 @@ Singleton {
objects: [...root.sinks, ...root.sources]
}
AudioCollector {
id: collector
}
CavaProvider {
id: cava
collector: collector
bars: Config.services.visualiserBars
}
BeatTracker {
id: beatTracker
collector: collector
}
}