Okay, starting to looks seriosly good, almost works on all platforms too now! lol

This commit is contained in:
pszsh 2026-02-26 16:54:16 -08:00
parent 30cecf586c
commit b6ef417242
11 changed files with 312 additions and 140 deletions

View File

@ -279,59 +279,77 @@ void AudioEngine::onFinished() {
// Notify UI that track is ready to play // Notify UI that track is ready to play
emit trackLoaded(true); emit trackLoaded(true);
// Emit immediately so analyzer can use pcmData fallback while Hilbert runs // Emit early with pcmData-only so analyzer can show spectrum immediately.
// The Hilbert task builds a NEW TrackData, so no data race.
emit trackDataChanged(m_trackData); emit trackDataChanged(m_trackData);
// OPTIMIZATION: Run heavy analysis in background to avoid blocking audio // Run heavy analysis in background thread pool
// thread FIX: Use QPointer to prevent crash if AudioEngine is deleted
// before task runs
QPointer<AudioEngine> self = this; QPointer<AudioEngine> self = this;
QThreadPool::globalInstance()->start([self, newData]() { // Capture pcmData via implicit sharing (cheap refcount bump)
QByteArray pcmSnap = newData->pcmData;
int sr = newData->sampleRate;
QThreadPool::globalInstance()->start([self, pcmSnap, sr]() {
if (!self) if (!self)
return; return;
const float *rawFloats = const float *rawFloats =
reinterpret_cast<const float *>(newData->pcmData.constData()); reinterpret_cast<const float *>(pcmSnap.constData());
long long totalFloats = newData->pcmData.size() / sizeof(float); long long totalFloats = pcmSnap.size() / sizeof(float);
long long totalFrames = totalFloats / 2; long long totalFrames = totalFloats / 2;
if (totalFrames > 0) { if (totalFrames <= 0)
return;
// 1. BPM Detection // 1. BPM Detection
#ifdef ENABLE_TEMPO_ESTIMATION #ifdef ENABLE_TEMPO_ESTIMATION
MemoryAudioReader reader(rawFloats, totalFrames, newData->sampleRate); MemoryAudioReader reader(rawFloats, totalFrames, sr);
auto bpmOpt = auto bpmOpt =
LTE::GetBpm(reader, LTE::FalsePositiveTolerance::Lenient, nullptr); LTE::GetBpm(reader, LTE::FalsePositiveTolerance::Lenient, nullptr);
// Emit BPM result back to main thread context
float bpm = bpmOpt.has_value() ? static_cast<float>(*bpmOpt) : 0.0f; float bpm = bpmOpt.has_value() ? static_cast<float>(*bpmOpt) : 0.0f;
if (self) { if (self) {
QMetaObject::invokeMethod(self, "analysisReady", Qt::QueuedConnection, QMetaObject::invokeMethod(self, "analysisReady", Qt::QueuedConnection,
Q_ARG(float, bpm), Q_ARG(float, 1.0f)); Q_ARG(float, bpm), Q_ARG(float, 1.0f));
} }
#endif #endif
// 2. Hilbert Transform // 2. Hilbert Transform — process one channel at a time to minimize
std::vector<double> inputL(totalFrames), inputR(totalFrames); // peak memory. FFTW uses 4 buffers per channel instead of 8.
for (size_t i = 0; i < totalFrames; ++i) { auto finalData = std::make_shared<TrackData>();
inputL[i] = static_cast<double>(rawFloats[i * 2]); finalData->pcmData = pcmSnap;
inputR[i] = static_cast<double>(rawFloats[i * 2 + 1]); finalData->sampleRate = sr;
} finalData->valid = true;
finalData->complexData.resize(totalFloats);
BlockHilbert blockHilbert; BlockHilbert blockHilbert;
auto analyticPair = blockHilbert.hilbertTransform(inputL, inputR); // Reusable input buffer (one channel at a time)
std::vector<double> input(totalFrames);
newData->complexData.resize(totalFloats); // Left channel: build input, transform, copy to complexData, free result
for (size_t i = 0; i < totalFrames; ++i) { for (size_t i = 0; i < static_cast<size_t>(totalFrames); ++i)
newData->complexData[i * 2] = analyticPair.first[i]; input[i] = static_cast<double>(rawFloats[i * 2]);
newData->complexData[i * 2 + 1] = analyticPair.second[i]; {
} auto analytic = blockHilbert.hilbertTransformSingle(input);
for (size_t i = 0; i < static_cast<size_t>(totalFrames); ++i)
finalData->complexData[i * 2] = analytic[i];
} // analytic freed
// Notify Analyzer that complex data is ready // Right channel: reuse input buffer
for (size_t i = 0; i < static_cast<size_t>(totalFrames); ++i)
input[i] = static_cast<double>(rawFloats[i * 2 + 1]);
{
auto analytic = blockHilbert.hilbertTransformSingle(input);
for (size_t i = 0; i < static_cast<size_t>(totalFrames); ++i)
finalData->complexData[i * 2 + 1] = analytic[i];
} // analytic freed
// Free input buffer
{ std::vector<double>().swap(input); }
// Notify Analyzer with the complete data
if (self) { if (self) {
QMetaObject::invokeMethod(self, "trackDataChanged", QMetaObject::invokeMethod(self, "trackDataChanged",
Qt::QueuedConnection, Qt::QueuedConnection,
Q_ARG(std::shared_ptr<TrackData>, newData)); Q_ARG(std::shared_ptr<TrackData>, finalData));
}
} }
}); });
} }
@ -591,7 +609,11 @@ void AudioAnalyzer::processLoop() {
specMain.db[b] = val; specMain.db[b] = val;
} }
} }
results.push_back({specMain.freqs, specMain.db, primaryDb}); FrameData fd{specMain.freqs, specMain.db, primaryDb, {}};
// Pass cepstrum from ch0 main processor only (mono is sufficient)
if (i == 0)
fd.cepstrum = std::move(specMain.cepstrum);
results.push_back(std::move(fd));
} }
// 6. Publish Result // 6. Publish Result

