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
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) {
if (totalFrames <= 0)
return;
// 1. BPM Detection
#ifdef ENABLE_TEMPO_ESTIMATION
MemoryAudioReader reader(rawFloats, totalFrames, newData->sampleRate);
MemoryAudioReader reader(rawFloats, totalFrames, sr);
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));
}
#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]);
}
// 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);
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);
for (size_t i = 0; i < totalFrames; ++i) {
newData->complexData[i * 2] = analyticPair.first[i];
newData->complexData[i * 2 + 1] = analyticPair.second[i];
}
// 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
// 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) {
QMetaObject::invokeMethod(self, "trackDataChanged",
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;
}
}
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

View File

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

View File

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

View File

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

View File

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

View File

@ -271,8 +271,7 @@ Processor::Spectrum Processor::getSpectrum() {
freqsFull[i] = freq;
}
// --- Cepstral Smoothing / Trig Interpolation ---
if (m_cepstralStrength > 0.0f) {
// --- 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
@ -285,16 +284,25 @@ Processor::Spectrum Processor::getSpectrum() {
// 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;
// 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) {
// 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};
}

View File

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

View File

@ -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);
// 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();
}
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
@ -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) {

View File

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

View File

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

View File

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