visual stuff

This commit is contained in:
pszsh 2026-01-28 17:20:07 -08:00
parent b0f33d984a
commit 9ad565ecfb
7 changed files with 234 additions and 33 deletions

View File

@ -1,3 +1,5 @@
// src/AudioEngine.cpp
#include "AudioEngine.h"
#include <QMediaDevices>
#include <QAudioDevice>
@ -7,10 +9,30 @@
#include <QAudioBuffer>
#include <QDebug>
#include <QtGlobal>
#include <algorithm>
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<int>(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<float> 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<float> 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<FrameData> 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);
}

View File

@ -51,9 +51,11 @@ private:
QFile* m_fileSource = nullptr;
QTimer* m_processTimer = nullptr;
std::vector<Processor*> m_processors;
int m_frameSize = 32768;
std::vector<Processor*> m_processors; // Main (Steady)
std::vector<Processor*> 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;
};

View File

@ -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);

View File

@ -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;
};

View File

@ -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<float> 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();

View File

@ -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<float>& data);
struct Spectrum {
@ -39,7 +46,14 @@ private:
// Moving Average History
std::deque<std::vector<float>> 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<float>& freqs, const std::vector<float>& db, float targetFreq);
};

View File

@ -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<float> 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<float> 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<float> 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);