plugin: add BeatTracker
Replaces beat-detector.cpp
This commit is contained in:
parent
ebc457b0cd
commit
d5afda9d95
9 changed files with 176 additions and 31 deletions
|
|
@ -1,8 +1,7 @@
|
||||||
import Quickshell
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
|
|
||||||
QtObject {
|
QtObject {
|
||||||
required property Singleton service
|
required property var service
|
||||||
|
|
||||||
Component.onCompleted: service.refCount++
|
Component.onCompleted: service.refCount++
|
||||||
Component.onDestruction: service.refCount--
|
Component.onDestruction: service.refCount--
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import qs.components.controls
|
||||||
import qs.services
|
import qs.services
|
||||||
import qs.utils
|
import qs.utils
|
||||||
import qs.config
|
import qs.config
|
||||||
|
import Caelestia
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Widgets
|
import Quickshell.Widgets
|
||||||
import Quickshell.Services.Mpris
|
import Quickshell.Services.Mpris
|
||||||
|
|
@ -58,6 +59,10 @@ Item {
|
||||||
service: Cava
|
service: Cava
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ref {
|
||||||
|
service: BeatTracker
|
||||||
|
}
|
||||||
|
|
||||||
Shape {
|
Shape {
|
||||||
id: visualiser
|
id: visualiser
|
||||||
|
|
||||||
|
|
@ -525,7 +530,7 @@ Item {
|
||||||
height: visualiser.height * 0.75
|
height: visualiser.height * 0.75
|
||||||
|
|
||||||
playing: Players.active?.isPlaying ?? false
|
playing: Players.active?.isPlaying ?? false
|
||||||
speed: BeatDetector.bpm / 300
|
speed: BeatTracker.bpm / 300
|
||||||
source: Paths.absolutePath(Config.paths.mediaGif)
|
source: Paths.absolutePath(Config.paths.mediaGif)
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
fillMode: AnimatedImage.PreserveAspectFit
|
fillMode: AnimatedImage.PreserveAspectFit
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
import qs.components
|
import qs.components
|
||||||
|
import qs.components.misc
|
||||||
import qs.services
|
import qs.services
|
||||||
import qs.config
|
import qs.config
|
||||||
import qs.utils
|
import qs.utils
|
||||||
|
import Caelestia
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Shapes
|
import QtQuick.Shapes
|
||||||
|
|
||||||
|
|
@ -31,6 +33,10 @@ Item {
|
||||||
onTriggered: Players.active?.positionChanged()
|
onTriggered: Players.active?.positionChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ref {
|
||||||
|
service: BeatTracker
|
||||||
|
}
|
||||||
|
|
||||||
Shape {
|
Shape {
|
||||||
preferredRendererType: Shape.CurveRenderer
|
preferredRendererType: Shape.CurveRenderer
|
||||||
|
|
||||||
|
|
@ -208,7 +214,7 @@ Item {
|
||||||
anchors.margins: Appearance.padding.large * 2
|
anchors.margins: Appearance.padding.large * 2
|
||||||
|
|
||||||
playing: Players.active?.isPlaying ?? false
|
playing: Players.active?.isPlaying ?? false
|
||||||
speed: BeatDetector.bpm / 300
|
speed: BeatTracker.bpm / 300
|
||||||
source: Paths.absolutePath(Config.paths.mediaGif)
|
source: Paths.absolutePath(Config.paths.mediaGif)
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
fillMode: AnimatedImage.PreserveAspectFit
|
fillMode: AnimatedImage.PreserveAspectFit
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,7 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
nativeBuildInputs = [cmake ninja pkg-config];
|
nativeBuildInputs = [cmake ninja pkg-config];
|
||||||
buildInputs = [qt6.qtbase qt6.qtdeclarative libqalculate];
|
buildInputs = [qt6.qtbase qt6.qtdeclarative qt6.multimedia libqalculate aubio];
|
||||||
|
|
||||||
dontWrapQtApps = true;
|
dontWrapQtApps = true;
|
||||||
cmakeFlags =
|
cmakeFlags =
|
||||||
|
|
|
||||||
|
|
@ -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)
|
if(QT_KNOWN_POLICY_QTP0001)
|
||||||
qt_policy(SET QTP0001 NEW)
|
qt_policy(SET QTP0001 NEW)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
find_package(PkgConfig REQUIRED)
|
find_package(PkgConfig REQUIRED)
|
||||||
pkg_check_modules(QALCULATE REQUIRED libqalculate)
|
pkg_check_modules(QALCULATE REQUIRED libqalculate)
|
||||||
|
pkg_check_modules(AUBIO REQUIRED aubio)
|
||||||
|
|
||||||
qt_add_qml_module(caelestia
|
qt_add_qml_module(caelestia
|
||||||
URI Caelestia
|
URI Caelestia
|
||||||
|
|
@ -9,6 +10,7 @@ qt_add_qml_module(caelestia
|
||||||
cachingimagemanager.hpp cachingimagemanager.cpp
|
cachingimagemanager.hpp cachingimagemanager.cpp
|
||||||
filesystemmodel.hpp filesystemmodel.cpp
|
filesystemmodel.hpp filesystemmodel.cpp
|
||||||
qalculator.hpp qalculator.cpp
|
qalculator.hpp qalculator.cpp
|
||||||
|
beattracker.hpp beattracker.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
qt_query_qml_module(caelestia
|
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_qmldir}" DESTINATION "${module_dir}")
|
||||||
install(FILES "${module_typeinfo}" DESTINATION "${module_dir}")
|
install(FILES "${module_typeinfo}" DESTINATION "${module_dir}")
|
||||||
|
|
||||||
target_include_directories(caelestia SYSTEM PRIVATE ${QALCULATE_INCLUDE_DIRS})
|
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 ${QALCULATE_LIBRARIES})
|
target_link_libraries(caelestia PRIVATE
|
||||||
|
Qt::Core Qt::Qml Qt::Gui Qt::Concurrent Qt::Multimedia
|
||||||
|
${QALCULATE_LIBRARIES} ${AUBIO_LIBRARIES}
|
||||||
|
)
|
||||||
|
|
|
||||||
106
plugin/src/Caelestia/beattracker.cpp
Normal file
106
plugin/src/Caelestia/beattracker.cpp
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
47
plugin/src/Caelestia/beattracker.hpp
Normal file
47
plugin/src/Caelestia/beattracker.hpp
Normal file
|
|
@ -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;
|
||||||
|
};
|
||||||
|
|
@ -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]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Add table
Reference in a new issue