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:
parent
d674c25a29
commit
2b18e76c8f
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;");
|
||||||
|
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;");
|
"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) {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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 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;
|
float responsiveness = 0.2f;
|
||||||
bin.visualDb =
|
bin.visualDb =
|
||||||
(bin.visualDb * (1.0f - responsiveness)) + (rawVal * responsiveness);
|
(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();
|
||||||
|
|
||||||
for (int i = 0; i < numPasses; i++) {
|
|
||||||
QMatrix4x4 proj;
|
QMatrix4x4 proj;
|
||||||
proj.ortho(0, (float)w, (float)h, 0, -1, 1);
|
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;
|
QMatrix4x4 mvp = correction * proj;
|
||||||
u->updateDynamicBuffer(m_ubuf.get(), i * m_ubufAlign, 64,
|
u->updateDynamicBuffer(m_ubuf.get(), 0, 64, mvp.constData());
|
||||||
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,9 +540,7 @@ 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++) {
|
|
||||||
QRhiCommandBuffer::DynamicOffset dynOfs(0, quint32(i * m_ubufAlign));
|
|
||||||
|
|
||||||
if (m_fillVertexCount > 0) {
|
if (m_fillVertexCount > 0) {
|
||||||
cb->setGraphicsPipeline(m_fillPipeline.get());
|
cb->setGraphicsPipeline(m_fillPipeline.get());
|
||||||
|
|
@ -483,13 +555,10 @@ void VisualizerWidget::render(QRhiCommandBuffer *cb) {
|
||||||
cb->setVertexInput(0, 1, &vbufBinding);
|
cb->setVertexInput(0, 1, &vbufBinding);
|
||||||
cb->draw(m_lineVertexCount, 1, m_fillVertexCount, 0);
|
cb->draw(m_lineVertexCount, 1, m_fillVertexCount, 0);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// --- Cepstral Thread (single full-screen pass, after mirror loop) ---
|
if (m_cepstrumVertexCount > 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->setVertexInput(0, 1, &vbufBinding);
|
||||||
cb->draw(m_cepstrumVertexCount, 1, m_fillVertexCount + m_lineVertexCount, 0);
|
cb->draw(m_cepstrumVertexCount, 1, m_fillVertexCount + m_lineVertexCount, 0);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue