summaryrefslogtreecommitdiff
path: root/plugin/src
diff options
context:
space:
mode:
Diffstat (limited to 'plugin/src')
-rw-r--r--plugin/src/Caelestia/CMakeLists.txt20
-rw-r--r--plugin/src/Caelestia/audiocollector.cpp252
-rw-r--r--plugin/src/Caelestia/audiocollector.hpp77
-rw-r--r--plugin/src/Caelestia/audioprovider.cpp79
-rw-r--r--plugin/src/Caelestia/audioprovider.hpp51
-rw-r--r--plugin/src/Caelestia/beattracker.cpp110
-rw-r--r--plugin/src/Caelestia/beattracker.hpp46
-rw-r--r--plugin/src/Caelestia/cachingimagemanager.cpp19
-rw-r--r--plugin/src/Caelestia/cachingimagemanager.hpp8
-rw-r--r--plugin/src/Caelestia/cavaprovider.cpp143
-rw-r--r--plugin/src/Caelestia/cavaprovider.hpp63
-rw-r--r--plugin/src/Caelestia/cutils.cpp13
-rw-r--r--plugin/src/Caelestia/cutils.hpp7
-rw-r--r--plugin/src/Caelestia/filesystemmodel.cpp12
-rw-r--r--plugin/src/Caelestia/filesystemmodel.hpp15
-rw-r--r--plugin/src/Caelestia/qalculator.cpp1
-rw-r--r--plugin/src/Caelestia/qalculator.hpp2
-rw-r--r--plugin/src/Caelestia/service.cpp54
-rw-r--r--plugin/src/Caelestia/service.hpp31
-rw-r--r--plugin/src/Caelestia/serviceref.cpp42
-rw-r--r--plugin/src/Caelestia/serviceref.hpp28
21 files changed, 913 insertions, 160 deletions
diff --git a/plugin/src/Caelestia/CMakeLists.txt b/plugin/src/Caelestia/CMakeLists.txt
index cc8e567..bff65c5 100644
--- a/plugin/src/Caelestia/CMakeLists.txt
+++ b/plugin/src/Caelestia/CMakeLists.txt
@@ -1,16 +1,23 @@
find_package(PkgConfig REQUIRED)
-pkg_check_modules(QALCULATE REQUIRED libqalculate)
-pkg_check_modules(AUBIO REQUIRED aubio)
+pkg_check_modules(Qalculate IMPORTED_TARGET libqalculate REQUIRED)
+pkg_check_modules(Pipewire IMPORTED_TARGET libpipewire-0.3 REQUIRED)
+pkg_check_modules(Aubio IMPORTED_TARGET aubio REQUIRED)
+pkg_check_modules(Cava IMPORTED_TARGET cava REQUIRED)
qt_add_qml_module(caelestia
URI Caelestia
- VERSION 0.1
+ VERSION ${VERSION_SHORT}
SOURCES
cutils.hpp cutils.cpp
cachingimagemanager.hpp cachingimagemanager.cpp
filesystemmodel.hpp filesystemmodel.cpp
qalculator.hpp qalculator.cpp
beattracker.hpp beattracker.cpp
+ service.hpp service.cpp
+ serviceref.hpp serviceref.cpp
+ audiocollector.hpp audiocollector.cpp
+ audioprovider.hpp audioprovider.cpp
+ cavaprovider.hpp cavaprovider.cpp
)
qt_query_qml_module(caelestia
@@ -20,18 +27,17 @@ qt_query_qml_module(caelestia
TARGET_PATH module_target_path
QMLDIR module_qmldir
TYPEINFO module_typeinfo
- QML_FILES module_qml_files
- RESOURCES module_resources
)
+message(STATUS "Created QML module ${module_uri}, version ${module_version}")
+
set(module_dir "${INSTALL_QMLDIR}/${module_target_path}")
install(TARGETS caelestia LIBRARY DESTINATION "${module_dir}" RUNTIME DESTINATION "${module_dir}")
install(TARGETS "${module_plugin_target}" LIBRARY DESTINATION "${module_dir}" RUNTIME DESTINATION "${module_dir}")
install(FILES "${module_qmldir}" DESTINATION "${module_dir}")
install(FILES "${module_typeinfo}" DESTINATION "${module_dir}")
-target_include_directories(caelestia SYSTEM PRIVATE ${QALCULATE_INCLUDE_DIRS} ${AUBIO_INCLUDE_DIRS})
target_link_libraries(caelestia PRIVATE
Qt::Core Qt::Qml Qt::Gui Qt::Concurrent Qt::Multimedia
- ${QALCULATE_LIBRARIES} ${AUBIO_LIBRARIES}
+ PkgConfig::Qalculate PkgConfig::Pipewire PkgConfig::Aubio PkgConfig::Cava
)
diff --git a/plugin/src/Caelestia/audiocollector.cpp b/plugin/src/Caelestia/audiocollector.cpp
new file mode 100644
index 0000000..b9dfc23
--- /dev/null
+++ b/plugin/src/Caelestia/audiocollector.cpp
@@ -0,0 +1,252 @@
+#include "audiocollector.hpp"
+
+#include "service.hpp"
+#include <algorithm>
+#include <cstdint>
+#include <mutex>
+#include <pipewire/pipewire.h>
+#include <qdebug.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/param/latency-utils.h>
+#include <stop_token>
+#include <vector>
+
+namespace caelestia {
+
+PipeWireWorker::PipeWireWorker(std::stop_token token, AudioCollector* collector)
+ : m_loop(nullptr)
+ , m_stream(nullptr)
+ , m_timer(nullptr)
+ , m_idle(true)
+ , m_token(token)
+ , m_collector(collector) {
+ pw_init(nullptr, nullptr);
+
+ m_loop = pw_main_loop_new(nullptr);
+ if (!m_loop) {
+ qWarning() << "PipeWireWorker::init: failed to create PipeWire main loop";
+ return;
+ }
+
+ timespec timeout = { 0, 10 * SPA_NSEC_PER_MSEC };
+ m_timer = pw_loop_add_timer(pw_main_loop_get_loop(m_loop), handleTimeout, this);
+ pw_loop_update_timer(pw_main_loop_get_loop(m_loop), m_timer, &timeout, &timeout, false);
+
+ 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_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());
+ spa_pod_builder b;
+ spa_pod_builder_init(&b, buffer.data(), static_cast<uint32_t>(buffer.size()));
+
+ spa_audio_info_raw info{};
+ info.format = SPA_AUDIO_FORMAT_S16;
+ info.rate = collector->sampleRate();
+ info.channels = 1;
+
+ const spa_pod* params[1];
+ params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, &info);
+
+ pw_stream_events events{};
+ events.state_changed = [](void* data, pw_stream_state, pw_stream_state state, const char*) {
+ auto* self = static_cast<PipeWireWorker*>(data);
+ self->streamStateChanged(state);
+ };
+ events.process = [](void* data) {
+ auto* self = static_cast<PipeWireWorker*>(data);
+ self->processStream();
+ };
+
+ 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,
+ static_cast<pw_stream_flags>(
+ PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS | PW_STREAM_FLAG_RT_PROCESS),
+ params, 1);
+
+ pw_main_loop_run(m_loop);
+
+ pw_stream_destroy(m_stream);
+ pw_main_loop_destroy(m_loop);
+ pw_deinit();
+}
+
+void PipeWireWorker::handleTimeout(void* data, uint64_t expirations) {
+ auto* self = static_cast<PipeWireWorker*>(data);
+
+ if (self->m_token.stop_requested()) {
+ pw_main_loop_quit(self->m_loop);
+ return;
+ }
+
+ if (!self->m_idle) {
+ if (expirations < 10) {
+ self->m_collector->clearBuffer();
+ } else {
+ self->m_idle = true;
+ timespec timeout = { 0, 500 * SPA_NSEC_PER_MSEC };
+ pw_loop_update_timer(pw_main_loop_get_loop(self->m_loop), self->m_timer, &timeout, &timeout, false);
+ }
+ }
+}
+
+void PipeWireWorker::streamStateChanged(pw_stream_state state) {
+ m_idle = false;
+ switch (state) {
+ case PW_STREAM_STATE_PAUSED: {
+ timespec timeout = { 0, 10 * SPA_NSEC_PER_MSEC };
+ pw_loop_update_timer(pw_main_loop_get_loop(m_loop), m_timer, &timeout, &timeout, false);
+ break;
+ }
+ case PW_STREAM_STATE_STREAMING:
+ pw_loop_update_timer(pw_main_loop_get_loop(m_loop), m_timer, nullptr, nullptr, false);
+ break;
+ case PW_STREAM_STATE_ERROR:
+ pw_main_loop_quit(m_loop);
+ break;
+ default:
+ break;
+ }
+}
+
+void PipeWireWorker::processStream() {
+ if (m_token.stop_requested()) {
+ pw_main_loop_quit(m_loop);
+ return;
+ }
+
+ pw_buffer* buffer = pw_stream_dequeue_buffer(m_stream);
+ if (buffer == nullptr) {
+ return;
+ }
+
+ const spa_buffer* buf = buffer->buffer;
+ const int16_t* samples = reinterpret_cast<const int16_t*>(buf->datas[0].data);
+ if (samples == nullptr) {
+ return;
+ }
+
+ const uint32_t count = buf->datas[0].chunk->size / 2;
+ m_collector->loadChunk(samples, count);
+
+ pw_stream_queue_buffer(m_stream, buffer);
+}
+
+unsigned int PipeWireWorker::nextPowerOf2(unsigned int n) {
+ if (n == 0) {
+ return 1;
+ }
+
+ n--;
+ n |= n >> 1;
+ n |= n >> 2;
+ n |= n >> 4;
+ n |= n >> 8;
+ n |= n >> 16;
+ n++;
+
+ return n;
+}
+
+AudioCollector::AudioCollector(uint32_t sampleRate, uint32_t chunkSize, QObject* parent)
+ : Service(parent)
+ , m_buffer1(chunkSize)
+ , m_buffer2(chunkSize)
+ , m_readBuffer(&m_buffer1)
+ , m_writeBuffer(&m_buffer2)
+ , m_sampleRate(sampleRate)
+ , m_chunkSize(chunkSize) {}
+
+AudioCollector::~AudioCollector() {
+ stop();
+}
+
+AudioCollector* AudioCollector::instance() {
+ std::lock_guard<std::mutex> lock(s_mutex);
+ if (s_instance == nullptr) {
+ s_instance = new AudioCollector();
+ }
+ return s_instance;
+}
+
+uint32_t AudioCollector::sampleRate() const {
+ return m_sampleRate;
+}
+
+uint32_t AudioCollector::chunkSize() const {
+ return m_chunkSize;
+}
+
+void AudioCollector::clearBuffer() {
+ auto* writeBuffer = m_writeBuffer.load(std::memory_order_relaxed);
+ std::fill(writeBuffer->begin(), writeBuffer->end(), 0.0f);
+
+ auto* oldRead = m_readBuffer.exchange(writeBuffer, std::memory_order_acq_rel);
+ m_writeBuffer.store(oldRead, std::memory_order_release);
+}
+
+void AudioCollector::loadChunk(const int16_t* samples, uint32_t count) {
+ if (count > m_chunkSize) {
+ count = m_chunkSize;
+ }
+
+ auto* writeBuffer = m_writeBuffer.load(std::memory_order_relaxed);
+ std::transform(samples, samples + count, writeBuffer->begin(), [](int16_t sample) {
+ return sample / 32768.0f;
+ });
+
+ auto* oldRead = m_readBuffer.exchange(writeBuffer, std::memory_order_acq_rel);
+ m_writeBuffer.store(oldRead, std::memory_order_release);
+}
+
+uint32_t AudioCollector::readChunk(float* out, uint32_t count) {
+ if (count == 0 || count > m_chunkSize) {
+ count = m_chunkSize;
+ }
+
+ auto* readBuffer = m_readBuffer.load(std::memory_order_acquire);
+ std::memcpy(out, readBuffer->data(), count * sizeof(float));
+
+ return count;
+}
+
+uint32_t AudioCollector::readChunk(double* out, uint32_t count) {
+ if (count == 0 || count > m_chunkSize) {
+ count = m_chunkSize;
+ }
+
+ auto* readBuffer = m_readBuffer.load(std::memory_order_acquire);
+ std::transform(readBuffer->begin(), readBuffer->begin() + count, out, [](float sample) {
+ return static_cast<double>(sample);
+ });
+
+ return count;
+}
+
+void AudioCollector::start() {
+ if (m_thread.joinable()) {
+ return;
+ }
+
+ clearBuffer();
+
+ m_thread = std::jthread([this](std::stop_token token) {
+ PipeWireWorker worker(token, this);
+ });
+}
+
+void AudioCollector::stop() {
+ if (m_thread.joinable()) {
+ m_thread.request_stop();
+ m_thread.join();
+ }
+}
+
+} // namespace caelestia
diff --git a/plugin/src/Caelestia/audiocollector.hpp b/plugin/src/Caelestia/audiocollector.hpp
new file mode 100644
index 0000000..283b758
--- /dev/null
+++ b/plugin/src/Caelestia/audiocollector.hpp
@@ -0,0 +1,77 @@
+#pragma once
+
+#include "service.hpp"
+#include <atomic>
+#include <cstdint>
+#include <mutex>
+#include <pipewire/pipewire.h>
+#include <spa/param/audio/format-utils.h>
+#include <stop_token>
+#include <thread>
+#include <vector>
+
+namespace caelestia {
+
+class AudioCollector;
+
+class PipeWireWorker {
+public:
+ explicit PipeWireWorker(std::stop_token token, AudioCollector* collector);
+
+ void run();
+
+private:
+ pw_main_loop* m_loop;
+ pw_stream* m_stream;
+ spa_source* m_timer;
+ bool m_idle;
+
+ 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 int16_t* samples, uint32_t count);
+
+ [[nodiscard]] unsigned int nextPowerOf2(unsigned int n);
+};
+
+class AudioCollector : public Service {
+ Q_OBJECT
+
+public:
+ explicit AudioCollector(uint32_t sampleRate = 44100, uint32_t chunkSize = 512, QObject* parent = nullptr);
+ ~AudioCollector();
+
+ static AudioCollector* instance();
+
+ [[nodiscard]] uint32_t sampleRate() const;
+ [[nodiscard]] uint32_t chunkSize() const;
+
+ 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);
+
+private:
+ inline static AudioCollector* s_instance = nullptr;
+ inline static std::mutex s_mutex;
+
+ std::jthread m_thread;
+ std::vector<float> m_buffer1;
+ std::vector<float> m_buffer2;
+ std::atomic<std::vector<float>*> m_readBuffer;
+ std::atomic<std::vector<float>*> m_writeBuffer;
+ uint32_t m_sampleCount;
+
+ const uint32_t m_sampleRate;
+ const uint32_t m_chunkSize;
+
+ void start() override;
+ void stop() override;
+};
+
+} // namespace caelestia
diff --git a/plugin/src/Caelestia/audioprovider.cpp b/plugin/src/Caelestia/audioprovider.cpp
new file mode 100644
index 0000000..4188460
--- /dev/null
+++ b/plugin/src/Caelestia/audioprovider.cpp
@@ -0,0 +1,79 @@
+#include "audioprovider.hpp"
+
+#include "audiocollector.hpp"
+#include "service.hpp"
+#include <qdebug.h>
+#include <qthread.h>
+
+namespace caelestia {
+
+AudioProcessor::AudioProcessor(QObject* parent)
+ : QObject(parent)
+ , m_sampleRate(AudioCollector::instance()->sampleRate())
+ , m_chunkSize(AudioCollector::instance()->chunkSize()) {}
+
+AudioProcessor::~AudioProcessor() {
+ stop();
+}
+
+void AudioProcessor::init() {
+ m_timer = new QTimer(this);
+ m_timer->setInterval(static_cast<int>(m_chunkSize * 1000.0 / m_sampleRate));
+ connect(m_timer, &QTimer::timeout, this, &AudioProcessor::process);
+}
+
+void AudioProcessor::start() {
+ AudioCollector::instance()->ref();
+ if (m_timer) {
+ m_timer->start();
+ }
+}
+
+void AudioProcessor::stop() {
+ if (m_timer) {
+ m_timer->stop();
+ }
+ AudioCollector::instance()->unref();
+}
+
+AudioProvider::AudioProvider(QObject* parent)
+ : Service(parent)
+ , m_processor(nullptr)
+ , m_thread(nullptr) {}
+
+AudioProvider::~AudioProvider() {
+ if (m_thread) {
+ m_thread->quit();
+ m_thread->wait();
+ }
+}
+
+void AudioProvider::init() {
+ if (!m_processor) {
+ qWarning() << "AudioProvider::init: attempted to init with no processor set";
+ return;
+ }
+
+ m_thread = new QThread(this);
+ m_processor->moveToThread(m_thread);
+
+ connect(m_thread, &QThread::started, m_processor, &AudioProcessor::init);
+ connect(m_thread, &QThread::finished, m_processor, &AudioProcessor::deleteLater);
+ connect(m_thread, &QThread::finished, m_thread, &QThread::deleteLater);
+
+ m_thread->start();
+}
+
+void AudioProvider::start() {
+ if (m_processor) {
+ QMetaObject::invokeMethod(m_processor, "start", Qt::QueuedConnection);
+ }
+}
+
+void AudioProvider::stop() {
+ if (m_processor) {
+ QMetaObject::invokeMethod(m_processor, "stop", Qt::QueuedConnection);
+ }
+}
+
+} // namespace caelestia
diff --git a/plugin/src/Caelestia/audioprovider.hpp b/plugin/src/Caelestia/audioprovider.hpp
new file mode 100644
index 0000000..aa7fb83
--- /dev/null
+++ b/plugin/src/Caelestia/audioprovider.hpp
@@ -0,0 +1,51 @@
+#pragma once
+
+#include "service.hpp"
+#include <cstdint>
+#include <qqmlintegration.h>
+#include <qtimer.h>
+
+namespace caelestia {
+
+class AudioProcessor : public QObject {
+ Q_OBJECT
+
+public:
+ explicit AudioProcessor(QObject* parent = nullptr);
+ ~AudioProcessor();
+
+ void init();
+
+protected:
+ uint32_t m_sampleRate;
+ uint32_t m_chunkSize;
+
+private:
+ QTimer* m_timer;
+
+ Q_INVOKABLE void start();
+ Q_INVOKABLE void stop();
+
+ virtual void process() = 0;
+};
+
+class AudioProvider : public Service {
+ Q_OBJECT
+
+public:
+ explicit AudioProvider(QObject* parent = nullptr);
+ ~AudioProvider();
+
+protected:
+ AudioProcessor* m_processor;
+
+ void init();
+
+private:
+ QThread* m_thread;
+
+ void start() override;
+ void stop() override;
+};
+
+} // namespace caelestia
diff --git a/plugin/src/Caelestia/beattracker.cpp b/plugin/src/Caelestia/beattracker.cpp
index 433ffab..a23d052 100644
--- a/plugin/src/Caelestia/beattracker.cpp
+++ b/plugin/src/Caelestia/beattracker.cpp
@@ -1,109 +1,49 @@
#include "beattracker.hpp"
-#include <QAudioSource>
-#include <QDebug>
-#include <QIODevice>
-#include <QMediaDevices>
-#include <QObject>
+#include "audiocollector.hpp"
+#include "audioprovider.hpp"
#include <aubio/aubio.h>
namespace caelestia {
-BeatTracker::BeatTracker(uint_t sampleRate, uint_t hopSize, QObject* parent)
- : QObject(parent)
- , m_tempo(new_aubio_tempo("default", 1024, hopSize, sampleRate))
- , m_in(new_fvec(hopSize))
- , m_out(new_fvec(2))
- , m_hopSize(hopSize)
- , m_bpm(120)
- , m_refCount(0) {
- QAudioFormat format;
- format.setSampleRate(static_cast<int>(sampleRate));
- format.setChannelCount(1);
- format.setSampleFormat(QAudioFormat::Int16);
+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)) {};
- m_source = new QAudioSource(QMediaDevices::defaultAudioInput(), format, this);
- connect(m_source, &QAudioSource::stateChanged, this, &BeatTracker::handleStateChanged);
-};
-
-BeatTracker::~BeatTracker() {
+BeatProcessor::~BeatProcessor() {
del_aubio_tempo(m_tempo);
del_fvec(m_in);
del_fvec(m_out);
-
- m_source->stop();
- delete m_source;
-}
-
-smpl_t BeatTracker::bpm() const {
- return m_bpm;
}
-int BeatTracker::refCount() const {
- return m_refCount;
-}
+void BeatProcessor::process() {
+ AudioCollector::instance()->readChunk(m_in->data, m_chunkSize);
-void BeatTracker::setRefCount(int refCount) {
- if (m_refCount == refCount) {
- return;
- }
-
- m_refCount = refCount;
- emit refCountChanged();
-
- if (m_refCount == 0) {
- stop();
- } else if (!m_device) {
- start();
+ aubio_tempo_do(m_tempo, m_in, m_out);
+ if (m_out->data[0] != 0.0f) {
+ emit beat(aubio_tempo_get_bpm(m_tempo));
}
}
-void BeatTracker::start() {
- m_device = m_source->start();
- connect(m_device, &QIODevice::readyRead, this, &BeatTracker::process);
-}
+BeatTracker::BeatTracker(QObject* parent)
+ : AudioProvider(parent)
+ , m_bpm(120) {
+ m_processor = new BeatProcessor();
+ init();
-void BeatTracker::stop() {
- m_source->stop();
- m_device = nullptr;
+ connect(static_cast<BeatProcessor*>(m_processor), &BeatProcessor::beat, this, &BeatTracker::updateBpm);
}
-void BeatTracker::process() {
- const QByteArray data = m_device->readAll();
- const int16_t* samples = reinterpret_cast<const int16_t*>(data.constData());
- const size_t count = static_cast<size_t>(data.size()) / sizeof(int16_t);
-
- for (size_t i = 0; i < count; ++i) {
- m_in->data[i % m_hopSize] = samples[i] / 32768.0f;
- if ((i + 1) % m_hopSize == 0) {
- aubio_tempo_do(m_tempo, m_in, m_out);
- if (m_out->data[0] != 0.0f) {
- m_bpm = aubio_tempo_get_bpm(m_tempo);
- emit bpmChanged();
- emit beat(m_bpm);
- }
- }
- }
+smpl_t BeatTracker::bpm() const {
+ return m_bpm;
}
-void BeatTracker::handleStateChanged(QtAudio::State state) const {
- if (state == QtAudio::StoppedState && m_source->error() != QtAudio::NoError) {
- switch (m_source->error()) {
- case QtAudio::OpenError:
- qWarning() << "BeatTracker: failed to open audio device";
- break;
- case QtAudio::IOError:
- qWarning() << "BeatTracker: an error occurred during read/write of audio device";
- break;
- case QtAudio::UnderrunError:
- qWarning() << "BeatTracker: audio data is not being fed to audio device fast enough";
- break;
- case QtAudio::FatalError:
- qCritical() << "BeatTracker: fatal error in audio device";
- break;
- default:
- break;
- }
+void BeatTracker::updateBpm(smpl_t bpm) {
+ if (!qFuzzyCompare(bpm + 1.0f, m_bpm + 1.0f)) {
+ m_bpm = bpm;
+ emit bpmChanged();
}
}
diff --git a/plugin/src/Caelestia/beattracker.hpp b/plugin/src/Caelestia/beattracker.hpp
index fda0ddc..c7737e1 100644
--- a/plugin/src/Caelestia/beattracker.hpp
+++ b/plugin/src/Caelestia/beattracker.hpp
@@ -1,51 +1,49 @@
#pragma once
-#include <QAudioSource>
-#include <QIODevice>
-#include <QObject>
+#include "audioprovider.hpp"
#include <aubio/aubio.h>
#include <qqmlintegration.h>
namespace caelestia {
-class BeatTracker : public QObject {
+class BeatProcessor : public AudioProcessor {
+ Q_OBJECT
+
+public:
+ explicit BeatProcessor(QObject* parent = nullptr);
+ ~BeatProcessor();
+
+signals:
+ void beat(smpl_t bpm);
+
+private:
+ aubio_tempo_t* m_tempo;
+ fvec_t* m_in;
+ fvec_t* m_out;
+
+ void process() override;
+};
+
+class BeatTracker : public AudioProvider {
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
Q_PROPERTY(smpl_t bpm READ bpm NOTIFY bpmChanged)
- Q_PROPERTY(int refCount READ refCount WRITE setRefCount NOTIFY refCountChanged)
public:
- explicit BeatTracker(uint_t sampleRate = 44100, uint_t hopSize = 512, QObject* parent = nullptr);
- ~BeatTracker();
+ explicit BeatTracker(QObject* parent = nullptr);
[[nodiscard]] smpl_t bpm() const;
- [[nodiscard]] int refCount() const;
- void setRefCount(int refCount);
-
signals:
void bpmChanged();
- void refCountChanged();
void beat(smpl_t bpm);
private:
- QAudioSource* m_source;
- QIODevice* m_device;
-
- aubio_tempo_t* m_tempo;
- fvec_t* m_in;
- fvec_t* m_out;
- uint_t m_hopSize;
-
smpl_t m_bpm;
- int m_refCount;
- void start();
- void stop();
- void process();
- void handleStateChanged(QtAudio::State state) const;
+ void updateBpm(smpl_t bpm);
};
} // namespace caelestia
diff --git a/plugin/src/Caelestia/cachingimagemanager.cpp b/plugin/src/Caelestia/cachingimagemanager.cpp
index f50e8a2..3394f89 100644
--- a/plugin/src/Caelestia/cachingimagemanager.cpp
+++ b/plugin/src/Caelestia/cachingimagemanager.cpp
@@ -1,16 +1,13 @@
#include "cachingimagemanager.hpp"
-#include <QCryptographicHash>
-#include <QDir>
-#include <QFile>
-#include <QFutureWatcher>
-#include <QImageReader>
-#include <QObject>
-#include <QPainter>
-#include <QThreadPool>
-#include <QtConcurrent>
-#include <QtQuick/QQuickItem>
-#include <QtQuick/QQuickWindow>
+#include <QtQuick/qquickwindow.h>
+#include <qcryptographichash.h>
+#include <qdir.h>
+#include <qfileinfo.h>
+#include <qfuturewatcher.h>
+#include <qimagereader.h>
+#include <qpainter.h>
+#include <qtconcurrentrun.h>
namespace caelestia {
diff --git a/plugin/src/Caelestia/cachingimagemanager.hpp b/plugin/src/Caelestia/cachingimagemanager.hpp
index 21ad8e8..f05ea34 100644
--- a/plugin/src/Caelestia/cachingimagemanager.hpp
+++ b/plugin/src/Caelestia/cachingimagemanager.hpp
@@ -1,8 +1,7 @@
#pragma once
-#include <QFuture>
-#include <QObject>
-#include <QtQuick/QQuickItem>
+#include <QtQuick/qquickitem.h>
+#include <qobject.h>
#include <qqmlintegration.h>
namespace caelestia {
@@ -19,7 +18,8 @@ class CachingImageManager : public QObject {
public:
explicit CachingImageManager(QObject* parent = nullptr)
- : QObject(parent) {}
+ : QObject(parent)
+ , m_item(nullptr) {}
[[nodiscard]] QQuickItem* item() const;
void setItem(QQuickItem* item);
diff --git a/plugin/src/Caelestia/cavaprovider.cpp b/plugin/src/Caelestia/cavaprovider.cpp
new file mode 100644
index 0000000..d053702
--- /dev/null
+++ b/plugin/src/Caelestia/cavaprovider.cpp
@@ -0,0 +1,143 @@
+#include "cavaprovider.hpp"
+
+#include "audiocollector.hpp"
+#include "audioprovider.hpp"
+#include <cava/cavacore.h>
+#include <cmath>
+#include <cstddef>
+#include <qdebug.h>
+
+namespace caelestia {
+
+CavaProcessor::CavaProcessor(QObject* parent)
+ : AudioProcessor(parent)
+ , m_plan(nullptr)
+ , m_in(new double[static_cast<size_t>(m_chunkSize)])
+ , m_out(nullptr)
+ , m_bars(0) {};
+
+CavaProcessor::~CavaProcessor() {
+ cleanup();
+ delete[] m_in;
+}
+
+void CavaProcessor::setBars(int bars) {
+ if (bars < 0) {
+ qWarning() << "CavaProcessor::setBars: bars must be greater than 0. Setting to 0.";
+ bars = 0;
+ }
+
+ if (m_bars != bars) {
+ m_bars = bars;
+ reload();
+ }
+}
+
+void CavaProcessor::reload() {
+ cleanup();
+ initCava();
+}
+
+void CavaProcessor::cleanup() {
+ if (!m_plan) {
+ return;
+ }
+
+ cava_destroy(m_plan);
+ m_plan = nullptr;
+
+ if (m_out) {
+ delete[] m_out;
+ m_out = nullptr;
+ }
+}
+
+void CavaProcessor::initCava() {
+ if (m_plan || m_bars == 0) {
+ return;
+ }
+
+ m_plan = cava_init(m_bars, static_cast<unsigned int>(m_sampleRate), 1, 1, 0.85, 50, 10000);
+
+ if (m_plan->status == -1) {
+ qWarning() << "CavaProcessor::initCava: failed to initialise cava plan";
+ cleanup();
+ return;
+ }
+
+ m_out = new double[static_cast<size_t>(m_bars)];
+}
+
+void CavaProcessor::process() {
+ if (!m_plan || m_bars == 0) {
+ return;
+ }
+
+ 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]);
+ }
+ }
+
+ // 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) {
+ m_processor = new CavaProcessor();
+ init();
+
+ connect(static_cast<CavaProcessor*>(m_processor), &CavaProcessor::valuesChanged, this, &CavaProvider::updateValues);
+}
+
+int CavaProvider::bars() const {
+ return m_bars;
+}
+
+void CavaProvider::setBars(int bars) {
+ if (bars < 0) {
+ qWarning() << "CavaProvider::setBars: bars must be greater than 0. Setting to 0.";
+ bars = 0;
+ }
+
+ if (m_bars == bars) {
+ return;
+ }
+
+ m_values.resize(bars);
+ m_bars = bars;
+ emit barsChanged();
+ emit valuesChanged();
+
+ QMetaObject::invokeMethod(m_processor, "setBars", Qt::QueuedConnection, Q_ARG(int, bars));
+}
+
+QVector<double> CavaProvider::values() const {
+ return m_values;
+}
+
+void CavaProvider::updateValues(QVector<double> values) {
+ if (values != m_values) {
+ m_values = values;
+ emit valuesChanged();
+ }
+}
+
+} // namespace caelestia
diff --git a/plugin/src/Caelestia/cavaprovider.hpp b/plugin/src/Caelestia/cavaprovider.hpp
new file mode 100644
index 0000000..131e166
--- /dev/null
+++ b/plugin/src/Caelestia/cavaprovider.hpp
@@ -0,0 +1,63 @@
+#pragma once
+
+#include "audioprovider.hpp"
+#include <cava/cavacore.h>
+#include <qqmlintegration.h>
+
+namespace caelestia {
+
+class CavaProcessor : public AudioProcessor {
+ Q_OBJECT
+
+public:
+ explicit CavaProcessor(QObject* parent = nullptr);
+ ~CavaProcessor();
+
+signals:
+ void valuesChanged(QVector<double> values);
+
+private:
+ struct cava_plan* m_plan;
+ double* m_in;
+ double* m_out;
+
+ int m_bars;
+ QVector<double> m_values;
+
+ Q_INVOKABLE void setBars(int bars);
+
+ void reload();
+ void initCava();
+ void cleanup();
+
+ void process() override;
+};
+
+class CavaProvider : public AudioProvider {
+ Q_OBJECT
+ QML_ELEMENT
+
+ Q_PROPERTY(int bars READ bars WRITE setBars NOTIFY barsChanged)
+
+ Q_PROPERTY(QVector<double> values READ values NOTIFY valuesChanged)
+
+public:
+ explicit CavaProvider(QObject* parent = nullptr);
+
+ [[nodiscard]] int bars() const;
+ void setBars(int bars);
+
+ [[nodiscard]] QVector<double> values() const;
+
+signals:
+ void barsChanged();
+ void valuesChanged();
+
+private:
+ int m_bars;
+ QVector<double> m_values;
+
+ void updateValues(QVector<double> values);
+};
+
+} // namespace caelestia
diff --git a/plugin/src/Caelestia/cutils.cpp b/plugin/src/Caelestia/cutils.cpp
index 59a33ba..0e0ded5 100644
--- a/plugin/src/Caelestia/cutils.cpp
+++ b/plugin/src/Caelestia/cutils.cpp
@@ -1,12 +1,11 @@
#include "cutils.hpp"
-#include <QDir>
-#include <QObject>
-#include <QQmlEngine>
-#include <QThreadPool>
-#include <QtQuick/QQuickItem>
-#include <QtQuick/QQuickItemGrabResult>
-#include <QtQuick/QQuickWindow>
+#include <QtQuick/qquickitemgrabresult.h>
+#include <QtQuick/qquickwindow.h>
+#include <qdir.h>
+#include <qfileinfo.h>
+#include <qqmlengine.h>
+#include <qthreadpool.h>
namespace caelestia {
diff --git a/plugin/src/Caelestia/cutils.hpp b/plugin/src/Caelestia/cutils.hpp
index 654c333..892ff86 100644
--- a/plugin/src/Caelestia/cutils.hpp
+++ b/plugin/src/Caelestia/cutils.hpp
@@ -1,6 +1,6 @@
#pragma once
-#include <QtQuick/QQuickItem>
+#include <QtQuick/qquickitem.h>
#include <qobject.h>
#include <qqmlintegration.h>
@@ -12,13 +12,14 @@ class CUtils : public QObject {
QML_SINGLETON
public:
+ // clang-format off
Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path);
Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, const QRect& rect);
Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, QJSValue onSaved);
Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, QJSValue onSaved, QJSValue onFailed);
Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, QJSValue onSaved);
- Q_INVOKABLE void saveItem(
- QQuickItem* target, const QUrl& path, const QRect& rect, QJSValue onSaved, QJSValue onFailed);
+ Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, QJSValue onSaved, QJSValue onFailed);
+ // clang-format on
Q_INVOKABLE bool copyFile(const QUrl& source, const QUrl& target) const;
Q_INVOKABLE bool copyFile(const QUrl& source, const QUrl& target, bool overwrite) const;
diff --git a/plugin/src/Caelestia/filesystemmodel.cpp b/plugin/src/Caelestia/filesystemmodel.cpp
index 15de489..9a435a8 100644
--- a/plugin/src/Caelestia/filesystemmodel.cpp
+++ b/plugin/src/Caelestia/filesystemmodel.cpp
@@ -1,14 +1,8 @@
#include "filesystemmodel.hpp"
-#include <QAbstractListModel>
-#include <QDir>
-#include <QDirIterator>
-#include <QFileInfo>
-#include <QFutureWatcher>
-#include <QImageReader>
-#include <QObject>
-#include <QtConcurrent>
-#include <qqmlintegration.h>
+#include <qdiriterator.h>
+#include <qfuturewatcher.h>
+#include <qtconcurrentrun.h>
namespace caelestia {
diff --git a/plugin/src/Caelestia/filesystemmodel.hpp b/plugin/src/Caelestia/filesystemmodel.hpp
index 4115e30..db9fff9 100644
--- a/plugin/src/Caelestia/filesystemmodel.hpp
+++ b/plugin/src/Caelestia/filesystemmodel.hpp
@@ -1,13 +1,12 @@
#pragma once
-#include <QAbstractListModel>
-#include <QDir>
-#include <QFileInfo>
-#include <QFileSystemWatcher>
-#include <QFuture>
-#include <QImageReader>
-#include <QMimeDatabase>
-#include <QObject>
+#include <qabstractitemmodel.h>
+#include <qdir.h>
+#include <qfilesystemwatcher.h>
+#include <qfuture.h>
+#include <qimagereader.h>
+#include <qmimedatabase.h>
+#include <qobject.h>
#include <qqmlintegration.h>
namespace caelestia {
diff --git a/plugin/src/Caelestia/qalculator.cpp b/plugin/src/Caelestia/qalculator.cpp
index 2755c08..44e8d21 100644
--- a/plugin/src/Caelestia/qalculator.cpp
+++ b/plugin/src/Caelestia/qalculator.cpp
@@ -1,6 +1,5 @@
#include "qalculator.hpp"
-#include <QObject>
#include <libqalculate/qalculate.h>
namespace caelestia {
diff --git a/plugin/src/Caelestia/qalculator.hpp b/plugin/src/Caelestia/qalculator.hpp
index 1791999..a07a8a2 100644
--- a/plugin/src/Caelestia/qalculator.hpp
+++ b/plugin/src/Caelestia/qalculator.hpp
@@ -1,6 +1,6 @@
#pragma once
-#include <QObject>
+#include <qobject.h>
#include <qqmlintegration.h>
namespace caelestia {
diff --git a/plugin/src/Caelestia/service.cpp b/plugin/src/Caelestia/service.cpp
new file mode 100644
index 0000000..7c0df92
--- /dev/null
+++ b/plugin/src/Caelestia/service.cpp
@@ -0,0 +1,54 @@
+#include "service.hpp"
+
+#include <qdebug.h>
+
+namespace caelestia {
+
+Service::Service(QObject* parent)
+ : QObject(parent)
+ , m_refCount(0) {}
+
+int Service::refCount() {
+ 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++;
+ }
+ emit refCountChanged();
+
+ if (needsStart) {
+ QMetaObject::invokeMethod(this, &Service::start, Qt::QueuedConnection);
+ }
+}
+
+void Service::unref() {
+ bool needsStop = false;
+
+ {
+ QMutexLocker locker(&m_mutex);
+ if (m_refCount == 0) {
+ qWarning() << "ServiceRef::unref: attempted to unref service with no active refs";
+ return;
+ }
+ m_refCount--;
+ if (m_refCount == 0) {
+ needsStop = true;
+ }
+ }
+ emit refCountChanged();
+
+ if (needsStop) {
+ QMetaObject::invokeMethod(this, &Service::stop, Qt::QueuedConnection);
+ }
+}
+
+} // namespace caelestia
diff --git a/plugin/src/Caelestia/service.hpp b/plugin/src/Caelestia/service.hpp
new file mode 100644
index 0000000..6422b1f
--- /dev/null
+++ b/plugin/src/Caelestia/service.hpp
@@ -0,0 +1,31 @@
+#pragma once
+
+#include <qmutex.h>
+#include <qobject.h>
+namespace caelestia {
+
+class Service : public QObject {
+ Q_OBJECT
+
+ Q_PROPERTY(int refCount READ refCount NOTIFY refCountChanged)
+
+public:
+ explicit Service(QObject* parent = nullptr);
+
+ [[nodiscard]] int refCount();
+
+ void ref();
+ void unref();
+
+signals:
+ void refCountChanged();
+
+private:
+ int m_refCount;
+ QMutex m_mutex;
+
+ virtual void start() = 0;
+ virtual void stop() = 0;
+};
+
+} // namespace caelestia
diff --git a/plugin/src/Caelestia/serviceref.cpp b/plugin/src/Caelestia/serviceref.cpp
new file mode 100644
index 0000000..dc82811
--- /dev/null
+++ b/plugin/src/Caelestia/serviceref.cpp
@@ -0,0 +1,42 @@
+#include "serviceref.hpp"
+
+#include "service.hpp"
+
+namespace caelestia {
+
+ServiceRef::ServiceRef(Service* service, QObject* parent)
+ : QObject(parent)
+ , m_service(service) {
+ if (m_service) {
+ m_service->ref();
+ }
+}
+
+ServiceRef::~ServiceRef() {
+ if (m_service) {
+ m_service->unref();
+ }
+}
+
+Service* ServiceRef::service() const {
+ return m_service;
+}
+
+void ServiceRef::setService(Service* service) {
+ if (m_service == service) {
+ return;
+ }
+
+ if (m_service) {
+ m_service->unref();
+ }
+
+ m_service = service;
+ emit serviceChanged();
+
+ if (m_service) {
+ m_service->ref();
+ }
+}
+
+} // namespace caelestia
diff --git a/plugin/src/Caelestia/serviceref.hpp b/plugin/src/Caelestia/serviceref.hpp
new file mode 100644
index 0000000..072419e
--- /dev/null
+++ b/plugin/src/Caelestia/serviceref.hpp
@@ -0,0 +1,28 @@
+#pragma once
+
+#include "service.hpp"
+#include <qqmlintegration.h>
+
+namespace caelestia {
+
+class ServiceRef : public QObject {
+ Q_OBJECT
+ QML_ELEMENT
+
+ Q_PROPERTY(Service* service READ service WRITE setService NOTIFY serviceChanged)
+
+public:
+ explicit ServiceRef(Service* service = nullptr, QObject* parent = nullptr);
+ ~ServiceRef();
+
+ [[nodiscard]] Service* service() const;
+ void setService(Service* service);
+
+signals:
+ void serviceChanged();
+
+private:
+ Service* m_service;
+};
+
+} // namespace caelestia