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_ANDROID "Build for Android" OFF)
option(BUILD_IOS "Build for iOS" 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 REQUIRED COMPONENTS Core Gui Widgets Multimedia)
find_package(Qt6 QUIET COMPONENTS ShaderTools) find_package(Qt6 QUIET COMPONENTS ShaderTools)
@ -103,13 +88,6 @@ else()
FetchContent_MakeAvailable(fftw3_source) FetchContent_MakeAvailable(fftw3_source)
endif() 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 --- # --- ICON GENERATION ---
# ========================================== # ==========================================
@ -260,10 +238,6 @@ target_link_libraries(YrCrystals PRIVATE
${FFTW_TARGET} ${FFTW_TARGET}
) )
if(ENABLE_TEMPO_ESTIMATION)
target_link_libraries(YrCrystals PRIVATE loop-tempo-estimator)
endif()
if(BUILD_ANDROID) if(BUILD_ANDROID)
target_link_libraries(YrCrystals PRIVATE log m) target_link_libraries(YrCrystals PRIVATE log m)
set_property(TARGET YrCrystals PROPERTY QT_ANDROID_PACKAGE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/android") set_property(TARGET YrCrystals PROPERTY QT_ANDROID_PACKAGE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/android")

View File

@ -6,9 +6,7 @@
#include <QDebug> #include <QDebug>
#include <QDir> #include <QDir>
#include <QMediaDevices> #include <QMediaDevices>
#include <QPointer>
#include <QStandardPaths> #include <QStandardPaths>
#include <QThreadPool>
#include <QUrl> #include <QUrl>
#include <QtEndian> #include <QtEndian>
#include <algorithm> #include <algorithm>
@ -19,39 +17,6 @@
#endif #endif
#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 // AudioEngine (Playback) Implementation
// ========================================================= // =========================================================
@ -279,40 +244,7 @@ void AudioEngine::onFinished() {
// Notify UI that track is ready to play // Notify UI that track is ready to play
emit trackLoaded(true); emit trackLoaded(true);
// Emit 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); 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() { void AudioEngine::play() {

View File

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

View File

@ -87,12 +87,9 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) {
// Analyzer -> UI // Analyzer -> UI
connect(m_analyzer, &AudioAnalyzer::spectrumAvailable, this, connect(m_analyzer, &AudioAnalyzer::spectrumAvailable, this,
&MainWindow::onSpectrumAvailable); &MainWindow::onSpectrumAvailable);
connect(m_engine, &AudioEngine::analysisReady, this,
&MainWindow::onAnalysisReady);
// Settings -> Analyzer // Settings -> Analyzer
connect(m_playerPage->settings(), &SettingsWidget::paramsChanged, this, 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) { int granularity, int detail, float strength) {
QMetaObject::invokeMethod( QMetaObject::invokeMethod(
m_analyzer, "setSmoothingParams", Qt::QueuedConnection, m_analyzer, "setSmoothingParams", Qt::QueuedConnection,
@ -198,13 +195,9 @@ void MainWindow::initUi() {
connect(set, &SettingsWidget::dspParamsChanged, this, connect(set, &SettingsWidget::dspParamsChanged, this,
&MainWindow::onDspChanged); &MainWindow::onDspChanged);
connect(set, &SettingsWidget::binsChanged, this, &MainWindow::onBinsChanged); connect(set, &SettingsWidget::binsChanged, this, &MainWindow::onBinsChanged);
connect(set, &SettingsWidget::bpmScaleChanged, this,
&MainWindow::updateSmoothing);
connect(set, &SettingsWidget::paramsChanged, this, &MainWindow::saveSettings); connect(set, &SettingsWidget::paramsChanged, this, &MainWindow::saveSettings);
connect(set, &SettingsWidget::fpsChanged, this, [&](int) { saveSettings(); }); connect(set, &SettingsWidget::fpsChanged, this, [&](int) { saveSettings(); });
connect(set, &SettingsWidget::binsChanged, this, &MainWindow::saveSettings); connect(set, &SettingsWidget::binsChanged, this, &MainWindow::saveSettings);
connect(set, &SettingsWidget::bpmScaleChanged, this,
&MainWindow::saveSettings);
connect(m_playerPage, &PlayerPage::toggleFullScreen, this, connect(m_playerPage, &PlayerPage::toggleFullScreen, this,
&MainWindow::onToggleFullScreen); &MainWindow::onToggleFullScreen);
@ -408,13 +401,16 @@ void MainWindow::loadSettings() {
if (f.open(QIODevice::ReadOnly)) { if (f.open(QIODevice::ReadOnly)) {
QJsonDocument doc = QJsonDocument::fromJson(f.readAll()); QJsonDocument doc = QJsonDocument::fromJson(f.readAll());
QJsonObject root = doc.object(); 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( 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["albumColors"].toBool(false), root["mirrored"].toBool(false),
root["inverted"].toBool(false), root["bins"].toInt(26), root["inverted"].toBool(false), root["bins"].toInt(26),
root["fps"].toInt(60), root["brightness"].toDouble(1.0), root["fps"].toInt(60), root["brightness"].toDouble(1.0),
root["granularity"].toInt(33), root["detail"].toInt(50), 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(); SettingsWidget *s = m_playerPage->settings();
QJsonObject root; QJsonObject root;
root["glass"] = s->isGlass(); root["glass"] = s->isGlass();
root["focus"] = s->isFocus();
root["albumColors"] = s->isAlbumColors(); root["albumColors"] = s->isAlbumColors();
root["mirrored"] = s->isMirrored(); root["mirrored"] = s->isMirrored();
root["inverted"] = s->isInverted(); root["inverted"] = s->isInverted();
@ -434,7 +429,8 @@ void MainWindow::saveSettings() {
root["granularity"] = s->getGranularity(); root["granularity"] = s->getGranularity();
root["detail"] = s->getDetail(); root["detail"] = s->getDetail();
root["strength"] = s->getStrength(); root["strength"] = s->getStrength();
root["bpmScaleIndex"] = s->getBpmScaleIndex(); root["entropyEnabled"] = s->isEntropy();
root["entropy"] = s->getEntropy();
QFile f(QDir(m_settingsDir).filePath(".yrcrystals.json")); QFile f(QDir(m_settingsDir).filePath(".yrcrystals.json"));
if (f.open(QIODevice::WriteOnly)) if (f.open(QIODevice::WriteOnly))
f.write(QJsonDocument(root).toJson()); 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) { void MainWindow::onTrackDoubleClicked(QListWidgetItem *item) {
loadIndex(m_playlist->row(item)); loadIndex(m_playlist->row(item));
} }

View File

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

View File

@ -133,7 +133,7 @@ SettingsWidget::SettingsWidget(QWidget *parent) : QWidget(parent) {
// Updated Defaults based on user request // Updated Defaults based on user request
m_checkGlass = createCheck("Glass", true, 0, 0); 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_checkAlbumColors = createCheck("Album Colors", false, 1, 0);
m_checkMirrored = createCheck("Mirrored", true, 1, 1); m_checkMirrored = createCheck("Mirrored", true, 1, 1);
m_checkInverted = createCheck("Invert", false, 2, 0); m_checkInverted = createCheck("Invert", false, 2, 0);
@ -185,25 +185,32 @@ SettingsWidget::SettingsWidget(QWidget *parent) : QWidget(parent) {
connect(m_sliderStrength, &QSlider::valueChanged, this, connect(m_sliderStrength, &QSlider::valueChanged, this,
&SettingsWidget::onSmoothingChanged); &SettingsWidget::onSmoothingChanged);
// BPM Scale Selector // Entropy slider — shown only when Entropy checkbox is checked
QHBoxLayout *bpmLayout = new QHBoxLayout(); {
QLabel *lblBpm = new QLabel("BPM Scale:", this); m_entropyContainer = new QWidget(this);
lblBpm->setStyleSheet("color: white; font-weight: bold; border: none; " m_entropyContainer->setStyleSheet("border: none; background: transparent;");
"background: transparent; min-width: 80px;"); 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); connect(m_sliderEntropy, &QSlider::valueChanged, this,
m_comboBpmScale->addItems({"1/1", "1/2", "1/4 (Default)", "1/8", "1/16"}); &SettingsWidget::onEntropyChanged);
m_comboBpmScale->setCurrentIndex(4); // Default to 1/16 connect(m_checkEntropy, &QCheckBox::toggled, m_entropyContainer,
m_comboBpmScale->setStyleSheet( &QWidget::setVisible);
"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);
QHBoxLayout *padsLayout = new QHBoxLayout(); QHBoxLayout *padsLayout = new QHBoxLayout();
@ -238,13 +245,12 @@ SettingsWidget::SettingsWidget(QWidget *parent) : QWidget(parent) {
layout->addLayout(padsLayout); 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, bool mirrored, bool inverted, int bins, int fps,
float brightness, int granularity, int detail, float brightness, int granularity, int detail,
float strength, int bpmScaleIndex) { float strength, float entropy) {
bool oldState = blockSignals(true); bool oldState = blockSignals(true);
m_checkGlass->setChecked(glass); m_checkGlass->setChecked(glass);
m_checkFocus->setChecked(focus);
m_checkAlbumColors->setChecked(albumColors); m_checkAlbumColors->setChecked(albumColors);
m_checkMirrored->setChecked(mirrored); m_checkMirrored->setChecked(mirrored);
m_checkInverted->setChecked(inverted); m_checkInverted->setChecked(inverted);
@ -268,8 +274,11 @@ void SettingsWidget::setParams(bool glass, bool focus, bool albumColors,
m_strength = strength; m_strength = strength;
m_sliderStrength->setValue(static_cast<int>(strength * 100.0f)); m_sliderStrength->setValue(static_cast<int>(strength * 100.0f));
if (bpmScaleIndex >= 0 && bpmScaleIndex < m_comboBpmScale->count()) { m_checkEntropy->setChecked(entropy > -2.0f);
m_comboBpmScale->setCurrentIndex(bpmScaleIndex); 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); blockSignals(oldState);
@ -279,36 +288,19 @@ void SettingsWidget::setParams(bool glass, bool focus, bool albumColors,
} }
void SettingsWidget::emitParams() { 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_checkAlbumColors->isChecked(),
m_checkMirrored->isChecked(), m_checkMirrored->isChecked(),
m_checkInverted->isChecked(), m_hue, m_contrast, 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 { void SettingsWidget::onEntropyChanged(int val) {
switch (m_comboBpmScale->currentIndex()) { m_entropy = val / 100.0f;
case 0: m_lblEntropy->setText(QString("Entropy: %1").arg(m_entropy, 0, 'f', 1));
return 0.25f; // 1/1 emitParams();
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::onDspPadChanged(float x, float y) { void SettingsWidget::onDspPadChanged(float x, float y) {

View File

@ -4,7 +4,6 @@
#include "CommonWidgets.h" #include "CommonWidgets.h"
#include "VisualizerWidget.h" #include "VisualizerWidget.h"
#include <QCheckBox> #include <QCheckBox>
#include <QComboBox>
#include <QLabel> #include <QLabel>
#include <QPushButton> #include <QPushButton>
#include <QSlider> #include <QSlider>
@ -42,7 +41,8 @@ public:
SettingsWidget(QWidget *parent = nullptr); SettingsWidget(QWidget *parent = nullptr);
bool isGlass() const { return m_checkGlass->isChecked(); } 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 isAlbumColors() const { return m_checkAlbumColors->isChecked(); }
bool isMirrored() const { return m_checkMirrored->isChecked(); } bool isMirrored() const { return m_checkMirrored->isChecked(); }
bool isInverted() const { return m_checkInverted->isChecked(); } bool isInverted() const { return m_checkInverted->isChecked(); }
@ -54,24 +54,19 @@ public:
int getDetail() const { return m_sliderDetail->value(); } int getDetail() const { return m_sliderDetail->value(); }
float getStrength() const { return m_strength; } float getStrength() const { return m_strength; }
// Returns the multiplier (e.g., 1.0 for 1/4, 0.5 for 1/2) void setParams(bool glass, bool albumColors, bool mirrored,
float getBpmScale() const;
int getBpmScaleIndex() const;
void setParams(bool glass, bool focus, bool albumColors, bool mirrored,
bool inverted, int bins, int fps, float brightness, bool inverted, int bins, int fps, float brightness,
int granularity, int detail, float strength, int granularity, int detail, float strength,
int bpmScaleIndex); float entropy);
signals: 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, bool inverted, float hue, float contrast,
float brightness, int granularity, int detail, float brightness, float entropy,
float strength); int granularity, int detail, float strength);
void fpsChanged(int fps); void fpsChanged(int fps);
void dspParamsChanged(int fft, int hop); void dspParamsChanged(int fft, int hop);
void binsChanged(int n); void binsChanged(int n);
void bpmScaleChanged(float scale);
void closeClicked(); void closeClicked();
private slots: private slots:
@ -81,11 +76,11 @@ private slots:
void onBinsChanged(int val); void onBinsChanged(int val);
void onBrightnessChanged(int val); void onBrightnessChanged(int val);
void onSmoothingChanged(int val); void onSmoothingChanged(int val);
void onBpmScaleChanged(int index); void onEntropyChanged(int val);
private: private:
QCheckBox *m_checkGlass; QCheckBox *m_checkGlass;
QCheckBox *m_checkFocus; QCheckBox *m_checkEntropy;
QCheckBox *m_checkAlbumColors; QCheckBox *m_checkAlbumColors;
QCheckBox *m_checkMirrored; QCheckBox *m_checkMirrored;
QCheckBox *m_checkInverted; QCheckBox *m_checkInverted;
@ -105,7 +100,10 @@ private:
QSlider *m_sliderStrength; QSlider *m_sliderStrength;
QLabel *m_lblStrength; 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_hue = 0.9f;
float m_contrast = 1.0f; 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)); 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, bool mirrored, bool inverted, float hue,
float contrast, float brightness) { float contrast, float brightness,
float entropy) {
m_glass = glass; m_glass = glass;
m_focus = focus;
m_useAlbumColors = albumColors; m_useAlbumColors = albumColors;
m_mirrored = mirrored; m_mirrored = mirrored;
m_inverted = inverted; m_inverted = inverted;
m_hueFactor = hue; m_hueFactor = hue;
m_contrast = contrast; m_contrast = contrast;
m_brightness = brightness; m_brightness = brightness;
m_entropyStrength = entropy;
update(); update();
} }
@ -86,7 +87,47 @@ QColor VisualizerWidget::applyModifiers(QColor c) {
return QColor::fromHsvF(c.hsvHueF(), s, v); 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( void VisualizerWidget::updateData(
const std::vector<AudioAnalyzer::FrameData> &data) { const std::vector<AudioAnalyzer::FrameData> &data) {
@ -171,14 +212,52 @@ void VisualizerWidget::updateData(
std::vector<float> vertexEnergy(numBins); std::vector<float> vertexEnergy(numBins);
float globalMax = 0.001f; 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) { for (size_t i = 0; i < numBins; ++i) {
auto &bin = bins[i]; auto &bin = bins[i];
float rawVal = db[i]; float rawVal = db[i];
float primaryVal = (i < primaryDb.size()) ? primaryDb[i] : rawVal; float primaryVal = (i < primaryDb.size()) ? primaryDb[i] : rawVal;
float responsiveness = 0.2f; float change = rawVal - bin.visualDb;
bin.visualDb = if (useEntropy) {
(bin.visualDb * (1.0f - responsiveness)) + (rawVal * responsiveness); // 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; float patternResp = 0.1f;
bin.primaryVisualDb = (bin.primaryVisualDb * (1.0f - patternResp)) + bin.primaryVisualDb = (bin.primaryVisualDb * (1.0f - patternResp)) +
@ -225,8 +304,9 @@ void VisualizerWidget::updateData(
if (curr > prev && curr > next) { if (curr > prev && curr > next) {
bool leftDominant = (prev > next); bool leftDominant = (prev > next);
float sharpness = std::min(curr - prev, curr - next); float sharpness = std::min(curr - prev, curr - next);
float entropyFactor = useEntropy ? std::max(0.1f, std::abs(m_entropyStrength)) : 1.0f;
float peakIntensity = 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); float decayBase = 0.65f - std::clamp(sharpness * 3.0f, 0.0f, 0.35f);
auto applyPattern = [&](int dist, bool isBrightSide, int direction) { auto applyPattern = [&](int dist, bool isBrightSide, int direction) {
@ -318,7 +398,7 @@ void VisualizerWidget::initialize(QRhiCommandBuffer *cb) {
2048 * 6 * sizeof(float))); 2048 * 6 * sizeof(float)));
m_vbuf->create(); 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, m_ubuf.reset(m_rhi->newBuffer(QRhiBuffer::Dynamic,
QRhiBuffer::UniformBuffer, QRhiBuffer::UniformBuffer,
m_ubufAlign * 5)); m_ubufAlign * 5));
@ -392,6 +472,34 @@ void VisualizerWidget::render(QRhiCommandBuffer *cb) {
m_lastBuildH = h; m_lastBuildH = h;
if (m_mirrored) { if (m_mirrored) {
buildVertices(w * 0.55f, h / 2); 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); buildCepstrumVertices(w, h);
} else { } else {
buildVertices(w, h); buildVertices(w, h);
@ -399,8 +507,6 @@ void VisualizerWidget::render(QRhiCommandBuffer *cb) {
} }
} }
int numPasses = m_mirrored ? 4 : 1;
// Prepare resource updates // Prepare resource updates
QRhiResourceUpdateBatch *u = m_rhi->nextResourceUpdateBatch(); 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(); QMatrix4x4 correction = m_rhi->clipSpaceCorrMatrix();
QMatrix4x4 proj;
for (int i = 0; i < numPasses; i++) { proj.ortho(0, (float)w, (float)h, 0, -1, 1);
QMatrix4x4 proj; QMatrix4x4 mvp = correction * proj;
proj.ortho(0, (float)w, (float)h, 0, -1, 1); u->updateDynamicBuffer(m_ubuf.get(), 0, 64, mvp.constData());
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());
}
// Begin render pass // Begin render pass
cb->beginPass(renderTarget(), QColor(0, 0, 0, 255), {1.0f, 0}, u); cb->beginPass(renderTarget(), QColor(0, 0, 0, 255), {1.0f, 0}, u);
@ -466,30 +540,25 @@ void VisualizerWidget::render(QRhiCommandBuffer *cb) {
(float)outputSize.height()}); (float)outputSize.height()});
const QRhiCommandBuffer::VertexInput vbufBinding(m_vbuf.get(), 0); const QRhiCommandBuffer::VertexInput vbufBinding(m_vbuf.get(), 0);
QRhiCommandBuffer::DynamicOffset dynOfs(0, 0);
for (int i = 0; i < numPasses; i++) { if (m_fillVertexCount > 0) {
QRhiCommandBuffer::DynamicOffset dynOfs(0, quint32(i * m_ubufAlign)); cb->setGraphicsPipeline(m_fillPipeline.get());
cb->setShaderResources(m_srb.get(), 1, &dynOfs);
if (m_fillVertexCount > 0) { cb->setVertexInput(0, 1, &vbufBinding);
cb->setGraphicsPipeline(m_fillPipeline.get()); cb->draw(m_fillVertexCount);
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);
}
} }
// --- Cepstral Thread (single full-screen pass, after mirror loop) --- if (m_lineVertexCount > 0) {
if (m_mirrored && m_cepstrumVertexCount > 0) {
QRhiCommandBuffer::DynamicOffset cepOfs(0, quint32(4 * m_ubufAlign));
cb->setGraphicsPipeline(m_linePipeline.get()); 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->setVertexInput(0, 1, &vbufBinding);
cb->draw(m_cepstrumVertexCount, 1, m_fillVertexCount + m_lineVertexCount, 0); cb->draw(m_cepstrumVertexCount, 1, m_fillVertexCount + m_lineVertexCount, 0);
} }

View File

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

View File

@ -103,12 +103,13 @@ void RealtimeHilbert::reinit(size_t fft_size) {
} }
// Create plans for Left Channel // 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); int n = static_cast<int>(m_fft_size);
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); 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 // 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_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(m_fft_size, m_ifft_input_spectrum_R, m_ifft_output_complex_R, FFTW_BACKWARD, 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) { if (!m_plan_forward_L || !m_plan_backward_L || !m_plan_forward_R || !m_plan_backward_R) {
cleanup(); cleanup();