From 9ad565ecfb35653accbc25946cb9ca3878a341e5 Mon Sep 17 00:00:00 2001 From: pszsh Date: Wed, 28 Jan 2026 17:20:07 -0800 Subject: [PATCH] visual stuff --- src/AudioEngine.cpp | 75 ++++++++++++++++++++++---- src/AudioEngine.h | 8 +-- src/PlayerControls.cpp | 10 ++-- src/PlayerControls.h | 2 +- src/Processor.cpp | 44 ++++++++++++--- src/Processor.h | 16 +++++- src/VisualizerWidget.cpp | 112 ++++++++++++++++++++++++++++++++++++--- 7 files changed, 234 insertions(+), 33 deletions(-) diff --git a/src/AudioEngine.cpp b/src/AudioEngine.cpp index 74e7b50..7e8f28d 100644 --- a/src/AudioEngine.cpp +++ b/src/AudioEngine.cpp @@ -1,3 +1,5 @@ +// src/AudioEngine.cpp + #include "AudioEngine.h" #include #include @@ -7,10 +9,30 @@ #include #include #include +#include AudioEngine::AudioEngine(QObject* parent) : QObject(parent) { + // Main Processors (Steady State) m_processors.push_back(new Processor(m_frameSize, m_sampleRate)); m_processors.push_back(new Processor(m_frameSize, m_sampleRate)); + + // Configure Main: Expander + HPF + Moderate Smoothing + for(auto p : m_processors) { + p->setExpander(1.5f, -50.0f); // Gentle expansion to clean up noise + p->setHPF(60.0f); // Cut rumble below 60Hz + p->setSmoothing(3); + } + + // Transient Processors (Secondary, Fast) + int transSize = std::max(64, m_frameSize / 4); + m_transientProcessors.push_back(new Processor(transSize, m_sampleRate)); + m_transientProcessors.push_back(new Processor(transSize, m_sampleRate)); + + // Configure Transient: Aggressive expansion, light smoothing + for(auto p : m_transientProcessors) { + p->setExpander(2.5f, -40.0f); // Expand dynamics around -40dB + p->setSmoothing(2); // Fast decay + } m_processTimer = new QTimer(this); m_processTimer->setInterval(16); @@ -20,11 +42,13 @@ AudioEngine::AudioEngine(QObject* parent) : QObject(parent) { AudioEngine::~AudioEngine() { stop(); for(auto p : m_processors) delete p; + for(auto p : m_transientProcessors) 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); } void AudioEngine::loadTrack(const QString& filePath) { @@ -32,6 +56,8 @@ void AudioEngine::loadTrack(const QString& filePath) { m_pcmData.clear(); m_buffer.close(); + m_sampleRate = 48000; + if (m_fileSource) { m_fileSource->close(); delete m_fileSource; @@ -42,7 +68,6 @@ void AudioEngine::loadTrack(const QString& filePath) { m_decoder = new QAudioDecoder(this); QAudioFormat format; - format.setSampleRate(44100); format.setChannelCount(2); format.setSampleFormat(QAudioFormat::Int16); m_decoder->setAudioFormat(format); @@ -60,7 +85,6 @@ void AudioEngine::loadTrack(const QString& filePath) { } else { delete m_fileSource; m_fileSource = nullptr; - // Fix: Handle content:// URIs correctly if (filePath.startsWith("content://")) { m_decoder->setSource(QUrl(filePath)); } else { @@ -83,7 +107,13 @@ void AudioEngine::onBufferReady() { QAudioBuffer buffer = m_decoder->read(); if (!buffer.isValid()) return; - // Fix: Explicit cast to int to avoid warning + if (buffer.format().sampleRate() != m_sampleRate && buffer.format().sampleRate() > 0) { + m_sampleRate = buffer.format().sampleRate(); + 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); + } + const int frames = static_cast(buffer.frameCount()); const int channels = buffer.format().channelCount(); auto sampleType = buffer.format().sampleFormat(); @@ -153,7 +183,7 @@ void AudioEngine::play() { } QAudioFormat format; - format.setSampleRate(44100); + format.setSampleRate(m_sampleRate); format.setChannelCount(2); format.setSampleFormat(QAudioFormat::Float); @@ -164,13 +194,12 @@ void AudioEngine::play() { } if (!device.isFormatSupported(format)) { - qWarning() << "AudioEngine: Float format not supported, using preferred format."; + qWarning() << "AudioEngine: Format not supported, using preferred format."; format = device.preferredFormat(); } m_sink = new QAudioSink(device, format, this); - connect(m_sink, &QAudioSink::stateChanged, this, [this](QAudio::State state){ if (state == QAudio::IdleState && m_sink->error() == QAudio::NoError) { if (m_buffer.bytesAvailable() == 0) { @@ -208,7 +237,13 @@ void AudioEngine::seek(float position) { void AudioEngine::setDspParams(int frameSize, int hopSize) { m_frameSize = frameSize; m_hopSize = hopSize; + + // Main: Full size for(auto p : m_processors) p->setFrameSize(frameSize); + + // Transient: 1/4 size (Minimum 64) + int transSize = std::max(64, frameSize / 4); + for(auto p : m_transientProcessors) p->setFrameSize(transSize); } void AudioEngine::onProcessTimer() { @@ -223,6 +258,7 @@ void AudioEngine::onProcessTimer() { if (sampleIdx + m_frameSize * 2 >= totalSamples) return; + // Prepare data for Main Processors std::vector ch0(m_frameSize), ch1(m_frameSize); for (int i = 0; i < m_frameSize; ++i) { ch0[i] = samples[sampleIdx + i*2]; @@ -232,10 +268,31 @@ void AudioEngine::onProcessTimer() { m_processors[0]->pushData(ch0); m_processors[1]->pushData(ch1); + // Prepare data for Transient Processors (Smaller window) + int transSize = std::max(64, m_frameSize / 4); + std::vector tCh0(transSize), tCh1(transSize); + int offset = m_frameSize - transSize; + for (int i = 0; i < transSize; ++i) { + tCh0[i] = ch0[offset + i]; + tCh1[i] = ch1[offset + i]; + } + + m_transientProcessors[0]->pushData(tCh0); + m_transientProcessors[1]->pushData(tCh1); + std::vector results; - for (auto p : m_processors) { - auto spec = p->getSpectrum(); - results.push_back({spec.freqs, spec.db}); + for (size_t i = 0; i < m_processors.size(); ++i) { + auto specMain = m_processors[i]->getSpectrum(); + auto specTrans = m_transientProcessors[i]->getSpectrum(); + + // Mix: Overlay the expanded transient peaks onto the main spectrum + if (specMain.db.size() == specTrans.db.size()) { + for(size_t b = 0; b < specMain.db.size(); ++b) { + specMain.db[b] = std::max(specMain.db[b], specTrans.db[b]); + } + } + + results.push_back({specMain.freqs, specMain.db}); } emit spectrumReady(results); } \ No newline at end of file diff --git a/src/AudioEngine.h b/src/AudioEngine.h index 7ac49f1..7814871 100644 --- a/src/AudioEngine.h +++ b/src/AudioEngine.h @@ -51,9 +51,11 @@ private: QFile* m_fileSource = nullptr; QTimer* m_processTimer = nullptr; - std::vector m_processors; - int m_frameSize = 32768; + std::vector m_processors; // Main (Steady) + std::vector m_transientProcessors; // Secondary (Fast/Transient) + + int m_frameSize = 4096; int m_hopSize = 1024; - int m_sampleRate = 44100; + int m_sampleRate = 48000; int m_channels = 2; }; \ No newline at end of file diff --git a/src/PlayerControls.cpp b/src/PlayerControls.cpp index eeffc04..d7f1dc1 100644 --- a/src/PlayerControls.cpp +++ b/src/PlayerControls.cpp @@ -160,12 +160,15 @@ SettingsWidget::SettingsWidget(QWidget* parent) : QWidget(parent) { m_padDsp = new XYPad("DSP", this); m_padDsp->setFormatter([](float x, float y) { - int fft = std::pow(2, 13 + (int)(x * 4.0f + 0.5f)); + // 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); }); - m_padDsp->setValues(0.5f, 0.118f); + // Default to ~4096 FFT (x approx 0.857) and reasonable hop + m_padDsp->setValues(0.857f, 0.118f); connect(m_padDsp, &XYPad::valuesChanged, this, &SettingsWidget::onDspPadChanged); padsLayout->addWidget(m_padDsp); @@ -225,7 +228,8 @@ void SettingsWidget::emitParams() { } void SettingsWidget::onDspPadChanged(float x, float y) { - int power = 13 + (int)(x * 4.0f + 0.5f); + // 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); emit dspParamsChanged(m_fft, m_hop); diff --git a/src/PlayerControls.h b/src/PlayerControls.h index 470570b..f840ead 100644 --- a/src/PlayerControls.h +++ b/src/PlayerControls.h @@ -81,7 +81,7 @@ private: float m_contrast = 1.0f; float m_brightness = 1.0f; float m_entropy = 1.0f; - int m_fft = 32768; + int m_fft = 4096; int m_hop = 1024; int m_bins = 26; }; diff --git a/src/Processor.cpp b/src/Processor.cpp index 33aef44..f61acb4 100644 --- a/src/Processor.cpp +++ b/src/Processor.cpp @@ -21,10 +21,30 @@ Processor::~Processor() { if (m_out) fftwf_free(m_out); } +void Processor::setSampleRate(int rate) { + m_sampleRate = rate; +} + +void Processor::setSmoothing(int historyLength) { + m_smoothingLength = std::max(1, historyLength); + while (m_history.size() > m_smoothingLength) { + m_history.pop_front(); + } +} + +void Processor::setExpander(float ratio, float thresholdDb) { + m_expandRatio = ratio; + m_expandThreshold = thresholdDb; +} + +void Processor::setHPF(float cutoffFreq) { + m_hpfCutoff = cutoffFreq; +} + void Processor::setNumBins(int n) { m_customBins.clear(); m_freqsConst.clear(); - m_history.clear(); // Clear history on bin change to avoid size mismatch + m_history.clear(); float minFreq = 40.0f; float maxFreq = 11000.0f; @@ -54,7 +74,7 @@ void Processor::setFrameSize(int size) { m_plan = fftwf_plan_dft_r2c_1d(m_frameSize, m_in, m_out, FFTW_ESTIMATE); m_window.resize(m_frameSize); - // Blackman-Harris window for excellent side-lobe suppression (reduces spectral leakage/noise) + // Blackman-Harris window for (int i = 0; i < m_frameSize; ++i) { float a0 = 0.35875f; float a1 = 0.48829f; @@ -114,24 +134,32 @@ Processor::Spectrum Processor::getSpectrum() { float im = m_out[i][1]; float mag = 2.0f * std::sqrt(re*re + im*im) / m_frameSize; + float freq = i * (float)m_sampleRate / m_frameSize; + + // HPF: Attenuate frequencies below cutoff + if (freq < m_hpfCutoff) { + mag *= 0.0001f; // -80dB attenuation + } + dbFull[i] = 20.0f * std::log10(std::max(mag, 1e-12f)); - freqsFull[i] = i * (float)m_sampleRate / m_frameSize; + 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]); + + // 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; currentDb[i] = val; } // 5. Moving Average Filter - // CRITICAL CHANGE: Reduced smoothing to 1 (effectively off) to allow - // the VisualizerWidget to detect sharp transients (Flux) accurately. - // The Visualizer will handle its own aesthetic smoothing. - m_smoothingLength = 1; - m_history.push_back(currentDb); if (m_history.size() > m_smoothingLength) { m_history.pop_front(); diff --git a/src/Processor.h b/src/Processor.h index e4c0871..7a011d1 100644 --- a/src/Processor.h +++ b/src/Processor.h @@ -11,7 +11,14 @@ public: ~Processor(); void setFrameSize(int size); + void setSampleRate(int rate); void setNumBins(int n); + + // DSP Effects + void setSmoothing(int historyLength); + void setExpander(float ratio, float thresholdDb); + void setHPF(float cutoffFreq); + void pushData(const std::vector& data); struct Spectrum { @@ -39,7 +46,14 @@ private: // Moving Average History std::deque> m_history; - size_t m_smoothingLength = 3; // Number of frames to average + size_t m_smoothingLength = 3; + + // Expander Settings + float m_expandRatio = 1.0f; + float m_expandThreshold = -60.0f; + + // HPF Settings + float m_hpfCutoff = 0.0f; float getInterpolatedDb(const std::vector& freqs, const std::vector& db, float targetFreq); }; \ No newline at end of file diff --git a/src/VisualizerWidget.cpp b/src/VisualizerWidget.cpp index cb35c88..07ff24f 100644 --- a/src/VisualizerWidget.cpp +++ b/src/VisualizerWidget.cpp @@ -268,6 +268,87 @@ void VisualizerWidget::drawContent(QPainter& p, int w, int h) { const auto& bins = m_channels[ch].bins; if (bins.empty()) continue; + // 1. Calculate Raw Energy + std::vector rawEnergy(bins.size()); + for (size_t j = 0; j < bins.size(); ++j) { + rawEnergy[j] = std::clamp((bins[j].visualDb + 80.0f) / 80.0f, 0.0f, 1.0f); + } + + // 2. Auto-Balance Highs vs Lows (Dynamic Normalization) + size_t splitIdx = bins.size() / 2; + float maxLow = 0.01f; + float maxHigh = 0.01f; + + for (size_t j = 0; j < splitIdx; ++j) maxLow = std::max(maxLow, rawEnergy[j]); + for (size_t j = splitIdx; j < bins.size(); ++j) maxHigh = std::max(maxHigh, rawEnergy[j]); + + float trebleBoost = maxLow / maxHigh; + trebleBoost = std::clamp(trebleBoost, 1.0f, 40.0f); + + std::vector vertexEnergy(bins.size()); + float globalMax = 0.001f; + + for (size_t j = 0; j < bins.size(); ++j) { + float val = rawEnergy[j]; + + if (j >= splitIdx) { + float t = (float)(j - splitIdx) / (bins.size() - splitIdx); + float boost = 1.0f + (trebleBoost - 1.0f) * t; + val *= boost; + } + + // Soft-Knee Compression + float compressed = std::tanh(val); + vertexEnergy[j] = compressed; + if (compressed > globalMax) globalMax = compressed; + } + + // 3. Global Normalization (Ensure peaks hit 1.0) + // This fixes the "faded highs" issue by ensuring the loudest part of the spectrum + // (even if it's just high hats) is fully opaque. + for (float& v : vertexEnergy) { + v = std::clamp(v / globalMax, 0.0f, 1.0f); + } + + // 4. Calculate Segment Modifiers (Vote System) + std::vector segmentModifiers(freqs.size() - 1, 0.0f); + + for (size_t i = 1; i < vertexEnergy.size() - 1; ++i) { + float curr = vertexEnergy[i]; + float prev = vertexEnergy[i-1]; + float next = vertexEnergy[i+1]; + + // Is this vertex a local peak? + if (curr > prev && curr > next) { + bool leftDominant = (prev > next); + float sharpness = std::min(curr - prev, curr - next); + float intensity = std::clamp(sharpness * 4.0f, 0.0f, 1.0f); + + float brightBoost = 1.0f * intensity; + + if (leftDominant) { + // Left Segment (i-1) gets Brightness + if (i > 0) segmentModifiers[i-1] += brightBoost; + + // Right Side (Dark Side) + if (i < segmentModifiers.size()) segmentModifiers[i] -= 0.6f * intensity; // Dark + if (i + 1 < segmentModifiers.size()) segmentModifiers[i+1] -= 0.9f * intensity; // Darker + if (i + 2 < segmentModifiers.size()) segmentModifiers[i+2] -= 0.6f * intensity; // Dark + if (i + 3 < segmentModifiers.size()) segmentModifiers[i+3] -= 0.3f * intensity; // Fade + + } else { + // Right Segment (i) gets Brightness + if (i < segmentModifiers.size()) segmentModifiers[i] += brightBoost; + + // Left Side (Dark Side) + if (i > 0) segmentModifiers[i-1] -= 0.6f * intensity; // Dark + if (i >= 2) segmentModifiers[i-2] -= 0.9f * intensity; // Darker + if (i >= 3) segmentModifiers[i-3] -= 0.6f * intensity; // Dark + if (i >= 4) segmentModifiers[i-4] -= 0.3f * intensity; // Fade + } + } + } + float xOffset = (ch == 1 && m_data.size() > 1) ? 1.005f : 1.0f; for (size_t i = 0; i < freqs.size() - 1; ++i) { @@ -288,30 +369,45 @@ void VisualizerWidget::drawContent(QPainter& p, int w, int h) { binColor = QColor::fromHsvF(hue, 1.0f, 1.0f); } - // Use visualDb (Physics processed) - float meanDb = (b.visualDb + bNext.visualDb) / 2.0f; - float intensity = std::clamp((meanDb + 80.0f) / 80.0f, 0.0f, 1.0f); + // Base Brightness from Energy + float avgEnergy = (vertexEnergy[i] + vertexEnergy[i+1]) / 2.0f; + float baseBrightness = std::pow(avgEnergy, 0.5f); + + // Apply Shadow/Highlight Modifiers to Brightness + float modifier = segmentModifiers[i]; + float multiplier = (modifier >= 0) ? (1.0f + modifier) : (1.0f / (1.0f - modifier * 2.0f)); + + float finalBrightness = std::clamp(baseBrightness * multiplier * m_brightness, 0.0f, 1.0f); float h_val, s, v, a; binColor.getHsvF(&h_val, &s, &v, &a); - float brightness = (0.4f + 0.6f * intensity) * m_brightness; - v = std::clamp(v * brightness, 0.0f, 1.0f); + v = std::clamp(v * finalBrightness, 0.0f, 1.0f); QColor dynamicBinColor = QColor::fromHsvF(h_val, s, v); QColor fillColor, lineColor; if (m_glass) { float uh, us, uv, ua; unifiedColor.getHsvF(&uh, &us, &uv, &ua); - float uBrightness = (0.4f + 0.6f * intensity) * m_brightness; - fillColor = QColor::fromHsvF(uh, us, std::clamp(uv * uBrightness, 0.0f, 1.0f)); + fillColor = QColor::fromHsvF(uh, us, std::clamp(uv * finalBrightness, 0.0f, 1.0f)); lineColor = dynamicBinColor; } else { fillColor = dynamicBinColor; lineColor = dynamicBinColor; } + // --- Alpha Calculation with Shadow Logic --- + // 1. Start with the boosted, normalized energy (avgEnergy) + // 2. Apply the shadow modifier (linear scalar for alpha) + // If modifier is negative (shadow), reduce alpha. + // If positive (peak), boost alpha slightly. + float alphaScalar = (modifier >= 0) ? (1.0f + modifier * 0.2f) : (1.0f + modifier); + + float intensity = avgEnergy; float alpha = 0.4f + (intensity - 0.5f) * m_contrast; - alpha = std::clamp(alpha, 0.0f, 1.0f); + + // Apply the shadow scalar to the calculated alpha + alpha = std::clamp(alpha * alphaScalar, 0.0f, 1.0f); + fillColor.setAlphaF(alpha); lineColor.setAlphaF(0.9f);