dashboard
This commit is contained in:
@@ -1,5 +1,12 @@
|
||||
find_package(Qt6 REQUIRED COMPONENTS Core Qml Gui Quick Concurrent Sql)
|
||||
find_package(Qt6 REQUIRED COMPONENTS Core Qml Gui Quick Concurrent Sql Network DBus)
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(Qalculate IMPORTED_TARGET libqalculate REQUIRED)
|
||||
pkg_check_modules(Pipewire IMPORTED_TARGET libpipewire-0.3 REQUIRED)
|
||||
pkg_check_modules(Aubio IMPORTED_TARGET aubio REQUIRED)
|
||||
pkg_check_modules(Cava IMPORTED_TARGET libcava QUIET)
|
||||
if(NOT Cava_FOUND)
|
||||
pkg_check_modules(Cava IMPORTED_TARGET cava REQUIRED)
|
||||
endif()
|
||||
|
||||
set(QT_QML_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/qml")
|
||||
qt_standard_project_setup(REQUIRES 6.9)
|
||||
@@ -34,6 +41,7 @@ qml_module(ZShell
|
||||
writefile.hpp writefile.cpp
|
||||
appdb.hpp appdb.cpp
|
||||
imageanalyser.hpp imageanalyser.cpp
|
||||
requests.hpp requests.cpp
|
||||
LIBRARIES
|
||||
Qt::Gui
|
||||
Qt::Quick
|
||||
@@ -43,3 +51,4 @@ qml_module(ZShell
|
||||
|
||||
add_subdirectory(Models)
|
||||
add_subdirectory(Internal)
|
||||
add_subdirectory(Services)
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
qml_module(ZShell-services
|
||||
URI ZShell.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
|
||||
)
|
||||
@@ -0,0 +1,247 @@
|
||||
#include "audiocollector.hpp"
|
||||
|
||||
#include "service.hpp"
|
||||
#include <algorithm>
|
||||
#include <pipewire/pipewire.h>
|
||||
#include <qdebug.h>
|
||||
#include <qmutex.h>
|
||||
#include <spa/param/audio/format-utils.h>
|
||||
#include <spa/param/latency-utils.h>
|
||||
#include <stop_token>
|
||||
#include <vector>
|
||||
|
||||
namespace ZShell::services {
|
||||
|
||||
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 * ac::SAMPLE_RATE / 48000), ac::SAMPLE_RATE);
|
||||
pw_properties_set(props, PW_KEY_NODE_PASSIVE, "true");
|
||||
pw_properties_set(props, PW_KEY_NODE_VIRTUAL, "true");
|
||||
pw_properties_set(props, PW_KEY_STREAM_DONT_REMIX, "false");
|
||||
pw_properties_set(props, "channelmix.upmix", "true");
|
||||
|
||||
std::vector<uint8_t> buffer(ac::CHUNK_SIZE);
|
||||
spa_pod_builder b;
|
||||
spa_pod_builder_init(&b, buffer.data(), static_cast<quint32>(buffer.size()));
|
||||
|
||||
spa_audio_info_raw info{};
|
||||
info.format = SPA_AUDIO_FORMAT_S16;
|
||||
info.rate = ac::SAMPLE_RATE;
|
||||
info.channels = 1;
|
||||
|
||||
const spa_pod* params[1];
|
||||
params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, &info);
|
||||
|
||||
pw_stream_events events{};
|
||||
events.state_changed = [](void* data, pw_stream_state, pw_stream_state state, const char*) {
|
||||
auto* self = static_cast<PipeWireWorker*>(data);
|
||||
self->streamStateChanged(state);
|
||||
};
|
||||
events.process = [](void* data) {
|
||||
auto* self = static_cast<PipeWireWorker*>(data);
|
||||
self->processStream();
|
||||
};
|
||||
|
||||
m_stream = pw_stream_new_simple(pw_main_loop_get_loop(m_loop), "ZShell-shell", props, &events, this);
|
||||
|
||||
const int success = pw_stream_connect(m_stream, PW_DIRECTION_INPUT, PW_ID_ANY,
|
||||
static_cast<pw_stream_flags>(
|
||||
PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS | PW_STREAM_FLAG_RT_PROCESS),
|
||||
params, 1);
|
||||
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<PipeWireWorker*>(data);
|
||||
|
||||
if (self->m_token.stop_requested()) {
|
||||
pw_main_loop_quit(self->m_loop);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!self->m_idle) {
|
||||
if (expirations < 10) {
|
||||
self->m_collector->clearBuffer();
|
||||
} else {
|
||||
self->m_idle = true;
|
||||
timespec timeout = { 0, 500 * SPA_NSEC_PER_MSEC };
|
||||
pw_loop_update_timer(pw_main_loop_get_loop(self->m_loop), self->m_timer, &timeout, &timeout, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PipeWireWorker::streamStateChanged(pw_stream_state state) {
|
||||
m_idle = false;
|
||||
switch (state) {
|
||||
case PW_STREAM_STATE_PAUSED: {
|
||||
timespec timeout = { 0, 10 * SPA_NSEC_PER_MSEC };
|
||||
pw_loop_update_timer(pw_main_loop_get_loop(m_loop), m_timer, &timeout, &timeout, false);
|
||||
break;
|
||||
}
|
||||
case PW_STREAM_STATE_STREAMING:
|
||||
pw_loop_update_timer(pw_main_loop_get_loop(m_loop), m_timer, nullptr, nullptr, false);
|
||||
break;
|
||||
case PW_STREAM_STATE_ERROR:
|
||||
pw_main_loop_quit(m_loop);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void PipeWireWorker::processStream() {
|
||||
if (m_token.stop_requested()) {
|
||||
pw_main_loop_quit(m_loop);
|
||||
return;
|
||||
}
|
||||
|
||||
pw_buffer* buffer = pw_stream_dequeue_buffer(m_stream);
|
||||
if (buffer == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const spa_buffer* buf = buffer->buffer;
|
||||
const qint16* samples = reinterpret_cast<const qint16*>(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::instance() {
|
||||
static AudioCollector instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
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 > ac::CHUNK_SIZE) {
|
||||
count = ac::CHUNK_SIZE;
|
||||
}
|
||||
|
||||
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 > ac::CHUNK_SIZE) {
|
||||
count = ac::CHUNK_SIZE;
|
||||
}
|
||||
|
||||
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 > ac::CHUNK_SIZE) {
|
||||
count = ac::CHUNK_SIZE;
|
||||
}
|
||||
|
||||
auto* readBuffer = m_readBuffer.load(std::memory_order_acquire);
|
||||
std::transform(readBuffer->begin(), readBuffer->begin() + count, out, [](float sample) {
|
||||
return static_cast<double>(sample);
|
||||
});
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
AudioCollector::AudioCollector(QObject* parent)
|
||||
: Service(parent)
|
||||
, m_buffer1(ac::CHUNK_SIZE)
|
||||
, m_buffer2(ac::CHUNK_SIZE)
|
||||
, m_readBuffer(&m_buffer1)
|
||||
, m_writeBuffer(&m_buffer2) {
|
||||
}
|
||||
|
||||
AudioCollector::~AudioCollector() {
|
||||
stop();
|
||||
}
|
||||
|
||||
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 ZShell::services
|
||||
@@ -0,0 +1,76 @@
|
||||
#pragma once
|
||||
|
||||
#include "service.hpp"
|
||||
#include <atomic>
|
||||
#include <pipewire/pipewire.h>
|
||||
#include <qmutex.h>
|
||||
#include <qqmlintegration.h>
|
||||
#include <spa/param/audio/format-utils.h>
|
||||
#include <stop_token>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace ZShell::services {
|
||||
|
||||
namespace ac {
|
||||
|
||||
constexpr quint32 SAMPLE_RATE = 44100;
|
||||
constexpr quint32 CHUNK_SIZE = 512;
|
||||
|
||||
} // namespace ac
|
||||
|
||||
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;
|
||||
|
||||
static void handleTimeout(void* data, uint64_t expirations);
|
||||
void streamStateChanged(pw_stream_state state);
|
||||
void processStream();
|
||||
|
||||
[[nodiscard]] unsigned int nextPowerOf2(unsigned int n);
|
||||
};
|
||||
|
||||
class AudioCollector : public Service {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
AudioCollector(const AudioCollector&) = delete;
|
||||
AudioCollector& operator=(const AudioCollector&) = delete;
|
||||
|
||||
static AudioCollector& instance();
|
||||
|
||||
void clearBuffer();
|
||||
void loadChunk(const qint16* samples, quint32 count);
|
||||
quint32 readChunk(float* out, quint32 count = 0);
|
||||
quint32 readChunk(double* out, quint32 count = 0);
|
||||
|
||||
private:
|
||||
explicit AudioCollector(QObject* parent = nullptr);
|
||||
~AudioCollector();
|
||||
|
||||
std::jthread m_thread;
|
||||
std::vector<float> m_buffer1;
|
||||
std::vector<float> m_buffer2;
|
||||
std::atomic<std::vector<float>*> m_readBuffer;
|
||||
std::atomic<std::vector<float>*> m_writeBuffer;
|
||||
quint32 m_sampleCount;
|
||||
|
||||
void reload();
|
||||
void start() override;
|
||||
void stop() override;
|
||||
};
|
||||
|
||||
} // namespace ZShell::services
|
||||
@@ -0,0 +1,80 @@
|
||||
#include "audioprovider.hpp"
|
||||
|
||||
#include "audiocollector.hpp"
|
||||
#include "service.hpp"
|
||||
#include <qdebug.h>
|
||||
#include <qthread.h>
|
||||
|
||||
namespace ZShell::services {
|
||||
|
||||
AudioProcessor::AudioProcessor(QObject* parent)
|
||||
: QObject(parent) {
|
||||
}
|
||||
|
||||
AudioProcessor::~AudioProcessor() {
|
||||
stop();
|
||||
}
|
||||
|
||||
void AudioProcessor::init() {
|
||||
m_timer = new QTimer(this);
|
||||
m_timer->setInterval(static_cast<int>(ac::CHUNK_SIZE * 1000.0 / ac::SAMPLE_RATE));
|
||||
connect(m_timer, &QTimer::timeout, this, &AudioProcessor::process);
|
||||
}
|
||||
|
||||
void AudioProcessor::start() {
|
||||
QMetaObject::invokeMethod(&AudioCollector::instance(), &AudioCollector::ref, Qt::QueuedConnection, this);
|
||||
if (m_timer) {
|
||||
m_timer->start();
|
||||
}
|
||||
}
|
||||
|
||||
void AudioProcessor::stop() {
|
||||
if (m_timer) {
|
||||
m_timer->stop();
|
||||
}
|
||||
QMetaObject::invokeMethod(&AudioCollector::instance(), &AudioCollector::unref, Qt::QueuedConnection, this);
|
||||
}
|
||||
|
||||
AudioProvider::AudioProvider(QObject* parent)
|
||||
: Service(parent)
|
||||
, m_processor(nullptr)
|
||||
, m_thread(nullptr) {
|
||||
}
|
||||
|
||||
AudioProvider::~AudioProvider() {
|
||||
if (m_thread) {
|
||||
m_thread->quit();
|
||||
m_thread->wait();
|
||||
}
|
||||
}
|
||||
|
||||
void AudioProvider::init() {
|
||||
if (!m_processor) {
|
||||
qWarning() << "AudioProvider::init: attempted to init with no processor set";
|
||||
return;
|
||||
}
|
||||
|
||||
m_thread = new QThread(this);
|
||||
m_processor->moveToThread(m_thread);
|
||||
|
||||
connect(m_thread, &QThread::started, m_processor, &AudioProcessor::init);
|
||||
connect(m_thread, &QThread::finished, m_processor, &AudioProcessor::deleteLater);
|
||||
connect(m_thread, &QThread::finished, m_thread, &QThread::deleteLater);
|
||||
|
||||
m_thread->start();
|
||||
}
|
||||
|
||||
void AudioProvider::start() {
|
||||
if (m_processor) {
|
||||
AudioCollector::instance(); // Create instance on main thread
|
||||
QMetaObject::invokeMethod(m_processor, &AudioProcessor::start);
|
||||
}
|
||||
}
|
||||
|
||||
void AudioProvider::stop() {
|
||||
if (m_processor) {
|
||||
QMetaObject::invokeMethod(m_processor, &AudioProcessor::stop);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ZShell::services
|
||||
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
|
||||
#include "service.hpp"
|
||||
#include <qqmlintegration.h>
|
||||
#include <qtimer.h>
|
||||
|
||||
namespace ZShell::services {
|
||||
|
||||
class AudioProcessor : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit AudioProcessor(QObject* parent = nullptr);
|
||||
~AudioProcessor();
|
||||
|
||||
void init();
|
||||
|
||||
public slots:
|
||||
void start();
|
||||
void stop();
|
||||
|
||||
protected:
|
||||
virtual void process() = 0;
|
||||
|
||||
private:
|
||||
QTimer* m_timer;
|
||||
};
|
||||
|
||||
class AudioProvider : public Service {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit AudioProvider(QObject* parent = nullptr);
|
||||
~AudioProvider();
|
||||
|
||||
protected:
|
||||
AudioProcessor* m_processor;
|
||||
|
||||
void init();
|
||||
|
||||
private:
|
||||
QThread* m_thread;
|
||||
|
||||
void start() override;
|
||||
void stop() override;
|
||||
};
|
||||
|
||||
} // namespace ZShell::services
|
||||
@@ -0,0 +1,59 @@
|
||||
#include "beattracker.hpp"
|
||||
|
||||
#include "audiocollector.hpp"
|
||||
#include "audioprovider.hpp"
|
||||
#include <aubio/aubio.h>
|
||||
|
||||
namespace ZShell::services {
|
||||
|
||||
BeatProcessor::BeatProcessor(QObject* parent)
|
||||
: AudioProcessor(parent)
|
||||
, m_tempo(new_aubio_tempo("default", 1024, ac::CHUNK_SIZE, ac::SAMPLE_RATE))
|
||||
, m_in(new_fvec(ac::CHUNK_SIZE))
|
||||
, m_out(new_fvec(2)) {
|
||||
};
|
||||
|
||||
BeatProcessor::~BeatProcessor() {
|
||||
if (m_tempo) {
|
||||
del_aubio_tempo(m_tempo);
|
||||
}
|
||||
if (m_in) {
|
||||
del_fvec(m_in);
|
||||
}
|
||||
del_fvec(m_out);
|
||||
}
|
||||
|
||||
void BeatProcessor::process() {
|
||||
if (!m_tempo || !m_in) {
|
||||
return;
|
||||
}
|
||||
|
||||
AudioCollector::instance().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();
|
||||
init();
|
||||
|
||||
connect(static_cast<BeatProcessor*>(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 ZShell::services
|
||||
@@ -0,0 +1,49 @@
|
||||
#pragma once
|
||||
|
||||
#include "audioprovider.hpp"
|
||||
#include <aubio/aubio.h>
|
||||
#include <qqmlintegration.h>
|
||||
|
||||
namespace ZShell::services {
|
||||
|
||||
class BeatProcessor : public AudioProcessor {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit BeatProcessor(QObject* parent = nullptr);
|
||||
~BeatProcessor();
|
||||
|
||||
signals:
|
||||
void beat(smpl_t bpm);
|
||||
|
||||
protected:
|
||||
void process() override;
|
||||
|
||||
private:
|
||||
aubio_tempo_t* m_tempo;
|
||||
fvec_t* m_in;
|
||||
fvec_t* m_out;
|
||||
};
|
||||
|
||||
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 ZShell::services
|
||||
@@ -0,0 +1,141 @@
|
||||
#include "cavaprovider.hpp"
|
||||
|
||||
#include "audiocollector.hpp"
|
||||
#include "audioprovider.hpp"
|
||||
#include <cava/cavacore.h>
|
||||
#include <cstddef>
|
||||
#include <qdebug.h>
|
||||
|
||||
namespace ZShell::services {
|
||||
|
||||
CavaProcessor::CavaProcessor(QObject* parent)
|
||||
: AudioProcessor(parent)
|
||||
, m_plan(nullptr)
|
||||
, m_in(new double[ac::CHUNK_SIZE])
|
||||
, m_out(nullptr)
|
||||
, m_bars(0) {
|
||||
};
|
||||
|
||||
CavaProcessor::~CavaProcessor() {
|
||||
cleanup();
|
||||
delete[] m_in;
|
||||
}
|
||||
|
||||
void CavaProcessor::process() {
|
||||
if (!m_plan || m_bars == 0 || !m_out) {
|
||||
return;
|
||||
}
|
||||
|
||||
const int count = static_cast<int>(AudioCollector::instance().readChunk(m_in));
|
||||
|
||||
// Process in data via cava
|
||||
cava_execute(m_in, count, m_out, m_plan);
|
||||
|
||||
// Apply monstercat filter
|
||||
QVector<double> values(m_bars);
|
||||
|
||||
// Left to right pass
|
||||
const double inv = 1.0 / 1.5;
|
||||
double carry = 0.0;
|
||||
for (int i = 0; i < m_bars; ++i) {
|
||||
carry = std::max(m_out[i], carry * inv);
|
||||
values[i] = carry;
|
||||
}
|
||||
|
||||
// Right to left pass and combine
|
||||
carry = 0.0;
|
||||
for (int i = m_bars - 1; i >= 0; --i) {
|
||||
carry = std::max(m_out[i], carry * inv);
|
||||
values[i] = std::max(values[i], carry);
|
||||
}
|
||||
|
||||
// Update values
|
||||
if (values != m_values) {
|
||||
m_values = std::move(values);
|
||||
emit valuesChanged(m_values);
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_plan = cava_init(m_bars, ac::SAMPLE_RATE, 1, 1, 0.85, 50, 10000);
|
||||
m_out = new double[static_cast<size_t>(m_bars)];
|
||||
}
|
||||
|
||||
CavaProvider::CavaProvider(QObject* parent)
|
||||
: AudioProvider(parent)
|
||||
, m_bars(0)
|
||||
, m_values(m_bars, 0.0) {
|
||||
m_processor = new CavaProcessor();
|
||||
init();
|
||||
|
||||
connect(static_cast<CavaProcessor*>(m_processor), &CavaProcessor::valuesChanged, this, &CavaProvider::updateValues);
|
||||
}
|
||||
|
||||
int CavaProvider::bars() const {
|
||||
return m_bars;
|
||||
}
|
||||
|
||||
void CavaProvider::setBars(int bars) {
|
||||
if (bars < 0) {
|
||||
qWarning() << "CavaProvider::setBars: bars must be greater than 0. Setting to 0.";
|
||||
bars = 0;
|
||||
}
|
||||
|
||||
if (m_bars == bars) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_values.resize(bars, 0.0);
|
||||
m_bars = bars;
|
||||
emit barsChanged();
|
||||
emit valuesChanged();
|
||||
|
||||
QMetaObject::invokeMethod(
|
||||
static_cast<CavaProcessor*>(m_processor), &CavaProcessor::setBars, Qt::QueuedConnection, bars);
|
||||
}
|
||||
|
||||
QVector<double> CavaProvider::values() const {
|
||||
return m_values;
|
||||
}
|
||||
|
||||
void CavaProvider::updateValues(QVector<double> values) {
|
||||
if (values != m_values) {
|
||||
m_values = values;
|
||||
emit valuesChanged();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ZShell::services
|
||||
@@ -0,0 +1,64 @@
|
||||
#pragma once
|
||||
|
||||
#include "audioprovider.hpp"
|
||||
#include <cava/cavacore.h>
|
||||
#include <qqmlintegration.h>
|
||||
|
||||
namespace ZShell::services {
|
||||
|
||||
class CavaProcessor : public AudioProcessor {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit CavaProcessor(QObject* parent = nullptr);
|
||||
~CavaProcessor();
|
||||
|
||||
void setBars(int bars);
|
||||
|
||||
signals:
|
||||
void valuesChanged(QVector<double> values);
|
||||
|
||||
protected:
|
||||
void process() override;
|
||||
|
||||
private:
|
||||
struct cava_plan* m_plan;
|
||||
double* m_in;
|
||||
double* m_out;
|
||||
|
||||
int m_bars;
|
||||
QVector<double> m_values;
|
||||
|
||||
void reload();
|
||||
void initCava();
|
||||
void cleanup();
|
||||
};
|
||||
|
||||
class CavaProvider : public AudioProvider {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
Q_PROPERTY(int bars READ bars WRITE setBars NOTIFY barsChanged)
|
||||
|
||||
Q_PROPERTY(QVector<double> values READ values NOTIFY valuesChanged)
|
||||
|
||||
public:
|
||||
explicit CavaProvider(QObject* parent = nullptr);
|
||||
|
||||
[[nodiscard]] int bars() const;
|
||||
void setBars(int bars);
|
||||
|
||||
[[nodiscard]] QVector<double> values() const;
|
||||
|
||||
signals:
|
||||
void barsChanged();
|
||||
void valuesChanged();
|
||||
|
||||
private:
|
||||
int m_bars;
|
||||
QVector<double> m_values;
|
||||
|
||||
void updateValues(QVector<double> values);
|
||||
};
|
||||
|
||||
} // namespace ZShell::services
|
||||
@@ -0,0 +1,27 @@
|
||||
#include "service.hpp"
|
||||
|
||||
#include <qdebug.h>
|
||||
#include <qpointer.h>
|
||||
|
||||
namespace ZShell::services {
|
||||
|
||||
Service::Service(QObject* parent)
|
||||
: QObject(parent) {
|
||||
}
|
||||
|
||||
void Service::ref(QObject* sender) {
|
||||
if (m_refs.isEmpty()) {
|
||||
start();
|
||||
}
|
||||
|
||||
QObject::connect(sender, &QObject::destroyed, this, &Service::unref);
|
||||
m_refs << sender;
|
||||
}
|
||||
|
||||
void Service::unref(QObject* sender) {
|
||||
if (m_refs.remove(sender) && m_refs.isEmpty()) {
|
||||
stop();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ZShell::services
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include <qobject.h>
|
||||
#include <qset.h>
|
||||
|
||||
namespace ZShell::services {
|
||||
|
||||
class Service : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit Service(QObject* parent = nullptr);
|
||||
|
||||
void ref(QObject* sender);
|
||||
void unref(QObject* sender);
|
||||
|
||||
private:
|
||||
QSet<QObject*> m_refs;
|
||||
|
||||
virtual void start() = 0;
|
||||
virtual void stop() = 0;
|
||||
};
|
||||
|
||||
} // namespace ZShell::services
|
||||
@@ -0,0 +1,36 @@
|
||||
#include "serviceref.hpp"
|
||||
|
||||
#include "service.hpp"
|
||||
|
||||
namespace ZShell::services {
|
||||
|
||||
ServiceRef::ServiceRef(Service* service, QObject* parent)
|
||||
: QObject(parent)
|
||||
, m_service(service) {
|
||||
if (m_service) {
|
||||
m_service->ref(this);
|
||||
}
|
||||
}
|
||||
|
||||
Service* ServiceRef::service() const {
|
||||
return m_service;
|
||||
}
|
||||
|
||||
void ServiceRef::setService(Service* service) {
|
||||
if (m_service == service) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_service) {
|
||||
m_service->unref(this);
|
||||
}
|
||||
|
||||
m_service = service;
|
||||
emit serviceChanged();
|
||||
|
||||
if (m_service) {
|
||||
m_service->ref(this);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ZShell::services
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include "service.hpp"
|
||||
#include <qpointer.h>
|
||||
#include <qqmlintegration.h>
|
||||
|
||||
namespace ZShell::services {
|
||||
|
||||
class ServiceRef : public QObject {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
Q_PROPERTY(ZShell::services::Service* service READ service WRITE setService NOTIFY serviceChanged)
|
||||
|
||||
public:
|
||||
explicit ServiceRef(Service* service = nullptr, QObject* parent = nullptr);
|
||||
|
||||
[[nodiscard]] Service* service() const;
|
||||
void setService(Service* service);
|
||||
|
||||
signals:
|
||||
void serviceChanged();
|
||||
|
||||
private:
|
||||
QPointer<Service> m_service;
|
||||
};
|
||||
|
||||
} // namespace ZShell::services
|
||||
@@ -0,0 +1,36 @@
|
||||
#include "requests.hpp"
|
||||
|
||||
#include <qnetworkaccessmanager.h>
|
||||
#include <qnetworkreply.h>
|
||||
#include <qnetworkrequest.h>
|
||||
|
||||
namespace ZShell {
|
||||
|
||||
Requests::Requests(QObject* parent)
|
||||
: QObject(parent)
|
||||
, m_manager(new QNetworkAccessManager(this)) {
|
||||
}
|
||||
|
||||
void Requests::get(const QUrl& url, QJSValue onSuccess, QJSValue onError) const {
|
||||
if (!onSuccess.isCallable()) {
|
||||
qWarning() << "Requests::get: onSuccess is not callable";
|
||||
return;
|
||||
}
|
||||
|
||||
QNetworkRequest request(url);
|
||||
auto reply = m_manager->get(request);
|
||||
|
||||
QObject::connect(reply, &QNetworkReply::finished, [reply, onSuccess, onError]() {
|
||||
if (reply->error() == QNetworkReply::NoError) {
|
||||
onSuccess.call({ QString(reply->readAll()) });
|
||||
} else if (onError.isCallable()) {
|
||||
onError.call({ reply->errorString() });
|
||||
} else {
|
||||
qWarning() << "Requests::get: request failed with error" << reply->errorString();
|
||||
}
|
||||
|
||||
reply->deleteLater();
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace ZShell
|
||||
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include <qnetworkaccessmanager.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmlengine.h>
|
||||
|
||||
namespace ZShell {
|
||||
|
||||
class Requests : public QObject {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_SINGLETON
|
||||
|
||||
public:
|
||||
explicit Requests(QObject* parent = nullptr);
|
||||
|
||||
Q_INVOKABLE void get(const QUrl& url, QJSValue callback, QJSValue onError = QJSValue()) const;
|
||||
|
||||
private:
|
||||
QNetworkAccessManager* m_manager;
|
||||
};
|
||||
|
||||
} // namespace ZShell
|
||||
Reference in New Issue
Block a user