From 306cfc06ed38a2f86616c1f2fe64de45321f21a6 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 13 Sep 2025 14:38:44 +1000 Subject: plugin: refactor into modules --- plugin/src/Caelestia/CMakeLists.txt | 77 ++-- plugin/src/Caelestia/Managers/CMakeLists.txt | 9 + .../src/Caelestia/Managers/cachingimagemanager.cpp | 223 +++++++++++ .../src/Caelestia/Managers/cachingimagemanager.hpp | 65 ++++ plugin/src/Caelestia/Models/CMakeLists.txt | 8 + plugin/src/Caelestia/Models/filesystemmodel.cpp | 430 +++++++++++++++++++++ plugin/src/Caelestia/Models/filesystemmodel.hpp | 131 +++++++ plugin/src/Caelestia/Services/CMakeLists.txt | 14 + plugin/src/Caelestia/Services/audiocollector.cpp | 275 +++++++++++++ plugin/src/Caelestia/Services/audiocollector.hpp | 86 +++++ plugin/src/Caelestia/Services/audioprovider.cpp | 122 ++++++ plugin/src/Caelestia/Services/audioprovider.hpp | 61 +++ plugin/src/Caelestia/Services/beattracker.cpp | 82 ++++ plugin/src/Caelestia/Services/beattracker.hpp | 52 +++ plugin/src/Caelestia/Services/cavaprovider.cpp | 163 ++++++++ plugin/src/Caelestia/Services/cavaprovider.hpp | 67 ++++ plugin/src/Caelestia/Services/service.cpp | 70 ++++ plugin/src/Caelestia/Services/service.hpp | 32 ++ plugin/src/Caelestia/Services/serviceref.cpp | 42 ++ plugin/src/Caelestia/Services/serviceref.hpp | 28 ++ plugin/src/Caelestia/audiocollector.cpp | 275 ------------- plugin/src/Caelestia/audiocollector.hpp | 86 ----- plugin/src/Caelestia/audioprovider.cpp | 122 ------ plugin/src/Caelestia/audioprovider.hpp | 61 --- plugin/src/Caelestia/beattracker.cpp | 82 ---- plugin/src/Caelestia/beattracker.hpp | 52 --- plugin/src/Caelestia/cachingimagemanager.cpp | 223 ----------- plugin/src/Caelestia/cachingimagemanager.hpp | 65 ---- plugin/src/Caelestia/cavaprovider.cpp | 163 -------- plugin/src/Caelestia/cavaprovider.hpp | 67 ---- plugin/src/Caelestia/filesystemmodel.cpp | 430 --------------------- plugin/src/Caelestia/filesystemmodel.hpp | 131 ------- plugin/src/Caelestia/service.cpp | 70 ---- plugin/src/Caelestia/service.hpp | 32 -- plugin/src/Caelestia/serviceref.cpp | 42 -- plugin/src/Caelestia/serviceref.hpp | 28 -- 36 files changed, 1998 insertions(+), 1968 deletions(-) create mode 100644 plugin/src/Caelestia/Managers/CMakeLists.txt create mode 100644 plugin/src/Caelestia/Managers/cachingimagemanager.cpp create mode 100644 plugin/src/Caelestia/Managers/cachingimagemanager.hpp create mode 100644 plugin/src/Caelestia/Models/CMakeLists.txt create mode 100644 plugin/src/Caelestia/Models/filesystemmodel.cpp create mode 100644 plugin/src/Caelestia/Models/filesystemmodel.hpp create mode 100644 plugin/src/Caelestia/Services/CMakeLists.txt create mode 100644 plugin/src/Caelestia/Services/audiocollector.cpp create mode 100644 plugin/src/Caelestia/Services/audiocollector.hpp create mode 100644 plugin/src/Caelestia/Services/audioprovider.cpp create mode 100644 plugin/src/Caelestia/Services/audioprovider.hpp create mode 100644 plugin/src/Caelestia/Services/beattracker.cpp create mode 100644 plugin/src/Caelestia/Services/beattracker.hpp create mode 100644 plugin/src/Caelestia/Services/cavaprovider.cpp create mode 100644 plugin/src/Caelestia/Services/cavaprovider.hpp create mode 100644 plugin/src/Caelestia/Services/service.cpp create mode 100644 plugin/src/Caelestia/Services/service.hpp create mode 100644 plugin/src/Caelestia/Services/serviceref.cpp create mode 100644 plugin/src/Caelestia/Services/serviceref.hpp delete mode 100644 plugin/src/Caelestia/audiocollector.cpp delete mode 100644 plugin/src/Caelestia/audiocollector.hpp delete mode 100644 plugin/src/Caelestia/audioprovider.cpp delete mode 100644 plugin/src/Caelestia/audioprovider.hpp delete mode 100644 plugin/src/Caelestia/beattracker.cpp delete mode 100644 plugin/src/Caelestia/beattracker.hpp delete mode 100644 plugin/src/Caelestia/cachingimagemanager.cpp delete mode 100644 plugin/src/Caelestia/cachingimagemanager.hpp delete mode 100644 plugin/src/Caelestia/cavaprovider.cpp delete mode 100644 plugin/src/Caelestia/cavaprovider.hpp delete mode 100644 plugin/src/Caelestia/filesystemmodel.cpp delete mode 100644 plugin/src/Caelestia/filesystemmodel.hpp delete mode 100644 plugin/src/Caelestia/service.cpp delete mode 100644 plugin/src/Caelestia/service.hpp delete mode 100644 plugin/src/Caelestia/serviceref.cpp delete mode 100644 plugin/src/Caelestia/serviceref.hpp (limited to 'plugin/src') diff --git a/plugin/src/Caelestia/CMakeLists.txt b/plugin/src/Caelestia/CMakeLists.txt index 94496c6..2832b5d 100644 --- a/plugin/src/Caelestia/CMakeLists.txt +++ b/plugin/src/Caelestia/CMakeLists.txt @@ -8,50 +8,49 @@ pkg_check_modules(Cava IMPORTED_TARGET cava REQUIRED) set(QT_QML_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/qml") qt_standard_project_setup(REQUIRES 6.9) -qt_add_qml_module(caelestia +function(qml_module arg_TARGET) + cmake_parse_arguments(PARSE_ARGV 1 arg "" "URI" "SOURCES;LIBRARIES") + + qt_add_qml_module(${arg_TARGET} + URI ${arg_URI} + VERSION ${VERSION} + SOURCES ${arg_SOURCES} + ) + + qt_query_qml_module(${arg_TARGET} + URI module_uri + VERSION module_version + PLUGIN_TARGET module_plugin_target + TARGET_PATH module_target_path + QMLDIR module_qmldir + TYPEINFO module_typeinfo + ) + + message(STATUS "Created QML module ${module_uri}, version ${module_version}") + + set(module_dir "${INSTALL_QMLDIR}/${module_target_path}") + install(TARGETS ${arg_TARGET} 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_link_libraries(${arg_TARGET} PRIVATE Qt::Core Qt::Qml ${arg_LIBRARIES}) +endfunction() + +qml_module(caelestia URI Caelestia - VERSION ${VERSION} 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 appdb.hpp appdb.cpp requests.hpp requests.cpp + LIBRARIES + Qt::Gui + Qt::Quick + Qt::Sql + PkgConfig::Qalculate ) -qt_query_qml_module(caelestia - URI module_uri - VERSION module_version - PLUGIN_TARGET module_plugin_target - TARGET_PATH module_target_path - QMLDIR module_qmldir - TYPEINFO module_typeinfo -) - -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_link_libraries(caelestia PRIVATE - Qt::Core - Qt::Qml - Qt::Gui - Qt::Quick - Qt::Concurrent - Qt::Sql - PkgConfig::Qalculate - PkgConfig::Pipewire - PkgConfig::Aubio - PkgConfig::Cava -) +add_subdirectory(Managers) +add_subdirectory(Models) +add_subdirectory(Services) diff --git a/plugin/src/Caelestia/Managers/CMakeLists.txt b/plugin/src/Caelestia/Managers/CMakeLists.txt new file mode 100644 index 0000000..9bb5baa --- /dev/null +++ b/plugin/src/Caelestia/Managers/CMakeLists.txt @@ -0,0 +1,9 @@ +qml_module(caelestia-managers + URI Caelestia.Managers + SOURCES + cachingimagemanager.hpp cachingimagemanager.cpp + LIBRARIES + Qt::Gui + Qt::Quick + Qt::Concurrent +) diff --git a/plugin/src/Caelestia/Managers/cachingimagemanager.cpp b/plugin/src/Caelestia/Managers/cachingimagemanager.cpp new file mode 100644 index 0000000..3394f89 --- /dev/null +++ b/plugin/src/Caelestia/Managers/cachingimagemanager.cpp @@ -0,0 +1,223 @@ +#include "cachingimagemanager.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace caelestia { + +qreal CachingImageManager::effectiveScale() const { + if (m_item && m_item->window()) { + return m_item->window()->devicePixelRatio(); + } + + return 1.0; +} + +QSize CachingImageManager::effectiveSize() const { + if (!m_item) { + return QSize(); + } + + const qreal scale = effectiveScale(); + const QSize size = QSizeF(m_item->width() * scale, m_item->height() * scale).toSize(); + m_item->setProperty("sourceSize", size); + return size; +} + +QQuickItem* CachingImageManager::item() const { + return m_item; +} + +void CachingImageManager::setItem(QQuickItem* item) { + if (m_item == item) { + return; + } + + if (m_widthConn) { + disconnect(m_widthConn); + } + if (m_heightConn) { + disconnect(m_heightConn); + } + + m_item = item; + emit itemChanged(); + + if (item) { + m_widthConn = connect(item, &QQuickItem::widthChanged, this, [this]() { + updateSource(); + }); + m_heightConn = connect(item, &QQuickItem::heightChanged, this, [this]() { + updateSource(); + }); + updateSource(); + } +} + +QUrl CachingImageManager::cacheDir() const { + return m_cacheDir; +} + +void CachingImageManager::setCacheDir(const QUrl& cacheDir) { + if (m_cacheDir == cacheDir) { + return; + } + + m_cacheDir = cacheDir; + if (!m_cacheDir.path().endsWith("/")) { + m_cacheDir.setPath(m_cacheDir.path() + "/"); + } + emit cacheDirChanged(); +} + +QString CachingImageManager::path() const { + return m_path; +} + +void CachingImageManager::setPath(const QString& path) { + if (m_path == path) { + return; + } + + m_path = path; + emit pathChanged(); + + if (!path.isEmpty()) { + updateSource(path); + } +} + +void CachingImageManager::updateSource() { + updateSource(m_path); +} + +void CachingImageManager::updateSource(const QString& path) { + if (path.isEmpty() || path == m_shaPath) { + // Path is empty or already calculating sha for path + return; + } + + m_shaPath = path; + + const auto future = QtConcurrent::run(&CachingImageManager::sha256sum, path); + + const auto watcher = new QFutureWatcher(this); + + connect(watcher, &QFutureWatcher::finished, this, [watcher, path, this]() { + if (m_path != path) { + // Object is destroyed or path has changed, ignore + watcher->deleteLater(); + return; + } + + const QSize size = effectiveSize(); + + if (!m_item || !size.width() || !size.height()) { + watcher->deleteLater(); + return; + } + + const QString fillMode = m_item->property("fillMode").toString(); + // clang-format off + const QString filename = QString("%1@%2x%3-%4.png") + .arg(watcher->result()).arg(size.width()).arg(size.height()) + .arg(fillMode == "PreserveAspectCrop" ? "crop" : fillMode == "PreserveAspectFit" ? "fit" : "stretch"); + // clang-format on + + const QUrl cache = m_cacheDir.resolved(QUrl(filename)); + if (m_cachePath == cache) { + watcher->deleteLater(); + return; + } + + m_cachePath = cache; + emit cachePathChanged(); + + if (!cache.isLocalFile()) { + qWarning() << "CachingImageManager::updateSource: cachePath" << cache << "is not a local file"; + watcher->deleteLater(); + return; + } + + const QImageReader reader(cache.toLocalFile()); + if (reader.canRead()) { + m_item->setProperty("source", cache); + } else { + m_item->setProperty("source", QUrl::fromLocalFile(path)); + createCache(path, cache.toLocalFile(), fillMode, size); + } + + // Clear current running sha if same + if (m_shaPath == path) { + m_shaPath = QString(); + } + + watcher->deleteLater(); + }); + + watcher->setFuture(future); +} + +QUrl CachingImageManager::cachePath() const { + return m_cachePath; +} + +void CachingImageManager::createCache( + const QString& path, const QString& cache, const QString& fillMode, const QSize& size) const { + QThreadPool::globalInstance()->start([path, cache, fillMode, size] { + QImage image(path); + + if (image.isNull()) { + qWarning() << "CachingImageManager::createCache: failed to read" << path; + return; + } + + image.convertTo(QImage::Format_ARGB32); + + if (fillMode == "PreserveAspectCrop") { + image = image.scaled(size, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation); + } else if (fillMode == "PreserveAspectFit") { + image = image.scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation); + } else { + image = image.scaled(size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + } + + if (fillMode == "PreserveAspectCrop" || fillMode == "PreserveAspectFit") { + QImage canvas(size, QImage::Format_ARGB32); + canvas.fill(Qt::transparent); + + QPainter painter(&canvas); + painter.drawImage((size.width() - image.width()) / 2, (size.height() - image.height()) / 2, image); + painter.end(); + + image = canvas; + } + + const QString parent = QFileInfo(cache).absolutePath(); + if (!QDir().mkpath(parent) || !image.save(cache)) { + qWarning() << "CachingImageManager::createCache: failed to save to" << cache; + } + }); +} + +QString CachingImageManager::sha256sum(const QString& path) { + QFile file(path); + if (!file.open(QIODevice::ReadOnly)) { + qWarning() << "CachingImageManager::sha256sum: failed to open" << path; + return ""; + } + + QCryptographicHash hash(QCryptographicHash::Sha256); + hash.addData(&file); + file.close(); + + return hash.result().toHex(); +} + +} // namespace caelestia diff --git a/plugin/src/Caelestia/Managers/cachingimagemanager.hpp b/plugin/src/Caelestia/Managers/cachingimagemanager.hpp new file mode 100644 index 0000000..f05ea34 --- /dev/null +++ b/plugin/src/Caelestia/Managers/cachingimagemanager.hpp @@ -0,0 +1,65 @@ +#pragma once + +#include +#include +#include + +namespace caelestia { + +class CachingImageManager : public QObject { + Q_OBJECT + QML_ELEMENT + + Q_PROPERTY(QQuickItem* item READ item WRITE setItem NOTIFY itemChanged REQUIRED) + Q_PROPERTY(QUrl cacheDir READ cacheDir WRITE setCacheDir NOTIFY cacheDirChanged REQUIRED) + + Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged) + Q_PROPERTY(QUrl cachePath READ cachePath NOTIFY cachePathChanged) + +public: + explicit CachingImageManager(QObject* parent = nullptr) + : QObject(parent) + , m_item(nullptr) {} + + [[nodiscard]] QQuickItem* item() const; + void setItem(QQuickItem* item); + + [[nodiscard]] QUrl cacheDir() const; + void setCacheDir(const QUrl& cacheDir); + + [[nodiscard]] QString path() const; + void setPath(const QString& path); + + [[nodiscard]] QUrl cachePath() const; + + Q_INVOKABLE void updateSource(); + Q_INVOKABLE void updateSource(const QString& path); + +signals: + void itemChanged(); + void cacheDirChanged(); + + void pathChanged(); + void cachePathChanged(); + void usingCacheChanged(); + +private: + QString m_shaPath; + + QQuickItem* m_item; + QUrl m_cacheDir; + + QString m_path; + QUrl m_cachePath; + + QMetaObject::Connection m_widthConn; + QMetaObject::Connection m_heightConn; + + [[nodiscard]] qreal effectiveScale() const; + [[nodiscard]] QSize effectiveSize() const; + + void createCache(const QString& path, const QString& cache, const QString& fillMode, const QSize& size) const; + [[nodiscard]] static QString sha256sum(const QString& path); +}; + +} // namespace caelestia diff --git a/plugin/src/Caelestia/Models/CMakeLists.txt b/plugin/src/Caelestia/Models/CMakeLists.txt new file mode 100644 index 0000000..640e29e --- /dev/null +++ b/plugin/src/Caelestia/Models/CMakeLists.txt @@ -0,0 +1,8 @@ +qml_module(caelestia-models + URI Caelestia.Models + SOURCES + filesystemmodel.hpp filesystemmodel.cpp + LIBRARIES + Qt::Gui + Qt::Concurrent +) diff --git a/plugin/src/Caelestia/Models/filesystemmodel.cpp b/plugin/src/Caelestia/Models/filesystemmodel.cpp new file mode 100644 index 0000000..54807b5 --- /dev/null +++ b/plugin/src/Caelestia/Models/filesystemmodel.cpp @@ -0,0 +1,430 @@ +#include "filesystemmodel.hpp" + +#include +#include +#include + +namespace caelestia { + +FileSystemEntry::FileSystemEntry(const QString& path, const QString& relativePath, QObject* parent) + : QObject(parent) + , m_fileInfo(QFileInfo(path)) + , m_path(path) + , m_relativePath(relativePath) + , m_isImageInitialised(false) + , m_mimeTypeInitialised(false) {} + +QString FileSystemEntry::path() const { + return m_path; +}; + +QString FileSystemEntry::relativePath() const { + return m_relativePath; +}; + +QString FileSystemEntry::name() const { + return m_fileInfo.fileName(); +}; + +QString FileSystemEntry::parentDir() const { + return m_fileInfo.absolutePath(); +}; + +QString FileSystemEntry::suffix() const { + return m_fileInfo.completeSuffix(); +}; + +qint64 FileSystemEntry::size() const { + return m_fileInfo.size(); +}; + +bool FileSystemEntry::isDir() const { + return m_fileInfo.isDir(); +}; + +bool FileSystemEntry::isImage() const { + if (!m_isImageInitialised) { + QImageReader reader(m_path); + m_isImage = reader.canRead(); + m_isImageInitialised = true; + } + return m_isImage; +} + +QString FileSystemEntry::mimeType() const { + if (!m_mimeTypeInitialised) { + const QMimeDatabase db; + m_mimeType = db.mimeTypeForFile(m_path).name(); + m_mimeTypeInitialised = true; + } + return m_mimeType; +} + +FileSystemModel::FileSystemModel(QObject* parent) + : QAbstractListModel(parent) + , m_recursive(false) + , m_watchChanges(true) + , m_showHidden(false) + , m_filter(NoFilter) { + connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &FileSystemModel::watchDirIfRecursive); + connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &FileSystemModel::updateEntriesForDir); +} + +int FileSystemModel::rowCount(const QModelIndex& parent) const { + if (parent != QModelIndex()) { + return 0; + } + return static_cast(m_entries.size()); +} + +QVariant FileSystemModel::data(const QModelIndex& index, int role) const { + if (role != Qt::UserRole || !index.isValid() || index.row() >= m_entries.size()) { + return QVariant(); + } + return QVariant::fromValue(m_entries.at(index.row())); +} + +QHash FileSystemModel::roleNames() const { + return { { Qt::UserRole, "modelData" } }; +} + +QString FileSystemModel::path() const { + return m_path; +} + +void FileSystemModel::setPath(const QString& path) { + if (m_path == path) { + return; + } + + m_path = path; + emit pathChanged(); + + m_dir.setPath(m_path); + update(); +} + +bool FileSystemModel::recursive() const { + return m_recursive; +} + +void FileSystemModel::setRecursive(bool recursive) { + if (m_recursive == recursive) { + return; + } + + m_recursive = recursive; + emit recursiveChanged(); + + update(); +} + +bool FileSystemModel::watchChanges() const { + return m_watchChanges; +} + +void FileSystemModel::setWatchChanges(bool watchChanges) { + if (m_watchChanges == watchChanges) { + return; + } + + m_watchChanges = watchChanges; + emit watchChangesChanged(); + + update(); +} + +bool FileSystemModel::showHidden() const { + return m_showHidden; +} + +void FileSystemModel::setShowHidden(bool showHidden) { + if (m_showHidden == showHidden) { + return; + } + + m_showHidden = showHidden; + emit showHiddenChanged(); + + update(); +} + +FileSystemModel::Filter FileSystemModel::filter() const { + return m_filter; +} + +void FileSystemModel::setFilter(Filter filter) { + if (m_filter == filter) { + return; + } + + m_filter = filter; + emit filterChanged(); + + update(); +} + +QList FileSystemModel::entries() const { + return m_entries; +} + +void FileSystemModel::watchDirIfRecursive(const QString& path) { + if (m_recursive && m_watchChanges) { + const auto currentDir = m_dir; + const bool showHidden = m_showHidden; + const auto future = QtConcurrent::run([showHidden, path]() { + QDir::Filters filters = QDir::Dirs | QDir::NoDotAndDotDot; + if (showHidden) { + filters |= QDir::Hidden; + } + + QDirIterator iter(path, filters, QDirIterator::Subdirectories); + QStringList dirs; + while (iter.hasNext()) { + dirs << iter.next(); + } + return dirs; + }); + const auto watcher = new QFutureWatcher(this); + connect(watcher, &QFutureWatcher::finished, this, [currentDir, showHidden, watcher, this]() { + const auto paths = watcher->result(); + if (currentDir == m_dir && showHidden == m_showHidden && !paths.isEmpty()) { + // Ignore if dir or showHidden has changed + m_watcher.addPaths(paths); + } + watcher->deleteLater(); + }); + watcher->setFuture(future); + } +} + +void FileSystemModel::update() { + updateWatcher(); + updateEntries(); +} + +void FileSystemModel::updateWatcher() { + if (!m_watcher.directories().isEmpty()) { + m_watcher.removePaths(m_watcher.directories()); + } + + if (!m_watchChanges || m_path.isEmpty()) { + return; + } + + m_watcher.addPath(m_path); + watchDirIfRecursive(m_path); +} + +void FileSystemModel::updateEntries() { + if (m_path.isEmpty()) { + if (!m_entries.isEmpty()) { + beginResetModel(); + qDeleteAll(m_entries); + m_entries.clear(); + emit entriesChanged(); + endResetModel(); + } + + return; + } + + for (auto& future : m_futures) { + future.cancel(); + } + m_futures.clear(); + + updateEntriesForDir(m_path); +} + +void FileSystemModel::updateEntriesForDir(const QString& dir) { + const bool recursive = m_recursive; + const bool showHidden = m_showHidden; + const auto filter = m_filter; + const auto oldEntries = m_entries; + const auto baseDir = m_dir; + + const auto future = QtConcurrent::run([dir, recursive, showHidden, filter, oldEntries, baseDir]( + QPromise, QSet>>& promise) { + const auto flags = recursive ? QDirIterator::Subdirectories : QDirIterator::NoIteratorFlags; + + std::optional iter; + + if (filter == Images) { + QStringList nameFilters; + for (const auto& format : QImageReader::supportedImageFormats()) { + nameFilters << "*." + format; + } + + QDir::Filters filters = QDir::Files; + if (showHidden) { + filters |= QDir::Hidden; + } + + iter.emplace(dir, nameFilters, filters, flags); + } else { + QDir::Filters filters; + + if (filter == Files) { + filters = QDir::Files; + } else if (filter == Dirs) { + filters = QDir::Dirs | QDir::NoDotAndDotDot; + } else { + filters = QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot; + } + + if (showHidden) { + filters |= QDir::Hidden; + } + + iter.emplace(dir, filters, flags); + } + + QSet newPaths; + while (iter->hasNext()) { + if (promise.isCanceled()) { + return; + } + + QString path = iter->next(); + + if (filter == Images) { + QImageReader reader(path); + if (!reader.canRead()) { + continue; + } + } + + newPaths.insert(path); + } + + QSet oldPaths; + for (const auto& entry : oldEntries) { + oldPaths.insert(entry->path()); + } + + if (promise.isCanceled() || newPaths == oldPaths) { + return; + } + + promise.addResult(qMakePair(oldPaths - newPaths, newPaths - oldPaths)); + }); + + if (m_futures.contains(dir)) { + m_futures[dir].cancel(); + } + m_futures.insert(dir, future); + + const auto watcher = new QFutureWatcher, QSet>>(this); + + connect(watcher, &QFutureWatcher, QSet>>::finished, this, [dir, watcher, this]() { + m_futures.remove(dir); + + if (!watcher->future().isResultReadyAt(0)) { + watcher->deleteLater(); + return; + } + + const auto result = watcher->result(); + applyChanges(result.first, result.second); + + watcher->deleteLater(); + }); + + watcher->setFuture(future); +} + +void FileSystemModel::applyChanges(const QSet& removedPaths, const QSet& addedPaths) { + QList removedIndices; + for (int i = 0; i < m_entries.size(); ++i) { + if (removedPaths.contains(m_entries[i]->path())) { + removedIndices << i; + } + } + std::sort(removedIndices.begin(), removedIndices.end(), std::greater()); + + int start = -1; + int end = -1; + for (int idx : removedIndices) { + if (start == -1) { + start = idx; + end = idx; + } else if (idx == end - 1) { + end = idx; + } else { + beginRemoveRows(QModelIndex(), end, start); + for (int i = start; i >= end; --i) { + emit removed(m_entries[i]->path()); + delete m_entries.takeAt(i); + } + endRemoveRows(); + + start = idx; + end = idx; + } + } + if (start != -1) { + beginRemoveRows(QModelIndex(), end, start); + for (int i = start; i >= end; --i) { + emit removed(m_entries[i]->path()); + delete m_entries.takeAt(i); + } + endRemoveRows(); + } + + QList newEntries; + for (const auto& path : addedPaths) { + newEntries << new FileSystemEntry(path, m_dir.relativeFilePath(path), this); + } + std::sort(newEntries.begin(), newEntries.end(), &FileSystemModel::compareEntries); + + int insertStart = -1; + int prevRow = -1; + QList batchItems; + for (const auto& entry : newEntries) { + const auto it = std::lower_bound(m_entries.begin(), m_entries.end(), entry, &FileSystemModel::compareEntries); + int row = static_cast(it - m_entries.begin()); + + if (insertStart == -1) { + insertStart = row; + prevRow = row; + batchItems.clear(); + batchItems << entry; + } else if (row == prevRow + 1) { + prevRow = row; + batchItems << entry; + } else { + beginInsertRows(QModelIndex(), insertStart, static_cast(insertStart + batchItems.size() - 1)); + for (int i = 0; i < batchItems.size(); ++i) { + m_entries.insert(insertStart + i, batchItems[i]); + emit added(batchItems[i]); + } + endInsertRows(); + + insertStart = row; + prevRow = row; + batchItems.clear(); + batchItems << entry; + } + prevRow = static_cast(m_entries.indexOf(entry)); + } + if (!batchItems.isEmpty()) { + beginInsertRows(QModelIndex(), insertStart, static_cast(insertStart + batchItems.size() - 1)); + for (int i = 0; i < batchItems.size(); ++i) { + m_entries.insert(insertStart + i, batchItems[i]); + emit added(batchItems[i]); + } + endInsertRows(); + } + + emit entriesChanged(); +} + +bool FileSystemModel::compareEntries(const FileSystemEntry* a, const FileSystemEntry* b) { + if (a->isDir() != b->isDir()) { + return a->isDir(); + } + return a->relativePath().localeAwareCompare(b->relativePath()) < 0; +} + +} // namespace caelestia diff --git a/plugin/src/Caelestia/Models/filesystemmodel.hpp b/plugin/src/Caelestia/Models/filesystemmodel.hpp new file mode 100644 index 0000000..4ea5f0a --- /dev/null +++ b/plugin/src/Caelestia/Models/filesystemmodel.hpp @@ -0,0 +1,131 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace caelestia { + +class FileSystemEntry : public QObject { + Q_OBJECT + QML_ELEMENT + QML_UNCREATABLE("FileSystemEntry instances can only be retrieved from a FileSystemModel") + + Q_PROPERTY(QString path READ path CONSTANT) + Q_PROPERTY(QString relativePath READ relativePath CONSTANT) + Q_PROPERTY(QString name READ name CONSTANT) + Q_PROPERTY(QString parentDir READ parentDir CONSTANT) + Q_PROPERTY(QString suffix READ suffix CONSTANT) + Q_PROPERTY(qint64 size READ size CONSTANT) + Q_PROPERTY(bool isDir READ isDir CONSTANT) + Q_PROPERTY(bool isImage READ isImage CONSTANT) + Q_PROPERTY(QString mimeType READ mimeType CONSTANT) + +public: + explicit FileSystemEntry(const QString& path, const QString& relativePath, QObject* parent = nullptr); + + [[nodiscard]] QString path() const; + [[nodiscard]] QString relativePath() const; + [[nodiscard]] QString name() const; + [[nodiscard]] QString parentDir() const; + [[nodiscard]] QString suffix() const; + [[nodiscard]] qint64 size() const; + [[nodiscard]] bool isDir() const; + [[nodiscard]] bool isImage() const; + [[nodiscard]] QString mimeType() const; + +private: + const QFileInfo m_fileInfo; + + const QString m_path; + const QString m_relativePath; + + mutable bool m_isImage; + mutable bool m_isImageInitialised; + + mutable QString m_mimeType; + mutable bool m_mimeTypeInitialised; +}; + +class FileSystemModel : public QAbstractListModel { + Q_OBJECT + QML_ELEMENT + + Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged) + Q_PROPERTY(bool recursive READ recursive WRITE setRecursive NOTIFY recursiveChanged) + Q_PROPERTY(bool watchChanges READ watchChanges WRITE setWatchChanges NOTIFY watchChangesChanged) + Q_PROPERTY(bool showHidden READ showHidden WRITE setShowHidden NOTIFY showHiddenChanged) + Q_PROPERTY(Filter filter READ filter WRITE setFilter NOTIFY filterChanged) + + Q_PROPERTY(QList entries READ entries NOTIFY entriesChanged) + +public: + enum Filter { + NoFilter, + Images, + Files, + Dirs + }; + Q_ENUM(Filter) + + explicit FileSystemModel(QObject* parent = nullptr); + + int rowCount(const QModelIndex& parent = QModelIndex()) const override; + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + QHash roleNames() const override; + + [[nodiscard]] QString path() const; + void setPath(const QString& path); + + [[nodiscard]] bool recursive() const; + void setRecursive(bool recursive); + + [[nodiscard]] bool watchChanges() const; + void setWatchChanges(bool watchChanges); + + [[nodiscard]] bool showHidden() const; + void setShowHidden(bool showHidden); + + [[nodiscard]] Filter filter() const; + void setFilter(Filter filter); + + [[nodiscard]] QList entries() const; + +signals: + void pathChanged(); + void recursiveChanged(); + void watchChangesChanged(); + void showHiddenChanged(); + void filterChanged(); + void entriesChanged(); + + void added(const FileSystemEntry* entry); + void removed(const QString& path); + +private: + QDir m_dir; + QFileSystemWatcher m_watcher; + QList m_entries; + QHash, QSet>>> m_futures; + + QString m_path; + bool m_recursive; + bool m_watchChanges; + bool m_showHidden; + Filter m_filter; + + void watchDirIfRecursive(const QString& path); + void update(); + void updateWatcher(); + void updateEntries(); + void updateEntriesForDir(const QString& dir); + void applyChanges(const QSet& removedPaths, const QSet& addedPaths); + [[nodiscard]] static bool compareEntries(const FileSystemEntry* a, const FileSystemEntry* b); +}; + +} // namespace caelestia diff --git a/plugin/src/Caelestia/Services/CMakeLists.txt b/plugin/src/Caelestia/Services/CMakeLists.txt new file mode 100644 index 0000000..8ce868b --- /dev/null +++ b/plugin/src/Caelestia/Services/CMakeLists.txt @@ -0,0 +1,14 @@ +qml_module(caelestia-services + URI Caelestia.Services + SOURCES + service.hpp service.cpp + serviceref.hpp serviceref.cpp + beattracker.hpp beattracker.cpp + audiocollector.hpp audiocollector.cpp + audioprovider.hpp audioprovider.cpp + cavaprovider.hpp cavaprovider.cpp + LIBRARIES + PkgConfig::Pipewire + PkgConfig::Aubio + PkgConfig::Cava +) diff --git a/plugin/src/Caelestia/Services/audiocollector.cpp b/plugin/src/Caelestia/Services/audiocollector.cpp new file mode 100644 index 0000000..9dc3871 --- /dev/null +++ b/plugin/src/Caelestia/Services/audiocollector.cpp @@ -0,0 +1,275 @@ +#include "audiocollector.hpp" + +#include "service.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +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"; + pw_deinit(); + 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 buffer(collector->chunkSize()); + spa_pod_builder b; + spa_pod_builder_init(&b, buffer.data(), static_cast(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(data); + self->streamStateChanged(state); + }; + events.process = [](void* data) { + auto* self = static_cast(data); + self->processStream(); + }; + + 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(), + 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); + + 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(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 qint16* samples = reinterpret_cast(buf->datas[0].data); + if (samples == nullptr) { + return; + } + + const quint32 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(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(); + } +} + +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 qint16* samples, quint32 count) { + if (count > m_chunkSize) { + count = m_chunkSize; + } + + auto* writeBuffer = m_writeBuffer.load(std::memory_order_relaxed); + std::transform(samples, samples + count, writeBuffer->begin(), [](qint16 sample) { + return sample / 32768.0f; + }); + + auto* oldRead = m_readBuffer.exchange(writeBuffer, std::memory_order_acq_rel); + m_writeBuffer.store(oldRead, std::memory_order_release); +} + +quint32 AudioCollector::readChunk(float* out, quint32 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; +} + +quint32 AudioCollector::readChunk(double* out, quint32 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(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/Services/audiocollector.hpp b/plugin/src/Caelestia/Services/audiocollector.hpp new file mode 100644 index 0000000..74b0877 --- /dev/null +++ b/plugin/src/Caelestia/Services/audiocollector.hpp @@ -0,0 +1,86 @@ +#pragma once + +#include "service.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +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 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(); + + [[nodiscard]] quint32 sampleRate() const; + [[nodiscard]] quint32 chunkSize() const; + + [[nodiscard]] quint32 nodeId(); + void setNodeId(quint32 nodeId); + + 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; + + std::jthread m_thread; + std::vector m_buffer1; + std::vector m_buffer2; + std::atomic*> m_readBuffer; + std::atomic*> m_writeBuffer; + quint32 m_sampleCount; + + void reload(); + void start() override; + void stop() override; +}; + +} // namespace caelestia diff --git a/plugin/src/Caelestia/Services/audioprovider.cpp b/plugin/src/Caelestia/Services/audioprovider.cpp new file mode 100644 index 0000000..f31480e --- /dev/null +++ b/plugin/src/Caelestia/Services/audioprovider.cpp @@ -0,0 +1,122 @@ +#include "audioprovider.hpp" + +#include "audiocollector.hpp" +#include "service.hpp" +#include +#include + +namespace caelestia { + +AudioProcessor::AudioProcessor(AudioCollector* collector, QObject* parent) + : QObject(parent) + , m_collector(collector) {} + +AudioProcessor::~AudioProcessor() { + stop(); +} + +void AudioProcessor::init() { + m_timer = new QTimer(this); + 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::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 && m_collector) { + m_timer->stop(); + m_collector->unref(); + } +} + +AudioProvider::AudioProvider(QObject* parent) + : Service(parent) + , m_collector(nullptr) + , m_processor(nullptr) + , m_thread(nullptr) {} + +AudioProvider::~AudioProvider() { + if (m_thread) { + m_thread->quit(); + m_thread->wait(); + } +} + +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"; + 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/Services/audioprovider.hpp b/plugin/src/Caelestia/Services/audioprovider.hpp new file mode 100644 index 0000000..c92b965 --- /dev/null +++ b/plugin/src/Caelestia/Services/audioprovider.hpp @@ -0,0 +1,61 @@ +#pragma once + +#include "audiocollector.hpp" +#include "service.hpp" +#include +#include + +namespace caelestia { + +class AudioProcessor : public QObject { + Q_OBJECT + +public: + explicit AudioProcessor(AudioCollector* collector, QObject* parent = nullptr); + ~AudioProcessor(); + + void init(); + +protected: + AudioCollector* m_collector; + + Q_INVOKABLE virtual void setCollector(AudioCollector* collector); + +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(); + +private: + QThread* m_thread; + + void start() override; + void stop() override; +}; + +} // namespace caelestia diff --git a/plugin/src/Caelestia/Services/beattracker.cpp b/plugin/src/Caelestia/Services/beattracker.cpp new file mode 100644 index 0000000..462abb8 --- /dev/null +++ b/plugin/src/Caelestia/Services/beattracker.cpp @@ -0,0 +1,82 @@ +#include "beattracker.hpp" + +#include "audiocollector.hpp" +#include "audioprovider.hpp" +#include + +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() { + 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() { + if (!m_collector || !m_tempo || !m_in) { + return; + } + + m_collector->readChunk(m_in->data); + + aubio_tempo_do(m_tempo, m_in, m_out); + if (!qFuzzyIsNull(m_out->data[0])) { + emit beat(aubio_tempo_get_bpm(m_tempo)); + } +} + +BeatTracker::BeatTracker(QObject* parent) + : AudioProvider(parent) + , m_bpm(120) { + m_processor = new BeatProcessor(m_collector); + init(); + + connect(static_cast(m_processor), &BeatProcessor::beat, this, &BeatTracker::updateBpm); +} + +smpl_t BeatTracker::bpm() const { + return m_bpm; +} + +void BeatTracker::updateBpm(smpl_t bpm) { + if (!qFuzzyCompare(bpm + 1.0f, m_bpm + 1.0f)) { + m_bpm = bpm; + emit bpmChanged(); + } +} + +} // namespace caelestia diff --git a/plugin/src/Caelestia/Services/beattracker.hpp b/plugin/src/Caelestia/Services/beattracker.hpp new file mode 100644 index 0000000..ab18373 --- /dev/null +++ b/plugin/src/Caelestia/Services/beattracker.hpp @@ -0,0 +1,52 @@ +#pragma once + +#include "audiocollector.hpp" +#include "audioprovider.hpp" +#include +#include + +namespace caelestia { + +class BeatProcessor : public AudioProcessor { + Q_OBJECT + +public: + 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; + fvec_t* m_out; + + void process() override; +}; + +class BeatTracker : public AudioProvider { + Q_OBJECT + QML_ELEMENT + + Q_PROPERTY(smpl_t bpm READ bpm NOTIFY bpmChanged) + +public: + explicit BeatTracker(QObject* parent = nullptr); + + [[nodiscard]] smpl_t bpm() const; + +signals: + void bpmChanged(); + void beat(smpl_t bpm); + +private: + smpl_t m_bpm; + + void updateBpm(smpl_t bpm); +}; + +} // namespace caelestia diff --git a/plugin/src/Caelestia/Services/cavaprovider.cpp b/plugin/src/Caelestia/Services/cavaprovider.cpp new file mode 100644 index 0000000..76a1a10 --- /dev/null +++ b/plugin/src/Caelestia/Services/cavaprovider.cpp @@ -0,0 +1,163 @@ +#include "cavaprovider.hpp" + +#include "audiocollector.hpp" +#include "audioprovider.hpp" +#include +#include +#include +#include + +namespace caelestia { + +CavaProcessor::CavaProcessor(AudioCollector* collector, QObject* parent) + : AudioProcessor(collector, parent) + , m_plan(nullptr) + , m_in(nullptr) + , m_out(nullptr) + , m_bars(0) { + if (collector) { + m_in = new double[collector->chunkSize()]; + } +}; + +CavaProcessor::~CavaProcessor() { + cleanup(); + 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) { + 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) { + 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 || !m_collector) { + return; + } + + 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"; + cleanup(); + return; + } + + m_out = new double[static_cast(m_bars)]; +} + +void CavaProcessor::process() { + if (!m_plan || m_bars == 0 || !m_collector || !m_in || !m_out) { + return; + } + + const int count = static_cast(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 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); + init(); + + connect(static_cast(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, 0.0); + m_bars = bars; + emit barsChanged(); + emit valuesChanged(); + + QMetaObject::invokeMethod(m_processor, "setBars", Qt::QueuedConnection, Q_ARG(int, bars)); +} + +QVector CavaProvider::values() const { + return m_values; +} + +void CavaProvider::updateValues(QVector values) { + if (values != m_values) { + m_values = values; + emit valuesChanged(); + } +} + +} // namespace caelestia diff --git a/plugin/src/Caelestia/Services/cavaprovider.hpp b/plugin/src/Caelestia/Services/cavaprovider.hpp new file mode 100644 index 0000000..6dab635 --- /dev/null +++ b/plugin/src/Caelestia/Services/cavaprovider.hpp @@ -0,0 +1,67 @@ +#pragma once + +#include "audiocollector.hpp" +#include "audioprovider.hpp" +#include +#include + +namespace caelestia { + +class CavaProcessor : public AudioProcessor { + Q_OBJECT + +public: + 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; + double* m_out; + + int m_bars; + QVector 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 values READ values NOTIFY valuesChanged) + +public: + explicit CavaProvider(QObject* parent = nullptr); + + [[nodiscard]] int bars() const; + void setBars(int bars); + + [[nodiscard]] QVector values() const; + +signals: + void barsChanged(); + void valuesChanged(); + +private: + int m_bars; + QVector m_values; + + void updateValues(QVector values); +}; + +} // namespace caelestia diff --git a/plugin/src/Caelestia/Services/service.cpp b/plugin/src/Caelestia/Services/service.cpp new file mode 100644 index 0000000..bc919c9 --- /dev/null +++ b/plugin/src/Caelestia/Services/service.cpp @@ -0,0 +1,70 @@ +#include "service.hpp" + +#include +#include + +namespace caelestia { + +Service::Service(QObject* parent) + : 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++; + } + emit refCountChanged(); + + if (needsStart) { + const QPointer 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; + } + } + emit refCountChanged(); + + if (needsStop) { + const QPointer self(this); + QMetaObject::invokeMethod( + this, + [self]() { + if (self) { + self->stop(); + } + }, + Qt::QueuedConnection); + } +} + +} // namespace caelestia diff --git a/plugin/src/Caelestia/Services/service.hpp b/plugin/src/Caelestia/Services/service.hpp new file mode 100644 index 0000000..787818b --- /dev/null +++ b/plugin/src/Caelestia/Services/service.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include +#include + +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() const; + + void ref(); + void unref(); + +signals: + void refCountChanged(); + +private: + int m_refCount; + mutable QMutex m_mutex; + + virtual void start() = 0; + virtual void stop() = 0; +}; + +} // namespace caelestia diff --git a/plugin/src/Caelestia/Services/serviceref.cpp b/plugin/src/Caelestia/Services/serviceref.cpp new file mode 100644 index 0000000..dc82811 --- /dev/null +++ b/plugin/src/Caelestia/Services/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/Services/serviceref.hpp b/plugin/src/Caelestia/Services/serviceref.hpp new file mode 100644 index 0000000..072419e --- /dev/null +++ b/plugin/src/Caelestia/Services/serviceref.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include "service.hpp" +#include + +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 diff --git a/plugin/src/Caelestia/audiocollector.cpp b/plugin/src/Caelestia/audiocollector.cpp deleted file mode 100644 index 9dc3871..0000000 --- a/plugin/src/Caelestia/audiocollector.cpp +++ /dev/null @@ -1,275 +0,0 @@ -#include "audiocollector.hpp" - -#include "service.hpp" -#include -#include -#include -#include -#include -#include -#include -#include - -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"; - pw_deinit(); - 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 buffer(collector->chunkSize()); - spa_pod_builder b; - spa_pod_builder_init(&b, buffer.data(), static_cast(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(data); - self->streamStateChanged(state); - }; - events.process = [](void* data) { - auto* self = static_cast(data); - self->processStream(); - }; - - 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(), - 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); - - 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(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 qint16* samples = reinterpret_cast(buf->datas[0].data); - if (samples == nullptr) { - return; - } - - const quint32 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(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(); - } -} - -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 qint16* samples, quint32 count) { - if (count > m_chunkSize) { - count = m_chunkSize; - } - - auto* writeBuffer = m_writeBuffer.load(std::memory_order_relaxed); - std::transform(samples, samples + count, writeBuffer->begin(), [](qint16 sample) { - return sample / 32768.0f; - }); - - auto* oldRead = m_readBuffer.exchange(writeBuffer, std::memory_order_acq_rel); - m_writeBuffer.store(oldRead, std::memory_order_release); -} - -quint32 AudioCollector::readChunk(float* out, quint32 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; -} - -quint32 AudioCollector::readChunk(double* out, quint32 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(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 deleted file mode 100644 index 74b0877..0000000 --- a/plugin/src/Caelestia/audiocollector.hpp +++ /dev/null @@ -1,86 +0,0 @@ -#pragma once - -#include "service.hpp" -#include -#include -#include -#include -#include -#include -#include -#include - -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 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(); - - [[nodiscard]] quint32 sampleRate() const; - [[nodiscard]] quint32 chunkSize() const; - - [[nodiscard]] quint32 nodeId(); - void setNodeId(quint32 nodeId); - - 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; - - std::jthread m_thread; - std::vector m_buffer1; - std::vector m_buffer2; - std::atomic*> m_readBuffer; - std::atomic*> m_writeBuffer; - quint32 m_sampleCount; - - void reload(); - void start() override; - void stop() override; -}; - -} // namespace caelestia diff --git a/plugin/src/Caelestia/audioprovider.cpp b/plugin/src/Caelestia/audioprovider.cpp deleted file mode 100644 index f31480e..0000000 --- a/plugin/src/Caelestia/audioprovider.cpp +++ /dev/null @@ -1,122 +0,0 @@ -#include "audioprovider.hpp" - -#include "audiocollector.hpp" -#include "service.hpp" -#include -#include - -namespace caelestia { - -AudioProcessor::AudioProcessor(AudioCollector* collector, QObject* parent) - : QObject(parent) - , m_collector(collector) {} - -AudioProcessor::~AudioProcessor() { - stop(); -} - -void AudioProcessor::init() { - m_timer = new QTimer(this); - 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::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 && m_collector) { - m_timer->stop(); - m_collector->unref(); - } -} - -AudioProvider::AudioProvider(QObject* parent) - : Service(parent) - , m_collector(nullptr) - , m_processor(nullptr) - , m_thread(nullptr) {} - -AudioProvider::~AudioProvider() { - if (m_thread) { - m_thread->quit(); - m_thread->wait(); - } -} - -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"; - 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 deleted file mode 100644 index c92b965..0000000 --- a/plugin/src/Caelestia/audioprovider.hpp +++ /dev/null @@ -1,61 +0,0 @@ -#pragma once - -#include "audiocollector.hpp" -#include "service.hpp" -#include -#include - -namespace caelestia { - -class AudioProcessor : public QObject { - Q_OBJECT - -public: - explicit AudioProcessor(AudioCollector* collector, QObject* parent = nullptr); - ~AudioProcessor(); - - void init(); - -protected: - AudioCollector* m_collector; - - Q_INVOKABLE virtual void setCollector(AudioCollector* collector); - -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(); - -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 deleted file mode 100644 index 462abb8..0000000 --- a/plugin/src/Caelestia/beattracker.cpp +++ /dev/null @@ -1,82 +0,0 @@ -#include "beattracker.hpp" - -#include "audiocollector.hpp" -#include "audioprovider.hpp" -#include - -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() { - 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() { - if (!m_collector || !m_tempo || !m_in) { - return; - } - - m_collector->readChunk(m_in->data); - - aubio_tempo_do(m_tempo, m_in, m_out); - if (!qFuzzyIsNull(m_out->data[0])) { - emit beat(aubio_tempo_get_bpm(m_tempo)); - } -} - -BeatTracker::BeatTracker(QObject* parent) - : AudioProvider(parent) - , m_bpm(120) { - m_processor = new BeatProcessor(m_collector); - init(); - - connect(static_cast(m_processor), &BeatProcessor::beat, this, &BeatTracker::updateBpm); -} - -smpl_t BeatTracker::bpm() const { - return m_bpm; -} - -void BeatTracker::updateBpm(smpl_t bpm) { - if (!qFuzzyCompare(bpm + 1.0f, m_bpm + 1.0f)) { - m_bpm = bpm; - emit bpmChanged(); - } -} - -} // namespace caelestia diff --git a/plugin/src/Caelestia/beattracker.hpp b/plugin/src/Caelestia/beattracker.hpp deleted file mode 100644 index ab18373..0000000 --- a/plugin/src/Caelestia/beattracker.hpp +++ /dev/null @@ -1,52 +0,0 @@ -#pragma once - -#include "audiocollector.hpp" -#include "audioprovider.hpp" -#include -#include - -namespace caelestia { - -class BeatProcessor : public AudioProcessor { - Q_OBJECT - -public: - 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; - fvec_t* m_out; - - void process() override; -}; - -class BeatTracker : public AudioProvider { - Q_OBJECT - QML_ELEMENT - - Q_PROPERTY(smpl_t bpm READ bpm NOTIFY bpmChanged) - -public: - explicit BeatTracker(QObject* parent = nullptr); - - [[nodiscard]] smpl_t bpm() const; - -signals: - void bpmChanged(); - void beat(smpl_t bpm); - -private: - smpl_t m_bpm; - - void updateBpm(smpl_t bpm); -}; - -} // namespace caelestia diff --git a/plugin/src/Caelestia/cachingimagemanager.cpp b/plugin/src/Caelestia/cachingimagemanager.cpp deleted file mode 100644 index 3394f89..0000000 --- a/plugin/src/Caelestia/cachingimagemanager.cpp +++ /dev/null @@ -1,223 +0,0 @@ -#include "cachingimagemanager.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include - -namespace caelestia { - -qreal CachingImageManager::effectiveScale() const { - if (m_item && m_item->window()) { - return m_item->window()->devicePixelRatio(); - } - - return 1.0; -} - -QSize CachingImageManager::effectiveSize() const { - if (!m_item) { - return QSize(); - } - - const qreal scale = effectiveScale(); - const QSize size = QSizeF(m_item->width() * scale, m_item->height() * scale).toSize(); - m_item->setProperty("sourceSize", size); - return size; -} - -QQuickItem* CachingImageManager::item() const { - return m_item; -} - -void CachingImageManager::setItem(QQuickItem* item) { - if (m_item == item) { - return; - } - - if (m_widthConn) { - disconnect(m_widthConn); - } - if (m_heightConn) { - disconnect(m_heightConn); - } - - m_item = item; - emit itemChanged(); - - if (item) { - m_widthConn = connect(item, &QQuickItem::widthChanged, this, [this]() { - updateSource(); - }); - m_heightConn = connect(item, &QQuickItem::heightChanged, this, [this]() { - updateSource(); - }); - updateSource(); - } -} - -QUrl CachingImageManager::cacheDir() const { - return m_cacheDir; -} - -void CachingImageManager::setCacheDir(const QUrl& cacheDir) { - if (m_cacheDir == cacheDir) { - return; - } - - m_cacheDir = cacheDir; - if (!m_cacheDir.path().endsWith("/")) { - m_cacheDir.setPath(m_cacheDir.path() + "/"); - } - emit cacheDirChanged(); -} - -QString CachingImageManager::path() const { - return m_path; -} - -void CachingImageManager::setPath(const QString& path) { - if (m_path == path) { - return; - } - - m_path = path; - emit pathChanged(); - - if (!path.isEmpty()) { - updateSource(path); - } -} - -void CachingImageManager::updateSource() { - updateSource(m_path); -} - -void CachingImageManager::updateSource(const QString& path) { - if (path.isEmpty() || path == m_shaPath) { - // Path is empty or already calculating sha for path - return; - } - - m_shaPath = path; - - const auto future = QtConcurrent::run(&CachingImageManager::sha256sum, path); - - const auto watcher = new QFutureWatcher(this); - - connect(watcher, &QFutureWatcher::finished, this, [watcher, path, this]() { - if (m_path != path) { - // Object is destroyed or path has changed, ignore - watcher->deleteLater(); - return; - } - - const QSize size = effectiveSize(); - - if (!m_item || !size.width() || !size.height()) { - watcher->deleteLater(); - return; - } - - const QString fillMode = m_item->property("fillMode").toString(); - // clang-format off - const QString filename = QString("%1@%2x%3-%4.png") - .arg(watcher->result()).arg(size.width()).arg(size.height()) - .arg(fillMode == "PreserveAspectCrop" ? "crop" : fillMode == "PreserveAspectFit" ? "fit" : "stretch"); - // clang-format on - - const QUrl cache = m_cacheDir.resolved(QUrl(filename)); - if (m_cachePath == cache) { - watcher->deleteLater(); - return; - } - - m_cachePath = cache; - emit cachePathChanged(); - - if (!cache.isLocalFile()) { - qWarning() << "CachingImageManager::updateSource: cachePath" << cache << "is not a local file"; - watcher->deleteLater(); - return; - } - - const QImageReader reader(cache.toLocalFile()); - if (reader.canRead()) { - m_item->setProperty("source", cache); - } else { - m_item->setProperty("source", QUrl::fromLocalFile(path)); - createCache(path, cache.toLocalFile(), fillMode, size); - } - - // Clear current running sha if same - if (m_shaPath == path) { - m_shaPath = QString(); - } - - watcher->deleteLater(); - }); - - watcher->setFuture(future); -} - -QUrl CachingImageManager::cachePath() const { - return m_cachePath; -} - -void CachingImageManager::createCache( - const QString& path, const QString& cache, const QString& fillMode, const QSize& size) const { - QThreadPool::globalInstance()->start([path, cache, fillMode, size] { - QImage image(path); - - if (image.isNull()) { - qWarning() << "CachingImageManager::createCache: failed to read" << path; - return; - } - - image.convertTo(QImage::Format_ARGB32); - - if (fillMode == "PreserveAspectCrop") { - image = image.scaled(size, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation); - } else if (fillMode == "PreserveAspectFit") { - image = image.scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation); - } else { - image = image.scaled(size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); - } - - if (fillMode == "PreserveAspectCrop" || fillMode == "PreserveAspectFit") { - QImage canvas(size, QImage::Format_ARGB32); - canvas.fill(Qt::transparent); - - QPainter painter(&canvas); - painter.drawImage((size.width() - image.width()) / 2, (size.height() - image.height()) / 2, image); - painter.end(); - - image = canvas; - } - - const QString parent = QFileInfo(cache).absolutePath(); - if (!QDir().mkpath(parent) || !image.save(cache)) { - qWarning() << "CachingImageManager::createCache: failed to save to" << cache; - } - }); -} - -QString CachingImageManager::sha256sum(const QString& path) { - QFile file(path); - if (!file.open(QIODevice::ReadOnly)) { - qWarning() << "CachingImageManager::sha256sum: failed to open" << path; - return ""; - } - - QCryptographicHash hash(QCryptographicHash::Sha256); - hash.addData(&file); - file.close(); - - return hash.result().toHex(); -} - -} // namespace caelestia diff --git a/plugin/src/Caelestia/cachingimagemanager.hpp b/plugin/src/Caelestia/cachingimagemanager.hpp deleted file mode 100644 index f05ea34..0000000 --- a/plugin/src/Caelestia/cachingimagemanager.hpp +++ /dev/null @@ -1,65 +0,0 @@ -#pragma once - -#include -#include -#include - -namespace caelestia { - -class CachingImageManager : public QObject { - Q_OBJECT - QML_ELEMENT - - Q_PROPERTY(QQuickItem* item READ item WRITE setItem NOTIFY itemChanged REQUIRED) - Q_PROPERTY(QUrl cacheDir READ cacheDir WRITE setCacheDir NOTIFY cacheDirChanged REQUIRED) - - Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged) - Q_PROPERTY(QUrl cachePath READ cachePath NOTIFY cachePathChanged) - -public: - explicit CachingImageManager(QObject* parent = nullptr) - : QObject(parent) - , m_item(nullptr) {} - - [[nodiscard]] QQuickItem* item() const; - void setItem(QQuickItem* item); - - [[nodiscard]] QUrl cacheDir() const; - void setCacheDir(const QUrl& cacheDir); - - [[nodiscard]] QString path() const; - void setPath(const QString& path); - - [[nodiscard]] QUrl cachePath() const; - - Q_INVOKABLE void updateSource(); - Q_INVOKABLE void updateSource(const QString& path); - -signals: - void itemChanged(); - void cacheDirChanged(); - - void pathChanged(); - void cachePathChanged(); - void usingCacheChanged(); - -private: - QString m_shaPath; - - QQuickItem* m_item; - QUrl m_cacheDir; - - QString m_path; - QUrl m_cachePath; - - QMetaObject::Connection m_widthConn; - QMetaObject::Connection m_heightConn; - - [[nodiscard]] qreal effectiveScale() const; - [[nodiscard]] QSize effectiveSize() const; - - void createCache(const QString& path, const QString& cache, const QString& fillMode, const QSize& size) const; - [[nodiscard]] static QString sha256sum(const QString& path); -}; - -} // namespace caelestia diff --git a/plugin/src/Caelestia/cavaprovider.cpp b/plugin/src/Caelestia/cavaprovider.cpp deleted file mode 100644 index 76a1a10..0000000 --- a/plugin/src/Caelestia/cavaprovider.cpp +++ /dev/null @@ -1,163 +0,0 @@ -#include "cavaprovider.hpp" - -#include "audiocollector.hpp" -#include "audioprovider.hpp" -#include -#include -#include -#include - -namespace caelestia { - -CavaProcessor::CavaProcessor(AudioCollector* collector, QObject* parent) - : AudioProcessor(collector, parent) - , m_plan(nullptr) - , m_in(nullptr) - , m_out(nullptr) - , m_bars(0) { - if (collector) { - m_in = new double[collector->chunkSize()]; - } -}; - -CavaProcessor::~CavaProcessor() { - cleanup(); - 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) { - 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) { - 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 || !m_collector) { - return; - } - - 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"; - cleanup(); - return; - } - - m_out = new double[static_cast(m_bars)]; -} - -void CavaProcessor::process() { - if (!m_plan || m_bars == 0 || !m_collector || !m_in || !m_out) { - return; - } - - const int count = static_cast(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 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); - init(); - - connect(static_cast(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, 0.0); - m_bars = bars; - emit barsChanged(); - emit valuesChanged(); - - QMetaObject::invokeMethod(m_processor, "setBars", Qt::QueuedConnection, Q_ARG(int, bars)); -} - -QVector CavaProvider::values() const { - return m_values; -} - -void CavaProvider::updateValues(QVector 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 deleted file mode 100644 index 6dab635..0000000 --- a/plugin/src/Caelestia/cavaprovider.hpp +++ /dev/null @@ -1,67 +0,0 @@ -#pragma once - -#include "audiocollector.hpp" -#include "audioprovider.hpp" -#include -#include - -namespace caelestia { - -class CavaProcessor : public AudioProcessor { - Q_OBJECT - -public: - 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; - double* m_out; - - int m_bars; - QVector 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 values READ values NOTIFY valuesChanged) - -public: - explicit CavaProvider(QObject* parent = nullptr); - - [[nodiscard]] int bars() const; - void setBars(int bars); - - [[nodiscard]] QVector values() const; - -signals: - void barsChanged(); - void valuesChanged(); - -private: - int m_bars; - QVector m_values; - - void updateValues(QVector values); -}; - -} // namespace caelestia diff --git a/plugin/src/Caelestia/filesystemmodel.cpp b/plugin/src/Caelestia/filesystemmodel.cpp deleted file mode 100644 index 54807b5..0000000 --- a/plugin/src/Caelestia/filesystemmodel.cpp +++ /dev/null @@ -1,430 +0,0 @@ -#include "filesystemmodel.hpp" - -#include -#include -#include - -namespace caelestia { - -FileSystemEntry::FileSystemEntry(const QString& path, const QString& relativePath, QObject* parent) - : QObject(parent) - , m_fileInfo(QFileInfo(path)) - , m_path(path) - , m_relativePath(relativePath) - , m_isImageInitialised(false) - , m_mimeTypeInitialised(false) {} - -QString FileSystemEntry::path() const { - return m_path; -}; - -QString FileSystemEntry::relativePath() const { - return m_relativePath; -}; - -QString FileSystemEntry::name() const { - return m_fileInfo.fileName(); -}; - -QString FileSystemEntry::parentDir() const { - return m_fileInfo.absolutePath(); -}; - -QString FileSystemEntry::suffix() const { - return m_fileInfo.completeSuffix(); -}; - -qint64 FileSystemEntry::size() const { - return m_fileInfo.size(); -}; - -bool FileSystemEntry::isDir() const { - return m_fileInfo.isDir(); -}; - -bool FileSystemEntry::isImage() const { - if (!m_isImageInitialised) { - QImageReader reader(m_path); - m_isImage = reader.canRead(); - m_isImageInitialised = true; - } - return m_isImage; -} - -QString FileSystemEntry::mimeType() const { - if (!m_mimeTypeInitialised) { - const QMimeDatabase db; - m_mimeType = db.mimeTypeForFile(m_path).name(); - m_mimeTypeInitialised = true; - } - return m_mimeType; -} - -FileSystemModel::FileSystemModel(QObject* parent) - : QAbstractListModel(parent) - , m_recursive(false) - , m_watchChanges(true) - , m_showHidden(false) - , m_filter(NoFilter) { - connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &FileSystemModel::watchDirIfRecursive); - connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &FileSystemModel::updateEntriesForDir); -} - -int FileSystemModel::rowCount(const QModelIndex& parent) const { - if (parent != QModelIndex()) { - return 0; - } - return static_cast(m_entries.size()); -} - -QVariant FileSystemModel::data(const QModelIndex& index, int role) const { - if (role != Qt::UserRole || !index.isValid() || index.row() >= m_entries.size()) { - return QVariant(); - } - return QVariant::fromValue(m_entries.at(index.row())); -} - -QHash FileSystemModel::roleNames() const { - return { { Qt::UserRole, "modelData" } }; -} - -QString FileSystemModel::path() const { - return m_path; -} - -void FileSystemModel::setPath(const QString& path) { - if (m_path == path) { - return; - } - - m_path = path; - emit pathChanged(); - - m_dir.setPath(m_path); - update(); -} - -bool FileSystemModel::recursive() const { - return m_recursive; -} - -void FileSystemModel::setRecursive(bool recursive) { - if (m_recursive == recursive) { - return; - } - - m_recursive = recursive; - emit recursiveChanged(); - - update(); -} - -bool FileSystemModel::watchChanges() const { - return m_watchChanges; -} - -void FileSystemModel::setWatchChanges(bool watchChanges) { - if (m_watchChanges == watchChanges) { - return; - } - - m_watchChanges = watchChanges; - emit watchChangesChanged(); - - update(); -} - -bool FileSystemModel::showHidden() const { - return m_showHidden; -} - -void FileSystemModel::setShowHidden(bool showHidden) { - if (m_showHidden == showHidden) { - return; - } - - m_showHidden = showHidden; - emit showHiddenChanged(); - - update(); -} - -FileSystemModel::Filter FileSystemModel::filter() const { - return m_filter; -} - -void FileSystemModel::setFilter(Filter filter) { - if (m_filter == filter) { - return; - } - - m_filter = filter; - emit filterChanged(); - - update(); -} - -QList FileSystemModel::entries() const { - return m_entries; -} - -void FileSystemModel::watchDirIfRecursive(const QString& path) { - if (m_recursive && m_watchChanges) { - const auto currentDir = m_dir; - const bool showHidden = m_showHidden; - const auto future = QtConcurrent::run([showHidden, path]() { - QDir::Filters filters = QDir::Dirs | QDir::NoDotAndDotDot; - if (showHidden) { - filters |= QDir::Hidden; - } - - QDirIterator iter(path, filters, QDirIterator::Subdirectories); - QStringList dirs; - while (iter.hasNext()) { - dirs << iter.next(); - } - return dirs; - }); - const auto watcher = new QFutureWatcher(this); - connect(watcher, &QFutureWatcher::finished, this, [currentDir, showHidden, watcher, this]() { - const auto paths = watcher->result(); - if (currentDir == m_dir && showHidden == m_showHidden && !paths.isEmpty()) { - // Ignore if dir or showHidden has changed - m_watcher.addPaths(paths); - } - watcher->deleteLater(); - }); - watcher->setFuture(future); - } -} - -void FileSystemModel::update() { - updateWatcher(); - updateEntries(); -} - -void FileSystemModel::updateWatcher() { - if (!m_watcher.directories().isEmpty()) { - m_watcher.removePaths(m_watcher.directories()); - } - - if (!m_watchChanges || m_path.isEmpty()) { - return; - } - - m_watcher.addPath(m_path); - watchDirIfRecursive(m_path); -} - -void FileSystemModel::updateEntries() { - if (m_path.isEmpty()) { - if (!m_entries.isEmpty()) { - beginResetModel(); - qDeleteAll(m_entries); - m_entries.clear(); - emit entriesChanged(); - endResetModel(); - } - - return; - } - - for (auto& future : m_futures) { - future.cancel(); - } - m_futures.clear(); - - updateEntriesForDir(m_path); -} - -void FileSystemModel::updateEntriesForDir(const QString& dir) { - const bool recursive = m_recursive; - const bool showHidden = m_showHidden; - const auto filter = m_filter; - const auto oldEntries = m_entries; - const auto baseDir = m_dir; - - const auto future = QtConcurrent::run([dir, recursive, showHidden, filter, oldEntries, baseDir]( - QPromise, QSet>>& promise) { - const auto flags = recursive ? QDirIterator::Subdirectories : QDirIterator::NoIteratorFlags; - - std::optional iter; - - if (filter == Images) { - QStringList nameFilters; - for (const auto& format : QImageReader::supportedImageFormats()) { - nameFilters << "*." + format; - } - - QDir::Filters filters = QDir::Files; - if (showHidden) { - filters |= QDir::Hidden; - } - - iter.emplace(dir, nameFilters, filters, flags); - } else { - QDir::Filters filters; - - if (filter == Files) { - filters = QDir::Files; - } else if (filter == Dirs) { - filters = QDir::Dirs | QDir::NoDotAndDotDot; - } else { - filters = QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot; - } - - if (showHidden) { - filters |= QDir::Hidden; - } - - iter.emplace(dir, filters, flags); - } - - QSet newPaths; - while (iter->hasNext()) { - if (promise.isCanceled()) { - return; - } - - QString path = iter->next(); - - if (filter == Images) { - QImageReader reader(path); - if (!reader.canRead()) { - continue; - } - } - - newPaths.insert(path); - } - - QSet oldPaths; - for (const auto& entry : oldEntries) { - oldPaths.insert(entry->path()); - } - - if (promise.isCanceled() || newPaths == oldPaths) { - return; - } - - promise.addResult(qMakePair(oldPaths - newPaths, newPaths - oldPaths)); - }); - - if (m_futures.contains(dir)) { - m_futures[dir].cancel(); - } - m_futures.insert(dir, future); - - const auto watcher = new QFutureWatcher, QSet>>(this); - - connect(watcher, &QFutureWatcher, QSet>>::finished, this, [dir, watcher, this]() { - m_futures.remove(dir); - - if (!watcher->future().isResultReadyAt(0)) { - watcher->deleteLater(); - return; - } - - const auto result = watcher->result(); - applyChanges(result.first, result.second); - - watcher->deleteLater(); - }); - - watcher->setFuture(future); -} - -void FileSystemModel::applyChanges(const QSet& removedPaths, const QSet& addedPaths) { - QList removedIndices; - for (int i = 0; i < m_entries.size(); ++i) { - if (removedPaths.contains(m_entries[i]->path())) { - removedIndices << i; - } - } - std::sort(removedIndices.begin(), removedIndices.end(), std::greater()); - - int start = -1; - int end = -1; - for (int idx : removedIndices) { - if (start == -1) { - start = idx; - end = idx; - } else if (idx == end - 1) { - end = idx; - } else { - beginRemoveRows(QModelIndex(), end, start); - for (int i = start; i >= end; --i) { - emit removed(m_entries[i]->path()); - delete m_entries.takeAt(i); - } - endRemoveRows(); - - start = idx; - end = idx; - } - } - if (start != -1) { - beginRemoveRows(QModelIndex(), end, start); - for (int i = start; i >= end; --i) { - emit removed(m_entries[i]->path()); - delete m_entries.takeAt(i); - } - endRemoveRows(); - } - - QList newEntries; - for (const auto& path : addedPaths) { - newEntries << new FileSystemEntry(path, m_dir.relativeFilePath(path), this); - } - std::sort(newEntries.begin(), newEntries.end(), &FileSystemModel::compareEntries); - - int insertStart = -1; - int prevRow = -1; - QList batchItems; - for (const auto& entry : newEntries) { - const auto it = std::lower_bound(m_entries.begin(), m_entries.end(), entry, &FileSystemModel::compareEntries); - int row = static_cast(it - m_entries.begin()); - - if (insertStart == -1) { - insertStart = row; - prevRow = row; - batchItems.clear(); - batchItems << entry; - } else if (row == prevRow + 1) { - prevRow = row; - batchItems << entry; - } else { - beginInsertRows(QModelIndex(), insertStart, static_cast(insertStart + batchItems.size() - 1)); - for (int i = 0; i < batchItems.size(); ++i) { - m_entries.insert(insertStart + i, batchItems[i]); - emit added(batchItems[i]); - } - endInsertRows(); - - insertStart = row; - prevRow = row; - batchItems.clear(); - batchItems << entry; - } - prevRow = static_cast(m_entries.indexOf(entry)); - } - if (!batchItems.isEmpty()) { - beginInsertRows(QModelIndex(), insertStart, static_cast(insertStart + batchItems.size() - 1)); - for (int i = 0; i < batchItems.size(); ++i) { - m_entries.insert(insertStart + i, batchItems[i]); - emit added(batchItems[i]); - } - endInsertRows(); - } - - emit entriesChanged(); -} - -bool FileSystemModel::compareEntries(const FileSystemEntry* a, const FileSystemEntry* b) { - if (a->isDir() != b->isDir()) { - return a->isDir(); - } - return a->relativePath().localeAwareCompare(b->relativePath()) < 0; -} - -} // namespace caelestia diff --git a/plugin/src/Caelestia/filesystemmodel.hpp b/plugin/src/Caelestia/filesystemmodel.hpp deleted file mode 100644 index 4ea5f0a..0000000 --- a/plugin/src/Caelestia/filesystemmodel.hpp +++ /dev/null @@ -1,131 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include - -namespace caelestia { - -class FileSystemEntry : public QObject { - Q_OBJECT - QML_ELEMENT - QML_UNCREATABLE("FileSystemEntry instances can only be retrieved from a FileSystemModel") - - Q_PROPERTY(QString path READ path CONSTANT) - Q_PROPERTY(QString relativePath READ relativePath CONSTANT) - Q_PROPERTY(QString name READ name CONSTANT) - Q_PROPERTY(QString parentDir READ parentDir CONSTANT) - Q_PROPERTY(QString suffix READ suffix CONSTANT) - Q_PROPERTY(qint64 size READ size CONSTANT) - Q_PROPERTY(bool isDir READ isDir CONSTANT) - Q_PROPERTY(bool isImage READ isImage CONSTANT) - Q_PROPERTY(QString mimeType READ mimeType CONSTANT) - -public: - explicit FileSystemEntry(const QString& path, const QString& relativePath, QObject* parent = nullptr); - - [[nodiscard]] QString path() const; - [[nodiscard]] QString relativePath() const; - [[nodiscard]] QString name() const; - [[nodiscard]] QString parentDir() const; - [[nodiscard]] QString suffix() const; - [[nodiscard]] qint64 size() const; - [[nodiscard]] bool isDir() const; - [[nodiscard]] bool isImage() const; - [[nodiscard]] QString mimeType() const; - -private: - const QFileInfo m_fileInfo; - - const QString m_path; - const QString m_relativePath; - - mutable bool m_isImage; - mutable bool m_isImageInitialised; - - mutable QString m_mimeType; - mutable bool m_mimeTypeInitialised; -}; - -class FileSystemModel : public QAbstractListModel { - Q_OBJECT - QML_ELEMENT - - Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged) - Q_PROPERTY(bool recursive READ recursive WRITE setRecursive NOTIFY recursiveChanged) - Q_PROPERTY(bool watchChanges READ watchChanges WRITE setWatchChanges NOTIFY watchChangesChanged) - Q_PROPERTY(bool showHidden READ showHidden WRITE setShowHidden NOTIFY showHiddenChanged) - Q_PROPERTY(Filter filter READ filter WRITE setFilter NOTIFY filterChanged) - - Q_PROPERTY(QList entries READ entries NOTIFY entriesChanged) - -public: - enum Filter { - NoFilter, - Images, - Files, - Dirs - }; - Q_ENUM(Filter) - - explicit FileSystemModel(QObject* parent = nullptr); - - int rowCount(const QModelIndex& parent = QModelIndex()) const override; - QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; - QHash roleNames() const override; - - [[nodiscard]] QString path() const; - void setPath(const QString& path); - - [[nodiscard]] bool recursive() const; - void setRecursive(bool recursive); - - [[nodiscard]] bool watchChanges() const; - void setWatchChanges(bool watchChanges); - - [[nodiscard]] bool showHidden() const; - void setShowHidden(bool showHidden); - - [[nodiscard]] Filter filter() const; - void setFilter(Filter filter); - - [[nodiscard]] QList entries() const; - -signals: - void pathChanged(); - void recursiveChanged(); - void watchChangesChanged(); - void showHiddenChanged(); - void filterChanged(); - void entriesChanged(); - - void added(const FileSystemEntry* entry); - void removed(const QString& path); - -private: - QDir m_dir; - QFileSystemWatcher m_watcher; - QList m_entries; - QHash, QSet>>> m_futures; - - QString m_path; - bool m_recursive; - bool m_watchChanges; - bool m_showHidden; - Filter m_filter; - - void watchDirIfRecursive(const QString& path); - void update(); - void updateWatcher(); - void updateEntries(); - void updateEntriesForDir(const QString& dir); - void applyChanges(const QSet& removedPaths, const QSet& addedPaths); - [[nodiscard]] static bool compareEntries(const FileSystemEntry* a, const FileSystemEntry* b); -}; - -} // namespace caelestia diff --git a/plugin/src/Caelestia/service.cpp b/plugin/src/Caelestia/service.cpp deleted file mode 100644 index bc919c9..0000000 --- a/plugin/src/Caelestia/service.cpp +++ /dev/null @@ -1,70 +0,0 @@ -#include "service.hpp" - -#include -#include - -namespace caelestia { - -Service::Service(QObject* parent) - : 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++; - } - emit refCountChanged(); - - if (needsStart) { - const QPointer 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; - } - } - emit refCountChanged(); - - if (needsStop) { - const QPointer self(this); - QMetaObject::invokeMethod( - this, - [self]() { - if (self) { - self->stop(); - } - }, - Qt::QueuedConnection); - } -} - -} // namespace caelestia diff --git a/plugin/src/Caelestia/service.hpp b/plugin/src/Caelestia/service.hpp deleted file mode 100644 index 787818b..0000000 --- a/plugin/src/Caelestia/service.hpp +++ /dev/null @@ -1,32 +0,0 @@ -#pragma once - -#include -#include - -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() const; - - void ref(); - void unref(); - -signals: - void refCountChanged(); - -private: - int m_refCount; - mutable 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 deleted file mode 100644 index dc82811..0000000 --- a/plugin/src/Caelestia/serviceref.cpp +++ /dev/null @@ -1,42 +0,0 @@ -#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 deleted file mode 100644 index 072419e..0000000 --- a/plugin/src/Caelestia/serviceref.hpp +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once - -#include "service.hpp" -#include - -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 -- cgit v1.2.3-freya