gpu/parallel rendering upgrade overhaul. but bugs exist still.
This commit is contained in:
parent
61e220f185
commit
30cecf586c
|
|
@ -36,7 +36,8 @@ else()
|
||||||
message(STATUS "Tempo Estimation (Entropy) Disabled")
|
message(STATUS "Tempo Estimation (Entropy) Disabled")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets Multimedia OpenGLWidgets)
|
find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets Multimedia)
|
||||||
|
find_package(Qt6 QUIET COMPONENTS ShaderTools)
|
||||||
|
|
||||||
# --- FFTW3 Configuration (Double Precision) ---
|
# --- FFTW3 Configuration (Double Precision) ---
|
||||||
|
|
||||||
|
|
@ -206,6 +207,28 @@ set(PROJECT_HEADERS
|
||||||
|
|
||||||
qt_add_executable(YrCrystals MANUAL_FINALIZATION ${PROJECT_SOURCES} ${PROJECT_HEADERS})
|
qt_add_executable(YrCrystals MANUAL_FINALIZATION ${PROJECT_SOURCES} ${PROJECT_HEADERS})
|
||||||
|
|
||||||
|
if(TARGET Qt6::ShaderTools)
|
||||||
|
qt_add_shaders(YrCrystals "visualizer_shaders"
|
||||||
|
BATCHABLE
|
||||||
|
PREFIX "/"
|
||||||
|
FILES
|
||||||
|
shaders/visualizer.vert
|
||||||
|
shaders/visualizer.frag
|
||||||
|
)
|
||||||
|
else()
|
||||||
|
set(QSB_DIR "${CMAKE_CURRENT_SOURCE_DIR}/build_macos/.qsb/shaders")
|
||||||
|
if(NOT EXISTS "${QSB_DIR}/visualizer.vert.qsb" OR NOT EXISTS "${QSB_DIR}/visualizer.frag.qsb")
|
||||||
|
message(FATAL_ERROR "Pre-compiled shaders not found. Run 'make macos' first.")
|
||||||
|
endif()
|
||||||
|
qt_add_resources(YrCrystals "visualizer_shaders"
|
||||||
|
PREFIX "/shaders"
|
||||||
|
BASE "${QSB_DIR}"
|
||||||
|
FILES
|
||||||
|
${QSB_DIR}/visualizer.vert.qsb
|
||||||
|
${QSB_DIR}/visualizer.frag.qsb
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
if(EXISTS "${ICON_SOURCE}" AND MAGICK_EXECUTABLE)
|
if(EXISTS "${ICON_SOURCE}" AND MAGICK_EXECUTABLE)
|
||||||
add_dependencies(YrCrystals GenerateIcons)
|
add_dependencies(YrCrystals GenerateIcons)
|
||||||
endif()
|
endif()
|
||||||
|
|
@ -231,7 +254,7 @@ else()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
target_link_libraries(YrCrystals PRIVATE
|
target_link_libraries(YrCrystals PRIVATE
|
||||||
Qt6::Core Qt6::Gui Qt6::Widgets Qt6::Multimedia Qt6::OpenGLWidgets
|
Qt6::Core Qt6::Gui Qt6::GuiPrivate Qt6::Widgets Qt6::Multimedia
|
||||||
${FFTW_TARGET}
|
${FFTW_TARGET}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
2
Makefile
2
Makefile
|
|
@ -50,7 +50,7 @@ android:
|
||||||
@cmake --build $(BUILD_DIR_ANDROID) --target apk
|
@cmake --build $(BUILD_DIR_ANDROID) --target apk
|
||||||
@echo "APK generated at $(APK_PATH)"
|
@echo "APK generated at $(APK_PATH)"
|
||||||
|
|
||||||
ios:
|
ios: macos
|
||||||
@if [ ! -d "$(QT_IOS_KIT)" ]; then echo "Error: QT_IOS_KIT not found at $(QT_IOS_KIT)"; exit 1; fi
|
@if [ ! -d "$(QT_IOS_KIT)" ]; then echo "Error: QT_IOS_KIT not found at $(QT_IOS_KIT)"; exit 1; fi
|
||||||
@mkdir -p $(BUILD_DIR_IOS)
|
@mkdir -p $(BUILD_DIR_IOS)
|
||||||
@echo "Configuring iOS CMake..."
|
@echo "Configuring iOS CMake..."
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
#version 440
|
||||||
|
|
||||||
|
layout(location = 0) in vec4 v_color;
|
||||||
|
layout(location = 0) out vec4 fragColor;
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
fragColor = v_color;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
#version 440
|
||||||
|
|
||||||
|
layout(location = 0) in vec2 a_position;
|
||||||
|
layout(location = 1) in vec4 a_color;
|
||||||
|
|
||||||
|
layout(std140, binding = 0) uniform buf {
|
||||||
|
mat4 mvp;
|
||||||
|
};
|
||||||
|
|
||||||
|
layout(location = 0) out vec4 v_color;
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
gl_Position = mvp * vec4(a_position, 0.0, 1.0);
|
||||||
|
v_color = a_color;
|
||||||
|
}
|
||||||
|
|
@ -13,6 +13,12 @@
|
||||||
#include <QtEndian>
|
#include <QtEndian>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
|
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
|
||||||
|
#ifndef IS_MOBILE
|
||||||
|
#define IS_MOBILE
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifdef ENABLE_TEMPO_ESTIMATION
|
#ifdef ENABLE_TEMPO_ESTIMATION
|
||||||
#include "LoopTempoEstimator/LoopTempoEstimator.h"
|
#include "LoopTempoEstimator/LoopTempoEstimator.h"
|
||||||
|
|
||||||
|
|
@ -273,6 +279,9 @@ void AudioEngine::onFinished() {
|
||||||
// Notify UI that track is ready to play
|
// Notify UI that track is ready to play
|
||||||
emit trackLoaded(true);
|
emit trackLoaded(true);
|
||||||
|
|
||||||
|
// Emit immediately so analyzer can use pcmData fallback while Hilbert runs
|
||||||
|
emit trackDataChanged(m_trackData);
|
||||||
|
|
||||||
// OPTIMIZATION: Run heavy analysis in background to avoid blocking audio
|
// OPTIMIZATION: Run heavy analysis in background to avoid blocking audio
|
||||||
// thread FIX: Use QPointer to prevent crash if AudioEngine is deleted
|
// thread FIX: Use QPointer to prevent crash if AudioEngine is deleted
|
||||||
// before task runs
|
// before task runs
|
||||||
|
|
@ -370,6 +379,9 @@ void AudioEngine::play() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
#ifdef IS_MOBILE
|
||||||
|
m_source.enablePrebuffer(150);
|
||||||
|
#endif
|
||||||
m_sink->start(&m_source);
|
m_sink->start(&m_source);
|
||||||
m_playTimer->start();
|
m_playTimer->start();
|
||||||
}
|
}
|
||||||
|
|
@ -512,8 +524,14 @@ void AudioAnalyzer::processLoop() {
|
||||||
// 1. Poll Atomic Position (Non-blocking)
|
// 1. Poll Atomic Position (Non-blocking)
|
||||||
double pos = m_posRef->load();
|
double pos = m_posRef->load();
|
||||||
|
|
||||||
// 2. Calculate Index
|
// 2. Calculate Index — use complexData if available, else fallback to pcmData
|
||||||
size_t totalSamples = m_data->complexData.size() / 2;
|
bool useComplex = !m_data->complexData.empty();
|
||||||
|
size_t totalSamples;
|
||||||
|
if (useComplex) {
|
||||||
|
totalSamples = m_data->complexData.size() / 2;
|
||||||
|
} else {
|
||||||
|
totalSamples = m_data->pcmData.size() / sizeof(float) / 2;
|
||||||
|
}
|
||||||
size_t sampleIdx = static_cast<size_t>(pos * totalSamples);
|
size_t sampleIdx = static_cast<size_t>(pos * totalSamples);
|
||||||
|
|
||||||
// Boundary check
|
// Boundary check
|
||||||
|
|
@ -522,10 +540,19 @@ void AudioAnalyzer::processLoop() {
|
||||||
|
|
||||||
// 3. Extract Data (Read-only from shared memory)
|
// 3. Extract Data (Read-only from shared memory)
|
||||||
std::vector<std::complex<double>> ch0(m_frameSize), ch1(m_frameSize);
|
std::vector<std::complex<double>> ch0(m_frameSize), ch1(m_frameSize);
|
||||||
|
if (useComplex) {
|
||||||
for (int i = 0; i < m_frameSize; ++i) {
|
for (int i = 0; i < m_frameSize; ++i) {
|
||||||
ch0[i] = m_data->complexData[(sampleIdx + i) * 2];
|
ch0[i] = m_data->complexData[(sampleIdx + i) * 2];
|
||||||
ch1[i] = m_data->complexData[(sampleIdx + i) * 2 + 1];
|
ch1[i] = m_data->complexData[(sampleIdx + i) * 2 + 1];
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
const float *raw =
|
||||||
|
reinterpret_cast<const float *>(m_data->pcmData.constData());
|
||||||
|
for (int i = 0; i < m_frameSize; ++i) {
|
||||||
|
ch0[i] = std::complex<double>(raw[(sampleIdx + i) * 2], 0.0);
|
||||||
|
ch1[i] = std::complex<double>(raw[(sampleIdx + i) * 2 + 1], 0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 4. Push to Processors
|
// 4. Push to Processors
|
||||||
m_processors[0]->pushData(ch0);
|
m_processors[0]->pushData(ch0);
|
||||||
|
|
|
||||||
|
|
@ -36,11 +36,33 @@ public:
|
||||||
void setData(const QByteArray &pcmFloat) {
|
void setData(const QByteArray &pcmFloat) {
|
||||||
m_data = pcmFloat;
|
m_data = pcmFloat;
|
||||||
m_pos = 0;
|
m_pos = 0;
|
||||||
|
m_prebufferRemaining = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void setTargetFormat(const QAudioFormat &fmt) { m_targetFormat = fmt; }
|
void setTargetFormat(const QAudioFormat &fmt) { m_targetFormat = fmt; }
|
||||||
|
|
||||||
|
void enablePrebuffer(int ms) {
|
||||||
|
int bytesPerSample =
|
||||||
|
(m_targetFormat.sampleFormat() == QAudioFormat::Int16) ? 2 : 4;
|
||||||
|
int sampleRate = m_targetFormat.sampleRate();
|
||||||
|
int channels = m_targetFormat.channelCount();
|
||||||
|
if (sampleRate <= 0 || channels <= 0)
|
||||||
|
return;
|
||||||
|
qint64 total =
|
||||||
|
(qint64)sampleRate * channels * bytesPerSample * ms / 1000;
|
||||||
|
// Align to frame boundary
|
||||||
|
qint64 frameBytes = channels * bytesPerSample;
|
||||||
|
m_prebufferRemaining = (total / frameBytes) * frameBytes;
|
||||||
|
}
|
||||||
|
|
||||||
qint64 readData(char *data, qint64 maxlen) override {
|
qint64 readData(char *data, qint64 maxlen) override {
|
||||||
|
if (m_prebufferRemaining > 0) {
|
||||||
|
qint64 toWrite = std::min(maxlen, m_prebufferRemaining);
|
||||||
|
memset(data, 0, toWrite);
|
||||||
|
m_prebufferRemaining -= toWrite;
|
||||||
|
return toWrite;
|
||||||
|
}
|
||||||
|
|
||||||
if (m_pos >= m_data.size())
|
if (m_pos >= m_data.size())
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
|
|
@ -92,6 +114,7 @@ public:
|
||||||
void close() override {
|
void close() override {
|
||||||
QIODevice::close();
|
QIODevice::close();
|
||||||
m_pos = 0;
|
m_pos = 0;
|
||||||
|
m_prebufferRemaining = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isAtEnd() const { return m_pos >= m_data.size(); }
|
bool isAtEnd() const { return m_pos >= m_data.size(); }
|
||||||
|
|
@ -105,12 +128,21 @@ public:
|
||||||
// Custom seek (in float domain bytes)
|
// Custom seek (in float domain bytes)
|
||||||
void seekFloatBytes(qint64 pos) {
|
void seekFloatBytes(qint64 pos) {
|
||||||
m_pos = std::clamp(pos, 0LL, (qint64)m_data.size());
|
m_pos = std::clamp(pos, 0LL, (qint64)m_data.size());
|
||||||
|
// Keep QIODevice internal position in sync to avoid stale state on
|
||||||
|
// CoreAudio/AAudio resume after seek
|
||||||
|
if (isOpen()) {
|
||||||
|
qint64 outputPos = m_pos;
|
||||||
|
if (m_targetFormat.sampleFormat() == QAudioFormat::Int16)
|
||||||
|
outputPos = m_pos / 2;
|
||||||
|
QIODevice::seek(outputPos);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
qint64 sizeFloatBytes() const { return m_data.size(); }
|
qint64 sizeFloatBytes() const { return m_data.size(); }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QByteArray m_data; // Always stored as Float32
|
QByteArray m_data; // Always stored as Float32
|
||||||
qint64 m_pos = 0;
|
qint64 m_pos = 0;
|
||||||
|
qint64 m_prebufferRemaining = 0;
|
||||||
QAudioFormat m_targetFormat;
|
QAudioFormat m_targetFormat;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
// src/CommonWidgets.cpp
|
// src/CommonWidgets.cpp
|
||||||
|
|
||||||
#include "CommonWidgets.h"
|
#include "CommonWidgets.h"
|
||||||
#include <QFileInfo> // Added for file info extraction
|
#include <QBoxLayout>
|
||||||
|
#include <QFileInfo>
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
#include <QListWidget> // Added for WelcomeWidget
|
#include <QListWidget>
|
||||||
#include <QMouseEvent>
|
#include <QMouseEvent>
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
|
#include <QResizeEvent>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
|
@ -226,27 +228,58 @@ WelcomeWidget::WelcomeWidget(QWidget *parent) : QWidget(parent) {
|
||||||
|
|
||||||
layout->addLayout(btnLayout);
|
layout->addLayout(btnLayout);
|
||||||
|
|
||||||
// --- Recents List ---
|
// --- Lists Container (Recent + Frequents) ---
|
||||||
QLabel *recentLabel = new QLabel("Recent", this);
|
QString listStyle =
|
||||||
recentLabel->setStyleSheet("color: #aaa; font-size: 16px; margin-top: 20px;");
|
|
||||||
layout->addWidget(recentLabel);
|
|
||||||
|
|
||||||
m_recentList = new QListWidget(this);
|
|
||||||
m_recentList->setStyleSheet(
|
|
||||||
"QListWidget { background: transparent; border: none; color: #ddd; "
|
"QListWidget { background: transparent; border: none; color: #ddd; "
|
||||||
"font-size: 16px; }"
|
"font-size: 16px; }"
|
||||||
"QListWidget::item { padding: 10px; border-bottom: 1px solid #333; }"
|
"QListWidget::item { padding: 10px; border-bottom: 1px solid #333; }"
|
||||||
"QListWidget::item:hover { background: #222; }"
|
"QListWidget::item:hover { background: #222; }"
|
||||||
"QListWidget::item:selected { background: #333; }");
|
"QListWidget::item:selected { background: #333; }";
|
||||||
|
QString labelStyle = "color: #aaa; font-size: 16px; margin-top: 20px;";
|
||||||
|
|
||||||
|
m_listsContainer = new QWidget(this);
|
||||||
|
|
||||||
|
// Recent column
|
||||||
|
QWidget *recentCol = new QWidget(m_listsContainer);
|
||||||
|
QVBoxLayout *recentLayout = new QVBoxLayout(recentCol);
|
||||||
|
recentLayout->setContentsMargins(0, 0, 0, 0);
|
||||||
|
QLabel *recentLabel = new QLabel("Recent", recentCol);
|
||||||
|
recentLabel->setStyleSheet(labelStyle);
|
||||||
|
recentLayout->addWidget(recentLabel);
|
||||||
|
m_recentList = new QListWidget(recentCol);
|
||||||
|
m_recentList->setStyleSheet(listStyle);
|
||||||
m_recentList->setFocusPolicy(Qt::NoFocus);
|
m_recentList->setFocusPolicy(Qt::NoFocus);
|
||||||
m_recentList->setCursor(Qt::PointingHandCursor);
|
m_recentList->setCursor(Qt::PointingHandCursor);
|
||||||
m_recentList->setSelectionMode(QAbstractItemView::SingleSelection);
|
m_recentList->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||||
connect(m_recentList, &QListWidget::itemClicked, this,
|
connect(m_recentList, &QListWidget::itemClicked, this,
|
||||||
&WelcomeWidget::onRecentClicked);
|
&WelcomeWidget::onRecentClicked);
|
||||||
|
recentLayout->addWidget(m_recentList);
|
||||||
|
|
||||||
layout->addWidget(m_recentList);
|
// Frequents column
|
||||||
|
QWidget *freqCol = new QWidget(m_listsContainer);
|
||||||
|
QVBoxLayout *freqLayout = new QVBoxLayout(freqCol);
|
||||||
|
freqLayout->setContentsMargins(0, 0, 0, 0);
|
||||||
|
QLabel *freqLabel = new QLabel("Frequents", freqCol);
|
||||||
|
freqLabel->setStyleSheet(labelStyle);
|
||||||
|
freqLayout->addWidget(freqLabel);
|
||||||
|
m_frequentList = new QListWidget(freqCol);
|
||||||
|
m_frequentList->setStyleSheet(listStyle);
|
||||||
|
m_frequentList->setFocusPolicy(Qt::NoFocus);
|
||||||
|
m_frequentList->setCursor(Qt::PointingHandCursor);
|
||||||
|
m_frequentList->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||||
|
connect(m_frequentList, &QListWidget::itemClicked, this,
|
||||||
|
&WelcomeWidget::onRecentClicked);
|
||||||
|
freqLayout->addWidget(m_frequentList);
|
||||||
|
|
||||||
|
// Default to vertical layout
|
||||||
|
m_listsLayout = new QVBoxLayout(m_listsContainer);
|
||||||
|
m_listsLayout->setContentsMargins(0, 0, 0, 0);
|
||||||
|
m_listsLayout->addWidget(recentCol);
|
||||||
|
m_listsLayout->addWidget(freqCol);
|
||||||
|
m_isHorizontal = false;
|
||||||
|
|
||||||
|
layout->addWidget(m_listsContainer, 1);
|
||||||
|
|
||||||
// Refresh on init
|
|
||||||
refreshRecents();
|
refreshRecents();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -255,12 +288,10 @@ void WelcomeWidget::refreshRecents() {
|
||||||
QStringList files = Utils::getRecentFiles();
|
QStringList files = Utils::getRecentFiles();
|
||||||
QStringList folders = Utils::getRecentFolders();
|
QStringList folders = Utils::getRecentFolders();
|
||||||
|
|
||||||
// Interleave or section them? Let's just list folders then files.
|
|
||||||
for (const auto &path : folders) {
|
for (const auto &path : folders) {
|
||||||
QListWidgetItem *item =
|
QListWidgetItem *item =
|
||||||
new QListWidgetItem("📁 " + QFileInfo(path).fileName());
|
new QListWidgetItem("📁 " + QFileInfo(path).fileName());
|
||||||
item->setData(Qt::UserRole, path);
|
item->setData(Qt::UserRole, path);
|
||||||
// Tooltip showing full path
|
|
||||||
item->setToolTip(path);
|
item->setToolTip(path);
|
||||||
m_recentList->addItem(item);
|
m_recentList->addItem(item);
|
||||||
}
|
}
|
||||||
|
|
@ -271,6 +302,48 @@ void WelcomeWidget::refreshRecents() {
|
||||||
item->setToolTip(path);
|
item->setToolTip(path);
|
||||||
m_recentList->addItem(item);
|
m_recentList->addItem(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Refresh frequents
|
||||||
|
m_frequentList->clear();
|
||||||
|
auto freqs = Utils::getFrequentPaths(10);
|
||||||
|
for (const auto &pair : freqs) {
|
||||||
|
QString name = QFileInfo(pair.first).fileName();
|
||||||
|
QListWidgetItem *item =
|
||||||
|
new QListWidgetItem(name + " (" + QString::number(pair.second) + ")");
|
||||||
|
item->setData(Qt::UserRole, pair.first);
|
||||||
|
item->setToolTip(pair.first);
|
||||||
|
m_frequentList->addItem(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void WelcomeWidget::resizeEvent(QResizeEvent *event) {
|
||||||
|
QWidget::resizeEvent(event);
|
||||||
|
updateListsLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WelcomeWidget::updateListsLayout() {
|
||||||
|
bool wantHorizontal = (width() > height());
|
||||||
|
if (wantHorizontal == m_isHorizontal)
|
||||||
|
return;
|
||||||
|
m_isHorizontal = wantHorizontal;
|
||||||
|
|
||||||
|
// Reparent children out of old layout
|
||||||
|
QList<QWidget *> children;
|
||||||
|
while (m_listsLayout->count() > 0) {
|
||||||
|
QLayoutItem *item = m_listsLayout->takeAt(0);
|
||||||
|
if (item->widget())
|
||||||
|
children.append(item->widget());
|
||||||
|
delete item;
|
||||||
|
}
|
||||||
|
delete m_listsLayout;
|
||||||
|
|
||||||
|
if (m_isHorizontal)
|
||||||
|
m_listsLayout = new QHBoxLayout(m_listsContainer);
|
||||||
|
else
|
||||||
|
m_listsLayout = new QVBoxLayout(m_listsContainer);
|
||||||
|
m_listsLayout->setContentsMargins(0, 0, 0, 0);
|
||||||
|
for (auto *w : children)
|
||||||
|
m_listsLayout->addWidget(w);
|
||||||
}
|
}
|
||||||
|
|
||||||
void WelcomeWidget::onRecentClicked(QListWidgetItem *item) {
|
void WelcomeWidget::onRecentClicked(QListWidgetItem *item) {
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@ private:
|
||||||
QWidget *m_content;
|
QWidget *m_content;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class QBoxLayout;
|
||||||
class QListWidget;
|
class QListWidget;
|
||||||
class QListWidgetItem;
|
class QListWidgetItem;
|
||||||
|
|
||||||
|
|
@ -66,9 +67,16 @@ signals:
|
||||||
void openFileClicked();
|
void openFileClicked();
|
||||||
void openFolderClicked();
|
void openFolderClicked();
|
||||||
void pathSelected(const QString &path);
|
void pathSelected(const QString &path);
|
||||||
|
protected:
|
||||||
|
void resizeEvent(QResizeEvent *event) override;
|
||||||
private slots:
|
private slots:
|
||||||
void onRecentClicked(QListWidgetItem *item);
|
void onRecentClicked(QListWidgetItem *item);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void updateListsLayout();
|
||||||
QListWidget *m_recentList;
|
QListWidget *m_recentList;
|
||||||
|
QListWidget *m_frequentList;
|
||||||
|
QWidget *m_listsContainer;
|
||||||
|
QBoxLayout *m_listsLayout = nullptr;
|
||||||
|
bool m_isHorizontal = false;
|
||||||
};
|
};
|
||||||
|
|
@ -660,6 +660,7 @@ void addRecentFile(const QString &path) {
|
||||||
while (files.size() > 10)
|
while (files.size() > 10)
|
||||||
files.removeLast();
|
files.removeLast();
|
||||||
settings.setValue("recentFiles", files);
|
settings.setValue("recentFiles", files);
|
||||||
|
incrementOpenCount(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
void addRecentFolder(const QString &path) {
|
void addRecentFolder(const QString &path) {
|
||||||
|
|
@ -670,6 +671,27 @@ void addRecentFolder(const QString &path) {
|
||||||
while (folders.size() > 10)
|
while (folders.size() > 10)
|
||||||
folders.removeLast();
|
folders.removeLast();
|
||||||
settings.setValue("recentFolders", folders);
|
settings.setValue("recentFolders", folders);
|
||||||
|
incrementOpenCount(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
void incrementOpenCount(const QString &path) {
|
||||||
|
QSettings settings("YrCrystals", "App");
|
||||||
|
QVariantMap freq = settings.value("frequentPaths").toMap();
|
||||||
|
freq[path] = freq.value(path, 0).toInt() + 1;
|
||||||
|
settings.setValue("frequentPaths", freq);
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<QPair<QString, int>> getFrequentPaths(int limit) {
|
||||||
|
QSettings settings("YrCrystals", "App");
|
||||||
|
QVariantMap freq = settings.value("frequentPaths").toMap();
|
||||||
|
QList<QPair<QString, int>> list;
|
||||||
|
for (auto it = freq.constBegin(); it != freq.constEnd(); ++it)
|
||||||
|
list.append({it.key(), it.value().toInt()});
|
||||||
|
std::sort(list.begin(), list.end(),
|
||||||
|
[](const auto &a, const auto &b) { return a.second > b.second; });
|
||||||
|
while (list.size() > limit)
|
||||||
|
list.removeLast();
|
||||||
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
QStringList getRecentFiles() {
|
QStringList getRecentFiles() {
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,10 @@ void addRecentFolder(const QString &path);
|
||||||
QStringList getRecentFiles();
|
QStringList getRecentFiles();
|
||||||
QStringList getRecentFolders();
|
QStringList getRecentFolders();
|
||||||
|
|
||||||
|
// Frequency Tracking
|
||||||
|
void incrementOpenCount(const QString &path);
|
||||||
|
QList<QPair<QString, int>> getFrequentPaths(int limit = 10);
|
||||||
|
|
||||||
#ifdef Q_OS_IOS
|
#ifdef Q_OS_IOS
|
||||||
void openIosPicker(bool folder, std::function<void(QString)> callback);
|
void openIosPicker(bool folder, std::function<void(QString)> callback);
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,7 @@
|
||||||
#include "VisualizerWidget.h"
|
#include "VisualizerWidget.h"
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QDateTime>
|
#include <QDateTime>
|
||||||
#include <QLinearGradient>
|
#include <QFile>
|
||||||
#include <QMouseEvent>
|
|
||||||
#include <QPainter>
|
|
||||||
#include <QPainterPath>
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <numeric>
|
#include <numeric>
|
||||||
|
|
@ -13,25 +10,23 @@
|
||||||
#define M_PI 3.14159265358979323846
|
#define M_PI 3.14159265358979323846
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
VisualizerWidget::VisualizerWidget(QWidget *parent) : QWidget(parent) {
|
static QShader loadShader(const QString &name) {
|
||||||
setAttribute(Qt::WA_OpaquePaintEvent);
|
QFile f(name);
|
||||||
setNumBins(26);
|
if (f.open(QIODevice::ReadOnly))
|
||||||
|
return QShader::fromSerialized(f.readAll());
|
||||||
|
qWarning() << "Failed to load shader:" << name;
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
#if defined(Q_OS_IOS)
|
VisualizerWidget::VisualizerWidget(QWidget *parent) : QRhiWidget(parent) {
|
||||||
// IOS Optimization: Cap internal rendering resolution
|
setSampleCount(4);
|
||||||
// Native retina (3.0) is overkill for this visualizer and kills fill-rate.
|
setNumBins(26);
|
||||||
// 2.0 is visually indistinguishable for moving graphics but much faster.
|
|
||||||
// Note: We cannot easily change the widget's DPR directly without affecting
|
|
||||||
// layout, but we can scale the painter or use a target pixmap. For now,
|
|
||||||
// simpler optimization: rely on NO Antialiasing.
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void VisualizerWidget::mouseReleaseEvent(QMouseEvent *event) {
|
void VisualizerWidget::mouseReleaseEvent(QMouseEvent *event) {
|
||||||
if (event->button() == Qt::LeftButton) {
|
if (event->button() == Qt::LeftButton)
|
||||||
emit tapDetected();
|
emit tapDetected();
|
||||||
}
|
QRhiWidget::mouseReleaseEvent(event);
|
||||||
QWidget::mouseReleaseEvent(event);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void VisualizerWidget::setNumBins(int n) {
|
void VisualizerWidget::setNumBins(int n) {
|
||||||
|
|
@ -59,17 +54,11 @@ void VisualizerWidget::setParams(bool glass, bool focus, bool albumColors,
|
||||||
m_hueFactor = hue;
|
m_hueFactor = hue;
|
||||||
m_contrast = contrast;
|
m_contrast = contrast;
|
||||||
m_brightness = brightness;
|
m_brightness = brightness;
|
||||||
|
|
||||||
// Clear cache if params change
|
|
||||||
if (!m_cache.isNull())
|
|
||||||
m_cache = QPixmap();
|
|
||||||
|
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
void VisualizerWidget::setAlbumPalette(const std::vector<QColor> &palette) {
|
void VisualizerWidget::setAlbumPalette(const std::vector<QColor> &palette) {
|
||||||
m_albumPalette.clear();
|
m_albumPalette.clear();
|
||||||
// Cast size_t to int
|
|
||||||
int targetLen = static_cast<int>(m_customBins.size()) - 1;
|
int targetLen = static_cast<int>(m_customBins.size()) - 1;
|
||||||
if (palette.empty())
|
if (palette.empty())
|
||||||
return;
|
return;
|
||||||
|
|
@ -96,13 +85,15 @@ QColor VisualizerWidget::applyModifiers(QColor c) {
|
||||||
return QColor::fromHsvF(c.hsvHueF(), s, v);
|
return QColor::fromHsvF(c.hsvHueF(), s, v);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== Spectrum Processing (unchanged) =====
|
||||||
|
|
||||||
void VisualizerWidget::updateData(
|
void VisualizerWidget::updateData(
|
||||||
const std::vector<AudioAnalyzer::FrameData> &data) {
|
const std::vector<AudioAnalyzer::FrameData> &data) {
|
||||||
if (QApplication::applicationState() != Qt::ApplicationActive || !isVisible())
|
if (!isVisible())
|
||||||
return;
|
return;
|
||||||
m_data = data;
|
m_data = data;
|
||||||
|
m_dataDirty = true;
|
||||||
|
|
||||||
// --- FPS Limit ---
|
|
||||||
qint64 now = QDateTime::currentMSecsSinceEpoch();
|
qint64 now = QDateTime::currentMSecsSinceEpoch();
|
||||||
if (now - m_lastFrameTime < (1000 / m_targetFps))
|
if (now - m_lastFrameTime < (1000 / m_targetFps))
|
||||||
return;
|
return;
|
||||||
|
|
@ -111,7 +102,7 @@ void VisualizerWidget::updateData(
|
||||||
if (m_channels.size() != data.size())
|
if (m_channels.size() != data.size())
|
||||||
m_channels.resize(data.size());
|
m_channels.resize(data.size());
|
||||||
|
|
||||||
// --- 1. Calculate Unified Glass Color (Once per frame) ---
|
// --- 1. Unified Glass Color ---
|
||||||
if (m_glass && !m_data.empty()) {
|
if (m_glass && !m_data.empty()) {
|
||||||
size_t midIdx = m_data[0].freqs.size() / 2;
|
size_t midIdx = m_data[0].freqs.size() / 2;
|
||||||
float frameMidFreq =
|
float frameMidFreq =
|
||||||
|
|
@ -125,10 +116,12 @@ void VisualizerWidget::updateData(
|
||||||
|
|
||||||
float logMin = std::log10(20.0f);
|
float logMin = std::log10(20.0f);
|
||||||
float logMax = std::log10(20000.0f);
|
float logMax = std::log10(20000.0f);
|
||||||
float frameFreqNorm = (std::log10(std::max(frameMidFreq, 1e-9f)) - logMin) /
|
float frameFreqNorm =
|
||||||
|
(std::log10(std::max(frameMidFreq, 1e-9f)) - logMin) /
|
||||||
(logMax - logMin);
|
(logMax - logMin);
|
||||||
|
|
||||||
float frameAmpNorm = std::clamp((frameMeanDb + 80.0f) / 80.0f, 0.0f, 1.0f);
|
float frameAmpNorm =
|
||||||
|
std::clamp((frameMeanDb + 80.0f) / 80.0f, 0.0f, 1.0f);
|
||||||
float frameAmpWeight = 1.0f / std::pow(frameFreqNorm + 1e-4f, 5.0f);
|
float frameAmpWeight = 1.0f / std::pow(frameFreqNorm + 1e-4f, 5.0f);
|
||||||
frameAmpWeight = std::clamp(frameAmpWeight * 2.0f, 0.5f, 6.0f);
|
frameAmpWeight = std::clamp(frameAmpWeight * 2.0f, 0.5f, 6.0f);
|
||||||
|
|
||||||
|
|
@ -139,7 +132,6 @@ void VisualizerWidget::updateData(
|
||||||
if (frameHue < 0)
|
if (frameHue < 0)
|
||||||
frameHue += 1.0f;
|
frameHue += 1.0f;
|
||||||
|
|
||||||
// OPTIMIZATION: Optimized MWA Filter for Hue (Running Sum)
|
|
||||||
float angle = frameHue * 2.0f * M_PI;
|
float angle = frameHue * 2.0f * M_PI;
|
||||||
float cosVal = std::cos(angle);
|
float cosVal = std::cos(angle);
|
||||||
float sinVal = std::sin(angle);
|
float sinVal = std::sin(angle);
|
||||||
|
|
@ -169,24 +161,20 @@ void VisualizerWidget::updateData(
|
||||||
for (size_t ch = 0; ch < data.size(); ++ch) {
|
for (size_t ch = 0; ch < data.size(); ++ch) {
|
||||||
const auto &db = data[ch].db;
|
const auto &db = data[ch].db;
|
||||||
const auto &primaryDb = data[ch].primaryDb;
|
const auto &primaryDb = data[ch].primaryDb;
|
||||||
const auto &freqs = data[ch].freqs;
|
|
||||||
|
|
||||||
size_t numBins = db.size();
|
size_t numBins = db.size();
|
||||||
auto &bins = m_channels[ch].bins;
|
auto &bins = m_channels[ch].bins;
|
||||||
if (bins.size() != numBins)
|
if (bins.size() != numBins)
|
||||||
bins.resize(numBins);
|
bins.resize(numBins);
|
||||||
|
|
||||||
// Pre-calculate energy for pattern logic
|
|
||||||
std::vector<float> vertexEnergy(numBins);
|
std::vector<float> vertexEnergy(numBins);
|
||||||
float globalMax = 0.001f;
|
float globalMax = 0.001f;
|
||||||
|
|
||||||
// Physics & Energy Calculation
|
|
||||||
for (size_t i = 0; i < numBins; ++i) {
|
for (size_t i = 0; i < numBins; ++i) {
|
||||||
auto &bin = bins[i];
|
auto &bin = bins[i];
|
||||||
float rawVal = db[i];
|
float rawVal = db[i];
|
||||||
float primaryVal = (i < primaryDb.size()) ? primaryDb[i] : rawVal;
|
float primaryVal = (i < primaryDb.size()) ? primaryDb[i] : rawVal;
|
||||||
|
|
||||||
// Physics
|
|
||||||
float responsiveness = 0.2f;
|
float responsiveness = 0.2f;
|
||||||
bin.visualDb =
|
bin.visualDb =
|
||||||
(bin.visualDb * (1.0f - responsiveness)) + (rawVal * responsiveness);
|
(bin.visualDb * (1.0f - responsiveness)) + (rawVal * responsiveness);
|
||||||
|
|
@ -195,12 +183,10 @@ void VisualizerWidget::updateData(
|
||||||
bin.primaryVisualDb = (bin.primaryVisualDb * (1.0f - patternResp)) +
|
bin.primaryVisualDb = (bin.primaryVisualDb * (1.0f - patternResp)) +
|
||||||
(primaryVal * patternResp);
|
(primaryVal * patternResp);
|
||||||
|
|
||||||
// Energy for Pattern
|
|
||||||
vertexEnergy[i] =
|
vertexEnergy[i] =
|
||||||
std::clamp((bin.primaryVisualDb + 80.0f) / 80.0f, 0.0f, 1.0f);
|
std::clamp((bin.primaryVisualDb + 80.0f) / 80.0f, 0.0f, 1.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-Balance Highs vs Lows
|
|
||||||
size_t splitIdx = numBins / 2;
|
size_t splitIdx = numBins / 2;
|
||||||
float maxLow = 0.01f;
|
float maxLow = 0.01f;
|
||||||
float maxHigh = 0.01f;
|
float maxHigh = 0.01f;
|
||||||
|
|
@ -209,14 +195,12 @@ void VisualizerWidget::updateData(
|
||||||
for (size_t j = splitIdx; j < numBins; ++j)
|
for (size_t j = splitIdx; j < numBins; ++j)
|
||||||
maxHigh = std::max(maxHigh, vertexEnergy[j]);
|
maxHigh = std::max(maxHigh, vertexEnergy[j]);
|
||||||
|
|
||||||
float trebleBoost = maxLow / maxHigh;
|
float trebleBoost = std::clamp(maxLow / maxHigh, 1.0f, 40.0f);
|
||||||
trebleBoost = std::clamp(trebleBoost, 1.0f, 40.0f);
|
|
||||||
|
|
||||||
for (size_t j = 0; j < numBins; ++j) {
|
for (size_t j = 0; j < numBins; ++j) {
|
||||||
if (j >= splitIdx) {
|
if (j >= splitIdx) {
|
||||||
float t = (float)(j - splitIdx) / (numBins - splitIdx);
|
float t = (float)(j - splitIdx) / (numBins - splitIdx);
|
||||||
float boost = 1.0f + (trebleBoost - 1.0f) * t;
|
vertexEnergy[j] *= 1.0f + (trebleBoost - 1.0f) * t;
|
||||||
vertexEnergy[j] *= boost;
|
|
||||||
}
|
}
|
||||||
float compressed = std::tanh(vertexEnergy[j]);
|
float compressed = std::tanh(vertexEnergy[j]);
|
||||||
vertexEnergy[j] = compressed;
|
vertexEnergy[j] = compressed;
|
||||||
|
|
@ -226,14 +210,12 @@ void VisualizerWidget::updateData(
|
||||||
for (float &v : vertexEnergy)
|
for (float &v : vertexEnergy)
|
||||||
v = std::clamp(v / globalMax, 0.0f, 1.0f);
|
v = std::clamp(v / globalMax, 0.0f, 1.0f);
|
||||||
|
|
||||||
// --- 3. Calculate Procedural Pattern (Modifiers) ---
|
// --- 3. Procedural Pattern ---
|
||||||
// Reset modifiers
|
|
||||||
for (auto &b : bins) {
|
for (auto &b : bins) {
|
||||||
b.brightMod = 0.0f;
|
b.brightMod = 0.0f;
|
||||||
b.alphaMod = 0.0f;
|
b.alphaMod = 0.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cast size_t to int for loop bounds or use size_t consistently
|
|
||||||
for (size_t i = 1; i < vertexEnergy.size() - 1; ++i) {
|
for (size_t i = 1; i < vertexEnergy.size() - 1; ++i) {
|
||||||
float curr = vertexEnergy[i];
|
float curr = vertexEnergy[i];
|
||||||
float prev = vertexEnergy[i - 1];
|
float prev = vertexEnergy[i - 1];
|
||||||
|
|
@ -247,34 +229,29 @@ void VisualizerWidget::updateData(
|
||||||
float decayBase = 0.65f - std::clamp(sharpness * 3.0f, 0.0f, 0.35f);
|
float decayBase = 0.65f - std::clamp(sharpness * 3.0f, 0.0f, 0.35f);
|
||||||
|
|
||||||
auto applyPattern = [&](int dist, bool isBrightSide, int direction) {
|
auto applyPattern = [&](int dist, bool isBrightSide, int direction) {
|
||||||
// Cast size_t i to int for arithmetic
|
|
||||||
int segIdx = (direction == -1) ? (static_cast<int>(i) - dist)
|
int segIdx = (direction == -1) ? (static_cast<int>(i) - dist)
|
||||||
: (static_cast<int>(i) + dist - 1);
|
: (static_cast<int>(i) + dist - 1);
|
||||||
// Cast bins.size() to int
|
|
||||||
if (segIdx < 0 || segIdx >= static_cast<int>(bins.size()))
|
if (segIdx < 0 || segIdx >= static_cast<int>(bins.size()))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
int cycle = (dist - 1) / 3;
|
int cycle = (dist - 1) / 3;
|
||||||
int step = (dist - 1) % 3;
|
int step = (dist - 1) % 3;
|
||||||
float decay = std::pow(decayBase, cycle);
|
float decay = std::pow(decayBase, cycle);
|
||||||
float intensity = peakIntensity * decay;
|
float intensity = peakIntensity * decay;
|
||||||
if (intensity < 0.01f)
|
if (intensity < 0.01f)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
int type = step;
|
int type = step;
|
||||||
if (isBrightSide)
|
if (isBrightSide)
|
||||||
type = (type + 2) % 3;
|
type = (type + 2) % 3;
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 0: // Ghost
|
case 0:
|
||||||
bins[segIdx].brightMod += 0.8f * intensity;
|
bins[segIdx].brightMod += 0.8f * intensity;
|
||||||
bins[segIdx].alphaMod -= 0.8f * intensity;
|
bins[segIdx].alphaMod -= 0.8f * intensity;
|
||||||
break;
|
break;
|
||||||
case 1: // Shadow
|
case 1:
|
||||||
bins[segIdx].brightMod -= 0.8f * intensity;
|
bins[segIdx].brightMod -= 0.8f * intensity;
|
||||||
bins[segIdx].alphaMod += 0.2f * intensity;
|
bins[segIdx].alphaMod += 0.2f * intensity;
|
||||||
break;
|
break;
|
||||||
case 2: // Highlight
|
case 2:
|
||||||
bins[segIdx].brightMod += 0.8f * intensity;
|
bins[segIdx].brightMod += 0.8f * intensity;
|
||||||
bins[segIdx].alphaMod += 0.2f * intensity;
|
bins[segIdx].alphaMod += 0.2f * intensity;
|
||||||
break;
|
break;
|
||||||
|
|
@ -295,8 +272,8 @@ void VisualizerWidget::updateData(
|
||||||
if (m_useAlbumColors && !m_albumPalette.empty()) {
|
if (m_useAlbumColors && !m_albumPalette.empty()) {
|
||||||
int palIdx = static_cast<int>(i);
|
int palIdx = static_cast<int>(i);
|
||||||
if (m_mirrored)
|
if (m_mirrored)
|
||||||
palIdx =
|
palIdx = static_cast<int>(m_albumPalette.size()) - 1 -
|
||||||
static_cast<int>(m_albumPalette.size()) - 1 - static_cast<int>(i);
|
static_cast<int>(i);
|
||||||
palIdx =
|
palIdx =
|
||||||
std::clamp(palIdx, 0, static_cast<int>(m_albumPalette.size()) - 1);
|
std::clamp(palIdx, 0, static_cast<int>(m_albumPalette.size()) - 1);
|
||||||
binColor = m_albumPalette[palIdx];
|
binColor = m_albumPalette[palIdx];
|
||||||
|
|
@ -313,101 +290,217 @@ void VisualizerWidget::updateData(
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
void VisualizerWidget::paintEvent(QPaintEvent *) {
|
// ===== QRhiWidget GPU Rendering =====
|
||||||
if (QApplication::applicationState() != Qt::ApplicationActive || !isVisible())
|
|
||||||
|
void VisualizerWidget::initialize(QRhiCommandBuffer *cb) {
|
||||||
|
if (m_rhi != rhi()) {
|
||||||
|
m_fillPipeline.reset();
|
||||||
|
m_rhi = rhi();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_fillPipeline) {
|
||||||
|
m_ubufAlign = m_rhi->ubufAlignment();
|
||||||
|
|
||||||
|
// Vertex buffer: dynamic, sized for worst case
|
||||||
|
m_vbuf.reset(m_rhi->newBuffer(QRhiBuffer::Dynamic,
|
||||||
|
QRhiBuffer::VertexBuffer,
|
||||||
|
2048 * 6 * sizeof(float)));
|
||||||
|
m_vbuf->create();
|
||||||
|
|
||||||
|
// Uniform buffer: 4 aligned MVP matrices (for mirrored mode)
|
||||||
|
m_ubuf.reset(m_rhi->newBuffer(QRhiBuffer::Dynamic,
|
||||||
|
QRhiBuffer::UniformBuffer,
|
||||||
|
m_ubufAlign * 4));
|
||||||
|
m_ubuf->create();
|
||||||
|
|
||||||
|
// Shader resource bindings with dynamic UBO offset
|
||||||
|
m_srb.reset(m_rhi->newShaderResourceBindings());
|
||||||
|
m_srb->setBindings({QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(
|
||||||
|
0, QRhiShaderResourceBinding::VertexStage, m_ubuf.get(), 64)});
|
||||||
|
m_srb->create();
|
||||||
|
|
||||||
|
// Load compiled shaders
|
||||||
|
QShader vs = loadShader(QStringLiteral(":/shaders/visualizer.vert.qsb"));
|
||||||
|
QShader fs = loadShader(QStringLiteral(":/shaders/visualizer.frag.qsb"));
|
||||||
|
|
||||||
|
// Vertex layout: [x, y, r, g, b, a] = 6 floats, 24 bytes stride
|
||||||
|
QRhiVertexInputLayout inputLayout;
|
||||||
|
inputLayout.setBindings({{6 * sizeof(float)}});
|
||||||
|
inputLayout.setAttributes(
|
||||||
|
{{0, 0, QRhiVertexInputAttribute::Float2, 0},
|
||||||
|
{0, 1, QRhiVertexInputAttribute::Float4, 2 * sizeof(float)}});
|
||||||
|
|
||||||
|
// Alpha blending
|
||||||
|
QRhiGraphicsPipeline::TargetBlend blend;
|
||||||
|
blend.enable = true;
|
||||||
|
blend.srcColor = QRhiGraphicsPipeline::SrcAlpha;
|
||||||
|
blend.dstColor = QRhiGraphicsPipeline::OneMinusSrcAlpha;
|
||||||
|
blend.srcAlpha = QRhiGraphicsPipeline::One;
|
||||||
|
blend.dstAlpha = QRhiGraphicsPipeline::OneMinusSrcAlpha;
|
||||||
|
|
||||||
|
// Fill pipeline (triangles)
|
||||||
|
m_fillPipeline.reset(m_rhi->newGraphicsPipeline());
|
||||||
|
m_fillPipeline->setShaderStages(
|
||||||
|
{{QRhiShaderStage::Vertex, vs}, {QRhiShaderStage::Fragment, fs}});
|
||||||
|
m_fillPipeline->setVertexInputLayout(inputLayout);
|
||||||
|
m_fillPipeline->setShaderResourceBindings(m_srb.get());
|
||||||
|
m_fillPipeline->setRenderPassDescriptor(renderTarget()->renderPassDescriptor());
|
||||||
|
m_fillPipeline->setTopology(QRhiGraphicsPipeline::Triangles);
|
||||||
|
m_fillPipeline->setTargetBlends({blend});
|
||||||
|
m_fillPipeline->setSampleCount(sampleCount());
|
||||||
|
m_fillPipeline->create();
|
||||||
|
|
||||||
|
// Line pipeline (same shader, line topology)
|
||||||
|
m_linePipeline.reset(m_rhi->newGraphicsPipeline());
|
||||||
|
m_linePipeline->setShaderStages(
|
||||||
|
{{QRhiShaderStage::Vertex, vs}, {QRhiShaderStage::Fragment, fs}});
|
||||||
|
m_linePipeline->setVertexInputLayout(inputLayout);
|
||||||
|
m_linePipeline->setShaderResourceBindings(m_srb.get());
|
||||||
|
m_linePipeline->setRenderPassDescriptor(renderTarget()->renderPassDescriptor());
|
||||||
|
m_linePipeline->setTopology(QRhiGraphicsPipeline::Lines);
|
||||||
|
m_linePipeline->setTargetBlends({blend});
|
||||||
|
m_linePipeline->setSampleCount(sampleCount());
|
||||||
|
m_linePipeline->create();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void VisualizerWidget::render(QRhiCommandBuffer *cb) {
|
||||||
|
if (!m_fillPipeline)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
QPainter p(this);
|
const QSize outputSize = renderTarget()->pixelSize();
|
||||||
p.fillRect(rect(), Qt::black);
|
|
||||||
|
|
||||||
#if defined(Q_OS_IOS)
|
|
||||||
// iOS Optimization: Disable Antialiasing for performance
|
|
||||||
// Retina screens are high density enough that AA is often not needed
|
|
||||||
#else
|
|
||||||
p.setRenderHint(QPainter::Antialiasing);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (m_data.empty())
|
|
||||||
return;
|
|
||||||
|
|
||||||
int w = width();
|
int w = width();
|
||||||
int h = height();
|
int h = height();
|
||||||
|
|
||||||
|
// Only rebuild vertices when new data has arrived
|
||||||
|
if (m_dataDirty) {
|
||||||
|
m_dataDirty = false;
|
||||||
|
if (m_mirrored)
|
||||||
|
buildVertices(w / 2, h / 2);
|
||||||
|
else
|
||||||
|
buildVertices(w, h);
|
||||||
|
}
|
||||||
|
|
||||||
|
int numPasses = m_mirrored ? 4 : 1;
|
||||||
|
|
||||||
|
// Prepare resource updates
|
||||||
|
QRhiResourceUpdateBatch *u = m_rhi->nextResourceUpdateBatch();
|
||||||
|
|
||||||
|
// Upload vertex data
|
||||||
|
if (!m_vertices.empty()) {
|
||||||
|
int dataSize = static_cast<int>(m_vertices.size() * sizeof(float));
|
||||||
|
if (dataSize > m_vbuf->size()) {
|
||||||
|
m_vbuf->setSize(dataSize);
|
||||||
|
m_vbuf->create();
|
||||||
|
}
|
||||||
|
u->updateDynamicBuffer(m_vbuf.get(), 0, dataSize, m_vertices.data());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload MVP matrices
|
||||||
|
QMatrix4x4 correction = m_rhi->clipSpaceCorrMatrix();
|
||||||
|
|
||||||
|
for (int i = 0; i < numPasses; i++) {
|
||||||
|
QMatrix4x4 proj;
|
||||||
|
proj.ortho(0, (float)w, (float)h, 0, -1, 1);
|
||||||
|
|
||||||
if (m_mirrored) {
|
if (m_mirrored) {
|
||||||
// --- Single Quadrant Optimization ---
|
switch (i) {
|
||||||
int hw = w / 2;
|
case 0: break;
|
||||||
int hh = h / 2;
|
case 1:
|
||||||
|
proj.translate(w, 0, 0);
|
||||||
// Rebuild cache if size changed or cache is invalid
|
proj.scale(-1, 1, 1);
|
||||||
if (m_cache.size() != QSize(hw, hh)) {
|
break;
|
||||||
m_cache = QPixmap(hw, hh);
|
case 2:
|
||||||
m_cache.fill(Qt::transparent);
|
proj.translate(0, h, 0);
|
||||||
}
|
proj.scale(1, -1, 1);
|
||||||
|
break;
|
||||||
// Draw ONLY the first quadrant into the cache
|
case 3:
|
||||||
// We use a separate painter for the cache
|
proj.translate(w, h, 0);
|
||||||
{
|
proj.scale(-1, -1, 1);
|
||||||
m_cache.fill(Qt::transparent); // Clear old frame
|
break;
|
||||||
QPainter cachePainter(&m_cache);
|
|
||||||
#if !defined(Q_OS_IOS)
|
|
||||||
cachePainter.setRenderHint(QPainter::Antialiasing);
|
|
||||||
#endif
|
|
||||||
drawContent(cachePainter, hw, hh);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now just blit the texture 4 times
|
|
||||||
p.drawPixmap(0, 0, m_cache);
|
|
||||||
|
|
||||||
p.save();
|
|
||||||
p.translate(w, 0);
|
|
||||||
p.scale(-1, 1);
|
|
||||||
p.drawPixmap(0, 0, m_cache);
|
|
||||||
p.restore();
|
|
||||||
|
|
||||||
p.save();
|
|
||||||
p.translate(0, h);
|
|
||||||
p.scale(1, -1);
|
|
||||||
p.drawPixmap(0, 0, m_cache);
|
|
||||||
p.restore();
|
|
||||||
|
|
||||||
p.save();
|
|
||||||
p.translate(w, h);
|
|
||||||
p.scale(-1, -1);
|
|
||||||
p.drawPixmap(0, 0, m_cache);
|
|
||||||
p.restore();
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// Standard full draw
|
|
||||||
drawContent(p, w, h);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void VisualizerWidget::drawContent(QPainter &p, int w, int h) {
|
QMatrix4x4 mvp = correction * proj;
|
||||||
// --- Draw Trails REMOVED ---
|
u->updateDynamicBuffer(m_ubuf.get(), i * m_ubufAlign, 64,
|
||||||
|
mvp.constData());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Begin render pass
|
||||||
|
cb->beginPass(renderTarget(), QColor(0, 0, 0, 255), {1.0f, 0}, u);
|
||||||
|
cb->setViewport({0, 0, (float)outputSize.width(),
|
||||||
|
(float)outputSize.height()});
|
||||||
|
|
||||||
|
const QRhiCommandBuffer::VertexInput vbufBinding(m_vbuf.get(), 0);
|
||||||
|
|
||||||
|
for (int i = 0; i < numPasses; i++) {
|
||||||
|
QRhiCommandBuffer::DynamicOffset dynOfs(0, quint32(i * m_ubufAlign));
|
||||||
|
|
||||||
|
if (m_fillVertexCount > 0) {
|
||||||
|
cb->setGraphicsPipeline(m_fillPipeline.get());
|
||||||
|
cb->setShaderResources(m_srb.get(), 1, &dynOfs);
|
||||||
|
cb->setVertexInput(0, 1, &vbufBinding);
|
||||||
|
cb->draw(m_fillVertexCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_lineVertexCount > 0) {
|
||||||
|
cb->setGraphicsPipeline(m_linePipeline.get());
|
||||||
|
cb->setShaderResources(m_srb.get(), 1, &dynOfs);
|
||||||
|
cb->setVertexInput(0, 1, &vbufBinding);
|
||||||
|
cb->draw(m_lineVertexCount, 1, m_fillVertexCount, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cb->endPass();
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void VisualizerWidget::releaseResources() {
|
||||||
|
m_linePipeline.reset();
|
||||||
|
m_fillPipeline.reset();
|
||||||
|
m_srb.reset();
|
||||||
|
m_ubuf.reset();
|
||||||
|
m_vbuf.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Vertex Building (identical logic to old drawContent) =====
|
||||||
|
|
||||||
|
void VisualizerWidget::buildVertices(int w, int h) {
|
||||||
|
m_vertices.clear();
|
||||||
|
m_fillVertexCount = 0;
|
||||||
|
m_lineVertexCount = 0;
|
||||||
|
|
||||||
|
std::vector<float> lineVerts;
|
||||||
|
|
||||||
// --- Draw Bars ---
|
|
||||||
for (size_t ch = 0; ch < m_channels.size(); ++ch) {
|
for (size_t ch = 0; ch < m_channels.size(); ++ch) {
|
||||||
|
if (ch >= m_data.size())
|
||||||
|
break;
|
||||||
const auto &freqs = m_data[ch].freqs;
|
const auto &freqs = m_data[ch].freqs;
|
||||||
const auto &bins = m_channels[ch].bins;
|
const auto &bins = m_channels[ch].bins;
|
||||||
if (bins.empty())
|
if (bins.empty() || freqs.size() < 2)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
float xOffset = (ch == 1 && m_data.size() > 1) ? 1.005f : 1.0f;
|
float xOffset = (ch == 1 && m_data.size() > 1) ? 1.005f : 1.0f;
|
||||||
|
|
||||||
for (size_t i = 0; i < freqs.size() - 1; ++i) {
|
for (size_t i = 0; i + 1 < freqs.size(); ++i) {
|
||||||
|
if (i + 1 >= bins.size())
|
||||||
|
break;
|
||||||
const auto &b = bins[i];
|
const auto &b = bins[i];
|
||||||
const auto &bNext = bins[i + 1];
|
const auto &bNext = bins[i + 1];
|
||||||
|
|
||||||
// Calculate Final Color using pre-calculated modifiers
|
// --- Brightness ---
|
||||||
float avgEnergy =
|
float avgEnergy =
|
||||||
std::clamp((b.primaryVisualDb + 80.0f) / 80.0f, 0.0f, 1.0f);
|
std::clamp((b.primaryVisualDb + 80.0f) / 80.0f, 0.0f, 1.0f);
|
||||||
float baseBrightness = std::pow(avgEnergy, 0.5f);
|
float baseBrightness = std::pow(avgEnergy, 0.5f);
|
||||||
|
|
||||||
float bMod = b.brightMod;
|
float bMod = b.brightMod;
|
||||||
float bMult = (bMod >= 0) ? (1.0f + bMod) : (1.0f / (1.0f - bMod * 2.0f));
|
float bMult =
|
||||||
|
(bMod >= 0) ? (1.0f + bMod) : (1.0f / (1.0f - bMod * 2.0f));
|
||||||
float finalBrightness =
|
float finalBrightness =
|
||||||
std::clamp(baseBrightness * bMult * m_brightness, 0.0f, 1.0f);
|
std::clamp(baseBrightness * bMult * m_brightness, 0.0f, 1.0f);
|
||||||
|
|
||||||
|
// --- Color ---
|
||||||
QColor dynamicBinColor = b.cachedColor;
|
QColor dynamicBinColor = b.cachedColor;
|
||||||
float h_val, s, v, a;
|
float h_val, s, v, a;
|
||||||
dynamicBinColor.getHsvF(&h_val, &s, &v, &a);
|
dynamicBinColor.getHsvF(&h_val, &s, &v, &a);
|
||||||
|
|
@ -426,17 +519,18 @@ void VisualizerWidget::drawContent(QPainter &p, int w, int h) {
|
||||||
lineColor = dynamicBinColor;
|
lineColor = dynamicBinColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Alpha ---
|
||||||
float aMod = b.alphaMod;
|
float aMod = b.alphaMod;
|
||||||
float aMult = (aMod >= 0) ? (1.0f + aMod * 0.5f) : (1.0f + aMod);
|
float aMult = (aMod >= 0) ? (1.0f + aMod * 0.5f) : (1.0f + aMod);
|
||||||
if (aMult < 0.1f)
|
if (aMult < 0.1f)
|
||||||
aMult = 0.1f;
|
aMult = 0.1f;
|
||||||
|
|
||||||
float alpha = 0.4f + (avgEnergy - 0.5f) * m_contrast;
|
float alpha = 0.4f + (avgEnergy - 0.5f) * m_contrast;
|
||||||
alpha = std::clamp(alpha * aMult, 0.0f, 1.0f);
|
alpha = std::clamp(alpha * aMult, 0.0f, 1.0f);
|
||||||
|
|
||||||
fillColor.setAlphaF(alpha);
|
fillColor.setAlphaF(alpha);
|
||||||
lineColor.setAlphaF(0.9f);
|
lineColor.setAlphaF(0.9f);
|
||||||
|
|
||||||
|
// --- Channel 1 tint ---
|
||||||
if (ch == 1 && m_data.size() > 1) {
|
if (ch == 1 && m_data.size() > 1) {
|
||||||
int r, g, b_val, a_val;
|
int r, g, b_val, a_val;
|
||||||
fillColor.getRgb(&r, &g, &b_val, &a_val);
|
fillColor.getRgb(&r, &g, &b_val, &a_val);
|
||||||
|
|
@ -447,32 +541,46 @@ void VisualizerWidget::drawContent(QPainter &p, int w, int h) {
|
||||||
std::min(255, b_val + 40), a_val);
|
std::min(255, b_val + 40), a_val);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Geometry ---
|
||||||
float x1 = getX(freqs[i] * xOffset) * w;
|
float x1 = getX(freqs[i] * xOffset) * w;
|
||||||
float x2 = getX(freqs[i + 1] * xOffset) * w;
|
float x2 = getX(freqs[i + 1] * xOffset) * w;
|
||||||
|
|
||||||
float barH1 =
|
float barH1 =
|
||||||
std::clamp((b.visualDb + 80.0f) / 80.0f * h, 0.0f, (float)h);
|
std::clamp((b.visualDb + 80.0f) / 80.0f * h, 0.0f, (float)h);
|
||||||
float barH2 =
|
float barH2 =
|
||||||
std::clamp((bNext.visualDb + 80.0f) / 80.0f * h, 0.0f, (float)h);
|
std::clamp((bNext.visualDb + 80.0f) / 80.0f * h, 0.0f, (float)h);
|
||||||
|
|
||||||
// Always anchor bottom
|
|
||||||
float anchorY = h;
|
float anchorY = h;
|
||||||
float y1 = h - barH1;
|
float y1 = h - barH1;
|
||||||
float y2 = h - barH2;
|
float y2 = h - barH2;
|
||||||
|
|
||||||
QPainterPath fillPath;
|
float fr = fillColor.redF(), fg = fillColor.greenF(),
|
||||||
fillPath.moveTo(x1, anchorY);
|
fb = fillColor.blueF(), fa = fillColor.alphaF();
|
||||||
fillPath.lineTo(x1, y1);
|
|
||||||
fillPath.lineTo(x2, y2);
|
|
||||||
fillPath.lineTo(x2, anchorY);
|
|
||||||
fillPath.closeSubpath();
|
|
||||||
p.fillPath(fillPath, fillColor);
|
|
||||||
|
|
||||||
p.setPen(QPen(lineColor, 1));
|
// Triangle 1
|
||||||
p.drawLine(QPointF(x1, anchorY), QPointF(x1, y1));
|
m_vertices.insert(m_vertices.end(),
|
||||||
if (i == freqs.size() - 2) {
|
{x1, anchorY, fr, fg, fb, fa, x1, y1, fr, fg, fb, fa,
|
||||||
p.drawLine(QPointF(x2, anchorY), QPointF(x2, y2));
|
x2, y2, fr, fg, fb, fa});
|
||||||
|
// Triangle 2
|
||||||
|
m_vertices.insert(m_vertices.end(),
|
||||||
|
{x1, anchorY, fr, fg, fb, fa, x2, y2, fr, fg, fb, fa,
|
||||||
|
x2, anchorY, fr, fg, fb, fa});
|
||||||
|
m_fillVertexCount += 6;
|
||||||
|
|
||||||
|
float lr = lineColor.redF(), lg = lineColor.greenF(),
|
||||||
|
lb = lineColor.blueF(), la = lineColor.alphaF();
|
||||||
|
|
||||||
|
// Left edge
|
||||||
|
lineVerts.insert(lineVerts.end(),
|
||||||
|
{x1, anchorY, lr, lg, lb, la, x1, y1, lr, lg, lb, la});
|
||||||
|
|
||||||
|
// Right edge (last bin only)
|
||||||
|
if (i + 2 == freqs.size()) {
|
||||||
|
lineVerts.insert(
|
||||||
|
lineVerts.end(),
|
||||||
|
{x2, anchorY, lr, lg, lb, la, x2, y2, lr, lg, lb, la});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m_lineVertexCount = static_cast<int>(lineVerts.size()) / 6;
|
||||||
|
m_vertices.insert(m_vertices.end(), lineVerts.begin(), lineVerts.end());
|
||||||
}
|
}
|
||||||
|
|
@ -1,14 +1,16 @@
|
||||||
// src/VisualizerWidget.h
|
// src/VisualizerWidget.h
|
||||||
#pragma once
|
#pragma once
|
||||||
#include <QWidget>
|
#include <QMatrix4x4>
|
||||||
#include <QMouseEvent>
|
#include <QMouseEvent>
|
||||||
#include <vector>
|
#include <QRhiWidget>
|
||||||
#include <deque>
|
#include <deque>
|
||||||
#include <utility> // For std::pair
|
#include <memory>
|
||||||
#include <QPointF>
|
#include <rhi/qrhi.h>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
#include "AudioEngine.h"
|
#include "AudioEngine.h"
|
||||||
|
|
||||||
class VisualizerWidget : public QWidget {
|
class VisualizerWidget : public QRhiWidget {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
VisualizerWidget(QWidget* parent = nullptr);
|
VisualizerWidget(QWidget* parent = nullptr);
|
||||||
|
|
@ -22,18 +24,18 @@ signals:
|
||||||
void tapDetected();
|
void tapDetected();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void paintEvent(QPaintEvent* e) override;
|
void initialize(QRhiCommandBuffer *cb) override;
|
||||||
|
void render(QRhiCommandBuffer *cb) override;
|
||||||
|
void releaseResources() override;
|
||||||
void mouseReleaseEvent(QMouseEvent* event) override;
|
void mouseReleaseEvent(QMouseEvent* event) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void drawContent(QPainter& p, int w, int h);
|
void buildVertices(int w, int h);
|
||||||
|
|
||||||
struct BinState {
|
struct BinState {
|
||||||
float visualDb = -100.0f; // Mixed (Height)
|
float visualDb = -100.0f;
|
||||||
float primaryVisualDb = -100.0f; // Primary (Pattern)
|
float primaryVisualDb = -100.0f;
|
||||||
float lastRawDb = -100.0f; // To calculate flux
|
float lastRawDb = -100.0f;
|
||||||
|
|
||||||
// Pre-calculated visual modifiers (Optimization)
|
|
||||||
float brightMod = 0.0f;
|
float brightMod = 0.0f;
|
||||||
float alphaMod = 0.0f;
|
float alphaMod = 0.0f;
|
||||||
QColor cachedColor;
|
QColor cachedColor;
|
||||||
|
|
@ -48,13 +50,11 @@ private:
|
||||||
std::vector<QColor> m_albumPalette;
|
std::vector<QColor> m_albumPalette;
|
||||||
std::vector<float> m_customBins;
|
std::vector<float> m_customBins;
|
||||||
|
|
||||||
// Hue Smoothing History (Cos, Sin)
|
|
||||||
std::deque<std::pair<float, float>> m_hueHistory;
|
std::deque<std::pair<float, float>> m_hueHistory;
|
||||||
float m_hueSumCos = 0.0f;
|
float m_hueSumCos = 0.0f;
|
||||||
float m_hueSumSin = 0.0f;
|
float m_hueSumSin = 0.0f;
|
||||||
|
|
||||||
QColor m_unifiedColor = Qt::white; // Calculated in updateData
|
QColor m_unifiedColor = Qt::white;
|
||||||
QPixmap m_cache; // For mirrored mode optimization
|
|
||||||
|
|
||||||
bool m_glass = true;
|
bool m_glass = true;
|
||||||
bool m_focus = false;
|
bool m_focus = false;
|
||||||
|
|
@ -66,6 +66,21 @@ private:
|
||||||
|
|
||||||
int m_targetFps = 60;
|
int m_targetFps = 60;
|
||||||
qint64 m_lastFrameTime = 0;
|
qint64 m_lastFrameTime = 0;
|
||||||
|
bool m_dataDirty = false;
|
||||||
|
|
||||||
|
// RHI resources
|
||||||
|
QRhi *m_rhi = nullptr;
|
||||||
|
std::unique_ptr<QRhiGraphicsPipeline> m_fillPipeline;
|
||||||
|
std::unique_ptr<QRhiGraphicsPipeline> m_linePipeline;
|
||||||
|
std::unique_ptr<QRhiBuffer> m_vbuf;
|
||||||
|
std::unique_ptr<QRhiBuffer> m_ubuf;
|
||||||
|
std::unique_ptr<QRhiShaderResourceBindings> m_srb;
|
||||||
|
int m_ubufAlign = 256;
|
||||||
|
|
||||||
|
// Per-frame vertex staging
|
||||||
|
std::vector<float> m_vertices;
|
||||||
|
int m_fillVertexCount = 0;
|
||||||
|
int m_lineVertexCount = 0;
|
||||||
|
|
||||||
float getX(float freq);
|
float getX(float freq);
|
||||||
QColor applyModifiers(QColor c);
|
QColor applyModifiers(QColor c);
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ int main(int argc, char *argv[]) {
|
||||||
QApplication::setApplicationVersion("1.0");
|
QApplication::setApplicationVersion("1.0");
|
||||||
|
|
||||||
qRegisterMetaType<std::vector<AudioAnalyzer::FrameData>>("std::vector<AudioAnalyzer::FrameData>");
|
qRegisterMetaType<std::vector<AudioAnalyzer::FrameData>>("std::vector<AudioAnalyzer::FrameData>");
|
||||||
|
qRegisterMetaType<std::shared_ptr<TrackData>>("std::shared_ptr<TrackData>");
|
||||||
|
|
||||||
QPalette p = app.palette();
|
QPalette p = app.palette();
|
||||||
p.setColor(QPalette::Window, Qt::black);
|
p.setColor(QPalette::Window, Qt::black);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue