summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author2 * r + 2 * t <61896496+soramanew@users.noreply.github.com>2025-09-04 22:38:23 +1000
committer2 * r + 2 * t <61896496+soramanew@users.noreply.github.com>2025-09-04 22:38:23 +1000
commitd5afda9d953f423fb88100e0d496db87a0e3e47d (patch)
treee6125d7740820c654eeb85336f43aafaba47719a
parentnix: remove unneeded deps (diff)
downloadcaelestia-shell-d5afda9d953f423fb88100e0d496db87a0e3e47d.tar.gz
caelestia-shell-d5afda9d953f423fb88100e0d496db87a0e3e47d.tar.bz2
caelestia-shell-d5afda9d953f423fb88100e0d496db87a0e3e47d.zip
plugin: add BeatTracker
Replaces beat-detector.cpp
-rw-r--r--components/misc/Ref.qml3
-rw-r--r--modules/dashboard/Media.qml7
-rw-r--r--modules/dashboard/dash/Media.qml8
-rw-r--r--nix/default.nix2
-rw-r--r--plugin/CMakeLists.txt2
-rw-r--r--plugin/src/Caelestia/CMakeLists.txt9
-rw-r--r--plugin/src/Caelestia/beattracker.cpp106
-rw-r--r--plugin/src/Caelestia/beattracker.hpp47
-rw-r--r--services/BeatDetector.qml23
9 files changed, 176 insertions, 31 deletions
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 <QAudioSource>
+#include <QDebug>
+#include <QIODevice>
+#include <QMediaDevices>
+#include <QObject>
+#include <aubio/aubio.h>
+
+BeatTracker::BeatTracker(uint_t sampleRate, uint_t hopSize, QObject* parent)
+ : QObject(parent)
+ , m_tempo(new_aubio_tempo("default", 1024, hopSize, sampleRate))
+ , m_in(new_fvec(hopSize))
+ , m_out(new_fvec(2))
+ , m_hopSize(hopSize)
+ , m_bpm(120)
+ , m_refCount(0) {
+ QAudioFormat format;
+ format.setSampleRate(static_cast<int>(sampleRate));
+ format.setChannelCount(1);
+ format.setSampleFormat(QAudioFormat::Int16);
+
+ 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<const int16_t*>(data.constData());
+ const size_t count = static_cast<size_t>(data.size()) / sizeof(int16_t);
+
+ for (size_t i = 0; i < count; ++i) {
+ m_in->data[i % m_hopSize] = samples[i] / 32768.0f;
+ if ((i + 1) % m_hopSize == 0) {
+ aubio_tempo_do(m_tempo, m_in, m_out);
+ if (m_out->data[0] != 0.0f) {
+ m_bpm = aubio_tempo_get_bpm(m_tempo);
+ emit bpmChanged();
+ emit beat(m_bpm);
+ }
+ }
+ }
+}
+
+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 <QAudioSource>
+#include <QIODevice>
+#include <QObject>
+#include <aubio/aubio.h>
+#include <qqmlintegration.h>
+
+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]);
- }
- }
- }
-}