diff --git a/CMakeLists.txt b/CMakeLists.txt index 38394f3..cb1011f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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") diff --git a/src/AudioEngine.cpp b/src/AudioEngine.cpp index 1841cf2..784901b 100644 --- a/src/AudioEngine.cpp +++ b/src/AudioEngine.cpp @@ -6,9 +6,7 @@ #include #include #include -#include #include -#include #include #include #include @@ -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(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 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(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(*bpmOpt) : 0.0f; - if (self) { - QMetaObject::invokeMethod(self, "analysisReady", Qt::QueuedConnection, - Q_ARG(float, bpm), Q_ARG(float, 1.0f)); - } -#endif - - }); } void AudioEngine::play() { diff --git a/src/AudioEngine.h b/src/AudioEngine.h index ac2945f..4031b2e 100644 --- a/src/AudioEngine.h +++ b/src/AudioEngine.h @@ -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 data); private slots: diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index afbc90d..679d9a2 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -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)); } diff --git a/src/MainWindow.h b/src/MainWindow.h index d1c5224..fd24a0d 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -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 m_metaLoader; QPointer m_metaThread; diff --git a/src/PlayerControls.cpp b/src/PlayerControls.cpp index ce22670..44c0d0b 100644 --- a/src/PlayerControls.cpp +++ b/src/PlayerControls.cpp @@ -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::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(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(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) { diff --git a/src/PlayerControls.h b/src/PlayerControls.h index e74890c..0047591 100644 --- a/src/PlayerControls.h +++ b/src/PlayerControls.h @@ -4,7 +4,6 @@ #include "CommonWidgets.h" #include "VisualizerWidget.h" #include -#include #include #include #include @@ -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; diff --git a/src/VisualizerWidget.cpp b/src/VisualizerWidget.cpp index 5bcf5aa..87c3fa4 100644 --- a/src/VisualizerWidget.cpp +++ b/src/VisualizerWidget.cpp @@ -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 &history) { + if (history.size() < 4) + return 0.0f; + + int N = static_cast(history.size()); + + // Forward DFT (O(N²) for N≈30 is ~900 ops — trivial, no FFTW needed) + std::vector> 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(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 &data) { @@ -171,14 +212,52 @@ void VisualizerWidget::updateData( std::vector 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 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 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 baseFill(m_vertices.begin(), + m_vertices.begin() + fillFloats); + std::vector baseLine(m_vertices.begin() + fillFloats, + m_vertices.end()); + m_vertices.clear(); + auto mir = [](const std::vector &src, std::vector &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); } diff --git a/src/VisualizerWidget.h b/src/VisualizerWidget.h index 1ae95ce..a2e136f 100644 --- a/src/VisualizerWidget.h +++ b/src/VisualizerWidget.h @@ -15,7 +15,7 @@ class VisualizerWidget : public QRhiWidget { public: VisualizerWidget(QWidget* parent = nullptr); void updateData(const std::vector& 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& 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 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& history); QColor applyModifiers(QColor c); }; diff --git a/src/complex_frames.cpp b/src/complex_frames.cpp index 4647b65..61725c2 100644 --- a/src/complex_frames.cpp +++ b/src/complex_frames.cpp @@ -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(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();