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 "AudioEngine.h"
#include <QMediaDevices> #include <QMediaDevices>
#include <QAudioDevice> #include <QAudioDevice>
@ -7,10 +9,30 @@
#include <QAudioBuffer> #include <QAudioBuffer>
#include <QDebug> #include <QDebug>
#include <QtGlobal> #include <QtGlobal>
#include <algorithm>
AudioEngine::AudioEngine(QObject* parent) : QObject(parent) { 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));
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 = new QTimer(this);
m_processTimer->setInterval(16); m_processTimer->setInterval(16);
@ -20,11 +42,13 @@ AudioEngine::AudioEngine(QObject* parent) : QObject(parent) {
AudioEngine::~AudioEngine() { 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;
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);
} }
void AudioEngine::loadTrack(const QString& filePath) { void AudioEngine::loadTrack(const QString& filePath) {
@ -32,6 +56,8 @@ void AudioEngine::loadTrack(const QString& filePath) {
m_pcmData.clear(); m_pcmData.clear();
m_buffer.close(); m_buffer.close();
m_sampleRate = 48000;
if (m_fileSource) { if (m_fileSource) {
m_fileSource->close(); m_fileSource->close();
delete m_fileSource; delete m_fileSource;
@ -42,7 +68,6 @@ void AudioEngine::loadTrack(const QString& filePath) {
m_decoder = new QAudioDecoder(this); m_decoder = new QAudioDecoder(this);
QAudioFormat format; QAudioFormat format;
format.setSampleRate(44100);
format.setChannelCount(2); format.setChannelCount(2);
format.setSampleFormat(QAudioFormat::Int16); format.setSampleFormat(QAudioFormat::Int16);
m_decoder->setAudioFormat(format); m_decoder->setAudioFormat(format);
@ -60,7 +85,6 @@ void AudioEngine::loadTrack(const QString& filePath) {
} else { } else {
delete m_fileSource; delete m_fileSource;
m_fileSource = nullptr; m_fileSource = nullptr;
// Fix: Handle content:// URIs correctly
if (filePath.startsWith("content://")) { if (filePath.startsWith("content://")) {
m_decoder->setSource(QUrl(filePath)); m_decoder->setSource(QUrl(filePath));
} else { } else {
@ -83,7 +107,13 @@ void AudioEngine::onBufferReady() {
QAudioBuffer buffer = m_decoder->read(); QAudioBuffer buffer = m_decoder->read();
if (!buffer.isValid()) return; 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 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();
@ -153,7 +183,7 @@ void AudioEngine::play() {
} }
QAudioFormat format; QAudioFormat format;
format.setSampleRate(44100); format.setSampleRate(m_sampleRate);
format.setChannelCount(2); format.setChannelCount(2);
format.setSampleFormat(QAudioFormat::Float); format.setSampleFormat(QAudioFormat::Float);
@ -164,13 +194,12 @@ void AudioEngine::play() {
} }
if (!device.isFormatSupported(format)) { if (!device.isFormatSupported(format)) {
qWarning() << "AudioEngine: Float format not supported, using preferred format."; qWarning() << "AudioEngine: Format not supported, using preferred format.";
format = device.preferredFormat(); format = device.preferredFormat();
} }
m_sink = new QAudioSink(device, format, this); m_sink = new QAudioSink(device, format, this);
connect(m_sink, &QAudioSink::stateChanged, this, [this](QAudio::State state){ connect(m_sink, &QAudioSink::stateChanged, this, [this](QAudio::State state){
if (state == QAudio::IdleState && m_sink->error() == QAudio::NoError) { if (state == QAudio::IdleState && m_sink->error() == QAudio::NoError) {
if (m_buffer.bytesAvailable() == 0) { if (m_buffer.bytesAvailable() == 0) {
@ -208,7 +237,13 @@ void AudioEngine::seek(float position) {
void AudioEngine::setDspParams(int frameSize, int hopSize) { void AudioEngine::setDspParams(int frameSize, int hopSize) {
m_frameSize = frameSize; m_frameSize = frameSize;
m_hopSize = hopSize; m_hopSize = hopSize;
// Main: Full size
for(auto p : m_processors) p->setFrameSize(frameSize); 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() { void AudioEngine::onProcessTimer() {
@ -223,6 +258,7 @@ void AudioEngine::onProcessTimer() {
if (sampleIdx + m_frameSize * 2 >= totalSamples) return; if (sampleIdx + m_frameSize * 2 >= totalSamples) return;
// Prepare data for Main Processors
std::vector<float> ch0(m_frameSize), ch1(m_frameSize); std::vector<float> 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] = samples[sampleIdx + i*2];
@ -232,10 +268,31 @@ void AudioEngine::onProcessTimer() {
m_processors[0]->pushData(ch0); m_processors[0]->pushData(ch0);
m_processors[1]->pushData(ch1); 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; std::vector<FrameData> results;
for (auto p : m_processors) { for (size_t i = 0; i < m_processors.size(); ++i) {
auto spec = p->getSpectrum(); auto specMain = m_processors[i]->getSpectrum();
results.push_back({spec.freqs, spec.db}); 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); emit spectrumReady(results);
} }

View File

@ -51,9 +51,11 @@ private:
QFile* m_fileSource = nullptr; QFile* m_fileSource = nullptr;
QTimer* m_processTimer = nullptr; QTimer* m_processTimer = nullptr;
std::vector<Processor*> m_processors; std::vector<Processor*> m_processors; // Main (Steady)
int m_frameSize = 32768; std::vector<Processor*> m_transientProcessors; // Secondary (Fast/Transient)
int m_frameSize = 4096;
int m_hopSize = 1024; int m_hopSize = 1024;
int m_sampleRate = 44100; int m_sampleRate = 48000;
int m_channels = 2; int m_channels = 2;
}; };

View File

@ -160,12 +160,15 @@ SettingsWidget::SettingsWidget(QWidget* parent) : QWidget(parent) {
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) {
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); 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);
}); });
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); connect(m_padDsp, &XYPad::valuesChanged, this, &SettingsWidget::onDspPadChanged);
padsLayout->addWidget(m_padDsp); padsLayout->addWidget(m_padDsp);
@ -225,7 +228,8 @@ void SettingsWidget::emitParams() {
} }
void SettingsWidget::onDspPadChanged(float x, float y) { 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_fft = std::pow(2, power);
m_hop = 64 + y * (8192 - 64); m_hop = 64 + y * (8192 - 64);
emit dspParamsChanged(m_fft, m_hop); emit dspParamsChanged(m_fft, m_hop);

View File

@ -81,7 +81,7 @@ private:
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; float m_entropy = 1.0f;
int m_fft = 32768; int m_fft = 4096;
int m_hop = 1024; int m_hop = 1024;
int m_bins = 26; int m_bins = 26;
}; };

View File

@ -21,10 +21,30 @@ Processor::~Processor() {
if (m_out) fftwf_free(m_out); 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) { void Processor::setNumBins(int n) {
m_customBins.clear(); m_customBins.clear();
m_freqsConst.clear(); m_freqsConst.clear();
m_history.clear(); // Clear history on bin change to avoid size mismatch m_history.clear();
float minFreq = 40.0f; float minFreq = 40.0f;
float maxFreq = 11000.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_plan = fftwf_plan_dft_r2c_1d(m_frameSize, m_in, m_out, FFTW_ESTIMATE);
m_window.resize(m_frameSize); 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) { for (int i = 0; i < m_frameSize; ++i) {
float a0 = 0.35875f; float a0 = 0.35875f;
float a1 = 0.48829f; float a1 = 0.48829f;
@ -114,24 +134,32 @@ Processor::Spectrum Processor::getSpectrum() {
float im = m_out[i][1]; float im = m_out[i][1];
float mag = 2.0f * std::sqrt(re*re + im*im) / m_frameSize; 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)); 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) // 4. Map to Custom Bins (Log Scale)
std::vector<float> currentDb(m_freqsConst.size()); std::vector<float> 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]); 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; if (val < -100.0f) val = -100.0f;
currentDb[i] = val; currentDb[i] = val;
} }
// 5. Moving Average Filter // 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); m_history.push_back(currentDb);
if (m_history.size() > m_smoothingLength) { if (m_history.size() > m_smoothingLength) {
m_history.pop_front(); m_history.pop_front();

View File

@ -11,7 +11,14 @@ public:
~Processor(); ~Processor();
void setFrameSize(int size); void setFrameSize(int size);
void setSampleRate(int rate);
void setNumBins(int n); 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); void pushData(const std::vector<float>& data);
struct Spectrum { struct Spectrum {
@ -39,7 +46,14 @@ private:
// Moving Average History // Moving Average History
std::deque<std::vector<float>> m_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); 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; const auto& bins = m_channels[ch].bins;
if (bins.empty()) continue; 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; float xOffset = (ch == 1 && m_data.size() > 1) ? 1.005f : 1.0f;
for (size_t i = 0; i < freqs.size() - 1; ++i) { 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); binColor = QColor::fromHsvF(hue, 1.0f, 1.0f);
} }
// Use visualDb (Physics processed) // Base Brightness from Energy
float meanDb = (b.visualDb + bNext.visualDb) / 2.0f; float avgEnergy = (vertexEnergy[i] + vertexEnergy[i+1]) / 2.0f;
float intensity = std::clamp((meanDb + 80.0f) / 80.0f, 0.0f, 1.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; float h_val, s, v, a;
binColor.getHsvF(&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 * finalBrightness, 0.0f, 1.0f);
v = std::clamp(v * brightness, 0.0f, 1.0f);
QColor dynamicBinColor = QColor::fromHsvF(h_val, s, v); QColor dynamicBinColor = QColor::fromHsvF(h_val, s, v);
QColor fillColor, lineColor; QColor fillColor, lineColor;
if (m_glass) { if (m_glass) {
float uh, us, uv, ua; float uh, us, uv, ua;
unifiedColor.getHsvF(&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 * finalBrightness, 0.0f, 1.0f));
fillColor = QColor::fromHsvF(uh, us, std::clamp(uv * uBrightness, 0.0f, 1.0f));
lineColor = dynamicBinColor; lineColor = dynamicBinColor;
} else { } else {
fillColor = dynamicBinColor; fillColor = dynamicBinColor;
lineColor = 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; 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); fillColor.setAlphaF(alpha);
lineColor.setAlphaF(0.9f); lineColor.setAlphaF(0.9f);