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")
|
||||
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}
|
||||
)
|
||||
|
||||
|
|
|
|||
2
Makefile
2
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..."
|
||||
|
|
|
|||
|
|
@ -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 <algorithm>
|
||||
|
||||
#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<size_t>(pos * totalSamples);
|
||||
|
||||
// Boundary check
|
||||
|
|
@ -522,9 +540,18 @@ void AudioAnalyzer::processLoop() {
|
|||
|
||||
// 3. Extract Data (Read-only from shared memory)
|
||||
std::vector<std::complex<double>> 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<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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
// src/CommonWidgets.cpp
|
||||
|
||||
#include "CommonWidgets.h"
|
||||
#include <QFileInfo> // Added for file info extraction
|
||||
#include <QBoxLayout>
|
||||
#include <QFileInfo>
|
||||
#include <QHBoxLayout>
|
||||
#include <QListWidget> // Added for WelcomeWidget
|
||||
#include <QListWidget>
|
||||
#include <QMouseEvent>
|
||||
#include <QPainter>
|
||||
#include <QPushButton>
|
||||
#include <QResizeEvent>
|
||||
#include <QVBoxLayout>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
|
@ -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<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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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<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() {
|
||||
|
|
|
|||
|
|
@ -46,6 +46,10 @@ void addRecentFolder(const QString &path);
|
|||
QStringList getRecentFiles();
|
||||
QStringList getRecentFolders();
|
||||
|
||||
// Frequency Tracking
|
||||
void incrementOpenCount(const QString &path);
|
||||
QList<QPair<QString, int>> getFrequentPaths(int limit = 10);
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
void openIosPicker(bool folder, std::function<void(QString)> callback);
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
#include "VisualizerWidget.h"
|
||||
#include <QApplication>
|
||||
#include <QDateTime>
|
||||
#include <QLinearGradient>
|
||||
#include <QMouseEvent>
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QFile>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <numeric>
|
||||
|
|
@ -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<QColor> &palette) {
|
||||
m_albumPalette.clear();
|
||||
// Cast size_t to int
|
||||
int targetLen = static_cast<int>(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<AudioAnalyzer::FrameData> &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<float> 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<int>(i) - dist)
|
||||
: (static_cast<int>(i) + dist - 1);
|
||||
// Cast bins.size() to int
|
||||
if (segIdx < 0 || segIdx >= static_cast<int>(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<int>(i);
|
||||
if (m_mirrored)
|
||||
palIdx =
|
||||
static_cast<int>(m_albumPalette.size()) - 1 - static_cast<int>(i);
|
||||
palIdx = static_cast<int>(m_albumPalette.size()) - 1 -
|
||||
static_cast<int>(i);
|
||||
palIdx =
|
||||
std::clamp(palIdx, 0, static_cast<int>(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<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) {
|
||||
// --- 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<float> 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});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
#pragma once
|
||||
#include <QWidget>
|
||||
#include <QMatrix4x4>
|
||||
#include <QMouseEvent>
|
||||
#include <vector>
|
||||
#include <QRhiWidget>
|
||||
#include <deque>
|
||||
#include <utility> // For std::pair
|
||||
#include <QPointF>
|
||||
#include <memory>
|
||||
#include <rhi/qrhi.h>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#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;
|
||||
|
|
@ -48,13 +50,11 @@ private:
|
|||
std::vector<QColor> m_albumPalette;
|
||||
std::vector<float> m_customBins;
|
||||
|
||||
// Hue Smoothing History (Cos, Sin)
|
||||
std::deque<std::pair<float, float>> 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;
|
||||
|
|
@ -66,6 +66,21 @@ private:
|
|||
|
||||
int m_targetFps = 60;
|
||||
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);
|
||||
QColor applyModifiers(QColor c);
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ int main(int argc, char *argv[]) {
|
|||
QApplication::setApplicationVersion("1.0");
|
||||
|
||||
qRegisterMetaType<std::vector<AudioAnalyzer::FrameData>>("std::vector<AudioAnalyzer::FrameData>");
|
||||
qRegisterMetaType<std::shared_ptr<TrackData>>("std::shared_ptr<TrackData>");
|
||||
|
||||
QPalette p = app.palette();
|
||||
p.setColor(QPalette::Window, Qt::black);
|
||||
|
|
|
|||
Loading…
Reference in New Issue