diff options
Diffstat (limited to 'plugin/src')
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 |