diff --git a/src/AudioEngine.cpp b/src/AudioEngine.cpp index 33d4be0..3eeccf5 100644 --- a/src/AudioEngine.cpp +++ b/src/AudioEngine.cpp @@ -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 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(newData->pcmData.constData()); - long long totalFloats = newData->pcmData.size() / sizeof(float); + reinterpret_cast(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(*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(*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 inputL(totalFrames), inputR(totalFrames); - for (size_t i = 0; i < totalFrames; ++i) { - inputL[i] = static_cast(rawFloats[i * 2]); - inputR[i] = static_cast(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(); + 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 input(totalFrames); - // Notify Analyzer that complex data is ready - if (self) { - QMetaObject::invokeMethod(self, "trackDataChanged", - Qt::QueuedConnection, - Q_ARG(std::shared_ptr, newData)); - } + // Left channel: build input, transform, copy to complexData, free result + for (size_t i = 0; i < static_cast(totalFrames); ++i) + input[i] = static_cast(rawFloats[i * 2]); + { + auto analytic = blockHilbert.hilbertTransformSingle(input); + for (size_t i = 0; i < static_cast(totalFrames); ++i) + finalData->complexData[i * 2] = analytic[i]; + } // analytic freed + + // Right channel: reuse input buffer + for (size_t i = 0; i < static_cast(totalFrames); ++i) + input[i] = static_cast(rawFloats[i * 2 + 1]); + { + auto analytic = blockHilbert.hilbertTransformSingle(input); + for (size_t i = 0; i < static_cast(totalFrames); ++i) + finalData->complexData[i * 2 + 1] = analytic[i]; + } // analytic freed + + // Free input buffer + { std::vector().swap(input); } + + // Notify Analyzer with the complete data + if (self) { + QMetaObject::invokeMethod(self, "trackDataChanged", + Qt::QueuedConnection, + Q_ARG(std::shared_ptr, 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 diff --git a/src/AudioEngine.h b/src/AudioEngine.h index c44f0c2..8f0a602 100644 --- a/src/AudioEngine.h +++ b/src/AudioEngine.h @@ -211,6 +211,7 @@ public: std::vector freqs; std::vector db; std::vector primaryDb; + std::vector cepstrum; // from main processor ch0 only }; // Thread-safe pull for UI diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 2e47f6f..afbc90d 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -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()); } diff --git a/src/PlayerControls.cpp b/src/PlayerControls.cpp index cedf3ae..4149735 100644 --- a/src/PlayerControls.cpp +++ b/src/PlayerControls.cpp @@ -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); } diff --git a/src/PlayerControls.h b/src/PlayerControls.h index 64589c6..e74890c 100644 --- a/src/PlayerControls.h +++ b/src/PlayerControls.h @@ -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; diff --git a/src/Processor.cpp b/src/Processor.cpp index f853f6a..b2b70ae 100644 --- a/src/Processor.cpp +++ b/src/Processor.cpp @@ -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 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 cepCoeffs(halfN); + double cepScale = 1.0 / m_frameSize; + for(int i=0; i(m_cep_out[i][0] * cepScale); + } + // --- Cepstral Smoothing / Trig Interpolation --- if (m_cepstralStrength > 0.0f) { - // 1. Log Magnitude - for(int i=0; i 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; - for(int i=1; i envelope = idealizeCurve(magFull); - + // Apply Strength (Mix) for(size_t i=0; i freqsRet(m_freqsConst.size()); for(size_t i=0; i(m_freqsConst[i]); - return {freqsRet, averagedDb}; + return {freqsRet, averagedDb, cepCoeffs}; } \ No newline at end of file diff --git a/src/Processor.h b/src/Processor.h index 3d1e454..120b019 100644 --- a/src/Processor.h +++ b/src/Processor.h @@ -29,6 +29,7 @@ public: struct Spectrum { std::vector freqs; std::vector db; + std::vector cepstrum; // raw cepstral coefficients (positive quefrencies) }; Spectrum getSpectrum(); diff --git a/src/VisualizerWidget.cpp b/src/VisualizerWidget.cpp index 70d2dba..101d308 100644 --- a/src/VisualizerWidget.cpp +++ b/src/VisualizerWidget.cpp @@ -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(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(m_vertices.size() * sizeof(float)); + int cepSize = static_cast(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) { diff --git a/src/VisualizerWidget.h b/src/VisualizerWidget.h index 369b476..86d9e69 100644 --- a/src/VisualizerWidget.h +++ b/src/VisualizerWidget.h @@ -15,7 +15,7 @@ class VisualizerWidget : public QRhiWidget { public: VisualizerWidget(QWidget* parent = nullptr); void updateData(const std::vector& 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& 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 m_smoothedCepstrum; + std::vector m_cepstrumVerts; + int m_cepstrumVertexCount = 0; + void buildCepstrumVertices(int w, int h); + float getX(float freq); QColor applyModifiers(QColor c); }; diff --git a/src/complex_block.cpp b/src/complex_block.cpp index 3666cd0..8c7249b 100644 --- a/src/complex_block.cpp +++ b/src/complex_block.cpp @@ -32,71 +32,69 @@ #include #include -std::pair>, std::vector>> -BlockHilbert::hilbertTransform(const std::vector& left_signal, const std::vector& right_signal) { - size_t n = left_signal.size(); - if (n == 0 || n != right_signal.size()) { - return {{}, {}}; - } +std::vector> +BlockHilbert::hilbertTransformSingle(const std::vector& 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(n), fft_in_L, fft_out_L, FFTW_FORWARD, FFTW_ESTIMATE); - fftw_plan plan_backward_L = fftw_plan_dft_1d(static_cast(n), ifft_in_L, ifft_out_L, FFTW_BACKWARD, FFTW_ESTIMATE); - fftw_plan plan_forward_R = fftw_plan_dft_1d(static_cast(n), fft_in_R, fft_out_R, FFTW_FORWARD, FFTW_ESTIMATE); - fftw_plan plan_backward_R = fftw_plan_dft_1d(static_cast(n), ifft_in_R, ifft_out_R, FFTW_BACKWARD, FFTW_ESTIMATE); + fftw_plan plan_fwd = fftw_plan_dft_1d(static_cast(n), fft_in, fft_out, FFTW_FORWARD, FFTW_ESTIMATE); + fftw_plan plan_bwd = fftw_plan_dft_1d(static_cast(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; } + 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> analytic_L(n); - std::vector> analytic_R(n); + std::vector> result(n); + double inv_n = 1.0 / static_cast(n); for (size_t i = 0; i < n; ++i) { - analytic_L[i].real(ifft_out_L[i][0] / static_cast(n)); - analytic_L[i].imag(ifft_out_L[i][1] / static_cast(n)); - analytic_R[i].real(ifft_out_R[i][0] / static_cast(n)); - analytic_R[i].imag(ifft_out_R[i][1] / static_cast(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>> +BlockHilbert::hilbertTransform(const std::vector& left_signal, const std::vector& 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)}; } \ No newline at end of file diff --git a/src/complex_block.h b/src/complex_block.h index 8745f56..e61d288 100644 --- a/src/complex_block.h +++ b/src/complex_block.h @@ -17,7 +17,12 @@ class BlockHilbert { public: - std::pair>, std::vector>> + // Single-channel transform (lower peak memory) + std::vector> + hilbertTransformSingle(const std::vector& signal); + + // Stereo convenience (processes channels sequentially) + std::pair>, std::vector>> hilbertTransform(const std::vector& left_signal, const std::vector& right_signal); };