View File

@ -211,6 +211,7 @@ public:
std::vector<float> freqs; std::vector<float> freqs;
std::vector<float> db; std::vector<float> db;
std::vector<float> primaryDb; std::vector<float> primaryDb;
std::vector<float> cepstrum; // from main processor ch0 only
}; };
// Thread-safe pull for UI // Thread-safe pull for UI

View File

@ -92,8 +92,8 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) {
// Settings -> Analyzer // Settings -> Analyzer
connect(m_playerPage->settings(), &SettingsWidget::paramsChanged, this, connect(m_playerPage->settings(), &SettingsWidget::paramsChanged, this,
[this](bool, bool, bool, bool, float, float, float, int granularity, [this](bool, bool, bool, bool, bool, float, float, float,
int detail, float strength) { int granularity, int detail, float strength) {
QMetaObject::invokeMethod( QMetaObject::invokeMethod(
m_analyzer, "setSmoothingParams", Qt::QueuedConnection, m_analyzer, "setSmoothingParams", Qt::QueuedConnection,
Q_ARG(int, granularity), Q_ARG(int, detail), Q_ARG(int, granularity), Q_ARG(int, detail),
@ -411,10 +411,10 @@ void MainWindow::loadSettings() {
m_playerPage->settings()->setParams( m_playerPage->settings()->setParams(
root["glass"].toBool(true), root["focus"].toBool(false), root["glass"].toBool(true), root["focus"].toBool(false),
root["albumColors"].toBool(false), root["mirrored"].toBool(false), root["albumColors"].toBool(false), root["mirrored"].toBool(false),
root["bins"].toInt(26), root["fps"].toInt(60), root["inverted"].toBool(false), root["bins"].toInt(26),
root["brightness"].toDouble(1.0), root["granularity"].toInt(33), root["fps"].toInt(60), root["brightness"].toDouble(1.0),
root["detail"].toInt(50), root["strength"].toDouble(0.0), root["granularity"].toInt(33), root["detail"].toInt(50),
root["bpmScaleIndex"].toInt(2)); root["strength"].toDouble(0.0), root["bpmScaleIndex"].toInt(2));
} }
} }
@ -427,6 +427,7 @@ void MainWindow::saveSettings() {
root["focus"] = s->isFocus(); root["focus"] = s->isFocus();
root["albumColors"] = s->isAlbumColors(); root["albumColors"] = s->isAlbumColors();
root["mirrored"] = s->isMirrored(); root["mirrored"] = s->isMirrored();
root["inverted"] = s->isInverted();
root["bins"] = s->getBins(); root["bins"] = s->getBins();
root["fps"] = s->getFps(); root["fps"] = s->getFps();
root["brightness"] = s->getBrightness(); root["brightness"] = s->getBrightness();
@ -489,7 +490,7 @@ void MainWindow::updateSmoothing() {
float targetStrength = 0.8f * (1.0f - normalized); float targetStrength = 0.8f * (1.0f - normalized);
SettingsWidget *s = m_playerPage->settings(); SettingsWidget *s = m_playerPage->settings();
s->setParams(s->isGlass(), s->isFocus(), s->isAlbumColors(), s->isMirrored(), s->setParams(s->isGlass(), s->isFocus(), s->isAlbumColors(), s->isMirrored(),
s->getBins(), s->getFps(), s->getBrightness(), s->isInverted(), s->getBins(), s->getFps(), s->getBrightness(),
s->getGranularity(), s->getDetail(), targetStrength, s->getGranularity(), s->getDetail(), targetStrength,
s->getBpmScaleIndex()); s->getBpmScaleIndex());
} }

View File

@ -136,6 +136,7 @@ SettingsWidget::SettingsWidget(QWidget *parent) : QWidget(parent) {
m_checkFocus = createCheck("Focus", true, 0, 1); m_checkFocus = createCheck("Focus", true, 0, 1);
m_checkAlbumColors = createCheck("Album Colors", false, 1, 0); m_checkAlbumColors = createCheck("Album Colors", false, 1, 0);
m_checkMirrored = createCheck("Mirrored", true, 1, 1); m_checkMirrored = createCheck("Mirrored", true, 1, 1);
m_checkInverted = createCheck("Invert", false, 2, 0);
layout->addLayout(grid); layout->addLayout(grid);
// Helper for sliders // Helper for sliders
@ -237,7 +238,7 @@ SettingsWidget::SettingsWidget(QWidget *parent) : QWidget(parent) {
} }
void SettingsWidget::setParams(bool glass, bool focus, bool albumColors, void SettingsWidget::setParams(bool glass, bool focus, bool albumColors,
bool mirrored, int bins, int fps, bool mirrored, bool inverted, int bins, int fps,
float brightness, int granularity, int detail, float brightness, int granularity, int detail,
float strength, int bpmScaleIndex) { float strength, int bpmScaleIndex) {
bool oldState = blockSignals(true); bool oldState = blockSignals(true);
@ -245,6 +246,7 @@ void SettingsWidget::setParams(bool glass, bool focus, bool albumColors,
m_checkFocus->setChecked(focus); m_checkFocus->setChecked(focus);
m_checkAlbumColors->setChecked(albumColors); m_checkAlbumColors->setChecked(albumColors);
m_checkMirrored->setChecked(mirrored); m_checkMirrored->setChecked(mirrored);
m_checkInverted->setChecked(inverted);
m_sliderBins->setValue(bins); m_sliderBins->setValue(bins);
m_lblBins->setText(QString("Bins: %1").arg(bins)); m_lblBins->setText(QString("Bins: %1").arg(bins));
@ -278,7 +280,8 @@ void SettingsWidget::setParams(bool glass, bool focus, bool albumColors,
void SettingsWidget::emitParams() { void SettingsWidget::emitParams() {
emit paramsChanged(m_checkGlass->isChecked(), m_checkFocus->isChecked(), emit paramsChanged(m_checkGlass->isChecked(), m_checkFocus->isChecked(),
m_checkAlbumColors->isChecked(), m_checkAlbumColors->isChecked(),
m_checkMirrored->isChecked(), m_hue, m_contrast, m_checkMirrored->isChecked(),
m_checkInverted->isChecked(), m_hue, m_contrast,
m_brightness, m_granularity, m_detail, m_strength); m_brightness, m_granularity, m_detail, m_strength);
} }

View File

@ -45,6 +45,7 @@ public:
bool isFocus() const { return m_checkFocus->isChecked(); } bool isFocus() const { return m_checkFocus->isChecked(); }
bool isAlbumColors() const { return m_checkAlbumColors->isChecked(); } bool isAlbumColors() const { return m_checkAlbumColors->isChecked(); }
bool isMirrored() const { return m_checkMirrored->isChecked(); } bool isMirrored() const { return m_checkMirrored->isChecked(); }
bool isInverted() const { return m_checkInverted->isChecked(); }
int getBins() const { return m_sliderBins->value(); } int getBins() const { return m_sliderBins->value(); }
int getFps() const { return m_sliderFps->value(); } int getFps() const { return m_sliderFps->value(); }
float getBrightness() const { return m_brightness; } float getBrightness() const { return m_brightness; }
@ -58,13 +59,15 @@ public:
int getBpmScaleIndex() const; int getBpmScaleIndex() const;
void setParams(bool glass, bool focus, bool albumColors, bool mirrored, void setParams(bool glass, bool focus, bool albumColors, bool mirrored,
int bins, int fps, float brightness, int granularity, bool inverted, int bins, int fps, float brightness,
int detail, float strength, int bpmScaleIndex); int granularity, int detail, float strength,
int bpmScaleIndex);
signals: signals:
void paramsChanged(bool glass, bool focus, bool albumColors, bool mirrored, void paramsChanged(bool glass, bool focus, bool albumColors, bool mirrored,
float hue, float contrast, float brightness, bool inverted, float hue, float contrast,
int granularity, int detail, float strength); float brightness, int granularity, int detail,
float strength);
void fpsChanged(int fps); void fpsChanged(int fps);
void dspParamsChanged(int fft, int hop); void dspParamsChanged(int fft, int hop);
void binsChanged(int n); void binsChanged(int n);
@ -85,6 +88,7 @@ private:
QCheckBox *m_checkFocus; QCheckBox *m_checkFocus;
QCheckBox *m_checkAlbumColors; QCheckBox *m_checkAlbumColors;
QCheckBox *m_checkMirrored; QCheckBox *m_checkMirrored;
QCheckBox *m_checkInverted;
XYPad *m_padDsp; XYPad *m_padDsp;
XYPad *m_padColor; XYPad *m_padColor;
QSlider *m_sliderBins; QSlider *m_sliderBins;

View File

@ -271,8 +271,7 @@ Processor::Spectrum Processor::getSpectrum() {
freqsFull[i] = freq; freqsFull[i] = freq;
} }
// --- Cepstral Smoothing / Trig Interpolation --- // --- Cepstral IFFT (always compute for cepstrum visualization) ---
if (m_cepstralStrength > 0.0f) {
// 1. Log Magnitude // 1. Log Magnitude
for(int i=0; i<m_frameSize; ++i) { for(int i=0; i<m_frameSize; ++i) {
// Mirror for symmetry to get real cepstrum // Mirror for symmetry to get real cepstrum
@ -285,16 +284,25 @@ Processor::Spectrum Processor::getSpectrum() {
// 2. IFFT -> Cepstrum // 2. IFFT -> Cepstrum
fftw_execute(m_cep_plan_inv); // Result in m_cep_out (scaled by N) fftw_execute(m_cep_plan_inv); // Result in m_cep_out (scaled by N)
// 3. Hilbert on Cepstrum (Analytic Cepstrum) // 3. Extract positive quefrencies for visualization
double scale = 1.0 / m_frameSize; int halfN = m_frameSize / 2;
m_cep_in[0][0] = m_cep_out[0][0] * scale; std::vector<float> cepCoeffs(halfN);
m_cep_in[0][1] = m_cep_out[0][1] * scale; double cepScale = 1.0 / m_frameSize;
for(int i=0; i<halfN; ++i) {
cepCoeffs[i] = static_cast<float>(m_cep_out[i][0] * cepScale);
}
// --- Cepstral Smoothing / Trig Interpolation ---
if (m_cepstralStrength > 0.0f) {
// Hilbert on Cepstrum (Analytic Cepstrum)
m_cep_in[0][0] = m_cep_out[0][0] * cepScale;
m_cep_in[0][1] = m_cep_out[0][1] * cepScale;
for(int i=1; i<m_frameSize; ++i) { for(int i=1; i<m_frameSize; ++i) {
if (i < m_frameSize/2) { if (i < m_frameSize/2) {
// Positive quefrencies * 2 // Positive quefrencies * 2
m_cep_in[i][0] = m_cep_out[i][0] * scale * 2.0; m_cep_in[i][0] = m_cep_out[i][0] * cepScale * 2.0;
m_cep_in[i][1] = m_cep_out[i][1] * scale * 2.0; m_cep_in[i][1] = m_cep_out[i][1] * cepScale * 2.0;
} else { } else {
// Negative quefrencies = 0 // Negative quefrencies = 0
m_cep_in[i][0] = 0.0; m_cep_in[i][0] = 0.0;
@ -302,7 +310,7 @@ Processor::Spectrum Processor::getSpectrum() {
} }
} }
// 4. Idealize Curve (Smoothing) // Idealize Curve (Smoothing)
std::vector<double> envelope = idealizeCurve(magFull); std::vector<double> envelope = idealizeCurve(magFull);
// Apply Strength (Mix) // Apply Strength (Mix)
@ -351,5 +359,5 @@ Processor::Spectrum Processor::getSpectrum() {
std::vector<float> freqsRet(m_freqsConst.size()); 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]); for(size_t i=0; i<m_freqsConst.size(); ++i) freqsRet[i] = static_cast<float>(m_freqsConst[i]);
return {freqsRet, averagedDb}; return {freqsRet, averagedDb, cepCoeffs};
} }

View File

@ -29,6 +29,7 @@ public:
struct Spectrum { struct Spectrum {
std::vector<float> freqs; std::vector<float> freqs;
std::vector<float> db; std::vector<float> db;
std::vector<float> cepstrum; // raw cepstral coefficients (positive quefrencies)
}; };
Spectrum getSpectrum(); Spectrum getSpectrum();

View File

@ -45,12 +45,13 @@ void VisualizerWidget::setTargetFps(int fps) {
} }
void VisualizerWidget::setParams(bool glass, bool focus, bool albumColors, void VisualizerWidget::setParams(bool glass, bool focus, bool albumColors,
bool mirrored, float hue, float contrast, bool mirrored, bool inverted, float hue,
float brightness) { float contrast, float brightness) {
m_glass = glass; m_glass = glass;
m_focus = focus; m_focus = focus;
m_useAlbumColors = albumColors; m_useAlbumColors = albumColors;
m_mirrored = mirrored; m_mirrored = mirrored;
m_inverted = inverted;
m_hueFactor = hue; m_hueFactor = hue;
m_contrast = contrast; m_contrast = contrast;
m_brightness = brightness; m_brightness = brightness;
@ -287,6 +288,16 @@ void VisualizerWidget::updateData(
b.cachedColor = binColor; b.cachedColor = binColor;
} }
} }
// --- 5. Cepstral Thread Smoothing (mirrored mode only) ---
if (m_mirrored && !data.empty() && !data[0].cepstrum.empty()) {
const auto &raw = data[0].cepstrum;
if (m_smoothedCepstrum.size() != raw.size())
m_smoothedCepstrum.assign(raw.size(), 0.0f);
for (size_t i = 0; i < raw.size(); ++i)
m_smoothedCepstrum[i] = 0.15f * raw[i] + 0.85f * m_smoothedCepstrum[i];
}
update(); update();
} }
@ -307,10 +318,10 @@ void VisualizerWidget::initialize(QRhiCommandBuffer *cb) {
2048 * 6 * sizeof(float))); 2048 * 6 * sizeof(float)));
m_vbuf->create(); m_vbuf->create();
// Uniform buffer: 4 aligned MVP matrices (for mirrored mode) // Uniform buffer: 5 aligned MVP matrices (4 mirror passes + 1 cepstrum)
m_ubuf.reset(m_rhi->newBuffer(QRhiBuffer::Dynamic, m_ubuf.reset(m_rhi->newBuffer(QRhiBuffer::Dynamic,
QRhiBuffer::UniformBuffer, QRhiBuffer::UniformBuffer,
m_ubufAlign * 4)); m_ubufAlign * 5));
m_ubuf->create(); m_ubuf->create();
// Shader resource bindings with dynamic UBO offset // Shader resource bindings with dynamic UBO offset
@ -376,10 +387,13 @@ void VisualizerWidget::render(QRhiCommandBuffer *cb) {
// Only rebuild vertices when new data has arrived // Only rebuild vertices when new data has arrived
if (m_dataDirty) { if (m_dataDirty) {
m_dataDirty = false; m_dataDirty = false;
if (m_mirrored) if (m_mirrored) {
buildVertices(w / 2, h / 2); buildVertices(w * 0.55f, h / 2);
else buildCepstrumVertices(w, h);
} else {
buildVertices(w, h); buildVertices(w, h);
m_cepstrumVertexCount = 0;
}
} }
int numPasses = m_mirrored ? 4 : 1; int numPasses = m_mirrored ? 4 : 1;
@ -387,14 +401,21 @@ void VisualizerWidget::render(QRhiCommandBuffer *cb) {
// Prepare resource updates // Prepare resource updates
QRhiResourceUpdateBatch *u = m_rhi->nextResourceUpdateBatch(); QRhiResourceUpdateBatch *u = m_rhi->nextResourceUpdateBatch();
// Upload vertex data // Upload vertex data (main + cepstrum appended)
if (!m_vertices.empty()) { {
int dataSize = static_cast<int>(m_vertices.size() * sizeof(float)); int mainSize = static_cast<int>(m_vertices.size() * sizeof(float));
if (dataSize > m_vbuf->size()) { int cepSize = static_cast<int>(m_cepstrumVerts.size() * sizeof(float));
m_vbuf->setSize(dataSize); int totalSize = mainSize + cepSize;
if (totalSize > 0) {
if (totalSize > m_vbuf->size()) {
m_vbuf->setSize(totalSize);
m_vbuf->create(); m_vbuf->create();
} }
u->updateDynamicBuffer(m_vbuf.get(), 0, dataSize, m_vertices.data()); if (mainSize > 0)
u->updateDynamicBuffer(m_vbuf.get(), 0, mainSize, m_vertices.data());
if (cepSize > 0)
u->updateDynamicBuffer(m_vbuf.get(), mainSize, cepSize, m_cepstrumVerts.data());
}
} }
// Upload MVP matrices // Upload MVP matrices
@ -427,6 +448,15 @@ void VisualizerWidget::render(QRhiCommandBuffer *cb) {
mvp.constData()); mvp.constData());
} }
// Upload full-screen ortho MVP for cepstrum (slot 4)
if (m_mirrored && m_cepstrumVertexCount > 0) {
QMatrix4x4 cepProj;
cepProj.ortho(0, (float)w, (float)h, 0, -1, 1);
QMatrix4x4 cepMvp = correction * cepProj;
u->updateDynamicBuffer(m_ubuf.get(), 4 * m_ubufAlign, 64,
cepMvp.constData());
}
// Begin render pass // Begin render pass
cb->beginPass(renderTarget(), QColor(0, 0, 0, 255), {1.0f, 0}, u); cb->beginPass(renderTarget(), QColor(0, 0, 0, 255), {1.0f, 0}, u);
cb->setViewport({0, 0, (float)outputSize.width(), cb->setViewport({0, 0, (float)outputSize.width(),
@ -452,6 +482,15 @@ void VisualizerWidget::render(QRhiCommandBuffer *cb) {
} }
} }
// --- Cepstral Thread (single full-screen pass, after mirror loop) ---
if (m_mirrored && m_cepstrumVertexCount > 0) {
QRhiCommandBuffer::DynamicOffset cepOfs(0, quint32(4 * m_ubufAlign));
cb->setGraphicsPipeline(m_linePipeline.get());
cb->setShaderResources(m_srb.get(), 1, &cepOfs);
cb->setVertexInput(0, 1, &vbufBinding);
cb->draw(m_cepstrumVertexCount, 1, m_fillVertexCount + m_lineVertexCount, 0);
}
cb->endPass(); cb->endPass();
update(); update();
} }
@ -464,6 +503,76 @@ void VisualizerWidget::releaseResources() {
m_vbuf.reset(); m_vbuf.reset();
} }
// ===== Cepstral Thread Vertex Building =====
void VisualizerWidget::buildCepstrumVertices(int w, int h) {
m_cepstrumVerts.clear();
m_cepstrumVertexCount = 0;
if (m_smoothedCepstrum.empty())
return;
// Quefrency range: indices 12-600 (~80Hz to ~4000Hz pitch at 48kHz)
int qStart = 12;
int qEnd = std::min(600, (int)m_smoothedCepstrum.size());
if (qEnd <= qStart)
return;
// Find peak magnitude for normalization
float peak = 0.0f;
for (int i = qStart; i < qEnd; ++i)
peak = std::max(peak, std::abs(m_smoothedCepstrum[i]));
if (peak < 1e-7f)
return; // silence — don't draw
float invPeak = 1.0f / peak;
float maxDisp = w * 0.06f;
float cx = w * 0.5f;
// Color: unified color desaturated slightly, alpha ~0.45
float cr, cg, cb;
{
float ch, cs, cv;
m_unifiedColor.getHsvF(&ch, &cs, &cv);
cs *= 0.7f; // desaturate
QColor c = QColor::fromHsvF(ch, cs, cv);
cr = c.redF();
cg = c.greenF();
cb = c.blueF();
}
float ca = 0.45f;
// Build line segments with top/bottom edge fade
float fadeMargin = 0.08f; // fade over 8% of height at each end
float prevX = cx + m_smoothedCepstrum[qStart] * invPeak * maxDisp;
float prevY = 0.0f;
float prevT = 0.0f;
for (int i = qStart + 1; i < qEnd; ++i) {
float t = (float)(i - qStart) / (qEnd - qStart);
float y = t * h;
float x = cx + m_smoothedCepstrum[i] * invPeak * maxDisp;
// Fade alpha near top and bottom edges
auto edgeFade = [&](float tt) -> float {
if (tt < fadeMargin) return tt / fadeMargin;
if (tt > 1.0f - fadeMargin) return (1.0f - tt) / fadeMargin;
return 1.0f;
};
float a0 = ca * edgeFade(prevT);
float a1 = ca * edgeFade(t);
m_cepstrumVerts.insert(m_cepstrumVerts.end(),
{prevX, prevY, cr, cg, cb, a0,
x, y, cr, cg, cb, a1});
prevX = x;
prevY = y;
prevT = t;
}
m_cepstrumVertexCount = (int)m_cepstrumVerts.size() / 6;
}
// ===== Vertex Building (identical logic to old drawContent) ===== // ===== Vertex Building (identical logic to old drawContent) =====
void VisualizerWidget::buildVertices(int w, int h) { void VisualizerWidget::buildVertices(int w, int h) {
@ -483,11 +592,13 @@ void VisualizerWidget::buildVertices(int w, int h) {
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 + 1 < freqs.size(); ++i) { size_t numBins = std::min(freqs.size(), bins.size());
if (i + 1 >= bins.size()) for (size_t i = 0; i + 1 < numBins; ++i) {
break; // When inverted, read bin data in reverse order (highs left, lows right)
const auto &b = bins[i]; size_t di = m_inverted ? (numBins - 1 - i) : i;
const auto &bNext = bins[i + 1]; size_t diN = m_inverted ? (numBins - 2 - i) : (i + 1);
const auto &b = bins[di];
const auto &bNext = bins[diN];
// --- Brightness --- // --- Brightness ---
float avgEnergy = float avgEnergy =
@ -527,8 +638,19 @@ void VisualizerWidget::buildVertices(int w, int h) {
float alpha = 0.4f + (avgEnergy - 0.5f) * m_contrast; float alpha = 0.4f + (avgEnergy - 0.5f) * m_contrast;
alpha = std::clamp(alpha * aMult, 0.0f, 1.0f); alpha = std::clamp(alpha * aMult, 0.0f, 1.0f);
// --- Edge fade: taper last bins to transparent near center gap ---
if (m_mirrored) {
int fadeBins = 4;
int fromEnd = (int)(numBins - 2) - (int)i;
if (fromEnd < fadeBins) {
float fade = (float)(fromEnd + 1) / (float)(fadeBins + 1);
fade = fade * fade; // ease-in for smoother taper
alpha *= fade;
}
}
fillColor.setAlphaF(alpha); fillColor.setAlphaF(alpha);
lineColor.setAlphaF(0.9f); lineColor.setAlphaF(std::min(0.9f, alpha));
// --- Channel 1 tint --- // --- Channel 1 tint ---
if (ch == 1 && m_data.size() > 1) { if (ch == 1 && m_data.size() > 1) {

View File

@ -15,7 +15,7 @@ class VisualizerWidget : public QRhiWidget {
public: public:
VisualizerWidget(QWidget* parent = nullptr); VisualizerWidget(QWidget* parent = nullptr);
void updateData(const std::vector<AudioAnalyzer::FrameData>& data); void updateData(const std::vector<AudioAnalyzer::FrameData>& data);
void setParams(bool glass, bool focus, bool albumColors, bool mirrored, float hue, float contrast, float brightness); void setParams(bool glass, bool focus, bool albumColors, bool mirrored, bool inverted, 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);
void setTargetFps(int fps); void setTargetFps(int fps);
@ -60,6 +60,7 @@ private:
bool m_focus = false; bool m_focus = false;
bool m_useAlbumColors = false; bool m_useAlbumColors = false;
bool m_mirrored = false; bool m_mirrored = false;
bool m_inverted = false;
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;
@ -82,6 +83,12 @@ private:
int m_fillVertexCount = 0; int m_fillVertexCount = 0;
int m_lineVertexCount = 0; int m_lineVertexCount = 0;
// Cepstral thread visualization
std::vector<float> m_smoothedCepstrum;
std::vector<float> m_cepstrumVerts;
int m_cepstrumVertexCount = 0;
void buildCepstrumVertices(int w, int h);
float getX(float freq); float getX(float freq);
QColor applyModifiers(QColor c); QColor applyModifiers(QColor c);
}; };

View File

@ -32,71 +32,69 @@
#include <stdexcept> #include <stdexcept>
#include <cmath> #include <cmath>
std::pair<std::vector<std::complex<double>>, std::vector<std::complex<double>>> std::vector<std::complex<double>>
BlockHilbert::hilbertTransform(const std::vector<double>& left_signal, const std::vector<double>& right_signal) { BlockHilbert::hilbertTransformSingle(const std::vector<double>& signal) {
size_t n = left_signal.size(); size_t n = signal.size();
if (n == 0 || n != right_signal.size()) { if (n == 0) return {};
return {{}, {}};
}
fftw_complex* fft_in_L = (fftw_complex*)fftw_malloc(sizeof(fftw_complex) * n); fftw_complex* fft_in = (fftw_complex*)fftw_malloc(sizeof(fftw_complex) * n);
fftw_complex* fft_out_L = (fftw_complex*)fftw_malloc(sizeof(fftw_complex) * n); fftw_complex* fft_out = (fftw_complex*)fftw_malloc(sizeof(fftw_complex) * n);
fftw_complex* ifft_in_L = (fftw_complex*)fftw_malloc(sizeof(fftw_complex) * n); fftw_complex* ifft_in = (fftw_complex*)fftw_malloc(sizeof(fftw_complex) * n);
fftw_complex* ifft_out_L = (fftw_complex*)fftw_malloc(sizeof(fftw_complex) * n); fftw_complex* ifft_out= (fftw_complex*)fftw_malloc(sizeof(fftw_complex) * n);
fftw_complex* fft_in_R = (fftw_complex*)fftw_malloc(sizeof(fftw_complex) * n); if (!fft_in || !fft_out || !ifft_in || !ifft_out) {
fftw_complex* fft_out_R = (fftw_complex*)fftw_malloc(sizeof(fftw_complex) * n); fftw_free(fft_in); fftw_free(fft_out); fftw_free(ifft_in); fftw_free(ifft_out);
fftw_complex* ifft_in_R = (fftw_complex*)fftw_malloc(sizeof(fftw_complex) * n);
fftw_complex* ifft_out_R = (fftw_complex*)fftw_malloc(sizeof(fftw_complex) * n);
if (!fft_in_L || !fft_out_L || !ifft_in_L || !ifft_out_L || !fft_in_R || !fft_out_R || !ifft_in_R || !ifft_out_R) {
throw std::runtime_error("FFTW memory allocation failed in BlockHilbert."); throw std::runtime_error("FFTW memory allocation failed in BlockHilbert.");
} }
fftw_plan plan_forward_L = fftw_plan_dft_1d(static_cast<int>(n), fft_in_L, fft_out_L, FFTW_FORWARD, FFTW_ESTIMATE); fftw_plan plan_fwd = fftw_plan_dft_1d(static_cast<int>(n), fft_in, fft_out, FFTW_FORWARD, FFTW_ESTIMATE);
fftw_plan plan_backward_L = fftw_plan_dft_1d(static_cast<int>(n), ifft_in_L, ifft_out_L, FFTW_BACKWARD, FFTW_ESTIMATE); fftw_plan plan_bwd = fftw_plan_dft_1d(static_cast<int>(n), ifft_in, ifft_out, FFTW_BACKWARD, FFTW_ESTIMATE);
fftw_plan plan_forward_R = fftw_plan_dft_1d(static_cast<int>(n), fft_in_R, fft_out_R, FFTW_FORWARD, FFTW_ESTIMATE);
fftw_plan plan_backward_R = fftw_plan_dft_1d(static_cast<int>(n), ifft_in_R, ifft_out_R, FFTW_BACKWARD, FFTW_ESTIMATE);
if (!plan_forward_L || !plan_backward_L || !plan_forward_R || !plan_backward_R) { if (!plan_fwd || !plan_bwd) {
fftw_free(fft_in_L); fftw_free(fft_out_L); fftw_free(ifft_in_L); fftw_free(ifft_out_L); fftw_free(fft_in); fftw_free(fft_out); fftw_free(ifft_in); fftw_free(ifft_out);
fftw_free(fft_in_R); fftw_free(fft_out_R); fftw_free(ifft_in_R); fftw_free(ifft_out_R);
throw std::runtime_error("FFTW plan creation failed in BlockHilbert."); throw std::runtime_error("FFTW plan creation failed in BlockHilbert.");
} }
for (size_t i = 0; i < n; ++i) { for (size_t i = 0; i < n; ++i) {
fft_in_L[i][0] = left_signal[i]; fft_in_L[i][1] = 0.0; fft_in[i][0] = signal[i]; fft_in[i][1] = 0.0;
fft_in_R[i][0] = right_signal[i]; fft_in_R[i][1] = 0.0;
} }
fftw_execute(plan_forward_L); fftw_execute(plan_fwd);
fftw_execute(plan_forward_R);
for (size_t i = 0; i < n; ++i) { for (size_t i = 0; i < n; ++i) {
double multiplier = 1.0; double multiplier = 1.0;
if (i > 0 && i < n / 2.0) { multiplier = 2.0; } if (i > 0 && i < n / 2.0) { multiplier = 2.0; }
else if (i > n / 2.0) { multiplier = 0.0; } else if (i > n / 2.0) { multiplier = 0.0; }
ifft_in[i][0] = fft_out[i][0] * multiplier;
ifft_in_L[i][0] = fft_out_L[i][0] * multiplier; ifft_in_L[i][1] = fft_out_L[i][1] * multiplier; ifft_in[i][1] = fft_out[i][1] * multiplier;
ifft_in_R[i][0] = fft_out_R[i][0] * multiplier; ifft_in_R[i][1] = fft_out_R[i][1] * multiplier;
} }
fftw_execute(plan_backward_L); fftw_execute(plan_bwd);
fftw_execute(plan_backward_R);
std::vector<std::complex<double>> analytic_L(n); std::vector<std::complex<double>> result(n);
std::vector<std::complex<double>> analytic_R(n); double inv_n = 1.0 / static_cast<double>(n);
for (size_t i = 0; i < n; ++i) { for (size_t i = 0; i < n; ++i) {
analytic_L[i].real(ifft_out_L[i][0] / static_cast<double>(n)); result[i].real(ifft_out[i][0] * inv_n);
analytic_L[i].imag(ifft_out_L[i][1] / static_cast<double>(n)); result[i].imag(ifft_out[i][1] * inv_n);
analytic_R[i].real(ifft_out_R[i][0] / static_cast<double>(n));
analytic_R[i].imag(ifft_out_R[i][1] / static_cast<double>(n));
} }
fftw_destroy_plan(plan_forward_L); fftw_destroy_plan(plan_backward_L); fftw_destroy_plan(plan_fwd);
fftw_destroy_plan(plan_forward_R); fftw_destroy_plan(plan_backward_R); fftw_destroy_plan(plan_bwd);
fftw_free(fft_in_L); fftw_free(fft_out_L); fftw_free(ifft_in_L); fftw_free(ifft_out_L); fftw_free(fft_in); fftw_free(fft_out); fftw_free(ifft_in); fftw_free(ifft_out);
fftw_free(fft_in_R); fftw_free(fft_out_R); fftw_free(ifft_in_R); fftw_free(ifft_out_R);
return {analytic_L, analytic_R}; return result;
}
std::pair<std::vector<std::complex<double>>, std::vector<std::complex<double>>>
BlockHilbert::hilbertTransform(const std::vector<double>& left_signal, const std::vector<double>& right_signal) {
if (left_signal.empty() || left_signal.size() != right_signal.size()) {
return {{}, {}};
}
// Process channels sequentially to halve peak FFTW memory
auto analytic_L = hilbertTransformSingle(left_signal);
// Left channel FFTW buffers are freed before right channel begins
auto analytic_R = hilbertTransformSingle(right_signal);
return {std::move(analytic_L), std::move(analytic_R)};
} }

View File

@ -17,6 +17,11 @@
class BlockHilbert { class BlockHilbert {
public: public:
// Single-channel transform (lower peak memory)
std::vector<std::complex<double>>
hilbertTransformSingle(const std::vector<double>& signal);
// Stereo convenience (processes channels sequentially)
std::pair<std::vector<std::complex<double>>, std::vector<std::complex<double>>> std::pair<std::vector<std::complex<double>>, std::vector<std::complex<double>>>
hilbertTransform(const std::vector<double>& left_signal, const std::vector<double>& right_signal); hilbertTransform(const std::vector<double>& left_signal, const std::vector<double>& right_signal);
}; };