gpu/parallel rendering upgrade overhaul. but bugs exist still.

This commit is contained in:
pszsh 2026-02-26 01:02:35 -08:00
parent 61e220f185
commit 30cecf586c
13 changed files with 517 additions and 179 deletions

View File

@ -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}
) )

View File

@ -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..."

9
shaders/visualizer.frag Normal file
View File

@ -0,0 +1,9 @@
#version 440
layout(location = 0) in vec4 v_color;
layout(location = 0) out vec4 fragColor;
void main()
{
fragColor = v_color;
}

16
shaders/visualizer.vert Normal file
View File

@ -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;
}

View File

@ -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,9 +540,18 @@ 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);
for (int i = 0; i < m_frameSize; ++i) { if (useComplex) {
ch0[i] = m_data->complexData[(sampleIdx + i) * 2]; for (int i = 0; i < m_frameSize; ++i) {
ch1[i] = m_data->complexData[(sampleIdx + i) * 2 + 1]; ch0[i] = m_data->complexData[(sampleIdx + i) * 2];
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

View File

@ -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;
}; };

View File

@ -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) {

View File

@ -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;
}; };

View File

@ -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() {

View File

@ -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

View File

@ -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 =
(logMax - logMin); (std::log10(std::max(frameMidFreq, 1e-9f)) - 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();
if (m_mirrored) { // Only rebuild vertices when new data has arrived
// --- Single Quadrant Optimization --- if (m_dataDirty) {
int hw = w / 2; m_dataDirty = false;
int hh = h / 2; if (m_mirrored)
buildVertices(w / 2, h / 2);
// Rebuild cache if size changed or cache is invalid else
if (m_cache.size() != QSize(hw, hh)) { buildVertices(w, h);
m_cache = QPixmap(hw, hh);
m_cache.fill(Qt::transparent);
}
// Draw ONLY the first quadrant into the cache
// We use a separate painter for the cache
{
m_cache.fill(Qt::transparent); // Clear old frame
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);
} }
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) {
switch (i) {
case 0: break;
case 1:
proj.translate(w, 0, 0);
proj.scale(-1, 1, 1);
break;
case 2:
proj.translate(0, h, 0);
proj.scale(1, -1, 1);
break;
case 3:
proj.translate(w, h, 0);
proj.scale(-1, -1, 1);
break;
}
}
QMatrix4x4 mvp = correction * proj;
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::drawContent(QPainter &p, int w, int h) { void VisualizerWidget::releaseResources() {
// --- Draw Trails REMOVED --- 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());
} }

View File

@ -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);

View File

@ -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);