From 26ccd55d8c28be8906ef28ae3dd7b2a1ab31b8db Mon Sep 17 00:00:00 2001 From: pszsh Date: Wed, 28 Jan 2026 22:40:20 -0800 Subject: [PATCH] getting very close to a real release. of course, i'll have to deal with the bugs first. so instead, here's more visual effects! --- CMakeLists.txt | 22 ++-- src/AudioEngine.cpp | 113 ++++++++++++++--- src/AudioEngine.h | 15 ++- src/MainWindow.cpp | 68 ++++++---- src/MainWindow.h | 3 + src/PlayerControls.cpp | 145 ++++++++++++--------- src/PlayerControls.h | 38 ++++-- src/Processor.cpp | 266 +++++++++++++++++++++++++++++++-------- src/Processor.h | 40 ++++-- src/VisualizerWidget.cpp | 53 ++------ src/VisualizerWidget.h | 5 +- 11 files changed, 540 insertions(+), 228 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c81ab1e..016e200 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,15 +17,15 @@ option(BUILD_IOS "Build for iOS" OFF) find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets Multimedia OpenGLWidgets) -# --- FFTW3 Configuration --- +# --- FFTW3 Configuration (Double Precision) --- if(APPLE AND CMAKE_SYSTEM_PROCESSOR STREQUAL "arm64" AND NOT BUILD_IOS) - message(STATUS "Detected Apple Silicon Desktop. Using Homebrew FFTW3F.") - find_library(FFTW3_LIB NAMES fftw3f libfftw3f PATHS /opt/homebrew/lib NO_DEFAULT_PATH) + message(STATUS "Detected Apple Silicon Desktop. Using Homebrew FFTW3 (Double).") + find_library(FFTW3_LIB NAMES fftw3 libfftw3 PATHS /opt/homebrew/lib NO_DEFAULT_PATH) find_path(FFTW3_INCLUDE_DIR fftw3.h PATHS /opt/homebrew/include NO_DEFAULT_PATH) if(NOT FFTW3_LIB OR NOT FFTW3_INCLUDE_DIR) - message(FATAL_ERROR "FFTW3F not found in /opt/homebrew. Please run: brew install fftw") + message(FATAL_ERROR "FFTW3 not found in /opt/homebrew. Please run: brew install fftw") endif() add_library(fftw3 STATIC IMPORTED) @@ -35,9 +35,9 @@ if(APPLE AND CMAKE_SYSTEM_PROCESSOR STREQUAL "arm64" AND NOT BUILD_IOS) ) else() - message(STATUS "Building FFTW3 from source...") + message(STATUS "Building FFTW3 from source (Double Precision)...") - set(ENABLE_FLOAT ON CACHE BOOL "Build single precision" FORCE) + set(ENABLE_FLOAT OFF CACHE BOOL "Build double precision" FORCE) set(ENABLE_SSE OFF CACHE BOOL "Disable SSE" FORCE) set(ENABLE_SSE2 OFF CACHE BOOL "Disable SSE2" FORCE) set(ENABLE_AVX OFF CACHE BOOL "Disable AVX" FORCE) @@ -68,7 +68,6 @@ else() endif() # --- Loop Tempo Estimator --- -# Disable tests and vamp plugin for the library to speed up build and reduce deps 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) @@ -83,7 +82,6 @@ set(IOS_ASSETS_PATH "${CMAKE_CURRENT_SOURCE_DIR}/ios/Assets.xcassets") set(IOS_CONTENTS_JSON "${IOS_ASSETS_PATH}/Contents.json") set(ANDROID_RES_PATH "${CMAKE_CURRENT_SOURCE_DIR}/android/res/mipmap-mdpi/ic_launcher.png") -# Find ImageMagick 'magick' executable find_program(MAGICK_EXECUTABLE NAMES magick) if(NOT MAGICK_EXECUTABLE) message(WARNING "ImageMagick 'magick' not found. Icons will not be generated.") @@ -115,6 +113,8 @@ set(PROJECT_SOURCES src/CommonWidgets.cpp src/PlayerControls.cpp src/MainWindow.cpp + src/complex_block.cpp + src/trig_interpolation.cpp ) if(EXISTS "${ICON_SOURCE}") @@ -129,6 +129,8 @@ set(PROJECT_HEADERS src/CommonWidgets.h src/PlayerControls.h src/MainWindow.h + src/complex_block.h + src/trig_interpolation.h ) qt_add_executable(YrCrystals MANUAL_FINALIZATION ${PROJECT_SOURCES} ${PROJECT_HEADERS}) @@ -144,8 +146,8 @@ endif() # --- Linking --- -if(TARGET fftw3f) - set(FFTW_TARGET fftw3f) +if(TARGET fftw3) + set(FFTW_TARGET fftw3) target_include_directories(YrCrystals PRIVATE "${fftw3_source_SOURCE_DIR}/api" "${fftw3_source_BINARY_DIR}" diff --git a/src/AudioEngine.cpp b/src/AudioEngine.cpp index e42addb..1d62731 100644 --- a/src/AudioEngine.cpp +++ b/src/AudioEngine.cpp @@ -51,7 +51,7 @@ AudioEngine::AudioEngine(QObject* parent) : QObject(parent) { // Configure Main: Expander + HPF + Moderate Smoothing for(auto p : m_processors) { p->setExpander(1.5f, -50.0f); - p->setHPF(80.0f); // Mid 2nd Order HPF (80Hz) + p->setHPF(80.0f); p->setSmoothing(3); } @@ -63,10 +63,22 @@ AudioEngine::AudioEngine(QObject* parent) : QObject(parent) { // Configure Transient: Aggressive expansion, light smoothing for(auto p : m_transientProcessors) { p->setExpander(2.5f, -40.0f); - p->setHPF(100.0f); // Clean up transients + p->setHPF(100.0f); p->setSmoothing(2); } + // Deep Processors (Tertiary, High Res) + // Initial size will be set in setDspParams, default to 2x frameSize + m_deepProcessors.push_back(new Processor(m_frameSize * 2, m_sampleRate)); + m_deepProcessors.push_back(new Processor(m_frameSize * 2, m_sampleRate)); + + // Configure Deep: Low expander, no HPF (catch sub-bass), heavy smoothing + for(auto p : m_deepProcessors) { + p->setExpander(1.2f, -60.0f); + p->setHPF(0.0f); // Allow full sub-bass + p->setSmoothing(5); + } + m_processTimer = new QTimer(this); m_processTimer->setInterval(16); connect(m_processTimer, &QTimer::timeout, this, &AudioEngine::onProcessTimer); @@ -76,18 +88,33 @@ AudioEngine::~AudioEngine() { stop(); for(auto p : m_processors) delete p; for(auto p : m_transientProcessors) delete p; + for(auto p : m_deepProcessors) delete p; if (m_fileSource) delete m_fileSource; } void AudioEngine::setNumBins(int n) { for(auto p : m_processors) p->setNumBins(n); for(auto p : m_transientProcessors) p->setNumBins(n); + for(auto p : m_deepProcessors) p->setNumBins(n); +} + +void AudioEngine::setSmoothingParams(int granularity, int detail, float strength) { + for(auto p : m_processors) p->setCepstralParams(granularity, detail, strength); + // Transient: Less smoothing to keep punch + for(auto p : m_transientProcessors) p->setCepstralParams(granularity, detail, strength * 0.3f); + // Deep: More smoothing for stability + for(auto p : m_deepProcessors) p->setCepstralParams(granularity, detail, strength * 1.2f); } void AudioEngine::loadTrack(const QString& filePath) { stop(); - m_pcmData.clear(); - m_buffer.close(); + + { + QMutexLocker locker(&m_dataMutex); + m_pcmData.clear(); + m_complexData.clear(); + m_buffer.close(); + } m_sampleRate = 48000; @@ -145,12 +172,15 @@ void AudioEngine::onBufferReady() { qDebug() << "AudioEngine: Switching sample rate to" << m_sampleRate; for(auto p : m_processors) p->setSampleRate(m_sampleRate); for(auto p : m_transientProcessors) p->setSampleRate(m_sampleRate); + for(auto p : m_deepProcessors) p->setSampleRate(m_sampleRate); } const int frames = static_cast(buffer.frameCount()); const int channels = buffer.format().channelCount(); auto sampleType = buffer.format().sampleFormat(); + QMutexLocker locker(&m_dataMutex); + if (sampleType == QAudioFormat::Int16) { const int16_t* src = buffer.constData(); if (!src) return; @@ -194,6 +224,8 @@ void AudioEngine::onBufferReady() { } void AudioEngine::onFinished() { + QMutexLocker locker(&m_dataMutex); + if (m_pcmData.isEmpty()) { emit trackLoaded(false); return; @@ -219,6 +251,26 @@ void AudioEngine::onFinished() { emit analysisReady(0.0f, 0.0f); } } + + // --- Block Hilbert Transform (Offline Processing) --- + if (totalFrames > 0) { + std::vector inputL(totalFrames); + std::vector inputR(totalFrames); + + for (size_t i = 0; i < totalFrames; ++i) { + inputL[i] = static_cast(rawFloats[i * 2]); + inputR[i] = static_cast(rawFloats[i * 2 + 1]); + } + + BlockHilbert blockHilbert; + auto analyticPair = blockHilbert.hilbertTransform(inputL, inputR); + + m_complexData.resize(totalFloats); + for (size_t i = 0; i < totalFrames; ++i) { + m_complexData[i * 2] = analyticPair.first[i]; + m_complexData[i * 2 + 1] = analyticPair.second[i]; + } + } // ---------------------------- m_buffer.setData(m_pcmData); @@ -284,6 +336,7 @@ void AudioEngine::stop() { } void AudioEngine::seek(float position) { + QMutexLocker locker(&m_dataMutex); if (m_pcmData.isEmpty()) return; qint64 pos = position * m_pcmData.size(); pos -= pos % 8; @@ -300,25 +353,34 @@ void AudioEngine::setDspParams(int frameSize, int hopSize) { // Transient: 1/4 size (Minimum 64) int transSize = std::max(64, frameSize / 4); for(auto p : m_transientProcessors) p->setFrameSize(transSize); + + // Deep: 2x or 4x size + int deepSize; + if (frameSize < 2048) { + deepSize = frameSize * 4; + } else { + deepSize = frameSize * 2; + } + for(auto p : m_deepProcessors) p->setFrameSize(deepSize); } void AudioEngine::onProcessTimer() { if (!m_buffer.isOpen()) return; + QMutexLocker locker(&m_dataMutex); + qint64 currentPos = m_buffer.pos(); emit positionChanged((float)currentPos / m_pcmData.size()); - const float* samples = reinterpret_cast(m_pcmData.constData()); - qint64 sampleIdx = currentPos / sizeof(float); - qint64 totalSamples = m_pcmData.size() / sizeof(float); + qint64 sampleIdx = currentPos / sizeof(float); + + if (sampleIdx + m_frameSize * 2 >= m_complexData.size()) return; - if (sampleIdx + m_frameSize * 2 >= totalSamples) return; - - // Prepare data for Main Processors - std::vector ch0(m_frameSize), ch1(m_frameSize); + // Prepare data for Main Processors (Complex Double) + std::vector> ch0(m_frameSize), ch1(m_frameSize); for (int i = 0; i < m_frameSize; ++i) { - ch0[i] = samples[sampleIdx + i*2]; - ch1[i] = samples[sampleIdx + i*2 + 1]; + ch0[i] = m_complexData[sampleIdx + i*2]; + ch1[i] = m_complexData[sampleIdx + i*2 + 1]; } m_processors[0]->pushData(ch0); @@ -326,7 +388,7 @@ void AudioEngine::onProcessTimer() { // Prepare data for Transient Processors (Smaller window) int transSize = std::max(64, m_frameSize / 4); - std::vector tCh0(transSize), tCh1(transSize); + std::vector> tCh0(transSize), tCh1(transSize); int offset = m_frameSize - transSize; for (int i = 0; i < transSize; ++i) { tCh0[i] = ch0[offset + i]; @@ -336,6 +398,21 @@ void AudioEngine::onProcessTimer() { m_transientProcessors[0]->pushData(tCh0); m_transientProcessors[1]->pushData(tCh1); + // Prepare data for Deep Processors (Larger window) + // We need to grab more data from m_complexData if available + // Deep size is dynamic, check first processor + // Note: Processor::pushData handles buffering, so we just push the current m_frameSize chunk + // and the processor will append it to its internal history. + // However, for best results with a larger FFT, we should ideally provide the full window if possible, + // but since we are streaming, pushing the hop (m_frameSize) is the standard overlap-add approach. + // Wait, m_frameSize here acts as the "hop" for the larger processors if we just push it. + // Processor::pushData shifts by data.size(). + // So if we push m_frameSize samples, the Deep processor (size e.g. 8192) will shift by 4096 and append 4096. + // This results in 50% overlap if DeepSize = 2 * FrameSize. Perfect. + + m_deepProcessors[0]->pushData(ch0); + m_deepProcessors[1]->pushData(ch1); + std::vector results; // Final Compressor Settings @@ -345,14 +422,16 @@ void AudioEngine::onProcessTimer() { for (size_t i = 0; i < m_processors.size(); ++i) { auto specMain = m_processors[i]->getSpectrum(); auto specTrans = m_transientProcessors[i]->getSpectrum(); + auto specDeep = m_deepProcessors[i]->getSpectrum(); // Capture Primary DB (Steady State) for Crystal Pattern std::vector primaryDb = specMain.db; - // Mix: Overlay the expanded transient peaks onto the main spectrum - if (specMain.db.size() == specTrans.db.size()) { + // Mix: Overlay Main + Transient + Deep + if (specMain.db.size() == specTrans.db.size() && specMain.db.size() == specDeep.db.size()) { for(size_t b = 0; b < specMain.db.size(); ++b) { - float val = std::max(specMain.db[b], specTrans.db[b]); + // Max of all three + float val = std::max({specMain.db[b], specTrans.db[b], specDeep.db[b]}); // Final Compressor (Hard Knee) if (val > compThreshold) { diff --git a/src/AudioEngine.h b/src/AudioEngine.h index fa3be9c..fac48ce 100644 --- a/src/AudioEngine.h +++ b/src/AudioEngine.h @@ -7,8 +7,11 @@ #include #include #include +#include #include +#include #include "Processor.h" +#include "complex_block.h" class AudioEngine : public QObject { Q_OBJECT @@ -18,7 +21,7 @@ public: struct FrameData { std::vector freqs; - std::vector db; // Mixed (Primary + Transient) -> For Bar Height + std::vector db; // Mixed (Primary + Transient + Deep) std::vector primaryDb; // Primary Only -> For Crystal Pattern }; @@ -30,6 +33,9 @@ public slots: void seek(float position); void setDspParams(int frameSize, int hopSize); void setNumBins(int n); + + // Cepstral/Smoothing Controls + void setSmoothingParams(int granularity, int detail, float strength); signals: void playbackFinished(); @@ -47,7 +53,11 @@ private slots: private: QAudioSink* m_sink = nullptr; QBuffer m_buffer; - QByteArray m_pcmData; + QByteArray m_pcmData; // Raw PCM for playback (Real) + mutable QMutex m_dataMutex; // Protects m_pcmData and m_complexData + + // Complex Analytical Stream (Pre-calculated) - Double Precision + std::vector> m_complexData; QAudioDecoder* m_decoder = nullptr; QFile* m_fileSource = nullptr; @@ -55,6 +65,7 @@ private: std::vector m_processors; // Main (Steady) std::vector m_transientProcessors; // Secondary (Fast/Transient) + std::vector m_deepProcessors; // Tertiary (Deep/Bass) int m_frameSize = 4096; int m_hopSize = 1024; diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index aa5894c..6012e18 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -48,6 +48,12 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) { connect(m_engine, &AudioEngine::spectrumReady, m_playerPage->visualizer(), &VisualizerWidget::updateData); connect(m_engine, &AudioEngine::analysisReady, this, &MainWindow::onAnalysisReady); + // Connect new smoothing params from Settings to Engine + connect(m_playerPage->settings(), &SettingsWidget::paramsChanged, this, [this](bool, bool, bool, bool, bool, bool, float, float, float, int granularity, int detail, float strength){ + QMetaObject::invokeMethod(m_engine, "setSmoothingParams", Qt::QueuedConnection, + Q_ARG(int, granularity), Q_ARG(int, detail), Q_ARG(float, strength)); + }); + audioThread->start(); } @@ -81,9 +87,14 @@ void MainWindow::initUi() { connect(set, &SettingsWidget::paramsChanged, viz, &VisualizerWidget::setParams); connect(set, &SettingsWidget::dspParamsChanged, this, &MainWindow::onDspChanged); connect(set, &SettingsWidget::binsChanged, this, &MainWindow::onBinsChanged); + + // Connect BPM Scale change to update logic + connect(set, &SettingsWidget::bpmScaleChanged, this, &MainWindow::updateSmoothing); connect(set, &SettingsWidget::paramsChanged, this, &MainWindow::saveSettings); connect(set, &SettingsWidget::binsChanged, this, &MainWindow::saveSettings); + // Also save when BPM scale changes + connect(set, &SettingsWidget::bpmScaleChanged, this, &MainWindow::saveSettings); connect(m_playerPage, &PlayerPage::toggleFullScreen, this, &MainWindow::onToggleFullScreen); @@ -241,9 +252,14 @@ void MainWindow::loadSettings() { bool mirrored = root["mirrored"].toBool(false); int bins = root["bins"].toInt(26); float brightness = root["brightness"].toDouble(1.0); - float entropy = root["entropy"].toDouble(1.0); + + // New Smoothing Params + int granularity = root["granularity"].toInt(33); + int detail = root["detail"].toInt(50); + float strength = root["strength"].toDouble(0.0); + int bpmScaleIndex = root["bpmScaleIndex"].toInt(2); // Default 1/4 - m_playerPage->settings()->setParams(glass, focus, trails, albumColors, shadow, mirrored, bins, brightness, entropy); + m_playerPage->settings()->setParams(glass, focus, trails, albumColors, shadow, mirrored, bins, brightness, granularity, detail, strength, bpmScaleIndex); } } @@ -261,7 +277,12 @@ void MainWindow::saveSettings() { root["mirrored"] = s->isMirrored(); root["bins"] = s->getBins(); root["brightness"] = s->getBrightness(); - root["entropy"] = s->getEntropy(); + + // New Smoothing Params + root["granularity"] = s->getGranularity(); + root["detail"] = s->getDetail(); + root["strength"] = s->getStrength(); + root["bpmScaleIndex"] = s->getBpmScaleIndex(); QFile f(QDir(m_settingsDir).filePath(".yrcrystals.json")); if (f.open(QIODevice::WriteOnly)) { @@ -299,32 +320,35 @@ 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; + // Feedback Mechanism: - // Adjust Entropy based on BPM. - // High BPM (Fast/Punchy) -> Lower Entropy Slider (0.5 - 0.8) to allow transients. - // Low BPM (Slow/Ambient) -> Higher Entropy Slider (1.0 - 1.5) to smooth noise. - // Default (No BPM) -> 1.0 + // Adjust Smoothing Strength based on effective BPM. + // High BPM (Fast/Punchy) -> Lower Strength (More Raw). + // Low BPM (Slow/Ambient) -> Higher Strength (Smoother). + + float targetStrength = 0.0f; - float targetEntropy = 1.0f; + // Map 60..180 BPM to 0.8..0.0 Strength + float normalized = std::clamp((effectiveBpm - 60.0f) / 120.0f, 0.0f, 1.0f); + targetStrength = 0.8f * (1.0f - normalized); + + qDebug() << "Feedback: BPM" << m_lastBpm << "Scale" << scale << "Effective" << effectiveBpm << "-> Strength" << targetStrength; - if (bpm > 0.0f) { - // Map 60..180 BPM to 1.5..0.5 Entropy - // Formula: 1.5 - ((bpm - 60) / 120) - // Clamped between 0.5 and 1.5 - float normalized = (bpm - 60.0f) / 120.0f; - targetEntropy = 1.5f - normalized; - targetEntropy = std::clamp(targetEntropy, 0.5f, 1.5f); - qDebug() << "Feedback: BPM" << bpm << "-> Setting Entropy to" << targetEntropy; - } else { - qDebug() << "Feedback: No BPM -> Default Entropy 1.0"; - } - - // Update Settings Widget (which updates Visualizer) + // Update Settings Widget (which updates Visualizer/Engine) SettingsWidget* s = m_playerPage->settings(); s->setParams( s->isGlass(), s->isFocus(), s->isTrails(), s->isAlbumColors(), s->isShadow(), s->isMirrored(), s->getBins(), s->getBrightness(), - targetEntropy + s->getGranularity(), s->getDetail(), targetStrength, s->getBpmScaleIndex() ); } diff --git a/src/MainWindow.h b/src/MainWindow.h index 7f29e2e..1f5494d 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -28,6 +28,7 @@ private slots: void onTrackLoaded(bool success); void onTrackDoubleClicked(QListWidgetItem* item); void onAnalysisReady(float bpm, float confidence); + void updateSmoothing(); // New slot for BPM feedback logic void play(); void pause(); void nextTrack(); @@ -59,4 +60,6 @@ private: enum class PendingAction { None, File, Folder }; PendingAction m_pendingAction = PendingAction::None; QString m_settingsDir; + + float m_lastBpm = 0.0f; // Store last detected BPM }; \ No newline at end of file diff --git a/src/PlayerControls.cpp b/src/PlayerControls.cpp index d7f1dc1..2eec554 100644 --- a/src/PlayerControls.cpp +++ b/src/PlayerControls.cpp @@ -76,7 +76,7 @@ SettingsWidget::SettingsWidget(QWidget* parent) : QWidget(parent) { QVBoxLayout* layout = new QVBoxLayout(this); layout->setContentsMargins(15, 15, 15, 15); - layout->setSpacing(15); + layout->setSpacing(10); QHBoxLayout* header = new QHBoxLayout(); QLabel* title = new QLabel("Settings", this); @@ -102,73 +102,71 @@ SettingsWidget::SettingsWidget(QWidget* parent) : QWidget(parent) { return cb; }; - // Defaults: Only Glass checked + // Updated Defaults based on user request m_checkGlass = createCheck("Glass", true, 0, 0); - m_checkFocus = createCheck("Focus", false, 0, 1); - m_checkTrails = createCheck("Trails", false, 1, 0); + m_checkFocus = createCheck("Focus", true, 0, 1); + m_checkTrails = createCheck("Trails", true, 1, 0); m_checkAlbumColors = createCheck("Album Colors", false, 1, 1); m_checkShadow = createCheck("Shadow", false, 2, 0); - m_checkMirrored = createCheck("Mirrored", false, 2, 1); + m_checkMirrored = createCheck("Mirrored", true, 2, 1); layout->addLayout(grid); - // Bins Slider - QHBoxLayout* binsLayout = new QHBoxLayout(); - m_lblBins = new QLabel("Bins: 26", this); - m_lblBins->setStyleSheet("color: white; font-weight: bold; border: none; background: transparent; min-width: 80px;"); + // Helper for sliders + auto addSlider = [&](const QString& label, int min, int max, int val, QSlider*& slider, QLabel*& lbl) { + QHBoxLayout* h = new QHBoxLayout(); + lbl = new QLabel(label, this); + lbl->setStyleSheet("color: white; font-weight: bold; border: none; background: transparent; min-width: 80px;"); + slider = new QSlider(Qt::Horizontal, this); + slider->setRange(min, max); + slider->setValue(val); + slider->setStyleSheet("QSlider::handle:horizontal { background: #aaa; width: 24px; margin: -10px 0; border-radius: 12px; } QSlider::groove:horizontal { background: #444; height: 4px; }"); + h->addWidget(lbl); + h->addWidget(slider); + layout->addLayout(h); + }; - m_sliderBins = new QSlider(Qt::Horizontal, this); - m_sliderBins->setRange(10, 64); - m_sliderBins->setValue(26); - m_sliderBins->setStyleSheet("QSlider::handle:horizontal { background: #aaa; width: 24px; margin: -10px 0; border-radius: 12px; } QSlider::groove:horizontal { background: #444; height: 4px; }"); + // Updated Slider Defaults + addSlider("Bins: 21", 10, 64, 21, m_sliderBins, m_lblBins); connect(m_sliderBins, &QSlider::valueChanged, this, &SettingsWidget::onBinsChanged); - binsLayout->addWidget(m_lblBins); - binsLayout->addWidget(m_sliderBins); - layout->addLayout(binsLayout); - - // Brightness Slider - QHBoxLayout* brightLayout = new QHBoxLayout(); - m_lblBrightness = new QLabel("Bright: 100%", this); - m_lblBrightness->setStyleSheet("color: white; font-weight: bold; border: none; background: transparent; min-width: 80px;"); - - m_sliderBrightness = new QSlider(Qt::Horizontal, this); - m_sliderBrightness->setRange(10, 200); // 10% to 200% - m_sliderBrightness->setValue(100); - m_sliderBrightness->setStyleSheet("QSlider::handle:horizontal { background: #aaa; width: 24px; margin: -10px 0; border-radius: 12px; } QSlider::groove:horizontal { background: #444; height: 4px; }"); + addSlider("Bright: 66%", 10, 200, 66, m_sliderBrightness, m_lblBrightness); connect(m_sliderBrightness, &QSlider::valueChanged, this, &SettingsWidget::onBrightnessChanged); - brightLayout->addWidget(m_lblBrightness); - brightLayout->addWidget(m_sliderBrightness); - layout->addLayout(brightLayout); + addSlider("Granularity", 0, 100, 95, m_sliderGranularity, m_lblGranularity); + connect(m_sliderGranularity, &QSlider::valueChanged, this, &SettingsWidget::onSmoothingChanged); - // Entropy Slider - QHBoxLayout* entropyLayout = new QHBoxLayout(); - m_lblEntropy = new QLabel("Entropy: 1.0", this); - m_lblEntropy->setStyleSheet("color: white; font-weight: bold; border: none; background: transparent; min-width: 80px;"); + addSlider("Detail", 0, 100, 5, m_sliderDetail, m_lblDetail); + connect(m_sliderDetail, &QSlider::valueChanged, this, &SettingsWidget::onSmoothingChanged); - m_sliderEntropy = new QSlider(Qt::Horizontal, this); - m_sliderEntropy->setRange(0, 300); // 0.0 to 3.0 - m_sliderEntropy->setValue(100); - m_sliderEntropy->setStyleSheet("QSlider::handle:horizontal { background: #aaa; width: 24px; margin: -10px 0; border-radius: 12px; } QSlider::groove:horizontal { background: #444; height: 4px; }"); - connect(m_sliderEntropy, &QSlider::valueChanged, this, &SettingsWidget::onEntropyChanged); + addSlider("Strength", 0, 100, 95, m_sliderStrength, m_lblStrength); + connect(m_sliderStrength, &QSlider::valueChanged, this, &SettingsWidget::onSmoothingChanged); - entropyLayout->addWidget(m_lblEntropy); - entropyLayout->addWidget(m_sliderEntropy); - layout->addLayout(entropyLayout); + // 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;"); + + 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); QHBoxLayout* padsLayout = new QHBoxLayout(); m_padDsp = new XYPad("DSP", this); m_padDsp->setFormatter([](float x, float y) { - // Range: 2^6 (64) to 2^13 (8192) int power = 6 + (int)(x * 7.0f + 0.5f); int fft = std::pow(2, power); int hop = 64 + y * (8192 - 64); return QString("FFT: %1\nHop: %2").arg(fft).arg(hop); }); - - // Default to ~4096 FFT (x approx 0.857) and reasonable hop - m_padDsp->setValues(0.857f, 0.118f); + // Default to FFT 8192 (x=1.0), Hop 64 (y=0.0) + m_padDsp->setValues(1.0f, 0.0f); connect(m_padDsp, &XYPad::valuesChanged, this, &SettingsWidget::onDspPadChanged); padsLayout->addWidget(m_padDsp); @@ -178,14 +176,15 @@ SettingsWidget::SettingsWidget(QWidget* parent) : QWidget(parent) { float cont = 0.1f + y * 2.9f; return QString("Hue: %1\nCont: %2").arg(hue, 0, 'f', 2).arg(cont, 0, 'f', 2); }); - m_padColor->setValues(0.45f, (1.0f - 0.1f) / 2.9f); + // Default to Hue 0.35 (x=0.175), Cont 0.10 (y=0.0) + m_padColor->setValues(0.175f, 0.0f); connect(m_padColor, &XYPad::valuesChanged, this, &SettingsWidget::onColorPadChanged); padsLayout->addWidget(m_padColor); layout->addLayout(padsLayout); } -void SettingsWidget::setParams(bool glass, bool focus, bool trails, bool albumColors, bool shadow, bool mirrored, int bins, float brightness, float entropy) { +void SettingsWidget::setParams(bool glass, bool focus, bool trails, bool albumColors, bool shadow, bool mirrored, int bins, float brightness, int granularity, int detail, float strength, int bpmScaleIndex) { bool oldState = blockSignals(true); m_checkGlass->setChecked(glass); m_checkFocus->setChecked(focus); @@ -197,14 +196,21 @@ void SettingsWidget::setParams(bool glass, bool focus, bool trails, bool albumCo m_lblBins->setText(QString("Bins: %1").arg(bins)); m_brightness = brightness; - int brightVal = static_cast(brightness * 100.0f); - m_sliderBrightness->setValue(brightVal); - m_lblBrightness->setText(QString("Bright: %1%").arg(brightVal)); + m_sliderBrightness->setValue(static_cast(brightness * 100.0f)); + m_lblBrightness->setText(QString("Bright: %1%").arg(static_cast(brightness * 100.0f))); - m_entropy = entropy; - int entVal = static_cast(entropy * 100.0f); - m_sliderEntropy->setValue(entVal); - m_lblEntropy->setText(QString("Entropy: %1").arg(entropy, 0, 'f', 1)); + m_granularity = granularity; + m_sliderGranularity->setValue(granularity); + + m_detail = detail; + m_sliderDetail->setValue(detail); + + m_strength = strength; + m_sliderStrength->setValue(static_cast(strength * 100.0f)); + + if (bpmScaleIndex >= 0 && bpmScaleIndex < m_comboBpmScale->count()) { + m_comboBpmScale->setCurrentIndex(bpmScaleIndex); + } blockSignals(oldState); @@ -223,12 +229,32 @@ void SettingsWidget::emitParams() { m_hue, m_contrast, m_brightness, - m_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::onDspPadChanged(float x, float y) { - // Range: 2^6 (64) to 2^13 (8192) int power = 6 + (int)(x * 7.0f + 0.5f); m_fft = std::pow(2, power); m_hop = 64 + y * (8192 - 64); @@ -253,9 +279,10 @@ void SettingsWidget::onBrightnessChanged(int val) { emitParams(); } -void SettingsWidget::onEntropyChanged(int val) { - m_entropy = val / 100.0f; - m_lblEntropy->setText(QString("Entropy: %1").arg(m_entropy, 0, 'f', 1)); +void SettingsWidget::onSmoothingChanged(int val) { + m_granularity = m_sliderGranularity->value(); + m_detail = m_sliderDetail->value(); + m_strength = m_sliderStrength->value() / 100.0f; emitParams(); } diff --git a/src/PlayerControls.h b/src/PlayerControls.h index f840ead..173a529 100644 --- a/src/PlayerControls.h +++ b/src/PlayerControls.h @@ -6,6 +6,7 @@ #include #include #include +#include #include "VisualizerWidget.h" #include "CommonWidgets.h" @@ -46,22 +47,33 @@ public: bool isMirrored() const { return m_checkMirrored->isChecked(); } int getBins() const { return m_sliderBins->value(); } float getBrightness() const { return m_brightness; } - float getEntropy() const { return m_entropy; } + + int getGranularity() const { return m_sliderGranularity->value(); } + 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 trails, bool albumColors, bool shadow, bool mirrored, int bins, float brightness, float entropy); + void setParams(bool glass, bool focus, bool trails, bool albumColors, bool shadow, bool mirrored, int bins, float brightness, int granularity, int detail, float strength, int bpmScaleIndex); signals: - void paramsChanged(bool glass, bool focus, bool trails, bool albumColors, bool shadow, bool mirrored, float hue, float contrast, float brightness, float entropy); + void paramsChanged(bool glass, bool focus, bool trails, bool albumColors, bool shadow, bool mirrored, float hue, float contrast, float brightness, int granularity, int detail, float strength); void dspParamsChanged(int fft, int hop); void binsChanged(int n); + void bpmScaleChanged(float scale); void closeClicked(); + private slots: void emitParams(); void onDspPadChanged(float x, float y); void onColorPadChanged(float x, float y); void onBinsChanged(int val); void onBrightnessChanged(int val); - void onEntropyChanged(int val); + void onSmoothingChanged(int val); + void onBpmScaleChanged(int index); + private: QCheckBox* m_checkGlass; QCheckBox* m_checkFocus; @@ -75,12 +87,24 @@ private: QLabel* m_lblBins; QSlider* m_sliderBrightness; QLabel* m_lblBrightness; - QSlider* m_sliderEntropy; - QLabel* m_lblEntropy; + + QSlider* m_sliderGranularity; + QLabel* m_lblGranularity; + QSlider* m_sliderDetail; + QLabel* m_lblDetail; + QSlider* m_sliderStrength; + QLabel* m_lblStrength; + + QComboBox* m_comboBpmScale; + float m_hue = 0.9f; float m_contrast = 1.0f; float m_brightness = 1.0f; - float m_entropy = 1.0f; + + int m_granularity = 33; + int m_detail = 50; + float m_strength = 0.0f; + int m_fft = 4096; int m_hop = 1024; int m_bins = 26; diff --git a/src/Processor.cpp b/src/Processor.cpp index 8cfdee7..1413e2a 100644 --- a/src/Processor.cpp +++ b/src/Processor.cpp @@ -4,21 +4,28 @@ #include #include #include +#include const double PI = 3.14159265358979323846; Processor::Processor(int frameSize, int sampleRate) : m_frameSize(0), m_sampleRate(sampleRate), - m_in(nullptr), m_out(nullptr), m_plan(nullptr) + m_in(nullptr), m_out(nullptr), m_plan(nullptr), + m_cep_in(nullptr), m_cep_out(nullptr), m_cep_plan_fwd(nullptr), m_cep_plan_inv(nullptr) { setFrameSize(frameSize); setNumBins(26); } Processor::~Processor() { - if (m_plan) fftwf_destroy_plan(m_plan); - if (m_in) fftwf_free(m_in); - if (m_out) fftwf_free(m_out); + if (m_plan) fftw_destroy_plan(m_plan); + if (m_in) fftw_free(m_in); + if (m_out) fftw_free(m_out); + + if (m_cep_plan_fwd) fftw_destroy_plan(m_cep_plan_fwd); + if (m_cep_plan_inv) fftw_destroy_plan(m_cep_plan_inv); + if (m_cep_in) fftw_free(m_cep_in); + if (m_cep_out) fftw_free(m_cep_out); } void Processor::setSampleRate(int rate) { @@ -41,55 +48,73 @@ void Processor::setHPF(float cutoffFreq) { m_hpfCutoff = cutoffFreq; } +void Processor::setCepstralParams(int granularity, int detail, float strength) { + m_granularity = granularity; + m_detail = detail; + m_cepstralStrength = strength; +} + void Processor::setNumBins(int n) { m_customBins.clear(); m_freqsConst.clear(); m_history.clear(); - float minFreq = 40.0f; - float maxFreq = 11000.0f; + double minFreq = 40.0; + double maxFreq = 11000.0; for (int i = 0; i <= n; ++i) { - float f = minFreq * std::pow(maxFreq / minFreq, (float)i / n); + double f = minFreq * std::pow(maxFreq / minFreq, (double)i / n); m_customBins.push_back(f); } - m_freqsConst.push_back(10.0f); + m_freqsConst.push_back(10.0); for (size_t i = 0; i < m_customBins.size() - 1; ++i) { - m_freqsConst.push_back((m_customBins[i] + m_customBins[i+1]) / 2.0f); + m_freqsConst.push_back((m_customBins[i] + m_customBins[i+1]) / 2.0); } } void Processor::setFrameSize(int size) { if (m_frameSize == size) return; - if (m_plan) fftwf_destroy_plan(m_plan); - if (m_in) fftwf_free(m_in); - if (m_out) fftwf_free(m_out); + if (m_plan) fftw_destroy_plan(m_plan); + if (m_in) fftw_free(m_in); + if (m_out) fftw_free(m_out); + + if (m_cep_plan_fwd) fftw_destroy_plan(m_cep_plan_fwd); + if (m_cep_plan_inv) fftw_destroy_plan(m_cep_plan_inv); + if (m_cep_in) fftw_free(m_cep_in); + if (m_cep_out) fftw_free(m_cep_out); m_frameSize = size; - m_in = (float*)fftwf_malloc(sizeof(float) * m_frameSize); - m_out = (fftwf_complex*)fftwf_malloc(sizeof(fftwf_complex) * (m_frameSize / 2 + 1)); - m_plan = fftwf_plan_dft_r2c_1d(m_frameSize, m_in, m_out, FFTW_ESTIMATE); + // Main FFT (Complex -> Complex) + m_in = (fftw_complex*)fftw_malloc(sizeof(fftw_complex) * m_frameSize); + m_out = (fftw_complex*)fftw_malloc(sizeof(fftw_complex) * m_frameSize); + m_plan = fftw_plan_dft_1d(m_frameSize, m_in, m_out, FFTW_FORWARD, FFTW_ESTIMATE); + + // Cepstral FFTs (Complex -> Complex) + m_cep_in = (fftw_complex*)fftw_malloc(sizeof(fftw_complex) * m_frameSize); + m_cep_out = (fftw_complex*)fftw_malloc(sizeof(fftw_complex) * m_frameSize); + m_cep_plan_fwd = fftw_plan_dft_1d(m_frameSize, m_cep_in, m_cep_out, FFTW_FORWARD, FFTW_ESTIMATE); + m_cep_plan_inv = fftw_plan_dft_1d(m_frameSize, m_cep_in, m_cep_out, FFTW_BACKWARD, FFTW_ESTIMATE); m_window.resize(m_frameSize); // Blackman-Harris window for (int i = 0; i < m_frameSize; ++i) { - float a0 = 0.35875f; - float a1 = 0.48829f; - float a2 = 0.14128f; - float a3 = 0.01168f; - m_window[i] = a0 - a1 * std::cos(2.0f * PI * i / (m_frameSize - 1)) - + a2 * std::cos(4.0f * PI * i / (m_frameSize - 1)) - - a3 * std::cos(6.0f * PI * i / (m_frameSize - 1)); + double a0 = 0.35875; + double a1 = 0.48829; + double a2 = 0.14128; + double a3 = 0.01168; + m_window[i] = a0 - a1 * std::cos(2.0 * PI * i / (m_frameSize - 1)) + + a2 * std::cos(4.0 * PI * i / (m_frameSize - 1)) + - a3 * std::cos(6.0 * PI * i / (m_frameSize - 1)); } - m_buffer.assign(m_frameSize, 0.0f); + m_buffer.assign(m_frameSize, {0.0, 0.0}); m_history.clear(); } -void Processor::pushData(const std::vector& data) { +void Processor::pushData(const std::vector>& data) { if (data.size() == m_frameSize) { std::copy(data.begin(), data.end(), m_buffer.begin()); } else if (data.size() < m_frameSize) { @@ -98,7 +123,7 @@ void Processor::pushData(const std::vector& data) { } } -float Processor::getInterpolatedDb(const std::vector& freqs, const std::vector& db, float targetFreq) { +double Processor::getInterpolatedDb(const std::vector& freqs, const std::vector& db, double targetFreq) { auto it = std::lower_bound(freqs.begin(), freqs.end(), targetFreq); if (it == freqs.begin()) return db[0]; if (it == freqs.end()) return db.back(); @@ -106,61 +131,188 @@ float Processor::getInterpolatedDb(const std::vector& freqs, const std::v size_t idxUpper = std::distance(freqs.begin(), it); size_t idxLower = idxUpper - 1; - float f0 = freqs[idxLower]; - float f1 = freqs[idxUpper]; - float d0 = db[idxLower]; - float d1 = db[idxUpper]; + double f0 = freqs[idxLower]; + double f1 = freqs[idxUpper]; + double d0 = db[idxLower]; + double d1 = db[idxUpper]; - float t = (targetFreq - f0) / (f1 - f0); + double t = (targetFreq - f0) / (f1 - f0); return d0 + t * (d1 - d0); } +// Implementation of the Trig Interpolation "Idealize Curve" logic +std::vector Processor::idealizeCurve(const std::vector& magSpectrum) { + size_t n = magSpectrum.size(); + std::vector current_curve = magSpectrum; + + // Map slider values to algorithm parameters + int num_bins = 4 + static_cast(m_granularity / 100.0 * 60.0); + int iterations = 1 + static_cast(m_detail / 100.0 * 4.0); + + for (int iter = 0; iter < iterations; ++iter) { + std::vector next_curve(n, 0.0); + std::vector is_point_set(n, false); + + // 1. Binning + std::vector bin_boundaries; + double log_n = std::log((double)n); + for (int i = 0; i <= num_bins; ++i) { + double ratio = static_cast(i) / num_bins; + size_t boundary = static_cast(std::exp(ratio * log_n)); + boundary = std::max((size_t)1, std::min(n - 1, boundary)); + bin_boundaries.push_back(boundary); + } + bin_boundaries[0] = 0; + + // 2. Find extrema and process within bins + for (int i = 0; i < num_bins; ++i) { + size_t bin_start = bin_boundaries[i]; + size_t bin_end = bin_boundaries[i+1]; + if (bin_start >= bin_end -1) continue; + + std::vector> extrema; + for (size_t j = bin_start + 1; j < bin_end; ++j) { + if ((current_curve[j] > current_curve[j-1] && current_curve[j] > current_curve[j+1]) || + (current_curve[j] < current_curve[j-1] && current_curve[j] < current_curve[j+1])) { + extrema.push_back({j, current_curve[j]}); + } + } + if (extrema.empty()) continue; + + // 3. Weaving/Smoothing via cosine bell interpolation + for (size_t j = bin_start; j <= bin_end; ++j) { + double total_weight = 0.0; + double weighted_sum = 0.0; + for (const auto& extremum : extrema) { + double dist = static_cast(j) - extremum.first; + double lobe_width = (bin_end - bin_start) / 2.0; + if (std::abs(dist) < lobe_width) { + double weight = (std::cos(dist / lobe_width * PI) + 1.0) / 2.0; // Hann window + weighted_sum += extremum.second * weight; + total_weight += weight; + } + } + if (total_weight > 0) { + next_curve[j] = weighted_sum / total_weight; + is_point_set[j] = true; + } + } + } + + // 4. Gap Filling (linear interpolation) + size_t last_set_point = 0; + if(is_point_set[0]) last_set_point = 0; + else { + for(size_t i=0; i last_set_point + 1) { // Gap detected + double start_val = next_curve[last_set_point]; + double end_val = next_curve[i]; + for (size_t j = last_set_point + 1; j < i; ++j) { + double progress = static_cast(j - last_set_point) / (i - last_set_point); + next_curve[j] = start_val + (end_val - start_val) * progress; + } + } + last_set_point = i; + } + } + current_curve = next_curve; + } + return current_curve; +} + Processor::Spectrum Processor::getSpectrum() { - // 1. Windowing + // 1. Windowing (Complex) for (int i = 0; i < m_frameSize; ++i) { - m_in[i] = m_buffer[i] * m_window[i]; + m_in[i][0] = m_buffer[i].real() * m_window[i]; + m_in[i][1] = m_buffer[i].imag() * m_window[i]; } // 2. FFT - fftwf_execute(m_plan); + fftw_execute(m_plan); - // 3. Compute Magnitude (dB) + // 3. Compute Magnitude int bins = m_frameSize / 2 + 1; - std::vector freqsFull(bins); - std::vector dbFull(bins); + std::vector freqsFull(bins); + std::vector magFull(bins); for (int i = 0; i < bins; ++i) { - float re = m_out[i][0]; - float im = m_out[i][1]; - float mag = 2.0f * std::sqrt(re*re + im*im) / m_frameSize; + double re = m_out[i][0]; + double im = m_out[i][1]; + double mag = 2.0 * std::sqrt(re*re + im*im) / m_frameSize; - float freq = i * (float)m_sampleRate / m_frameSize; + double freq = i * (double)m_sampleRate / m_frameSize; - // HPF: 2nd Order Butterworth Curve - // Gain = 1 / sqrt(1 + (fc/f)^4) + // HPF if (m_hpfCutoff > 0.0f && freq > 0.0f) { - float ratio = m_hpfCutoff / freq; - float gain = 1.0f / std::sqrt(1.0f + (ratio * ratio * ratio * ratio)); + double ratio = m_hpfCutoff / freq; + double gain = 1.0 / std::sqrt(1.0 + (ratio * ratio * ratio * ratio)); mag *= gain; - } else if (freq == 0.0f) { - mag = 0.0f; + } else if (freq == 0.0) { + mag = 0.0; } - - dbFull[i] = 20.0f * std::log10(std::max(mag, 1e-12f)); + + magFull[i] = mag; freqsFull[i] = freq; } - // 4. Map to Custom Bins (Log Scale) - std::vector currentDb(m_freqsConst.size()); - for (size_t i = 0; i < m_freqsConst.size(); ++i) { - float val = getInterpolatedDb(freqsFull, dbFull, m_freqsConst[i]); + // --- Cepstral Smoothing / Trig Interpolation --- + if (m_cepstralStrength > 0.0f) { + // 1. Log Magnitude + for(int i=0; i Cepstrum + fftw_execute(m_cep_plan_inv); // Result in m_cep_out (scaled by N) + // 3. Hilbert on Cepstrum (Analytic Cepstrum) + double scale = 1.0 / m_frameSize; + m_cep_in[0][0] = m_cep_out[0][0] * scale; + m_cep_in[0][1] = m_cep_out[0][1] * scale; + + for(int i=1; i envelope = idealizeCurve(magFull); + + // Apply Strength (Mix) + for(size_t i=0; i currentDb(m_freqsConst.size()); + for (size_t i = 0; i < m_freqsConst.size(); ++i) { + double val = getInterpolatedDb(freqsFull, magFull, m_freqsConst[i]); + + // Convert to dB + val = 20.0 * std::log10(std::max(val, 1e-12)); + // 4b. Apply Expander (Before Smoothing) if (m_expandRatio != 1.0f) { val = (val - m_expandThreshold) * m_expandRatio + m_expandThreshold; } - if (val < -100.0f) val = -100.0f; + if (val < -100.0) val = -100.0; currentDb[i] = val; } @@ -174,7 +326,7 @@ Processor::Spectrum Processor::getSpectrum() { if (!m_history.empty()) { for (const auto& vec : m_history) { for (size_t i = 0; i < vec.size(); ++i) { - averagedDb[i] += vec[i]; + averagedDb[i] += static_cast(vec[i]); } } float factor = 1.0f / m_history.size(); @@ -183,5 +335,9 @@ Processor::Spectrum Processor::getSpectrum() { } } - return {m_freqsConst, averagedDb}; + // Convert freqs to float for return + std::vector freqsRet(m_freqsConst.size()); + for(size_t i=0; i(m_freqsConst[i]); + + return {freqsRet, averagedDb}; } \ No newline at end of file diff --git a/src/Processor.h b/src/Processor.h index 7a011d1..3d1e454 100644 --- a/src/Processor.h +++ b/src/Processor.h @@ -3,6 +3,7 @@ #pragma once #include #include +#include #include class Processor { @@ -18,8 +19,12 @@ public: void setSmoothing(int historyLength); void setExpander(float ratio, float thresholdDb); void setHPF(float cutoffFreq); + + // Cepstral Smoothing (Trig Interpolation) + void setCepstralParams(int granularity, int detail, float strength); - void pushData(const std::vector& data); + // Input is now double precision complex + void pushData(const std::vector>& data); struct Spectrum { std::vector freqs; @@ -32,20 +37,27 @@ private: int m_frameSize; int m_sampleRate; - float* m_in; - fftwf_complex* m_out; - fftwf_plan m_plan; - std::vector m_window; + // FFTW Resources for Main Spectrum (Double Precision) + fftw_complex* m_in; + fftw_complex* m_out; + fftw_plan m_plan; + std::vector m_window; + + // FFTW Resources for Cepstral Smoothing (Double Precision) + fftw_complex* m_cep_in; + fftw_complex* m_cep_out; + fftw_plan m_cep_plan_fwd; + fftw_plan m_cep_plan_inv; // Buffer for the current audio frame - std::vector m_buffer; + std::vector> m_buffer; // Mapping & Smoothing - std::vector m_customBins; - std::vector m_freqsConst; + std::vector m_customBins; + std::vector m_freqsConst; // Moving Average History - std::deque> m_history; + std::deque> m_history; size_t m_smoothingLength = 3; // Expander Settings @@ -55,5 +67,13 @@ private: // HPF Settings float m_hpfCutoff = 0.0f; - float getInterpolatedDb(const std::vector& freqs, const std::vector& db, float targetFreq); + // Cepstral Settings + int m_granularity = 33; + int m_detail = 50; + float m_cepstralStrength = 0.0f; + + double getInterpolatedDb(const std::vector& freqs, const std::vector& db, double targetFreq); + + // Internal helper for the Trig Interpolation logic + std::vector idealizeCurve(const std::vector& magSpectrum); }; \ No newline at end of file diff --git a/src/VisualizerWidget.cpp b/src/VisualizerWidget.cpp index 59a8eff..21d2f0a 100644 --- a/src/VisualizerWidget.cpp +++ b/src/VisualizerWidget.cpp @@ -33,7 +33,7 @@ void VisualizerWidget::setNumBins(int n) { m_channels.clear(); } -void VisualizerWidget::setParams(bool glass, bool focus, bool trails, bool albumColors, bool shadow, bool mirrored, float hue, float contrast, float brightness, float entropy) { +void VisualizerWidget::setParams(bool glass, bool focus, bool trails, bool albumColors, bool shadow, bool mirrored, float hue, float contrast, float brightness) { m_glass = glass; m_focus = focus; m_trailsEnabled = trails; @@ -43,7 +43,6 @@ void VisualizerWidget::setParams(bool glass, bool focus, bool trails, bool album m_hueFactor = hue; m_contrast = contrast; m_brightness = brightness; - m_entropyStrength = entropy; update(); } @@ -73,16 +72,6 @@ QColor VisualizerWidget::applyModifiers(QColor c) { return QColor::fromHsvF(c.hsvHueF(), s, v); } -float VisualizerWidget::calculateEntropy(const std::deque& history) { - if (history.size() < 2) return 0.0f; - float sum = std::accumulate(history.begin(), history.end(), 0.0f); - float mean = sum / history.size(); - float sqSum = 0.0f; - for (float v : history) sqSum += (v - mean) * (v - mean); - // Normalize: 10dB std dev is considered "Max Chaos" - return std::clamp(std::sqrt(sqSum / history.size()) / 10.0f, 0.0f, 1.0f); -} - void VisualizerWidget::updateData(const std::vector& data) { if (QApplication::applicationState() != Qt::ApplicationActive || !isVisible()) return; m_data = data; @@ -102,39 +91,29 @@ void VisualizerWidget::updateData(const std::vector& dat float rawVal = db[i]; float primaryVal = (i < primaryDb.size()) ? primaryDb[i] : rawVal; - // 1. Update History & Calculate Entropy - bin.history.push_back(rawVal); - if (bin.history.size() > 15) bin.history.pop_front(); - - float entropy = calculateEntropy(bin.history); - float order = 1.0f - entropy; + // 1. Calculate Responsiveness (Simplified Physics) + float responsiveness = 0.2f; - // 2. Calculate Responsiveness (The Physics Core) - float p = 3.0f * m_entropyStrength; - float responsiveness = 0.02f + (0.98f * std::pow(order, p)); - - // 3. Update Visual Bar Height (Mixed Signal) + // 2. Update Visual Bar Height (Mixed Signal) bin.visualDb = (bin.visualDb * (1.0f - responsiveness)) + (rawVal * responsiveness); - // 4. Update Primary Visual DB (Steady Signal for Pattern) - // Use a fixed, slower responsiveness for the pattern to keep it stable + // 3. Update Primary Visual DB (Steady Signal for Pattern) float patternResp = 0.1f; bin.primaryVisualDb = (bin.primaryVisualDb * (1.0f - patternResp)) + (primaryVal * patternResp); - // 5. Trail Physics + // 4. Trail Physics bin.trailLife = std::max(bin.trailLife - 0.02f, 0.0f); float flux = rawVal - bin.lastRawDb; bin.lastRawDb = rawVal; if (flux > 0) { - float impactMultiplier = 1.0f + (3.0f * order * m_entropyStrength); - float jumpTarget = bin.visualDb + (flux * impactMultiplier); + float jumpTarget = bin.visualDb + (flux * 1.5f); if (jumpTarget > bin.trailDb) { bin.trailDb = jumpTarget; bin.trailLife = 1.0f; - bin.trailThickness = 1.0f + (order * 4.0f); + bin.trailThickness = 2.0f; } } @@ -314,7 +293,7 @@ void VisualizerWidget::drawContent(QPainter& p, int w, int h) { v = std::clamp(v / globalMax, 0.0f, 1.0f); } - // 4. Calculate Segment Modifiers (Procedural Pattern with Competition) + // 4. Calculate Segment Modifiers (Procedural Pattern) std::vector brightMods(freqs.size() - 1, 0.0f); std::vector alphaMods(freqs.size() - 1, 0.0f); @@ -328,18 +307,8 @@ void VisualizerWidget::drawContent(QPainter& p, int w, int h) { bool leftDominant = (prev > next); float sharpness = std::min(curr - prev, curr - next); - // COMPETITION LOGIC (Controlled by Entropy) - // m_entropyStrength (0.0 to 3.0) - // Low Entropy = Smoother, Less Aggressive - // High Entropy = Sharper, More Faceted - - float entropyFactor = std::max(0.1f, m_entropyStrength); - - // Aggressive Contrast: Boost low sharpness - float peakIntensity = std::clamp(std::pow(sharpness * 10.0f * entropyFactor, 0.3f), 0.0f, 1.0f); - - // Dynamic Decay: - float decayBase = 0.65f - std::clamp(sharpness * 3.0f * entropyFactor, 0.0f, 0.35f); + float peakIntensity = std::clamp(std::pow(sharpness * 10.0f, 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) { int segIdx = (direction == -1) ? (i - dist) : (i + dist - 1); diff --git a/src/VisualizerWidget.h b/src/VisualizerWidget.h index 21395eb..6ea8096 100644 --- a/src/VisualizerWidget.h +++ b/src/VisualizerWidget.h @@ -13,7 +13,7 @@ class VisualizerWidget : public QWidget { public: VisualizerWidget(QWidget* parent = nullptr); void updateData(const std::vector& data); - void setParams(bool glass, bool focus, bool trails, bool albumColors, bool shadow, bool mirrored, float hue, float contrast, float brightness, float entropy); + void setParams(bool glass, bool focus, bool trails, bool albumColors, bool shadow, bool mirrored, float hue, float contrast, float brightness); void setAlbumPalette(const std::vector& palette); void setNumBins(int n); @@ -28,7 +28,6 @@ private: void drawContent(QPainter& p, int w, int h); struct BinState { - std::deque history; // For entropy calculation float visualDb = -100.0f; // Mixed (Height) float primaryVisualDb = -100.0f; // Primary (Pattern) float lastRawDb = -100.0f; // To calculate flux @@ -57,9 +56,7 @@ private: float m_hueFactor = 0.9f; float m_contrast = 1.0f; float m_brightness = 1.0f; - float m_entropyStrength = 1.0f; float getX(float freq); QColor applyModifiers(QColor c); - float calculateEntropy(const std::deque& history); }; \ No newline at end of file