From d5afda9d953f423fb88100e0d496db87a0e3e47d Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 4 Sep 2025 22:38:23 +1000 Subject: plugin: add BeatTracker Replaces beat-detector.cpp --- components/misc/Ref.qml | 3 +- modules/dashboard/Media.qml | 7 ++- modules/dashboard/dash/Media.qml | 8 ++- nix/default.nix | 2 +- plugin/CMakeLists.txt | 2 +- plugin/src/Caelestia/CMakeLists.txt | 9 ++- plugin/src/Caelestia/beattracker.cpp | 106 +++++++++++++++++++++++++++++++++++ plugin/src/Caelestia/beattracker.hpp | 47 ++++++++++++++++ services/BeatDetector.qml | 23 -------- 9 files changed, 176 insertions(+), 31 deletions(-) create mode 100644 plugin/src/Caelestia/beattracker.cpp create mode 100644 plugin/src/Caelestia/beattracker.hpp delete mode 100644 services/BeatDetector.qml diff --git a/components/misc/Ref.qml b/components/misc/Ref.qml index 679f52f..0a694a4 100644 --- a/components/misc/Ref.qml +++ b/components/misc/Ref.qml @@ -1,8 +1,7 @@ -import Quickshell import QtQuick QtObject { - required property Singleton service + required property var service Component.onCompleted: service.refCount++ Component.onDestruction: service.refCount-- diff --git a/modules/dashboard/Media.qml b/modules/dashboard/Media.qml index 937e07e..eeebaf2 100644 --- a/modules/dashboard/Media.qml +++ b/modules/dashboard/Media.qml @@ -7,6 +7,7 @@ import qs.components.controls import qs.services import qs.utils import qs.config +import Caelestia import Quickshell import Quickshell.Widgets import Quickshell.Services.Mpris @@ -58,6 +59,10 @@ Item { service: Cava } + Ref { + service: BeatTracker + } + Shape { id: visualiser @@ -525,7 +530,7 @@ Item { height: visualiser.height * 0.75 playing: Players.active?.isPlaying ?? false - speed: BeatDetector.bpm / 300 + speed: BeatTracker.bpm / 300 source: Paths.absolutePath(Config.paths.mediaGif) asynchronous: true fillMode: AnimatedImage.PreserveAspectFit diff --git a/modules/dashboard/dash/Media.qml b/modules/dashboard/dash/Media.qml index 7cb9e3e..657c364 100644 --- a/modules/dashboard/dash/Media.qml +++ b/modules/dashboard/dash/Media.qml @@ -1,7 +1,9 @@ import qs.components +import qs.components.misc import qs.services import qs.config import qs.utils +import Caelestia import QtQuick import QtQuick.Shapes @@ -31,6 +33,10 @@ Item { onTriggered: Players.active?.positionChanged() } + Ref { + service: BeatTracker + } + Shape { preferredRendererType: Shape.CurveRenderer @@ -208,7 +214,7 @@ Item { anchors.margins: Appearance.padding.large * 2 playing: Players.active?.isPlaying ?? false - speed: BeatDetector.bpm / 300 + speed: BeatTracker.bpm / 300 source: Paths.absolutePath(Config.paths.mediaGif) asynchronous: true fillMode: AnimatedImage.PreserveAspectFit diff --git a/nix/default.nix b/nix/default.nix index d6bc88f..bc9f018 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -88,7 +88,7 @@ }; nativeBuildInputs = [cmake ninja pkg-config]; - buildInputs = [qt6.qtbase qt6.qtdeclarative libqalculate]; + buildInputs = [qt6.qtbase qt6.qtdeclarative qt6.multimedia libqalculate aubio]; dontWrapQtApps = true; cmakeFlags = diff --git a/plugin/CMakeLists.txt b/plugin/CMakeLists.txt index 9d5f128..0228765 100644 --- a/plugin/CMakeLists.txt +++ b/plugin/CMakeLists.txt @@ -1,4 +1,4 @@ -find_package(Qt6 REQUIRED COMPONENTS Core Qml Gui Concurrent) +find_package(Qt6 REQUIRED COMPONENTS Core Qml Gui Concurrent Multimedia) if(QT_KNOWN_POLICY_QTP0001) qt_policy(SET QTP0001 NEW) diff --git a/plugin/src/Caelestia/CMakeLists.txt b/plugin/src/Caelestia/CMakeLists.txt index 6461027..cc8e567 100644 --- a/plugin/src/Caelestia/CMakeLists.txt +++ b/plugin/src/Caelestia/CMakeLists.txt @@ -1,5 +1,6 @@ find_package(PkgConfig REQUIRED) pkg_check_modules(QALCULATE REQUIRED libqalculate) +pkg_check_modules(AUBIO REQUIRED aubio) qt_add_qml_module(caelestia URI Caelestia @@ -9,6 +10,7 @@ qt_add_qml_module(caelestia cachingimagemanager.hpp cachingimagemanager.cpp filesystemmodel.hpp filesystemmodel.cpp qalculator.hpp qalculator.cpp + beattracker.hpp beattracker.cpp ) qt_query_qml_module(caelestia @@ -28,5 +30,8 @@ install(TARGETS "${module_plugin_target}" LIBRARY DESTINATION "${module_dir}" RU install(FILES "${module_qmldir}" DESTINATION "${module_dir}") install(FILES "${module_typeinfo}" DESTINATION "${module_dir}") -target_include_directories(caelestia SYSTEM PRIVATE ${QALCULATE_INCLUDE_DIRS}) -target_link_libraries(caelestia PRIVATE Qt::Core Qt::Qml Qt::Gui Qt::Concurrent ${QALCULATE_LIBRARIES}) +target_include_directories(caelestia SYSTEM PRIVATE ${QALCULATE_INCLUDE_DIRS} ${AUBIO_INCLUDE_DIRS}) +target_link_libraries(caelestia PRIVATE + Qt::Core Qt::Qml Qt::Gui Qt::Concurrent Qt::Multimedia + ${QALCULATE_LIBRARIES} ${AUBIO_LIBRARIES} +) diff --git a/plugin/src/Caelestia/beattracker.cpp b/plugin/src/Caelestia/beattracker.cpp new file mode 100644 index 0000000..434c335 --- /dev/null +++ b/plugin/src/Caelestia/beattracker.cpp @@ -0,0 +1,106 @@ +#include "beattracker.hpp" + +#include +#include +#include +#include +#include +#include + +BeatTracker::BeatTracker(uint_t sampleRate, uint_t hopSize, QObject* parent) + : QObject(parent) + , m_tempo(new_aubio_tempo("default", 1024, hopSize, sampleRate)) + , m_in(new_fvec(hopSize)) + , m_out(new_fvec(2)) + , m_hopSize(hopSize) + , m_bpm(120) + , m_refCount(0) { + QAudioFormat format; + format.setSampleRate(static_cast(sampleRate)); + format.setChannelCount(1); + format.setSampleFormat(QAudioFormat::Int16); + + m_source = new QAudioSource(QMediaDevices::defaultAudioInput(), format, this); + connect(m_source, &QAudioSource::stateChanged, this, &BeatTracker::handleStateChanged); +}; + +BeatTracker::~BeatTracker() { + del_aubio_tempo(m_tempo); + del_fvec(m_in); + del_fvec(m_out); + + m_source->stop(); + delete m_source; +} + +smpl_t BeatTracker::bpm() const { + return m_bpm; +} + +int BeatTracker::refCount() const { + return m_refCount; +} + +void BeatTracker::setRefCount(int refCount) { + if (m_refCount == refCount) { + return; + } + + m_refCount = refCount; + emit refCountChanged(); + + if (m_refCount == 0) { + stop(); + } else if (!m_device) { + start(); + } +} + +void BeatTracker::start() { + m_device = m_source->start(); + connect(m_device, &QIODevice::readyRead, this, &BeatTracker::process); +} + +void BeatTracker::stop() { + m_source->stop(); + m_device = nullptr; +} + +void BeatTracker::process() { + const QByteArray data = m_device->readAll(); + const int16_t* samples = reinterpret_cast(data.constData()); + const size_t count = static_cast(data.size()) / sizeof(int16_t); + + for (size_t i = 0; i < count; ++i) { + m_in->data[i % m_hopSize] = samples[i] / 32768.0f; + if ((i + 1) % m_hopSize == 0) { + aubio_tempo_do(m_tempo, m_in, m_out); + if (m_out->data[0] != 0.0f) { + m_bpm = aubio_tempo_get_bpm(m_tempo); + emit bpmChanged(); + emit beat(m_bpm); + } + } + } +} + +void BeatTracker::handleStateChanged(QtAudio::State state) const { + if (state == QtAudio::StoppedState && m_source->error() != QtAudio::NoError) { + switch (m_source->error()) { + case QtAudio::OpenError: + qWarning() << "BeatTracker: failed to open audio device"; + break; + case QtAudio::IOError: + qWarning() << "BeatTracker: an error occurred during read/write of audio device"; + break; + case QtAudio::UnderrunError: + qWarning() << "BeatTracker: audio data is not being fed to audio device fast enough"; + break; + case QtAudio::FatalError: + qCritical() << "BeatTracker: fatal error in audio device"; + break; + default: + break; + } + } +} \ No newline at end of file diff --git a/plugin/src/Caelestia/beattracker.hpp b/plugin/src/Caelestia/beattracker.hpp new file mode 100644 index 0000000..a300c20 --- /dev/null +++ b/plugin/src/Caelestia/beattracker.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include +#include +#include +#include +#include + +class BeatTracker : public QObject { + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + + Q_PROPERTY(smpl_t bpm READ bpm NOTIFY bpmChanged) + Q_PROPERTY(int refCount READ refCount WRITE setRefCount NOTIFY refCountChanged) + +public: + explicit BeatTracker(uint_t sampleRate = 44100, uint_t hopSize = 512, QObject* parent = nullptr); + ~BeatTracker(); + + [[nodiscard]] smpl_t bpm() const; + + [[nodiscard]] int refCount() const; + void setRefCount(int refCount); + +signals: + void bpmChanged(); + void refCountChanged(); + void beat(smpl_t bpm); + +private: + QAudioSource* m_source; + QIODevice* m_device; + + aubio_tempo_t* m_tempo; + fvec_t* m_in; + fvec_t* m_out; + uint_t m_hopSize; + + smpl_t m_bpm; + int m_refCount; + + void start(); + void stop(); + void process(); + void handleStateChanged(QtAudio::State state) const; +}; diff --git a/services/BeatDetector.qml b/services/BeatDetector.qml deleted file mode 100644 index 24e19ec..0000000 --- a/services/BeatDetector.qml +++ /dev/null @@ -1,23 +0,0 @@ -pragma Singleton - -import qs.utils -import Quickshell -import Quickshell.Io - -Singleton { - id: root - - property real bpm: 150 - - Process { - running: true - command: [`${Paths.libdir}/beat_detector`, "--no-log", "--no-stats", "--no-visual"] - stdout: SplitParser { - onRead: data => { - const match = data.match(/BPM: ([0-9]+\.[0-9])/); - if (match) - root.bpm = parseFloat(match[1]); - } - } - } -} -- cgit v1.2.3-freya