From 30cecf586cc1273dd6bec7fbafa3c624dc7ae076 Mon Sep 17 00:00:00 2001 From: pszsh Date: Thu, 26 Feb 2026 01:02:35 -0800 Subject: [PATCH] gpu/parallel rendering upgrade overhaul. but bugs exist still. --- CMakeLists.txt | 27 ++- Makefile | 2 +- shaders/visualizer.frag | 9 + shaders/visualizer.vert | 16 ++ src/AudioEngine.cpp | 37 +++- src/AudioEngine.h | 32 ++++ src/CommonWidgets.cpp | 101 +++++++++-- src/CommonWidgets.h | 8 + src/Utils.cpp | 22 +++ src/Utils.h | 4 + src/VisualizerWidget.cpp | 382 +++++++++++++++++++++++++-------------- src/VisualizerWidget.h | 55 ++++-- src/main.cpp | 1 + 13 files changed, 517 insertions(+), 179 deletions(-) create mode 100644 shaders/visualizer.frag create mode 100644 shaders/visualizer.vert diff --git a/CMakeLists.txt b/CMakeLists.txt index 961f78a..20f0404 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,7 +36,8 @@ else() message(STATUS "Tempo Estimation (Entropy) Disabled") 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) --- @@ -206,6 +207,28 @@ set(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) add_dependencies(YrCrystals GenerateIcons) endif() @@ -231,7 +254,7 @@ else() endif() 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} ) diff --git a/Makefile b/Makefile index eaa6349..13e9eab 100644 --- a/Makefile +++ b/Makefile @@ -50,7 +50,7 @@ android: @cmake --build $(BUILD_DIR_ANDROID) --target apk @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 @mkdir -p $(BUILD_DIR_IOS) @echo "Configuring iOS CMake..." diff --git a/shaders/visualizer.frag b/shaders/visualizer.frag new file mode 100644 index 0000000..1e21017 --- /dev/null +++ b/shaders/visualizer.frag @@ -0,0 +1,9 @@ +#version 440 + +layout(location = 0) in vec4 v_color; +layout(location = 0) out vec4 fragColor; + +void main() +{ + fragColor = v_color; +} diff --git a/shaders/visualizer.vert b/shaders/visualizer.vert new file mode 100644 index 0000000..eb90d50 --- /dev/null +++ b/shaders/visualizer.vert @@ -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; +} diff --git a/src/AudioEngine.cpp b/src/AudioEngine.cpp index 0223b83..33d4be0 100644 --- a/src/AudioEngine.cpp +++ b/src/AudioEngine.cpp @@ -13,6 +13,12 @@ #include #include +#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) +#ifndef IS_MOBILE +#define IS_MOBILE +#endif +#endif + #ifdef ENABLE_TEMPO_ESTIMATION #include "LoopTempoEstimator/LoopTempoEstimator.h" @@ -273,6 +279,9 @@ void AudioEngine::onFinished() { // Notify UI that track is ready to play 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 // thread FIX: Use QPointer to prevent crash if AudioEngine is deleted // before task runs @@ -370,6 +379,9 @@ void AudioEngine::play() { } } }); +#ifdef IS_MOBILE + m_source.enablePrebuffer(150); +#endif m_sink->start(&m_source); m_playTimer->start(); } @@ -512,8 +524,14 @@ void AudioAnalyzer::processLoop() { // 1. Poll Atomic Position (Non-blocking) double pos = m_posRef->load(); - // 2. Calculate Index - size_t totalSamples = m_data->complexData.size() / 2; + // 2. Calculate Index — use complexData if available, else fallback to pcmData + 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(pos * totalSamples); // Boundary check @@ -522,9 +540,18 @@ void AudioAnalyzer::processLoop() { // 3. Extract Data (Read-only from shared memory) std::vector> ch0(m_frameSize), ch1(m_frameSize); - for (int i = 0; i < m_frameSize; ++i) { - ch0[i] = m_data->complexData[(sampleIdx + i) * 2]; - ch1[i] = m_data->complexData[(sampleIdx + i) * 2 + 1]; + if (useComplex) { + for (int i = 0; i < m_frameSize; ++i) { + ch0[i] = m_data->complexData[(sampleIdx + i) * 2]; + ch1[i] = m_data->complexData[(sampleIdx + i) * 2 + 1]; + } + } else { + const float *raw = + reinterpret_cast(m_data->pcmData.constData()); + for (int i = 0; i < m_frameSize; ++i) { + ch0[i] = std::complex(raw[(sampleIdx + i) * 2], 0.0); + ch1[i] = std::complex(raw[(sampleIdx + i) * 2 + 1], 0.0); + } } // 4. Push to Processors diff --git a/src/AudioEngine.h b/src/AudioEngine.h index c22f262..c44f0c2 100644 --- a/src/AudioEngine.h +++ b/src/AudioEngine.h @@ -36,11 +36,33 @@ public: void setData(const QByteArray &pcmFloat) { m_data = pcmFloat; m_pos = 0; + m_prebufferRemaining = 0; } 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 { + 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()) return 0; @@ -92,6 +114,7 @@ public: void close() override { QIODevice::close(); m_pos = 0; + m_prebufferRemaining = 0; } bool isAtEnd() const { return m_pos >= m_data.size(); } @@ -105,12 +128,21 @@ public: // Custom seek (in float domain bytes) void seekFloatBytes(qint64 pos) { 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(); } private: QByteArray m_data; // Always stored as Float32 qint64 m_pos = 0; + qint64 m_prebufferRemaining = 0; QAudioFormat m_targetFormat; }; diff --git a/src/CommonWidgets.cpp b/src/CommonWidgets.cpp index b65b21d..d35c965 100644 --- a/src/CommonWidgets.cpp +++ b/src/CommonWidgets.cpp @@ -1,12 +1,14 @@ // src/CommonWidgets.cpp #include "CommonWidgets.h" -#include // Added for file info extraction +#include +#include #include -#include // Added for WelcomeWidget +#include #include #include #include +#include #include #include #include @@ -226,27 +228,58 @@ WelcomeWidget::WelcomeWidget(QWidget *parent) : QWidget(parent) { layout->addLayout(btnLayout); - // --- Recents List --- - QLabel *recentLabel = new QLabel("Recent", this); - recentLabel->setStyleSheet("color: #aaa; font-size: 16px; margin-top: 20px;"); - layout->addWidget(recentLabel); - - m_recentList = new QListWidget(this); - m_recentList->setStyleSheet( + // --- Lists Container (Recent + Frequents) --- + QString listStyle = "QListWidget { background: transparent; border: none; color: #ddd; " "font-size: 16px; }" "QListWidget::item { padding: 10px; border-bottom: 1px solid #333; }" "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->setCursor(Qt::PointingHandCursor); m_recentList->setSelectionMode(QAbstractItemView::SingleSelection); connect(m_recentList, &QListWidget::itemClicked, this, &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(); } @@ -255,12 +288,10 @@ void WelcomeWidget::refreshRecents() { QStringList files = Utils::getRecentFiles(); QStringList folders = Utils::getRecentFolders(); - // Interleave or section them? Let's just list folders then files. for (const auto &path : folders) { QListWidgetItem *item = new QListWidgetItem("📁 " + QFileInfo(path).fileName()); item->setData(Qt::UserRole, path); - // Tooltip showing full path item->setToolTip(path); m_recentList->addItem(item); } @@ -271,6 +302,48 @@ void WelcomeWidget::refreshRecents() { item->setToolTip(path); 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 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) { diff --git a/src/CommonWidgets.h b/src/CommonWidgets.h index c7103a8..570df66 100644 --- a/src/CommonWidgets.h +++ b/src/CommonWidgets.h @@ -54,6 +54,7 @@ private: QWidget *m_content; }; +class QBoxLayout; class QListWidget; class QListWidgetItem; @@ -66,9 +67,16 @@ signals: void openFileClicked(); void openFolderClicked(); void pathSelected(const QString &path); +protected: + void resizeEvent(QResizeEvent *event) override; private slots: void onRecentClicked(QListWidgetItem *item); private: + void updateListsLayout(); QListWidget *m_recentList; + QListWidget *m_frequentList; + QWidget *m_listsContainer; + QBoxLayout *m_listsLayout = nullptr; + bool m_isHorizontal = false; }; \ No newline at end of file diff --git a/src/Utils.cpp b/src/Utils.cpp index 98a02b4..364c9e8 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -660,6 +660,7 @@ void addRecentFile(const QString &path) { while (files.size() > 10) files.removeLast(); settings.setValue("recentFiles", files); + incrementOpenCount(path); } void addRecentFolder(const QString &path) { @@ -670,6 +671,27 @@ void addRecentFolder(const QString &path) { while (folders.size() > 10) folders.removeLast(); 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> getFrequentPaths(int limit) { + QSettings settings("YrCrystals", "App"); + QVariantMap freq = settings.value("frequentPaths").toMap(); + QList> 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() { diff --git a/src/Utils.h b/src/Utils.h index 74d17b3..dd1c664 100644 --- a/src/Utils.h +++ b/src/Utils.h @@ -46,6 +46,10 @@ void addRecentFolder(const QString &path); QStringList getRecentFiles(); QStringList getRecentFolders(); +// Frequency Tracking +void incrementOpenCount(const QString &path); +QList> getFrequentPaths(int limit = 10); + #ifdef Q_OS_IOS void openIosPicker(bool folder, std::function callback); #endif diff --git a/src/VisualizerWidget.cpp b/src/VisualizerWidget.cpp index b49da54..70d2dba 100644 --- a/src/VisualizerWidget.cpp +++ b/src/VisualizerWidget.cpp @@ -1,10 +1,7 @@ #include "VisualizerWidget.h" #include #include -#include -#include -#include -#include +#include #include #include #include @@ -13,25 +10,23 @@ #define M_PI 3.14159265358979323846 #endif -VisualizerWidget::VisualizerWidget(QWidget *parent) : QWidget(parent) { - setAttribute(Qt::WA_OpaquePaintEvent); - setNumBins(26); +static QShader loadShader(const QString &name) { + QFile f(name); + if (f.open(QIODevice::ReadOnly)) + return QShader::fromSerialized(f.readAll()); + qWarning() << "Failed to load shader:" << name; + return {}; +} -#if defined(Q_OS_IOS) - // IOS Optimization: Cap internal rendering resolution - // Native retina (3.0) is overkill for this visualizer and kills fill-rate. - // 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 +VisualizerWidget::VisualizerWidget(QWidget *parent) : QRhiWidget(parent) { + setSampleCount(4); + setNumBins(26); } void VisualizerWidget::mouseReleaseEvent(QMouseEvent *event) { - if (event->button() == Qt::LeftButton) { + if (event->button() == Qt::LeftButton) emit tapDetected(); - } - QWidget::mouseReleaseEvent(event); + QRhiWidget::mouseReleaseEvent(event); } void VisualizerWidget::setNumBins(int n) { @@ -59,17 +54,11 @@ void VisualizerWidget::setParams(bool glass, bool focus, bool albumColors, m_hueFactor = hue; m_contrast = contrast; m_brightness = brightness; - - // Clear cache if params change - if (!m_cache.isNull()) - m_cache = QPixmap(); - update(); } void VisualizerWidget::setAlbumPalette(const std::vector &palette) { m_albumPalette.clear(); - // Cast size_t to int int targetLen = static_cast(m_customBins.size()) - 1; if (palette.empty()) return; @@ -96,13 +85,15 @@ QColor VisualizerWidget::applyModifiers(QColor c) { return QColor::fromHsvF(c.hsvHueF(), s, v); } +// ===== Spectrum Processing (unchanged) ===== + void VisualizerWidget::updateData( const std::vector &data) { - if (QApplication::applicationState() != Qt::ApplicationActive || !isVisible()) + if (!isVisible()) return; m_data = data; + m_dataDirty = true; - // --- FPS Limit --- qint64 now = QDateTime::currentMSecsSinceEpoch(); if (now - m_lastFrameTime < (1000 / m_targetFps)) return; @@ -111,7 +102,7 @@ void VisualizerWidget::updateData( if (m_channels.size() != 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()) { size_t midIdx = m_data[0].freqs.size() / 2; float frameMidFreq = @@ -125,10 +116,12 @@ void VisualizerWidget::updateData( float logMin = std::log10(20.0f); float logMax = std::log10(20000.0f); - float frameFreqNorm = (std::log10(std::max(frameMidFreq, 1e-9f)) - logMin) / - (logMax - logMin); + float frameFreqNorm = + (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); frameAmpWeight = std::clamp(frameAmpWeight * 2.0f, 0.5f, 6.0f); @@ -139,7 +132,6 @@ void VisualizerWidget::updateData( if (frameHue < 0) frameHue += 1.0f; - // OPTIMIZATION: Optimized MWA Filter for Hue (Running Sum) float angle = frameHue * 2.0f * M_PI; float cosVal = std::cos(angle); float sinVal = std::sin(angle); @@ -169,24 +161,20 @@ void VisualizerWidget::updateData( for (size_t ch = 0; ch < data.size(); ++ch) { const auto &db = data[ch].db; const auto &primaryDb = data[ch].primaryDb; - const auto &freqs = data[ch].freqs; size_t numBins = db.size(); auto &bins = m_channels[ch].bins; if (bins.size() != numBins) bins.resize(numBins); - // Pre-calculate energy for pattern logic std::vector vertexEnergy(numBins); float globalMax = 0.001f; - // Physics & Energy Calculation for (size_t i = 0; i < numBins; ++i) { auto &bin = bins[i]; float rawVal = db[i]; float primaryVal = (i < primaryDb.size()) ? primaryDb[i] : rawVal; - // Physics float responsiveness = 0.2f; bin.visualDb = (bin.visualDb * (1.0f - responsiveness)) + (rawVal * responsiveness); @@ -195,12 +183,10 @@ void VisualizerWidget::updateData( bin.primaryVisualDb = (bin.primaryVisualDb * (1.0f - patternResp)) + (primaryVal * patternResp); - // Energy for Pattern vertexEnergy[i] = std::clamp((bin.primaryVisualDb + 80.0f) / 80.0f, 0.0f, 1.0f); } - // Auto-Balance Highs vs Lows size_t splitIdx = numBins / 2; float maxLow = 0.01f; float maxHigh = 0.01f; @@ -209,14 +195,12 @@ void VisualizerWidget::updateData( for (size_t j = splitIdx; j < numBins; ++j) maxHigh = std::max(maxHigh, vertexEnergy[j]); - float trebleBoost = maxLow / maxHigh; - trebleBoost = std::clamp(trebleBoost, 1.0f, 40.0f); + float trebleBoost = std::clamp(maxLow / maxHigh, 1.0f, 40.0f); for (size_t j = 0; j < numBins; ++j) { if (j >= splitIdx) { float t = (float)(j - splitIdx) / (numBins - splitIdx); - float boost = 1.0f + (trebleBoost - 1.0f) * t; - vertexEnergy[j] *= boost; + vertexEnergy[j] *= 1.0f + (trebleBoost - 1.0f) * t; } float compressed = std::tanh(vertexEnergy[j]); vertexEnergy[j] = compressed; @@ -226,14 +210,12 @@ void VisualizerWidget::updateData( for (float &v : vertexEnergy) v = std::clamp(v / globalMax, 0.0f, 1.0f); - // --- 3. Calculate Procedural Pattern (Modifiers) --- - // Reset modifiers + // --- 3. Procedural Pattern --- for (auto &b : bins) { b.brightMod = 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) { float curr = vertexEnergy[i]; 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); auto applyPattern = [&](int dist, bool isBrightSide, int direction) { - // Cast size_t i to int for arithmetic int segIdx = (direction == -1) ? (static_cast(i) - dist) : (static_cast(i) + dist - 1); - // Cast bins.size() to int if (segIdx < 0 || segIdx >= static_cast(bins.size())) return; - int cycle = (dist - 1) / 3; int step = (dist - 1) % 3; float decay = std::pow(decayBase, cycle); float intensity = peakIntensity * decay; if (intensity < 0.01f) return; - int type = step; if (isBrightSide) type = (type + 2) % 3; - switch (type) { - case 0: // Ghost + case 0: bins[segIdx].brightMod += 0.8f * intensity; bins[segIdx].alphaMod -= 0.8f * intensity; break; - case 1: // Shadow + case 1: bins[segIdx].brightMod -= 0.8f * intensity; bins[segIdx].alphaMod += 0.2f * intensity; break; - case 2: // Highlight + case 2: bins[segIdx].brightMod += 0.8f * intensity; bins[segIdx].alphaMod += 0.2f * intensity; break; @@ -295,8 +272,8 @@ void VisualizerWidget::updateData( if (m_useAlbumColors && !m_albumPalette.empty()) { int palIdx = static_cast(i); if (m_mirrored) - palIdx = - static_cast(m_albumPalette.size()) - 1 - static_cast(i); + palIdx = static_cast(m_albumPalette.size()) - 1 - + static_cast(i); palIdx = std::clamp(palIdx, 0, static_cast(m_albumPalette.size()) - 1); binColor = m_albumPalette[palIdx]; @@ -313,101 +290,217 @@ void VisualizerWidget::updateData( update(); } -void VisualizerWidget::paintEvent(QPaintEvent *) { - if (QApplication::applicationState() != Qt::ApplicationActive || !isVisible()) +// ===== QRhiWidget GPU Rendering ===== + +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; - QPainter p(this); - 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; + const QSize outputSize = renderTarget()->pixelSize(); int w = width(); int h = height(); - if (m_mirrored) { - // --- Single Quadrant Optimization --- - int hw = w / 2; - int hh = h / 2; - - // Rebuild cache if size changed or cache is invalid - if (m_cache.size() != QSize(hw, hh)) { - 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); + // 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(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) { - // --- Draw Trails REMOVED --- +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 lineVerts; - // --- Draw Bars --- 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 &bins = m_channels[ch].bins; - if (bins.empty()) + if (bins.empty() || freqs.size() < 2) continue; 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 &bNext = bins[i + 1]; - // Calculate Final Color using pre-calculated modifiers + // --- Brightness --- float avgEnergy = std::clamp((b.primaryVisualDb + 80.0f) / 80.0f, 0.0f, 1.0f); float baseBrightness = std::pow(avgEnergy, 0.5f); 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 = std::clamp(baseBrightness * bMult * m_brightness, 0.0f, 1.0f); + // --- Color --- QColor dynamicBinColor = b.cachedColor; float 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; } + // --- Alpha --- float aMod = b.alphaMod; float aMult = (aMod >= 0) ? (1.0f + aMod * 0.5f) : (1.0f + aMod); if (aMult < 0.1f) aMult = 0.1f; - float alpha = 0.4f + (avgEnergy - 0.5f) * m_contrast; alpha = std::clamp(alpha * aMult, 0.0f, 1.0f); fillColor.setAlphaF(alpha); lineColor.setAlphaF(0.9f); + // --- Channel 1 tint --- if (ch == 1 && m_data.size() > 1) { int 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); } + // --- Geometry --- float x1 = getX(freqs[i] * xOffset) * w; float x2 = getX(freqs[i + 1] * xOffset) * w; - float barH1 = std::clamp((b.visualDb + 80.0f) / 80.0f * h, 0.0f, (float)h); float barH2 = std::clamp((bNext.visualDb + 80.0f) / 80.0f * h, 0.0f, (float)h); - - // Always anchor bottom float anchorY = h; float y1 = h - barH1; float y2 = h - barH2; - QPainterPath fillPath; - fillPath.moveTo(x1, anchorY); - fillPath.lineTo(x1, y1); - fillPath.lineTo(x2, y2); - fillPath.lineTo(x2, anchorY); - fillPath.closeSubpath(); - p.fillPath(fillPath, fillColor); + float fr = fillColor.redF(), fg = fillColor.greenF(), + fb = fillColor.blueF(), fa = fillColor.alphaF(); - p.setPen(QPen(lineColor, 1)); - p.drawLine(QPointF(x1, anchorY), QPointF(x1, y1)); - if (i == freqs.size() - 2) { - p.drawLine(QPointF(x2, anchorY), QPointF(x2, y2)); + // Triangle 1 + m_vertices.insert(m_vertices.end(), + {x1, anchorY, fr, fg, fb, fa, x1, y1, fr, fg, fb, fa, + 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}); } } } -} \ No newline at end of file + + m_lineVertexCount = static_cast(lineVerts.size()) / 6; + m_vertices.insert(m_vertices.end(), lineVerts.begin(), lineVerts.end()); +} diff --git a/src/VisualizerWidget.h b/src/VisualizerWidget.h index 751b9fb..369b476 100644 --- a/src/VisualizerWidget.h +++ b/src/VisualizerWidget.h @@ -1,14 +1,16 @@ // src/VisualizerWidget.h #pragma once -#include +#include #include -#include +#include #include -#include // For std::pair -#include +#include +#include +#include +#include #include "AudioEngine.h" -class VisualizerWidget : public QWidget { +class VisualizerWidget : public QRhiWidget { Q_OBJECT public: VisualizerWidget(QWidget* parent = nullptr); @@ -22,18 +24,18 @@ signals: void tapDetected(); protected: - void paintEvent(QPaintEvent* e) override; + void initialize(QRhiCommandBuffer *cb) override; + void render(QRhiCommandBuffer *cb) override; + void releaseResources() override; void mouseReleaseEvent(QMouseEvent* event) override; private: - void drawContent(QPainter& p, int w, int h); + void buildVertices(int w, int h); struct BinState { - float visualDb = -100.0f; // Mixed (Height) - float primaryVisualDb = -100.0f; // Primary (Pattern) - float lastRawDb = -100.0f; // To calculate flux - - // Pre-calculated visual modifiers (Optimization) + float visualDb = -100.0f; + float primaryVisualDb = -100.0f; + float lastRawDb = -100.0f; float brightMod = 0.0f; float alphaMod = 0.0f; QColor cachedColor; @@ -47,26 +49,39 @@ private: std::vector m_channels; std::vector m_albumPalette; std::vector m_customBins; - - // Hue Smoothing History (Cos, Sin) + std::deque> m_hueHistory; float m_hueSumCos = 0.0f; float m_hueSumSin = 0.0f; - - QColor m_unifiedColor = Qt::white; // Calculated in updateData - QPixmap m_cache; // For mirrored mode optimization + + QColor m_unifiedColor = Qt::white; bool m_glass = true; bool m_focus = false; - bool m_useAlbumColors = false; + bool m_useAlbumColors = false; bool m_mirrored = false; float m_hueFactor = 0.9f; float m_contrast = 1.0f; float m_brightness = 1.0f; - + int m_targetFps = 60; qint64 m_lastFrameTime = 0; + bool m_dataDirty = false; + + // RHI resources + QRhi *m_rhi = nullptr; + std::unique_ptr m_fillPipeline; + std::unique_ptr m_linePipeline; + std::unique_ptr m_vbuf; + std::unique_ptr m_ubuf; + std::unique_ptr m_srb; + int m_ubufAlign = 256; + + // Per-frame vertex staging + std::vector m_vertices; + int m_fillVertexCount = 0; + int m_lineVertexCount = 0; float getX(float freq); QColor applyModifiers(QColor c); -}; \ No newline at end of file +}; diff --git a/src/main.cpp b/src/main.cpp index 1b161f6..4751358 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -31,6 +31,7 @@ int main(int argc, char *argv[]) { QApplication::setApplicationVersion("1.0"); qRegisterMetaType>("std::vector"); + qRegisterMetaType>("std::shared_ptr"); QPalette p = app.palette(); p.setColor(QPalette::Window, Qt::black);