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!

This commit is contained in:
pszsh 2026-01-28 22:40:20 -08:00
parent f662dcb989
commit 26ccd55d8c
11 changed files with 540 additions and 228 deletions

View File

@ -17,15 +17,15 @@ option(BUILD_IOS "Build for iOS" OFF)
find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets Multimedia OpenGLWidgets) 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) if(APPLE AND CMAKE_SYSTEM_PROCESSOR STREQUAL "arm64" AND NOT BUILD_IOS)
message(STATUS "Detected Apple Silicon Desktop. Using Homebrew FFTW3F.") message(STATUS "Detected Apple Silicon Desktop. Using Homebrew FFTW3 (Double).")
find_library(FFTW3_LIB NAMES fftw3f libfftw3f PATHS /opt/homebrew/lib NO_DEFAULT_PATH) 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) find_path(FFTW3_INCLUDE_DIR fftw3.h PATHS /opt/homebrew/include NO_DEFAULT_PATH)
if(NOT FFTW3_LIB OR NOT FFTW3_INCLUDE_DIR) 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() endif()
add_library(fftw3 STATIC IMPORTED) add_library(fftw3 STATIC IMPORTED)
@ -35,9 +35,9 @@ if(APPLE AND CMAKE_SYSTEM_PROCESSOR STREQUAL "arm64" AND NOT BUILD_IOS)
) )
else() 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_SSE OFF CACHE BOOL "Disable SSE" FORCE)
set(ENABLE_SSE2 OFF CACHE BOOL "Disable SSE2" FORCE) set(ENABLE_SSE2 OFF CACHE BOOL "Disable SSE2" FORCE)
set(ENABLE_AVX OFF CACHE BOOL "Disable AVX" FORCE) set(ENABLE_AVX OFF CACHE BOOL "Disable AVX" FORCE)
@ -68,7 +68,6 @@ else()
endif() endif()
# --- Loop Tempo Estimator --- # --- 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_TESTS OFF CACHE BOOL "Build tests" FORCE)
set(BUILD_VAMP_PLUGIN OFF CACHE BOOL "Build Vamp plugin" FORCE) set(BUILD_VAMP_PLUGIN OFF CACHE BOOL "Build Vamp plugin" FORCE)
add_subdirectory(libraries/loop-tempo-estimator) 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(IOS_CONTENTS_JSON "${IOS_ASSETS_PATH}/Contents.json")
set(ANDROID_RES_PATH "${CMAKE_CURRENT_SOURCE_DIR}/android/res/mipmap-mdpi/ic_launcher.png") 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) find_program(MAGICK_EXECUTABLE NAMES magick)
if(NOT MAGICK_EXECUTABLE) if(NOT MAGICK_EXECUTABLE)
message(WARNING "ImageMagick 'magick' not found. Icons will not be generated.") message(WARNING "ImageMagick 'magick' not found. Icons will not be generated.")
@ -115,6 +113,8 @@ set(PROJECT_SOURCES
src/CommonWidgets.cpp src/CommonWidgets.cpp
src/PlayerControls.cpp src/PlayerControls.cpp
src/MainWindow.cpp src/MainWindow.cpp
src/complex_block.cpp
src/trig_interpolation.cpp
) )
if(EXISTS "${ICON_SOURCE}") if(EXISTS "${ICON_SOURCE}")
@ -129,6 +129,8 @@ set(PROJECT_HEADERS
src/CommonWidgets.h src/CommonWidgets.h
src/PlayerControls.h src/PlayerControls.h
src/MainWindow.h src/MainWindow.h
src/complex_block.h
src/trig_interpolation.h
) )
qt_add_executable(YrCrystals MANUAL_FINALIZATION ${PROJECT_SOURCES} ${PROJECT_HEADERS}) qt_add_executable(YrCrystals MANUAL_FINALIZATION ${PROJECT_SOURCES} ${PROJECT_HEADERS})
@ -144,8 +146,8 @@ endif()
# --- Linking --- # --- Linking ---
if(TARGET fftw3f) if(TARGET fftw3)
set(FFTW_TARGET fftw3f) set(FFTW_TARGET fftw3)
target_include_directories(YrCrystals PRIVATE target_include_directories(YrCrystals PRIVATE
"${fftw3_source_SOURCE_DIR}/api" "${fftw3_source_SOURCE_DIR}/api"
"${fftw3_source_BINARY_DIR}" "${fftw3_source_BINARY_DIR}"

View File

@ -51,7 +51,7 @@ AudioEngine::AudioEngine(QObject* parent) : QObject(parent) {
// Configure Main: Expander + HPF + Moderate Smoothing // Configure Main: Expander + HPF + Moderate Smoothing
for(auto p : m_processors) { for(auto p : m_processors) {
p->setExpander(1.5f, -50.0f); p->setExpander(1.5f, -50.0f);
p->setHPF(80.0f); // Mid 2nd Order HPF (80Hz) p->setHPF(80.0f);
p->setSmoothing(3); p->setSmoothing(3);
} }
@ -63,10 +63,22 @@ AudioEngine::AudioEngine(QObject* parent) : QObject(parent) {
// Configure Transient: Aggressive expansion, light smoothing // Configure Transient: Aggressive expansion, light smoothing
for(auto p : m_transientProcessors) { for(auto p : m_transientProcessors) {
p->setExpander(2.5f, -40.0f); p->setExpander(2.5f, -40.0f);
p->setHPF(100.0f); // Clean up transients p->setHPF(100.0f);
p->setSmoothing(2); 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 = new QTimer(this);
m_processTimer->setInterval(16); m_processTimer->setInterval(16);
connect(m_processTimer, &QTimer::timeout, this, &AudioEngine::onProcessTimer); connect(m_processTimer, &QTimer::timeout, this, &AudioEngine::onProcessTimer);
@ -76,18 +88,33 @@ AudioEngine::~AudioEngine() {
stop(); stop();
for(auto p : m_processors) delete p; for(auto p : m_processors) delete p;
for(auto p : m_transientProcessors) delete p; for(auto p : m_transientProcessors) delete p;
for(auto p : m_deepProcessors) delete p;
if (m_fileSource) delete m_fileSource; if (m_fileSource) delete m_fileSource;
} }
void AudioEngine::setNumBins(int n) { void AudioEngine::setNumBins(int n) {
for(auto p : m_processors) p->setNumBins(n); for(auto p : m_processors) p->setNumBins(n);
for(auto p : m_transientProcessors) 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) { void AudioEngine::loadTrack(const QString& filePath) {
stop(); stop();
{
QMutexLocker locker(&m_dataMutex);
m_pcmData.clear(); m_pcmData.clear();
m_complexData.clear();
m_buffer.close(); m_buffer.close();
}
m_sampleRate = 48000; m_sampleRate = 48000;
@ -145,12 +172,15 @@ void AudioEngine::onBufferReady() {
qDebug() << "AudioEngine: Switching sample rate to" << m_sampleRate; qDebug() << "AudioEngine: Switching sample rate to" << m_sampleRate;
for(auto p : m_processors) p->setSampleRate(m_sampleRate); for(auto p : m_processors) p->setSampleRate(m_sampleRate);
for(auto p : m_transientProcessors) 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<int>(buffer.frameCount()); const int frames = static_cast<int>(buffer.frameCount());
const int channels = buffer.format().channelCount(); const int channels = buffer.format().channelCount();
auto sampleType = buffer.format().sampleFormat(); auto sampleType = buffer.format().sampleFormat();
QMutexLocker locker(&m_dataMutex);
if (sampleType == QAudioFormat::Int16) { if (sampleType == QAudioFormat::Int16) {
const int16_t* src = buffer.constData<int16_t>(); const int16_t* src = buffer.constData<int16_t>();
if (!src) return; if (!src) return;
@ -194,6 +224,8 @@ void AudioEngine::onBufferReady() {
} }
void AudioEngine::onFinished() { void AudioEngine::onFinished() {
QMutexLocker locker(&m_dataMutex);
if (m_pcmData.isEmpty()) { if (m_pcmData.isEmpty()) {
emit trackLoaded(false); emit trackLoaded(false);
return; return;
@ -219,6 +251,26 @@ void AudioEngine::onFinished() {
emit analysisReady(0.0f, 0.0f); emit analysisReady(0.0f, 0.0f);
} }
} }
// --- Block Hilbert Transform (Offline Processing) ---
if (totalFrames > 0) {
std::vector<double> inputL(totalFrames);
std::vector<double> inputR(totalFrames);
for (size_t i = 0; i < totalFrames; ++i) {
inputL[i] = static_cast<double>(rawFloats[i * 2]);
inputR[i] = static_cast<double>(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); m_buffer.setData(m_pcmData);
@ -284,6 +336,7 @@ void AudioEngine::stop() {
} }
void AudioEngine::seek(float position) { void AudioEngine::seek(float position) {
QMutexLocker locker(&m_dataMutex);
if (m_pcmData.isEmpty()) return; if (m_pcmData.isEmpty()) return;
qint64 pos = position * m_pcmData.size(); qint64 pos = position * m_pcmData.size();
pos -= pos % 8; pos -= pos % 8;
@ -300,25 +353,34 @@ void AudioEngine::setDspParams(int frameSize, int hopSize) {
// Transient: 1/4 size (Minimum 64) // Transient: 1/4 size (Minimum 64)
int transSize = std::max(64, frameSize / 4); int transSize = std::max(64, frameSize / 4);
for(auto p : m_transientProcessors) p->setFrameSize(transSize); 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() { void AudioEngine::onProcessTimer() {
if (!m_buffer.isOpen()) return; if (!m_buffer.isOpen()) return;
QMutexLocker locker(&m_dataMutex);
qint64 currentPos = m_buffer.pos(); qint64 currentPos = m_buffer.pos();
emit positionChanged((float)currentPos / m_pcmData.size()); emit positionChanged((float)currentPos / m_pcmData.size());
const float* samples = reinterpret_cast<const float*>(m_pcmData.constData());
qint64 sampleIdx = currentPos / sizeof(float); qint64 sampleIdx = currentPos / sizeof(float);
qint64 totalSamples = m_pcmData.size() / sizeof(float);
if (sampleIdx + m_frameSize * 2 >= totalSamples) return; if (sampleIdx + m_frameSize * 2 >= m_complexData.size()) return;
// Prepare data for Main Processors // Prepare data for Main Processors (Complex Double)
std::vector<float> ch0(m_frameSize), ch1(m_frameSize); std::vector<std::complex<double>> ch0(m_frameSize), ch1(m_frameSize);
for (int i = 0; i < m_frameSize; ++i) { for (int i = 0; i < m_frameSize; ++i) {
ch0[i] = samples[sampleIdx + i*2]; ch0[i] = m_complexData[sampleIdx + i*2];
ch1[i] = samples[sampleIdx + i*2 + 1]; ch1[i] = m_complexData[sampleIdx + i*2 + 1];
} }
m_processors[0]->pushData(ch0); m_processors[0]->pushData(ch0);
@ -326,7 +388,7 @@ void AudioEngine::onProcessTimer() {
// Prepare data for Transient Processors (Smaller window) // Prepare data for Transient Processors (Smaller window)
int transSize = std::max(64, m_frameSize / 4); int transSize = std::max(64, m_frameSize / 4);
std::vector<float> tCh0(transSize), tCh1(transSize); std::vector<std::complex<double>> tCh0(transSize), tCh1(transSize);
int offset = m_frameSize - transSize; int offset = m_frameSize - transSize;
for (int i = 0; i < transSize; ++i) { for (int i = 0; i < transSize; ++i) {
tCh0[i] = ch0[offset + i]; tCh0[i] = ch0[offset + i];
@ -336,6 +398,21 @@ void AudioEngine::onProcessTimer() {
m_transientProcessors[0]->pushData(tCh0); m_transientProcessors[0]->pushData(tCh0);
m_transientProcessors[1]->pushData(tCh1); 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<FrameData> results; std::vector<FrameData> results;
// Final Compressor Settings // Final Compressor Settings
@ -345,14 +422,16 @@ void AudioEngine::onProcessTimer() {
for (size_t i = 0; i < m_processors.size(); ++i) { for (size_t i = 0; i < m_processors.size(); ++i) {
auto specMain = m_processors[i]->getSpectrum(); auto specMain = m_processors[i]->getSpectrum();
auto specTrans = m_transientProcessors[i]->getSpectrum(); auto specTrans = m_transientProcessors[i]->getSpectrum();
auto specDeep = m_deepProcessors[i]->getSpectrum();
// Capture Primary DB (Steady State) for Crystal Pattern // Capture Primary DB (Steady State) for Crystal Pattern
std::vector<float> primaryDb = specMain.db; std::vector<float> primaryDb = specMain.db;
// Mix: Overlay the expanded transient peaks onto the main spectrum // Mix: Overlay Main + Transient + Deep
if (specMain.db.size() == specTrans.db.size()) { if (specMain.db.size() == specTrans.db.size() && specMain.db.size() == specDeep.db.size()) {
for(size_t b = 0; b < specMain.db.size(); ++b) { 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) // Final Compressor (Hard Knee)
if (val > compThreshold) { if (val > compThreshold) {

View File

@ -7,8 +7,11 @@
#include <QBuffer> #include <QBuffer>
#include <QFile> #include <QFile>
#include <QTimer> #include <QTimer>
#include <QMutex>
#include <vector> #include <vector>
#include <complex>
#include "Processor.h" #include "Processor.h"
#include "complex_block.h"
class AudioEngine : public QObject { class AudioEngine : public QObject {
Q_OBJECT Q_OBJECT
@ -18,7 +21,7 @@ public:
struct FrameData { struct FrameData {
std::vector<float> freqs; std::vector<float> freqs;
std::vector<float> db; // Mixed (Primary + Transient) -> For Bar Height std::vector<float> db; // Mixed (Primary + Transient + Deep)
std::vector<float> primaryDb; // Primary Only -> For Crystal Pattern std::vector<float> primaryDb; // Primary Only -> For Crystal Pattern
}; };
@ -31,6 +34,9 @@ public slots:
void setDspParams(int frameSize, int hopSize); void setDspParams(int frameSize, int hopSize);
void setNumBins(int n); void setNumBins(int n);
// Cepstral/Smoothing Controls
void setSmoothingParams(int granularity, int detail, float strength);
signals: signals:
void playbackFinished(); void playbackFinished();
void positionChanged(float pos); void positionChanged(float pos);
@ -47,7 +53,11 @@ private slots:
private: private:
QAudioSink* m_sink = nullptr; QAudioSink* m_sink = nullptr;
QBuffer m_buffer; 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<std::complex<double>> m_complexData;
QAudioDecoder* m_decoder = nullptr; QAudioDecoder* m_decoder = nullptr;
QFile* m_fileSource = nullptr; QFile* m_fileSource = nullptr;
@ -55,6 +65,7 @@ private:
std::vector<Processor*> m_processors; // Main (Steady) std::vector<Processor*> m_processors; // Main (Steady)
std::vector<Processor*> m_transientProcessors; // Secondary (Fast/Transient) std::vector<Processor*> m_transientProcessors; // Secondary (Fast/Transient)
std::vector<Processor*> m_deepProcessors; // Tertiary (Deep/Bass)
int m_frameSize = 4096; int m_frameSize = 4096;
int m_hopSize = 1024; int m_hopSize = 1024;

View File

@ -48,6 +48,12 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
connect(m_engine, &AudioEngine::spectrumReady, m_playerPage->visualizer(), &VisualizerWidget::updateData); connect(m_engine, &AudioEngine::spectrumReady, m_playerPage->visualizer(), &VisualizerWidget::updateData);
connect(m_engine, &AudioEngine::analysisReady, this, &MainWindow::onAnalysisReady); 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(); audioThread->start();
} }
@ -82,8 +88,13 @@ void MainWindow::initUi() {
connect(set, &SettingsWidget::dspParamsChanged, this, &MainWindow::onDspChanged); connect(set, &SettingsWidget::dspParamsChanged, this, &MainWindow::onDspChanged);
connect(set, &SettingsWidget::binsChanged, this, &MainWindow::onBinsChanged); 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::paramsChanged, this, &MainWindow::saveSettings);
connect(set, &SettingsWidget::binsChanged, 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); connect(m_playerPage, &PlayerPage::toggleFullScreen, this, &MainWindow::onToggleFullScreen);
@ -241,9 +252,14 @@ void MainWindow::loadSettings() {
bool mirrored = root["mirrored"].toBool(false); bool mirrored = root["mirrored"].toBool(false);
int bins = root["bins"].toInt(26); int bins = root["bins"].toInt(26);
float brightness = root["brightness"].toDouble(1.0); float brightness = root["brightness"].toDouble(1.0);
float entropy = root["entropy"].toDouble(1.0);
m_playerPage->settings()->setParams(glass, focus, trails, albumColors, shadow, mirrored, bins, brightness, entropy); // 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, granularity, detail, strength, bpmScaleIndex);
} }
} }
@ -261,7 +277,12 @@ void MainWindow::saveSettings() {
root["mirrored"] = s->isMirrored(); root["mirrored"] = s->isMirrored();
root["bins"] = s->getBins(); root["bins"] = s->getBins();
root["brightness"] = s->getBrightness(); 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")); QFile f(QDir(m_settingsDir).filePath(".yrcrystals.json"));
if (f.open(QIODevice::WriteOnly)) { if (f.open(QIODevice::WriteOnly)) {
@ -299,32 +320,35 @@ void MainWindow::onTrackLoaded(bool success) {
} }
void MainWindow::onAnalysisReady(float bpm, float confidence) { 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: // Feedback Mechanism:
// Adjust Entropy based on BPM. // Adjust Smoothing Strength based on effective BPM.
// High BPM (Fast/Punchy) -> Lower Entropy Slider (0.5 - 0.8) to allow transients. // High BPM (Fast/Punchy) -> Lower Strength (More Raw).
// Low BPM (Slow/Ambient) -> Higher Entropy Slider (1.0 - 1.5) to smooth noise. // Low BPM (Slow/Ambient) -> Higher Strength (Smoother).
// Default (No BPM) -> 1.0
float targetEntropy = 1.0f; float targetStrength = 0.0f;
if (bpm > 0.0f) { // Map 60..180 BPM to 0.8..0.0 Strength
// Map 60..180 BPM to 1.5..0.5 Entropy float normalized = std::clamp((effectiveBpm - 60.0f) / 120.0f, 0.0f, 1.0f);
// Formula: 1.5 - ((bpm - 60) / 120) targetStrength = 0.8f * (1.0f - normalized);
// 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) qDebug() << "Feedback: BPM" << m_lastBpm << "Scale" << scale << "Effective" << effectiveBpm << "-> Strength" << targetStrength;
// Update Settings Widget (which updates Visualizer/Engine)
SettingsWidget* s = m_playerPage->settings(); SettingsWidget* s = m_playerPage->settings();
s->setParams( s->setParams(
s->isGlass(), s->isFocus(), s->isTrails(), s->isAlbumColors(), s->isGlass(), s->isFocus(), s->isTrails(), s->isAlbumColors(),
s->isShadow(), s->isMirrored(), s->getBins(), s->getBrightness(), s->isShadow(), s->isMirrored(), s->getBins(), s->getBrightness(),
targetEntropy s->getGranularity(), s->getDetail(), targetStrength, s->getBpmScaleIndex()
); );
} }

View File

@ -28,6 +28,7 @@ private slots:
void onTrackLoaded(bool success); void onTrackLoaded(bool success);
void onTrackDoubleClicked(QListWidgetItem* item); void onTrackDoubleClicked(QListWidgetItem* item);
void onAnalysisReady(float bpm, float confidence); void onAnalysisReady(float bpm, float confidence);
void updateSmoothing(); // New slot for BPM feedback logic
void play(); void play();
void pause(); void pause();
void nextTrack(); void nextTrack();
@ -59,4 +60,6 @@ private:
enum class PendingAction { None, File, Folder }; enum class PendingAction { None, File, Folder };
PendingAction m_pendingAction = PendingAction::None; PendingAction m_pendingAction = PendingAction::None;
QString m_settingsDir; QString m_settingsDir;
float m_lastBpm = 0.0f; // Store last detected BPM
}; };

View File

@ -76,7 +76,7 @@ SettingsWidget::SettingsWidget(QWidget* parent) : QWidget(parent) {
QVBoxLayout* layout = new QVBoxLayout(this); QVBoxLayout* layout = new QVBoxLayout(this);
layout->setContentsMargins(15, 15, 15, 15); layout->setContentsMargins(15, 15, 15, 15);
layout->setSpacing(15); layout->setSpacing(10);
QHBoxLayout* header = new QHBoxLayout(); QHBoxLayout* header = new QHBoxLayout();
QLabel* title = new QLabel("Settings", this); QLabel* title = new QLabel("Settings", this);
@ -102,73 +102,71 @@ SettingsWidget::SettingsWidget(QWidget* parent) : QWidget(parent) {
return cb; return cb;
}; };
// Defaults: Only Glass checked // Updated Defaults based on user request
m_checkGlass = createCheck("Glass", true, 0, 0); m_checkGlass = createCheck("Glass", true, 0, 0);
m_checkFocus = createCheck("Focus", false, 0, 1); m_checkFocus = createCheck("Focus", true, 0, 1);
m_checkTrails = createCheck("Trails", false, 1, 0); m_checkTrails = createCheck("Trails", true, 1, 0);
m_checkAlbumColors = createCheck("Album Colors", false, 1, 1); m_checkAlbumColors = createCheck("Album Colors", false, 1, 1);
m_checkShadow = createCheck("Shadow", false, 2, 0); m_checkShadow = createCheck("Shadow", false, 2, 0);
m_checkMirrored = createCheck("Mirrored", false, 2, 1); m_checkMirrored = createCheck("Mirrored", true, 2, 1);
layout->addLayout(grid); layout->addLayout(grid);
// Bins Slider // Helper for sliders
QHBoxLayout* binsLayout = new QHBoxLayout(); auto addSlider = [&](const QString& label, int min, int max, int val, QSlider*& slider, QLabel*& lbl) {
m_lblBins = new QLabel("Bins: 26", this); QHBoxLayout* h = new QHBoxLayout();
m_lblBins->setStyleSheet("color: white; font-weight: bold; border: none; background: transparent; min-width: 80px;"); 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); // Updated Slider Defaults
m_sliderBins->setRange(10, 64); addSlider("Bins: 21", 10, 64, 21, m_sliderBins, m_lblBins);
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; }");
connect(m_sliderBins, &QSlider::valueChanged, this, &SettingsWidget::onBinsChanged); connect(m_sliderBins, &QSlider::valueChanged, this, &SettingsWidget::onBinsChanged);
binsLayout->addWidget(m_lblBins); addSlider("Bright: 66%", 10, 200, 66, m_sliderBrightness, m_lblBrightness);
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; }");
connect(m_sliderBrightness, &QSlider::valueChanged, this, &SettingsWidget::onBrightnessChanged); connect(m_sliderBrightness, &QSlider::valueChanged, this, &SettingsWidget::onBrightnessChanged);
brightLayout->addWidget(m_lblBrightness); addSlider("Granularity", 0, 100, 95, m_sliderGranularity, m_lblGranularity);
brightLayout->addWidget(m_sliderBrightness); connect(m_sliderGranularity, &QSlider::valueChanged, this, &SettingsWidget::onSmoothingChanged);
layout->addLayout(brightLayout);
// Entropy Slider addSlider("Detail", 0, 100, 5, m_sliderDetail, m_lblDetail);
QHBoxLayout* entropyLayout = new QHBoxLayout(); connect(m_sliderDetail, &QSlider::valueChanged, this, &SettingsWidget::onSmoothingChanged);
m_lblEntropy = new QLabel("Entropy: 1.0", this);
m_lblEntropy->setStyleSheet("color: white; font-weight: bold; border: none; background: transparent; min-width: 80px;");
m_sliderEntropy = new QSlider(Qt::Horizontal, this); addSlider("Strength", 0, 100, 95, m_sliderStrength, m_lblStrength);
m_sliderEntropy->setRange(0, 300); // 0.0 to 3.0 connect(m_sliderStrength, &QSlider::valueChanged, this, &SettingsWidget::onSmoothingChanged);
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);
entropyLayout->addWidget(m_lblEntropy); // BPM Scale Selector
entropyLayout->addWidget(m_sliderEntropy); QHBoxLayout* bpmLayout = new QHBoxLayout();
layout->addLayout(entropyLayout); 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<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();
m_padDsp = new XYPad("DSP", this); m_padDsp = new XYPad("DSP", this);
m_padDsp->setFormatter([](float x, float y) { 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 power = 6 + (int)(x * 7.0f + 0.5f);
int fft = std::pow(2, power); int fft = std::pow(2, power);
int hop = 64 + y * (8192 - 64); int hop = 64 + y * (8192 - 64);
return QString("FFT: %1\nHop: %2").arg(fft).arg(hop); return QString("FFT: %1\nHop: %2").arg(fft).arg(hop);
}); });
// Default to FFT 8192 (x=1.0), Hop 64 (y=0.0)
// Default to ~4096 FFT (x approx 0.857) and reasonable hop m_padDsp->setValues(1.0f, 0.0f);
m_padDsp->setValues(0.857f, 0.118f);
connect(m_padDsp, &XYPad::valuesChanged, this, &SettingsWidget::onDspPadChanged); connect(m_padDsp, &XYPad::valuesChanged, this, &SettingsWidget::onDspPadChanged);
padsLayout->addWidget(m_padDsp); padsLayout->addWidget(m_padDsp);
@ -178,14 +176,15 @@ SettingsWidget::SettingsWidget(QWidget* parent) : QWidget(parent) {
float cont = 0.1f + y * 2.9f; float cont = 0.1f + y * 2.9f;
return QString("Hue: %1\nCont: %2").arg(hue, 0, 'f', 2).arg(cont, 0, 'f', 2); 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); connect(m_padColor, &XYPad::valuesChanged, this, &SettingsWidget::onColorPadChanged);
padsLayout->addWidget(m_padColor); padsLayout->addWidget(m_padColor);
layout->addLayout(padsLayout); 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); bool oldState = blockSignals(true);
m_checkGlass->setChecked(glass); m_checkGlass->setChecked(glass);
m_checkFocus->setChecked(focus); 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_lblBins->setText(QString("Bins: %1").arg(bins));
m_brightness = brightness; m_brightness = brightness;
int brightVal = static_cast<int>(brightness * 100.0f); m_sliderBrightness->setValue(static_cast<int>(brightness * 100.0f));
m_sliderBrightness->setValue(brightVal); m_lblBrightness->setText(QString("Bright: %1%").arg(static_cast<int>(brightness * 100.0f)));
m_lblBrightness->setText(QString("Bright: %1%").arg(brightVal));
m_entropy = entropy; m_granularity = granularity;
int entVal = static_cast<int>(entropy * 100.0f); m_sliderGranularity->setValue(granularity);
m_sliderEntropy->setValue(entVal);
m_lblEntropy->setText(QString("Entropy: %1").arg(entropy, 0, 'f', 1)); m_detail = detail;
m_sliderDetail->setValue(detail);
m_strength = strength;
m_sliderStrength->setValue(static_cast<int>(strength * 100.0f));
if (bpmScaleIndex >= 0 && bpmScaleIndex < m_comboBpmScale->count()) {
m_comboBpmScale->setCurrentIndex(bpmScaleIndex);
}
blockSignals(oldState); blockSignals(oldState);
@ -223,12 +229,32 @@ void SettingsWidget::emitParams() {
m_hue, m_hue,
m_contrast, m_contrast,
m_brightness, 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) { void SettingsWidget::onDspPadChanged(float x, float y) {
// Range: 2^6 (64) to 2^13 (8192)
int power = 6 + (int)(x * 7.0f + 0.5f); int power = 6 + (int)(x * 7.0f + 0.5f);
m_fft = std::pow(2, power); m_fft = std::pow(2, power);
m_hop = 64 + y * (8192 - 64); m_hop = 64 + y * (8192 - 64);
@ -253,9 +279,10 @@ void SettingsWidget::onBrightnessChanged(int val) {
emitParams(); emitParams();
} }
void SettingsWidget::onEntropyChanged(int val) { void SettingsWidget::onSmoothingChanged(int val) {
m_entropy = val / 100.0f; m_granularity = m_sliderGranularity->value();
m_lblEntropy->setText(QString("Entropy: %1").arg(m_entropy, 0, 'f', 1)); m_detail = m_sliderDetail->value();
m_strength = m_sliderStrength->value() / 100.0f;
emitParams(); emitParams();
} }

View File

@ -6,6 +6,7 @@
#include <QPushButton> #include <QPushButton>
#include <QCheckBox> #include <QCheckBox>
#include <QLabel> #include <QLabel>
#include <QComboBox>
#include "VisualizerWidget.h" #include "VisualizerWidget.h"
#include "CommonWidgets.h" #include "CommonWidgets.h"
@ -46,22 +47,33 @@ public:
bool isMirrored() const { return m_checkMirrored->isChecked(); } bool isMirrored() const { return m_checkMirrored->isChecked(); }
int getBins() const { return m_sliderBins->value(); } int getBins() const { return m_sliderBins->value(); }
float getBrightness() const { return m_brightness; } float getBrightness() const { return m_brightness; }
float getEntropy() const { return m_entropy; }
void setParams(bool glass, bool focus, bool trails, bool albumColors, bool shadow, bool mirrored, int bins, float brightness, float 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, int granularity, int detail, float strength, int bpmScaleIndex);
signals: 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 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:
void emitParams(); void emitParams();
void onDspPadChanged(float x, float y); void onDspPadChanged(float x, float y);
void onColorPadChanged(float x, float y); void onColorPadChanged(float x, float y);
void onBinsChanged(int val); void onBinsChanged(int val);
void onBrightnessChanged(int val); void onBrightnessChanged(int val);
void onEntropyChanged(int val); void onSmoothingChanged(int val);
void onBpmScaleChanged(int index);
private: private:
QCheckBox* m_checkGlass; QCheckBox* m_checkGlass;
QCheckBox* m_checkFocus; QCheckBox* m_checkFocus;
@ -75,12 +87,24 @@ private:
QLabel* m_lblBins; QLabel* m_lblBins;
QSlider* m_sliderBrightness; QSlider* m_sliderBrightness;
QLabel* m_lblBrightness; 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_hue = 0.9f;
float m_contrast = 1.0f; float m_contrast = 1.0f;
float m_brightness = 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_fft = 4096;
int m_hop = 1024; int m_hop = 1024;
int m_bins = 26; int m_bins = 26;

View File

@ -4,21 +4,28 @@
#include <cmath> #include <cmath>
#include <algorithm> #include <algorithm>
#include <cstring> #include <cstring>
#include <numeric>
const double PI = 3.14159265358979323846; const double PI = 3.14159265358979323846;
Processor::Processor(int frameSize, int sampleRate) Processor::Processor(int frameSize, int sampleRate)
: m_frameSize(0), m_sampleRate(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); setFrameSize(frameSize);
setNumBins(26); setNumBins(26);
} }
Processor::~Processor() { Processor::~Processor() {
if (m_plan) fftwf_destroy_plan(m_plan); if (m_plan) fftw_destroy_plan(m_plan);
if (m_in) fftwf_free(m_in); if (m_in) fftw_free(m_in);
if (m_out) fftwf_free(m_out); 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) { void Processor::setSampleRate(int rate) {
@ -41,55 +48,73 @@ void Processor::setHPF(float cutoffFreq) {
m_hpfCutoff = 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) { void Processor::setNumBins(int n) {
m_customBins.clear(); m_customBins.clear();
m_freqsConst.clear(); m_freqsConst.clear();
m_history.clear(); m_history.clear();
float minFreq = 40.0f; double minFreq = 40.0;
float maxFreq = 11000.0f; double maxFreq = 11000.0;
for (int i = 0; i <= n; ++i) { 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_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) { 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) { void Processor::setFrameSize(int size) {
if (m_frameSize == size) return; if (m_frameSize == size) return;
if (m_plan) fftwf_destroy_plan(m_plan); if (m_plan) fftw_destroy_plan(m_plan);
if (m_in) fftwf_free(m_in); if (m_in) fftw_free(m_in);
if (m_out) fftwf_free(m_out); 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_frameSize = size;
m_in = (float*)fftwf_malloc(sizeof(float) * m_frameSize); // Main FFT (Complex -> Complex)
m_out = (fftwf_complex*)fftwf_malloc(sizeof(fftwf_complex) * (m_frameSize / 2 + 1)); m_in = (fftw_complex*)fftw_malloc(sizeof(fftw_complex) * m_frameSize);
m_plan = fftwf_plan_dft_r2c_1d(m_frameSize, m_in, m_out, FFTW_ESTIMATE); 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); m_window.resize(m_frameSize);
// Blackman-Harris window // Blackman-Harris window
for (int i = 0; i < m_frameSize; ++i) { for (int i = 0; i < m_frameSize; ++i) {
float a0 = 0.35875f; double a0 = 0.35875;
float a1 = 0.48829f; double a1 = 0.48829;
float a2 = 0.14128f; double a2 = 0.14128;
float a3 = 0.01168f; double a3 = 0.01168;
m_window[i] = a0 - a1 * std::cos(2.0f * PI * i / (m_frameSize - 1)) m_window[i] = a0 - a1 * std::cos(2.0 * PI * i / (m_frameSize - 1))
+ a2 * std::cos(4.0f * PI * i / (m_frameSize - 1)) + a2 * std::cos(4.0 * PI * i / (m_frameSize - 1))
- a3 * std::cos(6.0f * 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(); m_history.clear();
} }
void Processor::pushData(const std::vector<float>& data) { void Processor::pushData(const std::vector<std::complex<double>>& data) {
if (data.size() == m_frameSize) { if (data.size() == m_frameSize) {
std::copy(data.begin(), data.end(), m_buffer.begin()); std::copy(data.begin(), data.end(), m_buffer.begin());
} else if (data.size() < m_frameSize) { } else if (data.size() < m_frameSize) {
@ -98,7 +123,7 @@ void Processor::pushData(const std::vector<float>& data) {
} }
} }
float Processor::getInterpolatedDb(const std::vector<float>& freqs, const std::vector<float>& db, float targetFreq) { double Processor::getInterpolatedDb(const std::vector<double>& freqs, const std::vector<double>& db, double targetFreq) {
auto it = std::lower_bound(freqs.begin(), freqs.end(), targetFreq); auto it = std::lower_bound(freqs.begin(), freqs.end(), targetFreq);
if (it == freqs.begin()) return db[0]; if (it == freqs.begin()) return db[0];
if (it == freqs.end()) return db.back(); if (it == freqs.end()) return db.back();
@ -106,61 +131,188 @@ float Processor::getInterpolatedDb(const std::vector<float>& freqs, const std::v
size_t idxUpper = std::distance(freqs.begin(), it); size_t idxUpper = std::distance(freqs.begin(), it);
size_t idxLower = idxUpper - 1; size_t idxLower = idxUpper - 1;
float f0 = freqs[idxLower]; double f0 = freqs[idxLower];
float f1 = freqs[idxUpper]; double f1 = freqs[idxUpper];
float d0 = db[idxLower]; double d0 = db[idxLower];
float d1 = db[idxUpper]; double d1 = db[idxUpper];
float t = (targetFreq - f0) / (f1 - f0); double t = (targetFreq - f0) / (f1 - f0);
return d0 + t * (d1 - d0); return d0 + t * (d1 - d0);
} }
// Implementation of the Trig Interpolation "Idealize Curve" logic
std::vector<double> Processor::idealizeCurve(const std::vector<double>& magSpectrum) {
size_t n = magSpectrum.size();
std::vector<double> current_curve = magSpectrum;
// Map slider values to algorithm parameters
int num_bins = 4 + static_cast<int>(m_granularity / 100.0 * 60.0);
int iterations = 1 + static_cast<int>(m_detail / 100.0 * 4.0);
for (int iter = 0; iter < iterations; ++iter) {
std::vector<double> next_curve(n, 0.0);
std::vector<bool> is_point_set(n, false);
// 1. Binning
std::vector<size_t> bin_boundaries;
double log_n = std::log((double)n);
for (int i = 0; i <= num_bins; ++i) {
double ratio = static_cast<double>(i) / num_bins;
size_t boundary = static_cast<size_t>(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<std::pair<size_t, double>> 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<double>(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<n; ++i) if(is_point_set[i]) { last_set_point = i; break; }
}
for (size_t i = 1; i < n; ++i) {
if (is_point_set[i]) {
if (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<double>(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() { Processor::Spectrum Processor::getSpectrum() {
// 1. Windowing // 1. Windowing (Complex)
for (int i = 0; i < m_frameSize; ++i) { 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 // 2. FFT
fftwf_execute(m_plan); fftw_execute(m_plan);
// 3. Compute Magnitude (dB) // 3. Compute Magnitude
int bins = m_frameSize / 2 + 1; int bins = m_frameSize / 2 + 1;
std::vector<float> freqsFull(bins); std::vector<double> freqsFull(bins);
std::vector<float> dbFull(bins); std::vector<double> magFull(bins);
for (int i = 0; i < bins; ++i) { for (int i = 0; i < bins; ++i) {
float re = m_out[i][0]; double re = m_out[i][0];
float im = m_out[i][1]; double im = m_out[i][1];
float mag = 2.0f * std::sqrt(re*re + im*im) / m_frameSize; 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 // HPF
// Gain = 1 / sqrt(1 + (fc/f)^4)
if (m_hpfCutoff > 0.0f && freq > 0.0f) { if (m_hpfCutoff > 0.0f && freq > 0.0f) {
float ratio = m_hpfCutoff / freq; double ratio = m_hpfCutoff / freq;
float gain = 1.0f / std::sqrt(1.0f + (ratio * ratio * ratio * ratio)); double gain = 1.0 / std::sqrt(1.0 + (ratio * ratio * ratio * ratio));
mag *= gain; mag *= gain;
} else if (freq == 0.0f) { } else if (freq == 0.0) {
mag = 0.0f; mag = 0.0;
} }
dbFull[i] = 20.0f * std::log10(std::max(mag, 1e-12f)); magFull[i] = mag;
freqsFull[i] = freq; freqsFull[i] = freq;
} }
// 4. Map to Custom Bins (Log Scale) // --- Cepstral Smoothing / Trig Interpolation ---
std::vector<float> currentDb(m_freqsConst.size()); if (m_cepstralStrength > 0.0f) {
// 1. Log Magnitude
for(int i=0; i<m_frameSize; ++i) {
// Mirror for symmetry to get real cepstrum
int idx = (i <= m_frameSize/2) ? i : (m_frameSize - i);
double val = std::max(1e-9, magFull[idx]);
m_cep_in[i][0] = std::log(val);
m_cep_in[i][1] = 0.0;
}
// 2. IFFT -> 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<m_frameSize; ++i) {
if (i < m_frameSize/2) {
// Positive quefrencies * 2
m_cep_in[i][0] = m_cep_out[i][0] * scale * 2.0;
m_cep_in[i][1] = m_cep_out[i][1] * scale * 2.0;
} else {
// Negative quefrencies = 0
m_cep_in[i][0] = 0.0;
m_cep_in[i][1] = 0.0;
}
}
// 4. Idealize Curve (Smoothing)
std::vector<double> envelope = idealizeCurve(magFull);
// Apply Strength (Mix)
for(size_t i=0; i<magFull.size(); ++i) {
magFull[i] = magFull[i] * (1.0 - m_cepstralStrength) + envelope[i] * m_cepstralStrength;
}
}
// 4. Map to Custom Bins (Log Scale) & Convert to dB
std::vector<double> currentDb(m_freqsConst.size());
for (size_t i = 0; i < m_freqsConst.size(); ++i) { for (size_t i = 0; i < m_freqsConst.size(); ++i) {
float val = getInterpolatedDb(freqsFull, dbFull, m_freqsConst[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) // 4b. Apply Expander (Before Smoothing)
if (m_expandRatio != 1.0f) { if (m_expandRatio != 1.0f) {
val = (val - m_expandThreshold) * m_expandRatio + m_expandThreshold; 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; currentDb[i] = val;
} }
@ -174,7 +326,7 @@ Processor::Spectrum Processor::getSpectrum() {
if (!m_history.empty()) { if (!m_history.empty()) {
for (const auto& vec : m_history) { for (const auto& vec : m_history) {
for (size_t i = 0; i < vec.size(); ++i) { for (size_t i = 0; i < vec.size(); ++i) {
averagedDb[i] += vec[i]; averagedDb[i] += static_cast<float>(vec[i]);
} }
} }
float factor = 1.0f / m_history.size(); 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<float> freqsRet(m_freqsConst.size());
for(size_t i=0; i<m_freqsConst.size(); ++i) freqsRet[i] = static_cast<float>(m_freqsConst[i]);
return {freqsRet, averagedDb};
} }

View File

@ -3,6 +3,7 @@
#pragma once #pragma once
#include <vector> #include <vector>
#include <deque> #include <deque>
#include <complex>
#include <fftw3.h> #include <fftw3.h>
class Processor { class Processor {
@ -19,7 +20,11 @@ public:
void setExpander(float ratio, float thresholdDb); void setExpander(float ratio, float thresholdDb);
void setHPF(float cutoffFreq); void setHPF(float cutoffFreq);
void pushData(const std::vector<float>& data); // Cepstral Smoothing (Trig Interpolation)
void setCepstralParams(int granularity, int detail, float strength);
// Input is now double precision complex
void pushData(const std::vector<std::complex<double>>& data);
struct Spectrum { struct Spectrum {
std::vector<float> freqs; std::vector<float> freqs;
@ -32,20 +37,27 @@ private:
int m_frameSize; int m_frameSize;
int m_sampleRate; int m_sampleRate;
float* m_in; // FFTW Resources for Main Spectrum (Double Precision)
fftwf_complex* m_out; fftw_complex* m_in;
fftwf_plan m_plan; fftw_complex* m_out;
std::vector<float> m_window; fftw_plan m_plan;
std::vector<double> 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 // Buffer for the current audio frame
std::vector<float> m_buffer; std::vector<std::complex<double>> m_buffer;
// Mapping & Smoothing // Mapping & Smoothing
std::vector<float> m_customBins; std::vector<double> m_customBins;
std::vector<float> m_freqsConst; std::vector<double> m_freqsConst;
// Moving Average History // Moving Average History
std::deque<std::vector<float>> m_history; std::deque<std::vector<double>> m_history;
size_t m_smoothingLength = 3; size_t m_smoothingLength = 3;
// Expander Settings // Expander Settings
@ -55,5 +67,13 @@ private:
// HPF Settings // HPF Settings
float m_hpfCutoff = 0.0f; float m_hpfCutoff = 0.0f;
float getInterpolatedDb(const std::vector<float>& freqs, const std::vector<float>& db, float targetFreq); // Cepstral Settings
int m_granularity = 33;
int m_detail = 50;
float m_cepstralStrength = 0.0f;
double getInterpolatedDb(const std::vector<double>& freqs, const std::vector<double>& db, double targetFreq);
// Internal helper for the Trig Interpolation logic
std::vector<double> idealizeCurve(const std::vector<double>& magSpectrum);
}; };

View File

@ -33,7 +33,7 @@ void VisualizerWidget::setNumBins(int n) {
m_channels.clear(); 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_glass = glass;
m_focus = focus; m_focus = focus;
m_trailsEnabled = trails; m_trailsEnabled = trails;
@ -43,7 +43,6 @@ void VisualizerWidget::setParams(bool glass, bool focus, bool trails, bool album
m_hueFactor = hue; m_hueFactor = hue;
m_contrast = contrast; m_contrast = contrast;
m_brightness = brightness; m_brightness = brightness;
m_entropyStrength = entropy;
update(); update();
} }
@ -73,16 +72,6 @@ QColor VisualizerWidget::applyModifiers(QColor c) {
return QColor::fromHsvF(c.hsvHueF(), s, v); return QColor::fromHsvF(c.hsvHueF(), s, v);
} }
float VisualizerWidget::calculateEntropy(const std::deque<float>& 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<AudioEngine::FrameData>& data) { void VisualizerWidget::updateData(const std::vector<AudioEngine::FrameData>& data) {
if (QApplication::applicationState() != Qt::ApplicationActive || !isVisible()) return; if (QApplication::applicationState() != Qt::ApplicationActive || !isVisible()) return;
m_data = data; m_data = data;
@ -102,39 +91,29 @@ void VisualizerWidget::updateData(const std::vector<AudioEngine::FrameData>& dat
float rawVal = db[i]; float rawVal = db[i];
float primaryVal = (i < primaryDb.size()) ? primaryDb[i] : rawVal; float primaryVal = (i < primaryDb.size()) ? primaryDb[i] : rawVal;
// 1. Update History & Calculate Entropy // 1. Calculate Responsiveness (Simplified Physics)
bin.history.push_back(rawVal); float responsiveness = 0.2f;
if (bin.history.size() > 15) bin.history.pop_front();
float entropy = calculateEntropy(bin.history); // 2. Update Visual Bar Height (Mixed Signal)
float order = 1.0f - entropy;
// 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)
bin.visualDb = (bin.visualDb * (1.0f - responsiveness)) + (rawVal * responsiveness); bin.visualDb = (bin.visualDb * (1.0f - responsiveness)) + (rawVal * responsiveness);
// 4. Update Primary Visual DB (Steady Signal for Pattern) // 3. Update Primary Visual DB (Steady Signal for Pattern)
// Use a fixed, slower responsiveness for the pattern to keep it stable
float patternResp = 0.1f; float patternResp = 0.1f;
bin.primaryVisualDb = (bin.primaryVisualDb * (1.0f - patternResp)) + (primaryVal * patternResp); 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); bin.trailLife = std::max(bin.trailLife - 0.02f, 0.0f);
float flux = rawVal - bin.lastRawDb; float flux = rawVal - bin.lastRawDb;
bin.lastRawDb = rawVal; bin.lastRawDb = rawVal;
if (flux > 0) { if (flux > 0) {
float impactMultiplier = 1.0f + (3.0f * order * m_entropyStrength); float jumpTarget = bin.visualDb + (flux * 1.5f);
float jumpTarget = bin.visualDb + (flux * impactMultiplier);
if (jumpTarget > bin.trailDb) { if (jumpTarget > bin.trailDb) {
bin.trailDb = jumpTarget; bin.trailDb = jumpTarget;
bin.trailLife = 1.0f; 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); 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<float> brightMods(freqs.size() - 1, 0.0f); std::vector<float> brightMods(freqs.size() - 1, 0.0f);
std::vector<float> alphaMods(freqs.size() - 1, 0.0f); std::vector<float> alphaMods(freqs.size() - 1, 0.0f);
@ -328,18 +307,8 @@ void VisualizerWidget::drawContent(QPainter& p, int w, int h) {
bool leftDominant = (prev > next); bool leftDominant = (prev > next);
float sharpness = std::min(curr - prev, curr - next); float sharpness = std::min(curr - prev, curr - next);
// COMPETITION LOGIC (Controlled by Entropy) float peakIntensity = std::clamp(std::pow(sharpness * 10.0f, 0.3f), 0.0f, 1.0f);
// m_entropyStrength (0.0 to 3.0) float decayBase = 0.65f - std::clamp(sharpness * 3.0f, 0.0f, 0.35f);
// 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);
auto applyPattern = [&](int dist, bool isBrightSide, int direction) { auto applyPattern = [&](int dist, bool isBrightSide, int direction) {
int segIdx = (direction == -1) ? (i - dist) : (i + dist - 1); int segIdx = (direction == -1) ? (i - dist) : (i + dist - 1);

View File

@ -13,7 +13,7 @@ class VisualizerWidget : public QWidget {
public: public:
VisualizerWidget(QWidget* parent = nullptr); VisualizerWidget(QWidget* parent = nullptr);
void updateData(const std::vector<AudioEngine::FrameData>& data); void updateData(const std::vector<AudioEngine::FrameData>& 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<QColor>& palette); void setAlbumPalette(const std::vector<QColor>& palette);
void setNumBins(int n); void setNumBins(int n);
@ -28,7 +28,6 @@ private:
void drawContent(QPainter& p, int w, int h); void drawContent(QPainter& p, int w, int h);
struct BinState { struct BinState {
std::deque<float> history; // For entropy calculation
float visualDb = -100.0f; // Mixed (Height) float visualDb = -100.0f; // Mixed (Height)
float primaryVisualDb = -100.0f; // Primary (Pattern) float primaryVisualDb = -100.0f; // Primary (Pattern)
float lastRawDb = -100.0f; // To calculate flux float lastRawDb = -100.0f; // To calculate flux
@ -57,9 +56,7 @@ private:
float m_hueFactor = 0.9f; float m_hueFactor = 0.9f;
float m_contrast = 1.0f; float m_contrast = 1.0f;
float m_brightness = 1.0f; float m_brightness = 1.0f;
float m_entropyStrength = 1.0f;
float getX(float freq); float getX(float freq);
QColor applyModifiers(QColor c); QColor applyModifiers(QColor c);
float calculateEntropy(const std::deque<float>& history);
}; };