here marks the commit that I should release on. but instead im gonna redo the ui first because it's terrible on mobile.

This commit is contained in:
pszsh 2026-02-28 03:23:16 -08:00
parent d674c25a29
commit 2b18e76c8f
10 changed files with 209 additions and 268 deletions

View File

@ -21,21 +21,6 @@ include(FetchContent)
option(BUILD_ANDROID "Build for Android" OFF)
option(BUILD_IOS "Build for iOS" OFF)
# --- Feature Flags ---
# Default to OFF for mobile to save resources, ON for desktop
if(BUILD_ANDROID OR BUILD_IOS)
option(ENABLE_TEMPO_ESTIMATION "Enable Loop Tempo Estimator for BPM detection" OFF)
else()
option(ENABLE_TEMPO_ESTIMATION "Enable Loop Tempo Estimator for BPM detection" ON)
endif()
if(ENABLE_TEMPO_ESTIMATION)
message(STATUS "Tempo Estimation (Entropy) Enabled")
add_compile_definitions(ENABLE_TEMPO_ESTIMATION)
else()
message(STATUS "Tempo Estimation (Entropy) Disabled")
endif()
find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets Multimedia)
find_package(Qt6 QUIET COMPONENTS ShaderTools)
@ -103,13 +88,6 @@ else()
FetchContent_MakeAvailable(fftw3_source)
endif()
# --- Loop Tempo Estimator ---
if(ENABLE_TEMPO_ESTIMATION)
set(BUILD_TESTS OFF CACHE BOOL "Build tests" FORCE)
set(BUILD_VAMP_PLUGIN OFF CACHE BOOL "Build Vamp plugin" FORCE)
add_subdirectory(libraries/loop-tempo-estimator)
endif()
# ==========================================
# --- ICON GENERATION ---
# ==========================================
@ -260,10 +238,6 @@ target_link_libraries(YrCrystals PRIVATE
${FFTW_TARGET}
)
if(ENABLE_TEMPO_ESTIMATION)
target_link_libraries(YrCrystals PRIVATE loop-tempo-estimator)
endif()
if(BUILD_ANDROID)
target_link_libraries(YrCrystals PRIVATE log m)
set_property(TARGET YrCrystals PROPERTY QT_ANDROID_PACKAGE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/android")

View File

@ -6,9 +6,7 @@
#include <QDebug>
#include <QDir>
#include <QMediaDevices>
#include <QPointer>
#include <QStandardPaths>
#include <QThreadPool>
#include <QUrl>
#include <QtEndian>
#include <algorithm>
@ -19,39 +17,6 @@
#endif
#endif
#ifdef ENABLE_TEMPO_ESTIMATION
#include "LoopTempoEstimator/LoopTempoEstimator.h"
// --- Helper: Memory Reader for BPM ---
class MemoryAudioReader : public LTE::LteAudioReader {
public:
MemoryAudioReader(const float *data, long long numFrames, int sampleRate)
: m_data(data), m_numFrames(numFrames), m_sampleRate(sampleRate) {}
double GetSampleRate() const override {
return static_cast<double>(m_sampleRate);
}
long long GetNumSamples() const override { return m_numFrames; }
void ReadFloats(float *buffer, long long where,
size_t numFrames) const override {
for (size_t i = 0; i < numFrames; ++i) {
long long srcIdx = (where + i) * 2;
if (srcIdx + 1 < m_numFrames * 2) {
float l = m_data[srcIdx];
float r = m_data[srcIdx + 1];
buffer[i] = (l + r) * 0.5f;
} else {
buffer[i] = 0.0f;
}
}
}
private:
const float *m_data;
long long m_numFrames;
int m_sampleRate;
};
#endif
// =========================================================
// AudioEngine (Playback) Implementation
// =========================================================
@ -279,40 +244,7 @@ void AudioEngine::onFinished() {
// Notify UI that track is ready to play
emit trackLoaded(true);
// Emit early with pcmData-only so analyzer can show spectrum immediately.
// The Hilbert task builds a NEW TrackData, so no data race.
emit trackDataChanged(m_trackData);
// Run heavy analysis in background thread pool
QPointer<AudioEngine> self = this;
// Capture pcmData via implicit sharing (cheap refcount bump)
QByteArray pcmSnap = newData->pcmData;
int sr = newData->sampleRate;
QThreadPool::globalInstance()->start([self, pcmSnap, sr]() {
if (!self)
return;
const float *rawFloats =
reinterpret_cast<const float *>(pcmSnap.constData());
long long totalFloats = pcmSnap.size() / sizeof(float);
long long totalFrames = totalFloats / 2;
if (totalFrames <= 0)
return;
// 1. BPM Detection
#ifdef ENABLE_TEMPO_ESTIMATION
MemoryAudioReader reader(rawFloats, totalFrames, sr);
auto bpmOpt =
LTE::GetBpm(reader, LTE::FalsePositiveTolerance::Lenient, nullptr);
float bpm = bpmOpt.has_value() ? static_cast<float>(*bpmOpt) : 0.0f;
if (self) {
QMetaObject::invokeMethod(self, "analysisReady", Qt::QueuedConnection,
Q_ARG(float, bpm), Q_ARG(float, 1.0f));
}
#endif
});
}
void AudioEngine::play() {

View File

@ -172,7 +172,6 @@ signals:
void playbackFinished();
void trackLoaded(bool success);
void positionChanged(float position); // Restored signal
void analysisReady(float bpm, float confidence);
void trackDataChanged(std::shared_ptr<TrackData> data);
private slots:

View File

@ -87,12 +87,9 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) {
// Analyzer -> UI
connect(m_analyzer, &AudioAnalyzer::spectrumAvailable, this,
&MainWindow::onSpectrumAvailable);
connect(m_engine, &AudioEngine::analysisReady, this,
&MainWindow::onAnalysisReady);
// Settings -> Analyzer
connect(m_playerPage->settings(), &SettingsWidget::paramsChanged, this,
[this](bool, bool, bool, bool, bool, float, float, float,
[this](bool, bool, bool, bool, float, float, float, float,
int granularity, int detail, float strength) {
QMetaObject::invokeMethod(
m_analyzer, "setSmoothingParams", Qt::QueuedConnection,
@ -198,13 +195,9 @@ void MainWindow::initUi() {
connect(set, &SettingsWidget::dspParamsChanged, this,
&MainWindow::onDspChanged);
connect(set, &SettingsWidget::binsChanged, this, &MainWindow::onBinsChanged);
connect(set, &SettingsWidget::bpmScaleChanged, this,
&MainWindow::updateSmoothing);
connect(set, &SettingsWidget::paramsChanged, this, &MainWindow::saveSettings);
connect(set, &SettingsWidget::fpsChanged, this, [&](int) { saveSettings(); });
connect(set, &SettingsWidget::binsChanged, this, &MainWindow::saveSettings);
connect(set, &SettingsWidget::bpmScaleChanged, this,
&MainWindow::saveSettings);
connect(m_playerPage, &PlayerPage::toggleFullScreen, this,
&MainWindow::onToggleFullScreen);
@ -408,13 +401,16 @@ void MainWindow::loadSettings() {
if (f.open(QIODevice::ReadOnly)) {
QJsonDocument doc = QJsonDocument::fromJson(f.readAll());
QJsonObject root = doc.object();
float entropy = root.contains("entropy") ? root["entropy"].toDouble(0.0) : 0.0;
if (!root["entropyEnabled"].toBool(false))
entropy = -100.0f;
m_playerPage->settings()->setParams(
root["glass"].toBool(true), root["focus"].toBool(false),
root["glass"].toBool(true),
root["albumColors"].toBool(false), root["mirrored"].toBool(false),
root["inverted"].toBool(false), root["bins"].toInt(26),
root["fps"].toInt(60), root["brightness"].toDouble(1.0),
root["granularity"].toInt(33), root["detail"].toInt(50),
root["strength"].toDouble(0.0), root["bpmScaleIndex"].toInt(2));
root["strength"].toDouble(0.0), entropy);
}
}
@ -424,7 +420,6 @@ void MainWindow::saveSettings() {
SettingsWidget *s = m_playerPage->settings();
QJsonObject root;
root["glass"] = s->isGlass();
root["focus"] = s->isFocus();
root["albumColors"] = s->isAlbumColors();
root["mirrored"] = s->isMirrored();
root["inverted"] = s->isInverted();
@ -434,7 +429,8 @@ void MainWindow::saveSettings() {
root["granularity"] = s->getGranularity();
root["detail"] = s->getDetail();
root["strength"] = s->getStrength();
root["bpmScaleIndex"] = s->getBpmScaleIndex();
root["entropyEnabled"] = s->isEntropy();
root["entropy"] = s->getEntropy();
QFile f(QDir(m_settingsDir).filePath(".yrcrystals.json"));
if (f.open(QIODevice::WriteOnly))
f.write(QJsonDocument(root).toJson());
@ -476,25 +472,6 @@ void MainWindow::onTrackLoaded(bool success) {
}
}
void MainWindow::onAnalysisReady(float bpm, float confidence) {
m_lastBpm = bpm;
updateSmoothing();
}
void MainWindow::updateSmoothing() {
if (m_lastBpm <= 0.0f)
return;
float scale = m_playerPage->settings()->getBpmScale();
float effectiveBpm = m_lastBpm * scale;
float normalized = std::clamp((effectiveBpm - 60.0f) / 120.0f, 0.0f, 1.0f);
float targetStrength = 0.8f * (1.0f - normalized);
SettingsWidget *s = m_playerPage->settings();
s->setParams(s->isGlass(), s->isFocus(), s->isAlbumColors(), s->isMirrored(),
s->isInverted(), s->getBins(), s->getFps(), s->getBrightness(),
s->getGranularity(), s->getDetail(), targetStrength,
s->getBpmScaleIndex());
}
void MainWindow::onTrackDoubleClicked(QListWidgetItem *item) {
loadIndex(m_playlist->row(item));
}

View File

@ -29,8 +29,6 @@ private slots:
void onTrackFinished();
void onTrackLoaded(bool success);
void onTrackDoubleClicked(QListWidgetItem* item);
void onAnalysisReady(float bpm, float confidence);
void updateSmoothing();
void play();
void pause();
void nextTrack();
@ -78,8 +76,6 @@ private:
PendingAction m_pendingAction = PendingAction::None;
QString m_settingsDir;
float m_lastBpm = 0.0f;
// FIX: Use QPointer for both loader and thread to prevent use-after-free
QPointer<Utils::MetadataLoader> m_metaLoader;
QPointer<QThread> m_metaThread;

View File

@ -133,7 +133,7 @@ SettingsWidget::SettingsWidget(QWidget *parent) : QWidget(parent) {
// Updated Defaults based on user request
m_checkGlass = createCheck("Glass", true, 0, 0);
m_checkFocus = createCheck("Focus", true, 0, 1);
m_checkEntropy = createCheck("Entropy", false, 0, 1);
m_checkAlbumColors = createCheck("Album Colors", false, 1, 0);
m_checkMirrored = createCheck("Mirrored", true, 1, 1);
m_checkInverted = createCheck("Invert", false, 2, 0);
@ -185,25 +185,32 @@ SettingsWidget::SettingsWidget(QWidget *parent) : QWidget(parent) {
connect(m_sliderStrength, &QSlider::valueChanged, this,
&SettingsWidget::onSmoothingChanged);
// BPM Scale Selector
QHBoxLayout *bpmLayout = new QHBoxLayout();
QLabel *lblBpm = new QLabel("BPM Scale:", this);
lblBpm->setStyleSheet("color: white; font-weight: bold; border: none; "
"background: transparent; min-width: 80px;");
// Entropy slider — shown only when Entropy checkbox is checked
{
m_entropyContainer = new QWidget(this);
m_entropyContainer->setStyleSheet("border: none; background: transparent;");
QHBoxLayout *h = new QHBoxLayout(m_entropyContainer);
h->setContentsMargins(0, 0, 0, 0);
m_lblEntropy = new QLabel("Entropy: 0.0", this);
m_lblEntropy->setStyleSheet("color: white; font-weight: bold; border: none; "
"background: transparent; min-width: 80px;");
m_sliderEntropy = new QSlider(Qt::Horizontal, this);
m_sliderEntropy->setRange(-150, 150); // -1.5 to 1.5, center detent at 0
m_sliderEntropy->setValue(0);
m_sliderEntropy->setStyleSheet(
"QSlider::handle:horizontal { background: #aaa; width: 24px; margin: "
"-10px 0; border-radius: 12px; } QSlider::groove:horizontal { "
"background: #444; height: 4px; }");
h->addWidget(m_lblEntropy);
h->addWidget(m_sliderEntropy);
layout->addWidget(m_entropyContainer);
m_entropyContainer->setVisible(false);
m_comboBpmScale = new QComboBox(this);
m_comboBpmScale->addItems({"1/1", "1/2", "1/4 (Default)", "1/8", "1/16"});
m_comboBpmScale->setCurrentIndex(4); // Default to 1/16
m_comboBpmScale->setStyleSheet(
"QComboBox { background: #444; color: white; border: 1px solid #666; "
"border-radius: 4px; padding: 4px; } QComboBox::drop-down { border: "
"none; }");
connect(m_comboBpmScale, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &SettingsWidget::onBpmScaleChanged);
bpmLayout->addWidget(lblBpm);
bpmLayout->addWidget(m_comboBpmScale);
layout->addLayout(bpmLayout);
connect(m_sliderEntropy, &QSlider::valueChanged, this,
&SettingsWidget::onEntropyChanged);
connect(m_checkEntropy, &QCheckBox::toggled, m_entropyContainer,
&QWidget::setVisible);
}
QHBoxLayout *padsLayout = new QHBoxLayout();
@ -238,13 +245,12 @@ SettingsWidget::SettingsWidget(QWidget *parent) : QWidget(parent) {
layout->addLayout(padsLayout);
}
void SettingsWidget::setParams(bool glass, bool focus, bool albumColors,
void SettingsWidget::setParams(bool glass, bool albumColors,
bool mirrored, bool inverted, int bins, int fps,
float brightness, int granularity, int detail,
float strength, int bpmScaleIndex) {
float strength, float entropy) {
bool oldState = blockSignals(true);
m_checkGlass->setChecked(glass);
m_checkFocus->setChecked(focus);
m_checkAlbumColors->setChecked(albumColors);
m_checkMirrored->setChecked(mirrored);
m_checkInverted->setChecked(inverted);
@ -268,8 +274,11 @@ void SettingsWidget::setParams(bool glass, bool focus, bool albumColors,
m_strength = strength;
m_sliderStrength->setValue(static_cast<int>(strength * 100.0f));
if (bpmScaleIndex >= 0 && bpmScaleIndex < m_comboBpmScale->count()) {
m_comboBpmScale->setCurrentIndex(bpmScaleIndex);
m_checkEntropy->setChecked(entropy > -2.0f);
if (entropy > -2.0f) {
m_entropy = entropy;
m_sliderEntropy->setValue(static_cast<int>(entropy * 100.0f));
m_lblEntropy->setText(QString("Entropy: %1").arg(entropy, 0, 'f', 1));
}
blockSignals(oldState);
@ -279,36 +288,19 @@ void SettingsWidget::setParams(bool glass, bool focus, bool albumColors,
}
void SettingsWidget::emitParams() {
emit paramsChanged(m_checkGlass->isChecked(), m_checkFocus->isChecked(),
float entropy = m_checkEntropy->isChecked() ? m_entropy : -100.0f;
emit paramsChanged(m_checkGlass->isChecked(),
m_checkAlbumColors->isChecked(),
m_checkMirrored->isChecked(),
m_checkInverted->isChecked(), m_hue, m_contrast,
m_brightness, m_granularity, m_detail, m_strength);
m_brightness, entropy,
m_granularity, m_detail, m_strength);
}
float SettingsWidget::getBpmScale() const {
switch (m_comboBpmScale->currentIndex()) {
case 0:
return 0.25f; // 1/1
case 1:
return 0.5f; // 1/2
case 2:
return 1.0f; // 1/4 (Default)
case 3:
return 2.0f; // 1/8
case 4:
return 4.0f; // 1/16
default:
return 1.0f;
}
}
int SettingsWidget::getBpmScaleIndex() const {
return m_comboBpmScale->currentIndex();
}
void SettingsWidget::onBpmScaleChanged(int index) {
emit bpmScaleChanged(getBpmScale());
void SettingsWidget::onEntropyChanged(int val) {
m_entropy = val / 100.0f;
m_lblEntropy->setText(QString("Entropy: %1").arg(m_entropy, 0, 'f', 1));
emitParams();
}
void SettingsWidget::onDspPadChanged(float x, float y) {

View File

@ -4,7 +4,6 @@
#include "CommonWidgets.h"
#include "VisualizerWidget.h"
#include <QCheckBox>
#include <QComboBox>
#include <QLabel>
#include <QPushButton>
#include <QSlider>
@ -42,7 +41,8 @@ public:
SettingsWidget(QWidget *parent = nullptr);
bool isGlass() const { return m_checkGlass->isChecked(); }
bool isFocus() const { return m_checkFocus->isChecked(); }
bool isEntropy() const { return m_checkEntropy->isChecked(); }
float getEntropy() const { return m_entropy; }
bool isAlbumColors() const { return m_checkAlbumColors->isChecked(); }
bool isMirrored() const { return m_checkMirrored->isChecked(); }
bool isInverted() const { return m_checkInverted->isChecked(); }
@ -54,24 +54,19 @@ public:
int getDetail() const { return m_sliderDetail->value(); }
float getStrength() const { return m_strength; }
// Returns the multiplier (e.g., 1.0 for 1/4, 0.5 for 1/2)
float getBpmScale() const;
int getBpmScaleIndex() const;
void setParams(bool glass, bool focus, bool albumColors, bool mirrored,
void setParams(bool glass, bool albumColors, bool mirrored,
bool inverted, int bins, int fps, float brightness,
int granularity, int detail, float strength,
int bpmScaleIndex);
float entropy);
signals:
void paramsChanged(bool glass, bool focus, bool albumColors, bool mirrored,
void paramsChanged(bool glass, bool albumColors, bool mirrored,
bool inverted, float hue, float contrast,
float brightness, int granularity, int detail,
float strength);
float brightness, float entropy,
int granularity, int detail, float strength);
void fpsChanged(int fps);
void dspParamsChanged(int fft, int hop);
void binsChanged(int n);
void bpmScaleChanged(float scale);
void closeClicked();
private slots:
@ -81,11 +76,11 @@ private slots:
void onBinsChanged(int val);
void onBrightnessChanged(int val);
void onSmoothingChanged(int val);
void onBpmScaleChanged(int index);
void onEntropyChanged(int val);
private:
QCheckBox *m_checkGlass;
QCheckBox *m_checkFocus;
QCheckBox *m_checkEntropy;
QCheckBox *m_checkAlbumColors;
QCheckBox *m_checkMirrored;
QCheckBox *m_checkInverted;
@ -105,7 +100,10 @@ private:
QSlider *m_sliderStrength;
QLabel *m_lblStrength;
QComboBox *m_comboBpmScale;
QSlider *m_sliderEntropy;
QLabel *m_lblEntropy;
QWidget *m_entropyContainer;
float m_entropy = 0.0f;
float m_hue = 0.9f;
float m_contrast = 1.0f;

View File

@ -44,17 +44,18 @@ void VisualizerWidget::setTargetFps(int fps) {
m_targetFps = std::max(15, std::min(120, fps));
}
void VisualizerWidget::setParams(bool glass, bool focus, bool albumColors,
void VisualizerWidget::setParams(bool glass, bool albumColors,
bool mirrored, bool inverted, float hue,
float contrast, float brightness) {
float contrast, float brightness,
float entropy) {
m_glass = glass;
m_focus = focus;
m_useAlbumColors = albumColors;
m_mirrored = mirrored;
m_inverted = inverted;
m_hueFactor = hue;
m_contrast = contrast;
m_brightness = brightness;
m_entropyStrength = entropy;
update();
}
@ -86,7 +87,47 @@ QColor VisualizerWidget::applyModifiers(QColor c) {
return QColor::fromHsvF(c.hsvHueF(), s, v);
}
// ===== Spectrum Processing (unchanged) =====
float VisualizerWidget::calculateEntropy(const std::deque<float> &history) {
if (history.size() < 4)
return 0.0f;
int N = static_cast<int>(history.size());
// Forward DFT (O(N²) for N≈30 is ~900 ops — trivial, no FFTW needed)
std::vector<std::complex<double>> X(N);
for (int k = 0; k < N; ++k) {
double re = 0, im = 0;
for (int n = 0; n < N; ++n) {
double angle = -2.0 * M_PI * k * n / N;
re += history[n] * std::cos(angle);
im += history[n] * std::sin(angle);
}
X[k] = {re, im};
}
// Analytic signal: zero negative freqs, double positive freqs
for (int k = N / 2 + 1; k < N; ++k)
X[k] = 0;
for (int k = 1; k < (N + 1) / 2; ++k)
X[k] *= 2.0;
// Inverse DFT — imaginary part only (zero-centered, phase-aligned)
float sqSum = 0.0f;
for (int n = 0; n < N; ++n) {
double im = 0;
for (int k = 0; k < N; ++k) {
double angle = 2.0 * M_PI * k * n / N;
im += X[k].real() * std::sin(angle) + X[k].imag() * std::cos(angle);
}
im /= N;
sqSum += static_cast<float>(im * im);
}
// RMS of zero-centered signal, normalized: 10dB RMS = max chaos
return std::clamp(std::sqrt(sqSum / N) / 10.0f, 0.0f, 1.0f);
}
// ===== Spectrum Processing =====
void VisualizerWidget::updateData(
const std::vector<AudioAnalyzer::FrameData> &data) {
@ -171,14 +212,52 @@ void VisualizerWidget::updateData(
std::vector<float> vertexEnergy(numBins);
float globalMax = 0.001f;
bool useEntropy = m_entropyStrength > -2.0f;
// Pass 1: compute per-bin entropy from output history (self-referencing)
std::vector<float> binEntropy(numBins, 0.0f);
if (useEntropy) {
for (size_t i = 0; i < numBins; ++i)
binEntropy[i] = calculateEntropy(bins[i].history);
}
// Find the midpoint — median entropy across all bins
float medianEntropy = 0.0f;
if (useEntropy) {
std::vector<float> sorted = binEntropy;
std::nth_element(sorted.begin(), sorted.begin() + numBins / 2,
sorted.end());
medianEntropy = sorted[numBins / 2];
}
// Pass 2: apply entropy-relative multiplier and update visual state
for (size_t i = 0; i < numBins; ++i) {
auto &bin = bins[i];
float rawVal = db[i];
float primaryVal = (i < primaryDb.size()) ? primaryDb[i] : rawVal;
float responsiveness = 0.2f;
bin.visualDb =
(bin.visualDb * (1.0f - responsiveness)) + (rawVal * responsiveness);
float change = rawVal - bin.visualDb;
if (useEntropy) {
// Position relative to midpoint: >0 = more stable, <0 = more chaotic
float relative = medianEntropy - binEntropy[i];
// Slider controls ratio of reward (stable bins) to penalty (chaotic bins)
// At 0: equal gains → balanced/neutral. +1.5: all reward. -1.5: all penalty.
float base = 1.5f;
float rewardGain = base + m_entropyStrength; // 0..3
float penaltyGain = base - m_entropyStrength; // 3..0
float gain = (relative >= 0.0f) ? rewardGain : penaltyGain;
float multiplier = 1.0f + relative * gain * 2.0f;
multiplier = std::clamp(multiplier, 0.05f, 4.0f);
bin.visualDb += change * multiplier;
// Feed output back into history — the compressor sees its own work
bin.history.push_back(bin.visualDb);
if (bin.history.size() > 30)
bin.history.pop_front();
} else {
float responsiveness = 0.2f;
bin.visualDb =
(bin.visualDb * (1.0f - responsiveness)) + (rawVal * responsiveness);
}
float patternResp = 0.1f;
bin.primaryVisualDb = (bin.primaryVisualDb * (1.0f - patternResp)) +
@ -225,8 +304,9 @@ void VisualizerWidget::updateData(
if (curr > prev && curr > next) {
bool leftDominant = (prev > next);
float sharpness = std::min(curr - prev, curr - next);
float entropyFactor = useEntropy ? std::max(0.1f, std::abs(m_entropyStrength)) : 1.0f;
float peakIntensity =
std::clamp(std::pow(sharpness * 10.0f, 0.3f), 0.0f, 1.0f);
std::clamp(std::pow(sharpness * 10.0f * entropyFactor, 0.3f), 0.0f, 1.0f);
float decayBase = 0.65f - std::clamp(sharpness * 3.0f, 0.0f, 0.35f);
auto applyPattern = [&](int dist, bool isBrightSide, int direction) {
@ -318,7 +398,7 @@ void VisualizerWidget::initialize(QRhiCommandBuffer *cb) {
2048 * 6 * sizeof(float)));
m_vbuf->create();
// Uniform buffer: 5 aligned MVP matrices (4 mirror passes + 1 cepstrum)
// Uniform buffer: single MVP matrix (mirroring baked into vertices)
m_ubuf.reset(m_rhi->newBuffer(QRhiBuffer::Dynamic,
QRhiBuffer::UniformBuffer,
m_ubufAlign * 5));
@ -392,6 +472,34 @@ void VisualizerWidget::render(QRhiCommandBuffer *cb) {
m_lastBuildH = h;
if (m_mirrored) {
buildVertices(w * 0.55f, h / 2);
// Mirror into 4 quadrants directly (avoids multi-pass dynamic UBO
// issues on Android GPU drivers)
{
int fillFloats = m_fillVertexCount * 6;
std::vector<float> baseFill(m_vertices.begin(),
m_vertices.begin() + fillFloats);
std::vector<float> baseLine(m_vertices.begin() + fillFloats,
m_vertices.end());
m_vertices.clear();
auto mir = [](const std::vector<float> &src, std::vector<float> &dst,
float sx, float sy, float tx, float ty) {
for (size_t j = 0; j < src.size(); j += 6) {
dst.push_back(src[j] * sx + tx);
dst.push_back(src[j + 1] * sy + ty);
dst.insert(dst.end(), src.begin() + j + 2, src.begin() + j + 6);
}
};
mir(baseFill, m_vertices, 1, 1, 0, 0);
mir(baseFill, m_vertices, -1, 1, (float)w, 0);
mir(baseFill, m_vertices, 1, -1, 0, (float)h);
mir(baseFill, m_vertices, -1, -1, (float)w, (float)h);
m_fillVertexCount *= 4;
mir(baseLine, m_vertices, 1, 1, 0, 0);
mir(baseLine, m_vertices, -1, 1, (float)w, 0);
mir(baseLine, m_vertices, 1, -1, 0, (float)h);
mir(baseLine, m_vertices, -1, -1, (float)w, (float)h);
m_lineVertexCount *= 4;
}
buildCepstrumVertices(w, h);
} else {
buildVertices(w, h);
@ -399,8 +507,6 @@ void VisualizerWidget::render(QRhiCommandBuffer *cb) {
}
}
int numPasses = m_mirrored ? 4 : 1;
// Prepare resource updates
QRhiResourceUpdateBatch *u = m_rhi->nextResourceUpdateBatch();
@ -421,44 +527,12 @@ void VisualizerWidget::render(QRhiCommandBuffer *cb) {
}
}
// Upload MVP matrices
// Single full-screen ortho MVP — mirroring is baked into vertex positions
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());
}
// Upload full-screen ortho MVP for cepstrum (slot 4)
if (m_mirrored && m_cepstrumVertexCount > 0) {
QMatrix4x4 cepProj;
cepProj.ortho(0, (float)w, (float)h, 0, -1, 1);
QMatrix4x4 cepMvp = correction * cepProj;
u->updateDynamicBuffer(m_ubuf.get(), 4 * m_ubufAlign, 64,
cepMvp.constData());
}
QMatrix4x4 proj;
proj.ortho(0, (float)w, (float)h, 0, -1, 1);
QMatrix4x4 mvp = correction * proj;
u->updateDynamicBuffer(m_ubuf.get(), 0, 64, mvp.constData());
// Begin render pass
cb->beginPass(renderTarget(), QColor(0, 0, 0, 255), {1.0f, 0}, u);
@ -466,30 +540,25 @@ void VisualizerWidget::render(QRhiCommandBuffer *cb) {
(float)outputSize.height()});
const QRhiCommandBuffer::VertexInput vbufBinding(m_vbuf.get(), 0);
QRhiCommandBuffer::DynamicOffset dynOfs(0, 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);
}
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);
}
// --- Cepstral Thread (single full-screen pass, after mirror loop) ---
if (m_mirrored && m_cepstrumVertexCount > 0) {
QRhiCommandBuffer::DynamicOffset cepOfs(0, quint32(4 * m_ubufAlign));
if (m_lineVertexCount > 0) {
cb->setGraphicsPipeline(m_linePipeline.get());
cb->setShaderResources(m_srb.get(), 1, &cepOfs);
cb->setShaderResources(m_srb.get(), 1, &dynOfs);
cb->setVertexInput(0, 1, &vbufBinding);
cb->draw(m_lineVertexCount, 1, m_fillVertexCount, 0);
}
if (m_cepstrumVertexCount > 0) {
cb->setGraphicsPipeline(m_linePipeline.get());
cb->setShaderResources(m_srb.get(), 1, &dynOfs);
cb->setVertexInput(0, 1, &vbufBinding);
cb->draw(m_cepstrumVertexCount, 1, m_fillVertexCount + m_lineVertexCount, 0);
}

View File

@ -15,7 +15,7 @@ class VisualizerWidget : public QRhiWidget {
public:
VisualizerWidget(QWidget* parent = nullptr);
void updateData(const std::vector<AudioAnalyzer::FrameData>& data);
void setParams(bool glass, bool focus, bool albumColors, bool mirrored, bool inverted, float hue, float contrast, float brightness);
void setParams(bool glass, bool albumColors, bool mirrored, bool inverted, float hue, float contrast, float brightness, float entropy);
void setAlbumPalette(const std::vector<QColor>& palette);
void setNumBins(int n);
void setTargetFps(int fps);
@ -39,6 +39,7 @@ private:
float brightMod = 0.0f;
float alphaMod = 0.0f;
QColor cachedColor;
std::deque<float> history;
};
struct ChannelState {
@ -57,7 +58,6 @@ private:
QColor m_unifiedColor = Qt::white;
bool m_glass = true;
bool m_focus = false;
bool m_useAlbumColors = false;
bool m_mirrored = false;
bool m_inverted = false;
@ -91,6 +91,9 @@ private:
int m_cepstrumVertexCount = 0;
void buildCepstrumVertices(int w, int h);
float m_entropyStrength = -100.0f; // <-2 = disabled, -1.5..1.5 = enabled
float getX(float freq);
float calculateEntropy(const std::deque<float>& history);
QColor applyModifiers(QColor c);
};

View File

@ -103,12 +103,13 @@ void RealtimeHilbert::reinit(size_t fft_size) {
}
// Create plans for Left Channel
m_plan_forward_L = fftw_plan_dft_r2c_1d(m_fft_size, m_fft_input_real_L, m_fft_output_spectrum_L, FFTW_ESTIMATE);
m_plan_backward_L = fftw_plan_dft_1d(m_fft_size, m_ifft_input_spectrum_L, m_ifft_output_complex_L, FFTW_BACKWARD, FFTW_ESTIMATE);
int n = static_cast<int>(m_fft_size);
m_plan_forward_L = fftw_plan_dft_r2c_1d(n, m_fft_input_real_L, m_fft_output_spectrum_L, FFTW_ESTIMATE);
m_plan_backward_L = fftw_plan_dft_1d(n, m_ifft_input_spectrum_L, m_ifft_output_complex_L, FFTW_BACKWARD, FFTW_ESTIMATE);
// Create plans for Right Channel
m_plan_forward_R = fftw_plan_dft_r2c_1d(m_fft_size, m_fft_input_real_R, m_fft_output_spectrum_R, FFTW_ESTIMATE);
m_plan_backward_R = fftw_plan_dft_1d(m_fft_size, m_ifft_input_spectrum_R, m_ifft_output_complex_R, FFTW_BACKWARD, FFTW_ESTIMATE);
m_plan_forward_R = fftw_plan_dft_r2c_1d(n, m_fft_input_real_R, m_fft_output_spectrum_R, FFTW_ESTIMATE);
m_plan_backward_R = fftw_plan_dft_1d(n, m_ifft_input_spectrum_R, m_ifft_output_complex_R, FFTW_BACKWARD, FFTW_ESTIMATE);
if (!m_plan_forward_L || !m_plan_backward_L || !m_plan_forward_R || !m_plan_backward_R) {
cleanup();