Okay, starting to looks seriosly good, almost works on all platforms too now! lol
This commit is contained in:
parent
30cecf586c
commit
b6ef417242
|
|
@ -279,59 +279,77 @@ void AudioEngine::onFinished() {
|
|||
// Notify UI that track is ready to play
|
||||
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);
|
||||
|
||||
// OPTIMIZATION: Run heavy analysis in background to avoid blocking audio
|
||||
// thread FIX: Use QPointer to prevent crash if AudioEngine is deleted
|
||||
// before task runs
|
||||
// Run heavy analysis in background thread pool
|
||||
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)
|
||||
return;
|
||||
|
||||
const float *rawFloats =
|
||||
reinterpret_cast<const float *>(newData->pcmData.constData());
|
||||
long long totalFloats = newData->pcmData.size() / sizeof(float);
|
||||
reinterpret_cast<const float *>(pcmSnap.constData());
|
||||
long long totalFloats = pcmSnap.size() / sizeof(float);
|
||||
long long totalFrames = totalFloats / 2;
|
||||
|
||||
if (totalFrames > 0) {
|
||||
// 1. BPM Detection
|
||||
if (totalFrames <= 0)
|
||||
return;
|
||||
|
||||
// 1. BPM Detection
|
||||
#ifdef ENABLE_TEMPO_ESTIMATION
|
||||
MemoryAudioReader reader(rawFloats, totalFrames, newData->sampleRate);
|
||||
auto bpmOpt =
|
||||
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;
|
||||
|
||||
if (self) {
|
||||
QMetaObject::invokeMethod(self, "analysisReady", Qt::QueuedConnection,
|
||||
Q_ARG(float, bpm), Q_ARG(float, 1.0f));
|
||||
}
|
||||
MemoryAudioReader reader(rawFloats, totalFrames, sr);
|
||||
auto bpmOpt =
|
||||
LTE::GetBpm(reader, LTE::FalsePositiveTolerance::Lenient, nullptr);
|
||||
float bpm = bpmOpt.has_value() ? static_cast<float>(*bpmOpt) : 0.0f;
|
||||
if (self) {
|
||||
QMetaObject::invokeMethod(self, "analysisReady", Qt::QueuedConnection,
|
||||
Q_ARG(float, bpm), Q_ARG(float, 1.0f));
|
||||
}
|
||||
#endif
|
||||
|
||||
// 2. Hilbert Transform
|
||||
std::vector<double> inputL(totalFrames), 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);
|
||||
// 2. Hilbert Transform — process one channel at a time to minimize
|
||||
// peak memory. FFTW uses 4 buffers per channel instead of 8.
|
||||
auto finalData = std::make_shared<TrackData>();
|
||||
finalData->pcmData = pcmSnap;
|
||||
finalData->sampleRate = sr;
|
||||
finalData->valid = true;
|
||||
finalData->complexData.resize(totalFloats);
|
||||
|
||||
newData->complexData.resize(totalFloats);
|
||||
for (size_t i = 0; i < totalFrames; ++i) {
|
||||
newData->complexData[i * 2] = analyticPair.first[i];
|
||||
newData->complexData[i * 2 + 1] = analyticPair.second[i];
|
||||
}
|
||||
BlockHilbert blockHilbert;
|
||||
// Reusable input buffer (one channel at a time)
|
||||
std::vector<double> input(totalFrames);
|
||||
|
||||
// Notify Analyzer that complex data is ready
|
||||
if (self) {
|
||||
QMetaObject::invokeMethod(self, "trackDataChanged",
|
||||
Qt::QueuedConnection,
|
||||
Q_ARG(std::shared_ptr<TrackData>, newData));
|
||||
}
|
||||
// Left channel: build input, transform, copy to complexData, free result
|
||||
for (size_t i = 0; i < static_cast<size_t>(totalFrames); ++i)
|
||||
input[i] = static_cast<double>(rawFloats[i * 2]);
|
||||
{
|
||||
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
|
||||
|
||||
// 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) {
|
||||
QMetaObject::invokeMethod(self, "trackDataChanged",
|
||||
Qt::QueuedConnection,
|
||||
Q_ARG(std::shared_ptr<TrackData>, finalData));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -591,7 +609,11 @@ void AudioAnalyzer::processLoop() {
|
|||
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
|
||||
|
|
|
|||
|
|
@ -211,6 +211,7 @@ public:
|
|||
std::vector<float> freqs;
|
||||
std::vector<float> db;
|
||||
std::vector<float> primaryDb;
|
||||
std::vector<float> cepstrum; // from main processor ch0 only
|
||||
};
|
||||
|
||||
// Thread-safe pull for UI
|
||||
|
|
|
|||
|
|
@ -92,8 +92,8 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) {
|
|||
|
||||
// Settings -> Analyzer
|
||||
connect(m_playerPage->settings(), &SettingsWidget::paramsChanged, this,
|
||||
[this](bool, bool, bool, bool, float, float, float, int granularity,
|
||||
int detail, float strength) {
|
||||
[this](bool, bool, bool, bool, bool, float, float, float,
|
||||
int granularity, int detail, float strength) {
|
||||
QMetaObject::invokeMethod(
|
||||
m_analyzer, "setSmoothingParams", Qt::QueuedConnection,
|
||||
Q_ARG(int, granularity), Q_ARG(int, detail),
|
||||
|
|
@ -411,10 +411,10 @@ void MainWindow::loadSettings() {
|
|||
m_playerPage->settings()->setParams(
|
||||
root["glass"].toBool(true), root["focus"].toBool(false),
|
||||
root["albumColors"].toBool(false), root["mirrored"].toBool(false),
|
||||
root["bins"].toInt(26), root["fps"].toInt(60),
|
||||
root["brightness"].toDouble(1.0), root["granularity"].toInt(33),
|
||||
root["detail"].toInt(50), root["strength"].toDouble(0.0),
|
||||
root["bpmScaleIndex"].toInt(2));
|
||||
root["inverted"].toBool(false), root["bins"].toInt(26),
|
||||
root["fps"].toInt(60), root["brightness"].toDouble(1.0),
|
||||
root["granularity"].toInt(33), root["detail"].toInt(50),
|
||||
root["strength"].toDouble(0.0), root["bpmScaleIndex"].toInt(2));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -427,6 +427,7 @@ void MainWindow::saveSettings() {
|
|||
root["focus"] = s->isFocus();
|
||||
root["albumColors"] = s->isAlbumColors();
|
||||
root["mirrored"] = s->isMirrored();
|
||||
root["inverted"] = s->isInverted();
|
||||
root["bins"] = s->getBins();
|
||||
root["fps"] = s->getFps();
|
||||
root["brightness"] = s->getBrightness();
|
||||
|
|
@ -489,7 +490,7 @@ void MainWindow::updateSmoothing() {
|
|||
float targetStrength = 0.8f * (1.0f - normalized);
|
||||
SettingsWidget *s = m_playerPage->settings();
|
||||
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->getBpmScaleIndex());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -136,6 +136,7 @@ SettingsWidget::SettingsWidget(QWidget *parent) : QWidget(parent) {
|
|||
m_checkFocus = createCheck("Focus", true, 0, 1);
|
||||
m_checkAlbumColors = createCheck("Album Colors", false, 1, 0);
|
||||
m_checkMirrored = createCheck("Mirrored", true, 1, 1);
|
||||
m_checkInverted = createCheck("Invert", false, 2, 0);
|
||||
layout->addLayout(grid);
|
||||
|
||||
// Helper for sliders
|
||||
|
|
@ -237,7 +238,7 @@ SettingsWidget::SettingsWidget(QWidget *parent) : QWidget(parent) {
|
|||
}
|
||||
|
||||
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 strength, int bpmScaleIndex) {
|
||||
bool oldState = blockSignals(true);
|
||||
|
|
@ -245,6 +246,7 @@ void SettingsWidget::setParams(bool glass, bool focus, bool albumColors,
|
|||
m_checkFocus->setChecked(focus);
|
||||
m_checkAlbumColors->setChecked(albumColors);
|
||||
m_checkMirrored->setChecked(mirrored);
|
||||
m_checkInverted->setChecked(inverted);
|
||||
m_sliderBins->setValue(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() {
|
||||
emit paramsChanged(m_checkGlass->isChecked(), m_checkFocus->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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ public:
|
|||
bool isFocus() const { return m_checkFocus->isChecked(); }
|
||||
bool isAlbumColors() const { return m_checkAlbumColors->isChecked(); }
|
||||
bool isMirrored() const { return m_checkMirrored->isChecked(); }
|
||||
bool isInverted() const { return m_checkInverted->isChecked(); }
|
||||
int getBins() const { return m_sliderBins->value(); }
|
||||
int getFps() const { return m_sliderFps->value(); }
|
||||
float getBrightness() const { return m_brightness; }
|
||||
|
|
@ -58,13 +59,15 @@ public:
|
|||
int getBpmScaleIndex() const;
|
||||
|
||||
void setParams(bool glass, bool focus, bool albumColors, bool mirrored,
|
||||
int bins, int fps, float brightness, int granularity,
|
||||
int detail, float strength, int bpmScaleIndex);
|
||||
bool inverted, int bins, int fps, float brightness,
|
||||
int granularity, int detail, float strength,
|
||||
int bpmScaleIndex);
|
||||
|
||||
signals:
|
||||
void paramsChanged(bool glass, bool focus, bool albumColors, bool mirrored,
|
||||
float hue, float contrast, float brightness,
|
||||
int granularity, int detail, float strength);
|
||||
bool inverted, float hue, float contrast,
|
||||
float brightness, int granularity, int detail,
|
||||
float strength);
|
||||
void fpsChanged(int fps);
|
||||
void dspParamsChanged(int fft, int hop);
|
||||
void binsChanged(int n);
|
||||
|
|
@ -85,6 +88,7 @@ private:
|
|||
QCheckBox *m_checkFocus;
|
||||
QCheckBox *m_checkAlbumColors;
|
||||
QCheckBox *m_checkMirrored;
|
||||
QCheckBox *m_checkInverted;
|
||||
XYPad *m_padDsp;
|
||||
XYPad *m_padColor;
|
||||
QSlider *m_sliderBins;
|
||||
|
|
|
|||
|
|
@ -271,30 +271,38 @@ Processor::Spectrum Processor::getSpectrum() {
|
|||
freqsFull[i] = freq;
|
||||
}
|
||||
|
||||
// --- Cepstral IFFT (always compute for cepstrum visualization) ---
|
||||
// 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. Extract positive quefrencies for visualization
|
||||
int halfN = m_frameSize / 2;
|
||||
std::vector<float> cepCoeffs(halfN);
|
||||
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) {
|
||||
// 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;
|
||||
// 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) {
|
||||
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;
|
||||
m_cep_in[i][0] = m_cep_out[i][0] * cepScale * 2.0;
|
||||
m_cep_in[i][1] = m_cep_out[i][1] * cepScale * 2.0;
|
||||
} else {
|
||||
// Negative quefrencies = 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);
|
||||
|
||||
// Apply Strength (Mix)
|
||||
|
|
@ -351,5 +359,5 @@ Processor::Spectrum Processor::getSpectrum() {
|
|||
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};
|
||||
return {freqsRet, averagedDb, cepCoeffs};
|
||||
}
|
||||
|
|
@ -29,6 +29,7 @@ public:
|
|||
struct Spectrum {
|
||||
std::vector<float> freqs;
|
||||
std::vector<float> db;
|
||||
std::vector<float> cepstrum; // raw cepstral coefficients (positive quefrencies)
|
||||
};
|
||||
|
||||
Spectrum getSpectrum();
|
||||
|
|
|
|||
|
|
@ -45,12 +45,13 @@ void VisualizerWidget::setTargetFps(int fps) {
|
|||
}
|
||||
|
||||
void VisualizerWidget::setParams(bool glass, bool focus, bool albumColors,
|
||||
bool mirrored, float hue, float contrast,
|
||||
float brightness) {
|
||||
bool mirrored, bool inverted, float hue,
|
||||
float contrast, float brightness) {
|
||||
m_glass = glass;
|
||||
m_focus = focus;
|
||||
m_useAlbumColors = albumColors;
|
||||
m_mirrored = mirrored;
|
||||
m_inverted = inverted;
|
||||
m_hueFactor = hue;
|
||||
m_contrast = contrast;
|
||||
m_brightness = brightness;
|
||||
|
|
@ -287,6 +288,16 @@ void VisualizerWidget::updateData(
|
|||
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();
|
||||
}
|
||||
|
||||
|
|
@ -307,10 +318,10 @@ void VisualizerWidget::initialize(QRhiCommandBuffer *cb) {
|
|||
2048 * 6 * sizeof(float)));
|
||||
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,
|
||||
QRhiBuffer::UniformBuffer,
|
||||
m_ubufAlign * 4));
|
||||
m_ubufAlign * 5));
|
||||
m_ubuf->create();
|
||||
|
||||
// Shader resource bindings with dynamic UBO offset
|
||||
|
|
@ -376,10 +387,13 @@ void VisualizerWidget::render(QRhiCommandBuffer *cb) {
|
|||
// Only rebuild vertices when new data has arrived
|
||||
if (m_dataDirty) {
|
||||
m_dataDirty = false;
|
||||
if (m_mirrored)
|
||||
buildVertices(w / 2, h / 2);
|
||||
else
|
||||
if (m_mirrored) {
|
||||
buildVertices(w * 0.55f, h / 2);
|
||||
buildCepstrumVertices(w, h);
|
||||
} else {
|
||||
buildVertices(w, h);
|
||||
m_cepstrumVertexCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
int numPasses = m_mirrored ? 4 : 1;
|
||||
|
|
@ -387,14 +401,21 @@ void VisualizerWidget::render(QRhiCommandBuffer *cb) {
|
|||
// Prepare resource updates
|
||||
QRhiResourceUpdateBatch *u = m_rhi->nextResourceUpdateBatch();
|
||||
|
||||
// Upload vertex data
|
||||
if (!m_vertices.empty()) {
|
||||
int dataSize = static_cast<int>(m_vertices.size() * sizeof(float));
|
||||
if (dataSize > m_vbuf->size()) {
|
||||
m_vbuf->setSize(dataSize);
|
||||
m_vbuf->create();
|
||||
// Upload vertex data (main + cepstrum appended)
|
||||
{
|
||||
int mainSize = static_cast<int>(m_vertices.size() * sizeof(float));
|
||||
int cepSize = static_cast<int>(m_cepstrumVerts.size() * sizeof(float));
|
||||
int totalSize = mainSize + cepSize;
|
||||
if (totalSize > 0) {
|
||||
if (totalSize > m_vbuf->size()) {
|
||||
m_vbuf->setSize(totalSize);
|
||||
m_vbuf->create();
|
||||
}
|
||||
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());
|
||||
}
|
||||
u->updateDynamicBuffer(m_vbuf.get(), 0, dataSize, m_vertices.data());
|
||||
}
|
||||
|
||||
// Upload MVP matrices
|
||||
|
|
@ -427,6 +448,15 @@ void VisualizerWidget::render(QRhiCommandBuffer *cb) {
|
|||
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
|
||||
cb->beginPass(renderTarget(), QColor(0, 0, 0, 255), {1.0f, 0}, u);
|
||||
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();
|
||||
update();
|
||||
}
|
||||
|
|
@ -464,6 +503,76 @@ void VisualizerWidget::releaseResources() {
|
|||
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) =====
|
||||
|
||||
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;
|
||||
|
||||
for (size_t i = 0; i + 1 < freqs.size(); ++i) {
|
||||
if (i + 1 >= bins.size())
|
||||
break;
|
||||
const auto &b = bins[i];
|
||||
const auto &bNext = bins[i + 1];
|
||||
size_t numBins = std::min(freqs.size(), bins.size());
|
||||
for (size_t i = 0; i + 1 < numBins; ++i) {
|
||||
// When inverted, read bin data in reverse order (highs left, lows right)
|
||||
size_t di = m_inverted ? (numBins - 1 - i) : i;
|
||||
size_t diN = m_inverted ? (numBins - 2 - i) : (i + 1);
|
||||
const auto &b = bins[di];
|
||||
const auto &bNext = bins[diN];
|
||||
|
||||
// --- Brightness ---
|
||||
float avgEnergy =
|
||||
|
|
@ -527,8 +638,19 @@ void VisualizerWidget::buildVertices(int w, int h) {
|
|||
float alpha = 0.4f + (avgEnergy - 0.5f) * m_contrast;
|
||||
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);
|
||||
lineColor.setAlphaF(0.9f);
|
||||
lineColor.setAlphaF(std::min(0.9f, alpha));
|
||||
|
||||
// --- Channel 1 tint ---
|
||||
if (ch == 1 && m_data.size() > 1) {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ class VisualizerWidget : public QRhiWidget {
|
|||
public:
|
||||
VisualizerWidget(QWidget* parent = nullptr);
|
||||
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 setNumBins(int n);
|
||||
void setTargetFps(int fps);
|
||||
|
|
@ -60,6 +60,7 @@ private:
|
|||
bool m_focus = false;
|
||||
bool m_useAlbumColors = false;
|
||||
bool m_mirrored = false;
|
||||
bool m_inverted = false;
|
||||
float m_hueFactor = 0.9f;
|
||||
float m_contrast = 1.0f;
|
||||
float m_brightness = 1.0f;
|
||||
|
|
@ -82,6 +83,12 @@ private:
|
|||
int m_fillVertexCount = 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);
|
||||
QColor applyModifiers(QColor c);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -32,71 +32,69 @@
|
|||
#include <stdexcept>
|
||||
#include <cmath>
|
||||
|
||||
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) {
|
||||
size_t n = left_signal.size();
|
||||
if (n == 0 || n != right_signal.size()) {
|
||||
return {{}, {}};
|
||||
}
|
||||
std::vector<std::complex<double>>
|
||||
BlockHilbert::hilbertTransformSingle(const std::vector<double>& signal) {
|
||||
size_t n = signal.size();
|
||||
if (n == 0) return {};
|
||||
|
||||
fftw_complex* fft_in_L = (fftw_complex*)fftw_malloc(sizeof(fftw_complex) * n);
|
||||
fftw_complex* fft_out_L = (fftw_complex*)fftw_malloc(sizeof(fftw_complex) * n);
|
||||
fftw_complex* ifft_in_L = (fftw_complex*)fftw_malloc(sizeof(fftw_complex) * n);
|
||||
fftw_complex* ifft_out_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 = (fftw_complex*)fftw_malloc(sizeof(fftw_complex) * n);
|
||||
fftw_complex* ifft_in = (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);
|
||||
fftw_complex* fft_out_R = (fftw_complex*)fftw_malloc(sizeof(fftw_complex) * n);
|
||||
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) {
|
||||
if (!fft_in || !fft_out || !ifft_in || !ifft_out) {
|
||||
fftw_free(fft_in); fftw_free(fft_out); fftw_free(ifft_in); fftw_free(ifft_out);
|
||||
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_backward_L = fftw_plan_dft_1d(static_cast<int>(n), ifft_in_L, ifft_out_L, 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);
|
||||
fftw_plan plan_fwd = fftw_plan_dft_1d(static_cast<int>(n), fft_in, fft_out, FFTW_FORWARD, FFTW_ESTIMATE);
|
||||
fftw_plan plan_bwd = fftw_plan_dft_1d(static_cast<int>(n), ifft_in, ifft_out, FFTW_BACKWARD, FFTW_ESTIMATE);
|
||||
|
||||
if (!plan_forward_L || !plan_backward_L || !plan_forward_R || !plan_backward_R) {
|
||||
fftw_free(fft_in_L); fftw_free(fft_out_L); fftw_free(ifft_in_L); fftw_free(ifft_out_L);
|
||||
fftw_free(fft_in_R); fftw_free(fft_out_R); fftw_free(ifft_in_R); fftw_free(ifft_out_R);
|
||||
if (!plan_fwd || !plan_bwd) {
|
||||
fftw_free(fft_in); fftw_free(fft_out); fftw_free(ifft_in); fftw_free(ifft_out);
|
||||
throw std::runtime_error("FFTW plan creation failed in BlockHilbert.");
|
||||
}
|
||||
|
||||
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_R[i][0] = right_signal[i]; fft_in_R[i][1] = 0.0;
|
||||
fft_in[i][0] = signal[i]; fft_in[i][1] = 0.0;
|
||||
}
|
||||
|
||||
fftw_execute(plan_forward_L);
|
||||
fftw_execute(plan_forward_R);
|
||||
fftw_execute(plan_fwd);
|
||||
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
double multiplier = 1.0;
|
||||
if (i > 0 && i < n / 2.0) { multiplier = 2.0; }
|
||||
else if (i > n / 2.0) { multiplier = 0.0; }
|
||||
|
||||
ifft_in_L[i][0] = fft_out_L[i][0] * multiplier; ifft_in_L[i][1] = fft_out_L[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;
|
||||
ifft_in[i][0] = fft_out[i][0] * multiplier;
|
||||
ifft_in[i][1] = fft_out[i][1] * multiplier;
|
||||
}
|
||||
|
||||
fftw_execute(plan_backward_L);
|
||||
fftw_execute(plan_backward_R);
|
||||
fftw_execute(plan_bwd);
|
||||
|
||||
std::vector<std::complex<double>> analytic_L(n);
|
||||
std::vector<std::complex<double>> analytic_R(n);
|
||||
std::vector<std::complex<double>> result(n);
|
||||
double inv_n = 1.0 / static_cast<double>(n);
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
analytic_L[i].real(ifft_out_L[i][0] / static_cast<double>(n));
|
||||
analytic_L[i].imag(ifft_out_L[i][1] / static_cast<double>(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));
|
||||
result[i].real(ifft_out[i][0] * inv_n);
|
||||
result[i].imag(ifft_out[i][1] * inv_n);
|
||||
}
|
||||
|
||||
fftw_destroy_plan(plan_forward_L); fftw_destroy_plan(plan_backward_L);
|
||||
fftw_destroy_plan(plan_forward_R); fftw_destroy_plan(plan_backward_R);
|
||||
fftw_free(fft_in_L); fftw_free(fft_out_L); fftw_free(ifft_in_L); fftw_free(ifft_out_L);
|
||||
fftw_free(fft_in_R); fftw_free(fft_out_R); fftw_free(ifft_in_R); fftw_free(ifft_out_R);
|
||||
fftw_destroy_plan(plan_fwd);
|
||||
fftw_destroy_plan(plan_bwd);
|
||||
fftw_free(fft_in); fftw_free(fft_out); fftw_free(ifft_in); fftw_free(ifft_out);
|
||||
|
||||
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)};
|
||||
}
|
||||
|
|
@ -17,6 +17,11 @@
|
|||
|
||||
class BlockHilbert {
|
||||
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>>>
|
||||
hilbertTransform(const std::vector<double>& left_signal, const std::vector<double>& right_signal);
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue